Skip to content

Commit 5fd2129

Browse files
committed
Add BTX flash, valve characteristics, and process flowsheet examples
1 parent 069f2e5 commit 5fd2129

File tree

3 files changed

+697
-0
lines changed

3 files changed

+697
-0
lines changed
Lines changed: 286 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,286 @@
1+
{
2+
"cells": [
3+
{
4+
"cell_type": "markdown",
5+
"metadata": {},
6+
"source": [
7+
"# Multi-Component Flash Separation (BTX)\n",
8+
"\n",
9+
"Simulating an isothermal flash drum for a ternary **benzene–toluene–p-xylene** (BTX) mixture.\n",
10+
"The `MultiComponentFlash` block uses Raoult's law with Antoine correlations to compute K-values\n",
11+
"and solves the Rachford-Rice equation via Brent's method.\n",
12+
"\n",
13+
"This example is inspired by [MiniSim's SimpleFlash example](https://github.com/Nukleon84/MiniSim/blob/master/doc/SimpleFlash.ipynb),\n",
14+
"adapted to PathSim's dynamic simulation framework.\n",
15+
"\n",
16+
"**Feed conditions:**\n",
17+
"- 10 mol/s total flow\n",
18+
"- 50% benzene, 10% toluene, 40% p-xylene (molar)\n",
19+
"- 1 atm pressure\n",
20+
"- Temperature sweep: 340 K → 420 K"
21+
]
22+
},
23+
{
24+
"cell_type": "code",
25+
"execution_count": null,
26+
"metadata": {},
27+
"outputs": [],
28+
"source": [
29+
"import numpy as np\n",
30+
"import matplotlib.pyplot as plt\n",
31+
"\n",
32+
"from pathsim import Simulation, Connection\n",
33+
"from pathsim.blocks import Source, Scope\n",
34+
"\n",
35+
"from pathsim_chem.process import MultiComponentFlash"
36+
]
37+
},
38+
{
39+
"cell_type": "markdown",
40+
"metadata": {},
41+
"source": [
42+
"## Flash Drum Setup\n",
43+
"\n",
44+
"The `MultiComponentFlash` block defaults to BTX Antoine parameters (ln form, Pa, K):\n",
45+
"\n",
46+
"| Component | A | B | C |\n",
47+
"|-----------|-------|---------|--------|\n",
48+
"| Benzene | 20.7936 | 2788.51 | -52.36 |\n",
49+
"| Toluene | 20.9064 | 3096.52 | -53.67 |\n",
50+
"| p-Xylene | 20.9891 | 3346.65 | -57.84 |\n",
51+
"\n",
52+
"We feed the drum with constant composition and pressure while ramping temperature\n",
53+
"to observe the transition from all-liquid through two-phase to all-vapor."
54+
]
55+
},
56+
{
57+
"cell_type": "code",
58+
"execution_count": null,
59+
"metadata": {},
60+
"outputs": [],
61+
"source": [
62+
"# Create the flash drum (3 components, BTX defaults)\n",
63+
"flash = MultiComponentFlash(N_comp=3, holdup=100.0)\n",
64+
"\n",
65+
"# Feed sources\n",
66+
"F_feed = Source(func=lambda t: 10.0) # 10 mol/s\n",
67+
"z_benzene = Source(func=lambda t: 0.5) # 50% benzene\n",
68+
"z_toluene = Source(func=lambda t: 0.1) # 10% toluene (p-xylene = 40% inferred)\n",
69+
"T_sweep = Source(func=lambda t: 340.0 + t) # ramp 340 -> 420 K\n",
70+
"P_feed = Source(func=lambda t: 101325.0) # 1 atm\n",
71+
"\n",
72+
"# Record all outputs: V_rate, L_rate, y_benzene, y_toluene, x_benzene, x_toluene\n",
73+
"scp = Scope(labels=[\"V_rate\", \"L_rate\", \"y_benz\", \"y_tol\", \"x_benz\", \"x_tol\"])\n",
74+
"\n",
75+
"sim = Simulation(\n",
76+
" blocks=[F_feed, z_benzene, z_toluene, T_sweep, P_feed, flash, scp],\n",
77+
" connections=[\n",
78+
" Connection(F_feed, flash), # F -> port 0\n",
79+
" Connection(z_benzene, flash[1]), # z_1 (benzene) -> port 1\n",
80+
" Connection(z_toluene, flash[2]), # z_2 (toluene) -> port 2\n",
81+
" Connection(T_sweep, flash[3]), # T -> port 3\n",
82+
" Connection(P_feed, flash[4]), # P -> port 4\n",
83+
" Connection(flash, scp), # V_rate -> scope 0\n",
84+
" Connection(flash[1], scp[1]), # L_rate -> scope 1\n",
85+
" Connection(flash[2], scp[2]), # y_benzene -> scope 2\n",
86+
" Connection(flash[3], scp[3]), # y_toluene -> scope 3\n",
87+
" Connection(flash[4], scp[4]), # x_benzene -> scope 4\n",
88+
" Connection(flash[5], scp[5]), # x_toluene -> scope 5\n",
89+
" ],\n",
90+
" dt=0.5,\n",
91+
")\n",
92+
"\n",
93+
"sim.run(80)"
94+
]
95+
},
96+
{
97+
"cell_type": "markdown",
98+
"metadata": {},
99+
"source": [
100+
"## Results: Flow Rates\n",
101+
"\n",
102+
"As temperature increases, the vapor fraction grows. Below the bubble point the drum produces\n",
103+
"only liquid; above the dew point it produces only vapor."
104+
]
105+
},
106+
{
107+
"cell_type": "code",
108+
"execution_count": null,
109+
"metadata": {},
110+
"outputs": [],
111+
"source": [
112+
"time, signals = scp.read()\n",
113+
"T = 340.0 + time # temperature axis\n",
114+
"\n",
115+
"V_rate, L_rate = signals[0], signals[1]\n",
116+
"y_benz, y_tol = signals[2], signals[3]\n",
117+
"x_benz, x_tol = signals[4], signals[5]\n",
118+
"\n",
119+
"# Infer p-xylene fractions\n",
120+
"y_xyl = 1.0 - y_benz - y_tol\n",
121+
"x_xyl = 1.0 - x_benz - x_tol\n",
122+
"\n",
123+
"fig, axes = plt.subplots(1, 3, figsize=(15, 5))\n",
124+
"\n",
125+
"# Flow rates\n",
126+
"ax = axes[0]\n",
127+
"ax.plot(T, V_rate, label=\"Vapor\")\n",
128+
"ax.plot(T, L_rate, label=\"Liquid\")\n",
129+
"ax.set_xlabel(\"Temperature [K]\")\n",
130+
"ax.set_ylabel(\"Flow rate [mol/s]\")\n",
131+
"ax.set_title(\"Flash Drum Flow Rates\")\n",
132+
"ax.legend()\n",
133+
"ax.grid(True, alpha=0.3)\n",
134+
"\n",
135+
"# Vapor compositions\n",
136+
"ax = axes[1]\n",
137+
"ax.plot(T, y_benz, label=\"Benzene\")\n",
138+
"ax.plot(T, y_tol, label=\"Toluene\")\n",
139+
"ax.plot(T, y_xyl, label=\"p-Xylene\")\n",
140+
"ax.set_xlabel(\"Temperature [K]\")\n",
141+
"ax.set_ylabel(\"Vapor mole fraction\")\n",
142+
"ax.set_title(\"Vapor Composition\")\n",
143+
"ax.legend()\n",
144+
"ax.grid(True, alpha=0.3)\n",
145+
"\n",
146+
"# Liquid compositions\n",
147+
"ax = axes[2]\n",
148+
"ax.plot(T, x_benz, label=\"Benzene\")\n",
149+
"ax.plot(T, x_tol, label=\"Toluene\")\n",
150+
"ax.plot(T, x_xyl, label=\"p-Xylene\")\n",
151+
"ax.set_xlabel(\"Temperature [K]\")\n",
152+
"ax.set_ylabel(\"Liquid mole fraction\")\n",
153+
"ax.set_title(\"Liquid Composition\")\n",
154+
"ax.legend()\n",
155+
"ax.grid(True, alpha=0.3)\n",
156+
"\n",
157+
"plt.tight_layout()\n",
158+
"plt.show()"
159+
]
160+
},
161+
{
162+
"cell_type": "markdown",
163+
"metadata": {},
164+
"source": [
165+
"## Results: VLE Diagram\n",
166+
"\n",
167+
"Plot the vapor vs liquid composition for each component across the temperature sweep.\n",
168+
"The diagonal represents equal vapor and liquid composition — deviation from it shows\n",
169+
"the separation achieved by the flash."
170+
]
171+
},
172+
{
173+
"cell_type": "code",
174+
"execution_count": null,
175+
"metadata": {},
176+
"outputs": [],
177+
"source": [
178+
"fig, axes = plt.subplots(1, 3, figsize=(15, 5))\n",
179+
"\n",
180+
"for ax, xi, yi, name in zip(axes,\n",
181+
" [x_benz, x_tol, x_xyl],\n",
182+
" [y_benz, y_tol, y_xyl],\n",
183+
" [\"Benzene\", \"Toluene\", \"p-Xylene\"]):\n",
184+
" ax.plot(xi, yi, \".\", markersize=3)\n",
185+
" ax.plot([0, 1], [0, 1], \"k--\", alpha=0.3)\n",
186+
" ax.set_xlabel(f\"$x$ ({name})\")\n",
187+
" ax.set_ylabel(f\"$y$ ({name})\")\n",
188+
" ax.set_title(f\"{name} (x, y)-Diagram\")\n",
189+
" ax.set_aspect(\"equal\")\n",
190+
" ax.grid(True, alpha=0.3)\n",
191+
"\n",
192+
"plt.tight_layout()\n",
193+
"plt.show()"
194+
]
195+
},
196+
{
197+
"cell_type": "markdown",
198+
"metadata": {},
199+
"source": [
200+
"## Fixed-Temperature Flash at 380 K\n",
201+
"\n",
202+
"For a direct comparison with MiniSim's result (which solves at steady state),\n",
203+
"we run a fixed-temperature flash and let the holdup reach equilibrium."
204+
]
205+
},
206+
{
207+
"cell_type": "code",
208+
"execution_count": null,
209+
"metadata": {},
210+
"outputs": [],
211+
"source": [
212+
"flash2 = MultiComponentFlash(N_comp=3, holdup=100.0)\n",
213+
"\n",
214+
"F_src = Source(func=lambda t: 10.0)\n",
215+
"z1_src = Source(func=lambda t: 0.5)\n",
216+
"z2_src = Source(func=lambda t: 0.1)\n",
217+
"T_src = Source(func=lambda t: 380.0) # fixed at 380 K (~107 °C)\n",
218+
"P_src = Source(func=lambda t: 101325.0)\n",
219+
"\n",
220+
"scp2 = Scope(labels=[\"V_rate\", \"L_rate\", \"y_benz\", \"y_tol\", \"x_benz\", \"x_tol\"])\n",
221+
"\n",
222+
"sim2 = Simulation(\n",
223+
" blocks=[F_src, z1_src, z2_src, T_src, P_src, flash2, scp2],\n",
224+
" connections=[\n",
225+
" Connection(F_src, flash2),\n",
226+
" Connection(z1_src, flash2[1]),\n",
227+
" Connection(z2_src, flash2[2]),\n",
228+
" Connection(T_src, flash2[3]),\n",
229+
" Connection(P_src, flash2[4]),\n",
230+
" Connection(flash2, scp2),\n",
231+
" Connection(flash2[1], scp2[1]),\n",
232+
" Connection(flash2[2], scp2[2]),\n",
233+
" Connection(flash2[3], scp2[3]),\n",
234+
" Connection(flash2[4], scp2[4]),\n",
235+
" Connection(flash2[5], scp2[5]),\n",
236+
" ],\n",
237+
" dt=0.5,\n",
238+
")\n",
239+
"\n",
240+
"sim2.run(100) # let it reach steady state\n",
241+
"\n",
242+
"time2, signals2 = scp2.read()\n",
243+
"\n",
244+
"# Extract final steady-state values\n",
245+
"V_ss = signals2[0][-1]\n",
246+
"L_ss = signals2[1][-1]\n",
247+
"y_benz_ss = signals2[2][-1]\n",
248+
"y_tol_ss = signals2[3][-1]\n",
249+
"x_benz_ss = signals2[4][-1]\n",
250+
"x_tol_ss = signals2[5][-1]\n",
251+
"\n",
252+
"print(\"BTX Flash at 380 K, 1 atm\")\n",
253+
"print(\"=\" * 40)\n",
254+
"print(f\"{'':20s} {'Vapor':>10s} {'Liquid':>10s}\")\n",
255+
"print(f\"{'-'*40}\")\n",
256+
"print(f\"{'Flow rate [mol/s]':20s} {V_ss:10.3f} {L_ss:10.3f}\")\n",
257+
"print(f\"{'Benzene':20s} {y_benz_ss:10.4f} {x_benz_ss:10.4f}\")\n",
258+
"print(f\"{'Toluene':20s} {y_tol_ss:10.4f} {x_tol_ss:10.4f}\")\n",
259+
"print(f\"{'p-Xylene':20s} {1-y_benz_ss-y_tol_ss:10.4f} {1-x_benz_ss-x_tol_ss:10.4f}\")"
260+
]
261+
},
262+
{
263+
"cell_type": "markdown",
264+
"metadata": {},
265+
"source": [
266+
"The lighter component (benzene) is enriched in the vapor phase while the heavier\n",
267+
"component (p-xylene) concentrates in the liquid — exactly the separation behaviour\n",
268+
"expected from VLE. The dynamic formulation reaches the same steady state that\n",
269+
"an equation-oriented solver (like MiniSim) finds directly."
270+
]
271+
}
272+
],
273+
"metadata": {
274+
"kernelspec": {
275+
"display_name": "Python 3",
276+
"language": "python",
277+
"name": "python3"
278+
},
279+
"language_info": {
280+
"name": "python",
281+
"version": "3.11.0"
282+
}
283+
},
284+
"nbformat": 4,
285+
"nbformat_minor": 4
286+
}

0 commit comments

Comments
 (0)