use super::error::CharacterError;
use crate::ffield::{FiniteField, GfMatrix};
use crate::flint::nmod::{FlintNmodPoly, FlintNmodPolyFactor};
pub(super) fn pow_mod(base: u64, exp: u64, p: u64) -> u64 {
let mut result: u64 = 1 % p;
let mut b = base % p;
let mut e = exp;
while e > 0 {
if e & 1 == 1 {
result = ((result as u128 * b as u128) % p as u128) as u64;
}
b = ((b as u128 * b as u128) % p as u128) as u64;
e >>= 1;
}
result
}
pub(super) fn mul_mod(a: u64, b: u64, p: u64) -> u64 {
((a as u128 * b as u128) % p as u128) as u64
}
pub(super) fn inv_mod(a: u64, p: u64) -> Result<u64, CharacterError> {
let a = a % p;
if a == 0 {
return Err(CharacterError::Internal {
detail: format!("tried to invert 0 modulo {p}"),
});
}
Ok(pow_mod(a, p - 2, p))
}
pub(super) fn distinct_prime_factors(mut n: u64) -> Vec<u64> {
let mut out = Vec::new();
let mut d = 2u64;
while d <= n / d {
if n % d == 0 {
out.push(d);
while n % d == 0 {
n /= d;
}
}
d += 1;
}
if n > 1 {
out.push(n);
}
out
}
const PRIME_SEARCH_CANDIDATES: u64 = 200_000;
pub(super) fn working_prime(exponent: u64, floor: u64) -> Result<u64, CharacterError> {
if exponent == 0 {
return Err(CharacterError::Internal {
detail: "exp G came out as 0".to_string(),
});
}
let start = floor / exponent + 1;
let mut last = 0u64;
for k in start..start.saturating_add(PRIME_SEARCH_CANDIDATES) {
let candidate = match k.checked_mul(exponent).and_then(|v| v.checked_add(1)) {
Some(v) => v,
None => break,
};
last = candidate;
if candidate > floor && crate::modular::is_prime(candidate) {
return Ok(candidate);
}
}
Err(CharacterError::NoSuitablePrime {
exponent,
order: floor.to_string(),
searched_to: last,
})
}
pub(super) fn root_of_unity(p: u64, e: u64) -> Result<u64, CharacterError> {
if e == 0 || (p - 1) % e != 0 {
return Err(CharacterError::Internal {
detail: format!("{e} does not divide p - 1 = {}", p - 1),
});
}
if e == 1 {
return Ok(1 % p);
}
let cofactor = (p - 1) / e;
let primes = distinct_prime_factors(e);
for base in 2..p {
let w = pow_mod(base, cofactor, p);
if w <= 1 {
continue;
}
if primes.iter().all(|&q| pow_mod(w, e / q, p) != 1) {
if pow_mod(w, e, p) != 1 {
return Err(CharacterError::Internal {
detail: format!("{w} has order not dividing {e} in GF({p})"),
});
}
return Ok(w);
}
}
Err(CharacterError::Internal {
detail: format!("GF({p}) has no element of order {e} despite {e} | p - 1"),
})
}
fn matrix(
field: &FiniteField,
rows: usize,
cols: usize,
data: &[u64],
) -> Result<GfMatrix, CharacterError> {
GfMatrix::from_u64(field, rows, cols, data).map_err(CharacterError::from)
}
fn entries(m: &GfMatrix) -> Result<Vec<u64>, CharacterError> {
m.to_u64().ok_or_else(|| CharacterError::Internal {
detail: "a matrix over a prime field did not render as u64 entries".to_string(),
})
}
fn roots(p: u64, ascending: &[u64]) -> Vec<u64> {
let degree = match ascending.iter().rposition(|&c| c % p != 0) {
Some(d) => d,
None => return Vec::new(),
};
if degree == 0 {
return Vec::new();
}
let mut poly = FlintNmodPoly::new(p);
for (i, &c) in ascending.iter().take(degree + 1).enumerate() {
poly.set_coeff(i, c % p);
}
let mut factorisation = FlintNmodPolyFactor::new();
factorisation.factor(&poly);
let mut out = Vec::new();
for i in 0..factorisation.len() {
let factor = factorisation.poly_at(p, i);
if factor.degree() != 1 {
continue;
}
let a1 = factor.get_coeff(1);
let a0 = factor.get_coeff(0);
if let Ok(inv) = inv_mod(a1, p) {
let neg = (p - a0 % p) % p;
out.push(mul_mod(neg, inv, p));
}
}
out.sort_unstable();
out.dedup();
out
}
fn intersect(u: &GfMatrix, v: &GfMatrix) -> Result<GfMatrix, CharacterError> {
let rows = u.nrows();
let (a, b) = (u.ncols(), v.ncols());
let field = u.field();
if a == 0 || b == 0 {
return GfMatrix::zeros(field, rows, 0).map_err(CharacterError::from);
}
let ud = entries(u)?;
let vd = entries(v)?;
let mut stacked = vec![0u64; rows * (a + b)];
for i in 0..rows {
for j in 0..a {
stacked[i * (a + b) + j] = ud[i * a + j];
}
for j in 0..b {
stacked[i * (a + b) + a + j] = vd[i * b + j];
}
}
let kernel = matrix(field, rows, a + b, &stacked)?.nullspace()?;
let t = kernel.ncols();
if t == 0 {
return GfMatrix::zeros(field, rows, 0).map_err(CharacterError::from);
}
let kd = entries(&kernel)?;
let mut top = vec![0u64; a * t];
for i in 0..a {
for j in 0..t {
top[i * t + j] = kd[i * t + j];
}
}
let top = matrix(field, a, t, &top)?;
u.mul(&top).map_err(CharacterError::from)
}
pub(super) fn common_eigenvectors(
p: u64,
classes: usize,
matrices: &[Vec<Vec<u64>>],
) -> Result<Vec<Vec<u64>>, CharacterError> {
let field = FiniteField::prime(p)?;
if classes == 0 {
return Ok(Vec::new());
}
let mut parts = vec![GfMatrix::identity(&field, classes)?];
for m in matrices {
if parts.len() >= classes {
break;
}
if m.len() != classes {
return Err(CharacterError::Internal {
detail: format!(
"a class multiplication matrix has {} rows, not {classes}",
m.len()
),
});
}
let mut flat = Vec::with_capacity(classes * classes);
for row in m {
if row.len() != classes {
return Err(CharacterError::Internal {
detail: format!(
"a class multiplication matrix row has {} entries, not {classes}",
row.len()
),
});
}
flat.extend(row.iter().map(|&v| v % p));
}
let mk = matrix(&field, classes, classes, &flat)?;
let mut cp = Vec::new();
for c in mk.charpoly()? {
cp.push(c.as_u64().ok_or_else(|| CharacterError::Internal {
detail: "a charpoly coefficient over a prime field was not a scalar".to_string(),
})?);
}
let eigenvalues = roots(p, &cp);
let mut eigenspaces = Vec::with_capacity(eigenvalues.len());
let identity = GfMatrix::identity(&field, classes)?;
for &lambda in &eigenvalues {
let shifted = mk.sub(&identity.scalar_mul(&field.scalar(lambda))?)?;
let kernel = shifted.nullspace()?;
if kernel.ncols() > 0 {
eigenspaces.push(kernel);
}
}
let mut refined = Vec::with_capacity(parts.len());
for part in &parts {
if part.ncols() <= 1 {
refined.push(part.clone());
continue;
}
let mut covered = 0usize;
for space in &eigenspaces {
let piece = intersect(part, space)?;
if piece.ncols() > 0 {
covered += piece.ncols();
refined.push(piece);
}
}
if covered != part.ncols() {
return Err(CharacterError::Internal {
detail: format!(
"the eigenspaces of a class multiplication matrix cover {covered} of \
{} dimensions of an invariant subspace",
part.ncols()
),
});
}
}
parts = refined;
}
if parts.len() != classes || parts.iter().any(|b| b.ncols() != 1) {
return Err(CharacterError::SplittingIncomplete {
found: parts.len(),
classes,
});
}
let mut out = Vec::with_capacity(classes);
for part in &parts {
out.push(entries(part)?);
}
Ok(out)
}