Skip to main content

thermite_special/specialized/generic/
fresnel.rs

1//! The Fresnel integrals `C(x) = int_0^x cos(pi t^2/2) dt` and
2//! `S(x) = int_0^x sin(pi t^2/2) dt`.
3//!
4//! # Two regions
5//!
6//! Below a crossover (2.5265 at both precisions) the integrands are summed
7//! directly: `C(x)/x` and `S(x)/x^3` are both smooth functions of `w = x^4`, fitted
8//! as Chebyshev series and summed by Clenshaw. Above it the standard auxiliary
9//! form
10//!
11//! ```text
12//! C = 1/2 + f sin t - g cos t,   S = 1/2 - f cos t - g sin t,   t = pi x^2 / 2
13//! ```
14//!
15//! with `f = P(u)/(pi x)` and `g = Q(u)/(pi^2 x^3)` for `u = 1/(pi x^2)^2`, both
16//! `P` and `Q` plain Horner polynomials tending to 1.
17//!
18//! # Why Chebyshev below and Horner above
19//!
20//! Measurement, not symmetry. The small-argument fit in the monomial basis has an
21//! error amplification (`sum |c_k| |w|^k / |f|`) of 3482 at this crossover, and
22//! 1.26e5 if the crossover moves to 3. The same fit in the Chebyshev basis summed
23//! by Clenshaw sits at 5.8 and 7.0. Monomial Horner would cap the f64 kernel at
24//! about 4e-13. Clenshaw costs two operations per term against Horner's one and
25//! buys three orders of magnitude, which is also what lets the crossover sit far
26//! enough out for the auxiliaries to be well conditioned. Their own amplification
27//! is 1.02 there, so they keep Horner.
28//!
29//! # The phase
30//!
31//! `t = pi x^2 / 2` computed as `x*x*0.5` is worthless long before the function is:
32//! measured against a 45-digit oracle, `sin(pi*(x*x*0.5))` in binary64 is 5.2e-13
33//! off at `x = 123`, 9.8e-11 at 1234, **5.3e-6 at 98765**, and returns the wrong
34//! sign by `x ~ 1e9`. Since `C` and `S` are `1/2` plus a term of size `1/(pi x)`,
35//! that error lands directly on the result.
36//!
37//! [`phase_half_x2`] fixes it in about ten operations, and the fix is exact: `x*x`
38//! splits as `p + e` with `e` always representable, halving is exact, and each half
39//! reduces mod 2 exactly by Sterbenz. Both halves must be reduced _before_ being
40//! added. `|e/2|` reaches `ulp(x^2)/4`, which is 32 at `x = 1e9`, and adding that
41//! to an already-reduced `p/2` rounds the latter's low bits straight off. Measured
42//! 2.80 ulp (`C`) and 2.64 (`S`) in f64 over `x` from 1e-4 to 1e15, and 2.14 / 3.40
43//! in f32 out to 1e7. The naive phase alone is worth thousands of ulp there.
44//!
45//! Above `x = 1.147e16` (f64) / `2.136e7` (f32) the correction has fallen under
46//! half an ulp of `1/2` and both functions are exactly `1/2`. Below that `x^2`
47//! cannot overflow, so the phase needs no range guard.
48
49use thermite::{
50    element::FloatElement,
51    math::{
52        CoreMathWithPolicy as _, TranscendentalMathWithPolicy as _,
53        policy::{Policy, PrecisionPolicy},
54        specialized::SpecializedTranscendentalMath,
55    },
56    prelude::*,
57};
58
59use super::chebyshev::chebyshev_series;
60
61/// `x^2/2 mod 2`, the argument for `sincos_pi`, to full precision for every `x`
62/// whose square is finite.
63///
64/// See the module docs for why the two words are reduced separately. Below
65/// `Average` the residual is dropped entirely and this is the naive `x*x/2`,
66/// which is accurate only while `x^2` is exact.
67#[inline(always)]
68pub fn phase_half_x2<P, E, V>(x: V) -> V
69where
70    E: FloatElement,
71    V: FloatVector<Element = E>,
72    P: Policy,
73{
74    // v - 2*round(v/2). Exact: `round` is ties-to-even and the subtraction is
75    // Sterbenz-exact whenever |v - 2k| <= 1 <= |v|/2.
76    let rem2 = |v: V| -> V { (v * V::HALF).round().nmul_adde(V::TWO, v) };
77
78    let p = x * x;
79
80    if const { P::POLICY.precision.le(PrecisionPolicy::Medium) } {
81        return rem2(p * V::HALF);
82    }
83
84    // `mul_sub`, deliberately, not `mul_sube`. The residual has to be EXACT: it is
85    // multiplied by nothing and added to a quantity of size 1, so any error in it is
86    // an error in the phase, and only a fused (or correctly-rounded emulated)
87    // multiply-add produces it. On a backend with hardware FMA the two spellings emit
88    // the same instruction. On one without, this is the one place in the kernel that
89    // pays for the emulation, which buys the entire large-argument accuracy claim.
90    // A Veltkamp split would be the same result in about eight ordinary operations,
91    // but needs a per-format splitting constant, and the emulation is already
92    // correctly rounded here.
93    let e = x.mul_sub(x, p);
94
95    rem2(rem2(p * V::HALF) + rem2(e * V::HALF))
96}
97
98/// `(S(x), C(x))`, in SciPy's order.
99///
100/// `cheb_c` and `cheb_s` are Chebyshev coefficients for `C(x)/x` and `S(x)/x^3` in
101/// `w = x^4` mapped onto `[-1, 1]` by `w*map - 1`. `aux_p` and `aux_q` are ascending
102/// monomial coefficients in `u = 1/(pi x^2)^2`. `x0` is the crossover and `cutoff`
103/// the point above which both functions are `1/2`.
104#[inline(always)]
105pub fn fresnel_with<P, E, V, const NC: usize, const NS: usize, const NP: usize, const NQ: usize>(
106    x: V,
107    x0: E,
108    map: E,
109    cutoff: E,
110    cheb_c: &[E; NC],
111    cheb_s: &[E; NS],
112    aux_p: &[E; NP],
113    aux_q: &[E; NQ],
114) -> (V, V)
115where
116    E: FloatElement,
117    V: FloatVector<Element = E> + SpecializedTranscendentalMath<E>,
118    P: Policy,
119{
120    let ax = x.abs();
121    let q = ax * ax;
122    let is_small = ax.cmp_le(V::splat(x0));
123
124    if !const { P::POLICY.avoid_branching } && is_small.all() {
125        let (c, s) = fresnel_small::<P, E, V, NC, NS>(ax, q, map, cheb_c, cheb_s);
126        // Both functions are odd and both are positive for x > 0, so the sign is a
127        // copysign rather than a branch. It carries -0.0 through unchanged.
128        return (s.copysign(x), c.copysign(x));
129    }
130
131    // One division for the whole branch: r = 1/(pi x^2), and then
132    // f = P(u) r x = P(u)/(pi x) and g = Q(u) u x = Q(u)/(pi^2 x^3).
133    let r = V::FRAC_1_PI.approx_div_p::<P>(q);
134    let u = r * r;
135    let f = (u.poly_n_p::<P, NP>(aux_p) * r) * ax;
136    let g = (u.poly_n_p::<P, NQ>(aux_q) * u) * ax;
137
138    let (sin_t, cos_t) = phase_half_x2::<P, E, V>(ax).sincos_pi_p::<P>();
139
140    // Two chained FMAs apiece: 1/2 + f sin t - g cos t and 1/2 - f cos t - g sin t.
141    let mut c = g.nmul_adde(cos_t, f.mul_adde(sin_t, V::HALF));
142    let mut s = g.nmul_adde(sin_t, f.nmul_adde(cos_t, V::HALF));
143
144    if const { P::POLICY.avoid_branching } || thermite::unlikely(is_small.any()) {
145        let (cs, ss) = fresnel_small::<P, E, V, NC, NS>(ax, q, map, cheb_c, cheb_s);
146        c = is_small.select(cs, c);
147        s = is_small.select(ss, s);
148    }
149
150    // Past the cutoff `f` and `g` have fallen under half an ulp of 1/2. Naming it is
151    // a shortcut and also what keeps the infinities right, since `q` is
152    // +inf there and the auxiliary branch would otherwise take the trig of a NaN.
153    let done = ax.cmp_gt(V::splat(cutoff));
154    c = done.select(V::HALF, c);
155    s = done.select(V::HALF, s);
156
157    (s.copysign(x), c.copysign(x))
158}
159
160/// `(C, S)` from the Chebyshev series in `w = x^4`, for `|x|` under the crossover.
161#[inline(always)]
162fn fresnel_small<P, E, V, const NC: usize, const NS: usize>(
163    ax: V,
164    q: V,
165    map: E,
166    cheb_c: &[E; NC],
167    cheb_s: &[E; NS],
168) -> (V, V)
169where
170    E: FloatElement,
171    V: FloatVector<Element = E>,
172    P: Policy,
173{
174    // w = x^4, mapped onto [-1, 1] by a single FMA.
175    let w = q * q;
176    let t = w.mul_sube(V::splat(map), V::ONE);
177    let a = chebyshev_series::<P, E, V, 1, NC, true>(t, cheb_c);
178    let b = chebyshev_series::<P, E, V, 1, NS, true>(t, cheb_s);
179    (ax * a, (ax * q) * b)
180}
Last built: 2026-09-08 21:35:55 UTC