Skip to main content

thermite_special/specialized/generic/
hermite.rs

1use thermite::{
2    element::FloatElement,
3    math::{
4        TranscendentalMathWithPolicy as _,
5        policy::{Policy, PrecisionPolicy},
6        specialized::SpecializedTranscendentalMath,
7    },
8    prelude::*,
9};
10
11/// The two halves of `$\psi_0(x) = \pi^{-1/4} e^{-x^2/2}$`, as `(f, g)` with
12/// `f = e^{-x^2/4}` and `g = pi^{-1/4} f`, so `psi_0 = g * f`.
13///
14/// Splitting the Gaussian in half is what buys the Hermite functions their range. Run
15/// naively from `psi_0`, the recurrence carries `psi_k` values that are `O(1)` at most,
16/// but the *seed* underflows once `x^2/2` passes the exponent range (about 87 in
17/// binary32, 708 in binary64), and past a turning point `$x \approx \sqrt{2n+1}$` the true
18/// `psi_n(x)` there is `O(1)`, so degrees above roughly 87 / 708 return zero where they
19/// should not. Seeding with `g = pi^{-1/4} f` instead carries `psi_k / f`, which grows only
20/// like `e^{+x^2/4}`, and multiplying by `f` at the end restores `psi_k`. Underflow of `f`
21/// and overflow of `psi_k / f` now both sit at `x^2/4`, twice as far out: full accuracy at
22/// every degree for `|x|` under about 18.7 (binary32) or 53 (binary64), which covers every
23/// degree up to about 175 / 1400 everywhere on the line.
24///
25/// `x^2` is the whole error budget for a Gaussian: an absolute error `d` in the exponent
26/// is a relative error `d` in the value, and rounding `x*x` costs `x^2 eps`. Under a
27/// `Best`-or-better policy on true-FMA hardware the residual of the square is recovered
28/// exactly and applied to first order, taking the seed from `O(x^2 eps)` to `O(eps)`. As
29/// in `compound`, no correction is attempted without a fused multiply-add: the residual is
30/// only a residual if the product was single-rounded.
31///
32/// `EXACT_FMA` says whether the multiply-add is a single rounding. `V::HAS_NATIVE_FMA`
33/// cannot: a `Complex` or `Dual` over a hardware-FMA vector answers `True` while rounding
34/// more than once, so `x.mul_sube(x, q)` on it is the residual of nothing. `ps`/`pd` pass
35/// `true`; the `SpecializedSpecialMath` defaults pass `false`. A composite that knows its
36/// arithmetic is single-rounded may pass `true` from its own override.
37#[inline(always)]
38fn seed<P, E, V, const EXACT_FMA: bool>(x: V) -> (V, V)
39where
40    P: Policy,
41    E: FloatElement,
42    V: FloatVector<Element = E> + SpecializedTranscendentalMath<E>,
43{
44    let neg_quarter = V::splat(<E as FloatElement>::ConstRatio::<{ -1 }, 4>::VALUE);
45
46    let q = x * x;
47    let mut f = (q * neg_quarter).exp_p::<P>();
48
49    if const { EXACT_FMA && P::POLICY.precision.ge(PrecisionPolicy::Best) && matches!(V::HAS_NATIVE_FMA, thermite::tribool::True) } {
50        // e^{-(q + q_lo)/4} = f * (1 - q_lo/4) to first order, and q_lo/4 is at most an ulp of
51        // q/4 so the second-order term is below working precision. Guarded on a finite
52        // square: past overflow f is already the correct zero and the residual is NaN.
53        let q_lo = x.mul_sube(x, q);
54        f = q.is_finite().select((q_lo * neg_quarter).mul_adde(f, f), f);
55    }
56
57    (f, V::FRAC_1_SQRT_SQRT_PI * f)
58}
59
60/// The orthonormal Hermite function `$\psi_N(x) = (2^N N! \sqrt{\pi})^{-1/2} e^{-x^2/2} H_N(x)$`.
61///
62/// Three-term recurrence on the functions themselves,
63///
64/// ```text
65/// psi_{k+1} = sqrt(2/(k+1)) x psi_k - sqrt(k/(k+1)) psi_{k-1}
66/// ```
67///
68/// which keeps every intermediate `O(1)`: the polynomial's growth and the Gaussian's decay
69/// cancel *inside* each step instead of being formed separately and multiplied. Both
70/// square roots are literals under the unrolled loop, so the per-step cost is one FMA on
71/// the critical path plus one multiply beside it. See [`seed`] for the range and the
72/// precision of the Gaussian factor.
73#[inline(always)]
74pub fn hermite_function_n<P, E, V, const N: usize, const EXACT_FMA: bool>(x: V) -> V
75where
76    P: Policy,
77    E: FloatElement,
78    V: FloatVector<Element = E> + SpecializedTranscendentalMath<E>,
79{
80    let (f, g0) = seed::<P, E, V, EXACT_FMA>(x);
81
82    if const { N == 0 } {
83        return g0 * f;
84    }
85
86    // psi_1 = sqrt(2) x psi_0
87    let mut p0 = g0;
88    let mut p1 = (V::SQRT_2 * x) * g0;
89
90    let mut k = 1;
91    while k < N {
92        // psi_{k+1} = sqrt(2/(k+1)) x psi_k - sqrt(k/(k+1)) psi_{k-1}, the subtraction
93        // carried in the constant. p0 is two steps back, so its multiply is off the chain.
94        let ax = x * V::splat(a::<E>(k));
95        let next = ax.mul_adde(p1, p0 * V::splat(b::<E>(k)));
96        p0 = p1;
97        p1 = next;
98        k += 1;
99    }
100
101    p1 * f
102}
103
104/// The runtime-degree twin of [`hermite_function_n`].
105///
106/// The same seed, recurrence and final factor, with the degree as a value.
107/// `a_k` and `b_k` are computed per step rather than folded. Both are a correctly rounded
108/// division and square root either way, so the result agrees with the const form to the bit.
109#[inline(always)]
110pub fn hermite_function<P, E, V, const EXACT_FMA: bool>(x: V, n: u32) -> V
111where
112    P: Policy,
113    E: FloatElement,
114    V: FloatVector<Element = E> + SpecializedTranscendentalMath<E>,
115{
116    let (f, g0) = seed::<P, E, V, EXACT_FMA>(x);
117
118    if n == 0 {
119        return g0 * f;
120    }
121
122    let mut p0 = g0;
123    let mut p1 = (V::SQRT_2 * x) * g0;
124
125    let n = n as usize;
126    let mut k = 1;
127    while k < n {
128        let ax = x * V::splat(a::<E>(k));
129        let next = ax.mul_adde(p1, p0 * V::splat(b::<E>(k)));
130        p0 = p1;
131        p1 = next;
132        k += 1;
133    }
134
135    p1 * f
136}
137
138/// Runtime-length form of [`hermite_function_series`].
139///
140/// A genuine port of the recurrence rather than a fold over the const kernel: a series
141/// carries `k`-dependent state and does not partition the way the slice reductions in
142/// `thermite` do. Both forms must be edited together.
143///
144/// Same pre-scaling by `f`, same seed, same final factor. Read [`hermite_function_series`]
145/// for why the split is there. The runtime length costs the unrolling and turns `a_k`,
146/// `b_{k+1}` into per-step square roots of a ratio rather than folded literals, which is
147/// the expensive part here.
148///
149/// The empty series is `0`, where the const form refuses to compile.
150#[inline(always)]
151pub fn hermite_function_series_slice<P, E, V, const EXACT_FMA: bool>(x: V, coeffs: &[E]) -> V
152where
153    P: Policy,
154    E: FloatElement,
155    V: FloatVector<Element = E> + SpecializedTranscendentalMath<E>,
156{
157    let n = coeffs.len();
158
159    if n == 0 {
160        return V::ZERO;
161    }
162
163    let (f, g0) = seed::<P, E, V, EXACT_FMA>(x);
164
165    if n == 1 {
166        return (f * V::splat(coeffs[0])) * g0;
167    }
168
169    let sqrt2_x = V::SQRT_2 * x;
170    let fcn1 = f * V::splat(coeffs[n - 1]);
171
172    if n == 2 {
173        return sqrt2_x.mul_adde(fcn1, f * V::splat(coeffs[0])) * g0;
174    }
175
176    let mut y2 = fcn1;
177    let mut y1 = (x * V::splat(a::<E>(n - 2))).mul_adde(fcn1, f * V::splat(coeffs[n - 2]));
178
179    let mut k = n - 2;
180    while k > 1 {
181        k -= 1;
182        let ax = x * V::splat(a::<E>(k));
183        let yk = ax.mul_adde(y1, y2.mul_adde(V::splat(b::<E>(k + 1)), f * V::splat(coeffs[k])));
184        y2 = y1;
185        y1 = yk;
186    }
187
188    sqrt2_x.mul_adde(y1, y2.mul_adde(-V::FRAC_1_SQRT_2, f * V::splat(coeffs[0]))) * g0
189}
190
191/// Clenshaw summation of a Hermite-function series, `$\sum_{k=0}^{N-1} c_k \psi_k(x)$`.
192///
193/// Runs Clenshaw over `h_k = psi_k / psi_0`, whose recurrence is the same as `psi_k`'s, and
194/// multiplies by `psi_0` once at the end. `h_k` grows like `e^{+x^2/2}` where `psi_k` is
195/// `O(1)`, so to keep the same range as [`hermite_function`] the coefficients are pre-scaled
196/// by `f = e^{-x^2/4}` (Clenshaw is linear in them) and the final factor is only
197/// `pi^{-1/4} f`: the running values stay within `e^{+x^2/4}` and the outer factor within
198/// `e^{-x^2/4}`, the same split as the single-function kernel.
199///
200/// ```text
201/// y_k = f c_k + sqrt(2/(k+1)) x y_{k+1} - sqrt((k+1)/(k+2)) y_{k+2}     k = N-1 down to 1
202/// S   = pi^{-1/4} f * (f c_0 + sqrt(2) x y_1 - sqrt(1/2) y_2)
203/// ```
204#[inline(always)]
205pub fn hermite_function_series<P, E, V, const N: usize, const EXACT_FMA: bool>(x: V, coeffs: &[E; N]) -> V
206where
207    P: Policy,
208    E: FloatElement,
209    V: FloatVector<Element = E> + SpecializedTranscendentalMath<E>,
210{
211    const {
212        assert!(N >= 1, "hermite_function_series: N must be at least 1");
213    }
214
215    let (f, g0) = seed::<P, E, V, EXACT_FMA>(x);
216
217    // S = c_0 psi_0
218    if const { N == 1 } {
219        return (f * V::splat(coeffs[0])) * g0;
220    }
221
222    let sqrt2_x = V::SQRT_2 * x;
223    let fcn1 = f * V::splat(coeffs[N - 1]);
224
225    // S = psi_0 (c_0 + c_1 sqrt(2) x)
226    if const { N == 2 } {
227        return sqrt2_x.mul_adde(fcn1, f * V::splat(coeffs[0])) * g0;
228    }
229
230    // Hoist the top two steps (zero seeds):
231    //     k = N-1:  y = f c_{N-1}
232    //     k = N-2:  y = f c_{N-2} + a_{N-2} x (f c_{N-1})
233    let mut y2 = fcn1;
234    let mut y1 = (x * V::splat(a::<E>(N - 2))).mul_adde(fcn1, f * V::splat(coeffs[N - 2]));
235
236    // k = N-3 down to 1.
237    let mut k = N - 2;
238    while k > 1 {
239        k -= 1;
240        // y_k = a_k x y_{k+1} + (f c_k + b_{k+1} y_{k+2}), b negative.
241        let ax = x * V::splat(a::<E>(k));
242        let yk = ax.mul_adde(y1, y2.mul_adde(V::splat(b::<E>(k + 1)), f * V::splat(coeffs[k])));
243        y2 = y1;
244        y1 = yk;
245    }
246
247    // S = g0 * (sqrt(2) x y_1 + (f c_0 - sqrt(1/2) y_2)); b_1 = -sqrt(1/2) = -1/sqrt(2).
248    sqrt2_x.mul_adde(y1, y2.mul_adde(-V::FRAC_1_SQRT_2, f * V::splat(coeffs[0]))) * g0
249}
250
251/// `sqrt(2/(k+1))`, the coefficient of `x psi_k` in the step to `psi_{k+1}`.
252#[inline(always)]
253fn a<E: FloatElement>(k: usize) -> E {
254    FloatElement::sqrt(E::from_ratio(2, (k + 1) as thermite::LargeInt))
255}
256
257/// `-sqrt(k/(k+1))`, the coefficient of `psi_{k-1}` in the step to `psi_{k+1}`, negated.
258#[inline(always)]
259fn b<E: FloatElement>(k: usize) -> E {
260    -FloatElement::sqrt(E::from_ratio(k as thermite::LargeInt, (k + 1) as thermite::LargeInt))
261}
Last built: 2026-09-08 21:35:55 UTC