mathhook_core/calculus/integrals/numerical/
simpson.rs

1//! Adaptive Simpson's rule integration
2//!
3//! Implements adaptive Simpson's rule with recursive subdivision
4//! for automatic error control.
5
6use super::{IntegrationConfig, IntegrationResult, NumericalIntegrator};
7use crate::error::MathError;
8
9/// Adaptive Simpson's rule integrator
10pub struct AdaptiveSimpson;
11
12/// Parameters for adaptive Simpson recursion
13struct SimpsonRecursionParams<'a> {
14    tolerance: f64,
15    s_whole: f64,
16    fa: f64,
17    fb: f64,
18    depth: usize,
19    max_depth: usize,
20    subdivisions: &'a mut usize,
21}
22
23impl AdaptiveSimpson {
24    /// Create a new adaptive Simpson integrator
25    ///
26    /// # Examples
27    ///
28    /// ```rust
29    /// use mathhook_core::calculus::integrals::numerical::AdaptiveSimpson;
30    ///
31    /// let integrator = AdaptiveSimpson::new();
32    /// ```
33    pub fn new() -> Self {
34        Self
35    }
36
37    /// Compute Simpson's rule estimate over [a, b]
38    fn simpson_step<F>(&self, f: &F, a: f64, b: f64, fa: f64, fb: f64) -> f64
39    where
40        F: Fn(f64) -> f64,
41    {
42        let mid = (a + b) / 2.0;
43        let fmid = f(mid);
44        let h = (b - a) / 6.0;
45        h * (fa + 4.0 * fmid + fb)
46    }
47
48    /// Recursive adaptive Simpson integration
49    fn adaptive_simpson_recursive<F>(
50        &self,
51        f: &F,
52        a: f64,
53        b: f64,
54        params: &mut SimpsonRecursionParams,
55    ) -> Result<f64, MathError>
56    where
57        F: Fn(f64) -> f64,
58    {
59        if params.depth > params.max_depth {
60            return Err(MathError::MaxIterationsReached {
61                max_iterations: params.max_depth,
62            });
63        }
64
65        let mid = (a + b) / 2.0;
66        let fmid = f(mid);
67
68        let s_left = self.simpson_step(f, a, mid, params.fa, fmid);
69        let s_right = self.simpson_step(f, mid, b, fmid, params.fb);
70        let s_split = s_left + s_right;
71
72        let error = (s_split - params.s_whole).abs();
73
74        if error < 15.0 * params.tolerance {
75            *params.subdivisions += 1;
76            Ok(s_split + error / 15.0)
77        } else {
78            *params.subdivisions += 1;
79
80            let mut left_params = SimpsonRecursionParams {
81                tolerance: params.tolerance / 2.0,
82                s_whole: s_left,
83                fa: params.fa,
84                fb: fmid,
85                depth: params.depth + 1,
86                max_depth: params.max_depth,
87                subdivisions: params.subdivisions,
88            };
89
90            let left_result = self.adaptive_simpson_recursive(f, a, mid, &mut left_params)?;
91
92            let mut right_params = SimpsonRecursionParams {
93                tolerance: params.tolerance / 2.0,
94                s_whole: s_right,
95                fa: fmid,
96                fb: params.fb,
97                depth: params.depth + 1,
98                max_depth: params.max_depth,
99                subdivisions: params.subdivisions,
100            };
101
102            let right_result = self.adaptive_simpson_recursive(f, mid, b, &mut right_params)?;
103
104            Ok(left_result + right_result)
105        }
106    }
107}
108
109impl Default for AdaptiveSimpson {
110    fn default() -> Self {
111        Self::new()
112    }
113}
114
115impl NumericalIntegrator for AdaptiveSimpson {
116    fn integrate<F>(
117        &self,
118        f: F,
119        a: f64,
120        b: f64,
121        config: &IntegrationConfig,
122    ) -> Result<IntegrationResult, MathError>
123    where
124        F: Fn(f64) -> f64,
125    {
126        if a >= b {
127            return Err(MathError::InvalidInterval { lower: a, upper: b });
128        }
129
130        let fa = f(a);
131        let fb = f(b);
132        let s_whole = self.simpson_step(&f, a, b, fa, fb);
133
134        let max_depth = (config.max_iterations as f64).log2().ceil() as usize;
135
136        let mut subdivisions = 0;
137
138        let mut params = SimpsonRecursionParams {
139            tolerance: config.tolerance,
140            s_whole,
141            fa,
142            fb,
143            depth: 0,
144            max_depth,
145            subdivisions: &mut subdivisions,
146        };
147
148        let value = self.adaptive_simpson_recursive(&f, a, b, &mut params)?;
149
150        let error_estimate = config.tolerance;
151
152        Ok(IntegrationResult {
153            value,
154            error_estimate,
155            iterations: subdivisions,
156            subdivisions,
157        })
158    }
159}
160
161#[cfg(test)]
162mod tests {
163    use super::*;
164
165    #[test]
166    fn test_adaptive_simpson_polynomial() {
167        let integrator = AdaptiveSimpson::new();
168        let config = IntegrationConfig {
169            tolerance: 1e-10,
170            ..Default::default()
171        };
172
173        let result = integrator.integrate(|x| x * x, 0.0, 1.0, &config).unwrap();
174
175        assert!((result.value - 1.0 / 3.0).abs() < 1e-9);
176    }
177
178    #[test]
179    fn test_adaptive_simpson_sine() {
180        let integrator = AdaptiveSimpson::new();
181        let config = IntegrationConfig {
182            tolerance: 1e-8,
183            ..Default::default()
184        };
185
186        let result = integrator
187            .integrate(|x| x.sin(), 0.0, std::f64::consts::PI, &config)
188            .unwrap();
189
190        assert!((result.value - 2.0).abs() < 1e-7);
191    }
192
193    #[test]
194    fn test_adaptive_simpson_exponential() {
195        let integrator = AdaptiveSimpson::new();
196        let config = IntegrationConfig {
197            tolerance: 1e-10,
198            ..Default::default()
199        };
200
201        let result = integrator
202            .integrate(|x| x.exp(), 0.0, 1.0, &config)
203            .unwrap();
204
205        let expected = std::f64::consts::E - 1.0;
206        assert!((result.value - expected).abs() < 1e-9);
207    }
208
209    #[test]
210    fn test_adaptive_simpson_oscillatory() {
211        let integrator = AdaptiveSimpson::new();
212        let config = IntegrationConfig {
213            tolerance: 1e-6,
214            ..Default::default()
215        };
216
217        let result = integrator
218            .integrate(|x| (10.0 * x).sin(), 0.0, 1.0, &config)
219            .unwrap();
220
221        let expected = (1.0 - (10.0_f64).cos()) / 10.0;
222        assert!((result.value - expected).abs() < 1e-5);
223    }
224
225    #[test]
226    fn test_adaptive_simpson_cubic() {
227        let integrator = AdaptiveSimpson::new();
228        let config = IntegrationConfig {
229            tolerance: 1e-10,
230            ..Default::default()
231        };
232
233        let result = integrator
234            .integrate(|x| x * x * x, 0.0, 2.0, &config)
235            .unwrap();
236
237        assert!((result.value - 4.0).abs() < 1e-9);
238    }
239
240    #[test]
241    fn test_adaptive_simpson_invalid_interval() {
242        let integrator = AdaptiveSimpson::new();
243        let config = IntegrationConfig::default();
244
245        let result = integrator.integrate(|x| x, 1.0, 0.0, &config);
246        assert!(result.is_err());
247    }
248}