use crate::forms::integral::diagonal::{
odd_unit_residue, rat_val, rational_congruence_diagonal, rdiv, signature_from_diagonal,
unit_mod8, DegenerateBehavior,
};
use crate::forms::try_is_square_qp;
use crate::forms::IntegralForm;
use crate::linalg::integer::prime_factors;
use crate::scalar::{Rational, Scalar};
use std::collections::BTreeMap;
use std::fmt;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ScaleSymbol {
pub scale: u128,
pub dim: usize,
pub sign: i128,
pub det_mod8: i128,
pub type_ii: bool,
pub oddity: i128,
}
fn render_scale_symbol(base: impl fmt::Display, s: &ScaleSymbol) -> String {
let sign = if s.sign >= 0 { "+" } else { "-" };
if s.type_ii {
format!("{base}_II^{sign}{}", s.dim)
} else {
format!("{base}_{}^{sign}{}", s.oddity, s.dim)
}
}
impl ScaleSymbol {
pub fn display(&self) -> String {
self.to_string()
}
}
impl fmt::Display for ScaleSymbol {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", render_scale_symbol("p", self))
}
}
#[derive(Clone, Debug)]
pub struct Genus {
pub dim: usize,
pub signature: (usize, usize),
pub det: i128,
symbols: BTreeMap<u128, Vec<ScaleSymbol>>,
}
fn r_int(n: i128) -> Rational {
Rational::from_int(n)
}
fn unit_sign_odd(r: &Rational, p: i128) -> i128 {
if try_is_square_qp(odd_unit_residue(r, p), p as u128)
.expect("odd genus prime must be supported")
{
1
} else {
-1
}
}
fn sign_from_mod8(u: i128) -> i128 {
if u == 1 || u == 7 {
1
} else {
-1
}
}
type RMat = Vec<Vec<Rational>>;
fn to_rational(gram: &[Vec<i128>]) -> RMat {
gram.iter()
.map(|row| row.iter().map(|&x| r_int(x)).collect())
.collect()
}
struct RawBlock {
scale: u128,
dim: usize,
sign: i128,
det_mod8: i128,
type_ii: bool,
oddity: i128,
}
fn min_val_entry(a: &RMat, active: &[usize], p: i128) -> Option<(usize, usize)> {
let mut best: Option<(i128, usize, usize)> = None;
for (ii, &i) in active.iter().enumerate() {
for &j in &active[ii..] {
if !a[i][j].is_zero() {
let v = rat_val(&a[i][j], p);
if best.is_none_or(|(bv, bi, bj)| v < bv || (v == bv && i == j && bi != bj)) {
best = Some((v, i, j));
}
}
}
}
best.map(|(_, i, j)| (i, j))
}
fn jordan_blocks(gram: &[Vec<i128>], p: i128) -> Option<Vec<RawBlock>> {
let mut a = to_rational(gram);
let mut active: Vec<usize> = (0..gram.len()).collect();
let mut blocks = Vec::new();
let mut guard = 0usize;
let guard_max = 8 * gram.len() * gram.len() + 16;
while !active.is_empty() {
guard += 1;
if guard > guard_max {
return None; }
let (i, j) = min_val_entry(&a, &active, p)?;
let scale = rat_val(&a[i][j], p);
if i != j && p != 2 {
for &c in &active {
a[i][c] = a[i][c].add(&a[j][c].clone());
}
for &r in &active {
a[r][i] = a[r][i].add(&a[r][j].clone());
}
continue;
}
let scale = u128::try_from(scale).ok()?; if i == j {
let d = a[i][i].clone();
let (sign, det_mod8, type_ii, oddity) = if p == 2 {
let u8v = unit_mod8(&d);
(sign_from_mod8(u8v), u8v, false, u8v)
} else {
(unit_sign_odd(&d, p), 1, false, 0)
};
blocks.push(RawBlock {
scale,
dim: 1,
sign,
det_mod8,
type_ii,
oddity,
});
let pivot = d;
let rest: Vec<usize> = active.iter().copied().filter(|&r| r != i).collect();
for &r in &rest {
for &s in &rest {
let corr = rdiv(&a[r][i].mul(&a[i][s]), &pivot);
a[r][s] = a[r][s].sub(&corr);
}
}
active = rest;
} else {
let alpha = a[i][i].clone();
let beta = a[i][j].clone();
let gamma = a[j][j].clone();
let det = alpha.mul(&gamma).sub(&beta.mul(&beta));
let det_mod8 = unit_mod8(&det);
let sign = sign_from_mod8(det_mod8);
blocks.push(RawBlock {
scale,
dim: 2,
sign,
det_mod8,
type_ii: true,
oddity: 0,
});
let rest: Vec<usize> = active
.iter()
.copied()
.filter(|&r| r != i && r != j)
.collect();
for &r in &rest {
for &s in &rest {
let t0 = gamma.mul(&a[i][s]).sub(&beta.mul(&a[j][s]));
let t1 = alpha.mul(&a[j][s]).sub(&beta.mul(&a[i][s]));
let numer = a[r][i].mul(&t0).add(&a[r][j].mul(&t1));
let corr = rdiv(&numer, &det);
a[r][s] = a[r][s].sub(&corr);
}
}
active = rest;
}
}
Some(blocks)
}
fn mul_mod8_unit(a: i128, b: i128) -> i128 {
(a * b).rem_euclid(8)
}
fn aggregate(blocks: &[RawBlock], p: i128) -> Vec<ScaleSymbol> {
let mut by_scale: BTreeMap<u128, (usize, i128, i128, bool, i128)> = BTreeMap::new();
for b in blocks {
let e = by_scale.entry(b.scale).or_insert((0, 1, 1, p == 2, 0));
e.0 += b.dim;
e.1 *= b.sign;
if p == 2 {
e.2 = mul_mod8_unit(e.2, b.det_mod8);
}
if p == 2 && !b.type_ii {
e.3 = false; }
if !b.type_ii {
e.4 = (e.4 + b.oddity).rem_euclid(8);
}
}
by_scale
.into_iter()
.map(
|(scale, (dim, sign, det_mod8, type_ii, oddity))| ScaleSymbol {
scale,
dim,
sign,
det_mod8,
type_ii,
oddity: if type_ii { 0 } else { oddity },
},
)
.collect()
}
fn signature(gram: &[Vec<i128>]) -> (usize, usize) {
let diag = rational_congruence_diagonal(gram, DegenerateBehavior::RequireNonsingular);
signature_from_diagonal(&diag)
}
fn relevant_primes(det: i128) -> Vec<u128> {
let mut ps = prime_factors(det.unsigned_abs());
if !ps.contains(&2) {
ps.push(2);
ps.sort_unstable();
}
ps
}
impl Genus {
pub fn from_lattice(lattice: &IntegralForm) -> Option<Genus> {
let det = lattice.determinant();
if det == 0 {
return None;
}
let gram = lattice.gram();
let mut symbols = BTreeMap::new();
for p in relevant_primes(det) {
let blocks = jordan_blocks(gram, p as i128)?;
symbols.insert(p, aggregate(&blocks, p as i128));
}
Some(Genus {
dim: lattice.dim(),
signature: signature(gram),
det,
symbols,
})
}
pub fn symbol_at(&self, p: u128) -> &[ScaleSymbol] {
self.symbols.get(&p).map_or(&[], |v| v)
}
pub fn canonical_symbol_at(&self, p: u128) -> Vec<ScaleSymbol> {
let symbol = self.symbol_at(p);
if p == 2 {
canonical_2adic_symbol(symbol)
} else {
symbol.to_vec()
}
}
pub fn primes(&self) -> Vec<u128> {
self.symbols.keys().copied().collect()
}
pub fn display(&self) -> String {
self.to_string()
}
}
impl fmt::Display for Genus {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"Genus(dim={}, signature=({}, {}), det={}",
self.dim, self.signature.0, self.signature.1, self.det
)?;
for p in self.primes() {
let rendered = self
.canonical_symbol_at(p)
.iter()
.map(|s| {
let q = u32::try_from(s.scale)
.ok()
.and_then(|e| p.checked_pow(e))
.unwrap_or(p);
render_scale_symbol(q, s)
})
.collect::<Vec<_>>()
.join(" ");
write!(f, ", {p}: [{rendered}]")?;
}
write!(f, ")")
}
}
fn fuse_oddities(syms: &[ScaleSymbol]) -> Vec<ScaleSymbol> {
let mut out = syms.to_vec();
let mut i = 0;
while i < out.len() {
if out[i].type_ii {
i += 1;
continue;
}
let mut j = i;
let mut total = 0i128;
loop {
total += out[j].oddity;
let extends =
j + 1 < out.len() && !out[j + 1].type_ii && out[j + 1].scale == out[j].scale + 1;
if !extends {
break;
}
j += 1;
}
let total = total.rem_euclid(8);
out[i].oddity = total;
for k in (i + 1)..=j {
out[k].oddity = 0;
}
i = j + 1;
}
out
}
fn two_adic_compartments(syms: &[ScaleSymbol]) -> Vec<Vec<usize>> {
let mut out = Vec::new();
let mut i = 0usize;
while i < syms.len() {
if syms[i].type_ii {
i += 1;
continue;
}
let mut comp = vec![i];
let mut j = i;
while j + 1 < syms.len() && !syms[j + 1].type_ii && syms[j + 1].scale == syms[j].scale + 1 {
j += 1;
comp.push(j);
}
out.push(comp);
i = j + 1;
}
out
}
fn two_adic_trains(syms: &[ScaleSymbol]) -> Vec<Vec<usize>> {
if syms.is_empty() {
return Vec::new();
}
let mut out = Vec::new();
let mut cur = vec![0usize];
for i in 1..syms.len() {
let scale_gap = syms[i].scale - syms[i - 1].scale;
let connected_by_type_i = !syms[i].type_ii || !syms[i - 1].type_ii;
let connected_across_empty_type_ii =
scale_gap == 2 && !syms[i].type_ii && !syms[i - 1].type_ii;
if (scale_gap == 1 && connected_by_type_i) || connected_across_empty_type_ii {
cur.push(i);
} else {
out.push(cur);
cur = vec![i];
}
}
out.push(cur);
out
}
fn set_canonical_det_from_sign(sym: &mut ScaleSymbol) {
sym.det_mod8 = if sym.sign > 0 { 1 } else { 3 };
}
fn canonical_2adic_symbol(syms: &[ScaleSymbol]) -> Vec<ScaleSymbol> {
let mut out = syms.to_vec();
for sym in &mut out {
sym.sign = sign_from_mod8(sym.det_mod8);
set_canonical_det_from_sign(sym);
if sym.type_ii {
sym.oddity = 0;
} else {
sym.oddity = sym.oddity.rem_euclid(8);
}
}
out = fuse_oddities(&out);
let compartments = two_adic_compartments(&out);
for train in two_adic_trains(&out) {
for &i in train.iter().rev().take(train.len().saturating_sub(1)) {
if out[i].sign < 0 {
out[i].sign = 1;
set_canonical_det_from_sign(&mut out[i]);
let prev = i - 1;
out[prev].sign *= -1;
set_canonical_det_from_sign(&mut out[prev]);
for compartment in &compartments {
if compartment.contains(&prev) || compartment.contains(&i) {
let head = compartment[0];
out[head].oddity = (out[head].oddity + 4).rem_euclid(8);
}
}
}
}
}
out
}
pub fn are_in_same_genus(a: &IntegralForm, b: &IntegralForm) -> bool {
let (Some(ga), Some(gb)) = (Genus::from_lattice(a), Genus::from_lattice(b)) else {
return false;
};
if ga.dim != gb.dim || ga.signature != gb.signature || ga.det != gb.det {
return false;
}
if ga.symbols.keys().ne(gb.symbols.keys()) {
return false;
}
for (&p, sa) in &ga.symbols {
let sb = &gb.symbols[&p];
if p == 2 {
if canonical_2adic_symbol(sa) != canonical_2adic_symbol(sb) {
return false;
}
} else if sa != sb {
return false;
}
}
true
}
#[cfg(test)]
mod tests {
use super::*;
use crate::forms::{d16_plus, is_root_lattice};
use crate::forms::{e_6, e_7, e_8};
fn a_n(n: usize) -> IntegralForm {
crate::forms::a_n(n).unwrap()
}
fn d_n(n: usize) -> IntegralForm {
crate::forms::d_n(n).unwrap()
}
fn zn(n: usize) -> IntegralForm {
IntegralForm::diagonal(&vec![1i128; n])
}
fn s2(scale: u128, dim: usize, det_mod8: i128, type_ii: bool, oddity: i128) -> ScaleSymbol {
ScaleSymbol {
scale,
dim,
sign: sign_from_mod8(det_mod8.rem_euclid(8)),
det_mod8: det_mod8.rem_euclid(8),
type_ii,
oddity,
}
}
fn congruent(g: &IntegralForm, shears: &[(usize, usize, i128)]) -> IntegralForm {
let n = g.dim();
let mut u = vec![vec![0i128; n]; n];
for (i, row) in u.iter_mut().enumerate() {
row[i] = 1;
}
for &(i, j, c) in shears {
u[i][j] += c; }
let gram = g.gram();
let mut gu = vec![vec![0i128; n]; n];
for i in 0..n {
for j in 0..n {
let mut s = 0i128;
for k in 0..n {
s += gram[i][k] * u[k][j];
}
gu[i][j] = s;
}
}
let mut out = vec![vec![0i128; n]; n];
for i in 0..n {
for j in 0..n {
let mut s = 0i128;
for k in 0..n {
s += u[k][i] * gu[k][j];
}
out[i][j] = s;
}
}
IntegralForm::new(out).unwrap()
}
#[test]
fn z8_and_e8_differ_only_at_two() {
let z8 = Genus::from_lattice(&zn(8)).unwrap();
let e8 = Genus::from_lattice(&e_8()).unwrap();
assert_eq!(z8.dim, e8.dim);
assert_eq!(z8.signature, e8.signature);
assert_eq!(z8.det, e8.det);
let s2_z = z8.symbol_at(2);
assert_eq!(s2_z.len(), 1);
assert_eq!((s2_z[0].scale, s2_z[0].dim, s2_z[0].type_ii), (0, 8, false));
let s2_e = e8.symbol_at(2);
assert_eq!(s2_e.len(), 1);
assert_eq!((s2_e[0].scale, s2_e[0].dim, s2_e[0].type_ii), (0, 8, true));
assert!(!are_in_same_genus(&zn(8), &e_8()));
}
#[test]
fn jordan_symbols_match_known_oracles() {
let a2 = Genus::from_lattice(&a_n(2)).unwrap();
let s2 = a2.symbol_at(2);
assert_eq!(s2.len(), 1);
assert!(s2[0].type_ii && s2[0].dim == 2 && s2[0].scale == 0);
assert_eq!(a2.det, 3);
let d4 = Genus::from_lattice(&d_n(4)).unwrap();
let s2 = d4.symbol_at(2);
assert_eq!(s2.len(), 2);
assert_eq!((s2[0].scale, s2[0].dim, s2[0].type_ii), (0, 2, true));
assert_eq!((s2[1].scale, s2[1].dim, s2[1].type_ii), (1, 2, true));
let a1 = Genus::from_lattice(&IntegralForm::diagonal(&[2])).unwrap();
let s2 = a1.symbol_at(2);
assert_eq!(s2.len(), 1);
assert_eq!(
(s2[0].scale, s2[0].dim, s2[0].type_ii, s2[0].oddity),
(1, 1, false, 1)
);
}
#[test]
fn two_adic_jordan_prefers_odd_block_on_valuation_tie() {
let g = IntegralForm::new(vec![vec![2, 1], vec![1, 1]]).unwrap();
assert!(are_in_same_genus(&zn(2), &g));
let s2_g = Genus::from_lattice(&g).unwrap().symbol_at(2).to_vec();
assert_eq!(s2_g.len(), 1);
assert_eq!((s2_g[0].scale, s2_g[0].dim, s2_g[0].type_ii), (0, 2, false));
}
#[test]
fn two_adic_compartments_and_trains_follow_the_corrected_rules() {
let syms = vec![
s2(0, 1, 1, false, 1),
s2(1, 2, 3, true, 0),
s2(2, 1, 1, false, 1),
s2(4, 1, 1, false, 1),
];
assert_eq!(
two_adic_compartments(&syms),
vec![vec![0], vec![2], vec![3]]
);
assert_eq!(two_adic_trains(&syms), vec![vec![0, 1, 2, 3]]);
let gap_bridge = vec![s2(0, 1, 1, false, 1), s2(2, 1, 1, false, 1)];
assert_eq!(two_adic_trains(&gap_bridge), vec![vec![0, 1]]);
}
#[test]
fn two_adic_trains_continue_across_one_empty_scale() {
let a = IntegralForm::diagonal(&[1, 20]);
let b = IntegralForm::diagonal(&[5, 4]);
assert!(are_in_same_genus(&a, &b));
}
#[test]
fn two_adic_sign_walking_canonicalizes_train_signs() {
let a = vec![s2(0, 1, 3, false, 3), s2(1, 1, 3, false, 3)];
let b = vec![s2(0, 1, 1, false, 1), s2(1, 1, 1, false, 1)];
assert_ne!(fuse_oddities(&a), fuse_oddities(&b));
assert_eq!(canonical_2adic_symbol(&a), canonical_2adic_symbol(&b));
let c = vec![s2(0, 3, 3, false, 1), s2(1, 1, 1, true, 0)];
let canon = canonical_2adic_symbol(&c);
assert_eq!(canon[0].sign, -1); assert_eq!(canon[1].sign, 1);
}
#[test]
fn canonical_symbol_at_exposes_the_compared_two_adic_symbol() {
let g = Genus::from_lattice(&IntegralForm::diagonal(&[1, 6])).unwrap();
let raw = g.symbol_at(2);
let canonical = g.canonical_symbol_at(2);
assert_ne!(raw, canonical.as_slice());
assert_eq!(
canonical
.iter()
.map(|s| (s.scale, s.dim, s.sign, s.type_ii, s.oddity))
.collect::<Vec<_>>(),
vec![(0, 1, -1, false, 0), (1, 1, 1, false, 0)]
);
assert_eq!(g.canonical_symbol_at(3), g.symbol_at(3).to_vec());
}
#[test]
fn two_adic_reduction_matches_sage_quintuple_examples() {
let fused = canonical_2adic_symbol(&[s2(0, 1, 1, false, 1), s2(1, 1, 1, false, 1)]);
assert_eq!(
fused
.iter()
.map(|s| (s.scale, s.dim, s.sign, s.type_ii, s.oddity))
.collect::<Vec<_>>(),
vec![(0, 1, 1, false, 2), (1, 1, 1, false, 0)]
);
let sage = canonical_2adic_symbol(&[
s2(1, 2, 3, false, 4),
s2(2, 1, 1, false, 1),
s2(3, 1, 1, false, 1),
]);
assert_eq!(
sage.iter()
.map(|s| (s.scale, s.dim, s.sign, s.type_ii, s.oddity))
.collect::<Vec<_>>(),
vec![
(1, 2, -1, false, 6),
(2, 1, 1, false, 0),
(3, 1, 1, false, 0)
]
);
let crossed = canonical_2adic_symbol(&[s2(0, 1, 1, false, 1), s2(1, 1, 3, false, 3)]);
assert_eq!(
crossed
.iter()
.map(|s| (s.scale, s.dim, s.sign, s.type_ii, s.oddity))
.collect::<Vec<_>>(),
vec![(0, 1, -1, false, 0), (1, 1, 1, false, 0)]
);
}
#[test]
fn two_adic_sign_walking_across_type_ii_bridge_is_pinned() {
let crossed = canonical_2adic_symbol(&[
s2(0, 1, 1, false, 1),
s2(1, 2, 1, true, 0),
s2(2, 1, 3, false, 1),
]);
assert_eq!(
crossed
.iter()
.map(|s| (s.scale, s.dim, s.sign, s.type_ii, s.oddity))
.collect::<Vec<_>>(),
vec![
(0, 1, -1, false, 5),
(1, 2, 1, true, 0),
(2, 1, 1, false, 5)
]
);
}
#[test]
fn reflexive_and_isometry_invariant() {
for g in [a_n(2), a_n(4), d_n(4), d_n(5), e_6(), e_7(), e_8(), zn(5)] {
assert!(are_in_same_genus(&g, &g), "reflexive");
}
let cases = [a_n(3), d_n(4), e_6(), e_8(), zn(6)];
let shear_sets: &[&[(usize, usize, i128)]] = &[
&[(0, 1, 1)],
&[(0, 1, 2), (1, 2, -1)],
&[(0, 2, 1), (0, 1, -3), (1, 2, 1)],
];
for g in &cases {
for shears in shear_sets {
let valid: Vec<_> = shears
.iter()
.copied()
.filter(|&(i, j, _)| i < g.dim() && j < g.dim() && i < j)
.collect();
let h = congruent(g, &valid);
assert_eq!(h.determinant(), g.determinant(), "congruence keeps det");
assert!(
are_in_same_genus(g, &h),
"isometric copy must share the genus (dim {})",
g.dim()
);
}
}
}
#[test]
fn determinant_and_signature_distinguish_genera() {
assert!(!are_in_same_genus(&a_n(2), &a_n(3))); assert!(!are_in_same_genus(
&IntegralForm::diagonal(&[1]),
&IntegralForm::diagonal(&[3])
));
let pos = IntegralForm::diagonal(&[1, 1]);
let indef = IntegralForm::diagonal(&[1, -1]);
assert_eq!(pos.determinant().abs(), indef.determinant().abs());
assert!(!are_in_same_genus(&pos, &indef));
}
#[test]
fn even_unimodular_rank16_share_a_genus() {
let e8e8 = e_8().direct_sum(&e_8());
let d16 = d16_plus();
assert_eq!(e8e8.determinant(), 1);
assert_eq!(d16.determinant(), 1);
assert!(e8e8.is_even());
assert!(d16.is_even());
assert!(are_in_same_genus(&e8e8, &d16));
assert!(is_root_lattice(&e8e8));
assert!(!is_root_lattice(&d16));
let g = Genus::from_lattice(&e8e8).unwrap();
assert_eq!(g.signature, (16, 0));
let s2 = g.symbol_at(2);
assert_eq!(s2.len(), 1);
assert_eq!((s2[0].dim, s2[0].type_ii), (16, true));
}
#[test]
fn scale_symbol_display_renders_the_conway_sloane_notation() {
let e8 = Genus::from_lattice(&e_8()).unwrap();
let s2 = &e8.canonical_symbol_at(2)[0];
assert_eq!(s2.to_string(), "p_II^+8");
assert_eq!(s2.display(), s2.to_string());
let a2 = Genus::from_lattice(&a_n(2)).unwrap();
let s3 = &a2.canonical_symbol_at(3)[0];
assert_eq!(s3.to_string(), "p_0^-1");
}
#[test]
fn genus_display_renders_signature_det_and_canonical_symbols() {
let z1 = Genus::from_lattice(&IntegralForm::diagonal(&[1])).unwrap();
assert_eq!(
z1.to_string(),
"Genus(dim=1, signature=(1, 0), det=1, 2: [1_1^+1])"
);
assert_eq!(z1.display(), z1.to_string());
let e8 = Genus::from_lattice(&e_8()).unwrap();
assert_eq!(
e8.to_string(),
"Genus(dim=8, signature=(8, 0), det=1, 2: [1_II^+8])"
);
let a2 = Genus::from_lattice(&a_n(2)).unwrap();
assert_eq!(
a2.to_string(),
"Genus(dim=2, signature=(2, 0), det=3, 2: [1_II^-2], 3: [1_0^-1 3_0^-1])"
);
}
}