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}