codexdistillation-ideal-enrichment-and-material-balance.py
distillation-ideal-enrichment-and-material-balance.py
"""Original stipulated equilibrium and product-balance diagram. Python 3.14/matplotlib 3.10."""
from pathlib import Path
from fractions import Fraction
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
from matplotlib.patches import FancyArrowPatch, FancyBboxPatch
import numpy as np
def main():
fig = plt.figure(figsize=(11.5, 6.5), dpi=140, facecolor='white')
fig.text(.045, .95, 'Enrichment and recovery answer different questions', fontsize=19, weight='bold')
fig.text(.045, .905, 'Stipulated models only • no named liquid • no measured data or temperature curve', fontsize=11, color='#555555')
ax = fig.add_axes([.075, .23, .39, .60])
u = np.linspace(0, 1, 500)
ax.plot(u, 2*u/(1+u), color='#216c9a', lw=2.5, label=r'$y=2x/(1+x)$, $\alpha=2$')
ax.plot(u, u, color='#929da5', lw=1.2, ls='--', label=r'$y=x$')
stages = [Fraction(3, 10)]
for _ in range(3):
stages.append(2*stages[-1]/(1+stages[-1]))
for i, (a, b) in enumerate(zip(stages, stages[1:])):
x, y = float(a), float(b)
ax.plot([x, x, y], [x, y, y], color='#c27025', lw=1.7)
ax.scatter([x], [y], color='#c27025', zorder=5, s=34)
ax.annotate(f'{i+1}', (x, y), xytext=(-14, 6), textcoords='offset points', fontsize=10, color='#9b4e16')
ax.scatter([.3], [.3], color='#c27025', s=26, zorder=5)
ax.set(xlim=(0, 1), ylim=(0, 1), xlabel=r'Liquid mole fraction $x$', ylabel=r'Equilibrium vapor mole fraction $y$')
ax.set_title('A. Formal repeated equilibration', loc='left', fontsize=13, pad=14)
ax.set_aspect('equal')
ax.legend(loc='upper left', fontsize=10, frameon=False)
ax.grid(alpha=.15)
ax.spines[['top', 'right']].set_visible(False)
fig.text(.067, .13, '0.300000 → 0.461538 → 0.631579 → 0.774194', fontsize=11, color='#9b4e16')
fig.text(.067, .055, 'Condense completely between formal steps.\nNot a reflux design or a single pot’s time history.', fontsize=10.5, linespacing=1.65)
bx = fig.add_axes([.52, .22, .46, .61])
bx.set(xlim=(-.2, 10), ylim=(0, 10))
bx.axis('off')
bx.text(0, 10, 'B. Independent overall material balance', fontsize=13, va='bottom')
def box(x, y, w, h, text, fill):
bx.add_patch(FancyBboxPatch((x, y), w, h, boxstyle='round,pad=0.14',
facecolor=fill, edgecolor='#627d8e', lw=1.4))
bx.text(x+w/2, y+h/2, text, ha='center', va='center', fontsize=11, linespacing=1.6)
box(.1, 4.5, 2.4, 2.1, 'Feed\nF = 1 mol\nz = 0.35', '#eef3f7')
box(3.3, 4.5, 2.2, 2.1, 'Separation\noverall\nboundary', '#f7f5ed')
box(6.7, 6.6, 3.0, 2.2, 'Distillate\nD = 5/14 mol\nxᴅ = 0.80', '#e5f1f7')
box(6.7, 2.0, 3.0, 2.2, 'Bottoms\nB = 9/14 mol\nxʙ = 0.10', '#f7eadb')
for start, end in [((2.65, 5.55), (3.1, 5.55)), ((5.65, 5.8), (6.5, 7.5)), ((5.65, 5.2), (6.5, 3.1))]:
bx.add_patch(FancyArrowPatch(start, end, arrowstyle='-|>', mutation_scale=16, lw=1.5, color='#496b7d'))
bx.text(0.15, .4, r'$D+B=1$ mol' '\n' r'$0.80D+0.10B=0.35$ mol', fontsize=12, linespacing=1.7)
fig.text(.535, .14, 'Component A recovered in distillate: 40/49 ≈ 81.63%', fontsize=10.5, color='#216c9a')
fig.text(.535, .045, 'Balance only: does not determine heat,\ntemperatures, stage count, or feasibility.', fontsize=10.5, linespacing=1.65)
fig.savefig(Path(__file__).stem + '.png', facecolor='white', dpi=140)
plt.close(fig)
if __name__ == '__main__':
main()