Skip to main content

thermite_special/specialized/generic/bessel/
ik.rs

1//! Modified Bessel functions of the first kind, orders 0 and 1, scaled and unscaled.
2//!
3//! # Two regions, not four
4//!
5//! `$I_0$` and `$I_1$` split at `x = 7.75` and nowhere else: an ascending series in
6//! `$a = x^2/4$` below, and `$e^{x} P(1/x)/\sqrt{x}$` above. That is the whole shape.
7//!
8//! The obvious reference for a SIMD Bessel is fdlibm, and fdlibm is the wrong model. Its
9//! `j0f` splits the _asymptotic envelope alone_ into four sub-intervals with a rational
10//! apiece, which is optimal when a branch picks one and skips the rest, and pathological
11//! here. A vector unit evaluates all four and discards three.
12//! The tables underneath this kernel take Boost's route instead: fewer regions, higher
13//! degree in each. Same trade the Faddeeva kernel makes for the same reason.
14//!
15//! The scalar sources carry a third region near the top of the range. It is not an accuracy
16//! region. It exists so `$e^x$` cannot overflow before `$/\sqrt{x}$` brings the product back
17//! down, and is reached here only under [`thermite::unlikely`], so the common path pays one
18//! compare.
19//!
20//! f32 `$I_0$` is the one exception, and there the far fit is genuine: its `large` minimax
21//! is fitted over `[7.75, 50]` and its constant term is wrong in the seventh digit, so
22//! extending it to infinity costs about 16 ulp. See [`crate::tables::bessel`].
23//!
24//! # The scaled forms are the cheaper ones
25//!
26//! `$e^{-x} I_n(x)$` is not a wrapper that multiplies an exponential back out. Above 7.75
27//! the tables _are_ the scaled value, so the scaled entry points skip the exponential
28//! entirely and the unscaled ones pay for it. Below 7.75 the relationship inverts. Each form
29//! therefore costs one transcendental in exactly one of its two arms, and neither is built
30//! from the other. That is the relationship `zetac` has to `zeta`, for the same reason
31//! (`$I_0(800)$` overflows f64 while `$e^{-800}I_0(800)$` is a perfectly ordinary `0.0141`).
32
33use thermite::{
34    math::{
35        PrimalProjection, TranscendentalMathWithPolicy,
36        policy::{Policy, PrecisionPolicy},
37    },
38    prelude::*,
39};
40
41use thermite::element::FloatElement;
42
43use crate::specialized::BesselDetails;
44use crate::tables::bessel::{BesselI, BesselK};
45
46/// `$I_0(x)$`, or `$e^{-|x|} I_0(x)$` when `SCALED`.
47///
48/// Even in `x`, so the sign is dropped up front and never restored.
49#[inline(always)]
50pub fn bessel_i0_impl<P, V, const NS: usize, const NL: usize, const NF: usize, const SCALED: bool>(
51    x: V,
52    t: &BesselI<V::Element, NS, NL, NF>,
53) -> V
54where
55    V: FloatVector + TranscendentalMathWithPolicy,
56    P: Policy,
57{
58    let ax = x.abs();
59    let small = ax.cmp_lt(V::splat(t.small_threshold));
60
61    // Ascending series, `1 + a P(a)` with `a = x^2/4`. All terms positive: no cancellation
62    // anywhere in this arm, at any x it is used for.
63    let h = ax * V::HALF;
64    let a = h * h;
65    let mut lo = a.mul_adde(a.poly_n_p::<P, NS>(&t.small), V::ONE);
66    if const { SCALED } {
67        lo *= (-ax).exp_p::<P>();
68    }
69
70    // Asymptotic envelope. The tables give the _scaled_ value directly.
71    let inv = V::ONE / ax;
72    let mut hi = inv.poly_n_p::<P, NL>(&t.large) / ax.sqrt();
73
74    // One mask, one reduction, reused for both the polynomial swap and the exponential
75    // assembly below. The two are the same region and must not test it twice.
76    let far = ax.cmp_ge(V::splat(t.far_threshold));
77    let any_far = thermite::unlikely(far.any());
78    if any_far {
79        hi = far.select(inv.poly_n_p::<P, NF>(&t.far) / ax.sqrt(), hi);
80    }
81
82    if const { !SCALED } {
83        // One `exp` in the common case. Past `far_threshold` the exponential is halved and
84        // applied twice, which is what keeps `exp(x)` from reaching infinity before the
85        // `1/sqrt(x)` and the sub-unit polynomial can bring it back down.
86        let full = ax.exp_p::<P>();
87        hi = if any_far {
88            let half = (ax * V::HALF).exp_p::<P>();
89            far.select((hi * half) * half, hi * full)
90        } else {
91            hi * full
92        };
93    }
94
95    small.select(lo, hi)
96}
97
98/// `$I_1(x)$`, or `$e^{-|x|} I_1(x)$` when `SCALED`.
99///
100/// Odd in `x`: computed on `|x|` and signed at the end, so the small arm keeps its
101/// all-positive series and the large arm keeps a positive reciprocal.
102#[inline(always)]
103pub fn bessel_i1_impl<P, V, const NS: usize, const NL: usize, const NF: usize, const SCALED: bool>(
104    x: V,
105    t: &BesselI<V::Element, NS, NL, NF>,
106) -> V
107where
108    V: FloatVector + TranscendentalMathWithPolicy,
109    P: Policy,
110{
111    let ax = x.abs();
112    let small = ax.cmp_lt(V::splat(t.small_threshold));
113
114    // `(x/2)(1 + a(1/2 + a P(a)))`, Boost's nested `Q` written out. The leading `x/2` is
115    // what makes `I_1(x) ~ x/2` exact as `x -> 0` rather than a subtraction of near-equals.
116    let h = ax * V::HALF;
117    let a = h * h;
118    let inner = a.mul_adde(a.poly_n_p::<P, NS>(&t.small), V::HALF);
119    let mut lo = h * a.mul_adde(inner, V::ONE);
120    if const { SCALED } {
121        lo *= (-ax).exp_p::<P>();
122    }
123
124    let inv = V::ONE / ax;
125    let mut hi = inv.poly_n_p::<P, NL>(&t.large) / ax.sqrt();
126
127    // One mask, one reduction, reused for both the polynomial swap and the exponential
128    // assembly below. The two are the same region and must not test it twice.
129    let far = ax.cmp_ge(V::splat(t.far_threshold));
130    let any_far = thermite::unlikely(far.any());
131    if any_far {
132        hi = far.select(inv.poly_n_p::<P, NF>(&t.far) / ax.sqrt(), hi);
133    }
134
135    if const { !SCALED } {
136        let full = ax.exp_p::<P>();
137        hi = if any_far {
138            let half = (ax * V::HALF).exp_p::<P>();
139            far.select((hi * half) * half, hi * full)
140        } else {
141            hi * full
142        };
143    }
144
145    small.select(lo, hi).copysign(x)
146}
147
148/// The `x`-coefficient of the downward recurrence's trip count, by precision tier.
149///
150/// The continued fraction below needs `O(x)` iterations, a property of the method rather than
151/// of this implementation, and Boost says so in its own `CF1_ik`: "|x| <= |v|, CF1_ik
152/// converges rapidly; |x| > |v|, CF1_ik needs O(|x|) iterations to converge". So the _only_
153/// knob is how far to run it, which makes the trip count itself the precision tier.
154///
155/// Measured against mpmath over `N` in 2..80 and `x` in 0.01..700, holding the flat margin at
156/// [`RECURRENCE_MARGIN`]:
157///
158/// | tier | coefficient | worst relative error | max trips |
159/// |---|---|---|---|
160/// | `Best` / `Reference` | 0.35 | 5.19e-15 | 349 |
161/// | `Average` (the default) | 0.25 | 2.43e-12 | 279 |
162/// | `Medium` | 0.15 | 4.27e-08 | 209 |
163/// | `Worst` | 0.10 | 2.84e-05 | 174 |
164///
165/// Monotone in precision, as the policy ladder requires. `Medium`'s 4.3e-08 is about f32
166/// epsilon, which is why it sits there. Note 0.35 is not a rounded-up 0.5: 0.5 measured
167/// _identically_ at 5.19e-15 while costing 454 trips instead of 349, so the extra was pure
168/// waste. The margin and the coefficient interact (dropping the margin to 16 pushes even the
169/// 0.35 rung to 2.08e-12), so neither is tunable alone.
170#[inline(always)]
171pub(super) const fn recurrence_x_coeff(p: PrecisionPolicy) -> (i64, i64) {
172    match p {
173        PrecisionPolicy::Best | PrecisionPolicy::Reference => (35, 100),
174        PrecisionPolicy::Average => (25, 100),
175        PrecisionPolicy::Medium => (15, 100),
176        _ => (10, 100),
177    }
178}
179
180/// The flat part of the trip count, on top of `N` and the `x`-scaled part.
181///
182/// 24 is the knee: 16 costs an order of magnitude at the top tier and 8 costs five.
183pub(super) const RECURRENCE_MARGIN: usize = 24;
184
185/// `I_N(x)` for `N >= 2`, or `e^{-|x|} I_N(x)` when `SCALED`, by downward recurrence on the
186/// **ratios** rather than on the values.
187///
188/// # Why ratios
189///
190/// Writing `r_k = I_k(x)/I_{k-1}(x)`, the three-term recurrence
191/// `I_{k-1} = I_{k+1} + (2k/x) I_k` divides through to
192///
193/// ```math
194/// r_k = \frac{1}{2k/x + r_{k+1}}
195/// ```
196///
197/// which is the same continued fraction Boost evaluates with Lentz's method in `CF1_ik`. The
198/// point for a vector unit is that **every `r_k` lies in `(0, 1)`**, so nothing can overflow
199/// and no rescaling is needed anywhere. The textbook alternative, Miller's linear downward
200/// pass on the values themselves, has intermediates growing like `2^M M!/x^M`, which leaves
201/// f64 range around order 50 and f32 range around order 8, and needs a per-lane rescale
202/// _select_ inside the loop to survive. That is three selects per iteration to buy nothing.
203///
204/// Seeding is from `I_0`, which the closed form already provides, and `I_N = I_0 \prod r_k`.
205/// The scaled and unscaled forms differ **only in that seed**. A ratio is scale-free, so
206/// `SCALED` never reaches the loop.
207///
208/// # Why forward recurrence is not used
209///
210/// The textbook rule is "forward when `N < x`, downward otherwise", which would bound both
211/// trip counts by `N` alone. Measured, it does not work: forward recurrence's amplification
212/// grows with `N` faster than it decays with `x`, giving 2.1e-06 at `N = 50, x = 100`, and
213/// `N = 80` never reaches 1e-13 for any `x` up to 700. No crossover rescues it: the best
214/// fitted rule still left 1.4e-03. So there is one path here, and therefore no select between
215/// paths at all.
216///
217/// # Trip count
218///
219/// `N + 24 + coeff * x`, with the per-lane start following that lane's own `x`. Lanes are
220/// free to start _higher_ than they need, because the recurrence is self-correcting downward
221/// from a zero seed. No lane is ever cut short, and the loop simply runs until every lane has
222/// walked down to `k = 1`. The packet therefore pays its worst lane, which is the standing
223/// trade for a data-dependent trip count here.
224#[inline(always)]
225pub fn bessel_in_pair_impl<
226    P,
227    E,
228    V,
229    const NS: usize,
230    const NL: usize,
231    const NF: usize,
232    const N: i32,
233    const SCALED: bool,
234>(
235    x: V,
236    t: &BesselI<E, NS, NL, NF>,
237) -> (V, V)
238where
239    E: FloatElement,
240    V: FloatVector<Element = E> + TranscendentalMathWithPolicy,
241    P: Policy,
242{
243    // No `const { assert!(N >= 2) }`: a statically-false `if const` arm still monomorphizes
244    // the function it names, so the assert would fire from the dispatch's dead N = 0, 1
245    // branches. Those orders are merely slower here, not wrong.
246
247    // `I_{-n} = I_n` for integer `n`, so a negative order is the same computation and there is
248    // no sign for the caller to apply. See `bessel_kn_recur` for why the absolute value is
249    // taken in the body rather than in a const-generic argument.
250    let m = const { N.unsigned_abs() as usize };
251
252    let ax = x.abs();
253
254    // ---- Which lanes take which arm -------------------------------------------------
255    //
256    // Above the crossover the asymptotic series is both cheaper and more accurate, and the
257    // two properties move the same way: the recurrence gets worse with `x` (its trip count is
258    // linear in it) while the series gets better _and_ shorter. Measured crossover to 1e-15,
259    // against mpmath: `x ~ 20` for orders up to 5, 30 at order 12, 50 at order 20, 700 at
260    // order 50, about `N^2/3`. Floored at 40 rather than 20 because near the crossover the
261    // recurrence is still only ~30 divides and the series wants ~24 terms, so there is
262    // nothing to win until `x` is a little higher.
263    let use_asym = ax.cmp_ge(V::splat(E::from_int(
264        const { asymptotic_threshold(N.unsigned_abs() as usize) } as _,
265    )));
266    let need_rec = !use_asym;
267    let any_rec = need_rec.any();
268
269    let mut value = V::ZERO;
270    let mut prev = V::ZERO;
271
272    // ---- Downward ratio recurrence --------------------------------------------------
273    if any_rec {
274        let i0 = bessel_i0_impl::<P, V, NS, NL, NF, SCALED>(ax, t);
275
276        // 2/x. Infinite at x = 0, which is exactly right: it drives every ratio to zero, and
277        // `I_N(0) = 0` for N >= 1 falls out of the product with no special case.
278        let two_over_x = V::TWO / ax;
279
280        let (cn, cd) = const { recurrence_x_coeff(P::POLICY.precision) };
281        let coeff = V::splat(E::from_ratio(cn, cd));
282        let start_f = V::splat(E::from_int((m + RECURRENCE_MARGIN) as _));
283        let n_f = V::splat(E::from_int(m as _));
284
285        // Each lane starts at its own `N + 24 + coeff*x`, and the asymptotic lanes start at
286        // zero so they cannot drag the packet's trip count, which is the whole point of the
287        // arm, since a single large `x` would otherwise cost every lane hundreds of divides.
288        let mut k = need_rec.select(ax.mul_adde(coeff, start_f).ceil(), V::ZERO);
289        let mut r = V::ZERO;
290        let mut prod = V::ONE;
291        // `I_{N-1}` for free: the same ladder, stopped one rung short. Having it is what lets
292        // `Dual` differentiate without a second pass, since every derivative identity in this
293        // family reaches DOWN one order and never up.
294        let mut prod_prev = V::ONE;
295        let nm1_f = V::splat(E::from_int((m as i64) - 1));
296
297        loop {
298            // One mask, one reduction, reused for both the ratio update and the accumulate.
299            let active = k.cmp_ge(V::ONE);
300            if active.none() {
301                break;
302            }
303
304            r = active.select(V::ONE / two_over_x.mul_adde(k, r), r);
305            // The last N rungs of each lane's own descent are its r_N .. r_1.
306            prod = (active & k.cmp_le(n_f)).select(prod * r, prod);
307            prod_prev = (active & k.cmp_le(nm1_f)).select(prod_prev * r, prod_prev);
308
309            k -= V::ONE;
310        }
311
312        value = i0 * prod;
313        prev = i0 * prod_prev;
314    }
315
316    // ---- Large-x asymptotic series --------------------------------------------------
317    if use_asym.any() {
318        let a = asymptotic_series::<P, E, V, SCALED>(ax, m, t.far_threshold);
319        value = use_asym.select(a, value);
320        if m > 0 {
321            let b = asymptotic_series::<P, E, V, SCALED>(ax, m - 1, t.far_threshold);
322            prev = use_asym.select(b, prev);
323        }
324    }
325
326    // I_N is even for even N and odd for odd N. `I_{N-1}` has the opposite parity, so the two
327    // take opposite sign treatment. Unlike `J_1`, `copysign` is safe for both, because
328    // `I_nu` is positive on the whole positive axis at every order.
329    let v = if const { N % 2 == 0 } { value } else { value.copysign(x) };
330    let p = if const { N % 2 == 0 } { prev.copysign(x) } else { prev };
331    (p, v)
332}
333
334/// Where [`bessel_in_impl`] hands the recurrence over to the asymptotic series.
335///
336/// `N^2/3`, floored at 40. The measured crossovers to 1e-15 are `x = 20` for orders 0..5,
337/// 30 at order 12, 50 at order 20 and 700 at order 50. `N^2/3` clears all of them (order 20
338/// gets 133, order 50 gets 833) without being so loose that the series is asked to work where
339/// it cannot.
340#[inline(always)]
341const fn asymptotic_threshold(n: usize) -> usize {
342    let t = n * n / 3;
343    if t > 40 { t } else { 40 }
344}
345
346/// Terms in the asymptotic series.
347///
348/// The series is divergent, so this is a truncation and not a convergence count. 24 was the
349/// best of 1..24 at every measured crossover point, so the optimal truncation is still
350/// further out there. Past the crossover the terms shrink fast, and 24 is comfortably safe
351/// everywhere the arm runs. It is a plausible future tier knob, but not one today, because
352/// the arm is already cheap next to the hundreds of divides it replaces.
353const ASYMPTOTIC_TERMS: usize = 24;
354
355/// `(a e^{x}, b e^{x})` for two scaled `I` values, with the exponential halved past
356/// `far_threshold`.
357///
358/// Every unscaled `I` arm in the family ends this way, and the halving is not decoration:
359/// `$I_\nu(x) \sim e^x/\sqrt{2\pi x}$` is representable to about `x = 714`, while a single
360/// `$e^x$` overflows at 709.78, so between the two a direct product returns `inf` for a finite
361/// answer. `far_threshold` comes from the `BesselI` table so every arm turns the corner at
362/// the same place. The pair form exists because the neighbouring order rides along for free
363/// in every kernel that returns one, and should not cost a second exponential.
364#[inline(always)]
365pub(super) fn unscale_i_pair<P, E, V>(a: V, b: V, ax: V, far_threshold: E) -> (V, V)
366where
367    E: FloatElement,
368    V: FloatVector<Element = E> + TranscendentalMathWithPolicy,
369    P: Policy,
370{
371    unscale_i_pair_masked::<P, V>(a, b, ax, ax.cmp_ge(V::splat(far_threshold)))
372}
373
374/// `unscale_i_pair` with the far mask supplied, for an arithmetic where "far" is not a
375/// plain comparison: over C it is `Re z >= threshold`, and `z` itself is the exponent.
376#[inline(always)]
377pub fn unscale_i_pair_masked<P, V>(a: V, b: V, z: V, far: V::Mask) -> (V, V)
378where
379    V: FloatVector + TranscendentalMathWithPolicy,
380    P: Policy,
381{
382    let full = z.exp_p::<P>();
383    if thermite::unlikely(far.any()) {
384        let half = (z * V::HALF).exp_p::<P>();
385        (
386            far.select((a * half) * half, a * full),
387            far.select((b * half) * half, b * full),
388        )
389    } else {
390        (a * full, b * full)
391    }
392}
393
394/// [`unscale_i_pair`] for one value.
395#[inline(always)]
396pub(super) fn unscale_i<P, E, V>(a: V, ax: V, far_threshold: E) -> V
397where
398    E: FloatElement,
399    V: FloatVector<Element = E> + TranscendentalMathWithPolicy,
400    P: Policy,
401{
402    unscale_i_pair::<P, E, V>(a, a, ax, far_threshold).0
403}
404
405/// `$K_0(x)$`, or `$e^{x} K_0(x)$` when `SCALED`.
406///
407/// Two regions, splitting at `x = 1`:
408///
409/// ```math
410/// K_0(x) = P(x^2) - \ln(x)\,I_0(x), \qquad
411/// K_0(x) = \frac{e^{-x}}{\sqrt{x}}\left(Y + \frac{P(1/x)}{Q(1/x)}\right)
412/// ```
413///
414/// The `$I_0$` in the small arm is the shipped kernel, not a second copy of its coefficients.
415/// Boost fits a cut-down `$I_0$` valid only on `[0,1]` for this. Reusing the real one trades a
416/// longer Horner for one fewer table and a slightly better factor.
417///
418/// `$K$` has no reflection: it is undefined for `x < 0`, and NaN there rather than a mirrored
419/// value. At `x = 0` it is `$+\infty$`, which falls out of `$-\ln(0)$` without a special case.
420#[inline(always)]
421pub fn bessel_k0_impl<
422    P,
423    E,
424    V,
425    const NS: usize,
426    const DS: usize,
427    const NL: usize,
428    const DL: usize,
429    const IS: usize,
430    const IL: usize,
431    const IF: usize,
432    const SCALED: bool,
433>(
434    x: V,
435    t: &BesselK<E, NS, DS, NL, DL>,
436    ti: &BesselI<E, IS, IL, IF>,
437) -> V
438where
439    E: FloatElement,
440    V: FloatVector<Element = E> + TranscendentalMathWithPolicy,
441    P: Policy,
442{
443    let small = x.cmp_le(V::splat(t.small_threshold));
444    let mut value = V::ZERO;
445
446    if small.any() {
447        // `-ln(x) I_0(x)` carries the singularity and the rational carries everything else.
448        let i0 = bessel_i0_impl::<P, V, IS, IL, IF, false>(x, ti);
449        let mut lo = (x * x).poly_rational_n_p::<P, NS, DS>(&t.small_num, &t.small_den) - x.ln_p::<P>() * i0;
450        if const { SCALED } {
451            lo *= x.exp_p::<P>();
452        }
453        value = lo;
454    }
455
456    if !small.all() {
457        let inv = V::ONE / x;
458        let mut hi =
459            (inv.poly_rational_n_p::<P, NL, DL>(&t.large_num, &t.large_den) + V::splat(t.large_offset)) / x.sqrt();
460        if const { !SCALED } {
461            // Halve the exponent where `e^-x` would underflow to zero before `1/sqrt(x)` and
462            // the sub-unit rational could scale it back up (the mirror of the overflow guard
463            // in the `I` kernels, reached for the same structural reason).
464            let tiny = x.cmp_ge(V::splat(t.exp_split_threshold));
465            hi = if thermite::unlikely(tiny.any()) {
466                let half = (-x * V::HALF).exp_p::<P>();
467                tiny.select((hi * half) * half, hi * (-x).exp_p::<P>())
468            } else {
469                hi * (-x).exp_p::<P>()
470            };
471        }
472        value = small.select(value, hi);
473    }
474
475    // Undefined off the positive axis.
476    x.cmp_lt(V::ZERO).select(V::NAN, value)
477}
478
479/// `$K_1(x)$`, or `$e^{x} K_1(x)$` when `SCALED`.
480///
481/// ```math
482/// K_1(x) = R(x^2)\,x + \frac{1}{x} + \ln(x)\,I_1(x), \qquad
483/// K_1(x) = \frac{e^{-x}}{\sqrt{x}}\left(Y + \frac{P(1/x)}{Q(1/x)}\right)
484/// ```
485///
486/// The `$1/x$` is the singularity and is left as a bare reciprocal rather than folded into
487/// the rational, because it is the entire value as `$x \to 0$` and any rearrangement that
488/// mixes it with the `$O(x)$` terms cancels it away.
489#[inline(always)]
490pub fn bessel_k1_impl<
491    P,
492    E,
493    V,
494    const NS: usize,
495    const DS: usize,
496    const NL: usize,
497    const DL: usize,
498    const IS: usize,
499    const IL: usize,
500    const IF: usize,
501    const SCALED: bool,
502>(
503    x: V,
504    t: &BesselK<E, NS, DS, NL, DL>,
505    ti: &BesselI<E, IS, IL, IF>,
506) -> V
507where
508    E: FloatElement,
509    V: FloatVector<Element = E> + TranscendentalMathWithPolicy,
510    P: Policy,
511{
512    let small = x.cmp_le(V::splat(t.small_threshold));
513    let mut value = V::ZERO;
514
515    if small.any() {
516        let i1 = bessel_i1_impl::<P, V, IS, IL, IF, false>(x, ti);
517        let mut lo = (x * x)
518            .poly_rational_n_p::<P, NS, DS>(&t.small_num, &t.small_den)
519            .mul_adde(x, V::ONE / x)
520            + x.ln_p::<P>() * i1;
521        if const { SCALED } {
522            lo *= x.exp_p::<P>();
523        }
524        value = lo;
525    }
526
527    if !small.all() {
528        let inv = V::ONE / x;
529        let mut hi =
530            (inv.poly_rational_n_p::<P, NL, DL>(&t.large_num, &t.large_den) + V::splat(t.large_offset)) / x.sqrt();
531        if const { !SCALED } {
532            let tiny = x.cmp_ge(V::splat(t.exp_split_threshold));
533            hi = if thermite::unlikely(tiny.any()) {
534                let half = (-x * V::HALF).exp_p::<P>();
535                tiny.select((hi * half) * half, hi * (-x).exp_p::<P>())
536            } else {
537                hi * (-x).exp_p::<P>()
538            };
539        }
540        value = small.select(value, hi);
541    }
542
543    x.cmp_lt(V::ZERO).select(V::NAN, value)
544}
545
546/// `$K_N(x)$` for `N >= 2` from `$K_0$` and `$K_1$`, by **upward** recurrence.
547///
548/// ```math
549/// K_{n+1}(x) = K_{n-1}(x) + \frac{2n}{x} K_n(x)
550/// ```
551///
552/// Takes the two seeds rather than the coefficient tables that produce them. The recurrence
553/// has nothing to do with any table, and threading four of them through so it could call the
554/// order-0 and order-1 kernels itself cost **fourteen** const-generic array lengths on a
555/// function whose arithmetic needs none. The one place that already names the tables
556/// concretely, the per-element dispatch, builds the seeds instead.
557///
558/// Scaled and unscaled both work with no flag: `$e^{x}$` is a common factor of every term and
559/// passes straight through, so whichever form the seeds are in is the form that comes out.
560///
561/// # Why upward, when `I` needs downward
562///
563/// `$K_\nu$` is the **dominant** solution of the modified Bessel equation and `$I_\nu$` the
564/// minimal one, so the stability argument inverts exactly. Recurring `$K$` upward amplifies
565/// what is already growing, which is harmless. Recurring the _minimal_ solution upward
566/// destroys it, which is what forces `$I$` onto a downward continued fraction with an
567/// `$O(x)$` trip count. Here the cost is `N - 1` steps: no continued fraction, no dependence
568/// on `x`, and no precision tier to measure. That is why this is the one kernel in the file
569/// with no `P: Policy` parameter at all. Every operation in it is exact-by-construction
570/// arithmetic, so there is no approximation to pick a tier for.
571///
572/// # Overflow
573///
574/// `$K_N$` grows quickly in the _order_: `$K_{50}(1)$` is about `$2.6\times10^{78}$` and
575/// `$K_{60}(1)$` leaves f64. The recurrence sums terms of like sign, so it saturates to
576/// `$+\infty$` rather than returning a wrong finite value, and the scaled form buys nothing
577/// here: unlike `$I$`, this overflow is in `N`, not in `x`.
578#[inline(always)]
579pub fn bessel_kn_recur<E, V, const N: i32>(x: V, k0: V, k1: V) -> (V, V)
580where
581    E: FloatElement,
582    V: FloatVector<Element = E>,
583{
584    let mut prev = k0;
585    let mut cur = k1;
586
587    let two_over_x = V::TWO / x;
588
589    // `K_{-n} = K_n` for integer `n`, so a negative order is genuinely the same walk. Unlike
590    // `J`/`Y`, the caller has no sign to apply afterwards. The absolute value is taken here
591    // rather than in a const-generic argument because `foo::<{ N.unsigned_abs() }>` needs
592    // `generic_const_exprs`. A `const` block folds the same way, so this still unrolls.
593    let m = const { N.unsigned_abs() as usize };
594
595    // `m` is const, so this unrolls and every `2n` is an immediate.
596    let mut n = 1usize;
597    while n < m {
598        let next = two_over_x.mul_adde(V::splat(E::from_int(n as _)) * cur, prev);
599        prev = cur;
600        cur = next;
601        n += 1;
602    }
603
604    // `(K_{N-1}, K_N)`. The upward walk passes through order `N-1` anyway, so returning both
605    // is free, and is what lets `Dual` use `K_N' = -K_{N-1} - (N/x)K_N` without a second
606    // pass over the recurrence.
607    (prev, cur)
608}
609
610/// The large-`x` asymptotic series for `$I_\nu$`, scaled or not.
611///
612/// ```math
613/// I_\nu(x) \sim \frac{e^x}{\sqrt{2\pi x}} \sum_k \frac{(-1)^k a_k(\nu)}{x^k},
614/// \qquad a_k(\nu) = \frac{\prod_j\left(4\nu^2 - (2j-1)^2\right)}{k!\,8^k}
615/// ```
616///
617/// `nu` is a **runtime** parameter, not a const one, purely so the same body can be called at
618/// `N` and `N - 1` without `generic_const_exprs`. The per-term ratio is still the same small
619/// rational, `((2k+1)^2 - 4nu^2) / (8(k+1))`, so nothing here needs a coefficient table at any
620/// order. What is lost against a const `nu` is only the folding of that one multiplier.
621#[inline(always)]
622fn asymptotic_series<P, E, V, const SCALED: bool>(ax: V, nu: usize, far_threshold: E) -> V
623where
624    E: FloatElement,
625    V: FloatVector<Element = E> + TranscendentalMathWithPolicy,
626    P: Policy,
627{
628    let w = V::ONE / ax;
629    let four_nu2 = 4 * (nu as i64) * (nu as i64);
630
631    let mut term = V::ONE;
632    let mut sum = V::ONE;
633    let mut i = 0usize;
634    while i < ASYMPTOTIC_TERMS {
635        let num = (2 * i as i64 + 1) * (2 * i as i64 + 1) - four_nu2;
636        let den = 8 * (i as i64 + 1);
637        term *= w * V::splat(E::from_ratio(num, den));
638        sum += term;
639        i += 1;
640    }
641
642    let a = (sum * V::FRAC_1_SQRT_TAU) / ax.sqrt();
643    if const { SCALED } {
644        a
645    } else {
646        unscale_i::<P, E, V>(a, ax, far_threshold)
647    }
648}
649
650/// `$I_N(x)$` for `N >= 2`, or `$e^{-|x|} I_N(x)$` when `SCALED`.
651///
652/// Thin wrapper over [`bessel_in_pair_impl`], which computes `$I_{N-1}$` alongside at no extra
653/// cost in the recurrence arm. Callers that want the derivative should take the pair directly.
654#[inline(always)]
655pub fn bessel_in_impl<P, E, V, const NS: usize, const NL: usize, const NF: usize, const N: i32, const SCALED: bool>(
656    x: V,
657    t: &BesselI<E, NS, NL, NF>,
658) -> V
659where
660    E: FloatElement,
661    V: FloatVector<Element = E> + TranscendentalMathWithPolicy,
662    P: Policy,
663{
664    bessel_in_pair_impl::<P, E, V, NS, NL, NF, N, SCALED>(x, t).1
665}
666
667/// The large-`x` asymptotic series for `I_nu`, with a **per-lane** order.
668///
669/// Same expansion as [`asymptotic_series`], which takes a scalar `nu` and folds
670/// `4 nu^2` into an immediate. Here `nu` is a vector, so the term numerator
671/// `(2k+1)^2 - 4 nu^2` becomes one vector subtract and one vector multiply per term
672/// (24 terms, so about 48 extra ops). The denominator `8(k+1)` is still a compile-time
673/// constant and still folds.
674///
675/// This exists because leaving it out was measurably expensive. Without an asymptotic arm
676/// the runtime-order path falls back to the recurrence at large `x`, and the recurrence's
677/// trip count IS the precision tier, which put `bessel_i_scaled` at 698 to 2634 ULP on the
678/// `performance` and `size` tiers over `[-400, 400]`, against 3-6 ULP at `precision`. The
679/// const path never showed that, because its asymptotic arm took over exactly where the
680/// recurrence starts to degrade.
681#[inline(always)]
682pub(super) fn asymptotic_series_v<P, E, V, const SCALED: bool>(ax: V, nu: V, far_threshold: E) -> V
683where
684    E: FloatElement,
685    V: FloatVector<Element = E> + TranscendentalMathWithPolicy,
686    P: Policy,
687{
688    let w = V::ONE / ax;
689    let four_nu2 = (nu * nu) * V::splat(E::from_int(4));
690
691    let mut term = V::ONE;
692    let mut sum = V::ONE;
693    let mut i = 0usize;
694    while i < ASYMPTOTIC_TERMS {
695        let odd_sq = (2 * i as i64 + 1) * (2 * i as i64 + 1);
696        let num = V::splat(E::from_int(odd_sq)) - four_nu2;
697        let inv_den = V::splat(E::from_ratio(1, 8 * (i as i64 + 1)));
698        term *= (w * num) * inv_den;
699        sum += term;
700        i += 1;
701    }
702
703    let a = (sum * V::FRAC_1_SQRT_TAU) / ax.sqrt();
704    if const { SCALED } {
705        a
706    } else {
707        unscale_i::<P, E, V>(a, ax, far_threshold)
708    }
709}
710
711/// `asymptotic_series_v` over a general arithmetic: the argument `z` in `C`, the order in
712/// its real primal `R`, and the far mask supplied by the caller.
713///
714/// This is the body the real-order kernel runs, and through it the complex one. The loop is
715/// the same as `asymptotic_series_v`'s. It is a separate function rather than that one's
716/// body because the integer and half-integer kernels call the real form from bounds that
717/// know nothing of [`BesselDetails`], and the second-term machinery below is only meaningful
718/// off the real axis. Everything `z`-dependent is `C` arithmetic. The term numerators
719/// `(2k+1)^2 - 4 nu^2` stay real and enter through `C: Mul<R>`, so a complex instantiation
720/// pays two real multiplies per term rather than a complex one.
721///
722/// # The second exponential
723///
724/// The expansion has two exponential terms (DLMF 10.40.5), and the real line keeps one
725/// because the other is `e^{-2x}` relative, below epsilon anywhere this arm runs. That is
726/// a fact about the real axis. Off it the second term's modulus is `e^{-2 Re z}`, which on
727/// the imaginary axis is one, so an arithmetic can ask for it through
728/// [`BesselDetails::ASYM_TWO_TERMS`]. It costs one more exponential and no extra series
729/// evaluation: the second sum is the first with every other sign flipped, so both are
730/// accumulated in the one loop.
731#[inline(always)]
732pub fn asymptotic_series_g<P, E, R, C, const SCALED: bool>(z: C, nu: R, far: C::Mask) -> C
733where
734    E: FloatElement,
735    R: FloatVector<Element = E>,
736    C: FloatVector<Mask = R::Mask>
737        + TranscendentalMathWithPolicy
738        + PrimalProjection<Primal = R>
739        + BesselDetails<C>
740        + core::ops::Mul<R, Output = C>,
741    P: Policy,
742{
743    let w = C::ONE / z;
744    let four_nu2 = (nu * nu) * R::splat(E::from_int(4));
745
746    let mut term = C::ONE;
747    let mut sum = C::ONE;
748    let mut sum_alt = C::ONE;
749    let mut i = 0usize;
750    while i < ASYMPTOTIC_TERMS {
751        let odd_sq = (2 * i as i64 + 1) * (2 * i as i64 + 1);
752        let num = R::splat(E::from_int(odd_sq)) - four_nu2;
753        let inv_den = R::splat(E::from_ratio(1, 8 * (i as i64 + 1)));
754        // Associated exactly as `asymptotic_series_v` does, so the real instantiation
755        // rounds identically.
756        term *= (w * num) * inv_den;
757        sum += term;
758        if const { C::ASYM_TWO_TERMS } {
759            // `i` is the loop index, so this parity folds.
760            sum_alt = if i.is_multiple_of(2) {
761                sum_alt - term
762            } else {
763                sum_alt + term
764            };
765        }
766        i += 1;
767    }
768
769    if const { C::ASYM_TWO_TERMS } {
770        sum += sum_alt * C::asym_second_exponent(z, nu).exp_p::<P>();
771    }
772
773    let a = (sum * C::FRAC_1_SQRT_TAU) / z.sqrt();
774    if const { SCALED } {
775        a
776    } else {
777        unscale_i_pair_masked::<P, C>(a, a, z, far).0
778    }
779}
780
781/// `I_n(x)` with a **per-lane** order, or `e^{-|x|} I_n(x)` when `SCALED`.
782///
783/// The const-generic form is the one to reach for when the order is known. This exists for the
784/// case `hermitev` exists for: an order that arrives as data. Every lane may ask for a
785/// different one.
786///
787/// # What changes, and what does not
788///
789/// Almost nothing. The ratio recurrence already accumulates its product under a mask
790/// (`k <= N`), so making `N` a vector rather than a splat is a one-word change. The trip-count
791/// start `N + 24 + c*x` was already per-lane in `x` and simply becomes per-lane in `n` too.
792/// The loop still ends when the last lane reaches `k = 1`, so the packet pays for its widest
793/// (order, argument) pair, the standing trade.
794///
795/// The asymptotic arm generalises too, via [`asymptotic_series_v`]: `4 nu^2` no longer folds,
796/// which costs one vector subtract and one multiply per term. Without the arm the lower tiers
797/// reached 698 to 2634 ULP on `bessel_i_scaled` past `x ~ 40`, because the recurrence's trip
798/// count is the tier and its low rungs are far too short there.
799#[inline(always)]
800pub fn bessel_iv_impl<P, E, V, const NS: usize, const NL: usize, const NF: usize, const SCALED: bool>(
801    x: V,
802    n: V,
803    t: &BesselI<E, NS, NL, NF>,
804) -> V
805where
806    E: FloatElement,
807    V: FloatVector<Element = E> + TranscendentalMathWithPolicy,
808    P: Policy,
809{
810    let ax = x.abs();
811
812    // Which lanes take which arm, exactly as the const form decides it: the asymptotic series
813    // past `max(40, n^2/3)`, the recurrence below. Per-lane now, since `n` is per-lane.
814    let n2_third = (n * n) * V::splat(E::from_ratio(1, 3));
815    let thresh = n2_third.max(V::splat(E::from_int(40)));
816    let use_asym = ax.cmp_ge(thresh);
817    let need_rec = !use_asym;
818
819    let two_over_x = V::TWO / ax;
820    let mut value = V::ZERO;
821
822    if need_rec.any() {
823        let i0 = bessel_i0_impl::<P, V, NS, NL, NF, SCALED>(ax, t);
824
825        let (cn, cd) = const { recurrence_x_coeff(P::POLICY.precision) };
826        let coeff = V::splat(E::from_ratio(cn, cd));
827        let margin = V::splat(E::from_int(RECURRENCE_MARGIN as _));
828
829        // Asymptotic lanes start at zero so they cannot drag the packet's trip count.
830        let mut k = need_rec.select(ax.mul_adde(coeff, n + margin).ceil(), V::ZERO);
831        let mut r = V::ZERO;
832        let mut prod = V::ONE;
833
834        loop {
835            let active = k.cmp_ge(V::ONE);
836            if active.none() {
837                break;
838            }
839            r = active.select(V::ONE / two_over_x.mul_adde(k, r), r);
840            prod = (active & k.cmp_le(n)).select(prod * r, prod);
841            k -= V::ONE;
842        }
843
844        value = i0 * prod;
845    }
846
847    if use_asym.any() {
848        value = use_asym.select(asymptotic_series_v::<P, E, V, SCALED>(ax, n, t.far_threshold), value);
849    }
850
851    // Parity is per-lane: even orders are even in `x`, odd orders odd. `I_nu` is positive on
852    // the positive axis at every order, so the sign is a straight per-lane negate.
853    let odd_order = (n * V::HALF).fract().cmp_gt(V::ZERO);
854    value.neg_c(odd_order & x.is_negative())
855}
856
857/// `K_n(x)` with a per-lane order, or `e^{x} K_n(x)` when `SCALED`.
858///
859/// Upward recurrence, each lane freezing once it reaches its own order, the `hermitev` shape.
860/// The loop runs to the packet's largest order.
861#[inline(always)]
862#[allow(clippy::too_many_arguments)]
863pub fn bessel_kv_impl<
864    P,
865    E,
866    V,
867    const AS: usize,
868    const AD: usize,
869    const AL: usize,
870    const ALD: usize,
871    const BS: usize,
872    const BD: usize,
873    const BL: usize,
874    const BLD: usize,
875    const IS: usize,
876    const IL: usize,
877    const IF: usize,
878    const JS: usize,
879    const JL: usize,
880    const JF: usize,
881    const SCALED: bool,
882>(
883    x: V,
884    n: V,
885    t0: &BesselK<E, AS, AD, AL, ALD>,
886    t1: &BesselK<E, BS, BD, BL, BLD>,
887    ti0: &BesselI<E, IS, IL, IF>,
888    ti1: &BesselI<E, JS, JL, JF>,
889) -> V
890where
891    E: FloatElement,
892    V: FloatVector<Element = E> + TranscendentalMathWithPolicy,
893    P: Policy,
894{
895    let k0 = bessel_k0_impl::<P, E, V, AS, AD, AL, ALD, IS, IL, IF, SCALED>(x, t0, ti0);
896    let k1 = bessel_k1_impl::<P, E, V, BS, BD, BL, BLD, JS, JL, JF, SCALED>(x, t1, ti1);
897
898    let two_over_x = V::TWO / x;
899    let mut prev = k0;
900    let mut cur = k1;
901    let mut step = V::ONE;
902
903    loop {
904        // Freeze BOTH halves on a lane that has reached its order. Advancing only `cur` would
905        // leave `prev` one rung behind and corrupt the next step, the same trap `hermitev`
906        // documents for its pair.
907        let cont = step.cmp_lt(n);
908        if cont.none() {
909            break;
910        }
911        let next = two_over_x.mul_adde(step * cur, prev);
912        prev = cont.select(cur, prev);
913        cur = cont.select(next, cur);
914        step += V::ONE;
915    }
916
917    // Order 0 never entered the loop, so pick it out directly.
918    n.cmp_le(V::ZERO).select(k0, cur)
919}
Last built: 2026-09-08 21:35:55 UTC