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        self.params[2].is_finite()
335    }
336
337    fn as_param_slice(&self) -> &[f64] {
338        self.params.as_slice()
339    }
340
341    fn as_param_slice_mut(&mut self) -> &mut [f64] {
342        self.params.as_mut_slice()
343    }
344
345    fn from_param_slice(s: &[f64]) -> Self {
346        debug_assert_eq!(s.len(), 3);
347        SE2 {
348            params: SVector::from_column_slice(s),
349        }
350    }
351
352    /// Vee operator: log(g)^∨.
353    ///
354    /// Maps a group element g ∈ G to its tangent vector log(g)^∨ ∈ 𝔤.
355    /// For SE(2), this is the same as log().
356    fn vee(&self) -> Self::TangentVector {
357        self.log(None)
358    }
359
360    /// Check if the element is approximately equal to another element.
361    ///
362    /// # Arguments
363    /// * `other` - The other element to compare with
364    /// * `tolerance` - The tolerance for the comparison
365    fn is_approx(&self, other: &Self, tolerance: f64) -> bool {
366        let difference = self.right_minus(other, None, None);
367        difference.is_zero(tolerance)
368    }
369}
370
371/// SE(2) tangent space element representing elements in the Lie algebra se(2).
372///
373/// Following manif conventions, internally represented as [x, y, theta] where:
374/// - x, y: translational components
375/// - theta: rotational component
376#[derive(Clone, PartialEq)]
377pub struct SE2Tangent {
378    /// Internal data: [x, y, theta]
379    data: Vector3<f64>,
380}
381
382impl fmt::Display for SE2Tangent {
383    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
384        write!(
385            f,
386            "SE2Tangent(x: {:.4}, y: {:.4}, theta: {:.4})",
387            self.x(),
388            self.y(),
389            self.angle()
390        )
391    }
392}
393
394impl SE2Tangent {
395    /// Create a new SE2Tangent from x, y, and theta components.
396    #[inline]
397    pub fn new(x: f64, y: f64, theta: f64) -> Self {
398        SE2Tangent {
399            data: Vector3::new(x, y, theta),
400        }
401    }
402
403    /// Alias for `new()`, for API consistency with SO3Tangent/SE3Tangent.
404    #[inline]
405    pub fn from_components(x: f64, y: f64, theta: f64) -> Self {
406        SE2Tangent::new(x, y, theta)
407    }
408
409    /// Get the x (translational) component.
410    #[inline]
411    pub fn x(&self) -> f64 {
412        self.data[0]
413    }
414
415    /// Get the y (translational) component.
416    #[inline]
417    pub fn y(&self) -> f64 {
418        self.data[1]
419    }
420
421    /// Get the theta (rotational) component.
422    #[inline]
423    pub fn angle(&self) -> f64 {
424        self.data[2]
425    }
426
427    /// Get the translation part as Vector2.
428    #[inline]
429    pub fn translation(&self) -> Vector2<f64> {
430        Vector2::new(self.x(), self.y())
431    }
432}
433
434impl Tangent<SE2> for SE2Tangent {
435    /// Dimension of the tangent space
436    const DIM: usize = 3;
437
438    /// Get the SE2 element.
439    fn exp(&self, jacobian: Option<&mut <SE2 as LieGroup>::JacobianMatrix>) -> SE2 {
440        let theta = self.angle();
441        let cos_theta = theta.cos();
442        let sin_theta = theta.sin();
443        let theta_sq = theta * theta;
444
445        let (a, b) = if theta_sq < crate::SMALL_ANGLE_THRESHOLD {
446            // Taylor approximation
447            let a = 1.0 - theta_sq / 6.0;
448            let b = 0.5 * theta - theta * theta_sq / 24.0;
449            (a, b)
450        } else {
451            // Euler
452            let a = sin_theta / theta;
453            let b = (1.0 - cos_theta) / theta;
454            (a, b)
455        };
456
457        let translation = Vector2::new(a * self.x() - b * self.y(), b * self.x() + a * self.y());
458        let rotation = UnitComplex::from_cos_sin_unchecked(cos_theta, sin_theta);
459
460        if let Some(jac) = jacobian {
461            *jac = self.right_jacobian();
462        }
463
464        SE2::new(translation, rotation)
465    }
466
467    /// Right Jacobian Jr.
468    fn right_jacobian(&self) -> <SE2 as LieGroup>::JacobianMatrix {
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 mut jac = Matrix3::identity();
487        jac[(0, 0)] = a;
488        jac[(0, 1)] = b;
489        jac[(1, 0)] = -b;
490        jac[(1, 1)] = a;
491
492        if theta_sq < crate::SMALL_ANGLE_THRESHOLD {
493            jac[(0, 2)] = -self.y() / 2.0 + theta * self.x() / 6.0;
494            jac[(1, 2)] = self.x() / 2.0 + theta * self.y() / 6.0;
495        } else {
496            jac[(0, 2)] = (-self.y() + theta * self.x() + self.y() * cos_theta
497                - self.x() * sin_theta)
498                / theta_sq;
499            jac[(1, 2)] =
500                (self.x() + theta * self.y() - self.x() * cos_theta - self.y() * sin_theta)
501                    / theta_sq;
502        }
503
504        jac
505    }
506
507    /// Left Jacobian Jl.
508    fn left_jacobian(&self) -> <SE2 as LieGroup>::JacobianMatrix {
509        let theta = self.angle();
510        let cos_theta = theta.cos();
511        let sin_theta = theta.sin();
512        let theta_sq = theta * theta;
513
514        let (a, b) = if theta_sq < crate::SMALL_ANGLE_THRESHOLD {
515            // Taylor approximation
516            let a = 1.0 - theta_sq / 6.0;
517            let b = 0.5 * theta - theta * theta_sq / 24.0;
518            (a, b)
519        } else {
520            // Euler
521            let a = sin_theta / theta;
522            let b = (1.0 - cos_theta) / theta;
523            (a, b)
524        };
525
526        let mut jac = Matrix3::identity();
527        jac[(0, 0)] = a;
528        jac[(0, 1)] = -b;
529        jac[(1, 0)] = b;
530        jac[(1, 1)] = a;
531
532        if theta_sq < crate::SMALL_ANGLE_THRESHOLD {
533            jac[(0, 2)] = self.y() / 2.0 + theta * self.x() / 6.0;
534            jac[(1, 2)] = -self.x() / 2.0 + theta * self.y() / 6.0;
535        } else {
536            jac[(0, 2)] =
537                (self.y() + theta * self.x() - self.y() * cos_theta - self.x() * sin_theta)
538                    / theta_sq;
539            jac[(1, 2)] = (-self.x() + theta * self.y() + self.x() * cos_theta
540                - self.y() * sin_theta)
541                / theta_sq;
542        }
543
544        jac
545    }
546
547    /// Inverse of right Jacobian Jr⁻¹.
548    fn right_jacobian_inv(&self) -> <SE2 as LieGroup>::JacobianMatrix {
549        let theta = self.angle();
550        let cos_theta = theta.cos();
551        let sin_theta = theta.sin();
552        let theta_sq = theta * theta;
553
554        let mut jac_inv = Matrix3::zeros();
555        jac_inv[(0, 1)] = -theta * 0.5;
556        jac_inv[(1, 0)] = -jac_inv[(0, 1)];
557        jac_inv[(2, 2)] = 1.0;
558
559        if theta_sq > crate::SMALL_ANGLE_THRESHOLD {
560            let a = theta * sin_theta;
561            let b = theta * cos_theta;
562
563            jac_inv[(0, 0)] = -a / (2.0 * cos_theta - 2.0);
564            jac_inv[(1, 1)] = jac_inv[(0, 0)];
565
566            let den = 2.0 * theta * (cos_theta - 1.0);
567            jac_inv[(0, 2)] = (a * self.x() + b * self.y() - theta * self.y()
568                + 2.0 * self.x() * cos_theta
569                - 2.0 * self.x())
570                / den;
571            jac_inv[(1, 2)] =
572                (-b * self.x() + a * self.y() + theta * self.x() + 2.0 * self.y() * cos_theta
573                    - 2.0 * self.y())
574                    / den;
575        } else {
576            jac_inv[(0, 0)] = 1.0 - theta_sq / 12.0;
577            jac_inv[(1, 1)] = jac_inv[(0, 0)];
578
579            jac_inv[(0, 2)] = self.y() / 2.0 + theta * self.x() / 12.0;
580            jac_inv[(1, 2)] = -self.x() / 2.0 + theta * self.y() / 12.0;
581        }
582
583        jac_inv
584    }
585
586    /// Inverse of left Jacobian Jl⁻¹.
587    fn left_jacobian_inv(&self) -> <SE2 as LieGroup>::JacobianMatrix {
588        let theta = self.angle();
589        let cos_theta = theta.cos();
590        let sin_theta = theta.sin();
591        let theta_sq = theta * theta;
592
593        let mut jac_inv = Matrix3::zeros();
594        jac_inv[(0, 1)] = theta * 0.5;
595        jac_inv[(1, 0)] = -jac_inv[(0, 1)];
596        jac_inv[(2, 2)] = 1.0;
597
598        if theta_sq > crate::SMALL_ANGLE_THRESHOLD {
599            let a = theta * sin_theta;
600            let b = theta * cos_theta;
601
602            jac_inv[(0, 0)] = -a / (2.0 * cos_theta - 2.0);
603            jac_inv[(1, 1)] = jac_inv[(0, 0)];
604
605            let den = 2.0 * theta * (cos_theta - 1.0);
606            jac_inv[(0, 2)] =
607                (a * self.x() - b * self.y() + theta * self.y() + 2.0 * self.x() * cos_theta
608                    - 2.0 * self.x())
609                    / den;
610            jac_inv[(1, 2)] = (b * self.x() + a * self.y() - theta * self.x()
611                + 2.0 * self.y() * cos_theta
612                - 2.0 * self.y())
613                / den;
614        } else {
615            jac_inv[(0, 0)] = 1.0 - theta_sq / 12.0;
616            jac_inv[(1, 1)] = jac_inv[(0, 0)];
617
618            jac_inv[(0, 2)] = -self.y() / 2.0 + theta * self.x() / 12.0;
619            jac_inv[(1, 2)] = self.x() / 2.0 + theta * self.y() / 12.0;
620        }
621
622        jac_inv
623    }
624
625    /// Hat operator: φ^∧ (vector to matrix).
626    fn hat(&self) -> <SE2 as LieGroup>::LieAlgebra {
627        Matrix3::new(
628            0.0,
629            -self.angle(),
630            self.x(),
631            self.angle(),
632            0.0,
633            self.y(),
634            0.0,
635            0.0,
636            0.0,
637        )
638    }
639
640    /// Zero tangent vector.
641    fn zero() -> <SE2 as LieGroup>::TangentVector {
642        SE2Tangent::new(0.0, 0.0, 0.0)
643    }
644
645    /// Random tangent vector (useful for testing).
646    fn random() -> <SE2 as LieGroup>::TangentVector {
647        use rand::Rng;
648        let mut rng = rand::rng();
649        SE2Tangent::new(
650            rng.random_range(-1.0..1.0),
651            rng.random_range(-1.0..1.0),
652            rng.random_range(-std::f64::consts::PI..std::f64::consts::PI),
653        )
654    }
655
656    /// Check if the tangent vector is approximately zero.
657    fn is_zero(&self, tolerance: f64) -> bool {
658        self.data.norm() < tolerance
659    }
660
661    /// Normalize the tangent vector to unit norm.
662    fn normalize(&mut self) {
663        let norm = self.data.norm();
664        if norm > f64::EPSILON {
665            self.data /= norm;
666        }
667    }
668
669    /// Return a unit tangent vector in the same direction.
670    fn normalized(&self) -> <SE2 as LieGroup>::TangentVector {
671        let norm = self.data.norm();
672        if norm > f64::EPSILON {
673            SE2Tangent {
674                data: self.data / norm,
675            }
676        } else {
677            SE2Tangent::zero()
678        }
679    }
680
681    fn as_slice(&self) -> &[f64] {
682        self.data.as_slice()
683    }
684
685    fn from_slice(s: &[f64]) -> Self {
686        debug_assert_eq!(s.len(), 3);
687        SE2Tangent {
688            data: Vector3::from_column_slice(s),
689        }
690    }
691
692    /// Small adjoint matrix for SE(2).
693    ///
694    /// For SE(2), the small adjoint involves the angular component.
695    fn small_adj(&self) -> <SE2 as LieGroup>::JacobianMatrix {
696        let x = self.x();
697        let y = self.y();
698
699        let mut small_adj = Matrix3::zeros();
700        small_adj[(0, 1)] = -self.angle();
701        small_adj[(1, 0)] = self.angle();
702        small_adj[(0, 2)] = y;
703        small_adj[(1, 2)] = -x;
704
705        small_adj
706    }
707
708    /// Lie bracket for SE(2).
709    ///
710    /// Computes the Lie bracket [this, other] = this.small_adj() * other.
711    fn lie_bracket(&self, other: &Self) -> <SE2 as LieGroup>::TangentVector {
712        let bracket_result = self.small_adj() * other.data;
713        SE2Tangent {
714            data: bracket_result,
715        }
716    }
717
718    /// Check if this tangent vector is approximately equal to another.
719    ///
720    /// # Arguments
721    /// * `other` - The other tangent vector to compare with
722    /// * `tolerance` - The tolerance for the comparison
723    fn is_approx(&self, other: &Self, tolerance: f64) -> bool {
724        (self.data - other.data).norm() < tolerance
725    }
726
727    /// Get the ith generator of the SE(2) Lie algebra.
728    ///
729    /// # Arguments
730    /// * `i` - Index of the generator (0, 1, or 2 for SE(2))
731    ///
732    /// # Returns
733    /// The generator matrix
734    fn generator(&self, i: usize) -> <SE2 as LieGroup>::LieAlgebra {
735        assert!(i < 3, "SE(2) only has generators for indices 0, 1, 2");
736
737        match i {
738            0 => {
739                // Generator E1 for x translation
740                Matrix3::new(0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0)
741            }
742            1 => {
743                // Generator E2 for y translation
744                Matrix3::new(0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0)
745            }
746            2 => {
747                // Generator E3 for rotation
748                Matrix3::new(0.0, -1.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0)
749            }
750            _ => unreachable!(),
751        }
752    }
753}
754
755#[cfg(test)]
756mod tests {
757    use super::*;
758    use std::f64::consts::PI;
759
760    const TOLERANCE: f64 = 1e-12;
761
762    #[test]
763    fn test_se2_tangent_basic() {
764        let tangent = SE2Tangent::new(4.0, 2.0, PI);
765        assert_eq!(tangent.x(), 4.0);
766        assert_eq!(tangent.y(), 2.0);
767        assert_eq!(tangent.angle(), PI);
768    }
769
770    #[test]
771    fn test_se2_tangent_zero() {
772        let zero = SE2Tangent::zero();
773        assert_eq!(zero.data, Vector3::zeros());
774        assert!(zero.is_zero(1e-10));
775    }
776
777    #[test]
778    fn test_se2_identity() {
779        let identity = SE2::identity();
780        assert!(identity.is_valid(TOLERANCE));
781        assert_eq!(identity.x(), 0.0);
782        assert_eq!(identity.y(), 0.0);
783        assert_eq!(identity.angle(), 0.0);
784    }
785
786    #[test]
787    fn test_se2_new() {
788        let translation = Vector2::new(1.0, 2.0);
789        let rotation = UnitComplex::from_angle(PI / 4.0);
790        let se2 = SE2::new(translation, rotation);
791
792        assert!(se2.is_valid(TOLERANCE));
793        assert_eq!(se2.x(), 1.0);
794        assert_eq!(se2.y(), 2.0);
795        assert!((se2.angle() - PI / 4.0).abs() < TOLERANCE);
796    }
797
798    #[test]
799    fn test_se2_from_xy_angle() {
800        let se2 = SE2::from_xy_angle(4.0, 2.0, 0.0);
801        assert_eq!(se2.x(), 4.0);
802        assert_eq!(se2.y(), 2.0);
803        assert_eq!(se2.angle(), 0.0);
804    }
805
806    #[test]
807    fn test_se2_from_xy_complex() {
808        let se2 = SE2::from_xy_complex(4.0, 2.0, 1.0, 0.0);
809        assert_eq!(se2.x(), 4.0);
810        assert_eq!(se2.y(), 2.0);
811        assert_eq!(se2.real(), 1.0);
812        assert_eq!(se2.imag(), 0.0);
813        assert_eq!(se2.angle(), 0.0);
814    }
815
816    #[test]
817    fn test_se2_inverse() {
818        let se2 = SE2::from_xy_angle(1.0, 1.0, PI);
819        let se2_inv = se2.inverse(None);
820
821        assert!((se2_inv.x() - 1.0).abs() < TOLERANCE);
822        assert!((se2_inv.y() - 1.0).abs() < TOLERANCE);
823        assert!((se2_inv.angle() + PI).abs() < TOLERANCE);
824
825        // Test that g * g^-1 = identity
826        let composed = se2.compose(&se2_inv, None, None);
827        let identity = SE2::identity();
828
829        assert!((composed.x() - identity.x()).abs() < TOLERANCE);
830        assert!((composed.y() - identity.y()).abs() < TOLERANCE);
831        assert!((composed.angle() - identity.angle()).abs() < TOLERANCE);
832    }
833
834    #[test]
835    fn test_se2_compose() {
836        let se2a = SE2::from_xy_angle(1.0, 1.0, PI / 2.0);
837        let se2b = SE2::from_xy_angle(2.0, 2.0, PI / 2.0);
838        let se2c = se2a.compose(&se2b, None, None);
839
840        assert!((se2c.x() - (-1.0)).abs() < TOLERANCE);
841        assert!((se2c.y() - 3.0).abs() < TOLERANCE);
842        assert!((se2c.angle() - PI).abs() < TOLERANCE);
843    }
844
845    #[test]
846    fn test_se2_exp_log() {
847        let tangent = SE2Tangent::new(4.0, 2.0, PI);
848        let se2 = tangent.exp(None);
849        let recovered_tangent = se2.log(None);
850
851        assert!((tangent.x() - recovered_tangent.x()).abs() < TOLERANCE);
852        assert!((tangent.y() - recovered_tangent.y()).abs() < TOLERANCE);
853        assert!((tangent.angle() - recovered_tangent.angle()).abs() < TOLERANCE);
854    }
855
856    #[test]
857    fn test_se2_exp_zero() {
858        let zero_tangent = SE2Tangent::zero();
859        let se2 = zero_tangent.exp(None);
860        let identity = SE2::identity();
861
862        assert!((se2.x() - identity.x()).abs() < TOLERANCE);
863        assert!((se2.y() - identity.y()).abs() < TOLERANCE);
864        assert!((se2.angle() - identity.angle()).abs() < TOLERANCE);
865    }
866
867    #[test]
868    fn test_se2_log_identity() {
869        let identity = SE2::identity();
870        let tangent = identity.log(None);
871
872        assert!(tangent.data.norm() < TOLERANCE);
873    }
874
875    #[test]
876    fn test_se2_act() {
877        let se2 = SE2::from_xy_angle(1.0, 1.0, PI / 2.0);
878        let point = Vector3::new(1.0, 1.0, 0.0);
879        let transformed_point = se2.act(&point, None, None);
880
881        assert!((transformed_point.x - 0.0).abs() < TOLERANCE);
882        assert!((transformed_point.y - 2.0).abs() < TOLERANCE);
883        assert!((transformed_point.z - 0.0).abs() < TOLERANCE);
884    }
885
886    #[test]
887    fn test_se2_between() {
888        let se2a = SE2::from_xy_angle(1.0, 1.0, PI);
889        let se2b = SE2::from_xy_angle(1.0, 1.0, PI);
890        let se2c = se2a.between(&se2b, None, None);
891
892        assert!((se2c.x() - 0.0).abs() < TOLERANCE);
893        assert!((se2c.y() - 0.0).abs() < TOLERANCE);
894        assert!((se2c.angle() - 0.0).abs() < TOLERANCE);
895    }
896
897    #[test]
898    fn test_se2_adjoint() {
899        let se2 = SE2::random();
900        let adj = se2.adjoint();
901
902        assert_eq!(adj.nrows(), 3);
903        assert_eq!(adj.ncols(), 3);
904    }
905
906    #[test]
907    fn test_se2_manifold_properties() {
908        assert_eq!(SE2::DIM, 2);
909        assert_eq!(SE2::DOF, 3);
910        assert_eq!(SE2::REP_SIZE, 3);
911    }
912
913    #[test]
914    fn test_se2_random() {
915        let se2 = SE2::random();
916        assert!(se2.is_valid(TOLERANCE));
917    }
918
919    #[test]
920    fn test_se2_normalize() {
921        let mut se2 = SE2::from_xy_complex(1.0, 2.0, 0.5, 0.5); // Not normalized complex
922        se2.normalize();
923        assert!(se2.is_valid(TOLERANCE));
924    }
925
926    #[test]
927    fn test_se2_tangent_exp_jacobians() {
928        let tangent = SE2Tangent::new(1.0, 2.0, 0.1);
929
930        let se2_element = tangent.exp(None);
931        assert!(se2_element.is_valid(TOLERANCE));
932
933        // Test Jacobians have correct dimensions
934        let right_jac = tangent.right_jacobian();
935        let left_jac = tangent.left_jacobian();
936        let right_jac_inv = tangent.right_jacobian_inv();
937        let left_jac_inv = tangent.left_jacobian_inv();
938
939        assert_eq!(right_jac.nrows(), 3);
940        assert_eq!(right_jac.ncols(), 3);
941        assert_eq!(left_jac.nrows(), 3);
942        assert_eq!(left_jac.ncols(), 3);
943        assert_eq!(right_jac_inv.nrows(), 3);
944        assert_eq!(right_jac_inv.ncols(), 3);
945        assert_eq!(left_jac_inv.nrows(), 3);
946        assert_eq!(left_jac_inv.ncols(), 3);
947    }
948
949    #[test]
950    fn test_se2_tangent_hat() {
951        let tangent = SE2Tangent::new(4.0, 2.0, PI);
952        let hat_matrix = tangent.hat();
953
954        assert_eq!(hat_matrix.nrows(), 3);
955        assert_eq!(hat_matrix.ncols(), 3);
956        assert_eq!(hat_matrix[(0, 2)], 4.0);
957        assert_eq!(hat_matrix[(1, 2)], 2.0);
958        assert_eq!(hat_matrix[(1, 0)], PI);
959        assert_eq!(hat_matrix[(0, 1)], -PI);
960    }
961
962    #[test]
963    fn test_se2_consistency() {
964        // Test associativity: (g1 * g2) * g3 = g1 * (g2 * g3)
965        let se2_1 = SE2::random();
966        let se2_2 = SE2::random();
967        let se2_3 = SE2::random();
968
969        let left_assoc = se2_1
970            .compose(&se2_2, None, None)
971            .compose(&se2_3, None, None);
972        let right_assoc = se2_1.compose(&se2_2.compose(&se2_3, None, None), None, None);
973
974        let translation_diff = (left_assoc.translation() - right_assoc.translation()).norm();
975        let angle_diff = (left_assoc.angle() - right_assoc.angle()).abs();
976
977        assert!(translation_diff < 1e-10);
978        assert!(angle_diff < 1e-10);
979    }
980
981    #[test]
982    fn test_se2_isometry() {
983        let translation = Translation2::new(1.0, 2.0);
984        let rotation = UnitComplex::from_angle(PI / 4.0);
985        let isometry = Isometry2::from_parts(translation, rotation);
986
987        let se2 = SE2::from_isometry(isometry);
988        let recovered_isometry = se2.isometry();
989
990        let translation_diff =
991            (isometry.translation.vector - recovered_isometry.translation.vector).norm();
992        let angle_diff = (isometry.rotation.angle() - recovered_isometry.rotation.angle()).abs();
993
994        assert!(translation_diff < TOLERANCE);
995        assert!(angle_diff < TOLERANCE);
996    }
997
998    #[test]
999    fn test_se2_matrix() {
1000        let se2 = SE2::random();
1001        let matrix = se2.matrix();
1002
1003        // Check matrix is 3x3
1004        assert_eq!(matrix.nrows(), 3);
1005        assert_eq!(matrix.ncols(), 3);
1006
1007        // Check bottom row is [0, 0, 1]
1008        assert!((matrix[(2, 0)]).abs() < TOLERANCE);
1009        assert!((matrix[(2, 1)]).abs() < TOLERANCE);
1010        assert!((matrix[(2, 2)] - 1.0).abs() < TOLERANCE);
1011    }
1012
1013    // Additional comprehensive tests based on manif C++ test suite
1014
1015    #[test]
1016    fn test_se2_constructor_copy() {
1017        let se2_original = SE2::from_xy_complex(4.0, 2.0, (PI / 4.0).cos(), (PI / 4.0).sin());
1018        let se2_copy = se2_original.clone();
1019
1020        assert_eq!(se2_copy.x(), 4.0);
1021        assert_eq!(se2_copy.y(), 2.0);
1022        assert!((se2_copy.angle() - PI / 4.0).abs() < TOLERANCE);
1023    }
1024
1025    #[test]
1026    fn test_se2_assign_op() {
1027        let _se2a = SE2::from_xy_angle(0.0, 0.0, 0.0);
1028        let se2b = SE2::from_xy_angle(4.0, 2.0, PI);
1029
1030        let se2a = se2b.clone(); // Rust equivalent of assignment
1031
1032        assert_eq!(se2a.x(), 4.0);
1033        assert_eq!(se2a.y(), 2.0);
1034        assert!((se2a.angle() - PI).abs() < TOLERANCE);
1035    }
1036
1037    #[test]
1038    fn test_se2_inverse_detailed() {
1039        // Test with identity
1040        let se2 = SE2::identity();
1041        let se2_inv = se2.inverse(None);
1042
1043        assert!((se2_inv.x() - 0.0).abs() < TOLERANCE);
1044        assert!((se2_inv.y() - 0.0).abs() < TOLERANCE);
1045        assert!((se2_inv.angle() - 0.0).abs() < TOLERANCE);
1046        assert!((se2_inv.real() - 1.0).abs() < TOLERANCE);
1047        assert!((se2_inv.imag() - 0.0).abs() < TOLERANCE);
1048
1049        // Test with specific values
1050        let se2 = SE2::from_xy_angle(0.7, 2.3, PI / 3.0);
1051        let se2_inv = se2.inverse(None);
1052
1053        assert!((se2_inv.x() - (-2.341858428704209)).abs() < 1e-10);
1054        assert!((se2_inv.y() - (-0.543782217350893)).abs() < 1e-10);
1055        assert!((se2_inv.angle() - (-PI / 3.0)).abs() < 1e-10);
1056    }
1057
1058    #[test]
1059    fn test_se2_rplus_zero() {
1060        let se2a = SE2::from_xy_angle(1.0, 1.0, PI / 2.0);
1061        let se2b = SE2Tangent::zero();
1062
1063        let se2c = se2a.right_plus(&se2b, None, None);
1064
1065        assert!((se2c.x() - 1.0).abs() < TOLERANCE);
1066        assert!((se2c.y() - 1.0).abs() < TOLERANCE);
1067        assert!((se2c.angle() - PI / 2.0).abs() < TOLERANCE);
1068    }
1069
1070    #[test]
1071    fn test_se2_rplus() {
1072        let se2a = SE2::from_xy_angle(1.0, 1.0, PI / 2.0);
1073        let se2b = SE2Tangent::new(1.0, 1.0, PI / 2.0);
1074
1075        let se2c = se2a.right_plus(&se2b, None, None);
1076
1077        assert!((se2c.angle() - PI).abs() < TOLERANCE);
1078    }
1079
1080    #[test]
1081    fn test_se2_lplus_zero() {
1082        let se2a = SE2::from_xy_angle(1.0, 1.0, PI / 2.0);
1083        let se2b = SE2Tangent::zero();
1084
1085        let se2c = se2a.left_plus(&se2b, None, None);
1086
1087        assert!((se2c.x() - 1.0).abs() < TOLERANCE);
1088        assert!((se2c.y() - 1.0).abs() < TOLERANCE);
1089        assert!((se2c.angle() - PI / 2.0).abs() < TOLERANCE);
1090    }
1091
1092    #[test]
1093    fn test_se2_lplus() {
1094        let se2a = SE2::from_xy_angle(1.0, 1.0, PI / 2.0);
1095        let se2b = SE2Tangent::new(1.0, 1.0, PI / 2.0);
1096
1097        let se2c = se2a.left_plus(&se2b, None, None);
1098
1099        assert!((se2c.angle() - PI).abs() < TOLERANCE);
1100    }
1101
1102    #[test]
1103    fn test_se2_rminus_zero() {
1104        let se2a = SE2::identity();
1105        let se2b = SE2::identity();
1106
1107        let se2c = se2a.right_minus(&se2b, None, None);
1108
1109        assert!((se2c.x() - 0.0).abs() < TOLERANCE);
1110        assert!((se2c.y() - 0.0).abs() < TOLERANCE);
1111        assert!((se2c.angle() - 0.0).abs() < TOLERANCE);
1112    }
1113
1114    #[test]
1115    fn test_se2_rminus() {
1116        let se2a = SE2::from_xy_angle(1.0, 1.0, PI);
1117        let se2b = SE2::from_xy_angle(2.0, 2.0, PI / 2.0);
1118
1119        let se2c = se2a.right_minus(&se2b, None, None);
1120
1121        assert!((se2c.angle() - PI / 2.0).abs() < TOLERANCE);
1122    }
1123
1124    #[test]
1125    fn test_se2_lminus_identity() {
1126        let se2a = SE2::identity();
1127        let se2b = SE2::identity();
1128
1129        let se2c = se2a.left_minus(&se2b, None, None);
1130
1131        assert!((se2c.x() - 0.0).abs() < TOLERANCE);
1132        assert!((se2c.y() - 0.0).abs() < TOLERANCE);
1133        assert!((se2c.angle() - 0.0).abs() < TOLERANCE);
1134    }
1135
1136    #[test]
1137    fn test_se2_lminus() {
1138        let se2a = SE2::from_xy_angle(1.0, 1.0, PI);
1139        let se2b = SE2::from_xy_angle(2.0, 2.0, PI / 2.0);
1140
1141        let se2c = se2a.left_minus(&se2b, None, None);
1142
1143        assert!((se2c.angle() - PI / 2.0).abs() < TOLERANCE);
1144    }
1145
1146    #[test]
1147    fn test_se2_lift() {
1148        let se2 = SE2::from_xy_angle(1.0, 1.0, PI);
1149        let se2_log = se2.log(None);
1150
1151        assert!((se2_log.angle() - PI).abs() < TOLERANCE);
1152    }
1153
1154    #[test]
1155    fn test_se2_compose_detailed() {
1156        let se2a = SE2::from_xy_angle(1.0, 1.0, PI / 2.0);
1157        let se2b = SE2::from_xy_angle(2.0, 2.0, PI / 2.0);
1158
1159        let se2c = se2a.compose(&se2b, None, None);
1160
1161        assert!((se2c.x() - (-1.0)).abs() < TOLERANCE);
1162        assert!((se2c.y() - 3.0).abs() < TOLERANCE);
1163        assert!((se2c.angle() - PI).abs() < TOLERANCE);
1164    }
1165
1166    #[test]
1167    fn test_se2_between_identity() {
1168        let se2a = SE2::from_xy_angle(1.0, 1.0, PI);
1169        let se2b = SE2::from_xy_angle(1.0, 1.0, PI);
1170
1171        let se2c = se2a.between(&se2b, None, None);
1172
1173        assert!((se2c.x() - 0.0).abs() < TOLERANCE);
1174        assert!((se2c.y() - 0.0).abs() < TOLERANCE);
1175        assert!((se2c.angle() - 0.0).abs() < TOLERANCE);
1176    }
1177
1178    #[test]
1179    fn test_se2_between_detailed() {
1180        let se2a = SE2::from_xy_angle(1.0, 1.0, PI);
1181        let se2b = SE2::from_xy_angle(2.0, 2.0, PI / 2.0);
1182
1183        let se2c = se2a.between(&se2b, None, None);
1184
1185        assert!((se2c.x() - (-1.0)).abs() < TOLERANCE);
1186        assert!((se2c.y() - (-1.0)).abs() < TOLERANCE);
1187        assert!((se2c.angle() - (-PI / 2.0)).abs() < TOLERANCE);
1188    }
1189
1190    #[test]
1191    fn test_se2_act_detailed() {
1192        let se2 = SE2::from_xy_angle(1.0, 1.0, PI / 2.0);
1193        let point = Vector3::new(1.0, 1.0, 0.0);
1194        let transformed_point = se2.act(&point, None, None);
1195
1196        assert!((transformed_point.x - 0.0).abs() < TOLERANCE);
1197        assert!((transformed_point.y - 2.0).abs() < TOLERANCE);
1198
1199        let se2 = SE2::from_xy_angle(1.0, 1.0, -PI / 2.0);
1200        let transformed_point = se2.act(&point, None, None);
1201
1202        assert!((transformed_point.x - 2.0).abs() < TOLERANCE);
1203        assert!((transformed_point.y - 0.0).abs() < TOLERANCE);
1204
1205        let se2 = SE2::identity();
1206        let transformed_point = se2.act(&point, None, None);
1207
1208        assert!((transformed_point.x - 1.0).abs() < TOLERANCE);
1209        assert!((transformed_point.y - 1.0).abs() < TOLERANCE);
1210    }
1211
1212    #[test]
1213    fn test_se2_rotation_matrix() {
1214        let se2 = SE2::identity();
1215        let r = se2.rotation_matrix();
1216
1217        assert_eq!(r.nrows(), 2);
1218        assert_eq!(r.ncols(), 2);
1219
1220        // Should be identity matrix for zero rotation
1221        assert!((r[(0, 0)] - 1.0).abs() < TOLERANCE);
1222        assert!((r[(0, 1)] - 0.0).abs() < TOLERANCE);
1223        assert!((r[(1, 0)] - 0.0).abs() < TOLERANCE);
1224        assert!((r[(1, 1)] - 1.0).abs() < TOLERANCE);
1225    }
1226
1227    // SE2Tangent specific tests
1228
1229    #[test]
1230    fn test_se2_tangent_data() {
1231        let se2_tan = SE2Tangent::new(4.0, 2.0, PI);
1232
1233        // Test access functions
1234        assert_eq!(se2_tan.x(), 4.0);
1235        assert_eq!(se2_tan.y(), 2.0);
1236        assert_eq!(se2_tan.angle(), PI);
1237    }
1238
1239    #[test]
1240    fn test_se2_tangent_retract() {
1241        let se2_tan = SE2Tangent::new(4.0, 2.0, PI);
1242
1243        assert_eq!(se2_tan.x(), 4.0);
1244        assert_eq!(se2_tan.y(), 2.0);
1245        assert_eq!(se2_tan.angle(), PI);
1246
1247        let se2_exp = se2_tan.exp(None);
1248
1249        assert!((se2_exp.real() - PI.cos()).abs() < TOLERANCE);
1250        assert!((se2_exp.imag() - PI.sin()).abs() < TOLERANCE);
1251        assert!((se2_exp.angle() - PI).abs() < TOLERANCE);
1252    }
1253
1254    #[test]
1255    fn test_se2_tangent_retract_jac() {
1256        let se2_tan = SE2Tangent::new(4.0, 2.0, PI);
1257
1258        let mut j_ret = Matrix3::zeros();
1259        let se2_exp = se2_tan.exp(Some(&mut j_ret));
1260
1261        assert!((se2_exp.real() - PI.cos()).abs() < TOLERANCE);
1262        assert!((se2_exp.imag() - PI.sin()).abs() < TOLERANCE);
1263        assert!((se2_exp.angle() - PI).abs() < TOLERANCE);
1264
1265        // Check Jacobian dimensions
1266        assert_eq!(j_ret.nrows(), 3);
1267        assert_eq!(j_ret.ncols(), 3);
1268    }
1269
1270    #[test]
1271    fn test_se2_small_angle_approximations() {
1272        // Test behavior with very small angles
1273        let small_tangent = SE2Tangent::new(1e-8, 2e-8, 1e-9);
1274
1275        let se2 = small_tangent.exp(None);
1276        let recovered = se2.log(None);
1277
1278        let diff = (Vector3::new(small_tangent.x(), small_tangent.y(), small_tangent.angle())
1279            - Vector3::new(recovered.x(), recovered.y(), recovered.angle()))
1280        .norm();
1281        assert!(diff < TOLERANCE);
1282    }
1283
1284    #[test]
1285    fn test_se2_tangent_norm() {
1286        let tangent = SE2Tangent::new(3.0, 4.0, 0.0);
1287        let norm = Vector3::new(tangent.x(), tangent.y(), tangent.angle()).norm();
1288        assert!((norm - 5.0).abs() < TOLERANCE); // sqrt(3^2 + 4^2) = 5
1289    }
1290
1291    // New tests for the additional functions
1292
1293    #[test]
1294    fn test_se2_vee() {
1295        let se2 = SE2::random();
1296        let tangent_log = se2.log(None);
1297        let tangent_vee = se2.vee();
1298
1299        assert!((tangent_log.data - tangent_vee.data).norm() < 1e-10);
1300    }
1301
1302    #[test]
1303    fn test_se2_is_approx() {
1304        let se2_1 = SE2::random();
1305        let se2_2 = se2_1.clone();
1306
1307        assert!(se2_1.is_approx(&se2_1, 1e-10));
1308        assert!(se2_1.is_approx(&se2_2, 1e-10));
1309
1310        // Test with small perturbation
1311        let small_tangent = SE2Tangent::new(1e-12, 1e-12, 1e-12);
1312        let se2_perturbed = se2_1.right_plus(&small_tangent, None, None);
1313        assert!(se2_1.is_approx(&se2_perturbed, 1e-10));
1314    }
1315
1316    #[test]
1317    fn test_se2_tangent_small_adj() {
1318        let tangent = SE2Tangent::new(0.1, 0.2, 0.3);
1319        let small_adj = tangent.small_adj();
1320
1321        // Verify the structure of the small adjoint matrix for SE(2)
1322        // Following C++ manif implementation:
1323        // [ 0  -θ   y ]
1324        // [ θ   0  -x ]
1325        // [ 0   0   0 ]
1326        assert_eq!(small_adj[(0, 0)], 0.0);
1327        assert_eq!(small_adj[(1, 1)], 0.0);
1328        assert_eq!(small_adj[(2, 2)], 0.0);
1329        assert_eq!(small_adj[(0, 1)], -tangent.angle());
1330        assert_eq!(small_adj[(1, 0)], tangent.angle());
1331        assert_eq!(small_adj[(0, 2)], tangent.y());
1332        assert_eq!(small_adj[(1, 2)], -tangent.x());
1333    }
1334
1335    #[test]
1336    fn test_se2_tangent_lie_bracket() {
1337        let tangent_a = SE2Tangent::new(0.1, 0.0, 0.0); // Pure x translation
1338        let tangent_b = SE2Tangent::new(0.0, 0.0, 0.2); // Pure rotation
1339
1340        let bracket_ab = tangent_a.lie_bracket(&tangent_b);
1341        let bracket_ba = tangent_b.lie_bracket(&tangent_a);
1342
1343        // Anti-symmetry test: [a,b] = -[b,a]
1344        assert!((bracket_ab.data + bracket_ba.data).norm() < 1e-10);
1345
1346        // [a,a] = 0
1347        let bracket_aa = tangent_a.lie_bracket(&tangent_a);
1348        assert!(bracket_aa.is_zero(1e-10));
1349
1350        // Verify bracket relationship with hat operator
1351        let bracket_hat = bracket_ab.hat();
1352        let expected = tangent_a.hat() * tangent_b.hat() - tangent_b.hat() * tangent_a.hat();
1353        assert!((bracket_hat - expected).norm() < 1e-10);
1354    }
1355
1356    #[test]
1357    fn test_se2_tangent_is_approx() {
1358        let tangent_1 = SE2Tangent::new(0.1, 0.2, 0.3);
1359        let tangent_2 = SE2Tangent::new(0.1 + 1e-12, 0.2, 0.3);
1360        let tangent_3 = SE2Tangent::new(0.5, 0.6, 0.7);
1361
1362        assert!(tangent_1.is_approx(&tangent_1, 1e-10));
1363        assert!(tangent_1.is_approx(&tangent_2, 1e-10));
1364        assert!(!tangent_1.is_approx(&tangent_3, 1e-10));
1365    }
1366
1367    #[test]
1368    fn test_se2_generators() {
1369        let tangent = SE2Tangent::new(1.0, 1.0, 1.0);
1370
1371        // Test all three generators
1372        for i in 0..3 {
1373            let generator = tangent.generator(i);
1374
1375            // Verify that generators are 3x3 matrices
1376            assert_eq!(generator.nrows(), 3);
1377            assert_eq!(generator.ncols(), 3);
1378        }
1379
1380        // Test specific values for the generators
1381        let e1 = tangent.generator(0); // x translation
1382        let e2 = tangent.generator(1); // y translation
1383        let e3 = tangent.generator(2); // rotation
1384
1385        // Expected generators for SE(2)
1386        let expected_e1 = Matrix3::new(0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0);
1387        let expected_e2 = Matrix3::new(0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0);
1388        let expected_e3 = Matrix3::new(0.0, -1.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0);
1389
1390        assert!((e1 - expected_e1).norm() < 1e-10);
1391        assert!((e2 - expected_e2).norm() < 1e-10);
1392        assert!((e3 - expected_e3).norm() < 1e-10);
1393    }
1394
1395    #[test]
1396    #[should_panic]
1397    fn test_se2_generator_invalid_index() {
1398        let tangent = SE2Tangent::new(1.0, 1.0, 1.0);
1399        let _generator = tangent.generator(3); // Should panic for SE(2)
1400    }
1401
1402    #[test]
1403    fn test_se2_jacobi_identity() {
1404        // Test Jacobi identity: [x,[y,z]]+[y,[z,x]]+[z,[x,y]]=0
1405        let x = SE2Tangent::new(0.1, 0.0, 0.0);
1406        let y = SE2Tangent::new(0.0, 0.2, 0.0);
1407        let z = SE2Tangent::new(0.0, 0.0, 0.3);
1408
1409        let term1 = x.lie_bracket(&y.lie_bracket(&z));
1410        let term2 = y.lie_bracket(&z.lie_bracket(&x));
1411        let term3 = z.lie_bracket(&x.lie_bracket(&y));
1412
1413        let jacobi_sum = SE2Tangent {
1414            data: term1.data + term2.data + term3.data,
1415        };
1416        assert!(jacobi_sum.is_zero(1e-10));
1417    }
1418
1419    #[test]
1420    fn test_se2_hat_vee_consistency() {
1421        let tangent = SE2Tangent::new(0.1, 0.2, 0.3);
1422        let hat_matrix = tangent.hat();
1423
1424        // For SE(2), verify hat matrix structure
1425        // The hat matrix should be 3x3, not 4x4 like SE(3)
1426        assert_eq!(hat_matrix[(0, 2)], tangent.x());
1427        assert_eq!(hat_matrix[(1, 2)], tangent.y());
1428        assert_eq!(hat_matrix[(0, 1)], -tangent.angle());
1429        assert_eq!(hat_matrix[(1, 0)], tangent.angle());
1430        assert_eq!(hat_matrix[(2, 0)], 0.0);
1431        assert_eq!(hat_matrix[(2, 1)], 0.0);
1432        assert_eq!(hat_matrix[(2, 2)], 0.0);
1433    }
1434
1435    // T3: Accumulated Error Tests
1436
1437    #[test]
1438    fn test_se2_circular_path_accumulation() {
1439        // Robot driving in a circle: 360 steps of 1° turn + 1cm forward
1440        let step = SE2::from_xy_angle(0.01, 0.0, (std::f64::consts::PI * 2.0) / 360.0);
1441
1442        let mut pose = SE2::identity();
1443        for _ in 0..360 {
1444            pose = pose.compose(&step, None, None);
1445        }
1446
1447        // Should return near identity (full circle)
1448        assert!(pose.is_approx(&SE2::identity(), 1e-2));
1449    }
1450
1451    // T2: Edge Case Tests
1452
1453    #[test]
1454    fn test_se2_angle_wrap_around() {
1455        // Angles > 2π should wrap correctly
1456        let se2_wrapped = SE2::from_xy_angle(1.0, 2.0, 3.0 * std::f64::consts::PI);
1457        let se2_canonical = SE2::from_xy_angle(1.0, 2.0, std::f64::consts::PI);
1458
1459        // Log and exp should handle wrapping
1460        let tangent_wrapped = se2_wrapped.log(None);
1461        let tangent_canonical = se2_canonical.log(None);
1462
1463        // Angles should be equivalent modulo 2π
1464        let angle_diff = (tangent_wrapped.angle() - tangent_canonical.angle()).abs();
1465        assert!(angle_diff < 1e-10 || (angle_diff - 2.0 * std::f64::consts::PI).abs() < 1e-10);
1466    }
1467
1468    #[test]
1469    fn test_se2_negative_angles() {
1470        let se2_neg = SE2::from_xy_angle(0.5, -0.3, -std::f64::consts::PI / 4.0);
1471        let tangent = se2_neg.log(None);
1472        let recovered = tangent.exp(None);
1473        assert!(se2_neg.is_approx(&recovered, 1e-10));
1474    }
1475
1476    // T4: Jacobian Inverse Identity Tests
1477
1478    #[test]
1479    fn test_se2_right_jacobian_inverse_identity() {
1480        let test_tangents = vec![
1481            SE2Tangent::new(0.1, 0.2, 0.3),
1482            SE2Tangent::new(0.5, -0.3, 1.5),
1483        ];
1484
1485        for tangent in test_tangents {
1486            let jr = tangent.right_jacobian();
1487            let jr_inv = tangent.right_jacobian_inv();
1488            let product = jr * jr_inv;
1489            let identity = Matrix3::identity();
1490
1491            assert!((product - identity).norm() < 1e-10);
1492        }
1493    }
1494
1495    #[test]
1496    fn test_se2_left_jacobian_inverse_identity() {
1497        // Same pattern for left Jacobian
1498        let tangent = SE2Tangent::new(0.2, 0.3, 0.8);
1499        let jl = tangent.left_jacobian();
1500        let jl_inv = tangent.left_jacobian_inv();
1501        let product = jl * jl_inv;
1502
1503        assert!((product - Matrix3::identity()).norm() < 1e-10);
1504    }
1505
1506    #[test]
1507    fn test_se2_tangent_from_components() {
1508        let t1 = SE2Tangent::new(1.0, 2.0, 0.5);
1509        let t2 = SE2Tangent::from_components(1.0, 2.0, 0.5);
1510        assert!(t1.is_approx(&t2, 1e-15));
1511        assert_eq!(t2.x(), 1.0);
1512        assert_eq!(t2.y(), 2.0);
1513        assert_eq!(t2.angle(), 0.5);
1514    }
1515
1516    #[test]
1517    fn test_se2_small_angle_threshold_exp_log() {
1518        // Test angles near the SMALL_ANGLE_THRESHOLD boundary round-trip correctly
1519        // sqrt(1e-10) ≈ 1e-5 is the effective angle threshold
1520        let near_threshold_angles = [1e-6, 1e-5, 1e-4, 1e-3];
1521
1522        for &angle in &near_threshold_angles {
1523            let tangent = SE2Tangent::new(1.0, 2.0, angle);
1524            let se2 = tangent.exp(None);
1525            let recovered = se2.log(None);
1526            assert!(
1527                (tangent.x() - recovered.x()).abs() < 1e-10
1528                    && (tangent.y() - recovered.y()).abs() < 1e-10
1529                    && (tangent.angle() - recovered.angle()).abs() < 1e-10,
1530                "SE2 exp-log round-trip failed for angle = {angle}"
1531            );
1532        }
1533    }
1534
1535    #[test]
1536    fn se2_param_slice_round_trip() {
1537        let g = SE2::random();
1538        let recovered = SE2::from_param_slice(g.as_param_slice());
1539        assert!(g.is_approx(&recovered, 1e-14));
1540    }
1541
1542    #[test]
1543    fn se2_tangent_slice_round_trip() {
1544        let t = SE2Tangent::random();
1545        let recovered = SE2Tangent::from_slice(t.as_slice());
1546        assert!(t.is_approx(&recovered, 1e-14));
1547    }
1548}