Skip to main content

thermite_special/specialized/generic/bessel/
ratio.rs

1//! `A_nu(x) = I_nu(x) / I_{nu-1}(x)`, the modified Bessel ratio, and its inverse.
2//!
3//! # Where it comes from
4//!
5//! With `p = 2 nu` this is the mean resultant length of a von Mises-Fisher distribution
6//! on the sphere `S^{p-1}` as a function of its concentration `kappa`, and its inverse is
7//! the maximum-likelihood concentration from an observed mean resultant length: the one
8//! step of every vMF fit that is not a matrix product. `p = 2` (`nu = 1`) is the von Mises
9//! circle, `I_1/I_0`. `p = 3` (`nu = 3/2`) collapses to the elementary Langevin function
10//! `coth x - 1/x`, which is the [`langevin`](crate::RealSpecialMath::langevin) family. The
11//! order arrives as a plain vector, so those cases reach the integer and half-integer
12//! Bessel kernels through the order simplifier, not the general real-order machinery.
13//!
14//! # Forward, in three arms
15//!
16//! The obvious quotient of the two scaled Bessel functions fails at small `x` when the
17//! order is large: `e^{-x} I_nu(x)` is `(x/2)^nu / Gamma(nu+1)` there and underflows to
18//! zero (`I_150(0.5)` is `1e-457`) while the ratio, about `x / 2nu`, is ordinary. And it
19//! keeps failing well past that: the scaled `I_nu(x)` is below `e^{-0.17 nu}` at `x = nu`.
20//!
21//! - `x <= 0.9 sqrt(nu)`: the ratio of the two power series, `A = (x / 2nu) S_nu / S_{nu-1}`
22//!   with `S_a = sum (x^2/4)^k / (k! (a+1)_k)`, twelve terms each (`1 / (4^12 12!)` at the
23//!   edge). Never underflows: only the ratio is formed.
24//! - `x < 8 nu`: the Perron continued fraction for `I_{nu}/I_{nu-1}`
25//!   (`cf1_i_ratio`, the Bessel kernels' own), which converges for every `(nu, x)` in about
26//!   `6 sqrt(x)` iterations.
27//! - else: the quotient of the scaled Bessels from [`bessel_iv`], where `x` dominates the
28//!   order and both are ordinary numbers, and the asymptotic arm inside is fast.
29//!
30//! # The derivative
31//!
32//! From the recurrence `I_{nu-1} - I_{nu+1} = (2 nu / x) I_nu`:
33//! `A' = 1 - A^2 - (2 nu - 1) A / x`, a closed form in `A` itself, which is what makes
34//! the inverse a one-evaluation Newton and gives `Dual` its factor for free.
35//!
36//! # Inverse
37//!
38//! Banerjee's `kappa_0 = r (p - r^2) / (1 - r^2)` (within ten percent everywhere), then
39//! `newtons_method` on `A(kappa) - r` with the derivative above, bracketed by a factor of two,
40//! eight iterations at most. Sra (2012) found two steps from that seed reach working
41//! precision, and the tolerance is a few ulp of `r`. Below `r = 1e-8` the answer is
42//! `2 nu r` outright (the next term is `r^3`).
43//!
44//! The inverse is ill-conditioned as `r -> 1`, where `kappa ~ (p-1)/(2(1-r))`: an ulp of
45//! `r` moves `kappa` by `2 kappa^2 eps / (p-1)`, so the _relative_ error grows like
46//! `kappa`, and the result is the exact inverse of the given `r` only to that extent. A
47//! complement form taking `1 - r`, the way `inv_langevin_1m` does for `p = 3`, is the
48//! remedy and is not built.
49
50use thermite::{
51    element::FloatElement,
52    math::{
53        PrimalProjection,
54        algorithms::newtons_method,
55        policy::{Policy, policies::MaxIterations},
56    },
57    prelude::*,
58};
59
60use super::ik_real::cf1_i_ratio;
61use crate::specialized::generic::ndtr::residual_tolerance;
62use crate::BesselOrder;
63use crate::specialized::SpecializedSpecialMath;
64
65/// `A' = 1 - A^2 - (2 nu - 1) A / x` from `A` itself.
66#[inline(always)]
67pub fn bessel_i_ratio_deriv<P, E, V>(a: V, x: V, nu: V) -> V
68where
69    P: Policy,
70    E: FloatElement,
71    V: FloatVector<Element = E>,
72{
73    let two_nu_m1 = nu.mul_sube(V::TWO, V::ONE);
74    a.nmul_adde(a, V::ONE) - two_nu_m1 * a / x
75}
76
77/// `I_nu(x) / I_{nu-1}(x)`, for `x >= 0` and `nu >= 1`. Odd in `x`.
78#[inline(always)]
79pub fn bessel_i_ratio_impl<P, E, V>(x: V, nu: V) -> V
80where
81    P: Policy,
82    E: FloatElement,
83    V: FloatVector<Element = E> + PrimalProjection<Primal = V> + SpecializedSpecialMath<E>,
84{
85    let neg = x.is_negative();
86    let x = x.abs();
87
88    let two_nu = nu + nu;
89    let small = x.cmp_le(nu.sqrt() * V::splat(<E as FloatElement>::ConstRatio::<9, 10>::VALUE));
90    let mid = !small & x.cmp_lt(nu * V::splat(<E as FloatElement>::ConstInt::<8>::VALUE));
91    let big = !(small | mid);
92
93    let mut a = V::ZERO;
94
95    if const { P::POLICY.avoid_branching } || small.any() {
96        // S_a = sum_k (x^2/4)^k / (k! (a+1)_k) for a = nu and a = nu - 1, twelve terms,
97        // as the ratio of the running terms so the two sums share the loop.
98        let q = x * x * V::FRAC_1_4;
99        let mut term_hi = V::ONE;
100        let mut term_lo = V::ONE;
101        let mut s_hi = V::ONE;
102        let mut s_lo = V::ONE;
103        let mut k = V::ONE;
104        let mut i = 0;
105        while i < 12 {
106            term_hi *= q / (k * (nu + k));
107            term_lo *= q / (k * (nu + k - V::ONE));
108            s_hi += term_hi;
109            s_lo += term_lo;
110            k += V::ONE;
111            i += 1;
112        }
113        a = x / two_nu * (s_hi / s_lo);
114    }
115
116    if const { P::POLICY.avoid_branching } || mid.any() {
117        // The continued fraction evaluates I_{a+1}/I_a, with a = nu - 1.
118        let cf = cf1_i_ratio::<P, E, V, V>(nu - V::ONE, x, mid);
119        a = mid.select(cf, a);
120    }
121
122    if const { P::POLICY.avoid_branching } || big.any() {
123        let hi = <V as SpecializedSpecialMath<E>>::bessel_iv::<P, true>(x, BesselOrder::Real(nu));
124        let lo = <V as SpecializedSpecialMath<E>>::bessel_iv::<P, true>(x, BesselOrder::Real(nu - V::ONE));
125        a = big.select(hi / lo, a);
126    }
127
128    // A(0) = 0 (the series gives 0/1 * 1 = 0 exactly), A(inf) = 1, A(NaN) = NaN.
129    let a = x.cmp_eq(V::INFINITY).select(V::ONE, a);
130    a.neg_c(neg)
131}
132
133/// `A'` from the complement `c = 1 - A`, with `1 - A^2` as `c (2 - c)`: no cancellation
134/// where `A` is within an ulp of 1.
135#[inline(always)]
136pub fn bessel_i_ratio_deriv_1m<P, E, V>(c: V, x: V, nu: V) -> V
137where
138    P: Policy,
139    E: FloatElement,
140    V: FloatVector<Element = E>,
141{
142    let two_nu_m1 = nu.mul_sube(V::TWO, V::ONE);
143    c * (V::TWO - c) - two_nu_m1 * (V::ONE - c) / x
144}
145
146/// `1 - A_nu(x)`, the complement of the ratio, accurate where `A` is within an ulp of 1.
147///
148/// For `x >= max(8 nu, 20)` the complement is evaluated directly. The order is reduced to
149/// `nu_0 = nu - K` in `[1, 2)`, where the Hankel expansions of `I_{nu_0}` and `I_{nu_0 - 1}`
150/// converge to `e^{-2x}` (their smallest term is at `k ~ 2x`, 40 terms at `x = 20`, and
151/// the difference of the two series is formed term by term, so `1 - N/D` is never
152/// evaluated), then the ratio recurrence `A_{m+1} = 1/A_m - 2m/x` written for the
153/// complement, `c_{m+1} = 2m/x - c_m/(1 - c_m)`, walks the `K` orders up. The two terms
154/// of that step are `(2m)/x` and about `(2m-1)/(2x)`, so the subtraction costs a bit and
155/// the walk is stable as long as `c` stays small, which `x >= 8 nu` guarantees at every
156/// intermediate order. Elsewhere `c = 1 - A` from the forward: below `x = 8 nu` the
157/// complement is at least `1/8` and `1 - A` is within `8 eps`. In the corner `8 nu <= x < 20`
158/// (only `nu < 2.5`) it is within `2x eps / (2 nu - 1)`, forty ulp at worst.
159///
160/// `A` is odd, so `c(-x) = 2 - c(x)`.
161#[inline(always)]
162pub fn bessel_i_ratio_1m_impl<P, E, V>(x: V, nu: V) -> V
163where
164    P: Policy,
165    E: FloatElement,
166    V: FloatVector<Element = E> + PrimalProjection<Primal = V> + SpecializedSpecialMath<E>,
167{
168    let neg = x.is_negative();
169    let x = x.abs();
170
171    let asym = x.cmp_ge(nu * V::splat(<E as FloatElement>::ConstInt::<8>::VALUE))
172        & x.cmp_ge(V::splat(<E as FloatElement>::ConstInt::<20>::VALUE));
173
174    let mut c = V::ONE;
175    if const { P::POLICY.avoid_branching } || !asym.all() {
176        c = V::ONE - bessel_i_ratio_impl::<P, E, V>(asym.select(V::ONE, x), nu);
177    }
178
179    if const { P::POLICY.avoid_branching } || asym.any() {
180        // Reduce the order into [1, 2).
181        let k = (nu - V::ONE).floor();
182        let nu0 = nu - k;
183
184        // Hankel series of I_{nu0} (hi) and I_{nu0 - 1} (lo) in 1/x: a_j = a_{j-1} (mu - (2j-1)^2)/(8j),
185        // alternating. `diff` accumulates D - N term by term, `d` accumulates D.
186        let mu_hi = nu0 * nu0 * V::splat(<E as FloatElement>::ConstInt::<4>::VALUE);
187        let nm1 = nu0 - V::ONE;
188        let mu_lo = nm1 * nm1 * V::splat(<E as FloatElement>::ConstInt::<4>::VALUE);
189        let neg_xinv = -(V::ONE / x);
190        let mut a_hi = V::ONE;
191        let mut a_lo = V::ONE;
192        let mut pw = V::ONE;
193        let mut d = V::ONE;
194        let mut diff = V::ZERO;
195        let mut j = 1u32;
196        while j <= 40 {
197            let odd = E::from_int((2 * j - 1) as i64 * (2 * j - 1) as i64);
198            let scale = V::splat(E::ONE / E::from_int(8 * j as i64));
199            a_hi *= (mu_hi - V::splat(odd)) * scale;
200            a_lo *= (mu_lo - V::splat(odd)) * scale;
201            pw *= neg_xinv;
202            d = a_lo.mul_adde(pw, d);
203            diff = (a_lo - a_hi).mul_adde(pw, diff);
204            j += 1;
205        }
206        let mut cm = diff / d;
207
208        // The walk up, c_{m+1} = 2m/x - c_m/(1 - c_m), amplifies an error by 1/(1 - c_m)^2 per
209        // step, about e^{nu^2/x} over the whole walk: fine while nu^2 <= x, 1e6 at nu = 150
210        // and x = 3000. Above that the _downward_ map c_m = y/(1 + y), y = 2m/x - c_{m+1},
211        // contracts by (1 - c_m)^2 per step instead, so those lanes descend from an order
212        // M = nu + J high enough that the start's error has decayed by e^{-(M^2 - nu^2)/x}:
213        // J = ceil(sqrt(nu^2 + 32x) - nu), seeded with Amos's bound at M, whose own error
214        // is a few parts in a thousand and is what the 32 buys down to 1e-16.
215        let two_over_x = V::TWO / x;
216        let up = asym & (nu * nu).cmp_le(x);
217        let down = asym & !up;
218
219        if const { P::POLICY.avoid_branching } || up.any() {
220            let mut m = nu0;
221            loop {
222                let live = up & m.cmp_lt(nu - V::HALF);
223                if live.none() {
224                    break;
225                }
226                let next = (m * two_over_x) - cm / (V::ONE - cm);
227                cm = live.select(next, cm);
228                m += V::ONE;
229            }
230        }
231
232        if const { P::POLICY.avoid_branching } || down.any() {
233            let j = (nu
234                .mul_adde(nu, x * V::splat(<E as FloatElement>::ConstInt::<32>::VALUE))
235                .sqrt()
236                - nu)
237                .ceil();
238            let mut m = nu + j;
239            // Amos: A_M(x) ~ x / (M - 1/2 + sqrt((M + 1/2)^2 + x^2)), so
240            // 1 - A_M = (M - 1/2 + (sqrt(...) - x)) / (M - 1/2 + sqrt(...)), with the
241            // difference of the root and x as (M + 1/2)^2 / (sqrt(...) + x).
242            let mh = m + V::HALF;
243            let root = mh.mul_adde(mh, x * x).sqrt();
244            let denom = m - V::HALF + root;
245            let mut cd = (m - V::HALF + mh * mh / (root + x)) / denom;
246            loop {
247                let live = down & m.cmp_gt(nu + V::HALF);
248                if live.none() {
249                    break;
250                }
251                m -= V::ONE;
252                let y = (m * two_over_x) - cd;
253                cd = live.select(y / (V::ONE + y), cd);
254            }
255            cm = down.select(cd, cm);
256        }
257
258        c = asym.select(cm, c);
259    }
260
261    // c(0) = 1, c(inf) = 0, and the reflection.
262    let c = x.cmp_eq(V::INFINITY).select(V::ZERO, c);
263    neg.select(V::TWO - c, c)
264}
265
266/// The `kappa` with `1 - A_nu(kappa) = t`, for `0 < t <= 2` (`t = 1 - r`), the complement
267/// form of [`inv_bessel_i_ratio_impl`]: well conditioned as `t -> 0`, where the plain form
268/// loses `2 kappa eps / (p - 1)` to the rounding of `r`.
269#[inline(always)]
270pub fn inv_bessel_i_ratio_1m_impl<P, E, V>(t: V, nu: V) -> V
271where
272    P: Policy,
273    E: FloatElement,
274    V: FloatVector<Element = E> + PrimalProjection<Primal = V> + SpecializedSpecialMath<E>,
275{
276    // t in (1, 2] is r < 0: kappa(r) is odd, and 2 - t is exact there.
277    let mirror = t.cmp_gt(V::ONE);
278    let t = mirror.select(V::TWO - t, t);
279
280    let p = nu + nu;
281    // Above t = 1/2 the plain form is well conditioned and 1 - t is exact (Sterbenz), and
282    // the complement's bracket would collapse at t = 1 where kappa_0 = 0.
283    let plain = t.cmp_ge(V::HALF);
284    let active = t.cmp_gt(V::ZERO) & !plain;
285
286    // Banerjee in the complement: r (p - r^2)/(1 - r^2) with r = 1 - t, 1 - r^2 = t (2 - t)
287    // and p - r^2 = (p - 1) + t (2 - t), nothing cancelling.
288    let u = t * (V::TWO - t);
289    let k0 = (V::ONE - t) * ((p - V::ONE) + u) / u;
290
291    let mut kappa = k0;
292    if const { P::POLICY.avoid_branching } || plain.any() {
293        let via_plain = inv_bessel_i_ratio_impl::<P, E, V>(V::ONE - t, nu);
294        kappa = plain.select(via_plain, kappa);
295    }
296    if const { P::POLICY.avoid_branching } || active.any() {
297        let tol = residual_tolerance::<P, E, V>(t);
298        let bounds = Some((k0 * V::HALF, k0 + k0));
299        let (root, _) = newtons_method::<V, MaxIterations<P, 8>, _>(k0, tol, active, bounds, |k| {
300            let c = bessel_i_ratio_1m_impl::<P, E, V>(k, nu);
301            (c - t, -bessel_i_ratio_deriv_1m::<P, E, V>(c, k, nu))
302        });
303        kappa = active.select(root, kappa);
304    }
305
306    let kappa = t.cmp_eq(V::ZERO).select(V::INFINITY, kappa);
307    let kappa = t.cmp_lt(V::ZERO).select(V::NAN, kappa);
308    kappa.neg_c(mirror)
309}
310
311/// The `kappa >= 0` with `I_nu(kappa) / I_{nu-1}(kappa) = r`, for `0 <= r < 1`. Odd in `r`.
312#[inline(always)]
313pub fn inv_bessel_i_ratio_impl<P, E, V>(r: V, nu: V) -> V
314where
315    P: Policy,
316    E: FloatElement,
317    V: FloatVector<Element = E> + PrimalProjection<Primal = V> + SpecializedSpecialMath<E>,
318{
319    let neg = r.is_negative();
320    let r = r.abs();
321
322    let p = nu + nu;
323    let tiny = r.cmp_lt(V::splat(<E as FloatElement>::ConstRatio::<1, 100_000_000>::VALUE));
324    let active = r.cmp_lt(V::ONE) & !tiny;
325
326    // Banerjee: r (p - r^2) / (1 - r^2). No transcendental in it.
327    let r2 = r * r;
328    let k0 = r * (p - r2) / (V::ONE - r2);
329
330    let mut kappa = p * r;
331
332    if const { P::POLICY.avoid_branching } || active.any() {
333        let tol = residual_tolerance::<P, E, V>(r);
334        let bounds = Some((k0 * V::HALF, k0 + k0));
335        let (root, _) = newtons_method::<V, MaxIterations<P, 8>, _>(k0, tol, active, bounds, |k| {
336            let a = bessel_i_ratio_impl::<P, E, V>(k, nu);
337            (a - r, bessel_i_ratio_deriv::<P, E, V>(a, k, nu))
338        });
339        kappa = active.select(root, kappa);
340    }
341
342    let kappa = r.cmp_eq(V::ONE).select(V::INFINITY, kappa);
343    let kappa = r.cmp_gt(V::ONE).select(V::NAN, kappa);
344    kappa.neg_c(neg)
345}
Last built: 2026-09-08 21:35:55 UTC