systems.builtin.deterministic.discrete.DiscreteCSTR

systems.builtin.deterministic.discrete.DiscreteCSTR(*args, **kwargs)
Discrete-time Continuous Stirred-Tank Reactor (CSTR) with cooling jacket.

Physical System:

A continuous flow reactor where reactant A converts to product B in
an exothermic reaction, modeled in discrete time with periodic sampling
and control actuation.

Unlike batch reactors, CSTRs operate at steady state with continuous
feed and product removal. The discrete-time formulation is appropriate
for digital control systems with:
- Periodic concentration measurements (e.g., via online analyzers)
- Discrete temperature sensor readings
- Digital control actuation of cooling jacket

Key features:
- Continuous flow: Feed enters, product exits at same rate
- Perfect mixing: Uniform concentration and temperature
- Exothermic reaction: Heat generation from reaction
- Jacket cooling: Heat removal to maintain temperature
- Discrete measurements and control updates

The reactor can exhibit:
- Multiple steady states (low/high conversion)
- Oscillatory behavior (limit cycles)
- Thermal runaway (if cooling insufficient)
- Complex bifurcations as parameters vary

State Space:

State: x[k] = [Cₐ[k], T[k]]
    Concentration state:
    - Cₐ: Concentration of reactant A in reactor [mol/L]
      * Lower than feed concentration due to reaction
      * Cₐ,feed > Cₐ > 0
      * Steady-state value depends on temperature and residence time
      * High Cₐ → low conversion (inefficient)
      * Low Cₐ → high conversion (efficient but expensive cooling)

    Temperature state:
    - T: Reactor temperature [K]
      * Higher than feed due to exothermic reaction
      * T > T_feed (for exothermic reactions)
      * Critical state: affects reaction rate exponentially
      * Small T change → large rate change (Arrhenius)
      * Must be controlled to prevent runaway

Control: u[k] = [T_jacket[k]]
    - T_jacket: Cooling jacket temperature [K]
      * Manipulated variable for temperature control
      * Typically T_jacket < T (removing heat)
      * Can be T_jacket > T for startup heating
      * Typical range: 280-340 K
      * Physical limits: chiller/heater capacity

Output: y[k] = [Cₐ[k], T[k]]
    - Full state measurement
    - In practice:
      * Cₐ measured via online analyzer (GC, HPLC, spectroscopy)
      * T measured via thermocouple or RTD
      * Both have sampling delays and noise

Dynamics:

The discrete-time dynamics use Euler discretization:

    Cₐ[k+1] = Cₐ[k] + dt·[(F/V)·(Cₐ,feed - Cₐ[k]) - r[k]]
    T[k+1] = T[k] + dt·[(F/V)·(T_feed - T[k]) + (-ΔH/ρCₚ)·r[k] + UA/(VρCₚ)·(T_jacket[k] - T[k])]

**Reaction Rate (Arrhenius kinetics)**:
    r[k] = k₀·Cₐ[k]·exp(-E/T[k])  [mol/(L·s)]

where:
- k₀: Pre-exponential factor [1/s]
- E: Activation energy [K] (using Eₐ/R as temperature)
- exp(-E/T): Arrhenius temperature dependence

**Physical Interpretation**:

Material Balance:
- (F/V)·(Cₐ,feed - Cₐ): Convective in/out (dilution)
- F/V = 1/τ: Inverse residence time [1/s]
- τ = V/F: Average time molecule spends in reactor [s]
- -r: Consumption by reaction
- At steady state: inflow - outflow - reaction = 0

Energy Balance:
- (F/V)·(T_feed - T): Convective heat in/out
- (-ΔH/ρCₚ)·r: Heat generation from reaction
  * ΔH < 0 (exothermic) → heat generation
  * |ΔH| large → strong thermal coupling
- UA/(VρCₚ)·(T_jacket - T): Heat removal via jacket
  * UA: Overall heat transfer coefficient × area
  * Larger UA → better temperature control
  * T_jacket < T → cooling (typical)

