1use crate::{LieGroup, Tangent};
36use nalgebra::{Matrix3, Matrix4, Quaternion, SVector, Unit, UnitQuaternion, Vector3};
37use std::{
38 fmt,
39 fmt::{Display, Formatter},
40};
41
42#[derive(Clone, Debug, PartialEq)]
48pub struct SO3 {
49 params: SVector<f64, 4>,
51}
52
53impl Display for SO3 {
54 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
55 write!(
56 f,
57 "SO3(quaternion: [w: {:.4}, x: {:.4}, y: {:.4}, z: {:.4}])",
58 self.params[0], self.params[1], self.params[2], self.params[3]
59 )
60 }
61}
62
63impl SO3 {
64 pub const DIM: usize = 3;
66
67 pub const DOF: usize = 3;
69
70 pub const REP_SIZE: usize = 4;
72
73 pub fn identity() -> Self {
77 SO3 {
78 params: SVector::<f64, 4>::new(1.0, 0.0, 0.0, 0.0),
79 }
80 }
81
82 pub fn jacobian_identity() -> Matrix3<f64> {
86 Matrix3::<f64>::identity()
87 }
88
89 #[inline]
94 pub fn new(quaternion: UnitQuaternion<f64>) -> Self {
95 Self::from_unit_quaternion(quaternion)
96 }
97
98 #[inline]
100 fn unit_quaternion(&self) -> UnitQuaternion<f64> {
101 UnitQuaternion::from_quaternion(Quaternion::new(
102 self.params[0],
103 self.params[1],
104 self.params[2],
105 self.params[3],
106 ))
107 }
108
109 #[inline]
111 fn from_unit_quaternion(q: UnitQuaternion<f64>) -> Self {
112 let r = q.quaternion();
113 SO3 {
114 params: SVector::<f64, 4>::new(r.w, r.i, r.j, r.k),
115 }
116 }
117
118 pub fn from_quaternion_wxyz(w: f64, x: f64, y: f64, z: f64) -> Self {
130 let q = UnitQuaternion::from_quaternion(Quaternion::new(w, x, y, z));
131 Self::from_unit_quaternion(q)
132 }
133
134 pub const QUATERNION_NORM_TOLERANCE: f64 = 1e-6;
136
137 pub fn try_from_quaternion_wxyz(w: f64, x: f64, y: f64, z: f64) -> Option<Self> {
150 let norm = (w * w + x * x + y * y + z * z).sqrt();
151 if !norm.is_finite() || (norm - 1.0).abs() > Self::QUATERNION_NORM_TOLERANCE {
152 return None;
153 }
154 Some(Self::from_quaternion_wxyz(w, x, y, z))
155 }
156
157 pub fn from_euler_angles(roll: f64, pitch: f64, yaw: f64) -> Self {
159 Self::from_unit_quaternion(UnitQuaternion::from_euler_angles(roll, pitch, yaw))
160 }
161
162 pub fn from_axis_angle(axis: &Vector3<f64>, angle: f64) -> Self {
164 let unit_axis = Unit::new_normalize(*axis);
165 Self::from_unit_quaternion(UnitQuaternion::from_axis_angle(&unit_axis, angle))
166 }
167
168 pub fn from_scaled_axis(axis_angle: Vector3<f64>) -> Self {
170 Self::from_unit_quaternion(UnitQuaternion::from_scaled_axis(axis_angle))
171 }
172
173 pub fn quaternion(&self) -> UnitQuaternion<f64> {
175 self.unit_quaternion()
176 }
177
178 pub fn from_quaternion(quaternion: UnitQuaternion<f64>) -> Self {
180 Self::from_unit_quaternion(quaternion)
181 }
182
183 pub fn to_quaternion(&self) -> UnitQuaternion<f64> {
185 self.unit_quaternion()
186 }
187
188 pub fn quat(&self) -> Quaternion<f64> {
190 *self.unit_quaternion().quaternion()
191 }
192
193 #[inline]
195 pub fn x(&self) -> f64 {
196 self.params[1]
197 }
198
199 #[inline]
201 pub fn y(&self) -> f64 {
202 self.params[2]
203 }
204
205 #[inline]
207 pub fn z(&self) -> f64 {
208 self.params[3]
209 }
210
211 #[inline]
213 pub fn w(&self) -> f64 {
214 self.params[0]
215 }
216
217 pub fn rotation_matrix(&self) -> Matrix3<f64> {
219 self.unit_quaternion().to_rotation_matrix().into_inner()
220 }
221
222 pub fn transform(&self) -> Matrix4<f64> {
224 self.unit_quaternion().to_homogeneous()
225 }
226
227 pub fn set_quaternion(&mut self, coeffs: &[f64; 4]) {
229 let q = UnitQuaternion::from_quaternion(Quaternion::new(
230 coeffs[0], coeffs[1], coeffs[2], coeffs[3],
231 ));
232 let r = q.quaternion();
233 self.params = SVector::<f64, 4>::new(r.w, r.i, r.j, r.k);
234 }
235
236 #[inline]
238 pub fn coeffs(&self) -> [f64; 4] {
239 [
240 self.params[0],
241 self.params[1],
242 self.params[2],
243 self.params[3],
244 ]
245 }
246
247 #[inline]
249 pub fn params(&self) -> &SVector<f64, 4> {
250 &self.params
251 }
252
253 pub fn distance(&self, other: &Self) -> f64 {
258 self.between(other, None, None).log(None).angle()
259 }
260
261 pub fn slerp(&self, other: &Self, t: f64) -> Self {
267 Self::from_quaternion(self.quaternion().slerp(&other.quaternion(), t))
268 }
269}
270
271impl LieGroup for SO3 {
273 const NAME: &'static str = "SO3";
274
275 type TangentVector = SO3Tangent;
276 type JacobianMatrix = Matrix3<f64>;
277 type LieAlgebra = Matrix3<f64>;
278
279 fn inverse(&self, jacobian: Option<&mut Self::JacobianMatrix>) -> Self {
291 let inverse_quat = self.unit_quaternion().inverse();
292
293 if let Some(jac) = jacobian {
294 *jac = -self.adjoint();
295 }
296
297 Self::from_unit_quaternion(inverse_quat)
298 }
299
300 fn compose(
315 &self,
316 other: &Self,
317 jacobian_self: Option<&mut Self::JacobianMatrix>,
318 jacobian_other: Option<&mut Self::JacobianMatrix>,
319 ) -> Self {
320 let result = Self::from_unit_quaternion(self.unit_quaternion() * other.unit_quaternion());
321
322 if let Some(jac_self) = jacobian_self {
323 *jac_self = other.inverse(None).adjoint();
324 }
325
326 if let Some(jac_other) = jacobian_other {
327 *jac_other = Matrix3::identity();
328 }
329
330 result
331 }
332
333 fn log(&self, jacobian: Option<&mut Self::JacobianMatrix>) -> Self::TangentVector {
346 let uq = self.unit_quaternion();
347 debug_assert!(
348 (uq.norm() - 1.0).abs() < 1e-6,
349 "SO3::log() requires normalized quaternion, norm = {}",
350 uq.norm()
351 );
352
353 let q = uq.quaternion();
354 let sin_angle_squared = q.i * q.i + q.j * q.j + q.k * q.k;
355
356 let log_coeff = if sin_angle_squared > crate::SMALL_ANGLE_THRESHOLD {
357 let sin_angle = sin_angle_squared.sqrt();
358 let cos_angle = q.w;
359
360 let two_angle = 2.0
368 * if cos_angle < 0.0 {
369 f64::atan2(-sin_angle, -cos_angle)
370 } else {
371 f64::atan2(sin_angle, cos_angle)
372 };
373
374 two_angle / sin_angle
375 } else {
376 if q.w < 0.0 { -2.0 } else { 2.0 }
380 };
381
382 let axis_angle = SO3Tangent::new(Vector3::new(
383 q.i * log_coeff,
384 q.j * log_coeff,
385 q.k * log_coeff,
386 ));
387
388 if let Some(jac) = jacobian {
389 *jac = axis_angle.right_jacobian_inv();
390 }
391
392 axis_angle
393 }
394
395 fn act(
396 &self,
397 vector: &Vector3<f64>,
398 jacobian_self: Option<&mut Self::JacobianMatrix>,
399 jacobian_vector: Option<&mut Matrix3<f64>>,
400 ) -> Vector3<f64> {
401 let result = self.unit_quaternion() * vector;
402
403 if let Some(jac_self) = jacobian_self {
404 let vector_hat = SO3Tangent::new(*vector).hat();
406 *jac_self = -self.rotation_matrix() * vector_hat;
407 }
408
409 if let Some(jac_vector) = jacobian_vector {
410 *jac_vector = self.rotation_matrix();
411 }
412
413 result
414 }
415
416 fn adjoint(&self) -> Self::JacobianMatrix {
417 self.rotation_matrix()
418 }
419
420 fn random() -> Self {
421 Self::from_unit_quaternion(UnitQuaternion::from_scaled_axis(Vector3::new(
422 rand::random::<f64>() * 2.0 - 1.0,
423 rand::random::<f64>() * 2.0 - 1.0,
424 rand::random::<f64>() * 2.0 - 1.0,
425 )))
426 }
427
428 fn jacobian_identity() -> Self::JacobianMatrix {
429 Matrix3::<f64>::identity()
430 }
431
432 fn zero_jacobian() -> Self::JacobianMatrix {
433 Matrix3::<f64>::zeros()
434 }
435
436 fn normalize(&mut self) {
437 let q = UnitQuaternion::from_quaternion(
438 Quaternion::new(
439 self.params[0],
440 self.params[1],
441 self.params[2],
442 self.params[3],
443 )
444 .normalize(),
445 );
446 let r = q.quaternion();
447 self.params = SVector::<f64, 4>::new(r.w, r.i, r.j, r.k);
448 }
449
450 fn is_valid(&self, tolerance: f64) -> bool {
451 if !tolerance.is_finite() {
455 return false;
456 }
457 let norm_sq = self.params[0] * self.params[0]
458 + self.params[1] * self.params[1]
459 + self.params[2] * self.params[2]
460 + self.params[3] * self.params[3];
461 (norm_sq.sqrt() - 1.0).abs() < tolerance
462 }
463
464 fn as_param_slice(&self) -> &[f64] {
465 self.params.as_slice()
466 }
467
468 fn as_param_slice_mut(&mut self) -> &mut [f64] {
469 self.params.as_mut_slice()
470 }
471
472 fn from_param_slice(s: &[f64]) -> Self {
473 debug_assert_eq!(s.len(), 4);
474 SO3 {
475 params: SVector::from_column_slice(s),
476 }
477 }
478
479 fn vee(&self) -> Self::TangentVector {
484 self.log(None)
485 }
486
487 fn is_approx(&self, other: &Self, tolerance: f64) -> bool {
493 let difference = self.right_minus(other, None, None);
494 difference.is_zero(tolerance)
495 }
496}
497
498#[derive(Clone, PartialEq)]
504pub struct SO3Tangent {
505 data: Vector3<f64>,
507}
508
509impl fmt::Display for SO3Tangent {
510 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
511 write!(
512 f,
513 "so3(axis-angle: [{:.4}, {:.4}, {:.4}])",
514 self.data.x, self.data.y, self.data.z
515 )
516 }
517}
518
519impl SO3Tangent {
520 #[inline]
525 pub fn new(axis_angle: Vector3<f64>) -> Self {
526 SO3Tangent { data: axis_angle }
527 }
528
529 pub fn from_components(x: f64, y: f64, z: f64) -> Self {
531 SO3Tangent::new(Vector3::new(x, y, z))
532 }
533
534 #[inline]
536 pub fn axis_angle(&self) -> Vector3<f64> {
537 self.data
538 }
539
540 #[inline]
542 pub fn angle(&self) -> f64 {
543 self.data.norm()
544 }
545
546 #[inline]
548 pub fn axis(&self) -> Vector3<f64> {
549 let norm = self.data.norm();
550 if norm < f64::EPSILON {
551 Vector3::identity()
552 } else {
553 self.data / norm
554 }
555 }
556
557 #[inline]
559 pub fn x(&self) -> f64 {
560 self.data.x
561 }
562
563 #[inline]
565 pub fn y(&self) -> f64 {
566 self.data.y
567 }
568
569 #[inline]
571 pub fn z(&self) -> f64 {
572 self.data.z
573 }
574
575 #[inline]
577 pub fn coeffs(&self) -> Vector3<f64> {
578 self.data
579 }
580
581 pub fn ang(&self) -> Vector3<f64> {
583 self.data
584 }
585}
586
587impl Tangent<SO3> for SO3Tangent {
589 const DIM: usize = 3;
591
592 fn exp(&self, jacobian: Option<&mut <SO3 as LieGroup>::JacobianMatrix>) -> SO3 {
606 let theta_squared = self.data.norm_squared();
607
608 let quaternion = if theta_squared > crate::SMALL_ANGLE_THRESHOLD {
609 UnitQuaternion::from_scaled_axis(self.data)
610 } else {
611 UnitQuaternion::from_quaternion(Quaternion::new(
612 1.0,
613 self.data.x / 2.0,
614 self.data.y / 2.0,
615 self.data.z / 2.0,
616 ))
617 };
618
619 if let Some(jac) = jacobian {
620 *jac = self.right_jacobian();
621 }
622
623 SO3::from_unit_quaternion(quaternion)
624 }
625
626 fn right_jacobian(&self) -> <SO3 as LieGroup>::JacobianMatrix {
633 self.left_jacobian().transpose()
634 }
635
636 fn left_jacobian(&self) -> <SO3 as LieGroup>::JacobianMatrix {
643 let angle = self.data.norm_squared();
644 let tangent_skew = self.hat();
645
646 if angle <= crate::SMALL_ANGLE_THRESHOLD {
647 Matrix3::identity() + 0.5 * tangent_skew
648 } else {
649 let theta = angle.sqrt();
650 let sin_theta = theta.sin();
651 let cos_theta = theta.cos();
652
653 Matrix3::identity()
654 + (1.0 - cos_theta) / angle * tangent_skew
655 + (theta - sin_theta) / (angle * theta) * tangent_skew * tangent_skew
656 }
657 }
658
659 fn right_jacobian_inv(&self) -> <SO3 as LieGroup>::JacobianMatrix {
666 self.left_jacobian_inv().transpose()
667 }
668
669 fn left_jacobian_inv(&self) -> <SO3 as LieGroup>::JacobianMatrix {
676 let angle = self.data.norm_squared();
677 let tangent_skew = self.hat();
678
679 if angle <= crate::SMALL_ANGLE_THRESHOLD {
680 Matrix3::identity() - 0.5 * tangent_skew
681 } else {
682 let theta = angle.sqrt();
683 let sin_theta = theta.sin();
684 let cos_theta = theta.cos();
685
686 Matrix3::identity() - (0.5 * tangent_skew)
687 + (1.0 / angle - (1.0 + cos_theta) / (2.0 * theta * sin_theta))
688 * tangent_skew
689 * tangent_skew
690 }
691 }
692
693 fn hat(&self) -> <SO3 as LieGroup>::LieAlgebra {
699 Matrix3::new(
700 0.0,
701 -self.data.z,
702 self.data.y,
703 self.data.z,
704 0.0,
705 -self.data.x,
706 -self.data.y,
707 self.data.x,
708 0.0,
709 )
710 }
711
712 fn zero() -> <SO3 as LieGroup>::TangentVector {
713 Self::new(Vector3::zeros())
714 }
715
716 fn random() -> <SO3 as LieGroup>::TangentVector {
717 Self::new(Vector3::new(
718 rand::random::<f64>() * 0.2 - 0.1,
719 rand::random::<f64>() * 0.2 - 0.1,
720 rand::random::<f64>() * 0.2 - 0.1,
721 ))
722 }
723
724 fn is_zero(&self, tolerance: f64) -> bool {
725 self.data.norm() < tolerance
726 }
727
728 fn normalize(&mut self) {
729 let norm = self.data.norm();
730 if norm > f64::EPSILON {
731 self.data /= norm;
732 }
733 }
734
735 fn normalized(&self) -> <SO3 as LieGroup>::TangentVector {
736 let norm = self.data.norm();
737 if norm > f64::EPSILON {
738 SO3Tangent::new(self.data / norm)
739 } else {
740 Self::zero()
741 }
742 }
743
744 fn as_slice(&self) -> &[f64] {
745 self.data.as_slice()
746 }
747
748 fn from_slice(s: &[f64]) -> Self {
749 debug_assert_eq!(s.len(), 3);
750 SO3Tangent {
751 data: Vector3::from_column_slice(s),
752 }
753 }
754
755 fn small_adj(&self) -> <SO3 as LieGroup>::JacobianMatrix {
759 self.hat()
760 }
761
762 fn lie_bracket(&self, other: &Self) -> <SO3 as LieGroup>::TangentVector {
766 let bracket_result = self.small_adj() * other.data;
767 SO3Tangent::new(bracket_result)
768 }
769
770 fn is_approx(&self, other: &Self, tolerance: f64) -> bool {
776 (self.data - other.data).norm() < tolerance
777 }
778
779 fn generator(&self, i: usize) -> <SO3 as LieGroup>::LieAlgebra {
787 assert!(i < 3, "SO(3) only has generators for indices 0, 1, 2");
788
789 match i {
790 0 => {
791 Matrix3::new(0.0, 0.0, 0.0, 0.0, 0.0, -1.0, 0.0, 1.0, 0.0)
793 }
794 1 => {
795 Matrix3::new(0.0, 0.0, 1.0, 0.0, 0.0, 0.0, -1.0, 0.0, 0.0)
797 }
798 2 => {
799 Matrix3::new(0.0, -1.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0)
801 }
802 _ => unreachable!(),
803 }
804 }
805}
806
807#[cfg(test)]
808mod tests {
809 use super::*;
810 use core::f64;
811 use std::f64::consts::PI;
812
813 const TOLERANCE: f64 = 1e-12;
814
815 #[test]
816 fn test_so3_constructor_datatype() {
817 let so3 = SO3::from_quaternion_wxyz(1.0, 0.0, 0.0, 0.0);
818 assert_eq!(0.0, so3.x());
819 assert_eq!(0.0, so3.y());
820 assert_eq!(0.0, so3.z());
821 assert_eq!(1.0, so3.w());
822 }
823
824 #[test]
825 fn test_so3_constructor_quat() {
826 let quat = UnitQuaternion::identity();
827 let so3 = SO3::new(quat);
828 assert_eq!(0.0, so3.x());
829 assert_eq!(0.0, so3.y());
830 assert_eq!(0.0, so3.z());
831 assert_eq!(1.0, so3.w());
832 }
833
834 #[test]
835 fn test_so3_constructor_euler() {
836 let so3 = SO3::from_euler_angles(0.0, 0.0, 0.0);
837 assert_eq!(0.0, so3.x());
838 assert_eq!(0.0, so3.y());
839 assert_eq!(0.0, so3.z());
840 assert_eq!(1.0, so3.w());
841 }
842
843 #[test]
844 fn test_so3_identity() {
845 let so3 = SO3::identity();
846 assert_eq!(0.0, so3.x());
847 assert_eq!(0.0, so3.y());
848 assert_eq!(0.0, so3.z());
849 assert_eq!(1.0, so3.w());
850 }
851
852 #[test]
853 fn test_so3_coeffs() {
854 let so3 = SO3::from_quaternion_wxyz(1.0, 0.0, 0.0, 0.0);
856 let coeffs = so3.coeffs();
857 assert!((coeffs[0] - 1.0).abs() < TOLERANCE); assert!((coeffs[1] - 0.0).abs() < TOLERANCE); assert!((coeffs[2] - 0.0).abs() < TOLERANCE); assert!((coeffs[3] - 0.0).abs() < TOLERANCE); let so3 = SO3::from_quaternion_wxyz(0.4, 0.1, 0.2, 0.3);
864 let coeffs = so3.coeffs();
865 let original_quat = Quaternion::new(0.4, 0.1, 0.2, 0.3);
866 let normalized_quat = original_quat.normalize();
867 assert!((coeffs[0] - normalized_quat.w).abs() < TOLERANCE);
868 assert!((coeffs[1] - normalized_quat.i).abs() < TOLERANCE);
869 assert!((coeffs[2] - normalized_quat.j).abs() < TOLERANCE);
870 assert!((coeffs[3] - normalized_quat.k).abs() < TOLERANCE);
871 }
872
873 #[test]
874 fn test_so3_random() {
875 let so3 = SO3::random();
876 assert!((so3.quaternion().norm() - 1.0).abs() < TOLERANCE);
877 }
878
879 #[test]
880 fn test_so3_transform() {
881 let so3 = SO3::identity();
882 let transform = so3.transform();
883
884 assert_eq!(4, transform.nrows());
885 assert_eq!(4, transform.ncols());
886
887 for i in 0..4 {
889 for j in 0..4 {
890 if i == j {
891 assert!((transform[(i, j)] - 1.0).abs() < TOLERANCE);
892 } else {
893 assert!(transform[(i, j)].abs() < TOLERANCE);
894 }
895 }
896 }
897 }
898
899 #[test]
900 fn test_so3_rotation() {
901 let so3 = SO3::identity();
902 let rotation = so3.rotation_matrix();
903
904 assert_eq!(3, rotation.nrows());
905 assert_eq!(3, rotation.ncols());
906
907 for i in 0..3 {
909 for j in 0..3 {
910 if i == j {
911 assert!((rotation[(i, j)] - 1.0).abs() < TOLERANCE);
912 } else {
913 assert!(rotation[(i, j)].abs() < TOLERANCE);
914 }
915 }
916 }
917 }
918
919 #[test]
920 fn test_so3_inverse() {
921 let so3 = SO3::identity();
923 let so3_inv = so3.inverse(None);
924 assert_eq!(0.0, so3_inv.x());
925 assert_eq!(0.0, so3_inv.y());
926 assert_eq!(0.0, so3_inv.z());
927 assert_eq!(1.0, so3_inv.w());
928
929 let so3 = SO3::random();
931 let so3_inv = so3.inverse(None);
932 assert!((so3.x() + so3_inv.x()).abs() < TOLERANCE);
933 assert!((so3.y() + so3_inv.y()).abs() < TOLERANCE);
934 assert!((so3.z() + so3_inv.z()).abs() < TOLERANCE);
935 assert!((so3.w() - so3_inv.w()).abs() < TOLERANCE);
936 }
937
938 #[test]
939 fn test_so3_inverse_jacobian() {
940 let so3 = SO3::identity();
941 let mut jacobian = Matrix3::zeros();
942 let so3_inv = so3.inverse(Some(&mut jacobian));
943
944 assert_eq!(0.0, so3_inv.x());
946 assert_eq!(0.0, so3_inv.y());
947 assert_eq!(0.0, so3_inv.z());
948 assert_eq!(1.0, so3_inv.w());
949
950 let expected_jac = -Matrix3::identity();
952 assert!((jacobian - expected_jac).norm() < TOLERANCE);
953 }
954
955 #[test]
956 fn test_so3_rplus() {
957 let so3a = SO3::identity();
959 let so3b = SO3Tangent::new(Vector3::zeros());
960 let so3c = so3a.right_plus(&so3b, None, None);
961 assert_eq!(0.0, so3c.x());
962 assert_eq!(0.0, so3c.y());
963 assert_eq!(0.0, so3c.z());
964 assert_eq!(1.0, so3c.w());
965
966 let so3a = SO3::random();
968 let so3c = so3a.right_plus(&so3b, None, None);
969 assert!((so3a.x() - so3c.x()).abs() < TOLERANCE);
970 assert!((so3a.y() - so3c.y()).abs() < TOLERANCE);
971 assert!((so3a.z() - so3c.z()).abs() < TOLERANCE);
972 assert!((so3a.w() - so3c.w()).abs() < TOLERANCE);
973 }
974
975 #[test]
976 fn test_so3_lplus() {
977 let so3a = SO3::identity();
979 let so3t = SO3Tangent::new(Vector3::zeros());
980 let so3c = so3a.left_plus(&so3t, None, None);
981 assert_eq!(0.0, so3c.x());
982 assert_eq!(0.0, so3c.y());
983 assert_eq!(0.0, so3c.z());
984 assert_eq!(1.0, so3c.w());
985
986 let so3a = SO3::random();
988 let so3c = so3a.left_plus(&so3t, None, None);
989 assert!((so3a.x() - so3c.x()).abs() < TOLERANCE);
990 assert!((so3a.y() - so3c.y()).abs() < TOLERANCE);
991 assert!((so3a.z() - so3c.z()).abs() < TOLERANCE);
992 assert!((so3a.w() - so3c.w()).abs() < TOLERANCE);
993 }
994
995 #[test]
996 fn test_so3_rminus() {
997 let so3a = SO3::identity();
999 let so3b = SO3::identity();
1000 let so3c = so3a.right_minus(&so3b, None, None);
1001 assert!(so3c.x().abs() < TOLERANCE);
1002 assert!(so3c.y().abs() < TOLERANCE);
1003 assert!(so3c.z().abs() < TOLERANCE);
1004
1005 let so3a = SO3::random();
1007 let so3b = so3a.clone();
1008 let so3c = so3a.right_minus(&so3b, None, None);
1009 assert!(so3c.data.norm() < TOLERANCE);
1010 }
1011
1012 #[test]
1013 fn test_so3_minus() {
1014 let so3a = SO3::random();
1016 let so3b = SO3::random();
1017 let so3c = so3a.minus(&so3b, None, None);
1018 let so3d = so3a.right_minus(&so3b, None, None);
1019 assert!((so3c.data - so3d.data).norm() < TOLERANCE);
1020 }
1021
1022 #[test]
1023 fn test_so3_exp_log() {
1024 let so3t = SO3Tangent::new(Vector3::zeros());
1026 let so3 = so3t.exp(None);
1027 assert_eq!(0.0, so3.x());
1028 assert_eq!(0.0, so3.y());
1029 assert_eq!(0.0, so3.z());
1030 assert_eq!(1.0, so3.w());
1031
1032 let so3t = SO3Tangent::new(Vector3::new(0.1, 0.2, 0.3));
1034 let so3 = so3t.exp(None);
1035 let so3n = SO3Tangent::new(Vector3::new(-0.1, -0.2, -0.3));
1036 let so3_inv = so3n.exp(None);
1037 assert!((so3_inv.x() + so3.x()).abs() < TOLERANCE);
1038 assert!((so3_inv.y() + so3.y()).abs() < TOLERANCE);
1039 assert!((so3_inv.z() + so3.z()).abs() < TOLERANCE);
1040 assert!((so3_inv.w() - so3.w()).abs() < TOLERANCE);
1041 }
1042
1043 #[test]
1044 fn test_so3_log() {
1045 let so3 = SO3::identity();
1047 let so3_log = so3.log(None);
1048 assert!(so3_log.x().abs() < TOLERANCE);
1049 assert!(so3_log.y().abs() < TOLERANCE);
1050 assert!(so3_log.z().abs() < TOLERANCE);
1051
1052 let so3 = SO3::random();
1054 let so3_log = so3.log(None);
1055 let so3_inv_log = so3.inverse(None).log(None);
1056 assert!((so3_inv_log.x() + so3_log.x()).abs() < TOLERANCE);
1057 assert!((so3_inv_log.y() + so3_log.y()).abs() < TOLERANCE);
1058 assert!((so3_inv_log.z() + so3_log.z()).abs() < TOLERANCE);
1059 }
1060
1061 #[test]
1062 fn test_so3_tangent_hat() {
1063 let so3_tan = SO3Tangent::new(Vector3::new(1.0, 2.0, 3.0));
1064 let so3_lie = so3_tan.hat();
1065
1066 assert!((so3_lie[(0, 0)] - 0.0).abs() < TOLERANCE);
1067 assert!((so3_lie[(0, 1)] + 3.0).abs() < TOLERANCE);
1068 assert!((so3_lie[(0, 2)] - 2.0).abs() < TOLERANCE);
1069 assert!((so3_lie[(1, 0)] - 3.0).abs() < TOLERANCE);
1070 assert!((so3_lie[(1, 1)] - 0.0).abs() < TOLERANCE);
1071 assert!((so3_lie[(1, 2)] + 1.0).abs() < TOLERANCE);
1072 assert!((so3_lie[(2, 0)] + 2.0).abs() < TOLERANCE);
1073 assert!((so3_lie[(2, 1)] - 1.0).abs() < TOLERANCE);
1074 assert!((so3_lie[(2, 2)] - 0.0).abs() < TOLERANCE);
1075 }
1076
1077 #[test]
1078 fn test_so3_act() {
1079 let so3 = SO3::identity();
1080 let transformed_point = so3.act(&Vector3::new(1.0, 1.0, 1.0), None, None);
1081 assert!((transformed_point.x - 1.0).abs() < TOLERANCE);
1082 assert!((transformed_point.y - 1.0).abs() < TOLERANCE);
1083 assert!((transformed_point.z - 1.0).abs() < TOLERANCE);
1084
1085 let so3 = SO3::from_euler_angles(PI, PI / 2.0, PI / 4.0);
1086 let transformed_point = so3.act(&Vector3::new(1.0, 1.0, 1.0), None, None);
1087 assert!((transformed_point.x - 0.0).abs() < TOLERANCE);
1088 assert!((transformed_point.y + f64::consts::SQRT_2).abs() < 1e-10);
1089 assert!((transformed_point.z + 1.0).abs() < TOLERANCE);
1090 }
1091
1092 #[test]
1093 fn test_so3_tangent_angular_velocity() {
1094 let so3tan = SO3Tangent::new(Vector3::new(1.0, 2.0, 3.0));
1095 let ang_vel = so3tan.ang();
1096 assert!((ang_vel - Vector3::new(1.0, 2.0, 3.0)).norm() < TOLERANCE);
1097 }
1098
1099 #[test]
1100 fn test_so3_compose() {
1101 let so3_1 = SO3::random();
1102 let so3_2 = SO3::random();
1103 let composed = so3_1.compose(&so3_2, None, None);
1104 assert!(composed.is_valid(TOLERANCE));
1105
1106 let identity = SO3::identity();
1108 let composed_with_identity = so3_1.compose(&identity, None, None);
1109 assert!((composed_with_identity.distance(&so3_1)).abs() < TOLERANCE);
1110 }
1111
1112 #[test]
1113 fn test_so3_exp_log_consistency() {
1114 let tangent = SO3Tangent::new(Vector3::new(0.1, 0.2, 0.3));
1115 let so3 = tangent.exp(None);
1116 let recovered_tangent = so3.log(None);
1117 assert!((tangent.data - recovered_tangent.data).norm() < TOLERANCE);
1118 }
1119
1120 #[test]
1121 fn test_so3_right_left_jacobian_relationship() {
1122 let tangent = SO3Tangent::new(Vector3::zeros());
1124 let ljac = tangent.left_jacobian();
1125 let rjac = tangent.right_jacobian();
1126 assert!((ljac - rjac).norm() < TOLERANCE);
1127 assert!((ljac - Matrix3::identity()).norm() < TOLERANCE);
1128
1129 let tangent = SO3Tangent::new(Vector3::new(0.1, 0.2, 0.3));
1131 let ljac = tangent.left_jacobian();
1132 let rjac = tangent.right_jacobian();
1133
1134 assert!((ljac - rjac.transpose()).norm() < TOLERANCE);
1137 assert!((rjac - ljac.transpose()).norm() < TOLERANCE);
1138 }
1139
1140 #[test]
1141 fn test_so3_manifold_properties() {
1142 assert_eq!(SO3::DIM, 3);
1143 assert_eq!(SO3::DOF, 3);
1144 assert_eq!(SO3::REP_SIZE, 4);
1145 }
1146
1147 #[test]
1148 fn test_so3_normalize() {
1149 let mut so3 = SO3::from_quaternion_wxyz(0.5, 0.5, 0.5, 0.5);
1150 so3.normalize();
1151 assert!(so3.is_valid(TOLERANCE));
1152 }
1153
1154 #[test]
1155 fn test_so3_tangent_norms() {
1156 let tangent = SO3Tangent::new(Vector3::new(3.0, 4.0, 0.0));
1157 let norm = tangent.data.norm();
1158 assert!((norm - 5.0).abs() < TOLERANCE);
1159
1160 let squared_norm = tangent.data.norm_squared();
1161 assert!((squared_norm - 25.0).abs() < TOLERANCE);
1162 }
1163
1164 #[test]
1165 fn test_so3_tangent_zero() {
1166 let zero = SO3Tangent::zero();
1167 assert!(zero.data.norm() < TOLERANCE);
1168
1169 let tangent = SO3Tangent::new(Vector3::zeros());
1170 assert!(tangent.is_zero(TOLERANCE));
1171 }
1172
1173 #[test]
1174 fn test_so3_tangent_normalize() {
1175 let mut tangent = SO3Tangent::new(Vector3::new(3.0, 4.0, 0.0));
1176 tangent.normalize();
1177 assert!((tangent.data.norm() - 1.0).abs() < TOLERANCE);
1178 }
1179
1180 #[test]
1181 fn test_so3_adjoint() {
1182 let so3 = SO3::random();
1183 let adj = so3.adjoint();
1184 assert_eq!(adj.nrows(), 3);
1185 assert_eq!(adj.ncols(), 3);
1186
1187 let det = adj.determinant();
1189 assert!((det - 1.0).abs() < TOLERANCE);
1190 }
1191
1192 #[test]
1193 fn test_so3_small_angle_approximations() {
1194 let small_tangent = SO3Tangent::new(Vector3::new(1e-8, 2e-8, 3e-8));
1195 let so3 = small_tangent.exp(None);
1196 let recovered = so3.log(None);
1197 assert!((small_tangent.data - recovered.data).norm() < TOLERANCE);
1198 }
1199
1200 #[test]
1201 fn test_so3_specific_rotations() {
1202 let so3_x = SO3::from_axis_angle(&Vector3::x(), PI / 2.0);
1204 let point_y = Vector3::y();
1205 let rotated = so3_x.act(&point_y, None, None);
1206 assert!((rotated - Vector3::z()).norm() < TOLERANCE);
1207
1208 let so3_z = SO3::from_axis_angle(&Vector3::z(), PI / 2.0);
1210 let point_x = Vector3::x();
1211 let rotated = so3_z.act(&point_x, None, None);
1212 assert!((rotated - Vector3::y()).norm() < TOLERANCE);
1213 }
1214
1215 #[test]
1216 fn test_so3_from_components() {
1217 let so3 = SO3::from_quaternion_wxyz(1.0, 0.0, 0.0, 0.0);
1218 assert_eq!(so3.x(), 0.0);
1219 assert_eq!(so3.y(), 0.0);
1220 assert_eq!(so3.z(), 0.0);
1221 assert_eq!(so3.w(), 1.0);
1222 }
1223
1224 #[test]
1225 fn test_so3_tangent_from_components() {
1226 let tangent = SO3Tangent::from_components(1.0, 2.0, 3.0);
1227 assert_eq!(tangent.x(), 1.0);
1228 assert_eq!(tangent.y(), 2.0);
1229 assert_eq!(tangent.z(), 3.0);
1230 }
1231
1232 #[test]
1233 fn test_so3_consistency_with_manif() {
1234 let so3_1 = SO3::random();
1236 let so3_2 = SO3::random();
1237
1238 let so3_3 = SO3::random();
1240 let left_assoc = so3_1
1241 .compose(&so3_2, None, None)
1242 .compose(&so3_3, None, None);
1243 let right_assoc = so3_1.compose(&so3_2.compose(&so3_3, None, None), None, None);
1244
1245 assert!(left_assoc.distance(&right_assoc) < 1e-10);
1246 }
1247
1248 #[test]
1249 fn test_so3_tangent_accessors() {
1250 let tangent = SO3Tangent::new(Vector3::new(1.0, 2.0, 3.0));
1251 assert_eq!(tangent.x(), 1.0);
1252 assert_eq!(tangent.y(), 2.0);
1253 assert_eq!(tangent.z(), 3.0);
1254
1255 let coeffs = tangent.coeffs();
1256 assert_eq!(coeffs, Vector3::new(1.0, 2.0, 3.0));
1257 }
1258
1259 #[test]
1260 fn test_so3_between() {
1261 let so3_1 = SO3::random();
1262 let so3_2 = SO3::random();
1263 let between = so3_1.between(&so3_2, None, None);
1264
1265 let result = so3_1.compose(&between, None, None);
1267 assert!(result.distance(&so3_2) < TOLERANCE);
1268 }
1269
1270 #[test]
1271 fn test_so3_distance() {
1272 let so3_1 = SO3::random();
1273 let so3_2 = SO3::random();
1274 let distance = so3_1.distance(&so3_2);
1275 assert!(distance >= 0.0);
1276 assert!(so3_1.distance(&so3_1) < TOLERANCE);
1277 }
1278
1279 #[test]
1282 fn test_so3_vee() {
1283 let so3 = SO3::random();
1284 let tangent_log = so3.log(None);
1285 let tangent_vee = so3.vee();
1286
1287 assert!((tangent_log.data - tangent_vee.data).norm() < 1e-10);
1288 }
1289
1290 #[test]
1291 fn test_so3_is_approx() {
1292 let so3_1 = SO3::random();
1293 let so3_2 = so3_1.clone();
1294
1295 assert!(so3_1.is_approx(&so3_1, 1e-10));
1296 assert!(so3_1.is_approx(&so3_2, 1e-10));
1297
1298 let small_tangent = SO3Tangent::new(Vector3::new(1e-12, 1e-12, 1e-12));
1300 let so3_perturbed = so3_1.right_plus(&small_tangent, None, None);
1301 assert!(so3_1.is_approx(&so3_perturbed, 1e-10));
1302 }
1303
1304 #[test]
1305 fn test_so3_tangent_small_adj() {
1306 let axis_angle = Vector3::new(0.1, 0.2, 0.3);
1307 let tangent = SO3Tangent::new(axis_angle);
1308 let small_adj = tangent.small_adj();
1309 let hat_matrix = tangent.hat();
1310
1311 assert!((small_adj - hat_matrix).norm() < 1e-10);
1313 }
1314
1315 #[test]
1316 fn test_so3_tangent_lie_bracket() {
1317 let tangent_a = SO3Tangent::new(Vector3::new(0.1, 0.0, 0.0));
1318 let tangent_b = SO3Tangent::new(Vector3::new(0.0, 0.2, 0.0));
1319
1320 let bracket_ab = tangent_a.lie_bracket(&tangent_b);
1321 let bracket_ba = tangent_b.lie_bracket(&tangent_a);
1322
1323 assert!((bracket_ab.data + bracket_ba.data).norm() < 1e-10);
1325
1326 let bracket_aa = tangent_a.lie_bracket(&tangent_a);
1328 assert!(bracket_aa.is_zero(1e-10));
1329
1330 let bracket_hat = bracket_ab.hat();
1332 let expected = tangent_a.hat() * tangent_b.hat() - tangent_b.hat() * tangent_a.hat();
1333 assert!((bracket_hat - expected).norm() < 1e-10);
1334 }
1335
1336 #[test]
1337 fn test_so3_tangent_is_approx() {
1338 let tangent_1 = SO3Tangent::new(Vector3::new(0.1, 0.2, 0.3));
1339 let tangent_2 = SO3Tangent::new(Vector3::new(0.1 + 1e-12, 0.2, 0.3));
1340 let tangent_3 = SO3Tangent::new(Vector3::new(0.5, 0.6, 0.7));
1341
1342 assert!(tangent_1.is_approx(&tangent_1, 1e-10));
1343 assert!(tangent_1.is_approx(&tangent_2, 1e-10));
1344 assert!(!tangent_1.is_approx(&tangent_3, 1e-10));
1345 }
1346
1347 #[test]
1348 fn test_so3_generators() {
1349 let tangent = SO3Tangent::new(Vector3::new(1.0, 1.0, 1.0));
1350
1351 for i in 0..3 {
1353 let generator = tangent.generator(i);
1354
1355 assert!((generator + generator.transpose()).norm() < 1e-10);
1357
1358 assert!(generator.trace().abs() < 1e-10);
1360 }
1361
1362 let e1 = tangent.generator(0);
1364 let e2 = tangent.generator(1);
1365 let e3 = tangent.generator(2);
1366
1367 let expected_e1 = Matrix3::new(0.0, 0.0, 0.0, 0.0, 0.0, -1.0, 0.0, 1.0, 0.0);
1369 let expected_e2 = Matrix3::new(0.0, 0.0, 1.0, 0.0, 0.0, 0.0, -1.0, 0.0, 0.0);
1370 let expected_e3 = Matrix3::new(0.0, -1.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0);
1371
1372 assert!((e1 - expected_e1).norm() < 1e-10);
1373 assert!((e2 - expected_e2).norm() < 1e-10);
1374 assert!((e3 - expected_e3).norm() < 1e-10);
1375 }
1376
1377 #[test]
1378 #[should_panic]
1379 fn test_so3_generator_invalid_index() {
1380 let tangent = SO3Tangent::new(Vector3::new(1.0, 1.0, 1.0));
1381 let _generator = tangent.generator(3); }
1383
1384 #[test]
1385 fn test_so3_jacobi_identity() {
1386 let x = SO3Tangent::new(Vector3::new(0.1, 0.0, 0.0));
1388 let y = SO3Tangent::new(Vector3::new(0.0, 0.2, 0.0));
1389 let z = SO3Tangent::new(Vector3::new(0.0, 0.0, 0.3));
1390
1391 let term1 = x.lie_bracket(&y.lie_bracket(&z));
1392 let term2 = y.lie_bracket(&z.lie_bracket(&x));
1393 let term3 = z.lie_bracket(&x.lie_bracket(&y));
1394
1395 let jacobi_sum = SO3Tangent::new(term1.data + term2.data + term3.data);
1396 assert!(jacobi_sum.is_zero(1e-10));
1397 }
1398
1399 #[test]
1402 fn test_so3_right_jacobian_numerical() {
1403 let xi = Vector3::new(0.1, 0.2, 0.3);
1408 let tangent = SO3Tangent::new(xi);
1409 let jr_analytical = tangent.right_jacobian();
1410
1411 let delta_size = 1e-6;
1413 let test_deltas = vec![
1414 Vector3::new(delta_size, 0.0, 0.0),
1415 Vector3::new(0.0, delta_size, 0.0),
1416 Vector3::new(0.0, 0.0, delta_size),
1417 ];
1418
1419 for delta in test_deltas {
1420 let analytical_result = jr_analytical * delta;
1422
1423 let base = SO3Tangent::new(xi).exp(None);
1425 let perturbed = SO3Tangent::new(xi + delta).exp(None);
1426 let base_inv = base.inverse(None);
1427 let diff_element = base_inv.compose(&perturbed, None, None);
1428 let diff_log = diff_element.log(None);
1429 let numerical_result = Vector3::new(diff_log.x(), diff_log.y(), diff_log.z());
1430
1431 let error = (analytical_result - numerical_result).norm();
1432 assert!(
1433 error < 1e-7,
1434 "Right Jacobian verification failed: error = {}, delta = {:?}",
1435 error,
1436 delta
1437 );
1438 }
1439 }
1440
1441 #[test]
1442 fn test_so3_left_jacobian_numerical() {
1443 let xi = Vector3::new(0.1, 0.2, 0.3);
1448 let tangent = SO3Tangent::new(xi);
1449 let jl_analytical = tangent.left_jacobian();
1450
1451 let delta_size = 1e-6;
1453 let test_deltas = vec![
1454 Vector3::new(delta_size, 0.0, 0.0),
1455 Vector3::new(0.0, delta_size, 0.0),
1456 Vector3::new(0.0, 0.0, delta_size),
1457 ];
1458
1459 for delta in test_deltas {
1460 let analytical_result = jl_analytical * delta;
1462
1463 let base = SO3Tangent::new(xi).exp(None);
1465 let perturbed = SO3Tangent::new(xi + delta).exp(None);
1466 let base_inv = base.inverse(None);
1467 let diff_element = perturbed.compose(&base_inv, None, None);
1468 let diff_log = diff_element.log(None);
1469 let numerical_result = Vector3::new(diff_log.x(), diff_log.y(), diff_log.z());
1470
1471 let error = (analytical_result - numerical_result).norm();
1472 assert!(
1473 error < 1e-7,
1474 "Left Jacobian verification failed: error = {}, delta = {:?}",
1475 error,
1476 delta
1477 );
1478 }
1479 }
1480
1481 #[test]
1482 fn test_so3_accumulated_error_small_rotations() {
1483 let small_rotation = SO3::from_scaled_axis(Vector3::new(0.001, 0.002, -0.001));
1485 let mut accumulated = SO3::identity();
1486
1487 for _ in 0..1000 {
1488 accumulated = accumulated.compose(&small_rotation, None, None);
1489 }
1490
1491 let expected_tangent = SO3Tangent::new(Vector3::new(1.0, 2.0, -1.0));
1493 let expected = expected_tangent.exp(None);
1494
1495 assert!(accumulated.is_approx(&expected, 1e-6));
1497 }
1498
1499 #[test]
1500 fn test_so3_inverse_composition_chain() {
1501 let rotation = SO3::from_euler_angles(0.1, 0.2, 0.3);
1503 let inverse = rotation.inverse(None);
1504 let mut accumulated = SO3::identity();
1505
1506 for _ in 0..500 {
1507 accumulated = accumulated.compose(&rotation, None, None);
1508 accumulated = accumulated.compose(&inverse, None, None);
1509 }
1510
1511 assert!(accumulated.is_approx(&SO3::identity(), 1e-9));
1513 }
1514
1515 #[test]
1518 fn test_so3_antipodal_quaternions() {
1519 let q = UnitQuaternion::from_euler_angles(0.5, 0.3, 0.2);
1521 let so3_pos = SO3::new(q);
1522 let so3_neg = SO3::new(UnitQuaternion::from_quaternion(-q.quaternion()));
1523
1524 let log_pos = so3_pos.log(None);
1526 let log_neg = so3_neg.log(None);
1527
1528 let diff_norm = (log_pos.data - log_neg.data).norm();
1530 let sum_norm = (log_pos.data + log_neg.data).norm();
1531 assert!(diff_norm < 1e-10 || sum_norm < 1e-10);
1532 }
1533
1534 #[test]
1535 fn test_so3_near_pi_rotation() {
1536 let axis = Vector3::new(1.0, 0.0, 0.0).normalize();
1538 let angle = std::f64::consts::PI - 1e-8;
1539 let so3 = SO3::from_axis_angle(&axis, angle);
1540
1541 let tangent = so3.log(None);
1542 let recovered = tangent.exp(None);
1543
1544 assert!(so3.is_approx(&recovered, 1e-6));
1545 }
1546
1547 #[test]
1548 fn test_so3_right_jacobian_inverse_identity() {
1549 let tangent = SO3Tangent::new(Vector3::new(0.001, 0.002, 0.003));
1550 let jr = tangent.right_jacobian();
1551 let jr_inv = tangent.right_jacobian_inv();
1552 let product = jr * jr_inv;
1553 let identity = Matrix3::identity();
1554
1555 assert!(
1556 (product - identity).norm() < 1e-10,
1557 "Jr * Jr_inv should be identity, got error: {}",
1558 (product - identity).norm()
1559 );
1560 }
1561
1562 #[test]
1563 fn test_so3_left_jacobian_inverse_identity() {
1564 let tangent = SO3Tangent::new(Vector3::new(0.001, 0.002, 0.003));
1565 let jl = tangent.left_jacobian();
1566 let jl_inv = tangent.left_jacobian_inv();
1567 let product = jl * jl_inv;
1568 let identity = Matrix3::identity();
1569
1570 assert!(
1571 (product - identity).norm() < 1e-10,
1572 "Jl * Jl_inv should be identity, got error: {}",
1573 (product - identity).norm()
1574 );
1575 }
1576
1577 #[test]
1578 fn test_so3_from_quaternion_wxyz() {
1579 let (w, x, y, z) = (144. / 205., 96. / 205., 72. / 205., 83. / 205.);
1583 let so3 = SO3::from_quaternion_wxyz(w, x, y, z);
1584 assert_eq!(so3.w(), w);
1585 assert_eq!(so3.x(), x);
1586 assert_eq!(so3.y(), y);
1587 assert_eq!(so3.z(), z);
1588 }
1589
1590 #[test]
1591 fn test_so3_tangent_is_not_dynamic() {
1592 assert!(!SO3Tangent::is_dynamic());
1593 assert_eq!(SO3Tangent::DIM, 3);
1594 }
1595
1596 #[test]
1597 fn test_so3_small_angle_threshold_exp_log() {
1598 let near_threshold_angles = [1e-6, 1e-5, 1e-4, 1e-3];
1601
1602 for &angle in &near_threshold_angles {
1603 let tangent = SO3Tangent::new(Vector3::new(angle, 0.0, 0.0));
1604 let so3 = tangent.exp(None);
1605 let recovered = so3.log(None);
1606 assert!(
1607 (tangent.data - recovered.data).norm() < 1e-10,
1608 "SO3 exp-log round-trip failed for angle = {angle}"
1609 );
1610 }
1611 }
1612
1613 #[test]
1617 fn test_so3_log_negative_w_small_angle_sign() {
1618 let cases = [1e-5_f64, 1e-6, 1e-7];
1619 for &s in &cases {
1620 let q = SO3::from_quaternion_wxyz(-(s / 2.0).cos(), 0.0, 0.0, (s / 2.0).sin());
1624 assert!(
1625 q.unit_quaternion().quaternion().w < 0.0,
1626 "fixture must exercise the negative-w branch"
1627 );
1628 let log = q.log(None);
1629 let expected = Vector3::new(0.0, 0.0, -s);
1630 assert!(
1631 (log.data - expected).norm() < 1e-9,
1632 "log of the w<0 rotation by -{s} must be -{s} in z, got {log}"
1633 );
1634 let round_trip = log.exp(None);
1639 let rm_g = q.unit_quaternion().to_rotation_matrix().into_inner();
1640 let rm_rt = round_trip
1641 .unit_quaternion()
1642 .to_rotation_matrix()
1643 .into_inner();
1644 assert!(
1645 (rm_g - rm_rt).norm() < 1e-12,
1646 "exp(log(g)) != g for w<0 near identity at s = {s}"
1647 );
1648 }
1649 }
1650
1651 #[test]
1652 fn test_so3_display() {
1653 let r = SO3::identity();
1654 let s = format!("{r}");
1655 assert!(!s.is_empty(), "Display should produce output");
1656 }
1657
1658 #[test]
1659 fn test_so3_accessors_xyzw() {
1660 let r = SO3::from_axis_angle(&Vector3::z(), std::f64::consts::FRAC_PI_2);
1661 let _ = r.x();
1662 let _ = r.y();
1663 let _ = r.z();
1664 let _ = r.w();
1665 let q = r.to_quaternion();
1666 let q2 = r.quat();
1667 assert!((q.w - q2.w).abs() < 1e-10);
1668 }
1669
1670 #[test]
1671 fn test_so3_set_quaternion_and_coeffs() {
1672 let r_orig = SO3::from_euler_angles(0.1, 0.2, 0.3);
1674 let orig_coeffs = r_orig.coeffs();
1675 assert_eq!(orig_coeffs.len(), 4);
1676
1677 let mut r2 = SO3::identity();
1679 r2.set_quaternion(&orig_coeffs);
1680 let c2 = r2.coeffs();
1681 for i in 0..4 {
1682 assert!(
1683 (c2[i] - orig_coeffs[i]).abs() < 1e-9,
1684 "coeffs[{i}] mismatch"
1685 );
1686 }
1687 }
1688
1689 #[test]
1690 fn test_so3_from_scaled_axis_constructor() {
1691 let v = Vector3::new(0.1, 0.2, 0.3);
1692 let r = SO3::from_scaled_axis(v);
1693 assert!(r.is_valid(1e-6));
1694 }
1695
1696 #[test]
1697 fn test_so3_from_axis_angle_constructor() {
1698 let axis = Vector3::x();
1699 let r = SO3::from_axis_angle(&axis, std::f64::consts::FRAC_PI_4);
1700 assert!(r.is_valid(1e-6));
1701 }
1702
1703 #[test]
1704 fn test_so3_tangent_axis_angle_accessors() {
1705 let t = SO3Tangent::new(Vector3::new(0.1, 0.2, 0.3));
1706
1707 let av = t.axis_angle();
1709 assert!((av.norm() - t.angle()).abs() < 1e-10);
1710
1711 let ax = t.axis();
1713 assert!((ax.norm() - 1.0).abs() < 1e-9);
1714
1715 let c = t.coeffs();
1720 assert_eq!(c.len(), 3);
1721
1722 let ang = t.ang();
1724 assert!((ang.norm() - t.angle()).abs() < 1e-10);
1725 }
1726
1727 #[test]
1728 fn test_so3_right_jacobian_inv() {
1729 use crate::Tangent;
1730 let t = SO3Tangent::new(Vector3::new(0.1, 0.2, 0.3));
1731 let jr_inv = t.right_jacobian_inv();
1732 assert_eq!(jr_inv.nrows(), 3);
1733 assert_eq!(jr_inv.ncols(), 3);
1734
1735 let jr = t.right_jacobian();
1736 let product = jr_inv * jr;
1737 for i in 0..3 {
1738 for j in 0..3 {
1739 let expected = if i == j { 1.0 } else { 0.0 };
1740 assert!(
1741 (product[(i, j)] - expected).abs() < 1e-4,
1742 "product[{i},{j}] = {} != {expected}",
1743 product[(i, j)]
1744 );
1745 }
1746 }
1747 }
1748
1749 #[test]
1750 fn test_so3_left_jacobian_inv() {
1751 use crate::Tangent;
1752 let t = SO3Tangent::new(Vector3::new(0.1, 0.2, 0.3));
1753 let jl_inv = t.left_jacobian_inv();
1754 assert_eq!(jl_inv.nrows(), 3);
1755
1756 let jl = t.left_jacobian();
1757 let product = jl_inv * jl;
1758 for i in 0..3 {
1759 for j in 0..3 {
1760 let expected = if i == j { 1.0 } else { 0.0 };
1761 assert!(
1762 (product[(i, j)] - expected).abs() < 1e-4,
1763 "product[{i},{j}] = {} != {expected}",
1764 product[(i, j)]
1765 );
1766 }
1767 }
1768 }
1769
1770 #[test]
1771 fn test_so3_tangent_normalized() {
1772 use crate::Tangent;
1773 let t = SO3Tangent::new(Vector3::new(0.0, 0.0, 3.0));
1774 let tn = t.normalized();
1775 assert!((tn.angle() - 1.0).abs() < 1e-9);
1776 }
1777
1778 #[test]
1779 fn test_so3_tangent_is_zero() {
1780 use crate::Tangent;
1781 let zero = SO3Tangent::new(Vector3::zeros());
1782 assert!(zero.is_zero(1e-9));
1783 let nonzero = SO3Tangent::new(Vector3::new(0.1, 0.0, 0.0));
1784 assert!(!nonzero.is_zero(1e-9));
1785 }
1786
1787 #[test]
1788 fn test_so3_inverse_with_jacobian() {
1789 use crate::LieGroup;
1790 let r = SO3::from_euler_angles(0.1, 0.2, 0.3);
1791 let mut jac = Matrix3::zeros();
1792 let inv = r.inverse(Some(&mut jac));
1793 assert!(inv.is_valid(1e-6));
1794 assert!(jac[(0, 0)].is_finite());
1795 }
1796
1797 #[test]
1798 fn test_so3_compose_with_jacobians() {
1799 use crate::LieGroup;
1800 let r1 = SO3::from_euler_angles(0.1, 0.2, 0.3);
1801 let r2 = SO3::from_euler_angles(0.4, 0.1, 0.2);
1802 let mut j_self = Matrix3::zeros();
1803 let mut j_other = Matrix3::zeros();
1804 let result = r1.compose(&r2, Some(&mut j_self), Some(&mut j_other));
1805 assert!(result.is_valid(1e-6));
1806 assert!(j_self[(0, 0)].is_finite());
1807 assert!(j_other[(0, 0)].is_finite());
1808 }
1809
1810 #[test]
1811 fn test_so3_zero_jacobian() {
1812 use crate::LieGroup;
1813 let zj = SO3::zero_jacobian();
1814 for i in 0..3 {
1815 for j in 0..3 {
1816 assert!(zj[(i, j)].abs() < 1e-10);
1817 }
1818 }
1819 }
1820
1821 #[test]
1822 fn test_so3_act_with_jacobians() {
1823 use crate::LieGroup;
1824 let r = SO3::from_euler_angles(0.1, 0.2, 0.3);
1825 let v = Vector3::new(1.0, 0.0, 0.0);
1826 let mut j_self = Matrix3::zeros();
1827 let mut j_vec = Matrix3::zeros();
1828 let result = r.act(&v, Some(&mut j_self), Some(&mut j_vec));
1829 assert!(result.norm() > 0.0);
1830 assert!(j_self[(0, 0)].is_finite());
1831 assert!(j_vec[(0, 0)].is_finite());
1832 }
1833
1834 #[test]
1835 fn so3_param_slice_round_trip() {
1836 let g = SO3::random();
1837 let recovered = SO3::from_param_slice(g.as_param_slice());
1838 assert!(g.is_approx(&recovered, 1e-14));
1839 }
1840
1841 #[test]
1842 fn so3_tangent_slice_round_trip() {
1843 let t = SO3Tangent::random();
1844 let recovered = SO3Tangent::from_slice(t.as_slice());
1845 assert!(t.is_approx(&recovered, 1e-14));
1846 }
1847
1848 #[test]
1849 fn test_from_dvector_wxyz() {
1850 let dv = ::nalgebra::dvector![144., 96., 72., 83.] / 205.; let so3 = SO3::from_param_slice(dv.as_slice());
1852 assert_eq!(144. / 205., so3.w());
1853 assert_eq!(96. / 205., so3.x());
1854 assert_eq!(72. / 205., so3.y());
1855 assert_eq!(83. / 205., so3.z());
1856 }
1857
1858 #[test]
1859 fn test_from_s03_wxyz() {
1860 let so3 = SO3::from_quaternion_wxyz(144. / 205., 96. / 205., 72. / 205., 83. / 205.); let s = so3.as_param_slice();
1862 assert_eq!(144. / 205., s[0]);
1863 assert_eq!(96. / 205., s[1]);
1864 assert_eq!(72. / 205., s[2]);
1865 assert_eq!(83. / 205., s[3]);
1866 }
1867
1868 #[test]
1869 fn test_bijective_from_dvector() {
1870 let so3_expected =
1871 SO3::from_quaternion_wxyz(144. / 205., 96. / 205., 72. / 205., 83. / 205.); let slice_expected = so3_expected.as_param_slice();
1873 let so3_actual = SO3::from_param_slice(slice_expected);
1874 assert!(so3_expected.is_approx(&so3_actual, 1e-14));
1875 }
1876
1877 #[test]
1880 fn slerp_halfway_between_identity_and_half_turn_is_quarter_turn() {
1881 let half_turn = SO3::from_axis_angle(&Vector3::x(), std::f64::consts::PI);
1882 let mid = SO3::identity().slerp(&half_turn, 0.5);
1883 let quarter = SO3::from_axis_angle(&Vector3::x(), std::f64::consts::FRAC_PI_2);
1884 assert!(mid.distance(&quarter) < 1e-12);
1885 }
1886
1887 #[test]
1890 fn try_from_quaternion_wxyz_accepts_a_near_unit_norm() {
1891 let scale = (1.0f64 + 5e-7).sqrt();
1893 let q = match SO3::try_from_quaternion_wxyz(scale, 0.0, 0.0, 0.0) {
1894 Some(q) => q,
1895 None => panic!("a norm within tolerance must be accepted"),
1896 };
1897 assert!((q.w() - 1.0).abs() < 1e-9);
1898 }
1899
1900 #[test]
1903 fn try_from_quaternion_wxyz_rejects_a_non_unit_norm() {
1904 assert!(SO3::try_from_quaternion_wxyz(2.0, 0.0, 0.0, 0.0).is_none());
1905 assert!(SO3::try_from_quaternion_wxyz(0.0, 0.0, 0.0, 0.0).is_none());
1906 }
1907
1908 #[test]
1910 fn try_from_quaternion_wxyz_rejects_a_non_finite_component() {
1911 assert!(SO3::try_from_quaternion_wxyz(f64::NAN, 0.0, 0.0, 0.0).is_none());
1912 assert!(SO3::try_from_quaternion_wxyz(f64::INFINITY, 0.0, 0.0, 0.0).is_none());
1913 }
1914
1915 #[test]
1917 fn try_from_quaternion_wxyz_matches_the_unchecked_constructor() {
1918 let q = match SO3::try_from_quaternion_wxyz(144. / 205., 96. / 205., 72. / 205., 83. / 205.)
1919 {
1920 Some(q) => q,
1921 None => panic!("a unit-norm quaternion must be accepted"),
1922 };
1923 assert!(q.is_approx(
1924 &SO3::from_quaternion_wxyz(144. / 205., 96. / 205., 72. / 205., 83. / 205.),
1925 1e-15
1926 ));
1927 }
1928
1929 #[test]
1930 fn slerp_endpoints_return_the_operands() {
1931 let a = SO3::from_euler_angles(0.1, 0.2, 0.3);
1932 let b = SO3::from_euler_angles(0.4, 0.5, 0.6);
1933 assert!(a.slerp(&b, 0.0).distance(&a) < 1e-12);
1934 assert!(a.slerp(&b, 1.0).distance(&b) < 1e-12);
1935 }
1936}