1use crate::traits::{AntiHermitianByConstruction, LieAlgebra, LieGroup, TracelessByConstruction};
40use nalgebra::Matrix3;
41use std::fmt;
42use std::ops::{Add, Mul, MulAssign, Neg, Sub};
43
44#[derive(Clone, Copy, Debug, PartialEq)]
67pub struct So3Algebra(pub [f64; 3]);
68
69impl Add for So3Algebra {
70 type Output = Self;
71 fn add(self, rhs: Self) -> Self {
72 Self([
73 self.0[0] + rhs.0[0],
74 self.0[1] + rhs.0[1],
75 self.0[2] + rhs.0[2],
76 ])
77 }
78}
79
80impl Add<&So3Algebra> for So3Algebra {
81 type Output = So3Algebra;
82 fn add(self, rhs: &So3Algebra) -> So3Algebra {
83 self + *rhs
84 }
85}
86
87impl Add<So3Algebra> for &So3Algebra {
88 type Output = So3Algebra;
89 fn add(self, rhs: So3Algebra) -> So3Algebra {
90 *self + rhs
91 }
92}
93
94impl Add<&So3Algebra> for &So3Algebra {
95 type Output = So3Algebra;
96 fn add(self, rhs: &So3Algebra) -> So3Algebra {
97 *self + *rhs
98 }
99}
100
101impl Sub for So3Algebra {
102 type Output = Self;
103 fn sub(self, rhs: Self) -> Self {
104 Self([
105 self.0[0] - rhs.0[0],
106 self.0[1] - rhs.0[1],
107 self.0[2] - rhs.0[2],
108 ])
109 }
110}
111
112impl Neg for So3Algebra {
113 type Output = Self;
114 fn neg(self) -> Self {
115 Self([-self.0[0], -self.0[1], -self.0[2]])
116 }
117}
118
119impl Mul<f64> for So3Algebra {
120 type Output = Self;
121 fn mul(self, scalar: f64) -> Self {
122 Self([self.0[0] * scalar, self.0[1] * scalar, self.0[2] * scalar])
123 }
124}
125
126impl Mul<So3Algebra> for f64 {
127 type Output = So3Algebra;
128 fn mul(self, rhs: So3Algebra) -> So3Algebra {
129 rhs * self
130 }
131}
132
133impl LieAlgebra for So3Algebra {
134 const DIM: usize = 3;
135
136 #[inline]
137 fn zero() -> Self {
138 Self([0.0, 0.0, 0.0])
139 }
140
141 #[inline]
142 fn add(&self, other: &Self) -> Self {
143 Self([
144 self.0[0] + other.0[0],
145 self.0[1] + other.0[1],
146 self.0[2] + other.0[2],
147 ])
148 }
149
150 #[inline]
151 fn scale(&self, scalar: f64) -> Self {
152 Self([self.0[0] * scalar, self.0[1] * scalar, self.0[2] * scalar])
153 }
154
155 #[inline]
156 fn norm(&self) -> f64 {
157 (self.0[0].powi(2) + self.0[1].powi(2) + self.0[2].powi(2)).sqrt()
158 }
159
160 #[inline]
161 fn basis_element(i: usize) -> Self {
162 assert!(i < 3, "SO(3) algebra is 3-dimensional");
163 let mut v = [0.0; 3];
164 v[i] = 1.0;
165 Self(v)
166 }
167
168 #[inline]
169 fn from_components(components: &[f64]) -> Self {
170 assert_eq!(components.len(), 3, "so(3) has dimension 3");
171 Self([components[0], components[1], components[2]])
172 }
173
174 #[inline]
175 fn to_components(&self) -> Vec<f64> {
176 self.0.to_vec()
177 }
178
179 #[inline]
204 fn bracket(&self, other: &Self) -> Self {
205 let v = self.0;
207 let w = other.0;
208
209 Self([
210 v[1] * w[2] - v[2] * w[1], v[2] * w[0] - v[0] * w[2], v[0] * w[1] - v[1] * w[0], ])
214 }
215}
216
217#[derive(Clone, Debug, PartialEq)]
243pub struct SO3 {
244 pub matrix: Matrix3<f64>,
246}
247
248impl SO3 {
249 #[must_use]
251 pub fn identity() -> Self {
252 Self {
253 matrix: Matrix3::identity(),
254 }
255 }
256
257 #[must_use]
263 pub fn rotation_x(angle: f64) -> Self {
264 let c = angle.cos();
265 let s = angle.sin();
266
267 Self {
268 matrix: Matrix3::new(1.0, 0.0, 0.0, 0.0, c, -s, 0.0, s, c),
269 }
270 }
271
272 #[must_use]
278 pub fn rotation_y(angle: f64) -> Self {
279 let c = angle.cos();
280 let s = angle.sin();
281
282 Self {
283 matrix: Matrix3::new(c, 0.0, s, 0.0, 1.0, 0.0, -s, 0.0, c),
284 }
285 }
286
287 #[must_use]
293 pub fn rotation_z(angle: f64) -> Self {
294 let c = angle.cos();
295 let s = angle.sin();
296
297 Self {
298 matrix: Matrix3::new(c, -s, 0.0, s, c, 0.0, 0.0, 0.0, 1.0),
299 }
300 }
301
302 #[must_use]
311 pub fn rotation(axis: [f64; 3], angle: f64) -> Self {
312 let norm = (axis[0].powi(2) + axis[1].powi(2) + axis[2].powi(2)).sqrt();
313 if norm < 1e-10 {
314 return Self::identity();
315 }
316
317 let n = [axis[0] / norm, axis[1] / norm, axis[2] / norm];
319
320 let c = angle.cos();
322 let s = angle.sin();
323 let t = 1.0 - c;
324
325 let matrix = Matrix3::new(
326 t * n[0] * n[0] + c,
327 t * n[0] * n[1] - s * n[2],
328 t * n[0] * n[2] + s * n[1],
329 t * n[0] * n[1] + s * n[2],
330 t * n[1] * n[1] + c,
331 t * n[1] * n[2] - s * n[0],
332 t * n[0] * n[2] - s * n[1],
333 t * n[1] * n[2] + s * n[0],
334 t * n[2] * n[2] + c,
335 );
336
337 Self { matrix }
338 }
339
340 #[must_use]
342 pub fn verify_orthogonality(&self, tolerance: f64) -> bool {
343 let product = self.matrix.transpose() * self.matrix;
344 let identity = Matrix3::identity();
345
346 (product - identity).norm() < tolerance
347 }
348
349 #[must_use]
351 pub fn inverse(&self) -> Self {
352 Self {
353 matrix: self.matrix.transpose(),
354 }
355 }
356
357 #[must_use]
364 pub fn distance_to_identity(&self) -> f64 {
365 let trace = self.matrix.trace();
366 let cos_theta = (trace - 1.0) / 2.0;
367
368 let cos_theta = cos_theta.clamp(-1.0, 1.0);
370
371 cos_theta.acos()
372 }
373
374 #[must_use]
391 pub fn interpolate(&self, other: &Self, t: f64) -> Self {
392 if t <= 0.0 {
393 return self.clone();
394 }
395 if t >= 1.0 {
396 return other.clone();
397 }
398
399 let interpolated = self.matrix * (1.0 - t) + other.matrix * t;
401
402 Self::gram_schmidt_orthogonalize(interpolated)
404 }
405
406 #[must_use]
417 pub fn gram_schmidt_orthogonalize(matrix: Matrix3<f64>) -> Self {
418 use nalgebra::Vector3;
419
420 let c0 = Vector3::new(matrix[(0, 0)], matrix[(1, 0)], matrix[(2, 0)]);
422 let c1 = Vector3::new(matrix[(0, 1)], matrix[(1, 1)], matrix[(2, 1)]);
423
424 let e0 = c0.normalize();
426 let e1 = (c1 - e0 * e0.dot(&c1)).normalize();
427 let e2 = e0.cross(&e1);
429
430 let orthogonal = Matrix3::new(
431 e0[0], e1[0], e2[0], e0[1], e1[1], e2[1], e0[2], e1[2], e2[2],
432 );
433
434 Self { matrix: orthogonal }
435 }
436
437 #[must_use]
442 pub fn renormalize(&self) -> Self {
443 Self::gram_schmidt_orthogonalize(self.matrix)
444 }
445
446 #[must_use]
452 pub fn geodesic_distance(&self, other: &Self) -> f64 {
453 let relative = Self {
455 matrix: self.matrix.transpose() * other.matrix,
456 };
457 relative.distance_to_identity()
458 }
459}
460
461impl fmt::Display for So3Algebra {
462 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
463 write!(
464 f,
465 "so(3)[{:.4}, {:.4}, {:.4}]",
466 self.0[0], self.0[1], self.0[2]
467 )
468 }
469}
470
471impl fmt::Display for SO3 {
472 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
473 let dist = self.distance_to_identity();
474 write!(f, "SO(3)(θ={:.4})", dist)
475 }
476}
477
478impl Mul<&SO3> for &SO3 {
480 type Output = SO3;
481 fn mul(self, rhs: &SO3) -> SO3 {
482 SO3 {
483 matrix: self.matrix * rhs.matrix,
484 }
485 }
486}
487
488impl Mul<&SO3> for SO3 {
489 type Output = SO3;
490 fn mul(self, rhs: &SO3) -> SO3 {
491 &self * rhs
492 }
493}
494
495impl MulAssign<&SO3> for SO3 {
496 fn mul_assign(&mut self, rhs: &SO3) {
497 self.matrix *= rhs.matrix;
498 }
499}
500
501impl LieGroup for SO3 {
502 const DIM: usize = 3;
503
504 type Algebra = So3Algebra;
505
506 fn identity() -> Self {
507 Self::identity()
508 }
509
510 fn compose(&self, other: &Self) -> Self {
511 Self {
512 matrix: self.matrix * other.matrix,
513 }
514 }
515
516 fn inverse(&self) -> Self {
517 Self::inverse(self)
518 }
519
520 fn adjoint(&self) -> Self {
521 self.inverse()
523 }
524
525 fn adjoint_action(&self, algebra_element: &So3Algebra) -> So3Algebra {
526 let v = algebra_element.0;
530 let rotated = self.matrix * nalgebra::Vector3::new(v[0], v[1], v[2]);
531
532 So3Algebra([rotated[0], rotated[1], rotated[2]])
533 }
534
535 fn distance_to_identity(&self) -> f64 {
536 Self::distance_to_identity(self)
537 }
538
539 fn exp(tangent: &So3Algebra) -> Self {
540 let angle = tangent.norm();
542
543 if angle < 1e-10 {
544 return Self::identity();
545 }
546
547 let axis = [
548 tangent.0[0] / angle,
549 tangent.0[1] / angle,
550 tangent.0[2] / angle,
551 ];
552
553 Self::rotation(axis, angle)
554 }
555
556 fn log(&self) -> crate::error::LogResult<So3Algebra> {
557 use crate::error::LogError;
558
559 let trace = self.matrix.trace();
569
570 let cos_theta = ((trace - 1.0) / 2.0).clamp(-1.0, 1.0);
572 let theta = cos_theta.acos();
573
574 const SMALL_ANGLE_THRESHOLD: f64 = 1e-10;
575 const SINGULARITY_THRESHOLD: f64 = 1e-6;
576
577 if theta.abs() < SMALL_ANGLE_THRESHOLD {
578 return Ok(So3Algebra::zero());
580 }
581
582 if (theta - std::f64::consts::PI).abs() < SINGULARITY_THRESHOLD {
583 return Err(LogError::Singularity {
586 reason: format!(
587 "SO(3) element at rotation angle θ ≈ π (θ = {:.6}), axis is ambiguous",
588 theta
589 ),
590 });
591 }
592
593 let sin_theta = theta.sin();
606
607 if sin_theta.abs() < 1e-10 {
608 return Err(LogError::NumericalInstability {
610 reason: "sin(θ) too small for reliable axis extraction".to_string(),
611 });
612 }
613
614 let two_sin_theta = 2.0 * sin_theta;
615 let nx = (self.matrix[(2, 1)] - self.matrix[(1, 2)]) / two_sin_theta;
616 let ny = (self.matrix[(0, 2)] - self.matrix[(2, 0)]) / two_sin_theta;
617 let nz = (self.matrix[(1, 0)] - self.matrix[(0, 1)]) / two_sin_theta;
618
619 Ok(So3Algebra([theta * nx, theta * ny, theta * nz]))
621 }
622
623 fn dim() -> usize {
624 3 }
626
627 fn trace(&self) -> num_complex::Complex<f64> {
628 num_complex::Complex::new(self.matrix.trace(), 0.0)
630 }
631}
632
633use crate::traits::{Compact, SemiSimple, Simple};
638
639impl Compact for SO3 {}
644
645impl Simple for SO3 {}
649
650impl SemiSimple for SO3 {}
652
653impl TracelessByConstruction for So3Algebra {}
662
663impl AntiHermitianByConstruction for So3Algebra {}
668
669#[cfg(test)]
670mod tests {
671 use super::*;
672 use approx::assert_relative_eq;
673
674 #[test]
675 fn test_identity() {
676 let id = SO3::identity();
677 assert!(id.verify_orthogonality(1e-10));
678 assert_relative_eq!(id.distance_to_identity(), 0.0, epsilon = 1e-10);
679 }
680
681 #[test]
682 fn test_rotations_orthogonal() {
683 let rx = SO3::rotation_x(0.5);
684 let ry = SO3::rotation_y(1.2);
685 let rz = SO3::rotation_z(2.1);
686
687 assert!(rx.verify_orthogonality(1e-10));
688 assert!(ry.verify_orthogonality(1e-10));
689 assert!(rz.verify_orthogonality(1e-10));
690 }
691
692 #[test]
693 fn test_inverse() {
694 let r = SO3::rotation_x(0.7);
695 let r_inv = r.inverse();
696 let product = r.compose(&r_inv);
697
698 assert_relative_eq!(product.distance_to_identity(), 0.0, epsilon = 1e-10);
699 }
700
701 #[test]
702 fn test_bracket_antisymmetry() {
703 use crate::traits::LieAlgebra;
704
705 let v = So3Algebra([0.7, -0.3, 1.2]);
706 let w = So3Algebra([-0.5, 0.9, 0.4]);
707
708 let vw = v.bracket(&w);
709 let wv = w.bracket(&v);
710
711 for i in 0..3 {
713 assert_relative_eq!(vw.0[i], -wv.0[i], epsilon = 1e-14);
714 }
715 }
716
717 #[test]
718 fn test_bracket_bilinearity() {
719 use crate::traits::LieAlgebra;
720
721 let x = So3Algebra([1.0, 0.0, 0.0]);
722 let y = So3Algebra([0.0, 1.0, 0.0]);
723 let z = So3Algebra([0.0, 0.0, 1.0]);
724 let alpha = 2.5;
725
726 let lhs = x.scale(alpha).add(&y).bracket(&z);
728 let rhs = x.bracket(&z).scale(alpha).add(&y.bracket(&z));
729 for i in 0..3 {
730 assert_relative_eq!(lhs.0[i], rhs.0[i], epsilon = 1e-14);
731 }
732
733 let lhs = z.bracket(&x.scale(alpha).add(&y));
735 let rhs = z.bracket(&x).scale(alpha).add(&z.bracket(&y));
736 for i in 0..3 {
737 assert_relative_eq!(lhs.0[i], rhs.0[i], epsilon = 1e-14);
738 }
739 }
740
741 #[test]
742 fn test_algebra_bracket() {
743 use crate::traits::LieAlgebra;
744
745 let lx = So3Algebra::basis_element(0);
746 let ly = So3Algebra::basis_element(1);
747 let bracket = lx.bracket(&ly);
748
749 assert_relative_eq!(bracket.0[0], 0.0, epsilon = 1e-10);
751 assert_relative_eq!(bracket.0[1], 0.0, epsilon = 1e-10);
752 assert_relative_eq!(bracket.0[2], 1.0, epsilon = 1e-10);
753 }
754
755 #[test]
756 fn test_adjoint_action() {
757 use crate::traits::LieGroup;
758
759 let rz = SO3::rotation_z(std::f64::consts::FRAC_PI_2);
761 let lx = So3Algebra([1.0, 0.0, 0.0]);
762 let rotated = rz.adjoint_action(&lx);
763
764 assert_relative_eq!(rotated.0[0], 0.0, epsilon = 1e-10);
766 assert_relative_eq!(rotated.0[1], 1.0, epsilon = 1e-10);
767 assert_relative_eq!(rotated.0[2], 0.0, epsilon = 1e-10);
768 }
769
770 #[test]
771 fn test_interpolate_orthogonality() {
772 let r1 = SO3::rotation_x(0.3);
774 let r2 = SO3::rotation_y(1.2);
775
776 for t in [0.0, 0.25, 0.5, 0.75, 1.0] {
777 let interp = r1.interpolate(&r2, t);
778 assert!(
779 interp.verify_orthogonality(1e-10),
780 "Interpolated matrix not orthogonal at t={}",
781 t
782 );
783 }
784 }
785
786 #[test]
787 fn test_interpolate_endpoints() {
788 let r1 = SO3::rotation_z(0.5);
789 let r2 = SO3::rotation_z(1.5);
790
791 let at_0 = r1.interpolate(&r2, 0.0);
792 let at_1 = r1.interpolate(&r2, 1.0);
793
794 assert_relative_eq!(
795 at_0.distance_to_identity(),
796 r1.distance_to_identity(),
797 epsilon = 1e-10
798 );
799 assert_relative_eq!(
800 at_1.distance_to_identity(),
801 r2.distance_to_identity(),
802 epsilon = 1e-10
803 );
804 }
805
806 #[test]
807 fn test_gram_schmidt_orthogonalize() {
808 let perturbed = Matrix3::new(1.001, 0.01, 0.0, 0.0, 0.999, 0.01, 0.0, 0.0, 1.002);
810
811 let fixed = SO3::gram_schmidt_orthogonalize(perturbed);
812 assert!(fixed.verify_orthogonality(1e-10));
813
814 let det = fixed.matrix.determinant();
816 assert_relative_eq!(det, 1.0, epsilon = 1e-10);
817 }
818
819 #[test]
820 fn test_renormalize() {
821 let r = SO3::rotation_z(0.001);
823 let mut accumulated = SO3::identity();
824
825 for _ in 0..1000 {
826 accumulated = accumulated.compose(&r);
827 }
828
829 let renormalized = accumulated.renormalize();
831 assert!(renormalized.verify_orthogonality(1e-12));
832 }
833
834 #[test]
835 fn test_geodesic_distance_symmetric() {
836 let r1 = SO3::rotation_x(0.5);
837 let r2 = SO3::rotation_y(1.0);
838
839 let d12 = r1.geodesic_distance(&r2);
840 let d21 = r2.geodesic_distance(&r1);
841
842 assert_relative_eq!(d12, d21, epsilon = 1e-10);
843 }
844
845 #[test]
846 fn test_exp_log_roundtrip() {
847 use crate::traits::{LieAlgebra, LieGroup};
848
849 for &angle in &[0.1, 0.5, 1.0, 2.0] {
851 let v = So3Algebra([angle * 0.6, angle * 0.8, 0.0]);
852 let r = SO3::exp(&v);
853 assert!(r.verify_orthogonality(1e-10));
854
855 let v_back = SO3::log(&r).expect("log should succeed for small angles");
856 let diff = v.add(&v_back.scale(-1.0));
857 assert!(
858 diff.norm() < 1e-9,
859 "exp/log roundtrip failed at angle {}: error = {:.2e}",
860 angle,
861 diff.norm()
862 );
863 }
864 }
865
866 #[test]
867 fn test_log_exp_roundtrip() {
868 use crate::traits::LieGroup;
869
870 let rotations = [
872 SO3::rotation_x(0.7),
873 SO3::rotation_y(1.3),
874 SO3::rotation_z(0.4),
875 SO3::rotation_x(0.3).compose(&SO3::rotation_y(0.5)),
876 ];
877
878 for (i, r) in rotations.iter().enumerate() {
879 let v = SO3::log(r).expect("log should succeed");
880 let r_back = SO3::exp(&v);
881 let dist = r.geodesic_distance(&r_back);
882 assert!(
883 dist < 1e-9,
884 "log/exp roundtrip failed for rotation {}: distance = {:.2e}",
885 i,
886 dist
887 );
888 }
889 }
890
891 #[test]
892 fn test_jacobi_identity() {
893 use crate::traits::LieAlgebra;
894
895 let x = So3Algebra([1.0, 0.0, 0.0]);
896 let y = So3Algebra([0.0, 1.0, 0.0]);
897 let z = So3Algebra([0.0, 0.0, 1.0]);
898
899 let t1 = x.bracket(&y.bracket(&z));
901 let t2 = y.bracket(&z.bracket(&x));
902 let t3 = z.bracket(&x.bracket(&y));
903 let sum = t1.add(&t2).add(&t3);
904
905 assert!(
906 sum.norm() < 1e-14,
907 "Jacobi identity violated: ||sum|| = {:.2e}",
908 sum.norm()
909 );
910 }
911
912 #[test]
913 fn test_geodesic_distance_to_self() {
914 let r = SO3::rotation([1.0, 2.0, 3.0], 0.8);
915 let d = r.geodesic_distance(&r);
916 assert_relative_eq!(d, 0.0, epsilon = 1e-10);
917 }
918
919 #[test]
920 fn test_geodesic_distance_to_identity() {
921 let angle = 0.7;
922 let r = SO3::rotation_z(angle);
923
924 let d = SO3::identity().geodesic_distance(&r);
925 assert_relative_eq!(d, angle, epsilon = 1e-10);
926 }
927
928 #[cfg(feature = "proptest")]
941 use proptest::prelude::*;
942
943 #[cfg(feature = "proptest")]
950 fn arb_so3() -> impl Strategy<Value = SO3> {
951 use std::f64::consts::TAU;
952
953 let alpha = 0.0..TAU;
955 let beta = 0.0..TAU;
956 let gamma = 0.0..TAU;
957
958 (alpha, beta, gamma).prop_map(|(a, b, c)| {
959 SO3::rotation_z(a)
960 .compose(&SO3::rotation_y(b))
961 .compose(&SO3::rotation_x(c))
962 })
963 }
964
965 #[cfg(feature = "proptest")]
970 fn arb_so3_algebra() -> impl Strategy<Value = So3Algebra> {
971 use std::f64::consts::PI;
972
973 ((-PI..PI), (-PI..PI), (-PI..PI)).prop_map(|(a, b, c)| So3Algebra([a, b, c]))
974 }
975
976 #[cfg(feature = "proptest")]
977 proptest! {
978 #[test]
989 fn prop_identity_axiom(r in arb_so3()) {
990 let e = SO3::identity();
991
992 let left = e.compose(&r);
994 prop_assert!(
995 left.distance(&r) < 1e-7,
996 "Left identity failed: I·R != R, distance = {}",
997 left.distance(&r)
998 );
999
1000 let right = r.compose(&e);
1002 prop_assert!(
1003 right.distance(&r) < 1e-7,
1004 "Right identity failed: R·I != R, distance = {}",
1005 right.distance(&r)
1006 );
1007 }
1008
1009 #[test]
1020 fn prop_inverse_axiom(r in arb_so3()) {
1021 let r_inv = r.inverse();
1022
1023 let right_product = r.compose(&r_inv);
1025 prop_assert!(
1026 right_product.is_near_identity(1e-7),
1027 "Right inverse failed: R·R⁻¹ != I, distance = {}",
1028 right_product.distance_to_identity()
1029 );
1030
1031 let left_product = r_inv.compose(&r);
1033 prop_assert!(
1034 left_product.is_near_identity(1e-7),
1035 "Left inverse failed: R⁻¹·R != I, distance = {}",
1036 left_product.distance_to_identity()
1037 );
1038 }
1039
1040 #[test]
1050 fn prop_associativity(r1 in arb_so3(), r2 in arb_so3(), r3 in arb_so3()) {
1051 let left_assoc = r1.compose(&r2).compose(&r3);
1053
1054 let right_assoc = r1.compose(&r2.compose(&r3));
1056
1057 prop_assert!(
1058 left_assoc.distance(&right_assoc) < 1e-7,
1059 "Associativity failed: (R₁·R₂)·R₃ != R₁·(R₂·R₃), distance = {}",
1060 left_assoc.distance(&right_assoc)
1061 );
1062 }
1063
1064 #[test]
1069 fn prop_inverse_continuity(r in arb_so3()) {
1070 let epsilon = 0.01;
1072 let perturbation = SO3::rotation_x(epsilon);
1073 let r_perturbed = r.compose(&perturbation);
1074
1075 let inv_distance = r.inverse().distance(&r_perturbed.inverse());
1077
1078 prop_assert!(
1079 inv_distance < 0.1,
1080 "Inverse not continuous: small perturbation caused large inverse change, distance = {}",
1081 inv_distance
1082 );
1083 }
1084
1085 #[test]
1090 fn prop_orthogonality_preserved(r1 in arb_so3(), r2 in arb_so3()) {
1091 let product = r1.compose(&r2);
1093 prop_assert!(
1094 product.verify_orthogonality(1e-10),
1095 "Composition violated orthogonality"
1096 );
1097
1098 let inv = r1.inverse();
1100 prop_assert!(
1101 inv.verify_orthogonality(1e-10),
1102 "Inverse violated orthogonality"
1103 );
1104 }
1105
1106 #[test]
1114 fn prop_adjoint_homomorphism(
1115 r1 in arb_so3(),
1116 r2 in arb_so3(),
1117 v in arb_so3_algebra()
1118 ) {
1119 let r_composed = r1.compose(&r2);
1121 let left = r_composed.adjoint_action(&v);
1122
1123 let ad_r2_v = r2.adjoint_action(&v);
1125 let right = r1.adjoint_action(&ad_r2_v);
1126
1127 let diff = left.add(&right.scale(-1.0));
1129 prop_assert!(
1130 diff.norm() < 1e-7,
1131 "Adjoint homomorphism failed: Ad_{{R₁∘R₂}}(v) != Ad_{{R₁}}(Ad_{{R₂}}(v)), diff norm = {}",
1132 diff.norm()
1133 );
1134 }
1135
1136 #[test]
1141 fn prop_adjoint_identity(v in arb_so3_algebra()) {
1142 let e = SO3::identity();
1143 let result = e.adjoint_action(&v);
1144
1145 let diff = result.add(&v.scale(-1.0));
1146 prop_assert!(
1147 diff.norm() < 1e-10,
1148 "Identity action failed: Ad_I(v) != v, diff norm = {}",
1149 diff.norm()
1150 );
1151 }
1152
1153 #[test]
1161 fn prop_adjoint_bracket_preservation(
1162 r in arb_so3(),
1163 v in arb_so3_algebra(),
1164 w in arb_so3_algebra()
1165 ) {
1166 use crate::traits::LieAlgebra;
1167
1168 let bracket_vw = v.bracket(&w);
1170 let left = r.adjoint_action(&bracket_vw);
1171
1172 let ad_v = r.adjoint_action(&v);
1174 let ad_w = r.adjoint_action(&w);
1175 let right = ad_v.bracket(&ad_w);
1176
1177 let diff = left.add(&right.scale(-1.0));
1179 prop_assert!(
1180 diff.norm() < 1e-6,
1181 "Bracket preservation failed: Ad_R([v,w]) != [Ad_R(v), Ad_R(w)], diff norm = {}",
1182 diff.norm()
1183 );
1184 }
1185
1186 #[test]
1191 fn prop_adjoint_inverse(r in arb_so3(), v in arb_so3_algebra()) {
1192 let ad_r_v = r.adjoint_action(&v);
1194 let r_inv = r.inverse();
1195 let result = r_inv.adjoint_action(&ad_r_v);
1196
1197 let diff = result.add(&v.scale(-1.0));
1199 prop_assert!(
1200 diff.norm() < 1e-7,
1201 "Inverse property failed: Ad_{{R⁻¹}}(Ad_R(v)) != v, diff norm = {}",
1202 diff.norm()
1203 );
1204 }
1205 }
1206}