mathhook_core/calculus/ode/numerical/
adaptive.rs

1//! Adaptive step size methods for numerical ODE solving
2//!
3//! Implements Runge-Kutta-Fehlberg method (RKF45) with automatic step size adjustment
4//! based on error tolerance. Uses embedded 4th and 5th order Runge-Kutta formulas
5//! to estimate local truncation error.
6
7use crate::calculus::ode::first_order::ODEError;
8
9/// Configuration for adaptive step size solver
10#[derive(Debug, Clone)]
11pub struct AdaptiveConfig {
12    /// Error tolerance for step size adjustment
13    pub tolerance: f64,
14    /// Minimum allowed step size
15    pub min_step: f64,
16    /// Maximum allowed step size
17    pub max_step: f64,
18    /// Safety factor for step size adjustment
19    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
33/// Solves a first-order ODE using Runge-Kutta-Fehlberg method with adaptive step size
34///
35/// # Arguments
36///
37/// * `f` - The derivative function f(x, y) where dy/dx = f(x, y)
38/// * `x0` - Initial x value
39/// * `y0` - Initial y value
40/// * `x_end` - Final x value
41/// * `initial_step` - Initial step size
42/// * `config` - Configuration for adaptive stepping
43///
44/// # Returns
45///
46/// Vector of (x, y) solution points with adaptively chosen steps
47///
48/// # Examples
49///
50/// ```
51/// use mathhook_core::calculus::ode::numerical::adaptive::{rkf45_method, AdaptiveConfig};
52///
53/// let solution = rkf45_method(
54///     |_x, y| y,
55///     0.0,
56///     1.0,
57///     1.0,
58///     0.1,
59///     &AdaptiveConfig::default()
60/// );
61///
62/// assert!(solution.len() > 0);
63/// let (_, y_final) = solution.last().unwrap();
64/// let expected = 1.0_f64.exp();
65/// assert!((y_final - expected).abs() < 1e-5);
66/// ```
67pub 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
156/// Solves a first-order ODE using adaptive RKF45 method with Result type
157///
158/// # Arguments
159///
160/// * `f` - The derivative function f(x, y)
161/// * `x0` - Initial x value
162/// * `y0` - Initial y value
163/// * `x_end` - Final x value
164/// * `initial_step` - Initial step size
165/// * `config` - Adaptive configuration
166///
167/// # Returns
168///
169/// Result containing vector of (x, y) solution points
170pub 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}