1#![allow(clippy::indexing_slicing)] use 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
32fn 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
43fn 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
51fn 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
68fn 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
85fn 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
99fn 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
117fn 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
137fn 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
157fn 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
182fn 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 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 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#[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#[derive(Debug, Default)]
284pub(crate) struct Wigner3jRow {
285 first: i32,
286 values: Vec<f64>,
287}
288
289impl Wigner3jRow {
290 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 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
319pub(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
324pub(crate) fn index(l: i32, m: i32) -> usize {
326 usize::try_from(l + m).unwrap_or_default()
327}
328
329pub(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 "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 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
366fn 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
410pub 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 "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
432pub 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
448const PARALLEL: Parallel = Parallel::AtLeast(1024);
450
451#[derive(Debug)]
454pub struct WignerDResidual {
455 labels: Vec<[i32; 3]>,
456 angles: [Vec<Complex>; 3],
457 size: usize,
458}
459
460impl WignerDResidual {
461 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 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 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
533fn 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
546pub 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 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 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 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}