use crate::clifford::Metric;
use crate::forms::{
as_diagonal, classify_surcomplex, classify_surreal, clifford_brauer_class, finite_odd_witt,
try_disc_class, try_hasse_at_place_ff, try_ramified_places_ff, try_relevant_places_ff,
try_square_free, Brauer2Class, FiniteOddField, FunctionFieldPlace, Place, WittClassG,
WittClassGError,
};
use crate::scalar::{Nimber, Rational, RationalFunction, Scalar, Surcomplex, Surreal};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum BrauerWallError {
DifferentFields,
DifferentGroundFields,
}
impl std::fmt::Display for BrauerWallError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
BrauerWallError::DifferentFields => {
f.write_str("Brauer-Wall classes are from different finite fields")
}
BrauerWallError::DifferentGroundFields => {
f.write_str("cannot add Brauer-Wall classes across different ground fields")
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BrauerWallClass {
Real(u128),
Complex(u128),
OddChar {
field_order: u128,
kappa: u128,
e0: u128,
sclass: u128,
},
Char2 {
field_degree: u128,
arf: u128,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RationalBrauerWallClass {
dimension_parity: u128,
signed_discriminant: i128,
clifford_brauer_class: Brauer2Class,
}
#[derive(Debug, Clone)]
pub struct FunctionFieldBrauer2Class<S: FiniteOddField> {
ramified: Vec<FunctionFieldPlace<S>>,
}
impl<S: FiniteOddField> PartialEq for FunctionFieldBrauer2Class<S> {
fn eq(&self, other: &Self) -> bool {
self.ramified.len() == other.ramified.len()
&& self.ramified.iter().all(|p| other.ramified.contains(p))
}
}
impl<S: FiniteOddField> Eq for FunctionFieldBrauer2Class<S> {}
impl<S: FiniteOddField> FunctionFieldBrauer2Class<S> {
pub fn split() -> Self {
FunctionFieldBrauer2Class {
ramified: Vec::new(),
}
}
pub fn from_ramified_places(places: Vec<FunctionFieldPlace<S>>) -> Self {
let mut ramified = Vec::new();
for place in places {
if !ramified.contains(&place) {
ramified.push(place);
}
}
FunctionFieldBrauer2Class { ramified }
}
pub fn is_split(&self) -> bool {
self.ramified.is_empty()
}
pub fn ramified_places(&self) -> &[FunctionFieldPlace<S>] {
&self.ramified
}
pub fn add(&self, other: &Self) -> Self {
let mut ramified = self.ramified.clone();
for place in &other.ramified {
if let Some(pos) = ramified.iter().position(|p| p == place) {
ramified.remove(pos);
} else {
ramified.push(place.clone());
}
}
FunctionFieldBrauer2Class { ramified }
}
pub fn local_invariant(&self, place: &FunctionFieldPlace<S>) -> 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: &RationalFunction<S>, b: &RationalFunction<S>) -> Option<Self> {
Some(FunctionFieldBrauer2Class::from_ramified_places(
try_ramified_places_ff(a, b)?,
))
}
pub fn display(&self) -> String {
self.to_string()
}
}
impl<S: FiniteOddField> std::fmt::Display for FunctionFieldBrauer2Class<S> {
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, "FunctionFieldBrauer2Class(ramified={places:?})")
}
}
#[derive(Debug, Clone)]
pub struct FunctionFieldBrauerWallClass<S: FiniteOddField> {
dimension_parity: u128,
signed_discriminant: RationalFunction<S>,
clifford_brauer_class: FunctionFieldBrauer2Class<S>,
}
impl<S: FiniteOddField> PartialEq for FunctionFieldBrauerWallClass<S> {
fn eq(&self, other: &Self) -> bool {
self.dimension_parity == other.dimension_parity
&& self.clifford_brauer_class == other.clifford_brauer_class
&& same_global_square_class_ff(&self.signed_discriminant, &other.signed_discriminant)
}
}
impl<S: FiniteOddField> Eq for FunctionFieldBrauerWallClass<S> {}
impl<S: FiniteOddField> FunctionFieldBrauerWallClass<S> {
pub fn split() -> Self {
FunctionFieldBrauerWallClass {
dimension_parity: 0,
signed_discriminant: RationalFunction::one(),
clifford_brauer_class: FunctionFieldBrauer2Class::split(),
}
}
pub fn from_parts(
dimension_parity: u128,
signed_discriminant: RationalFunction<S>,
clifford_brauer_class: FunctionFieldBrauer2Class<S>,
) -> Option<Self> {
if dimension_parity > 1 || signed_discriminant.is_zero() {
return None;
}
Some(FunctionFieldBrauerWallClass {
dimension_parity,
signed_discriminant,
clifford_brauer_class,
})
}
pub fn is_split(&self) -> bool {
self.dimension_parity == 0
&& crate::forms::is_global_square_ff(&self.signed_discriminant)
&& self.clifford_brauer_class.is_split()
}
pub fn dimension_parity(&self) -> u128 {
self.dimension_parity
}
pub fn signed_discriminant(&self) -> &RationalFunction<S> {
&self.signed_discriminant
}
pub fn clifford_brauer_class(&self) -> &FunctionFieldBrauer2Class<S> {
&self.clifford_brauer_class
}
pub fn zero_like(&self) -> Self {
FunctionFieldBrauerWallClass::split()
}
pub fn try_add(&self, other: &Self) -> Option<Self> {
let p = self.dimension_parity;
let q = other.dimension_parity;
let mut signed_disc = self.signed_discriminant.mul(&other.signed_discriminant);
if p == 1 && q == 1 {
signed_disc = signed_disc.neg();
}
let mut brauer = self.clifford_brauer_class.add(&other.clifford_brauer_class);
brauer = brauer.add(&FunctionFieldBrauer2Class::quaternion(
&self.signed_discriminant,
&other.signed_discriminant,
)?);
let neg_one = ff_neg_one::<S>();
if p == 0 && q == 1 {
brauer = brauer.add(&FunctionFieldBrauer2Class::quaternion(
&neg_one,
&self.signed_discriminant,
)?);
}
if p == 1 && q == 0 {
brauer = brauer.add(&FunctionFieldBrauer2Class::quaternion(
&neg_one,
&other.signed_discriminant,
)?);
}
Some(FunctionFieldBrauerWallClass {
dimension_parity: (p + q) % 2,
signed_discriminant: signed_disc,
clifford_brauer_class: brauer,
})
}
pub fn display(&self) -> String {
self.to_string()
}
}
impl<S: FiniteOddField> std::fmt::Display for FunctionFieldBrauerWallClass<S> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"BW(F_q(t)): parity {} signed_disc {} c(q) ramified {:?}",
self.dimension_parity,
self.signed_discriminant,
self.clifford_brauer_class.ramified_places()
)
}
}
impl RationalBrauerWallClass {
pub fn split() -> Self {
RationalBrauerWallClass {
dimension_parity: 0,
signed_discriminant: 1,
clifford_brauer_class: Brauer2Class::split(),
}
}
pub fn from_parts(
dimension_parity: u128,
signed_discriminant: i128,
clifford_brauer_class: Brauer2Class,
) -> Option<Self> {
if dimension_parity > 1 || signed_discriminant == 0 {
return None;
}
Some(RationalBrauerWallClass {
dimension_parity,
signed_discriminant: try_square_free(signed_discriminant)?,
clifford_brauer_class,
})
}
pub fn is_split(&self) -> bool {
self.dimension_parity == 0
&& self.signed_discriminant == 1
&& self.clifford_brauer_class.is_split()
}
pub fn dimension_parity(&self) -> u128 {
self.dimension_parity
}
pub fn signed_discriminant(&self) -> i128 {
self.signed_discriminant
}
pub fn clifford_brauer_class(&self) -> &Brauer2Class {
&self.clifford_brauer_class
}
pub fn zero_like(&self) -> Self {
RationalBrauerWallClass::split()
}
pub fn try_add(&self, other: &Self) -> Option<Self> {
let p = self.dimension_parity;
let q = other.dimension_parity;
let mut signed_disc = self
.signed_discriminant
.checked_mul(other.signed_discriminant)?;
if p == 1 && q == 1 {
signed_disc = signed_disc.checked_neg()?;
}
let signed_disc = try_square_free(signed_disc)?;
let mut brauer = self.clifford_brauer_class.add(&other.clifford_brauer_class);
brauer = brauer.add(&Brauer2Class::quaternion(
self.signed_discriminant,
other.signed_discriminant,
)?);
if p == 0 && q == 1 {
brauer = brauer.add(&Brauer2Class::quaternion(-1, self.signed_discriminant)?);
}
if p == 1 && q == 0 {
brauer = brauer.add(&Brauer2Class::quaternion(-1, other.signed_discriminant)?);
}
Some(RationalBrauerWallClass {
dimension_parity: (p + q) % 2,
signed_discriminant: signed_disc,
clifford_brauer_class: brauer,
})
}
pub fn real_bott_index(&self) -> u128 {
let disc_negative = self.signed_discriminant < 0;
let real_brauer = self
.clifford_brauer_class
.ramified_places()
.contains(&Place::Real);
match (self.dimension_parity, disc_negative, real_brauer) {
(0, false, false) => 0,
(1, true, false) => 1,
(0, true, true) => 2,
(1, false, true) => 3,
(0, false, true) => 4,
(1, true, true) => 5,
(0, true, false) => 6,
(1, false, false) => 7,
_ => unreachable!("dimension_parity is always a bit"),
}
}
pub fn real_class(&self) -> BrauerWallClass {
BrauerWallClass::Real(self.real_bott_index())
}
}
impl std::fmt::Display for RationalBrauerWallClass {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"BW(Q): parity {} signed_disc {} c(q) ramified {:?}",
self.dimension_parity,
self.signed_discriminant,
self.clifford_brauer_class.ramified_places()
)
}
}
impl BrauerWallClass {
pub fn try_add(&self, other: &BrauerWallClass) -> Result<BrauerWallClass, BrauerWallError> {
match (*self, *other) {
(BrauerWallClass::Real(a), BrauerWallClass::Real(b)) => {
Ok(BrauerWallClass::Real((a + b) % 8))
}
(BrauerWallClass::Complex(a), BrauerWallClass::Complex(b)) => {
Ok(BrauerWallClass::Complex((a + b) % 2))
}
(
BrauerWallClass::OddChar {
field_order: qa,
kappa: ka,
e0: e0a,
sclass: sa,
},
BrauerWallClass::OddChar {
field_order: qb,
kappa: kb,
e0: e0b,
sclass: sb,
},
) => {
let w = WittClassG::OddChar {
field_order: qa,
kappa: ka,
e0: e0a,
sclass: sa,
}
.try_add(&WittClassG::OddChar {
field_order: qb,
kappa: kb,
e0: e0b,
sclass: sb,
})
.map_err(|e| match e {
WittClassGError::DifferentFields => BrauerWallError::DifferentFields,
_ => BrauerWallError::DifferentGroundFields,
})?;
match w {
WittClassG::OddChar {
field_order,
kappa,
e0,
sclass,
} => Ok(BrauerWallClass::OddChar {
field_order,
kappa,
e0,
sclass,
}),
_ => unreachable!(),
}
}
(
BrauerWallClass::Char2 {
field_degree: ma,
arf: a,
},
BrauerWallClass::Char2 {
field_degree: mb,
arf: b,
},
) => {
if ma != mb {
return Err(BrauerWallError::DifferentFields);
}
Ok(BrauerWallClass::Char2 {
field_degree: ma,
arf: a ^ b,
})
}
_ => Err(BrauerWallError::DifferentGroundFields),
}
}
pub fn zero_like(&self) -> BrauerWallClass {
match *self {
BrauerWallClass::Real(_) => BrauerWallClass::Real(0),
BrauerWallClass::Complex(_) => BrauerWallClass::Complex(0),
BrauerWallClass::OddChar {
field_order, kappa, ..
} => BrauerWallClass::OddChar {
field_order,
kappa,
e0: 0,
sclass: 0,
},
BrauerWallClass::Char2 { field_degree, .. } => BrauerWallClass::Char2 {
field_degree,
arf: 0,
},
}
}
pub fn display(&self) -> String {
self.to_string()
}
}
impl std::fmt::Display for BrauerWallClass {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
BrauerWallClass::Real(s) => write!(f, "BrauerWallClass::Real({s})"),
BrauerWallClass::Complex(p) => write!(f, "BrauerWallClass::Complex({p})"),
BrauerWallClass::OddChar {
field_order,
kappa,
e0,
sclass,
} => write!(
f,
"BrauerWallClass::OddChar(field_order={field_order}, kappa={kappa}, e0={e0}, sclass={sclass})"
),
BrauerWallClass::Char2 { field_degree, arf } => {
write!(f, "BrauerWallClass::Char2(field_degree={field_degree}, arf={arf})")
}
}
}
}
fn rational_square_class(x: &Rational) -> Option<i128> {
try_square_free(x.numer().checked_mul(x.denom())?)
}
pub fn rational_signed_discriminant_class(entries: &[i128]) -> Option<i128> {
if entries.contains(&0) {
return None;
}
let disc = if entries.is_empty() {
1
} else {
try_disc_class(entries)?
};
if matches!(entries.len() % 4, 0 | 1) {
Some(disc)
} else {
try_square_free(disc.checked_neg()?)
}
}
fn ff_neg_one<S: FiniteOddField>() -> RationalFunction<S> {
RationalFunction::from_base(S::one().neg())
}
fn ff_discriminant<S: FiniteOddField>(
entries: &[RationalFunction<S>],
) -> Option<RationalFunction<S>> {
if entries.iter().any(|x| x.is_zero()) {
return None;
}
let mut disc = RationalFunction::one();
for x in entries {
disc = disc.mul(x);
}
Some(disc)
}
fn same_global_square_class_ff<S: FiniteOddField>(
a: &RationalFunction<S>,
b: &RationalFunction<S>,
) -> bool {
if a.is_zero() || b.is_zero() {
return false;
}
b.inv()
.map(|binv| crate::forms::is_global_square_ff(&a.mul(&binv)))
.unwrap_or(false)
}
pub fn function_field_signed_discriminant_class<S: FiniteOddField>(
entries: &[RationalFunction<S>],
) -> Option<RationalFunction<S>> {
let disc = ff_discriminant(entries)?;
if matches!(entries.len() % 4, 0 | 1) {
Some(disc)
} else {
Some(disc.neg())
}
}
pub fn hasse_brauer_class_ff<S: FiniteOddField>(
entries: &[RationalFunction<S>],
) -> Option<FunctionFieldBrauer2Class<S>> {
if entries.iter().any(|x| x.is_zero()) {
return None;
}
let mut ramified = Vec::new();
for place in try_relevant_places_ff(entries)? {
if try_hasse_at_place_ff(entries, &place)? == -1 {
ramified.push(place);
}
}
Some(FunctionFieldBrauer2Class::from_ramified_places(ramified))
}
fn clifford_correction_ff<S: FiniteOddField>(
n: usize,
d: &RationalFunction<S>,
) -> Option<FunctionFieldBrauer2Class<S>> {
let r = n % 8;
let neg_one = ff_neg_one::<S>();
let mut delta = FunctionFieldBrauer2Class::split();
if matches!(r, 0 | 3 | 4 | 7) {
delta = delta.add(&FunctionFieldBrauer2Class::quaternion(&neg_one, d)?);
}
if matches!(r, 3..=6) {
delta = delta.add(&FunctionFieldBrauer2Class::quaternion(&neg_one, &neg_one)?);
}
Some(delta)
}
pub fn clifford_brauer_class_ff<S: FiniteOddField>(
entries: &[RationalFunction<S>],
) -> Option<FunctionFieldBrauer2Class<S>> {
if entries.iter().any(|x| x.is_zero()) {
return None;
}
let s = hasse_brauer_class_ff(entries)?;
let d = ff_discriminant(entries)?;
Some(s.add(&clifford_correction_ff(entries.len(), &d)?))
}
pub fn bw_class_real(metric: &Metric<Surreal>) -> Option<BrauerWallClass> {
let (p, q) = classify_surreal(metric)?.signature;
Some(BrauerWallClass::Real(
(q as i128 - p as i128).rem_euclid(8) as u128
))
}
pub fn bw_class_complex(metric: &Metric<Surcomplex<Surreal>>) -> Option<BrauerWallClass> {
let ct = classify_surcomplex(metric)?;
let (p, q) = ct.signature;
Some(BrauerWallClass::Complex(((p + q) % 2) as u128))
}
pub fn bw_class_finite_odd<F: FiniteOddField>(metric: &Metric<F>) -> Option<BrauerWallClass> {
match finite_odd_witt(metric)? {
WittClassG::OddChar {
field_order,
kappa,
e0,
sclass,
} => Some(BrauerWallClass::OddChar {
field_order,
kappa,
e0,
sclass,
}),
_ => unreachable!("finite_odd_witt returns the OddChar variant"),
}
}
pub fn bw_class_rational(metric: &Metric<Rational>) -> Option<RationalBrauerWallClass> {
let diag = as_diagonal(metric)?;
let mut entries = Vec::new();
for x in &diag.q {
if !x.is_zero() {
entries.push(rational_square_class(x)?);
}
}
Some(RationalBrauerWallClass {
dimension_parity: (entries.len() % 2) as u128,
signed_discriminant: rational_signed_discriminant_class(&entries)?,
clifford_brauer_class: clifford_brauer_class(&entries)?,
})
}
pub fn bw_class_function_field<S: FiniteOddField>(
metric: &Metric<RationalFunction<S>>,
) -> Option<FunctionFieldBrauerWallClass<S>> {
let diag = as_diagonal(metric)?;
let mut entries = Vec::new();
for x in &diag.q {
if !x.is_zero() {
entries.push(x.clone());
}
}
Some(FunctionFieldBrauerWallClass {
dimension_parity: (entries.len() % 2) as u128,
signed_discriminant: function_field_signed_discriminant_class(&entries)?,
clifford_brauer_class: clifford_brauer_class_ff(&entries)?,
})
}
pub fn bw_class_nimber(metric: &Metric<Nimber>) -> Option<BrauerWallClass> {
match WittClassG::try_char2_from_metric(metric).ok()? {
WittClassG::Char2 { field_degree, arf } => {
Some(BrauerWallClass::Char2 { field_degree, arf })
}
_ => unreachable!("try_char2_from_metric returns the Char2 variant"),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::clifford::CliffordAlgebra;
use crate::forms::classify_real;
use crate::scalar::{Fp, Nimber, Poly, Rational, RationalFunction, Scalar, Surcomplex};
use std::collections::BTreeSet;
#[test]
fn brauer_wall_class_display_render_pin() {
assert_eq!(
BrauerWallClass::Real(7).to_string(),
"BrauerWallClass::Real(7)"
);
assert_eq!(
BrauerWallClass::Complex(1).to_string(),
"BrauerWallClass::Complex(1)"
);
assert_eq!(
BrauerWallClass::OddChar {
field_order: 5,
kappa: 0,
e0: 1,
sclass: 0
}
.to_string(),
"BrauerWallClass::OddChar(field_order=5, kappa=0, e0=1, sclass=0)"
);
let c2 = BrauerWallClass::Char2 {
field_degree: 1,
arf: 1,
};
assert_eq!(
c2.to_string(),
"BrauerWallClass::Char2(field_degree=1, arf=1)"
);
assert_eq!(c2.display(), c2.to_string());
}
#[test]
fn function_field_brauer2_class_display_render_pin() {
assert_eq!(
FunctionFieldBrauer2Class::<Fp<5>>::split().to_string(),
"FunctionFieldBrauer2Class(ramified=[])"
);
let t = rf(&[0, 1], &[1]);
let two = rf(&[2], &[1]);
let quat = FunctionFieldBrauer2Class::quaternion(&t, &two)
.expect("test square classes are defined");
assert_eq!(
quat.to_string(),
"FunctionFieldBrauer2Class(ramified=[\"t\", \"∞\"])"
);
assert_eq!(quat.display(), quat.to_string());
}
fn real_diag(signs: &[i128]) -> Metric<Surreal> {
Metric::diagonal(
signs
.iter()
.map(|&s| {
if s >= 0 {
Surreal::one()
} else {
Surreal::one().neg()
}
})
.collect(),
)
}
fn rational_diag(entries: &[i128]) -> Metric<Rational> {
Metric::diagonal(entries.iter().map(|&x| Rational::from_int(x)).collect())
}
type F5 = RationalFunction<Fp<5>>;
type Poly5 = Poly<Fp<5>>;
fn rf(num: &[i128], den: &[i128]) -> F5 {
RationalFunction::new(
num.iter().map(|&n| Fp::<5>::from_int(n)).collect(),
den.iter().map(|&n| Fp::<5>::from_int(n)).collect(),
)
}
fn poly(c: &[i128]) -> Poly5 {
Poly::new(c.iter().map(|&n| Fp::<5>::from_int(n)).collect())
}
fn ff_diag(entries: &[F5]) -> Metric<F5> {
Metric::diagonal(entries.to_vec())
}
fn subgroup_order(g: BrauerWallClass) -> usize {
let id = g.zero_like();
let mut cur = g;
let mut n = 1;
while cur != id {
cur = cur
.try_add(&g)
.expect("subgroup walk stays inside one Brauer-Wall leg");
n += 1;
assert!(n <= 64, "subgroup walk did not close — bug in add");
}
n
}
fn rational_subgroup_order(g: RationalBrauerWallClass) -> usize {
let id = g.zero_like();
let mut cur = g.clone();
let mut n = 1;
while cur != id {
cur = cur
.try_add(&g)
.expect("subgroup walk stays inside rational BW");
n += 1;
assert!(n <= 64, "subgroup walk did not close — bug in add");
}
n
}
#[test]
fn bw_real_is_the_bott_clock() {
for p in 0..5usize {
for q in 0..5usize {
let signs: Vec<i128> = std::iter::repeat_n(1, p)
.chain(std::iter::repeat_n(-1, q))
.collect();
if signs.is_empty() {
continue;
}
let s = (q as i128 - p as i128).rem_euclid(8) as u128;
assert_eq!(
bw_class_real(&real_diag(&signs)),
Some(BrauerWallClass::Real(s))
);
let _ = classify_real(p, q, 0); }
}
}
#[test]
fn bw_real_is_homomorphism_over_graded_tensor() {
let v = real_diag(&[1, 1, -1]); let w = real_diag(&[-1, -1]); let av = CliffordAlgebra::new(3, v.clone());
let aw = CliffordAlgebra::new(2, w.clone());
let tensored = av.graded_tensor(&aw);
assert_eq!(
bw_class_real(tensored.metric()),
Some(
bw_class_real(&v)
.unwrap()
.try_add(&bw_class_real(&w).unwrap())
.expect("same real Brauer-Wall leg")
)
);
}
#[test]
fn bw_real_discovers_z8() {
let g = bw_class_real(&real_diag(&[-1])).unwrap();
assert_eq!(g, BrauerWallClass::Real(1));
assert_eq!(subgroup_order(g), 8);
let h = bw_class_real(&real_diag(&[1])).unwrap();
assert_eq!(h, BrauerWallClass::Real(7));
assert_eq!(subgroup_order(h), 8);
}
#[test]
fn bw_complex_is_z2() {
let one = Surcomplex::<Surreal>::one();
let m1 = Metric::diagonal(vec![one.clone()]);
let m2 = Metric::diagonal(vec![one.clone(), one.clone()]);
assert_eq!(bw_class_complex(&m1), Some(BrauerWallClass::Complex(1)));
assert_eq!(bw_class_complex(&m2), Some(BrauerWallClass::Complex(0)));
assert_eq!(subgroup_order(BrauerWallClass::Complex(1)), 2);
}
#[test]
fn bw_rational_projects_to_clifford_and_signed_discriminant() {
let m = rational_diag(&[1, 1, 1]);
let c = bw_class_rational(&m).unwrap();
assert_eq!(c.dimension_parity(), 1);
assert_eq!(c.signed_discriminant(), -1);
assert_eq!(
c.clifford_brauer_class(),
&clifford_brauer_class(&[1, 1, 1]).unwrap()
);
assert_eq!(
*c.clifford_brauer_class().ramified_places(),
[Place::Real, Place::Prime(2)].into_iter().collect()
);
let h = rational_diag(&[1, -1, 0]);
let hc = bw_class_rational(&h).unwrap();
assert!(
hc.is_split(),
"the radical is projected away and <1,-1> is hyperbolic"
);
}
#[test]
fn bw_rational_uses_the_wall_twisted_group_law() {
let forms: &[&[i128]] = &[
&[1],
&[-1],
&[1, -1],
&[1, 1],
&[-1, -1],
&[2, 3],
&[1, 2, 3],
&[1, -2, 5],
&[2, 3, 5, 7],
];
for a in forms {
for b in forms {
let ma = rational_diag(a);
let mb = rational_diag(b);
let direct = ma.direct_sum(&mb);
assert_eq!(
bw_class_rational(&direct),
bw_class_rational(&ma)
.unwrap()
.try_add(&bw_class_rational(&mb).unwrap()),
"Wall law failed for {a:?} ⊥ {b:?}"
);
}
}
}
#[test]
fn bw_rational_extends_to_the_real_bott_clock() {
for p in 0..5usize {
for q in 0..5usize {
if p + q == 0 {
continue;
}
let signs: Vec<i128> = std::iter::repeat_n(1, p)
.chain(std::iter::repeat_n(-1, q))
.collect();
let rational = bw_class_rational(&rational_diag(&signs)).unwrap();
assert_eq!(
rational.real_class(),
bw_class_real(&real_diag(&signs)).unwrap(),
"Q -> R extension failed for signature ({p},{q})"
);
}
}
let g = bw_class_rational(&rational_diag(&[-1])).unwrap();
assert_eq!(g.real_bott_index(), 1);
assert_eq!(rational_subgroup_order(g), 8);
}
#[test]
fn bw_function_field_projects_to_clifford_and_signed_discriminant() {
let t = rf(&[0, 1], &[1]);
let two = rf(&[2], &[1]);
let m = ff_diag(&[t.clone(), two.clone()]);
let c = bw_class_function_field(&m).unwrap();
assert_eq!(c.dimension_parity(), 0);
assert_eq!(c.signed_discriminant(), &rf(&[0, 3], &[1])); let quat = FunctionFieldBrauer2Class::quaternion(&t, &two).unwrap();
assert_eq!(c.clifford_brauer_class(), &quat);
assert!(quat.satisfies_reciprocity());
assert_eq!(quat.ramified_places().len(), 2);
assert!(quat
.ramified_places()
.contains(&FunctionFieldPlace::Finite(poly(&[0, 1]))));
assert!(quat
.ramified_places()
.contains(&FunctionFieldPlace::Infinite));
}
#[test]
fn bw_function_field_uses_wall_law_and_metric_facade() {
let forms: Vec<Vec<F5>> = vec![
vec![rf(&[0, 1], &[1])], vec![rf(&[2], &[1])], vec![rf(&[1, 1], &[1])], vec![rf(&[0, 1], &[1]), rf(&[2], &[1])],
vec![rf(&[1], &[1]), rf(&[0, 4], &[1])],
];
for a in &forms {
for b in &forms {
let ma = ff_diag(a);
let mb = ff_diag(b);
let direct = ma.direct_sum(&mb);
assert_eq!(
bw_class_function_field(&direct),
bw_class_function_field(&ma)
.unwrap()
.try_add(&bw_class_function_field(&mb).unwrap()),
"Wall law failed over F_5(t) for {a:?} ⊥ {b:?}"
);
assert_eq!(ma.bw_class().ok(), bw_class_function_field(&ma));
}
}
}
#[test]
fn bw_function_field_projects_radicals_and_compares_square_classes() {
let t = rf(&[0, 1], &[1]);
let two = rf(&[2], &[1]);
let singular = ff_diag(&[t.clone(), F5::zero(), two.clone()]);
let nonsingular = ff_diag(&[t.clone(), two.clone()]);
assert_eq!(
bw_class_function_field(&singular),
bw_class_function_field(&nonsingular)
);
let square = rf(&[1, 2, 1], &[1]); let c1 = FunctionFieldBrauerWallClass::from_parts(
0,
t.clone(),
FunctionFieldBrauer2Class::split(),
)
.unwrap();
let c2 = FunctionFieldBrauerWallClass::from_parts(
0,
t.mul(&square),
FunctionFieldBrauer2Class::split(),
)
.unwrap();
assert_eq!(c1, c2, "signed discriminants are compared modulo squares");
}
fn expected_correction_ff<S: FiniteOddField>(
n: usize,
d: &RationalFunction<S>,
) -> FunctionFieldBrauer2Class<S> {
let neg_one = ff_neg_one::<S>();
let mut delta = FunctionFieldBrauer2Class::split();
if matches!(n % 8, 3..=6) {
delta = delta.add(&FunctionFieldBrauer2Class::quaternion(&neg_one, &neg_one).unwrap());
}
if matches!(n % 8, 0 | 3 | 4 | 7) {
delta = delta.add(&FunctionFieldBrauer2Class::quaternion(&neg_one, d).unwrap());
}
delta
}
#[test]
fn ff_clifford_is_hasse_plus_the_documented_correction_all_residues() {
let t = rf(&[0, 1], &[1]);
let tp1 = rf(&[1, 1], &[1]);
let tp2 = rf(&[2, 1], &[1]);
let tp3 = rf(&[3, 1], &[1]);
let two = rf(&[2], &[1]);
let three = rf(&[3], &[1]);
let four = rf(&[4], &[1]);
let one = rf(&[1], &[1]);
let forms: Vec<Vec<F5>> = vec![
vec![t.clone()], vec![t.clone(), two.clone()], vec![t.clone(), two.clone(), tp1.clone()], vec![t.clone(), two.clone(), tp1.clone(), three.clone()], vec![
t.clone(),
two.clone(),
tp1.clone(),
three.clone(),
tp2.clone(),
], vec![
t.clone(),
two.clone(),
tp1.clone(),
three.clone(),
tp2.clone(),
four.clone(),
], vec![
t.clone(),
two.clone(),
tp1.clone(),
three.clone(),
tp2.clone(),
four.clone(),
tp3.clone(),
], vec![
t.clone(),
two.clone(),
tp1.clone(),
three.clone(),
tp2.clone(),
four.clone(),
tp3.clone(),
one.clone(),
], ];
for f in &forms {
let d = ff_discriminant(f).expect("no zero entries");
let expected = hasse_brauer_class_ff(f)
.unwrap()
.add(&expected_correction_ff(f.len(), &d));
assert_eq!(
clifford_brauer_class_ff(f).unwrap(),
expected,
"correction mismatch for n={} (n mod 8 = {})",
f.len(),
f.len() % 8
);
}
}
#[test]
fn ff_rank_two_clifford_is_the_quaternion_algebra() {
let a_vals = [
rf(&[0, 1], &[1]),
rf(&[2], &[1]),
rf(&[1, 1], &[1]),
rf(&[3], &[1]),
];
let b_vals = [
rf(&[1, 1], &[1]),
rf(&[3], &[1]),
rf(&[0, 1], &[1]),
rf(&[2], &[1]),
];
for a in &a_vals {
for b in &b_vals {
if a.is_zero() || b.is_zero() {
continue;
}
assert_eq!(
clifford_brauer_class_ff(&[a.clone(), b.clone()]).unwrap(),
FunctionFieldBrauer2Class::quaternion(a, b).unwrap(),
"C(<{a:?},{b:?}>) != (a,b)"
);
}
}
}
#[test]
fn ff_rank_three_even_clifford_is_minus_ab_minus_ac() {
let vals = [rf(&[0, 1], &[1]), rf(&[2], &[1]), rf(&[1, 1], &[1])];
for a in &vals {
for b in &vals {
for c in &vals {
let neg_ab = a.neg().mul(b);
let neg_ac = a.neg().mul(c);
assert_eq!(
clifford_brauer_class_ff(&[a.clone(), b.clone(), c.clone()]).unwrap(),
FunctionFieldBrauer2Class::quaternion(&neg_ab, &neg_ac).unwrap(),
"C0(<{a:?},{b:?},{c:?}>) != (-ab,-ac)"
);
}
}
}
}
fn oddchar_diag<const P: u128>(qs: &[u128]) -> Metric<Fp<P>> {
Metric::diagonal(qs.iter().map(|&x| Fp::<P>::from_u128(x)).collect())
}
fn nimber_metric(qs: &[u128], pairs: &[(usize, usize)]) -> Metric<Nimber> {
let mut b = std::collections::BTreeMap::new();
for &(i, j) in pairs {
b.insert((i, j), Nimber(1));
}
Metric::new(qs.iter().map(|&q| Nimber(q)).collect(), b)
}
#[test]
fn bw_oddchar_homomorphism_and_discovered_order_four() {
fn collect_group<const P: u128>() -> (BTreeSet<(u128, u128, u128, u128)>, bool) {
let gens: Vec<BrauerWallClass> = (1..P)
.map(|a| bw_class_finite_odd(&oddchar_diag::<P>(&[a])).unwrap())
.collect();
let mut seen: BTreeSet<(u128, u128, u128, u128)> = BTreeSet::new();
let key = |c: &BrauerWallClass| match *c {
BrauerWallClass::OddChar {
field_order,
kappa,
e0,
sclass,
} => (field_order, kappa, e0, sclass),
_ => unreachable!(),
};
let id = gens[0].zero_like();
let mut frontier = vec![id];
seen.insert(key(&id));
while let Some(x) = frontier.pop() {
for g in &gens {
let y = x.try_add(g).expect("odd-char closure stays in one field");
if seen.insert(key(&y)) {
frontier.push(y);
}
}
}
let has_order_4 = gens.iter().any(|&g| subgroup_order(g) == 4);
(seen, has_order_4)
}
let a = oddchar_diag::<3>(&[1, 2]);
let b = oddchar_diag::<3>(&[2]);
assert_eq!(
bw_class_finite_odd(&a.direct_sum(&b)),
Some(
bw_class_finite_odd(&a)
.unwrap()
.try_add(&bw_class_finite_odd(&b).unwrap())
.expect("same odd-char Brauer-Wall leg")
)
);
let (g3, cyclic3) = collect_group::<3>();
assert_eq!(g3.len(), 4, "BW(F_3) discovered order");
assert!(cyclic3, "BW(F_3) ≅ ℤ/4 (−1 nonsquare)");
let (g5, cyclic5) = collect_group::<5>();
assert_eq!(g5.len(), 4, "BW(F_5) discovered order");
assert!(
!cyclic5,
"BW(F_5) ≅ (ℤ/2)² (−1 square): no element of order 4"
);
}
#[test]
fn bw_char2_is_arf_witt_z2_and_homomorphic() {
let h = nimber_metric(&[0, 0], &[(0, 1)]); let a = nimber_metric(&[1, 1], &[(0, 1)]); assert_eq!(
bw_class_nimber(&h),
Some(BrauerWallClass::Char2 {
field_degree: 1,
arf: 0
})
);
assert_eq!(
bw_class_nimber(&a),
Some(BrauerWallClass::Char2 {
field_degree: 1,
arf: 1
})
);
assert_eq!(
bw_class_nimber(&a.direct_sum(&a)),
Some(BrauerWallClass::Char2 {
field_degree: 1,
arf: 0
})
);
assert_eq!(
bw_class_nimber(&a.direct_sum(&h)),
Some(
bw_class_nimber(&a)
.unwrap()
.try_add(&bw_class_nimber(&h).unwrap())
.expect("same nimber Brauer-Wall leg")
)
);
assert_eq!(
subgroup_order(BrauerWallClass::Char2 {
field_degree: 1,
arf: 1
}),
2
);
}
#[test]
fn bw_char2_checks_actual_graded_tensor_metric() {
let h = nimber_metric(&[0, 0], &[(0, 1)]);
let a = nimber_metric(&[1, 1], &[(0, 1)]);
let ah = CliffordAlgebra::new(2, h.clone());
let aa = CliffordAlgebra::new(2, a.clone());
let tensored = aa.graded_tensor(&ah);
assert_eq!(
bw_class_nimber(tensored.metric()),
Some(
bw_class_nimber(&a)
.unwrap()
.try_add(&bw_class_nimber(&h).unwrap())
.expect("same nimber Brauer-Wall leg")
)
);
}
#[test]
fn bw_char2_rejects_singular_and_general_bilinear_metrics() {
let singular = nimber_metric(&[1], &[]);
assert_eq!(bw_class_nimber(&singular), None);
let mut upper = std::collections::BTreeMap::new();
upper.insert((0usize, 1usize), Nimber(1));
let general = Metric::general(
vec![Nimber(1), Nimber(1)],
std::collections::BTreeMap::<(usize, usize), Nimber>::new(),
upper,
);
assert_eq!(bw_class_nimber(&general), None);
}
}