outram_foam_basic_lib/ode/
rkf45.rs1use super::{adaptive_step, integrate_interval, normalize_error, OdeError, OdeSolverConfig, OdeSystem};
23
24const C2: f64 = 1.0 / 4.0;
27const C3: f64 = 3.0 / 8.0;
28const C4: f64 = 12.0 / 13.0;
29const C5: f64 = 1.0;
30const C6: f64 = 1.0 / 2.0;
31
32const A21: f64 = 1.0 / 4.0;
33const A31: f64 = 3.0 / 32.0;
34const A32: f64 = 9.0 / 32.0;
35const A41: f64 = 1932.0 / 2197.0;
36const A42: f64 = -7200.0 / 2197.0;
37const A43: f64 = 7296.0 / 2197.0;
38const A51: f64 = 439.0 / 216.0;
39const A52: f64 = -8.0;
40const A53: f64 = 3680.0 / 513.0;
41const A54: f64 = -845.0 / 4104.0;
42const A61: f64 = -8.0 / 27.0;
43const A62: f64 = 2.0;
44const A63: f64 = -3544.0 / 2565.0;
45const A64: f64 = 1859.0 / 4104.0;
46const A65: f64 = -11.0 / 40.0;
47
48const B1: f64 = 16.0 / 135.0;
50const B3: f64 = 6656.0 / 12825.0;
51const B4: f64 = 28561.0 / 56430.0;
52const B5: f64 = -9.0 / 50.0;
53const B6: f64 = 2.0 / 55.0;
54
55const E1: f64 = 25.0 / 216.0 - B1;
57const E3: f64 = 1408.0 / 2565.0 - B3;
58const E4: f64 = 2197.0 / 4104.0 - B4;
59const E5: f64 = -1.0 / 5.0 - B5;
60const E6: f64 = -B6;
61
62#[derive(Debug, Clone)]
65pub struct Rkf45 {
66 pub config: OdeSolverConfig,
68 dydx0: Vec<f64>,
69 y_temp: Vec<f64>,
70 k2: Vec<f64>,
71 k3: Vec<f64>,
72 k4: Vec<f64>,
73 k5: Vec<f64>,
74 k6: Vec<f64>,
75 err: Vec<f64>,
76 y_stage: Vec<f64>,
77}
78
79impl Rkf45 {
80 pub fn new(n: usize, abs_tol: f64, rel_tol: f64) -> Self {
84 let mut cfg = OdeSolverConfig::default();
85 cfg.abs_tol = abs_tol;
86 cfg.rel_tol = rel_tol;
87 Self {
88 config: cfg,
89 dydx0: vec![0.0; n],
90 y_temp: vec![0.0; n],
91 k2: vec![0.0; n],
92 k3: vec![0.0; n],
93 k4: vec![0.0; n],
94 k5: vec![0.0; n],
95 k6: vec![0.0; n],
96 err: vec![0.0; n],
97 y_stage: vec![0.0; n],
98 }
99 }
100
101 pub fn solve_step<Sys: OdeSystem + ?Sized>(
104 &mut self,
105 ode: &Sys,
106 x: &mut f64,
107 y: &mut Vec<f64>,
108 dx_try: &mut f64,
109 ) -> Result<(), OdeError> {
110 let cfg = self.config.clone();
111 let abs_tol = cfg.abs_tol;
112 let rel_tol = cfg.rel_tol;
113
114 let k2 = &mut self.k2;
115 let k3 = &mut self.k3;
116 let k4 = &mut self.k4;
117 let k5 = &mut self.k5;
118 let k6 = &mut self.k6;
119 let err = &mut self.err;
120 let y_stage = &mut self.y_stage;
121
122 adaptive_step(
123 &cfg,
124 |x0, y0, dydx0, dx, y_out| {
125 let n = y0.len();
126
127 for i in 0..n {
129 y_stage[i] = y0[i] + A21 * dx * dydx0[i];
130 }
131 ode.derivatives(x0 + C2 * dx, y_stage, k2);
132
133 for i in 0..n {
135 y_stage[i] = y0[i] + dx * (A31 * dydx0[i] + A32 * k2[i]);
136 }
137 ode.derivatives(x0 + C3 * dx, y_stage, k3);
138
139 for i in 0..n {
141 y_stage[i] = y0[i] + dx * (A41 * dydx0[i] + A42 * k2[i] + A43 * k3[i]);
142 }
143 ode.derivatives(x0 + C4 * dx, y_stage, k4);
144
145 for i in 0..n {
147 y_stage[i] =
148 y0[i] + dx * (A51 * dydx0[i] + A52 * k2[i] + A53 * k3[i] + A54 * k4[i]);
149 }
150 ode.derivatives(x0 + C5 * dx, y_stage, k5);
151
152 for i in 0..n {
154 y_stage[i] = y0[i]
155 + dx * (A61 * dydx0[i]
156 + A62 * k2[i]
157 + A63 * k3[i]
158 + A64 * k4[i]
159 + A65 * k5[i]);
160 }
161 ode.derivatives(x0 + C6 * dx, y_stage, k6);
162
163 for i in 0..n {
165 y_out[i] = y0[i]
166 + dx * (B1 * dydx0[i] + B3 * k3[i] + B4 * k4[i] + B5 * k5[i] + B6 * k6[i]);
167 }
168
169 for i in 0..n {
171 err[i] =
172 dx * (E1 * dydx0[i] + E3 * k3[i] + E4 * k4[i] + E5 * k5[i] + E6 * k6[i]);
173 }
174
175 normalize_error(y0, y_out, err, abs_tol, rel_tol)
176 },
177 ode,
178 x,
179 y,
180 &mut self.dydx0,
181 &mut self.y_temp,
182 dx_try,
183 )
184 }
185
186 pub fn integrate<Sys: OdeSystem + ?Sized>(
189 &mut self,
190 ode: &Sys,
191 x_start: f64,
192 x_end: f64,
193 y: &mut Vec<f64>,
194 dx_est: &mut f64,
195 ) -> Result<(), OdeError> {
196 let cfg = self.config.clone();
197 integrate_interval(
198 &cfg,
199 &mut |x, y, dx| self.solve_step(ode, x, y, dx),
200 x_start,
201 x_end,
202 y,
203 dx_est,
204 )
205 }
206}
207
208#[cfg(test)]
209mod tests {
210 use petir::mathf::RealMath;
211 use super::*;
212
213 struct DecayOde;
214 impl OdeSystem for DecayOde {
215 fn n_eqns(&self) -> usize {
216 1
217 }
218 fn derivatives(&self, _x: f64, y: &[f64], dydx: &mut Vec<f64>) {
219 dydx[0] = -y[0];
220 }
221 }
222
223 struct VanDerPol {
224 mu: f64,
225 }
226 impl OdeSystem for VanDerPol {
227 fn n_eqns(&self) -> usize {
228 2
229 }
230 fn derivatives(&self, _x: f64, y: &[f64], dydx: &mut Vec<f64>) {
231 dydx[0] = y[1];
232 dydx[1] = self.mu * (1.0 - y[0] * y[0]) * y[1] - y[0];
233 }
234 }
235
236 struct DecayNanOutsideRange;
240 impl OdeSystem for DecayNanOutsideRange {
241 fn n_eqns(&self) -> usize {
242 1
243 }
244 fn derivatives(&self, _x: f64, y: &[f64], dydx: &mut Vec<f64>) {
245 dydx[0] = if y[0].abs() > 2.0 { f64::NAN } else { -y[0] };
246 }
247 }
248
249 #[test]
261 fn rkf45_recovers_from_a_trial_step_that_overflows() {
262 let mut solver = Rkf45::new(1, 1e-8, 1e-6);
263 let mut y = vec![1.0_f64];
264 let mut dx = 10.0;
265 solver
266 .integrate(&DecayNanOutsideRange, 0.0, 1.0, &mut y, &mut dx)
267 .unwrap();
268 assert!((y[0] - (-1.0_f64).r_exp()).abs() < 1e-6, "y = {}", y[0]);
269 }
270
271 #[test]
274 fn rkf45_reports_a_non_finite_derivative_at_the_accepted_state() {
275 let mut solver = Rkf45::new(1, 1e-8, 1e-6);
276 let mut y = vec![3.0_f64];
277 let mut dx = 0.1;
278 let outcome = solver.integrate(&DecayNanOutsideRange, 0.0, 1.0, &mut y, &mut dx);
279 assert_eq!(outcome, Err(OdeError::NonFiniteState));
280 }
281
282 #[test]
283 fn rkf45_exponential_decay() {
284 let ode = DecayOde;
286 let mut solver = Rkf45::new(1, 1e-8, 1e-6);
287 let mut y = vec![1.0_f64];
288 let mut dx = 0.1;
289 solver.integrate(&ode, 0.0, 1.0, &mut y, &mut dx).unwrap();
290 let expected = (-1.0_f64).r_exp();
291 assert!(
292 (y[0] - expected).abs() < 1e-6,
293 "y={:.10}, expected={:.10}",
294 y[0],
295 expected
296 );
297 }
298
299 #[test]
300 fn rkf45_decay_to_5() {
301 let ode = DecayOde;
303 let mut solver = Rkf45::new(1, 1e-9, 1e-7);
304 let mut y = vec![1.0_f64];
305 let mut dx = 0.1;
306 solver.integrate(&ode, 0.0, 5.0, &mut y, &mut dx).unwrap();
307 let expected = (-5.0_f64).r_exp();
308 assert!(
309 (y[0] - expected).abs() < 1e-7,
310 "y={:.12}, expected={:.12}",
311 y[0],
312 expected
313 );
314 }
315
316 #[test]
317 fn rkf45_nonstiff_vdp() {
318 let ode = VanDerPol { mu: 0.5 };
321 let mut solver = Rkf45::new(2, 1e-7, 1e-5);
322 let mut y = vec![2.0_f64, 0.0];
323 let mut dx = 0.1;
324 solver.integrate(&ode, 0.0, 2.0, &mut y, &mut dx).unwrap();
325 assert!(y[0].abs() < 3.0, "y0={}", y[0]);
326 assert!(y[1].abs() < 3.0, "y1={}", y[1]);
327 }
328
329 #[test]
330 fn rkf45_convergence_order_is_fifth() {
331 let exact = (-1.0_f64).r_exp();
336
337 let run = |n: usize| -> f64 {
338 let ode = DecayOde;
339 let mut solver = Rkf45::new(1, 1.0, 0.0);
340 let mut y = vec![1.0_f64];
341 let h = 1.0 / n as f64;
342 let mut x = 0.0_f64;
343 for _ in 0..n {
344 let mut dx_try = h;
345 solver
346 .solve_step(&ode, &mut x, &mut y, &mut dx_try)
347 .unwrap();
348 }
349 (y[0] - exact).abs()
350 };
351
352 let err_coarse = run(5); let err_fine = run(10); let ratio = err_coarse / err_fine;
355 assert!(
357 ratio > 16.0 && ratio < 64.0,
358 "order ratio = {ratio:.2} (expected ~32 for RKF45 5th-order propagation)"
359 );
360 }
361
362 #[test]
363 fn rkf45_exceeds_steps_on_stiff_vdp() {
364 let ode = VanDerPol { mu: 1000.0 };
367 let mut solver = Rkf45::new(2, 1e-6, 1e-4);
368 solver.config.max_steps = 200;
369 let mut y = vec![2.0_f64, 0.0];
370 let mut dx = 0.1;
371 let result = solver.integrate(&ode, 0.0, 0.5, &mut y, &mut dx);
372 assert!(
373 matches!(result, Err(OdeError::MaxStepsExceeded(_))),
374 "expected MaxStepsExceeded, got {:?}",
375 result
376 );
377 }
378}