use numra::ode::{Radau5, SolverOptions};
use numra::{solve_forward_sensitivity, ParametricOdeSystem};
struct Robertson {
k: [f64; 3],
}
impl ParametricOdeSystem<f64> for Robertson {
fn n_states(&self) -> usize {
3
}
fn n_params(&self) -> usize {
3
}
fn params(&self) -> &[f64] {
&self.k
}
fn rhs_with_params(&self, _t: f64, y: &[f64], k: &[f64], dy: &mut [f64]) {
let (y0, y1, y2) = (y[0], y[1], y[2]);
let (k1, k2, k3) = (k[0], k[1], k[2]);
dy[0] = -k1 * y0 + k3 * y1 * y2;
dy[1] = k1 * y0 - k2 * y1 * y1 - k3 * y1 * y2;
dy[2] = k2 * y1 * y1;
}
fn jacobian_y(&self, _t: f64, y: &[f64], jy: &mut [f64]) {
let (y1, y2) = (y[1], y[2]);
let (k1, k2, k3) = (self.k[0], self.k[1], self.k[2]);
jy[0] = -k1; jy[1] = k3 * y2; jy[2] = k3 * y1; jy[3] = k1; jy[4] = -2.0 * k2 * y1 - k3 * y2; jy[5] = -k3 * y1; jy[6] = 0.0; jy[7] = 2.0 * k2 * y1; jy[8] = 0.0; }
fn jacobian_p(&self, _t: f64, y: &[f64], jp: &mut [f64]) {
let (y0, y1, y2) = (y[0], y[1], y[2]);
jp[0] = -y0; jp[1] = y0; jp[2] = 0.0; jp[3] = 0.0; jp[4] = -y1 * y1; jp[5] = y1 * y1; jp[6] = y1 * y2; jp[7] = -y1 * y2; jp[8] = 0.0; }
fn has_analytical_jacobian_y(&self) -> bool {
true
}
fn has_analytical_jacobian_p(&self) -> bool {
true
}
}
fn main() {
println!("=== Robertson Stiff Kinetics — Forward Sensitivity ===\n");
println!("Reaction network:");
println!(" A -> B (k1 = 0.04)");
println!(" 2B -> C + B (k2 = 3.0e7)");
println!(" B + C -> A + C (k3 = 1.0e4)\n");
let k_nominal = [0.04_f64, 3.0e7, 1.0e4];
let system = Robertson { k: k_nominal };
let y0 = [1.0_f64, 0.0, 0.0];
let t_final = 40.0;
let opts = SolverOptions::default().rtol(1e-8).atol(1e-12);
let result = solve_forward_sensitivity::<Radau5, f64, _>(&system, 0.0, t_final, &y0, &opts)
.expect("Radau5 augmented solve failed");
assert!(result.success, "{}", result.message);
println!("Integration: t in [0, {}]", t_final);
println!(
" output points : {}\n RHS evals : {}\n Jacobian evals: {}\n accepted steps: {}\n rejected steps: {}\n LU decomps : {}",
result.len(),
result.stats.n_eval,
result.stats.n_jac,
result.stats.n_accept,
result.stats.n_reject,
result.stats.n_lu,
);
let y_final = result.final_state();
println!("\nState at t = {}:", t_final);
println!(" y0 (A) = {:.6e}", y_final[0]);
println!(" y1 (B) = {:.6e}", y_final[1]);
println!(" y2 (C) = {:.6e}", y_final[2]);
let mass = y_final[0] + y_final[1] + y_final[2];
println!(" sum = {:.12} (should be 1.0)", mass);
let last = result.len() - 1;
println!("\nSensitivity matrix S(t={}) = dy/dk:", t_final);
println!(" ∂/∂k1 ∂/∂k2 ∂/∂k3");
for state in 0..3 {
print!(" ∂y{}/∂k_* =", state);
for param in 0..3 {
print!(" {:>13.4e}", result.dyi_dpj(last, state, param));
}
println!();
}
println!("\nPer-parameter sensitivity vectors (final time):");
for param in 0..3 {
let col = result.sensitivity_for_param(last, param);
println!(
" ∂y/∂k{} = [{:>11.4e}, {:>11.4e}, {:>11.4e}]",
param + 1,
col[0],
col[1],
col[2],
);
}
println!("\nNormalised sensitivity (k_k / y_j) · (∂y_j/∂k_k):");
println!(" k1 k2 k3");
let s_norm = result.normalized_sensitivity_at(last, &k_nominal);
let n = system.n_states();
for state in 0..3 {
print!(" y{} normalised =", state);
for param in 0..3 {
print!(" {:>13.4e}", s_norm[param * n + state]);
}
println!();
}
println!("\nInfluence ranking (largest |normalised sensitivity| first):");
let names = ["k1", "k2", "k3"];
for state in 0..3 {
let mut ranked: Vec<(usize, f64)> = (0..3)
.map(|param| (param, s_norm[param * n + state].abs()))
.collect();
ranked.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap());
let formatted: Vec<String> = ranked
.iter()
.map(|(p, mag)| format!("{} ({:.2e})", names[*p], mag))
.collect();
println!(" y{}: {}", state, formatted.join(" > "));
}
println!("\nDone.");
}