pub fn rk4_step<F>(f: &F, t: f64, y: &[f64], h: f64) -> Vec<f64>
where
F: Fn(f64, &[f64]) -> Vec<f64>,
{
let n = y.len();
let k1 = f(t, y);
let y2: Vec<f64> = (0..n).map(|i| y[i] + 0.5 * h * k1[i]).collect();
let k2 = f(t + 0.5 * h, &y2);
let y3: Vec<f64> = (0..n).map(|i| y[i] + 0.5 * h * k2[i]).collect();
let k3 = f(t + 0.5 * h, &y3);
let y4: Vec<f64> = (0..n).map(|i| y[i] + h * k3[i]).collect();
let k4 = f(t + h, &y4);
(0..n)
.map(|i| y[i] + (h / 6.0) * (k1[i] + 2.0 * k2[i] + 2.0 * k3[i] + k4[i]))
.collect()
}
#[derive(Clone, Copy, Debug)]
pub struct Tolerance {
pub rtol: f64,
pub atol: f64,
pub h_min: f64,
pub h_max: f64,
}
impl Default for Tolerance {
fn default() -> Self {
Self {
rtol: 1e-9,
atol: 1e-12,
h_min: 1e-12,
h_max: f64::INFINITY,
}
}
}
pub fn step_doubling<F>(f: &F, t: f64, y: &[f64], h: f64, tol: &Tolerance) -> (Vec<f64>, f64, f64)
where
F: Fn(f64, &[f64]) -> Vec<f64>,
{
let big = rk4_step(f, t, y, h);
let half = rk4_step(f, t, y, 0.5 * h);
let two_half = rk4_step(f, t + 0.5 * h, &half, 0.5 * h);
let mut err = 0.0_f64;
for i in 0..y.len() {
let diff = (two_half[i] - big[i]) / 15.0;
let scale = tol.atol + tol.rtol * two_half[i].abs().max(y[i].abs());
let r = diff / scale;
err += r * r;
}
err = (err / y.len() as f64).sqrt();
let factor = if err > 0.0 { 0.9 * err.powf(-0.2) } else { 5.0 };
let h_next = (h * factor.clamp(0.2, 5.0)).clamp(tol.h_min, tol.h_max);
(two_half, err, h_next)
}
#[derive(Clone, Debug)]
pub struct Solution {
pub t: f64,
pub y: Vec<f64>,
pub accepted: usize,
pub rejected: usize,
}
pub fn integrate<F>(f: &F, t0: f64, y0: &[f64], t_end: f64, h0: f64, tol: &Tolerance) -> Solution
where
F: Fn(f64, &[f64]) -> Vec<f64>,
{
let mut t = t0;
let mut y = y0.to_vec();
let mut h = h0.max(tol.h_min).min(tol.h_max);
let (mut accepted, mut rejected) = (0, 0);
while t < t_end {
if t + h > t_end {
h = t_end - t;
}
let (y_next, err, h_next) = step_doubling(f, t, &y, h, tol);
if err <= 1.0 || h <= tol.h_min {
t += h;
y = y_next;
accepted += 1;
h = h_next;
} else {
rejected += 1;
h = h_next;
}
}
Solution {
t,
y,
accepted,
rejected,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rk4_integrates_exponential_growth() {
let f = |_t: f64, y: &[f64]| vec![y[0]];
let mut y = vec![1.0];
let h = 0.001;
let mut t = 0.0;
while t < 1.0 - 1e-9 {
y = rk4_step(&f, t, &y, h);
t += h;
}
assert!((y[0] - std::f64::consts::E).abs() < 1e-9, "y(1) = {}", y[0]);
}
#[test]
fn rk4_is_fourth_order() {
let f = |_t: f64, y: &[f64]| vec![y[0]];
let err_at = |h: f64| {
let mut y = vec![1.0];
let mut t = 0.0;
let steps = (1.0 / h).round() as usize;
for _ in 0..steps {
y = rk4_step(&f, t, &y, h);
t += h;
}
(y[0] - std::f64::consts::E).abs()
};
let ratio = err_at(0.05) / err_at(0.025);
assert!((12.0..20.0).contains(&ratio), "4th-order ratio = {ratio}");
}
#[test]
fn harmonic_oscillator_conserves_energy() {
let f = |_t: f64, y: &[f64]| vec![y[1], -y[0]];
let mut y = vec![1.0, 0.0]; let n_steps = 10_000usize;
let h = std::f64::consts::TAU / n_steps as f64;
let mut t = 0.0;
for _ in 0..n_steps {
y = rk4_step(&f, t, &y, h);
t += h;
}
let energy = y[0] * y[0] + y[1] * y[1];
assert!((energy - 1.0).abs() < 1e-8, "energy drift {energy}");
assert!(
(y[0] - 1.0).abs() < 1e-6 && y[1].abs() < 1e-6,
"state {:?}",
y
);
}
#[test]
fn adaptive_integration_meets_tolerance_with_variable_steps() {
let f = |_t: f64, y: &[f64]| vec![y[0]];
let tol = Tolerance {
rtol: 1e-10,
atol: 1e-12,
..Tolerance::default()
};
let sol = integrate(&f, 0.0, &[1.0], 1.0, 0.1, &tol);
assert!((sol.t - 1.0).abs() < 1e-12);
assert!(
(sol.y[0] - std::f64::consts::E).abs() < 1e-8,
"y = {}",
sol.y[0]
);
assert!(
sol.accepted >= 1,
"should take real steps: {}",
sol.accepted
);
}
#[test]
fn adaptive_takes_larger_steps_on_an_easy_problem() {
let easy = integrate(
&|_t, _y| vec![0.01],
0.0,
&[0.0],
10.0,
0.01,
&Tolerance::default(),
);
let hard = integrate(
&|_t, y: &[f64]| vec![y[0]],
0.0,
&[1.0],
10.0,
0.01,
&Tolerance::default(),
);
assert!(
easy.accepted < hard.accepted,
"easy {} should need fewer steps than hard {}",
easy.accepted,
hard.accepted
);
}
#[test]
fn step_doubling_error_estimate_shrinks_with_step() {
let f = |_t: f64, y: &[f64]| vec![y[0]];
let tol = Tolerance::default();
let (_, e1, _) = step_doubling(&f, 0.0, &[1.0], 0.2, &tol);
let (_, e2, _) = step_doubling(&f, 0.0, &[1.0], 0.1, &tol);
assert!(
e2 < e1,
"smaller step should have smaller error: {e2} !< {e1}"
);
}
}