use super::*;
fn irr(series: Series, dynkin: &[i64]) -> Irrep {
Irrep::from_dynkin(series, dynkin).unwrap()
}
#[test]
fn dynkin_partition_round_trip() {
for (series, d) in [
(Series::B, vec![1, 0]),
(Series::B, vec![2, 2, 4]),
(Series::C, vec![1, 0, 1]),
(Series::C, vec![3, 1]),
(Series::D, vec![1, 0, 0, 0]),
(Series::D, vec![0, 1, 1]), (Series::D, vec![2, 1, 1, 1]),
] {
let s = irr(series, &d);
assert_eq!(s.dynkin(), d, "round trip {series:?} {d:?}");
assert_eq!(s.rank(), d.len());
}
}
#[test]
fn partition_is_nonincreasing_and_dominant() {
assert_eq!(
irr(Series::B, &[1, 0, 0]).partition().unwrap(),
vec![1, 0, 0]
);
assert_eq!(irr(Series::C, &[1, 0]).partition().unwrap(), vec![1, 0]);
assert_eq!(irr(Series::C, &[2, 0]).partition().unwrap(), vec![2, 0]);
let p = irr(Series::D, &[0, 2, 0]).partition().unwrap();
let q = irr(Series::D, &[0, 0, 2]).partition().unwrap();
assert_eq!(p, vec![1, 1, -1]);
assert_eq!(q, vec![1, 1, 1]);
}
#[test]
fn from_dynkin_in_rejects_a_foreign_group_and_a_wrong_length_label() {
use crate::group::{GlobalForm, GroupId, RootSystem};
let a3 = GroupId {
root_system: RootSystem::A(3),
form: GlobalForm::SimplyConnected,
};
assert_eq!(
Irrep::from_dynkin_in(&a3, &[1, 0, 0]),
Err(BcdError::UnsupportedRootSystem {
root_system: RootSystem::A(3)
})
);
assert_eq!(
Irrep::from_dynkin_in(&GroupId::spin(7).unwrap(), &[1, 0]),
Err(BcdError::RankMismatch {
expected: 3,
got: 2
})
);
assert_eq!(
Irrep::from_dynkin_in(&GroupId::spin(7).unwrap(), &[]),
Err(BcdError::EmptyLabel)
);
}
#[test]
fn low_rank_redirect_is_form_aware() {
use crate::group::GroupId;
let redirect = |g, d: &[i64]| match Irrep::from_dynkin_in(&g, d) {
Err(BcdError::ExcludedRank { redirect, .. }) => redirect,
other => panic!("expected ExcludedRank, got {other:?}"),
};
assert_eq!(
redirect(GroupId::spin(3).unwrap(), &[1]),
"use SU(2) instead"
);
assert_eq!(
redirect(GroupId::so(3).unwrap(), &[2]),
"use SU(2) with integer j only instead"
);
assert_eq!(
redirect(GroupId::spin(4).unwrap(), &[0, 0]),
"use SU(2)×SU(2) instead"
);
assert_eq!(
redirect(GroupId::so(4).unwrap(), &[0, 0]),
"use SU(2)×SU(2) with j₁+j₂ integer instead"
);
}
#[test]
fn excluded_low_ranks_are_typed_errors_with_redirection() {
assert!(matches!(
Irrep::from_dynkin(Series::B, &[2]),
Err(BcdError::ExcludedRank {
series: Series::B,
rank: 1,
redirect
}) if redirect.contains("SU(2)")
));
assert!(matches!(
Irrep::from_dynkin(Series::C, &[1]),
Err(BcdError::ExcludedRank {
series: Series::C,
rank: 1,
redirect
}) if redirect.contains("SU(2)")
));
assert!(matches!(
Irrep::from_dynkin(Series::D, &[0, 0]),
Err(BcdError::ExcludedRank {
series: Series::D,
rank: 2,
redirect
}) if redirect.contains("SU(2)×SU(2)")
));
}
#[test]
fn malformed_and_spinor_labels_are_typed_errors() {
assert_eq!(
Irrep::from_dynkin(Series::B, &[]),
Err(BcdError::EmptyLabel)
);
assert!(matches!(
Irrep::from_dynkin(Series::C, &[1, -1]),
Err(BcdError::NegativeDynkin { .. })
));
assert!(matches!(
Irrep::from_dynkin(Series::B, &[0, 1]),
Err(BcdError::NotAdmissible { .. })
));
assert!(Irrep::from_dynkin(Series::D, &[1, 0, 0]).is_ok()); assert!(Irrep::from_dynkin(Series::D, &[0, 1, 1]).is_ok()); assert!(matches!(
Irrep::from_dynkin(Series::D, &[0, 1, 2]),
Err(BcdError::NotAdmissible { .. }) ));
assert!(Irrep::from_dynkin(Series::C, &[1, 1]).is_ok());
}
#[test]
fn dim_anchors_b2_so5() {
let d = |a: &[i64]| irr(Series::B, a).dim();
assert_eq!(d(&[0, 0]), BigInt::from(1));
assert_eq!(d(&[1, 0]), BigInt::from(5)); assert_eq!(d(&[0, 2]), BigInt::from(10)); assert_eq!(d(&[2, 0]), BigInt::from(14)); assert_eq!(d(&[1, 2]), BigInt::from(35)); assert_eq!(d(&[2, 2]), BigInt::from(81)); }
#[test]
fn dim_anchors_c2_sp4() {
let d = |a: &[i64]| irr(Series::C, a).dim();
assert_eq!(d(&[0, 0]), BigInt::from(1));
assert_eq!(d(&[1, 0]), BigInt::from(4)); assert_eq!(d(&[0, 1]), BigInt::from(5));
assert_eq!(d(&[2, 0]), BigInt::from(10)); assert_eq!(d(&[1, 1]), BigInt::from(16));
}
#[test]
fn dim_anchors_d_series() {
assert_eq!(irr(Series::D, &[1, 0, 0, 0]).dim(), BigInt::from(8));
assert_eq!(irr(Series::D, &[0, 1, 0, 0]).dim(), BigInt::from(28)); assert_eq!(irr(Series::D, &[2, 0, 0, 0]).dim(), BigInt::from(35)); assert_eq!(irr(Series::D, &[1, 0, 0]).dim(), BigInt::from(6));
assert_eq!(irr(Series::D, &[0, 1, 1]).dim(), BigInt::from(15)); assert_eq!(irr(Series::D, &[2, 0, 0]).dim(), BigInt::from(20));
}
#[test]
fn dual_is_involution_and_preserves_dim() {
for (series, d) in [
(Series::B, vec![1, 2]),
(Series::B, vec![2, 0, 2]),
(Series::C, vec![1, 1]),
(Series::C, vec![2, 0, 1]),
(Series::D, vec![1, 0, 0]),
(Series::D, vec![1, 1, 3]), (Series::D, vec![1, 0, 1, 1]), ] {
let s = irr(series, &d);
assert_eq!(s.dual().dual(), s, "involution {series:?} {d:?}");
assert_eq!(s.dim(), s.dual().dim(), "dim {series:?} {d:?}");
}
}
#[test]
fn dual_bc_and_d_even_self_dual() {
for (series, d) in [
(Series::B, vec![2, 4]),
(Series::C, vec![1, 3]),
(Series::D, vec![1, 1, 0, 2]), ] {
let s = irr(series, &d);
assert_eq!(s.dual(), s, "self-dual {series:?} {d:?}");
}
}
#[test]
fn dual_d_odd_swaps_last_two_dynkin() {
let s = irr(Series::D, &[0, 1, 3]); assert_eq!(s.dual().dynkin(), vec![0, 3, 1]);
assert_eq!(irr(Series::D, &[1, 0, 0]).dual().dynkin(), vec![1, 0, 0]);
assert_ne!(irr(Series::D, &[0, 2, 0]), irr(Series::D, &[0, 0, 2]));
assert_eq!(
irr(Series::D, &[0, 2, 0]).dual(),
irr(Series::D, &[0, 0, 2])
);
}
#[test]
fn frobenius_schur_by_series() {
for d in [vec![0, 0], vec![1, 0], vec![2, 2], vec![1, 2]] {
assert_eq!(irr(Series::B, &d).frobenius_schur(), 1, "B {d:?}");
}
assert_eq!(irr(Series::C, &[1, 0]).frobenius_schur(), -1);
assert_eq!(irr(Series::C, &[2, 0]).frobenius_schur(), 1);
assert_eq!(irr(Series::C, &[0, 1]).frobenius_schur(), 1);
assert_eq!(irr(Series::C, &[1, 0, 0]).frobenius_schur(), -1);
assert_eq!(irr(Series::C, &[0, 0, 1]).frobenius_schur(), -1);
assert_eq!(irr(Series::D, &[1, 0, 0, 0]).frobenius_schur(), 1);
assert_eq!(irr(Series::D, &[0, 2, 0]).frobenius_schur(), 0);
assert_eq!(irr(Series::D, &[1, 0, 0]).frobenius_schur(), 1);
}
#[test]
fn weight_multiplicities_sum_to_dim() {
for (series, d) in [
(Series::B, vec![1, 0]),
(Series::B, vec![2, 2]), (Series::C, vec![1, 0]),
(Series::C, vec![2, 0]),
(Series::D, vec![1, 0, 0]),
(Series::D, vec![0, 1, 1]),
(Series::D, vec![1, 0, 0, 0]),
] {
let s = irr(series, &d);
let total: u64 = s
.weight_multiplicities()
.expect("tensor irrep")
.iter()
.map(|(mu, &m)| m * weyl_orbit(series, mu).len() as u64)
.sum();
assert_eq!(
BigInt::from(total),
s.dim(),
"weight count {series:?} {d:?}"
);
}
}
#[test]
fn adjoint_has_rank_zero_weight() {
let s = irr(Series::B, &[0, 2]);
assert_eq!(
s.weight_multiplicities().unwrap().get(&vec![0, 0]),
Some(&2)
);
}
fn decomp(a: &Irrep, b: &Irrep) -> Vec<(Vec<i64>, u32)> {
let mut v: Vec<(Vec<i64>, u32)> = directproduct(a, b)
.unwrap()
.into_iter()
.map(|(c, n)| (c.dynkin(), n))
.collect();
v.sort();
v
}
#[test]
fn so5_vector_squared() {
let v = irr(Series::B, &[1, 0]);
let got = decomp(&v, &v);
assert_eq!(got, vec![(vec![0, 0], 1), (vec![0, 2], 1), (vec![2, 0], 1)]);
}
#[test]
fn sp4_fundamental_squared() {
let v = irr(Series::C, &[1, 0]);
let got = decomp(&v, &v);
assert_eq!(got, vec![(vec![0, 0], 1), (vec![0, 1], 1), (vec![2, 0], 1)]);
}
#[test]
fn so8_vector_squared() {
let v = irr(Series::D, &[1, 0, 0, 0]);
let got = decomp(&v, &v);
assert_eq!(
got,
vec![
(vec![0, 0, 0, 0], 1),
(vec![0, 1, 0, 0], 1),
(vec![2, 0, 0, 0], 1),
]
);
}
#[test]
fn so6_vector_squared() {
let v = irr(Series::D, &[1, 0, 0]);
let got = decomp(&v, &v);
assert_eq!(
got,
vec![(vec![0, 0, 0], 1), (vec![0, 1, 1], 1), (vec![2, 0, 0], 1)]
);
}
fn dim_sum_rule(a: &Irrep, b: &Irrep) {
let lhs = a.dim() * b.dim();
let rhs: BigInt = directproduct(a, b)
.unwrap()
.into_iter()
.map(|(c, n)| c.dim() * BigInt::from(n))
.sum();
assert_eq!(lhs, rhs, "dim-sum rule {:?} ⊗ {:?}", a.dynkin(), b.dynkin());
}
fn product_symmetry(a: &Irrep, b: &Irrep) {
assert_eq!(
directproduct(a, b).unwrap(),
directproduct(b, a).unwrap(),
"N^c_ab == N^c_ba for {:?}, {:?}",
a.dynkin(),
b.dynkin()
);
}
fn dual_twist(a: &Irrep, b: &Irrep) {
let dp = directproduct(a, b).unwrap();
let dpd = directproduct(&a.dual(), &b.dual()).unwrap();
let twisted: BTreeMap<Irrep, u32> = dp.into_iter().map(|(c, n)| (c.dual(), n)).collect();
assert_eq!(
dpd,
twisted,
"dual twist {:?}, {:?}",
a.dynkin(),
b.dynkin()
);
}
#[test]
fn group_mismatch_is_typed_error() {
let b2 = irr(Series::B, &[1, 0]);
let c2 = irr(Series::C, &[1, 0]);
let b3 = irr(Series::B, &[1, 0, 0]);
assert!(matches!(
directproduct(&b2, &c2),
Err(BcdError::GroupMismatch { .. })
));
assert!(matches!(
directproduct(&b2, &b3),
Err(BcdError::GroupMismatch { .. })
));
}
#[test]
fn randomized_property_sweep() {
use rand::{Rng, SeedableRng};
let mut rng = rand_chacha::ChaCha8Rng::seed_from_u64(0x0BC0_DE19_5150_4E37);
let max_label = |r: usize| -> i64 {
match r {
2 => 3,
3 => 2,
_ => 1,
}
};
for series in [Series::B, Series::C, Series::D] {
let min_r = series.min_rank();
for r in min_r..=4usize {
let hi = max_label(r);
let rand_irrep = |rng: &mut rand_chacha::ChaCha8Rng| -> Irrep {
loop {
let d: Vec<i64> = (0..r).map(|_| rng.gen_range(0..=hi)).collect();
if let Ok(s) = Irrep::from_dynkin(series, &d) {
return s; }
}
};
for _ in 0..30 {
let a = rand_irrep(&mut rng);
let b = rand_irrep(&mut rng);
dim_sum_rule(&a, &b);
product_symmetry(&a, &b);
dual_twist(&a, &b);
assert_eq!(a.dual().dual(), a);
}
}
}
}
#[test]
fn external_oracle_fixtures() {
let path = concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/fixtures/bcd_fixtures.json"
);
let raw = std::fs::read_to_string(path)
.expect("fixture file present (run tools/gen_bcd_fixtures.{py,jl} first)");
check_fixtures(&raw);
}
fn check_fixtures(raw: &str) {
for line in raw.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with('#') || line.starts_with("---") {
continue;
}
let parts: Vec<&str> = line.split('|').map(|s| s.trim()).collect();
assert!(parts.len() >= 5, "malformed fixture line: {line}");
let head: Vec<&str> = parts[0].split_whitespace().collect();
let series = match head[0] {
"B" => Series::B,
"C" => Series::C,
"D" => Series::D,
other => panic!("unknown series {other}"),
};
let ints = |s: &str| -> Vec<i64> {
s.split(',')
.filter(|x| !x.is_empty())
.map(|x| x.parse().unwrap())
.collect()
};
let a = Irrep::from_dynkin(series, &ints(parts[1])).unwrap();
let b = Irrep::from_dynkin(series, &ints(parts[2])).unwrap();
let dims: Vec<&str> = parts[3].split_whitespace().collect();
assert_eq!(a.dim().to_string(), dims[0], "dim_a for {line}");
assert_eq!(b.dim().to_string(), dims[1], "dim_b for {line}");
let mut want: BTreeMap<Vec<i64>, u32> = BTreeMap::new();
for tok in parts[4].split_whitespace() {
let (c, n) = tok.split_once(':').expect("c:n token");
want.insert(ints(c), n.parse().unwrap());
}
let got: BTreeMap<Vec<i64>, u32> = directproduct(&a, &b)
.unwrap()
.into_iter()
.map(|(c, n)| (c.dynkin(), n))
.collect();
assert_eq!(got, want, "decomposition mismatch for {line}");
}
}