1use crate::{
33 LieGroup, Tangent,
34 so3::{SO3, SO3Tangent},
35};
36use nalgebra::{DVector, 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 rotation: SO3,
53 translation: Vector3<f64>,
55 scale: f64,
57}
58
59impl Display for Sim3 {
60 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
61 let t = self.translation();
62 let s = self.scale();
63 let q = self.rotation_quaternion();
64 write!(
65 f,
66 "Sim3(translation: [{:.4}, {:.4}, {:.4}], scale: {:.4}, rotation: [w: {:.4}, x: {:.4}, y: {:.4}, z: {:.4}])",
67 t.x, t.y, t.z, s, q.w, q.i, q.j, q.k
68 )
69 }
70}
71
72impl Sim3 {
73 pub const DIM: usize = 3;
75
76 pub const DOF: usize = 7;
78
79 pub const REP_SIZE: usize = 8;
81
82 pub fn identity() -> Self {
84 Sim3 {
85 rotation: SO3::identity(),
86 translation: Vector3::zeros(),
87 scale: 1.0,
88 }
89 }
90
91 pub fn jacobian_identity() -> Matrix7<f64> {
93 Matrix7::<f64>::identity()
94 }
95
96 pub fn new(translation: Vector3<f64>, rotation: UnitQuaternion<f64>, scale: f64) -> Self {
103 assert!(scale > 0.0, "Scale must be positive");
104 Sim3 {
105 rotation: SO3::new(rotation),
106 translation,
107 scale,
108 }
109 }
110
111 pub fn from_components(translation: Vector3<f64>, rotation: SO3, scale: f64) -> Self {
113 assert!(scale > 0.0, "Scale must be positive");
114 Sim3 {
115 rotation,
116 translation,
117 scale,
118 }
119 }
120
121 pub fn translation(&self) -> Vector3<f64> {
123 self.translation
124 }
125
126 pub fn scale(&self) -> f64 {
128 self.scale
129 }
130
131 pub fn rotation_so3(&self) -> SO3 {
133 self.rotation.clone()
134 }
135
136 pub fn rotation_quaternion(&self) -> UnitQuaternion<f64> {
138 self.rotation.quaternion()
139 }
140
141 pub fn rotation_matrix(&self) -> Matrix3<f64> {
143 self.rotation.rotation_matrix()
144 }
145
146 pub fn matrix(&self) -> Matrix4<f64> {
148 let mut mat = Matrix4::identity();
149 let rot_mat = self.rotation_matrix();
150
151 for i in 0..3 {
153 for j in 0..3 {
154 mat[(i, j)] = self.scale * rot_mat[(i, j)];
155 }
156 }
157
158 mat[(0, 3)] = self.translation.x;
160 mat[(1, 3)] = self.translation.y;
161 mat[(2, 3)] = self.translation.z;
162
163 mat
164 }
165
166 pub fn x(&self) -> f64 {
168 self.translation.x
169 }
170
171 pub fn y(&self) -> f64 {
173 self.translation.y
174 }
175
176 pub fn z(&self) -> f64 {
178 self.translation.z
179 }
180
181 pub fn coeffs(&self) -> [f64; 8] {
183 let q = self.rotation.quaternion();
184 [
185 self.translation.x,
186 self.translation.y,
187 self.translation.z,
188 q.w,
189 q.i,
190 q.j,
191 q.k,
192 self.scale,
193 ]
194 }
195}
196
197impl From<DVector<f64>> for Sim3 {
198 fn from(data: DVector<f64>) -> Self {
199 let translation = Vector3::new(data[0], data[1], data[2]);
200 let rotation = SO3::from_quaternion_wxyz(data[3], data[4], data[5], data[6]);
201 let scale = data[7];
202 Sim3::from_components(translation, rotation, scale)
203 }
204}
205
206impl From<Sim3> for DVector<f64> {
207 fn from(sim3: Sim3) -> Self {
208 let q = sim3.rotation.quaternion();
209 DVector::from_vec(vec![
210 sim3.translation.x,
211 sim3.translation.y,
212 sim3.translation.z,
213 q.w,
214 q.i,
215 q.j,
216 q.k,
217 sim3.scale,
218 ])
219 }
220}
221
222impl LieGroup for Sim3 {
223 type TangentVector = Sim3Tangent;
224 type JacobianMatrix = Matrix7<f64>;
225 type LieAlgebra = Matrix4<f64>;
226
227 fn inverse(&self, jacobian: Option<&mut Self::JacobianMatrix>) -> Self {
231 let rot_inv = self.rotation.inverse(None);
232 let scale_inv = 1.0 / self.scale;
233 let trans_inv = -rot_inv.act(&self.translation, None, None) * scale_inv;
234
235 if let Some(jac) = jacobian {
236 *jac = -self.adjoint();
237 }
238
239 Sim3::from_components(trans_inv, rot_inv, scale_inv)
240 }
241
242 fn compose(
246 &self,
247 other: &Self,
248 jacobian_self: Option<&mut Self::JacobianMatrix>,
249 jacobian_other: Option<&mut Self::JacobianMatrix>,
250 ) -> Self {
251 let composed_rotation = self.rotation.compose(&other.rotation, None, None);
252 let composed_translation =
253 self.scale * self.rotation.act(&other.translation, None, None) + self.translation;
254 let composed_scale = self.scale * other.scale;
255
256 let result = Sim3::from_components(composed_translation, composed_rotation, composed_scale);
257
258 if let Some(jac_self) = jacobian_self {
259 *jac_self = other.inverse(None).adjoint();
260 }
261
262 if let Some(jac_other) = jacobian_other {
263 *jac_other = Matrix7::identity();
264 }
265
266 result
267 }
268
269 fn log(&self, jacobian: Option<&mut Self::JacobianMatrix>) -> Self::TangentVector {
271 let theta = self.rotation.log(None);
272 let sigma = self.scale.ln();
273 let mut data = Vector7::zeros();
274
275 let theta_tangent = SO3Tangent::new(theta.coeffs());
278 let v_inv = Self::compute_v_inv(&theta_tangent, sigma);
279 let translation_vector = v_inv * self.translation;
280
281 data.fixed_rows_mut::<3>(0).copy_from(&translation_vector);
282 data.fixed_rows_mut::<3>(3).copy_from(&theta.coeffs());
283 data[6] = sigma;
284
285 let result = Sim3Tangent { data };
286
287 if let Some(jac) = jacobian {
288 *jac = result.right_jacobian_inv();
289 }
290
291 result
292 }
293
294 fn act(
295 &self,
296 vector: &Vector3<f64>,
297 jacobian_self: Option<&mut Self::JacobianMatrix>,
298 jacobian_vector: Option<&mut Matrix3<f64>>,
299 ) -> Vector3<f64> {
300 let result = self.scale * self.rotation.act(vector, None, None) + self.translation;
301
302 if let Some(jac_self) = jacobian_self {
303 let rotation_matrix = self.rotation.rotation_matrix();
304 let rotated_vector = self.rotation.act(vector, None, None);
305
306 jac_self
308 .fixed_view_mut::<3, 3>(0, 0)
309 .copy_from(&Matrix3::identity());
310
311 jac_self
313 .fixed_view_mut::<3, 3>(0, 3)
314 .copy_from(&(-self.scale * rotation_matrix * SO3Tangent::new(*vector).hat()));
315
316 jac_self
318 .fixed_view_mut::<3, 1>(0, 6)
319 .copy_from(&rotated_vector);
320 }
321
322 if let Some(jac_vector) = jacobian_vector {
323 *jac_vector = self.scale * self.rotation.rotation_matrix();
324 }
325
326 result
327 }
328
329 fn adjoint(&self) -> Self::JacobianMatrix {
330 let rotation_matrix = self.rotation.rotation_matrix();
331 let translation = self.translation;
332 let scale = self.scale;
333 let mut adjoint_matrix = Matrix7::zeros();
334
335 adjoint_matrix
342 .fixed_view_mut::<3, 3>(0, 0)
343 .copy_from(&(scale * rotation_matrix));
344
345 let top_middle = SO3Tangent::new(translation).hat() * scale * rotation_matrix;
347 adjoint_matrix
348 .fixed_view_mut::<3, 3>(0, 3)
349 .copy_from(&top_middle);
350
351 adjoint_matrix
353 .fixed_view_mut::<3, 3>(3, 3)
354 .copy_from(&rotation_matrix);
355
356 adjoint_matrix[(6, 6)] = 1.0;
358
359 adjoint_matrix
360 }
361
362 fn random() -> Self {
363 use rand::Rng;
364 let mut rng = rand::rng();
365
366 let translation = Vector3::new(
367 rng.random_range(-1.0..1.0),
368 rng.random_range(-1.0..1.0),
369 rng.random_range(-1.0..1.0),
370 );
371
372 let rotation = SO3::random();
373 let scale = rng.random_range(0.5..2.0);
374
375 Sim3::from_components(translation, rotation, scale)
376 }
377
378 fn normalize(&mut self) {
379 self.rotation.normalize();
380 if self.scale <= 0.0 {
381 self.scale = 1.0;
382 }
383 }
384
385 fn is_valid(&self, tolerance: f64) -> bool {
386 self.rotation.is_valid(tolerance) && self.scale > 0.0
387 }
388
389 fn vee(&self) -> Self::TangentVector {
390 self.log(None)
391 }
392
393 fn is_approx(&self, other: &Self, tolerance: f64) -> bool {
394 let difference = self.right_minus(other, None, None);
395 difference.is_zero(tolerance)
396 }
397
398 fn jacobian_identity() -> Self::JacobianMatrix {
399 Matrix7::<f64>::identity()
400 }
401
402 fn zero_jacobian() -> Self::JacobianMatrix {
403 Matrix7::<f64>::zeros()
404 }
405}
406
407impl Sim3 {
408 fn compute_v_inv(theta: &SO3Tangent, sigma: f64) -> Matrix3<f64> {
412 let v = Sim3Tangent::v_matrix(theta, sigma);
413 v.try_inverse().unwrap_or(Matrix3::identity())
417 }
418}
419
420#[derive(Clone, PartialEq)]
427pub struct Sim3Tangent {
428 data: Vector7<f64>,
430}
431
432impl fmt::Display for Sim3Tangent {
433 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
434 let rho = self.rho();
435 let theta = self.theta();
436 let sigma = self.sigma();
437 write!(
438 f,
439 "sim3(rho: [{:.4}, {:.4}, {:.4}], theta: [{:.4}, {:.4}, {:.4}], sigma: {:.4})",
440 rho.x, rho.y, rho.z, theta.x, theta.y, theta.z, sigma
441 )
442 }
443}
444
445impl From<DVector<f64>> for Sim3Tangent {
446 fn from(data_vector: DVector<f64>) -> Self {
447 Sim3Tangent {
448 data: Vector7::from_iterator(data_vector.iter().copied()),
449 }
450 }
451}
452
453impl From<Sim3Tangent> for DVector<f64> {
454 fn from(tangent: Sim3Tangent) -> Self {
455 DVector::from_vec(tangent.data.as_slice().to_vec())
456 }
457}
458
459impl Sim3Tangent {
460 pub fn new(rho: Vector3<f64>, theta: Vector3<f64>, sigma: f64) -> Self {
462 let mut data = Vector7::zeros();
463 data.fixed_rows_mut::<3>(0).copy_from(&rho);
464 data.fixed_rows_mut::<3>(3).copy_from(&theta);
465 data[6] = sigma;
466 Sim3Tangent { data }
467 }
468
469 pub fn rho(&self) -> Vector3<f64> {
471 self.data.fixed_rows::<3>(0).into_owned()
472 }
473
474 pub fn theta(&self) -> Vector3<f64> {
476 self.data.fixed_rows::<3>(3).into_owned()
477 }
478
479 pub fn sigma(&self) -> f64 {
481 self.data[6]
482 }
483
484 pub fn from_components(
486 rho_x: f64,
487 rho_y: f64,
488 rho_z: f64,
489 theta_x: f64,
490 theta_y: f64,
491 theta_z: f64,
492 sigma: f64,
493 ) -> Self {
494 Sim3Tangent {
495 data: Vector7::from_column_slice(&[
496 rho_x, rho_y, rho_z, theta_x, theta_y, theta_z, sigma,
497 ]),
498 }
499 }
500
501 fn v_matrix(theta: &SO3Tangent, sigma: f64) -> Matrix3<f64> {
507 let theta_norm_sq = theta.coeffs().norm_squared();
508
509 if theta_norm_sq < crate::SMALL_ANGLE_THRESHOLD
510 && sigma.abs() < crate::SMALL_ANGLE_THRESHOLD
511 {
512 return Matrix3::identity();
513 }
514
515 let theta_hat = theta.hat();
516
517 if theta_norm_sq < crate::SMALL_ANGLE_THRESHOLD {
518 let a = (sigma.exp() - 1.0) / sigma;
520 return a * Matrix3::identity();
521 }
522
523 let theta_norm = theta_norm_sq.sqrt();
524 let sin_theta = theta_norm.sin();
525 let cos_theta = theta_norm.cos();
526
527 if sigma.abs() < crate::SMALL_ANGLE_THRESHOLD {
528 let a = 1.0;
530 let b = (1.0 - cos_theta) / theta_norm_sq;
531 let c = (theta_norm - sin_theta) / (theta_norm * theta_norm_sq);
532 return a * Matrix3::identity() + b * theta_hat + c * theta_hat * theta_hat;
533 }
534
535 let e_sigma = sigma.exp();
537 let alpha_sq = sigma * sigma + theta_norm_sq;
538
539 let a = (e_sigma - 1.0) / sigma;
540 let b = (e_sigma * (sigma * sin_theta - theta_norm * cos_theta) + theta_norm)
541 / (theta_norm * alpha_sq);
542 let cos_integral =
543 (e_sigma * (sigma * cos_theta + theta_norm * sin_theta) - sigma) / alpha_sq;
544 let c = (a - cos_integral) / theta_norm_sq;
545
546 a * Matrix3::identity() + b * theta_hat + c * theta_hat * theta_hat
547 }
548
549 fn q_matrix(rho: Vector3<f64>, theta: Vector3<f64>, sigma: f64) -> Matrix3<f64> {
551 let rho_skew = SO3Tangent::new(rho).hat();
552 let theta_skew = SO3Tangent::new(theta).hat();
553 let theta_squared = theta.norm_squared();
554
555 if theta_squared < crate::SMALL_ANGLE_THRESHOLD && sigma.abs() < f64::EPSILON {
556 return 0.5 * rho_skew;
557 }
558
559 let a = 0.5;
560 let mut b = 1.0 / 6.0;
561 let mut c = -1.0 / 24.0;
562 let mut d = -1.0 / 60.0;
563
564 if theta_squared > crate::SMALL_ANGLE_THRESHOLD {
565 let theta_norm = theta_squared.sqrt();
566 let theta_norm_3 = theta_norm * theta_squared;
567 let theta_norm_4 = theta_squared * theta_squared;
568 let theta_norm_5 = theta_norm_3 * theta_squared;
569 let sin_theta = theta_norm.sin();
570 let cos_theta = theta_norm.cos();
571
572 b = (theta_norm - sin_theta) / theta_norm_3;
573 c = (1.0 - theta_squared / 2.0 - cos_theta) / theta_norm_4;
574 d = (c - 3.0) * (theta_norm - sin_theta - theta_norm_3 / 6.0) / theta_norm_5;
575 }
576
577 let rho_skew_theta_skew = rho_skew * theta_skew;
578 let theta_skew_rho_skew = theta_skew * rho_skew;
579 let theta_skew_rho_skew_theta_skew = theta_skew * rho_skew * theta_skew;
580 let rho_skew_theta_skew_sq2 = rho_skew * theta_skew * theta_skew;
581
582 let m1 = rho_skew;
583 let m2 = theta_skew_rho_skew + rho_skew_theta_skew + theta_skew_rho_skew_theta_skew;
584 let m3 = rho_skew_theta_skew_sq2
585 - rho_skew_theta_skew_sq2.transpose()
586 - 3.0 * theta_skew_rho_skew_theta_skew;
587 let m4 = theta_skew_rho_skew_theta_skew * theta_skew;
588
589 m1 * a + m2 * b - m3 * c - m4 * d
590 }
591}
592
593impl Tangent<Sim3> for Sim3Tangent {
594 const DIM: usize = 7;
595
596 fn exp(&self, jacobian: Option<&mut <Sim3 as LieGroup>::JacobianMatrix>) -> Sim3 {
598 let rho = self.rho();
599 let theta = self.theta();
600 let sigma = self.sigma();
601
602 let theta_tangent = SO3Tangent::new(theta);
603 let rotation = theta_tangent.exp(None);
604 let v_matrix = Self::v_matrix(&theta_tangent, sigma);
605 let translation = v_matrix * rho;
606 let scale = sigma.exp();
607
608 if let Some(jac) = jacobian {
609 *jac = self.right_jacobian();
610 }
611
612 Sim3::from_components(translation, rotation, scale)
613 }
614
615 fn right_jacobian(&self) -> <Sim3 as LieGroup>::JacobianMatrix {
617 let mut jac = Matrix7::zeros();
618 let rho = self.rho();
619 let theta = self.theta();
620 let sigma = self.sigma();
621
622 let theta_right_jac = SO3Tangent::new(-theta).right_jacobian();
623 let q_block = Self::q_matrix(-rho, -theta, -sigma);
624
625 jac.fixed_view_mut::<3, 3>(0, 0).copy_from(&theta_right_jac);
627 jac.fixed_view_mut::<3, 3>(3, 3).copy_from(&theta_right_jac);
628 jac.fixed_view_mut::<3, 3>(0, 3).copy_from(&q_block);
629
630 jac[(6, 6)] = 1.0;
632
633 let v_deriv = Self::v_matrix(&SO3Tangent::new(-theta), -sigma);
635 jac.fixed_view_mut::<3, 1>(0, 6)
636 .copy_from(&(v_deriv * (-rho)));
637
638 jac
639 }
640
641 fn left_jacobian(&self) -> <Sim3 as LieGroup>::JacobianMatrix {
643 let mut jac = Matrix7::zeros();
644 let rho = self.rho();
645 let theta = self.theta();
646 let sigma = self.sigma();
647
648 let theta_left_jac = SO3Tangent::new(theta).left_jacobian();
649 let q_block = Self::q_matrix(rho, theta, sigma);
650
651 jac.fixed_view_mut::<3, 3>(0, 0).copy_from(&theta_left_jac);
652 jac.fixed_view_mut::<3, 3>(3, 3).copy_from(&theta_left_jac);
653 jac.fixed_view_mut::<3, 3>(0, 3).copy_from(&q_block);
654
655 jac[(6, 6)] = 1.0;
656
657 let v_deriv = Self::v_matrix(&SO3Tangent::new(theta), sigma);
658 jac.fixed_view_mut::<3, 1>(0, 6).copy_from(&(v_deriv * rho));
659
660 jac
661 }
662
663 fn right_jacobian_inv(&self) -> <Sim3 as LieGroup>::JacobianMatrix {
665 self.right_jacobian()
668 .try_inverse()
669 .unwrap_or(Matrix7::identity())
670 }
671
672 fn left_jacobian_inv(&self) -> <Sim3 as LieGroup>::JacobianMatrix {
674 self.left_jacobian()
677 .try_inverse()
678 .unwrap_or(Matrix7::identity())
679 }
680
681 fn hat(&self) -> <Sim3 as LieGroup>::LieAlgebra {
683 let mut lie_alg = Matrix4::zeros();
684
685 let theta_hat = SO3Tangent::new(self.theta()).hat();
686
687 for i in 0..3 {
689 for j in 0..3 {
690 lie_alg[(i, j)] = theta_hat[(i, j)];
691 }
692 lie_alg[(i, i)] += self.sigma();
693 }
694
695 let rho = self.rho();
697 lie_alg[(0, 3)] = rho[0];
698 lie_alg[(1, 3)] = rho[1];
699 lie_alg[(2, 3)] = rho[2];
700
701 lie_alg
702 }
703
704 fn zero() -> <Sim3 as LieGroup>::TangentVector {
705 Sim3Tangent::new(Vector3::zeros(), Vector3::zeros(), 0.0)
706 }
707
708 fn random() -> <Sim3 as LieGroup>::TangentVector {
709 use rand::Rng;
710 let mut rng = rand::rng();
711 Sim3Tangent::new(
712 Vector3::new(
713 rng.random_range(-1.0..1.0),
714 rng.random_range(-1.0..1.0),
715 rng.random_range(-1.0..1.0),
716 ),
717 Vector3::new(
718 rng.random_range(-0.1..0.1),
719 rng.random_range(-0.1..0.1),
720 rng.random_range(-0.1..0.1),
721 ),
722 rng.random_range(-0.5..0.5),
723 )
724 }
725
726 fn is_zero(&self, tolerance: f64) -> bool {
727 self.data.norm() < tolerance
728 }
729
730 fn normalize(&mut self) {
731 let theta_norm = self.theta().norm();
732 if theta_norm > f64::EPSILON {
733 self.data[3] /= theta_norm;
734 self.data[4] /= theta_norm;
735 self.data[5] /= theta_norm;
736 }
737 }
738
739 fn normalized(&self) -> <Sim3 as LieGroup>::TangentVector {
740 let norm = self.theta().norm();
741 if norm > f64::EPSILON {
742 Sim3Tangent::new(self.rho(), self.theta() / norm, self.sigma())
743 } else {
744 Sim3Tangent::new(self.rho(), Vector3::zeros(), self.sigma())
745 }
746 }
747
748 fn small_adj(&self) -> <Sim3 as LieGroup>::JacobianMatrix {
749 let mut small_adj = Matrix7::zeros();
750 let rho_skew = SO3Tangent::new(self.rho()).hat();
751 let theta_skew = SO3Tangent::new(self.theta()).hat();
752 let sigma = self.sigma();
753
754 for i in 0..3 {
760 for j in 0..3 {
761 small_adj[(i, j)] = theta_skew[(i, j)];
762 }
763 small_adj[(i, i)] += sigma;
764 }
765
766 small_adj.fixed_view_mut::<3, 3>(0, 3).copy_from(&rho_skew);
767 small_adj
768 .fixed_view_mut::<3, 1>(0, 6)
769 .copy_from(&(-self.rho()));
770 small_adj
771 .fixed_view_mut::<3, 3>(3, 3)
772 .copy_from(&theta_skew);
773
774 small_adj
775 }
776
777 fn lie_bracket(&self, other: &Self) -> <Sim3 as LieGroup>::TangentVector {
778 let bracket_result = self.small_adj() * other.data;
779 Sim3Tangent {
780 data: bracket_result,
781 }
782 }
783
784 fn is_approx(&self, other: &Self, tolerance: f64) -> bool {
785 (self.data - other.data).norm() < tolerance
786 }
787
788 fn generator(&self, i: usize) -> <Sim3 as LieGroup>::LieAlgebra {
789 assert!(i < 7, "Sim(3) only has generators for indices 0-6");
790
791 let mut generator = Matrix4::zeros();
792
793 match i {
794 0..=2 => {
795 generator[(i, 3)] = 1.0;
797 }
798 3..=5 => {
799 let idx = i - 3;
801 match idx {
802 0 => {
803 generator[(1, 2)] = -1.0;
804 generator[(2, 1)] = 1.0;
805 }
806 1 => {
807 generator[(0, 2)] = 1.0;
808 generator[(2, 0)] = -1.0;
809 }
810 2 => {
811 generator[(0, 1)] = -1.0;
812 generator[(1, 0)] = 1.0;
813 }
814 _ => unreachable!(),
815 }
816 }
817 6 => {
818 generator[(0, 0)] = 1.0;
820 generator[(1, 1)] = 1.0;
821 generator[(2, 2)] = 1.0;
822 }
823 _ => unreachable!(),
824 }
825
826 generator
827 }
828}
829
830#[cfg(test)]
831mod tests {
832 use super::*;
833
834 const TOLERANCE: f64 = 1e-9;
835
836 #[test]
837 fn test_sim3_identity() {
838 let identity = Sim3::identity();
839 assert!(identity.is_valid(TOLERANCE));
840 assert!(identity.translation().norm() < TOLERANCE);
841 assert!((identity.scale() - 1.0).abs() < TOLERANCE);
842 assert!(identity.rotation_quaternion().angle() < TOLERANCE);
843 }
844
845 #[test]
846 fn test_sim3_new() {
847 let translation = Vector3::new(1.0, 2.0, 3.0);
848 let rotation = UnitQuaternion::from_euler_angles(0.1, 0.2, 0.3);
849 let scale = 1.5;
850
851 let sim3 = Sim3::new(translation, rotation, scale);
852 assert!(sim3.is_valid(TOLERANCE));
853 assert!((sim3.translation() - translation).norm() < TOLERANCE);
854 assert!((sim3.scale() - scale).abs() < TOLERANCE);
855 }
856
857 #[test]
858 #[should_panic(expected = "Scale must be positive")]
859 fn test_sim3_new_negative_scale() {
860 let translation = Vector3::new(1.0, 2.0, 3.0);
861 let rotation = UnitQuaternion::identity();
862 let _sim3 = Sim3::new(translation, rotation, -1.0);
863 }
864
865 #[test]
866 fn test_sim3_random() {
867 let sim3 = Sim3::random();
868 assert!(sim3.is_valid(TOLERANCE));
869 assert!(sim3.scale() > 0.0);
870 }
871
872 #[test]
873 fn test_sim3_inverse() {
874 let sim3 = Sim3::random();
875 let sim3_inv = sim3.inverse(None);
876
877 let composed = sim3.compose(&sim3_inv, None, None);
878 let identity = Sim3::identity();
879
880 assert!(composed.is_approx(&identity, TOLERANCE));
881 }
882
883 #[test]
884 fn test_sim3_compose() {
885 let sim3_1 = Sim3::random();
886 let sim3_2 = Sim3::random();
887
888 let composed = sim3_1.compose(&sim3_2, None, None);
889 assert!(composed.is_valid(TOLERANCE));
890
891 let identity = Sim3::identity();
892 let composed_with_identity = sim3_1.compose(&identity, None, None);
893 assert!(composed_with_identity.is_approx(&sim3_1, TOLERANCE));
894 }
895
896 #[test]
897 fn test_sim3_exp_log() {
898 let tangent = Sim3Tangent::new(
899 Vector3::new(0.1, 0.2, 0.3),
900 Vector3::new(0.01, 0.02, 0.03),
901 0.1,
902 );
903
904 let sim3 = tangent.exp(None);
905 let recovered_tangent = sim3.log(None);
906
907 assert!((tangent.data - recovered_tangent.data).norm() < TOLERANCE);
908 }
909
910 #[test]
911 fn test_sim3_exp_zero() {
912 let zero_tangent = Sim3Tangent::zero();
913 let sim3 = zero_tangent.exp(None);
914 let identity = Sim3::identity();
915
916 assert!(sim3.is_approx(&identity, TOLERANCE));
917 }
918
919 #[test]
920 fn test_sim3_log_identity() {
921 let identity = Sim3::identity();
922 let tangent = identity.log(None);
923
924 assert!(tangent.data.norm() < TOLERANCE);
925 }
926
927 #[test]
928 fn test_sim3_adjoint() {
929 let sim3 = Sim3::random();
930 let adj = sim3.adjoint();
931
932 assert_eq!(adj.nrows(), 7);
933 assert_eq!(adj.ncols(), 7);
934 }
935
936 #[test]
937 fn test_sim3_act() {
938 let sim3 = Sim3::random();
939 let point = Vector3::new(1.0, 2.0, 3.0);
940
941 let _transformed_point = sim3.act(&point, None, None);
942
943 let identity = Sim3::identity();
944 let identity_transformed = identity.act(&point, None, None);
945
946 assert!((identity_transformed - point).norm() < TOLERANCE);
947 }
948
949 #[test]
950 fn test_sim3_between() {
951 let sim3_a = Sim3::random();
952 let sim3_b = sim3_a.clone();
953 let sim3_between_identity = sim3_a.between(&sim3_b, None, None);
954 assert!(sim3_between_identity.is_approx(&Sim3::identity(), TOLERANCE));
955
956 let sim3_c = Sim3::random();
957 let sim3_between = sim3_a.between(&sim3_c, None, None);
958 let expected = sim3_a.inverse(None).compose(&sim3_c, None, None);
959 assert!(sim3_between.is_approx(&expected, TOLERANCE));
960 }
961
962 #[test]
963 fn test_sim3_tangent_zero() {
964 let zero = Sim3Tangent::zero();
965 assert!(zero.data.norm() < TOLERANCE);
966
967 let tangent = Sim3Tangent::new(Vector3::zeros(), Vector3::zeros(), 0.0);
968 assert!(tangent.is_zero(TOLERANCE));
969 }
970
971 #[test]
972 fn test_sim3_manifold_properties() {
973 assert_eq!(Sim3::DIM, 3);
974 assert_eq!(Sim3::DOF, 7);
975 assert_eq!(Sim3::REP_SIZE, 8);
976 }
977
978 #[test]
979 fn test_sim3_consistency() {
980 let sim3_1 = Sim3::random();
981 let sim3_2 = Sim3::random();
982 let sim3_3 = Sim3::random();
983
984 let left_assoc = sim3_1
986 .compose(&sim3_2, None, None)
987 .compose(&sim3_3, None, None);
988 let right_assoc = sim3_1.compose(&sim3_2.compose(&sim3_3, None, None), None, None);
989
990 assert!(left_assoc.is_approx(&right_assoc, 1e-10));
991 }
992
993 #[test]
994 fn test_sim3_scale_composition() {
995 let sim3_1 = Sim3::new(Vector3::zeros(), UnitQuaternion::identity(), 2.0);
996 let sim3_2 = Sim3::new(Vector3::zeros(), UnitQuaternion::identity(), 3.0);
997
998 let composed = sim3_1.compose(&sim3_2, None, None);
999 assert!((composed.scale() - 6.0).abs() < TOLERANCE);
1000 }
1001
1002 #[test]
1003 fn test_sim3_tangent_small_adj() {
1004 let tangent = Sim3Tangent::new(
1005 Vector3::new(0.1, 0.2, 0.3),
1006 Vector3::new(0.4, 0.5, 0.6),
1007 0.1,
1008 );
1009 let small_adj = tangent.small_adj();
1010
1011 assert_eq!(small_adj.nrows(), 7);
1012 assert_eq!(small_adj.ncols(), 7);
1013 }
1014
1015 #[test]
1016 fn test_sim3_tangent_lie_bracket() {
1017 let tangent_a = Sim3Tangent::new(
1018 Vector3::new(0.1, 0.0, 0.0),
1019 Vector3::new(0.0, 0.2, 0.0),
1020 0.1,
1021 );
1022 let tangent_b = Sim3Tangent::new(
1023 Vector3::new(0.0, 0.3, 0.0),
1024 Vector3::new(0.0, 0.0, 0.4),
1025 0.2,
1026 );
1027
1028 let bracket_ab = tangent_a.lie_bracket(&tangent_b);
1029 let bracket_ba = tangent_b.lie_bracket(&tangent_a);
1030
1031 assert!((bracket_ab.data + bracket_ba.data).norm() < 1e-10);
1033
1034 let bracket_aa = tangent_a.lie_bracket(&tangent_a);
1036 assert!(bracket_aa.is_zero(1e-10));
1037 }
1038
1039 #[test]
1040 fn test_sim3_tangent_is_approx() {
1041 let tangent_1 = Sim3Tangent::new(
1042 Vector3::new(0.1, 0.2, 0.3),
1043 Vector3::new(0.4, 0.5, 0.6),
1044 0.1,
1045 );
1046 let tangent_2 = Sim3Tangent::new(
1047 Vector3::new(0.1 + 1e-12, 0.2, 0.3),
1048 Vector3::new(0.4, 0.5, 0.6),
1049 0.1,
1050 );
1051 let tangent_3 = Sim3Tangent::new(
1052 Vector3::new(1.0, 2.0, 3.0),
1053 Vector3::new(4.0, 5.0, 6.0),
1054 1.0,
1055 );
1056
1057 assert!(tangent_1.is_approx(&tangent_1, 1e-10));
1058 assert!(tangent_1.is_approx(&tangent_2, 1e-10));
1059 assert!(!tangent_1.is_approx(&tangent_3, 1e-10));
1060 }
1061
1062 #[test]
1063 fn test_sim3_generators() {
1064 let tangent = Sim3Tangent::new(
1065 Vector3::new(1.0, 1.0, 1.0),
1066 Vector3::new(1.0, 1.0, 1.0),
1067 1.0,
1068 );
1069
1070 for i in 0..7 {
1071 let generator = tangent.generator(i);
1072 assert_eq!(generator.nrows(), 4);
1073 assert_eq!(generator.ncols(), 4);
1074
1075 assert_eq!(generator[(3, 0)], 0.0);
1077 assert_eq!(generator[(3, 1)], 0.0);
1078 assert_eq!(generator[(3, 2)], 0.0);
1079 assert_eq!(generator[(3, 3)], 0.0);
1080 }
1081 }
1082
1083 #[test]
1084 #[should_panic]
1085 fn test_sim3_generator_invalid_index() {
1086 let tangent = Sim3Tangent::new(
1087 Vector3::new(1.0, 1.0, 1.0),
1088 Vector3::new(1.0, 1.0, 1.0),
1089 1.0,
1090 );
1091 let _generator = tangent.generator(7);
1092 }
1093
1094 #[test]
1095 fn test_sim3_vee() {
1096 let sim3 = Sim3::random();
1097 let tangent_log = sim3.log(None);
1098 let tangent_vee = sim3.vee();
1099
1100 assert!((tangent_log.data - tangent_vee.data).norm() < 1e-10);
1101 }
1102
1103 #[test]
1104 fn test_sim3_is_approx() {
1105 let sim3_1 = Sim3::random();
1106 let sim3_2 = sim3_1.clone();
1107
1108 assert!(sim3_1.is_approx(&sim3_1, 1e-10));
1109 assert!(sim3_1.is_approx(&sim3_2, 1e-10));
1110
1111 let small_tangent = Sim3Tangent::new(
1112 Vector3::new(1e-12, 1e-12, 1e-12),
1113 Vector3::new(1e-12, 1e-12, 1e-12),
1114 1e-12,
1115 );
1116 let sim3_perturbed = sim3_1.right_plus(&small_tangent, None, None);
1117 assert!(sim3_1.is_approx(&sim3_perturbed, 1e-10));
1118 }
1119
1120 #[test]
1121 fn test_sim3_small_angle_approximations() {
1122 let small_tangent = Sim3Tangent::new(
1123 Vector3::new(1e-8, 2e-8, 3e-8),
1124 Vector3::new(1e-9, 2e-9, 3e-9),
1125 1e-8,
1126 );
1127
1128 let sim3 = small_tangent.exp(None);
1129 let recovered = sim3.log(None);
1130
1131 assert!((small_tangent.data - recovered.data).norm() < TOLERANCE);
1132 }
1133
1134 #[test]
1135 fn test_sim3_accessors() {
1136 let translation = Vector3::new(1.0, 2.0, 3.0);
1137 let rotation = UnitQuaternion::identity();
1138 let scale = 1.5;
1139
1140 let sim3 = Sim3::new(translation, rotation, scale);
1141
1142 assert_eq!(sim3.x(), 1.0);
1143 assert_eq!(sim3.y(), 2.0);
1144 assert_eq!(sim3.z(), 3.0);
1145 assert_eq!(sim3.scale(), 1.5);
1146 }
1147
1148 #[test]
1149 fn test_sim3_matrix() {
1150 let translation = Vector3::new(1.0, 2.0, 3.0);
1151 let rotation = UnitQuaternion::identity();
1152 let scale = 2.0;
1153
1154 let sim3 = Sim3::new(translation, rotation, scale);
1155 let mat = sim3.matrix();
1156
1157 for i in 0..3 {
1159 for j in 0..3 {
1160 if i == j {
1161 assert!((mat[(i, j)] - 2.0).abs() < TOLERANCE);
1162 } else {
1163 assert!(mat[(i, j)].abs() < TOLERANCE);
1164 }
1165 }
1166 }
1167
1168 assert!((mat[(0, 3)] - 1.0).abs() < TOLERANCE);
1170 assert!((mat[(1, 3)] - 2.0).abs() < TOLERANCE);
1171 assert!((mat[(2, 3)] - 3.0).abs() < TOLERANCE);
1172
1173 assert!(mat[(3, 0)].abs() < TOLERANCE);
1175 assert!(mat[(3, 1)].abs() < TOLERANCE);
1176 assert!(mat[(3, 2)].abs() < TOLERANCE);
1177 assert!((mat[(3, 3)] - 1.0).abs() < TOLERANCE);
1178 }
1179
1180 #[test]
1181 fn test_sim3_scale_action() {
1182 let scale = 2.0;
1183 let sim3 = Sim3::new(Vector3::zeros(), UnitQuaternion::identity(), scale);
1184 let point = Vector3::new(1.0, 2.0, 3.0);
1185
1186 let transformed = sim3.act(&point, None, None);
1187
1188 assert!((transformed.x - 2.0).abs() < TOLERANCE);
1189 assert!((transformed.y - 4.0).abs() < TOLERANCE);
1190 assert!((transformed.z - 6.0).abs() < TOLERANCE);
1191 }
1192
1193 #[test]
1194 fn test_sim3_tangent_basic() {
1195 let rho = Vector3::new(0.1, 0.2, 0.3);
1196 let theta = Vector3::new(0.4, 0.5, 0.6);
1197 let sigma = 0.1;
1198 let tangent = Sim3Tangent::new(rho, theta, sigma);
1199 assert!((tangent.rho() - rho).norm() < TOLERANCE);
1200 assert!((tangent.theta() - theta).norm() < TOLERANCE);
1201 assert!((tangent.sigma() - sigma).abs() < TOLERANCE);
1202 }
1203
1204 #[test]
1205 fn test_sim3_tangent_from_components() {
1206 let t1 = Sim3Tangent::new(
1207 Vector3::new(1.0, 2.0, 3.0),
1208 Vector3::new(0.4, 0.5, 0.6),
1209 0.1,
1210 );
1211 let t2 = Sim3Tangent::from_components(1.0, 2.0, 3.0, 0.4, 0.5, 0.6, 0.1);
1212 assert!(t1.is_approx(&t2, TOLERANCE));
1213 }
1214
1215 #[test]
1216 fn test_sim3_normalize() {
1217 let mut sim3 = Sim3::random();
1218 sim3.normalize();
1219 assert!(sim3.is_valid(TOLERANCE));
1220 }
1221
1222 #[test]
1223 fn test_sim3_coeffs() {
1224 let translation = Vector3::new(1.0, 2.0, 3.0);
1225 let rotation = UnitQuaternion::identity();
1226 let scale = 1.5;
1227 let sim3 = Sim3::new(translation, rotation, scale);
1228 let c = sim3.coeffs();
1229 assert!((c[0] - 1.0).abs() < TOLERANCE);
1230 assert!((c[1] - 2.0).abs() < TOLERANCE);
1231 assert!((c[2] - 3.0).abs() < TOLERANCE);
1232 assert!((c[3] - 1.0).abs() < TOLERANCE); assert!((c[7] - 1.5).abs() < TOLERANCE); }
1235
1236 #[test]
1237 fn test_sim3_manif_like_operations() {
1238 let a = Sim3::random();
1239 let b = Sim3::random();
1240
1241 let a_to_b = a.between(&b, None, None);
1242 let recovered_b = a.compose(&a_to_b, None, None);
1243 assert!(recovered_b.is_approx(&b, TOLERANCE));
1244
1245 let tangent = Sim3Tangent::new(
1246 Vector3::new(0.01, 0.02, 0.03),
1247 Vector3::new(0.001, 0.002, 0.003),
1248 0.01,
1249 );
1250 let perturbed = a.plus(&tangent, None, None);
1251 let recovered_tangent = perturbed.minus(&a, None, None);
1252 assert!((tangent.data - recovered_tangent.data).norm() < 1e-6);
1253 }
1254
1255 #[test]
1256 fn test_sim3_right_jacobian_inverse_identity() {
1257 let tangent = Sim3Tangent::new(
1258 Vector3::new(0.1, 0.2, 0.3),
1259 Vector3::new(0.01, 0.02, 0.03),
1260 0.1,
1261 );
1262 let jr = tangent.right_jacobian();
1263 let jr_inv = tangent.right_jacobian_inv();
1264 let product = jr * jr_inv;
1265 let identity = Matrix7::<f64>::identity();
1266 assert!((product - identity).norm() < 0.01);
1267 }
1268
1269 #[test]
1270 fn test_sim3_left_jacobian_inverse_identity() {
1271 let tangent = Sim3Tangent::new(
1272 Vector3::new(0.1, 0.2, 0.3),
1273 Vector3::new(0.01, 0.02, 0.03),
1274 0.1,
1275 );
1276 let jl = tangent.left_jacobian();
1277 let jl_inv = tangent.left_jacobian_inv();
1278 let product = jl * jl_inv;
1279 let identity = Matrix7::<f64>::identity();
1280 assert!((product - identity).norm() < 0.01);
1281 }
1282
1283 #[test]
1284 fn test_sim3_jacobi_identity() {
1285 let a = Sim3Tangent::random();
1286 let b = Sim3Tangent::random();
1287 let c = Sim3Tangent::random();
1288
1289 let term1 = a.lie_bracket(&b.lie_bracket(&c));
1290 let term2 = b.lie_bracket(&c.lie_bracket(&a));
1291 let term3 = c.lie_bracket(&a.lie_bracket(&b));
1292
1293 assert!((term1.data + term2.data + term3.data).norm() < 1e-8);
1294 }
1295
1296 #[test]
1297 fn test_sim3_hat_matrix_structure() {
1298 let tangent = Sim3Tangent::new(
1299 Vector3::new(1.0, 2.0, 3.0),
1300 Vector3::new(0.1, 0.2, 0.3),
1301 0.5,
1302 );
1303 let hat = tangent.hat();
1304
1305 for j in 0..4 {
1306 assert_eq!(hat[(3, j)], 0.0);
1307 }
1308
1309 assert!((hat[(0, 3)] - 1.0).abs() < TOLERANCE);
1310 assert!((hat[(1, 3)] - 2.0).abs() < TOLERANCE);
1311 assert!((hat[(2, 3)] - 3.0).abs() < TOLERANCE);
1312
1313 assert!((hat[(0, 0)] - 0.5).abs() < 0.5); }
1316
1317 #[test]
1318 fn test_sim3_dvector_round_trip() {
1319 let sim3 = Sim3::random();
1320 let dvec: DVector<f64> = sim3.clone().into();
1321 let recovered: Sim3 = dvec.into();
1322 assert!(sim3.is_approx(&recovered, TOLERANCE));
1323 }
1324
1325 #[test]
1328 fn test_sim3_display() {
1329 let sim3 = Sim3::identity();
1330 let s = format!("{}", sim3);
1331 assert!(s.contains("Sim3"));
1332
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 ts = format!("{}", tangent);
1339 assert!(ts.contains("sim3"));
1340 }
1341
1342 #[test]
1343 fn test_sim3_jacobian_identity_static() {
1344 let jac = Sim3::jacobian_identity();
1345 assert!((jac - Matrix7::identity()).norm() < TOLERANCE);
1346 }
1347
1348 #[test]
1349 fn test_sim3_rotation_so3() {
1350 let sim3 = Sim3::random();
1351 let so3 = sim3.rotation_so3();
1352 assert!(so3.is_valid(TOLERANCE));
1353 }
1354
1355 #[test]
1356 fn test_sim3_inverse_with_jacobian() {
1357 let sim3 = Sim3::random();
1358 let mut jac = Matrix7::zeros();
1359 let inv = sim3.inverse(Some(&mut jac));
1360 let expected_jac = -sim3.adjoint();
1361 assert!((jac - expected_jac).norm() < TOLERANCE);
1362 let composed = sim3.compose(&inv, None, None);
1363 assert!(composed.is_approx(&Sim3::identity(), TOLERANCE));
1364 }
1365
1366 #[test]
1367 fn test_sim3_compose_with_jacobians() {
1368 let a = Sim3::random();
1369 let b = Sim3::random();
1370 let mut jac_a = Matrix7::zeros();
1371 let mut jac_b = Matrix7::zeros();
1372 let _composed = a.compose(&b, Some(&mut jac_a), Some(&mut jac_b));
1373 assert!(jac_a.norm() > 0.0);
1374 assert!(jac_b.norm() > 0.0);
1375 }
1376
1377 #[test]
1378 fn test_sim3_act_with_jacobians() {
1379 let sim3 = Sim3::random();
1380 let point = Vector3::new(1.0, 2.0, 3.0);
1381 let mut jac_self = Matrix7::zeros();
1382 let mut jac_point = Matrix3::<f64>::zeros();
1383 let result = sim3.act(&point, Some(&mut jac_self), Some(&mut jac_point));
1384 assert!(result.norm() > 0.0);
1385 assert!(jac_self.norm() > 0.0);
1386 assert!(jac_point.norm() > 0.0);
1387 }
1388
1389 #[test]
1390 fn test_sim3_liegroup_jacobian_identity() {
1391 let jac = <Sim3 as LieGroup>::jacobian_identity();
1392 assert!((jac - Matrix7::identity()).norm() < TOLERANCE);
1393
1394 let zero = <Sim3 as LieGroup>::zero_jacobian();
1395 assert!(zero.norm() < TOLERANCE);
1396 }
1397
1398 #[test]
1399 fn test_sim3_tangent_dvector_conversions() {
1400 let tangent = Sim3Tangent::new(
1401 Vector3::new(1.0, 2.0, 3.0),
1402 Vector3::new(0.1, 0.2, 0.3),
1403 0.5,
1404 );
1405 let dvec: DVector<f64> = tangent.clone().into();
1406 let recovered = Sim3Tangent::from(dvec);
1407 assert!(tangent.is_approx(&recovered, TOLERANCE));
1408 }
1409
1410 #[test]
1411 fn test_sim3_exp_with_jacobian() {
1412 let tangent = Sim3Tangent::new(
1413 Vector3::new(0.1, 0.2, 0.3),
1414 Vector3::new(0.01, 0.02, 0.03),
1415 0.1,
1416 );
1417 let mut jac = Matrix7::zeros();
1418 let _result = tangent.exp(Some(&mut jac));
1419 assert!(jac.norm() > 0.0);
1420 }
1421
1422 #[test]
1423 fn test_sim3_exp_log_stress() {
1424 for _ in 0..100 {
1425 let tangent = Sim3Tangent::random();
1426 let sim3 = tangent.exp(None);
1427 let recovered = sim3.log(None);
1428 assert!(
1429 tangent.is_approx(&recovered, 1e-6),
1430 "exp/log round-trip failed: error = {}",
1431 (tangent.data - recovered.data).norm()
1432 );
1433 }
1434 }
1435
1436 #[test]
1437 fn test_sim3_right_plus_minus_round_trip() {
1438 let a = Sim3::random();
1439 let b = Sim3::random();
1440 let diff = a.right_minus(&b, None, None);
1441 let recovered = b.right_plus(&diff, None, None);
1442 assert!(a.is_approx(&recovered, 1e-6));
1443 }
1444
1445 #[test]
1446 fn test_sim3_left_plus_minus_round_trip() {
1447 let a = Sim3::random();
1448 let b = Sim3::random();
1449 let diff = a.left_minus(&b, None, None);
1450 let recovered = b.left_plus(&diff, None, None);
1451 assert!(a.is_approx(&recovered, 1e-6));
1452 }
1453
1454 #[test]
1455 fn test_sim3_compose_associativity() {
1456 let a = Sim3::random();
1457 let b = Sim3::random();
1458 let c = Sim3::random();
1459 let ab_c = a.compose(&b, None, None).compose(&c, None, None);
1460 let a_bc = a.compose(&b.compose(&c, None, None), None, None);
1461 assert!(ab_c.is_approx(&a_bc, 1e-6));
1462 }
1463
1464 #[test]
1465 fn test_sim3_inverse_twice() {
1466 let g = Sim3::random();
1467 let g_inv_inv = g.inverse(None).inverse(None);
1468 assert!(g.is_approx(&g_inv_inv, 1e-6));
1469 }
1470
1471 #[test]
1472 fn test_sim3_pure_scale() {
1473 let tangent = Sim3Tangent::new(Vector3::zeros(), Vector3::zeros(), 0.5);
1475 let sim3 = tangent.exp(None);
1476 let recovered = sim3.log(None);
1477 assert!(tangent.is_approx(&recovered, TOLERANCE));
1478 assert!((sim3.scale() - 0.5_f64.exp()).abs() < TOLERANCE);
1479 }
1480
1481 #[test]
1482 fn test_sim3_pure_rotation() {
1483 let tangent = Sim3Tangent::new(
1485 Vector3::new(0.1, 0.2, 0.3),
1486 Vector3::new(0.05, 0.1, 0.15),
1487 0.0,
1488 );
1489 let sim3 = tangent.exp(None);
1490 let recovered = sim3.log(None);
1491 assert!(tangent.is_approx(&recovered, 1e-6));
1492 assert!((sim3.scale() - 1.0).abs() < TOLERANCE);
1493 }
1494
1495 #[test]
1496 fn test_sim3_scale_compose() {
1497 let a = Sim3::from_components(Vector3::zeros(), SO3::identity(), 2.0);
1498 let b = Sim3::from_components(Vector3::zeros(), SO3::identity(), 3.0);
1499 let ab = a.compose(&b, None, None);
1500 assert!((ab.scale() - 6.0).abs() < TOLERANCE);
1501 }
1502
1503 #[test]
1504 fn test_sim3_tangent_normalize_zero_theta() {
1505 let tangent = Sim3Tangent::new(Vector3::new(1.0, 2.0, 3.0), Vector3::zeros(), 0.5);
1506 let normalized = tangent.normalized();
1507 assert!(normalized.theta().norm() < TOLERANCE);
1508 }
1509
1510 #[test]
1511 fn test_sim3_tangent_generator_all() {
1512 let tangent = Sim3Tangent::zero();
1513 for i in 0..7 {
1514 let g = tangent.generator(i);
1515 assert!(g.norm() > 0.0, "Generator {} should be non-zero", i);
1516 }
1517 }
1518
1519 #[test]
1520 fn test_sim3_v_matrix_sigma_zero_matches_so3_jl() {
1521 let theta = SO3Tangent::new(Vector3::new(0.3, 0.4, 0.5));
1523 let v = Sim3Tangent::v_matrix(&theta, 0.0);
1524 let jl = theta.left_jacobian();
1525 assert!(
1526 (v - jl).norm() < 1e-10,
1527 "V(θ,0) should equal Jl(θ), error = {}",
1528 (v - jl).norm()
1529 );
1530 }
1531
1532 #[test]
1533 fn test_sim3_v_matrix_theta_zero() {
1534 let theta = SO3Tangent::new(Vector3::zeros());
1536 let sigma = 0.5;
1537 let v = Sim3Tangent::v_matrix(&theta, sigma);
1538 let expected = (sigma.exp() - 1.0) / sigma * Matrix3::<f64>::identity();
1539 assert!(
1540 (v - expected).norm() < 1e-10,
1541 "V(0,σ) should equal (e^σ-1)/σ * I, error = {}",
1542 (v - expected).norm()
1543 );
1544 }
1545}