Skip to main content

thermite_special/specialized/
ps.rs

1use thermite::{
2    math::{
3        TranscendentalMathWithPolicy,
4        policy::{
5            DenormalBehavior, PrecisionPolicy,
6            policies::{CheckOverflow, ExtraPrecision, MediumPrecision, WorstPrecision},
7        },
8        specialized::SpecializedTranscendentalMath,
9        specialized::reference::{is_reference, map1, map1x2, map2},
10    },
11    prelude::*,
12    register::NativeCapability,
13};
14
15use crate::RealSpecialMathWithPolicy as _;
16
17use super::*;
18
19impl<V: FloatVectorWithBits<Element = f32>> SpecializedSpecialMath<f32> for V
20where
21    V: TranscendentalMathWithPolicy<Element = f32>,
22    V: SpecializedTranscendentalMath<f32>,
23    // Pins the projection so `V`'s real-special methods (whose impl requires
24    // `Primal = V`) resolve. A type parameter's `Primal` will not normalize
25    // through the blanket impl on its own.
26    V: thermite::math::PrimalProjection<Primal = V>,
27    V: thermite::math::RealMathWithPolicy<Element = f32>,
28{
29    /// Through the strict `FloatVectorWithBits::two_sum`, so it survives `algebraic-scalar`.
30    #[inline(always)]
31    fn exp_two_sum(a: Self, b: Self) -> (Self, Self) {
32        a.two_sum(b)
33    }
34
35    // `EXACT_FMA = true`: a real vector's multiply-add is a single rounding, so the seed's
36    // `x*x` residual is real. See `generic::hermite::seed`.
37    #[inline(always)]
38    fn hermite_function_n<P: Policy, const N: usize>(mut x: Self) -> Self {
39        #[cfg(not(target_arch = "spirv"))]
40        if let Some(new_x) = thermite::math::specialized::FlushDenormals::<P>::flush_denormals([x]) {
41            x = new_x[0];
42        }
43
44        generic::hermite::hermite_function_n::<P, _, _, N, true>(x)
45    }
46
47    #[inline(always)]
48    fn hermite_function<P: Policy>(mut x: Self, n: u32) -> Self {
49        #[cfg(not(target_arch = "spirv"))]
50        if let Some(new_x) = thermite::math::specialized::FlushDenormals::<P>::flush_denormals([x]) {
51            x = new_x[0];
52        }
53
54        generic::hermite::hermite_function::<P, _, _, true>(x, n)
55    }
56
57    #[inline(always)]
58    fn hermite_function_series_n<P: Policy, const N: usize>(self, coeffs: &[Self::Element; N]) -> Self {
59        generic::hermite::hermite_function_series::<P, _, _, N, true>(self, coeffs)
60    }
61
62    #[inline(always)]
63    fn hermite_function_series<P: Policy>(self, coeffs: &[Self::Element]) -> Self {
64        generic::hermite::hermite_function_series_slice::<P, _, _, true>(self, coeffs)
65    }
66
67    #[inline(always)]
68    fn zetac<P: Policy>(self) -> Self {
69        generic::zeta::zeta_impl::<P, _, _, true>(self)
70    }
71
72    #[inline(always)]
73    fn polylog<P: Policy>(self, order: crate::PolylogOrder<f32, i32>) -> Self {
74        generic::polylog::polylog_impl::<P, f32, Self>(self, order)
75    }
76
77    #[inline(always)]
78    fn zeta<P: Policy>(self) -> Self {
79        generic::zeta::zeta_impl::<P, _, _, false>(self)
80    }
81
82    #[inline(always)]
83    fn zeta_with_deriv<P: Policy, const ZETAC: bool>(self) -> (Self, Self) {
84        generic::zeta::zeta_core::<P, _, _, ZETAC, true>(self)
85    }
86
87    #[inline(always)]
88    fn bessel_i<P: Policy, const N: i32>(self) -> Self {
89        bessel_i_dispatch::<P, Self, N, false>(self)
90    }
91
92    #[inline(always)]
93    fn bessel_i_scaled<P: Policy, const N: i32>(self) -> Self {
94        bessel_i_dispatch::<P, Self, N, true>(self)
95    }
96
97    #[inline(always)]
98    fn bessel_k<P: Policy, const N: i32>(self) -> Self {
99        bessel_k_dispatch::<P, Self, N, false>(self)
100    }
101
102    #[inline(always)]
103    fn bessel_k_scaled<P: Policy, const N: i32>(self) -> Self {
104        bessel_k_dispatch::<P, Self, N, true>(self)
105    }
106
107    #[inline(always)]
108    fn bessel_j<P: Policy, const N: i32>(self) -> Self {
109        use crate::tables::bessel::jy::{BESSEL_J0_F32, BESSEL_J1_F32};
110        // `Reference` is contractually bit-identical to libm, lane by lane. Unlike most of
111        // this crate, libm actually has these (the C/POSIX XSI set) at every order, so the
112        // arm exists. `I`/`K` have no libm counterpart and therefore no reference arm.
113        if const { is_reference::<P>() } {
114            let v = if const { N == 0 } {
115                map1(self, libm::j0f)
116            } else if const { N.unsigned_abs() == 1 } {
117                map1(self, libm::j1f)
118            } else {
119                map1(self, |v| libm::jnf(N.abs(), v))
120            };
121            // A sign flip is exact, so reflecting libm's own value keeps the tier's
122            // bit-identity promise rather than trading it for a second algorithm.
123            return if const { bessel_reflect_negates(N) } { -v } else { v };
124        }
125        let v = if const { N == 0 } {
126            generic::bessel::jy::bessel_j0_impl::<P, f32, _, _, _, _>(self, &BESSEL_J0_F32)
127        } else if const { N.unsigned_abs() == 1 } {
128            generic::bessel::jy::bessel_j1_impl::<P, f32, _, _, _, _>(self, &BESSEL_J1_F32)
129        } else {
130            generic::bessel::jy::bessel_jn_pair_impl::<P, f32, _, _, _, _, _, _, _, N>(
131                self,
132                &BESSEL_J0_F32,
133                &BESSEL_J1_F32,
134            )
135            .1
136        };
137        // `J_{-n} = (-1)^n J_n`. Every arm above evaluated at `|N|`.
138        if const { bessel_reflect_negates(N) } { -v } else { v }
139    }
140
141    #[inline(always)]
142    fn bessel_y<P: Policy, const N: i32>(self) -> Self {
143        use crate::tables::bessel::jy::{BESSEL_J0_F32, BESSEL_J1_F32, BESSEL_Y0_F32, BESSEL_Y1_F32};
144        if const { is_reference::<P>() } {
145            let v = if const { N == 0 } {
146                map1(self, libm::y0f)
147            } else if const { N.unsigned_abs() == 1 } {
148                map1(self, libm::y1f)
149            } else {
150                map1(self, |v| libm::ynf(N.abs(), v))
151            };
152            return if const { bessel_reflect_negates(N) } { -v } else { v };
153        }
154        let v = if const { N.unsigned_abs() >= 2 } {
155            // Y is the dominant solution, so upward recurrence is stable and costs exactly
156            // |N|-1 steps (no trip count question at all, unlike J).
157            let y0 = generic::bessel::jy::bessel_y_impl::<P, f32, _, _, _, _, _, _, _, _, false>(
158                self,
159                &BESSEL_Y0_F32,
160                &BESSEL_J0_F32,
161            );
162            let y1 = generic::bessel::jy::bessel_y_impl::<P, f32, _, _, _, _, _, _, _, _, true>(
163                self,
164                &BESSEL_Y1_F32,
165                &BESSEL_J1_F32,
166            );
167            generic::bessel::jy::bessel_yn_recur::<f32, _, N>(self, y0, y1).1
168        } else if const { N == 0 } {
169            generic::bessel::jy::bessel_y_impl::<P, f32, _, _, _, _, _, _, _, _, false>(
170                self,
171                &BESSEL_Y0_F32,
172                &BESSEL_J0_F32,
173            )
174        } else {
175            generic::bessel::jy::bessel_y_impl::<P, f32, _, _, _, _, _, _, _, _, true>(
176                self,
177                &BESSEL_Y1_F32,
178                &BESSEL_J1_F32,
179            )
180        };
181        // `Y_{-n} = (-1)^n Y_n`, the same reflection `J` gets.
182        if const { bessel_reflect_negates(N) } { -v } else { v }
183    }
184
185    #[inline(always)]
186    fn bessel_i_with_deriv<P: Policy, const N: i32, const SCALED: bool>(self) -> (Self, Self) {
187        bessel_i_deriv_dispatch::<P, Self, N, SCALED>(self)
188    }
189
190    #[inline(always)]
191    fn bessel_k_with_deriv<P: Policy, const N: i32, const SCALED: bool>(self) -> (Self, Self) {
192        bessel_k_deriv_dispatch::<P, Self, N, SCALED>(self)
193    }
194
195    #[inline(always)]
196    fn bessel_iv<P: Policy, const SCALED: bool>(self, order: crate::BesselOrder<Self, Self::Signed>) -> Self {
197        // Half-integer order is elementary: hyperbolic seeds and the same two recurrence
198        // directions the integer kernel uses. See `generic::bessel_half`.
199        let order = order.simplify();
200        if let crate::BesselOrder::HalfInteger(k) = order {
201            return generic::bessel::half::bessel_ik_half::<P, f32, _, SCALED>(
202                Self::from_signed_integer(k) * Self::HALF,
203                self,
204                crate::tables::bessel::BESSEL_I0_F32.far_threshold,
205            )
206            .0;
207        }
208        // `Thirds` and `Real` take the table-free arms in `generic::bessel_ik_nu`.
209        let Some(n) = order.as_integer() else {
210            return generic::bessel::ik_real::bessel_ik_real::<P, f32, _, _, 11, 11, SCALED, true>(
211                order.to_real(),
212                self,
213                &crate::tables::lgamma1p::LGAMMA1P_F32,
214                crate::tables::bessel::BESSEL_I0_F32.far_threshold,
215            )
216            .0;
217        };
218        // `I_{-n} = I_n` for integer `n`, so only the magnitude matters and no sign is owed
219        // afterwards. `J`/`Y` below are the ones that reflect.
220        let nf = Self::from_signed_integer(n).abs();
221        let v = generic::bessel::ik::bessel_iv_impl::<P, f32, _, _, _, _, SCALED>(
222            self,
223            nf,
224            &crate::tables::bessel::BESSEL_I0_F32,
225        );
226        // Orders 0 and 1 have closed forms, and the ratio ladder is measurably worse at them:
227        // it reaches order 1 as `I_0 * r_1`, paying the continued fraction for a value the
228        // table gives directly. Measured 5.49 ULP against 3.04 before this select was added.
229        let i1 =
230            generic::bessel::ik::bessel_i1_impl::<P, _, _, _, _, SCALED>(self, &crate::tables::bessel::BESSEL_I1_F32);
231        nf.cmp_le(Self::ONE).select(
232            nf.cmp_le(Self::ZERO).select(
233                generic::bessel::ik::bessel_i0_impl::<P, _, _, _, _, SCALED>(
234                    self,
235                    &crate::tables::bessel::BESSEL_I0_F32,
236                ),
237                i1,
238            ),
239            v,
240        )
241    }
242
243    #[inline(always)]
244    fn bessel_kv<P: Policy, const SCALED: bool>(self, order: crate::BesselOrder<Self, Self::Signed>) -> Self {
245        // Half-integer order is elementary. See `generic::bessel_half`.
246        let order = order.simplify();
247        if let crate::BesselOrder::HalfInteger(k) = order {
248            return generic::bessel::half::bessel_ik_half::<P, f32, _, SCALED>(
249                Self::from_signed_integer(k) * Self::HALF,
250                self,
251                crate::tables::bessel::BESSEL_I0_F32.far_threshold,
252            )
253            .1;
254        }
255        // `Thirds` and `Real` take the table-free arms in `generic::bessel_ik_nu`.
256        let Some(n) = order.as_integer() else {
257            // `NEED_I = false`: this entry wants only `K`, which is the cheap half. Skipping
258            // `I` skips the continued fraction and the asymptotic series both.
259            return generic::bessel::ik_real::bessel_ik_real::<P, f32, _, _, 11, 11, SCALED, false>(
260                order.to_real(),
261                self,
262                &crate::tables::lgamma1p::LGAMMA1P_F32,
263                crate::tables::bessel::BESSEL_I0_F32.far_threshold,
264            )
265            .1;
266        };
267        // `K_{-n} = K_n`, as with `I`.
268        let nf = Self::from_signed_integer(n).abs();
269        generic::bessel::ik::bessel_kv_impl::<P, f32, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, SCALED>(
270            self,
271            nf,
272            &crate::tables::bessel::BESSEL_K0_F32,
273            &crate::tables::bessel::BESSEL_K1_F32,
274            &crate::tables::bessel::BESSEL_I0_F32,
275            &crate::tables::bessel::BESSEL_I1_F32,
276        )
277    }
278
279    #[inline(always)]
280    fn bessel_jv<P: Policy>(self, order: crate::BesselOrder<Self, Self::Signed>) -> Self {
281        // Half-integer order is elementary. See `generic::bessel_half`. `simplify` has
282        // already turned an even numerator into `Integer`, so anything still tagged
283        // `HalfInteger` here is a genuine half-odd order.
284        let order = order.simplify();
285        if let crate::BesselOrder::HalfInteger(k) = order {
286            return generic::bessel::half::bessel_jy_half::<P, f32, _>(Self::from_signed_integer(k) * Self::HALF, self)
287                .0;
288        }
289        // `Thirds` and `Real` take the table-free arms in `generic::bessel_nu`, which cover
290        // the whole axis at any real order. Thirds are not specialised beyond that, and
291        // deliberately: see the module docs there.
292        let Some(n) = order.as_integer() else {
293            let nu = order.to_real();
294            return generic::bessel::jy_real::bessel_jy_real::<P, f32, _, 5, 11, 11, 13, 2>(
295                nu,
296                self,
297                Self::ZERO,
298                &crate::tables::lgamma1p::LGAMMA1P_F32,
299            )
300            .0;
301        };
302        // The const form routes `Reference` to libm, and so must this one, or the tier silently
303        // stops meaning "bit-identical to libm" as soon as the order moves into a register.
304        if const { is_reference::<P>() } {
305            let mut out = self;
306            let mut i = 0;
307            while i < Self::LANES {
308                // Reflected here rather than handed to libm signed, so the tier means the
309                // same thing at negative order as the const form does.
310                let k = n.extractv(i);
311                let r = libm::jnf(k.unsigned_abs() as i32, self.extractv(i));
312                out = out.insertv(i, if k < 0 && k % 2 != 0 { -r } else { r });
313                i += 1;
314            }
315            return out;
316        }
317        let (nf, flip) = bessel_reflect_v(Self::from_signed_integer(n));
318        generic::bessel::jy::bessel_jv_impl::<P, f32, _, _, _, _, _, _, _>(
319            self,
320            nf,
321            &crate::tables::bessel::jy::BESSEL_J0_F32,
322            &crate::tables::bessel::jy::BESSEL_J1_F32,
323        )
324        .neg_c(flip)
325    }
326
327    #[inline(always)]
328    fn bessel_yv<P: Policy>(self, order: crate::BesselOrder<Self, Self::Signed>) -> Self {
329        // Half-integer order is elementary. See `generic::bessel_half`.
330        let order = order.simplify();
331        if let crate::BesselOrder::HalfInteger(k) = order {
332            return generic::bessel::half::bessel_jy_half::<P, f32, _>(Self::from_signed_integer(k) * Self::HALF, self)
333                .1;
334        }
335        let Some(n) = order.as_integer() else {
336            let nu = order.to_real();
337            return generic::bessel::jy_real::bessel_jy_real::<P, f32, _, 5, 11, 11, 13, 2>(
338                nu,
339                self,
340                Self::ZERO,
341                &crate::tables::lgamma1p::LGAMMA1P_F32,
342            )
343            .1;
344        };
345        if const { is_reference::<P>() } {
346            let mut out = self;
347            let mut i = 0;
348            while i < Self::LANES {
349                let k = n.extractv(i);
350                let r = libm::ynf(k.unsigned_abs() as i32, self.extractv(i));
351                out = out.insertv(i, if k < 0 && k % 2 != 0 { -r } else { r });
352                i += 1;
353            }
354            return out;
355        }
356        let (nf, flip) = bessel_reflect_v(Self::from_signed_integer(n));
357        generic::bessel::jy::bessel_yv_impl::<P, f32, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _>(
358            self,
359            nf,
360            &crate::tables::bessel::jy::BESSEL_Y0_F32,
361            &crate::tables::bessel::jy::BESSEL_Y1_F32,
362            &crate::tables::bessel::jy::BESSEL_J0_F32,
363            &crate::tables::bessel::jy::BESSEL_J1_F32,
364        )
365        .neg_c(flip)
366    }
367
368    impl_sph_bessel_entries!(f32, crate::tables::bessel::BESSEL_I0_F32);
369
370    impl_airy_entries!(
371        f32,
372        5,
373        11,
374        11,
375        13,
376        2,
377        &crate::tables::lgamma1p::LGAMMA1P_F32,
378        &crate::tables::bessel::airy::AIRY_ZERO_F32,
379        crate::tables::bessel::BESSEL_I0_F32
380    );
381
382    #[inline(always)]
383    fn bessel_j_with_deriv<P: Policy, const N: i32>(self) -> (Self, Self) {
384        // Order N-1 comes from the recurrence, which walks through it either way: forward
385        // passes it on the last step, downward keeps the shorter product.
386        let (prev, v) = if const { N == 0 } {
387            // J_{-1} = -J_1, so the identity still holds and the N/x term vanishes.
388            (-Self::bessel_j::<P, 1>(self), Self::bessel_j::<P, 0>(self))
389        } else if const { N.unsigned_abs() == 1 } {
390            (Self::bessel_j::<P, 0>(self), Self::bessel_j::<P, 1>(self))
391        } else {
392            generic::bessel::jy::bessel_jn_pair_impl::<P, f32, _, _, _, _, _, _, _, N>(
393                self,
394                &crate::tables::bessel::jy::BESSEL_J0_F32,
395                &crate::tables::bessel::jy::BESSEL_J1_F32,
396            )
397        };
398        let d = if const { N == 0 } {
399            prev
400        } else {
401            prev - v * (Self::splat(N.unsigned_abs() as f32) / self)
402        };
403        // The pair above is at `|N|`. Reflecting a negative order scales the function by a
404        // constant `(-1)^n`, so differentiating both sides carries the identical sign.
405        if const { bessel_reflect_negates(N) } {
406            (-v, -d)
407        } else {
408            (v, d)
409        }
410    }
411
412    #[inline(always)]
413    fn bessel_y_with_deriv<P: Policy, const N: i32>(self) -> (Self, Self) {
414        let (prev, v) = if const { N == 0 } {
415            (-Self::bessel_y::<P, 1>(self), Self::bessel_y::<P, 0>(self))
416        } else if const { N.unsigned_abs() == 1 } {
417            (Self::bessel_y::<P, 0>(self), Self::bessel_y::<P, 1>(self))
418        } else {
419            let y0 = generic::bessel::jy::bessel_y_impl::<P, f32, _, _, _, _, _, _, _, _, false>(
420                self,
421                &crate::tables::bessel::jy::BESSEL_Y0_F32,
422                &crate::tables::bessel::jy::BESSEL_J0_F32,
423            );
424            let y1 = generic::bessel::jy::bessel_y_impl::<P, f32, _, _, _, _, _, _, _, _, true>(
425                self,
426                &crate::tables::bessel::jy::BESSEL_Y1_F32,
427                &crate::tables::bessel::jy::BESSEL_J1_F32,
428            );
429            generic::bessel::jy::bessel_yn_recur::<f32, _, N>(self, y0, y1)
430        };
431        let d = if const { N == 0 } {
432            prev
433        } else {
434            prev - v * (Self::splat(N.unsigned_abs() as f32) / self)
435        };
436        // The pair above is at `|N|`. Reflecting a negative order scales the function by a
437        // constant `(-1)^n`, so differentiating both sides carries the identical sign.
438        if const { bessel_reflect_negates(N) } {
439            (-v, -d)
440        } else {
441            (v, d)
442        }
443    }
444
445    type ExpIntDetails = Self;
446    const LAGUERRE_PRODUCT_SEED_CAP: i32 = 29;
447
448    #[inline(always)]
449    fn chebyshev_n<P: Policy, const K: usize, const N: usize>(self, coeffs: &[f32; N]) -> Self {
450        // See the trait default: the kernel reads `N = 0` as "runtime length", so the
451        // empty-series rejection belongs to the entry point.
452        const {
453            assert!(N >= 1, "chebyshev_n: N must be at least 1");
454        }
455
456        // Real vectors have copysign and a real nearest endpoint, so the Reinsch form is
457        // available, but the kernel still gates it on the policy asking for `Best` or better.
458        generic::chebyshev::chebyshev_series::<P, _, _, K, N, true>(self, coeffs)
459    }
460
461    #[inline(always)]
462    fn chebyshev<P: Policy, const K: usize>(self, coeffs: &[f32]) -> Self {
463        // Reinsch available here too, on the same terms. See `chebyshev_n`.
464        generic::chebyshev::chebyshev_series::<P, _, _, K, 0, true>(self, coeffs)
465    }
466
467    // TEMP(bessel_j): disabled until orders beyond J_0 exist. See the note in lib.rs.
468    // The `bessel_j0`/`bessel_j0_pqzero` machinery this called is kept below under the
469    // same marker.
470    //#[inline(always)]
471    //fn bessel_j<P: Policy, const N: i32>(self) -> Self {
472    //    match N {
473    //        0 => bessel_j0::<Self, P>(self),
474    //        _ => todo!(),
475    //    }
476    //}
477
478    #[inline(always)]
479    fn lambert_w<P: Policy>(self) -> (Self, Self) {
480        // Computes both W_0(x) and W_{-1}(x) simultaneously.
481        //
482        // Lambert W_0(x): principal branch, defined for x >= -1/e, returns values >= -1.
483        // Lambert W_{-1}(x): secondary real branch, defined for -1/e <= x < 0, returns values <= -1.
484        // Both satisfy w*e^w = x.
485        //
486        // Uses Halley's method with piecewise initial approximations, interleaving
487        // iterations for both branches to maximize instruction-level parallelism.
488        //
489        // Halley's iteration for w*exp(w) = x:
490        //   ew = exp(w), f = w*ew - x, wp1 = w + 1
491        //   Denominator rewritten to avoid an extra division:
492        //     d = 2*wp1^2*ew - (w+2)*f
493        //   w' = w - 2*wp1*f / d
494
495        // For initial guess and first Halley iterations, use fast and loose precision
496        type Approx<P> = WorstPrecision<CheckOverflow<P, false>>;
497
498        let x = self;
499
500        // --- Initial approximation (piecewise) ---
501        //
502        // Branch-point region (x near -1/e): damped Puiseux series.
503        //
504        // W has a square-root singularity at x = -1/e (double root of w*e^w - x at w = -1),
505        // so Halley degenerates to linear convergence without a sqrt-based initial guess.
506        //
507        // The raw Puiseux series is W ≈ -1 ± (p - p^2/3 + 11p^3/72) where p = sqrt(2(ex+1)),
508        // with + for W_0 and - for W_{-1}. This converges well near -1/e but diverges further
509        // out. We damp it with a denominator that grows with distance from -1/e:
510        //
511        //   w_branch = -1 ± p*(1 + p*(-1/3 + p*11/72)) / (1 + K*p_0*p)
512        //
513        // where p_0 = ex+1, and K = 1/(C * e^(3/2) * sqrt(2)) with C ≈ 1.2144578338 found by
514        // minimizing the integrated backward error |w*e^w - x| over [-1/e, 0] in Desmos.
515        // The denominator arises from (x + 1/e)^1.5 / C = (p_0/e)^1.5 / C = p_0*p / (C*e^(3/2)*sqrt(2)).
516
517        let p0 = x.mul_adde(Self::E, Self::ONE); // ex + 1
518        let p = (p0 + p0).sqrt(); // sqrt(2(ex+1))
519
520        // p*(1 + p*(-1/3 + p*11/72))
521        let puiseux_numer = p * p.mul_adde(
522            p.mul_adde(
523                thermite::const_splat!(f32: 11.0 / 72.0),
524                thermite::const_splat!(f32: -1.0 / 3.0),
525            ),
526            Self::ONE,
527        );
528
529        // 1 + K*p_0*p
530        let puiseux_denom = p0.mul_adde(p * thermite::const_splat!(f32: 0.12991546098765432), Self::ONE);
531
532        let puiseux = puiseux_numer / puiseux_denom;
533
534        // W_0 branch: -1 + series, W_{-1} branch: -1 - series
535        let w0_branch = puiseux + Self::NEG_ONE;
536        let wm1_branch = Self::NEG_ONE - puiseux;
537
538        // W_0 middle region: ex/(2+ex)
539        let ex = x * Self::E;
540        let w0_mid = ex / (Self::TWO + ex);
541
542        // Shared ln for asymptotic regions
543        let lnx = x.abs().ln_p::<Approx<P>>();
544
545        // W_0 asymptotic (x > e): L_1 - L_2 + L_2/L_1 where L_1 = ln(x), L_2 = ln(L_1).
546        // The L_2/L_1 correction is 0 at x = e (since L_2 = ln(1) = 0), so it doesn't
547        // overshoot near the transition, but closes the gap at large x.
548        let l2 = lnx.ln_p::<Approx<P>>();
549
550        let w0_asymptotic = if const { P::POLICY.precision.le(PrecisionPolicy::Average) && V::HAS_APPROX_RCP } {
551            l2.mul_adde(lnx.approx_reciprocal_p::<Approx<P>>(), lnx - l2)
552        } else {
553            (lnx - l2) + (l2 / lnx)
554        };
555
556        // W_{-1} asymptotic (x near 0^-): L_1 - L_2 where L_1 = ln(-x), L_2 = ln(-L_1)
557        // lnx = ln(|x|) = ln(-x) since x < 0; this is negative for small |x|.
558        // -lnx is positive, so (-lnx).ln() = ln(-ln(-x)) = L_2.
559        let wm1_asymptotic = lnx - (-lnx).ln_p::<Approx<P>>();
560
561        // Select initial guesses
562        let near_branch = x.cmp_lt(thermite::const_splat!(f32: -0.1));
563        let large = x.cmp_gt(Self::E);
564
565        let mut w0 = near_branch.select(w0_branch, large.select(w0_asymptotic, w0_mid));
566
567        let near_branch_m1 = x.cmp_lt(thermite::const_splat!(f32: -0.25));
568        let mut wm1 = near_branch_m1.select(wm1_branch, wm1_asymptotic);
569
570        // --- Interleaved Halley iterations ---
571        #[inline(always)]
572        fn halley_step<P: Policy, W>(w: W, x: W) -> W
573        where
574            W: FloatVectorWithBits<Element = f32> + SpecializedTranscendentalMath<f32>,
575        {
576            // Use exp(-w) to avoid overflow/underflow in e^w for extreme w.
577            // g = w - x*e^{-w} = f*e^{-w}, d = (w^2+2w+2) + (w+2)*x*e^{-w}
578            // g and d are both single FMAs off enw, independent of each other.
579            let enw = (-w).exp_p::<P>();
580
581            let wp1 = w + W::ONE;
582            let q = wp1.mul_adde(wp1, W::ONE); // (w+1)^2 + 1 = w^2 + 2w + 2
583            let wp2h_x = wp1.mul_adde(x, x); // (w+2)*x - no exp dependency
584            let g = x.nmul_adde(enw, w); // w - x*e^{-w}
585            let d = wp2h_x.mul_adde(enw, q); // (w+2)*x*e^{-w} + (w^2+2w+2)
586            (wp1 + wp1).nmul_adde(g / d, w)
587        }
588
589        #[rustfmt::skip]
590        let num_iters = if const { P::POLICY.precision.ge(PrecisionPolicy::Best) } { 2 } else { 1 };
591
592        // warmup iteration with the looser precision to get close enough
593        // for the main iterations to converge in the target precision
594        w0 = halley_step::<Approx<P>, Self>(w0, x);
595        wm1 = halley_step::<Approx<P>, Self>(wm1, x);
596
597        let mut _iter = 0usize;
598        while _iter < num_iters {
599            _iter += 1;
600            // don't need to check overflow within these since it should be well-defined for
601            // all intermediate values, and the final check will catch any issues.
602            w0 = halley_step::<CheckOverflow<P, false>, Self>(w0, x);
603            wm1 = halley_step::<CheckOverflow<P, false>, Self>(wm1, x);
604        }
605
606        // --- Edge cases ---
607        if const { P::POLICY.precision.ge(PrecisionPolicy::Average) } {
608            let x_is_zero = x.is_zero();
609
610            // At x = -1/e, both W_0 and W_{-1} = -1
611            w0 = x.cmp_eq(Self::FRAC_NEG_1_E).select(Self::NEG_ONE, w0);
612            // Honestly the approximation handles W_0(0) = 0 pretty well,
613            // but just in case, explicitly set it to the correct value.
614            w0 = w0.nz(x_is_zero); // W_0(0) = 0
615
616            wm1 = x.cmp_eq(Self::FRAC_NEG_1_E).select(Self::NEG_ONE, wm1);
617            wm1 = x_is_zero.select(Self::NEG_INFINITY, wm1); // W_{-1}(0) = -inf
618        }
619
620        if const { matches!(P::POLICY.denormal_behavior, DenormalBehavior::Preserve) } {
621            // for subnormal inputs, W_0(x) ≈ x
622            w0 = x.is_subnormal().select(x, w0);
623
624            // NOTE: Somehow wm1 handles denormals fine on its own,
625            // at least to the accuracy of the reference crate,
626            // so we don't actually need this.
627            //
628            // wm1 = is_subnormal.select(wm1_asymptotic, wm1);
629        }
630
631        if const { P::POLICY.check_overflow } {
632            let in_domain = x.cmp_ge(Self::FRAC_NEG_1_E);
633
634            // W_0 is undefined for x < -1/e, +inf -> +inf
635            w0 = in_domain.select(w0, Self::NAN);
636            w0 = x.cmp_eq(Self::INFINITY).select(Self::INFINITY, w0);
637
638            // W_{-1} is only defined for -1/e <= x < 0
639            wm1 = in_domain.select(wm1, Self::NAN);
640            wm1 = x.cmp_gt(Self::ZERO).select(Self::NAN, wm1);
641        }
642
643        (w0, wm1)
644    }
645
646    #[inline(always)]
647    #[allow(const_item_mutation)]
648    fn erf<P: Policy>(self) -> Self {
649        if const { is_reference::<P>() } {
650            return map1(self, libm::erff);
651        }
652
653        erf_f_internal::<Self, P, false, false>(self, &mut V::EMPTY)
654    }
655
656    #[inline(always)]
657    #[allow(const_item_mutation)]
658    fn erfc<P: Policy>(self) -> Self {
659        if const { is_reference::<P>() } {
660            return map1(self, libm::erfcf);
661        }
662
663        erf_f_internal::<Self, P, true, false>(self, &mut V::EMPTY)
664    }
665
666    #[inline(always)]
667    fn erfcx<P: Policy>(self) -> Self {
668        // No libm counterpart at any tier. `erfcx` is not in the C library, and
669        // `exp(x*x) * erfcf(x)` is exactly the overflowing form this replaces.
670        super::generic::erfcx::erfcx_internal::<Self, f32, P>(self)
671    }
672
673    #[inline(always)]
674    fn logistic_sigmoid<P: Policy>(self) -> Self {
675        if const { is_reference::<P>() } {
676            return map1(self, |x| (1.0 / (1.0 + libm::exp(-(x as f64)))) as f32);
677        }
678
679        if const { P::POLICY.precision.gt(PrecisionPolicy::Average) } {
680            let is_pos = self.is_positive();
681            let x = self.neg_c(is_pos); // conditionally negate if positive
682            let e = x.exp_p::<P>();
683
684            let n = is_pos.select(Self::ONE, e);
685            let d = Self::ONE + e;
686
687            return n / d;
688        }
689
690        (Self::ONE + (-self).exp_p::<P>()).approx_reciprocal_p::<ExtraPrecision<P>>()
691    }
692
693    // This ended up being a bust, but I'll keep it around anyway.
694    // #[inline(always)]
695    // fn sigmoid<P: Policy>(self) -> Self {
696    //     if const { P::POLICY.precision.ge(PrecisionPolicy::Average) } {
697    //         Self::ONE / (Self::ONE + (-self).exp_p::<P>())
698    //     } else {
699    //         let (r, d) = const {
700    //             match P::POLICY.precision {
701    //                 PrecisionPolicy::Worst => (8, -1.0 / (1 << 8) as f32),
702    //                 PrecisionPolicy::Medium => (12, -1.0 / (1 << 12) as f32),
703    //                 _ => (0, 0.0), // not used since Average and above use the other method
704    //             }
705    //         };
706
707    //         let mut base = self.mul_adde(Self::splat(d), Self::ONE);
708
709    //         for _ in 0..(r - 1) {
710    //             base *= base;
711    //         }
712
713    //         base.mul_adde(base, Self::ONE).approx_reciprocal_p::<P>()
714    //     }
715    // }
716
717    #[inline(always)]
718    fn lgamma<P: Policy>(self) -> Self {
719        if const { is_reference::<P>() } {
720            return map1(self, libm::lgammaf);
721        }
722
723        Self::lgamma_r::<P>(self).0
724    }
725
726    #[inline(always)]
727    fn tgamma<P: Policy>(self) -> Self {
728        if const { is_reference::<P>() } {
729            return map1(self, libm::tgammaf);
730        }
731
732        let z = self;
733
734        if const { P::POLICY.precision.lt(PrecisionPolicy::Average) } {
735            // We have a good lgamma approximation, so use it for tgamma on lower precisions.
736            let (lgamma, sign) = z.lgamma_r_p::<P>();
737
738            // use min(P + 1, Average) precision here. We want decent precision,
739            // but not more than average.
740            return lgamma.exp_p::<ExtraPrecision<P>>() * sign;
741        }
742
743        // 36 is the largest integer whose factorial is finite in f32.
744        generic::gamma::tgamma_impl::<P, _, _, _>(
745            z,
746            &crate::tables::gamma::LANCZOS_F32,
747            36.0,
748            crate::tables::gamma::LN_MAX_F32,
749        )
750    }
751
752    #[inline(always)]
753    fn trigamma<P: Policy>(self) -> Self {
754        generic::trigamma::trigamma_impl::<P, _, _>(self, &crate::tables::gamma::TRIGAMMA_F32)
755    }
756
757    #[inline(always)]
758    fn polygamma<P: Policy>(self, n: u32) -> Self {
759        generic::polygamma::polygamma_impl::<P, _, _>(self, n)
760    }
761
762    #[inline(always)]
763    fn digamma<P: Policy>(self) -> Self {
764        generic::digamma::digamma_impl::<P, _, _, _, _, _, _>(self, &crate::tables::gamma::DIGAMMA_F32)
765    }
766
767    #[inline(always)]
768    fn beta<P: Policy>(a: Self, b: Self) -> Self {
769        if const { is_reference::<P>() } {
770            return map2(a, b, |a, b| {
771                let (a, b) = (a as f64, b as f64);
772                let (la, sa) = libm::lgamma_r(a);
773                let (lb, sb) = libm::lgamma_r(b);
774                let (lab, sab) = libm::lgamma_r(a + b);
775                (libm::exp(la + lb - lab) * ((sa * sb * sab) as f64)) as f32
776            });
777        }
778
779        generic::gamma::beta_impl::<P, _, _, _>(a, b, &crate::tables::gamma::LANCZOS_F32)
780    }
781
782    #[inline(always)]
783    fn expint_n<P: Policy, const N: usize>(self) -> Self {
784        generic::expint::expint_double_n::<P, f32, Self, N>(self)
785    }
786
787    #[inline(always)]
788    fn expint_primal_n<P: Policy, const N: usize>(self) -> (Self, Self) {
789        generic::expint::expint_double_primal_n::<P, f32, Self, N>(self)
790    }
791
792    #[inline(always)]
793    fn phi_n<P: Policy, const N: usize>(self) -> Self {
794        // Fixed series length for f32: truncation gets at most 1/32 of the tier's ulp
795        // budget, which at `Best` and above is under one ulp.
796        let terms = const {
797            let needed = super::generic::phi::phi_series_terms(
798                N,
799                f32::EPSILON as f64 * P::POLICY.precision.tolerance() as f64 / 32.0,
800            );
801            if needed < P::POLICY.max_iterations {
802                needed
803            } else {
804                P::POLICY.max_iterations
805            }
806        };
807        super::generic::phi::phi_internal_n::<Self, f32, P, N, false>(self, terms)
808    }
809
810    #[inline(always)]
811    fn expint<P: Policy>(self, n: u32) -> Self {
812        generic::expint::expint_double::<P, f32, Self>(self, n)
813    }
814
815    #[inline(always)]
816    fn expint_primal<P: Policy>(self, n: u32) -> (Self, Self) {
817        generic::expint::expint_double_primal::<P, f32, Self>(self, n)
818    }
819
820    #[inline(always)]
821    fn phi<P: Policy>(self, n: u32) -> Self {
822        // Precomputed per policy, as for f64.
823        const EPS_SCALE: f64 = 1.0 / 32.0;
824        let table = const {
825            super::generic::phi::phi_terms_table(
826                f32::EPSILON as f64 * P::POLICY.precision.tolerance() as f64 * EPS_SCALE,
827                P::POLICY.max_iterations,
828            )
829        };
830        let terms = match table.get(n as usize) {
831            Some(&t) => t,
832            None => {
833                let needed = super::generic::phi::phi_series_terms(
834                    n as usize,
835                    f32::EPSILON as f64 * P::POLICY.precision.tolerance() as f64 * EPS_SCALE,
836                );
837                if needed < P::POLICY.max_iterations {
838                    needed
839                } else {
840                    P::POLICY.max_iterations
841                }
842            }
843        };
844        super::generic::phi::phi_internal::<Self, f32, P, false>(self, n, terms)
845    }
846}
847
848// TEMP(bessel_j): dead while `bessel_j` is off the public trait. Kept, not deleted,
849// because it is the working f32 `J_0` kernel and comes back with the rest of the
850// family. Re-enable it together with the other TEMP(bessel_j) markers.
851#[allow(dead_code)]
852#[inline(always)]
853fn bessel_j0_pqzero<V, P: Policy>(x: V, ix: V::Bits) -> (V, V)
854where
855    V: FloatVectorWithBits<Element = f32> + SpecializedSpecialMath<f32>,
856{
857    /* The asymptotic expansions of pzero is
858     *      1 - 9/128 s^2 + 11025/98304 s^4 - ...,  where s = 1/x.
859     * For x >= 2, We approximate pzero by
860     *      pzero(x) = 1 + (R/S)
861     * where  R = pR0 + pR1*s^2 + pR2*s^4 + ... + pR5*s^10
862     *        S = 1 + pS0*s^2 + ... + pS4*s^10
863     * and
864     *      | pzero(x)-1-R/S | <= 2  ** ( -60.26)
865     */
866    const PR8: [f32; 6] = [
867        /* for x in [inf, 8]=1/[0,0.125] */
868        -5.2530439453e+03, /* 0xc5a4285a */
869        -2.4852163086e+03, /* 0xc51b5376 */
870        -2.5706311035e+02, /* 0xc3808814 */
871        -8.0816707611e+00, /* 0xc1014e86 */
872        -7.0312500000e-02, /* 0xbd900000 */
873        0.0000000000e+00,  /* 0x00000000 */
874    ];
875    const PS8: [f32; 5] = [
876        4.7627726562e+04, /* 0x473a0bba */
877        1.1675296875e+05, /* 0x47e4087c */
878        4.0597855469e+04, /* 0x471e95db */
879        3.8337448730e+03, /* 0x456f9beb */
880        1.1653436279e+02, /* 0x42e91198 */
881    ];
882    const PR5: [f32; 6] = [
883        /* for x in [8,4.5454]=1/[0.125,0.22001] */
884        -3.4643338013e+02, /* 0xc3ad3779 */
885        -3.3123129272e+02, /* 0xc3a59d9b */
886        -6.7674766541e+01, /* 0xc287597b */
887        -4.1596107483e+00, /* 0xc0851b88 */
888        -7.0312492549e-02, /* 0xbd8fffff */
889        -1.1412546255e-11, /* 0xad48c58a */
890    ];
891    const PS5: [f32; 5] = [
892        2.4060581055e+03, /* 0x451660ee */
893        9.6254453125e+03, /* 0x461665c8 */
894        5.9789707031e+03, /* 0x45bad7c4 */
895        1.0512523193e+03, /* 0x44836813 */
896        6.0753936768e+01, /* 0x42730408 */
897    ];
898
899    const PR3: [f32; 6] = [
900        /* for x in [4.547,2.8571]=1/[0.2199,0.35001] */
901        -3.1447946548e+01, /* 0xc1fb9565 */
902        -5.8079170227e+01, /* 0xc2685112 */
903        -2.1965976715e+01, /* 0xc1afba52 */
904        -2.4090321064e+00, /* 0xc01a2d95 */
905        -7.0311963558e-02, /* 0xbd8fffb8 */
906        -2.5470459075e-09, /* 0xb12f081b */
907    ];
908    const PS3: [f32; 5] = [
909        1.7358093262e+02, /* 0x432d94b8 */
910        1.1279968262e+03, /* 0x448cffe6 */
911        1.1936077881e+03, /* 0x44953373 */
912        3.6151397705e+02, /* 0x43b4c1ca */
913        3.5856033325e+01, /* 0x420f6c94 */
914    ];
915
916    const PR2: [f32; 6] = [
917        /* for x in [2.8570,2]=1/[0.3499,0.5] */
918        -3.2336456776e+00, /* 0xc04ef40d */
919        -1.1193166733e+01, /* 0xc1331736 */
920        -7.6356959343e+00, /* 0xc0f4579f */
921        -1.4507384300e+00, /* 0xbfb9b1cc */
922        -7.0303097367e-02, /* 0xbd8ffb12 */
923        -8.8753431271e-08, /* 0xb3be98b7 */
924    ];
925    const PS2: [f32; 5] = [
926        1.4657617569e+01, /* 0x416a859a */
927        1.5387539673e+02, /* 0x4319e01a */
928        2.7047027588e+02, /* 0x43873c32 */
929        1.3620678711e+02, /* 0x430834f0 */
930        2.2220300674e+01, /* 0x41b1c32d */
931    ];
932
933    /* For x >= 8, the asymptotic expansions of qzero is
934     *      -1/8 s + 75/1024 s^3 - ..., where s = 1/x.
935     * We approximate pzero by
936     *      qzero(x) = s*(-1.25 + (R/S))
937     * where  R = qR0 + qR1*s^2 + qR2*s^4 + ... + qR5*s^10
938     *        S = 1 + qS0*s^2 + ... + qS5*s^12
939     * and
940     *      | qzero(x)/s +1.25-R/S | <= 2  ** ( -61.22)
941     */
942    const QR8: [f32; 6] = [
943        /* for x in [inf, 8]=1/[0,0.125] */
944        3.7014625000e+04, /* 0x471096a0 */
945        8.8591972656e+03, /* 0x460a6cca */
946        5.5767340088e+02, /* 0x440b6b19 */
947        1.1768206596e+01, /* 0x413c4a93 */
948        7.3242187500e-02, /* 0x3d960000 */
949        0.0000000000e+00, /* 0x00000000 */
950    ];
951    const QS8: [f32; 6] = [
952        -3.4389928125e+05, /* 0xc8a7eb69 */
953        8.4050156250e+05,  /* 0x494d3359 */
954        8.0330925000e+05,  /* 0x49441ed4 */
955        1.4253829688e+05,  /* 0x480b3293 */
956        8.0983447266e+03,  /* 0x45fd12c2 */
957        1.6377603149e+02,  /* 0x4323c6aa */
958    ];
959
960    const QR5: [f32; 6] = [
961        /* for x in [8,4.5454]=1/[0.125,0.22001] */
962        1.9899779053e+03, /* 0x44f8bf4b */
963        1.0272437744e+03, /* 0x448067cd */
964        1.3511157227e+02, /* 0x43071c90 */
965        5.8356351852e+00, /* 0x40babd86 */
966        7.3242180049e-02, /* 0x3d95ffff */
967        1.8408595828e-11, /* 0x2da1ec79 */
968    ];
969    const QS5: [f32; 6] = [
970        -5.3543427734e+03, /* 0xc5a752be */
971        3.5976753906e+04,  /* 0x470c88c1 */
972        5.6751113281e+04,  /* 0x475daf1d */
973        1.8847289062e+04,  /* 0x46933e94 */
974        2.0778142090e+03,  /* 0x4501dd07 */
975        8.2776611328e+01,  /* 0x42a58da0 */
976    ];
977
978    const QR3: [f32; 6] = [
979        /* for x in [4.547,2.8571]=1/[0.2199,0.35001] */
980        1.6673394775e+02, /* 0x4326bbe4 */
981        1.7080809021e+02, /* 0x432acedf */
982        4.2621845245e+01, /* 0x422a7cc5 */
983        3.3442313671e+00, /* 0x405607e3 */
984        7.3241114616e-02, /* 0x3d95ff70 */
985        4.3774099900e-09, /* 0x3196681b */
986    ];
987    const QS3: [f32; 6] = [
988        -1.4924745178e+02, /* 0xc3153f59 */
989        2.5163337402e+03,  /* 0x451d4557 */
990        6.4604252930e+03,  /* 0x45c9e367 */
991        3.7041481934e+03,  /* 0x4567825f */
992        7.0968920898e+02,  /* 0x44316c1c */
993        4.8758872986e+01,  /* 0x42430916 */
994    ];
995
996    const QR2: [f32; 6] = [
997        /* for x in [2.8570,2]=1/[0.3499,0.5] */
998        1.6252708435e+01, /* 0x4182058c */
999        3.1666231155e+01, /* 0x41fd5471 */
1000        1.4495602608e+01, /* 0x4167edfd */
1001        1.9981917143e+00, /* 0x3fffc4bf */
1002        7.3223426938e-02, /* 0x3d95f62a */
1003        1.5044444979e-07, /* 0x342189db */
1004    ];
1005    const QS2: [f32; 6] = [
1006        -5.3109550476e+00, /* 0xc0a9f358 */
1007        2.1266638184e+02,  /* 0x4354aa98 */
1008        8.8293585205e+02,  /* 0x445cbbe5 */
1009        8.4478375244e+02,  /* 0x44533229 */
1010        2.6934811401e+02,  /* 0x4386ac8f */
1011        3.0365585327e+01,  /* 0x41f2ecb8 */
1012    ];
1013
1014    let z = x.approx_reciprocal_p::<P>();
1015    let z2 = z * z;
1016
1017    let m8 = ix.cmp_ge(thermite::const_splat!(u32: 0x41000000)); // |x| >= 8.0
1018    let m5 = ix.cmp_ge(thermite::const_splat!(u32: 0x409173eb)); // |x| >= 4.5454
1019    let m3 = ix.cmp_ge(thermite::const_splat!(u32: 0x4036d917)); // |x| >= 2.8571
1020
1021    // Evaluate numerators and denominators for all 4 regions independently,
1022    // then select before dividing once.
1023    let pn8 = z2.poly_rev_n_p::<P, _>(&PR8);
1024    let pn5 = z2.poly_rev_n_p::<P, _>(&PR5);
1025    let pn3 = z2.poly_rev_n_p::<P, _>(&PR3);
1026    let pn2 = z2.poly_rev_n_p::<P, _>(&PR2);
1027
1028    let pd8 = z2.poly_rev_n_p::<P, _>(&PS8);
1029    let pd5 = z2.poly_rev_n_p::<P, _>(&PS5);
1030    let pd3 = z2.poly_rev_n_p::<P, _>(&PS3);
1031    let pd2 = z2.poly_rev_n_p::<P, _>(&PS2);
1032
1033    let pn = m3.select(m5.select(m8.select(pn8, pn5), pn3), pn2);
1034    let pd = m3.select(m5.select(m8.select(pd8, pd5), pd3), pd2);
1035
1036    let qn8 = z2.poly_rev_n_p::<P, _>(&QR8);
1037    let qn5 = z2.poly_rev_n_p::<P, _>(&QR5);
1038    let qn3 = z2.poly_rev_n_p::<P, _>(&QR3);
1039    let qn2 = z2.poly_rev_n_p::<P, _>(&QR2);
1040
1041    let qd8 = z2.poly_rev_n_p::<P, _>(&QS8);
1042    let qd5 = z2.poly_rev_n_p::<P, _>(&QS5);
1043    let qd3 = z2.poly_rev_n_p::<P, _>(&QS3);
1044    let qd2 = z2.poly_rev_n_p::<P, _>(&QS2);
1045
1046    let qn = m3.select(m5.select(m8.select(qn8, qn5), qn3), qn2);
1047    let qd = m3.select(m5.select(m8.select(qd8, qd5), qd3), qd2);
1048
1049    let pzero = V::ONE + pn / pd.mul_adde(z2, V::ONE);
1050    let mut qzero = qn / qd.mul_adde(z2, V::ONE);
1051
1052    let neg_eighth: V = thermite::const_splat!(f32: -0.125);
1053
1054    if const { matches!(V::HAS_NATIVE_FMA, thermite::tribool::True) } {
1055        // z*-1/8 can be computed earlier,
1056        // so despite this having the same number of
1057        // instructions as the non-FMA version, it will
1058        // be slightly faster
1059        qzero = qzero.mul_add(z, z * neg_eighth);
1060    } else {
1061        qzero = (qzero + neg_eighth) * z;
1062    }
1063
1064    (pzero, qzero)
1065}
1066
1067// TEMP(bessel_j): see above.
1068#[cfg(any())]
1069#[inline(always)]
1070fn bessel_j0<V, P: Policy>(x: V) -> V
1071where
1072    V: FloatVectorWithBits<Element = f32> + SpecializedSpecialMath<f32>,
1073{
1074    let ax = x.abs().flush_denormals_p::<P>();
1075    let ix: V::Bits = ax.into_bits();
1076    let large = ix.cmp_ge(thermite::const_splat!(u32: 0x40000000)); // |x| >= 2.0
1077
1078    // ========================================================
1079    // Small-x path: |x| < 2
1080    // J_0(x) ≈ (1+x/2)(1-x/2) + z*(R(z)/S(z)),  z = x^2
1081    // The (1+x/2)(1-x/2) form avoids cancellation vs 1-x^2/4.
1082    // ========================================================
1083    let z = x * x;
1084
1085    /* R0/S0 on [0, 2.00] */
1086    let r = z * z.poly_rev_n_p::<P, _>(&[
1087        -4.6183270541e-09, /* 0xb19eaf3c */
1088        1.8295404516e-06,  /* 0x35f58e88 */
1089        -1.8997929874e-04, /* 0xb947352e */
1090        1.5625000000e-02,  /* 0x3c800000 */
1091    ]);
1092
1093    let s = z.poly_rev_n_p::<P, _>(&[
1094        1.1661400734e-09, /* 0x30a045e8 */
1095        5.1354652442e-07, /* 0x3509daa6 */
1096        1.1692678527e-04, /* 0x38f53697 */
1097        1.5619102865e-02, /* 0x3c7fe744 */
1098    ]);
1099
1100    let s = s.mul_adde(z, V::ONE);
1101
1102    let mut y = ax
1103        .mul_adde(V::HALF, V::ONE)
1104        .mul_adde(ax.nmul_adde(V::HALF, V::ONE), z * (r / s));
1105
1106    // ========================================================
1107    // Large-x path: |x| >= 2
1108    // J_0(x) = FRAC_1_SQRT_PI * (P(x)*cc - Q(x)*ss) / sqrt(x)
1109    //
1110    // cc and ss encode cos(x-π/4) and sin(x-π/4) via a
1111    // numerical conditioning trick to avoid cancellation.
1112    // ========================================================
1113
1114    if large.any() {
1115        let (sinx, cosx) = ax.sin_cos_p::<P>();
1116
1117        // -cos(2x): fresh trig call at doubled argument for Best+ precision
1118        // (avoids cancellation in 1-2cos^2x near x ≈ kπ/4);
1119        // otherwise 1-2cos^2x, which is exact at the cancellation point
1120        // and only loses bits near (but not at) those values.
1121        let neg_cos2x = if const { P::POLICY.precision.ge(PrecisionPolicy::Best) } {
1122            -(ax + ax).cos_p::<P>()
1123        } else {
1124            (cosx * cosx).nmul_adde(V::TWO, V::ONE)
1125        };
1126
1127        // cc = sin(x) + cos(x),  ss = sin(x) - cos(x)
1128        // Identity: cc * ss = sin^2x - cos^2x = -cos(2x)
1129        // Whichever of |cc|, |ss| is smaller gets recomputed
1130        // as -cos(2x) / (the larger one) for better precision.
1131        let cc_raw = sinx + cosx;
1132        let ss_raw = sinx - cosx;
1133
1134        let fix_cc = (sinx * cosx).is_negative();
1135        let ratio = neg_cos2x / fix_cc.select(ss_raw, cc_raw);
1136        let cc = fix_cc.select(ratio, cc_raw);
1137        let ss = fix_cc.select(ss_raw, ratio);
1138
1139        // Envelope polynomials (combined to share masks and 1/x^2)
1140        let (pz, qz) = bessel_j0_pqzero::<V, P>(ax, ix);
1141
1142        let yl = V::FRAC_1_SQRT_PI * (pz * cc - qz * ss) / ax.sqrt();
1143
1144        y = large.select(yl, y);
1145
1146        if const { P::POLICY.precision.ge(PrecisionPolicy::Best) } {
1147            let very_large = ix.cmp_ge(thermite::const_splat!(u32: 0x7f800000));
1148
1149            y = very_large.select(ax.square().approx_reciprocal_p::<P>(), y);
1150        }
1151    }
1152
1153    y
1154}
1155
1156impl<V: FloatVectorWithBits<Element = f32>> SpecializedRealSpecialMath<f32> for V
1157where
1158    V: TranscendentalMathWithPolicy<Element = f32>,
1159    V: SpecializedTranscendentalMath<f32>,
1160    // Pins the projection: a type parameter's `Primal` will not normalize through
1161    // the blanket impl on its own, and the table signatures need `Primal = Self`.
1162    V: thermite::math::PrimalProjection<Primal = V>,
1163{
1164    #[inline(always)]
1165    fn fresnel<P: Policy>(self) -> (Self, Self) {
1166        use crate::tables::fresnel as t;
1167        generic::fresnel::fresnel_with::<P, _, _, _, _, _, _>(
1168            self,
1169            t::X0_F32,
1170            t::MAP_F32,
1171            t::CUTOFF_F32,
1172            &t::CHEB_C_F32,
1173            &t::CHEB_S_F32,
1174            &t::AUX_P_F32,
1175            &t::AUX_Q_F32,
1176        )
1177    }
1178
1179    #[inline(always)]
1180    fn sici<P: Policy>(self) -> (Self, Self) {
1181        use crate::tables::sici as t;
1182        generic::sici::sici_with::<P, _, _, _, _, _, _>(
1183            self,
1184            t::X0_F32,
1185            t::MAP_F32,
1186            t::CUTOFF_F32,
1187            &t::CHEB_SI_F32,
1188            &t::CHEB_CIN_F32,
1189            &t::AUX_P_F32,
1190            &t::AUX_Q_F32,
1191        )
1192    }
1193
1194    // --- Spherical harmonics: the compile-time-table fast paths ---
1195    //
1196    // A concrete `f32`/`f64` element has a `ShConsts` table, which the generic
1197    // defaults cannot assume. Both overrides are guarded by `L <= MAX_SH_DEGREE`,
1198    // the extent of the stamped ladder, and fall back to the generic body above it.
1199    // A statically-false `if const` arm is dropped before monomorphization, so the
1200    // out-of-range table is never built.
1201
1202    #[inline(always)]
1203    fn spherical_harmonics<P: Policy, const L: usize, const N: usize, const CS: bool>(
1204        x: Self,
1205        y: Self,
1206        z: Self,
1207        out: &mut [Self; N],
1208    ) {
1209        // Fully unrolled, constants folded into the instruction stream: no table is
1210        // materialized at all, so there is nothing to hoist out of a loop. Above
1211        // MAX_SH_DEGREE the kernel routes itself to the general path.
1212        sh_impl::<P, f32, Self, L, N, CS>(x, y, z, out);
1213    }
1214
1215    #[inline(always)]
1216    fn spherical_harmonics_table<P: Policy, const L: usize, const N: usize, const CS: bool>(
1217        table: &mut ShTable<Self, N>,
1218    ) {
1219        if const { L <= MAX_SH_DEGREE } {
1220            // Every coefficient is already a compile-time constant of the right
1221            // phase, so building the runtime table is a splat per entry, with none of
1222            // the sqrt/divide work the generic default does.
1223            let src = &<f32 as ShConsts<L, N, CS>>::TABLE;
1224
1225            let mut i = 0;
1226            while i < N {
1227                table.qmm[i] = Self::splat(src.qmm[i]);
1228                table.em[i] = Self::splat(src.em[i]);
1229                table.a[i] = Self::splat(src.a[i]);
1230                table.nb[i] = Self::splat(src.nb[i]);
1231                table.f[i] = Self::splat(src.f[i]);
1232                table.mf[i] = Self::splat(src.mf[i]);
1233                i += 1;
1234            }
1235        } else {
1236            sh_table_impl::<Self, L, N, CS>(table);
1237        }
1238    }
1239
1240    #[inline(always)]
1241    fn bessel_i_ratio<P: Policy>(self, nu: Self) -> Self {
1242        generic::bessel::ratio::bessel_i_ratio_impl::<P, f32, Self>(self, nu)
1243    }
1244
1245    #[inline(always)]
1246    fn inv_bessel_i_ratio<P: Policy>(self, nu: Self) -> Self {
1247        generic::bessel::ratio::inv_bessel_i_ratio_impl::<P, f32, Self>(self, nu)
1248    }
1249
1250    #[inline(always)]
1251    fn bessel_i_ratio_1m<P: Policy>(self, nu: Self) -> Self {
1252        generic::bessel::ratio::bessel_i_ratio_1m_impl::<P, f32, Self>(self, nu)
1253    }
1254
1255    #[inline(always)]
1256    fn inv_bessel_i_ratio_1m<P: Policy>(self, nu: Self) -> Self {
1257        generic::bessel::ratio::inv_bessel_i_ratio_1m_impl::<P, f32, Self>(self, nu)
1258    }
1259
1260    #[inline(always)]
1261    fn erfinv<P: Policy>(self) -> Self {
1262        // (-1, 1) range
1263        let x = self.flush_denormals_p::<P>().clamp(
1264            thermite::const_splat!(f32: -0.99999),
1265            thermite::const_splat!(f32: 0.99999),
1266        );
1267
1268        let w = -x.nmul_adde(x, V::ONE).ln_p::<P>();
1269
1270        let ge5 = w.cmp_ge(thermite::const_splat!(f32: 5.0));
1271
1272        let w0 = w - thermite::const_splat!(f32: 2.5);
1273        let mut p0 = w0.poly_rev_n_p::<P, _>(&[
1274            2.81022636e-08,
1275            3.43273939e-07,
1276            -3.5233877e-06,
1277            -4.39150654e-06,
1278            0.00021858087,
1279            -0.00125372503,
1280            -0.00417768164,
1281            0.246640727,
1282            1.50140941,
1283        ]);
1284
1285        if const { P::POLICY.avoid_branching } || thermite::unlikely(ge5.any()) {
1286            let w1 = w.sqrt() - thermite::const_splat!(f32: 3.0);
1287            let p1 = w1.poly_rev_n_p::<P, _>(&[
1288                -0.000200214257,
1289                0.000100950558,
1290                0.00134934322,
1291                -0.00367342844,
1292                0.00573950773,
1293                -0.0076224613,
1294                0.00943887047,
1295                1.00167406,
1296                2.83297682,
1297            ]);
1298
1299            p0 = ge5.select(p1, p0);
1300        }
1301
1302        p0 * x
1303    }
1304
1305    /// Uses the algorithm from Peter John Acklam, sourced from here:
1306    /// <https://web.archive.org/web/20151030215612/http://home.online.no/~pjacklam/notes/invnorm/>
1307    fn probit<P: Policy>(self) -> Self {
1308        const A: [f32; 6] = [
1309            2.506628277459239e+00,
1310            -3.066479806614716e+01,
1311            1.383577518672690e+02,
1312            -2.759285104469687e+02,
1313            2.209460984245205e+02,
1314            -3.969683028665376e+01,
1315        ];
1316
1317        const B: [f32; 6] = [
1318            1.0,
1319            -1.328068155288572e+01,
1320            6.680131188771972e+01,
1321            -1.556989798598866e+02,
1322            1.615858368580409e+02,
1323            -5.447609879822406e+01,
1324        ];
1325
1326        const C: [f32; 6] = [
1327            2.938163982698783e+00,
1328            4.374664141464968e+00,
1329            -2.549732539343734e+00,
1330            -2.400758277161838e+00,
1331            -3.223964580411365e-01,
1332            -7.784894002430293e-03,
1333        ];
1334
1335        const D: [f32; 5] = [
1336            1.0,
1337            3.754408661907416e+00,
1338            2.445134137142996e+00,
1339            3.224671290700398e-01,
1340            7.784695709041462e-03,
1341        ];
1342
1343        // f32 is at its precision limit without refinement (REFINE = false).
1344        generic::probit::probit_acklam::<P, _, _, false>(self, &A, &B, &C, &D)
1345    }
1346
1347    #[inline(always)]
1348    fn langevin<P: Policy>(self) -> Self {
1349        // The Worst/Medium tiers take the short table (see it for its error).
1350        if const { P::POLICY.precision.le(PrecisionPolicy::Medium) } {
1351            generic::langevin::langevin_primal::<P, _, _, 5, false>(self, &LANGEVIN_SMALL_F32_LO).0
1352        } else {
1353            generic::langevin::langevin_primal::<P, _, _, 8, false>(self, &LANGEVIN_SMALL_F32).0
1354        }
1355    }
1356
1357    #[inline(always)]
1358    fn langevin_1m<P: Policy>(self) -> Self {
1359        // The Worst/Medium tiers take the short table (see it for its error).
1360        if const { P::POLICY.precision.le(PrecisionPolicy::Medium) } {
1361            generic::langevin::langevin_primal::<P, _, _, 5, true>(self, &LANGEVIN_SMALL_F32_LO).0
1362        } else {
1363            generic::langevin::langevin_primal::<P, _, _, 8, true>(self, &LANGEVIN_SMALL_F32).0
1364        }
1365    }
1366
1367    // f32 refines with Newton (see the kernel docs).
1368    #[inline(always)]
1369    fn inv_langevin<P: Policy>(self) -> Self {
1370        generic::langevin::inv_langevin::<P, _, _, 8, 5, false, false>(self, &LANGEVIN_SMALL_F32, &LANGEVIN_SEED_F32)
1371    }
1372
1373    #[inline(always)]
1374    fn inv_langevin_1m<P: Policy>(self) -> Self {
1375        generic::langevin::inv_langevin::<P, _, _, 8, 5, false, true>(self, &LANGEVIN_SMALL_F32, &LANGEVIN_SEED_F32)
1376    }
1377
1378    #[inline(always)]
1379    fn lgamma_r<P: Policy>(self) -> (Self, Self) {
1380        if const { is_reference::<P>() } {
1381            return map1x2(self, |x| {
1382                let (v, s) = libm::lgammaf_r(x);
1383                (v, s as f32)
1384            });
1385        }
1386
1387        let z = self.flush_denormals_p::<P>();
1388        let mut signum = Self::ONE;
1389
1390        let reflect = z.is_negative();
1391
1392        // `Average` and above take the Lanczos path below. This arm is the cheap tier: two
1393        // approximations of lgamma(x+1), split at the point where their error curves cross.
1394        if const { P::POLICY.precision.le(PrecisionPolicy::Medium) } {
1395            let x = reflect.select(Self::ONE - z, z);
1396            let w = Self::ONE / x; // try to get this dispatched early since division is so slow
1397
1398            // Below the crossover, a degree-12 minimax polynomial (mpmath `chebyfit` of
1399            // lgamma(x+1) over [0, 4]).
1400            //
1401            // Plain polynomial rather than a rational: on a bounded interval with no poles,
1402            // lgamma(x+1) is analytic and a rational buys nothing, while the divide sits on the
1403            // critical path and does not pipeline. This is 13 coefficients against the previous
1404            // [7/9] rational's 18 plus that divide.
1405            //
1406            // Evaluated in t = 2x/C - 1 rather than in x directly. The monomial basis over a wide
1407            // interval is badly conditioned, and fails silently: fitting in x gives perfectly
1408            // reasonable-looking coefficients whose exact-arithmetic error is fine, but rounding
1409            // them to f32 cost four orders of magnitude (1.9e-8 -> 2.0e-4 at degree 16) because
1410            // x^k amplifies each rounding error, and x^16 reaches 4.3e9 over this interval. Mapping
1411            // to [-1, 1] bounds every power by one and the loss disappears.
1412            //
1413            // C = 4 puts the scale at exactly 0.5, so the mapping is a single exact FMA. The
1414            // error-balanced crossover is nearer 3.83, but the difference is one f32 ulp against
1415            // this tier's 10000-ulp budget and not worth an inexact constant.
1416            let t = x.mul_adde(Self::HALF, Self::NEG_ONE);
1417
1418            let mut y = t.poly_n_p::<P, _>(&[
1419                6.931471825e-01,
1420                1.845574498e+00,
1421                7.898645401e-01,
1422                -2.056447715e-01,
1423                7.939288765e-02,
1424                -3.510471061e-02,
1425                1.755452715e-02,
1426                -1.377308462e-02,
1427                7.950476371e-03,
1428                2.705342602e-03,
1429                -1.809931011e-03,
1430                -4.729579668e-03,
1431                2.926796675e-03,
1432            ]);
1433
1434            // Above the crossover, Stirling's series, written for lgamma(x+1) so it drops into
1435            // the same slot:
1436            //
1437            //   lgamma(x+1) = (x + 1/2) ln(x) - x + ln(2pi)/2 + 1/(12x) - 1/(360x^3) + ...
1438            //
1439            // Its coefficients are the Bernoulli terms B_2n/(2n(2n-1)), exact rationals rather
1440            // than a fit. No polynomial can take its place out here: lgamma(x) ~ x ln(x) is not
1441            // rational, so a Pade approximant decays away from its expansion point and
1442            // eventually changes sign, returning -17690 at x = 300 where the answer is 1409.
1443            //
1444            // Nor can Stirling take over the small end. Its series is asymptotic, not convergent:
1445            // at x = 0.5 it is 1.6% off, at x = 0.1 it returns the wrong sign, and adding terms
1446            // there makes it worse rather than better. Reaching small arguments would need the
1447            // recurrence shift lgamma(x) = lgamma(x+N) - ln(x(x+1)...(x+N-1)), whose second
1448            // logarithm on the common path is the cost this whole arm exists to avoid.
1449            //
1450            // Cost here is one reciprocal and three FMAs: the ln(x) is the one this path already
1451            // computes below for the lgamma(x+1) -> lgamma(x) offset.
1452            // The crossover sits near where the two error curves meet, located by bisection
1453            // against an mpmath reference: the polynomial holds 1.22e-6 over [0, 4] and Stirling
1454            // 7.4e-7 from there up, so neither arm is stretched. That is 5 and 3 f32 ulp
1455            // respectively, against this tier's 10000-ulp budget.
1456            let big = x.cmp_ge(thermite::const_splat!(f32: 4.0));
1457
1458            // Both arms approximate lgamma(x+1), so the result carries an offset of ln(x); when
1459            // reflected the offset is ln(|sin(pi z)| / x) instead. Folding that division into the
1460            // logarithm's argument rather than taking two logarithms and subtracting is what
1461            // keeps this path to a single `ln` in every ordinary case.
1462            let mut e = x;
1463
1464            // reflection for negative values
1465            if const { P::POLICY.avoid_branching } || thermite::unlikely(reflect.any()) {
1466                let pix = (z * Self::PI).sin_p::<P>();
1467
1468                signum |= reflect.select(pix.signed_zero(), signum);
1469
1470                e = reflect.select(pix.abs() / x, x);
1471            }
1472
1473            let ln_e = e.ln_p::<P>();
1474
1475            // Stirling wants ln(x) on its own. Away from the reflection that *is* `ln_e`, so it
1476            // costs nothing. Only a lane that is both reflected and above the crossover needs a
1477            // second logarithm, because there the two arguments genuinely differ and no
1478            // rearrangement merges them: the reflection needs ln|sin(pi z)| and Stirling needs
1479            // ln(x), which are independent transcendentals. Those lanes are large negative
1480            // arguments, so the extra `ln` sits behind a doubly-unlikely guard.
1481            let mut lnx = ln_e;
1482
1483            if const { P::POLICY.avoid_branching } || thermite::unlikely((reflect & big).any()) {
1484                lnx = reflect.select(x.ln_p::<P>(), ln_e);
1485            }
1486
1487            let c = (w * w).mul_adde(
1488                thermite::const_splat!(f32: -1.0 / 360.0),
1489                thermite::const_splat!(f32: 1.0 / 12.0),
1490            );
1491            let stirling = (x + Self::HALF).mul_adde(lnx, w.mul_adde(c, Self::FRAC_LN_TAU_2 - x));
1492
1493            y = big.select(stirling, y);
1494            y = reflect.select(Self::LN_PI - y, y);
1495            y -= ln_e;
1496
1497            if const { P::POLICY.check_overflow } {
1498                // Stirling's `(x + 1/2) ln(x) - x` is inf - inf at an infinite argument, and
1499                // the arm is selected there since inf >= the crossover. lgamma diverges at
1500                // both ends (the negative side reaches this through the reflection, whose
1501                // `x = 1 - z` is likewise infinite), so both map to +inf.
1502                y = z.is_infinite().select(Self::INFINITY, y);
1503            }
1504
1505            return (y, signum);
1506        }
1507
1508        generic::gamma::lgamma_r_impl::<P, _, _, _>(z, &crate::tables::gamma::LANCZOS_F32)
1509    }
1510
1511    #[inline(always)]
1512    fn gelu<P: Policy>(self, alpha: Self) -> Self {
1513        if const { is_reference::<P>() } {
1514            return map2(self, alpha, |x, a| {
1515                let (x, a) = (x as f64, a as f64);
1516                (0.5 * x * libm::erfc(-a * x * core::f64::consts::FRAC_1_SQRT_2)) as f32
1517            });
1518        }
1519
1520        let x = self;
1521
1522        let alpha_x = alpha * x;
1523
1524        // GELU(x) = 0.5 * x * (1 + erf(ax / sqrt(2))) = 0.5 * x * erfc(-ax / sqrt(2))
1525        // O = false: skip the exp(-ax^2) byproduct that only the derivative needs.
1526        let mut unused = Self::EMPTY;
1527        let c = erf_f_internal::<V, P, true, false>(alpha_x * -Self::FRAC_1_SQRT_2, &mut unused);
1528
1529        (x * Self::HALF) * c
1530    }
1531}
1532
1533impl<V: FloatVectorWithBits<Element = f32>> SpecializedRealPrimalMath<f32> for V
1534where
1535    V: thermite::math::PrimalProjection<Primal = V>,
1536{
1537    #[inline(always)]
1538    fn langevin_d<P: Policy>(self) -> (Self, Self) {
1539        generic::langevin::langevin_primal::<P, _, _, 8, false>(self, &LANGEVIN_SMALL_F32)
1540    }
1541
1542    #[inline(always)]
1543    #[allow(clippy::too_many_arguments)]
1544    fn spherical_harmonics_d<P: Policy, const L: usize, const N: usize, const CS: bool>(
1545        x: Self,
1546        y: Self,
1547        z: Self,
1548        out: &mut [Self; N],
1549        ddx: &mut [Self; N],
1550        ddy: &mut [Self; N],
1551        ddz: &mut [Self; N],
1552    ) {
1553        sh_d_impl::<P, f32, Self, L, N, CS>(x, y, z, out, ddx, ddy, ddz);
1554    }
1555
1556    #[inline(always)]
1557    fn gelu_d<P: Policy>(self, alpha: Self) -> (Self, Self) {
1558        let x = self;
1559
1560        let alpha_x = alpha * x;
1561
1562        // 0.5 * x * erfc(-ax / sqrt(2))
1563        let mut exp_neg_ax2 = Self::EMPTY;
1564        let c = erf_f_internal::<V, P, true, true>(alpha_x * -Self::FRAC_1_SQRT_2, &mut exp_neg_ax2);
1565
1566        let half_c = c * Self::HALF; // 0.5 * (1 + erf(ax/sqrt(2)))
1567        let alpha_x_scaled = x.scale(FloatConsts::FRAC_1_SQRT_TAU);
1568
1569        let y = x * half_c;
1570        let dy = if matches!(V::HAS_NATIVE_FMA, thermite::tribool::True) {
1571            alpha_x_scaled.mul_add(exp_neg_ax2, half_c)
1572        } else {
1573            half_c + alpha_x_scaled * exp_neg_ax2
1574        };
1575
1576        (y, dy)
1577    }
1578}
1579
1580#[allow(clippy::approx_constant)]
1581#[inline(always)]
1582fn erf_f_internal<V: FloatVectorWithBits<Element = f32>, P: Policy, const C: bool, const O: bool>(
1583    x0: V,
1584    out_exp_neg_x2: &mut V,
1585) -> V {
1586    // Extract the sign bit once. abs(x0) = x0 ^ sign, and sign is reused
1587    // for the final operation in every branch, avoiding a redundant bitand.
1588    let sign = x0.signed_zero();
1589    let x = (x0 ^ sign).flush_denormals_p::<P>();
1590
1591    // NOTE: For GPUs, exp is usually free, so these approximations are actually more expensive
1592    // than just using exp, but for CPUs they can be much faster, and the precision is still decent for many use cases.
1593    if const {
1594        matches!(P::POLICY.precision, PrecisionPolicy::Worst | PrecisionPolicy::Medium if !V::NATIVE_CAP.has(NativeCapability::EXP))
1595    } {
1596        // the polynomials below are sensitive to large inputs, so we need to clamp x to avoid exploding into inf/nan,
1597        // and erf(x) is saturating to 1.0 around x=3.81, so 4.5 is a safe clamping point that won't cause significant precision
1598        // loss for large inputs, but will prevent overflow in the polynomial evaluation.
1599        let x = x.min(thermite::const_splat!(f32: 4.5));
1600
1601        // Both use erf(x) ≈ 1 - 1/t^n for a polynomial t, and only the poly and
1602        // exponent differ. Worst: A&S degree-4, t^4.  Medium: A&S 7.1.27 degree-6, t^16 (3e-7).
1603        let tn = if const { matches!(P::POLICY.precision, PrecisionPolicy::Worst) } {
1604            let t = x.poly_rev_n_p::<P, _>(&[0.078108, 0.000972, 0.230389, 0.278393, 1.0]);
1605
1606            t.powi_p::<P>(4)
1607        } else {
1608            let t = x.poly_rev_n_p::<P, _>(&[
1609                0.0000430638,
1610                0.0002765672,
1611                0.0001520143,
1612                0.0092705272,
1613                0.0422820123,
1614                0.0705230784,
1615                1.0,
1616            ]);
1617
1618            t.powi_p::<P>(16)
1619        };
1620
1621        if const { O } {
1622            // We need a relatively accurate exp(-x^2) for GELU derivative, so opt for medium precision even in worst case,
1623            // which is still much cheaper than a full exp.
1624            *out_exp_neg_x2 = (-x * x).exp_p::<MediumPrecision<CheckOverflow<P, false>>>();
1625        }
1626
1627        // The second flag is "may we use the RAW `rcp()` estimate here". The `true` arms
1628        // below call it with a hand-rolled Newton step, so they inherit its
1629        // denormal-as-zero behaviour and `Preserve` forbids them. The `false` arms go
1630        // through `approx_reciprocal_p`, which is exact there.
1631        match const {
1632            (
1633                C,
1634                V::HAS_APPROX_RCP && !matches!(P::POLICY.denormal_behavior, DenormalBehavior::Preserve),
1635            )
1636        } {
1637            (false, true) => {
1638                let y = tn.rcp();
1639                y.nmul_adde(tn.nmul_adde(y, V::TWO), V::ONE) ^ sign
1640            }
1641            (false, false) => (V::ONE - tn.approx_reciprocal_p::<ExtraPrecision<P>>()) ^ sign,
1642            (true, true) => {
1643                let y = tn.rcp();
1644                let k = tn.nmul_adde(y, V::TWO);
1645
1646                if matches!(V::HAS_NATIVE_FMA, thermite::tribool::True) {
1647                    sign.select_negative(y.nmul_add(k, V::TWO), y * k)
1648                } else {
1649                    let erfc_pos = y * k;
1650                    sign.select_negative(V::TWO - erfc_pos, erfc_pos)
1651                }
1652            }
1653            (true, false) => {
1654                let y = tn.approx_reciprocal_p::<ExtraPrecision<P>>();
1655                sign.select_negative(V::TWO - y, y)
1656            }
1657        }
1658    }
1659    // higher precision policies or GPU with native exp support.
1660    else {
1661        // Past |x| = 1.8e19 (and at infinity) x^2 overflows and `exp_neg_x2 * t` is `0 * 0`
1662        // at best and NaN through the reciprocal at worst. erfc has underflowed by 10.1
1663        // and erf(4) is exactly 1, so clamping at 16 changes no finite result.
1664        // Compare-and-select rather than `min` so a NaN input stays NaN on every backend.
1665        let x = if const { P::POLICY.check_overflow } {
1666            let cap: V = thermite::const_splat!(f32: 16.0);
1667            x.cmp_gt(cap).select(cap, x)
1668        } else {
1669            x
1670        };
1671
1672        // if ignoring denormals (and not clamping), just multiply x0 by itself to save like
1673        // one cycle, instead of waiting on abs(), otherwise use the denormal-flushed x value
1674        let x2 = if const { matches!(P::POLICY.denormal_behavior, DenormalBehavior::Ignore) && !P::POLICY.check_overflow }
1675        {
1676            x0 * x0
1677        } else {
1678            x * x
1679        };
1680
1681        let exp_neg_x2 = (-x2).exp_p::<P>();
1682
1683        // Improved A&S method from Wikipedia, max error ~2e-9
1684        let p1: V = thermite::const_splat!(f32: 0.406742016006509);
1685        let p2: V = thermite::const_splat!(f32: 0.0072279182302319);
1686
1687        let t = x.mul_adde(x.mul_adde(p2, p1), V::ONE).approx_reciprocal_p::<P>();
1688
1689        let m = t.poly_rev_n_p::<P, _>(&[
1690            0.0382613542530727,
1691            -0.393127715207728,
1692            1.20644903073232,
1693            -1.11694155120396,
1694            1.08680830347054,
1695            -0.138329314150635,
1696            0.316879890481381, // A1
1697        ]);
1698
1699        if const { O } {
1700            *out_exp_neg_x2 = exp_neg_x2;
1701        }
1702
1703        // NOTE: We multiple e by t here, instead of
1704        // t * t.poly, as this noticeably
1705        // improve precision at zero cost.
1706        let e = exp_neg_x2 * t;
1707
1708        if const { C } {
1709            if const {
1710                matches!(V::HAS_NATIVE_FMA, thermite::tribool::True) && P::POLICY.precision.lt(PrecisionPolicy::Average)
1711            } {
1712                return sign.select_negative(e.nmul_add(m, V::TWO), e * m);
1713            }
1714
1715            let mut y = e * m;
1716
1717            let is_big = x.cmp_gt(V::ONE);
1718
1719            if const { P::POLICY.precision.ge(PrecisionPolicy::Average) }
1720                && (const { P::POLICY.avoid_branching } || is_big.any())
1721            {
1722                let s = x.approx_reciprocal_p::<P>();
1723
1724                let big_y = if const { P::POLICY.precision.ge(PrecisionPolicy::Reference) } {
1725                    // slow reference code from libm, matches nearly exactly to libm itself.
1726                    let r = s.poly_rev_n_p::<P, _>(&[
1727                        -4.8351919556e+02,
1728                        -1.0250950928e+03,
1729                        -6.3756646729e+02,
1730                        -1.6063638306e+02,
1731                        -1.7757955551e+01,
1732                        -7.9928326607e-01,
1733                        -9.8649431020e-03,
1734                    ]);
1735
1736                    let b = s.poly_rev_n_p::<P, _>(&[
1737                        -2.2440952301e+01,
1738                        4.7452853394e+02,
1739                        2.5530502930e+03,
1740                        3.1998581543e+03,
1741                        1.5367296143e+03,
1742                        3.2579251099e+02,
1743                        3.0338060379e+01,
1744                        1.0,
1745                    ]);
1746
1747                    let z: V = {
1748                        // Bit-split: zero low 13 mantissa bits so z*z is exact in f32.
1749                        let mut ix: V::Bits = x.into_bits();
1750                        ix &= thermite::const_splat!(u32: 0xffffe000);
1751                        ix.into_bits()
1752                    };
1753
1754                    let a = (-z * z - thermite::const_splat!(f32: 0.5625)).exp_p::<CheckOverflow<P, false>>();
1755                    let b = ((z - x) * (z + x) + r / b).exp_p::<CheckOverflow<P, false>>() / x;
1756
1757                    a * b
1758                } else {
1759                    // fast minimax approximation with a 68 ULP max difference, avg 0.282 ULP
1760                    exp_neg_x2
1761                        * s.mul_adde(
1762                            thermite::const_splat!(f32: 9.0 / 4.0),
1763                            thermite::const_splat!(f32: -5.0 / 4.0),
1764                        )
1765                        .poly_rev_n_p::<P, _>(&[
1766                            -1.5849000192247331142425537109375e-5,
1767                            4.057946716784499585628509521484375e-5,
1768                            -2.17467240872792899608612060546875e-5,
1769                            -9.03195686987601220607757568359375e-5,
1770                            4.285395261831581592559814453125e-4,
1771                            -1.16943917237222194671630859375e-3,
1772                            1.68157299049198627471923828125e-3,
1773                            3.04660876281559467315673828125e-3,
1774                            -3.5686969757080078125e-2,
1775                            0.18081049621105194091796875,
1776                            0.278560101985931396484375,
1777                        ])
1778                };
1779
1780                y = is_big.select(big_y, y);
1781            }
1782
1783            sign.select_negative(V::TWO - y, y)
1784        } else {
1785            let mut y = e.nmul_adde(m, V::ONE);
1786
1787            if const { P::POLICY.precision.ge(PrecisionPolicy::Average) } {
1788                let small = if const { P::POLICY.precision.le(PrecisionPolicy::Average) } {
1789                    // Taylor series for erf(x)/x, faster but slightly less accurate at points
1790                    x * x2.poly_rev_n_p::<P, _>(&[
1791                        0.00012055332981789664251,
1792                        -0.00085483270234508528325,
1793                        0.0052239776254421878421,
1794                        -0.026866170645131251759,
1795                        0.11283791670955125739,
1796                        -0.37612638903183752463,
1797                        1.1283791670955125739,
1798                    ])
1799                } else {
1800                    // Pade approximate for (Erf(x)-x)/x
1801                    let n = x2.poly_rev_n_p::<P, _>(&[
1802                        -2.3763017452e-05,
1803                        -5.7702702470e-03,
1804                        -2.8481749818e-02,
1805                        -3.2504209876e-01,
1806                        1.2837916613e-01,
1807                    ]);
1808
1809                    let d = x2.poly_rev_n_p::<P, _>(&[
1810                        -3.9602282413e-06,
1811                        1.3249473704e-04,
1812                        5.0813062117e-03,
1813                        6.5022252500e-02,
1814                        3.9791721106e-01,
1815                        1.0,
1816                    ]);
1817
1818                    x.mul_adde(n / d, x)
1819                };
1820
1821                y = x.cmp_lt(V::ONE).select(small, y);
1822            }
1823
1824            y | sign
1825        }
1826    }
1827}
1828
1829/// Every default applies: `expint` on the real line is what they were written for.
1830impl<V: FloatVectorWithBits<Element = f32>> super::ExpIntDetails<f32, V> for V {}
1831
1832/// Minimax fit of `L(x)/x` as a polynomial in `x^2` on `[0, 2]`, relative error
1833/// `4.1e-8` after rounding to f32 (`crates/thermite-special/scripts/langevin_coeffs.py`).
1834const LANGEVIN_SMALL_F32: [f32; 8] = [
1835    0.3333333432674408,
1836    -0.022222189232707024,
1837    0.0021162214688956738,
1838    -0.0002112639049300924,
1839    2.098539516737219e-05,
1840    -1.934508873091545e-06,
1841    1.394919024733099e-07,
1842    -5.404849012791146e-09,
1843];
1844
1845/// Minimax fit of `L^-1(y) (1 - y^2) / y` as a polynomial in `y^2` on `[0, 0.85^2]`,
1846/// relative error `7.6e-5`. The inverse's Newton seed below the `1/(1-y)` tail.
1847const LANGEVIN_SEED_F32: [f32; 5] = [
1848    2.9997715950012207,
1849    -1.1931958198547363,
1850    -0.12431719899177551,
1851    -0.007050277199596167,
1852    0.39067453145980835,
1853];
1854
1855/// The `Worst`/`Medium` forward table: same fit as [`LANGEVIN_SMALL_F32`] at degree 4,
1856/// relative error `5.1e-6` (below the Worst tier's ~3e-4 hardware reciprocal, and inside
1857/// Medium's 1e4 eps), three FMAs cheaper. The inverse keeps the full table.
1858const LANGEVIN_SMALL_F32_LO: [f32; 5] = [
1859    0.3333316445350647,
1860    -0.02220052480697632,
1861    0.0020704844500869513,
1862    -0.00017605189350433648,
1863    8.862235517881345e-06,
1864];
1865
1866/// Order dispatch for the modified Bessel entry points. `N` is a const parameter, so the
1867/// `if const` collapses to one arm and the unused table is never built.
1868///
1869/// Orders 0 and 1 are closed forms. Everything above seeds from the order-0 form and walks
1870/// the ratio recurrence down. All three arms are selected at compile time, so a call site
1871/// pays for exactly one.
1872#[inline(always)]
1873fn bessel_i_dispatch<P: Policy, V, const N: i32, const SCALED: bool>(x: V) -> V
1874where
1875    V: thermite::vector::FloatVector<Element = f32> + thermite::math::TranscendentalMathWithPolicy,
1876{
1877    if const { N == 0 } {
1878        generic::bessel::ik::bessel_i0_impl::<P, _, _, _, _, SCALED>(x, &crate::tables::bessel::BESSEL_I0_F32)
1879    } else if const { N.unsigned_abs() == 1 } {
1880        generic::bessel::ik::bessel_i1_impl::<P, _, _, _, _, SCALED>(x, &crate::tables::bessel::BESSEL_I1_F32)
1881    } else {
1882        // Orders past 1 seed from the order-0 closed form and walk the ratio recurrence down.
1883        generic::bessel::ik::bessel_in_impl::<P, f32, _, _, _, _, N, SCALED>(x, &crate::tables::bessel::BESSEL_I0_F32)
1884    }
1885}
1886
1887/// Order dispatch for the modified Bessel functions of the second kind.
1888///
1889/// Orders 0 and 1 are closed forms. Above that the recurrence runs **upward**, the opposite
1890/// of the `I` family and stable for exactly that reason: `K` is the dominant solution.
1891/// Both `K` kernels also need the `I` tables, because their small arms are
1892/// `P(x^2) - ln(x) I_0(x)` and `R(x^2) x + 1/x + ln(x) I_1(x)`.
1893#[inline(always)]
1894fn bessel_k_dispatch<P: Policy, V, const N: i32, const SCALED: bool>(x: V) -> V
1895where
1896    V: thermite::vector::FloatVector<Element = f32> + thermite::math::TranscendentalMathWithPolicy,
1897{
1898    use crate::tables::bessel::{BESSEL_I0_F32, BESSEL_I1_F32, BESSEL_K0_F32, BESSEL_K1_F32};
1899    if const { N == 0 } {
1900        generic::bessel::ik::bessel_k0_impl::<P, f32, _, _, _, _, _, _, _, _, SCALED>(x, &BESSEL_K0_F32, &BESSEL_I0_F32)
1901    } else if const { N.unsigned_abs() == 1 } {
1902        generic::bessel::ik::bessel_k1_impl::<P, f32, _, _, _, _, _, _, _, _, SCALED>(x, &BESSEL_K1_F32, &BESSEL_I1_F32)
1903    } else {
1904        // The recurrence takes the two seeds, not the tables. Each closed form infers its
1905        // own array lengths here, at the one place that already names them concretely.
1906        let k0 = generic::bessel::ik::bessel_k0_impl::<P, f32, _, _, _, _, _, _, _, _, SCALED>(
1907            x,
1908            &BESSEL_K0_F32,
1909            &BESSEL_I0_F32,
1910        );
1911        let k1 = generic::bessel::ik::bessel_k1_impl::<P, f32, _, _, _, _, _, _, _, _, SCALED>(
1912            x,
1913            &BESSEL_K1_F32,
1914            &BESSEL_I1_F32,
1915        );
1916        generic::bessel::ik::bessel_kn_recur::<f32, _, N>(x, k0, k1).1
1917    }
1918}
1919
1920/// `(I_N, I_N prime)`, sharing the order-`N-1` value the recurrence already produces.
1921///
1922/// `I_N' = I_{N-1} - (N/x) I_N`, and at `N = 0` the second term vanishes because
1923/// `I_{-1} = I_1`. One formula covers every order, with the `N = 0` case written out to
1924/// keep `0/x` from becoming `0/0` at the origin.
1925///
1926/// Scaled adds one term: `d/dx e^{-|x|}f = e^{-|x|}(f' - sgn(x) f)`.
1927#[inline(always)]
1928fn bessel_i_deriv_dispatch<P: Policy, V, const N: i32, const SCALED: bool>(x: V) -> (V, V)
1929where
1930    V: thermite::vector::FloatVector<Element = f32> + thermite::math::TranscendentalMathWithPolicy,
1931{
1932    let (prev, v) = if const { N == 0 } {
1933        (
1934            bessel_i_dispatch::<P, V, 1, SCALED>(x),
1935            bessel_i_dispatch::<P, V, 0, SCALED>(x),
1936        )
1937    } else if const { N.unsigned_abs() == 1 } {
1938        (
1939            bessel_i_dispatch::<P, V, 0, SCALED>(x),
1940            bessel_i_dispatch::<P, V, 1, SCALED>(x),
1941        )
1942    } else {
1943        generic::bessel::ik::bessel_in_pair_impl::<P, f32, _, _, _, _, N, SCALED>(
1944            x,
1945            &crate::tables::bessel::BESSEL_I0_F32,
1946        )
1947    };
1948
1949    let mut d = if const { N == 0 } {
1950        prev
1951    } else {
1952        prev - v * (V::splat(N.unsigned_abs() as f32) / x)
1953    };
1954    if const { SCALED } {
1955        d -= v.copysign(x);
1956    }
1957    (v, d)
1958}
1959
1960/// `(K_N, K_N prime)`. `K_N' = -K_{N-1} - (N/x) K_N`, and `K_{-1} = K_1`.
1961///
1962/// Scaled subtracts rather than adds, since the scaling runs the other way:
1963/// `d/dx e^{x}f = e^{x}(f' + f)`.
1964#[inline(always)]
1965fn bessel_k_deriv_dispatch<P: Policy, V, const N: i32, const SCALED: bool>(x: V) -> (V, V)
1966where
1967    V: thermite::vector::FloatVector<Element = f32> + thermite::math::TranscendentalMathWithPolicy,
1968{
1969    let (prev, v) = if const { N == 0 } {
1970        (
1971            bessel_k_dispatch::<P, V, 1, SCALED>(x),
1972            bessel_k_dispatch::<P, V, 0, SCALED>(x),
1973        )
1974    } else if const { N.unsigned_abs() == 1 } {
1975        (
1976            bessel_k_dispatch::<P, V, 0, SCALED>(x),
1977            bessel_k_dispatch::<P, V, 1, SCALED>(x),
1978        )
1979    } else {
1980        // The upward recurrence walks THROUGH order N-1 on its way to N, so the pair costs
1981        // nothing beyond returning it.
1982        let k0 = generic::bessel::ik::bessel_k0_impl::<P, f32, _, _, _, _, _, _, _, _, SCALED>(
1983            x,
1984            &crate::tables::bessel::BESSEL_K0_F32,
1985            &crate::tables::bessel::BESSEL_I0_F32,
1986        );
1987        let k1 = generic::bessel::ik::bessel_k1_impl::<P, f32, _, _, _, _, _, _, _, _, SCALED>(
1988            x,
1989            &crate::tables::bessel::BESSEL_K1_F32,
1990            &crate::tables::bessel::BESSEL_I1_F32,
1991        );
1992        generic::bessel::ik::bessel_kn_recur::<f32, _, N>(x, k0, k1)
1993    };
1994
1995    let mut d = if const { N == 0 } {
1996        -prev
1997    } else {
1998        -prev - v * (V::splat(N.unsigned_abs() as f32) / x)
1999    };
2000    if const { SCALED } {
2001        d += v;
2002    }
2003    (v, d)
2004}
Last built: 2026-09-08 21:35:55 UTC