**Nonlinear Coupling**:
1. Temperature affects reaction rate exponentially (Arrhenius)
2. Reaction generates heat (thermal feedback)
3. High T → fast reaction → more heat → higher T (runaway risk)
4. Cooling must balance heat generation for stability

Parameters:

F : float, default=100.0
    Volumetric flow rate [L/s]
    Higher F → shorter residence time → lower conversion
    Lower F → longer residence time → higher conversion

V : float, default=100.0
    Reactor volume [L]
    Determines residence time τ = V/F
    Larger V → more conversion for given F

C_A_feed : float, default=1.0
    Feed concentration [mol/L]
    Typical: 0.5-2.0 mol/L
    Higher feed → more product but more heat generation

T_feed : float, default=350.0
    Feed temperature [K]
    Typical: 300-360 K
    Pre-heating can improve conversion but reduces stability margin

k0 : float, default=7.2e10
    Pre-exponential factor [1/s]
    Collision frequency in Arrhenius equation
    Typical: 10⁶-10¹² for liquid phase reactions
    Determines reaction speed at given temperature

E : float, default=8750.0
    Activation energy [K] (actually Eₐ/R)
    Energy barrier for reaction to occur
    Typical: 5000-15000 K for Eₐ/R
    Higher E → more temperature-sensitive
    Physical Eₐ typically 40-120 kJ/mol

delta_H : float, default=-5e4
    Heat of reaction [J/mol]
    Negative = exothermic (releases heat)
    Positive = endothermic (absorbs heat)
    Typical for exothermic: -20,000 to -200,000 J/mol
    Larger |ΔH| → stronger thermal coupling, harder control

rho : float, default=1000.0
    Density [kg/L]
    Typical for aqueous solutions: 900-1100 kg/L
    Affects thermal inertia (heat capacity)

Cp : float, default=0.239
    Specific heat capacity [J/(kg·K)]
    Typical for aqueous: 0.2-0.5 J/(kg·K)
    Higher Cₚ → slower temperature changes (more stable)

UA : float, default=5e4
    Overall heat transfer coefficient × area [J/(s·K)]
    Combines jacket film coefficient, wall conduction, reactor-side film
    Typical: 10³-10⁵ J/(s·K)
    Higher UA → better temperature control, faster cooling
    Limited by physical design (jacket size, flow rate)

dt : float, default=0.1
    Sampling/discretization time step [s]
    Critical parameter for stability!
    - Too large → numerical instability, oscillations
    - Too small → slow simulation, high computational cost
    - Rule of thumb: dt < 0.1/max(λ) where λ are eigenvalues
    - Typical for CSTR: 0.01-1.0 seconds
    - Must be smaller than fastest system time scale

Equilibria:

**Multiple Steady States** (hallmark of CSTRs!):

CSTR can have 1, 2, or 3 steady states depending on parameters.
For given feed conditions and jacket temperature:

1. **Low conversion state** (stable):
   - Low T ≈ T_feed + small rise
   - Low reaction rate (slow kinetics)
   - High Cₐ ≈ Cₐ,feed (minimal conversion)
   - Heat generation < Heat removal
   - Easy to control but inefficient
   - Attractive for cold startup

2. **High conversion state** (stable):
   - High T >> T_feed
   - High reaction rate (fast kinetics)
   - Low Cₐ << Cₐ,feed (high conversion)
   - Heat generation balanced by cooling
   - Desirable operating point (efficient)
   - Risk: close to instability/runaway

3. **Intermediate state** (unstable):
   - Saddle point in phase space
   - Not physically realizable (unstable)
   - Forms separatrix between basins of attraction
   - System will move toward stable states

**Stability depends on**:
- Residence time τ = V/F (longer → more conversion, less stable)
- Activation energy E (higher → more sensitive)
- Heat of reaction ΔH (larger |ΔH| → more coupling)
- Cooling capacity UA (higher → more stable)
- Feed temperature T_feed (higher → less stable margin)

