Skip to main content

thermite_special/specialized/generic/
gamma.rs

1use thermite::{
2    element::FloatElementWithBits,
3    mask::GenericMask,
4    math::{
5        CoreMathWithPolicy as _, TranscendentalMathWithPolicy as _,
6        policy::{DenormalBehavior, Policy, PrecisionPolicy, policies::ExtraPrecision},
7    },
8    prelude::*,
9};
10
11use crate::specialized::SpecializedSpecialMath;
12use crate::tables::gamma::Lanczos;
13
14/// Shared `tgamma` implementation for all real element types.
15///
16/// Covers only the `precision >= Average` path; the low-precision shortcut through
17/// `lgamma_r` stays at the call site, because on f32 `lgamma_r` has its own
18/// low-precision Pade branch that this module deliberately does not know about.
19///
20/// * `int_cap`: the largest integer whose factorial is finite in `E` (36 for f32,
21///   172 for f64). Bounds the integer fast-path loop.
22/// * `ln_max`: `ln(E::MAX)`, the overflow threshold for the `pow` in the main term.
23#[inline(always)]
24pub fn tgamma_impl<P, E, V, const N: usize>(z_in: V, l: &Lanczos<E, N>, int_cap: E, ln_max: E) -> V
25where
26    P: Policy,
27    E: FloatElementWithBits,
28    V: FloatVectorWithBits<Element = E> + SpecializedSpecialMath<E>,
29{
30    let mut z = z_in.flush_denormals_p::<P>();
31
32    let orig_z = z;
33
34    let is_negative = z.is_negative();
35    let mut reflected = GenericMask::FALSY;
36
37    let mut res = V::ONE;
38
39    // Reflect ALL negative values via Γ(z) = -π / (z*sin(πz)*Γ(|z|))
40    // This avoids the repeated-division recurrence which accumulates rounding error.
41    if const { P::POLICY.avoid_branching } || is_negative.any() {
42        reflected = is_negative;
43        let refl_res = z * z.sin_pi_p::<P>(); // z * sin(πz)
44        res = reflected.select(refl_res, res);
45        z = z.abs();
46    }
47
48    // Negative integer poles and ±0
49    let is_neg_int = is_negative & orig_z.cmp_eq(orig_z.floor()) & orig_z.cmp_ne(V::ZERO);
50    let is_zero = orig_z.cmp_eq(V::ZERO);
51
52    // Shift z ∈ (SQRT_EPSILON, 1) up by 1 via Γ(z) = Γ(z+1)/z.
53    // The Lanczos polynomial is fit for z >= 1; evaluating below that is the
54    // primary source of error in the (0, 1) range.
55    if const { P::POLICY.precision.ge(PrecisionPolicy::Best) } {
56        let needs_shift = z.cmp_lt(V::ONE) & z.cmp_ge(V::SQRT_EPSILON);
57        res = needs_shift.select(res / z, res);
58        z = needs_shift.select(z + V::ONE, z);
59    }
60
61    // Integers (positive, after reflection)
62
63    let mut is_int = GenericMask::FALSY;
64    let mut int_res = V::ONE;
65
66    if const { P::POLICY.precision.ge(PrecisionPolicy::Best) } {
67        let zf = z.floor();
68        // Capped at int_cap - Γ overflows beyond that, and this bounds the loop.
69        is_int = zf.cmp_eq(z) & zf.cmp_lt(V::splat(int_cap)) & !is_neg_int & !is_zero;
70
71        if thermite::unlikely(is_int.any()) {
72            let mut j = V::ONE;
73            // Mask with is_int so non-integer lanes with large zf can't keep the loop alive.
74            let mut k = j.cmp_lt(zf) & is_int;
75
76            while k.any() {
77                V::_loop_hint();
78
79                int_res = k.select(int_res * j, int_res);
80                j += V::ONE;
81                k = j.cmp_lt(zf) & is_int;
82            }
83
84            if thermite::unlikely(is_int.all()) {
85                return int_res;
86            }
87        }
88    }
89
90    // Full
91
92    let gh = V::splat(l.g) - V::HALF;
93
94    // Uses the leading-term-first (reversed) Lanczos arrays - see `Lanczos`.
95    let lanczos_sum = z.poly_rev_n_p::<P, _>(&l.p_rev) / z.poly_rev_n_p::<P, _>(&l.q_rev);
96
97    let zgh = z + gh;
98    let lzgh = zgh.ln_p::<P>();
99
100    // (z * lzfg) > ln(E::MAX)
101    let very_large = (z * lzgh).cmp_gt(V::splat(ln_max));
102
103    // only compute powf once
104    let h = zgh.powf_p::<P>(very_large.select(z.mul_sube(V::HALF, V::splat(E::from_f64(0.25))), z - V::HALF));
105
106    // save a couple cycles by avoiding this division, but worst-case precision is slightly worse
107    let denom = if const { P::POLICY.precision.ge(PrecisionPolicy::Best) } {
108        lanczos_sum / zgh.exp_p::<P>()
109    } else {
110        lanczos_sum * (-zgh).exp_p::<P>()
111    };
112
113    // ASSOCIATION IS LOAD-BEARING. `h` is the half-exponent power precisely so that
114    // `zgh^(z - 1/2)` never forms as one value, and `h * h` forms it anyway: at
115    // z = 142.75 (f64) that is 10^308.8, an overflow to +inf, and `denom` (~1e-64,
116    // which would have brought it back into range) arrives too late to help. Fold
117    // `denom` in BETWEEN the two halves. Same instruction count as `(h * h) * denom`.
118    let hd = h * denom;
119    let mut normal_res = very_large.select(hd * h, hd);
120
121    if const { P::POLICY.check_overflow } {
122        // Past `int_cap` the answer exceeds the format, and the arithmetic above cannot
123        // say so on its own: `h` overflows to `inf` while `denom` underflows to 0, and
124        // `inf * 0` is NaN. Measured before this guard: `tgamma(1e30)` and `tgamma(inf)`
125        // returned **NaN**, as did everything above ~300 (f64) and ~100 (f32).
126        //
127        // `int_cap` is 172 / 36, the first integer whose factorial overflows, and the
128        // true overflow points are 171.624 and 35.040, so this cannot clip a finite
129        // result. The interval between is handled by the arithmetic, which reaches `inf`
130        // there without help.
131        //
132        // Correct for the reflected lanes too, and not by accident: a reflected result is
133        // `-pi / res`, so driving `res` to infinity gives -0.0, which is the right
134        // saturation for `Gamma` of a large negative non-integer. A NaN input compares
135        // false and passes through untouched.
136        normal_res = z.cmp_ge(V::splat(int_cap)).select(V::INFINITY, normal_res);
137    }
138
139    // Tiny
140    if const { P::POLICY.precision.ge(PrecisionPolicy::Best) } {
141        let is_tiny = z.cmp_lt(V::SQRT_EPSILON);
142        let tiny_res = z.approx_reciprocal_p::<P>() - V::EULER_GAMMA;
143        res *= is_tiny.select(tiny_res, normal_res);
144    } else {
145        res *= normal_res;
146    }
147
148    // Edge cases: Γ(-int) = NaN, Γ(±0) = ±∞
149    let zero_res = is_negative.select(V::NEG_INFINITY, V::INFINITY);
150    let result = reflected.select(-V::PI / res, is_int.select(int_res, res));
151    let mut result = is_neg_int.select(V::NAN, result);
152
153    if const {
154        P::POLICY.precision.ge(PrecisionPolicy::Best)
155            && matches!(P::POLICY.denormal_behavior, DenormalBehavior::Preserve)
156    } {
157        let is_subnormal = z.is_subnormal();
158
159        if thermite::unlikely(is_subnormal.any()) {
160            result = is_subnormal.select(V::ONE / orig_z, result);
161        }
162    }
163
164    is_zero.select(zero_res, result)
165}
166
167/// Shared `lgamma_r` implementation (log-gamma with its separate sign) for all real
168/// element types, via the `exp(g)`-scaled Lanczos sum.
169///
170/// f32 short-circuits to a cheaper Pade approximant below `Average` precision; that
171/// branch lives at the call site and never reaches here.
172#[inline(always)]
173pub fn lgamma_r_impl<P, E, V, const N: usize>(z_in: V, l: &Lanczos<E, N>) -> (V, V)
174where
175    P: Policy,
176    E: FloatElementWithBits,
177    V: FloatVectorWithBits<Element = E> + SpecializedSpecialMath<E>,
178{
179    let mut z = z_in.flush_denormals_p::<P>();
180    let mut signum = V::ONE;
181
182    let reflect = z.is_negative();
183
184    let mut t = V::ONE;
185
186    if const { P::POLICY.avoid_branching } || reflect.any() {
187        let pix = z * z.sin_pi_p::<P>(); // z * sin(pi * z)
188
189        signum |= reflect.select(pix.signed_zero(), signum);
190
191        t = reflect.select(pix.abs(), t);
192        z = z.abs();
193    }
194
195    let b = z - V::HALF;
196    let g = V::splat(l.g);
197
198    let mut lanczos_sum = z.poly_rational_n_p::<P, _, _>(&l.p_expg_scaled, &l.q);
199
200    // Full A term
201    let mut a = (b + g).ln_p::<P>() - V::ONE;
202
203    // tiny value handling
204    if const { P::POLICY.precision.gt(PrecisionPolicy::Average) } {
205        let is_not_tiny = z.cmp_ge(V::SQRT_EPSILON);
206
207        // shove the tiny result into the log down below
208        lanczos_sum = is_not_tiny.select(lanczos_sum, z.approx_reciprocal_p::<P>() - V::EULER_GAMMA);
209
210        // force multiplier to zero for tiny case, allowing the modified
211        // lanczos sum and ln(t) to be combined for cheap
212        a = a.zz(is_not_tiny);
213    }
214
215    let c = (lanczos_sum * t).ln_p::<P>();
216
217    let res = a.mul_adde(b, c);
218
219    let y = reflect.select(V::LN_PI - res, res);
220
221    (y, signum)
222}
223
224/// Shared `beta` implementation for all real element types.
225///
226/// `B(a, b) = Gamma(a)Gamma(b)/Gamma(a+b)`, evaluated from the `exp(g)`-scaled
227/// Lanczos sums directly rather than through three `tgamma` calls, so the large
228/// common factors cancel symbolically instead of overflowing.
229///
230/// Only defined for `a, b > 0`; anything else is NaN under `check_overflow`.
231#[inline(always)]
232pub fn beta_impl<P, E, V, const N: usize>(a: V, b: V, l: &Lanczos<E, N>) -> V
233where
234    P: Policy,
235    E: FloatElementWithBits,
236    V: FloatVectorWithBits<Element = E> + SpecializedSpecialMath<E>,
237{
238    let (a, b) = (a.flush_denormals_p::<P>(), b.flush_denormals_p::<P>());
239
240    let is_valid = a.cmp_gt(V::ZERO) & b.cmp_gt(V::ZERO);
241
242    if const { P::POLICY.check_overflow && !P::POLICY.avoid_branching } && is_valid.none() {
243        return V::NAN;
244    }
245
246    let c = a + b;
247
248    // if a < b then swap
249    let (a, b) = (a.max(b), a.min(b));
250
251    let mut result = a.poly_rational_n_p::<P, _, _>(&l.p_expg_scaled, &l.q)
252        * (b.poly_rational_n_p::<P, _, _>(&l.p_expg_scaled, &l.q)
253            / c.poly_rational_n_p::<P, _, _>(&l.p_expg_scaled, &l.q));
254
255    let gh = V::splat(l.g) - V::HALF;
256
257    let agh = a + gh;
258    let bgh = b + gh;
259    let cgh = c + gh;
260
261    let agh_d_cgh = agh / cgh;
262    let bgh_d_cgh = bgh / cgh;
263    let agh_p_bgh = agh * bgh;
264    let cgh_p_cgh = cgh * cgh;
265
266    let base = cgh
267        .cmp_gt(V::splat(E::from_f64(1e10)))
268        .select(agh_d_cgh * bgh_d_cgh, agh_p_bgh / cgh_p_cgh);
269
270    let denom = if const { P::POLICY.precision.gt(PrecisionPolicy::Average) } {
271        V::SQRT_E / bgh.sqrt()
272    } else {
273        // bump up the precision a little to improve beta function accuracy
274        V::SQRT_E * bgh.inverse_sqrt_p::<ExtraPrecision<P>>()
275    };
276
277    // encourage instruction-level parallelism
278    result *= agh_d_cgh.powf_p::<P>(a - V::HALF - b) * (base.powf_p::<P>(b) * denom);
279
280    if const { P::POLICY.check_overflow } {
281        result = is_valid.select(result, V::NAN);
282    }
283
284    result
285}
Last built: 2026-09-08 21:35:55 UTC