use num_rational::Ratio;
use crate::cache::{cache_3j, cache_6j, cache_f};
use crate::exact::SignedSqrtRational;
use crate::primefactor::{factorial as pf_factorial, mul_factorial, sum_series, Pf};
fn delta_sq_pf(a: u32, b: u32, c: u32) -> (Pf, Pf) {
let (a, b, c) = (a as i64, b as i64, c as i64);
let t1 = ((a + b - c) / 2) as u64;
let t2 = ((a - b + c) / 2) as u64;
let t3 = ((-a + b + c) / 2) as u64;
let t4 = ((a + b + c) / 2 + 1) as u64;
let mut num = pf_factorial(t1);
mul_factorial(&mut num, t2);
mul_factorial(&mut num, t3);
(num, pf_factorial(t4))
}
fn triangle_ok(a: u32, b: u32, c: u32) -> bool {
let (a, b, c) = (a as i64, b as i64, c as i64);
(a + b + c) % 2 == 0 && c >= (a - b).abs() && c <= a + b
}
fn check_triangle(a: u32, b: u32, c: u32) -> Result<(), AdmissibilityViolation> {
if triangle_ok(a, b, c) {
Ok(())
} else {
Err(AdmissibilityViolation::Triangle { a, b, c })
}
}
fn check_6j_admissible(
dj1: u32,
dj2: u32,
dj3: u32,
dj4: u32,
dj5: u32,
dj6: u32,
) -> Result<(), AdmissibilityViolation> {
check_triangle(dj1, dj2, dj3)?;
check_triangle(dj1, dj5, dj6)?;
check_triangle(dj4, dj2, dj6)?;
check_triangle(dj4, dj5, dj3)?;
Ok(())
}
pub fn wigner_6j(dj1: u32, dj2: u32, dj3: u32, dj4: u32, dj5: u32, dj6: u32) -> SignedSqrtRational {
match canonical_regge_6j(dj1, dj2, dj3, dj4, dj5, dj6) {
Ok(key) => {
cache_6j().get_or_compute(key, || wigner_6j_uncached(dj1, dj2, dj3, dj4, dj5, dj6))
}
Err(_) => wigner_6j_uncached(dj1, dj2, dj3, dj4, dj5, dj6),
}
}
fn wigner_6j_uncached(
dj1: u32,
dj2: u32,
dj3: u32,
dj4: u32,
dj5: u32,
dj6: u32,
) -> SignedSqrtRational {
if !(triangle_ok(dj1, dj2, dj3)
&& triangle_ok(dj1, dj5, dj6)
&& triangle_ok(dj4, dj2, dj6)
&& triangle_ok(dj4, dj5, dj3))
{
return SignedSqrtRational::zero();
}
let (n1, d1) = delta_sq_pf(dj1, dj2, dj3);
let (n2, d2) = delta_sq_pf(dj1, dj5, dj6);
let (n3, d3) = delta_sq_pf(dj4, dj2, dj6);
let (n4, d4) = delta_sq_pf(dj4, dj5, dj3);
let mut num = n1;
num.mul_assign(&n2);
num.mul_assign(&n3);
num.mul_assign(&n4);
let mut den = d1;
den.mul_assign(&d2);
den.mul_assign(&d3);
den.mul_assign(&d4);
let (mut snum, mut rnum) = num.splitsquare();
let (mut sden, mut rden) = den.splitsquare();
Pf::divgcd(&mut snum, &mut sden);
Pf::divgcd(&mut rnum, &mut rden);
let s = Ratio::new(snum.to_bigint(), sden.to_bigint());
let r = Ratio::new(rnum.to_bigint(), rden.to_bigint());
let (j1, j2, j3, j4, j5, j6) = (
dj1 as i64, dj2 as i64, dj3 as i64, dj4 as i64, dj5 as i64, dj6 as i64,
);
let t1 = (j1 + j2 + j3) / 2;
let t2 = (j1 + j5 + j6) / 2;
let t3 = (j4 + j2 + j6) / 2;
let t4 = (j4 + j5 + j3) / 2;
let t5 = (j1 + j2 + j4 + j5) / 2;
let t6 = (j2 + j3 + j5 + j6) / 2;
let t7 = (j3 + j1 + j6 + j4) / 2;
let kmin = t1.max(t2).max(t3).max(t4);
let kmax = t5.min(t6).min(t7);
let mut terms = Vec::with_capacity((kmax - kmin + 1).max(0) as usize);
for k in kmin..=kmax {
let mut nump = pf_factorial((k + 1) as u64);
if k % 2 != 0 {
nump = nump.neg();
}
let mut denp = pf_factorial((k - t1) as u64);
mul_factorial(&mut denp, (k - t2) as u64);
mul_factorial(&mut denp, (k - t3) as u64);
mul_factorial(&mut denp, (k - t4) as u64);
mul_factorial(&mut denp, (t5 - k) as u64);
mul_factorial(&mut denp, (t6 - k) as u64);
mul_factorial(&mut denp, (t7 - k) as u64);
Pf::divgcd(&mut nump, &mut denp);
terms.push((nump, denp));
}
let series = sum_series(terms);
SignedSqrtRational::from_prefactor_radical(s * series, r)
}
pub fn wigner_3j(dj1: u32, dj2: u32, dj3: u32, dm1: i32, dm2: i32, dm3: i32) -> SignedSqrtRational {
match canonical_regge_3j(dj1, dj2, dj3, dm1, dm2, dm3) {
Ok((key, phase)) => {
let rep = cache_3j().get_or_compute(key, || {
phase.apply(wigner_3j_uncached(dj1, dj2, dj3, dm1, dm2, dm3))
});
phase.apply(rep)
}
Err(_) => wigner_3j_uncached(dj1, dj2, dj3, dm1, dm2, dm3),
}
}
fn wigner_3j_uncached(
dj1: u32,
dj2: u32,
dj3: u32,
dm1: i32,
dm2: i32,
dm3: i32,
) -> SignedSqrtRational {
if !admissible_3j(dj1, dj2, dj3, dm1, dm2, dm3) {
return SignedSqrtRational::zero();
}
let (j1, j2, j3) = (dj1 as i64, dj2 as i64, dj3 as i64);
let (m1, m2, m3) = (dm1 as i64, dm2 as i64, dm3 as i64);
let (mut num, den) = delta_sq_pf(dj1, dj2, dj3);
for (dj, dm) in [(j1, m1), (j2, m2), (j3, m3)] {
mul_factorial(&mut num, ((dj + dm) / 2) as u64);
mul_factorial(&mut num, ((dj - dm) / 2) as u64);
}
let (mut snum, mut rnum) = num.splitsquare();
let (mut sden, mut rden) = den.splitsquare();
Pf::divgcd(&mut snum, &mut sden);
Pf::divgcd(&mut rnum, &mut rden);
let s = Ratio::new(snum.to_bigint(), sden.to_bigint());
let r = Ratio::new(rnum.to_bigint(), rden.to_bigint());
let a = (j1 + j2 - j3) / 2;
let b = (j1 - m1) / 2;
let c = (j2 + m2) / 2;
let add1 = (j3 - j2 + m1) / 2;
let add2 = (j3 - j1 - m2) / 2;
let kmin = 0i64.max(-add1).max(-add2);
let kmax = a.min(b).min(c);
let mut terms = Vec::with_capacity((kmax - kmin + 1).max(0) as usize);
for k in kmin..=kmax {
let nump = if k % 2 == 0 {
Pf::one()
} else {
Pf::one().neg()
};
let mut denp = pf_factorial(k as u64);
mul_factorial(&mut denp, (a - k) as u64);
mul_factorial(&mut denp, (b - k) as u64);
mul_factorial(&mut denp, (c - k) as u64);
mul_factorial(&mut denp, (k + add1) as u64);
mul_factorial(&mut denp, (k + add2) as u64);
terms.push((nump, denp));
}
let mut value = s * sum_series(terms);
if phase_is_negative((j1 - j2 - m3) / 2) {
value = -value;
}
SignedSqrtRational::from_prefactor_radical(value, r)
}
pub fn clebsch_gordan(
dj1: u32,
dm1: i32,
dj2: u32,
dm2: i32,
dj3: u32,
dm3: i32,
) -> SignedSqrtRational {
let w3 = wigner_3j(dj1, dj2, dj3, dm1, dm2, -dm3);
if w3.sign() == 0 {
return SignedSqrtRational::zero();
}
let cg = w3.times_sqrt_int((dj3 + 1) as u64);
if phase_is_negative(((dj2 as i64) - (dj1 as i64) - (dm3 as i64)) / 2) {
cg.neg_value()
} else {
cg
}
}
pub fn su2_r_symbol(dj1: u32, dj2: u32, dj3: u32) -> f64 {
if !triangle_ok(dj1, dj2, dj3) {
return 0.0;
}
if phase_is_negative(((dj1 as i64) + (dj2 as i64) - (dj3 as i64)) / 2) {
-1.0
} else {
1.0
}
}
pub fn su2_frobenius_schur(dj: u32) -> f64 {
if dj.is_multiple_of(2) {
1.0
} else {
-1.0
}
}
fn f_symbol_exact(
dj1: u32,
dj2: u32,
dj3: u32,
dj4: u32,
dj5: u32,
dj6: u32,
) -> SignedSqrtRational {
let w = wigner_6j(dj1, dj2, dj5, dj3, dj4, dj6);
if w.sign() == 0 {
return SignedSqrtRational::zero();
}
let v = w
.times_sqrt_int((dj5 as u64) + 1)
.times_sqrt_int((dj6 as u64) + 1);
if phase_is_negative(((dj1 as i64) + (dj2 as i64) + (dj3 as i64) + (dj4 as i64)) / 2) {
v.neg_value()
} else {
v
}
}
pub fn su2_f_symbol(dj1: u32, dj2: u32, dj3: u32, dj4: u32, dj5: u32, dj6: u32) -> f64 {
match canonical_regge_6j(dj1, dj2, dj5, dj3, dj4, dj6) {
Ok(regge) => {
let key = FKey {
regge,
dim: ((dj5 as u64) + 1) * ((dj6 as u64) + 1),
phase_neg: phase_is_negative(
((dj1 as i64) + (dj2 as i64) + (dj3 as i64) + (dj4 as i64)) / 2,
),
};
cache_f().get_or_compute(key, || {
f_symbol_exact(dj1, dj2, dj3, dj4, dj5, dj6).to_f64()
})
}
Err(_) => f_symbol_exact(dj1, dj2, dj3, dj4, dj5, dj6).to_f64(),
}
}
fn check_3j_admissible(
dj1: u32,
dj2: u32,
dj3: u32,
dm1: i32,
dm2: i32,
dm3: i32,
) -> Result<(), AdmissibilityViolation> {
if dm1 + dm2 + dm3 != 0 {
return Err(AdmissibilityViolation::ProjectionSum { dm1, dm2, dm3 });
}
for (dj, dm) in [(dj1, dm1), (dj2, dm2), (dj3, dm3)] {
let dji = dj as i64;
let dmi = dm as i64;
if dmi.abs() > dji || (dji + dmi) % 2 != 0 {
return Err(AdmissibilityViolation::Projection { dj, dm });
}
}
check_triangle(dj1, dj2, dj3)
}
fn admissible_3j(dj1: u32, dj2: u32, dj3: u32, dm1: i32, dm2: i32, dm3: i32) -> bool {
check_3j_admissible(dj1, dj2, dj3, dm1, dm2, dm3).is_ok()
}
#[inline]
fn phase_is_negative(p: i64) -> bool {
p.rem_euclid(2) == 1
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct Regge6j([u16; 6]);
impl Regge6j {
pub fn components(&self) -> [u16; 6] {
self.0
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub(crate) struct FKey {
regge: Regge6j,
dim: u64,
phase_neg: bool,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ReggeError {
NonAdmissible,
Overflow,
}
impl std::fmt::Display for ReggeError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ReggeError::NonAdmissible => write!(f, "non-admissible 6j label set has no Regge key"),
ReggeError::Overflow => write!(f, "Regge key component exceeds u16::MAX"),
}
}
}
impl std::error::Error for ReggeError {}
pub fn canonical_regge_6j(
dj1: u32,
dj2: u32,
dj3: u32,
dj4: u32,
dj5: u32,
dj6: u32,
) -> Result<Regge6j, ReggeError> {
if !(triangle_ok(dj1, dj2, dj3)
&& triangle_ok(dj1, dj5, dj6)
&& triangle_ok(dj4, dj2, dj6)
&& triangle_ok(dj4, dj5, dj3))
{
return Err(ReggeError::NonAdmissible);
}
let (dj1, dj2, dj3, dj4, dj5, dj6) = (
dj1 as i64, dj2 as i64, dj3 as i64, dj4 as i64, dj5 as i64, dj6 as i64,
);
let mut alpha = [
(dj1 + dj2 + dj4 + dj5) / 2,
(dj1 + dj3 + dj4 + dj6) / 2,
(dj2 + dj3 + dj5 + dj6) / 2,
];
alpha.sort_unstable();
let mut beta = [
(dj1 + dj2 + dj3) / 2,
(dj1 + dj5 + dj6) / 2,
(dj2 + dj4 + dj6) / 2,
(dj3 + dj4 + dj5) / 2,
];
beta.sort_unstable_by(|a, b| b.cmp(a));
let raw = [
alpha[2] - beta[3], alpha[1] - beta[3], alpha[0] - beta[3], alpha[0] - beta[2], alpha[0] - beta[1], alpha[0] - beta[0], ];
let mut out = [0u16; 6];
for (slot, &v) in out.iter_mut().zip(raw.iter()) {
debug_assert!(v >= 0, "admissible 6j produced a negative Regge component");
if v > u16::MAX as i64 {
return Err(ReggeError::Overflow);
}
*slot = v as u16;
}
Ok(Regge6j(out))
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ReggePhase {
Plus,
Minus,
}
impl ReggePhase {
pub fn apply(self, v: SignedSqrtRational) -> SignedSqrtRational {
match self {
ReggePhase::Plus => v,
ReggePhase::Minus => v.neg_value(),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct Regge3j {
dj: [u16; 3],
dm: [i32; 3],
}
impl Regge3j {
pub fn doubled_spins(&self) -> [u16; 3] {
self.dj
}
pub fn doubled_projections(&self) -> [i32; 3] {
self.dm
}
}
fn canonicalize3j(dj: [i64; 3], dm: [i64; 3]) -> ([i64; 3], [i64; 3], i8) {
const PERMS: [([usize; 3], bool); 6] = [
([0, 1, 2], false),
([0, 2, 1], true),
([1, 0, 2], true),
([1, 2, 0], false),
([2, 0, 1], false),
([2, 1, 0], true),
];
let j_odd = ((dj[0] + dj[1] + dj[2]) / 2) % 2 != 0;
let mut best: Option<([i64; 3], [i64; 3])> = None;
let mut best_eps = 1i8;
for (p, p_odd) in PERMS {
let pj = [dj[p[0]], dj[p[1]], dj[p[2]]];
let pm = [dm[p[0]], dm[p[1]], dm[p[2]]];
for neg in [false, true] {
let m = if neg { [-pm[0], -pm[1], -pm[2]] } else { pm };
let odd_ops = (p_odd ^ neg) as i8;
let eps: i8 = if j_odd && odd_ops == 1 { -1 } else { 1 };
let cand = (pj, m);
if best.as_ref().is_none_or(|b| cand > *b) {
best = Some(cand);
best_eps = eps;
}
}
}
let (bj, bm) = best.expect("the identity image always seeds `best`");
(bj, bm, best_eps)
}
pub fn canonical_regge_3j(
dj1: u32,
dj2: u32,
dj3: u32,
dm1: i32,
dm2: i32,
dm3: i32,
) -> Result<(Regge3j, ReggePhase), ReggeError> {
if !admissible_3j(dj1, dj2, dj3, dm1, dm2, dm3) {
return Err(ReggeError::NonAdmissible);
}
let (dj, dm, sign) = canonicalize3j(
[dj1 as i64, dj2 as i64, dj3 as i64],
[dm1 as i64, dm2 as i64, dm3 as i64],
);
let mut dju = [0u16; 3];
for (slot, &v) in dju.iter_mut().zip(dj.iter()) {
if v > u16::MAX as i64 {
return Err(ReggeError::Overflow);
}
*slot = v as u16;
}
let dmi = [dm[0] as i32, dm[1] as i32, dm[2] as i32];
let phase = if sign < 0 {
ReggePhase::Minus
} else {
ReggePhase::Plus
};
Ok((Regge3j { dj: dju, dm: dmi }, phase))
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Su2Irrep(u32);
impl Su2Irrep {
pub fn new(dj: u32) -> Self {
Su2Irrep(dj)
}
pub fn dj(self) -> u32 {
self.0
}
pub fn dim(self) -> u64 {
self.0 as u64 + 1
}
pub fn dual(self) -> Self {
self
}
pub fn fusion(self, other: Self) -> Result<Su2Fusion, Su2Error> {
let hi = self.0.checked_add(other.0).ok_or(Su2Error::LabelOverflow {
left: self.0,
right: other.0,
})?;
let lo = self.0.abs_diff(other.0);
let remaining = ((hi - lo) / 2) as usize + 1;
Ok(Su2Fusion {
front: lo,
back: hi,
remaining,
})
}
}
#[derive(Clone, Copy, Debug)]
pub struct Su2Fusion {
front: u32,
back: u32,
remaining: usize,
}
impl Iterator for Su2Fusion {
type Item = Su2Irrep;
fn next(&mut self) -> Option<Su2Irrep> {
if self.remaining == 0 {
return None;
}
let dj = self.front;
self.remaining -= 1;
if self.remaining > 0 {
self.front += 2;
}
Some(Su2Irrep(dj))
}
fn size_hint(&self) -> (usize, Option<usize>) {
(self.remaining, Some(self.remaining))
}
}
impl DoubleEndedIterator for Su2Fusion {
fn next_back(&mut self) -> Option<Su2Irrep> {
if self.remaining == 0 {
return None;
}
let dj = self.back;
self.remaining -= 1;
if self.remaining > 0 {
self.back -= 2;
}
Some(Su2Irrep(dj))
}
}
impl ExactSizeIterator for Su2Fusion {}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum AdmissibilityViolation {
Triangle {
a: u32,
b: u32,
c: u32,
},
Projection {
dj: u32,
dm: i32,
},
ProjectionSum {
dm1: i32,
dm2: i32,
dm3: i32,
},
}
impl std::fmt::Display for AdmissibilityViolation {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
AdmissibilityViolation::Triangle { a, b, c } => write!(
f,
"doubled-spin triple ({a}, {b}, {c}) violates the triangle condition"
),
AdmissibilityViolation::Projection { dj, dm } => write!(
f,
"doubled projection {dm} is not on the ladder of doubled spin {dj}"
),
AdmissibilityViolation::ProjectionSum { dm1, dm2, dm3 } => write!(
f,
"doubled projections {dm1} + {dm2} + {dm3} do not sum to zero"
),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Su2Error {
LabelOverflow {
left: u32,
right: u32,
},
NotAdmissible(AdmissibilityViolation),
}
impl std::fmt::Display for Su2Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Su2Error::LabelOverflow { left, right } => write!(
f,
"doubled spins {left} + {right} overflow the u32 label space"
),
Su2Error::NotAdmissible(v) => write!(f, "inadmissible SU(2) coupling: {v}"),
}
}
}
impl std::error::Error for Su2Error {}
pub fn wigner_6j_checked(
dj1: u32,
dj2: u32,
dj3: u32,
dj4: u32,
dj5: u32,
dj6: u32,
) -> Result<SignedSqrtRational, Su2Error> {
check_6j_admissible(dj1, dj2, dj3, dj4, dj5, dj6).map_err(Su2Error::NotAdmissible)?;
Ok(wigner_6j(dj1, dj2, dj3, dj4, dj5, dj6))
}
pub fn wigner_3j_checked(
dj1: u32,
dj2: u32,
dj3: u32,
dm1: i32,
dm2: i32,
dm3: i32,
) -> Result<SignedSqrtRational, Su2Error> {
check_3j_admissible(dj1, dj2, dj3, dm1, dm2, dm3).map_err(Su2Error::NotAdmissible)?;
Ok(wigner_3j(dj1, dj2, dj3, dm1, dm2, dm3))
}
pub fn clebsch_gordan_checked(
dj1: u32,
dm1: i32,
dj2: u32,
dm2: i32,
dj3: u32,
dm3: i32,
) -> Result<SignedSqrtRational, Su2Error> {
check_3j_admissible(dj1, dj2, dj3, dm1, dm2, -dm3).map_err(Su2Error::NotAdmissible)?;
Ok(clebsch_gordan(dj1, dm1, dj2, dm2, dj3, dm3))
}
pub fn su2_f_symbol_checked(
dj1: u32,
dj2: u32,
dj3: u32,
dj4: u32,
dj5: u32,
dj6: u32,
) -> Result<f64, Su2Error> {
check_6j_admissible(dj1, dj2, dj5, dj3, dj4, dj6).map_err(Su2Error::NotAdmissible)?;
Ok(su2_f_symbol(dj1, dj2, dj3, dj4, dj5, dj6))
}
pub fn su2_r_symbol_checked(dj1: u32, dj2: u32, dj3: u32) -> Result<f64, Su2Error> {
check_triangle(dj1, dj2, dj3).map_err(Su2Error::NotAdmissible)?;
Ok(su2_r_symbol(dj1, dj2, dj3))
}
pub fn su2_authority_fingerprint() -> &'static [u8] {
b"racah:su2-exact:model=bigrational-round-once:3j=condon-shortley:cg=condon-shortley:6j=racah-single-sum:f=tks-su2irrep:r=tks-su2irrep:fs=tks-su2irrep:epoch=1"
}
#[cfg(test)]
mod tests {
use super::*;
use num_bigint::BigInt;
fn sq(v: &SignedSqrtRational) -> Ratio<BigInt> {
v.signed_square()
}
#[test]
fn triangle_admissibility() {
assert!(triangle_ok(1, 1, 2)); assert!(triangle_ok(2, 2, 2)); assert!(!triangle_ok(1, 1, 1)); assert!(!triangle_ok(2, 2, 6)); }
#[test]
fn six_j_known_value() {
let v = wigner_6j(1, 1, 2, 1, 1, 2);
assert_eq!(sq(&v), Ratio::new(BigInt::from(1), BigInt::from(36)));
assert!((v.to_f64() - 1.0 / 6.0).abs() < 1e-14);
}
#[test]
fn six_j_all_ones() {
let v = wigner_6j(2, 2, 2, 2, 2, 2);
assert_eq!(sq(&v), Ratio::new(BigInt::from(1), BigInt::from(36)));
}
#[test]
fn six_j_nonadmissible_is_zero() {
let v = wigner_6j(1, 1, 1, 1, 1, 1);
assert_eq!(v, SignedSqrtRational::zero());
}
#[test]
fn three_j_known_value() {
let v = wigner_3j(1, 1, 2, 1, -1, 0);
assert_eq!(sq(&v), Ratio::new(BigInt::from(1), BigInt::from(6)));
}
#[test]
fn three_j_m_sum_nonzero_is_zero() {
let v = wigner_3j(1, 1, 2, 1, 1, 0);
assert_eq!(v, SignedSqrtRational::zero());
}
#[test]
fn cg_known_value() {
let v = clebsch_gordan(1, 1, 1, -1, 2, 0);
assert_eq!(sq(&v), Ratio::new(BigInt::from(1), BigInt::from(2)));
assert!((v.to_f64() - (0.5f64).sqrt()).abs() < 1e-14);
}
#[test]
fn cg_stretched_is_one() {
let v = clebsch_gordan(1, 1, 1, 1, 2, 2);
assert_eq!(sq(&v), Ratio::from(BigInt::from(1)));
assert!((v.to_f64() - 1.0).abs() < 1e-14);
}
#[test]
fn regge_key_orbit_invariance_small() {
let base = canonical_regge_6j(2, 4, 4, 6, 4, 2).unwrap();
let swap12 = canonical_regge_6j(2, 4, 4, 6, 2, 4).unwrap();
assert_eq!(base, swap12);
assert_ne!([2u32, 4, 4, 6, 4, 2], [2u32, 4, 4, 6, 2, 4]);
}
#[test]
fn regge_overflow_reported() {
let big = 200_000u32;
assert_eq!(
canonical_regge_6j(big, big, big, big, big, big),
Err(ReggeError::Overflow)
);
}
#[test]
fn regge_nonadmissible_is_error_not_a_key() {
assert_eq!(
canonical_regge_6j(1, 1, 1, 1, 1, 1),
Err(ReggeError::NonAdmissible)
);
let admissible = canonical_regge_6j(2, 2, 2, 2, 2, 2);
assert!(admissible.is_ok());
assert_ne!(
canonical_regge_6j(1, 1, 1, 1, 1, 1).ok(),
admissible.ok(),
"non-admissible input must not share a key with an admissible one"
);
}
fn orbit_images(dj: [u32; 3], dm: [i32; 3]) -> Vec<([u32; 3], [i32; 3])> {
const PERMS: [[usize; 3]; 6] = [
[0, 1, 2],
[0, 2, 1],
[1, 0, 2],
[1, 2, 0],
[2, 0, 1],
[2, 1, 0],
];
let mut out = Vec::with_capacity(12);
for p in PERMS {
let pj = [dj[p[0]], dj[p[1]], dj[p[2]]];
let pm = [dm[p[0]], dm[p[1]], dm[p[2]]];
out.push((pj, pm));
out.push((pj, [-pm[0], -pm[1], -pm[2]]));
}
out
}
#[test]
fn regge3j_orbit_same_key_and_phase_compensated_value() {
let dj = [2u32, 2, 2];
let dm = [2i32, 0, -2];
let mut key0 = None;
let mut rep_value = None;
let mut saw_negative_phase = false;
for (pj, pm) in orbit_images(dj, dm) {
let (key, phase) =
canonical_regge_3j(pj[0], pj[1], pj[2], pm[0], pm[1], pm[2]).unwrap();
match key0 {
None => key0 = Some(key),
Some(k) => assert_eq!(k, key, "orbit image produced a different key"),
}
let raw = wigner_3j(pj[0], pj[1], pj[2], pm[0], pm[1], pm[2]);
let compensated = phase.apply(raw.clone());
match &rep_value {
None => rep_value = Some(compensated),
Some(rv) => assert_eq!(
rv, &compensated,
"phase-compensated value differs across the orbit"
),
}
if phase == ReggePhase::Minus {
saw_negative_phase = true;
assert_ne!(raw, phase.apply(raw.clone()));
}
}
assert!(
saw_negative_phase,
"J-odd orbit must exercise the Minus phase"
);
}
#[test]
fn regge3j_even_j_phase_is_always_plus() {
let dj = [2u32, 2, 4];
let dm = [2i32, -2, 0];
for (pj, pm) in orbit_images(dj, dm) {
let (_, phase) = canonical_regge_3j(pj[0], pj[1], pj[2], pm[0], pm[1], pm[2]).unwrap();
assert_eq!(phase, ReggePhase::Plus, "even-J phase must be Plus");
}
}
#[test]
fn regge3j_nonadmissible_is_error_not_a_key() {
assert_eq!(
canonical_regge_3j(2, 2, 2, 2, 2, 0),
Err(ReggeError::NonAdmissible)
);
assert_eq!(
canonical_regge_3j(2, 2, 2, 4, -2, -2),
Err(ReggeError::NonAdmissible)
);
assert_eq!(
canonical_regge_3j(1, 1, 1, 1, -1, 0),
Err(ReggeError::NonAdmissible)
);
}
#[test]
fn cached_3j_matches_uncached_over_grid() {
for dj1 in 0..=4u32 {
for dj2 in 0..=4u32 {
for dj3 in 0..=4u32 {
for dm1 in -4..=4i32 {
for dm2 in -4..=4i32 {
for dm3 in -4..=4i32 {
let raw = wigner_3j_uncached(dj1, dj2, dj3, dm1, dm2, dm3);
let ctx = (dj1, dj2, dj3, dm1, dm2, dm3);
assert_eq!(
wigner_3j(dj1, dj2, dj3, dm1, dm2, dm3),
raw,
"cached != uncached at {ctx:?}"
);
assert_eq!(
wigner_3j(dj1, dj2, dj3, dm1, dm2, dm3),
raw,
"cache-hit != uncached at {ctx:?}"
);
}
}
}
}
}
}
}
#[test]
fn cached_6j_matches_uncached_over_grid() {
for dj1 in 0..=4u32 {
for dj2 in 0..=4u32 {
for dj3 in 0..=4u32 {
for dj4 in 0..=4u32 {
for dj5 in 0..=4u32 {
for dj6 in 0..=4u32 {
let raw = wigner_6j_uncached(dj1, dj2, dj3, dj4, dj5, dj6);
let ctx = (dj1, dj2, dj3, dj4, dj5, dj6);
assert_eq!(
wigner_6j(dj1, dj2, dj3, dj4, dj5, dj6),
raw,
"cached != uncached at {ctx:?}"
);
assert_eq!(
wigner_6j(dj1, dj2, dj3, dj4, dj5, dj6),
raw,
"cache-hit != uncached at {ctx:?}"
);
}
}
}
}
}
}
}
#[test]
fn public_stats_and_reset_smoke() {
let _ = wigner_6j(2, 2, 2, 2, 2, 2);
let _ = wigner_3j(2, 2, 2, 2, 0, -2);
let s = crate::cache::stats();
assert!(s.hits + s.misses >= 1, "activity should register in stats");
crate::cache::reset();
let _ = crate::cache::stats();
}
#[test]
fn regge3j_overflow_reported() {
let big = 200_000u32;
assert_eq!(
canonical_regge_3j(big, big, big, 0, 0, 0),
Err(ReggeError::Overflow)
);
}
#[test]
fn f_symbol_exact_composition_identity() {
for dj1 in 0..=4u32 {
for dj2 in 0..=4u32 {
for dj3 in 0..=4u32 {
for dj4 in 0..=4u32 {
for dj5 in 0..=4u32 {
for dj6 in 0..=4u32 {
let f = f_symbol_exact(dj1, dj2, dj3, dj4, dj5, dj6);
let w = wigner_6j(dj1, dj2, dj5, dj3, dj4, dj6);
let dim = BigInt::from(((dj5 + 1) * (dj6 + 1)) as i64);
let mut expected = w.signed_square() * Ratio::from(dim);
let sum = (dj1 as i64) + (dj2 as i64) + (dj3 as i64) + (dj4 as i64);
if (sum / 2).rem_euclid(2) == 1 {
expected = -expected;
}
let ctx = (dj1, dj2, dj3, dj4, dj5, dj6);
assert_eq!(f.signed_square(), expected, "F^2 identity at {ctx:?}");
}
}
}
}
}
}
}
#[test]
fn f_symbol_cached_matches_exact_over_grid() {
for dj1 in 0..=4u32 {
for dj2 in 0..=4u32 {
for dj3 in 0..=4u32 {
for dj4 in 0..=4u32 {
for dj5 in 0..=4u32 {
for dj6 in 0..=4u32 {
let want = f_symbol_exact(dj1, dj2, dj3, dj4, dj5, dj6).to_f64();
let ctx = (dj1, dj2, dj3, dj4, dj5, dj6);
assert_eq!(
su2_f_symbol(dj1, dj2, dj3, dj4, dj5, dj6),
want,
"cached != exact at {ctx:?}"
);
assert_eq!(
su2_f_symbol(dj1, dj2, dj3, dj4, dj5, dj6),
want,
"cache-hit != exact at {ctx:?}"
);
}
}
}
}
}
}
}
#[test]
fn f_symbol_all_trivial_is_one() {
assert_eq!(su2_f_symbol(0, 0, 0, 0, 0, 0), 1.0);
}
#[test]
fn f_symbol_nonadmissible_is_zero() {
assert_eq!(su2_f_symbol(1, 1, 1, 1, 1, 1), 0.0);
}
#[test]
fn r_symbol_matches_convention() {
assert_eq!(su2_r_symbol(1, 1, 2), 1.0);
assert_eq!(su2_r_symbol(1, 1, 0), -1.0);
assert_eq!(su2_r_symbol(2, 2, 2), -1.0);
assert_eq!(su2_r_symbol(2, 2, 4), 1.0);
assert_eq!(su2_r_symbol(1, 1, 1), 0.0);
assert_eq!(su2_r_symbol(2, 2, 8), 0.0);
}
#[test]
fn frobenius_schur_is_sign_of_doubled_spin() {
assert_eq!(su2_frobenius_schur(0), 1.0); assert_eq!(su2_frobenius_schur(1), -1.0); assert_eq!(su2_frobenius_schur(2), 1.0); assert_eq!(su2_frobenius_schur(3), -1.0); assert_eq!(su2_frobenius_schur(4), 1.0); }
#[test]
fn regge_triangle_inequality_is_nonadmissible_not_overflow() {
assert_eq!(
canonical_regge_6j(2, 2, 20, 2, 2, 2),
Err(ReggeError::NonAdmissible)
);
}
}
#[cfg(test)]
mod checked_tests {
use super::*;
use rand::{Rng, SeedableRng};
use rand_chacha::ChaCha8Rng;
#[test]
fn irrep_accessors_and_self_dual() {
let s = Su2Irrep::new(3); assert_eq!(s.dj(), 3);
assert_eq!(s.dim(), 4); assert_eq!(s.dual(), s); assert_eq!(Su2Irrep::new(0).dim(), 1);
}
#[test]
fn dim_does_not_overflow_at_max_label() {
assert_eq!(Su2Irrep::new(u32::MAX).dim(), u32::MAX as u64 + 1);
}
#[test]
fn fusion_range_matches_direct_triangle_scan() {
for dj1 in 0..=8u32 {
for dj2 in 0..=8u32 {
let got: Vec<u32> = Su2Irrep::new(dj1)
.fusion(Su2Irrep::new(dj2))
.unwrap()
.map(|s| s.dj())
.collect();
let want: Vec<u32> = (0..=dj1 + dj2)
.filter(|&c| triangle_ok(dj1, dj2, c))
.collect();
assert_eq!(got, want, "fusion {dj1} x {dj2}");
}
}
}
#[test]
fn fusion_exact_size_and_double_ended() {
let mut it = Su2Irrep::new(4).fusion(Su2Irrep::new(2)).unwrap(); assert_eq!(it.len(), 3);
assert_eq!(it.next().map(|s| s.dj()), Some(2));
assert_eq!(it.next_back().map(|s| s.dj()), Some(6));
assert_eq!(it.len(), 1);
assert_eq!(it.next().map(|s| s.dj()), Some(4));
assert_eq!(it.len(), 0);
assert!(it.next().is_none());
assert!(it.next_back().is_none());
let full: Vec<u32> = Su2Irrep::new(4)
.fusion(Su2Irrep::new(2))
.unwrap()
.rev()
.map(|s| s.dj())
.collect();
assert_eq!(full, vec![6, 4, 2]);
}
#[test]
fn fusion_overflow_at_u32_boundary() {
let a = Su2Irrep::new(u32::MAX);
assert!(matches!(
a.fusion(Su2Irrep::new(1)),
Err(Su2Error::LabelOverflow {
left: u32::MAX,
right: 1,
})
));
assert!(Su2Irrep::new(u32::MAX - 1).fusion(Su2Irrep::new(1)).is_ok());
}
#[test]
fn fusion_full_back_drain_reaches_singlet_without_underflow() {
let got: Vec<u32> = Su2Irrep::new(2)
.fusion(Su2Irrep::new(2))
.unwrap()
.rev()
.map(|s| s.dj())
.collect();
assert_eq!(got, vec![4, 2, 0]);
}
#[test]
fn fusion_full_forward_drain_at_u32_max_without_overflow() {
let got: Vec<u32> = Su2Irrep::new(u32::MAX - 1)
.fusion(Su2Irrep::new(1))
.unwrap()
.map(|s| s.dj())
.collect();
assert_eq!(got, vec![u32::MAX - 2, u32::MAX]);
}
#[test]
fn fusion_mixed_drain_to_singlet_without_underflow() {
let mut it = Su2Irrep::new(2).fusion(Su2Irrep::new(2)).unwrap();
let mut got = vec![
it.next().unwrap().dj(), it.next_back().unwrap().dj(), it.next_back().unwrap().dj(), ];
assert!(it.next().is_none());
assert!(it.next_back().is_none());
got.sort_unstable();
assert_eq!(got, vec![0, 2, 4]);
}
#[test]
fn guard_6j_triangle() {
assert_eq!(
wigner_6j_checked(2, 2, 20, 2, 2, 2),
Err(Su2Error::NotAdmissible(AdmissibilityViolation::Triangle {
a: 2,
b: 2,
c: 20,
}))
);
assert!(matches!(
wigner_6j_checked(2, 3, 3, 7, 2, 100),
Err(Su2Error::NotAdmissible(
AdmissibilityViolation::Triangle { .. }
))
));
}
#[test]
fn guard_3j_projection_sum() {
assert_eq!(
wigner_3j_checked(2, 2, 2, 2, 2, 2),
Err(Su2Error::NotAdmissible(
AdmissibilityViolation::ProjectionSum {
dm1: 2,
dm2: 2,
dm3: 2,
}
))
);
}
#[test]
fn guard_3j_projection_out_of_range() {
assert_eq!(
wigner_3j_checked(2, 2, 2, 4, -2, -2),
Err(Su2Error::NotAdmissible(
AdmissibilityViolation::Projection { dj: 2, dm: 4 }
))
);
}
#[test]
fn guard_3j_projection_parity() {
assert_eq!(
wigner_3j_checked(2, 2, 2, 1, 1, -2),
Err(Su2Error::NotAdmissible(
AdmissibilityViolation::Projection { dj: 2, dm: 1 }
))
);
}
#[test]
fn guard_3j_triangle() {
assert_eq!(
wigner_3j_checked(2, 2, 8, 0, 0, 0),
Err(Su2Error::NotAdmissible(AdmissibilityViolation::Triangle {
a: 2,
b: 2,
c: 8,
}))
);
}
#[test]
fn guard_cg_inadmissible() {
assert!(matches!(
clebsch_gordan_checked(2, 2, 2, 2, 2, 0),
Err(Su2Error::NotAdmissible(
AdmissibilityViolation::ProjectionSum { .. }
))
));
}
#[test]
fn guard_f_symbol_triangle() {
assert!(matches!(
su2_f_symbol_checked(1, 1, 1, 1, 1, 1),
Err(Su2Error::NotAdmissible(
AdmissibilityViolation::Triangle { .. }
))
));
}
#[test]
fn guard_r_symbol_triangle() {
assert_eq!(
su2_r_symbol_checked(1, 1, 1),
Err(Su2Error::NotAdmissible(AdmissibilityViolation::Triangle {
a: 1,
b: 1,
c: 1,
}))
);
assert_eq!(su2_r_symbol_checked(1, 1, 2), Ok(1.0));
}
#[test]
fn checked_value_near_u32_max_is_not_admissible_without_overflow() {
assert!(matches!(
wigner_6j_checked(u32::MAX, 2, 0, 2, 2, 2),
Err(Su2Error::NotAdmissible(_))
));
}
#[test]
fn admissible_but_zero_6j_is_ok_not_err() {
let v = wigner_6j_checked(2, 3, 3, 7, 6, 6).expect("admissible tuple");
assert_eq!(v, SignedSqrtRational::zero());
assert_eq!(wigner_6j(2, 3, 3, 7, 6, 6), SignedSqrtRational::zero());
}
#[test]
fn property_checked_equals_unchecked_when_admissible() {
let mut rng = ChaCha8Rng::seed_from_u64(0xCA5C_ADE5_u64);
let mut tested_6j = 0;
let mut tested_3j = 0;
for _ in 0..20_000 {
let d = [(); 6].map(|_| rng.gen_range(0..=10u32));
if let Ok(v) = wigner_6j_checked(d[0], d[1], d[2], d[3], d[4], d[5]) {
assert_eq!(v, wigner_6j(d[0], d[1], d[2], d[3], d[4], d[5]));
tested_6j += 1;
}
let dj = [(); 3].map(|_| rng.gen_range(0..=8u32));
let dm1 = rng.gen_range(-(dj[0] as i32)..=dj[0] as i32);
let dm2 = rng.gen_range(-(dj[1] as i32)..=dj[1] as i32);
let dm = [dm1, dm2, -(dm1 + dm2)];
if let Ok(v) = wigner_3j_checked(dj[0], dj[1], dj[2], dm[0], dm[1], dm[2]) {
assert_eq!(v, wigner_3j(dj[0], dj[1], dj[2], dm[0], dm[1], dm[2]));
tested_3j += 1;
}
}
assert!(
tested_6j > 100,
"too few admissible 6j samples ({tested_6j})"
);
assert!(
tested_3j > 100,
"too few admissible 3j samples ({tested_3j})"
);
}
#[test]
fn property_forbidden_is_zero_and_not_admissible() {
let mut rng = ChaCha8Rng::seed_from_u64(0xBEEF);
let mut tested_6j = 0;
let mut tested_3j = 0;
for _ in 0..40_000 {
let d = [(); 6].map(|_| rng.gen_range(0..=8u32));
match wigner_6j_checked(d[0], d[1], d[2], d[3], d[4], d[5]) {
Err(Su2Error::NotAdmissible(_)) => {
assert_eq!(
wigner_6j(d[0], d[1], d[2], d[3], d[4], d[5]),
SignedSqrtRational::zero(),
"structurally forbidden 6j must be an unchecked zero"
);
tested_6j += 1;
}
Err(Su2Error::LabelOverflow { .. }) => unreachable!("6j checked never overflows"),
Ok(_) => {}
}
let dj = [(); 3].map(|_| rng.gen_range(0..=6u32));
let dm = [(); 3].map(|_| rng.gen_range(-6..=6i32));
if let Err(Su2Error::NotAdmissible(_)) =
wigner_3j_checked(dj[0], dj[1], dj[2], dm[0], dm[1], dm[2])
{
assert_eq!(
wigner_3j(dj[0], dj[1], dj[2], dm[0], dm[1], dm[2]),
SignedSqrtRational::zero(),
"structurally forbidden 3j must be an unchecked zero"
);
tested_3j += 1;
}
}
assert!(
tested_6j > 100,
"too few forbidden 6j samples ({tested_6j})"
);
assert!(
tested_3j > 100,
"too few forbidden 3j samples ({tested_3j})"
);
}
}