Skip to main content

lie_groups/
su2.rs

1//! SU(2): The Special Unitary Group in 2 Dimensions
2//!
3//! This module implements SU(2) group elements using **2×2 complex matrices**.
4//! We define the Pauli generators directly (no external dependencies needed).
5//!
6//! # SU(2) Implementations
7//!
8//! This library provides multiple implementations of SU(2):
9//!
10//! ## Matrix-Based (this module)
11//!
12//! - **`SU2`**: Specialized 2×2 complex matrix implementation
13//!   - Uses `ndarray` for matrix operations
14//!   - Has convenient constructors (`rotation_x`, `rotation_y`, `rotation_z`)
15//!   - Good for learning, verification, and physics applications
16//!
17//! - **`SU2Generic`** ([`crate::sun::SUN<2>`]): Generic N×N matrix implementation
18//!   - Part of the unified SU(N) framework
19//!   - Same performance as `SU2` (both use matrix multiplication)
20//!   - Useful when writing code generic over SU(N)
21//!
22//! ## Quaternion-Based
23//!
24//! - **[`UnitQuaternion`](crate::UnitQuaternion)**: Quaternion representation
25//!   - Uses SU(2) ≅ S³ isomorphism (4 real numbers)
26//!   - **~3× faster** for multiplication than matrix representation
27//!   - Better numerical stability (no matrix decomposition)
28//!   - Recommended for performance-critical rotation computations
29//!
30//! # Performance Comparison
31//!
32//! Benchmarked on typical hardware (multiplication, 1000 ops):
33//! ```text
34//! UnitQuaternion: ~16 µs  (4 real multiplies + adds)
35//! SU2 (matrix):   ~45 µs  (complex 2×2 matrix multiply)
36//! ```
37//!
38//! For applications requiring compatibility with higher SU(N) groups (e.g.,
39//! Wilson loops, parallel transport), the matrix representation is preferred.
40//!
41//! ## Example
42//!
43//! ```rust
44//! use lie_groups::{SU2, LieGroup};
45//!
46//! // SU2 has specialized constructors:
47//! let g1 = SU2::rotation_x(0.5);
48//! let g2 = SU2::rotation_y(0.3);
49//!
50//! // Compose rotations
51//! let g3 = g1.compose(&g2);
52//! ```
53
54use crate::traits::{AntiHermitianByConstruction, LieAlgebra, LieGroup, TracelessByConstruction};
55use ndarray::Array2;
56use num_complex::Complex64;
57use std::fmt;
58use std::ops::{Add, Mul, MulAssign, Neg, Sub};
59
60// ============================================================================
61// Numerical Tolerance Constants
62// ============================================================================
63//
64// These thresholds are chosen based on IEEE 754 f64 precision (ε ≈ 2.2e-16)
65// and practical considerations for accumulated floating-point error.
66//
67// For SU(2) matrices, unitarity ensures |Re(Tr(U))/2| ≤ 1 exactly.
68// Violations indicate matrix corruption (drift from the group manifold).
69//
70// Reference: Higham, "Accuracy and Stability of Numerical Algorithms" (2002)
71
72/// Threshold for detecting unitarity violations.
73/// If |cos(θ/2)| exceeds 1 by more than this, the matrix has drifted from SU(2).
74const UNITARITY_VIOLATION_THRESHOLD: f64 = 1e-10;
75
76/// Threshold for small angle detection in `exp()`.
77/// Below this, return identity to avoid division by near-zero.
78const SMALL_ANGLE_EXP_THRESHOLD: f64 = 1e-10;
79
80/// Threshold for sin(θ/2) in `log()` axis extraction.
81/// Below this, the rotation axis cannot be reliably determined.
82const SIN_HALF_THETA_THRESHOLD: f64 = 1e-10;
83
84/// Lie algebra su(2) ≅ ℝ³
85///
86/// The Lie algebra of SU(2) consists of 2×2 traceless anti-Hermitian matrices.
87/// We represent these using 3 real coordinates.
88///
89/// # Convention
90///
91/// We identify su(2) with ℝ³ via the basis `{e₀, e₁, e₂} = {iσₓ/2, iσᵧ/2, iσᵤ/2}`,
92/// where σᵢ are the Pauli matrices:
93/// ```text
94/// σₓ = [[0, 1], [1, 0]]    e₀ = iσₓ/2 = [[0, i/2], [i/2, 0]]
95/// σᵧ = [[0, -i], [i, 0]]   e₁ = iσᵧ/2 = [[0, 1/2], [-1/2, 0]]
96/// σᵤ = [[1, 0], [0, -1]]   e₂ = iσᵤ/2 = [[i/2, 0], [0, -i/2]]
97/// ```
98///
99/// An element `Su2Algebra([a, b, c])` corresponds to the matrix
100/// `(a·iσₓ + b·iσᵧ + c·iσᵤ)/2`, and the parameter `‖(a,b,c)‖` is the
101/// rotation angle in the exponential map.
102///
103/// # Structure Constants
104///
105/// With this basis, the Lie bracket satisfies `[eᵢ, eⱼ] = -εᵢⱼₖ eₖ`, giving
106/// structure constants `fᵢⱼₖ = -εᵢⱼₖ` (negative Levi-Civita symbol).
107/// In ℝ³ coordinates, `[X, Y] = -(X × Y)`.
108///
109/// # Isomorphism with ℝ³
110///
111/// su(2) is isomorphic to ℝ³ as a vector space, and as a Lie algebra
112/// the bracket is the negative cross product. The norm `‖v‖` equals the
113/// rotation angle θ, matching the exponential map
114/// `exp(v) = cos(θ/2)I + i·sin(θ/2)·v̂·σ`.
115///
116/// # Examples
117///
118/// ```
119/// use lie_groups::su2::Su2Algebra;
120/// use lie_groups::traits::LieAlgebra;
121///
122/// // Create algebra element in X direction
123/// let v = Su2Algebra::from_components(&[1.0, 0.0, 0.0]);
124///
125/// // Scale and add
126/// let w = v.scale(2.0);
127/// let sum = v.add(&w);
128/// ```
129#[derive(Clone, Copy, Debug, PartialEq)]
130pub struct Su2Algebra(pub [f64; 3]);
131
132impl Add for Su2Algebra {
133    type Output = Self;
134    fn add(self, rhs: Self) -> Self {
135        Self([
136            self.0[0] + rhs.0[0],
137            self.0[1] + rhs.0[1],
138            self.0[2] + rhs.0[2],
139        ])
140    }
141}
142
143impl Add<&Su2Algebra> for Su2Algebra {
144    type Output = Su2Algebra;
145    fn add(self, rhs: &Su2Algebra) -> Su2Algebra {
146        Self([
147            self.0[0] + rhs.0[0],
148            self.0[1] + rhs.0[1],
149            self.0[2] + rhs.0[2],
150        ])
151    }
152}
153
154impl Add<Su2Algebra> for &Su2Algebra {
155    type Output = Su2Algebra;
156    fn add(self, rhs: Su2Algebra) -> Su2Algebra {
157        Su2Algebra([
158            self.0[0] + rhs.0[0],
159            self.0[1] + rhs.0[1],
160            self.0[2] + rhs.0[2],
161        ])
162    }
163}
164
165impl Add<&Su2Algebra> for &Su2Algebra {
166    type Output = Su2Algebra;
167    fn add(self, rhs: &Su2Algebra) -> Su2Algebra {
168        Su2Algebra([
169            self.0[0] + rhs.0[0],
170            self.0[1] + rhs.0[1],
171            self.0[2] + rhs.0[2],
172        ])
173    }
174}
175
176impl Sub for Su2Algebra {
177    type Output = Self;
178    fn sub(self, rhs: Self) -> Self {
179        Self([
180            self.0[0] - rhs.0[0],
181            self.0[1] - rhs.0[1],
182            self.0[2] - rhs.0[2],
183        ])
184    }
185}
186
187impl Neg for Su2Algebra {
188    type Output = Self;
189    fn neg(self) -> Self {
190        Self([-self.0[0], -self.0[1], -self.0[2]])
191    }
192}
193
194impl Mul<f64> for Su2Algebra {
195    type Output = Self;
196    fn mul(self, scalar: f64) -> Self {
197        Self([self.0[0] * scalar, self.0[1] * scalar, self.0[2] * scalar])
198    }
199}
200
201impl Mul<Su2Algebra> for f64 {
202    type Output = Su2Algebra;
203    fn mul(self, rhs: Su2Algebra) -> Su2Algebra {
204        rhs * self
205    }
206}
207
208impl LieAlgebra for Su2Algebra {
209    const DIM: usize = 3;
210
211    #[inline]
212    fn zero() -> Self {
213        Self([0.0, 0.0, 0.0])
214    }
215
216    #[inline]
217    fn add(&self, other: &Self) -> Self {
218        Self([
219            self.0[0] + other.0[0],
220            self.0[1] + other.0[1],
221            self.0[2] + other.0[2],
222        ])
223    }
224
225    #[inline]
226    fn scale(&self, scalar: f64) -> Self {
227        Self([self.0[0] * scalar, self.0[1] * scalar, self.0[2] * scalar])
228    }
229
230    #[inline]
231    fn norm(&self) -> f64 {
232        (self.0[0].powi(2) + self.0[1].powi(2) + self.0[2].powi(2)).sqrt()
233    }
234
235    #[inline]
236    fn basis_element(i: usize) -> Self {
237        assert!(i < 3, "SU(2) algebra is 3-dimensional");
238        let mut v = [0.0; 3];
239        v[i] = 1.0;
240        Self(v)
241    }
242
243    #[inline]
244    fn from_components(components: &[f64]) -> Self {
245        assert_eq!(components.len(), 3, "su(2) has dimension 3");
246        Self([components[0], components[1], components[2]])
247    }
248
249    #[inline]
250    fn to_components(&self) -> Vec<f64> {
251        self.0.to_vec()
252    }
253
254    /// Lie bracket for su(2): [X, Y] = -(X × Y)
255    ///
256    /// # Convention
257    ///
258    /// We represent su(2) as ℝ³ with basis `{eᵢ} = {iσᵢ/2}`. The matrix
259    /// commutator gives:
260    /// ```text
261    /// [iσᵢ/2, iσⱼ/2] = (i²/4)[σᵢ, σⱼ] = -(1/4)(2iεᵢⱼₖσₖ) = -εᵢⱼₖ(iσₖ/2)
262    /// ```
263    ///
264    /// In ℝ³ coordinates, this is the **negative cross product**:
265    /// ```text
266    /// [X, Y] = -(X × Y)
267    /// ```
268    ///
269    /// The negative sign is the unique bracket consistent with the half-angle
270    /// exponential map `exp(v) = cos(‖v‖/2)I + i·sin(‖v‖/2)·v̂·σ`, ensuring
271    /// the BCH formula `exp(X)·exp(Y) = exp(X + Y - ½(X×Y) + ...)` holds.
272    ///
273    /// # Properties
274    ///
275    /// - Structure constants: `fᵢⱼₖ = -εᵢⱼₖ`
276    /// - Antisymmetric: `[X, Y] = -[Y, X]`
277    /// - Jacobi identity: `[X, [Y, Z]] + [Y, [Z, X]] + [Z, [X, Y]] = 0`
278    /// - Killing form: `B(X, Y) = -2(X · Y)`
279    ///
280    /// # Examples
281    ///
282    /// ```
283    /// use lie_groups::Su2Algebra;
284    /// use lie_groups::LieAlgebra;
285    ///
286    /// let e1 = Su2Algebra::basis_element(0);  // (1, 0, 0)
287    /// let e2 = Su2Algebra::basis_element(1);  // (0, 1, 0)
288    /// let bracket = e1.bracket(&e2);          // [e₁, e₂] = -e₃
289    ///
290    /// // Should give -e₃ = (0, 0, -1)
291    /// assert!((bracket.0[0]).abs() < 1e-10);
292    /// assert!((bracket.0[1]).abs() < 1e-10);
293    /// assert!((bracket.0[2] - (-1.0)).abs() < 1e-10);
294    /// ```
295    #[inline]
296    fn bracket(&self, other: &Self) -> Self {
297        // Negative cross product: -(X × Y)
298        // Consistent with exp convention exp(v) = exp_matrix(iv·σ/2)
299        let x = self.0;
300        let y = other.0;
301        Self([
302            -(x[1] * y[2] - x[2] * y[1]),
303            -(x[2] * y[0] - x[0] * y[2]),
304            -(x[0] * y[1] - x[1] * y[0]),
305        ])
306    }
307}
308
309// ============================================================================
310// Casimir Operators for SU(2)
311// ============================================================================
312
313impl crate::Casimir for Su2Algebra {
314    type Representation = crate::representation::Spin;
315
316    #[inline]
317    fn quadratic_casimir_eigenvalue(irrep: &Self::Representation) -> f64 {
318        let j = irrep.value();
319        j * (j + 1.0)
320    }
321
322    #[inline]
323    fn rank() -> usize {
324        1 // SU(2) has rank 1 (dimension of Cartan subalgebra)
325    }
326}
327
328/// SU(2) group element represented as a 2×2 unitary matrix.
329///
330/// SU(2) is the group of 2×2 complex unitary matrices with determinant 1:
331/// ```text
332/// U ∈ SU(2)  ⟺  U†U = I  and  det(U) = 1
333/// ```
334///
335/// # Applications
336/// - Represents rotations and spin transformations
337/// - Acts on spinor fields: ψ → U ψ
338/// - Preserves inner products (unitarity)
339#[derive(Debug, Clone)]
340pub struct SU2 {
341    /// The 2×2 unitary matrix representation
342    pub matrix: Array2<Complex64>,
343}
344
345impl SU2 {
346    /// Identity element: I₂
347    #[must_use]
348    pub fn identity() -> Self {
349        Self {
350            matrix: Array2::eye(2),
351        }
352    }
353
354    /// Get Pauli X generator (`σ_x` / 2)
355    ///
356    /// Returns: (1/2) * [[0, 1], [1, 0]]
357    #[must_use]
358    pub fn pauli_x() -> Array2<Complex64> {
359        let mut matrix = Array2::zeros((2, 2));
360        matrix[[0, 1]] = Complex64::new(0.5, 0.0);
361        matrix[[1, 0]] = Complex64::new(0.5, 0.0);
362        matrix
363    }
364
365    /// Get Pauli Y generator (`σ_y` / 2)
366    ///
367    /// Returns: (1/2) * [[0, -i], [i, 0]]
368    #[must_use]
369    pub fn pauli_y() -> Array2<Complex64> {
370        let mut matrix = Array2::zeros((2, 2));
371        matrix[[0, 1]] = Complex64::new(0.0, -0.5);
372        matrix[[1, 0]] = Complex64::new(0.0, 0.5);
373        matrix
374    }
375
376    /// Get Pauli Z generator (`σ_z` / 2)
377    ///
378    /// Returns: (1/2) * [[1, 0], [0, -1]]
379    #[must_use]
380    pub fn pauli_z() -> Array2<Complex64> {
381        let mut matrix = Array2::zeros((2, 2));
382        matrix[[0, 0]] = Complex64::new(0.5, 0.0);
383        matrix[[1, 1]] = Complex64::new(-0.5, 0.0);
384        matrix
385    }
386
387    /// Rotation around X-axis by angle θ
388    ///
389    /// Uses closed form: U = cos(θ/2) I + i sin(θ/2) `σ_x`
390    #[inline]
391    #[must_use]
392    pub fn rotation_x(theta: f64) -> Self {
393        let c = (theta / 2.0).cos();
394        let s = (theta / 2.0).sin();
395
396        let mut matrix = Array2::zeros((2, 2));
397        matrix[[0, 0]] = Complex64::new(c, 0.0);
398        matrix[[0, 1]] = Complex64::new(0.0, s);
399        matrix[[1, 0]] = Complex64::new(0.0, s);
400        matrix[[1, 1]] = Complex64::new(c, 0.0);
401
402        Self { matrix }
403    }
404
405    /// Rotation around Y-axis by angle θ
406    ///
407    /// Uses closed form: U = cos(θ/2) I + i sin(θ/2) `σ_y`
408    #[inline]
409    #[must_use]
410    pub fn rotation_y(theta: f64) -> Self {
411        let c = (theta / 2.0).cos();
412        let s = (theta / 2.0).sin();
413
414        let mut matrix = Array2::zeros((2, 2));
415        matrix[[0, 0]] = Complex64::new(c, 0.0);
416        matrix[[0, 1]] = Complex64::new(s, 0.0);
417        matrix[[1, 0]] = Complex64::new(-s, 0.0);
418        matrix[[1, 1]] = Complex64::new(c, 0.0);
419
420        Self { matrix }
421    }
422
423    /// Rotation around Z-axis by angle θ
424    ///
425    /// Uses closed form: U = cos(θ/2) I + i sin(θ/2) `σ_z`
426    #[inline]
427    #[must_use]
428    pub fn rotation_z(theta: f64) -> Self {
429        let c = (theta / 2.0).cos();
430        let s = (theta / 2.0).sin();
431
432        let mut matrix = Array2::zeros((2, 2));
433        matrix[[0, 0]] = Complex64::new(c, s);
434        matrix[[0, 1]] = Complex64::new(0.0, 0.0);
435        matrix[[1, 0]] = Complex64::new(0.0, 0.0);
436        matrix[[1, 1]] = Complex64::new(c, -s);
437
438        Self { matrix }
439    }
440
441    /// Random SU(2) element uniformly distributed according to Haar measure
442    ///
443    /// Requires the `rand` feature (enabled by default).
444    ///
445    /// Samples uniformly from SU(2) ≅ S³ (the 3-sphere) using the quaternion
446    /// representation and Gaussian sampling method.
447    ///
448    /// # Mathematical Background
449    ///
450    /// SU(2) is diffeomorphic to the 3-sphere S³ ⊂ ℝ⁴:
451    /// ```text
452    /// SU(2) = {(a,b,c,d) ∈ ℝ⁴ : a² + b² + c² + d² = 1}
453    /// ```
454    ///
455    /// represented as matrices:
456    /// ```text
457    /// U = [[a+ib,  c+id ],
458    ///      [-c+id, a-ib]]
459    /// ```
460    ///
461    /// ## Haar Measure Sampling
462    ///
463    /// To sample uniformly from S³:
464    /// 1. Generate 4 independent standard Gaussian variables (μ=0, σ=1)
465    /// 2. Normalize to unit length
466    ///
467    /// This gives the uniform (Haar) measure on SU(2) due to the rotational
468    /// invariance of the Gaussian distribution.
469    ///
470    /// # References
471    ///
472    /// - Shoemake, K.: "Uniform Random Rotations" (Graphics Gems III, 1992)
473    /// - Diaconis & Saloff-Coste: "Comparison Theorems for Random Walks on Groups" (1993)
474    ///
475    /// # Examples
476    ///
477    /// ```rust
478    /// use lie_groups::SU2;
479    /// use rand::SeedableRng;
480    ///
481    /// let mut rng = rand::rngs::StdRng::seed_from_u64(42);
482    /// let g = SU2::random_haar(&mut rng);
483    ///
484    /// // Verify it's unitary
485    /// assert!(g.verify_unitarity(1e-10));
486    /// ```
487    #[cfg(feature = "rand")]
488    #[must_use]
489    pub fn random_haar<R: rand::Rng>(rng: &mut R) -> Self {
490        use rand_distr::{Distribution, StandardNormal};
491
492        // Numerical stability constant: minimum acceptable norm before re-sampling
493        // Probability of norm < 1e-10 for 4 Gaussians is ~10^-40, but we guard anyway
494        const MIN_NORM: f64 = 1e-10;
495
496        loop {
497            // Generate 4 independent Gaussian(0,1) random variables
498            let a: f64 = StandardNormal.sample(rng);
499            let b: f64 = StandardNormal.sample(rng);
500            let c: f64 = StandardNormal.sample(rng);
501            let d: f64 = StandardNormal.sample(rng);
502
503            // Normalize to unit length (project onto S³)
504            let norm = (a * a + b * b + c * c + d * d).sqrt();
505
506            // Guard against numerical instability: if norm is too small, re-sample
507            // This prevents division by ~0 which would produce Inf/NaN
508            if norm < MIN_NORM {
509                continue;
510            }
511
512            let a = a / norm;
513            let b = b / norm;
514            let c = c / norm;
515            let d = d / norm;
516
517            // Construct SU(2) matrix from quaternion (a,b,c,d)
518            // U = [[a+ib,  c+id ],
519            //      [-c+id, a-ib]]
520            let mut matrix = Array2::zeros((2, 2));
521            matrix[[0, 0]] = Complex64::new(a, b);
522            matrix[[0, 1]] = Complex64::new(c, d);
523            matrix[[1, 0]] = Complex64::new(-c, d);
524            matrix[[1, 1]] = Complex64::new(a, -b);
525
526            return Self { matrix };
527        }
528    }
529
530    /// Group inverse: U⁻¹ = U† (conjugate transpose for unitary matrices)
531    #[must_use]
532    pub fn inverse(&self) -> Self {
533        let matrix = self.matrix.t().mapv(|z| z.conj());
534        Self { matrix }
535    }
536
537    /// Hermitian adjoint (conjugate transpose): U†
538    ///
539    /// For unitary matrices U ∈ SU(2), we have U† = U⁻¹
540    /// This is used in gauge transformations: A' = g A g†
541    #[must_use]
542    pub fn adjoint(&self) -> Self {
543        self.inverse()
544    }
545
546    /// Act on a 2D vector: ψ → U ψ
547    #[must_use]
548    pub fn act_on_vector(&self, v: &[Complex64; 2]) -> [Complex64; 2] {
549        let vec = Array2::from_shape_vec((2, 1), vec![v[0], v[1]])
550            .expect("Failed to create 2x1 array from 2-element vector");
551        let result = self.matrix.dot(&vec);
552        [result[[0, 0]], result[[1, 0]]]
553    }
554
555    /// Trace of the matrix: Tr(U)
556    #[must_use]
557    pub fn trace(&self) -> Complex64 {
558        self.matrix[[0, 0]] + self.matrix[[1, 1]]
559    }
560
561    /// Distance from identity (geodesic distance in Lie group manifold)
562    ///
563    /// Computed as: d(U, I) = ||log(U)||_F (Frobenius norm of logarithm)
564    ///
565    /// # Numerical Behavior
566    ///
567    /// For valid SU(2) matrices, `|Re(Tr(U))/2| ≤ 1` exactly. Small violations
568    /// (within `UNITARITY_VIOLATION_THRESHOLD`) are clamped silently. Larger
569    /// violations trigger a debug assertion, indicating matrix corruption.
570    #[must_use]
571    pub fn distance_to_identity(&self) -> f64 {
572        // For SU(2), use the formula: d = arccos(Re(Tr(U))/2)
573        // This is the angle of rotation
574        let trace_val = self.trace();
575        let raw_cos_half = trace_val.re / 2.0;
576
577        // Detect unitarity violations: for valid SU(2), |cos(θ/2)| ≤ 1
578        debug_assert!(
579            raw_cos_half.abs() <= 1.0 + UNITARITY_VIOLATION_THRESHOLD,
580            "SU(2) unitarity violation: |cos(θ/2)| = {:.10} > 1 + ε. \
581             Matrix has drifted from the group manifold.",
582            raw_cos_half.abs()
583        );
584
585        // Safe clamp for tiny numerical overshoot
586        let cos_half_angle = raw_cos_half.clamp(-1.0, 1.0);
587        2.0 * cos_half_angle.acos()
588    }
589
590    /// Extract rotation angle θ and axis n̂ from SU(2) element.
591    ///
592    /// For U = cos(θ/2)·I + i·sin(θ/2)·(n̂·σ), returns (θ, n̂).
593    ///
594    /// # Returns
595    ///
596    /// Tuple of:
597    /// - `angle`: Rotation angle θ ∈ [0, 2π]
598    /// - `axis`: Unit 3-vector n̂ = `[n_x, n_y, n_z]` (or `[0,0,1]` if θ ≈ 0)
599    ///
600    /// # Usage in Topological Charge
601    ///
602    /// For computing topological charge, the product `F_μν · F_ρσ` involves
603    /// the dot product of the orientation axes: `(n_μν · n_ρσ)`.
604    #[must_use]
605    pub fn angle_and_axis(&self) -> (f64, [f64; 3]) {
606        // U = cos(θ/2)·I + i·sin(θ/2)·(n̂·σ)
607        // matrix[[0,0]] = cos(θ/2) + i·sin(θ/2)·n_z
608        // matrix[[0,1]] = sin(θ/2)·n_y + i·sin(θ/2)·n_x
609        // matrix[[1,0]] = -sin(θ/2)·n_y + i·sin(θ/2)·n_x
610        // matrix[[1,1]] = cos(θ/2) - i·sin(θ/2)·n_z
611
612        let cos_half = self.matrix[[0, 0]].re.clamp(-1.0, 1.0);
613        let angle = 2.0 * cos_half.acos();
614
615        // For small angles, axis is ill-defined; return default
616        if angle < 1e-10 {
617            return (angle, [0.0, 0.0, 1.0]);
618        }
619
620        let sin_half = (angle / 2.0).sin();
621        if sin_half.abs() < 1e-10 {
622            // Near identity or near -I
623            return (angle, [0.0, 0.0, 1.0]);
624        }
625
626        // Extract axis components
627        let n_z = self.matrix[[0, 0]].im / sin_half;
628        let n_x = self.matrix[[0, 1]].im / sin_half;
629        let n_y = self.matrix[[0, 1]].re / sin_half;
630
631        // Normalize (should already be unit, but ensure numerical stability)
632        let norm = (n_x * n_x + n_y * n_y + n_z * n_z).sqrt();
633        if norm < 1e-10 {
634            return (angle, [0.0, 0.0, 1.0]);
635        }
636
637        (angle, [n_x / norm, n_y / norm, n_z / norm])
638    }
639
640    /// Verify this is approximately in SU(2)
641    ///
642    /// Checks: U†U ≈ I and |det(U) - 1| ≈ 0
643    #[must_use]
644    pub fn verify_unitarity(&self, tolerance: f64) -> bool {
645        // Check U†U = I
646        let u_dagger = self.matrix.t().mapv(|z: Complex64| z.conj());
647        let product = u_dagger.dot(&self.matrix);
648        let identity = Array2::eye(2);
649
650        let diff_norm: f64 = product
651            .iter()
652            .zip(identity.iter())
653            .map(|(a, b): (&Complex64, &Complex64)| (a - b).norm_sqr())
654            .sum::<f64>()
655            .sqrt();
656
657        diff_norm < tolerance
658    }
659
660    /// Convert to 2×2 array format (for quaternion compatibility)
661    #[must_use]
662    pub fn to_matrix_array(&self) -> [[num_complex::Complex64; 2]; 2] {
663        [
664            [
665                num_complex::Complex64::new(self.matrix[[0, 0]].re, self.matrix[[0, 0]].im),
666                num_complex::Complex64::new(self.matrix[[0, 1]].re, self.matrix[[0, 1]].im),
667            ],
668            [
669                num_complex::Complex64::new(self.matrix[[1, 0]].re, self.matrix[[1, 0]].im),
670                num_complex::Complex64::new(self.matrix[[1, 1]].re, self.matrix[[1, 1]].im),
671            ],
672        ]
673    }
674
675    /// Create from 2×2 array format (for quaternion compatibility)
676    #[must_use]
677    pub fn from_matrix_array(arr: [[num_complex::Complex64; 2]; 2]) -> Self {
678        let mut matrix = Array2::zeros((2, 2));
679        matrix[[0, 0]] = Complex64::new(arr[0][0].re, arr[0][0].im);
680        matrix[[0, 1]] = Complex64::new(arr[0][1].re, arr[0][1].im);
681        matrix[[1, 0]] = Complex64::new(arr[1][0].re, arr[1][0].im);
682        matrix[[1, 1]] = Complex64::new(arr[1][1].re, arr[1][1].im);
683        Self { matrix }
684    }
685
686    // ========================================================================
687    // Numerically Stable Logarithm
688    // ========================================================================
689
690    /// Compute logarithm using numerically stable atan2 formulation.
691    ///
692    /// This is more stable than the standard `log()` method, especially for
693    /// rotation angles approaching π. Uses `atan2(sin(θ/2), cos(θ/2))` instead
694    /// of `acos(cos(θ/2))` for improved numerical conditioning.
695    ///
696    /// # Stability Improvements
697    ///
698    /// | Angle θ | acos stability | atan2 stability |
699    /// |---------|----------------|-----------------|
700    /// | θ ≈ 0   | Poor (derivative ∞) | Good |
701    /// | θ ≈ π/2 | Good | Good |
702    /// | θ ≈ π   | Good | Good |
703    ///
704    /// The atan2 formulation avoids the derivative singularity at θ = 0
705    /// where d(acos)/dx → ∞.
706    ///
707    /// # Returns
708    ///
709    /// Same as `log()`, but with improved numerical stability.
710    pub fn log_stable(&self) -> crate::error::LogResult<Su2Algebra> {
711        use crate::error::LogError;
712
713        // For SU(2): U = cos(θ/2)I + i·sin(θ/2)·(n̂·σ)
714        //
715        // Extract sin(θ/2)·n̂ directly from matrix elements:
716        // U[[0,0]] = cos(θ/2) + i·nz·sin(θ/2)  →  sin_nz = Im(U[[0,0]])
717        // U[[0,1]] = (ny + i·nx)·sin(θ/2)      →  sin_nx = Im(U[[0,1]])
718        //                                          sin_ny = Re(U[[0,1]])
719
720        let sin_nx = self.matrix[[0, 1]].im;
721        let sin_ny = self.matrix[[0, 1]].re;
722        let sin_nz = self.matrix[[0, 0]].im;
723
724        // Compute sin(θ/2) = ||sin(θ/2)·n̂||
725        let sin_half_theta = (sin_nx * sin_nx + sin_ny * sin_ny + sin_nz * sin_nz).sqrt();
726
727        // Compute cos(θ/2) = Re(Tr(U))/2
728        let raw_cos_half = self.matrix[[0, 0]].re; // = cos(θ/2) for proper SU(2)
729
730        // Detect unitarity violations
731        if (raw_cos_half.abs() - 1.0).max(0.0) > UNITARITY_VIOLATION_THRESHOLD
732            && sin_half_theta < UNITARITY_VIOLATION_THRESHOLD
733        {
734            // Check if cos² + sin² ≈ 1
735            let norm_check = raw_cos_half * raw_cos_half + sin_half_theta * sin_half_theta;
736            if (norm_check - 1.0).abs() > 1e-6 {
737                return Err(LogError::NumericalInstability {
738                    reason: format!(
739                        "SU(2) unitarity violation: cos²(θ/2) + sin²(θ/2) = {:.10} ≠ 1",
740                        norm_check
741                    ),
742                });
743            }
744        }
745
746        // Use atan2 for stable angle extraction
747        // atan2(sin, cos) is well-conditioned everywhere except at origin
748        let half_theta = sin_half_theta.atan2(raw_cos_half);
749        let theta = 2.0 * half_theta;
750
751        // Handle identity case
752        if sin_half_theta < 1e-15 {
753            return Ok(Su2Algebra::zero());
754        }
755
756        // Note: θ = π is NOT a singularity for SU(2). At θ = π,
757        // sin(θ/2) = 1 and the rotation axis is perfectly extractable.
758        // The true singularity is at θ = 2π (U = -I, sin(θ/2) = 0),
759        // which is caught by the sin_half_theta check above.
760
761        // Extract normalized axis and scale by angle
762        let scale = theta / sin_half_theta;
763        Ok(Su2Algebra([scale * sin_nx, scale * sin_ny, scale * sin_nz]))
764    }
765
766    /// Compute logarithm with conditioning information.
767    ///
768    /// Returns both the logarithm and a [`LogCondition`](crate::LogCondition)
769    /// structure that provides information about the numerical reliability
770    /// of the result.
771    ///
772    /// Unlike `log()`, this method also provides condition number information
773    /// so callers can assess the numerical reliability of the result near
774    /// the cut locus (θ → 2π, U → -I).
775    ///
776    /// # Example
777    ///
778    /// ```rust
779    /// use lie_groups::SU2;
780    ///
781    /// let g = SU2::rotation_x(2.9); // Close to π
782    /// let (log_g, cond) = g.log_with_condition().unwrap();
783    ///
784    /// if cond.is_well_conditioned() {
785    ///     println!("Reliable result: {:?}", log_g);
786    /// } else {
787    ///     println!("Warning: condition number = {:.1}", cond.condition_number);
788    /// }
789    /// ```
790    ///
791    /// # Cut Locus Behavior
792    ///
793    /// The SU(2) cut locus is at θ = 2π (U = -I), where sin(θ/2) = 0
794    /// and the axis is undefined. As θ → 2π, the condition number
795    /// diverges as 1/sin(θ/2). The method still returns a result — it's
796    /// up to the caller to decide whether to use it based on the
797    /// conditioning information.
798    pub fn log_with_condition(&self) -> crate::error::ConditionedLogResult<Su2Algebra> {
799        use crate::error::{LogCondition, LogError};
800
801        // Extract sin(θ/2)·n̂ from matrix elements
802        let sin_nx = self.matrix[[0, 1]].im;
803        let sin_ny = self.matrix[[0, 1]].re;
804        let sin_nz = self.matrix[[0, 0]].im;
805        let sin_half_theta = (sin_nx * sin_nx + sin_ny * sin_ny + sin_nz * sin_nz).sqrt();
806
807        let raw_cos_half = self.matrix[[0, 0]].re;
808
809        // Unitarity check
810        let norm_check = raw_cos_half * raw_cos_half + sin_half_theta * sin_half_theta;
811        if (norm_check - 1.0).abs() > 1e-6 {
812            return Err(LogError::NumericalInstability {
813                reason: format!(
814                    "SU(2) unitarity violation: cos²(θ/2) + sin²(θ/2) = {:.10} ≠ 1",
815                    norm_check
816                ),
817            });
818        }
819
820        // Stable angle extraction using atan2
821        let half_theta = sin_half_theta.atan2(raw_cos_half);
822        let theta = 2.0 * half_theta;
823
824        // Compute conditioning
825        let condition = LogCondition::from_angle(theta);
826
827        // Handle identity case
828        if sin_half_theta < 1e-15 {
829            return Ok((Su2Algebra::zero(), condition));
830        }
831
832        // For θ < 2π, the axis is extractable. The result is well-conditioned
833        // for most of the range; only near θ = 2π (U = -I) does sin(θ/2) → 0.
834        let scale = theta / sin_half_theta;
835        let result = Su2Algebra([scale * sin_nx, scale * sin_ny, scale * sin_nz]);
836
837        Ok((result, condition))
838    }
839}
840
841/// Group multiplication: U₁ * U₂
842impl Mul<&SU2> for &SU2 {
843    type Output = SU2;
844
845    fn mul(self, rhs: &SU2) -> SU2 {
846        SU2 {
847            matrix: self.matrix.dot(&rhs.matrix),
848        }
849    }
850}
851
852impl Mul<&SU2> for SU2 {
853    type Output = SU2;
854
855    fn mul(self, rhs: &SU2) -> SU2 {
856        &self * rhs
857    }
858}
859
860impl MulAssign<&SU2> for SU2 {
861    fn mul_assign(&mut self, rhs: &SU2) {
862        self.matrix = self.matrix.dot(&rhs.matrix);
863    }
864}
865
866impl fmt::Display for Su2Algebra {
867    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
868        write!(
869            f,
870            "su(2)[{:.4}, {:.4}, {:.4}]",
871            self.0[0], self.0[1], self.0[2]
872        )
873    }
874}
875
876impl fmt::Display for SU2 {
877    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
878        // Show rotation angle and distance to identity
879        let dist = self.distance_to_identity();
880        write!(f, "SU(2)(θ={:.4})", dist)
881    }
882}
883
884/// Implementation of the `LieGroup` trait for SU(2).
885///
886/// This provides the abstract group interface, making SU(2) usable in
887/// generic gauge theory algorithms.
888///
889/// # Mathematical Verification
890///
891/// The implementation satisfies all group axioms:
892/// - Identity: Verified in tests (`test_identity`)
893/// - Inverse: Verified in tests (`test_inverse`)
894/// - Associativity: Follows from matrix multiplication
895/// - Closure: Matrix multiplication of unitaries is unitary
896impl LieGroup for SU2 {
897    const DIM: usize = 2;
898
899    type Algebra = Su2Algebra;
900
901    fn identity() -> Self {
902        Self::identity() // Delegate to inherent method
903    }
904
905    fn compose(&self, other: &Self) -> Self {
906        // Group composition is matrix multiplication
907        self * other
908    }
909
910    fn inverse(&self) -> Self {
911        Self::inverse(self) // Delegate to inherent method
912    }
913
914    fn adjoint(&self) -> Self {
915        Self::adjoint(self) // Delegate to inherent method
916    }
917
918    fn distance_to_identity(&self) -> f64 {
919        Self::distance_to_identity(self) // Delegate to inherent method
920    }
921
922    fn exp(tangent: &Su2Algebra) -> Self {
923        // Su2Algebra([a, b, c]) corresponds to X = i(a·σₓ + b·σᵧ + c·σᵤ)/2
924        // with rotation angle θ = ||(a,b,c)||.
925        let angle = tangent.norm();
926
927        if angle < SMALL_ANGLE_EXP_THRESHOLD {
928            // For small angles, return identity (avoid division by zero)
929            return Self::identity();
930        }
931
932        // Normalize to get rotation axis
933        let axis = tangent.scale(1.0 / angle);
934
935        // Rodrigues formula: exp(i(θ/2)·n̂·σ) = cos(θ/2)I + i·sin(θ/2)·n̂·σ
936        let c = (angle / 2.0).cos();
937        let s = (angle / 2.0).sin();
938
939        let mut matrix = Array2::zeros((2, 2));
940        matrix[[0, 0]] = Complex64::new(c, s * axis.0[2]);
941        matrix[[0, 1]] = Complex64::new(s * axis.0[1], s * axis.0[0]);
942        matrix[[1, 0]] = Complex64::new(-s * axis.0[1], s * axis.0[0]);
943        matrix[[1, 1]] = Complex64::new(c, -s * axis.0[2]);
944
945        Self { matrix }
946    }
947
948    fn log(&self) -> crate::error::LogResult<Su2Algebra> {
949        use crate::error::LogError;
950
951        // For SU(2), we use the formula:
952        // U = cos(θ/2)I + i·sin(θ/2)·(n̂·σ)
953        //
954        // Where θ is the rotation angle and n̂ is the rotation axis.
955        //
956        // The logarithm is:
957        // log(U) = θ·n̂ ∈ su(2) ≅ ℝ³
958        //
959        // Step 1: Extract angle from trace
960        // Tr(U) = 2·cos(θ/2)
961        let tr = self.trace();
962        let raw_cos_half = tr.re / 2.0;
963
964        // Detect unitarity violations: for valid SU(2), |cos(θ/2)| ≤ 1
965        if raw_cos_half.abs() > 1.0 + UNITARITY_VIOLATION_THRESHOLD {
966            return Err(LogError::NumericalInstability {
967                reason: format!(
968                    "SU(2) unitarity violation: |cos(θ/2)| = {:.10} > 1 + ε. \
969                     Matrix has drifted from the group manifold.",
970                    raw_cos_half.abs()
971                ),
972            });
973        }
974
975        // Safe clamp for tiny numerical overshoot
976        let cos_half_theta = raw_cos_half.clamp(-1.0, 1.0);
977        let half_theta = cos_half_theta.acos();
978        let theta = 2.0 * half_theta;
979
980        // Step 2: Handle special cases
981        const SMALL_ANGLE_THRESHOLD: f64 = 1e-10;
982
983        if theta.abs() < SMALL_ANGLE_THRESHOLD {
984            // Near identity: log(I) = 0
985            return Ok(Su2Algebra::zero());
986        }
987
988        // Note: θ = π is NOT a singularity for SU(2). At θ = π,
989        // sin(θ/2) = 1 and the axis is perfectly extractable.
990        // The true singularity is at θ = 2π where U = -I and
991        // sin(θ/2) = 0, which is caught by the sin_half_theta
992        // check below.
993
994        // Step 3: Extract rotation axis from matrix elements
995        // For U = cos(θ/2)I + i·sin(θ/2)·(n̂·σ), we have:
996        //
997        // U = [[cos(θ/2) + i·nz·sin(θ/2),  (ny + i·nx)·sin(θ/2)    ],
998        //      [(-ny + i·nx)·sin(θ/2),     cos(θ/2) - i·nz·sin(θ/2)]]
999        //
1000        // Extracting:
1001        // U[[0,0]] = cos(θ/2) + i·nz·sin(θ/2)  →  nz = Im(U[[0,0]]) / sin(θ/2)
1002        // U[[0,1]] = (ny + i·nx)·sin(θ/2)      →  nx = Im(U[[0,1]]) / sin(θ/2)
1003        //                                          ny = Re(U[[0,1]]) / sin(θ/2)
1004
1005        let sin_half_theta = (half_theta).sin();
1006
1007        if sin_half_theta.abs() < SIN_HALF_THETA_THRESHOLD {
1008            // This shouldn't happen given our checks above, but guard against it
1009            return Err(LogError::NumericalInstability {
1010                reason: "sin(θ/2) too small for reliable axis extraction".to_string(),
1011            });
1012        }
1013
1014        let nx = self.matrix[[0, 1]].im / sin_half_theta;
1015        let ny = self.matrix[[0, 1]].re / sin_half_theta;
1016        let nz = self.matrix[[0, 0]].im / sin_half_theta;
1017
1018        // The logarithm is log(U) = θ·(nx, ny, nz) ∈ su(2)
1019        Ok(Su2Algebra([theta * nx, theta * ny, theta * nz]))
1020    }
1021
1022    fn adjoint_action(&self, algebra_element: &Su2Algebra) -> Su2Algebra {
1023        // Ad_g(X) = g X g† for matrix groups
1024        //
1025        // We construct the matrix M = i(a·σₓ + b·σᵧ + c·σᵤ) = 2X, where
1026        // X = i(a·σₓ + b·σᵧ + c·σᵤ)/2 is the actual algebra element in the
1027        // {iσ/2} basis. The factor of 2 cancels: we compute gMg† and extract
1028        // coefficients using the same convention, recovering (a', b', c').
1029
1030        let [a, b, c] = algebra_element.0;
1031        let i = Complex64::new(0.0, 1.0);
1032
1033        // M = i(a·σₓ + b·σᵧ + c·σᵤ) = [[c·i, b + a·i], [-b + a·i, -c·i]]
1034        let mut x_matrix = Array2::zeros((2, 2));
1035        x_matrix[[0, 0]] = i * c; // c·i
1036        x_matrix[[0, 1]] = Complex64::new(b, a); // b + a·i
1037        x_matrix[[1, 0]] = Complex64::new(-b, a); // -b + a·i
1038        x_matrix[[1, 1]] = -i * c; // -c·i
1039
1040        // Compute g X g† where g† is conjugate transpose
1041        let g_x = self.matrix.dot(&x_matrix);
1042        let g_adjoint_matrix = self.matrix.t().mapv(|z| z.conj()); // Conjugate transpose
1043        let result = g_x.dot(&g_adjoint_matrix);
1044
1045        // Extract coefficients: result = [[c'·i, b'+a'·i], [-b'+a'·i, -c'·i]]
1046
1047        let a_prime = result[[0, 1]].im; // Imaginary part of result[[0,1]]
1048        let b_prime = result[[0, 1]].re; // Real part of result[[0,1]]
1049        let c_prime = result[[0, 0]].im; // Imaginary part of result[[0,0]]
1050
1051        Su2Algebra([a_prime, b_prime, c_prime])
1052    }
1053
1054    fn dim() -> usize {
1055        2 // SU(2) consists of 2×2 matrices
1056    }
1057
1058    fn trace(&self) -> Complex64 {
1059        // Tr(M) = M₀₀ + M₁₁
1060        self.matrix[[0, 0]] + self.matrix[[1, 1]]
1061    }
1062}
1063
1064// ============================================================================
1065// Mathematical Property Implementations for SU(2)
1066// ============================================================================
1067
1068use crate::traits::{Compact, SemiSimple, Simple};
1069
1070/// SU(2) is compact.
1071///
1072/// All elements are bounded: ||U|| = 1 for all U ∈ SU(2).
1073/// The group is diffeomorphic to the 3-sphere S³.
1074impl Compact for SU2 {}
1075
1076/// SU(2) is simple.
1077///
1078/// It has no non-trivial normal subgroups. This is a fundamental
1079/// result in Lie theory - SU(2) is one of the classical simple groups.
1080impl Simple for SU2 {}
1081
1082/// SU(2) is semi-simple.
1083///
1084/// As a simple group, it is automatically semi-simple.
1085/// (Simple ⊂ Semi-simple in the classification hierarchy)
1086impl SemiSimple for SU2 {}
1087
1088// ============================================================================
1089// Algebra Marker Traits
1090// ============================================================================
1091
1092/// su(2) algebra elements are traceless by construction.
1093///
1094/// The representation `Su2Algebra([f64; 3])` stores coefficients in the
1095/// Pauli basis {iσ₁, iσ₂, iσ₃}. Since each Pauli matrix is traceless,
1096/// any linear combination is also traceless.
1097///
1098/// # Lean Connection
1099///
1100/// Combined with `det_exp_eq_exp_trace`: det(exp(X)) = exp(tr(X)) = exp(0) = 1.
1101/// Therefore `SU2::exp` always produces elements with determinant 1.
1102impl TracelessByConstruction for Su2Algebra {}
1103
1104/// su(2) algebra elements are anti-Hermitian by construction.
1105///
1106/// The representation uses {iσ₁, iσ₂, iσ₃} where σᵢ are Hermitian.
1107/// Since (iσ)† = -iσ† = -iσ, each basis element is anti-Hermitian,
1108/// and any real linear combination is also anti-Hermitian.
1109///
1110/// # Lean Connection
1111///
1112/// Combined with `exp_antiHermitian_unitary`: exp(X)† · exp(X) = I.
1113/// Therefore `SU2::exp` always produces unitary elements.
1114impl AntiHermitianByConstruction for Su2Algebra {}
1115
1116#[cfg(test)]
1117mod tests {
1118    use super::*;
1119    use approx::assert_relative_eq;
1120
1121    #[test]
1122    fn test_identity() {
1123        let id = SU2::identity();
1124        assert!(id.verify_unitarity(1e-10));
1125        assert_relative_eq!(id.distance_to_identity(), 0.0, epsilon = 1e-10);
1126    }
1127
1128    #[test]
1129    fn test_rotation_unitarity() {
1130        let u = SU2::rotation_x(0.5);
1131        assert!(u.verify_unitarity(1e-10));
1132    }
1133
1134    #[test]
1135    fn test_inverse() {
1136        let u = SU2::rotation_y(1.2);
1137        let u_inv = u.inverse();
1138        let product = &u * &u_inv;
1139
1140        assert_relative_eq!(product.distance_to_identity(), 0.0, epsilon = 1e-8);
1141    }
1142
1143    #[test]
1144    fn test_group_multiplication() {
1145        let u1 = SU2::rotation_x(0.3);
1146        let u2 = SU2::rotation_y(0.7);
1147        let product = &u1 * &u2;
1148
1149        assert!(product.verify_unitarity(1e-10));
1150    }
1151
1152    #[test]
1153    fn test_action_on_vector() {
1154        let id = SU2::identity();
1155        let v = [Complex64::new(1.0, 0.0), Complex64::new(0.0, 1.0)];
1156        let result = id.act_on_vector(&v);
1157
1158        assert_relative_eq!(result[0].re, v[0].re, epsilon = 1e-10);
1159        assert_relative_eq!(result[1].im, v[1].im, epsilon = 1e-10);
1160    }
1161
1162    #[test]
1163    fn test_rotation_preserves_norm() {
1164        let u = SU2::rotation_z(2.5);
1165        let v = [Complex64::new(3.0, 4.0), Complex64::new(1.0, 2.0)];
1166
1167        let norm_before = v[0].norm_sqr() + v[1].norm_sqr();
1168        let rotated = u.act_on_vector(&v);
1169        let norm_after = rotated[0].norm_sqr() + rotated[1].norm_sqr();
1170
1171        assert_relative_eq!(norm_before, norm_after, epsilon = 1e-10);
1172    }
1173
1174    // ========================================================================
1175    // LieGroup Trait Tests
1176    // ========================================================================
1177
1178    #[test]
1179    fn test_lie_group_identity() {
1180        use crate::traits::LieGroup;
1181
1182        let g = SU2::rotation_x(1.5);
1183        let e = SU2::identity();
1184
1185        // Right identity: g * e = g
1186        let g_times_e = g.compose(&e);
1187        assert_relative_eq!(g_times_e.distance(&g), 0.0, epsilon = 1e-10);
1188
1189        // Left identity: e * g = g
1190        let e_times_g = e.compose(&g);
1191        assert_relative_eq!(e_times_g.distance(&g), 0.0, epsilon = 1e-10);
1192    }
1193
1194    #[test]
1195    fn test_lie_group_inverse() {
1196        use crate::traits::LieGroup;
1197
1198        let g = SU2::rotation_y(2.3);
1199        let g_inv = g.inverse();
1200
1201        // g * g⁻¹ = e
1202        // Use 1e-7 tolerance to account for accumulated numerical error
1203        // in matrix operations (2×2 matrix multiplication + arccos)
1204        let right_product = g.compose(&g_inv);
1205        assert!(
1206            right_product.is_near_identity(1e-7),
1207            "Right inverse failed: distance = {}",
1208            right_product.distance_to_identity()
1209        );
1210
1211        // g⁻¹ * g = e
1212        let left_product = g_inv.compose(&g);
1213        assert!(
1214            left_product.is_near_identity(1e-7),
1215            "Left inverse failed: distance = {}",
1216            left_product.distance_to_identity()
1217        );
1218    }
1219
1220    #[test]
1221    fn test_lie_group_associativity() {
1222        use crate::traits::LieGroup;
1223
1224        let g1 = SU2::rotation_x(0.5);
1225        let g2 = SU2::rotation_y(0.8);
1226        let g3 = SU2::rotation_z(1.2);
1227
1228        // (g1 * g2) * g3
1229        let left_assoc = g1.compose(&g2).compose(&g3);
1230
1231        // g1 * (g2 * g3)
1232        let right_assoc = g1.compose(&g2.compose(&g3));
1233
1234        assert_relative_eq!(left_assoc.distance(&right_assoc), 0.0, epsilon = 1e-10);
1235    }
1236
1237    #[test]
1238    fn test_lie_group_distance_symmetry() {
1239        let g = SU2::rotation_x(1.8);
1240        let g_inv = g.inverse();
1241
1242        // d(g, e) = d(g⁻¹, e) by symmetry
1243        let d1 = g.distance_to_identity();
1244        let d2 = g_inv.distance_to_identity();
1245
1246        assert_relative_eq!(d1, d2, epsilon = 1e-10);
1247    }
1248
1249    #[test]
1250    fn test_lie_group_is_near_identity() {
1251        use crate::traits::LieGroup;
1252
1253        let e = SU2::identity();
1254        assert!(
1255            e.is_near_identity(1e-10),
1256            "Identity should be near identity"
1257        );
1258
1259        let almost_e = SU2::rotation_x(1e-12);
1260        assert!(
1261            almost_e.is_near_identity(1e-10),
1262            "Small rotation should be near identity"
1263        );
1264
1265        let g = SU2::rotation_y(0.5);
1266        assert!(
1267            !g.is_near_identity(1e-10),
1268            "Large rotation should not be near identity"
1269        );
1270    }
1271
1272    #[test]
1273    fn test_lie_group_generic_algorithm() {
1274        use crate::traits::LieGroup;
1275
1276        // Generic parallel transport function (works for any LieGroup!)
1277        fn parallel_transport<G: LieGroup>(path: &[G]) -> G {
1278            path.iter().fold(G::identity(), |acc, g| acc.compose(g))
1279        }
1280
1281        let path = vec![
1282            SU2::rotation_x(0.1),
1283            SU2::rotation_y(0.2),
1284            SU2::rotation_z(0.3),
1285        ];
1286
1287        let holonomy = parallel_transport(&path);
1288
1289        // Verify it's a valid SU(2) element
1290        assert!(holonomy.verify_unitarity(1e-10));
1291
1292        // Should be non-trivial (not identity)
1293        assert!(holonomy.distance_to_identity() > 0.1);
1294    }
1295
1296    // ========================================================================
1297    // Property-Based Tests for Group Axioms
1298    // ========================================================================
1299    //
1300    // These tests use proptest to verify that SU(2) satisfies the
1301    // mathematical axioms of a Lie group for randomly generated elements.
1302    //
1303    // This is a form of **specification-based testing**: the group axioms
1304    // are the specification, and we verify they hold for all inputs.
1305    //
1306    // Run with: cargo test --features property-tests
1307
1308    #[cfg(feature = "proptest")]
1309    use proptest::prelude::*;
1310
1311    /// Strategy for generating arbitrary SU(2) elements.
1312    ///
1313    /// We generate SU(2) elements by composing three Euler rotations:
1314    /// `g = R_z(α) · R_y(β) · R_x(γ)`
1315    ///
1316    /// This gives a good coverage of SU(2) ≅ S³.
1317    #[cfg(feature = "proptest")]
1318    fn arb_su2() -> impl Strategy<Value = SU2> {
1319        use std::f64::consts::TAU;
1320
1321        // Generate three Euler angles
1322        let alpha = 0.0..TAU;
1323        let beta = 0.0..TAU;
1324        let gamma = 0.0..TAU;
1325
1326        (alpha, beta, gamma).prop_map(|(a, b, c)| {
1327            SU2::rotation_z(a)
1328                .compose(&SU2::rotation_y(b))
1329                .compose(&SU2::rotation_x(c))
1330        })
1331    }
1332
1333    #[cfg(feature = "proptest")]
1334    proptest! {
1335        /// **Group Axiom 1: Identity Element**
1336        ///
1337        /// For all g ∈ SU(2):
1338        /// - e · g = g (left identity)
1339        /// - g · e = g (right identity)
1340        ///
1341        /// where e = identity element
1342        ///
1343        /// Note: We use tolerance 1e-7 to account for floating-point
1344        /// rounding errors in matrix operations.
1345        #[test]
1346        fn prop_identity_axiom(g in arb_su2()) {
1347            let e = SU2::identity();
1348
1349            // Left identity: e · g = g
1350            let left = e.compose(&g);
1351            prop_assert!(
1352                left.distance(&g) < 1e-7,
1353                "Left identity failed: e·g != g, distance = {}",
1354                left.distance(&g)
1355            );
1356
1357            // Right identity: g · e = g
1358            let right = g.compose(&e);
1359            prop_assert!(
1360                right.distance(&g) < 1e-7,
1361                "Right identity failed: g·e != g, distance = {}",
1362                right.distance(&g)
1363            );
1364        }
1365
1366        /// **Group Axiom 2: Inverse Element**
1367        ///
1368        /// For all g ∈ SU(2):
1369        /// - g · g⁻¹ = e (right inverse)
1370        /// - g⁻¹ · g = e (left inverse)
1371        ///
1372        /// where g⁻¹ = inverse of g
1373        ///
1374        /// Note: We use tolerance 1e-7 to account for floating-point
1375        /// rounding errors in matrix operations.
1376        #[test]
1377        fn prop_inverse_axiom(g in arb_su2()) {
1378            let g_inv = g.inverse();
1379
1380            // Right inverse: g · g⁻¹ = e
1381            let right_product = g.compose(&g_inv);
1382            prop_assert!(
1383                right_product.is_near_identity(1e-7),
1384                "Right inverse failed: g·g⁻¹ != e, distance = {}",
1385                right_product.distance_to_identity()
1386            );
1387
1388            // Left inverse: g⁻¹ · g = e
1389            let left_product = g_inv.compose(&g);
1390            prop_assert!(
1391                left_product.is_near_identity(1e-7),
1392                "Left inverse failed: g⁻¹·g != e, distance = {}",
1393                left_product.distance_to_identity()
1394            );
1395        }
1396
1397        /// **Group Axiom 3: Associativity**
1398        ///
1399        /// For all g₁, g₂, g₃ ∈ SU(2):
1400        /// - (g₁ · g₂) · g₃ = g₁ · (g₂ · g₃)
1401        ///
1402        /// Group multiplication is associative.
1403        ///
1404        /// Note: We use tolerance 1e-7 to account for floating-point
1405        /// rounding errors in matrix operations.
1406        #[test]
1407        fn prop_associativity(g1 in arb_su2(), g2 in arb_su2(), g3 in arb_su2()) {
1408            // Left association: (g₁ · g₂) · g₃
1409            let left_assoc = g1.compose(&g2).compose(&g3);
1410
1411            // Right association: g₁ · (g₂ · g₃)
1412            let right_assoc = g1.compose(&g2.compose(&g3));
1413
1414            prop_assert!(
1415                left_assoc.distance(&right_assoc) < 1e-7,
1416                "Associativity failed: (g₁·g₂)·g₃ != g₁·(g₂·g₃), distance = {}",
1417                left_assoc.distance(&right_assoc)
1418            );
1419        }
1420
1421        /// **Lie Group Property: Inverse is Smooth**
1422        ///
1423        /// For SU(2), the inverse operation is smooth (continuously differentiable).
1424        /// We verify this by checking that nearby elements have nearby inverses.
1425        #[test]
1426        fn prop_inverse_continuity(g in arb_su2()) {
1427            // Create a small perturbation
1428            let epsilon = 0.01;
1429            let perturbation = SU2::rotation_x(epsilon);
1430            let g_perturbed = g.compose(&perturbation);
1431
1432            // Check that inverses are close
1433            let inv_distance = g.inverse().distance(&g_perturbed.inverse());
1434
1435            prop_assert!(
1436                inv_distance < 0.1,
1437                "Inverse not continuous: small perturbation caused large inverse change, distance = {}",
1438                inv_distance
1439            );
1440        }
1441
1442        /// **Unitarity Preservation**
1443        ///
1444        /// All SU(2) operations should preserve unitarity.
1445        /// This is not strictly a group axiom, but it's essential for SU(2).
1446        #[test]
1447        fn prop_unitarity_preserved(g1 in arb_su2(), g2 in arb_su2()) {
1448            // Composition preserves unitarity
1449            let product = g1.compose(&g2);
1450            prop_assert!(
1451                product.verify_unitarity(1e-10),
1452                "Composition violated unitarity"
1453            );
1454
1455            // Inverse preserves unitarity
1456            let inv = g1.inverse();
1457            prop_assert!(
1458                inv.verify_unitarity(1e-10),
1459                "Inverse violated unitarity"
1460            );
1461        }
1462
1463        /// **Adjoint Representation: Group Homomorphism**
1464        ///
1465        /// The adjoint representation Ad: G → Aut(𝔤) is a group homomorphism:
1466        /// - Ad_{g₁∘g₂}(X) = Ad_{g₁}(Ad_{g₂}(X))
1467        ///
1468        /// This is a fundamental property that must hold for the adjoint action
1469        /// to be a valid representation of the group.
1470        #[test]
1471        fn prop_adjoint_homomorphism(
1472            g1 in arb_su2(),
1473            g2 in arb_su2(),
1474            x in arb_su2_algebra()
1475        ) {
1476            // Compute Ad_{g₁∘g₂}(X)
1477            let g_composed = g1.compose(&g2);
1478            let left = g_composed.adjoint_action(&x);
1479
1480            // Compute Ad_{g₁}(Ad_{g₂}(X))
1481            let ad_g2_x = g2.adjoint_action(&x);
1482            let right = g1.adjoint_action(&ad_g2_x);
1483
1484            // They should be equal
1485            let diff = left.add(&right.scale(-1.0));
1486            prop_assert!(
1487                diff.norm() < 1e-7,
1488                "Adjoint homomorphism failed: Ad_{{g₁∘g₂}}(X) != Ad_{{g₁}}(Ad_{{g₂}}(X)), diff norm = {}",
1489                diff.norm()
1490            );
1491        }
1492
1493        /// **Adjoint Representation: Identity Action**
1494        ///
1495        /// The identity element acts trivially on the Lie algebra:
1496        /// - Ad_e(X) = X for all X ∈ 𝔤
1497        #[test]
1498        fn prop_adjoint_identity(x in arb_su2_algebra()) {
1499            let e = SU2::identity();
1500            let result = e.adjoint_action(&x);
1501
1502            let diff = result.add(&x.scale(-1.0));
1503            prop_assert!(
1504                diff.norm() < 1e-10,
1505                "Identity action failed: Ad_e(X) != X, diff norm = {}",
1506                diff.norm()
1507            );
1508        }
1509
1510        /// **Adjoint Representation: Lie Bracket Preservation**
1511        ///
1512        /// The adjoint representation preserves the Lie bracket:
1513        /// - Ad_g([X,Y]) = [Ad_g(X), Ad_g(Y)]
1514        ///
1515        /// This is a critical property that ensures the adjoint action
1516        /// is a Lie algebra automorphism.
1517        #[test]
1518        fn prop_adjoint_bracket_preservation(
1519            g in arb_su2(),
1520            x in arb_su2_algebra(),
1521            y in arb_su2_algebra()
1522        ) {
1523            use crate::traits::LieAlgebra;
1524
1525            // Compute Ad_g([X,Y])
1526            let bracket_xy = x.bracket(&y);
1527            let left = g.adjoint_action(&bracket_xy);
1528
1529            // Compute [Ad_g(X), Ad_g(Y)]
1530            let ad_x = g.adjoint_action(&x);
1531            let ad_y = g.adjoint_action(&y);
1532            let right = ad_x.bracket(&ad_y);
1533
1534            // They should be equal
1535            let diff = left.add(&right.scale(-1.0));
1536            prop_assert!(
1537                diff.norm() < 1e-6,
1538                "Bracket preservation failed: Ad_g([X,Y]) != [Ad_g(X), Ad_g(Y)], diff norm = {}",
1539                diff.norm()
1540            );
1541        }
1542
1543        /// **Jacobi Identity: Fundamental Lie Algebra Axiom**
1544        ///
1545        /// The Jacobi identity must hold for all X, Y, Z ∈ 𝔤:
1546        /// - [X, [Y, Z]] + [Y, [Z, X]] + [Z, [X, Y]] = 0
1547        ///
1548        /// This is equivalent to the derivation property of ad_X tested elsewhere,
1549        /// but here we test it directly with three random elements.
1550        #[test]
1551        fn prop_jacobi_identity(
1552            x in arb_su2_algebra(),
1553            y in arb_su2_algebra(),
1554            z in arb_su2_algebra()
1555        ) {
1556            use crate::traits::LieAlgebra;
1557
1558            // Compute [X, [Y, Z]]
1559            let yz = y.bracket(&z);
1560            let term1 = x.bracket(&yz);
1561
1562            // Compute [Y, [Z, X]]
1563            let zx = z.bracket(&x);
1564            let term2 = y.bracket(&zx);
1565
1566            // Compute [Z, [X, Y]]
1567            let xy = x.bracket(&y);
1568            let term3 = z.bracket(&xy);
1569
1570            // Sum should be zero
1571            let sum = term1.add(&term2).add(&term3);
1572            prop_assert!(
1573                sum.norm() < 1e-10,
1574                "Jacobi identity failed: ||[X,[Y,Z]] + [Y,[Z,X]] + [Z,[X,Y]]|| = {:.2e}",
1575                sum.norm()
1576            );
1577        }
1578
1579        /// **Adjoint Representation: Inverse Property**
1580        ///
1581        /// The inverse of an element acts as the inverse transformation:
1582        /// - Ad_{g⁻¹}(Ad_g(X)) = X
1583        #[test]
1584        fn prop_adjoint_inverse(g in arb_su2(), x in arb_su2_algebra()) {
1585            // Apply Ad_g then Ad_{g⁻¹}
1586            let ad_g_x = g.adjoint_action(&x);
1587            let g_inv = g.inverse();
1588            let result = g_inv.adjoint_action(&ad_g_x);
1589
1590            // Should recover X
1591            let diff = result.add(&x.scale(-1.0));
1592            prop_assert!(
1593                diff.norm() < 1e-7,
1594                "Inverse property failed: Ad_{{g⁻¹}}(Ad_g(X)) != X, diff norm = {}",
1595                diff.norm()
1596            );
1597        }
1598    }
1599
1600    /// Strategy for generating arbitrary `Su2Algebra` elements.
1601    ///
1602    /// We generate algebra elements by picking random coefficients in [-π, π]
1603    /// for each of the three basis directions (`σ_x`, `σ_y`, `σ_z`).
1604    #[cfg(feature = "proptest")]
1605    fn arb_su2_algebra() -> impl Strategy<Value = Su2Algebra> {
1606        use proptest::prelude::*;
1607        use std::f64::consts::PI;
1608
1609        ((-PI..PI), (-PI..PI), (-PI..PI)).prop_map(|(a, b, c)| Su2Algebra([a, b, c]))
1610    }
1611
1612    #[test]
1613    #[cfg(feature = "rand")]
1614    fn test_random_haar_unitarity() {
1615        use rand::SeedableRng;
1616
1617        let mut rng = rand::rngs::StdRng::seed_from_u64(42);
1618
1619        // Generate many random elements and verify they're all unitary
1620        for _ in 0..100 {
1621            let g = SU2::random_haar(&mut rng);
1622            assert!(
1623                g.verify_unitarity(1e-10),
1624                "Random Haar element should be unitary"
1625            );
1626        }
1627    }
1628
1629    #[test]
1630    fn test_bracket_bilinearity() {
1631        use crate::traits::LieAlgebra;
1632
1633        let x = Su2Algebra([1.0, 0.0, 0.0]);
1634        let y = Su2Algebra([0.0, 1.0, 0.0]);
1635        let z = Su2Algebra([0.0, 0.0, 1.0]);
1636        let alpha = 2.5;
1637
1638        // Left linearity: [αX + Y, Z] = α[X, Z] + [Y, Z]
1639        let lhs = x.scale(alpha).add(&y).bracket(&z);
1640        let rhs = x.bracket(&z).scale(alpha).add(&y.bracket(&z));
1641        for i in 0..3 {
1642            assert!(
1643                (lhs.0[i] - rhs.0[i]).abs() < 1e-14,
1644                "Left bilinearity failed at component {}: {} vs {}",
1645                i,
1646                lhs.0[i],
1647                rhs.0[i]
1648            );
1649        }
1650
1651        // Right linearity: [Z, αX + Y] = α[Z, X] + [Z, Y]
1652        let lhs = z.bracket(&x.scale(alpha).add(&y));
1653        let rhs = z.bracket(&x).scale(alpha).add(&z.bracket(&y));
1654        for i in 0..3 {
1655            assert!(
1656                (lhs.0[i] - rhs.0[i]).abs() < 1e-14,
1657                "Right bilinearity failed at component {}: {} vs {}",
1658                i,
1659                lhs.0[i],
1660                rhs.0[i]
1661            );
1662        }
1663    }
1664
1665    /// **Jacobi Identity Test with Random Elements**
1666    ///
1667    /// The Jacobi identity is the defining axiom of a Lie algebra:
1668    /// ```text
1669    /// [X, [Y, Z]] + [Y, [Z, X]] + [Z, [X, Y]] = 0
1670    /// ```
1671    ///
1672    /// This test verifies it holds for random algebra elements.
1673    #[test]
1674    fn test_jacobi_identity_random() {
1675        use crate::traits::LieAlgebra;
1676        use rand::SeedableRng;
1677        use rand_distr::{Distribution, StandardNormal};
1678
1679        let mut rng = rand::rngs::StdRng::seed_from_u64(12345);
1680
1681        // Test with 100 random triples
1682        for _ in 0..100 {
1683            // Generate random algebra elements
1684            let x = Su2Algebra([
1685                StandardNormal.sample(&mut rng),
1686                StandardNormal.sample(&mut rng),
1687                StandardNormal.sample(&mut rng),
1688            ]);
1689            let y = Su2Algebra([
1690                StandardNormal.sample(&mut rng),
1691                StandardNormal.sample(&mut rng),
1692                StandardNormal.sample(&mut rng),
1693            ]);
1694            let z = Su2Algebra([
1695                StandardNormal.sample(&mut rng),
1696                StandardNormal.sample(&mut rng),
1697                StandardNormal.sample(&mut rng),
1698            ]);
1699
1700            // Compute [X, [Y, Z]]
1701            let yz = y.bracket(&z);
1702            let term1 = x.bracket(&yz);
1703
1704            // Compute [Y, [Z, X]]
1705            let zx = z.bracket(&x);
1706            let term2 = y.bracket(&zx);
1707
1708            // Compute [Z, [X, Y]]
1709            let xy = x.bracket(&y);
1710            let term3 = z.bracket(&xy);
1711
1712            // Sum should be zero
1713            let sum = term1.add(&term2).add(&term3);
1714            assert!(
1715                sum.norm() < 1e-10,
1716                "Jacobi identity failed: ||[X,[Y,Z]] + [Y,[Z,X]] + [Z,[X,Y]]|| = {:.2e}",
1717                sum.norm()
1718            );
1719        }
1720    }
1721
1722    /// **Exp-Log Round-Trip Test**
1723    ///
1724    /// For any algebra element X with ||X|| < π, we should have:
1725    /// ```text
1726    /// log(exp(X)) = X
1727    /// ```
1728    #[test]
1729    fn test_exp_log_roundtrip() {
1730        use crate::traits::{LieAlgebra, LieGroup};
1731        use rand::SeedableRng;
1732        use rand_distr::{Distribution, Uniform};
1733
1734        let mut rng = rand::rngs::StdRng::seed_from_u64(54321);
1735        let dist = Uniform::new(-2.0, 2.0); // Stay within log domain
1736
1737        for _ in 0..100 {
1738            let x = Su2Algebra([
1739                dist.sample(&mut rng),
1740                dist.sample(&mut rng),
1741                dist.sample(&mut rng),
1742            ]);
1743
1744            // exp then log
1745            let g = SU2::exp(&x);
1746            let x_recovered = g.log().expect("log should succeed for exp output");
1747
1748            // Should recover original (up to numerical precision)
1749            let diff = x.add(&x_recovered.scale(-1.0));
1750            assert!(
1751                diff.norm() < 1e-10,
1752                "log(exp(X)) should equal X: ||diff|| = {:.2e}",
1753                diff.norm()
1754            );
1755        }
1756    }
1757
1758    /// **Log-Exp Round-Trip Test**
1759    ///
1760    /// For any group element g, we should have:
1761    /// ```text
1762    /// exp(log(g)) = g
1763    /// ```
1764    #[test]
1765    #[cfg(feature = "rand")]
1766    fn test_log_exp_roundtrip() {
1767        use crate::traits::LieGroup;
1768        use rand::SeedableRng;
1769
1770        let mut rng = rand::rngs::StdRng::seed_from_u64(98765);
1771
1772        for _ in 0..100 {
1773            let g = SU2::random_haar(&mut rng);
1774
1775            // log then exp
1776            let x = g.log().expect("log should succeed for valid SU(2) element");
1777            let g_recovered = SU2::exp(&x);
1778
1779            // Should recover original (numerical precision varies with rotation angle)
1780            let diff = g.compose(&g_recovered.inverse()).distance_to_identity();
1781            assert!(
1782                diff < 1e-7,
1783                "exp(log(g)) should equal g: diff = {:.2e}",
1784                diff
1785            );
1786        }
1787    }
1788
1789    #[test]
1790    #[cfg(feature = "rand")]
1791    fn test_random_haar_distribution() {
1792        use rand::SeedableRng;
1793
1794        let mut rng = rand::rngs::StdRng::seed_from_u64(123);
1795
1796        // Generate many samples and check statistical properties
1797        let samples: Vec<SU2> = (0..1000).map(|_| SU2::random_haar(&mut rng)).collect();
1798
1799        // Check that average distance to identity is approximately correct
1800        // For uniform distribution on SU(2) ≅ S³, the expected distance is non-trivial
1801        let mean_distance: f64 =
1802            samples.iter().map(SU2::distance_to_identity).sum::<f64>() / samples.len() as f64;
1803
1804        // For S³ with uniform (Haar) measure, the mean geodesic distance from identity
1805        // is approximately π (the maximum is π for antipodal points).
1806        // Empirically, the mean is close to π.
1807        assert!(
1808            mean_distance > 2.5 && mean_distance < 3.5,
1809            "Mean distance from identity should be ~π, got {}",
1810            mean_distance
1811        );
1812
1813        // Check that some elements are far from identity
1814        let far_from_identity = samples
1815            .iter()
1816            .filter(|g| g.distance_to_identity() > std::f64::consts::PI / 2.0)
1817            .count();
1818
1819        assert!(
1820            far_from_identity > 100,
1821            "Should have many elements far from identity, got {}",
1822            far_from_identity
1823        );
1824    }
1825
1826    #[test]
1827    fn test_adjoint_action_simple() {
1828        use crate::traits::LieGroup;
1829
1830        // Test with identity: Ad_e(X) = X
1831        let e = SU2::identity();
1832        let x = Su2Algebra([1.0, 0.0, 0.0]);
1833        let result = e.adjoint_action(&x);
1834
1835        println!("Identity test: X = {:?}, Ad_e(X) = {:?}", x, result);
1836        assert!((result.0[0] - x.0[0]).abs() < 1e-10);
1837        assert!((result.0[1] - x.0[1]).abs() < 1e-10);
1838        assert!((result.0[2] - x.0[2]).abs() < 1e-10);
1839
1840        // Test with rotation around Z by 90 degrees
1841        // Should rotate X basis element into Y basis element
1842        let g = SU2::rotation_z(std::f64::consts::FRAC_PI_2);
1843        let x_basis = Su2Algebra([1.0, 0.0, 0.0]);
1844        let rotated = g.adjoint_action(&x_basis);
1845
1846        println!(
1847            "Rotation test: X = {:?}, Ad_{{Rz(π/2)}}(X) = {:?}",
1848            x_basis, rotated
1849        );
1850        // Should be approximately (0, 1, 0) - rotated 90° around Z
1851    }
1852
1853    // ========================================================================
1854    // Casimir Operator Tests
1855    // ========================================================================
1856
1857    #[test]
1858    fn test_su2_casimir_scalar() {
1859        // j = 0 (scalar): c₂ = 0
1860        use crate::representation::Spin;
1861        use crate::Casimir;
1862
1863        let c2 = Su2Algebra::quadratic_casimir_eigenvalue(&Spin::ZERO);
1864        assert_eq!(c2, 0.0, "Casimir of scalar representation should be 0");
1865    }
1866
1867    #[test]
1868    fn test_su2_casimir_spinor() {
1869        // j = 1/2 (spinor): c₂ = 3/4
1870        use crate::representation::Spin;
1871        use crate::Casimir;
1872
1873        let c2 = Su2Algebra::quadratic_casimir_eigenvalue(&Spin::HALF);
1874        assert_eq!(c2, 0.75, "Casimir of spinor representation should be 3/4");
1875    }
1876
1877    #[test]
1878    fn test_su2_casimir_vector() {
1879        // j = 1 (vector/adjoint): c₂ = 2
1880        use crate::representation::Spin;
1881        use crate::Casimir;
1882
1883        let c2 = Su2Algebra::quadratic_casimir_eigenvalue(&Spin::ONE);
1884        assert_eq!(c2, 2.0, "Casimir of vector representation should be 2");
1885    }
1886
1887    #[test]
1888    fn test_su2_casimir_j_three_halves() {
1889        // j = 3/2: c₂ = 15/4
1890        use crate::representation::Spin;
1891        use crate::Casimir;
1892
1893        let j_three_halves = Spin::from_half_integer(3);
1894        let c2 = Su2Algebra::quadratic_casimir_eigenvalue(&j_three_halves);
1895        assert_eq!(c2, 3.75, "Casimir for j=3/2 should be 15/4 = 3.75");
1896    }
1897
1898    #[test]
1899    fn test_su2_casimir_formula() {
1900        // Test the general formula c₂(j) = j(j+1) for various spins
1901        use crate::representation::Spin;
1902        use crate::Casimir;
1903
1904        for two_j in 0..10 {
1905            let spin = Spin::from_half_integer(two_j);
1906            let j = spin.value();
1907            let c2 = Su2Algebra::quadratic_casimir_eigenvalue(&spin);
1908            let expected = j * (j + 1.0);
1909
1910            assert_relative_eq!(c2, expected, epsilon = 1e-10);
1911        }
1912    }
1913
1914    #[test]
1915    fn test_su2_rank() {
1916        // SU(2) has rank 1
1917        use crate::Casimir;
1918
1919        assert_eq!(Su2Algebra::rank(), 1, "SU(2) should have rank 1");
1920        assert_eq!(
1921            Su2Algebra::num_casimirs(),
1922            1,
1923            "SU(2) should have 1 Casimir operator"
1924        );
1925    }
1926
1927    // ========================================================================
1928    // Stable Logarithm Tests
1929    // ========================================================================
1930
1931    #[test]
1932    fn test_log_stable_identity() {
1933        let e = SU2::identity();
1934        let log_e = e.log_stable().expect("log of identity should succeed");
1935        assert!(log_e.norm() < 1e-10, "log(I) should be zero");
1936    }
1937
1938    #[test]
1939    fn test_log_stable_small_rotation() {
1940        let theta = 0.1;
1941        let g = SU2::rotation_x(theta);
1942        let log_g = g
1943            .log_stable()
1944            .expect("log of small rotation should succeed");
1945
1946        // Should give θ * (1, 0, 0)
1947        assert!((log_g.0[0] - theta).abs() < 1e-10);
1948        assert!(log_g.0[1].abs() < 1e-10);
1949        assert!(log_g.0[2].abs() < 1e-10);
1950    }
1951
1952    #[test]
1953    fn test_log_stable_large_rotation() {
1954        let theta = 2.5; // Less than π
1955        let g = SU2::rotation_y(theta);
1956        let log_g = g.log_stable().expect("log of rotation < π should succeed");
1957
1958        // Should give θ * (0, 1, 0)
1959        assert!(log_g.0[0].abs() < 1e-10);
1960        assert!((log_g.0[1] - theta).abs() < 1e-10);
1961        assert!(log_g.0[2].abs() < 1e-10);
1962    }
1963
1964    #[test]
1965    fn test_log_stable_vs_log_consistency() {
1966        use crate::traits::{LieAlgebra, LieGroup};
1967
1968        // For rotations away from 2π, log_stable and log should agree
1969        // Note: θ = π is NOT a singularity for SU(2), so we include it.
1970        for theta in [0.1, 0.5, 1.0, 1.5, 2.0, 2.5, std::f64::consts::PI] {
1971            let g = SU2::rotation_z(theta);
1972            let log_standard = g.log().expect("log should succeed");
1973            let log_stable = g.log_stable().expect("log_stable should succeed");
1974
1975            let diff = log_standard.add(&log_stable.scale(-1.0)).norm();
1976            assert!(
1977                diff < 1e-10,
1978                "log and log_stable should agree for θ = {}: diff = {:.2e}",
1979                theta,
1980                diff
1981            );
1982        }
1983    }
1984
1985    #[test]
1986    fn test_log_with_condition_returns_condition() {
1987        let theta = 1.0;
1988        let g = SU2::rotation_x(theta);
1989        let (log_g, cond) = g
1990            .log_with_condition()
1991            .expect("log_with_condition should succeed");
1992
1993        // Should be well-conditioned for θ = 1
1994        assert!(
1995            cond.is_well_conditioned(),
1996            "θ = 1 should be well-conditioned"
1997        );
1998        assert!(
1999            (cond.angle - theta).abs() < 1e-10,
2000            "reported angle should match"
2001        );
2002        assert!((log_g.0[0] - theta).abs() < 1e-10);
2003    }
2004
2005    #[test]
2006    fn test_log_with_condition_near_pi() {
2007        // At θ ≈ π, sin(θ/2) ≈ 1, so axis extraction is perfectly stable.
2008        // The cut locus for SU(2) is at θ = 2π (U = -I), not θ = π.
2009        let theta = std::f64::consts::PI - 0.01;
2010        let g = SU2::rotation_z(theta);
2011        let (log_g, cond) = g
2012            .log_with_condition()
2013            .expect("log_with_condition should return best-effort");
2014
2015        // At θ ≈ π, sin(θ/2) ≈ 1, so numerical extraction is stable
2016        assert!(
2017            cond.is_well_conditioned(),
2018            "θ ≈ π should be numerically well-conditioned: κ = {}",
2019            cond.condition_number
2020        );
2021
2022        // Distance to cut locus (2π) should be about π
2023        assert!(
2024            (cond.distance_to_cut_locus - (std::f64::consts::PI + 0.01)).abs() < 1e-10,
2025            "distance to cut locus should be ≈ π + 0.01: got {}",
2026            cond.distance_to_cut_locus
2027        );
2028
2029        // Result should be correct
2030        assert!((log_g.0[2] - theta).abs() < 1e-6);
2031    }
2032
2033    #[test]
2034    fn test_log_condition_from_angle() {
2035        use crate::{LogCondition, LogQuality};
2036
2037        // Small angle θ = 0.5: sin(0.25) ≈ 0.247, κ ≈ 4.0 → Good
2038        // The condition number tracks stability of axis extraction (dividing by sin(θ/2))
2039        let cond_small = LogCondition::from_angle(0.5);
2040        assert_eq!(cond_small.quality, LogQuality::Good);
2041        assert!(cond_small.condition_number > 2.0 && cond_small.condition_number < 10.0);
2042
2043        // π/2: sin(π/4) ≈ 0.707, κ ≈ 1.4 → Excellent
2044        let cond_half_pi = LogCondition::from_angle(std::f64::consts::FRAC_PI_2);
2045        assert_eq!(cond_half_pi.quality, LogQuality::Excellent);
2046        assert!(cond_half_pi.is_well_conditioned());
2047
2048        // Near π: sin((π-0.001)/2) ≈ 1, κ ≈ 1.0 → Excellent (numerically stable)
2049        // Note: This is NUMERICALLY stable even though the axis is MATHEMATICALLY ambiguous
2050        let cond_near_pi = LogCondition::from_angle(std::f64::consts::PI - 0.001);
2051        assert!(
2052            cond_near_pi.is_well_conditioned(),
2053            "Near π should be numerically stable: κ = {}",
2054            cond_near_pi.condition_number
2055        );
2056
2057        // Near zero: sin(0.001/2) ≈ 0.0005, κ ≈ 2000 → AtSingularity
2058        // This is where axis extraction becomes unstable (dividing by small number)
2059        let cond_near_zero = LogCondition::from_angle(0.001);
2060        assert!(
2061            !cond_near_zero.is_well_conditioned(),
2062            "Near zero should have poor conditioning: κ = {}",
2063            cond_near_zero.condition_number
2064        );
2065    }
2066
2067    #[test]
2068    fn test_log_stable_roundtrip() {
2069        use crate::traits::{LieAlgebra, LieGroup};
2070        use rand::SeedableRng;
2071        use rand_distr::{Distribution, Uniform};
2072
2073        let mut rng = rand::rngs::StdRng::seed_from_u64(99999);
2074        let dist = Uniform::new(-2.5, 2.5); // Stay within log domain but include large angles
2075
2076        for _ in 0..100 {
2077            let x = Su2Algebra([
2078                dist.sample(&mut rng),
2079                dist.sample(&mut rng),
2080                dist.sample(&mut rng),
2081            ]);
2082
2083            // Skip elements near the singularity
2084            if x.norm() > std::f64::consts::PI - 0.2 {
2085                continue;
2086            }
2087
2088            // exp then log_stable
2089            let g = SU2::exp(&x);
2090            let x_recovered = g.log_stable().expect("log_stable should succeed");
2091
2092            // Should recover original
2093            let diff = x.add(&x_recovered.scale(-1.0));
2094            assert!(
2095                diff.norm() < 1e-8,
2096                "log_stable(exp(X)) should equal X: ||diff|| = {:.2e}",
2097                diff.norm()
2098            );
2099        }
2100    }
2101
2102    #[test]
2103    fn test_log_quality_display() {
2104        use crate::LogQuality;
2105
2106        assert_eq!(format!("{}", LogQuality::Excellent), "Excellent");
2107        assert_eq!(format!("{}", LogQuality::Good), "Good");
2108        assert_eq!(format!("{}", LogQuality::Acceptable), "Acceptable");
2109        assert_eq!(format!("{}", LogQuality::Poor), "Poor");
2110        assert_eq!(format!("{}", LogQuality::AtSingularity), "AtSingularity");
2111    }
2112}