thermite_special/specialized/generic/bessel/half.rs
1//! Bessel functions at **half-integer order**, where all four families are elementary.
2//!
3//! `$J_{1/2}(x) = \sqrt{2/\pi x}\,\sin x$` and `$J_{-1/2}(x) = \sqrt{2/\pi x}\,\cos x$`. The
4//! modified pair swaps the circular functions for hyperbolic ones and `$K$` is a bare
5//! exponential. Every other half-integer order follows from the same three-term recurrence the
6//! whole family obeys, so this arm needs **no continued fraction, no `$\Gamma$`, and no
7//! series**: one `sin_cos` (or one `exp_m1`), one `sqrt`, and a bounded walk.
8//!
9//! This is the spherical Bessel family wearing different clothes:
10//! `$j_n(x) = \sqrt{\pi/2x}\,J_{n+1/2}(x)$` and `$y_n(x) = \sqrt{\pi/2x}\,Y_{n+1/2}(x)$`.
11//!
12//! # Boost does not do this, and that is not an oversight to copy
13//!
14//! Boost special-cases `$\nu = 1/2$` for `cyl_bessel_i` only. Its
15//! `cyl_bessel_j` runs the full Steed machinery at every half-integer order, and its spherical
16//! functions are thin wrappers that call straight back into it. So the
17//! elementary route below is _not_ a port. It is the identity Boost declines to exploit,
18//! presumably because a scalar library gains little from it. Under SIMD it is the difference
19//! between two continued fractions and a `sin_cos`.
20//!
21//! What Boost's caution _is_ about is real: **the unstable recurrence direction is still
22//! unstable at half-integer order.** `$J$` and `$I$` are the minimal solutions and `$Y$` and
23//! `$K$` the dominant ones, exactly as at whole order, so the two arms here reuse the two
24//! shapes the integer-order kernels measured, see
25//! [`super::jy::bessel_jn_pair_impl`] and [`super::ik::bessel_in_pair_impl`].
26//!
27//! # Domain
28//!
29//! `$x > 0$`. At half-integer order these functions carry a `$\sqrt{x}$` and are genuinely
30//! complex for negative `$x$`, so unlike the integer-order entry points there is no sign to
31//! fold. The caller gets a NaN out of the `sqrt`.
32
33use thermite::{
34 math::{TranscendentalMathWithPolicy, policy::Policy},
35 prelude::*,
36};
37
38use thermite::element::FloatElement;
39
40/// The walk both oscillating half-integer families share, given their four seeds.
41///
42/// Returns `$(J_{a-1}, J_a, Y_{a-1}, Y_a)$`, the neighbour as well as the wanted order,
43/// because every derivative identity in this family reaches **down** one and the walk passes
44/// through it anyway.
45///
46/// # Why the spherical functions reuse this unchanged
47///
48/// `$j_n(x) = \sqrt{\pi/2x}\,J_{n+1/2}(x)$`, and that factor **does not depend on the order**.
49/// So the spherical family satisfies the same three-term recurrence with the same
50/// coefficients, and differs only in its seeds. Passing spherically-normalised seeds in gives
51/// spherically-normalised values out, with no rescaling anywhere and, more usefully, without
52/// ever forming the `$\sqrt{2/\pi x}$` that would then have to be cancelled against a
53/// `$\sqrt{\pi/2x}$`. See [`super::spherical`].
54///
55/// `a` is the order on the **cylindrical** grid, `n + 1/2`, in both cases.
56#[inline(always)]
57pub(super) fn walk_jy<P, E, V>(x: V, a: V, j_lo: V, j_hi: V, y_lo: V, y_hi: V) -> (V, V, V, V)
58where
59 E: FloatElement,
60 V: FloatVector<Element = E>,
61 P: Policy,
62{
63 // `m` is whole and non-negative: the number of recurrence steps up from order 1/2.
64 let m = a - V::HALF;
65 let two_over_x = V::TWO / x;
66
67 // ---- upward, for Y always and for J while the order stays under x --------------------
68 //
69 // `Y` is the dominant solution, so upward is its stable direction at every order. `J` is
70 // the minimal one and upward is safe only below `x`. The lanes where it is not are
71 // overwritten by the downward arm below.
72 let use_fwd = x.cmp_gt(a);
73
74 let mut jp = j_lo;
75 let mut jc = j_hi;
76 let mut yp = y_lo;
77 let mut yc = y_hi;
78
79 // `h` is the order the step is taken _at_: 1/2, 3/2, ... The recurrence is
80 // `f_{h+1} = (2h/x) f_h - f_{h-1}`.
81 let mut h = V::HALF;
82 let mut step = V::ONE;
83
84 loop {
85 let live = step.cmp_le(m);
86 if live.none() {
87 break;
88 }
89 V::_loop_hint();
90
91 let coeff = two_over_x * h;
92
93 let jn = coeff.mul_sube(jc, jp);
94 jp = live.select(jc, jp);
95 jc = live.select(jn, jc);
96
97 let yn = coeff.mul_sube(yc, yp);
98 yp = live.select(yc, yp);
99 yc = live.select(yn, yc);
100
101 h += V::ONE;
102 step += V::ONE;
103 }
104
105 // The seed pair is `(f_{-1/2}, f_{1/2})`, so the wanted order is the _second_ slot: after
106 // `m` steps `cur` holds order `m + 1/2`, and at `m = 0` the loop never ran and it is still
107 // the seed. (`bessel_jy_real`'s walk reads its `prev` instead, since its Temme seed pair starts
108 // _at_ the base order rather than one below it.)
109 let (mut j_prev, mut j_a) = (jp, jc);
110
111 // ---- downward on ratios, where forward is unstable -----------------------------------
112 //
113 // Identical in shape to the integer-order arm, with the order grid offset by 1/2:
114 // `r_h = J_h/J_{h-1}` satisfies `r_h = 1/(2h/x - r_{h+1})`, seeded at zero well above the
115 // wanted order and walked down. The trip count and its tier come from the same two
116 // constants the integer kernel measured.
117 if !use_fwd.all() {
118 let (cn, cd) = const { super::ik::recurrence_x_coeff(P::POLICY.precision) };
119 let coeff = V::splat(E::from_ratio(cn, cd));
120 let margin = V::splat(E::from_int(super::ik::RECURRENCE_MARGIN as _));
121
122 // Start above the wanted order by the same margin, on the half-integer grid.
123 let mut k = (!use_fwd).select(x.mul_adde(coeff, a + margin).ceil() + V::HALF, V::ZERO);
124
125 let mut r = V::ZERO;
126 let mut prod = V::ONE;
127 let mut prod_prev = V::ONE;
128 let mut r_half = V::ONE;
129
130 let three_halves = thermite::const_splat!(ratio <E>: 3 / 2);
131 let a_prev = a - V::ONE;
132
133 loop {
134 let active = k.cmp_ge(V::HALF);
135 if active.none() {
136 break;
137 }
138 V::_loop_hint();
139
140 r = active.select(V::ONE / two_over_x.mul_sube(k, r), r);
141
142 let in_range = active & k.cmp_ge(three_halves);
143 prod = (in_range & k.cmp_le(a)).select(prod * r, prod);
144 // One factor short of `prod`, which is order `a - 1`. The walk visits it either
145 // way, so carrying it costs one select rather than a second pass.
146 prod_prev = (in_range & k.cmp_le(a_prev)).select(prod_prev * r, prod_prev);
147
148 r_half = (active & k.cmp_le(V::HALF)).select(r, r_half);
149
150 k -= V::ONE;
151 }
152
153 // `J_{1/2}` vanishes at every multiple of pi and `J_{-1/2}` at every odd multiple of
154 // pi/2, and they share no zero, so normalising by whichever is larger is always safe.
155 // Seeding from `J_{-1/2}` multiplies by `r_{1/2} = J_{1/2}/J_{-1/2}`, which cancels
156 // the small value rather than dividing by it. Same trap, same fix, as the integer
157 // kernel's `J_0`/`J_1` choice.
158 let use_lo = j_lo.abs().cmp_ge(j_hi.abs());
159 let base = use_lo.select(j_lo * r_half, j_hi);
160
161 j_a = use_fwd.select(j_a, base * prod);
162 // At `a = 1/2` there is no order below `1/2` on this grid except the seed itself, and
163 // `prod_prev` is then the empty product, so this is `base`, which is `J_{1/2}` and not
164 // `J_{-1/2}`. The `m = 0` case is therefore the caller's to handle, and both callers
165 // do: it is exactly where the derivative identity folds back onto a seed.
166 j_prev = use_fwd.select(j_prev, base * prod_prev);
167 }
168
169 (j_prev, j_a, yp, yc)
170}
171
172/// `$(J_\nu(x), Y_\nu(x))$` at half-integer `$\nu$`, both signs of `$\nu$`, for `$x > 0$`.
173///
174/// `nu` must be exactly a half-odd-integer (`$\pm 1/2, \pm 3/2, \ldots$`). Whole orders do not
175/// belong here and are not detected: [`BesselOrder::simplify`](crate::BesselOrder::simplify)
176/// narrows `HalfInteger(2m)` to `Integer(m)` before any kernel sees it, which is why the tag
177/// stores a numerator.
178///
179/// # Negative order is a swap, not a rotation
180///
181/// The general rule at non-integer order is the rotation
182/// `$J_{-\nu} = J_\nu\cos\nu\pi - Y_\nu\sin\nu\pi$`. At `$\nu = m + 1/2$` the cosine vanishes
183/// **exactly** and the sine is `$(-1)^m$`, so the rotation degenerates into an exchange:
184///
185/// ```math
186/// J_{-(m+1/2)} = (-1)^{m+1}\,Y_{m+1/2}, \qquad Y_{-(m+1/2)} = (-1)^m\,J_{m+1/2}
187/// ```
188///
189/// No trigonometry is evaluated for it, and no cancellation is possible in it, which is the
190/// second reason this order class is worth its own kernel, since the general path pays a
191/// `sincos_pi` and a pair of products to reach the same answer less exactly.
192#[inline(always)]
193pub fn bessel_jy_half<P, E, V>(nu: V, x: V) -> (V, V)
194where
195 E: FloatElement,
196 V: FloatVector<Element = E> + TranscendentalMathWithPolicy,
197 P: Policy,
198{
199 let a = nu.abs();
200 let m = a - V::HALF;
201
202 // sqrt(2/(pi x)), the envelope every order here rides on.
203 let amp = (V::FRAC_2_PI / x).sqrt();
204 let (sin_x, cos_x) = x.sin_cos_p::<P>();
205
206 // The four seeds. `J_{-1/2}` and `Y_{-1/2}` are not extra work. They are the second
207 // element each recurrence needs, and they cost a negation apiece from the pair already
208 // in hand.
209 let j_lo = amp * cos_x; // J_{-1/2}
210 let j_hi = amp * sin_x; // J_{+1/2}
211 let y_lo = j_hi; // Y_{-1/2} = J_{1/2}
212 let y_hi = -j_lo; // Y_{+1/2} = -J_{-1/2}
213
214 let (_, j_a, _, y_a) = walk_jy::<P, E, V>(x, a, j_lo, j_hi, y_lo, y_hi);
215
216 // The origin: `J_a(0) = 0` and `Y_a(0) = -inf` at every positive half-integer order, and
217 // the exchange below turns those into the right signed infinities at negative order. The
218 // seeds are `inf * 0` there, so this is a select rather than something that falls out.
219 let zero = x.is_zero();
220 let j_a = zero.select(V::ZERO, j_a);
221 let y_a = zero.select(V::NEG_INFINITY, y_a);
222
223 // Both vanish at infinity, where the seeds are `0 * NaN`.
224 let inf = x.cmp_eq(V::INFINITY);
225 let j_a = inf.select(V::ZERO, j_a);
226 let y_a = inf.select(V::ZERO, y_a);
227
228 // ---- negative order: an exchange with a sign, no trigonometry -------------------------
229 let m_odd = (m * V::HALF).fract().cmp_gt(V::ZERO);
230 let reflected = nu.is_negative();
231
232 (
233 reflected.select(y_a.neg_c(!m_odd), j_a),
234 reflected.select(j_a.neg_c(m_odd), y_a),
235 )
236}
237
238/// The walk both modified half-integer families share, given their seeds.
239///
240/// Returns `$(I_{a-1}, I_a, K_{a-1}, K_a)$`. `K`'s two seeds are equal (`$K_{-1/2} =
241/// K_{1/2}$`), so it takes one value where `$I$` takes two.
242///
243/// Reused unchanged by the modified **spherical** family for the same reason
244/// [`walk_jy`] is: `$i_n(x) = \sqrt{\pi/2x}\,I_{n+1/2}(x)$` and that factor does not depend on
245/// the order, so spherically-normalised seeds give spherically-normalised values with no
246/// rescaling. `a` is the order on the cylindrical grid, `n + 1/2`.
247///
248/// Everything here is in the scaled domain, `$(e^{-x}I,\; e^{x}K)$`. The recurrences are
249/// homogeneous, so a uniform scaling passes straight through both.
250#[inline(always)]
251pub(super) fn walk_ik<P, E, V>(x: V, a: V, i_lo: V, i_hi: V, k_seed: V, asym_scale: V) -> (V, V, V, V)
252where
253 E: FloatElement,
254 V: FloatVector<Element = E> + TranscendentalMathWithPolicy,
255 P: Policy,
256{
257 let m = a - V::HALF;
258 let two_over_x = V::TWO / x;
259
260 // ---- K upward, always stable ---------------------------------------------------------
261 //
262 // The two seeds are equal, so the pair starts degenerate and the first step is what
263 // separates them.
264 let mut kp = k_seed;
265 let mut kc = k_seed;
266
267 let mut h = V::HALF;
268 let mut step = V::ONE;
269
270 loop {
271 let live = step.cmp_le(m);
272 if live.none() {
273 break;
274 }
275 V::_loop_hint();
276
277 // K_{h+1} = K_{h-1} + (2h/x) K_h: an addition, where J/Y had a subtraction. That
278 // sign is the whole difference between a dominant solution and a minimal one.
279 let kn = (two_over_x * h).mul_adde(kc, kp);
280 kp = live.select(kc, kp);
281 kc = live.select(kn, kc);
282
283 h += V::ONE;
284 step += V::ONE;
285 }
286
287 // ---- I: the asymptotic series at large x, the ratio recurrence below -------------------
288 //
289 // The handover is what makes the tiered trip count safe. The recurrence starts at
290 // `a + 24 + c*x` with `c` the tier's coefficient (0.35 at `Best`, 0.25 at `Average`), so
291 // below the top tier its accuracy decays with `x`, and past this threshold nothing calls
292 // it. Without the arm, `sph_bessel_i_scaled(n = 2)` measured 14131 ULP at `Average`
293 // against 4.32 at `Best`. Low orders suffer most, since a larger `a` buys its own
294 // headroom. Threshold and series are the integer kernel's own (`asymptotic_series_v`
295 // takes a per-lane order): 0.99 eps for `nu = 1/3` at `x = 40`, where it takes over.
296 let thresh = (a * a * V::splat(E::from_ratio(1, 3))).max(V::splat(E::from_int(40)));
297 let use_asym = x.cmp_ge(thresh);
298
299 let (cn, cd) = const { super::ik::recurrence_x_coeff(P::POLICY.precision) };
300 let coeff = V::splat(E::from_ratio(cn, cd));
301 let margin = V::splat(E::from_int(super::ik::RECURRENCE_MARGIN as _));
302
303 // Asymptotic lanes start at zero so they cannot drag the packet's trip count, the same
304 // guard `bessel_iv_impl` uses.
305 let mut k = (!use_asym).select(x.mul_adde(coeff, a + margin).ceil() + V::HALF, V::ZERO);
306 let mut r = V::ZERO;
307 let mut prod = V::ONE;
308 let mut prod_prev = V::ONE;
309
310 let a_prev = a - V::ONE;
311
312 loop {
313 let active = k.cmp_ge(V::HALF);
314 if active.none() {
315 break;
316 }
317 V::_loop_hint();
318
319 // `+ r` here, `- r` in the J arm: the modified equation flips it, and with it the
320 // guarantee that every `r_h` lands in `(0, 1)` so nothing needs rescaling.
321 r = active.select(V::ONE / two_over_x.mul_adde(k, r), r);
322 prod = (active & k.cmp_le(a)).select(prod * r, prod);
323 prod_prev = (active & k.cmp_le(a_prev)).select(prod_prev * r, prod_prev);
324
325 k -= V::ONE;
326 }
327
328 // `prod` spans `h = 1/2 ..= a`, so it is `I_a / I_{-1/2}`. At `a = 1/2` the direct seed is
329 // better than one continued-fraction step reproducing it, the same reason the integer
330 // entry point selects its order-0 and order-1 closed forms out of the ladder.
331 let mut i_a = a.cmp_le(V::HALF).select(i_hi, i_lo * prod);
332
333 // One order lower. At `a = 1/2` that is `I_{-1/2}`, which is the seed and where
334 // `prod_prev` is the empty product, so the same expression covers it.
335 let mut i_prev = i_lo * prod_prev;
336
337 if use_asym.any() {
338 // `far_threshold` is only read on the unscaled path, and this one is scaled.
339 let far = E::from_int(50);
340
341 // The recurrence is normalization-agnostic, which is what lets the spherical family
342 // reuse this walk. An absolute series is not: `asymptotic_series_v` computes the
343 // CYLINDRICAL `e^-x I_nu`, so a spherical caller passes `sqrt(pi/2x)` as
344 // `asym_scale` to land back in its own convention.
345 i_a = use_asym.select(
346 asym_scale * super::ik::asymptotic_series_v::<P, E, V, true>(x, a, far),
347 i_a,
348 );
349 i_prev = use_asym.select(
350 asym_scale * super::ik::asymptotic_series_v::<P, E, V, true>(x, a_prev, far),
351 i_prev,
352 );
353 }
354
355 (i_prev, i_a, kp, kc)
356}
357
358/// `$(I_\nu(x), K_\nu(x))$` at half-integer `$\nu$`, both signs of `$\nu$`, for `$x > 0$`.
359///
360/// With `SCALED`, returns `$(e^{-x}I_\nu(x),\; e^{x}K_\nu(x))$`, the same convention the
361/// integer-order entry points use, and the one this kernel works in **internally regardless**,
362/// because the seeds are otherwise unrepresentable: `$\sinh x$` overflows at `$x = 710$` while
363/// `$e^{-x}\sinh x$` is `$1/2$` forever. The unscaled form is the scaled one times an
364/// exponential, and pays that exponential's `$x\,\varepsilon/2$` relative error, which is the
365/// documented reason to prefer the scaled twin on accuracy grounds, not only on range.
366///
367/// # Seeds
368///
369/// ```math
370/// I_{1/2} = \sqrt{\tfrac{2}{\pi x}}\sinh x,\quad
371/// I_{-1/2} = \sqrt{\tfrac{2}{\pi x}}\cosh x,\quad
372/// K_{1/2} = K_{-1/2} = \sqrt{\tfrac{\pi}{2x}}\,e^{-x}
373/// ```
374///
375/// Scaled, `$e^{-x}\sinh x = -\mathrm{expm1}(-2x)/2$` and `$e^{-x}\cosh x = (1 + e^{-2x})/2$`,
376/// so **one `exp_m1` supplies both** and neither loses a bit to cancellation at small `$x$`,
377/// which the algebraically equal `$(1 - e^{-2x})/2$` would.
378///
379/// # Directions
380///
381/// `$K$` is the dominant solution and walks **upward**, `$n$` steps, no trip count and no `$x$`
382/// dependence. `$I$` is the minimal one and cannot: its upward recurrence subtracts nearly
383/// equal terms for `$k \ll x$` and loses bits every step whatever the order. So `$I$` takes the
384/// downward **ratio** recurrence `$r_h = 1/(2h/x + r_{h+1})$`, seeded at zero above the wanted
385/// order, exactly as the integer-order `$I$` kernel does and with the same two tier constants.
386///
387/// Unlike `$J$` there is no zero to trip over: `$I_{-1/2} = \sqrt{2/\pi x}\cosh x$` is positive
388/// everywhere, so the normalization needs no choice between two seeds.
389///
390/// # Negative order
391///
392/// `$K$` is even in `$\nu$` at every order and needs nothing. `$I$` is not, at non-integer
393/// order, and the reflection brings `$K$` in:
394///
395/// ```math
396/// I_{-(m+1/2)}(x) = I_{m+1/2}(x) + \tfrac{2}{\pi}(-1)^m K_{m+1/2}(x)
397/// ```
398///
399/// This is a genuine subtraction when `$m$` is odd, and `$I_{-(m+1/2)}$` really does have
400/// zeros: `$I_{-3/2}$` vanishes near `$x = 1.1997$`, where `$\tanh x = 1/x$`. The contract is
401/// absolute against the larger term, not relative, for the same reason it is for `$J$` at its
402/// zeros. Boost's `bessel_ik` carries the same formula with the same exposure.
403///
404/// `far_threshold` is where the unscaled form halves its exponential, see
405/// [`unscale_i`](super::ik::unscale_i). It comes from the `BesselI` table so every `$I$`
406/// arm in the crate turns that corner at the same `x`.
407#[inline(always)]
408pub fn bessel_ik_half<P, E, V, const SCALED: bool>(nu: V, x: V, far_threshold: E) -> (V, V)
409where
410 E: FloatElement,
411 V: FloatVector<Element = E> + TranscendentalMathWithPolicy,
412 P: Policy,
413{
414 let a = nu.abs();
415 let m = a - V::HALF;
416
417 let amp = (V::FRAC_2_PI / x).sqrt();
418
419 // `e^{-2x} - 1`, from which both hyperbolic seeds follow without cancellation.
420 let e2m1 = (-(x + x)).exp_m1_p::<P>();
421
422 let i_lo = amp * (V::TWO + e2m1) * V::HALF; // e^{-x} I_{-1/2} = amp (1 + e^{-2x})/2
423 let i_hi = amp * (-e2m1) * V::HALF; // e^{-x} I_{+1/2} = amp (1 - e^{-2x})/2
424 let k_seed = (V::FRAC_PI_2 / x).sqrt(); // e^{ x} K_{\pm 1/2}
425
426 // `asym_scale` is one: this kernel's seeds are already in the cylindrical normalization
427 // the asymptotic series produces.
428 let (_, i_a, _, k_a) = walk_ik::<P, E, V>(x, a, i_lo, i_hi, k_seed, V::ONE);
429
430 // The origin: `I_a(0) = 0` and `K_a(0) = +inf`, in either scaling. The reflection below
431 // then gives `I_{-a}(0)` its signed infinity through the `K` term.
432 let zero = x.is_zero();
433 let i_a = zero.select(V::ZERO, i_a);
434 let k_a = zero.select(V::INFINITY, k_a);
435
436 // ---- negative order ------------------------------------------------------------------
437 let m_odd = (m * V::HALF).fract().cmp_gt(V::ZERO);
438 let reflected = nu.is_negative();
439
440 // In the scaled domain the reflection's `K` term carries an extra `e^{-2x}`, since the two
441 // families are scaled in opposite directions.
442 //
443 // That factor is a **second** exponential and cannot be recovered as `1 + expm1(-2x)`:
444 // past about `x = 8` the `expm1` sits within an ulp of `-1`, so adding one back leaves
445 // `eps/2` absolute on a quantity of size `e^{-2x}` (`I_{-21/2}(15)` at 3.72e-14 against
446 // 3.85e-16 for `K`). The `any()` guard keeps the extra call off packets with no negative
447 // order, which is most of them.
448 let i_out = match reflected.any() {
449 false => i_a,
450 true => {
451 let k_term = (V::FRAC_2_PI * k_a * (-(x + x)).exp_p::<P>()).neg_c(m_odd);
452 reflected.select(i_a + k_term, i_a)
453 }
454 };
455
456 match const { SCALED } {
457 true => (i_out, k_a),
458 false => (
459 super::ik::unscale_i::<P, E, V>(i_out, x, far_threshold),
460 k_a * (-x).exp_p::<P>(),
461 ),
462 }
463}