From ab7223f94f8a61e5e63639d62364d0688f996b5a Mon Sep 17 00:00:00 2001 From: Milan Rother Date: Mon, 2 Mar 2026 14:15:10 +0100 Subject: [PATCH 1/4] Fix distillation example: use scope data instead of block.state for steady-state profile --- docs/source/examples/flash_distillation.ipynb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/examples/flash_distillation.ipynb b/docs/source/examples/flash_distillation.ipynb index 981ca88..5468fee 100644 --- a/docs/source/examples/flash_distillation.ipynb +++ b/docs/source/examples/flash_distillation.ipynb @@ -154,7 +154,7 @@ "execution_count": null, "metadata": {}, "outputs": [], - "source": "time, signals = scp.read()\n\nfig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5))\n\n# Dynamic tray composition evolution\nfor i in range(N_trays):\n ax1.plot(time, signals[i], label=f\"Tray {i+1}\")\nax1.set_xlabel(\"Time [s]\")\nax1.set_ylabel(r\"$x$ (light component)\")\nax1.set_title(\"Tray Compositions Over Time\")\nax1.legend()\nax1.grid(True, alpha=0.3)\n\n# Steady-state composition profile\nx_profile = [tray.state[0] for tray in trays]\ntray_nums = list(range(1, N_trays + 1))\n\nax2.plot(tray_nums, x_profile, \"o-\", markersize=8)\nax2.set_xlabel(\"Tray number (top to bottom)\")\nax2.set_ylabel(r\"$x$ (light component)\")\nax2.set_title(\"Composition Profile (Steady State)\")\nax2.grid(True, alpha=0.3)\n\nplt.tight_layout()\nplt.show()" + "source": "time, signals = scp.read()\n\nfig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5))\n\n# Dynamic tray composition evolution\nfor i in range(N_trays):\n ax1.plot(time, signals[i], label=f\"Tray {i+1}\")\nax1.set_xlabel(\"Time [s]\")\nax1.set_ylabel(r\"$x$ (light component)\")\nax1.set_title(\"Tray Compositions Over Time\")\nax1.legend()\nax1.grid(True, alpha=0.3)\n\n# Steady-state composition profile (from scope data)\nx_profile = [signals[i][-1] for i in range(N_trays)]\ntray_nums = list(range(1, N_trays + 1))\n\nax2.plot(tray_nums, x_profile, \"o-\", markersize=8)\nax2.set_xlabel(\"Tray number (top to bottom)\")\nax2.set_ylabel(r\"$x$ (light component)\")\nax2.set_title(\"Composition Profile (Steady State)\")\nax2.grid(True, alpha=0.3)\n\nplt.tight_layout()\nplt.show()" }, { "cell_type": "markdown", From 921b10f9d3bcb2fea101e0ac217d307d0539fe83 Mon Sep 17 00:00:00 2001 From: Milan Rother Date: Mon, 2 Mar 2026 14:20:59 +0100 Subject: [PATCH 2/4] Expose per-cell temperatures as output ports on HeatExchanger and PFR --- docs/source/examples/heat_exchanger.ipynb | 12 ++++-------- src/pathsim_chem/process/heat_exchanger.py | 22 +++++++++++++++------- src/pathsim_chem/process/pfr.py | 21 ++++++++++++++------- tests/process/test_heat_exchanger.py | 10 ++++++++-- tests/process/test_pfr.py | 10 ++++++++-- 5 files changed, 49 insertions(+), 26 deletions(-) diff --git a/docs/source/examples/heat_exchanger.ipynb b/docs/source/examples/heat_exchanger.ipynb index 8d80903..32baaf1 100644 --- a/docs/source/examples/heat_exchanger.ipynb +++ b/docs/source/examples/heat_exchanger.ipynb @@ -52,30 +52,26 @@ { "cell_type": "markdown", "metadata": {}, - "source": [ - "Feed hot water at 370 K and cold water at 290 K. Record the outlet temperatures over time." - ] + "source": "Feed hot water at 370 K and cold water at 290 K. Record the outlet temperatures over time and the per-cell temperature profiles for spatial analysis." }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], - "source": "T_h_in = Source(func=lambda t: 370.0) # hot inlet [K]\nT_c_in = Source(func=lambda t: 290.0) # cold inlet [K]\n\nscp = Scope(labels=[\"T_h_out [K]\", \"T_c_out [K]\"])\n\nsim = Simulation(\n blocks=[T_h_in, T_c_in, hx, scp],\n connections=[\n Connection(T_h_in, hx), # T_h_in -> port 0\n Connection(T_c_in, hx[1]), # T_c_in -> port 1\n Connection(hx, scp), # T_h_out -> scope port 0\n Connection(hx[1], scp[1]), # T_c_out -> scope port 1\n ],\n dt=0.02,\n)\n\nsim.run(200)" + "source": "N = 10 # must match N_cells\n\nT_h_in = Source(func=lambda t: 370.0) # hot inlet [K]\nT_c_in = Source(func=lambda t: 290.0) # cold inlet [K]\n\nscp_out = Scope(labels=[\"T_h_out [K]\", \"T_c_out [K]\"])\nscp_hot = Scope(labels=[f\"T_h_{i+1}\" for i in range(N)])\nscp_cold = Scope(labels=[f\"T_c_{i+1}\" for i in range(N)])\n\nconnections = [\n Connection(T_h_in, hx), # T_h_in -> port 0\n Connection(T_c_in, hx[1]), # T_c_in -> port 1\n Connection(hx, scp_out), # T_h_out -> outlet scope\n Connection(hx[1], scp_out[1]), # T_c_out -> outlet scope\n]\n\n# Connect per-cell temperatures to profile scopes\nfor i in range(N):\n connections.append(Connection(hx[2 + i], scp_hot[i])) # T_h_i\n connections.append(Connection(hx[2 + N + i], scp_cold[i])) # T_c_i\n\nsim = Simulation(\n blocks=[T_h_in, T_c_in, hx, scp_out, scp_hot, scp_cold],\n connections=connections,\n dt=0.02,\n)\n\nsim.run(200)" }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], - "source": "time, signals = scp.read()\n\nfig, ax = plt.subplots(figsize=(8, 5))\n\nax.plot(time, signals[0], label=\"Hot outlet\")\nax.plot(time, signals[1], label=\"Cold outlet\")\nax.axhline(370, color=\"gray\", ls=\"--\", alpha=0.4, label=\"Hot inlet\")\nax.axhline(290, color=\"gray\", ls=\"-.\", alpha=0.4, label=\"Cold inlet\")\nax.set_xlabel(\"Time [s]\")\nax.set_ylabel(\"Temperature [K]\")\nax.set_title(\"Heat Exchanger Outlet Temperatures\")\nax.legend()\nax.grid(True, alpha=0.3)\n\nplt.tight_layout()\nplt.show()" + "source": "time, out = scp_out.read()\n_, hot_profile = scp_hot.read()\n_, cold_profile = scp_cold.read()\n\nfig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 5))\n\n# Outlet temperatures over time\nax1.plot(time, out[0], label=\"Hot outlet\")\nax1.plot(time, out[1], label=\"Cold outlet\")\nax1.axhline(370, color=\"gray\", ls=\"--\", alpha=0.4, label=\"Hot inlet\")\nax1.axhline(290, color=\"gray\", ls=\"-.\", alpha=0.4, label=\"Cold inlet\")\nax1.set_xlabel(\"Time [s]\")\nax1.set_ylabel(\"Temperature [K]\")\nax1.set_title(\"Outlet Temperatures\")\nax1.legend()\nax1.grid(True, alpha=0.3)\n\n# Spatial temperature profile at steady state\ncells = np.arange(1, N + 1)\nT_h_ss = [hot_profile[i][-1] for i in range(N)]\nT_c_ss = [cold_profile[i][-1] for i in range(N)]\n\nax2.plot(cells, T_h_ss, \"o-\", color=\"tab:red\", label=\"Hot stream\")\nax2.plot(cells, T_c_ss, \"s-\", color=\"tab:blue\", label=\"Cold stream\")\nax2.set_xlabel(\"Cell number (hot flow direction →)\")\nax2.set_ylabel(\"Temperature [K]\")\nax2.set_title(\"Spatial Profile (Steady State)\")\nax2.legend()\nax2.grid(True, alpha=0.3)\n\nplt.tight_layout()\nplt.show()" }, { "cell_type": "markdown", "metadata": {}, - "source": [ - "At startup, both sides are cold. The hot fluid quickly warms the exchanger from the inlet side while the cold fluid absorbs heat. At steady state, the counter-current arrangement creates the characteristic temperature cross — the cold outlet approaches the hot inlet and vice versa, achieving high thermal efficiency." - ] + "source": "At startup, both sides are cold. The hot fluid quickly warms the exchanger from the inlet side while the cold fluid absorbs heat. At steady state, the counter-current arrangement creates the characteristic temperature cross — the cold outlet approaches the hot inlet and vice versa. The spatial profile shows the classic counter-current pattern where both streams decrease along the hot flow direction." } ], "metadata": { diff --git a/src/pathsim_chem/process/heat_exchanger.py b/src/pathsim_chem/process/heat_exchanger.py index 4c15cfb..f1a587a 100644 --- a/src/pathsim_chem/process/heat_exchanger.py +++ b/src/pathsim_chem/process/heat_exchanger.py @@ -72,11 +72,6 @@ class HeatExchanger(DynamicalSystem): "T_c_in": 1, } - output_port_labels = { - "T_h_out": 0, - "T_c_out": 1, - } - def __init__(self, N_cells=5, F_h=0.1, F_c=0.1, V_h=0.5, V_c=0.5, UA=500.0, rho_h=1000.0, Cp_h=4184.0, rho_c=1000.0, Cp_c=4184.0, T_h0=370.0, T_c0=300.0): @@ -107,6 +102,12 @@ def __init__(self, N_cells=5, F_h=0.1, F_c=0.1, V_h=0.5, V_c=0.5, N = self.N_cells + # dynamic output port labels: outlets + per-cell profiles + self.output_port_labels = {"T_h_out": 0, "T_c_out": 1} + for i in range(N): + self.output_port_labels[f"T_h_{i+1}"] = 2 + i + self.output_port_labels[f"T_c_{i+1}"] = 2 + N + i + # initial state: interleaved [T_h1, T_c1, T_h2, T_c2, ...] x0 = np.empty(2 * N) x0[0::2] = T_h0 @@ -192,9 +193,16 @@ def _jc_d(x, u, t): return J - # output: hot outlet = last cell hot, cold outlet = first cell cold + # output: outlets + per-cell profiles + # [T_h_out, T_c_out, T_h_1..T_h_N, T_c_1..T_c_N] def _fn_a(x, u, t): - return np.array([x[2*(self.N_cells - 1)], x[1]]) + N = self.N_cells + out = np.empty(2 + 2 * N) + out[0] = x[2 * (N - 1)] # T_h_out + out[1] = x[1] # T_c_out + out[2:N + 2] = x[0::2] # hot profile + out[N + 2:] = x[1::2] # cold profile + return out super().__init__( func_dyn=_fn_d, diff --git a/src/pathsim_chem/process/pfr.py b/src/pathsim_chem/process/pfr.py index 05b2875..78e79a5 100644 --- a/src/pathsim_chem/process/pfr.py +++ b/src/pathsim_chem/process/pfr.py @@ -76,11 +76,6 @@ class PFR(DynamicalSystem): "T_in": 1, } - output_port_labels = { - "C_out": 0, - "T_out": 1, - } - def __init__(self, N_cells=5, V=1.0, F=0.1, k0=1e6, Ea=50000.0, n=1.0, dH_rxn=-50000.0, rho=1000.0, Cp=4184.0, C0=0.0, T0=300.0): @@ -110,6 +105,12 @@ def __init__(self, N_cells=5, V=1.0, F=0.1, k0=1e6, Ea=50000.0, n=1.0, N = self.N_cells + # dynamic output port labels: outlets + per-cell profiles + self.output_port_labels = {"C_out": 0, "T_out": 1} + for i in range(N): + self.output_port_labels[f"C_{i+1}"] = 2 + i + self.output_port_labels[f"T_{i+1}"] = 2 + N + i + # initial state: interleaved [C_1, T_1, C_2, T_2, ...] x0 = np.empty(2 * N) x0[0::2] = C0 @@ -196,10 +197,16 @@ def _jc_d(x, u, t): return J - # output: last cell values + # output: outlets + per-cell profiles + # [C_out, T_out, C_1..C_N, T_1..T_N] def _fn_a(x, u, t): N = self.N_cells - return np.array([x[2*(N-1)], x[2*(N-1) + 1]]) + out = np.empty(2 + 2 * N) + out[0] = x[2 * (N - 1)] # C_out + out[1] = x[2 * (N - 1) + 1] # T_out + out[2:N + 2] = x[0::2] # concentration profile + out[N + 2:] = x[1::2] # temperature profile + return out super().__init__( func_dyn=_fn_d, diff --git a/tests/process/test_heat_exchanger.py b/tests/process/test_heat_exchanger.py index 7ba0549..9d04a19 100644 --- a/tests/process/test_heat_exchanger.py +++ b/tests/process/test_heat_exchanger.py @@ -60,8 +60,14 @@ def test_port_labels(self): """Test port label definitions.""" self.assertEqual(HeatExchanger.input_port_labels["T_h_in"], 0) self.assertEqual(HeatExchanger.input_port_labels["T_c_in"], 1) - self.assertEqual(HeatExchanger.output_port_labels["T_h_out"], 0) - self.assertEqual(HeatExchanger.output_port_labels["T_c_out"], 1) + hx = HeatExchanger(N_cells=5) + self.assertEqual(hx.output_port_labels["T_h_out"], 0) + self.assertEqual(hx.output_port_labels["T_c_out"], 1) + # per-cell ports + self.assertEqual(hx.output_port_labels["T_h_1"], 2) + self.assertEqual(hx.output_port_labels["T_c_1"], 7) + self.assertEqual(hx.output_port_labels["T_h_5"], 6) + self.assertEqual(hx.output_port_labels["T_c_5"], 11) def test_state_size(self): """Test that state vector has correct size.""" diff --git a/tests/process/test_pfr.py b/tests/process/test_pfr.py index df7ba6b..448cd7b 100644 --- a/tests/process/test_pfr.py +++ b/tests/process/test_pfr.py @@ -60,8 +60,14 @@ def test_port_labels(self): """Test port label definitions.""" self.assertEqual(PFR.input_port_labels["C_in"], 0) self.assertEqual(PFR.input_port_labels["T_in"], 1) - self.assertEqual(PFR.output_port_labels["C_out"], 0) - self.assertEqual(PFR.output_port_labels["T_out"], 1) + pfr = PFR(N_cells=5) + self.assertEqual(pfr.output_port_labels["C_out"], 0) + self.assertEqual(pfr.output_port_labels["T_out"], 1) + # per-cell ports + self.assertEqual(pfr.output_port_labels["C_1"], 2) + self.assertEqual(pfr.output_port_labels["T_1"], 7) + self.assertEqual(pfr.output_port_labels["C_5"], 6) + self.assertEqual(pfr.output_port_labels["T_5"], 11) def test_state_size(self): """Test that state vector has correct size.""" From cb083a80050817c741eb7ac42c621102c3901dbf Mon Sep 17 00:00:00 2001 From: Milan Rother Date: Mon, 2 Mar 2026 14:39:25 +0100 Subject: [PATCH 3/4] Revert "Expose per-cell temperatures as output ports on HeatExchanger and PFR" This reverts commit 921b10f9d3bcb2fea101e0ac217d307d0539fe83. --- docs/source/examples/heat_exchanger.ipynb | 12 ++++++++---- src/pathsim_chem/process/heat_exchanger.py | 22 +++++++--------------- src/pathsim_chem/process/pfr.py | 21 +++++++-------------- tests/process/test_heat_exchanger.py | 10 ++-------- tests/process/test_pfr.py | 10 ++-------- 5 files changed, 26 insertions(+), 49 deletions(-) diff --git a/docs/source/examples/heat_exchanger.ipynb b/docs/source/examples/heat_exchanger.ipynb index 32baaf1..8d80903 100644 --- a/docs/source/examples/heat_exchanger.ipynb +++ b/docs/source/examples/heat_exchanger.ipynb @@ -52,26 +52,30 @@ { "cell_type": "markdown", "metadata": {}, - "source": "Feed hot water at 370 K and cold water at 290 K. Record the outlet temperatures over time and the per-cell temperature profiles for spatial analysis." + "source": [ + "Feed hot water at 370 K and cold water at 290 K. Record the outlet temperatures over time." + ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], - "source": "N = 10 # must match N_cells\n\nT_h_in = Source(func=lambda t: 370.0) # hot inlet [K]\nT_c_in = Source(func=lambda t: 290.0) # cold inlet [K]\n\nscp_out = Scope(labels=[\"T_h_out [K]\", \"T_c_out [K]\"])\nscp_hot = Scope(labels=[f\"T_h_{i+1}\" for i in range(N)])\nscp_cold = Scope(labels=[f\"T_c_{i+1}\" for i in range(N)])\n\nconnections = [\n Connection(T_h_in, hx), # T_h_in -> port 0\n Connection(T_c_in, hx[1]), # T_c_in -> port 1\n Connection(hx, scp_out), # T_h_out -> outlet scope\n Connection(hx[1], scp_out[1]), # T_c_out -> outlet scope\n]\n\n# Connect per-cell temperatures to profile scopes\nfor i in range(N):\n connections.append(Connection(hx[2 + i], scp_hot[i])) # T_h_i\n connections.append(Connection(hx[2 + N + i], scp_cold[i])) # T_c_i\n\nsim = Simulation(\n blocks=[T_h_in, T_c_in, hx, scp_out, scp_hot, scp_cold],\n connections=connections,\n dt=0.02,\n)\n\nsim.run(200)" + "source": "T_h_in = Source(func=lambda t: 370.0) # hot inlet [K]\nT_c_in = Source(func=lambda t: 290.0) # cold inlet [K]\n\nscp = Scope(labels=[\"T_h_out [K]\", \"T_c_out [K]\"])\n\nsim = Simulation(\n blocks=[T_h_in, T_c_in, hx, scp],\n connections=[\n Connection(T_h_in, hx), # T_h_in -> port 0\n Connection(T_c_in, hx[1]), # T_c_in -> port 1\n Connection(hx, scp), # T_h_out -> scope port 0\n Connection(hx[1], scp[1]), # T_c_out -> scope port 1\n ],\n dt=0.02,\n)\n\nsim.run(200)" }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], - "source": "time, out = scp_out.read()\n_, hot_profile = scp_hot.read()\n_, cold_profile = scp_cold.read()\n\nfig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 5))\n\n# Outlet temperatures over time\nax1.plot(time, out[0], label=\"Hot outlet\")\nax1.plot(time, out[1], label=\"Cold outlet\")\nax1.axhline(370, color=\"gray\", ls=\"--\", alpha=0.4, label=\"Hot inlet\")\nax1.axhline(290, color=\"gray\", ls=\"-.\", alpha=0.4, label=\"Cold inlet\")\nax1.set_xlabel(\"Time [s]\")\nax1.set_ylabel(\"Temperature [K]\")\nax1.set_title(\"Outlet Temperatures\")\nax1.legend()\nax1.grid(True, alpha=0.3)\n\n# Spatial temperature profile at steady state\ncells = np.arange(1, N + 1)\nT_h_ss = [hot_profile[i][-1] for i in range(N)]\nT_c_ss = [cold_profile[i][-1] for i in range(N)]\n\nax2.plot(cells, T_h_ss, \"o-\", color=\"tab:red\", label=\"Hot stream\")\nax2.plot(cells, T_c_ss, \"s-\", color=\"tab:blue\", label=\"Cold stream\")\nax2.set_xlabel(\"Cell number (hot flow direction →)\")\nax2.set_ylabel(\"Temperature [K]\")\nax2.set_title(\"Spatial Profile (Steady State)\")\nax2.legend()\nax2.grid(True, alpha=0.3)\n\nplt.tight_layout()\nplt.show()" + "source": "time, signals = scp.read()\n\nfig, ax = plt.subplots(figsize=(8, 5))\n\nax.plot(time, signals[0], label=\"Hot outlet\")\nax.plot(time, signals[1], label=\"Cold outlet\")\nax.axhline(370, color=\"gray\", ls=\"--\", alpha=0.4, label=\"Hot inlet\")\nax.axhline(290, color=\"gray\", ls=\"-.\", alpha=0.4, label=\"Cold inlet\")\nax.set_xlabel(\"Time [s]\")\nax.set_ylabel(\"Temperature [K]\")\nax.set_title(\"Heat Exchanger Outlet Temperatures\")\nax.legend()\nax.grid(True, alpha=0.3)\n\nplt.tight_layout()\nplt.show()" }, { "cell_type": "markdown", "metadata": {}, - "source": "At startup, both sides are cold. The hot fluid quickly warms the exchanger from the inlet side while the cold fluid absorbs heat. At steady state, the counter-current arrangement creates the characteristic temperature cross — the cold outlet approaches the hot inlet and vice versa. The spatial profile shows the classic counter-current pattern where both streams decrease along the hot flow direction." + "source": [ + "At startup, both sides are cold. The hot fluid quickly warms the exchanger from the inlet side while the cold fluid absorbs heat. At steady state, the counter-current arrangement creates the characteristic temperature cross — the cold outlet approaches the hot inlet and vice versa, achieving high thermal efficiency." + ] } ], "metadata": { diff --git a/src/pathsim_chem/process/heat_exchanger.py b/src/pathsim_chem/process/heat_exchanger.py index f1a587a..4c15cfb 100644 --- a/src/pathsim_chem/process/heat_exchanger.py +++ b/src/pathsim_chem/process/heat_exchanger.py @@ -72,6 +72,11 @@ class HeatExchanger(DynamicalSystem): "T_c_in": 1, } + output_port_labels = { + "T_h_out": 0, + "T_c_out": 1, + } + def __init__(self, N_cells=5, F_h=0.1, F_c=0.1, V_h=0.5, V_c=0.5, UA=500.0, rho_h=1000.0, Cp_h=4184.0, rho_c=1000.0, Cp_c=4184.0, T_h0=370.0, T_c0=300.0): @@ -102,12 +107,6 @@ def __init__(self, N_cells=5, F_h=0.1, F_c=0.1, V_h=0.5, V_c=0.5, N = self.N_cells - # dynamic output port labels: outlets + per-cell profiles - self.output_port_labels = {"T_h_out": 0, "T_c_out": 1} - for i in range(N): - self.output_port_labels[f"T_h_{i+1}"] = 2 + i - self.output_port_labels[f"T_c_{i+1}"] = 2 + N + i - # initial state: interleaved [T_h1, T_c1, T_h2, T_c2, ...] x0 = np.empty(2 * N) x0[0::2] = T_h0 @@ -193,16 +192,9 @@ def _jc_d(x, u, t): return J - # output: outlets + per-cell profiles - # [T_h_out, T_c_out, T_h_1..T_h_N, T_c_1..T_c_N] + # output: hot outlet = last cell hot, cold outlet = first cell cold def _fn_a(x, u, t): - N = self.N_cells - out = np.empty(2 + 2 * N) - out[0] = x[2 * (N - 1)] # T_h_out - out[1] = x[1] # T_c_out - out[2:N + 2] = x[0::2] # hot profile - out[N + 2:] = x[1::2] # cold profile - return out + return np.array([x[2*(self.N_cells - 1)], x[1]]) super().__init__( func_dyn=_fn_d, diff --git a/src/pathsim_chem/process/pfr.py b/src/pathsim_chem/process/pfr.py index 78e79a5..05b2875 100644 --- a/src/pathsim_chem/process/pfr.py +++ b/src/pathsim_chem/process/pfr.py @@ -76,6 +76,11 @@ class PFR(DynamicalSystem): "T_in": 1, } + output_port_labels = { + "C_out": 0, + "T_out": 1, + } + def __init__(self, N_cells=5, V=1.0, F=0.1, k0=1e6, Ea=50000.0, n=1.0, dH_rxn=-50000.0, rho=1000.0, Cp=4184.0, C0=0.0, T0=300.0): @@ -105,12 +110,6 @@ def __init__(self, N_cells=5, V=1.0, F=0.1, k0=1e6, Ea=50000.0, n=1.0, N = self.N_cells - # dynamic output port labels: outlets + per-cell profiles - self.output_port_labels = {"C_out": 0, "T_out": 1} - for i in range(N): - self.output_port_labels[f"C_{i+1}"] = 2 + i - self.output_port_labels[f"T_{i+1}"] = 2 + N + i - # initial state: interleaved [C_1, T_1, C_2, T_2, ...] x0 = np.empty(2 * N) x0[0::2] = C0 @@ -197,16 +196,10 @@ def _jc_d(x, u, t): return J - # output: outlets + per-cell profiles - # [C_out, T_out, C_1..C_N, T_1..T_N] + # output: last cell values def _fn_a(x, u, t): N = self.N_cells - out = np.empty(2 + 2 * N) - out[0] = x[2 * (N - 1)] # C_out - out[1] = x[2 * (N - 1) + 1] # T_out - out[2:N + 2] = x[0::2] # concentration profile - out[N + 2:] = x[1::2] # temperature profile - return out + return np.array([x[2*(N-1)], x[2*(N-1) + 1]]) super().__init__( func_dyn=_fn_d, diff --git a/tests/process/test_heat_exchanger.py b/tests/process/test_heat_exchanger.py index 9d04a19..7ba0549 100644 --- a/tests/process/test_heat_exchanger.py +++ b/tests/process/test_heat_exchanger.py @@ -60,14 +60,8 @@ def test_port_labels(self): """Test port label definitions.""" self.assertEqual(HeatExchanger.input_port_labels["T_h_in"], 0) self.assertEqual(HeatExchanger.input_port_labels["T_c_in"], 1) - hx = HeatExchanger(N_cells=5) - self.assertEqual(hx.output_port_labels["T_h_out"], 0) - self.assertEqual(hx.output_port_labels["T_c_out"], 1) - # per-cell ports - self.assertEqual(hx.output_port_labels["T_h_1"], 2) - self.assertEqual(hx.output_port_labels["T_c_1"], 7) - self.assertEqual(hx.output_port_labels["T_h_5"], 6) - self.assertEqual(hx.output_port_labels["T_c_5"], 11) + self.assertEqual(HeatExchanger.output_port_labels["T_h_out"], 0) + self.assertEqual(HeatExchanger.output_port_labels["T_c_out"], 1) def test_state_size(self): """Test that state vector has correct size.""" diff --git a/tests/process/test_pfr.py b/tests/process/test_pfr.py index 448cd7b..df7ba6b 100644 --- a/tests/process/test_pfr.py +++ b/tests/process/test_pfr.py @@ -60,14 +60,8 @@ def test_port_labels(self): """Test port label definitions.""" self.assertEqual(PFR.input_port_labels["C_in"], 0) self.assertEqual(PFR.input_port_labels["T_in"], 1) - pfr = PFR(N_cells=5) - self.assertEqual(pfr.output_port_labels["C_out"], 0) - self.assertEqual(pfr.output_port_labels["T_out"], 1) - # per-cell ports - self.assertEqual(pfr.output_port_labels["C_1"], 2) - self.assertEqual(pfr.output_port_labels["T_1"], 7) - self.assertEqual(pfr.output_port_labels["C_5"], 6) - self.assertEqual(pfr.output_port_labels["T_5"], 11) + self.assertEqual(PFR.output_port_labels["C_out"], 0) + self.assertEqual(PFR.output_port_labels["T_out"], 1) def test_state_size(self): """Test that state vector has correct size.""" From 68181b8d474e9679d04003c4da77e8719e4bf31c Mon Sep 17 00:00:00 2001 From: Milan Rother Date: Mon, 2 Mar 2026 15:16:12 +0100 Subject: [PATCH 4/4] Align example notebooks with pathsim style conventions --- docs/source/examples/cstr_reaction.ipynb | 34 +--- docs/source/examples/equation_of_state.ipynb | 107 +---------- docs/source/examples/flash_distillation.ipynb | 72 +------- docs/source/examples/heat_exchanger.ipynb | 14 +- .../examples/multicomponent_flash.ipynb | 167 +----------------- docs/source/examples/process_flowsheet.ipynb | 136 +------------- .../examples/valve_characteristics.ipynb | 87 +-------- .../examples/vapor_pressure_curves.ipynb | 62 +------ docs/source/examples/vle_calculation.ipynb | 90 +--------- docs/source/pathsim_docs.mplstyle | 74 ++++++++ 10 files changed, 121 insertions(+), 722 deletions(-) create mode 100644 docs/source/pathsim_docs.mplstyle diff --git a/docs/source/examples/cstr_reaction.ipynb b/docs/source/examples/cstr_reaction.ipynb index 918a9d6..203bc6f 100644 --- a/docs/source/examples/cstr_reaction.ipynb +++ b/docs/source/examples/cstr_reaction.ipynb @@ -27,14 +27,7 @@ "execution_count": null, "metadata": {}, "outputs": [], - "source": [ - "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 CSTR" - ] + "source": "import matplotlib.pyplot as plt\n\nplt.style.use('../pathsim_docs.mplstyle')\n\nfrom pathsim import Simulation, Connection\nfrom pathsim.blocks import Source, Scope\n\nfrom pathsim_chem.process import CSTR" }, { "cell_type": "markdown", @@ -48,7 +41,7 @@ "execution_count": null, "metadata": {}, "outputs": [], - "source": "cstr = CSTR(\n V=1.0, # reactor volume [m³]\n F=0.05, # volumetric flow rate [m³/s] -> tau = 20 s\n k0=1e6, # pre-exponential factor [1/s]\n Ea=40000.0, # activation energy [J/mol]\n n=1.0, # first-order reaction\n dH_rxn=-40000.0, # exothermic [J/mol]\n rho=1000.0, # density [kg/m³]\n Cp=4184.0, # heat capacity [J/(kg·K)]\n UA=800.0, # cooling jacket [W/K]\n C_A0=0.0, # start empty\n T0=300.0, # initial temperature [K]\n)" + "source": "reactor = CSTR(\n V=1.0, # reactor volume [m³]\n F=0.05, # volumetric flow rate [m³/s] -> tau = 20 s\n k0=1e6, # pre-exponential factor [1/s]\n Ea=40000.0, # activation energy [J/mol]\n n=1.0, # first-order reaction\n dH_rxn=-40000.0, # exothermic [J/mol]\n rho=1000.0, # density [kg/m³]\n Cp=4184.0, # heat capacity [J/(kg·K)]\n UA=800.0, # cooling jacket [W/K]\n C_A0=0.0, # start empty\n T0=300.0, # initial temperature [K]\n)" }, { "cell_type": "markdown", @@ -60,33 +53,14 @@ "execution_count": null, "metadata": {}, "outputs": [], - "source": "# Constant feed and coolant conditions\nC_feed = Source(func=lambda t: 1000.0) # feed concentration [mol/m³]\nT_feed = Source(func=lambda t: 320.0) # feed temperature [K]\nT_cool = Source(func=lambda t: 290.0) # coolant temperature [K]\n\nscp = Scope(labels=[\"C_A [mol/m³]\", \"T [K]\"])\n\nsim = Simulation(\n blocks=[C_feed, T_feed, T_cool, cstr, scp],\n connections=[\n Connection(C_feed, cstr), # C_in -> port 0\n Connection(T_feed, cstr[1]), # T_in -> port 1\n Connection(T_cool, cstr[2]), # T_c -> port 2\n Connection(cstr, scp), # C_out -> scope port 0\n Connection(cstr[1], scp[1]), # T_out -> scope port 1\n ],\n dt=0.1,\n)\n\nsim.run(200)" + "source": "# Constant feed and coolant conditions\nsrc_c = Source(lambda t: 1000.0) # feed concentration [mol/m³]\nsrc_t = Source(lambda t: 320.0) # feed temperature [K]\nsrc_tc = Source(lambda t: 290.0) # coolant temperature [K]\n\nsco = Scope(labels=[\"C_A [mol/m³]\", \"T [K]\"])\n\nsim = Simulation(\n [src_c, src_t, src_tc, reactor, sco],\n [\n Connection(src_c, reactor), # C_in\n Connection(src_t, reactor[1]), # T_in\n Connection(src_tc, reactor[2]), # T_c\n Connection(reactor, sco), # C_out\n Connection(reactor[1], sco[1]), # T_out\n ],\n dt=0.1,\n log=True,\n)\n\nsim.run(200)" }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], - "source": [ - "time, signals = scp.read()\n", - "\n", - "fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5))\n", - "\n", - "ax1.plot(time, signals[0])\n", - "ax1.set_xlabel(\"Time [s]\")\n", - "ax1.set_ylabel(\"Concentration [mol/m³]\")\n", - "ax1.set_title(\"Outlet Concentration\")\n", - "ax1.grid(True, alpha=0.3)\n", - "\n", - "ax2.plot(time, signals[1])\n", - "ax2.set_xlabel(\"Time [s]\")\n", - "ax2.set_ylabel(\"Temperature [K]\")\n", - "ax2.set_title(\"Reactor Temperature\")\n", - "ax2.grid(True, alpha=0.3)\n", - "\n", - "plt.tight_layout()\n", - "plt.show()" - ] + "source": "sco.plot(lw=1.5)\nplt.show()" }, { "cell_type": "markdown", diff --git a/docs/source/examples/equation_of_state.ipynb b/docs/source/examples/equation_of_state.ipynb index e04226d..9a54c1f 100644 --- a/docs/source/examples/equation_of_state.ipynb +++ b/docs/source/examples/equation_of_state.ipynb @@ -23,14 +23,7 @@ "execution_count": null, "metadata": {}, "outputs": [], - "source": [ - "import matplotlib.pyplot as plt\n", - "\n", - "from pathsim import Simulation, Connection\n", - "from pathsim.blocks import Source, Constant, Scope\n", - "\n", - "from pathsim_chem.thermodynamics import PengRobinson, RedlichKwongSoave" - ] + "source": "import matplotlib.pyplot as plt\n\nplt.style.use('../pathsim_docs.mplstyle')\n\nfrom pathsim import Simulation, Connection\nfrom pathsim.blocks import Source, Constant, Scope\n\nfrom pathsim_chem.thermodynamics import PengRobinson, RedlichKwongSoave" }, { "cell_type": "markdown", @@ -46,23 +39,7 @@ "execution_count": null, "metadata": {}, "outputs": [], - "source": [ - "# Critical properties of methane\n", - "Tc, Pc, omega = 190.6, 4.6e6, 0.011\n", - "\n", - "# EoS blocks\n", - "pr = PengRobinson(Tc=Tc, Pc=Pc, omega=omega)\n", - "rks = RedlichKwongSoave(Tc=Tc, Pc=Pc, omega=omega)\n", - "\n", - "# Fixed temperature, logarithmic pressure sweep\n", - "import numpy as np\n", - "T_const = Constant(250) # 250 K (above Tc, supercritical)\n", - "P_src = Source(func=lambda t: 10**(4 + t * 0.035)) # 10 kPa to ~30 MPa over 100s\n", - "\n", - "# Scopes: record Z from both EoS (output port 1)\n", - "scp_pr = Scope(labels=[\"Z_PR\"])\n", - "scp_rks = Scope(labels=[\"Z_RKS\"])" - ] + "source": "import numpy as np\n\n# Critical properties of methane\nTc, Pc, omega = 190.6, 4.6e6, 0.011\n\n# EoS blocks\npr = PengRobinson(Tc=Tc, Pc=Pc, omega=omega)\nrks = RedlichKwongSoave(Tc=Tc, Pc=Pc, omega=omega)\n\n# Fixed temperature, logarithmic pressure sweep\nsrc_t = Constant(250) # 250 K (above Tc, supercritical)\nsrc_p = Source(lambda t: 10**(4 + t * 0.035)) # 10 kPa to ~30 MPa over 100s\n\n# Scopes: record Z from both EoS (output port 1)\nsco_pr = Scope(labels=[\"Z_PR\"])\nsco_rks = Scope(labels=[\"Z_RKS\"])" }, { "cell_type": "markdown", @@ -78,46 +55,14 @@ "execution_count": null, "metadata": {}, "outputs": [], - "source": [ - "sim = Simulation(\n", - " blocks=[T_const, P_src, pr, rks, scp_pr, scp_rks],\n", - " connections=[\n", - " # Temperature -> both EoS (input port 0)\n", - " Connection(T_const, pr, rks),\n", - " # Pressure -> both EoS (input port 1)\n", - " Connection(P_src, pr[1], rks[1]),\n", - " # Z output (port 1) -> scopes\n", - " Connection(pr[1], scp_pr),\n", - " Connection(rks[1], scp_rks),\n", - " ],\n", - " dt=1.0,\n", - ")\n", - "\n", - "sim.run(100)" - ] + "source": "sim = Simulation(\n [src_t, src_p, pr, rks, sco_pr, sco_rks],\n [\n Connection(src_t, pr, rks), # T -> both EoS\n Connection(src_p, pr[1], rks[1]), # P -> both EoS\n Connection(pr[1], sco_pr), # Z_PR\n Connection(rks[1], sco_rks), # Z_RKS\n ],\n dt=1.0,\n log=True,\n)\n\nsim.run(100)" }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], - "source": [ - "time, Z_pr = scp_pr.read()\n", - "_, Z_rks = scp_rks.read()\n", - "P_vals = 10**(4 + time * 0.035) / 1e6 # MPa\n", - "\n", - "fig, ax = plt.subplots(figsize=(7, 5))\n", - "ax.semilogx(P_vals, Z_pr[0], label=\"Peng-Robinson\")\n", - "ax.semilogx(P_vals, Z_rks[0], \"--\", label=\"Soave-Redlich-Kwong\")\n", - "ax.axhline(1.0, color=\"gray\", linestyle=\"-.\", alpha=0.5, label=\"Ideal gas\")\n", - "ax.set_xlabel(\"Pressure [MPa]\")\n", - "ax.set_ylabel(\"Compressibility Factor Z\")\n", - "ax.set_title(\"Methane at T = 250 K\")\n", - "ax.legend()\n", - "ax.grid(True, alpha=0.3)\n", - "plt.tight_layout()\n", - "plt.show()" - ] + "source": "time, [Z_pr] = sco_pr.read()\n_, [Z_rks] = sco_rks.read()\nP_vals = 10**(4 + time * 0.035) / 1e6 # MPa\n\nfig, ax = plt.subplots(figsize=(7, 5))\nax.semilogx(P_vals, Z_pr, label=\"Peng-Robinson\")\nax.semilogx(P_vals, Z_rks, \"--\", label=\"Soave-Redlich-Kwong\")\nax.axhline(1.0, color=\"gray\", linestyle=\"-.\", alpha=0.5, label=\"Ideal gas\")\nax.set_xlabel(\"Pressure [MPa]\")\nax.set_ylabel(\"Compressibility Factor Z\")\nax.set_title(\"Methane at T = 250 K\")\nax.legend()\nax.grid(True, alpha=0.3)\nplt.tight_layout()\nplt.show()" }, { "cell_type": "markdown", @@ -140,52 +85,14 @@ "execution_count": null, "metadata": {}, "outputs": [], - "source": [ - "pr_mix = PengRobinson(\n", - " Tc=[190.6, 305.3],\n", - " Pc=[4.6e6, 4.872e6],\n", - " omega=[0.011, 0.099],\n", - " x=[0.7, 0.3],\n", - ")\n", - "\n", - "T_const2 = Constant(300)\n", - "P_src2 = Source(func=lambda t: 10**(4 + t * 0.035))\n", - "scp_mix = Scope(labels=[\"Z_mixture\"])\n", - "\n", - "sim_mix = Simulation(\n", - " blocks=[T_const2, P_src2, pr_mix, scp_mix],\n", - " connections=[\n", - " Connection(T_const2, pr_mix),\n", - " Connection(P_src2, pr_mix[1]),\n", - " Connection(pr_mix[1], scp_mix),\n", - " ],\n", - " dt=1.0,\n", - ")\n", - "\n", - "sim_mix.run(100)" - ] + "source": "pr_mix = PengRobinson(\n Tc=[190.6, 305.3],\n Pc=[4.6e6, 4.872e6],\n omega=[0.011, 0.099],\n x=[0.7, 0.3],\n)\n\nsrc_t2 = Constant(300)\nsrc_p2 = Source(lambda t: 10**(4 + t * 0.035))\nsco_mix = Scope(labels=[\"Z_mixture\"])\n\nsim_mix = Simulation(\n [src_t2, src_p2, pr_mix, sco_mix],\n [\n Connection(src_t2, pr_mix),\n Connection(src_p2, pr_mix[1]),\n Connection(pr_mix[1], sco_mix),\n ],\n dt=1.0,\n log=True,\n)\n\nsim_mix.run(100)" }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], - "source": [ - "time_m, Z_mix = scp_mix.read()\n", - "P_mix = 10**(4 + time_m * 0.035) / 1e6\n", - "\n", - "fig, ax = plt.subplots(figsize=(7, 5))\n", - "ax.semilogx(P_vals, Z_pr[0], label=\"Pure CH₄ (250 K)\")\n", - "ax.semilogx(P_mix, Z_mix[0], \"--\", label=\"70/30 CH₄-C₂H₆ (300 K)\")\n", - "ax.axhline(1.0, color=\"gray\", linestyle=\"-.\", alpha=0.5)\n", - "ax.set_xlabel(\"Pressure [MPa]\")\n", - "ax.set_ylabel(\"Compressibility Factor Z\")\n", - "ax.set_title(\"Peng-Robinson: Pure vs Mixture\")\n", - "ax.legend()\n", - "ax.grid(True, alpha=0.3)\n", - "plt.tight_layout()\n", - "plt.show()" - ] + "source": "time_m, [Z_mix] = sco_mix.read()\nP_mix = 10**(4 + time_m * 0.035) / 1e6\n\nfig, ax = plt.subplots(figsize=(7, 5))\nax.semilogx(P_vals, Z_pr, label=\"Pure CH₄ (250 K)\")\nax.semilogx(P_mix, Z_mix, \"--\", label=\"70/30 CH₄-C₂H₆ (300 K)\")\nax.axhline(1.0, color=\"gray\", linestyle=\"-.\", alpha=0.5)\nax.set_xlabel(\"Pressure [MPa]\")\nax.set_ylabel(\"Compressibility Factor Z\")\nax.set_title(\"Peng-Robinson: Pure vs Mixture\")\nax.legend()\nax.grid(True, alpha=0.3)\nplt.tight_layout()\nplt.show()" }, { "cell_type": "markdown", @@ -208,4 +115,4 @@ }, "nbformat": 4, "nbformat_minor": 4 -} +} \ No newline at end of file diff --git a/docs/source/examples/flash_distillation.ipynb b/docs/source/examples/flash_distillation.ipynb index 5468fee..b0c129d 100644 --- a/docs/source/examples/flash_distillation.ipynb +++ b/docs/source/examples/flash_distillation.ipynb @@ -27,14 +27,7 @@ "execution_count": null, "metadata": {}, "outputs": [], - "source": [ - "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 FlashDrum, DistillationTray" - ] + "source": "import matplotlib.pyplot as plt\n\nplt.style.use('../pathsim_docs.mplstyle')\n\nfrom pathsim import Simulation, Connection\nfrom pathsim.blocks import Source, Scope\n\nfrom pathsim_chem.process import FlashDrum, DistillationTray" }, { "cell_type": "markdown", @@ -46,14 +39,14 @@ "execution_count": null, "metadata": {}, "outputs": [], - "source": "flash = FlashDrum(holdup=100.0) # default benzene/toluene Antoine params\n\n# Feed: 10 mol/s, equimolar (z1 = 0.5), 1 atm\nF_src = Source(func=lambda t: 10.0)\nz_src = Source(func=lambda t: 0.5)\nT_src = Source(func=lambda t: 340.0 + t * 0.5) # ramp 340 -> 400 K\nP_src = Source(func=lambda t: 101325.0)\n\nscp = Scope(labels=[\"V_rate\", \"L_rate\", \"y_1 (benzene)\", \"x_1 (benzene)\"])\n\nsim = Simulation(\n blocks=[F_src, z_src, T_src, P_src, flash, scp],\n connections=[\n Connection(F_src, flash), # F -> port 0\n Connection(z_src, flash[1]), # z_1 -> port 1\n Connection(T_src, flash[2]), # T -> port 2\n Connection(P_src, flash[3]), # P -> port 3\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_1 -> scope 2\n Connection(flash[3], scp[3]), # x_1 -> scope 3\n ],\n dt=0.5,\n)\n\nsim.run(120)" + "source": "flash = FlashDrum(holdup=100.0) # default benzene/toluene Antoine params\n\n# Feed: 10 mol/s, equimolar (z1 = 0.5), 1 atm\nsrc_f = Source(lambda t: 10.0)\nsrc_z = Source(lambda t: 0.5)\nsrc_t = Source(lambda t: 340.0 + t * 0.5) # ramp 340 -> 400 K\nsrc_p = Source(lambda t: 101325.0)\n\nsco = Scope(labels=[\"V_rate\", \"L_rate\", \"y_1 (benzene)\", \"x_1 (benzene)\"])\n\nsim = Simulation(\n [src_f, src_z, src_t, src_p, flash, sco],\n [\n Connection(src_f, flash), # F\n Connection(src_z, flash[1]), # z_1\n Connection(src_t, flash[2]), # T\n Connection(src_p, flash[3]), # P\n Connection(flash, sco), # V_rate\n Connection(flash[1], sco[1]), # L_rate\n Connection(flash[2], sco[2]), # y_1\n Connection(flash[3], sco[3]), # x_1\n ],\n dt=0.5,\n log=True,\n)\n\nsim.run(120)" }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], - "source": "time, signals = scp.read()\nT_sweep = 340.0 + time * 0.5\n\nfig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5))\n\nax1.plot(T_sweep, signals[0], label=\"Vapor rate\")\nax1.plot(T_sweep, signals[1], label=\"Liquid rate\")\nax1.set_xlabel(\"Temperature [K]\")\nax1.set_ylabel(\"Flow rate [mol/s]\")\nax1.set_title(\"Flash Drum Flow Rates\")\nax1.legend()\nax1.grid(True, alpha=0.3)\n\nax2.plot(T_sweep, signals[2], label=r\"$y_1$ (vapor)\")\nax2.plot(T_sweep, signals[3], label=r\"$x_1$ (liquid)\")\nax2.axhline(0.5, color=\"gray\", ls=\"--\", alpha=0.4, label=\"Feed\")\nax2.set_xlabel(\"Temperature [K]\")\nax2.set_ylabel(\"Mole fraction (benzene)\")\nax2.set_title(\"VLE Compositions\")\nax2.legend()\nax2.grid(True, alpha=0.3)\n\nplt.tight_layout()\nplt.show()" + "source": "time, [V_rate, L_rate, y1, x1] = sco.read()\nT_sweep = 340.0 + time * 0.5\n\nfig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5))\n\nax1.plot(T_sweep, V_rate, label=\"Vapor rate\")\nax1.plot(T_sweep, L_rate, label=\"Liquid rate\")\nax1.set_xlabel(\"Temperature [K]\")\nax1.set_ylabel(\"Flow rate [mol/s]\")\nax1.set_title(\"Flash Drum Flow Rates\")\nax1.legend()\nax1.grid(True, alpha=0.3)\n\nax2.plot(T_sweep, y1, label=r\"$y_1$ (vapor)\")\nax2.plot(T_sweep, x1, label=r\"$x_1$ (liquid)\")\nax2.axhline(0.5, color=\"gray\", ls=\"--\", alpha=0.4, label=\"Feed\")\nax2.set_xlabel(\"Temperature [K]\")\nax2.set_ylabel(\"Mole fraction (benzene)\")\nax2.set_title(\"VLE Compositions\")\nax2.legend()\nax2.grid(True, alpha=0.3)\n\nplt.tight_layout()\nplt.show()" }, { "cell_type": "markdown", @@ -80,28 +73,7 @@ "execution_count": null, "metadata": {}, "outputs": [], - "source": [ - "# Column parameters\n", - "N_trays = 5\n", - "alpha = 2.5 # relative volatility\n", - "L = 5.0 # liquid flow rate [mol/s]\n", - "V = 5.0 # vapor flow rate [mol/s]\n", - "x_feed = 0.5 # feed composition (light component)\n", - "\n", - "# Create tray blocks (all start at x = 0.5)\n", - "trays = [DistillationTray(M=1.0, alpha=alpha, x0=0.5) for _ in range(N_trays)]\n", - "\n", - "# Liquid feed from condenser (enters tray 0 from above)\n", - "L_src = Source(func=lambda t: L)\n", - "x_top = Source(func=lambda t: 0.95) # reflux composition (nearly pure light)\n", - "\n", - "# Vapor feed from reboiler (enters tray N-1 from below)\n", - "V_src = Source(func=lambda t: V)\n", - "y_bot = Source(func=lambda t: 0.05) # reboiler vapor (nearly pure heavy)\n", - "\n", - "# Record composition on each tray\n", - "scp = Scope(labels=[f\"Tray {i+1}\" for i in range(N_trays)])" - ] + "source": "# Column parameters\nN_trays = 5\nalpha = 2.5 # relative volatility\nL = 5.0 # liquid flow rate [mol/s]\nV = 5.0 # vapor flow rate [mol/s]\n\n# Create tray blocks (all start at x = 0.5)\ntrays = [DistillationTray(M=1.0, alpha=alpha, x0=0.5) for _ in range(N_trays)]\n\n# Liquid feed from condenser (enters tray 0 from above)\nsrc_l = Source(lambda t: L)\nsrc_xtop = Source(lambda t: 0.95) # reflux composition\n\n# Vapor feed from reboiler (enters tray N-1 from below)\nsrc_v = Source(lambda t: V)\nsrc_ybot = Source(lambda t: 0.05) # reboiler vapor\n\n# Record composition on each tray\nsco = Scope(labels=[f\"Tray {i+1}\" for i in range(N_trays)])" }, { "cell_type": "markdown", @@ -115,46 +87,14 @@ "execution_count": null, "metadata": {}, "outputs": [], - "source": [ - "connections = []\n", - "\n", - "# Top tray (0): liquid from reflux\n", - "connections.append(Connection(L_src, trays[0])) # L_in -> port 0\n", - "connections.append(Connection(x_top, trays[0][1])) # x_in -> port 1\n", - "\n", - "# Bottom tray (N-1): vapor from reboiler\n", - "connections.append(Connection(V_src, trays[-1][2])) # V_in -> port 2\n", - "connections.append(Connection(y_bot, trays[-1][3])) # y_in -> port 3\n", - "\n", - "# Inter-tray connections\n", - "for i in range(N_trays - 1):\n", - " # Liquid flows down: tray i -> tray i+1\n", - " connections.append(Connection(trays[i], trays[i+1])) # L_out -> L_in\n", - " connections.append(Connection(trays[i][1], trays[i+1][1])) # x_out -> x_in\n", - "\n", - " # Vapor flows up: tray i+1 -> tray i\n", - " connections.append(Connection(trays[i+1][2], trays[i][2])) # V_out -> V_in\n", - " connections.append(Connection(trays[i+1][3], trays[i][3])) # y_out -> y_in\n", - "\n", - "# Connect each tray's liquid composition to scope\n", - "for i, tray in enumerate(trays):\n", - " connections.append(Connection(tray[1], scp[i])) # x_out -> scope\n", - "\n", - "sim = Simulation(\n", - " blocks=[L_src, x_top, V_src, y_bot, *trays, scp],\n", - " connections=connections,\n", - " dt=0.05,\n", - ")\n", - "\n", - "sim.run(30)" - ] + "source": "connections = []\n\n# Top tray (0): liquid from reflux\nconnections.append(Connection(src_l, trays[0])) # L_in\nconnections.append(Connection(src_xtop, trays[0][1])) # x_in\n\n# Bottom tray (N-1): vapor from reboiler\nconnections.append(Connection(src_v, trays[-1][2])) # V_in\nconnections.append(Connection(src_ybot, trays[-1][3])) # y_in\n\n# Inter-tray connections\nfor i in range(N_trays - 1):\n # Liquid flows down: tray i -> tray i+1\n connections.append(Connection(trays[i], trays[i+1])) # L_out -> L_in\n connections.append(Connection(trays[i][1], trays[i+1][1])) # x_out -> x_in\n\n # Vapor flows up: tray i+1 -> tray i\n connections.append(Connection(trays[i+1][2], trays[i][2])) # V_out -> V_in\n connections.append(Connection(trays[i+1][3], trays[i][3])) # y_out -> y_in\n\n# Connect each tray's liquid composition to scope\nfor i, tray in enumerate(trays):\n connections.append(Connection(tray[1], sco[i])) # x_out\n\nsim = Simulation(\n [src_l, src_xtop, src_v, src_ybot, *trays, sco],\n connections,\n dt=0.05,\n log=True,\n)\n\nsim.run(30)" }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], - "source": "time, signals = scp.read()\n\nfig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5))\n\n# Dynamic tray composition evolution\nfor i in range(N_trays):\n ax1.plot(time, signals[i], label=f\"Tray {i+1}\")\nax1.set_xlabel(\"Time [s]\")\nax1.set_ylabel(r\"$x$ (light component)\")\nax1.set_title(\"Tray Compositions Over Time\")\nax1.legend()\nax1.grid(True, alpha=0.3)\n\n# Steady-state composition profile (from scope data)\nx_profile = [signals[i][-1] for i in range(N_trays)]\ntray_nums = list(range(1, N_trays + 1))\n\nax2.plot(tray_nums, x_profile, \"o-\", markersize=8)\nax2.set_xlabel(\"Tray number (top to bottom)\")\nax2.set_ylabel(r\"$x$ (light component)\")\nax2.set_title(\"Composition Profile (Steady State)\")\nax2.grid(True, alpha=0.3)\n\nplt.tight_layout()\nplt.show()" + "source": "time, signals = sco.read()\n\nfig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5))\n\n# Dynamic tray composition evolution\nfor i in range(N_trays):\n ax1.plot(time, signals[i], label=f\"Tray {i+1}\")\nax1.set_xlabel(\"Time [s]\")\nax1.set_ylabel(r\"$x$ (light component)\")\nax1.set_title(\"Tray Compositions Over Time\")\nax1.legend()\nax1.grid(True, alpha=0.3)\n\n# Steady-state composition profile (from scope data)\nx_profile = [signals[i][-1] for i in range(N_trays)]\ntray_nums = list(range(1, N_trays + 1))\n\nax2.plot(tray_nums, x_profile, \"o-\", markersize=8)\nax2.set_xlabel(\"Tray number (top to bottom)\")\nax2.set_ylabel(r\"$x$ (light component)\")\nax2.set_title(\"Composition Profile (Steady State)\")\nax2.grid(True, alpha=0.3)\n\nplt.tight_layout()\nplt.show()" }, { "cell_type": "markdown", diff --git a/docs/source/examples/heat_exchanger.ipynb b/docs/source/examples/heat_exchanger.ipynb index 8d80903..c78234a 100644 --- a/docs/source/examples/heat_exchanger.ipynb +++ b/docs/source/examples/heat_exchanger.ipynb @@ -27,15 +27,7 @@ "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 HeatExchanger" - ] + "source": "import numpy as np\nimport matplotlib.pyplot as plt\n\nplt.style.use('../pathsim_docs.mplstyle')\n\nfrom pathsim import Simulation, Connection\nfrom pathsim.blocks import Source, Scope\n\nfrom pathsim_chem.process import HeatExchanger" }, { "cell_type": "markdown", @@ -61,14 +53,14 @@ "execution_count": null, "metadata": {}, "outputs": [], - "source": "T_h_in = Source(func=lambda t: 370.0) # hot inlet [K]\nT_c_in = Source(func=lambda t: 290.0) # cold inlet [K]\n\nscp = Scope(labels=[\"T_h_out [K]\", \"T_c_out [K]\"])\n\nsim = Simulation(\n blocks=[T_h_in, T_c_in, hx, scp],\n connections=[\n Connection(T_h_in, hx), # T_h_in -> port 0\n Connection(T_c_in, hx[1]), # T_c_in -> port 1\n Connection(hx, scp), # T_h_out -> scope port 0\n Connection(hx[1], scp[1]), # T_c_out -> scope port 1\n ],\n dt=0.02,\n)\n\nsim.run(200)" + "source": "src_th = Source(lambda t: 370.0) # hot inlet [K]\nsrc_tc = Source(lambda t: 290.0) # cold inlet [K]\n\nsco = Scope(labels=[\"T_h_out [K]\", \"T_c_out [K]\"])\n\nsim = Simulation(\n [src_th, src_tc, hx, sco],\n [\n Connection(src_th, hx), # T_h_in\n Connection(src_tc, hx[1]), # T_c_in\n Connection(hx, sco), # T_h_out\n Connection(hx[1], sco[1]), # T_c_out\n ],\n dt=0.02,\n log=True,\n)\n\nsim.run(200)" }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], - "source": "time, signals = scp.read()\n\nfig, ax = plt.subplots(figsize=(8, 5))\n\nax.plot(time, signals[0], label=\"Hot outlet\")\nax.plot(time, signals[1], label=\"Cold outlet\")\nax.axhline(370, color=\"gray\", ls=\"--\", alpha=0.4, label=\"Hot inlet\")\nax.axhline(290, color=\"gray\", ls=\"-.\", alpha=0.4, label=\"Cold inlet\")\nax.set_xlabel(\"Time [s]\")\nax.set_ylabel(\"Temperature [K]\")\nax.set_title(\"Heat Exchanger Outlet Temperatures\")\nax.legend()\nax.grid(True, alpha=0.3)\n\nplt.tight_layout()\nplt.show()" + "source": "sco.plot(lw=1.5)\nplt.show()" }, { "cell_type": "markdown", diff --git a/docs/source/examples/multicomponent_flash.ipynb b/docs/source/examples/multicomponent_flash.ipynb index ee5ed17..3d7da8c 100644 --- a/docs/source/examples/multicomponent_flash.ipynb +++ b/docs/source/examples/multicomponent_flash.ipynb @@ -25,15 +25,7 @@ "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" - ] + "source": "import numpy as np\nimport matplotlib.pyplot as plt\n\nplt.style.use('../pathsim_docs.mplstyle')\n\nfrom pathsim import Simulation, Connection\nfrom pathsim.blocks import Source, Scope\n\nfrom pathsim_chem.process import MultiComponentFlash" }, { "cell_type": "markdown", @@ -58,40 +50,7 @@ "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)" - ] + "source": "# Create the flash drum (3 components, BTX defaults)\nflash = MultiComponentFlash(N_comp=3, holdup=100.0)\n\n# Feed sources\nsrc_f = Source(lambda t: 10.0) # 10 mol/s\nsrc_z1 = Source(lambda t: 0.5) # 50% benzene\nsrc_z2 = Source(lambda t: 0.1) # 10% toluene (p-xylene = 40% inferred)\nsrc_t = Source(lambda t: 340.0 + t) # ramp 340 -> 420 K\nsrc_p = Source(lambda t: 101325.0) # 1 atm\n\n# Record all outputs: V_rate, L_rate, y_benzene, y_toluene, x_benzene, x_toluene\nsco = Scope(labels=[\"V_rate\", \"L_rate\", \"y_benz\", \"y_tol\", \"x_benz\", \"x_tol\"])\n\nsim = Simulation(\n [src_f, src_z1, src_z2, src_t, src_p, flash, sco],\n [\n Connection(src_f, flash), # F\n Connection(src_z1, flash[1]), # z_1 (benzene)\n Connection(src_z2, flash[2]), # z_2 (toluene)\n Connection(src_t, flash[3]), # T\n Connection(src_p, flash[4]), # P\n Connection(flash, sco), # V_rate\n Connection(flash[1], sco[1]), # L_rate\n Connection(flash[2], sco[2]), # y_benzene\n Connection(flash[3], sco[3]), # y_toluene\n Connection(flash[4], sco[4]), # x_benzene\n Connection(flash[5], sco[5]), # x_toluene\n ],\n dt=0.5,\n log=True,\n)\n\nsim.run(80)" }, { "cell_type": "markdown", @@ -108,55 +67,7 @@ "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()" - ] + "source": "time, [V_rate, L_rate, y_benz, y_tol, x_benz, x_tol] = sco.read()\nT = 340.0 + time # temperature axis\n\n# Infer p-xylene fractions\ny_xyl = 1.0 - y_benz - y_tol\nx_xyl = 1.0 - x_benz - x_tol\n\nfig, axes = plt.subplots(1, 3, figsize=(15, 5))\n\n# Flow rates\nax = axes[0]\nax.plot(T, V_rate, label=\"Vapor\")\nax.plot(T, L_rate, label=\"Liquid\")\nax.set_xlabel(\"Temperature [K]\")\nax.set_ylabel(\"Flow rate [mol/s]\")\nax.set_title(\"Flash Drum Flow Rates\")\nax.legend()\nax.grid(True, alpha=0.3)\n\n# Vapor compositions\nax = axes[1]\nax.plot(T, y_benz, label=\"Benzene\")\nax.plot(T, y_tol, label=\"Toluene\")\nax.plot(T, y_xyl, label=\"p-Xylene\")\nax.set_xlabel(\"Temperature [K]\")\nax.set_ylabel(\"Vapor mole fraction\")\nax.set_title(\"Vapor Composition\")\nax.legend()\nax.grid(True, alpha=0.3)\n\n# Liquid compositions\nax = axes[2]\nax.plot(T, x_benz, label=\"Benzene\")\nax.plot(T, x_tol, label=\"Toluene\")\nax.plot(T, x_xyl, label=\"p-Xylene\")\nax.set_xlabel(\"Temperature [K]\")\nax.set_ylabel(\"Liquid mole fraction\")\nax.set_title(\"Liquid Composition\")\nax.legend()\nax.grid(True, alpha=0.3)\n\nplt.tight_layout()\nplt.show()" }, { "cell_type": "markdown", @@ -174,24 +85,7 @@ "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()" - ] + "source": "fig, axes = plt.subplots(1, 3, figsize=(15, 5))\n\nfor 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\nplt.tight_layout()\nplt.show()" }, { "cell_type": "markdown", @@ -208,56 +102,7 @@ "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}\")" - ] + "source": "flash2 = MultiComponentFlash(N_comp=3, holdup=100.0)\n\nsrc_f2 = Source(lambda t: 10.0)\nsrc_z1b = Source(lambda t: 0.5)\nsrc_z2b = Source(lambda t: 0.1)\nsrc_t2 = Source(lambda t: 380.0) # fixed at 380 K (~107 °C)\nsrc_p2 = Source(lambda t: 101325.0)\n\nsco2 = Scope(labels=[\"V_rate\", \"L_rate\", \"y_benz\", \"y_tol\", \"x_benz\", \"x_tol\"])\n\nsim2 = Simulation(\n [src_f2, src_z1b, src_z2b, src_t2, src_p2, flash2, sco2],\n [\n Connection(src_f2, flash2),\n Connection(src_z1b, flash2[1]),\n Connection(src_z2b, flash2[2]),\n Connection(src_t2, flash2[3]),\n Connection(src_p2, flash2[4]),\n Connection(flash2, sco2),\n Connection(flash2[1], sco2[1]),\n Connection(flash2[2], sco2[2]),\n Connection(flash2[3], sco2[3]),\n Connection(flash2[4], sco2[4]),\n Connection(flash2[5], sco2[5]),\n ],\n dt=0.5,\n log=True,\n)\n\nsim2.run(100) # let it reach steady state\n\n_, [V_ss, L_ss, y_benz_ss, y_tol_ss, x_benz_ss, x_tol_ss] = sco2.read()\n\nprint(\"BTX Flash at 380 K, 1 atm\")\nprint(\"=\" * 40)\nprint(f\"{'':20s} {'Vapor':>10s} {'Liquid':>10s}\")\nprint(f\"{'-'*40}\")\nprint(f\"{'Flow rate [mol/s]':20s} {V_ss[-1]:10.3f} {L_ss[-1]:10.3f}\")\nprint(f\"{'Benzene':20s} {y_benz_ss[-1]:10.4f} {x_benz_ss[-1]:10.4f}\")\nprint(f\"{'Toluene':20s} {y_tol_ss[-1]:10.4f} {x_tol_ss[-1]:10.4f}\")\nprint(f\"{'p-Xylene':20s} {1-y_benz_ss[-1]-y_tol_ss[-1]:10.4f} {1-x_benz_ss[-1]-x_tol_ss[-1]:10.4f}\")" }, { "cell_type": "markdown", @@ -283,4 +128,4 @@ }, "nbformat": 4, "nbformat_minor": 4 -} +} \ No newline at end of file diff --git a/docs/source/examples/process_flowsheet.ipynb b/docs/source/examples/process_flowsheet.ipynb index a5178b3..fef294c 100644 --- a/docs/source/examples/process_flowsheet.ipynb +++ b/docs/source/examples/process_flowsheet.ipynb @@ -21,15 +21,7 @@ "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 Mixer, Heater, CSTR" - ] + "source": "import numpy as np\nimport matplotlib.pyplot as plt\n\nplt.style.use('../pathsim_docs.mplstyle')\n\nfrom pathsim import Simulation, Connection\nfrom pathsim.blocks import Source, Scope\n\nfrom pathsim_chem.process import Mixer, Heater, CSTR" }, { "cell_type": "markdown", @@ -48,44 +40,7 @@ "execution_count": null, "metadata": {}, "outputs": [], - "source": [ - "# Unit operations\n", - "mixer = Mixer()\n", - "heater = Heater(rho=1000.0, Cp=4184.0) # water-like fluid\n", - "cstr = CSTR(\n", - " V=1.0, # reactor volume [m³]\n", - " F=0.1, # total flow [m³/s] (will be overridden by mixer output)\n", - " k0=1e6, # pre-exponential factor [1/s]\n", - " Ea=50000.0, # activation energy [J/mol]\n", - " n=1.0, # first-order reaction\n", - " dH_rxn=-50000.0, # exothermic [J/mol]\n", - " rho=1000.0,\n", - " Cp=4184.0,\n", - " UA=500.0, # heat transfer [W/K]\n", - " C_A0=0.0, # start empty\n", - " T0=300.0, # start at 300 K\n", - ")\n", - "\n", - "# Feed sources\n", - "F1_src = Source(func=lambda t: 0.05) # fresh feed flow [m³/s]\n", - "T1_src = Source(func=lambda t: 300.0) # fresh feed temp [K]\n", - "F2_src = Source(func=lambda t: 0.05) # second stream flow [m³/s]\n", - "T2_src = Source(func=lambda t: 310.0) # second stream temp [K]\n", - "\n", - "# Heater duty [W]\n", - "Q_src = Source(func=lambda t: 200000.0)\n", - "\n", - "# Reactant concentration in combined feed [mol/m³]\n", - "C_in_src = Source(func=lambda t: 2.0)\n", - "\n", - "# Coolant temperature [K]\n", - "T_cool = Source(func=lambda t: 290.0)\n", - "\n", - "# Scopes for recording\n", - "scp_mix = Scope(labels=[\"F_mix\", \"T_mix\"])\n", - "scp_heat = Scope(labels=[\"F_heat\", \"T_heated\"])\n", - "scp_cstr = Scope(labels=[\"C_out\", \"T_reactor\"])" - ] + "source": "# Unit operations\nmixer = Mixer()\nheater = Heater(rho=1000.0, Cp=4184.0) # water-like fluid\ncstr = CSTR(\n V=1.0, # reactor volume [m³]\n F=0.1, # total flow [m³/s]\n k0=1e6, # pre-exponential factor [1/s]\n Ea=50000.0, # activation energy [J/mol]\n n=1.0, # first-order reaction\n dH_rxn=-50000.0, # exothermic [J/mol]\n rho=1000.0,\n Cp=4184.0,\n UA=500.0, # heat transfer [W/K]\n C_A0=0.0, # start empty\n T0=300.0, # start at 300 K\n)\n\n# Feed sources\nsrc_f1 = Source(lambda t: 0.05) # fresh feed flow [m³/s]\nsrc_t1 = Source(lambda t: 300.0) # fresh feed temp [K]\nsrc_f2 = Source(lambda t: 0.05) # second stream flow [m³/s]\nsrc_t2 = Source(lambda t: 310.0) # second stream temp [K]\n\n# Heater duty [W]\nsrc_q = Source(lambda t: 200000.0)\n\n# Reactant concentration in combined feed [mol/m³]\nsrc_cin = Source(lambda t: 2.0)\n\n# Coolant temperature [K]\nsrc_tc = Source(lambda t: 290.0)\n\n# Scopes for recording\nsco_mix = Scope(labels=[\"F_mix\", \"T_mix\"])\nsco_heat = Scope(labels=[\"F_heat\", \"T_heated\"])\nsco_cstr = Scope(labels=[\"C_out\", \"T_reactor\"])" }, { "cell_type": "markdown", @@ -109,47 +64,7 @@ "execution_count": null, "metadata": {}, "outputs": [], - "source": [ - "sim = Simulation(\n", - " blocks=[\n", - " F1_src, T1_src, F2_src, T2_src, Q_src, C_in_src, T_cool,\n", - " mixer, heater, cstr,\n", - " scp_mix, scp_heat, scp_cstr,\n", - " ],\n", - " connections=[\n", - " # Mixer inputs: (F_1, T_1, F_2, T_2)\n", - " Connection(F1_src, mixer),\n", - " Connection(T1_src, mixer[1]),\n", - " Connection(F2_src, mixer[2]),\n", - " Connection(T2_src, mixer[3]),\n", - "\n", - " # Mixer outputs -> Heater inputs: (F, T_in)\n", - " Connection(mixer, heater), # F_out -> F\n", - " Connection(mixer[1], heater[1]), # T_out -> T_in\n", - "\n", - " # Heater heat duty\n", - " Connection(Q_src, heater[2]), # Q\n", - "\n", - " # Heater output temperature -> CSTR inlet temperature\n", - " Connection(C_in_src, cstr), # C_in -> port 0\n", - " Connection(heater[1], cstr[1]), # T_heated -> T_in (port 1)\n", - "\n", - " # CSTR coolant\n", - " Connection(T_cool, cstr[2]), # T_c -> port 2\n", - "\n", - " # Recording\n", - " Connection(mixer, scp_mix),\n", - " Connection(mixer[1], scp_mix[1]),\n", - " Connection(heater, scp_heat),\n", - " Connection(heater[1], scp_heat[1]),\n", - " Connection(cstr, scp_cstr),\n", - " Connection(cstr[1], scp_cstr[1]),\n", - " ],\n", - " dt=0.1,\n", - ")\n", - "\n", - "sim.run(100)" - ] + "source": "sim = Simulation(\n [\n src_f1, src_t1, src_f2, src_t2, src_q, src_cin, src_tc,\n mixer, heater, cstr,\n sco_mix, sco_heat, sco_cstr,\n ],\n [\n # Mixer inputs: (F_1, T_1, F_2, T_2)\n Connection(src_f1, mixer),\n Connection(src_t1, mixer[1]),\n Connection(src_f2, mixer[2]),\n Connection(src_t2, mixer[3]),\n\n # Mixer outputs -> Heater inputs: (F, T_in)\n Connection(mixer, heater), # F_out -> F\n Connection(mixer[1], heater[1]), # T_out -> T_in\n\n # Heater heat duty\n Connection(src_q, heater[2]), # Q\n\n # Heater output temperature -> CSTR inlet temperature\n Connection(src_cin, cstr), # C_in\n Connection(heater[1], cstr[1]), # T_heated -> T_in\n\n # CSTR coolant\n Connection(src_tc, cstr[2]), # T_c\n\n # Recording\n Connection(mixer, sco_mix),\n Connection(mixer[1], sco_mix[1]),\n Connection(heater, sco_heat),\n Connection(heater[1], sco_heat[1]),\n Connection(cstr, sco_cstr),\n Connection(cstr[1], sco_cstr[1]),\n ],\n dt=0.1,\n log=True,\n)\n\nsim.run(100)" }, { "cell_type": "markdown", @@ -165,34 +80,7 @@ "execution_count": null, "metadata": {}, "outputs": [], - "source": [ - "t_mix, s_mix = scp_mix.read()\n", - "t_heat, s_heat = scp_heat.read()\n", - "t_cstr, s_cstr = scp_cstr.read()\n", - "\n", - "fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 5))\n", - "\n", - "# Temperature evolution\n", - "ax1.plot(t_mix, s_mix[1], label=\"After Mixer\")\n", - "ax1.plot(t_heat, s_heat[1], label=\"After Heater\")\n", - "ax1.plot(t_cstr, s_cstr[1], label=\"Reactor (CSTR)\")\n", - "ax1.axhline(290, color=\"blue\", ls=\"--\", alpha=0.3, label=\"Coolant\")\n", - "ax1.set_xlabel(\"Time [s]\")\n", - "ax1.set_ylabel(\"Temperature [K]\")\n", - "ax1.set_title(\"Temperature Profile\")\n", - "ax1.legend()\n", - "ax1.grid(True, alpha=0.3)\n", - "\n", - "# Reactor concentration\n", - "ax2.plot(t_cstr, s_cstr[0], color=\"tab:red\")\n", - "ax2.set_xlabel(\"Time [s]\")\n", - "ax2.set_ylabel(\"Concentration $C_A$ [mol/m³]\")\n", - "ax2.set_title(\"Reactor Outlet Concentration\")\n", - "ax2.grid(True, alpha=0.3)\n", - "\n", - "plt.tight_layout()\n", - "plt.show()" - ] + "source": "t_mix, [F_mix, T_mix] = sco_mix.read()\nt_heat, [F_heat, T_heated] = sco_heat.read()\nt_cstr, [C_out, T_reactor] = sco_cstr.read()\n\nfig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 5))\n\n# Temperature evolution\nax1.plot(t_mix, T_mix, label=\"After Mixer\")\nax1.plot(t_heat, T_heated, label=\"After Heater\")\nax1.plot(t_cstr, T_reactor, label=\"Reactor (CSTR)\")\nax1.axhline(290, color=\"blue\", ls=\"--\", alpha=0.3, label=\"Coolant\")\nax1.set_xlabel(\"Time [s]\")\nax1.set_ylabel(\"Temperature [K]\")\nax1.set_title(\"Temperature Profile\")\nax1.legend()\nax1.grid(True, alpha=0.3)\n\n# Reactor concentration\nax2.plot(t_cstr, C_out, color=\"tab:red\")\nax2.set_xlabel(\"Time [s]\")\nax2.set_ylabel(\"Concentration $C_A$ [mol/m³]\")\nax2.set_title(\"Reactor Outlet Concentration\")\nax2.grid(True, alpha=0.3)\n\nplt.tight_layout()\nplt.show()" }, { "cell_type": "markdown", @@ -206,19 +94,7 @@ "execution_count": null, "metadata": {}, "outputs": [], - "source": [ - "print(\"Process Steady State\")\n", - "print(\"=\" * 45)\n", - "print(f\"{'Unit':15s} {'Flow [m³/s]':>12s} {'T [K]':>10s}\")\n", - "print(\"-\" * 45)\n", - "print(f\"{'Fresh feed':15s} {'0.05':>12s} {'300.0':>10s}\")\n", - "print(f\"{'Second stream':15s} {'0.05':>12s} {'310.0':>10s}\")\n", - "print(f\"{'Mixer outlet':15s} {s_mix[0][-1]:12.4f} {s_mix[1][-1]:10.2f}\")\n", - "print(f\"{'Heater outlet':15s} {s_heat[0][-1]:12.4f} {s_heat[1][-1]:10.2f}\")\n", - "print(f\"{'CSTR outlet':15s} {'—':>12s} {s_cstr[1][-1]:10.2f}\")\n", - "print(f\"\\nReactor C_A = {s_cstr[0][-1]:.4f} mol/m³ (feed = 2.0 mol/m³)\")\n", - "print(f\"Conversion = {(1 - s_cstr[0][-1]/2.0)*100:.1f}%\")" - ] + "source": "print(\"Process Steady State\")\nprint(\"=\" * 45)\nprint(f\"{'Unit':15s} {'Flow [m³/s]':>12s} {'T [K]':>10s}\")\nprint(\"-\" * 45)\nprint(f\"{'Fresh feed':15s} {'0.05':>12s} {'300.0':>10s}\")\nprint(f\"{'Second stream':15s} {'0.05':>12s} {'310.0':>10s}\")\nprint(f\"{'Mixer outlet':15s} {F_mix[-1]:12.4f} {T_mix[-1]:10.2f}\")\nprint(f\"{'Heater outlet':15s} {F_heat[-1]:12.4f} {T_heated[-1]:10.2f}\")\nprint(f\"{'CSTR outlet':15s} {'—':>12s} {T_reactor[-1]:10.2f}\")\nprint(f\"\\nReactor C_A = {C_out[-1]:.4f} mol/m³ (feed = 2.0 mol/m³)\")\nprint(f\"Conversion = {(1 - C_out[-1]/2.0)*100:.1f}%\")" }, { "cell_type": "markdown", @@ -243,4 +119,4 @@ }, "nbformat": 4, "nbformat_minor": 4 -} +} \ No newline at end of file diff --git a/docs/source/examples/valve_characteristics.ipynb b/docs/source/examples/valve_characteristics.ipynb index 01e8122..5faaae2 100644 --- a/docs/source/examples/valve_characteristics.ipynb +++ b/docs/source/examples/valve_characteristics.ipynb @@ -20,15 +20,7 @@ "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 Valve" - ] + "source": "import numpy as np\nimport matplotlib.pyplot as plt\n\nplt.style.use('../pathsim_docs.mplstyle')\n\nfrom pathsim import Simulation, Connection\nfrom pathsim.blocks import Source, Scope\n\nfrom pathsim_chem.process import Valve" }, { "cell_type": "markdown", @@ -45,56 +37,14 @@ "execution_count": null, "metadata": {}, "outputs": [], - "source": [ - "results = {}\n", - "\n", - "for Cv in [0.5, 1.0, 2.0, 5.0]:\n", - " valve = Valve(Cv=Cv)\n", - "\n", - " P_in = Source(func=lambda t: 100000.0 + t * 10000.0) # 1 bar -> 10 bar\n", - " P_out = Source(func=lambda t: 100000.0) # 1 bar fixed\n", - " T_in = Source(func=lambda t: 350.0) # 350 K\n", - "\n", - " scp = Scope(labels=[\"F\", \"T_out\"])\n", - "\n", - " sim = Simulation(\n", - " blocks=[P_in, P_out, T_in, valve, scp],\n", - " connections=[\n", - " Connection(P_in, valve), # P_in -> port 0\n", - " Connection(P_out, valve[1]), # P_out -> port 1\n", - " Connection(T_in, valve[2]), # T_in -> port 2\n", - " Connection(valve, scp), # F -> scope 0\n", - " Connection(valve[1], scp[1]), # T_out -> scope 1\n", - " ],\n", - " dt=0.5,\n", - " )\n", - "\n", - " sim.run(90)\n", - " time, signals = scp.read()\n", - " dP = (100000.0 + time * 10000.0) - 100000.0 # pressure drop [Pa]\n", - " results[f\"Cv={Cv}\"] = (dP / 1e5, signals[0]) # convert dP to bar" - ] + "source": "results = {}\n\nfor Cv in [0.5, 1.0, 2.0, 5.0]:\n valve = Valve(Cv=Cv)\n\n src_pin = Source(lambda t: 100000.0 + t * 10000.0) # 1 bar -> 10 bar\n src_pout = Source(lambda t: 100000.0) # 1 bar fixed\n src_tin = Source(lambda t: 350.0) # 350 K\n\n sco = Scope(labels=[\"F\", \"T_out\"])\n\n sim = Simulation(\n [src_pin, src_pout, src_tin, valve, sco],\n [\n Connection(src_pin, valve), # P_in\n Connection(src_pout, valve[1]), # P_out\n Connection(src_tin, valve[2]), # T_in\n Connection(valve, sco), # F\n Connection(valve[1], sco[1]), # T_out\n ],\n dt=0.5,\n )\n\n sim.run(90)\n time, [F, T_out] = sco.read()\n dP = (100000.0 + time * 10000.0) - 100000.0 # pressure drop [Pa]\n results[f\"Cv={Cv}\"] = (dP / 1e5, F) # convert dP to bar" }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], - "source": [ - "fig, ax = plt.subplots(figsize=(10, 6))\n", - "\n", - "for label, (dP_bar, F) in results.items():\n", - " ax.plot(dP_bar, F, label=label)\n", - "\n", - "ax.set_xlabel(r\"Pressure drop $\\Delta P$ [bar]\")\n", - "ax.set_ylabel(\"Flow rate $F$\")\n", - "ax.set_title(\"Valve Characteristic Curves\")\n", - "ax.legend()\n", - "ax.grid(True, alpha=0.3)\n", - "\n", - "plt.tight_layout()\n", - "plt.show()" - ] + "source": "fig, ax = plt.subplots(figsize=(10, 6))\n\nfor label, (dP_bar, F) in results.items():\n ax.plot(dP_bar, F, label=label)\n\nax.set_xlabel(r\"Pressure drop $\\Delta P$ [bar]\")\nax.set_ylabel(\"Flow rate $F$\")\nax.set_title(\"Valve Characteristic Curves\")\nax.legend()\nax.grid(True, alpha=0.3)\n\nplt.tight_layout()\nplt.show()" }, { "cell_type": "markdown", @@ -119,34 +69,7 @@ "execution_count": null, "metadata": {}, "outputs": [], - "source": [ - "# Re-run one case and check T_out\n", - "valve = Valve(Cv=2.0)\n", - "\n", - "P_in = Source(func=lambda t: 100000.0 + t * 10000.0)\n", - "P_out = Source(func=lambda t: 100000.0)\n", - "T_in = Source(func=lambda t: 350.0)\n", - "\n", - "scp = Scope(labels=[\"F\", \"T_out\"])\n", - "\n", - "sim = Simulation(\n", - " blocks=[P_in, P_out, T_in, valve, scp],\n", - " connections=[\n", - " Connection(P_in, valve),\n", - " Connection(P_out, valve[1]),\n", - " Connection(T_in, valve[2]),\n", - " Connection(valve, scp),\n", - " Connection(valve[1], scp[1]),\n", - " ],\n", - " dt=0.5,\n", - ")\n", - "\n", - "sim.run(90)\n", - "time, signals = scp.read()\n", - "\n", - "print(f\"T_out range: {signals[1].min():.2f} K to {signals[1].max():.2f} K\")\n", - "print(f\"T_in = 350.00 K (constant) -> isenthalpic confirmed\")" - ] + "source": "# Re-run one case and check T_out\nvalve = Valve(Cv=2.0)\n\nsrc_pin = Source(lambda t: 100000.0 + t * 10000.0)\nsrc_pout = Source(lambda t: 100000.0)\nsrc_tin = Source(lambda t: 350.0)\n\nsco = Scope(labels=[\"F\", \"T_out\"])\n\nsim = Simulation(\n [src_pin, src_pout, src_tin, valve, sco],\n [\n Connection(src_pin, valve),\n Connection(src_pout, valve[1]),\n Connection(src_tin, valve[2]),\n Connection(valve, sco),\n Connection(valve[1], sco[1]),\n ],\n dt=0.5,\n)\n\nsim.run(90)\ntime, [F, T_out] = sco.read()\n\nprint(f\"T_out range: {T_out.min():.2f} K to {T_out.max():.2f} K\")\nprint(f\"T_in = 350.00 K (constant) -> isenthalpic confirmed\")" } ], "metadata": { @@ -162,4 +85,4 @@ }, "nbformat": 4, "nbformat_minor": 4 -} +} \ No newline at end of file diff --git a/docs/source/examples/vapor_pressure_curves.ipynb b/docs/source/examples/vapor_pressure_curves.ipynb index 0191f36..c2533e0 100644 --- a/docs/source/examples/vapor_pressure_curves.ipynb +++ b/docs/source/examples/vapor_pressure_curves.ipynb @@ -27,14 +27,7 @@ "execution_count": null, "metadata": {}, "outputs": [], - "source": [ - "import matplotlib.pyplot as plt\n", - "\n", - "from pathsim import Simulation, Connection\n", - "from pathsim.blocks import Source, Scope\n", - "\n", - "from pathsim_chem.thermodynamics import Antoine, Kirchhoff, Wagner" - ] + "source": "import matplotlib.pyplot as plt\n\nplt.style.use('../pathsim_docs.mplstyle')\n\nfrom pathsim import Simulation, Connection\nfrom pathsim.blocks import Source, Scope\n\nfrom pathsim_chem.thermodynamics import Antoine, Kirchhoff, Wagner" }, { "cell_type": "markdown", @@ -71,61 +64,14 @@ "execution_count": null, "metadata": {}, "outputs": [], - "source": [ - "# Temperature ramp: 300 K to 500 K over 200 seconds\n", - "T_src = Source(func=lambda t: 300 + t)\n", - "\n", - "# Scope to record all three vapor pressures\n", - "scp = Scope(labels=[\"Antoine\", \"Kirchhoff\", \"Wagner\"])\n", - "\n", - "sim = Simulation(\n", - " blocks=[T_src, antoine, kirchhoff, wagner, scp],\n", - " connections=[\n", - " Connection(T_src, antoine, kirchhoff, wagner),\n", - " Connection(antoine, scp),\n", - " Connection(kirchhoff, scp[1]),\n", - " Connection(wagner, scp[2]),\n", - " ],\n", - " dt=1.0,\n", - ")\n", - "\n", - "sim.run(200)" - ] + "source": "# Temperature ramp: 300 K to 500 K over 200 seconds\nsrc_t = Source(lambda t: 300 + t)\n\n# Scope to record all three vapor pressures\nsco = Scope(labels=[\"Antoine\", \"Kirchhoff\", \"Wagner\"])\n\nsim = Simulation(\n [src_t, antoine, kirchhoff, wagner, sco],\n [\n Connection(src_t, antoine, kirchhoff, wagner),\n Connection(antoine, sco),\n Connection(kirchhoff, sco[1]),\n Connection(wagner, sco[2]),\n ],\n dt=1.0,\n log=True,\n)\n\nsim.run(200)" }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], - "source": [ - "time, signals = scp.read()\n", - "\n", - "fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5))\n", - "\n", - "T_celsius = time + 300 - 273.15\n", - "labels = [\"Antoine\", \"Kirchhoff\", \"Wagner\"]\n", - "styles = [\"-\", \"--\", \":\"]\n", - "\n", - "for i, (label, style) in enumerate(zip(labels, styles)):\n", - " ax1.plot(T_celsius, signals[i] / 1e3, style, label=label)\n", - " ax2.semilogy(1000 / (time + 300), signals[i], style, label=label)\n", - "\n", - "ax1.axhline(101.325, color=\"gray\", linestyle=\"-.\", alpha=0.5, label=\"1 atm\")\n", - "ax1.set_xlabel(\"Temperature [°C]\")\n", - "ax1.set_ylabel(\"Vapor Pressure [kPa]\")\n", - "ax1.set_title(\"Vapor Pressure of Water\")\n", - "ax1.legend()\n", - "ax1.grid(True, alpha=0.3)\n", - "\n", - "ax2.set_xlabel(\"1000 / T [1/K]\")\n", - "ax2.set_ylabel(\"Vapor Pressure [Pa]\")\n", - "ax2.set_title(\"Clausius-Clapeyron Plot\")\n", - "ax2.legend()\n", - "ax2.grid(True, alpha=0.3)\n", - "\n", - "plt.tight_layout()\n", - "plt.show()" - ] + "source": "time, [P_antoine, P_kirchhoff, P_wagner] = sco.read()\n\nfig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5))\n\nT_celsius = time + 300 - 273.15\nlabels = [\"Antoine\", \"Kirchhoff\", \"Wagner\"]\nstyles = [\"-\", \"--\", \":\"]\npressures = [P_antoine, P_kirchhoff, P_wagner]\n\nfor P, label, style in zip(pressures, labels, styles):\n ax1.plot(T_celsius, P / 1e3, style, label=label)\n ax2.semilogy(1000 / (time + 300), P, style, label=label)\n\nax1.axhline(101.325, color=\"gray\", linestyle=\"-.\", alpha=0.5, label=\"1 atm\")\nax1.set_xlabel(\"Temperature [°C]\")\nax1.set_ylabel(\"Vapor Pressure [kPa]\")\nax1.set_title(\"Vapor Pressure of Water\")\nax1.legend()\nax1.grid(True, alpha=0.3)\n\nax2.set_xlabel(\"1000 / T [1/K]\")\nax2.set_ylabel(\"Vapor Pressure [Pa]\")\nax2.set_title(\"Clausius-Clapeyron Plot\")\nax2.legend()\nax2.grid(True, alpha=0.3)\n\nplt.tight_layout()\nplt.show()" }, { "cell_type": "markdown", @@ -148,4 +94,4 @@ }, "nbformat": 4, "nbformat_minor": 4 -} +} \ No newline at end of file diff --git a/docs/source/examples/vle_calculation.ipynb b/docs/source/examples/vle_calculation.ipynb index 99bf0cd..d70a465 100644 --- a/docs/source/examples/vle_calculation.ipynb +++ b/docs/source/examples/vle_calculation.ipynb @@ -25,14 +25,7 @@ "execution_count": null, "metadata": {}, "outputs": [], - "source": [ - "import matplotlib.pyplot as plt\n", - "\n", - "from pathsim import Simulation, Connection\n", - "from pathsim.blocks import Source, Scope, Function\n", - "\n", - "from pathsim_chem.thermodynamics import NRTL, Antoine" - ] + "source": "import matplotlib.pyplot as plt\n\nplt.style.use('../pathsim_docs.mplstyle')\n\nfrom pathsim import Simulation, Connection\nfrom pathsim.blocks import Source, Scope, Function\n\nfrom pathsim_chem.thermodynamics import NRTL, Antoine" }, { "cell_type": "markdown", @@ -48,22 +41,7 @@ "execution_count": null, "metadata": {}, "outputs": [], - "source": [ - "# Temperature ramp from 330 K to 380 K\n", - "T_src = Source(func=lambda t: 330 + t * 0.5)\n", - "\n", - "# Antoine vapor pressure correlations (ln form, T in K, P in Pa)\n", - "antoine_ethanol = Antoine(a0=23.5807, a1=3673.81, a2=-46.681)\n", - "antoine_water = Antoine(a0=23.2256, a1=3835.18, a2=-45.343)\n", - "\n", - "# NRTL activity coefficients for ethanol(1)-water(2)\n", - "nrtl = NRTL(\n", - " x=[0.4, 0.6],\n", - " a=[[0, -0.801], [3.458, 0]],\n", - " b=[[0, 200], [-100, 0]],\n", - " c=[[0, 0.3], [0.3, 0]],\n", - ")" - ] + "source": "# Temperature ramp from 330 K to 380 K\nsrc_t = Source(lambda t: 330 + t * 0.5)\n\n# Antoine vapor pressure correlations (ln form, T in K, P in Pa)\nantoine_ethanol = Antoine(a0=23.5807, a1=3673.81, a2=-46.681)\nantoine_water = Antoine(a0=23.2256, a1=3835.18, a2=-45.343)\n\n# NRTL activity coefficients for ethanol(1)-water(2)\nnrtl = NRTL(\n x=[0.4, 0.6],\n a=[[0, -0.801], [3.458, 0]],\n b=[[0, 200], [-100, 0]],\n c=[[0, 0.3], [0.3, 0]],\n)" }, { "cell_type": "markdown", @@ -79,18 +57,7 @@ "execution_count": null, "metadata": {}, "outputs": [], - "source": [ - "# Compute bubble pressure from gamma and Psat values\n", - "x1, x2 = 0.4, 0.6\n", - "\n", - "bubble = Function(\n", - " func=lambda g1, g2, P1, P2: x1 * g1 * P1 + x2 * g2 * P2\n", - ")\n", - "\n", - "# Scopes to record activity coefficients and bubble pressure\n", - "scp_gamma = Scope(labels=[\"gamma_ethanol\", \"gamma_water\"])\n", - "scp_bubble = Scope(labels=[\"P_bubble\"])" - ] + "source": "# Compute bubble pressure from gamma and Psat values\nx1, x2 = 0.4, 0.6\n\nbubble = Function(lambda g1, g2, P1, P2: x1 * g1 * P1 + x2 * g2 * P2)\n\n# Scopes to record activity coefficients and bubble pressure\nsco_gamma = Scope(labels=[\"gamma_ethanol\", \"gamma_water\"])\nsco_bubble = Scope(labels=[\"P_bubble\"])" }, { "cell_type": "markdown", @@ -106,59 +73,14 @@ "execution_count": null, "metadata": {}, "outputs": [], - "source": [ - "sim = Simulation(\n", - " blocks=[T_src, nrtl, antoine_ethanol, antoine_water, bubble, scp_gamma, scp_bubble],\n", - " connections=[\n", - " # Temperature drives all thermodynamic blocks\n", - " Connection(T_src, nrtl, antoine_ethanol, antoine_water),\n", - " # NRTL outputs -> scope and bubble pressure\n", - " Connection(nrtl, scp_gamma, bubble),\n", - " Connection(nrtl[1], scp_gamma[1], bubble[1]),\n", - " # Antoine outputs -> bubble pressure\n", - " Connection(antoine_ethanol, bubble[2]),\n", - " Connection(antoine_water, bubble[3]),\n", - " # Bubble pressure -> scope\n", - " Connection(bubble, scp_bubble),\n", - " ],\n", - " dt=0.5,\n", - ")\n", - "\n", - "sim.run(100)" - ] + "source": "sim = Simulation(\n [src_t, nrtl, antoine_ethanol, antoine_water, bubble, sco_gamma, sco_bubble],\n [\n Connection(src_t, nrtl, antoine_ethanol, antoine_water),\n Connection(nrtl, sco_gamma, bubble),\n Connection(nrtl[1], sco_gamma[1], bubble[1]),\n Connection(antoine_ethanol, bubble[2]),\n Connection(antoine_water, bubble[3]),\n Connection(bubble, sco_bubble),\n ],\n dt=0.5,\n log=True,\n)\n\nsim.run(100)" }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], - "source": [ - "time_g, gamma = scp_gamma.read()\n", - "time_b, P_bub = scp_bubble.read()\n", - "T_range = 330 + time_g * 0.5\n", - "\n", - "fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5))\n", - "\n", - "ax1.plot(T_range - 273.15, gamma[0], label=r\"$\\gamma_{\\mathrm{ethanol}}$\")\n", - "ax1.plot(T_range - 273.15, gamma[1], label=r\"$\\gamma_{\\mathrm{water}}$\")\n", - "ax1.axhline(1.0, color=\"gray\", linestyle=\"--\", alpha=0.5)\n", - "ax1.set_xlabel(\"Temperature [°C]\")\n", - "ax1.set_ylabel(r\"$\\gamma_i$\")\n", - "ax1.set_title(\"NRTL Activity Coefficients\")\n", - "ax1.legend()\n", - "ax1.grid(True, alpha=0.3)\n", - "\n", - "ax2.plot(T_range - 273.15, P_bub[0] / 1e3)\n", - "ax2.axhline(101.325, color=\"gray\", linestyle=\"-.\", alpha=0.5, label=\"1 atm\")\n", - "ax2.set_xlabel(\"Temperature [°C]\")\n", - "ax2.set_ylabel(\"Bubble Pressure [kPa]\")\n", - "ax2.set_title(\"Bubble Pressure (40/60 Ethanol-Water)\")\n", - "ax2.legend()\n", - "ax2.grid(True, alpha=0.3)\n", - "\n", - "plt.tight_layout()\n", - "plt.show()" - ] + "source": "time, [gamma_eth, gamma_w] = sco_gamma.read()\n_, [P_bub] = sco_bubble.read()\nT_range = 330 + time * 0.5\n\nfig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5))\n\nax1.plot(T_range - 273.15, gamma_eth, label=r\"$\\gamma_{\\mathrm{ethanol}}$\")\nax1.plot(T_range - 273.15, gamma_w, label=r\"$\\gamma_{\\mathrm{water}}$\")\nax1.axhline(1.0, color=\"gray\", linestyle=\"--\", alpha=0.5)\nax1.set_xlabel(\"Temperature [°C]\")\nax1.set_ylabel(r\"$\\gamma_i$\")\nax1.set_title(\"NRTL Activity Coefficients\")\nax1.legend()\nax1.grid(True, alpha=0.3)\n\nax2.plot(T_range - 273.15, P_bub / 1e3)\nax2.axhline(101.325, color=\"gray\", linestyle=\"-.\", alpha=0.5, label=\"1 atm\")\nax2.set_xlabel(\"Temperature [°C]\")\nax2.set_ylabel(\"Bubble Pressure [kPa]\")\nax2.set_title(\"Bubble Pressure (40/60 Ethanol-Water)\")\nax2.legend()\nax2.grid(True, alpha=0.3)\n\nplt.tight_layout()\nplt.show()" }, { "cell_type": "markdown", @@ -181,4 +103,4 @@ }, "nbformat": 4, "nbformat_minor": 4 -} +} \ No newline at end of file diff --git a/docs/source/pathsim_docs.mplstyle b/docs/source/pathsim_docs.mplstyle new file mode 100644 index 0000000..d47456f --- /dev/null +++ b/docs/source/pathsim_docs.mplstyle @@ -0,0 +1,74 @@ +# PathSim documentation matplotlib style +# Optimized for light/dark mode compatibility with transparent backgrounds + +# Figure +figure.figsize: 8, 4 +figure.dpi: 300 +figure.facecolor: none +figure.edgecolor: none +savefig.facecolor: none +savefig.edgecolor: none +savefig.transparent: True +savefig.bbox: tight +savefig.pad_inches: 0.1 + +# Axes +axes.facecolor: none +axes.edgecolor: "#7F7F7F" +axes.linewidth: 1.8 +axes.grid: False +axes.axisbelow: True +axes.labelsize: 11 +axes.titlesize: 12 +axes.labelcolor: "#7F7F7F" +axes.titleweight: bold +axes.spines.top: False +axes.spines.right: False + +# Grid +grid.color: "#7F7F7F" +grid.linestyle: -- +grid.linewidth: 0.8 +grid.alpha: 0.6 + +# Lines +lines.linewidth: 2.0 +lines.markersize: 8 +lines.markeredgewidth: 1.5 +lines.antialiased: True + +# Patches (for bars, etc.) +patch.linewidth: 1.0 +patch.facecolor: 4C72B0 +patch.edgecolor: none +patch.antialiased: True + +# Font +font.family: sans-serif +font.sans-serif: DejaVu Sans, Arial, Helvetica, sans-serif +font.size: 10 +text.color: "#7F7F7F" + +# Legend +legend.frameon: False +legend.framealpha: 0.0 +legend.fancybox: False +legend.facecolor: none +legend.edgecolor: none +legend.fontsize: 10 +legend.loc: best + +# Ticks +xtick.labelsize: 10 +ytick.labelsize: 10 +xtick.color: "#7F7F7F" +ytick.color: "#7F7F7F" +xtick.direction: out +ytick.direction: out +xtick.major.width: 1.8 +ytick.major.width: 1.8 +xtick.minor.width: 1.2 +ytick.minor.width: 1.2 + +# Color cycle - colors that work well with grey text +axes.prop_cycle: cycler('color', ['e41a1c', '377eb8', '4daf4a', '984ea3', 'ff7f00'])