Multi-Phase Batch Reactor¶
Background¶
Many batch processes use different catalysts or operating regimes for successive reactions. Here, feed \(A\) is first converted into intermediate \(B\); after a catalyst change, \(B\) is converted into product \(P\). Switching too early leaves too little intermediate, while switching too late leaves too little time to form product. Higher temperature intensity accelerates both reactions but carries an operating cost.
This makes the process a genuine multi-phase problem: the dynamics change at an optimized event time, while all concentrations remain continuous.
Problem formulation¶
The normalized concentration vector in each phase is \(x_j=(A_j,B_j,P_j)\). During phase 1, \(0\le t\le t_s\),
During phase 2, \(t_s\le t\le2\ \mathrm h\),
The initial condition, event connection, and bounds are
The terminal concentrations are free. The objective balances product yield against temperature effort:
Variables and units¶
| Symbol | Meaning | Unit |
|---|---|---|
| \(t\) | Batch time | h |
| \(A,B,P\) | Normalized feed, intermediate, and product concentrations | - |
| \(u_1,u_2\) | Normalized temperature intensities | - |
| \(k_1,k_2\) | Effective first-order rate constants | h\(^{-1}\) |
| \(t_s\) | Catalyst-switch time | h |
| \(\eta\) | Product yield from consumed \(B\) | - |
Modeling choices¶
Each catalyst regime is represented by its own Lobatto phase with 24 mesh intervals and two interpolation points per interval. This keeps the concentration and temperature reconstructions piecewise linear and prevents between-node bound overshoot without increasing the number of decision points relative to the original higher-order mesh. Shared static parameters hold the three concentrations on both sides of the event, so using the same parameters in the terminal boundary of phase 1 and the initial boundary of phase 2 enforces continuity exactly. The switch time is another shared parameter and is optimized together with both control profiles.
The yield \(\eta<1\) represents material lost to unmodeled by-products. The effort weight produces an interior optimum in both phases instead of the uninformative solution that holds temperature at its upper bound throughout.
Run the example¶
Save the figure without opening a window:
Key implementation¶
system = System(["a_switch", "b_switch", "p_switch",
"a_final", "b_final", "p_final", "t_switch"])
a_s, b_s, p_s, a_f, b_f, p_f, t_s = system.s
phase_1.set_boundary_condition(
[1.0, 0.0, 0.0], [a_s, b_s, p_s], 0.0, t_s
)
phase_2.set_boundary_condition(
[a_s, b_s, p_s], [a_f, b_f, p_f], t_s, BATCH_TIME
)
system.set_phase([phase_1, phase_2])
system.set_objective(
-p_f + EFFORT_WEIGHT * (phase_1.I[0] + phase_2.I[0])
)
Verified result¶
Ipopt terminates successfully with objective \(-0.45376749\). The optimized catalyst switch occurs at \(t_s=1.092691\ \mathrm h\), and the final product fraction is \(0.658482\). The time-average temperature intensities, computed as \((t_f-t_0)^{-1}\int u\,\mathrm dt\), are \(0.796591\) in phase 1 and \(0.860164\) in phase 2. The maximum concentration mismatch across the event is below \(2\times10^{-6}\), and a 2,001-point check in each phase shows no path-bound violation at the reported precision.

Source code¶
See the complete runnable example: examples/multiphase_batch_reactor.py.