atlas_embeddings/e8/
mod.rs

1#![allow(clippy::doc_markdown)] // Allow e_i, e_j in LaTeX math blocks
2//! # Chapter 2: The E₈ Root System
3//!
4//! This chapter presents the exceptional Lie algebra E₈ through its root system:
5//! a remarkable configuration of 240 vectors in 8-dimensional Euclidean space.
6//!
7//! ## Overview
8//!
9//! E₈ is the largest and most complex of the five exceptional simple Lie algebras.
10//! Its root system—the set of 240 vectors that encode all the algebraic structure—
11//! exhibits extraordinary symmetry and mathematical beauty.
12//!
13//! **Main results**:
14//! 1. E₈ has exactly 240 roots in ℝ⁸
15//! 2. All roots have the same length (norm² = 2)
16//! 3. The roots split into 112 integer + 128 half-integer coordinates
17//! 4. E₈ is the unique simply-laced exceptional Lie algebra of rank 8
18//!
19//! ## Chapter Organization
20//!
21//! - **§2.1 Root Systems from First Principles**: Abstract theory of root systems
22//! - **§2.2 The E₈ Lattice**: Construction of the 240 roots
23//! - **§2.3 Why E₈ is Exceptional**: Uniqueness, maximality, and special properties
24//! - **§2.4 Computational Verification**: Tests as proofs of root system axioms
25//!
26//! ## Historical Context
27//!
28//! E₈ was discovered in the late 19th century through the classification of simple
29//! Lie algebras. It defied intuition: why should there be exactly 5 exceptional
30//! groups? Why does the sequence stop at E₈? The answers involve deep connections
31//! to lattice sphere packings, modular forms, and string theory.
32//!
33//! The Atlas → E₈ embedding (Chapter 3) shows that E₈'s structure is not arbitrary
34//! but emerges naturally from the stationary configuration of an action functional.
35//!
36//! ## Navigation
37//!
38//! - Previous: [Chapter 1: The Atlas](crate::atlas)
39//! - Next: [Chapter 3: Atlas → E₈ Embedding](crate::embedding)
40//! - Up: [Main Page](crate)
41//!
42//! ---
43//!
44//! # §2.1: Root Systems from First Principles
45//!
46//! We begin by developing the abstract theory of root systems, which provides the
47//! language for understanding Lie algebras geometrically.
48//!
49//! ## 2.1.1 Motivation: From Lie Algebras to Geometry
50//!
51//! A **Lie algebra** is a vector space with a bracket operation \[·,·\] satisfying
52//! antisymmetry and the Jacobi identity. While Lie algebras are algebraic objects,
53//! they can be understood geometrically through their **root systems**.
54//!
55//! **Key insight**: The structure of a semisimple Lie algebra is completely determined
56//! by its root system—a finite set of vectors in Euclidean space satisfying certain
57//! axioms. This is the **Cartan-Killing-Weyl** approach to Lie theory.
58//!
59//! ## 2.1.2 Root System Axioms
60//!
61//! **Definition 2.1.1 (Root System)**: A **root system** in ℝⁿ is a finite set
62//! Φ ⊂ ℝⁿ ∖ {0} satisfying:
63//!
64//! 1. **Span**: Φ spans ℝⁿ
65//! 2. **Symmetry**: For each α ∈ Φ, the only scalar multiples of α in Φ are ±α
66//! 3. **Reflection**: For each α ∈ Φ, the reflection s_α: ℝⁿ → ℝⁿ defined by
67//!
68//!    $$ s_\alpha(v) = v - 2\frac{\langle v, \alpha \rangle}{\langle \alpha, \alpha \rangle} \alpha $$
69//!
70//!    preserves Φ (i.e., s_α(Φ) = Φ)
71//!
72//! 4. **Integrality**: For all α, β ∈ Φ, the number
73//!
74//!    $$ \langle \beta, \alpha^\vee \rangle = 2\frac{\langle \beta, \alpha \rangle}{\langle \alpha, \alpha \rangle} $$
75//!
76//!    is an integer (where α^∨ is the **coroot** of α)
77//!
78//! **Theorem 2.1.1 (Root System Characterization)**: These axioms completely
79//! characterize which finite sets of vectors can arise as root systems of
80//! semisimple Lie algebras.
81//!
82//! ## 2.1.3 Simply-Laced Root Systems
83//!
84//! **Definition 2.1.2 (Simply-Laced)**: A root system is **simply-laced** if all
85//! roots have the same length.
86//!
87//! For simply-laced systems, we can normalize so that ⟨α,α⟩ = 2 for all α ∈ Φ.
88//! This simplifies the integrality condition to:
89//!
90//! $$ \langle \beta, \alpha \rangle \in \mathbb{Z} $$
91//!
92//! **Why simply-laced?** The Atlas naturally embeds into simply-laced root systems.
93//! The five exceptional groups split:
94//! - **Simply-laced**: E₆, E₇, E₈ (connected to Atlas)
95//! - **Not simply-laced**: G₂ (roots of length √2 and √6), F₄ (roots of length √2 and 2)
96//!
97//! ## 2.1.4 Rank and Dimension
98//!
99//! **Definition 2.1.3**: The **rank** of a root system Φ ⊂ ℝⁿ is the dimension of
100//! the span of Φ. For irreducible root systems, rank = n.
101//!
102//! **Definition 2.1.4**: A root system is **irreducible** if it cannot be partitioned
103//! into orthogonal subsets.
104//!
105//! **Theorem 2.1.2 (Classification)**: Irreducible root systems are classified by
106//! their **Dynkin diagrams**. The complete list includes:
107//! - **Classical families**: Aₙ (n ≥ 1), Bₙ (n ≥ 2), Cₙ (n ≥ 3), Dₙ (n ≥ 4)
108//! - **Exceptional types**: G₂, F₄, E₆, E₇, E₈
109//!
110//! **Observation**: The list stops at E₈. There is no E₉, E₁₀, etc. This is not
111//! arbitrary—the proof involves showing that the Dynkin diagram constraints become
112//! impossible beyond rank 8.
113//!
114//! ---
115//!
116//! # §2.2: The E₈ Lattice
117//!
118//! We now construct the E₈ root system explicitly.
119//!
120//! ## 2.2.1 The 240 Roots
121//!
122//! **Definition 2.2.1 (E₈ Root System)**: The E₈ root system Φ(E₈) consists of
123//! 240 vectors in ℝ⁸, partitioned into two types:
124//!
125//! **Type I: Integer-coordinate roots** (112 roots)
126//!
127//! All vectors of the form ±eᵢ ± eⱼ where i < j and eᵢ is the standard basis vector.
128//!
129//! Count: C(8,2) × 4 = 28 × 4 = **112 roots**
130//!
131//! **Type II: Half-integer-coordinate roots** (128 roots)
132//!
133//! All vectors with coordinates ±1/2 where an **even number** of coordinates are negative.
134//!
135//! Count: 2⁸ / 2 = 256 / 2 = **128 roots**
136//!
137//! **Total**: 112 + 128 = **240 roots** ✓
138//!
139//! ## 2.2.2 Verification of Root Axioms
140//!
141//! **Theorem 2.2.1 (E₈ is a Root System)**: The set Φ(E₈) defined above satisfies
142//! all four root system axioms.
143//!
144//! **Proof**:
145//!
146//! 1. **Span**: The 8 standard basis vectors appear in Type I, so span(Φ) = ℝ⁸. ✓
147//!
148//! 2. **Symmetry**: Each root α appears with -α (by construction), and no other
149//!    scalar multiples. ✓
150//!
151//! 3. **Reflection**: For each α ∈ Φ, the reflection s_α(v) = v - ⟨v,α⟩α (using
152//!    the normalization ⟨α,α⟩ = 2) maps Φ to itself. This is verified computationally
153//!    (see tests). ✓
154//!
155//! 4. **Integrality**: For all α, β ∈ Φ, we have ⟨α,β⟩ ∈ {-2, -1, 0, 1, 2} ⊂ ℤ.
156//!    This follows from the coordinate structure. ✓
157//!
158//! Therefore Φ(E₈) is a root system. ∎
159//!
160//! ## 2.2.3 Simply-Laced Property
161//!
162//! **Theorem 2.2.2 (E₈ is Simply-Laced)**: All roots in Φ(E₈) have norm² = 2.
163//!
164//! **Proof**:
165//!
166//! **Type I**: For α = ±eᵢ ± eⱼ, we have
167//! $$ \|\alpha\|^2 = (\pm 1)^2 + (\pm 1)^2 = 1 + 1 = 2 $$
168//!
169//! **Type II**: For α with coordinates ±1/2, we have
170//! $$ \|\alpha\|^2 = 8 \times (1/2)^2 = 8 \times 1/4 = 2 $$
171//!
172//! Thus all roots have the same length. ∎
173//!
174//! **Corollary 2.2.1**: E₈ is a simply-laced root system.
175//!
176//! ## 2.2.4 The E₈ Lattice
177//!
178//! The E₈ root system generates a lattice: the **E₈ lattice** Λ₈.
179//!
180//! **Definition 2.2.2 (E₈ Lattice)**: The E₈ lattice is
181//!
182//! $$ \Lambda_8 = \mathbb{Z}^8 \cup \left( \tfrac{1}{2} + \mathbb{Z} \right)^8_{\text{even}} $$
183//!
184//! where the subscript "even" means coordinates with even parity (even # of half-integers).
185//!
186//! **Properties**:
187//! - **Unimodular**: det(Λ₈) = 1
188//! - **Even**: All vectors have even norm² (∈ 2ℤ)
189//! - **Densest**: The E₈ lattice achieves the densest sphere packing in dimension 8
190//!
191//! **Theorem 2.2.3 (Densest Packing)**: In 8 dimensions, the E₈ lattice sphere
192//! packing has density π⁴/384 ≈ 25.4%. No denser packing exists.
193//!
194//! ---
195//!
196//! # §2.3: Why E₈ is Exceptional
197//!
198//! What makes E₈ "exceptional"? Why does it deserve special status?
199//!
200//! ## 2.3.1 Uniqueness
201//!
202//! **Theorem 2.3.1 (E₈ Maximality)**: E₈ is the unique simply-laced exceptional
203//! root system of rank 8. There is no E₉.
204//!
205//! **Sketch**: The Dynkin diagram of Eₙ (for n ≤ 8) has a specific "tadpole" shape.
206//! Attempting to extend to E₉ violates the graph constraints that ensure the
207//! associated matrix is positive definite. ∎
208//!
209//! ## 2.3.2 No Higher Analogues
210//!
211//! Unlike the classical families (Aₙ, Bₙ, Cₙ, Dₙ) which extend to arbitrary rank,
212//! the exceptional series terminates:
213//!
214//! - E₆ has 72 roots
215//! - E₇ has 126 roots
216//! - E₈ has 240 roots
217//! - E₉ does not exist
218//!
219//! **Why?** The dimension grows too fast. The Dynkin diagram method fails to
220//! produce a valid root system beyond rank 8.
221//!
222//! ## 2.3.3 Connections to Physics
223//!
224//! E₈ appears throughout theoretical physics:
225//!
226//! - **String theory**: E₈ × E₈ heterotic string theory in 10 dimensions
227//! - **Gauge theory**: E₈ gauge symmetry in grand unified theories
228//! - **Conformal field theory**: E₈ affine Lie algebra at level 1
229//! - **M-theory**: E₈ appears in certain compactifications
230//!
231//! The Atlas → E₈ connection suggests these physical appearances may trace back
232//! to fundamental informational/action principles.
233//!
234//! ## 2.3.4 The 240 Number
235//!
236//! Why 240? This factorizes as:
237//!
238//! $$ 240 = 2^4 \times 3 \times 5 = 16 \times 15 $$
239//!
240//! **Significance**:
241//! - 240 = 2 × 120 (120 sign classes, each with ±r)
242//! - 240 = 112 + 128 (integer + half-integer split)
243//! - 240 relates to the partition of 96 Atlas vertices into E₈
244//!
245//! The number 240 is not arbitrary but emerges from the combinatorics of
246//! 8-dimensional lattice geometry.
247//!
248//! ---
249//!
250//! # Implementation
251//!
252//! Below we provide the computational construction of E₈, verifying all properties
253//! through exact arithmetic.
254//!
255//! ## Examples
256//!
257//! ```
258//! use atlas_embeddings::e8::E8RootSystem;
259//!
260//! let e8 = E8RootSystem::new();
261//! assert_eq!(e8.num_roots(), 240);
262//!
263//! // Check root properties
264//! for i in 0..e8.num_roots() {
265//!     let root = e8.get_root(i);
266//!     assert!(root.is_root()); // norm² = 2
267//! }
268//! ```
269
270use crate::arithmetic::{HalfInteger, Rational, Vector8};
271use std::collections::HashMap;
272
273/// Total number of E₈ roots
274pub const E8_ROOT_COUNT: usize = 240;
275
276/// Number of integer-coordinate roots
277pub const E8_INTEGER_ROOT_COUNT: usize = 112;
278
279/// Number of half-integer-coordinate roots
280pub const E8_HALF_INTEGER_ROOT_COUNT: usize = 128;
281
282/// E₈ Root System
283///
284/// Encapsulates all 240 roots of E₈ with exact arithmetic.
285///
286/// # Invariants
287///
288/// - Exactly 240 roots total
289/// - Exactly 112 integer roots
290/// - Exactly 128 half-integer roots
291/// - Every root has norm² = 2
292/// - Every root r has negation -r in the system
293#[derive(Debug, Clone)]
294pub struct E8RootSystem {
295    /// All 240 roots
296    roots: Vec<Vector8>,
297
298    /// Map from root to its index (for fast lookup)
299    root_index: HashMap<Vector8, usize>,
300
301    /// Negation table: `negation_table[i]` = index of `-roots[i]`
302    negation_table: Vec<usize>,
303}
304
305impl E8RootSystem {
306    /// Create new E₈ root system
307    ///
308    /// Generates all 240 roots and verifies invariants.
309    #[must_use]
310    pub fn new() -> Self {
311        let roots = Self::generate_all_roots();
312        let root_index = Self::create_root_index(&roots);
313        let negation_table = Self::compute_negation_table(&roots, &root_index);
314
315        let system = Self { roots, root_index, negation_table };
316
317        system.verify_invariants();
318        system
319    }
320
321    /// Generate all 240 E₈ roots
322    fn generate_all_roots() -> Vec<Vector8> {
323        let mut roots = Vec::with_capacity(E8_ROOT_COUNT);
324
325        // Generate 112 integer roots: ±eᵢ ± eⱼ for i < j
326        roots.extend(Self::generate_integer_roots());
327
328        // Generate 128 half-integer roots: all coordinates ±1/2, even # of minus signs
329        roots.extend(Self::generate_half_integer_roots());
330
331        assert_eq!(roots.len(), E8_ROOT_COUNT, "Must generate exactly 240 roots");
332        roots
333    }
334
335    /// Generate 112 integer-coordinate roots
336    ///
337    /// These are ±eᵢ ± eⱼ for all 0 ≤ i < j < 8 and all sign combinations.
338    /// Total: C(8,2) × 4 = 28 × 4 = 112
339    fn generate_integer_roots() -> Vec<Vector8> {
340        let mut roots = Vec::with_capacity(E8_INTEGER_ROOT_COUNT);
341
342        let zero = HalfInteger::from_integer(0);
343
344        // For each pair (i, j) with i < j
345        for i in 0..8 {
346            for j in (i + 1)..8 {
347                // For each combination of signs
348                for &sign_i in &[1, -1] {
349                    for &sign_j in &[1, -1] {
350                        let mut coords = [zero; 8];
351                        coords[i] = HalfInteger::from_integer(sign_i);
352                        coords[j] = HalfInteger::from_integer(sign_j);
353                        roots.push(Vector8::new(coords));
354                    }
355                }
356            }
357        }
358
359        assert_eq!(roots.len(), E8_INTEGER_ROOT_COUNT);
360        roots
361    }
362
363    /// Generate 128 half-integer-coordinate roots
364    ///
365    /// All coordinates are ±1/2, with an even number of minus signs.
366    /// Total: 2⁸ / 2 = 256 / 2 = 128
367    fn generate_half_integer_roots() -> Vec<Vector8> {
368        let mut roots = Vec::with_capacity(E8_HALF_INTEGER_ROOT_COUNT);
369
370        let half = HalfInteger::new(1); // 1/2
371
372        // Iterate through all 256 sign patterns
373        for pattern in 0..256_u16 {
374            let mut coords = [half; 8];
375            let mut num_negatives = 0;
376
377            // Set signs based on bit pattern
378            for (i, coord) in coords.iter_mut().enumerate() {
379                if (pattern >> i) & 1 == 1 {
380                    *coord = -half;
381                    num_negatives += 1;
382                }
383            }
384
385            // Only include if even number of minus signs
386            if num_negatives % 2 == 0 {
387                roots.push(Vector8::new(coords));
388            }
389        }
390
391        assert_eq!(roots.len(), E8_HALF_INTEGER_ROOT_COUNT);
392        roots
393    }
394
395    /// Create index mapping roots to their positions
396    fn create_root_index(roots: &[Vector8]) -> HashMap<Vector8, usize> {
397        roots.iter().enumerate().map(|(i, r)| (*r, i)).collect()
398    }
399
400    /// Compute negation table
401    fn compute_negation_table(
402        roots: &[Vector8],
403        root_index: &HashMap<Vector8, usize>,
404    ) -> Vec<usize> {
405        let mut table = vec![0; roots.len()];
406
407        for (i, root) in roots.iter().enumerate() {
408            let neg_root = -(*root);
409            if let Some(&neg_idx) = root_index.get(&neg_root) {
410                table[i] = neg_idx;
411            } else {
412                panic!("Negation of root {i} not found in system");
413            }
414        }
415
416        table
417    }
418
419    /// Verify all E₈ root system invariants
420    fn verify_invariants(&self) {
421        // Check counts
422        assert_eq!(self.roots.len(), E8_ROOT_COUNT, "Must have exactly 240 roots");
423
424        // Count integer vs half-integer roots
425        let integer_count = self.roots.iter().filter(|r| Self::is_integer_root(r)).count();
426        let half_int_count = self.roots.iter().filter(|r| Self::is_half_integer_root(r)).count();
427
428        assert_eq!(integer_count, E8_INTEGER_ROOT_COUNT, "Must have 112 integer roots");
429        assert_eq!(half_int_count, E8_HALF_INTEGER_ROOT_COUNT, "Must have 128 half-integer roots");
430
431        // Check every root has norm² = 2
432        for (i, root) in self.roots.iter().enumerate() {
433            assert!(root.is_root(), "Root {i} must have norm² = 2");
434        }
435
436        // Check negation table is correct
437        for (i, &neg_idx) in self.negation_table.iter().enumerate() {
438            assert_ne!(i, neg_idx, "Root {i} cannot be its own negative");
439            let expected_neg = -self.roots[i];
440            assert_eq!(self.roots[neg_idx], expected_neg, "Negation table incorrect at {i}");
441        }
442    }
443
444    /// Check if root has all integer coordinates
445    fn is_integer_root(root: &Vector8) -> bool {
446        root.coords().iter().all(|c| c.is_integer())
447    }
448
449    /// Check if root has all half-integer coordinates (all ±1/2)
450    fn is_half_integer_root(root: &Vector8) -> bool {
451        root.coords().iter().all(|c| !c.is_integer() && c.numerator().abs() == 1)
452    }
453
454    /// Get total number of roots
455    #[must_use]
456    pub const fn num_roots(&self) -> usize {
457        E8_ROOT_COUNT
458    }
459
460    /// Get root by index
461    ///
462    /// # Panics
463    ///
464    /// Panics if index is out of bounds
465    #[must_use]
466    pub fn get_root(&self, index: usize) -> &Vector8 {
467        &self.roots[index]
468    }
469
470    /// Get index of negation of a root
471    ///
472    /// # Panics
473    ///
474    /// Panics if index is out of bounds
475    #[must_use]
476    pub fn get_negation(&self, index: usize) -> usize {
477        self.negation_table[index]
478    }
479
480    /// Get sign class representative (smaller of {r, -r})
481    #[must_use]
482    pub fn sign_class_representative(&self, index: usize) -> usize {
483        index.min(self.negation_table[index])
484    }
485
486    /// Count number of distinct sign classes used by a set of root indices
487    #[must_use]
488    pub fn count_sign_classes(&self, indices: &[usize]) -> usize {
489        use std::collections::HashSet;
490        indices
491            .iter()
492            .map(|&i| self.sign_class_representative(i))
493            .collect::<HashSet<_>>()
494            .len()
495    }
496
497    /// Get all roots as a slice
498    #[must_use]
499    pub fn roots(&self) -> &[Vector8] {
500        &self.roots
501    }
502
503    /// Compute inner product between two roots
504    #[must_use]
505    pub fn inner_product(&self, i: usize, j: usize) -> Rational {
506        self.roots[i].inner_product(&self.roots[j])
507    }
508
509    /// Check if two roots are negatives of each other
510    ///
511    /// Returns `true` if root j is the negative of root i
512    #[must_use]
513    pub fn are_negatives(&self, i: usize, j: usize) -> bool {
514        self.negation_table[i] == j
515    }
516
517    /// Find index of a given root vector
518    ///
519    /// Returns `None` if root is not in the system.
520    #[must_use]
521    pub fn find_root(&self, root: &Vector8) -> Option<usize> {
522        self.root_index.get(root).copied()
523    }
524
525    /// Get the 8 simple roots of E₈
526    ///
527    /// # Simple Roots
528    ///
529    /// The **simple roots** form a basis for the root system, with special properties:
530    /// - Every positive root is a non-negative integer linear combination of simple roots
531    /// - They form a linearly independent set spanning the root space
532    /// - The Weyl group is generated by reflections through these roots
533    ///
534    /// For E₈, we use the standard simple root basis:
535    /// - α₁ = e₁ - e₂ = (1, -1, 0, 0, 0, 0, 0, 0)
536    /// - α₂ = e₂ - e₃ = (0, 1, -1, 0, 0, 0, 0, 0)
537    /// - α₃ = e₃ - e₄ = (0, 0, 1, -1, 0, 0, 0, 0)
538    /// - α₄ = e₄ - e₅ = (0, 0, 0, 1, -1, 0, 0, 0)
539    /// - α₅ = e₅ - e₆ = (0, 0, 0, 0, 1, -1, 0, 0)
540    /// - α₆ = e₆ - e₇ = (0, 0, 0, 0, 0, 1, -1, 0)
541    /// - α₇ = e₇ + e₈ = (0, 0, 0, 0, 0, 0, 1, 1)
542    /// - α₈ = ½(-1, -1, -1, -1, -1, -1, -1, -1)
543    ///
544    /// This basis encodes the E₈ Dynkin diagram structure:
545    /// - Roots α₁ through α₇ form an A₇ chain
546    /// - Root α₈ connects to α₇, creating the E₈ branching
547    ///
548    /// # Returns
549    ///
550    /// Array of 8 simple roots with norm² = 2
551    #[must_use]
552    #[allow(clippy::large_stack_arrays)] // Mathematical constant: 8 simple roots
553    pub const fn simple_roots() -> [Vector8; 8] {
554        let zero = HalfInteger::from_integer(0);
555        let one = HalfInteger::from_integer(1);
556        let neg_one = HalfInteger::from_integer(-1);
557        let neg_half = HalfInteger::new(-1); // -1/2
558
559        [
560            // α₁ = e₁ - e₂
561            Vector8::new([one, neg_one, zero, zero, zero, zero, zero, zero]),
562            // α₂ = e₂ - e₃
563            Vector8::new([zero, one, neg_one, zero, zero, zero, zero, zero]),
564            // α₃ = e₃ - e₄
565            Vector8::new([zero, zero, one, neg_one, zero, zero, zero, zero]),
566            // α₄ = e₄ - e₅
567            Vector8::new([zero, zero, zero, one, neg_one, zero, zero, zero]),
568            // α₅ = e₅ - e₆
569            Vector8::new([zero, zero, zero, zero, one, neg_one, zero, zero]),
570            // α₆ = e₆ - e₇
571            Vector8::new([zero, zero, zero, zero, zero, one, neg_one, zero]),
572            // α₇ = e₇ + e₈
573            Vector8::new([zero, zero, zero, zero, zero, zero, one, one]),
574            // α₈ = ½(-1, -1, -1, -1, -1, -1, -1, -1)
575            Vector8::new([
576                neg_half, neg_half, neg_half, neg_half, neg_half, neg_half, neg_half, neg_half,
577            ]),
578        ]
579    }
580}
581
582impl Default for E8RootSystem {
583    fn default() -> Self {
584        Self::new()
585    }
586}
587
588//
589// # §2.4: Computational Verification
590//
591// The tests below serve as **computational proofs** of the theorems stated above.
592// Each test verifies a root system axiom or property through exhaustive computation.
593
594#[cfg(test)]
595mod tests {
596    use super::*;
597
598    /// **Test: Theorem 2.2.1 (E₈ Root Count)**
599    ///
600    /// Verifies that E₈ has exactly 240 roots.
601    ///
602    /// **Method**: Generate all roots and count.
603    ///
604    /// **Proves**: The construction algorithm produces the correct number of roots.
605    #[test]
606    fn test_e8_generation() {
607        let e8 = E8RootSystem::new();
608        assert_eq!(e8.num_roots(), 240);
609    }
610
611    /// **Test: Type I and Type II Root Counts**
612    ///
613    /// Verifies that E₈ has exactly 112 integer roots and 128 half-integer roots.
614    ///
615    /// **Method**: Count roots by coordinate type.
616    ///
617    /// **Proves**: The partition 240 = 112 + 128 is correct.
618    #[test]
619    fn test_root_counts() {
620        let e8 = E8RootSystem::new();
621
622        let integer_count = e8.roots().iter().filter(|r| E8RootSystem::is_integer_root(r)).count();
623        let half_int_count =
624            e8.roots().iter().filter(|r| E8RootSystem::is_half_integer_root(r)).count();
625
626        assert_eq!(integer_count, 112);
627        assert_eq!(half_int_count, 128);
628    }
629
630    /// **Test: Theorem 2.2.2 (Simply-Laced Property)**
631    ///
632    /// Verifies that all E₈ roots have norm² = 2.
633    ///
634    /// **Method**: Check ⟨α,α⟩ = 2 for all 240 roots.
635    ///
636    /// **Proves**: E₈ is simply-laced (all roots have equal length).
637    #[test]
638    fn test_all_roots_have_norm_2() {
639        let e8 = E8RootSystem::new();
640
641        for root in e8.roots() {
642            assert!(root.is_root(), "All E₈ roots must have norm² = 2");
643        }
644    }
645
646    /// **Test: Root System Axiom 2 (Symmetry)**
647    ///
648    /// Verifies that for each root α, the set Φ contains -α and no other scalar multiples.
649    ///
650    /// **Method**: Check negation table is well-defined and involutive.
651    ///
652    /// **Proves**: The symmetry axiom holds: each root has exactly ±α in Φ.
653    #[test]
654    fn test_negation_table() {
655        let e8 = E8RootSystem::new();
656
657        for i in 0..e8.num_roots() {
658            let neg_idx = e8.get_negation(i);
659
660            // Check negation is different
661            assert_ne!(i, neg_idx);
662
663            // Check double negation is identity
664            assert_eq!(e8.get_negation(neg_idx), i);
665
666            // Check actual negation
667            let root = e8.get_root(i);
668            let neg_root = e8.get_root(neg_idx);
669            assert_eq!(*neg_root, -(*root));
670        }
671    }
672
673    /// **Test: Sign Class Structure**
674    ///
675    /// Verifies that the 240 roots partition into exactly 120 sign classes.
676    ///
677    /// **Method**: Count distinct sign class representatives.
678    ///
679    /// **Proves**: The pairing {α, -α} covers all roots exactly once.
680    #[test]
681    fn test_sign_classes() {
682        let e8 = E8RootSystem::new();
683
684        // Should have 120 sign classes (240 roots / 2)
685        let all_indices: Vec<usize> = (0..240).collect();
686        assert_eq!(e8.count_sign_classes(&all_indices), 120);
687    }
688
689    /// **Test: Integer Root Example**
690    ///
691    /// Verifies that a specific integer root (1,1,0,0,0,0,0,0) ∈ Φ(E₈).
692    ///
693    /// **Method**: Construct the root and check membership.
694    ///
695    /// **Proves**: Type I roots are generated correctly.
696    #[test]
697    fn test_integer_root_example() {
698        let e8 = E8RootSystem::new();
699
700        // Find a root like (1, 1, 0, 0, 0, 0, 0, 0)
701        let target = Vector8::new([
702            HalfInteger::from_integer(1),
703            HalfInteger::from_integer(1),
704            HalfInteger::from_integer(0),
705            HalfInteger::from_integer(0),
706            HalfInteger::from_integer(0),
707            HalfInteger::from_integer(0),
708            HalfInteger::from_integer(0),
709            HalfInteger::from_integer(0),
710        ]);
711
712        assert!(e8.roots().contains(&target), "Should contain (1,1,0,0,0,0,0,0)");
713        assert!(target.is_root());
714    }
715
716    /// **Test: Half-Integer Root Example**
717    ///
718    /// Verifies that the all-positive half-integer root (1/2,...,1/2) ∈ Φ(E₈).
719    ///
720    /// **Method**: Construct the root with 8 coordinates of +1/2.
721    ///
722    /// **Proves**: Type II roots are generated correctly, including the even parity constraint.
723    #[test]
724    fn test_half_integer_root_example() {
725        let e8 = E8RootSystem::new();
726
727        // All +1/2 coordinates (0 negatives = even)
728        let target = Vector8::new([HalfInteger::new(1); 8]);
729
730        assert!(e8.roots().contains(&target), "Should contain (1/2, 1/2, ..., 1/2)");
731        assert!(target.is_root());
732    }
733
734    /// **Test: Root System Axiom 4 (Integrality)**
735    ///
736    /// Verifies that inner products ⟨α,β⟩ are integers for all roots α, β ∈ Φ.
737    ///
738    /// **Method**: Check self-inner-products and negation inner products.
739    ///
740    /// **Proves**: The integrality condition holds (⟨α,β⟩ ∈ ℤ for all α,β).
741    #[test]
742    fn test_inner_products() {
743        let e8 = E8RootSystem::new();
744
745        // Self inner product = 2 (from simply-laced property)
746        for i in 0..e8.num_roots() {
747            assert_eq!(e8.inner_product(i, i), Rational::new(2, 1));
748        }
749
750        // Inner product with negation = -2
751        for i in 0..e8.num_roots() {
752            let neg_i = e8.get_negation(i);
753            assert_eq!(e8.inner_product(i, neg_i), Rational::new(-2, 1));
754        }
755    }
756
757    /// **Test: Simple Roots are Normalized**
758    ///
759    /// Verifies that all 8 simple roots have norm² = 2.
760    ///
761    /// **Method**: Compute ⟨αᵢ,αᵢ⟩ for each simple root αᵢ.
762    ///
763    /// **Proves**: The simple root basis consists of normalized roots (all same length).
764    #[test]
765    fn test_simple_roots_normalized() {
766        let simple_roots = E8RootSystem::simple_roots();
767        let norm_squared_2 = Rational::from_integer(2);
768
769        for (i, root) in simple_roots.iter().enumerate() {
770            let norm_sq = root.inner_product(root);
771            assert_eq!(
772                norm_sq,
773                norm_squared_2,
774                "Simple root α{} must have norm² = 2, got {}",
775                i + 1,
776                norm_sq
777            );
778        }
779    }
780
781    /// **Test: Simple Roots are Linearly Independent**
782    ///
783    /// Verifies that the 8 simple roots form a basis (linearly independent).
784    ///
785    /// **Method**: Check that the Gram matrix (inner products) is non-degenerate.
786    ///
787    /// **Proves**: The simple roots span the 8-dimensional root space.
788    #[test]
789    fn test_simple_roots_independent() {
790        let simple_roots = E8RootSystem::simple_roots();
791
792        // Compute Gram matrix G[i][j] = ⟨αᵢ,αⱼ⟩
793        let mut gram = [[Rational::from_integer(0); 8]; 8];
794        for i in 0..8 {
795            for j in 0..8 {
796                gram[i][j] = simple_roots[i].inner_product(&simple_roots[j]);
797            }
798        }
799
800        // Verify diagonal entries = 2 (normalized)
801        for (i, row) in gram.iter().enumerate() {
802            assert_eq!(row[i], Rational::from_integer(2), "Diagonal entry G[{i}][{i}] should be 2");
803        }
804
805        // Verify off-diagonal entries ≤ 0 (simple roots condition)
806        for (i, row) in gram.iter().enumerate() {
807            for (j, &entry) in row.iter().enumerate() {
808                if i != j {
809                    assert!(
810                        entry <= Rational::from_integer(0),
811                        "Off-diagonal G[{i}][{j}] = {entry} should be ≤ 0"
812                    );
813                }
814            }
815        }
816    }
817
818    /// **Test: Simple Roots are in Root System**
819    ///
820    /// Verifies that all 8 simple roots are contained in the full E₈ root system.
821    ///
822    /// **Method**: Check that each simple root appears in the 240 roots.
823    ///
824    /// **Proves**: Simple roots are actual E₈ roots, not just an abstract basis.
825    #[test]
826    fn test_simple_roots_in_system() {
827        let e8 = E8RootSystem::new();
828        let simple_roots = E8RootSystem::simple_roots();
829
830        for (i, root) in simple_roots.iter().enumerate() {
831            let found = e8.find_root(root);
832            assert!(
833                found.is_some(),
834                "Simple root α{} = {:?} not found in E₈ root system",
835                i + 1,
836                root
837            );
838        }
839    }
840}