|
| 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 | + ) |
0 commit comments