use super::*;
use crate::{Matrix, Vector};
const PI: f64 = core::f64::consts::PI;
const TAU: f64 = 2.0 * PI;
fn ydot(_t: f64, y: &Vector<f64, 2>) -> Vector<f64, 2> {
Vector::from_array([y[1], -y[0]])
}
#[test]
fn rk4_step_exponential_decay() {
let y = Vector::from_array([1.0_f64]);
let y1 = rk4_step(0.0, &y, 0.01, |_t, y| *y * (-1.0));
assert!((y1[0] - (-0.01_f64).exp()).abs() < 1e-10);
}
#[test]
fn rk4_harmonic_oscillator() {
let y0 = Vector::from_array([1.0_f64, 0.0]);
let yf = rk4(0.0, TAU, 0.001, &y0, ydot);
assert!((yf[0] - 1.0).abs() < 1e-8);
assert!(yf[1].abs() < 1e-8);
}
#[test]
fn rk4_backward() {
let y0 = Vector::from_array([1.0_f64, 0.0]);
let yf = rk4(0.0, -TAU, 0.001, &y0, ydot);
assert!((yf[0] - 1.0).abs() < 1e-8);
assert!(yf[1].abs() < 1e-8);
}
#[cfg(feature = "std")]
mod adaptive_tests {
use super::*;
fn tight_settings() -> AdaptiveSettings<f64> {
AdaptiveSettings {
abs_tol: 1e-12,
rel_tol: 1e-12,
..AdaptiveSettings::default()
}
}
fn test_harmonic<const N: usize, const NI: usize, S: RKAdaptive<N, NI>>() {
let y0 = Vector::from_array([1.0_f64, 0.0]);
let settings = tight_settings();
let sol = S::integrate(0.0, TAU, &y0, ydot, &settings).unwrap();
assert!((sol.y[0] - 1.0).abs() < 1e-10);
assert!(sol.y[1].abs() < 1e-10);
}
#[test]
fn harmonic_rkf45() {
test_harmonic::<6, 1, RKF45>();
}
#[test]
fn harmonic_rkts54() {
test_harmonic::<7, 4, RKTS54>();
}
#[test]
fn harmonic_rkv65() {
test_harmonic::<10, 6, RKV65>();
}
#[test]
fn harmonic_rkv87() {
test_harmonic::<17, 7, RKV87>();
}
#[test]
fn harmonic_rkv98() {
test_harmonic::<21, 8, RKV98>();
}
#[test]
fn harmonic_rkv98_nointerp() {
test_harmonic::<16, 1, RKV98NoInterp>();
}
#[test]
fn harmonic_rkv98_efficient() {
test_harmonic::<26, 9, RKV98Efficient>();
}
#[test]
fn backward_rkts54() {
let y0 = Vector::from_array([1.0_f64, 0.0]);
let settings = tight_settings();
let sol = RKTS54::integrate(0.0, -TAU, &y0, ydot, &settings).unwrap();
assert!((sol.y[0] - 1.0).abs() < 1e-10);
assert!(sol.y[1].abs() < 1e-10);
}
#[test]
fn backward_rkv87() {
let y0 = Vector::from_array([1.0_f64, 0.0]);
let settings = tight_settings();
let sol = RKV87::integrate(0.0, -TAU, &y0, ydot, &settings).unwrap();
assert!((sol.y[0] - 1.0).abs() < 1e-10);
assert!(sol.y[1].abs() < 1e-10);
}
#[test]
fn fsal_saves_evaluations() {
let y0 = Vector::from_array([1.0_f64, 0.0]);
let settings = tight_settings();
let sol_fsal = RKTS54::integrate(0.0, TAU, &y0, ydot, &settings).unwrap();
let sol_nonfsal = RKF45::integrate(0.0, TAU, &y0, ydot, &settings).unwrap();
assert!(sol_fsal.evals < sol_fsal.accepted * 7);
assert!((sol_fsal.y[0] - 1.0).abs() < 1e-10);
assert!((sol_nonfsal.y[0] - 1.0).abs() < 1e-10);
}
#[test]
fn rkv98_variants_agree() {
let y0 = Vector::from_array([1.0_f64, 0.0]);
let settings = tight_settings();
let sol_robust = RKV98::integrate(0.0, TAU, &y0, ydot, &settings).unwrap();
let sol_nointerp = RKV98NoInterp::integrate(0.0, TAU, &y0, ydot, &settings).unwrap();
let sol_efficient = RKV98Efficient::integrate(0.0, TAU, &y0, ydot, &settings).unwrap();
assert!((sol_robust.y[0] - 1.0).abs() < 1e-11);
assert!((sol_nointerp.y[0] - 1.0).abs() < 1e-11);
assert!((sol_efficient.y[0] - 1.0).abs() < 1e-11);
}
#[test]
fn tighter_tolerance_improves_accuracy() {
let y0 = Vector::from_array([1.0_f64, 0.0]);
let tols = [1e-4, 1e-6, 1e-8, 1e-10, 1e-12];
let mut prev_err = f64::MAX;
for &tol in &tols {
let settings = AdaptiveSettings {
abs_tol: tol,
rel_tol: tol,
..AdaptiveSettings::default()
};
let sol = RKTS54::integrate(0.0, TAU, &y0, ydot, &settings).unwrap();
let err = (sol.y[0] - 1.0).abs() + sol.y[1].abs();
assert!(err < prev_err, "tol={tol}: error {err} not smaller than previous {prev_err}");
prev_err = err;
}
}
#[test]
fn max_steps_exceeded() {
let y0 = Vector::from_array([1.0_f64, 0.0]);
let settings = AdaptiveSettings {
max_steps: 5,
..tight_settings()
};
let result = RKTS54::integrate(0.0, TAU, &y0, ydot, &settings);
assert!(matches!(result, Err(OdeError::MaxStepsExceeded)));
}
#[test]
fn harmonic_f32() {
let y0 = Vector::from_array([1.0_f32, 0.0]);
let settings = AdaptiveSettings::<f32> {
abs_tol: 1e-6,
rel_tol: 1e-6,
..AdaptiveSettings::default()
};
let sol = RKTS54::integrate(
0.0_f32,
core::f32::consts::TAU,
&y0,
|_t, y| Vector::from_array([y[1], -y[0]]),
&settings,
)
.unwrap();
assert!((sol.y[0] - 1.0).abs() < 1e-4);
assert!(sol.y[1].abs() < 1e-4);
}
fn test_interp<const N: usize, const NI: usize, S: RKAdaptive<N, NI>>() {
let y0 = Vector::from_array([1.0_f64, 0.0]);
let settings = AdaptiveSettings {
abs_tol: 1e-12,
rel_tol: 1e-12,
dense_output: true,
..AdaptiveSettings::default()
};
let sol = S::integrate(0.0, PI, &y0, ydot, &settings).unwrap();
for i in 0..=100 {
let t = PI * (i as f64) / 100.0;
let y_interp = S::interpolate(t, &sol).unwrap();
assert!((y_interp[0] - t.cos()).abs() < 1e-9);
assert!((y_interp[1] - (-t.sin())).abs() < 1e-9);
}
}
#[test]
fn interp_rkts54() {
test_interp::<7, 4, RKTS54>();
}
#[test]
fn interp_rkv65() {
test_interp::<10, 6, RKV65>();
}
#[test]
fn interp_rkv87() {
test_interp::<17, 7, RKV87>();
}
#[test]
fn interp_rkv98() {
test_interp::<21, 8, RKV98>();
}
#[test]
fn interp_rkv98_efficient() {
test_interp::<26, 9, RKV98Efficient>();
}
#[test]
fn interp_nointerp_returns_error() {
let y0 = Vector::from_array([1.0_f64, 0.0]);
let settings = AdaptiveSettings {
dense_output: true,
..tight_settings()
};
let sol = RKV98NoInterp::integrate(0.0, PI, &y0, ydot, &settings).unwrap();
assert_eq!(RKV98NoInterp::interpolate(0.5, &sol).unwrap_err(), OdeError::InterpNotImplemented);
}
#[test]
fn interp_out_of_bounds() {
let y0 = Vector::from_array([1.0_f64, 0.0]);
let settings = AdaptiveSettings {
dense_output: true,
..tight_settings()
};
let sol = RKTS54::integrate(0.0, PI, &y0, ydot, &settings).unwrap();
assert_eq!(RKTS54::interpolate(PI + 1.0, &sol).unwrap_err(), OdeError::InterpOutOfBounds);
}
#[test]
fn interp_no_dense() {
let y0 = Vector::from_array([1.0_f64, 0.0]);
let settings = tight_settings();
let sol = RKTS54::integrate(0.0, PI, &y0, ydot, &settings).unwrap();
assert_eq!(RKTS54::interpolate(0.5, &sol).unwrap_err(), OdeError::NoDenseOutput);
}
#[test]
fn interp_backward() {
let y0 = Vector::from_array([1.0_f64, 0.0]);
let settings = AdaptiveSettings {
dense_output: true,
..tight_settings()
};
let sol = RKTS54::integrate(0.0, -PI, &y0, ydot, &settings).unwrap();
for i in 0..=10 {
let t = -PI * (i as f64) / 10.0;
let y_interp = RKTS54::interpolate(t, &sol).unwrap();
assert!((y_interp[0] - t.cos()).abs() < 1e-9);
}
}
#[test]
fn rodas4_stiff_exponential_decay() {
let y0 = Vector::from_array([1.0_f64]);
let settings = AdaptiveSettings {
abs_tol: 1e-10,
rel_tol: 1e-10,
..AdaptiveSettings::default()
};
let sol = RODAS4::integrate(
0.0, 0.01, &y0,
|_t, y| Vector::from_array([-1000.0 * y[0]]),
|_t, _y| crate::Matrix::new([[-1000.0]]),
&settings,
).unwrap();
let exact = (-1000.0_f64 * 0.01).exp();
assert!(
(sol.y[0] - exact).abs() < 1e-8,
"rodas4 stiff decay: y = {}, exact = {}, err = {}",
sol.y[0], exact, (sol.y[0] - exact).abs()
);
}
#[test]
fn rodas4_van_der_pol_mu1000() {
let mu = 1000.0_f64;
let y0 = Vector::from_array([2.0, 0.0]);
let settings = AdaptiveSettings {
abs_tol: 1e-6,
rel_tol: 1e-6,
max_steps: 500_000,
..AdaptiveSettings::default()
};
let sol = RODAS4::integrate(
0.0, 100.0, &y0,
|_t, y| Vector::from_array([
y[1],
mu * ((1.0 - y[0] * y[0]) * y[1] - y[0]),
]),
|_t, y| crate::Matrix::new([
[0.0, 1.0],
[mu * (-2.0 * y[0] * y[1] - 1.0), mu * (1.0 - y[0] * y[0])],
]),
&settings,
).unwrap();
assert!(
sol.y[0].abs() < 3.0,
"Van der Pol y[0] = {} exceeds bound", sol.y[0]
);
}
#[test]
fn rodas4_robertson() {
let y0 = Vector::from_array([1.0, 0.0, 0.0]);
let settings = AdaptiveSettings {
abs_tol: 1e-8,
rel_tol: 1e-8,
..AdaptiveSettings::default()
};
let sol = RODAS4::integrate(
0.0, 1e3, &y0,
|_t, y| Vector::from_array([
-0.04 * y[0] + 1e4 * y[1] * y[2],
0.04 * y[0] - 1e4 * y[1] * y[2] - 3e7 * y[1] * y[1],
3e7 * y[1] * y[1],
]),
|_t, y| crate::Matrix::new([
[-0.04, 1e4 * y[2], 1e4 * y[1]],
[ 0.04, -1e4 * y[2] - 6e7 * y[1], -1e4 * y[1]],
[ 0.0, 6e7 * y[1], 0.0],
]),
&settings,
).unwrap();
let mass: f64 = sol.y[0] + sol.y[1] + sol.y[2];
assert!(
(mass - 1.0).abs() < 1e-6,
"Robertson mass conservation violated: sum = {}", mass
);
assert!(sol.y[0] >= -1e-10, "y[0] = {} is negative", sol.y[0]);
assert!(sol.y[1] >= -1e-10, "y[1] = {} is negative", sol.y[1]);
assert!(sol.y[2] >= -1e-10, "y[2] = {} is negative", sol.y[2]);
}
#[test]
fn rodas4_auto_jacobian_stiff_decay() {
let y0 = Vector::from_array([1.0_f64]);
let settings = AdaptiveSettings {
abs_tol: 1e-8,
rel_tol: 1e-8,
..AdaptiveSettings::default()
};
let sol = RODAS4::integrate_auto(
0.0, 0.01, &y0,
|_t, y| Vector::from_array([-1000.0 * y[0]]),
&settings,
).unwrap();
let exact = (-1000.0_f64 * 0.01).exp();
assert!(
(sol.y[0] - exact).abs() < 1e-6,
"auto-jac stiff decay: y = {}, exact = {}, err = {}",
sol.y[0], exact, (sol.y[0] - exact).abs()
);
}
#[test]
fn rodas4_auto_jacobian_robertson() {
let y0 = Vector::from_array([1.0, 0.0, 0.0]);
let settings = AdaptiveSettings {
abs_tol: 1e-6,
rel_tol: 1e-6,
..AdaptiveSettings::default()
};
let sol = RODAS4::integrate_auto(
0.0, 1e3, &y0,
|_t, y| Vector::from_array([
-0.04 * y[0] + 1e4 * y[1] * y[2],
0.04 * y[0] - 1e4 * y[1] * y[2] - 3e7 * y[1] * y[1],
3e7 * y[1] * y[1],
]),
&settings,
).unwrap();
let mass: f64 = sol.y[0] + sol.y[1] + sol.y[2];
assert!(
(mass - 1.0).abs() < 1e-4,
"auto-jac Robertson mass conservation: sum = {}", mass
);
}
#[test]
fn too_many_rejections_explicit() {
let y0 = Vector::from_array([1.0_f64, 0.0]);
let settings = AdaptiveSettings {
abs_tol: 1e-100,
rel_tol: 1e-100,
min_step: 0.0, max_steps: 1_000_000,
..AdaptiveSettings::default()
};
let result = RKTS54::integrate(0.0, TAU, &y0, ydot, &settings);
assert!(matches!(result, Err(OdeError::TooManyRejections)));
}
#[test]
fn too_many_rejections_rosenbrock() {
let y0 = Vector::from_array([1.0_f64]);
let settings = AdaptiveSettings {
abs_tol: 1e-100,
rel_tol: 1e-100,
min_step: 0.0,
max_steps: 1_000_000,
..AdaptiveSettings::default()
};
let result = RODAS4::integrate(
0.0, 0.01, &y0,
|_t, y| Vector::from_array([-1000.0 * y[0]]),
|_t, _y| crate::Matrix::new([[-1000.0]]),
&settings,
);
assert!(matches!(result, Err(OdeError::TooManyRejections)));
}
#[test]
fn h_min_prevents_rejection_loop() {
let y0 = Vector::from_array([1.0_f64, 0.0]);
let settings_no_hmin = AdaptiveSettings {
abs_tol: 1e-100,
rel_tol: 1e-100,
min_step: 0.0,
max_steps: 1_000_000,
..AdaptiveSettings::default()
};
let result_no_hmin = RKTS54::integrate(0.0, TAU, &y0, ydot, &settings_no_hmin);
assert!(matches!(result_no_hmin, Err(OdeError::TooManyRejections)));
let settings_hmin = AdaptiveSettings {
abs_tol: 1e-100,
rel_tol: 1e-100,
min_step: 0.0,
max_steps: 1_000_000,
h_min: Some(0.5),
..AdaptiveSettings::default()
};
let result_hmin = RKTS54::integrate(0.0, TAU, &y0, ydot, &settings_hmin);
assert!(!matches!(result_hmin, Err(OdeError::TooManyRejections)));
}
#[test]
fn rodas4_singular_w_matrix() {
let y0 = Vector::from_array([f64::NAN]);
let settings = AdaptiveSettings::default();
let result = RODAS4::integrate(
0.0, 1.0, &y0,
|_t, y| *y,
|_t, _y| crate::Matrix::new([[1.0]]),
&settings,
);
assert!(matches!(result, Err(OdeError::StepNotFinite)));
}
#[test]
fn rk4_matrix_exponential() {
let a = Matrix::new([[0.0_f64, 1.0], [-1.0, 0.0]]);
let x0: Matrix<f64, 2, 2> = Matrix::eye();
let xf = rk4(0.0, TAU, 0.001, &x0, |_t, x| a * *x);
assert!((xf[(0, 0)] - 1.0).abs() < 1e-7, "x00 = {}", xf[(0, 0)]);
assert!(xf[(0, 1)].abs() < 1e-7, "x01 = {}", xf[(0, 1)]);
assert!(xf[(1, 0)].abs() < 1e-7, "x10 = {}", xf[(1, 0)]);
assert!((xf[(1, 1)] - 1.0).abs() < 1e-7, "x11 = {}", xf[(1, 1)]);
}
#[test]
fn adaptive_matrix_exponential() {
let a = Matrix::new([[0.0_f64, 1.0], [-1.0, 0.0]]);
let x0: Matrix<f64, 2, 2> = Matrix::eye();
let settings = AdaptiveSettings {
abs_tol: 1e-10,
rel_tol: 1e-10,
..AdaptiveSettings::default()
};
let sol = RKTS54::integrate(0.0, TAU, &x0, |_t, x| a * *x, &settings).unwrap();
assert!((sol.y[(0, 0)] - 1.0).abs() < 1e-8, "x00 = {}", sol.y[(0, 0)]);
assert!(sol.y[(0, 1)].abs() < 1e-8, "x01 = {}", sol.y[(0, 1)]);
assert!(sol.y[(1, 0)].abs() < 1e-8, "x10 = {}", sol.y[(1, 0)]);
assert!((sol.y[(1, 1)] - 1.0).abs() < 1e-8, "x11 = {}", sol.y[(1, 1)]);
}
#[test]
fn rodas4_harmonic_oscillator() {
let y0 = Vector::from_array([1.0_f64, 0.0]);
let settings = AdaptiveSettings {
abs_tol: 1e-10,
rel_tol: 1e-10,
..AdaptiveSettings::default()
};
let sol = RODAS4::integrate(
0.0, TAU, &y0,
ydot,
|_t, _y| crate::Matrix::new([[0.0, 1.0], [-1.0, 0.0]]),
&settings,
).unwrap();
assert!(
(sol.y[0] - 1.0).abs() < 1e-6,
"harmonic y[0] = {}, err = {}", sol.y[0], (sol.y[0] - 1.0).abs()
);
assert!(
sol.y[1].abs() < 1e-6,
"harmonic y[1] = {}", sol.y[1]
);
}
}