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        // ISSUE-0008: the translation was never checked, only the rotation —
390        // a NaN/Inf translation previously passed silently.
391        self.translation_impl().iter().all(|t| t.is_finite())
392            && self.rotation_impl().is_valid(tolerance)
393    }
394
395    fn as_param_slice(&self) -> &[f64] {
396        self.params.as_slice()
397    }
398
399    /// Raw write access to the 7-parameter storage.
400    ///
401    /// # Safety of representation (not memory)
402    ///
403    /// Writing the quaternion components (`params[3..7]`) through this slice
404    /// does **not** renormalize: a non-unit quaternion silently degrades every
405    /// subsequent exp/retraction built on it. The solver path never needs
406    /// this — tangent steps go through the exponential map, which is
407    /// unit-preserving — but consumers seeding parameters from files or
408    /// sensors must call [`LieGroup::normalize`] once after batch writes:
409    ///
410    /// ```
411    /// # use apex_manifolds::se3::SE3;
412    /// # use apex_manifolds::LieGroup;
413    /// # use nalgebra::dvector;
414    /// let mut pose = SE3::identity();
415    /// pose.as_param_slice_mut()[3] += 0.5; // no longer unit
416    /// pose.normalize();
417    /// assert!(pose.is_valid(1e-9));
418    /// ```
419    fn as_param_slice_mut(&mut self) -> &mut [f64] {
420        self.params.as_mut_slice()
421    }
422
423    fn from_param_slice(s: &[f64]) -> Self {
424        debug_assert_eq!(s.len(), 7);
425        SE3 {
426            params: SVector::from_column_slice(s),
427        }
428    }
429
430    /// Vee operator: log(g)^∨.
431    ///
432    /// Maps a group element g ∈ G to its tangent vector log(g)^∨ ∈ 𝔤.
433    /// For SE(3), this is the same as log().
434    fn vee(&self) -> Self::TangentVector {
435        self.log(None)
436    }
437
438    /// Check if the element is approximately equal to another element.
439    ///
440    /// # Arguments
441    /// * `other` - The other element to compare with
442    /// * `tolerance` - The tolerance for the comparison
443    fn is_approx(&self, other: &Self, tolerance: f64) -> bool {
444        let difference = self.right_minus(other, None, None);
445        difference.is_zero(tolerance)
446    }
447}
448
449/// SE(3) tangent space element representing elements in the Lie algebra se(3).
450///
451/// Following manif conventions, internally represented as [rho(3), theta(3)] where:
452/// - rho: translational component [rho_x, rho_y, rho_z]
453/// - theta: rotational component [theta_x, theta_y, theta_z]
454#[derive(Clone, PartialEq)]
455pub struct SE3Tangent {
456    /// Internal data: [rho_x, rho_y, rho_z, theta_x, theta_y, theta_z]
457    data: Vector6<f64>,
458}
459
460impl fmt::Display for SE3Tangent {
461    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
462        let rho = self.rho();
463        let theta = self.theta();
464        write!(
465            f,
466            "se3(rho: [{:.4}, {:.4}, {:.4}], theta: [{:.4}, {:.4}, {:.4}])",
467            rho.x, rho.y, rho.z, theta.x, theta.y, theta.z
468        )
469    }
470}
471
472impl SE3Tangent {
473    /// Create a new SE3Tangent from rho (translational) and theta (rotational) components.
474    ///
475    /// # Arguments
476    /// * `rho` - Translational component [rho_x, rho_y, rho_z]
477    /// * `theta` - Rotational component [theta_x, theta_y, theta_z]
478    #[inline]
479    pub fn new(rho: Vector3<f64>, theta: Vector3<f64>) -> Self {
480        let mut data = Vector6::zeros();
481        data.fixed_rows_mut::<3>(0).copy_from(&rho);
482        data.fixed_rows_mut::<3>(3).copy_from(&theta);
483        SE3Tangent { data }
484    }
485
486    /// Create SE3Tangent from individual components.
487    pub fn from_components(
488        rho_x: f64,
489        rho_y: f64,
490        rho_z: f64,
491        theta_x: f64,
492        theta_y: f64,
493        theta_z: f64,
494    ) -> Self {
495        SE3Tangent {
496            data: Vector6::new(rho_x, rho_y, rho_z, theta_x, theta_y, theta_z),
497        }
498    }
499
500    /// Get the rho (translational) part.
501    #[inline]
502    pub fn rho(&self) -> Vector3<f64> {
503        self.data.fixed_rows::<3>(0).into_owned()
504    }
505
506    /// Get the theta (rotational) part.
507    #[inline]
508    pub fn theta(&self) -> Vector3<f64> {
509        self.data.fixed_rows::<3>(3).into_owned()
510    }
511
512    /// Equation 180: Q(ρ, θ) function for SE(3) Jacobians
513    /// Q(ρ, θ) = (1/2)ρₓ + (θ - sin θ)/θ³ (θₓρₓ + ρₓθₓ + θₓρₓθₓ)
514    ///           - (1 - θ²/2 - cos θ)/θ⁴ (θ²ₓρₓ + ρₓθ²ₓ - 3θₓρₓθₓ)
515    ///           - (1/2) * (c - 3e) * (θₓρₓθ²ₓ + θ²ₓρₓθₓ)
516    ///
517    /// where `c = (1 - θ²/2 - cos θ)/θ⁴` and `e = (θ - sin θ - θ³/6)/θ⁵`.
518    pub fn q_block_jacobian_matrix(rho: Vector3<f64>, theta: Vector3<f64>) -> Matrix3<f64> {
519        let rho_skew = SO3Tangent::new(rho).hat();
520        let theta_skew = SO3Tangent::new(theta).hat();
521        let theta_squared = theta.norm_squared();
522
523        let a = 0.5;
524        let mut b = 1.0 / 6.0 + 1.0 / 120.0 * theta_squared;
525        let mut c = -1.0 / 24.0 + 1.0 / 720.0 * theta_squared;
526        // d = (1/2)(c - 3e); Taylor series of that combination near θ = 0.
527        let mut d = -1.0 / 120.0 + theta_squared / 2520.0;
528
529        if theta_squared > crate::SMALL_ANGLE_THRESHOLD {
530            let theta_norm = theta_squared.sqrt();
531            let theta_norm_3 = theta_norm * theta_squared;
532            let theta_norm_4 = theta_squared * theta_squared;
533            let theta_norm_5 = theta_norm_3 * theta_squared;
534            let sin_theta = theta_norm.sin();
535            let cos_theta = theta_norm.cos();
536
537            b = (theta_norm - sin_theta) / theta_norm_3;
538            c = (1.0 - theta_squared / 2.0 - cos_theta) / theta_norm_4;
539            let e = (theta_norm - sin_theta - theta_norm_3 / 6.0) / theta_norm_5;
540            d = 0.5 * (c - 3.0 * e);
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        // The last term needs both θₓρₓθ²ₓ (= trt·θₓ) and θ²ₓρₓθₓ (= θₓ·trt).
548        let trt2 = trt * theta_skew;
549        let t2rt = theta_skew * trt;
550
551        rho_skew * a + (tr + rt + trt) * b
552            - (rt_t2 - rt_t2.transpose() - trt * 3.0) * c
553            - (trt2 + t2rt) * d
554    }
555}
556
557// Implement LieAlgebra trait for SE3Tangent
558impl Tangent<SE3> for SE3Tangent {
559    /// Dimension of the tangent space
560    const DIM: usize = 6;
561
562    /// Get the SE3 element.
563    ///
564    /// # Arguments
565    /// * `tangent` - Tangent vector [rho, theta]
566    /// * `jacobian` - Optional Jacobian matrix of the SE3 element wrt this.
567    ///
568    /// # Notes
569    /// # Equation 172: SE(3) exponential map
570    /// M = exp(τ) = [ R(θ)   t(ρ) ]
571    ///              [ 0       1   ]
572    fn exp(&self, jacobian: Option<&mut <SE3 as LieGroup>::JacobianMatrix>) -> SE3 {
573        let rho = self.rho();
574        let theta = self.theta();
575
576        let theta_tangent = SO3Tangent::new(theta);
577        // Compute rotation part using SO(3) exponential
578        let rotation = theta_tangent.exp(None);
579        let translation = theta_tangent.left_jacobian() * rho;
580
581        if let Some(jac) = jacobian {
582            *jac = self.right_jacobian();
583        }
584
585        SE3::from_translation_so3(translation, rotation)
586    }
587
588    /// Right Jacobian Jr.
589    ///
590    /// Computes the right Jacobian matrix such that for small δφ:
591    /// exp((φ + δφ)^∧) ≈ exp(φ^∧) ∘ exp((Jr δφ)^∧)
592    ///
593    /// For SE(3), this involves computing Jacobians for both translation and rotation parts.
594    ///
595    /// # Returns
596    /// The right Jacobian matrix (6x6)
597    fn right_jacobian(&self) -> <SE3 as LieGroup>::JacobianMatrix {
598        let mut jac = Matrix6::zeros();
599        let rho = self.rho();
600        let theta = self.theta();
601        let theta_right_jac = SO3Tangent::new(theta).right_jacobian();
602        jac.fixed_view_mut::<3, 3>(0, 0).copy_from(&theta_right_jac);
603        jac.fixed_view_mut::<3, 3>(3, 3).copy_from(&theta_right_jac);
604        jac.fixed_view_mut::<3, 3>(0, 3)
605            .copy_from(&SE3Tangent::q_block_jacobian_matrix(-rho, -theta));
606        jac
607    }
608
609    /// Left Jacobian Jl.
610    ///
611    /// Computes the left Jacobian matrix such that for small δφ:
612    /// exp((φ + δφ)^∧) ≈ exp((Jl δφ)^∧) ∘ exp(φ^∧)
613    ///
614    /// Following manif conventions for SE(3) left Jacobian computation.
615    ///
616    /// # Returns
617    /// The left Jacobian matrix (6x6)
618    fn left_jacobian(&self) -> <SE3 as LieGroup>::JacobianMatrix {
619        let mut jac = Matrix6::zeros();
620        let theta_left_jac = SO3Tangent::new(self.theta()).left_jacobian();
621        jac.fixed_view_mut::<3, 3>(0, 0).copy_from(&theta_left_jac);
622        jac.fixed_view_mut::<3, 3>(3, 3).copy_from(&theta_left_jac);
623        jac.fixed_view_mut::<3, 3>(0, 3)
624            .copy_from(&SE3Tangent::q_block_jacobian_matrix(
625                self.rho(),
626                self.theta(),
627            ));
628        jac
629    }
630
631    /// Inverse of right Jacobian Jr⁻¹.
632    ///
633    /// Computes the inverse of the right Jacobian. This is used for
634    /// computing perturbations and derivatives.
635    ///
636    /// # Numerical Conditioning Warning
637    ///
638    /// **This 6×6 Jacobian inverse inherits SO(3) conditioning issues plus scale effects.**
639    ///
640    /// Sources of numerical error:
641    /// - Embedded SO(3) rotation: (1 + cos θ) / (2θ sin θ) singularity
642    /// - Q-block coupling: rotation errors propagate to translation
643    /// - Scale amplification: large translations increase absolute error
644    ///
645    /// **Expected Precision**:
646    /// - Small rotations (θ < 0.01): Jr * Jr⁻¹ ≈ I within ~1e-6
647    /// - Moderate rotations (θ < 0.1): Jr * Jr⁻¹ ≈ I within ~1e-4
648    /// - Larger rotations: Jr * Jr⁻¹ ≈ I within ~0.01
649    ///
650    /// This is a **fundamental mathematical limitation** consistent with production
651    /// SLAM libraries. See module documentation for references.
652    ///
653    /// # Returns
654    /// The inverse right Jacobian matrix (6x6)
655    fn right_jacobian_inv(&self) -> <SE3 as LieGroup>::JacobianMatrix {
656        let mut jac = Matrix6::zeros();
657        let rho = self.rho();
658        let theta = self.theta();
659        let theta_right_inv_jac = SO3Tangent::new(theta).right_jacobian_inv();
660        let q_block_jac = SE3Tangent::q_block_jacobian_matrix(-rho, -theta);
661        jac.fixed_view_mut::<3, 3>(0, 0)
662            .copy_from(&theta_right_inv_jac);
663        jac.fixed_view_mut::<3, 3>(3, 3)
664            .copy_from(&theta_right_inv_jac);
665        let top_right = -1.0 * theta_right_inv_jac * q_block_jac * theta_right_inv_jac;
666        jac.fixed_view_mut::<3, 3>(0, 3).copy_from(&top_right);
667        jac
668    }
669
670    /// Inverse of left Jacobian Jl⁻¹.
671    ///
672    /// Computes the inverse of the left Jacobian following manif conventions.
673    ///
674    /// # Numerical Conditioning Warning
675    ///
676    /// **This 6×6 Jacobian inverse has the same conditioning issues as the right Jacobian inverse.**
677    ///
678    /// Sources of numerical error:
679    /// - Embedded SO(3) rotation: (1 + cos θ) / (2θ sin θ) singularity
680    /// - Q-block coupling: rotation errors propagate to translation
681    /// - Scale amplification: large translations increase absolute error
682    ///
683    /// **Expected Precision**: Same as right Jacobian inverse (see above).
684    ///
685    /// This is a **fundamental mathematical limitation** consistent with production
686    /// SLAM libraries. See module documentation for references.
687    ///
688    /// # Returns
689    /// The inverse left Jacobian matrix (6x6)
690    fn left_jacobian_inv(&self) -> <SE3 as LieGroup>::JacobianMatrix {
691        let mut jac = Matrix6::zeros();
692        let rho = self.rho();
693        let theta = self.theta();
694        let theta_left_inv_jac = SO3Tangent::new(theta).left_jacobian_inv();
695        let q_block_jac = SE3Tangent::q_block_jacobian_matrix(rho, theta);
696        let top_right_block = -1.0 * theta_left_inv_jac * q_block_jac * theta_left_inv_jac;
697        jac.fixed_view_mut::<3, 3>(0, 0)
698            .copy_from(&theta_left_inv_jac);
699        jac.fixed_view_mut::<3, 3>(3, 3)
700            .copy_from(&theta_left_inv_jac);
701        jac.fixed_view_mut::<3, 3>(0, 3).copy_from(&top_right_block);
702        jac
703    }
704
705    // Matrix representations
706
707    /// Hat operator: φ^∧ (vector to matrix).
708    ///
709    /// Converts the SE(3) tangent vector to its 4x4 matrix representation in the Lie algebra.
710    /// Following manif conventions, the structure is:
711    /// [  theta_×   rho  ]
712    /// [      0       0    ]
713    /// where theta_× is the skew-symmetric matrix of the rotational part.
714    ///
715    /// # Returns
716    /// The 4x4 matrix representation in the SE(3) Lie algebra
717    fn hat(&self) -> <SE3 as LieGroup>::LieAlgebra {
718        let mut lie_alg = Matrix4::zeros();
719
720        // Top-left 3x3: skew-symmetric matrix of rotational part
721        let theta_hat = SO3Tangent::new(self.theta()).hat();
722        lie_alg.view_mut((0, 0), (3, 3)).copy_from(&theta_hat);
723
724        // Top-right 3x1: translational part
725        let rho = self.rho();
726        lie_alg[(0, 3)] = rho[0];
727        lie_alg[(1, 3)] = rho[1];
728        lie_alg[(2, 3)] = rho[2];
729
730        lie_alg
731    }
732
733    // Utility functions
734
735    /// Zero tangent vector.
736    ///
737    /// Returns the zero element of the SE(3) tangent space.
738    ///
739    /// # Returns
740    /// A 6-dimensional zero vector
741    fn zero() -> <SE3 as LieGroup>::TangentVector {
742        SE3Tangent::new(Vector3::zeros(), Vector3::zeros())
743    }
744
745    /// Random tangent vector (useful for testing).
746    ///
747    /// Generates a random tangent vector with reasonable bounds.
748    /// Translation components are in [-1, 1] and rotation components in [-0.1, 0.1].
749    ///
750    /// # Returns
751    /// A random 6-dimensional tangent vector
752    fn random() -> <SE3 as LieGroup>::TangentVector {
753        use rand::Rng;
754        let mut rng = rand::rng();
755        SE3Tangent::from_components(
756            rng.random_range(-1.0..1.0), // rho_x
757            rng.random_range(-1.0..1.0), // rho_y
758            rng.random_range(-1.0..1.0), // rho_z
759            rng.random_range(-0.1..0.1), // theta_x
760            rng.random_range(-0.1..0.1), // theta_y
761            rng.random_range(-0.1..0.1), // theta_z
762        )
763    }
764
765    /// Check if the tangent vector is approximately zero.
766    ///
767    /// Compares the norm of the tangent vector to the given tolerance.
768    ///
769    /// # Arguments
770    /// * `tolerance` - Tolerance for zero comparison
771    ///
772    /// # Returns
773    /// True if the norm is below the tolerance
774    fn is_zero(&self, tolerance: f64) -> bool {
775        self.data.norm() < tolerance
776    }
777
778    /// Normalize the tangent vector to unit norm.
779    ///
780    /// Modifies this tangent vector to have unit norm. If the vector
781    /// is near zero, it remains unchanged.
782    fn normalize(&mut self) {
783        let theta_norm = self.theta().norm();
784        self.data[3] /= theta_norm;
785        self.data[4] /= theta_norm;
786        self.data[5] /= theta_norm;
787    }
788
789    /// Return a unit tangent vector in the same direction.
790    ///
791    /// Returns a new tangent vector with unit norm in the same direction.
792    /// If the original vector is near zero, returns zero.
793    ///
794    /// # Returns
795    /// A normalized copy of the tangent vector
796    fn normalized(&self) -> <SE3 as LieGroup>::TangentVector {
797        let norm = self.theta().norm();
798        if norm > f64::EPSILON {
799            SE3Tangent::new(self.rho(), self.theta() / norm)
800        } else {
801            SE3Tangent::new(self.rho(), Vector3::zeros())
802        }
803    }
804
805    fn as_slice(&self) -> &[f64] {
806        self.data.as_slice()
807    }
808
809    fn from_slice(s: &[f64]) -> Self {
810        debug_assert_eq!(s.len(), 6);
811        SE3Tangent {
812            data: Vector6::from_column_slice(s),
813        }
814    }
815
816    /// Small adjoint matrix for SE(3).
817    ///
818    /// For SE(3), the small adjoint matrix has the structure:
819    /// [ Omega  V   ]
820    /// [   0  Omega ]
821    /// where Omega is the skew-symmetric matrix of the angular part
822    /// and V is the skew-symmetric matrix of the linear part.
823    fn small_adj(&self) -> <SE3 as LieGroup>::JacobianMatrix {
824        let mut small_adj = Matrix6::zeros();
825        let rho_skew = SO3Tangent::new(self.rho()).hat();
826        let theta_skew = SO3Tangent::new(self.theta()).hat();
827
828        // Top-left and bottom-right blocks: theta_skew (skew-symmetric of angular part)
829        small_adj
830            .fixed_view_mut::<3, 3>(0, 0)
831            .copy_from(&theta_skew);
832        small_adj
833            .fixed_view_mut::<3, 3>(3, 3)
834            .copy_from(&theta_skew);
835
836        // Top-right block: rho_skew (skew-symmetric of linear part)
837        small_adj.fixed_view_mut::<3, 3>(0, 3).copy_from(&rho_skew);
838
839        // Bottom-left block: zeros (already set)
840
841        small_adj
842    }
843
844    /// Lie bracket for SE(3).
845    ///
846    /// Computes the Lie bracket [this, other] = this.small_adj() * other.
847    fn lie_bracket(&self, other: &Self) -> <SE3 as LieGroup>::TangentVector {
848        let bracket_result = self.small_adj() * other.data;
849        SE3Tangent {
850            data: bracket_result,
851        }
852    }
853
854    /// Check if this tangent vector is approximately equal to another.
855    ///
856    /// # Arguments
857    /// * `other` - The other tangent vector to compare with
858    /// * `tolerance` - The tolerance for the comparison
859    fn is_approx(&self, other: &Self, tolerance: f64) -> bool {
860        (self.data - other.data).norm() < tolerance
861    }
862
863    /// Get the ith generator of the SE(3) Lie algebra.
864    ///
865    /// # Arguments
866    /// * `i` - Index of the generator (0-5 for SE(3))
867    ///
868    /// # Returns
869    /// The generator matrix
870    fn generator(&self, i: usize) -> <SE3 as LieGroup>::LieAlgebra {
871        assert!(i < 6, "SE(3) only has generators for indices 0-5");
872
873        let mut generator = Matrix4::zeros();
874
875        match i {
876            0 => {
877                // Generator for rho_x (translation in x)
878                generator[(0, 3)] = 1.0;
879            }
880            1 => {
881                // Generator for rho_y (translation in y)
882                generator[(1, 3)] = 1.0;
883            }
884            2 => {
885                // Generator for rho_z (translation in z)
886                generator[(2, 3)] = 1.0;
887            }
888            3 => {
889                // Generator for theta_x (rotation around x-axis)
890                generator[(1, 2)] = -1.0;
891                generator[(2, 1)] = 1.0;
892            }
893            4 => {
894                // Generator for theta_y (rotation around y-axis)
895                generator[(0, 2)] = 1.0;
896                generator[(2, 0)] = -1.0;
897            }
898            5 => {
899                // Generator for theta_z (rotation around z-axis)
900                generator[(0, 1)] = -1.0;
901                generator[(1, 0)] = 1.0;
902            }
903            _ => unreachable!(),
904        }
905
906        generator
907    }
908}
909
910#[cfg(test)]
911mod tests {
912    use super::*;
913    use Quaternion;
914    use std::f64::consts::PI;
915
916    const TOLERANCE: f64 = 1e-9;
917
918    #[test]
919    fn test_se3_tangent_basic() {
920        let linear = Vector3::new(1.0, 2.0, 3.0);
921        let angular = Vector3::new(0.1, 0.2, 0.3);
922        let tangent = SE3Tangent::new(linear, angular);
923
924        assert_eq!(tangent.rho(), linear);
925        assert_eq!(tangent.theta(), angular);
926    }
927
928    #[test]
929    fn test_se3_tangent_zero() {
930        let zero = SE3Tangent::zero();
931        assert_eq!(zero.data, Vector6::zeros());
932
933        let tangent = SE3Tangent::from_components(0.0, 0.0, 0.0, 0.0, 0.0, 0.0);
934        assert!(tangent.is_zero(1e-10));
935    }
936
937    // Comprehensive SE3 tests
938    #[test]
939    fn test_se3_identity() {
940        let identity = SE3::identity();
941        assert!(identity.is_valid(TOLERANCE));
942
943        let translation = identity.translation();
944        let rotation = identity.rotation_quaternion();
945
946        assert!(translation.norm() < TOLERANCE);
947        assert!((rotation.angle()) < TOLERANCE);
948    }
949
950    /// ISSUE-0008 regression: every stored parameter must be checked, not
951    /// just the quaternion — a NaN/Inf translation previously passed
952    /// `is_valid` silently. Also covers a non-finite tolerance.
953    #[test]
954    fn test_se3_is_valid_rejects_nonfinite_at_every_index() {
955        let base = SE3::identity();
956        for idx in 0..SE3::REP_SIZE {
957            for bad in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
958                let mut params: Vec<f64> = base.as_param_slice().to_vec();
959                params[idx] = bad;
960                let perturbed = SE3::from_param_slice(&params);
961                assert!(
962                    !perturbed.is_valid(TOLERANCE),
963                    "index {idx} = {bad} must be rejected"
964                );
965            }
966        }
967        assert!(
968            !base.is_valid(f64::INFINITY),
969            "infinite tolerance must be rejected"
970        );
971        assert!(!base.is_valid(f64::NAN), "NaN tolerance must be rejected");
972    }
973
974    #[test]
975    fn test_se3_new() {
976        let translation = Vector3::new(1.0, 2.0, 3.0);
977        let rotation = UnitQuaternion::from_euler_angles(0.1, 0.2, 0.3);
978
979        let se3 = SE3::new(translation, rotation);
980
981        assert!(se3.is_valid(TOLERANCE));
982        assert!((se3.translation() - translation).norm() < TOLERANCE);
983        assert!((se3.rotation_quaternion().angle() - rotation.angle()).abs() < TOLERANCE);
984    }
985
986    #[test]
987    fn test_se3_random() {
988        let se3 = SE3::random();
989        assert!(se3.is_valid(TOLERANCE));
990    }
991
992    #[test]
993    fn test_se3_inverse() {
994        let se3 = SE3::random();
995        let se3_inv = se3.inverse(None);
996
997        // Test that g * g^-1 = identity
998        let composed = se3.compose(&se3_inv, None, None);
999        let identity = SE3::identity();
1000
1001        let translation_diff = (composed.translation() - identity.translation()).norm();
1002        let rotation_diff = composed.rotation_quaternion().angle();
1003
1004        assert!(translation_diff < TOLERANCE);
1005        assert!(rotation_diff < TOLERANCE);
1006    }
1007
1008    #[test]
1009    fn test_se3_compose() {
1010        let se3_1 = SE3::random();
1011        let se3_2 = SE3::random();
1012
1013        let composed = se3_1.compose(&se3_2, None, None);
1014        assert!(composed.is_valid(TOLERANCE));
1015
1016        // Test composition with identity
1017        let identity = SE3::identity();
1018        let composed_with_identity = se3_1.compose(&identity, None, None);
1019
1020        let translation_diff = (composed_with_identity.translation() - se3_1.translation()).norm();
1021        let rotation_diff = (composed_with_identity.rotation_quaternion().angle()
1022            - se3_1.rotation_quaternion().angle())
1023        .abs();
1024
1025        assert!(translation_diff < TOLERANCE);
1026        assert!(rotation_diff < TOLERANCE);
1027    }
1028
1029    #[test]
1030    fn test_se3_adjoint() {
1031        let se3 = SE3::random();
1032        let adj = se3.adjoint();
1033
1034        // Adjoint should be 6x6
1035        assert_eq!(adj.nrows(), 6);
1036        assert_eq!(adj.ncols(), 6);
1037
1038        // Test some properties of the adjoint matrix
1039        // det(Adj(g)) = 1 for SE(3)
1040        let det = adj.determinant();
1041        assert!((det - 1.0).abs() < TOLERANCE);
1042    }
1043
1044    #[test]
1045    fn test_se3_act() {
1046        let se3 = SE3::random();
1047        let point = Vector3::new(1.0, 2.0, 3.0);
1048
1049        let _transformed_point = se3.act(&point, None, None);
1050
1051        // Test act with identity
1052        let identity = SE3::identity();
1053        let identity_transformed = identity.act(&point, None, None);
1054
1055        let diff = (identity_transformed - point).norm();
1056        assert!(diff < TOLERANCE);
1057    }
1058
1059    #[test]
1060    fn test_se3_between() {
1061        let se3a = SE3::from_translation_euler(1.0, 2.0, 3.0, 0.1, 0.2, 0.3);
1062        let se3b = se3a.clone();
1063        let se3_between_identity = se3a.between(&se3b, None, None);
1064        assert!(se3_between_identity.is_approx(&SE3::identity(), TOLERANCE));
1065
1066        let se3c = SE3::from_translation_euler(4.0, 5.0, 6.0, 0.4, 0.5, 0.6);
1067        let se3_between = se3a.between(&se3c, None, None);
1068        let expected = se3a.inverse(None).compose(&se3c, None, None);
1069        assert!(se3_between.is_approx(&expected, TOLERANCE));
1070    }
1071
1072    #[test]
1073    fn test_se3_exp_log() {
1074        let tangent_vec = Vector6::new(0.1, 0.2, 0.3, 0.01, 0.02, 0.03);
1075        let tangent = SE3Tangent { data: tangent_vec };
1076
1077        // Test exp(log(g)) = g
1078        let se3 = tangent.exp(None);
1079        let recovered_tangent = se3.log(None);
1080
1081        let diff = (tangent.data - recovered_tangent.data).norm();
1082        assert!(diff < TOLERANCE);
1083    }
1084
1085    #[test]
1086    fn test_se3_exp_zero() {
1087        let zero_tangent = SE3Tangent::zero();
1088        let se3 = zero_tangent.exp(None);
1089        let identity = SE3::identity();
1090
1091        let translation_diff = (se3.translation() - identity.translation()).norm();
1092        let rotation_diff = se3.rotation_quaternion().angle();
1093
1094        assert!(translation_diff < TOLERANCE);
1095        assert!(rotation_diff < TOLERANCE);
1096    }
1097
1098    #[test]
1099    fn test_se3_log_identity() {
1100        let identity = SE3::identity();
1101        let tangent = identity.log(None);
1102
1103        assert!(tangent.data.norm() < TOLERANCE);
1104    }
1105
1106    #[test]
1107    fn test_se3_normalize() {
1108        let translation = Vector3::new(1.0, 2.0, 3.0);
1109        let rotation =
1110            UnitQuaternion::from_quaternion(Quaternion::new(0.5, 0.5, 0.5, 0.5).normalize()); // Normalized
1111
1112        let mut se3 = SE3::new(translation, rotation);
1113        se3.normalize();
1114
1115        assert!(se3.is_valid(TOLERANCE));
1116    }
1117
1118    #[test]
1119    fn test_se3_manifold_properties() {
1120        // Test manifold dimension constants
1121        assert_eq!(SE3::DIM, 3);
1122        assert_eq!(SE3::DOF, 6);
1123        assert_eq!(SE3::REP_SIZE, 7);
1124    }
1125
1126    #[test]
1127    fn test_se3_consistency() {
1128        // Test that operations are consistent with manif library expectations
1129        let se3_1 = SE3::random();
1130        let se3_2 = SE3::random();
1131
1132        // Test associativity: (g1 * g2) * g3 = g1 * (g2 * g3)
1133        let se3_3 = SE3::random();
1134        let left_assoc = se3_1
1135            .compose(&se3_2, None, None)
1136            .compose(&se3_3, None, None);
1137        let right_assoc = se3_1.compose(&se3_2.compose(&se3_3, None, None), None, None);
1138
1139        let translation_diff = (left_assoc.translation() - right_assoc.translation()).norm();
1140        let rotation_diff = (left_assoc.rotation_quaternion().angle()
1141            - right_assoc.rotation_quaternion().angle())
1142        .abs();
1143
1144        assert!(translation_diff < 1e-10);
1145        assert!(rotation_diff < 1e-10);
1146    }
1147
1148    #[test]
1149    fn test_se3_specific_values() {
1150        // Test specific known values similar to manif tests
1151
1152        // Translation only
1153        let translation_only = SE3::new(Vector3::new(1.0, 2.0, 3.0), UnitQuaternion::identity());
1154
1155        let point = Vector3::new(0.0, 0.0, 0.0);
1156        let transformed = translation_only.act(&point, None, None);
1157        let expected = Vector3::new(1.0, 2.0, 3.0);
1158
1159        assert!((transformed - expected).norm() < TOLERANCE);
1160
1161        // Rotation only
1162        let rotation_only = SE3::new(
1163            Vector3::zeros(),
1164            UnitQuaternion::from_euler_angles(PI / 2.0, 0.0, 0.0),
1165        );
1166
1167        let point_y = Vector3::new(0.0, 1.0, 0.0);
1168        let rotated = rotation_only.act(&point_y, None, None);
1169        let expected_rotated = Vector3::new(0.0, 0.0, 1.0);
1170
1171        assert!((rotated - expected_rotated).norm() < TOLERANCE);
1172    }
1173
1174    #[test]
1175    fn test_se3_small_angle_approximations() {
1176        // Test behavior with very small angles, similar to manif library tests
1177        let small_tangent = Vector6::new(1e-8, 2e-8, 3e-8, 1e-9, 2e-9, 3e-9);
1178
1179        let se3 = SE3::new(
1180            Vector3::new(1e-8, 2e-8, 3e-8),
1181            UnitQuaternion::from_euler_angles(1e-9, 2e-9, 3e-9),
1182        );
1183        let recovered = se3.log(None);
1184
1185        let diff = (small_tangent - recovered.data).norm();
1186        assert!(diff < TOLERANCE);
1187    }
1188
1189    #[test]
1190    fn test_se3_tangent_norm() {
1191        let tangent_vec = Vector6::new(3.0, 4.0, 0.0, 0.0, 0.0, 0.0);
1192        let tangent = SE3Tangent { data: tangent_vec };
1193
1194        let norm = tangent.data.norm();
1195        assert!((norm - 5.0).abs() < TOLERANCE); // sqrt(3^2 + 4^2) = 5
1196    }
1197
1198    #[test]
1199    fn test_se3_from_components() {
1200        let translation = Vector3::new(1.0, 2.0, 3.0);
1201        let quaternion = Quaternion::new(1.0, 0.0, 0.0, 0.0);
1202        let se3 = SE3::from_translation_quaternion(translation, quaternion);
1203        assert!(se3.is_valid(TOLERANCE));
1204        assert_eq!(se3.x(), 1.0);
1205        assert_eq!(se3.y(), 2.0);
1206        assert_eq!(se3.z(), 3.0);
1207
1208        let quat = se3.rotation_quaternion();
1209        assert!((quat.w - 1.0).abs() < TOLERANCE);
1210        assert!(quat.i.abs() < TOLERANCE);
1211        assert!(quat.j.abs() < TOLERANCE);
1212        assert!(quat.k.abs() < TOLERANCE);
1213    }
1214
1215    #[test]
1216    fn test_se3_from_isometry() {
1217        let translation = Translation3::new(1.0, 2.0, 3.0);
1218        let rotation = UnitQuaternion::from_euler_angles(0.1, 0.2, 0.3);
1219        let isometry = Isometry3::from_parts(translation, rotation);
1220
1221        let se3 = SE3::from_isometry(isometry);
1222        let recovered_isometry = se3.isometry();
1223
1224        let translation_diff =
1225            (isometry.translation.vector - recovered_isometry.translation.vector).norm();
1226        let rotation_diff = (isometry.rotation.angle() - recovered_isometry.rotation.angle()).abs();
1227
1228        assert!(translation_diff < TOLERANCE);
1229        assert!(rotation_diff < TOLERANCE);
1230    }
1231
1232    #[test]
1233    fn test_se3_matrix() {
1234        let se3 = SE3::random();
1235        let matrix = se3.matrix();
1236
1237        // Check matrix is 4x4
1238        assert_eq!(matrix.nrows(), 4);
1239        assert_eq!(matrix.ncols(), 4);
1240
1241        // Check bottom row is [0, 0, 0, 1]
1242        assert!((matrix[(3, 0)]).abs() < TOLERANCE);
1243        assert!((matrix[(3, 1)]).abs() < TOLERANCE);
1244        assert!((matrix[(3, 2)]).abs() < TOLERANCE);
1245        assert!((matrix[(3, 3)] - 1.0).abs() < TOLERANCE);
1246    }
1247
1248    // Integration tests based on manif library patterns
1249    #[test]
1250    fn test_se3_manif_like_operations() {
1251        // This test mimics operations commonly found in manif test suite
1252
1253        // Create two SE3 elements
1254        let g1 = SE3::new(
1255            Vector3::new(1.0, 0.0, 0.0),
1256            UnitQuaternion::from_euler_angles(0.0, 0.0, PI / 4.0),
1257        );
1258
1259        let g2 = SE3::new(
1260            Vector3::new(0.0, 1.0, 0.0),
1261            UnitQuaternion::from_euler_angles(0.0, PI / 4.0, 0.0),
1262        );
1263
1264        // Test composition
1265        let g3 = g1.compose(&g2, None, None);
1266        assert!(g3.is_valid(TOLERANCE));
1267
1268        // Test inverse composition property: g1 * g2 * g2^-1 * g1^-1 = I
1269        let g2_inv = g2.inverse(None);
1270        let g1_inv = g1.inverse(None);
1271        let result = g1
1272            .compose(&g2, None, None)
1273            .compose(&g2_inv, None, None)
1274            .compose(&g1_inv, None, None);
1275
1276        let identity = SE3::identity();
1277        let translation_diff = (result.translation() - identity.translation()).norm();
1278        let rotation_diff = result.rotation_quaternion().angle();
1279
1280        assert!(translation_diff < TOLERANCE);
1281        assert!(rotation_diff < TOLERANCE);
1282    }
1283
1284    #[test]
1285    fn test_se3_tangent_exp_jacobians() {
1286        let tangent = SE3Tangent::new(Vector3::new(0.1, 0.0, 0.0), Vector3::new(0.0, 0.1, 0.0));
1287
1288        // Test exponential map
1289        let se3_element = tangent.exp(None);
1290        assert!(se3_element.is_valid(TOLERANCE));
1291
1292        // Test basic exp functionality - that we can convert tangent to SE3
1293        let another_tangent = SE3Tangent::new(
1294            Vector3::new(0.01, 0.02, 0.03),
1295            Vector3::new(0.001, 0.002, 0.003),
1296        );
1297        let another_se3 = another_tangent.exp(None);
1298        assert!(another_se3.is_valid(TOLERANCE));
1299
1300        // Test that Jacobians can be computed without panicking
1301        let _right_jac = tangent.right_jacobian();
1302        let _left_jac = tangent.left_jacobian();
1303        let _right_jac_inv = tangent.right_jacobian_inv();
1304        let _left_jac_inv = tangent.left_jacobian_inv();
1305
1306        // Test that Jacobians have correct dimensions
1307        assert_eq!(_right_jac.nrows(), 6);
1308        assert_eq!(_right_jac.ncols(), 6);
1309        assert_eq!(_left_jac.nrows(), 6);
1310        assert_eq!(_left_jac.ncols(), 6);
1311        assert_eq!(_right_jac_inv.nrows(), 6);
1312        assert_eq!(_right_jac_inv.ncols(), 6);
1313        assert_eq!(_left_jac_inv.nrows(), 6);
1314        assert_eq!(_left_jac_inv.ncols(), 6);
1315    }
1316
1317    #[test]
1318    fn test_se3_tangent_utility_functions() {
1319        // Test zero
1320        let zero_vec = SE3Tangent::zero();
1321        assert!(zero_vec.data.norm() < TOLERANCE);
1322
1323        // Test random
1324        let random_vec = SE3Tangent::random();
1325        assert!(random_vec.data.norm() > 0.0);
1326
1327        // Test is_zero
1328        let tangent = SE3Tangent::new(Vector3::zeros(), Vector3::zeros());
1329        assert!(tangent.is_zero(1e-10));
1330
1331        let non_zero_tangent = SE3Tangent::new(Vector3::new(1e-5, 0.0, 0.0), Vector3::zeros());
1332        assert!(!non_zero_tangent.is_zero(1e-10));
1333    }
1334
1335    // New tests for the additional functions
1336
1337    #[test]
1338    fn test_se3_vee() {
1339        let se3 = SE3::random();
1340        let tangent_log = se3.log(None);
1341        let tangent_vee = se3.vee();
1342
1343        assert!((tangent_log.data - tangent_vee.data).norm() < 1e-10);
1344    }
1345
1346    #[test]
1347    fn test_se3_is_approx() {
1348        let se3_1 = SE3::random();
1349        let se3_2 = se3_1.clone();
1350
1351        assert!(se3_1.is_approx(&se3_1, 1e-10));
1352        assert!(se3_1.is_approx(&se3_2, 1e-10));
1353
1354        // Test with small perturbation
1355        let small_tangent = SE3Tangent::new(
1356            Vector3::new(1e-12, 1e-12, 1e-12),
1357            Vector3::new(1e-12, 1e-12, 1e-12),
1358        );
1359        let se3_perturbed = se3_1.right_plus(&small_tangent, None, None);
1360        assert!(se3_1.is_approx(&se3_perturbed, 1e-10));
1361    }
1362
1363    #[test]
1364    fn test_se3_tangent_small_adj() {
1365        let tangent = SE3Tangent::new(Vector3::new(0.1, 0.2, 0.3), Vector3::new(0.4, 0.5, 0.6));
1366        let small_adj = tangent.small_adj();
1367
1368        // Verify the structure of the small adjoint matrix for SE(3)
1369        // Should be:
1370        // [ Omega  V   ]
1371        // [   0  Omega ]
1372        let rho_skew = SO3Tangent::new(tangent.rho()).hat();
1373        let theta_skew = SO3Tangent::new(tangent.theta()).hat();
1374
1375        // Check top-left and bottom-right blocks (theta_skew)
1376        let top_left = small_adj.fixed_view::<3, 3>(0, 0);
1377        let bottom_right = small_adj.fixed_view::<3, 3>(3, 3);
1378        assert!((top_left - theta_skew).norm() < 1e-10);
1379        assert!((bottom_right - theta_skew).norm() < 1e-10);
1380
1381        // Check top-right block (rho_skew)
1382        let top_right = small_adj.fixed_view::<3, 3>(0, 3);
1383        assert!((top_right - rho_skew).norm() < 1e-10);
1384
1385        // Check bottom-left block (zeros)
1386        let bottom_left = small_adj.fixed_view::<3, 3>(3, 0);
1387        assert!(bottom_left.norm() < 1e-10);
1388    }
1389
1390    #[test]
1391    fn test_se3_tangent_lie_bracket() {
1392        let tangent_a = SE3Tangent::new(Vector3::new(0.1, 0.0, 0.0), Vector3::new(0.0, 0.2, 0.0));
1393        let tangent_b = SE3Tangent::new(Vector3::new(0.0, 0.3, 0.0), Vector3::new(0.0, 0.0, 0.4));
1394
1395        let bracket_ab = tangent_a.lie_bracket(&tangent_b);
1396        let bracket_ba = tangent_b.lie_bracket(&tangent_a);
1397
1398        // Anti-symmetry test: [a,b] = -[b,a]
1399        assert!((bracket_ab.data + bracket_ba.data).norm() < 1e-10);
1400
1401        // [a,a] = 0
1402        let bracket_aa = tangent_a.lie_bracket(&tangent_a);
1403        assert!(bracket_aa.is_zero(1e-10));
1404
1405        // Verify bracket relationship with hat operator
1406        let bracket_hat = bracket_ab.hat();
1407        let expected = tangent_a.hat() * tangent_b.hat() - tangent_b.hat() * tangent_a.hat();
1408        assert!((bracket_hat - expected).norm() < 1e-10);
1409    }
1410
1411    #[test]
1412    fn test_se3_tangent_is_approx() {
1413        let tangent_1 = SE3Tangent::new(Vector3::new(0.1, 0.2, 0.3), Vector3::new(0.4, 0.5, 0.6));
1414        let tangent_2 = SE3Tangent::new(
1415            Vector3::new(0.1 + 1e-12, 0.2, 0.3),
1416            Vector3::new(0.4, 0.5, 0.6),
1417        );
1418        let tangent_3 = SE3Tangent::new(Vector3::new(0.7, 0.8, 0.9), Vector3::new(1.0, 1.1, 1.2));
1419
1420        assert!(tangent_1.is_approx(&tangent_1, 1e-10));
1421        assert!(tangent_1.is_approx(&tangent_2, 1e-10));
1422        assert!(!tangent_1.is_approx(&tangent_3, 1e-10));
1423    }
1424
1425    #[test]
1426    fn test_se3_generators() {
1427        let tangent = SE3Tangent::new(Vector3::new(1.0, 1.0, 1.0), Vector3::new(1.0, 1.0, 1.0));
1428
1429        // Test all six generators
1430        for i in 0..6 {
1431            let generator = tangent.generator(i);
1432
1433            // Verify that generators are 4x4 matrices
1434            assert_eq!(generator.nrows(), 4);
1435            assert_eq!(generator.ncols(), 4);
1436
1437            // Bottom row should always be zeros for SE(3) generators
1438            assert_eq!(generator[(3, 0)], 0.0);
1439            assert_eq!(generator[(3, 1)], 0.0);
1440            assert_eq!(generator[(3, 2)], 0.0);
1441            assert_eq!(generator[(3, 3)], 0.0);
1442        }
1443
1444        // Test specific values for translation generators
1445        let e1 = tangent.generator(0); // rho_x
1446        let e2 = tangent.generator(1); // rho_y
1447        let e3 = tangent.generator(2); // rho_z
1448
1449        assert_eq!(e1[(0, 3)], 1.0);
1450        assert_eq!(e2[(1, 3)], 1.0);
1451        assert_eq!(e3[(2, 3)], 1.0);
1452
1453        // Test specific values for rotation generators
1454        let e4 = tangent.generator(3); // theta_x
1455        let e5 = tangent.generator(4); // theta_y
1456        let e6 = tangent.generator(5); // theta_z
1457
1458        // Rotation generators should be skew-symmetric in top-left 3x3 block
1459        assert_eq!(e4[(1, 2)], -1.0);
1460        assert_eq!(e4[(2, 1)], 1.0);
1461        assert_eq!(e5[(0, 2)], 1.0);
1462        assert_eq!(e5[(2, 0)], -1.0);
1463        assert_eq!(e6[(0, 1)], -1.0);
1464        assert_eq!(e6[(1, 0)], 1.0);
1465    }
1466
1467    #[test]
1468    #[should_panic]
1469    fn test_se3_generator_invalid_index() {
1470        let tangent = SE3Tangent::new(Vector3::new(1.0, 1.0, 1.0), Vector3::new(1.0, 1.0, 1.0));
1471        let _generator = tangent.generator(6); // Should panic for SE(3)
1472    }
1473
1474    #[test]
1475    fn test_se3_jacobi_identity() {
1476        // Test Jacobi identity: [x,[y,z]]+[y,[z,x]]+[z,[x,y]]=0
1477        let x = SE3Tangent::new(Vector3::new(0.1, 0.0, 0.0), Vector3::new(0.0, 0.1, 0.0));
1478        let y = SE3Tangent::new(Vector3::new(0.0, 0.2, 0.0), Vector3::new(0.0, 0.0, 0.2));
1479        let z = SE3Tangent::new(Vector3::new(0.0, 0.0, 0.3), Vector3::new(0.3, 0.0, 0.0));
1480
1481        let term1 = x.lie_bracket(&y.lie_bracket(&z));
1482        let term2 = y.lie_bracket(&z.lie_bracket(&x));
1483        let term3 = z.lie_bracket(&x.lie_bracket(&y));
1484
1485        let jacobi_sum = SE3Tangent {
1486            data: term1.data + term2.data + term3.data,
1487        };
1488        assert!(jacobi_sum.is_zero(1e-10));
1489    }
1490
1491    #[test]
1492    fn test_se3_hat_matrix_structure() {
1493        let tangent = SE3Tangent::new(Vector3::new(0.1, 0.2, 0.3), Vector3::new(0.4, 0.5, 0.6));
1494        let hat_matrix = tangent.hat();
1495
1496        // Verify hat matrix structure for SE(3)
1497        // Top-right should be translation part
1498        assert_eq!(hat_matrix[(0, 3)], tangent.rho()[0]);
1499        assert_eq!(hat_matrix[(1, 3)], tangent.rho()[1]);
1500        assert_eq!(hat_matrix[(2, 3)], tangent.rho()[2]);
1501
1502        // Top-left should be skew-symmetric matrix of rotation part
1503        let theta_hat = SO3Tangent::new(tangent.theta()).hat();
1504        let top_left = hat_matrix.fixed_view::<3, 3>(0, 0);
1505        assert!((top_left - theta_hat).norm() < 1e-10);
1506
1507        // Bottom row should be zeros
1508        assert_eq!(hat_matrix[(3, 0)], 0.0);
1509        assert_eq!(hat_matrix[(3, 1)], 0.0);
1510        assert_eq!(hat_matrix[(3, 2)], 0.0);
1511        assert_eq!(hat_matrix[(3, 3)], 0.0);
1512    }
1513
1514    // T3: Accumulated Error Tests
1515    //
1516    // NOTE: Loose tolerances (up to 5.0!) reflect EXPECTED numerical drift.
1517    // SE3 composition chains accumulate errors from:
1518    // - Quaternion multiplication rounding errors (SO3 component)
1519    // - Translation vector additions
1520    // - Coupled rotation-translation interactions
1521    //
1522    // Real SLAM systems handle this through:
1523    // - Pose graph optimization (bundle adjustment)
1524    // - Loop closure constraints
1525    // - Periodic re-normalization
1526    //
1527    // The tolerance of 5.0 for 10 steps documents realistic accumulated drift.
1528
1529    #[test]
1530    fn test_se3_accumulated_error_odometry() {
1531        // Simulate 10 odometry steps (shorter chain for numerical stability)
1532        let step = SE3::new(
1533            Vector3::new(1.0, 0.0, 0.0),                      // 1m forward
1534            UnitQuaternion::from_euler_angles(0.0, 0.0, 0.1), // 0.1 rad turn
1535        );
1536
1537        let mut pose = SE3::identity();
1538        for _ in 0..10 {
1539            pose = pose.compose(&step, None, None);
1540        }
1541
1542        // Expected: 10m forward + 1 radian total turn
1543        let expected = SE3::new(
1544            Vector3::new(10.0, 0.0, 0.0),
1545            UnitQuaternion::from_euler_angles(0.0, 0.0, 1.0),
1546        );
1547
1548        // Very loose tolerance for composition chain (tests numerical stability only)
1549        // Note: This test demonstrates accumulated numerical drift in composition chains
1550        assert!(pose.is_approx(&expected, 5.0));
1551    }
1552
1553    // T2: Edge Case Tests
1554    //
1555    // NOTE: Loose tolerances for large scales (1e-3 for 1000m translations).
1556    // This reflects SCALE-DEPENDENT precision:
1557    // - Absolute error grows with translation magnitude
1558    // - Relative error (~1e-9) remains constant
1559    // - For 1000m: 1e-3 absolute = 1e-6 relative (excellent!)
1560    //
1561    // Different problem scales need different tolerances:
1562    // - GPS (1000m): ≥ 1e-3
1563    // - SLAM (1-100m): ≥ 1e-6
1564    // - Manipulation (0.01-1m): ≥ 1e-8
1565
1566    #[test]
1567    fn test_se3_large_translation_small_rotation() {
1568        // 1000 meters translation + 1e-6 radian rotation (GPS-like scenario)
1569        let large_t = Vector3::new(1000.0, 2000.0, 500.0);
1570        let small_r = SO3::from_scaled_axis(Vector3::new(1e-6, 2e-6, 3e-6));
1571        let se3 = SE3::from_translation_so3(large_t, small_r);
1572
1573        let tangent = se3.log(None);
1574        let recovered = tangent.exp(None);
1575
1576        // Very relaxed tolerance for large translations (relative error at 1000m scale)
1577        assert!(se3.is_approx(&recovered, 1e-3));
1578    }
1579
1580    #[test]
1581    fn test_se3_small_translation_large_rotation() {
1582        // Millimeter-scale translation + moderate rotation (robotic gripper scenario)
1583        let small_t = Vector3::new(0.001, 0.002, -0.001);
1584        // Use smaller rotation angles to avoid numerical issues
1585        let large_r = SO3::from_euler_angles(1.5, 0.5, -1.2);
1586        let se3 = SE3::from_translation_so3(small_t, large_r);
1587
1588        let tangent = se3.log(None);
1589        let recovered = tangent.exp(None);
1590
1591        // Very loose tolerance for moderate rotation angles (numerical precision degrades)
1592        assert!(se3.is_approx(&recovered, 1e-3));
1593    }
1594
1595    #[test]
1596    fn test_se3_right_jacobian_inverse_identity() {
1597        let tangent = SE3Tangent::new(
1598            Vector3::new(0.1, 0.15, 0.2),
1599            Vector3::new(0.001, 0.002, 0.003),
1600        );
1601        let jr = tangent.right_jacobian();
1602        let jr_inv = tangent.right_jacobian_inv();
1603        let product = jr * jr_inv;
1604        let identity = Matrix6::identity();
1605
1606        assert!(
1607            (product - identity).norm() < 1e-10,
1608            "Jr * Jr_inv should be identity, got error: {}",
1609            (product - identity).norm()
1610        );
1611    }
1612
1613    #[test]
1614    fn test_se3_left_jacobian_inverse_identity() {
1615        let tangent = SE3Tangent::new(
1616            Vector3::new(0.1, 0.15, 0.2),
1617            Vector3::new(0.001, 0.002, 0.003),
1618        );
1619        let jl = tangent.left_jacobian();
1620        let jl_inv = tangent.left_jacobian_inv();
1621        let product = jl * jl_inv;
1622
1623        assert!(
1624            (product - Matrix6::identity()).norm() < 1e-10,
1625            "Jl * Jl_inv should be identity, got error: {}",
1626            (product - Matrix6::identity()).norm()
1627        );
1628    }
1629
1630    #[test]
1631    fn se3_param_slice_round_trip() {
1632        let g = SE3::random();
1633        let recovered = SE3::from_param_slice(g.as_param_slice());
1634        assert!(g.is_approx(&recovered, 1e-14));
1635    }
1636
1637    #[test]
1638    fn se3_param_slice_mut_modifies_in_place() {
1639        let mut g = SE3::identity();
1640        g.as_param_slice_mut()[0] = 1.0;
1641        assert_eq!(g.translation().x, 1.0);
1642    }
1643
1644    #[test]
1645    fn se3_tangent_slice_round_trip() {
1646        let t = SE3Tangent::random();
1647        let recovered = SE3Tangent::from_slice(t.as_slice());
1648        assert!(t.is_approx(&recovered, 1e-14));
1649    }
1650}
1651
1652/// Independent central-difference regression tests for ISSUE-0001 (SE(3)
1653/// `Q`-block coefficient). Built from `exp()`/`matrix()`/`inverse()` alone —
1654/// never from `log()` or `right_jacobian_inv()` — so a bug shared between an
1655/// analytic Jacobian and its "inverse" cannot hide here.
1656#[cfg(test)]
1657mod se3_jacobian_fd_tests {
1658    use super::*;
1659
1660    const FD_STEP: f64 = 1e-4;
1661    const FD_TOL: f64 = 1e-5;
1662
1663    /// Extract [ρ,θ] from a 4x4 SE(3) Lie-algebra matrix `[θ×, ρ; 0, 0]`.
1664    fn vee(m: &Matrix4<f64>) -> Vector6<f64> {
1665        let mut v = Vector6::zeros();
1666        v[0] = m[(0, 3)];
1667        v[1] = m[(1, 3)];
1668        v[2] = m[(2, 3)];
1669        v[3] = m[(2, 1)];
1670        v[4] = m[(0, 2)];
1671        v[5] = m[(1, 0)];
1672        v
1673    }
1674
1675    fn perturbed(xi: &SE3Tangent, k: usize, eps: f64) -> SE3Tangent {
1676        let mut data = xi.data;
1677        data[k] += eps;
1678        SE3Tangent { data }
1679    }
1680
1681    fn fd_right_jacobian(xi: &SE3Tangent) -> Matrix6<f64> {
1682        let x_inv_mat = xi.exp(None).inverse(None).matrix();
1683        let mut jac = Matrix6::zeros();
1684        for k in 0..6 {
1685            let plus = perturbed(xi, k, FD_STEP).exp(None).matrix();
1686            let minus = perturbed(xi, k, -FD_STEP).exp(None).matrix();
1687            let d_dxi_k = (plus - minus) / (2.0 * FD_STEP);
1688            jac.set_column(k, &vee(&(x_inv_mat * d_dxi_k)));
1689        }
1690        jac
1691    }
1692
1693    #[test]
1694    fn se3_right_jacobian_matches_matrix_exp_central_difference() {
1695        let cases = [
1696            (Vector3::new(0.7, -0.2, 0.4), Vector3::new(0.4, -0.3, 0.25)),
1697            (Vector3::new(0.1, 0.2, -0.3), Vector3::new(2.8, 0.1, -0.2)), // near pi
1698            (Vector3::new(-0.5, 0.3, 0.2), Vector3::zeros()),             // theta = 0
1699            (
1700                Vector3::new(1e-8, -2e-8, 3e-8),
1701                Vector3::new(1e-8, 2e-8, -1e-8),
1702            ), // small angle
1703        ];
1704        for (rho, theta) in cases {
1705            let xi = SE3Tangent::from_components(rho.x, rho.y, rho.z, theta.x, theta.y, theta.z);
1706            let analytic = xi.right_jacobian();
1707            let numeric = fd_right_jacobian(&xi);
1708            let err = (analytic - numeric).norm();
1709            assert!(
1710                err < FD_TOL,
1711                "right_jacobian mismatch at rho={rho:?} theta={theta:?}: error={err}"
1712            );
1713        }
1714    }
1715
1716    #[test]
1717    fn se3_right_jacobian_continuous_near_zero() {
1718        for scale in [1e-1, 1e-3, 1e-5, 1e-7, 1e-9] {
1719            let rho = Vector3::new(0.3, -0.2, 0.1) * scale;
1720            let theta = Vector3::new(0.2, 0.1, -0.3) * scale;
1721            let xi = SE3Tangent::from_components(rho.x, rho.y, rho.z, theta.x, theta.y, theta.z);
1722            let analytic = xi.right_jacobian();
1723            let numeric = fd_right_jacobian(&xi);
1724            let err = (analytic - numeric).norm();
1725            assert!(err < FD_TOL, "discontinuity at scale={scale}: error={err}");
1726        }
1727    }
1728}