1use crate::{
33 LieGroup, Tangent,
34 so3::{SO3, SO3Tangent},
35};
36use nalgebra::{Matrix3, Matrix4, SMatrix, SVector, UnitQuaternion, Vector3};
37
38type Vector7<T> = SVector<T, 7>;
40type Matrix7<T> = SMatrix<T, 7, 7>;
41use std::{
42 fmt,
43 fmt::{Display, Formatter},
44};
45
46#[derive(Clone, PartialEq)]
50pub struct Sim3 {
51 params: SVector<f64, 8>,
53}
54
55impl Display for Sim3 {
56 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
57 let t = self.translation();
58 let s = self.scale();
59 let q = self.rotation_quaternion();
60 write!(
61 f,
62 "Sim3(translation: [{:.4}, {:.4}, {:.4}], scale: {:.4}, rotation: [w: {:.4}, x: {:.4}, y: {:.4}, z: {:.4}])",
63 t.x, t.y, t.z, s, q.w, q.i, q.j, q.k
64 )
65 }
66}
67
68fn regularized_inverse_3(m: &Matrix3<f64>, what: &str) -> Matrix3<f64> {
75 if let Some(inv) = m.try_inverse() {
76 return inv;
77 }
78 let eps = 1e-9 * (m.trace().abs() + 1.0) / 3.0;
79 tracing::warn!(
80 "Sim(3) {what} is numerically singular; using Tikhonov-regularized inverse (eps = {eps:.3e})"
81 );
82 (*m + Matrix3::identity() * eps)
83 .try_inverse()
84 .unwrap_or_else(|| {
85 tracing::error!("Sim(3) {what}: regularized inverse also failed; using identity");
86 Matrix3::identity()
87 })
88}
89
90fn regularized_inverse_7(m: &Matrix7<f64>, what: &str) -> Matrix7<f64> {
91 if let Some(inv) = m.try_inverse() {
92 return inv;
93 }
94 let eps = 1e-9 * (m.trace().abs() + 1.0) / 7.0;
95 tracing::warn!(
96 "Sim(3) {what} is numerically singular; using Tikhonov-regularized inverse (eps = {eps:.3e})"
97 );
98 (*m + Matrix7::identity() * eps)
99 .try_inverse()
100 .unwrap_or_else(|| {
101 tracing::error!("Sim(3) {what}: regularized inverse also failed; using identity");
102 Matrix7::identity()
103 })
104}
105
106impl Sim3 {
107 pub const DIM: usize = 3;
109
110 pub const DOF: usize = 7;
112
113 pub const REP_SIZE: usize = 8;
115
116 #[inline]
117 fn translation_impl(&self) -> Vector3<f64> {
118 Vector3::new(self.params[0], self.params[1], self.params[2])
119 }
120
121 #[inline]
122 fn rotation_impl(&self) -> SO3 {
123 SO3::from_quaternion_wxyz(
124 self.params[3],
125 self.params[4],
126 self.params[5],
127 self.params[6],
128 )
129 }
130
131 #[inline]
132 fn scale_impl(&self) -> f64 {
133 self.params[7]
134 }
135
136 #[inline]
137 fn from_parts(t: Vector3<f64>, r: &SO3, scale: f64) -> Self {
138 let q = r.params();
139 Sim3 {
140 params: SVector::<f64, 8>::from([t.x, t.y, t.z, q[0], q[1], q[2], q[3], scale]),
141 }
142 }
143
144 pub fn identity() -> Self {
146 Sim3 {
147 params: SVector::<f64, 8>::from([0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0]),
148 }
149 }
150
151 pub fn jacobian_identity() -> Matrix7<f64> {
153 Matrix7::<f64>::identity()
154 }
155
156 pub fn new(translation: Vector3<f64>, rotation: UnitQuaternion<f64>, scale: f64) -> Self {
163 assert!(scale > 0.0, "Scale must be positive");
164 Sim3::from_parts(translation, &SO3::new(rotation), scale)
165 }
166
167 pub fn from_components(translation: Vector3<f64>, rotation: SO3, scale: f64) -> Self {
169 assert!(scale > 0.0, "Scale must be positive");
170 Sim3::from_parts(translation, &rotation, scale)
171 }
172
173 pub fn translation(&self) -> Vector3<f64> {
175 self.translation_impl()
176 }
177
178 pub fn scale(&self) -> f64 {
180 self.scale_impl()
181 }
182
183 pub fn rotation_so3(&self) -> SO3 {
185 self.rotation_impl()
186 }
187
188 pub fn rotation_quaternion(&self) -> UnitQuaternion<f64> {
190 self.rotation_impl().quaternion()
191 }
192
193 pub fn rotation_matrix(&self) -> Matrix3<f64> {
195 self.rotation_impl().rotation_matrix()
196 }
197
198 pub fn matrix(&self) -> Matrix4<f64> {
200 let mut mat = Matrix4::identity();
201 let rot_mat = self.rotation_matrix();
202 let scale = self.scale_impl();
203
204 for i in 0..3 {
205 for j in 0..3 {
206 mat[(i, j)] = scale * rot_mat[(i, j)];
207 }
208 }
209
210 mat[(0, 3)] = self.params[0];
211 mat[(1, 3)] = self.params[1];
212 mat[(2, 3)] = self.params[2];
213
214 mat
215 }
216
217 pub fn x(&self) -> f64 {
219 self.params[0]
220 }
221
222 pub fn y(&self) -> f64 {
224 self.params[1]
225 }
226
227 pub fn z(&self) -> f64 {
229 self.params[2]
230 }
231
232 pub fn coeffs(&self) -> [f64; 8] {
234 [
235 self.params[0],
236 self.params[1],
237 self.params[2],
238 self.params[3],
239 self.params[4],
240 self.params[5],
241 self.params[6],
242 self.params[7],
243 ]
244 }
245}
246
247impl LieGroup for Sim3 {
248 const NAME: &'static str = "Sim3";
249
250 type TangentVector = Sim3Tangent;
251 type JacobianMatrix = Matrix7<f64>;
252 type LieAlgebra = Matrix4<f64>;
253
254 fn inverse(&self, jacobian: Option<&mut Self::JacobianMatrix>) -> Self {
258 let rot = self.rotation_impl();
259 let rot_inv = rot.inverse(None);
260 let scale_inv = 1.0 / self.scale_impl();
261 let trans_inv = -rot_inv.act(&self.translation_impl(), None, None) * scale_inv;
262
263 if let Some(jac) = jacobian {
264 *jac = -self.adjoint();
265 }
266
267 Sim3::from_parts(trans_inv, &rot_inv, scale_inv)
268 }
269
270 fn compose(
274 &self,
275 other: &Self,
276 jacobian_self: Option<&mut Self::JacobianMatrix>,
277 jacobian_other: Option<&mut Self::JacobianMatrix>,
278 ) -> Self {
279 let rot = self.rotation_impl();
280 let scale = self.scale_impl();
281 let composed_rotation = rot.compose(&other.rotation_impl(), None, None);
282 let composed_translation =
283 scale * rot.act(&other.translation_impl(), None, None) + self.translation_impl();
284 let composed_scale = scale * other.scale_impl();
285
286 let result = Sim3::from_parts(composed_translation, &composed_rotation, composed_scale);
287
288 if let Some(jac_self) = jacobian_self {
289 *jac_self = other.inverse(None).adjoint();
290 }
291
292 if let Some(jac_other) = jacobian_other {
293 *jac_other = Matrix7::identity();
294 }
295
296 result
297 }
298
299 fn log(&self, jacobian: Option<&mut Self::JacobianMatrix>) -> Self::TangentVector {
301 let theta = self.rotation_impl().log(None);
302 let sigma = self.scale_impl().ln();
303 let mut data = Vector7::zeros();
304
305 let theta_tangent = SO3Tangent::new(theta.coeffs());
308 let v_inv = Self::compute_v_inv(&theta_tangent, sigma);
309 let translation_vector = v_inv * self.translation_impl();
310
311 data.fixed_rows_mut::<3>(0).copy_from(&translation_vector);
312 data.fixed_rows_mut::<3>(3).copy_from(&theta.coeffs());
313 data[6] = sigma;
314
315 let result = Sim3Tangent { data };
316
317 if let Some(jac) = jacobian {
318 *jac = result.right_jacobian_inv();
319 }
320
321 result
322 }
323
324 fn act(
325 &self,
326 vector: &Vector3<f64>,
327 jacobian_self: Option<&mut Self::JacobianMatrix>,
328 jacobian_vector: Option<&mut Matrix3<f64>>,
329 ) -> Vector3<f64> {
330 let rot = self.rotation_impl();
331 let scale = self.scale_impl();
332 let rotation_matrix = rot.rotation_matrix();
333 let result = scale * rot.act(vector, None, None) + self.translation_impl();
334
335 if let Some(jac_self) = jacobian_self {
336 let rotated_vector = rot.act(vector, None, None);
337
338 jac_self
342 .fixed_view_mut::<3, 3>(0, 0)
343 .copy_from(&(scale * rotation_matrix));
344
345 jac_self
346 .fixed_view_mut::<3, 3>(0, 3)
347 .copy_from(&(-scale * rotation_matrix * SO3Tangent::new(*vector).hat()));
348
349 jac_self
352 .fixed_view_mut::<3, 1>(0, 6)
353 .copy_from(&(scale * rotated_vector));
354 }
355
356 if let Some(jac_vector) = jacobian_vector {
357 *jac_vector = scale * rotation_matrix;
358 }
359
360 result
361 }
362
363 fn adjoint(&self) -> Self::JacobianMatrix {
364 let rotation_matrix = self.rotation_impl().rotation_matrix();
365 let translation = self.translation_impl();
366 let scale = self.scale_impl();
367 let mut adjoint_matrix = Matrix7::zeros();
368
369 adjoint_matrix
376 .fixed_view_mut::<3, 3>(0, 0)
377 .copy_from(&(scale * rotation_matrix));
378
379 let top_middle = SO3Tangent::new(translation).hat() * rotation_matrix;
381 adjoint_matrix
382 .fixed_view_mut::<3, 3>(0, 3)
383 .copy_from(&top_middle);
384
385 adjoint_matrix
387 .fixed_view_mut::<3, 1>(0, 6)
388 .copy_from(&(-translation));
389
390 adjoint_matrix
392 .fixed_view_mut::<3, 3>(3, 3)
393 .copy_from(&rotation_matrix);
394
395 adjoint_matrix[(6, 6)] = 1.0;
397
398 adjoint_matrix
399 }
400
401 fn random() -> Self {
402 use rand::Rng;
403 let mut rng = rand::rng();
404
405 let translation = Vector3::new(
406 rng.random_range(-1.0..1.0),
407 rng.random_range(-1.0..1.0),
408 rng.random_range(-1.0..1.0),
409 );
410
411 let rotation = SO3::random();
412 let scale = rng.random_range(0.5..2.0);
413
414 Sim3::from_parts(translation, &rotation, scale)
415 }
416
417 fn normalize(&mut self) {
418 let mut rot = self.rotation_impl();
419 rot.normalize();
420 let q = rot.params();
421 self.params[3] = q[0];
422 self.params[4] = q[1];
423 self.params[5] = q[2];
424 self.params[6] = q[3];
425 if self.params[7] <= 0.0 {
426 self.params[7] = 1.0;
427 }
428 }
429
430 fn is_valid(&self, tolerance: f64) -> bool {
431 self.translation_impl().iter().all(|t| t.is_finite())
436 && self.rotation_impl().is_valid(tolerance)
437 && self.scale_impl().is_finite()
438 && self.scale_impl() > 0.0
439 }
440
441 fn as_param_slice(&self) -> &[f64] {
442 self.params.as_slice()
443 }
444
445 fn as_param_slice_mut(&mut self) -> &mut [f64] {
446 self.params.as_mut_slice()
447 }
448
449 fn from_param_slice(s: &[f64]) -> Self {
450 debug_assert_eq!(s.len(), 8);
451 Sim3 {
452 params: SVector::from_column_slice(s),
453 }
454 }
455
456 fn vee(&self) -> Self::TangentVector {
457 self.log(None)
458 }
459
460 fn is_approx(&self, other: &Self, tolerance: f64) -> bool {
461 let difference = self.right_minus(other, None, None);
462 difference.is_zero(tolerance)
463 }
464
465 fn jacobian_identity() -> Self::JacobianMatrix {
466 Matrix7::<f64>::identity()
467 }
468
469 fn zero_jacobian() -> Self::JacobianMatrix {
470 Matrix7::<f64>::zeros()
471 }
472}
473
474impl Sim3 {
475 fn compute_v_inv(theta: &SO3Tangent, sigma: f64) -> Matrix3<f64> {
479 let v = Sim3Tangent::v_matrix(theta, sigma);
480 regularized_inverse_3(&v, "V⁻¹")
481 }
482}
483
484#[derive(Clone, PartialEq)]
491pub struct Sim3Tangent {
492 data: Vector7<f64>,
494}
495
496impl fmt::Display for Sim3Tangent {
497 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
498 let rho = self.rho();
499 let theta = self.theta();
500 let sigma = self.sigma();
501 write!(
502 f,
503 "sim3(rho: [{:.4}, {:.4}, {:.4}], theta: [{:.4}, {:.4}, {:.4}], sigma: {:.4})",
504 rho.x, rho.y, rho.z, theta.x, theta.y, theta.z, sigma
505 )
506 }
507}
508
509impl Sim3Tangent {
510 pub fn new(rho: Vector3<f64>, theta: Vector3<f64>, sigma: f64) -> Self {
512 let mut data = Vector7::zeros();
513 data.fixed_rows_mut::<3>(0).copy_from(&rho);
514 data.fixed_rows_mut::<3>(3).copy_from(&theta);
515 data[6] = sigma;
516 Sim3Tangent { data }
517 }
518
519 pub fn rho(&self) -> Vector3<f64> {
521 self.data.fixed_rows::<3>(0).into_owned()
522 }
523
524 pub fn theta(&self) -> Vector3<f64> {
526 self.data.fixed_rows::<3>(3).into_owned()
527 }
528
529 pub fn sigma(&self) -> f64 {
531 self.data[6]
532 }
533
534 pub fn from_components(
536 rho_x: f64,
537 rho_y: f64,
538 rho_z: f64,
539 theta_x: f64,
540 theta_y: f64,
541 theta_z: f64,
542 sigma: f64,
543 ) -> Self {
544 Sim3Tangent {
545 data: Vector7::from_column_slice(&[
546 rho_x, rho_y, rho_z, theta_x, theta_y, theta_z, sigma,
547 ]),
548 }
549 }
550
551 fn v_matrix(theta: &SO3Tangent, sigma: f64) -> Matrix3<f64> {
557 let theta_norm_sq = theta.coeffs().norm_squared();
558
559 if theta_norm_sq < crate::SMALL_ANGLE_THRESHOLD
560 && sigma.abs() < crate::SMALL_ANGLE_THRESHOLD
561 {
562 return Matrix3::identity();
563 }
564
565 let theta_hat = theta.hat();
566
567 if theta_norm_sq < crate::SMALL_ANGLE_THRESHOLD {
568 let a = (sigma.exp() - 1.0) / sigma;
570 return a * Matrix3::identity();
571 }
572
573 let theta_norm = theta_norm_sq.sqrt();
574 let sin_theta = theta_norm.sin();
575 let cos_theta = theta_norm.cos();
576
577 if sigma.abs() < crate::SMALL_ANGLE_THRESHOLD {
578 let a = 1.0;
580 let b = (1.0 - cos_theta) / theta_norm_sq;
581 let c = (theta_norm - sin_theta) / (theta_norm * theta_norm_sq);
582 return a * Matrix3::identity() + b * theta_hat + c * theta_hat * theta_hat;
583 }
584
585 let e_sigma = sigma.exp();
587 let alpha_sq = sigma * sigma + theta_norm_sq;
588
589 let a = (e_sigma - 1.0) / sigma;
590 let b = (e_sigma * (sigma * sin_theta - theta_norm * cos_theta) + theta_norm)
591 / (theta_norm * alpha_sq);
592 let cos_integral =
593 (e_sigma * (sigma * cos_theta + theta_norm * sin_theta) - sigma) / alpha_sq;
594 let c = (a - cos_integral) / theta_norm_sq;
595
596 a * Matrix3::identity() + b * theta_hat + c * theta_hat * theta_hat
597 }
598}
599
600fn integrate_adjoint_of_negated_exp(xi: &Sim3Tangent) -> Matrix7<f64> {
603 const INTERVALS: usize = 256;
609 let h = 1.0 / INTERVALS as f64;
610
611 let adjoint_at = |t: f64| -> Matrix7<f64> {
612 let scaled = Sim3Tangent {
613 data: xi.data * (-t),
614 };
615 scaled.exp(None).adjoint()
616 };
617
618 let mut sum = adjoint_at(0.0) + adjoint_at(1.0);
619 for i in 1..INTERVALS {
620 let t = i as f64 * h;
621 let weight = if i % 2 == 1 { 4.0 } else { 2.0 };
622 sum += adjoint_at(t) * weight;
623 }
624
625 sum * (h / 3.0)
626}
627
628impl Tangent<Sim3> for Sim3Tangent {
629 const DIM: usize = 7;
630
631 fn exp(&self, jacobian: Option<&mut <Sim3 as LieGroup>::JacobianMatrix>) -> Sim3 {
633 let rho = self.rho();
634 let theta = self.theta();
635 let sigma = self.sigma();
636
637 let theta_tangent = SO3Tangent::new(theta);
638 let rotation = theta_tangent.exp(None);
639 let v_matrix = Self::v_matrix(&theta_tangent, sigma);
640 let translation = v_matrix * rho;
641 let scale = sigma.exp();
642
643 if let Some(jac) = jacobian {
644 *jac = self.right_jacobian();
645 }
646
647 Sim3::from_components(translation, rotation, scale)
648 }
649
650 fn right_jacobian(&self) -> <Sim3 as LieGroup>::JacobianMatrix {
660 integrate_adjoint_of_negated_exp(self)
661 }
662
663 fn left_jacobian(&self) -> <Sim3 as LieGroup>::JacobianMatrix {
665 let negated = Sim3Tangent { data: -self.data };
666 negated.right_jacobian()
667 }
668
669 fn right_jacobian_inv(&self) -> <Sim3 as LieGroup>::JacobianMatrix {
671 regularized_inverse_7(&self.right_jacobian(), "right-Jacobian⁻¹")
672 }
673
674 fn left_jacobian_inv(&self) -> <Sim3 as LieGroup>::JacobianMatrix {
676 regularized_inverse_7(&self.left_jacobian(), "left-Jacobian⁻¹")
677 }
678
679 fn hat(&self) -> <Sim3 as LieGroup>::LieAlgebra {
681 let mut lie_alg = Matrix4::zeros();
682
683 let theta_hat = SO3Tangent::new(self.theta()).hat();
684
685 for i in 0..3 {
687 for j in 0..3 {
688 lie_alg[(i, j)] = theta_hat[(i, j)];
689 }
690 lie_alg[(i, i)] += self.sigma();
691 }
692
693 let rho = self.rho();
695 lie_alg[(0, 3)] = rho[0];
696 lie_alg[(1, 3)] = rho[1];
697 lie_alg[(2, 3)] = rho[2];
698
699 lie_alg
700 }
701
702 fn zero() -> <Sim3 as LieGroup>::TangentVector {
703 Sim3Tangent::new(Vector3::zeros(), Vector3::zeros(), 0.0)
704 }
705
706 fn random() -> <Sim3 as LieGroup>::TangentVector {
707 use rand::Rng;
708 let mut rng = rand::rng();
709 Sim3Tangent::new(
710 Vector3::new(
711 rng.random_range(-1.0..1.0),
712 rng.random_range(-1.0..1.0),
713 rng.random_range(-1.0..1.0),
714 ),
715 Vector3::new(
716 rng.random_range(-0.1..0.1),
717 rng.random_range(-0.1..0.1),
718 rng.random_range(-0.1..0.1),
719 ),
720 rng.random_range(-0.5..0.5),
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 theta_norm = self.theta().norm();
730 if theta_norm > f64::EPSILON {
731 self.data[3] /= theta_norm;
732 self.data[4] /= theta_norm;
733 self.data[5] /= theta_norm;
734 }
735 }
736
737 fn normalized(&self) -> <Sim3 as LieGroup>::TangentVector {
738 let norm = self.theta().norm();
739 if norm > f64::EPSILON {
740 Sim3Tangent::new(self.rho(), self.theta() / norm, self.sigma())
741 } else {
742 Sim3Tangent::new(self.rho(), Vector3::zeros(), self.sigma())
743 }
744 }
745
746 fn as_slice(&self) -> &[f64] {
747 self.data.as_slice()
748 }
749
750 fn from_slice(s: &[f64]) -> Self {
751 debug_assert_eq!(s.len(), 7);
752 Sim3Tangent {
753 data: Vector7::from_column_slice(s),
754 }
755 }
756
757 fn small_adj(&self) -> <Sim3 as LieGroup>::JacobianMatrix {
758 let mut small_adj = Matrix7::zeros();
759 let rho_skew = SO3Tangent::new(self.rho()).hat();
760 let theta_skew = SO3Tangent::new(self.theta()).hat();
761 let sigma = self.sigma();
762
763 for i in 0..3 {
769 for j in 0..3 {
770 small_adj[(i, j)] = theta_skew[(i, j)];
771 }
772 small_adj[(i, i)] += sigma;
773 }
774
775 small_adj.fixed_view_mut::<3, 3>(0, 3).copy_from(&rho_skew);
776 small_adj
777 .fixed_view_mut::<3, 1>(0, 6)
778 .copy_from(&(-self.rho()));
779 small_adj
780 .fixed_view_mut::<3, 3>(3, 3)
781 .copy_from(&theta_skew);
782
783 small_adj
784 }
785
786 fn lie_bracket(&self, other: &Self) -> <Sim3 as LieGroup>::TangentVector {
787 let bracket_result = self.small_adj() * other.data;
788 Sim3Tangent {
789 data: bracket_result,
790 }
791 }
792
793 fn is_approx(&self, other: &Self, tolerance: f64) -> bool {
794 (self.data - other.data).norm() < tolerance
795 }
796
797 fn generator(&self, i: usize) -> <Sim3 as LieGroup>::LieAlgebra {
798 assert!(i < 7, "Sim(3) only has generators for indices 0-6");
799
800 let mut generator = Matrix4::zeros();
801
802 match i {
803 0..=2 => {
804 generator[(i, 3)] = 1.0;
806 }
807 3..=5 => {
808 let idx = i - 3;
810 match idx {
811 0 => {
812 generator[(1, 2)] = -1.0;
813 generator[(2, 1)] = 1.0;
814 }
815 1 => {
816 generator[(0, 2)] = 1.0;
817 generator[(2, 0)] = -1.0;
818 }
819 2 => {
820 generator[(0, 1)] = -1.0;
821 generator[(1, 0)] = 1.0;
822 }
823 _ => unreachable!(),
824 }
825 }
826 6 => {
827 generator[(0, 0)] = 1.0;
829 generator[(1, 1)] = 1.0;
830 generator[(2, 2)] = 1.0;
831 }
832 _ => unreachable!(),
833 }
834
835 generator
836 }
837}
838
839#[cfg(test)]
840mod tests {
841 use super::*;
842
843 const TOLERANCE: f64 = 1e-9;
844
845 #[test]
846 fn test_sim3_identity() {
847 let identity = Sim3::identity();
848 assert!(identity.is_valid(TOLERANCE));
849 assert!(identity.translation().norm() < TOLERANCE);
850 assert!((identity.scale() - 1.0).abs() < TOLERANCE);
851 assert!(identity.rotation_quaternion().angle() < TOLERANCE);
852 }
853
854 #[test]
860 fn test_sim3_is_valid_rejects_nonfinite_at_every_index() {
861 let base = Sim3::identity();
862 for idx in 0..Sim3::REP_SIZE {
863 for bad in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
864 let mut params: Vec<f64> = base.as_param_slice().to_vec();
865 params[idx] = bad;
866 let perturbed = Sim3::from_param_slice(¶ms);
867 assert!(
868 !perturbed.is_valid(TOLERANCE),
869 "index {idx} = {bad} must be rejected"
870 );
871 }
872 }
873 assert!(
874 !base.is_valid(f64::INFINITY),
875 "infinite tolerance must be rejected"
876 );
877 assert!(!base.is_valid(f64::NAN), "NaN tolerance must be rejected");
878 }
879
880 #[test]
881 fn test_sim3_new() {
882 let translation = Vector3::new(1.0, 2.0, 3.0);
883 let rotation = UnitQuaternion::from_euler_angles(0.1, 0.2, 0.3);
884 let scale = 1.5;
885
886 let sim3 = Sim3::new(translation, rotation, scale);
887 assert!(sim3.is_valid(TOLERANCE));
888 assert!((sim3.translation() - translation).norm() < TOLERANCE);
889 assert!((sim3.scale() - scale).abs() < TOLERANCE);
890 }
891
892 #[test]
893 #[should_panic(expected = "Scale must be positive")]
894 fn test_sim3_new_negative_scale() {
895 let translation = Vector3::new(1.0, 2.0, 3.0);
896 let rotation = UnitQuaternion::identity();
897 let _sim3 = Sim3::new(translation, rotation, -1.0);
898 }
899
900 #[test]
901 fn test_sim3_random() {
902 let sim3 = Sim3::random();
903 assert!(sim3.is_valid(TOLERANCE));
904 assert!(sim3.scale() > 0.0);
905 }
906
907 #[test]
908 fn test_sim3_inverse() {
909 let sim3 = Sim3::random();
910 let sim3_inv = sim3.inverse(None);
911
912 let composed = sim3.compose(&sim3_inv, None, None);
913 let identity = Sim3::identity();
914
915 assert!(composed.is_approx(&identity, TOLERANCE));
916 }
917
918 #[test]
919 fn test_sim3_compose() {
920 let sim3_1 = Sim3::random();
921 let sim3_2 = Sim3::random();
922
923 let composed = sim3_1.compose(&sim3_2, None, None);
924 assert!(composed.is_valid(TOLERANCE));
925
926 let identity = Sim3::identity();
927 let composed_with_identity = sim3_1.compose(&identity, None, None);
928 assert!(composed_with_identity.is_approx(&sim3_1, TOLERANCE));
929 }
930
931 #[test]
932 fn test_sim3_exp_log() {
933 let tangent = Sim3Tangent::new(
934 Vector3::new(0.1, 0.2, 0.3),
935 Vector3::new(0.01, 0.02, 0.03),
936 0.1,
937 );
938
939 let sim3 = tangent.exp(None);
940 let recovered_tangent = sim3.log(None);
941
942 assert!((tangent.data - recovered_tangent.data).norm() < TOLERANCE);
943 }
944
945 #[test]
946 fn test_sim3_exp_zero() {
947 let zero_tangent = Sim3Tangent::zero();
948 let sim3 = zero_tangent.exp(None);
949 let identity = Sim3::identity();
950
951 assert!(sim3.is_approx(&identity, TOLERANCE));
952 }
953
954 #[test]
955 fn test_sim3_log_identity() {
956 let identity = Sim3::identity();
957 let tangent = identity.log(None);
958
959 assert!(tangent.data.norm() < TOLERANCE);
960 }
961
962 #[test]
963 fn test_sim3_adjoint() {
964 let sim3 = Sim3::random();
965 let adj = sim3.adjoint();
966
967 assert_eq!(adj.nrows(), 7);
968 assert_eq!(adj.ncols(), 7);
969 }
970
971 #[test]
972 fn test_sim3_act() {
973 let sim3 = Sim3::random();
974 let point = Vector3::new(1.0, 2.0, 3.0);
975
976 let _transformed_point = sim3.act(&point, None, None);
977
978 let identity = Sim3::identity();
979 let identity_transformed = identity.act(&point, None, None);
980
981 assert!((identity_transformed - point).norm() < TOLERANCE);
982 }
983
984 #[test]
985 fn test_sim3_between() {
986 let sim3_a = Sim3::random();
987 let sim3_b = sim3_a.clone();
988 let sim3_between_identity = sim3_a.between(&sim3_b, None, None);
989 assert!(sim3_between_identity.is_approx(&Sim3::identity(), TOLERANCE));
990
991 let sim3_c = Sim3::random();
992 let sim3_between = sim3_a.between(&sim3_c, None, None);
993 let expected = sim3_a.inverse(None).compose(&sim3_c, None, None);
994 assert!(sim3_between.is_approx(&expected, TOLERANCE));
995 }
996
997 #[test]
998 fn test_sim3_tangent_zero() {
999 let zero = Sim3Tangent::zero();
1000 assert!(zero.data.norm() < TOLERANCE);
1001
1002 let tangent = Sim3Tangent::new(Vector3::zeros(), Vector3::zeros(), 0.0);
1003 assert!(tangent.is_zero(TOLERANCE));
1004 }
1005
1006 #[test]
1007 fn test_sim3_manifold_properties() {
1008 assert_eq!(Sim3::DIM, 3);
1009 assert_eq!(Sim3::DOF, 7);
1010 assert_eq!(Sim3::REP_SIZE, 8);
1011 }
1012
1013 #[test]
1014 fn test_sim3_consistency() {
1015 let sim3_1 = Sim3::random();
1016 let sim3_2 = Sim3::random();
1017 let sim3_3 = Sim3::random();
1018
1019 let left_assoc = sim3_1
1021 .compose(&sim3_2, None, None)
1022 .compose(&sim3_3, None, None);
1023 let right_assoc = sim3_1.compose(&sim3_2.compose(&sim3_3, None, None), None, None);
1024
1025 assert!(left_assoc.is_approx(&right_assoc, 1e-10));
1026 }
1027
1028 #[test]
1029 fn test_sim3_scale_composition() {
1030 let sim3_1 = Sim3::new(Vector3::zeros(), UnitQuaternion::identity(), 2.0);
1031 let sim3_2 = Sim3::new(Vector3::zeros(), UnitQuaternion::identity(), 3.0);
1032
1033 let composed = sim3_1.compose(&sim3_2, None, None);
1034 assert!((composed.scale() - 6.0).abs() < TOLERANCE);
1035 }
1036
1037 #[test]
1038 fn test_sim3_tangent_small_adj() {
1039 let tangent = Sim3Tangent::new(
1040 Vector3::new(0.1, 0.2, 0.3),
1041 Vector3::new(0.4, 0.5, 0.6),
1042 0.1,
1043 );
1044 let small_adj = tangent.small_adj();
1045
1046 assert_eq!(small_adj.nrows(), 7);
1047 assert_eq!(small_adj.ncols(), 7);
1048 }
1049
1050 #[test]
1051 fn test_sim3_tangent_lie_bracket() {
1052 let tangent_a = Sim3Tangent::new(
1053 Vector3::new(0.1, 0.0, 0.0),
1054 Vector3::new(0.0, 0.2, 0.0),
1055 0.1,
1056 );
1057 let tangent_b = Sim3Tangent::new(
1058 Vector3::new(0.0, 0.3, 0.0),
1059 Vector3::new(0.0, 0.0, 0.4),
1060 0.2,
1061 );
1062
1063 let bracket_ab = tangent_a.lie_bracket(&tangent_b);
1064 let bracket_ba = tangent_b.lie_bracket(&tangent_a);
1065
1066 assert!((bracket_ab.data + bracket_ba.data).norm() < 1e-10);
1068
1069 let bracket_aa = tangent_a.lie_bracket(&tangent_a);
1071 assert!(bracket_aa.is_zero(1e-10));
1072 }
1073
1074 #[test]
1075 fn test_sim3_tangent_is_approx() {
1076 let tangent_1 = Sim3Tangent::new(
1077 Vector3::new(0.1, 0.2, 0.3),
1078 Vector3::new(0.4, 0.5, 0.6),
1079 0.1,
1080 );
1081 let tangent_2 = Sim3Tangent::new(
1082 Vector3::new(0.1 + 1e-12, 0.2, 0.3),
1083 Vector3::new(0.4, 0.5, 0.6),
1084 0.1,
1085 );
1086 let tangent_3 = Sim3Tangent::new(
1087 Vector3::new(1.0, 2.0, 3.0),
1088 Vector3::new(4.0, 5.0, 6.0),
1089 1.0,
1090 );
1091
1092 assert!(tangent_1.is_approx(&tangent_1, 1e-10));
1093 assert!(tangent_1.is_approx(&tangent_2, 1e-10));
1094 assert!(!tangent_1.is_approx(&tangent_3, 1e-10));
1095 }
1096
1097 #[test]
1098 fn test_sim3_generators() {
1099 let tangent = Sim3Tangent::new(
1100 Vector3::new(1.0, 1.0, 1.0),
1101 Vector3::new(1.0, 1.0, 1.0),
1102 1.0,
1103 );
1104
1105 for i in 0..7 {
1106 let generator = tangent.generator(i);
1107 assert_eq!(generator.nrows(), 4);
1108 assert_eq!(generator.ncols(), 4);
1109
1110 assert_eq!(generator[(3, 0)], 0.0);
1112 assert_eq!(generator[(3, 1)], 0.0);
1113 assert_eq!(generator[(3, 2)], 0.0);
1114 assert_eq!(generator[(3, 3)], 0.0);
1115 }
1116 }
1117
1118 #[test]
1119 #[should_panic]
1120 fn test_sim3_generator_invalid_index() {
1121 let tangent = Sim3Tangent::new(
1122 Vector3::new(1.0, 1.0, 1.0),
1123 Vector3::new(1.0, 1.0, 1.0),
1124 1.0,
1125 );
1126 let _generator = tangent.generator(7);
1127 }
1128
1129 #[test]
1130 fn test_sim3_vee() {
1131 let sim3 = Sim3::random();
1132 let tangent_log = sim3.log(None);
1133 let tangent_vee = sim3.vee();
1134
1135 assert!((tangent_log.data - tangent_vee.data).norm() < 1e-10);
1136 }
1137
1138 #[test]
1139 fn test_sim3_is_approx() {
1140 let sim3_1 = Sim3::random();
1141 let sim3_2 = sim3_1.clone();
1142
1143 assert!(sim3_1.is_approx(&sim3_1, 1e-10));
1144 assert!(sim3_1.is_approx(&sim3_2, 1e-10));
1145
1146 let small_tangent = Sim3Tangent::new(
1147 Vector3::new(1e-12, 1e-12, 1e-12),
1148 Vector3::new(1e-12, 1e-12, 1e-12),
1149 1e-12,
1150 );
1151 let sim3_perturbed = sim3_1.right_plus(&small_tangent, None, None);
1152 assert!(sim3_1.is_approx(&sim3_perturbed, 1e-10));
1153 }
1154
1155 #[test]
1156 fn test_sim3_small_angle_approximations() {
1157 let small_tangent = Sim3Tangent::new(
1158 Vector3::new(1e-8, 2e-8, 3e-8),
1159 Vector3::new(1e-9, 2e-9, 3e-9),
1160 1e-8,
1161 );
1162
1163 let sim3 = small_tangent.exp(None);
1164 let recovered = sim3.log(None);
1165
1166 assert!((small_tangent.data - recovered.data).norm() < TOLERANCE);
1167 }
1168
1169 #[test]
1170 fn test_sim3_accessors() {
1171 let translation = Vector3::new(1.0, 2.0, 3.0);
1172 let rotation = UnitQuaternion::identity();
1173 let scale = 1.5;
1174
1175 let sim3 = Sim3::new(translation, rotation, scale);
1176
1177 assert_eq!(sim3.x(), 1.0);
1178 assert_eq!(sim3.y(), 2.0);
1179 assert_eq!(sim3.z(), 3.0);
1180 assert_eq!(sim3.scale(), 1.5);
1181 }
1182
1183 #[test]
1184 fn test_sim3_matrix() {
1185 let translation = Vector3::new(1.0, 2.0, 3.0);
1186 let rotation = UnitQuaternion::identity();
1187 let scale = 2.0;
1188
1189 let sim3 = Sim3::new(translation, rotation, scale);
1190 let mat = sim3.matrix();
1191
1192 for i in 0..3 {
1194 for j in 0..3 {
1195 if i == j {
1196 assert!((mat[(i, j)] - 2.0).abs() < TOLERANCE);
1197 } else {
1198 assert!(mat[(i, j)].abs() < TOLERANCE);
1199 }
1200 }
1201 }
1202
1203 assert!((mat[(0, 3)] - 1.0).abs() < TOLERANCE);
1205 assert!((mat[(1, 3)] - 2.0).abs() < TOLERANCE);
1206 assert!((mat[(2, 3)] - 3.0).abs() < TOLERANCE);
1207
1208 assert!(mat[(3, 0)].abs() < TOLERANCE);
1210 assert!(mat[(3, 1)].abs() < TOLERANCE);
1211 assert!(mat[(3, 2)].abs() < TOLERANCE);
1212 assert!((mat[(3, 3)] - 1.0).abs() < TOLERANCE);
1213 }
1214
1215 #[test]
1216 fn test_sim3_scale_action() {
1217 let scale = 2.0;
1218 let sim3 = Sim3::new(Vector3::zeros(), UnitQuaternion::identity(), scale);
1219 let point = Vector3::new(1.0, 2.0, 3.0);
1220
1221 let transformed = sim3.act(&point, None, None);
1222
1223 assert!((transformed.x - 2.0).abs() < TOLERANCE);
1224 assert!((transformed.y - 4.0).abs() < TOLERANCE);
1225 assert!((transformed.z - 6.0).abs() < TOLERANCE);
1226 }
1227
1228 #[test]
1229 fn test_sim3_tangent_basic() {
1230 let rho = Vector3::new(0.1, 0.2, 0.3);
1231 let theta = Vector3::new(0.4, 0.5, 0.6);
1232 let sigma = 0.1;
1233 let tangent = Sim3Tangent::new(rho, theta, sigma);
1234 assert!((tangent.rho() - rho).norm() < TOLERANCE);
1235 assert!((tangent.theta() - theta).norm() < TOLERANCE);
1236 assert!((tangent.sigma() - sigma).abs() < TOLERANCE);
1237 }
1238
1239 #[test]
1240 fn test_sim3_tangent_from_components() {
1241 let t1 = Sim3Tangent::new(
1242 Vector3::new(1.0, 2.0, 3.0),
1243 Vector3::new(0.4, 0.5, 0.6),
1244 0.1,
1245 );
1246 let t2 = Sim3Tangent::from_components(1.0, 2.0, 3.0, 0.4, 0.5, 0.6, 0.1);
1247 assert!(t1.is_approx(&t2, TOLERANCE));
1248 }
1249
1250 #[test]
1251 fn test_sim3_normalize() {
1252 let mut sim3 = Sim3::random();
1253 sim3.normalize();
1254 assert!(sim3.is_valid(TOLERANCE));
1255 }
1256
1257 #[test]
1258 fn test_sim3_coeffs() {
1259 let translation = Vector3::new(1.0, 2.0, 3.0);
1260 let rotation = UnitQuaternion::identity();
1261 let scale = 1.5;
1262 let sim3 = Sim3::new(translation, rotation, scale);
1263 let c = sim3.coeffs();
1264 assert!((c[0] - 1.0).abs() < TOLERANCE);
1265 assert!((c[1] - 2.0).abs() < TOLERANCE);
1266 assert!((c[2] - 3.0).abs() < TOLERANCE);
1267 assert!((c[3] - 1.0).abs() < TOLERANCE); assert!((c[7] - 1.5).abs() < TOLERANCE); }
1270
1271 #[test]
1272 fn test_sim3_manif_like_operations() {
1273 let a = Sim3::random();
1274 let b = Sim3::random();
1275
1276 let a_to_b = a.between(&b, None, None);
1277 let recovered_b = a.compose(&a_to_b, None, None);
1278 assert!(recovered_b.is_approx(&b, TOLERANCE));
1279
1280 let tangent = Sim3Tangent::new(
1281 Vector3::new(0.01, 0.02, 0.03),
1282 Vector3::new(0.001, 0.002, 0.003),
1283 0.01,
1284 );
1285 let perturbed = a.plus(&tangent, None, None);
1286 let recovered_tangent = perturbed.minus(&a, None, None);
1287 assert!((tangent.data - recovered_tangent.data).norm() < 1e-6);
1288 }
1289
1290 #[test]
1291 fn test_sim3_right_jacobian_inverse_identity() {
1292 let tangent = Sim3Tangent::new(
1293 Vector3::new(0.1, 0.2, 0.3),
1294 Vector3::new(0.01, 0.02, 0.03),
1295 0.1,
1296 );
1297 let jr = tangent.right_jacobian();
1298 let jr_inv = tangent.right_jacobian_inv();
1299 let product = jr * jr_inv;
1300 let identity = Matrix7::<f64>::identity();
1301 assert!((product - identity).norm() < 0.01);
1302 }
1303
1304 #[test]
1305 fn test_sim3_left_jacobian_inverse_identity() {
1306 let tangent = Sim3Tangent::new(
1307 Vector3::new(0.1, 0.2, 0.3),
1308 Vector3::new(0.01, 0.02, 0.03),
1309 0.1,
1310 );
1311 let jl = tangent.left_jacobian();
1312 let jl_inv = tangent.left_jacobian_inv();
1313 let product = jl * jl_inv;
1314 let identity = Matrix7::<f64>::identity();
1315 assert!((product - identity).norm() < 0.01);
1316 }
1317
1318 #[test]
1319 fn test_sim3_jacobi_identity() {
1320 let a = Sim3Tangent::random();
1321 let b = Sim3Tangent::random();
1322 let c = Sim3Tangent::random();
1323
1324 let term1 = a.lie_bracket(&b.lie_bracket(&c));
1325 let term2 = b.lie_bracket(&c.lie_bracket(&a));
1326 let term3 = c.lie_bracket(&a.lie_bracket(&b));
1327
1328 assert!((term1.data + term2.data + term3.data).norm() < 1e-8);
1329 }
1330
1331 #[test]
1332 fn test_sim3_hat_matrix_structure() {
1333 let tangent = Sim3Tangent::new(
1334 Vector3::new(1.0, 2.0, 3.0),
1335 Vector3::new(0.1, 0.2, 0.3),
1336 0.5,
1337 );
1338 let hat = tangent.hat();
1339
1340 for j in 0..4 {
1341 assert_eq!(hat[(3, j)], 0.0);
1342 }
1343
1344 assert!((hat[(0, 3)] - 1.0).abs() < TOLERANCE);
1345 assert!((hat[(1, 3)] - 2.0).abs() < TOLERANCE);
1346 assert!((hat[(2, 3)] - 3.0).abs() < TOLERANCE);
1347
1348 assert!((hat[(0, 0)] - 0.5).abs() < 0.5); }
1351
1352 #[test]
1353 fn test_sim3_param_slice_round_trip() {
1354 let sim3 = Sim3::random();
1355 let recovered = Sim3::from_param_slice(sim3.as_param_slice());
1356 assert!(sim3.is_approx(&recovered, TOLERANCE));
1357 }
1358
1359 #[test]
1362 fn test_sim3_display() {
1363 let sim3 = Sim3::identity();
1364 let s = format!("{}", sim3);
1365 assert!(s.contains("Sim3"));
1366
1367 let tangent = Sim3Tangent::new(
1368 Vector3::new(1.0, 2.0, 3.0),
1369 Vector3::new(0.1, 0.2, 0.3),
1370 0.5,
1371 );
1372 let ts = format!("{}", tangent);
1373 assert!(ts.contains("sim3"));
1374 }
1375
1376 #[test]
1377 fn test_sim3_jacobian_identity_static() {
1378 let jac = Sim3::jacobian_identity();
1379 assert!((jac - Matrix7::identity()).norm() < TOLERANCE);
1380 }
1381
1382 #[test]
1383 fn test_sim3_rotation_so3() {
1384 let sim3 = Sim3::random();
1385 let so3 = sim3.rotation_so3();
1386 assert!(so3.is_valid(TOLERANCE));
1387 }
1388
1389 #[test]
1390 fn test_sim3_inverse_with_jacobian() {
1391 let sim3 = Sim3::random();
1392 let mut jac = Matrix7::zeros();
1393 let inv = sim3.inverse(Some(&mut jac));
1394 let expected_jac = -sim3.adjoint();
1395 assert!((jac - expected_jac).norm() < TOLERANCE);
1396 let composed = sim3.compose(&inv, None, None);
1397 assert!(composed.is_approx(&Sim3::identity(), TOLERANCE));
1398 }
1399
1400 #[test]
1401 fn test_sim3_compose_with_jacobians() {
1402 let a = Sim3::random();
1403 let b = Sim3::random();
1404 let mut jac_a = Matrix7::zeros();
1405 let mut jac_b = Matrix7::zeros();
1406 let _composed = a.compose(&b, Some(&mut jac_a), Some(&mut jac_b));
1407 assert!(jac_a.norm() > 0.0);
1408 assert!(jac_b.norm() > 0.0);
1409 }
1410
1411 #[test]
1412 fn test_sim3_act_with_jacobians() {
1413 let sim3 = Sim3::random();
1414 let point = Vector3::new(1.0, 2.0, 3.0);
1415 let mut jac_self = Matrix7::zeros();
1416 let mut jac_point = Matrix3::<f64>::zeros();
1417 let result = sim3.act(&point, Some(&mut jac_self), Some(&mut jac_point));
1418 assert!(result.norm() > 0.0);
1419 assert!(jac_self.norm() > 0.0);
1420 assert!(jac_point.norm() > 0.0);
1421 }
1422
1423 #[test]
1424 fn test_sim3_liegroup_jacobian_identity() {
1425 let jac = <Sim3 as LieGroup>::jacobian_identity();
1426 assert!((jac - Matrix7::identity()).norm() < TOLERANCE);
1427
1428 let zero = <Sim3 as LieGroup>::zero_jacobian();
1429 assert!(zero.norm() < TOLERANCE);
1430 }
1431
1432 #[test]
1433 fn test_sim3_tangent_slice_round_trip() {
1434 let tangent = Sim3Tangent::new(
1435 Vector3::new(1.0, 2.0, 3.0),
1436 Vector3::new(0.1, 0.2, 0.3),
1437 0.5,
1438 );
1439 let recovered = Sim3Tangent::from_slice(tangent.as_slice());
1440 assert!(tangent.is_approx(&recovered, TOLERANCE));
1441 }
1442
1443 #[test]
1444 fn test_sim3_exp_with_jacobian() {
1445 let tangent = Sim3Tangent::new(
1446 Vector3::new(0.1, 0.2, 0.3),
1447 Vector3::new(0.01, 0.02, 0.03),
1448 0.1,
1449 );
1450 let mut jac = Matrix7::zeros();
1451 let _result = tangent.exp(Some(&mut jac));
1452 assert!(jac.norm() > 0.0);
1453 }
1454
1455 #[test]
1456 fn test_sim3_exp_log_stress() {
1457 for _ in 0..100 {
1458 let tangent = Sim3Tangent::random();
1459 let sim3 = tangent.exp(None);
1460 let recovered = sim3.log(None);
1461 assert!(
1462 tangent.is_approx(&recovered, 1e-6),
1463 "exp/log round-trip failed: error = {}",
1464 (tangent.data - recovered.data).norm()
1465 );
1466 }
1467 }
1468
1469 #[test]
1470 fn test_sim3_right_plus_minus_round_trip() {
1471 let a = Sim3::random();
1472 let b = Sim3::random();
1473 let diff = a.right_minus(&b, None, None);
1474 let recovered = b.right_plus(&diff, None, None);
1475 assert!(a.is_approx(&recovered, 1e-6));
1476 }
1477
1478 #[test]
1479 fn test_sim3_left_plus_minus_round_trip() {
1480 let a = Sim3::random();
1481 let b = Sim3::random();
1482 let diff = a.left_minus(&b, None, None);
1483 let recovered = b.left_plus(&diff, None, None);
1484 assert!(a.is_approx(&recovered, 1e-6));
1485 }
1486
1487 #[test]
1488 fn test_sim3_compose_associativity() {
1489 let a = Sim3::random();
1490 let b = Sim3::random();
1491 let c = Sim3::random();
1492 let ab_c = a.compose(&b, None, None).compose(&c, None, None);
1493 let a_bc = a.compose(&b.compose(&c, None, None), None, None);
1494 assert!(ab_c.is_approx(&a_bc, 1e-6));
1495 }
1496
1497 #[test]
1498 fn test_sim3_inverse_twice() {
1499 let g = Sim3::random();
1500 let g_inv_inv = g.inverse(None).inverse(None);
1501 assert!(g.is_approx(&g_inv_inv, 1e-6));
1502 }
1503
1504 #[test]
1505 fn test_sim3_pure_scale() {
1506 let tangent = Sim3Tangent::new(Vector3::zeros(), Vector3::zeros(), 0.5);
1508 let sim3 = tangent.exp(None);
1509 let recovered = sim3.log(None);
1510 assert!(tangent.is_approx(&recovered, TOLERANCE));
1511 assert!((sim3.scale() - 0.5_f64.exp()).abs() < TOLERANCE);
1512 }
1513
1514 #[test]
1515 fn test_sim3_pure_rotation() {
1516 let tangent = Sim3Tangent::new(
1518 Vector3::new(0.1, 0.2, 0.3),
1519 Vector3::new(0.05, 0.1, 0.15),
1520 0.0,
1521 );
1522 let sim3 = tangent.exp(None);
1523 let recovered = sim3.log(None);
1524 assert!(tangent.is_approx(&recovered, 1e-6));
1525 assert!((sim3.scale() - 1.0).abs() < TOLERANCE);
1526 }
1527
1528 #[test]
1529 fn test_sim3_scale_compose() {
1530 let a = Sim3::from_components(Vector3::zeros(), SO3::identity(), 2.0);
1531 let b = Sim3::from_components(Vector3::zeros(), SO3::identity(), 3.0);
1532 let ab = a.compose(&b, None, None);
1533 assert!((ab.scale() - 6.0).abs() < TOLERANCE);
1534 }
1535
1536 #[test]
1537 fn test_sim3_tangent_normalize_zero_theta() {
1538 let tangent = Sim3Tangent::new(Vector3::new(1.0, 2.0, 3.0), Vector3::zeros(), 0.5);
1539 let normalized = tangent.normalized();
1540 assert!(normalized.theta().norm() < TOLERANCE);
1541 }
1542
1543 #[test]
1544 fn test_sim3_tangent_generator_all() {
1545 let tangent = Sim3Tangent::zero();
1546 for i in 0..7 {
1547 let g = tangent.generator(i);
1548 assert!(g.norm() > 0.0, "Generator {} should be non-zero", i);
1549 }
1550 }
1551
1552 #[test]
1553 fn test_sim3_v_matrix_sigma_zero_matches_so3_jl() {
1554 let theta = SO3Tangent::new(Vector3::new(0.3, 0.4, 0.5));
1556 let v = Sim3Tangent::v_matrix(&theta, 0.0);
1557 let jl = theta.left_jacobian();
1558 assert!(
1559 (v - jl).norm() < 1e-10,
1560 "V(θ,0) should equal Jl(θ), error = {}",
1561 (v - jl).norm()
1562 );
1563 }
1564
1565 #[test]
1566 fn test_sim3_v_matrix_theta_zero() {
1567 let theta = SO3Tangent::new(Vector3::zeros());
1569 let sigma = 0.5;
1570 let v = Sim3Tangent::v_matrix(&theta, sigma);
1571 let expected = (sigma.exp() - 1.0) / sigma * Matrix3::<f64>::identity();
1572 assert!(
1573 (v - expected).norm() < 1e-10,
1574 "V(0,σ) should equal (e^σ-1)/σ * I, error = {}",
1575 (v - expected).norm()
1576 );
1577 }
1578
1579 #[test]
1580 fn sim3_param_slice_round_trip() {
1581 let g = Sim3::random();
1582 let recovered = Sim3::from_param_slice(g.as_param_slice());
1583 assert!(g.is_approx(&recovered, 1e-14));
1584 }
1585
1586 #[test]
1587 fn sim3_tangent_slice_round_trip() {
1588 let t = Sim3Tangent::random();
1589 let recovered = Sim3Tangent::from_slice(t.as_slice());
1590 assert!(t.is_approx(&recovered, 1e-14));
1591 }
1592}
1593
1594#[cfg(test)]
1595mod regularized_inverse_tests {
1596 use super::*;
1597 use nalgebra::Matrix3;
1598
1599 #[test]
1600 fn regular_matrix_inverts_unchanged() {
1601 let m = Matrix3::new(2.0, 1.0, 0.0, 1.0, 3.0, 1.0, 0.0, 1.0, 2.0);
1602 let inv = regularized_inverse_3(&m, "test");
1603 let identity = m * inv;
1604 assert!((identity - Matrix3::identity()).norm() < 1e-12);
1605 }
1606
1607 #[test]
1608 fn singular_matrix_warns_and_regularizes() {
1609 let singular = Matrix3::from_columns(&[
1613 Vector3::new(1.0, 2.0, 3.0),
1614 Vector3::new(2.0, 4.0, 6.0),
1615 Vector3::zeros(),
1616 ]);
1617 let inv = regularized_inverse_3(&singular, "test");
1618 assert!(
1619 (inv - Matrix3::identity()).norm() > 1e-3,
1620 "regularized inverse must not silently fall back to identity"
1621 );
1622 }
1623}
1624
1625#[cfg(test)]
1633mod sim3_jacobian_fd_tests {
1634 use super::*;
1635
1636 const FD_STEP: f64 = 1e-4;
1643 const FD_TOL: f64 = 1e-5;
1644
1645 fn vee(m: &Matrix4<f64>) -> Vector7<f64> {
1649 let mut v = Vector7::zeros();
1650 v[0] = m[(0, 3)];
1651 v[1] = m[(1, 3)];
1652 v[2] = m[(2, 3)];
1653 v[3] = m[(2, 1)];
1654 v[4] = m[(0, 2)];
1655 v[5] = m[(1, 0)];
1656 v[6] = (m[(0, 0)] + m[(1, 1)] + m[(2, 2)]) / 3.0;
1657 v
1658 }
1659
1660 fn perturbed(xi: &Sim3Tangent, k: usize, eps: f64) -> Sim3Tangent {
1661 let mut data = xi.data;
1662 data[k] += eps;
1663 Sim3Tangent { data }
1664 }
1665
1666 fn fd_right_jacobian(xi: &Sim3Tangent) -> Matrix7<f64> {
1669 let x_inv_mat = xi.exp(None).inverse(None).matrix();
1670 let mut jac = Matrix7::zeros();
1671 for k in 0..7 {
1672 let plus = perturbed(xi, k, FD_STEP).exp(None).matrix();
1673 let minus = perturbed(xi, k, -FD_STEP).exp(None).matrix();
1674 let d_dxi_k = (plus - minus) / (2.0 * FD_STEP);
1675 jac.set_column(k, &vee(&(x_inv_mat * d_dxi_k)));
1676 }
1677 jac
1678 }
1679
1680 #[test]
1681 fn sim3_right_jacobian_matches_matrix_exp_central_difference() {
1682 let cases = [
1683 (
1684 Vector3::new(0.7, -0.2, 0.4),
1685 Vector3::new(0.4, -0.3, 0.25),
1686 0.3,
1687 ),
1688 (
1689 Vector3::new(0.1, 0.2, -0.3),
1690 Vector3::new(2.8, 0.1, -0.2),
1691 0.5,
1692 ), (Vector3::new(-0.5, 0.3, 0.2), Vector3::zeros(), 0.4), (
1695 Vector3::new(0.3, -0.1, 0.2),
1696 Vector3::new(0.2, 0.1, -0.1),
1697 0.0,
1698 ), (
1700 Vector3::new(1e-8, -2e-8, 3e-8),
1701 Vector3::new(1e-8, 2e-8, -1e-8),
1702 1e-8,
1703 ), ];
1705 for (rho, theta, sigma) in cases {
1706 let xi = Sim3Tangent::new(rho, theta, sigma);
1707 let analytic = xi.right_jacobian();
1708 let numeric = fd_right_jacobian(&xi);
1709 let err = (analytic - numeric).norm();
1710 assert!(
1711 err < FD_TOL,
1712 "right_jacobian mismatch at rho={rho:?} theta={theta:?} sigma={sigma}: error={err}"
1713 );
1714 }
1715 }
1716
1717 #[test]
1718 fn sim3_left_jacobian_matches_matrix_exp_central_difference() {
1719 let xi = Sim3Tangent::new(
1720 Vector3::new(0.6, -0.3, 0.2),
1721 Vector3::new(-0.4, 0.5, 0.1),
1722 0.35,
1723 );
1724 let x_inv_mat = xi.exp(None).inverse(None).matrix();
1725
1726 let mut numeric = Matrix7::zeros();
1727 for k in 0..7 {
1728 let plus = perturbed(&xi, k, FD_STEP).exp(None).matrix();
1729 let minus = perturbed(&xi, k, -FD_STEP).exp(None).matrix();
1730 let d_dxi_k = (plus - minus) / (2.0 * FD_STEP);
1731 numeric.set_column(k, &vee(&(d_dxi_k * x_inv_mat)));
1733 }
1734
1735 let analytic = xi.left_jacobian();
1736 let err = (analytic - numeric).norm();
1737 assert!(err < FD_TOL, "left_jacobian mismatch: error={err}");
1738 }
1739
1740 #[test]
1741 fn sim3_act_jacobian_matches_finite_difference_at_nonunit_scale() {
1742 let sim3 = Sim3::new(
1743 Vector3::new(0.3, -0.2, 0.5),
1744 UnitQuaternion::from_euler_angles(0.2, -0.4, 0.3),
1745 1.7,
1746 );
1747 let p = Vector3::new(0.9, -1.1, 0.4);
1748
1749 let mut jac_self = Matrix7::zeros();
1750 let mut jac_point = Matrix3::zeros();
1751 sim3.act(&p, Some(&mut jac_self), Some(&mut jac_point));
1752
1753 for k in 0..3 {
1754 let mut p_plus = p;
1755 p_plus[k] += FD_STEP;
1756 let mut p_minus = p;
1757 p_minus[k] -= FD_STEP;
1758 let numeric =
1759 (sim3.act(&p_plus, None, None) - sim3.act(&p_minus, None, None)) / (2.0 * FD_STEP);
1760 let analytic = jac_point.column(k).into_owned();
1761 let err = (analytic - numeric).norm();
1762 assert!(err < FD_TOL, "act jacobian_vector col {k} mismatch: {err}");
1763 }
1764
1765 for k in 0..7 {
1766 let mut e = Vector7::zeros();
1767 e[k] = FD_STEP;
1768 let plus = sim3.right_plus(&Sim3Tangent { data: e }, None, None);
1769 e[k] = -FD_STEP;
1770 let minus = sim3.right_plus(&Sim3Tangent { data: e }, None, None);
1771 let numeric = (plus.act(&p, None, None) - minus.act(&p, None, None)) / (2.0 * FD_STEP);
1772 let analytic = Vector3::new(jac_self[(0, k)], jac_self[(1, k)], jac_self[(2, k)]);
1773 let err = (analytic - numeric).norm();
1774 assert!(err < FD_TOL, "act jacobian_self col {k} mismatch: {err}");
1775 }
1776 }
1777
1778 #[test]
1779 fn sim3_adjoint_matches_conjugation_finite_difference() {
1780 let cases = [
1781 (
1782 Vector3::new(0.8, -0.4, 0.6),
1783 Vector3::new(0.3, -0.2, 0.5),
1784 1.6,
1785 ), (Vector3::new(1.2, 0.5, -0.7), Vector3::zeros(), 1.0), (Vector3::zeros(), Vector3::zeros(), 2.3), ];
1789 for (t, r, s) in cases {
1790 let x = Sim3::new(t, UnitQuaternion::from_euler_angles(r.x, r.y, r.z), s);
1791 let analytic = x.adjoint();
1792 let x_mat = x.matrix();
1793 let x_inv_mat = x.inverse(None).matrix();
1794
1795 let mut numeric = Matrix7::zeros();
1796 for k in 0..7 {
1797 let mut e_plus = Vector7::zeros();
1798 e_plus[k] = FD_STEP;
1799 let g_plus = (Sim3Tangent { data: e_plus }).exp(None).matrix();
1800 let mut e_minus = Vector7::zeros();
1801 e_minus[k] = -FD_STEP;
1802 let g_minus = (Sim3Tangent { data: e_minus }).exp(None).matrix();
1803
1804 let conj_plus = x_mat * g_plus * x_inv_mat;
1805 let conj_minus = x_mat * g_minus * x_inv_mat;
1806 let d = (conj_plus - conj_minus) / (2.0 * FD_STEP);
1807 numeric.set_column(k, &vee(&d));
1808 }
1809
1810 let err = (analytic - numeric).norm();
1811 assert!(
1812 err < FD_TOL,
1813 "adjoint mismatch at t={t:?} r={r:?} s={s}: error={err}"
1814 );
1815 }
1816 }
1817
1818 #[test]
1819 fn sim3_right_jacobian_continuous_near_zero_and_joint_limit() {
1820 for scale in [1e-1, 1e-3, 1e-5, 1e-7, 1e-9] {
1825 let xi = Sim3Tangent::new(
1826 Vector3::new(0.3, -0.2, 0.1) * scale,
1827 Vector3::new(0.2, 0.1, -0.3) * scale,
1828 0.15 * scale,
1829 );
1830 let analytic = xi.right_jacobian();
1831 let numeric = fd_right_jacobian(&xi);
1832 let err = (analytic - numeric).norm();
1833 assert!(err < FD_TOL, "discontinuity at scale={scale}: error={err}");
1834 }
1835 }
1836}