Skip to main content

ogdoad/forms/integral/
weyl_versors.rs

1//! ADE Weyl reflections realized by Clifford versors.
2//!
3//! A simply-laced root lattice has Gram matrix `G` with `G_ii = 2`. Feeding that
4//! lattice into [`IntegralForm::clifford_metric`] makes each simple root a
5//! grade-1 Clifford vector with square `2`, hence an invertible Pin versor. Its
6//! twisted adjoint action is the root reflection
7//!
8//! ```text
9//! s_i(e_j) = e_j - <e_j,e_i> e_i.
10//! ```
11//!
12//! The report surface checks this action against the Cartan reflection matrix,
13//! checks every simple reflection has determinant `-1` via the outermorphism
14//! determinant, and forms the Coxeter versor `e_0 e_1 ... e_{n-1}` whose Pin
15//! action has the expected Coxeter order.
16
17use super::{IntegralForm, NiemeierComponentKind};
18use crate::clifford::{determinant, versor_grade_parity, CliffordAlgebra, LinearMap, Multivector};
19use crate::scalar::{Rational, Scalar};
20use std::fmt;
21
22/// The Clifford/Weyl report for one irreducible ADE component.
23#[derive(Clone, Debug, PartialEq, Eq)]
24pub struct WeylVersorInvariants {
25    /// Irreducible ADE type.
26    pub kind: NiemeierComponentKind,
27    /// Rank of the root system.
28    pub rank: usize,
29    /// Order of its Weyl group.
30    pub weyl_group_order: u128,
31    /// Coxeter number.
32    pub coxeter_number: u128,
33    /// Whether Clifford and Cartan reflection matrices agree.
34    pub simple_reflections_match_cartan: bool,
35    /// Whether every simple reflection has determinant `-1`.
36    pub simple_reflection_determinants_are_minus_one: bool,
37    /// Computed order of the Coxeter versor action.
38    pub coxeter_versor_order: u128,
39    /// Whether that order equals the Coxeter number.
40    pub coxeter_order_matches: bool,
41    /// Grade parity of the Coxeter versor when defined.
42    pub coxeter_versor_grade_parity: Option<u128>,
43}
44
45impl WeylVersorInvariants {
46    /// Return the canonical display representation.
47    pub fn display(&self) -> String {
48        self.to_string()
49    }
50}
51
52impl fmt::Display for WeylVersorInvariants {
53    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
54        let parity = self
55            .coxeter_versor_grade_parity
56            .map_or_else(|| "none".to_string(), |p| p.to_string());
57        write!(
58            f,
59            "WeylVersorInvariants(kind={}, rank={}, weyl_group_order={}, coxeter_number={}, coxeter_versor_order={}, coxeter_order_matches={}, simple_reflections_match_cartan={}, simple_reflection_determinants_are_minus_one={}, coxeter_versor_grade_parity={})",
60            self.kind,
61            self.rank,
62            self.weyl_group_order,
63            self.coxeter_number,
64            self.coxeter_versor_order,
65            self.coxeter_order_matches,
66            self.simple_reflections_match_cartan,
67            self.simple_reflection_determinants_are_minus_one,
68            parity,
69        )
70    }
71}
72
73fn r(n: i128) -> Rational {
74    Rational::from_int(n)
75}
76
77fn rational_clifford(lattice: &IntegralForm) -> CliffordAlgebra<Rational> {
78    let metric = lattice.clifford_metric();
79    CliffordAlgebra::new(metric.q.len(), metric)
80}
81
82/// The simple-root versors `e_i` in the rational Clifford algebra of `lattice`.
83pub fn weyl_simple_root_versors(lattice: &IntegralForm) -> Vec<Multivector<Rational>> {
84    let alg = rational_clifford(lattice);
85    (0..lattice.dim()).map(|i| alg.e(i)).collect()
86}
87
88/// The Cartan reflection matrix for the `i`-th simple root, as a grade-1
89/// [`LinearMap`].
90pub fn weyl_simple_reflection_map(lattice: &IntegralForm, i: usize) -> Option<LinearMap<Rational>> {
91    let n = lattice.dim();
92    if i >= n || lattice.gram()[i][i] != 2 {
93        return None;
94    }
95    let mut cols = Vec::with_capacity(n);
96    for j in 0..n {
97        let mut col = vec![Rational::zero(); n];
98        col[j] = Rational::one();
99        col[i] = col[i].sub(&r(lattice.gram()[j][i]));
100        cols.push(col);
101    }
102    Some(LinearMap::from_columns(cols))
103}
104
105/// All Cartan simple-reflection maps for `lattice`.
106pub fn weyl_simple_reflection_maps(lattice: &IntegralForm) -> Option<Vec<LinearMap<Rational>>> {
107    (0..lattice.dim())
108        .map(|i| weyl_simple_reflection_map(lattice, i))
109        .collect()
110}
111
112fn grade1_coords(
113    alg: &CliffordAlgebra<Rational>,
114    mv: &Multivector<Rational>,
115) -> Option<Vec<Rational>> {
116    let mut out = vec![Rational::zero(); alg.dim()];
117    for (&mask, coeff) in mv.terms() {
118        if mask == 0 || mask.count_ones() != 1 {
119            return None;
120        }
121        let i = mask.trailing_zeros() as usize;
122        if i >= alg.dim() {
123            return None;
124        }
125        out[i] = coeff.clone();
126    }
127    Some(out)
128}
129
130/// The grade-1 linear map induced by the Pin twisted-adjoint action of `versor`.
131pub fn weyl_versor_action_map(
132    lattice: &IntegralForm,
133    versor: &Multivector<Rational>,
134) -> Option<LinearMap<Rational>> {
135    let alg = rational_clifford(lattice);
136    let mut cols = Vec::with_capacity(alg.dim());
137    for i in 0..alg.dim() {
138        let image = alg.twisted_sandwich(versor, &alg.e(i))?;
139        cols.push(grade1_coords(&alg, &image)?);
140    }
141    Some(LinearMap::from_columns(cols))
142}
143
144fn simple_reflections_match_cartan(lattice: &IntegralForm) -> bool {
145    let alg = rational_clifford(lattice);
146    for i in 0..lattice.dim() {
147        let Some(map) = weyl_simple_reflection_map(lattice, i) else {
148            return false;
149        };
150        let versor = alg.e(i);
151        for j in 0..lattice.dim() {
152            let Some(image) = alg.reflect(&versor, &alg.e(j)) else {
153                return false;
154            };
155            if image != map.image(&alg, j) {
156                return false;
157            }
158        }
159    }
160    true
161}
162
163fn simple_reflection_determinants_are_minus_one(lattice: &IntegralForm) -> bool {
164    let alg = rational_clifford(lattice);
165    let Some(maps) = weyl_simple_reflection_maps(lattice) else {
166        return false;
167    };
168    maps.iter().all(|f| determinant(&alg, f) == r(-1))
169}
170
171/// The Coxeter versor `e_0 e_1 ... e_{n-1}` for the simple-root order used by
172/// the lattice constructor.
173pub fn weyl_coxeter_versor(lattice: &IntegralForm) -> Option<Multivector<Rational>> {
174    if lattice
175        .gram()
176        .iter()
177        .enumerate()
178        .any(|(i, row)| row[i] != 2)
179    {
180        return None;
181    }
182    let alg = rational_clifford(lattice);
183    let mut out = alg.scalar(Rational::one());
184    for i in 0..lattice.dim() {
185        out = alg.mul(&out, &alg.e(i));
186    }
187    Some(out)
188}
189
190fn linear_map_order(map: &LinearMap<Rational>, max_order: u128) -> Option<u128> {
191    let id = LinearMap::identity(map.n());
192    let mut cur = id.clone();
193    for k in 1..=max_order {
194        cur = map.compose(&cur);
195        if cur == id {
196            return Some(k);
197        }
198    }
199    None
200}
201
202/// The order of the Coxeter versor's Pin action, bounded by `max_order`.
203pub fn weyl_coxeter_action_order(lattice: &IntegralForm, max_order: u128) -> Option<u128> {
204    let c = weyl_coxeter_versor(lattice)?;
205    let action = weyl_versor_action_map(lattice, &c)?;
206    linear_map_order(&action, max_order)
207}
208
209/// Clifford-versor report for the irreducible ADE root component `kind`.
210pub fn weyl_versor_report(kind: NiemeierComponentKind) -> Option<WeylVersorInvariants> {
211    let lattice = kind.root_lattice()?;
212    let coxeter_number = kind.coxeter_number()?;
213    let coxeter_versor = weyl_coxeter_versor(&lattice)?;
214    let coxeter_order = weyl_coxeter_action_order(&lattice, coxeter_number)?;
215    Some(WeylVersorInvariants {
216        kind,
217        rank: kind.rank(),
218        weyl_group_order: kind.weyl_group_order()?,
219        coxeter_number,
220        simple_reflections_match_cartan: simple_reflections_match_cartan(&lattice),
221        simple_reflection_determinants_are_minus_one: simple_reflection_determinants_are_minus_one(
222            &lattice,
223        ),
224        coxeter_versor_order: coxeter_order,
225        coxeter_order_matches: coxeter_order == coxeter_number,
226        coxeter_versor_grade_parity: versor_grade_parity(&coxeter_versor),
227    })
228}
229
230#[cfg(test)]
231mod tests {
232    use super::*;
233    use crate::forms::E8_WEYL_GROUP_ORDER;
234
235    #[test]
236    fn a2_simple_roots_act_as_cartan_reflections() {
237        let report = weyl_versor_report(NiemeierComponentKind::A(2)).unwrap();
238        assert_eq!(report.rank, 2);
239        assert_eq!(report.weyl_group_order, 6);
240        assert_eq!(report.coxeter_number, 3);
241        assert!(report.simple_reflections_match_cartan);
242        assert!(report.simple_reflection_determinants_are_minus_one);
243        assert_eq!(report.coxeter_versor_order, 3);
244        assert!(report.coxeter_order_matches);
245        assert_eq!(report.coxeter_versor_grade_parity, Some(0));
246    }
247
248    #[test]
249    fn display_renders_the_full_report() {
250        let report = weyl_versor_report(NiemeierComponentKind::A(2)).unwrap();
251        assert_eq!(
252            report.to_string(),
253            "WeylVersorInvariants(kind=A_2, rank=2, weyl_group_order=6, \
254             coxeter_number=3, coxeter_versor_order=3, coxeter_order_matches=true, \
255             simple_reflections_match_cartan=true, \
256             simple_reflection_determinants_are_minus_one=true, \
257             coxeter_versor_grade_parity=0)"
258        );
259        assert_eq!(report.display(), report.to_string());
260    }
261
262    #[test]
263    fn d4_report_uses_weyl_order_not_full_diagram_automorphisms() {
264        let report = weyl_versor_report(NiemeierComponentKind::D(4)).unwrap();
265        assert_eq!(report.weyl_group_order, 192);
266        assert_eq!(report.coxeter_number, 6);
267        assert_eq!(report.coxeter_versor_order, 6);
268        assert!(report.simple_reflections_match_cartan);
269    }
270
271    #[test]
272    fn e8_coxeter_versor_has_order_30() {
273        let report = weyl_versor_report(NiemeierComponentKind::E8).unwrap();
274        assert_eq!(report.weyl_group_order, E8_WEYL_GROUP_ORDER);
275        assert_eq!(report.coxeter_number, 30);
276        assert_eq!(report.coxeter_versor_order, 30);
277        assert!(report.coxeter_order_matches);
278        assert!(report.simple_reflection_determinants_are_minus_one);
279    }
280
281    /// Sweeps past the `A_2`/`D_4`/`E_8` spot checks above: `E_6`/`E_7` plus a
282    /// couple more `A_n`/`D_n` ranks, pinned against the standard Coxeter-number
283    /// formulas `h(E_6) = 12`, `h(E_7) = 18`, `h(A_n) = n+1`, `h(D_n) = 2(n-1)`.
284    #[test]
285    fn e6_e7_and_more_ade_ranks_match_standard_coxeter_numbers() {
286        let e6 = weyl_versor_report(NiemeierComponentKind::E6).unwrap();
287        assert_eq!(e6.coxeter_number, 12);
288        assert_eq!(e6.coxeter_versor_order, 12);
289        assert!(e6.coxeter_order_matches);
290        assert!(e6.simple_reflections_match_cartan);
291        assert!(e6.simple_reflection_determinants_are_minus_one);
292
293        let e7 = weyl_versor_report(NiemeierComponentKind::E7).unwrap();
294        assert_eq!(e7.coxeter_number, 18);
295        assert_eq!(e7.coxeter_versor_order, 18);
296        assert!(e7.coxeter_order_matches);
297        assert!(e7.simple_reflections_match_cartan);
298        assert!(e7.simple_reflection_determinants_are_minus_one);
299
300        for n in [3usize, 5] {
301            let report = weyl_versor_report(NiemeierComponentKind::A(n)).unwrap();
302            let h = n as u128 + 1;
303            assert_eq!(report.coxeter_number, h, "A_{n} Coxeter number");
304            assert_eq!(report.coxeter_versor_order, h);
305            assert!(report.coxeter_order_matches);
306            assert!(report.simple_reflections_match_cartan);
307            assert!(report.simple_reflection_determinants_are_minus_one);
308        }
309
310        for n in [5usize, 6] {
311            let report = weyl_versor_report(NiemeierComponentKind::D(n)).unwrap();
312            let h = 2 * (n as u128 - 1);
313            assert_eq!(report.coxeter_number, h, "D_{n} Coxeter number");
314            assert_eq!(report.coxeter_versor_order, h);
315            assert!(report.coxeter_order_matches);
316            assert!(report.simple_reflections_match_cartan);
317        }
318    }
319}