mod coeff;
use core::ops::ControlFlow;
use coeff::*;
#[allow(unused_imports)]
use crate::math::F64Ext;
use crate::{
DynamicalSystem, IntegrationError, IntegrationOutcome, Integrator, OdeState, Tolerances,
};
pub struct Dop853;
fn dop853_step_impl<S: DynamicalSystem>(
system: &S,
t: f64,
state: &S::State,
dt: f64,
k1: &S::State,
) -> (S::State, S::State, S::State) {
let s2 = state.axpy(dt * A21, k1);
let k2 = system.derivatives(t + C2 * dt, &s2);
let s3 = state.axpy(dt * A31, k1).axpy(dt * A32, &k2);
let k3 = system.derivatives(t + C3 * dt, &s3);
let s4 = state.axpy(dt * A41, k1).axpy(dt * A43, &k3);
let k4 = system.derivatives(t + C4 * dt, &s4);
let s5 = state
.axpy(dt * A51, k1)
.axpy(dt * A53, &k3)
.axpy(dt * A54, &k4);
let k5 = system.derivatives(t + C5 * dt, &s5);
let s6 = state
.axpy(dt * A61, k1)
.axpy(dt * A64, &k4)
.axpy(dt * A65, &k5);
let k6 = system.derivatives(t + C6 * dt, &s6);
let s7 = state
.axpy(dt * A71, k1)
.axpy(dt * A74, &k4)
.axpy(dt * A75, &k5)
.axpy(dt * A76, &k6);
let k7 = system.derivatives(t + C7 * dt, &s7);
let s8 = state
.axpy(dt * A81, k1)
.axpy(dt * A84, &k4)
.axpy(dt * A85, &k5)
.axpy(dt * A86, &k6)
.axpy(dt * A87, &k7);
let k8 = system.derivatives(t + C8 * dt, &s8);
let s9 = state
.axpy(dt * A91, k1)
.axpy(dt * A94, &k4)
.axpy(dt * A95, &k5)
.axpy(dt * A96, &k6)
.axpy(dt * A97, &k7)
.axpy(dt * A98, &k8);
let k9 = system.derivatives(t + C9 * dt, &s9);
let s10 = state
.axpy(dt * A101, k1)
.axpy(dt * A104, &k4)
.axpy(dt * A105, &k5)
.axpy(dt * A106, &k6)
.axpy(dt * A107, &k7)
.axpy(dt * A108, &k8)
.axpy(dt * A109, &k9);
let k10 = system.derivatives(t + C10 * dt, &s10);
let s11 = state
.axpy(dt * A111, k1)
.axpy(dt * A114, &k4)
.axpy(dt * A115, &k5)
.axpy(dt * A116, &k6)
.axpy(dt * A117, &k7)
.axpy(dt * A118, &k8)
.axpy(dt * A119, &k9)
.axpy(dt * A1110, &k10);
let k11 = system.derivatives(t + C11 * dt, &s11);
let s12 = state
.axpy(dt * A121, k1)
.axpy(dt * A124, &k4)
.axpy(dt * A125, &k5)
.axpy(dt * A126, &k6)
.axpy(dt * A127, &k7)
.axpy(dt * A128, &k8)
.axpy(dt * A129, &k9)
.axpy(dt * A1210, &k10)
.axpy(dt * A1211, &k11);
let k12 = system.derivatives(t + dt, &s12);
let y8 = state
.axpy(dt * B1, k1)
.axpy(dt * B6, &k6)
.axpy(dt * B7, &k7)
.axpy(dt * B8, &k8)
.axpy(dt * B9, &k9)
.axpy(dt * B10, &k10)
.axpy(dt * B11, &k11)
.axpy(dt * B12, &k12);
let k13 = system.derivatives(t + dt, &y8);
let err5 = k1
.scale(ER1)
.axpy(ER6, &k6)
.axpy(ER7, &k7)
.axpy(ER8, &k8)
.axpy(ER9, &k9)
.axpy(ER10, &k10)
.axpy(ER11, &k11)
.axpy(ER12, &k12)
.scale(dt);
let err3 = k1
.scale(B1 - BHH1)
.axpy(B6, &k6)
.axpy(B7, &k7)
.axpy(B8, &k8)
.axpy(B9 - BHH2, &k9)
.axpy(B10, &k10)
.axpy(B11, &k11)
.axpy(B12 - BHH3, &k12)
.scale(dt);
let _ = err3; let error = err5;
(y8, error, k13)
}
impl Integrator for Dop853 {
fn step<S: DynamicalSystem>(&self, system: &S, t: f64, state: &S::State, dt: f64) -> S::State {
let k1 = system.derivatives(t, state);
let (y8, _, _) = dop853_step_impl(system, t, state, dt, &k1);
y8
}
}
pub enum AdvanceOutcome853<B> {
Reached,
Event { reason: B },
}
pub struct AdaptiveStepper853<'a, S: DynamicalSystem> {
system: &'a S,
state: S::State,
t: f64,
dt: f64,
k1: S::State,
tol: Tolerances,
pub dt_min: f64,
}
impl<'a, S: DynamicalSystem> AdaptiveStepper853<'a, S> {
pub fn advance_to<F, E, B>(
&mut self,
t_target: f64,
mut callback: F,
event_check: E,
) -> Result<AdvanceOutcome853<B>, IntegrationError>
where
F: FnMut(f64, &S::State),
E: Fn(f64, &S::State) -> ControlFlow<B>,
{
while self.t < t_target {
let h = self.dt.min(t_target - self.t);
let (y8, error, k13) = dop853_step_impl(self.system, self.t, &self.state, h, &self.k1);
if !y8.is_finite() {
return Err(IntegrationError::NonFiniteState { t: self.t + h });
}
let err = self.state.error_norm(&y8, &error, &self.tol);
if err <= 1.0 {
self.state = y8;
self.t += h;
self.k1 = k13;
callback(self.t, &self.state);
if let ControlFlow::Break(reason) = event_check(self.t, &self.state) {
return Ok(AdvanceOutcome853::Event { reason });
}
let factor = if err < 1e-15 {
MAX_FACTOR
} else {
(SAFETY * err.powf(-ORDER_EXP)).clamp(MIN_FACTOR, MAX_FACTOR)
};
self.dt = h * factor;
} else {
let factor = (SAFETY * err.powf(-ORDER_EXP)).clamp(MIN_FACTOR, 1.0);
self.dt = h * factor;
if self.dt < self.dt_min {
return Err(IntegrationError::StepSizeTooSmall {
t: self.t,
dt: self.dt,
});
}
}
}
Ok(AdvanceOutcome853::Reached)
}
pub fn state(&self) -> &S::State {
&self.state
}
pub fn t(&self) -> f64 {
self.t
}
pub fn dt(&self) -> f64 {
self.dt
}
pub fn into_state(self) -> S::State {
self.state
}
}
impl Dop853 {
pub fn stepper<'a, S: DynamicalSystem>(
&self,
system: &'a S,
initial: S::State,
t0: f64,
dt: f64,
tol: Tolerances,
) -> AdaptiveStepper853<'a, S> {
let k1 = system.derivatives(t0, &initial);
let dt_min = 1e-12 * (dt * 100.0).abs().max(1.0);
AdaptiveStepper853 {
system,
state: initial,
t: t0,
dt,
k1,
tol,
dt_min,
}
}
pub fn step_full<S: DynamicalSystem>(
&self,
system: &S,
t: f64,
state: &S::State,
dt: f64,
) -> (S::State, S::State, S::State) {
let k1 = system.derivatives(t, state);
dop853_step_impl(system, t, state, dt, &k1)
}
#[allow(clippy::too_many_arguments)]
pub fn integrate_adaptive_with_events<S, F, E, B>(
&self,
system: &S,
initial: S::State,
t0: f64,
t_end: f64,
dt_initial: f64,
tol: &Tolerances,
callback: F,
event_check: E,
) -> IntegrationOutcome<S::State, B>
where
S: DynamicalSystem,
F: FnMut(f64, &S::State),
E: Fn(f64, &S::State) -> ControlFlow<B>,
{
let mut stepper =
self.stepper(system, initial, t0, dt_initial.min(t_end - t0), tol.clone());
stepper.dt_min = 1e-12 * (t_end - t0).abs().max(1.0);
match stepper.advance_to(t_end, callback, event_check) {
Ok(AdvanceOutcome853::Reached) => IntegrationOutcome::Completed(stepper.into_state()),
Ok(AdvanceOutcome853::Event { reason }) => {
let t = stepper.t();
IntegrationOutcome::Terminated {
state: stepper.into_state(),
t,
reason,
}
}
Err(e) => IntegrationOutcome::Error(e),
}
}
}
#[cfg(test)]
mod tests {
use core::ops::ControlFlow;
use nalgebra::vector;
use crate::test_systems::*;
use crate::{IntegrationError, IntegrationOutcome, Integrator, State, Tolerances};
use super::*;
#[test]
fn step_uniform_motion_exact() {
let system = UniformMotion {
constant_velocity: vector![1.0, 0.0, 0.0],
};
let state = State::<3, 2>::new(vector![0.0, 0.0, 0.0], vector![1.0, 0.0, 0.0]);
let (y8, _error, _k13) = Dop853.step_full(&system, 0.0, &state, 1.0);
let eps = 1e-12;
assert!((y8.y().x - 1.0).abs() < eps, "y8 pos: {}", y8.y().x);
assert!((y8.dy().x - 1.0).abs() < eps, "y8 vel: {}", y8.dy().x);
}
#[test]
fn step_constant_acceleration_exact() {
let system = ConstantAcceleration {
acceleration: vector![0.0, -9.8, 0.0],
};
let state = State::<3, 2>::new(vector![0.0, 0.0, 0.0], vector![10.0, 20.0, 0.0]);
let dt = 1.0;
let (y8, _error, _k13) = Dop853.step_full(&system, 0.0, &state, dt);
let expected_px = 10.0;
let expected_py = 20.0 + 0.5 * (-9.8) * 1.0;
let expected_vy = 20.0 + (-9.8) * 1.0;
let eps = 1e-12;
assert!((y8.y().x - expected_px).abs() < eps);
assert!((y8.y().y - expected_py).abs() < eps);
assert!((y8.dy().y - expected_vy).abs() < eps);
}
#[test]
fn step_error_estimate_reasonable() {
let system = HarmonicOscillator;
let state = State::<3, 2>::new(vector![1.0, 0.0, 0.0], vector![0.0, 0.0, 0.0]);
let dt = 0.5;
let (y8, error, _k13) = Dop853.step_full(&system, 0.0, &state, dt);
let analytical_x = dt.cos();
let actual_err = (y8.y().x - analytical_x).abs();
let estimated_err = error.y().x.abs();
assert!(actual_err > 0.0, "Actual error should be nonzero");
assert!(estimated_err > 0.0, "Estimated error should be nonzero");
let ratio = actual_err / estimated_err;
assert!(
ratio > 1e-4 && ratio < 1e4,
"Error estimate ratio: actual={actual_err:.2e}, estimated={estimated_err:.2e}, ratio={ratio:.2}"
);
}
#[test]
fn step_fsal_property() {
let system = HarmonicOscillator;
let state = State::<3, 2>::new(vector![1.0, 0.0, 0.0], vector![0.0, 0.0, 0.0]);
let dt = 0.1;
let (y8, _error, k13) = Dop853.step_full(&system, 0.0, &state, dt);
let k1_next = system.derivatives(dt, &y8);
let eps = 1e-14;
assert!(
(k13.y() - k1_next.y()).magnitude() < eps,
"FSAL velocity mismatch: {:?} vs {:?}",
k13.y(),
k1_next.y()
);
assert!(
(k13.dy() - k1_next.dy()).magnitude() < eps,
"FSAL acceleration mismatch: {:?} vs {:?}",
k13.dy(),
k1_next.dy()
);
}
#[test]
fn step_local_truncation_order() {
let system = HarmonicOscillator;
let state = State::<3, 2>::new(vector![1.0, 0.0, 0.0], vector![0.0, 0.0, 0.0]);
let dt1 = 0.5;
let dt2 = 0.25;
let (y8_coarse, _, _) = Dop853.step_full(&system, 0.0, &state, dt1);
let (y8_fine, _, _) = Dop853.step_full(&system, 0.0, &state, dt2);
let err_coarse = (y8_coarse.y().x - dt1.cos()).abs();
let err_fine = (y8_fine.y().x - dt2.cos()).abs();
let ratio = err_coarse / err_fine;
assert!(
ratio > 200.0 && ratio < 1500.0,
"Local truncation order ratio = {ratio:.2}, expected ~512 (err_coarse={err_coarse:.2e}, err_fine={err_fine:.2e})"
);
}
#[test]
fn integrate_uniform_motion() {
let system = UniformMotion {
constant_velocity: vector![1.0, 0.0, 0.0],
};
let initial = State::<3, 2>::new(vector![0.0, 0.0, 0.0], vector![1.0, 0.0, 0.0]);
let final_state = Dop853.integrate(&system, initial, 0.0, 1.0, 0.1, |_, _| {});
assert!((final_state.y().x - 1.0).abs() < 1e-12);
}
#[test]
fn integrate_harmonic_full_period() {
let system = HarmonicOscillator;
let initial = State::<3, 2>::new(vector![1.0, 0.0, 0.0], vector![0.0, 0.0, 0.0]);
let t_end = 2.0 * std::f64::consts::PI;
let dt = 0.01;
let final_state = Dop853.integrate(&system, initial, 0.0, t_end, dt, |_, _| {});
let eps = 1e-13;
assert!(
(final_state.y().x - 1.0).abs() < eps,
"After full period, x should be ~1.0, got {} (err={:.2e})",
final_state.y().x,
(final_state.y().x - 1.0).abs()
);
assert!(
final_state.dy().x.abs() < eps,
"After full period, vx should be ~0.0, got {} (err={:.2e})",
final_state.dy().x,
final_state.dy().x.abs()
);
}
#[test]
fn integrate_8th_order_convergence() {
fn harmonic_error(dt: f64, steps: usize) -> f64 {
let system = HarmonicOscillator;
let mut state = State::<3, 2>::new(vector![1.0, 0.0, 0.0], vector![0.0, 0.0, 0.0]);
let mut t = 0.0;
for _ in 0..steps {
let (y8, _, _) = Dop853.step_full(&system, t, &state, dt);
state = y8;
t += dt;
}
let x_error = (state.y().x - t.cos()).abs();
let v_error = (state.dy().x + t.sin()).abs();
x_error.max(v_error)
}
let err_coarse = harmonic_error(0.5, 20);
let err_fine = harmonic_error(0.25, 40);
let ratio = err_coarse / err_fine;
assert!(
ratio > 100.0 && ratio < 700.0,
"DOP853 global convergence ratio = {ratio:.2}, expected ~256 (err_coarse={err_coarse:.2e}, err_fine={err_fine:.2e})"
);
}
#[test]
fn adaptive_completes_normally() {
let system = UniformMotion {
constant_velocity: vector![1.0, 0.0, 0.0],
};
let initial = State::<3, 2>::new(vector![0.0, 0.0, 0.0], vector![1.0, 0.0, 0.0]);
let tol = Tolerances::default();
let outcome: IntegrationOutcome<State<3, 2>, ()> = Dop853.integrate_adaptive_with_events(
&system,
initial,
0.0,
1.0,
0.1,
&tol,
|_t, _state| {},
|_t, _state| ControlFlow::Continue(()),
);
match outcome {
IntegrationOutcome::Completed(state) => {
assert!(
(state.y().x - 1.0).abs() < 1e-8,
"Expected position ~1.0, got {}",
state.y().x
);
}
other => panic!("Expected Completed, got {other:?}"),
}
}
#[test]
fn adaptive_harmonic_full_period() {
let system = HarmonicOscillator;
let initial = State::<3, 2>::new(vector![1.0, 0.0, 0.0], vector![0.0, 0.0, 0.0]);
let t_end = 2.0 * std::f64::consts::PI;
let tol = Tolerances {
atol: 1e-10,
rtol: 1e-8,
};
let outcome: IntegrationOutcome<State<3, 2>, ()> = Dop853.integrate_adaptive_with_events(
&system,
initial,
0.0,
t_end,
0.1,
&tol,
|_t, _state| {},
|_t, _state| ControlFlow::Continue(()),
);
match outcome {
IntegrationOutcome::Completed(state) => {
let eps = 1e-6;
assert!(
(state.y().x - 1.0).abs() < eps,
"After full period, x={} (err={:.2e})",
state.y().x,
(state.y().x - 1.0).abs()
);
assert!(
state.dy().x.abs() < eps,
"After full period, vx={} (err={:.2e})",
state.dy().x,
state.dy().x.abs()
);
}
other => panic!("Expected Completed, got {other:?}"),
}
}
#[test]
fn adaptive_energy_conservation() {
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 * (initial.dy().norm_squared() + initial.y().norm_squared());
let mut max_energy_drift: f64 = 0.0;
let t_end = 2.0 * std::f64::consts::PI;
let tol = Tolerances {
atol: 1e-10,
rtol: 1e-8,
};
let outcome: IntegrationOutcome<State<3, 2>, ()> = Dop853.integrate_adaptive_with_events(
&system,
initial,
0.0,
t_end,
0.1,
&tol,
|_t, state| {
let energy = 0.5 * (state.dy().norm_squared() + state.y().norm_squared());
let drift = (energy - initial_energy).abs();
max_energy_drift = max_energy_drift.max(drift);
},
|_t, _state| ControlFlow::Continue(()),
);
assert!(matches!(outcome, IntegrationOutcome::Completed(_)));
assert!(
max_energy_drift < 1e-7,
"Energy drift {max_energy_drift:.2e} too large"
);
}
#[test]
fn adaptive_lands_on_t_end() {
let system = HarmonicOscillator;
let initial = State::<3, 2>::new(vector![1.0, 0.0, 0.0], vector![0.0, 0.0, 0.0]);
let t_end = 1.234;
let tol = Tolerances::default();
let mut last_t = 0.0;
let outcome: IntegrationOutcome<State<3, 2>, ()> = Dop853.integrate_adaptive_with_events(
&system,
initial,
0.0,
t_end,
0.1,
&tol,
|t, _state| {
last_t = t;
},
|_t, _state| ControlFlow::Continue(()),
);
assert!(matches!(outcome, IntegrationOutcome::Completed(_)));
assert!(
(last_t - t_end).abs() < 1e-12,
"Last callback t={last_t}, expected t_end={t_end}"
);
}
#[test]
fn adaptive_terminates_on_event() {
let system = UniformMotion {
constant_velocity: vector![1.0, 0.0, 0.0],
};
let initial = State::<3, 2>::new(vector![0.0, 0.0, 0.0], vector![1.0, 0.0, 0.0]);
let tol = Tolerances::default();
let outcome = Dop853.integrate_adaptive_with_events(
&system,
initial,
0.0,
10.0,
0.1,
&tol,
|_t, _state| {},
|_t, state| {
if state.y().x > 0.5 {
ControlFlow::Break("crossed threshold")
} else {
ControlFlow::Continue(())
}
},
);
match outcome {
IntegrationOutcome::Terminated { t, reason, .. } => {
assert!(t < 10.0);
assert!(
t > 0.4 && t < 1.5,
"Expected termination near 0.5, got t={t}"
);
assert_eq!(reason, "crossed threshold");
}
other => panic!("Expected Terminated, got {other:?}"),
}
}
#[test]
fn adaptive_detects_nan() {
use crate::DynamicalSystem;
struct ExplodingSystem;
impl DynamicalSystem for ExplodingSystem {
type State = State<3, 2>;
fn derivatives(&self, t: f64, state: &State<3, 2>) -> State<3, 2> {
let accel = if t > 0.3 {
vector![f64::INFINITY, 0.0, 0.0]
} else {
vector![0.0, 0.0, 0.0]
};
State::<3, 2>::from_derivative(*state.dy(), accel)
}
}
let initial = State::<3, 2>::new(vector![1.0, 0.0, 0.0], vector![0.0, 0.0, 0.0]);
let tol = Tolerances::default();
let outcome: IntegrationOutcome<State<3, 2>, ()> = Dop853.integrate_adaptive_with_events(
&ExplodingSystem,
initial,
0.0,
10.0,
0.1,
&tol,
|_t, _state| {},
|_t, _state| ControlFlow::Continue(()),
);
match outcome {
IntegrationOutcome::Error(IntegrationError::NonFiniteState { t }) => {
assert!(t > 0.3, "NaN detected at t={t}, expected after 0.3");
}
other => panic!("Expected NonFiniteState error, got {other:?}"),
}
}
#[test]
fn adaptive_detects_step_too_small() {
use crate::DynamicalSystem;
struct VeryStiffSystem;
impl DynamicalSystem for VeryStiffSystem {
type State = State<3, 2>;
fn derivatives(&self, _t: f64, state: &State<3, 2>) -> State<3, 2> {
State::<3, 2>::from_derivative(*state.dy(), -1e20 * state.y())
}
}
let initial = State::<3, 2>::new(vector![1.0, 0.0, 0.0], vector![0.0, 0.0, 0.0]);
let tol = Tolerances {
atol: 1e-12,
rtol: 1e-12,
};
let outcome: IntegrationOutcome<State<3, 2>, ()> = Dop853.integrate_adaptive_with_events(
&VeryStiffSystem,
initial,
0.0,
10.0,
1.0,
&tol,
|_t, _state| {},
|_t, _state| ControlFlow::Continue(()),
);
assert!(
matches!(
outcome,
IntegrationOutcome::Error(IntegrationError::StepSizeTooSmall { .. })
),
"Expected StepSizeTooSmall, got {outcome:?}"
);
}
#[test]
fn adaptive_fewer_steps_than_dp45() {
let system = HarmonicOscillator;
let initial = State::<3, 2>::new(vector![1.0, 0.0, 0.0], vector![0.0, 0.0, 0.0]);
let t_end = 2.0 * std::f64::consts::PI;
let tol = Tolerances {
atol: 1e-10,
rtol: 1e-8,
};
let mut dop853_steps = 0u64;
let outcome853: IntegrationOutcome<State<3, 2>, ()> = Dop853
.integrate_adaptive_with_events(
&system,
initial.clone(),
0.0,
t_end,
1.0,
&tol,
|_t, _state| {
dop853_steps += 1;
},
|_t, _state| ControlFlow::Continue(()),
);
assert!(matches!(outcome853, IntegrationOutcome::Completed(_)));
let mut dp45_steps = 0u64;
let outcome45: IntegrationOutcome<State<3, 2>, ()> = crate::DormandPrince
.integrate_adaptive_with_events(
&system,
initial,
0.0,
t_end,
1.0,
&tol,
|_t, _state| {
dp45_steps += 1;
},
|_t, _state| ControlFlow::Continue(()),
);
assert!(matches!(outcome45, IntegrationOutcome::Completed(_)));
assert!(
dop853_steps <= dp45_steps,
"DOP853 should use fewer or equal steps: dop853={dop853_steps}, dp45={dp45_steps}"
);
}
#[test]
fn coarse_step_dop853_accurate_rk4_fails() {
let system = HarmonicOscillator;
let initial = State::<3, 2>::new(vector![1.0, 0.0, 0.0], vector![0.0, 0.0, 0.0]);
let t_end = 2.0 * std::f64::consts::PI;
let dt = 0.5;
let n_steps = (t_end / dt).ceil() as usize;
let actual_dt = t_end / n_steps as f64;
let dop853_final =
Dop853.integrate(&system, initial.clone(), 0.0, t_end, actual_dt, |_, _| {});
let dop853_err = (dop853_final.y().x - 1.0).abs();
let rk4_final = crate::Rk4.integrate(&system, initial, 0.0, t_end, actual_dt, |_, _| {});
let rk4_err = (rk4_final.y().x - 1.0).abs();
assert!(
dop853_err < 1e-8,
"DOP853 error {dop853_err:.2e} should be < 1e-8 with dt={actual_dt:.3}"
);
assert!(
rk4_err > dop853_err * 100.0,
"RK4 error {rk4_err:.2e} should be >100x DOP853 error {dop853_err:.2e}"
);
}
#[test]
fn long_integration_dop853_better_energy_than_dp45() {
let system = HarmonicOscillator;
let initial = State::<3, 2>::new(vector![1.0, 0.0, 0.0], vector![0.0, 0.0, 0.0]);
let t_end = 100.0 * 2.0 * std::f64::consts::PI; let tol = Tolerances {
atol: 1e-10,
rtol: 1e-10,
};
let energy = |s: &State<3, 2>| 0.5 * (s.y().norm_squared() + s.dy().norm_squared());
let e0 = energy(&initial);
let mut max_drift_853: f64 = 0.0;
let outcome853: IntegrationOutcome<State<3, 2>, ()> = Dop853
.integrate_adaptive_with_events(
&system,
initial.clone(),
0.0,
t_end,
1.0,
&tol,
|_t, state| {
max_drift_853 = max_drift_853.max((energy(state) - e0).abs());
},
|_t, _state| ControlFlow::Continue(()),
);
assert!(matches!(outcome853, IntegrationOutcome::Completed(_)));
let mut max_drift_dp45: f64 = 0.0;
let outcome45: IntegrationOutcome<State<3, 2>, ()> = crate::DormandPrince
.integrate_adaptive_with_events(
&system,
initial,
0.0,
t_end,
1.0,
&tol,
|_t, state| {
max_drift_dp45 = max_drift_dp45.max((energy(state) - e0).abs());
},
|_t, _state| ControlFlow::Continue(()),
);
assert!(matches!(outcome45, IntegrationOutcome::Completed(_)));
assert!(
max_drift_853 < max_drift_dp45,
"DOP853 energy drift {max_drift_853:.2e} should be less than DP45 {max_drift_dp45:.2e}"
);
}
#[test]
fn tight_tolerance_dop853_fewer_evaluations() {
let system = HarmonicOscillator;
let initial = State::<3, 2>::new(vector![1.0, 0.0, 0.0], vector![0.0, 0.0, 0.0]);
let t_end = 10.0 * 2.0 * std::f64::consts::PI; let tol = Tolerances {
atol: 1e-13,
rtol: 1e-13,
};
let mut dop853_steps = 0u64;
let _: IntegrationOutcome<State<3, 2>, ()> = Dop853.integrate_adaptive_with_events(
&system,
initial.clone(),
0.0,
t_end,
1.0,
&tol,
|_t, _state| {
dop853_steps += 1;
},
|_t, _state| ControlFlow::Continue(()),
);
let dop853_evals = dop853_steps * 13;
let mut dp45_steps = 0u64;
let _: IntegrationOutcome<State<3, 2>, ()> = crate::DormandPrince
.integrate_adaptive_with_events(
&system,
initial,
0.0,
t_end,
1.0,
&tol,
|_t, _state| {
dp45_steps += 1;
},
|_t, _state| ControlFlow::Continue(()),
);
let dp45_evals = dp45_steps * 7;
assert!(
dop853_evals < dp45_evals,
"DOP853 evals {dop853_evals} (={dop853_steps}×13) should be < DP45 evals {dp45_evals} (={dp45_steps}×7)"
);
}
#[test]
fn long_fixed_step_rk4_drifts_dop853_accurate() {
let system = HarmonicOscillator;
let initial = State::<3, 2>::new(vector![1.0, 0.0, 0.0], vector![0.0, 0.0, 0.0]);
let t_end = 1000.0 * 2.0 * std::f64::consts::PI; let dt = 0.3;
let rk4_final = crate::Rk4.integrate(&system, initial.clone(), 0.0, t_end, dt, |_, _| {});
let dop853_final = Dop853.integrate(&system, initial, 0.0, t_end, dt, |_, _| {});
let rk4_err = (rk4_final.y().x - 1.0).abs();
let dop853_err = (dop853_final.y().x - 1.0).abs();
assert!(
dop853_err < rk4_err * 1e-4,
"DOP853 err {dop853_err:.2e} should be >10000x better than RK4 err {rk4_err:.2e}"
);
assert!(
rk4_err > 1e-2,
"RK4 error {rk4_err:.2e} should be noticeable after 1000 periods with dt=0.3"
);
}
}