use crate::clifford::{Metric, MAX_BASIS_DIM};
use crate::forms::ArfInvariants;
use crate::scalar::{
nim_frobenius_iter, nim_mul, nim_relative_trace, nim_solve_artin_schreier, nim_trace,
CyclicGaloisExtension, FieldExtension, Fp, Nimber, Scalar,
};
use std::collections::BTreeMap;
fn assemble_twisted_form<E: Scalar, T: Scalar>(
basis: &[E],
twist: impl Fn(&E) -> E,
trace: impl Fn(&E) -> T,
) -> Metric<T> {
let n = basis.len();
let tw: Vec<E> = basis.iter().map(&twist).collect();
let q: Vec<T> = basis
.iter()
.zip(&tw)
.map(|(e, te)| trace(&e.mul(te)))
.collect();
let mut b = BTreeMap::new();
for i in 0..n {
for j in (i + 1)..n {
let t = trace(&basis[i].mul(&tw[j]).add(&basis[j].mul(&tw[i])));
if !t.is_zero() {
b.insert((i, j), t);
}
}
}
Metric::general(q, b, BTreeMap::new())
}
fn insert_metric_block<S: Scalar>(
q: &mut [S],
b: &mut BTreeMap<(usize, usize), S>,
offset: usize,
block: Metric<S>,
) {
let (bq, bb, ba) = block.into_parts();
debug_assert!(ba.is_empty());
for (i, qi) in bq.into_iter().enumerate() {
q[offset + i] = qi;
}
for ((i, j), v) in bb {
b.insert((offset + i, offset + j), v);
}
}
pub fn trace_twisted_form<E>(k: usize) -> Metric<E::Base>
where
E: CyclicGaloisExtension,
{
assemble_twisted_form(&E::basis(), |e| e.sigma_power(k), |z| z.trace())
}
pub fn cyclic_algebra_trace_form<E>(a: &E::Base) -> Metric<E::Base>
where
E: CyclicGaloisExtension,
{
let basis = E::basis();
let n = basis.len();
let dim = n
.checked_mul(n)
.expect("cyclic algebra trace-form dimension overflowed");
assert!(
dim <= MAX_BASIS_DIM,
"cyclic_algebra_trace_form has dimension [E:F]^2={dim}, exceeding {MAX_BASIS_DIM}"
);
let mut q = vec![E::Base::zero(); dim];
let mut b = BTreeMap::new();
let line0 = assemble_twisted_form(&basis, |x| x.clone(), |z| z.trace());
insert_metric_block(&mut q, &mut b, 0, line0);
if n % 2 == 0 {
let mid = n / 2;
let middle = assemble_twisted_form(&basis, |x| x.sigma_power(mid), |z| a.mul(&z.trace()));
insert_metric_block(&mut q, &mut b, mid * n, middle);
}
for i in 1..n {
let j = n - i;
if i >= j {
continue;
}
for r in 0..n {
for s in 0..n {
let term = basis[r]
.mul(&basis[s].sigma_power(i))
.add(&basis[s].mul(&basis[r].sigma_power(j)));
let value = a.mul(&term.trace());
if !value.is_zero() {
b.insert((i * n + r, j * n + s), value);
}
}
}
}
Metric::general(q, b, BTreeMap::new())
}
pub fn restrict_diagonal<E>(entries: &[E::Base]) -> Metric<E>
where
E: CyclicGaloisExtension,
{
Metric::diagonal(entries.iter().map(E::embed).collect())
}
pub fn transfer_diagonal<E>(entries: &[E]) -> Metric<E::Base>
where
E: CyclicGaloisExtension,
{
let basis = E::basis();
let mut result = Metric::diagonal(Vec::new());
for lambda in entries {
let block = assemble_twisted_form(&basis, |x| lambda.mul(x), |z| z.trace());
result = result.direct_sum(&block);
}
result
}
pub fn trace_form_arf<E>(k: usize) -> Option<ArfInvariants>
where
E: CyclicGaloisExtension + FieldExtension<Base = Fp<2>>,
{
trace_twisted_form::<E>(k)
.map(|x| Nimber(x.value()))
.classify()
.ok()
}
fn assert_nim_subfield_degree(m: usize) {
assert!(
m.is_power_of_two() && m <= 128,
"the nimbers < 2^m form a subfield only for m a power of two <= 128"
);
}
fn assert_in_nim_subfield(x: u128, m: usize) {
assert!(
m == 128 || x < (1u128 << m),
"the scale must lie in the selected nim subfield"
);
}
pub fn gold_component_diagonal_dual(m: usize, a: usize, c: u128) -> u128 {
assert_nim_subfield_degree(m);
assert_in_nim_subfield(c, m);
fn recurse(m: usize, a: usize, c: u128) -> u128 {
if m == 1 {
return c & 1;
}
let half = m / 2;
let u = 1u128 << half;
let low_scale = nim_relative_trace(c, m as u128, half as u128);
let u_twisted = nim_frobenius_iter(u, a % m);
let top_scale =
nim_relative_trace(nim_mul(c, nim_mul(u, u_twisted)), m as u128, half as u128);
let low_dual = recurse(half, a, low_scale);
let top_dual = recurse(half, a, top_scale);
(low_dual ^ top_dual) ^ (low_dual << half)
}
recurse(m, a, c)
}
pub fn gold_diagonal_dual(m: usize, a: usize) -> u128 {
gold_component_diagonal_dual(m, a, 1)
}
pub fn gold_diagonal_artin_schreier_source(m: usize, a: usize) -> u128 {
assert!(
m >= 2,
"the Artin--Schreier source needs an even extension degree"
);
let lambda = gold_diagonal_dual(m, a);
debug_assert!(lambda < (1u128 << (m / 2)));
nim_solve_artin_schreier(lambda, m as u128)
.expect("the Gold diagonal dual lies in the Artin--Schreier image")
}
pub fn gold_form(m: usize, a: usize) -> Metric<Nimber> {
assert_nim_subfield_degree(m);
let basis: Vec<Nimber> = (0..m).map(|i| Nimber(1u128 << i)).collect();
assemble_twisted_form(
&basis,
|x| {
Nimber(nim_frobenius_iter(x.0, a % m))
},
|x| Nimber(nim_trace(x.0, m as u128)),
)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::scalar::{Fp, Fpn, Qq, Rational, Surcomplex};
fn r(n: i128) -> Rational {
Rational::from_int(n)
}
fn gcd(a: usize, b: usize) -> usize {
if b == 0 {
a
} else {
gcd(b, a % b)
}
}
fn eval_rational_metric(m: &Metric<Rational>, coords: &[i128]) -> Rational {
assert_eq!(m.dim(), coords.len());
let mut total = Rational::zero();
for (i, &ci) in coords.iter().enumerate() {
let x = r(ci);
total = total.add(&m.q[i].mul(&x).mul(&x));
}
for (&(i, j), bij) in &m.b {
total = total.add(&bij.mul(&r(coords[i])).mul(&r(coords[j])));
}
total
}
#[test]
fn surcomplex_twist_is_the_norm_form() {
let m = trace_twisted_form::<Surcomplex<Rational>>(1);
assert_eq!(m.q, vec![Rational::from_int(2), Rational::from_int(2)]);
assert!(m.b.is_empty());
}
#[test]
fn cyclic_trace_form_degree_two_is_literal_trd_square() {
for a in [-3i128, -1, 2, 5] {
let m = cyclic_algebra_trace_form::<Surcomplex<Rational>>(&r(a));
assert_eq!(m.q, vec![r(2), r(-2), r(2 * a), r(2 * a)]);
assert!(m.b.is_empty());
}
}
#[test]
fn cyclic_trace_form_degree_two_satisfies_cayley_hamilton_relation() {
for a in [-3i128, 2, 5] {
let m = cyclic_algebra_trace_form::<Surcomplex<Rational>>(&r(a));
for p in -1..=1 {
for q in -1..=1 {
for u in -1..=1 {
for v in -1..=1 {
let lhs = eval_rational_metric(&m, &[p, q, u, v]);
let trd = 2 * p;
let nrd = p * p + q * q - a * u * u - a * v * v;
let rhs = r(trd * trd - 2 * nrd);
assert_eq!(lhs, rhs, "a={a}, coords={:?}", [p, q, u, v]);
}
}
}
}
}
}
#[test]
fn qq_twist_uses_the_unramified_galois_basis() {
type Q9 = Qq<3, 3, 2>;
let m = trace_twisted_form::<Q9>(1);
assert_eq!(m.q.len(), 2);
assert!(m.q.iter().all(|x| !x.is_zero()));
assert!(m.q.iter().all(|x| x.valuation().is_some()));
}
#[test]
fn cyclic_trace_form_degree_three_has_hyperbolic_cross_pair() {
let t = cyclic_algebra_trace_form::<Fpn<3, 3>>(&Fp::<3>::one());
let line0 = trace_twisted_form::<Fpn<3, 3>>(0);
assert_eq!(t.dim(), 9);
assert_eq!(&t.q[..3], line0.q());
assert!(t.q[3..].iter().all(|x| x.is_zero()));
for (&(i, j), v) in line0.b() {
assert_eq!(t.b.get(&(i, j)), Some(v));
}
let t_dec = t.witt_decompose().expect("F_3 trace form decomposition");
let line_dec = line0
.witt_decompose()
.expect("F_3 line trace form decomposition");
assert_eq!(t_dec.anisotropic_dim, line_dec.anisotropic_dim);
}
#[test]
fn gold_form_over_small_fpn_matches_rank_formula() {
let f4 = trace_form_arf::<Fpn<2, 2>>(1).unwrap();
assert_eq!((f4.rank, f4.radical_dim), (0, 2));
let f8 = trace_form_arf::<Fpn<2, 3>>(1).unwrap();
assert_eq!((f8.rank, f8.radical_dim), (2, 1));
}
#[test]
fn gold_form_over_nim_subfields_matches_rank_formula() {
for m in [2usize, 4, 8] {
let a = 1usize;
let arf = gold_form(m, a).classify().unwrap();
let g = gcd(2 * a, m);
assert_eq!(
(arf.rank, arf.radical_dim),
(m - g, g),
"Gold form over F_2^{m} (a={a})"
);
}
let arf = gold_form(8, 3).classify().unwrap();
assert_eq!((arf.rank, arf.radical_dim), (6, 2));
assert_eq!(gold_form(8, usize::MAX), gold_form(8, usize::MAX % 8));
}
#[test]
fn scaled_gold_diagonal_dual_matches_the_trace_pairing() {
for m in [1usize, 2, 4, 8] {
let scales = if m == 1 { 2 } else { 1u128 << m };
for a in 0..m {
for c in 0..scales {
let lambda = gold_component_diagonal_dual(m, a, c);
for i in 0..m {
let e = 1u128 << i;
let twisted = nim_frobenius_iter(e, a);
let direct = nim_trace(nim_mul(c, nim_mul(e, twisted)), m as u128);
let paired = nim_trace(nim_mul(lambda, e), m as u128);
assert_eq!(paired, direct, "m={m}, a={a}, c={c}, i={i}");
}
}
}
}
}
#[test]
fn unscaled_gold_dual_descends_and_has_artin_schreier_source() {
for m in [2usize, 4, 8, 16, 32, 64, 128] {
for a in [0usize, 1, 2, 3, 4, 7, m - 1, m] {
let lambda = gold_diagonal_dual(m, a);
assert!(
lambda < (1u128 << (m / 2)),
"unscaled dual did not descend: m={m}, a={a}, lambda={lambda}"
);
let w = gold_diagonal_artin_schreier_source(m, a);
assert_eq!(nim_mul(w, w) ^ w, lambda, "m={m}, a={a}");
for i in 0..m {
let e = 1u128 << i;
let sourced = nim_trace(nim_mul(nim_mul(w, w) ^ w, e), m as u128);
let gold = nim_trace(nim_mul(e, nim_frobenius_iter(e, a % m)), m as u128);
assert_eq!(sourced, gold, "m={m}, a={a}, i={i}");
}
}
}
assert_eq!(gold_diagonal_dual(4, 2), 0);
assert_eq!(gold_diagonal_dual(8, 2), 6);
assert_eq!(gold_diagonal_dual(16, 2), 102);
assert_eq!(gold_diagonal_dual(32, 2), 24_582);
assert_eq!(gold_diagonal_dual(16, 4), 31);
assert_eq!(gold_diagonal_dual(32, 4), 8_030);
}
#[test]
fn transfer_of_unit_form_is_the_k0_twisted_form() {
let s = transfer_diagonal::<Fpn<3, 2>>(&[Fpn::<3, 2>::one()]);
let t0 = trace_twisted_form::<Fpn<3, 2>>(0);
assert_eq!(s.q, t0.q);
assert_eq!(s.b, t0.b);
}
#[test]
fn transfer_of_a_hyperbolic_form_is_split() {
let one = Fpn::<3, 2>::one();
let hyp = transfer_diagonal::<Fpn<3, 2>>(&[one, one.neg()]);
let dec = hyp.witt_decompose().expect("Fp<3> Witt decomposition");
assert_eq!(
dec.anisotropic_dim, 0,
"transfer of a hyperbolic form splits"
);
}
#[test]
fn restriction_preserves_orthogonal_sums_and_exposes_even_degree_kernel() {
let left = [Fp::<3>::one(), Fp::<3>::from_int(2)];
let right = [Fp::<3>::one()];
let together = restrict_diagonal::<Fpn<3, 2>>(&[left[0], left[1], right[0]]);
let separately = restrict_diagonal::<Fpn<3, 2>>(&left)
.direct_sum(&restrict_diagonal::<Fpn<3, 2>>(&right));
assert_eq!(
together, separately,
"restriction is additive on orthogonal sums"
);
let killed = restrict_diagonal::<Fpn<3, 2>>(&[Fp::<3>::one(), Fp::<3>::one()]);
match killed.witt_decompose().expect("F_9 Witt decomposition") {
crate::forms::FiniteFieldWittDecomp::Odd(d) => assert_eq!(d.anisotropic_dim, 0),
other => panic!("expected odd-characteristic decomposition, got {other:?}"),
}
}
#[test]
fn frobenius_reciprocity_projection_formula() {
let x = [Fp::<3>::one(), Fp::<3>::from_int(2)];
let y = [
Fpn::<3, 2>::from_coeffs(&[1, 1]),
Fpn::<3, 2>::from_coeffs(&[2, 1]),
];
let restricted = restrict_diagonal::<Fpn<3, 2>>(&x);
let product = crate::forms::tensor_form(&restricted, &Metric::diagonal(y.to_vec()))
.expect("both representatives are diagonal");
let lhs = transfer_diagonal::<Fpn<3, 2>>(product.q());
let transferred = transfer_diagonal::<Fpn<3, 2>>(&y);
let scale = |c: Fp<3>| {
Metric::general(
transferred.q.iter().map(|v| c.mul(v)).collect(),
transferred.b.iter().map(|(k, v)| (*k, c.mul(v))),
Vec::new(),
)
};
let rhs = scale(x[0]).direct_sum(&scale(x[1]));
assert_eq!(lhs, rhs);
}
#[test]
fn springer_odd_degree_restriction_is_injective() {
let aniso = Metric::<Fp<3>>::diagonal(vec![Fp::<3>::one(), Fp::<3>::one()]);
let base_dec = aniso.witt_decompose().expect("Fp<3> Witt decomposition");
assert_eq!(base_dec.anisotropic_dim, 2, "⟨1,1⟩ anisotropic over F_3");
let restricted = restrict_diagonal::<Fpn<3, 3>>(&[Fp::<3>::one(), Fp::<3>::one()]);
match restricted
.witt_decompose()
.expect("F_27 Witt decomposition")
{
crate::forms::FiniteFieldWittDecomp::Odd(d) => {
assert_eq!(
d.anisotropic_dim, 2,
"still anisotropic over F_27 ⇒ injective"
);
}
other => panic!("expected odd-characteristic decomposition, got {other:?}"),
}
}
#[test]
fn metric_map_lifts_fp2_to_nimber() {
let over_f2 = trace_twisted_form::<Fpn<2, 3>>(1);
let lifted = over_f2.map(|x| Nimber(x.value()));
assert_eq!(lifted.q.len(), over_f2.q.len());
for (i, qi) in over_f2.q.iter().enumerate() {
assert_eq!(lifted.q[i].0, qi.value());
}
assert_eq!(lifted.b.len(), over_f2.b.len());
}
}