thermite_special/specialized/generic/quadrature.rs
1//! Gauss-Legendre nodes and weights, one root per lane.
2//!
3//! The `n`-point rule integrates polynomials through degree `2n - 1` exactly on `[-1, 1]`:
4//! `int f ~ sum_k w_k f(x_k)` with `x_k` the roots of `P_n` and `w_k = 2 / ((1 - x_k^2) P_n'(x_k)^2)`.
5//!
6//! # Shape
7//!
8//! The root index `k` is the lane's input (`0` is the largest root, `n - 1` the smallest,
9//! `x_{n-1-k} = -x_k`), and `n` is uniform, so a packet of consecutive indices _is_ the rule:
10//! a caller sweeps `k` in packets and stores nodes and weights as it goes. Every lane runs the
11//! same `O(n)` recurrence per Newton step, so the packet costs one root, not `LANES`.
12//!
13//! # Method
14//!
15//! Tricomi's seed `cos(pi (k + 3/4) / (n + 1/2))` is within `O(1/n)` of the root and inside
16//! its Newton basin (the roots of `P_n` are separated by about `pi/n` and the seed's error
17//! is a fraction of that), then `newtons_method` on `P_n` with `P_n'` from the recurrence,
18//! `P_n'(x) = n (x P_n - P_{n-1}) / (x^2 - 1)`, two to four steps in practice, eight at most.
19//! The recurrence `j P_j = (2j - 1) x P_{j-1} - (j - 1) P_{j-2}` runs with its two scalar
20//! coefficients formed once per `j`, no vector division. The residual tolerance scales with
21//! `n`, which is the recurrence's own rounding. Through `|P_n'|`, that is a few `eps`
22//! absolute on interior nodes and far less at the ends, where `P_n'` is `O(n^2)`.
23
24use thermite::{
25 element::FloatElement,
26 math::{
27 TranscendentalMathWithPolicy as _,
28 algorithms::newtons_method,
29 policy::{
30 Policy,
31 policies::{LessPrecision, MaxIterations},
32 },
33 },
34 prelude::*,
35};
36
37use super::ndtr::residual_tolerance;
38use crate::specialized::SpecializedSpecialMath;
39
40/// `(P_n(x), P_n'(x))` by the three-term recurrence.
41#[inline(always)]
42fn legendre_pair<E, V>(x: V, n: u32) -> (V, V)
43where
44 E: FloatElement,
45 V: FloatVector<Element = E>,
46{
47 let mut p_prev = V::ONE;
48 let mut p = x;
49 let mut j = 2u32;
50 while j <= n {
51 // Scalar coefficients once per step: (2j - 1)/j and (j - 1)/j.
52 let jf = E::from_int(j as i64);
53 let a = V::splat(E::from_int((2 * j - 1) as i64) / jf);
54 let b = V::splat(E::from_int((j - 1) as i64) / jf);
55 let next = (a * x).mul_sube(p, b * p_prev);
56 p_prev = p;
57 p = next;
58 j += 1;
59 }
60 // P_n' = n (x P_n - P_{n-1}) / (x^2 - 1). `x^2 - 1` as `-(1 - x)(1 + x)`: near the
61 // end roots `1 - x` is exact (Sterbenz) where `1 - x*x` would lose `eps / (1 - x^2)`,
62 // 150 ulp of the extreme weight at n = 33.
63 let nf = V::splat(E::from_int(n as i64));
64 let dp = -(nf * (x * p - p_prev)) / ((V::ONE - x) * (V::ONE + x));
65 (p, dp)
66}
67
68/// The `k`-th node and weight of the `n`-point Gauss-Legendre rule, `k` per lane.
69#[inline(always)]
70pub fn gauss_legendre_impl<P, E, V>(k: V, n: u32) -> (V, V)
71where
72 P: Policy,
73 E: FloatElement,
74 V: FloatVector<Element = E> + SpecializedSpecialMath<E>,
75{
76 let nf = V::splat(E::from_int(n as i64));
77 let valid = valid_index(k, nf);
78
79 if n == 1 {
80 // P_1 = x: the one root is 0 with weight 2.
81 return (valid.select(V::ZERO, V::NAN), valid.select(V::TWO, V::NAN));
82 }
83
84 // Tricomi's seed, one tier down.
85 let theta = (k + V::splat(<E as FloatElement>::ConstRatio::<3, 4>::VALUE)) / (nf + V::HALF) * V::PI;
86 let x0 = theta.cos_p::<LessPrecision<P>>();
87
88 let tol = residual_tolerance::<P, E, V>(nf);
89 let (x, _) = newtons_method::<V, MaxIterations<P, 8>, _>(x0, tol, valid, None, |x| legendre_pair::<E, V>(x, n));
90
91 // The tolerance is `n` ulp of residual, but `|P_n'|` at the interior roots is only
92 // about `sqrt(n)`, so Newton may stop `8 sqrt(n)` eps short there (56 eps at n = 64).
93 // The pair is needed for the weight anyway, and one more step from it costs a division and
94 // lands the node at the recurrence's own noise floor. The weight keeps this pair's
95 // `P_n'`: its error is `P''/P'` times the step, which is negligible where the step is
96 // large (interior, `P''/P' ~ 2x`) and the step is negligible where `P''/P'` is large.
97 let (p, dp) = legendre_pair::<E, V>(x, n);
98 let x = x - p / dp;
99
100 let one_m_x2 = (V::ONE - x) * (V::ONE + x);
101 let w = V::TWO / (one_m_x2 * dp * dp);
102
103 (valid.select(x, V::NAN), valid.select(w, V::NAN))
104}
105
106/// `k` is a whole number in `[0, n)`.
107#[inline(always)]
108fn valid_index<E: FloatElement, V: FloatVector<Element = E>>(k: V, nf: V) -> V::Mask {
109 k.cmp_ge(V::ZERO) & k.cmp_lt(nf) & k.cmp_eq(k.round())
110}
111
112/// Newton in `x`-space on a `(value, derivative)` pair, for the polynomial rules whose
113/// magnitude varies too much across the interval for a function-space tolerance: a lane
114/// stops when its own step is under `8 sqrt(n)` eps of `1 + |x|`, at most eight steps. No
115/// polishing step: with an `x`-space stop the iterate is already at the recurrence's noise
116/// floor, and a further step there is a random walk of about an ulp (measured on the
117/// smallest root of `L_16^2`, where the floor itself is 7 ulp of 0.38 because the
118/// intermediate `L_m` are about 150 against `L' = 129`).
119#[inline(always)]
120fn newton_x<E, V, F>(mut x: V, n: u32, active: V::Mask, mut f: F) -> (V, V, V)
121where
122 E: FloatElement,
123 V: FloatVector<Element = E>,
124 F: FnMut(V) -> (V, V),
125{
126 let tol = <V as FloatVector>::EPSILON * V::splat(E::from_int(8)) * V::splat(E::from_int(n as i64)).sqrt();
127 // A lane freezes once its own step is small: extra steps at the noise floor walk it
128 // off its answer, and a lane's count of steps must not depend on its packet-mates.
129 let mut frozen = !active;
130 let mut i = 0;
131 while i < 8 {
132 let (p, dp) = f(x);
133 let dx = p / dp;
134 x = frozen.select(x, x - dx);
135 frozen |= dx.abs().cmp_le(tol * (V::ONE + x.abs()));
136 if frozen.all() {
137 break;
138 }
139 i += 1;
140 }
141 let (p, dp) = f(x);
142 (x, p, dp)
143}
144
145/// The `k`-th node and weight of the `n`-point Gauss-Hermite rule (weight `e^{-x^2}` on
146/// the line), `k` per lane, `k = 0` the largest root.
147///
148/// Seed: the WKB phase of the Hermite equation, `x = sqrt(2n+1) cos(phi)` with
149/// `phi - sin(2 phi)/2 = 2 pi (k + 3/4)/(2n + 1)`, solved per lane by four Newton steps
150/// from `phi = (3c/2)^{1/3}`. At the edge this reproduces the Airy constant
151/// (`x_0 ~ sqrt(2n+1) - 1.856 (2n+1)^{-1/6}`) to three digits. The left half is the mirror
152/// of the right. Newton then runs on `h_m = H_m / m!`, whose recurrence
153/// `h_{m+1} = (2x h_m - 2 h_{m-1})/(m + 1)` has a scalar divisor and stays in range where
154/// the raw `H_m` overflows at degree 48, with `h_n' = 2 h_{n-1}`. Weight
155/// `w = sqrt(pi) 2^{n-1} / ((n-1)! n h_{n-1}(x_k)^2)`, the scalar factor a running product.
156/// That factor underflows past `n = 170` in f64 and `n = 40` in f32, which bounds the rule.
157#[inline(always)]
158pub fn gauss_hermite_impl<P, E, V>(k: V, n: u32) -> (V, V)
159where
160 P: Policy,
161 E: FloatElement,
162 V: FloatVector<Element = E> + SpecializedSpecialMath<E>,
163{
164 let nf = V::splat(E::from_int(n as i64));
165 let valid = valid_index(k, nf);
166
167 if n == 1 {
168 return (valid.select(V::ZERO, V::NAN), valid.select(V::SQRT_PI, V::NAN));
169 }
170
171 // Right-half index and the sign to put back.
172 let kk = k.min(nf - V::ONE - k);
173 let neg = k.cmp_gt(kk);
174
175 // phi from the WKB phase.
176 let two_n_p1 = nf.mul_adde(V::TWO, V::ONE);
177 let c = (kk + V::splat(<E as FloatElement>::ConstRatio::<3, 4>::VALUE)) * (V::TAU / two_n_p1);
178 let mut phi = (c * V::splat(<E as FloatElement>::ConstRatio::<3, 2>::VALUE))
179 .cbrt_p::<LessPrecision<P>>()
180 .min(V::FRAC_PI_2);
181 let mut i = 0;
182 while i < 4 {
183 let (s, co) = (phi + phi).sin_cos_p::<LessPrecision<P>>();
184 phi -= (phi - s * V::HALF - c) / (V::ONE - co).max(<V as FloatVector>::EPSILON);
185 i += 1;
186 }
187 let x0 = two_n_p1.sqrt() * phi.cos_p::<LessPrecision<P>>();
188
189 let pair = |x: V| -> (V, V) {
190 let mut h_prev = V::ONE;
191 let mut h = x + x;
192 let mut m = 1u32;
193 while m < n {
194 // (2x h - 2 h_prev)/(m+1) as 2/(m+1) * (x h - h_prev): one FMA and one multiply.
195 let two_inv = V::splat(E::from_ratio(2, (m + 1) as i64));
196 let next = x.mul_sube(h, h_prev) * two_inv;
197 h_prev = h;
198 h = next;
199 m += 1;
200 }
201 (h, h_prev + h_prev)
202 };
203
204 let (x, _, dp) = newton_x::<E, V, _>(x0, n, valid, pair);
205 // dp = 2 h_{n-1} at the (nearly converged) node.
206 let h_nm1 = dp * V::HALF;
207
208 // 2^{n-1} / (n-1)! as a scalar product.
209 let mut factor = E::ONE;
210 let mut m = 1u32;
211 while m < n {
212 factor = factor * E::from_int(2) / E::from_int(m as i64);
213 m += 1;
214 }
215 let w = V::SQRT_PI * V::splat(factor) / (nf * h_nm1 * h_nm1);
216
217 (valid.select(x.neg_c(neg), V::NAN), valid.select(w, V::NAN))
218}
219
220/// The `k`-th node and weight of the `n`-point Gauss-Laguerre rule (weight `x^alpha e^{-x}`
221/// on `[0, inf)`), `k` per lane, `k = 0` the largest root, `alpha > -1` per lane.
222///
223/// Seed: the WKB phase of the Laguerre equation with `x = nu cos^2(psi/2)`,
224/// `nu = 4n + 2 alpha + 2`, `psi - sin psi = 4 pi (k + 3/4)/nu`, five Newton steps from
225/// `psi = (6c)^{1/3}`. The count of phase between the two turning points is
226/// `n + alpha/2 + 1/2`, which is the Bessel-zero offset `alpha/2 - 1/4` on the left and the
227/// Airy `3/4` on the right, so the seed is uniformly within a fraction of a root spacing.
228/// Newton on the raw `L_m^alpha`, `(m+1) L_{m+1} = (2m + alpha + 1 - x) L_m - (m + alpha) L_{m-1}`,
229/// scalar divisor, with `x L_n' = n L_n - (n + alpha) L_{n-1}`. Weight from Hildebrand's
230/// `w = Gamma(n + alpha + 1) / (n! x L_n'(x_k)^2)`, the Gamma ratio as the running product
231/// `Gamma(alpha + 1) prod (m + alpha)/m`. `L_{n-1}` at the largest root grows like
232/// `e^{x/2}`, which bounds the rule near `n = 170` in f64 and `n = 20` in f32.
233#[inline(always)]
234pub fn gauss_laguerre_impl<P, E, V>(k: V, alpha: V, n: u32) -> (V, V)
235where
236 P: Policy,
237 E: FloatElement,
238 V: FloatVector<Element = E> + SpecializedSpecialMath<E>,
239{
240 let nf = V::splat(E::from_int(n as i64));
241 let valid = valid_index(k, nf) & alpha.cmp_gt(-V::ONE);
242 let gamma_a1 = <V as SpecializedSpecialMath<E>>::tgamma::<P>(alpha + V::ONE);
243
244 if n == 1 {
245 // L_1 = 1 + alpha - x. The weight is the whole mass Gamma(alpha + 1).
246 return (valid.select(alpha + V::ONE, V::NAN), valid.select(gamma_a1, V::NAN));
247 }
248
249 let nu = nf.mul_adde(
250 V::splat(<E as FloatElement>::ConstInt::<4>::VALUE),
251 alpha.mul_adde(V::TWO, V::TWO),
252 );
253 let c = (k + V::splat(<E as FloatElement>::ConstRatio::<3, 4>::VALUE))
254 * (V::splat(<E as FloatElement>::ConstInt::<4>::VALUE) * V::PI / nu);
255 let mut psi = (c * V::splat(<E as FloatElement>::ConstInt::<6>::VALUE))
256 .cbrt_p::<LessPrecision<P>>()
257 .min(V::PI);
258 let mut i = 0;
259 while i < 5 {
260 let (s, co) = psi.sin_cos_p::<LessPrecision<P>>();
261 psi -= (psi - s - c) / (V::ONE - co).max(<V as FloatVector>::EPSILON);
262 i += 1;
263 }
264 // x = nu (1 + cos psi)/2, with 1 + cos psi as 2 cos^2(psi/2) for the small roots.
265 let half_cos = (psi * V::HALF).cos_p::<LessPrecision<P>>();
266 let x0 = nu * half_cos * half_cos;
267
268 let pair = |x: V| -> (V, V) {
269 let mut l_prev = V::ONE;
270 let mut l = alpha + V::ONE - x;
271 // `alpha - x` is loop-invariant, so only the `2m + 1` and `m` move.
272 let amx = alpha - x;
273 let mut m = 1u32;
274 while m < n {
275 let mf = V::splat(E::from_int(m as i64));
276 let c1 = V::splat(E::from_int((2 * m + 1) as i64));
277 let inv = V::splat(E::ONE / E::from_int((m + 1) as i64));
278 let next = (amx + c1).mul_sube(l, (alpha + mf) * l_prev) * inv;
279 l_prev = l;
280 l = next;
281 m += 1;
282 }
283 // x L_n' = n L_n - (n + alpha) L_{n-1}
284 let dl = (nf * l - (nf + alpha) * l_prev) / x;
285 (l, dl)
286 };
287
288 let (x, _, dl) = newton_x::<E, V, _>(x0, n, valid, pair);
289
290 // Gamma(n + alpha + 1)/n! = Gamma(alpha + 1) prod_{m=1}^{n} (m + alpha)/m.
291 let mut ratio = gamma_a1;
292 let mut m = 1u32;
293 while m <= n {
294 // `1/m` is a scalar, so `n` vector divisions become `n` vector multiplies.
295 let mf = V::splat(E::from_int(m as i64));
296 let inv_m = V::splat(E::ONE / E::from_int(m as i64));
297 ratio *= (mf + alpha) * inv_m;
298 m += 1;
299 }
300 let w = ratio / (x * dl * dl);
301
302 (valid.select(x, V::NAN), valid.select(w, V::NAN))
303}