use std::collections::{BTreeMap, BTreeSet};
use crate::forms::{Brauer2Class, Place};
use crate::scalar::{
CyclicGaloisExtension, ExactFieldScalar, Fp, Fpn, Rational, ResidueField, Scalar, Valued,
};
fn frac_mod_one(r: &Rational) -> Rational {
Rational::try_new(r.numer().rem_euclid(r.denom()), r.denom())
.expect("a positive denominator stays valid under rem_euclid")
}
pub trait TameSymbolResidueField: ExactFieldScalar + Copy {
fn tame_field_order() -> Option<u128>;
fn tame_from_index(i: u128) -> Self;
}
impl<const P: u128> TameSymbolResidueField for Fp<P> {
fn tame_field_order() -> Option<u128> {
Fp::<P>::modulus_is_prime().then_some(P)
}
fn tame_from_index(i: u128) -> Self {
Fp::<P>::from_u128(i)
}
}
impl<const P: u128, const N: usize> TameSymbolResidueField for Fpn<P, N> {
fn tame_field_order() -> Option<u128> {
Fpn::<P, N>::field_order_checked()
}
fn tame_from_index(mut i: u128) -> Self {
let mut coeffs = [0u128; N];
for c in &mut coeffs {
*c = i % P;
i /= P;
}
Fpn::<P, N>::from_coeffs(&coeffs)
}
}
fn residue_pow_signed<F: TameSymbolResidueField>(base: F, e: i128) -> Option<F> {
if e >= 0 {
Some(base.pow(e as u128))
} else {
Some(base.inv()?.pow(e.unsigned_abs()))
}
}
fn residue_order<F: TameSymbolResidueField>(x: F) -> Option<u128> {
if x.is_zero() {
return None;
}
let group = F::tame_field_order()?.checked_sub(1)?;
let mut cur = F::one();
for k in 1..=group {
cur = cur.mul(&x);
if cur == F::one() {
return Some(k);
}
}
None
}
fn residue_primitive<F: TameSymbolResidueField>() -> Option<F> {
let group = F::tame_field_order()?.checked_sub(1)?;
for i in 1..F::tame_field_order()? {
let g = F::tame_from_index(i);
if residue_order(g) == Some(group) {
return Some(g);
}
}
None
}
fn residue_discrete_log<F: TameSymbolResidueField>(base: F, x: F) -> Option<u128> {
if base.is_zero() || x.is_zero() {
return None;
}
let order = residue_order(base)?;
let mut cur = F::one();
for e in 0..order {
if cur == x {
return Some(e);
}
cur = cur.mul(&base);
}
None
}
fn residue_tame_raw<F: TameSymbolResidueField>(
alpha: i128,
beta: i128,
a_unit: F,
b_unit: F,
) -> Option<F> {
let mut raw = F::one();
if alpha.rem_euclid(2) == 1 && beta.rem_euclid(2) == 1 {
raw = raw.neg();
}
raw = raw.mul(&residue_pow_signed(a_unit, beta)?);
raw = raw.mul(&residue_pow_signed(b_unit, -alpha)?);
Some(raw)
}
fn residue_tame_symbol_exponent<F: TameSymbolResidueField>(
n: u128,
alpha: i128,
beta: i128,
a_unit: F,
b_unit: F,
) -> Option<u128> {
if n == 0 {
return None;
}
let group = F::tame_field_order()?.checked_sub(1)?;
if group % n != 0 {
return None;
}
if n == 1 {
return Some(0);
}
let raw = residue_tame_raw(alpha, beta, a_unit, b_unit)?;
let primitive = residue_primitive::<F>()?;
Some(residue_discrete_log(primitive, raw)? % n)
}
#[derive(Debug, Clone, PartialEq)]
pub struct BrauerClass {
local: BTreeMap<Place, Rational>,
}
impl BrauerClass {
pub fn split() -> Self {
BrauerClass {
local: BTreeMap::new(),
}
}
pub fn is_split(&self) -> bool {
self.local.is_empty()
}
pub fn local(&self) -> &BTreeMap<Place, Rational> {
&self.local
}
pub fn local_invariant(&self, place: Place) -> Rational {
self.local
.get(&place)
.cloned()
.unwrap_or_else(Rational::zero)
}
pub fn from_local(entries: impl IntoIterator<Item = (Place, Rational)>) -> Self {
let mut local = BTreeMap::new();
for (place, inv) in entries {
let r = frac_mod_one(&inv);
if !r.is_zero() {
local.insert(place, r);
}
}
BrauerClass { local }
}
pub fn add(&self, other: &Self) -> Self {
let mut local = self.local.clone();
for (place, inv) in &other.local {
let sum = frac_mod_one(
&local
.get(place)
.cloned()
.unwrap_or_else(Rational::zero)
.add(inv),
);
if sum.is_zero() {
local.remove(place);
} else {
local.insert(*place, sum);
}
}
BrauerClass { local }
}
pub fn invariant_sum(&self) -> Rational {
frac_mod_one(
&self
.local
.values()
.fold(Rational::zero(), |acc, inv| acc.add(inv)),
)
}
pub fn from_two_torsion(class: &Brauer2Class) -> Self {
let half = Rational::try_new(1, 2).expect("1/2 is a valid rational");
BrauerClass {
local: class
.ramified_places()
.iter()
.map(|&place| (place, half.clone()))
.collect(),
}
}
pub fn two_torsion(&self) -> Option<BTreeSet<Place>> {
let half = Rational::try_new(1, 2).expect("1/2 is a valid rational");
let mut set = BTreeSet::new();
for (place, inv) in &self.local {
if *inv != half {
return None;
}
set.insert(*place);
}
Some(set)
}
pub fn display(&self) -> String {
self.to_string()
}
}
impl std::fmt::Display for BrauerClass {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let local: Vec<(String, Rational)> = self
.local
.iter()
.map(|(&place, inv)| (place.to_string(), inv.clone()))
.collect();
write!(f, "BrauerClass(local={local:?})")
}
}
pub fn cyclic_algebra_invariant<E>(a: &E::Base) -> Option<Rational>
where
E: CyclicGaloisExtension,
E::Base: Valued,
{
let n = i128::try_from(E::extension_degree()).ok()?;
let v = a.valuation()?;
Some(frac_mod_one(&Rational::try_new(v, n)?))
}
pub fn tame_symbol_exponent<K>(n: u128, a: &K, b: &K) -> Option<u128>
where
K: ResidueField,
K::Residue: TameSymbolResidueField,
{
let alpha = a.valuation()?;
let beta = b.valuation()?;
residue_tame_symbol_exponent(n, alpha, beta, a.residue_unit()?, b.residue_unit()?)
}
pub fn tame_symbol_invariant<K>(n: u128, a: &K, b: &K) -> Option<Rational>
where
K: ResidueField,
K::Residue: TameSymbolResidueField,
{
let e = i128::try_from(tame_symbol_exponent(n, a, b)?).ok()?;
let ni = i128::try_from(n).ok()?;
Some(frac_mod_one(&Rational::try_new(e, ni)?))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::forms::{brauer_local_invariants, try_hilbert_symbol_qp, try_is_isotropic_at_p};
use crate::scalar::{FieldExtension, Fpn, Qq, Rational, Surcomplex, WittVec};
fn half() -> Rational {
Rational::try_new(1, 2).unwrap()
}
fn third() -> Rational {
Rational::try_new(1, 3).unwrap()
}
fn two_thirds() -> Rational {
Rational::try_new(2, 3).unwrap()
}
fn q(n: i128, d: i128) -> Rational {
Rational::try_new(n, d).unwrap()
}
#[test]
fn display_render_pin() {
assert_eq!(BrauerClass::split().to_string(), "BrauerClass(local=[])");
let c = BrauerClass::from_local([(Place::Prime(7), third()), (Place::Real, half())]);
assert_eq!(
c.to_string(),
"BrauerClass(local=[(\"R\", 1/2), (\"Q_7\", 1/3)])"
);
assert_eq!(c.display(), c.to_string());
}
#[test]
fn add_is_modular_and_drops_cancellations() {
let a = BrauerClass::from_local([(Place::Prime(7), third())]);
let b = BrauerClass::from_local([(Place::Prime(7), two_thirds())]);
assert!(a.add(&b).is_split(), "1/3 + 2/3 ≡ 0 at the place");
assert_eq!(a.add(&BrauerClass::split()), a);
let c = BrauerClass::from_local([(Place::Prime(5), half())]);
assert_eq!(a.add(&c), c.add(&a));
assert_eq!(a.add(&a).local_invariant(Place::Prime(7)), two_thirds());
}
#[test]
fn from_local_reduces_mod_z_and_drops_zeros() {
let c = BrauerClass::from_local([
(Place::Prime(2), q(7, 3)),
(Place::Prime(3), q(2, 2)),
(Place::Real, q(-1, 3)),
]);
assert_eq!(c.local_invariant(Place::Prime(2)), third());
assert!(
c.local().get(&Place::Prime(3)).is_none(),
"integer ⇒ dropped"
);
assert_eq!(c.local_invariant(Place::Real), two_thirds());
assert_eq!(c.local_invariant(Place::Prime(11)), Rational::zero());
}
#[test]
fn invariant_sum_reduces_mod_z() {
let c = BrauerClass::from_local([(Place::Real, half()), (Place::Prime(2), half())]);
assert_eq!(c.invariant_sum(), Rational::zero());
let d = BrauerClass::from_local([
(Place::Prime(2), third()),
(Place::Prime(3), third()),
(Place::Prime(5), third()),
]);
assert_eq!(d.invariant_sum(), Rational::zero());
assert_eq!(
BrauerClass::from_local([(Place::Prime(7), third())]).invariant_sum(),
third()
);
}
#[test]
fn two_torsion_round_trips_with_bridge_f() {
let f = Brauer2Class::quaternion(-1, -1).unwrap();
let k = BrauerClass::from_two_torsion(&f);
assert_eq!(k.local_invariant(Place::Real), half());
assert_eq!(k.local_invariant(Place::Prime(2)), half());
assert_eq!(k.two_torsion().as_ref(), Some(f.ramified_places()));
}
#[test]
fn non_two_torsion_class_has_no_ramification_set() {
let c = BrauerClass::from_local([(Place::Prime(7), third())]);
assert_eq!(c.two_torsion(), None);
}
#[test]
fn reciprocity_reread_through_brauer_class() {
for (a, b) in [(-1i128, -1i128), (-1, 7), (2, 3), (-3, 5), (6, -7)] {
let f = Brauer2Class::quaternion(a, b).unwrap();
assert_eq!(
BrauerClass::from_two_torsion(&f).invariant_sum(),
Rational::zero(),
"reciprocity for ({a},{b})"
);
}
}
#[test]
fn from_two_torsion_is_additive() {
let x = Brauer2Class::quaternion(-1, -1).unwrap();
let y = Brauer2Class::quaternion(2, 5).unwrap();
let lhs = BrauerClass::from_two_torsion(&x.add(&y));
let rhs = BrauerClass::from_two_torsion(&x).add(&BrauerClass::from_two_torsion(&y));
assert_eq!(lhs, rhs);
}
type Qp = Qq<5, 4, 1>;
#[test]
fn degree_two_splitting_law() {
let cases = [(1i128, 0i128), (5, 1), (25, 2), (125, 3)];
for (a, v) in cases {
let elt = Qp::from_int(a);
assert_eq!(elt.valuation(), Some(v), "v_5({a}) = {v}");
let inv = cyclic_algebra_invariant::<Qq<5, 4, 2>>(&elt).unwrap();
let expected = if v % 2 == 0 { Rational::zero() } else { half() };
assert_eq!(inv, expected, "inv of v={v}");
}
assert_eq!(
cyclic_algebra_invariant::<Qq<5, 4, 2>>(&Qp::from_int(0)),
None
);
}
#[test]
fn degree_two_compat_with_shipped_quaternion_invariant() {
let d = 2i128; for (a, v) in [(1i128, 0i128), (5, 1), (25, 2), (125, 3)] {
let k = cyclic_algebra_invariant::<Qq<5, 4, 2>>(&Qp::from_int(a)).unwrap();
let invs =
brauer_local_invariants(&Rational::from_int(d), &Rational::from_int(a)).unwrap();
let f = invs
.iter()
.find(|(pl, _)| *pl == Place::Prime(5))
.map(|(_, r)| r.clone())
.unwrap_or_else(Rational::zero);
assert_eq!(k, f, "K vs F at Prime(5) for v_5(a)={v}");
assert_eq!(k, if v % 2 == 0 { Rational::zero() } else { half() });
}
}
#[test]
fn degree_three_image_additivity_and_convention() {
let p = Qp::from_int(5); let p2 = Qp::from_int(25); let p3 = Qp::from_int(125); let i1 = cyclic_algebra_invariant::<Qq<5, 4, 3>>(&p).unwrap();
let i2 = cyclic_algebra_invariant::<Qq<5, 4, 3>>(&p2).unwrap();
let i3 = cyclic_algebra_invariant::<Qq<5, 4, 3>>(&p3).unwrap();
assert_eq!(i1, third());
assert_eq!(i2, two_thirds(), "convention pin: inv(a²)=2/3, not 1/3");
assert_eq!(i3, Rational::zero(), "n ∣ v ⇒ splits");
let aa = p.mul(&p); assert_eq!(
cyclic_algebra_invariant::<Qq<5, 4, 3>>(&aa).unwrap(),
frac_mod_one(&i1.add(&i1))
);
assert_eq!(frac_mod_one(&i1.add(&i1).add(&i1)), Rational::zero());
}
#[test]
fn norm_classes_split() {
type Q9 = Qq<3, 3, 2>;
let g = WittVec::<3, 3, 2>([1, 1]);
let x = Q9::from_witt(g);
let nm = FieldExtension::norm(&x); assert_eq!(
cyclic_algebra_invariant::<Q9>(&nm),
Some(Rational::zero()),
"norm class splits"
);
let px = Q9::from_int(3).mul(&x);
let npx = FieldExtension::norm(&px);
assert_eq!(cyclic_algebra_invariant::<Q9>(&npx), Some(Rational::zero()));
}
#[test]
fn tame_quadratic_symbol_matches_hilbert_symbol_qp() {
type Q5 = crate::scalar::Qp<5, 4>;
for (a, b) in [(2i128, 5i128), (5, 2), (10, 25), (3, 50), (-5, 2)] {
let exp = tame_symbol_exponent(2, &Q5::from_int(a), &Q5::from_int(b)).unwrap();
let hilb = try_hilbert_symbol_qp(a, b, 5).unwrap();
assert_eq!(exp, if hilb == 1 { 0 } else { 1 }, "a={a}, b={b}");
assert_eq!(
tame_symbol_invariant(2, &Q5::from_int(a), &Q5::from_int(b)).unwrap(),
if hilb == 1 { Rational::zero() } else { half() },
"invariant a={a}, b={b}"
);
}
}
#[test]
fn tame_symbol_pins_kummer_sign_convention() {
type Q5 = crate::scalar::Qp<5, 4>;
let pi = Q5::from_p_power(1);
let two = Q5::from_int(2); assert_eq!(tame_symbol_exponent(4, &two, &pi), Some(1));
assert_eq!(tame_symbol_invariant(4, &two, &pi), Some(q(1, 4)));
assert_eq!(
tame_symbol_exponent(4, &pi, &two),
Some(3),
"the requested a^v(b)/b^v(a) convention gives the inverse"
);
assert_eq!(tame_symbol_invariant(4, &pi, &two), Some(q(3, 4)));
assert_eq!(tame_symbol_exponent(3, &two, &pi), None, "3 ∤ |F_5*|");
assert_eq!(
tame_symbol_exponent(4, &Q5::zero(), &two),
None,
"0 is outside K*"
);
}
#[test]
fn tame_symbol_reads_extension_residue_field() {
type Q9 = Qq<3, 3, 2>;
let pi = Q9::from_p_power(1);
let g = Q9::teichmuller(Fpn::<3, 2>::primitive_element());
assert_eq!(
tame_symbol_exponent(8, &g, &pi),
Some(1),
"residue field is F_9, so μ_8 is visible"
);
assert_eq!(tame_symbol_invariant(8, &g, &pi), Some(q(1, 8)));
assert_eq!(tame_symbol_exponent(8, &pi, &g), Some(7));
assert_eq!(tame_symbol_invariant(8, &pi, &g), Some(q(7, 8)));
}
#[test]
fn degree_two_norm_form_oracle() {
use crate::forms::trace_twisted_form;
let q1 = trace_twisted_form::<Surcomplex<Rational>>(1);
assert_eq!(q1.q, vec![Rational::from_int(2), Rational::from_int(2)]);
assert!(q1.b.is_empty());
for a in [-7i128, -3, -2, -1, 2, 3, 5, 6, 7] {
let class = BrauerClass::from_two_torsion(&Brauer2Class::quaternion(-1, a).unwrap());
let nrd: Vec<i128> = vec![1, 1, -a, -a];
for p in crate::forms::relevant_primes(&nrd) {
let iso = try_is_isotropic_at_p(&nrd, p).unwrap();
let splits = class.local_invariant(Place::Prime(p)).is_zero();
assert_eq!(iso, splits, "norm-form oracle at p={p} for a={a}");
}
let real_iso = a > 0;
let real_splits = class.local_invariant(Place::Real).is_zero();
assert_eq!(real_iso, real_splits, "norm-form oracle at ℝ for a={a}");
}
}
}