use super::complex::Complex64;
use super::gauss_sum::{mat_approx_eq, mat_identity, mat_mul, mat_pow, mat_scale, GaussSum};
use crate::forms::integral::diagonal::{
rat_val, rational_congruence_diagonal, rational_mod_int, unit_mod8, DegenerateBehavior,
};
use crate::forms::integral::{is_prime_power, Genus, IntegralForm};
use crate::linalg::field::inverse_matrix;
use crate::linalg::integer::{gcd, normalize_relation_rows, prime_factors, reduce_integer_vector};
use crate::scalar::{Rational, Scalar};
use std::collections::BTreeSet;
use std::collections::HashSet;
use std::fmt;
fn rational_to_f64(x: &Rational) -> f64 {
(x.numer() as f64) / (x.denom() as f64)
}
fn dot_inv(v: &[i128], inv: &[Vec<Rational>], w: &[i128]) -> Rational {
let n = v.len();
let mut acc = Rational::zero();
for i in 0..n {
if v[i] == 0 {
continue;
}
for (j, &wj) in w.iter().enumerate() {
if wj == 0 {
continue;
}
acc = acc.add(
&Rational::from_int(v[i])
.mul(&inv[i][j])
.mul(&Rational::from_int(wj)),
);
}
}
acc
}
fn enumerate_hnf_reps(rows: &[Vec<i128>]) -> Option<Vec<Vec<i128>>> {
let n = rows.len();
if n == 0 {
return Some(vec![Vec::new()]);
}
if rows.iter().any(|r| r.len() != n) {
return None;
}
let mut pivots = Vec::with_capacity(n);
for (i, row) in rows.iter().enumerate() {
let lead = row.iter().position(|&x| x != 0)?;
if lead != i || row[i] <= 0 {
return None;
}
pivots.push(row[i]);
}
let mut reps = BTreeSet::new();
let mut cur = vec![0i128; n];
fn rec(
idx: usize,
pivots: &[i128],
cur: &mut [i128],
rows: &[Vec<i128>],
reps: &mut BTreeSet<Vec<i128>>,
) {
if idx == pivots.len() {
let mut v = cur.to_vec();
reduce_integer_vector(&mut v, rows.to_vec());
reps.insert(v);
return;
}
for x in 0..pivots[idx] {
cur[idx] = x;
rec(idx + 1, pivots, cur, rows, reps);
}
cur[idx] = 0;
}
rec(0, &pivots, &mut cur, rows, &mut reps);
Some(reps.into_iter().collect())
}
fn pow_mod8(mut base: i128, mut exp: u128) -> i128 {
base = base.rem_euclid(8);
let mut acc = 1i128;
while exp > 0 {
if exp & 1 == 1 {
acc = (acc * base).rem_euclid(8);
}
base = (base * base).rem_euclid(8);
exp >>= 1;
}
acc
}
fn is_antisquare_2(u: i128) -> bool {
matches!(u.rem_euclid(8), 3 | 5)
}
fn diagonal_entries(lattice: &IntegralForm) -> Option<Vec<Rational>> {
if lattice.determinant() == 0 {
return None;
}
Some(rational_congruence_diagonal(
lattice.gram(),
DegenerateBehavior::RequireNonsingular,
))
}
fn two_adic_oddity(diag: &[Rational]) -> i128 {
diag.iter()
.map(|d| {
let u = unit_mod8(d);
let antisquare = rat_val(d, 2).rem_euclid(2) != 0 && is_antisquare_2(u);
(u + if antisquare { 4 } else { 0 }).rem_euclid(8)
})
.sum::<i128>()
.rem_euclid(8)
}
fn genus_oddity_and_p_excess_mod8(lattice: &IntegralForm) -> Option<(i128, i128)> {
let genus = Genus::from_lattice(lattice)?;
let oddity = genus
.symbol_at(2)
.iter()
.map(|s| s.oddity)
.sum::<i128>()
.rem_euclid(8);
let p_excess = genus
.primes()
.into_iter()
.filter(|&p| p != 2)
.flat_map(|p| {
genus
.symbol_at(p)
.iter()
.map(move |s| symbol_p_excess_mod8(p, s.scale, s.dim, s.sign))
})
.sum::<i128>()
.rem_euclid(8);
Some((oddity, p_excess))
}
fn symbol_p_excess_mod8(p: u128, scale: u128, dim: usize, sign: i128) -> i128 {
let q = pow_mod8(p as i128, scale);
let antisquare = if scale % 2 == 1 && sign < 0 { 4 } else { 0 };
((dim as i128) * (q - 1) + antisquare).rem_euclid(8)
}
const ISO_GROUP_CAP: usize = 256;
const ISO_NODE_BUDGET: u128 = 50_000_000;
pub(crate) struct IsoTables {
pub(crate) zero: usize,
pub(crate) q: Vec<Rational>,
pub(crate) order: Vec<usize>,
pub(crate) add: Vec<Vec<usize>>,
}
fn checked_i128_add(a: i128, b: i128) -> Option<i128> {
a.checked_add(b)
}
fn checked_i128_sub(a: i128, b: i128) -> Option<i128> {
a.checked_sub(b)
}
fn checked_i128_mul(a: i128, b: i128) -> Option<i128> {
a.checked_mul(b)
}
fn lcm_usize(a: usize, b: usize) -> Option<usize> {
let g = usize::try_from(gcd(i128::try_from(a).ok()?, i128::try_from(b).ok()?)).ok()?;
a.checked_div(g)?.checked_mul(b)
}
fn divisors(n: usize) -> Vec<usize> {
let mut out = Vec::new();
let mut d = 1usize;
while d <= n / d {
if n.is_multiple_of(d) {
out.push(d);
if d != n / d {
out.push(n / d);
}
}
d += 1;
}
out.sort_unstable();
out
}
fn poly_trim(mut p: Vec<i128>) -> Vec<i128> {
while p.len() > 1 && p.last() == Some(&0) {
p.pop();
}
p
}
fn poly_mul(a: &[i128], b: &[i128]) -> Option<Vec<i128>> {
if a.is_empty() || b.is_empty() {
return Some(vec![0]);
}
let mut out = vec![0i128; a.len() + b.len() - 1];
for (i, &x) in a.iter().enumerate() {
if x == 0 {
continue;
}
for (j, &y) in b.iter().enumerate() {
if y == 0 {
continue;
}
let term = checked_i128_mul(x, y)?;
out[i + j] = checked_i128_add(out[i + j], term)?;
}
}
Some(poly_trim(out))
}
fn poly_div_exact(num: &[i128], den: &[i128]) -> Option<Vec<i128>> {
if den.is_empty() || den.last() != Some(&1) {
return None;
}
if num.len() < den.len() {
return if num.iter().all(|&x| x == 0) {
Some(vec![0])
} else {
None
};
}
let den_deg = den.len() - 1;
let q_len = num.len() - den_deg;
let mut rem = num.to_vec();
let mut q = vec![0i128; q_len];
for k in (0..q_len).rev() {
let coeff = rem[k + den_deg];
q[k] = coeff;
if coeff == 0 {
continue;
}
for j in 0..=den_deg {
let term = checked_i128_mul(coeff, den[j])?;
rem[k + j] = checked_i128_sub(rem[k + j], term)?;
}
}
if rem[..den_deg].iter().any(|&x| x != 0) || rem[den_deg..].iter().any(|&x| x != 0) {
return None;
}
Some(poly_trim(q))
}
fn cyclotomic_polynomial(
n: usize,
cache: &mut std::collections::BTreeMap<usize, Vec<i128>>,
) -> Option<Vec<i128>> {
if let Some(p) = cache.get(&n) {
return Some(p.clone());
}
let phi = if n == 1 {
vec![-1, 1]
} else {
let mut numerator = vec![0i128; n + 1];
numerator[0] = -1;
numerator[n] = 1;
let mut product = vec![1i128];
for d in divisors(n).into_iter().filter(|&d| d < n) {
let pd = cyclotomic_polynomial(d, cache)?;
product = poly_mul(&product, &pd)?;
}
poly_div_exact(&numerator, &product)?
};
cache.insert(n, phi.clone());
Some(phi)
}
fn reduce_cyclotomic(mut p: Vec<i128>, phi: &[i128]) -> Option<Vec<i128>> {
let degree = phi.len().checked_sub(1)?;
if degree == 0 {
return None;
}
while p.len() > degree {
let high_idx = p.len() - 1;
let coeff = p.pop().expect("length checked");
if coeff == 0 {
continue;
}
let offset = high_idx - degree;
for (j, &c) in phi[..degree].iter().enumerate() {
let term = checked_i128_mul(coeff, c)?;
p[offset + j] = checked_i128_sub(p[offset + j], term)?;
}
}
p.resize(degree, 0);
Some(p)
}
#[derive(Clone, Debug, PartialEq, Eq)]
struct Cyclo {
coeffs: Vec<i128>,
}
struct CycloContext {
order: usize,
phi: Vec<i128>,
powers: Vec<Vec<i128>>,
}
impl CycloContext {
fn new(order: usize) -> Option<Self> {
if order == 0 || order > FQM_CYCLOTOMIC_ORDER_CAP {
return None;
}
let mut cache = std::collections::BTreeMap::new();
let phi = cyclotomic_polynomial(order, &mut cache)?;
let mut powers = Vec::with_capacity(order);
for k in 0..order {
let mut p = vec![0i128; k + 1];
p[k] = 1;
powers.push(reduce_cyclotomic(p, &phi)?);
}
Some(CycloContext { order, phi, powers })
}
fn zero(&self) -> Cyclo {
Cyclo {
coeffs: vec![0; self.phi.len() - 1],
}
}
fn constant(&self, c: i128) -> Cyclo {
let mut out = self.zero();
out.coeffs[0] = c;
out
}
fn root_power(&self, exp: isize) -> Cyclo {
let order = self.order as isize;
let idx = exp.rem_euclid(order) as usize;
Cyclo {
coeffs: self.powers[idx].clone(),
}
}
}
impl Cyclo {
fn add_assign(&mut self, rhs: &Cyclo) -> Option<()> {
for (a, &b) in self.coeffs.iter_mut().zip(&rhs.coeffs) {
*a = checked_i128_add(*a, b)?;
}
Some(())
}
fn mul(&self, rhs: &Cyclo, ctx: &CycloContext) -> Option<Cyclo> {
let mut raw = vec![0i128; self.coeffs.len() + rhs.coeffs.len() - 1];
for (i, &x) in self.coeffs.iter().enumerate() {
if x == 0 {
continue;
}
for (j, &y) in rhs.coeffs.iter().enumerate() {
if y == 0 {
continue;
}
let term = checked_i128_mul(x, y)?;
raw[i + j] = checked_i128_add(raw[i + j], term)?;
}
}
Some(Cyclo {
coeffs: reduce_cyclotomic(raw, &ctx.phi)?,
})
}
fn mul_root(&self, exp: isize, ctx: &CycloContext) -> Option<Cyclo> {
self.mul(&ctx.root_power(exp), ctx)
}
fn conjugate(&self, ctx: &CycloContext) -> Option<Cyclo> {
let mut out = ctx.zero();
for (i, &c) in self.coeffs.iter().enumerate() {
if c == 0 {
continue;
}
let mut term = ctx.root_power(-(i as isize));
for x in &mut term.coeffs {
*x = checked_i128_mul(*x, c)?;
}
out.add_assign(&term)?;
}
Some(out)
}
fn principal_real_f64(&self, ctx: &CycloContext) -> f64 {
let step = std::f64::consts::TAU / (ctx.order as f64);
self.coeffs
.iter()
.enumerate()
.map(|(k, &c)| (c as f64) * ((k as f64) * step).cos())
.sum()
}
}
const FQM_GAUSS_GROUP_CAP: usize = 4096;
const FQM_CYCLOTOMIC_ORDER_CAP: usize = 4096;
pub(crate) fn phase_mod8_from_q_values<'a>(
q_values: impl IntoIterator<Item = &'a Rational>,
group_order: usize,
) -> Option<i128> {
let q_values: Vec<Rational> = q_values.into_iter().cloned().collect();
if q_values.len() != group_order {
return None;
}
let mut root_order = 8usize;
for q in &q_values {
let den = usize::try_from(q.denom()).ok()?;
root_order = lcm_usize(root_order, den.checked_mul(2)?)?;
}
let ctx = CycloContext::new(root_order)?;
let mut sum = ctx.zero();
for q in &q_values {
let den = usize::try_from(q.denom()).ok()?;
let period = den.checked_mul(2)?;
let numer = q.numer().rem_euclid(i128::try_from(period).ok()?);
let scale = root_order.checked_div(period)?;
let exp = usize::try_from(numer).ok()?.checked_mul(scale)? % root_order;
sum.add_assign(&ctx.root_power(exp as isize))?;
}
let order_const = ctx.constant(i128::try_from(group_order).ok()?);
let eighth_shift = root_order.checked_div(8)?;
let mut candidates = Vec::new();
for beta in 0..8i128 {
let shift = -isize::try_from(beta.checked_mul(i128::try_from(eighth_shift).ok()?)?).ok()?;
let t = sum.mul_root(shift, &ctx)?;
if t.conjugate(&ctx)? != t {
continue;
}
if t.mul(&t, &ctx)? == order_const {
candidates.push((beta, t));
}
}
match candidates.as_slice() {
[(beta, _)] => Some(*beta),
[] => None,
_ => {
candidates
.into_iter()
.find(|(_, t)| t.principal_real_f64(&ctx) > 0.0)
.map(|(beta, _)| beta)
}
}
}
fn subgroup_closure(t: &IsoTables, gens: &[usize]) -> HashSet<usize> {
let mut set: HashSet<usize> = HashSet::new();
set.insert(t.zero);
let mut frontier = vec![t.zero];
while let Some(x) = frontier.pop() {
for &g in gens {
let nx = t.add[x][g];
if set.insert(nx) {
frontier.push(nx);
}
}
}
set
}
fn min_generators(t: &IsoTables) -> Vec<usize> {
let n = t.order.len();
let mut gens: Vec<usize> = Vec::new();
let mut covered = subgroup_closure(t, &gens);
while covered.len() < n {
let g = (0..n)
.filter(|i| !covered.contains(i))
.max_by_key(|&i| t.order[i])
.expect("a non-covered element exists while |covered| < |A|");
gens.push(g);
covered = subgroup_closure(t, &gens);
}
gens
}
fn verify_iso(lt: &IsoTables, mt: &IsoTables, gens: &[usize], img: &[usize]) -> bool {
let n = lt.order.len();
let mut phi = vec![usize::MAX; n];
phi[lt.zero] = mt.zero;
let mut frontier = vec![lt.zero];
while let Some(x) = frontier.pop() {
for (t, &g) in gens.iter().enumerate() {
let nx = lt.add[x][g];
let nimg = mt.add[phi[x]][img[t]];
if phi[nx] == usize::MAX {
phi[nx] = nimg;
frontier.push(nx);
} else if phi[nx] != nimg {
return false; }
}
}
if phi.contains(&usize::MAX) {
return false; }
let mut seen: HashSet<usize> = HashSet::new();
if !phi.iter().all(|&p| seen.insert(p)) {
return false;
}
(0..n).all(|i| mt.q[phi[i]] == lt.q[i])
}
fn search_iso(
lt: &IsoTables,
mt: &IsoTables,
gens: &[usize],
img: &mut Vec<usize>,
budget: &mut u128,
) -> Option<bool> {
let depth = img.len();
if depth == gens.len() {
return Some(verify_iso(lt, mt, gens, img));
}
let g = gens[depth];
for cand in 0..mt.order.len() {
if mt.order[cand] != lt.order[g] || mt.q[cand] != lt.q[g] {
continue;
}
if *budget == 0 {
return None;
}
*budget -= 1;
img.push(cand);
match search_iso(lt, mt, gens, img, budget) {
Some(true) => return Some(true),
Some(false) => {}
None => return None,
}
img.pop();
}
Some(false)
}
#[derive(Clone, Debug, PartialEq)]
struct DiscriminantCore {
group: Vec<i128>,
reps: Vec<Vec<i128>>,
gram_inv: Vec<Vec<Rational>>,
}
impl DiscriminantCore {
fn from_lattice(lattice: &IntegralForm) -> Option<Self> {
if lattice.determinant() == 0 {
return None;
}
let mat: Vec<Vec<Rational>> = lattice
.gram()
.iter()
.map(|row| row.iter().map(|&x| Rational::from_int(x)).collect())
.collect();
let gram_inv = inverse_matrix(mat)?;
let hnf = normalize_relation_rows(lattice.gram().to_vec());
let reps = enumerate_hnf_reps(&hnf)?;
let det = lattice.determinant().unsigned_abs() as usize;
if reps.len() != det {
return None;
}
let group = lattice
.invariant_factors()
.into_iter()
.filter(|&d| d > 1)
.collect();
Some(DiscriminantCore {
group,
reps,
gram_inv,
})
}
fn bilinear_value_mod1(&self, y: &[i128], z: &[i128]) -> Rational {
rational_mod_int(dot_inv(y, &self.gram_inv, z), 1)
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct DiscriminantForm {
core: DiscriminantCore,
}
#[derive(Clone, Debug, PartialEq)]
pub struct OddDiscriminantForm {
core: DiscriminantCore,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct OddMilgramInvariants {
pub signature_mod8: i128,
pub oddity_mod8: i128,
pub p_excess_mod8: i128,
pub corrected_signature_mod8: i128,
pub genus_signature_mod8: i128,
}
impl OddMilgramInvariants {
pub fn verified(&self) -> bool {
self.corrected_signature_mod8 == self.signature_mod8
&& self.genus_signature_mod8 == self.signature_mod8
}
pub fn display(&self) -> String {
self.to_string()
}
}
impl fmt::Display for OddMilgramInvariants {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"OddMilgramInvariants(signature_mod8={}, oddity_mod8={}, p_excess_mod8={}, corrected_signature_mod8={}, genus_signature_mod8={}, verified={})",
self.signature_mod8,
self.oddity_mod8,
self.p_excess_mod8,
self.corrected_signature_mod8,
self.genus_signature_mod8,
self.verified(),
)
}
}
impl DiscriminantForm {
pub fn from_lattice(lattice: &IntegralForm) -> Option<Self> {
if !lattice.is_even() {
return None;
}
Some(DiscriminantForm {
core: DiscriminantCore::from_lattice(lattice)?,
})
}
pub fn group(&self) -> &[i128] {
&self.core.group
}
pub fn reps(&self) -> &[Vec<i128>] {
&self.core.reps
}
pub fn gram_inv(&self) -> &[Vec<Rational>] {
&self.core.gram_inv
}
pub fn quadratic_value_mod2(&self, y: &[i128]) -> Rational {
rational_mod_int(dot_inv(y, &self.core.gram_inv, y), 2)
}
pub fn bilinear_value_mod1(&self, y: &[i128], z: &[i128]) -> Rational {
self.core.bilinear_value_mod1(y, z)
}
pub fn gauss_sum(&self) -> GaussSum {
let mut re = 0.0f64;
let mut im = 0.0f64;
for y in &self.core.reps {
let theta = std::f64::consts::PI * rational_to_f64(&self.quadratic_value_mod2(y));
re += theta.cos();
im += theta.sin();
}
let scale = 1.0 / (self.core.reps.len() as f64).sqrt();
GaussSum {
re: re * scale,
im: im * scale,
}
}
pub fn milgram_signature_mod8(&self) -> Option<i128> {
self.gauss_sum().phase_mod8(1e-8)
}
pub fn brown_invariant(&self) -> Option<crate::forms::BrownInvariants> {
use crate::forms::char2::beta_from_gauss;
if !self.core.group.iter().all(|&d| d == 2) {
return None;
}
let mut counts = [0i128; 4];
for gamma in &self.core.reps {
let two_q = self.quadratic_value_mod2(gamma);
let two_q = two_q.add(&two_q);
if !two_q.is_integer() {
return None; }
counts[two_q.numer().rem_euclid(4) as usize] += 1;
}
let re = counts[0] - counts[2];
let im = counts[1] - counts[3];
Some(crate::forms::BrownInvariants {
beta: beta_from_gauss(re, im)?,
rank: self.core.group.len(),
radical_dim: 0,
radical_anisotropic: false,
})
}
fn element_index(&self, v: &[i128]) -> Option<usize> {
self.core
.reps
.iter()
.position(|r| self.equivalent_mod_lattice(r, v))
}
pub(crate) fn tables_bounded(&self, group_cap: usize) -> Option<IsoTables> {
let n = self.core.reps.len();
if n > group_cap {
return None;
}
let zero = self
.core
.reps
.iter()
.position(|r| r.iter().all(|&x| x == 0))?;
let q: Vec<Rational> = self
.core
.reps
.iter()
.map(|r| self.quadratic_value_mod2(r))
.collect();
let mut add = vec![vec![0usize; n]; n];
for i in 0..n {
for j in 0..n {
let s: Vec<i128> = self.core.reps[i]
.iter()
.zip(&self.core.reps[j])
.map(|(&a, &b)| a + b)
.collect();
add[i][j] = self.element_index(&s)?;
}
}
let mut order = vec![1usize; n];
for i in 0..n {
let mut cur = i;
let mut k = 1usize;
while cur != zero {
cur = add[cur][i];
k += 1;
}
order[i] = k;
}
Some(IsoTables {
zero,
q,
order,
add,
})
}
pub fn fqm_gauss_phase(&self) -> Option<super::phases::FqmGaussPhase> {
use super::phases::FqmPrimaryPhase;
let tables = self.tables_bounded(FQM_GAUSS_GROUP_CAP)?;
let order = self.core.reps.len();
let total = phase_mod8_from_q_values(tables.q.iter(), order)?;
let mut primes = BTreeSet::new();
for &d in &self.core.group {
for p in prime_factors(d.unsigned_abs()) {
primes.insert(p);
}
}
let mut primary = Vec::new();
for p in primes {
let indices: Vec<usize> = tables
.order
.iter()
.enumerate()
.filter_map(|(i, &ord)| is_prime_power(ord as u128, p).then_some(i))
.collect();
let exponent = indices
.iter()
.map(|&i| tables.order[i] as u128)
.max()
.unwrap_or(1);
let qs: Vec<&Rational> = indices.iter().map(|&i| &tables.q[i]).collect();
let phase_mod8 = phase_mod8_from_q_values(qs, indices.len())?;
primary.push(FqmPrimaryPhase {
prime: p,
order: indices.len(),
exponent,
phase_mod8,
});
}
let sum = primary
.iter()
.map(|c| c.phase_mod8)
.sum::<i128>()
.rem_euclid(8);
if sum != total {
return None;
}
Some(super::phases::FqmGaussPhase {
order,
phase_mod8: total,
primary,
})
}
pub fn milgram_signature_mod8_fqm(&self) -> Option<i128> {
Some(self.fqm_gauss_phase()?.phase_mod8)
}
fn iso_tables(&self) -> Option<IsoTables> {
self.tables_bounded(ISO_GROUP_CAP)
}
pub fn is_isomorphic(&self, other: &Self) -> Option<bool> {
self.is_isomorphic_bounded(other, ISO_NODE_BUDGET)
}
pub fn is_isomorphic_bounded(&self, other: &Self, node_budget: u128) -> Option<bool> {
if self.core.reps.len() != other.core.reps.len() {
return Some(false);
}
let mut g1 = self.core.group.clone();
let mut g2 = other.core.group.clone();
g1.sort_unstable();
g2.sort_unstable();
if g1 != g2 {
return Some(false);
}
let lt = self.iso_tables()?;
let mt = other.iso_tables()?;
let mut ql: Vec<(i128, i128)> = lt.q.iter().map(|x| (x.numer(), x.denom())).collect();
let mut qm: Vec<(i128, i128)> = mt.q.iter().map(|x| (x.numer(), x.denom())).collect();
ql.sort_unstable();
qm.sort_unstable();
if ql != qm {
return Some(false);
}
let gens = min_generators(<);
let mut budget = node_budget;
let mut img: Vec<usize> = Vec::with_capacity(gens.len());
search_iso(<, &mt, &gens, &mut img, &mut budget)
}
fn equivalent_mod_lattice(&self, a: &[i128], b: &[i128]) -> bool {
let n = self.core.gram_inv.len();
if a.len() != n || b.len() != n {
return false;
}
let diff: Vec<i128> = a.iter().zip(b).map(|(&x, &y)| x - y).collect();
for row in &self.core.gram_inv {
let mut coord = Rational::zero();
for (r, &d) in row.iter().zip(&diff) {
if d != 0 {
coord = coord.add(&r.mul(&Rational::from_int(d)));
}
}
if !coord.is_integer() {
return false;
}
}
true
}
fn negation_matrix(&self) -> Option<Vec<Vec<Complex64>>> {
let n = self.core.reps.len();
let mut out = vec![vec![Complex64::zero(); n]; n];
for (col, gamma) in self.core.reps.iter().enumerate() {
let neg_gamma: Vec<i128> = gamma.iter().map(|&x| -x).collect();
let row = self
.core
.reps
.iter()
.position(|delta| self.equivalent_mod_lattice(delta, &neg_gamma))?;
out[row][col] = Complex64::one();
}
Some(out)
}
fn weil_t_matrix(&self) -> Vec<Vec<Complex64>> {
let t = self.weil_t();
let mut out = vec![vec![Complex64::zero(); t.len()]; t.len()];
for (i, z) in t.into_iter().enumerate() {
out[i][i] = z;
}
out
}
pub fn weil_t(&self) -> Vec<Complex64> {
self.core
.reps
.iter()
.map(|gamma| {
let theta =
std::f64::consts::PI * rational_to_f64(&self.quadratic_value_mod2(gamma));
Complex64::cis(theta)
})
.collect()
}
pub fn weil_s_prefactor_phase_mod8(&self) -> Option<i128> {
Some((-self.milgram_signature_mod8()?).rem_euclid(8))
}
pub fn weil_s_recovers_milgram_phase_mod8(&self) -> Option<i128> {
Some((-self.weil_s_prefactor_phase_mod8()?).rem_euclid(8))
}
pub fn weil_s(&self) -> Option<Vec<Vec<Complex64>>> {
let n = self.core.reps.len();
if n == 0 {
return None;
}
let sigma = Complex64::eighth_root(self.weil_s_prefactor_phase_mod8()?);
let scale = 1.0 / (n as f64).sqrt();
let mut out = vec![vec![Complex64::zero(); n]; n];
for (col, gamma) in self.core.reps.iter().enumerate() {
for (row, delta) in self.core.reps.iter().enumerate() {
let theta = -2.0
* std::f64::consts::PI
* rational_to_f64(&self.bilinear_value_mod1(gamma, delta));
out[row][col] = sigma.mul(&Complex64::cis(theta)).scale(scale);
}
}
Some(out)
}
pub fn verify_weil_relations(&self) -> bool {
let Some(s_phase) = self.weil_s_prefactor_phase_mod8() else {
return false;
};
if self.weil_s_recovers_milgram_phase_mod8() != self.milgram_signature_mod8() {
return false;
}
let Some(s) = self.weil_s() else {
return false;
};
let t = self.weil_t_matrix();
let Some(neg) = self.negation_matrix() else {
return false;
};
let tol = 1e-8;
if self.weil_t().iter().any(|z| (z.abs() - 1.0).abs() > tol) {
return false;
}
let s2 = mat_pow(&s, 2);
let s4 = mat_pow(&s, 4);
let st3 = mat_pow(&mat_mul(&s, &t), 3);
let s2_target = mat_scale(&neg, Complex64::eighth_root(2 * s_phase));
let s4_target = mat_scale(
&mat_identity(self.core.reps.len()),
Complex64::eighth_root(4 * s_phase),
);
mat_approx_eq(&s2, &s2_target, tol)
&& mat_approx_eq(&s4, &s4_target, tol)
&& mat_approx_eq(&st3, &s2, tol)
}
}
impl OddDiscriminantForm {
pub fn from_lattice(lattice: &IntegralForm) -> Option<Self> {
if lattice.is_even() {
return None;
}
Some(OddDiscriminantForm {
core: DiscriminantCore::from_lattice(lattice)?,
})
}
pub fn group(&self) -> &[i128] {
&self.core.group
}
pub fn reps(&self) -> &[Vec<i128>] {
&self.core.reps
}
pub fn gram_inv(&self) -> &[Vec<Rational>] {
&self.core.gram_inv
}
pub fn quadratic_value_mod1(&self, y: &[i128]) -> Rational {
rational_mod_int(dot_inv(y, &self.core.gram_inv, y), 1)
}
pub fn bilinear_value_mod1(&self, y: &[i128], z: &[i128]) -> Rational {
self.core.bilinear_value_mod1(y, z)
}
pub fn gauss_sum(&self) -> GaussSum {
let mut re = 0.0f64;
let mut im = 0.0f64;
for y in &self.core.reps {
let theta = std::f64::consts::TAU * rational_to_f64(&self.quadratic_value_mod1(y));
re += theta.cos();
im += theta.sin();
}
let scale = 1.0 / (self.core.reps.len() as f64).sqrt();
GaussSum {
re: re * scale,
im: im * scale,
}
}
pub fn gauss_phase_mod8(&self) -> Option<i128> {
self.gauss_sum().phase_mod8(1e-8)
}
}
pub fn genus_signature_mod8(lattice: &IntegralForm) -> Option<i128> {
let diag = diagonal_entries(lattice)?;
let oddity = two_adic_oddity(&diag);
let (_, p_excess) = genus_oddity_and_p_excess_mod8(lattice)?;
Some((oddity - p_excess).rem_euclid(8))
}
pub fn verify_milgram(lattice: &IntegralForm) -> Option<bool> {
let disc = DiscriminantForm::from_lattice(lattice)?;
let phase = disc.milgram_signature_mod8_fqm()?;
let float_phase = disc.milgram_signature_mod8()?;
let (pos, neg) = lattice.signature();
let sig = (pos as i128 - neg as i128).rem_euclid(8);
let genus_sig = genus_signature_mod8(lattice)?;
Some(phase == sig && float_phase == sig && genus_sig == sig)
}
pub fn odd_milgram_report(lattice: &IntegralForm) -> Option<OddMilgramInvariants> {
let _disc = OddDiscriminantForm::from_lattice(lattice)?;
let (pos, neg) = lattice.signature();
let signature_mod8 = (pos as i128 - neg as i128).rem_euclid(8);
let (oddity_mod8, p_excess_mod8) = genus_oddity_and_p_excess_mod8(lattice)?;
let corrected_signature_mod8 = (oddity_mod8 - p_excess_mod8).rem_euclid(8);
Some(OddMilgramInvariants {
signature_mod8,
oddity_mod8,
p_excess_mod8,
corrected_signature_mod8,
genus_signature_mod8: genus_signature_mod8(lattice)?,
})
}
pub fn verify_odd_milgram(lattice: &IntegralForm) -> Option<bool> {
Some(odd_milgram_report(lattice)?.verified())
}