#![allow(clippy::needless_range_loop)]
use super::error::FpGroupError;
use crate::matrix::normal_form::{hermite_basis, hermite_form, smith_invariants, IntegerMatrix};
use rug::Integer;
use std::fmt;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct AbelianInvariants {
free_rank: usize,
torsion: Vec<Integer>,
}
impl AbelianInvariants {
pub fn trivial() -> AbelianInvariants {
AbelianInvariants {
free_rank: 0,
torsion: Vec::new(),
}
}
pub fn new(free_rank: usize, torsion: Vec<Integer>) -> AbelianInvariants {
let torsion = torsion
.into_iter()
.map(|d| d.abs())
.filter(|d| *d > 1)
.collect();
AbelianInvariants { free_rank, torsion }
}
pub fn free_rank(&self) -> usize {
self.free_rank
}
pub fn torsion(&self) -> &[Integer] {
&self.torsion
}
pub fn is_trivial(&self) -> bool {
self.free_rank == 0 && self.torsion.is_empty()
}
pub fn is_finite(&self) -> bool {
self.free_rank == 0
}
pub fn order(&self) -> Option<Integer> {
if self.free_rank > 0 {
return None;
}
let mut n = Integer::from(1);
for d in &self.torsion {
n *= d;
}
Some(n)
}
}
impl fmt::Display for AbelianInvariants {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.is_trivial() {
return write!(f, "0");
}
let mut parts: Vec<String> = Vec::new();
match self.free_rank {
0 => {}
1 => parts.push("Z".to_string()),
r => parts.push(format!("Z^{r}")),
}
for d in &self.torsion {
parts.push(format!("Z/{d}"));
}
write!(f, "{}", parts.join(" + "))
}
}
fn matrix_from_rows(rows: &[Vec<Integer>], cols: usize) -> Result<IntegerMatrix, FpGroupError> {
if rows.is_empty() {
return IntegerMatrix::from_rug_rows(Vec::new()).map_err(FpGroupError::from);
}
for (i, r) in rows.iter().enumerate() {
if r.len() != cols {
return Err(FpGroupError::Internal {
detail: format!("row {i} has {} entries, expected {cols}", r.len()),
});
}
}
IntegerMatrix::from_rug_rows(rows.to_vec()).map_err(FpGroupError::from)
}
fn echelon_basis(
rows: &[Vec<Integer>],
cols: usize,
) -> Result<(Vec<Vec<Integer>>, Vec<usize>), FpGroupError> {
if rows.is_empty() || cols == 0 {
return Ok((Vec::new(), Vec::new()));
}
let m = matrix_from_rows(rows, cols)?;
let h = hermite_basis(&m);
let mut basis: Vec<Vec<Integer>> = Vec::new();
let mut pivots: Vec<usize> = Vec::new();
for i in 0..h.rows {
let row: Vec<Integer> = (0..h.cols).map(|j| h.get(i, j).clone()).collect();
if let Some(p) = row.iter().position(|e| *e != 0) {
if pivots.last().is_some_and(|&q| q >= p) {
return Err(FpGroupError::Internal {
detail: "Hermite normal form is not in increasing-pivot echelon form".into(),
});
}
pivots.push(p);
basis.push(row);
}
}
Ok((basis, pivots))
}
fn left_kernel(rows: &[Vec<Integer>], cols: usize) -> Result<Vec<Vec<Integer>>, FpGroupError> {
if rows.is_empty() {
return Ok(Vec::new());
}
if cols == 0 {
return Ok((0..rows.len())
.map(|i| {
(0..rows.len())
.map(|j| Integer::from(usize::from(i == j)))
.collect()
})
.collect());
}
let m = matrix_from_rows(rows, cols)?;
let (h, u) = hermite_form(&m);
let mut kernel: Vec<Vec<Integer>> = Vec::new();
for i in 0..h.rows {
if (0..h.cols).all(|j| *h.get(i, j) == 0) {
kernel.push((0..u.cols).map(|j| u.get(i, j).clone()).collect());
}
}
Ok(kernel)
}
fn coordinates_in(basis: &[Vec<Integer>], pivots: &[usize], v: &[Integer]) -> Option<Vec<Integer>> {
let mut residue: Vec<Integer> = v.to_vec();
let mut coords = vec![Integer::from(0); basis.len()];
for (i, &p) in pivots.iter().enumerate() {
if residue[p] == 0 {
continue;
}
let piv = &basis[i][p];
let (q, r) = residue[p].clone().div_rem(piv.clone());
if r != 0 {
return None;
}
if q != 0 {
for (c, entry) in basis[i].iter().enumerate().skip(p) {
let delta = Integer::from(&q * entry);
residue[c] -= delta;
}
}
coords[i] = q;
}
if residue.iter().any(|e| *e != 0) {
return None;
}
Some(coords)
}
pub(super) fn quotient_invariants(
dim: usize,
k_gens: &[Vec<Integer>],
s_gens: &[Vec<Integer>],
) -> Result<AbelianInvariants, FpGroupError> {
if dim == 0 {
return Ok(AbelianInvariants::trivial());
}
let (basis, pivots) = echelon_basis(k_gens, dim)?;
let r = basis.len();
if r == 0 {
for s in s_gens {
if s.iter().any(|e| *e != 0) {
return Err(FpGroupError::Internal {
detail: "a subgroup generator lies outside the kernel lattice".into(),
});
}
}
return Ok(AbelianInvariants::trivial());
}
let mut coord_rows: Vec<Vec<Integer>> = Vec::with_capacity(s_gens.len());
for s in s_gens {
if s.len() != dim {
return Err(FpGroupError::Internal {
detail: format!("subgroup generator has {} entries, expected {dim}", s.len()),
});
}
if s.iter().all(|e| *e == 0) {
continue;
}
match coordinates_in(&basis, &pivots, s) {
Some(c) => coord_rows.push(c),
None => {
return Err(FpGroupError::Internal {
detail: "a subgroup generator lies outside the kernel lattice, so the \
complex does not satisfy d o d = 0"
.into(),
})
}
}
}
if coord_rows.is_empty() {
return Ok(AbelianInvariants::new(r, Vec::new()));
}
let c = matrix_from_rows(&coord_rows, r)?;
let mut torsion: Vec<Integer> = Vec::new();
let mut rank = 0usize;
for d in smith_invariants(&c) {
let d = d.abs();
if d != 0 {
rank += 1;
if d > 1 {
torsion.push(d);
}
}
}
if rank > r {
return Err(FpGroupError::Internal {
detail: "coordinate matrix has rank above the lattice rank".into(),
});
}
Ok(AbelianInvariants::new(r - rank, torsion))
}
pub(super) fn preimage_lattice(
m_rows: &[Vec<Integer>],
dim: usize,
cols: usize,
moduli: &[Integer],
) -> Result<Vec<Vec<Integer>>, FpGroupError> {
if m_rows.len() != dim {
return Err(FpGroupError::Internal {
detail: format!("expected {dim} rows, got {}", m_rows.len()),
});
}
if moduli.len() != cols {
return Err(FpGroupError::Internal {
detail: format!("expected {cols} moduli, got {}", moduli.len()),
});
}
let mut stacked: Vec<Vec<Integer>> = m_rows.to_vec();
for (j, mj) in moduli.iter().enumerate() {
if *mj > 0 {
let mut row = vec![Integer::from(0); cols];
row[j] = -mj.clone();
stacked.push(row);
}
}
let kernel = left_kernel(&stacked, cols)?;
let mut out: Vec<Vec<Integer>> = Vec::with_capacity(kernel.len());
for k in &kernel {
if k.len() < dim {
return Err(FpGroupError::Internal {
detail: "kernel row is shorter than the domain".into(),
});
}
let projected: Vec<Integer> = k[..dim].to_vec();
if projected.iter().any(|e| *e != 0) {
out.push(projected);
}
}
Ok(out)
}
const PREIMAGE_BLOCK: usize = 64;
fn unit_row(i: usize, dim: usize) -> Vec<Integer> {
let mut row = vec![Integer::from(0); dim];
row[i] = Integer::from(1);
row
}
pub(super) fn preimage_lattice_blocked(
m_rows: &[Vec<Integer>],
dim: usize,
cols: usize,
moduli: &[Integer],
) -> Result<Vec<Vec<Integer>>, FpGroupError> {
if cols <= PREIMAGE_BLOCK || moduli.iter().all(|m| *m == 0) {
return preimage_lattice(m_rows, dim, cols, moduli);
}
if m_rows.len() != dim || moduli.len() != cols {
return Err(FpGroupError::Internal {
detail: format!(
"blocked preimage got {} rows and {} moduli for dim {dim} and {cols} columns",
m_rows.len(),
moduli.len()
),
});
}
let lcm_modulus = if moduli.iter().all(|m| *m > 0) {
moduli.iter().fold(Integer::from(1), |acc, m| acc.lcm(m))
} else {
Integer::from(0)
};
let mut basis: Vec<Vec<Integer>> = (0..dim).map(|i| unit_row(i, dim)).collect();
let mut start = 0;
while start < cols && !basis.is_empty() {
let end = (start + PREIMAGE_BLOCK).min(cols);
let width = end - start;
let mut block_rows: Vec<Vec<Integer>> = Vec::with_capacity(basis.len());
for b in &basis {
let mut row = Vec::with_capacity(width);
for j in start..end {
let mut acc = Integer::from(0);
for (i, bi) in b.iter().enumerate() {
if *bi != 0 && m_rows[i][j] != 0 {
acc += Integer::from(bi * &m_rows[i][j]);
}
}
row.push(acc);
}
block_rows.push(row);
}
let gens = preimage_lattice(&block_rows, basis.len(), width, &moduli[start..end])?;
let mut mapped: Vec<Vec<Integer>> = Vec::with_capacity(gens.len());
for g in &gens {
let mut row = vec![Integer::from(0); dim];
for (i, gi) in g.iter().enumerate() {
if *gi == 0 {
continue;
}
for c in 0..dim {
if basis[i][c] != 0 {
let term = Integer::from(gi * &basis[i][c]);
row[c] += term;
}
}
}
mapped.push(row);
}
if lcm_modulus > 0 {
for row in &mut mapped {
for e in row.iter_mut() {
*e = e.clone().modulo(&lcm_modulus);
}
}
}
let (next, _pivots) = echelon_basis(&mapped, dim)?;
basis = next;
start = end;
}
if lcm_modulus > 0 {
for i in 0..dim {
let mut row = vec![Integer::from(0); dim];
row[i] = lcm_modulus.clone();
basis.push(row);
}
}
Ok(basis)
}