use std::collections::BTreeMap;
use super::padic::{
relevant_primes, try_hilbert_reciprocity_product, try_hilbert_symbol_at, try_is_isotropic_at_p,
Place,
};
use crate::scalar::{Rational, Scalar};
fn square_class(q: &Rational) -> Option<i128> {
q.numer().checked_mul(q.denom())
}
pub fn hilbert_product(a: &Rational, b: &Rational) -> Option<i128> {
try_hilbert_reciprocity_product(square_class(a)?, square_class(b)?)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AdelicIsotropy {
pub real: bool,
pub local: BTreeMap<u128, bool>,
}
impl AdelicIsotropy {
pub fn is_global(&self) -> bool {
self.real && self.local.values().all(|&b| b)
}
pub fn display(&self) -> String {
self.to_string()
}
}
impl std::fmt::Display for AdelicIsotropy {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"AdelicIsotropy(real={}, local={:?}, is_global={})",
self.real,
self.local,
self.is_global()
)
}
}
pub fn isotropy_over_adeles(entries: &[i128]) -> Option<AdelicIsotropy> {
if entries.len() < 3 {
return None;
}
let has_zero = entries.contains(&0);
let has_pos = entries.iter().any(|&e| e > 0);
let has_neg = entries.iter().any(|&e| e < 0);
let real = has_zero || (has_pos && has_neg);
let nz: Vec<i128> = entries.iter().copied().filter(|&e| e != 0).collect();
let local = if has_zero {
BTreeMap::new() } else {
relevant_primes(&nz)
.into_iter()
.map(|p| try_is_isotropic_at_p(&nz, p).map(|b| (p, b)))
.collect::<Option<BTreeMap<_, _>>>()?
};
Some(AdelicIsotropy { real, local })
}
pub fn brauer_local_invariants(a: &Rational, b: &Rational) -> Option<Vec<(Place, Rational)>> {
let (ai, bi) = (square_class(a)?, square_class(b)?);
let mut out = Vec::new();
let zero = Rational::zero();
let half = Rational::try_new(1, 2).expect("1/2 is a valid rational");
let inv = |sym: i128| if sym == 1 { zero.clone() } else { half.clone() };
out.push((
Place::Real,
inv(try_hilbert_symbol_at(ai, bi, Place::Real)?),
));
for p in relevant_primes(&[ai, bi]) {
out.push((
Place::Prime(p),
inv(try_hilbert_symbol_at(ai, bi, Place::Prime(p))?),
));
}
Some(out)
}
pub fn brauer_invariant_sum(a: &Rational, b: &Rational) -> Option<Rational> {
Some(
brauer_local_invariants(a, b)?
.into_iter()
.fold(Rational::zero(), |acc, (_, inv)| acc.add(&inv)),
)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::forms::try_is_isotropic_q;
fn q(n: i128, d: i128) -> Rational {
Rational::try_new(n, d).expect("test rational is valid")
}
#[test]
fn adelic_isotropy_display_render_pin() {
let iso = AdelicIsotropy {
real: true,
local: BTreeMap::from([(2, true), (3, false)]),
};
assert_eq!(
iso.to_string(),
"AdelicIsotropy(real=true, local={2: true, 3: false}, is_global=false)"
);
assert_eq!(iso.display(), iso.to_string());
}
#[test]
fn hilbert_product_is_plus_one_reciprocity() {
for an in -6i128..=6 {
for ad in 1i128..=4 {
for bn in -6i128..=6 {
for bd in 1i128..=4 {
if an == 0 || bn == 0 {
continue;
}
assert_eq!(
hilbert_product(&q(an, ad), &q(bn, bd))
.expect("test square classes fit i128"),
1,
"reciprocity failed at ({an}/{ad}, {bn}/{bd})"
);
}
}
}
}
}
#[test]
fn rank_below_three_is_out_of_domain_not_a_verdict() {
assert_eq!(isotropy_over_adeles(&[]), None);
assert_eq!(isotropy_over_adeles(&[1]), None);
assert_eq!(isotropy_over_adeles(&[1, -1]), None);
}
#[test]
fn adelic_hasse_minkowski_matches_is_isotropic_q() {
let forms: &[&[i128]] = &[
&[1, 1, 1],
&[1, 1, -1],
&[1, 1, -3],
&[1, 1, -2],
&[1, 1, 1, 1],
&[1, 1, 1, -1],
&[1, 1, 1, 1, -1],
&[1, 1, 1, 1, 1],
&[2, 3, -1],
&[1, -2, -5],
&[3, 5, 7, -1],
];
for f in forms {
assert_eq!(
isotropy_over_adeles(f)
.expect("test square classes fit i128")
.is_global(),
try_is_isotropic_q(f).expect("test square classes fit i128"),
"adelic vs global isotropy disagree on {f:?}"
);
}
}
#[test]
fn brauer_invariant_sum_is_zero_in_q_mod_z() {
for an in -8i128..=8 {
for bn in -8i128..=8 {
if an == 0 || bn == 0 {
continue;
}
let s = brauer_invariant_sum(&q(an, 1), &q(bn, 1))
.expect("test square classes fit i128");
assert!(
s.is_integer(),
"Σ inv_v = {s:?} is not ≡ 0 mod ℤ for ({an},{bn})"
);
}
}
}
#[test]
fn hamilton_quaternions_ramify_at_2_and_infinity() {
let invs =
brauer_local_invariants(&q(-1, 1), &q(-1, 1)).expect("test square classes fit i128");
let ramified: Vec<Place> = invs
.iter()
.filter(|(_, inv)| *inv == Rational::try_new(1, 2).expect("1/2 is valid"))
.map(|(pl, _)| *pl)
.collect();
assert_eq!(ramified, vec![Place::Real, Place::Prime(2)]);
let real_inv = &invs[0];
assert_eq!(real_inv.0, Place::Real);
assert_eq!(real_inv.1, Rational::try_new(1, 2).expect("1/2 is valid"));
assert_eq!(
brauer_invariant_sum(&q(1, 1), &q(-7, 1)).expect("test square classes fit i128"),
Rational::zero()
);
}
}