Skip to main content

thermite_compensated/
special.rs

1use super::{Compensated, CompensatedFloatVector};
2
3use thermite::math::policy::PrecisionPolicy;
4use thermite::math::{RealMathWithPolicy, TranscendentalMathWithPolicy};
5use thermite::prelude::*;
6
7use thermite_special::specialized::{SpecializedRealPrimalMath, SpecializedRealSpecialMath, SpecializedSpecialMath};
8use thermite_special::{RealSpecialMathWithPolicy, SpecialMathWithPolicy};
9
10use crate::specialized::special::SpecializedCompensatedSpecialMath;
11
12// The elliptic kernels (`carlson`, `ellint`) are generic over any float vector whose element
13// carries the Carlson convergence threshold `(3 eps)^(1/8)`: the deviation at which the
14// 7th-order Taylor tail (`~ deviation^8`) drops below rounding. A double-double has its own
15// `eps` (`2^-104` / `2^-46`), so the threshold is recomputed for it rather than lifted from
16// the inner element: with the f64 value the tail would stop at 6.8e-16 and the whole
17// second word would be noise. About three more duplication steps per call buys the rest.
18impl thermite_special::elliptic::EllipticConsts for Compensated<f64> {
19    /// `(3 * 2^-104)^(1/8)`
20    const CARLSON_THRESH: Self = Compensated { value: 0.00014003939092283656, error: 0.0 };
21    /// `2^-14`: the `R_C` series tail `t^8/17` is then `1e-34`. Below this the `ln` arm of
22    /// the closed form loses about `eps/s` with `s ~ sqrt(t) = 0.008`, a few units of 1e-30,
23    /// which is the accuracy floor of `R_J` on this type.
24    const RC_SERIES_THRESH: Self = Compensated { value: 6.103515625e-5, error: 0.0 };
25}
26
27impl thermite_special::elliptic::EllipticConsts for Compensated<f32> {
28    /// `(3 * 2^-46)^(1/8)`
29    const CARLSON_THRESH: Self = Compensated { value: 0.021316588, error: 0.0 };
30    /// `1/128` still: the tail `1.5e-17` is below this type's `2^-46`.
31    const RC_SERIES_THRESH: Self = Compensated { value: 0.0078125, error: 0.0 };
32}
33
34// Compensated is a single-value real, so it belongs in the "primal" tier and gains the
35// value-and-derivative (`_d`) activation forms (via the trait defaults).
36impl<V: CompensatedFloatVector> SpecializedRealPrimalMath<Compensated<V::Element>> for Compensated<V>
37where
38    V: SpecialMathWithPolicy + RealSpecialMathWithPolicy + RealMathWithPolicy,
39    V: SpecializedCompensatedSpecialMath<V::Element>,
40{
41    #[inline(always)]
42    fn langevin_d<P: Policy>(self) -> (Self, Self) {
43        <V as SpecializedCompensatedSpecialMath<V::Element>>::compensated_langevin_d::<P, false>(self)
44    }
45}
46
47impl<V: CompensatedFloatVector> Compensated<V>
48where
49    V: RealMathWithPolicy,
50{
51    #[inline(always)]
52    fn erf_internal_p<P: Policy>(self) -> (Self, Self) {
53        let x = self;
54        let abs_x = x.abs();
55
56        // Series below the split, continued fraction at or above it.
57        //
58        // The series computes *erf* and erfc comes out of it as 1 - erf: the cancellation
59        // in that subtraction is what sets erfc's accuracy, and it grows with erf.
60        // erf(2) = 0.9953 costs ~8 bits, erf(3) = 0.99998 costs ~16. The continued
61        // fraction computes erfc directly with no cancellation, but its Lentz iteration
62        // count climbs as |x| falls: measured 338 double-double steps at 1.5, 202 at 2.0,
63        // 101 at 3.0, and those steps are real (the value is not final until step 316 at
64        // 1.5, so this is not a stopping-test artifact). Lowering the split buys accuracy
65        // with time, and there is no value that gets both.
66        //
67        // So the split is BOTH per type and policy-gated:
68        //
69        // - `Best` and above take `ERF_CF_SPLIT`, which is 1.5 for f64 double-double and
70        //   2 for f32 double-single. The two widths genuinely disagree: at f64 width the
71        //   continued fraction holds a flat ~2-7e-31 relative from 1.5 up, against the
72        //   series' 6.9e-31 at 1.5 and 7.2e-30 at 1.821. At f32 width the continued
73        //   fraction floor is ~1.4-2.4e-12 (400-700 ulp) and the series beats it at every
74        //   point from 1.25 to 2.25.
75        // - Below `Best`, both widths use 2. The accuracy is the historical accuracy and
76        //   nothing pays the continued fraction's iteration count uninvited.
77        //
78        // This matters beyond erf: `erfinv` refines against erfc, and needs it most
79        // exactly where it was weakest, since large x corresponds to y near 1.
80        // erfinv(0.99) lands on x = 1.821, and at f64/`Best` it improves 13.7x, from
81        // 1.16e-30 to 8.5e-32, while costing ~27,500 ns against ~4,800.
82        let split = if const { P::POLICY.precision.ge(PrecisionPolicy::Best) } {
83            V::ERF_CF_SPLIT
84        } else {
85            V::splat(<V::Element as FloatElement>::ConstInt::<2>::VALUE)
86        };
87
88        let use_series: V::Mask = abs_x.value().cmp_lt(split);
89
90        let use_only_series = use_series.all();
91        let use_only_cf = use_series.none();
92
93        // --- Init Series (erf) ---
94        // erf(x) = 2/sqrt(pi) * (x - x^3/3 + x^5/10 ...)
95        let x2 = -x.square(); // -x^2 for alternating series
96        let mut sum_s = abs_x;
97        let mut term_s = abs_x;
98
99        // --- Init Continued Fraction (erfc) ---
100        // Lentz's method vars
101        //
102        // `tiny` is the stand-in for a denominator that came out non-positive, so it only
103        // has to be negligible against any real term - but it also gets *reciprocated* on
104        // the very first step, and that is what constrains it here.
105        //
106        // `MIN_POSITIVE` cannot be used: 1/2.2e-308 is 4.5e307, and compensated
107        // multiplication splits its operands with Dekker's 2^27+1 factor, which overflows
108        // to infinity for anything past ~1.3e300. The next `f *= c * d` then produced NaN,
109        // which is why erf and erfc returned NaN for every |x| >= 3 - the entire
110        // continued-fraction tail, the only regime that reaches this code.
111        //
112        // sqrt(MIN_POSITIVE)/EPSILON leaves both the sentinel and its reciprocal far
113        // inside the splitter's range while staying utterly negligible as a floor.
114        let tiny = Self::new(V::MIN_POSITIVE.sqrt() / <V as FloatVector>::EPSILON);
115        let mut f = tiny;
116        let mut a = Self::ONE; // a_1 = 1
117        let mut c = tiny;
118        let mut d = Self::ZERO;
119        let b = abs_x; // `b` in Lentz's method is |x|
120
121        let shift = if P::POLICY.unroll_loops { 2 } else { 0 };
122        let mut i = 1;
123        let max_i = (P::POLICY.max_iterations >> shift) + 1;
124
125        // the inner loops are expensive, so avoid unnecessary work
126
127        #[rustfmt::skip]
128        let () = match (use_only_series, use_only_cf) {
129            (true, true) => {
130                // both true -> all values are NaN or Inf
131            },
132            (true, false) => while i < max_i {
133                let next_i = i + (1 << shift);
134                let prev_s = sum_s;
135
136                for k in i..next_i {
137                    // --- Series Update ---
138                    // term *= -x^2 * (2k-1) / (k * (2k+1))
139                    let k_f = k as i64;
140                    let k2_p1 = (2 * k + 1) as i64;
141                    let k2_m1 = (2 * k - 1) as i64;
142
143                    let num = FloatElement::from_int(k2_m1);
144                    let den = FloatElement::from_int(k_f * k2_p1);
145
146                    term_s *= x2 * Self::from_fraction(V::splat(num), V::splat(den));
147
148                    sum_s.accumulate_unnormalized(term_s);
149                }
150
151                if prev_s.cmp_eq(sum_s).all() {
152                    // println!("erf converged at i={}", next_i - 1);
153                    break;
154                }
155
156                i = next_i;
157            },
158            (false, true) => while i < max_i {
159                let next_i = i + (1 << shift);
160                let prev_f = f;
161
162                for k in i..next_i {
163                    // --- CF Update ---
164                    // Lentz coefficients: a_k = (k-1)/2
165                    if k > 1 {
166                        a = Self::splat(FloatElement::from_int((k - 1) as i64)) * Self::HALF;
167                    }
168
169                    // Lentz steps: D = b + a*D, C = b + a/C
170                    d = a.mul_adde(d, b); // D = b + a*D
171
172                    c = b + a / c.max(tiny); // if C<=0 -> tiny
173                    d = d.max(tiny).approx_reciprocal_p::<P>(); // if D<=0 -> tiny
174
175                    f *= c * d;
176                }
177
178                if prev_f.cmp_eq(f).all() {
179                    // println!("erfc converged at i={}", next_i - 1);
180                    break;
181                }
182
183                i = next_i;
184            },
185            (false, false) => while i < max_i {
186                let next_i = i + (1 << shift);
187                let prev_s = sum_s;
188                let prev_f = f;
189
190                for k in i..next_i {
191                    // --- Series Update ---
192                    // term *= -x^2 * (2k-1) / (k * (2k+1))
193                    let k_f = k as i64;
194                    let k2_p1 = (2 * k + 1) as i64;
195                    let k2_m1 = (2 * k - 1) as i64;
196
197                    let num = FloatElement::from_int(k2_m1);
198                    let den = FloatElement::from_int(k_f * k2_p1);
199
200                    term_s *= x2 * Self::from_fraction(V::splat(num), V::splat(den));
201
202                    let mut new_sum_s = sum_s;
203                    new_sum_s.accumulate_unnormalized(term_s);
204
205                    sum_s = use_series.select(new_sum_s, sum_s); // but avoid overflowing the sum
206
207                    // --- CF Update ---
208                    // Lentz coefficients: a_k = (k-1)/2
209                    if k > 1 {
210                        a = Self::splat(FloatElement::from_int((k - 1) as i64)) * Self::HALF;
211                    }
212
213                    // Lentz steps: D = b + a*D, C = b + a/C
214                    d = a.mul_adde(d, b); // D = b + a*D
215
216                    c = b + a / c.max(tiny); // if C<=0 -> tiny
217                    d = d.max(tiny).approx_reciprocal_p::<P>(); // if D<=0 -> tiny
218
219                    f = use_series.select(f, f * c * d);
220                }
221
222                let series_converged = use_only_cf || prev_s.cmp_eq(sum_s).all();
223                let cf_converged = use_only_series || prev_f.cmp_eq(f).all();
224
225                if series_converged && cf_converged {
226                    // println!("erf converged at i={}", next_i - 1);
227                    break;
228                }
229
230                i = next_i;
231            },
232        };
233
234        // --- Finalize ---
235
236        // 1. Result from Series, also normalizes the compensated sum
237        let res_erf_s = sum_s * Self::FRAC_2_SQRT_PI;
238
239        // 2. Result from CF (if used)
240        // erfc = e^(-x^2)/sqrt(pi) * f
241        let res_erfc_c = if use_only_series {
242            Self::ZERO // avoid doing exp if not needed
243        } else {
244            f * (-x.square()).exp_p::<P>() * Self::FRAC_1_SQRT_PI
245        };
246
247        // 3. Select based on Method
248        // If series used: erf = res_erf_s,         erfc = 1 - res_erf_s
249        // If CF used:     erf = 1 - res_erfc_c,    erfc = res_erfc_c
250
251        let erf_val = use_series.select(res_erf_s, Self::ONE - res_erfc_c);
252        let erfc_val = use_series.select(Self::ONE - res_erf_s, res_erfc_c);
253
254        // 4. Symmetry for x < 0
255        // erf(-x) = -erf(x)
256        // erfc(-x) = 2 - erfc(x)
257
258        let is_neg = x.value().is_negative();
259
260        let final_erf = erf_val.neg_c(is_neg); // is_neg.select(-erf_val, erf_val);
261        let final_erfc = is_neg.select(Self::TWO - erfc_val, erfc_val);
262
263        (final_erf, final_erfc)
264    }
265}
266
267impl<V: CompensatedFloatVector> SpecializedRealSpecialMath<Compensated<V::Element>> for Compensated<V>
268where
269    V: SpecialMathWithPolicy + RealSpecialMathWithPolicy + RealMathWithPolicy,
270    V: SpecializedCompensatedSpecialMath<V::Element>,
271{
272    #[inline(always)]
273    fn erfinv<P: Policy>(self) -> Self {
274        // High-performance erfinv using Halley's Method seeded by Winitzki's approximation.
275        // This converges in ~3 iterations for 106-bit precision.
276        // However, for very small |y|, we can do better with the Maclaurin series expansion,
277        // despite more iterations, since it avoids expensive calls to log/exp/sqrt functions.
278
279        let y = self;
280        let abs_y = y.abs();
281        let y_value = y.value();
282        let abs_y_value = abs_y.value();
283
284        if abs_y_value.cmp_le(V::MAX_ERFINV_SERIES).all() {
285            // For small |y|, use the Maclaurin series expansion for better performance,
286            // since it doesn't need to call log/exp/sqrt/etc. functions.
287
288            // Maclaurin series for erf_inv(y):
289            // erf_inv(y) = sum_{k=0 to inf} (c_k / (2k+1)) * (sqrt(pi)/2 * y)^(2k+1)
290            // where c_0 = 1, c_k = sum_{m=0 to k-1} (c_m * c_{k-1-m}) / ((m+1)(2m+1))
291
292            let w = abs_y * Self::FRAC_SQRT_PI_2; // Variable w = (sqrt(pi)/2) * |y|
293            let w2 = w.square();
294
295            let mut sum = w; // Initial term (k=0): c_0 = 1, term = w
296            let mut w_pow = w; // Stores w^(2k+1)
297
298            const MAX_COEFFS: usize = 64;
299
300            // Scalar Coefficient history buffer
301            // We need this to compute the convolution for the next c_k.
302            // 64 terms is generally sufficient for convergence where defined,
303            // though it gets slow near |y| ~ 1.
304            let mut coeffs: [Compensated<V::Element>; MAX_COEFFS] = [Element::ZERO; MAX_COEFFS];
305
306            coeffs[0] = Element::ONE; // c_0 = 1
307
308            let max_k = P::POLICY.max_iterations.min(MAX_COEFFS - 1);
309
310            for k in 1..max_k {
311                let prev = sum;
312
313                let mut c_k: Compensated<V::Element> = Element::ZERO;
314
315                for m in 0..k {
316                    // Term: (c_m * c_{k-1-m}) / ((m+1)(2m+1))
317                    let num = coeffs[m] * coeffs[k - 1 - m];
318
319                    let m_i = m as i64;
320                    let den_i = (m_i + 1) * (2 * m_i + 1);
321
322                    c_k.accumulate_unnormalized(num / <V::Element as FloatElement>::from_int(den_i));
323                }
324
325                coeffs[k] = c_k.normalize();
326
327                // Term = (c_k / (2k+1)) * w^(2k+1)
328                w_pow *= w2; // Next odd power of w
329
330                let k_term_den = (2 * k + 1) as i64;
331
332                sum.accumulate_unnormalized(Self::splat(c_k) * w_pow / V::splat(FloatElement::from_int(k_term_den)));
333
334                // Check for convergence
335                if prev.cmp_eq(sum).all() {
336                    // Restore sign: erf_inv(-y) = -erf_inv(y)
337                    sum.value = sum.value.mul_sign(y_value);
338                    sum.error = sum.error.mul_sign(y_value);
339
340                    return sum.normalize();
341                }
342            }
343
344            // The MAX_ERFINV_SERIES cutoff should guarantee convergence,
345            // but if we reach here, we fallback to Halley's method.
346            // This should be very rare, if not impossible.
347        }
348
349        // Detect singularities
350        let is_zero = abs_y_value.cmp_eq(V::ZERO);
351        let is_one = abs_y_value.cmp_eq(V::ONE);
352
353        // 1. Initial guess: the inner vector's own `erfinv`. Measured at y = 0.99 it
354        //    carries a relative 1.9e-8, about 27 bits, not the element's full 53, since
355        //    the inner kernel is tuned as an approximation rather than as a seed.
356        //
357        //    This used to be Winitzki's approximation, good to a relative 3.5e-4, about
358        //    11 bits. Halley is cubic, so 11 bits needs three passes to clear 106 and 27
359        //    needs two, and every pass costs a compensated `erf` *and* a compensated
360        //    `exp` to form f and f'. Seeding from the cheaper, far better starting point
361        //    trades a scalar `erfinv` for one of each.
362        let mut x = Self::new(abs_y_value.erfinv_p::<P>());
363
364        // 2. Halley's Method Iterations (Cubic Convergence)
365        // x_{n+1} = x_n - u / (1 + x_n * u) where u = f(x_n) / f'(x_n)
366        let skip = is_zero | is_one; // cannot be solved as roots
367
368        // Halley is cubic and the seed above already carries ~27 bits, so 27 -> 81 -> 243
369        // clears double-double (106 bits for f64, 48 for f32) in two passes, and three holds
370        // even under a policy whose scalar `erfinv` seed is only the ~11-bit Winitzki
371        // value.
372        //
373        // The cap is a cost guard rather than an accuracy knob. The equality test below
374        // cannot fire when the last bit of the correction oscillates, and that measured as
375        // the full 10,000-iteration policy budget (~12 ms against ~2 us, a 2000x cliff)
376        // on roughly 40% of arguments in [0.05, 1), every one of which had already reached
377        // its final value within three passes.
378        const MAX_HALLEY: usize = 3;
379
380        for _ in 0..P::POLICY.max_iterations.min(MAX_HALLEY) {
381            let prev_x = x;
382            // f = erf(x) - y, routed through erf = 1 - erfc so that near y = 1 the two
383            // quantities being subtracted are both *small* rather than both near 1.
384            //
385            // Measured, this changes nothing: erfinv(0.9999) sits at 3.43e-27 either way,
386            // bit for bit. The cancellation it avoids is not the one that limits this -
387            // f -> 0 at the root by definition, so some cancellation is unavoidable. Kept
388            // because it is the better-conditioned spelling and costs nothing (the kernel
389            // computes erf and erfc together).
390            //
391            // The y -> 1 shortfall was erfc's, not this subtraction's: the residual here
392            // tracks erfc's own relative error times x / (2 * erfc(x) * exp(x^2) / sqrt(pi)),
393            // which at x = 1.821 is about 0.13. erfc measured 7.2e-30 there and this
394            // measured 1.16e-30. Moving `erf_internal_p`'s regime split to 1.5 fixed both.
395            let f = (Self::ONE - abs_y) - x.erfc_p::<P>();
396
397            // f / f'(x) = f * (sqrt(pi)/2) * exp(x^2)
398            let u = f * (Self::FRAC_SQRT_PI_2 * x.square().exp_p::<P>());
399
400            // Halley step: u / (1 + x*u)
401            // Note: f''/f' = -2x, so the Halley term simplifies to this.
402            x.reduce_unnormalized(u / x.mul_adde(u, V::ONE));
403
404            if (skip | x.cmp_eq(prev_x)).all() {
405                break;
406            }
407        }
408
409        x = is_zero.select(Self::ZERO, is_one.select(Self::INFINITY, x));
410
411        // Restore sign: erf_inv(-y) = -erf_inv(y)
412        x.value = x.value.mul_sign(y_value);
413        x.error = x.error.mul_sign(y_value);
414
415        x.normalize()
416    }
417
418    // Exact identity: probit(p) = sqrt(2) * erfinv(2p - 1), with every step in
419    // compensated arithmetic (2p - 1 is an error-free transform here, and erfinv
420    // refines to full double-double precision via Halley's method).
421    #[inline(always)]
422    fn probit<P: Policy>(self) -> Self {
423        Self::erfinv::<P>(self + self - Self::ONE) * Self::SQRT_2
424    }
425
426    #[inline(always)]
427    fn lgamma_r<P: Policy>(self) -> (Self, Self) {
428        <V as SpecializedCompensatedSpecialMath<V::Element>>::compensated_lgamma_r::<P>(self)
429    }
430
431    #[inline(always)]
432    fn langevin<P: Policy>(self) -> Self {
433        <V as SpecializedCompensatedSpecialMath<V::Element>>::compensated_langevin_d::<P, false>(self).0
434    }
435
436    #[inline(always)]
437    fn langevin_1m<P: Policy>(self) -> Self {
438        <V as SpecializedCompensatedSpecialMath<V::Element>>::compensated_langevin_d::<P, true>(self).0
439    }
440
441    #[inline(always)]
442    fn inv_langevin<P: Policy>(self) -> Self {
443        let y = self.abs();
444        // Exact in compensated arithmetic, which is why the complement entry point is
445        // only a different seed here rather than a different algorithm.
446        let t = Self::ONE - y;
447        let y_val = y.value();
448        Self::inv_langevin_refine::<P>(
449            Self::new(y_val.inv_langevin_p::<P>()),
450            self,
451            y,
452            t,
453            y_val.cmp_ge(V::ONE),
454            y_val.cmp_gt(V::ONE) | y_val.is_nan(),
455        )
456    }
457
458    #[inline(always)]
459    fn inv_langevin_1m<P: Policy>(self) -> Self {
460        let y_in = Self::ONE - self;
461        let y = y_in.abs();
462        let t = y_in.value().is_negative().select(Self::ONE - y, self);
463        // The pole and the domain edge read off `t` itself: `1 - t` is exactly 1 in the
464        // leading limb for any tiny `t`, and that is a perfectly good input here.
465        let t_val = self.value();
466        Self::inv_langevin_refine::<P>(
467            Self::new(t_val.inv_langevin_1m_p::<P>()),
468            y_in,
469            y,
470            t,
471            t_val.cmp_eq(V::ZERO) | (y_in.value().is_negative() & y.value().cmp_ge(V::ONE)),
472            t_val.cmp_lt(V::ZERO) | (y_in.value().is_negative() & y.value().cmp_gt(V::ONE)) | t_val.is_nan(),
473        )
474    }
475}
476
477impl<V: CompensatedFloatVector> Compensated<V>
478where
479    V: SpecialMathWithPolicy + RealSpecialMathWithPolicy + RealMathWithPolicy,
480    V: SpecializedCompensatedSpecialMath<V::Element>,
481{
482    /// Newton in compensated arithmetic from a seed accurate to the inner width's u.
483    /// The error squares per step, so double-double needs one and double-single two.
484    /// `y_in` carries the sign, `y = |y_in|`, `t = 1 - y`, and the two masks are the
485    /// pole (`+inf`) and the domain edge (NaN under overflow checking).
486    #[inline(always)]
487    fn inv_langevin_refine<P: Policy>(
488        mut x: Self,
489        y_in: Self,
490        y: Self,
491        t: Self,
492        at_pole: V::Mask,
493        out_of_domain: V::Mask,
494    ) -> Self {
495        let mut i = 0;
496        while i < <V as SpecializedCompensatedSpecialMath<V::Element>>::INV_LANGEVIN_STEPS {
497            x = <V as SpecializedCompensatedSpecialMath<V::Element>>::compensated_inv_langevin_newton::<P>(x, y, t);
498            i += 1;
499        }
500
501        // The pole (a Newton step there is 0/0) and the domain edge.
502        x = at_pole.select(Self::INFINITY, x);
503        if const { P::POLICY.check_overflow } {
504            x = out_of_domain.select(Self::NAN, x);
505        }
506
507        x.copysign(y_in)
508    }
509}
510
511// The gamma family is blanket-implemented over `SpecializedCompensatedSpecialMath`, the
512// same way `SpecialMath` is blanket-implemented over *this* trait. Adding a gamma method
513// therefore touches only the lower rung; see `crate::specialized` for why that rung has
514// to exist at all (the two Compensated widths need different coefficients).
515impl<V: CompensatedFloatVector> SpecializedSpecialMath<Compensated<V::Element>> for Compensated<V>
516where
517    V: SpecialMathWithPolicy + RealSpecialMathWithPolicy + RealMathWithPolicy,
518    V: SpecializedCompensatedSpecialMath<V::Element>,
519{
520    type ExpIntDetails = Self;
521
522    #[inline(always)]
523    fn erf<P: Policy>(self) -> Self {
524        Self::erf_internal_p::<P>(self).0
525    }
526
527    #[inline(always)]
528    fn erfc<P: Policy>(self) -> Self {
529        Self::erf_internal_p::<P>(self).1
530    }
531
532    #[inline(always)]
533    fn tgamma<P: Policy>(self) -> Self {
534        <V as SpecializedCompensatedSpecialMath<V::Element>>::compensated_tgamma::<P>(self)
535    }
536
537    #[inline(always)]
538    fn beta<P: Policy>(a: Self, b: Self) -> Self {
539        <V as SpecializedCompensatedSpecialMath<V::Element>>::compensated_beta::<P>(a, b)
540    }
541
542    fn lambert_w<P: Policy>(self) -> (Self, Self) {
543        // Seed from the standard-precision lambert_w on the value field,
544        // then refine each branch with a single compensated Halley iteration.
545        //
546        // Halley's iteration for w*e^w = x:
547        //   ew = exp(w), f = w*ew - x, wp1 = w + 1
548        //   d = 2*wp1^2*ew - (w+2)*f
549        //   w' = w - 2*wp1*f / d
550
551        let x = self;
552        let (w0_seed, wm1_seed) = x.value.lambert_w_p::<P>();
553
554        let mut w0 = Self::new(w0_seed);
555        let mut wm1 = Self::new(wm1_seed);
556
557        // One compensated Halley step per branch
558        #[inline(always)]
559        fn halley_refine<P: Policy, W>(w: Compensated<W>, x: Compensated<W>) -> Compensated<W>
560        where
561            W: CompensatedFloatVector + RealMathWithPolicy,
562        {
563            let ew = w.exp_p::<P>();
564            let f = w.mul_sube(ew, x);
565            let wp1 = w + W::ONE;
566            let wp2 = wp1 + wp1;
567            let d = (wp1 + W::ONE).nmul_adde(f, wp2 * wp1 * ew);
568            wp2.nmul_adde(f / d, w)
569        }
570
571        w0 = halley_refine::<P, V>(w0, x);
572        wm1 = halley_refine::<P, V>(wm1, x);
573
574        // Edge cases
575        let x_val = x.value();
576        let at_branch = x_val.cmp_eq(FloatConsts::FRAC_NEG_1_E);
577        let at_zero = x_val.is_zero();
578
579        w0 = at_branch.select(Self::NEG_ONE, w0);
580        w0 = at_zero.select(Self::ZERO, w0);
581        wm1 = at_branch.select(Self::NEG_ONE, wm1);
582        wm1 = at_zero.select(Self::new(V::NEG_INFINITY), wm1);
583
584        if const { P::POLICY.check_overflow } {
585            let in_domain = x_val.cmp_ge(FloatConsts::FRAC_NEG_1_E);
586
587            w0 = in_domain.select(w0, Self::NAN);
588            w0 = x_val.cmp_eq(V::INFINITY).select(Self::INFINITY, w0);
589
590            wm1 = in_domain.select(wm1, Self::NAN);
591            wm1 = x_val.cmp_gt(V::ZERO).select(Self::NAN, wm1);
592        }
593
594        (w0, wm1)
595    }
596
597    fn lgamma<P: Policy>(self) -> Self {
598        Self::lgamma_r::<P>(self).0
599    }
600
601    #[inline(always)]
602    fn digamma<P: Policy>(self) -> Self {
603        <V as SpecializedCompensatedSpecialMath<V::Element>>::compensated_digamma::<P>(self)
604    }
605
606    #[inline(always)]
607    fn trigamma<P: Policy>(self) -> Self {
608        <V as SpecializedCompensatedSpecialMath<V::Element>>::compensated_trigamma::<P>(self)
609    }
610
611    /// `n = 0` and `n = 1` reach the tuned double-double digamma/trigamma. **`n >= 2`
612    /// returns NaN**: no double-double algorithm exists for the higher orders yet, and
613    /// silently routing through an f64-precision path would put 53 good bits in a
614    /// 106-bit container, the same reason `Compensated` refuses the shared Lanczos
615    /// tables. NaN over quiet precision loss, like the real kernel's unimplemented
616    /// regions.
617    #[inline(always)]
618    fn polygamma<P: Policy>(self, n: u32) -> Self {
619        match n {
620            0 => SpecializedSpecialMath::digamma::<P>(self),
621            1 => SpecializedSpecialMath::trigamma::<P>(self),
622            // Unimplemented, not undefined: there is no double-double algorithm for the
623            // higher orders here yet. `n` is a scalar, so this is a whole-call `todo!`
624            // rather than a NaN that would propagate silently.
625            _ => todo!("Compensated polygamma(n >= 2) has no double-double algorithm yet"),
626        }
627    }
628
629    // TEMP(bessel_j): disabled until orders beyond J_0 exist - see thermite-special/src/lib.rs.
630    //fn bessel_j<P: Policy, const N: usize>(self) -> Self {
631    //    todo!()
632    //}
633}
634
635/// Double-double is still real arithmetic, so the regime and domain rules apply
636/// unchanged - but the Lentz sentinel does not.
637impl<V: CompensatedFloatVector> thermite_special::specialized::ExpIntDetails<Compensated<V::Element>, Compensated<V>>
638    for Compensated<V>
639where
640    Compensated<V>: thermite::vector::FloatVector<Element = Compensated<V::Element>>,
641{
642    /// The default, `MIN_POSITIVE`, is reciprocated on the first Lentz step, and
643    /// 1/2.2e-308 = 4.5e307 is past the ~1.3e300 where compensated multiplication's
644    /// Dekker 2^27+1 splitter overflows to infinity - so every continued-fraction lane
645    /// came back NaN. `expint` takes the fraction for x >= 1, which is exactly where it
646    /// failed.
647    ///
648    /// Same defect and same fix as `Complex`, and as the `erf`/`erfc` tail in this crate:
649    /// a sentinel only has to be negligible as a *floor*, but this one also has to
650    /// survive being inverted.
651    #[inline(always)]
652    fn cf_tiny() -> Compensated<V> {
653        Compensated::new(V::MIN_POSITIVE.sqrt() / <V as FloatVector>::EPSILON)
654    }
655}
Last built: 2026-09-08 21:35:55 UTC