use numra::linalg::{DenseMatrix, LUFactorization, Matrix};
use numra::nonlinear::{newton_solve, NonlinearSystem};
use numra::ode::{
auto_solve,
Bdf,
DoPri5,
Esdirk32,
Esdirk43,
Esdirk54,
OdeProblem,
Radau5,
Solver,
SolverOptions,
Tsit5,
Vern6,
Vern7,
};
use numra::uncertainty::{compute_sensitivities, Uncertain};
#[test]
fn test_lu_with_ode_jacobian() {
let mut a: DenseMatrix<f64> = DenseMatrix::zeros(2, 2);
a.set(0, 0, -1.0);
a.set(0, 1, 0.5);
a.set(1, 0, 0.5);
a.set(1, 1, -1.0);
let lu = LUFactorization::new(&a).unwrap();
let b = vec![1.0, 1.0];
let x = lu.solve(&b).unwrap();
let ax0: f64 = -x[0] + 0.5 * x[1];
assert!((ax0 - 1.0).abs() < 1e-10);
}
#[test]
fn test_newton_stage_equations() {
struct StageEq {
h: f64,
y0: f64,
}
impl NonlinearSystem<f64> for StageEq {
fn dim(&self) -> usize {
1
}
fn eval(&self, k: &[f64], f: &mut [f64]) {
let a = 1.0 / 9.0;
f[0] = k[0] * (1.0 + self.h * a) + self.y0;
}
}
let system = StageEq { h: 0.1, y0: 1.0 };
let result = newton_solve(&system, &[0.0], None).unwrap();
assert!(result.converged);
}
#[test]
fn test_uncertainty_with_ode() {
let solve_ode = |p: &[f64]| -> f64 {
let k = p[0];
let problem = OdeProblem::new(
move |_t, y: &[f64], dydt: &mut [f64]| {
dydt[0] = -k * y[0];
},
0.0,
1.0,
vec![1.0],
);
DoPri5::solve(&problem, 0.0, 1.0, &[1.0], &SolverOptions::default())
.unwrap()
.y_final()
.unwrap()[0]
};
let sens = compute_sensitivities(solve_ode, &[1.0], &["k"], None);
let expected = (-1.0_f64).exp();
assert!((sens.output - expected).abs() < 1e-4);
}
#[test]
fn test_dopri5_radau5_agreement() {
let problem = OdeProblem::new(
|_t, y: &[f64], dydt: &mut [f64]| {
dydt[0] = -y[0];
},
0.0,
5.0,
vec![1.0],
);
let dopri_opts = SolverOptions::default();
let radau_opts = SolverOptions::default().rtol(1e-3).atol(1e-6);
let y_dopri = DoPri5::solve(&problem, 0.0, 5.0, &[1.0], &dopri_opts)
.unwrap()
.y_final()
.unwrap()[0];
let y_radau = Radau5::solve(&problem, 0.0, 5.0, &[1.0], &radau_opts)
.unwrap()
.y_final()
.unwrap()[0];
assert!((y_dopri - y_radau).abs() < 1e-3);
}
#[test]
fn test_van_der_pol_radau5() {
let mu = 100.0;
let problem = OdeProblem::new(
move |_t, y: &[f64], dydt: &mut [f64]| {
dydt[0] = y[1];
dydt[1] = mu * (1.0 - y[0] * y[0]) * y[1] - y[0];
},
0.0,
20.0,
vec![2.0, 0.0],
);
let options = SolverOptions::default().rtol(1e-3).atol(1e-5);
let result = Radau5::solve(&problem, 0.0, 20.0, &[2.0, 0.0], &options);
assert!(result.is_ok());
}
#[test]
fn test_full_composition() {
let k = Uncertain::from_std(2.0, 0.2);
let solve_with_k = |params: &[f64]| -> f64 {
let k_val = params[0];
let problem = OdeProblem::new(
move |_t, y: &[f64], dydt: &mut [f64]| {
dydt[0] = -k_val * y[0];
},
0.0,
1.0,
vec![1.0],
);
DoPri5::solve(&problem, 0.0, 1.0, &[1.0], &SolverOptions::default())
.unwrap()
.y_final()
.unwrap()[0]
};
let sens = compute_sensitivities(solve_with_k, &[k.mean], &["k"], None);
let y_var = sens.propagate_uncertainty(&[k.variance]);
let y_uncertain = Uncertain::new(sens.output, y_var);
let expected = (-2.0_f64).exp();
assert!((y_uncertain.mean - expected).abs() < 1e-4);
assert!(y_uncertain.std() > 0.0);
}
#[test]
fn test_radau5_with_sensitivity() {
let mu = Uncertain::from_std(10.0, 1.0);
let solve_stiff = |params: &[f64]| -> f64 {
let mu_val = params[0];
let problem = OdeProblem::new(
move |_t, y: &[f64], dydt: &mut [f64]| {
dydt[0] = y[1];
dydt[1] = mu_val * (1.0 - y[0] * y[0]) * y[1] - y[0];
},
0.0,
2.0,
vec![2.0, 0.0],
);
let options = SolverOptions::default().rtol(1e-4).atol(1e-6);
Radau5::solve(&problem, 0.0, 2.0, &[2.0, 0.0], &options)
.unwrap()
.y_final()
.unwrap()[0]
};
let sens = compute_sensitivities(solve_stiff, &[mu.mean], &["mu"], None);
assert!(sens.output.is_finite());
assert!(sens.sensitivities[0].coefficient.is_finite());
}
#[test]
fn test_all_explicit_methods_agree() {
let problem = OdeProblem::new(
|_t, y: &[f64], dydt: &mut [f64]| {
dydt[0] = -y[0];
},
0.0,
2.0,
vec![1.0],
);
let options = SolverOptions::default().rtol(1e-6);
let expected = (-2.0_f64).exp();
let y_dopri = DoPri5::solve(&problem, 0.0, 2.0, &[1.0], &options)
.unwrap()
.y_final()
.unwrap()[0];
let y_tsit5 = Tsit5::solve(&problem, 0.0, 2.0, &[1.0], &options)
.unwrap()
.y_final()
.unwrap()[0];
let y_vern6 = Vern6::solve(&problem, 0.0, 2.0, &[1.0], &options)
.unwrap()
.y_final()
.unwrap()[0];
let y_vern7 = Vern7::solve(&problem, 0.0, 2.0, &[1.0], &options)
.unwrap()
.y_final()
.unwrap()[0];
assert!((y_dopri - expected).abs() < 1e-4, "DoPri5 failed");
assert!((y_tsit5 - expected).abs() < 1e-4, "Tsit5 failed");
assert!((y_vern6 - expected).abs() < 1e-4, "Vern6 failed");
assert!((y_vern7 - expected).abs() < 1e-4, "Vern7 failed");
}
#[test]
fn test_all_implicit_methods_agree() {
let problem = OdeProblem::new(
|_t, y: &[f64], dydt: &mut [f64]| {
dydt[0] = -50.0 * y[0];
},
0.0,
0.2,
vec![1.0],
);
let options = SolverOptions::default().rtol(1e-4).atol(1e-6);
let expected = (-10.0_f64).exp();
let y_radau = Radau5::solve(&problem, 0.0, 0.2, &[1.0], &options)
.unwrap()
.y_final()
.unwrap()[0];
let y_esdirk32 = Esdirk32::solve(&problem, 0.0, 0.2, &[1.0], &options)
.unwrap()
.y_final()
.unwrap()[0];
let y_esdirk43 = Esdirk43::solve(&problem, 0.0, 0.2, &[1.0], &options)
.unwrap()
.y_final()
.unwrap()[0];
let y_esdirk54 = Esdirk54::solve(&problem, 0.0, 0.2, &[1.0], &options)
.unwrap()
.y_final()
.unwrap()[0];
assert!(
(y_radau - expected).abs() < 1e-4,
"Radau5 failed: got {}, expected {}",
y_radau,
expected
);
assert!(
(y_esdirk32 - expected).abs() < 1e-4,
"ESDIRK32 failed: got {}, expected {}",
y_esdirk32,
expected
);
assert!(
(y_esdirk43 - expected).abs() < 1e-4,
"ESDIRK43 failed: got {}, expected {}",
y_esdirk43,
expected
);
assert!(
(y_esdirk54 - expected).abs() < 1e-4,
"ESDIRK54 failed: got {}, expected {}",
y_esdirk54,
expected
);
}
#[test]
fn test_bdf_variable_order() {
let problem = OdeProblem::new(
|_t, y: &[f64], dydt: &mut [f64]| {
dydt[0] = -100.0 * y[0];
dydt[1] = -0.5 * y[1]; },
0.0,
1.0,
vec![1.0, 1.0],
);
let options = SolverOptions::default().rtol(1e-4).atol(1e-8);
let result = Bdf::solve(&problem, 0.0, 1.0, &[1.0, 1.0], &options).unwrap();
assert!(result.success);
let y_final = result.y_final().unwrap();
assert!(
y_final[0].abs() < 1e-3,
"Fast component should decay: got {}",
y_final[0]
);
let expected = (-0.5_f64).exp();
assert!(
(y_final[1] - expected).abs() < 0.1,
"Slow component: got {}, expected {}",
y_final[1],
expected
);
}
#[test]
fn test_auto_selection_nonstiff() {
let problem = OdeProblem::new(
|_t, y: &[f64], dydt: &mut [f64]| {
dydt[0] = -y[0];
},
0.0,
5.0,
vec![1.0],
);
let options = SolverOptions::default();
let result = auto_solve(&problem, 0.0, 5.0, &[1.0], &options).unwrap();
assert!(result.success);
let y_final = result.y_final().unwrap()[0];
let expected = (-5.0_f64).exp();
assert!((y_final - expected).abs() < 1e-4);
}
#[test]
fn test_auto_selection_stiff() {
let problem = OdeProblem::new(
|_t, y: &[f64], dydt: &mut [f64]| {
dydt[0] = -50.0 * y[0];
},
0.0,
0.2,
vec![1.0],
);
let options = SolverOptions::default().rtol(1e-4);
let result = Esdirk54::solve(&problem, 0.0, 0.2, &[1.0], &options).unwrap();
assert!(result.success);
let y_final = result.y_final().unwrap()[0];
let expected = (-10.0_f64).exp(); assert!((y_final - expected).abs() < 0.01);
}
#[test]
fn test_verner_high_accuracy() {
let problem = OdeProblem::new(
|_t, y: &[f64], dydt: &mut [f64]| {
dydt[0] = y[1];
dydt[1] = -y[0];
},
0.0,
10.0,
vec![1.0, 0.0],
);
let options = SolverOptions::default().rtol(1e-8).atol(1e-10);
let result = DoPri5::solve(&problem, 0.0, 10.0, &[1.0, 0.0], &options).unwrap();
let y_final = result.y_final().unwrap();
assert!((y_final[0] - 10.0_f64.cos()).abs() < 1e-6);
assert!((y_final[1] + 10.0_f64.sin()).abs() < 1e-6);
}
#[test]
fn test_esdirk_stiff_system() {
let mu = 20.0;
let problem = OdeProblem::new(
move |_t, y: &[f64], dydt: &mut [f64]| {
dydt[0] = y[1];
dydt[1] = mu * (1.0 - y[0] * y[0]) * y[1] - y[0];
},
0.0,
10.0,
vec![2.0, 0.0],
);
let options = SolverOptions::default().rtol(1e-4).atol(1e-6);
let result = Esdirk54::solve(&problem, 0.0, 10.0, &[2.0, 0.0], &options);
assert!(
result.is_ok(),
"ESDIRK54 Van der Pol failed: {:?}",
result.err()
);
}
#[test]
fn test_solver_with_sensitivity_week4() {
let solve_with_tsit5 = |params: &[f64]| -> f64 {
let k = params[0];
let problem = OdeProblem::new(
move |_t, y: &[f64], dydt: &mut [f64]| {
dydt[0] = -k * y[0];
},
0.0,
1.0,
vec![1.0],
);
Tsit5::solve(&problem, 0.0, 1.0, &[1.0], &SolverOptions::default())
.unwrap()
.y_final()
.unwrap()[0]
};
let sens = compute_sensitivities(solve_with_tsit5, &[2.0], &["k"], None);
assert!(sens.output.is_finite());
assert!((sens.output - (-2.0_f64).exp()).abs() < 1e-4);
}
#[test]
fn test_week4_full_composition() {
let k = Uncertain::from_std(1.0, 0.1);
let solve_with_auto = |params: &[f64]| -> f64 {
let k_val = params[0];
let problem = OdeProblem::new(
move |_t, y: &[f64], dydt: &mut [f64]| {
dydt[0] = -k_val * y[0];
},
0.0,
2.0,
vec![1.0],
);
auto_solve(&problem, 0.0, 2.0, &[1.0], &SolverOptions::default())
.unwrap()
.y_final()
.unwrap()[0]
};
let sens = compute_sensitivities(solve_with_auto, &[k.mean], &["k"], None);
let y_var = sens.propagate_uncertainty(&[k.variance]);
let y_uncertain = Uncertain::new(sens.output, y_var);
let expected = (-2.0_f64).exp();
assert!((y_uncertain.mean - expected).abs() < 1e-4);
assert!(y_uncertain.std() > 0.0);
}
#[test]
fn test_lorenz_with_all_methods() {
let sigma = 10.0;
let rho = 28.0;
let beta = 8.0 / 3.0;
let problem = OdeProblem::new(
move |_t, y: &[f64], dydt: &mut [f64]| {
dydt[0] = sigma * (y[1] - y[0]);
dydt[1] = y[0] * (rho - y[2]) - y[1];
dydt[2] = y[0] * y[1] - beta * y[2];
},
0.0,
1.0,
vec![1.0, 1.0, 1.0],
);
let options = SolverOptions::default().rtol(1e-4).atol(1e-6);
let r_dopri5 = DoPri5::solve(&problem, 0.0, 1.0, &[1.0, 1.0, 1.0], &options);
assert!(r_dopri5.is_ok(), "DoPri5 Lorenz failed");
let r_vern6 = Vern6::solve(&problem, 0.0, 1.0, &[1.0, 1.0, 1.0], &options);
assert!(r_vern6.is_ok(), "Vern6 Lorenz failed");
let r_esdirk54 = Esdirk54::solve(&problem, 0.0, 1.0, &[1.0, 1.0, 1.0], &options);
assert!(r_esdirk54.is_ok(), "ESDIRK54 Lorenz failed");
let r_auto = auto_solve(&problem, 0.0, 1.0, &[1.0, 1.0, 1.0], &options);
assert!(r_auto.is_ok(), "Auto Lorenz failed");
}