use rug::{Integer, Rational};
use super::classes::{to_u64, ConjugacyClasses};
use super::dixon;
use super::error::CharacterError;
use crate::group::PermutationGroup;
use crate::numfield::{NumberField, NumberFieldElement};
pub const DEFAULT_CHARACTER_TABLE_CAP: u64 = 5_000;
pub const MAX_EXPONENT_FIELD_DEGREE: usize = 256;
#[derive(Clone, Debug)]
pub struct CharacterTable {
field: NumberField,
exponent: u64,
prime: u64,
classes: ConjugacyClasses,
degrees: Vec<u64>,
values: Vec<Vec<NumberFieldElement>>,
}
impl CharacterTable {
pub fn of(group: &PermutationGroup) -> Result<Self, CharacterError> {
Self::of_with_cap(group, DEFAULT_CHARACTER_TABLE_CAP)
}
pub fn of_with_cap(group: &PermutationGroup, cap: u64) -> Result<Self, CharacterError> {
let classes = ConjugacyClasses::of_with_cap(group, cap)?;
Self::from_classes(classes)
}
pub fn from_classes(classes: ConjugacyClasses) -> Result<Self, CharacterError> {
let r = classes.len();
let exponent = classes.exponent();
let phi = euler_phi(exponent);
if phi > MAX_EXPONENT_FIELD_DEGREE {
return Err(CharacterError::ExponentFieldTooLarge {
exponent,
degree: phi,
max: MAX_EXPONENT_FIELD_DEGREE,
});
}
let field = NumberField::cyclotomic(exponent)?;
let zeta = field.generator();
let order = to_u64(classes.group_order(), "|G|")?;
let p = dixon::working_prime(exponent, order)?;
let w = dixon::root_of_unity(p, exponent)?;
let mut matrices = Vec::with_capacity(r);
for k in 0..r {
matrices.push(classes.multiplication_matrix(k)?);
}
let eigenvectors = dixon::common_eigenvectors(p, r, &matrices)?;
let mut size_inverses = Vec::with_capacity(r);
let mut inverse_class = Vec::with_capacity(r);
let mut power_classes: Vec<Vec<usize>> = Vec::with_capacity(r);
for i in 0..r {
let class = classes.class(i)?;
size_inverses.push(dixon::inv_mod(class.size() % p, p)?);
inverse_class.push(classes.inverse_class(i)?);
let m = class.element_order();
let mut powers = Vec::with_capacity(m as usize);
for l in 0..m {
let g_l = class.representative().pow(l as i64);
powers.push(classes.class_of(&g_l)?);
}
power_classes.push(powers);
}
let order_mod_p = order % p;
let mut rows: Vec<(u64, Vec<NumberFieldElement>)> = Vec::with_capacity(r);
for vector in &eigenvectors {
if vector.len() != r {
return Err(CharacterError::Internal {
detail: format!("an eigenvector has length {}, not {r}", vector.len()),
});
}
let scale = dixon::inv_mod(vector[0], p)?;
let omega: Vec<u64> = vector
.iter()
.map(|&v| dixon::mul_mod(v, scale, p))
.collect();
let mut accumulator = 0u64;
for i in 0..r {
let term = dixon::mul_mod(
dixon::mul_mod(omega[i], omega[inverse_class[i]], p),
size_inverses[i],
p,
);
accumulator = (accumulator + term) % p;
}
let square = dixon::mul_mod(order_mod_p, dixon::inv_mod(accumulator, p)?, p);
let degree = isqrt(square);
if degree == 0 || degree * degree != square || order % degree != 0 {
return Err(CharacterError::SelfCheckFailed {
check: "degree",
detail: format!(
"chi(1)^2 lifted to {square}, which is not the square of a divisor of \
|G| = {order}"
),
});
}
let residues: Vec<u64> = (0..r)
.map(|i| {
dixon::mul_mod(dixon::mul_mod(degree % p, omega[i], p), size_inverses[i], p)
})
.collect();
let mut row = Vec::with_capacity(r);
for powers in &power_classes {
row.push(lift_value(
&field, &zeta, p, w, exponent, degree, &residues, powers,
)?);
}
rows.push((degree, row));
}
let one = field.one();
let width = field.degree();
rows.sort_by_cached_key(|(degree, row)| {
let trivial = if row.iter().all(|v| *v == one) {
0u8
} else {
1u8
};
let coords: Vec<Vec<Rational>> = row.iter().map(|v| padded(v, width)).collect();
(trivial, *degree, coords)
});
let degrees: Vec<u64> = rows.iter().map(|(d, _)| *d).collect();
let values: Vec<Vec<NumberFieldElement>> = rows.into_iter().map(|(_, row)| row).collect();
let table = CharacterTable {
field,
exponent,
prime: p,
classes,
degrees,
values,
};
table.verify()?;
Ok(table)
}
pub fn field(&self) -> &NumberField {
&self.field
}
pub fn exponent(&self) -> u64 {
self.exponent
}
pub fn working_prime(&self) -> u64 {
self.prime
}
pub fn classes(&self) -> &ConjugacyClasses {
&self.classes
}
pub fn len(&self) -> usize {
self.values.len()
}
pub fn is_empty(&self) -> bool {
self.values.is_empty()
}
pub fn degrees(&self) -> &[u64] {
&self.degrees
}
pub fn character(&self, i: usize) -> Result<&[NumberFieldElement], CharacterError> {
self.values
.get(i)
.map(|row| row.as_slice())
.ok_or(CharacterError::ClassIndexOutOfRange {
index: i,
classes: self.values.len(),
})
}
pub fn value(&self, i: usize, c: usize) -> Result<&NumberFieldElement, CharacterError> {
let row = self
.values
.get(i)
.ok_or(CharacterError::ClassIndexOutOfRange {
index: i,
classes: self.values.len(),
})?;
row.get(c).ok_or(CharacterError::ClassIndexOutOfRange {
index: c,
classes: row.len(),
})
}
pub fn inner_product(&self, i: usize, j: usize) -> Result<Rational, CharacterError> {
let left = self.character(i)?;
let right = self.character(j)?;
let mut total = self.field.zero();
for (c, value) in left.iter().enumerate() {
let inverse = self.classes.inverse_class(c)?;
let weight = Rational::from(self.classes.class(c)?.size());
let term = value
.mul(&right[inverse])?
.mul(&self.field.rational(&weight))?;
total = total.add(&term)?;
}
let order = Rational::from(self.classes.group_order().clone());
let scaled = total.mul(&self.field.rational(&order.recip()))?;
as_rational(&scaled).ok_or_else(|| CharacterError::SelfCheckFailed {
check: "row orthogonality",
detail: format!(
"<chi_{i}, chi_{j}> came out as {scaled}, which is not a rational number"
),
})
}
pub fn verify(&self) -> Result<(), CharacterError> {
let r = self.classes.len();
if self.values.len() != r {
return Err(CharacterError::SelfCheckFailed {
check: "character count",
detail: format!(
"{} irreducible characters for {r} conjugacy classes",
self.values.len()
),
});
}
let order = self.classes.group_order().clone();
let mut square_sum = Integer::new();
for (i, &d) in self.degrees.iter().enumerate() {
if d == 0 || !order.is_divisible(&Integer::from(d)) {
return Err(CharacterError::SelfCheckFailed {
check: "degree",
detail: format!("chi_{i}(1) = {d} does not divide |G| = {order}"),
});
}
let value = self.value(i, 0)?;
if *value != self.field.rational(&Rational::from(d)) {
return Err(CharacterError::SelfCheckFailed {
check: "degree",
detail: format!("chi_{i}(1) is recorded as {d} but the table holds {value}"),
});
}
square_sum += Integer::from(d) * Integer::from(d);
}
if square_sum != order {
return Err(CharacterError::SelfCheckFailed {
check: "degree sum",
detail: format!("the squared degrees sum to {square_sum}, not to |G| = {order}"),
});
}
for i in 0..r {
for j in 0..r {
let product = self.inner_product(i, j)?;
let expected = if i == j { 1 } else { 0 };
if product != expected {
return Err(CharacterError::SelfCheckFailed {
check: "row orthogonality",
detail: format!("<chi_{i}, chi_{j}> = {product}, expected {expected}"),
});
}
}
}
for c in 0..r {
for d in 0..r {
let inverse = self.classes.inverse_class(d)?;
let mut total = self.field.zero();
for i in 0..r {
total = total.add(&self.value(i, c)?.mul(self.value(i, inverse)?)?)?;
}
let expected = if c == d {
Rational::from(self.classes.class(c)?.centraliser_order().clone())
} else {
Rational::new()
};
if total != self.field.rational(&expected) {
return Err(CharacterError::SelfCheckFailed {
check: "column orthogonality",
detail: format!(
"sum_i chi_i(g_{c}) conj(chi_i(g_{d})) = {total}, expected {expected}"
),
});
}
}
}
Ok(())
}
pub fn render(&self) -> String {
let mut cells: Vec<Vec<String>> = Vec::with_capacity(self.values.len() + 1);
let mut header = vec![format!("Q(zeta_{})", self.exponent)];
for class in self.classes.classes() {
header.push(format!("{}^{}", class.element_order(), class.size()));
}
cells.push(header);
for (i, row) in self.values.iter().enumerate() {
let mut line = vec![format!("chi_{i}")];
for value in row {
line.push(value.to_string());
}
cells.push(line);
}
let columns = cells[0].len();
let widths: Vec<usize> = (0..columns)
.map(|j| {
cells
.iter()
.map(|row| row[j].chars().count())
.max()
.unwrap_or(0)
})
.collect();
let mut out = String::new();
for row in &cells {
for (j, cell) in row.iter().enumerate() {
if j > 0 {
out.push_str(" ");
}
out.push_str(cell);
for _ in cell.chars().count()..widths[j] {
out.push(' ');
}
}
while out.ends_with(' ') {
out.pop();
}
out.push('\n');
}
out
}
}
impl std::fmt::Display for CharacterTable {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.render())
}
}
#[allow(clippy::too_many_arguments)]
fn lift_value(
field: &NumberField,
zeta: &NumberFieldElement,
p: u64,
w: u64,
exponent: u64,
degree: u64,
residues: &[u64],
powers: &[usize],
) -> Result<NumberFieldElement, CharacterError> {
let m = powers.len() as u64;
if m == 0 || exponent % m != 0 {
return Err(CharacterError::Internal {
detail: format!("an element order of {m} does not divide exp G = {exponent}"),
});
}
let z = dixon::pow_mod(w, exponent / m, p);
let m_inverse = dixon::inv_mod(m % p, p)?;
let mut total = field.zero();
let mut multiplicity_sum = 0u64;
for k in 0..m {
let mut accumulator = 0u64;
for (l, &class) in powers.iter().enumerate() {
let shift = (m - (k * l as u64) % m) % m;
let term = dixon::mul_mod(residues[class], dixon::pow_mod(z, shift, p), p);
accumulator = (accumulator + term) % p;
}
let multiplicity = dixon::mul_mod(accumulator, m_inverse, p);
if multiplicity > degree {
return Err(CharacterError::SelfCheckFailed {
check: "eigenvalue multiplicity",
detail: format!(
"the multiplicity of zeta_{m}^{k} lifted to {multiplicity}, above the \
character degree {degree}"
),
});
}
multiplicity_sum += multiplicity;
if multiplicity != 0 {
let power = zeta.pow(k * (exponent / m));
let scaled = power.mul(&field.rational(&Rational::from(multiplicity)))?;
total = total.add(&scaled)?;
}
}
if multiplicity_sum != degree {
return Err(CharacterError::SelfCheckFailed {
check: "eigenvalue multiplicity",
detail: format!(
"the eigenvalue multiplicities of an element of order {m} sum to \
{multiplicity_sum}, not to the character degree {degree}"
),
});
}
Ok(total)
}
fn padded(value: &NumberFieldElement, width: usize) -> Vec<Rational> {
let mut coefficients = value.coefficients();
coefficients.resize(width.max(coefficients.len()), Rational::new());
coefficients
}
fn as_rational(value: &NumberFieldElement) -> Option<Rational> {
let coefficients = value.coefficients();
let constant = coefficients.first().cloned().unwrap_or_else(Rational::new);
if coefficients.iter().skip(1).all(|c| *c == 0) {
Some(constant)
} else {
None
}
}
fn isqrt(n: u64) -> u64 {
if n == 0 {
return 0;
}
let mut x = (n as f64).sqrt() as u64;
while x > 0 && x.saturating_mul(x) > n {
x -= 1;
}
while (x + 1).saturating_mul(x + 1) <= n {
x += 1;
}
x
}
fn euler_phi(n: u64) -> usize {
if n == 0 {
return 0;
}
let mut result = n;
for q in dixon::distinct_prime_factors(n) {
result = result / q * (q - 1);
}
result as usize
}
#[cfg(kani)]
mod verification {
use super::*;
#[kani::proof]
#[kani::unwind(4)]
fn isqrt_is_floor_sqrt() {
let n: u64 = kani::any_where(|n: &u64| *n < (1 << 16));
let x = isqrt(n);
assert!(x * x <= n);
assert!((x + 1) * (x + 1) > n);
}
}