#![cfg(feature = "cgc-gen")]
use racah::bcd::{
check_commutators, check_f_unitarity, check_hexagon, check_pentagon, defining_seed,
directproduct, spinor_seeds, BcdError, CanonicalCatalog, CatalogCgc, Irrep, Series,
};
use racah::group::GroupId;
const FAMILIES: &[(Series, usize, usize)] = &[
(Series::B, 2, 5),
(Series::B, 3, 7),
(Series::D, 3, 6),
(Series::D, 4, 8),
];
fn ir(n: usize, dynkin: &[i64]) -> Irrep {
Irrep::from_dynkin_in(&GroupId::spin(n).unwrap(), dynkin).expect("Spin(N) label")
}
#[test]
fn spinor_seed_commutators_close() {
for &(series, r, _) in FAMILIES {
for (label, seed) in spinor_seeds(series, r).unwrap() {
check_commutators(&seed)
.unwrap_or_else(|e| panic!("{series:?}_{r} spinor {label:?}: {e}"));
}
}
}
#[test]
fn spinor_seed_structure_constants_match_the_defining_seed() {
for &(series, r, _) in FAMILIES {
let reference = check_commutators(&defining_seed(series, r).unwrap()).unwrap();
for (label, seed) in spinor_seeds(series, r).unwrap() {
let got = check_commutators(&seed).unwrap();
assert_eq!(
got.cartan_coeffs, reference.cartan_coeffs,
"{series:?}_{r} spinor {label:?}: [Sp,Sp†] coefficients"
);
assert_eq!(
got.root_weights, reference.root_weights,
"{series:?}_{r} spinor {label:?}: root components"
);
}
}
}
#[test]
fn spinor_seed_inventory_is_one_for_b_two_for_d_none_for_c() {
assert!(spinor_seeds(Series::C, 3).unwrap().is_empty());
for &(series, r, _) in FAMILIES {
let seeds = spinor_seeds(series, r).unwrap();
let (want_count, want_dim) = match series {
Series::B => (1, 1usize << r),
_ => (2, 1usize << (r - 1)),
};
assert_eq!(seeds.len(), want_count, "{series:?}_{r} seed count");
for (_, seed) in &seeds {
assert_eq!(seed.dim(), want_dim, "{series:?}_{r} carrier dimension");
}
}
}
#[test]
fn spinor_seeds_reject_the_excluded_low_ranks() {
assert!(matches!(
spinor_seeds(Series::B, 1),
Err(BcdError::ExcludedRank { redirect, .. }) if redirect.contains("SU(2)")
));
assert!(matches!(
spinor_seeds(Series::D, 2),
Err(BcdError::ExcludedRank { redirect, .. }) if redirect.contains("SU(2)×SU(2)")
));
}
#[test]
fn spinor_labels_are_admissible_only_in_the_cover() {
for &(series, r, n) in FAMILIES {
for (label, _) in spinor_seeds(series, r).unwrap() {
assert!(
Irrep::from_dynkin_in(&GroupId::spin(n).unwrap(), &label).is_ok(),
"Spin({n}) must admit {label:?}"
);
assert!(
matches!(
Irrep::from_dynkin_in(&GroupId::so(n).unwrap(), &label),
Err(BcdError::NotAdmissible { .. })
),
"SO({n}) must reject {label:?}"
);
assert!(
matches!(
Irrep::from_dynkin(series, &label),
Err(BcdError::NotAdmissible { .. })
),
"the published constructor must still reject {label:?}"
);
}
}
}
#[test]
fn spinor_weights_are_half_integers_in_the_doubled_encoding() {
let s = ir(7, &[0, 0, 1]);
assert_eq!(s.two_partition(), &[1, 1, 1]);
assert!(s.is_spinor());
assert_eq!(s.partition(), None);
assert_eq!(s.weight_multiplicities(), None);
let v = ir(7, &[1, 0, 0]);
assert_eq!(v.two_partition(), &[2, 0, 0]);
assert!(!v.is_spinor());
assert_eq!(v.partition(), Some(vec![1, 0, 0]));
}
#[test]
fn spinor_dimensions_are_the_clifford_module_dimensions() {
for &(series, r, n) in FAMILIES {
let want: u64 = match series {
Series::B => 1 << r,
_ => 1 << (r - 1),
};
for (label, _) in spinor_seeds(series, r).unwrap() {
assert_eq!(
ir(n, &label).dim(),
want.into(),
"dim of Spin({n}) {label:?}"
);
}
}
}
#[test]
fn spinor_frobenius_schur_is_the_n_mod_8_reality_type() {
let cases: &[(usize, &[i64], i32)] = &[
(5, &[0, 1], -1), (7, &[0, 0, 1], 1), (9, &[0, 0, 0, 1], 1), (11, &[0, 0, 0, 0, 1], -1), (6, &[0, 0, 1], 0), (8, &[0, 0, 0, 1], 1), (10, &[0, 0, 0, 0, 1], 0), (12, &[0, 0, 0, 0, 0, 1], -1), ];
for &(n, dynkin, want) in cases {
assert_eq!(
ir(n, dynkin).frobenius_schur(),
want,
"FS of Spin({n}) {dynkin:?}"
);
}
}
#[test]
fn half_spinor_duality_follows_the_d_series_chirality_rule() {
let s = ir(6, &[0, 0, 1]);
assert_eq!(s.dual().dynkin(), vec![0, 1, 0], "Spin(6) 4* = 4bar");
let s8 = ir(8, &[0, 0, 0, 1]);
assert_eq!(
s8.dual().dynkin(),
vec![0, 0, 0, 1],
"Spin(8) 8_s self-dual"
);
}
#[test]
fn spinor_fusion_matches_the_textbook_products() {
let channels = |a: &Irrep, b: &Irrep| -> Vec<(Vec<i64>, u32)> {
let mut v: Vec<(Vec<i64>, u32)> = directproduct(a, b)
.unwrap()
.into_iter()
.map(|(k, m)| (k.dynkin(), m))
.collect();
v.sort();
v
};
assert_eq!(
channels(&ir(7, &[0, 0, 1]), &ir(7, &[0, 0, 1])),
vec![
(vec![0, 0, 0], 1),
(vec![0, 0, 2], 1),
(vec![0, 1, 0], 1),
(vec![1, 0, 0], 1),
]
);
assert_eq!(
channels(&ir(8, &[0, 0, 0, 1]), &ir(8, &[1, 0, 0, 0])),
vec![(vec![0, 0, 1, 0], 1), (vec![1, 0, 0, 1], 1)]
);
assert_eq!(
channels(&ir(5, &[0, 1]), &ir(5, &[0, 1])),
vec![(vec![0, 0], 1), (vec![0, 2], 1), (vec![1, 0], 1)]
);
}
fn isometry_residual(c: &CatalogCgc) -> f64 {
let (rows, cols) = c.copy_shape();
let mut worst: f64 = 0.0;
for mu in 0..c.multiplicity() {
let m = c.copy(mu);
for i in 0..cols {
for j in 0..cols {
let dot: f64 = (0..rows).map(|k| m[i * rows + k] * m[j * rows + k]).sum();
let target = if i == j { 1.0 } else { 0.0 };
worst = worst.max((dot - target).abs());
}
}
}
worst
}
#[test]
fn spinor_cgc_columns_are_orthonormal() {
for &(series, r, n) in FAMILIES {
let mut cat = CanonicalCatalog::new(series, r).unwrap();
let spinors: Vec<Irrep> = spinor_seeds(series, r)
.unwrap()
.into_iter()
.map(|(label, _)| ir(n, &label))
.collect();
let s = spinors[0].clone();
for partner in [s.clone(), s.dual()] {
for (c, _) in directproduct(&s, &partner).unwrap() {
let cgc = cat.cgc(&s, &partner, &c).unwrap();
let residual = isometry_residual(&cgc);
assert!(
residual < 1e-10,
"Spin({n}) {:?}⊗{:?}→{:?} is not an isometry: {residual:e}",
s.dynkin(),
partner.dynkin(),
c.dynkin()
);
}
}
}
}
#[test]
fn the_recursion_reaches_a_non_base_spinor() {
let x = ir(5, &[1, 1]);
assert_eq!(x.dim(), 16u32.into());
let mut cat = CanonicalCatalog::new(Series::B, 2).unwrap();
assert_eq!(cat.generators(&x).unwrap().dim(), 16);
}
#[test]
fn the_tensor_bootstrap_never_touches_the_spinor_sector() {
let mut cat = CanonicalCatalog::new(Series::B, 2).unwrap();
let before = cat.len();
let adjoint = Irrep::from_dynkin(Series::B, &[0, 2]).unwrap();
cat.generators(&adjoint).unwrap();
assert!(cat.len() > before, "the adjoint must materialize something");
let spinor = ir(5, &[0, 1]);
let mut probe = CanonicalCatalog::new(Series::B, 2).unwrap();
probe.generators(&adjoint).unwrap();
assert_eq!(
probe.len(),
cat.len(),
"materialization must be deterministic"
);
assert!(
probe.cgc(&spinor, &spinor, &adjoint).is_ok(),
"the spinor product is still available on demand"
);
}
#[test]
fn spinor_f_symbols_are_unitary_on_spin6() {
let mut cat = CanonicalCatalog::new(Series::D, 3).unwrap();
let s = ir(6, &[0, 0, 1]);
let sb = ir(6, &[0, 1, 0]);
check_f_unitarity(&mut cat, &s, &sb, &s, &sb).unwrap();
check_f_unitarity(&mut cat, &s, &s, &s, &s).unwrap();
}
#[test]
fn spinor_pentagon_closes_on_spin6() {
let mut cat = CanonicalCatalog::new(Series::D, 3).unwrap();
let s = ir(6, &[0, 0, 1]);
let sb = ir(6, &[0, 1, 0]);
check_pentagon(&mut cat, &s, &sb, &s, &sb).unwrap();
}
#[test]
fn spinor_hexagon_closes_on_spin6() {
let mut cat = CanonicalCatalog::new(Series::D, 3).unwrap();
let s = ir(6, &[0, 0, 1]);
let sb = ir(6, &[0, 1, 0]);
let v = ir(6, &[1, 0, 0]);
check_hexagon(&mut cat, &s, &sb, &v).unwrap();
}
#[test]
fn spinor_recoupling_closes_through_the_defining_channel_on_spin6() {
let s = ir(6, &[0, 0, 1]);
let mut cat = CanonicalCatalog::new(Series::D, 3).unwrap();
check_pentagon(&mut cat, &s, &s, &s, &s).unwrap();
let mut cat = CanonicalCatalog::new(Series::D, 3).unwrap();
check_hexagon(&mut cat, &s, &s, &s).unwrap();
}