Skip to main content

apex_manifolds/
sim3.rs

1//! Sim(3) - Similarity Transformations in 3D
2//!
3//! This module implements the Similarity group Sim(3), which represents
4//! transformations including rotation, translation, and uniform scaling in 3D space.
5//!
6//! Sim(3) is the semi-direct product: (SO(3) × ℝ₊) ⋉ ℝ³
7//!
8//! Sim(3) elements are represented as (R, t, s) where:
9//! - R ∈ SO(3): rotation
10//! - t ∈ ℝ³: translation
11//! - s ∈ ℝ₊: scale (positive real number)
12//!
13//! Sim(3) tangent elements are represented as [ρ(3), θ(3), σ(1)] = 7 components:
14//! - ρ: translational component
15//! - θ: rotational component (axis-angle)
16//! - σ: logarithmic scale (log(s))
17//!
18//! The implementation follows conventions from:
19//! - Ethan Eade's "Lie Groups for Computer Vision" (Section 6)
20//! - Sophus library Sim3 implementation
21//! - manif C++ library patterns
22//!
23//! # Use Cases
24//! - Visual SLAM with scale ambiguity (monocular cameras)
25//! - Structure from Motion
26//! - 3D reconstruction with unknown scale
27//!
28//! # References
29//! - Ethan Eade: "Lie Groups for Computer Vision" - https://www.ethaneade.com/lie.pdf
30//! - Sophus library: sophus/sim3.hpp
31
32use crate::{
33    LieGroup, Tangent,
34    so3::{SO3, SO3Tangent},
35};
36use nalgebra::{DVector, Matrix3, Matrix4, SMatrix, SVector, UnitQuaternion, Vector3};
37
38// Type aliases for Sim(3) - 7 DOF
39type Vector7<T> = SVector<T, 7>;
40type Matrix7<T> = SMatrix<T, 7, 7>;
41use std::{
42    fmt,
43    fmt::{Display, Formatter},
44};
45
46/// Sim(3) group element representing similarity transformations in 3D.
47///
48/// Represented as (rotation, translation, scale).
49#[derive(Clone, PartialEq)]
50pub struct Sim3 {
51    /// Rotation part as SO(3) element
52    rotation: SO3,
53    /// Translation part as Vector3
54    translation: Vector3<f64>,
55    /// Scale factor (positive real number)
56    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    /// Space dimension - dimension of the ambient space
74    pub const DIM: usize = 3;
75
76    /// Degrees of freedom - dimension of the tangent space
77    pub const DOF: usize = 7;
78
79    /// Representation size - size of the underlying data representation
80    pub const REP_SIZE: usize = 8;
81
82    /// Get the identity element of the group.
83    pub fn identity() -> Self {
84        Sim3 {
85            rotation: SO3::identity(),
86            translation: Vector3::zeros(),
87            scale: 1.0,
88        }
89    }
90
91    /// Get the identity matrix for Jacobians.
92    pub fn jacobian_identity() -> Matrix7<f64> {
93        Matrix7::<f64>::identity()
94    }
95
96    /// Create a new Sim(3) element from translation, rotation, and scale.
97    ///
98    /// # Arguments
99    /// * `translation` - Translation vector [x, y, z]
100    /// * `rotation` - Unit quaternion representing rotation
101    /// * `scale` - Scale factor (must be positive)
102    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    /// Create Sim(3) from components.
112    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    /// Get the translation part as a Vector3.
122    pub fn translation(&self) -> Vector3<f64> {
123        self.translation
124    }
125
126    /// Get the scale factor.
127    pub fn scale(&self) -> f64 {
128        self.scale
129    }
130
131    /// Get the rotation part as SO3.
132    pub fn rotation_so3(&self) -> SO3 {
133        self.rotation.clone()
134    }
135
136    /// Get the rotation part as a UnitQuaternion.
137    pub fn rotation_quaternion(&self) -> UnitQuaternion<f64> {
138        self.rotation.quaternion()
139    }
140
141    /// Get the rotation matrix (3x3).
142    pub fn rotation_matrix(&self) -> Matrix3<f64> {
143        self.rotation.rotation_matrix()
144    }
145
146    /// Get the 4x4 homogeneous transformation matrix.
147    pub fn matrix(&self) -> Matrix4<f64> {
148        let mut mat = Matrix4::identity();
149        let rot_mat = self.rotation_matrix();
150
151        // Top-left 3x3: s*R
152        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        // Top-right 3x1: t
159        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    /// Get the x component of translation.
167    pub fn x(&self) -> f64 {
168        self.translation.x
169    }
170
171    /// Get the y component of translation.
172    pub fn y(&self) -> f64 {
173        self.translation.y
174    }
175
176    /// Get the z component of translation.
177    pub fn z(&self) -> f64 {
178        self.translation.z
179    }
180
181    /// Get the parameter vector [tx, ty, tz, qw, qx, qy, qz, s].
182    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    /// Get the inverse.
228    ///
229    /// For Sim(3): g^{-1} = (R^T, -R^T * t / s, 1/s)
230    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    /// Composition of this and another Sim(3) element.
243    ///
244    /// g1 ∘ g2 = (R1*R2, s1*R1*t2 + t1, s1*s2)
245    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    /// Logarithmic map from Sim(3) to its tangent space.
270    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        // Compute the V^{-1} matrix for Sim(3)
276        // V^{-1} = J_l^{-1}(θ) * (I - σ/2 * [θ]× + ...)
277        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            // Jacobian w.r.t. translation
307            jac_self
308                .fixed_view_mut::<3, 3>(0, 0)
309                .copy_from(&Matrix3::identity());
310
311            // Jacobian w.r.t. rotation
312            jac_self
313                .fixed_view_mut::<3, 3>(0, 3)
314                .copy_from(&(-self.scale * rotation_matrix * SO3Tangent::new(*vector).hat()));
315
316            // Jacobian w.r.t. scale
317            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        // Block structure for Sim(3):
336        // [sR   [t]×sR   0]
337        // [0      R      0]
338        // [0      0      1]
339
340        // Top-left: s*R
341        adjoint_matrix
342            .fixed_view_mut::<3, 3>(0, 0)
343            .copy_from(&(scale * rotation_matrix));
344
345        // Top-middle: [t]× * s*R
346        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        // Middle-middle: R
352        adjoint_matrix
353            .fixed_view_mut::<3, 3>(3, 3)
354            .copy_from(&rotation_matrix);
355
356        // Bottom-right: 1
357        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    /// Compute V^{-1} matrix for Sim(3) logarithm.
409    ///
410    /// Compute the inverse of the V matrix for Sim(3) logarithm.
411    fn compute_v_inv(theta: &SO3Tangent, sigma: f64) -> Matrix3<f64> {
412        let v = Sim3Tangent::v_matrix(theta, sigma);
413        // SAFETY: V is non-singular for all valid Sim(3) elements (non-zero scale).
414        // The identity fallback is only reached in degenerate near-zero-scale cases,
415        // which are outside the valid domain of Sim(3).
416        v.try_inverse().unwrap_or(Matrix3::identity())
417    }
418}
419
420/// Sim(3) tangent space element.
421///
422/// Represented as [ρ(3), θ(3), σ(1)] where:
423/// - ρ: translational component
424/// - θ: rotational component (axis-angle)
425/// - σ: logarithmic scale
426#[derive(Clone, PartialEq)]
427pub struct Sim3Tangent {
428    /// Internal data: [ρ_x, ρ_y, ρ_z, θ_x, θ_y, θ_z, σ]
429    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    /// Create a new Sim(3)Tangent from components.
461    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    /// Get the ρ (translational) part.
470    pub fn rho(&self) -> Vector3<f64> {
471        self.data.fixed_rows::<3>(0).into_owned()
472    }
473
474    /// Get the θ (rotational) part.
475    pub fn theta(&self) -> Vector3<f64> {
476        self.data.fixed_rows::<3>(3).into_owned()
477    }
478
479    /// Get the σ (logarithmic scale) part.
480    pub fn sigma(&self) -> f64 {
481        self.data[6]
482    }
483
484    /// Create Sim3Tangent from individual scalar components.
485    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    /// Compute the V matrix for Sim(3) exponential map.
502    ///
503    /// V = ∫₀¹ exp(σt) · exp(θt×) dt = A·I + B·[θ]× + C·[θ]ײ
504    ///
505    /// Based on Ethan Eade's "Lie Groups for Computer Vision" formulation.
506    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            // Pure scale, no rotation: V = (e^σ - 1)/σ · I
519            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            // Pure rotation, no scale: V = SO(3) left Jacobian
529            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        // General case: both sigma and theta nonzero
536        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    /// Compute the Q matrix for Sim(3) Jacobians.
550    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    /// Exponential map to Sim(3).
597    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    /// Right Jacobian for Sim(3).
616    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        // Block structure for Sim(3)
626        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        // Scale part
631        jac[(6, 6)] = 1.0;
632
633        // Coupling between translation and scale
634        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    /// Left Jacobian for Sim(3).
642    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    /// Inverse of right Jacobian.
664    fn right_jacobian_inv(&self) -> <Sim3 as LieGroup>::JacobianMatrix {
665        // SAFETY: The right Jacobian is non-singular for valid Sim(3) tangent elements.
666        // Identity fallback only occurs at degenerate inputs outside the valid domain.
667        self.right_jacobian()
668            .try_inverse()
669            .unwrap_or(Matrix7::identity())
670    }
671
672    /// Inverse of left Jacobian.
673    fn left_jacobian_inv(&self) -> <Sim3 as LieGroup>::JacobianMatrix {
674        // SAFETY: The left Jacobian is non-singular for valid Sim(3) tangent elements.
675        // Identity fallback only occurs at degenerate inputs outside the valid domain.
676        self.left_jacobian()
677            .try_inverse()
678            .unwrap_or(Matrix7::identity())
679    }
680
681    /// Hat operator: maps tangent vector to Lie algebra matrix (4x4).
682    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        // Top-left 3x3: [θ]× + σ*I
688        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        // Top-right 3x1: ρ
696        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        // Block structure for Sim(3):
755        // [θ× + σ*I   ρ×   ρ]
756        // [   0       θ×   0]
757        // [   0       0    0]
758
759        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                // Translation generators (rho)
796                generator[(i, 3)] = 1.0;
797            }
798            3..=5 => {
799                // Rotation generators (theta)
800                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                // Scale generator (sigma)
819                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        // Test associativity
985        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        // Anti-symmetry
1032        assert!((bracket_ab.data + bracket_ba.data).norm() < 1e-10);
1033
1034        // [a,a] = 0
1035        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            // Bottom row should be zeros
1076            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        // Check that top-left 3x3 is scaled rotation (2*I in this case)
1158        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        // Check translation
1169        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        // Check bottom row
1174        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); // qw
1233        assert!((c[7] - 1.5).abs() < TOLERANCE); // scale
1234    }
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        // Diagonal includes sigma
1314        assert!((hat[(0, 0)] - 0.5).abs() < 0.5); // theta_skew + sigma
1315    }
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    // --- Additional coverage tests ---
1326
1327    #[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        // exp/log with only sigma nonzero (theta=0)
1474        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        // exp/log with only theta nonzero (sigma=0)
1484        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        // When sigma=0, V-matrix should equal SO3 left Jacobian
1522        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        // When theta=0, V-matrix should be (e^σ - 1)/σ * I
1535        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}