use std::collections::HashMap;
use num_bigint::BigInt;
use super::seeds::spinor_seeds;
use super::sweep::{align_block, decompose, Block, Generators, SweepError};
use super::{defining_seed, directproduct, BcdError, Irrep, Series};
const DEFAULT_MAX_BYTES: usize = 256 << 20;
const TOL_BASIS_COHERENT: f64 = 1.0e-10;
#[derive(Clone, Debug, PartialEq)]
pub enum CatalogError {
WrongGroup {
catalog: (Series, usize),
got: (Series, usize),
},
Label(BcdError),
ZeroFusionChannel {
a: Vec<i64>,
b: Vec<i64>,
c: Vec<i64>,
},
BudgetExceeded {
limit: usize,
needed: usize,
},
Sweep(SweepError),
BasisIncoherent {
irrep: Vec<i64>,
product: (Vec<i64>, Vec<i64>),
residual: f64,
},
NoCanonicalParent {
dynkin: Vec<i64>,
},
}
impl std::fmt::Display for CatalogError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
CatalogError::WrongGroup { catalog, got } => write!(
f,
"irrep of group {got:?} passed to a catalog owning group {catalog:?}"
),
CatalogError::Label(e) => write!(f, "invalid label: {e}"),
CatalogError::ZeroFusionChannel { a, b, c } => write!(
f,
"ill-posed coupling: irrep {c:?} does not appear in {a:?} ⊗ {b:?} (N^c_ab = 0)"
),
CatalogError::BudgetExceeded { limit, needed } => write!(
f,
"byte budget exceeded: request needs {needed} bytes, budget is {limit}"
),
CatalogError::Sweep(e) => write!(f, "sweep failed during materialization: {e}"),
CatalogError::BasisIncoherent {
irrep,
product,
residual,
} => write!(
f,
"irrep {irrep:?} from product {:?}⊗{:?} could not be aligned onto its \
stored canonical basis (post-alignment generator residual {residual:e} > \
coherence tol) — genuinely different frame or a numerically hopeless embedding",
product.0, product.1
),
CatalogError::NoCanonicalParent { dynkin } => write!(
f,
"no admissible canonical parent for irrep {dynkin:?} \
(unreachable by the box-count-first existence theorem)"
),
}
}
}
impl std::error::Error for CatalogError {}
impl From<BcdError> for CatalogError {
fn from(e: BcdError) -> Self {
CatalogError::Label(e)
}
}
impl From<SweepError> for CatalogError {
fn from(e: SweepError) -> Self {
CatalogError::Sweep(e)
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct CatalogCgc {
s1: Irrep,
s2: Irrep,
s3: Irrep,
rows: usize,
d3: usize,
multiplicity: usize,
cols: Vec<f64>,
}
impl CatalogCgc {
pub fn s1(&self) -> &Irrep {
&self.s1
}
pub fn s2(&self) -> &Irrep {
&self.s2
}
pub fn s3(&self) -> &Irrep {
&self.s3
}
pub fn multiplicity(&self) -> usize {
self.multiplicity
}
pub fn copy_shape(&self) -> (usize, usize) {
(self.rows, self.d3)
}
pub fn copy(&self, mu: usize) -> &[f64] {
let stride = self.rows * self.d3;
&self.cols[mu * stride..(mu + 1) * stride]
}
pub fn data(&self) -> &[f64] {
&self.cols
}
pub(crate) fn storage_bytes(&self) -> usize {
self.cols.len() * std::mem::size_of::<f64>() + std::mem::size_of::<Self>()
}
}
#[derive(Debug)]
pub struct CanonicalCatalog {
series: Series,
rank: usize,
store: HashMap<Irrep, Generators>,
bytes: usize,
max_bytes: usize,
}
impl CanonicalCatalog {
pub fn new(series: Series, r: usize) -> Result<Self, CatalogError> {
Self::with_budget(series, r, DEFAULT_MAX_BYTES)
}
pub fn with_budget(series: Series, r: usize, max_bytes: usize) -> Result<Self, CatalogError> {
let mut cat = CanonicalCatalog {
series,
rank: r,
store: HashMap::new(),
bytes: 0,
max_bytes,
};
cat.seed_base()?;
Ok(cat)
}
pub fn series(&self) -> Series {
self.series
}
pub fn rank(&self) -> usize {
self.rank
}
pub fn bytes(&self) -> usize {
self.bytes
}
pub fn budget(&self) -> usize {
self.max_bytes
}
pub fn len(&self) -> usize {
self.store.len()
}
pub fn is_empty(&self) -> bool {
self.store.is_empty()
}
fn seed_base(&mut self) -> Result<(), CatalogError> {
let seed = defining_seed(self.series, self.rank)?;
let defining_irrep = self.defining_irrep()?;
let expected = std::collections::BTreeMap::from([(defining_irrep.clone(), 1u32)]);
let decomp = decompose(&Generators::from_seed(&seed), &expected)?;
let defining = decomp.blocks()[0].generators().clone();
let trivial_irrep = Irrep::trivial(self.series, self.rank)?;
let trivial = Generators::trivial(self.series, self.rank);
self.commit_one(trivial_irrep, trivial);
self.commit_one(defining_irrep, defining);
Ok(())
}
fn defining_irrep(&self) -> Result<Irrep, CatalogError> {
let mut dynkin = vec![0i64; self.rank];
dynkin[0] = 1;
Ok(Irrep::from_dynkin(self.series, &dynkin)?)
}
fn check_group(&self, c: &Irrep) -> Result<(), CatalogError> {
if c.series() != self.series || c.rank() != self.rank {
return Err(CatalogError::WrongGroup {
catalog: (self.series, self.rank),
got: (c.series(), c.rank()),
});
}
Ok(())
}
pub fn generators(&mut self, c: &Irrep) -> Result<&Generators, CatalogError> {
self.check_group(c)?;
self.ensure(c)?;
Ok(self.store.get(c).expect("ensure guarantees presence"))
}
pub fn cgc(&mut self, s1: &Irrep, s2: &Irrep, s3: &Irrep) -> Result<CatalogCgc, CatalogError> {
self.check_group(s1)?;
self.check_group(s2)?;
self.check_group(s3)?;
let expected = directproduct(s1, s2)?;
if expected.get(s3).copied().unwrap_or(0) == 0 {
return Err(CatalogError::ZeroFusionChannel {
a: s1.dynkin(),
b: s2.dynkin(),
c: s3.dynkin(),
});
}
self.ensure(s1)?;
self.ensure(s2)?;
self.ensure(s3)?;
let g1 = self.store.get(s1).expect("ensured").clone();
let g2 = self.store.get(s2).expect("ensured").clone();
let product = Generators::product(&g1, &g2)?;
let decomp = decompose(&product, &expected)?;
let mut copies: Vec<&Block> = decomp.blocks().iter().filter(|b| b.irrep() == s3).collect();
copies.sort_by_key(|b| b.outer_multiplicity().0);
self.assemble_cgc(s1, s2, s3, &copies)
}
pub(crate) fn cgc_product(
&mut self,
s1: &Irrep,
s2: &Irrep,
) -> Result<Vec<CatalogCgc>, CatalogError> {
self.check_group(s1)?;
self.check_group(s2)?;
self.ensure(s1)?;
self.ensure(s2)?;
let expected = directproduct(s1, s2)?;
let g1 = self.store.get(s1).expect("ensured").clone();
let g2 = self.store.get(s2).expect("ensured").clone();
let product = Generators::product(&g1, &g2)?;
let decomp = decompose(&product, &expected)?;
let mut by_irrep: std::collections::BTreeMap<Irrep, Vec<&Block>> =
std::collections::BTreeMap::new();
for b in decomp.blocks() {
by_irrep.entry(b.irrep().clone()).or_default().push(b);
}
let channels: Vec<Irrep> = by_irrep.keys().cloned().collect();
for c in &channels {
self.ensure(c)?;
}
let mut out = Vec::with_capacity(by_irrep.len());
for (c, mut copies) in by_irrep {
copies.sort_by_key(|b| b.outer_multiplicity().0);
out.push(self.assemble_cgc(s1, s2, &c, &copies)?);
}
Ok(out)
}
fn assemble_cgc(
&self,
s1: &Irrep,
s2: &Irrep,
s3: &Irrep,
copies: &[&Block],
) -> Result<CatalogCgc, CatalogError> {
let stored = self.store.get(s3).expect("caller ensured s3");
let (rows, d3) = copies[0].cgc_shape();
let mut cols = Vec::with_capacity(rows * d3 * copies.len());
for b in copies {
debug_assert_cartan_matches(b, stored);
let raw = b.generators().coherence_residual(stored);
if raw <= TOL_BASIS_COHERENT {
cols.extend_from_slice(b.cgc());
} else {
let (aligned, residual) = align_block(b, stored)?;
if residual > TOL_BASIS_COHERENT {
return Err(CatalogError::BasisIncoherent {
irrep: s3.dynkin(),
product: (s1.dynkin(), s2.dynkin()),
residual,
});
}
cols.extend_from_slice(&aligned.data);
}
}
Ok(CatalogCgc {
s1: s1.clone(),
s2: s2.clone(),
s3: s3.clone(),
rows,
d3,
multiplicity: copies.len(),
cols,
})
}
pub fn reset(&mut self) {
self.store.clear();
self.bytes = 0;
self.seed_base()
.expect("base re-seed cannot fail after a valid construction");
}
fn ensure(&mut self, c: &Irrep) -> Result<(), CatalogError> {
if self.store.contains_key(c) {
return Ok(());
}
let mut staged: Vec<(Irrep, Generators)> = Vec::new();
build_into(self.series, self.rank, &self.store, &mut staged, c)?;
let add: usize = staged.iter().map(|(_, g)| gen_bytes(g)).sum();
let needed = self.bytes + add;
if needed > self.max_bytes {
return Err(CatalogError::BudgetExceeded {
limit: self.max_bytes,
needed,
});
}
for (k, v) in staged {
self.commit_one(k, v);
}
Ok(())
}
fn commit_one(&mut self, irrep: Irrep, gens: Generators) {
self.bytes += gen_bytes(&gens);
self.store.insert(irrep, gens);
}
#[cfg(test)]
pub(crate) fn is_materialized(&self, c: &Irrep) -> bool {
self.store.contains_key(c)
}
#[cfg(test)]
pub(crate) fn stored_commutator_residual(&self, c: &Irrep) -> Option<f64> {
self.store.get(c).map(|g| g.max_commutator_residual())
}
}
fn gen_bytes(g: &Generators) -> usize {
let d = g.dim();
let r = g.rank();
let f = std::mem::size_of::<f64>();
r * (d * d + d) * f + std::mem::size_of::<Generators>()
}
fn is_spinor_base(c: &Irrep) -> bool {
c.is_spinor() && c.two_partition().iter().all(|x| x.abs() == 1)
}
fn box_count(c: &Irrep) -> i64 {
c.two_partition().iter().map(|x| x.abs()).sum()
}
fn prec_key(c: &Irrep) -> (i64, BigInt, Vec<i64>) {
(box_count(c), c.dim(), c.dynkin())
}
fn canonical_parent(series: Series, rank: usize, c: &Irrep) -> Option<(Irrep, Irrep)> {
struct Cand {
sum: BigInt,
dim_a: BigInt,
dynkin_a: Vec<i64>,
dynkin_b: Vec<i64>,
a: Irrep,
b: Irrep,
}
impl Cand {
fn key(&self) -> (&BigInt, &BigInt, &Vec<i64>, &Vec<i64>) {
(&self.sum, &self.dim_a, &self.dynkin_a, &self.dynkin_b)
}
}
let key_c = prec_key(c);
let below = irreps_below(series, rank, c);
let mut best: Option<Cand> = None;
for a in &below {
let dim_a = a.dim();
if let Some(cur) = &best {
if &dim_a * 2 > cur.sum {
break; }
}
let Ok(prod) = directproduct(&a.dual(), c) else {
continue;
};
for b in prod.keys() {
if prec_key(b) >= key_c {
continue; }
let cand = Cand {
sum: &dim_a + b.dim(),
dim_a: dim_a.clone(),
dynkin_a: a.dynkin(),
dynkin_b: b.dynkin(),
a: a.clone(),
b: b.clone(),
};
if best.as_ref().is_none_or(|cur| cand.key() < cur.key()) {
best = Some(cand);
}
}
}
best.map(|c| (c.a, c.b))
}
fn irreps_below(series: Series, rank: usize, c: &Irrep) -> Vec<Irrep> {
let max_boxes = box_count(c);
let key_c = prec_key(c);
let mut out: Vec<Irrep> = Vec::new();
let mut cur = vec![0i64; rank];
enum_partitions(series, rank, max_boxes, 0, 0, 0, &mut cur, &mut out);
if c.is_spinor() {
enum_partitions(series, rank, max_boxes, 1, 0, 0, &mut cur, &mut out);
}
out.retain(|x| prec_key(x) < key_c);
out.sort_by_key(|x| (x.dim(), x.dynkin()));
out
}
#[allow(clippy::too_many_arguments)]
fn enum_partitions(
series: Series,
rank: usize,
max_boxes: i64,
parity: i64,
pos: usize,
used: i64,
cur: &mut Vec<i64>,
out: &mut Vec<Irrep>,
) {
if pos == rank {
push_partition_irrep(series, cur, out);
return;
}
let upper = if pos == 0 { max_boxes } else { cur[pos - 1] };
let mut v = parity;
while v <= upper {
if used + v > max_boxes {
break;
}
cur[pos] = v;
enum_partitions(series, rank, max_boxes, parity, pos + 1, used + v, cur, out);
v += 2;
}
cur[pos] = parity;
}
fn push_partition_irrep(series: Series, cur: &[i64], out: &mut Vec<Irrep>) {
out.push(make_irrep(series, cur.to_vec()));
if series == Series::D {
let last = cur.len() - 1;
if cur[last] > 0 {
let mut w = cur.to_vec();
w[last] = -w[last];
out.push(make_irrep(series, w));
}
}
}
fn make_irrep(series: Series, two_weight: Vec<i64>) -> Irrep {
super::Irrep::from_two_weight(series, two_weight)
}
fn lookup<'a>(
store: &'a HashMap<Irrep, Generators>,
staged: &'a [(Irrep, Generators)],
c: &Irrep,
) -> Option<&'a Generators> {
store
.get(c)
.or_else(|| staged.iter().find(|(k, _)| k == c).map(|(_, g)| g))
}
fn build_into(
series: Series,
rank: usize,
store: &HashMap<Irrep, Generators>,
staged: &mut Vec<(Irrep, Generators)>,
c: &Irrep,
) -> Result<(), CatalogError> {
if lookup(store, staged, c).is_some() {
return Ok(()); }
if is_spinor_base(c) {
for (label, seed) in spinor_seeds(series, rank)? {
if Irrep::from_dynkin_in(&series.cover_group(rank), &label)? == *c {
let raw = Generators::from_seed(&seed);
let expected = std::collections::BTreeMap::from([(c.clone(), 1u32)]);
let decomp = decompose(&raw, &expected)?;
let gens = decomp.blocks()[0].generators().clone();
staged.push((c.clone(), gens));
return Ok(());
}
}
}
let (a, b) = canonical_parent(series, rank, c)
.ok_or_else(|| CatalogError::NoCanonicalParent { dynkin: c.dynkin() })?;
build_into(series, rank, store, staged, &a)?;
build_into(series, rank, store, staged, &b)?;
let ga = lookup(store, staged, &a)
.expect("staged by recursion")
.clone();
let gb = lookup(store, staged, &b)
.expect("staged by recursion")
.clone();
let product = Generators::product(&ga, &gb)?;
let expected = directproduct(&a, &b)?;
let decomp = decompose(&product, &expected)?;
for block in decomp.blocks() {
let ci = block.irrep();
if let Some(existing) = lookup(store, staged, ci) {
debug_assert_cartan_matches(block, existing);
continue;
}
if block.outer_multiplicity().0 != 0 {
continue;
}
if canonical_parent(series, rank, ci).as_ref() == Some(&(a.clone(), b.clone())) {
staged.push((ci.clone(), block.generators().clone()));
}
}
debug_assert!(
lookup(store, staged, c).is_some(),
"the canonical parent of c must produce c's block"
);
Ok(())
}
fn debug_assert_cartan_matches(block: &Block, stored: &Generators) {
debug_assert_eq!(
block.dim(),
stored.dim(),
"rediscovered block dim disagrees with stored generators"
);
if !cfg!(debug_assertions) {
return;
}
let rank = stored.rank();
let d = stored.dim();
let round = |x: f64| (2.0 * x).round() as i64;
let mut block_w: Vec<Vec<i64>> = (0..d)
.map(|s| (0..rank).map(|j| round(block.weight(s, j))).collect())
.collect();
let mut stored_w: Vec<Vec<i64>> = (0..d)
.map(|s| (0..rank).map(|j| round(stored.cartan_diag(j)[s])).collect())
.collect();
block_w.sort_unstable();
stored_w.sort_unstable();
debug_assert_eq!(
block_w, stored_w,
"rediscovered block weight multiset disagrees with stored generators"
);
}
#[cfg(test)]
mod tests;