use nalgebra::{SVector, vector};
use proptest::prelude::*;
use crate::test_systems::*;
use crate::{Integrator, Rk4, State, StormerVerlet};
proptest! {
#[test]
fn rk4_not_time_reversible(
x0 in -10.0f64..10.0,
v0 in -5.0f64..5.0,
) {
let system = HarmonicOscillator1D;
let initial = State::<1, 2>::new(SVector::from([x0]), SVector::from([v0]));
let dt = 0.1;
let n_steps = 50;
let mut state = initial.clone();
let mut t = 0.0;
for _ in 0..n_steps {
state = Rk4.step(&system, t, &state, dt);
t += dt;
}
for _ in 0..n_steps {
t -= dt;
state = Rk4.step(&system, t, &state, -dt);
}
let x_err = (state.y()[0] - x0).abs();
let v_err = (state.dy()[0] - v0).abs();
let total_err = x_err + v_err;
prop_assert!(
total_err > 1e-14,
"RK4 should NOT be time-reversible, but error={total_err:.2e}"
);
}
}
fn energy_drift_halves<F>(integrator: F, t_end: f64, dt: f64) -> (f64, f64)
where
F: Fn(
&HarmonicOscillator,
State<3, 2>,
f64,
f64,
f64,
&mut dyn FnMut(f64, &State<3, 2>),
) -> State<3, 2>,
{
let system = HarmonicOscillator;
let initial = State::<3, 2>::new(vector![1.0, 0.0, 0.0], vector![0.0, 0.0, 0.0]);
let initial_energy = 0.5;
let t_mid = t_end / 2.0;
let mut first_half: f64 = 0.0;
let mut second_half: f64 = 0.0;
integrator(&system, initial, 0.0, t_end, dt, &mut |t,
state: &State<
3,
2,
>| {
let energy = 0.5 * (state.dy().norm_squared() + state.y().norm_squared());
let drift = (energy - initial_energy).abs();
if t < t_mid {
first_half = first_half.max(drift);
} else {
second_half = second_half.max(drift);
}
});
(first_half, second_half)
}
#[test]
fn verlet_no_secular_energy_drift() {
let dt = 0.05;
let t_end = 1000.0 * 2.0 * std::f64::consts::PI;
let (first, second) = energy_drift_halves(
|sys, init, t0, te, dt, cb| StormerVerlet.integrate(sys, init, t0, te, dt, cb),
t_end,
dt,
);
let ratio = second / first;
assert!(
ratio < 1.2,
"Verlet energy drift ratio (2nd/1st half) should be ~1.0, got {ratio:.2} \
(first={first:.2e}, second={second:.2e})"
);
}
#[test]
fn rk4_has_secular_energy_drift() {
let dt = 0.05;
let t_end = 1000.0 * 2.0 * std::f64::consts::PI;
let (first, second) = energy_drift_halves(
|sys, init, t0, te, dt, cb| Rk4.integrate(sys, init, t0, te, dt, cb),
t_end,
dt,
);
let ratio = second / first;
assert!(
ratio > 1.5,
"RK4 energy drift ratio (2nd/1st half) should be ~2.0, got {ratio:.2} \
(first={first:.2e}, second={second:.2e})"
);
}
#[test]
fn verlet_less_accurate_per_step_than_rk4() {
let system = HarmonicOscillator;
let initial = State::<3, 2>::new(vector![1.0, 0.0, 0.0], vector![0.0, 0.0, 0.0]);
let dt = 0.01;
let t_end = 2.0 * std::f64::consts::PI;
let verlet_final = StormerVerlet.integrate(&system, initial.clone(), 0.0, t_end, dt, |_, _| {});
let rk4_final = Rk4.integrate(&system, initial, 0.0, t_end, dt, |_, _| {});
let verlet_err = (verlet_final.y().x - 1.0).abs();
let rk4_err = (rk4_final.y().x - 1.0).abs();
assert!(
rk4_err < verlet_err,
"RK4 ({rk4_err:.2e}) should be more accurate than Verlet ({verlet_err:.2e}) at same dt"
);
assert!(
verlet_err / rk4_err > 100.0,
"Verlet/RK4 error ratio should be large (2nd vs 4th order): {:.0}x",
verlet_err / rk4_err
);
}