1const CBRT2: f64 = 1.259_921_049_894_873_2;
26
27pub fn verlet_step<F, G>(q: f64, p: f64, h: f64, force: F, kinetic_velocity: G) -> (f64, f64)
35where
36 F: Fn(f64) -> f64,
37 G: Fn(f64) -> f64,
38{
39 let p_half = p + 0.5 * h * force(q);
40 let q_new = q + h * kinetic_velocity(p_half);
41 let p_new = p_half + 0.5 * h * force(q_new);
42 (q_new, p_new)
43}
44
45pub fn ruth3_step<F, G>(q: f64, p: f64, h: f64, force: F, kinetic_velocity: G) -> (f64, f64)
48where
49 F: Fn(f64) -> f64,
50 G: Fn(f64) -> f64,
51{
52 const C: [f64; 3] = [1.0, -2.0 / 3.0, 2.0 / 3.0];
54 const D: [f64; 3] = [-1.0 / 24.0, 3.0 / 4.0, 7.0 / 24.0];
55 let mut q = q;
56 let mut p = p;
57 for i in 0..3 {
58 p += C[i] * h * force(q);
59 q += D[i] * h * kinetic_velocity(p);
60 }
61 (q, p)
62}
63
64pub fn yoshida4_step<F, G>(q: f64, p: f64, h: f64, force: F, kinetic_velocity: G) -> (f64, f64)
67where
68 F: Fn(f64) -> f64,
69 G: Fn(f64) -> f64,
70{
71 let w1 = 1.0 / (2.0 - CBRT2);
72 let w0 = -CBRT2 * w1;
73 let (q, p) = verlet_step(q, p, w1 * h, &force, &kinetic_velocity);
74 let (q, p) = verlet_step(q, p, w0 * h, &force, &kinetic_velocity);
75 verlet_step(q, p, w1 * h, &force, &kinetic_velocity)
76}
77
78#[derive(Debug, Clone, Copy, PartialEq, Eq)]
80pub enum SymplecticMethod {
81 Verlet,
83 Ruth3,
85 Yoshida4,
87}
88
89#[derive(Debug, Clone, Copy, PartialEq)]
93pub struct SymplecticResult {
94 pub q: f64,
95 pub p: f64,
96 pub max_energy_drift: f64,
97}
98
99#[allow(clippy::too_many_arguments)]
104pub fn integrate_symplectic<F, G, H>(
105 q0: f64,
106 p0: f64,
107 h: f64,
108 steps: u64,
109 force: F,
110 kinetic_velocity: G,
111 hamiltonian: H,
112 method: SymplecticMethod,
113) -> SymplecticResult
114where
115 F: Fn(f64) -> f64,
116 G: Fn(f64) -> f64,
117 H: Fn(f64, f64) -> f64,
118{
119 let mut q = q0;
120 let mut p = p0;
121 let e0 = hamiltonian(q0, p0);
122 let mut max_drift = 0.0f64;
123
124 for _ in 0..steps {
125 let (qn, pn) = match method {
126 SymplecticMethod::Verlet => verlet_step(q, p, h, &force, &kinetic_velocity),
127 SymplecticMethod::Ruth3 => ruth3_step(q, p, h, &force, &kinetic_velocity),
128 SymplecticMethod::Yoshida4 => yoshida4_step(q, p, h, &force, &kinetic_velocity),
129 };
130 q = qn;
131 p = pn;
132 let drift = (hamiltonian(q, p) - e0).abs();
133 if drift > max_drift {
134 max_drift = drift;
135 }
136 }
137
138 SymplecticResult {
139 q,
140 p,
141 max_energy_drift: max_drift,
142 }
143}
144
145const NEWTON_TOL: f64 = 1e-12;
149const NEWTON_MAX_ITERS: u32 = 64;
150
151#[inline]
153fn dfdy_fd<F: Fn(f64, f64) -> f64>(f: &F, t: f64, y: f64) -> f64 {
154 let eps = 1e-7 * y.abs().max(1.0);
155 (f(t, y + eps) - f(t, y - eps)) / (2.0 * eps)
156}
157
158pub fn bdf1_step<F: Fn(f64, f64) -> f64>(t0: f64, y0: f64, h: f64, f: F) -> f64 {
162 let t1 = t0 + h;
163 let mut y = y0 + h * f(t0, y0); for _ in 0..NEWTON_MAX_ITERS {
165 let g = y - y0 - h * f(t1, y);
166 let dg = 1.0 - h * dfdy_fd(&f, t1, y);
167 let dy = g / dg;
168 y -= dy;
169 if dy.abs() <= NEWTON_TOL * y.abs().max(1.0) {
170 break;
171 }
172 }
173 y
174}
175
176pub fn bdf2_step<F: Fn(f64, f64) -> f64>(t1: f64, y1: f64, y0: f64, h: f64, f: F) -> f64 {
179 let t2 = t1 + h;
180 let c = (4.0 / 3.0) * y1 - (1.0 / 3.0) * y0;
181 let beta = 2.0 / 3.0;
182 let mut y = y1 + h * f(t1, y1); for _ in 0..NEWTON_MAX_ITERS {
184 let g = y - c - beta * h * f(t2, y);
185 let dg = 1.0 - beta * h * dfdy_fd(&f, t2, y);
186 let dy = g / dg;
187 y -= dy;
188 if dy.abs() <= NEWTON_TOL * y.abs().max(1.0) {
189 break;
190 }
191 }
192 y
193}
194
195pub fn integrate_bdf<F: Fn(f64, f64) -> f64>(t0: f64, y0: f64, h: f64, steps: u64, f: F) -> f64 {
199 if steps == 0 {
200 return y0;
201 }
202 let mut y_prev = y0;
204 let mut y_curr = bdf1_step(t0, y0, h, &f);
205 let mut t = t0 + h;
206 for _ in 1..steps {
207 let y_next = bdf2_step(t, y_curr, y_prev, h, &f);
208 y_prev = y_curr;
209 y_curr = y_next;
210 t += h;
211 }
212 y_curr
213}
214
215pub fn hermite_dense_output(y0: f64, f0: f64, y1: f64, f1: f64, h: f64, theta: f64) -> f64 {
222 let t = theta;
223 let t2 = t * t;
224 let t3 = t2 * t;
225 let h00 = 2.0 * t3 - 3.0 * t2 + 1.0;
226 let h10 = t3 - 2.0 * t2 + t;
227 let h01 = -2.0 * t3 + 3.0 * t2;
228 let h11 = t3 - t2;
229 h00 * y0 + h10 * h * f0 + h01 * y1 + h11 * h * f1
230}
231
232#[derive(Debug, Clone, Copy, PartialEq)]
236pub struct SensitivityResult {
237 pub y: f64,
239 pub sensitivity: f64,
241}
242
243pub fn integrate_with_sensitivity<F: Fn(f64, f64) -> f64>(
248 t0: f64,
249 y0: f64,
250 h: f64,
251 steps: u64,
252 f: F,
253) -> SensitivityResult {
254 let mut t = t0;
255 let mut y = y0;
256 let mut s = 1.0f64; let deriv = |t: f64, y: f64, s: f64| -> (f64, f64) { (f(t, y), dfdy_fd(&f, t, y) * s) };
260
261 for _ in 0..steps {
262 let (k1y, k1s) = deriv(t, y, s);
263 let (k2y, k2s) = deriv(t + 0.5 * h, y + 0.5 * h * k1y, s + 0.5 * h * k1s);
264 let (k3y, k3s) = deriv(t + 0.5 * h, y + 0.5 * h * k2y, s + 0.5 * h * k2s);
265 let (k4y, k4s) = deriv(t + h, y + h * k3y, s + h * k3s);
266 y += (h / 6.0) * (k1y + 2.0 * k2y + 2.0 * k3y + k4y);
267 s += (h / 6.0) * (k1s + 2.0 * k2s + 2.0 * k3s + k4s);
268 t += h;
269 }
270
271 SensitivityResult { y, sensitivity: s }
272}
273
274#[cfg(test)]
275mod tests {
276 use super::*;
277
278 fn ho_force(q: f64) -> f64 {
281 -q
282 }
283 fn ho_kin(p: f64) -> f64 {
284 p
285 }
286 fn ho_energy(q: f64, p: f64) -> f64 {
287 0.5 * p * p + 0.5 * q * q
288 }
289
290 #[test]
291 fn symplectic_methods_conserve_energy_over_many_periods() {
292 let h = 0.01;
294 let steps = (200.0 * 2.0 * std::f64::consts::PI / h) as u64;
295 for method in [
296 SymplecticMethod::Verlet,
297 SymplecticMethod::Ruth3,
298 SymplecticMethod::Yoshida4,
299 ] {
300 let r = integrate_symplectic(1.0, 0.0, h, steps, ho_force, ho_kin, ho_energy, method);
301 assert!(
303 r.max_energy_drift < 5e-3,
304 "{method:?} energy drift {} too large",
305 r.max_energy_drift
306 );
307 }
308 }
309
310 #[test]
311 fn symplectic_convergence_orders_match_labels() {
312 let t_end = 1.0f64;
320 let exact = t_end.cos();
321 let err_at = |m, h: f64| {
322 let steps = (t_end / h).round().max(1.0) as u64;
323 let h = t_end / steps as f64; let r = integrate_symplectic(1.0, 0.0, h, steps, ho_force, ho_kin, ho_energy, m);
325 (r.q - exact).abs()
326 };
327 let order = |m| {
328 let e1 = err_at(m, 0.04);
329 let e2 = err_at(m, 0.02);
330 (e1 / e2).log2()
331 };
332 let o2 = order(SymplecticMethod::Verlet);
333 let o3 = order(SymplecticMethod::Ruth3);
334 let o4 = order(SymplecticMethod::Yoshida4);
335 assert!(
336 (o2 - 2.0).abs() < 0.5,
337 "Verlet should be ~2nd order, got {o2:.2}"
338 );
339 assert!(
340 (o3 - 3.0).abs() < 0.6,
341 "Ruth3 should be ~3rd order, got {o3:.2}"
342 );
343 assert!(
344 (o4 - 4.0).abs() < 0.8,
345 "Yoshida4 should be ~4th order, got {o4:.2}"
346 );
347 }
348
349 #[test]
350 fn bdf_is_stable_on_a_stiff_equation() {
351 let f = |_t: f64, y: f64| -10.0 * y; let y1 = bdf1_step(0.0, 1.0, 0.5, f);
357 assert!(
358 (y1 - 1.0 / 6.0).abs() < 1e-9,
359 "BDF1 implicit-Euler value, got {y1}"
360 );
361
362 let stiff = |_t: f64, y: f64| -1000.0 * y;
364 let yf = integrate_bdf(0.0, 1.0, 0.1, 5, stiff);
365 assert!(
366 yf.abs() < 1e-2 && yf.is_finite(),
367 "BDF2 stiff result blew up: {yf}"
368 );
369 assert!(
370 yf >= 0.0,
371 "L-stable decay should not overshoot below 0: {yf}"
372 );
373 }
374
375 #[test]
376 fn bdf2_matches_linear_decay_accurately() {
377 let f = |_t: f64, y: f64| -y;
379 let yf = integrate_bdf(0.0, 1.0, 1e-3, 1000, f);
380 let exact = (-1.0f64).exp();
381 assert!((yf - exact).abs() < 1e-5, "BDF2 got {yf}, exact {exact}");
382 }
383
384 #[test]
385 fn dense_output_is_exact_for_a_cubic() {
386 let y = |t: f64| 1.0 + 2.0 * t + 3.0 * t * t + 4.0 * t * t * t;
388 let f = |t: f64| 2.0 + 6.0 * t + 12.0 * t * t;
389 let (t0, t1) = (0.0, 1.0);
390 let h = t1 - t0;
391 for &theta in &[0.0, 0.25, 0.5, 0.75, 1.0] {
392 let interp = hermite_dense_output(y(t0), f(t0), y(t1), f(t1), h, theta);
393 let exact = y(t0 + theta * h);
394 assert!(
395 (interp - exact).abs() < 1e-12,
396 "θ={theta}: {interp} vs {exact}"
397 );
398 }
399 }
400
401 #[test]
402 fn forward_sensitivity_matches_analytic_exponential() {
403 let lambda = 2.0;
406 let f = move |_t: f64, y: f64| -lambda * y;
407 let r = integrate_with_sensitivity(0.0, 0.5, 1e-3, 1000, f);
408 let exp_m2 = (-2.0f64).exp();
409 assert!(
410 (r.sensitivity - exp_m2).abs() < 1e-5,
411 "∂y/∂y0 got {}, want {exp_m2}",
412 r.sensitivity
413 );
414 assert!(
415 (r.y - 0.5 * exp_m2).abs() < 1e-6,
416 "y got {}, want {}",
417 r.y,
418 0.5 * exp_m2
419 );
420 }
421}