Skip to main content

apex_manifolds/
se2.rs

1//! SE(2) - Special Euclidean Group in 2D
2//!
3//! This module implements the Special Euclidean group SE(2), which represents
4//! rigid body transformations in 2D space (rotation + translation).
5//!
6//! SE(2) elements are represented as a combination of 2D rotation and Vector2 translation.
7//! SE(2) tangent elements are represented as [x, y, theta] = 3 components,
8//! where x,y 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
13use crate::{LieGroup, Tangent, so2::SO2};
14use nalgebra::{
15    Complex, DVector, Isometry2, Matrix2, Matrix3, Point2, Translation2, UnitComplex, Vector2,
16    Vector3,
17};
18use std::{
19    fmt,
20    fmt::{Display, Formatter},
21};
22
23/// SE(2) group element representing rigid body transformations in 2D.
24///
25/// Represented as a combination of 2D rotation and Vector2 translation.
26#[derive(Clone, PartialEq)]
27pub struct SE2 {
28    /// Translation part as Vector2
29    translation: Vector2<f64>,
30    /// Rotation part as UnitComplex
31    rotation: UnitComplex<f64>,
32}
33
34impl Display for SE2 {
35    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
36        let t = self.translation();
37        write!(
38            f,
39            "SE2(translation: [{:.4}, {:.4}], rotation: {:.4})",
40            t.x,
41            t.y,
42            self.angle()
43        )
44    }
45}
46
47// Conversion traits for integration with generic Problem
48impl From<DVector<f64>> for SE2 {
49    fn from(data: DVector<f64>) -> Self {
50        // Input order is [x, y, theta] to match G2O format
51        SE2::from_xy_angle(data[0], data[1], data[2])
52    }
53}
54
55impl From<SE2> for DVector<f64> {
56    fn from(se2: SE2) -> Self {
57        DVector::from_vec(vec![
58            se2.translation.x,    // x first
59            se2.translation.y,    // y second
60            se2.rotation.angle(), // theta third
61        ])
62    }
63}
64
65impl SE2 {
66    /// Space dimension - dimension of the ambient space that the group acts on
67    pub const DIM: usize = 2;
68
69    /// Degrees of freedom - dimension of the tangent space
70    pub const DOF: usize = 3;
71
72    /// Representation size - size of the underlying data representation
73    pub const REP_SIZE: usize = 4;
74
75    /// Get the identity element of the group.
76    ///
77    /// Returns the neutral element e such that e ∘ g = g ∘ e = g for any group element g.
78    pub fn identity() -> Self {
79        SE2 {
80            translation: Vector2::zeros(),
81            rotation: UnitComplex::identity(),
82        }
83    }
84
85    /// Get the identity matrix for Jacobians.
86    ///
87    /// Returns the identity matrix in the appropriate dimension for Jacobian computations.
88    pub fn jacobian_identity() -> Matrix3<f64> {
89        Matrix3::<f64>::identity()
90    }
91
92    /// Create a new SE2 element from translation and rotation.
93    ///
94    /// # Arguments
95    /// * `translation` - Translation vector [x, y]
96    /// * `rotation` - Unit complex number representing rotation
97    #[inline]
98    pub fn new(translation: Vector2<f64>, rotation: UnitComplex<f64>) -> Self {
99        SE2 {
100            translation,
101            rotation,
102        }
103    }
104
105    /// Create SE2 from translation components and angle.
106    pub fn from_xy_angle(x: f64, y: f64, theta: f64) -> Self {
107        let translation = Vector2::new(x, y);
108        let rotation = UnitComplex::from_angle(theta);
109        Self::new(translation, rotation)
110    }
111
112    /// Create SE2 from translation components and complex rotation.
113    pub fn from_xy_complex(x: f64, y: f64, real: f64, imag: f64) -> Self {
114        let translation = Vector2::new(x, y);
115        let complex = Complex::new(real, imag);
116        let rotation = UnitComplex::from_complex(complex);
117        Self::new(translation, rotation)
118    }
119
120    /// Create SE2 directly from an Isometry2.
121    pub fn from_isometry(isometry: Isometry2<f64>) -> Self {
122        SE2 {
123            translation: isometry.translation.vector,
124            rotation: isometry.rotation,
125        }
126    }
127
128    /// Create SE2 from Vector2 and SO2 components.
129    pub fn from_translation_so2(translation: Vector2<f64>, rotation: SO2) -> Self {
130        SE2 {
131            translation,
132            rotation: rotation.complex(),
133        }
134    }
135
136    /// Get the translation part as a Vector2.
137    pub fn translation(&self) -> Vector2<f64> {
138        self.translation
139    }
140
141    /// Get the rotation part as UnitComplex.
142    pub fn rotation_complex(&self) -> UnitComplex<f64> {
143        self.rotation
144    }
145
146    /// Get the rotation angle.
147    pub fn rotation_angle(&self) -> f64 {
148        self.rotation.angle()
149    }
150
151    /// Get the rotation part as SO2.
152    pub fn rotation_so2(&self) -> SO2 {
153        SO2::new(self.rotation)
154    }
155
156    /// Get as an Isometry2 (convenience method).
157    pub fn isometry(&self) -> Isometry2<f64> {
158        Isometry2::from_parts(Translation2::from(self.translation), self.rotation)
159    }
160
161    /// Get the transformation matrix (3x3 homogeneous matrix).
162    pub fn matrix(&self) -> Matrix3<f64> {
163        self.isometry().to_homogeneous()
164    }
165
166    /// Get the rotation matrix (2x2).
167    pub fn rotation_matrix(&self) -> Matrix2<f64> {
168        self.rotation.to_rotation_matrix().into_inner()
169    }
170
171    /// Get the x component of translation.
172    #[inline]
173    pub fn x(&self) -> f64 {
174        self.translation.x
175    }
176
177    /// Get the y component of translation.
178    #[inline]
179    pub fn y(&self) -> f64 {
180        self.translation.y
181    }
182
183    /// Get the real part of the complex rotation.
184    pub fn real(&self) -> f64 {
185        self.rotation.re
186    }
187
188    /// Get the imaginary part of the complex rotation.
189    pub fn imag(&self) -> f64 {
190        self.rotation.im
191    }
192
193    /// Get the rotation angle in radians.
194    #[inline]
195    pub fn angle(&self) -> f64 {
196        self.rotation.angle()
197    }
198}
199
200// Implement basic trait requirements for LieGroup
201impl LieGroup for SE2 {
202    type TangentVector = SE2Tangent;
203    type JacobianMatrix = Matrix3<f64>;
204    type LieAlgebra = Matrix3<f64>;
205
206    /// Get the inverse.
207    ///
208    /// # Arguments
209    /// * `jacobian` - Optional Jacobian matrix of the inverse wrt this.
210    ///
211    /// # Notes
212    /// For SE(2): g^{-1} = [R^T, -R^T * t; 0, 1]
213    fn inverse(&self, jacobian: Option<&mut Self::JacobianMatrix>) -> Self {
214        let rot_inv = self.rotation.inverse();
215        let trans_inv = -(rot_inv * self.translation);
216
217        if let Some(jac) = jacobian {
218            *jac = -self.adjoint();
219        }
220
221        SE2::new(trans_inv, rot_inv)
222    }
223
224    /// Composition of this and another SE2 element.
225    fn compose(
226        &self,
227        other: &Self,
228        jacobian_self: Option<&mut Self::JacobianMatrix>,
229        jacobian_other: Option<&mut Self::JacobianMatrix>,
230    ) -> Self {
231        let composed_rotation = self.rotation * other.rotation;
232        let composed_translation = self
233            .rotation
234            .transform_point(&Point2::from(other.translation))
235            .coords
236            + self.translation;
237
238        let result = SE2::new(composed_translation, composed_rotation);
239
240        if let Some(jac_self) = jacobian_self {
241            *jac_self = other.inverse(None).adjoint();
242        }
243
244        if let Some(jac_other) = jacobian_other {
245            *jac_other = Matrix3::identity();
246        }
247
248        result
249    }
250
251    /// Get the SE2 corresponding Lie algebra element in vector form.
252    fn log(&self, jacobian: Option<&mut Self::JacobianMatrix>) -> Self::TangentVector {
253        let theta = self.angle();
254        let cos_theta = theta.cos();
255        let sin_theta = theta.sin();
256        let theta_sq = theta * theta;
257
258        let (a, b) = if theta_sq < crate::SMALL_ANGLE_THRESHOLD {
259            // Taylor approximation
260            let a = 1.0 - theta_sq / 6.0;
261            let b = 0.5 * theta - theta * theta_sq / 24.0;
262            (a, b)
263        } else {
264            // Euler
265            let a = sin_theta / theta;
266            let b = (1.0 - cos_theta) / theta;
267            (a, b)
268        };
269
270        let den = 1.0 / (a * a + b * b);
271        let a_scaled = a * den;
272        let b_scaled = b * den;
273
274        let x = a_scaled * self.x() + b_scaled * self.y();
275        let y = -b_scaled * self.x() + a_scaled * self.y();
276
277        let result = SE2Tangent::new(x, y, theta);
278
279        if let Some(jac) = jacobian {
280            *jac = result.right_jacobian_inv();
281        }
282
283        result
284    }
285
286    fn act(
287        &self,
288        vector: &Vector3<f64>,
289        jacobian_self: Option<&mut Self::JacobianMatrix>,
290        jacobian_vector: Option<&mut Matrix3<f64>>,
291    ) -> Vector3<f64> {
292        // For SE(2), we operate on 2D vectors but maintain 3D interface compatibility
293        let point2d = Vector2::new(vector.x, vector.y);
294        let transformed_2d =
295            self.rotation.transform_point(&Point2::from(point2d)).coords + self.translation;
296        let result = Vector3::new(transformed_2d.x, transformed_2d.y, vector.z);
297
298        if let Some(jac_self) = jacobian_self {
299            let r = self.rotation_matrix();
300            jac_self.fixed_view_mut::<2, 2>(0, 0).copy_from(&r);
301            jac_self[(0, 2)] = -point2d.y;
302            jac_self[(1, 2)] = point2d.x;
303            jac_self[(2, 0)] = 0.0;
304            jac_self[(2, 1)] = 0.0;
305            jac_self[(2, 2)] = 1.0;
306        }
307
308        if let Some(jac_vector) = jacobian_vector {
309            *jac_vector = Matrix3::identity();
310            let r = self.rotation_matrix();
311            jac_vector.fixed_view_mut::<2, 2>(0, 0).copy_from(&r);
312        }
313
314        result
315    }
316
317    fn adjoint(&self) -> Self::JacobianMatrix {
318        let mut adjoint_matrix = Matrix3::identity();
319        let rotation_matrix = self.rotation_matrix();
320
321        adjoint_matrix
322            .fixed_view_mut::<2, 2>(0, 0)
323            .copy_from(&rotation_matrix);
324        adjoint_matrix[(0, 2)] = self.y();
325        adjoint_matrix[(1, 2)] = -self.x();
326
327        adjoint_matrix
328    }
329
330    fn random() -> Self {
331        use rand::Rng;
332        let mut rng = rand::rng();
333
334        // Random translation in [-1, 1]²
335        let translation = Vector2::new(rng.random_range(-1.0..1.0), rng.random_range(-1.0..1.0));
336
337        // Random rotation
338        let angle = rng.random_range(-std::f64::consts::PI..std::f64::consts::PI);
339        let rotation = UnitComplex::from_angle(angle);
340
341        SE2::new(translation, rotation)
342    }
343
344    fn jacobian_identity() -> Self::JacobianMatrix {
345        Matrix3::<f64>::identity()
346    }
347
348    fn zero_jacobian() -> Self::JacobianMatrix {
349        Matrix3::<f64>::zeros()
350    }
351
352    fn normalize(&mut self) {
353        self.rotation.renormalize();
354    }
355
356    fn is_valid(&self, tolerance: f64) -> bool {
357        (self.rotation.norm() - 1.0).abs() < tolerance
358    }
359
360    /// Vee operator: log(g)^∨.
361    ///
362    /// Maps a group element g ∈ G to its tangent vector log(g)^∨ ∈ 𝔤.
363    /// For SE(2), this is the same as log().
364    fn vee(&self) -> Self::TangentVector {
365        self.log(None)
366    }
367
368    /// Check if the element is approximately equal to another element.
369    ///
370    /// # Arguments
371    /// * `other` - The other element to compare with
372    /// * `tolerance` - The tolerance for the comparison
373    fn is_approx(&self, other: &Self, tolerance: f64) -> bool {
374        let difference = self.right_minus(other, None, None);
375        difference.is_zero(tolerance)
376    }
377}
378
379/// SE(2) tangent space element representing elements in the Lie algebra se(2).
380///
381/// Following manif conventions, internally represented as [x, y, theta] where:
382/// - x, y: translational components
383/// - theta: rotational component
384#[derive(Clone, PartialEq)]
385pub struct SE2Tangent {
386    /// Internal data: [x, y, theta]
387    data: Vector3<f64>,
388}
389
390impl fmt::Display for SE2Tangent {
391    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
392        write!(
393            f,
394            "SE2Tangent(x: {:.4}, y: {:.4}, theta: {:.4})",
395            self.x(),
396            self.y(),
397            self.angle()
398        )
399    }
400}
401
402// Conversion traits for integration with generic Problem
403impl From<DVector<f64>> for SE2Tangent {
404    fn from(data_vector: DVector<f64>) -> Self {
405        // Input order is [x, y, theta] to match G2O format
406        // Internal storage is [x, y, theta]
407        SE2Tangent {
408            data: Vector3::new(data_vector[0], data_vector[1], data_vector[2]),
409        }
410    }
411}
412
413impl From<SE2Tangent> for DVector<f64> {
414    fn from(se2_tangent: SE2Tangent) -> Self {
415        DVector::from_vec(vec![
416            se2_tangent.data[0], // x first
417            se2_tangent.data[1], // y second
418            se2_tangent.data[2], // theta third
419        ])
420    }
421}
422
423impl SE2Tangent {
424    /// Create a new SE2Tangent from x, y, and theta components.
425    #[inline]
426    pub fn new(x: f64, y: f64, theta: f64) -> Self {
427        SE2Tangent {
428            data: Vector3::new(x, y, theta),
429        }
430    }
431
432    /// Alias for `new()`, for API consistency with SO3Tangent/SE3Tangent.
433    #[inline]
434    pub fn from_components(x: f64, y: f64, theta: f64) -> Self {
435        SE2Tangent::new(x, y, theta)
436    }
437
438    /// Get the x (translational) component.
439    #[inline]
440    pub fn x(&self) -> f64 {
441        self.data[0]
442    }
443
444    /// Get the y (translational) component.
445    #[inline]
446    pub fn y(&self) -> f64 {
447        self.data[1]
448    }
449
450    /// Get the theta (rotational) component.
451    #[inline]
452    pub fn angle(&self) -> f64 {
453        self.data[2]
454    }
455
456    /// Get the translation part as Vector2.
457    #[inline]
458    pub fn translation(&self) -> Vector2<f64> {
459        Vector2::new(self.x(), self.y())
460    }
461}
462
463impl Tangent<SE2> for SE2Tangent {
464    /// Dimension of the tangent space
465    const DIM: usize = 3;
466
467    /// Get the SE2 element.
468    fn exp(&self, jacobian: Option<&mut <SE2 as LieGroup>::JacobianMatrix>) -> SE2 {
469        let theta = self.angle();
470        let cos_theta = theta.cos();
471        let sin_theta = theta.sin();
472        let theta_sq = theta * theta;
473
474        let (a, b) = if theta_sq < crate::SMALL_ANGLE_THRESHOLD {
475            // Taylor approximation
476            let a = 1.0 - theta_sq / 6.0;
477            let b = 0.5 * theta - theta * theta_sq / 24.0;
478            (a, b)
479        } else {
480            // Euler
481            let a = sin_theta / theta;
482            let b = (1.0 - cos_theta) / theta;
483            (a, b)
484        };
485
486        let translation = Vector2::new(a * self.x() - b * self.y(), b * self.x() + a * self.y());
487        let rotation = UnitComplex::from_cos_sin_unchecked(cos_theta, sin_theta);
488
489        if let Some(jac) = jacobian {
490            *jac = self.right_jacobian();
491        }
492
493        SE2::new(translation, rotation)
494    }
495
496    /// Right Jacobian Jr.
497    fn right_jacobian(&self) -> <SE2 as LieGroup>::JacobianMatrix {
498        let theta = self.angle();
499        let cos_theta = theta.cos();
500        let sin_theta = theta.sin();
501        let theta_sq = theta * theta;
502
503        let (a, b) = if theta_sq < crate::SMALL_ANGLE_THRESHOLD {
504            // Taylor approximation
505            let a = 1.0 - theta_sq / 6.0;
506            let b = 0.5 * theta - theta * theta_sq / 24.0;
507            (a, b)
508        } else {
509            // Euler
510            let a = sin_theta / theta;
511            let b = (1.0 - cos_theta) / theta;
512            (a, b)
513        };
514
515        let mut jac = Matrix3::identity();
516        jac[(0, 0)] = a;
517        jac[(0, 1)] = b;
518        jac[(1, 0)] = -b;
519        jac[(1, 1)] = a;
520
521        if theta_sq < crate::SMALL_ANGLE_THRESHOLD {
522            jac[(0, 2)] = -self.y() / 2.0 + theta * self.x() / 6.0;
523            jac[(1, 2)] = self.x() / 2.0 + theta * self.y() / 6.0;
524        } else {
525            jac[(0, 2)] = (-self.y() + theta * self.x() + self.y() * cos_theta
526                - self.x() * sin_theta)
527                / theta_sq;
528            jac[(1, 2)] =
529                (self.x() + theta * self.y() - self.x() * cos_theta - self.y() * sin_theta)
530                    / theta_sq;
531        }
532
533        jac
534    }
535
536    /// Left Jacobian Jl.
537    fn left_jacobian(&self) -> <SE2 as LieGroup>::JacobianMatrix {
538        let theta = self.angle();
539        let cos_theta = theta.cos();
540        let sin_theta = theta.sin();
541        let theta_sq = theta * theta;
542
543        let (a, b) = if theta_sq < crate::SMALL_ANGLE_THRESHOLD {
544            // Taylor approximation
545            let a = 1.0 - theta_sq / 6.0;
546            let b = 0.5 * theta - theta * theta_sq / 24.0;
547            (a, b)
548        } else {
549            // Euler
550            let a = sin_theta / theta;
551            let b = (1.0 - cos_theta) / theta;
552            (a, b)
553        };
554
555        let mut jac = Matrix3::identity();
556        jac[(0, 0)] = a;
557        jac[(0, 1)] = -b;
558        jac[(1, 0)] = b;
559        jac[(1, 1)] = a;
560
561        if theta_sq < crate::SMALL_ANGLE_THRESHOLD {
562            jac[(0, 2)] = self.y() / 2.0 + theta * self.x() / 6.0;
563            jac[(1, 2)] = -self.x() / 2.0 + theta * self.y() / 6.0;
564        } else {
565            jac[(0, 2)] =
566                (self.y() + theta * self.x() - self.y() * cos_theta - self.x() * sin_theta)
567                    / theta_sq;
568            jac[(1, 2)] = (-self.x() + theta * self.y() + self.x() * cos_theta
569                - self.y() * sin_theta)
570                / theta_sq;
571        }
572
573        jac
574    }
575
576    /// Inverse of right Jacobian Jr⁻¹.
577    fn right_jacobian_inv(&self) -> <SE2 as LieGroup>::JacobianMatrix {
578        let theta = self.angle();
579        let cos_theta = theta.cos();
580        let sin_theta = theta.sin();
581        let theta_sq = theta * theta;
582
583        let mut jac_inv = Matrix3::zeros();
584        jac_inv[(0, 1)] = -theta * 0.5;
585        jac_inv[(1, 0)] = -jac_inv[(0, 1)];
586        jac_inv[(2, 2)] = 1.0;
587
588        if theta_sq > crate::SMALL_ANGLE_THRESHOLD {
589            let a = theta * sin_theta;
590            let b = theta * cos_theta;
591
592            jac_inv[(0, 0)] = -a / (2.0 * cos_theta - 2.0);
593            jac_inv[(1, 1)] = jac_inv[(0, 0)];
594
595            let den = 2.0 * theta * (cos_theta - 1.0);
596            jac_inv[(0, 2)] = (a * self.x() + b * self.y() - theta * self.y()
597                + 2.0 * self.x() * cos_theta
598                - 2.0 * self.x())
599                / den;
600            jac_inv[(1, 2)] =
601                (-b * self.x() + a * self.y() + theta * self.x() + 2.0 * self.y() * cos_theta
602                    - 2.0 * self.y())
603                    / den;
604        } else {
605            jac_inv[(0, 0)] = 1.0 - theta_sq / 12.0;
606            jac_inv[(1, 1)] = jac_inv[(0, 0)];
607
608            jac_inv[(0, 2)] = self.y() / 2.0 + theta * self.x() / 12.0;
609            jac_inv[(1, 2)] = -self.x() / 2.0 + theta * self.y() / 12.0;
610        }
611
612        jac_inv
613    }
614
615    /// Inverse of left Jacobian Jl⁻¹.
616    fn left_jacobian_inv(&self) -> <SE2 as LieGroup>::JacobianMatrix {
617        let theta = self.angle();
618        let cos_theta = theta.cos();
619        let sin_theta = theta.sin();
620        let theta_sq = theta * theta;
621
622        let mut jac_inv = Matrix3::zeros();
623        jac_inv[(0, 1)] = theta * 0.5;
624        jac_inv[(1, 0)] = -jac_inv[(0, 1)];
625        jac_inv[(2, 2)] = 1.0;
626
627        if theta_sq > crate::SMALL_ANGLE_THRESHOLD {
628            let a = theta * sin_theta;
629            let b = theta * cos_theta;
630
631            jac_inv[(0, 0)] = -a / (2.0 * cos_theta - 2.0);
632            jac_inv[(1, 1)] = jac_inv[(0, 0)];
633
634            let den = 2.0 * theta * (cos_theta - 1.0);
635            jac_inv[(0, 2)] =
636                (a * self.x() - b * self.y() + theta * self.y() + 2.0 * self.x() * cos_theta
637                    - 2.0 * self.x())
638                    / den;
639            jac_inv[(1, 2)] = (b * self.x() + a * self.y() - theta * self.x()
640                + 2.0 * self.y() * cos_theta
641                - 2.0 * self.y())
642                / den;
643        } else {
644            jac_inv[(0, 0)] = 1.0 - theta_sq / 12.0;
645            jac_inv[(1, 1)] = jac_inv[(0, 0)];
646
647            jac_inv[(0, 2)] = -self.y() / 2.0 + theta * self.x() / 12.0;
648            jac_inv[(1, 2)] = self.x() / 2.0 + theta * self.y() / 12.0;
649        }
650
651        jac_inv
652    }
653
654    /// Hat operator: φ^∧ (vector to matrix).
655    fn hat(&self) -> <SE2 as LieGroup>::LieAlgebra {
656        Matrix3::new(
657            0.0,
658            -self.angle(),
659            self.x(),
660            self.angle(),
661            0.0,
662            self.y(),
663            0.0,
664            0.0,
665            0.0,
666        )
667    }
668
669    /// Zero tangent vector.
670    fn zero() -> <SE2 as LieGroup>::TangentVector {
671        SE2Tangent::new(0.0, 0.0, 0.0)
672    }
673
674    /// Random tangent vector (useful for testing).
675    fn random() -> <SE2 as LieGroup>::TangentVector {
676        use rand::Rng;
677        let mut rng = rand::rng();
678        SE2Tangent::new(
679            rng.random_range(-1.0..1.0),
680            rng.random_range(-1.0..1.0),
681            rng.random_range(-std::f64::consts::PI..std::f64::consts::PI),
682        )
683    }
684
685    /// Check if the tangent vector is approximately zero.
686    fn is_zero(&self, tolerance: f64) -> bool {
687        self.data.norm() < tolerance
688    }
689
690    /// Normalize the tangent vector to unit norm.
691    fn normalize(&mut self) {
692        let norm = self.data.norm();
693        if norm > f64::EPSILON {
694            self.data /= norm;
695        }
696    }
697
698    /// Return a unit tangent vector in the same direction.
699    fn normalized(&self) -> <SE2 as LieGroup>::TangentVector {
700        let norm = self.data.norm();
701        if norm > f64::EPSILON {
702            SE2Tangent {
703                data: self.data / norm,
704            }
705        } else {
706            SE2Tangent::zero()
707        }
708    }
709
710    /// Small adjoint matrix for SE(2).
711    ///
712    /// For SE(2), the small adjoint involves the angular component.
713    fn small_adj(&self) -> <SE2 as LieGroup>::JacobianMatrix {
714        let x = self.x();
715        let y = self.y();
716
717        let mut small_adj = Matrix3::zeros();
718        small_adj[(0, 1)] = -self.angle();
719        small_adj[(1, 0)] = self.angle();
720        small_adj[(0, 2)] = y;
721        small_adj[(1, 2)] = -x;
722
723        small_adj
724    }
725
726    /// Lie bracket for SE(2).
727    ///
728    /// Computes the Lie bracket [this, other] = this.small_adj() * other.
729    fn lie_bracket(&self, other: &Self) -> <SE2 as LieGroup>::TangentVector {
730        let bracket_result = self.small_adj() * other.data;
731        SE2Tangent {
732            data: bracket_result,
733        }
734    }
735
736    /// Check if this tangent vector is approximately equal to another.
737    ///
738    /// # Arguments
739    /// * `other` - The other tangent vector to compare with
740    /// * `tolerance` - The tolerance for the comparison
741    fn is_approx(&self, other: &Self, tolerance: f64) -> bool {
742        (self.data - other.data).norm() < tolerance
743    }
744
745    /// Get the ith generator of the SE(2) Lie algebra.
746    ///
747    /// # Arguments
748    /// * `i` - Index of the generator (0, 1, or 2 for SE(2))
749    ///
750    /// # Returns
751    /// The generator matrix
752    fn generator(&self, i: usize) -> <SE2 as LieGroup>::LieAlgebra {
753        assert!(i < 3, "SE(2) only has generators for indices 0, 1, 2");
754
755        match i {
756            0 => {
757                // Generator E1 for x translation
758                Matrix3::new(0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0)
759            }
760            1 => {
761                // Generator E2 for y translation
762                Matrix3::new(0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0)
763            }
764            2 => {
765                // Generator E3 for rotation
766                Matrix3::new(0.0, -1.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0)
767            }
768            _ => unreachable!(),
769        }
770    }
771}
772
773#[cfg(test)]
774mod tests {
775    use super::*;
776    use std::f64::consts::PI;
777
778    const TOLERANCE: f64 = 1e-12;
779
780    #[test]
781    fn test_se2_tangent_basic() {
782        let tangent = SE2Tangent::new(4.0, 2.0, PI);
783        assert_eq!(tangent.x(), 4.0);
784        assert_eq!(tangent.y(), 2.0);
785        assert_eq!(tangent.angle(), PI);
786    }
787
788    #[test]
789    fn test_se2_tangent_zero() {
790        let zero = SE2Tangent::zero();
791        assert_eq!(zero.data, Vector3::zeros());
792        assert!(zero.is_zero(1e-10));
793    }
794
795    #[test]
796    fn test_se2_identity() {
797        let identity = SE2::identity();
798        assert!(identity.is_valid(TOLERANCE));
799        assert_eq!(identity.x(), 0.0);
800        assert_eq!(identity.y(), 0.0);
801        assert_eq!(identity.angle(), 0.0);
802    }
803
804    #[test]
805    fn test_se2_new() {
806        let translation = Vector2::new(1.0, 2.0);
807        let rotation = UnitComplex::from_angle(PI / 4.0);
808        let se2 = SE2::new(translation, rotation);
809
810        assert!(se2.is_valid(TOLERANCE));
811        assert_eq!(se2.x(), 1.0);
812        assert_eq!(se2.y(), 2.0);
813        assert!((se2.angle() - PI / 4.0).abs() < TOLERANCE);
814    }
815
816    #[test]
817    fn test_se2_from_xy_angle() {
818        let se2 = SE2::from_xy_angle(4.0, 2.0, 0.0);
819        assert_eq!(se2.x(), 4.0);
820        assert_eq!(se2.y(), 2.0);
821        assert_eq!(se2.angle(), 0.0);
822    }
823
824    #[test]
825    fn test_se2_from_xy_complex() {
826        let se2 = SE2::from_xy_complex(4.0, 2.0, 1.0, 0.0);
827        assert_eq!(se2.x(), 4.0);
828        assert_eq!(se2.y(), 2.0);
829        assert_eq!(se2.real(), 1.0);
830        assert_eq!(se2.imag(), 0.0);
831        assert_eq!(se2.angle(), 0.0);
832    }
833
834    #[test]
835    fn test_se2_inverse() {
836        let se2 = SE2::from_xy_angle(1.0, 1.0, PI);
837        let se2_inv = se2.inverse(None);
838
839        assert!((se2_inv.x() - 1.0).abs() < TOLERANCE);
840        assert!((se2_inv.y() - 1.0).abs() < TOLERANCE);
841        assert!((se2_inv.angle() + PI).abs() < TOLERANCE);
842
843        // Test that g * g^-1 = identity
844        let composed = se2.compose(&se2_inv, None, None);
845        let identity = SE2::identity();
846
847        assert!((composed.x() - identity.x()).abs() < TOLERANCE);
848        assert!((composed.y() - identity.y()).abs() < TOLERANCE);
849        assert!((composed.angle() - identity.angle()).abs() < TOLERANCE);
850    }
851
852    #[test]
853    fn test_se2_compose() {
854        let se2a = SE2::from_xy_angle(1.0, 1.0, PI / 2.0);
855        let se2b = SE2::from_xy_angle(2.0, 2.0, PI / 2.0);
856        let se2c = se2a.compose(&se2b, None, None);
857
858        assert!((se2c.x() - (-1.0)).abs() < TOLERANCE);
859        assert!((se2c.y() - 3.0).abs() < TOLERANCE);
860        assert!((se2c.angle() - PI).abs() < TOLERANCE);
861    }
862
863    #[test]
864    fn test_se2_exp_log() {
865        let tangent = SE2Tangent::new(4.0, 2.0, PI);
866        let se2 = tangent.exp(None);
867        let recovered_tangent = se2.log(None);
868
869        assert!((tangent.x() - recovered_tangent.x()).abs() < TOLERANCE);
870        assert!((tangent.y() - recovered_tangent.y()).abs() < TOLERANCE);
871        assert!((tangent.angle() - recovered_tangent.angle()).abs() < TOLERANCE);
872    }
873
874    #[test]
875    fn test_se2_exp_zero() {
876        let zero_tangent = SE2Tangent::zero();
877        let se2 = zero_tangent.exp(None);
878        let identity = SE2::identity();
879
880        assert!((se2.x() - identity.x()).abs() < TOLERANCE);
881        assert!((se2.y() - identity.y()).abs() < TOLERANCE);
882        assert!((se2.angle() - identity.angle()).abs() < TOLERANCE);
883    }
884
885    #[test]
886    fn test_se2_log_identity() {
887        let identity = SE2::identity();
888        let tangent = identity.log(None);
889
890        assert!(tangent.data.norm() < TOLERANCE);
891    }
892
893    #[test]
894    fn test_se2_act() {
895        let se2 = SE2::from_xy_angle(1.0, 1.0, PI / 2.0);
896        let point = Vector3::new(1.0, 1.0, 0.0);
897        let transformed_point = se2.act(&point, None, None);
898
899        assert!((transformed_point.x - 0.0).abs() < TOLERANCE);
900        assert!((transformed_point.y - 2.0).abs() < TOLERANCE);
901        assert!((transformed_point.z - 0.0).abs() < TOLERANCE);
902    }
903
904    #[test]
905    fn test_se2_between() {
906        let se2a = SE2::from_xy_angle(1.0, 1.0, PI);
907        let se2b = SE2::from_xy_angle(1.0, 1.0, PI);
908        let se2c = se2a.between(&se2b, None, None);
909
910        assert!((se2c.x() - 0.0).abs() < TOLERANCE);
911        assert!((se2c.y() - 0.0).abs() < TOLERANCE);
912        assert!((se2c.angle() - 0.0).abs() < TOLERANCE);
913    }
914
915    #[test]
916    fn test_se2_adjoint() {
917        let se2 = SE2::random();
918        let adj = se2.adjoint();
919
920        assert_eq!(adj.nrows(), 3);
921        assert_eq!(adj.ncols(), 3);
922    }
923
924    #[test]
925    fn test_se2_manifold_properties() {
926        assert_eq!(SE2::DIM, 2);
927        assert_eq!(SE2::DOF, 3);
928        assert_eq!(SE2::REP_SIZE, 4);
929    }
930
931    #[test]
932    fn test_se2_random() {
933        let se2 = SE2::random();
934        assert!(se2.is_valid(TOLERANCE));
935    }
936
937    #[test]
938    fn test_se2_normalize() {
939        let mut se2 = SE2::from_xy_complex(1.0, 2.0, 0.5, 0.5); // Not normalized complex
940        se2.normalize();
941        assert!(se2.is_valid(TOLERANCE));
942    }
943
944    #[test]
945    fn test_se2_tangent_exp_jacobians() {
946        let tangent = SE2Tangent::new(1.0, 2.0, 0.1);
947
948        let se2_element = tangent.exp(None);
949        assert!(se2_element.is_valid(TOLERANCE));
950
951        // Test Jacobians have correct dimensions
952        let right_jac = tangent.right_jacobian();
953        let left_jac = tangent.left_jacobian();
954        let right_jac_inv = tangent.right_jacobian_inv();
955        let left_jac_inv = tangent.left_jacobian_inv();
956
957        assert_eq!(right_jac.nrows(), 3);
958        assert_eq!(right_jac.ncols(), 3);
959        assert_eq!(left_jac.nrows(), 3);
960        assert_eq!(left_jac.ncols(), 3);
961        assert_eq!(right_jac_inv.nrows(), 3);
962        assert_eq!(right_jac_inv.ncols(), 3);
963        assert_eq!(left_jac_inv.nrows(), 3);
964        assert_eq!(left_jac_inv.ncols(), 3);
965    }
966
967    #[test]
968    fn test_se2_tangent_hat() {
969        let tangent = SE2Tangent::new(4.0, 2.0, PI);
970        let hat_matrix = tangent.hat();
971
972        assert_eq!(hat_matrix.nrows(), 3);
973        assert_eq!(hat_matrix.ncols(), 3);
974        assert_eq!(hat_matrix[(0, 2)], 4.0);
975        assert_eq!(hat_matrix[(1, 2)], 2.0);
976        assert_eq!(hat_matrix[(1, 0)], PI);
977        assert_eq!(hat_matrix[(0, 1)], -PI);
978    }
979
980    #[test]
981    fn test_se2_consistency() {
982        // Test associativity: (g1 * g2) * g3 = g1 * (g2 * g3)
983        let se2_1 = SE2::random();
984        let se2_2 = SE2::random();
985        let se2_3 = SE2::random();
986
987        let left_assoc = se2_1
988            .compose(&se2_2, None, None)
989            .compose(&se2_3, None, None);
990        let right_assoc = se2_1.compose(&se2_2.compose(&se2_3, None, None), None, None);
991
992        let translation_diff = (left_assoc.translation() - right_assoc.translation()).norm();
993        let angle_diff = (left_assoc.angle() - right_assoc.angle()).abs();
994
995        assert!(translation_diff < 1e-10);
996        assert!(angle_diff < 1e-10);
997    }
998
999    #[test]
1000    fn test_se2_isometry() {
1001        let translation = Translation2::new(1.0, 2.0);
1002        let rotation = UnitComplex::from_angle(PI / 4.0);
1003        let isometry = Isometry2::from_parts(translation, rotation);
1004
1005        let se2 = SE2::from_isometry(isometry);
1006        let recovered_isometry = se2.isometry();
1007
1008        let translation_diff =
1009            (isometry.translation.vector - recovered_isometry.translation.vector).norm();
1010        let angle_diff = (isometry.rotation.angle() - recovered_isometry.rotation.angle()).abs();
1011
1012        assert!(translation_diff < TOLERANCE);
1013        assert!(angle_diff < TOLERANCE);
1014    }
1015
1016    #[test]
1017    fn test_se2_matrix() {
1018        let se2 = SE2::random();
1019        let matrix = se2.matrix();
1020
1021        // Check matrix is 3x3
1022        assert_eq!(matrix.nrows(), 3);
1023        assert_eq!(matrix.ncols(), 3);
1024
1025        // Check bottom row is [0, 0, 1]
1026        assert!((matrix[(2, 0)]).abs() < TOLERANCE);
1027        assert!((matrix[(2, 1)]).abs() < TOLERANCE);
1028        assert!((matrix[(2, 2)] - 1.0).abs() < TOLERANCE);
1029    }
1030
1031    // Additional comprehensive tests based on manif C++ test suite
1032
1033    #[test]
1034    fn test_se2_constructor_copy() {
1035        let se2_original = SE2::from_xy_complex(4.0, 2.0, (PI / 4.0).cos(), (PI / 4.0).sin());
1036        let se2_copy = se2_original.clone();
1037
1038        assert_eq!(se2_copy.x(), 4.0);
1039        assert_eq!(se2_copy.y(), 2.0);
1040        assert!((se2_copy.angle() - PI / 4.0).abs() < TOLERANCE);
1041    }
1042
1043    #[test]
1044    fn test_se2_assign_op() {
1045        let _se2a = SE2::from_xy_angle(0.0, 0.0, 0.0);
1046        let se2b = SE2::from_xy_angle(4.0, 2.0, PI);
1047
1048        let se2a = se2b.clone(); // Rust equivalent of assignment
1049
1050        assert_eq!(se2a.x(), 4.0);
1051        assert_eq!(se2a.y(), 2.0);
1052        assert!((se2a.angle() - PI).abs() < TOLERANCE);
1053    }
1054
1055    #[test]
1056    fn test_se2_inverse_detailed() {
1057        // Test with identity
1058        let se2 = SE2::identity();
1059        let se2_inv = se2.inverse(None);
1060
1061        assert!((se2_inv.x() - 0.0).abs() < TOLERANCE);
1062        assert!((se2_inv.y() - 0.0).abs() < TOLERANCE);
1063        assert!((se2_inv.angle() - 0.0).abs() < TOLERANCE);
1064        assert!((se2_inv.real() - 1.0).abs() < TOLERANCE);
1065        assert!((se2_inv.imag() - 0.0).abs() < TOLERANCE);
1066
1067        // Test with specific values
1068        let se2 = SE2::from_xy_angle(0.7, 2.3, PI / 3.0);
1069        let se2_inv = se2.inverse(None);
1070
1071        assert!((se2_inv.x() - (-2.341858428704209)).abs() < 1e-10);
1072        assert!((se2_inv.y() - (-0.543782217350893)).abs() < 1e-10);
1073        assert!((se2_inv.angle() - (-PI / 3.0)).abs() < 1e-10);
1074    }
1075
1076    #[test]
1077    fn test_se2_rplus_zero() {
1078        let se2a = SE2::from_xy_angle(1.0, 1.0, PI / 2.0);
1079        let se2b = SE2Tangent::zero();
1080
1081        let se2c = se2a.right_plus(&se2b, None, None);
1082
1083        assert!((se2c.x() - 1.0).abs() < TOLERANCE);
1084        assert!((se2c.y() - 1.0).abs() < TOLERANCE);
1085        assert!((se2c.angle() - PI / 2.0).abs() < TOLERANCE);
1086    }
1087
1088    #[test]
1089    fn test_se2_rplus() {
1090        let se2a = SE2::from_xy_angle(1.0, 1.0, PI / 2.0);
1091        let se2b = SE2Tangent::new(1.0, 1.0, PI / 2.0);
1092
1093        let se2c = se2a.right_plus(&se2b, None, None);
1094
1095        assert!((se2c.angle() - PI).abs() < TOLERANCE);
1096    }
1097
1098    #[test]
1099    fn test_se2_lplus_zero() {
1100        let se2a = SE2::from_xy_angle(1.0, 1.0, PI / 2.0);
1101        let se2b = SE2Tangent::zero();
1102
1103        let se2c = se2a.left_plus(&se2b, None, None);
1104
1105        assert!((se2c.x() - 1.0).abs() < TOLERANCE);
1106        assert!((se2c.y() - 1.0).abs() < TOLERANCE);
1107        assert!((se2c.angle() - PI / 2.0).abs() < TOLERANCE);
1108    }
1109
1110    #[test]
1111    fn test_se2_lplus() {
1112        let se2a = SE2::from_xy_angle(1.0, 1.0, PI / 2.0);
1113        let se2b = SE2Tangent::new(1.0, 1.0, PI / 2.0);
1114
1115        let se2c = se2a.left_plus(&se2b, None, None);
1116
1117        assert!((se2c.angle() - PI).abs() < TOLERANCE);
1118    }
1119
1120    #[test]
1121    fn test_se2_rminus_zero() {
1122        let se2a = SE2::identity();
1123        let se2b = SE2::identity();
1124
1125        let se2c = se2a.right_minus(&se2b, None, None);
1126
1127        assert!((se2c.x() - 0.0).abs() < TOLERANCE);
1128        assert!((se2c.y() - 0.0).abs() < TOLERANCE);
1129        assert!((se2c.angle() - 0.0).abs() < TOLERANCE);
1130    }
1131
1132    #[test]
1133    fn test_se2_rminus() {
1134        let se2a = SE2::from_xy_angle(1.0, 1.0, PI);
1135        let se2b = SE2::from_xy_angle(2.0, 2.0, PI / 2.0);
1136
1137        let se2c = se2a.right_minus(&se2b, None, None);
1138
1139        assert!((se2c.angle() - PI / 2.0).abs() < TOLERANCE);
1140    }
1141
1142    #[test]
1143    fn test_se2_lminus_identity() {
1144        let se2a = SE2::identity();
1145        let se2b = SE2::identity();
1146
1147        let se2c = se2a.left_minus(&se2b, None, None);
1148
1149        assert!((se2c.x() - 0.0).abs() < TOLERANCE);
1150        assert!((se2c.y() - 0.0).abs() < TOLERANCE);
1151        assert!((se2c.angle() - 0.0).abs() < TOLERANCE);
1152    }
1153
1154    #[test]
1155    fn test_se2_lminus() {
1156        let se2a = SE2::from_xy_angle(1.0, 1.0, PI);
1157        let se2b = SE2::from_xy_angle(2.0, 2.0, PI / 2.0);
1158
1159        let se2c = se2a.left_minus(&se2b, None, None);
1160
1161        assert!((se2c.angle() - PI / 2.0).abs() < TOLERANCE);
1162    }
1163
1164    #[test]
1165    fn test_se2_lift() {
1166        let se2 = SE2::from_xy_angle(1.0, 1.0, PI);
1167        let se2_log = se2.log(None);
1168
1169        assert!((se2_log.angle() - PI).abs() < TOLERANCE);
1170    }
1171
1172    #[test]
1173    fn test_se2_compose_detailed() {
1174        let se2a = SE2::from_xy_angle(1.0, 1.0, PI / 2.0);
1175        let se2b = SE2::from_xy_angle(2.0, 2.0, PI / 2.0);
1176
1177        let se2c = se2a.compose(&se2b, None, None);
1178
1179        assert!((se2c.x() - (-1.0)).abs() < TOLERANCE);
1180        assert!((se2c.y() - 3.0).abs() < TOLERANCE);
1181        assert!((se2c.angle() - PI).abs() < TOLERANCE);
1182    }
1183
1184    #[test]
1185    fn test_se2_between_identity() {
1186        let se2a = SE2::from_xy_angle(1.0, 1.0, PI);
1187        let se2b = SE2::from_xy_angle(1.0, 1.0, PI);
1188
1189        let se2c = se2a.between(&se2b, None, None);
1190
1191        assert!((se2c.x() - 0.0).abs() < TOLERANCE);
1192        assert!((se2c.y() - 0.0).abs() < TOLERANCE);
1193        assert!((se2c.angle() - 0.0).abs() < TOLERANCE);
1194    }
1195
1196    #[test]
1197    fn test_se2_between_detailed() {
1198        let se2a = SE2::from_xy_angle(1.0, 1.0, PI);
1199        let se2b = SE2::from_xy_angle(2.0, 2.0, PI / 2.0);
1200
1201        let se2c = se2a.between(&se2b, None, None);
1202
1203        assert!((se2c.x() - (-1.0)).abs() < TOLERANCE);
1204        assert!((se2c.y() - (-1.0)).abs() < TOLERANCE);
1205        assert!((se2c.angle() - (-PI / 2.0)).abs() < TOLERANCE);
1206    }
1207
1208    #[test]
1209    fn test_se2_act_detailed() {
1210        let se2 = SE2::from_xy_angle(1.0, 1.0, PI / 2.0);
1211        let point = Vector3::new(1.0, 1.0, 0.0);
1212        let transformed_point = se2.act(&point, None, None);
1213
1214        assert!((transformed_point.x - 0.0).abs() < TOLERANCE);
1215        assert!((transformed_point.y - 2.0).abs() < TOLERANCE);
1216
1217        let se2 = SE2::from_xy_angle(1.0, 1.0, -PI / 2.0);
1218        let transformed_point = se2.act(&point, None, None);
1219
1220        assert!((transformed_point.x - 2.0).abs() < TOLERANCE);
1221        assert!((transformed_point.y - 0.0).abs() < TOLERANCE);
1222
1223        let se2 = SE2::identity();
1224        let transformed_point = se2.act(&point, None, None);
1225
1226        assert!((transformed_point.x - 1.0).abs() < TOLERANCE);
1227        assert!((transformed_point.y - 1.0).abs() < TOLERANCE);
1228    }
1229
1230    #[test]
1231    fn test_se2_rotation_matrix() {
1232        let se2 = SE2::identity();
1233        let r = se2.rotation_matrix();
1234
1235        assert_eq!(r.nrows(), 2);
1236        assert_eq!(r.ncols(), 2);
1237
1238        // Should be identity matrix for zero rotation
1239        assert!((r[(0, 0)] - 1.0).abs() < TOLERANCE);
1240        assert!((r[(0, 1)] - 0.0).abs() < TOLERANCE);
1241        assert!((r[(1, 0)] - 0.0).abs() < TOLERANCE);
1242        assert!((r[(1, 1)] - 1.0).abs() < TOLERANCE);
1243    }
1244
1245    // SE2Tangent specific tests
1246
1247    #[test]
1248    fn test_se2_tangent_data() {
1249        let se2_tan = SE2Tangent::new(4.0, 2.0, PI);
1250
1251        // Test access functions
1252        assert_eq!(se2_tan.x(), 4.0);
1253        assert_eq!(se2_tan.y(), 2.0);
1254        assert_eq!(se2_tan.angle(), PI);
1255    }
1256
1257    #[test]
1258    fn test_se2_tangent_retract() {
1259        let se2_tan = SE2Tangent::new(4.0, 2.0, PI);
1260
1261        assert_eq!(se2_tan.x(), 4.0);
1262        assert_eq!(se2_tan.y(), 2.0);
1263        assert_eq!(se2_tan.angle(), PI);
1264
1265        let se2_exp = se2_tan.exp(None);
1266
1267        assert!((se2_exp.real() - PI.cos()).abs() < TOLERANCE);
1268        assert!((se2_exp.imag() - PI.sin()).abs() < TOLERANCE);
1269        assert!((se2_exp.angle() - PI).abs() < TOLERANCE);
1270    }
1271
1272    #[test]
1273    fn test_se2_tangent_retract_jac() {
1274        let se2_tan = SE2Tangent::new(4.0, 2.0, PI);
1275
1276        let mut j_ret = Matrix3::zeros();
1277        let se2_exp = se2_tan.exp(Some(&mut j_ret));
1278
1279        assert!((se2_exp.real() - PI.cos()).abs() < TOLERANCE);
1280        assert!((se2_exp.imag() - PI.sin()).abs() < TOLERANCE);
1281        assert!((se2_exp.angle() - PI).abs() < TOLERANCE);
1282
1283        // Check Jacobian dimensions
1284        assert_eq!(j_ret.nrows(), 3);
1285        assert_eq!(j_ret.ncols(), 3);
1286    }
1287
1288    #[test]
1289    fn test_se2_small_angle_approximations() {
1290        // Test behavior with very small angles
1291        let small_tangent = SE2Tangent::new(1e-8, 2e-8, 1e-9);
1292
1293        let se2 = small_tangent.exp(None);
1294        let recovered = se2.log(None);
1295
1296        let diff = (Vector3::new(small_tangent.x(), small_tangent.y(), small_tangent.angle())
1297            - Vector3::new(recovered.x(), recovered.y(), recovered.angle()))
1298        .norm();
1299        assert!(diff < TOLERANCE);
1300    }
1301
1302    #[test]
1303    fn test_se2_tangent_norm() {
1304        let tangent = SE2Tangent::new(3.0, 4.0, 0.0);
1305        let norm = Vector3::new(tangent.x(), tangent.y(), tangent.angle()).norm();
1306        assert!((norm - 5.0).abs() < TOLERANCE); // sqrt(3^2 + 4^2) = 5
1307    }
1308
1309    // New tests for the additional functions
1310
1311    #[test]
1312    fn test_se2_vee() {
1313        let se2 = SE2::random();
1314        let tangent_log = se2.log(None);
1315        let tangent_vee = se2.vee();
1316
1317        assert!((tangent_log.data - tangent_vee.data).norm() < 1e-10);
1318    }
1319
1320    #[test]
1321    fn test_se2_is_approx() {
1322        let se2_1 = SE2::random();
1323        let se2_2 = se2_1.clone();
1324
1325        assert!(se2_1.is_approx(&se2_1, 1e-10));
1326        assert!(se2_1.is_approx(&se2_2, 1e-10));
1327
1328        // Test with small perturbation
1329        let small_tangent = SE2Tangent::new(1e-12, 1e-12, 1e-12);
1330        let se2_perturbed = se2_1.right_plus(&small_tangent, None, None);
1331        assert!(se2_1.is_approx(&se2_perturbed, 1e-10));
1332    }
1333
1334    #[test]
1335    fn test_se2_tangent_small_adj() {
1336        let tangent = SE2Tangent::new(0.1, 0.2, 0.3);
1337        let small_adj = tangent.small_adj();
1338
1339        // Verify the structure of the small adjoint matrix for SE(2)
1340        // Following C++ manif implementation:
1341        // [ 0  -θ   y ]
1342        // [ θ   0  -x ]
1343        // [ 0   0   0 ]
1344        assert_eq!(small_adj[(0, 0)], 0.0);
1345        assert_eq!(small_adj[(1, 1)], 0.0);
1346        assert_eq!(small_adj[(2, 2)], 0.0);
1347        assert_eq!(small_adj[(0, 1)], -tangent.angle());
1348        assert_eq!(small_adj[(1, 0)], tangent.angle());
1349        assert_eq!(small_adj[(0, 2)], tangent.y());
1350        assert_eq!(small_adj[(1, 2)], -tangent.x());
1351    }
1352
1353    #[test]
1354    fn test_se2_tangent_lie_bracket() {
1355        let tangent_a = SE2Tangent::new(0.1, 0.0, 0.0); // Pure x translation
1356        let tangent_b = SE2Tangent::new(0.0, 0.0, 0.2); // Pure rotation
1357
1358        let bracket_ab = tangent_a.lie_bracket(&tangent_b);
1359        let bracket_ba = tangent_b.lie_bracket(&tangent_a);
1360
1361        // Anti-symmetry test: [a,b] = -[b,a]
1362        assert!((bracket_ab.data + bracket_ba.data).norm() < 1e-10);
1363
1364        // [a,a] = 0
1365        let bracket_aa = tangent_a.lie_bracket(&tangent_a);
1366        assert!(bracket_aa.is_zero(1e-10));
1367
1368        // Verify bracket relationship with hat operator
1369        let bracket_hat = bracket_ab.hat();
1370        let expected = tangent_a.hat() * tangent_b.hat() - tangent_b.hat() * tangent_a.hat();
1371        assert!((bracket_hat - expected).norm() < 1e-10);
1372    }
1373
1374    #[test]
1375    fn test_se2_tangent_is_approx() {
1376        let tangent_1 = SE2Tangent::new(0.1, 0.2, 0.3);
1377        let tangent_2 = SE2Tangent::new(0.1 + 1e-12, 0.2, 0.3);
1378        let tangent_3 = SE2Tangent::new(0.5, 0.6, 0.7);
1379
1380        assert!(tangent_1.is_approx(&tangent_1, 1e-10));
1381        assert!(tangent_1.is_approx(&tangent_2, 1e-10));
1382        assert!(!tangent_1.is_approx(&tangent_3, 1e-10));
1383    }
1384
1385    #[test]
1386    fn test_se2_generators() {
1387        let tangent = SE2Tangent::new(1.0, 1.0, 1.0);
1388
1389        // Test all three generators
1390        for i in 0..3 {
1391            let generator = tangent.generator(i);
1392
1393            // Verify that generators are 3x3 matrices
1394            assert_eq!(generator.nrows(), 3);
1395            assert_eq!(generator.ncols(), 3);
1396        }
1397
1398        // Test specific values for the generators
1399        let e1 = tangent.generator(0); // x translation
1400        let e2 = tangent.generator(1); // y translation
1401        let e3 = tangent.generator(2); // rotation
1402
1403        // Expected generators for SE(2)
1404        let expected_e1 = Matrix3::new(0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0);
1405        let expected_e2 = Matrix3::new(0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0);
1406        let expected_e3 = Matrix3::new(0.0, -1.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0);
1407
1408        assert!((e1 - expected_e1).norm() < 1e-10);
1409        assert!((e2 - expected_e2).norm() < 1e-10);
1410        assert!((e3 - expected_e3).norm() < 1e-10);
1411    }
1412
1413    #[test]
1414    #[should_panic]
1415    fn test_se2_generator_invalid_index() {
1416        let tangent = SE2Tangent::new(1.0, 1.0, 1.0);
1417        let _generator = tangent.generator(3); // Should panic for SE(2)
1418    }
1419
1420    #[test]
1421    fn test_se2_jacobi_identity() {
1422        // Test Jacobi identity: [x,[y,z]]+[y,[z,x]]+[z,[x,y]]=0
1423        let x = SE2Tangent::new(0.1, 0.0, 0.0);
1424        let y = SE2Tangent::new(0.0, 0.2, 0.0);
1425        let z = SE2Tangent::new(0.0, 0.0, 0.3);
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 = SE2Tangent {
1432            data: term1.data + term2.data + term3.data,
1433        };
1434        assert!(jacobi_sum.is_zero(1e-10));
1435    }
1436
1437    #[test]
1438    fn test_se2_hat_vee_consistency() {
1439        let tangent = SE2Tangent::new(0.1, 0.2, 0.3);
1440        let hat_matrix = tangent.hat();
1441
1442        // For SE(2), verify hat matrix structure
1443        // The hat matrix should be 3x3, not 4x4 like SE(3)
1444        assert_eq!(hat_matrix[(0, 2)], tangent.x());
1445        assert_eq!(hat_matrix[(1, 2)], tangent.y());
1446        assert_eq!(hat_matrix[(0, 1)], -tangent.angle());
1447        assert_eq!(hat_matrix[(1, 0)], tangent.angle());
1448        assert_eq!(hat_matrix[(2, 0)], 0.0);
1449        assert_eq!(hat_matrix[(2, 1)], 0.0);
1450        assert_eq!(hat_matrix[(2, 2)], 0.0);
1451    }
1452
1453    // T3: Accumulated Error Tests
1454
1455    #[test]
1456    fn test_se2_circular_path_accumulation() {
1457        // Robot driving in a circle: 360 steps of 1° turn + 1cm forward
1458        let step = SE2::from_xy_angle(0.01, 0.0, (std::f64::consts::PI * 2.0) / 360.0);
1459
1460        let mut pose = SE2::identity();
1461        for _ in 0..360 {
1462            pose = pose.compose(&step, None, None);
1463        }
1464
1465        // Should return near identity (full circle)
1466        assert!(pose.is_approx(&SE2::identity(), 1e-2));
1467    }
1468
1469    // T2: Edge Case Tests
1470
1471    #[test]
1472    fn test_se2_angle_wrap_around() {
1473        // Angles > 2π should wrap correctly
1474        let se2_wrapped = SE2::from_xy_angle(1.0, 2.0, 3.0 * std::f64::consts::PI);
1475        let se2_canonical = SE2::from_xy_angle(1.0, 2.0, std::f64::consts::PI);
1476
1477        // Log and exp should handle wrapping
1478        let tangent_wrapped = se2_wrapped.log(None);
1479        let tangent_canonical = se2_canonical.log(None);
1480
1481        // Angles should be equivalent modulo 2π
1482        let angle_diff = (tangent_wrapped.angle() - tangent_canonical.angle()).abs();
1483        assert!(angle_diff < 1e-10 || (angle_diff - 2.0 * std::f64::consts::PI).abs() < 1e-10);
1484    }
1485
1486    #[test]
1487    fn test_se2_negative_angles() {
1488        let se2_neg = SE2::from_xy_angle(0.5, -0.3, -std::f64::consts::PI / 4.0);
1489        let tangent = se2_neg.log(None);
1490        let recovered = tangent.exp(None);
1491        assert!(se2_neg.is_approx(&recovered, 1e-10));
1492    }
1493
1494    // T4: Jacobian Inverse Identity Tests
1495
1496    #[test]
1497    fn test_se2_right_jacobian_inverse_identity() {
1498        let test_tangents = vec![
1499            SE2Tangent::new(0.1, 0.2, 0.3),
1500            SE2Tangent::new(0.5, -0.3, 1.5),
1501        ];
1502
1503        for tangent in test_tangents {
1504            let jr = tangent.right_jacobian();
1505            let jr_inv = tangent.right_jacobian_inv();
1506            let product = jr * jr_inv;
1507            let identity = Matrix3::identity();
1508
1509            assert!((product - identity).norm() < 1e-10);
1510        }
1511    }
1512
1513    #[test]
1514    fn test_se2_left_jacobian_inverse_identity() {
1515        // Same pattern for left Jacobian
1516        let tangent = SE2Tangent::new(0.2, 0.3, 0.8);
1517        let jl = tangent.left_jacobian();
1518        let jl_inv = tangent.left_jacobian_inv();
1519        let product = jl * jl_inv;
1520
1521        assert!((product - Matrix3::identity()).norm() < 1e-10);
1522    }
1523
1524    #[test]
1525    fn test_se2_tangent_from_components() {
1526        let t1 = SE2Tangent::new(1.0, 2.0, 0.5);
1527        let t2 = SE2Tangent::from_components(1.0, 2.0, 0.5);
1528        assert!(t1.is_approx(&t2, 1e-15));
1529        assert_eq!(t2.x(), 1.0);
1530        assert_eq!(t2.y(), 2.0);
1531        assert_eq!(t2.angle(), 0.5);
1532    }
1533
1534    #[test]
1535    fn test_se2_small_angle_threshold_exp_log() {
1536        // Test angles near the SMALL_ANGLE_THRESHOLD boundary round-trip correctly
1537        // sqrt(1e-10) ≈ 1e-5 is the effective angle threshold
1538        let near_threshold_angles = [1e-6, 1e-5, 1e-4, 1e-3];
1539
1540        for &angle in &near_threshold_angles {
1541            let tangent = SE2Tangent::new(1.0, 2.0, angle);
1542            let se2 = tangent.exp(None);
1543            let recovered = se2.log(None);
1544            assert!(
1545                (tangent.x() - recovered.x()).abs() < 1e-10
1546                    && (tangent.y() - recovered.y()).abs() < 1e-10
1547                    && (tangent.angle() - recovered.angle()).abs() < 1e-10,
1548                "SE2 exp-log round-trip failed for angle = {angle}"
1549            );
1550        }
1551    }
1552}