treams_core/lattice/
reciprocal.rs

1//! Reciprocal-space terms of the five sums (family and lattice dimension: sw3d, sw2d,
2//! sw1d, cw2d, cw1d) and the self term.
3//!
4//! Upstream: `recsum*`, `_recsw*`, `_reccw*`, `_n_sum_*`, `_s_sum_*`, `zero2d` and
5//! `zero3d` of `lattice/_esum.pyx`. The compensated threshold distance and the outgoing
6//! sheets are treams-rs extensions.
7
8use std::f64::consts::{PI, TAU};
9
10use super::{
11    accuracy::Rounding,
12    reduced::{Reduced, reduced_gamma},
13    sheets::{Sheet, Split, lower_side, self_sheet, sheet_turns, split_sheet},
14    wave::{Family, azimuthal, negative_order_sign, solid_jet},
15};
16use crate::{
17    Complex, Error, Result,
18    numerics::Jet,
19    special::{self, harmonic_normalization, log_factorial},
20};
21
22/// `F_1(v) = Gamma(1, v) / (-v) = exp(-v) / (-v)` of the 3D spherical and 2D
23/// cylindrical reciprocal sums, in closed form with `dF_1/dv = F_2 = (1 + v) exp(-v) / v^2`.
24fn exponential_kernel<const N: usize>(v: Jet<N>) -> Jet<N> {
25    let z = lower_side(v.value);
26    let exponential = (-z).exp();
27    let value = exponential / (-z);
28    if N == 0 {
29        return Jet::constant(value);
30    }
31    v.chain(value, (1.0 + z) * exponential / (z * z))
32}
33
34/// The inner sum `S_lmn` of the 2D spherical summand of [`reciprocal_term`] (upstream
35/// `_s_sum_sw2d` times `e^(i m phi)`), with `z = k r_z`, `beta^2 = beta2` and
36/// `beta e^(i phi) = (q_x + i q_y) / k`:
37/// `sum_s (-z)^(2n - s) beta^(l - s) e^(i m phi) / ((2n - s)! (s - n)! ((l + |m| - s) / 2)!
38/// ((l - |m| - s) / 2)!)` over `s = n..=min(l - |m|, 2n)` with even `l - |m| - s`.
39fn polynomial_sw2d<const N: usize>(
40    l: i32,
41    m: i32,
42    n: i32,
43    z: Jet<N>,
44    beta2: Jet<N>,
45    azimuth: Jet<N>,
46) -> Jet<N> {
47    let mut sum = Jet::default();
48    // On the lattice plane only the constant and, for jets, linear powers of z
49    // survive. In particular the linear term must stay: a vanishing sum may have a
50    // nonzero derivative normal to the plane.
51    let first = if z.value == Complex::default() {
52        n.max(2 * n - i32::from(N > 0))
53    } else {
54        n
55    };
56    for s in first..=(l - m.abs()).min(2 * n) {
57        if (l - m.abs() - s) % 2 != 0 {
58            continue;
59        }
60        sum += (-z).powi(2 * n - s)
61            * beta2.powi((l - s - m.abs()) / 2)
62            * (-log_factorial(2 * n - s)
63                - log_factorial(s - n)
64                - log_factorial((l + m.abs() - s) / 2)
65                - log_factorial((l - m.abs() - s) / 2))
66            .exp();
67    }
68    sum * azimuth.powi(m.abs())
69}
70
71/// The inner sum `S_lmn` of the 1D spherical summand of [`reciprocal_term`] (upstream
72/// `_s_sum_sw1d` times `(-1)^m e^(i m phi)`), with `rho2 = (k rho)^2`, `beta = q_z / k`
73/// and the azimuth `phi` of the shift: `sum_s (k rho)^(2n - s) beta^(l - s) (-1)^m
74/// e^(i m phi) / ((n - (s + |m|) / 2)! (n - (s - |m|) / 2)! (l - s)! (s - n)!)` over
75/// `s = n..=min(2n - |m|, l)` with even `s - |m|`.
76fn polynomial_sw1d<const N: usize>(
77    l: i32,
78    m: i32,
79    n: i32,
80    rho2: Jet<N>,
81    beta: Jet<N>,
82    azimuth: Jet<N>,
83) -> Jet<N> {
84    let mut sum = Jet::default();
85    for s in n..=(2 * n - m.abs()).min(l) {
86        if (m.abs() - s) % 2 != 0 {
87            continue;
88        }
89        sum += rho2.powi((2 * n - s - m.abs()) / 2)
90            * beta.powi(l - s)
91            * (-log_factorial(n - s.midpoint(m.abs()))
92                - log_factorial(n - (s - m.abs()) / 2)
93                - log_factorial(l - s)
94                - log_factorial(s - n))
95            .exp();
96    }
97    sum * azimuth.powi(m.abs())
98}
99
100/// The inner sum `S_ln` of the 1D cylindrical summand of [`reciprocal_term`] (upstream
101/// `_s_sum_cw1d`, whose docstring formula omits the factor `l!` that its code includes)
102/// for `l = |m|`, with `y = k r_y` (`-k r_y` for negative `m`) and `beta = q_x / k`:
103/// `sum_s (-y)^(2n - s) beta^(l - s) l! / (2^s (2n - s)! (l - s)! (s - n)!)` over
104/// `s = n..=min(2n, l)`.
105fn polynomial_cw1d<const N: usize>(l: i32, n: i32, y: Jet<N>, beta: Jet<N>) -> Jet<N> {
106    let mut sum = Jet::default();
107    for s in n..=(2 * n).min(l) {
108        sum += (-y).powi(2 * n - s)
109            * beta.powi(l - s)
110            * (log_factorial(l)
111                - log_factorial(2 * n - s)
112                - log_factorial(l - s)
113                - log_factorial(s - n)
114                - f64::from(s) * 2.0_f64.ln())
115            .exp();
116    }
117    sum
118}
119
120/// The reciprocal term of a value-only 1D cylindrical sum on its axis, which needs half
121/// of the polynomial terms: adjacent half-integer gamma functions share one evaluation,
122/// recurring downward near zero and upward at large arguments, where a downward step
123/// would subtract the leading asymptotic term. On a `sheet` ([`reduced_gamma`]) the
124/// endpoint term takes the phase `e^(-i pi n)` of the lower side.
125fn axial_cylindrical_reciprocal(
126    order: i32,
127    beta: Complex,
128    z: Complex,
129    eta: Complex,
130    sheet: Option<Sheet>,
131) -> Complex {
132    let maximum = order / 2;
133    let upward = z.norm() > 4.0;
134    let mut gamma = reduced_gamma(1 - 2 * if upward { maximum } else { 0 }, z, sheet);
135    let endpoint = if maximum > 0 {
136        (if sheet.is_none() && z.im > 0.0 {
137            Complex::i()
138        } else {
139            -Complex::i()
140        }) * (-z).exp()
141    } else {
142        Complex::default()
143    };
144    let mut sum = Complex::default();
145    for step in 0..=maximum {
146        let n = if upward { maximum - step } else { step };
147        let weight = beta.powi(order - 2 * n)
148            * (0.5 * eta * eta).powi(n)
149            * (log_factorial(order) - log_factorial(n) - log_factorial(order - 2 * n)).exp()
150            / (2.0_f64.sqrt() * eta);
151        sum += weight * gamma;
152        if step < maximum {
153            let phase = if n % 2 == 0 { endpoint } else { -endpoint };
154            gamma = if upward {
155                ((0.5 - f64::from(n)) * gamma + phase) / (-z)
156            } else {
157                (-z * gamma + phase) / (-0.5 - f64::from(n))
158            };
159        }
160    }
161    sum
162}
163
164/// `q . q - k^2` of a diffraction order to a few ulps of itself, from exact products and
165/// compensated additions: next to a threshold the rounded `q . q / k^2 - 1` loses about
166/// `eps k^2 / |k_q^2|` of it. Only the zeroth order of an unreduced Bloch vector takes it
167/// (within [`NEAR_THRESHOLD`]): every other `q` carries the rounding of its reciprocal
168/// vector, as large as the rounding this removes.
169fn compensated_threshold_distance(q: [Complex; 3], k: Complex) -> Complex {
170    let (mut re, mut im) = (Compensated::default(), Compensated::default());
171    for q in q {
172        re.add_product(q.re, q.re);
173        re.add_product(-q.im, q.im);
174        im.add_product(2.0 * q.re, q.im);
175    }
176    re.add_product(-k.re, k.re);
177    re.add_product(k.im, k.im);
178    im.add_product(-2.0 * k.re, k.im);
179    Complex::new(re.total(), im.total())
180}
181
182/// Largest moduli of the real and imaginary parts of `q . q / k^2 - 1` of the zeroth
183/// diffraction order at which the reciprocal sums take
184/// [`compensated_threshold_distance`]: there the rounded form loses 50 ulps of `v` or
185/// more (about 3 ulps of `q . q / k^2` over the difference).
186const NEAR_THRESHOLD: f64 = 1.0 / 16.0;
187
188/// Largest `|v|` (for the reciprocal terms) or `|k_q^2 / k^2|` (for the spectral series)
189/// at which a diffraction order counts as on its threshold `|q| = k`, where the sums
190/// are singular.
191pub(super) const THRESHOLD_DISTANCE: f64 = 1e-14;
192
193/// A sum of products that keeps the rounding errors of its products and additions
194/// apart until the end.
195#[derive(Clone, Copy, Debug, Default)]
196struct Compensated {
197    sum: f64,
198    error: f64,
199}
200
201impl Compensated {
202    fn add_product(&mut self, a: f64, b: f64) {
203        let product = a * b;
204        let total = self.sum + product;
205        let back = total - self.sum;
206        self.error += a.mul_add(b, -product) + (self.sum - (total - back)) + (product - back);
207        self.sum = total;
208    }
209
210    fn total(self) -> f64 {
211        self.sum + self.error
212    }
213}
214
215/// The wavevector `q = kpar + G` of one reciprocal summand and its shared arguments.
216#[derive(Clone, Copy, Debug)]
217pub(super) struct Diffraction<const N: usize> {
218    q: [Jet<N>; 3],
219    beta2: Jet<N>,
220    value: Jet<N>,
221}
222
223impl<const N: usize> Diffraction<N> {
224    /// Prepare the arguments once per diffraction point. `exact` marks the zeroth
225    /// order of an unreduced Bloch vector, which takes compensated threshold distance.
226    #[allow(clippy::inline_always)]
227    #[inline(always)]
228    pub(super) fn new(q: [Jet<N>; 3], exact: bool, k: Jet<N>, eta: Complex) -> Result<Self> {
229        let beta2 = q.into_iter().map(|q| q * q).sum::<Jet<N>>() / (k * k);
230        let mut value = (beta2 - 1.0) / (2.0 * eta * eta);
231        let distance = beta2.value - 1.0;
232        if exact && distance.re.abs().max(distance.im.abs()) < NEAR_THRESHOLD {
233            value.value = compensated_threshold_distance(q.map(|q| q.value), k.value)
234                / (k.value * k.value)
235                / (2.0 * eta * eta);
236        }
237        value.value = lower_side(value.value);
238        if value.value.norm() <= THRESHOLD_DISTANCE {
239            return Err(Error::InvalidInput(
240                "lattice sum is at a diffraction threshold; supply a limiting complex wavenumber"
241                    .into(),
242            ));
243        }
244        Ok(Self { q, beta2, value })
245    }
246}
247
248/// One reciprocal summand `T(q)` of the sum, without the Bloch phase `exp(-i q . r)`
249/// that the caller multiplies in. Every sum shares the argument `v = (q . q / k^2 - 1) /
250/// (2 eta^2)`; `wave` and `dim` select the sum. 1D spherical sums, whose odd Kambe
251/// orders cancel by up to `e^(w^2)` at `w = k rho eta`, add the rounding bounds of its
252/// value and derivatives in units of epsilon to `bound`; the other sums keep their
253/// relative accuracy.
254pub(super) fn reciprocal_term<const N: usize>(
255    wave: Family,
256    dim: usize,
257    k: Jet<N>,
258    Diffraction { q, beta2, value }: Diffraction<N>,
259    r: [Jet<N>; 3],
260    eta: Complex,
261    split: &Split,
262    measure: Jet<N>,
263    bound: &mut Rounding<N>,
264    plane_integrals: Option<&mut Vec<Option<Complex>>>,
265) -> Result<Jet<N>> {
266    let i = Complex::i();
267    // The five summands share this one match: the term loop of every sum inlines
268    // this function, and in separate functions the summands make the forward 3D sums
269    // up to 13% slower.
270    Ok(match (wave, dim) {
271        // 3D spherical sum (upstream `recsumsw3d`, summand `_recsw3d`), with the cell
272        // volume `V = measure` and `F_1` of `exponential_kernel`:
273        // `2 i (-i)^l pi Y_lm(q / |q|) (|q| / k)^l F_1(v) / (V k^3 eta^2)`.
274        (Family::Spherical { l, m }, 3) => {
275            let harmonic = harmonic_normalization(l, m) * solid_jet(l, m, q);
276            2.0 * i * (-i).powi(l) * PI / (measure * k.powi(3)) * harmonic / k.powi(l)
277                * exponential_kernel(value)
278                / (eta * eta)
279        }
280        // 2D cylindrical sum (upstream `recsumcw2d`, summand `_reccw2d`), with the cell
281        // area `A = measure`, `beta e^(i phi) = (q_x + i q_y) / k` and `F_1` of
282        // `exponential_kernel`: `2 i (-i)^m beta^|m| e^(i m phi) F_1(v) / (A k^2 eta^2)`.
283        (Family::Cylindrical { m }, 2) => {
284            let xy = azimuthal(q[0], q[1], m);
285            2.0 * i * (-i).powi(m) / (measure * k * k)
286                * (xy / k).powi(m.abs())
287                * exponential_kernel(value)
288                / (eta * eta)
289        }
290        // 2D spherical sum for any shift (upstream `recsumsw2d_shift`, summand
291        // `_n_sum_sw2d`; at `r_z = 0` it also gives upstream `recsumsw2d`), with the cell
292        // area `A = measure`:
293        //
294        // `sqrt(2l + 1) (-i)^m sqrt((l + m)! (l - m)!) / (A k^2 (-2)^l)
295        // sum_(n = 0..=l - |m|) (2 eta^2)^n / (sqrt(2) eta) F_(1/2 - n)(v, (k r_z eta)^2) S_lmn`
296        //
297        // with the reduced integrals `F` of `Reduced` and the inner sums `S_lmn` of
298        // `polynomial_sw2d`, summed by `half_integer_orders`.
299        (Family::Spherical { l, m }, 2) => {
300            let z = k * r[2];
301            let azimuth = azimuthal(q[0], q[1], m) / k;
302            let factor = 1.0 / (2.0_f64.sqrt() * eta);
303            // For Im k < 0 the sums in the plane depend on the split, and with
304            // Re(k eta) < 0 the real parts do not converge to the sum: both keep the
305            // principal branches.
306            let sheet = (k.value.im >= 0.0 && (k.value * eta).re > 0.0).then(|| {
307                let q2 = q[0].value * q[0].value + q[1].value * q[1].value;
308                split_sheet(split, q2)
309            });
310            let mut reduced = Reduced::new(value, (z * eta).powi(2), sheet);
311            // Higher orders contain only powers z^2 and above on the plane, or z
312            // and above when no derivatives are requested.
313            let orders = if z.value == Complex::default() {
314                (l - m.abs()).midpoint(i32::from(N > 0))
315            } else {
316                l - m.abs()
317            };
318            let sum = half_integer_orders(
319                &mut reduced,
320                split,
321                (factor, eta),
322                orders,
323                |n| polynomial_sw2d(l, m, n, z, beta2, azimuth),
324                plane_integrals,
325            );
326            f64::from(2 * l + 1).sqrt()
327                * (-i).powi(m)
328                * log_factorial(l + m).midpoint(log_factorial(l - m)).exp()
329                / (measure * k * k * (-2.0_f64).powi(l))
330                * sum
331        }
332        // 1D spherical sum along `z` (upstream `recsumsw1d_shift`, summand
333        // `_n_sum_sw1d`), with the period `a = measure` and `rho^2 = r_x^2 + r_y^2`:
334        //
335        // `-i sqrt((2l + 1) / pi) (-i)^(l - m) sqrt((l + m)! (l - m)!) / (2 a k)
336        // sum_(n = |m|..=l) (eta^2 / 2)^n F_(-n)(v, (k rho eta)^2) S_lmn`
337        //
338        // with the reduced integrals `F` of `Reduced` and the inner sums `S_lmn` of
339        // `polynomial_sw1d`. Adds the rounding bounds of the integer orders `F_(-n)` to
340        // `bound`.
341        (Family::Spherical { l, m }, 1) => {
342            let beta = q[2] / k;
343            let rho2 = k * k * (r[0] * r[0] + r[1] * r[1]);
344            let azimuth = -k * azimuthal(r[0], r[1], m);
345            let mut sum = Jet::default();
346            let mult = 0.5 * eta * eta;
347            let mut factor = mult.powi(m.abs());
348            let sheet = split_sheet(split, q[2].value * q[2].value);
349            let mut reduced = Reduced::new(value, rho2 * eta * eta, Some(sheet));
350            let mut sum_bound = Rounding::default();
351            for n in m.abs()..=l {
352                let polynomial = polynomial_sw1d(l, m, n, rho2, beta, azimuth);
353                if !polynomial.is_zero() {
354                    let (integral, integral_bound) = reduced.get(-2 * n);
355                    sum += factor * integral * polynomial;
356                    sum_bound += integral_bound.times(&(factor * polynomial));
357                }
358                factor *= mult;
359            }
360            let prefactor = -i
361                * (f64::from(2 * l + 1) / PI).sqrt()
362                * (-i).powi(l - m)
363                * log_factorial(l + m).midpoint(log_factorial(l - m)).exp()
364                / (2.0 * measure * k);
365            *bound += sum_bound.times(&prefactor);
366            prefactor * sum
367        }
368        // 1D cylindrical sum along `x` (upstream `recsumcw1d_shift`, summand
369        // `_n_sum_cw1d`), with the period `a = measure`:
370        //
371        // `2 (-i)^m / (sqrt(pi) a k)
372        // sum_(n = 0..=|m|) (2 eta^2)^n / (sqrt(2) eta') F_(1/2 - n)(v, (k r_y eta)^2) S_(|m|n)`
373        //
374        // with `eta' = ±eta`, the root of `eta^2` with `Re(k eta') >= 0`, the reduced
375        // integrals `F` of `Reduced` and the inner sums `S_ln` of `polynomial_cw1d`,
376        // summed by `half_integer_orders`. Value-only sums on the axis (`r_y = 0`) take
377        // `axial_cylindrical_reciprocal`.
378        (Family::Cylindrical { m }, 1) => {
379            let beta = q[0] / k;
380            let y = k * if m < 0 { -r[1] } else { r[1] };
381            // The real part and the reduced integrals depend on eta^2 alone, the sum on
382            // the sign of eta only through the factor 1 / eta: splits with Re(k eta) < 0
383            // take the root -eta of eta^2, with which the Ewald parts converge to the
384            // sum. For Im k < 0 the sums on the axis depend on the split: they keep the
385            // principal branches.
386            let root = if (k.value * eta).re < 0.0 { -eta } else { eta };
387            let factor = 1.0 / (2.0_f64.sqrt() * root);
388            let sheet = (k.value.im >= 0.0).then(|| split_sheet(split, q[0].value * q[0].value));
389            if N == 0 && r[1].value == Complex::default() {
390                return Ok(Jet::constant(
391                    2.0 * (-i).powi(m) / (PI.sqrt() * measure.value * k.value)
392                        * axial_cylindrical_reciprocal(
393                            m.abs(),
394                            beta.value,
395                            value.value,
396                            root,
397                            sheet,
398                        ),
399                ));
400            }
401            let mut reduced = Reduced::new(value, (y * eta).powi(2), sheet);
402            let sum = half_integer_orders(
403                &mut reduced,
404                split,
405                (factor, eta),
406                m.abs(),
407                |n| polynomial_cw1d(m.abs(), n, y, beta),
408                None,
409            );
410            2.0 * (-i).powi(m) / (PI.sqrt() * measure * k) * sum
411        }
412        _ => return Err(Error::InvalidInput("invalid lattice dimension".into())),
413    })
414}
415
416/// `sum_n factor (2 eta^2)^n F_(1/2 - n) P(n)` over the half-integer orders
417/// `n = 0..=orders` of one reciprocal point with the degree polynomials `P` (zero ones
418/// skipped). Below every automatic split the orders weigh their terms, and are summed
419/// again from the single Kambe integrals where those bound the sum more tightly
420/// ([`Reduced::prefers_single`]).
421fn half_integer_orders<const N: usize>(
422    reduced: &mut Reduced<N>,
423    split: &Split,
424    (factor, eta): (Complex, Complex),
425    orders: i32,
426    polynomial: impl Fn(i32) -> Jet<N>,
427    mut plane_integrals: Option<&mut Vec<Option<Complex>>>,
428) -> Jet<N> {
429    let mut sum = |reduced: &mut Reduced<N>| {
430        let (mut sum, mut factor) = (Jet::default(), factor);
431        for n in 0..=orders {
432            let polynomial = polynomial(n);
433            if !polynomial.is_zero() {
434                if split.small {
435                    reduced.weigh(1 - 2 * n, factor * polynomial.value);
436                }
437                let integral = match plane_integrals.as_deref_mut() {
438                    Some(cached) => reduced.get_plane_cached(1 - 2 * n, cached),
439                    None => reduced.get(1 - 2 * n).0,
440                };
441                sum += factor * integral * polynomial;
442            }
443            factor *= 2.0 * eta * eta;
444        }
445        sum
446    };
447    let from_recurrence = sum(reduced);
448    if split.small && reduced.prefers_single() {
449        sum(reduced)
450    } else {
451        from_recurrence
452    }
453}
454
455/// The self term: the Cartesian Taylor term of `real_Ewald(-r) - outgoing(-r)` at the
456/// origin. Upstream `zero2d` (cylindrical) and `zero3d` (spherical) give its value at
457/// degree and order zero. Keeping its first-order polynomial gives the regular
458/// image-sum derivative at self.
459///
460/// # Branch
461///
462/// Its incomplete gamma function of `v = -1 / (2 eta^2)` takes the sheet
463/// ([`self_sheet`]) of the principal `ln((k eta)^2)` of the real-space Kambe integrals
464/// less `2 ln k` of the outgoing waves. For a sheet `j` turns off the principal one,
465/// DLMF 8.2.10 gives `2 Gamma(a) - Gamma(a, v)` for half-integer `a` and odd `j`, and
466/// `Gamma(-n, v) - 2 pi i j (-1)^n / n!` for `a = -n`.
467pub(super) fn self_term<const N: usize>(
468    wave: Family,
469    k: Jet<N>,
470    r: [Jet<N>; 3],
471    eta: Complex,
472) -> Jet<N> {
473    let value = lower_side(-0.5 / (eta * eta));
474    let turns = sheet_turns(self_sheet(k.value, eta), value);
475    match wave {
476        Family::Spherical { l, m } => {
477            let degree = -f64::from(l) - 0.5;
478            let mut gamma = special::upper_gamma(degree, value);
479            if turns % 2.0 != 0.0 {
480                gamma = 2.0 * libm::tgamma(degree) - gamma;
481            }
482            k.powi(l)
483                * solid_jet(l, m, r)
484                * harmonic_normalization(l, m)
485                * (2.0_f64.powi(-l - 1) / PI.sqrt())
486                * gamma
487        }
488        Family::Cylindrical { m } => {
489            let (xy, sign) = (azimuthal(r[0], r[1], m), negative_order_sign(m));
490            let n = m.abs();
491            let parity = if n % 2 == 0 { 1.0 } else { -1.0 };
492            let gamma = special::upper_gamma(-f64::from(n), value)
493                - Complex::new(0.0, TAU * turns) * parity * (-log_factorial(n)).exp();
494            Complex::i() / PI * sign * (0.5 * k * xy).powi(n) * gamma
495        }
496    }
497}
498
499#[cfg(test)]
500mod tests {
501    use super::polynomial_sw2d;
502    use crate::{Complex, numerics::Jet, special::log_factorial};
503
504    /// The unrestricted polynomial, including all powers that vanish on the plane,
505    /// is the reference for the shorter evaluation and its normal derivative.
506    fn full_polynomial<const N: usize>(
507        l: i32,
508        m: i32,
509        n: i32,
510        z: Jet<N>,
511        beta2: Jet<N>,
512        azimuth: Jet<N>,
513    ) -> Jet<N> {
514        let mut sum = Jet::default();
515        for s in n..=(l - m.abs()).min(2 * n) {
516            if (l - m.abs() - s) % 2 != 0 {
517                continue;
518            }
519            sum += (-z).powi(2 * n - s)
520                * beta2.powi((l - s - m.abs()) / 2)
521                * (-log_factorial(2 * n - s)
522                    - log_factorial(s - n)
523                    - log_factorial((l + m.abs() - s) / 2)
524                    - log_factorial((l - m.abs() - s) / 2))
525                .exp();
526        }
527        sum * azimuth.powi(m.abs())
528    }
529
530    fn check_polynomials<const N: usize>() {
531        for z in [0.0, 1e-20, -1e-20, 0.3] {
532            let z = Jet::<N>::variable(z, 0);
533            let beta2 = Jet::variable(Complex::new(0.73, 0.19), 1);
534            let azimuth = Jet::variable(Complex::new(-0.41, 0.24), 2);
535            for l in 0_i32..=12 {
536                for m in -l..=l {
537                    for n in 0..=l - m.abs() {
538                        let value = polynomial_sw2d(l, m, n, z, beta2, azimuth);
539                        let reference = full_polynomial(l, m, n, z, beta2, azimuth);
540                        assert_eq!(value.value, reference.value, "l={l}, m={m}, n={n}");
541                        assert_eq!(
542                            value.derivative, reference.derivative,
543                            "l={l}, m={m}, n={n}"
544                        );
545                    }
546                }
547            }
548        }
549    }
550
551    #[test]
552    fn planar_polynomials_keep_the_full_values_and_gradients() {
553        check_polynomials::<0>();
554        check_polynomials::<3>();
555    }
556}