**Bifurcation Behavior**:
As cooling capacity (T_jacket) decreases:
1. Unique stable high-conversion state
2. Saddle-node bifurcation → 3 steady states appear
3. Two stable states (low and high conversion)
4. Another saddle-node → only low-conversion state
5. Further decrease → thermal runaway (no steady state)

Control Objectives:

Common control goals for CSTR:

1. **Setpoint tracking**: Maintain T[k] ≈ T_setpoint
   - Most common objective
   - Balances conversion and stability
   - PID/LQR/MPC controllers typical
   - Challenge: nonlinearity and multiple steady states

2. **Startup control**: Transition low → high conversion state
   - Must cross unstable intermediate state
   - Requires large transient cooling capacity
   - Bang-bang or optimal trajectory control
   - Risk of overshoot → runaway

3. **Disturbance rejection**: Handle feed variations
   - Feed concentration changes: Cₐ,feed(t)
   - Feed temperature disturbances: T_feed(t)
   - Flow rate fluctuations: F(t)
   - Jacket temperature limits

4. **Optimal operation**: Maximize profit
   - Balance conversion (revenue) vs cooling cost
   - Economic objective: J = price·F·(Cₐ,feed - Cₐ) - cooling_cost
   - Constraint: T_max safety limit
   - May operate near instability for profit

5. **Runaway prevention**: Safety constraint
   - Monitor temperature rate: dT/dt < threshold
   - Emergency cooling if T > T_max
   - May require batch shutdown

State Constraints:

Physical constraints that must be enforced:

1. **Non-negativity**: Cₐ[k] ≥ 0
   - Concentration cannot be negative
   - Physical meaning: species present or absent
   - Euler discretization may violate if dt too large

2. **Concentration bounds**: 0 ≤ Cₐ[k] ≤ Cₐ,feed
   - Cannot exceed feed concentration (dilution + reaction)
   - Upper bound: Cₐ ≤ Cₐ,feed (no reaction case)
   - Useful for validation

3. **Temperature limits**: T_min ≤ T[k] ≤ T_max
   - Safety: prevent runaway (T_max ≈ 450-500 K)
   - Operability: prevent freezing/solidification (T_min ≈ 280 K)
   - Jacket temperature limits: T_jacket,min ≤ T_jacket ≤ T_jacket,max
   - Typical limits: 280 K ≤ T ≤ 450 K

4. **Jacket temperature constraints**: T_jacket,min ≤ T_jacket[k] ≤ T_jacket,max
   - Physical cooling/heating capacity
   - Chiller: T_jacket,min ≈ 280 K
   - Heater: T_jacket,max ≈ 400 K
   - Rate limit: |T_jacket[k+1] - T_jacket[k]| ≤ ΔT_jacket,max

Numerical Considerations:

**Stability**: The explicit Euler discretization is stable if:
    dt < 2/λ_max

where λ_max is the maximum eigenvalue of the Jacobian.

For CSTR, typical eigenvalues:
    λ₁ ≈ -(1/τ + k₀·exp(-E/T))  (concentration dynamics)
    λ₂ ≈ -(1/τ + UA/(VρCₚ))  (temperature dynamics)

At high temperature, λ_max can be large (fast dynamics), requiring
small dt for stability.

**Rule of thumb**:
    dt < 0.1 · min(τ, VρCₚ/UA, 1/(k₀·exp(-E/T)))

**Stiffness**: CSTR is moderately stiff due to:
- Fast reaction at high temperature
- Different time scales (concentration vs temperature)
- Exponential temperature dependence

For better accuracy, use higher-order discretization:
        cstr_continuous = ContinuousCSTR(...)
        cstr_discrete = cstr_continuous.discretize(dt=0.1, method='rk4')
