Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
286 changes: 286 additions & 0 deletions docs/source/examples/multicomponent_flash.ipynb
Original file line number Diff line number Diff line change
@@ -0,0 +1,286 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Multi-Component Flash Separation (BTX)\n",
"\n",
"Simulating an isothermal flash drum for a ternary **benzene–toluene–p-xylene** (BTX) mixture.\n",
"The `MultiComponentFlash` block uses Raoult's law with Antoine correlations to compute K-values\n",
"and solves the Rachford-Rice equation via Brent's method.\n",
"\n",
"This example is inspired by [MiniSim's SimpleFlash example](https://github.com/Nukleon84/MiniSim/blob/master/doc/SimpleFlash.ipynb),\n",
"adapted to PathSim's dynamic simulation framework.\n",
"\n",
"**Feed conditions:**\n",
"- 10 mol/s total flow\n",
"- 50% benzene, 10% toluene, 40% p-xylene (molar)\n",
"- 1 atm pressure\n",
"- Temperature sweep: 340 K → 420 K"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import numpy as np\n",
"import matplotlib.pyplot as plt\n",
"\n",
"from pathsim import Simulation, Connection\n",
"from pathsim.blocks import Source, Scope\n",
"\n",
"from pathsim_chem.process import MultiComponentFlash"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Flash Drum Setup\n",
"\n",
"The `MultiComponentFlash` block defaults to BTX Antoine parameters (ln form, Pa, K):\n",
"\n",
"| Component | A | B | C |\n",
"|-----------|-------|---------|--------|\n",
"| Benzene | 20.7936 | 2788.51 | -52.36 |\n",
"| Toluene | 20.9064 | 3096.52 | -53.67 |\n",
"| p-Xylene | 20.9891 | 3346.65 | -57.84 |\n",
"\n",
"We feed the drum with constant composition and pressure while ramping temperature\n",
"to observe the transition from all-liquid through two-phase to all-vapor."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Create the flash drum (3 components, BTX defaults)\n",
"flash = MultiComponentFlash(N_comp=3, holdup=100.0)\n",
"\n",
"# Feed sources\n",
"F_feed = Source(func=lambda t: 10.0) # 10 mol/s\n",
"z_benzene = Source(func=lambda t: 0.5) # 50% benzene\n",
"z_toluene = Source(func=lambda t: 0.1) # 10% toluene (p-xylene = 40% inferred)\n",
"T_sweep = Source(func=lambda t: 340.0 + t) # ramp 340 -> 420 K\n",
"P_feed = Source(func=lambda t: 101325.0) # 1 atm\n",
"\n",
"# Record all outputs: V_rate, L_rate, y_benzene, y_toluene, x_benzene, x_toluene\n",
"scp = Scope(labels=[\"V_rate\", \"L_rate\", \"y_benz\", \"y_tol\", \"x_benz\", \"x_tol\"])\n",
"\n",
"sim = Simulation(\n",
" blocks=[F_feed, z_benzene, z_toluene, T_sweep, P_feed, flash, scp],\n",
" connections=[\n",
" Connection(F_feed, flash), # F -> port 0\n",
" Connection(z_benzene, flash[1]), # z_1 (benzene) -> port 1\n",
" Connection(z_toluene, flash[2]), # z_2 (toluene) -> port 2\n",
" Connection(T_sweep, flash[3]), # T -> port 3\n",
" Connection(P_feed, flash[4]), # P -> port 4\n",
" Connection(flash, scp), # V_rate -> scope 0\n",
" Connection(flash[1], scp[1]), # L_rate -> scope 1\n",
" Connection(flash[2], scp[2]), # y_benzene -> scope 2\n",
" Connection(flash[3], scp[3]), # y_toluene -> scope 3\n",
" Connection(flash[4], scp[4]), # x_benzene -> scope 4\n",
" Connection(flash[5], scp[5]), # x_toluene -> scope 5\n",
" ],\n",
" dt=0.5,\n",
")\n",
"\n",
"sim.run(80)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Results: Flow Rates\n",
"\n",
"As temperature increases, the vapor fraction grows. Below the bubble point the drum produces\n",
"only liquid; above the dew point it produces only vapor."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"time, signals = scp.read()\n",
"T = 340.0 + time # temperature axis\n",
"\n",
"V_rate, L_rate = signals[0], signals[1]\n",
"y_benz, y_tol = signals[2], signals[3]\n",
"x_benz, x_tol = signals[4], signals[5]\n",
"\n",
"# Infer p-xylene fractions\n",
"y_xyl = 1.0 - y_benz - y_tol\n",
"x_xyl = 1.0 - x_benz - x_tol\n",
"\n",
"fig, axes = plt.subplots(1, 3, figsize=(15, 5))\n",
"\n",
"# Flow rates\n",
"ax = axes[0]\n",
"ax.plot(T, V_rate, label=\"Vapor\")\n",
"ax.plot(T, L_rate, label=\"Liquid\")\n",
"ax.set_xlabel(\"Temperature [K]\")\n",
"ax.set_ylabel(\"Flow rate [mol/s]\")\n",
"ax.set_title(\"Flash Drum Flow Rates\")\n",
"ax.legend()\n",
"ax.grid(True, alpha=0.3)\n",
"\n",
"# Vapor compositions\n",
"ax = axes[1]\n",
"ax.plot(T, y_benz, label=\"Benzene\")\n",
"ax.plot(T, y_tol, label=\"Toluene\")\n",
"ax.plot(T, y_xyl, label=\"p-Xylene\")\n",
"ax.set_xlabel(\"Temperature [K]\")\n",
"ax.set_ylabel(\"Vapor mole fraction\")\n",
"ax.set_title(\"Vapor Composition\")\n",
"ax.legend()\n",
"ax.grid(True, alpha=0.3)\n",
"\n",
"# Liquid compositions\n",
"ax = axes[2]\n",
"ax.plot(T, x_benz, label=\"Benzene\")\n",
"ax.plot(T, x_tol, label=\"Toluene\")\n",
"ax.plot(T, x_xyl, label=\"p-Xylene\")\n",
"ax.set_xlabel(\"Temperature [K]\")\n",
"ax.set_ylabel(\"Liquid mole fraction\")\n",
"ax.set_title(\"Liquid Composition\")\n",
"ax.legend()\n",
"ax.grid(True, alpha=0.3)\n",
"\n",
"plt.tight_layout()\n",
"plt.show()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Results: VLE Diagram\n",
"\n",
"Plot the vapor vs liquid composition for each component across the temperature sweep.\n",
"The diagonal represents equal vapor and liquid composition — deviation from it shows\n",
"the separation achieved by the flash."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"fig, axes = plt.subplots(1, 3, figsize=(15, 5))\n",
"\n",
"for ax, xi, yi, name in zip(axes,\n",
" [x_benz, x_tol, x_xyl],\n",
" [y_benz, y_tol, y_xyl],\n",
" [\"Benzene\", \"Toluene\", \"p-Xylene\"]):\n",
" ax.plot(xi, yi, \".\", markersize=3)\n",
" ax.plot([0, 1], [0, 1], \"k--\", alpha=0.3)\n",
" ax.set_xlabel(f\"$x$ ({name})\")\n",
" ax.set_ylabel(f\"$y$ ({name})\")\n",
" ax.set_title(f\"{name} (x, y)-Diagram\")\n",
" ax.set_aspect(\"equal\")\n",
" ax.grid(True, alpha=0.3)\n",
"\n",
"plt.tight_layout()\n",
"plt.show()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Fixed-Temperature Flash at 380 K\n",
"\n",
"For a direct comparison with MiniSim's result (which solves at steady state),\n",
"we run a fixed-temperature flash and let the holdup reach equilibrium."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"flash2 = MultiComponentFlash(N_comp=3, holdup=100.0)\n",
"\n",
"F_src = Source(func=lambda t: 10.0)\n",
"z1_src = Source(func=lambda t: 0.5)\n",
"z2_src = Source(func=lambda t: 0.1)\n",
"T_src = Source(func=lambda t: 380.0) # fixed at 380 K (~107 °C)\n",
"P_src = Source(func=lambda t: 101325.0)\n",
"\n",
"scp2 = Scope(labels=[\"V_rate\", \"L_rate\", \"y_benz\", \"y_tol\", \"x_benz\", \"x_tol\"])\n",
"\n",
"sim2 = Simulation(\n",
" blocks=[F_src, z1_src, z2_src, T_src, P_src, flash2, scp2],\n",
" connections=[\n",
" Connection(F_src, flash2),\n",
" Connection(z1_src, flash2[1]),\n",
" Connection(z2_src, flash2[2]),\n",
" Connection(T_src, flash2[3]),\n",
" Connection(P_src, flash2[4]),\n",
" Connection(flash2, scp2),\n",
" Connection(flash2[1], scp2[1]),\n",
" Connection(flash2[2], scp2[2]),\n",
" Connection(flash2[3], scp2[3]),\n",
" Connection(flash2[4], scp2[4]),\n",
" Connection(flash2[5], scp2[5]),\n",
" ],\n",
" dt=0.5,\n",
")\n",
"\n",
"sim2.run(100) # let it reach steady state\n",
"\n",
"time2, signals2 = scp2.read()\n",
"\n",
"# Extract final steady-state values\n",
"V_ss = signals2[0][-1]\n",
"L_ss = signals2[1][-1]\n",
"y_benz_ss = signals2[2][-1]\n",
"y_tol_ss = signals2[3][-1]\n",
"x_benz_ss = signals2[4][-1]\n",
"x_tol_ss = signals2[5][-1]\n",
"\n",
"print(\"BTX Flash at 380 K, 1 atm\")\n",
"print(\"=\" * 40)\n",
"print(f\"{'':20s} {'Vapor':>10s} {'Liquid':>10s}\")\n",
"print(f\"{'-'*40}\")\n",
"print(f\"{'Flow rate [mol/s]':20s} {V_ss:10.3f} {L_ss:10.3f}\")\n",
"print(f\"{'Benzene':20s} {y_benz_ss:10.4f} {x_benz_ss:10.4f}\")\n",
"print(f\"{'Toluene':20s} {y_tol_ss:10.4f} {x_tol_ss:10.4f}\")\n",
"print(f\"{'p-Xylene':20s} {1-y_benz_ss-y_tol_ss:10.4f} {1-x_benz_ss-x_tol_ss:10.4f}\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The lighter component (benzene) is enriched in the vapor phase while the heavier\n",
"component (p-xylene) concentrates in the liquid — exactly the separation behaviour\n",
"expected from VLE. The dynamic formulation reaches the same steady state that\n",
"an equation-oriented solver (like MiniSim) finds directly."
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"name": "python",
"version": "3.11.0"
}
},
"nbformat": 4,
"nbformat_minor": 4
}
Loading