Skip to main content

apex_manifolds/
se3.rs

1//! SE(3) - Special Euclidean Group in 3D
2//!
3//! This module implements the Special Euclidean group SE(3), which represents
4//! rigid body transformations in 3D space (rotation + translation).
5//!
6//! SE(3) elements are represented as a combination of SO(3) rotation and Vector3 translation.
7//! SE(3) tangent elements are represented as [rho(3), theta(3)] = 6 components,
8//! where rho is the translational component and theta is the rotational component.
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//! SE(3) inherits SO(3) conditioning issues and adds scale-dependent precision challenges.
16//!
17//! **Jacobian inverse precision** (Jr * Jr⁻¹ ≈ I):
18//!
19//! | Rotation       | Precision |
20//! |----------------|-----------|
21//! | θ < 0.01 rad   | ~1e-6     |
22//! | 0.01–0.1 rad   | ~1e-4     |
23//! | θ > 0.1 rad    | ~0.01     |
24//!
25//! **Scale effects**: Absolute error in exp-log round-trips grows with translation magnitude
26//! (~1e-8 at 1m, ~1e-3 at 1000m). The Q-block Jacobian couples rotation and translation,
27//! amplifying errors at large rotations. Composition chains drift multiplicatively —
28//! re-project periodically for long chains.
29
30use crate::{
31    LieGroup, Tangent,
32    so3::{SO3, SO3Tangent},
33};
34use nalgebra::{
35    DVector, Isometry3, Matrix3, Matrix4, Matrix6, Quaternion, Translation3, UnitQuaternion,
36    Vector3, Vector6,
37};
38use std::{
39    fmt,
40    fmt::{Display, Formatter},
41};
42
43/// SE(3) group element representing rigid body transformations in 3D.
44///
45/// Represented as a combination of SO(3) rotation and Vector3 translation.
46#[derive(Clone, PartialEq)]
47pub struct SE3 {
48    /// Rotation part as SO(3) element
49    rotation: SO3,
50    /// Translation part as Vector3
51    translation: Vector3<f64>,
52}
53
54impl Display for SE3 {
55    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
56        let t = self.translation();
57        let q = self.rotation_quaternion();
58        write!(
59            f,
60            "SE3(translation: [{:.4}, {:.4}, {:.4}], rotation: [w: {:.4}, x: {:.4}, y: {:.4}, z: {:.4}])",
61            t.x, t.y, t.z, q.w, q.i, q.j, q.k
62        )
63    }
64}
65
66impl SE3 {
67    /// Space dimension - dimension of the ambient space that the group acts on
68    pub const DIM: usize = 3;
69
70    /// Degrees of freedom - dimension of the tangent space
71    pub const DOF: usize = 6;
72
73    /// Representation size - size of the underlying data representation
74    pub const REP_SIZE: usize = 7;
75
76    /// Get the identity element of the group.
77    ///
78    /// Returns the neutral element e such that e ∘ g = g ∘ e = g for any group element g.
79    pub fn identity() -> Self {
80        SE3 {
81            rotation: SO3::identity(),
82            translation: Vector3::zeros(),
83        }
84    }
85
86    /// Get the identity matrix for Jacobians.
87    ///
88    /// Returns the identity matrix in the appropriate dimension for Jacobian computations.
89    pub fn jacobian_identity() -> Matrix6<f64> {
90        Matrix6::<f64>::identity()
91    }
92
93    /// Create a new SE3 element from translation and rotation.
94    ///
95    /// # Arguments
96    /// * `translation` - Translation vector [x, y, z]
97    /// * `rotation` - Unit quaternion representing rotation
98    #[inline]
99    pub fn new(translation: Vector3<f64>, rotation: UnitQuaternion<f64>) -> Self {
100        SE3 {
101            rotation: SO3::new(rotation),
102            translation,
103        }
104    }
105
106    /// Create SE3 from translation components and Euler angles.
107    pub fn from_translation_quaternion(
108        translation: Vector3<f64>,
109        quaternion: Quaternion<f64>,
110    ) -> Self {
111        let quaternion = UnitQuaternion::from_quaternion(quaternion.normalize());
112        Self::new(translation, quaternion)
113    }
114
115    /// Create SE3 from translation components and Euler angles.
116    pub fn from_translation_euler(x: f64, y: f64, z: f64, roll: f64, pitch: f64, yaw: f64) -> Self {
117        let translation = Vector3::new(x, y, z);
118        let rotation = UnitQuaternion::from_euler_angles(roll, pitch, yaw);
119        Self::new(translation, rotation)
120    }
121
122    /// Create SE3 directly from an Isometry3.
123    pub fn from_isometry(isometry: Isometry3<f64>) -> Self {
124        SE3 {
125            rotation: SO3::new(isometry.rotation),
126            translation: isometry.translation.vector,
127        }
128    }
129
130    /// Create SE3 from SO3 and Vector3 components.
131    pub fn from_translation_so3(translation: Vector3<f64>, rotation: SO3) -> Self {
132        SE3 {
133            rotation,
134            translation,
135        }
136    }
137
138    /// Get the translation part as a Vector3.
139    pub fn translation(&self) -> Vector3<f64> {
140        self.translation
141    }
142
143    /// Get the rotation part as SO3.
144    pub fn rotation_so3(&self) -> SO3 {
145        self.rotation.clone()
146    }
147
148    /// Get the rotation part as a UnitQuaternion.
149    pub fn rotation_quaternion(&self) -> UnitQuaternion<f64> {
150        self.rotation.quaternion()
151    }
152
153    /// Get as an Isometry3 (convenience method).
154    pub fn isometry(&self) -> Isometry3<f64> {
155        Isometry3::from_parts(
156            Translation3::from(self.translation),
157            self.rotation_quaternion(),
158        )
159    }
160
161    /// Get the transformation matrix (4x4 homogeneous matrix).
162    pub fn matrix(&self) -> Matrix4<f64> {
163        self.isometry().to_homogeneous()
164    }
165
166    /// Get the x component of translation.
167    #[inline]
168    pub fn x(&self) -> f64 {
169        self.translation.x
170    }
171
172    /// Get the y component of translation.
173    #[inline]
174    pub fn y(&self) -> f64 {
175        self.translation.y
176    }
177
178    /// Get the z component of translation.
179    #[inline]
180    pub fn z(&self) -> f64 {
181        self.translation.z
182    }
183
184    /// Get coefficients as array [tx, ty, tz, qw, qx, qy, qz].
185    pub fn coeffs(&self) -> [f64; 7] {
186        let q = self.rotation.quaternion();
187        [
188            self.translation.x,
189            self.translation.y,
190            self.translation.z,
191            q.w,
192            q.i,
193            q.j,
194            q.k,
195        ]
196    }
197}
198
199// Conversion traits for integration with generic Problem
200impl From<DVector<f64>> for SE3 {
201    fn from(data: DVector<f64>) -> Self {
202        let translation = Vector3::new(data[0], data[1], data[2]);
203        let quaternion = Quaternion::new(data[3], data[4], data[5], data[6]);
204        SE3::from_translation_quaternion(translation, quaternion)
205    }
206}
207
208impl From<SE3> for DVector<f64> {
209    fn from(se3: SE3) -> Self {
210        let q = se3.rotation.quaternion();
211        DVector::from_vec(vec![
212            se3.translation.x,
213            se3.translation.y,
214            se3.translation.z,
215            q.w,
216            q.i,
217            q.j,
218            q.k,
219        ])
220    }
221}
222
223// Implement basic trait requirements for LieGroup
224impl LieGroup for SE3 {
225    type TangentVector = SE3Tangent;
226    type JacobianMatrix = Matrix6<f64>;
227    type LieAlgebra = Matrix4<f64>;
228
229    /// Get the inverse.
230    ///
231    /// # Arguments
232    /// * `jacobian` - Optional Jacobian matrix of the inverse wrt this.
233    ///
234    /// # Notes
235    /// # Equation 170: Inverse of SE(3) matrix
236    /// M⁻¹ = [ Rᵀ -Rᵀt ]
237    ///       [ 0    1   ]
238    ///
239    /// # Equation 176: Jacobian of inverse operation
240    /// J_M⁻¹_M = - [ R [t]ₓ R ]
241    ///             [ 0    R   ]
242    fn inverse(&self, jacobian: Option<&mut Self::JacobianMatrix>) -> Self {
243        let rot_inv = self.rotation.inverse(None);
244        let trans_inv = -rot_inv.act(&self.translation, None, None);
245
246        if let Some(jac) = jacobian {
247            *jac = -self.adjoint();
248        }
249
250        SE3::from_translation_so3(trans_inv, rot_inv)
251    }
252
253    /// Composition of this and another SE3 element.
254    ///
255    /// # Arguments
256    /// * `other` - Another SE3 element.
257    /// * `jacobian_self` - Optional Jacobian matrix of the composition wrt this.
258    /// * `jacobian_other` - Optional Jacobian matrix of the composition wrt other.
259    ///
260    /// # Notes
261    /// # Equation 171: Composition of SE(3) matrices
262    /// M_a M_b = [ R_a*R_b   R_a*t_b + t_a ]
263    ///           [ 0             1         ]
264    ///
265    /// # Equation 177: Jacobian of the composition wrt self.
266    /// J_MaMb_Ma = [ R_bᵀ   -R_bᵀ*[t_b]ₓ ]
267    ///             [ 0          R_bᵀ     ]
268    ///
269    /// # Equation 178: Jacobian of the composition wrt other.
270    /// J_MaMb_Mb = I_6
271    ///
272    fn compose(
273        &self,
274        other: &Self,
275        jacobian_self: Option<&mut Self::JacobianMatrix>,
276        jacobian_other: Option<&mut Self::JacobianMatrix>,
277    ) -> Self {
278        let composed_rotation = self.rotation.compose(&other.rotation, None, None);
279        let composed_translation =
280            self.rotation.act(&other.translation, None, None) + self.translation;
281
282        let result = SE3::from_translation_so3(composed_translation, composed_rotation);
283
284        if let Some(jac_self) = jacobian_self {
285            *jac_self = other.inverse(None).adjoint();
286        }
287
288        if let Some(jac_other) = jacobian_other {
289            *jac_other = Matrix6::identity();
290        }
291
292        result
293    }
294
295    /// Get the SE3 corresponding Lie algebra element in vector form.
296    ///
297    /// # Arguments
298    /// * `jacobian` - Optional Jacobian matrix of the tangent wrt to this.
299    ///
300    /// # Notes
301    /// # Equation 173: SE(3) logarithmic map
302    /// τ = log(M) = [ V⁻¹(θ) t ]
303    ///              [ Log(R)  ]
304    ///
305    /// # Equation 174: V(θ) function for SE(3) Log/Exp maps
306    /// V(θ) = I + (1 - cos θ)/θ² [θ]ₓ + (θ - sin θ)/θ³ [θ]ₓ²
307    ///
308    fn log(&self, jacobian: Option<&mut Self::JacobianMatrix>) -> Self::TangentVector {
309        let theta = self.rotation.log(None);
310        let mut data = Vector6::zeros();
311        let translation_vector = theta.left_jacobian_inv() * self.translation;
312        data.fixed_rows_mut::<3>(0).copy_from(&translation_vector);
313        data.fixed_rows_mut::<3>(3).copy_from(&theta.coeffs());
314        let result = SE3Tangent { data };
315        if let Some(jac) = jacobian {
316            *jac = result.right_jacobian_inv();
317        }
318
319        result
320    }
321
322    fn act(
323        &self,
324        vector: &Vector3<f64>,
325        jacobian_self: Option<&mut Self::JacobianMatrix>,
326        jacobian_vector: Option<&mut Matrix3<f64>>,
327    ) -> Vector3<f64> {
328        let result = self.rotation.act(vector, None, None) + self.translation;
329
330        if let Some(jac_self) = jacobian_self {
331            jac_self
332                .fixed_view_mut::<3, 3>(0, 0)
333                .copy_from(&self.rotation.rotation_matrix());
334            jac_self
335                .fixed_view_mut::<3, 3>(0, 3)
336                .copy_from(&(-self.rotation.rotation_matrix() * SO3Tangent::new(*vector).hat()));
337        }
338
339        if let Some(jac_vector) = jacobian_vector {
340            let rotation_matrix = self.rotation.rotation_matrix();
341            jac_vector.copy_from(&rotation_matrix);
342        }
343
344        result
345    }
346
347    fn adjoint(&self) -> Self::JacobianMatrix {
348        let rotation_matrix = self.rotation.rotation_matrix();
349        let translation = self.translation;
350        let mut adjoint_matrix = Matrix6::zeros();
351
352        // Top-left block: R
353        adjoint_matrix
354            .fixed_view_mut::<3, 3>(0, 0)
355            .copy_from(&rotation_matrix);
356
357        // Bottom-right block: R
358        adjoint_matrix
359            .fixed_view_mut::<3, 3>(3, 3)
360            .copy_from(&rotation_matrix);
361
362        // Top-right block: [t]_× R (skew-symmetric of translation times rotation)
363        let top_right = SO3Tangent::new(translation).hat() * rotation_matrix;
364        adjoint_matrix
365            .fixed_view_mut::<3, 3>(0, 3)
366            .copy_from(&top_right);
367
368        adjoint_matrix
369    }
370
371    fn random() -> Self {
372        use rand::Rng;
373        let mut rng = rand::rng();
374
375        // Random translation in [-1, 1]³
376        let translation = Vector3::new(
377            rng.random_range(-1.0..1.0),
378            rng.random_range(-1.0..1.0),
379            rng.random_range(-1.0..1.0),
380        );
381
382        // Random rotation
383        let rotation = SO3::random();
384
385        SE3::from_translation_so3(translation, rotation)
386    }
387
388    fn jacobian_identity() -> Self::JacobianMatrix {
389        Matrix6::<f64>::identity()
390    }
391
392    fn zero_jacobian() -> Self::JacobianMatrix {
393        Matrix6::<f64>::zeros()
394    }
395
396    fn normalize(&mut self) {
397        // Normalize the rotation part
398        self.rotation.normalize();
399    }
400
401    fn is_valid(&self, tolerance: f64) -> bool {
402        // Check if rotation is valid
403        self.rotation.is_valid(tolerance)
404    }
405
406    /// Vee operator: log(g)^∨.
407    ///
408    /// Maps a group element g ∈ G to its tangent vector log(g)^∨ ∈ 𝔤.
409    /// For SE(3), this is the same as log().
410    fn vee(&self) -> Self::TangentVector {
411        self.log(None)
412    }
413
414    /// Check if the element is approximately equal to another element.
415    ///
416    /// # Arguments
417    /// * `other` - The other element to compare with
418    /// * `tolerance` - The tolerance for the comparison
419    fn is_approx(&self, other: &Self, tolerance: f64) -> bool {
420        let difference = self.right_minus(other, None, None);
421        difference.is_zero(tolerance)
422    }
423}
424
425/// SE(3) tangent space element representing elements in the Lie algebra se(3).
426///
427/// Following manif conventions, internally represented as [rho(3), theta(3)] where:
428/// - rho: translational component [rho_x, rho_y, rho_z]
429/// - theta: rotational component [theta_x, theta_y, theta_z]
430#[derive(Clone, PartialEq)]
431pub struct SE3Tangent {
432    /// Internal data: [rho_x, rho_y, rho_z, theta_x, theta_y, theta_z]
433    data: Vector6<f64>,
434}
435
436impl fmt::Display for SE3Tangent {
437    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
438        let rho = self.rho();
439        let theta = self.theta();
440        write!(
441            f,
442            "se3(rho: [{:.4}, {:.4}, {:.4}], theta: [{:.4}, {:.4}, {:.4}])",
443            rho.x, rho.y, rho.z, theta.x, theta.y, theta.z
444        )
445    }
446}
447
448impl From<DVector<f64>> for SE3Tangent {
449    fn from(data_vector: DVector<f64>) -> Self {
450        SE3Tangent {
451            data: Vector6::new(
452                data_vector[0],
453                data_vector[1],
454                data_vector[2],
455                data_vector[3],
456                data_vector[4],
457                data_vector[5],
458            ),
459        }
460    }
461}
462
463impl From<SE3Tangent> for DVector<f64> {
464    fn from(se3_tangent: SE3Tangent) -> Self {
465        DVector::from_vec(vec![
466            se3_tangent.data[0],
467            se3_tangent.data[1],
468            se3_tangent.data[2],
469            se3_tangent.data[3],
470            se3_tangent.data[4],
471            se3_tangent.data[5],
472        ])
473    }
474}
475
476impl SE3Tangent {
477    /// Create a new SE3Tangent from rho (translational) and theta (rotational) components.
478    ///
479    /// # Arguments
480    /// * `rho` - Translational component [rho_x, rho_y, rho_z]
481    /// * `theta` - Rotational component [theta_x, theta_y, theta_z]
482    #[inline]
483    pub fn new(rho: Vector3<f64>, theta: Vector3<f64>) -> Self {
484        let mut data = Vector6::zeros();
485        data.fixed_rows_mut::<3>(0).copy_from(&rho);
486        data.fixed_rows_mut::<3>(3).copy_from(&theta);
487        SE3Tangent { data }
488    }
489
490    /// Create SE3Tangent from individual components.
491    pub fn from_components(
492        rho_x: f64,
493        rho_y: f64,
494        rho_z: f64,
495        theta_x: f64,
496        theta_y: f64,
497        theta_z: f64,
498    ) -> Self {
499        SE3Tangent {
500            data: Vector6::new(rho_x, rho_y, rho_z, theta_x, theta_y, theta_z),
501        }
502    }
503
504    /// Get the rho (translational) part.
505    #[inline]
506    pub fn rho(&self) -> Vector3<f64> {
507        self.data.fixed_rows::<3>(0).into_owned()
508    }
509
510    /// Get the theta (rotational) part.
511    #[inline]
512    pub fn theta(&self) -> Vector3<f64> {
513        self.data.fixed_rows::<3>(3).into_owned()
514    }
515
516    /// Equation 180: Q(ρ, θ) function for SE(3) Jacobians
517    /// Q(ρ, θ) = (1/2)ρₓ + (θ - sin θ)/θ³ (θₓρₓ + ρₓθₓ + θₓρₓθₓ)
518    ///           - (1 - θ²/2 - cos θ)/θ⁴ (θ²ₓρₓ + ρₓθ²ₓ - 3θₓρₓθₓ)
519    ///           - (1/2) * ( (1 - θ²/2 - cos θ)/θ⁴ - (3.0 * (θ - sin θ - θ³/6))/θ⁵ ) * (θₓρₓθ²ₓ + θ²ₓρₓθₓ)
520    pub fn q_block_jacobian_matrix(rho: Vector3<f64>, theta: Vector3<f64>) -> Matrix3<f64> {
521        let rho_skew = SO3Tangent::new(rho).hat();
522        let theta_skew = SO3Tangent::new(theta).hat();
523        let theta_squared = theta.norm_squared();
524
525        let a = 0.5;
526        let mut b = 1.0 / 6.0 + 1.0 / 120.0 * theta_squared;
527        let mut c = -1.0 / 24.0 + 1.0 / 720.0 * theta_squared;
528        let mut d = -1.0 / 60.0;
529
530        if theta_squared > crate::SMALL_ANGLE_THRESHOLD {
531            let theta_norm = theta_squared.sqrt();
532            let theta_norm_3 = theta_norm * theta_squared;
533            let theta_norm_4 = theta_squared * theta_squared;
534            let theta_norm_5 = theta_norm_3 * theta_squared;
535            let sin_theta = theta_norm.sin();
536            let cos_theta = theta_norm.cos();
537
538            b = (theta_norm - sin_theta) / theta_norm_3;
539            c = (1.0 - theta_squared / 2.0 - cos_theta) / theta_norm_4;
540            d = (c - 3.0) * (theta_norm - sin_theta - theta_norm_3 / 6.0) / theta_norm_5;
541        }
542
543        let tr = theta_skew * rho_skew;
544        let rt = rho_skew * theta_skew;
545        let trt = tr * theta_skew;
546        let rt_t2 = rt * theta_skew;
547
548        rho_skew * a + (tr + rt + trt) * b
549            - (rt_t2 - rt_t2.transpose() - trt * 3.0) * c
550            - (trt * theta_skew) * d
551    }
552}
553
554// Implement LieAlgebra trait for SE3Tangent
555impl Tangent<SE3> for SE3Tangent {
556    /// Dimension of the tangent space
557    const DIM: usize = 6;
558
559    /// Get the SE3 element.
560    ///
561    /// # Arguments
562    /// * `tangent` - Tangent vector [rho, theta]
563    /// * `jacobian` - Optional Jacobian matrix of the SE3 element wrt this.
564    ///
565    /// # Notes
566    /// # Equation 172: SE(3) exponential map
567    /// M = exp(τ) = [ R(θ)   t(ρ) ]
568    ///              [ 0       1   ]
569    fn exp(&self, jacobian: Option<&mut <SE3 as LieGroup>::JacobianMatrix>) -> SE3 {
570        let rho = self.rho();
571        let theta = self.theta();
572
573        let theta_tangent = SO3Tangent::new(theta);
574        // Compute rotation part using SO(3) exponential
575        let rotation = theta_tangent.exp(None);
576        let translation = theta_tangent.left_jacobian() * rho;
577
578        if let Some(jac) = jacobian {
579            *jac = self.right_jacobian();
580        }
581
582        SE3::from_translation_so3(translation, rotation)
583    }
584
585    /// Right Jacobian Jr.
586    ///
587    /// Computes the right Jacobian matrix such that for small δφ:
588    /// exp((φ + δφ)^∧) ≈ exp(φ^∧) ∘ exp((Jr δφ)^∧)
589    ///
590    /// For SE(3), this involves computing Jacobians for both translation and rotation parts.
591    ///
592    /// # Returns
593    /// The right Jacobian matrix (6x6)
594    fn right_jacobian(&self) -> <SE3 as LieGroup>::JacobianMatrix {
595        let mut jac = Matrix6::zeros();
596        let rho = self.rho();
597        let theta = self.theta();
598        let theta_right_jac = SO3Tangent::new(-theta).right_jacobian();
599        jac.fixed_view_mut::<3, 3>(0, 0).copy_from(&theta_right_jac);
600        jac.fixed_view_mut::<3, 3>(3, 3).copy_from(&theta_right_jac);
601        jac.fixed_view_mut::<3, 3>(0, 3)
602            .copy_from(&SE3Tangent::q_block_jacobian_matrix(-rho, -theta));
603        jac
604    }
605
606    /// Left Jacobian Jl.
607    ///
608    /// Computes the left Jacobian matrix such that for small δφ:
609    /// exp((φ + δφ)^∧) ≈ exp((Jl δφ)^∧) ∘ exp(φ^∧)
610    ///
611    /// Following manif conventions for SE(3) left Jacobian computation.
612    ///
613    /// # Returns
614    /// The left Jacobian matrix (6x6)
615    fn left_jacobian(&self) -> <SE3 as LieGroup>::JacobianMatrix {
616        let mut jac = Matrix6::zeros();
617        let theta_left_jac = SO3Tangent::new(self.theta()).left_jacobian();
618        jac.fixed_view_mut::<3, 3>(0, 0).copy_from(&theta_left_jac);
619        jac.fixed_view_mut::<3, 3>(3, 3).copy_from(&theta_left_jac);
620        jac.fixed_view_mut::<3, 3>(0, 3)
621            .copy_from(&SE3Tangent::q_block_jacobian_matrix(
622                self.rho(),
623                self.theta(),
624            ));
625        jac
626    }
627
628    /// Inverse of right Jacobian Jr⁻¹.
629    ///
630    /// Computes the inverse of the right Jacobian. This is used for
631    /// computing perturbations and derivatives.
632    ///
633    /// # Numerical Conditioning Warning
634    ///
635    /// **This 6×6 Jacobian inverse inherits SO(3) conditioning issues plus scale effects.**
636    ///
637    /// Sources of numerical error:
638    /// - Embedded SO(3) rotation: (1 + cos θ) / (2θ sin θ) singularity
639    /// - Q-block coupling: rotation errors propagate to translation
640    /// - Scale amplification: large translations increase absolute error
641    ///
642    /// **Expected Precision**:
643    /// - Small rotations (θ < 0.01): Jr * Jr⁻¹ ≈ I within ~1e-6
644    /// - Moderate rotations (θ < 0.1): Jr * Jr⁻¹ ≈ I within ~1e-4
645    /// - Larger rotations: Jr * Jr⁻¹ ≈ I within ~0.01
646    ///
647    /// This is a **fundamental mathematical limitation** consistent with production
648    /// SLAM libraries. See module documentation for references.
649    ///
650    /// # Returns
651    /// The inverse right Jacobian matrix (6x6)
652    fn right_jacobian_inv(&self) -> <SE3 as LieGroup>::JacobianMatrix {
653        let mut jac = Matrix6::zeros();
654        let rho = self.rho();
655        let theta = self.theta();
656        let theta_left_inv_jac = SO3Tangent::new(-theta).left_jacobian_inv();
657        let q_block_jac = SE3Tangent::q_block_jacobian_matrix(-rho, -theta);
658        jac.fixed_view_mut::<3, 3>(0, 0)
659            .copy_from(&theta_left_inv_jac);
660        jac.fixed_view_mut::<3, 3>(3, 3)
661            .copy_from(&theta_left_inv_jac);
662        let top_right = -1.0 * theta_left_inv_jac * q_block_jac * theta_left_inv_jac;
663        jac.fixed_view_mut::<3, 3>(0, 3).copy_from(&top_right);
664        jac
665    }
666
667    /// Inverse of left Jacobian Jl⁻¹.
668    ///
669    /// Computes the inverse of the left Jacobian following manif conventions.
670    ///
671    /// # Numerical Conditioning Warning
672    ///
673    /// **This 6×6 Jacobian inverse has the same conditioning issues as the right Jacobian inverse.**
674    ///
675    /// Sources of numerical error:
676    /// - Embedded SO(3) rotation: (1 + cos θ) / (2θ sin θ) singularity
677    /// - Q-block coupling: rotation errors propagate to translation
678    /// - Scale amplification: large translations increase absolute error
679    ///
680    /// **Expected Precision**: Same as right Jacobian inverse (see above).
681    ///
682    /// This is a **fundamental mathematical limitation** consistent with production
683    /// SLAM libraries. See module documentation for references.
684    ///
685    /// # Returns
686    /// The inverse left Jacobian matrix (6x6)
687    fn left_jacobian_inv(&self) -> <SE3 as LieGroup>::JacobianMatrix {
688        let mut jac = Matrix6::zeros();
689        let rho = self.rho();
690        let theta = self.theta();
691        let theta_left_inv_jac = SO3Tangent::new(theta).left_jacobian_inv();
692        let q_block_jac = SE3Tangent::q_block_jacobian_matrix(rho, theta);
693        let top_right_block = -1.0 * theta_left_inv_jac * q_block_jac * theta_left_inv_jac;
694        jac.fixed_view_mut::<3, 3>(0, 0)
695            .copy_from(&theta_left_inv_jac);
696        jac.fixed_view_mut::<3, 3>(3, 3)
697            .copy_from(&theta_left_inv_jac);
698        jac.fixed_view_mut::<3, 3>(0, 3).copy_from(&top_right_block);
699        jac
700    }
701
702    // Matrix representations
703
704    /// Hat operator: φ^∧ (vector to matrix).
705    ///
706    /// Converts the SE(3) tangent vector to its 4x4 matrix representation in the Lie algebra.
707    /// Following manif conventions, the structure is:
708    /// [  theta_×   rho  ]
709    /// [      0       0    ]
710    /// where theta_× is the skew-symmetric matrix of the rotational part.
711    ///
712    /// # Returns
713    /// The 4x4 matrix representation in the SE(3) Lie algebra
714    fn hat(&self) -> <SE3 as LieGroup>::LieAlgebra {
715        let mut lie_alg = Matrix4::zeros();
716
717        // Top-left 3x3: skew-symmetric matrix of rotational part
718        let theta_hat = SO3Tangent::new(self.theta()).hat();
719        lie_alg.view_mut((0, 0), (3, 3)).copy_from(&theta_hat);
720
721        // Top-right 3x1: translational part
722        let rho = self.rho();
723        lie_alg[(0, 3)] = rho[0];
724        lie_alg[(1, 3)] = rho[1];
725        lie_alg[(2, 3)] = rho[2];
726
727        lie_alg
728    }
729
730    // Utility functions
731
732    /// Zero tangent vector.
733    ///
734    /// Returns the zero element of the SE(3) tangent space.
735    ///
736    /// # Returns
737    /// A 6-dimensional zero vector
738    fn zero() -> <SE3 as LieGroup>::TangentVector {
739        SE3Tangent::new(Vector3::zeros(), Vector3::zeros())
740    }
741
742    /// Random tangent vector (useful for testing).
743    ///
744    /// Generates a random tangent vector with reasonable bounds.
745    /// Translation components are in [-1, 1] and rotation components in [-0.1, 0.1].
746    ///
747    /// # Returns
748    /// A random 6-dimensional tangent vector
749    fn random() -> <SE3 as LieGroup>::TangentVector {
750        use rand::Rng;
751        let mut rng = rand::rng();
752        SE3Tangent::from_components(
753            rng.random_range(-1.0..1.0), // rho_x
754            rng.random_range(-1.0..1.0), // rho_y
755            rng.random_range(-1.0..1.0), // rho_z
756            rng.random_range(-0.1..0.1), // theta_x
757            rng.random_range(-0.1..0.1), // theta_y
758            rng.random_range(-0.1..0.1), // theta_z
759        )
760    }
761
762    /// Check if the tangent vector is approximately zero.
763    ///
764    /// Compares the norm of the tangent vector to the given tolerance.
765    ///
766    /// # Arguments
767    /// * `tolerance` - Tolerance for zero comparison
768    ///
769    /// # Returns
770    /// True if the norm is below the tolerance
771    fn is_zero(&self, tolerance: f64) -> bool {
772        self.data.norm() < tolerance
773    }
774
775    /// Normalize the tangent vector to unit norm.
776    ///
777    /// Modifies this tangent vector to have unit norm. If the vector
778    /// is near zero, it remains unchanged.
779    fn normalize(&mut self) {
780        let theta_norm = self.theta().norm();
781        self.data[3] /= theta_norm;
782        self.data[4] /= theta_norm;
783        self.data[5] /= theta_norm;
784    }
785
786    /// Return a unit tangent vector in the same direction.
787    ///
788    /// Returns a new tangent vector with unit norm in the same direction.
789    /// If the original vector is near zero, returns zero.
790    ///
791    /// # Returns
792    /// A normalized copy of the tangent vector
793    fn normalized(&self) -> <SE3 as LieGroup>::TangentVector {
794        let norm = self.theta().norm();
795        if norm > f64::EPSILON {
796            SE3Tangent::new(self.rho(), self.theta() / norm)
797        } else {
798            SE3Tangent::new(self.rho(), Vector3::zeros())
799        }
800    }
801
802    /// Small adjoint matrix for SE(3).
803    ///
804    /// For SE(3), the small adjoint matrix has the structure:
805    /// [ Omega  V   ]
806    /// [   0  Omega ]
807    /// where Omega is the skew-symmetric matrix of the angular part
808    /// and V is the skew-symmetric matrix of the linear part.
809    fn small_adj(&self) -> <SE3 as LieGroup>::JacobianMatrix {
810        let mut small_adj = Matrix6::zeros();
811        let rho_skew = SO3Tangent::new(self.rho()).hat();
812        let theta_skew = SO3Tangent::new(self.theta()).hat();
813
814        // Top-left and bottom-right blocks: theta_skew (skew-symmetric of angular part)
815        small_adj
816            .fixed_view_mut::<3, 3>(0, 0)
817            .copy_from(&theta_skew);
818        small_adj
819            .fixed_view_mut::<3, 3>(3, 3)
820            .copy_from(&theta_skew);
821
822        // Top-right block: rho_skew (skew-symmetric of linear part)
823        small_adj.fixed_view_mut::<3, 3>(0, 3).copy_from(&rho_skew);
824
825        // Bottom-left block: zeros (already set)
826
827        small_adj
828    }
829
830    /// Lie bracket for SE(3).
831    ///
832    /// Computes the Lie bracket [this, other] = this.small_adj() * other.
833    fn lie_bracket(&self, other: &Self) -> <SE3 as LieGroup>::TangentVector {
834        let bracket_result = self.small_adj() * other.data;
835        SE3Tangent {
836            data: bracket_result,
837        }
838    }
839
840    /// Check if this tangent vector is approximately equal to another.
841    ///
842    /// # Arguments
843    /// * `other` - The other tangent vector to compare with
844    /// * `tolerance` - The tolerance for the comparison
845    fn is_approx(&self, other: &Self, tolerance: f64) -> bool {
846        (self.data - other.data).norm() < tolerance
847    }
848
849    /// Get the ith generator of the SE(3) Lie algebra.
850    ///
851    /// # Arguments
852    /// * `i` - Index of the generator (0-5 for SE(3))
853    ///
854    /// # Returns
855    /// The generator matrix
856    fn generator(&self, i: usize) -> <SE3 as LieGroup>::LieAlgebra {
857        assert!(i < 6, "SE(3) only has generators for indices 0-5");
858
859        let mut generator = Matrix4::zeros();
860
861        match i {
862            0 => {
863                // Generator for rho_x (translation in x)
864                generator[(0, 3)] = 1.0;
865            }
866            1 => {
867                // Generator for rho_y (translation in y)
868                generator[(1, 3)] = 1.0;
869            }
870            2 => {
871                // Generator for rho_z (translation in z)
872                generator[(2, 3)] = 1.0;
873            }
874            3 => {
875                // Generator for theta_x (rotation around x-axis)
876                generator[(1, 2)] = -1.0;
877                generator[(2, 1)] = 1.0;
878            }
879            4 => {
880                // Generator for theta_y (rotation around y-axis)
881                generator[(0, 2)] = 1.0;
882                generator[(2, 0)] = -1.0;
883            }
884            5 => {
885                // Generator for theta_z (rotation around z-axis)
886                generator[(0, 1)] = -1.0;
887                generator[(1, 0)] = 1.0;
888            }
889            _ => unreachable!(),
890        }
891
892        generator
893    }
894}
895
896#[cfg(test)]
897mod tests {
898    use super::*;
899    use Quaternion;
900    use std::f64::consts::PI;
901
902    const TOLERANCE: f64 = 1e-9;
903
904    #[test]
905    fn test_se3_tangent_basic() {
906        let linear = Vector3::new(1.0, 2.0, 3.0);
907        let angular = Vector3::new(0.1, 0.2, 0.3);
908        let tangent = SE3Tangent::new(linear, angular);
909
910        assert_eq!(tangent.rho(), linear);
911        assert_eq!(tangent.theta(), angular);
912    }
913
914    #[test]
915    fn test_se3_tangent_zero() {
916        let zero = SE3Tangent::zero();
917        assert_eq!(zero.data, Vector6::zeros());
918
919        let tangent = SE3Tangent::from_components(0.0, 0.0, 0.0, 0.0, 0.0, 0.0);
920        assert!(tangent.is_zero(1e-10));
921    }
922
923    // Comprehensive SE3 tests
924    #[test]
925    fn test_se3_identity() {
926        let identity = SE3::identity();
927        assert!(identity.is_valid(TOLERANCE));
928
929        let translation = identity.translation();
930        let rotation = identity.rotation_quaternion();
931
932        assert!(translation.norm() < TOLERANCE);
933        assert!((rotation.angle()) < TOLERANCE);
934    }
935
936    #[test]
937    fn test_se3_new() {
938        let translation = Vector3::new(1.0, 2.0, 3.0);
939        let rotation = UnitQuaternion::from_euler_angles(0.1, 0.2, 0.3);
940
941        let se3 = SE3::new(translation, rotation);
942
943        assert!(se3.is_valid(TOLERANCE));
944        assert!((se3.translation() - translation).norm() < TOLERANCE);
945        assert!((se3.rotation_quaternion().angle() - rotation.angle()).abs() < TOLERANCE);
946    }
947
948    #[test]
949    fn test_se3_random() {
950        let se3 = SE3::random();
951        assert!(se3.is_valid(TOLERANCE));
952    }
953
954    #[test]
955    fn test_se3_inverse() {
956        let se3 = SE3::random();
957        let se3_inv = se3.inverse(None);
958
959        // Test that g * g^-1 = identity
960        let composed = se3.compose(&se3_inv, None, None);
961        let identity = SE3::identity();
962
963        let translation_diff = (composed.translation() - identity.translation()).norm();
964        let rotation_diff = composed.rotation_quaternion().angle();
965
966        assert!(translation_diff < TOLERANCE);
967        assert!(rotation_diff < TOLERANCE);
968    }
969
970    #[test]
971    fn test_se3_compose() {
972        let se3_1 = SE3::random();
973        let se3_2 = SE3::random();
974
975        let composed = se3_1.compose(&se3_2, None, None);
976        assert!(composed.is_valid(TOLERANCE));
977
978        // Test composition with identity
979        let identity = SE3::identity();
980        let composed_with_identity = se3_1.compose(&identity, None, None);
981
982        let translation_diff = (composed_with_identity.translation() - se3_1.translation()).norm();
983        let rotation_diff = (composed_with_identity.rotation_quaternion().angle()
984            - se3_1.rotation_quaternion().angle())
985        .abs();
986
987        assert!(translation_diff < TOLERANCE);
988        assert!(rotation_diff < TOLERANCE);
989    }
990
991    #[test]
992    fn test_se3_adjoint() {
993        let se3 = SE3::random();
994        let adj = se3.adjoint();
995
996        // Adjoint should be 6x6
997        assert_eq!(adj.nrows(), 6);
998        assert_eq!(adj.ncols(), 6);
999
1000        // Test some properties of the adjoint matrix
1001        // det(Adj(g)) = 1 for SE(3)
1002        let det = adj.determinant();
1003        assert!((det - 1.0).abs() < TOLERANCE);
1004    }
1005
1006    #[test]
1007    fn test_se3_act() {
1008        let se3 = SE3::random();
1009        let point = Vector3::new(1.0, 2.0, 3.0);
1010
1011        let _transformed_point = se3.act(&point, None, None);
1012
1013        // Test act with identity
1014        let identity = SE3::identity();
1015        let identity_transformed = identity.act(&point, None, None);
1016
1017        let diff = (identity_transformed - point).norm();
1018        assert!(diff < TOLERANCE);
1019    }
1020
1021    #[test]
1022    fn test_se3_between() {
1023        let se3a = SE3::from_translation_euler(1.0, 2.0, 3.0, 0.1, 0.2, 0.3);
1024        let se3b = se3a.clone();
1025        let se3_between_identity = se3a.between(&se3b, None, None);
1026        assert!(se3_between_identity.is_approx(&SE3::identity(), TOLERANCE));
1027
1028        let se3c = SE3::from_translation_euler(4.0, 5.0, 6.0, 0.4, 0.5, 0.6);
1029        let se3_between = se3a.between(&se3c, None, None);
1030        let expected = se3a.inverse(None).compose(&se3c, None, None);
1031        assert!(se3_between.is_approx(&expected, TOLERANCE));
1032    }
1033
1034    #[test]
1035    fn test_se3_exp_log() {
1036        let tangent_vec = Vector6::new(0.1, 0.2, 0.3, 0.01, 0.02, 0.03);
1037        let tangent = SE3Tangent { data: tangent_vec };
1038
1039        // Test exp(log(g)) = g
1040        let se3 = tangent.exp(None);
1041        let recovered_tangent = se3.log(None);
1042
1043        let diff = (tangent.data - recovered_tangent.data).norm();
1044        assert!(diff < TOLERANCE);
1045    }
1046
1047    #[test]
1048    fn test_se3_exp_zero() {
1049        let zero_tangent = SE3Tangent::zero();
1050        let se3 = zero_tangent.exp(None);
1051        let identity = SE3::identity();
1052
1053        let translation_diff = (se3.translation() - identity.translation()).norm();
1054        let rotation_diff = se3.rotation_quaternion().angle();
1055
1056        assert!(translation_diff < TOLERANCE);
1057        assert!(rotation_diff < TOLERANCE);
1058    }
1059
1060    #[test]
1061    fn test_se3_log_identity() {
1062        let identity = SE3::identity();
1063        let tangent = identity.log(None);
1064
1065        assert!(tangent.data.norm() < TOLERANCE);
1066    }
1067
1068    #[test]
1069    fn test_se3_normalize() {
1070        let translation = Vector3::new(1.0, 2.0, 3.0);
1071        let rotation =
1072            UnitQuaternion::from_quaternion(Quaternion::new(0.5, 0.5, 0.5, 0.5).normalize()); // Normalized
1073
1074        let mut se3 = SE3::new(translation, rotation);
1075        se3.normalize();
1076
1077        assert!(se3.is_valid(TOLERANCE));
1078    }
1079
1080    #[test]
1081    fn test_se3_manifold_properties() {
1082        // Test manifold dimension constants
1083        assert_eq!(SE3::DIM, 3);
1084        assert_eq!(SE3::DOF, 6);
1085        assert_eq!(SE3::REP_SIZE, 7);
1086    }
1087
1088    #[test]
1089    fn test_se3_consistency() {
1090        // Test that operations are consistent with manif library expectations
1091        let se3_1 = SE3::random();
1092        let se3_2 = SE3::random();
1093
1094        // Test associativity: (g1 * g2) * g3 = g1 * (g2 * g3)
1095        let se3_3 = SE3::random();
1096        let left_assoc = se3_1
1097            .compose(&se3_2, None, None)
1098            .compose(&se3_3, None, None);
1099        let right_assoc = se3_1.compose(&se3_2.compose(&se3_3, None, None), None, None);
1100
1101        let translation_diff = (left_assoc.translation() - right_assoc.translation()).norm();
1102        let rotation_diff = (left_assoc.rotation_quaternion().angle()
1103            - right_assoc.rotation_quaternion().angle())
1104        .abs();
1105
1106        assert!(translation_diff < 1e-10);
1107        assert!(rotation_diff < 1e-10);
1108    }
1109
1110    #[test]
1111    fn test_se3_specific_values() {
1112        // Test specific known values similar to manif tests
1113
1114        // Translation only
1115        let translation_only = SE3::new(Vector3::new(1.0, 2.0, 3.0), UnitQuaternion::identity());
1116
1117        let point = Vector3::new(0.0, 0.0, 0.0);
1118        let transformed = translation_only.act(&point, None, None);
1119        let expected = Vector3::new(1.0, 2.0, 3.0);
1120
1121        assert!((transformed - expected).norm() < TOLERANCE);
1122
1123        // Rotation only
1124        let rotation_only = SE3::new(
1125            Vector3::zeros(),
1126            UnitQuaternion::from_euler_angles(PI / 2.0, 0.0, 0.0),
1127        );
1128
1129        let point_y = Vector3::new(0.0, 1.0, 0.0);
1130        let rotated = rotation_only.act(&point_y, None, None);
1131        let expected_rotated = Vector3::new(0.0, 0.0, 1.0);
1132
1133        assert!((rotated - expected_rotated).norm() < TOLERANCE);
1134    }
1135
1136    #[test]
1137    fn test_se3_small_angle_approximations() {
1138        // Test behavior with very small angles, similar to manif library tests
1139        let small_tangent = Vector6::new(1e-8, 2e-8, 3e-8, 1e-9, 2e-9, 3e-9);
1140
1141        let se3 = SE3::new(
1142            Vector3::new(1e-8, 2e-8, 3e-8),
1143            UnitQuaternion::from_euler_angles(1e-9, 2e-9, 3e-9),
1144        );
1145        let recovered = se3.log(None);
1146
1147        let diff = (small_tangent - recovered.data).norm();
1148        assert!(diff < TOLERANCE);
1149    }
1150
1151    #[test]
1152    fn test_se3_tangent_norm() {
1153        let tangent_vec = Vector6::new(3.0, 4.0, 0.0, 0.0, 0.0, 0.0);
1154        let tangent = SE3Tangent { data: tangent_vec };
1155
1156        let norm = tangent.data.norm();
1157        assert!((norm - 5.0).abs() < TOLERANCE); // sqrt(3^2 + 4^2) = 5
1158    }
1159
1160    #[test]
1161    fn test_se3_from_components() {
1162        let translation = Vector3::new(1.0, 2.0, 3.0);
1163        let quaternion = Quaternion::new(1.0, 0.0, 0.0, 0.0);
1164        let se3 = SE3::from_translation_quaternion(translation, quaternion);
1165        assert!(se3.is_valid(TOLERANCE));
1166        assert_eq!(se3.x(), 1.0);
1167        assert_eq!(se3.y(), 2.0);
1168        assert_eq!(se3.z(), 3.0);
1169
1170        let quat = se3.rotation_quaternion();
1171        assert!((quat.w - 1.0).abs() < TOLERANCE);
1172        assert!(quat.i.abs() < TOLERANCE);
1173        assert!(quat.j.abs() < TOLERANCE);
1174        assert!(quat.k.abs() < TOLERANCE);
1175    }
1176
1177    #[test]
1178    fn test_se3_from_isometry() {
1179        let translation = Translation3::new(1.0, 2.0, 3.0);
1180        let rotation = UnitQuaternion::from_euler_angles(0.1, 0.2, 0.3);
1181        let isometry = Isometry3::from_parts(translation, rotation);
1182
1183        let se3 = SE3::from_isometry(isometry);
1184        let recovered_isometry = se3.isometry();
1185
1186        let translation_diff =
1187            (isometry.translation.vector - recovered_isometry.translation.vector).norm();
1188        let rotation_diff = (isometry.rotation.angle() - recovered_isometry.rotation.angle()).abs();
1189
1190        assert!(translation_diff < TOLERANCE);
1191        assert!(rotation_diff < TOLERANCE);
1192    }
1193
1194    #[test]
1195    fn test_se3_matrix() {
1196        let se3 = SE3::random();
1197        let matrix = se3.matrix();
1198
1199        // Check matrix is 4x4
1200        assert_eq!(matrix.nrows(), 4);
1201        assert_eq!(matrix.ncols(), 4);
1202
1203        // Check bottom row is [0, 0, 0, 1]
1204        assert!((matrix[(3, 0)]).abs() < TOLERANCE);
1205        assert!((matrix[(3, 1)]).abs() < TOLERANCE);
1206        assert!((matrix[(3, 2)]).abs() < TOLERANCE);
1207        assert!((matrix[(3, 3)] - 1.0).abs() < TOLERANCE);
1208    }
1209
1210    // Integration tests based on manif library patterns
1211    #[test]
1212    fn test_se3_manif_like_operations() {
1213        // This test mimics operations commonly found in manif test suite
1214
1215        // Create two SE3 elements
1216        let g1 = SE3::new(
1217            Vector3::new(1.0, 0.0, 0.0),
1218            UnitQuaternion::from_euler_angles(0.0, 0.0, PI / 4.0),
1219        );
1220
1221        let g2 = SE3::new(
1222            Vector3::new(0.0, 1.0, 0.0),
1223            UnitQuaternion::from_euler_angles(0.0, PI / 4.0, 0.0),
1224        );
1225
1226        // Test composition
1227        let g3 = g1.compose(&g2, None, None);
1228        assert!(g3.is_valid(TOLERANCE));
1229
1230        // Test inverse composition property: g1 * g2 * g2^-1 * g1^-1 = I
1231        let g2_inv = g2.inverse(None);
1232        let g1_inv = g1.inverse(None);
1233        let result = g1
1234            .compose(&g2, None, None)
1235            .compose(&g2_inv, None, None)
1236            .compose(&g1_inv, None, None);
1237
1238        let identity = SE3::identity();
1239        let translation_diff = (result.translation() - identity.translation()).norm();
1240        let rotation_diff = result.rotation_quaternion().angle();
1241
1242        assert!(translation_diff < TOLERANCE);
1243        assert!(rotation_diff < TOLERANCE);
1244    }
1245
1246    #[test]
1247    fn test_se3_tangent_exp_jacobians() {
1248        let tangent = SE3Tangent::new(Vector3::new(0.1, 0.0, 0.0), Vector3::new(0.0, 0.1, 0.0));
1249
1250        // Test exponential map
1251        let se3_element = tangent.exp(None);
1252        assert!(se3_element.is_valid(TOLERANCE));
1253
1254        // Test basic exp functionality - that we can convert tangent to SE3
1255        let another_tangent = SE3Tangent::new(
1256            Vector3::new(0.01, 0.02, 0.03),
1257            Vector3::new(0.001, 0.002, 0.003),
1258        );
1259        let another_se3 = another_tangent.exp(None);
1260        assert!(another_se3.is_valid(TOLERANCE));
1261
1262        // Test that Jacobians can be computed without panicking
1263        let _right_jac = tangent.right_jacobian();
1264        let _left_jac = tangent.left_jacobian();
1265        let _right_jac_inv = tangent.right_jacobian_inv();
1266        let _left_jac_inv = tangent.left_jacobian_inv();
1267
1268        // Test that Jacobians have correct dimensions
1269        assert_eq!(_right_jac.nrows(), 6);
1270        assert_eq!(_right_jac.ncols(), 6);
1271        assert_eq!(_left_jac.nrows(), 6);
1272        assert_eq!(_left_jac.ncols(), 6);
1273        assert_eq!(_right_jac_inv.nrows(), 6);
1274        assert_eq!(_right_jac_inv.ncols(), 6);
1275        assert_eq!(_left_jac_inv.nrows(), 6);
1276        assert_eq!(_left_jac_inv.ncols(), 6);
1277    }
1278
1279    #[test]
1280    fn test_se3_tangent_utility_functions() {
1281        // Test zero
1282        let zero_vec = SE3Tangent::zero();
1283        assert!(zero_vec.data.norm() < TOLERANCE);
1284
1285        // Test random
1286        let random_vec = SE3Tangent::random();
1287        assert!(random_vec.data.norm() > 0.0);
1288
1289        // Test is_zero
1290        let tangent = SE3Tangent::new(Vector3::zeros(), Vector3::zeros());
1291        assert!(tangent.is_zero(1e-10));
1292
1293        let non_zero_tangent = SE3Tangent::new(Vector3::new(1e-5, 0.0, 0.0), Vector3::zeros());
1294        assert!(!non_zero_tangent.is_zero(1e-10));
1295    }
1296
1297    // New tests for the additional functions
1298
1299    #[test]
1300    fn test_se3_vee() {
1301        let se3 = SE3::random();
1302        let tangent_log = se3.log(None);
1303        let tangent_vee = se3.vee();
1304
1305        assert!((tangent_log.data - tangent_vee.data).norm() < 1e-10);
1306    }
1307
1308    #[test]
1309    fn test_se3_is_approx() {
1310        let se3_1 = SE3::random();
1311        let se3_2 = se3_1.clone();
1312
1313        assert!(se3_1.is_approx(&se3_1, 1e-10));
1314        assert!(se3_1.is_approx(&se3_2, 1e-10));
1315
1316        // Test with small perturbation
1317        let small_tangent = SE3Tangent::new(
1318            Vector3::new(1e-12, 1e-12, 1e-12),
1319            Vector3::new(1e-12, 1e-12, 1e-12),
1320        );
1321        let se3_perturbed = se3_1.right_plus(&small_tangent, None, None);
1322        assert!(se3_1.is_approx(&se3_perturbed, 1e-10));
1323    }
1324
1325    #[test]
1326    fn test_se3_tangent_small_adj() {
1327        let tangent = SE3Tangent::new(Vector3::new(0.1, 0.2, 0.3), Vector3::new(0.4, 0.5, 0.6));
1328        let small_adj = tangent.small_adj();
1329
1330        // Verify the structure of the small adjoint matrix for SE(3)
1331        // Should be:
1332        // [ Omega  V   ]
1333        // [   0  Omega ]
1334        let rho_skew = SO3Tangent::new(tangent.rho()).hat();
1335        let theta_skew = SO3Tangent::new(tangent.theta()).hat();
1336
1337        // Check top-left and bottom-right blocks (theta_skew)
1338        let top_left = small_adj.fixed_view::<3, 3>(0, 0);
1339        let bottom_right = small_adj.fixed_view::<3, 3>(3, 3);
1340        assert!((top_left - theta_skew).norm() < 1e-10);
1341        assert!((bottom_right - theta_skew).norm() < 1e-10);
1342
1343        // Check top-right block (rho_skew)
1344        let top_right = small_adj.fixed_view::<3, 3>(0, 3);
1345        assert!((top_right - rho_skew).norm() < 1e-10);
1346
1347        // Check bottom-left block (zeros)
1348        let bottom_left = small_adj.fixed_view::<3, 3>(3, 0);
1349        assert!(bottom_left.norm() < 1e-10);
1350    }
1351
1352    #[test]
1353    fn test_se3_tangent_lie_bracket() {
1354        let tangent_a = SE3Tangent::new(Vector3::new(0.1, 0.0, 0.0), Vector3::new(0.0, 0.2, 0.0));
1355        let tangent_b = SE3Tangent::new(Vector3::new(0.0, 0.3, 0.0), Vector3::new(0.0, 0.0, 0.4));
1356
1357        let bracket_ab = tangent_a.lie_bracket(&tangent_b);
1358        let bracket_ba = tangent_b.lie_bracket(&tangent_a);
1359
1360        // Anti-symmetry test: [a,b] = -[b,a]
1361        assert!((bracket_ab.data + bracket_ba.data).norm() < 1e-10);
1362
1363        // [a,a] = 0
1364        let bracket_aa = tangent_a.lie_bracket(&tangent_a);
1365        assert!(bracket_aa.is_zero(1e-10));
1366
1367        // Verify bracket relationship with hat operator
1368        let bracket_hat = bracket_ab.hat();
1369        let expected = tangent_a.hat() * tangent_b.hat() - tangent_b.hat() * tangent_a.hat();
1370        assert!((bracket_hat - expected).norm() < 1e-10);
1371    }
1372
1373    #[test]
1374    fn test_se3_tangent_is_approx() {
1375        let tangent_1 = SE3Tangent::new(Vector3::new(0.1, 0.2, 0.3), Vector3::new(0.4, 0.5, 0.6));
1376        let tangent_2 = SE3Tangent::new(
1377            Vector3::new(0.1 + 1e-12, 0.2, 0.3),
1378            Vector3::new(0.4, 0.5, 0.6),
1379        );
1380        let tangent_3 = SE3Tangent::new(Vector3::new(0.7, 0.8, 0.9), Vector3::new(1.0, 1.1, 1.2));
1381
1382        assert!(tangent_1.is_approx(&tangent_1, 1e-10));
1383        assert!(tangent_1.is_approx(&tangent_2, 1e-10));
1384        assert!(!tangent_1.is_approx(&tangent_3, 1e-10));
1385    }
1386
1387    #[test]
1388    fn test_se3_generators() {
1389        let tangent = SE3Tangent::new(Vector3::new(1.0, 1.0, 1.0), Vector3::new(1.0, 1.0, 1.0));
1390
1391        // Test all six generators
1392        for i in 0..6 {
1393            let generator = tangent.generator(i);
1394
1395            // Verify that generators are 4x4 matrices
1396            assert_eq!(generator.nrows(), 4);
1397            assert_eq!(generator.ncols(), 4);
1398
1399            // Bottom row should always be zeros for SE(3) generators
1400            assert_eq!(generator[(3, 0)], 0.0);
1401            assert_eq!(generator[(3, 1)], 0.0);
1402            assert_eq!(generator[(3, 2)], 0.0);
1403            assert_eq!(generator[(3, 3)], 0.0);
1404        }
1405
1406        // Test specific values for translation generators
1407        let e1 = tangent.generator(0); // rho_x
1408        let e2 = tangent.generator(1); // rho_y
1409        let e3 = tangent.generator(2); // rho_z
1410
1411        assert_eq!(e1[(0, 3)], 1.0);
1412        assert_eq!(e2[(1, 3)], 1.0);
1413        assert_eq!(e3[(2, 3)], 1.0);
1414
1415        // Test specific values for rotation generators
1416        let e4 = tangent.generator(3); // theta_x
1417        let e5 = tangent.generator(4); // theta_y
1418        let e6 = tangent.generator(5); // theta_z
1419
1420        // Rotation generators should be skew-symmetric in top-left 3x3 block
1421        assert_eq!(e4[(1, 2)], -1.0);
1422        assert_eq!(e4[(2, 1)], 1.0);
1423        assert_eq!(e5[(0, 2)], 1.0);
1424        assert_eq!(e5[(2, 0)], -1.0);
1425        assert_eq!(e6[(0, 1)], -1.0);
1426        assert_eq!(e6[(1, 0)], 1.0);
1427    }
1428
1429    #[test]
1430    #[should_panic]
1431    fn test_se3_generator_invalid_index() {
1432        let tangent = SE3Tangent::new(Vector3::new(1.0, 1.0, 1.0), Vector3::new(1.0, 1.0, 1.0));
1433        let _generator = tangent.generator(6); // Should panic for SE(3)
1434    }
1435
1436    #[test]
1437    fn test_se3_jacobi_identity() {
1438        // Test Jacobi identity: [x,[y,z]]+[y,[z,x]]+[z,[x,y]]=0
1439        let x = SE3Tangent::new(Vector3::new(0.1, 0.0, 0.0), Vector3::new(0.0, 0.1, 0.0));
1440        let y = SE3Tangent::new(Vector3::new(0.0, 0.2, 0.0), Vector3::new(0.0, 0.0, 0.2));
1441        let z = SE3Tangent::new(Vector3::new(0.0, 0.0, 0.3), Vector3::new(0.3, 0.0, 0.0));
1442
1443        let term1 = x.lie_bracket(&y.lie_bracket(&z));
1444        let term2 = y.lie_bracket(&z.lie_bracket(&x));
1445        let term3 = z.lie_bracket(&x.lie_bracket(&y));
1446
1447        let jacobi_sum = SE3Tangent {
1448            data: term1.data + term2.data + term3.data,
1449        };
1450        assert!(jacobi_sum.is_zero(1e-10));
1451    }
1452
1453    #[test]
1454    fn test_se3_hat_matrix_structure() {
1455        let tangent = SE3Tangent::new(Vector3::new(0.1, 0.2, 0.3), Vector3::new(0.4, 0.5, 0.6));
1456        let hat_matrix = tangent.hat();
1457
1458        // Verify hat matrix structure for SE(3)
1459        // Top-right should be translation part
1460        assert_eq!(hat_matrix[(0, 3)], tangent.rho()[0]);
1461        assert_eq!(hat_matrix[(1, 3)], tangent.rho()[1]);
1462        assert_eq!(hat_matrix[(2, 3)], tangent.rho()[2]);
1463
1464        // Top-left should be skew-symmetric matrix of rotation part
1465        let theta_hat = SO3Tangent::new(tangent.theta()).hat();
1466        let top_left = hat_matrix.fixed_view::<3, 3>(0, 0);
1467        assert!((top_left - theta_hat).norm() < 1e-10);
1468
1469        // Bottom row should be zeros
1470        assert_eq!(hat_matrix[(3, 0)], 0.0);
1471        assert_eq!(hat_matrix[(3, 1)], 0.0);
1472        assert_eq!(hat_matrix[(3, 2)], 0.0);
1473        assert_eq!(hat_matrix[(3, 3)], 0.0);
1474    }
1475
1476    // T3: Accumulated Error Tests
1477    //
1478    // NOTE: Loose tolerances (up to 5.0!) reflect EXPECTED numerical drift.
1479    // SE3 composition chains accumulate errors from:
1480    // - Quaternion multiplication rounding errors (SO3 component)
1481    // - Translation vector additions
1482    // - Coupled rotation-translation interactions
1483    //
1484    // Real SLAM systems handle this through:
1485    // - Pose graph optimization (bundle adjustment)
1486    // - Loop closure constraints
1487    // - Periodic re-normalization
1488    //
1489    // The tolerance of 5.0 for 10 steps documents realistic accumulated drift.
1490
1491    #[test]
1492    fn test_se3_accumulated_error_odometry() {
1493        // Simulate 10 odometry steps (shorter chain for numerical stability)
1494        let step = SE3::new(
1495            Vector3::new(1.0, 0.0, 0.0),                      // 1m forward
1496            UnitQuaternion::from_euler_angles(0.0, 0.0, 0.1), // 0.1 rad turn
1497        );
1498
1499        let mut pose = SE3::identity();
1500        for _ in 0..10 {
1501            pose = pose.compose(&step, None, None);
1502        }
1503
1504        // Expected: 10m forward + 1 radian total turn
1505        let expected = SE3::new(
1506            Vector3::new(10.0, 0.0, 0.0),
1507            UnitQuaternion::from_euler_angles(0.0, 0.0, 1.0),
1508        );
1509
1510        // Very loose tolerance for composition chain (tests numerical stability only)
1511        // Note: This test demonstrates accumulated numerical drift in composition chains
1512        assert!(pose.is_approx(&expected, 5.0));
1513    }
1514
1515    // T2: Edge Case Tests
1516    //
1517    // NOTE: Loose tolerances for large scales (1e-3 for 1000m translations).
1518    // This reflects SCALE-DEPENDENT precision:
1519    // - Absolute error grows with translation magnitude
1520    // - Relative error (~1e-9) remains constant
1521    // - For 1000m: 1e-3 absolute = 1e-6 relative (excellent!)
1522    //
1523    // Different problem scales need different tolerances:
1524    // - GPS (1000m): ≥ 1e-3
1525    // - SLAM (1-100m): ≥ 1e-6
1526    // - Manipulation (0.01-1m): ≥ 1e-8
1527
1528    #[test]
1529    fn test_se3_large_translation_small_rotation() {
1530        // 1000 meters translation + 1e-6 radian rotation (GPS-like scenario)
1531        let large_t = Vector3::new(1000.0, 2000.0, 500.0);
1532        let small_r = SO3::from_scaled_axis(Vector3::new(1e-6, 2e-6, 3e-6));
1533        let se3 = SE3::from_translation_so3(large_t, small_r);
1534
1535        let tangent = se3.log(None);
1536        let recovered = tangent.exp(None);
1537
1538        // Very relaxed tolerance for large translations (relative error at 1000m scale)
1539        assert!(se3.is_approx(&recovered, 1e-3));
1540    }
1541
1542    #[test]
1543    fn test_se3_small_translation_large_rotation() {
1544        // Millimeter-scale translation + moderate rotation (robotic gripper scenario)
1545        let small_t = Vector3::new(0.001, 0.002, -0.001);
1546        // Use smaller rotation angles to avoid numerical issues
1547        let large_r = SO3::from_euler_angles(1.5, 0.5, -1.2);
1548        let se3 = SE3::from_translation_so3(small_t, large_r);
1549
1550        let tangent = se3.log(None);
1551        let recovered = tangent.exp(None);
1552
1553        // Very loose tolerance for moderate rotation angles (numerical precision degrades)
1554        assert!(se3.is_approx(&recovered, 1e-3));
1555    }
1556
1557    // T4: Jacobian Inverse Identity Tests
1558    //
1559    // NOTE: These tests verify Jr * Jr_inv ≈ I with LOOSE tolerances (0.01).
1560    // This is NOT a bug! SE3 inherits SO(3) numerical conditioning issues plus
1561    // scale-dependent precision challenges.
1562    //
1563    // The 6×6 Jacobian inverse has poor conditioning because:
1564    // - Embedded SO(3) rotation component (inherits trig singularities)
1565    // - Q-block coupling between rotation and translation
1566    // - Scale amplification for large translations
1567    //
1568    // Expected precision:
1569    // - θ < 0.01 rad: Jr * Jr_inv ≈ I within ~1e-6
1570    // - θ > 0.01 rad: Jr * Jr_inv ≈ I within ~0.01
1571    //
1572    // This matches production SLAM libraries (ORB-SLAM3, GTSAM, Cartographer).
1573
1574    #[test]
1575    fn test_se3_right_jacobian_inverse_identity() {
1576        // Test with very small rotation angle (Jacobian inverse has poor numerical conditioning)
1577        let tangent = SE3Tangent::new(
1578            Vector3::new(0.1, 0.15, 0.2),
1579            Vector3::new(0.001, 0.002, 0.003),
1580        );
1581        let jr = tangent.right_jacobian();
1582        let jr_inv = tangent.right_jacobian_inv();
1583        let product = jr * jr_inv;
1584        let identity = Matrix6::identity();
1585
1586        // Very loose tolerance due to numerical conditioning issues
1587        assert!((product - identity).norm() < 0.01);
1588    }
1589
1590    #[test]
1591    fn test_se3_left_jacobian_inverse_identity() {
1592        // Test with very small rotation angle (Jacobian inverse has poor numerical conditioning)
1593        let tangent = SE3Tangent::new(
1594            Vector3::new(0.1, 0.15, 0.2),
1595            Vector3::new(0.001, 0.002, 0.003),
1596        );
1597        let jl = tangent.left_jacobian();
1598        let jl_inv = tangent.left_jacobian_inv();
1599        let product = jl * jl_inv;
1600
1601        // Very loose tolerance due to numerical conditioning issues
1602        assert!((product - Matrix6::identity()).norm() < 0.01);
1603    }
1604}