Skip to main content

oxiphysics_core/ode/
functions.rs

1//! Auto-generated module
2//!
3//! 🤖 Generated with [SplitRS](https://github.com/cool-japan/splitrs)
4
5#![allow(clippy::needless_range_loop)]
6/// Trait representing the right-hand side of an ODE system `dy/dt = f(t, y)`.
7///
8/// Implement this trait to define a system of ordinary differential equations
9/// for use with `AdaptiveIntegrator` and other solvers.
10pub trait OdeSystem {
11    /// Evaluate the right-hand side of the ODE system.
12    ///
13    /// # Arguments
14    /// * `t` - Current time.
15    /// * `y` - Current state vector (length `n`).
16    /// * `dydt` - Output derivative vector (length `n`, must be filled).
17    fn ode_rhs(&self, t: f64, y: &[f64], dydt: &mut [f64]);
18}
19/// Euler forward step: `y_new = y + dt * dydt`
20pub fn euler_step(y: &[f64], dydt: &[f64], dt: f64) -> Vec<f64> {
21    y.iter()
22        .zip(dydt.iter())
23        .map(|(yi, fi)| yi + dt * fi)
24        .collect()
25}
26/// RK2 midpoint method (Heun's method): `y_new = y + dt * f(y + 0.5*dt*f(y))`
27pub fn rk2_step(y: &[f64], f: impl Fn(&[f64]) -> Vec<f64>, dt: f64) -> Vec<f64> {
28    let k1 = f(y);
29    let y_mid: Vec<f64> = y
30        .iter()
31        .zip(k1.iter())
32        .map(|(yi, ki)| yi + 0.5 * dt * ki)
33        .collect();
34    let k2 = f(&y_mid);
35    y.iter()
36        .zip(k2.iter())
37        .map(|(yi, ki)| yi + dt * ki)
38        .collect()
39}
40/// Classic RK4 step.
41pub fn rk4_step(y: &[f64], f: impl Fn(&[f64]) -> Vec<f64>, dt: f64) -> Vec<f64> {
42    let n = y.len();
43    let k1 = f(y);
44    let y2: Vec<f64> = (0..n).map(|i| y[i] + 0.5 * dt * k1[i]).collect();
45    let k2 = f(&y2);
46    let y3: Vec<f64> = (0..n).map(|i| y[i] + 0.5 * dt * k2[i]).collect();
47    let k3 = f(&y3);
48    let y4: Vec<f64> = (0..n).map(|i| y[i] + dt * k3[i]).collect();
49    let k4 = f(&y4);
50    (0..n)
51        .map(|i| y[i] + (dt / 6.0) * (k1[i] + 2.0 * k2[i] + 2.0 * k3[i] + k4[i]))
52        .collect()
53}
54/// Dormand-Prince RK45 adaptive step.
55///
56/// Returns `(y_new, error_estimate)` where `error_estimate` is the RMS norm of
57/// the difference between the 5th- and 4th-order solutions.
58pub fn rk45_step(y: &[f64], f: impl Fn(&[f64]) -> Vec<f64>, dt: f64, _tol: f64) -> (Vec<f64>, f64) {
59    let n = y.len();
60    let a21 = 1.0 / 5.0;
61    let a31 = 3.0 / 40.0;
62    let a32 = 9.0 / 40.0;
63    let a41 = 44.0 / 45.0;
64    let a42 = -56.0 / 15.0;
65    let a43 = 32.0 / 9.0;
66    let a51 = 19372.0 / 6561.0;
67    let a52 = -25360.0 / 2187.0;
68    let a53 = 64448.0 / 6561.0;
69    let a54 = -212.0 / 729.0;
70    let a61 = 9017.0 / 3168.0;
71    let a62 = -355.0 / 33.0;
72    let a63 = 46732.0 / 5247.0;
73    let a64 = 49.0 / 176.0;
74    let a65 = -5103.0 / 18656.0;
75    let b1 = 35.0 / 384.0;
76    let b3 = 500.0 / 1113.0;
77    let b4 = 125.0 / 192.0;
78    let b5 = -2187.0 / 6784.0;
79    let b6 = 11.0 / 84.0;
80    let bh1 = 5179.0 / 57600.0;
81    let bh3 = 7571.0 / 16695.0;
82    let bh4 = 393.0 / 640.0;
83    let bh5 = -92097.0 / 339200.0;
84    let bh6 = 187.0 / 2100.0;
85    let bh7 = 1.0 / 40.0;
86    let k1 = f(y);
87    let y2: Vec<f64> = (0..n).map(|i| y[i] + dt * a21 * k1[i]).collect();
88    let k2 = f(&y2);
89    let y3: Vec<f64> = (0..n)
90        .map(|i| y[i] + dt * (a31 * k1[i] + a32 * k2[i]))
91        .collect();
92    let k3 = f(&y3);
93    let y4: Vec<f64> = (0..n)
94        .map(|i| y[i] + dt * (a41 * k1[i] + a42 * k2[i] + a43 * k3[i]))
95        .collect();
96    let k4 = f(&y4);
97    let y5: Vec<f64> = (0..n)
98        .map(|i| y[i] + dt * (a51 * k1[i] + a52 * k2[i] + a53 * k3[i] + a54 * k4[i]))
99        .collect();
100    let k5 = f(&y5);
101    let y6: Vec<f64> = (0..n)
102        .map(|i| y[i] + dt * (a61 * k1[i] + a62 * k2[i] + a63 * k3[i] + a64 * k4[i] + a65 * k5[i]))
103        .collect();
104    let k6 = f(&y6);
105    let y_new: Vec<f64> = (0..n)
106        .map(|i| y[i] + dt * (b1 * k1[i] + b3 * k3[i] + b4 * k4[i] + b5 * k5[i] + b6 * k6[i]))
107        .collect();
108    let k7 = f(&y_new);
109    let y4th: Vec<f64> = (0..n)
110        .map(|i| {
111            y[i] + dt
112                * (bh1 * k1[i]
113                    + bh3 * k3[i]
114                    + bh4 * k4[i]
115                    + bh5 * k5[i]
116                    + bh6 * k6[i]
117                    + bh7 * k7[i])
118        })
119        .collect();
120    let err = {
121        let sum_sq: f64 = y_new
122            .iter()
123            .zip(y4th.iter())
124            .map(|(a, b)| (a - b) * (a - b))
125            .sum();
126        (sum_sq / n as f64).sqrt()
127    };
128    (y_new, err)
129}
130/// Adaptive RK45 integration using Dormand-Prince.
131///
132/// Returns a vector of `(t, y)` pairs at each accepted step.
133pub fn adaptive_rk45(
134    y0: Vec<f64>,
135    f: impl Fn(&[f64]) -> Vec<f64>,
136    t0: f64,
137    t_end: f64,
138    dt_init: f64,
139    atol: f64,
140    rtol: f64,
141) -> Vec<(f64, Vec<f64>)> {
142    let mut t = t0;
143    let mut y = y0;
144    let mut dt = dt_init;
145    let mut result = vec![(t, y.clone())];
146    let safety = 0.9_f64;
147    let min_scale = 0.2_f64;
148    let max_scale = 10.0_f64;
149    while t < t_end {
150        if t + dt > t_end {
151            dt = t_end - t;
152        }
153        let (y_new, err) = rk45_step(&y, &f, dt, atol);
154        let tol_norm = (atol + rtol * y.iter().map(|v| v.abs()).fold(0.0_f64, f64::max)).max(atol);
155        let err_norm = err / tol_norm;
156        if err_norm <= 1.0 || err == 0.0 {
157            t += dt;
158            y = y_new;
159            result.push((t, y.clone()));
160            let scale = if err == 0.0 {
161                max_scale
162            } else {
163                (safety * err_norm.powf(-0.2)).clamp(min_scale, max_scale)
164            };
165            dt *= scale;
166        } else {
167            let scale = (safety * err_norm.powf(-0.25)).clamp(min_scale, 1.0);
168            dt *= scale;
169        }
170        if dt < 1e-15 * t_end.abs().max(1.0) {
171            break;
172        }
173    }
174    result
175}
176/// Leapfrog step (Störmer-Verlet).
177///
178/// Returns `(new_pos, new_vel)`:
179/// - `pos_new = pos + vel*dt + 0.5*acc*dt²`
180/// - `vel_new = vel + acc*dt`
181pub fn leapfrog_step(pos: &[f64], vel: &[f64], acc: &[f64], dt: f64) -> (Vec<f64>, Vec<f64>) {
182    let new_pos: Vec<f64> = (0..pos.len())
183        .map(|i| pos[i] + vel[i] * dt + 0.5 * acc[i] * dt * dt)
184        .collect();
185    let new_vel: Vec<f64> = (0..vel.len()).map(|i| vel[i] + acc[i] * dt).collect();
186    (new_pos, new_vel)
187}
188/// Velocity-Verlet step.
189///
190/// Returns `(new_pos, new_vel)`:
191/// - `pos_new = pos + vel*dt + 0.5*acc_old*dt²`
192/// - `vel_new = vel + 0.5*(acc_old + acc_new)*dt`
193pub fn velocity_verlet(
194    pos: &[f64],
195    vel: &[f64],
196    acc_old: &[f64],
197    acc_new: &[f64],
198    dt: f64,
199) -> (Vec<f64>, Vec<f64>) {
200    let new_pos: Vec<f64> = (0..pos.len())
201        .map(|i| pos[i] + vel[i] * dt + 0.5 * acc_old[i] * dt * dt)
202        .collect();
203    let new_vel: Vec<f64> = (0..vel.len())
204        .map(|i| vel[i] + 0.5 * (acc_old[i] + acc_new[i]) * dt)
205        .collect();
206    (new_pos, new_vel)
207}
208/// Solves a linear system `A * x = b` using Gaussian elimination with partial pivoting.
209///
210/// Returns `Some(x)` on success, or `None` if the matrix is singular.
211pub fn solve_linear_system(a: &[Vec<f64>], b: &[f64]) -> Option<Vec<f64>> {
212    let n = b.len();
213    let mut mat: Vec<Vec<f64>> = (0..n)
214        .map(|i| {
215            let mut row = a[i].clone();
216            row.push(b[i]);
217            row
218        })
219        .collect();
220    for col in 0..n {
221        let pivot_row = (col..n).max_by(|&r1, &r2| {
222            mat[r1][col]
223                .abs()
224                .partial_cmp(&mat[r2][col].abs())
225                .unwrap_or(std::cmp::Ordering::Equal)
226        })?;
227        mat.swap(col, pivot_row);
228        let pivot = mat[col][col];
229        if pivot.abs() < 1e-15 {
230            return None;
231        }
232        for row in (col + 1)..n {
233            let factor = mat[row][col] / pivot;
234            for k in col..=n {
235                let val = mat[col][k];
236                mat[row][k] -= factor * val;
237            }
238        }
239    }
240    let mut x = vec![0.0_f64; n];
241    for i in (0..n).rev() {
242        let mut sum = mat[i][n];
243        for j in (i + 1)..n {
244            sum -= mat[i][j] * x[j];
245        }
246        x[i] = sum / mat[i][i];
247    }
248    Some(x)
249}
250/// Implicit Euler step: solve `(I - dt*J) * y_new = y + dt*rhs`.
251///
252/// Uses Gaussian elimination with partial pivoting.
253pub fn implicit_euler_step(y: &[f64], jacobian: &[Vec<f64>], rhs: &[f64], dt: f64) -> Vec<f64> {
254    let n = y.len();
255    let mat: Vec<Vec<f64>> = (0..n)
256        .map(|i| {
257            (0..n)
258                .map(|j| {
259                    let delta = if i == j { 1.0 } else { 0.0 };
260                    delta - dt * jacobian[i][j]
261                })
262                .collect()
263        })
264        .collect();
265    let b: Vec<f64> = (0..n).map(|i| y[i] + dt * rhs[i]).collect();
266    solve_linear_system(&mat, &b).unwrap_or_else(|| b.clone())
267}
268/// Compute the mixed absolute/relative RMS error norm.
269///
270/// Uses the formula from Hairer & Wanner:
271/// `norm = sqrt(1/n * sum_i (err_i / (atol + |y_i| * rtol))^2)`
272pub fn mixed_error_norm(y: &[f64], err: &[f64], atol: f64, rtol: f64) -> f64 {
273    let n = y.len();
274    if n == 0 {
275        return 0.0;
276    }
277    let sum: f64 = y
278        .iter()
279        .zip(err.iter())
280        .map(|(&yi, &ei)| {
281            let sc = atol + yi.abs() * rtol;
282            (ei / sc).powi(2)
283        })
284        .sum();
285    (sum / n as f64).sqrt()
286}
287#[cfg(test)]
288mod tests {
289    use super::*;
290    use crate::ode::Adams4;
291    use crate::ode::AdamsBashforthMoulton4;
292    use crate::ode::AdaptiveIntegrator;
293    use crate::ode::Bdf2;
294    use crate::ode::BdfOrder2;
295    use crate::ode::BulirschStoer;
296    use crate::ode::DenseOutputSegment;
297    use crate::ode::DormandPrince;
298    use crate::ode::DormandPrince45;
299    use crate::ode::EventDetector;
300    use crate::ode::Fehlberg45;
301    use crate::ode::ImplicitEulerNewton;
302    use crate::ode::LeapFrog;
303    use crate::ode::PiStepController;
304    use crate::ode::RungeKuttaFehlberg;
305    use crate::ode::StiffOdeSolver;
306    use crate::ode::Verlet;
307    use std::f64::consts::{PI, TAU};
308    #[test]
309    fn test_euler_step() {
310        let y = vec![1.0_f64];
311        let dydt = vec![0.5_f64];
312        let y_new = euler_step(&y, &dydt, 0.1);
313        assert!((y_new[0] - 1.05).abs() < 1e-12, "got {}", y_new[0]);
314    }
315    #[test]
316    fn test_rk4_step_exponential() {
317        let mut y = vec![1.0_f64];
318        let dt = 0.01;
319        let steps = (1.0 / dt) as usize;
320        for _ in 0..steps {
321            y = rk4_step(&y, |v| vec![v[0]], dt);
322        }
323        let e = std::f64::consts::E;
324        assert!((y[0] - e).abs() < 1e-8, "y[0]={}, e={}", y[0], e);
325    }
326    #[test]
327    fn test_leapfrog_uniform_acceleration() {
328        let pos = vec![0.0_f64];
329        let vel = vec![0.0_f64];
330        let acc = vec![1.0_f64];
331        let dt = 0.5;
332        let (new_pos, new_vel) = leapfrog_step(&pos, &vel, &acc, dt);
333        let expected_pos = 0.5 * 1.0 * dt * dt;
334        let expected_vel = 1.0 * dt;
335        assert!(
336            (new_pos[0] - expected_pos).abs() < 1e-12,
337            "pos={}",
338            new_pos[0]
339        );
340        assert!(
341            (new_vel[0] - expected_vel).abs() < 1e-12,
342            "vel={}",
343            new_vel[0]
344        );
345    }
346    #[test]
347    fn test_velocity_verlet_constant_acceleration() {
348        let pos = vec![0.0_f64];
349        let vel = vec![0.0_f64];
350        let acc_old = vec![2.0_f64];
351        let acc_new = vec![2.0_f64];
352        let dt = 1.0;
353        let (new_pos, new_vel) = velocity_verlet(&pos, &vel, &acc_old, &acc_new, dt);
354        assert!((new_pos[0] - 1.0).abs() < 1e-12, "pos={}", new_pos[0]);
355        assert!((new_vel[0] - 2.0).abs() < 1e-12, "vel={}", new_vel[0]);
356    }
357    #[test]
358    fn test_adaptive_rk45_exponential() {
359        let y0 = vec![1.0_f64];
360        let result = adaptive_rk45(y0, |v| vec![v[0]], 0.0, 1.0, 0.1, 1e-8, 1e-6);
361        let (_, y_end) = result.last().expect("should have at least one step");
362        let e = std::f64::consts::E;
363        assert!(
364            (y_end[0] - e).abs() < 1e-5,
365            "y_end={}, e={}, diff={}",
366            y_end[0],
367            e,
368            (y_end[0] - e).abs()
369        );
370    }
371    #[test]
372    fn test_solve_linear_system_2x2() {
373        let a = vec![vec![2.0_f64, 1.0], vec![1.0, 3.0]];
374        let b = vec![5.0_f64, 10.0];
375        let x = solve_linear_system(&a, &b).expect("should solve");
376        assert!((x[0] - 1.0).abs() < 1e-12, "x[0]={}", x[0]);
377        assert!((x[1] - 3.0).abs() < 1e-12, "x[1]={}", x[1]);
378    }
379    /// Simple harmonic oscillator: d²x/dt² = -omega²*x
380    /// State: \[x, v\], RHS: \[v, -omega²*x\]
381    fn harmonic_rhs(y: &[f64], omega: f64) -> Vec<f64> {
382        vec![y[1], -omega * omega * y[0]]
383    }
384    fn harmonic_energy(y: &[f64], omega: f64) -> f64 {
385        0.5 * (y[1] * y[1] + omega * omega * y[0] * y[0])
386    }
387    #[test]
388    fn test_rk4_harmonic_energy_conservation() {
389        let omega = 2.0;
390        let mut y = vec![1.0_f64, 0.0_f64];
391        let e0 = harmonic_energy(&y, omega);
392        let dt = 0.01;
393        let n_steps = 1000;
394        for _ in 0..n_steps {
395            y = rk4_step(&y, |yv| harmonic_rhs(yv, omega), dt);
396        }
397        let e_final = harmonic_energy(&y, omega);
398        assert!(
399            (e_final - e0).abs() / e0 < 1e-6,
400            "Energy drift: e0={e0}, e_final={e_final}"
401        );
402    }
403    #[test]
404    fn test_rk4_harmonic_period() {
405        let omega = 1.0;
406        let t_period = 2.0 * PI / omega;
407        let dt = 0.001;
408        let n_steps = (t_period / dt).round() as usize;
409        let mut y = vec![1.0_f64, 0.0_f64];
410        for _ in 0..n_steps {
411            y = rk4_step(&y, |yv| harmonic_rhs(yv, omega), dt);
412        }
413        assert!((y[0] - 1.0).abs() < 1e-3, "x after period = {}", y[0]);
414        assert!(y[1].abs() < 1e-3, "v after period = {}", y[1]);
415    }
416    #[test]
417    fn test_dormand_prince45_step_accepted() {
418        let dp = DormandPrince45::new(1e-8, 1e-6);
419        let y = vec![1.0_f64];
420        let res = dp.step(&y, |v| vec![-v[0]], 0.1);
421        let exact = (-0.1_f64).exp();
422        assert!(
423            (res.y[0] - exact).abs() < 1e-9,
424            "y={}, exact={}",
425            res.y[0],
426            exact
427        );
428    }
429    #[test]
430    fn test_dormand_prince45_step_result_fields() {
431        let dp = DormandPrince45::new(1e-6, 1e-4);
432        let y = vec![0.0_f64, 1.0_f64];
433        let res = dp.step(&y, |v| vec![v[1], -v[0]], 0.01);
434        assert_eq!(res.y.len(), 2);
435        assert!(res.error_estimate >= 0.0);
436    }
437    #[test]
438    fn test_fehlberg45_exponential() {
439        let rkf = Fehlberg45::new();
440        let y = vec![1.0_f64];
441        let (y_new, err) = rkf.step(&y, |v| vec![v[0]], 0.1);
442        let exact = 0.1_f64.exp();
443        assert!(
444            (y_new[0] - exact).abs() < 1e-8,
445            "y={}, exact={}",
446            y_new[0],
447            exact
448        );
449        assert!(err >= 0.0, "Error estimate should be non-negative");
450    }
451    #[test]
452    fn test_fehlberg45_harmonic_energy() {
453        let omega = 1.0;
454        let mut y = vec![1.0_f64, 0.0_f64];
455        let e0 = harmonic_energy(&y, omega);
456        let rkf = Fehlberg45::new();
457        let dt = 0.01;
458        for _ in 0..500 {
459            let (y_new, _) = rkf.step(&y, |yv| harmonic_rhs(yv, omega), dt);
460            y = y_new;
461        }
462        let e_final = harmonic_energy(&y, omega);
463        assert!(
464            (e_final - e0).abs() / e0 < 1e-4,
465            "Fehlberg energy drift: e0={e0}, e_final={e_final}"
466        );
467    }
468    #[test]
469    fn test_adams4_exponential() {
470        let dt = 0.01;
471        let mut y = vec![1.0_f64];
472        let mut ab = Adams4::new();
473        for _ in 0..4 {
474            let dydt = vec![y[0]];
475            ab.push(dydt);
476            y = rk4_step(&y, |v| vec![v[0]], dt);
477        }
478        for _ in 0..96 {
479            let y_new = ab.step(&y, dt);
480            let dydt_new = vec![y_new[0]];
481            ab.push(dydt_new);
482            y = y_new;
483        }
484        let e = std::f64::consts::E;
485        assert!((y[0] - e).abs() < 5e-4, "Adams4: y={}, e={}", y[0], e);
486    }
487    #[test]
488    fn test_adams4_ready_flag() {
489        let mut ab = Adams4::new();
490        assert!(!ab.ready());
491        for i in 0..4 {
492            ab.push(vec![i as f64]);
493        }
494        assert!(ab.ready());
495    }
496    #[test]
497    fn test_bdf2_stiff_decay() {
498        let mut bdf = Bdf2::new();
499        let mut y = vec![1.0_f64];
500        let dt = 0.1;
501        let n_steps = 10;
502        for step in 0..n_steps {
503            let t_new = (step + 1) as f64 * dt;
504            let y_new = bdf.step(
505                &y,
506                |t, yv| {
507                    let _ = t;
508                    vec![-100.0 * yv[0]]
509                },
510                t_new,
511                dt,
512            );
513            y = y_new;
514        }
515        let t_final = n_steps as f64 * dt;
516        let exact = (-100.0 * t_final).exp();
517        assert!(
518            y[0] < 0.01 || (y[0] - exact).abs() < 0.1,
519            "BDF2 stiff: y={}, exact={}",
520            y[0],
521            exact
522        );
523    }
524    pub(super) struct HarmonicOscillator {
525        omega: f64,
526    }
527    impl OdeSystem for HarmonicOscillator {
528        fn ode_rhs(&self, _t: f64, y: &[f64], dydt: &mut [f64]) {
529            dydt[0] = y[1];
530            dydt[1] = -self.omega * self.omega * y[0];
531        }
532    }
533    #[test]
534    fn test_adaptive_integrator_harmonic() {
535        let system = HarmonicOscillator { omega: 1.0 };
536        let integrator = AdaptiveIntegrator::new(1e-8, 1e-6);
537        let y0 = vec![1.0_f64, 0.0_f64];
538        let result = integrator.integrate(&system, y0, 0.0, 2.0 * PI, 0.1);
539        let (_, y_end) = result.last().expect("should have steps");
540        assert!(
541            (y_end[0] - 1.0).abs() < 1e-4,
542            "x after period = {}",
543            y_end[0]
544        );
545        assert!(y_end[1].abs() < 1e-4, "v after period = {}", y_end[1]);
546    }
547    #[test]
548    fn test_adaptive_integrator_exponential() {
549        pub(super) struct ExponentialDecay;
550        impl OdeSystem for ExponentialDecay {
551            fn ode_rhs(&self, _t: f64, y: &[f64], dydt: &mut [f64]) {
552                dydt[0] = -y[0];
553            }
554        }
555        let integrator = AdaptiveIntegrator::new(1e-9, 1e-7);
556        let result = integrator.integrate(&ExponentialDecay, vec![1.0_f64], 0.0, 1.0, 0.1);
557        let (_, y_end) = result.last().unwrap();
558        let exact = (-1.0_f64).exp();
559        assert!(
560            (y_end[0] - exact).abs() < 1e-6,
561            "y={}, exact={}",
562            y_end[0],
563            exact
564        );
565    }
566    #[test]
567    fn test_adaptive_integrator_step_count() {
568        let system = HarmonicOscillator { omega: 2.0 };
569        let integrator = AdaptiveIntegrator::new(1e-6, 1e-4);
570        let result = integrator.integrate(&system, vec![1.0, 0.0], 0.0, 1.0, 0.05);
571        assert!(result.len() > 1, "Should have more than start point");
572    }
573    #[test]
574    fn test_adaptive_integrator_energy_conservation() {
575        let omega = 3.0;
576        let system = HarmonicOscillator { omega };
577        let integrator = AdaptiveIntegrator::new(1e-9, 1e-7);
578        let y0 = vec![1.0_f64, 0.0_f64];
579        let e0 = harmonic_energy(&y0, omega);
580        let result = integrator.integrate(&system, y0, 0.0, 10.0, 0.05);
581        let (_, y_end) = result.last().unwrap();
582        let e_final = harmonic_energy(y_end, omega);
583        assert!(
584            (e_final - e0).abs() / e0 < 1e-5,
585            "Energy drift: e0={e0}, e_final={e_final}"
586        );
587    }
588    #[test]
589    fn test_dormand_prince_integrates_exp() {
590        let dp = DormandPrince::new(1e-8, 1e-10, 0.5);
591        let result = dp.integrate(|_t, y| vec![y[0]], 0.0, 1.0, vec![1.0_f64]);
592        let (_, y_end) = result.last().expect("should have steps");
593        let e = std::f64::consts::E;
594        assert!(
595            (y_end[0] - e).abs() < 1e-5,
596            "DormandPrince: y={}, e={}",
597            y_end[0],
598            e
599        );
600    }
601    #[test]
602    fn test_dormand_prince_step_count() {
603        let dp = DormandPrince::new(1e-6, 1e-8, 1.0);
604        let result = dp.integrate(|_t, y| vec![-y[0]], 0.0, 2.0, vec![1.0_f64]);
605        assert!(result.len() > 1, "should produce multiple steps");
606    }
607    #[test]
608    fn test_bulirsch_stoer_midpoint_exp() {
609        let f = |_t: f64, y: &[f64]| vec![y[0]];
610        let y0 = vec![1.0_f64];
611        let h = 1.0;
612        for &n in &[2usize, 4, 8, 16] {
613            let yn = BulirschStoer::midpoint_method(&f, 0.0, &y0, h, n);
614            let e = std::f64::consts::E;
615            assert!(
616                (yn[0] - e).abs() < 0.1,
617                "midpoint n={n}: y={}, e={e}",
618                yn[0]
619            );
620        }
621    }
622    #[test]
623    fn test_bulirsch_stoer_midpoint_error_decreases() {
624        let f = |_t: f64, y: &[f64]| vec![y[0]];
625        let y0 = vec![1.0_f64];
626        let e = std::f64::consts::E;
627        let err4 = (BulirschStoer::midpoint_method(&f, 0.0, &y0, 1.0, 4)[0] - e).abs();
628        let err8 = (BulirschStoer::midpoint_method(&f, 0.0, &y0, 1.0, 8)[0] - e).abs();
629        assert!(
630            err8 < err4,
631            "error should decrease with more substeps: err4={err4}, err8={err8}"
632        );
633    }
634    #[test]
635    fn test_bulirsch_stoer_extrapolate() {
636        let f = |_t: f64, y: &[f64]| vec![y[0]];
637        let y0 = vec![1.0_f64];
638        let h = 1.0;
639        let e = std::f64::consts::E;
640        let t_table: Vec<Vec<f64>> = (1..=4)
641            .map(|k| BulirschStoer::midpoint_method(&f, 0.0, &y0, h, 2 * k))
642            .collect();
643        let extrap = BulirschStoer::extrapolate(&t_table, 4);
644        let err_extrap = (extrap[0] - e).abs();
645        let err_direct = (t_table[0][0] - e).abs();
646        assert!(
647            err_extrap < err_direct,
648            "extrapolation should improve accuracy: extrap_err={err_extrap}, direct_err={err_direct}"
649        );
650    }
651    #[test]
652    fn test_verlet_harmonic_energy() {
653        let omega = 1.0_f64;
654        let dt = 0.01_f64;
655        let mut x = vec![1.0_f64];
656        let a0_val = -omega * omega * x[0];
657        let mut x_prev = vec![x[0] - 0.0 * dt + 0.5 * a0_val * dt * dt];
658        let n_steps = 1000;
659        for _ in 0..n_steps {
660            let a: Vec<f64> = vec![-omega * omega * x[0]];
661            Verlet::step(&mut x, &mut x_prev, &a, dt);
662        }
663        let v_approx = (x[0] - x_prev[0]) / dt;
664        let energy = 0.5 * (v_approx * v_approx + omega * omega * x[0] * x[0]);
665        assert!(
666            (energy - 0.5).abs() < 0.1,
667            "Verlet energy drift: E={energy}, expected ~0.5"
668        );
669    }
670    #[test]
671    fn test_verlet_constant_force() {
672        let dt = 0.1_f64;
673        let a = vec![1.0_f64];
674        let mut x = vec![0.0_f64];
675        let mut x_prev = vec![0.5 * dt * dt];
676        for _ in 0..10 {
677            Verlet::step(&mut x, &mut x_prev, &a, dt);
678        }
679        assert!(
680            (x[0] - 0.5).abs() < 0.01,
681            "Verlet constant force: x={}",
682            x[0]
683        );
684    }
685    #[test]
686    fn test_leapfrog_kick_drift() {
687        let mut v = vec![0.0_f64];
688        let a = vec![1.0_f64];
689        let mut x = vec![0.0_f64];
690        let dt = 0.1_f64;
691        LeapFrog::kick(&mut v, &a, 0.5 * dt);
692        LeapFrog::drift(&mut x, &v, dt);
693        LeapFrog::kick(&mut v, &a, 0.5 * dt);
694        assert!((v[0] - 0.1).abs() < 1e-12, "v={}", v[0]);
695        assert!((x[0] - 0.005).abs() < 1e-12, "x={}", x[0]);
696    }
697    #[test]
698    fn test_leapfrog_conserves_hamiltonian() {
699        let omega = 1.0_f64;
700        let dt = 0.01_f64;
701        let n_steps = 2000;
702        let mut x = vec![1.0_f64];
703        let mut v = vec![0.0_f64];
704        let e0 = 0.5 * (v[0] * v[0] + omega * omega * x[0] * x[0]);
705        for _ in 0..n_steps {
706            let a: Vec<f64> = vec![-omega * omega * x[0]];
707            LeapFrog::kick(&mut v, &a, 0.5 * dt);
708            LeapFrog::drift(&mut x, &v, dt);
709            let a_new: Vec<f64> = vec![-omega * omega * x[0]];
710            LeapFrog::kick(&mut v, &a_new, 0.5 * dt);
711        }
712        let e_final = 0.5 * (v[0] * v[0] + omega * omega * x[0] * x[0]);
713        assert!(
714            (e_final - e0).abs() / e0 < 1e-4,
715            "LeapFrog energy drift: e0={e0}, e_final={e_final}"
716        );
717    }
718    #[test]
719    fn test_stiff_solver_decay() {
720        let solver = StiffOdeSolver::new(0.5);
721        let mut y = vec![1.0_f64];
722        let dt = 0.05_f64;
723        let n_steps = 20;
724        for step in 0..n_steps {
725            let t = step as f64 * dt;
726            y = solver.step_fixed(|_t, yv| vec![-100.0 * yv[0]], t, &y, dt);
727        }
728        let t_final = n_steps as f64 * dt;
729        let exact = (-100.0 * t_final).exp();
730        assert!(
731            y[0].abs() < 0.1 || (y[0] - exact).abs() < 0.1,
732            "StiffOdeSolver: y={}, exact={}",
733            y[0],
734            exact
735        );
736    }
737    #[test]
738    fn test_stiff_solver_constant_rhs() {
739        let solver = StiffOdeSolver::new(0.5);
740        let y = vec![0.0_f64];
741        let dt = 0.1_f64;
742        let y_new = solver.step_fixed(|_t, _y| vec![1.0], 0.0, &y, dt);
743        assert!((y_new[0] - dt).abs() < 1e-6, "y_new={}", y_new[0]);
744    }
745    #[test]
746    fn test_abm4_exponential() {
747        let dt = 0.01;
748        let f = |_t: f64, y: &[f64]| vec![y[0]];
749        let mut abm = AdamsBashforthMoulton4::new();
750        let mut y = vec![1.0_f64];
751        let mut t = 0.0_f64;
752        for _ in 0..4 {
753            let k = f(t, &y);
754            abm.push_history(t, y.clone(), k);
755            y = rk4_step(&y, |v| f(t, v), dt);
756            t += dt;
757        }
758        for _ in 0..96 {
759            let y_new = abm.step(&f, t, &y, dt);
760            let k_new = f(t + dt, &y_new);
761            abm.push_history(t + dt, y_new.clone(), k_new);
762            y = y_new;
763            t += dt;
764        }
765        let e = std::f64::consts::E;
766        assert!((y[0] - e).abs() < 5e-4, "ABM4: y={}, e={}", y[0], e);
767    }
768    #[test]
769    fn test_abm4_harmonic() {
770        let omega = 1.0_f64;
771        let f = |_t: f64, y: &[f64]| harmonic_rhs(y, omega);
772        let mut abm = AdamsBashforthMoulton4::new();
773        let mut y = vec![1.0_f64, 0.0_f64];
774        let e0 = harmonic_energy(&y, omega);
775        let dt = 0.01;
776        let mut t = 0.0_f64;
777        for _ in 0..4 {
778            let k = f(t, &y);
779            abm.push_history(t, y.clone(), k);
780            y = rk4_step(&y, |v| f(t, v), dt);
781            t += dt;
782        }
783        for _ in 0..496 {
784            let y_new = abm.step(&f, t, &y, dt);
785            let k_new = f(t + dt, &y_new);
786            abm.push_history(t + dt, y_new.clone(), k_new);
787            y = y_new;
788            t += dt;
789        }
790        let e_final = harmonic_energy(&y, omega);
791        assert!(
792            (e_final - e0).abs() / e0 < 1e-3,
793            "ABM4 energy drift: e0={e0}, e_final={e_final}"
794        );
795    }
796    #[test]
797    fn test_implicit_euler_newton_decay() {
798        let solver = ImplicitEulerNewton::new(50, 1e-10);
799        let mut y = vec![1.0_f64];
800        let dt = 0.1_f64;
801        for step in 0..5 {
802            let t = step as f64 * dt;
803            y = solver.step(|_t, yv| vec![-100.0 * yv[0]], t, &y, dt);
804        }
805        assert!(y[0] < 0.01, "ImplicitEulerNewton: y={}", y[0]);
806    }
807    #[test]
808    fn test_implicit_euler_newton_linear_growth() {
809        let solver = ImplicitEulerNewton::new(50, 1e-12);
810        let mut y = vec![0.0_f64];
811        let dt = 0.1_f64;
812        for step in 0..10 {
813            let t = step as f64 * dt;
814            y = solver.step(|_t, _yv| vec![1.0], t, &y, dt);
815        }
816        assert!(
817            (y[0] - 1.0).abs() < 1e-8,
818            "ImplicitEulerNewton linear: y={}",
819            y[0]
820        );
821    }
822    #[test]
823    fn test_bdf_order2_exponential() {
824        let bdf = BdfOrder2::new(50, 1e-10);
825        let mut y = vec![1.0_f64];
826        let dt = 0.01_f64;
827        let t0 = 0.0_f64;
828        let y1 = bdf.first_step(|_t, yv| vec![-yv[0]], t0, &y, dt);
829        let mut history = (y.clone(), y1.clone());
830        y = y1;
831        let mut t = dt;
832        for _ in 0..99 {
833            let y_new = bdf.step(|_t, yv| vec![-yv[0]], t, &history.0, &history.1, dt);
834            history = (history.1.clone(), y_new.clone());
835            y = y_new;
836            t += dt;
837        }
838        let expected = (-1.0_f64).exp();
839        assert!(
840            (y[0] - expected).abs() < 1e-4,
841            "BDF2: y={}, expected={}",
842            y[0],
843            expected
844        );
845    }
846    #[test]
847    fn test_event_detector_zero_crossing_sine() {
848        let f = |t: f64| t.sin();
849        let crossings = EventDetector::find_zero_crossings(f, 0.01, TAU, 0.05, 1e-10);
850        let near_pi = crossings.iter().any(|&c| (c - PI).abs() < 0.01);
851        assert!(
852            near_pi,
853            "No crossing found near π; crossings: {:?}",
854            crossings
855        );
856    }
857    #[test]
858    fn test_event_detector_multiple_crossings() {
859        let f = |t: f64| (2.0 * t).sin();
860        let crossings = EventDetector::find_zero_crossings(f, 0.01, TAU, 0.05, 1e-10);
861        assert!(
862            crossings.len() >= 3,
863            "Found only {} crossings",
864            crossings.len()
865        );
866    }
867    #[test]
868    fn test_dense_output_interpolates_linearly() {
869        let seg = DenseOutputSegment {
870            t0: 0.0,
871            t1: 1.0,
872            y0: vec![0.0],
873            y1: vec![1.0],
874            k1: vec![1.0],
875            k6: vec![1.0],
876        };
877        let mid = seg.evaluate(0.5);
878        assert!(mid[0] > 0.0 && mid[0] < 1.0, "dense output mid={}", mid[0]);
879    }
880    #[test]
881    fn test_dense_output_endpoints() {
882        let seg = DenseOutputSegment {
883            t0: 0.0,
884            t1: 2.0,
885            y0: vec![3.0],
886            y1: vec![5.0],
887            k1: vec![1.0],
888            k6: vec![1.0],
889        };
890        let at_t0 = seg.evaluate(0.0);
891        let at_t1 = seg.evaluate(2.0);
892        assert!(
893            (at_t0[0] - 3.0).abs() < 1e-10,
894            "dense output at t0={}",
895            at_t0[0]
896        );
897        assert!(
898            (at_t1[0] - 5.0).abs() < 1e-10,
899            "dense output at t1={}",
900            at_t1[0]
901        );
902    }
903    #[test]
904    fn test_pi_controller_accepts_small_error() {
905        let ctrl = PiStepController::new(1e-6, 1e-8, 0.01, 5.0, 0.2);
906        let (new_dt, accepted) = ctrl.control(0.01, 1e-10);
907        assert!(accepted, "should accept tiny error");
908        assert!(new_dt >= 0.01, "step should not decrease with tiny error");
909    }
910    #[test]
911    fn test_pi_controller_rejects_large_error() {
912        let ctrl = PiStepController::new(1e-6, 1e-8, 1e-6, 5.0, 0.9);
913        let (new_dt, accepted) = ctrl.control(0.1, 5.0);
914        assert!(!accepted, "should reject large error (err=5.0)");
915        assert!(
916            new_dt < 0.1,
917            "step should shrink after rejection, got new_dt={new_dt}"
918        );
919    }
920    #[test]
921    fn test_pi_controller_clamps_max_dt() {
922        let ctrl = PiStepController::new(1e-6, 1e-8, 0.01, 0.5, 0.2);
923        let (new_dt, _) = ctrl.control(0.01, 1e-15);
924        assert!(new_dt <= 0.5, "step should be clamped to max_dt={}", new_dt);
925    }
926    #[test]
927    fn test_mixed_error_norm_zero() {
928        let y = vec![1.0, 2.0, 3.0];
929        let err = vec![0.0, 0.0, 0.0];
930        let norm = mixed_error_norm(&y, &err, 1e-6, 1e-8);
931        assert!(norm < 1e-10, "zero error should give near-zero norm");
932    }
933    #[test]
934    fn test_mixed_error_norm_consistent() {
935        let y = vec![1.0];
936        let err = vec![1e-7];
937        let norm1 = mixed_error_norm(&y, &err, 1e-6, 1e-8);
938        let norm2 = mixed_error_norm(&y, &err, 1e-5, 1e-8);
939        assert!(norm1 > norm2 || norm1.is_finite(), "norm should be finite");
940    }
941    #[test]
942    fn test_rk45_step_exponential() {
943        let y = vec![1.0_f64];
944        let h = 0.1;
945        let (y_new, err) = rk45_step(&y, |v| vec![-v[0]], h, 1e-6);
946        let exact = (-h).exp();
947        assert!(
948            (y_new[0] - exact).abs() < 1e-9,
949            "rk45_step: y_new={}, exact={}",
950            y_new[0],
951            exact
952        );
953        assert!(err >= 0.0, "error estimate must be non-negative");
954    }
955    #[test]
956    fn test_rk45_step_harmonic() {
957        let omega = 2.0;
958        let y = vec![1.0_f64, 0.0_f64];
959        let h = 0.05;
960        let (y_new, _) = rk45_step(&y, |v| harmonic_rhs(v, omega), h, 1e-8);
961        let e0 = harmonic_energy(&y, omega);
962        let e1 = harmonic_energy(&y_new, omega);
963        assert!(
964            (e1 - e0).abs() / e0 < 1e-6,
965            "RK45 energy drift: {}",
966            (e1 - e0).abs() / e0
967        );
968    }
969    #[test]
970    fn test_rk45_multi_step_integration() {
971        let mut y = vec![1.0_f64];
972        let dt = 0.05;
973        for _ in 0..20 {
974            let (y_new, _) = rk45_step(&y, |v| vec![-v[0]], dt, 1e-8);
975            y = y_new;
976        }
977        let exact = (-1.0_f64).exp();
978        assert!(
979            (y[0] - exact).abs() < 1e-8,
980            "RK45 multi-step: y={}, exact={}",
981            y[0],
982            exact
983        );
984    }
985    #[test]
986    fn test_dormand_prince45_error_control() {
987        let dp = DormandPrince45::new(1e-10, 1e-8);
988        let y = vec![1.0_f64, 0.0_f64];
989        let res = dp.step(&y, |v| harmonic_rhs(v, 1.0), 0.001);
990        assert!(
991            res.error_estimate < 1e-9,
992            "DP45 error with small h: {}",
993            res.error_estimate
994        );
995    }
996    #[test]
997    fn test_dormand_prince45_larger_step_larger_error() {
998        let dp = DormandPrince45::new(1e-10, 1e-8);
999        let y = vec![1.0_f64];
1000        let res_small = dp.step(&y, |v| vec![v[0]], 0.001);
1001        let res_large = dp.step(&y, |v| vec![v[0]], 0.5);
1002        assert!(
1003            res_large.error_estimate >= res_small.error_estimate,
1004            "small={} large={}",
1005            res_small.error_estimate,
1006            res_large.error_estimate
1007        );
1008    }
1009    #[test]
1010    fn test_abm4_predicts_then_corrects() {
1011        let f = |_t: f64, y: &[f64]| vec![y[0]];
1012        let mut abm = AdamsBashforthMoulton4::new();
1013        let mut y = vec![1.0_f64];
1014        let dt = 0.01;
1015        let mut t = 0.0;
1016        for _ in 0..4 {
1017            let k = f(t, &y);
1018            abm.push_history(t, y.clone(), k);
1019            y = rk4_step(&y, |v| f(t, v), dt);
1020            t += dt;
1021        }
1022        let y_new = abm.step(&f, t, &y, dt);
1023        assert_eq!(y_new.len(), y.len(), "ABM4 output length mismatch");
1024    }
1025    #[test]
1026    fn test_abm4_ready_after_4_pushes() {
1027        let mut abm = AdamsBashforthMoulton4::new();
1028        assert!(!abm.ready());
1029        for i in 0..4 {
1030            abm.push_history(i as f64 * 0.1, vec![1.0], vec![1.0]);
1031        }
1032        assert!(abm.ready());
1033    }
1034    #[test]
1035    fn test_implicit_euler_newton_2d_decay() {
1036        let solver = ImplicitEulerNewton::new(50, 1e-10);
1037        let mut y = vec![1.0_f64, 1.0_f64];
1038        let dt = 0.1;
1039        for step in 0..10 {
1040            let t = step as f64 * dt;
1041            y = solver.step(|_t, yv| vec![-10.0 * yv[0], -20.0 * yv[1]], t, &y, dt);
1042        }
1043        assert!(
1044            y[0] < 0.1 && y[1] < 0.1,
1045            "Both components should decay: y0={}, y1={}",
1046            y[0],
1047            y[1]
1048        );
1049    }
1050    #[test]
1051    fn test_event_detector_no_crossings() {
1052        let f = |_t: f64| 1.0_f64;
1053        let crossings = EventDetector::find_zero_crossings(f, 0.0, 10.0, 0.1, 1e-10);
1054        assert!(
1055            crossings.is_empty(),
1056            "No crossings expected: {:?}",
1057            crossings
1058        );
1059    }
1060    #[test]
1061    fn test_event_detector_crossing_accuracy() {
1062        let crossings = EventDetector::find_zero_crossings(f64::cos, 0.1, 3.0, 0.1, 1e-10);
1063        let near_half_pi = crossings.iter().any(|&c| (c - PI / 2.0).abs() < 0.01);
1064        assert!(
1065            near_half_pi,
1066            "No crossing near π/2; crossings: {:?}",
1067            crossings
1068        );
1069    }
1070    #[test]
1071    fn test_event_bisect_accuracy() {
1072        let root = EventDetector::bisect(&f64::sin, 3.0, 3.5, 1e-12);
1073        assert!((root - PI).abs() < 1e-10, "bisect root={root}");
1074    }
1075    #[test]
1076    fn test_dense_output_is_monotone_for_monotone_solution() {
1077        let seg = DenseOutputSegment {
1078            t0: 0.0,
1079            t1: 1.0,
1080            y0: vec![0.0],
1081            y1: vec![1.0],
1082            k1: vec![1.0],
1083            k6: vec![1.0],
1084        };
1085        let ts = [0.0, 0.25, 0.5, 0.75, 1.0];
1086        let vals: Vec<f64> = ts.iter().map(|&t| seg.evaluate(t)[0]).collect();
1087        for i in 1..vals.len() {
1088            assert!(
1089                vals[i] >= vals[i - 1],
1090                "Dense output should be non-decreasing: {vals:?}"
1091            );
1092        }
1093    }
1094    #[test]
1095    fn test_dense_output_multiple_components() {
1096        let seg = DenseOutputSegment {
1097            t0: 0.0,
1098            t1: 1.0,
1099            y0: vec![0.0, 1.0],
1100            y1: vec![1.0, 0.0],
1101            k1: vec![1.0, -1.0],
1102            k6: vec![1.0, -1.0],
1103        };
1104        let mid = seg.evaluate(0.5);
1105        assert_eq!(mid.len(), 2);
1106        assert!(mid[0].is_finite() && mid[1].is_finite());
1107    }
1108    #[test]
1109    fn test_implicit_euler_step_linear() {
1110        let y = vec![1.0_f64];
1111        let dt = 0.1;
1112        let lambda = 10.0_f64;
1113        let jacobian = vec![vec![-lambda]];
1114        let rhs = vec![0.0_f64];
1115        let y_new = implicit_euler_step(&y, &jacobian, &rhs, dt);
1116        let expected = 1.0 / (1.0 + dt * lambda);
1117        assert!(
1118            (y_new[0] - expected).abs() < 1e-8,
1119            "implicit Euler: y_new={}, expected={}",
1120            y_new[0],
1121            expected
1122        );
1123    }
1124    #[test]
1125    fn test_implicit_euler_step_constant_rhs() {
1126        let y = vec![2.0_f64];
1127        let dt = 0.5;
1128        let jacobian = vec![vec![0.0_f64]];
1129        let rhs = vec![1.0_f64];
1130        let y_new = implicit_euler_step(&y, &jacobian, &rhs, dt);
1131        assert!(
1132            (y_new[0] - 2.5).abs() < 1e-8,
1133            "implicit Euler constant: y_new={}",
1134            y_new[0]
1135        );
1136    }
1137    #[test]
1138    fn test_mixed_error_norm_empty() {
1139        let norm = mixed_error_norm(&[], &[], 1e-6, 1e-8);
1140        assert!(norm < 1e-15, "empty norm should be 0");
1141    }
1142    #[test]
1143    fn test_mixed_error_norm_single_component() {
1144        let y = vec![10.0];
1145        let err = vec![1e-5];
1146        let norm = mixed_error_norm(&y, &err, 1e-4, 1e-8);
1147        assert!(norm.is_finite() && norm > 0.0);
1148    }
1149    #[test]
1150    fn test_rk2_step_exponential() {
1151        let y = vec![1.0_f64];
1152        let h = 0.01;
1153        let y_new = rk2_step(&y, |v| vec![v[0]], h);
1154        let exact = h.exp();
1155        assert!((y_new[0] - exact).abs() < 1e-5, "RK2: {}", y_new[0]);
1156    }
1157    #[test]
1158    fn test_euler_step_basic() {
1159        let y = vec![0.0_f64, 5.0_f64];
1160        let dydt = vec![1.0_f64, -2.0_f64];
1161        let y_new = euler_step(&y, &dydt, 0.5);
1162        assert!((y_new[0] - 0.5).abs() < 1e-12);
1163        assert!((y_new[1] - 4.0).abs() < 1e-12);
1164    }
1165    #[test]
1166    fn test_solve_linear_system_3x3() {
1167        let a = vec![
1168            vec![1.0_f64, 1.0, 1.0],
1169            vec![1.0, 2.0, 3.0],
1170            vec![1.0, 4.0, 9.0],
1171        ];
1172        let b = vec![6.0_f64, 14.0, 36.0];
1173        let x = solve_linear_system(&a, &b).expect("should solve 3x3");
1174        assert!((x[0] - 1.0).abs() < 1e-10, "x[0]={}", x[0]);
1175        assert!((x[1] - 2.0).abs() < 1e-10, "x[1]={}", x[1]);
1176        assert!((x[2] - 3.0).abs() < 1e-10, "x[2]={}", x[2]);
1177    }
1178    #[test]
1179    fn test_solve_linear_system_singular_returns_none() {
1180        let a = vec![vec![1.0_f64, 2.0], vec![2.0, 4.0]];
1181        let b = vec![1.0_f64, 2.0];
1182        let result = solve_linear_system(&a, &b);
1183        let _ = result;
1184    }
1185    #[test]
1186    fn test_rkf45_step_exponential_decay() {
1187        let rkf = RungeKuttaFehlberg::new(1e-6, 1e-8, 1e-10, 1.0);
1188        let y0 = vec![1.0_f64];
1189        let h = 0.1;
1190        let (y4, _y5, _h_new) = rkf.step(&|_t, y| vec![-y[0]], 0.0, &y0, h);
1191        let exact = (-h).exp();
1192        assert!(
1193            (y4[0] - exact).abs() < 1e-6,
1194            "RKF45 exp decay: y4={}, exact={}",
1195            y4[0],
1196            exact
1197        );
1198    }
1199    #[test]
1200    fn test_rkf45_suggested_step_grows_on_smooth_problem() {
1201        let rkf = RungeKuttaFehlberg::new(1e-6, 1e-8, 1e-12, 10.0);
1202        let y0 = vec![1.0_f64];
1203        let h = 0.01;
1204        let (_y4, _y5, h_new) = rkf.step(&|_t, y| vec![y[0]], 0.0, &y0, h);
1205        assert!(
1206            h_new > 0.0,
1207            "suggested step should be positive, got {h_new}"
1208        );
1209    }
1210    #[test]
1211    fn test_rkf45_integrate_exponential() {
1212        let rkf = RungeKuttaFehlberg::new(1e-6, 1e-8, 1e-12, 1.0);
1213        let traj = rkf.integrate(|_t, y| vec![y[0]], 0.0, 1.0, vec![1.0_f64]);
1214        let (t_last, y_last) = traj.last().unwrap();
1215        assert!((*t_last - 1.0).abs() < 1e-10, "t_last={t_last}");
1216        assert!(
1217            (y_last[0] - 1.0_f64.exp()).abs() < 1e-4,
1218            "RKF45 integrate exp: y={}",
1219            y_last[0]
1220        );
1221    }
1222    #[test]
1223    fn test_rkf45_step_constant_rhs() {
1224        let rkf = RungeKuttaFehlberg::new(1e-8, 1e-10, 1e-15, 1.0);
1225        let y0 = vec![0.0_f64];
1226        let h = 0.5;
1227        let (y4, _y5, _h_new) = rkf.step(&|_t, _y| vec![2.0_f64], 0.0, &y0, h);
1228        assert!((y4[0] - 1.0).abs() < 1e-10, "RKF45 constant rhs: {}", y4[0]);
1229    }
1230    #[test]
1231    fn test_rkf45_step_multidimensional() {
1232        let rkf = RungeKuttaFehlberg::new(1e-8, 1e-10, 1e-15, 1.0);
1233        let y0 = vec![0.0_f64, 1.0_f64];
1234        let h = 0.2;
1235        let (y4, _y5, _h_new) = rkf.step(&|_t, _y| vec![1.0_f64, -1.0_f64], 0.0, &y0, h);
1236        assert!((y4[0] - h).abs() < 1e-10, "y[0]={}", y4[0]);
1237        assert!((y4[1] - (1.0 - h)).abs() < 1e-10, "y[1]={}", y4[1]);
1238    }
1239    #[test]
1240    fn test_dopri_error_norm_zero_for_zero_stages() {
1241        let dp = DormandPrince::new(1e-6, 1e-8, 1.0);
1242        let n = 4;
1243        let z = vec![0.0_f64; n];
1244        let y = vec![1.0_f64; n];
1245        let norm = dp.compute_error_norm(&y, &z, &z, &z, &z, &z, &z, 0.1);
1246        assert!(
1247            norm < 1e-14,
1248            "zero stages should give zero error norm: {norm}"
1249        );
1250    }
1251    #[test]
1252    fn test_dopri_error_norm_positive_for_nonzero_stages() {
1253        let dp = DormandPrince::new(1e-6, 1e-8, 1.0);
1254        let n = 2;
1255        let y = vec![1.0_f64; n];
1256        let k = vec![1.0_f64; n];
1257        let z = vec![0.0_f64; n];
1258        let norm = dp.compute_error_norm(&y, &k, &z, &z, &z, &z, &z, 1.0);
1259        assert!(
1260            norm > 0.0,
1261            "nonzero stage should give positive norm: {norm}"
1262        );
1263        assert!(norm.is_finite(), "norm must be finite");
1264    }
1265    #[test]
1266    fn test_dopri_error_norm_scales_with_h() {
1267        let dp = DormandPrince::new(1e-6, 1e-8, 1.0);
1268        let y = vec![1.0_f64; 3];
1269        let k = vec![1.0_f64; 3];
1270        let z = vec![0.0_f64; 3];
1271        let norm1 = dp.compute_error_norm(&y, &k, &z, &z, &z, &z, &z, 1.0);
1272        let norm2 = dp.compute_error_norm(&y, &k, &z, &z, &z, &z, &z, 2.0);
1273        assert!(
1274            norm2 > norm1,
1275            "error norm should scale with h: {norm1} vs {norm2}"
1276        );
1277    }
1278    #[test]
1279    fn test_richardson_extrapolate_single_estimate() {
1280        let estimates = vec![vec![3.0_f64, 4.0_f64]];
1281        let n_steps = vec![2usize];
1282        let (best, err) = BulirschStoer::richardson_extrapolate(&estimates, &n_steps);
1283        assert_eq!(best, vec![3.0, 4.0]);
1284        assert!(err < 1e-15, "single estimate: error should be 0, got {err}");
1285    }
1286    #[test]
1287    fn test_richardson_extrapolate_improves_accuracy() {
1288        let estimates = vec![vec![1.0_f64], vec![1.0_f64], vec![1.0_f64]];
1289        let n_steps = vec![2usize, 4, 6];
1290        let (best, _err) = BulirschStoer::richardson_extrapolate(&estimates, &n_steps);
1291        assert!(
1292            (best[0] - 1.0).abs() < 1e-12,
1293            "extrapolated value: {}",
1294            best[0]
1295        );
1296    }
1297}