1use crate::clifford::{Metric, MAX_BASIS_DIM};
39use crate::forms::{relevant_primes, try_disc_class, try_hasse_at_place, try_square_free, Place};
40use crate::scalar::Surcomplex;
41use crate::scalar::Surreal;
42use crate::scalar::{ExactRoots, Rational, Scalar};
43use std::cmp::Ordering;
44
45#[derive(Debug, Clone, Copy, PartialEq, Eq)]
46pub enum BaseField {
48 R,
50 C,
52 H,
54}
55
56impl BaseField {
57 fn symbol(self) -> &'static str {
58 match self {
59 BaseField::R => "R",
60 BaseField::C => "C",
61 BaseField::H => "H",
62 }
63 }
64
65 fn real_dimension_log2(self) -> usize {
66 match self {
67 BaseField::R => 0,
68 BaseField::C => 1,
69 BaseField::H => 2,
70 }
71 }
72}
73
74#[derive(Debug, Clone, PartialEq, Eq)]
78pub struct CliffordInvariants {
79 pub base: BaseField,
81 pub matrix_dim: u128,
83 pub doubled: bool,
85 pub radical_dim: usize,
87 pub ground: BaseField,
90 pub signature: (usize, usize),
93}
94
95impl CliffordInvariants {
96 pub fn display(&self) -> String {
99 self.to_string()
100 }
101}
102
103impl std::fmt::Display for CliffordInvariants {
104 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
105 let unit = if self.matrix_dim == 1 {
106 self.base.symbol().to_string()
107 } else {
108 format!("M_{}({})", self.matrix_dim, self.base.symbol())
109 };
110 let core = if self.doubled {
111 format!("{unit} ⊕ {unit}")
112 } else {
113 unit
114 };
115 if self.radical_dim > 0 {
116 write!(
117 f,
118 "{core} ⊗̂ Λ({}^{})",
119 self.ground.symbol(),
120 self.radical_dim
121 )
122 } else {
123 f.write_str(&core)
124 }
125 }
126}
127
128#[derive(Debug, Clone, PartialEq, Eq)]
129pub struct RationalPlaceInvariant {
131 pub place: Place,
133 pub hasse: i128,
135}
136
137#[derive(Debug, Clone, PartialEq, Eq)]
147pub struct RationalCliffordInvariants {
148 pub dim: usize,
150 pub radical_dim: usize,
152 pub discriminant: i128,
154 pub signature: (usize, usize),
156 pub local_hasse: Vec<RationalPlaceInvariant>,
158 pub real_closure: CliffordInvariants,
160}
161
162impl RationalCliffordInvariants {
163 pub fn display(&self) -> String {
165 self.to_string()
166 }
167}
168
169impl std::fmt::Display for RationalCliffordInvariants {
170 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
171 let locals = self
172 .local_hasse
173 .iter()
174 .map(|h| match h.place {
175 Place::Real => format!("R:{:+}", h.hasse),
176 Place::Prime(p) => format!("Q_{}:{:+}", p, h.hasse),
177 })
178 .collect::<Vec<_>>()
179 .join(", ");
180 let rad = if self.radical_dim > 0 {
181 format!(" radical {}", self.radical_dim)
182 } else {
183 String::new()
184 };
185 write!(
186 f,
187 "Q: dim {} disc {} sig ({},{}) hasse [{}]{}; over R: {}",
188 self.dim,
189 self.discriminant,
190 self.signature.0,
191 self.signature.1,
192 locals,
193 rad,
194 self.real_closure
195 )
196 }
197}
198
199fn p2(k: usize) -> u128 {
201 1u128
202 .checked_shl(k.try_into().expect("matrix exponent fits u32"))
203 .expect("matrix dimension exceeds u128")
204}
205
206fn real_core(p: usize, q: usize) -> (BaseField, u128, bool) {
209 let n = p + q;
210 let s = (q as i128 - p as i128).rem_euclid(8) as usize;
211 let base = match s {
212 0 | 6 | 7 => BaseField::R,
213 1 | 5 => BaseField::C,
214 2..=4 => BaseField::H,
215 _ => unreachable!(),
216 };
217 let doubled = s % 4 == 3;
218 let matrix_exp = (n - base.real_dimension_log2() - usize::from(doubled)) / 2;
219 (base, p2(matrix_exp), doubled)
220}
221
222pub fn classify_real(p: usize, q: usize, r: usize) -> CliffordInvariants {
224 assert!(
225 p + q <= MAX_BASIS_DIM,
226 "classify_real: signature dimension p+q={} exceeds MAX_BASIS_DIM={MAX_BASIS_DIM}",
227 p + q
228 );
229 let (base, matrix_dim, doubled) = real_core(p, q);
230 CliffordInvariants {
231 base,
232 matrix_dim,
233 doubled,
234 radical_dim: r,
235 ground: BaseField::R,
236 signature: (p, q),
237 }
238}
239
240pub fn classify_complex(n: usize, r: usize) -> CliffordInvariants {
242 assert!(
243 n <= MAX_BASIS_DIM,
244 "classify_complex: dimension n={n} exceeds MAX_BASIS_DIM={MAX_BASIS_DIM}"
245 );
246 let doubled = !n.is_multiple_of(2);
247 let matrix_dim = p2((n - usize::from(doubled)) / 2);
248 CliffordInvariants {
249 base: BaseField::C,
250 matrix_dim,
251 doubled,
252 radical_dim: r,
253 ground: BaseField::C,
254 signature: (n, 0),
255 }
256}
257
258pub(crate) fn surreal_signature(metric: &Metric<Surreal>) -> Option<(usize, usize, usize)> {
262 let diag = crate::forms::as_diagonal(metric)?;
263 let (mut p, mut q, mut r) = (0, 0, 0);
264 for x in &diag.q {
265 match x.sign() {
266 Ordering::Greater => {
267 x.sqrt()?; p += 1;
269 }
270 Ordering::Less => {
271 x.neg().sqrt()?;
272 q += 1;
273 }
274 Ordering::Equal => r += 1,
275 }
276 }
277 Some((p, q, r))
278}
279
280pub(crate) fn surcomplex_rank(metric: &Metric<Surcomplex<Surreal>>) -> Option<(usize, usize)> {
284 let diag = crate::forms::as_diagonal(metric)?;
285 let mut nonzero = 0usize;
286 let mut radical = 0usize;
287 for z in &diag.q {
288 if z.is_zero() {
289 radical += 1;
290 } else {
291 z.sqrt()?;
292 nonzero += 1;
293 }
294 }
295 Some((nonzero, radical))
296}
297
298fn rational_square_class(x: &Rational) -> Option<i128> {
299 try_square_free(x.numer().checked_mul(x.denom())?)
300}
301
302pub fn classify_rational(metric: &Metric<Rational>) -> Option<RationalCliffordInvariants> {
306 let diag = crate::forms::as_diagonal(metric)?;
307 let mut entries = Vec::new();
308 let mut radical_dim = 0usize;
309 let mut signature = (0usize, 0usize);
310 for x in &diag.q {
311 if x.is_zero() {
312 radical_dim += 1;
313 continue;
314 }
315 match x.sign() {
316 Ordering::Greater => signature.0 += 1,
317 Ordering::Less => signature.1 += 1,
318 Ordering::Equal => unreachable!("zero handled above"),
319 }
320 entries.push(rational_square_class(x)?);
321 }
322 let discriminant = if entries.is_empty() {
323 1
324 } else {
325 try_disc_class(&entries)?
326 };
327 let mut local_hasse = vec![RationalPlaceInvariant {
328 place: Place::Real,
329 hasse: try_hasse_at_place(&entries, Place::Real)?,
330 }];
331 for p in relevant_primes(&entries) {
332 local_hasse.push(RationalPlaceInvariant {
333 place: Place::Prime(p),
334 hasse: try_hasse_at_place(&entries, Place::Prime(p))?,
335 });
336 }
337 Some(RationalCliffordInvariants {
338 dim: entries.len(),
339 radical_dim,
340 discriminant,
341 signature,
342 local_hasse,
343 real_closure: classify_real(signature.0, signature.1, radical_dim),
344 })
345}
346
347pub fn classify_surreal(metric: &Metric<Surreal>) -> Option<CliffordInvariants> {
351 let (p, q, r) = surreal_signature(metric)?;
352 Some(classify_real(p, q, r))
353}
354
355pub fn classify_surcomplex(metric: &Metric<Surcomplex<Surreal>>) -> Option<CliffordInvariants> {
358 let (nonzero, r) = surcomplex_rank(metric)?;
359 Some(classify_complex(nonzero, r))
360}
361
362#[cfg(test)]
363mod tests {
364 use super::*;
365 use crate::clifford::{CliffordAlgebra, Metric};
366 use crate::scalar::Scalar;
367
368 fn rat(n: i128) -> Rational {
369 Rational::from_int(n)
370 }
371 fn surreal_diag(qs: &[i128]) -> Metric<Surreal> {
372 Metric::diagonal(qs.iter().map(|&x| Surreal::from_int(x)).collect())
373 }
374 fn cl_real(qs: &[i128]) -> Option<CliffordInvariants> {
375 classify_surreal(&surreal_diag(qs))
376 }
377 fn name(qs: &[i128]) -> String {
378 cl_real(qs).unwrap().display()
379 }
380
381 #[test]
382 fn low_dimensional_real_clifford_table() {
383 assert_eq!(name(&[]), "R"); assert_eq!(name(&[1]), "R ⊕ R"); assert_eq!(name(&[-1]), "C"); assert_eq!(name(&[1, 1]), "M_2(R)"); assert_eq!(name(&[1, -1]), "M_2(R)"); assert_eq!(name(&[-1, -1]), "H"); assert_eq!(name(&[1, 1, 1]), "M_2(C)"); assert_eq!(name(&[-1, -1, -1]), "H ⊕ H"); assert_eq!(name(&[-1, -1, -1, -1]), "M_2(H)"); }
393
394 #[test]
395 fn physics_signatures() {
396 assert_eq!(name(&[1, -1, -1, -1]), "M_2(H)"); assert_eq!(name(&[1, 1, 1, -1]), "M_4(R)"); assert_eq!(name(&[1, 1, 1, 1, -1]), "M_4(C)"); }
403
404 #[test]
405 fn dimension_is_consistent() {
406 for p in 0..=5usize {
408 for q in 0..=5usize {
409 let t = classify_real(p, q, 0);
410 let unit = match t.base {
411 BaseField::R => 1u128,
412 BaseField::C => 2u128,
413 BaseField::H => 4u128,
414 };
415 let copies = if t.doubled { 2u128 } else { 1u128 };
416 let real_dim = copies * unit * t.matrix_dim * t.matrix_dim;
417 assert_eq!(real_dim, 1u128 << (p + q), "Cl({p},{q})");
418 }
419 }
420 }
421
422 #[test]
423 fn radical_gives_exterior_factor() {
424 assert_eq!(name(&[-1, 0, 0]), "C ⊗̂ Λ(R^2)");
426 assert_eq!(name(&[0, 0, 0]), "R ⊗̂ Λ(R^3)");
428 }
429
430 #[test]
431 fn matrix_dimension_reaches_dim_128_boundary() {
432 assert_eq!(classify_real(128, 0, 0).matrix_dim, 1u128 << 64);
433 assert_eq!(classify_complex(128, 0).matrix_dim, 1u128 << 64);
434 }
435
436 #[test]
440 #[should_panic(expected = "MAX_BASIS_DIM")]
441 fn classify_real_rejects_dimension_past_max_basis_dim() {
442 classify_real(129, 0, 0);
443 }
444
445 #[test]
446 #[should_panic(expected = "MAX_BASIS_DIM")]
447 fn classify_complex_rejects_dimension_past_max_basis_dim() {
448 classify_complex(129, 0);
449 }
450
451 #[test]
452 fn rational_classification_keeps_square_classes_and_local_hasse_data() {
453 let one = classify_rational(&Metric::diagonal(vec![rat(1)])).unwrap();
454 let two = classify_rational(&Metric::diagonal(vec![rat(2)])).unwrap();
455 assert_eq!(one.signature, two.signature);
456 assert_ne!(one.discriminant, two.discriminant);
457
458 let h = classify_rational(&Metric::diagonal(vec![rat(-1), rat(-1)])).unwrap();
459 assert_eq!(h.discriminant, 1);
460 assert_eq!(h.signature, (0, 2));
461 assert!(h
462 .local_hasse
463 .iter()
464 .any(|x| x.place == Place::Real && x.hasse == -1));
465 assert!(h
466 .local_hasse
467 .iter()
468 .any(|x| x.place == Place::Prime(2) && x.hasse == -1));
469 }
470
471 #[test]
472 fn surreal_accepts_represented_exact_square_classes() {
473 let m = Metric::diagonal(vec![Surreal::omega(), Surreal::epsilon().neg()]);
476 assert_eq!(classify_surreal(&m).unwrap().display(), "M_2(R)");
477 assert_eq!(
478 classify_surreal(&surreal_diag(&[4])).unwrap().display(),
479 "R ⊕ R"
480 );
481 }
482
483 #[test]
484 fn surreal_declines_unrepresented_square_classes() {
485 assert_eq!(classify_surreal(&surreal_diag(&[2])), None);
488 }
489
490 #[test]
491 fn surcomplex_is_two_fold_on_exact_square_subdomain() {
492 let even =
493 Metric::<Surcomplex<Surreal>>::diagonal(vec![Surcomplex::one(), Surcomplex::one()]);
494 assert_eq!(classify_surcomplex(&even).unwrap().display(), "M_2(C)"); let odd = Metric::<Surcomplex<Surreal>>::diagonal(vec![Surcomplex::one()]);
496 assert_eq!(classify_surcomplex(&odd).unwrap().display(), "C ⊕ C"); let minus_one = Metric::<Surcomplex<Surreal>>::diagonal(vec![Surcomplex::new(
498 Surreal::from_int(-1),
499 Surreal::zero(),
500 )]);
501 assert_eq!(classify_surcomplex(&minus_one).unwrap().display(), "C ⊕ C");
502 let square_of_two_plus_i = Metric::<Surcomplex<Surreal>>::diagonal(vec![Surcomplex::new(
503 Surreal::from_int(3),
504 Surreal::from_int(4),
505 )]);
506 assert_eq!(
507 classify_surcomplex(&square_of_two_plus_i)
508 .unwrap()
509 .display(),
510 "C ⊕ C"
511 );
512 }
513
514 #[test]
515 fn surcomplex_declines_unrepresented_square_classes() {
516 let two = Metric::<Surcomplex<Surreal>>::diagonal(vec![Surcomplex::new(
517 Surreal::from_int(2),
518 Surreal::zero(),
519 )]);
520 assert_eq!(classify_surcomplex(&two), None);
521 }
522
523 #[test]
524 fn even_subalgebra_classification_drops_one_dimension() {
525 let alg = CliffordAlgebra::new(3, Metric::diagonal(vec![rat(1), rat(1), rat(1)]));
527 let even = alg.even_subalgebra().unwrap();
528 assert_eq!(
529 classify_rational(even.metric())
530 .unwrap()
531 .real_closure
532 .display(),
533 "H"
534 );
535 let st = CliffordAlgebra::new(4, Metric::diagonal(vec![rat(1), rat(-1), rat(-1), rat(-1)]));
537 let st_even = st.even_subalgebra().unwrap();
538 assert_eq!(
541 classify_rational(st_even.metric())
542 .unwrap()
543 .real_closure
544 .display(),
545 classify_real(1, 2, 0).display()
546 );
547 }
548}