Skip to main content

gradostat_multiperiod_socp/
gradostat_multiperiod_socp.rs

1//! Multi-period second-order cone optimization of the gradostat (model `P_RC`).
2//!
3//! Replication of Section 7.2 of Taylor & Rapaport (2021).
4//! A four-tank gradostat with constant flows and diffusions is
5//! optimized over `TAU = 1000` time periods to maximize cumulative
6//! biogas production.
7//!
8//! # Model
9//!
10//! Growth parameters `mu_max = K = y = 1`, unit tank volumes `V = I`, Euler's
11//! explicit method with time step `Delta = 1`, and a periodic horizon
12//! (`S(1) = S(TAU+1)`, `X(1) = X(TAU+1)`).
13//!
14//! ```text
15//! max   sum_{i,t}  T_i(t)                                             (eq. 24)
16//! s.t.  S_i(t+1) - S_i(t) = -T_i(t) + [(M+L) S(t)]_i + Qin_i Sin_i(t) (25a)
17//!       X_i(t+1) - X_i(t) =  T_i(t) + [(M+L) X(t)]_i + Qin_i Xin_i(t) (25b)
18//!       || [S_i(t), T_i(t), X_i(t)] || <= X_i(t) + S_i(t) - T_i(t)    (eq. 9a)
19//!       S_i(t) - T_i(t) >= 0                                          (eq. 9b)
20//!       sum_i Qin_i Xin_i(t) <= 3                                     (biomass-inflow budget)
21//!       S, X, T >= 0,   Xin >= 0
22//! ```
23//!
24//! The substrate inflow `Sin(t)` is a fixed data profile, the biomass inflow
25//! `Xin(t)` is the decision variable.
26//!
27//! # Reproduction vs. the authors' code
28//!
29//! This model matches the authors' reference implementation and
30//! returns the same values.
31//!
32//! ## Details matched to the MATLAB code:
33//!
34//! 1. Diffusion `d = 0.3` on each existing pipe (`d = 0.3*(Q>0)`).
35//! 2. Budget on the inflow `Xin(t)`: `Qin' Xin(t) <= 3`, not on the tank
36//!    concentration `X(t)`.
37//! 3. Time base: The paper indexes `t in {1..TAU}`, here periods are
38//!    `p in {0..TAU-1}` with `t = p + 1` used in the `Sin` profiles.
39//!
40//! Run:
41//!   cargo run -p oximo --example gradostat_multiperiod_socp --features clarabel
42//!
43//! References:
44//! - Taylor, J. A., & Rapaport, A. (2021).
45//!   Second-order cone optimization of the gradostat.
46//!   Computers & Chemical Engineering, 151, 107347.
47//!   <https://doi.org/10.1016/j.compchemeng.2021.107347>
48
49#[cfg(feature = "clarabel")]
50fn main() -> Result<(), Box<dyn std::error::Error>> {
51    use oximo::ClarabelOptions;
52    use oximo::prelude::*;
53    use oximo::solvers::Clarabel;
54
55    // Problem data
56    const N: usize = 4; // tanks
57    const TAU: usize = 1000; // time periods
58
59    // Forced-flow + diffusion matrix M + L.
60    const ML: [[f64; N]; N] = [
61        [-2.3, 0.3, 0.0, 0.0],
62        [1.3, -3.9, 0.3, 1.3],
63        [0.0, 2.3, -3.6, 0.3],
64        [0.0, 0.3, 1.3, -2.6],
65    ];
66
67    // C = diag(Qin), the external water inflow at each tank.
68    const QIN: [f64; N] = [2.0, 1.0, 1.0, 1.0];
69
70    // Total biomass mass allowed in the tanks per period: 1' Qin X(t) <= BUDGET.
71    const BUDGET: f64 = 3.0;
72
73    // Substrate inflow profiles Sin_i(t), indexed sin[i][p] with paper time t=p+1.
74    #[expect(clippy::needless_range_loop, clippy::cast_precision_loss)]
75    let sin: Vec<Vec<f64>> = {
76        let pi = std::f64::consts::PI;
77        let tau = TAU as f64;
78        let mut sin = vec![vec![0.0f64; TAU]; N];
79        for p in 0..TAU {
80            let t = (p + 1) as f64;
81            let ti = p + 1;
82            sin[0][p] = 1.0 + (4.0 * pi * t / tau).sin();
83            sin[1][p] = 0.0;
84            sin[2][p] = if ti > TAU / 4 && ti <= 3 * TAU / 4 { 0.5 } else { 0.0 };
85            sin[3][p] = 1.0 + (4.0 * pi * t / tau).cos();
86        }
87        sin
88    };
89
90    let m = Model::new("gradostat_multiperiod");
91    let tanks = Set::range(0..N);
92    let periods = Set::range(0..TAU);
93
94    variable!(m, s[i in tanks, p in periods] >= 0.0); // substrate  S
95    variable!(m, x[i in tanks, p in periods] >= 0.0); // biomass    X
96    variable!(m, kin[i in tanks, p in periods] >= 0.0); // kinetics T
97    variable!(m, xin[i in tanks, p in periods] >= 0.0); // biomass inflow Xin (decision)
98
99    // Dynamic substrate and biomass balances (25a, 25b), Euler-explicit, V=y=1.
100    constraint!(m, s_bal[i in tanks, p in periods],
101        s[i, (p + 1) % TAU] - s[i, p]
102            == -kin[i, p] + sum!(ML[i][j] * s[j, p] for j in tanks) + QIN[i] * sin[i][p]);
103    constraint!(m, x_bal[i in tanks, p in periods],
104        x[i, (p + 1) % TAU] - x[i, p]
105            == kin[i, p] + sum!(ML[i][j] * x[j, p] for j in tanks) + QIN[i] * xin[i, p]);
106
107    // Contois growth as a second-order cone (eq. 9, mu_max = K = 1).
108    soc_constraint!(m, growth[i in tanks, p in periods],
109        [s[i, p], kin[i, p], x[i, p]] <= x[i, p] + s[i, p] - kin[i, p]);
110    constraint!(m, st_diff[i in tanks, p in periods], s[i, p] - kin[i, p] >= 0.0);
111
112    // Per-period biomass-inflow budget, 1' Qin Xin(t) <= BUDGET.
113    constraint!(m, bio_budget[p in periods], sum!(QIN[i] * xin[i, p] for i in tanks) <= BUDGET);
114
115    // Maximize cumulative biogas, sum_{i,t} V_ii T_i with V = I (eq. 24, undiscounted).
116    objective!(m, Max, sum!(kin[i, p] for i in tanks, p in periods));
117
118    assert_eq!(m.kind(), ModelKind::SOCP);
119
120    let res = Clarabel.solve(&m, &ClarabelOptions::default())?;
121    assert_eq!(res.termination, TerminationStatus::Optimal);
122
123    // Exactness metric E = max_{i,t} |r(S,X)-T|/r(S,X), with the Contois
124    // kinetics r(S,X) = mu_max S X/(K X + S) evaluated at the solution.
125    let mut e_max = 0.0f64;
126    for i in 0..N {
127        for p in 0..TAU {
128            let sv = res.value_of(s[(i, p)])?.unwrap();
129            let xv = res.value_of(x[(i, p)])?.unwrap();
130            let tv = res.value_of(kin[(i, p)])?.unwrap();
131            let denom = sv + xv;
132            if denom > 1e-9 {
133                let r = sv * xv / denom;
134                if r > 1e-9 {
135                    e_max = e_max.max((r - tv).abs() / r);
136                }
137            }
138        }
139    }
140
141    println!("Results:");
142    println!("Termination: {:?}", res.termination);
143    println!("Objective: {:.4}", res.objective().unwrap());
144    println!("Exactness E: {e_max:.3e}");
145    println!("Iterations: {}", res.iterations);
146    println!("Solve time: {:.3} s", res.solve_time.as_secs_f64());
147
148    Ok(())
149}
150
151#[cfg(not(feature = "clarabel"))]
152fn main() {
153    eprintln!("This example needs the clarabel feature:");
154    eprintln!("  cargo run -p oximo --example gradostat_multiperiod_socp --features clarabel");
155}