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}