#![allow(clippy::identity_op, clippy::erasing_op, clippy::needless_range_loop)]
use numra_ode::sensitivity::{
solve_forward_sensitivity, solve_initial_condition_sensitivity,
solve_initial_condition_sensitivity_with, ParametricOdeSystem,
};
use numra_ode::{DoPri5, OdeSystem, SolverOptions};
struct Linear2x2HandSeeded;
impl Linear2x2HandSeeded {
const A: [[f64; 2]; 2] = [[-1.0, 0.5], [0.0, -0.5]];
}
impl ParametricOdeSystem<f64> for Linear2x2HandSeeded {
fn n_states(&self) -> usize {
2
}
fn n_params(&self) -> usize {
2
} fn params(&self) -> &[f64] {
&[0.0, 0.0]
}
fn rhs_with_params(&self, _t: f64, y: &[f64], _p: &[f64], dy: &mut [f64]) {
let a = Self::A;
dy[0] = a[0][0] * y[0] + a[0][1] * y[1];
dy[1] = a[1][0] * y[0] + a[1][1] * y[1];
}
fn jacobian_y(&self, _t: f64, _y: &[f64], jy: &mut [f64]) {
let a = Self::A;
jy[0] = a[0][0];
jy[1] = a[0][1];
jy[2] = a[1][0];
jy[3] = a[1][1];
}
fn jacobian_p(&self, _t: f64, _y: &[f64], jp: &mut [f64]) {
for slot in jp.iter_mut() {
*slot = 0.0;
}
}
fn initial_sensitivity(&self, _y0: &[f64], s0: &mut [f64]) {
for slot in s0.iter_mut() {
*slot = 0.0;
}
s0[0 * 2 + 0] = 1.0;
s0[1 * 2 + 1] = 1.0;
}
fn has_analytical_jacobian_y(&self) -> bool {
true
}
fn has_analytical_jacobian_p(&self) -> bool {
true
}
}
struct Linear2x2OdeSystem;
impl OdeSystem<f64> for Linear2x2OdeSystem {
fn dim(&self) -> usize {
2
}
fn rhs(&self, _t: f64, y: &[f64], dy: &mut [f64]) {
let a = Linear2x2HandSeeded::A;
dy[0] = a[0][0] * y[0] + a[0][1] * y[1];
dy[1] = a[1][0] * y[0] + a[1][1] * y[1];
}
fn jacobian(&self, _t: f64, _y: &[f64], jac: &mut [f64]) {
let a = Linear2x2HandSeeded::A;
jac[0] = a[0][0];
jac[1] = a[0][1];
jac[2] = a[1][0];
jac[3] = a[1][1];
}
}
#[test]
fn ic_matches_hand_seeded_variational() {
let y0 = [1.0_f64, 0.5];
let (t0, tf) = (0.0_f64, 1.5_f64);
let opts = SolverOptions::default().rtol(1e-9).atol(1e-12);
let hand = solve_forward_sensitivity::<DoPri5, _, _>(&Linear2x2HandSeeded, t0, tf, &y0, &opts)
.expect("hand-seeded variational solve failed");
let ic = solve_initial_condition_sensitivity::<DoPri5, _, _>(
&Linear2x2OdeSystem,
t0,
tf,
&y0,
&opts,
)
.expect("IC solve failed");
assert_eq!(hand.t.len(), ic.len());
assert_eq!(hand.n_states, ic.dim());
assert_eq!(hand.n_params, ic.dim());
for i in 0..ic.len() {
assert_eq!(hand.t[i], ic.t()[i], "t differs at i={i}");
let hy = hand.y_at(i);
let iy = ic.y(i);
assert_eq!(hy, iy, "y differs at i={i}");
let h_phi = hand.sensitivity_at(i);
let i_phi = ic.phi(i);
assert_eq!(
h_phi, i_phi,
"Φ(t_{i}) differs: hand={h_phi:?} ic={i_phi:?}"
);
for row in 0..2 {
for col in 0..2 {
let h = hand.dyi_dpj(i, row, col);
let g = ic.phi_ij(i, row, col);
assert_eq!(
h, g,
"phi_ij(i={i},row={row},col={col}) differs: hand={h} ic={g}"
);
}
}
}
}
#[test]
fn scalar_linear_matches_exp_minus_kt() {
let k = 0.5_f64;
let result = solve_initial_condition_sensitivity_with::<DoPri5, f64, _>(
move |_t, y, dy| {
dy[0] = -k * y[0];
},
&[1.0],
0.0,
2.0,
&SolverOptions::default().rtol(1e-9).atol(1e-12),
)
.expect("IC solve failed");
assert!(result.success());
assert_eq!(result.dim(), 1);
assert!((result.phi_ij(0, 0, 0) - 1.0).abs() < 1e-12);
for i in 0..result.len() {
let t_i = result.t()[i];
let phi = result.phi_ij(i, 0, 0);
let exact = (-k * t_i).exp();
assert!(
(phi - exact).abs() < 1e-7,
"i={i} t={t_i} phi={phi} exact={exact} |err|={}",
(phi - exact).abs()
);
}
}
fn expected_phi_2x2(t: f64) -> [[f64; 2]; 2] {
let em1t = (-t).exp();
let em05t = (-0.5 * t).exp();
[[em1t, em05t - em1t], [0.0, em05t]]
}
#[test]
fn linear_2x2_matches_expm_a_t() {
let opts = SolverOptions::default().rtol(1e-10).atol(1e-13);
let result = solve_initial_condition_sensitivity::<DoPri5, f64, _>(
&Linear2x2OdeSystem,
0.0,
1.5,
&[1.0, 0.5],
&opts,
)
.expect("IC 2x2 solve failed");
assert!(result.success());
assert_eq!(result.dim(), 2);
let phi0 = result.phi(0);
assert!((phi0[0 * 2 + 0] - 1.0).abs() < 1e-12, "Φ(0)_{{0,0}}");
assert!((phi0[1 * 2 + 1] - 1.0).abs() < 1e-12, "Φ(0)_{{1,1}}");
assert!(phi0[0 * 2 + 1].abs() < 1e-12, "Φ(0)_{{1,0}}");
assert!(phi0[1 * 2 + 0].abs() < 1e-12, "Φ(0)_{{0,1}}");
for i in 0..result.len() {
let t_i = result.t()[i];
let expected = expected_phi_2x2(t_i);
for row in 0..2 {
for col in 0..2 {
let got = result.phi_ij(i, row, col);
let want = expected[row][col];
assert!(
(got - want).abs() < 1e-7,
"Φ(t_{i})_{{{row},{col}}} got={got} want={want} t={t_i}"
);
}
}
}
}
struct IcShapeWithParamProbe {
k: f64,
dummy_p: Vec<f64>,
}
impl IcShapeWithParamProbe {
fn new(k: f64, dummy_p_value: f64) -> Self {
Self {
k,
dummy_p: vec![dummy_p_value],
}
}
}
impl ParametricOdeSystem<f64> for IcShapeWithParamProbe {
fn n_states(&self) -> usize {
1
}
fn n_params(&self) -> usize {
1
} fn params(&self) -> &[f64] {
&self.dummy_p
}
fn rhs_with_params(&self, _t: f64, y: &[f64], _p: &[f64], dy: &mut [f64]) {
dy[0] = -self.k * y[0];
}
fn jacobian_y(&self, _t: f64, _y: &[f64], jy: &mut [f64]) {
jy[0] = -self.k;
}
fn jacobian_p(&self, _t: f64, _y: &[f64], jp: &mut [f64]) {
jp[0] = 0.0;
}
fn initial_sensitivity(&self, _y0: &[f64], s0: &mut [f64]) {
s0[0] = 1.0; }
fn has_analytical_jacobian_y(&self) -> bool {
true
}
fn has_analytical_jacobian_p(&self) -> bool {
true
}
}
#[test]
fn ic_dummy_params_are_unread() {
let opts = SolverOptions::default().rtol(1e-9).atol(1e-12);
let with_zero = solve_forward_sensitivity::<DoPri5, _, _>(
&IcShapeWithParamProbe::new(0.5, 0.0),
0.0,
1.0,
&[1.0],
&opts,
)
.expect("solve with zero dummy params failed");
let with_garbage_nan = solve_forward_sensitivity::<DoPri5, _, _>(
&IcShapeWithParamProbe::new(0.5, f64::NAN),
0.0,
1.0,
&[1.0],
&opts,
)
.expect("solve with NaN dummy params failed");
let with_garbage_inf = solve_forward_sensitivity::<DoPri5, _, _>(
&IcShapeWithParamProbe::new(0.5, f64::INFINITY),
0.0,
1.0,
&[1.0],
&opts,
)
.expect("solve with ∞ dummy params failed");
assert_eq!(with_zero.len(), with_garbage_nan.len());
assert_eq!(with_zero.len(), with_garbage_inf.len());
for i in 0..with_zero.len() {
let z = with_zero.dyi_dpj(i, 0, 0);
let n = with_garbage_nan.dyi_dpj(i, 0, 0);
let f = with_garbage_inf.dyi_dpj(i, 0, 0);
assert!(z.is_finite(), "zero-dummy result not finite at i={i}: {z}");
assert!(n.is_finite(), "NaN-dummy poisoned trajectory at i={i}: {n}");
assert!(f.is_finite(), "∞-dummy poisoned trajectory at i={i}: {f}");
assert_eq!(z, n, "Φ differs between zero and NaN dummy at i={i}");
assert_eq!(z, f, "Φ differs between zero and ∞ dummy at i={i}");
}
}