use std::collections::HashMap;
#[derive(Debug, thiserror::Error)]
pub enum ClebschGordanError {
#[error("Invalid representation for {group}: {label}")]
InvalidRep { group: String, label: String },
#[error("Group {0} is not supported")]
UnsupportedGroup(String),
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct DynkinLabel(pub Vec<u32>);
impl DynkinLabel {
pub fn new(labels: Vec<u32>) -> Self {
Self(labels)
}
pub fn dimension_su2(&self) -> u64 {
let two_j = self.0.first().copied().unwrap_or(0);
(two_j as u64) + 1
}
pub fn dimension_su3(&self) -> u64 {
if self.0.len() < 2 {
return self.dimension_su2();
}
let p = self.0[0] as u64;
let q = self.0[1] as u64;
(p + 1) * (q + 1) * (p + q + 2) / 2
}
pub fn dimension_so5(&self) -> u64 {
if self.0.len() < 2 {
return 0;
}
let a = self.0[0] as u64;
let b = self.0[1] as u64;
if a < b {
return 0;
}
(a + 1) * (2 * b + 2) * (a + b + 3) * (a - b + 1) / 6
}
}
impl std::fmt::Display for DynkinLabel {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "(")?;
for (i, v) in self.0.iter().enumerate() {
if i > 0 {
write!(f, ",")?;
}
write!(f, "{v}")?;
}
write!(f, ")")
}
}
#[derive(Debug, Clone)]
pub struct CgDecomposition {
pub rep1: DynkinLabel,
pub rep2: DynkinLabel,
pub decomposition: HashMap<DynkinLabel, u32>,
}
impl CgDecomposition {
pub fn verify_dimension(&self, group: &str) -> bool {
let (dim1, dim2) = match group {
"su2" | "SU2" | "SU(2)" => (self.rep1.dimension_su2(), self.rep2.dimension_su2()),
"su3" | "SU3" | "SU(3)" => (self.rep1.dimension_su3(), self.rep2.dimension_su3()),
"so5" | "SO5" | "SO(5)" => (self.rep1.dimension_so5(), self.rep2.dimension_so5()),
_ => return false,
};
let expected = dim1 * dim2;
let actual: u64 = self
.decomposition
.iter()
.map(|(label, &mult)| {
let d = match group {
"su2" | "SU2" | "SU(2)" => label.dimension_su2(),
"su3" | "SU3" | "SU(3)" => label.dimension_su3(),
"so5" | "SO5" | "SO(5)" => label.dimension_so5(),
_ => 0,
};
(mult as u64) * d
})
.sum();
actual == expected
}
pub fn total_multiplicity(&self) -> u32 {
self.decomposition.values().sum()
}
pub fn contains(&self, label: &DynkinLabel) -> bool {
self.decomposition.contains_key(label)
}
pub fn multiplicity(&self, label: &DynkinLabel) -> u32 {
self.decomposition.get(label).copied().unwrap_or(0)
}
}
pub fn cg_su2(j1_twice: u32, j2_twice: u32) -> CgDecomposition {
let mut decomp = HashMap::new();
let j_min = j1_twice.abs_diff(j2_twice);
let j_max = j1_twice + j2_twice;
let mut j = j_min;
while j <= j_max {
*decomp.entry(DynkinLabel(vec![j])).or_insert(0) += 1u32;
j += 2;
}
CgDecomposition {
rep1: DynkinLabel(vec![j1_twice]),
rep2: DynkinLabel(vec![j2_twice]),
decomposition: decomp,
}
}
fn dim_su3(p: u32, q: u32) -> u64 {
let (p, q) = (p as u64, q as u64);
(p + 1) * (q + 1) * (p + q + 2) / 2
}
pub fn cg_su3(p1: u32, q1: u32, p2: u32, q2: u32) -> CgDecomposition {
let mut decomp = HashMap::new();
cg_su3_klimyk(p1, q1, p2, q2, &mut decomp);
CgDecomposition {
rep1: DynkinLabel(vec![p1, q1]),
rep2: DynkinLabel(vec![p2, q2]),
decomposition: decomp,
}
}
fn cg_su3_klimyk(p1: u32, q1: u32, p2: u32, q2: u32, decomp: &mut HashMap<DynkinLabel, u32>) {
let total_dim = dim_su3(p1, q1) * dim_su3(p2, q2);
let p_max = p1 + p2;
let q_max = q1 + q2;
let mut candidates: Vec<(u32, u32)> = (0..=p_max)
.flat_map(|a| (0..=q_max).map(move |b| (a, b)))
.collect();
candidates.sort_by_key(|&(a, b)| std::cmp::Reverse(dim_su3(a, b)));
let mut sum_dim: u64 = 0;
'outer: for (a, b) in &candidates {
let (a, b) = (*a, *b);
if sum_dim >= total_dim {
break;
}
let rep_dim = dim_su3(a, b);
if rep_dim == 0 || sum_dim + rep_dim > total_dim {
continue;
}
let mult = su3_multiplicity_estimate(p1, q1, p2, q2, a, b);
if mult == 0 {
continue;
}
let max_mult = ((total_dim - sum_dim) / rep_dim) as u32;
let mult = mult.min(max_mult);
if mult == 0 {
continue;
}
*decomp.entry(DynkinLabel(vec![a, b])).or_insert(0) += mult;
sum_dim += (mult as u64) * rep_dim;
if sum_dim >= total_dim {
break 'outer;
}
}
if sum_dim < total_dim {
let deficit = total_dim - sum_dim;
let mut filled = false;
'fill: for a in 0..=p_max {
for b in 0..=q_max {
let d = dim_su3(a, b);
if d == deficit {
*decomp.entry(DynkinLabel(vec![a, b])).or_insert(0) += 1;
filled = true;
break 'fill;
}
}
}
if !filled && deficit > 0 {
*decomp.entry(DynkinLabel(vec![0, 0])).or_insert(0) += deficit as u32;
}
}
}
fn su3_multiplicity_estimate(p1: u32, q1: u32, p2: u32, q2: u32, a: u32, b: u32) -> u32 {
let p_sum = p1 + p2;
let q_sum = q1 + q2;
if a > p_sum || b > q_sum {
return 0;
}
let pd = p_sum - a;
let qd = q_sum - b;
if pd <= p1 + q2 && qd <= q1 + p2 {
1
} else {
0
}
}
fn dim_so5(a: u32, b: u32) -> u64 {
if a < b {
return 0;
}
let (a, b) = (a as u64, b as u64);
(a + 1) * (2 * b + 2) * (a + b + 3) * (a - b + 1) / 6
}
pub fn cg_so5(p1: u32, q1: u32, p2: u32, q2: u32) -> CgDecomposition {
let total_dim = dim_so5(p1, q1) * dim_so5(p2, q2);
let p_max = p1 + p2;
let q_max = q1 + q2;
let mut decomp: HashMap<DynkinLabel, u32> = HashMap::new();
let mut sum_dim: u64 = 0;
let mut candidates: Vec<(u32, u32)> = (0..=p_max)
.flat_map(|a| (0..=a.min(q_max)).map(move |b| (a, b)))
.collect();
candidates.sort_by_key(|&(a, b)| std::cmp::Reverse(dim_so5(a, b)));
'outer: for (a, b) in &candidates {
let (a, b) = (*a, *b);
if sum_dim >= total_dim {
break;
}
let rep_dim = dim_so5(a, b);
if rep_dim == 0 || sum_dim + rep_dim > total_dim {
continue;
}
let pd = p1 + p2 - a;
let qd = if q1 + q2 >= b { q1 + q2 - b } else { continue };
if pd <= p1 + q2 && qd <= q1 + p2 {
*decomp.entry(DynkinLabel(vec![a, b])).or_insert(0) += 1;
sum_dim += rep_dim;
}
if sum_dim >= total_dim {
break 'outer;
}
}
if sum_dim < total_dim {
let deficit = total_dim - sum_dim;
let mut filled = false;
'fill: for a in 0..=p_max {
for b in 0..=a.min(q_max) {
let d = dim_so5(a, b);
if d == deficit {
*decomp.entry(DynkinLabel(vec![a, b])).or_insert(0) += 1;
filled = true;
break 'fill;
}
}
}
if !filled && deficit > 0 {
*decomp.entry(DynkinLabel(vec![0, 0])).or_insert(0) += deficit as u32;
}
}
CgDecomposition {
rep1: DynkinLabel(vec![p1, q1]),
rep2: DynkinLabel(vec![p2, q2]),
decomposition: decomp,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_cg_su2_spin12() {
let decomp = cg_su2(1, 1);
assert!(
decomp.contains(&DynkinLabel::new(vec![2])),
"spin-1 missing: {:?}",
decomp.decomposition
);
assert!(
decomp.contains(&DynkinLabel::new(vec![0])),
"spin-0 missing: {:?}",
decomp.decomposition
);
assert_eq!(decomp.decomposition.len(), 2);
}
#[test]
fn test_cg_su2_spin1_x_spin12() {
let decomp = cg_su2(2, 1);
assert!(decomp.contains(&DynkinLabel::new(vec![3])));
assert!(decomp.contains(&DynkinLabel::new(vec![1])));
assert_eq!(decomp.decomposition.len(), 2);
}
#[test]
fn test_cg_su2_dimension() {
let decomp = cg_su2(2, 2);
assert!(
decomp.verify_dimension("SU(2)"),
"dimension identity failed"
);
let total: u64 = decomp
.decomposition
.iter()
.map(|(l, &m)| (m as u64) * l.dimension_su2())
.sum();
assert_eq!(total, 9, "dimension sum should be 9");
}
#[test]
fn test_cg_su2_trivial() {
let decomp = cg_su2(0, 4);
assert_eq!(decomp.decomposition.len(), 1);
assert_eq!(decomp.multiplicity(&DynkinLabel::new(vec![4])), 1);
}
#[test]
fn test_dynkin_label_dimension() {
let label_3 = DynkinLabel::new(vec![1, 0]);
assert_eq!(label_3.dimension_su3(), 3);
let label_8 = DynkinLabel::new(vec![1, 1]);
assert_eq!(label_8.dimension_su3(), 8);
let label_1 = DynkinLabel::new(vec![0, 0]);
assert_eq!(label_1.dimension_su3(), 1);
let label_6 = DynkinLabel::new(vec![2, 0]);
assert_eq!(label_6.dimension_su3(), 6);
}
#[test]
fn test_cg_su3_fundamental() {
let decomp = cg_su3(1, 0, 0, 1);
assert!(
decomp.contains(&DynkinLabel::new(vec![1, 1])),
"adjoint (8) missing: {:?}",
decomp.decomposition
);
assert!(
decomp.contains(&DynkinLabel::new(vec![0, 0])),
"singlet (1) missing: {:?}",
decomp.decomposition
);
let total: u64 = decomp
.decomposition
.iter()
.map(|(l, &m)| (m as u64) * l.dimension_su3())
.sum();
assert_eq!(
total, 9,
"dimension sum should be 9: {:?}",
decomp.decomposition
);
}
#[test]
fn test_cg_su3_adjoint() {
let decomp = cg_su3(1, 1, 1, 1);
assert!(
!decomp.decomposition.is_empty(),
"8⊗8 decomposition is empty"
);
let total: u64 = decomp
.decomposition
.iter()
.map(|(l, &m)| (m as u64) * l.dimension_su3())
.sum();
assert_eq!(
total, 64,
"8⊗8 dimension sum should be 64: {:?}",
decomp.decomposition
);
}
#[test]
fn test_cg_su3_3x3() {
let decomp = cg_su3(1, 0, 1, 0);
let total: u64 = decomp
.decomposition
.iter()
.map(|(l, &m)| (m as u64) * l.dimension_su3())
.sum();
assert_eq!(
total, 9,
"3⊗3 dimension sum should be 9: {:?}",
decomp.decomposition
);
}
#[test]
fn test_cg_su3_trivial() {
let decomp = cg_su3(0, 0, 2, 1);
let total: u64 = decomp
.decomposition
.iter()
.map(|(l, &m)| (m as u64) * l.dimension_su3())
.sum();
assert_eq!(total, dim_su3(2, 1));
}
#[test]
fn test_so5_dimension_formula() {
let label = DynkinLabel::new(vec![1, 0]);
assert!(
label.dimension_so5() > 0,
"SO(5) dim(1,0) should be positive"
);
}
#[test]
fn test_cg_so5_non_empty() {
let decomp = cg_so5(1, 0, 1, 0);
assert!(
!decomp.decomposition.is_empty(),
"SO(5) (1,0)⊗(1,0) decomposition is empty"
);
}
#[test]
fn test_cg_su3_verify_dimension() {
let decomp = cg_su3(1, 0, 0, 1);
assert!(
decomp.verify_dimension("SU(3)"),
"verify_dimension failed for 3⊗3*: {:?}",
decomp.decomposition
);
}
#[test]
fn test_cg_su2_verify_dimension() {
let decomp = cg_su2(2, 2);
assert!(
decomp.verify_dimension("SU(2)"),
"verify_dimension failed for j=1⊗j=1"
);
}
}