1#![allow(clippy::needless_range_loop)]
6pub trait OdeSystem {
11 fn ode_rhs(&self, t: f64, y: &[f64], dydt: &mut [f64]);
18}
19pub 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}
26pub 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}
40pub 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}
54pub 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}
130pub 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}
176pub 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}
188pub 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}
208pub 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}
250pub 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}
268pub 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 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}