use std::collections::{BTreeMap, HashMap};
use std::fmt;
use std::sync::Arc;
use num_bigint::BigInt;
use num_rational::Ratio;
use num_traits::{One, Signed, Zero};
use crate::SignedSqrtRational;
mod cgc;
mod fr;
mod linalg;
pub use cgc::{cgc, Cgc, CgcEntry};
pub use fr::{
check_f_unitarity, check_hexagon, check_pentagon, f_symbol, r_symbol, FBlock, RBlock,
};
#[derive(Clone, Debug, PartialEq)]
pub enum SunError {
EmptyLabel,
NotNonincreasing {
weight: Vec<i64>,
},
NegativeDynkin {
dynkin: Vec<i64>,
},
UnsupportedRootSystem {
root_system: crate::group::RootSystem,
},
LabelRankMismatch {
expected: usize,
got: usize,
},
NotAdmissible {
group: crate::group::GroupId,
dynkin: Vec<i64>,
},
RankMismatch {
a: usize,
b: usize,
},
NullspaceDimMismatch {
expected: usize,
found: usize,
},
NotOrthonormal {
residual: f64,
},
LadderInconsistent {
residual: f64,
},
Linalg(String),
ZeroFusionChannel {
a: Vec<i64>,
b: Vec<i64>,
c: Vec<i64>,
},
FNotUnitary {
residual: f64,
},
PentagonViolation {
residual: f64,
},
HexagonViolation {
residual: f64,
},
}
impl fmt::Display for SunError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
SunError::EmptyLabel => write!(f, "SU(N) irrep label must be non-empty"),
SunError::NotNonincreasing { weight } => {
write!(f, "SU(N) weight is not nonincreasing: {weight:?}")
}
SunError::UnsupportedRootSystem { root_system } => write!(
f,
"root system {root_system} is not an A_r: SU(N) labels need A(N-1)"
),
SunError::LabelRankMismatch { expected, got } => write!(
f,
"Dynkin label of length {got} for a rank-{expected} group"
),
SunError::NotAdmissible { group, dynkin } => write!(
f,
"Dynkin label {dynkin:?} is a dominant integral weight of SU(N) but not a \
representation of {group:?}: its N-ality is non-trivial on the \
quotiented-out subgroup of the center — construct it through \
Irrep::from_dynkin (the simply connected form)"
),
SunError::NegativeDynkin { dynkin } => {
write!(f, "SU(N) Dynkin label has a negative component: {dynkin:?}")
}
SunError::RankMismatch { a, b } => {
write!(
f,
"directproduct of SU({a}) and SU({b}) irreps (rank mismatch)"
)
}
SunError::NullspaceDimMismatch { expected, found } => write!(
f,
"CGC nullspace dimension {found} != fusion multiplicity {expected}"
),
SunError::NotOrthonormal { residual } => {
write!(f, "CGC columns not orthonormal (residual {residual:e})")
}
SunError::LadderInconsistent { residual } => {
write!(f, "CGC ladder-descent inconsistent (residual {residual:e})")
}
SunError::Linalg(msg) => write!(f, "dense factorization failed: {msg}"),
SunError::ZeroFusionChannel { a, b, c } => write!(
f,
"empty fusion vertex {a:?} ⊗ {b:?} → {c:?} (N = 0) in an F/R request"
),
SunError::FNotUnitary { residual } => {
write!(f, "F-move matrix not unitary (residual {residual:e})")
}
SunError::PentagonViolation { residual } => {
write!(f, "pentagon identity violated (residual {residual:e})")
}
SunError::HexagonViolation { residual } => {
write!(f, "hexagon identity violated (residual {residual:e})")
}
}
}
}
impl std::error::Error for SunError {}
#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct Irrep {
weight: Box<[i64]>,
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub(crate) struct SunProductKey {
left: Irrep,
right: Irrep,
}
impl SunProductKey {
pub(crate) fn new(a: &Irrep, b: &Irrep) -> Self {
let (left, right) = if a <= b { (a, b) } else { (b, a) };
Self {
left: left.clone(),
right: right.clone(),
}
}
}
impl crate::cache::CacheKeyCharge for SunProductKey {
fn key_bytes(&self) -> usize {
self.left.key_bytes().saturating_add(self.right.key_bytes())
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SunProduct(Arc<[(Irrep, u32)]>);
impl SunProduct {
pub(crate) fn from_map(product: BTreeMap<Irrep, u32>) -> Self {
Self(product.into_iter().collect())
}
pub fn iter(&self) -> impl Iterator<Item = (&Irrep, u32)> {
self.0
.iter()
.map(|(irrep, multiplicity)| (irrep, *multiplicity))
}
pub fn multiplicity(&self, irrep: &Irrep) -> u32 {
self.0
.binary_search_by(|(candidate, _)| candidate.cmp(irrep))
.map(|index| self.0[index].1)
.unwrap_or(0)
}
#[cfg(test)]
pub(crate) fn ptr_eq(&self, other: &Self) -> bool {
Arc::ptr_eq(&self.0, &other.0)
}
#[cfg(test)]
pub(crate) fn strong_count(&self) -> usize {
Arc::strong_count(&self.0)
}
}
#[cfg(test)]
std::thread_local! {
static PUBLIC_DIRECTPRODUCT_RECONSTRUCTIONS: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
}
#[cfg(test)]
pub(crate) fn reset_public_directproduct_reconstructions() {
PUBLIC_DIRECTPRODUCT_RECONSTRUCTIONS.with(|count| count.set(0));
}
#[cfg(test)]
pub(crate) fn public_directproduct_reconstructions() -> usize {
PUBLIC_DIRECTPRODUCT_RECONSTRUCTIONS.with(std::cell::Cell::get)
}
impl crate::cache::CacheCharge for SunProduct {
fn value_bytes(&self) -> usize {
std::mem::size_of::<Self>()
.saturating_add(2 * std::mem::size_of::<usize>())
.saturating_add(std::mem::size_of_val(self.0.as_ref()))
.saturating_add(
self.0
.iter()
.map(|(irrep, _)| std::mem::size_of_val(irrep.weight.as_ref()))
.sum::<usize>(),
)
}
}
impl crate::cache::CacheKeyCharge for Irrep {
fn key_bytes(&self) -> usize {
std::mem::size_of::<Self>().saturating_add(std::mem::size_of_val(self.weight.as_ref()))
}
}
impl Irrep {
pub fn from_weight(weight: &[i64]) -> Result<Self, SunError> {
if weight.is_empty() {
return Err(SunError::EmptyLabel);
}
for w in weight.windows(2) {
if w[0] < w[1] {
return Err(SunError::NotNonincreasing {
weight: weight.to_vec(),
});
}
}
let last = weight[weight.len() - 1];
let norm: Box<[i64]> = weight.iter().map(|x| x - last).collect();
Ok(Irrep { weight: norm })
}
pub fn from_dynkin(dynkin: &[i64]) -> Result<Self, SunError> {
if dynkin.iter().any(|&a| a < 0) {
return Err(SunError::NegativeDynkin {
dynkin: dynkin.to_vec(),
});
}
let n = dynkin.len() + 1;
let mut w = vec![0i64; n];
for i in (0..n - 1).rev() {
w[i] = w[i + 1] + dynkin[i];
}
Ok(Irrep {
weight: w.into_boxed_slice(),
})
}
pub fn from_dynkin_in(group: &crate::group::GroupId, dynkin: &[i64]) -> Result<Self, SunError> {
let r = match group.root_system {
crate::group::RootSystem::A(r) => r,
other => return Err(SunError::UnsupportedRootSystem { root_system: other }),
};
if dynkin.len() != r {
return Err(SunError::LabelRankMismatch {
expected: r,
got: dynkin.len(),
});
}
if dynkin.iter().any(|&a| a < 0) {
return Err(SunError::NegativeDynkin {
dynkin: dynkin.to_vec(),
});
}
if !group.admits(dynkin) {
return Err(SunError::NotAdmissible {
group: *group,
dynkin: dynkin.to_vec(),
});
}
Self::from_dynkin(dynkin)
}
pub fn trivial(n: usize) -> Result<Self, SunError> {
if n == 0 {
return Err(SunError::EmptyLabel);
}
Ok(Irrep {
weight: vec![0i64; n].into_boxed_slice(),
})
}
pub fn rank(&self) -> usize {
self.weight.len()
}
pub fn weight(&self) -> &[i64] {
&self.weight
}
pub fn dynkin(&self) -> Vec<i64> {
self.weight.windows(2).map(|w| w[0] - w[1]).collect()
}
pub fn dim(&self) -> BigInt {
let w = &self.weight;
let n = w.len();
let mut acc = Ratio::<BigInt>::one();
for k2 in 2..=n {
for k1 in 1..k2 {
let d = BigInt::from(k2 - k1);
let numer = &d + BigInt::from(w[k1 - 1]) - BigInt::from(w[k2 - 1]);
acc *= Ratio::new(numer, d);
}
}
acc.to_integer()
}
pub fn dual(&self) -> Irrep {
let mut d = self.dynkin();
d.reverse();
Irrep::from_dynkin(&d).expect("reversed nonnegative Dynkin label is valid")
}
pub fn patterns(&self) -> Vec<GtPattern> {
let n = self.rank();
gt_enumerate(&self.weight)
.into_iter()
.map(|data| GtPattern {
n,
data: data.into_boxed_slice(),
})
.collect()
}
pub fn creation(&self) -> Vec<Vec<LadderEntry>> {
let n = self.rank();
let pats = self.patterns();
let table: HashMap<&GtPattern, usize> =
pats.iter().enumerate().map(|(i, m)| (m, i)).collect();
let mut result: Vec<Vec<LadderEntry>> = vec![Vec::new(); n.saturating_sub(1)];
for (i, m) in pats.iter().enumerate() {
if n < 2 {
break;
}
for l in 1..=(n - 1) {
for k in 1..=l {
let mkl = m.get(k, l);
let mut coef = Ratio::<BigInt>::from(BigInt::from(-1));
let mut skip = false;
for kp in 1..=(l + 1) {
let base = mkl + (kp as i64) - (k as i64);
let f1 = m.get(kp, l + 1) - base;
coef *= BigInt::from(f1);
if kp < l {
let f2 = m.get(kp, l - 1) - base - 1;
coef *= BigInt::from(f2);
}
if coef.numer().is_zero() {
skip = true; break;
}
if kp <= l && kp != k {
let g = m.get(kp, l) - base;
let den = g * (g - 1);
if den == 0 {
skip = true;
break;
}
coef /= BigInt::from(den);
}
}
if skip || coef.numer().is_zero() {
continue;
}
let mut mp = m.clone();
mp.set(k, l, mkl + 1);
let &j = table.get(&mp).expect(
"GT invariant violated: coef != 0 implies the raised \
pattern is a valid basis member",
);
result[l - 1].push(LadderEntry {
row: j,
col: i,
value: signedroot(&coef),
});
}
}
}
result
}
pub fn annihilation(&self) -> Vec<Vec<LadderEntry>> {
self.creation()
.into_iter()
.map(|mat| {
mat.into_iter()
.map(|e| LadderEntry {
row: e.col,
col: e.row,
value: e.value,
})
.collect()
})
.collect()
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct LadderEntry {
pub row: usize,
pub col: usize,
pub value: SignedSqrtRational,
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct GtPattern {
n: usize,
data: Box<[i64]>,
}
impl GtPattern {
pub fn data(&self) -> &[i64] {
&self.data
}
pub fn rank(&self) -> usize {
self.n
}
#[inline]
pub fn get(&self, k: usize, l: usize) -> i64 {
self.data[Self::flat_index(self.n, k, l)]
}
#[inline]
fn set(&mut self, k: usize, l: usize, v: i64) {
let idx = Self::flat_index(self.n, k, l);
self.data[idx] = v;
}
#[inline]
fn flat_index(n: usize, k: usize, l: usize) -> usize {
(k - 1) + (((l + 1 + n) * (n - l)) >> 1)
}
}
fn gt_enumerate(toprow: &[i64]) -> Vec<Vec<i64>> {
let n = toprow.len();
if n == 1 {
return vec![vec![toprow[0]]];
}
let mut out = Vec::new();
for subrow in subrow_order(toprow) {
for sub in gt_enumerate(&subrow) {
let mut data = toprow.to_vec();
data.extend_from_slice(&sub);
out.push(data);
}
}
out
}
fn subrow_order(toprow: &[i64]) -> Vec<Vec<i64>> {
let m = toprow.len() - 1;
let mut out = Vec::new();
let mut cur = Vec::with_capacity(m);
subrow_rec(0, m, toprow, &mut cur, &mut out);
out
}
fn subrow_rec(j: usize, m: usize, toprow: &[i64], cur: &mut Vec<i64>, out: &mut Vec<Vec<i64>>) {
if j == m {
out.push(cur.clone());
return;
}
for val in toprow[j + 1]..=toprow[j] {
cur.push(val);
subrow_rec(j + 1, m, toprow, cur, out);
cur.pop();
}
}
pub fn directproduct(a: &Irrep, b: &Irrep) -> Result<BTreeMap<Irrep, u32>, SunError> {
let product = shared_directproduct(a, b)?;
#[cfg(test)]
PUBLIC_DIRECTPRODUCT_RECONSTRUCTIONS.with(|count| count.set(count.get() + 1));
Ok(product
.iter()
.map(|(irrep, multiplicity)| (irrep.clone(), multiplicity))
.collect())
}
pub fn shared_directproduct(a: &Irrep, b: &Irrep) -> Result<SunProduct, SunError> {
if a.rank() != b.rank() {
return Err(SunError::RankMismatch {
a: a.rank(),
b: b.rank(),
});
}
let key = SunProductKey::new(a, b);
Ok(crate::cache::cache_sun_product().get_or_compute(key, || directproduct_uncached(a, b)))
}
fn directproduct_uncached(a: &Irrep, b: &Irrep) -> SunProduct {
let (a, b) = if a.dim() <= b.dim() { (a, b) } else { (b, a) };
let n = a.rank();
let mut result: BTreeMap<Irrep, u32> = BTreeMap::new();
for m in a.patterns() {
let mut t: Vec<i64> = b.weight.to_vec();
let mut bad = false;
'scan: for k in 1..=n {
for l in (k..=n).rev() {
let mut bkl = m.get(k, l);
if l > k {
bkl -= m.get(k, l - 1);
}
t[l - 1] += bkl;
if l > 1 && t[l - 2] < t[l - 1] {
bad = true;
break 'scan;
}
}
}
if !bad {
let s = Irrep::from_weight(&t).expect("GT descent yields a valid weight");
*result.entry(s).or_insert(0) += 1;
}
}
SunProduct::from_map(result)
}
fn signedroot(coef: &Ratio<BigInt>) -> SignedSqrtRational {
if coef.is_zero() {
return SignedSqrtRational::zero();
}
let s = if coef.is_negative() {
Ratio::from(BigInt::from(-1))
} else {
Ratio::from(BigInt::from(1))
};
SignedSqrtRational::from_prefactor_radical(s, coef.abs())
}
#[cfg(feature = "cgc-gen")]
pub fn sun_authority_fingerprint() -> &'static [u8] {
b"racah:sun-gt:ref=sunrep-0.4:basis=gt-order:gauge=qrpos-cref:descent=ladder-lstsq:tol=sunrep-tol-tier:epoch=1"
}
#[cfg(test)]
mod tests {
use super::*;
fn irr(dynkin: &[i64]) -> Irrep {
Irrep::from_dynkin(dynkin).unwrap()
}
#[test]
fn weight_normalizes_by_shift() {
let a = Irrep::from_weight(&[3, 1, 0]).unwrap();
let b = Irrep::from_weight(&[5, 3, 2]).unwrap();
assert_eq!(a, b);
assert_eq!(a.weight(), &[3, 1, 0]);
assert_eq!(a.dynkin(), vec![2, 1]);
}
#[test]
fn dynkin_round_trip() {
let s = irr(&[2, 0, 1]); assert_eq!(s.rank(), 4);
let round = Irrep::from_dynkin(&s.dynkin()).unwrap();
assert_eq!(s, round);
}
#[test]
fn malformed_labels_are_typed_errors_not_panics() {
assert_eq!(Irrep::from_weight(&[]), Err(SunError::EmptyLabel));
assert!(matches!(
Irrep::from_weight(&[1, 2, 0]),
Err(SunError::NotNonincreasing { .. })
));
assert!(matches!(
Irrep::from_dynkin(&[1, -1]),
Err(SunError::NegativeDynkin { .. })
));
assert_eq!(Irrep::trivial(0), Err(SunError::EmptyLabel));
}
#[test]
fn weyl_dim_known_su3() {
assert_eq!(irr(&[0, 0]).dim(), BigInt::from(1)); assert_eq!(irr(&[1, 0]).dim(), BigInt::from(3)); assert_eq!(irr(&[0, 1]).dim(), BigInt::from(3)); assert_eq!(irr(&[1, 1]).dim(), BigInt::from(8)); assert_eq!(irr(&[2, 0]).dim(), BigInt::from(6));
assert_eq!(irr(&[3, 0]).dim(), BigInt::from(10));
}
#[test]
fn weyl_dim_large_su2_labels_stay_exact() {
assert_eq!(irr(&[i64::MAX]).dim(), BigInt::from(1_u64) << 63);
assert_eq!(irr(&[i64::MAX - 1]).dim(), BigInt::from(i64::MAX));
}
#[test]
fn dual_is_reverse_dynkin_and_involutive() {
let s = irr(&[1, 0]); assert_eq!(s.dual().dynkin(), vec![0, 1]); for d in [vec![1, 0], vec![2, 1, 0], vec![1, 2, 0, 3]] {
let x = irr(&d);
assert_eq!(x.dual().dual(), x);
assert_eq!(x.dim(), x.dual().dim()); }
}
#[test]
fn patterns_count_equals_dim() {
for d in [
vec![1, 0],
vec![1, 1],
vec![2, 1],
vec![1, 0, 1],
vec![1, 1, 0, 1],
] {
let s = irr(&d);
assert_eq!(BigInt::from(s.patterns().len()), s.dim());
}
}
#[test]
fn su3_fundamental_pattern_order() {
let s = Irrep::from_weight(&[1, 0, 0]).unwrap();
let got: Vec<Vec<i64>> = s.patterns().iter().map(|p| p.data().to_vec()).collect();
assert_eq!(
got,
vec![
vec![1, 0, 0, 0, 0, 0],
vec![1, 0, 0, 1, 0, 0],
vec![1, 0, 0, 1, 0, 1],
]
);
}
#[test]
fn pattern_get_matches_reference_layout() {
let s = Irrep::from_weight(&[1, 0, 0]).unwrap();
let p = &s.patterns()[2]; assert_eq!(p.get(1, 3), 1);
assert_eq!(p.get(2, 3), 0);
assert_eq!(p.get(3, 3), 0);
assert_eq!(p.get(1, 2), 1);
assert_eq!(p.get(2, 2), 0);
assert_eq!(p.get(1, 1), 1);
}
#[test]
fn su3_product_known() {
let dp = directproduct(&irr(&[1, 0]), &irr(&[0, 1])).unwrap();
let mut got: Vec<(Vec<i64>, u32)> = dp.iter().map(|(k, &v)| (k.dynkin(), v)).collect();
got.sort();
assert_eq!(got, vec![(vec![0, 0], 1), (vec![1, 1], 1)]);
let dp = directproduct(&irr(&[1, 0]), &irr(&[1, 0])).unwrap();
let mut got: Vec<(Vec<i64>, u32)> = dp.iter().map(|(k, &v)| (k.dynkin(), v)).collect();
got.sort();
assert_eq!(got, vec![(vec![0, 1], 1), (vec![2, 0], 1)]);
}
#[test]
fn directproduct_rank_mismatch_is_typed_error() {
let su3 = irr(&[1, 0]);
let su4 = irr(&[1, 0, 0]);
assert_eq!(
directproduct(&su3, &su4),
Err(SunError::RankMismatch { a: 3, b: 4 })
);
}
#[test]
fn directproduct_dim_sum_rule() {
for (da, db) in [
(vec![1, 1], vec![1, 1]), (vec![2, 1], vec![1, 2]), (vec![1, 0, 1], vec![1, 1, 0]), (vec![1, 1, 0, 1], vec![0, 1, 1, 0]), ] {
assert_dim_sum_rule(&irr(&da), &irr(&db));
}
}
#[test]
fn directproduct_commutes_and_dual_twist() {
assert_commute_and_dual_twist(&irr(&[2, 1]), &irr(&[1, 1]));
}
#[test]
#[ignore = "release-mode product access collector; not a CI timing gate"]
fn sun_product_private_collector() {
use std::hint::black_box;
use std::time::Instant;
let a = irr(&[1, 1]);
let b = irr(&[1, 1]);
let channel = irr(&[1, 1]);
let product = shared_directproduct(&a, &b).unwrap();
let repetitions = 1_000_000u32;
let started = Instant::now();
for _ in 0..repetitions {
black_box(product.multiplicity(black_box(&channel)));
}
let shared_multiplicity = started.elapsed();
let started = Instant::now();
for _ in 0..repetitions {
black_box(
product
.iter()
.map(|(_, multiplicity)| multiplicity)
.sum::<u32>(),
);
}
let shared_channels = started.elapsed();
let started = Instant::now();
for _ in 0..repetitions {
black_box(
shared_directproduct(black_box(&a), black_box(&b))
.unwrap()
.multiplicity(black_box(&channel)),
);
}
let cached_shared_multiplicity = started.elapsed();
let started = Instant::now();
for _ in 0..repetitions {
black_box(
shared_directproduct(black_box(&a), black_box(&b))
.unwrap()
.iter()
.map(|(_, multiplicity)| multiplicity)
.sum::<u32>(),
);
}
let cached_shared_channels = started.elapsed();
let started = Instant::now();
for _ in 0..repetitions {
let map = directproduct(black_box(&a), black_box(&b)).unwrap();
black_box(map.get(black_box(&channel)).copied().unwrap_or(0));
}
let public_multiplicity = started.elapsed();
let started = Instant::now();
for _ in 0..repetitions {
black_box(
directproduct(black_box(&a), black_box(&b))
.unwrap()
.into_keys()
.collect::<Vec<_>>(),
);
}
let public_channels = started.elapsed();
let ns = |elapsed: std::time::Duration| elapsed.as_nanos() as f64 / repetitions as f64;
eprintln!(
"SUN_PRODUCT_PRIVATE_ACCESS case=su3_8x8 channels={} repetitions={} shared_value_multiplicity_ns={:.3} shared_value_channels_ns={:.3} cached_shared_multiplicity_ns={:.3} cached_shared_channels_ns={:.3} public_multiplicity_ns={:.3} public_channels_ns={:.3}",
product.0.len(),
repetitions,
ns(shared_multiplicity),
ns(shared_channels),
ns(cached_shared_multiplicity),
ns(cached_shared_channels),
ns(public_multiplicity),
ns(public_channels),
);
}
fn assert_dim_sum_rule(a: &Irrep, b: &Irrep) {
let lhs = a.dim() * b.dim();
let rhs: BigInt = directproduct(a, b)
.unwrap()
.iter()
.map(|(c, &m)| c.dim() * BigInt::from(m))
.sum();
assert_eq!(
lhs,
rhs,
"sum rule failed for {:?} ⊗ {:?}",
a.dynkin(),
b.dynkin()
);
}
fn assert_commute_and_dual_twist(a: &Irrep, b: &Irrep) {
assert_eq!(directproduct(a, b), directproduct(b, a));
let dp = directproduct(a, b).unwrap();
let dpd = directproduct(&a.dual(), &b.dual()).unwrap();
let twisted: BTreeMap<Irrep, u32> = dp.iter().map(|(c, &m)| (c.dual(), m)).collect();
assert_eq!(dpd, twisted);
}
#[test]
fn randomized_property_sweep() {
use rand::{Rng, SeedableRng};
let mut rng = rand_chacha::ChaCha8Rng::seed_from_u64(0x5150_4E37_0DEC_0DE5);
let max_dynkin = |n: usize| -> i64 {
match n {
2 => 4,
3 => 3,
4 => 2,
_ => 1,
}
};
let rand_irrep = |rng: &mut rand_chacha::ChaCha8Rng, n: usize| -> Irrep {
let hi = max_dynkin(n);
let dynkin: Vec<i64> = (0..n - 1).map(|_| rng.gen_range(0..=hi)).collect();
Irrep::from_dynkin(&dynkin).unwrap()
};
for n in 2..=5usize {
for _ in 0..50 {
let a = rand_irrep(&mut rng, n);
let b = rand_irrep(&mut rng, n);
assert_dim_sum_rule(&a, &b);
assert_commute_and_dual_twist(&a, &b);
assert_eq!(a.dual().dual(), a); let _ = a.creation(); }
}
}
#[test]
fn su3_adjoint_creation_matches_reference() {
let s = Irrep::from_weight(&[2, 1, 0]).unwrap();
let cr = s.creation();
let key = |mat: &Vec<LadderEntry>| -> Vec<(usize, usize, (i64, i64))> {
let mut v: Vec<_> = mat
.iter()
.map(|e| {
let sq = e.value.signed_square();
(
e.row + 1,
e.col + 1,
(
sq.numer().try_into().unwrap(),
sq.denom().try_into().unwrap(),
),
)
})
.collect();
v.sort();
v
};
assert_eq!(
key(&cr[0]),
vec![
(2, 1, (1, 1)),
(5, 4, (2, 1)),
(6, 5, (2, 1)),
(8, 7, (1, 1)),
]
);
assert_eq!(
key(&cr[1]),
vec![
(3, 2, (3, 2)),
(4, 1, (1, 1)),
(5, 2, (1, 2)),
(7, 3, (3, 2)),
(7, 5, (1, 2)),
(8, 6, (1, 1)),
]
);
}
#[test]
fn annihilation_is_transpose_of_creation() {
let s = Irrep::from_weight(&[2, 1, 0]).unwrap();
let cr = s.creation();
let an = s.annihilation();
for (cm, am) in cr.iter().zip(an.iter()) {
let mut a: Vec<_> = am.iter().map(|e| (e.row, e.col, e.value.clone())).collect();
let mut ct: Vec<_> = cm.iter().map(|e| (e.col, e.row, e.value.clone())).collect();
a.sort_by_key(|x| (x.0, x.1));
ct.sort_by_key(|x| (x.0, x.1));
assert_eq!(a, ct);
}
}
#[test]
fn su2_dim_dual_fusion_match_closed_form() {
for dj in 0..=6i64 {
let j = irr(&[dj]); assert_eq!(j.dim(), BigInt::from(dj + 1)); assert_eq!(j.dual(), j); }
for dj1 in 0..=4i64 {
for dj2 in 0..=4i64 {
let dp = directproduct(&irr(&[dj1]), &irr(&[dj2])).unwrap();
let mut got: Vec<i64> = dp
.iter()
.map(|(c, &m)| {
assert_eq!(m, 1);
c.dynkin()[0]
})
.collect();
got.sort();
let want: Vec<i64> = ((dj1 - dj2).abs()..=(dj1 + dj2)).step_by(2).collect();
assert_eq!(got, want, "SU(2) fusion {dj1}⊗{dj2}");
}
}
}
#[test]
fn su2_creation_matches_closed_form() {
for dj in 1..=6i64 {
let s = irr(&[dj]);
let cr = s.creation();
assert_eq!(cr.len(), 1);
let mut got: Vec<(usize, usize, BigInt)> = cr[0]
.iter()
.map(|e| {
assert_eq!(e.value.sign(), 1);
(e.row, e.col, e.value.signed_square().to_integer())
})
.collect();
got.sort();
let want: Vec<(usize, usize, BigInt)> = (0..dj as usize)
.map(|x| (x + 1, x, BigInt::from((dj - x as i64) * (x as i64 + 1))))
.collect();
assert_eq!(got, want, "SU(2) creation dj={dj}");
}
}
}