Skip to main content

thermite_special/specialized/generic/bessel/
jy.rs

1//! The oscillatory Bessel functions `$J_0$`, `$J_1$`, `$Y_0$`, `$Y_1$`.
2//!
3//! # What "accuracy" means for a function with zeros
4//!
5//! This is the one decision to make before any tolerance is written, and getting it wrong
6//! produces a test that is either impossible to pass or meaningless.
7//!
8//! `$J_\nu$` and `$Y_\nu$` oscillate through zero forever. At a zero the _relative_ error of
9//! any implementation is unbounded (the true value is 0 and the computed one is not), so a
10//! relative-error contract is not merely hard to meet, it is not a statement about anything.
11//! What every implementation actually delivers, and what this kernel promises, is accuracy
12//! **relative to the envelope**:
13//!
14//! ```math
15//! \left|\,\hat{f}(x) - f(x)\,\right| \;\lesssim\; C\,\varepsilon\,\sqrt{\frac{2}{\pi x}}
16//! ```
17//!
18//! since `$\sqrt{2/\pi x}$` is the amplitude the oscillation rides on. Equivalently: absolute
19//! error scaled by `$\sqrt{x}$` is bounded. Tests here compare on that basis.
20//!
21//! Below `x = 8` there is a stronger guarantee. It is why the fits are shaped the way they
22//! are. Each sub-8 region carries one zero of the function, factored out as
23//! `$(x + x_k)\left((x - x_{k1}/256) - x_{k2}\right)$`: `$x_{k1}/256$` is a power-of-two-scaled
24//! integer and therefore exact, so the subtraction near the root loses nothing and full
25//! _relative_ accuracy survives at the first two or three zeros. Nobody does this above 8
26//! (Boost included) because the number of zeros to factor grows without bound.
27//!
28//! # Regions
29//!
30//! `$J$` splits at 4 and 8, `$Y_0$` at 3, 5.5 and 8, `$Y_1$` at 4 and 8. Above 8 all four
31//! share the Hankel form: one amplitude pair in `$(8/x)^2$` against `sin x` and `cos x`.
32//!
33//! That last point is the whole reason this file exists rather than a port of fdlibm, whose
34//! `j0f` splits the asymptotic envelope alone into **four** sub-intervals with a rational
35//! apiece. A branch picks one and skips three. A vector unit evaluates all four and discards
36//! three. Boost's single Hankel region is higher degree and strictly cheaper here.
37//!
38//! # `Y` calls `J`
39//!
40//! `$Y_\nu$` is singular at the origin, and the singularity is carried by a
41//! `$\frac{2}{\pi}\ln(x/x_k)\,J_\nu(x)$` term rather than by the rational, so these kernels
42//! call the `$J$` kernels, exactly as `$K$` calls `$I$`. The log is taken about the region's
43//! own root, not as a bare `$\ln x$`, which is what stops that term from swamping the rational
44//! near the zero.
45
46use thermite::{
47    math::{
48        TranscendentalMathWithPolicy,
49        policy::{Policy, PolicyParameters, PrecisionPolicy},
50    },
51    prelude::*,
52};
53
54/// Compensated Horner for the rationals in this file, at `Best` and above only.
55///
56/// No standard policy sets `use_compensation`, so something has to turn it on. This is that
57/// something, **tier-gated** rather than unconditional. Both halves of that matter.
58///
59/// It is on at all because these fits were ill-conditioned in a way the measurement made
60/// specific: `J_1`/`Y_1` region 2 evaluated at `y = x^2` up to 64 against coefficients reaching
61/// 1.7e18, and real Boost (compiled and graded) measures 35-37 ULP there against a fit whose
62/// exact-arithmetic error is 0.002 ULP.
63///
64/// It is off below `Best` because it is **not cheap and no longer load-bearing**. Measured
65/// against the pre-compensation baseline it cost about **4x on f64x4** and, before the non-FMA
66/// arm existed, 27x on the 1-lane seed, against a tier spread of only 23-43%. Meanwhile the
67/// bounded-variable substitution in the tables now attacks the same conditioning from the other
68/// side, so the default tier does not need both. A caller who wants the old behavior asks for
69/// `Precision`.
70///
71/// Spelled as a `Policy` impl rather than `UseCompensation<P, true>` because the flag has to be
72/// _computed_ from `P`, and a const-generic bool in a type alias cannot depend on `P` on stable.
73#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
74struct Comp<P: Policy>(core::marker::PhantomData<P>);
75
76impl<P: Policy> Policy for Comp<P> {
77    const POLICY: PolicyParameters = PolicyParameters {
78        use_compensation: P::POLICY.precision.ge(PrecisionPolicy::Best),
79        ..P::POLICY
80    };
81}
82
83use thermite::element::FloatElement;
84
85use crate::tables::bessel::{BesselJ, BesselY};
86
87/// `(x + root) * ((x - root_hi/256) - root_lo)`, the exactly-split root factor.
88///
89/// `root_hi/256` is exact by construction, so the inner subtraction is exact whenever `x` is
90/// near the root, which is the entire point. Writing this as `x*x - root*root`, or even as
91/// `(x + root) * (x - root)`, throws that away and costs every digit at the zero.
92#[inline(always)]
93fn root_factor<E, V>(x: V, root: V, hi: V, lo: V) -> V
94where
95    E: FloatElement,
96    V: FloatVector<Element = E>,
97{
98    (x + root) * ((x - hi * V::splat(E::from_ratio(1, 256))) - lo)
99}
100
101/// `$J_0(x)$`. Even in `x`.
102#[inline(always)]
103pub fn bessel_j0_impl<P, E, V, const N1: usize, const N2: usize, const NH: usize>(x: V, t: &BesselJ<E, N1, N2, NH>) -> V
104where
105    E: FloatElement,
106    V: FloatVector<Element = E> + TranscendentalMathWithPolicy,
107    P: Policy,
108{
109    let ax = x.abs();
110    let four = V::splat(E::from_int(4));
111    let eight = V::splat(E::from_int(8));
112
113    let r1 = ax.cmp_le(four);
114    let r2 = ax.cmp_le(eight);
115
116    let mut value = V::ZERO;
117
118    if r2.any() {
119        let y = ax * ax;
120        // Region 1 is a rational in x^2, region 2 in `1 - x^2/64`, which is the same
121        // interval mapped to [0, 1] so the fit does not have to span two decades.
122        // Each region's rational is skipped when no lane wants it, the way the `x > 8` branch
123        // already guards itself. A packet spanning both still pays for both (that is the
124        // standing trade), but a packet inside one region now pays for one.
125        let lo = if r1.none() {
126            V::ZERO
127        } else {
128            root_factor::<E, V>(ax, V::splat(t.root1), V::splat(t.root1_hi), V::splat(t.root1_lo))
129                * y.poly_rational_n_p::<Comp<P>, N1, N1>(&t.p1, &t.q1)
130        };
131        let mid = if r1.all() {
132            V::ZERO
133        } else {
134            root_factor::<E, V>(ax, V::splat(t.root2), V::splat(t.root2_hi), V::splat(t.root2_lo))
135                * (V::ONE - y * V::splat(E::from_ratio(1, 64))).poly_rational_n_p::<Comp<P>, N2, N2>(&t.p2, &t.q2)
136        };
137        value = r1.select(lo, mid);
138    }
139
140    if !r2.all() {
141        value = r2.select(value, hankel::<P, E, V, NH>(ax, t.pc, t.qc, t.ps, t.qs, false));
142    }
143
144    value
145}
146
147/// `$J_1(x)$`. Odd in `x`.
148#[inline(always)]
149pub fn bessel_j1_impl<P, E, V, const N1: usize, const N2: usize, const NH: usize>(x: V, t: &BesselJ<E, N1, N2, NH>) -> V
150where
151    E: FloatElement,
152    V: FloatVector<Element = E> + TranscendentalMathWithPolicy,
153    P: Policy,
154{
155    let ax = x.abs();
156    let four = V::splat(E::from_int(4));
157    let eight = V::splat(E::from_int(8));
158
159    let r1 = ax.cmp_le(four);
160    let r2 = ax.cmp_le(eight);
161
162    let mut value = V::ZERO;
163
164    if r2.any() {
165        let y = ax * ax;
166        // The extra leading `x` is `J_1`'s odd factor, and also supplies the zero at the
167        // origin exactly rather than through the rational.
168        // See `bessel_j0_impl`: skip a region's rational when no lane is in it.
169        let lo = if r1.none() {
170            V::ZERO
171        } else {
172            ax * root_factor::<E, V>(ax, V::splat(t.root1), V::splat(t.root1_hi), V::splat(t.root1_lo))
173                * y.poly_rational_n_p::<Comp<P>, N1, N1>(&t.p1, &t.q1)
174        };
175        // Region 2 in `1 - x^2/64`, as `J_0` does. Boost leaves `J_1` on the raw `x^2`, which
176        // runs to 64 against coefficients reaching 1.7e18. Compiled and graded, that costs real
177        // Boost 35.60 eps here against 3.20 for `j0`. The table carries the same rational
178        // re-expressed by exact algebra (see `scripts/bessel_jy_tables.py`), coefficients
179        // topping out near 7.4e5.
180        let mid = if r1.all() {
181            V::ZERO
182        } else {
183            ax * root_factor::<E, V>(ax, V::splat(t.root2), V::splat(t.root2_hi), V::splat(t.root2_lo))
184                * (V::ONE - y * V::splat(E::from_ratio(1, 64))).poly_rational_n_p::<Comp<P>, N2, N2>(&t.p2, &t.q2)
185        };
186        value = r1.select(lo, mid);
187    }
188
189    if !r2.all() {
190        value = r2.select(value, hankel::<P, E, V, NH>(ax, t.pc, t.qc, t.ps, t.qs, true));
191    }
192
193    // NOT `copysign(x)`: `J_1` is odd but also oscillates, so past its first zero at 3.83 it
194    // is negative for positive `x` and `copysign` would force it positive. `I_1` gets away
195    // with `copysign` only because it is positive on the whole positive axis.
196    value.neg_c(x.is_negative())
197}
198
199/// `(sin x + cos x, sin x - cos x)`, with whichever one is near zero rebuilt so it is not.
200///
201/// Both combinations vanish periodically (`sin x + cos x` at `x = 3pi/4 + k pi`, `sin x - cos x`
202/// at `pi/4 + k pi`), and at those points the subtraction of two same-magnitude numbers loses
203/// every digit that sets the Bessel function's phase. The repair is an exact identity:
204///
205/// ```math
206/// (\sin x + \cos x)(\sin x - \cos x) = \sin^2 x - \cos^2 x = -\cos 2x
207/// ```
208///
209/// so the small one equals `-cos(2x)` divided by the large one, and the large one never
210/// cancels. They cannot both be small: their squares sum to 2.
211///
212/// `sin x * cos x < 0` is exactly the condition for `sin x + cos x` being the small one, which
213/// is why the sign of the product picks the branch.
214///
215/// fdlibm does this and Boost does not. Boost writes the addition formulae out flat. It is the
216/// larger part of libm's remaining accuracy advantage in the asymptotic region, and the trick
217/// was already in this crate, in the dormant fdlibm `bessel_j0` port at
218/// `crates/thermite-special/src/specialized/ps.rs`. It did not survive the move to the
219/// Boost-shaped kernel.
220///
221/// Gated at `Average` and above, which includes the default policy. Below that the pair is
222/// returned unrepaired. See the comment in the body for why that beats the cheap algebraic
223/// `-cos 2x` it replaced.
224#[inline(always)]
225fn sum_diff_repaired<P, E, V>(ax: V, sx: V, cx: V) -> (V, V)
226where
227    E: FloatElement,
228    V: FloatVector<Element = E> + TranscendentalMathWithPolicy,
229    P: Policy,
230{
231    let cc = sx + cx;
232    let ss = sx - cx;
233
234    // Below `Average` the pair is returned unrepaired, which is the measured second-best form.
235    // The cheap algebraic repair, `-cos 2x = 1 - 2cos^2 x`, cancels exactly where it is needed
236    // (`cx` near `1/sqrt2`, so `cx*cx` carries ~1.3e-16 absolute, ~2.6e-16 in the result),
237    // while the plain `sx - cx` is already Sterbenz-exact there and limited only by sin/cos's
238    // own half-ulp: about 2.4x worse, for two extra ops and two divides.
239    //
240    // The gate is `ge(Average)`, so the DEFAULT policy repairs. `gt` would have excluded every
241    // plain, non-`_p` call.
242    if const { !P::POLICY.precision.ge(PrecisionPolicy::Average) } {
243        return (cc, ss);
244    }
245
246    // `-cos(2x)`, from a fresh call at the doubled argument. `ax + ax` is exact, so this is the
247    // only form with RELATIVE accuracy near its own zero, which is where the repair is used.
248    let neg_cos2x = -(ax + ax).cos_p::<P>();
249
250    // The replacement is formed from the ORIGINAL large one, then selected in. Computing it
251    // from an already-repaired other would feed the repair its own rounding. Only one of the
252    // pair is ever rebuilt per lane, so the divisor is selected and the division happens once.
253    let fix_cc = (sx * cx).is_negative();
254    let rebuilt = neg_cos2x / fix_cc.select(ss, cc);
255
256    (fix_cc.select(rebuilt, cc), fix_cc.select(ss, rebuilt))
257}
258
259/// The shared Hankel asymptotic for `x > 8`.
260///
261/// ```math
262/// J_\nu(x) = \sqrt{\frac{1}{\pi x}}\left(R_c \cos z - \tfrac{8}{x} R_s \sin z\right),
263/// \qquad z = x - \left(\tfrac{\nu}{2} + \tfrac14\right)\pi
264/// ```
265///
266/// Written out through the sin/cos addition formulae instead of forming `z`. Two reasons, and
267/// the second is the important one: it saves a subtraction, and more to the point `x - z_0`
268/// for a large `x` and an irrational `z_0` loses exactly the low bits that set the phase. The
269/// `$\sin(\pi/4) = \cos(\pi/4) = 1/\sqrt2$` factors then cancel against the `$1/\sqrt{\pi x}$`
270/// out front, which is why no `$1/\sqrt2$` appears anywhere below.
271#[inline(always)]
272fn hankel<P, E, V, const NH: usize>(ax: V, pc: [E; NH], qc: [E; NH], ps: [E; NH], qs: [E; NH], order_one: bool) -> V
273where
274    E: FloatElement,
275    V: FloatVector<Element = E> + TranscendentalMathWithPolicy,
276    P: Policy,
277{
278    let y = V::splat(E::from_int(8)) / ax;
279    let y2 = y * y;
280    let rc = y2.poly_rational_n_p::<Comp<P>, NH, NH>(&pc, &qc);
281    let rs = y2.poly_rational_n_p::<Comp<P>, NH, NH>(&ps, &qs);
282    let (sx, cx) = ax.sin_cos_p::<P>();
283    // `cc = sin+cos`, `ss = sin-cos`, each repaired where it cancels.
284    let (cc, ss) = sum_diff_repaired::<P, E, V>(ax, sx, cx);
285    // `sqrt(1/(pi x))`, not `(1/sqrt pi) / sqrt(x)`. Same op count (one divide, one sqrt), but
286    // the sqrt HALVES the relative error entering it instead of adding to it, so the constant's
287    // and the divide's roundings cost half as much: ~1 ulp against ~1.5.
288    let factor = (V::FRAC_1_PI / ax).sqrt();
289
290    let yrs = y * rs;
291    let value = if order_one {
292        factor * yrs.mul_adde(cc, rc * ss)
293    } else {
294        factor * yrs.nmul_adde(ss, rc * cc)
295    };
296    // Zero at infinity, where `sin_cos` is NaN and `factor` is 0.
297    ax.cmp_eq(V::INFINITY).select(V::ZERO, value)
298}
299
300/// `$Y_0(x)$` and `$Y_1(x)$`, selected by `ORDER_ONE`.
301///
302/// Both are `$\frac{2}{\pi}\ln(x/x_k)J_\nu(x) + \text{(root-factored rational)}$` below 8, and
303/// the shared Hankel above it. Undefined for `x <= 0`: NaN there, and `$-\infty$` at 0.
304#[inline(always)]
305#[allow(clippy::too_many_arguments)]
306pub fn bessel_y_impl<
307    P,
308    E,
309    V,
310    const N1: usize,
311    const N2: usize,
312    const N3: usize,
313    const NH: usize,
314    const J1: usize,
315    const J2: usize,
316    const JH: usize,
317    const ORDER_ONE: bool,
318>(
319    x: V,
320    t: &BesselY<E, N1, N2, N3, NH>,
321    tj: &BesselJ<E, J1, J2, JH>,
322) -> V
323where
324    E: FloatElement,
325    V: FloatVector<Element = E> + TranscendentalMathWithPolicy,
326    P: Policy,
327{
328    let eight = V::splat(E::from_int(8));
329    let small = x.cmp_le(eight);
330
331    let mut value = V::ZERO;
332
333    if small.any() {
334        let y = x * x;
335        let j = if const { ORDER_ONE } {
336            bessel_j1_impl::<P, E, V, J1, J2, JH>(x, tj)
337        } else {
338            bessel_j0_impl::<P, E, V, J1, J2, JH>(x, tj)
339        };
340        let two_over_pi = V::FRAC_2_PI;
341
342        // Every region shares the shape. Only the rational, the root and the log's base move.
343        // The three are evaluated and selected rather than branched, which is the standing
344        // trade here, but the `$\ln$` and the `$J$` are computed once for all of them.
345        // `Y_1`'s upper region spans (4, 8], so its argument runs to 64 against coefficients
346        // reaching 1.15e19, the same defect `J_1` had, and Boost has it in both. The table
347        // carries that rational re-expressed in `1 - x^2/64` (exact algebra, see
348        // `scripts/bessel_jy_tables.py`), so the argument here must match.
349        //
350        // `Y_0` stays on `x^2`: it bounds its argument the other way, by splitting (0, 8] into
351        // three narrow regions, and measures 4.43 against Boost's 37.49 for `Y_1`.
352        //
353        // Region 3 is `Y_1`'s duplicate of region 2 and is unreachable for it (`threshold2` is
354        // 8.0, so `in2` is always true below 8), but it shares the argument anyway rather than
355        // evaluating a `u`-basis rational at a `y`-basis point.
356        let arg2 = if const { ORDER_ONE } {
357            V::ONE - y * V::splat(E::from_ratio(1, 64))
358        } else {
359            y
360        };
361
362        let in1 = x.cmp_le(V::splat(t.threshold1));
363        let in2 = x.cmp_le(V::splat(t.threshold2));
364
365        let a = root_factor::<E, V>(x, V::splat(t.root1), V::splat(t.root1_hi), V::splat(t.root1_lo))
366            * y.poly_rational_n_p::<Comp<P>, N1, N1>(&t.p1, &t.q1);
367        let b = root_factor::<E, V>(x, V::splat(t.root2), V::splat(t.root2_hi), V::splat(t.root2_lo))
368            * arg2.poly_rational_n_p::<Comp<P>, N2, N2>(&t.p2, &t.q2);
369
370        let mut rat = if const { ORDER_ONE } {
371            // `Y_1` has only TWO regions. It fills the third slot with a copy of the second so
372            // the table keeps one shape. `threshold2` is 8.0, so `in2` is true across this
373            // whole branch and region 3 is unreachable. Evaluating it anyway cost a full
374            // degree-9/9 rational per call, discarded.
375            b
376        } else {
377            // `Y_0` genuinely has three. Skip region 3's rational when no lane is in it. The
378            // `x > 8` branch already guards itself this way.
379            let c = if in2.all() {
380                V::ZERO
381            } else {
382                root_factor::<E, V>(x, V::splat(t.root3), V::splat(t.root3_hi), V::splat(t.root3_lo))
383                    * arg2.poly_rational_n_p::<Comp<P>, N3, N3>(&t.p3, &t.q3)
384            };
385            in2.select(b, c)
386        };
387        rat = in1.select(a, rat);
388
389        let root = in1.select(V::splat(t.root1), in2.select(V::splat(t.root2), V::splat(t.root3)));
390        // `ln(x / x_k)`, about the region's own root: a bare `ln x` would make this term
391        // dominate the rational near the zero and take the accuracy with it.
392        //
393        // Not `ln_1p(delta / x_k)` off the exactly-split delta (LOG 2026-08-30): near the root
394        // the quotient's rounding vanishes with the term (under 0.4 eps envelope-relative),
395        // and at the far end of region 1 (`x ~ 1e-3`) `delta / x_k` approaches -1 and cancels
396        // INSIDE `ln_1p`, 142x worse at `x = 0.00114` and `Y_0` at 10.85 eps against libm's 0.13.
397        let z = two_over_pi * (x / root).ln_p::<P>() * j;
398
399        // `J_1`'s factored form carries an extra `1/x` on the Y side. Folding the addend into
400        // an FMA measured zero change on all three compensation arms (LOG 2026-08-31): the
401        // 1.58 of 2.20 eps here is the `ln` and the rounded `2/pi`.
402        value = if const { ORDER_ONE } { z + rat / x } else { z + rat };
403    }
404
405    if !small.all() {
406        // Y's Hankel is J's with sin and cos exchanged (the two are a quarter-period apart).
407        let y = eight / x;
408        let y2 = y * y;
409        let rc = y2.poly_rational_n_p::<Comp<P>, NH, NH>(&t.pc, &t.qc);
410        let rs = y2.poly_rational_n_p::<Comp<P>, NH, NH>(&t.ps, &t.qs);
411        let (sx, cx) = x.sin_cos_p::<P>();
412        let (cc, ss) = sum_diff_repaired::<P, E, V>(x, sx, cx);
413        // See `hankel`: the sqrt halves the incoming relative error rather than adding to it.
414        let factor = (V::FRAC_1_PI / x).sqrt();
415        let yrs = y * rs;
416        let hi = if const { ORDER_ONE } {
417            factor * yrs.mul_sube(ss, rc * cc)
418        } else {
419            factor * yrs.mul_adde(cc, rc * ss)
420        };
421        // Zero at infinity, as for `J`.
422        let hi = x.cmp_eq(V::INFINITY).select(V::ZERO, hi);
423        value = small.select(value, hi);
424    }
425
426    // No reflection: Y has a branch cut on the negative axis.
427    x.cmp_lt(V::ZERO).select(V::NAN, value)
428}
429
430/// `(Y_{N-1}, Y_N)` by **upward** recurrence from the two closed forms.
431///
432/// ```math
433/// Y_{n+1}(x) = \frac{2n}{x} Y_n(x) - Y_{n-1}(x)
434/// ```
435///
436/// `$Y_\nu$` is the dominant solution of Bessel's equation, so upward is stable and costs
437/// exactly `N - 1` steps, with no trip count, `x` dependence or precision tier. Same argument
438/// and same shape as [`bessel_kn_recur`](super::ik::bessel_kn_recur). The only difference
439/// is the sign, since this is the unmodified equation.
440///
441/// Measured against mpmath over orders 2..50 and `x` in 0.1..300: worst **2.8e-14** relative.
442/// Looser than `K`'s 1.4e-15 because this recurrence _subtracts_ where `K`'s adds, so it does
443/// accumulate a little cancellation, but `Y_n` grows with `n`, which keeps it bounded.
444#[inline(always)]
445pub fn bessel_yn_recur<E, V, const N: i32>(x: V, y0: V, y1: V) -> (V, V)
446where
447    E: FloatElement,
448    V: FloatVector<Element = E>,
449{
450    let mut prev = y0;
451    let mut cur = y1;
452    let two_over_x = V::TWO / x;
453
454    // `N` is signed and the loop walks to `|N|`: the reflection `Y_{-n} = (-1)^n Y_n` is a
455    // sign the CALLER applies, because the recurrence itself has no notion of a negative
456    // order. Taking the absolute value here rather than in a const-generic argument is
457    // forced: `foo::<{ N.unsigned_abs() }>` needs `generic_const_exprs`, which is not
458    // stable. A `const` block folds identically, so trip counts still unroll.
459    let m = const { N.unsigned_abs() as usize };
460
461    let mut n = 1usize;
462    while n < m {
463        let next = two_over_x.mul_sube(V::splat(E::from_int(n as _)) * cur, prev);
464        prev = cur;
465        cur = next;
466        n += 1;
467    }
468
469    (prev, cur)
470}
471
472/// `(J_{N-1}, J_N)` for `N >= 2`.
473///
474/// # Two arms, and why `J` gets a crossover that `I` did not
475///
476/// `$J_\nu$` is the minimal solution, so upward recurrence is unstable _in general_, and yet
477/// measured against mpmath it is **exact whenever `N < x`**: worst 3.08e-16 envelope-relative
478/// over orders 2..50 and `x` to 300.
479///
480/// That is the reverse of what happened for `$I_\nu$`, where forward recurrence failed even
481/// far into the region the textbook rule blesses, and the reason is worth writing down because
482/// the two look like the same recurrence. `I` accumulates **cancellation**:
483/// `I_{k+1} = I_{k-1} - (2k/x)I_k` subtracts two nearly equal numbers for `k << x`, losing bits
484/// every step regardless of which solution dominates. `J` oscillates, so its terms are not
485/// systematically close and there is nothing to cancel. What is left is dominant-solution
486/// admixture, and `Y_n/J_n` is `O(1)` precisely while `N < x`.
487///
488/// So the split is on `N < x`, and **both arms are bounded by `N` alone**: forward costs
489/// `N - 1` steps, and the downward arm is only reached where `x <= N`, which caps its
490/// `x`-scaled trip count at `0.35N`. No `O(x)` tail, and therefore no asymptotic arm needed.
491///
492/// # The normalization, and the trap in it
493///
494/// The downward arm produces ratios `r_k = J_k/J_{k-1}` and recovers `J_N = J_0 \prod r_k`.
495/// That normalization is fine for `$I$`, whose order-0 value is positive everywhere, and
496/// **wrong for `$J$`**, whose `J_0` vanishes at 2.405, 5.520, ... Boost normalizes by `J_0`
497/// regardless. Here the seed is whichever of `J_0`, `J_1` is larger in magnitude. They have no
498/// common zero, so one of them is always well away from zero. `J_1` seeding divides the
499/// product by `r_1`, which is exactly `J_0/J_1` and cancels the bad factor rather than
500/// carrying it.
501#[inline(always)]
502#[allow(clippy::too_many_arguments)]
503pub fn bessel_jn_pair_impl<
504    P,
505    E,
506    V,
507    const A1: usize,
508    const A2: usize,
509    const AH: usize,
510    const B1: usize,
511    const B2: usize,
512    const BH: usize,
513    const N: i32,
514>(
515    x: V,
516    t0: &BesselJ<E, A1, A2, AH>,
517    t1: &BesselJ<E, B1, B2, BH>,
518) -> (V, V)
519where
520    E: FloatElement,
521    V: FloatVector<Element = E> + TranscendentalMathWithPolicy,
522    P: Policy,
523{
524    let ax = x.abs();
525    let j0 = bessel_j0_impl::<P, E, V, A1, A2, AH>(ax, t0);
526    let j1 = bessel_j1_impl::<P, E, V, B1, B2, BH>(ax, t1);
527
528    // Evaluated at `|N|`. `J_{-n} = (-1)^n J_n` is the caller's to apply. See
529    // `bessel_yn_recur` for why the absolute value cannot live in the generic argument.
530    let m = const { N.unsigned_abs() as usize };
531
532    let n_f = V::splat(E::from_int(m as _));
533    let two_over_x = V::TWO / ax;
534
535    // `N < x`: forward is exact here, and costs N-1 FMAs.
536    let use_fwd = ax.cmp_gt(n_f);
537
538    let mut value = V::ZERO;
539    let mut prev_out = V::ZERO;
540
541    if use_fwd.any() {
542        let mut prev = j0;
543        let mut cur = j1;
544        let mut n = 1usize;
545        while n < m {
546            let next = two_over_x.mul_sube(V::splat(E::from_int(n as _)) * cur, prev);
547            prev = cur;
548            cur = next;
549            n += 1;
550        }
551        value = cur;
552        prev_out = prev;
553    }
554
555    if !use_fwd.all() {
556        // Downward ratio recurrence, `r_k = 1/(2k/x - r_{k+1})`. The minus is the whole
557        // difference from the `I` version. The ratios are no longer confined to (0,1), but
558        // they still cannot overflow, because a near-pole in one is followed by a near-zero
559        // in the next and the running product telescopes.
560        let (cn, cd) = const { super::ik::recurrence_x_coeff(P::POLICY.precision) };
561        let coeff = V::splat(E::from_ratio(cn, cd));
562        let start_f = V::splat(E::from_int((m + super::ik::RECURRENCE_MARGIN) as _));
563        let nm1_f = V::splat(E::from_int((m as i64) - 1));
564
565        let mut k = (!use_fwd).select(ax.mul_adde(coeff, start_f).ceil(), V::ZERO);
566        let mut r = V::ZERO;
567        // Both products EXCLUDE `r_1`, and `r_1` is carried separately. Folding it in and
568        // dividing it back out looks equivalent and is not: at a zero of `J_0`, `r_1 = J_1/J_0`
569        // overflows to infinity, and then `(j1 / inf) * inf` is NaN rather than the right
570        // answer. Keeping `r_1` out of the product means the `J_1` seed never has to undo it.
571        let mut prod = V::ONE;
572        let mut prod_prev = V::ONE;
573        let mut r1 = V::ONE;
574        let two_f = V::splat(E::from_int(2));
575
576        loop {
577            let active = k.cmp_ge(V::ONE);
578            if active.none() {
579                break;
580            }
581            r = active.select(V::ONE / two_over_x.mul_sube(k, r), r);
582            let in_prod = active & k.cmp_ge(two_f);
583            prod = (in_prod & k.cmp_le(n_f)).select(prod * r, prod);
584            prod_prev = (in_prod & k.cmp_le(nm1_f)).select(prod_prev * r, prod_prev);
585            // The last rung of the descent is r_1 = J_1/J_0.
586            r1 = (active & k.cmp_le(V::ONE)).select(r, r1);
587            k -= V::ONE;
588        }
589
590        // Seed from whichever closed form is further from its own zeros. `J_0` and `J_1` have
591        // no common zero, so one of them is always healthy, and `J_0` vanishes at 2.405,
592        // 5.520, ... where Boost's unconditional `J_0` normalization has nothing to divide by.
593        // The unused arm may evaluate to NaN. `select` is bitwise, so it does not propagate.
594        let use_j0 = j0.abs().cmp_ge(j1.abs());
595        let base = use_j0.select(j0 * r1, j1);
596        value = use_fwd.select(value, base * prod);
597        prev_out = use_fwd.select(prev_out, base * prod_prev);
598    }
599
600    // J_N has the parity of N, and J_{N-1} the opposite.
601    let odd = x.is_negative();
602    let v = if const { N % 2 == 0 } { value } else { value.neg_c(odd) };
603    let p = if const { N % 2 == 0 } {
604        prev_out.neg_c(odd)
605    } else {
606        prev_out
607    };
608    (p, v)
609}
610
611/// `Y_n(x)` with a **per-lane** order.
612///
613/// Upward recurrence with each lane freezing at its own order, the `hermitev` shape. `Y` is
614/// the dominant solution so there is only ever one arm, which makes this the simplest of the
615/// four runtime-order entry points.
616#[inline(always)]
617#[allow(clippy::too_many_arguments)]
618pub fn bessel_yv_impl<
619    P,
620    E,
621    V,
622    const A1: usize,
623    const A2: usize,
624    const A3: usize,
625    const AH: usize,
626    const B1: usize,
627    const B2: usize,
628    const B3: usize,
629    const BH: usize,
630    const J1: usize,
631    const J2: usize,
632    const JH: usize,
633    const K1: usize,
634    const K2: usize,
635    const KH: usize,
636>(
637    x: V,
638    n: V,
639    t0: &BesselY<E, A1, A2, A3, AH>,
640    t1: &BesselY<E, B1, B2, B3, BH>,
641    tj0: &BesselJ<E, J1, J2, JH>,
642    tj1: &BesselJ<E, K1, K2, KH>,
643) -> V
644where
645    E: FloatElement,
646    V: FloatVector<Element = E> + TranscendentalMathWithPolicy,
647    P: Policy,
648{
649    let y0 = bessel_y_impl::<P, E, V, A1, A2, A3, AH, J1, J2, JH, false>(x, t0, tj0);
650    let y1 = bessel_y_impl::<P, E, V, B1, B2, B3, BH, K1, K2, KH, true>(x, t1, tj1);
651
652    let two_over_x = V::TWO / x;
653    let mut prev = y0;
654    let mut cur = y1;
655    let mut step = V::ONE;
656
657    loop {
658        let cont = step.cmp_lt(n);
659        if cont.none() {
660            break;
661        }
662        let next = two_over_x.mul_sube(step * cur, prev);
663        prev = cont.select(cur, prev);
664        cur = cont.select(next, cur);
665        step += V::ONE;
666    }
667
668    n.cmp_le(V::ZERO).select(y0, cur)
669}
670
671/// `J_n(x)` with a **per-lane** order.
672///
673/// Both arms, selected per lane as in the const form: forward where `n < x`, downward ratio
674/// otherwise. The forward arm freezes each lane at its own order. The downward arm was already
675/// masked on `k <= N`, so a vector `n` slots straight in.
676///
677/// The `J_0`-zero guard carries over unchanged and matters just as much: `r_1` is kept out of
678/// the running product and folded into the `J_0` seed, so a lane sitting on a zero of `J_0`
679/// takes the `J_1` seed without ever forming `inf * 0`.
680#[inline(always)]
681#[allow(clippy::too_many_arguments)]
682pub fn bessel_jv_impl<
683    P,
684    E,
685    V,
686    const A1: usize,
687    const A2: usize,
688    const AH: usize,
689    const B1: usize,
690    const B2: usize,
691    const BH: usize,
692>(
693    x: V,
694    n: V,
695    t0: &BesselJ<E, A1, A2, AH>,
696    t1: &BesselJ<E, B1, B2, BH>,
697) -> V
698where
699    E: FloatElement,
700    V: FloatVector<Element = E> + TranscendentalMathWithPolicy,
701    P: Policy,
702{
703    let ax = x.abs();
704    let j0 = bessel_j0_impl::<P, E, V, A1, A2, AH>(ax, t0);
705    let j1 = bessel_j1_impl::<P, E, V, B1, B2, BH>(ax, t1);
706    let two_over_x = V::TWO / ax;
707
708    let use_fwd = ax.cmp_gt(n);
709    let mut value = V::ZERO;
710
711    if use_fwd.any() {
712        let mut prev = j0;
713        let mut cur = j1;
714        let mut step = V::ONE;
715        loop {
716            let cont = step.cmp_lt(n) & use_fwd;
717            if cont.none() {
718                break;
719            }
720            let next = two_over_x.mul_sube(step * cur, prev);
721            prev = cont.select(cur, prev);
722            cur = cont.select(next, cur);
723            step += V::ONE;
724        }
725        value = n.cmp_le(V::ZERO).select(j0, cur);
726    }
727
728    if !use_fwd.all() {
729        let (cn, cd) = const { super::ik::recurrence_x_coeff(P::POLICY.precision) };
730        let coeff = V::splat(E::from_ratio(cn, cd));
731        let margin = V::splat(E::from_int(super::ik::RECURRENCE_MARGIN as _));
732        let two_f = V::splat(E::from_int(2));
733
734        let mut k = (!use_fwd).select(ax.mul_adde(coeff, n + margin).ceil(), V::ZERO);
735        let mut r = V::ZERO;
736        let mut prod = V::ONE;
737        let mut r1 = V::ONE;
738
739        loop {
740            let active = k.cmp_ge(V::ONE);
741            if active.none() {
742                break;
743            }
744            r = active.select(V::ONE / two_over_x.mul_sube(k, r), r);
745            prod = (active & k.cmp_ge(two_f) & k.cmp_le(n)).select(prod * r, prod);
746            r1 = (active & k.cmp_le(V::ONE)).select(r, r1);
747            k -= V::ONE;
748        }
749
750        let use_j0 = j0.abs().cmp_ge(j1.abs());
751        let base = use_j0.select(j0 * r1, j1);
752        // Order 0 has an empty product AND no `r_1` factor, so it is the seed itself.
753        let down = n.cmp_le(V::ZERO).select(j0, base * prod);
754        value = use_fwd.select(value, down);
755    }
756
757    let odd_order = (n * V::HALF).fract().cmp_gt(V::ZERO);
758    value.neg_c(odd_order & x.is_negative())
759}
Last built: 2026-09-08 21:35:55 UTC