mathhook_core/calculus/ode/numerical/
adaptive.rs1use crate::calculus::ode::first_order::ODEError;
8
9#[derive(Debug, Clone)]
11pub struct AdaptiveConfig {
12 pub tolerance: f64,
14 pub min_step: f64,
16 pub max_step: f64,
18 pub safety_factor: f64,
20}
21
22impl Default for AdaptiveConfig {
23 fn default() -> Self {
24 Self {
25 tolerance: 1e-6,
26 min_step: 1e-10,
27 max_step: 1.0,
28 safety_factor: 0.9,
29 }
30 }
31}
32
33pub fn rkf45_method<F>(
68 f: F,
69 x0: f64,
70 y0: f64,
71 x_end: f64,
72 initial_step: f64,
73 config: &AdaptiveConfig,
74) -> Vec<(f64, f64)>
75where
76 F: Fn(f64, f64) -> f64,
77{
78 if initial_step <= 0.0 {
79 return vec![(x0, y0)];
80 }
81
82 let mut solution = Vec::new();
83 let mut x = x0;
84 let mut y = y0;
85 let direction = if x_end > x0 { 1.0 } else { -1.0 };
86 let mut h = initial_step.min(config.max_step) * direction;
87
88 solution.push((x, y));
89 while (direction > 0.0 && x < x_end) || (direction < 0.0 && x > x_end) {
90 if h.abs() < config.min_step {
91 h = config.min_step * direction;
92 }
93
94 if (direction > 0.0 && x + h > x_end) || (direction < 0.0 && x + h < x_end) {
95 h = x_end - x;
96 }
97
98 let (y_new, error, h_new) = rkf45_step(&f, x, y, h, config);
99
100 if error <= config.tolerance || h.abs() <= config.min_step {
101 x += h;
102 y = y_new;
103 solution.push((x, y));
104 }
105
106 h = h_new;
107 }
108
109 solution
110}
111
112fn rkf45_step<F>(f: &F, x: f64, y: f64, h: f64, config: &AdaptiveConfig) -> (f64, f64, f64)
113where
114 F: Fn(f64, f64) -> f64,
115{
116 let k1 = f(x, y);
117 let k2 = f(x + h / 4.0, y + h * k1 / 4.0);
118 let k3 = f(x + 3.0 * h / 8.0, y + h * (3.0 * k1 + 9.0 * k2) / 32.0);
119 let k4 = f(
120 x + 12.0 * h / 13.0,
121 y + h * (1932.0 * k1 - 7200.0 * k2 + 7296.0 * k3) / 2197.0,
122 );
123 let k5 = f(
124 x + h,
125 y + h * (439.0 * k1 / 216.0 - 8.0 * k2 + 3680.0 * k3 / 513.0 - 845.0 * k4 / 4104.0),
126 );
127 let k6 = f(
128 x + h / 2.0,
129 y + h
130 * (-8.0 * k1 / 27.0 + 2.0 * k2 - 3544.0 * k3 / 2565.0 + 1859.0 * k4 / 4104.0
131 - 11.0 * k5 / 40.0),
132 );
133
134 let y4 = y + h * (25.0 * k1 / 216.0 + 1408.0 * k3 / 2565.0 + 2197.0 * k4 / 4104.0 - k5 / 5.0);
135
136 let y5 = y + h
137 * (16.0 * k1 / 135.0 + 6656.0 * k3 / 12825.0 + 28561.0 * k4 / 56430.0 - 9.0 * k5 / 50.0
138 + 2.0 * k6 / 55.0);
139
140 let error = (y5 - y4).abs();
141
142 let h_new = if error > 0.0 {
143 let scale = config.safety_factor * (config.tolerance / error).powf(0.2);
144 (h * scale.clamp(0.1, 4.0))
145 .abs()
146 .max(config.min_step)
147 .min(config.max_step)
148 * h.signum()
149 } else {
150 (h.abs() * 2.0).min(config.max_step) * h.signum()
151 };
152
153 (y5, error, h_new)
154}
155
156pub fn solve_adaptive<F>(
171 f: F,
172 x0: f64,
173 y0: f64,
174 x_end: f64,
175 initial_step: f64,
176 config: AdaptiveConfig,
177) -> Result<Vec<(f64, f64)>, ODEError>
178where
179 F: Fn(f64, f64) -> f64,
180{
181 if initial_step <= 0.0 {
182 return Err(ODEError::InvalidInput {
183 message: "Initial step size must be positive".to_owned(),
184 });
185 }
186
187 if config.tolerance <= 0.0 {
188 return Err(ODEError::InvalidInput {
189 message: "Tolerance must be positive".to_owned(),
190 });
191 }
192
193 if !x0.is_finite() || !y0.is_finite() || !x_end.is_finite() {
194 return Err(ODEError::InvalidInput {
195 message: "Initial values and endpoints must be finite".to_owned(),
196 });
197 }
198
199 Ok(rkf45_method(f, x0, y0, x_end, initial_step, &config))
200}
201
202#[cfg(test)]
203mod tests {
204 use super::*;
205
206 #[test]
207 fn test_adaptive_constant_derivative() {
208 let solution = rkf45_method(|_x, _y| 2.0, 0.0, 0.0, 1.0, 0.1, &AdaptiveConfig::default());
209
210 assert!(!solution.is_empty());
211 let (x_final, y_final) = solution.last().unwrap();
212 assert!((x_final - 1.0).abs() < 1e-10);
213 assert!((y_final - 2.0).abs() < 1e-5);
214 }
215
216 #[test]
217 fn test_adaptive_linear_ode() {
218 let solution = rkf45_method(|x, _y| x, 0.0, 0.0, 1.0, 0.1, &AdaptiveConfig::default());
219
220 let (x_final, y_final) = solution.last().unwrap();
221 assert!((x_final - 1.0).abs() < 1e-10);
222 assert!((y_final - 0.5).abs() < 1e-5);
223 }
224
225 #[test]
226 fn test_adaptive_exponential_growth() {
227 let solution = rkf45_method(|_x, y| y, 0.0, 1.0, 1.0, 0.1, &AdaptiveConfig::default());
228
229 let (x_final, y_final) = solution.last().unwrap();
230 assert!((x_final - 1.0).abs() < 1e-10);
231
232 let expected = 1.0_f64.exp();
233 let relative_error = (y_final - expected).abs() / expected;
234 assert!(relative_error < 1e-5);
235 }
236
237 #[test]
238 fn test_adaptive_stiff_problem() {
239 let config = AdaptiveConfig {
240 tolerance: 1e-8,
241 min_step: 1e-12,
242 max_step: 0.1,
243 safety_factor: 0.9,
244 };
245
246 let solution = rkf45_method(|_x, y| -100.0 * y, 0.0, 1.0, 1.0, 0.1, &config);
247
248 let (x_final, y_final) = solution.last().unwrap();
249 assert!((x_final - 1.0).abs() < 1e-10);
250
251 let expected = (-100.0_f64).exp();
252 assert!((y_final - expected).abs() < 1e-6);
253 }
254
255 #[test]
256 fn test_adaptive_backward_integration() {
257 let solution = rkf45_method(|x, _y| x, 1.0, 0.5, 0.0, 0.1, &AdaptiveConfig::default());
258
259 assert!(solution.len() > 1);
260 let (x_first, y_first) = solution[0];
261 let (x_final, y_final) = solution.last().unwrap();
262
263 assert_eq!((x_first, y_first), (1.0, 0.5));
264 assert!((x_final - 0.0).abs() < 1e-10);
265 assert!((y_final - 0.0).abs() < 1e-5);
266 }
267
268 #[test]
269 fn test_adaptive_step_adjustment() {
270 let solution = rkf45_method(|_x, y| y, 0.0, 1.0, 1.0, 0.5, &AdaptiveConfig::default());
271
272 let steps: Vec<f64> = solution
273 .windows(2)
274 .map(|w| (w[1].0 - w[0].0).abs())
275 .collect();
276
277 let min_step = steps.iter().cloned().fold(f64::INFINITY, f64::min);
278 let max_step = steps.iter().cloned().fold(0.0, f64::max);
279
280 assert!(max_step > min_step);
281 }
282
283 #[test]
284 fn test_solve_adaptive_invalid_input() {
285 let result = solve_adaptive(|x, _y| x, 0.0, 0.0, 1.0, -0.1, AdaptiveConfig::default());
286 assert!(result.is_err());
287
288 let config = AdaptiveConfig {
289 tolerance: -1.0,
290 ..Default::default()
291 };
292 let result = solve_adaptive(|x, _y| x, 0.0, 0.0, 1.0, 0.1, config);
293 assert!(result.is_err());
294 }
295
296 #[test]
297 fn test_adaptive_better_accuracy() {
298 use crate::calculus::ode::numerical::runge_kutta::rk4_method;
299
300 let adaptive_sol = rkf45_method(|_x, y| y, 0.0, 1.0, 1.0, 0.1, &AdaptiveConfig::default());
301 let rk4_sol = rk4_method(|_x, y| y, 0.0, 1.0, 1.0, 0.1);
302
303 let expected = 1.0_f64.exp();
304 let (_, y_adaptive) = adaptive_sol.last().unwrap();
305 let (_, y_rk4) = rk4_sol.last().unwrap();
306
307 let error_adaptive = (y_adaptive - expected).abs();
308 let error_rk4 = (y_rk4 - expected).abs();
309
310 assert!(error_adaptive <= error_rk4 * 1.1);
311 }
312}