use libm::lgamma;
use ndarray::Array2;
use crate::math::complex::Complex;
use crate::types::F;
#[inline]
fn lfact(n: i64) -> F {
lgamma(n as F + 1.0)
}
pub fn wigner_small_d(l: u32, m_prime: i32, m: i32, beta: F) -> F {
let li = l as i32;
if m.unsigned_abs() > l || m_prime.unsigned_abs() > l {
return 0.0;
}
let cos_half = (beta * 0.5).cos();
let sin_half = (beta * 0.5).sin();
let s_min = 0i64.max((m - m_prime) as i64);
let s_max = ((li + m) as i64).min((li - m_prime) as i64);
if s_min > s_max {
return 0.0;
}
let log_prefactor = 0.5
* (lfact((li + m) as i64)
+ lfact((li - m) as i64)
+ lfact((li + m_prime) as i64)
+ lfact((li - m_prime) as i64));
let mut sum: F = 0.0;
for s in s_min..=s_max {
let si = s as i32;
let p_cos = 2 * li + m - m_prime - 2 * si;
let p_sin = m_prime - m + 2 * si;
let log_denom = lfact((li + m - si) as i64)
+ lfact(s)
+ lfact((m_prime - m + si) as i64)
+ lfact((li - m_prime - si) as i64);
let cos_term = if p_cos == 0 {
1.0
} else {
cos_half.powi(p_cos)
};
let sin_term = if p_sin == 0 {
1.0
} else {
sin_half.powi(p_sin)
};
let sign = if (m_prime - m + si).rem_euclid(2) == 0 {
1.0
} else {
-1.0
};
sum += sign * (log_prefactor - log_denom).exp() * cos_term * sin_term;
}
sum
}
pub fn wigner_d_element(l: u32, m_prime: i32, m: i32, alpha: F, beta: F, gamma: F) -> Complex {
let d = wigner_small_d(l, m_prime, m, beta);
let phase = Complex::from_polar(1.0, -(m_prime as F) * alpha - (m as F) * gamma);
phase.scale(d)
}
pub fn wigner_d_matrix(l: u32, alpha: F, beta: F, gamma: F) -> Array2<Complex> {
let n = (2 * l + 1) as usize;
let mut out = Array2::<Complex>::default((n, n));
for m_prime in -(l as i32)..=(l as i32) {
for m in -(l as i32)..=(l as i32) {
let r = (m_prime + l as i32) as usize;
let c = (m + l as i32) as usize;
out[[r, c]] = wigner_d_element(l, m_prime, m, alpha, beta, gamma);
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
use std::f64::consts::{FRAC_PI_2, PI};
const TOL: F = 1e-12;
const COARSE_TOL: F = 1e-9;
fn approx_eq(a: F, b: F, tol: F) {
assert!((a - b).abs() < tol, "expected {b}, got {a} (Δ={})", a - b);
}
fn approx_eq_c(a: Complex, b: Complex, tol: F) {
assert!(
(a.re - b.re).abs() < tol && (a.im - b.im).abs() < tol,
"expected ({}, {}), got ({}, {})",
b.re,
b.im,
a.re,
a.im,
);
}
#[test]
fn d_zero_is_one() {
for &b in &[0.0, 1.0, PI, 0.4] {
approx_eq(wigner_small_d(0, 0, 0, b), 1.0, TOL);
}
}
#[test]
fn d_one_closed_form() {
let b = 0.7;
approx_eq(wigner_small_d(1, 0, 0, b), b.cos(), TOL);
approx_eq(wigner_small_d(1, 1, 1, b), 0.5 * (1.0 + b.cos()), TOL);
approx_eq(wigner_small_d(1, 1, -1, b), 0.5 * (1.0 - b.cos()), TOL);
approx_eq(wigner_small_d(1, 1, 0, b), -b.sin() / 2.0_f64.sqrt(), TOL);
approx_eq(wigner_small_d(1, -1, 1, b), 0.5 * (1.0 - b.cos()), TOL);
}
#[test]
fn d_two_zero_zero() {
let b: F = 1.1;
let expected = 0.5 * (3.0 * b.cos().powi(2) - 1.0);
approx_eq(wigner_small_d(2, 0, 0, b), expected, TOL);
}
#[test]
fn beta_zero_is_identity() {
for l in 0..=4 {
for m_prime in -(l as i32)..=(l as i32) {
for m in -(l as i32)..=(l as i32) {
let expected = if m == m_prime { 1.0 } else { 0.0 };
approx_eq(wigner_small_d(l, m_prime, m, 0.0), expected, COARSE_TOL);
}
}
}
}
#[test]
fn d_negation_symmetry() {
let l = 3;
let b = 0.6;
for m_prime in -(l as i32)..=(l as i32) {
for m in -(l as i32)..=(l as i32) {
let lhs = wigner_small_d(l, m_prime, m, -b);
let rhs = wigner_small_d(l, m, m_prime, b);
approx_eq(lhs, rhs, TOL);
}
}
}
#[test]
fn small_d_row_orthogonal() {
let l = 3;
let b = 0.85;
for m1 in -(l as i32)..=(l as i32) {
for m2 in -(l as i32)..=(l as i32) {
let mut acc: F = 0.0;
for m in -(l as i32)..=(l as i32) {
acc += wigner_small_d(l, m1, m, b) * wigner_small_d(l, m2, m, b);
}
let expected = if m1 == m2 { 1.0 } else { 0.0 };
approx_eq(acc, expected, COARSE_TOL);
}
}
}
#[test]
fn d_matrix_identity_at_origin() {
let l = 2u32;
let d = wigner_d_matrix(l, 0.0, 0.0, 0.0);
let n = (2 * l + 1) as usize;
for r in 0..n {
for c in 0..n {
let expected = if r == c { Complex::ONE } else { Complex::ZERO };
approx_eq_c(d[[r, c]], expected, COARSE_TOL);
}
}
}
#[test]
fn d_element_simple_phase() {
let alpha = 0.4;
let gamma = 0.6;
let beta = FRAC_PI_2;
let l = 1;
let m_prime = 1;
let m = -1;
let d_small = wigner_small_d(l, m_prime, m, beta); let elem = wigner_d_element(l, m_prime, m, alpha, beta, gamma);
let phase = Complex::from_polar(1.0, -(m_prime as F) * alpha - (m as F) * gamma);
approx_eq_c(elem, phase.scale(d_small), TOL);
}
}