use crate::common::IntegrateFloat;
use crate::error::IntegrateResult;
use crate::ode::types::{ODEMethod, ODEOptions, ODEResult};
use scirs2_core::ndarray::{Array1, ArrayView1};
#[allow(dead_code)]
pub fn rk45_method<F, Func>(
f: Func,
t_span: [F; 2],
y0: Array1<F>,
opts: ODEOptions<F>,
) -> IntegrateResult<ODEResult<F>>
where
F: IntegrateFloat,
Func: Fn(F, ArrayView1<F>) -> Array1<F>,
{
let [t_start, t_end] = t_span;
let n_dim = y0.len();
let h0 = opts.h0.unwrap_or_else(|| {
let _span = t_end - t_start;
_span / F::from_usize(100).expect("Operation failed")
});
let min_step = opts.min_step.unwrap_or_else(|| {
let _span = t_end - t_start;
_span * F::from_f64(1e-8).expect("Operation failed") });
let max_step = opts.max_step.unwrap_or_else(|| {
t_end - t_start });
let mut t = t_start;
let mut y = y0.clone();
let mut h = h0;
let mut t_values = vec![t_start];
let mut y_values = vec![y0.clone()];
let mut func_evals = 0;
let mut step_count = 0;
let mut accepted_steps = 0;
let mut rejected_steps = 0;
let c2 = F::from_f64(1.0 / 5.0).expect("Operation failed");
let c3 = F::from_f64(3.0 / 10.0).expect("Operation failed");
let c4 = F::from_f64(4.0 / 5.0).expect("Operation failed");
let c5 = F::from_f64(8.0 / 9.0).expect("Operation failed");
let c6 = F::one();
while t < t_end && step_count < opts.max_steps {
if t + h > t_end {
h = t_end - t;
}
h = h.min(max_step).max(min_step);
let k1 = f(t, y.view());
let mut y_stage = y.clone();
for i in 0..n_dim {
y_stage[i] = y[i] + h * F::from_f64(1.0 / 5.0).expect("Operation failed") * k1[i];
}
let k2 = f(t + c2 * h, y_stage.view());
let mut y_stage = y.clone();
for i in 0..n_dim {
y_stage[i] = y[i]
+ h * (F::from_f64(3.0 / 40.0).expect("Operation failed") * k1[i]
+ F::from_f64(9.0 / 40.0).expect("Operation failed") * k2[i]);
}
let k3 = f(t + c3 * h, y_stage.view());
let mut y_stage = y.clone();
for i in 0..n_dim {
y_stage[i] = y[i]
+ h * (F::from_f64(44.0 / 45.0).expect("Operation failed") * k1[i]
+ F::from_f64(-56.0 / 15.0).expect("Operation failed") * k2[i]
+ F::from_f64(32.0 / 9.0).expect("Operation failed") * k3[i]);
}
let k4 = f(t + c4 * h, y_stage.view());
let mut y_stage = y.clone();
for i in 0..n_dim {
y_stage[i] = y[i]
+ h * (F::from_f64(19372.0 / 6561.0).expect("Operation failed") * k1[i]
+ F::from_f64(-25360.0 / 2187.0).expect("Operation failed") * k2[i]
+ F::from_f64(64448.0 / 6561.0).expect("Operation failed") * k3[i]
+ F::from_f64(-212.0 / 729.0).expect("Operation failed") * k4[i]);
}
let k5 = f(t + c5 * h, y_stage.view());
let mut y_stage = y.clone();
for i in 0..n_dim {
y_stage[i] = y[i]
+ h * (F::from_f64(9017.0 / 3168.0).expect("Operation failed") * k1[i]
+ F::from_f64(-355.0 / 33.0).expect("Operation failed") * k2[i]
+ F::from_f64(46732.0 / 5247.0).expect("Operation failed") * k3[i]
+ F::from_f64(49.0 / 176.0).expect("Operation failed") * k4[i]
+ F::from_f64(-5103.0 / 18656.0).expect("Operation failed") * k5[i]);
}
let k6 = f(t + c6 * h, y_stage.view());
let mut y_stage = y.clone();
for i in 0..n_dim {
y_stage[i] = y[i]
+ h * (F::from_f64(35.0 / 384.0).expect("Operation failed") * k1[i]
+ F::zero() * k2[i]
+ F::from_f64(500.0 / 1113.0).expect("Operation failed") * k3[i]
+ F::from_f64(125.0 / 192.0).expect("Operation failed") * k4[i]
+ F::from_f64(-2187.0 / 6784.0).expect("Operation failed") * k5[i]
+ F::from_f64(11.0 / 84.0).expect("Operation failed") * k6[i]);
}
let k7 = f(t + h, y_stage.view());
func_evals += 7;
let mut y5 = y.clone();
for i in 0..n_dim {
y5[i] = y[i]
+ h * (F::from_f64(35.0 / 384.0).expect("Operation failed") * k1[i]
+ F::zero() * k2[i]
+ F::from_f64(500.0 / 1113.0).expect("Operation failed") * k3[i]
+ F::from_f64(125.0 / 192.0).expect("Operation failed") * k4[i]
+ F::from_f64(-2187.0 / 6784.0).expect("Operation failed") * k5[i]
+ F::from_f64(11.0 / 84.0).expect("Operation failed") * k6[i]
+ F::zero() * k7[i]);
}
let mut y4 = y.clone();
for i in 0..n_dim {
y4[i] = y[i]
+ h * (F::from_f64(5179.0 / 57600.0).expect("Operation failed") * k1[i]
+ F::zero() * k2[i]
+ F::from_f64(7571.0 / 16695.0).expect("Operation failed") * k3[i]
+ F::from_f64(393.0 / 640.0).expect("Operation failed") * k4[i]
+ F::from_f64(-92097.0 / 339200.0).expect("Operation failed") * k5[i]
+ F::from_f64(187.0 / 2100.0).expect("Operation failed") * k6[i]
+ F::from_f64(1.0 / 40.0).expect("Operation failed") * k7[i]);
}
let mut err_norm = F::zero();
for i in 0..n_dim {
let sc = opts.atol + opts.rtol * y5[i].abs();
let err = (y5[i] - y4[i]).abs() / sc;
err_norm = err_norm.max(err);
}
let order = F::from_f64(5.0).expect("Operation failed"); let exponent = F::one() / (order + F::one());
let safety = F::from_f64(0.9).expect("Operation failed");
let factor = safety * (F::one() / err_norm).powf(exponent);
let factor_min = F::from_f64(0.2).expect("Operation failed");
let factor_max = F::from_f64(5.0).expect("Operation failed");
let factor = factor.min(factor_max).max(factor_min);
if err_norm <= F::one() {
t += h;
y = y5;
t_values.push(t);
y_values.push(y.clone());
if err_norm <= F::from_f64(0.1).expect("Operation failed") {
h *= factor.max(F::from_f64(2.0).expect("Operation failed"));
} else {
h *= factor;
}
step_count += 1;
accepted_steps += 1;
} else {
h *= factor.min(F::one());
rejected_steps += 1;
if h < min_step {
return Err(crate::error::IntegrateError::StepSizeTooSmall(format!(
"Step size {h} too small at t {t}"
)));
}
}
}
let success = t >= t_end;
let message = if !success {
Some(format!(
"Maximum number of steps ({}) reached",
opts.max_steps
))
} else {
None
};
Ok(ODEResult {
t: t_values,
y: y_values,
success,
message,
n_eval: func_evals,
n_steps: step_count,
n_accepted: accepted_steps,
n_rejected: rejected_steps,
n_lu: 0, n_jac: 0, method: ODEMethod::RK45,
})
}
#[allow(clippy::too_many_arguments)]
fn pi_step_factor<F: IntegrateFloat>(
err_norm: F,
err_prev: F,
accepted: bool,
just_recovered_from_rejection: bool,
alpha: F,
beta_gain: F,
safety: F,
growth_min: F,
growth_max: F,
) -> (F, F) {
let tiny = F::from_f64(1e-300).expect("Operation failed");
let err_eff = err_norm.max(tiny);
let fac_elementary = err_eff.powf(alpha);
if accepted {
let history = err_prev.max(tiny).powf(beta_gain);
let mut factor = safety / (fac_elementary * history);
factor = factor.min(growth_max).max(growth_min);
if just_recovered_from_rejection {
factor = factor.min(F::one());
}
let updated_err_prev = err_norm.max(F::from_f64(1e-10).expect("Operation failed"));
(factor, updated_err_prev)
} else {
let factor = (safety / fac_elementary).min(F::one()).max(growth_min);
(factor, err_prev)
}
}
#[allow(dead_code)]
pub fn rk23_method<F, Func>(
f: Func,
t_span: [F; 2],
y0: Array1<F>,
opts: ODEOptions<F>,
) -> IntegrateResult<ODEResult<F>>
where
F: IntegrateFloat,
Func: Fn(F, ArrayView1<F>) -> Array1<F>,
{
let [t_start, t_end] = t_span;
let n_dim = y0.len();
let h0 = opts.h0.unwrap_or_else(|| {
let _span = t_end - t_start;
_span / F::from_usize(100).expect("Operation failed")
});
let min_step = opts.min_step.unwrap_or_else(|| {
let _span = t_end - t_start;
_span * F::from_f64(1e-8).expect("Operation failed") });
let max_step = opts.max_step.unwrap_or_else(|| {
t_end - t_start });
let mut t = t_start;
let mut y = y0.clone();
let mut h = h0;
let mut t_values = vec![t_start];
let mut y_values = vec![y0.clone()];
let mut func_evals = 0;
let mut step_count = 0;
let mut accepted_steps = 0;
let mut rejected_steps = 0;
let c2 = F::from_f64(0.5).expect("Operation failed");
let c3 = F::from_f64(0.75).expect("Operation failed");
let a21 = F::from_f64(0.5).expect("Operation failed");
let a32 = F::from_f64(0.75).expect("Operation failed");
let b1 = F::from_f64(2.0 / 9.0).expect("Operation failed");
let b2 = F::from_f64(1.0 / 3.0).expect("Operation failed");
let b3 = F::from_f64(4.0 / 9.0).expect("Operation failed");
let e1 = F::from_f64(5.0 / 72.0).expect("Operation failed");
let e2 = F::from_f64(-1.0 / 12.0).expect("Operation failed");
let e3 = F::from_f64(-1.0 / 9.0).expect("Operation failed");
let e4 = F::from_f64(0.125).expect("Operation failed");
let alpha = F::one() / F::from_f64(3.0).expect("Operation failed");
let beta_gain = F::from_f64(0.08).expect("Operation failed");
let safety = F::from_f64(0.9).expect("Operation failed");
let growth_min = F::from_f64(0.2).expect("Operation failed");
let growth_max = F::from_f64(10.0).expect("Operation failed");
let mut err_prev = F::one();
let mut just_rejected = false;
while t < t_end && step_count < opts.max_steps {
if t + h > t_end {
h = t_end - t;
}
h = h.min(max_step).max(min_step);
let k1 = f(t, y.view());
let mut y_stage = y.clone();
for i in 0..n_dim {
y_stage[i] = y[i] + h * a21 * k1[i];
}
let k2 = f(t + c2 * h, y_stage.view());
let mut y_stage = y.clone();
for i in 0..n_dim {
y_stage[i] = y[i] + h * a32 * k2[i];
}
let k3 = f(t + c3 * h, y_stage.view());
let mut y3 = y.clone();
for i in 0..n_dim {
y3[i] = y[i] + h * (b1 * k1[i] + b2 * k2[i] + b3 * k3[i]);
}
let k4 = f(t + h, y3.view());
func_evals += 4;
let mut err_norm = F::zero();
for i in 0..n_dim {
let sc = opts.atol + opts.rtol * y3[i].abs().max(y[i].abs());
let err_i = e1 * k1[i] + e2 * k2[i] + e3 * k3[i] + e4 * k4[i];
err_norm = err_norm.max((h * err_i / sc).abs());
}
let accepted = err_norm <= F::one();
let (factor, new_err_prev) = pi_step_factor(
err_norm,
err_prev,
accepted,
just_rejected,
alpha,
beta_gain,
safety,
growth_min,
growth_max,
);
if accepted {
t += h;
y = y3;
t_values.push(t);
y_values.push(y.clone());
err_prev = new_err_prev;
just_rejected = false;
h *= factor;
step_count += 1;
accepted_steps += 1;
} else {
h *= factor;
just_rejected = true;
rejected_steps += 1;
if h < min_step {
return Err(crate::error::IntegrateError::StepSizeTooSmall(format!(
"Step size {h} too small at t {t}"
)));
}
}
}
let success = t >= t_end;
let message = if !success {
Some(format!(
"Maximum number of steps ({}) reached",
opts.max_steps
))
} else {
None
};
Ok(ODEResult {
t: t_values,
y: y_values,
success,
message,
n_eval: func_evals,
n_steps: step_count,
n_accepted: accepted_steps,
n_rejected: rejected_steps,
n_lu: 0, n_jac: 0, method: ODEMethod::RK23,
})
}
const DOP853_STAGES: usize = 12;
const DOP853_C: [f64; 12] = [
0.0,
0.05260015195876773,
0.0789002279381516,
0.1183503419072274,
0.2816496580927726,
0.3333333333333333,
0.25,
0.3076923076923077,
0.6512820512820513,
0.6,
0.8571428571428571,
1.0,
];
const DOP853_A: [[f64; 12]; 12] = [
[0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
[
0.05260015195876773,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
],
[
0.0197250569845379,
0.0591751709536137,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
],
[
0.02958758547680685,
0.0,
0.08876275643042054,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
],
[
0.2413651341592667,
0.0,
-0.8845494793282861,
0.924834003261792,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
],
[
0.037037037037037035,
0.0,
0.0,
0.17082860872947386,
0.12546768756682242,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
],
[
0.037109375,
0.0,
0.0,
0.17025221101954405,
0.06021653898045596,
-0.017578125,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
],
[
0.03709200011850479,
0.0,
0.0,
0.17038392571223998,
0.10726203044637328,
-0.015319437748624402,
0.008273789163814023,
0.0,
0.0,
0.0,
0.0,
0.0,
],
[
0.6241109587160757,
0.0,
0.0,
-3.3608926294469414,
-0.868219346841726,
27.59209969944671,
20.154067550477894,
-43.48988418106996,
0.0,
0.0,
0.0,
0.0,
],
[
0.47766253643826434,
0.0,
0.0,
-2.4881146199716677,
-0.590290826836843,
21.230051448181193,
15.279233632882423,
-33.28821096898486,
-0.020331201708508627,
0.0,
0.0,
0.0,
],
[
-0.9371424300859873,
0.0,
0.0,
5.186372428844064,
1.0914373489967295,
-8.149787010746927,
-18.52006565999696,
22.739487099350505,
2.4936055526796523,
-3.0467644718982196,
0.0,
0.0,
],
[
2.273310147516538,
0.0,
0.0,
-10.53449546673725,
-2.0008720582248625,
-17.9589318631188,
27.94888452941996,
-2.8589982771350235,
-8.87285693353063,
12.360567175794303,
0.6433927460157636,
0.0,
],
];
const DOP853_B: [f64; 12] = [
0.054293734116568765,
0.0,
0.0,
0.0,
0.0,
4.450312892752409,
1.8915178993145003,
-5.801203960010585,
0.3111643669578199,
-0.1521609496625161,
0.20136540080403034,
0.04471061572777259,
];
const DOP853_E5: [f64; 12] = [
0.01312004499419488,
0.0,
0.0,
0.0,
0.0,
-1.2251564463762044,
-0.4957589496572502,
1.6643771824549864,
-0.35032884874997366,
0.3341791187130175,
0.08192320648511571,
-0.022355307863886294,
];
const DOP853_E3_ADJUST: [(usize, f64); 3] = [
(0, 0.2440944881889764),
(8, 0.7338466882816118),
(11, 0.022058823529411766),
];
#[allow(dead_code)]
pub fn dop853_method<F, Func>(
f: Func,
t_span: [F; 2],
y0: Array1<F>,
opts: ODEOptions<F>,
) -> IntegrateResult<ODEResult<F>>
where
F: IntegrateFloat,
Func: Fn(F, ArrayView1<F>) -> Array1<F>,
{
let [t_start, t_end] = t_span;
let n_dim = y0.len();
let h0 = opts.h0.unwrap_or_else(|| {
let _span = t_end - t_start;
_span / F::from_usize(100).expect("Operation failed")
});
let min_step = opts.min_step.unwrap_or_else(|| {
let _span = t_end - t_start;
_span * F::from_f64(1e-8).expect("Operation failed") });
let max_step = opts.max_step.unwrap_or_else(|| {
t_end - t_start });
let mut t = t_start;
let mut y = y0.clone();
let mut h = h0;
let mut t_values = vec![t_start];
let mut y_values = vec![y0.clone()];
let mut func_evals = 0;
let mut step_count = 0;
let mut accepted_steps = 0;
let mut rejected_steps = 0;
let mut e3 = DOP853_B;
for &(idx, adjust) in DOP853_E3_ADJUST.iter() {
e3[idx] -= adjust;
}
let alpha = F::from_f64(0.125).expect("Operation failed");
let beta_gain = F::zero();
let safety = F::from_f64(0.9).expect("Operation failed");
let growth_min = F::from_f64(1.0 / 3.0).expect("Operation failed");
let growth_max = F::from_f64(6.0).expect("Operation failed");
let mut err_prev = F::one();
let mut just_rejected = false;
let n_dim_f = F::from_usize(n_dim).expect("Operation failed");
let err3_weight = F::from_f64(0.01).expect("Operation failed");
let non_finite_penalty = F::from_f64(1e30).expect("Operation failed");
while t < t_end && step_count < opts.max_steps {
if t + h > t_end {
h = t_end - t;
}
h = h.min(max_step).max(min_step);
let mut k_stages: Vec<Array1<F>> = Vec::with_capacity(DOP853_STAGES);
k_stages.push(f(t, y.view()));
for s in 1..DOP853_STAGES {
let mut y_stage = y.clone();
for (j, k_j) in k_stages.iter().enumerate().take(s) {
let a_sj = DOP853_A[s][j];
if a_sj != 0.0 {
let a_sj = F::from_f64(a_sj).expect("Operation failed");
for d in 0..n_dim {
y_stage[d] += h * a_sj * k_j[d];
}
}
}
let c_s = F::from_f64(DOP853_C[s]).expect("Operation failed");
k_stages.push(f(t + c_s * h, y_stage.view()));
}
func_evals += DOP853_STAGES;
let mut y8 = y.clone();
for (j, k_j) in k_stages.iter().enumerate() {
let b_j = DOP853_B[j];
if b_j != 0.0 {
let b_j = F::from_f64(b_j).expect("Operation failed");
for d in 0..n_dim {
y8[d] += h * b_j * k_j[d];
}
}
}
let mut err5_sq = F::zero();
let mut err3_sq = F::zero();
for d in 0..n_dim {
let scale = opts.atol + opts.rtol * y[d].abs().max(y8[d].abs());
let mut e5_d = F::zero();
let mut e3_d = F::zero();
for (j, k_j) in k_stages.iter().enumerate() {
let kjd = k_j[d];
let e5j = DOP853_E5[j];
if e5j != 0.0 {
e5_d += F::from_f64(e5j).expect("Operation failed") * kjd;
}
let e3j = e3[j];
if e3j != 0.0 {
e3_d += F::from_f64(e3j).expect("Operation failed") * kjd;
}
}
let e5s = e5_d / scale;
let e3s = e3_d / scale;
err5_sq += e5s * e5s;
err3_sq += e3s * e3s;
}
let denom = err5_sq + err3_weight * err3_sq;
let err_norm = if denom <= F::zero() {
F::zero()
} else {
let raw = h.abs() * err5_sq / (denom * n_dim_f).sqrt();
if raw.is_finite() {
raw
} else {
non_finite_penalty
}
};
let accepted = err_norm <= F::one();
let (factor, new_err_prev) = pi_step_factor(
err_norm,
err_prev,
accepted,
just_rejected,
alpha,
beta_gain,
safety,
growth_min,
growth_max,
);
if accepted {
t += h;
y = y8;
t_values.push(t);
y_values.push(y.clone());
err_prev = new_err_prev;
just_rejected = false;
h *= factor;
step_count += 1;
accepted_steps += 1;
} else {
h *= factor;
just_rejected = true;
rejected_steps += 1;
if h < min_step {
return Err(crate::error::IntegrateError::StepSizeTooSmall(format!(
"Step size {h} too small at t {t}"
)));
}
}
}
let success = t >= t_end;
let message = if !success {
Some(format!(
"Maximum number of steps ({}) reached",
opts.max_steps
))
} else {
None
};
Ok(ODEResult {
t: t_values,
y: y_values,
success,
message,
n_eval: func_evals,
n_steps: step_count,
n_accepted: accepted_steps,
n_rejected: rejected_steps,
n_lu: 0, n_jac: 0, method: ODEMethod::DOP853,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rk23_empirical_convergence_order_is_three() {
let k = 3.0_f64;
let f =
move |_t: f64, y: ArrayView1<f64>| -> Array1<f64> { Array1::from_vec(vec![-k * y[0]]) };
let t_end = 1.0_f64;
let y0 = 2.0_f64;
let y_exact = y0 * (-k * t_end).exp();
let mut prev_err: Option<f64> = None;
let mut min_ratio = f64::INFINITY;
let mut h = t_end / 8.0;
for _ in 0..3 {
let opts = ODEOptions {
method: ODEMethod::RK23,
rtol: 1.0,
atol: 1.0,
h0: Some(h),
min_step: Some(h),
max_step: Some(h),
max_steps: 1_000_000,
..Default::default()
};
let result = rk23_method(f, [0.0, t_end], Array1::from_vec(vec![y0]), opts)
.expect("RK23 fixed-step solve failed");
assert!(
result.success,
"RK23 fixed-step integration did not complete"
);
let y_final = result.y.last().expect("empty result")[0];
let err = (y_final - y_exact).abs();
if let Some(prev) = prev_err {
min_ratio = min_ratio.min(prev / err);
}
prev_err = Some(err);
h /= 2.0;
}
assert!(
min_ratio > 5.0,
"RK23 empirical convergence order too low: min ratio={min_ratio} (expected ~8)"
);
}
#[test]
fn dop853_empirical_convergence_order_is_eight() {
let k = 3.0_f64;
let f =
move |_t: f64, y: ArrayView1<f64>| -> Array1<f64> { Array1::from_vec(vec![-k * y[0]]) };
let t_end = 1.0_f64;
let y0 = 2.0_f64;
let y_exact = y0 * (-k * t_end).exp();
let mut prev_err: Option<f64> = None;
let mut min_ratio = f64::INFINITY;
let mut h = t_end / 2.0;
for _ in 0..3 {
let opts = ODEOptions {
method: ODEMethod::DOP853,
rtol: 1.0,
atol: 1.0,
h0: Some(h),
min_step: Some(h),
max_step: Some(h),
max_steps: 1_000_000,
..Default::default()
};
let result = dop853_method(f, [0.0, t_end], Array1::from_vec(vec![y0]), opts)
.expect("DOP853 fixed-step solve failed");
assert!(
result.success,
"DOP853 fixed-step integration did not complete"
);
let y_final = result.y.last().expect("empty result")[0];
let err = (y_final - y_exact).abs();
if let Some(prev) = prev_err {
min_ratio = min_ratio.min(prev / err);
}
prev_err = Some(err);
h /= 2.0;
}
assert!(
min_ratio > 50.0,
"DOP853 empirical convergence order too low: min ratio={min_ratio} (expected ~256)"
);
}
#[test]
fn rk23_dop853_agree_with_rk45_van_der_pol() {
let mu = 1.0_f64;
let f = move |_t: f64, y: ArrayView1<f64>| -> Array1<f64> {
Array1::from_vec(vec![y[1], mu * (1.0 - y[0] * y[0]) * y[1] - y[0]])
};
let y0 = Array1::from_vec(vec![0.5_f64, 0.0]);
let t_span = [0.0_f64, 3.0];
let opts_of = |method: ODEMethod| ODEOptions {
method,
rtol: 1e-10,
atol: 1e-12,
max_steps: 500_000,
..Default::default()
};
let rk45 = rk45_method(f, t_span, y0.clone(), opts_of(ODEMethod::RK45))
.expect("RK45 Van der Pol solve failed");
let rk23 = rk23_method(f, t_span, y0.clone(), opts_of(ODEMethod::RK23))
.expect("RK23 Van der Pol solve failed");
let dop853 = dop853_method(f, t_span, y0, opts_of(ODEMethod::DOP853))
.expect("DOP853 Van der Pol solve failed");
assert!(rk45.success && rk23.success && dop853.success);
let y_rk45 = rk45.y.last().expect("empty result");
let y_rk23 = rk23.y.last().expect("empty result");
let y_dop853 = dop853.y.last().expect("empty result");
for i in 0..2 {
assert!(
(y_rk23[i] - y_rk45[i]).abs() < 1e-5,
"RK23 vs RK45 disagreement at index {i}: {} vs {}",
y_rk23[i],
y_rk45[i]
);
assert!(
(y_dop853[i] - y_rk45[i]).abs() < 1e-5,
"DOP853 vs RK45 disagreement at index {i}: {} vs {}",
y_dop853[i],
y_rk45[i]
);
}
}
}