Skip to main content

apex_manifolds/
so2.rs

1//! SO(2) - Special Orthogonal Group in 2D
2//!
3//! This module implements the Special Orthogonal group SO(2), which represents
4//! rotations in 2D space.
5//!
6//! SO(2) elements are represented using nalgebra's UnitComplex internally.
7//! SO(2) tangent elements are represented as a single angle in radians.
8//!
9//! The implementation follows the [manif](https://github.com/artivis/manif) C++ library
10//! conventions and provides all operations required by the LieGroup and Tangent traits.
11
12use crate::{LieGroup, Tangent};
13use nalgebra::{DVector, Matrix1, Matrix2, Matrix3, UnitComplex, Vector2, Vector3};
14use std::{
15    fmt,
16    fmt::{Display, Formatter},
17};
18
19/// SO(2) group element representing rotations in 2D.
20///
21/// Internally represented using nalgebra's UnitComplex<f64> for efficient rotations.
22#[derive(Clone, PartialEq)]
23pub struct SO2 {
24    /// Internal representation as a unit complex number
25    complex: UnitComplex<f64>,
26}
27
28impl Display for SO2 {
29    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
30        write!(f, "SO2(angle: {:.4})", self.complex.angle())
31    }
32}
33
34// Conversion traits for integration with generic Problem
35impl From<DVector<f64>> for SO2 {
36    fn from(data: DVector<f64>) -> Self {
37        SO2::from_angle(data[0])
38    }
39}
40
41impl From<SO2> for DVector<f64> {
42    fn from(so2: SO2) -> Self {
43        DVector::from_vec(vec![so2.complex.angle()])
44    }
45}
46
47impl SO2 {
48    /// Space dimension - dimension of the ambient space that the group acts on
49    pub const DIM: usize = 2;
50
51    /// Degrees of freedom - dimension of the tangent space
52    pub const DOF: usize = 1;
53
54    /// Representation size - size of the underlying data representation
55    pub const REP_SIZE: usize = 2;
56
57    /// Get the identity element of the group.
58    ///
59    /// Returns the neutral element e such that e ∘ g = g ∘ e = g for any group element g.
60    pub fn identity() -> Self {
61        SO2 {
62            complex: UnitComplex::identity(),
63        }
64    }
65
66    /// Get the identity matrix for Jacobians.
67    ///
68    /// Returns the identity matrix in the appropriate dimension for Jacobian computations.
69    pub fn jacobian_identity() -> Matrix1<f64> {
70        Matrix1::<f64>::identity()
71    }
72
73    /// Create a new SO(2) element from a unit complex number.
74    ///
75    /// # Arguments
76    /// * `complex` - Unit complex number representing rotation
77    #[inline]
78    pub fn new(complex: UnitComplex<f64>) -> Self {
79        SO2 { complex }
80    }
81
82    /// Create SO(2) from an angle.
83    ///
84    /// # Arguments
85    /// * `angle` - Rotation angle in radians
86    pub fn from_angle(angle: f64) -> Self {
87        SO2::new(UnitComplex::from_angle(angle))
88    }
89
90    /// Get the underlying unit complex number.
91    pub fn complex(&self) -> UnitComplex<f64> {
92        self.complex
93    }
94
95    /// Get the rotation angle in radians.
96    #[inline]
97    pub fn angle(&self) -> f64 {
98        self.complex.angle()
99    }
100
101    /// Get the rotation matrix (2x2).
102    pub fn rotation_matrix(&self) -> Matrix2<f64> {
103        self.complex.to_rotation_matrix().into_inner()
104    }
105}
106
107impl LieGroup for SO2 {
108    type TangentVector = SO2Tangent;
109    type JacobianMatrix = Matrix1<f64>;
110    type LieAlgebra = Matrix2<f64>;
111
112    /// SO2 inverse.
113    ///
114    /// # Arguments
115    /// * `jacobian` - Optional Jacobian matrix of the inverse wrt self.
116    ///
117    /// # Notes
118    /// # Equation 118: SO(2) Inverse
119    /// R(θ)⁻¹ = R(-θ)
120    ///
121    /// # Equation 124: Jacobian of Inverse for SO(2)
122    /// J_R⁻¹_R = -I
123    fn inverse(&self, jacobian: Option<&mut Self::JacobianMatrix>) -> Self {
124        if let Some(jac) = jacobian {
125            *jac = -self.adjoint();
126        }
127        SO2 {
128            complex: self.complex.inverse(),
129        }
130    }
131
132    /// SO2 composition.
133    ///
134    /// # Arguments
135    /// * `other` - Another SO2 element.
136    /// * `jacobian_self` - Optional Jacobian matrix of the composition wrt self.
137    /// * `jacobian_other` - Optional Jacobian matrix of the composition wrt other.
138    ///
139    /// # Notes
140    /// # Equation 125: Jacobian of Composition for SO(2)
141    /// J_C_A = I
142    /// J_C_B = I
143    fn compose(
144        &self,
145        other: &Self,
146        jacobian_self: Option<&mut Self::JacobianMatrix>,
147        jacobian_other: Option<&mut Self::JacobianMatrix>,
148    ) -> Self {
149        if let Some(jac_self) = jacobian_self {
150            *jac_self = other.inverse(None).adjoint();
151        }
152        if let Some(jac_other) = jacobian_other {
153            *jac_other = Matrix1::identity();
154        }
155        SO2 {
156            complex: self.complex * other.complex,
157        }
158    }
159
160    /// Get the SO2 corresponding Lie algebra element in vector form.
161    ///
162    /// # Arguments
163    /// * `jacobian` - Optional Jacobian matrix of the tangent wrt to self.
164    ///
165    /// # Notes
166    /// # Equation 115: Logarithmic map for SO(2)
167    /// θ = atan2(R(1,0), R(0,0))
168    ///
169    /// # Equation 126: Jacobian of Logarithmic map for SO(2)
170    /// J_log_R = I
171    fn log(&self, jacobian: Option<&mut Self::JacobianMatrix>) -> Self::TangentVector {
172        if let Some(jac) = jacobian {
173            *jac = Matrix1::identity();
174        }
175        SO2Tangent {
176            data: self.complex.angle(),
177        }
178    }
179
180    /// Rotation action on a 3-vector.
181    ///
182    /// # Arguments
183    /// * `v` - A 3-vector.
184    /// * `jacobian_self` - Optional Jacobian of the new object wrt this.
185    /// * `jacobian_vector` - Optional Jacobian of the new object wrt input object.
186    ///
187    /// # Returns
188    /// The rotated 3-vector.
189    ///
190    /// # Notes
191    /// This is a convenience function that treats the 3D vector as a 2D vector and ignores the z component.
192    fn act(
193        &self,
194        vector: &Vector3<f64>,
195        _jacobian_self: Option<&mut Self::JacobianMatrix>,
196        _jacobian_vector: Option<&mut Matrix3<f64>>,
197    ) -> Vector3<f64> {
198        let point2d = Vector2::new(vector.x, vector.y);
199        let rotated_point = self.complex * point2d;
200        Vector3::new(rotated_point.x, rotated_point.y, vector.z)
201    }
202
203    /// Get the adjoint matrix of SO2 at this.
204    ///
205    /// # Notes
206    /// See Eq. (123).
207    fn adjoint(&self) -> Self::JacobianMatrix {
208        Matrix1::identity()
209    }
210
211    /// Generate a random element.
212    fn random() -> Self {
213        SO2::from_angle(rand::random::<f64>() * 2.0 * std::f64::consts::PI)
214    }
215
216    fn jacobian_identity() -> Self::JacobianMatrix {
217        Matrix1::<f64>::identity()
218    }
219
220    fn zero_jacobian() -> Self::JacobianMatrix {
221        Matrix1::<f64>::zeros()
222    }
223
224    /// Normalize the underlying complex number.
225    fn normalize(&mut self) {
226        self.complex.renormalize();
227    }
228
229    /// Check if the element is valid.
230    fn is_valid(&self, tolerance: f64) -> bool {
231        let norm_diff = (self.complex.norm() - 1.0).abs();
232        norm_diff < tolerance
233    }
234
235    /// Vee operator: log(g)^∨.
236    ///
237    /// Maps a group element g ∈ G to its tangent vector log(g)^∨ ∈ 𝔤.
238    /// For SO(2), this is the same as log().
239    fn vee(&self) -> Self::TangentVector {
240        self.log(None)
241    }
242
243    /// Check if the element is approximately equal to another element.
244    ///
245    /// # Arguments
246    /// * `other` - The other element to compare with
247    /// * `tolerance` - The tolerance for the comparison
248    fn is_approx(&self, other: &Self, tolerance: f64) -> bool {
249        let difference = self.right_minus(other, None, None);
250        difference.is_zero(tolerance)
251    }
252}
253
254/// SO(2) tangent space element representing elements in the Lie algebra so(2).
255///
256/// Internally represented as a single scalar (angle in radians).
257#[derive(Clone, PartialEq)]
258pub struct SO2Tangent {
259    /// Internal data: angle (radians)
260    data: f64,
261}
262
263impl fmt::Display for SO2Tangent {
264    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
265        write!(f, "so2(angle: {:.4})", self.data)
266    }
267}
268
269// Conversion traits for integration with generic Problem
270impl From<DVector<f64>> for SO2Tangent {
271    fn from(data_vector: DVector<f64>) -> Self {
272        SO2Tangent {
273            data: data_vector[0],
274        }
275    }
276}
277
278impl From<SO2Tangent> for DVector<f64> {
279    fn from(so2_tangent: SO2Tangent) -> Self {
280        DVector::from_vec(vec![so2_tangent.data])
281    }
282}
283
284impl SO2Tangent {
285    /// Create a new SO2Tangent from an angle.
286    ///
287    /// # Arguments
288    /// * `angle` - Angle in radians
289    #[inline]
290    pub fn new(angle: f64) -> Self {
291        SO2Tangent { data: angle }
292    }
293
294    /// Get the angle.
295    #[inline]
296    pub fn angle(&self) -> f64 {
297        self.data
298    }
299}
300
301impl Tangent<SO2> for SO2Tangent {
302    /// Dimension of the tangent space
303    const DIM: usize = 1;
304
305    /// SO2 exponential map.
306    ///
307    /// # Arguments
308    /// * `tangent` - Tangent vector (angle)
309    /// * `jacobian` - Optional Jacobian matrix of the SE(3) element wrt this.
310    fn exp(&self, jacobian: Option<&mut <SO2 as LieGroup>::JacobianMatrix>) -> SO2 {
311        let angle = self.angle();
312        let complex = UnitComplex::new(angle);
313
314        if let Some(jac) = jacobian {
315            *jac = Matrix1::identity();
316        }
317
318        SO2::new(complex)
319    }
320
321    /// Right Jacobian for SO(2) is identity.
322    fn right_jacobian(&self) -> <SO2 as LieGroup>::JacobianMatrix {
323        Matrix1::identity()
324    }
325
326    /// Left Jacobian for SO(2) is identity.
327    fn left_jacobian(&self) -> <SO2 as LieGroup>::JacobianMatrix {
328        Matrix1::identity()
329    }
330
331    /// Inverse of right Jacobian for SO(2) is identity.
332    fn right_jacobian_inv(&self) -> <SO2 as LieGroup>::JacobianMatrix {
333        Matrix1::identity()
334    }
335
336    /// Inverse of left Jacobian for SO(2) is identity.
337    fn left_jacobian_inv(&self) -> <SO2 as LieGroup>::JacobianMatrix {
338        Matrix1::identity()
339    }
340
341    /// Hat operator: θ^∧ (scalar to skew-symmetric matrix).
342    fn hat(&self) -> <SO2 as LieGroup>::LieAlgebra {
343        let theta = self.data;
344        Matrix2::new(0.0, -theta, theta, 0.0)
345    }
346
347    /// Zero tangent vector for SO2
348    fn zero() -> Self {
349        SO2Tangent { data: 0.0 }
350    }
351
352    /// Random tangent vector for SO2
353    fn random() -> Self {
354        SO2Tangent {
355            data: rand::random::<f64>() * 0.2 - 0.1,
356        }
357    }
358
359    /// Check if tangent vector is zero
360    fn is_zero(&self, tolerance: f64) -> bool {
361        self.data.abs() < tolerance
362    }
363
364    /// Normalize tangent vector
365    fn normalize(&mut self) {
366        // Normalizing a scalar doesn't make much sense unless it's a direction.
367        // For an angle, this is a no-op.
368    }
369
370    /// Return a unit tangent vector in the same direction.
371    fn normalized(&self) -> Self {
372        if self.data.abs() > f64::EPSILON {
373            SO2Tangent::new(self.data.signum())
374        } else {
375            SO2Tangent::new(0.0)
376        }
377    }
378
379    /// Small adjoint matrix for SO(2).
380    ///
381    /// For SO(2), the small adjoint is zero (since it's commutative).
382    fn small_adj(&self) -> <SO2 as LieGroup>::JacobianMatrix {
383        Matrix1::zeros()
384    }
385
386    /// Lie bracket for SO(2).
387    ///
388    /// For SO(2), the Lie bracket is always zero since it's commutative.
389    fn lie_bracket(&self, _other: &Self) -> <SO2 as LieGroup>::TangentVector {
390        SO2Tangent::zero()
391    }
392
393    /// Check if this tangent vector is approximately equal to another.
394    ///
395    /// # Arguments
396    /// * `other` - The other tangent vector to compare with
397    /// * `tolerance` - The tolerance for the comparison
398    fn is_approx(&self, other: &Self, tolerance: f64) -> bool {
399        (self.data - other.data).abs() < tolerance
400    }
401
402    /// Get the ith generator of the SO(2) Lie algebra.
403    ///
404    /// # Arguments
405    /// * `i` - Index of the generator (must be 0 for SO(2))
406    ///
407    /// # Returns
408    /// The generator matrix
409    fn generator(&self, i: usize) -> <SO2 as LieGroup>::LieAlgebra {
410        assert_eq!(i, 0, "SO(2) only has one generator (index 0)");
411        Matrix2::new(0.0, -1.0, 1.0, 0.0)
412    }
413}
414
415#[cfg(test)]
416mod tests {
417    use super::*;
418    use nalgebra::{DMatrix, DVector};
419    use std::f64::consts::PI;
420
421    const TOLERANCE: f64 = 1e-12;
422
423    /// Numerically compute Jacobian using central difference
424    fn numerical_jacobian<F>(
425        func: F,
426        point: &DVector<f64>,
427        output_dim: usize,
428        epsilon: f64,
429    ) -> DMatrix<f64>
430    where
431        F: Fn(&DVector<f64>) -> DVector<f64>,
432    {
433        let input_dim = point.len();
434        let mut jacobian = DMatrix::zeros(output_dim, input_dim);
435
436        for i in 0..input_dim {
437            let mut point_plus = point.clone();
438            let mut point_minus = point.clone();
439            point_plus[i] += epsilon;
440            point_minus[i] -= epsilon;
441
442            let output_plus = func(&point_plus);
443            let output_minus = func(&point_minus);
444            let derivative = (output_plus - output_minus) / (2.0 * epsilon);
445
446            jacobian.set_column(i, &derivative);
447        }
448
449        jacobian
450    }
451
452    #[test]
453    fn test_so2_identity() {
454        let so2 = SO2::identity();
455        assert!((so2.angle() - 0.0).abs() < TOLERANCE);
456    }
457
458    #[test]
459    fn test_so2_inverse() {
460        let so2 = SO2::from_angle(PI / 4.0);
461        let so2_inv = so2.inverse(None);
462        assert!((so2_inv.angle() + PI / 4.0).abs() < TOLERANCE);
463    }
464
465    #[test]
466    fn test_so2_compose() {
467        let so2_a = SO2::from_angle(PI / 4.0);
468        let so2_b = SO2::from_angle(PI / 2.0);
469        let composed = so2_a.compose(&so2_b, None, None);
470        assert!((composed.angle() - (3.0 * PI / 4.0)).abs() < TOLERANCE);
471    }
472
473    #[test]
474    fn test_so2_exp_log_consistency() {
475        let angle = PI / 4.0;
476        let tangent = SO2Tangent::new(angle);
477        let so2 = tangent.exp(None);
478        let recovered_tangent = so2.log(None);
479
480        assert!((tangent.angle() - recovered_tangent.angle()).abs() < 1e-10);
481    }
482
483    // New tests for the additional functions
484
485    #[test]
486    fn test_so2_vee() {
487        let so2 = SO2::from_angle(PI / 3.0);
488        let tangent_log = so2.log(None);
489        let tangent_vee = so2.vee();
490
491        assert!((tangent_log.angle() - tangent_vee.angle()).abs() < 1e-10);
492    }
493
494    #[test]
495    fn test_so2_is_approx() {
496        let so2_1 = SO2::from_angle(PI / 4.0);
497        let so2_2 = SO2::from_angle(PI / 4.0 + 1e-12);
498        let so2_3 = SO2::from_angle(PI / 2.0);
499
500        assert!(so2_1.is_approx(&so2_1, 1e-10));
501        assert!(so2_1.is_approx(&so2_2, 1e-10));
502        assert!(!so2_1.is_approx(&so2_3, 1e-10));
503    }
504
505    #[test]
506    fn test_so2_tangent_small_adj() {
507        let tangent = SO2Tangent::new(PI / 6.0);
508        let small_adj = tangent.small_adj();
509
510        // For SO(2), small adjoint should be zero (commutative group)
511        assert!((small_adj[(0, 0)]).abs() < 1e-10);
512    }
513
514    #[test]
515    fn test_so2_tangent_lie_bracket() {
516        let tangent_a = SO2Tangent::new(0.1);
517        let tangent_b = SO2Tangent::new(0.2);
518
519        let bracket = tangent_a.lie_bracket(&tangent_b);
520
521        // For SO(2), Lie bracket should be zero (commutative group)
522        assert!(bracket.is_zero(1e-10));
523
524        // Anti-symmetry test: [a,b] = -[b,a]
525        let bracket_ba = tangent_b.lie_bracket(&tangent_a);
526        assert!(bracket.lie_bracket(&tangent_b).is_zero(1e-10)); // [a,a] = 0
527
528        // Since SO(2) is commutative, both should be zero
529        assert!(bracket_ba.is_zero(1e-10));
530    }
531
532    #[test]
533    fn test_so2_tangent_is_approx() {
534        let tangent_1 = SO2Tangent::new(0.5);
535        let tangent_2 = SO2Tangent::new(0.5 + 1e-12);
536        let tangent_3 = SO2Tangent::new(1.0);
537
538        assert!(tangent_1.is_approx(&tangent_1, 1e-10));
539        assert!(tangent_1.is_approx(&tangent_2, 1e-10));
540        assert!(!tangent_1.is_approx(&tangent_3, 1e-10));
541    }
542
543    #[test]
544    fn test_so2_generator() {
545        let tangent = SO2Tangent::new(1.0);
546        let generator = tangent.generator(0);
547
548        // SO(2) generator should be the skew-symmetric matrix
549        let expected = Matrix2::new(0.0, -1.0, 1.0, 0.0);
550
551        assert!((generator - expected).norm() < 1e-10);
552    }
553
554    #[test]
555    #[should_panic]
556    fn test_so2_generator_invalid_index() {
557        let tangent = SO2Tangent::new(1.0);
558        let _generator = tangent.generator(1); // Should panic for SO(2)
559    }
560
561    #[test]
562    fn test_so2_bracket_hat_relationship() {
563        let a = SO2Tangent::new(0.1);
564        let b = SO2Tangent::new(0.2);
565
566        // For SO(2): [a,b]^ = a^ * b^ - b^ * a^ should be zero (commutative)
567        let bracket_hat = a.lie_bracket(&b).hat();
568        let expected = a.hat() * b.hat() - b.hat() * a.hat();
569
570        assert!((bracket_hat - expected).norm() < 1e-10);
571        assert!(expected.norm() < 1e-10); // Should be zero for SO(2)
572    }
573
574    // T1: Numerical Jacobian Verification Tests
575
576    #[test]
577    fn test_so2_right_jacobian_numerical() {
578        let epsilon = 1e-7;
579        let tolerance = 1e-4;
580
581        let tangent = SO2Tangent::new(0.5);
582        let jr_analytical = tangent.right_jacobian();
583
584        // Numerical Jacobian through exp-log round trip
585        let angle_vec = DVector::from_vec(vec![tangent.angle()]);
586        let jr_numerical = numerical_jacobian(
587            |theta| {
588                let tang = SO2Tangent::new(theta[0]);
589                let so2 = tang.exp(None);
590                let log_result = so2.log(None);
591                DVector::from_vec(vec![log_result.angle()])
592            },
593            &angle_vec,
594            1,
595            epsilon,
596        );
597
598        assert!(
599            (jr_analytical - &jr_numerical).norm() < tolerance,
600            "Right Jacobian mismatch: analytical = {}, numerical = {}",
601            jr_analytical,
602            jr_numerical
603        );
604    }
605
606    #[test]
607    fn test_so2_left_jacobian_numerical() {
608        let epsilon = 1e-7;
609        let tolerance = 1e-4;
610
611        let tangent = SO2Tangent::new(0.5);
612        let jl_analytical = tangent.left_jacobian();
613
614        let angle_vec = DVector::from_vec(vec![tangent.angle()]);
615        let jl_numerical = numerical_jacobian(
616            |theta| {
617                let tang = SO2Tangent::new(theta[0]);
618                let so2 = tang.exp(None);
619                let log_result = so2.log(None);
620                DVector::from_vec(vec![log_result.angle()])
621            },
622            &angle_vec,
623            1,
624            epsilon,
625        );
626
627        assert!((jl_analytical - jl_numerical).norm() < tolerance);
628    }
629
630    // T4: Jacobian Inverse Identity Tests
631
632    #[test]
633    fn test_so2_jacobian_inverse_identity() {
634        // SO2 Jacobians are scalars (1x1 matrices)
635        let tangent = SO2Tangent::new(0.5);
636        let jr = tangent.right_jacobian();
637        let jr_inv = tangent.right_jacobian_inv();
638        let product = jr * jr_inv;
639
640        assert!((product[(0, 0)] - 1.0).abs() < 1e-10);
641
642        // Also test left Jacobian
643        let jl = tangent.left_jacobian();
644        let jl_inv = tangent.left_jacobian_inv();
645        let product_left = jl * jl_inv;
646
647        assert!((product_left[(0, 0)] - 1.0).abs() < 1e-10);
648    }
649}