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