use crate::clifford::{Metric, MAX_BASIS_DIM};
use crate::scalar::{FiniteField, Ordered, QuadraticInvolution, Scalar, Surcomplex};
use std::cmp::Ordering;
use std::collections::BTreeMap;
#[derive(Debug, Clone, PartialEq)]
pub struct HermitianForm<S: Scalar> {
gram: Vec<Vec<Surcomplex<S>>>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct HermitianSignature {
pub pos: usize,
pub neg: usize,
pub radical: usize,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HermitianRestrictionError {
DimensionTooLarge {
hermitian_dim: usize,
max_hermitian_dim: usize,
},
FixedFieldProjection,
}
impl std::fmt::Display for HermitianRestrictionError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
HermitianRestrictionError::DimensionTooLarge {
hermitian_dim,
max_hermitian_dim,
} => write!(
f,
"Hermitian dimension {hermitian_dim} restricts past the supported maximum {max_hermitian_dim}"
),
HermitianRestrictionError::FixedFieldProjection => {
f.write_str("Hermitian restriction coefficient left the represented fixed field")
}
}
}
}
impl std::error::Error for HermitianRestrictionError {}
fn restrict_hermitian_gram<E>(
gram: &[Vec<E>],
) -> Result<Metric<E::Fixed>, HermitianRestrictionError>
where
E: QuadraticInvolution,
{
let n = gram.len();
let Some(dim) = n.checked_mul(2) else {
return Err(HermitianRestrictionError::DimensionTooLarge {
hermitian_dim: n,
max_hermitian_dim: MAX_BASIS_DIM / 2,
});
};
if dim > MAX_BASIS_DIM {
return Err(HermitianRestrictionError::DimensionTooLarge {
hermitian_dim: n,
max_hermitian_dim: MAX_BASIS_DIM / 2,
});
}
let basis = E::fixed_basis();
let mut q = Vec::with_capacity(dim);
for (i, row) in gram.iter().enumerate() {
for beta in &basis {
let value = beta.involute().mul(&row[i]).mul(beta);
q.push(
E::project_fixed(&value).ok_or(HermitianRestrictionError::FixedFieldProjection)?,
);
}
}
let mut b = BTreeMap::new();
for left in 0..dim {
let (i, a) = (left / 2, left % 2);
for right in (left + 1)..dim {
let (j, c) = (right / 2, right % 2);
let value = basis[a].involute().mul(&gram[i][j]).mul(&basis[c]);
let polar = value.add(&value.involute());
let fixed =
E::project_fixed(&polar).ok_or(HermitianRestrictionError::FixedFieldProjection)?;
if !fixed.is_zero() {
b.insert((left, right), fixed);
}
}
}
Ok(Metric::new(q, b))
}
impl HermitianSignature {
pub fn display(&self) -> String {
self.to_string()
}
}
impl std::fmt::Display for HermitianSignature {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"HermitianSignature(pos={}, neg={}, radical={})",
self.pos, self.neg, self.radical
)
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct FiniteHermitianForm<F: FiniteField> {
gram: Vec<Vec<F>>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct FiniteHermitianInvariants {
pub rank: usize,
pub radical_dim: usize,
pub characteristic: u128,
pub base_degree: usize,
pub extension_degree: usize,
pub base_field_order: Option<u128>,
pub extension_field_order: Option<u128>,
}
impl FiniteHermitianInvariants {
pub fn display(&self) -> String {
self.to_string()
}
}
impl std::fmt::Display for FiniteHermitianInvariants {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let ext = self.extension_field_order.map_or_else(
|| format!("{}^{}", self.characteristic, self.extension_degree),
|q| q.to_string(),
);
let base = self.base_field_order.map_or_else(
|| format!("{}^{}", self.characteristic, self.base_degree),
|q| q.to_string(),
);
write!(
f,
"FiniteHermitianInvariants(rank={}, radical_dim={}, field=F_{ext} over F_{base})",
self.rank, self.radical_dim,
)
}
}
fn checked_pow_u128(base: u128, exp: usize) -> Option<u128> {
let mut out = 1u128;
for _ in 0..exp {
out = out.checked_mul(base)?;
}
Some(out)
}
fn ensure_supported_finite_hermitian<F: FiniteField>() -> bool {
F::ext_degree() > 0 && F::ext_degree().is_multiple_of(2)
}
fn finite_hermitian_conj<F: FiniteField>(x: F) -> F {
x.frobenius_iter(F::ext_degree() / 2)
}
fn matrix_rank<F: Scalar>(rows: Vec<Vec<F>>) -> usize {
let ncols = rows.first().map_or(0, |r| r.len());
let nullspace = crate::linalg::field::unit_pivot_nullspace(rows, ncols)
.expect("finite-field pivot is always invertible; unit_pivot_nullspace returned None");
ncols - nullspace.len()
}
impl<F: FiniteField> FiniteHermitianForm<F> {
pub fn from_gram(gram: Vec<Vec<F>>) -> Option<Self> {
if !ensure_supported_finite_hermitian::<F>() {
return None;
}
let n = gram.len();
for row in &gram {
if row.len() != n {
return None;
}
}
for i in 0..n {
if finite_hermitian_conj(gram[i][i]) != gram[i][i] {
return None;
}
for j in 0..n {
if gram[i][j] != finite_hermitian_conj(gram[j][i]) {
return None;
}
}
}
Some(FiniteHermitianForm { gram })
}
pub fn diagonal(entries: Vec<F>) -> Option<Self> {
if entries.iter().any(|&x| finite_hermitian_conj(x) != x) {
return None;
}
let n = entries.len();
let mut gram = vec![vec![F::zero(); n]; n];
for (i, x) in entries.into_iter().enumerate() {
gram[i][i] = x;
}
Self::from_gram(gram)
}
pub fn dim(&self) -> usize {
self.gram.len()
}
pub fn gram(&self) -> &[Vec<F>] {
&self.gram
}
pub fn direct_sum(&self, other: &FiniteHermitianForm<F>) -> FiniteHermitianForm<F> {
let (n, m) = (self.dim(), other.dim());
let mut gram = vec![vec![F::zero(); n + m]; n + m];
for i in 0..n {
for j in 0..n {
gram[i][j] = self.gram[i][j];
}
}
for i in 0..m {
for j in 0..m {
gram[n + i][n + j] = other.gram[i][j];
}
}
FiniteHermitianForm { gram }
}
pub fn rank(&self) -> usize {
matrix_rank(self.gram.clone())
}
pub fn classify(&self) -> FiniteHermitianInvariants {
let rank = self.rank();
let extension_degree = F::ext_degree();
let base_degree = extension_degree / 2;
FiniteHermitianInvariants {
rank,
radical_dim: self.dim() - rank,
characteristic: F::characteristic(),
base_degree,
extension_degree,
base_field_order: checked_pow_u128(F::characteristic(), base_degree),
extension_field_order: checked_pow_u128(F::characteristic(), extension_degree),
}
}
pub fn restrict_scalars(
&self,
) -> Result<Metric<<F as QuadraticInvolution>::Fixed>, HermitianRestrictionError>
where
F: QuadraticInvolution,
{
restrict_hermitian_gram(&self.gram)
}
}
fn combine<S: Scalar>(
h: &mut [Vec<Surcomplex<S>>],
target: usize,
source: usize,
lambda: &Surcomplex<S>,
) {
let n = h.len();
for r in 0..n {
let add = lambda.mul(&h[r][source]);
h[r][target] = h[r][target].add(&add);
}
let cl = lambda.conj();
for c in 0..n {
let add = cl.mul(&h[source][c]);
h[target][c] = h[target][c].add(&add);
}
}
fn swap_rows_cols<S: Scalar>(h: &mut [Vec<Surcomplex<S>>], k: usize, i: usize) {
h.swap(k, i);
for row in h.iter_mut() {
row.swap(k, i);
}
}
fn ensure_pivot<S: Scalar>(h: &mut [Vec<Surcomplex<S>>], k: usize) -> bool {
let n = h.len();
if !h[k][k].is_zero() {
return true;
}
for i in (k + 1)..n {
if !h[i][i].is_zero() {
swap_rows_cols(h, k, i);
return true;
}
}
for j in (k + 1)..n {
if !h[k][j].is_zero() {
let lambda = h[k][j].conj();
combine(h, k, j, &lambda);
return true;
}
}
false }
impl<S: Scalar> HermitianForm<S> {
pub fn from_gram(gram: Vec<Vec<Surcomplex<S>>>) -> Option<Self> {
let n = gram.len();
for row in &gram {
if row.len() != n {
return None;
}
}
for i in 0..n {
if !gram[i][i].im.is_zero() {
return None; }
for j in 0..n {
if gram[i][j] != gram[j][i].conj() {
return None; }
}
}
Some(HermitianForm { gram })
}
pub fn from_skew(gram: Vec<Vec<Surcomplex<S>>>) -> Option<Self> {
let n = gram.len();
for row in &gram {
if row.len() != n {
return None;
}
}
for i in 0..n {
if !gram[i][i].re.is_zero() {
return None; }
for j in 0..n {
if gram[i][j] != gram[j][i].conj().neg() {
return None; }
}
}
let i_unit = Surcomplex::i();
let h: Vec<Vec<Surcomplex<S>>> = gram
.iter()
.map(|row| row.iter().map(|x| i_unit.mul(x)).collect())
.collect();
Self::from_gram(h)
}
pub fn diagonal(reals: Vec<S>) -> Self {
let n = reals.len();
let mut gram = vec![vec![Surcomplex::zero(); n]; n];
for (i, r) in reals.into_iter().enumerate() {
gram[i][i] = Surcomplex::new(r, S::zero());
}
HermitianForm { gram }
}
pub fn dim(&self) -> usize {
self.gram.len()
}
pub fn gram(&self) -> &[Vec<Surcomplex<S>>] {
&self.gram
}
pub fn restrict_scalars(&self) -> Result<Metric<S>, HermitianRestrictionError>
where
S: Ordered,
{
restrict_hermitian_gram(&self.gram)
}
pub fn direct_sum(&self, other: &HermitianForm<S>) -> HermitianForm<S> {
let (n, m) = (self.dim(), other.dim());
let mut gram = vec![vec![Surcomplex::zero(); n + m]; n + m];
for i in 0..n {
for j in 0..n {
gram[i][j] = self.gram[i][j].clone();
}
}
for i in 0..m {
for j in 0..m {
gram[n + i][n + j] = other.gram[i][j].clone();
}
}
HermitianForm { gram }
}
pub fn diagonalize(&self) -> Vec<S> {
let n = self.dim();
let mut h = self.gram.clone();
for k in 0..n {
if !ensure_pivot(&mut h, k) {
continue; }
let pinv = h[k][k]
.inv()
.expect("nonzero real pivot inverts in a field");
for i in (k + 1)..n {
if !h[i][k].is_zero() {
let mu = h[k][i].neg().mul(&pinv); combine(&mut h, i, k, &mu);
}
}
}
(0..n).map(|k| h[k][k].re.clone()).collect()
}
pub fn signature(&self, sign: impl Fn(&S) -> Ordering) -> HermitianSignature {
let mut sig = HermitianSignature {
pos: 0,
neg: 0,
radical: 0,
};
for d in self.diagonalize() {
match sign(&d) {
Ordering::Greater => sig.pos += 1,
Ordering::Less => sig.neg += 1,
Ordering::Equal => sig.radical += 1,
}
}
sig
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::scalar::{Fpn, Nimber, QuadraticInvolution, Rational, Surreal};
type GC = Surcomplex<Rational>;
fn gc(re: i128, im: i128) -> GC {
Surcomplex::new(Rational::from_int(re), Rational::from_int(im))
}
fn rsign(x: &Rational) -> Ordering {
x.sign()
}
fn evaluate_metric<S: Scalar>(metric: &Metric<S>, coordinates: &[S]) -> S {
assert_eq!(metric.dim(), coordinates.len());
let mut value = S::zero();
for (i, x) in coordinates.iter().enumerate() {
value = value.add(&metric.q()[i].mul(&x.mul(x)));
}
for (&(i, j), bij) in metric.b() {
value = value.add(&bij.mul(&coordinates[i].mul(&coordinates[j])));
}
value
}
#[test]
fn surcomplex_restriction_keeps_quadratic_and_trace_polar_data() {
let h = HermitianForm::from_gram(vec![vec![gc(2, 0), gc(0, 1)], vec![gc(0, -1), gc(2, 0)]])
.unwrap();
let restricted = h.restrict_scalars().unwrap();
assert_eq!(restricted.q(), vec![rsign_value(2); 4]);
assert_eq!(restricted.b().len(), 2);
assert_eq!(restricted.b().get(&(0, 3)), Some(&rsign_value(-2)));
assert_eq!(restricted.b().get(&(1, 2)), Some(&rsign_value(2)));
assert!(restricted.a().is_empty());
let diagonal = HermitianForm::<Rational>::diagonal(h.diagonalize())
.restrict_scalars()
.unwrap();
assert_eq!(
crate::forms::classify_rational(&restricted),
crate::forms::classify_rational(&diagonal),
"unitary diagonalization must preserve the restricted quadratic class"
);
}
fn rsign_value(n: i128) -> Rational {
Rational::from_int(n)
}
#[test]
fn surcomplex_restriction_doubles_signature_and_radical() {
let h = HermitianForm::<Surreal>::diagonal(vec![
Surreal::from_int(1),
Surreal::from_int(-1),
Surreal::zero(),
]);
let restricted = h.restrict_scalars().unwrap();
assert_eq!(restricted.dim(), 6);
assert_eq!(
crate::forms::surreal_signature(&restricted),
Some((2, 2, 2))
);
}
#[test]
fn restriction_refuses_a_doubled_dimension_past_the_engine_boundary() {
let h = HermitianForm::<Rational>::diagonal(vec![Rational::one(); 65]);
assert_eq!(
h.restrict_scalars(),
Err(HermitianRestrictionError::DimensionTooLarge {
hermitian_dim: 65,
max_hermitian_dim: 64,
})
);
}
#[test]
fn finite_hermitian_forms_over_f9_are_rank_classified() {
type F9 = Fpn<3, 2>;
let one = F9::one();
let two = F9::from_int(2);
let x = F9::from_coeffs(&[0, 1]);
let xbar = x.frobenius_iter(1);
let h = FiniteHermitianForm::<F9>::from_gram(vec![vec![one, x], vec![xbar, two]])
.expect("H* = H for the middle Frobenius involution");
let inv = h.classify();
assert_eq!(inv.rank, 2);
assert_eq!(inv.radical_dim, 0);
assert_eq!(inv.characteristic, 3);
assert_eq!(inv.base_degree, 1);
assert_eq!(inv.extension_degree, 2);
assert_eq!(inv.base_field_order, Some(3));
assert_eq!(inv.extension_field_order, Some(9));
let split = FiniteHermitianForm::<F9>::diagonal(vec![one, one]).unwrap();
let hyperbolic = FiniteHermitianForm::<F9>::from_gram(vec![
vec![F9::zero(), one],
vec![one, F9::zero()],
])
.unwrap();
assert_eq!(split.classify(), hyperbolic.classify());
assert!(
FiniteHermitianForm::<F9>::from_gram(vec![vec![one, x], vec![x, two]]).is_none(),
"lower off-diagonal entry must be conjugated"
);
assert!(
FiniteHermitianForm::<F9>::diagonal(vec![x]).is_none(),
"diagonal entries must be fixed by conjugation"
);
let norm_plane = FiniteHermitianForm::<F9>::diagonal(vec![one])
.unwrap()
.restrict_scalars()
.unwrap();
let ordinary = crate::forms::classify_finite_odd(&norm_plane).unwrap();
assert_eq!(ordinary.dim, 2);
assert_eq!(ordinary.radical_dim, 0);
assert!(ordinary.disc_is_square);
}
#[test]
fn finite_char2_restriction_keeps_the_norm_quadratic_form() {
type F4 = Fpn<2, 2>;
type F16 = Fpn<2, 4>;
let over_f2 = FiniteHermitianForm::<F4>::diagonal(vec![F4::one()])
.unwrap()
.restrict_scalars()
.unwrap();
let arf_f2 = crate::forms::arf_char2(&over_f2).unwrap();
assert_eq!((arf_f2.arf, arf_f2.rank, arf_f2.radical_dim), (1, 2, 0));
assert!(!arf_f2.radical_anisotropic);
let fixed_coefficient = F4::generator();
let coefficient = <F16 as QuadraticInvolution>::embed_fixed(&fixed_coefficient);
let h = FiniteHermitianForm::<F16>::diagonal(vec![coefficient]).unwrap();
let over_f4 = h.restrict_scalars().unwrap();
let arf_f4 = crate::forms::arf_char2(&over_f4).unwrap();
assert_eq!((arf_f4.arf, arf_f4.rank, arf_f4.radical_dim), (1, 2, 0));
assert!(!arf_f4.radical_anisotropic);
let basis = <F16 as QuadraticInvolution>::fixed_basis();
for left in 0..4u128 {
for right in 0..4u128 {
let coords = [
F4::from_coeffs(&[left & 1, (left >> 1) & 1]),
F4::from_coeffs(&[right & 1, (right >> 1) & 1]),
];
let vector = <F16 as QuadraticInvolution>::embed_fixed(&coords[0])
.mul(&basis[0])
.add(&<F16 as QuadraticInvolution>::embed_fixed(&coords[1]).mul(&basis[1]));
let hermitian_value = vector.involute().mul(&coefficient).mul(&vector);
let projected = <F16 as QuadraticInvolution>::project_fixed(&hermitian_value)
.expect("h(v,v) lies in F_4");
assert_eq!(evaluate_metric(&over_f4, &coords), projected);
}
}
let doubled = h.direct_sum(&h).restrict_scalars().unwrap();
let doubled_arf = crate::forms::arf_char2(&doubled).unwrap();
assert_eq!((doubled_arf.arf, doubled_arf.rank), (0, 4));
assert_eq!(doubled, over_f4.direct_sum(&over_f4));
}
#[test]
fn finite_hermitian_forms_include_char2_even_degree_fields() {
type F16 = Fpn<2, 4>;
let one = F16::one();
let x = F16::from_coeffs(&[0, 1, 0, 0]);
let xbar = x.frobenius_iter(2);
let h = FiniteHermitianForm::<F16>::from_gram(vec![
vec![one, x, F16::zero()],
vec![xbar, one, F16::zero()],
vec![F16::zero(), F16::zero(), F16::zero()],
])
.unwrap();
let inv = h.classify();
assert_eq!(inv.rank, 2);
assert_eq!(inv.radical_dim, 1);
assert_eq!(inv.characteristic, 2);
assert_eq!(inv.base_degree, 2);
assert_eq!(inv.base_field_order, Some(4));
assert_eq!(inv.extension_field_order, Some(16));
}
#[test]
fn finite_hermitian_forms_reject_odd_degree_fields() {
type F27 = Fpn<3, 3>;
assert!(FiniteHermitianForm::<F27>::from_gram(vec![vec![F27::one()]]).is_none());
}
#[test]
fn nimber_quadratic_middle_frobenius_reports_width_boundary() {
let h = FiniteHermitianForm::<Nimber>::diagonal(vec![Nimber(1), Nimber(0)]).unwrap();
let inv = h.classify();
assert_eq!(inv.rank, 1);
assert_eq!(inv.radical_dim, 1);
assert_eq!(inv.characteristic, 2);
assert_eq!(inv.base_degree, 64);
assert_eq!(inv.extension_degree, 128);
assert_eq!(inv.base_field_order, Some(1u128 << 64));
assert_eq!(inv.extension_field_order, None);
}
#[test]
fn diagonal_real_form_has_sylvester_signature() {
let h = HermitianForm::<Rational>::diagonal(vec![
Rational::from_int(1),
Rational::from_int(1),
Rational::from_int(-1),
]);
assert_eq!(
h.signature(rsign),
HermitianSignature {
pos: 2,
neg: 1,
radical: 0
}
);
}
#[test]
fn off_diagonal_hermitian_diagonalizes() {
let h = HermitianForm::from_gram(vec![vec![gc(2, 0), gc(0, 1)], vec![gc(0, -1), gc(2, 0)]])
.unwrap();
assert_eq!(
h.diagonalize(),
vec![Rational::from_int(2), Rational::new(3, 2)]
);
assert_eq!(
h.signature(rsign),
HermitianSignature {
pos: 2,
neg: 0,
radical: 0
}
);
assert!(HermitianForm::from_gram(vec![
vec![gc(2, 0), gc(0, 1)],
vec![gc(0, 1), gc(2, 0)], ])
.is_none());
}
#[test]
fn off_diagonal_pivot_uses_conjugate_partner() {
let h = HermitianForm::from_gram(vec![vec![gc(0, 0), gc(1, 1)], vec![gc(1, -1), gc(0, 0)]])
.unwrap();
assert_eq!(
h.diagonalize(),
vec![Rational::from_int(4), Rational::new(-1, 2)]
);
assert_eq!(
h.signature(rsign),
HermitianSignature {
pos: 1,
neg: 1,
radical: 0
}
);
}
#[test]
fn indefinite_and_radical() {
let h = HermitianForm::from_gram(vec![vec![gc(1, 0), gc(0, 0)], vec![gc(0, 0), gc(-1, 0)]])
.unwrap();
assert_eq!(h.signature(rsign).pos, 1);
assert_eq!(h.signature(rsign).neg, 1);
let rad =
HermitianForm::<Rational>::diagonal(vec![Rational::from_int(0), Rational::from_int(5)]);
assert_eq!(h.direct_sum(&h).signature(rsign).pos, 2); assert_eq!(rad.signature(rsign).radical, 1);
}
#[test]
fn skew_hermitian_signature_via_multiplication_by_i() {
let h = HermitianForm::<Rational>::from_skew(vec![
vec![gc(0, 0), gc(1, 0)],
vec![gc(-1, 0), gc(0, 0)],
])
.unwrap();
let sig = h.signature(rsign);
assert_eq!((sig.pos, sig.neg), (1, 1));
assert!(HermitianForm::<Rational>::from_skew(vec![
vec![gc(0, 2), gc(0, 0)],
vec![gc(0, 0), gc(0, -3)],
])
.is_some());
assert!(HermitianForm::<Rational>::from_skew(vec![
vec![gc(1, 0), gc(0, 0)],
vec![gc(0, 0), gc(0, 0)],
])
.is_none());
}
#[test]
fn signature_over_surreal_base() {
let h =
HermitianForm::<Surreal>::diagonal(vec![Surreal::omega(), Surreal::epsilon().neg()]);
let sig = h.signature(|x| x.sign());
assert_eq!(sig.pos, 1);
assert_eq!(sig.neg, 1);
}
}