**Multiple Steady States**: Discrete system inherits multiple equilibria
from continuous system. Simulation starting point determines which
equilibrium is reached (basin of attraction).

Example Usage:

>>> # Create CSTR with default parameters
>>> cstr = DiscreteCSTR(dt=0.1)
>>>
>>> # High conversion steady state (typical operating point)
>>> x_high = np.array([0.1, 390.0])  # [Low Cₐ, High T]
>>> u_high = np.array([350.0])  # [Cool jacket temperature]
>>>
>>> # Verify it's an equilibrium
>>> x_next = cstr.step(x_high, u_high)
>>> print(f"Change: {np.linalg.norm(x_next - x_high):.2e}")  # Should be small
>>>
>>> # Simulate with constant cooling
>>> result = cstr.simulate(
...     x0=x_high,
...     u_sequence=np.array([350.0]),  # Constant jacket temp
...     n_steps=100
... )
>>>
>>> # Plot concentration and temperature
>>> import matplotlib.pyplot as plt
>>> fig, axes = plt.subplots(2, 1, figsize=(10, 6))
>>> axes[0].plot(result['time_steps'] * cstr.dt, result['states'][:, 0])
>>> axes[0].set_ylabel('Cₐ [mol/L]')
>>> axes[1].plot(result['time_steps'] * cstr.dt, result['states'][:, 1])
>>> axes[1].set_ylabel('T [K]')
>>> axes[1].set_xlabel('Time [s]')
>>>
>>> # Design LQR controller for temperature regulation
>>> T_setpoint = 390.0
>>> C_A_setpoint = 0.1
>>> x_ref = np.array([C_A_setpoint, T_setpoint])
>>>
>>> # Calculate required jacket temperature for steady state
>>> # This requires solving energy balance, simplified here
>>> u_ref = np.array([350.0])
>>>
>>> # Linearize at operating point
>>> Ad, Bd = cstr.linearize(x_ref, u_ref)
>>>
>>> # Check discrete stability
>>> eigenvalues = np.linalg.eigvals(Ad)
>>> print(f"Eigenvalues: {eigenvalues}")
>>> print(f"Stable: {np.all(np.abs(eigenvalues) < 1)}")
>>>
>>> # Design LQR (care more about temperature than concentration)
>>> Q_lqr = np.diag([1.0, 100.0])  # Penalize temperature error heavily
>>> R_lqr = np.array([[1.0]])
>>> lqr_result = cstr.control.design_lqr(Ad, Bd, Q_lqr, R_lqr,
...                                       system_type='discrete')
>>> K = lqr_result['gain']
>>>
>>> # Simulate with LQR control
>>> def lqr_controller(x, k):
...     return -K @ (x - x_ref) + u_ref
>>>
>>> result_lqr = cstr.rollout(x_high, lqr_controller, n_steps=200)
>>>
>>> # Startup simulation: low conversion → high conversion
>>> x_low = np.array([0.9, 355.0])  # Low conversion state
>>>
>>> def startup_controller(x, k):
...     # Aggressive cooling to reach high-conversion state
...     if k < 50:
...         return np.array([340.0])  # Strong cooling
...     else:
...         return lqr_controller(x, k)  # Switch to regulator
>>>
>>> result_startup = cstr.rollout(x_low, startup_controller, n_steps=200)
>>>
>>> # Check if startup was successful
>>> final_state = result_startup['states'][-1, :]
>>> distance_to_target = np.linalg.norm(final_state - x_ref)
>>> print(f"Final state: Cₐ={final_state[0]:.3f}, T={final_state[1]:.1f}")
>>> print(f"Distance to target: {distance_to_target:.3f}")

Physical Insights:

**Thermal Runaway Risk**:
If cooling is insufficient, positive feedback occurs:
1. Temperature increases
2. Reaction rate increases exponentially (Arrhenius)
3. More heat generated (exothermic)
4. Temperature increases further → runaway!

