treams_core/lattice/reduced.rs
1//! Reduced integrals `F_n(v, t)` of the reciprocal terms.
2//!
3//! Upstream: `_redincgamma` and `_redintkambe` of `lattice/_esum.pyx`. The series in `t`,
4//! the continuation to other sheets and the rounding bounds are treams-rs extensions.
5
6use std::f64::consts::TAU;
7
8use super::{
9 accuracy::Rounding,
10 sheets::{Sheet, lower_side},
11};
12use crate::{
13 Complex,
14 numerics::Jet,
15 special::{self, Bessel, EvenKambe, ScaledGammaLadder, log_factorial},
16};
17
18/// `F_n(v) = Gamma(n, v) / (-v)^n` (`2n = twice_n`), the `w = 0` limit of
19/// [`reduced_kambe`].
20///
21/// # Branch
22///
23/// On the principal branches, or continued to the argument `sheet` of `v`, which callers
24/// pass for `Im k >= 0`: there the outgoing sums take the phase `v^n / (-v)^n =
25/// e^(-i pi n)` of the lower side of the real axis.
26///
27/// # Formula
28///
29/// A turn `delta` of the sheet adds `-delta v^-n / (-n)!` to integer `n <= 0`.
30/// Half-integer orders change sign above the real axis and lose `2 Gamma(n) / (-v)^n`
31/// after an odd number of turns (DLMF 8.2.10). That term solves the homogeneous degree
32/// recurrence, so the continued `F_n` keep their derivative identities and recurrences.
33pub(super) fn reduced_gamma(twice_n: i32, value: Complex, sheet: Option<Sheet>) -> Complex {
34 gamma_limit(
35 twice_n,
36 value,
37 sheet.map(|sheet| sheet.turns(lower_side(value))),
38 )
39}
40
41/// [`reduced_gamma`] with the whole `turns` from the principal argument of `v` (on the
42/// lower side of the real axis) to its sheet, found once for many orders.
43fn gamma_limit(twice_n: i32, value: Complex, turns: Option<f64>) -> Complex {
44 let z = lower_side(value);
45 let gamma = special::upper_gamma(0.5 * f64::from(twice_n), z) / reduced_power(twice_n, z);
46 on_sheet(twice_n, value, gamma, turns)
47}
48
49/// `(-z)^n` of [`reduced_gamma`], with the principal root for half-integer `n`.
50fn reduced_power(twice_n: i32, z: Complex) -> Complex {
51 let power = (-z).powi(twice_n.div_euclid(2));
52 if twice_n % 2 == 0 {
53 power
54 } else {
55 power * crate::numerics::complex_sqrt(-z)
56 }
57}
58
59/// `z^n / (-z)^n` on the principal branches, for `z` with a signed imaginary part:
60/// `(-1)^n`, or for half-integer `n` `(-1)^floor(n)` times `-i` on and below the real
61/// axis and `i` above it.
62fn principal_phase(twice_n: i32, z: Complex) -> Complex {
63 let sign = if twice_n.div_euclid(2) % 2 == 0 {
64 1.0
65 } else {
66 -1.0
67 };
68 if twice_n % 2 == 0 {
69 Complex::new(sign, 0.0)
70 } else if z.im > 0.0 {
71 Complex::new(0.0, sign)
72 } else {
73 Complex::new(0.0, -sign)
74 }
75}
76
77/// `F_n(v)` of [`reduced_gamma`] from its value `gamma` on the principal branches,
78/// continued by `turns` (see [`gamma_limit`]) to its sheet.
79fn on_sheet(twice_n: i32, value: Complex, mut gamma: Complex, turns: Option<f64>) -> Complex {
80 let Some(turns) = turns else {
81 return gamma;
82 };
83 let z = lower_side(value);
84 if twice_n % 2 != 0 {
85 // Whole turns: an odd number has an odd half (none, without a `trunc` call,
86 // on most sheets).
87 if turns != 0.0 && (0.5 * turns).fract() != 0.0 {
88 gamma -= 2.0 * libm::tgamma(0.5 * f64::from(twice_n)) / reduced_power(twice_n, z);
89 }
90 if z.im > 0.0 {
91 gamma = -gamma;
92 }
93 } else if twice_n <= 0 && turns != 0.0 {
94 let n = -twice_n / 2;
95 gamma -= Complex::new(0.0, TAU * turns) * value.powi(n) * (-log_factorial(n)).exp();
96 }
97 gamma
98}
99
100/// The argument `x = sqrt(-2 v) w` (`Re x >= 0`) and split `eta = -i / w` of the Kambe
101/// integrals with `F_n(v, w^2) = 2 I_(2n-1)(x, eta) w^2n`, for `Re w >= 0`.
102fn kambe_arguments(value: Complex, w: Complex) -> (Complex, Complex) {
103 let mut x = (-2.0 * lower_side(value) * w * w).sqrt();
104 // The outgoing limit approaches the real x-axis from the upper half plane.
105 if x.im == 0.0 {
106 x.im = 1e-100;
107 }
108 (x, -Complex::i() / w)
109}
110
111/// `F_n(v, w^2)` of integer `n` (the odd Kambe orders of 1D spherical sums) from Kambe
112/// integrals, with a first-order bound on its absolute rounding error in units of
113/// epsilon.
114///
115/// # Branch
116///
117/// The Kambe series takes the principal `ln a` of its argument `a` (`= v` up to
118/// rounding). Where the `sheet` of `v` ([`split_sheet`]) differs by `delta`, the result
119/// gains `-delta x^-n J_n(x) w^2n` with `x^2 = -2 v w^2` (DLMF 8.2.10 in each term). That
120/// term is an entire function of `v` and `w^2`, so the jets of the continued `F_n` stay
121/// exact.
122///
123/// [`split_sheet`]: super::sheets::split_sheet
124pub(super) fn reduced_kambe(
125 twice_n: i32,
126 value: Complex,
127 w: Complex,
128 sheet: Option<Sheet>,
129) -> (Complex, f64) {
130 let w = if w.re < 0.0 { -w } else { w };
131 let (x, eta) = kambe_arguments(value, w);
132 let (mut integral, mut bound) = special::kambe_with_bound(twice_n - 1, x, eta);
133 // The change delta of ln v from the principal branch of the series to the sheet.
134 let turns = sheet.map_or(0.0, |sheet| sheet.turns(special::kambe_argument(x, eta)));
135 if turns != 0.0 {
136 let delta = Complex::new(0.0, TAU * turns);
137 // The integral I_(2n-1) = F_n / (2 w^2n) changes by -delta/2 x^-n J_n(x).
138 let jump = -0.5 * delta * regular_bessel(twice_n / 2, x);
139 integral += jump;
140 bound += (8.0 + x.norm()) * jump.norm();
141 }
142 let power = w.powi(twice_n);
143 (2.0 * integral * power, 2.0 * bound * power.norm())
144}
145
146/// `x^-n J_n(x)`, entire in `x^2`, or NaN where AMOS fails.
147fn regular_bessel(n: i32, x: Complex) -> Complex {
148 let order = n.unsigned_abs();
149 let Ok(bessel) = special::bessel(f64::from(order), x, Bessel::J, false, 0) else {
150 return Complex::new(f64::NAN, f64::NAN);
151 };
152 if n >= 0 {
153 crate::numerics::ratio(bessel, x.powu(order))
154 } else {
155 // x^|n| J_-|n| = (-1)^n x^|n| J_|n|.
156 let sign = if order.is_multiple_of(2) { 1.0 } else { -1.0 };
157 sign * x.powu(order) * bessel
158 }
159}
160
161/// Largest `|t| = |w|^2` at which [`Reduced`] sums every integer order of `F_n(v, t)`
162/// from its series in `t`: the Kambe integrals take `eta = -i / w`, whose powers leave
163/// the range of `f64` from about `|w| = 1e-50`, and the series terms alternate at most
164/// like those of `e^-|t|`, losing a few ulps up to [`SERIES_REACH`].
165const SERIES_T: f64 = 1e-2;
166
167/// Largest `|t|` at which [`Reduced`] takes the series for the integer orders below the
168/// base pair of the Kambe recurrence (`2n < -2`), which need a series of gamma functions
169/// of their own each, while the series in `t` takes all orders from one ladder.
170const SERIES_REACH: f64 = 1.0;
171
172/// Largest `|t|` at which [`Reduced`] sums the half-integer orders from their series in
173/// `t` (which loses up to `e^|t|` ulps) instead of the Kambe chain (whose correlated
174/// errors the sums cancel less the smaller `|t|`).
175///
176/// Calibrated against 30-digit references: both keep 2D spherical and 1D cylindrical
177/// sums within 1e-13 of `max(|S|, 1)` for `|t|` from 0.3 to 1; the chain alone loses
178/// 2e-12 at `|t| = 1e-3` and the series alone 1e-12 at `|t| = 3`.
179pub(crate) const HALF_INTEGER_SERIES_T: f64 = 1.0;
180
181/// Terms of the series in `t` of [`Reduced`] at most; `|t| <=` [`SERIES_REACH`] stops
182/// within 20.
183const SERIES_TERMS: i32 = 64;
184
185/// Reduced integrals `F_n(v, t)` of one reciprocal point, as jets.
186///
187/// # Formula
188///
189/// `F_n(v, t) = (-1)^n int_1^inf s^(n-1) exp(-v s - t / (2s)) ds`, entire in `t`, with
190/// `dF_n/dv = F_(n+1)` and `dF_n/dt = F_(n-1) / 2`; `t = w^2` keeps them regular on the
191/// lattice plane and axis. Callers request orders in decreasing steps, so each value is
192/// evaluated once and then serves as a derivative of its neighbours.
193///
194/// # Paths
195///
196/// Near the plane or axis `F_n` is the series `sum_j (t/2)^j / j! F_(n-j)(v, 0)` of the
197/// limits [`reduced_gamma`] on their sheet, which tends to the limit as `t -> 0`:
198///
199/// - half-integer `n` (2D spherical and 1D cylindrical sums) up to
200/// [`HALF_INTEGER_SERIES_T`];
201/// - integer `n` (1D spherical sums) up to [`SERIES_T`], and their low orders up to
202/// [`SERIES_REACH`].
203///
204/// Farther off, the integer orders come from [`reduced_kambe`] with their rounding
205/// bounds, and the half-integer ones from one chain of Kambe integrals ([`EvenKambe`]).
206/// The errors of the chain are correlated across orders and cancel in the sums (taken
207/// order by order, each to a few ulps, they lose 1.4e-12 of `max(|S|, 1)`). At small
208/// splits the chain carries the rounding of its pair along the homogeneous solutions of
209/// the recurrence instead, and each diffraction order takes whichever of the chain and
210/// the single integrals bounds the sum more tightly ([`Self::prefers_single`]).
211pub(super) struct Reduced<const N: usize> {
212 v: Jet<N>,
213 t: Jet<N>,
214 w: Complex,
215 /// The sheet of `v` for [`reduced_kambe`] and the chain, and the turns to it for the
216 /// limits once they are needed.
217 sheet: Option<Sheet>,
218 turns: std::cell::OnceCell<Option<f64>>,
219 /// Whether `t` carries a derivative.
220 varies: bool,
221 /// The two most recent `(2n, F_n, error bound of F_n)`.
222 known: [Option<(i32, Complex, f64)>; 2],
223 /// The limits `F_d(v, 0)` at `2d = top, top - 2, ..., next + 2`, from the first
224 /// order requested down, with the ladder of `Gamma(d, v) / v^d` that continues them.
225 limits: Vec<Complex>,
226 ladder: Option<ScaledGammaLadder>,
227 top: i32,
228 next: i32,
229 /// The chain of the half-integer orders, with the factor that turns its Kambe
230 /// integrals into `F_n / w^2n`.
231 even_kambe: Option<(EvenKambe, f64)>,
232 /// The weights of `F_(1/2 - j)` in the value of the sum, recorded ([`Self::weigh`])
233 /// while the chain serves them.
234 weights: Vec<Complex>,
235 /// Whether the half-integer orders take the Kambe integrals of the chain one by one
236 /// ([`Self::prefers_single`]), and those it evaluated, `I_0, I_-2, ...`.
237 single: bool,
238 singles: Vec<Complex>,
239}
240
241impl<const N: usize> Reduced<N> {
242 pub(super) fn new(v: Jet<N>, t: Jet<N>, sheet: Option<Sheet>) -> Self {
243 Self {
244 v,
245 t,
246 w: t.value.sqrt(),
247 sheet,
248 turns: std::cell::OnceCell::new(),
249 varies: t.derivative.iter().any(|&d| d != Complex::default()),
250 known: [None; 2],
251 limits: Vec::new(),
252 ladder: None,
253 top: 0,
254 next: 0,
255 even_kambe: None,
256 weights: Vec::new(),
257 single: false,
258 singles: Vec::new(),
259 }
260 }
261
262 /// Records `weight`, the factor of the value of `F_n` in the sum (`2n = twice_n`),
263 /// for [`Self::prefers_single`] where the chain serves it.
264 pub(super) fn weigh(&mut self, twice_n: i32, weight: Complex) {
265 if self.single || self.t.value.norm() <= HALF_INTEGER_SERIES_T || twice_n % 2 == 0 {
266 return;
267 }
268 let Ok(j) = usize::try_from((1 - twice_n) / 2) else {
269 return;
270 };
271 if self.weights.len() <= j {
272 self.weights.resize(j + 1, Complex::default());
273 }
274 self.weights[j] += weight;
275 }
276
277 /// Whether the half-integer orders bound the sum they were weighed in
278 /// ([`Self::weigh`]) more tightly one by one ([`EvenKambe::single`]) than from the
279 /// chain ([`EvenKambe::sum_bounds`]), which they then take from here on. The single
280 /// integrals are evaluated only where the chain's bound exceeds the moduli of the
281 /// terms.
282 pub(super) fn prefers_single(&mut self) -> bool {
283 let Some((even_kambe, factor)) = &self.even_kambe else {
284 return false;
285 };
286 if self.single || self.weights.is_empty() {
287 return false;
288 }
289 let w = if self.w.re < 0.0 { -self.w } else { self.w };
290 let mut power = *factor * w;
291 let square = w * w;
292 let weights: Vec<Complex> = self
293 .weights
294 .iter()
295 .map(|&weight| {
296 let scaled = weight * power;
297 power /= square;
298 scaled
299 })
300 .collect();
301 let (recurrence, moduli) = even_kambe.sum_bounds(&weights);
302 // A bound that overflowed to NaN keeps the chain.
303 if recurrence.is_nan() || recurrence <= moduli {
304 return false;
305 }
306 let singles: Vec<(Complex, f64)> = (0..)
307 .zip(&weights)
308 .map(|(j, _)| even_kambe.single(-2 * j))
309 .collect();
310 let single: f64 = weights
311 .iter()
312 .zip(&singles)
313 .map(|(weight, (_, bound))| weight.l1_norm() * bound)
314 .sum();
315 // So does a bound of the single integrals that overflowed.
316 if single.is_nan() || single >= recurrence {
317 return false;
318 }
319 self.single = true;
320 self.singles = singles.into_iter().map(|(value, _)| value).collect();
321 self.known = [None; 2];
322 true
323 }
324
325 /// Half-integer `F_n = 2 I_(2n-1)(x, eta) w^2n` from the chain of the even Kambe
326 /// orders, whose series takes the root `sqrt(v) = x eta / sqrt 2` (`Re x >= 0`). Where
327 /// that is not the root `|v|^(1/2) e^(i sheet / 2)` of the sheet the chain takes the
328 /// split `-eta`: the even `I_(2n-1)` are odd in `eta` on a fixed root.
329 pub(super) fn by_even_recurrence(&mut self, twice_n: i32) -> Complex {
330 let w = if self.w.re < 0.0 { -self.w } else { self.w };
331 let (value, sheet) = (self.v.value, self.sheet);
332 let (even_kambe, factor) = self.even_kambe.get_or_insert_with(|| {
333 let (x, eta) = kambe_arguments(value, w);
334 if sheet.is_some_and(|sheet| sheet.opposes(x * eta, value)) {
335 (EvenKambe::new(x, -eta), -2.0)
336 } else {
337 (EvenKambe::new(x, eta), 2.0)
338 }
339 });
340 *factor * even_kambe.get(twice_n - 1) * w.powi(twice_n)
341 }
342
343 /// The turns from the principal argument of `v` to its sheet, for [`gamma_limit`].
344 fn turns(&self) -> Option<f64> {
345 *self.turns.get_or_init(|| {
346 self.sheet
347 .map(|sheet| sheet.turns(lower_side(self.v.value)))
348 })
349 }
350
351 fn value(&mut self, twice_n: i32) -> (Complex, f64) {
352 if let Some(&(_, value, bound)) = self.known.iter().flatten().find(|(n, ..)| *n == twice_n)
353 {
354 return (value, bound);
355 }
356 let (value, bound) = if self.t.value == Complex::default() {
357 // The limit itself: on the lattice plane or axis, where few orders take a
358 // gamma function each.
359 (gamma_limit(twice_n, self.v.value, self.turns()), 0.0)
360 } else if twice_n % 2 != 0 {
361 // Half-integer orders keep their relative accuracy and carry no bound.
362 if self.t.value.norm() <= HALF_INTEGER_SERIES_T {
363 (self.series(twice_n).0, 0.0)
364 } else if let (true, Some((even_kambe, factor))) = (self.single, &self.even_kambe) {
365 let w = if self.w.re < 0.0 { -self.w } else { self.w };
366 let integral = usize::try_from((1 - twice_n) / 2)
367 .ok()
368 .and_then(|j| self.singles.get(j).copied())
369 .unwrap_or_else(|| even_kambe.single(twice_n - 1).0);
370 (*factor * integral * w.powi(twice_n), 0.0)
371 } else {
372 (self.by_even_recurrence(twice_n), 0.0)
373 }
374 } else if self.t.value.norm() <= if twice_n < -2 { SERIES_REACH } else { SERIES_T } {
375 self.series(twice_n)
376 } else {
377 reduced_kambe(twice_n, self.v.value, self.w, self.sheet)
378 };
379 self.known = [Some((twice_n, value, bound)), self.known[0]];
380 (value, bound)
381 }
382
383 /// `F_n(v, t)` from its series in `t`, with the sum of the moduli of its terms as its
384 /// rounding bound in units of epsilon. The limits `F_(n-j)(v, 0)` stay within a small
385 /// factor of each other, so once the coefficients `(t/2)^j / j!` halve per term, the
386 /// first term below a quarter ulp of the moduli bounds the rest.
387 pub(super) fn series(&mut self, twice_n: i32) -> (Complex, f64) {
388 let half = 0.5 * self.t.value;
389 let (mut sum, mut magnitude) = (Complex::default(), 0.0);
390 let mut coefficient = Complex::new(1.0, 0.0);
391 for j in 0..SERIES_TERMS {
392 if j > 0 {
393 coefficient *= half / f64::from(j);
394 if coefficient == Complex::default() {
395 break;
396 }
397 }
398 let term = coefficient * self.limit(twice_n - 2 * j);
399 sum += term;
400 magnitude += term.l1_norm();
401 if j > 0
402 && f64::from(j) >= 2.0 * half.norm()
403 && term.l1_norm() <= 0.25 * f64::EPSILON * magnitude
404 {
405 break;
406 }
407 }
408 (sum, magnitude)
409 }
410
411 /// `F_d(v, 0)` of [`reduced_gamma`] for the series, once per order from the first one
412 /// requested down: `Gamma(d, v) / v^d` of a [`ScaledGammaLadder`] times `v^d / (-v)^d`.
413 fn limit(&mut self, twice_d: i32) -> Complex {
414 if self.limits.is_empty() {
415 (self.top, self.next) = (twice_d, twice_d);
416 }
417 let Ok(index) = usize::try_from((self.top - twice_d) / 2) else {
418 return gamma_limit(twice_d, self.v.value, self.turns());
419 };
420 let z = lower_side(self.v.value);
421 let (top, turns) = (self.top, self.turns());
422 let ladder = self
423 .ladder
424 .get_or_insert_with(|| ScaledGammaLadder::new(0.5 * f64::from(top), z, false));
425 while self.next >= twice_d {
426 let principal = ladder.next_lower() * principal_phase(self.next, z);
427 self.limits
428 .push(on_sheet(self.next, self.v.value, principal, turns));
429 self.next -= 2;
430 }
431 self.limits[index]
432 }
433
434 /// `F_n` as a jet, with the error bounds of its value and derivatives from those of
435 /// the orders [`reduced_kambe`] returns.
436 pub(super) fn get(&mut self, twice_n: i32) -> (Jet<N>, Rounding<N>) {
437 if N == 0 {
438 let (value, bound) = self.value(twice_n);
439 return (Jet::constant(value), Rounding::of(bound));
440 }
441 let (dz, dz_bound) = self.value(twice_n + 2);
442 let (value, bound) = self.value(twice_n);
443 let (z, t) = (self.v.derivative, self.t.derivative);
444 // Only odd orders carry bounds; the others skip the moduli.
445 let (derivative, derivative_bound) = if self.varies {
446 let (dt, dt_bound) = self.value(twice_n - 2);
447 let dt = 0.5 * dt;
448 (
449 std::array::from_fn(|i| dz * z[i] + dt * t[i]),
450 if dz_bound == 0.0 && dt_bound == 0.0 {
451 [0.0; N]
452 } else {
453 std::array::from_fn(|i| {
454 dz_bound * z[i].l1_norm() + 0.5 * dt_bound * t[i].l1_norm()
455 })
456 },
457 )
458 } else if dz_bound == 0.0 {
459 (z.map(|z| dz * z), [0.0; N])
460 } else {
461 (z.map(|z| dz * z), z.map(|z| dz_bound * z.l1_norm()))
462 };
463 (
464 Jet { value, derivative },
465 Rounding {
466 value: bound,
467 derivative: derivative_bound,
468 },
469 )
470 }
471
472 /// On-plane half-integer orders shared by the spherical harmonics of one
473 /// degree. Their scalar gamma limits do not depend on the harmonic or on the
474 /// order in which they are requested. The largest requested order is 3/2, the
475 /// derivative of F_(1/2). The plane has t = dt = 0, so only v varies.
476 pub(super) fn get_plane_cached(
477 &self,
478 twice_n: i32,
479 cached: &mut Vec<Option<Complex>>,
480 ) -> Jet<N> {
481 debug_assert!(self.t.is_zero() && twice_n % 2 != 0 && twice_n <= 1);
482 let mut gamma = |twice_n| {
483 let index = usize::try_from((3 - twice_n) / 2).unwrap_or_default();
484 if cached.len() <= index {
485 cached.resize(index + 1, None);
486 }
487 *cached[index].get_or_insert_with(|| gamma_limit(twice_n, self.v.value, self.turns()))
488 };
489 if N == 0 {
490 return Jet::constant(gamma(twice_n));
491 }
492 let dz = gamma(twice_n + 2);
493 Jet {
494 value: gamma(twice_n),
495 derivative: self.v.derivative.map(|d| dz * d),
496 }
497 }
498}