treams_core/special/
wigner.rs

1//! Wigner 3j symbols from the Schulten-Gordon recurrence in `j3`, continued in each
2//! direction while the symbols grow, and the Wigner small-d and D functions in the z-y-z
3//! convention.
4//!
5//! Upstream: `treams.special.wigner3j` (`_wigner3j.pyx`), `wignersmalld` and `wignerd`
6//! (`_wignerd.pyx`).
7//!
8//! Two algorithms compute small-d values, because their callers need different things:
9//!
10//! - [`wigner_small_d_matrix`] gives the whole real `(2l + 1) x (2l + 1)` block of one
11//!   degree at a real angle, from the eigenvectors of the symmetric angular-momentum
12//!   generator. Multipole rotations need every element of a block, and the eigensystem
13//!   avoids the cancellation of the factorial sums.
14//! - [`wigner_small_d`] gives one element at a real or complex angle from a Jacobi
15//!   polynomial recurrence in `O(l)` time. Its kernel takes a `numerics::Jet` angle, so
16//!   the Euler-angle pullbacks of [`wigner_d_array`] differentiate the same code.
17//!
18//! The two agree to `2e-12 + 1e-10 |d|` per element up to degree 128: the property
19//! `wigner_small_d_matches_the_generator_exponential` in `properties/special.rs` checks
20//! this bound.
21#![allow(clippy::indexing_slicing)] // Complete small-d matrices and fixed Euler-angle triples.
22
23use nalgebra::{DMatrix, SymmetricEigen};
24
25use super::log_factorial;
26use crate::{
27    Complex, Error, MAX_DEGREE, Result,
28    linalg::product,
29    numerics::{Jet, broadcast, finite, parallel::Parallel, parity},
30};
31
32/// The coefficient `A(j3)` of the three-term recurrence of Schulten and Gordon (1975)
33/// in `j3`, `j3 A(j3 + 1) f(j3 + 1) + B(j3) f(j3) + (j3 + 1) A(j3) f(j3 - 1) = 0` for
34/// `f(j3) = (j1 j2 j3; m1 m2 m3)`: Eq. (7) of Y.-L. Xu, J. Comput. Phys. 139, 137
35/// (1998), as cited by treams (`_coeffc`).
36fn c(j1: i32, j2: i32, j3: i32, m3: i32) -> f64 {
37    ((f64::from(j3).powi(2) - f64::from(j1 - j2).powi(2))
38        * (f64::from(j1 + j2 + 1).powi(2) - f64::from(j3).powi(2))
39        * (f64::from(j3).powi(2) - f64::from(m3).powi(2)))
40    .sqrt()
41}
42
43/// The coefficient `B(j3)` of the recurrence of [`c`]: Eq. (7) of Xu (1998), treams
44/// `_coeffd`.
45fn d(j1: i32, j2: i32, j3: i32, m1: i32, m2: i32, m3: i32) -> f64 {
46    f64::from(2 * j3 + 1)
47        * (f64::from(j3 * (j3 + 1)) * f64::from(m2 - m1)
48            + f64::from(j2 * (j2 + 1) - j1 * (j1 + 1)) * f64::from(m3))
49}
50
51/// The closed form of the symbol at the lower end `j3 = j1 - j2` of a row: Eq. (14) of
52/// Xu (1998), treams `_initforwardj`.
53fn initial_j(j1: i32, j2: i32, m1: i32, m2: i32) -> f64 {
54    parity(j1 + m1)
55        * (0.5
56            * (log_factorial(j1 - m1)
57                + log_factorial(j1 + m1)
58                + log_factorial(2 * j1 - 2 * j2)
59                + log_factorial(2 * j2)
60                - log_factorial(j2 - m2)
61                - log_factorial(j2 + m2)
62                - log_factorial(j1 - j2 - m1 - m2)
63                - log_factorial(j1 - j2 + m1 + m2)
64                - log_factorial(2 * j1 + 1)))
65        .exp()
66}
67
68/// The closed form of the symbol at the lower end `j3 = m1 + m2` of a row: Eq. (16) of
69/// Xu (1998), treams `_initforwardm`.
70fn initial_m(j1: i32, j2: i32, m1: i32, m2: i32) -> f64 {
71    parity(j2 + m2)
72        * (0.5
73            * (log_factorial(j1 + m1)
74                + log_factorial(j2 + m2)
75                + log_factorial(j1 + j2 - m1 - m2)
76                + log_factorial(2 * m1 + 2 * m2)
77                - log_factorial(j1 - m1)
78                - log_factorial(j2 - m2)
79                - log_factorial(j1 - j2 + m1 + m2)
80                - log_factorial(j2 - j1 + m1 + m2)
81                - log_factorial(j1 + j2 + m1 + m2 + 1)))
82        .exp()
83}
84
85/// The symbol at the lower end `j3 = minimum` of a row, in closed form: [`initial_j`]
86/// or [`initial_m`], with labels swapped or negated by the symmetries of the symbol.
87fn lowest(j1: i32, j2: i32, m1: i32, m2: i32, minimum: i32) -> f64 {
88    if minimum == j1 - j2 {
89        initial_j(j1, j2, m1, m2)
90    } else if minimum == j2 - j1 {
91        initial_j(j2, j1, m2, m1)
92    } else if minimum == m1 + m2 {
93        initial_m(j1, j2, m1, m2)
94    } else {
95        initial_m(j2, j1, -m2, -m1)
96    }
97}
98
99/// The symbol at the upper end `j3 = j1 + j2` of a row, in closed form, as treams starts
100/// its downward recurrence (`_wigner3jbackward`).
101fn highest(j1: i32, j2: i32, m1: i32, m2: i32) -> f64 {
102    let (maximum, m3) = (j1 + j2, -m1 - m2);
103    parity(maximum - m3)
104        * (0.5
105            * (log_factorial(2 * j1)
106                + log_factorial(2 * j2)
107                + log_factorial(maximum + m3)
108                + log_factorial(maximum - m3)
109                - log_factorial(2 * maximum + 1)
110                - log_factorial(j1 - m1)
111                - log_factorial(j1 + m1)
112                - log_factorial(j2 - m2)
113                - log_factorial(j2 + m2)))
114        .exp()
115}
116
117/// The first `j` in `minimum + 1..=stop` at which the upward recurrence of the row
118/// `(j1 j2 j; m1 m2 -m1-m2)` stops growing in magnitude, or `stop + 1`. Iterates the
119/// ratio of consecutive symbols, which needs no closed-form start value.
120fn growth_end(j1: i32, j2: i32, m1: i32, m2: i32, minimum: i32, stop: i32) -> i32 {
121    let m3 = -m1 - m2;
122    let mut ratio = f64::INFINITY;
123    for j in minimum + 1..=stop {
124        ratio = if minimum == 0 && j == 1 {
125            -f64::from(m2 - m1) / c(j1, j2, 1, m3)
126        } else {
127            (-d(j1, j2, j - 1, m1, m2, m3) - f64::from(j) * c(j1, j2, j - 1, m3) / ratio)
128                / (f64::from(j - 1) * c(j1, j2, j, m3))
129        };
130        if ratio.abs() < 1.0 {
131            return j;
132        }
133    }
134    stop + 1
135}
136
137/// The lowest `j` in `stop..=maximum` down to which the downward recurrence of the row
138/// from `maximum = j1 + j2` grows in magnitude at every step, so that the symbols decay
139/// upward from `j` on; `stop` if they do down to `stop`. The mirror of [`growth_end`].
140fn decay_start(j1: i32, j2: i32, m1: i32, m2: i32, maximum: i32, stop: i32) -> i32 {
141    let m3 = -m1 - m2;
142    let mut ratio = f64::INFINITY;
143    for j in (stop..maximum).rev() {
144        ratio = if j == maximum - 1 {
145            -d(j1, j2, maximum, m1, m2, m3) / (f64::from(maximum + 1) * c(j1, j2, maximum, m3))
146        } else {
147            (-d(j1, j2, j + 1, m1, m2, m3) - f64::from(j + 1) * c(j1, j2, j + 2, m3) / ratio)
148                / (f64::from(j + 2) * c(j1, j2, j + 1, m3))
149        };
150        if ratio.abs() < 1.0 {
151            return j + 1;
152        }
153    }
154    stop
155}
156
157/// The first degree [`visit`] recurs downward, as far as `first..=last` can tell:
158/// `max(growth_end, min(switch, decay_start))` with each scan cut off where it no
159/// longer decides a degree in the range. Whether a `j3 >= first` lies below the turn
160/// is therefore the same for every range.
161fn turn(
162    j1: i32,
163    j2: i32,
164    m1: i32,
165    m2: i32,
166    [minimum, maximum]: [i32; 2],
167    [first, last]: [i32; 2],
168) -> i32 {
169    let switch = (j1 + j2 - (j1 - j2).abs()) / 4 + (j1 - j2).abs();
170    let growth_end = growth_end(j1, j2, m1, m2, minimum, last);
171    if growth_end > last {
172        return last + 1;
173    }
174    let bound = growth_end.max(first);
175    if switch <= bound {
176        bound
177    } else {
178        switch.min(decay_start(j1, j2, m1, m2, maximum, bound))
179    }
180}
181
182/// Visits `(j3, (j1 j2 j3; m1 m2 -m1-m2))` for every `j3` in `first..=last`, a subrange of
183/// the admissible `max(|j1 - j2|, |m1 + m2|)..=j1 + j2`, with treams' two-sided
184/// recurrence in `j3` (Schulten and Gordon).
185///
186/// The upward recurrence from the closed form at the lower end is stable while the
187/// symbols grow, in the classically forbidden region above the lower end; the downward
188/// recurrence from the closed form at `j1 + j2` is stable while they grow downward,
189/// through the upper forbidden region; both are neutral where the symbols oscillate.
190/// treams switches from upward to downward at `|j1 - j2| + (j1 + j2 - |j1 - j2|) / 4`,
191/// whatever the orders. For extreme orders either forbidden region can reach past that
192/// switch, and recurring into it loses all accuracy (errors up to 1e-2 below `j = 90`,
193/// a factor of 1800 at `j = 260`). Here the upward recurrence therefore
194/// continues past the switch while the symbols grow, and the downward recurrence
195/// continues below it while they grow downward: the turn is
196/// `max(growth_end, min(switch, decay_start))`. Whether a `j3` is recurred upward or
197/// downward does not depend on `first` or `last`, so rows and single symbols agree bit
198/// for bit.
199fn visit(
200    j1: i32,
201    j2: i32,
202    m1: i32,
203    m2: i32,
204    [first, last]: [i32; 2],
205    mut emit: impl FnMut(i32, f64),
206) {
207    let m3 = -m1 - m2;
208    let minimum = (j1 - j2).abs().max(m3.abs());
209    let maximum = j1 + j2;
210    // The first degree recurred downward.
211    let turn = turn(j1, j2, m1, m2, [minimum, maximum], [first, last]);
212    if first < turn {
213        let (mut value, mut previous) = (lowest(j1, j2, m1, m2, minimum), 0.0);
214        for j in minimum..turn.min(last + 1) {
215            if j > minimum {
216                let next = if minimum == 0 && j == 1 {
217                    -value * f64::from(m2 - m1) / c(j1, j2, 1, m3)
218                } else {
219                    (-d(j1, j2, j - 1, m1, m2, m3) * value
220                        - f64::from(j) * c(j1, j2, j - 1, m3) * previous)
221                        / (f64::from(j - 1) * c(j1, j2, j, m3))
222                };
223                (previous, value) = (value, next);
224            }
225            if j >= first {
226                emit(j, value);
227            }
228        }
229    }
230    if turn > last {
231        return;
232    }
233    let (mut value, mut previous) = (highest(j1, j2, m1, m2), 0.0);
234    if last == maximum {
235        emit(maximum, value);
236    }
237    for j in (turn.max(first)..maximum).rev() {
238        // At the upper endpoint the second term is identically zero. Evaluating
239        // its coefficient outside the triangle would take sqrt of a negative.
240        let next = if j == maximum - 1 {
241            -d(j1, j2, j + 1, m1, m2, m3) * value / (f64::from(j + 2) * c(j1, j2, j + 1, m3))
242        } else {
243            (-d(j1, j2, j + 1, m1, m2, m3) * value
244                - f64::from(j + 1) * c(j1, j2, j + 2, m3) * previous)
245                / (f64::from(j + 2) * c(j1, j2, j + 1, m3))
246        };
247        (previous, value) = (value, next);
248        if j <= last {
249            emit(j, value);
250        }
251    }
252}
253
254/// Wigner 3j symbol `(j1 j2 j3; m1 m2 m3)`, zero outside the admissible labels.
255///
256/// Evaluated with treams' two-sided recurrence, continued in each direction for as long
257/// as the symbols grow (see `visit`).
258///
259/// Upstream: `treams.special.wigner3j`. Differences: more accurate for large `j` with
260/// extreme orders, and equal up to rounding elsewhere; negative `j` gives zero, where
261/// treams raises `ValueError`.
262#[must_use]
263pub fn wigner3j(j1: i32, j2: i32, j3: i32, m1: i32, m2: i32, m3: i32) -> f64 {
264    if j1 < 0
265        || j2 < 0
266        || j3 < 0
267        || j3 < (j1 - j2).abs()
268        || j3 > j1 + j2
269        || m1.abs() > j1
270        || m2.abs() > j2
271        || m3.abs() > j3
272        || m1 + m2 + m3 != 0
273    {
274        return 0.0;
275    }
276    let mut symbol = 0.0;
277    visit(j1, j2, m1, m2, [j3, j3], |_, value| symbol = value);
278    symbol
279}
280
281/// Wigner 3j symbols `(j1 j2 j3; m1 m2 -m1-m2)` for every admissible `j3` from one
282/// recurrence, equal bit for bit to [`wigner3j`] at each `j3`.
283#[derive(Debug, Default)]
284pub(crate) struct Wigner3jRow {
285    first: i32,
286    values: Vec<f64>,
287}
288
289impl Wigner3jRow {
290    /// The row `(j1 j2 j3; m1 m2 -m1-m2)` over every admissible `j3`; empty for
291    /// inadmissible `j1, j2, m1, m2`.
292    pub(crate) fn new(j1: i32, j2: i32, m1: i32, m2: i32) -> Self {
293        if j1 < 0 || j2 < 0 || m1.abs() > j1 || m2.abs() > j2 {
294            return Self::default();
295        }
296        let first = (j1 - j2).abs().max((m1 + m2).abs());
297        let mut values = vec![0.0; usize::try_from(j1 + j2 + 1 - first).unwrap_or_default()];
298        visit(j1, j2, m1, m2, [first, j1 + j2], |j3, value| {
299            if let Some(slot) = usize::try_from(j3 - first)
300                .ok()
301                .and_then(|index| values.get_mut(index))
302            {
303                *slot = value;
304            }
305        });
306        Self { first, values }
307    }
308
309    /// The symbol at `j3`; zero outside the admissible range.
310    pub(crate) fn get(&self, j3: i32) -> f64 {
311        usize::try_from(j3 - self.first)
312            .ok()
313            .and_then(|index| self.values.get(index))
314            .copied()
315            .unwrap_or_default()
316    }
317}
318
319/// Ladder coefficient `sqrt(l(l + 1) - m(m + 1)) / 2` of the angular-momentum generator.
320pub(crate) fn ladder(l: i32, m: i32) -> f64 {
321    0.5 * f64::from(l * (l + 1) - m * (m + 1)).max(0.0).sqrt()
322}
323
324/// Position `l + m` of order `m` in a degree-`l` block ordered `-l..=l`.
325pub(crate) fn index(l: i32, m: i32) -> usize {
326    usize::try_from(l + m).unwrap_or_default()
327}
328
329/// Complete real Wigner small-d matrix `d^l(theta)` of one degree, rows and columns
330/// ordered `-l..=l`, for multipole rotations. A symmetric angular-momentum eigensystem
331/// avoids factorial-sum cancellation.
332pub(crate) fn wigner_small_d_matrix(l: i32, theta: f64) -> Result<DMatrix<f64>> {
333    if !(0..=MAX_DEGREE).contains(&l) || !theta.is_finite() {
334        return Err(Error::InvalidInput(
335            // 128 is MAX_DEGREE.
336            "require 0 <= l <= 128 and a finite rotation angle".into(),
337        ));
338    }
339    let n = index(l, l) + 1;
340    if theta == 0.0 {
341        return Ok(DMatrix::identity(n, n));
342    }
343    let mut generator = DMatrix::zeros(n, n);
344    for m in -l..l {
345        let i = index(l, m);
346        generator[(i, i + 1)] = ladder(l, m);
347        generator[(i + 1, i)] = generator[(i, i + 1)];
348    }
349    let eigen = SymmetricEigen::new(generator);
350    let vectors = eigen.eigenvectors.map(|x| Complex::new(x, 0.0));
351    let mut weighted = vectors.clone();
352    for j in 0..n {
353        // The exact eigenvalues are integers; rounding prevents accumulated phase error.
354        let phase = (-Complex::i() * theta * eigen.eigenvalues[j].round()).exp();
355        for i in 0..n {
356            weighted[(i, j)] *= phase;
357        }
358    }
359    let exponential = product(&weighted, &vectors.transpose());
360    Ok(DMatrix::from_fn(n, n, |i, j| {
361        let power = i32::try_from(i).unwrap_or_default() - i32::try_from(j).unwrap_or_default();
362        ((-Complex::i()).powi(power) * exponential[(i, j)]).re
363    }))
364}
365
366/// One small-d element `d^l_(mk)(theta)` at a jet angle, from the Jacobi recurrence
367/// (DLMF 18.9.1-2) in `O(l)` time and `O(1)` storage. Half-angle powers keep the
368/// off-diagonal limits where `cos(theta)` rounds to one. Symmetries keep both Jacobi
369/// parameters nonnegative.
370fn wigner_small_d_jet<const N: usize>(l: i32, mut m: i32, mut k: i32, theta: Jet<N>) -> Jet<N> {
371    let mut sign = 1.0;
372    if k.abs() > m.abs() {
373        sign *= parity(m - k);
374        std::mem::swap(&mut m, &mut k);
375    }
376    if m < 0 {
377        sign *= parity(m - k);
378        m = -m;
379        k = -k;
380    }
381    let (a, b) = (f64::from(m - k), f64::from(m + k));
382    let x = theta.chain(theta.value.cos(), -theta.value.sin());
383    let half = theta * 0.5;
384    let sine = half.chain(half.value.sin(), half.value.cos());
385    let cosine = half.chain(half.value.cos(), -half.value.sin());
386    let normalization = (0.5
387        * (log_factorial(l + m) + log_factorial(l - m)
388            - log_factorial(l + k)
389            - log_factorial(l - k)))
390    .exp();
391    let n = l - m;
392    let mut prev = Jet::constant(1.0);
393    let mut polynomial = if n == 0 {
394        prev
395    } else {
396        0.5 * ((a - b) + (a + b + 2.0) * x)
397    };
398    for j in 2..=n {
399        let j = f64::from(j);
400        let t = 2.0 * j + a + b;
401        let next = ((t - 1.0) * (t * (t - 2.0) * x + a * a - b * b) * polynomial
402            - 2.0 * (j + a - 1.0) * (j + b - 1.0) * t * prev)
403            * (1.0 / (2.0 * j * (j + a + b) * (t - 2.0)));
404        prev = polynomial;
405        polynomial = next;
406    }
407    sign * parity(m - k) * normalization * sine.powi(m - k) * cosine.powi(m + k) * polynomial
408}
409
410/// Wigner small-d element `d^l_(mk)(theta)` at a real or complex angle, without a matrix.
411/// Orders with `|m| > l` or `|k| > l` give zero.
412///
413/// Upstream: `treams.special.wignersmalld`. Differences: degrees above 128 and
414/// non-finite angles give an error.
415pub fn wigner_small_d(l: i32, m: i32, k: i32, theta: Complex) -> Result<Complex> {
416    if !(0..=MAX_DEGREE).contains(&l) || !finite(theta) {
417        return Err(Error::InvalidInput(
418            // 128 is MAX_DEGREE.
419            "require 0 <= l <= 128 and finite Wigner angle".into(),
420        ));
421    }
422    if m.unsigned_abs() > l.unsigned_abs() || k.unsigned_abs() > l.unsigned_abs() {
423        return Ok(Complex::default());
424    }
425    let value = wigner_small_d_jet(l, m, k, Jet::<0>::constant(theta)).value;
426    if !finite(value) {
427        return Err(Error::SpecialFunction("non-finite Wigner result".into()));
428    }
429    Ok(value)
430}
431
432/// Wigner D element `D^l_(mk)(phi, theta, psi) = e^(-i m phi) d^l_(mk)(theta) e^(-i k psi)`
433/// in the z-y-z convention, allowing complex Euler angles.
434///
435/// Upstream: `treams.special.wignerd`. Differences: as [`wigner_small_d`].
436pub fn wigner_d(l: i32, m: i32, k: i32, angles: [Complex; 3]) -> Result<Complex> {
437    if angles.iter().any(|&z| !finite(z)) {
438        return Err(Error::InvalidInput("Wigner angles must be finite".into()));
439    }
440    let value = (-Complex::i() * (f64::from(m) * angles[0] + f64::from(k) * angles[2])).exp()
441        * wigner_small_d(l, m, k, angles[1])?;
442    if !finite(value) {
443        return Err(Error::SpecialFunction("non-finite Wigner result".into()));
444    }
445    Ok(value)
446}
447
448/// Wigner arrays evaluate in parallel from this many elements.
449const PARALLEL: Parallel = Parallel::AtLeast(1024);
450
451/// What [`wigner_d_array`] saves for its pullback: the labels and the Euler angles. The
452/// pullback applies the local chain rules.
453#[derive(Debug)]
454pub struct WignerDResidual {
455    labels: Vec<[i32; 3]>,
456    angles: [Vec<Complex>; 3],
457    size: usize,
458}
459
460impl WignerDResidual {
461    /// Keep the broadcast inputs needed by derivatives without evaluating values.
462    pub fn new(labels: Vec<[i32; 3]>, angles: [Vec<Complex>; 3]) -> Result<Self> {
463        let [a, b, c] = angles.each_ref().map(Vec::len);
464        let size = broadcast::size(
465            &[labels.len(), a, b, c],
466            "Wigner arrays must have equal lengths or scalar inputs",
467        )?;
468        Ok(Self {
469            labels,
470            angles,
471            size,
472        })
473    }
474
475    fn element(&self, i: usize) -> ([i32; 3], [Complex; 3]) {
476        (
477            broadcast::element(&self.labels, i),
478            self.angles.each_ref().map(|a| broadcast::element(a, i)),
479        )
480    }
481
482    /// Apply one direction in the Euler angles, carrying one jet slot per value.
483    pub fn pushforward(&self, tangents: [&[Complex]; 3]) -> Result<Vec<Complex>> {
484        broadcast::pushforward(
485            tangents,
486            self.size,
487            "Wigner tangents must be finite and broadcast to output",
488            PARALLEL,
489            |i, tangent| {
490                let ([l, m, k], angles) = self.element(i);
491                if tangent == [Complex::default(); 3]
492                    || m.unsigned_abs() > l.unsigned_abs()
493                    || k.unsigned_abs() > l.unsigned_abs()
494                {
495                    return Ok(Complex::default());
496                }
497                let angles: [Jet<1>; 3] = std::array::from_fn(|axis| Jet {
498                    value: angles[axis],
499                    derivative: [tangent[axis]],
500                });
501                Ok(wigner_d_jet(l, m, k, angles)?.derivative[0])
502            },
503        )
504    }
505
506    /// The gradients with respect to the three Euler angles, given the gradient
507    /// `cotangent` with respect to the values. An angle given as one value for all
508    /// elements receives the sum of its element gradients.
509    pub fn pullback(&self, cotangent: &[Complex]) -> Result<[Vec<Complex>; 3]> {
510        broadcast::pullback(
511            cotangent,
512            self.size,
513            "Wigner cotangent must be finite and match output",
514            self.angles.each_ref().map(Vec::len),
515            PARALLEL,
516            |i, &g| {
517                let ([l, m, k], angles) = self.element(i);
518                if g == Complex::default()
519                    || m.unsigned_abs() > l.unsigned_abs()
520                    || k.unsigned_abs() > l.unsigned_abs()
521                {
522                    return Ok([Complex::default(); 3]);
523                }
524                let angles: [Jet<3>; 3] =
525                    std::array::from_fn(|axis| Jet::variable(angles[axis], axis));
526                let value = wigner_d_jet(l, m, k, angles)?;
527                Ok(value.derivative.map(|derivative| g * derivative.conj()))
528            },
529        )
530    }
531}
532
533/// The same Euler-angle chain rule serves both one tangent and three adjoint
534/// partials, with the derivative width selected by the caller.
535fn wigner_d_jet<const N: usize>(l: i32, m: i32, k: i32, angles: [Jet<N>; 3]) -> Result<Jet<N>> {
536    let value = (-Complex::i() * (f64::from(m) * angles[0] + f64::from(k) * angles[2])).exp()
537        * wigner_small_d_jet(l, m, k, angles[1]);
538    if !value.finite() {
539        return Err(Error::SpecialFunction(
540            "non-finite Wigner derivative".into(),
541        ));
542    }
543    Ok(value)
544}
545
546/// Broadcast [`wigner_d`] values with all three Euler angles differentiable: each input
547/// is either one value or one value per element.
548///
549/// Upstream: `treams.special.wignerd` over arrays.
550pub fn wigner_d_array(
551    labels: Vec<[i32; 3]>,
552    angles: [Vec<Complex>; 3],
553) -> Result<(Vec<Complex>, WignerDResidual)> {
554    let residual = WignerDResidual::new(labels, angles)?;
555    let values = broadcast::map(residual.size, PARALLEL, |i| {
556        let ([l, m, k], a) = residual.element(i);
557        wigner_d(l, m, k, a)
558    })?;
559    Ok((values, residual))
560}
561
562#[cfg(test)]
563mod tests {
564    //! 3j rows against single symbols and exact values; the Wigner identities of the
565    //! public functions are in `properties/special.rs`.
566
567    use proptest::{prelude::*, test_runner::TestCaseError};
568
569    use super::{Wigner3jRow, wigner3j};
570    use crate::test_support::ALGEBRA_CASES;
571
572    proptest! {
573        #![proptest_config(ProptestConfig::with_cases(ALGEBRA_CASES))]
574
575        #[test]
576        fn wigner3j_rows_match_single_symbols(
577            (j1, j2, m1, m2) in (0_i32..=90, 0_i32..=90).prop_flat_map(|(j1, j2)| {
578                let order = |j: i32| prop_oneof![Just(j), Just(-j), -j..=j];
579                (Just(j1), Just(j2), order(j1), order(j2))
580            }),
581        ) {
582            check_wigner3j_row(j1, j2, m1, m2)?;
583        }
584    }
585
586    /// A row from one recurrence equals [`wigner3j`] bit for bit at every `j3`, including
587    /// zero just outside the admissible range.
588    fn check_wigner3j_row(j1: i32, j2: i32, m1: i32, m2: i32) -> Result<(), TestCaseError> {
589        let row = Wigner3jRow::new(j1, j2, m1, m2);
590        for j3 in -1..=j1 + j2 + 1 {
591            let single = wigner3j(j1, j2, j3, m1, m2, -m1 - m2);
592            prop_assert_eq!(row.get(j3).to_bits(), single.to_bits(), "j3 = {}", j3);
593        }
594        Ok(())
595    }
596
597    #[test]
598    fn wigner3j_matches_exact_symbols_in_the_forbidden_regions() {
599        // Exact values of the Racah formula in rational arithmetic. The first four lie in
600        // the lower classically forbidden region, where the downward recurrence of treams
601        // errs by 2e-8, 2e-10, 2e-3 and 4e-12. The last three lie in the upper one, where
602        // its upward recurrence below the switch gives -6.91248e-8, -7.8e-10 and -2.8e-9.
603        for (symbol, exact) in [
604            ([47, 60, 39, -47, 8, 39], 1.329_555_988_816_024_3e-11),
605            ([53, 69, 46, -52, 10, 42], -1.230_181_842_452_470_3e-9),
606            ([88, 66, 56, -10, 66, -56], 5.787_402_366_675_233e-17),
607            ([78, 71, 64, 0, 64, -64], -2.792_414_470_253_1e-8),
608            ([128, 64, 95, -128, 64, 64], -6.912_539_260_950_578e-8),
609            ([245, 195, 144, -245, 194, 51], -1.884_797_170_508_145_5e-10),
610            (
611                [260, 130, 190, -260, 129, 131],
612                -1.512_351_282_082_983_2e-12,
613            ),
614        ] {
615            let [j1, j2, j3, m1, m2, m3] = symbol;
616            let value = wigner3j(j1, j2, j3, m1, m2, m3);
617            assert!(
618                (value - exact).abs() <= 1e-12 * exact.abs(),
619                "{symbol:?}: {value:e} != {exact:e}"
620            );
621        }
622    }
623}