treams_core/special/integrals/
kambe.rs

1//! Kambe integrals `I_n(z, eta) = integral_eta^infinity t^n exp(-z^2 t^2/2 + 1/(2t^2)) dt`
2//! with first-order bounds on their rounding errors.
3//!
4//! [`kambe()`] runs the Kambe (1968) recurrence up from a base pair: closed forms for even
5//! orders, the series for odd ones. Below the pair it takes the series over scaled
6//! incomplete gamma functions or the downward recurrence, whichever bounds the error more
7//! tightly. [`EvenKambe`] carries the even orders of one reciprocal point of a lattice
8//! sum. [`SmallSplitKambe`] gives the integrals of the real-space terms of a lattice sum
9//! at splits below every automatic one.
10//!
11//! Upstream: `intkambe` of `treams/special/_integrals.pyx`. The rounding bounds, the
12//! choice below the base pair and the small-split series are treams-rs extensions.
13
14use std::f64::consts::{FRAC_1_SQRT_2, PI};
15
16use errorfunctions::ComplexErrorFunctions;
17
18use super::{
19    double::DoubleComplex,
20    gamma::{RECURRENCE_GROWTH, ScaledGammaLadder},
21};
22use crate::{Complex, numerics::finite};
23
24/// The argument `a = (z eta)^2 / 2` of the incomplete gamma functions of the Kambe
25/// series, as the series evaluates it. Odd orders take the principal branch of `ln a`,
26/// which the 1D spherical lattice sums continue to another sheet where they need it.
27pub(crate) fn kambe_argument(z: Complex, eta: Complex) -> Complex {
28    0.5 * (z * z) * eta * eta
29}
30
31/// Largest `|c|` that sets the length of the Kambe series. Its terms `c^j / j!` peak
32/// at about `e^|c| / sqrt(2 pi |c|)`, which overflows beyond `|c| = 715`: no larger
33/// `|c|` gives a finite sum.
34const SERIES_PEAK_LIMIT: f64 = 1000.0;
35
36/// Kambe integrals by expanding `exp(1/(2t^2))`: with `a = (z eta)^2 / 2`,
37/// `c = 1 / (2 eta^2)` and `R(d) = Gamma(d, a) / a^d` (`-1/d` as `a -> 0`),
38/// `I_n = eta^(n+1)/2 sum_j c^j/j! R((n+1)/2 - j)`, whose terms cancel by up to
39/// `e^(|c| - Re c)` for complex `eta`. Orders of one parity share one ladder of `R`, each
40/// with the magnitude of its terms as its error bound. Even orders take the branch
41/// `sqrt(a) = z eta / sqrt 2` with `Re z >= 0`.
42fn kambe_series<const K: usize>(orders: [i32; K], z: Complex, eta: Complex) -> [(Complex, f64); K] {
43    kambe_series_with(orders, z, eta, None)
44}
45
46/// The number of terms of the Kambe series at `c = 1 / (2 eta^2)` after which each order
47/// stops: its terms peak near `j = |c|` and soon fall below the rounding of the sum.
48fn series_terms(ratio: Complex) -> f64 {
49    200.0 + 3.0 * ratio.norm().min(SERIES_PEAK_LIMIT)
50}
51
52/// [`kambe_series`] with the coefficients `c^j/j!` from `table`, or recurred term by term.
53fn kambe_series_with<const K: usize>(
54    orders: [i32; K],
55    z: Complex,
56    eta: Complex,
57    table: Option<&[Complex]>,
58) -> [(Complex, f64); K] {
59    struct Partial {
60        start: i32,
61        coefficient: Complex,
62        sum: Complex,
63        magnitude: f64,
64        converged: bool,
65    }
66    let top = orders.into_iter().max().unwrap_or_default();
67    let even = top % 2 == 0;
68    let z = if even && z.re < 0.0 { -z } else { z };
69    let square = z * z;
70    let argument = kambe_argument(z, eta);
71    let reflected =
72        even && (crate::numerics::complex_sqrt(square * eta * eta) * (z * eta).conj()).re < 0.0;
73    let ratio = 0.5 / (eta * eta);
74    let peak = ratio.norm();
75    let last = series_terms(ratio);
76    let mut ladder = ScaledGammaLadder::new(0.5 * f64::from(top + 1), argument, reflected);
77    // Order n takes its first term (top - n) / 2 ladder steps down.
78    let mut partials = orders.map(|n| Partial {
79        start: (top - n) / 2,
80        coefficient: Complex::new(1.0, 0.0),
81        sum: Complex::default(),
82        magnitude: 0.0,
83        converged: false,
84    });
85    let mut step = 0;
86    loop {
87        let value = ladder.next_lower();
88        for partial in partials
89            .iter_mut()
90            .filter(|partial| partial.start <= step && !partial.converged)
91        {
92            let index = step - partial.start;
93            let j = f64::from(index);
94            if let Some(table) = table {
95                partial.coefficient = usize::try_from(index)
96                    .ok()
97                    .and_then(|index| table.get(index))
98                    .copied()
99                    .unwrap_or_default();
100            } else if j > 0.0 {
101                partial.coefficient *= ratio / j;
102            }
103            let term = partial.coefficient * value;
104            let modulus = term.l1_norm();
105            partial.sum += term;
106            partial.magnitude += modulus;
107            // With the shared coefficients of the lattice sums the test compares `|Re| +
108            // |Im|`, at most `sqrt 2` from the modulus, which takes no square roots.
109            let small = if table.is_some() {
110                modulus <= 1.4e-16 * partial.sum.l1_norm()
111            } else {
112                term.norm() <= 2e-16 * partial.sum.norm()
113            };
114            partial.converged = (j > 3.0 && j > peak && small) || j >= last || !finite(partial.sum);
115        }
116        if partials.iter().all(|partial| partial.converged) {
117            break;
118        }
119        step += 1;
120    }
121    std::array::from_fn(|i| {
122        let prefactor = 0.5 * eta.powi(orders[i] + 1);
123        (
124            prefactor * partials[i].sum,
125            prefactor.norm() * partials[i].magnitude,
126        )
127    })
128}
129
130/// Orders below the base pair from the series, which cancels by about `e^(|c| - Re c)`
131/// for complex `eta`, or the downward recurrence from the base pair (as in treams),
132/// accurate while `|1/eta^2|` exceeds about `|n|` and `|z|`: the smaller error bound
133/// wins. A series whose predicted loss stays within [`RECURRENCE_GROWTH`] is kept
134/// outright; otherwise odd orders sum their base pair in the same pass, and even orders
135/// keep a recurrence that grows by at most [`RECURRENCE_GROWTH`] and try the series only
136/// where its predicted loss is below [`RECURRENCE_GROWTH`] times the recurrence's growth.
137fn kambe_below_base(n: i32, z: Complex, eta: Complex) -> (Complex, f64) {
138    let ratio = 0.5 / (eta * eta);
139    let odd = n % 2 != 0;
140    let cancellation = ratio.norm() - ratio.re;
141    if cancellation > RECURRENCE_GROWTH.ln() {
142        if odd {
143            let [series, lower, upper] = kambe_series([n, -3, -1], z, eta);
144            return more_accurate(series, kambe_downward(n, z, eta, [lower, upper]));
145        }
146        let recurred = kambe_downward(n, z, eta, even_pair(z, eta));
147        let growth = recurred.1 / recurred.0.norm();
148        if growth <= RECURRENCE_GROWTH || cancellation.exp() >= RECURRENCE_GROWTH * growth {
149            return recurred;
150        }
151        let [series] = kambe_series([n], z, eta);
152        return more_accurate(series, recurred);
153    }
154    let [series] = kambe_series([n], z, eta);
155    if series.1 <= RECURRENCE_GROWTH * series.0.norm() {
156        return series;
157    }
158    more_accurate(series, kambe_downward(n, z, eta, base_pair(odd, z, eta)))
159}
160
161/// The value with the smaller error bound, unless the series overflowed.
162fn more_accurate(series: (Complex, f64), recurred: (Complex, f64)) -> (Complex, f64) {
163    if recurred.1 < series.1 || !finite(series.0) {
164        recurred
165    } else {
166        series
167    }
168}
169
170/// The Kambe (1968) recurrence `I_n = (n+3) I_(n+2) - z^2 I_(n+4) + eta^(n+3) e^(...)`
171/// run downward from the base pair, with the first-order absolute error bound in units
172/// of epsilon propagated from the pair and each endpoint term.
173fn kambe_downward(n: i32, z: Complex, eta: Complex, pair: [(Complex, f64); 2]) -> (Complex, f64) {
174    let [(mut lower, mut lower_error), (mut upper, mut upper_error)] = pair;
175    // At z = 0 the order above the base diverges, but its coefficient z^2 vanishes.
176    if z == Complex::default() {
177        (upper, upper_error) = (Complex::default(), 0.0);
178    }
179    let square = z * z;
180    let exponential = (0.5 * (1.0 / (eta * eta) - square * eta * eta)).exp();
181    let mut order = if n % 2 == 0 { -4 } else { -5 };
182    while order >= n {
183        let endpoint = eta.powi(order + 3) * exponential;
184        let value = f64::from(order + 3) * lower - square * upper + endpoint;
185        let error = f64::from(order + 3).abs() * lower_error
186            + square.norm() * upper_error
187            + endpoint.l1_norm();
188        (upper, upper_error) = (lower, lower_error);
189        (lower, lower_error) = (value, error);
190        order -= 2;
191    }
192    (lower, lower_error)
193}
194
195/// The base pair `I_-3, I_-1` (odd, by the series) or `I_-2, I_0` (even, in closed form),
196/// with error bounds in units of epsilon.
197fn base_pair(odd: bool, z: Complex, eta: Complex) -> [(Complex, f64); 2] {
198    if odd {
199        kambe_series([-3, -1], z, eta)
200    } else {
201        even_pair(z, eta)
202    }
203}
204
205/// `I_-2` and `I_0` in closed form by two complementary error functions, with the
206/// magnitude of their two terms as error bounds in units of epsilon.
207fn even_pair(z: Complex, eta: Complex) -> [(Complex, f64); 2] {
208    let (terms, z) = even_terms(z, eta);
209    let [lower, upper] = even_values(terms, z);
210    let magnitude = even_scale() * (terms[0].norm() + terms[1].norm());
211    [(lower, magnitude), (upper, magnitude / z.norm())]
212}
213
214/// The factor `sqrt(pi / 8)` of the closed form of the even base pair.
215fn even_scale() -> f64 {
216    0.5 * PI.sqrt() * FRAC_1_SQRT_2
217}
218
219/// The two terms of the closed form of the even base pair, and the argument with
220/// `Re z >= 0` they take (even orders are even in `z`).
221fn even_terms(z: Complex, eta: Complex) -> ([Complex; 2], Complex) {
222    let z = if z.re < 0.0 { -z } else { z };
223    let plus = ((z * eta - Complex::i() / eta) * FRAC_1_SQRT_2).erfc() * (-Complex::i() * z).exp();
224    let minus = ((z * eta + Complex::i() / eta) * FRAC_1_SQRT_2).erfc() * (Complex::i() * z).exp();
225    ([plus, minus], z)
226}
227
228/// `I_-2` and `I_0` from the terms of [`even_terms`].
229fn even_values([plus, minus]: [Complex; 2], z: Complex) -> [Complex; 2] {
230    let scale = even_scale();
231    [
232        -Complex::i() * scale * (plus - minus),
233        scale * (plus + minus) / z,
234    ]
235}
236
237/// Even Kambe orders `I_2, I_0, I_-2, I_-4, ...` of one argument from the closed-form pair
238/// `I_-2, I_0` and the Kambe recurrence, one step per order, as treams evaluates them:
239/// the orders of one reciprocal point of a 2D spherical or 1D cylindrical lattice sum,
240/// whose correlated errors those sums cancel (see `lattice::Reduced`).
241#[derive(Clone, Debug)]
242pub(crate) struct EvenKambe {
243    /// The argument `z` with `Re z >= 0` that the pair takes.
244    argument: Complex,
245    square: Complex,
246    eta: Complex,
247    exponential: Complex,
248    /// The moduli of the two terms of the closed-form pair ([`even_terms`]).
249    terms: [f64; 2],
250    /// `I_0, I_-2, ...` as far as requested.
251    values: Vec<Complex>,
252}
253
254impl EvenKambe {
255    /// The chain of `I_n(z, eta)`, starting from the pair.
256    pub(crate) fn new(z: Complex, eta: Complex) -> Self {
257        let (terms, argument) = even_terms(z, eta);
258        let [lower, upper] = even_values(terms, argument);
259        let square = z * z;
260        Self {
261            argument,
262            square,
263            eta,
264            exponential: (0.5 * (1.0 / (eta * eta) - square * eta * eta)).exp(),
265            terms: terms.map(Complex::norm),
266            values: vec![upper, lower],
267        }
268    }
269
270    /// `I_n` for even `n <= 0` on its own, with a first-order bound on its absolute
271    /// rounding error in units of epsilon ([`kambe_with_bound`]): below the base pair from
272    /// the series where that bounds it more tightly than the recurrence.
273    pub(crate) fn single(&self, n: i32) -> (Complex, f64) {
274        kambe_with_bound(n, self.argument, self.eta)
275    }
276
277    /// A first-order bound, in units of epsilon, on the absolute rounding error of
278    /// `sum_j weights[j] I_(-2j)` from this chain (which holds the orders down to the last
279    /// weight), and the sum of the moduli of its terms: one ulp of each term of the pair
280    /// and of each product and endpoint term of the recurrence, carried to the sum by the
281    /// adjoint of the recurrence, which sees the correlation of the errors.
282    pub(crate) fn sum_bounds(&self, weights: &[Complex]) -> (f64, f64) {
283        let values = &self.values;
284        let moduli = weights
285            .iter()
286            .zip(values)
287            .map(|(weight, value)| weight.l1_norm() * value.l1_norm())
288            .sum();
289        let (mut next, mut after) = (Complex::default(), Complex::default());
290        let mut bound = 0.0;
291        for (j, &weight) in weights.iter().enumerate().rev() {
292            // The derivative of the sum in I_(-2j) through every order below it: I_(-2j-2)
293            // takes (1 - 2j) I_(-2j) from j >= 1, I_(-2j-4) takes -z^2 I_(-2j).
294            let order = i32::try_from(j).unwrap_or(i32::MAX / 2);
295            let step = if j >= 1 {
296                f64::from(1 - 2 * order)
297            } else {
298                0.0
299            };
300            let adjoint = weight + step * next - self.square * after;
301            if let (Some(&lower), Some(&upper)) =
302                (values.get(j.wrapping_sub(1)), values.get(j.wrapping_sub(2)))
303                && j >= 2
304            {
305                let endpoint = self.eta.powi(3 - 2 * order) * self.exponential;
306                let rounding = (f64::from(3 - 2 * order) * lower).l1_norm()
307                    + (self.square * upper).l1_norm()
308                    + endpoint.l1_norm();
309                bound += adjoint.l1_norm() * rounding;
310            }
311            (after, next) = (next, adjoint);
312        }
313        // I_0 = s (p + m) / z and I_-2 = -i s (p - m) from the terms p, m of the pair.
314        let (zero, lower) = (next, after);
315        let scale = even_scale();
316        let sum = zero / self.argument;
317        let difference = Complex::i() * lower;
318        let pair = scale
319            * (self.terms[0] * (sum - difference).l1_norm()
320                + self.terms[1] * (sum + difference).l1_norm());
321        (bound + pair, moduli)
322    }
323
324    /// `I_n` for even `n <= 2`.
325    pub(crate) fn get(&mut self, n: i32) -> Complex {
326        debug_assert!(n <= 2 && n % 2 == 0, "even Kambe order {n} above 2");
327        if n > 0 {
328            // One upward step: I_2 = (I_0 - I_-2 + eta e^(...)) / z^2.
329            return (self.values[0] - self.values[1] + self.eta * self.exponential) / self.square;
330        }
331        let index = usize::try_from(-n / 2).unwrap_or_default();
332        let mut order = -2 * i32::try_from(self.values.len()).unwrap_or(i32::MAX / 2);
333        while order >= n {
334            let length = self.values.len();
335            let (upper, lower) = (self.values[length - 2], self.values[length - 1]);
336            self.values.push(
337                f64::from(order + 3) * lower - self.square * upper
338                    + self.eta.powi(order + 3) * self.exponential,
339            );
340            order -= 2;
341        }
342        self.values[index]
343    }
344}
345
346/// The Kambe integral `I_n(z, eta)` without input checks.
347///
348/// The Kambe (1968) recurrence, as in treams, runs upward from the base pair
349/// `I_-3, I_-1` (odd) or `I_-2, I_0` (even); orders below it come from the series or the
350/// downward recurrence, whichever amplifies rounding less. Orders above -2 diverge at
351/// `z = 0`, all at `eta = 0`.
352pub(crate) fn kambe(n: i32, z: Complex, eta: Complex) -> Complex {
353    let zero = Complex::default();
354    if eta == zero || (z == zero && n > -2) {
355        return Complex::new(f64::INFINITY, 0.0);
356    }
357    let odd = n % 2 != 0;
358    let base = if odd { -3 } else { -2 };
359    // Below the base pair the bounds choose between the series and the recurrence.
360    if n < base || (odd && n <= -1) {
361        return kambe_with_bound(n, z, eta).0;
362    }
363    // Above it the values of `kambe_with_bound`, without the moduli of their bounds.
364    let pair = if odd {
365        kambe_series([-3, -1], z, eta).map(|(value, _)| (value, 0.0))
366    } else {
367        let (terms, argument) = even_terms(z, eta);
368        let values = even_values(terms, argument);
369        // I_-2 alone skips the division of I_0.
370        if n == base {
371            return values[0];
372        }
373        values.map(|value| (value, 0.0))
374    };
375    let [(lower, _), (upper, _)] = pair;
376    if n <= base + 2 {
377        return if n == base { lower } else { upper };
378    }
379    kambe_upward::<false>(n, z, eta, pair, 1.0).0
380}
381
382/// [`kambe`] with a first-order bound on the absolute rounding error of its value in
383/// units of epsilon: from the series for `I_-3` and `I_-1`, below the base pair from
384/// the series or the downward recurrence that `kambe` takes, and above the base pair
385/// propagated through the upward recurrence. Requires nonzero `eta`, and nonzero `z`
386/// from order -1 up.
387pub(crate) fn kambe_with_bound(n: i32, z: Complex, eta: Complex) -> (Complex, f64) {
388    let odd = n % 2 != 0;
389    let base = if odd { -3 } else { -2 };
390    if n < base {
391        return kambe_below_base(n, z, eta);
392    }
393    if odd && n <= -1 {
394        let [series] = kambe_series([n], z, eta);
395        return series;
396    }
397    let [lower, upper] = base_pair(odd, z, eta);
398    if n <= base + 2 {
399        return if n == base { lower } else { upper };
400    }
401    kambe_upward::<true>(n, z, eta, [lower, upper], 1.0)
402}
403
404/// Kambe integrals `I_n(z, eta)` of the real-space terms of an Ewald sum at a split below
405/// every automatic one ([`Self::get`]), with the coefficients `c^j / j!` of their series
406/// at `c = 1 / (2 eta^2)`, which every integral at that split shares. There the
407/// closed-form pair and its recurrence ([`kambe_with_bound`]) cancel by up to `e^Re(c)`
408/// (each error function loses about `|a|^2` ulps to `exp(-a^2)` at its argument
409/// `a = (z eta -+ i / eta) / sqrt 2`), while the series sums terms of one phase for real
410/// `z` and `eta`; its coefficients are rounded from double-double arithmetic, since
411/// recurred in double they carry the rounding of `c` into every integral alike.
412#[derive(Clone, Debug)]
413pub(crate) struct SmallSplitKambe {
414    eta: Complex,
415    /// `|c| - Re c`, the cancellation of the series.
416    cancellation: f64,
417    coefficients: Vec<Complex>,
418}
419
420impl SmallSplitKambe {
421    /// The coefficients of the series at the split `eta`, as many as it may take.
422    #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] // At most 3201.
423    pub(crate) fn new(eta: Complex) -> Self {
424        let ratio = 0.5 / (eta * eta);
425        let terms = series_terms(ratio) as usize + 1;
426        let c = DoubleComplex::half_inverse_square(eta);
427        let mut coefficient = DoubleComplex::from(Complex::new(1.0, 0.0));
428        let coefficients = (0..terms)
429            .map(|j| {
430                if j > 0 {
431                    #[allow(clippy::cast_precision_loss)] // At most 3201.
432                    let j = j as f64;
433                    coefficient = (coefficient * c).divide(j);
434                }
435                coefficient.rounded()
436            })
437            .collect();
438        Self {
439            eta,
440            cancellation: ratio.norm() - ratio.re,
441            coefficients,
442        }
443    }
444
445    /// `I_n(z, eta)` of `orders` (from -1 up), each with a first-order bound on its
446    /// absolute rounding error in units of epsilon that counts the conditioning of the
447    /// elementary functions it takes: from the closed forms where the bound of the first
448    /// order stays within [`CLOSED_ULPS`] of its value or within `floor` (an error the
449    /// caller can afford), or where the series would cancel as much; otherwise from the
450    /// series where it is finite and bounds the first order at least as tightly.
451    pub(crate) fn get<const K: usize>(
452        &self,
453        orders: [i32; K],
454        z: Complex,
455        floor: f64,
456    ) -> [(Complex, f64); K] {
457        let eta = self.eta;
458        // The ladder of the series takes `exp(-a)` at `a = (z eta)^2 / 2`, which loses
459        // about `|a|` ulps, as the gamma function that seeds it.
460        let ulps = SERIES_ULPS + kambe_argument(z, eta).norm();
461        let closed = orders.map(|n| self.closed(n, z, ulps));
462        let modulus = closed[0].0.norm();
463        // The series cancels by about `e^(|c| - Re c)` relative to its terms.
464        if finite(closed[0].0)
465            && (closed[0].1 <= floor.max(CLOSED_ULPS * modulus)
466                || self.cancellation.exp() * ulps * modulus >= closed[0].1)
467        {
468            return closed;
469        }
470        let series = kambe_series_with(orders, z, eta, Some(&self.coefficients))
471            .map(|(value, bound)| (value, ulps * bound));
472        if finite(series[0].0) && (series[0].1 <= closed[0].1 || !finite(closed[0].0)) {
473            series
474        } else {
475            closed
476        }
477    }
478
479    /// [`kambe_with_bound`] from the closed-form pair (the series pair for odd orders) and
480    /// the upward recurrence, with the bounds of the pair and of the endpoint terms scaled
481    /// by the conditioning of the functions they take, and `ulps` those of the series.
482    fn closed(&self, n: i32, z: Complex, ulps: f64) -> (Complex, f64) {
483        let eta = self.eta;
484        let odd = n % 2 != 0;
485        let pair = if odd {
486            kambe_series_with([-3, -1], z, eta, Some(&self.coefficients))
487                .map(|(value, bound)| (value, ulps * bound))
488        } else {
489            let (terms, argument) = even_terms(z, eta);
490            let [lower, upper] = even_values(terms, argument);
491            // The error function at `(z eta -+ i / eta) / sqrt 2` of each term.
492            let ulps =
493                |sign: f64| 1.0 + 0.5 * (argument * eta + sign * Complex::i() / eta).norm_sqr();
494            let magnitude = even_scale()
495                * terms[0]
496                    .norm()
497                    .mul_add(ulps(-1.0), terms[1].norm() * ulps(1.0));
498            [(lower, magnitude), (upper, magnitude / argument.norm())]
499        };
500        let base = if odd { -3 } else { -2 };
501        debug_assert!(n >= base, "Kambe order {n} below the base pair");
502        if n <= base + 2 {
503            return pair[usize::from(n != base)];
504        }
505        let exponent = 0.5 * (1.0 / (eta * eta) - z * z * eta * eta);
506        kambe_upward::<true>(n, z, eta, pair, 1.0 + exponent.norm())
507    }
508}
509
510/// Bound of the closed forms of [`SmallSplitKambe`], relative to their value in ulps,
511/// up to which they are taken without the series.
512const CLOSED_ULPS: f64 = 64.0;
513
514/// Relative rounding error, in ulps, of the terms of the Kambe series of
515/// [`SmallSplitKambe`] beyond that of the exponential of their gamma functions: up to
516/// 22 against 40-digit quadrature (orders up to 28).
517const SERIES_ULPS: f64 = 32.0;
518
519/// The Kambe recurrence run upward from the base pair to order `n`, with error bounds in
520/// units of epsilon where `BOUND` asks for them (the endpoint terms count
521/// `endpoint_ulps`).
522fn kambe_upward<const BOUND: bool>(
523    n: i32,
524    z: Complex,
525    eta: Complex,
526    pair: [(Complex, f64); 2],
527    endpoint_ulps: f64,
528) -> (Complex, f64) {
529    let [(mut lower, mut lower_error), (mut upper, mut upper_error)] = pair;
530    let exponential = (0.5 * (1.0 / (eta * eta) - z * z * eta * eta)).exp();
531    let square = if BOUND { (z * z).norm() } else { 0.0 };
532    let mut order = if n % 2 == 0 { 2 } else { 1 };
533    while order <= n {
534        let endpoint = eta.powi(order - 1) * exponential;
535        let value = (f64::from(order - 1) * upper - lower + endpoint) / (z * z);
536        let error = if BOUND {
537            (f64::from(order - 1).abs() * upper_error
538                + lower_error
539                + endpoint_ulps * endpoint.l1_norm())
540                / square
541                + value.l1_norm()
542        } else {
543            0.0
544        };
545        (lower, lower_error) = (upper, upper_error);
546        (upper, upper_error) = (value, error);
547        order += 2;
548    }
549    (upper, upper_error)
550}
551
552#[cfg(test)]
553mod tests {
554    //! Kambe integrals against quadrature references and their recurrence; the lattice
555    //! sums built on them are checked in `properties/lattice/`.
556
557    use super::{
558        SmallSplitKambe, base_pair, even_pair, kambe, kambe_downward, kambe_series,
559        kambe_with_bound,
560    };
561    use crate::{
562        Complex,
563        numerics::finite,
564        special::integrals::{intkambe, intkambe_array, reference},
565        test_support::{ALGEBRA_CASES, central, complex, prop_assert_close},
566    };
567    use proptest::{prelude::*, test_runner::TestCaseError};
568
569    /// The accuracy of `intkambe` against 40-digit quadrature of its defining
570    /// integral (`references/kambe.txt`), for orders below, at and above the base pair.
571    #[test]
572    fn intkambe_matches_quadrature() {
573        let cases = reference(include_str!(concat!(
574            env!("CARGO_MANIFEST_DIR"),
575            "/references/kambe.txt"
576        )));
577        assert_eq!(cases.len(), 181);
578        for (key, expected) in cases {
579            #[allow(clippy::cast_possible_truncation)] // Integral orders.
580            let n = key[0] as i32;
581            let (z, eta) = (Complex::new(key[1], key[2]), Complex::new(key[3], 0.0));
582            let actual = intkambe(n, z, eta).unwrap();
583            assert!(
584                (actual - expected).norm() <= 1e-13 * expected.norm(),
585                "I_{n}({z}, {eta}) = {actual}, expected {expected}"
586            );
587        }
588    }
589
590    /// Rounding error of a sum or recurrence in units of epsilon times its first-order
591    /// bound, which counts each input and step once. Calibrated: with up to about 200
592    /// series terms the factor stays at or below 80 on the 19584 values that
593    /// `references/kambe_lattice.txt` is drawn from.
594    const SUM_ROUNDING: f64 = 256.0;
595
596    /// The accuracy of `intkambe` at the arguments of the lattice sums, x = sqrt(-2 v w^2)
597    /// and eta = -i/w with |w| in [0.5, 6] (`references/kambe_lattice.txt`), where the
598    /// series cancels by up to e^(w^2): each value is within 1e-13 plus [`SUM_ROUNDING`]
599    /// ulps of the smaller of the series and recurrence bounds and of the bound
600    /// `kambe_with_bound` reports, and even orders, which the closed-form pair and a
601    /// stable recurrence carry, within 1e-11. Odd orders lose what their base pair loses
602    /// to the series, up to all digits of tiny values at w = 6.
603    #[test]
604    fn intkambe_at_lattice_arguments() {
605        let cases = reference(include_str!(concat!(
606            env!("CARGO_MANIFEST_DIR"),
607            "/references/kambe_lattice.txt"
608        )));
609        assert_eq!(cases.len(), 1249);
610        for (key, expected) in cases {
611            #[allow(clippy::cast_possible_truncation)] // Integral orders.
612            let n = key[0] as i32;
613            let (z, eta) = (Complex::new(key[1], key[2]), Complex::new(key[3], key[4]));
614            let actual = intkambe(n, z, eta).unwrap();
615            let odd = n % 2 != 0;
616            let [series] = kambe_series([n], z, eta);
617            let bound = if n < -3 {
618                series
619                    .1
620                    .min(kambe_downward(n, z, eta, base_pair(odd, z, eta)).1)
621            } else if odd {
622                series.1
623            } else {
624                even_pair(z, eta)[usize::from(n == 0)].1
625            };
626            let error = (actual - expected).norm();
627            assert!(
628                error <= 1e-13 * expected.norm() + SUM_ROUNDING * f64::EPSILON * bound,
629                "I_{n}({z}, {eta}) = {actual}, expected {expected}, bound {bound}"
630            );
631            assert!(
632                n % 2 != 0 || error <= 1e-11 * expected.norm(),
633                "I_{n}({z}, {eta}) = {actual}, expected {expected}"
634            );
635            // The bound the 1D spherical lattice sums accumulate covers the same error.
636            let (value, reported) = kambe_with_bound(n, z, eta);
637            assert_eq!(value, actual, "I_{n}({z}, {eta})");
638            assert!(
639                error <= 1e-13 * expected.norm() + SUM_ROUNDING * f64::EPSILON * reported,
640                "I_{n}({z}, {eta}) = {actual}, expected {expected}, bound {reported}"
641            );
642        }
643    }
644
645    /// Small `z`, where `Gamma(d, (z eta)^2 / 2)` overflows at the orders of the Kambe
646    /// series while its scaled form stays finite: within 1e-13 of 80-digit references;
647    /// and at `z = 0`, where the scaled terms are exactly `1 / (j - d)`, the closed form
648    /// `I_-3(0, eta) = e^(1/(2 eta^2)) - 1`.
649    #[test]
650    fn kambe_series_at_small_arguments() {
651        for (key, expected) in reference(SMALL_ARGUMENTS) {
652            #[allow(clippy::cast_possible_truncation)] // Integral orders.
653            let n = key[0] as i32;
654            let (z, eta) = (Complex::new(key[1], key[2]), Complex::new(key[3], key[4]));
655            let actual = intkambe(n, z, eta).unwrap();
656            assert!(
657                (actual - expected).norm() <= 1e-13 * expected.norm(),
658                "I_{n}({z}, {eta}) = {actual}, expected {expected}"
659            );
660        }
661        let eta = Complex::new(0.9, 0.2);
662        let expected = (0.5 / (eta * eta)).exp() - 1.0;
663        let actual = intkambe(-3, Complex::default(), eta).unwrap();
664        assert!((actual - expected).norm() <= 1e-15 * expected.norm());
665    }
666
667    /// `n re(z) im(z) re(eta) im(eta): re im` of [`kambe_series_at_small_arguments`].
668    const SMALL_ARGUMENTS: &str = "
669        -11 1e-8 0.0 1.0 0.0: 0.1520560731298582 0.0
670        -4 1e-12 1e-12 0.0 -1.4285714285714286: 6.468012052193277e-25 -0.09890802845085557
671        -60 1e-6 0.0 0.8 0.0: 18842.496102322668 0.0";
672
673    /// Far evanescent orders of the 1D lattice sums, `I_n(i X, i E)` with `E = -1/w`,
674    /// where the gamma ladder of the series reaches `|d|` near `a = (X E)^2 / 2`, 84 to
675    /// 288 here, and its seed leaves the normal range of `f64`: each value is finite and
676    /// within its reported bound of a 40-digit quadrature, which grows like `e^(w^2)` with
677    /// the cancellation of the series: four digits at `w = 5`, none at 8 and 12.
678    #[test]
679    fn kambe_series_at_far_evanescent_orders() {
680        for (key, expected) in reference(FAR_EVANESCENT_ORDERS) {
681            #[allow(clippy::cast_possible_truncation)] // Integral orders.
682            let n = key[0] as i32;
683            let (x, eta) = (Complex::new(key[1], key[2]), Complex::new(key[3], key[4]));
684            let actual = intkambe(n, x, eta).unwrap();
685            let (value, bound) = kambe_with_bound(n, x, eta);
686            assert_eq!(value, actual, "I_{n}({x}, {eta})");
687            let error = (actual - expected).norm();
688            let tolerance = 1e-13 * expected.norm() + SUM_ROUNDING * f64::EPSILON * bound;
689            assert!(
690                finite(actual) && error <= tolerance,
691                "I_{n}({x}, {eta}) = {actual}, expected {expected}, bound {bound}"
692            );
693            if eta.im == -0.2 {
694                assert!(error <= 1e-4 * expected.norm(), "I_{n}({x}, {eta})");
695            } else {
696                // The bound admits no digits.
697                assert!(
698                    f64::EPSILON * bound >= expected.norm(),
699                    "I_{n}({x}, {eta}): bound {bound}"
700                );
701            }
702        }
703    }
704
705    /// `n re(z) im(z) re(eta) im(eta): re im` of
706    /// [`kambe_series_at_far_evanescent_orders`].
707    const FAR_EVANESCENT_ORDERS: &str = "
708        1 0.0 65.0 0.0 -0.2: -2.0660582937881563e-46 0.0
709        -3 0.0 65.0 0.0 -0.2: -1.2571235926483948e-43 0.0
710        -1 0.0 72.75 0.0 -0.2: 2.1076570255627118e-54 0.0
711        -5 0.0 72.75 0.0 -0.2: 1.2903462855893798e-51 0.0
712        -3 0.0 74.0 0.0 -0.2: -1.2811250838191011e-54 0.0
713        -9 0.0 74.0 0.0 -0.2: 1.9436884363619354e-50 0.0
714        -5 0.0 100.0 0.0 -0.2: 8.455104242709188e-93 0.0
715        1 0.0 100.0 0.0 -0.2: -5.497277311462082e-97 0.0
716        -9 0.0 104.0 0.0 -0.125: 3.6443751390988474e-46 0.0
717        -1 0.0 104.0 0.0 -0.125: 2.3262288907012024e-53 0.0
718        1 0.0 116.4 0.0 -0.125: -1.4178141658789873e-64 0.0
719        -3 0.0 116.4 0.0 -0.125: -5.65917325634384e-61 0.0
720        -1 0.0 118.4 0.0 -0.125: 2.1799896234970922e-64 0.0
721        -5 0.0 118.4 0.0 -0.125: 8.713921867089539e-61 0.0
722        -3 0.0 160.0 0.0 -0.125: -3.292074055899649e-102 0.0
723        -9 0.0 160.0 0.0 -0.125: 8.481758744830575e-97 0.0
724        -5 0.0 156.0 0.0 -0.08333333333333333: 5.401514253857145e-66 0.0
725        1 0.0 156.0 0.0 -0.08333333333333333: -2.0476729683120954e-72 0.0
726        -9 0.0 174.60000000000002 0.0 -0.08333333333333333: 2.9610339101407296e-71 0.0
727        -1 0.0 174.60000000000002 0.0 -0.08333333333333333: 7.549716659238215e-80 0.0
728        1 0.0 177.60000000000002 0.0 -0.08333333333333333: -1.254520496883189e-83 0.0
729        -3 0.0 177.60000000000002 0.0 -0.08333333333333333: -2.4885065995274332e-79 0.0
730        -1 0.0 240.0 0.0 -0.08333333333333333: 2.8622298120865214e-121 0.0
731        -5 0.0 240.0 0.0 -0.08333333333333333: 5.8465689416040285e-117 0.0";
732
733    /// The Kambe integrals of the real-space terms at splits below every automatic one,
734    /// where the closed forms and the recurrence lose up to 1.6e-3 (real), 3e6 ulps
735    /// (rotated split) and 1e-8 (lossy `z`): within 32 ulps of 50-digit quadrature and
736    /// within their reported bounds.
737    #[test]
738    fn small_split_integrals_keep_their_digits() {
739        for (key, expected) in reference(SMALL_SPLITS) {
740            #[allow(clippy::cast_possible_truncation)] // Integral orders.
741            let n = key[0] as i32;
742            let (z, eta) = (Complex::new(key[1], key[2]), Complex::new(key[3], key[4]));
743            let [(value, bound)] = SmallSplitKambe::new(eta).get([n], z, 0.0);
744            let error = (value - expected).norm();
745            assert!(
746                error <= 32.0 * f64::EPSILON * expected.norm() && error <= f64::EPSILON * bound,
747                "I_{n}({z}, {eta}) = {value}, expected {expected}, bound {bound}"
748            );
749        }
750    }
751
752    /// `n re(z) im(z) re(eta) im(eta): re im` of [`small_split_integrals_keep_their_digits`].
753    const SMALL_SPLITS: &str = "
754        10 0.51 0.0 0.135 0.0: 1979290.8095660476 0.0
755        14 1.19 0.0 0.135 0.0: 13164.26182468963 0.0
756        0 0.21 0.0 0.137 0.0: 1013302395.488461 0.0
757        2 0.17 0.0 0.135 0.0: 40717456.33548243 0.0
758        13 0.65 0.2 0.17 0.0: -5355697.287078753 8848712.085330464
759        8 1.59 0.13 0.166 -0.0513: 1.8142292945819555 -1.5315951220636088";
760
761    /// Small `eta` makes `|c| = 1 / (2 |eta|^2)` large, and the series needs about `|c|`
762    /// terms. Up to `|c| = 715` they stay representable: `I_-3(0, eta) = e^c - 1` holds
763    /// at `|c| = 707`. Beyond, the terms and the integrals (about `e^|c|`) overflow, and
764    /// each value must come back non-finite within a bounded number of terms (a watchdog).
765    #[test]
766    fn kambe_series_at_small_eta() {
767        let eta = Complex::new(0.0266, 0.0);
768        let expected = (0.5 / (eta * eta)).exp() - 1.0;
769        let actual = intkambe(-3, Complex::default(), eta).unwrap();
770        assert!((actual - expected).norm() <= 1e-14 * expected.norm());
771        let cases = [
772            (-1, 1.0, 1e-5),
773            (-4, 1.0, 1e-8),
774            (-3, 0.0, 1e-6),
775            (3, 1.0, 1e-6),
776        ];
777        let (sender, receiver) = std::sync::mpsc::channel();
778        std::thread::spawn(move || {
779            let values = cases.map(|(n, z, eta)| {
780                intkambe(n, Complex::new(z, 0.0), Complex::new(eta, 0.0)).unwrap()
781            });
782            sender.send(values).unwrap();
783        });
784        let values = receiver
785            .recv_timeout(std::time::Duration::from_secs(10))
786            .expect("the Kambe series must end within a bounded number of terms");
787        for ((n, z, eta), value) in cases.into_iter().zip(values) {
788            assert!(!finite(value), "I_{n}({z}, {eta}) = {value}");
789        }
790    }
791
792    proptest! {
793        #![proptest_config(ProptestConfig::with_cases(ALGEBRA_CASES))]
794
795        #[test]
796        fn kambe_series_and_recurrence(
797            n in -16_i32..=4,
798            z in complex(3.0),
799            (radius, phase) in (0.4_f64..3.0, -3.0_f64..1.5),
800        ) {
801            check_kambe_series(n, z, Complex::from_polar(radius, phase))?;
802        }
803
804        #[test]
805        fn kambe_argument_derivatives_and_reflection(
806            n in -8_i32..8, x in 0.5_f64..2.0, eta in 0.8_f64..1.8,
807        ) {
808            check_kambe(n, Complex::new(x, 0.1), Complex::new(eta, 0.03))?;
809        }
810    }
811
812    /// The gamma series below the base pair continues the closed-form even base pair
813    /// (a check of its branch sign), one pass of it over several orders gives each as
814    /// its own pass does, and every order obeys the Kambe recurrence
815    /// `I_n - (n+3) I_(n+2) + z^2 I_(n+4) = eta^(n+3) e^((1/eta^2 - z^2 eta^2)/2)`
816    /// relative to its largest term.
817    fn check_kambe_series(n: i32, z: Complex, eta: Complex) -> Result<(), TestCaseError> {
818        prop_assume!(z.norm() > 1e-3);
819        // The closed form cancels between its two erfc terms by up to a factor of
820        // about 10 here, and the series by the ratio of its term magnitudes to its sum
821        // (above 5000 at some complex eta); a wrong branch would miss by O(1). Both
822        // orders come from one pass of the series.
823        let pairs = kambe_series([-2, 0], z, eta)
824            .into_iter()
825            .zip(even_pair(z, eta));
826        for (order, ((series, magnitude), (expected, _))) in [-2, 0].into_iter().zip(pairs) {
827            prop_assert_close!(
828                series,
829                expected,
830                1e-12 * expected.norm() + SUM_ROUNDING * f64::EPSILON * magnitude,
831                "order {}",
832                order
833            );
834        }
835        if n % 2 != 0 && n < -3 {
836            let orders = [n, -3, -1];
837            let single = orders.map(|order| kambe_series([order], z, eta)[0].0);
838            for ((series, magnitude), expected) in
839                kambe_series(orders, z, eta).into_iter().zip(single)
840            {
841                prop_assert_close!(
842                    series,
843                    expected,
844                    1e-13 * expected.norm() + SUM_ROUNDING * f64::EPSILON * magnitude
845                );
846            }
847        }
848        let [value, middle, upper] = [n, n + 2, n + 4].map(|order| kambe(order, z, eta));
849        let endpoint = eta.powi(n + 3) * (0.5 * (1.0 / (eta * eta) - z * z * eta * eta)).exp();
850        let terms = [value, f64::from(n + 3) * middle, z * z * upper, endpoint];
851        let scale = terms.iter().map(|t| t.norm()).fold(0.0, f64::max);
852        // Even orders from -2 up rest on the closed-form base pair (see above).
853        let tolerance = if n % 2 == 0 && n + 4 >= -2 {
854            1e-12
855        } else {
856            1e-13
857        };
858        prop_assert_close!(value - terms[1] + terms[2], endpoint, tolerance * scale);
859        Ok(())
860    }
861
862    /// Kambe argument pullbacks match central differences in z and eta, and the
863    /// integral obeys Schwarz reflection in both arguments.
864    fn check_kambe(n: i32, z: Complex, eta: Complex) -> Result<(), TestCaseError> {
865        let g = Complex::new(0.7, -0.2);
866        let (_, residual) = intkambe_array(vec![n], [vec![z], vec![eta]]).unwrap();
867        let gradient = residual.pullback(&[g]).unwrap();
868        let delta = Complex::new(0.3, 0.4);
869        let differences = [
870            central(1e-5, |h| intkambe(n, z + h * delta, eta).unwrap()),
871            central(1e-5, |h| intkambe(n, z, eta + h * delta).unwrap()),
872        ];
873        for (gradient, difference) in gradient.iter().zip(differences) {
874            prop_assert_close!(
875                (gradient[0].conj() * delta).re,
876                (g.conj() * difference).re,
877                3e-6 * (1.0 + difference.norm())
878            );
879        }
880        let value = intkambe(n, z, eta).unwrap();
881        let reflected = intkambe(n, z.conj(), eta.conj()).unwrap();
882        prop_assert_close!(reflected, value.conj(), 1e-15 * value.norm());
883        Ok(())
884    }
885}