1use crate::{
31 LieGroup, Tangent,
32 so3::{SO3, SO3Tangent},
33};
34use nalgebra::{
35 Isometry3, Matrix3, Matrix4, Matrix6, Quaternion, SVector, Translation3, UnitQuaternion,
36 Vector3, Vector6,
37};
38use std::{
39 fmt,
40 fmt::{Display, Formatter},
41};
42
43#[derive(Clone, PartialEq)]
48pub struct SE3 {
49 params: SVector<f64, 7>,
51}
52
53impl Display for SE3 {
54 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
55 let t = self.translation();
56 let q = self.rotation_quaternion();
57 write!(
58 f,
59 "SE3(translation: [{:.4}, {:.4}, {:.4}], rotation: [w: {:.4}, x: {:.4}, y: {:.4}, z: {:.4}])",
60 t.x, t.y, t.z, q.w, q.i, q.j, q.k
61 )
62 }
63}
64
65impl SE3 {
66 pub const DIM: usize = 3;
68
69 pub const DOF: usize = 6;
71
72 pub const REP_SIZE: usize = 7;
74
75 #[inline]
76 fn translation_impl(&self) -> Vector3<f64> {
77 Vector3::new(self.params[0], self.params[1], self.params[2])
78 }
79
80 #[inline]
81 fn rotation_impl(&self) -> SO3 {
82 SO3::from_quaternion_wxyz(
83 self.params[3],
84 self.params[4],
85 self.params[5],
86 self.params[6],
87 )
88 }
89
90 #[inline]
91 fn from_parts(t: Vector3<f64>, r: &SO3) -> Self {
92 let q = r.params();
93 SE3 {
94 params: SVector::<f64, 7>::from([t.x, t.y, t.z, q[0], q[1], q[2], q[3]]),
95 }
96 }
97
98 pub fn identity() -> Self {
102 SE3 {
103 params: SVector::<f64, 7>::from([0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0]),
104 }
105 }
106
107 pub fn jacobian_identity() -> Matrix6<f64> {
111 Matrix6::<f64>::identity()
112 }
113
114 #[inline]
120 pub fn new(translation: Vector3<f64>, rotation: UnitQuaternion<f64>) -> Self {
121 SE3::from_parts(translation, &SO3::new(rotation))
122 }
123
124 pub fn from_translation_quaternion(
126 translation: Vector3<f64>,
127 quaternion: Quaternion<f64>,
128 ) -> Self {
129 let q = UnitQuaternion::from_quaternion(quaternion.normalize());
130 SE3::from_parts(translation, &SO3::new(q))
131 }
132
133 pub fn from_translation_euler(x: f64, y: f64, z: f64, roll: f64, pitch: f64, yaw: f64) -> Self {
135 let translation = Vector3::new(x, y, z);
136 let rotation = UnitQuaternion::from_euler_angles(roll, pitch, yaw);
137 SE3::from_parts(translation, &SO3::new(rotation))
138 }
139
140 pub fn from_isometry(isometry: Isometry3<f64>) -> Self {
142 SE3::from_parts(isometry.translation.vector, &SO3::new(isometry.rotation))
143 }
144
145 pub fn from_translation_so3(translation: Vector3<f64>, rotation: SO3) -> Self {
147 SE3::from_parts(translation, &rotation)
148 }
149
150 pub fn translation(&self) -> Vector3<f64> {
152 self.translation_impl()
153 }
154
155 pub fn rotation_so3(&self) -> SO3 {
157 self.rotation_impl()
158 }
159
160 pub fn rotation_quaternion(&self) -> UnitQuaternion<f64> {
162 self.rotation_impl().quaternion()
163 }
164
165 pub fn isometry(&self) -> Isometry3<f64> {
167 Isometry3::from_parts(
168 Translation3::from(self.translation()),
169 self.rotation_quaternion(),
170 )
171 }
172
173 pub fn matrix(&self) -> Matrix4<f64> {
175 self.isometry().to_homogeneous()
176 }
177
178 #[inline]
180 pub fn x(&self) -> f64 {
181 self.params[0]
182 }
183
184 #[inline]
186 pub fn y(&self) -> f64 {
187 self.params[1]
188 }
189
190 #[inline]
192 pub fn z(&self) -> f64 {
193 self.params[2]
194 }
195
196 pub fn coeffs(&self) -> [f64; 7] {
198 [
199 self.params[0],
200 self.params[1],
201 self.params[2],
202 self.params[3],
203 self.params[4],
204 self.params[5],
205 self.params[6],
206 ]
207 }
208}
209
210impl LieGroup for SE3 {
211 const NAME: &'static str = "SE3";
212
213 type TangentVector = SE3Tangent;
214 type JacobianMatrix = Matrix6<f64>;
215 type LieAlgebra = Matrix4<f64>;
216
217 fn inverse(&self, jacobian: Option<&mut Self::JacobianMatrix>) -> Self {
231 let rot = self.rotation_impl();
232 let rot_inv = rot.inverse(None);
233 let trans_inv = -rot_inv.act(&self.translation_impl(), None, None);
234
235 if let Some(jac) = jacobian {
236 *jac = -self.adjoint();
237 }
238
239 SE3::from_parts(trans_inv, &rot_inv)
240 }
241
242 fn compose(
262 &self,
263 other: &Self,
264 jacobian_self: Option<&mut Self::JacobianMatrix>,
265 jacobian_other: Option<&mut Self::JacobianMatrix>,
266 ) -> Self {
267 let rot = self.rotation_impl();
268 let composed_rotation = rot.compose(&other.rotation_impl(), None, None);
269 let composed_translation =
270 rot.act(&other.translation_impl(), None, None) + self.translation_impl();
271
272 let result = SE3::from_parts(composed_translation, &composed_rotation);
273
274 if let Some(jac_self) = jacobian_self {
275 *jac_self = other.inverse(None).adjoint();
276 }
277
278 if let Some(jac_other) = jacobian_other {
279 *jac_other = Matrix6::identity();
280 }
281
282 result
283 }
284
285 fn log(&self, jacobian: Option<&mut Self::JacobianMatrix>) -> Self::TangentVector {
299 let theta = self.rotation_impl().log(None);
300 let mut data = Vector6::zeros();
301 let translation_vector = theta.left_jacobian_inv() * self.translation_impl();
302 data.fixed_rows_mut::<3>(0).copy_from(&translation_vector);
303 data.fixed_rows_mut::<3>(3).copy_from(&theta.coeffs());
304 let result = SE3Tangent { data };
305 if let Some(jac) = jacobian {
306 *jac = result.right_jacobian_inv();
307 }
308
309 result
310 }
311
312 fn act(
313 &self,
314 vector: &Vector3<f64>,
315 jacobian_self: Option<&mut Self::JacobianMatrix>,
316 jacobian_vector: Option<&mut Matrix3<f64>>,
317 ) -> Vector3<f64> {
318 let rot = self.rotation_impl();
319 let result = rot.act(vector, None, None) + self.translation_impl();
320
321 if let Some(jac_self) = jacobian_self {
322 let rot_mat = rot.rotation_matrix();
323 jac_self.fixed_view_mut::<3, 3>(0, 0).copy_from(&rot_mat);
324 jac_self
325 .fixed_view_mut::<3, 3>(0, 3)
326 .copy_from(&(-rot_mat * SO3Tangent::new(*vector).hat()));
327 }
328
329 if let Some(jac_vector) = jacobian_vector {
330 jac_vector.copy_from(&rot.rotation_matrix());
331 }
332
333 result
334 }
335
336 fn adjoint(&self) -> Self::JacobianMatrix {
337 let rotation_matrix = self.rotation_impl().rotation_matrix();
338 let translation = self.translation_impl();
339 let mut adjoint_matrix = Matrix6::zeros();
340
341 adjoint_matrix
342 .fixed_view_mut::<3, 3>(0, 0)
343 .copy_from(&rotation_matrix);
344 adjoint_matrix
345 .fixed_view_mut::<3, 3>(3, 3)
346 .copy_from(&rotation_matrix);
347
348 let top_right = SO3Tangent::new(translation).hat() * rotation_matrix;
349 adjoint_matrix
350 .fixed_view_mut::<3, 3>(0, 3)
351 .copy_from(&top_right);
352
353 adjoint_matrix
354 }
355
356 fn random() -> Self {
357 use rand::Rng;
358 let mut rng = rand::rng();
359
360 let translation = Vector3::new(
361 rng.random_range(-1.0..1.0),
362 rng.random_range(-1.0..1.0),
363 rng.random_range(-1.0..1.0),
364 );
365 let rotation = SO3::random();
366
367 SE3::from_parts(translation, &rotation)
368 }
369
370 fn jacobian_identity() -> Self::JacobianMatrix {
371 Matrix6::<f64>::identity()
372 }
373
374 fn zero_jacobian() -> Self::JacobianMatrix {
375 Matrix6::<f64>::zeros()
376 }
377
378 fn normalize(&mut self) {
379 let mut rot = self.rotation_impl();
380 rot.normalize();
381 let q = rot.params();
382 self.params[3] = q[0];
383 self.params[4] = q[1];
384 self.params[5] = q[2];
385 self.params[6] = q[3];
386 }
387
388 fn is_valid(&self, tolerance: f64) -> bool {
389 self.translation_impl().iter().all(|t| t.is_finite())
392 && self.rotation_impl().is_valid(tolerance)
393 }
394
395 fn as_param_slice(&self) -> &[f64] {
396 self.params.as_slice()
397 }
398
399 fn as_param_slice_mut(&mut self) -> &mut [f64] {
420 self.params.as_mut_slice()
421 }
422
423 fn from_param_slice(s: &[f64]) -> Self {
424 debug_assert_eq!(s.len(), 7);
425 SE3 {
426 params: SVector::from_column_slice(s),
427 }
428 }
429
430 fn vee(&self) -> Self::TangentVector {
435 self.log(None)
436 }
437
438 fn is_approx(&self, other: &Self, tolerance: f64) -> bool {
444 let difference = self.right_minus(other, None, None);
445 difference.is_zero(tolerance)
446 }
447}
448
449#[derive(Clone, PartialEq)]
455pub struct SE3Tangent {
456 data: Vector6<f64>,
458}
459
460impl fmt::Display for SE3Tangent {
461 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
462 let rho = self.rho();
463 let theta = self.theta();
464 write!(
465 f,
466 "se3(rho: [{:.4}, {:.4}, {:.4}], theta: [{:.4}, {:.4}, {:.4}])",
467 rho.x, rho.y, rho.z, theta.x, theta.y, theta.z
468 )
469 }
470}
471
472impl SE3Tangent {
473 #[inline]
479 pub fn new(rho: Vector3<f64>, theta: Vector3<f64>) -> Self {
480 let mut data = Vector6::zeros();
481 data.fixed_rows_mut::<3>(0).copy_from(&rho);
482 data.fixed_rows_mut::<3>(3).copy_from(&theta);
483 SE3Tangent { data }
484 }
485
486 pub fn from_components(
488 rho_x: f64,
489 rho_y: f64,
490 rho_z: f64,
491 theta_x: f64,
492 theta_y: f64,
493 theta_z: f64,
494 ) -> Self {
495 SE3Tangent {
496 data: Vector6::new(rho_x, rho_y, rho_z, theta_x, theta_y, theta_z),
497 }
498 }
499
500 #[inline]
502 pub fn rho(&self) -> Vector3<f64> {
503 self.data.fixed_rows::<3>(0).into_owned()
504 }
505
506 #[inline]
508 pub fn theta(&self) -> Vector3<f64> {
509 self.data.fixed_rows::<3>(3).into_owned()
510 }
511
512 pub fn q_block_jacobian_matrix(rho: Vector3<f64>, theta: Vector3<f64>) -> Matrix3<f64> {
519 let rho_skew = SO3Tangent::new(rho).hat();
520 let theta_skew = SO3Tangent::new(theta).hat();
521 let theta_squared = theta.norm_squared();
522
523 let a = 0.5;
524 let mut b = 1.0 / 6.0 + 1.0 / 120.0 * theta_squared;
525 let mut c = -1.0 / 24.0 + 1.0 / 720.0 * theta_squared;
526 let mut d = -1.0 / 120.0 + theta_squared / 2520.0;
528
529 if theta_squared > crate::SMALL_ANGLE_THRESHOLD {
530 let theta_norm = theta_squared.sqrt();
531 let theta_norm_3 = theta_norm * theta_squared;
532 let theta_norm_4 = theta_squared * theta_squared;
533 let theta_norm_5 = theta_norm_3 * theta_squared;
534 let sin_theta = theta_norm.sin();
535 let cos_theta = theta_norm.cos();
536
537 b = (theta_norm - sin_theta) / theta_norm_3;
538 c = (1.0 - theta_squared / 2.0 - cos_theta) / theta_norm_4;
539 let e = (theta_norm - sin_theta - theta_norm_3 / 6.0) / theta_norm_5;
540 d = 0.5 * (c - 3.0 * e);
541 }
542
543 let tr = theta_skew * rho_skew;
544 let rt = rho_skew * theta_skew;
545 let trt = tr * theta_skew;
546 let rt_t2 = rt * theta_skew;
547 let trt2 = trt * theta_skew;
549 let t2rt = theta_skew * trt;
550
551 rho_skew * a + (tr + rt + trt) * b
552 - (rt_t2 - rt_t2.transpose() - trt * 3.0) * c
553 - (trt2 + t2rt) * d
554 }
555}
556
557impl Tangent<SE3> for SE3Tangent {
559 const DIM: usize = 6;
561
562 fn exp(&self, jacobian: Option<&mut <SE3 as LieGroup>::JacobianMatrix>) -> SE3 {
573 let rho = self.rho();
574 let theta = self.theta();
575
576 let theta_tangent = SO3Tangent::new(theta);
577 let rotation = theta_tangent.exp(None);
579 let translation = theta_tangent.left_jacobian() * rho;
580
581 if let Some(jac) = jacobian {
582 *jac = self.right_jacobian();
583 }
584
585 SE3::from_translation_so3(translation, rotation)
586 }
587
588 fn right_jacobian(&self) -> <SE3 as LieGroup>::JacobianMatrix {
598 let mut jac = Matrix6::zeros();
599 let rho = self.rho();
600 let theta = self.theta();
601 let theta_right_jac = SO3Tangent::new(theta).right_jacobian();
602 jac.fixed_view_mut::<3, 3>(0, 0).copy_from(&theta_right_jac);
603 jac.fixed_view_mut::<3, 3>(3, 3).copy_from(&theta_right_jac);
604 jac.fixed_view_mut::<3, 3>(0, 3)
605 .copy_from(&SE3Tangent::q_block_jacobian_matrix(-rho, -theta));
606 jac
607 }
608
609 fn left_jacobian(&self) -> <SE3 as LieGroup>::JacobianMatrix {
619 let mut jac = Matrix6::zeros();
620 let theta_left_jac = SO3Tangent::new(self.theta()).left_jacobian();
621 jac.fixed_view_mut::<3, 3>(0, 0).copy_from(&theta_left_jac);
622 jac.fixed_view_mut::<3, 3>(3, 3).copy_from(&theta_left_jac);
623 jac.fixed_view_mut::<3, 3>(0, 3)
624 .copy_from(&SE3Tangent::q_block_jacobian_matrix(
625 self.rho(),
626 self.theta(),
627 ));
628 jac
629 }
630
631 fn right_jacobian_inv(&self) -> <SE3 as LieGroup>::JacobianMatrix {
656 let mut jac = Matrix6::zeros();
657 let rho = self.rho();
658 let theta = self.theta();
659 let theta_right_inv_jac = SO3Tangent::new(theta).right_jacobian_inv();
660 let q_block_jac = SE3Tangent::q_block_jacobian_matrix(-rho, -theta);
661 jac.fixed_view_mut::<3, 3>(0, 0)
662 .copy_from(&theta_right_inv_jac);
663 jac.fixed_view_mut::<3, 3>(3, 3)
664 .copy_from(&theta_right_inv_jac);
665 let top_right = -1.0 * theta_right_inv_jac * q_block_jac * theta_right_inv_jac;
666 jac.fixed_view_mut::<3, 3>(0, 3).copy_from(&top_right);
667 jac
668 }
669
670 fn left_jacobian_inv(&self) -> <SE3 as LieGroup>::JacobianMatrix {
691 let mut jac = Matrix6::zeros();
692 let rho = self.rho();
693 let theta = self.theta();
694 let theta_left_inv_jac = SO3Tangent::new(theta).left_jacobian_inv();
695 let q_block_jac = SE3Tangent::q_block_jacobian_matrix(rho, theta);
696 let top_right_block = -1.0 * theta_left_inv_jac * q_block_jac * theta_left_inv_jac;
697 jac.fixed_view_mut::<3, 3>(0, 0)
698 .copy_from(&theta_left_inv_jac);
699 jac.fixed_view_mut::<3, 3>(3, 3)
700 .copy_from(&theta_left_inv_jac);
701 jac.fixed_view_mut::<3, 3>(0, 3).copy_from(&top_right_block);
702 jac
703 }
704
705 fn hat(&self) -> <SE3 as LieGroup>::LieAlgebra {
718 let mut lie_alg = Matrix4::zeros();
719
720 let theta_hat = SO3Tangent::new(self.theta()).hat();
722 lie_alg.view_mut((0, 0), (3, 3)).copy_from(&theta_hat);
723
724 let rho = self.rho();
726 lie_alg[(0, 3)] = rho[0];
727 lie_alg[(1, 3)] = rho[1];
728 lie_alg[(2, 3)] = rho[2];
729
730 lie_alg
731 }
732
733 fn zero() -> <SE3 as LieGroup>::TangentVector {
742 SE3Tangent::new(Vector3::zeros(), Vector3::zeros())
743 }
744
745 fn random() -> <SE3 as LieGroup>::TangentVector {
753 use rand::Rng;
754 let mut rng = rand::rng();
755 SE3Tangent::from_components(
756 rng.random_range(-1.0..1.0), rng.random_range(-1.0..1.0), rng.random_range(-1.0..1.0), rng.random_range(-0.1..0.1), rng.random_range(-0.1..0.1), rng.random_range(-0.1..0.1), )
763 }
764
765 fn is_zero(&self, tolerance: f64) -> bool {
775 self.data.norm() < tolerance
776 }
777
778 fn normalize(&mut self) {
783 let theta_norm = self.theta().norm();
784 self.data[3] /= theta_norm;
785 self.data[4] /= theta_norm;
786 self.data[5] /= theta_norm;
787 }
788
789 fn normalized(&self) -> <SE3 as LieGroup>::TangentVector {
797 let norm = self.theta().norm();
798 if norm > f64::EPSILON {
799 SE3Tangent::new(self.rho(), self.theta() / norm)
800 } else {
801 SE3Tangent::new(self.rho(), Vector3::zeros())
802 }
803 }
804
805 fn as_slice(&self) -> &[f64] {
806 self.data.as_slice()
807 }
808
809 fn from_slice(s: &[f64]) -> Self {
810 debug_assert_eq!(s.len(), 6);
811 SE3Tangent {
812 data: Vector6::from_column_slice(s),
813 }
814 }
815
816 fn small_adj(&self) -> <SE3 as LieGroup>::JacobianMatrix {
824 let mut small_adj = Matrix6::zeros();
825 let rho_skew = SO3Tangent::new(self.rho()).hat();
826 let theta_skew = SO3Tangent::new(self.theta()).hat();
827
828 small_adj
830 .fixed_view_mut::<3, 3>(0, 0)
831 .copy_from(&theta_skew);
832 small_adj
833 .fixed_view_mut::<3, 3>(3, 3)
834 .copy_from(&theta_skew);
835
836 small_adj.fixed_view_mut::<3, 3>(0, 3).copy_from(&rho_skew);
838
839 small_adj
842 }
843
844 fn lie_bracket(&self, other: &Self) -> <SE3 as LieGroup>::TangentVector {
848 let bracket_result = self.small_adj() * other.data;
849 SE3Tangent {
850 data: bracket_result,
851 }
852 }
853
854 fn is_approx(&self, other: &Self, tolerance: f64) -> bool {
860 (self.data - other.data).norm() < tolerance
861 }
862
863 fn generator(&self, i: usize) -> <SE3 as LieGroup>::LieAlgebra {
871 assert!(i < 6, "SE(3) only has generators for indices 0-5");
872
873 let mut generator = Matrix4::zeros();
874
875 match i {
876 0 => {
877 generator[(0, 3)] = 1.0;
879 }
880 1 => {
881 generator[(1, 3)] = 1.0;
883 }
884 2 => {
885 generator[(2, 3)] = 1.0;
887 }
888 3 => {
889 generator[(1, 2)] = -1.0;
891 generator[(2, 1)] = 1.0;
892 }
893 4 => {
894 generator[(0, 2)] = 1.0;
896 generator[(2, 0)] = -1.0;
897 }
898 5 => {
899 generator[(0, 1)] = -1.0;
901 generator[(1, 0)] = 1.0;
902 }
903 _ => unreachable!(),
904 }
905
906 generator
907 }
908}
909
910#[cfg(test)]
911mod tests {
912 use super::*;
913 use Quaternion;
914 use std::f64::consts::PI;
915
916 const TOLERANCE: f64 = 1e-9;
917
918 #[test]
919 fn test_se3_tangent_basic() {
920 let linear = Vector3::new(1.0, 2.0, 3.0);
921 let angular = Vector3::new(0.1, 0.2, 0.3);
922 let tangent = SE3Tangent::new(linear, angular);
923
924 assert_eq!(tangent.rho(), linear);
925 assert_eq!(tangent.theta(), angular);
926 }
927
928 #[test]
929 fn test_se3_tangent_zero() {
930 let zero = SE3Tangent::zero();
931 assert_eq!(zero.data, Vector6::zeros());
932
933 let tangent = SE3Tangent::from_components(0.0, 0.0, 0.0, 0.0, 0.0, 0.0);
934 assert!(tangent.is_zero(1e-10));
935 }
936
937 #[test]
939 fn test_se3_identity() {
940 let identity = SE3::identity();
941 assert!(identity.is_valid(TOLERANCE));
942
943 let translation = identity.translation();
944 let rotation = identity.rotation_quaternion();
945
946 assert!(translation.norm() < TOLERANCE);
947 assert!((rotation.angle()) < TOLERANCE);
948 }
949
950 #[test]
954 fn test_se3_is_valid_rejects_nonfinite_at_every_index() {
955 let base = SE3::identity();
956 for idx in 0..SE3::REP_SIZE {
957 for bad in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
958 let mut params: Vec<f64> = base.as_param_slice().to_vec();
959 params[idx] = bad;
960 let perturbed = SE3::from_param_slice(¶ms);
961 assert!(
962 !perturbed.is_valid(TOLERANCE),
963 "index {idx} = {bad} must be rejected"
964 );
965 }
966 }
967 assert!(
968 !base.is_valid(f64::INFINITY),
969 "infinite tolerance must be rejected"
970 );
971 assert!(!base.is_valid(f64::NAN), "NaN tolerance must be rejected");
972 }
973
974 #[test]
975 fn test_se3_new() {
976 let translation = Vector3::new(1.0, 2.0, 3.0);
977 let rotation = UnitQuaternion::from_euler_angles(0.1, 0.2, 0.3);
978
979 let se3 = SE3::new(translation, rotation);
980
981 assert!(se3.is_valid(TOLERANCE));
982 assert!((se3.translation() - translation).norm() < TOLERANCE);
983 assert!((se3.rotation_quaternion().angle() - rotation.angle()).abs() < TOLERANCE);
984 }
985
986 #[test]
987 fn test_se3_random() {
988 let se3 = SE3::random();
989 assert!(se3.is_valid(TOLERANCE));
990 }
991
992 #[test]
993 fn test_se3_inverse() {
994 let se3 = SE3::random();
995 let se3_inv = se3.inverse(None);
996
997 let composed = se3.compose(&se3_inv, None, None);
999 let identity = SE3::identity();
1000
1001 let translation_diff = (composed.translation() - identity.translation()).norm();
1002 let rotation_diff = composed.rotation_quaternion().angle();
1003
1004 assert!(translation_diff < TOLERANCE);
1005 assert!(rotation_diff < TOLERANCE);
1006 }
1007
1008 #[test]
1009 fn test_se3_compose() {
1010 let se3_1 = SE3::random();
1011 let se3_2 = SE3::random();
1012
1013 let composed = se3_1.compose(&se3_2, None, None);
1014 assert!(composed.is_valid(TOLERANCE));
1015
1016 let identity = SE3::identity();
1018 let composed_with_identity = se3_1.compose(&identity, None, None);
1019
1020 let translation_diff = (composed_with_identity.translation() - se3_1.translation()).norm();
1021 let rotation_diff = (composed_with_identity.rotation_quaternion().angle()
1022 - se3_1.rotation_quaternion().angle())
1023 .abs();
1024
1025 assert!(translation_diff < TOLERANCE);
1026 assert!(rotation_diff < TOLERANCE);
1027 }
1028
1029 #[test]
1030 fn test_se3_adjoint() {
1031 let se3 = SE3::random();
1032 let adj = se3.adjoint();
1033
1034 assert_eq!(adj.nrows(), 6);
1036 assert_eq!(adj.ncols(), 6);
1037
1038 let det = adj.determinant();
1041 assert!((det - 1.0).abs() < TOLERANCE);
1042 }
1043
1044 #[test]
1045 fn test_se3_act() {
1046 let se3 = SE3::random();
1047 let point = Vector3::new(1.0, 2.0, 3.0);
1048
1049 let _transformed_point = se3.act(&point, None, None);
1050
1051 let identity = SE3::identity();
1053 let identity_transformed = identity.act(&point, None, None);
1054
1055 let diff = (identity_transformed - point).norm();
1056 assert!(diff < TOLERANCE);
1057 }
1058
1059 #[test]
1060 fn test_se3_between() {
1061 let se3a = SE3::from_translation_euler(1.0, 2.0, 3.0, 0.1, 0.2, 0.3);
1062 let se3b = se3a.clone();
1063 let se3_between_identity = se3a.between(&se3b, None, None);
1064 assert!(se3_between_identity.is_approx(&SE3::identity(), TOLERANCE));
1065
1066 let se3c = SE3::from_translation_euler(4.0, 5.0, 6.0, 0.4, 0.5, 0.6);
1067 let se3_between = se3a.between(&se3c, None, None);
1068 let expected = se3a.inverse(None).compose(&se3c, None, None);
1069 assert!(se3_between.is_approx(&expected, TOLERANCE));
1070 }
1071
1072 #[test]
1073 fn test_se3_exp_log() {
1074 let tangent_vec = Vector6::new(0.1, 0.2, 0.3, 0.01, 0.02, 0.03);
1075 let tangent = SE3Tangent { data: tangent_vec };
1076
1077 let se3 = tangent.exp(None);
1079 let recovered_tangent = se3.log(None);
1080
1081 let diff = (tangent.data - recovered_tangent.data).norm();
1082 assert!(diff < TOLERANCE);
1083 }
1084
1085 #[test]
1086 fn test_se3_exp_zero() {
1087 let zero_tangent = SE3Tangent::zero();
1088 let se3 = zero_tangent.exp(None);
1089 let identity = SE3::identity();
1090
1091 let translation_diff = (se3.translation() - identity.translation()).norm();
1092 let rotation_diff = se3.rotation_quaternion().angle();
1093
1094 assert!(translation_diff < TOLERANCE);
1095 assert!(rotation_diff < TOLERANCE);
1096 }
1097
1098 #[test]
1099 fn test_se3_log_identity() {
1100 let identity = SE3::identity();
1101 let tangent = identity.log(None);
1102
1103 assert!(tangent.data.norm() < TOLERANCE);
1104 }
1105
1106 #[test]
1107 fn test_se3_normalize() {
1108 let translation = Vector3::new(1.0, 2.0, 3.0);
1109 let rotation =
1110 UnitQuaternion::from_quaternion(Quaternion::new(0.5, 0.5, 0.5, 0.5).normalize()); let mut se3 = SE3::new(translation, rotation);
1113 se3.normalize();
1114
1115 assert!(se3.is_valid(TOLERANCE));
1116 }
1117
1118 #[test]
1119 fn test_se3_manifold_properties() {
1120 assert_eq!(SE3::DIM, 3);
1122 assert_eq!(SE3::DOF, 6);
1123 assert_eq!(SE3::REP_SIZE, 7);
1124 }
1125
1126 #[test]
1127 fn test_se3_consistency() {
1128 let se3_1 = SE3::random();
1130 let se3_2 = SE3::random();
1131
1132 let se3_3 = SE3::random();
1134 let left_assoc = se3_1
1135 .compose(&se3_2, None, None)
1136 .compose(&se3_3, None, None);
1137 let right_assoc = se3_1.compose(&se3_2.compose(&se3_3, None, None), None, None);
1138
1139 let translation_diff = (left_assoc.translation() - right_assoc.translation()).norm();
1140 let rotation_diff = (left_assoc.rotation_quaternion().angle()
1141 - right_assoc.rotation_quaternion().angle())
1142 .abs();
1143
1144 assert!(translation_diff < 1e-10);
1145 assert!(rotation_diff < 1e-10);
1146 }
1147
1148 #[test]
1149 fn test_se3_specific_values() {
1150 let translation_only = SE3::new(Vector3::new(1.0, 2.0, 3.0), UnitQuaternion::identity());
1154
1155 let point = Vector3::new(0.0, 0.0, 0.0);
1156 let transformed = translation_only.act(&point, None, None);
1157 let expected = Vector3::new(1.0, 2.0, 3.0);
1158
1159 assert!((transformed - expected).norm() < TOLERANCE);
1160
1161 let rotation_only = SE3::new(
1163 Vector3::zeros(),
1164 UnitQuaternion::from_euler_angles(PI / 2.0, 0.0, 0.0),
1165 );
1166
1167 let point_y = Vector3::new(0.0, 1.0, 0.0);
1168 let rotated = rotation_only.act(&point_y, None, None);
1169 let expected_rotated = Vector3::new(0.0, 0.0, 1.0);
1170
1171 assert!((rotated - expected_rotated).norm() < TOLERANCE);
1172 }
1173
1174 #[test]
1175 fn test_se3_small_angle_approximations() {
1176 let small_tangent = Vector6::new(1e-8, 2e-8, 3e-8, 1e-9, 2e-9, 3e-9);
1178
1179 let se3 = SE3::new(
1180 Vector3::new(1e-8, 2e-8, 3e-8),
1181 UnitQuaternion::from_euler_angles(1e-9, 2e-9, 3e-9),
1182 );
1183 let recovered = se3.log(None);
1184
1185 let diff = (small_tangent - recovered.data).norm();
1186 assert!(diff < TOLERANCE);
1187 }
1188
1189 #[test]
1190 fn test_se3_tangent_norm() {
1191 let tangent_vec = Vector6::new(3.0, 4.0, 0.0, 0.0, 0.0, 0.0);
1192 let tangent = SE3Tangent { data: tangent_vec };
1193
1194 let norm = tangent.data.norm();
1195 assert!((norm - 5.0).abs() < TOLERANCE); }
1197
1198 #[test]
1199 fn test_se3_from_components() {
1200 let translation = Vector3::new(1.0, 2.0, 3.0);
1201 let quaternion = Quaternion::new(1.0, 0.0, 0.0, 0.0);
1202 let se3 = SE3::from_translation_quaternion(translation, quaternion);
1203 assert!(se3.is_valid(TOLERANCE));
1204 assert_eq!(se3.x(), 1.0);
1205 assert_eq!(se3.y(), 2.0);
1206 assert_eq!(se3.z(), 3.0);
1207
1208 let quat = se3.rotation_quaternion();
1209 assert!((quat.w - 1.0).abs() < TOLERANCE);
1210 assert!(quat.i.abs() < TOLERANCE);
1211 assert!(quat.j.abs() < TOLERANCE);
1212 assert!(quat.k.abs() < TOLERANCE);
1213 }
1214
1215 #[test]
1216 fn test_se3_from_isometry() {
1217 let translation = Translation3::new(1.0, 2.0, 3.0);
1218 let rotation = UnitQuaternion::from_euler_angles(0.1, 0.2, 0.3);
1219 let isometry = Isometry3::from_parts(translation, rotation);
1220
1221 let se3 = SE3::from_isometry(isometry);
1222 let recovered_isometry = se3.isometry();
1223
1224 let translation_diff =
1225 (isometry.translation.vector - recovered_isometry.translation.vector).norm();
1226 let rotation_diff = (isometry.rotation.angle() - recovered_isometry.rotation.angle()).abs();
1227
1228 assert!(translation_diff < TOLERANCE);
1229 assert!(rotation_diff < TOLERANCE);
1230 }
1231
1232 #[test]
1233 fn test_se3_matrix() {
1234 let se3 = SE3::random();
1235 let matrix = se3.matrix();
1236
1237 assert_eq!(matrix.nrows(), 4);
1239 assert_eq!(matrix.ncols(), 4);
1240
1241 assert!((matrix[(3, 0)]).abs() < TOLERANCE);
1243 assert!((matrix[(3, 1)]).abs() < TOLERANCE);
1244 assert!((matrix[(3, 2)]).abs() < TOLERANCE);
1245 assert!((matrix[(3, 3)] - 1.0).abs() < TOLERANCE);
1246 }
1247
1248 #[test]
1250 fn test_se3_manif_like_operations() {
1251 let g1 = SE3::new(
1255 Vector3::new(1.0, 0.0, 0.0),
1256 UnitQuaternion::from_euler_angles(0.0, 0.0, PI / 4.0),
1257 );
1258
1259 let g2 = SE3::new(
1260 Vector3::new(0.0, 1.0, 0.0),
1261 UnitQuaternion::from_euler_angles(0.0, PI / 4.0, 0.0),
1262 );
1263
1264 let g3 = g1.compose(&g2, None, None);
1266 assert!(g3.is_valid(TOLERANCE));
1267
1268 let g2_inv = g2.inverse(None);
1270 let g1_inv = g1.inverse(None);
1271 let result = g1
1272 .compose(&g2, None, None)
1273 .compose(&g2_inv, None, None)
1274 .compose(&g1_inv, None, None);
1275
1276 let identity = SE3::identity();
1277 let translation_diff = (result.translation() - identity.translation()).norm();
1278 let rotation_diff = result.rotation_quaternion().angle();
1279
1280 assert!(translation_diff < TOLERANCE);
1281 assert!(rotation_diff < TOLERANCE);
1282 }
1283
1284 #[test]
1285 fn test_se3_tangent_exp_jacobians() {
1286 let tangent = SE3Tangent::new(Vector3::new(0.1, 0.0, 0.0), Vector3::new(0.0, 0.1, 0.0));
1287
1288 let se3_element = tangent.exp(None);
1290 assert!(se3_element.is_valid(TOLERANCE));
1291
1292 let another_tangent = SE3Tangent::new(
1294 Vector3::new(0.01, 0.02, 0.03),
1295 Vector3::new(0.001, 0.002, 0.003),
1296 );
1297 let another_se3 = another_tangent.exp(None);
1298 assert!(another_se3.is_valid(TOLERANCE));
1299
1300 let _right_jac = tangent.right_jacobian();
1302 let _left_jac = tangent.left_jacobian();
1303 let _right_jac_inv = tangent.right_jacobian_inv();
1304 let _left_jac_inv = tangent.left_jacobian_inv();
1305
1306 assert_eq!(_right_jac.nrows(), 6);
1308 assert_eq!(_right_jac.ncols(), 6);
1309 assert_eq!(_left_jac.nrows(), 6);
1310 assert_eq!(_left_jac.ncols(), 6);
1311 assert_eq!(_right_jac_inv.nrows(), 6);
1312 assert_eq!(_right_jac_inv.ncols(), 6);
1313 assert_eq!(_left_jac_inv.nrows(), 6);
1314 assert_eq!(_left_jac_inv.ncols(), 6);
1315 }
1316
1317 #[test]
1318 fn test_se3_tangent_utility_functions() {
1319 let zero_vec = SE3Tangent::zero();
1321 assert!(zero_vec.data.norm() < TOLERANCE);
1322
1323 let random_vec = SE3Tangent::random();
1325 assert!(random_vec.data.norm() > 0.0);
1326
1327 let tangent = SE3Tangent::new(Vector3::zeros(), Vector3::zeros());
1329 assert!(tangent.is_zero(1e-10));
1330
1331 let non_zero_tangent = SE3Tangent::new(Vector3::new(1e-5, 0.0, 0.0), Vector3::zeros());
1332 assert!(!non_zero_tangent.is_zero(1e-10));
1333 }
1334
1335 #[test]
1338 fn test_se3_vee() {
1339 let se3 = SE3::random();
1340 let tangent_log = se3.log(None);
1341 let tangent_vee = se3.vee();
1342
1343 assert!((tangent_log.data - tangent_vee.data).norm() < 1e-10);
1344 }
1345
1346 #[test]
1347 fn test_se3_is_approx() {
1348 let se3_1 = SE3::random();
1349 let se3_2 = se3_1.clone();
1350
1351 assert!(se3_1.is_approx(&se3_1, 1e-10));
1352 assert!(se3_1.is_approx(&se3_2, 1e-10));
1353
1354 let small_tangent = SE3Tangent::new(
1356 Vector3::new(1e-12, 1e-12, 1e-12),
1357 Vector3::new(1e-12, 1e-12, 1e-12),
1358 );
1359 let se3_perturbed = se3_1.right_plus(&small_tangent, None, None);
1360 assert!(se3_1.is_approx(&se3_perturbed, 1e-10));
1361 }
1362
1363 #[test]
1364 fn test_se3_tangent_small_adj() {
1365 let tangent = SE3Tangent::new(Vector3::new(0.1, 0.2, 0.3), Vector3::new(0.4, 0.5, 0.6));
1366 let small_adj = tangent.small_adj();
1367
1368 let rho_skew = SO3Tangent::new(tangent.rho()).hat();
1373 let theta_skew = SO3Tangent::new(tangent.theta()).hat();
1374
1375 let top_left = small_adj.fixed_view::<3, 3>(0, 0);
1377 let bottom_right = small_adj.fixed_view::<3, 3>(3, 3);
1378 assert!((top_left - theta_skew).norm() < 1e-10);
1379 assert!((bottom_right - theta_skew).norm() < 1e-10);
1380
1381 let top_right = small_adj.fixed_view::<3, 3>(0, 3);
1383 assert!((top_right - rho_skew).norm() < 1e-10);
1384
1385 let bottom_left = small_adj.fixed_view::<3, 3>(3, 0);
1387 assert!(bottom_left.norm() < 1e-10);
1388 }
1389
1390 #[test]
1391 fn test_se3_tangent_lie_bracket() {
1392 let tangent_a = SE3Tangent::new(Vector3::new(0.1, 0.0, 0.0), Vector3::new(0.0, 0.2, 0.0));
1393 let tangent_b = SE3Tangent::new(Vector3::new(0.0, 0.3, 0.0), Vector3::new(0.0, 0.0, 0.4));
1394
1395 let bracket_ab = tangent_a.lie_bracket(&tangent_b);
1396 let bracket_ba = tangent_b.lie_bracket(&tangent_a);
1397
1398 assert!((bracket_ab.data + bracket_ba.data).norm() < 1e-10);
1400
1401 let bracket_aa = tangent_a.lie_bracket(&tangent_a);
1403 assert!(bracket_aa.is_zero(1e-10));
1404
1405 let bracket_hat = bracket_ab.hat();
1407 let expected = tangent_a.hat() * tangent_b.hat() - tangent_b.hat() * tangent_a.hat();
1408 assert!((bracket_hat - expected).norm() < 1e-10);
1409 }
1410
1411 #[test]
1412 fn test_se3_tangent_is_approx() {
1413 let tangent_1 = SE3Tangent::new(Vector3::new(0.1, 0.2, 0.3), Vector3::new(0.4, 0.5, 0.6));
1414 let tangent_2 = SE3Tangent::new(
1415 Vector3::new(0.1 + 1e-12, 0.2, 0.3),
1416 Vector3::new(0.4, 0.5, 0.6),
1417 );
1418 let tangent_3 = SE3Tangent::new(Vector3::new(0.7, 0.8, 0.9), Vector3::new(1.0, 1.1, 1.2));
1419
1420 assert!(tangent_1.is_approx(&tangent_1, 1e-10));
1421 assert!(tangent_1.is_approx(&tangent_2, 1e-10));
1422 assert!(!tangent_1.is_approx(&tangent_3, 1e-10));
1423 }
1424
1425 #[test]
1426 fn test_se3_generators() {
1427 let tangent = SE3Tangent::new(Vector3::new(1.0, 1.0, 1.0), Vector3::new(1.0, 1.0, 1.0));
1428
1429 for i in 0..6 {
1431 let generator = tangent.generator(i);
1432
1433 assert_eq!(generator.nrows(), 4);
1435 assert_eq!(generator.ncols(), 4);
1436
1437 assert_eq!(generator[(3, 0)], 0.0);
1439 assert_eq!(generator[(3, 1)], 0.0);
1440 assert_eq!(generator[(3, 2)], 0.0);
1441 assert_eq!(generator[(3, 3)], 0.0);
1442 }
1443
1444 let e1 = tangent.generator(0); let e2 = tangent.generator(1); let e3 = tangent.generator(2); assert_eq!(e1[(0, 3)], 1.0);
1450 assert_eq!(e2[(1, 3)], 1.0);
1451 assert_eq!(e3[(2, 3)], 1.0);
1452
1453 let e4 = tangent.generator(3); let e5 = tangent.generator(4); let e6 = tangent.generator(5); assert_eq!(e4[(1, 2)], -1.0);
1460 assert_eq!(e4[(2, 1)], 1.0);
1461 assert_eq!(e5[(0, 2)], 1.0);
1462 assert_eq!(e5[(2, 0)], -1.0);
1463 assert_eq!(e6[(0, 1)], -1.0);
1464 assert_eq!(e6[(1, 0)], 1.0);
1465 }
1466
1467 #[test]
1468 #[should_panic]
1469 fn test_se3_generator_invalid_index() {
1470 let tangent = SE3Tangent::new(Vector3::new(1.0, 1.0, 1.0), Vector3::new(1.0, 1.0, 1.0));
1471 let _generator = tangent.generator(6); }
1473
1474 #[test]
1475 fn test_se3_jacobi_identity() {
1476 let x = SE3Tangent::new(Vector3::new(0.1, 0.0, 0.0), Vector3::new(0.0, 0.1, 0.0));
1478 let y = SE3Tangent::new(Vector3::new(0.0, 0.2, 0.0), Vector3::new(0.0, 0.0, 0.2));
1479 let z = SE3Tangent::new(Vector3::new(0.0, 0.0, 0.3), Vector3::new(0.3, 0.0, 0.0));
1480
1481 let term1 = x.lie_bracket(&y.lie_bracket(&z));
1482 let term2 = y.lie_bracket(&z.lie_bracket(&x));
1483 let term3 = z.lie_bracket(&x.lie_bracket(&y));
1484
1485 let jacobi_sum = SE3Tangent {
1486 data: term1.data + term2.data + term3.data,
1487 };
1488 assert!(jacobi_sum.is_zero(1e-10));
1489 }
1490
1491 #[test]
1492 fn test_se3_hat_matrix_structure() {
1493 let tangent = SE3Tangent::new(Vector3::new(0.1, 0.2, 0.3), Vector3::new(0.4, 0.5, 0.6));
1494 let hat_matrix = tangent.hat();
1495
1496 assert_eq!(hat_matrix[(0, 3)], tangent.rho()[0]);
1499 assert_eq!(hat_matrix[(1, 3)], tangent.rho()[1]);
1500 assert_eq!(hat_matrix[(2, 3)], tangent.rho()[2]);
1501
1502 let theta_hat = SO3Tangent::new(tangent.theta()).hat();
1504 let top_left = hat_matrix.fixed_view::<3, 3>(0, 0);
1505 assert!((top_left - theta_hat).norm() < 1e-10);
1506
1507 assert_eq!(hat_matrix[(3, 0)], 0.0);
1509 assert_eq!(hat_matrix[(3, 1)], 0.0);
1510 assert_eq!(hat_matrix[(3, 2)], 0.0);
1511 assert_eq!(hat_matrix[(3, 3)], 0.0);
1512 }
1513
1514 #[test]
1530 fn test_se3_accumulated_error_odometry() {
1531 let step = SE3::new(
1533 Vector3::new(1.0, 0.0, 0.0), UnitQuaternion::from_euler_angles(0.0, 0.0, 0.1), );
1536
1537 let mut pose = SE3::identity();
1538 for _ in 0..10 {
1539 pose = pose.compose(&step, None, None);
1540 }
1541
1542 let expected = SE3::new(
1544 Vector3::new(10.0, 0.0, 0.0),
1545 UnitQuaternion::from_euler_angles(0.0, 0.0, 1.0),
1546 );
1547
1548 assert!(pose.is_approx(&expected, 5.0));
1551 }
1552
1553 #[test]
1567 fn test_se3_large_translation_small_rotation() {
1568 let large_t = Vector3::new(1000.0, 2000.0, 500.0);
1570 let small_r = SO3::from_scaled_axis(Vector3::new(1e-6, 2e-6, 3e-6));
1571 let se3 = SE3::from_translation_so3(large_t, small_r);
1572
1573 let tangent = se3.log(None);
1574 let recovered = tangent.exp(None);
1575
1576 assert!(se3.is_approx(&recovered, 1e-3));
1578 }
1579
1580 #[test]
1581 fn test_se3_small_translation_large_rotation() {
1582 let small_t = Vector3::new(0.001, 0.002, -0.001);
1584 let large_r = SO3::from_euler_angles(1.5, 0.5, -1.2);
1586 let se3 = SE3::from_translation_so3(small_t, large_r);
1587
1588 let tangent = se3.log(None);
1589 let recovered = tangent.exp(None);
1590
1591 assert!(se3.is_approx(&recovered, 1e-3));
1593 }
1594
1595 #[test]
1596 fn test_se3_right_jacobian_inverse_identity() {
1597 let tangent = SE3Tangent::new(
1598 Vector3::new(0.1, 0.15, 0.2),
1599 Vector3::new(0.001, 0.002, 0.003),
1600 );
1601 let jr = tangent.right_jacobian();
1602 let jr_inv = tangent.right_jacobian_inv();
1603 let product = jr * jr_inv;
1604 let identity = Matrix6::identity();
1605
1606 assert!(
1607 (product - identity).norm() < 1e-10,
1608 "Jr * Jr_inv should be identity, got error: {}",
1609 (product - identity).norm()
1610 );
1611 }
1612
1613 #[test]
1614 fn test_se3_left_jacobian_inverse_identity() {
1615 let tangent = SE3Tangent::new(
1616 Vector3::new(0.1, 0.15, 0.2),
1617 Vector3::new(0.001, 0.002, 0.003),
1618 );
1619 let jl = tangent.left_jacobian();
1620 let jl_inv = tangent.left_jacobian_inv();
1621 let product = jl * jl_inv;
1622
1623 assert!(
1624 (product - Matrix6::identity()).norm() < 1e-10,
1625 "Jl * Jl_inv should be identity, got error: {}",
1626 (product - Matrix6::identity()).norm()
1627 );
1628 }
1629
1630 #[test]
1631 fn se3_param_slice_round_trip() {
1632 let g = SE3::random();
1633 let recovered = SE3::from_param_slice(g.as_param_slice());
1634 assert!(g.is_approx(&recovered, 1e-14));
1635 }
1636
1637 #[test]
1638 fn se3_param_slice_mut_modifies_in_place() {
1639 let mut g = SE3::identity();
1640 g.as_param_slice_mut()[0] = 1.0;
1641 assert_eq!(g.translation().x, 1.0);
1642 }
1643
1644 #[test]
1645 fn se3_tangent_slice_round_trip() {
1646 let t = SE3Tangent::random();
1647 let recovered = SE3Tangent::from_slice(t.as_slice());
1648 assert!(t.is_approx(&recovered, 1e-14));
1649 }
1650}
1651
1652#[cfg(test)]
1657mod se3_jacobian_fd_tests {
1658 use super::*;
1659
1660 const FD_STEP: f64 = 1e-4;
1661 const FD_TOL: f64 = 1e-5;
1662
1663 fn vee(m: &Matrix4<f64>) -> Vector6<f64> {
1665 let mut v = Vector6::zeros();
1666 v[0] = m[(0, 3)];
1667 v[1] = m[(1, 3)];
1668 v[2] = m[(2, 3)];
1669 v[3] = m[(2, 1)];
1670 v[4] = m[(0, 2)];
1671 v[5] = m[(1, 0)];
1672 v
1673 }
1674
1675 fn perturbed(xi: &SE3Tangent, k: usize, eps: f64) -> SE3Tangent {
1676 let mut data = xi.data;
1677 data[k] += eps;
1678 SE3Tangent { data }
1679 }
1680
1681 fn fd_right_jacobian(xi: &SE3Tangent) -> Matrix6<f64> {
1682 let x_inv_mat = xi.exp(None).inverse(None).matrix();
1683 let mut jac = Matrix6::zeros();
1684 for k in 0..6 {
1685 let plus = perturbed(xi, k, FD_STEP).exp(None).matrix();
1686 let minus = perturbed(xi, k, -FD_STEP).exp(None).matrix();
1687 let d_dxi_k = (plus - minus) / (2.0 * FD_STEP);
1688 jac.set_column(k, &vee(&(x_inv_mat * d_dxi_k)));
1689 }
1690 jac
1691 }
1692
1693 #[test]
1694 fn se3_right_jacobian_matches_matrix_exp_central_difference() {
1695 let cases = [
1696 (Vector3::new(0.7, -0.2, 0.4), Vector3::new(0.4, -0.3, 0.25)),
1697 (Vector3::new(0.1, 0.2, -0.3), Vector3::new(2.8, 0.1, -0.2)), (Vector3::new(-0.5, 0.3, 0.2), Vector3::zeros()), (
1700 Vector3::new(1e-8, -2e-8, 3e-8),
1701 Vector3::new(1e-8, 2e-8, -1e-8),
1702 ), ];
1704 for (rho, theta) in cases {
1705 let xi = SE3Tangent::from_components(rho.x, rho.y, rho.z, theta.x, theta.y, theta.z);
1706 let analytic = xi.right_jacobian();
1707 let numeric = fd_right_jacobian(&xi);
1708 let err = (analytic - numeric).norm();
1709 assert!(
1710 err < FD_TOL,
1711 "right_jacobian mismatch at rho={rho:?} theta={theta:?}: error={err}"
1712 );
1713 }
1714 }
1715
1716 #[test]
1717 fn se3_right_jacobian_continuous_near_zero() {
1718 for scale in [1e-1, 1e-3, 1e-5, 1e-7, 1e-9] {
1719 let rho = Vector3::new(0.3, -0.2, 0.1) * scale;
1720 let theta = Vector3::new(0.2, 0.1, -0.3) * scale;
1721 let xi = SE3Tangent::from_components(rho.x, rho.y, rho.z, theta.x, theta.y, theta.z);
1722 let analytic = xi.right_jacobian();
1723 let numeric = fd_right_jacobian(&xi);
1724 let err = (analytic - numeric).norm();
1725 assert!(err < FD_TOL, "discontinuity at scale={scale}: error={err}");
1726 }
1727 }
1728}