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