use crate::forms::{
legendre, relevant_primes, try_chi_kappa, try_kappa_order, try_relevant_places_ff,
try_residue_unit_at, try_valuation_at_ff, unit_part, val_p, FiniteOddField, FunctionFieldPlace,
WittClassG,
};
use crate::scalar::{Poly, RationalFunction, Scalar};
use std::collections::BTreeMap;
pub type FunctionFieldMilnorResidues<S> = (WittClassG, Vec<(FunctionFieldPlace<S>, WittClassG)>);
fn second_residue_at(entries: &[i128], p: u128) -> WittClassG {
let pi = p as i128;
let mut leg_prod: i128 = 1; let mut m: i128 = 0; for &a in entries {
if val_p(a, pi) % 2 == 1 {
leg_prod *= legendre(unit_part(a, pi), pi);
m += 1;
}
}
let leg_neg1 = legendre(-1, pi); let signed_leg = if ((m * (m - 1) / 2) & 1) == 1 {
leg_prod * leg_neg1
} else {
leg_prod
};
WittClassG::OddChar {
field_order: p,
kappa: if leg_neg1 == 1 { 0 } else { 1 },
e0: (m & 1) as u128,
sclass: if signed_leg == 1 { 0 } else { 1 },
}
}
fn dyadic_residue_at(entries: &[i128]) -> WittClassG {
let arf = entries.iter().filter(|&&a| val_p(a, 2) % 2 == 1).count() as u128 & 1;
WittClassG::Char2 {
field_degree: 1,
arf,
}
}
fn is_zero_residue(w: &WittClassG) -> bool {
matches!(
w,
WittClassG::OddChar {
e0: 0,
sclass: 0,
..
} | WittClassG::Char2 { arf: 0, .. }
)
}
pub fn global_residues(entries: &[i128]) -> Option<(i128, BTreeMap<u128, WittClassG>)> {
if entries.contains(&0) {
return None;
}
let signature: i128 = entries.iter().map(|&a| a.signum()).sum();
let mut residues = BTreeMap::new();
for p in relevant_primes(entries) {
let w = if p == 2 {
dyadic_residue_at(entries)
} else {
second_residue_at(entries, p)
};
if !is_zero_residue(&w) {
residues.insert(p, w);
}
}
Some((signature, residues))
}
fn oddchar_witt_from_residue_units<S: FiniteOddField>(
units: &[Poly<S>],
place: &FunctionFieldPlace<S>,
) -> Option<WittClassG> {
let mut chi_prod: i128 = 1;
for unit in units {
chi_prod *= try_chi_kappa(unit, place)?;
}
let m = i128::try_from(units.len()).ok()?;
let field_order = try_kappa_order(place)?;
let chi_neg1 = if field_order % 4 == 1 { 1 } else { -1 };
let signed_chi = if ((m * (m - 1) / 2) & 1) == 1 {
chi_prod * chi_neg1
} else {
chi_prod
};
Some(WittClassG::OddChar {
field_order,
kappa: if chi_neg1 == 1 { 0 } else { 1 },
e0: (m & 1) as u128,
sclass: if signed_chi == 1 { 0 } else { 1 },
})
}
fn second_residue_at_ff<S: FiniteOddField>(
entries: &[RationalFunction<S>],
place: &FunctionFieldPlace<S>,
) -> Option<WittClassG> {
let mut units = Vec::new();
for entry in entries {
if try_valuation_at_ff(entry, place)?.rem_euclid(2) != 0 {
units.push(try_residue_unit_at(entry, place)?);
}
}
oddchar_witt_from_residue_units(&units, place)
}
fn constant_class_at_infinity_ff<S: FiniteOddField>(
entries: &[RationalFunction<S>],
) -> Option<WittClassG> {
let place = FunctionFieldPlace::Infinite;
let mut units = Vec::new();
for entry in entries {
if try_valuation_at_ff(entry, &place)?.rem_euclid(2) == 0 {
units.push(try_residue_unit_at(entry, &place)?);
}
}
oddchar_witt_from_residue_units(&units, &place)
}
pub fn global_residues_ff<S: FiniteOddField>(
entries: &[RationalFunction<S>],
) -> Option<FunctionFieldMilnorResidues<S>> {
if entries.iter().any(|entry| entry.is_zero()) {
return None;
}
let constant = constant_class_at_infinity_ff(entries)?;
let mut residues = Vec::new();
for place in try_relevant_places_ff(entries)? {
if matches!(place, FunctionFieldPlace::Infinite) {
continue;
}
let w = second_residue_at_ff(entries, &place)?;
if !is_zero_residue(&w) {
residues.push((place, w));
}
}
Some((constant, residues))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::clifford::Metric;
use crate::forms::{springer_decompose_qp, try_is_isotropic_q};
use crate::scalar::{Fp, Qp, RationalFunction};
fn springer_residue_q5(entries: &[i128]) -> WittClassG {
type Q5 = Qp<5, 6>;
let metric = Metric::diagonal(entries.iter().map(|&a| Q5::from_int(a)).collect());
let decomp = springer_decompose_qp(&metric).unwrap();
let mut dim = 0usize;
let mut disc_sq = true; for form in decomp.parity_layer(1) {
dim += form.dim;
disc_sq = disc_sq == form.disc_is_square; }
let m = dim as i128;
let leg_neg1 = legendre(-1, 5); let signed_sq = if ((m * (m - 1) / 2) & 1) == 1 && leg_neg1 != 1 {
!disc_sq
} else {
disc_sq
};
WittClassG::OddChar {
field_order: 5,
kappa: if leg_neg1 == 1 { 0 } else { 1 },
e0: (dim & 1) as u128,
sclass: if signed_sq { 0 } else { 1 },
}
}
fn springer_residue_q3(entries: &[i128]) -> WittClassG {
type Q3 = Qp<3, 6>;
let metric = Metric::diagonal(entries.iter().map(|&a| Q3::from_int(a)).collect());
let decomp = springer_decompose_qp(&metric).unwrap();
let mut dim = 0usize;
let mut disc_sq = true; for form in decomp.parity_layer(1) {
dim += form.dim;
disc_sq = disc_sq == form.disc_is_square; }
let m = dim as i128;
let leg_neg1 = legendre(-1, 3); let signed_sq = if ((m * (m - 1) / 2) & 1) == 1 && leg_neg1 != 1 {
!disc_sq
} else {
disc_sq
};
WittClassG::OddChar {
field_order: 3,
kappa: if leg_neg1 == 1 { 0 } else { 1 },
e0: (dim & 1) as u128,
sclass: if signed_sq { 0 } else { 1 },
}
}
fn f2_class(arf: u128) -> WittClassG {
WittClassG::Char2 {
field_degree: 1,
arf,
}
}
type F5 = RationalFunction<Fp<5>>;
type Poly5 = Poly<Fp<5>>;
fn rf(num: &[i128], den: &[i128]) -> F5 {
RationalFunction::new(
num.iter().map(|&n| Fp::<5>::from_int(n)).collect(),
den.iter().map(|&n| Fp::<5>::from_int(n)).collect(),
)
}
fn poly(c: &[i128]) -> Poly5 {
Poly::new(c.iter().map(|&n| Fp::<5>::from_int(n)).collect())
}
fn odd_class(field_order: u128, e0: u128, sclass: u128) -> WittClassG {
WittClassG::OddChar {
field_order,
kappa: if field_order % 4 == 1 { 0 } else { 1 },
e0,
sclass,
}
}
fn residue_at<'a>(
residues: &'a [(FunctionFieldPlace<Fp<5>>, WittClassG)],
place: &FunctionFieldPlace<Fp<5>>,
) -> Option<&'a WittClassG> {
residues.iter().find(|(pl, _)| pl == place).map(|(_, w)| w)
}
#[test]
fn second_residue_matches_springer_over_q5() {
for entries in [
vec![1, 5],
vec![2, 10],
vec![3, 15, 5],
vec![1, 1],
vec![7, 5, 25, 2],
] {
assert_eq!(
second_residue_at(&entries, 5),
springer_residue_q5(&entries),
"∂₅ mismatch on {entries:?}"
);
}
}
#[test]
fn second_residue_matches_springer_over_q3() {
for entries in [
vec![1, 3], vec![3, 6], vec![1, 1, 3, 3], vec![3, 6, 12], vec![2, 4], ] {
assert_eq!(
second_residue_at(&entries, 3),
springer_residue_q3(&entries),
"∂₃ mismatch on {entries:?}"
);
}
}
#[test]
fn dyadic_residue_is_milnors_hand_boundary() {
assert_eq!(dyadic_residue_at(&[1]), f2_class(0));
assert_eq!(dyadic_residue_at(&[2]), f2_class(1));
assert_eq!(dyadic_residue_at(&[-2]), f2_class(1));
assert_eq!(dyadic_residue_at(&[1, 2]), f2_class(1));
assert_eq!(dyadic_residue_at(&[2, -2]), f2_class(0));
}
#[test]
fn global_residues_include_the_dyadic_cell() {
for (entries, signature) in [(&[2i128][..], 1), (&[1, 2], 2), (&[-2], -1)] {
let (sig, res) = global_residues(entries).unwrap();
assert_eq!(sig, signature);
assert_eq!(res.get(&2), Some(&f2_class(1)), "entries={entries:?}");
}
let (sig, res) = global_residues(&[2, -2]).unwrap();
assert_eq!(sig, 0);
assert!(
res.is_empty(),
"the hyperbolic pair <2,-2> has zero residues"
);
let (_, mixed) = global_residues(&[6]).unwrap();
assert_eq!(
mixed.keys().copied().collect::<Vec<_>>(),
vec![2, 3],
"<6> has both dyadic and odd-prime residues"
);
}
#[test]
fn residues_have_finite_support_at_dividing_primes() {
let (sig, res) = global_residues(&[1, 1, 1]).unwrap();
assert_eq!(sig, 3);
assert!(res.is_empty());
let (sig, res) = global_residues(&[3, 5]).unwrap();
assert_eq!(sig, 2);
assert_eq!(res.keys().copied().collect::<Vec<_>>(), vec![3, 5]);
}
#[test]
fn radical_entry_is_rejected() {
assert_eq!(global_residues(&[1, 0, 2]), None);
}
#[test]
fn function_field_residues_split_at_infinity() {
let (constant, residues) = global_residues_ff(&[rf(&[1], &[1])]).unwrap();
assert_eq!(constant, odd_class(5, 1, 0));
assert!(
residues.is_empty(),
"constant forms have no finite residues"
);
let (constant, residues) = global_residues_ff(&[rf(&[0, 1], &[1])]).unwrap();
assert_eq!(constant, odd_class(5, 0, 0));
assert_eq!(
residue_at(&residues, &FunctionFieldPlace::Finite(poly(&[0, 1]))),
Some(&odd_class(5, 1, 0))
);
let (constant, residues) = global_residues_ff(&[rf(&[1], &[0, 1])]).unwrap();
assert_eq!(constant, odd_class(5, 0, 0));
assert_eq!(
residue_at(&residues, &FunctionFieldPlace::Finite(poly(&[0, 1]))),
Some(&odd_class(5, 1, 0))
);
let (constant, residues) = global_residues_ff(&[rf(&[2], &[1])]).unwrap();
assert_eq!(constant, odd_class(5, 1, 1), "2 is nonsquare in F_5");
assert!(residues.is_empty());
}
#[test]
fn function_field_residues_see_degree_two_places() {
let place = FunctionFieldPlace::Finite(poly(&[2, 0, 1])); let (constant, residues) = global_residues_ff(&[rf(&[2, 0, 1], &[1])]).unwrap();
assert_eq!(constant, odd_class(5, 1, 0));
assert_eq!(residue_at(&residues, &place), Some(&odd_class(25, 1, 0)));
}
#[test]
fn function_field_residues_are_square_and_hyperbolic_stable() {
let base = global_residues_ff(&[rf(&[0, 1], &[1])]).unwrap();
let square = rf(&[1, 1], &[1]).mul(&rf(&[1, 1], &[1]));
let square_multiple = global_residues_ff(&[rf(&[0, 1], &[1]).mul(&square)]).unwrap();
assert_eq!(square_multiple, base);
let hyperbolic = global_residues_ff(&[rf(&[0, 1], &[1]), rf(&[0, 4], &[1])]).unwrap();
assert_eq!(hyperbolic.0, odd_class(5, 0, 0));
assert!(hyperbolic.1.is_empty());
}
#[test]
fn function_field_residues_reject_radical_entries() {
assert_eq!(global_residues_ff(&[rf(&[1], &[1]), rf(&[0], &[1])]), None);
}
#[test]
fn witt_invariants_are_square_and_hyperbolic_stable() {
let base = global_residues(&[3]).unwrap();
assert_eq!(global_residues(&[12]).unwrap(), base);
assert_eq!(global_residues(&[3, 1, -1]).unwrap(), base);
let dyadic = global_residues(&[2]).unwrap();
assert_eq!(global_residues(&[8]).unwrap(), dyadic);
assert_eq!(global_residues(&[2, 1, -1]).unwrap(), dyadic);
}
#[test]
fn residues_distinguish_inequivalent_forms() {
let one = global_residues(&[1]).unwrap();
let three = global_residues(&[3]).unwrap();
assert_eq!(one.0, three.0, "same signature");
assert_ne!(one.1, three.1, "different residue at 3");
assert_eq!(try_is_isotropic_q(&[1, -3]), Some(false));
let two = global_residues(&[2]).unwrap();
assert_eq!(one.0, two.0, "same signature");
assert_ne!(one.1, two.1, "different dyadic residue");
assert_eq!(try_is_isotropic_q(&[1, -2]), Some(false));
}
#[test]
fn reconstruction_agrees_with_hasse_minkowski() {
assert_eq!(
global_residues(&[3]).unwrap(),
global_residues(&[12]).unwrap()
);
assert_eq!(try_is_isotropic_q(&[3, -12]), Some(true));
assert_eq!(
global_residues(&[3, 5]).unwrap(),
global_residues(&[12, 45]).unwrap()
);
assert_eq!(try_is_isotropic_q(&[3, 5, -12, -45]), Some(true));
assert_eq!(
global_residues(&[2]).unwrap(),
global_residues(&[8]).unwrap()
);
assert_eq!(try_is_isotropic_q(&[2, -8]), Some(true));
assert_ne!(
global_residues(&[2]).unwrap(),
global_residues(&[1]).unwrap()
);
assert_eq!(try_is_isotropic_q(&[2, -1]), Some(false));
}
}