use std::collections::BTreeSet;
use crate::forms::{
relevant_primes, try_disc_class, try_hasse_at_place, try_hilbert_symbol_at, Place,
};
use crate::scalar::{Rational, Scalar};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Brauer2Class {
ramified: BTreeSet<Place>,
}
impl Brauer2Class {
pub fn split() -> Self {
Brauer2Class {
ramified: BTreeSet::new(),
}
}
pub fn is_split(&self) -> bool {
self.ramified.is_empty()
}
pub fn ramified_places(&self) -> &BTreeSet<Place> {
&self.ramified
}
pub fn add(&self, other: &Self) -> Self {
Brauer2Class {
ramified: self
.ramified
.symmetric_difference(&other.ramified)
.copied()
.collect(),
}
}
pub fn local_invariant(&self, place: Place) -> Rational {
if self.ramified.contains(&place) {
Rational::try_new(1, 2).expect("1/2 is a valid rational")
} else {
Rational::zero()
}
}
pub fn satisfies_reciprocity(&self) -> bool {
self.ramified.len().is_multiple_of(2)
}
pub fn quaternion(a: i128, b: i128) -> Option<Self> {
if a == 0 || b == 0 {
return None;
}
let mut ramified = BTreeSet::new();
if try_hilbert_symbol_at(a, b, Place::Real)? == -1 {
ramified.insert(Place::Real);
}
for p in relevant_primes(&[a, b]) {
if try_hilbert_symbol_at(a, b, Place::Prime(p))? == -1 {
ramified.insert(Place::Prime(p));
}
}
Some(Brauer2Class { ramified })
}
pub fn display(&self) -> String {
self.to_string()
}
}
impl std::fmt::Display for Brauer2Class {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let places: Vec<String> = self.ramified.iter().map(|p| p.to_string()).collect();
write!(f, "Brauer2Class(ramified={places:?})")
}
}
pub fn hasse_brauer_class(entries: &[i128]) -> Option<Brauer2Class> {
if entries.contains(&0) {
return None;
}
let mut ramified = BTreeSet::new();
if try_hasse_at_place(entries, Place::Real)? == -1 {
ramified.insert(Place::Real);
}
for p in relevant_primes(entries) {
if try_hasse_at_place(entries, Place::Prime(p))? == -1 {
ramified.insert(Place::Prime(p));
}
}
Some(Brauer2Class { ramified })
}
fn clifford_correction(n: usize, d: i128) -> Option<Brauer2Class> {
let r = n % 8;
let mut delta = Brauer2Class::split();
if matches!(r, 0 | 3 | 4 | 7) {
delta = delta.add(&Brauer2Class::quaternion(-1, d)?);
}
if matches!(r, 3..=6) {
delta = delta.add(&Brauer2Class::quaternion(-1, -1)?);
}
Some(delta)
}
pub fn clifford_brauer_class(entries: &[i128]) -> Option<Brauer2Class> {
if entries.contains(&0) {
return None;
}
let s = hasse_brauer_class(entries)?;
let d = if entries.is_empty() {
1
} else {
try_disc_class(entries)?
};
Some(s.add(&clifford_correction(entries.len(), d)?))
}
#[cfg(test)]
mod tests {
use super::*;
fn places(ps: &[Place]) -> BTreeSet<Place> {
ps.iter().copied().collect()
}
fn clifford(entries: &[i128]) -> Brauer2Class {
clifford_brauer_class(entries).expect("test square classes fit i128")
}
fn hasse(entries: &[i128]) -> Brauer2Class {
hasse_brauer_class(entries).expect("test square classes fit i128")
}
fn quat(a: i128, b: i128) -> Brauer2Class {
Brauer2Class::quaternion(a, b).expect("test quaternion is defined")
}
#[test]
fn display_render_pin() {
assert_eq!(
Brauer2Class::split().to_string(),
"Brauer2Class(ramified=[])"
);
assert_eq!(
quat(-1, -1).to_string(),
"Brauer2Class(ramified=[\"R\", \"Q_2\"])"
);
assert_eq!(quat(-1, -1).display(), quat(-1, -1).to_string());
}
#[test]
fn add_is_xor_of_ramification_sets() {
let h = quat(-1, -1); assert!(h.add(&h).is_split(), "x + x = 0 (2-torsion)");
assert_eq!(h.add(&Brauer2Class::split()), h, "0 is the identity");
let k = quat(2, 5);
assert_eq!(h.add(&k), k.add(&h));
}
#[test]
fn split_form_is_split() {
assert!(clifford(&[1, -1]).is_split());
assert!(hasse(&[1, -1]).is_split());
}
#[test]
fn hamilton_quaternions_ramify_at_2_and_infinity() {
assert_eq!(
*clifford(&[-1, -1, -1]).ramified_places(),
places(&[Place::Real, Place::Prime(2)])
);
assert!(hasse(&[1, 1, 1]).is_split());
assert_eq!(
*clifford(&[1, 1, 1]).ramified_places(),
places(&[Place::Real, Place::Prime(2)])
);
}
#[test]
fn rank_two_clifford_is_the_quaternion_algebra() {
for a in [-3i128, -2, -1, 1, 2, 3, 5, 6, 7] {
for b in [-5i128, -3, -1, 1, 2, 3, 5, 7, 10] {
assert_eq!(clifford(&[a, b]), quat(a, b), "C(⟨{a},{b}⟩) ≠ (a,b)");
}
}
}
#[test]
fn rank_three_even_clifford_is_minus_ab_minus_ac() {
for a in [-3i128, -1, 1, 2, 3, 5] {
for b in [-3i128, -1, 1, 2, 5, 7] {
for c in [-5i128, -1, 1, 3, 6] {
assert_eq!(
clifford(&[a, b, c]),
quat(-a * b, -a * c),
"C₀(⟨{a},{b},{c}⟩) ≠ (−ab,−ac)"
);
}
}
}
}
#[test]
fn rank_one_is_always_split() {
for a in [-7i128, -2, -1, 1, 2, 3, 5] {
assert!(clifford(&[a]).is_split(), "⟨{a}⟩ should be split");
}
}
#[test]
fn every_class_satisfies_reciprocity() {
let forms: &[&[i128]] = &[
&[1, -1],
&[2, 3],
&[-1, -1, -1],
&[1, 1, 1],
&[2, 3, 5],
&[1, -2, -5],
&[1, 1, 1, 1],
&[1, 1, 1, -1],
&[2, 3, 5, 7],
&[1, 1, 1, 1, 1],
&[-1, -2, -3, -5, -7],
&[2, 3, 5, 7, 11, 13],
];
for f in forms {
assert!(
clifford(f).satisfies_reciprocity(),
"c({f:?}) ramifies oddly"
);
assert!(hasse(f).satisfies_reciprocity(), "s({f:?}) ramifies oddly");
}
}
fn expected_correction(n: usize, d: i128) -> Brauer2Class {
let mut delta = Brauer2Class::split();
if matches!(n % 8, 3..=6) {
delta = delta.add(&quat(-1, -1));
}
if matches!(n % 8, 0 | 3 | 4 | 7) {
delta = delta.add(&quat(-1, d));
}
delta
}
#[test]
fn clifford_is_hasse_plus_the_documented_correction() {
let forms: &[&[i128]] = &[
&[1], &[2, 3], &[1, 2, 3], &[1, 2, 3, 5], &[1, 2, 3, 5, 7], &[1, 2, 3, 5, 7, 11], &[2, 3, 5, 7, 11, 13, 1], &[1, 2, 3, 5, 7, 11, 13, 1], ];
for f in forms {
let d = try_disc_class(f).expect("disc fits i128");
let expected = hasse(f).add(&expected_correction(f.len(), d));
assert_eq!(clifford(f), expected, "correction mismatch for {f:?}");
}
}
#[test]
fn n_one_and_two_have_no_correction() {
for f in [&[3i128] as &[i128], &[5], &[2, 7], &[-3, 5]] {
assert_eq!(clifford(f), hasse(f), "δ should vanish for {f:?}");
}
}
#[test]
fn rejects_degenerate_and_overflow() {
assert_eq!(clifford_brauer_class(&[1, 0, 1]), None); assert_eq!(hasse_brauer_class(&[0]), None);
}
}