Skip to main content

apex_manifolds/
so3.rs

1//! SO3 - Special Orthogonal Group in 3D
2//!
3//! This module implements the Special Orthogonal group SO(3), which represents
4//! rotations in 3D space.
5//!
6//! SO(3) elements are represented using nalgebra's UnitQuaternion internally.
7//! SO(3) tangent elements are represented as axis-angle vectors in R³,
8//! where the direction gives the axis of rotation and the magnitude gives the angle.
9//!
10//! The implementation follows the [manif](https://github.com/artivis/manif) C++ library
11//! conventions and provides all operations required by the LieGroup and Tangent traits.
12//!
13//! # Numerical Conditioning
14//!
15//! The Jacobian inverses (Jr⁻¹, Jl⁻¹) contain the term `(1 + cos θ) / (2θ sin θ)` which
16//! is poorly conditioned near θ = 0 (indeterminate 0/0) and θ = π (singularity).
17//! Expected precision for Jr * Jr⁻¹ ≈ I:
18//!
19//! | Angle range     | Precision |
20//! |-----------------|-----------|
21//! | θ < 0.1 rad     | ~1e-8     |
22//! | 0.1–0.5 rad     | ~1e-6     |
23//! | 0.5–π/2 rad     | ~1e-4     |
24//! | θ > π/2 rad     | ~0.01     |
25//!
26//! Composition chains accumulate drift multiplicatively — re-normalize quaternions
27//! periodically for long chains. These properties are consistent with production
28//! Lie group libraries (manif, Sophus, GTSAM).
29//!
30//! ## References
31//!
32//! - Nurlanov et al. (2021): "Exploring SO(3) logarithmic map: degeneracies and derivatives"
33//! - Sophus library: <https://github.com/strasdat/Sophus/issues/179>
34
35use crate::{LieGroup, Tangent};
36use nalgebra::{DVector, Matrix3, Matrix4, Quaternion, Unit, UnitQuaternion, Vector3};
37use std::{
38    fmt,
39    fmt::{Display, Formatter},
40};
41
42/// SO(3) group element representing rotations in 3D.
43///
44/// Internally represented using nalgebra's UnitQuaternion<f64> for efficient rotations.
45#[derive(Clone, PartialEq)]
46pub struct SO3 {
47    /// Internal representation as a unit quaternion
48    quaternion: UnitQuaternion<f64>,
49}
50
51impl Display for SO3 {
52    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
53        let q = self.quaternion.quaternion();
54        write!(
55            f,
56            "SO3(quaternion: [w: {:.4}, x: {:.4}, y: {:.4}, z: {:.4}])",
57            q.w, q.i, q.j, q.k
58        )
59    }
60}
61
62impl SO3 {
63    /// Space dimension - dimension of the ambient space that the group acts on
64    pub const DIM: usize = 3;
65
66    /// Degrees of freedom - dimension of the tangent space
67    pub const DOF: usize = 3;
68
69    /// Representation size - size of the underlying data representation
70    pub const REP_SIZE: usize = 4;
71
72    /// Get the identity element of the group.
73    ///
74    /// Returns the neutral element e such that e ∘ g = g ∘ e = g for any group element g.
75    pub fn identity() -> Self {
76        SO3 {
77            quaternion: UnitQuaternion::identity(),
78        }
79    }
80
81    /// Get the identity matrix for Jacobians.
82    ///
83    /// Returns the identity matrix in the appropriate dimension for Jacobian computations.
84    pub fn jacobian_identity() -> Matrix3<f64> {
85        Matrix3::<f64>::identity()
86    }
87
88    /// Create a new SO(3) element from a unit quaternion.
89    ///
90    /// # Arguments
91    /// * `quaternion` - Unit quaternion representing rotation
92    #[inline]
93    pub fn new(quaternion: UnitQuaternion<f64>) -> Self {
94        SO3 { quaternion }
95    }
96
97    /// Create SO(3) from quaternion coefficients in G2O convention `[x, y, z, w]`.
98    ///
99    /// This parameter order matches the G2O file format where quaternion components
100    /// are stored as `(qx, qy, qz, qw)`. For nalgebra's `(w, x, y, z)` convention,
101    /// use [`from_quaternion_wxyz()`](Self::from_quaternion_wxyz).
102    ///
103    /// # Arguments
104    /// * `x` - i component of quaternion
105    /// * `y` - j component of quaternion
106    /// * `z` - k component of quaternion
107    /// * `w` - w (real) component of quaternion
108    pub fn from_quaternion_coeffs(x: f64, y: f64, z: f64, w: f64) -> Self {
109        let q = Quaternion::new(w, x, y, z);
110        SO3::new(UnitQuaternion::from_quaternion(q))
111    }
112
113    /// Create SO(3) from quaternion coefficients in nalgebra convention `[w, x, y, z]`.
114    ///
115    /// This parameter order matches nalgebra's `Quaternion::new(w, x, y, z)`.
116    /// For G2O file format order `(qx, qy, qz, qw)`, use
117    /// [`from_quaternion_coeffs()`](Self::from_quaternion_coeffs).
118    ///
119    /// # Arguments
120    /// * `w` - w (real) component of quaternion
121    /// * `x` - i component of quaternion
122    /// * `y` - j component of quaternion
123    /// * `z` - k component of quaternion
124    pub fn from_quaternion_wxyz(w: f64, x: f64, y: f64, z: f64) -> Self {
125        let q = Quaternion::new(w, x, y, z);
126        SO3::new(UnitQuaternion::from_quaternion(q))
127    }
128
129    /// Create SO(3) from Euler angles (roll, pitch, yaw).
130    pub fn from_euler_angles(roll: f64, pitch: f64, yaw: f64) -> Self {
131        let quaternion = UnitQuaternion::from_euler_angles(roll, pitch, yaw);
132        SO3::new(quaternion)
133    }
134
135    /// Create SO(3) from axis-angle representation.
136    pub fn from_axis_angle(axis: &Vector3<f64>, angle: f64) -> Self {
137        let unit_axis = Unit::new_normalize(*axis);
138        let quaternion = UnitQuaternion::from_axis_angle(&unit_axis, angle);
139        SO3::new(quaternion)
140    }
141
142    /// Create SO(3) from scaled axis (axis-angle vector).
143    pub fn from_scaled_axis(axis_angle: Vector3<f64>) -> Self {
144        let quaternion = UnitQuaternion::from_scaled_axis(axis_angle);
145        SO3::new(quaternion)
146    }
147
148    /// Get the quaternion representation.
149    pub fn quaternion(&self) -> UnitQuaternion<f64> {
150        self.quaternion
151    }
152
153    /// Create SO3 from quaternion (alias for new)
154    pub fn from_quaternion(quaternion: UnitQuaternion<f64>) -> Self {
155        Self::new(quaternion)
156    }
157
158    /// Get the quaternion representation (alias for quaternion)
159    pub fn to_quaternion(&self) -> UnitQuaternion<f64> {
160        self.quaternion
161    }
162
163    /// Get the raw quaternion coefficients.
164    pub fn quat(&self) -> Quaternion<f64> {
165        *self.quaternion.quaternion()
166    }
167
168    /// Get the x component of the quaternion.
169    #[inline]
170    pub fn x(&self) -> f64 {
171        self.quaternion.i
172    }
173
174    /// Get the y component of the quaternion.
175    #[inline]
176    pub fn y(&self) -> f64 {
177        self.quaternion.j
178    }
179
180    /// Get the z component of the quaternion.
181    #[inline]
182    pub fn z(&self) -> f64 {
183        self.quaternion.k
184    }
185
186    /// Get the w component of the quaternion.
187    #[inline]
188    pub fn w(&self) -> f64 {
189        self.quaternion.w
190    }
191
192    /// Get the rotation matrix (3x3).
193    pub fn rotation_matrix(&self) -> Matrix3<f64> {
194        self.quaternion.to_rotation_matrix().into_inner()
195    }
196
197    /// Get the homogeneous transformation matrix (4x4).
198    pub fn transform(&self) -> Matrix4<f64> {
199        self.quaternion.to_homogeneous()
200    }
201
202    /// Set the quaternion from coefficients array [w, x, y, z].
203    pub fn set_quaternion(&mut self, coeffs: &[f64; 4]) {
204        let q = Quaternion::new(coeffs[0], coeffs[1], coeffs[2], coeffs[3]);
205        self.quaternion = UnitQuaternion::from_quaternion(q);
206    }
207
208    /// Get coefficients as array [w, x, y, z].
209    #[inline]
210    pub fn coeffs(&self) -> [f64; 4] {
211        let q = self.quaternion.quaternion();
212        [q.w, q.i, q.j, q.k]
213    }
214
215    /// Calculate the distance between two SO3 elements
216    ///
217    /// Computes the geodesic distance, which is the norm of the log map
218    /// of the relative rotation between the two elements.
219    pub fn distance(&self, other: &Self) -> f64 {
220        self.between(other, None, None).log(None).angle()
221    }
222}
223
224// Conversion traits for integration with generic Problem
225impl From<DVector<f64>> for SO3 {
226    fn from(data: DVector<f64>) -> Self {
227        SO3::from_quaternion_coeffs(data[0], data[1], data[2], data[3])
228    }
229}
230
231impl From<SO3> for DVector<f64> {
232    fn from(so3: SO3) -> Self {
233        DVector::from_vec(so3.coeffs().to_vec())
234    }
235}
236
237// Implement basic trait requirements for LieGroup
238impl LieGroup for SO3 {
239    type TangentVector = SO3Tangent;
240    type JacobianMatrix = Matrix3<f64>;
241    type LieAlgebra = Matrix3<f64>;
242
243    /// SO3 inverse.
244    ///
245    /// # Arguments
246    /// * `jacobian` - Optional Jacobian matrix of the inverse wrt self.
247    ///
248    /// # Notes
249    /// R⁻¹ = Rᵀ, for quaternions: q⁻¹ = q*
250    ///
251    /// # Equation 140: Jacobian of Inverse for SO(3)
252    /// J_R⁻¹_R = -Adj(R) = -R
253    ///
254    fn inverse(&self, jacobian: Option<&mut Self::JacobianMatrix>) -> Self {
255        let inverse_quat = self.quaternion.inverse();
256
257        if let Some(jac) = jacobian {
258            *jac = -self.adjoint();
259        }
260
261        SO3 {
262            quaternion: inverse_quat,
263        }
264    }
265
266    /// SO3 composition.
267    ///
268    /// # Arguments
269    /// * `other` - Another SO3 element.
270    /// * `jacobian_self` - Optional Jacobian matrix of the composition wrt self.
271    /// * `jacobian_other` - Optional Jacobian matrix of the composition wrt other.
272    ///
273    /// # Notes
274    /// # Equation 141: Jacobian of the composition wrt self.
275    /// J_QR_R = Adj(R⁻¹) = Rᵀ
276    ///
277    /// # Equation 142: Jacobian of the composition wrt other.
278    /// J_QR_Q = I
279    ///
280    fn compose(
281        &self,
282        other: &Self,
283        jacobian_self: Option<&mut Self::JacobianMatrix>,
284        jacobian_other: Option<&mut Self::JacobianMatrix>,
285    ) -> Self {
286        let result = SO3 {
287            quaternion: self.quaternion * other.quaternion,
288        };
289
290        if let Some(jac_self) = jacobian_self {
291            *jac_self = other.inverse(None).adjoint();
292        }
293
294        if let Some(jac_other) = jacobian_other {
295            *jac_other = Matrix3::identity();
296        }
297
298        result
299    }
300
301    /// Get the SO3 corresponding Lie algebra element in vector form.
302    ///
303    /// # Arguments
304    /// * `jacobian` - Optional Jacobian matrix of the tangent wrt to self.
305    ///
306    /// # Notes
307    /// # Equation 133: Logarithmic map for unit quaternions (S³)
308    /// θu = Log(q) = (2 / ||v||) * v * arctan(||v||, w) ∈ R³
309    ///
310    /// # Equation 144: Inverse of Right Jacobian for SO(3) Exp map
311    /// J_R⁻¹(θ) = I + (1/2) [θ]ₓ + (1/θ² - (1 + cos θ)/(2θ sin θ)) [θ]ₓ²
312    ///
313    fn log(&self, jacobian: Option<&mut Self::JacobianMatrix>) -> Self::TangentVector {
314        debug_assert!(
315            (self.quaternion.norm() - 1.0).abs() < 1e-6,
316            "SO3::log() requires normalized quaternion, norm = {}",
317            self.quaternion.norm()
318        );
319
320        let q = self.quaternion.quaternion();
321        let sin_angle_squared = q.i * q.i + q.j * q.j + q.k * q.k;
322
323        let log_coeff = if sin_angle_squared > crate::SMALL_ANGLE_THRESHOLD {
324            let sin_angle = sin_angle_squared.sqrt();
325            let cos_angle = q.w;
326
327            // When cos_angle < 0, the rotation angle θ > π/2.
328            // Using atan2(sin, cos) directly would give θ/2 in (π/4, π/2].
329            // By negating both arguments: atan2(-sin, -cos), we get θ/2 in
330            // (-3π/4, -π/2], which when doubled gives the rotation angle in the
331            // correct range [-π, π]. This avoids discontinuities and matches the
332            // manif C++ library convention. The key insight is that atan2(-y, -x)
333            // = atan2(y, x) - π (or + π), shifting the result by π.
334            let two_angle = 2.0
335                * if cos_angle < 0.0 {
336                    f64::atan2(-sin_angle, -cos_angle)
337                } else {
338                    f64::atan2(sin_angle, cos_angle)
339                };
340
341            two_angle / sin_angle
342        } else {
343            2.0
344        };
345
346        let axis_angle = SO3Tangent::new(Vector3::new(
347            q.i * log_coeff,
348            q.j * log_coeff,
349            q.k * log_coeff,
350        ));
351
352        if let Some(jac) = jacobian {
353            *jac = axis_angle.right_jacobian_inv();
354        }
355
356        axis_angle
357    }
358
359    fn act(
360        &self,
361        vector: &Vector3<f64>,
362        jacobian_self: Option<&mut Self::JacobianMatrix>,
363        jacobian_vector: Option<&mut Matrix3<f64>>,
364    ) -> Vector3<f64> {
365        let result = self.quaternion * vector;
366
367        if let Some(jac_self) = jacobian_self {
368            // -R * [v]×
369            let vector_hat = SO3Tangent::new(*vector).hat();
370            *jac_self = -self.rotation_matrix() * vector_hat;
371        }
372
373        if let Some(jac_vector) = jacobian_vector {
374            *jac_vector = self.rotation_matrix();
375        }
376
377        result
378    }
379
380    fn adjoint(&self) -> Self::JacobianMatrix {
381        self.rotation_matrix()
382    }
383
384    fn random() -> Self {
385        SO3 {
386            quaternion: UnitQuaternion::from_scaled_axis(Vector3::new(
387                rand::random::<f64>() * 2.0 - 1.0,
388                rand::random::<f64>() * 2.0 - 1.0,
389                rand::random::<f64>() * 2.0 - 1.0,
390            )),
391        }
392    }
393
394    fn jacobian_identity() -> Self::JacobianMatrix {
395        Matrix3::<f64>::identity()
396    }
397
398    fn zero_jacobian() -> Self::JacobianMatrix {
399        Matrix3::<f64>::zeros()
400    }
401
402    fn normalize(&mut self) {
403        let q = self.quaternion.into_inner().normalize();
404        self.quaternion = UnitQuaternion::from_quaternion(q);
405    }
406
407    fn is_valid(&self, tolerance: f64) -> bool {
408        // Check if the quaternion is normalized
409        let norm_diff = (self.quaternion.norm() - 1.0).abs();
410        norm_diff < tolerance
411    }
412
413    /// Vee operator: log(g)^∨.
414    ///
415    /// Maps a group element g ∈ G to its tangent vector log(g)^∨ ∈ 𝔤.
416    /// For SO(3), this is the same as log().
417    fn vee(&self) -> Self::TangentVector {
418        self.log(None)
419    }
420
421    /// Check if the element is approximately equal to another element.
422    ///
423    /// # Arguments
424    /// * `other` - The other element to compare with
425    /// * `tolerance` - The tolerance for the comparison
426    fn is_approx(&self, other: &Self, tolerance: f64) -> bool {
427        let difference = self.right_minus(other, None, None);
428        difference.is_zero(tolerance)
429    }
430}
431
432/// SO(3) tangent space element representing elements in the Lie algebra so(3).
433///
434/// Internally represented as axis-angle vectors in R³ where:
435/// - Direction: axis of rotation (unit vector)
436/// - Magnitude: angle of rotation (radians)
437#[derive(Clone, PartialEq)]
438pub struct SO3Tangent {
439    /// Internal data: axis-angle vector [θx, θy, θz]
440    data: Vector3<f64>,
441}
442
443impl fmt::Display for SO3Tangent {
444    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
445        write!(
446            f,
447            "so3(axis-angle: [{:.4}, {:.4}, {:.4}])",
448            self.data.x, self.data.y, self.data.z
449        )
450    }
451}
452
453// Conversion traits for integration with generic Problem
454impl From<DVector<f64>> for SO3Tangent {
455    fn from(data_vector: DVector<f64>) -> Self {
456        SO3Tangent {
457            data: Vector3::new(data_vector[0], data_vector[1], data_vector[2]),
458        }
459    }
460}
461
462impl From<SO3Tangent> for DVector<f64> {
463    fn from(so3_tangent: SO3Tangent) -> Self {
464        DVector::from_vec(vec![
465            so3_tangent.data.x,
466            so3_tangent.data.y,
467            so3_tangent.data.z,
468        ])
469    }
470}
471
472impl SO3Tangent {
473    /// Create a new SO3Tangent from axis-angle vector.
474    ///
475    /// # Arguments
476    /// * `axis_angle` - Axis-angle vector [θx, θy, θz]
477    #[inline]
478    pub fn new(axis_angle: Vector3<f64>) -> Self {
479        SO3Tangent { data: axis_angle }
480    }
481
482    /// Create SO3Tangent from individual components.
483    pub fn from_components(x: f64, y: f64, z: f64) -> Self {
484        SO3Tangent::new(Vector3::new(x, y, z))
485    }
486
487    /// Get the axis-angle vector.
488    #[inline]
489    pub fn axis_angle(&self) -> Vector3<f64> {
490        self.data
491    }
492
493    /// Get the angle of rotation.
494    #[inline]
495    pub fn angle(&self) -> f64 {
496        self.data.norm()
497    }
498
499    /// Get the axis of rotation (normalized).
500    #[inline]
501    pub fn axis(&self) -> Vector3<f64> {
502        let norm = self.data.norm();
503        if norm < f64::EPSILON {
504            Vector3::identity()
505        } else {
506            self.data / norm
507        }
508    }
509
510    /// Get the x component.
511    #[inline]
512    pub fn x(&self) -> f64 {
513        self.data.x
514    }
515
516    /// Get the y component.
517    #[inline]
518    pub fn y(&self) -> f64 {
519        self.data.y
520    }
521
522    /// Get the z component.
523    #[inline]
524    pub fn z(&self) -> f64 {
525        self.data.z
526    }
527
528    /// Get the coefficients as a vector.
529    #[inline]
530    pub fn coeffs(&self) -> Vector3<f64> {
531        self.data
532    }
533
534    /// Get angular velocity representation (alias for axis_angle).
535    pub fn ang(&self) -> Vector3<f64> {
536        self.data
537    }
538}
539
540// Implement LieAlgebra trait for SO3Tangent
541impl Tangent<SO3> for SO3Tangent {
542    /// Dimension of the tangent space
543    const DIM: usize = 3;
544
545    /// SO3 exponential map.
546    ///
547    /// # Arguments
548    /// * `tangent` - Tangent vector [θx, θy, θz]
549    /// * `jacobian` - Optional Jacobian matrix of the SO3 element wrt self.
550    ///
551    /// # Notes
552    /// # Equation 132: Exponential map for unit quaternions (S³)
553    /// q = Exp(θu) = cos(θ/2) + u sin(θ/2) ∈ H
554    ///
555    /// # Equation 143: Right Jacobian for SO(3) Exp map
556    /// J_R(θ) = I - (1 - cos θ)/θ² [θ]ₓ + (θ - sin θ)/θ³ [θ]ₓ²
557    ///
558    fn exp(&self, jacobian: Option<&mut <SO3 as LieGroup>::JacobianMatrix>) -> SO3 {
559        let theta_squared = self.data.norm_squared();
560
561        let quaternion = if theta_squared > crate::SMALL_ANGLE_THRESHOLD {
562            UnitQuaternion::from_scaled_axis(self.data)
563        } else {
564            UnitQuaternion::from_quaternion(Quaternion::new(
565                1.0,
566                self.data.x / 2.0,
567                self.data.y / 2.0,
568                self.data.z / 2.0,
569            ))
570        };
571
572        if let Some(jac) = jacobian {
573            *jac = self.right_jacobian();
574        }
575
576        SO3 { quaternion }
577    }
578
579    /// Right Jacobian for SO(3)
580    ///
581    /// # Notes
582    /// # Equation 143: Right Jacobian for SO(3) Exp map
583    /// J_R(θ) = I - (1 - cos θ)/θ² [θ]ₓ + (θ - sin θ)/θ³ [θ]ₓ²
584    ///
585    fn right_jacobian(&self) -> <SO3 as LieGroup>::JacobianMatrix {
586        self.left_jacobian().transpose()
587    }
588
589    /// Left Jacobian for SO(3)
590    ///
591    /// # Notes
592    /// # Equation 144: Left Jacobian for SO(3) Exp map
593    /// J_R⁻¹(θ) = I + (1 - cos θ)/θ² [θ]ₓ + (θ - sin θ)/θ³ [θ]ₓ²
594    ///
595    fn left_jacobian(&self) -> <SO3 as LieGroup>::JacobianMatrix {
596        let angle = self.data.norm_squared();
597        let tangent_skew = self.hat();
598
599        if angle <= crate::SMALL_ANGLE_THRESHOLD {
600            Matrix3::identity() + 0.5 * tangent_skew
601        } else {
602            let theta = angle.sqrt();
603            let sin_theta = theta.sin();
604            let cos_theta = theta.cos();
605
606            Matrix3::identity()
607                + (1.0 - cos_theta) / angle * tangent_skew
608                + (theta - sin_theta) / (angle * angle) * tangent_skew * tangent_skew
609        }
610    }
611
612    /// Right Jacobian inverse for SO(3)
613    ///
614    /// # Notes
615    /// # Equation 145: Right Jacobian inverse for SO(3) Exp map
616    /// J_R⁻¹(θ) = I + (1 - cos θ)/θ² [θ]ₓ + (θ - sin θ)/θ³ [θ]ₓ²
617    ///
618    /// # Numerical Conditioning Warning
619    ///
620    /// **This function has inherent numerical conditioning issues that cannot be eliminated.**
621    ///
622    /// The formula contains the term `(1 + cos θ) / (2θ sin θ)` which:
623    /// - Becomes indeterminate (0/0) as θ → 0
624    /// - Has a singularity as θ → π (sin θ → 0)
625    /// - Amplifies floating-point errors for θ > 0.5 rad
626    ///
627    /// **Expected Precision**:
628    /// - θ < 0.01 rad: Jr * Jr⁻¹ ≈ I within ~1e-6
629    /// - 0.01 < θ < 0.1 rad: Jr * Jr⁻¹ ≈ I within ~1e-4
630    /// - θ > 0.1 rad: Jr * Jr⁻¹ ≈ I within ~0.01
631    ///
632    /// This is **mathematically unavoidable** and is consistent with all production
633    /// Lie group libraries (manif, Sophus, GTSAM). See module documentation for references.
634    ///
635    fn right_jacobian_inv(&self) -> <SO3 as LieGroup>::JacobianMatrix {
636        self.left_jacobian_inv().transpose()
637    }
638
639    /// Left Jacobian inverse for SO(3)
640    ///
641    /// # Notes
642    /// # Equation 146: Left Jacobian inverse for SO(3) Exp map
643    /// J_L⁻¹(θ) = I - (1/2) [θ]ₓ + (1/θ² - (1 + cos θ)/(2θ sin θ)) [θ]ₓ²
644    ///
645    /// # Numerical Conditioning Warning
646    ///
647    /// **This function has inherent numerical conditioning issues that cannot be eliminated.**
648    ///
649    /// The problematic term `1/θ² - (1 + cos θ)/(2θ sin θ)` exhibits:
650    /// - Catastrophic cancellation as θ → 0 (requires Taylor series expansion)
651    /// - Division by zero as θ → π (sin θ → 0)
652    /// - Poor conditioning for θ > 0.5 rad (~29°)
653    ///
654    /// **Expected Precision** (same as right Jacobian inverse):
655    /// - θ < 0.01 rad: Jl * Jl⁻¹ ≈ I within ~1e-6
656    /// - 0.01 < θ < 0.1 rad: Jl * Jl⁻¹ ≈ I within ~1e-4
657    /// - θ > 0.1 rad: Jl * Jl⁻¹ ≈ I within ~0.01
658    ///
659    /// This is a **fundamental mathematical limitation** documented in:
660    /// - Nurlanov et al. (2021): SO(3) log map degeneracies
661    /// - Sophus library: <https://github.com/strasdat/Sophus/issues/179>
662    ///
663    fn left_jacobian_inv(&self) -> <SO3 as LieGroup>::JacobianMatrix {
664        let angle = self.data.norm_squared();
665        let tangent_skew = self.hat();
666
667        if angle <= crate::SMALL_ANGLE_THRESHOLD {
668            Matrix3::identity() - 0.5 * tangent_skew
669        } else {
670            let theta = angle.sqrt();
671            let sin_theta = theta.sin();
672            let cos_theta = theta.cos();
673
674            Matrix3::identity() - (0.5 * tangent_skew)
675                + (1.0 / angle - (1.0 + cos_theta) / (2.0 * theta * sin_theta))
676                    * tangent_skew
677                    * tangent_skew
678        }
679    }
680
681    /// Hat map for SO(3)
682    ///
683    /// # Notes
684    /// [θ]ₓ = [0 -θz θy; θz 0 -θx; -θy θx 0]
685    ///
686    fn hat(&self) -> <SO3 as LieGroup>::LieAlgebra {
687        Matrix3::new(
688            0.0,
689            -self.data.z,
690            self.data.y,
691            self.data.z,
692            0.0,
693            -self.data.x,
694            -self.data.y,
695            self.data.x,
696            0.0,
697        )
698    }
699
700    fn zero() -> <SO3 as LieGroup>::TangentVector {
701        Self::new(Vector3::zeros())
702    }
703
704    fn random() -> <SO3 as LieGroup>::TangentVector {
705        Self::new(Vector3::new(
706            rand::random::<f64>() * 0.2 - 0.1,
707            rand::random::<f64>() * 0.2 - 0.1,
708            rand::random::<f64>() * 0.2 - 0.1,
709        ))
710    }
711
712    fn is_zero(&self, tolerance: f64) -> bool {
713        self.data.norm() < tolerance
714    }
715
716    fn normalize(&mut self) {
717        let norm = self.data.norm();
718        if norm > f64::EPSILON {
719            self.data /= norm;
720        }
721    }
722
723    fn normalized(&self) -> <SO3 as LieGroup>::TangentVector {
724        let norm = self.data.norm();
725        if norm > f64::EPSILON {
726            SO3Tangent::new(self.data / norm)
727        } else {
728            Self::zero()
729        }
730    }
731
732    /// Small adjoint matrix for SO(3).
733    ///
734    /// For SO(3), the small adjoint is the skew-symmetric matrix (hat operator).
735    fn small_adj(&self) -> <SO3 as LieGroup>::JacobianMatrix {
736        self.hat()
737    }
738
739    /// Lie bracket for SO(3).
740    ///
741    /// Computes the Lie bracket [this, other] = this.small_adj() * other.
742    fn lie_bracket(&self, other: &Self) -> <SO3 as LieGroup>::TangentVector {
743        let bracket_result = self.small_adj() * other.data;
744        SO3Tangent::new(bracket_result)
745    }
746
747    /// Check if this tangent vector is approximately equal to another.
748    ///
749    /// # Arguments
750    /// * `other` - The other tangent vector to compare with
751    /// * `tolerance` - The tolerance for the comparison
752    fn is_approx(&self, other: &Self, tolerance: f64) -> bool {
753        (self.data - other.data).norm() < tolerance
754    }
755
756    /// Get the ith generator of the SO(3) Lie algebra.
757    ///
758    /// # Arguments
759    /// * `i` - Index of the generator (0, 1, or 2 for SO(3))
760    ///
761    /// # Returns
762    /// The generator matrix
763    fn generator(&self, i: usize) -> <SO3 as LieGroup>::LieAlgebra {
764        assert!(i < 3, "SO(3) only has generators for indices 0, 1, 2");
765
766        match i {
767            0 => {
768                // Generator E1 for x-axis rotation
769                Matrix3::new(0.0, 0.0, 0.0, 0.0, 0.0, -1.0, 0.0, 1.0, 0.0)
770            }
771            1 => {
772                // Generator E2 for y-axis rotation
773                Matrix3::new(0.0, 0.0, 1.0, 0.0, 0.0, 0.0, -1.0, 0.0, 0.0)
774            }
775            2 => {
776                // Generator E3 for z-axis rotation
777                Matrix3::new(0.0, -1.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0)
778            }
779            _ => unreachable!(),
780        }
781    }
782}
783
784#[cfg(test)]
785mod tests {
786    use super::*;
787    use core::f64;
788    use std::f64::consts::PI;
789
790    const TOLERANCE: f64 = 1e-12;
791
792    #[test]
793    fn test_so3_constructor_datatype() {
794        let so3 = SO3::from_quaternion_coeffs(0.0, 0.0, 0.0, 1.0);
795        assert_eq!(0.0, so3.x());
796        assert_eq!(0.0, so3.y());
797        assert_eq!(0.0, so3.z());
798        assert_eq!(1.0, so3.w());
799    }
800
801    #[test]
802    fn test_so3_constructor_quat() {
803        let quat = UnitQuaternion::identity();
804        let so3 = SO3::new(quat);
805        assert_eq!(0.0, so3.x());
806        assert_eq!(0.0, so3.y());
807        assert_eq!(0.0, so3.z());
808        assert_eq!(1.0, so3.w());
809    }
810
811    #[test]
812    fn test_so3_constructor_euler() {
813        let so3 = SO3::from_euler_angles(0.0, 0.0, 0.0);
814        assert_eq!(0.0, so3.x());
815        assert_eq!(0.0, so3.y());
816        assert_eq!(0.0, so3.z());
817        assert_eq!(1.0, so3.w());
818    }
819
820    #[test]
821    fn test_so3_identity() {
822        let so3 = SO3::identity();
823        assert_eq!(0.0, so3.x());
824        assert_eq!(0.0, so3.y());
825        assert_eq!(0.0, so3.z());
826        assert_eq!(1.0, so3.w());
827    }
828
829    #[test]
830    fn test_so3_coeffs() {
831        // Create from normalized coefficients
832        let so3 = SO3::from_quaternion_coeffs(0.0, 0.0, 0.0, 1.0);
833        let coeffs = so3.coeffs();
834        assert!((coeffs[0] - 1.0).abs() < TOLERANCE); // w
835        assert!((coeffs[1] - 0.0).abs() < TOLERANCE); // x
836        assert!((coeffs[2] - 0.0).abs() < TOLERANCE); // y
837        assert!((coeffs[3] - 0.0).abs() < TOLERANCE); // z
838
839        // Test with non-normalized input - should get normalized output
840        let so3 = SO3::from_quaternion_coeffs(0.1, 0.2, 0.3, 0.4);
841        let coeffs = so3.coeffs();
842        let original_quat = Quaternion::new(0.4, 0.1, 0.2, 0.3);
843        let normalized_quat = original_quat.normalize();
844        assert!((coeffs[0] - normalized_quat.w).abs() < TOLERANCE);
845        assert!((coeffs[1] - normalized_quat.i).abs() < TOLERANCE);
846        assert!((coeffs[2] - normalized_quat.j).abs() < TOLERANCE);
847        assert!((coeffs[3] - normalized_quat.k).abs() < TOLERANCE);
848    }
849
850    #[test]
851    fn test_so3_random() {
852        let so3 = SO3::random();
853        assert!((so3.quaternion().norm() - 1.0).abs() < TOLERANCE);
854    }
855
856    #[test]
857    fn test_so3_transform() {
858        let so3 = SO3::identity();
859        let transform = so3.transform();
860
861        assert_eq!(4, transform.nrows());
862        assert_eq!(4, transform.ncols());
863
864        // Check identity transform
865        for i in 0..4 {
866            for j in 0..4 {
867                if i == j {
868                    assert!((transform[(i, j)] - 1.0).abs() < TOLERANCE);
869                } else {
870                    assert!(transform[(i, j)].abs() < TOLERANCE);
871                }
872            }
873        }
874    }
875
876    #[test]
877    fn test_so3_rotation() {
878        let so3 = SO3::identity();
879        let rotation = so3.rotation_matrix();
880
881        assert_eq!(3, rotation.nrows());
882        assert_eq!(3, rotation.ncols());
883
884        // Check identity rotation
885        for i in 0..3 {
886            for j in 0..3 {
887                if i == j {
888                    assert!((rotation[(i, j)] - 1.0).abs() < TOLERANCE);
889                } else {
890                    assert!(rotation[(i, j)].abs() < TOLERANCE);
891                }
892            }
893        }
894    }
895
896    #[test]
897    fn test_so3_inverse() {
898        // inverse of identity is identity
899        let so3 = SO3::identity();
900        let so3_inv = so3.inverse(None);
901        assert_eq!(0.0, so3_inv.x());
902        assert_eq!(0.0, so3_inv.y());
903        assert_eq!(0.0, so3_inv.z());
904        assert_eq!(1.0, so3_inv.w());
905
906        // inverse of random in quaternion form is conjugate
907        let so3 = SO3::random();
908        let so3_inv = so3.inverse(None);
909        assert!((so3.x() + so3_inv.x()).abs() < TOLERANCE);
910        assert!((so3.y() + so3_inv.y()).abs() < TOLERANCE);
911        assert!((so3.z() + so3_inv.z()).abs() < TOLERANCE);
912        assert!((so3.w() - so3_inv.w()).abs() < TOLERANCE);
913    }
914
915    #[test]
916    fn test_so3_inverse_jacobian() {
917        let so3 = SO3::identity();
918        let mut jacobian = Matrix3::zeros();
919        let so3_inv = so3.inverse(Some(&mut jacobian));
920
921        // Check result
922        assert_eq!(0.0, so3_inv.x());
923        assert_eq!(0.0, so3_inv.y());
924        assert_eq!(0.0, so3_inv.z());
925        assert_eq!(1.0, so3_inv.w());
926
927        // Check Jacobian is negative identity
928        let expected_jac = -Matrix3::identity();
929        assert!((jacobian - expected_jac).norm() < TOLERANCE);
930    }
931
932    #[test]
933    fn test_so3_rplus() {
934        // Adding zero to identity
935        let so3a = SO3::identity();
936        let so3b = SO3Tangent::new(Vector3::zeros());
937        let so3c = so3a.right_plus(&so3b, None, None);
938        assert_eq!(0.0, so3c.x());
939        assert_eq!(0.0, so3c.y());
940        assert_eq!(0.0, so3c.z());
941        assert_eq!(1.0, so3c.w());
942
943        // Adding zero to random
944        let so3a = SO3::random();
945        let so3c = so3a.right_plus(&so3b, None, None);
946        assert!((so3a.x() - so3c.x()).abs() < TOLERANCE);
947        assert!((so3a.y() - so3c.y()).abs() < TOLERANCE);
948        assert!((so3a.z() - so3c.z()).abs() < TOLERANCE);
949        assert!((so3a.w() - so3c.w()).abs() < TOLERANCE);
950    }
951
952    #[test]
953    fn test_so3_lplus() {
954        // Adding zero to identity
955        let so3a = SO3::identity();
956        let so3t = SO3Tangent::new(Vector3::zeros());
957        let so3c = so3a.left_plus(&so3t, None, None);
958        assert_eq!(0.0, so3c.x());
959        assert_eq!(0.0, so3c.y());
960        assert_eq!(0.0, so3c.z());
961        assert_eq!(1.0, so3c.w());
962
963        // Adding zero to random
964        let so3a = SO3::random();
965        let so3c = so3a.left_plus(&so3t, None, None);
966        assert!((so3a.x() - so3c.x()).abs() < TOLERANCE);
967        assert!((so3a.y() - so3c.y()).abs() < TOLERANCE);
968        assert!((so3a.z() - so3c.z()).abs() < TOLERANCE);
969        assert!((so3a.w() - so3c.w()).abs() < TOLERANCE);
970    }
971
972    #[test]
973    fn test_so3_rminus() {
974        // identity minus identity is zero
975        let so3a = SO3::identity();
976        let so3b = SO3::identity();
977        let so3c = so3a.right_minus(&so3b, None, None);
978        assert!(so3c.x().abs() < TOLERANCE);
979        assert!(so3c.y().abs() < TOLERANCE);
980        assert!(so3c.z().abs() < TOLERANCE);
981
982        // random minus the same is zero
983        let so3a = SO3::random();
984        let so3b = so3a.clone();
985        let so3c = so3a.right_minus(&so3b, None, None);
986        assert!(so3c.data.norm() < TOLERANCE);
987    }
988
989    #[test]
990    fn test_so3_minus() {
991        // minus is the same as right_minus
992        let so3a = SO3::random();
993        let so3b = SO3::random();
994        let so3c = so3a.minus(&so3b, None, None);
995        let so3d = so3a.right_minus(&so3b, None, None);
996        assert!((so3c.data - so3d.data).norm() < TOLERANCE);
997    }
998
999    #[test]
1000    fn test_so3_exp_log() {
1001        // exp of zero is identity
1002        let so3t = SO3Tangent::new(Vector3::zeros());
1003        let so3 = so3t.exp(None);
1004        assert_eq!(0.0, so3.x());
1005        assert_eq!(0.0, so3.y());
1006        assert_eq!(0.0, so3.z());
1007        assert_eq!(1.0, so3.w());
1008
1009        // exp of negative is inverse of exp
1010        let so3t = SO3Tangent::new(Vector3::new(0.1, 0.2, 0.3));
1011        let so3 = so3t.exp(None);
1012        let so3n = SO3Tangent::new(Vector3::new(-0.1, -0.2, -0.3));
1013        let so3_inv = so3n.exp(None);
1014        assert!((so3_inv.x() + so3.x()).abs() < TOLERANCE);
1015        assert!((so3_inv.y() + so3.y()).abs() < TOLERANCE);
1016        assert!((so3_inv.z() + so3.z()).abs() < TOLERANCE);
1017        assert!((so3_inv.w() - so3.w()).abs() < TOLERANCE);
1018    }
1019
1020    #[test]
1021    fn test_so3_log() {
1022        // log of identity is zero
1023        let so3 = SO3::identity();
1024        let so3_log = so3.log(None);
1025        assert!(so3_log.x().abs() < TOLERANCE);
1026        assert!(so3_log.y().abs() < TOLERANCE);
1027        assert!(so3_log.z().abs() < TOLERANCE);
1028
1029        // log of inverse is negative log
1030        let so3 = SO3::random();
1031        let so3_log = so3.log(None);
1032        let so3_inv_log = so3.inverse(None).log(None);
1033        assert!((so3_inv_log.x() + so3_log.x()).abs() < TOLERANCE);
1034        assert!((so3_inv_log.y() + so3_log.y()).abs() < TOLERANCE);
1035        assert!((so3_inv_log.z() + so3_log.z()).abs() < TOLERANCE);
1036    }
1037
1038    #[test]
1039    fn test_so3_tangent_hat() {
1040        let so3_tan = SO3Tangent::new(Vector3::new(1.0, 2.0, 3.0));
1041        let so3_lie = so3_tan.hat();
1042
1043        assert!((so3_lie[(0, 0)] - 0.0).abs() < TOLERANCE);
1044        assert!((so3_lie[(0, 1)] + 3.0).abs() < TOLERANCE);
1045        assert!((so3_lie[(0, 2)] - 2.0).abs() < TOLERANCE);
1046        assert!((so3_lie[(1, 0)] - 3.0).abs() < TOLERANCE);
1047        assert!((so3_lie[(1, 1)] - 0.0).abs() < TOLERANCE);
1048        assert!((so3_lie[(1, 2)] + 1.0).abs() < TOLERANCE);
1049        assert!((so3_lie[(2, 0)] + 2.0).abs() < TOLERANCE);
1050        assert!((so3_lie[(2, 1)] - 1.0).abs() < TOLERANCE);
1051        assert!((so3_lie[(2, 2)] - 0.0).abs() < TOLERANCE);
1052    }
1053
1054    #[test]
1055    fn test_so3_act() {
1056        let so3 = SO3::identity();
1057        let transformed_point = so3.act(&Vector3::new(1.0, 1.0, 1.0), None, None);
1058        assert!((transformed_point.x - 1.0).abs() < TOLERANCE);
1059        assert!((transformed_point.y - 1.0).abs() < TOLERANCE);
1060        assert!((transformed_point.z - 1.0).abs() < TOLERANCE);
1061
1062        let so3 = SO3::from_euler_angles(PI, PI / 2.0, PI / 4.0);
1063        let transformed_point = so3.act(&Vector3::new(1.0, 1.0, 1.0), None, None);
1064        assert!((transformed_point.x - 0.0).abs() < TOLERANCE);
1065        assert!((transformed_point.y + f64::consts::SQRT_2).abs() < 1e-10);
1066        assert!((transformed_point.z + 1.0).abs() < TOLERANCE);
1067    }
1068
1069    #[test]
1070    fn test_so3_tangent_angular_velocity() {
1071        let so3tan = SO3Tangent::new(Vector3::new(1.0, 2.0, 3.0));
1072        let ang_vel = so3tan.ang();
1073        assert!((ang_vel - Vector3::new(1.0, 2.0, 3.0)).norm() < TOLERANCE);
1074    }
1075
1076    #[test]
1077    fn test_so3_compose() {
1078        let so3_1 = SO3::random();
1079        let so3_2 = SO3::random();
1080        let composed = so3_1.compose(&so3_2, None, None);
1081        assert!(composed.is_valid(TOLERANCE));
1082
1083        // Test composition with identity
1084        let identity = SO3::identity();
1085        let composed_with_identity = so3_1.compose(&identity, None, None);
1086        assert!((composed_with_identity.distance(&so3_1)).abs() < TOLERANCE);
1087    }
1088
1089    #[test]
1090    fn test_so3_exp_log_consistency() {
1091        let tangent = SO3Tangent::new(Vector3::new(0.1, 0.2, 0.3));
1092        let so3 = tangent.exp(None);
1093        let recovered_tangent = so3.log(None);
1094        assert!((tangent.data - recovered_tangent.data).norm() < TOLERANCE);
1095    }
1096
1097    #[test]
1098    fn test_so3_right_left_jacobian_relationship() {
1099        // For zero tangent, left and right Jacobians should be equal (both identity)
1100        let tangent = SO3Tangent::new(Vector3::zeros());
1101        let ljac = tangent.left_jacobian();
1102        let rjac = tangent.right_jacobian();
1103        assert!((ljac - rjac).norm() < TOLERANCE);
1104        assert!((ljac - Matrix3::identity()).norm() < TOLERANCE);
1105
1106        // For non-zero tangent, test the general relationship
1107        let tangent = SO3Tangent::new(Vector3::new(0.1, 0.2, 0.3));
1108        let ljac = tangent.left_jacobian();
1109        let rjac = tangent.right_jacobian();
1110
1111        // The correct relationship for SO(3) should be that both are transposes
1112        // when the tangent is small enough
1113        assert!((ljac - rjac.transpose()).norm() < TOLERANCE);
1114        assert!((rjac - ljac.transpose()).norm() < TOLERANCE);
1115    }
1116
1117    #[test]
1118    fn test_so3_manifold_properties() {
1119        assert_eq!(SO3::DIM, 3);
1120        assert_eq!(SO3::DOF, 3);
1121        assert_eq!(SO3::REP_SIZE, 4);
1122    }
1123
1124    #[test]
1125    fn test_so3_normalize() {
1126        let mut so3 = SO3::from_quaternion_coeffs(0.5, 0.5, 0.5, 0.5);
1127        so3.normalize();
1128        assert!(so3.is_valid(TOLERANCE));
1129    }
1130
1131    #[test]
1132    fn test_so3_tangent_norms() {
1133        let tangent = SO3Tangent::new(Vector3::new(3.0, 4.0, 0.0));
1134        let norm = tangent.data.norm();
1135        assert!((norm - 5.0).abs() < TOLERANCE);
1136
1137        let squared_norm = tangent.data.norm_squared();
1138        assert!((squared_norm - 25.0).abs() < TOLERANCE);
1139    }
1140
1141    #[test]
1142    fn test_so3_tangent_zero() {
1143        let zero = SO3Tangent::zero();
1144        assert!(zero.data.norm() < TOLERANCE);
1145
1146        let tangent = SO3Tangent::new(Vector3::zeros());
1147        assert!(tangent.is_zero(TOLERANCE));
1148    }
1149
1150    #[test]
1151    fn test_so3_tangent_normalize() {
1152        let mut tangent = SO3Tangent::new(Vector3::new(3.0, 4.0, 0.0));
1153        tangent.normalize();
1154        assert!((tangent.data.norm() - 1.0).abs() < TOLERANCE);
1155    }
1156
1157    #[test]
1158    fn test_so3_adjoint() {
1159        let so3 = SO3::random();
1160        let adj = so3.adjoint();
1161        assert_eq!(adj.nrows(), 3);
1162        assert_eq!(adj.ncols(), 3);
1163
1164        // For SO(3), adjoint is the rotation matrix, so det should be 1
1165        let det = adj.determinant();
1166        assert!((det - 1.0).abs() < TOLERANCE);
1167    }
1168
1169    #[test]
1170    fn test_so3_small_angle_approximations() {
1171        let small_tangent = SO3Tangent::new(Vector3::new(1e-8, 2e-8, 3e-8));
1172        let so3 = small_tangent.exp(None);
1173        let recovered = so3.log(None);
1174        assert!((small_tangent.data - recovered.data).norm() < TOLERANCE);
1175    }
1176
1177    #[test]
1178    fn test_so3_specific_rotations() {
1179        // Test rotation around X axis
1180        let so3_x = SO3::from_axis_angle(&Vector3::x(), PI / 2.0);
1181        let point_y = Vector3::y();
1182        let rotated = so3_x.act(&point_y, None, None);
1183        assert!((rotated - Vector3::z()).norm() < TOLERANCE);
1184
1185        // Test rotation around Z axis
1186        let so3_z = SO3::from_axis_angle(&Vector3::z(), PI / 2.0);
1187        let point_x = Vector3::x();
1188        let rotated = so3_z.act(&point_x, None, None);
1189        assert!((rotated - Vector3::y()).norm() < TOLERANCE);
1190    }
1191
1192    #[test]
1193    fn test_so3_from_components() {
1194        let so3 = SO3::from_quaternion_coeffs(0.0, 0.0, 0.0, 1.0);
1195        assert_eq!(so3.x(), 0.0);
1196        assert_eq!(so3.y(), 0.0);
1197        assert_eq!(so3.z(), 0.0);
1198        assert_eq!(so3.w(), 1.0);
1199    }
1200
1201    #[test]
1202    fn test_so3_tangent_from_components() {
1203        let tangent = SO3Tangent::from_components(1.0, 2.0, 3.0);
1204        assert_eq!(tangent.x(), 1.0);
1205        assert_eq!(tangent.y(), 2.0);
1206        assert_eq!(tangent.z(), 3.0);
1207    }
1208
1209    #[test]
1210    fn test_so3_consistency_with_manif() {
1211        // Test that operations are consistent with manif library expectations
1212        let so3_1 = SO3::random();
1213        let so3_2 = SO3::random();
1214
1215        // Test associativity: (R1 * R2) * R3 = R1 * (R2 * R3)
1216        let so3_3 = SO3::random();
1217        let left_assoc = so3_1
1218            .compose(&so3_2, None, None)
1219            .compose(&so3_3, None, None);
1220        let right_assoc = so3_1.compose(&so3_2.compose(&so3_3, None, None), None, None);
1221
1222        assert!(left_assoc.distance(&right_assoc) < 1e-10);
1223    }
1224
1225    #[test]
1226    fn test_so3_tangent_accessors() {
1227        let tangent = SO3Tangent::new(Vector3::new(1.0, 2.0, 3.0));
1228        assert_eq!(tangent.x(), 1.0);
1229        assert_eq!(tangent.y(), 2.0);
1230        assert_eq!(tangent.z(), 3.0);
1231
1232        let coeffs = tangent.coeffs();
1233        assert_eq!(coeffs, Vector3::new(1.0, 2.0, 3.0));
1234    }
1235
1236    #[test]
1237    fn test_so3_between() {
1238        let so3_1 = SO3::random();
1239        let so3_2 = SO3::random();
1240        let between = so3_1.between(&so3_2, None, None);
1241
1242        // Check that so3_1 * between = so3_2
1243        let result = so3_1.compose(&between, None, None);
1244        assert!(result.distance(&so3_2) < TOLERANCE);
1245    }
1246
1247    #[test]
1248    fn test_so3_distance() {
1249        let so3_1 = SO3::random();
1250        let so3_2 = SO3::random();
1251        let distance = so3_1.distance(&so3_2);
1252        assert!(distance >= 0.0);
1253        assert!(so3_1.distance(&so3_1) < TOLERANCE);
1254    }
1255
1256    // New tests for the additional functions
1257
1258    #[test]
1259    fn test_so3_vee() {
1260        let so3 = SO3::random();
1261        let tangent_log = so3.log(None);
1262        let tangent_vee = so3.vee();
1263
1264        assert!((tangent_log.data - tangent_vee.data).norm() < 1e-10);
1265    }
1266
1267    #[test]
1268    fn test_so3_is_approx() {
1269        let so3_1 = SO3::random();
1270        let so3_2 = so3_1.clone();
1271
1272        assert!(so3_1.is_approx(&so3_1, 1e-10));
1273        assert!(so3_1.is_approx(&so3_2, 1e-10));
1274
1275        // Test with small perturbation
1276        let small_tangent = SO3Tangent::new(Vector3::new(1e-12, 1e-12, 1e-12));
1277        let so3_perturbed = so3_1.right_plus(&small_tangent, None, None);
1278        assert!(so3_1.is_approx(&so3_perturbed, 1e-10));
1279    }
1280
1281    #[test]
1282    fn test_so3_tangent_small_adj() {
1283        let axis_angle = Vector3::new(0.1, 0.2, 0.3);
1284        let tangent = SO3Tangent::new(axis_angle);
1285        let small_adj = tangent.small_adj();
1286        let hat_matrix = tangent.hat();
1287
1288        // For SO(3), small adjoint equals hat matrix
1289        assert!((small_adj - hat_matrix).norm() < 1e-10);
1290    }
1291
1292    #[test]
1293    fn test_so3_tangent_lie_bracket() {
1294        let tangent_a = SO3Tangent::new(Vector3::new(0.1, 0.0, 0.0));
1295        let tangent_b = SO3Tangent::new(Vector3::new(0.0, 0.2, 0.0));
1296
1297        let bracket_ab = tangent_a.lie_bracket(&tangent_b);
1298        let bracket_ba = tangent_b.lie_bracket(&tangent_a);
1299
1300        // Anti-symmetry test: [a,b] = -[b,a]
1301        assert!((bracket_ab.data + bracket_ba.data).norm() < 1e-10);
1302
1303        // [a,a] = 0
1304        let bracket_aa = tangent_a.lie_bracket(&tangent_a);
1305        assert!(bracket_aa.is_zero(1e-10));
1306
1307        // Verify bracket relationship with hat operator
1308        let bracket_hat = bracket_ab.hat();
1309        let expected = tangent_a.hat() * tangent_b.hat() - tangent_b.hat() * tangent_a.hat();
1310        assert!((bracket_hat - expected).norm() < 1e-10);
1311    }
1312
1313    #[test]
1314    fn test_so3_tangent_is_approx() {
1315        let tangent_1 = SO3Tangent::new(Vector3::new(0.1, 0.2, 0.3));
1316        let tangent_2 = SO3Tangent::new(Vector3::new(0.1 + 1e-12, 0.2, 0.3));
1317        let tangent_3 = SO3Tangent::new(Vector3::new(0.5, 0.6, 0.7));
1318
1319        assert!(tangent_1.is_approx(&tangent_1, 1e-10));
1320        assert!(tangent_1.is_approx(&tangent_2, 1e-10));
1321        assert!(!tangent_1.is_approx(&tangent_3, 1e-10));
1322    }
1323
1324    #[test]
1325    fn test_so3_generators() {
1326        let tangent = SO3Tangent::new(Vector3::new(1.0, 1.0, 1.0));
1327
1328        // Test all three generators
1329        for i in 0..3 {
1330            let generator = tangent.generator(i);
1331
1332            // Generator should be skew-symmetric
1333            assert!((generator + generator.transpose()).norm() < 1e-10);
1334
1335            // Generator should have trace zero
1336            assert!(generator.trace().abs() < 1e-10);
1337        }
1338
1339        // Test specific values for the generators
1340        let e1 = tangent.generator(0);
1341        let e2 = tangent.generator(1);
1342        let e3 = tangent.generator(2);
1343
1344        // Expected generators based on C++ manif implementation
1345        let expected_e1 = Matrix3::new(0.0, 0.0, 0.0, 0.0, 0.0, -1.0, 0.0, 1.0, 0.0);
1346        let expected_e2 = Matrix3::new(0.0, 0.0, 1.0, 0.0, 0.0, 0.0, -1.0, 0.0, 0.0);
1347        let expected_e3 = Matrix3::new(0.0, -1.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0);
1348
1349        assert!((e1 - expected_e1).norm() < 1e-10);
1350        assert!((e2 - expected_e2).norm() < 1e-10);
1351        assert!((e3 - expected_e3).norm() < 1e-10);
1352    }
1353
1354    #[test]
1355    #[should_panic]
1356    fn test_so3_generator_invalid_index() {
1357        let tangent = SO3Tangent::new(Vector3::new(1.0, 1.0, 1.0));
1358        let _generator = tangent.generator(3); // Should panic for SO(3)
1359    }
1360
1361    #[test]
1362    fn test_so3_jacobi_identity() {
1363        // Test Jacobi identity: [x,[y,z]]+[y,[z,x]]+[z,[x,y]]=0
1364        let x = SO3Tangent::new(Vector3::new(0.1, 0.0, 0.0));
1365        let y = SO3Tangent::new(Vector3::new(0.0, 0.2, 0.0));
1366        let z = SO3Tangent::new(Vector3::new(0.0, 0.0, 0.3));
1367
1368        let term1 = x.lie_bracket(&y.lie_bracket(&z));
1369        let term2 = y.lie_bracket(&z.lie_bracket(&x));
1370        let term3 = z.lie_bracket(&x.lie_bracket(&y));
1371
1372        let jacobi_sum = SO3Tangent::new(term1.data + term2.data + term3.data);
1373        assert!(jacobi_sum.is_zero(1e-10));
1374    }
1375
1376    // T1: Numerical Jacobian Verification Tests
1377
1378    #[test]
1379    fn test_so3_right_jacobian_numerical() {
1380        // The right Jacobian Jr relates tangent space perturbations to group elements:
1381        // exp(ξ + δξ) ≈ exp(ξ) ∘ exp(Jr(ξ)·δξ)
1382        // Equivalently: Jr(ξ)·δξ ≈ log(exp(ξ)^{-1} ∘ exp(ξ + δξ))
1383
1384        let xi = Vector3::new(0.1, 0.2, 0.3);
1385        let tangent = SO3Tangent::new(xi);
1386        let jr_analytical = tangent.right_jacobian();
1387
1388        // Test with small perturbations in each direction
1389        let delta_size = 1e-6;
1390        let test_deltas = vec![
1391            Vector3::new(delta_size, 0.0, 0.0),
1392            Vector3::new(0.0, delta_size, 0.0),
1393            Vector3::new(0.0, 0.0, delta_size),
1394        ];
1395
1396        for delta in test_deltas {
1397            // Analytical: Jr * δξ
1398            let analytical_result = jr_analytical * delta;
1399
1400            // Numerical: log(exp(ξ)^{-1} * exp(ξ + δξ))
1401            let base = SO3Tangent::new(xi).exp(None);
1402            let perturbed = SO3Tangent::new(xi + delta).exp(None);
1403            let base_inv = base.inverse(None);
1404            let diff_element = base_inv.compose(&perturbed, None, None);
1405            let diff_log = diff_element.log(None);
1406            let numerical_result = Vector3::new(diff_log.x(), diff_log.y(), diff_log.z());
1407
1408            let error = (analytical_result - numerical_result).norm();
1409            assert!(
1410                error < 1e-7,
1411                "Right Jacobian verification failed: error = {}, delta = {:?}",
1412                error,
1413                delta
1414            );
1415        }
1416    }
1417
1418    #[test]
1419    fn test_so3_left_jacobian_numerical() {
1420        // The left Jacobian Jl relates tangent space perturbations to group elements:
1421        // exp(ξ + δξ) ≈ exp(Jl(ξ)·δξ) ∘ exp(ξ)
1422        // Equivalently: Jl(ξ)·δξ ≈ log(exp(ξ + δξ) ∘ exp(ξ)^{-1})
1423
1424        let xi = Vector3::new(0.1, 0.2, 0.3);
1425        let tangent = SO3Tangent::new(xi);
1426        let jl_analytical = tangent.left_jacobian();
1427
1428        // Test with small perturbations in each direction
1429        let delta_size = 1e-6;
1430        let test_deltas = vec![
1431            Vector3::new(delta_size, 0.0, 0.0),
1432            Vector3::new(0.0, delta_size, 0.0),
1433            Vector3::new(0.0, 0.0, delta_size),
1434        ];
1435
1436        for delta in test_deltas {
1437            // Analytical: Jl * δξ
1438            let analytical_result = jl_analytical * delta;
1439
1440            // Numerical: log(exp(ξ + δξ) * exp(ξ)^{-1})
1441            let base = SO3Tangent::new(xi).exp(None);
1442            let perturbed = SO3Tangent::new(xi + delta).exp(None);
1443            let base_inv = base.inverse(None);
1444            let diff_element = perturbed.compose(&base_inv, None, None);
1445            let diff_log = diff_element.log(None);
1446            let numerical_result = Vector3::new(diff_log.x(), diff_log.y(), diff_log.z());
1447
1448            let error = (analytical_result - numerical_result).norm();
1449            assert!(
1450                error < 1e-7,
1451                "Left Jacobian verification failed: error = {}, delta = {:?}",
1452                error,
1453                delta
1454            );
1455        }
1456    }
1457
1458    // T3: Accumulated Error Tests
1459    //
1460    // NOTE: These tests demonstrate EXPECTED numerical drift in long composition chains.
1461    // This is NOT a bug! Each quaternion multiplication introduces rounding errors that
1462    // accumulate multiplicatively through the chain.
1463    //
1464    // Real SLAM systems (ORB-SLAM3, GTSAM, Cartographer) handle this by:
1465    // - Periodic re-normalization of quaternions
1466    // - Using manifold optimization instead of pure composition
1467    // - Bundle adjustment to correct accumulated drift
1468    //
1469    // The tolerances here (1e-6 for 1000 steps) are REALISTIC and document the actual
1470    // numerical behavior of composition chains.
1471
1472    #[test]
1473    fn test_so3_accumulated_error_small_rotations() {
1474        // Compose 1000 small rotations, verify final result
1475        let small_rotation = SO3::from_scaled_axis(Vector3::new(0.001, 0.002, -0.001));
1476        let mut accumulated = SO3::identity();
1477
1478        for _ in 0..1000 {
1479            accumulated = accumulated.compose(&small_rotation, None, None);
1480        }
1481
1482        // Expected: 1000 * small rotation
1483        let expected_tangent = SO3Tangent::new(Vector3::new(1.0, 2.0, -1.0));
1484        let expected = expected_tangent.exp(None);
1485
1486        // Tolerance reflects accumulated rounding error over 1000 compositions (expected)
1487        assert!(accumulated.is_approx(&expected, 1e-6));
1488    }
1489
1490    #[test]
1491    fn test_so3_inverse_composition_chain() {
1492        // g * g^{-1} * g * g^{-1} ... should stay near identity
1493        let rotation = SO3::from_euler_angles(0.1, 0.2, 0.3);
1494        let inverse = rotation.inverse(None);
1495        let mut accumulated = SO3::identity();
1496
1497        for _ in 0..500 {
1498            accumulated = accumulated.compose(&rotation, None, None);
1499            accumulated = accumulated.compose(&inverse, None, None);
1500        }
1501
1502        // Should still be identity (within numerical error)
1503        assert!(accumulated.is_approx(&SO3::identity(), 1e-9));
1504    }
1505
1506    // T2: Edge Case Tests
1507
1508    #[test]
1509    fn test_so3_antipodal_quaternions() {
1510        // q and -q represent the same rotation
1511        let q = UnitQuaternion::from_euler_angles(0.5, 0.3, 0.2);
1512        let so3_pos = SO3::new(q);
1513        let so3_neg = SO3::new(UnitQuaternion::from_quaternion(-q.quaternion()));
1514
1515        // Should represent the same rotation (up to numerical tolerance)
1516        let log_pos = so3_pos.log(None);
1517        let log_neg = so3_neg.log(None);
1518
1519        // One of these should be true: either logs are equal, or they're π apart
1520        let diff_norm = (log_pos.data - log_neg.data).norm();
1521        let sum_norm = (log_pos.data + log_neg.data).norm();
1522        assert!(diff_norm < 1e-10 || sum_norm < 1e-10);
1523    }
1524
1525    #[test]
1526    fn test_so3_near_pi_rotation() {
1527        // Test rotation very close to π (edge case in log())
1528        let axis = Vector3::new(1.0, 0.0, 0.0).normalize();
1529        let angle = std::f64::consts::PI - 1e-8;
1530        let so3 = SO3::from_axis_angle(&axis, angle);
1531
1532        let tangent = so3.log(None);
1533        let recovered = tangent.exp(None);
1534
1535        assert!(so3.is_approx(&recovered, 1e-6));
1536    }
1537
1538    // T4: Jacobian Inverse Identity Tests
1539    //
1540    // NOTE: These tests verify Jr * Jr_inv ≈ I, but use LOOSE tolerances (0.01).
1541    // This is NOT a bug! Jacobian inverses have inherent numerical conditioning issues
1542    // documented in robotics literature (Nurlanov et al. 2021, Sophus library).
1543    //
1544    // The formula contains term (1 + cos θ) / (2θ sin θ) which:
1545    // - Becomes indeterminate (0/0) as θ → 0
1546    // - Has singularity as θ → π (sin θ → 0)
1547    // - Amplifies floating-point errors for θ > 0.01 rad
1548    //
1549    // Even with θ = 0.001 rad (very small!), we get errors ~0.01. This is EXPECTED
1550    // and consistent with production libraries (manif, Sophus, GTSAM).
1551
1552    #[test]
1553    fn test_so3_right_jacobian_inverse_identity() {
1554        // Test with very small angle (Jacobian inverse has poor numerical conditioning)
1555        let tangent = SO3Tangent::new(Vector3::new(0.001, 0.002, 0.003));
1556        let jr = tangent.right_jacobian();
1557        let jr_inv = tangent.right_jacobian_inv();
1558        let product = jr * jr_inv;
1559        let identity = Matrix3::identity();
1560
1561        // Tolerance of 0.01 reflects inherent mathematical conditioning, not implementation bug
1562        assert!(
1563            (product - identity).norm() < 0.01,
1564            "Jr * Jr_inv should be identity, got error: {}",
1565            (product - identity).norm()
1566        );
1567    }
1568
1569    #[test]
1570    fn test_so3_left_jacobian_inverse_identity() {
1571        // Test with very small angle (Jacobian inverse has poor numerical conditioning)
1572        let tangent = SO3Tangent::new(Vector3::new(0.001, 0.002, 0.003));
1573        let jl = tangent.left_jacobian();
1574        let jl_inv = tangent.left_jacobian_inv();
1575        let product = jl * jl_inv;
1576        let identity = Matrix3::identity();
1577
1578        // Tolerance of 0.01 reflects inherent mathematical conditioning, not implementation bug
1579        assert!((product - identity).norm() < 0.01);
1580    }
1581
1582    #[test]
1583    fn test_so3_from_quaternion_wxyz() {
1584        // Identity quaternion: w=1, x=y=z=0
1585        let so3_wxyz = SO3::from_quaternion_wxyz(1.0, 0.0, 0.0, 0.0);
1586        let so3_xyzw = SO3::from_quaternion_coeffs(0.0, 0.0, 0.0, 1.0);
1587        assert!(so3_wxyz.is_approx(&so3_xyzw, 1e-12));
1588
1589        // Non-trivial quaternion: verify swapped argument order gives same result
1590        let so3_wxyz = SO3::from_quaternion_wxyz(0.4, 0.1, 0.2, 0.3);
1591        let so3_xyzw = SO3::from_quaternion_coeffs(0.1, 0.2, 0.3, 0.4);
1592        assert!(so3_wxyz.is_approx(&so3_xyzw, 1e-12));
1593    }
1594
1595    #[test]
1596    fn test_so3_tangent_is_not_dynamic() {
1597        assert!(!SO3Tangent::is_dynamic());
1598        assert_eq!(SO3Tangent::DIM, 3);
1599    }
1600
1601    #[test]
1602    fn test_so3_small_angle_threshold_exp_log() {
1603        // Test angles near the SMALL_ANGLE_THRESHOLD boundary round-trip correctly
1604        // sqrt(1e-10) ≈ 1e-5 is the effective angle threshold
1605        let near_threshold_angles = [1e-6, 1e-5, 1e-4, 1e-3];
1606
1607        for &angle in &near_threshold_angles {
1608            let tangent = SO3Tangent::new(Vector3::new(angle, 0.0, 0.0));
1609            let so3 = tangent.exp(None);
1610            let recovered = so3.log(None);
1611            assert!(
1612                (tangent.data - recovered.data).norm() < 1e-10,
1613                "SO3 exp-log round-trip failed for angle = {angle}"
1614            );
1615        }
1616    }
1617}