treams_core/special/integrals/
mod.rs

1//! Incomplete gamma and Kambe integrals used by the Ewald lattice sums.
2//! Recurrences follow DLMF 8.8 and Kambe (1968), as in treams.
3//!
4//! Upstream: `treams.special.incgamma` and `intkambe` (`_integrals.pyx`).
5//!
6//! [`incgamma`] and [`intkambe`] evaluate one integral after checking its inputs.
7//! [`incgamma_array`] and [`intkambe_array`] evaluate them over arrays and return an
8//! [`IncgammaResidual`] or [`IntkambeResidual`], which keeps the arguments for the
9//! gradient.
10//! The private submodules hold the numerics:
11//!
12//! - `gamma`: the upper incomplete gamma function and its scaled ladder;
13//! - `kambe`: the Kambe integrals;
14//! - `double`: double-double arithmetic for the small-split Kambe series.
15//!
16//! The Kambe integrals have several entry points, one per kind of caller:
17//!
18//! | Entry point | Returns | Caller |
19//! |---|---|---|
20//! | [`intkambe`] | `I_n(z, eta)` after checking the inputs | `treams_rs.special.intkambe`, [`intkambe_array`] |
21//! | `kambe` | `I_n(z, eta)` without checks | [`intkambe`], the [`IntkambeResidual`] pullback (order `n + 2`), real-space terms of the Ewald sums (`lattice::real`) |
22//! | `kambe_with_bound` | `I_n` and a bound on its rounding error | integer-order reduced integrals of the 1D spherical sums (`lattice::reduced`) |
23//! | `kambe_series`, `kambe_series_with` | the series over scaled incomplete gamma functions | `kambe`, `kambe_with_bound` and `SmallSplitKambe` only |
24//! | `EvenKambe` | the even orders of one argument from one recurrence | half-integer reduced integrals of the 2D spherical and 1D cylindrical sums (`lattice::reduced`) |
25//! | `SmallSplitKambe` | the orders of real-space terms at a split below every automatic one, with bounds | real-space terms at small splits (`lattice::real`) |
26#![allow(clippy::float_cmp, clippy::while_float, clippy::indexing_slicing)] // Half-integer degrees step exactly by one; order tables cover each requested order.
27
28mod double;
29mod gamma;
30mod kambe;
31
32pub(crate) use gamma::{ScaledGammaLadder, upper_gamma};
33pub(crate) use kambe::{EvenKambe, SmallSplitKambe, kambe, kambe_argument, kambe_with_bound};
34
35use super::MAX_LABEL;
36use crate::{
37    Complex, Error, MAX_DEGREE, Result,
38    numerics::{
39        broadcast::{self, element},
40        finite,
41        parallel::Parallel,
42    },
43};
44use gamma::gamma_derivative;
45
46/// Upper incomplete gamma function `Gamma(n, z) = integral_z^infinity t^(n-1) e^-t dt`.
47///
48/// The degree `n` is an integer or half-integer. The branch cut lies on the negative
49/// real axis, and the sign of a zero `Im z` selects its side.
50///
51/// Upstream: `treams.special.incgamma`. Differences: `|n|` above 128 and non-finite `z`
52/// give an error; treams runs one degree recurrence, while this function chooses among
53/// four methods the one that keeps relative accuracy at `z`.
54pub fn incgamma(n: f64, z: Complex) -> Result<Complex> {
55    if !n.is_finite() || (2.0 * n).fract() != 0.0 || n.abs() > f64::from(MAX_DEGREE) || !finite(z) {
56        return Err(Error::InvalidInput(
57            // 128 is MAX_DEGREE.
58            "gamma degree must be an integer or half-integer in [-128, 128], and z finite".into(),
59        ));
60    }
61    Ok(upper_gamma(n, z))
62}
63
64/// The Kambe integral `I_n(z, eta) = integral_eta^infinity t^n exp(-z^2 t^2/2 + 1/(2t^2)) dt`.
65///
66/// Upstream: `treams.special.intkambe`. Differences: `|n|` above 260 and non-finite
67/// arguments give an error. The odd orders -3 and -1 come from a series over scaled
68/// incomplete gamma functions, and lower orders from that series or the downward
69/// recurrence of treams, whichever bounds the rounding error more tightly.
70pub fn intkambe(n: i32, z: Complex, eta: Complex) -> Result<Complex> {
71    if !(-MAX_LABEL..=MAX_LABEL).contains(&n) || !finite(z) || !finite(eta) {
72        return Err(Error::InvalidInput(
73            // 260 is MAX_LABEL.
74            "Kambe order must be in [-260, 260] and arguments finite".into(),
75        ));
76    }
77    Ok(kambe(n, z, eta))
78}
79
80/// Elementwise integral evaluations and pullbacks run in parallel from this many elements.
81const PARALLEL: Parallel = Parallel::AtLeast(1024);
82const BROADCAST: &str = "arrays must have equal lengths or scalar inputs";
83const COTANGENT: &str = "integral cotangent must be finite and match output";
84
85/// [`incgamma`] over arrays, each input one value or one value per element, with the
86/// residual of their argument gradient.
87///
88/// Upstream: `treams.special.incgamma` over arrays.
89pub fn incgamma_array(
90    degrees: Vec<f64>,
91    arguments: Vec<Complex>,
92) -> Result<(Vec<Complex>, IncgammaResidual)> {
93    let residual = IncgammaResidual::new(degrees, arguments)?;
94    let value = broadcast::map(residual.size, PARALLEL, |i| {
95        incgamma(
96            element(&residual.degrees, i),
97            element(&residual.arguments, i),
98        )
99    })?;
100    Ok((value, residual))
101}
102
103/// What [`incgamma_array`] saves for its pullback: the degrees and the arguments. Only
104/// the arguments get gradients.
105#[derive(Debug)]
106pub struct IncgammaResidual {
107    degrees: Vec<f64>,
108    arguments: Vec<Complex>,
109    size: usize,
110}
111impl IncgammaResidual {
112    /// Keep the broadcast inputs needed by derivatives without evaluating values.
113    pub fn new(degrees: Vec<f64>, arguments: Vec<Complex>) -> Result<Self> {
114        let size = broadcast::size(&[degrees.len(), arguments.len()], BROADCAST)?;
115        Ok(Self {
116            degrees,
117            arguments,
118            size,
119        })
120    }
121
122    /// Directional derivative with respect to the complex argument.
123    pub fn pushforward(&self, tangents: [&[Complex]; 1]) -> Result<Vec<Complex>> {
124        broadcast::pushforward(
125            tangents,
126            self.size,
127            "gamma tangent must be finite and broadcast to output",
128            PARALLEL,
129            |i, [tangent]| {
130                if tangent == Complex::default() {
131                    return Ok(tangent);
132                }
133                let value = tangent
134                    * gamma_derivative(element(&self.degrees, i), element(&self.arguments, i));
135                if !finite(value) {
136                    return Err(Error::SpecialFunction(
137                        "gamma derivative is singular or overflowed".into(),
138                    ));
139                }
140                Ok(value)
141            },
142        )
143    }
144
145    /// The gradient with respect to the arguments, given the gradient `cotangent` with
146    /// respect to the values: `cotangent` times the conjugate of `dGamma/dz`.
147    pub fn pullback(&self, cotangent: &[Complex]) -> Result<Vec<Complex>> {
148        let [gradient] = broadcast::pullback(
149            cotangent,
150            self.size,
151            COTANGENT,
152            [self.arguments.len()],
153            PARALLEL,
154            |i, &g| {
155                if g == Complex::default() {
156                    return Ok([g]);
157                }
158                let gradient =
159                    g * gamma_derivative(element(&self.degrees, i), element(&self.arguments, i))
160                        .conj();
161                if !finite(gradient) {
162                    return Err(Error::SpecialFunction(
163                        "gamma derivative is singular or overflowed".into(),
164                    ));
165                }
166                Ok([gradient])
167            },
168        )?;
169        Ok(gradient)
170    }
171}
172
173/// [`intkambe`] over arrays, each input one value or one value per element, with the
174/// residual of their `z` and `eta` gradients; the integer order has none.
175///
176/// Upstream: `treams.special.intkambe` over arrays.
177pub fn intkambe_array(
178    orders: Vec<i32>,
179    arguments: [Vec<Complex>; 2],
180) -> Result<(Vec<Complex>, IntkambeResidual)> {
181    let residual = IntkambeResidual::new(orders, arguments)?;
182    let value = broadcast::map(residual.size, PARALLEL, |i| {
183        intkambe(
184            element(&residual.orders, i),
185            element(&residual.arguments[0], i),
186            element(&residual.arguments[1], i),
187        )
188    })?;
189    Ok((value, residual))
190}
191
192/// What [`intkambe_array`] saves for its pullback: the orders and the arguments. The
193/// pullback uses the adjacent-order recurrence and the endpoint integrand.
194#[derive(Debug)]
195pub struct IntkambeResidual {
196    orders: Vec<i32>,
197    arguments: [Vec<Complex>; 2],
198    size: usize,
199}
200impl IntkambeResidual {
201    /// Keep the broadcast inputs needed by derivatives without evaluating values.
202    pub fn new(orders: Vec<i32>, arguments: [Vec<Complex>; 2]) -> Result<Self> {
203        let size = broadcast::size(
204            &[orders.len(), arguments[0].len(), arguments[1].len()],
205            BROADCAST,
206        )?;
207        Ok(Self {
208            orders,
209            arguments,
210            size,
211        })
212    }
213
214    /// Directional derivative in `z` and `eta`. Inactive arguments do not evaluate
215    /// their derivative, so a direction in `eta` remains valid when only `dz` is singular.
216    pub fn pushforward(&self, tangents: [&[Complex]; 2]) -> Result<Vec<Complex>> {
217        broadcast::pushforward(
218            tangents,
219            self.size,
220            "Kambe tangents must be finite and broadcast to output",
221            PARALLEL,
222            |i, [tz, teta]| {
223                if tz == Complex::default() && teta == Complex::default() {
224                    return Ok(Complex::default());
225                }
226                let n = element(&self.orders, i);
227                let [z, eta] = self.arguments.each_ref().map(|v| element(v, i));
228                let dz = if tz == Complex::default() {
229                    Complex::default()
230                } else {
231                    tz * kambe_z_derivative(n, z, eta)
232                };
233                let deta = if teta == Complex::default() {
234                    Complex::default()
235                } else {
236                    teta * kambe_eta_derivative(n, z, eta)
237                };
238                let value = dz + deta;
239                if !finite(value) {
240                    return Err(Error::SpecialFunction(
241                        "Kambe derivative is singular or overflowed".into(),
242                    ));
243                }
244                Ok(value)
245            },
246        )
247    }
248
249    /// The gradients with respect to `z` and `eta`, given the gradient `cotangent` with
250    /// respect to the values. At `z = 0` the `z` gradient of orders `n <= -3` is its
251    /// limit, zero. Orders `n >= -2` have no `z` gradient at `z = 0` and give an error.
252    pub fn pullback(&self, cotangent: &[Complex]) -> Result<[Vec<Complex>; 2]> {
253        let lengths = self.arguments.each_ref().map(Vec::len);
254        broadcast::pullback(
255            cotangent,
256            self.size,
257            COTANGENT,
258            lengths,
259            PARALLEL,
260            |i, &g| {
261                if g == Complex::default() {
262                    return Ok([g; 2]);
263                }
264                let n = element(&self.orders, i);
265                let [z, eta] = self.arguments.each_ref().map(|v| element(v, i));
266                let dz = kambe_z_derivative(n, z, eta);
267                let deta = kambe_eta_derivative(n, z, eta);
268                let result = [dz, deta].map(|d| g * d.conj());
269                if result.iter().any(|&v| !finite(v)) {
270                    return Err(Error::SpecialFunction(
271                        "Kambe derivative is singular or overflowed".into(),
272                    ));
273                }
274                Ok(result)
275            },
276        )
277    }
278}
279
280fn kambe_z_derivative(n: i32, z: Complex, eta: Complex) -> Complex {
281    if z == Complex::default() && n <= -3 {
282        Complex::default()
283    } else {
284        -z * kambe(n + 2, z, eta)
285    }
286}
287
288fn kambe_eta_derivative(n: i32, z: Complex, eta: Complex) -> Complex {
289    -eta.powi(n) * (0.5 * (1.0 / (eta * eta) - z * z * eta * eta)).exp()
290}
291
292/// Reference cases of a `n re(z) im(z) [...]: re im` table with `#` comments, for the
293/// gamma and Kambe tests.
294#[cfg(test)]
295fn reference(text: &str) -> Vec<(Vec<f64>, Complex)> {
296    crate::test_support::table::<f64, f64>(text)
297        .into_iter()
298        .map(|(key, value)| (key, Complex::new(value[0], value[1])))
299        .collect()
300}