use flowmatch::ode::{integrate_fixed, OdeMethod};
use ndarray::Array1;
fn error_at_steps(method: OdeMethod, steps: usize) -> f64 {
let x0 = Array1::from_vec(vec![1.0f32]);
let exact = (-1.0f32).exp();
let dt = 1.0f32 / (steps as f32);
let result = integrate_fixed(method, &x0, 0.0, dt, steps, |x, _t| {
Ok(Array1::from_vec(vec![-x[0]]))
})
.unwrap();
(result[0] - exact).abs() as f64
}
fn estimate_order(method: OdeMethod, step_counts: &[usize]) -> f64 {
let errors: Vec<f64> = step_counts
.iter()
.map(|&s| error_at_steps(method, s))
.collect();
let mut ratios = Vec::new();
for i in 0..errors.len() - 1 {
if errors[i + 1] > 1e-12 {
let ratio = (errors[i] / errors[i + 1]).ln() / 2.0f64.ln();
ratios.push(ratio);
}
}
ratios.iter().sum::<f64>() / ratios.len() as f64
}
#[test]
fn euler_achieves_first_order_convergence() {
let steps = vec![10, 20, 40, 80, 160];
let order = estimate_order(OdeMethod::Euler, &steps);
assert!(
(0.85..1.15).contains(&order),
"Euler should be ~O(h^1), estimated order = {order:.3}"
);
}
#[test]
fn heun_achieves_second_order_convergence() {
let steps = vec![10, 20, 40, 80, 160];
let order = estimate_order(OdeMethod::Heun, &steps);
assert!(
(1.85..2.15).contains(&order),
"Heun should be ~O(h^2), estimated order = {order:.3}"
);
}
fn error_cosine_at_steps(method: OdeMethod, steps: usize) -> f64 {
let x0 = Array1::from_vec(vec![0.0f32]);
let t_final = 1.0f32;
let exact = t_final.sin();
let dt = t_final / (steps as f32);
let result = integrate_fixed(method, &x0, 0.0, dt, steps, |_x, t| {
Ok(Array1::from_vec(vec![t.cos()]))
})
.unwrap();
(result[0] - exact).abs() as f64
}
#[test]
fn euler_first_order_on_cosine_ode() {
let steps = [20, 40, 80, 160, 320];
let errors: Vec<f64> = steps
.iter()
.map(|&s| error_cosine_at_steps(OdeMethod::Euler, s))
.collect();
let mut ratios = Vec::new();
for i in 0..errors.len() - 1 {
if errors[i + 1] > 1e-12 {
ratios.push((errors[i] / errors[i + 1]).ln() / 2.0f64.ln());
}
}
let order = ratios.iter().sum::<f64>() / ratios.len() as f64;
assert!(
(0.85..1.15).contains(&order),
"Euler should be ~O(h^1) on cosine ODE, estimated order = {order:.3}"
);
}
#[test]
fn heun_second_order_on_cosine_ode() {
let steps = [5, 10, 20, 40, 80];
let errors: Vec<f64> = steps
.iter()
.map(|&s| error_cosine_at_steps(OdeMethod::Heun, s))
.collect();
let mut ratios = Vec::new();
for i in 0..errors.len() - 1 {
if errors[i + 1] > 1e-12 {
ratios.push((errors[i] / errors[i + 1]).ln() / 2.0f64.ln());
}
}
let order = ratios.iter().sum::<f64>() / ratios.len() as f64;
assert!(
(1.85..2.15).contains(&order),
"Heun should be ~O(h^2) on cosine ODE, estimated order = {order:.3}"
);
}