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