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
68/// Tikhonov-regularized inverse for numerically singular Sim(3) blocks.
69///
70/// A σ ≈ 0 tangent element makes the V/Jacobian blocks singular mid-solve.
71/// Returning identity (the old fallback) silently corrupts the step to a
72/// generic identity map; the regularized inverse keeps the block's structure
73/// so the step degrades gracefully, and the event is surfaced.
74fn regularized_inverse_3(m: &Matrix3<f64>, what: &str) -> Matrix3<f64> {
75    if let Some(inv) = m.try_inverse() {
76        return inv;
77    }
78    let eps = 1e-9 * (m.trace().abs() + 1.0) / 3.0;
79    tracing::warn!(
80        "Sim(3) {what} is numerically singular; using Tikhonov-regularized inverse (eps = {eps:.3e})"
81    );
82    (*m + Matrix3::identity() * eps)
83        .try_inverse()
84        .unwrap_or_else(|| {
85            tracing::error!("Sim(3) {what}: regularized inverse also failed; using identity");
86            Matrix3::identity()
87        })
88}
89
90fn regularized_inverse_7(m: &Matrix7<f64>, what: &str) -> Matrix7<f64> {
91    if let Some(inv) = m.try_inverse() {
92        return inv;
93    }
94    let eps = 1e-9 * (m.trace().abs() + 1.0) / 7.0;
95    tracing::warn!(
96        "Sim(3) {what} is numerically singular; using Tikhonov-regularized inverse (eps = {eps:.3e})"
97    );
98    (*m + Matrix7::identity() * eps)
99        .try_inverse()
100        .unwrap_or_else(|| {
101            tracing::error!("Sim(3) {what}: regularized inverse also failed; using identity");
102            Matrix7::identity()
103        })
104}
105
106impl Sim3 {
107    /// Space dimension - dimension of the ambient space
108    pub const DIM: usize = 3;
109
110    /// Degrees of freedom - dimension of the tangent space
111    pub const DOF: usize = 7;
112
113    /// Representation size - size of the underlying data representation
114    pub const REP_SIZE: usize = 8;
115
116    #[inline]
117    fn translation_impl(&self) -> Vector3<f64> {
118        Vector3::new(self.params[0], self.params[1], self.params[2])
119    }
120
121    #[inline]
122    fn rotation_impl(&self) -> SO3 {
123        SO3::from_quaternion_wxyz(
124            self.params[3],
125            self.params[4],
126            self.params[5],
127            self.params[6],
128        )
129    }
130
131    #[inline]
132    fn scale_impl(&self) -> f64 {
133        self.params[7]
134    }
135
136    #[inline]
137    fn from_parts(t: Vector3<f64>, r: &SO3, scale: f64) -> Self {
138        let q = r.params();
139        Sim3 {
140            params: SVector::<f64, 8>::from([t.x, t.y, t.z, q[0], q[1], q[2], q[3], scale]),
141        }
142    }
143
144    /// Get the identity element of the group.
145    pub fn identity() -> Self {
146        Sim3 {
147            params: SVector::<f64, 8>::from([0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0]),
148        }
149    }
150
151    /// Get the identity matrix for Jacobians.
152    pub fn jacobian_identity() -> Matrix7<f64> {
153        Matrix7::<f64>::identity()
154    }
155
156    /// Create a new Sim(3) element from translation, rotation, and scale.
157    ///
158    /// # Arguments
159    /// * `translation` - Translation vector [x, y, z]
160    /// * `rotation` - Unit quaternion representing rotation
161    /// * `scale` - Scale factor (must be positive)
162    pub fn new(translation: Vector3<f64>, rotation: UnitQuaternion<f64>, scale: f64) -> Self {
163        assert!(scale > 0.0, "Scale must be positive");
164        Sim3::from_parts(translation, &SO3::new(rotation), scale)
165    }
166
167    /// Create Sim(3) from components.
168    pub fn from_components(translation: Vector3<f64>, rotation: SO3, scale: f64) -> Self {
169        assert!(scale > 0.0, "Scale must be positive");
170        Sim3::from_parts(translation, &rotation, scale)
171    }
172
173    /// Get the translation part as a Vector3.
174    pub fn translation(&self) -> Vector3<f64> {
175        self.translation_impl()
176    }
177
178    /// Get the scale factor.
179    pub fn scale(&self) -> f64 {
180        self.scale_impl()
181    }
182
183    /// Get the rotation part as SO3.
184    pub fn rotation_so3(&self) -> SO3 {
185        self.rotation_impl()
186    }
187
188    /// Get the rotation part as a UnitQuaternion.
189    pub fn rotation_quaternion(&self) -> UnitQuaternion<f64> {
190        self.rotation_impl().quaternion()
191    }
192
193    /// Get the rotation matrix (3x3).
194    pub fn rotation_matrix(&self) -> Matrix3<f64> {
195        self.rotation_impl().rotation_matrix()
196    }
197
198    /// Get the 4x4 homogeneous transformation matrix.
199    pub fn matrix(&self) -> Matrix4<f64> {
200        let mut mat = Matrix4::identity();
201        let rot_mat = self.rotation_matrix();
202        let scale = self.scale_impl();
203
204        for i in 0..3 {
205            for j in 0..3 {
206                mat[(i, j)] = scale * rot_mat[(i, j)];
207            }
208        }
209
210        mat[(0, 3)] = self.params[0];
211        mat[(1, 3)] = self.params[1];
212        mat[(2, 3)] = self.params[2];
213
214        mat
215    }
216
217    /// Get the x component of translation.
218    pub fn x(&self) -> f64 {
219        self.params[0]
220    }
221
222    /// Get the y component of translation.
223    pub fn y(&self) -> f64 {
224        self.params[1]
225    }
226
227    /// Get the z component of translation.
228    pub fn z(&self) -> f64 {
229        self.params[2]
230    }
231
232    /// Get the parameter vector [tx, ty, tz, qw, qx, qy, qz, s].
233    pub fn coeffs(&self) -> [f64; 8] {
234        [
235            self.params[0],
236            self.params[1],
237            self.params[2],
238            self.params[3],
239            self.params[4],
240            self.params[5],
241            self.params[6],
242            self.params[7],
243        ]
244    }
245}
246
247impl LieGroup for Sim3 {
248    const NAME: &'static str = "Sim3";
249
250    type TangentVector = Sim3Tangent;
251    type JacobianMatrix = Matrix7<f64>;
252    type LieAlgebra = Matrix4<f64>;
253
254    /// Get the inverse.
255    ///
256    /// For Sim(3): g^{-1} = (R^T, -R^T * t / s, 1/s)
257    fn inverse(&self, jacobian: Option<&mut Self::JacobianMatrix>) -> Self {
258        let rot = self.rotation_impl();
259        let rot_inv = rot.inverse(None);
260        let scale_inv = 1.0 / self.scale_impl();
261        let trans_inv = -rot_inv.act(&self.translation_impl(), None, None) * scale_inv;
262
263        if let Some(jac) = jacobian {
264            *jac = -self.adjoint();
265        }
266
267        Sim3::from_parts(trans_inv, &rot_inv, scale_inv)
268    }
269
270    /// Composition of this and another Sim(3) element.
271    ///
272    /// g1 ∘ g2 = (R1*R2, s1*R1*t2 + t1, s1*s2)
273    fn compose(
274        &self,
275        other: &Self,
276        jacobian_self: Option<&mut Self::JacobianMatrix>,
277        jacobian_other: Option<&mut Self::JacobianMatrix>,
278    ) -> Self {
279        let rot = self.rotation_impl();
280        let scale = self.scale_impl();
281        let composed_rotation = rot.compose(&other.rotation_impl(), None, None);
282        let composed_translation =
283            scale * rot.act(&other.translation_impl(), None, None) + self.translation_impl();
284        let composed_scale = scale * other.scale_impl();
285
286        let result = Sim3::from_parts(composed_translation, &composed_rotation, composed_scale);
287
288        if let Some(jac_self) = jacobian_self {
289            *jac_self = other.inverse(None).adjoint();
290        }
291
292        if let Some(jac_other) = jacobian_other {
293            *jac_other = Matrix7::identity();
294        }
295
296        result
297    }
298
299    /// Logarithmic map from Sim(3) to its tangent space.
300    fn log(&self, jacobian: Option<&mut Self::JacobianMatrix>) -> Self::TangentVector {
301        let theta = self.rotation_impl().log(None);
302        let sigma = self.scale_impl().ln();
303        let mut data = Vector7::zeros();
304
305        // Compute the V^{-1} matrix for Sim(3)
306        // V^{-1} = J_l^{-1}(θ) * (I - σ/2 * [θ]× + ...)
307        let theta_tangent = SO3Tangent::new(theta.coeffs());
308        let v_inv = Self::compute_v_inv(&theta_tangent, sigma);
309        let translation_vector = v_inv * self.translation_impl();
310
311        data.fixed_rows_mut::<3>(0).copy_from(&translation_vector);
312        data.fixed_rows_mut::<3>(3).copy_from(&theta.coeffs());
313        data[6] = sigma;
314
315        let result = Sim3Tangent { data };
316
317        if let Some(jac) = jacobian {
318            *jac = result.right_jacobian_inv();
319        }
320
321        result
322    }
323
324    fn act(
325        &self,
326        vector: &Vector3<f64>,
327        jacobian_self: Option<&mut Self::JacobianMatrix>,
328        jacobian_vector: Option<&mut Matrix3<f64>>,
329    ) -> Vector3<f64> {
330        let rot = self.rotation_impl();
331        let scale = self.scale_impl();
332        let rotation_matrix = rot.rotation_matrix();
333        let result = scale * rot.act(vector, None, None) + self.translation_impl();
334
335        if let Some(jac_self) = jacobian_self {
336            let rotated_vector = rot.act(vector, None, None);
337
338            // y = s·R·p + t; under a right perturbation X·Exp(δ) the
339            // translation column is d(sRp+t)/dρ = s·R (not the identity —
340            // the point is transformed by the full scaled rotation first).
341            jac_self
342                .fixed_view_mut::<3, 3>(0, 0)
343                .copy_from(&(scale * rotation_matrix));
344
345            jac_self
346                .fixed_view_mut::<3, 3>(0, 3)
347                .copy_from(&(-scale * rotation_matrix * SO3Tangent::new(*vector).hat()));
348
349            // d(sRp)/dσ = s·R·p (the exponential's scale term contributes a
350            // factor of the already-scaled-and-rotated point, not R·p alone).
351            jac_self
352                .fixed_view_mut::<3, 1>(0, 6)
353                .copy_from(&(scale * rotated_vector));
354        }
355
356        if let Some(jac_vector) = jacobian_vector {
357            *jac_vector = scale * rotation_matrix;
358        }
359
360        result
361    }
362
363    fn adjoint(&self) -> Self::JacobianMatrix {
364        let rotation_matrix = self.rotation_impl().rotation_matrix();
365        let translation = self.translation_impl();
366        let scale = self.scale_impl();
367        let mut adjoint_matrix = Matrix7::zeros();
368
369        // From X·ξ^·X⁻¹ = (Ad_X ξ)^, block structure for Sim(3):
370        // [sR   [t]×R   -t]
371        // [0      R      0]
372        // [0      0       1]
373
374        // Top-left: s*R
375        adjoint_matrix
376            .fixed_view_mut::<3, 3>(0, 0)
377            .copy_from(&(scale * rotation_matrix));
378
379        // Top-middle: [t]× * R (the scale factors cancel against X⁻¹'s 1/s)
380        let top_middle = SO3Tangent::new(translation).hat() * rotation_matrix;
381        adjoint_matrix
382            .fixed_view_mut::<3, 3>(0, 3)
383            .copy_from(&top_middle);
384
385        // Top-right: -t (scale perturbation of X⁻¹'s translation contributes -t)
386        adjoint_matrix
387            .fixed_view_mut::<3, 1>(0, 6)
388            .copy_from(&(-translation));
389
390        // Middle-middle: R
391        adjoint_matrix
392            .fixed_view_mut::<3, 3>(3, 3)
393            .copy_from(&rotation_matrix);
394
395        // Bottom-right: 1
396        adjoint_matrix[(6, 6)] = 1.0;
397
398        adjoint_matrix
399    }
400
401    fn random() -> Self {
402        use rand::Rng;
403        let mut rng = rand::rng();
404
405        let translation = Vector3::new(
406            rng.random_range(-1.0..1.0),
407            rng.random_range(-1.0..1.0),
408            rng.random_range(-1.0..1.0),
409        );
410
411        let rotation = SO3::random();
412        let scale = rng.random_range(0.5..2.0);
413
414        Sim3::from_parts(translation, &rotation, scale)
415    }
416
417    fn normalize(&mut self) {
418        let mut rot = self.rotation_impl();
419        rot.normalize();
420        let q = rot.params();
421        self.params[3] = q[0];
422        self.params[4] = q[1];
423        self.params[5] = q[2];
424        self.params[6] = q[3];
425        if self.params[7] <= 0.0 {
426            self.params[7] = 1.0;
427        }
428    }
429
430    fn is_valid(&self, tolerance: f64) -> bool {
431        // ISSUE-0008: translation was never checked, and scale only checked
432        // `> 0.0` — `+infinity` passes a bare positivity check even though
433        // NaN correctly fails it (`NaN > 0.0` is `false`). Both translation
434        // and scale must be finite in addition to scale being positive.
435        self.translation_impl().iter().all(|t| t.is_finite())
436            && self.rotation_impl().is_valid(tolerance)
437            && self.scale_impl().is_finite()
438            && self.scale_impl() > 0.0
439    }
440
441    fn as_param_slice(&self) -> &[f64] {
442        self.params.as_slice()
443    }
444
445    fn as_param_slice_mut(&mut self) -> &mut [f64] {
446        self.params.as_mut_slice()
447    }
448
449    fn from_param_slice(s: &[f64]) -> Self {
450        debug_assert_eq!(s.len(), 8);
451        Sim3 {
452            params: SVector::from_column_slice(s),
453        }
454    }
455
456    fn vee(&self) -> Self::TangentVector {
457        self.log(None)
458    }
459
460    fn is_approx(&self, other: &Self, tolerance: f64) -> bool {
461        let difference = self.right_minus(other, None, None);
462        difference.is_zero(tolerance)
463    }
464
465    fn jacobian_identity() -> Self::JacobianMatrix {
466        Matrix7::<f64>::identity()
467    }
468
469    fn zero_jacobian() -> Self::JacobianMatrix {
470        Matrix7::<f64>::zeros()
471    }
472}
473
474impl Sim3 {
475    /// Compute V^{-1} matrix for Sim(3) logarithm.
476    ///
477    /// Compute the inverse of the V matrix for Sim(3) logarithm.
478    fn compute_v_inv(theta: &SO3Tangent, sigma: f64) -> Matrix3<f64> {
479        let v = Sim3Tangent::v_matrix(theta, sigma);
480        regularized_inverse_3(&v, "V⁻¹")
481    }
482}
483
484/// Sim(3) tangent space element.
485///
486/// Represented as [ρ(3), θ(3), σ(1)] where:
487/// - ρ: translational component
488/// - θ: rotational component (axis-angle)
489/// - σ: logarithmic scale
490#[derive(Clone, PartialEq)]
491pub struct Sim3Tangent {
492    /// Internal data: [ρ_x, ρ_y, ρ_z, θ_x, θ_y, θ_z, σ]
493    data: Vector7<f64>,
494}
495
496impl fmt::Display for Sim3Tangent {
497    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
498        let rho = self.rho();
499        let theta = self.theta();
500        let sigma = self.sigma();
501        write!(
502            f,
503            "sim3(rho: [{:.4}, {:.4}, {:.4}], theta: [{:.4}, {:.4}, {:.4}], sigma: {:.4})",
504            rho.x, rho.y, rho.z, theta.x, theta.y, theta.z, sigma
505        )
506    }
507}
508
509impl Sim3Tangent {
510    /// Create a new Sim(3)Tangent from components.
511    pub fn new(rho: Vector3<f64>, theta: Vector3<f64>, sigma: f64) -> Self {
512        let mut data = Vector7::zeros();
513        data.fixed_rows_mut::<3>(0).copy_from(&rho);
514        data.fixed_rows_mut::<3>(3).copy_from(&theta);
515        data[6] = sigma;
516        Sim3Tangent { data }
517    }
518
519    /// Get the ρ (translational) part.
520    pub fn rho(&self) -> Vector3<f64> {
521        self.data.fixed_rows::<3>(0).into_owned()
522    }
523
524    /// Get the θ (rotational) part.
525    pub fn theta(&self) -> Vector3<f64> {
526        self.data.fixed_rows::<3>(3).into_owned()
527    }
528
529    /// Get the σ (logarithmic scale) part.
530    pub fn sigma(&self) -> f64 {
531        self.data[6]
532    }
533
534    /// Create Sim3Tangent from individual scalar components.
535    pub fn from_components(
536        rho_x: f64,
537        rho_y: f64,
538        rho_z: f64,
539        theta_x: f64,
540        theta_y: f64,
541        theta_z: f64,
542        sigma: f64,
543    ) -> Self {
544        Sim3Tangent {
545            data: Vector7::from_column_slice(&[
546                rho_x, rho_y, rho_z, theta_x, theta_y, theta_z, sigma,
547            ]),
548        }
549    }
550
551    /// Compute the V matrix for Sim(3) exponential map.
552    ///
553    /// V = ∫₀¹ exp(σt) · exp(θt×) dt = A·I + B·[θ]× + C·[θ]ײ
554    ///
555    /// Based on Ethan Eade's "Lie Groups for Computer Vision" formulation.
556    fn v_matrix(theta: &SO3Tangent, sigma: f64) -> Matrix3<f64> {
557        let theta_norm_sq = theta.coeffs().norm_squared();
558
559        if theta_norm_sq < crate::SMALL_ANGLE_THRESHOLD
560            && sigma.abs() < crate::SMALL_ANGLE_THRESHOLD
561        {
562            return Matrix3::identity();
563        }
564
565        let theta_hat = theta.hat();
566
567        if theta_norm_sq < crate::SMALL_ANGLE_THRESHOLD {
568            // Pure scale, no rotation: V = (e^σ - 1)/σ · I
569            let a = (sigma.exp() - 1.0) / sigma;
570            return a * Matrix3::identity();
571        }
572
573        let theta_norm = theta_norm_sq.sqrt();
574        let sin_theta = theta_norm.sin();
575        let cos_theta = theta_norm.cos();
576
577        if sigma.abs() < crate::SMALL_ANGLE_THRESHOLD {
578            // Pure rotation, no scale: V = SO(3) left Jacobian
579            let a = 1.0;
580            let b = (1.0 - cos_theta) / theta_norm_sq;
581            let c = (theta_norm - sin_theta) / (theta_norm * theta_norm_sq);
582            return a * Matrix3::identity() + b * theta_hat + c * theta_hat * theta_hat;
583        }
584
585        // General case: both sigma and theta nonzero
586        let e_sigma = sigma.exp();
587        let alpha_sq = sigma * sigma + theta_norm_sq;
588
589        let a = (e_sigma - 1.0) / sigma;
590        let b = (e_sigma * (sigma * sin_theta - theta_norm * cos_theta) + theta_norm)
591            / (theta_norm * alpha_sq);
592        let cos_integral =
593            (e_sigma * (sigma * cos_theta + theta_norm * sin_theta) - sigma) / alpha_sq;
594        let c = (a - cos_integral) / theta_norm_sq;
595
596        a * Matrix3::identity() + b * theta_hat + c * theta_hat * theta_hat
597    }
598}
599
600/// `∫₀¹ Ad(Exp(-t·ξ)) dt` via composite Simpson's rule — see
601/// [`Sim3Tangent::right_jacobian`] for why this replaces a closed-form `Q` block.
602fn integrate_adjoint_of_negated_exp(xi: &Sim3Tangent) -> Matrix7<f64> {
603    // Every adjoint()/exp() entry integrated here is closed-form and
604    // singularity-free for all θ, σ, so the integrand is smooth (built from
605    // sin/cos/exp) with no small-angle branch to mistune; 256 intervals gives
606    // several orders of magnitude of margin below central-difference test
607    // tolerances (error bound ~1e-11 even at θ near π).
608    const INTERVALS: usize = 256;
609    let h = 1.0 / INTERVALS as f64;
610
611    let adjoint_at = |t: f64| -> Matrix7<f64> {
612        let scaled = Sim3Tangent {
613            data: xi.data * (-t),
614        };
615        scaled.exp(None).adjoint()
616    };
617
618    let mut sum = adjoint_at(0.0) + adjoint_at(1.0);
619    for i in 1..INTERVALS {
620        let t = i as f64 * h;
621        let weight = if i % 2 == 1 { 4.0 } else { 2.0 };
622        sum += adjoint_at(t) * weight;
623    }
624
625    sum * (h / 3.0)
626}
627
628impl Tangent<Sim3> for Sim3Tangent {
629    const DIM: usize = 7;
630
631    /// Exponential map to Sim(3).
632    fn exp(&self, jacobian: Option<&mut <Sim3 as LieGroup>::JacobianMatrix>) -> Sim3 {
633        let rho = self.rho();
634        let theta = self.theta();
635        let sigma = self.sigma();
636
637        let theta_tangent = SO3Tangent::new(theta);
638        let rotation = theta_tangent.exp(None);
639        let v_matrix = Self::v_matrix(&theta_tangent, sigma);
640        let translation = v_matrix * rho;
641        let scale = sigma.exp();
642
643        if let Some(jac) = jacobian {
644            *jac = self.right_jacobian();
645        }
646
647        Sim3::from_components(translation, rotation, scale)
648    }
649
650    /// Right Jacobian for Sim(3).
651    ///
652    /// Computed from the general Lie-group identity `Jr(ξ) = ∫₀¹ Ad(Exp(-tξ)) dt`
653    /// (e.g. Solà, "A micro Lie theory for state estimation in robotics", eq. 146),
654    /// evaluated by [`integrate_adjoint_of_negated_exp`]. Unlike SE(3)'s `Q`
655    /// block, Sim(3)'s exact translation/rotation/scale coupling does not
656    /// reduce to a compact closed form; this identity sidesteps re-deriving
657    /// one by hand and is verified against central differences of `Exp` in
658    /// the test suite.
659    fn right_jacobian(&self) -> <Sim3 as LieGroup>::JacobianMatrix {
660        integrate_adjoint_of_negated_exp(self)
661    }
662
663    /// Left Jacobian for Sim(3): `Jl(ξ) = Jr(-ξ)` (general Lie-group identity).
664    fn left_jacobian(&self) -> <Sim3 as LieGroup>::JacobianMatrix {
665        let negated = Sim3Tangent { data: -self.data };
666        negated.right_jacobian()
667    }
668
669    /// Inverse of right Jacobian.
670    fn right_jacobian_inv(&self) -> <Sim3 as LieGroup>::JacobianMatrix {
671        regularized_inverse_7(&self.right_jacobian(), "right-Jacobian⁻¹")
672    }
673
674    /// Inverse of left Jacobian.
675    fn left_jacobian_inv(&self) -> <Sim3 as LieGroup>::JacobianMatrix {
676        regularized_inverse_7(&self.left_jacobian(), "left-Jacobian⁻¹")
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    /// ISSUE-0008 regression: every stored parameter must be checked, not
855    /// just the quaternion — a NaN/Inf translation previously passed
856    /// `is_valid` silently, and scale only checked `> 0.0` (so `+infinity`
857    /// passed even though it isn't a valid finite scale). Also covers a
858    /// non-finite tolerance.
859    #[test]
860    fn test_sim3_is_valid_rejects_nonfinite_at_every_index() {
861        let base = Sim3::identity();
862        for idx in 0..Sim3::REP_SIZE {
863            for bad in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
864                let mut params: Vec<f64> = base.as_param_slice().to_vec();
865                params[idx] = bad;
866                let perturbed = Sim3::from_param_slice(&params);
867                assert!(
868                    !perturbed.is_valid(TOLERANCE),
869                    "index {idx} = {bad} must be rejected"
870                );
871            }
872        }
873        assert!(
874            !base.is_valid(f64::INFINITY),
875            "infinite tolerance must be rejected"
876        );
877        assert!(!base.is_valid(f64::NAN), "NaN tolerance must be rejected");
878    }
879
880    #[test]
881    fn test_sim3_new() {
882        let translation = Vector3::new(1.0, 2.0, 3.0);
883        let rotation = UnitQuaternion::from_euler_angles(0.1, 0.2, 0.3);
884        let scale = 1.5;
885
886        let sim3 = Sim3::new(translation, rotation, scale);
887        assert!(sim3.is_valid(TOLERANCE));
888        assert!((sim3.translation() - translation).norm() < TOLERANCE);
889        assert!((sim3.scale() - scale).abs() < TOLERANCE);
890    }
891
892    #[test]
893    #[should_panic(expected = "Scale must be positive")]
894    fn test_sim3_new_negative_scale() {
895        let translation = Vector3::new(1.0, 2.0, 3.0);
896        let rotation = UnitQuaternion::identity();
897        let _sim3 = Sim3::new(translation, rotation, -1.0);
898    }
899
900    #[test]
901    fn test_sim3_random() {
902        let sim3 = Sim3::random();
903        assert!(sim3.is_valid(TOLERANCE));
904        assert!(sim3.scale() > 0.0);
905    }
906
907    #[test]
908    fn test_sim3_inverse() {
909        let sim3 = Sim3::random();
910        let sim3_inv = sim3.inverse(None);
911
912        let composed = sim3.compose(&sim3_inv, None, None);
913        let identity = Sim3::identity();
914
915        assert!(composed.is_approx(&identity, TOLERANCE));
916    }
917
918    #[test]
919    fn test_sim3_compose() {
920        let sim3_1 = Sim3::random();
921        let sim3_2 = Sim3::random();
922
923        let composed = sim3_1.compose(&sim3_2, None, None);
924        assert!(composed.is_valid(TOLERANCE));
925
926        let identity = Sim3::identity();
927        let composed_with_identity = sim3_1.compose(&identity, None, None);
928        assert!(composed_with_identity.is_approx(&sim3_1, TOLERANCE));
929    }
930
931    #[test]
932    fn test_sim3_exp_log() {
933        let tangent = Sim3Tangent::new(
934            Vector3::new(0.1, 0.2, 0.3),
935            Vector3::new(0.01, 0.02, 0.03),
936            0.1,
937        );
938
939        let sim3 = tangent.exp(None);
940        let recovered_tangent = sim3.log(None);
941
942        assert!((tangent.data - recovered_tangent.data).norm() < TOLERANCE);
943    }
944
945    #[test]
946    fn test_sim3_exp_zero() {
947        let zero_tangent = Sim3Tangent::zero();
948        let sim3 = zero_tangent.exp(None);
949        let identity = Sim3::identity();
950
951        assert!(sim3.is_approx(&identity, TOLERANCE));
952    }
953
954    #[test]
955    fn test_sim3_log_identity() {
956        let identity = Sim3::identity();
957        let tangent = identity.log(None);
958
959        assert!(tangent.data.norm() < TOLERANCE);
960    }
961
962    #[test]
963    fn test_sim3_adjoint() {
964        let sim3 = Sim3::random();
965        let adj = sim3.adjoint();
966
967        assert_eq!(adj.nrows(), 7);
968        assert_eq!(adj.ncols(), 7);
969    }
970
971    #[test]
972    fn test_sim3_act() {
973        let sim3 = Sim3::random();
974        let point = Vector3::new(1.0, 2.0, 3.0);
975
976        let _transformed_point = sim3.act(&point, None, None);
977
978        let identity = Sim3::identity();
979        let identity_transformed = identity.act(&point, None, None);
980
981        assert!((identity_transformed - point).norm() < TOLERANCE);
982    }
983
984    #[test]
985    fn test_sim3_between() {
986        let sim3_a = Sim3::random();
987        let sim3_b = sim3_a.clone();
988        let sim3_between_identity = sim3_a.between(&sim3_b, None, None);
989        assert!(sim3_between_identity.is_approx(&Sim3::identity(), TOLERANCE));
990
991        let sim3_c = Sim3::random();
992        let sim3_between = sim3_a.between(&sim3_c, None, None);
993        let expected = sim3_a.inverse(None).compose(&sim3_c, None, None);
994        assert!(sim3_between.is_approx(&expected, TOLERANCE));
995    }
996
997    #[test]
998    fn test_sim3_tangent_zero() {
999        let zero = Sim3Tangent::zero();
1000        assert!(zero.data.norm() < TOLERANCE);
1001
1002        let tangent = Sim3Tangent::new(Vector3::zeros(), Vector3::zeros(), 0.0);
1003        assert!(tangent.is_zero(TOLERANCE));
1004    }
1005
1006    #[test]
1007    fn test_sim3_manifold_properties() {
1008        assert_eq!(Sim3::DIM, 3);
1009        assert_eq!(Sim3::DOF, 7);
1010        assert_eq!(Sim3::REP_SIZE, 8);
1011    }
1012
1013    #[test]
1014    fn test_sim3_consistency() {
1015        let sim3_1 = Sim3::random();
1016        let sim3_2 = Sim3::random();
1017        let sim3_3 = Sim3::random();
1018
1019        // Test associativity
1020        let left_assoc = sim3_1
1021            .compose(&sim3_2, None, None)
1022            .compose(&sim3_3, None, None);
1023        let right_assoc = sim3_1.compose(&sim3_2.compose(&sim3_3, None, None), None, None);
1024
1025        assert!(left_assoc.is_approx(&right_assoc, 1e-10));
1026    }
1027
1028    #[test]
1029    fn test_sim3_scale_composition() {
1030        let sim3_1 = Sim3::new(Vector3::zeros(), UnitQuaternion::identity(), 2.0);
1031        let sim3_2 = Sim3::new(Vector3::zeros(), UnitQuaternion::identity(), 3.0);
1032
1033        let composed = sim3_1.compose(&sim3_2, None, None);
1034        assert!((composed.scale() - 6.0).abs() < TOLERANCE);
1035    }
1036
1037    #[test]
1038    fn test_sim3_tangent_small_adj() {
1039        let tangent = Sim3Tangent::new(
1040            Vector3::new(0.1, 0.2, 0.3),
1041            Vector3::new(0.4, 0.5, 0.6),
1042            0.1,
1043        );
1044        let small_adj = tangent.small_adj();
1045
1046        assert_eq!(small_adj.nrows(), 7);
1047        assert_eq!(small_adj.ncols(), 7);
1048    }
1049
1050    #[test]
1051    fn test_sim3_tangent_lie_bracket() {
1052        let tangent_a = Sim3Tangent::new(
1053            Vector3::new(0.1, 0.0, 0.0),
1054            Vector3::new(0.0, 0.2, 0.0),
1055            0.1,
1056        );
1057        let tangent_b = Sim3Tangent::new(
1058            Vector3::new(0.0, 0.3, 0.0),
1059            Vector3::new(0.0, 0.0, 0.4),
1060            0.2,
1061        );
1062
1063        let bracket_ab = tangent_a.lie_bracket(&tangent_b);
1064        let bracket_ba = tangent_b.lie_bracket(&tangent_a);
1065
1066        // Anti-symmetry
1067        assert!((bracket_ab.data + bracket_ba.data).norm() < 1e-10);
1068
1069        // [a,a] = 0
1070        let bracket_aa = tangent_a.lie_bracket(&tangent_a);
1071        assert!(bracket_aa.is_zero(1e-10));
1072    }
1073
1074    #[test]
1075    fn test_sim3_tangent_is_approx() {
1076        let tangent_1 = Sim3Tangent::new(
1077            Vector3::new(0.1, 0.2, 0.3),
1078            Vector3::new(0.4, 0.5, 0.6),
1079            0.1,
1080        );
1081        let tangent_2 = Sim3Tangent::new(
1082            Vector3::new(0.1 + 1e-12, 0.2, 0.3),
1083            Vector3::new(0.4, 0.5, 0.6),
1084            0.1,
1085        );
1086        let tangent_3 = Sim3Tangent::new(
1087            Vector3::new(1.0, 2.0, 3.0),
1088            Vector3::new(4.0, 5.0, 6.0),
1089            1.0,
1090        );
1091
1092        assert!(tangent_1.is_approx(&tangent_1, 1e-10));
1093        assert!(tangent_1.is_approx(&tangent_2, 1e-10));
1094        assert!(!tangent_1.is_approx(&tangent_3, 1e-10));
1095    }
1096
1097    #[test]
1098    fn test_sim3_generators() {
1099        let tangent = Sim3Tangent::new(
1100            Vector3::new(1.0, 1.0, 1.0),
1101            Vector3::new(1.0, 1.0, 1.0),
1102            1.0,
1103        );
1104
1105        for i in 0..7 {
1106            let generator = tangent.generator(i);
1107            assert_eq!(generator.nrows(), 4);
1108            assert_eq!(generator.ncols(), 4);
1109
1110            // Bottom row should be zeros
1111            assert_eq!(generator[(3, 0)], 0.0);
1112            assert_eq!(generator[(3, 1)], 0.0);
1113            assert_eq!(generator[(3, 2)], 0.0);
1114            assert_eq!(generator[(3, 3)], 0.0);
1115        }
1116    }
1117
1118    #[test]
1119    #[should_panic]
1120    fn test_sim3_generator_invalid_index() {
1121        let tangent = Sim3Tangent::new(
1122            Vector3::new(1.0, 1.0, 1.0),
1123            Vector3::new(1.0, 1.0, 1.0),
1124            1.0,
1125        );
1126        let _generator = tangent.generator(7);
1127    }
1128
1129    #[test]
1130    fn test_sim3_vee() {
1131        let sim3 = Sim3::random();
1132        let tangent_log = sim3.log(None);
1133        let tangent_vee = sim3.vee();
1134
1135        assert!((tangent_log.data - tangent_vee.data).norm() < 1e-10);
1136    }
1137
1138    #[test]
1139    fn test_sim3_is_approx() {
1140        let sim3_1 = Sim3::random();
1141        let sim3_2 = sim3_1.clone();
1142
1143        assert!(sim3_1.is_approx(&sim3_1, 1e-10));
1144        assert!(sim3_1.is_approx(&sim3_2, 1e-10));
1145
1146        let small_tangent = Sim3Tangent::new(
1147            Vector3::new(1e-12, 1e-12, 1e-12),
1148            Vector3::new(1e-12, 1e-12, 1e-12),
1149            1e-12,
1150        );
1151        let sim3_perturbed = sim3_1.right_plus(&small_tangent, None, None);
1152        assert!(sim3_1.is_approx(&sim3_perturbed, 1e-10));
1153    }
1154
1155    #[test]
1156    fn test_sim3_small_angle_approximations() {
1157        let small_tangent = Sim3Tangent::new(
1158            Vector3::new(1e-8, 2e-8, 3e-8),
1159            Vector3::new(1e-9, 2e-9, 3e-9),
1160            1e-8,
1161        );
1162
1163        let sim3 = small_tangent.exp(None);
1164        let recovered = sim3.log(None);
1165
1166        assert!((small_tangent.data - recovered.data).norm() < TOLERANCE);
1167    }
1168
1169    #[test]
1170    fn test_sim3_accessors() {
1171        let translation = Vector3::new(1.0, 2.0, 3.0);
1172        let rotation = UnitQuaternion::identity();
1173        let scale = 1.5;
1174
1175        let sim3 = Sim3::new(translation, rotation, scale);
1176
1177        assert_eq!(sim3.x(), 1.0);
1178        assert_eq!(sim3.y(), 2.0);
1179        assert_eq!(sim3.z(), 3.0);
1180        assert_eq!(sim3.scale(), 1.5);
1181    }
1182
1183    #[test]
1184    fn test_sim3_matrix() {
1185        let translation = Vector3::new(1.0, 2.0, 3.0);
1186        let rotation = UnitQuaternion::identity();
1187        let scale = 2.0;
1188
1189        let sim3 = Sim3::new(translation, rotation, scale);
1190        let mat = sim3.matrix();
1191
1192        // Check that top-left 3x3 is scaled rotation (2*I in this case)
1193        for i in 0..3 {
1194            for j in 0..3 {
1195                if i == j {
1196                    assert!((mat[(i, j)] - 2.0).abs() < TOLERANCE);
1197                } else {
1198                    assert!(mat[(i, j)].abs() < TOLERANCE);
1199                }
1200            }
1201        }
1202
1203        // Check translation
1204        assert!((mat[(0, 3)] - 1.0).abs() < TOLERANCE);
1205        assert!((mat[(1, 3)] - 2.0).abs() < TOLERANCE);
1206        assert!((mat[(2, 3)] - 3.0).abs() < TOLERANCE);
1207
1208        // Check bottom row
1209        assert!(mat[(3, 0)].abs() < TOLERANCE);
1210        assert!(mat[(3, 1)].abs() < TOLERANCE);
1211        assert!(mat[(3, 2)].abs() < TOLERANCE);
1212        assert!((mat[(3, 3)] - 1.0).abs() < TOLERANCE);
1213    }
1214
1215    #[test]
1216    fn test_sim3_scale_action() {
1217        let scale = 2.0;
1218        let sim3 = Sim3::new(Vector3::zeros(), UnitQuaternion::identity(), scale);
1219        let point = Vector3::new(1.0, 2.0, 3.0);
1220
1221        let transformed = sim3.act(&point, None, None);
1222
1223        assert!((transformed.x - 2.0).abs() < TOLERANCE);
1224        assert!((transformed.y - 4.0).abs() < TOLERANCE);
1225        assert!((transformed.z - 6.0).abs() < TOLERANCE);
1226    }
1227
1228    #[test]
1229    fn test_sim3_tangent_basic() {
1230        let rho = Vector3::new(0.1, 0.2, 0.3);
1231        let theta = Vector3::new(0.4, 0.5, 0.6);
1232        let sigma = 0.1;
1233        let tangent = Sim3Tangent::new(rho, theta, sigma);
1234        assert!((tangent.rho() - rho).norm() < TOLERANCE);
1235        assert!((tangent.theta() - theta).norm() < TOLERANCE);
1236        assert!((tangent.sigma() - sigma).abs() < TOLERANCE);
1237    }
1238
1239    #[test]
1240    fn test_sim3_tangent_from_components() {
1241        let t1 = Sim3Tangent::new(
1242            Vector3::new(1.0, 2.0, 3.0),
1243            Vector3::new(0.4, 0.5, 0.6),
1244            0.1,
1245        );
1246        let t2 = Sim3Tangent::from_components(1.0, 2.0, 3.0, 0.4, 0.5, 0.6, 0.1);
1247        assert!(t1.is_approx(&t2, TOLERANCE));
1248    }
1249
1250    #[test]
1251    fn test_sim3_normalize() {
1252        let mut sim3 = Sim3::random();
1253        sim3.normalize();
1254        assert!(sim3.is_valid(TOLERANCE));
1255    }
1256
1257    #[test]
1258    fn test_sim3_coeffs() {
1259        let translation = Vector3::new(1.0, 2.0, 3.0);
1260        let rotation = UnitQuaternion::identity();
1261        let scale = 1.5;
1262        let sim3 = Sim3::new(translation, rotation, scale);
1263        let c = sim3.coeffs();
1264        assert!((c[0] - 1.0).abs() < TOLERANCE);
1265        assert!((c[1] - 2.0).abs() < TOLERANCE);
1266        assert!((c[2] - 3.0).abs() < TOLERANCE);
1267        assert!((c[3] - 1.0).abs() < TOLERANCE); // qw
1268        assert!((c[7] - 1.5).abs() < TOLERANCE); // scale
1269    }
1270
1271    #[test]
1272    fn test_sim3_manif_like_operations() {
1273        let a = Sim3::random();
1274        let b = Sim3::random();
1275
1276        let a_to_b = a.between(&b, None, None);
1277        let recovered_b = a.compose(&a_to_b, None, None);
1278        assert!(recovered_b.is_approx(&b, TOLERANCE));
1279
1280        let tangent = Sim3Tangent::new(
1281            Vector3::new(0.01, 0.02, 0.03),
1282            Vector3::new(0.001, 0.002, 0.003),
1283            0.01,
1284        );
1285        let perturbed = a.plus(&tangent, None, None);
1286        let recovered_tangent = perturbed.minus(&a, None, None);
1287        assert!((tangent.data - recovered_tangent.data).norm() < 1e-6);
1288    }
1289
1290    #[test]
1291    fn test_sim3_right_jacobian_inverse_identity() {
1292        let tangent = Sim3Tangent::new(
1293            Vector3::new(0.1, 0.2, 0.3),
1294            Vector3::new(0.01, 0.02, 0.03),
1295            0.1,
1296        );
1297        let jr = tangent.right_jacobian();
1298        let jr_inv = tangent.right_jacobian_inv();
1299        let product = jr * jr_inv;
1300        let identity = Matrix7::<f64>::identity();
1301        assert!((product - identity).norm() < 0.01);
1302    }
1303
1304    #[test]
1305    fn test_sim3_left_jacobian_inverse_identity() {
1306        let tangent = Sim3Tangent::new(
1307            Vector3::new(0.1, 0.2, 0.3),
1308            Vector3::new(0.01, 0.02, 0.03),
1309            0.1,
1310        );
1311        let jl = tangent.left_jacobian();
1312        let jl_inv = tangent.left_jacobian_inv();
1313        let product = jl * jl_inv;
1314        let identity = Matrix7::<f64>::identity();
1315        assert!((product - identity).norm() < 0.01);
1316    }
1317
1318    #[test]
1319    fn test_sim3_jacobi_identity() {
1320        let a = Sim3Tangent::random();
1321        let b = Sim3Tangent::random();
1322        let c = Sim3Tangent::random();
1323
1324        let term1 = a.lie_bracket(&b.lie_bracket(&c));
1325        let term2 = b.lie_bracket(&c.lie_bracket(&a));
1326        let term3 = c.lie_bracket(&a.lie_bracket(&b));
1327
1328        assert!((term1.data + term2.data + term3.data).norm() < 1e-8);
1329    }
1330
1331    #[test]
1332    fn test_sim3_hat_matrix_structure() {
1333        let tangent = Sim3Tangent::new(
1334            Vector3::new(1.0, 2.0, 3.0),
1335            Vector3::new(0.1, 0.2, 0.3),
1336            0.5,
1337        );
1338        let hat = tangent.hat();
1339
1340        for j in 0..4 {
1341            assert_eq!(hat[(3, j)], 0.0);
1342        }
1343
1344        assert!((hat[(0, 3)] - 1.0).abs() < TOLERANCE);
1345        assert!((hat[(1, 3)] - 2.0).abs() < TOLERANCE);
1346        assert!((hat[(2, 3)] - 3.0).abs() < TOLERANCE);
1347
1348        // Diagonal includes sigma
1349        assert!((hat[(0, 0)] - 0.5).abs() < 0.5); // theta_skew + sigma
1350    }
1351
1352    #[test]
1353    fn test_sim3_param_slice_round_trip() {
1354        let sim3 = Sim3::random();
1355        let recovered = Sim3::from_param_slice(sim3.as_param_slice());
1356        assert!(sim3.is_approx(&recovered, TOLERANCE));
1357    }
1358
1359    // --- Additional coverage tests ---
1360
1361    #[test]
1362    fn test_sim3_display() {
1363        let sim3 = Sim3::identity();
1364        let s = format!("{}", sim3);
1365        assert!(s.contains("Sim3"));
1366
1367        let tangent = Sim3Tangent::new(
1368            Vector3::new(1.0, 2.0, 3.0),
1369            Vector3::new(0.1, 0.2, 0.3),
1370            0.5,
1371        );
1372        let ts = format!("{}", tangent);
1373        assert!(ts.contains("sim3"));
1374    }
1375
1376    #[test]
1377    fn test_sim3_jacobian_identity_static() {
1378        let jac = Sim3::jacobian_identity();
1379        assert!((jac - Matrix7::identity()).norm() < TOLERANCE);
1380    }
1381
1382    #[test]
1383    fn test_sim3_rotation_so3() {
1384        let sim3 = Sim3::random();
1385        let so3 = sim3.rotation_so3();
1386        assert!(so3.is_valid(TOLERANCE));
1387    }
1388
1389    #[test]
1390    fn test_sim3_inverse_with_jacobian() {
1391        let sim3 = Sim3::random();
1392        let mut jac = Matrix7::zeros();
1393        let inv = sim3.inverse(Some(&mut jac));
1394        let expected_jac = -sim3.adjoint();
1395        assert!((jac - expected_jac).norm() < TOLERANCE);
1396        let composed = sim3.compose(&inv, None, None);
1397        assert!(composed.is_approx(&Sim3::identity(), TOLERANCE));
1398    }
1399
1400    #[test]
1401    fn test_sim3_compose_with_jacobians() {
1402        let a = Sim3::random();
1403        let b = Sim3::random();
1404        let mut jac_a = Matrix7::zeros();
1405        let mut jac_b = Matrix7::zeros();
1406        let _composed = a.compose(&b, Some(&mut jac_a), Some(&mut jac_b));
1407        assert!(jac_a.norm() > 0.0);
1408        assert!(jac_b.norm() > 0.0);
1409    }
1410
1411    #[test]
1412    fn test_sim3_act_with_jacobians() {
1413        let sim3 = Sim3::random();
1414        let point = Vector3::new(1.0, 2.0, 3.0);
1415        let mut jac_self = Matrix7::zeros();
1416        let mut jac_point = Matrix3::<f64>::zeros();
1417        let result = sim3.act(&point, Some(&mut jac_self), Some(&mut jac_point));
1418        assert!(result.norm() > 0.0);
1419        assert!(jac_self.norm() > 0.0);
1420        assert!(jac_point.norm() > 0.0);
1421    }
1422
1423    #[test]
1424    fn test_sim3_liegroup_jacobian_identity() {
1425        let jac = <Sim3 as LieGroup>::jacobian_identity();
1426        assert!((jac - Matrix7::identity()).norm() < TOLERANCE);
1427
1428        let zero = <Sim3 as LieGroup>::zero_jacobian();
1429        assert!(zero.norm() < TOLERANCE);
1430    }
1431
1432    #[test]
1433    fn test_sim3_tangent_slice_round_trip() {
1434        let tangent = Sim3Tangent::new(
1435            Vector3::new(1.0, 2.0, 3.0),
1436            Vector3::new(0.1, 0.2, 0.3),
1437            0.5,
1438        );
1439        let recovered = Sim3Tangent::from_slice(tangent.as_slice());
1440        assert!(tangent.is_approx(&recovered, TOLERANCE));
1441    }
1442
1443    #[test]
1444    fn test_sim3_exp_with_jacobian() {
1445        let tangent = Sim3Tangent::new(
1446            Vector3::new(0.1, 0.2, 0.3),
1447            Vector3::new(0.01, 0.02, 0.03),
1448            0.1,
1449        );
1450        let mut jac = Matrix7::zeros();
1451        let _result = tangent.exp(Some(&mut jac));
1452        assert!(jac.norm() > 0.0);
1453    }
1454
1455    #[test]
1456    fn test_sim3_exp_log_stress() {
1457        for _ in 0..100 {
1458            let tangent = Sim3Tangent::random();
1459            let sim3 = tangent.exp(None);
1460            let recovered = sim3.log(None);
1461            assert!(
1462                tangent.is_approx(&recovered, 1e-6),
1463                "exp/log round-trip failed: error = {}",
1464                (tangent.data - recovered.data).norm()
1465            );
1466        }
1467    }
1468
1469    #[test]
1470    fn test_sim3_right_plus_minus_round_trip() {
1471        let a = Sim3::random();
1472        let b = Sim3::random();
1473        let diff = a.right_minus(&b, None, None);
1474        let recovered = b.right_plus(&diff, None, None);
1475        assert!(a.is_approx(&recovered, 1e-6));
1476    }
1477
1478    #[test]
1479    fn test_sim3_left_plus_minus_round_trip() {
1480        let a = Sim3::random();
1481        let b = Sim3::random();
1482        let diff = a.left_minus(&b, None, None);
1483        let recovered = b.left_plus(&diff, None, None);
1484        assert!(a.is_approx(&recovered, 1e-6));
1485    }
1486
1487    #[test]
1488    fn test_sim3_compose_associativity() {
1489        let a = Sim3::random();
1490        let b = Sim3::random();
1491        let c = Sim3::random();
1492        let ab_c = a.compose(&b, None, None).compose(&c, None, None);
1493        let a_bc = a.compose(&b.compose(&c, None, None), None, None);
1494        assert!(ab_c.is_approx(&a_bc, 1e-6));
1495    }
1496
1497    #[test]
1498    fn test_sim3_inverse_twice() {
1499        let g = Sim3::random();
1500        let g_inv_inv = g.inverse(None).inverse(None);
1501        assert!(g.is_approx(&g_inv_inv, 1e-6));
1502    }
1503
1504    #[test]
1505    fn test_sim3_pure_scale() {
1506        // exp/log with only sigma nonzero (theta=0)
1507        let tangent = Sim3Tangent::new(Vector3::zeros(), Vector3::zeros(), 0.5);
1508        let sim3 = tangent.exp(None);
1509        let recovered = sim3.log(None);
1510        assert!(tangent.is_approx(&recovered, TOLERANCE));
1511        assert!((sim3.scale() - 0.5_f64.exp()).abs() < TOLERANCE);
1512    }
1513
1514    #[test]
1515    fn test_sim3_pure_rotation() {
1516        // exp/log with only theta nonzero (sigma=0)
1517        let tangent = Sim3Tangent::new(
1518            Vector3::new(0.1, 0.2, 0.3),
1519            Vector3::new(0.05, 0.1, 0.15),
1520            0.0,
1521        );
1522        let sim3 = tangent.exp(None);
1523        let recovered = sim3.log(None);
1524        assert!(tangent.is_approx(&recovered, 1e-6));
1525        assert!((sim3.scale() - 1.0).abs() < TOLERANCE);
1526    }
1527
1528    #[test]
1529    fn test_sim3_scale_compose() {
1530        let a = Sim3::from_components(Vector3::zeros(), SO3::identity(), 2.0);
1531        let b = Sim3::from_components(Vector3::zeros(), SO3::identity(), 3.0);
1532        let ab = a.compose(&b, None, None);
1533        assert!((ab.scale() - 6.0).abs() < TOLERANCE);
1534    }
1535
1536    #[test]
1537    fn test_sim3_tangent_normalize_zero_theta() {
1538        let tangent = Sim3Tangent::new(Vector3::new(1.0, 2.0, 3.0), Vector3::zeros(), 0.5);
1539        let normalized = tangent.normalized();
1540        assert!(normalized.theta().norm() < TOLERANCE);
1541    }
1542
1543    #[test]
1544    fn test_sim3_tangent_generator_all() {
1545        let tangent = Sim3Tangent::zero();
1546        for i in 0..7 {
1547            let g = tangent.generator(i);
1548            assert!(g.norm() > 0.0, "Generator {} should be non-zero", i);
1549        }
1550    }
1551
1552    #[test]
1553    fn test_sim3_v_matrix_sigma_zero_matches_so3_jl() {
1554        // When sigma=0, V-matrix should equal SO3 left Jacobian
1555        let theta = SO3Tangent::new(Vector3::new(0.3, 0.4, 0.5));
1556        let v = Sim3Tangent::v_matrix(&theta, 0.0);
1557        let jl = theta.left_jacobian();
1558        assert!(
1559            (v - jl).norm() < 1e-10,
1560            "V(θ,0) should equal Jl(θ), error = {}",
1561            (v - jl).norm()
1562        );
1563    }
1564
1565    #[test]
1566    fn test_sim3_v_matrix_theta_zero() {
1567        // When theta=0, V-matrix should be (e^σ - 1)/σ * I
1568        let theta = SO3Tangent::new(Vector3::zeros());
1569        let sigma = 0.5;
1570        let v = Sim3Tangent::v_matrix(&theta, sigma);
1571        let expected = (sigma.exp() - 1.0) / sigma * Matrix3::<f64>::identity();
1572        assert!(
1573            (v - expected).norm() < 1e-10,
1574            "V(0,σ) should equal (e^σ-1)/σ * I, error = {}",
1575            (v - expected).norm()
1576        );
1577    }
1578
1579    #[test]
1580    fn sim3_param_slice_round_trip() {
1581        let g = Sim3::random();
1582        let recovered = Sim3::from_param_slice(g.as_param_slice());
1583        assert!(g.is_approx(&recovered, 1e-14));
1584    }
1585
1586    #[test]
1587    fn sim3_tangent_slice_round_trip() {
1588        let t = Sim3Tangent::random();
1589        let recovered = Sim3Tangent::from_slice(t.as_slice());
1590        assert!(t.is_approx(&recovered, 1e-14));
1591    }
1592}
1593
1594#[cfg(test)]
1595mod regularized_inverse_tests {
1596    use super::*;
1597    use nalgebra::Matrix3;
1598
1599    #[test]
1600    fn regular_matrix_inverts_unchanged() {
1601        let m = Matrix3::new(2.0, 1.0, 0.0, 1.0, 3.0, 1.0, 0.0, 1.0, 2.0);
1602        let inv = regularized_inverse_3(&m, "test");
1603        let identity = m * inv;
1604        assert!((identity - Matrix3::identity()).norm() < 1e-12);
1605    }
1606
1607    #[test]
1608    fn singular_matrix_warns_and_regularizes() {
1609        // Rank-1 matrix: try_inverse returns None, the helper must NOT return
1610        // identity (the old silent corruption) — the regularized inverse keeps
1611        // the block's row structure.
1612        let singular = Matrix3::from_columns(&[
1613            Vector3::new(1.0, 2.0, 3.0),
1614            Vector3::new(2.0, 4.0, 6.0),
1615            Vector3::zeros(),
1616        ]);
1617        let inv = regularized_inverse_3(&singular, "test");
1618        assert!(
1619            (inv - Matrix3::identity()).norm() > 1e-3,
1620            "regularized inverse must not silently fall back to identity"
1621        );
1622    }
1623}
1624
1625/// Independent central-difference regression tests for ISSUE-0002 (Sim(3)
1626/// differential geometry). These build their own finite-difference oracle
1627/// from `exp()`/`matrix()`/`inverse()` alone — never from `log()`,
1628/// `right_jacobian_inv()`, `left_jacobian_inv()`, or `v_matrix()` — so a bug
1629/// shared between an analytic Jacobian and its "inverse" cannot hide here
1630/// (see `codex/issues/ISSUE-0002-sim3-differentials.md`, "Do not validate an
1631/// analytic Jacobian solely with an inverse built from that same Jacobian.").
1632#[cfg(test)]
1633mod sim3_jacobian_fd_tests {
1634    use super::*;
1635
1636    // `SMALL_ANGLE_THRESHOLD` (1e-10, compared against θ²) means a step below
1637    // ~1e-5 can leave a perturbed θ inside `v_matrix`'s small-angle branch,
1638    // silently discarding the perturbation and making the central difference
1639    // artificially zero — not a bug in the analytic Jacobian. 1e-4 clears
1640    // that threshold with comfortable margin while keeping truncation error
1641    // far below `FD_TOL`.
1642    const FD_STEP: f64 = 1e-4;
1643    const FD_TOL: f64 = 1e-5;
1644
1645    /// Extract [ρ,θ,σ] from a 4x4 Sim(3) Lie-algebra matrix
1646    /// `[θ× + σI, ρ; 0, 0]` (the "vee" map). Purely algebraic — no calls into
1647    /// this module's own `log`/`v_matrix`/Jacobian code.
1648    fn vee(m: &Matrix4<f64>) -> Vector7<f64> {
1649        let mut v = Vector7::zeros();
1650        v[0] = m[(0, 3)];
1651        v[1] = m[(1, 3)];
1652        v[2] = m[(2, 3)];
1653        v[3] = m[(2, 1)];
1654        v[4] = m[(0, 2)];
1655        v[5] = m[(1, 0)];
1656        v[6] = (m[(0, 0)] + m[(1, 1)] + m[(2, 2)]) / 3.0;
1657        v
1658    }
1659
1660    fn perturbed(xi: &Sim3Tangent, k: usize, eps: f64) -> Sim3Tangent {
1661        let mut data = xi.data;
1662        data[k] += eps;
1663        Sim3Tangent { data }
1664    }
1665
1666    /// Central-difference right Jacobian from first principles: column k is
1667    /// `vee(X(ξ)⁻¹ · dX/dξ_k)`, using only `exp()` and `matrix()`.
1668    fn fd_right_jacobian(xi: &Sim3Tangent) -> Matrix7<f64> {
1669        let x_inv_mat = xi.exp(None).inverse(None).matrix();
1670        let mut jac = Matrix7::zeros();
1671        for k in 0..7 {
1672            let plus = perturbed(xi, k, FD_STEP).exp(None).matrix();
1673            let minus = perturbed(xi, k, -FD_STEP).exp(None).matrix();
1674            let d_dxi_k = (plus - minus) / (2.0 * FD_STEP);
1675            jac.set_column(k, &vee(&(x_inv_mat * d_dxi_k)));
1676        }
1677        jac
1678    }
1679
1680    #[test]
1681    fn sim3_right_jacobian_matches_matrix_exp_central_difference() {
1682        let cases = [
1683            (
1684                Vector3::new(0.7, -0.2, 0.4),
1685                Vector3::new(0.4, -0.3, 0.25),
1686                0.3,
1687            ),
1688            (
1689                Vector3::new(0.1, 0.2, -0.3),
1690                Vector3::new(2.8, 0.1, -0.2),
1691                0.5,
1692            ), // near pi
1693            (Vector3::new(-0.5, 0.3, 0.2), Vector3::zeros(), 0.4), // theta = 0
1694            (
1695                Vector3::new(0.3, -0.1, 0.2),
1696                Vector3::new(0.2, 0.1, -0.1),
1697                0.0,
1698            ), // sigma = 0
1699            (
1700                Vector3::new(1e-8, -2e-8, 3e-8),
1701                Vector3::new(1e-8, 2e-8, -1e-8),
1702                1e-8,
1703            ), // joint small-angle/small-scale limit
1704        ];
1705        for (rho, theta, sigma) in cases {
1706            let xi = Sim3Tangent::new(rho, theta, sigma);
1707            let analytic = xi.right_jacobian();
1708            let numeric = fd_right_jacobian(&xi);
1709            let err = (analytic - numeric).norm();
1710            assert!(
1711                err < FD_TOL,
1712                "right_jacobian mismatch at rho={rho:?} theta={theta:?} sigma={sigma}: error={err}"
1713            );
1714        }
1715    }
1716
1717    #[test]
1718    fn sim3_left_jacobian_matches_matrix_exp_central_difference() {
1719        let xi = Sim3Tangent::new(
1720            Vector3::new(0.6, -0.3, 0.2),
1721            Vector3::new(-0.4, 0.5, 0.1),
1722            0.35,
1723        );
1724        let x_inv_mat = xi.exp(None).inverse(None).matrix();
1725
1726        let mut numeric = Matrix7::zeros();
1727        for k in 0..7 {
1728            let plus = perturbed(&xi, k, FD_STEP).exp(None).matrix();
1729            let minus = perturbed(&xi, k, -FD_STEP).exp(None).matrix();
1730            let d_dxi_k = (plus - minus) / (2.0 * FD_STEP);
1731            // Left Jacobian definition: dX/dξ_k = (Jl(ξ)e_k)^ · X(ξ).
1732            numeric.set_column(k, &vee(&(d_dxi_k * x_inv_mat)));
1733        }
1734
1735        let analytic = xi.left_jacobian();
1736        let err = (analytic - numeric).norm();
1737        assert!(err < FD_TOL, "left_jacobian mismatch: error={err}");
1738    }
1739
1740    #[test]
1741    fn sim3_act_jacobian_matches_finite_difference_at_nonunit_scale() {
1742        let sim3 = Sim3::new(
1743            Vector3::new(0.3, -0.2, 0.5),
1744            UnitQuaternion::from_euler_angles(0.2, -0.4, 0.3),
1745            1.7,
1746        );
1747        let p = Vector3::new(0.9, -1.1, 0.4);
1748
1749        let mut jac_self = Matrix7::zeros();
1750        let mut jac_point = Matrix3::zeros();
1751        sim3.act(&p, Some(&mut jac_self), Some(&mut jac_point));
1752
1753        for k in 0..3 {
1754            let mut p_plus = p;
1755            p_plus[k] += FD_STEP;
1756            let mut p_minus = p;
1757            p_minus[k] -= FD_STEP;
1758            let numeric =
1759                (sim3.act(&p_plus, None, None) - sim3.act(&p_minus, None, None)) / (2.0 * FD_STEP);
1760            let analytic = jac_point.column(k).into_owned();
1761            let err = (analytic - numeric).norm();
1762            assert!(err < FD_TOL, "act jacobian_vector col {k} mismatch: {err}");
1763        }
1764
1765        for k in 0..7 {
1766            let mut e = Vector7::zeros();
1767            e[k] = FD_STEP;
1768            let plus = sim3.right_plus(&Sim3Tangent { data: e }, None, None);
1769            e[k] = -FD_STEP;
1770            let minus = sim3.right_plus(&Sim3Tangent { data: e }, None, None);
1771            let numeric = (plus.act(&p, None, None) - minus.act(&p, None, None)) / (2.0 * FD_STEP);
1772            let analytic = Vector3::new(jac_self[(0, k)], jac_self[(1, k)], jac_self[(2, k)]);
1773            let err = (analytic - numeric).norm();
1774            assert!(err < FD_TOL, "act jacobian_self col {k} mismatch: {err}");
1775        }
1776    }
1777
1778    #[test]
1779    fn sim3_adjoint_matches_conjugation_finite_difference() {
1780        let cases = [
1781            (
1782                Vector3::new(0.8, -0.4, 0.6),
1783                Vector3::new(0.3, -0.2, 0.5),
1784                1.6,
1785            ), // general
1786            (Vector3::new(1.2, 0.5, -0.7), Vector3::zeros(), 1.0), // pure translation
1787            (Vector3::zeros(), Vector3::zeros(), 2.3),             // pure scale
1788        ];
1789        for (t, r, s) in cases {
1790            let x = Sim3::new(t, UnitQuaternion::from_euler_angles(r.x, r.y, r.z), s);
1791            let analytic = x.adjoint();
1792            let x_mat = x.matrix();
1793            let x_inv_mat = x.inverse(None).matrix();
1794
1795            let mut numeric = Matrix7::zeros();
1796            for k in 0..7 {
1797                let mut e_plus = Vector7::zeros();
1798                e_plus[k] = FD_STEP;
1799                let g_plus = (Sim3Tangent { data: e_plus }).exp(None).matrix();
1800                let mut e_minus = Vector7::zeros();
1801                e_minus[k] = -FD_STEP;
1802                let g_minus = (Sim3Tangent { data: e_minus }).exp(None).matrix();
1803
1804                let conj_plus = x_mat * g_plus * x_inv_mat;
1805                let conj_minus = x_mat * g_minus * x_inv_mat;
1806                let d = (conj_plus - conj_minus) / (2.0 * FD_STEP);
1807                numeric.set_column(k, &vee(&d));
1808            }
1809
1810            let err = (analytic - numeric).norm();
1811            assert!(
1812                err < FD_TOL,
1813                "adjoint mismatch at t={t:?} r={r:?} s={s}: error={err}"
1814            );
1815        }
1816    }
1817
1818    #[test]
1819    fn sim3_right_jacobian_continuous_near_zero_and_joint_limit() {
1820        // Sweep theta and sigma norms down toward zero (independently and
1821        // jointly) and assert the analytic Jacobian stays close to the
1822        // matrix-exp finite-difference oracle throughout — no discontinuity
1823        // at the small-angle boundary.
1824        for scale in [1e-1, 1e-3, 1e-5, 1e-7, 1e-9] {
1825            let xi = Sim3Tangent::new(
1826                Vector3::new(0.3, -0.2, 0.1) * scale,
1827                Vector3::new(0.2, 0.1, -0.3) * scale,
1828                0.15 * scale,
1829            );
1830            let analytic = xi.right_jacobian();
1831            let numeric = fd_right_jacobian(&xi);
1832            let err = (analytic - numeric).norm();
1833            assert!(err < FD_TOL, "discontinuity at scale={scale}: error={err}");
1834        }
1835    }
1836}