Skip to main content

lie_groups/
so3.rs

1//! Lie group SO(3) - 3D rotation group
2//!
3//! SO(3) is the group of 3×3 real orthogonal matrices with determinant 1.
4//! It represents rotations in 3-dimensional Euclidean space.
5//!
6//! # Mathematical Structure
7//!
8//! ```text
9//! SO(3) = { R ∈ ℝ³ˣ³ | R^T R = I, det(R) = 1 }
10//! ```
11//!
12//! # Lie Algebra
13//!
14//! The Lie algebra so(3) consists of 3×3 real antisymmetric matrices:
15//! ```text
16//! so(3) = { X ∈ ℝ³ˣ³ | X^T = -X }
17//! ```
18//!
19//! This is 3-dimensional, naturally isomorphic to ℝ³ via the cross product:
20//! ```text
21//! X ↔ v  where  X·w = v × w  for all w ∈ ℝ³
22//! ```
23//!
24//! # Relationship to SU(2)
25//!
26//! SO(3) ≅ SU(2)/ℤ₂ — SU(2) is the double cover of SO(3).
27//! Both have 3-dimensional Lie algebras: so(3) ≅ su(2) ≅ ℝ³
28//!
29//! # Sign Convention
30//!
31//! The so(3) bracket is the cross product: `[v, w] = v × w`, giving
32//! structure constants `fᵢⱼₖ = +εᵢⱼₖ`. The su(2) bracket (in the `{iσ/2}`
33//! basis) uses `[X, Y] = -(X × Y)`, giving `fᵢⱼₖ = -εᵢⱼₖ`.
34//!
35//! The bracket-preserving isomorphism φ: su(2) → so(3) maps `eₐ ↦ -Lₐ`,
36//! so that `φ([eₐ, eᵦ]) = [φ(eₐ), φ(eᵦ)]`. This sign arises from the
37//! standard `{iσ/2}` basis choice for su(2).
38
39use crate::traits::{AntiHermitianByConstruction, LieAlgebra, LieGroup, TracelessByConstruction};
40use nalgebra::Matrix3;
41use std::fmt;
42use std::ops::{Add, Mul, MulAssign, Neg, Sub};
43
44/// Lie algebra so(3) ≅ ℝ³
45///
46/// Elements of so(3) are 3×3 real antisymmetric matrices, which we represent
47/// as 3-vectors via the natural isomorphism with ℝ³.
48///
49/// # Isomorphism with ℝ³
50///
51/// An element v = (x, y, z) ∈ ℝ³ corresponds to the antisymmetric matrix:
52/// ```text
53/// [v]_× = [[0, -z, y], [z, 0, -x], [-y, x, 0]]
54/// ```
55///
56/// This satisfies: `[v]_×` · w = v × w (cross product)
57///
58/// # Basis Elements
59///
60/// The standard basis corresponds to angular momentum operators:
61/// ```text
62/// L_x = (1, 0, 0) ↔ [[0, 0, 0], [0, 0, -1], [0, 1, 0]]
63/// L_y = (0, 1, 0) ↔ [[0, 0, 1], [0, 0, 0], [-1, 0, 0]]
64/// L_z = (0, 0, 1) ↔ [[0, -1, 0], [1, 0, 0], [0, 0, 0]]
65/// ```
66#[derive(Clone, Copy, Debug, PartialEq)]
67pub struct So3Algebra(pub [f64; 3]);
68
69impl Add for So3Algebra {
70    type Output = Self;
71    fn add(self, rhs: Self) -> Self {
72        Self([
73            self.0[0] + rhs.0[0],
74            self.0[1] + rhs.0[1],
75            self.0[2] + rhs.0[2],
76        ])
77    }
78}
79
80impl Add<&So3Algebra> for So3Algebra {
81    type Output = So3Algebra;
82    fn add(self, rhs: &So3Algebra) -> So3Algebra {
83        self + *rhs
84    }
85}
86
87impl Add<So3Algebra> for &So3Algebra {
88    type Output = So3Algebra;
89    fn add(self, rhs: So3Algebra) -> So3Algebra {
90        *self + rhs
91    }
92}
93
94impl Add<&So3Algebra> for &So3Algebra {
95    type Output = So3Algebra;
96    fn add(self, rhs: &So3Algebra) -> So3Algebra {
97        *self + *rhs
98    }
99}
100
101impl Sub for So3Algebra {
102    type Output = Self;
103    fn sub(self, rhs: Self) -> Self {
104        Self([
105            self.0[0] - rhs.0[0],
106            self.0[1] - rhs.0[1],
107            self.0[2] - rhs.0[2],
108        ])
109    }
110}
111
112impl Neg for So3Algebra {
113    type Output = Self;
114    fn neg(self) -> Self {
115        Self([-self.0[0], -self.0[1], -self.0[2]])
116    }
117}
118
119impl Mul<f64> for So3Algebra {
120    type Output = Self;
121    fn mul(self, scalar: f64) -> Self {
122        Self([self.0[0] * scalar, self.0[1] * scalar, self.0[2] * scalar])
123    }
124}
125
126impl Mul<So3Algebra> for f64 {
127    type Output = So3Algebra;
128    fn mul(self, rhs: So3Algebra) -> So3Algebra {
129        rhs * self
130    }
131}
132
133impl LieAlgebra for So3Algebra {
134    const DIM: usize = 3;
135
136    #[inline]
137    fn zero() -> Self {
138        Self([0.0, 0.0, 0.0])
139    }
140
141    #[inline]
142    fn add(&self, other: &Self) -> Self {
143        Self([
144            self.0[0] + other.0[0],
145            self.0[1] + other.0[1],
146            self.0[2] + other.0[2],
147        ])
148    }
149
150    #[inline]
151    fn scale(&self, scalar: f64) -> Self {
152        Self([self.0[0] * scalar, self.0[1] * scalar, self.0[2] * scalar])
153    }
154
155    #[inline]
156    fn norm(&self) -> f64 {
157        (self.0[0].powi(2) + self.0[1].powi(2) + self.0[2].powi(2)).sqrt()
158    }
159
160    #[inline]
161    fn basis_element(i: usize) -> Self {
162        assert!(i < 3, "SO(3) algebra is 3-dimensional");
163        let mut v = [0.0; 3];
164        v[i] = 1.0;
165        Self(v)
166    }
167
168    #[inline]
169    fn from_components(components: &[f64]) -> Self {
170        assert_eq!(components.len(), 3, "so(3) has dimension 3");
171        Self([components[0], components[1], components[2]])
172    }
173
174    #[inline]
175    fn to_components(&self) -> Vec<f64> {
176        self.0.to_vec()
177    }
178
179    /// Lie bracket for so(3): [v, w] = v × w (cross product)
180    ///
181    /// For so(3) ≅ ℝ³, the Lie bracket is exactly the vector cross product.
182    ///
183    /// # Properties
184    ///
185    /// - Antisymmetric: [v, w] = -[w, v]
186    /// - Jacobi identity: [u, [v, w]] + [v, [w, u]] + [w, [u, v]] = 0
187    ///
188    /// # Examples
189    ///
190    /// ```
191    /// use lie_groups::so3::So3Algebra;
192    /// use lie_groups::traits::LieAlgebra;
193    ///
194    /// let lx = So3Algebra::basis_element(0);  // (1, 0, 0)
195    /// let ly = So3Algebra::basis_element(1);  // (0, 1, 0)
196    /// let bracket = lx.bracket(&ly);           // (1,0,0) × (0,1,0) = (0,0,1)
197    ///
198    /// // Should give L_z = (0, 0, 1)
199    /// assert!((bracket.0[0]).abs() < 1e-10);
200    /// assert!((bracket.0[1]).abs() < 1e-10);
201    /// assert!((bracket.0[2] - 1.0).abs() < 1e-10);
202    /// ```
203    #[inline]
204    fn bracket(&self, other: &Self) -> Self {
205        // Cross product: v × w
206        let v = self.0;
207        let w = other.0;
208
209        Self([
210            v[1] * w[2] - v[2] * w[1], // x component
211            v[2] * w[0] - v[0] * w[2], // y component
212            v[0] * w[1] - v[1] * w[0], // z component
213        ])
214    }
215}
216
217/// SO(3) group element - 3×3 real orthogonal matrix with determinant 1
218///
219/// Represents a rotation in 3D space.
220///
221/// # Representation
222///
223/// We use `Matrix3<f64>` from nalgebra to represent the 3×3 rotation matrix.
224///
225/// # Constraints
226///
227/// - Orthogonality: R^T R = I
228/// - Determinant: det(R) = 1
229///
230/// # Examples
231///
232/// ```
233/// use lie_groups::so3::SO3;
234/// use lie_groups::traits::LieGroup;
235///
236/// // Rotation around Z-axis by π/2
237/// let rot = SO3::rotation_z(std::f64::consts::FRAC_PI_2);
238///
239/// // Verify it's orthogonal
240/// assert!(rot.verify_orthogonality(1e-10));
241/// ```
242#[derive(Clone, Debug, PartialEq)]
243pub struct SO3 {
244    /// 3×3 real orthogonal matrix
245    pub matrix: Matrix3<f64>,
246}
247
248impl SO3 {
249    /// Identity element (no rotation)
250    #[must_use]
251    pub fn identity() -> Self {
252        Self {
253            matrix: Matrix3::identity(),
254        }
255    }
256
257    /// Rotation around X-axis by angle θ (in radians)
258    ///
259    /// ```text
260    /// R_x(θ) = [[1, 0, 0], [0, cos(θ), -sin(θ)], [0, sin(θ), cos(θ)]]
261    /// ```
262    #[must_use]
263    pub fn rotation_x(angle: f64) -> Self {
264        let c = angle.cos();
265        let s = angle.sin();
266
267        Self {
268            matrix: Matrix3::new(1.0, 0.0, 0.0, 0.0, c, -s, 0.0, s, c),
269        }
270    }
271
272    /// Rotation around Y-axis by angle θ (in radians)
273    ///
274    /// ```text
275    /// R_y(θ) = [[cos(θ), 0, sin(θ)], [0, 1, 0], [-sin(θ), 0, cos(θ)]]
276    /// ```
277    #[must_use]
278    pub fn rotation_y(angle: f64) -> Self {
279        let c = angle.cos();
280        let s = angle.sin();
281
282        Self {
283            matrix: Matrix3::new(c, 0.0, s, 0.0, 1.0, 0.0, -s, 0.0, c),
284        }
285    }
286
287    /// Rotation around Z-axis by angle θ (in radians)
288    ///
289    /// ```text
290    /// R_z(θ) = [[cos(θ), -sin(θ), 0], [sin(θ), cos(θ), 0], [0, 0, 1]]
291    /// ```
292    #[must_use]
293    pub fn rotation_z(angle: f64) -> Self {
294        let c = angle.cos();
295        let s = angle.sin();
296
297        Self {
298            matrix: Matrix3::new(c, -s, 0.0, s, c, 0.0, 0.0, 0.0, 1.0),
299        }
300    }
301
302    /// Rotation around arbitrary axis by angle
303    ///
304    /// Uses Rodrigues' rotation formula:
305    /// ```text
306    /// R(θ, n̂) = I + sin(θ)[n̂]_× + (1-cos(θ))[n̂]_ײ
307    /// ```
308    ///
309    /// where [n̂]_× is the skew-symmetric matrix for axis n̂.
310    #[must_use]
311    pub fn rotation(axis: [f64; 3], angle: f64) -> Self {
312        let norm = (axis[0].powi(2) + axis[1].powi(2) + axis[2].powi(2)).sqrt();
313        if norm < 1e-10 {
314            return Self::identity();
315        }
316
317        // Normalize axis
318        let n = [axis[0] / norm, axis[1] / norm, axis[2] / norm];
319
320        // Rodrigues formula
321        let c = angle.cos();
322        let s = angle.sin();
323        let t = 1.0 - c;
324
325        let matrix = Matrix3::new(
326            t * n[0] * n[0] + c,
327            t * n[0] * n[1] - s * n[2],
328            t * n[0] * n[2] + s * n[1],
329            t * n[0] * n[1] + s * n[2],
330            t * n[1] * n[1] + c,
331            t * n[1] * n[2] - s * n[0],
332            t * n[0] * n[2] - s * n[1],
333            t * n[1] * n[2] + s * n[0],
334            t * n[2] * n[2] + c,
335        );
336
337        Self { matrix }
338    }
339
340    /// Verify orthogonality: R^T R = I
341    #[must_use]
342    pub fn verify_orthogonality(&self, tolerance: f64) -> bool {
343        let product = self.matrix.transpose() * self.matrix;
344        let identity = Matrix3::identity();
345
346        (product - identity).norm() < tolerance
347    }
348
349    /// Matrix inverse (equals transpose for orthogonal matrices)
350    #[must_use]
351    pub fn inverse(&self) -> Self {
352        Self {
353            matrix: self.matrix.transpose(),
354        }
355    }
356
357    /// Distance from identity (rotation angle)
358    ///
359    /// For a rotation matrix R, the angle θ satisfies:
360    /// ```text
361    /// trace(R) = 1 + 2cos(θ)
362    /// ```
363    #[must_use]
364    pub fn distance_to_identity(&self) -> f64 {
365        let trace = self.matrix.trace();
366        let cos_theta = (trace - 1.0) / 2.0;
367
368        // Clamp to [-1, 1] to handle numerical errors
369        let cos_theta = cos_theta.clamp(-1.0, 1.0);
370
371        cos_theta.acos()
372    }
373
374    /// Interpolate between two SO(3) elements with proper orthogonalization
375    ///
376    /// Uses linear interpolation of matrix elements followed by Gram-Schmidt
377    /// orthogonalization to ensure the result stays on SO(3).
378    ///
379    /// # Arguments
380    /// * `other` - The target rotation
381    /// * `t` - Interpolation parameter in [0, 1]
382    ///
383    /// # Returns
384    /// An SO(3) element: self at t=0, other at t=1
385    ///
386    /// # Note
387    /// This is NOT geodesic interpolation (SLERP). For true geodesic paths,
388    /// convert to quaternions and use quaternion SLERP. However, this method
389    /// guarantees the result is always a valid rotation matrix.
390    #[must_use]
391    pub fn interpolate(&self, other: &Self, t: f64) -> Self {
392        if t <= 0.0 {
393            return self.clone();
394        }
395        if t >= 1.0 {
396            return other.clone();
397        }
398
399        // Linear interpolation of matrix elements
400        let interpolated = self.matrix * (1.0 - t) + other.matrix * t;
401
402        // Gram-Schmidt orthogonalization to project back onto SO(3)
403        Self::gram_schmidt_orthogonalize(interpolated)
404    }
405
406    /// Orthogonalize a matrix using Gram-Schmidt process
407    ///
408    /// Takes a near-orthogonal matrix and projects it back onto SO(3).
409    /// This is essential for numerical stability when matrices drift
410    /// from orthogonality due to floating-point accumulation.
411    ///
412    /// # Algorithm
413    /// 1. Normalize first column
414    /// 2. Orthogonalize and normalize second column
415    /// 3. Compute third column as cross product (ensures determinant = 1)
416    #[must_use]
417    pub fn gram_schmidt_orthogonalize(matrix: Matrix3<f64>) -> Self {
418        use nalgebra::Vector3;
419
420        // Extract columns
421        let c0 = Vector3::new(matrix[(0, 0)], matrix[(1, 0)], matrix[(2, 0)]);
422        let c1 = Vector3::new(matrix[(0, 1)], matrix[(1, 1)], matrix[(2, 1)]);
423
424        // Gram-Schmidt orthogonalization
425        let e0 = c0.normalize();
426        let e1 = (c1 - e0 * e0.dot(&c1)).normalize();
427        // Third column is cross product to ensure det = +1 (proper rotation)
428        let e2 = e0.cross(&e1);
429
430        let orthogonal = Matrix3::new(
431            e0[0], e1[0], e2[0], e0[1], e1[1], e2[1], e0[2], e1[2], e2[2],
432        );
433
434        Self { matrix: orthogonal }
435    }
436
437    /// Re-normalize a rotation matrix that may have drifted
438    ///
439    /// Use this periodically after many matrix multiplications to
440    /// prevent numerical drift from accumulating.
441    #[must_use]
442    pub fn renormalize(&self) -> Self {
443        Self::gram_schmidt_orthogonalize(self.matrix)
444    }
445
446    /// Geodesic distance between two SO(3) elements
447    ///
448    /// d(R₁, R₂) = ||log(R₁ᵀ R₂)||
449    ///
450    /// This is the rotation angle of the relative rotation R₁ᵀ R₂.
451    #[must_use]
452    pub fn geodesic_distance(&self, other: &Self) -> f64 {
453        // Compute relative rotation: R_rel = R₁ᵀ R₂
454        let relative = Self {
455            matrix: self.matrix.transpose() * other.matrix,
456        };
457        relative.distance_to_identity()
458    }
459}
460
461impl fmt::Display for So3Algebra {
462    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
463        write!(
464            f,
465            "so(3)[{:.4}, {:.4}, {:.4}]",
466            self.0[0], self.0[1], self.0[2]
467        )
468    }
469}
470
471impl fmt::Display for SO3 {
472    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
473        let dist = self.distance_to_identity();
474        write!(f, "SO(3)(θ={:.4})", dist)
475    }
476}
477
478/// Group multiplication: R₁ · R₂
479impl Mul<&SO3> for &SO3 {
480    type Output = SO3;
481    fn mul(self, rhs: &SO3) -> SO3 {
482        SO3 {
483            matrix: self.matrix * rhs.matrix,
484        }
485    }
486}
487
488impl Mul<&SO3> for SO3 {
489    type Output = SO3;
490    fn mul(self, rhs: &SO3) -> SO3 {
491        &self * rhs
492    }
493}
494
495impl MulAssign<&SO3> for SO3 {
496    fn mul_assign(&mut self, rhs: &SO3) {
497        self.matrix *= rhs.matrix;
498    }
499}
500
501impl LieGroup for SO3 {
502    const DIM: usize = 3;
503
504    type Algebra = So3Algebra;
505
506    fn identity() -> Self {
507        Self::identity()
508    }
509
510    fn compose(&self, other: &Self) -> Self {
511        Self {
512            matrix: self.matrix * other.matrix,
513        }
514    }
515
516    fn inverse(&self) -> Self {
517        Self::inverse(self)
518    }
519
520    fn adjoint(&self) -> Self {
521        // For orthogonal matrices, adjoint = transpose = inverse
522        self.inverse()
523    }
524
525    fn adjoint_action(&self, algebra_element: &So3Algebra) -> So3Algebra {
526        // For SO(3): Ad_R(v) = R v (matrix-vector multiplication)
527        //
528        // This corresponds to rotating the axis v by rotation R
529        let v = algebra_element.0;
530        let rotated = self.matrix * nalgebra::Vector3::new(v[0], v[1], v[2]);
531
532        So3Algebra([rotated[0], rotated[1], rotated[2]])
533    }
534
535    fn distance_to_identity(&self) -> f64 {
536        Self::distance_to_identity(self)
537    }
538
539    fn exp(tangent: &So3Algebra) -> Self {
540        // Exponential map using Rodrigues formula
541        let angle = tangent.norm();
542
543        if angle < 1e-10 {
544            return Self::identity();
545        }
546
547        let axis = [
548            tangent.0[0] / angle,
549            tangent.0[1] / angle,
550            tangent.0[2] / angle,
551        ];
552
553        Self::rotation(axis, angle)
554    }
555
556    fn log(&self) -> crate::error::LogResult<So3Algebra> {
557        use crate::error::LogError;
558
559        // For SO(3), extract rotation angle and axis from the rotation matrix
560        //
561        // The angle θ satisfies:
562        // Tr(R) = 1 + 2·cos(θ)
563        //
564        // The axis n̂ can be extracted from the skew-symmetric part:
565        // R - R^T = 2·sin(θ)·[n̂]×
566        // where [n̂]× is the cross-product matrix
567
568        let trace = self.matrix.trace();
569
570        // Extract angle: θ = arccos((Tr(R) - 1) / 2)
571        let cos_theta = ((trace - 1.0) / 2.0).clamp(-1.0, 1.0);
572        let theta = cos_theta.acos();
573
574        const SMALL_ANGLE_THRESHOLD: f64 = 1e-10;
575        const SINGULARITY_THRESHOLD: f64 = 1e-6;
576
577        if theta.abs() < SMALL_ANGLE_THRESHOLD {
578            // Near identity: log(I) = 0
579            return Ok(So3Algebra::zero());
580        }
581
582        if (theta - std::f64::consts::PI).abs() < SINGULARITY_THRESHOLD {
583            // Near singularity at θ = π (rotation by 180°)
584            // At exactly θ = π, the axis direction is ambiguous
585            return Err(LogError::Singularity {
586                reason: format!(
587                    "SO(3) element at rotation angle θ ≈ π (θ = {:.6}), axis is ambiguous",
588                    theta
589                ),
590            });
591        }
592
593        // Extract rotation axis from skew-symmetric part
594        // R - R^T = 2·sin(θ)·[n̂]×
595        //
596        // [n̂]× = [[ 0,  -nz,  ny],
597        //         [ nz,  0,  -nx],
598        //         [-ny,  nx,  0 ]]
599        //
600        // So:
601        // R[2,1] - R[1,2] = 2·sin(θ)·nx
602        // R[0,2] - R[2,0] = 2·sin(θ)·ny
603        // R[1,0] - R[0,1] = 2·sin(θ)·nz
604
605        let sin_theta = theta.sin();
606
607        if sin_theta.abs() < 1e-10 {
608            // This shouldn't happen given our checks above, but guard against it
609            return Err(LogError::NumericalInstability {
610                reason: "sin(θ) too small for reliable axis extraction".to_string(),
611            });
612        }
613
614        let two_sin_theta = 2.0 * sin_theta;
615        let nx = (self.matrix[(2, 1)] - self.matrix[(1, 2)]) / two_sin_theta;
616        let ny = (self.matrix[(0, 2)] - self.matrix[(2, 0)]) / two_sin_theta;
617        let nz = (self.matrix[(1, 0)] - self.matrix[(0, 1)]) / two_sin_theta;
618
619        // The logarithm is log(R) = θ·(nx, ny, nz) ∈ so(3)
620        Ok(So3Algebra([theta * nx, theta * ny, theta * nz]))
621    }
622
623    fn dim() -> usize {
624        3 // SO(3) is 3-dimensional (3×3 matrices)
625    }
626
627    fn trace(&self) -> num_complex::Complex<f64> {
628        // For real matrices, trace is real
629        num_complex::Complex::new(self.matrix.trace(), 0.0)
630    }
631}
632
633// ============================================================================
634// Mathematical Property Implementations
635// ============================================================================
636
637use crate::traits::{Compact, SemiSimple, Simple};
638
639/// SO(3) is compact
640///
641/// The rotation group is diffeomorphic to ℝP³ (real projective 3-space).
642/// All rotations are bounded: ||R|| = 1.
643impl Compact for SO3 {}
644
645/// SO(3) is simple
646///
647/// It has no non-trivial normal subgroups (for dimension > 2).
648impl Simple for SO3 {}
649
650/// SO(3) is semi-simple
651impl SemiSimple for SO3 {}
652
653// ============================================================================
654// Algebra Marker Traits
655// ============================================================================
656
657/// so(3) algebra elements are traceless by construction.
658///
659/// The representation `So3Algebra([f64; 3])` stores coefficients for
660/// 3×3 antisymmetric matrices. All antisymmetric matrices are traceless.
661impl TracelessByConstruction for So3Algebra {}
662
663/// so(3) algebra elements are anti-Hermitian by construction.
664///
665/// Real antisymmetric matrices satisfy A^T = -A, which over ℝ is
666/// equivalent to A† = -A (anti-Hermitian).
667impl AntiHermitianByConstruction for So3Algebra {}
668
669#[cfg(test)]
670mod tests {
671    use super::*;
672    use approx::assert_relative_eq;
673
674    #[test]
675    fn test_identity() {
676        let id = SO3::identity();
677        assert!(id.verify_orthogonality(1e-10));
678        assert_relative_eq!(id.distance_to_identity(), 0.0, epsilon = 1e-10);
679    }
680
681    #[test]
682    fn test_rotations_orthogonal() {
683        let rx = SO3::rotation_x(0.5);
684        let ry = SO3::rotation_y(1.2);
685        let rz = SO3::rotation_z(2.1);
686
687        assert!(rx.verify_orthogonality(1e-10));
688        assert!(ry.verify_orthogonality(1e-10));
689        assert!(rz.verify_orthogonality(1e-10));
690    }
691
692    #[test]
693    fn test_inverse() {
694        let r = SO3::rotation_x(0.7);
695        let r_inv = r.inverse();
696        let product = r.compose(&r_inv);
697
698        assert_relative_eq!(product.distance_to_identity(), 0.0, epsilon = 1e-10);
699    }
700
701    #[test]
702    fn test_bracket_antisymmetry() {
703        use crate::traits::LieAlgebra;
704
705        let v = So3Algebra([0.7, -0.3, 1.2]);
706        let w = So3Algebra([-0.5, 0.9, 0.4]);
707
708        let vw = v.bracket(&w);
709        let wv = w.bracket(&v);
710
711        // [v, w] = -[w, v]
712        for i in 0..3 {
713            assert_relative_eq!(vw.0[i], -wv.0[i], epsilon = 1e-14);
714        }
715    }
716
717    #[test]
718    fn test_bracket_bilinearity() {
719        use crate::traits::LieAlgebra;
720
721        let x = So3Algebra([1.0, 0.0, 0.0]);
722        let y = So3Algebra([0.0, 1.0, 0.0]);
723        let z = So3Algebra([0.0, 0.0, 1.0]);
724        let alpha = 2.5;
725
726        // Left linearity: [αX + Y, Z] = α[X, Z] + [Y, Z]
727        let lhs = x.scale(alpha).add(&y).bracket(&z);
728        let rhs = x.bracket(&z).scale(alpha).add(&y.bracket(&z));
729        for i in 0..3 {
730            assert_relative_eq!(lhs.0[i], rhs.0[i], epsilon = 1e-14);
731        }
732
733        // Right linearity: [Z, αX + Y] = α[Z, X] + [Z, Y]
734        let lhs = z.bracket(&x.scale(alpha).add(&y));
735        let rhs = z.bracket(&x).scale(alpha).add(&z.bracket(&y));
736        for i in 0..3 {
737            assert_relative_eq!(lhs.0[i], rhs.0[i], epsilon = 1e-14);
738        }
739    }
740
741    #[test]
742    fn test_algebra_bracket() {
743        use crate::traits::LieAlgebra;
744
745        let lx = So3Algebra::basis_element(0);
746        let ly = So3Algebra::basis_element(1);
747        let bracket = lx.bracket(&ly);
748
749        // [L_x, L_y] = L_z
750        assert_relative_eq!(bracket.0[0], 0.0, epsilon = 1e-10);
751        assert_relative_eq!(bracket.0[1], 0.0, epsilon = 1e-10);
752        assert_relative_eq!(bracket.0[2], 1.0, epsilon = 1e-10);
753    }
754
755    #[test]
756    fn test_adjoint_action() {
757        use crate::traits::LieGroup;
758
759        // Rotate L_x by 90° around Z
760        let rz = SO3::rotation_z(std::f64::consts::FRAC_PI_2);
761        let lx = So3Algebra([1.0, 0.0, 0.0]);
762        let rotated = rz.adjoint_action(&lx);
763
764        // Should give L_y = (0, 1, 0)
765        assert_relative_eq!(rotated.0[0], 0.0, epsilon = 1e-10);
766        assert_relative_eq!(rotated.0[1], 1.0, epsilon = 1e-10);
767        assert_relative_eq!(rotated.0[2], 0.0, epsilon = 1e-10);
768    }
769
770    #[test]
771    fn test_interpolate_orthogonality() {
772        // Interpolation should always produce orthogonal matrices
773        let r1 = SO3::rotation_x(0.3);
774        let r2 = SO3::rotation_y(1.2);
775
776        for t in [0.0, 0.25, 0.5, 0.75, 1.0] {
777            let interp = r1.interpolate(&r2, t);
778            assert!(
779                interp.verify_orthogonality(1e-10),
780                "Interpolated matrix not orthogonal at t={}",
781                t
782            );
783        }
784    }
785
786    #[test]
787    fn test_interpolate_endpoints() {
788        let r1 = SO3::rotation_z(0.5);
789        let r2 = SO3::rotation_z(1.5);
790
791        let at_0 = r1.interpolate(&r2, 0.0);
792        let at_1 = r1.interpolate(&r2, 1.0);
793
794        assert_relative_eq!(
795            at_0.distance_to_identity(),
796            r1.distance_to_identity(),
797            epsilon = 1e-10
798        );
799        assert_relative_eq!(
800            at_1.distance_to_identity(),
801            r2.distance_to_identity(),
802            epsilon = 1e-10
803        );
804    }
805
806    #[test]
807    fn test_gram_schmidt_orthogonalize() {
808        // Create a slightly perturbed (non-orthogonal) matrix
809        let perturbed = Matrix3::new(1.001, 0.01, 0.0, 0.0, 0.999, 0.01, 0.0, 0.0, 1.002);
810
811        let fixed = SO3::gram_schmidt_orthogonalize(perturbed);
812        assert!(fixed.verify_orthogonality(1e-10));
813
814        // Check determinant is +1 (proper rotation)
815        let det = fixed.matrix.determinant();
816        assert_relative_eq!(det, 1.0, epsilon = 1e-10);
817    }
818
819    #[test]
820    fn test_renormalize() {
821        // Simulate drift from many multiplications
822        let r = SO3::rotation_z(0.001);
823        let mut accumulated = SO3::identity();
824
825        for _ in 0..1000 {
826            accumulated = accumulated.compose(&r);
827        }
828
829        // May have drifted slightly
830        let renormalized = accumulated.renormalize();
831        assert!(renormalized.verify_orthogonality(1e-12));
832    }
833
834    #[test]
835    fn test_geodesic_distance_symmetric() {
836        let r1 = SO3::rotation_x(0.5);
837        let r2 = SO3::rotation_y(1.0);
838
839        let d12 = r1.geodesic_distance(&r2);
840        let d21 = r2.geodesic_distance(&r1);
841
842        assert_relative_eq!(d12, d21, epsilon = 1e-10);
843    }
844
845    #[test]
846    fn test_exp_log_roundtrip() {
847        use crate::traits::{LieAlgebra, LieGroup};
848
849        // Small angles (well within convergence)
850        for &angle in &[0.1, 0.5, 1.0, 2.0] {
851            let v = So3Algebra([angle * 0.6, angle * 0.8, 0.0]);
852            let r = SO3::exp(&v);
853            assert!(r.verify_orthogonality(1e-10));
854
855            let v_back = SO3::log(&r).expect("log should succeed for small angles");
856            let diff = v.add(&v_back.scale(-1.0));
857            assert!(
858                diff.norm() < 1e-9,
859                "exp/log roundtrip failed at angle {}: error = {:.2e}",
860                angle,
861                diff.norm()
862            );
863        }
864    }
865
866    #[test]
867    fn test_log_exp_roundtrip() {
868        use crate::traits::LieGroup;
869
870        // Start from group elements, take log, then exp back
871        let rotations = [
872            SO3::rotation_x(0.7),
873            SO3::rotation_y(1.3),
874            SO3::rotation_z(0.4),
875            SO3::rotation_x(0.3).compose(&SO3::rotation_y(0.5)),
876        ];
877
878        for (i, r) in rotations.iter().enumerate() {
879            let v = SO3::log(r).expect("log should succeed");
880            let r_back = SO3::exp(&v);
881            let dist = r.geodesic_distance(&r_back);
882            assert!(
883                dist < 1e-9,
884                "log/exp roundtrip failed for rotation {}: distance = {:.2e}",
885                i,
886                dist
887            );
888        }
889    }
890
891    #[test]
892    fn test_jacobi_identity() {
893        use crate::traits::LieAlgebra;
894
895        let x = So3Algebra([1.0, 0.0, 0.0]);
896        let y = So3Algebra([0.0, 1.0, 0.0]);
897        let z = So3Algebra([0.0, 0.0, 1.0]);
898
899        // [X, [Y, Z]] + [Y, [Z, X]] + [Z, [X, Y]] = 0
900        let t1 = x.bracket(&y.bracket(&z));
901        let t2 = y.bracket(&z.bracket(&x));
902        let t3 = z.bracket(&x.bracket(&y));
903        let sum = t1.add(&t2).add(&t3);
904
905        assert!(
906            sum.norm() < 1e-14,
907            "Jacobi identity violated: ||sum|| = {:.2e}",
908            sum.norm()
909        );
910    }
911
912    #[test]
913    fn test_geodesic_distance_to_self() {
914        let r = SO3::rotation([1.0, 2.0, 3.0], 0.8);
915        let d = r.geodesic_distance(&r);
916        assert_relative_eq!(d, 0.0, epsilon = 1e-10);
917    }
918
919    #[test]
920    fn test_geodesic_distance_to_identity() {
921        let angle = 0.7;
922        let r = SO3::rotation_z(angle);
923
924        let d = SO3::identity().geodesic_distance(&r);
925        assert_relative_eq!(d, angle, epsilon = 1e-10);
926    }
927
928    // ========================================================================
929    // Property-Based Tests for Group Axioms
930    // ========================================================================
931    //
932    // These tests use proptest to verify that SO(3) satisfies the
933    // mathematical axioms of a Lie group for randomly generated elements.
934    //
935    // This is a form of **specification-based testing**: the group axioms
936    // are the specification, and we verify they hold for all inputs.
937    //
938    // Run with: cargo test --features nightly
939
940    #[cfg(feature = "proptest")]
941    use proptest::prelude::*;
942
943    /// Strategy for generating arbitrary SO(3) elements.
944    ///
945    /// We generate SO(3) elements by composing three Euler rotations:
946    /// `R = R_z(α) · R_y(β) · R_x(γ)`
947    ///
948    /// This gives good coverage of SO(3) ≅ ℝP³.
949    #[cfg(feature = "proptest")]
950    fn arb_so3() -> impl Strategy<Value = SO3> {
951        use std::f64::consts::TAU;
952
953        // Generate three Euler angles
954        let alpha = 0.0..TAU;
955        let beta = 0.0..TAU;
956        let gamma = 0.0..TAU;
957
958        (alpha, beta, gamma).prop_map(|(a, b, c)| {
959            SO3::rotation_z(a)
960                .compose(&SO3::rotation_y(b))
961                .compose(&SO3::rotation_x(c))
962        })
963    }
964
965    /// Strategy for generating arbitrary `So3Algebra` elements.
966    ///
967    /// We generate algebra elements by picking random coefficients in [-π, π]
968    /// for each of the three basis directions (`L_x`, `L_y`, `L_z`).
969    #[cfg(feature = "proptest")]
970    fn arb_so3_algebra() -> impl Strategy<Value = So3Algebra> {
971        use std::f64::consts::PI;
972
973        ((-PI..PI), (-PI..PI), (-PI..PI)).prop_map(|(a, b, c)| So3Algebra([a, b, c]))
974    }
975
976    #[cfg(feature = "proptest")]
977    proptest! {
978        /// **Group Axiom 1: Identity Element**
979        ///
980        /// For all R ∈ SO(3):
981        /// - I · R = R (left identity)
982        /// - R · I = R (right identity)
983        ///
984        /// where I = identity rotation
985        ///
986        /// Note: We use tolerance 1e-7 to account for floating-point
987        /// rounding errors in matrix operations.
988        #[test]
989        fn prop_identity_axiom(r in arb_so3()) {
990            let e = SO3::identity();
991
992            // Left identity: I · R = R
993            let left = e.compose(&r);
994            prop_assert!(
995                left.distance(&r) < 1e-7,
996                "Left identity failed: I·R != R, distance = {}",
997                left.distance(&r)
998            );
999
1000            // Right identity: R · I = R
1001            let right = r.compose(&e);
1002            prop_assert!(
1003                right.distance(&r) < 1e-7,
1004                "Right identity failed: R·I != R, distance = {}",
1005                right.distance(&r)
1006            );
1007        }
1008
1009        /// **Group Axiom 2: Inverse Element**
1010        ///
1011        /// For all R ∈ SO(3):
1012        /// - R · R⁻¹ = I (right inverse)
1013        /// - R⁻¹ · R = I (left inverse)
1014        ///
1015        /// where R⁻¹ = inverse of R (equals R^T for orthogonal matrices)
1016        ///
1017        /// Note: We use tolerance 1e-7 to account for floating-point
1018        /// rounding errors in matrix operations.
1019        #[test]
1020        fn prop_inverse_axiom(r in arb_so3()) {
1021            let r_inv = r.inverse();
1022
1023            // Right inverse: R · R⁻¹ = I
1024            let right_product = r.compose(&r_inv);
1025            prop_assert!(
1026                right_product.is_near_identity(1e-7),
1027                "Right inverse failed: R·R⁻¹ != I, distance = {}",
1028                right_product.distance_to_identity()
1029            );
1030
1031            // Left inverse: R⁻¹ · R = I
1032            let left_product = r_inv.compose(&r);
1033            prop_assert!(
1034                left_product.is_near_identity(1e-7),
1035                "Left inverse failed: R⁻¹·R != I, distance = {}",
1036                left_product.distance_to_identity()
1037            );
1038        }
1039
1040        /// **Group Axiom 3: Associativity**
1041        ///
1042        /// For all R₁, R₂, R₃ ∈ SO(3):
1043        /// - (R₁ · R₂) · R₃ = R₁ · (R₂ · R₃)
1044        ///
1045        /// Group multiplication is associative.
1046        ///
1047        /// Note: We use tolerance 1e-7 to account for floating-point
1048        /// rounding errors in matrix operations.
1049        #[test]
1050        fn prop_associativity(r1 in arb_so3(), r2 in arb_so3(), r3 in arb_so3()) {
1051            // Left association: (R₁ · R₂) · R₃
1052            let left_assoc = r1.compose(&r2).compose(&r3);
1053
1054            // Right association: R₁ · (R₂ · R₃)
1055            let right_assoc = r1.compose(&r2.compose(&r3));
1056
1057            prop_assert!(
1058                left_assoc.distance(&right_assoc) < 1e-7,
1059                "Associativity failed: (R₁·R₂)·R₃ != R₁·(R₂·R₃), distance = {}",
1060                left_assoc.distance(&right_assoc)
1061            );
1062        }
1063
1064        /// **Lie Group Property: Inverse is Smooth**
1065        ///
1066        /// For SO(3), the inverse operation is smooth (continuously differentiable).
1067        /// We verify this by checking that nearby elements have nearby inverses.
1068        #[test]
1069        fn prop_inverse_continuity(r in arb_so3()) {
1070            // Create a small perturbation
1071            let epsilon = 0.01;
1072            let perturbation = SO3::rotation_x(epsilon);
1073            let r_perturbed = r.compose(&perturbation);
1074
1075            // Check that inverses are close
1076            let inv_distance = r.inverse().distance(&r_perturbed.inverse());
1077
1078            prop_assert!(
1079                inv_distance < 0.1,
1080                "Inverse not continuous: small perturbation caused large inverse change, distance = {}",
1081                inv_distance
1082            );
1083        }
1084
1085        /// **Orthogonality Preservation**
1086        ///
1087        /// All SO(3) operations should preserve orthogonality.
1088        /// This is not strictly a group axiom, but it's essential for SO(3).
1089        #[test]
1090        fn prop_orthogonality_preserved(r1 in arb_so3(), r2 in arb_so3()) {
1091            // Composition preserves orthogonality
1092            let product = r1.compose(&r2);
1093            prop_assert!(
1094                product.verify_orthogonality(1e-10),
1095                "Composition violated orthogonality"
1096            );
1097
1098            // Inverse preserves orthogonality
1099            let inv = r1.inverse();
1100            prop_assert!(
1101                inv.verify_orthogonality(1e-10),
1102                "Inverse violated orthogonality"
1103            );
1104        }
1105
1106        /// **Adjoint Representation: Group Homomorphism**
1107        ///
1108        /// The adjoint representation Ad: G → Aut(𝔤) is a group homomorphism:
1109        /// - Ad_{R₁∘R₂}(v) = Ad_{R₁}(Ad_{R₂}(v))
1110        ///
1111        /// This is a fundamental property that must hold for the adjoint action
1112        /// to be a valid representation of the group.
1113        #[test]
1114        fn prop_adjoint_homomorphism(
1115            r1 in arb_so3(),
1116            r2 in arb_so3(),
1117            v in arb_so3_algebra()
1118        ) {
1119            // Compute Ad_{R₁∘R₂}(v)
1120            let r_composed = r1.compose(&r2);
1121            let left = r_composed.adjoint_action(&v);
1122
1123            // Compute Ad_{R₁}(Ad_{R₂}(v))
1124            let ad_r2_v = r2.adjoint_action(&v);
1125            let right = r1.adjoint_action(&ad_r2_v);
1126
1127            // They should be equal
1128            let diff = left.add(&right.scale(-1.0));
1129            prop_assert!(
1130                diff.norm() < 1e-7,
1131                "Adjoint homomorphism failed: Ad_{{R₁∘R₂}}(v) != Ad_{{R₁}}(Ad_{{R₂}}(v)), diff norm = {}",
1132                diff.norm()
1133            );
1134        }
1135
1136        /// **Adjoint Representation: Identity Action**
1137        ///
1138        /// The identity element acts trivially on the Lie algebra:
1139        /// - Ad_I(v) = v for all v ∈ so(3)
1140        #[test]
1141        fn prop_adjoint_identity(v in arb_so3_algebra()) {
1142            let e = SO3::identity();
1143            let result = e.adjoint_action(&v);
1144
1145            let diff = result.add(&v.scale(-1.0));
1146            prop_assert!(
1147                diff.norm() < 1e-10,
1148                "Identity action failed: Ad_I(v) != v, diff norm = {}",
1149                diff.norm()
1150            );
1151        }
1152
1153        /// **Adjoint Representation: Lie Bracket Preservation**
1154        ///
1155        /// The adjoint representation preserves the Lie bracket:
1156        /// - Ad_R([v,w]) = [Ad_R(v), Ad_R(w)]
1157        ///
1158        /// This is a critical property that ensures the adjoint action
1159        /// is a Lie algebra automorphism.
1160        #[test]
1161        fn prop_adjoint_bracket_preservation(
1162            r in arb_so3(),
1163            v in arb_so3_algebra(),
1164            w in arb_so3_algebra()
1165        ) {
1166            use crate::traits::LieAlgebra;
1167
1168            // Compute Ad_R([v,w])
1169            let bracket_vw = v.bracket(&w);
1170            let left = r.adjoint_action(&bracket_vw);
1171
1172            // Compute [Ad_R(v), Ad_R(w)]
1173            let ad_v = r.adjoint_action(&v);
1174            let ad_w = r.adjoint_action(&w);
1175            let right = ad_v.bracket(&ad_w);
1176
1177            // They should be equal
1178            let diff = left.add(&right.scale(-1.0));
1179            prop_assert!(
1180                diff.norm() < 1e-6,
1181                "Bracket preservation failed: Ad_R([v,w]) != [Ad_R(v), Ad_R(w)], diff norm = {}",
1182                diff.norm()
1183            );
1184        }
1185
1186        /// **Adjoint Representation: Inverse Property**
1187        ///
1188        /// The inverse of an element acts as the inverse transformation:
1189        /// - Ad_{R⁻¹}(Ad_R(v)) = v
1190        #[test]
1191        fn prop_adjoint_inverse(r in arb_so3(), v in arb_so3_algebra()) {
1192            // Apply Ad_R then Ad_{R⁻¹}
1193            let ad_r_v = r.adjoint_action(&v);
1194            let r_inv = r.inverse();
1195            let result = r_inv.adjoint_action(&ad_r_v);
1196
1197            // Should recover v
1198            let diff = result.add(&v.scale(-1.0));
1199            prop_assert!(
1200                diff.norm() < 1e-7,
1201                "Inverse property failed: Ad_{{R⁻¹}}(Ad_R(v)) != v, diff norm = {}",
1202                diff.norm()
1203            );
1204        }
1205    }
1206}