thermite_special/specialized/generic/bessel/spherical.rs
1//! The **spherical** Bessel functions `$j_n$`, `$y_n$`, `$i_n$`, `$k_n$`.
2//!
3//! ```math
4//! j_n(x) = \sqrt{\tfrac{\pi}{2x}}\,J_{n+1/2}(x), \qquad
5//! y_n(x) = \sqrt{\tfrac{\pi}{2x}}\,Y_{n+1/2}(x)
6//! ```
7//!
8//! and likewise for the modified pair. Both references ship these publicly: Boost as
9//! `sph_bessel` / `sph_neumann`, SciPy as `spherical_jn` /
10//! `spherical_yn` / `spherical_in` / `spherical_kn`. Boost has no modified spherical pair.
11//! SciPy has no oscillating primes as separate names. This module is the union.
12//!
13//! # The `sqrt` never gets formed, because it would only be cancelled
14//!
15//! Boost implements `sph_bessel` as literally `sqrt(pi/(2x)) * cyl_bessel_j(n + 1/2, x)`.
16//! That is two square roots that multiply to `$1/x$`: the cylindrical
17//! kernel builds its answer on `$\sqrt{2/\pi x}$` and the wrapper immediately multiplies by
18//! `$\sqrt{\pi/2x}$`.
19//!
20//! Here the recurrence is seeded in the **spherical** normalization directly:
21//!
22//! ```math
23//! j_{-1} = \frac{\cos x}{x},\quad j_0 = \frac{\sin x}{x}, \qquad
24//! y_{-1} = \frac{\sin x}{x},\quad y_0 = -\frac{\cos x}{x}
25//! ```
26//!
27//! The scaling factor between the two conventions does not depend on the order, so the
28//! recurrence is unchanged and [`walk_jy`](super::half::walk_jy) is reused verbatim
29//! (seeds in one normalization, values out in the same one). Two `sqrt`s, two divisions and a
30//! rounding disappear, and `$j_0$` becomes exactly [`sinc`](thermite::math::TranscendentalMath::sinc),
31//! which is correct **at `$x = 0$`** where the cylindrical route is `$0 \cdot \infty$`.
32//!
33//! Boost needs a small-`$z$` series below `$x = 1$` for that reason. This
34//! module needs none: the downward recurrence already covers small `$x$`, and is the arm
35//! that runs there anyway, since `$n < x$` is what selects the forward one.
36//!
37//! # Order
38//!
39//! `$n \ge 0$`, matching both references (Boost takes `unsigned`, SciPy documents `n >= 0`),
40//! and spelled `usize` so the constraint is the type rather than an assertion. That is the one
41//! place this family deliberately diverges from the cylindrical entry points, whose `i32`
42//! exists because negative orders there are meaningful. A caller who wants one here can use
43//! `$j_{-n-1}(x) = (-1)^{n+1} y_n(x)$`.
44//!
45//! # Negative `x`
46//!
47//! Unlike their cylindrical parents at half-integer order, `$j_n$`, `$y_n$` and `$i_n$` are
48//! elementary in `$\sin x$`, `$\cos x$`, `$\sinh x$`, `$\cosh x$` and powers of `$1/x$`, so
49//! they are real on the whole line and have definite parity:
50//!
51//! ```math
52//! j_n(-x) = (-1)^n j_n(x), \qquad y_n(-x) = (-1)^{n+1} y_n(x), \qquad i_n(-x) = (-1)^n i_n(x)
53//! ```
54//!
55//! which is SciPy's convention for `spherical_jn` / `spherical_yn` / `spherical_in`. The
56//! kernels evaluate on `$\lvert x\rvert$` and apply the sign at the end. They must, because the
57//! downward walk's trip count is `$a + 24 + c\,x$` and a negative `$x$` would shorten it to
58//! nothing. `$k_n$` has no parity (it is `$e^{-x}$` against `$e^{x}$`) and is NaN off the
59//! positive axis, as the cylindrical `$K$` and SciPy's `spherical_kn` are.
60
61use thermite::{
62 math::{TranscendentalMathWithPolicy, policy::Policy},
63 prelude::*,
64};
65
66use thermite::element::FloatElement;
67
68use super::half::{walk_ik, walk_jy};
69use super::ik::unscale_i_pair;
70
71/// `$(j_{n-1},\; j_n,\; y_{n-1},\; y_n)$`, on the whole real line.
72///
73/// The neighbour below comes back too, because every derivative identity in this family reaches
74/// down one order and the walk passes through it regardless:
75/// `$f_n' = f_{n-1} - \frac{n+1}{x} f_n$`.
76#[inline(always)]
77pub fn sph_jy_impl_n<P, E, V, const N: usize>(x: V) -> (V, V, V, V)
78where
79 E: FloatElement,
80 V: FloatVector<Element = E> + TranscendentalMathWithPolicy,
81 P: Policy,
82{
83 let a = V::splat(E::from_ratio(2 * N as i64 + 1, 2));
84
85 // Evaluated on `|x|` and signed at the end. See the module docs.
86 let ax = x.abs();
87
88 // Only the cosine is taken from here: the sine appears solely as `sin x / x`, which is
89 // `sinc`: identical away from the origin, and exactly 1 at it.
90 let cos_x = ax.cos_p::<P>();
91 let inv_x = V::ONE / ax;
92
93 // `sinc` rather than `sin_x * inv_x`: identical away from the origin, and exactly 1 at it,
94 // which is the value `j_0(0)` actually has.
95 let j_lo = cos_x * inv_x; // j_{-1}
96 let j_hi = ax.sinc_p::<P>(); // j_0
97 let y_lo = j_hi; // y_{-1} = j_0
98 let y_hi = -j_lo; // y_0 = -j_{-1}
99
100 // Order zero is the seeds, taken directly: the walk would route `n = 0` through its
101 // downward arm for `x < 1/2` and rebuild `j_0` as `j_{-1} r_{1/2}`, 1 ulp off at
102 // `x = 10^{-300}` where `j_0` should be `sinc` to the bit.
103 let (j_prev, j_n, y_prev, y_n) = match const { N == 0 } {
104 true => (j_lo, j_hi, y_lo, y_hi),
105 false => walk_jy::<P, E, V>(ax, a, j_lo, j_hi, y_lo, y_hi),
106 };
107
108 // `x = 0`: `j_0` is 1 and every higher order is 0, while every `y_n` is `-inf`. The
109 // recurrence cannot produce these (`1/x` is infinite and the downward normalization goes
110 // `inf * 0`), so the origin is a select. It is one compare for a value callers do ask for.
111 //
112 // The neighbour gets the same treatment one order down, which at `n = 0` means `j_{-1}`
113 // and `y_{-1}`: `cos(0)/0` is infinite and `sin(0)/0` is one.
114 let at_zero = x.is_zero();
115
116 let j_zero = if const { N == 0 } { V::ONE } else { V::ZERO };
117 let j_prev_zero = if const { N == 0 } {
118 V::INFINITY
119 } else if const { N == 1 } {
120 V::ONE
121 } else {
122 V::ZERO
123 };
124 let y_prev_zero = if const { N == 0 } { V::ONE } else { V::NEG_INFINITY };
125
126 let j_prev = at_zero.select(j_prev_zero, j_prev);
127 let j_n = at_zero.select(j_zero, j_n);
128 let y_prev = at_zero.select(y_prev_zero, y_prev);
129 let y_n = at_zero.select(V::NEG_INFINITY, y_n);
130
131 // All four vanish at infinity, where the seeds are `NaN * 0`.
132 let inf = ax.cmp_eq(V::INFINITY);
133 let (j_prev, j_n) = (inf.select(V::ZERO, j_prev), inf.select(V::ZERO, j_n));
134 let (y_prev, y_n) = (inf.select(V::ZERO, y_prev), inf.select(V::ZERO, y_n));
135
136 // The parity fold. Orders `n` and `n - 1` have opposite parity, and `y` has the opposite
137 // of `j` at each.
138 let neg = x.is_negative();
139 match const { N % 2 == 1 } {
140 true => (j_prev, j_n.neg_c(neg), y_prev.neg_c(neg), y_n),
141 false => (j_prev.neg_c(neg), j_n, y_prev, y_n.neg_c(neg)),
142 }
143}
144
145/// `$(i_{n-1},\; i_n,\; k_{n-1},\; k_n)$`, scaled by `$(e^{-|x|}, e^{x})$` when `SCALED`.
146/// `$i_n$` is folded by parity onto the whole line. `$k_n$` is NaN for `$x < 0$`.
147///
148/// The modified spherical pair, `$i_n(x) = \sqrt{\pi/2x}\,I_{n+1/2}(x)$` and likewise for
149/// `$k$`. SciPy ships both as `spherical_in` / `spherical_kn`. Boost ships neither.
150///
151/// Seeds are `$i_{-1} = \cosh x / x$`, `$i_0 = \sinh x / x$` and
152/// `$k_{-1} = k_0 = \tfrac{\pi}{2}e^{-x}/x$`, the last two equal because `$K$` is even in
153/// order. As with the oscillating pair, the work is done in the spherical normalization so no
154/// square root is formed only to be cancelled.
155///
156/// `far_threshold` is where the unscaled `$i$` halves its exponential. See
157/// [`unscale_i_pair`].
158#[inline(always)]
159pub fn sph_ik_impl_n<P, E, V, const N: usize, const SCALED: bool>(x: V, far_threshold: E) -> (V, V, V, V)
160where
161 E: FloatElement,
162 V: FloatVector<Element = E> + TranscendentalMathWithPolicy,
163 P: Policy,
164{
165 let a = V::splat(E::from_ratio(2 * N as i64 + 1, 2));
166
167 // Evaluated on `|x|` and signed at the end. See the module docs.
168 let ax = x.abs();
169 let inv_x = V::ONE / ax;
170
171 // Scaled seeds, from one `exp_m1`. `e^{-x} sinh x = -expm1(-2x)/2` and
172 // `e^{-x} cosh x = (1 + e^{-2x})/2`, neither of which cancels at small `x`. The
173 // algebraically equal `(1 - e^{-2x})/2` would.
174 let e2m1 = (-(ax + ax)).exp_m1_p::<P>();
175 let half_inv_x = inv_x * V::HALF;
176
177 let i_lo = (V::TWO + e2m1) * half_inv_x; // e^{-x} i_{-1}
178 let i_hi = -e2m1 * half_inv_x; // e^{-x} i_0
179 let k_seed = V::FRAC_PI_2 * inv_x; // e^{ x} k_0 = e^{x} k_{-1}
180
181 // Order zero is the seeds: cheaper and exacter, for the reason given in `sph_jy_impl_n`.
182 let (i_prev, i_n, k_prev, k_n) = match const { N == 0 } {
183 true => (i_lo, i_hi, k_seed, k_seed),
184 // The large-`x` asymptotic arm inside the walk produces a CYLINDRICAL value and is
185 // handed the factor that brings it back to this normalization.
186 false => walk_ik::<P, E, V>(ax, a, i_lo, i_hi, k_seed, (V::FRAC_PI_2 * inv_x).sqrt()),
187 };
188
189 // The origin, as for the oscillating pair: `i_0(0) = 1`, higher orders zero, every `k_n`
190 // infinite. `k` reaches that on its own through `1/x`, but `i`'s seeds are `0/0` there.
191 let at_zero = x.is_zero();
192
193 let i_zero = if const { N == 0 } { V::ONE } else { V::ZERO };
194 let i_prev_zero = if const { N == 0 } {
195 V::INFINITY
196 } else if const { N == 1 } {
197 V::ONE
198 } else {
199 V::ZERO
200 };
201
202 let (i_prev, i_n) = (at_zero.select(i_prev_zero, i_prev), at_zero.select(i_zero, i_n));
203
204 // The parity fold for `i`, as for `j`. `k` has none and is undefined there.
205 let neg = x.is_negative();
206 let (i_prev, i_n) = match const { N % 2 == 1 } {
207 true => (i_prev, i_n.neg_c(neg)),
208 false => (i_prev.neg_c(neg), i_n),
209 };
210 let bad = x.cmp_lt(V::ZERO);
211 let (k_prev, k_n) = (bad.select(V::NAN, k_prev), bad.select(V::NAN, k_n));
212
213 match const { SCALED } {
214 true => (i_prev, i_n, k_prev, k_n),
215 false => {
216 let (i_prev, i_n) = unscale_i_pair::<P, E, V>(i_prev, i_n, ax, far_threshold);
217 let em = (-ax).exp_p::<P>();
218 (i_prev, i_n, k_prev * em, k_n * em)
219 }
220 }
221}
222
223/// `$f_n'(x)$` from the pair the walk returns: `$f_n' = \pm f_{n-1} - \frac{n+1}{x} f_n$`.
224///
225/// `MINUS` selects the `$k$` case, whose neighbour enters negated, the same asymmetry the
226/// cylindrical `$K$` has, and for the same reason: `$K$` is the decaying solution, so its
227/// derivative is negative where the others' are not.
228///
229/// # Where this comes from
230///
231/// Not a separate identity: the cylindrical one plus the derivative of the
232/// normalization. With `$f_n = \sqrt{\pi/2x}\,F_{n+1/2}$` and
233/// `$F_\nu' = F_{\nu-1} - \frac{\nu}{x}F_\nu$`, the extra `$-\frac{1}{2x}$` from
234/// differentiating `$\sqrt{\pi/2x}$` turns `$\frac{n+1/2}{x}$` into `$\frac{n+1}{x}$`. That is
235/// the whole difference. It is why the coefficient is `$n+1$` rather than the `$n$` a
236/// half-remembered version of this formula would use.
237#[inline(always)]
238pub fn sph_deriv_n<E, V, const N: usize, const MINUS: bool>(x: V, prev: V, cur: V) -> V
239where
240 E: FloatElement,
241 V: FloatVector<Element = E>,
242{
243 let coeff = V::splat(E::from_int(N as i64 + 1)) / x;
244 let p = match const { MINUS } {
245 true => -prev,
246 false => prev,
247 };
248 let d = coeff.nmul_adde(cur, p);
249
250 // The origin, where `(n+1)/x` is infinite and the identity reads `inf * 0` or
251 // `inf - inf`. `j_1'(0) = i_1'(0) = 1/3` (from `j_1 ~ x/3`) and every other finite
252 // member has a zero derivative there. The singular members' derivatives are infinite with
253 // the opposite sign to the value, since `y_n -> -inf` rises and `k_n -> +inf` falls.
254 let finite_limit = if const { N == 1 } {
255 V::splat(E::from_ratio(1, 3))
256 } else {
257 V::ZERO
258 };
259 let limit = cur.is_finite().select(finite_limit, -cur);
260 x.is_zero().select(limit, d)
261}
262
263// ---- runtime-order twins ------------------------------------------------------------------
264//
265// The same three kernels with the order as a value. Every `const { N .. }` above is a plain
266// branch or select on `n` here and the walk is handed the same `a`, so the two forms agree to
267// the bit. That equality is what `tests/bessel_sph.rs` checks. The const forms stay because
268// their origin selects and parity fold cost nothing at a literal order.
269
270/// The runtime-order twin of [`sph_jy_impl_n`].
271#[inline(always)]
272pub fn sph_jy_impl<P, E, V>(x: V, n: u32) -> (V, V, V, V)
273where
274 E: FloatElement,
275 V: FloatVector<Element = E> + TranscendentalMathWithPolicy,
276 P: Policy,
277{
278 let a = V::splat(E::from_ratio(2 * n as i64 + 1, 2));
279
280 let ax = x.abs();
281 let cos_x = ax.cos_p::<P>();
282 let inv_x = V::ONE / ax;
283
284 let j_lo = cos_x * inv_x;
285 let j_hi = ax.sinc_p::<P>();
286 let y_lo = j_hi;
287 let y_hi = -j_lo;
288
289 let (j_prev, j_n, y_prev, y_n) = match n == 0 {
290 true => (j_lo, j_hi, y_lo, y_hi),
291 false => walk_jy::<P, E, V>(ax, a, j_lo, j_hi, y_lo, y_hi),
292 };
293
294 let at_zero = x.is_zero();
295
296 let j_zero = if n == 0 { V::ONE } else { V::ZERO };
297 let j_prev_zero = match n {
298 0 => V::INFINITY,
299 1 => V::ONE,
300 _ => V::ZERO,
301 };
302 let y_prev_zero = if n == 0 { V::ONE } else { V::NEG_INFINITY };
303
304 let j_prev = at_zero.select(j_prev_zero, j_prev);
305 let j_n = at_zero.select(j_zero, j_n);
306 let y_prev = at_zero.select(y_prev_zero, y_prev);
307 let y_n = at_zero.select(V::NEG_INFINITY, y_n);
308
309 let inf = ax.cmp_eq(V::INFINITY);
310 let (j_prev, j_n) = (inf.select(V::ZERO, j_prev), inf.select(V::ZERO, j_n));
311 let (y_prev, y_n) = (inf.select(V::ZERO, y_prev), inf.select(V::ZERO, y_n));
312
313 let neg = x.is_negative();
314 match n % 2 == 1 {
315 true => (j_prev, j_n.neg_c(neg), y_prev.neg_c(neg), y_n),
316 false => (j_prev.neg_c(neg), j_n, y_prev, y_n.neg_c(neg)),
317 }
318}
319
320/// The runtime-order twin of [`sph_ik_impl_n`].
321#[inline(always)]
322pub fn sph_ik_impl<P, E, V, const SCALED: bool>(x: V, n: u32, far_threshold: E) -> (V, V, V, V)
323where
324 E: FloatElement,
325 V: FloatVector<Element = E> + TranscendentalMathWithPolicy,
326 P: Policy,
327{
328 let a = V::splat(E::from_ratio(2 * n as i64 + 1, 2));
329
330 let ax = x.abs();
331 let inv_x = V::ONE / ax;
332
333 let e2m1 = (-(ax + ax)).exp_m1_p::<P>();
334 let half_inv_x = inv_x * V::HALF;
335
336 let i_lo = (V::TWO + e2m1) * half_inv_x;
337 let i_hi = -e2m1 * half_inv_x;
338 let k_seed = V::FRAC_PI_2 * inv_x;
339
340 let (i_prev, i_n, k_prev, k_n) = match n == 0 {
341 true => (i_lo, i_hi, k_seed, k_seed),
342 false => walk_ik::<P, E, V>(ax, a, i_lo, i_hi, k_seed, (V::FRAC_PI_2 * inv_x).sqrt()),
343 };
344
345 let at_zero = x.is_zero();
346
347 let i_zero = if n == 0 { V::ONE } else { V::ZERO };
348 let i_prev_zero = match n {
349 0 => V::INFINITY,
350 1 => V::ONE,
351 _ => V::ZERO,
352 };
353
354 let (i_prev, i_n) = (at_zero.select(i_prev_zero, i_prev), at_zero.select(i_zero, i_n));
355
356 let neg = x.is_negative();
357 let (i_prev, i_n) = match n % 2 == 1 {
358 true => (i_prev, i_n.neg_c(neg)),
359 false => (i_prev.neg_c(neg), i_n),
360 };
361 let bad = x.cmp_lt(V::ZERO);
362 let (k_prev, k_n) = (bad.select(V::NAN, k_prev), bad.select(V::NAN, k_n));
363
364 match const { SCALED } {
365 true => (i_prev, i_n, k_prev, k_n),
366 false => {
367 let (i_prev, i_n) = unscale_i_pair::<P, E, V>(i_prev, i_n, ax, far_threshold);
368 let em = (-ax).exp_p::<P>();
369 (i_prev, i_n, k_prev * em, k_n * em)
370 }
371 }
372}
373
374/// The runtime-order twin of [`sph_deriv_n`].
375#[inline(always)]
376pub fn sph_deriv<E, V, const MINUS: bool>(x: V, n: u32, prev: V, cur: V) -> V
377where
378 E: FloatElement,
379 V: FloatVector<Element = E>,
380{
381 let coeff = V::splat(E::from_int(n as i64 + 1)) / x;
382 let p = match const { MINUS } {
383 true => -prev,
384 false => prev,
385 };
386 let d = coeff.nmul_adde(cur, p);
387
388 let finite_limit = if n == 1 { V::splat(E::from_ratio(1, 3)) } else { V::ZERO };
389 let limit = cur.is_finite().select(finite_limit, -cur);
390 x.is_zero().select(limit, d)
391}