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}