Skip to main content

lie_groups/
root_systems.rs

1//! Root Systems for Semisimple Lie Algebras
2//!
3//! Implements the theory of root systems, which classify semisimple Lie algebras
4//! via Cartan-Killing classification. Root systems encode the structure of the
5//! Lie bracket via the adjoint representation on the Cartan subalgebra.
6//!
7//! # Mathematical Background
8//!
9//! For a semisimple Lie algebra 𝔤, choose a Cartan subalgebra 𝔥 (maximal abelian).
10//! The adjoint representation ad: 𝔤 → End(𝔤) diagonalizes on 𝔥, giving eigenvalues
11//! called **roots**:
12//!
13//! ```text
14//! [H, X_α] = α(H) X_α    for all H ∈ 𝔥
15//! ```
16//!
17//! The set of roots Φ ⊂ 𝔥* forms a **root system**, satisfying:
18//! 1. Φ is finite, spans 𝔥*, and 0 ∉ Φ
19//! 2. If α ∈ Φ, then -α ∈ Φ and no other scalar multiples
20//! 3. Φ is closed under Weyl reflections: `s_α(β) = β - 2⟨β,α⟩/⟨α,α⟩ · α`
21//! 4. ⟨β,α⟩ ∈ ℤ for all α,β ∈ Φ (Cartan integers)
22//!
23//! # Cartan Classification
24//!
25//! | Type | Rank | Group | Dimension | Example |
26//! |------|------|-------|-----------|---------|
27//! | Aₙ   | n    | SU(n+1) | n(n+2)  | A₁ = SU(2) |
28//! | Bₙ   | n    | SO(2n+1) | n(2n+1) | B₂ = SO(5) |
29//! | Cₙ   | n    | Sp(2n)  | n(2n+1) | C₂ = Sp(4) |
30//! | Dₙ   | n    | SO(2n)  | n(2n-1) | D₃ = SO(6) |
31//! | E₆   | 6    | E₆      | 78      | Exceptional |
32//! | E₇   | 7    | E₇      | 133     | Exceptional |
33//! | E₈   | 8    | E₈      | 248     | Exceptional |
34//! | F₄   | 4    | F₄      | 52      | Exceptional |
35//! | G₂   | 2    | G₂      | 14      | Exceptional |
36//!
37//! # References
38//!
39//! - Hall, *Lie Groups, Lie Algebras, and Representations*, Chapter 7
40//! - Humphreys, *Introduction to Lie Algebras and Representation Theory*, Ch. II
41//! - Fulton & Harris, *Representation Theory*, Lecture 21
42
43use ndarray::Array2;
44use num_complex::Complex64;
45use std::collections::HashSet;
46use std::fmt::{self, Write};
47
48/// A root in the dual space of a Cartan subalgebra.
49///
50/// Mathematically: α ∈ 𝔥* represented as a vector in ℝⁿ (rank n).
51/// For SU(n+1) (type Aₙ), roots are differences of standard basis vectors: eᵢ - eⱼ.
52///
53/// # Example
54///
55/// ```
56/// use lie_groups::Root;
57///
58/// // SU(3) root: e₁ - e₂
59/// let alpha = Root::new(vec![1.0, -1.0, 0.0]);
60/// assert_eq!(alpha.rank(), 3);
61/// assert!((alpha.norm_squared() - 2.0).abs() < 1e-10);
62/// ```
63#[derive(Debug, Clone, PartialEq)]
64pub struct Root {
65    /// Coordinates in ℝⁿ (n = rank of Lie algebra)
66    pub(crate) coords: Vec<f64>,
67}
68
69impl Root {
70    /// Create a new root from coordinates.
71    #[must_use]
72    pub fn new(coords: Vec<f64>) -> Self {
73        Self { coords }
74    }
75
76    /// Returns the coordinate vector of this root.
77    #[must_use]
78    pub fn coords(&self) -> &[f64] {
79        &self.coords
80    }
81
82    /// Rank of the Lie algebra (dimension of Cartan subalgebra).
83    #[must_use]
84    pub fn rank(&self) -> usize {
85        self.coords.len()
86    }
87
88    /// Inner product ⟨α, β⟩ (standard Euclidean).
89    #[must_use]
90    pub fn inner_product(&self, other: &Root) -> f64 {
91        assert_eq!(self.rank(), other.rank());
92        self.coords
93            .iter()
94            .zip(&other.coords)
95            .map(|(a, b)| a * b)
96            .sum()
97    }
98
99    /// Squared norm ⟨α, α⟩.
100    #[must_use]
101    pub fn norm_squared(&self) -> f64 {
102        self.inner_product(self)
103    }
104
105    /// Weyl reflection `s_α(β)` = β - 2⟨β,α⟩/⟨α,α⟩ · α.
106    #[must_use]
107    pub fn reflect(&self, beta: &Root) -> Root {
108        let factor = 2.0 * self.inner_product(beta) / self.norm_squared();
109        Root::new(
110            beta.coords
111                .iter()
112                .zip(&self.coords)
113                .map(|(b, a)| b - factor * a)
114                .collect(),
115        )
116    }
117
118    /// Cartan integer ⟨β, α^∨⟩ = 2⟨β,α⟩/⟨α,α⟩.
119    #[inline]
120    #[must_use]
121    pub fn cartan_integer(&self, beta: &Root) -> i32 {
122        let value = 2.0 * self.inner_product(beta) / self.norm_squared();
123        value.round() as i32
124    }
125
126    /// Check if this root is positive (first nonzero coordinate is positive).
127    #[must_use]
128    pub fn is_positive(&self) -> bool {
129        for &c in &self.coords {
130            if c.abs() > 1e-10 {
131                return c > 0.0;
132            }
133        }
134        false // Zero root (shouldn't happen)
135    }
136
137    /// Negate this root.
138    #[must_use]
139    pub fn negate(&self) -> Root {
140        Root::new(self.coords.iter().map(|c| -c).collect())
141    }
142}
143
144impl fmt::Display for Root {
145    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
146        write!(f, "(")?;
147        for (i, c) in self.coords.iter().enumerate() {
148            if i > 0 {
149                write!(f, ", ")?;
150            }
151            write!(f, "{:.2}", c)?;
152        }
153        write!(f, ")")
154    }
155}
156
157/// A root system for a semisimple Lie algebra.
158///
159/// Encodes the structure of the Lie bracket through roots and Weyl reflections.
160///
161/// # Example
162///
163/// ```
164/// use lie_groups::RootSystem;
165///
166/// // SU(3) = type A₂
167/// let su3 = RootSystem::type_a(2);
168/// assert_eq!(su3.rank(), 2);
169/// assert_eq!(su3.num_roots(), 6); // 3² - 1 = 8, but we store 6 roots
170/// assert_eq!(su3.num_positive_roots(), 3);
171/// ```
172#[derive(Debug, Clone)]
173pub struct RootSystem {
174    /// Rank of the Lie algebra (dimension of Cartan subalgebra)
175    rank: usize,
176
177    /// All roots (positive and negative)
178    roots: Vec<Root>,
179
180    /// Simple roots (basis for positive roots)
181    simple_roots: Vec<Root>,
182
183    /// Cartan matrix `A_ij` = ⟨`α_j`, `α_i`^∨⟩
184    cartan_matrix: Vec<Vec<i32>>,
185}
186
187impl RootSystem {
188    /// Create a type Aₙ root system (SU(n+1)).
189    ///
190    /// For SU(n+1), the rank is n, and roots are differences eᵢ - eⱼ for i ≠ j.
191    /// Simple roots: αᵢ = eᵢ - eᵢ₊₁ for i = 1, ..., n.
192    ///
193    /// # Example
194    ///
195    /// ```
196    /// use lie_groups::RootSystem;
197    ///
198    /// // SU(2) = A₁
199    /// let su2 = RootSystem::type_a(1);
200    /// assert_eq!(su2.rank(), 1);
201    /// assert_eq!(su2.num_roots(), 2); // ±α
202    ///
203    /// // SU(3) = A₂
204    /// let su3 = RootSystem::type_a(2);
205    /// assert_eq!(su3.rank(), 2);
206    /// assert_eq!(su3.simple_roots().len(), 2);
207    /// ```
208    #[must_use]
209    pub fn type_a(n: usize) -> Self {
210        assert!(n >= 1, "Type A_n requires n >= 1");
211
212        let rank = n;
213
214        // Simple roots: αᵢ = eᵢ - eᵢ₊₁ for i = 1, ..., n
215        // Embedded in ℝⁿ⁺¹ with constraint Σxᵢ = 0
216        let mut simple_roots = Vec::new();
217        for i in 0..n {
218            let mut coords = vec![0.0; n + 1];
219            coords[i] = 1.0;
220            coords[i + 1] = -1.0;
221            simple_roots.push(Root::new(coords));
222        }
223
224        // Generate all positive roots
225        let mut positive_roots = Vec::new();
226        for i in 0..=n {
227            for j in (i + 1)..=n {
228                let mut coords = vec![0.0; n + 1];
229                coords[i] = 1.0;
230                coords[j] = -1.0;
231                positive_roots.push(Root::new(coords));
232            }
233        }
234
235        // All roots = positive + negative
236        let mut roots = positive_roots.clone();
237        for root in &positive_roots {
238            roots.push(root.negate());
239        }
240
241        // Compute Cartan matrix: A_ij = ⟨α_j, α_i^∨⟩
242        let cartan_matrix = simple_roots
243            .iter()
244            .map(|alpha_i| {
245                simple_roots
246                    .iter()
247                    .map(|alpha_j| alpha_i.cartan_integer(alpha_j))
248                    .collect()
249            })
250            .collect();
251
252        Self {
253            rank,
254            roots,
255            simple_roots,
256            cartan_matrix,
257        }
258    }
259
260    /// Rank of the Lie algebra.
261    #[must_use]
262    pub fn rank(&self) -> usize {
263        self.rank
264    }
265
266    /// All roots (positive and negative).
267    #[must_use]
268    pub fn roots(&self) -> &[Root] {
269        &self.roots
270    }
271
272    /// Simple roots (basis for root system).
273    #[must_use]
274    pub fn simple_roots(&self) -> &[Root] {
275        &self.simple_roots
276    }
277
278    /// Cartan matrix `A_ij` = ⟨`α_j`, `α_i`^∨⟩.
279    #[must_use]
280    pub fn cartan_matrix(&self) -> &[Vec<i32>] {
281        &self.cartan_matrix
282    }
283
284    /// Number of roots.
285    #[must_use]
286    pub fn num_roots(&self) -> usize {
287        self.roots.len()
288    }
289
290    /// Number of positive roots.
291    #[must_use]
292    pub fn num_positive_roots(&self) -> usize {
293        self.roots.iter().filter(|r| r.is_positive()).count()
294    }
295
296    /// Get all positive roots.
297    #[must_use]
298    pub fn positive_roots(&self) -> Vec<Root> {
299        self.roots
300            .iter()
301            .filter(|r| r.is_positive())
302            .cloned()
303            .collect()
304    }
305
306    /// Get the highest root (longest root in the positive system).
307    ///
308    /// The highest root θ is the unique positive root with maximal height
309    /// (sum of coefficients when expanded in simple roots). It's also the
310    /// longest root in the root system for simply-laced types like Aₙ.
311    ///
312    /// For type `A_n` (SU(n+1)), the highest root is θ = α₁ + α₂ + ... + αₙ.
313    ///
314    /// # Example
315    ///
316    /// ```
317    /// use lie_groups::RootSystem;
318    ///
319    /// let su3 = RootSystem::type_a(2);
320    /// let theta = su3.highest_root();
321    ///
322    /// // For SU(3), θ = α₁ + α₂ = (1, 0, -1)
323    /// ```
324    #[must_use]
325    pub fn highest_root(&self) -> Root {
326        // For type A_n, highest root is α₁ + α₂ + ... + αₙ
327        // In coordinates: e₁ - eₙ₊₁ = (1, 0, ..., 0, -1)
328
329        let n = self.rank;
330
331        // Sum of simple roots
332        let mut coords = vec![0.0; n + 1];
333
334        for simple_root in &self.simple_roots {
335            for (i, &coord) in simple_root.coords.iter().enumerate() {
336                coords[i] += coord;
337            }
338        }
339
340        Root::new(coords)
341    }
342
343    /// Check if a root is in the system.
344    #[must_use]
345    pub fn contains_root(&self, root: &Root) -> bool {
346        self.roots.iter().any(|r| {
347            r.coords.len() == root.coords.len()
348                && r.coords
349                    .iter()
350                    .zip(root.coords.iter())
351                    .all(|(a, b)| (a - b).abs() < 1e-10)
352        })
353    }
354
355    /// Weyl reflection `s_α` for a root α.
356    #[must_use]
357    pub fn weyl_reflection(&self, alpha: &Root, beta: &Root) -> Root {
358        alpha.reflect(beta)
359    }
360
361    /// Generate the Weyl group orbit of a weight under simple reflections.
362    ///
363    /// The Weyl group is generated by reflections in simple roots.
364    /// For type Aₙ, |W| = (n+1)!
365    #[must_use]
366    pub fn weyl_orbit(&self, weight: &Root) -> Vec<Root> {
367        let mut orbit = vec![weight.clone()];
368        let mut seen = HashSet::new();
369        seen.insert(format!("{:?}", weight.coords));
370
371        let mut queue = vec![weight.clone()];
372
373        while let Some(current) = queue.pop() {
374            for simple_root in &self.simple_roots {
375                let reflected = simple_root.reflect(&current);
376                let key = format!("{:?}", reflected.coords);
377
378                if !seen.contains(&key) {
379                    seen.insert(key);
380                    orbit.push(reflected.clone());
381                    queue.push(reflected);
382                }
383            }
384        }
385
386        orbit
387    }
388
389    /// Dimension of the Lie algebra: rank + `num_roots`.
390    ///
391    /// For type Aₙ: dim = n + n(n+1) = n(n+2)
392    #[must_use]
393    pub fn dimension(&self) -> usize {
394        self.rank + self.num_roots()
395    }
396
397    /// Dominant weight chamber: λ such that ⟨λ, α⟩ ≥ 0 for all simple roots α.
398    #[must_use]
399    pub fn is_dominant_weight(&self, weight: &Root) -> bool {
400        self.simple_roots
401            .iter()
402            .all(|alpha| weight.inner_product(alpha) >= -1e-10)
403    }
404
405    /// Express a root as a linear combination of simple roots.
406    ///
407    /// Returns coefficients [c₁, c₂, ..., cₙ] such that β = Σ cᵢ αᵢ.
408    /// For roots in the system, coefficients are integers (positive for positive roots).
409    ///
410    /// Returns `None` if the root is not in this system, or if the expansion
411    /// is not yet implemented for general root systems.
412    ///
413    /// # Supported Systems
414    ///
415    /// - **Type A** (SU(n+1)): Fully implemented. Roots `e_i - e_j` expand as
416    ///   sums of consecutive simple roots.
417    /// - **Other types**: Returns `None`. General expansion requires Cartan
418    ///   matrix inversion, which is not yet implemented.
419    pub fn simple_root_expansion(&self, root: &Root) -> Option<Vec<i32>> {
420        if !self.contains_root(root) {
421            return None;
422        }
423
424        // For type A_n (SU(n+1)): roots are e_i - e_j in ℝⁿ⁺¹
425        // Simple roots are α_k = e_k - e_{k+1} for k = 0, ..., n-1
426        // A root e_i - e_j (i < j) = α_i + α_{i+1} + ... + α_{j-1}
427        //
428        // Detect type A by checking if root has exactly one +1 and one -1 coordinate
429        let coords = &root.coords;
430        let mut pos_idx: Option<usize> = None;
431        let mut neg_idx: Option<usize> = None;
432
433        for (idx, &val) in coords.iter().enumerate() {
434            if (val - 1.0).abs() < 1e-10 {
435                if pos_idx.is_some() {
436                    // Not type A format - fall through to general case
437                    return None;
438                }
439                pos_idx = Some(idx);
440            } else if (val + 1.0).abs() < 1e-10 {
441                if neg_idx.is_some() {
442                    return None;
443                }
444                neg_idx = Some(idx);
445            } else if val.abs() > 1e-10 {
446                // Non-zero entry that's not ±1 - not type A
447                return None;
448            }
449        }
450
451        let (Some(i), Some(j)) = (pos_idx, neg_idx) else {
452            return None;
453        };
454
455        // Build coefficients: for e_i - e_j, coefficient of α_k is:
456        // +1 if min(i,j) <= k < max(i,j) and i < j (positive root)
457        // -1 if min(i,j) <= k < max(i,j) and i > j (negative root)
458        let mut coeffs = vec![0i32; self.rank];
459        let (min_idx, max_idx) = if i < j { (i, j) } else { (j, i) };
460        let sign = if i < j { 1 } else { -1 };
461
462        for k in min_idx..max_idx {
463            if k < self.rank {
464                coeffs[k] = sign;
465            }
466        }
467
468        Some(coeffs)
469    }
470}
471
472/// Weight lattice for a root system.
473///
474/// Weights are elements λ ∈ 𝔥* such that ⟨λ, α^∨⟩ ∈ ℤ for all roots α.
475/// The weight lattice contains the root lattice as a sublattice.
476///
477/// # Fundamental Weights
478///
479/// For simple roots α₁, ..., αₙ, the fundamental weights ω₁, ..., ωₙ satisfy:
480/// ```text
481/// ⟨ωᵢ, αⱼ^∨⟩ = δᵢⱼ
482/// ```
483///
484/// Every dominant weight can be written λ = Σ mᵢ ωᵢ with mᵢ ∈ ℤ≥₀.
485pub struct WeightLattice {
486    root_system: RootSystem,
487    fundamental_weights: Vec<Root>,
488}
489
490impl WeightLattice {
491    /// Create weight lattice from root system.
492    #[must_use]
493    pub fn from_root_system(root_system: RootSystem) -> Self {
494        let fundamental_weights = Self::compute_fundamental_weights(&root_system);
495        Self {
496            root_system,
497            fundamental_weights,
498        }
499    }
500
501    /// Compute fundamental weights ωᵢ such that ⟨ωᵢ, αⱼ^∨⟩ = δᵢⱼ.
502    fn compute_fundamental_weights(rs: &RootSystem) -> Vec<Root> {
503        // For type A_n (SU(n+1)):
504        // In GL(n+1) basis: ωᵢ = (1, 1, ..., 1, 0, ..., 0) with i ones
505        // In SU(n+1) traceless basis: project to hyperplane Σxᵢ = 0
506        //
507        // Projection: x_traceless = x - (Σxᵢ)/(n+1) · (1,1,...,1)
508
509        let n = rs.rank();
510        let dim = n + 1; // Dimension of fundamental representation
511        let mut weights = Vec::new();
512
513        for i in 0..n {
514            // Start with GL(n+1) fundamental weight
515            let mut coords = vec![0.0; dim];
516            for j in 0..=i {
517                coords[j] = 1.0;
518            }
519
520            // Project to traceless subspace: subtract mean
521            let sum: f64 = coords.iter().sum();
522            let mean = sum / (dim as f64);
523            for coord in &mut coords {
524                *coord -= mean;
525            }
526
527            weights.push(Root::new(coords));
528        }
529
530        weights
531    }
532
533    /// Fundamental weights.
534    #[must_use]
535    pub fn fundamental_weights(&self) -> &[Root] {
536        &self.fundamental_weights
537    }
538
539    /// Convert Dynkin labels (m₁, ..., mₙ) to weight λ = Σ mᵢ ωᵢ.
540    #[must_use]
541    pub fn dynkin_to_weight(&self, dynkin_labels: &[usize]) -> Root {
542        assert_eq!(dynkin_labels.len(), self.root_system.rank());
543
544        let mut weight_coords = vec![0.0; self.root_system.rank() + 1];
545        for (i, &m) in dynkin_labels.iter().enumerate() {
546            for (j, &w) in self.fundamental_weights[i].coords.iter().enumerate() {
547                weight_coords[j] += (m as f64) * w;
548            }
549        }
550
551        Root::new(weight_coords)
552    }
553
554    /// Get the root system.
555    #[must_use]
556    pub fn root_system(&self) -> &RootSystem {
557        &self.root_system
558    }
559
560    /// Compute ρ (half-sum of positive roots) = sum of fundamental weights.
561    ///
562    /// For type Aₙ: ρ = ω₁ + ω₂ + ... + ωₙ
563    #[must_use]
564    pub fn rho(&self) -> Root {
565        let mut rho_coords = vec![0.0; self.root_system.rank() + 1];
566        for omega in &self.fundamental_weights {
567            for (i, &coord) in omega.coords.iter().enumerate() {
568                rho_coords[i] += coord;
569            }
570        }
571        Root::new(rho_coords)
572    }
573
574    /// Kostant's partition function P(γ): number of ways to write γ as a sum of positive roots.
575    ///
576    /// Counts solutions to: γ = Σ_{α ∈ Φ⁺} `n_α` · α where `n_α` ≥ 0 are non-negative integers.
577    ///
578    /// This is computed via dynamic programming with ORDERED roots to avoid overcounting.
579    ///
580    /// # Returns
581    /// Number of distinct ways (counting multiplicities) to express γ as a sum of positive roots.
582    /// Returns 0 if γ cannot be expressed as such a sum.
583    ///
584    /// # Algorithm
585    /// Uses DP with memoization and ROOT ORDERING to count unordered partitions.
586    /// For each root `α_i`, we only consider using roots `α_j` where j ≥ i.
587    /// This ensures we count combinations, not permutations.
588    ///
589    /// # Performance
590    /// O(|Φ⁺| × V) where V is the size of the reachable region.
591    /// For typical representations, this is manageable.
592    fn kostant_partition_function(&self, gamma: &Root) -> usize {
593        use std::collections::HashMap;
594
595        // Base case: zero vector has exactly one representation (empty sum)
596        if gamma.coords.iter().all(|&x| x.abs() < 1e-10) {
597            return 1;
598        }
599
600        // Use memoization to avoid recomputation
601        // Key: (gamma, starting_index) to track which roots we can still use
602        let mut memo: HashMap<(String, usize), usize> = HashMap::new();
603        let positive_roots: Vec<Root> = self.root_system.positive_roots();
604
605        Self::partition_helper_ordered(gamma, &positive_roots, 0, &mut memo)
606    }
607
608    /// Recursive helper for partition function with ROOT ORDERING to avoid overcounting.
609    ///
610    /// Only considers roots starting from index `start_idx` onwards.
611    /// This ensures we generate combinations (not permutations) of roots.
612    fn partition_helper_ordered(
613        gamma: &Root,
614        positive_roots: &[Root],
615        start_idx: usize,
616        memo: &mut std::collections::HashMap<(String, usize), usize>,
617    ) -> usize {
618        // Base case: zero
619        if gamma.coords.iter().all(|&x| x.abs() < 1e-10) {
620            return 1;
621        }
622
623        // Base case: no more roots to try
624        if start_idx >= positive_roots.len() {
625            return 0;
626        }
627
628        // Check memo
629        let key = (WeightLattice::weight_key(&gamma.coords), start_idx);
630        if let Some(&result) = memo.get(&key) {
631            return result;
632        }
633
634        // Early termination: if γ has large negative coordinates, it's not expressible
635        let mut has_large_negative = false;
636        for &coord in &gamma.coords {
637            if coord < -100.0 {
638                has_large_negative = true;
639                break;
640            }
641        }
642
643        if has_large_negative {
644            memo.insert(key, 0);
645            return 0;
646        }
647
648        // Recursive case: for current root α at start_idx, we have two choices:
649        // 1. Don't use this root at all: move to next root
650        // 2. Use this root k times (k ≥ 1): subtract k·α and stay at same index
651        let mut count = 0;
652        let alpha = &positive_roots[start_idx];
653
654        // Choice 1: Skip this root entirely
655        if memo.len() < 100_000 {
656            count += Self::partition_helper_ordered(gamma, positive_roots, start_idx + 1, memo);
657        }
658
659        // Choice 2: Use this root at least once
660        let mut gamma_minus_k_alpha = gamma.clone();
661        for (i, &a) in alpha.coords.iter().enumerate() {
662            gamma_minus_k_alpha.coords[i] -= a;
663        }
664
665        // Can we use this root? Check if γ - α is "valid" (not too negative)
666        let can_use = gamma_minus_k_alpha.coords.iter().all(|&x| x > -100.0);
667
668        if can_use && memo.len() < 100_000 {
669            // Recurse with γ - α, staying at same root index (can use it multiple times)
670            count += Self::partition_helper_ordered(
671                &gamma_minus_k_alpha,
672                positive_roots,
673                start_idx,
674                memo,
675            );
676        }
677
678        memo.insert(key, count);
679        count
680    }
681
682    /// Generate Weyl group elements for type `A_n` (symmetric group S_{n+1}).
683    ///
684    /// The Weyl group of type `A_n` is isomorphic to S_{n+1}, the symmetric group
685    /// of permutations of {1, 2, ..., n+1}.
686    ///
687    /// Each permutation σ acts on weights λ = (λ₁, ..., λ_{n+1}) by permuting coordinates.
688    ///
689    /// # Returns
690    /// Vector of (permutation, sign) pairs where:
691    /// - permutation[i] = j means coordinate i maps to coordinate j
692    /// - sign = +1 for even permutations, -1 for odd
693    ///
694    /// # Performance
695    /// Generates all (n+1)! permutations. For SU(3) this is 6, for SU(4) this is 24, etc.
696    fn weyl_group_type_a(&self) -> Vec<(Vec<usize>, i32)> {
697        let n_plus_1 = self.root_system.rank() + 1;
698        let mut perms = Vec::new();
699
700        // Generate all permutations via Heap's algorithm
701        let mut current: Vec<usize> = (0..n_plus_1).collect();
702        Self::generate_permutations(&mut current, n_plus_1, &mut perms);
703
704        perms
705    }
706
707    /// Generate all permutations using Heap's algorithm.
708    fn generate_permutations(arr: &mut [usize], size: usize, result: &mut Vec<(Vec<usize>, i32)>) {
709        if size == 1 {
710            let perm = arr.to_vec();
711            let sign = Self::permutation_sign(&perm);
712            result.push((perm, sign));
713            return;
714        }
715
716        for i in 0..size {
717            Self::generate_permutations(arr, size - 1, result);
718
719            if size % 2 == 0 {
720                arr.swap(i, size - 1);
721            } else {
722                arr.swap(0, size - 1);
723            }
724        }
725    }
726
727    /// Compute the sign of a permutation (+1 for even, -1 for odd).
728    fn permutation_sign(perm: &[usize]) -> i32 {
729        let n = perm.len();
730        let mut sign = 1i32;
731
732        for i in 0..n {
733            for j in (i + 1)..n {
734                if perm[i] > perm[j] {
735                    sign *= -1;
736                }
737            }
738        }
739
740        sign
741    }
742
743    /// Apply Weyl group element (permutation) to a weight.
744    fn weyl_action(&self, weight: &Root, permutation: &[usize]) -> Root {
745        let mut new_coords = vec![0.0; weight.coords.len()];
746        for (i, &pi) in permutation.iter().enumerate() {
747            new_coords[i] = weight.coords[pi];
748        }
749        Root::new(new_coords)
750    }
751
752    /// Compute weight multiplicity using Kostant's formula.
753    ///
754    /// Kostant's formula (exact, not recursive like Freudenthal):
755    /// ```text
756    /// m_Λ(λ) = Σ_{w ∈ W} ε(w) · P(w·(Λ+ρ) - (λ+ρ))
757    /// ```
758    ///
759    /// where:
760    /// - Λ = highest weight of the representation
761    /// - λ = weight whose multiplicity we compute
762    /// - W = Weyl group
763    /// - ε(w) = sign of w (+1 for even, -1 for odd)
764    /// - P(γ) = partition function counting ways to write γ as sum of positive roots
765    ///
766    /// # Mathematical Background
767    ///
768    /// This is the most general multiplicity formula, working for all weights in all
769    /// representations. Unlike Freudenthal (which requires dominance), Kostant works
770    /// directly.
771    ///
772    /// # Performance
773    /// O(|W| × `P_cost`) where |W| = (n+1)! for type `A_n` and `P_cost` is partition function cost.
774    /// For SU(3): 6 Weyl group elements.
775    /// For SU(4): 24 Weyl group elements.
776    ///
777    /// # References
778    /// - Humphreys, "Introduction to Lie Algebras and Representation Theory", §24.3
779    /// - Kostant (1959), "A Formula for the Multiplicity of a Weight"
780    #[must_use]
781    pub fn kostant_multiplicity(&self, highest_weight: &Root, weight: &Root) -> usize {
782        let rho = self.rho();
783
784        // Compute Λ + ρ
785        let mut lambda_plus_rho = highest_weight.clone();
786        for (i, &r) in rho.coords.iter().enumerate() {
787            lambda_plus_rho.coords[i] += r;
788        }
789
790        // Compute λ + ρ
791        let mut mu_plus_rho = weight.clone();
792        for (i, &r) in rho.coords.iter().enumerate() {
793            mu_plus_rho.coords[i] += r;
794        }
795
796        // Sum over Weyl group
797        let weyl_group = self.weyl_group_type_a();
798        let mut multiplicity = 0i32; // Use signed arithmetic for alternating sum
799
800        for (perm, sign) in weyl_group {
801            // Compute w·(Λ+ρ)
802            let w_lambda_plus_rho = self.weyl_action(&lambda_plus_rho, &perm);
803
804            // Compute γ = w·(Λ+ρ) - (λ+ρ)
805            let mut gamma = w_lambda_plus_rho.clone();
806            for (i, &mu) in mu_plus_rho.coords.iter().enumerate() {
807                gamma.coords[i] -= mu;
808            }
809
810            // Compute P(γ)
811            let p_gamma = self.kostant_partition_function(&gamma);
812
813            // Add signed contribution
814            multiplicity += sign * (p_gamma as i32);
815        }
816
817        // Result must be non-negative
818        assert!(
819            multiplicity >= 0,
820            "Kostant multiplicity must be non-negative, got {}",
821            multiplicity
822        );
823        multiplicity as usize
824    }
825
826    /// Compute dimension of irreducible representation using Weyl dimension formula.
827    ///
828    /// For type Aₙ with highest weight λ:
829    /// ```text
830    /// dim(λ) = ∏_{α>0} ⟨λ + ρ, α⟩ / ⟨ρ, α⟩
831    /// ```
832    ///
833    /// This is the EXACT dimension - no approximation.
834    #[must_use]
835    pub fn weyl_dimension(&self, highest_weight: &Root) -> usize {
836        let rho = self.rho();
837
838        // λ + ρ
839        let mut lambda_plus_rho = highest_weight.clone();
840        for (i, &r) in rho.coords.iter().enumerate() {
841            lambda_plus_rho.coords[i] += r;
842        }
843
844        let mut numerator = 1.0;
845        let mut denominator = 1.0;
846
847        for alpha in self.root_system.positive_roots() {
848            let num = lambda_plus_rho.inner_product(&alpha);
849            let denom = rho.inner_product(&alpha);
850
851            numerator *= num;
852            denominator *= denom;
853        }
854
855        (numerator / denominator).round() as usize
856    }
857
858    /// Helper function to create a stable `HashMap` key from weight coordinates.
859    ///
860    /// Rounds each coordinate to 10 decimal places to avoid floating-point precision issues
861    /// when using coordinates as `HashMap` keys.
862    fn weight_key(coords: &[f64]) -> String {
863        let rounded: Vec<String> = coords.iter().map(|&x| format!("{:.10}", x)).collect();
864        format!("[{}]", rounded.join(", "))
865    }
866
867    /// Generate all weights in an irreducible representation.
868    ///
869    /// Uses the CORRECT, GENERAL algorithm:
870    /// 1. Generate candidate weights by exploring from highest weight with ALL positive roots
871    /// 2. Add Weyl orbit of highest weight to ensure completeness
872    /// 3. Use Kostant's formula to compute accurate multiplicities
873    /// 4. Return only weights with multiplicity > 0
874    ///
875    /// This works for ALL representations, including adjoint where negative roots appear.
876    #[must_use]
877    pub fn weights_of_irrep(&self, highest_weight: &Root) -> Vec<(Root, usize)> {
878        use std::collections::{HashMap, VecDeque};
879
880        let mut candidates: HashMap<String, Root> = HashMap::new();
881        let mut queue: VecDeque<Root> = VecDeque::new();
882
883        // Start with highest weight
884        let hw_key = Self::weight_key(&highest_weight.coords);
885        candidates.insert(hw_key, highest_weight.clone());
886        queue.push_back(highest_weight.clone());
887
888        // BFS: Subtract ALL positive roots (not just simple roots!)
889        // This ensures we reach all weights, including negative roots in adjoint rep
890        while let Some(weight) = queue.pop_front() {
891            for pos_root in self.root_system.positive_roots() {
892                let mut new_weight = weight.clone();
893                for (i, &a) in pos_root.coords.iter().enumerate() {
894                    new_weight.coords[i] -= a;
895                }
896
897                let new_key = Self::weight_key(&new_weight.coords);
898
899                // Skip if already seen
900                if candidates.contains_key(&new_key) {
901                    continue;
902                }
903
904                // Check if in reasonable bounds (heuristic to avoid infinite exploration)
905                let norm = new_weight.norm_squared();
906                let hw_norm = highest_weight.norm_squared();
907                if norm > 3.0 * hw_norm + 10.0 {
908                    continue;
909                }
910
911                candidates.insert(new_key, new_weight.clone());
912                queue.push_back(new_weight);
913
914                // Safety limit
915                if candidates.len() > 1000 {
916                    break;
917                }
918            }
919
920            if candidates.len() > 1000 {
921                break;
922            }
923        }
924
925        // CRITICAL: Also add the full Weyl orbit of the highest weight
926        // This ensures we don't miss any extremal weights
927        let weyl_orbit = self.root_system.weyl_orbit(highest_weight);
928        for w in weyl_orbit {
929            let key = Self::weight_key(&w.coords);
930            candidates.insert(key, w);
931        }
932
933        // Compute accurate multiplicities using Kostant's formula
934        let candidate_list: Vec<Root> = candidates.into_values().collect();
935        let mut result = Vec::new();
936
937        for weight in candidate_list.iter() {
938            let mult = self.kostant_multiplicity(highest_weight, weight);
939            if mult > 0 {
940                result.push((weight.clone(), mult));
941            }
942        }
943
944        result
945    }
946
947    /// Generate a string representation of a weight diagram for rank 2.
948    ///
949    /// For SU(3) representations, this creates a 2D triangular lattice
950    /// showing all weights with their multiplicities.
951    ///
952    /// # Example
953    ///
954    /// ```
955    /// use lie_groups::{RootSystem, WeightLattice};
956    ///
957    /// let rs = RootSystem::type_a(2);
958    /// let wl = WeightLattice::from_root_system(rs);
959    /// let highest = wl.dynkin_to_weight(&[1, 1]); // Adjoint rep
960    ///
961    /// let diagram = wl.weight_diagram_string(&highest);
962    /// // Should show 8 weights (8-dimensional rep)
963    /// ```
964    #[must_use]
965    pub fn weight_diagram_string(&self, highest_weight: &Root) -> String {
966        if self.root_system.rank() != 2 {
967            return "Weight diagrams only implemented for rank 2".to_string();
968        }
969
970        let weights = self.weights_of_irrep(highest_weight);
971
972        let mut output = String::new();
973        writeln!(
974            &mut output,
975            "Weight diagram for highest weight {:?}",
976            highest_weight.coords
977        )
978        .expect("String formatting should not fail");
979        writeln!(&mut output, "Dimension: {}", weights.len())
980            .expect("String formatting should not fail");
981        output.push_str("Weights (multiplicity):\n");
982
983        for (weight, mult) in &weights {
984            writeln!(&mut output, "  {} (×{})", weight, mult)
985                .expect("String formatting should not fail");
986        }
987
988        output
989    }
990}
991
992/// Cartan subalgebra of a semisimple Lie algebra.
993///
994/// The Cartan subalgebra 𝔥 is the maximal abelian subalgebra consisting of
995/// simultaneously diagonalizable elements. For SU(n+1) (type Aₙ), it consists
996/// of traceless diagonal matrices.
997///
998/// # Mathematical Background
999///
1000/// - Dimension: rank of the Lie algebra
1001/// - Basis: {H₁, ..., Hₙ} where [Hᵢ, Hⱼ] = 0 (all commute)
1002/// - For SU(n+1): Hᵢ = Eᵢᵢ - Eᵢ₊₁,ᵢ₊₁ (diagonal matrices)
1003/// - Roots are functionals α: 𝔥 → ℝ
1004///
1005/// # Example
1006///
1007/// ```
1008/// use lie_groups::root_systems::CartanSubalgebra;
1009///
1010/// // SU(3) Cartan subalgebra (2-dimensional)
1011/// let cartan = CartanSubalgebra::type_a(2);
1012/// assert_eq!(cartan.dimension(), 2);
1013///
1014/// let basis = cartan.basis_matrices();
1015/// assert_eq!(basis.len(), 2);
1016/// ```
1017pub struct CartanSubalgebra {
1018    /// Dimension of the Cartan subalgebra (rank of Lie algebra)
1019    dimension: usize,
1020
1021    /// Basis matrices as complex arrays
1022    /// For SU(n+1), these are n diagonal traceless matrices
1023    basis_matrices: Vec<Array2<Complex64>>,
1024
1025    /// Matrix dimension (N for SU(N))
1026    matrix_size: usize,
1027}
1028
1029impl CartanSubalgebra {
1030    /// Create Cartan subalgebra for type Aₙ (SU(n+1)).
1031    ///
1032    /// The Cartan subalgebra consists of traceless diagonal matrices.
1033    /// Basis: Hᵢ = Eᵢᵢ - Eᵢ₊₁,ᵢ₊₁ for i = 1, ..., n
1034    ///
1035    /// # Example
1036    ///
1037    /// ```
1038    /// use lie_groups::root_systems::CartanSubalgebra;
1039    ///
1040    /// // SU(2) Cartan subalgebra: 1-dimensional
1041    /// let h = CartanSubalgebra::type_a(1);
1042    /// assert_eq!(h.dimension(), 1);
1043    ///
1044    /// // First basis element: diag(1, -1)
1045    /// let h1 = &h.basis_matrices()[0];
1046    /// assert!((h1[(0,0)].re - 1.0).abs() < 1e-10);
1047    /// assert!((h1[(1,1)].re + 1.0).abs() < 1e-10);
1048    /// ```
1049    #[must_use]
1050    pub fn type_a(n: usize) -> Self {
1051        assert!(n >= 1, "Type A_n requires n >= 1");
1052
1053        let matrix_size = n + 1;
1054        let dimension = n;
1055
1056        let mut basis_matrices = Vec::new();
1057
1058        // Generate Hᵢ = Eᵢᵢ - Eᵢ₊₁,ᵢ₊₁ for i = 0, ..., n-1
1059        for i in 0..n {
1060            let mut h = Array2::<Complex64>::zeros((matrix_size, matrix_size));
1061            h[(i, i)] = Complex64::new(1.0, 0.0);
1062            h[(i + 1, i + 1)] = Complex64::new(-1.0, 0.0);
1063            basis_matrices.push(h);
1064        }
1065
1066        Self {
1067            dimension,
1068            basis_matrices,
1069            matrix_size,
1070        }
1071    }
1072
1073    /// Dimension of the Cartan subalgebra (rank).
1074    #[must_use]
1075    pub fn dimension(&self) -> usize {
1076        self.dimension
1077    }
1078
1079    /// Basis matrices for the Cartan subalgebra.
1080    #[must_use]
1081    pub fn basis_matrices(&self) -> &[Array2<Complex64>] {
1082        &self.basis_matrices
1083    }
1084
1085    /// Matrix size (N for SU(N)).
1086    #[must_use]
1087    pub fn matrix_size(&self) -> usize {
1088        self.matrix_size
1089    }
1090
1091    /// Evaluate a root functional on a Cartan element.
1092    ///
1093    /// For a root α and Cartan element H = Σ cᵢ Hᵢ, computes α(H) = Σ cᵢ α(Hᵢ).
1094    ///
1095    /// # Arguments
1096    ///
1097    /// * `root` - Root as a vector of coordinates
1098    /// * `coefficients` - Coefficients [c₁, ..., cₙ] in basis {H₁, ..., Hₙ}
1099    ///
1100    /// # Returns
1101    ///
1102    /// The value α(H) ∈ ℂ
1103    #[must_use]
1104    pub fn evaluate_root(&self, root: &Root, coefficients: &[f64]) -> Complex64 {
1105        assert_eq!(
1106            coefficients.len(),
1107            self.dimension,
1108            "Coefficient vector must match Cartan dimension"
1109        );
1110        assert_eq!(
1111            root.rank(),
1112            self.matrix_size,
1113            "Root dimension must match matrix size"
1114        );
1115
1116        // For type A_n, α(H) is computed via the standard pairing
1117        // If H = diag(h₁, ..., hₙ₊₁) with Σhᵢ = 0, and α = (α₁, ..., αₙ₊₁)
1118        // then α(H) = Σ αᵢ hᵢ
1119
1120        // Reconstruct diagonal H from basis coefficients
1121        let mut h_diagonal = vec![Complex64::new(0.0, 0.0); self.matrix_size];
1122        for (i, &c) in coefficients.iter().enumerate() {
1123            h_diagonal[i] += Complex64::new(c, 0.0);
1124            h_diagonal[i + 1] -= Complex64::new(c, 0.0);
1125        }
1126
1127        // Compute α(H) = Σ αᵢ hᵢ
1128        root.coords
1129            .iter()
1130            .zip(&h_diagonal)
1131            .map(|(alpha_i, h_i)| h_i * alpha_i)
1132            .sum()
1133    }
1134
1135    /// Project a matrix onto the Cartan subalgebra.
1136    ///
1137    /// For a matrix M, finds coefficients [c₁, ..., cₙ] such that
1138    /// Σ cᵢ Hᵢ best approximates M in the Frobenius norm.
1139    ///
1140    /// Since the basis {H₁, ..., Hₙ} is not necessarily orthogonal,
1141    /// we solve the linear system: G c = g where Gᵢⱼ = ⟨Hᵢ, Hⱼ⟩.
1142    ///
1143    /// # Arguments
1144    ///
1145    /// * `matrix` - Matrix to project
1146    ///
1147    /// # Returns
1148    ///
1149    /// `Some(coefficients)` in the Cartan basis for rank ≤ 2,
1150    /// `None` for rank > 2 (Gaussian elimination not yet implemented).
1151    ///
1152    /// # Limitations
1153    ///
1154    /// Currently only supports rank 1 (SU(2)) and rank 2 (SU(3)) systems.
1155    /// Higher rank systems require general Gaussian elimination.
1156    #[must_use]
1157    pub fn project_matrix(&self, matrix: &Array2<Complex64>) -> Option<Vec<f64>> {
1158        assert_eq!(
1159            matrix.shape(),
1160            &[self.matrix_size, self.matrix_size],
1161            "Matrix must be {}×{}",
1162            self.matrix_size,
1163            self.matrix_size
1164        );
1165
1166        // Compute Gram matrix G and right-hand side g
1167        let n = self.dimension;
1168        let mut gram = vec![vec![0.0; n]; n];
1169        let mut rhs = vec![0.0; n];
1170
1171        for i in 0..n {
1172            for j in 0..n {
1173                // Gᵢⱼ = ⟨Hᵢ, Hⱼ⟩ = Tr(Hᵢ† Hⱼ)
1174                let inner: Complex64 = self.basis_matrices[i]
1175                    .iter()
1176                    .zip(self.basis_matrices[j].iter())
1177                    .map(|(h_i, h_j)| h_i.conj() * h_j)
1178                    .sum();
1179                gram[i][j] = inner.re;
1180            }
1181
1182            // gᵢ = ⟨M, Hᵢ⟩ = Tr(M† Hᵢ)
1183            let inner: Complex64 = matrix
1184                .iter()
1185                .zip(self.basis_matrices[i].iter())
1186                .map(|(m_ij, h_ij)| m_ij.conj() * h_ij)
1187                .sum();
1188            rhs[i] = inner.re;
1189        }
1190
1191        // Solve G c = g using direct formulas (small n only)
1192        let mut coefficients = vec![0.0; n];
1193
1194        match n {
1195            1 => {
1196                coefficients[0] = rhs[0] / gram[0][0];
1197                Some(coefficients)
1198            }
1199            2 => {
1200                // Solve 2×2 system directly
1201                let det = gram[0][0] * gram[1][1] - gram[0][1] * gram[1][0];
1202                coefficients[0] = (rhs[0] * gram[1][1] - rhs[1] * gram[0][1]) / det;
1203                coefficients[1] = (gram[0][0] * rhs[1] - gram[1][0] * rhs[0]) / det;
1204                Some(coefficients)
1205            }
1206            _ => {
1207                // Rank > 2: Gaussian elimination not yet implemented
1208                None
1209            }
1210        }
1211    }
1212
1213    /// Construct a Cartan element from coefficients.
1214    ///
1215    /// Given coefficients [c₁, ..., cₙ], returns H = Σ cᵢ Hᵢ.
1216    #[must_use]
1217    pub fn from_coefficients(&self, coefficients: &[f64]) -> Array2<Complex64> {
1218        assert_eq!(
1219            coefficients.len(),
1220            self.dimension,
1221            "Must provide {} coefficients",
1222            self.dimension
1223        );
1224
1225        let mut result = Array2::<Complex64>::zeros((self.matrix_size, self.matrix_size));
1226
1227        for (&c_i, h_i) in coefficients.iter().zip(&self.basis_matrices) {
1228            result = result + h_i * c_i;
1229        }
1230
1231        result
1232    }
1233
1234    /// Check if a matrix is in the Cartan subalgebra.
1235    ///
1236    /// Returns true if the matrix is diagonal (or nearly diagonal within tolerance).
1237    #[must_use]
1238    pub fn contains(&self, matrix: &Array2<Complex64>, tolerance: f64) -> bool {
1239        assert_eq!(
1240            matrix.shape(),
1241            &[self.matrix_size, self.matrix_size],
1242            "Matrix must be {}×{}",
1243            self.matrix_size,
1244            self.matrix_size
1245        );
1246
1247        // Check if matrix is diagonal
1248        for i in 0..self.matrix_size {
1249            for j in 0..self.matrix_size {
1250                if i != j && matrix[(i, j)].norm() > tolerance {
1251                    return false;
1252                }
1253            }
1254        }
1255
1256        // Check if diagonal entries sum to zero (traceless)
1257        let trace: Complex64 = (0..self.matrix_size).map(|i| matrix[(i, i)]).sum();
1258
1259        trace.norm() < tolerance
1260    }
1261
1262    /// Killing form restricted to Cartan subalgebra.
1263    ///
1264    /// For H, H' ∈ 𝔥, the Killing form is κ(H, H') = Tr(`ad_H` ∘ `ad_H`').
1265    /// For type `A_n`, this simplifies significantly.
1266    ///
1267    /// # Arguments
1268    ///
1269    /// * `coeffs1` - First Cartan element as coefficients
1270    /// * `coeffs2` - Second Cartan element as coefficients
1271    ///
1272    /// # Returns
1273    ///
1274    /// The value κ(H₁, H₂)
1275    #[must_use]
1276    pub fn killing_form(&self, coeffs1: &[f64], coeffs2: &[f64]) -> f64 {
1277        assert_eq!(coeffs1.len(), self.dimension);
1278        assert_eq!(coeffs2.len(), self.dimension);
1279
1280        // For SU(n+1), the Killing form on the Cartan is
1281        // κ(H, H') = 2(n+1) Tr(HH')
1282        //
1283        // For our basis Hᵢ = Eᵢᵢ - Eᵢ₊₁,ᵢ₊₁:
1284        // Tr(Hᵢ Hⱼ) = 2δᵢⱼ
1285
1286        let n_plus_1 = self.matrix_size as f64;
1287
1288        coeffs1
1289            .iter()
1290            .zip(coeffs2)
1291            .map(|(c1, c2)| 2.0 * n_plus_1 * 2.0 * c1 * c2)
1292            .sum()
1293    }
1294
1295    /// Dual basis in 𝔥* (root space).
1296    ///
1297    /// Returns roots {α₁, ..., αₙ} such that αᵢ(Hⱼ) = δᵢⱼ.
1298    /// For type `A_n`, these correspond to the simple roots.
1299    #[must_use]
1300    pub fn dual_basis(&self) -> Vec<Root> {
1301        let mut dual_roots = Vec::new();
1302
1303        for i in 0..self.dimension {
1304            // For Hᵢ = Eᵢᵢ - Eᵢ₊₁,ᵢ₊₁, the dual is αᵢ = eᵢ - eᵢ₊₁
1305            let mut coords = vec![0.0; self.matrix_size];
1306            coords[i] = 1.0;
1307            coords[i + 1] = -1.0;
1308            dual_roots.push(Root::new(coords));
1309        }
1310
1311        dual_roots
1312    }
1313}
1314
1315/// Weyl chamber for a root system.
1316///
1317/// A Weyl chamber is a connected component of the complement of the reflecting
1318/// hyperplanes in the dual Cartan space 𝔥*. The **fundamental Weyl chamber**
1319/// (or **dominant chamber**) is defined by the simple roots:
1320///
1321/// ```text
1322/// C = {λ ∈ 𝔥* : ⟨λ, αᵢ⟩ ≥ 0 for all simple roots αᵢ}
1323/// ```
1324///
1325/// # Mathematical Background
1326///
1327/// - The Weyl group W acts on 𝔥* by reflections through root hyperplanes
1328/// - W partitions 𝔥* into finitely many Weyl chambers
1329/// - All chambers are isometric under W
1330/// - The fundamental chamber parametrizes dominant weights (highest weights
1331///   of irreducible representations)
1332///
1333/// # Example
1334///
1335/// ```
1336/// use lie_groups::root_systems::{RootSystem, WeylChamber, Root};
1337///
1338/// let root_system = RootSystem::type_a(2); // SU(3)
1339/// let chamber = WeylChamber::fundamental(&root_system);
1340///
1341/// // Fundamental weight ω₁ = (2/3, -1/3, -1/3) is dominant (traceless for SU(3))
1342/// let omega1 = Root::new(vec![2.0/3.0, -1.0/3.0, -1.0/3.0]);
1343/// assert!(chamber.contains(&omega1, false)); // Non-strict since on boundary
1344///
1345/// // Negative weight is not dominant
1346/// let neg = Root::new(vec![-1.0, 0.0, 1.0]);
1347/// assert!(!chamber.contains(&neg, false));
1348/// ```
1349///
1350/// # References
1351///
1352/// - Humphreys, *Introduction to Lie Algebras*, §10.2
1353/// - Hall, *Lie Groups*, §8.4
1354#[derive(Debug, Clone)]
1355pub struct WeylChamber {
1356    /// The root system defining the chamber
1357    root_system: RootSystem,
1358
1359    /// Simple roots defining the chamber walls
1360    simple_roots: Vec<Root>,
1361}
1362
1363impl WeylChamber {
1364    /// Construct the fundamental Weyl chamber for a root system.
1365    ///
1366    /// The fundamental chamber is bounded by hyperplanes perpendicular to
1367    /// the simple roots:
1368    ///
1369    /// ```text
1370    /// C = {λ : ⟨λ, αᵢ⟩ ≥ 0 for all simple αᵢ}
1371    /// ```
1372    #[must_use]
1373    pub fn fundamental(root_system: &RootSystem) -> Self {
1374        Self {
1375            root_system: root_system.clone(),
1376            simple_roots: root_system.simple_roots.clone(),
1377        }
1378    }
1379
1380    /// Check if a weight is in the Weyl chamber.
1381    ///
1382    /// # Arguments
1383    ///
1384    /// * `weight` - Weight λ ∈ 𝔥* to test
1385    /// * `strict` - If true, use strict inequality (⟨λ, αᵢ⟩ > 0); else non-strict (≥ 0)
1386    ///
1387    /// # Returns
1388    ///
1389    /// True if λ is in the chamber (dominant weight)
1390    #[must_use]
1391    pub fn contains(&self, weight: &Root, strict: bool) -> bool {
1392        // For type A_n, roots have n+1 coordinates
1393        assert_eq!(weight.rank(), self.root_system.rank + 1);
1394
1395        for alpha in &self.simple_roots {
1396            let pairing = weight.inner_product(alpha);
1397
1398            if strict {
1399                if pairing <= 1e-10 {
1400                    return false;
1401                }
1402            } else if pairing < -1e-10 {
1403                return false;
1404            }
1405        }
1406
1407        true
1408    }
1409
1410    /// Project a weight onto the closure of the fundamental Weyl chamber.
1411    ///
1412    /// Uses the Weyl group to map an arbitrary weight to its unique dominant
1413    /// representative (closest point in the chamber).
1414    ///
1415    /// # Algorithm
1416    ///
1417    /// Iteratively reflect through violated hyperplanes until all simple root
1418    /// pairings are non-negative.
1419    #[must_use]
1420    pub fn project(&self, weight: &Root) -> Root {
1421        let mut current = weight.clone();
1422
1423        // Maximum iterations to prevent infinite loops
1424        let max_iterations = 100;
1425        let mut iterations = 0;
1426
1427        while iterations < max_iterations {
1428            let mut found_violation = false;
1429
1430            for alpha in &self.simple_roots {
1431                let pairing = current.inner_product(alpha);
1432
1433                if pairing < -1e-10 {
1434                    // Reflect through α to increase ⟨current, α⟩
1435                    current = alpha.reflect(&current);
1436                    found_violation = true;
1437                }
1438            }
1439
1440            if !found_violation {
1441                break;
1442            }
1443
1444            iterations += 1;
1445        }
1446
1447        if iterations >= max_iterations {
1448            eprintln!(
1449                "Warning: WeylChamber::project did not converge in {} iterations",
1450                max_iterations
1451            );
1452        }
1453
1454        current
1455    }
1456
1457    /// Get the simple roots defining the chamber walls.
1458    #[must_use]
1459    pub fn simple_roots(&self) -> &[Root] {
1460        &self.simple_roots
1461    }
1462}
1463
1464/// Fundamental alcove for an affine root system.
1465///
1466/// An **alcove** is a bounded region in the Cartan space defined by affine
1467/// hyperplanes (root hyperplanes shifted by integer levels). The fundamental
1468/// alcove is:
1469///
1470/// ```text
1471/// A = {λ ∈ 𝔥* : ⟨λ, αᵢ⟩ ≥ 0 for all simple αᵢ, and ⟨λ, θ⟩ ≤ 1}
1472/// ```
1473///
1474/// where θ is the **highest root** (longest root in the positive system).
1475///
1476/// # Mathematical Background
1477///
1478/// - Alcoves tile 𝔥* under the affine Weyl group Wₐff
1479/// - The fundamental alcove parametrizes integrable highest-weight modules
1480///   at level k (conformal field theory, loop groups)
1481/// - Vertices of the alcove are fundamental weights
1482///
1483/// # Example
1484///
1485/// ```
1486/// use lie_groups::root_systems::{RootSystem, Alcove, Root};
1487///
1488/// let root_system = RootSystem::type_a(2); // SU(3)
1489/// let alcove = Alcove::fundamental(&root_system);
1490///
1491/// // Weight inside alcove
1492/// let lambda = Root::new(vec![0.3, 0.3, -0.6]);
1493/// // (need to verify ⟨λ, αᵢ⟩ ≥ 0 and ⟨λ, θ⟩ ≤ 1)
1494/// ```
1495///
1496/// # References
1497///
1498/// - Kac, *Infinite Dimensional Lie Algebras*, §6.2
1499/// - Humphreys, *Reflection Groups and Coxeter Groups*, §4.4
1500#[derive(Debug, Clone)]
1501pub struct Alcove {
1502    /// The root system defining the alcove
1503    root_system: RootSystem,
1504
1505    /// Simple roots (defining lower walls)
1506    simple_roots: Vec<Root>,
1507
1508    /// Highest root θ (defining upper wall ⟨λ, θ⟩ ≤ level)
1509    highest_root: Root,
1510
1511    /// Level k (affine level; typically 1 for fundamental alcove)
1512    level: f64,
1513}
1514
1515impl Alcove {
1516    /// Construct the fundamental alcove at level k = 1.
1517    ///
1518    /// The fundamental alcove is:
1519    /// ```text
1520    /// A = {λ : ⟨λ, αᵢ⟩ ≥ 0, ⟨λ, θ⟩ ≤ 1}
1521    /// ```
1522    #[must_use]
1523    pub fn fundamental(root_system: &RootSystem) -> Self {
1524        let simple_roots = root_system.simple_roots.clone();
1525        let highest_root = root_system.highest_root();
1526
1527        Self {
1528            root_system: root_system.clone(),
1529            simple_roots,
1530            highest_root,
1531            level: 1.0,
1532        }
1533    }
1534
1535    /// Construct an alcove at a specified level k.
1536    ///
1537    /// # Arguments
1538    ///
1539    /// * `root_system` - The root system
1540    /// * `level` - Affine level k (positive integer for integrable modules)
1541    #[must_use]
1542    pub fn at_level(root_system: &RootSystem, level: f64) -> Self {
1543        assert!(level > 0.0, "Level must be positive");
1544
1545        let simple_roots = root_system.simple_roots.clone();
1546        let highest_root = root_system.highest_root();
1547
1548        Self {
1549            root_system: root_system.clone(),
1550            simple_roots,
1551            highest_root,
1552            level,
1553        }
1554    }
1555
1556    /// Check if a weight is in the alcove.
1557    ///
1558    /// # Arguments
1559    ///
1560    /// * `weight` - Weight λ ∈ 𝔥* to test
1561    /// * `strict` - If true, use strict inequalities
1562    ///
1563    /// # Returns
1564    ///
1565    /// True if λ is in the alcove
1566    #[must_use]
1567    pub fn contains(&self, weight: &Root, strict: bool) -> bool {
1568        // For type A_n, roots have n+1 coordinates
1569        assert_eq!(weight.rank(), self.root_system.rank + 1);
1570
1571        // Check lower walls: ⟨λ, αᵢ⟩ ≥ 0
1572        for alpha in &self.simple_roots {
1573            let pairing = weight.inner_product(alpha);
1574
1575            if strict {
1576                if pairing <= 1e-10 {
1577                    return false;
1578                }
1579            } else if pairing < -1e-10 {
1580                return false;
1581            }
1582        }
1583
1584        // Check upper wall: ⟨λ, θ⟩ ≤ level
1585        let pairing_highest = weight.inner_product(&self.highest_root);
1586
1587        if strict {
1588            if pairing_highest >= self.level - 1e-10 {
1589                return false;
1590            }
1591        } else if pairing_highest > self.level + 1e-10 {
1592            return false;
1593        }
1594
1595        true
1596    }
1597
1598    /// Get the level of the alcove.
1599    #[must_use]
1600    pub fn level(&self) -> f64 {
1601        self.level
1602    }
1603
1604    /// Get the highest root defining the upper wall.
1605    #[must_use]
1606    pub fn highest_root(&self) -> &Root {
1607        &self.highest_root
1608    }
1609
1610    /// Compute vertices of the fundamental alcove.
1611    ///
1612    /// For type `A_n`, the fundamental alcove is a simplex with n+1 vertices:
1613    /// the origin and the fundamental weights {ω₁, ..., ωₙ}.
1614    ///
1615    /// # Returns
1616    ///
1617    /// Vector of vertices (as Roots in 𝔥*)
1618    #[must_use]
1619    pub fn vertices(&self) -> Vec<Root> {
1620        if self.level != 1.0 {
1621            // For level k ≠ 1, vertices are scaled: k·ωᵢ/(k + h∨)
1622            // where h∨ is the dual Coxeter number
1623            // Not implemented for now
1624            return vec![];
1625        }
1626
1627        // For fundamental alcove at level 1:
1628        // Vertices are 0 and fundamental weights
1629
1630        let mut vertices = vec![Root::new(vec![0.0; self.root_system.rank + 1])];
1631
1632        // Add fundamental weights (computed from simple roots)
1633        // For type A_n, ωᵢ = (1/n+1) * (n+1-i, ..., n+1-i, -i, ..., -i)
1634        //                                 └── i times ──┘  └─ n+1-i ──┘
1635
1636        let n = self.root_system.rank;
1637
1638        for i in 1..=n {
1639            let mut coords = vec![0.0; n + 1];
1640
1641            // ωᵢ has i coordinates equal to (n+1-i)/(n+1) and (n+1-i) equal to -i/(n+1)
1642            for j in 0..i {
1643                coords[j] = (n + 1 - i) as f64 / (n + 1) as f64;
1644            }
1645            for j in i..=n {
1646                coords[j] = -(i as f64) / (n + 1) as f64;
1647            }
1648
1649            vertices.push(Root::new(coords));
1650        }
1651
1652        vertices
1653    }
1654}
1655
1656#[cfg(test)]
1657mod tests {
1658    use super::*;
1659
1660    #[test]
1661    fn test_root_operations() {
1662        let alpha = Root::new(vec![1.0, -1.0, 0.0]);
1663        let beta = Root::new(vec![0.0, 1.0, -1.0]);
1664
1665        assert_eq!(alpha.rank(), 3);
1666        assert!((alpha.norm_squared() - 2.0).abs() < 1e-10);
1667        assert!((alpha.inner_product(&beta) + 1.0).abs() < 1e-10);
1668        assert!(alpha.is_positive());
1669        assert!(!alpha.negate().is_positive());
1670    }
1671
1672    #[test]
1673    fn test_weyl_reflection() {
1674        let alpha = Root::new(vec![1.0, -1.0]);
1675        let beta = Root::new(vec![0.5, 0.5]);
1676
1677        let reflected = alpha.reflect(&beta);
1678        // s_α(β) = β - 2⟨β,α⟩/⟨α,α⟩ · α
1679        // ⟨β,α⟩ = 0.5 - 0.5 = 0, so s_α(β) = β
1680        assert!((reflected.coords[0] - 0.5).abs() < 1e-10);
1681        assert!((reflected.coords[1] - 0.5).abs() < 1e-10);
1682    }
1683
1684    #[test]
1685    fn test_cartan_integer() {
1686        let alpha = Root::new(vec![1.0, -1.0, 0.0]);
1687        let beta = Root::new(vec![0.0, 1.0, -1.0]);
1688
1689        // ⟨β, α^∨⟩ = 2⟨β,α⟩/⟨α,α⟩ = 2(-1)/2 = -1
1690        assert_eq!(alpha.cartan_integer(&beta), -1);
1691        assert_eq!(beta.cartan_integer(&alpha), -1);
1692    }
1693
1694    #[test]
1695    fn test_type_a1_su2() {
1696        let rs = RootSystem::type_a(1);
1697
1698        assert_eq!(rs.rank(), 1);
1699        assert_eq!(rs.num_roots(), 2); // ±α
1700        assert_eq!(rs.num_positive_roots(), 1);
1701        assert_eq!(rs.dimension(), 3); // SU(2) dim = 1 + 2 = 3
1702
1703        let cartan = rs.cartan_matrix();
1704        assert_eq!(cartan.len(), 1);
1705        assert_eq!(cartan[0][0], 2); // ⟨α, α^∨⟩ = 2
1706    }
1707
1708    #[test]
1709    fn test_type_a2_su3() {
1710        let rs = RootSystem::type_a(2);
1711
1712        assert_eq!(rs.rank(), 2);
1713        assert_eq!(rs.num_roots(), 6); // 3² - 1 = 8 total, 6 nonzero differences
1714        assert_eq!(rs.num_positive_roots(), 3);
1715        assert_eq!(rs.dimension(), 8); // SU(3) dim = 2 + 6 = 8
1716
1717        let cartan = rs.cartan_matrix();
1718        assert_eq!(cartan.len(), 2);
1719        assert_eq!(cartan[0][0], 2);
1720        assert_eq!(cartan[1][1], 2);
1721        assert_eq!(cartan[0][1], -1); // Adjacent simple roots
1722        assert_eq!(cartan[1][0], -1);
1723    }
1724
1725    #[test]
1726    fn test_simple_roots_su3() {
1727        let rs = RootSystem::type_a(2);
1728        let simple = rs.simple_roots();
1729
1730        assert_eq!(simple.len(), 2);
1731
1732        // α₁ = e₁ - e₂ = (1, -1, 0)
1733        assert!((simple[0].coords[0] - 1.0).abs() < 1e-10);
1734        assert!((simple[0].coords[1] + 1.0).abs() < 1e-10);
1735        assert!((simple[0].coords[2]).abs() < 1e-10);
1736
1737        // α₂ = e₂ - e₃ = (0, 1, -1)
1738        assert!((simple[1].coords[0]).abs() < 1e-10);
1739        assert!((simple[1].coords[1] - 1.0).abs() < 1e-10);
1740        assert!((simple[1].coords[2] + 1.0).abs() < 1e-10);
1741    }
1742
1743    #[test]
1744    fn test_positive_roots_su3() {
1745        let rs = RootSystem::type_a(2);
1746        let positive = rs.positive_roots();
1747
1748        assert_eq!(positive.len(), 3);
1749        // Should have: e₁-e₂, e₂-e₃, e₁-e₃
1750    }
1751
1752    #[test]
1753    fn test_dominant_weight() {
1754        let rs = RootSystem::type_a(2);
1755
1756        // (1, 0, 0) should be dominant
1757        let weight1 = Root::new(vec![1.0, 0.0, 0.0]);
1758        assert!(rs.is_dominant_weight(&weight1));
1759
1760        // (-1, 0, 0) should not be dominant
1761        let weight2 = Root::new(vec![-1.0, 0.0, 0.0]);
1762        assert!(!rs.is_dominant_weight(&weight2));
1763    }
1764
1765    #[test]
1766    fn test_weight_lattice_su2() {
1767        let rs = RootSystem::type_a(1);
1768        let wl = WeightLattice::from_root_system(rs);
1769
1770        assert_eq!(wl.fundamental_weights().len(), 1);
1771
1772        // Spin-1 representation: Dynkin label [2]
1773        let weight = wl.dynkin_to_weight(&[2]);
1774        assert_eq!(weight.rank(), 2);
1775    }
1776
1777    #[test]
1778    fn test_weight_lattice_su3() {
1779        let rs = RootSystem::type_a(2);
1780        let wl = WeightLattice::from_root_system(rs);
1781
1782        assert_eq!(wl.fundamental_weights().len(), 2);
1783
1784        // Fundamental representation: Dynkin labels [1, 0]
1785        let fund = wl.dynkin_to_weight(&[1, 0]);
1786        assert_eq!(fund.rank(), 3);
1787
1788        // Adjoint representation: Dynkin labels [1, 1]
1789        let adj = wl.dynkin_to_weight(&[1, 1]);
1790        assert_eq!(adj.rank(), 3);
1791    }
1792
1793    #[test]
1794    fn test_weyl_orbit_su2() {
1795        let rs = RootSystem::type_a(1);
1796        let weight = Root::new(vec![1.0, 0.0]);
1797
1798        let orbit = rs.weyl_orbit(&weight);
1799        assert_eq!(orbit.len(), 2); // Weyl group S₂ has 2 elements
1800    }
1801
1802    #[test]
1803    fn test_weyl_dimension_formula() {
1804        let rs = RootSystem::type_a(2); // SU(3)
1805        let wl = WeightLattice::from_root_system(rs);
1806
1807        // Test known dimensions for SU(3)
1808        // Fundamental [1,0]: dim = 3
1809        let fund = wl.dynkin_to_weight(&[1, 0]);
1810        assert_eq!(wl.weyl_dimension(&fund), 3);
1811
1812        // Anti-fundamental [0,1]: dim = 3
1813        let antifund = wl.dynkin_to_weight(&[0, 1]);
1814        assert_eq!(wl.weyl_dimension(&antifund), 3);
1815
1816        // Adjoint [1,1]: dim = 8
1817        let adj = wl.dynkin_to_weight(&[1, 1]);
1818        assert_eq!(wl.weyl_dimension(&adj), 8);
1819
1820        // [2,0]: dim = 6
1821        let sym2 = wl.dynkin_to_weight(&[2, 0]);
1822        assert_eq!(wl.weyl_dimension(&sym2), 6);
1823
1824        // [0,2]: dim = 6
1825        let sym2_bar = wl.dynkin_to_weight(&[0, 2]);
1826        assert_eq!(wl.weyl_dimension(&sym2_bar), 6);
1827
1828        // [1,0,0,...] for SU(2) - trivial rep has dim = 1
1829        let rs2 = RootSystem::type_a(1); // SU(2)
1830        let wl2 = WeightLattice::from_root_system(rs2);
1831        let trivial = wl2.dynkin_to_weight(&[0]);
1832        assert_eq!(wl2.weyl_dimension(&trivial), 1);
1833
1834        // [2] for SU(2) - spin-1 has dim = 3
1835        let spin1 = wl2.dynkin_to_weight(&[2]);
1836        assert_eq!(wl2.weyl_dimension(&spin1), 3);
1837    }
1838
1839    #[test]
1840    fn test_weights_of_irrep_su3_fundamental() {
1841        let rs = RootSystem::type_a(2);
1842        let wl = WeightLattice::from_root_system(rs);
1843
1844        // Fundamental representation [1, 0] should have dimension 3
1845        let highest = wl.dynkin_to_weight(&[1, 0]);
1846        let weights = wl.weights_of_irrep(&highest);
1847
1848        // Check exact dimension (Weyl dimension formula)
1849        let expected_dim = wl.weyl_dimension(&highest);
1850        assert_eq!(expected_dim, 3, "Fundamental rep has dimension 3");
1851        assert_eq!(
1852            weights.len(),
1853            3,
1854            "Should find exactly 3 weights, found {}",
1855            weights.len()
1856        );
1857    }
1858
1859    #[test]
1860    fn test_weights_of_irrep_su3_adjoint() {
1861        let rs = RootSystem::type_a(2);
1862        let wl = WeightLattice::from_root_system(rs);
1863
1864        // Adjoint representation [1, 1] should have dimension 8
1865        let highest = wl.dynkin_to_weight(&[1, 1]);
1866        let weights = wl.weights_of_irrep(&highest);
1867
1868        // Check dimension (Weyl dimension formula)
1869        let dim = wl.weyl_dimension(&highest);
1870        assert_eq!(dim, 8, "Adjoint rep has dimension 8");
1871
1872        // With Kostant's formula, we now compute proper multiplicities
1873        // For SU(3) adjoint: 6 roots (mult 1) + 1 zero weight (mult 2) = dimension 8
1874        let total_dim: usize = weights.iter().map(|(_, m)| m).sum();
1875        assert_eq!(
1876            total_dim, 8,
1877            "Total dimension (sum of multiplicities) should be 8"
1878        );
1879    }
1880
1881    #[test]
1882    fn test_partition_function_basics() {
1883        // Test partition function on simple cases we can verify by hand
1884        let rs = RootSystem::type_a(1); // SU(2)
1885        let wl = WeightLattice::from_root_system(rs);
1886
1887        // SU(2) has one positive root: α = [1, -1]
1888        // Test P(0) = 1 (empty sum)
1889        let zero = Root::new(vec![0.0, 0.0]);
1890        assert_eq!(wl.kostant_partition_function(&zero), 1, "P(0) = 1");
1891
1892        // Test P(α) = 1 (just α itself)
1893        let alpha = Root::new(vec![1.0, -1.0]);
1894        assert_eq!(wl.kostant_partition_function(&alpha), 1, "P(α) = 1");
1895
1896        // Test P(2α) = 1 (just 2·α)
1897        let two_alpha = Root::new(vec![2.0, -2.0]);
1898        assert_eq!(wl.kostant_partition_function(&two_alpha), 1, "P(2α) = 1");
1899
1900        // Test P(-α) = 0 (negative, impossible)
1901        let neg_alpha = Root::new(vec![-1.0, 1.0]);
1902        assert_eq!(wl.kostant_partition_function(&neg_alpha), 0, "P(-α) = 0");
1903    }
1904
1905    #[test]
1906    fn test_partition_function_su3() {
1907        // SU(3) has 3 positive roots: α₁=[1,-1,0], α₂=[0,1,-1], α₁+α₂=[1,0,-1]
1908        let rs = RootSystem::type_a(2);
1909        let wl = WeightLattice::from_root_system(rs);
1910
1911        // Test P(0) = 1
1912        let zero = Root::new(vec![0.0, 0.0, 0.0]);
1913        assert_eq!(wl.kostant_partition_function(&zero), 1, "P(0) = 1");
1914
1915        // Test P(α₁) = 1
1916        let alpha1 = Root::new(vec![1.0, -1.0, 0.0]);
1917        let p_alpha1 = wl.kostant_partition_function(&alpha1);
1918        eprintln!("P(α₁) = {}", p_alpha1);
1919        assert_eq!(p_alpha1, 1, "P(α₁) = 1");
1920
1921        // Test P(α₁+α₂) = 2 (can write as α₁+α₂ OR as the root [1,0,-1])
1922        // Wait, [1,0,-1] IS α₁+α₂, so P(α₁+α₂) = 1
1923        let alpha1_plus_alpha2 = Root::new(vec![1.0, 0.0, -1.0]);
1924        let p_sum = wl.kostant_partition_function(&alpha1_plus_alpha2);
1925        eprintln!("P(α₁+α₂) = {}", p_sum);
1926        // This should be 1, not 2! Because [1,0,-1] is itself a positive root.
1927    }
1928
1929    #[test]
1930    fn test_kostant_dimension_invariant() {
1931        // Verify that Kostant's formula satisfies the dimension invariant:
1932        // Σ_λ m(λ) = dim(V_Λ) for any representation
1933        let rs = RootSystem::type_a(2); // SU(3)
1934        let wl = WeightLattice::from_root_system(rs);
1935
1936        // Test fundamental [1,0]: dimension 3
1937        let fund = wl.dynkin_to_weight(&[1, 0]);
1938        let fund_weights = wl.weights_of_irrep(&fund);
1939        eprintln!("Fundamental [1,0] weights:");
1940        for (w, m) in &fund_weights {
1941            eprintln!("  {:?} mult {}", w.coords, m);
1942        }
1943        let fund_dim: usize = fund_weights.iter().map(|(_, m)| m).sum();
1944        eprintln!("Total dimension: {} (expected 3)", fund_dim);
1945        assert_eq!(fund_dim, 3, "Fundamental rep dimension invariant");
1946
1947        // Test adjoint [1,1]: dimension 8
1948        let adj = wl.dynkin_to_weight(&[1, 1]);
1949        let adj_weights = wl.weights_of_irrep(&adj);
1950        eprintln!("\nAdjoint [1,1] weights:");
1951        for (w, m) in &adj_weights {
1952            eprintln!("  {:?} mult {}", w.coords, m);
1953        }
1954        let adj_dim: usize = adj_weights.iter().map(|(_, m)| m).sum();
1955        eprintln!("Total dimension: {} (expected 8)", adj_dim);
1956        assert_eq!(adj_dim, 8, "Adjoint rep dimension invariant");
1957
1958        // Test antifundamental [0,1]: dimension 3
1959        let anti = wl.dynkin_to_weight(&[0, 1]);
1960        let anti_weights = wl.weights_of_irrep(&anti);
1961        let anti_dim: usize = anti_weights.iter().map(|(_, m)| m).sum();
1962        assert_eq!(anti_dim, 3, "Antifundamental rep dimension invariant");
1963    }
1964
1965    #[test]
1966    fn test_weight_diagram_string_su3() {
1967        let rs = RootSystem::type_a(2);
1968        let wl = WeightLattice::from_root_system(rs);
1969
1970        let highest = wl.dynkin_to_weight(&[1, 0]);
1971        let diagram = wl.weight_diagram_string(&highest);
1972
1973        // Check that diagram contains expected elements
1974        assert!(diagram.contains("Weight diagram"));
1975        assert!(diagram.contains("Dimension"));
1976        assert!(diagram.contains("Weights"));
1977    }
1978
1979    #[test]
1980    fn test_weight_diagram_rank1_not_supported() {
1981        let rs = RootSystem::type_a(1);
1982        let wl = WeightLattice::from_root_system(rs);
1983
1984        let highest = wl.dynkin_to_weight(&[2]);
1985        let diagram = wl.weight_diagram_string(&highest);
1986
1987        assert!(diagram.contains("only implemented for rank 2"));
1988    }
1989
1990    // --- Cartan Subalgebra Tests ---
1991
1992    #[test]
1993    fn test_cartan_subalgebra_su2() {
1994        let cartan = CartanSubalgebra::type_a(1);
1995
1996        assert_eq!(cartan.dimension(), 1);
1997        assert_eq!(cartan.matrix_size(), 2);
1998        assert_eq!(cartan.basis_matrices().len(), 1);
1999
2000        // H₁ = diag(1, -1)
2001        let h1 = &cartan.basis_matrices()[0];
2002        assert!((h1[(0, 0)].re - 1.0).abs() < 1e-10);
2003        assert!((h1[(1, 1)].re + 1.0).abs() < 1e-10);
2004        assert!(h1[(0, 1)].norm() < 1e-10);
2005        assert!(h1[(1, 0)].norm() < 1e-10);
2006    }
2007
2008    #[test]
2009    fn test_cartan_subalgebra_su3() {
2010        let cartan = CartanSubalgebra::type_a(2);
2011
2012        assert_eq!(cartan.dimension(), 2);
2013        assert_eq!(cartan.matrix_size(), 3);
2014        assert_eq!(cartan.basis_matrices().len(), 2);
2015
2016        // H₁ = diag(1, -1, 0)
2017        let h1 = &cartan.basis_matrices()[0];
2018        assert!((h1[(0, 0)].re - 1.0).abs() < 1e-10);
2019        assert!((h1[(1, 1)].re + 1.0).abs() < 1e-10);
2020        assert!(h1[(2, 2)].norm() < 1e-10);
2021
2022        // H₂ = diag(0, 1, -1)
2023        let h2 = &cartan.basis_matrices()[1];
2024        assert!(h2[(0, 0)].norm() < 1e-10);
2025        assert!((h2[(1, 1)].re - 1.0).abs() < 1e-10);
2026        assert!((h2[(2, 2)].re + 1.0).abs() < 1e-10);
2027    }
2028
2029    #[test]
2030    fn test_cartan_basis_commutes() {
2031        let cartan = CartanSubalgebra::type_a(2);
2032
2033        // [H₁, H₂] = 0 (diagonal matrices commute)
2034        let h1 = &cartan.basis_matrices()[0];
2035        let h2 = &cartan.basis_matrices()[1];
2036
2037        let commutator = h1.dot(h2) - h2.dot(h1);
2038
2039        for val in commutator.iter() {
2040            assert!(val.norm() < 1e-10, "Cartan basis elements must commute");
2041        }
2042    }
2043
2044    #[test]
2045    fn test_cartan_dual_basis() {
2046        let cartan = CartanSubalgebra::type_a(2);
2047        let dual = cartan.dual_basis();
2048
2049        assert_eq!(dual.len(), 2);
2050
2051        // α₁ = e₁ - e₂
2052        assert!((dual[0].coords[0] - 1.0).abs() < 1e-10);
2053        assert!((dual[0].coords[1] + 1.0).abs() < 1e-10);
2054        assert!(dual[0].coords[2].abs() < 1e-10);
2055
2056        // α₂ = e₂ - e₃
2057        assert!(dual[1].coords[0].abs() < 1e-10);
2058        assert!((dual[1].coords[1] - 1.0).abs() < 1e-10);
2059        assert!((dual[1].coords[2] + 1.0).abs() < 1e-10);
2060
2061        // These should be the simple roots
2062        let rs = RootSystem::type_a(2);
2063        let simple_roots = rs.simple_roots();
2064
2065        for (d, s) in dual.iter().zip(simple_roots) {
2066            for (dc, sc) in d.coords.iter().zip(&s.coords) {
2067                assert!((dc - sc).abs() < 1e-10);
2068            }
2069        }
2070    }
2071
2072    #[test]
2073    fn test_cartan_evaluate_root() {
2074        let cartan = CartanSubalgebra::type_a(1);
2075
2076        // For SU(2), H = c₁ · diag(1, -1)
2077        // Root α = (1, -1) (the simple root)
2078        let root = Root::new(vec![1.0, -1.0]);
2079
2080        // α(H) = 1·c₁ + (-1)·(-c₁) = 2c₁
2081        let result = cartan.evaluate_root(&root, &[1.0]);
2082        assert!((result.re - 2.0).abs() < 1e-10);
2083        assert!(result.im.abs() < 1e-10);
2084
2085        let result2 = cartan.evaluate_root(&root, &[0.5]);
2086        assert!((result2.re - 1.0).abs() < 1e-10);
2087    }
2088
2089    #[test]
2090    fn test_cartan_from_coefficients() {
2091        let cartan = CartanSubalgebra::type_a(2);
2092
2093        // H = 2H₁ + 3H₂
2094        let h = cartan.from_coefficients(&[2.0, 3.0]);
2095
2096        // Should be diag(2, -2+3, -3) = diag(2, 1, -3)
2097        assert!((h[(0, 0)].re - 2.0).abs() < 1e-10);
2098        assert!((h[(1, 1)].re - 1.0).abs() < 1e-10);
2099        assert!((h[(2, 2)].re + 3.0).abs() < 1e-10);
2100
2101        // Check traceless
2102        let trace: Complex64 = (0..3).map(|i| h[(i, i)]).sum();
2103        assert!(trace.norm() < 1e-10);
2104    }
2105
2106    #[test]
2107    fn test_cartan_project_matrix() {
2108        let cartan = CartanSubalgebra::type_a(2);
2109
2110        // Create a diagonal matrix
2111        let mut mat = Array2::<Complex64>::zeros((3, 3));
2112        mat[(0, 0)] = Complex64::new(1.0, 0.0);
2113        mat[(1, 1)] = Complex64::new(-0.5, 0.0);
2114        mat[(2, 2)] = Complex64::new(-0.5, 0.0);
2115
2116        let coeffs = cartan
2117            .project_matrix(&mat)
2118            .expect("Rank 2 should be supported");
2119        assert_eq!(coeffs.len(), 2);
2120
2121        // Reconstruct and check
2122        let reconstructed = cartan.from_coefficients(&coeffs);
2123
2124        // Should reconstruct exactly for diagonal traceless matrices
2125        for i in 0..3 {
2126            let diff = (mat[(i, i)] - reconstructed[(i, i)]).norm();
2127            assert!(
2128                diff < 1e-10,
2129                "Diagonal projection should be exact: diff at ({},{}) = {}",
2130                i,
2131                i,
2132                diff
2133            );
2134        }
2135    }
2136
2137    #[test]
2138    fn test_cartan_contains() {
2139        let cartan = CartanSubalgebra::type_a(2);
2140
2141        // Diagonal traceless matrix - should be in Cartan
2142        let mut h = Array2::<Complex64>::zeros((3, 3));
2143        h[(0, 0)] = Complex64::new(1.0, 0.0);
2144        h[(1, 1)] = Complex64::new(-0.5, 0.0);
2145        h[(2, 2)] = Complex64::new(-0.5, 0.0);
2146
2147        assert!(cartan.contains(&h, 1e-6));
2148
2149        // Non-diagonal matrix - should not be in Cartan
2150        let mut not_h = Array2::<Complex64>::zeros((3, 3));
2151        not_h[(0, 1)] = Complex64::new(1.0, 0.0);
2152
2153        assert!(!cartan.contains(&not_h, 1e-6));
2154
2155        // Diagonal but not traceless - should not be in Cartan
2156        let mut not_traceless = Array2::<Complex64>::zeros((3, 3));
2157        not_traceless[(0, 0)] = Complex64::new(1.0, 0.0);
2158
2159        assert!(!cartan.contains(&not_traceless, 1e-6));
2160    }
2161
2162    #[test]
2163    fn test_cartan_killing_form() {
2164        let cartan = CartanSubalgebra::type_a(1);
2165
2166        // For SU(2), κ(H, H) = 2(2)·2 = 8 for H = H₁
2167        let killing = cartan.killing_form(&[1.0], &[1.0]);
2168        assert!((killing - 8.0).abs() < 1e-10);
2169
2170        // κ(H, 0) = 0
2171        let killing_zero = cartan.killing_form(&[1.0], &[0.0]);
2172        assert!(killing_zero.abs() < 1e-10);
2173
2174        // κ is symmetric
2175        let k12 = cartan.killing_form(&[1.0], &[2.0]);
2176        let k21 = cartan.killing_form(&[2.0], &[1.0]);
2177        assert!((k12 - k21).abs() < 1e-10);
2178    }
2179
2180    #[test]
2181    fn test_cartan_killing_form_su3() {
2182        let cartan = CartanSubalgebra::type_a(2);
2183
2184        // For SU(3), κ(Hᵢ, Hⱼ) = 2(3)·2δᵢⱼ = 12δᵢⱼ
2185        let k11 = cartan.killing_form(&[1.0, 0.0], &[1.0, 0.0]);
2186        assert!((k11 - 12.0).abs() < 1e-10);
2187
2188        let k22 = cartan.killing_form(&[0.0, 1.0], &[0.0, 1.0]);
2189        assert!((k22 - 12.0).abs() < 1e-10);
2190
2191        // Off-diagonal should be zero for orthogonal basis
2192        let k12 = cartan.killing_form(&[1.0, 0.0], &[0.0, 1.0]);
2193        assert!(k12.abs() < 1e-10);
2194    }
2195
2196    #[test]
2197    fn test_weyl_chamber_contains_su2() {
2198        let rs = RootSystem::type_a(1); // SU(2)
2199        let chamber = WeylChamber::fundamental(&rs);
2200
2201        // Simple root α = (1, -1)
2202        // Dominant weights satisfy ⟨λ, α⟩ ≥ 0
2203
2204        // Dominant weight: (1, 0)
2205        let dominant = Root::new(vec![1.0, 0.0]);
2206        assert!(chamber.contains(&dominant, false));
2207        assert!(chamber.contains(&dominant, true));
2208
2209        // Non-dominant: (-1, 0)
2210        let non_dominant = Root::new(vec![-1.0, 0.0]);
2211        assert!(!chamber.contains(&non_dominant, false));
2212
2213        // Boundary: (0, 0)
2214        let boundary = Root::new(vec![0.0, 0.0]);
2215        assert!(chamber.contains(&boundary, false)); // Non-strict allows boundary
2216        assert!(!chamber.contains(&boundary, true)); // Strict excludes boundary
2217    }
2218
2219    #[test]
2220    fn test_weyl_chamber_contains_su3() {
2221        let rs = RootSystem::type_a(2); // SU(3)
2222        let chamber = WeylChamber::fundamental(&rs);
2223
2224        // Simple roots: α₁ = (1, -1, 0), α₂ = (0, 1, -1)
2225        // Dominant: ⟨λ, α₁⟩ ≥ 0 and ⟨λ, α₂⟩ ≥ 0
2226
2227        // Fundamental weight ω₁ = (1, 0, 0) - 1/3 for traceless
2228        let omega1 = Root::new(vec![2.0 / 3.0, -1.0 / 3.0, -1.0 / 3.0]);
2229        assert!(chamber.contains(&omega1, false));
2230
2231        // Negative weight
2232        let neg = Root::new(vec![-1.0, 0.0, 1.0]);
2233        assert!(!chamber.contains(&neg, false));
2234
2235        // Origin
2236        let origin = Root::new(vec![0.0, 0.0, 0.0]);
2237        assert!(chamber.contains(&origin, false));
2238        assert!(!chamber.contains(&origin, true));
2239    }
2240
2241    #[test]
2242    fn test_weyl_chamber_project() {
2243        let rs = RootSystem::type_a(1); // SU(2)
2244        let chamber = WeylChamber::fundamental(&rs);
2245
2246        // Already dominant
2247        let dominant = Root::new(vec![1.0, -1.0]);
2248        let projected = chamber.project(&dominant);
2249        assert!((projected.coords[0] - dominant.coords[0]).abs() < 1e-10);
2250        assert!((projected.coords[1] - dominant.coords[1]).abs() < 1e-10);
2251
2252        // Non-dominant: should reflect to dominant
2253        let non_dominant = Root::new(vec![-1.0, 1.0]);
2254        let projected = chamber.project(&non_dominant);
2255
2256        // After reflection by α = (1, -1), should get (1, -1)
2257        assert!(chamber.contains(&projected, false));
2258        assert!((projected.coords[0] - 1.0).abs() < 1e-10);
2259        assert!((projected.coords[1] + 1.0).abs() < 1e-10);
2260    }
2261
2262    #[test]
2263    fn test_weyl_chamber_project_su3() {
2264        let rs = RootSystem::type_a(2); // SU(3)
2265        let chamber = WeylChamber::fundamental(&rs);
2266
2267        // Non-dominant weight
2268        let lambda = Root::new(vec![-1.0, 2.0, -1.0]);
2269        let projected = chamber.project(&lambda);
2270
2271        // Projected weight should be dominant
2272        assert!(chamber.contains(&projected, false));
2273
2274        // Check that projection preserves norm (Weyl group is orthogonal)
2275        let norm_before = lambda.norm_squared();
2276        let norm_after = projected.norm_squared();
2277        assert!((norm_before - norm_after).abs() < 1e-8);
2278    }
2279
2280    #[test]
2281    fn test_alcove_contains_su2() {
2282        let rs = RootSystem::type_a(1); // SU(2)
2283        let alcove = Alcove::fundamental(&rs);
2284
2285        // Highest root for SU(2): θ = (1, -1) (same as simple root)
2286        // Alcove: ⟨λ, α⟩ ≥ 0 and ⟨λ, θ⟩ ≤ 1
2287
2288        // Inside alcove: (0.4, -0.4)
2289        let inside = Root::new(vec![0.4, -0.4]);
2290        // ⟨inside, α⟩ = 0.4·1 + (-0.4)·(-1) = 0.8 ≥ 0 ✓
2291        // ⟨inside, θ⟩ = 0.4·1 + (-0.4)·(-1) = 0.8 ≤ 1 ✓
2292        assert!(alcove.contains(&inside, false));
2293
2294        // Outside alcove (too large): (1, -1)
2295        let outside = Root::new(vec![1.0, -1.0]);
2296        // ⟨outside, θ⟩ = 2 > 1
2297        assert!(!alcove.contains(&outside, false));
2298
2299        // Boundary: origin
2300        let origin = Root::new(vec![0.0, 0.0]);
2301        assert!(alcove.contains(&origin, false));
2302        assert!(!alcove.contains(&origin, true)); // Strict excludes boundary
2303    }
2304
2305    #[test]
2306    fn test_alcove_vertices_su2() {
2307        let rs = RootSystem::type_a(1); // SU(2)
2308        let alcove = Alcove::fundamental(&rs);
2309
2310        let vertices = alcove.vertices();
2311
2312        // For SU(2), fundamental alcove has 2 vertices: 0 and ω₁
2313        assert_eq!(vertices.len(), 2);
2314
2315        // First vertex: origin
2316        assert!(vertices[0].coords[0].abs() < 1e-10);
2317        assert!(vertices[0].coords[1].abs() < 1e-10);
2318
2319        // Second vertex: fundamental weight ω₁ = (1/2, -1/2)
2320        assert!((vertices[1].coords[0] - 0.5).abs() < 1e-10);
2321        assert!((vertices[1].coords[1] + 0.5).abs() < 1e-10);
2322    }
2323
2324    #[test]
2325    fn test_alcove_vertices_su3() {
2326        let rs = RootSystem::type_a(2); // SU(3)
2327        let alcove = Alcove::fundamental(&rs);
2328
2329        let vertices = alcove.vertices();
2330
2331        // For SU(3), fundamental alcove has 3 vertices: 0, ω₁, ω₂
2332        assert_eq!(vertices.len(), 3);
2333
2334        // First vertex: origin
2335        for &coord in &vertices[0].coords {
2336            assert!(coord.abs() < 1e-10);
2337        }
2338
2339        // Verify all vertices are in the alcove
2340        for vertex in &vertices {
2341            assert!(alcove.contains(vertex, false));
2342        }
2343    }
2344
2345    #[test]
2346    fn test_alcove_at_level() {
2347        let rs = RootSystem::type_a(1); // SU(2)
2348        let alcove_k2 = Alcove::at_level(&rs, 2.0);
2349
2350        assert_eq!(alcove_k2.level(), 2.0);
2351
2352        // At level 2, the upper bound is ⟨λ, θ⟩ ≤ 2
2353        let inside = Root::new(vec![0.8, -0.8]);
2354        // ⟨inside, θ⟩ = 1.6 ≤ 2 ✓
2355        assert!(alcove_k2.contains(&inside, false));
2356
2357        let outside = Root::new(vec![1.5, -1.5]);
2358        // ⟨outside, θ⟩ = 3 > 2
2359        assert!(!alcove_k2.contains(&outside, false));
2360    }
2361
2362    #[test]
2363    fn test_alcove_highest_root() {
2364        let rs = RootSystem::type_a(2); // SU(3)
2365        let alcove = Alcove::fundamental(&rs);
2366
2367        let theta = alcove.highest_root();
2368
2369        // For SU(3), highest root is θ = α₁ + α₂ = (1, 0, -1)
2370        assert!((theta.coords[0] - 1.0).abs() < 1e-10);
2371        assert!(theta.coords[1].abs() < 1e-10);
2372        assert!((theta.coords[2] + 1.0).abs() < 1e-10);
2373
2374        // Verify it's the longest root
2375        let theta_norm = theta.norm_squared();
2376        for root in rs.positive_roots() {
2377            assert!(root.norm_squared() <= theta_norm + 1e-10);
2378        }
2379    }
2380
2381    // --- Simple Root Expansion Tests ---
2382
2383    #[test]
2384    fn test_simple_root_expansion_su2() {
2385        let rs = RootSystem::type_a(1); // SU(2)
2386
2387        // SU(2) has one simple root: α₁ = e₀ - e₁ = (1, -1)
2388        // The only positive root is α₁ itself
2389
2390        // Test the simple root itself: α₁ = 1·α₁
2391        let alpha1 = Root::new(vec![1.0, -1.0]);
2392        let coeffs = rs.simple_root_expansion(&alpha1);
2393        assert_eq!(coeffs, Some(vec![1]));
2394
2395        // Test the negative root: -α₁ = (-1)·α₁
2396        let neg_alpha1 = Root::new(vec![-1.0, 1.0]);
2397        let coeffs_neg = rs.simple_root_expansion(&neg_alpha1);
2398        assert_eq!(coeffs_neg, Some(vec![-1]));
2399    }
2400
2401    #[test]
2402    fn test_simple_root_expansion_su3() {
2403        let rs = RootSystem::type_a(2); // SU(3)
2404
2405        // SU(3) has 2 simple roots:
2406        // α₁ = e₀ - e₁ = (1, -1, 0)
2407        // α₂ = e₁ - e₂ = (0, 1, -1)
2408        // And a third positive root: α₁ + α₂ = e₀ - e₂ = (1, 0, -1)
2409
2410        // Test α₁ = (1, -1, 0): coefficients [1, 0]
2411        let alpha1 = Root::new(vec![1.0, -1.0, 0.0]);
2412        assert_eq!(rs.simple_root_expansion(&alpha1), Some(vec![1, 0]));
2413
2414        // Test α₂ = (0, 1, -1): coefficients [0, 1]
2415        let alpha2 = Root::new(vec![0.0, 1.0, -1.0]);
2416        assert_eq!(rs.simple_root_expansion(&alpha2), Some(vec![0, 1]));
2417
2418        // Test α₁ + α₂ = (1, 0, -1): coefficients [1, 1]
2419        let alpha1_plus_alpha2 = Root::new(vec![1.0, 0.0, -1.0]);
2420        assert_eq!(
2421            rs.simple_root_expansion(&alpha1_plus_alpha2),
2422            Some(vec![1, 1])
2423        );
2424
2425        // Test negative roots
2426        let neg_alpha1 = Root::new(vec![-1.0, 1.0, 0.0]);
2427        assert_eq!(rs.simple_root_expansion(&neg_alpha1), Some(vec![-1, 0]));
2428
2429        let neg_highest = Root::new(vec![-1.0, 0.0, 1.0]);
2430        assert_eq!(rs.simple_root_expansion(&neg_highest), Some(vec![-1, -1]));
2431    }
2432
2433    #[test]
2434    fn test_simple_root_expansion_su4() {
2435        let rs = RootSystem::type_a(3); // SU(4)
2436
2437        // SU(4) has 3 simple roots:
2438        // α₁ = e₀ - e₁ = (1, -1, 0, 0)
2439        // α₂ = e₁ - e₂ = (0, 1, -1, 0)
2440        // α₃ = e₂ - e₃ = (0, 0, 1, -1)
2441
2442        // Test simple roots
2443        let alpha1 = Root::new(vec![1.0, -1.0, 0.0, 0.0]);
2444        assert_eq!(rs.simple_root_expansion(&alpha1), Some(vec![1, 0, 0]));
2445
2446        let alpha2 = Root::new(vec![0.0, 1.0, -1.0, 0.0]);
2447        assert_eq!(rs.simple_root_expansion(&alpha2), Some(vec![0, 1, 0]));
2448
2449        let alpha3 = Root::new(vec![0.0, 0.0, 1.0, -1.0]);
2450        assert_eq!(rs.simple_root_expansion(&alpha3), Some(vec![0, 0, 1]));
2451
2452        // Test e₀ - e₂ = α₁ + α₂: coefficients [1, 1, 0]
2453        let e0_minus_e2 = Root::new(vec![1.0, 0.0, -1.0, 0.0]);
2454        assert_eq!(rs.simple_root_expansion(&e0_minus_e2), Some(vec![1, 1, 0]));
2455
2456        // Test e₁ - e₃ = α₂ + α₃: coefficients [0, 1, 1]
2457        let e1_minus_e3 = Root::new(vec![0.0, 1.0, 0.0, -1.0]);
2458        assert_eq!(rs.simple_root_expansion(&e1_minus_e3), Some(vec![0, 1, 1]));
2459
2460        // Test highest root e₀ - e₃ = α₁ + α₂ + α₃: coefficients [1, 1, 1]
2461        let highest = Root::new(vec![1.0, 0.0, 0.0, -1.0]);
2462        assert_eq!(rs.simple_root_expansion(&highest), Some(vec![1, 1, 1]));
2463
2464        // Test negative of highest root: coefficients [-1, -1, -1]
2465        let neg_highest = Root::new(vec![-1.0, 0.0, 0.0, 1.0]);
2466        assert_eq!(
2467            rs.simple_root_expansion(&neg_highest),
2468            Some(vec![-1, -1, -1])
2469        );
2470    }
2471
2472    #[test]
2473    fn test_simple_root_expansion_not_a_root() {
2474        let rs = RootSystem::type_a(2); // SU(3)
2475
2476        // Test a vector that is not a root
2477        let not_a_root = Root::new(vec![1.0, 1.0, -2.0]);
2478        assert_eq!(rs.simple_root_expansion(&not_a_root), None);
2479
2480        // Test zero vector
2481        let zero = Root::new(vec![0.0, 0.0, 0.0]);
2482        assert_eq!(rs.simple_root_expansion(&zero), None);
2483    }
2484
2485    #[test]
2486    fn test_simple_root_expansion_roundtrip() {
2487        // Verify that we can reconstruct roots from their expansions
2488        let rs = RootSystem::type_a(2); // SU(3)
2489
2490        for root in rs.roots() {
2491            let coeffs = rs.simple_root_expansion(root);
2492            assert!(coeffs.is_some(), "All roots should have expansions");
2493
2494            let coeffs = coeffs.unwrap();
2495            let simple = rs.simple_roots();
2496
2497            // Reconstruct: Σ cᵢ αᵢ
2498            let mut reconstructed = vec![0.0; root.coords.len()];
2499            for (i, &c) in coeffs.iter().enumerate() {
2500                for (j, &coord) in simple[i].coords.iter().enumerate() {
2501                    reconstructed[j] += (c as f64) * coord;
2502                }
2503            }
2504
2505            // Compare with original
2506            for (orig, recon) in root.coords.iter().zip(&reconstructed) {
2507                assert!(
2508                    (orig - recon).abs() < 1e-10,
2509                    "Reconstruction failed for root {:?}: expected {:?}, got {:?}",
2510                    root.coords,
2511                    root.coords,
2512                    reconstructed
2513                );
2514            }
2515        }
2516    }
2517}