use num::complex::Complex64;
use crate::utils::wigner::utils::{binomial, const_imax, const_umin};
mod utils {
const MAX_BINOMIAL: u64 = 67;
const SIZE: usize = table_size(MAX_BINOMIAL);
const fn table_size(n: u64) -> usize {
let x = n / 2 + 1;
(x * (x + (n & 1))) as usize
}
const fn index(n: u64, k: u64) -> usize {
let x = n / 2 + 1;
(x * (x - (1 - (n & 1))) + k) as usize
}
pub(crate) const fn const_umin(a: u64, b: u64) -> u64 {
if a < b {
a
} else {
b
}
}
pub(crate) const fn const_imax(a: i64, b: i64) -> i64 {
if a > b {
a
} else {
b
}
}
const fn build_binomial_table() -> [u64; SIZE] {
let mut data = [0u64; SIZE];
data[0] = 1;
let mut n = 1;
while n <= MAX_BINOMIAL {
let mut k = 0;
while k <= n / 2 {
let value = if k == 0 {
1
} else {
let nm1 = n - 1;
let a_k = const_umin(k, nm1 - k);
let km1 = k - 1;
let b_k = const_umin(km1, nm1 - km1);
data[index(nm1, a_k)] + data[index(nm1, b_k)]
};
data[index(n, k)] = value;
k += 1;
}
n += 1;
}
data
}
static BINOMIAL_TABLE: [u64; SIZE] = build_binomial_table();
#[inline]
pub(crate) const fn binomial(n: u64, k: u64) -> u64 {
if n > MAX_BINOMIAL || k > n {
return 0;
}
let k = const_umin(k, n - k);
BINOMIAL_TABLE[index(n, k)]
}
#[cfg(test)]
mod tests {
use super::binomial;
#[test]
fn test_binomial() {
assert_eq!(binomial(0, 0), 1);
assert_eq!(binomial(5, 0), 1);
assert_eq!(binomial(5, 1), 5);
assert_eq!(binomial(5, 2), 10);
assert_eq!(binomial(5, 3), 10);
assert_eq!(binomial(5, 4), 5);
assert_eq!(binomial(5, 5), 1);
assert_eq!(binomial(67, 33), 14_226_520_737_620_288_370);
assert_eq!(binomial(67, 34), 14_226_520_737_620_288_370);
assert_eq!(binomial(68, 1), 0);
assert_eq!(binomial(10, 11), 0);
}
}
}
#[inline]
const fn phase(x: u64) -> i64 {
1 - (2 * (x & 1) as i64)
}
#[inline]
const fn check_parity(dj: i64, dm: i64) -> bool {
(dj ^ dm) & 1 == 0
}
#[inline]
const fn check_jm(dj: i64, dm: i64) -> bool {
check_parity(dj, dm) && (dm.abs() <= dj)
}
#[inline]
const fn check_coupling(dj1: i64, dj2: i64, dj3: i64) -> bool {
(dj1 >= 0)
&& (dj2 >= 0)
&& (dj3 >= (dj1 - dj2).abs())
&& check_parity(dj1 + dj2, dj3)
&& (dj3 <= (dj1 + dj2))
}
pub fn clebsch_gordon(dj1: u64, dj2: u64, dj3: u64, dm1: i64, dm2: i64, dm3: i64) -> f64 {
if !(check_jm(dj1 as i64, dm1) && check_jm(dj2 as i64, dm2) && check_jm(dj3 as i64, dm3)) {
return 0.0;
}
if !check_coupling(dj1 as i64, dj2 as i64, dj3 as i64) {
return 0.0;
}
if dm1 + dm2 != dm3 {
return 0.0;
}
if dm1 == 0 && dm2 == 0 && dm3 == 0 {
let j1 = dj1 / 2;
let j2 = dj2 / 2;
let j3 = dj3 / 2;
let j = j1 + j2 + j3;
let g = j / 2;
return phase(g - j3) as f64 * (binomial(g, j3) * binomial(j3, g - j1)) as f64
/ ((binomial(j + 1, dj3 + 1) * binomial(dj3, j - dj1)) as f64).sqrt();
}
let j = (dj1 + dj2 + dj3) / 2;
let jm1 = j - dj1;
let jm2 = j - dj2;
let jm3 = j - dj3;
let j1mm1 = (dj1 as i64 - dm1) as u64 / 2;
let j2mm2 = (dj2 as i64 - dm2) as u64 / 2;
let j3mm3 = (dj3 as i64 - dm3) as u64 / 2;
let j2pm2 = (dj2 as i64 + dm2) as u64 / 2;
let a = ((binomial(dj1, jm2) * binomial(dj2, jm3)) as f64
/ (binomial(j + 1, jm3)
* binomial(dj1, j1mm1)
* binomial(dj2, j2mm2)
* binomial(dj3, j3mm3)) as f64)
.sqrt();
let mut b: i64 = 0;
let k_min = const_imax(
0,
const_imax(j1mm1 as i64 - jm2 as i64, j2pm2 as i64 - jm1 as i64),
) as u64;
let k_max = const_umin(jm3, const_umin(j1mm1, j2pm2));
for z in k_min..=k_max {
b = -b + (binomial(jm3, z) * binomial(jm2, j1mm1 - z) * binomial(jm1, j2pm2 - z)) as i64;
}
a * (phase(k_max) * b) as f64
}
pub fn wigner_3j(dj1: u64, dj2: u64, dj3: u64, dm1: i64, dm2: i64, dm3: i64) -> f64 {
if !(check_jm(dj1 as i64, dm1) && check_jm(dj2 as i64, dm2) && check_jm(dj3 as i64, dm3)) {
return 0.0;
}
if !check_coupling(dj1 as i64, dj2 as i64, dj3 as i64) {
return 0.0;
}
if dm1 + dm2 + dm3 != 0 {
return 0.0;
}
let j = (dj1 + dj2 + dj3) / 2;
let jm1 = j - dj1;
let jm2 = j - dj2;
let jm3 = j - dj3;
let j1mm1 = (dj1 as i64 - dm1) as u64 / 2;
let j2mm2 = (dj2 as i64 - dm2) as u64 / 2;
let j3mm3 = (dj3 as i64 - dm3) as u64 / 2;
let j1pm1 = (dj1 as i64 + dm1) as u64 / 2;
let a = ((binomial(dj1, jm2) * binomial(dj2, jm1)) as f64
/ ((j + 1)
* binomial(j, jm3)
* binomial(dj1, j1mm1)
* binomial(dj2, j2mm2)
* binomial(dj3, j3mm3)) as f64)
.sqrt();
let mut b: i64 = 0;
let k_min = const_imax(
0,
const_imax(j1pm1 as i64 - jm2 as i64, j2mm2 as i64 - jm1 as i64),
) as u64;
let k_max = const_umin(jm3, const_umin(j1pm1, j2mm2));
for z in k_min..=k_max {
b = -b + (binomial(jm3, z) * binomial(jm2, j1pm1 - z) * binomial(jm1, j2mm2 - z)) as i64;
}
a * (phase(dj1 + (dj3 as i64 + dm3) as u64 / 2 + k_max) * b) as f64
}
pub struct WignerDMatrix {
dj: i64, dmp: i64, dm: i64, jpm: i64, jmmp: i64, delta: i64, s_min: i64,
s_max: i64,
p_c0: i32, p_s0: i32, amp0: f64, sign0: f64, }
impl WignerDMatrix {
pub fn new(dj: u64, dmp: i64, dm: i64) -> Self {
let dj = dj as i64;
assert!(
dmp.abs() <= dj,
"|m'| > j is not allowed! (2*j = {}, 2*m' = {})",
dj,
dmp
);
assert!(
dm.abs() <= dj,
"|m| > j is not allowed! (2*j = {}, 2*m = {})",
dj,
dm
);
assert!(
check_parity(dj, dmp),
"j and m' must either both be integers or both be half-integers! (2*j = {}, 2*m' = {})",
dj,
dmp
);
assert!(
check_parity(dj, dm),
"j and m must either both be integers or both be half-integers! (2*j = {}, 2*m = {})",
dj,
dm
);
let jpmp = (dj + dmp) / 2;
let jmmp = (dj - dmp) / 2;
let jpm = (dj + dm) / 2;
let jmm = (dj - dm) / 2;
let delta = (dmp - dm) / 2;
let s_min = 0.max(-delta);
let s_max = jpm.min(jmmp);
assert!(
s_min <= s_max,
"summation bounds are incorrect (this shouldn't happen)!"
);
let mut ln_factorial = vec![0.0; dj as usize + 1];
for i in 1..=dj as usize {
ln_factorial[i] = ln_factorial[i - 1] + (i as f64).ln();
}
let ln_prefactor = 0.5
* (ln_factorial[jpmp as usize]
+ ln_factorial[jmmp as usize]
+ ln_factorial[jpm as usize]
+ ln_factorial[jmm as usize]);
let denom_ln_s_min = ln_factorial[(jpm - s_min) as usize]
+ ln_factorial[s_min as usize]
+ ln_factorial[(delta + s_min) as usize]
+ ln_factorial[(jmmp - s_min) as usize];
let p_c0 = (dj - delta - 2 * s_min) as i32;
let p_s0 = (delta + 2 * s_min) as i32;
let amp0 = (ln_prefactor - denom_ln_s_min).exp();
let sign0 = if ((s_min + delta) & 1) == 0 {
1.0
} else {
-1.0
};
Self {
dj,
dmp,
dm,
jpm,
jmmp,
delta,
s_min,
s_max,
p_c0,
p_s0,
amp0,
sign0,
}
}
#[inline(always)]
fn d_half(&self, ch: f64, sh: f64) -> f64 {
if sh.abs() < f64::EPSILON {
return if self.dmp == self.dm { 1.0 } else { 0.0 };
}
if ch.abs() < f64::EPSILON {
if self.dmp != -self.dm {
return 0.0;
}
return if (((self.dj + self.dm) / 2) & 1) == 0 {
1.0
} else {
-1.0
};
}
let ratio = (sh * sh) / (ch * ch);
let mut term = self.sign0 * self.amp0 * ch.powi(self.p_c0) * sh.powi(self.p_s0);
let mut sum = term;
for s in self.s_min..self.s_max {
let num = (self.jpm - s) as f64 * (self.jmmp - s) as f64;
let den = (s + 1) as f64 * (self.delta + s + 1) as f64;
term *= -num / den * ratio;
sum += term;
}
sum
}
#[inline(always)]
pub fn d(&self, beta: f64) -> f64 {
let h = 0.5 * beta;
self.d_half(h.cos(), h.sin())
}
#[inline(always)]
#[allow(non_snake_case)]
pub fn D(&self, alpha: f64, beta: f64, gamma: f64) -> Complex64 {
let d = self.d(beta);
let phi = -0.5 * ((self.dmp as f64) * alpha + (self.dm as f64) * gamma);
Complex64::new(phi.cos() * d, phi.sin() * d)
}
}
#[cfg(test)]
mod tests {
use approx::assert_relative_eq;
use num::complex::Complex64;
use std::f64::consts::{FRAC_1_SQRT_2, FRAC_PI_2, PI};
use super::*;
#[test]
fn test_phase() {
assert_eq!(phase(0), 1);
assert_eq!(phase(1), -1);
assert_eq!(phase(2), 1);
assert_eq!(phase(3), -1);
}
#[test]
fn singlet_triplet_for_two_spin_half() {
assert_relative_eq!(clebsch_gordon(1, 1, 2, 1, 1, 2), 1.0);
assert_relative_eq!(clebsch_gordon(1, 1, 2, 1, -1, 0), FRAC_1_SQRT_2);
assert_relative_eq!(clebsch_gordon(1, 1, 2, -1, 1, 0), FRAC_1_SQRT_2);
assert_relative_eq!(clebsch_gordon(1, 1, 0, 1, -1, 0), FRAC_1_SQRT_2);
assert_relative_eq!(clebsch_gordon(1, 1, 0, -1, 1, 0), -FRAC_1_SQRT_2);
}
#[test]
fn highest_weight_state_is_one() {
assert_relative_eq!(clebsch_gordon(2, 2, 4, 2, 2, 4), 1.0);
}
#[test]
fn known_spin_one_couplings() {
assert_relative_eq!(clebsch_gordon(2, 2, 4, 2, 0, 2), FRAC_1_SQRT_2);
assert_relative_eq!(clebsch_gordon(2, 2, 4, 0, 0, 0), (2.0 / 3.0_f64).sqrt());
assert_relative_eq!(clebsch_gordon(2, 2, 0, 0, 0, 0), -1.0 / 3.0_f64.sqrt());
}
#[test]
fn zero_when_m_sum_fails() {
assert_eq!(clebsch_gordon(1, 1, 2, 1, 1, 0), 0.0);
}
#[test]
fn zero_when_triangle_rule_fails() {
assert_eq!(clebsch_gordon(1, 1, 4, 1, 1, 2), 0.0);
}
#[test]
fn zero_when_m_out_of_range() {
assert_eq!(clebsch_gordon(1, 1, 2, 3, -1, 2), 0.0);
}
#[test]
fn normalization_for_fixed_jm() {
let c1 = clebsch_gordon(1, 1, 2, 1, -1, 0);
let c2 = clebsch_gordon(1, 1, 2, -1, 1, 0);
assert_relative_eq!(c1 * c1 + c2 * c2, 1.0);
}
#[test]
fn normalization_for_singlet() {
let c1 = clebsch_gordon(1, 1, 0, 1, -1, 0);
let c2 = clebsch_gordon(1, 1, 0, -1, 1, 0);
assert_relative_eq!(c1 * c1 + c2 * c2, 1.0);
}
#[test]
fn two_spin_half_cases() {
assert_relative_eq!(wigner_3j(1, 1, 2, 1, 1, -2), -1.0 / 3.0_f64.sqrt());
assert_relative_eq!(wigner_3j(1, 1, 0, 1, -1, 0), FRAC_1_SQRT_2);
assert_relative_eq!(wigner_3j(1, 1, 0, -1, 1, 0), -FRAC_1_SQRT_2);
}
#[test]
fn spin_one_cases() {
assert_relative_eq!(wigner_3j(2, 2, 0, 0, 0, 0), -1.0 / 3.0_f64.sqrt());
assert_relative_eq!(wigner_3j(2, 2, 4, 2, -2, 0), 1.0 / 30.0_f64.sqrt());
assert_relative_eq!(wigner_3j(2, 2, 4, 0, 0, 0), (2.0 / 15.0_f64).sqrt());
}
#[test]
fn selection_rule_failures_return_zero() {
assert_eq!(wigner_3j(1, 1, 0, 1, -1, 1), 0.0);
assert_eq!(wigner_3j(1, 1, 4, 1, -1, 0), 0.0);
assert_eq!(wigner_3j(1, 1, 0, 3, -1, -2), 0.0);
}
#[test]
fn odd_j_sum_with_all_zero_ms_vanishes() {
assert_eq!(wigner_3j(2, 2, 2, 0, 0, 0), 0.0);
}
#[test]
fn column_swap_symmetry_even_case() {
let a = wigner_3j(2, 2, 4, 2, -2, 0);
let b = wigner_3j(2, 2, 4, -2, 2, 0);
assert_relative_eq!(a, b);
}
#[test]
fn column_swap_symmetry_odd_case() {
let a = wigner_3j(1, 1, 0, 1, -1, 0);
let b = wigner_3j(1, 1, 0, -1, 1, 0);
assert_relative_eq!(a, -b);
}
#[test]
fn sign_flip_symmetry() {
let a = wigner_3j(1, 1, 0, 1, -1, 0);
let b = wigner_3j(1, 1, 0, -1, 1, 0);
assert_relative_eq!(b, -a);
let c = wigner_3j(2, 2, 4, 2, -2, 0);
let d = wigner_3j(2, 2, 4, -2, 2, 0);
assert_relative_eq!(d, c);
}
#[test]
fn relation_to_clebsch_gordon_examples() {
let cg = clebsch_gordon(1, 1, 0, 1, -1, 0);
let w3j = wigner_3j(1, 1, 0, 1, -1, 0);
assert_relative_eq!(w3j, cg);
let cg = clebsch_gordon(2, 2, 4, 2, -2, 0);
let expected = cg / 5.0_f64.sqrt(); let w3j = wigner_3j(2, 2, 4, 2, -2, 0);
assert_relative_eq!(w3j, expected);
}
#[test]
fn construct_integer_case() {
let _ = WignerDMatrix::new(2, 2, 0); }
#[test]
fn construct_half_integer_case() {
let _ = WignerDMatrix::new(1, 1, -1); }
#[test]
#[should_panic]
fn panic_when_mp_out_of_range() {
let _ = WignerDMatrix::new(2, 4, 0);
}
#[test]
#[should_panic]
fn panic_when_m_out_of_range() {
let _ = WignerDMatrix::new(2, 0, 4);
}
#[test]
#[should_panic]
fn panic_when_parity_mismatch_mp() {
let _ = WignerDMatrix::new(2, 1, 0);
}
#[test]
#[should_panic]
fn panic_when_parity_mismatch_m() {
let _ = WignerDMatrix::new(2, 0, 1);
}
#[test]
fn d_beta_zero_is_identity_integer_j() {
let vals = [-2, 0, 2];
for &mp in &vals {
for &m in &vals {
let w = WignerDMatrix::new(2, mp, m);
let expected = if mp == m { 1.0 } else { 0.0 };
assert_relative_eq!(w.d(0.0), expected);
}
}
}
#[test]
fn d_beta_zero_is_identity_half_integer_j() {
let vals = [-1, 1];
for &mp in &vals {
for &m in &vals {
let w = WignerDMatrix::new(1, mp, m);
let expected = if mp == m { 1.0 } else { 0.0 };
assert_relative_eq!(w.d(0.0), expected);
}
}
}
#[test]
fn d_beta_pi_selection_rule_j_one() {
let vals = [-2, 0, 2];
for &mp in &vals {
for &m in &vals {
let w = WignerDMatrix::new(2, mp, m);
let expected = if mp == -m {
let jm = (2 + m) / 2; if (jm & 1) == 0 {
1.0
} else {
-1.0
}
} else {
0.0
};
assert_relative_eq!(w.d(PI), expected);
}
}
}
#[test]
fn d_j_half_closed_forms() {
let beta = 0.73;
let c = f64::cos(beta / 2.0);
let s = f64::sin(beta / 2.0);
let w_pp = WignerDMatrix::new(1, 1, 1);
let w_pm = WignerDMatrix::new(1, 1, -1);
let w_mp = WignerDMatrix::new(1, -1, 1);
let w_mm = WignerDMatrix::new(1, -1, -1);
assert_relative_eq!(w_pp.d(beta), c);
assert_relative_eq!(w_pm.d(beta), -s);
assert_relative_eq!(w_mp.d(beta), s);
assert_relative_eq!(w_mm.d(beta), c);
}
#[test]
fn d_j_one_closed_forms() {
let beta = 1.1;
let cb = f64::cos(beta);
let sb = f64::sin(beta);
let w_11 = WignerDMatrix::new(2, 2, 2);
let w_10 = WignerDMatrix::new(2, 2, 0);
let w_1m1 = WignerDMatrix::new(2, 2, -2);
let w_00 = WignerDMatrix::new(2, 0, 0);
let w_01 = WignerDMatrix::new(2, 0, 2);
let w_0m1 = WignerDMatrix::new(2, 0, -2);
let w_m11 = WignerDMatrix::new(2, -2, 2);
let w_m10 = WignerDMatrix::new(2, -2, 0);
let w_m1m1 = WignerDMatrix::new(2, -2, -2);
assert_relative_eq!(w_11.d(beta), 0.5 * (1.0 + cb));
assert_relative_eq!(w_10.d(beta), -FRAC_1_SQRT_2 * sb);
assert_relative_eq!(w_1m1.d(beta), 0.5 * (1.0 - cb));
assert_relative_eq!(w_01.d(beta), FRAC_1_SQRT_2 * sb);
assert_relative_eq!(w_00.d(beta), cb);
assert_relative_eq!(w_0m1.d(beta), -FRAC_1_SQRT_2 * sb);
assert_relative_eq!(w_m11.d(beta), 0.5 * (1.0 - cb));
assert_relative_eq!(w_m10.d(beta), FRAC_1_SQRT_2 * sb);
assert_relative_eq!(w_m1m1.d(beta), 0.5 * (1.0 + cb));
}
#[test]
fn d_j_one_special_value() {
let w = WignerDMatrix::new(2, 2, 0);
assert_relative_eq!(w.d(FRAC_PI_2), -FRAC_1_SQRT_2);
}
#[test]
fn full_d_matches_phase_definition() {
let alpha = 0.31;
let beta = 0.82;
let gamma = -0.47;
let w = WignerDMatrix::new(3, 1, -1); let d = w.d(beta);
let expected_phase = Complex64::cis(-0.5 * (alpha - gamma));
let expected = expected_phase * d;
let val = w.D(alpha, beta, gamma);
assert_relative_eq!(val.re, expected.re);
assert_relative_eq!(val.im, expected.im);
}
#[test]
fn full_d_has_no_gamma_dependence_when_m_zero() {
let w = WignerDMatrix::new(2, 2, 0); let a = w.D(0.2, 0.7, 0.0);
let b = w.D(0.2, 0.7, 1.3);
assert_relative_eq!(a.re, b.re);
assert_relative_eq!(a.im, b.im);
}
#[test]
fn full_d_has_no_alpha_dependence_when_mp_zero() {
let w = WignerDMatrix::new(2, 0, 2); let a = w.D(0.0, 0.7, 0.2);
let b = w.D(1.3, 0.7, 0.2);
assert_relative_eq!(a.re, b.re);
assert_relative_eq!(a.im, b.im);
}
#[test]
fn d_symmetry_minus_indices() {
let beta = 0.91;
let w1 = WignerDMatrix::new(4, 2, -2); let w2 = WignerDMatrix::new(4, -2, 2);
let lhs = w1.d(beta);
let rhs = w2.d(beta);
assert_relative_eq!(lhs, rhs);
}
#[test]
fn d_symmetry_transpose_relation() {
let beta = 0.64;
let w1 = WignerDMatrix::new(3, 1, -1); let w2 = WignerDMatrix::new(3, -1, 1);
let lhs = w1.d(beta);
let rhs = w2.d(beta);
assert_relative_eq!(lhs, -rhs);
}
#[test]
fn d_is_real_for_real_beta() {
let w = WignerDMatrix::new(6, 2, -4); let d = w.d(0.37);
assert!(d.is_finite());
}
#[test]
fn full_d_magnitude_equals_abs_small_d() {
let w = WignerDMatrix::new(5, 1, -3); let d = w.d(1.23).abs();
#[allow(non_snake_case)]
let D = w.D(0.4, 1.23, -0.9).norm();
assert_relative_eq!(D, d);
}
}