Skip to content

Commit 6ad486c

Browse files
authored
Merge pull request #18 from pathsim/feature/process-units
process units
2 parents 28f4849 + 624d09f commit 6ad486c

11 files changed

Lines changed: 1186 additions & 0 deletions

File tree

src/pathsim_chem/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,4 @@
1515
#for direct block import from main package
1616
from .tritium import *
1717
from .thermodynamics import *
18+
from .process import *
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
#########################################################################################
2+
##
3+
## Dynamic Unit Operation Models for Chemical Processes
4+
##
5+
#########################################################################################
6+
7+
from .cstr import *
8+
from .heat_exchanger import *
9+
from .flash_drum import *
10+
from .distillation import *

src/pathsim_chem/process/cstr.py

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
#########################################################################################
2+
##
3+
## Continuous Stirred-Tank Reactor (CSTR) Block
4+
##
5+
#########################################################################################
6+
7+
# IMPORTS ===============================================================================
8+
9+
import numpy as np
10+
11+
from pathsim.blocks.dynsys import DynamicalSystem
12+
13+
# CONSTANTS =============================================================================
14+
15+
R_GAS = 8.314 # universal gas constant [J/(mol·K)]
16+
17+
# BLOCKS ================================================================================
18+
19+
class CSTR(DynamicalSystem):
20+
"""Continuous stirred-tank reactor with Arrhenius kinetics and energy balance.
21+
22+
Models a well-mixed tank where a single reaction A -> products occurs with
23+
nth-order kinetics. The reaction rate follows the Arrhenius temperature
24+
dependence. An external coolant jacket provides or removes heat.
25+
26+
Mathematical Formulation
27+
-------------------------
28+
The state vector is :math:`[C_A, T]` where :math:`C_A` is the concentration
29+
of species A and :math:`T` is the reactor temperature.
30+
31+
.. math::
32+
33+
\\frac{dC_A}{dt} = \\frac{C_{A,in} - C_A}{\\tau} - k(T) \\, C_A^n
34+
35+
.. math::
36+
37+
\\frac{dT}{dt} = \\frac{T_{in} - T}{\\tau}
38+
+ \\frac{(-\\Delta H_{rxn})}{\\rho \\, C_p} \\, k(T) \\, C_A^n
39+
+ \\frac{UA}{\\rho \\, C_p \\, V} \\, (T_c - T)
40+
41+
where the Arrhenius rate constant is:
42+
43+
.. math::
44+
45+
k(T) = k_0 \\, \\exp\\!\\left(-\\frac{E_a}{R \\, T}\\right)
46+
47+
and the residence time is :math:`\\tau = V / F`.
48+
49+
Parameters
50+
----------
51+
V : float
52+
Reactor volume [m³].
53+
F : float
54+
Volumetric flow rate [m³/s].
55+
k0 : float
56+
Pre-exponential Arrhenius factor [1/s for n=1, (m³/mol)^(n-1)/s].
57+
Ea : float
58+
Activation energy [J/mol].
59+
n : float
60+
Reaction order with respect to species A [-].
61+
dH_rxn : float
62+
Heat of reaction [J/mol]. Negative for exothermic reactions.
63+
rho : float
64+
Fluid density [kg/m³].
65+
Cp : float
66+
Fluid heat capacity [J/(kg·K)].
67+
UA : float
68+
Overall heat transfer coefficient times area [W/K].
69+
C_A0 : float
70+
Initial concentration of A [mol/m³].
71+
T0 : float
72+
Initial reactor temperature [K].
73+
"""
74+
75+
input_port_labels = {
76+
"C_in": 0,
77+
"T_in": 1,
78+
"T_c": 2,
79+
}
80+
81+
output_port_labels = {
82+
"C_out": 0,
83+
"T_out": 1,
84+
}
85+
86+
def __init__(self, V=1.0, F=0.1, k0=1e6, Ea=50000.0, n=1.0,
87+
dH_rxn=-50000.0, rho=1000.0, Cp=4184.0, UA=500.0,
88+
C_A0=0.0, T0=300.0):
89+
90+
# input validation
91+
if V <= 0:
92+
raise ValueError(f"'V' must be positive but is {V}")
93+
if F <= 0:
94+
raise ValueError(f"'F' must be positive but is {F}")
95+
if rho <= 0:
96+
raise ValueError(f"'rho' must be positive but is {rho}")
97+
if Cp <= 0:
98+
raise ValueError(f"'Cp' must be positive but is {Cp}")
99+
100+
# store parameters
101+
self.V = V
102+
self.F = F
103+
self.k0 = k0
104+
self.Ea = Ea
105+
self.n = n
106+
self.dH_rxn = dH_rxn
107+
self.rho = rho
108+
self.Cp = Cp
109+
self.UA = UA
110+
111+
# rhs of CSTR ode system
112+
def _fn_d(x, u, t):
113+
C_A, T = x
114+
C_A_in, T_in, T_c = u
115+
116+
tau = self.V / self.F
117+
k = self.k0 * np.exp(-self.Ea / (R_GAS * T))
118+
r = k * C_A**self.n
119+
rcp = (-self.dH_rxn) / (self.rho * self.Cp)
120+
ua_term = self.UA / (self.rho * self.Cp * self.V)
121+
122+
dC_A = (C_A_in - C_A) / tau - r
123+
dT = (T_in - T) / tau + rcp * r + ua_term * (T_c - T)
124+
125+
return np.array([dC_A, dT])
126+
127+
# jacobian of rhs wrt state [C_A, T]
128+
def _jc_d(x, u, t):
129+
C_A, T = x
130+
131+
tau = self.V / self.F
132+
k = self.k0 * np.exp(-self.Ea / (R_GAS * T))
133+
dk_dT = k * self.Ea / (R_GAS * T**2)
134+
135+
dr_dCA = k * self.n * C_A**(self.n - 1) if C_A > 0 else 0.0
136+
dr_dT = dk_dT * C_A**self.n
137+
138+
rcp = (-self.dH_rxn) / (self.rho * self.Cp)
139+
ua_term = self.UA / (self.rho * self.Cp * self.V)
140+
141+
return np.array([
142+
[-1.0/tau - dr_dCA, -dr_dT],
143+
[rcp * dr_dCA, -1.0/tau + rcp * dr_dT - ua_term]
144+
])
145+
146+
# output function: well-mixed => outlet = state
147+
def _fn_a(x, u, t):
148+
return x.copy()
149+
150+
super().__init__(
151+
func_dyn=_fn_d,
152+
jac_dyn=_jc_d,
153+
func_alg=_fn_a,
154+
initial_value=np.array([C_A0, T0]),
155+
)
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
#########################################################################################
2+
##
3+
## Single Equilibrium Distillation Tray Block
4+
##
5+
#########################################################################################
6+
7+
# IMPORTS ===============================================================================
8+
9+
import numpy as np
10+
11+
from pathsim.blocks.dynsys import DynamicalSystem
12+
13+
# BLOCKS ================================================================================
14+
15+
class DistillationTray(DynamicalSystem):
16+
"""Single equilibrium distillation tray with constant molar overflow.
17+
18+
Models one tray of a distillation column under the constant molar
19+
overflow (CMO) assumption. Liquid flows down from the tray above,
20+
vapor rises from the tray below. VLE is computed using constant
21+
relative volatility.
22+
23+
Multiple trays can be wired in series via ``Connection`` objects to
24+
build a full distillation column.
25+
26+
Mathematical Formulation
27+
-------------------------
28+
For a binary system with mole fraction :math:`x` of the light component
29+
on the tray:
30+
31+
.. math::
32+
33+
M \\frac{dx}{dt} = L_{in} \\, x_{in} + V_{in} \\, y_{in}
34+
- L_{out} \\, x - V_{out} \\, y
35+
36+
where VLE with constant relative volatility :math:`\\alpha` gives:
37+
38+
.. math::
39+
40+
y = \\frac{\\alpha \\, x}{1 + (\\alpha - 1) \\, x}
41+
42+
Under CMO: :math:`L_{out} = L_{in}` and :math:`V_{out} = V_{in}`.
43+
44+
Parameters
45+
----------
46+
M : float
47+
Liquid holdup on the tray [mol].
48+
alpha : float
49+
Relative volatility of light to heavy component [-].
50+
x0 : float
51+
Initial liquid mole fraction of light component [-].
52+
"""
53+
54+
input_port_labels = {
55+
"L_in": 0,
56+
"x_in": 1,
57+
"V_in": 2,
58+
"y_in": 3,
59+
}
60+
61+
output_port_labels = {
62+
"L_out": 0,
63+
"x_out": 1,
64+
"V_out": 2,
65+
"y_out": 3,
66+
}
67+
68+
def __init__(self, M=1.0, alpha=2.5, x0=0.5):
69+
70+
# input validation
71+
if M <= 0:
72+
raise ValueError(f"'M' must be positive but is {M}")
73+
if alpha <= 0:
74+
raise ValueError(f"'alpha' must be positive but is {alpha}")
75+
if not 0.0 <= x0 <= 1.0:
76+
raise ValueError(f"'x0' must be in [0, 1] but is {x0}")
77+
78+
self.M = M
79+
self.alpha = alpha
80+
81+
# VLE: y = alpha*x / (1 + (alpha-1)*x)
82+
def _vle(x_val):
83+
return self.alpha * x_val / (1.0 + (self.alpha - 1.0) * x_val)
84+
85+
# rhs of tray ode
86+
def _fn_d(x, u, t):
87+
L_in, x_in, V_in, y_in = u
88+
89+
x_tray = x[0]
90+
y_tray = _vle(x_tray)
91+
92+
# CMO: L_out = L_in, V_out = V_in
93+
dx = (L_in * x_in + V_in * y_in - L_in * x_tray - V_in * y_tray) / self.M
94+
return np.array([dx])
95+
96+
# jacobian wrt x
97+
def _jc_d(x, u, t):
98+
L_in, x_in, V_in, y_in = u
99+
x_tray = x[0]
100+
101+
# dy/dx = alpha / (1 + (alpha-1)*x)^2
102+
denom = (1.0 + (self.alpha - 1.0) * x_tray)**2
103+
dy_dx = self.alpha / denom
104+
105+
return np.array([[-L_in / self.M - V_in * dy_dx / self.M]])
106+
107+
# output: L_out, x, V_out, y (CMO: pass-through flows)
108+
def _fn_a(x, u, t):
109+
L_in, x_in, V_in, y_in = u
110+
x_tray = x[0]
111+
y_tray = _vle(x_tray)
112+
return np.array([L_in, x_tray, V_in, y_tray])
113+
114+
super().__init__(
115+
func_dyn=_fn_d,
116+
jac_dyn=_jc_d,
117+
func_alg=_fn_a,
118+
initial_value=np.array([x0]),
119+
)

0 commit comments

Comments
 (0)