Skip to main content

comnum_math/
quadrature.rs

1//! Quadrature: the composite Simpson rule ([`simpson`], [`try_simpson`],
2//! [`simpson_weight`]) and the Gauss rules ([`gauss_hermite`], [`gauss_legendre`], and
3//! [`gaussian_nodes`], the nodes of a Gaussian expectation), re-exported at the root.
4
5use crate::C64;
6
7/// The weight below which a node of [`gaussian_nodes`] is left out.
8const NEGLIGIBLE_WEIGHT: f64 = 1e-300;
9
10/// Composite Simpson's rule $\int_{a}^{b} f(t)\, dt$ on `n` intervals (`n` even, at
11/// least 2), exact for cubics.
12///
13/// # Examples
14/// ```
15/// use comnum_math::simpson;
16/// assert!((simpson(|t| t * t * t, 0.0, 2.0, 2) - 4.0).abs() < 1e-15);
17/// assert!((simpson(f64::sin, 0.0, std::f64::consts::PI, 200) - 2.0).abs() < 1e-8);
18/// ```
19pub fn simpson(f: impl Fn(f64) -> f64, a: f64, b: f64, n: usize) -> f64 {
20    try_simpson(|t| Ok::<f64, std::convert::Infallible>(f(t)), a, b, n).unwrap_or_else(|never| match never {})
21}
22
23/// [`simpson`] of a fallible integrand: the first error stops the quadrature.
24///
25/// # Errors
26/// The first error of `f`.
27///
28/// # Examples
29/// ```
30/// use comnum_math::try_simpson;
31/// let v: Result<f64, String> = try_simpson(|t| Ok(t * t), 0.0, 3.0, 4);
32/// assert!((v.unwrap() - 9.0).abs() < 1e-14);
33/// let e: Result<f64, String> = try_simpson(|t| if t > 1.0 { Err("beyond 1".into()) } else { Ok(t) }, 0.0, 2.0, 4);
34/// assert!(e.is_err());
35/// ```
36pub fn try_simpson<E>(f: impl Fn(f64) -> Result<f64, E>, a: f64, b: f64, n: usize) -> Result<f64, E> {
37    let n = n.max(2).div_ceil(2) * 2;
38    let h = (b - a) / n as f64;
39    let mut acc = f(a)? + f(b)?;
40    for k in 1..n {
41        acc += simpson_weight(k, n) * f(a + k as f64 * h)?;
42    }
43    Ok(acc * h / 3.0)
44}
45
46/// The weight of node `k` of Simpson's rule on `n` intervals (`n` even), before the
47/// factor $h/3$: 1 at the ends, 4 at the odd nodes, 2 at the even inner ones.
48#[inline]
49pub fn simpson_weight(k: usize, n: usize) -> f64 {
50    if k == 0 || k == n {
51        1.0
52    } else if k % 2 == 1 {
53        4.0
54    } else {
55        2.0
56    }
57}
58
59/// Nodes and weights of the `n`-point Gauss–Hermite rule,
60/// `∫ f(t) e^{-t²} dt ≈ Σ_k w_k f(t_k)`, exact for polynomials of degree `2n - 1`.
61///
62/// The nodes are the roots of the Hermite polynomial `H_n`, found by Newton's method
63/// on the orthonormal recurrence (Numerical Recipes, `gauher`); they come in
64/// decreasing order. An expectation over `X ~ N(0, s²)` is
65/// `E f(X) ≈ Σ_k w_k f(√2 s t_k) / √π`.
66///
67/// # Examples
68/// ```
69/// use comnum_math::gauss_hermite;
70/// let (t, w) = gauss_hermite(20);
71/// let pi = std::f64::consts::PI;
72/// assert!((w.iter().sum::<f64>() - pi.sqrt()).abs() < 1e-12);
73/// // E X⁴ = 3 for X ~ N(0, 1)
74/// let m4: f64 = t.iter().zip(&w).map(|(t, w)| w * (2f64.sqrt() * t).powi(4)).sum::<f64>() / pi.sqrt();
75/// assert!((m4 - 3.0).abs() < 1e-10);
76/// ```
77pub fn gauss_hermite(n: usize) -> (Vec<f64>, Vec<f64>) {
78    const PIM4: f64 = 0.751_125_544_464_942_5; // π^(-1/4)
79    let mut x = vec![0.0; n];
80    let mut w = vec![0.0; n];
81    let nf = n as f64;
82    let mut z = 0.0f64;
83    for i in 0..n.div_ceil(2) {
84        z = match i {
85            0 => (2.0 * nf + 1.0).sqrt() - 1.85575 * (2.0 * nf + 1.0).powf(-0.16667),
86            1 => z - 1.14 * nf.powf(0.426) / z,
87            2 => 1.86 * z - 0.86 * x[0],
88            3 => 1.91 * z - 0.91 * x[1],
89            _ => 2.0 * z - x[i - 2],
90        };
91        let mut pp = 1.0;
92        for _ in 0..200 {
93            let (mut p1, mut p2) = (PIM4, 0.0);
94            for j in 0..n {
95                let p3 = p2;
96                p2 = p1;
97                let jf = j as f64;
98                p1 = z * (2.0 / (jf + 1.0)).sqrt() * p2 - (jf / (jf + 1.0)).sqrt() * p3;
99            }
100            pp = (2.0 * nf).sqrt() * p2;
101            let z1 = z;
102            z = z1 - p1 / pp;
103            if (z - z1).abs() <= 1e-15 * z.abs().max(1.0) {
104                break;
105            }
106        }
107        x[i] = z;
108        x[n - 1 - i] = -z;
109        w[i] = 2.0 / (pp * pp);
110        w[n - 1 - i] = w[i];
111    }
112    (x, w)
113}
114
115/// Nodes and weights of the `n`-point Gauss–Legendre rule on $[-1, 1]$,
116/// `∫ f(t) dt ≈ Σ_k w_k f(t_k)`, exact for polynomials of degree `2n - 1`.
117///
118/// The nodes are the roots of the Legendre polynomial $P_{n}$, found by Newton's method
119/// on its recurrence (Numerical Recipes, `gauleg`); they come in increasing order. On
120/// $[a, b]$ the nodes are $\frac{a + b}{2} + \frac{b - a}{2} t_{k}$ and the weights
121/// $\frac{b - a}{2} w_{k}$.
122///
123/// # Examples
124/// ```
125/// use comnum_math::gauss_legendre;
126/// let (t, w) = gauss_legendre(5);
127/// assert!((w.iter().sum::<f64>() - 2.0).abs() < 1e-14);
128/// // exact for t⁸: 2/9
129/// let m8: f64 = t.iter().zip(&w).map(|(t, w)| w * t.powi(8)).sum();
130/// assert!((m8 - 2.0 / 9.0).abs() < 1e-14);
131/// assert!((t[4] - 0.906_179_845_938_664).abs() < 1e-15);
132/// ```
133pub fn gauss_legendre(n: usize) -> (Vec<f64>, Vec<f64>) {
134    let mut x = vec![0.0; n];
135    let mut w = vec![0.0; n];
136    let nf = n as f64;
137    for i in 0..n.div_ceil(2) {
138        let mut z = (std::f64::consts::PI * (i as f64 + 0.75) / (nf + 0.5)).cos();
139        let mut pp = 1.0;
140        for _ in 0..100 {
141            let (mut p1, mut p2) = (1.0, 0.0);
142            for j in 0..n {
143                let p3 = p2;
144                p2 = p1;
145                let jf = j as f64;
146                p1 = ((2.0 * jf + 1.0) * z * p2 - jf * p3) / (jf + 1.0);
147            }
148            pp = nf * (z * p1 - p2) / (z * z - 1.0);
149            let z1 = z;
150            z = z1 - p1 / pp;
151            if (z - z1).abs() <= 1e-15 {
152                break;
153            }
154        }
155        x[i] = -z;
156        x[n - 1 - i] = z;
157        w[i] = 2.0 / ((1.0 - z * z) * pp * pp);
158        w[n - 1 - i] = w[i];
159    }
160    (x, w)
161}
162
163/// The nodes $x_{k} = \cos\frac{(2k - 1)\pi}{2n}$ and the equal weights $\pi/n$ of the
164/// $n$-point Gauss–Chebyshev rule of the first kind:
165/// $\int_{-1}^{1} \frac{f(x)}{\sqrt{1 - x^{2}}}\,dx \approx \frac{\pi}{n}\sum_{k=1}^{n} f(x_{k})$,
166/// exact for polynomials of degree $2n - 1$. A smooth integrand $g$ on $[-1, 1]$ is
167/// integrated as $f(x) = g(x)\sqrt{1 - x^{2}}$ (the form of Ding et al. 2014, eq. (4)).
168///
169/// # Examples
170/// ```
171/// use comnum_math::gauss_chebyshev;
172/// let (x, w) = gauss_chebyshev(8);
173/// // the integral of x^2 / sqrt(1 - x^2) on [-1, 1] is pi/2
174/// let i: f64 = x.iter().zip(&w).map(|(x, w)| w * x * x).sum();
175/// assert!((i - std::f64::consts::FRAC_PI_2).abs() < 1e-14);
176/// ```
177pub fn gauss_chebyshev(n: usize) -> (Vec<f64>, Vec<f64>) {
178    let x = (1..=n).map(|k| ((2 * k - 1) as f64 * std::f64::consts::PI / (2 * n) as f64).cos()).collect();
179    (x, vec![std::f64::consts::PI / n as f64; n])
180}
181
182/// Quadrature nodes `(w, weight)` of Gaussian noise, `E f(W) ≈ Σ weight f(w)`, from the
183/// `n`-point [`gauss_hermite`] rule: `W ~ CN(0, σ²)` by the product rule on the two
184/// dimensions (`complex`), or `W ~ N(0, σ²)` on the real axis. The nodes of negligible
185/// weight (below `1e-300`) are left out.
186///
187/// # Examples
188/// ```
189/// use comnum_math::gaussian_nodes;
190/// // E|W|² = σ² for W ~ CN(0, σ²); E W² = σ² for W ~ N(0, σ²)
191/// for complex in [true, false] {
192///     let m2: f64 = gaussian_nodes(0.3, complex, 16).iter().map(|(w, p)| p * w.norm_sqr()).sum();
193///     assert!((m2 - 0.3).abs() < 1e-12);
194/// }
195/// ```
196pub fn gaussian_nodes(noise_var: f64, complex: bool, n: usize) -> Vec<(C64, f64)> {
197    let (t, a) = gauss_hermite(n.max(2));
198    let pi = std::f64::consts::PI;
199    if complex {
200        let s = noise_var.sqrt();
201        t.iter()
202            .zip(&a)
203            .flat_map(|(tr, ar)| t.iter().zip(&a).map(move |(ti, ai)| (C64::new(s * tr, s * ti), ar * ai / pi)))
204            .filter(|(_, w)| *w > NEGLIGIBLE_WEIGHT)
205            .collect()
206    } else {
207        let s = (2.0 * noise_var).sqrt();
208        t.iter().zip(&a).map(|(t, a)| (C64::new(s * t, 0.0), a / pi.sqrt())).collect()
209    }
210}