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