Prevention:
- Adequate cooling capacity (large UA)
- Temperature limits and alarms
- Emergency cooling/shutdown procedures
- Conservative setpoint selection

**Multiple Steady States**:
Creates control challenges:
- Which steady state to operate at?
- How to transition between states?
- Risk of unintended switching due to disturbances
- Hysteresis in startup/shutdown procedures

**Residence Time Effects**:
- Short τ (high F/V): Low conversion, stable, safe
- Long τ (low F/V): High conversion, less stable, runaway risk
- Economic optimum: maximize profit subject to stability

**Jacket Temperature Selection**:
- Lower T_jacket: More cooling, enables higher conversion
- But: smaller stability margin, closer to bifurcation
- Higher T_jacket: More stable, but lower conversion
- Must balance economics and safety

**Startup Strategy**:
Transitioning from low to high conversion:
1. Begin at low-conversion state (safe, stable)
2. Gradually decrease T_jacket (increase cooling)
3. System may jump to high-conversion state (bifurcation)
4. Or use aggressive transient cooling
5. Once at high conversion, switch to regulatory control

**Oscillatory Behavior**:
Near instability boundaries, system may exhibit:
- Sustained oscillations (Hopf bifurcation)
- Limit cycles in Cₐ-T phase plane
- Period-doubling route to chaos (rare but possible)
- Quasiperiodic dynamics

Comparison with Continuous Version:

This discrete-time CSTR approximates the continuous-time system:
- Continuous system: dx/dt = f(x, u) (ground truth)
- Discrete system: x[k+1] = x[k] + dt·f(x[k], u[k]) (Euler approximation)

Advantages of discrete formulation:
- Natural for digital control (computers, PLCs)
- Fixed time step (predictable computation)
- Easy to implement in simulation
- Matches physical sampling of sensors

Disadvantages:
- Approximation error (depends on dt)
- Stability limited by time step
- May not capture fast transients accurately

For better accuracy, create from continuous version:
    cstr_continuous = ContinuousCSTR(F=100, V=100, ...)
    cstr_discrete = cstr_continuous.discretize(dt=0.1, method='rk4')

See Also:

ContinuousCSTR : Continuous-time version (more accurate)
DiscreteBatchReactor : Batch operation instead of continuous
ContinuousBatchReactor : Continuous batch reactor

Methods

Name Description
compute_conversion Compute fractional conversion of reactant A.
compute_damkohler_number Compute Damköhler number Da = k·τ (reaction rate × residence time).
compute_residence_time Compute residence time τ = V/F.
define_system Define discrete-time CSTR dynamics.
find_steady_states Find all steady states for a given jacket temperature.
setup_equilibria Set up steady-state equilibrium if provided.

compute_conversion

systems.builtin.deterministic.discrete.DiscreteCSTR.compute_conversion(
    C_A,
    C_A_feed,
)

Compute fractional conversion of reactant A.

Parameters

Name Type Description Default
C_A float Current reactor concentration [mol/L] required
C_A_feed float Feed concentration [mol/L] required

Returns

Name Type Description
float Conversion fraction X_A = (C_A_feed - C_A) / C_A_feed

Examples

>>> cstr = DiscreteCSTR()
>>> X = cstr.compute_conversion(C_A=0.1, C_A_feed=1.0)
>>> print(f"Conversion: {X*100:.1f}%")
Conversion: 90.0%

Notes

High conversion (X > 0.9) typically corresponds to high-temperature steady state with fast kinetics and strong exothermic heat generation.

compute_damkohler_number

systems.builtin.deterministic.discrete.DiscreteCSTR.compute_damkohler_number(T)

Compute Damköhler number Da = k·τ (reaction rate × residence time).

Parameters

Name Type Description Default
T float Temperature [K] required

Returns

Name Type Description
float Damköhler number [dimensionless]

Notes

