Skip to main content

thermite_special/
bessel.rs

1//! The runtime Bessel order, and the cost class it selects.
2//!
3//! The const-order Bessel entry point ([`bessel_n`](crate::SpecialMath::bessel_n)) takes a
4//! whole-number order known at compile time. The runtime form
5//! ([`bessel`](crate::SpecialMath::bessel)) takes [`BesselOrder`] instead of a bare order
6//! value, because "what order is this?" and "what does that order cost?" are different
7//! questions and only the caller can answer the second one cheaply.
8//!
9//! The family markers ([`J`], [`Y`], [`I`], [`K`], [`Scaled`], and the Airy selectors) live
10//! in this module too. See [`BesselFamily`].
11//!
12//! # Why a tagged order rather than a plain `$\nu$`
13//!
14//! `$J_\nu$` at whole-number `$\nu$` is a table lookup plus a recurrence. At half-integer
15//! `$\nu$` it is elementary: sines and cosines. At arbitrary real `$\nu$` it is Steed's
16//! method: two continued fractions and a Temme series, one of which needs `$O(x)$`
17//! iterations. Those are three genuinely different algorithms with costs an order of
18//! magnitude apart, and **under SIMD the whole packet pays for whichever is selected**. A
19//! per-lane choice would make every lane pay every arm.
20//!
21//! So the class is carried as a _scalar_ tag and the order values as a vector. One packet
22//! runs one algorithm, and the caller can see in the type which one they asked for.
23//!
24//! # Exact by construction
25//!
26//! Each variant stores a **numerator**, not a rounded `$\nu$`: `HalfInteger(k)` means
27//! `$\nu = k/2$` and `Thirds(k)` means `$\nu = k/3$`. This is not decoration. `1/3` is not
28//! representable in binary, so a design that stored `$\nu$` as a float and tagged it
29//! separately could not distinguish `Thirds(1)` from a nearby real order, and the tag would
30//! be a promise the caller could break. Here the tag cannot disagree with the payload.
31//!
32//! # Downgrading
33//!
34//! [`simplify`](BesselOrder::simplify) narrows a value to the cheapest variant its data
35//! actually needs, so a caller who reaches for [`Real`](BesselOrder::Real) and happens to
36//! pass whole numbers gets the fast path anyway. The runtime forms call it themselves. It's
37//! public so a caller with a hot loop can hoist it out and pay the check once.
38//!
39//! The checks run in cost order and cost proportionally to how general the claim was, so
40//! [`Integer`](BesselOrder::Integer) checks nothing at all. Downgrading requires the
41//! condition to hold in **every** lane. One odd lane keeps the whole packet on the general
42//! path. A caller with a genuinely mixed packet can recover the fast path with
43//! [`group_by_value`](thermite::vector::PartialOrdVector::group_by_value), which turns a
44//! divergent packet into uniform sub-packets.
45//!
46//! ## `Real` does not downgrade to `Thirds`, deliberately
47//!
48//! `Real -> Integer` and `Real -> HalfInteger` are exact: whole numbers and halves are both
49//! representable, so the downgrade cannot change which function is evaluated.
50//!
51//! `Real -> Thirds` would not be. `fl(1.0/3.0)` is not `$1/3$`, so snapping it to
52//! `Thirds(1)` would silently evaluate a _different_ function than the caller asked for.
53//! Close, but wrong, and wrong in a way no test of `Thirds` itself would catch. A caller who
54//! wants an exact third writes `Thirds(1)`, which is the whole reason the variant carries a
55//! numerator.
56
57use thermite::math::policy::Policy;
58use thermite::math::scalar::Unwrap;
59use thermite::prelude::*;
60
61use crate::specialized::{SpecializedRealSpecialMath, SpecializedSpecialMath};
62
63// ---- Marker-selected entry points ----------------------------------------------------------
64//
65// `x.bessel_n::<J, 2>()`, `x.bessel::<Scaled<I>>(BesselOrder::Real(nu))`,
66// `x.sph_bessel::<K>(n)`, `x.airy::<Scaled<Ai>>()`. The family is a type parameter
67// (it never carries a value) and the order follows the crate's `_n` const / plain runtime
68// convention. Each marker's trait impl is the dispatch: there is no family enum and nothing
69// evaluates more than it was asked for. The markers reach the per-family hooks on
70// `SpecializedSpecialMath`, so a composite that overrides those hooks (`Dual`, `Complex`)
71// is reached through them with no marker-level work.
72
73mod sealed {
74    pub trait Sealed {}
75}
76
77/// Bessel function of the first kind, `$J_\nu$`. The oscillating minimal solution.
78#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
79pub struct J;
80
81/// Bessel function of the second kind, `$Y_\nu$` (Neumann). The oscillating dominant solution.
82#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
83pub struct Y;
84
85/// Modified Bessel function of the first kind, `$I_\nu$`. Grows like `$e^x$`.
86#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
87pub struct I;
88
89/// Modified Bessel function of the second kind, `$K_\nu$`. Decays like `$e^{-x}$`.
90#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
91pub struct K;
92
93/// The exponentially scaled form of a family or Airy function.
94///
95/// `Scaled(I)` is `$e^{-|x|} I_\nu$`, `Scaled(K)` is `$e^{x} K_\nu$`, `Scaled(Ai)` is
96/// `$e^{\zeta}\mathrm{Ai}$` on the positive axis. `Scaled(J)` and `Scaled(Y)` are SciPy's
97/// `jve`/`yve`, `$e^{-|\mathrm{Im}\,z|} J_\nu(z)$`: the factor is 1 on the real axis, so on a
98/// real vector they are `J` and `Y` unchanged and cost nothing extra. On a complex vector they
99/// are the scaled values. The scaled forms are never "the unscaled value times an
100/// exponential". Where the kernels are natively scaled they skip a transcendental and stay in
101/// range where the unscaled value has overflowed or underflowed.
102#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
103pub struct Scaled<F>(pub F);
104
105/// `$\mathrm{Ai}(x)$`.
106#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
107pub struct Ai;
108
109/// `$\mathrm{Ai}'(x)$`.
110#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
111pub struct AiPrime;
112
113/// `$\mathrm{Bi}(x)$`.
114#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
115pub struct Bi;
116
117/// `$\mathrm{Bi}'(x)$`.
118#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
119pub struct BiPrime;
120
121/// A Bessel family marker: [`J`], [`Y`], [`I`], [`K`], or one of them under [`Scaled`].
122///
123/// The methods are the dispatch, one per (cylindrical/spherical) x (const/runtime order)
124/// cell, each reaching the matching per-family hook on `SpecializedSpecialMath`. They are
125/// `#[doc(hidden)]` because nothing outside the marker layer calls them: the public spelling
126/// is [`bessel_n`](crate::SpecialMath::bessel_n) / [`bessel`](crate::SpecialMath::bessel) and
127/// the `sph_` pair. Cylindrical const orders are `i32` (the families reflect at negative
128/// order), spherical ones `usize`.
129pub trait BesselFamily: Copy + sealed::Sealed {
130    #[doc(hidden)]
131    fn cyl_n<P: Policy, E, V, const N: i32>(x: V) -> V
132    where
133        V: FloatVector<Element = E> + SpecializedSpecialMath<E>;
134
135    #[doc(hidden)]
136    fn cyl_v<P: Policy, E, V>(x: V, order: BesselOrder<V, V::Signed>) -> V
137    where
138        V: FloatVector<Element = E> + SpecializedSpecialMath<E>;
139
140    #[doc(hidden)]
141    fn sph_n<P: Policy, E, V, const N: usize>(x: V) -> V
142    where
143        V: FloatVector<Element = E> + SpecializedSpecialMath<E>;
144
145    #[doc(hidden)]
146    fn sph_v<P: Policy, E, V>(x: V, n: u32) -> V
147    where
148        V: FloatVector<Element = E> + SpecializedSpecialMath<E>;
149}
150
151/// Stamps `BesselFamily` for one marker from the four hooks it selects.
152macro_rules! impl_family {
153    ($($marker:ty => {
154        cyl_n: $cyl_n:ident $(::<$($cn:tt),*>)?,
155        cyl_v: $cyl_v:ident $(::<$($cv:tt),*>)?,
156        sph_n: $sph_n:ident,
157        sph_v: $sph_v:ident,
158    })*) => {$(
159        impl sealed::Sealed for $marker {}
160
161        impl BesselFamily for $marker {
162            #[inline(always)]
163            fn cyl_n<P: Policy, E, V, const N: i32>(x: V) -> V
164            where
165                V: FloatVector<Element = E> + SpecializedSpecialMath<E>,
166            {
167                <V as SpecializedSpecialMath<E>>::$cyl_n::<P, N $($(, $cn)*)?>(x)
168            }
169
170            #[inline(always)]
171            fn cyl_v<P: Policy, E, V>(x: V, order: BesselOrder<V, V::Signed>) -> V
172            where
173                V: FloatVector<Element = E> + SpecializedSpecialMath<E>,
174            {
175                <V as SpecializedSpecialMath<E>>::$cyl_v::<P $($(, $cv)*)?>(x, order)
176            }
177
178            #[inline(always)]
179            fn sph_n<P: Policy, E, V, const N: usize>(x: V) -> V
180            where
181                V: FloatVector<Element = E> + SpecializedSpecialMath<E>,
182            {
183                <V as SpecializedSpecialMath<E>>::$sph_n::<P, N>(x)
184            }
185
186            #[inline(always)]
187            fn sph_v<P: Policy, E, V>(x: V, n: u32) -> V
188            where
189                V: FloatVector<Element = E> + SpecializedSpecialMath<E>,
190            {
191                <V as SpecializedSpecialMath<E>>::$sph_v::<P>(x, n)
192            }
193        }
194    )*};
195}
196
197impl_family! {
198    J => { cyl_n: bessel_j, cyl_v: bessel_jv, sph_n: sph_bessel_j_n, sph_v: sph_bessel_j, }
199    Y => { cyl_n: bessel_y, cyl_v: bessel_yv, sph_n: sph_bessel_y_n, sph_v: sph_bessel_y, }
200    I => { cyl_n: bessel_i, cyl_v: bessel_iv::<false>, sph_n: sph_bessel_i_n, sph_v: sph_bessel_i, }
201    K => { cyl_n: bessel_k, cyl_v: bessel_kv::<false>, sph_n: sph_bessel_k_n, sph_v: sph_bessel_k, }
202    Scaled<I> => { cyl_n: bessel_i_scaled, cyl_v: bessel_iv::<true>, sph_n: sph_bessel_i_scaled_n, sph_v: sph_bessel_i_scaled, }
203    Scaled<K> => { cyl_n: bessel_k_scaled, cyl_v: bessel_kv::<true>, sph_n: sph_bessel_k_scaled_n, sph_v: sph_bessel_k_scaled, }
204    // The oscillating pair's scaling is a unit factor on the real axis: the const and
205    // spherical cells are the unscaled hooks outright, the runtime cell is the hook a
206    // complex vector overrides.
207    Scaled<J> => { cyl_n: bessel_j, cyl_v: bessel_jv_scaled, sph_n: sph_bessel_j_n, sph_v: sph_bessel_j, }
208    Scaled<Y> => { cyl_n: bessel_y, cyl_v: bessel_yv_scaled, sph_n: sph_bessel_y_n, sph_v: sph_bessel_y, }
209}
210
211/// A family with a ratio kernel `$F_\nu / F_{\nu-1}$` and its inverse, for
212/// [`bessel_ratio`](crate::RealSpecialMath::bessel_ratio) and its three companions. Only
213/// [`I`] today (the von Mises-Fisher quantities). A `K` ratio would be the next member.
214///
215/// Real vectors only, like the entries it serves: the ratio kernels compare and take
216/// absolute values along the real line.
217pub trait BesselRatioFamily: BesselFamily {
218    #[doc(hidden)]
219    fn ratio<P: Policy, E, V>(x: V, nu: V) -> V
220    where
221        V: FloatVector<Element = E> + SpecializedRealSpecialMath<E>;
222
223    #[doc(hidden)]
224    fn inv_ratio<P: Policy, E, V>(r: V, nu: V) -> V
225    where
226        V: FloatVector<Element = E> + SpecializedRealSpecialMath<E>;
227
228    #[doc(hidden)]
229    fn ratio_1m<P: Policy, E, V>(x: V, nu: V) -> V
230    where
231        V: FloatVector<Element = E> + SpecializedRealSpecialMath<E>;
232
233    #[doc(hidden)]
234    fn inv_ratio_1m<P: Policy, E, V>(t: V, nu: V) -> V
235    where
236        V: FloatVector<Element = E> + SpecializedRealSpecialMath<E>;
237}
238
239impl BesselRatioFamily for I {
240    #[inline(always)]
241    fn ratio<P: Policy, E, V>(x: V, nu: V) -> V
242    where
243        V: FloatVector<Element = E> + SpecializedRealSpecialMath<E>,
244    {
245        <V as SpecializedRealSpecialMath<E>>::bessel_i_ratio::<P>(x, nu)
246    }
247
248    #[inline(always)]
249    fn inv_ratio<P: Policy, E, V>(r: V, nu: V) -> V
250    where
251        V: FloatVector<Element = E> + SpecializedRealSpecialMath<E>,
252    {
253        <V as SpecializedRealSpecialMath<E>>::inv_bessel_i_ratio::<P>(r, nu)
254    }
255
256    #[inline(always)]
257    fn ratio_1m<P: Policy, E, V>(x: V, nu: V) -> V
258    where
259        V: FloatVector<Element = E> + SpecializedRealSpecialMath<E>,
260    {
261        <V as SpecializedRealSpecialMath<E>>::bessel_i_ratio_1m::<P>(x, nu)
262    }
263
264    #[inline(always)]
265    fn inv_ratio_1m<P: Policy, E, V>(t: V, nu: V) -> V
266    where
267        V: FloatVector<Element = E> + SpecializedRealSpecialMath<E>,
268    {
269        <V as SpecializedRealSpecialMath<E>>::inv_bessel_i_ratio_1m::<P>(t, nu)
270    }
271}
272
273/// An Airy selector: [`Ai`], [`AiPrime`], [`Bi`], [`BiPrime`], or one of them under
274/// [`Scaled`]. Each asks the kernel for exactly its own output, so the cost is one Bessel
275/// pass, the same as the long-form single entries.
276pub trait AiryFn: Copy + sealed::Sealed {
277    #[doc(hidden)]
278    fn eval<P: Policy, E, V, const SCALED: bool>(x: V) -> V
279    where
280        V: FloatVector<Element = E> + SpecializedSpecialMath<E>;
281}
282
283macro_rules! impl_airy_fn {
284    ($($marker:ident => $plain:ident / $scaled:ident;)*) => {$(
285        impl sealed::Sealed for $marker {}
286        impl sealed::Sealed for Scaled<$marker> {}
287
288        impl AiryFn for $marker {
289            #[inline(always)]
290            fn eval<P: Policy, E, V, const SCALED: bool>(x: V) -> V
291            where
292                V: FloatVector<Element = E> + SpecializedSpecialMath<E>,
293            {
294                if const { SCALED } {
295                    <V as SpecializedSpecialMath<E>>::$scaled::<P>(x)
296                } else {
297                    <V as SpecializedSpecialMath<E>>::$plain::<P>(x)
298                }
299            }
300        }
301
302        impl AiryFn for Scaled<$marker> {
303            #[inline(always)]
304            fn eval<P: Policy, E, V, const SCALED: bool>(x: V) -> V
305            where
306                V: FloatVector<Element = E> + SpecializedSpecialMath<E>,
307            {
308                <$marker as AiryFn>::eval::<P, E, V, true>(x)
309            }
310        }
311    )*};
312}
313
314impl_airy_fn! {
315    Ai => airy_ai / airy_ai_scaled;
316    AiPrime => airy_ai_prime / airy_ai_prime_scaled;
317    Bi => airy_bi / airy_bi_scaled;
318    BiPrime => airy_bi_prime / airy_bi_prime_scaled;
319}
320
321/// The order `$\nu$` for the runtime-order Bessel functions, tagged with the class of order
322/// it carries. See the [module documentation](self) for why the class is part of the value.
323///
324/// Variants are listed cheapest first. Every one stores its order **per lane**, so a packet
325/// may carry a different order in each lane. What it may not carry is a different _class_.
326///
327/// `V` is the float vector and `S` its signed-integer companion, in practice always
328/// `BesselOrder<V, V::Signed>`, which is what every entry point asks for and what inference
329/// produces from a plain `BesselOrder::Integer(k)`. They are separate parameters rather than
330/// one because the scalar surface unwraps each payload independently.
331#[derive(Debug, Clone, Copy, PartialEq, Eq)]
332pub enum BesselOrder<V, S> {
333    /// `$\nu = k$`, a whole number. Fitted minimax rationals at the low orders plus a
334    /// recurrence. The cheapest class, and the only one reaching a coefficient table.
335    Integer(S),
336
337    /// `$\nu = k/2$`. Half-integer orders are _elementary_: `$J_{1/2}(x) =
338    /// \sqrt{2/\pi x}\,\sin x$`, `$I_{1/2}(x) = \sqrt{2/\pi x}\,\sinh x$`,
339    /// `$K_{1/2}(x) = \sqrt{\pi/2x}\,e^{-x}$`, and the recurrence builds the rest with no
340    /// continued fraction, `$\Gamma$`, or series. This is also the spherical Bessel family,
341    /// via `$j_n(x) = \sqrt{\pi/2x}\,J_{n+1/2}(x)$`.
342    HalfInteger(S),
343
344    /// `$\nu = k/3$`. The Airy orders: `$\mathrm{Ai}$` and `$\mathrm{Bi}$` are Bessel
345    /// functions at `$\nu = \pm 1/3$` and their derivatives at `$\nu = \pm 2/3$`.
346    ///
347    /// **Costs the same as [`Real`](Self::Real)**, and the variant promises no shortcut.
348    /// No library has one, because there is none short of a dedicated minimax fit per order.
349    /// What it buys is _exactness_: a caller who writes
350    /// `Thirds(1)` gets the correctly-rounded `$1/3$` rather than whatever they typed.
351    Thirds(S),
352
353    /// Arbitrary real `$\nu$`. The general algorithm, and the expensive one.
354    Real(V),
355}
356
357/// Lets the generated scalar surface (`scalar_bessel_jv` and friends) carry an order: each
358/// payload unwraps on its own, which is the reason `V` and `S` are separate parameters.
359impl<V: Unwrap, S: Unwrap> Unwrap for BesselOrder<V, S> {
360    type Unwrapped = BesselOrder<V::Unwrapped, S::Unwrapped>;
361
362    #[inline(always)]
363    fn wrap(value: Self::Unwrapped) -> Self {
364        match value {
365            BesselOrder::Integer(k) => Self::Integer(Unwrap::wrap(k)),
366            BesselOrder::HalfInteger(k) => Self::HalfInteger(Unwrap::wrap(k)),
367            BesselOrder::Thirds(k) => Self::Thirds(Unwrap::wrap(k)),
368            BesselOrder::Real(v) => Self::Real(Unwrap::wrap(v)),
369        }
370    }
371
372    #[inline(always)]
373    fn unwrap(self) -> Self::Unwrapped {
374        match self {
375            Self::Integer(k) => BesselOrder::Integer(k.unwrap()),
376            Self::HalfInteger(k) => BesselOrder::HalfInteger(k.unwrap()),
377            Self::Thirds(k) => BesselOrder::Thirds(k.unwrap()),
378            Self::Real(v) => BesselOrder::Real(v.unwrap()),
379        }
380    }
381}
382
383impl<V: FloatVector> BesselOrder<V, V::Signed> {
384    /// The order as a float vector.
385    ///
386    /// Exact for [`Integer`](Self::Integer), [`HalfInteger`](Self::HalfInteger) and
387    /// [`Real`](Self::Real). **Lossy for [`Thirds`](Self::Thirds)**, necessarily, as thirds are
388    /// not binary-representable, which is why the variant stores a numerator in the first
389    /// place. Kernels that need an exact third must consume the numerator, not this.
390    #[inline(always)]
391    pub fn to_real(self) -> V {
392        match self {
393            Self::Integer(k) => V::from_signed_integer(k),
394            Self::HalfInteger(k) => V::from_signed_integer(k) * V::HALF,
395            Self::Thirds(k) => V::from_signed_integer(k) / thermite::const_splat!(int <V::Element>: 3),
396            Self::Real(v) => v,
397        }
398    }
399
400    /// The whole-number order, if [`simplify`](Self::simplify) reduces this to
401    /// [`Integer`](Self::Integer).
402    ///
403    /// `None` says the order genuinely is not whole, which is the question every entry point
404    /// asks last, after it has checked for the cheaper classes it can serve directly.
405    #[inline(always)]
406    pub fn as_integer(self) -> Option<V::Signed> {
407        match self.simplify() {
408            Self::Integer(k) => Some(k),
409            _ => None,
410        }
411    }
412
413    /// Narrow to the cheapest variant this data actually needs.
414    ///
415    /// Requires the condition to hold in every lane. Never widens, never changes the value
416    /// of `$\nu$`, and never turns [`Real`](Self::Real) into [`Thirds`](Self::Thirds). See
417    /// the [module documentation](self) for why that last one would be unsound.
418    #[inline(always)]
419    pub fn simplify(self) -> Self {
420        match self {
421            Self::Integer(_) => self,
422
423            // k/2 is a whole number exactly when k is even. One AND and a compare. The
424            // shift must be arithmetic, since `>>` is logical even on signed vectors.
425            Self::HalfInteger(k) => match (k & V::Signed::ONE).cmp_eq(V::Signed::ZERO).all() {
426                true => Self::Integer(k.srai::<1>()),
427                false => self,
428            },
429
430            // k/3 is a whole number exactly when 3 divides k. Done in the float domain to
431            // avoid an integer division: if 3 divides k then k/3 is exact and `round`
432            // recovers it, and if it does not then the recovered value fails the check.
433            Self::Thirds(k) => {
434                let three = thermite::const_splat!(int <V::Element>: 3);
435                let kf = V::from_signed_integer(k);
436                let m = (kf / three).round();
437
438                match (m * three).cmp_eq(kf).all() {
439                    true => Self::Integer(m.to_signed_integer()),
440                    false => self,
441                }
442            }
443
444            // Whole numbers and halves are both exactly representable, so neither of these
445            // changes which function gets evaluated. Thirds are not, and are not attempted.
446            Self::Real(v) => {
447                let r = v.round();
448
449                if r.cmp_eq(v).all() {
450                    return Self::Integer(r.to_signed_integer());
451                }
452
453                let two_v = v + v;
454                let h = two_v.round();
455
456                match h.cmp_eq(two_v).all() {
457                    true => Self::HalfInteger(h.to_signed_integer()),
458                    false => self,
459                }
460            }
461        }
462    }
463}
Last built: 2026-09-08 21:35:55 UTC