Skip to main content

apex_manifolds/
lib.rs

1//! Manifold representations for optimization on non-Euclidean spaces.
2//!
3//! This module provides manifold representations commonly used in computer vision and robotics:
4//! - **SE(3)**: Special Euclidean group (rigid body transformations)
5//! - **SO(3)**: Special Orthogonal group (rotations)
6//! - **Sim(3)**: Similarity transformations (rotation + translation + scale)
7//! - **SGal(3)**: Special Galilean group (rotation + translation + velocity + time)
8//! - **SE_2(3)**: Extended Special Euclidean group (rotation + translation + velocity)
9//! - **SE(2)**: Rigid transformations in 2D
10//! - **SO(2)**: Rotations in 2D
11//!
12//! ```text
13//! Lie group M,° | size   | dim | X ∈ M                   | Constraint      | T_E M             | T_X M                 | Exp(T)             | Comp. | Action
14//! ------------- | ------ | --- | ----------------------- | --------------- | ----------------- | --------------------- | ------------------ | ----- | ------
15//! n-D vector    | Rⁿ,+   | n   | n   | v ∈ Rⁿ            | |v-v|=0         | v ∈ Rⁿ            | v ∈ Rⁿ                | v = exp(v)         | v₁+v₂ | v + x
16//! Circle        | S¹,.   | 2   | 1   | z ∈ C             | z*z = 1         | iθ ∈ iR           | θ ∈ R                 | z = exp(iθ)        | z₁z₂  | zx
17//! `Rotation      | SO(2),.| 4   | 1   | R                 | RᵀR = I         | [θ]x ∈ so(2)      | [θ] ∈ R²              | R = exp([θ]x)      | R₁R₂  | Rx`
18//! `Rigid motion  | SE(2),.| 9   | 3   | M = [R t; 0 1]    | RᵀR = I         | [v̂] ∈ se(2)       | [v̂] ∈ R³              | Exp([v̂])           | M₁M₂  | Rx+t`
19//! 3-sphere      | S³,.   | 4   | 3   | q ∈ H             | q*q = 1         | θ/2 ∈ Hp          | θ ∈ R³                | q = exp(uθ/2)      | q₁q₂  | qxq*
20//! `Rotation      | SO(3),.| 9   | 3   | R                 | RᵀR = I         | [θ]x ∈ so(3)      | [θ] ∈ R³              | R = exp([θ]x)      | R₁R₂  | Rx`
21//! `Rigid motion  | SE(3),.| 16  | 6   | M = [R t; 0 1]    | RᵀR = I         | [v̂] ∈ se(3)       | [v̂] ∈ R⁶              | Exp([v̂])           | M₁M₂  | Rx+t`
22//! `Similarity    | Sim(3),.| 16 | 7   | M = [sR t; 0 1]   | RᵀR=I, s>0     | [v̂] ∈ sim(3)      | [ρ,θ,σ] ∈ R⁷          | Exp([v̂])           | M₁M₂  | sRx+t`
23//! `Galilean      | SGal(3),.| 25| 10  | (R,t,v,s)         | RᵀR = I         | [v̂] ∈ sgal(3)     | [ρ,ν,θ,s] ∈ R¹⁰       | Exp([v̂])           | M₁M₂  | Rx+t+sv`
24//! `Extended pose | SE_2(3),.| 25| 9   | (R,t,v)           | RᵀR = I         | [v̂] ∈ se_2_3      | [ρ,θ,ν] ∈ R⁹          | Exp([v̂])           | M₁M₂  | Rx+t`
25//! ```
26//!
27//! The design is inspired by the [manif](https://github.com/artivis/manif) C++ library
28//! and provides:
29//! - Analytic Jacobian computations for all operations
30//! - Right and left perturbation models
31//! - Composition and inverse operations
32//! - Exponential and logarithmic maps
33//! - Tangent space operations
34//!
35//! # Mathematical Background
36//!
37//! This module implements Lie group theory for robotics applications. Each manifold
38//! represents a Lie group with its associated tangent space (Lie algebra).
39//! Operations are differentiated with respect to perturbations on the local tangent space.
40//!
41
42use nalgebra::{Matrix3, Vector3};
43use std::ops::{Mul, Neg};
44
45/// Threshold for switching between exact formulas and Taylor approximations
46/// in small-angle computations.
47///
48/// `f64::EPSILON` (~2.2e-16) is too tight for small-angle detection — angles of ~1e-8 radians
49/// are well within Taylor approximation validity but would fall through to the exact formula
50/// path, where division by near-zero values causes numerical issues.
51///
52/// `1e-10` is chosen because:
53/// - Taylor expansions for sin(θ)/θ, (1-cos(θ))/θ² are accurate to ~1e-20 at this scale
54/// - Avoids catastrophic cancellation in exact formulas near zero
55/// - Consistent with production SLAM libraries (Sophus, GTSAM)
56///
57/// **Note:** This threshold is compared against `θ²` (not `θ`), so the effective angle
58/// threshold is `√(1e-10) ≈ 1e-5` radians (~0.00057°).
59pub const SMALL_ANGLE_THRESHOLD: f64 = 1e-10;
60
61pub mod rn;
62pub mod se2;
63pub mod se23;
64pub mod se3;
65pub mod sgal3;
66pub mod sim3;
67pub mod so2;
68pub mod so3;
69
70/// Errors that can occur during manifold operations.
71#[derive(Debug, Clone, PartialEq, thiserror::Error)]
72pub enum ManifoldError {
73    /// Invalid tangent vector dimension
74    #[error("Invalid tangent dimension: expected {expected}, got {actual}")]
75    InvalidTangentDimension { expected: usize, actual: usize },
76    /// Numerical instability in computation
77    #[error("Numerical instability: {0}")]
78    NumericalInstability(String),
79    /// Invalid manifold element
80    #[error("Invalid manifold element: {0}")]
81    InvalidElement(String),
82    /// Dimension validation failed during conversion
83    #[error("Dimension mismatch: expected {expected}, got {actual}")]
84    DimensionMismatch { expected: usize, actual: usize },
85    /// NaN or Inf detected in manifold element
86    #[error("Invalid number: NaN or Inf detected")]
87    InvalidNumber,
88    /// Normalization failed for manifold element
89    #[error("Normalization failed: {0}")]
90    NormalizationFailed(String),
91}
92
93#[derive(Debug, Clone, PartialEq)]
94pub enum ManifoldType {
95    RN,
96    SE2,
97    SE3,
98    SE23,
99    SGal3,
100    Sim3,
101    SO2,
102    SO3,
103}
104
105impl ManifoldType {
106    /// The manifold type whose [`LieGroup::NAME`] is `name` (`"Rn"`, `"SE3"`, …),
107    /// or `None` if the name does not match a known manifold.
108    pub fn from_name(name: &str) -> Option<Self> {
109        match name {
110            "Rn" => Some(Self::RN),
111            "SE2" => Some(Self::SE2),
112            "SE3" => Some(Self::SE3),
113            "SE23" => Some(Self::SE23),
114            "SGal3" => Some(Self::SGal3),
115            "Sim3" => Some(Self::Sim3),
116            "SO2" => Some(Self::SO2),
117            "SO3" => Some(Self::SO3),
118            _ => None,
119        }
120    }
121}
122
123/// Result type for manifold operations.
124pub type ManifoldResult<T> = Result<T, ManifoldError>;
125
126/// Core trait for Lie group operations.
127///
128/// Provides group operations, exponential/logarithmic maps, plus/minus with Jacobians,
129/// and adjoint representations.
130///
131/// # Associated Types
132///
133/// - `TangentVector`: Tangent space (Lie algebra) vector type
134/// - `JacobianMatrix`: Jacobian matrix type for this group
135/// - `LieAlgebra`: Matrix representation of the Lie algebra
136pub trait LieGroup: Clone + PartialEq {
137    /// Human-readable name for serialization and logging.
138    const NAME: &'static str;
139
140    /// The tangent space vector type
141    type TangentVector: Tangent<Self>;
142
143    /// The Jacobian matrix type
144    type JacobianMatrix: Clone
145        + PartialEq
146        + Neg<Output = Self::JacobianMatrix>
147        + Mul<Output = Self::JacobianMatrix>
148        + std::ops::Index<(usize, usize), Output = f64>;
149
150    /// Associated Lie algebra type
151    type LieAlgebra: Clone + PartialEq;
152
153    // Core group operations
154
155    /// Compute the inverse of this manifold element.
156    ///
157    /// For a group element g, returns g⁻¹ such that g ∘ g⁻¹ = e.
158    ///
159    /// # Arguments
160    /// * `jacobian` - Optional mutable reference to store the Jacobian ∂(g⁻¹)/∂g
161    fn inverse(&self, jacobian: Option<&mut Self::JacobianMatrix>) -> Self;
162
163    /// Compose this element with another (group multiplication).
164    ///
165    /// Computes g₁ ∘ g₂ where ∘ is the group operation.
166    ///
167    /// # Arguments
168    /// * `other` - The right operand for composition
169    /// * `jacobian_self` - Optional Jacobian ∂(g₁ ∘ g₂)/∂g₁
170    /// * `jacobian_other` - Optional Jacobian ∂(g₁ ∘ g₂)/∂g₂
171    fn compose(
172        &self,
173        other: &Self,
174        jacobian_self: Option<&mut Self::JacobianMatrix>,
175        jacobian_other: Option<&mut Self::JacobianMatrix>,
176    ) -> Self;
177
178    /// Logarithmic map from manifold to tangent space.
179    ///
180    /// Maps a group element g ∈ G to its tangent vector log(g)^∨ ∈ 𝔤.
181    ///
182    /// # Arguments
183    /// * `jacobian` - Optional Jacobian ∂log(g)^∨/∂g
184    fn log(&self, jacobian: Option<&mut Self::JacobianMatrix>) -> Self::TangentVector;
185
186    /// Vee operator: log(g)^∨.
187    ///
188    /// Maps a group element g ∈ G to its tangent vector log(g)^∨ ∈ 𝔤.
189    ///
190    /// # Arguments
191    /// * `jacobian` - Optional Jacobian ∂log(g)^∨/∂g
192    fn vee(&self) -> Self::TangentVector;
193
194    /// Act on a vector v: g ⊙ v.
195    ///
196    /// Group action on vectors (e.g., rotation for SO(3), transformation for SE(3)).
197    ///
198    /// # Arguments
199    /// * `vector` - Vector to transform
200    /// * `jacobian_self` - Optional Jacobian ∂(g ⊙ v)/∂g
201    /// * `jacobian_vector` - Optional Jacobian ∂(g ⊙ v)/∂v
202    fn act(
203        &self,
204        vector: &Vector3<f64>,
205        jacobian_self: Option<&mut Self::JacobianMatrix>,
206        jacobian_vector: Option<&mut Matrix3<f64>>,
207    ) -> Vector3<f64>;
208
209    // Adjoint operations
210
211    /// Adjoint matrix Ad(g).
212    ///
213    /// The adjoint representation maps the group to linear transformations
214    /// on the Lie algebra: Ad(g) φ = log(g ∘ exp(φ^∧) ∘ g⁻¹)^∨.
215    fn adjoint(&self) -> Self::JacobianMatrix;
216
217    // Utility operations
218
219    /// Generate a random element (useful for testing and initialization).
220    fn random() -> Self;
221
222    /// Get the identity matrix for Jacobians.
223    ///
224    /// Returns the identity matrix in the appropriate dimension for Jacobian computations.
225    /// This is used to initialize Jacobian matrices in optimization algorithms.
226    fn jacobian_identity() -> Self::JacobianMatrix;
227
228    /// Get a zero Jacobian matrix.
229    ///
230    /// Returns a zero matrix in the appropriate dimension for Jacobian computations.
231    /// This is used to initialize Jacobian matrices before optimization computations.
232    fn zero_jacobian() -> Self::JacobianMatrix;
233
234    /// Identity Jacobian sized for *this element's* tangent space.
235    ///
236    /// [`Self::jacobian_identity`] is an associated function, so it cannot see
237    /// the runtime dimension of a dynamically sized manifold — `Rn` has to
238    /// guess, and guesses wrong for every dimension but one. Any operation that
239    /// needs an identity block conformant with a concrete element must use this
240    /// method instead.
241    ///
242    /// The default forwards to [`Self::jacobian_identity`], which is correct for
243    /// every fixed-size group (SE2, SE3, SO2, SO3, Sim3, SE23, SGal3); `Rn`
244    /// overrides it with its runtime dimension.
245    fn jacobian_identity_for(&self) -> Self::JacobianMatrix {
246        Self::jacobian_identity()
247    }
248
249    /// Normalize/project the element to the manifold.
250    ///
251    /// Ensures the element satisfies manifold constraints (e.g., orthogonality for rotations).
252    fn normalize(&mut self);
253
254    /// Check if the element is approximately on the manifold.
255    fn is_valid(&self, tolerance: f64) -> bool;
256
257    /// Check if the element is approximately equal to another element.
258    ///
259    /// # Arguments
260    /// * `other` - The other element to compare with
261    /// * `tolerance` - The tolerance for the comparison
262    fn is_approx(&self, other: &Self, tolerance: f64) -> bool;
263
264    /// Returns the manifold parameters as a contiguous flat slice.
265    ///
266    /// Enables zero-copy faer views: `faer::col::from_slice(g.as_param_slice())`.
267    fn as_param_slice(&self) -> &[f64];
268
269    /// Mutably borrows the raw parameter storage for in-place retraction updates.
270    fn as_param_slice_mut(&mut self) -> &mut [f64];
271
272    /// Constructs from a raw parameter slice (same layout as `as_param_slice`).
273    fn from_param_slice(s: &[f64]) -> Self;
274
275    // Manifold plus/minus operations
276
277    /// Right plus operation: g ⊞ φ = g ∘ exp(φ^∧).
278    ///
279    /// Applies a tangent space perturbation to this manifold element.
280    ///
281    /// # Arguments
282    /// * `tangent` - Tangent vector perturbation
283    /// * `jacobian_self` - Optional Jacobian ∂(g ⊞ φ)/∂g
284    /// * `jacobian_tangent` - Optional Jacobian ∂(g ⊞ φ)/∂φ
285    ///
286    /// # Notes
287    /// # Equation 148:
288    /// J_R⊕θ_R = R(θ)ᵀ
289    /// J_R⊕θ_θ = J_r(θ)
290    fn right_plus(
291        &self,
292        tangent: &Self::TangentVector,
293        jacobian_self: Option<&mut Self::JacobianMatrix>,
294        jacobian_tangent: Option<&mut Self::JacobianMatrix>,
295    ) -> Self {
296        let exp_tangent = tangent.exp(None);
297
298        if let Some(jac_tangent) = jacobian_tangent {
299            *jac_tangent = tangent.right_jacobian();
300        }
301
302        self.compose(&exp_tangent, jacobian_self, None)
303    }
304
305    /// Right minus operation: g₁ ⊟ g₂ = log(g₂⁻¹ ∘ g₁)^∨.
306    ///
307    /// Computes the tangent vector that transforms g₂ to g₁.
308    ///
309    /// # Arguments
310    /// * `other` - The reference element g₂
311    /// * `jacobian_self` - Optional Jacobian ∂(g₁ ⊟ g₂)/∂g₁
312    /// * `jacobian_other` - Optional Jacobian ∂(g₁ ⊟ g₂)/∂g₂
313    ///
314    /// # Notes
315    /// # Equation 149:
316    /// J_Q⊖R_Q = J_r⁻¹(θ)
317    /// J_Q⊖R_R = -J_l⁻¹(θ)
318    fn right_minus(
319        &self,
320        other: &Self,
321        jacobian_self: Option<&mut Self::JacobianMatrix>,
322        jacobian_other: Option<&mut Self::JacobianMatrix>,
323    ) -> Self::TangentVector {
324        let other_inverse = other.inverse(None);
325        let result_group = other_inverse.compose(self, None, None);
326        let result = result_group.log(None);
327
328        if let Some(jac_self) = jacobian_self {
329            *jac_self = result.right_jacobian_inv();
330        }
331
332        if let Some(jac_other) = jacobian_other {
333            *jac_other = -result.left_jacobian_inv();
334        }
335
336        result
337    }
338
339    /// Left plus operation: φ ⊞ g = exp(φ^∧) ∘ g.
340    ///
341    /// # Arguments
342    /// * `tangent` - Tangent vector perturbation
343    /// * `jacobian_tangent` - Optional Jacobian ∂(φ ⊞ g)/∂φ
344    /// * `jacobian_self` - Optional Jacobian ∂(φ ⊞ g)/∂g
345    ///
346    /// # Notes
347    /// Inputs are perturbed on the right and the result is read in its own
348    /// right-local coordinates — the convention [`Self::right_plus`],
349    /// [`Self::compose`] and [`Self::between`] implement. Under it
350    /// `∂(φ ⊞ g)/∂g = I` and `∂(φ ⊞ g)/∂φ = Ad(g)⁻¹·J_r(φ)`; both are
351    /// checked against central differences in `tests/jacobian_identities.rs`.
352    fn left_plus(
353        &self,
354        tangent: &Self::TangentVector,
355        jacobian_tangent: Option<&mut Self::JacobianMatrix>,
356        jacobian_self: Option<&mut Self::JacobianMatrix>,
357    ) -> Self {
358        let exp_tangent = tangent.exp(None);
359        let result = exp_tangent.compose(self, None, None);
360
361        if let Some(jac_self) = jacobian_self {
362            // The result is `exp(φ)∘g`. Perturbing `g` on the right gives
363            // `exp(φ)∘(g·exp(δ)) = (exp(φ)∘g)·exp(δ)`, so in the result's
364            // right-local coordinates the Jacobian is the identity — the
365            // very value `compose` hands back for its `other` operand,
366            // which is what this computation bottoms out in. `Rn`'s
367            // override agrees.
368            //
369            // It used to return `self.adjoint()`, which is the identity
370            // under no convention: not `I` (right perturbation of the
371            // input, right-local output — the convention `compose`,
372            // `between` and `right_plus` implement, and the one
373            // `jacobian_tangent` below already uses), and not `Ad(g)⁻¹`
374            // either (left perturbation of the input).
375            *jac_self = self.jacobian_identity_for();
376        }
377
378        if let Some(jac_tangent) = jacobian_tangent {
379            *jac_tangent = self.inverse(None).adjoint() * tangent.right_jacobian();
380        }
381
382        result
383    }
384
385    /// Left minus operation: g₁ ⊟ g₂ = log(g₁ ∘ g₂⁻¹)^∨.
386    ///
387    /// # Arguments
388    /// * `other` - The reference element g₂
389    /// * `jacobian_self` - Optional Jacobian ∂(g₁ ⊟ g₂)/∂g₁
390    /// * `jacobian_other` - Optional Jacobian ∂(g₁ ⊟ g₂)/∂g₂
391    fn left_minus(
392        &self,
393        other: &Self,
394        jacobian_self: Option<&mut Self::JacobianMatrix>,
395        jacobian_other: Option<&mut Self::JacobianMatrix>,
396    ) -> Self::TangentVector {
397        let other_inverse = other.inverse(None);
398        let result_group = self.compose(&other_inverse, None, None);
399        let result = result_group.log(None);
400
401        if let Some(jac_self) = jacobian_self {
402            *jac_self = result.right_jacobian_inv() * other.adjoint();
403        }
404
405        if let Some(jac_other) = jacobian_other {
406            *jac_other = -(result.right_jacobian_inv() * other.adjoint());
407        }
408
409        result
410    }
411
412    // Convenience methods (use right operations by default)
413
414    /// Convenience method for right_plus. Equivalent to g ⊞ φ.
415    fn plus(
416        &self,
417        tangent: &Self::TangentVector,
418        jacobian_self: Option<&mut Self::JacobianMatrix>,
419        jacobian_tangent: Option<&mut Self::JacobianMatrix>,
420    ) -> Self {
421        self.right_plus(tangent, jacobian_self, jacobian_tangent)
422    }
423
424    /// Convenience method for right_minus. Equivalent to g₁ ⊟ g₂.
425    fn minus(
426        &self,
427        other: &Self,
428        jacobian_self: Option<&mut Self::JacobianMatrix>,
429        jacobian_other: Option<&mut Self::JacobianMatrix>,
430    ) -> Self::TangentVector {
431        self.right_minus(other, jacobian_self, jacobian_other)
432    }
433
434    // Additional operations
435
436    /// Compute g₁⁻¹ ∘ g₂ (relative transformation).
437    ///
438    /// # Arguments
439    /// * `other` - The target element g₂
440    /// * `jacobian_self` - Optional Jacobian with respect to g₁
441    /// * `jacobian_other` - Optional Jacobian with respect to g₂
442    fn between(
443        &self,
444        other: &Self,
445        jacobian_self: Option<&mut Self::JacobianMatrix>,
446        jacobian_other: Option<&mut Self::JacobianMatrix>,
447    ) -> Self {
448        let self_inverse = self.inverse(None);
449        let result = self_inverse.compose(other, None, None);
450
451        if let Some(jac_self) = jacobian_self {
452            *jac_self = -result.inverse(None).adjoint();
453        }
454
455        if let Some(jac_other) = jacobian_other {
456            // Sized from `self`, not from the type: `other` shares this
457            // element's tangent dimension, and for dynamically sized manifolds
458            // the type alone cannot supply it.
459            *jac_other = self.jacobian_identity_for();
460        }
461
462        result
463    }
464
465    /// Get the dimension of the tangent space for this manifold element.
466    ///
467    /// For most manifolds, this returns the compile-time constant from the TangentVector type.
468    /// For dynamically-sized manifolds like Rⁿ, this method should be overridden to return
469    /// the actual runtime dimension.
470    ///
471    /// # Returns
472    /// The dimension of the tangent space (degrees of freedom)
473    ///
474    /// # Default Implementation
475    /// Returns `Self::TangentVector::DIM` which works for fixed-size manifolds
476    /// (SE2=3, SE3=6, SO2=1, SO3=3).
477    fn tangent_dim(&self) -> usize {
478        Self::TangentVector::DIM
479    }
480}
481
482/// Trait for Lie algebra operations.
483///
484/// This trait provides operations for vectors in the Lie algebra of a Lie group,
485/// including vector space operations, adjoint actions, and conversions to matrix form.
486///
487/// # Type Parameters
488///
489/// - `G`: The associated Lie group type
490pub trait Tangent<Group: LieGroup>: Clone + PartialEq {
491    // Dimension constants
492
493    /// Dimension of the tangent space.
494    ///
495    /// For fixed-size manifolds (SE2, SE3, SO2, SO3), this is the compile-time constant.
496    /// For dynamic-size manifolds (Rn), this is `0` as a sentinel value — use the
497    /// `is_dynamic()` method to check, and the `LieGroup::tangent_dim()` instance method
498    /// to get the actual runtime dimension.
499    const DIM: usize;
500
501    /// Whether this tangent type has dynamic (runtime-determined) dimension.
502    ///
503    /// Returns `true` for `RnTangent` where `DIM == 0` is used as a sentinel.
504    /// Returns `false` for all fixed-size tangent types (SE2, SE3, SO2, SO3).
505    fn is_dynamic() -> bool {
506        Self::DIM == 0
507    }
508
509    // Exponential map and Jacobians
510
511    /// Exponential map to Lie group: exp(φ^∧).
512    ///
513    /// # Arguments
514    /// * `jacobian` - Optional Jacobian ∂exp(φ^∧)/∂φ
515    fn exp(&self, jacobian: Option<&mut Group::JacobianMatrix>) -> Group;
516
517    /// Right Jacobian Jr.
518    ///
519    /// Matrix Jr such that for small δφ:
520    /// exp((φ + δφ)^∧) ≈ exp(φ^∧) ∘ exp((Jr δφ)^∧)
521    fn right_jacobian(&self) -> Group::JacobianMatrix;
522
523    /// Left Jacobian Jl.
524    ///
525    /// Matrix Jl such that for small δφ:
526    /// exp((φ + δφ)^∧) ≈ exp((Jl δφ)^∧) ∘ exp(φ^∧)
527    fn left_jacobian(&self) -> Group::JacobianMatrix;
528
529    /// Inverse of right Jacobian Jr⁻¹.
530    fn right_jacobian_inv(&self) -> Group::JacobianMatrix;
531
532    /// Inverse of left Jacobian Jl⁻¹.
533    fn left_jacobian_inv(&self) -> Group::JacobianMatrix;
534
535    // Matrix representations
536
537    /// Hat operator: φ^∧ (vector to matrix).
538    ///
539    /// Maps the tangent vector to its matrix representation in the Lie algebra.
540    /// For SO(3): 3×1 vector → 3×3 skew-symmetric matrix
541    /// For SE(3): 6×1 vector → 4×4 transformation matrix
542    fn hat(&self) -> Group::LieAlgebra;
543
544    /// Small adjugate operator: adj(φ) = φ^∧.
545    ///
546    /// Maps the tangent vector to its matrix representation in the Lie algebra.
547    /// For SO(3): 3×1 vector → 3×3 skew-symmetric matrix
548    /// For SE(3): 6×1 vector → 4×4 transformation matrix
549    fn small_adj(&self) -> Group::JacobianMatrix;
550
551    /// Lie bracket: [φ, ψ] = φ ∘ ψ - ψ ∘ φ.
552    ///
553    /// Computes the Lie bracket of two tangent vectors in the Lie algebra.
554    /// For SO(3): 3×1 vector → 3×1 vector
555    /// For SE(3): 6×1 vector → 6×1 vector
556    fn lie_bracket(&self, other: &Self) -> Group::TangentVector;
557
558    /// Check if the tangent vector is approximately equal to another tangent vector.
559    ///
560    /// # Arguments
561    /// * `other` - The other tangent vector to compare with
562    /// * `tolerance` - The tolerance for the comparison
563    fn is_approx(&self, other: &Self, tolerance: f64) -> bool;
564
565    /// Get the i-th generator of the Lie algebra.
566    fn generator(&self, i: usize) -> Group::LieAlgebra;
567
568    // Utility functions
569
570    /// Zero tangent vector.
571    fn zero() -> Group::TangentVector;
572
573    /// Random tangent vector (useful for testing).
574    fn random() -> Group::TangentVector;
575
576    /// Check if the tangent vector is approximately zero.
577    fn is_zero(&self, tolerance: f64) -> bool;
578
579    /// Normalize the tangent vector to unit norm.
580    fn normalize(&mut self);
581
582    /// Return a unit tangent vector in the same direction.
583    fn normalized(&self) -> Group::TangentVector;
584
585    /// Borrows the raw tangent data as a flat slice. Zero allocation.
586    fn as_slice(&self) -> &[f64];
587
588    /// Constructs from a raw slice (same layout as `as_slice`).
589    fn from_slice(s: &[f64]) -> Self;
590}
591
592/// Trait for Lie groups that support interpolation.
593pub trait Interpolatable: LieGroup {
594    /// Linear interpolation in the manifold.
595    ///
596    /// For parameter t ∈ `[0,1]`: interp(g₁, g₂, 0) = g₁, interp(g₁, g₂, 1) = g₂.
597    ///
598    /// # Arguments
599    /// * `other` - Target element for interpolation
600    /// * `t` - Interpolation parameter in `[0,1]`
601    fn interp(&self, other: &Self, t: f64) -> Self;
602
603    /// Spherical linear interpolation (when applicable).
604    fn slerp(&self, other: &Self, t: f64) -> Self;
605}
606
607#[cfg(test)]
608mod tests {
609    use crate::LieGroup;
610    use crate::Tangent;
611    use crate::so2::{SO2, SO2Tangent};
612    use crate::so3::{SO3, SO3Tangent};
613    use crate::{ManifoldError, ManifoldType};
614    use nalgebra::Matrix1;
615
616    fn make_so2(angle: f64) -> SO2 {
617        SO2::from_angle(angle)
618    }
619
620    fn make_so2_tangent(angle: f64) -> SO2Tangent {
621        SO2Tangent::new(angle)
622    }
623
624    #[test]
625    fn manifold_error_display_invalid_tangent_dimension() {
626        let e = ManifoldError::InvalidTangentDimension {
627            expected: 3,
628            actual: 6,
629        };
630        let s = e.to_string();
631        assert!(s.contains("3"), "got: {s}");
632        assert!(s.contains("6"), "got: {s}");
633    }
634
635    #[test]
636    fn manifold_error_display_numerical_instability() {
637        let e = ManifoldError::NumericalInstability("singularity".to_string());
638        assert!(e.to_string().contains("singularity"));
639    }
640
641    #[test]
642    fn manifold_error_display_invalid_element() {
643        let e = ManifoldError::InvalidElement("bad quaternion".to_string());
644        assert!(e.to_string().contains("bad quaternion"));
645    }
646
647    #[test]
648    fn manifold_error_display_dimension_mismatch() {
649        let e = ManifoldError::DimensionMismatch {
650            expected: 4,
651            actual: 3,
652        };
653        let s = e.to_string();
654        assert!(s.contains("4") && s.contains("3"), "got: {s}");
655    }
656
657    #[test]
658    fn manifold_error_display_invalid_number() {
659        let e = ManifoldError::InvalidNumber;
660        assert!(!e.to_string().is_empty());
661    }
662
663    #[test]
664    fn manifold_error_display_normalization_failed() {
665        let e = ManifoldError::NormalizationFailed("zero vector".to_string());
666        assert!(e.to_string().contains("zero vector"));
667    }
668
669    #[test]
670    fn manifold_error_is_std_error() {
671        let e = ManifoldError::InvalidNumber;
672        let _: &dyn std::error::Error = &e;
673    }
674
675    #[test]
676    fn manifold_type_variants_are_distinct() {
677        let types = [
678            ManifoldType::RN,
679            ManifoldType::SE2,
680            ManifoldType::SE3,
681            ManifoldType::SE23,
682            ManifoldType::SGal3,
683            ManifoldType::Sim3,
684            ManifoldType::SO2,
685            ManifoldType::SO3,
686        ];
687        assert_eq!(types.len(), 8);
688        assert_eq!(ManifoldType::SO3, ManifoldType::SO3);
689        assert_ne!(ManifoldType::SO2, ManifoldType::SO3);
690    }
691
692    #[test]
693    fn default_right_plus_no_jacobians() {
694        let g = make_so2(0.3);
695        let t = make_so2_tangent(0.1);
696        let result = g.right_plus(&t, None, None);
697        assert!(result.is_valid(1e-9));
698    }
699
700    #[test]
701    fn default_right_plus_with_jacobians() {
702        let g = make_so2(0.3);
703        let t = make_so2_tangent(0.1);
704        let mut j_self = Matrix1::zeros();
705        let mut j_tan = Matrix1::zeros();
706        let result = g.right_plus(&t, Some(&mut j_self), Some(&mut j_tan));
707        assert!(result.is_valid(1e-9));
708        assert!(j_tan[0].is_finite());
709    }
710
711    #[test]
712    fn default_right_minus_no_jacobians() {
713        let g1 = make_so2(0.5);
714        let g2 = make_so2(0.2);
715        let _t = g1.right_minus(&g2, None, None);
716    }
717
718    #[test]
719    fn default_right_minus_with_jacobians() {
720        let g1 = make_so2(0.5);
721        let g2 = make_so2(0.2);
722        let mut j_self = Matrix1::zeros();
723        let mut j_other = Matrix1::zeros();
724        let _t = g1.right_minus(&g2, Some(&mut j_self), Some(&mut j_other));
725        assert!(j_self[0].is_finite());
726        assert!(j_other[0].is_finite());
727    }
728
729    #[test]
730    fn default_left_plus_no_jacobians() {
731        let g = make_so2(0.3);
732        let t = make_so2_tangent(0.1);
733        let result = g.left_plus(&t, None, None);
734        assert!(result.is_valid(1e-9));
735    }
736
737    #[test]
738    fn default_left_plus_with_jacobians() {
739        let g = make_so2(0.3);
740        let t = make_so2_tangent(0.1);
741        let mut j_tan = Matrix1::zeros();
742        let mut j_self = Matrix1::zeros();
743        let result = g.left_plus(&t, Some(&mut j_tan), Some(&mut j_self));
744        assert!(result.is_valid(1e-9));
745        assert!(j_tan[0].is_finite());
746        assert!(j_self[0].is_finite());
747    }
748
749    #[test]
750    fn default_left_minus_no_jacobians() {
751        let g1 = make_so2(0.5);
752        let g2 = make_so2(0.2);
753        let _t = g1.left_minus(&g2, None, None);
754    }
755
756    #[test]
757    fn default_left_minus_with_jacobians() {
758        let g1 = make_so2(0.5);
759        let g2 = make_so2(0.2);
760        let mut j_self = Matrix1::zeros();
761        let mut j_other = Matrix1::zeros();
762        let _t = g1.left_minus(&g2, Some(&mut j_self), Some(&mut j_other));
763        assert!(j_self[0].is_finite());
764        assert!(j_other[0].is_finite());
765    }
766
767    #[test]
768    fn default_plus_delegates_to_right_plus() {
769        let g = make_so2(0.3);
770        let t = make_so2_tangent(0.1);
771        let r1 = g.plus(&t, None, None);
772        let r2 = g.right_plus(&t, None, None);
773        assert!(r1.is_approx(&r2, 1e-9));
774    }
775
776    #[test]
777    fn default_minus_delegates_to_right_minus() {
778        let g1 = make_so2(0.5);
779        let g2 = make_so2(0.2);
780        let t1 = g1.minus(&g2, None, None);
781        let t2 = g1.right_minus(&g2, None, None);
782        assert!(t1.is_approx(&t2, 1e-9));
783    }
784
785    #[test]
786    fn default_between_no_jacobians() {
787        let g1 = make_so2(0.3);
788        let g2 = make_so2(0.7);
789        let b = g1.between(&g2, None, None);
790        assert!(b.is_valid(1e-9));
791    }
792
793    #[test]
794    fn default_between_with_jacobians() {
795        let g1 = make_so2(0.3);
796        let g2 = make_so2(0.7);
797        let mut j_self = Matrix1::zeros();
798        let mut j_other = Matrix1::zeros();
799        let b = g1.between(&g2, Some(&mut j_self), Some(&mut j_other));
800        assert!(b.is_valid(1e-9));
801        assert!(j_self[0].is_finite());
802        assert!(j_other[0].is_finite());
803    }
804
805    #[test]
806    fn default_tangent_dim_returns_dof() {
807        let g = make_so2(0.0);
808        assert_eq!(g.tangent_dim(), 1); // SO2 has 1 DOF
809    }
810
811    #[test]
812    fn tangent_is_dynamic_false_for_so2() {
813        assert!(!SO2Tangent::is_dynamic());
814    }
815
816    #[test]
817    fn manifold_error_clone_and_partial_eq() {
818        let e = ManifoldError::InvalidTangentDimension {
819            expected: 1,
820            actual: 2,
821        };
822        let e2 = e.clone();
823        assert_eq!(e, e2);
824
825        let e3 = ManifoldError::NumericalInstability("x".to_string());
826        let e4 = e3.clone();
827        assert_eq!(e3, e4);
828
829        let e5 = ManifoldError::InvalidElement("y".to_string());
830        let e6 = e5.clone();
831        assert_eq!(e5, e6);
832
833        let e7 = ManifoldError::DimensionMismatch {
834            expected: 3,
835            actual: 4,
836        };
837        let e8 = e7.clone();
838        assert_eq!(e7, e8);
839
840        let e9 = ManifoldError::InvalidNumber;
841        let e10 = e9.clone();
842        assert_eq!(e9, e10);
843
844        let e11 = ManifoldError::NormalizationFailed("z".to_string());
845        let e12 = e11.clone();
846        assert_eq!(e11, e12);
847    }
848
849    #[test]
850    fn manifold_type_clone_and_eq() {
851        let all_types = [
852            ManifoldType::RN,
853            ManifoldType::SE2,
854            ManifoldType::SE3,
855            ManifoldType::SE23,
856            ManifoldType::SGal3,
857            ManifoldType::Sim3,
858            ManifoldType::SO2,
859            ManifoldType::SO3,
860        ];
861        for t in &all_types {
862            let t2 = t.clone();
863            assert_eq!(t, &t2);
864        }
865        // Ensure different variants are not equal
866        assert_ne!(ManifoldType::RN, ManifoldType::SE2);
867        assert_ne!(ManifoldType::SE2, ManifoldType::SE3);
868        assert_ne!(ManifoldType::SE3, ManifoldType::SE23);
869        assert_ne!(ManifoldType::SE23, ManifoldType::SGal3);
870        assert_ne!(ManifoldType::SGal3, ManifoldType::Sim3);
871        assert_ne!(ManifoldType::Sim3, ManifoldType::SO2);
872        assert_ne!(ManifoldType::SO2, ManifoldType::SO3);
873    }
874
875    #[test]
876    fn manifold_type_debug() {
877        let s = format!("{:?}", ManifoldType::RN);
878        assert!(!s.is_empty());
879        let s2 = format!("{:?}", ManifoldType::SE23);
880        assert!(!s2.is_empty());
881        let s3 = format!("{:?}", ManifoldType::SGal3);
882        assert!(!s3.is_empty());
883        let s4 = format!("{:?}", ManifoldType::Sim3);
884        assert!(!s4.is_empty());
885    }
886
887    #[test]
888    fn manifold_error_debug() {
889        let e = ManifoldError::InvalidNumber;
890        let s = format!("{e:?}");
891        assert!(!s.is_empty());
892    }
893
894    // Test SO3-based defaults to exercise more code paths
895
896    #[test]
897    fn so3_default_right_plus_with_jacobians() {
898        use crate::LieGroup;
899        let r = SO3::from_euler_angles(0.1, 0.2, 0.3);
900        let t = SO3Tangent::new(nalgebra::Vector3::new(0.05, 0.0, 0.0));
901        let mut j_self = nalgebra::Matrix3::zeros();
902        let mut j_tan = nalgebra::Matrix3::zeros();
903        let result = r.right_plus(&t, Some(&mut j_self), Some(&mut j_tan));
904        assert!(result.is_valid(1e-6));
905        assert!(j_self[(0, 0)].is_finite());
906        assert!(j_tan[(0, 0)].is_finite());
907    }
908
909    #[test]
910    fn so3_default_left_plus_with_jacobians() {
911        use crate::LieGroup;
912        let r = SO3::from_euler_angles(0.1, 0.2, 0.3);
913        let t = SO3Tangent::new(nalgebra::Vector3::new(0.05, 0.0, 0.0));
914        let mut j_tan = nalgebra::Matrix3::zeros();
915        let mut j_self = nalgebra::Matrix3::zeros();
916        let result = r.left_plus(&t, Some(&mut j_tan), Some(&mut j_self));
917        assert!(result.is_valid(1e-6));
918        assert!(j_tan[(0, 0)].is_finite());
919        assert!(j_self[(0, 0)].is_finite());
920    }
921
922    #[test]
923    fn so3_default_right_minus_with_jacobians() {
924        use crate::LieGroup;
925        let r1 = SO3::from_euler_angles(0.3, 0.1, 0.2);
926        let r2 = SO3::from_euler_angles(0.1, 0.0, 0.1);
927        let mut j_self = nalgebra::Matrix3::zeros();
928        let mut j_other = nalgebra::Matrix3::zeros();
929        let _t = r1.right_minus(&r2, Some(&mut j_self), Some(&mut j_other));
930        assert!(j_self[(0, 0)].is_finite());
931        assert!(j_other[(0, 0)].is_finite());
932    }
933
934    #[test]
935    fn so3_default_left_minus_with_jacobians() {
936        use crate::LieGroup;
937        let r1 = SO3::from_euler_angles(0.3, 0.1, 0.2);
938        let r2 = SO3::from_euler_angles(0.1, 0.0, 0.1);
939        let mut j_self = nalgebra::Matrix3::zeros();
940        let mut j_other = nalgebra::Matrix3::zeros();
941        let _t = r1.left_minus(&r2, Some(&mut j_self), Some(&mut j_other));
942        assert!(j_self[(0, 0)].is_finite());
943        assert!(j_other[(0, 0)].is_finite());
944    }
945
946    #[test]
947    fn so3_default_between_with_jacobians() {
948        use crate::LieGroup;
949        let r1 = SO3::from_euler_angles(0.1, 0.2, 0.3);
950        let r2 = SO3::from_euler_angles(0.4, 0.1, 0.2);
951        let mut j_self = nalgebra::Matrix3::zeros();
952        let mut j_other = nalgebra::Matrix3::zeros();
953        let b = r1.between(&r2, Some(&mut j_self), Some(&mut j_other));
954        assert!(b.is_valid(1e-6));
955        assert!(j_self[(0, 0)].is_finite());
956        assert!(j_other[(0, 0)].is_finite());
957    }
958
959    #[test]
960    fn so3_default_tangent_dim() {
961        use crate::LieGroup;
962        let r = SO3::identity();
963        assert_eq!(r.tangent_dim(), 3);
964    }
965}