bacon_sci/integrate/
mod.rs

1/* This file is part of bacon.
2 * Copyright (c) Wyatt Campbell.
3 *
4 * See repository LICENSE for information.
5 */
6
7use nalgebra::{ComplexField, RealField};
8use num_traits::{FromPrimitive, One, Zero};
9use std::f64;
10
11mod gaussian;
12pub use gaussian::*;
13mod tables;
14
15use tables::WEIGHTS_DE;
16
17// Taken and modified from https://github.com/Eh2406/quadrature/blob/master/src/double_exponential/mod.rs
18// published under the BSD license
19fn integrate_core<N: ComplexField + FromPrimitive + Copy, F: FnMut(N::RealField) -> N>(
20    mut f: F,
21    tol: N::RealField,
22) -> Result<N, String>
23where
24    <N as ComplexField>::RealField: FromPrimitive + Copy,
25{
26    let mut error_estimate = N::RealField::one() + tol;
27    let mut num_function_evaluations = 1;
28    let mut current_delta = N::RealField::zero();
29
30    let half = N::from_f64(0.5).unwrap();
31    let one_point_nine = N::from_f64(1.9).unwrap().real();
32    let two_point_one = N::from_f64(2.1).unwrap().real();
33    let pi = N::from_f64(f64::consts::PI).unwrap();
34
35    let mut integral = pi * f(N::RealField::zero());
36
37    for &weight in &WEIGHTS_DE {
38        let new_contribution = weight
39            .iter()
40            .map(|&(w, x)| {
41                let x = N::from_f64(x).unwrap().real();
42                N::from_f64(w).unwrap() * (f(x) + f(-x))
43            })
44            .fold(N::zero(), |sum, x| sum + x);
45        num_function_evaluations += 2 * weight.len();
46
47        // difference in consecutive integral estimates
48        let previous_delta_ln = current_delta.ln();
49        current_delta = (half * integral - new_contribution).abs();
50        integral = half * integral + new_contribution;
51
52        // Once convergence kicks in, error is approximately squared at each step.
53        // Determine whether we're in the convergent region by looking at the trend in the error.
54        if num_function_evaluations <= 13 {
55            // level <= 1
56            continue; // previousDelta meaningless, so cannot check convergence.
57        }
58
59        // Exact comparison with zero is harmless here.  Could possibly be replaced with
60        // a small positive upper limit on the size of currentDelta, but determining
61        // that upper limit would be difficult.  At worse, the loop is executed more
62        // times than necessary.  But no infinite loop can result since there is
63        // an upper bound on the loop variable.
64        if current_delta == N::RealField::zero() {
65            error_estimate = N::RealField::zero();
66            break;
67        }
68        // previousDelta != 0 or would have been kicked out previously
69        let r = current_delta.ln() / previous_delta_ln;
70
71        if r > one_point_nine && r < two_point_one {
72            // If convergence theory applied perfectly, r would be 2 in the convergence region.
73            // r close to 2 is good enough. We expect the difference between this integral estimate
74            // and the next one to be roughly delta^2.
75            error_estimate = current_delta * current_delta;
76        } else {
77            // Not in the convergence region.  Assume only that error is decreasing.
78            error_estimate = current_delta;
79        }
80
81        if error_estimate < tol {
82            break;
83        }
84    }
85
86    if error_estimate < tol {
87        Ok(integral)
88    } else {
89        Err("integrate: maximum iterations exceeded".to_owned())
90    }
91}
92
93pub fn integrate<N: ComplexField + FromPrimitive + Copy, F: FnMut(N::RealField) -> N>(
94    left: N::RealField,
95    right: N::RealField,
96    mut f: F,
97    tol: N::RealField,
98) -> Result<N, String>
99where
100    <N as ComplexField>::RealField: FromPrimitive + Copy,
101{
102    if left >= right {
103        return Err("integrate: left must be less than right".to_owned());
104    }
105    if !tol.is_sign_positive() {
106        return Err("integrate: tolerance must be positive".to_owned());
107    }
108
109    let half = N::from_f64(0.5).unwrap().real();
110
111    let scale = (right - left) * half;
112    let shift = (right + left) * half;
113    let scale_cmplx = N::from_real(scale);
114
115    let fun = |x: N::RealField| -> N {
116        let out = f(scale * x + shift);
117        if out.is_finite() {
118            out
119        } else {
120            N::zero()
121        }
122    };
123
124    Ok(integrate_core(fun, tol)? * scale_cmplx)
125}
126
127/// Numerically integrate a function over an interval within a tolerance.
128///
129/// Given a function and end points, numerically integrate using adaptive
130/// simpson's rule until the error is within tolerance or the maximum
131/// iterations are exceeded.
132pub fn integrate_simpson<N: ComplexField + FromPrimitive + Copy, F: FnMut(N::RealField) -> N>(
133    left: N::RealField,
134    right: N::RealField,
135    mut f: F,
136    tol: N::RealField,
137    n_max: usize,
138) -> Result<N, String>
139where
140    <N as ComplexField>::RealField: FromPrimitive + Copy,
141{
142    if left >= right {
143        return Err("integrate: left must be less than right".to_owned());
144    }
145    if !tol.is_sign_positive() {
146        return Err("integrate: tolerance must be positive".to_owned());
147    }
148
149    let sixth = N::from_f64(1.0 / 6.0).unwrap();
150    let third = N::from_f64(1.0 / 3.0).unwrap();
151    let half_real = N::from_f64(0.5).unwrap().real();
152    let one_and_a_half_real = N::from_f64(1.5).unwrap().real();
153    let four = N::from_i32(4).unwrap();
154
155    let mut area = N::zero();
156    let mut i = 1;
157    let mut tol_i = vec![N::from_i32(10).unwrap().real() * tol];
158    let mut left_i = vec![left];
159    let mut step_i = vec![(right - left) * half_real];
160    let mut f_ai = vec![f(left)];
161    let mut f_ci = vec![f(left + step_i[0])];
162    let mut f_bi = vec![f(right)];
163    let mut sum_i = vec![N::from_real(step_i[0]) * (f_ai[0] + four * f_ci[0] + f_bi[0]) * third];
164    let mut l_i = vec![1];
165
166    while i > 0 {
167        let f_d = f(left_i[i - 1] + half_real * step_i[i - 1]);
168        let f_e = f(left_i[i - 1] + one_and_a_half_real * step_i[i - 1]);
169        let s1 = N::from_real(step_i[i - 1]) * (f_ai[i - 1] + four * f_d + f_ci[i - 1]) * sixth;
170        let s2 = N::from_real(step_i[i - 1]) * (f_ci[i - 1] + four * f_e + f_bi[i - 1]) * sixth;
171        let v_1 = left_i[i - 1];
172        let v_2 = f_ai[i - 1];
173        let v_3 = f_ci[i - 1];
174        let v_4 = f_bi[i - 1];
175        let v_5 = step_i[i - 1];
176        let v_6 = tol_i[i - 1];
177        let v_7 = sum_i[i - 1];
178        let v_8 = l_i[i - 1];
179
180        i -= 1;
181
182        if (s1 + s2 - v_7).abs() < v_6 {
183            area += s1 + s2;
184        } else {
185            if v_8 >= n_max {
186                return Err("integrate: maximum iterations exceeded".to_owned());
187            }
188
189            i += 1;
190            if i > left_i.len() {
191                left_i.push(v_1 + v_5);
192                f_ai.push(v_3);
193                f_ci.push(f_e);
194                f_bi.push(v_4);
195                step_i.push(half_real * v_5);
196                tol_i.push(half_real * v_6);
197                sum_i.push(s2);
198                l_i.push(v_8 + 1);
199            } else {
200                left_i[i - 1] = v_1 + v_5;
201                f_ai[i - 1] = v_3;
202                f_ci[i - 1] = f_e;
203                f_bi[i - 1] = v_4;
204                step_i[i - 1] = half_real * v_5;
205                tol_i[i - 1] = half_real * v_6;
206                sum_i[i - 1] = s2;
207                l_i[i - 1] = v_8 + 1;
208            }
209
210            i += 1;
211            if i > left_i.len() {
212                left_i.push(v_1);
213                f_ai.push(v_2);
214                f_ci.push(f_d);
215                f_bi.push(v_3);
216                step_i.push(step_i[i - 2]);
217                tol_i.push(tol_i[i - 2]);
218                sum_i.push(s1);
219                l_i.push(l_i[i - 2]);
220            } else {
221                left_i[i - 1] = v_1;
222                f_ai[i - 1] = v_2;
223                f_ci[i - 1] = f_d;
224                f_bi[i - 1] = v_3;
225                step_i[i - 1] = step_i[i - 2];
226                tol_i[i - 1] = tol_i[i - 2];
227                sum_i[i - 1] = sum_i[i - 2];
228                l_i[i - 1] = l_i[i - 2];
229            }
230        }
231    }
232
233    Ok(area)
234}
235
236/// Numerically integrate a function over an interval.
237///
238/// Given a function and end points, numerically integrate
239/// using Romberg integration. Uses `n` steps.
240pub fn integrate_fixed<N: ComplexField + FromPrimitive + Copy, F: FnMut(N::RealField) -> N>(
241    left: N::RealField,
242    right: N::RealField,
243    mut f: F,
244    n: usize,
245) -> Result<N, String>
246where
247    <N as ComplexField>::RealField: FromPrimitive + Copy,
248{
249    if left >= right {
250        return Err("integrate_fixed: left must be less than right".to_owned());
251    }
252
253    let half = N::from_f64(0.5).unwrap();
254    let half_real = N::from_f64(0.5).unwrap().real();
255    let four = N::from_i32(4).unwrap();
256
257    let mut h = right - left;
258
259    let mut prev_rows = vec![N::zero(); n];
260    prev_rows[0] = N::from_real(h) * half * (f(left) + f(right));
261    let mut next = vec![N::zero(); n];
262
263    for i in 2..=n {
264        let mut acc = N::zero();
265        for k in 1..=(1 << (i - 2)) {
266            acc += f(left + N::from_f64(k as f64 - 0.5).unwrap().real() * h);
267        }
268        acc *= N::from_real(h);
269        acc += prev_rows[0];
270        acc *= half;
271        next[0] = acc;
272        for j in 2..=i {
273            next[j - 1] = next[j - 2]
274                + (next[j - 2] - prev_rows[j - 2]) / (four.powi(j as i32 - 1) - N::one());
275        }
276
277        h *= half_real;
278        prev_rows[..i].clone_from_slice(&next[..i]);
279    }
280
281    Ok(prev_rows[n - 1])
282}