Skip to main content

thermite_special/specialized/generic/
expint.rs

1use thermite::{
2    element::FloatElementWithBits,
3    mask::GenericMask,
4    math::{
5        CoreMathWithPolicy as _, FloatConsts, TranscendentalMathWithPolicy as _,
6        policy::{Policy, PrecisionPolicy},
7    },
8    vector::FloatVectorWithBits,
9};
10
11/// How much error amplification the forward recurrence is allowed before the continued
12/// fraction takes over, in ulps of the seed.
13///
14/// The resulting worst-case relative error on the recurrence path is `AMP_CAP * eps`, i.e.
15/// about 1.4e-14 in binary64 and 7.6e-6 in binary32 - proportional in either format, which is
16/// why this is a pure count and not a function of the mantissa width.
17const AMP_CAP: f64 = 64.0;
18
19// Computes the largest x for which the forward recurrence E_1 -> E_N holds full precision.
20//
21// The recurrence E_{n+1}(x) = (e^{-x} - x*E_n(x)) / n has a homogeneous growing solution:
22// any error δ in E_1 is amplified after N-1 steps to δ * x^(N-1) / (N-1)!
23//
24// Requiring that amplification to stay under AMP_CAP gives
25//
26//   x_cf = (AMP_CAP * (N-1)!)^(1/(N-1))
27//
28// which is 64 at N = 2, 11.3 at N = 3, and settles into the 6-to-17 range for everything
29// above that (it grows like (N-1)/e). Past it, `expint_fraction_n` runs instead.
30//
31// The cap has to be a fixed number of ulp, not the whole mantissa. Solving
32// x^(N-1)/(N-1)! = 2^mantissa_bits instead runs the recurrence until the amplification has
33// consumed every bit, which leaves the answer with no correct digits at all just below the
34// threshold for any N >= 6 (8.2e-3 relative at N = 12, x = 90; 24% at N = 20, x = 51). The
35// asymptotic series on the far side needs x of 45 to 110 to converge, so that rule also
36// leaves a band for every N >= 4 where neither method works. Boost.Math avoids the question
37// entirely: it takes a continued fraction for essentially all x >= 1 and has no forward
38// recurrence.
39const fn recurrence_threshold(n: usize) -> f64 {
40    if n <= 1 {
41        return f64::MAX;
42    }
43
44    // Binary exponentiation, used for bisection below.
45    const fn const_powi_f64(mut x: f64, mut n: u32) -> f64 {
46        let mut r = 1.0f64;
47        while n > 0 {
48            if n & 1 == 1 {
49                r *= x;
50            }
51            n >>= 1;
52            if n > 0 {
53                x *= x;
54            }
55        }
56        r
57    }
58
59    let k = (n - 1) as u32;
60
61    // target = AMP_CAP * (n-1)!
62    let target: f64 = {
63        let mut f = AMP_CAP;
64        let mut i = 2usize;
65        while i < n {
66            f *= i as f64;
67            i += 1;
68        }
69        f
70    };
71
72    // Bisect for the k-th root of target. The largest value it can take is at k = 1
73    // (target itself, AMP_CAP); above that the root pulls it into the single digits, so a
74    // ceiling of AMP_CAP bounds every case.
75    let mut lo = 0.0f64;
76    let mut hi = if k == 1 { target } else { AMP_CAP };
77
78    let mut i = 0;
79    while i < 64 {
80        let mid = (lo + hi) * 0.5;
81        if const_powi_f64(mid, k) < target {
82            lo = mid;
83        } else {
84            hi = mid;
85        }
86        i += 1;
87    }
88
89    // No lower clamp is needed: the continued fraction converges for every x > 0 (more
90    // slowly as x falls, but it converges), unlike the asymptotic series this replaced,
91    // which diverged outright below x = N and forced a clamp there.
92    (lo + hi) * 0.5
93}
94
95/// Iteration cap for [`expint_fraction_n`].
96///
97/// The fraction is only entered above [`recurrence_threshold`], and the slowest case at any
98/// threshold needs 26 iterations (N = 5..10, where the threshold bottoms out near x = 6);
99/// it falls to 8 by x = 60 and 6 by x = 90. Lanes freeze as they converge and the loop
100/// exits once all of them have, so this bound is a backstop rather than a trip count.
101const CF_MAX_ITER: u32 = 48;
102
103// NOTE: When const-generics are more mature, we can have these polynomials by dynamic in size based on the
104// type of `Self`. For now, it's only the double-precision (f64) polynomials.
105pub trait ExpIntConsts<const N: usize>: FloatConsts + Sized {
106    // polynomial coefficients
107    const SMALL_N: [Self; 6];
108    const SMALL_D: [Self; 6];
109    const LARGE_N: [Self; 11];
110    const LARGE_D: [Self; 12];
111    const ASYMPTOTIC_CONST: Self;
112    const RECURRENCE_THRESHOLD: Self;
113    const ONE_OVER_N_MINUS_1: Self;
114    const FACTORS: [Self; N]; // n+2
115    const RECIPROCALS: [Self; N]; // reciprocal of factors
116
117    /// [`RECURRENCE_THRESHOLD`](Self::RECURRENCE_THRESHOLD) for every order below
118    /// [`EXPINT_TABLE_ORDERS`], precomputed: the threshold is a bisection, which the
119    /// runtime-order form must not pay per call.
120    const RECURRENCE_THRESHOLDS: [Self; EXPINT_TABLE_ORDERS];
121    /// `1/(2+k)` for `k` below [`EXPINT_TABLE_ORDERS`], the recurrence's per-step scale.
122    const RECIPROCAL_TABLE: [Self; EXPINT_TABLE_ORDERS];
123
124    /// [`RECURRENCE_THRESHOLD`](Self::RECURRENCE_THRESHOLD) for an order known only at runtime:
125    /// a table lookup, falling back to the bisection only past the table.
126    fn recurrence_threshold_dyn(n: u32) -> Self;
127}
128
129/// How many orders the runtime-order `expint` has precomputed constants for. Past this the
130/// per-step scale is a division and the threshold a bisection, per call.
131pub const EXPINT_TABLE_ORDERS: usize = 33;
132
133macro_rules! impl_expint_consts {
134    (
135        SMALL_N [ $($sn_value:literal),* $(,)? ],
136        SMALL_D [ $($sd_value:literal),* $(,)? ],
137        LARGE_N [ $($ln_value:literal),* $(,)? ],
138        LARGE_D [ $($ld_value:literal),* $(,)? ]
139    ) => {
140        impl<const N: usize> ExpIntConsts<N> for f32 {
141            const SMALL_N: [Self; 6] = [$($sn_value),*];
142            const SMALL_D: [Self; 6] = [$($sd_value),*];
143            const LARGE_N: [Self; 11] = [$($ln_value),*];
144            const LARGE_D: [Self; 12] = [$($ld_value),*];
145            const ASYMPTOTIC_CONST: Self = 0.66373538970947265625;
146            const RECURRENCE_THRESHOLD: Self = const { recurrence_threshold(N) as f32 };
147
148            const RECURRENCE_THRESHOLDS: [Self; EXPINT_TABLE_ORDERS] = {
149                let mut t = [0.0; EXPINT_TABLE_ORDERS]; let mut i = 0;
150                while i < EXPINT_TABLE_ORDERS { t[i] = recurrence_threshold(i) as f32; i += 1; }
151                t
152            };
153            const RECIPROCAL_TABLE: [Self; EXPINT_TABLE_ORDERS] = {
154                let mut r = [0.0; EXPINT_TABLE_ORDERS]; let mut i = 0;
155                while i < EXPINT_TABLE_ORDERS { r[i] = 1.0 / (2 + i) as Self; i += 1; }
156                r
157            };
158
159            #[inline(always)]
160            fn recurrence_threshold_dyn(n: u32) -> Self {
161                match <Self as ExpIntConsts<N>>::RECURRENCE_THRESHOLDS.get(n as usize) {
162                    Some(&t) => t,
163                    None => recurrence_threshold(n as usize) as f32,
164                }
165            }
166            const ONE_OVER_N_MINUS_1: Self = if N > 1 { 1.0 / (N as Self - 1.0) } else { Self::INFINITY };
167            const FACTORS: [Self; N] = {
168                let mut facts = [0.0; N]; let mut i = 0;
169                while i < N { facts[i] = (2 + i) as Self; i += 1; }
170                facts
171            };
172            const RECIPROCALS: [Self; N] = {
173                let mut r = Self::FACTORS; let mut i = 0;
174                while i < N { r[i] = 1.0 / r[i]; i += 1; }
175                r
176            };
177        }
178
179        impl<const N: usize> ExpIntConsts<N> for f64 {
180            const SMALL_N: [Self; 6] = [$($sn_value),*];
181            const SMALL_D: [Self; 6] = [$($sd_value),*];
182            const LARGE_N: [Self; 11] = [$($ln_value),*];
183            const LARGE_D: [Self; 12] = [$($ld_value),*];
184            const ASYMPTOTIC_CONST: Self = 0.66373538970947265625;
185            const RECURRENCE_THRESHOLD: Self = const { recurrence_threshold(N) };
186
187            const RECURRENCE_THRESHOLDS: [Self; EXPINT_TABLE_ORDERS] = {
188                let mut t = [0.0; EXPINT_TABLE_ORDERS]; let mut i = 0;
189                while i < EXPINT_TABLE_ORDERS { t[i] = recurrence_threshold(i); i += 1; }
190                t
191            };
192            const RECIPROCAL_TABLE: [Self; EXPINT_TABLE_ORDERS] = {
193                let mut r = [0.0; EXPINT_TABLE_ORDERS]; let mut i = 0;
194                while i < EXPINT_TABLE_ORDERS { r[i] = 1.0 / (2 + i) as Self; i += 1; }
195                r
196            };
197
198            #[inline(always)]
199            fn recurrence_threshold_dyn(n: u32) -> Self {
200                match <Self as ExpIntConsts<N>>::RECURRENCE_THRESHOLDS.get(n as usize) {
201                    Some(&t) => t,
202                    None => recurrence_threshold(n as usize),
203                }
204            }
205            const ONE_OVER_N_MINUS_1: Self = if N > 1 { 1.0 / (N as Self - 1.0) } else { Self::INFINITY };
206            const FACTORS: [Self; N] = {
207                let mut facts = [0.0; N]; let mut i = 0;
208                while i < N { facts[i] = (2 + i) as Self; i += 1; }
209                facts
210            };
211            const RECIPROCALS: [Self; N] = {
212                let mut r = Self::FACTORS; let mut i = 0;
213                while i < N { r[i] = 1.0 / r[i]; i += 1; }
214                r
215            };
216        }
217    };
218}
219
220impl_expint_consts! {
221    SMALL_N [
222        -0.000111507792921197858394,
223        -0.00399167106081113256961,
224        -0.0368031736257943745142,
225        -0.245088216639761496153,
226        0.0320913665303559189999,
227        0.0865197248079397976498,
228    ],
229    SMALL_D [
230        -0.528611029520217142048e-6,
231        0.000131049900798434683324,
232        0.00427347600017103698101,
233        0.056770677104207528384,
234        0.37091387659397013215,
235        1.0,
236    ],
237    LARGE_N [
238        -1185.45720315201027667,
239        -14751.4895786128450662,
240        -54844.4587226402067411,
241        -86273.1567711649528784,
242        -66598.2652345418633509,
243        -27182.6254466733970467,
244        -6046.8250112711035463,
245        -724.581482791462469795,
246        -43.3058660811817946037,
247        -0.999999999999998811143,
248        -0.121013190657725568138e-18,
249    ],
250    LARGE_D [
251        -0.776491285282330997549,
252        1229.20784182403048905,
253        18455.4124737722049515,
254        86722.3403467334749201,
255        180329.498380501819718,
256        192104.047790227984431,
257        113057.05869159631492,
258        38129.5594484818471461,
259        7417.37624454689546708,
260        809.193214954550328455,
261        45.3058660811801465927,
262        1.0,
263    ]
264}
265
266/// `$E_N(x)$` by its continued fraction, evaluated in modified Lentz form.
267///
268/// ```text
269/// E_n(x) = e^{-x} / (x + n - 1*n/(x + n + 2 - 2*(n+1)/(x + n + 4 - ...)))
270/// ```
271///
272/// This is what Boost.Math uses for `$E_n$` at essentially all `$x \ge 1$`, and what this
273/// kernel now uses above [`recurrence_threshold`]. It replaced an asymptotic series that
274/// could not converge in the range it was being asked to cover: measured against a 45-digit
275/// reference, the fraction holds **~1e-16 for every order from 1 to 20 at every `$x \ge 2$`**,
276/// where the series it replaced left an unreachable band for every `$N \ge 4$`.
277///
278/// Lentz's formulation is the one to use here because it never forms the convergents
279/// directly. It carries the *ratios* `c` and `d`, so nothing overflows even where the
280/// numerator and denominator separately would. The two `is_zero` guards are Lentz's own: a
281/// vanishing denominator is substituted with a tiny value, which perturbs the result by less
282/// than an ulp and keeps the recurrence going.
283///
284/// Divisions here are exact rather than `approx_div` at every tier. The policy enters through
285/// the *convergence tolerance* instead, which is the knob that actually pays: iterations, not
286/// the cost of each one.
287///
288/// # The tolerance is where the policy lives
289///
290/// The loop stops a lane once `|delta - 1|` falls under the tolerance, and each tier names
291/// **a fraction of the mantissa to keep** rather than an absolute figure, so it means the
292/// same thing in either format. Measured against a 40-digit reference at `N = 8`, `x = 6.5`:
293///
294/// | tier | tolerance | binary64 | binary32 |
295/// |---|---|---|---|
296/// | `Average` and up | `EPSILON` | 1.5e-16 | 1.3e-07 |
297/// | `Medium` | `eps^(3/4)`, three quarters | 4.4e-13 | 5.4e-07 |
298/// | `Worst` | `sqrt(eps)`, half | 1.1e-09 | 6.8e-05 |
299///
300/// Those savings land exactly where the cost is. The fraction is dearest near the threshold
301/// and converges in 6 to 8 iterations by `$x = 60$` whatever the tier, so the tiers collapse
302/// on their own where the function is easy.
303///
304/// **`Average` and above are untouched**, matching `poisson`'s `stirlerr_terms`: only the two
305/// tiers that exist to trade accuracy for speed do so, and the default policy keeps full
306/// precision. The tolerance is also floored at the format's `EPSILON`, so asking binary32 for
307/// `1e-8` does not spin the loop chasing digits it cannot represent.
308///
309/// An asymptotic series was considered for the low tiers and rejected. It is *anti-correlated
310/// with need*: at `N = 8, x = 6.1` - the same worst case above - the best it can reach is
311/// 100% relative error, because its terms grow rather than shrink until `$x > N$`. By the
312/// time it is accurate (`$x \approx 90$`) the fraction already converges in 7 iterations. It
313/// can only help where help is least needed.
314#[inline(always)]
315fn expint_fraction_n<P, E, V, const N: usize>(x: V, exp_neg_x: V) -> V
316where
317    P: Policy,
318    E: FloatElementWithBits + ExpIntConsts<N>,
319    V: FloatVectorWithBits<Element = E> + crate::specialized::SpecializedSpecialMath<E>,
320{
321    // Stated as a fraction of the format's mantissa rather than as an absolute figure, so
322    // the tiers mean the same thing in binary32 and binary64. An absolute constant does not
323    // survive the format change: 1e-8 is a real relaxation against a binary64 epsilon of
324    // 2.2e-16 and is *below* a binary32 one of 1.2e-7, so binary32 would clamp straight back
325    // to full precision and the tier would buy nothing at all.
326    let tol = if const { P::POLICY.precision.le(PrecisionPolicy::Worst) } {
327        // Half the mantissa: 1.5e-8 in binary64, 3.4e-4 in binary32.
328        <V as FloatConsts>::SQRT_EPSILON
329    } else if const { P::POLICY.precision.le(PrecisionPolicy::Medium) } {
330        // Three quarters of it: eps^(3/4) is eps^(1/2) * eps^(1/4), so the two constants
331        // FloatConsts already carries give it as a plain multiply rather than a root.
332        // 3.4e-12 in binary64, 3.6e-6 in binary32.
333        <V as FloatConsts>::SQRT_EPSILON * <V as FloatConsts>::FOURTH_ROOT_EPSILON
334    } else {
335        <V as FloatConsts>::EPSILON
336    };
337
338    let tiny = V::MIN_POSITIVE;
339    let two = V::TWO;
340    let n_large = const { N as thermite::LargeInt };
341
342    // b_0 = x + n, and the first convergent is 1/b_0. Both are positive for x > 0, so the
343    // opening reciprocal needs no guard.
344    let mut b = x + V::splat(E::from_int(n_large));
345    let mut c = V::MAX;
346    let mut d = V::ONE / b;
347    let mut h = d;
348
349    let mut active = <V::Mask as GenericMask>::TRUTHY;
350    let mut i = 1u32;
351
352    while i <= CF_MAX_ITER {
353        V::_loop_hint();
354
355        // a_i = -i(n + i - 1)
356        let a = V::splat(E::from_int(
357            -(i as thermite::LargeInt) * (n_large - 1 + i as thermite::LargeInt),
358        ));
359        b += two;
360
361        let den = a.mul_adde(d, b);
362        d = V::ONE / den.is_zero().select(tiny, den);
363
364        let num = b + a / c;
365        c = num.is_zero().select(tiny, num);
366
367        let delta = c * d;
368
369        // Frozen lanes keep the value they converged to. Continuing to multiply a converged
370        // lane by a delta that is only approximately one would walk it back off the answer.
371        h = active.select(h * delta, h);
372
373        // Converged once delta reaches one. Checked every fourth iteration so the reduction
374        // is amortized.
375        active &= (delta - V::ONE).abs().cmp_gt(tol);
376
377        if i.is_multiple_of(4) && active.none() {
378            break;
379        }
380
381        i += 1;
382    }
383
384    h * exp_neg_x
385}
386
387#[inline(always)]
388/// `$E_N(x)$` only. See [`expint_double_primal_n`] for the shape of the computation.
389pub fn expint_double_n<P: Policy, E, V, const N: usize>(x: V) -> V
390where
391    E: FloatElementWithBits + ExpIntConsts<N>,
392    V: FloatVectorWithBits<Element = E> + crate::specialized::SpecializedSpecialMath<E>,
393{
394    expint_double_primal_n::<P, E, V, N>(x).0
395}
396
397// ---- runtime-order twins ------------------------------------------------------------------
398//
399// The order-independent constants come from `ExpIntConsts<1>`. The per-order ones
400// (`FACTORS`, `RECIPROCALS`, `ONE_OVER_N_MINUS_1`, `RECURRENCE_THRESHOLD`) are the same
401// correctly rounded divisions the const impl folds, formed here per call. So the two forms
402// agree to the bit, which `tests/expint.rs` checks.
403
404/// The runtime-order twin of [`expint_fraction_n`].
405#[inline(always)]
406fn expint_fraction<P, E, V>(x: V, exp_neg_x: V, n: u32) -> V
407where
408    P: Policy,
409    E: FloatElementWithBits,
410    V: FloatVectorWithBits<Element = E> + crate::specialized::SpecializedSpecialMath<E>,
411{
412    let tol = if const { P::POLICY.precision.le(PrecisionPolicy::Worst) } {
413        <V as FloatConsts>::SQRT_EPSILON
414    } else if const { P::POLICY.precision.le(PrecisionPolicy::Medium) } {
415        <V as FloatConsts>::SQRT_EPSILON * <V as FloatConsts>::FOURTH_ROOT_EPSILON
416    } else {
417        <V as FloatConsts>::EPSILON
418    };
419
420    let tiny = V::MIN_POSITIVE;
421    let two = V::TWO;
422    let n_large = n as thermite::LargeInt;
423
424    let mut b = x + V::splat(E::from_int(n_large));
425    let mut c = V::MAX;
426    let mut d = V::ONE / b;
427    let mut h = d;
428
429    let mut active = <V::Mask as GenericMask>::TRUTHY;
430    let mut i = 1u32;
431
432    while i <= CF_MAX_ITER {
433        V::_loop_hint();
434
435        let a = V::splat(E::from_int(
436            -(i as thermite::LargeInt) * (n_large - 1 + i as thermite::LargeInt),
437        ));
438        b += two;
439
440        let den = a.mul_adde(d, b);
441        d = V::ONE / den.is_zero().select(tiny, den);
442
443        let num = b + a / c;
444        c = num.is_zero().select(tiny, num);
445
446        let delta = c * d;
447
448        h = active.select(h * delta, h);
449
450        active &= (delta - V::ONE).abs().cmp_gt(tol);
451
452        if i.is_multiple_of(4) && active.none() {
453            break;
454        }
455
456        i += 1;
457    }
458
459    h * exp_neg_x
460}
461
462/// The runtime-order twin of [`expint_double_primal_n`].
463#[inline(always)]
464pub fn expint_double_primal<P: Policy, E, V>(x: V, n: u32) -> (V, V)
465where
466    E: FloatElementWithBits + ExpIntConsts<1>,
467    V: FloatVectorWithBits<Element = E> + crate::specialized::SpecializedSpecialMath<E>,
468{
469    let exp_neg_x = (-x).exp_p::<P>();
470    let x_ex = exp_neg_x / x;
471
472    if n == 0 {
473        let mut result = x_ex;
474        let mut prev = x_ex * (V::ONE + x.approx_reciprocal_p::<P>());
475
476        if const { P::POLICY.check_overflow } {
477            let x_is_zero = x.is_zero();
478            result = x_is_zero.select(V::INFINITY, result);
479            prev = x_is_zero.select(V::INFINITY, prev);
480
481            let bad = x.cmp_lt(V::ZERO) | x.is_nan();
482            result = bad.select(V::NAN, result);
483            prev = bad.select(V::NAN, prev);
484        }
485
486        return (result, prev);
487    }
488
489    let is_large = x.cmp_gt(V::ONE);
490
491    let inv_x = x.approx_reciprocal_p::<P>();
492
493    let mut e_n = x
494        .poly_rev_n_p::<P, _>(&<E as ExpIntConsts<1>>::SMALL_N)
495        .approx_div_p::<P>(x.poly_rev_n_p::<P, _>(&<E as ExpIntConsts<1>>::SMALL_D));
496
497    let large_e1 = inv_x
498        .poly_rev_n_p::<P, _>(&<E as ExpIntConsts<1>>::LARGE_N)
499        .approx_div_p::<P>(inv_x.poly_rev_n_p::<P, _>(&<E as ExpIntConsts<1>>::LARGE_D));
500
501    e_n += x - x.ln_p::<P>() - V::splat(<E as ExpIntConsts<1>>::ASYMPTOTIC_CONST);
502
503    e_n = is_large.select((V::ONE + large_e1) * x_ex, e_n);
504
505    // Recurrence `E_1 -> E_n`, the `n = 1` step peeled as in the const form.
506    let mut e_prev = x_ex;
507
508    if n > 1 {
509        e_prev = e_n;
510        e_n = x.nmul_adde(e_n, exp_neg_x);
511
512        let mut k = 0usize;
513        while k < n as usize - 2 {
514            e_prev = e_n;
515            let f = E::from_int((2 + k) as thermite::LargeInt);
516            e_n = if const { P::POLICY.precision.ge(PrecisionPolicy::Best) } {
517                x.nmul_adde(e_n, exp_neg_x) / V::splat(f)
518            } else {
519                // The precomputed reciprocal where there is one, and the same division
520                // past the table.
521                let r = match <E as ExpIntConsts<1>>::RECIPROCAL_TABLE.get(k) {
522                    Some(&r) => r,
523                    None => E::ONE / f,
524                };
525                x.nmul_adde(e_n, exp_neg_x).scale(r)
526            };
527            k += 1;
528        }
529    }
530
531    let is_very_large = x.cmp_ge(V::splat(<E as ExpIntConsts<1>>::recurrence_threshold_dyn(n)));
532
533    if n > 1 && thermite::unlikely(is_very_large.any()) {
534        e_n = is_very_large.select(expint_fraction::<P, E, V>(x, exp_neg_x, n), e_n);
535
536        let back = (exp_neg_x - e_n.scale(E::from_int(n as thermite::LargeInt - 1))) / x;
537        e_prev = is_very_large.select(back, e_prev);
538    }
539
540    if const { P::POLICY.check_overflow } {
541        let x_is_zero = x.is_zero();
542
543        if n == 1 {
544            e_n = x_is_zero.select(V::INFINITY, e_n);
545        } else {
546            e_n = x_is_zero.select(V::splat(E::ONE / E::from_int(n as thermite::LargeInt - 1)), e_n);
547        }
548
549        if n <= 2 {
550            e_prev = x_is_zero.select(V::INFINITY, e_prev);
551        } else {
552            e_prev = x_is_zero.select(V::splat(E::ONE / E::from_int(n as thermite::LargeInt - 2)), e_prev);
553        }
554
555        let bad = x.cmp_lt(V::ZERO) | x.is_nan();
556        e_n = bad.select(V::NAN, e_n);
557        e_prev = bad.select(V::NAN, e_prev);
558    }
559
560    (e_n, e_prev)
561}
562
563/// `$E_n(x)$` only, for a runtime order. See [`expint_double_primal`].
564#[inline(always)]
565pub fn expint_double<P: Policy, E, V>(x: V, n: u32) -> V
566where
567    E: FloatElementWithBits + ExpIntConsts<1>,
568    V: FloatVectorWithBits<Element = E> + crate::specialized::SpecializedSpecialMath<E>,
569{
570    expint_double_primal::<P, E, V>(x, n).0
571}
572
573/// `$E_N(x)$` together with the adjacent lower order `$E_{N-1}(x)$`, which is
574/// `$-E_N'(x)$` by differentiation under the integral sign.
575///
576/// The lower order comes from whichever direction is stable in the regime the value
577/// itself was computed in: below [`ExpIntConsts::RECURRENCE_THRESHOLD`] the forward
578/// recurrence is running anyway, so `E_{N-1}` is just its previous iterate. Above it,
579/// where the asymptotic series takes over, the recurrence is inverted instead --
580/// `$E_{N-1}(x) = (e^{-x} - (N-1) E_N(x)) / x$`. Inverting is the *stable* direction
581/// (it damps by `1/x` where the forward one amplifies by `x`) and its only weakness,
582/// the cancellation as `x -> 0`, is unreachable here because that branch only runs for
583/// very large `x`.
584pub fn expint_double_primal_n<P: Policy, E, V, const N: usize>(x: V) -> (V, V)
585where
586    E: FloatElementWithBits + ExpIntConsts<N>,
587    V: FloatVectorWithBits<Element = E> + crate::specialized::SpecializedSpecialMath<E>,
588{
589    let exp_neg_x = (-x).exp_p::<P>();
590    let x_ex = exp_neg_x / x;
591
592    if const { N == 0 } {
593        let mut result = x_ex;
594        // E_{-1}(x) = e^-x (1 + 1/x) / x
595        let mut prev = x_ex * (V::ONE + x.approx_reciprocal_p::<P>());
596
597        if const { P::POLICY.check_overflow } {
598            let x_is_zero = x.is_zero();
599            result = x_is_zero.select(V::INFINITY, result);
600            prev = x_is_zero.select(V::INFINITY, prev);
601
602            let bad = x.cmp_lt(V::ZERO) | x.is_nan();
603            result = bad.select(V::NAN, result);
604            prev = bad.select(V::NAN, prev);
605        }
606
607        return (result, prev);
608    }
609
610    let is_large = x.cmp_gt(V::ONE);
611
612    let inv_x = x.approx_reciprocal_p::<P>();
613
614    // Coefficients from Boost.Math expint_1_rational<double> (John Maddock, BSL-1.0)
615    let mut e_n = x
616        .poly_rev_n_p::<P, _>(&E::SMALL_N)
617        .approx_div_p::<P>(x.poly_rev_n_p::<P, _>(&E::SMALL_D));
618
619    // Coefficients from Boost.Math expint_1_rational<double> (John Maddock, BSL-1.0)
620    let large_e1 = inv_x
621        .poly_rev_n_p::<P, _>(&E::LARGE_N)
622        .approx_div_p::<P>(inv_x.poly_rev_n_p::<P, _>(&E::LARGE_D));
623
624    // Equation and constant from Boost.Math expint_1_rational<double> (John Maddock, BSL-1.0)
625    e_n += x - x.ln_p::<P>() - V::splat(E::ASYMPTOTIC_CONST);
626
627    e_n = is_large.select((V::ONE + large_e1) * x_ex, e_n);
628
629    // --- Recurrence E_1 -> E_N for x < 2.5*N ---
630    // E_{n+1}(x) = (e^{-x} - x*E_n(x)) / n
631    //
632    // The n=1 step has no division (divides by 1), so it is peeled out to avoid a
633    // runtime `if n > 1` check inside the loop.
634    // One order below whatever `e_n` currently holds. The rational path above produced
635    // E_1, so before any recurrence step that is E_0 = e^-x / x.
636    let mut e_prev = x_ex;
637
638    if const { N > 1 } {
639        e_prev = e_n;
640        e_n = x.nmul_adde(e_n, exp_neg_x);
641
642        if const { N > 2 } {
643            if const { P::POLICY.precision.ge(PrecisionPolicy::Best) } {
644                let mut n = 0;
645                while n < (N - 2) {
646                    e_prev = e_n;
647                    e_n = x.nmul_adde(e_n, exp_neg_x) / V::splat(E::FACTORS[n]);
648                    n += 1;
649                }
650            } else {
651                use crunchy::unroll;
652
653                macro_rules! unroll_recurrence {
654                    ($($len:tt),*) => {
655                        $( if const { N == ($len + 2) } {
656                            unroll! { for n in 0..$len {
657                                e_prev = e_n;
658                                e_n = x.nmul_adde(e_n, exp_neg_x)
659                                    .scale(const { if n < N { E::RECIPROCALS[n] } else { E::ONE } });
660                            }}
661                        } else )* {
662                            let mut n = 0;
663                            while n < const { if N > 2 { N - 2 } else { 0 } } {
664                                e_prev = e_n;
665                                e_n = x.nmul_adde(e_n, exp_neg_x).scale(E::RECIPROCALS[n]);
666                                n += 1;
667                            }
668                        }
669                    };
670                }
671
672                unroll_recurrence!(1, 2, 3, 4, 5, 6); // up to N=8
673            }
674        }
675    }
676
677    let is_very_large = x.cmp_ge(V::splat(E::RECURRENCE_THRESHOLD));
678
679    // Past the point where the forward recurrence still holds its digits, take the
680    // continued fraction instead. See [`expint_fraction_n`] and [`recurrence_threshold`].
681    // This replaced an asymptotic series that left an unreachable band for every N >= 4.
682    if const { N > 1 } && thermite::unlikely(is_very_large.any()) {
683        e_n = is_very_large.select(expint_fraction_n::<P, E, V, N>(x, exp_neg_x), e_n);
684
685        // The forward-carried `e_prev` came from a recurrence this branch just rejected
686        // as unreliable, so re-derive it by inverting that recurrence instead:
687        //   E_N = (e^-x - x*E_{N-1}) / (N-1)  =>  E_{N-1} = (e^-x - (N-1)*E_N) / x
688        // Backward is the stable direction (it damps by 1/x where the forward one
689        // amplifies by x), and this branch only runs above the threshold, far from the
690        // x -> 0 cancellation that would otherwise spoil it.
691        let back = (exp_neg_x - e_n.scale(E::from_int(const { N as thermite::LargeInt - 1 }))) / x;
692        e_prev = is_very_large.select(back, e_prev);
693    }
694
695    if const { P::POLICY.check_overflow } {
696        // E_1(0) = +inf, E_n(0) = 1/(n-1) for n > 1
697        let x_is_zero = x.is_zero();
698
699        if const { N == 1 } {
700            e_n = x_is_zero.select(V::INFINITY, e_n);
701        } else if const { N > 1 } {
702            e_n = x_is_zero.select(V::splat(E::ONE_OVER_N_MINUS_1), e_n);
703        }
704
705        // Same rule one order down: E_0 and E_1 both diverge at zero, E_n (n >= 2) does not.
706        if const { N <= 2 } {
707            e_prev = x_is_zero.select(V::INFINITY, e_prev);
708        } else {
709            e_prev = x_is_zero.select(
710                V::splat(E::ONE / E::from_int(const { N as thermite::LargeInt - 2 })),
711                e_prev,
712            );
713        }
714
715        // Negative x: NaN, and NaN in, NaN out.
716        let bad = x.cmp_lt(V::ZERO) | x.is_nan();
717        e_n = bad.select(V::NAN, e_n);
718        e_prev = bad.select(V::NAN, e_prev);
719    }
720
721    (e_n, e_prev)
722}
Last built: 2026-09-08 21:35:55 UTC