Skip to main content

thermite_compensated/specialized/special/
mod.rs

1//! The gamma family's backend for [`Compensated`], one rung below
2//! [`SpecializedSpecialMath`](thermite_special::specialized::SpecializedSpecialMath).
3//!
4//! See [the parent module](super) for why this rung exists. In short: the gamma family
5//! is the part of `thermite-special` that is driven by fitted coefficients, and the
6//! width of a `Compensated` decides which coefficients are correct - so it needs a
7//! dispatch axis that `Compensated`'s single blanket backend impl does not have.
8//!
9//! # Implementing
10//!
11//! Every method has a default, so the minimal impl is empty:
12//!
13//! ```ignore
14//! impl<V: FloatVector<Element = f64>> SpecializedCompensatedSpecialMath<Compensated<f64>>
15//!     for Compensated<V> {}
16//! ```
17//!
18//! Override a method when this width can do better than the generic series - typically
19//! by carrying a table tuned to it.
20//!
21//! # The generic algorithm
22//!
23//! The defaults are Stirling and its derivatives, which is one expansion in one set of
24//! constants for the whole family. Shift the argument up by the recurrences until it is
25//! large (`$x \gtrsim 30$` for double-double), then:
26//!
27//! ```math
28//! \ln\Gamma(x) \sim (x - \tfrac{1}{2})\ln x - x + \tfrac{1}{2}\ln 2\pi
29//!     + \sum_{n \ge 1} \frac{B_{2n}}{2n(2n-1)x^{2n-1}}
30//! ```
31//! ```math
32//! \psi(x) \sim \ln x - \frac{1}{2x} - \sum_{n \ge 1} \frac{B_{2n}}{2n\,x^{2n}}
33//! \qquad
34//! \psi_1(x) \sim \frac{1}{x} + \frac{1}{2x^2} + \sum_{n \ge 1} \frac{B_{2n}}{x^{2n+1}}
35//! ```
36//!
37//! The Bernoulli numbers are exact rationals, so unlike the minimax rationals the real
38//! `f32`/`f64` paths use, they extend to any precision without refitting - there is no
39//! oracle to chase and no table to source. Roughly 13 terms clear `$2^{-106}$` at
40//! `$x > 30$`, about half that for double-single, so the term count is a `const` off the
41//! mantissa width rather than a fixed loop.
42//!
43//! This is why `Compensated` may never want Lanczos. The real paths use it because it
44//! skips the shift loop; here every operation is already an order of magnitude more
45//! expensive, so the loop costs relatively less and a 24-coefficient table at 32 digits
46//! costs a lot to source and validate.
47
48mod pd;
49mod ps;
50
51use thermite::math::policy::Policy;
52use thermite::math::{FloatConsts, TranscendentalMathWithPolicy};
53use thermite::prelude::*;
54
55use crate::Compensated;
56
57/// What a default body needs of `Compensated<V>` in order to do compensated arithmetic.
58///
59/// Requested per method rather than as a supertrait of
60/// [`SpecializedCompensatedSpecialMath`] - see that trait's docs for why the difference
61/// matters.
62pub trait CompensatedGammaOps: FloatVector + TranscendentalMathWithPolicy {}
63impl<T> CompensatedGammaOps for T where T: FloatVector + TranscendentalMathWithPolicy {}
64
65/// Argument the shift loop drives `z` up to before the asymptotic series is used.
66///
67/// 30 rather than 20 trades ten more shift steps for two fewer series terms: the
68/// double-double case needs 13 coefficients at 20 and 11 at 30. Shift steps are one
69/// multiply or divide each, series terms are a multiply-add plus a constant, and going
70/// further out (40) starts costing more in the loop than it saves in the tail.
71///
72/// Numerator size is *not* a constraint on this choice - `CompensatedConstRatio`
73/// evaluates the ratio in f64 before splitting it across the two limbs, so a coefficient
74/// like B_24's 236364091 is carried exactly even at double-single width.
75const SHIFT_TARGET: i64 = 30;
76
77/// Compile-time rational `N/D`, split across both limbs.
78///
79/// `CompensatedConstRatio` does the splitting at const time, so the coefficients cost no
80/// runtime division and carry the full double-double value of the ratio - not the ratio
81/// rounded to the element type first.
82#[inline(always)]
83fn frac<C: FloatVector, const N: i64, const D: i64>() -> C {
84    C::splat(const { <C::Element as FloatElement>::ConstRatio::<N, D>::VALUE })
85}
86
87/// Compile-time integer `N`, same mechanism.
88#[inline(always)]
89fn int_frac<C: FloatVector, const N: i64>() -> C {
90    C::splat(const { <C::Element as FloatElement>::ConstInt::<N>::VALUE })
91}
92
93/// `$\sum_{n\ge1} rac{B_{2n}}{2n(2n-1)} w^{n-1}$`, Horner in `$w = 1/z^2$`.
94///
95/// The coefficients are exact rationals of small integers - no floating-point literals
96/// anywhere - so one table serves every width. Emitted highest-order first, which is
97/// what Horner wants and also what makes truncating cheap: a narrower type only needs
98/// the last few, and dropping leading terms is exactly what starting the accumulator at
99/// zero does.
100///
101/// Double-double needs all eleven at `z >= 30`; double-single needs three. Both are
102/// evaluated for now, which costs the narrow case a few multiply-adds it does not need.
103#[inline(always)]
104fn stirling_series<C: FloatVector>(w: C) -> C {
105    let mut acc = <C as NumericVector>::ZERO;
106
107    macro_rules! horner {
108        ($(($n:literal, $d:literal)),* $(,)?) => {
109            $( acc = acc.mul_add(w, frac::<C, $n, $d>()); )*
110        };
111    }
112
113    horner!(
114        (77683, 5796),
115        (-174611, 125400),
116        (43867, 244188),
117        (-3617, 122400),
118        (1, 156),
119        (-691, 360360),
120        (1, 1188),
121        (-1, 1680),
122        (1, 1260),
123        (-1, 360),
124        (1, 12),
125    );
126
127    acc
128}
129
130/// `$\sum_{n\ge1} rac{B_{2n}}{2n} w^{n-1}$` for digamma, Horner in `$w = 1/z^2$`.
131///
132/// Eleven terms, the same reach as the Stirling series at the same shift target.
133#[inline(always)]
134fn digamma_series<C: FloatVector>(w: C) -> C {
135    let mut acc = <C as NumericVector>::ZERO;
136
137    macro_rules! horner {
138        ($(($n:literal, $d:literal)),* $(,)?) => {
139            $( acc = acc.mul_add(w, frac::<C, $n, $d>()); )*
140        };
141    }
142
143    horner!(
144        (77683, 276),
145        (-174611, 6600),
146        (43867, 14364),
147        (-3617, 8160),
148        (1, 12),
149        (-691, 32760),
150        (1, 132),
151        (-1, 240),
152        (1, 252),
153        (-1, 120),
154        (1, 12),
155    );
156
157    acc
158}
159
160/// `$\sum_{n\ge1} B_{2n} w^{n-1}$` for trigamma, Horner in `$w = 1/z^2$`.
161///
162/// Twelve terms rather than eleven: trigamma's coefficients are the bare Bernoulli
163/// numbers, without the `$1/2n$` or `$1/2n(2n-1)$` damping the other two series get, so
164/// the tail decays one term slower.
165#[inline(always)]
166fn trigamma_series<C: FloatVector>(w: C) -> C {
167    let mut acc = <C as NumericVector>::ZERO;
168
169    macro_rules! horner {
170        ($(($n:literal, $d:literal)),* $(,)?) => {
171            $( acc = acc.mul_add(w, frac::<C, $n, $d>()); )*
172        };
173    }
174
175    horner!(
176        (-236364091, 2730),
177        (854513, 138),
178        (-174611, 330),
179        (43867, 798),
180        (-3617, 510),
181        (7, 6),
182        (-691, 2730),
183        (5, 66),
184        (-1, 30),
185        (1, 42),
186        (-1, 30),
187        (1, 6),
188    );
189
190    acc
191}
192
193/// Backend for the coefficient-bearing part of `Compensated`'s special math.
194///
195/// # Implemented on the inner vector, not on `Compensated`
196///
197/// This is implemented for `V`, with `E = V::Element` (`f32` or `f64`) as the dispatch
198/// tag, and its methods take `Compensated<Self>` by argument rather than by `self`. That
199/// looks backwards for a math trait and is load-bearing.
200///
201/// The natural spelling - implement it for `Compensated<V>`, take `self`, and give it
202/// `FloatVector<Element = E>` as a supertrait so that default bodies can do arithmetic -
203/// does not work. Naming that supertrait asserts the projection
204/// `<Compensated<V> as GenericVector>::Element == Compensated<V::Element>` at every use
205/// of the bound, and `crate::special`'s seam then normalizes through *that* rather than
206/// through `CompensatedFloatVector`, losing the `Mask: CastMask<..>` obligations its
207/// `erf` / `erfinv` / `lambert_w` bodies depend on. It surfaces a hundred lines away as
208/// unrelated `mismatched types` errors in code that was never touched.
209///
210/// Hanging the trait off `V` avoids that entirely: the seam then constrains `V`, which
211/// cannot say anything about `Compensated<V>`'s projections. What a default body needs
212/// is requested per method via [`CompensatedGammaOps`], scoped to that method alone -
213/// which is what makes real default bodies possible here at all.
214///
215/// The element parameter is also what keeps the two per-width impls from colliding:
216/// without it both would be `impl<V> .. for V`, differing only in `V::Element`, which
217/// coherence does not accept as disjoint.
218pub trait SpecializedCompensatedSpecialMath<E>: Sized {
219    /// `$\Gamma(x)$`.
220    ///
221    /// Exponentiates [`compensated_lgamma_r`](Self::compensated_lgamma_r) rather than
222    /// running its own reduction, which costs a few bits and saves a second copy of the
223    /// reflection: an absolute error `d` in `$\ln\Gamma$` is a *relative* error `d` in
224    /// `$\Gamma$`, so the loss is `$\log_2|\ln\Gamma(x)|$` bits - about 6 near x = 30 and
225    /// 10 at the overflow edge, out of 106. Avoiding it entirely means a direct Stirling
226    /// for `$\Gamma$`, which is only worth writing if those bits are ever missed.
227    #[inline(always)]
228    fn compensated_tgamma<P: Policy>(x: Compensated<Self>) -> Compensated<Self>
229    where
230        Compensated<Self>: CompensatedGammaOps,
231    {
232        let (lg, sign) = Self::compensated_lgamma_r::<P>(x);
233
234        sign * lg.exp_p::<P>()
235    }
236
237    /// `$(\ln|\Gamma(x)|, \operatorname{sign}\Gamma(x))$`.
238    ///
239    /// The sign is carried separately because `lgamma` discards it and `beta` needs it.
240    ///
241    /// Shift-and-Stirling, with the reflection below `1/2`. See the module docs for the
242    /// expansion and for why the shift target is 30.
243    #[inline(always)]
244    fn compensated_lgamma_r<P: Policy>(x: Compensated<Self>) -> (Compensated<Self>, Compensated<Self>)
245    where
246        Compensated<Self>: CompensatedGammaOps,
247    {
248        let one = <Compensated<Self> as NumericVector>::ONE;
249        let half = frac::<Compensated<Self>, 1, 2>();
250
251        // Below 1/2 the series is useless, so evaluate at 1 - x and reflect afterwards.
252        let reflect = x.cmp_lt(half);
253        let z0 = reflect.select(one - x, x);
254
255        // Shift up to the target, accumulating the divided-out product rather than its
256        // log: one `ln` at the end instead of thirty. z0 >= 1/2 here, so 30 steps always
257        // suffice, and the product tops out around 3e31 - nowhere near overflow.
258        let target = int_frac::<Compensated<Self>, SHIFT_TARGET>();
259        let mut z = z0;
260        let mut prod = one;
261
262        let mut i = 0;
263        while i < SHIFT_TARGET {
264            let shifting = z.cmp_lt(target);
265            prod = prod.mul_c(shifting, z);
266            z = z.add_c(shifting, one);
267            i += 1;
268        }
269
270        // Stirling: (z - 1/2) ln z - z + ln(2pi)/2 + poly_n(1/z^2)/z
271        let w = one / (z * z);
272        let poly = stirling_series::<Compensated<Self>>(w);
273
274        let half_ln_tau = (<Compensated<Self> as FloatConsts>::LN_2 + <Compensated<Self> as FloatConsts>::LN_PI) * half;
275        let stirling = (z - half).mul_add(z.ln_p::<P>(), half_ln_tau - z) + poly / z;
276
277        let lg = stirling - prod.ln_p::<P>();
278
279        // Reflection: ln|Gamma(x)| = ln(pi) - ln|sin(pi x)| - ln|Gamma(1 - x)|, and
280        // sign(Gamma(x)) = sign(sin(pi x)) since Gamma(1 - x) > 0 for x < 1/2. The poles
281        // at the non-positive integers fall out on their own: sin(pi x) is zero there, so
282        // the log is -inf and the result is +inf.
283        let sp = x.sin_pi_p::<P>();
284        let reflected = (<Compensated<Self> as FloatConsts>::LN_PI - sp.abs().ln_p::<P>()) - lg;
285
286        let mut value = reflect.select(reflected, lg);
287        let sign = one.neg_c(reflect & sp.is_negative());
288
289        // The poles at the non-positive integers have to be selected in rather than left
290        // to `ln(0) = -inf` propagating through the reflection. Infinities do not survive
291        // compensated arithmetic: `two_sum(finite, inf)` evaluates `inf - inf` while
292        // forming the error word, so the pair normalizes to NaN rather than to infinity.
293        let zero = <Compensated<Self> as NumericVector>::ZERO;
294        let is_pole = reflect & x.cmp_le(zero) & x.cmp_eq(x.floor());
295        value = is_pole.select(<Compensated<Self> as FloatVector>::INFINITY, value);
296
297        (value, sign)
298    }
299
300    /// `$\psi(x)$`, the digamma function.
301    ///
302    /// Same shape as [`compensated_lgamma_r`](Self::compensated_lgamma_r) - shift up,
303    /// then the asymptotic series - but the recurrence `$\psi(x) = \psi(x+1) - 1/x$`
304    /// accumulates a *sum* of reciprocals rather than a product, so it cannot be deferred
305    /// to a single log at the end.
306    #[inline(always)]
307    fn compensated_digamma<P: Policy>(x: Compensated<Self>) -> Compensated<Self>
308    where
309        Compensated<Self>: CompensatedGammaOps,
310    {
311        let one = <Compensated<Self> as NumericVector>::ONE;
312        let half = frac::<Compensated<Self>, 1, 2>();
313
314        let reflect = x.cmp_lt(half);
315        let z0 = reflect.select(one - x, x);
316
317        let target = int_frac::<Compensated<Self>, SHIFT_TARGET>();
318        let mut z = z0;
319        let mut acc = <Compensated<Self> as NumericVector>::ZERO;
320
321        let mut i = 0;
322        while i < SHIFT_TARGET {
323            let shifting = z.cmp_lt(target);
324            acc = acc.add_c(shifting, one / z);
325            z = z.add_c(shifting, one);
326            i += 1;
327        }
328
329        // psi(z) ~ ln z - 1/(2z) - sum B_2n/(2n z^2n), the sum being w * horner(w).
330        let w = one / (z * z);
331        let psi = (z.ln_p::<P>() - half / z) - w * digamma_series::<Compensated<Self>>(w);
332
333        let value = psi - acc;
334
335        // psi(x) = psi(1 - x) - pi cot(pi x). Both halves of the cotangent come out of one
336        // reduction, and it is exactly the poles of `sin_pi` that carry psi's own poles.
337        let (sp, cp) = x.sincos_pi_p::<P>();
338        let reflected = value - <Compensated<Self> as FloatConsts>::PI * (cp / sp);
339
340        reflect.select(reflected, value)
341    }
342
343    /// `$\psi_1(x)$`, the trigamma function.
344    ///
345    /// As [`compensated_digamma`](Self::compensated_digamma), with the recurrence
346    /// `$\psi_1(x) = \psi_1(x+1) + 1/x^2$` and the reflection
347    /// `$\psi_1(x) + \psi_1(1-x) = \pi^2/\sin^2(\pi x)$`. Note the reflection *adds*
348    /// rather than subtracting, unlike digamma's.
349    #[inline(always)]
350    fn compensated_trigamma<P: Policy>(x: Compensated<Self>) -> Compensated<Self>
351    where
352        Compensated<Self>: CompensatedGammaOps,
353    {
354        let one = <Compensated<Self> as NumericVector>::ONE;
355        let half = frac::<Compensated<Self>, 1, 2>();
356
357        let reflect = x.cmp_lt(half);
358        let z0 = reflect.select(one - x, x);
359
360        let target = int_frac::<Compensated<Self>, SHIFT_TARGET>();
361        let mut z = z0;
362        let mut acc = <Compensated<Self> as NumericVector>::ZERO;
363
364        let mut i = 0;
365        while i < SHIFT_TARGET {
366            let shifting = z.cmp_lt(target);
367            acc = acc.add_c(shifting, one / (z * z));
368            z = z.add_c(shifting, one);
369            i += 1;
370        }
371
372        // psi_1(z) ~ (1 + 1/(2z) + w*horner(w)) / z, with w = 1/z^2.
373        let w = one / (z * z);
374        let psi1 = (one + half / z + w * trigamma_series::<Compensated<Self>>(w)) / z;
375
376        let value = psi1 + acc;
377
378        let sp = x.sin_pi_p::<P>();
379        let reflected = <Compensated<Self> as FloatConsts>::PI_SQUARED / (sp * sp) - value;
380
381        reflect.select(reflected, value)
382    }
383
384    /// `$B(a, b) = \Gamma(a)\Gamma(b)/\Gamma(a+b)$`.
385    ///
386    /// Through logs rather than as a ratio of gammas, which overflows for arguments the
387    /// beta function itself handles perfectly well. Rides entirely on
388    /// [`compensated_lgamma_r`](Self::compensated_lgamma_r), so a width that overrides
389    /// that one gets this for free and should never need to touch this.
390    #[inline(always)]
391    fn compensated_beta<P: Policy>(a: Compensated<Self>, b: Compensated<Self>) -> Compensated<Self>
392    where
393        Compensated<Self>: CompensatedGammaOps,
394    {
395        let (la, sa) = Self::compensated_lgamma_r::<P>(a);
396        let (lb, sb) = Self::compensated_lgamma_r::<P>(b);
397        let (lab, sab) = Self::compensated_lgamma_r::<P>(a + b);
398
399        ((la + lb) - lab).exp_p::<P>() * ((sa * sb) / sab)
400    }
401
402    // --- Langevin ---
403
404    /// A double-double literal `(hi, lo)` splat at this width. The Langevin table
405    /// below is fitted at double-double, so this is a plain splat of both limbs for
406    /// `f64` and a re-split of `hi` for `f32`.
407    fn dd_const(hi: f64, lo: f64) -> Compensated<Self>;
408
409    /// Newton steps [`compensated_inv_langevin_newton`](Self::compensated_inv_langevin_newton)
410    /// needs from the inner vector's own `inv_langevin` (`~u` of that width) to reach
411    /// this width: one for double-double, two for double-single.
412    const INV_LANGEVIN_STEPS: usize;
413
414    /// `L(x)` (or `1 - L(x)` with `ONE_MINUS`) and `L'(x)`. Same structure as
415    /// `thermite-special`'s kernel with the crossover at `|x| = 1` (`3u/x^2` of
416    /// cancellation is 3 ulp there, and the double-double table is 22 terms already).
417    /// The complement on the large branch is `1/x - 2q/(1-q)`, which at worst (x = 1)
418    /// cancels to 0.69 of `1/x`.
419    #[inline(always)]
420    fn compensated_langevin_d<P: Policy, const ONE_MINUS: bool>(
421        x: Compensated<Self>,
422    ) -> (Compensated<Self>, Compensated<Self>)
423    where
424        Compensated<Self>: CompensatedGammaOps,
425    {
426        let one = <Compensated<Self> as NumericVector>::ONE;
427        let ax = x.abs();
428        let is_small = ax.cmp_le(one);
429
430        let p = Self::langevin_small_poly_n(x * x);
431        let l_small = x * p;
432        let mut dl = l_small.nmul_add(l_small, p.nmul_add(one + one, one));
433        let mut l = if const { ONE_MINUS } { one - l_small } else { l_small };
434
435        if const { P::POLICY.avoid_branching } || !is_small.all() {
436            let (rcp, w, csch2) = Self::langevin_large_parts::<P>(ax);
437            let lpos = (one - rcp) + w;
438            let big = if const { ONE_MINUS } {
439                x.select_negative(one + lpos, rcp - w)
440            } else {
441                lpos.copysign(x)
442            };
443            l = is_small.select(l, big);
444            dl = is_small.select(dl, rcp.mul_sub(rcp, csch2));
445        }
446
447        (l, dl)
448    }
449
450    /// One Newton step of `L(x) = y` from `x`, with `t = 1 - y` supplied exactly (the
451    /// residual is `((1-y) - 1/x) + 2q/(1-q)` on the large branch, which is what keeps
452    /// the step accurate where `L` sits within an ulp of 1).
453    #[inline(always)]
454    fn compensated_inv_langevin_newton<P: Policy>(
455        x: Compensated<Self>,
456        y: Compensated<Self>,
457        t: Compensated<Self>,
458    ) -> Compensated<Self>
459    where
460        Compensated<Self>: CompensatedGammaOps,
461    {
462        let one = <Compensated<Self> as NumericVector>::ONE;
463        let is_small = x.cmp_le(one);
464
465        let p = Self::langevin_small_poly_n(x * x);
466        let l = x * p;
467        let mut r = l - y;
468        let mut dl = l.nmul_add(l, p.nmul_add(one + one, one));
469
470        if const { P::POLICY.avoid_branching } || !is_small.all() {
471            let (rcp, w, csch2) = Self::langevin_large_parts::<P>(x);
472            r = is_small.select(r, (t - rcp) + w);
473            dl = is_small.select(dl, rcp.mul_sub(rcp, csch2));
474        }
475
476        x - r / dl
477    }
478
479    /// Minimax fit of `L(x)/x` in `x^2` on `[0, 1]` at double-double (relative error
480    /// `3e-36`, `crates/thermite-special/scripts/langevin_coeffs_dd.py`), Horner.
481    #[inline(always)]
482    fn langevin_small_poly_n(t: Compensated<Self>) -> Compensated<Self>
483    where
484        Compensated<Self>: CompensatedGammaOps,
485    {
486        let mut acc = <Compensated<Self> as NumericVector>::ZERO;
487
488        macro_rules! horner {
489            ($(($hi:literal, $lo:literal)),* $(,)?) => {
490                $( acc = acc.mul_add(t, Self::dd_const($hi, $lo)); )*
491            };
492        }
493
494        horner!(
495            (-9.11633225690645e-23, -2.504399575470363e-39),
496            (1.9025424779056182e-21, 5.868781095916726e-38),
497            (-2.3916673923044965e-20, 2.1419731321638627e-37),
498            (2.523435541202226e-19, -6.080654083866406e-36),
499            (-2.526330105697189e-18, -1.8885083563769902e-34),
500            (2.4991707970892547e-17, 6.097177114288303e-34),
501            (-2.467294162067776e-16, -2.537473496829264e-33),
502            (2.4351898573469184e-15, 8.529618682040395e-32),
503            (-2.40344120482019e-14, 1.4293725238332017e-30),
504            (2.3721017244813595e-13, 2.468717598005709e-29),
505            (-2.341170681396468e-12, -1.4496933593157017e-28),
506            (2.3106432598827537e-11, 5.519082276699843e-28),
507            (-2.2805151204588079e-10, 3.688676935569084e-27),
508            (2.250784651680892e-09, -1.5678904023044363e-25),
509            (-2.2214608789979678e-08, 4.0602449852842407e-26),
510            (2.1925947851873778e-07, -5.669610792596665e-25),
511            (-2.1644042808063972e-06, 1.44134557203705e-23),
512            (2.1377799155576935e-05, -1.2363216969621178e-21),
513            (-0.00021164021164021165, 8.851449492739956e-21),
514            (0.0021164021164021165, -1.427246034469328e-19),
515            (-0.022222222222222223, 8.480870326997734e-19),
516            (0.3333333333333333, 1.850371707708594e-17),
517        );
518
519        acc
520    }
521
522    /// `1/x`, `2q/(1-q)` and `csch^2(x)` for `x >= 1`, one division between them
523    /// (`r = 1/(x(1-q))`, `1/x = (1-q) r`, `1/(1-q) = x r`). At `x = inf` the products
524    /// are `inf * 0`, so those lanes are set to their limits explicitly.
525    #[inline(always)]
526    fn langevin_large_parts<P: Policy>(
527        x: Compensated<Self>,
528    ) -> (Compensated<Self>, Compensated<Self>, Compensated<Self>)
529    where
530        Compensated<Self>: CompensatedGammaOps,
531    {
532        let one = <Compensated<Self> as NumericVector>::ONE;
533        let zero = <Compensated<Self> as NumericVector>::ZERO;
534
535        let q = (-(x + x)).exp_p::<P>();
536        let omq = one - q;
537        let r = one / (x * omq);
538        let rcp = omq * r;
539        let d = x * r;
540        let w = (q + q) * d;
541        let csch2 = w * (d + d);
542
543        let inf = x.cmp_eq(<Compensated<Self> as FloatVector>::INFINITY);
544        (inf.select(zero, rcp), inf.select(zero, w), inf.select(zero, csch2))
545    }
546}
Last built: 2026-09-08 21:35:55 UTC