1use crate::{LieGroup, Tangent};
14use nalgebra::{Matrix1, Matrix2, Matrix3, UnitComplex, Vector2, Vector3};
15use std::{
16 fmt,
17 fmt::{Display, Formatter},
18};
19
20#[derive(Clone, PartialEq)]
25pub struct SO2 {
26 theta: f64,
28}
29
30impl Display for SO2 {
31 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
32 write!(f, "SO2(angle: {:.4})", self.theta)
33 }
34}
35
36impl SO2 {
37 pub const DIM: usize = 2;
39
40 pub const DOF: usize = 1;
42
43 pub const REP_SIZE: usize = 1;
45
46 pub fn identity() -> Self {
48 SO2 { theta: 0.0 }
49 }
50
51 pub fn jacobian_identity() -> Matrix1<f64> {
53 Matrix1::<f64>::identity()
54 }
55
56 #[inline]
58 pub fn new(complex: UnitComplex<f64>) -> Self {
59 SO2 {
60 theta: complex.angle(),
61 }
62 }
63
64 pub fn from_angle(angle: f64) -> Self {
66 SO2 { theta: angle }
67 }
68
69 #[inline]
71 fn unit_complex(&self) -> UnitComplex<f64> {
72 UnitComplex::new(self.theta)
73 }
74
75 pub fn complex(&self) -> UnitComplex<f64> {
77 self.unit_complex()
78 }
79
80 #[inline]
82 pub fn angle(&self) -> f64 {
83 self.theta
84 }
85
86 pub fn rotation_matrix(&self) -> Matrix2<f64> {
88 self.unit_complex().to_rotation_matrix().into_inner()
89 }
90}
91
92impl LieGroup for SO2 {
93 const NAME: &'static str = "SO2";
94
95 type TangentVector = SO2Tangent;
96 type JacobianMatrix = Matrix1<f64>;
97 type LieAlgebra = Matrix2<f64>;
98
99 fn inverse(&self, jacobian: Option<&mut Self::JacobianMatrix>) -> Self {
101 if let Some(jac) = jacobian {
102 *jac = -self.adjoint();
103 }
104 SO2 { theta: -self.theta }
105 }
106
107 fn compose(
109 &self,
110 other: &Self,
111 jacobian_self: Option<&mut Self::JacobianMatrix>,
112 jacobian_other: Option<&mut Self::JacobianMatrix>,
113 ) -> Self {
114 if let Some(jac_self) = jacobian_self {
115 *jac_self = other.inverse(None).adjoint();
116 }
117 if let Some(jac_other) = jacobian_other {
118 *jac_other = Matrix1::identity();
119 }
120 SO2 {
121 theta: (self.unit_complex() * other.unit_complex()).angle(),
122 }
123 }
124
125 fn log(&self, jacobian: Option<&mut Self::JacobianMatrix>) -> Self::TangentVector {
127 if let Some(jac) = jacobian {
128 *jac = Matrix1::identity();
129 }
130 SO2Tangent {
131 data: self.unit_complex().angle(),
132 }
133 }
134
135 fn act(
137 &self,
138 vector: &Vector3<f64>,
139 _jacobian_self: Option<&mut Self::JacobianMatrix>,
140 _jacobian_vector: Option<&mut Matrix3<f64>>,
141 ) -> Vector3<f64> {
142 let point2d = Vector2::new(vector.x, vector.y);
143 let rotated = self.unit_complex() * point2d;
144 Vector3::new(rotated.x, rotated.y, vector.z)
145 }
146
147 fn adjoint(&self) -> Self::JacobianMatrix {
149 Matrix1::identity()
150 }
151
152 fn random() -> Self {
153 SO2::from_angle(rand::random::<f64>() * 2.0 * std::f64::consts::PI)
154 }
155
156 fn jacobian_identity() -> Self::JacobianMatrix {
157 Matrix1::<f64>::identity()
158 }
159
160 fn zero_jacobian() -> Self::JacobianMatrix {
161 Matrix1::<f64>::zeros()
162 }
163
164 fn normalize(&mut self) {
166 self.theta = self.unit_complex().angle();
167 }
168
169 fn is_valid(&self, _tolerance: f64) -> bool {
171 self.theta.is_finite()
172 }
173
174 fn vee(&self) -> Self::TangentVector {
175 self.log(None)
176 }
177
178 fn is_approx(&self, other: &Self, tolerance: f64) -> bool {
179 let difference = self.right_minus(other, None, None);
180 difference.is_zero(tolerance)
181 }
182
183 fn as_param_slice(&self) -> &[f64] {
184 std::slice::from_ref(&self.theta)
185 }
186
187 fn as_param_slice_mut(&mut self) -> &mut [f64] {
188 std::slice::from_mut(&mut self.theta)
189 }
190
191 fn from_param_slice(s: &[f64]) -> Self {
192 debug_assert_eq!(s.len(), 1);
193 SO2 { theta: s[0] }
194 }
195}
196
197#[derive(Clone, PartialEq)]
201pub struct SO2Tangent {
202 data: f64,
204}
205
206impl fmt::Display for SO2Tangent {
207 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
208 write!(f, "so2(angle: {:.4})", self.data)
209 }
210}
211
212impl SO2Tangent {
213 #[inline]
218 pub fn new(angle: f64) -> Self {
219 SO2Tangent { data: angle }
220 }
221
222 #[inline]
224 pub fn angle(&self) -> f64 {
225 self.data
226 }
227}
228
229impl Tangent<SO2> for SO2Tangent {
230 const DIM: usize = 1;
232
233 fn exp(&self, jacobian: Option<&mut <SO2 as LieGroup>::JacobianMatrix>) -> SO2 {
239 let angle = self.angle();
240 let complex = UnitComplex::new(angle);
241
242 if let Some(jac) = jacobian {
243 *jac = Matrix1::identity();
244 }
245
246 SO2::new(complex)
247 }
248
249 fn right_jacobian(&self) -> <SO2 as LieGroup>::JacobianMatrix {
251 Matrix1::identity()
252 }
253
254 fn left_jacobian(&self) -> <SO2 as LieGroup>::JacobianMatrix {
256 Matrix1::identity()
257 }
258
259 fn right_jacobian_inv(&self) -> <SO2 as LieGroup>::JacobianMatrix {
261 Matrix1::identity()
262 }
263
264 fn left_jacobian_inv(&self) -> <SO2 as LieGroup>::JacobianMatrix {
266 Matrix1::identity()
267 }
268
269 fn hat(&self) -> <SO2 as LieGroup>::LieAlgebra {
271 let theta = self.data;
272 Matrix2::new(0.0, -theta, theta, 0.0)
273 }
274
275 fn zero() -> Self {
277 SO2Tangent { data: 0.0 }
278 }
279
280 fn random() -> Self {
282 SO2Tangent {
283 data: rand::random::<f64>() * 0.2 - 0.1,
284 }
285 }
286
287 fn is_zero(&self, tolerance: f64) -> bool {
289 self.data.abs() < tolerance
290 }
291
292 fn normalize(&mut self) {
294 }
297
298 fn normalized(&self) -> Self {
300 if self.data.abs() > f64::EPSILON {
301 SO2Tangent::new(self.data.signum())
302 } else {
303 SO2Tangent::new(0.0)
304 }
305 }
306
307 fn as_slice(&self) -> &[f64] {
308 std::slice::from_ref(&self.data)
309 }
310
311 fn from_slice(s: &[f64]) -> Self {
312 debug_assert_eq!(s.len(), 1);
313 SO2Tangent { data: s[0] }
314 }
315
316 fn small_adj(&self) -> <SO2 as LieGroup>::JacobianMatrix {
320 Matrix1::zeros()
321 }
322
323 fn lie_bracket(&self, _other: &Self) -> <SO2 as LieGroup>::TangentVector {
327 SO2Tangent::zero()
328 }
329
330 fn is_approx(&self, other: &Self, tolerance: f64) -> bool {
336 (self.data - other.data).abs() < tolerance
337 }
338
339 fn generator(&self, i: usize) -> <SO2 as LieGroup>::LieAlgebra {
347 assert_eq!(i, 0, "SO(2) only has one generator (index 0)");
348 Matrix2::new(0.0, -1.0, 1.0, 0.0)
349 }
350}
351
352#[cfg(test)]
353mod tests {
354 use super::*;
355 use nalgebra::{DMatrix, DVector};
356 use std::f64::consts::PI;
357
358 const TOLERANCE: f64 = 1e-12;
359
360 fn numerical_jacobian<F>(
362 func: F,
363 point: &DVector<f64>,
364 output_dim: usize,
365 epsilon: f64,
366 ) -> DMatrix<f64>
367 where
368 F: Fn(&DVector<f64>) -> DVector<f64>,
369 {
370 let input_dim = point.len();
371 let mut jacobian = DMatrix::zeros(output_dim, input_dim);
372
373 for i in 0..input_dim {
374 let mut point_plus = point.clone();
375 let mut point_minus = point.clone();
376 point_plus[i] += epsilon;
377 point_minus[i] -= epsilon;
378
379 let output_plus = func(&point_plus);
380 let output_minus = func(&point_minus);
381 let derivative = (output_plus - output_minus) / (2.0 * epsilon);
382
383 jacobian.set_column(i, &derivative);
384 }
385
386 jacobian
387 }
388
389 #[test]
390 fn test_so2_identity() {
391 let so2 = SO2::identity();
392 assert!((so2.angle() - 0.0).abs() < TOLERANCE);
393 }
394
395 #[test]
396 fn test_so2_inverse() {
397 let so2 = SO2::from_angle(PI / 4.0);
398 let so2_inv = so2.inverse(None);
399 assert!((so2_inv.angle() + PI / 4.0).abs() < TOLERANCE);
400 }
401
402 #[test]
403 fn test_so2_compose() {
404 let so2_a = SO2::from_angle(PI / 4.0);
405 let so2_b = SO2::from_angle(PI / 2.0);
406 let composed = so2_a.compose(&so2_b, None, None);
407 assert!((composed.angle() - (3.0 * PI / 4.0)).abs() < TOLERANCE);
408 }
409
410 #[test]
411 fn test_so2_exp_log_consistency() {
412 let angle = PI / 4.0;
413 let tangent = SO2Tangent::new(angle);
414 let so2 = tangent.exp(None);
415 let recovered_tangent = so2.log(None);
416
417 assert!((tangent.angle() - recovered_tangent.angle()).abs() < 1e-10);
418 }
419
420 #[test]
423 fn test_so2_vee() {
424 let so2 = SO2::from_angle(PI / 3.0);
425 let tangent_log = so2.log(None);
426 let tangent_vee = so2.vee();
427
428 assert!((tangent_log.angle() - tangent_vee.angle()).abs() < 1e-10);
429 }
430
431 #[test]
432 fn test_so2_is_approx() {
433 let so2_1 = SO2::from_angle(PI / 4.0);
434 let so2_2 = SO2::from_angle(PI / 4.0 + 1e-12);
435 let so2_3 = SO2::from_angle(PI / 2.0);
436
437 assert!(so2_1.is_approx(&so2_1, 1e-10));
438 assert!(so2_1.is_approx(&so2_2, 1e-10));
439 assert!(!so2_1.is_approx(&so2_3, 1e-10));
440 }
441
442 #[test]
443 fn test_so2_tangent_small_adj() {
444 let tangent = SO2Tangent::new(PI / 6.0);
445 let small_adj = tangent.small_adj();
446
447 assert!((small_adj[(0, 0)]).abs() < 1e-10);
449 }
450
451 #[test]
452 fn test_so2_tangent_lie_bracket() {
453 let tangent_a = SO2Tangent::new(0.1);
454 let tangent_b = SO2Tangent::new(0.2);
455
456 let bracket = tangent_a.lie_bracket(&tangent_b);
457
458 assert!(bracket.is_zero(1e-10));
460
461 let bracket_ba = tangent_b.lie_bracket(&tangent_a);
463 assert!(bracket.lie_bracket(&tangent_b).is_zero(1e-10)); assert!(bracket_ba.is_zero(1e-10));
467 }
468
469 #[test]
470 fn test_so2_tangent_is_approx() {
471 let tangent_1 = SO2Tangent::new(0.5);
472 let tangent_2 = SO2Tangent::new(0.5 + 1e-12);
473 let tangent_3 = SO2Tangent::new(1.0);
474
475 assert!(tangent_1.is_approx(&tangent_1, 1e-10));
476 assert!(tangent_1.is_approx(&tangent_2, 1e-10));
477 assert!(!tangent_1.is_approx(&tangent_3, 1e-10));
478 }
479
480 #[test]
481 fn test_so2_generator() {
482 let tangent = SO2Tangent::new(1.0);
483 let generator = tangent.generator(0);
484
485 let expected = Matrix2::new(0.0, -1.0, 1.0, 0.0);
487
488 assert!((generator - expected).norm() < 1e-10);
489 }
490
491 #[test]
492 #[should_panic]
493 fn test_so2_generator_invalid_index() {
494 let tangent = SO2Tangent::new(1.0);
495 let _generator = tangent.generator(1); }
497
498 #[test]
499 fn test_so2_bracket_hat_relationship() {
500 let a = SO2Tangent::new(0.1);
501 let b = SO2Tangent::new(0.2);
502
503 let bracket_hat = a.lie_bracket(&b).hat();
505 let expected = a.hat() * b.hat() - b.hat() * a.hat();
506
507 assert!((bracket_hat - expected).norm() < 1e-10);
508 assert!(expected.norm() < 1e-10); }
510
511 #[test]
512 fn test_so2_right_jacobian_numerical() {
513 let epsilon = 1e-7;
514 let tolerance = 1e-4;
515
516 let tangent = SO2Tangent::new(0.5);
517 let jr_analytical = tangent.right_jacobian();
518
519 let angle_vec = DVector::from_vec(vec![tangent.angle()]);
521 let jr_numerical = numerical_jacobian(
522 |theta| {
523 let tang = SO2Tangent::new(theta[0]);
524 let so2 = tang.exp(None);
525 let log_result = so2.log(None);
526 DVector::from_vec(vec![log_result.angle()])
527 },
528 &angle_vec,
529 1,
530 epsilon,
531 );
532
533 assert!(
534 (jr_analytical - &jr_numerical).norm() < tolerance,
535 "Right Jacobian mismatch: analytical = {}, numerical = {}",
536 jr_analytical,
537 jr_numerical
538 );
539 }
540
541 #[test]
542 fn test_so2_left_jacobian_numerical() {
543 let epsilon = 1e-7;
544 let tolerance = 1e-4;
545
546 let tangent = SO2Tangent::new(0.5);
547 let jl_analytical = tangent.left_jacobian();
548
549 let angle_vec = DVector::from_vec(vec![tangent.angle()]);
550 let jl_numerical = numerical_jacobian(
551 |theta| {
552 let tang = SO2Tangent::new(theta[0]);
553 let so2 = tang.exp(None);
554 let log_result = so2.log(None);
555 DVector::from_vec(vec![log_result.angle()])
556 },
557 &angle_vec,
558 1,
559 epsilon,
560 );
561
562 assert!((jl_analytical - jl_numerical).norm() < tolerance);
563 }
564
565 #[test]
568 fn test_so2_jacobian_inverse_identity() {
569 let tangent = SO2Tangent::new(0.5);
571 let jr = tangent.right_jacobian();
572 let jr_inv = tangent.right_jacobian_inv();
573 let product = jr * jr_inv;
574
575 assert!((product[(0, 0)] - 1.0).abs() < 1e-10);
576
577 let jl = tangent.left_jacobian();
579 let jl_inv = tangent.left_jacobian_inv();
580 let product_left = jl * jl_inv;
581
582 assert!((product_left[(0, 0)] - 1.0).abs() < 1e-10);
583 }
584
585 #[test]
586 fn test_so2_display() {
587 let r = SO2::from_angle(0.5);
588 let s = format!("{r}");
589 assert!(!s.is_empty(), "Display should produce output, got: {s}");
590
591 let t = SO2Tangent::new(1.2);
592 let st = format!("{t}");
593 assert!(
594 !st.is_empty(),
595 "Tangent Display should produce output, got: {st}"
596 );
597 }
598
599 #[test]
600 fn test_so2_from_slice_and_back() {
601 let angle = 1.0f64;
602 let r = SO2::from_param_slice(&[angle]);
603 let back = DVector::from_column_slice(r.as_param_slice());
604 assert_eq!(back.len(), 1);
605 assert!((back[0] - angle).abs() < 1e-9);
606 }
607
608 #[test]
609 fn test_so2_tangent_from_slice_and_back() {
610 let t = SO2Tangent::from_slice(&[0.7f64]);
611 let v2 = DVector::from_column_slice(t.as_slice());
612 assert!((v2[0] - 0.7).abs() < 1e-10);
613 }
614
615 #[test]
616 fn test_so2_rotation_matrix() {
617 let r = SO2::from_angle(0.0);
618 let mat = r.rotation_matrix();
619 assert!((mat[(0, 0)] - 1.0).abs() < 1e-10);
620 assert!(mat[(0, 1)].abs() < 1e-10);
621 }
622
623 #[test]
624 fn test_so2_complex_angle_accessors() {
625 let angle = std::f64::consts::FRAC_PI_4;
626 let r = SO2::from_angle(angle);
627 let c = r.complex();
628 assert!((c.re - angle.cos()).abs() < 1e-9);
629 assert!((c.im - angle.sin()).abs() < 1e-9);
630 assert!((r.angle() - angle).abs() < 1e-9);
631 }
632
633 #[test]
634 fn test_so2_normalize_is_valid() {
635 let mut r = SO2::from_angle(0.3);
636 r.normalize();
637 assert!(r.is_valid(1e-6));
638 }
639
640 #[test]
641 fn test_so2_tangent_normalized() {
642 let t_pos = SO2Tangent::new(3.0);
643 let tn = t_pos.normalized();
644 assert!((tn.angle() - 1.0).abs() < 1e-9);
645
646 let t_neg = SO2Tangent::new(-2.0);
647 let tn_neg = t_neg.normalized();
648 assert!((tn_neg.angle() - (-1.0)).abs() < 1e-9);
649
650 let t_zero = SO2Tangent::new(0.0);
651 let tn_zero = t_zero.normalized();
652 assert!((tn_zero.angle()).abs() < 1e-9);
653 }
654
655 #[test]
656 fn test_so2_tangent_is_zero() {
657 let zero = SO2Tangent::new(0.0);
658 assert!(zero.is_zero(1e-9));
659 let nonzero = SO2Tangent::new(0.1);
660 assert!(!nonzero.is_zero(1e-9));
661 }
662
663 #[test]
664 fn test_so2_random() {
665 let r = SO2::random();
666 assert!(r.is_valid(1e-6));
667
668 let t = SO2Tangent::random();
669 let _ = t; }
671
672 #[test]
673 fn test_so2_adjoint_zero_jacobian_identity() {
674 let r = SO2::from_angle(0.5);
675 let adj = r.adjoint();
676 assert!((adj[0] - 1.0).abs() < 1e-10);
677
678 let zj = SO2::zero_jacobian();
679 assert!(zj[0].abs() < 1e-10);
680
681 let ji = SO2::jacobian_identity();
682 assert!((ji[0] - 1.0).abs() < 1e-10);
683 }
684
685 #[test]
686 fn test_so2_compose_with_jacobians() {
687 let r1 = SO2::from_angle(0.3);
688 let r2 = SO2::from_angle(0.2);
689 let mut j_self = Matrix1::zeros();
690 let mut j_other = Matrix1::zeros();
691 let result = r1.compose(&r2, Some(&mut j_self), Some(&mut j_other));
692 assert!(result.is_valid(1e-9));
693 assert!(j_self[0].is_finite());
694 assert!(j_other[0].is_finite());
695 }
696
697 #[test]
698 fn test_so2_log_with_jacobian() {
699 let r = SO2::from_angle(0.5);
700 let mut jac = Matrix1::zeros();
701 let t = r.log(Some(&mut jac));
702 assert!((t.angle() - 0.5).abs() < 1e-9);
703 assert!((jac[0] - 1.0).abs() < 1e-10);
704 }
705
706 #[test]
707 fn test_so2_inverse_with_jacobian() {
708 let r = SO2::from_angle(0.4);
709 let mut jac = Matrix1::zeros();
710 let inv = r.inverse(Some(&mut jac));
711 assert!(inv.is_valid(1e-9));
712 assert!(jac[0].is_finite());
713 }
714
715 #[test]
716 fn test_so2_tangent_hat() {
717 let t = SO2Tangent::new(1.0);
718 let hat = t.hat();
719 assert!(hat[(0, 0)].abs() < 1e-10);
721 assert!((hat[(1, 0)] - 1.0).abs() < 1e-10);
722 assert!((hat[(0, 1)] - (-1.0)).abs() < 1e-10);
723 }
724
725 #[test]
726 fn test_so2_tangent_exp_with_jacobian() {
727 use crate::Tangent;
728 let t = SO2Tangent::new(0.3);
729 let mut jac = Matrix1::zeros();
730 let r = t.exp(Some(&mut jac));
731 assert!(r.is_valid(1e-9));
732 assert!((jac[0] - 1.0).abs() < 1e-10);
733 }
734
735 #[test]
736 fn test_so2_tangent_zero() {
737 use crate::Tangent;
738 let zero = SO2Tangent::zero();
739 assert!(zero.is_zero(1e-9));
740 }
741
742 #[test]
743 fn so2_param_slice_round_trip() {
744 let g = SO2::random();
745 let recovered = SO2::from_param_slice(g.as_param_slice());
746 assert!(g.is_approx(&recovered, 1e-14));
747 }
748
749 #[test]
750 fn so2_tangent_slice_round_trip() {
751 let t = SO2Tangent::random();
752 let recovered = SO2Tangent::from_slice(t.as_slice());
753 assert!(t.is_approx(&recovered, 1e-14));
754 }
755}