Damköhler number measures reaction rate relative to flow rate: - Da << 1: Reaction slow, flow dominates, low conversion - Da >> 1: Reaction fast, kinetics dominate, high conversion - Da ≈ 1: Balanced, optimal efficiency

Examples

>>> cstr = DiscreteCSTR()
>>> Da_low = cstr.compute_damkohler_number(T=350.0)
>>> Da_high = cstr.compute_damkohler_number(T=400.0)
>>> print(f"Da(350K) = {Da_low:.2f}")
>>> print(f"Da(400K) = {Da_high:.2f}")

compute_residence_time

systems.builtin.deterministic.discrete.DiscreteCSTR.compute_residence_time()

Compute residence time τ = V/F.

Returns

Name Type Description
float Residence time [s]

Examples

>>> cstr = DiscreteCSTR(F=100.0, V=100.0)
>>> tau = cstr.compute_residence_time()
>>> print(f"Residence time: {tau} s")
Residence time: 1.0 s

Notes

Residence time is the average time a molecule spends in the reactor. - Longer τ (smaller F): More conversion, less stable - Shorter τ (larger F): Less conversion, more stable

define_system

systems.builtin.deterministic.discrete.DiscreteCSTR.define_system(
    F_val=100.0,
    V_val=100.0,
    C_A_feed_val=1.0,
    T_feed_val=350.0,
    k0_val=72000000000.0,
    E_val=8750.0,
    delta_H_val=-50000.0,
    rho_val=1000.0,
    Cp_val=0.239,
    UA_val=50000.0,
    dt=0.1,
    x_ss=None,
    u_ss=None,
)

Define discrete-time CSTR dynamics.

Parameters

Name Type Description Default
F_val float Volumetric flow rate [L/s] 100.0
V_val float Reactor volume [L] 100.0
C_A_feed_val float Feed concentration [mol/L] 1.0
T_feed_val float Feed temperature [K] 350.0
k0_val float Pre-exponential factor [1/s] 72000000000.0
E_val float Activation energy [K] 8750.0
delta_H_val float Heat of reaction [J/mol] (negative = exothermic) -50000.0
rho_val float Density [kg/L] 1000.0
Cp_val float Specific heat capacity [J/(kg·K)] 0.239
UA_val float Overall heat transfer coefficient × area [J/(s·K)] 50000.0
dt float Sampling time step [s] 0.1
x_ss Optional[np.ndarray] Steady-state [Cₐ, T] for equilibrium setup None
u_ss Optional[np.ndarray] Steady-state [T_jacket] for equilibrium setup None

find_steady_states

systems.builtin.deterministic.discrete.DiscreteCSTR.find_steady_states(
    T_jacket,
    T_range=(300.0, 500.0),
    n_points=100,
)

Find all steady states for a given jacket temperature.

Uses graphical method: plots dC_A/dt and dT/dt as functions of T, finds where both are zero simultaneously.

Parameters

Name Type Description Default
T_jacket float Jacket temperature [K] required
T_range tuple Temperature range to search (T_min, T_max) [K] (300.0, 500.0)
n_points int Number of points for graphical search 100

Returns

Name Type Description
list List of (C_A, T) steady states

Examples

>>> cstr = DiscreteCSTR()
>>> steady_states = cstr.find_steady_states(T_jacket=350.0)
>>> print(f"Found {len(steady_states)} steady states")
>>> for i, (C_A, T) in enumerate(steady_states):
...     print(f"  State {i+1}: C_A={C_A:.3f}, T={T:.1f}")

Notes

This is a simple implementation. For production code, use: - scipy.optimize.fsolve for more robust root finding - Continuation methods for bifurcation analysis - homotopy methods for finding all solutions

setup_equilibria

systems.builtin.deterministic.discrete.DiscreteCSTR.setup_equilibria()

Set up steady-state equilibrium if provided.

Notes

CSTR can have multiple steady states! Only add user-provided equilibrium. Finding all equilibria requires solving nonlinear algebraic equations (see find_steady_states() method).