use crate::clifford::Metric;
use crate::forms::{is_square_finite, FiniteOddField};
use crate::scalar::ResidueField;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LocalResidueForm {
pub valuation: i128,
pub dim: usize,
pub disc_is_square: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LocalSpringerDecomp {
pub graded: Vec<LocalResidueForm>,
pub radical_dim: usize,
}
impl LocalSpringerDecomp {
pub fn parity_layer(&self, parity: u128) -> Vec<&LocalResidueForm> {
self.graded
.iter()
.filter(|g| (g.valuation.rem_euclid(2) as u128) == parity)
.collect()
}
pub fn display(&self) -> String {
self.to_string()
}
}
impl std::fmt::Display for LocalSpringerDecomp {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let layers: Vec<(i128, usize, bool)> = self
.graded
.iter()
.map(|g| (g.valuation, g.dim, g.disc_is_square))
.collect();
write!(
f,
"LocalSpringerDecomp(graded={layers:?}, radical_dim={})",
self.radical_dim
)
}
}
pub fn springer_decompose_local<K>(metric: &Metric<K>) -> Option<LocalSpringerDecomp>
where
K: ResidueField,
K::Residue: FiniteOddField,
{
if !K::Residue::is_supported_odd_field() {
return None; }
if !metric.b.is_empty() || metric.has_upper() {
return None; }
let mut buckets: Vec<(i128, usize, bool)> = Vec::new(); let mut radical_dim = 0usize;
for x in &metric.q {
match x.valuation() {
None => radical_dim += 1, Some(v) => {
let unit = x
.residue_unit()
.expect("a nonzero element has an angular component");
let sq = is_square_finite::<K::Residue>(unit);
match buckets.iter_mut().find(|(bv, _, _)| *bv == v) {
Some((_, dim, disc)) => {
*dim += 1;
*disc = *disc == sq; }
None => buckets.push((v, 1, sq)),
}
}
}
}
buckets.sort_by_key(|x| std::cmp::Reverse(x.0)); let graded = buckets
.into_iter()
.map(|(valuation, dim, disc_is_square)| LocalResidueForm {
valuation,
dim,
disc_is_square,
})
.collect();
Some(LocalSpringerDecomp {
graded,
radical_dim,
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::scalar::{Fp, Laurent, NewtonPolygon, Poly, Qp, Qq, Scalar};
#[test]
fn display_render_pin() {
let qp = Metric::diagonal(vec![Qp::<5, 4>::from_int(1), Qp::<5, 4>::from_int(5)]);
let d = springer_decompose_local(&qp).unwrap();
assert_eq!(
d.to_string(),
"LocalSpringerDecomp(graded=[(1, 1, true), (0, 1, true)], radical_dim=0)"
);
assert_eq!(d.display(), d.to_string());
}
#[test]
fn one_engine_decomposes_every_discrete_leg() {
let qp = Metric::diagonal(vec![Qp::<5, 4>::from_int(1), Qp::<5, 4>::from_int(5)]);
let dp = springer_decompose_local(&qp).unwrap();
assert_eq!(dp.graded.len(), 2);
assert_eq!(dp.parity_layer(0).len(), 1);
assert_eq!(dp.parity_layer(1).len(), 1);
let lt = Metric::diagonal(vec![
Laurent::<Fp<5>, 4>::from_coeffs(vec![Fp::<5>::from_int(1)], 0),
Laurent::<Fp<5>, 4>::from_coeffs(vec![Fp::<5>::from_int(1)], 1),
]);
let dl = springer_decompose_local(<).unwrap();
assert_eq!(dl.graded.len(), 2);
assert_eq!(dp.graded, dl.graded);
}
#[test]
fn residue_char_two_is_rejected_uniformly() {
assert!(springer_decompose_local(&Metric::diagonal(vec![Qp::<2, 4>::one()])).is_none());
assert!(springer_decompose_local(&Metric::diagonal(vec![Laurent::<
crate::scalar::Fpn<2, 3>,
4,
>::one()]))
.is_none());
}
#[test]
fn unramified_qq_reads_extension_residue() {
use crate::scalar::{Fpn, WittVec};
type Q9 = Qq<3, 3, 2>;
let ns = (0..9u128)
.map(|c| Fpn::<3, 2>::from_coeffs(&[c % 3, c / 3]))
.find(|x| !x.is_zero() && !x.is_square())
.expect("F_9 has nonsquares");
let m = Metric::diagonal(vec![
Q9::from_witt(WittVec::<3, 3, 2>(ns.into_coeffs())).mul(&Q9::from_int(3)),
Q9::from_witt(WittVec::<3, 3, 2>(ns.mul(&ns).into_coeffs())),
]);
let d = springer_decompose_local(&m).unwrap();
assert_eq!(d.graded.len(), 2);
assert_eq!(d.graded[0].valuation, 1);
assert!(!d.graded[0].disc_is_square, "ns is a nonsquare in F_9");
assert!(d.graded[1].disc_is_square, "ns² is a square in F_9");
}
fn prod_x_minus<K: Scalar>(roots: &[K]) -> Poly<K> {
roots.iter().fold(Poly::one(), |f, a| {
f.mul(&Poly::new(vec![a.neg(), K::one()]))
})
}
fn assert_polygon_is_springer_shadow<K>(roots: Vec<K>)
where
K: ResidueField,
K::Residue: FiniteOddField,
{
let sp = springer_decompose_local(&Metric::diagonal(roots.clone())).unwrap();
let f = prod_x_minus(&roots);
let np = NewtonPolygon::from_coeffs(f.coeffs()).unwrap();
assert_eq!(np.zero_root_multiplicity(), 0);
let mut poly_side: Vec<(i128, usize)> = np
.root_valuations()
.into_iter()
.map(|(lam, mult)| {
assert!(lam.is_integer(), "entry valuations are integers");
(lam.numer(), mult as usize)
})
.collect();
let mut spr_side: Vec<(i128, usize)> =
sp.graded.iter().map(|g| (g.valuation, g.dim)).collect();
poly_side.sort();
spr_side.sort();
assert_eq!(poly_side, spr_side, "Newton shadow ≠ Springer buckets");
for parity in [0u128, 1] {
let spr: usize = sp.parity_layer(parity).iter().map(|g| g.dim).sum();
let poly: usize = np
.root_valuations()
.into_iter()
.filter(|(lam, _)| (lam.numer().rem_euclid(2) as u128) == parity)
.map(|(_, m)| m as usize)
.sum();
assert_eq!(spr, poly, "parity-{parity} layer cardinality");
}
}
#[test]
fn polygon_is_the_springer_shadow() {
assert_polygon_is_springer_shadow(vec![
Qp::<5, 8>::from_int(1),
Qp::<5, 8>::from_int(5),
Qp::<5, 8>::from_int(7),
Qp::<5, 8>::from_int(25),
Qp::<5, 8>::from_int(10),
]);
let l = |c: i128, val: usize| {
Laurent::<Fp<7>, 8>::from_coeffs(vec![Fp::<7>::from_int(c)], val as i128)
};
assert_polygon_is_springer_shadow(vec![l(1, 0), l(3, 1), l(2, 0), l(5, 2)]);
type Q9 = Qq<3, 3, 2>;
assert_polygon_is_springer_shadow(vec![
Q9::from_int(1),
Q9::from_int(1).mul(&Q9::from_int(3)), Q9::from_int(2),
]);
}
#[test]
fn polygon_outlives_springer() {
let coeffs = vec![
Qp::<2, 8>::from_int(-2),
Qp::<2, 8>::zero(),
Qp::<2, 8>::one(),
];
assert!(NewtonPolygon::from_coeffs(&coeffs).is_some());
assert!(springer_decompose_local(&Metric::diagonal(vec![
Qp::<2, 8>::from_int(2),
Qp::<2, 8>::one()
]))
.is_none());
}
}