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::{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/// Stored as a flat `SVector<f64, 8>` = [tx, ty, tz, qw, qx, qy, qz, scale].
49#[derive(Clone, PartialEq)]
50pub struct Sim3 {
51    /// Flat parameter storage: [tx, ty, tz, qw, qx, qy, qz, scale]
52    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
68impl Sim3 {
69    /// Space dimension - dimension of the ambient space
70    pub const DIM: usize = 3;
71
72    /// Degrees of freedom - dimension of the tangent space
73    pub const DOF: usize = 7;
74
75    /// Representation size - size of the underlying data representation
76    pub const REP_SIZE: usize = 8;
77
78    #[inline]
79    fn translation_impl(&self) -> Vector3<f64> {
80        Vector3::new(self.params[0], self.params[1], self.params[2])
81    }
82
83    #[inline]
84    fn rotation_impl(&self) -> SO3 {
85        SO3::from_quaternion_wxyz(
86            self.params[3],
87            self.params[4],
88            self.params[5],
89            self.params[6],
90        )
91    }
92
93    #[inline]
94    fn scale_impl(&self) -> f64 {
95        self.params[7]
96    }
97
98    #[inline]
99    fn from_parts(t: Vector3<f64>, r: &SO3, scale: f64) -> Self {
100        let q = r.params();
101        Sim3 {
102            params: SVector::<f64, 8>::from([t.x, t.y, t.z, q[0], q[1], q[2], q[3], scale]),
103        }
104    }
105
106    /// Get the identity element of the group.
107    pub fn identity() -> Self {
108        Sim3 {
109            params: SVector::<f64, 8>::from([0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0]),
110        }
111    }
112
113    /// Get the identity matrix for Jacobians.
114    pub fn jacobian_identity() -> Matrix7<f64> {
115        Matrix7::<f64>::identity()
116    }
117
118    /// Create a new Sim(3) element from translation, rotation, and scale.
119    ///
120    /// # Arguments
121    /// * `translation` - Translation vector [x, y, z]
122    /// * `rotation` - Unit quaternion representing rotation
123    /// * `scale` - Scale factor (must be positive)
124    pub fn new(translation: Vector3<f64>, rotation: UnitQuaternion<f64>, scale: f64) -> Self {
125        assert!(scale > 0.0, "Scale must be positive");
126        Sim3::from_parts(translation, &SO3::new(rotation), scale)
127    }
128
129    /// Create Sim(3) from components.
130    pub fn from_components(translation: Vector3<f64>, rotation: SO3, scale: f64) -> Self {
131        assert!(scale > 0.0, "Scale must be positive");
132        Sim3::from_parts(translation, &rotation, scale)
133    }
134
135    /// Get the translation part as a Vector3.
136    pub fn translation(&self) -> Vector3<f64> {
137        self.translation_impl()
138    }
139
140    /// Get the scale factor.
141    pub fn scale(&self) -> f64 {
142        self.scale_impl()
143    }
144
145    /// Get the rotation part as SO3.
146    pub fn rotation_so3(&self) -> SO3 {
147        self.rotation_impl()
148    }
149
150    /// Get the rotation part as a UnitQuaternion.
151    pub fn rotation_quaternion(&self) -> UnitQuaternion<f64> {
152        self.rotation_impl().quaternion()
153    }
154
155    /// Get the rotation matrix (3x3).
156    pub fn rotation_matrix(&self) -> Matrix3<f64> {
157        self.rotation_impl().rotation_matrix()
158    }
159
160    /// Get the 4x4 homogeneous transformation matrix.
161    pub fn matrix(&self) -> Matrix4<f64> {
162        let mut mat = Matrix4::identity();
163        let rot_mat = self.rotation_matrix();
164        let scale = self.scale_impl();
165
166        for i in 0..3 {
167            for j in 0..3 {
168                mat[(i, j)] = scale * rot_mat[(i, j)];
169            }
170        }
171
172        mat[(0, 3)] = self.params[0];
173        mat[(1, 3)] = self.params[1];
174        mat[(2, 3)] = self.params[2];
175
176        mat
177    }
178
179    /// Get the x component of translation.
180    pub fn x(&self) -> f64 {
181        self.params[0]
182    }
183
184    /// Get the y component of translation.
185    pub fn y(&self) -> f64 {
186        self.params[1]
187    }
188
189    /// Get the z component of translation.
190    pub fn z(&self) -> f64 {
191        self.params[2]
192    }
193
194    /// Get the parameter vector [tx, ty, tz, qw, qx, qy, qz, s].
195    pub fn coeffs(&self) -> [f64; 8] {
196        [
197            self.params[0],
198            self.params[1],
199            self.params[2],
200            self.params[3],
201            self.params[4],
202            self.params[5],
203            self.params[6],
204            self.params[7],
205        ]
206    }
207}
208
209impl LieGroup for Sim3 {
210    const NAME: &'static str = "Sim3";
211
212    type TangentVector = Sim3Tangent;
213    type JacobianMatrix = Matrix7<f64>;
214    type LieAlgebra = Matrix4<f64>;
215
216    /// Get the inverse.
217    ///
218    /// For Sim(3): g^{-1} = (R^T, -R^T * t / s, 1/s)
219    fn inverse(&self, jacobian: Option<&mut Self::JacobianMatrix>) -> Self {
220        let rot = self.rotation_impl();
221        let rot_inv = rot.inverse(None);
222        let scale_inv = 1.0 / self.scale_impl();
223        let trans_inv = -rot_inv.act(&self.translation_impl(), None, None) * scale_inv;
224
225        if let Some(jac) = jacobian {
226            *jac = -self.adjoint();
227        }
228
229        Sim3::from_parts(trans_inv, &rot_inv, scale_inv)
230    }
231
232    /// Composition of this and another Sim(3) element.
233    ///
234    /// g1 ∘ g2 = (R1*R2, s1*R1*t2 + t1, s1*s2)
235    fn compose(
236        &self,
237        other: &Self,
238        jacobian_self: Option<&mut Self::JacobianMatrix>,
239        jacobian_other: Option<&mut Self::JacobianMatrix>,
240    ) -> Self {
241        let rot = self.rotation_impl();
242        let scale = self.scale_impl();
243        let composed_rotation = rot.compose(&other.rotation_impl(), None, None);
244        let composed_translation =
245            scale * rot.act(&other.translation_impl(), None, None) + self.translation_impl();
246        let composed_scale = scale * other.scale_impl();
247
248        let result = Sim3::from_parts(composed_translation, &composed_rotation, composed_scale);
249
250        if let Some(jac_self) = jacobian_self {
251            *jac_self = other.inverse(None).adjoint();
252        }
253
254        if let Some(jac_other) = jacobian_other {
255            *jac_other = Matrix7::identity();
256        }
257
258        result
259    }
260
261    /// Logarithmic map from Sim(3) to its tangent space.
262    fn log(&self, jacobian: Option<&mut Self::JacobianMatrix>) -> Self::TangentVector {
263        let theta = self.rotation_impl().log(None);
264        let sigma = self.scale_impl().ln();
265        let mut data = Vector7::zeros();
266
267        // Compute the V^{-1} matrix for Sim(3)
268        // V^{-1} = J_l^{-1}(θ) * (I - σ/2 * [θ]× + ...)
269        let theta_tangent = SO3Tangent::new(theta.coeffs());
270        let v_inv = Self::compute_v_inv(&theta_tangent, sigma);
271        let translation_vector = v_inv * self.translation_impl();
272
273        data.fixed_rows_mut::<3>(0).copy_from(&translation_vector);
274        data.fixed_rows_mut::<3>(3).copy_from(&theta.coeffs());
275        data[6] = sigma;
276
277        let result = Sim3Tangent { data };
278
279        if let Some(jac) = jacobian {
280            *jac = result.right_jacobian_inv();
281        }
282
283        result
284    }
285
286    fn act(
287        &self,
288        vector: &Vector3<f64>,
289        jacobian_self: Option<&mut Self::JacobianMatrix>,
290        jacobian_vector: Option<&mut Matrix3<f64>>,
291    ) -> Vector3<f64> {
292        let rot = self.rotation_impl();
293        let scale = self.scale_impl();
294        let rotation_matrix = rot.rotation_matrix();
295        let result = scale * rot.act(vector, None, None) + self.translation_impl();
296
297        if let Some(jac_self) = jacobian_self {
298            let rotated_vector = rot.act(vector, None, None);
299
300            jac_self
301                .fixed_view_mut::<3, 3>(0, 0)
302                .copy_from(&Matrix3::identity());
303
304            jac_self
305                .fixed_view_mut::<3, 3>(0, 3)
306                .copy_from(&(-scale * rotation_matrix * SO3Tangent::new(*vector).hat()));
307
308            jac_self
309                .fixed_view_mut::<3, 1>(0, 6)
310                .copy_from(&rotated_vector);
311        }
312
313        if let Some(jac_vector) = jacobian_vector {
314            *jac_vector = scale * rotation_matrix;
315        }
316
317        result
318    }
319
320    fn adjoint(&self) -> Self::JacobianMatrix {
321        let rotation_matrix = self.rotation_impl().rotation_matrix();
322        let translation = self.translation_impl();
323        let scale = self.scale_impl();
324        let mut adjoint_matrix = Matrix7::zeros();
325
326        // Block structure for Sim(3):
327        // [sR   [t]×sR   0]
328        // [0      R      0]
329        // [0      0      1]
330
331        // Top-left: s*R
332        adjoint_matrix
333            .fixed_view_mut::<3, 3>(0, 0)
334            .copy_from(&(scale * rotation_matrix));
335
336        // Top-middle: [t]× * s*R
337        let top_middle = SO3Tangent::new(translation).hat() * scale * rotation_matrix;
338        adjoint_matrix
339            .fixed_view_mut::<3, 3>(0, 3)
340            .copy_from(&top_middle);
341
342        // Middle-middle: R
343        adjoint_matrix
344            .fixed_view_mut::<3, 3>(3, 3)
345            .copy_from(&rotation_matrix);
346
347        // Bottom-right: 1
348        adjoint_matrix[(6, 6)] = 1.0;
349
350        adjoint_matrix
351    }
352
353    fn random() -> Self {
354        use rand::Rng;
355        let mut rng = rand::rng();
356
357        let translation = Vector3::new(
358            rng.random_range(-1.0..1.0),
359            rng.random_range(-1.0..1.0),
360            rng.random_range(-1.0..1.0),
361        );
362
363        let rotation = SO3::random();
364        let scale = rng.random_range(0.5..2.0);
365
366        Sim3::from_parts(translation, &rotation, scale)
367    }
368
369    fn normalize(&mut self) {
370        let mut rot = self.rotation_impl();
371        rot.normalize();
372        let q = rot.params();
373        self.params[3] = q[0];
374        self.params[4] = q[1];
375        self.params[5] = q[2];
376        self.params[6] = q[3];
377        if self.params[7] <= 0.0 {
378            self.params[7] = 1.0;
379        }
380    }
381
382    fn is_valid(&self, tolerance: f64) -> bool {
383        self.rotation_impl().is_valid(tolerance) && self.params[7] > 0.0
384    }
385
386    fn as_param_slice(&self) -> &[f64] {
387        self.params.as_slice()
388    }
389
390    fn as_param_slice_mut(&mut self) -> &mut [f64] {
391        self.params.as_mut_slice()
392    }
393
394    fn from_param_slice(s: &[f64]) -> Self {
395        debug_assert_eq!(s.len(), 8);
396        Sim3 {
397            params: SVector::from_column_slice(s),
398        }
399    }
400
401    fn vee(&self) -> Self::TangentVector {
402        self.log(None)
403    }
404
405    fn is_approx(&self, other: &Self, tolerance: f64) -> bool {
406        let difference = self.right_minus(other, None, None);
407        difference.is_zero(tolerance)
408    }
409
410    fn jacobian_identity() -> Self::JacobianMatrix {
411        Matrix7::<f64>::identity()
412    }
413
414    fn zero_jacobian() -> Self::JacobianMatrix {
415        Matrix7::<f64>::zeros()
416    }
417}
418
419impl Sim3 {
420    /// Compute V^{-1} matrix for Sim(3) logarithm.
421    ///
422    /// Compute the inverse of the V matrix for Sim(3) logarithm.
423    fn compute_v_inv(theta: &SO3Tangent, sigma: f64) -> Matrix3<f64> {
424        let v = Sim3Tangent::v_matrix(theta, sigma);
425        // SAFETY: V is non-singular for all valid Sim(3) elements (non-zero scale).
426        // The identity fallback is only reached in degenerate near-zero-scale cases,
427        // which are outside the valid domain of Sim(3).
428        v.try_inverse().unwrap_or(Matrix3::identity())
429    }
430}
431
432/// Sim(3) tangent space element.
433///
434/// Represented as [ρ(3), θ(3), σ(1)] where:
435/// - ρ: translational component
436/// - θ: rotational component (axis-angle)
437/// - σ: logarithmic scale
438#[derive(Clone, PartialEq)]
439pub struct Sim3Tangent {
440    /// Internal data: [ρ_x, ρ_y, ρ_z, θ_x, θ_y, θ_z, σ]
441    data: Vector7<f64>,
442}
443
444impl fmt::Display for Sim3Tangent {
445    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
446        let rho = self.rho();
447        let theta = self.theta();
448        let sigma = self.sigma();
449        write!(
450            f,
451            "sim3(rho: [{:.4}, {:.4}, {:.4}], theta: [{:.4}, {:.4}, {:.4}], sigma: {:.4})",
452            rho.x, rho.y, rho.z, theta.x, theta.y, theta.z, sigma
453        )
454    }
455}
456
457impl Sim3Tangent {
458    /// Create a new Sim(3)Tangent from components.
459    pub fn new(rho: Vector3<f64>, theta: Vector3<f64>, sigma: f64) -> Self {
460        let mut data = Vector7::zeros();
461        data.fixed_rows_mut::<3>(0).copy_from(&rho);
462        data.fixed_rows_mut::<3>(3).copy_from(&theta);
463        data[6] = sigma;
464        Sim3Tangent { data }
465    }
466
467    /// Get the ρ (translational) part.
468    pub fn rho(&self) -> Vector3<f64> {
469        self.data.fixed_rows::<3>(0).into_owned()
470    }
471
472    /// Get the θ (rotational) part.
473    pub fn theta(&self) -> Vector3<f64> {
474        self.data.fixed_rows::<3>(3).into_owned()
475    }
476
477    /// Get the σ (logarithmic scale) part.
478    pub fn sigma(&self) -> f64 {
479        self.data[6]
480    }
481
482    /// Create Sim3Tangent from individual scalar components.
483    pub fn from_components(
484        rho_x: f64,
485        rho_y: f64,
486        rho_z: f64,
487        theta_x: f64,
488        theta_y: f64,
489        theta_z: f64,
490        sigma: f64,
491    ) -> Self {
492        Sim3Tangent {
493            data: Vector7::from_column_slice(&[
494                rho_x, rho_y, rho_z, theta_x, theta_y, theta_z, sigma,
495            ]),
496        }
497    }
498
499    /// Compute the V matrix for Sim(3) exponential map.
500    ///
501    /// V = ∫₀¹ exp(σt) · exp(θt×) dt = A·I + B·[θ]× + C·[θ]ײ
502    ///
503    /// Based on Ethan Eade's "Lie Groups for Computer Vision" formulation.
504    fn v_matrix(theta: &SO3Tangent, sigma: f64) -> Matrix3<f64> {
505        let theta_norm_sq = theta.coeffs().norm_squared();
506
507        if theta_norm_sq < crate::SMALL_ANGLE_THRESHOLD
508            && sigma.abs() < crate::SMALL_ANGLE_THRESHOLD
509        {
510            return Matrix3::identity();
511        }
512
513        let theta_hat = theta.hat();
514
515        if theta_norm_sq < crate::SMALL_ANGLE_THRESHOLD {
516            // Pure scale, no rotation: V = (e^σ - 1)/σ · I
517            let a = (sigma.exp() - 1.0) / sigma;
518            return a * Matrix3::identity();
519        }
520
521        let theta_norm = theta_norm_sq.sqrt();
522        let sin_theta = theta_norm.sin();
523        let cos_theta = theta_norm.cos();
524
525        if sigma.abs() < crate::SMALL_ANGLE_THRESHOLD {
526            // Pure rotation, no scale: V = SO(3) left Jacobian
527            let a = 1.0;
528            let b = (1.0 - cos_theta) / theta_norm_sq;
529            let c = (theta_norm - sin_theta) / (theta_norm * theta_norm_sq);
530            return a * Matrix3::identity() + b * theta_hat + c * theta_hat * theta_hat;
531        }
532
533        // General case: both sigma and theta nonzero
534        let e_sigma = sigma.exp();
535        let alpha_sq = sigma * sigma + theta_norm_sq;
536
537        let a = (e_sigma - 1.0) / sigma;
538        let b = (e_sigma * (sigma * sin_theta - theta_norm * cos_theta) + theta_norm)
539            / (theta_norm * alpha_sq);
540        let cos_integral =
541            (e_sigma * (sigma * cos_theta + theta_norm * sin_theta) - sigma) / alpha_sq;
542        let c = (a - cos_integral) / theta_norm_sq;
543
544        a * Matrix3::identity() + b * theta_hat + c * theta_hat * theta_hat
545    }
546
547    /// Compute the Q matrix for Sim(3) Jacobians.
548    fn q_matrix(rho: Vector3<f64>, theta: Vector3<f64>, sigma: f64) -> Matrix3<f64> {
549        let rho_skew = SO3Tangent::new(rho).hat();
550        let theta_skew = SO3Tangent::new(theta).hat();
551        let theta_squared = theta.norm_squared();
552
553        if theta_squared < crate::SMALL_ANGLE_THRESHOLD && sigma.abs() < f64::EPSILON {
554            return 0.5 * rho_skew;
555        }
556
557        let a = 0.5;
558        let mut b = 1.0 / 6.0;
559        let mut c = -1.0 / 24.0;
560        let mut d = -1.0 / 60.0;
561
562        if theta_squared > crate::SMALL_ANGLE_THRESHOLD {
563            let theta_norm = theta_squared.sqrt();
564            let theta_norm_3 = theta_norm * theta_squared;
565            let theta_norm_4 = theta_squared * theta_squared;
566            let theta_norm_5 = theta_norm_3 * theta_squared;
567            let sin_theta = theta_norm.sin();
568            let cos_theta = theta_norm.cos();
569
570            b = (theta_norm - sin_theta) / theta_norm_3;
571            c = (1.0 - theta_squared / 2.0 - cos_theta) / theta_norm_4;
572            d = (c - 3.0) * (theta_norm - sin_theta - theta_norm_3 / 6.0) / theta_norm_5;
573        }
574
575        let rho_skew_theta_skew = rho_skew * theta_skew;
576        let theta_skew_rho_skew = theta_skew * rho_skew;
577        let theta_skew_rho_skew_theta_skew = theta_skew * rho_skew * theta_skew;
578        let rho_skew_theta_skew_sq2 = rho_skew * theta_skew * theta_skew;
579
580        let m1 = rho_skew;
581        let m2 = theta_skew_rho_skew + rho_skew_theta_skew + theta_skew_rho_skew_theta_skew;
582        let m3 = rho_skew_theta_skew_sq2
583            - rho_skew_theta_skew_sq2.transpose()
584            - 3.0 * theta_skew_rho_skew_theta_skew;
585        let m4 = theta_skew_rho_skew_theta_skew * theta_skew;
586
587        m1 * a + m2 * b - m3 * c - m4 * d
588    }
589}
590
591impl Tangent<Sim3> for Sim3Tangent {
592    const DIM: usize = 7;
593
594    /// Exponential map to Sim(3).
595    fn exp(&self, jacobian: Option<&mut <Sim3 as LieGroup>::JacobianMatrix>) -> Sim3 {
596        let rho = self.rho();
597        let theta = self.theta();
598        let sigma = self.sigma();
599
600        let theta_tangent = SO3Tangent::new(theta);
601        let rotation = theta_tangent.exp(None);
602        let v_matrix = Self::v_matrix(&theta_tangent, sigma);
603        let translation = v_matrix * rho;
604        let scale = sigma.exp();
605
606        if let Some(jac) = jacobian {
607            *jac = self.right_jacobian();
608        }
609
610        Sim3::from_components(translation, rotation, scale)
611    }
612
613    /// Right Jacobian for Sim(3).
614    fn right_jacobian(&self) -> <Sim3 as LieGroup>::JacobianMatrix {
615        let mut jac = Matrix7::zeros();
616        let rho = self.rho();
617        let theta = self.theta();
618        let sigma = self.sigma();
619
620        let theta_right_jac = SO3Tangent::new(-theta).right_jacobian();
621        let q_block = Self::q_matrix(-rho, -theta, -sigma);
622
623        // Block structure for Sim(3)
624        jac.fixed_view_mut::<3, 3>(0, 0).copy_from(&theta_right_jac);
625        jac.fixed_view_mut::<3, 3>(3, 3).copy_from(&theta_right_jac);
626        jac.fixed_view_mut::<3, 3>(0, 3).copy_from(&q_block);
627
628        // Scale part
629        jac[(6, 6)] = 1.0;
630
631        // Coupling between translation and scale
632        let v_deriv = Self::v_matrix(&SO3Tangent::new(-theta), -sigma);
633        jac.fixed_view_mut::<3, 1>(0, 6)
634            .copy_from(&(v_deriv * (-rho)));
635
636        jac
637    }
638
639    /// Left Jacobian for Sim(3).
640    fn left_jacobian(&self) -> <Sim3 as LieGroup>::JacobianMatrix {
641        let mut jac = Matrix7::zeros();
642        let rho = self.rho();
643        let theta = self.theta();
644        let sigma = self.sigma();
645
646        let theta_left_jac = SO3Tangent::new(theta).left_jacobian();
647        let q_block = Self::q_matrix(rho, theta, sigma);
648
649        jac.fixed_view_mut::<3, 3>(0, 0).copy_from(&theta_left_jac);
650        jac.fixed_view_mut::<3, 3>(3, 3).copy_from(&theta_left_jac);
651        jac.fixed_view_mut::<3, 3>(0, 3).copy_from(&q_block);
652
653        jac[(6, 6)] = 1.0;
654
655        let v_deriv = Self::v_matrix(&SO3Tangent::new(theta), sigma);
656        jac.fixed_view_mut::<3, 1>(0, 6).copy_from(&(v_deriv * rho));
657
658        jac
659    }
660
661    /// Inverse of right Jacobian.
662    fn right_jacobian_inv(&self) -> <Sim3 as LieGroup>::JacobianMatrix {
663        // SAFETY: The right Jacobian is non-singular for valid Sim(3) tangent elements.
664        // Identity fallback only occurs at degenerate inputs outside the valid domain.
665        self.right_jacobian()
666            .try_inverse()
667            .unwrap_or(Matrix7::identity())
668    }
669
670    /// Inverse of left Jacobian.
671    fn left_jacobian_inv(&self) -> <Sim3 as LieGroup>::JacobianMatrix {
672        // SAFETY: The left Jacobian is non-singular for valid Sim(3) tangent elements.
673        // Identity fallback only occurs at degenerate inputs outside the valid domain.
674        self.left_jacobian()
675            .try_inverse()
676            .unwrap_or(Matrix7::identity())
677    }
678
679    /// Hat operator: maps tangent vector to Lie algebra matrix (4x4).
680    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        // Top-left 3x3: [θ]× + σ*I
686        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        // Top-right 3x1: ρ
694        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        // Block structure for Sim(3):
764        // [θ× + σ*I   ρ×   ρ]
765        // [   0       θ×   0]
766        // [   0       0    0]
767
768        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                // Translation generators (rho)
805                generator[(i, 3)] = 1.0;
806            }
807            3..=5 => {
808                // Rotation generators (theta)
809                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                // Scale generator (sigma)
828                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]
855    fn test_sim3_new() {
856        let translation = Vector3::new(1.0, 2.0, 3.0);
857        let rotation = UnitQuaternion::from_euler_angles(0.1, 0.2, 0.3);
858        let scale = 1.5;
859
860        let sim3 = Sim3::new(translation, rotation, scale);
861        assert!(sim3.is_valid(TOLERANCE));
862        assert!((sim3.translation() - translation).norm() < TOLERANCE);
863        assert!((sim3.scale() - scale).abs() < TOLERANCE);
864    }
865
866    #[test]
867    #[should_panic(expected = "Scale must be positive")]
868    fn test_sim3_new_negative_scale() {
869        let translation = Vector3::new(1.0, 2.0, 3.0);
870        let rotation = UnitQuaternion::identity();
871        let _sim3 = Sim3::new(translation, rotation, -1.0);
872    }
873
874    #[test]
875    fn test_sim3_random() {
876        let sim3 = Sim3::random();
877        assert!(sim3.is_valid(TOLERANCE));
878        assert!(sim3.scale() > 0.0);
879    }
880
881    #[test]
882    fn test_sim3_inverse() {
883        let sim3 = Sim3::random();
884        let sim3_inv = sim3.inverse(None);
885
886        let composed = sim3.compose(&sim3_inv, None, None);
887        let identity = Sim3::identity();
888
889        assert!(composed.is_approx(&identity, TOLERANCE));
890    }
891
892    #[test]
893    fn test_sim3_compose() {
894        let sim3_1 = Sim3::random();
895        let sim3_2 = Sim3::random();
896
897        let composed = sim3_1.compose(&sim3_2, None, None);
898        assert!(composed.is_valid(TOLERANCE));
899
900        let identity = Sim3::identity();
901        let composed_with_identity = sim3_1.compose(&identity, None, None);
902        assert!(composed_with_identity.is_approx(&sim3_1, TOLERANCE));
903    }
904
905    #[test]
906    fn test_sim3_exp_log() {
907        let tangent = Sim3Tangent::new(
908            Vector3::new(0.1, 0.2, 0.3),
909            Vector3::new(0.01, 0.02, 0.03),
910            0.1,
911        );
912
913        let sim3 = tangent.exp(None);
914        let recovered_tangent = sim3.log(None);
915
916        assert!((tangent.data - recovered_tangent.data).norm() < TOLERANCE);
917    }
918
919    #[test]
920    fn test_sim3_exp_zero() {
921        let zero_tangent = Sim3Tangent::zero();
922        let sim3 = zero_tangent.exp(None);
923        let identity = Sim3::identity();
924
925        assert!(sim3.is_approx(&identity, TOLERANCE));
926    }
927
928    #[test]
929    fn test_sim3_log_identity() {
930        let identity = Sim3::identity();
931        let tangent = identity.log(None);
932
933        assert!(tangent.data.norm() < TOLERANCE);
934    }
935
936    #[test]
937    fn test_sim3_adjoint() {
938        let sim3 = Sim3::random();
939        let adj = sim3.adjoint();
940
941        assert_eq!(adj.nrows(), 7);
942        assert_eq!(adj.ncols(), 7);
943    }
944
945    #[test]
946    fn test_sim3_act() {
947        let sim3 = Sim3::random();
948        let point = Vector3::new(1.0, 2.0, 3.0);
949
950        let _transformed_point = sim3.act(&point, None, None);
951
952        let identity = Sim3::identity();
953        let identity_transformed = identity.act(&point, None, None);
954
955        assert!((identity_transformed - point).norm() < TOLERANCE);
956    }
957
958    #[test]
959    fn test_sim3_between() {
960        let sim3_a = Sim3::random();
961        let sim3_b = sim3_a.clone();
962        let sim3_between_identity = sim3_a.between(&sim3_b, None, None);
963        assert!(sim3_between_identity.is_approx(&Sim3::identity(), TOLERANCE));
964
965        let sim3_c = Sim3::random();
966        let sim3_between = sim3_a.between(&sim3_c, None, None);
967        let expected = sim3_a.inverse(None).compose(&sim3_c, None, None);
968        assert!(sim3_between.is_approx(&expected, TOLERANCE));
969    }
970
971    #[test]
972    fn test_sim3_tangent_zero() {
973        let zero = Sim3Tangent::zero();
974        assert!(zero.data.norm() < TOLERANCE);
975
976        let tangent = Sim3Tangent::new(Vector3::zeros(), Vector3::zeros(), 0.0);
977        assert!(tangent.is_zero(TOLERANCE));
978    }
979
980    #[test]
981    fn test_sim3_manifold_properties() {
982        assert_eq!(Sim3::DIM, 3);
983        assert_eq!(Sim3::DOF, 7);
984        assert_eq!(Sim3::REP_SIZE, 8);
985    }
986
987    #[test]
988    fn test_sim3_consistency() {
989        let sim3_1 = Sim3::random();
990        let sim3_2 = Sim3::random();
991        let sim3_3 = Sim3::random();
992
993        // Test associativity
994        let left_assoc = sim3_1
995            .compose(&sim3_2, None, None)
996            .compose(&sim3_3, None, None);
997        let right_assoc = sim3_1.compose(&sim3_2.compose(&sim3_3, None, None), None, None);
998
999        assert!(left_assoc.is_approx(&right_assoc, 1e-10));
1000    }
1001
1002    #[test]
1003    fn test_sim3_scale_composition() {
1004        let sim3_1 = Sim3::new(Vector3::zeros(), UnitQuaternion::identity(), 2.0);
1005        let sim3_2 = Sim3::new(Vector3::zeros(), UnitQuaternion::identity(), 3.0);
1006
1007        let composed = sim3_1.compose(&sim3_2, None, None);
1008        assert!((composed.scale() - 6.0).abs() < TOLERANCE);
1009    }
1010
1011    #[test]
1012    fn test_sim3_tangent_small_adj() {
1013        let tangent = Sim3Tangent::new(
1014            Vector3::new(0.1, 0.2, 0.3),
1015            Vector3::new(0.4, 0.5, 0.6),
1016            0.1,
1017        );
1018        let small_adj = tangent.small_adj();
1019
1020        assert_eq!(small_adj.nrows(), 7);
1021        assert_eq!(small_adj.ncols(), 7);
1022    }
1023
1024    #[test]
1025    fn test_sim3_tangent_lie_bracket() {
1026        let tangent_a = Sim3Tangent::new(
1027            Vector3::new(0.1, 0.0, 0.0),
1028            Vector3::new(0.0, 0.2, 0.0),
1029            0.1,
1030        );
1031        let tangent_b = Sim3Tangent::new(
1032            Vector3::new(0.0, 0.3, 0.0),
1033            Vector3::new(0.0, 0.0, 0.4),
1034            0.2,
1035        );
1036
1037        let bracket_ab = tangent_a.lie_bracket(&tangent_b);
1038        let bracket_ba = tangent_b.lie_bracket(&tangent_a);
1039
1040        // Anti-symmetry
1041        assert!((bracket_ab.data + bracket_ba.data).norm() < 1e-10);
1042
1043        // [a,a] = 0
1044        let bracket_aa = tangent_a.lie_bracket(&tangent_a);
1045        assert!(bracket_aa.is_zero(1e-10));
1046    }
1047
1048    #[test]
1049    fn test_sim3_tangent_is_approx() {
1050        let tangent_1 = Sim3Tangent::new(
1051            Vector3::new(0.1, 0.2, 0.3),
1052            Vector3::new(0.4, 0.5, 0.6),
1053            0.1,
1054        );
1055        let tangent_2 = Sim3Tangent::new(
1056            Vector3::new(0.1 + 1e-12, 0.2, 0.3),
1057            Vector3::new(0.4, 0.5, 0.6),
1058            0.1,
1059        );
1060        let tangent_3 = Sim3Tangent::new(
1061            Vector3::new(1.0, 2.0, 3.0),
1062            Vector3::new(4.0, 5.0, 6.0),
1063            1.0,
1064        );
1065
1066        assert!(tangent_1.is_approx(&tangent_1, 1e-10));
1067        assert!(tangent_1.is_approx(&tangent_2, 1e-10));
1068        assert!(!tangent_1.is_approx(&tangent_3, 1e-10));
1069    }
1070
1071    #[test]
1072    fn test_sim3_generators() {
1073        let tangent = Sim3Tangent::new(
1074            Vector3::new(1.0, 1.0, 1.0),
1075            Vector3::new(1.0, 1.0, 1.0),
1076            1.0,
1077        );
1078
1079        for i in 0..7 {
1080            let generator = tangent.generator(i);
1081            assert_eq!(generator.nrows(), 4);
1082            assert_eq!(generator.ncols(), 4);
1083
1084            // Bottom row should be zeros
1085            assert_eq!(generator[(3, 0)], 0.0);
1086            assert_eq!(generator[(3, 1)], 0.0);
1087            assert_eq!(generator[(3, 2)], 0.0);
1088            assert_eq!(generator[(3, 3)], 0.0);
1089        }
1090    }
1091
1092    #[test]
1093    #[should_panic]
1094    fn test_sim3_generator_invalid_index() {
1095        let tangent = Sim3Tangent::new(
1096            Vector3::new(1.0, 1.0, 1.0),
1097            Vector3::new(1.0, 1.0, 1.0),
1098            1.0,
1099        );
1100        let _generator = tangent.generator(7);
1101    }
1102
1103    #[test]
1104    fn test_sim3_vee() {
1105        let sim3 = Sim3::random();
1106        let tangent_log = sim3.log(None);
1107        let tangent_vee = sim3.vee();
1108
1109        assert!((tangent_log.data - tangent_vee.data).norm() < 1e-10);
1110    }
1111
1112    #[test]
1113    fn test_sim3_is_approx() {
1114        let sim3_1 = Sim3::random();
1115        let sim3_2 = sim3_1.clone();
1116
1117        assert!(sim3_1.is_approx(&sim3_1, 1e-10));
1118        assert!(sim3_1.is_approx(&sim3_2, 1e-10));
1119
1120        let small_tangent = Sim3Tangent::new(
1121            Vector3::new(1e-12, 1e-12, 1e-12),
1122            Vector3::new(1e-12, 1e-12, 1e-12),
1123            1e-12,
1124        );
1125        let sim3_perturbed = sim3_1.right_plus(&small_tangent, None, None);
1126        assert!(sim3_1.is_approx(&sim3_perturbed, 1e-10));
1127    }
1128
1129    #[test]
1130    fn test_sim3_small_angle_approximations() {
1131        let small_tangent = Sim3Tangent::new(
1132            Vector3::new(1e-8, 2e-8, 3e-8),
1133            Vector3::new(1e-9, 2e-9, 3e-9),
1134            1e-8,
1135        );
1136
1137        let sim3 = small_tangent.exp(None);
1138        let recovered = sim3.log(None);
1139
1140        assert!((small_tangent.data - recovered.data).norm() < TOLERANCE);
1141    }
1142
1143    #[test]
1144    fn test_sim3_accessors() {
1145        let translation = Vector3::new(1.0, 2.0, 3.0);
1146        let rotation = UnitQuaternion::identity();
1147        let scale = 1.5;
1148
1149        let sim3 = Sim3::new(translation, rotation, scale);
1150
1151        assert_eq!(sim3.x(), 1.0);
1152        assert_eq!(sim3.y(), 2.0);
1153        assert_eq!(sim3.z(), 3.0);
1154        assert_eq!(sim3.scale(), 1.5);
1155    }
1156
1157    #[test]
1158    fn test_sim3_matrix() {
1159        let translation = Vector3::new(1.0, 2.0, 3.0);
1160        let rotation = UnitQuaternion::identity();
1161        let scale = 2.0;
1162
1163        let sim3 = Sim3::new(translation, rotation, scale);
1164        let mat = sim3.matrix();
1165
1166        // Check that top-left 3x3 is scaled rotation (2*I in this case)
1167        for i in 0..3 {
1168            for j in 0..3 {
1169                if i == j {
1170                    assert!((mat[(i, j)] - 2.0).abs() < TOLERANCE);
1171                } else {
1172                    assert!(mat[(i, j)].abs() < TOLERANCE);
1173                }
1174            }
1175        }
1176
1177        // Check translation
1178        assert!((mat[(0, 3)] - 1.0).abs() < TOLERANCE);
1179        assert!((mat[(1, 3)] - 2.0).abs() < TOLERANCE);
1180        assert!((mat[(2, 3)] - 3.0).abs() < TOLERANCE);
1181
1182        // Check bottom row
1183        assert!(mat[(3, 0)].abs() < TOLERANCE);
1184        assert!(mat[(3, 1)].abs() < TOLERANCE);
1185        assert!(mat[(3, 2)].abs() < TOLERANCE);
1186        assert!((mat[(3, 3)] - 1.0).abs() < TOLERANCE);
1187    }
1188
1189    #[test]
1190    fn test_sim3_scale_action() {
1191        let scale = 2.0;
1192        let sim3 = Sim3::new(Vector3::zeros(), UnitQuaternion::identity(), scale);
1193        let point = Vector3::new(1.0, 2.0, 3.0);
1194
1195        let transformed = sim3.act(&point, None, None);
1196
1197        assert!((transformed.x - 2.0).abs() < TOLERANCE);
1198        assert!((transformed.y - 4.0).abs() < TOLERANCE);
1199        assert!((transformed.z - 6.0).abs() < TOLERANCE);
1200    }
1201
1202    #[test]
1203    fn test_sim3_tangent_basic() {
1204        let rho = Vector3::new(0.1, 0.2, 0.3);
1205        let theta = Vector3::new(0.4, 0.5, 0.6);
1206        let sigma = 0.1;
1207        let tangent = Sim3Tangent::new(rho, theta, sigma);
1208        assert!((tangent.rho() - rho).norm() < TOLERANCE);
1209        assert!((tangent.theta() - theta).norm() < TOLERANCE);
1210        assert!((tangent.sigma() - sigma).abs() < TOLERANCE);
1211    }
1212
1213    #[test]
1214    fn test_sim3_tangent_from_components() {
1215        let t1 = Sim3Tangent::new(
1216            Vector3::new(1.0, 2.0, 3.0),
1217            Vector3::new(0.4, 0.5, 0.6),
1218            0.1,
1219        );
1220        let t2 = Sim3Tangent::from_components(1.0, 2.0, 3.0, 0.4, 0.5, 0.6, 0.1);
1221        assert!(t1.is_approx(&t2, TOLERANCE));
1222    }
1223
1224    #[test]
1225    fn test_sim3_normalize() {
1226        let mut sim3 = Sim3::random();
1227        sim3.normalize();
1228        assert!(sim3.is_valid(TOLERANCE));
1229    }
1230
1231    #[test]
1232    fn test_sim3_coeffs() {
1233        let translation = Vector3::new(1.0, 2.0, 3.0);
1234        let rotation = UnitQuaternion::identity();
1235        let scale = 1.5;
1236        let sim3 = Sim3::new(translation, rotation, scale);
1237        let c = sim3.coeffs();
1238        assert!((c[0] - 1.0).abs() < TOLERANCE);
1239        assert!((c[1] - 2.0).abs() < TOLERANCE);
1240        assert!((c[2] - 3.0).abs() < TOLERANCE);
1241        assert!((c[3] - 1.0).abs() < TOLERANCE); // qw
1242        assert!((c[7] - 1.5).abs() < TOLERANCE); // scale
1243    }
1244
1245    #[test]
1246    fn test_sim3_manif_like_operations() {
1247        let a = Sim3::random();
1248        let b = Sim3::random();
1249
1250        let a_to_b = a.between(&b, None, None);
1251        let recovered_b = a.compose(&a_to_b, None, None);
1252        assert!(recovered_b.is_approx(&b, TOLERANCE));
1253
1254        let tangent = Sim3Tangent::new(
1255            Vector3::new(0.01, 0.02, 0.03),
1256            Vector3::new(0.001, 0.002, 0.003),
1257            0.01,
1258        );
1259        let perturbed = a.plus(&tangent, None, None);
1260        let recovered_tangent = perturbed.minus(&a, None, None);
1261        assert!((tangent.data - recovered_tangent.data).norm() < 1e-6);
1262    }
1263
1264    #[test]
1265    fn test_sim3_right_jacobian_inverse_identity() {
1266        let tangent = Sim3Tangent::new(
1267            Vector3::new(0.1, 0.2, 0.3),
1268            Vector3::new(0.01, 0.02, 0.03),
1269            0.1,
1270        );
1271        let jr = tangent.right_jacobian();
1272        let jr_inv = tangent.right_jacobian_inv();
1273        let product = jr * jr_inv;
1274        let identity = Matrix7::<f64>::identity();
1275        assert!((product - identity).norm() < 0.01);
1276    }
1277
1278    #[test]
1279    fn test_sim3_left_jacobian_inverse_identity() {
1280        let tangent = Sim3Tangent::new(
1281            Vector3::new(0.1, 0.2, 0.3),
1282            Vector3::new(0.01, 0.02, 0.03),
1283            0.1,
1284        );
1285        let jl = tangent.left_jacobian();
1286        let jl_inv = tangent.left_jacobian_inv();
1287        let product = jl * jl_inv;
1288        let identity = Matrix7::<f64>::identity();
1289        assert!((product - identity).norm() < 0.01);
1290    }
1291
1292    #[test]
1293    fn test_sim3_jacobi_identity() {
1294        let a = Sim3Tangent::random();
1295        let b = Sim3Tangent::random();
1296        let c = Sim3Tangent::random();
1297
1298        let term1 = a.lie_bracket(&b.lie_bracket(&c));
1299        let term2 = b.lie_bracket(&c.lie_bracket(&a));
1300        let term3 = c.lie_bracket(&a.lie_bracket(&b));
1301
1302        assert!((term1.data + term2.data + term3.data).norm() < 1e-8);
1303    }
1304
1305    #[test]
1306    fn test_sim3_hat_matrix_structure() {
1307        let tangent = Sim3Tangent::new(
1308            Vector3::new(1.0, 2.0, 3.0),
1309            Vector3::new(0.1, 0.2, 0.3),
1310            0.5,
1311        );
1312        let hat = tangent.hat();
1313
1314        for j in 0..4 {
1315            assert_eq!(hat[(3, j)], 0.0);
1316        }
1317
1318        assert!((hat[(0, 3)] - 1.0).abs() < TOLERANCE);
1319        assert!((hat[(1, 3)] - 2.0).abs() < TOLERANCE);
1320        assert!((hat[(2, 3)] - 3.0).abs() < TOLERANCE);
1321
1322        // Diagonal includes sigma
1323        assert!((hat[(0, 0)] - 0.5).abs() < 0.5); // theta_skew + sigma
1324    }
1325
1326    #[test]
1327    fn test_sim3_param_slice_round_trip() {
1328        let sim3 = Sim3::random();
1329        let recovered = Sim3::from_param_slice(sim3.as_param_slice());
1330        assert!(sim3.is_approx(&recovered, TOLERANCE));
1331    }
1332
1333    // --- Additional coverage tests ---
1334
1335    #[test]
1336    fn test_sim3_display() {
1337        let sim3 = Sim3::identity();
1338        let s = format!("{}", sim3);
1339        assert!(s.contains("Sim3"));
1340
1341        let tangent = Sim3Tangent::new(
1342            Vector3::new(1.0, 2.0, 3.0),
1343            Vector3::new(0.1, 0.2, 0.3),
1344            0.5,
1345        );
1346        let ts = format!("{}", tangent);
1347        assert!(ts.contains("sim3"));
1348    }
1349
1350    #[test]
1351    fn test_sim3_jacobian_identity_static() {
1352        let jac = Sim3::jacobian_identity();
1353        assert!((jac - Matrix7::identity()).norm() < TOLERANCE);
1354    }
1355
1356    #[test]
1357    fn test_sim3_rotation_so3() {
1358        let sim3 = Sim3::random();
1359        let so3 = sim3.rotation_so3();
1360        assert!(so3.is_valid(TOLERANCE));
1361    }
1362
1363    #[test]
1364    fn test_sim3_inverse_with_jacobian() {
1365        let sim3 = Sim3::random();
1366        let mut jac = Matrix7::zeros();
1367        let inv = sim3.inverse(Some(&mut jac));
1368        let expected_jac = -sim3.adjoint();
1369        assert!((jac - expected_jac).norm() < TOLERANCE);
1370        let composed = sim3.compose(&inv, None, None);
1371        assert!(composed.is_approx(&Sim3::identity(), TOLERANCE));
1372    }
1373
1374    #[test]
1375    fn test_sim3_compose_with_jacobians() {
1376        let a = Sim3::random();
1377        let b = Sim3::random();
1378        let mut jac_a = Matrix7::zeros();
1379        let mut jac_b = Matrix7::zeros();
1380        let _composed = a.compose(&b, Some(&mut jac_a), Some(&mut jac_b));
1381        assert!(jac_a.norm() > 0.0);
1382        assert!(jac_b.norm() > 0.0);
1383    }
1384
1385    #[test]
1386    fn test_sim3_act_with_jacobians() {
1387        let sim3 = Sim3::random();
1388        let point = Vector3::new(1.0, 2.0, 3.0);
1389        let mut jac_self = Matrix7::zeros();
1390        let mut jac_point = Matrix3::<f64>::zeros();
1391        let result = sim3.act(&point, Some(&mut jac_self), Some(&mut jac_point));
1392        assert!(result.norm() > 0.0);
1393        assert!(jac_self.norm() > 0.0);
1394        assert!(jac_point.norm() > 0.0);
1395    }
1396
1397    #[test]
1398    fn test_sim3_liegroup_jacobian_identity() {
1399        let jac = <Sim3 as LieGroup>::jacobian_identity();
1400        assert!((jac - Matrix7::identity()).norm() < TOLERANCE);
1401
1402        let zero = <Sim3 as LieGroup>::zero_jacobian();
1403        assert!(zero.norm() < TOLERANCE);
1404    }
1405
1406    #[test]
1407    fn test_sim3_tangent_slice_round_trip() {
1408        let tangent = Sim3Tangent::new(
1409            Vector3::new(1.0, 2.0, 3.0),
1410            Vector3::new(0.1, 0.2, 0.3),
1411            0.5,
1412        );
1413        let recovered = Sim3Tangent::from_slice(tangent.as_slice());
1414        assert!(tangent.is_approx(&recovered, TOLERANCE));
1415    }
1416
1417    #[test]
1418    fn test_sim3_exp_with_jacobian() {
1419        let tangent = Sim3Tangent::new(
1420            Vector3::new(0.1, 0.2, 0.3),
1421            Vector3::new(0.01, 0.02, 0.03),
1422            0.1,
1423        );
1424        let mut jac = Matrix7::zeros();
1425        let _result = tangent.exp(Some(&mut jac));
1426        assert!(jac.norm() > 0.0);
1427    }
1428
1429    #[test]
1430    fn test_sim3_exp_log_stress() {
1431        for _ in 0..100 {
1432            let tangent = Sim3Tangent::random();
1433            let sim3 = tangent.exp(None);
1434            let recovered = sim3.log(None);
1435            assert!(
1436                tangent.is_approx(&recovered, 1e-6),
1437                "exp/log round-trip failed: error = {}",
1438                (tangent.data - recovered.data).norm()
1439            );
1440        }
1441    }
1442
1443    #[test]
1444    fn test_sim3_right_plus_minus_round_trip() {
1445        let a = Sim3::random();
1446        let b = Sim3::random();
1447        let diff = a.right_minus(&b, None, None);
1448        let recovered = b.right_plus(&diff, None, None);
1449        assert!(a.is_approx(&recovered, 1e-6));
1450    }
1451
1452    #[test]
1453    fn test_sim3_left_plus_minus_round_trip() {
1454        let a = Sim3::random();
1455        let b = Sim3::random();
1456        let diff = a.left_minus(&b, None, None);
1457        let recovered = b.left_plus(&diff, None, None);
1458        assert!(a.is_approx(&recovered, 1e-6));
1459    }
1460
1461    #[test]
1462    fn test_sim3_compose_associativity() {
1463        let a = Sim3::random();
1464        let b = Sim3::random();
1465        let c = Sim3::random();
1466        let ab_c = a.compose(&b, None, None).compose(&c, None, None);
1467        let a_bc = a.compose(&b.compose(&c, None, None), None, None);
1468        assert!(ab_c.is_approx(&a_bc, 1e-6));
1469    }
1470
1471    #[test]
1472    fn test_sim3_inverse_twice() {
1473        let g = Sim3::random();
1474        let g_inv_inv = g.inverse(None).inverse(None);
1475        assert!(g.is_approx(&g_inv_inv, 1e-6));
1476    }
1477
1478    #[test]
1479    fn test_sim3_pure_scale() {
1480        // exp/log with only sigma nonzero (theta=0)
1481        let tangent = Sim3Tangent::new(Vector3::zeros(), Vector3::zeros(), 0.5);
1482        let sim3 = tangent.exp(None);
1483        let recovered = sim3.log(None);
1484        assert!(tangent.is_approx(&recovered, TOLERANCE));
1485        assert!((sim3.scale() - 0.5_f64.exp()).abs() < TOLERANCE);
1486    }
1487
1488    #[test]
1489    fn test_sim3_pure_rotation() {
1490        // exp/log with only theta nonzero (sigma=0)
1491        let tangent = Sim3Tangent::new(
1492            Vector3::new(0.1, 0.2, 0.3),
1493            Vector3::new(0.05, 0.1, 0.15),
1494            0.0,
1495        );
1496        let sim3 = tangent.exp(None);
1497        let recovered = sim3.log(None);
1498        assert!(tangent.is_approx(&recovered, 1e-6));
1499        assert!((sim3.scale() - 1.0).abs() < TOLERANCE);
1500    }
1501
1502    #[test]
1503    fn test_sim3_scale_compose() {
1504        let a = Sim3::from_components(Vector3::zeros(), SO3::identity(), 2.0);
1505        let b = Sim3::from_components(Vector3::zeros(), SO3::identity(), 3.0);
1506        let ab = a.compose(&b, None, None);
1507        assert!((ab.scale() - 6.0).abs() < TOLERANCE);
1508    }
1509
1510    #[test]
1511    fn test_sim3_tangent_normalize_zero_theta() {
1512        let tangent = Sim3Tangent::new(Vector3::new(1.0, 2.0, 3.0), Vector3::zeros(), 0.5);
1513        let normalized = tangent.normalized();
1514        assert!(normalized.theta().norm() < TOLERANCE);
1515    }
1516
1517    #[test]
1518    fn test_sim3_tangent_generator_all() {
1519        let tangent = Sim3Tangent::zero();
1520        for i in 0..7 {
1521            let g = tangent.generator(i);
1522            assert!(g.norm() > 0.0, "Generator {} should be non-zero", i);
1523        }
1524    }
1525
1526    #[test]
1527    fn test_sim3_v_matrix_sigma_zero_matches_so3_jl() {
1528        // When sigma=0, V-matrix should equal SO3 left Jacobian
1529        let theta = SO3Tangent::new(Vector3::new(0.3, 0.4, 0.5));
1530        let v = Sim3Tangent::v_matrix(&theta, 0.0);
1531        let jl = theta.left_jacobian();
1532        assert!(
1533            (v - jl).norm() < 1e-10,
1534            "V(θ,0) should equal Jl(θ), error = {}",
1535            (v - jl).norm()
1536        );
1537    }
1538
1539    #[test]
1540    fn test_sim3_v_matrix_theta_zero() {
1541        // When theta=0, V-matrix should be (e^σ - 1)/σ * I
1542        let theta = SO3Tangent::new(Vector3::zeros());
1543        let sigma = 0.5;
1544        let v = Sim3Tangent::v_matrix(&theta, sigma);
1545        let expected = (sigma.exp() - 1.0) / sigma * Matrix3::<f64>::identity();
1546        assert!(
1547            (v - expected).norm() < 1e-10,
1548            "V(0,σ) should equal (e^σ-1)/σ * I, error = {}",
1549            (v - expected).norm()
1550        );
1551    }
1552
1553    #[test]
1554    fn sim3_param_slice_round_trip() {
1555        let g = Sim3::random();
1556        let recovered = Sim3::from_param_slice(g.as_param_slice());
1557        assert!(g.is_approx(&recovered, 1e-14));
1558    }
1559
1560    #[test]
1561    fn sim3_tangent_slice_round_trip() {
1562        let t = Sim3Tangent::random();
1563        let recovered = Sim3Tangent::from_slice(t.as_slice());
1564        assert!(t.is_approx(&recovered, 1e-14));
1565    }
1566}