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}