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}