use crate::error::{SpecialError, SpecialResult};
const LOG_FACT_MAX: usize = 300;
fn build_log_factorial_table() -> Vec<f64> {
let mut table = Vec::with_capacity(LOG_FACT_MAX + 1);
table.push(0.0); for n in 1..=LOG_FACT_MAX {
let prev = *table.last().expect("non-empty");
table.push(prev + (n as f64).ln());
}
table
}
thread_local! {
static LOG_FACT_TABLE: Vec<f64> = build_log_factorial_table();
}
pub fn log_factorial(n: usize) -> f64 {
if n <= LOG_FACT_MAX {
LOG_FACT_TABLE.with(|t| t[n])
} else {
let nf = n as f64;
nf * nf.ln() - nf + 0.5 * (2.0 * std::f64::consts::PI * nf).ln()
}
}
fn log_fact_signed(n: i64) -> Option<f64> {
if n < 0 {
None } else {
Some(log_factorial(n as usize))
}
}
pub fn triangle_condition(a: f64, b: f64, c: f64) -> bool {
let diff = (a - b).abs();
let sum = a + b;
c >= diff - 1e-10 && c <= sum + 1e-10
}
fn is_half_integer(x: f64) -> bool {
let twice = (2.0 * x).round();
(2.0 * x - twice).abs() < 1e-10
}
fn twice(x: f64) -> i64 {
(2.0 * x).round() as i64
}
pub fn clebsch_gordan(j1: f64, m1: f64, j2: f64, m2: f64, j: f64, m: f64) -> f64 {
if !is_half_integer(j1)
|| !is_half_integer(m1)
|| !is_half_integer(j2)
|| !is_half_integer(m2)
|| !is_half_integer(j)
|| !is_half_integer(m)
{
return 0.0;
}
if j1 < 0.0 || j2 < 0.0 || j < 0.0 {
return 0.0;
}
if (m - m1 - m2).abs() > 1e-10 {
return 0.0;
}
if m1.abs() > j1 + 1e-10 || m2.abs() > j2 + 1e-10 || m.abs() > j + 1e-10 {
return 0.0;
}
if !triangle_condition(j1, j2, j) {
return 0.0;
}
let j1_2 = twice(j1);
let m1_2 = twice(m1);
let j2_2 = twice(j2);
let m2_2 = twice(m2);
let j_2 = twice(j);
let m_2 = twice(m);
if (j1_2 + j2_2 + j_2) % 2 != 0 {
return 0.0;
}
let a = (j1_2 + j2_2 - j_2) / 2;
let b = (j1_2 - j2_2 + j_2) / 2;
let c = (-j1_2 + j2_2 + j_2) / 2;
let d = (j1_2 + j2_2 + j_2) / 2 + 1;
if a < 0 || b < 0 || c < 0 || d < 0 {
return 0.0;
}
let log_delta =
log_factorial(a as usize) + log_factorial(b as usize) + log_factorial(c as usize)
- log_factorial(d as usize);
let jp_m = (j_2 + m_2) / 2;
let jm_m = (j_2 - m_2) / 2;
let j1p_m1 = (j1_2 + m1_2) / 2;
let j1m_m1 = (j1_2 - m1_2) / 2;
let j2p_m2 = (j2_2 + m2_2) / 2;
let j2m_m2 = (j2_2 - m2_2) / 2;
if jp_m < 0 || jm_m < 0 || j1p_m1 < 0 || j1m_m1 < 0 || j2p_m2 < 0 || j2m_m2 < 0 {
return 0.0;
}
let log_sqrt_factor = log_factorial(jp_m as usize)
+ log_factorial(jm_m as usize)
+ log_factorial(j1p_m1 as usize)
+ log_factorial(j1m_m1 as usize)
+ log_factorial(j2p_m2 as usize)
+ log_factorial(j2m_m2 as usize);
let log_2jp1 = ((j_2 + 1) as f64).ln();
let e = (j_2 - j2_2 + m1_2) / 2; let f_val = (j_2 - j1_2 - m2_2) / 2;
let k_min = (-e).max(-f_val).max(0i64);
let k_max = a.min(j1m_m1).min(j2p_m2);
if k_min > k_max {
return 0.0;
}
let mut sum = 0.0f64;
for k in k_min..=k_max {
let ek = e + k;
let fk = f_val + k;
if ek < 0 || fk < 0 {
continue;
}
let log_denom = log_factorial(k as usize)
+ log_factorial((a - k) as usize)
+ log_factorial((j1m_m1 - k) as usize)
+ log_factorial((j2p_m2 - k) as usize)
+ log_factorial(ek as usize)
+ log_factorial(fk as usize);
let log_term = -log_denom;
let term = log_term.exp();
let sign: f64 = if k % 2 == 0 { 1.0 } else { -1.0 };
sum += sign * term;
}
if sum == 0.0 {
return 0.0;
}
let log_abs_cg = 0.5 * log_2jp1 + 0.5 * log_delta + 0.5 * log_sqrt_factor + sum.abs().ln();
let sign_cg = if sum > 0.0 { 1.0 } else { -1.0 };
sign_cg * log_abs_cg.exp()
}
pub fn wigner_3j(j1: f64, m1: f64, j2: f64, m2: f64, j3: f64, m3: f64) -> f64 {
if (m1 + m2 + m3).abs() > 1e-10 {
return 0.0;
}
let cg = clebsch_gordan(j1, m1, j2, m2, j3, -m3);
if cg == 0.0 {
return 0.0;
}
let phase_exp = twice(j1) - twice(j2) + twice(m3);
let phase: f64 = if phase_exp % 2 == 0 { 1.0 } else { -1.0 };
let norm = (2.0 * j3 + 1.0).sqrt();
if norm < 1e-15 {
return 0.0;
}
phase * cg / norm
}
pub fn wigner_6j(j1: f64, j2: f64, j3: f64, j4: f64, j5: f64, j6: f64) -> f64 {
if !triangle_condition(j1, j2, j3)
|| !triangle_condition(j1, j5, j6)
|| !triangle_condition(j4, j2, j6)
|| !triangle_condition(j4, j5, j3)
{
return 0.0;
}
for &x in &[j1, j2, j3, j4, j5, j6] {
if !is_half_integer(x) || x < 0.0 {
return 0.0;
}
}
let log_delta = |a: f64, b: f64, c: f64| -> Option<f64> {
let a2 = twice(a);
let b2 = twice(b);
let c2 = twice(c);
let p = (a2 + b2 - c2) / 2;
let q = (a2 - b2 + c2) / 2;
let r = (-a2 + b2 + c2) / 2;
let s = (a2 + b2 + c2) / 2 + 1;
if p < 0 || q < 0 || r < 0 || s < 0 {
return None;
}
Some(
log_factorial(p as usize) + log_factorial(q as usize) + log_factorial(r as usize)
- log_factorial(s as usize),
)
};
let log_d1 = match log_delta(j1, j2, j3) {
Some(v) => v,
None => return 0.0,
};
let log_d2 = match log_delta(j1, j5, j6) {
Some(v) => v,
None => return 0.0,
};
let log_d3 = match log_delta(j4, j2, j6) {
Some(v) => v,
None => return 0.0,
};
let log_d4 = match log_delta(j4, j5, j3) {
Some(v) => v,
None => return 0.0,
};
let log_delta_total = log_d1 + log_d2 + log_d3 + log_d4;
let j1_2 = twice(j1);
let j2_2 = twice(j2);
let j3_2 = twice(j3);
let j4_2 = twice(j4);
let j5_2 = twice(j5);
let j6_2 = twice(j6);
let a_2 = (j1_2 + j2_2 + j3_2) / 2;
let b_2 = (j1_2 + j5_2 + j6_2) / 2;
let c_2 = (j4_2 + j2_2 + j6_2) / 2;
let d_2 = (j4_2 + j5_2 + j3_2) / 2;
let e_2 = (j1_2 + j2_2 + j4_2 + j5_2) / 2;
let f_2 = (j2_2 + j3_2 + j5_2 + j6_2) / 2;
let g_2 = (j1_2 + j3_2 + j4_2 + j6_2) / 2;
let t_min = a_2.max(b_2).max(c_2).max(d_2);
let t_max = e_2.min(f_2).min(g_2);
if t_min > t_max {
return 0.0;
}
let mut sum = 0.0f64;
for t in t_min..=t_max {
let ta = t - a_2;
let tb = t - b_2;
let tc = t - c_2;
let td = t - d_2;
let te = e_2 - t;
let tf = f_2 - t;
let tg = g_2 - t;
if ta < 0 || tb < 0 || tc < 0 || td < 0 || te < 0 || tf < 0 || tg < 0 {
continue;
}
let log_num = log_factorial((t + 1) as usize);
let log_den = log_factorial(ta as usize)
+ log_factorial(tb as usize)
+ log_factorial(tc as usize)
+ log_factorial(td as usize)
+ log_factorial(te as usize)
+ log_factorial(tf as usize)
+ log_factorial(tg as usize);
let log_term = log_num - log_den;
let term = log_term.exp();
let sign: f64 = if t % 2 == 0 { 1.0 } else { -1.0 };
sum += sign * term;
}
if sum == 0.0 {
return 0.0;
}
let log_prefactor = 0.5 * log_delta_total;
let sign: f64 = if sum > 0.0 { 1.0 } else { -1.0 };
sign * (log_prefactor + sum.abs().ln()).exp()
}
pub fn cg_table(j1: f64, j2: f64) -> SpecialResult<Vec<Vec<Vec<f64>>>> {
if !is_half_integer(j1) || !is_half_integer(j2) || j1 < 0.0 || j2 < 0.0 {
return Err(SpecialError::ValueError(format!(
"j1={j1}, j2={j2} must be non-negative half-integers"
)));
}
let n1 = twice(j1) as usize + 1; let n2 = twice(j2) as usize + 1;
let j_min = (j1 - j2).abs();
let j_max = j1 + j2;
let n_j = ((j_max - j_min).round() as usize) + 1;
let mut table: Vec<Vec<Vec<f64>>> = vec![vec![vec![0.0; n_j]; n2]; n1];
for m1_i in 0..n1 {
let m1 = -j1 + m1_i as f64;
for m2_i in 0..n2 {
let m2 = -j2 + m2_i as f64;
for j_i in 0..n_j {
let j = j_min + j_i as f64;
let m = m1 + m2;
if m.abs() > j + 1e-10 {
table[m1_i][m2_i][j_i] = 0.0;
} else {
table[m1_i][m2_i][j_i] = clebsch_gordan(j1, m1, j2, m2, j, m);
}
}
}
}
Ok(table)
}
#[cfg(test)]
mod tests {
use super::*;
use std::f64::consts::SQRT_2;
#[test]
fn test_triangle_condition_basic() {
assert!(triangle_condition(1.0, 1.0, 1.0));
assert!(triangle_condition(1.0, 1.0, 2.0));
assert!(triangle_condition(1.0, 1.0, 0.0));
assert!(!triangle_condition(2.0, 0.0, 1.0));
assert!(!triangle_condition(1.0, 1.0, 3.0));
}
#[test]
fn test_triangle_condition_half_integers() {
assert!(triangle_condition(0.5, 0.5, 1.0));
assert!(triangle_condition(0.5, 0.5, 0.0));
assert!(!triangle_condition(0.5, 0.5, 2.0));
}
#[test]
fn test_cg_half_half_to_1() {
let cg = clebsch_gordan(0.5, 0.5, 0.5, -0.5, 1.0, 0.0);
let expected = 1.0 / SQRT_2;
assert!(
(cg - expected).abs() < 1e-6,
"CG(1/2,1/2;1/2,-1/2|1,0) = {cg}, expected {expected}"
);
}
#[test]
fn test_cg_half_half_maximum() {
let cg = clebsch_gordan(0.5, 0.5, 0.5, 0.5, 1.0, 1.0);
assert!(
(cg - 1.0).abs() < 1e-6,
"CG(1/2,1/2;1/2,1/2|1,1) = {cg}, expected 1"
);
}
#[test]
fn test_cg_half_half_to_0() {
let cg = clebsch_gordan(0.5, 0.5, 0.5, -0.5, 0.0, 0.0);
assert!(
(cg.abs() - 1.0 / SQRT_2).abs() < 1e-6,
"|CG(1/2,1/2;1/2,-1/2|0,0)| = {}, expected {}",
cg.abs(),
1.0 / SQRT_2
);
}
#[test]
fn test_cg_selection_rule_m_sum() {
let cg = clebsch_gordan(1.0, 1.0, 1.0, 0.0, 2.0, 0.0);
assert!(cg.abs() < 1e-10, "CG with m1+m2≠M should be 0, got {cg}");
}
#[test]
fn test_cg_triangle_violation() {
let cg = clebsch_gordan(1.0, 0.0, 1.0, 0.0, 3.0, 0.0);
assert!(
cg.abs() < 1e-10,
"CG violating triangle should be 0, got {cg}"
);
}
#[test]
fn test_cg_integer_spins_1_1_to_2() {
let cg = clebsch_gordan(1.0, 1.0, 1.0, 1.0, 2.0, 2.0);
assert!(
(cg - 1.0).abs() < 1e-6,
"CG(1,1;1,1|2,2) = {cg}, expected 1"
);
}
#[test]
fn test_cg_orthonormality() {
let j1 = 0.5;
let j2 = 0.5;
let m1 = 0.5;
let m2 = -0.5;
let m = m1 + m2;
let cg1 = clebsch_gordan(j1, m1, j2, m2, 1.0, m);
let cg0 = clebsch_gordan(j1, m1, j2, m2, 0.0, m);
let sum_sq = cg1 * cg1 + cg0 * cg0;
assert!(
(sum_sq - 1.0).abs() < 1e-6,
"Orthonormality: Σ|CG|² = {sum_sq}, expected 1"
);
}
#[test]
fn test_wigner_3j_basic() {
let w3j = wigner_3j(0.5, 0.5, 0.5, -0.5, 1.0, 0.0);
assert!(w3j.abs() > 0.0, "3j symbol should be non-zero: {w3j}");
}
#[test]
fn test_wigner_3j_selection_rule() {
let w3j = wigner_3j(1.0, 1.0, 1.0, 0.0, 1.0, 0.0);
assert!(w3j.abs() < 1e-10, "3j with m_sum≠0 should be 0, got {w3j}");
}
#[test]
fn test_wigner_6j_triangle_violation() {
let w6j = wigner_6j(1.0, 1.0, 3.0, 1.0, 1.0, 1.0);
assert!(
w6j.abs() < 1e-10,
"6j with triangle violation should be 0, got {w6j}"
);
}
#[test]
fn test_wigner_6j_basic() {
let w6j = wigner_6j(0.5, 0.5, 1.0, 0.5, 0.5, 1.0);
assert!(
w6j.abs() > 0.0,
"6j symbol for valid quantum numbers should be non-zero: {w6j}"
);
}
#[test]
fn test_log_factorial_small() {
let lf5 = log_factorial(5);
let expected = 120.0f64.ln();
assert!(
(lf5 - expected).abs() < 1e-10,
"log_factorial(5) = {lf5}, expected {expected}"
);
}
#[test]
fn test_log_factorial_zero() {
assert_eq!(log_factorial(0), 0.0);
}
#[test]
fn test_cg_table_half_half() {
let table = cg_table(0.5, 0.5).expect("cg_table");
assert_eq!(table.len(), 2); assert_eq!(table[0].len(), 2); assert_eq!(table[0][0].len(), 2);
let cg_11 = table[1][1][1]; assert!(
(cg_11 - 1.0).abs() < 1e-6,
"CG table[1][1][1] = {cg_11}, expected 1"
);
}
#[test]
fn test_is_half_integer() {
assert!(is_half_integer(0.0));
assert!(is_half_integer(0.5));
assert!(is_half_integer(1.0));
assert!(is_half_integer(1.5));
assert!(!is_half_integer(0.3));
assert!(!is_half_integer(1.2));
}
#[test]
fn test_cg_normalization() {
let j1 = 0.5;
let j2 = 0.5;
let j = 1.0;
let cg_m1 = clebsch_gordan(j1, 0.5, j2, 0.5, j, 1.0);
assert!(
(cg_m1 * cg_m1 - 1.0).abs() < 1e-6,
"Norm for M=1: {}, expected 1",
cg_m1 * cg_m1
);
let cg_a = clebsch_gordan(j1, 0.5, j2, -0.5, j, 0.0);
let cg_b = clebsch_gordan(j1, -0.5, j2, 0.5, j, 0.0);
let norm_m0 = cg_a * cg_a + cg_b * cg_b;
assert!(
(norm_m0 - 1.0).abs() < 1e-6,
"Norm for M=0: {norm_m0}, expected 1"
);
}
#[test]
fn test_cg_orthogonality() {
let j1 = 0.5;
let j2 = 0.5;
let mut cross = 0.0f64;
for &m1 in &[-0.5f64, 0.5] {
for &m2 in &[-0.5f64, 0.5] {
let m = m1 + m2;
let cg0 = clebsch_gordan(j1, m1, j2, m2, 0.0, m);
let cg1 = clebsch_gordan(j1, m1, j2, m2, 1.0, m);
cross += cg0 * cg1;
}
}
assert!(
cross.abs() < 1e-6,
"Orthogonality: Σ CG(J=0)*CG(J=1) = {cross}, expected 0"
);
}
#[test]
fn test_cg_half_half_singlet() {
let cg = clebsch_gordan(0.5, 0.5, 0.5, -0.5, 0.0, 0.0);
let expected_abs = 1.0 / SQRT_2;
assert!(
(cg.abs() - expected_abs).abs() < 1e-6,
"|CG(1/2,1/2;1/2,-1/2|0,0)| = {}, expected {}",
cg.abs(),
expected_abs
);
}
#[test]
fn test_cg_one_one_to_zero() {
let cg = clebsch_gordan(1.0, 0.0, 1.0, 0.0, 0.0, 0.0);
let expected_abs = 1.0 / 3.0f64.sqrt();
assert!(
(cg.abs() - expected_abs).abs() < 1e-6,
"|CG(1,0;1,0|0,0)| = {}, expected {expected_abs}",
cg.abs()
);
}
#[test]
fn test_cg_symmetry_exchange() {
let j1 = 1.0;
let m1 = 0.0;
let j2 = 0.5;
let m2 = 0.5;
let j = 1.5;
let m = m1 + m2;
let cg_direct = clebsch_gordan(j1, m1, j2, m2, j, m);
let cg_swapped = clebsch_gordan(j2, m2, j1, m1, j, m);
let phase_exp = (twice(j1) + twice(j2) - twice(j)) / 2;
let phase: f64 = if phase_exp % 2 == 0 { 1.0 } else { -1.0 };
assert!(
(cg_direct - phase * cg_swapped).abs() < 1e-6,
"CG symmetry: {cg_direct} vs {phase} * {cg_swapped}"
);
}
#[test]
fn test_wigner_3j_triangle_violation() {
let w3j = wigner_3j(1.0, 0.0, 1.0, 0.0, 5.0, 0.0);
assert!(
w3j.abs() < 1e-10,
"3j outside triangle should be 0, got {w3j}"
);
}
#[test]
fn test_wigner_3j_known_half() {
let w3j = wigner_3j(0.5, 0.5, 0.5, -0.5, 0.0, 0.0);
let cg = clebsch_gordan(0.5, 0.5, 0.5, -0.5, 0.0, 0.0);
assert!(
(w3j.abs() - cg.abs()).abs() < 1e-6,
"|3j| = {}, |CG| = {} (should match for j3=0)",
w3j.abs(),
cg.abs()
);
}
#[test]
fn test_wigner_6j_known_racah() {
let w6j_half = wigner_6j(0.5, 0.5, 1.0, 0.5, 0.5, 1.0);
let expected = 1.0f64 / 6.0f64;
assert!(
(w6j_half.abs() - expected).abs() < 1e-5,
"|6j(1/2,1/2,1;1/2,1/2,1)| = {}, expected {expected}",
w6j_half.abs()
);
let w6j_zero = wigner_6j(0.5, 0.5, 0.0, 0.5, 0.5, 0.0);
assert!(
(w6j_zero.abs() - 0.5).abs() < 1e-5,
"|6j(1/2,1/2,0;1/2,1/2,0)| = {}, expected 0.5",
w6j_zero.abs()
);
}
#[test]
fn test_cg_table_size_dimensions() {
let table = cg_table(1.0, 1.0).expect("cg_table(1,1)");
assert_eq!(table.len(), 3, "j1=1: n1 = 2*j1+1 = 3");
assert_eq!(table[0].len(), 3, "j2=1: n2 = 2*j2+1 = 3");
assert_eq!(table[0][0].len(), 3, "J values: |1-1|=0 to 1+1=2, nJ=3");
let table2 = cg_table(1.5, 0.5).expect("cg_table(3/2,1/2)");
assert_eq!(table2.len(), 4, "j1=3/2: n1 = 4");
assert_eq!(table2[0].len(), 2, "j2=1/2: n2 = 2");
assert_eq!(table2[0][0].len(), 2, "J values: |3/2-1/2|=1 to 2, nJ=2");
}
#[test]
fn test_log_factorial_correctness() {
let lf = log_factorial(5);
let expected = 120.0f64.ln();
assert!(
(lf - expected).abs() < 1e-12,
"log_factorial(5) = {lf}, expected {expected}"
);
let lf10 = log_factorial(10);
let expected10 = 3_628_800.0f64.ln();
assert!(
(lf10 - expected10).abs() < 1e-10,
"log_factorial(10) = {lf10}, expected {expected10}"
);
}
}