Skip to main content

apex_solver/factors/
between_factor.rs

1use super::Factor;
2use apex_manifolds::{LieGroup, Tangent};
3use faer::prelude::ReborrowMut;
4
5/// Generic between factor for Lie group pose constraints.
6///
7/// Represents a relative pose measurement between two poses of any Lie group manifold type.
8/// This is a generic implementation that works with SE(2), SE(3), SO(2), SO(3), and Rⁿ
9/// using static dispatch for zero runtime overhead.
10///
11/// # Type Parameter
12///
13/// * `T` - The Lie group manifold type (e.g., SE2, SE3, SO2, SO3, Rn)
14///
15/// # Mathematical Formulation
16///
17/// Given two poses `T_i` and `T_j` in a Lie group, and a measurement `T_ij`, the residual is:
18///
19/// ```text
20/// r = log(T_ij⁻¹ ⊕ T_i⁻¹ ⊕ T_j)
21/// ```
22///
23/// where:
24/// - `⊕` is the Lie group composition operation
25/// - `log` is the logarithm map (converts from manifold to tangent space)
26/// - The residual dimensionality depends on the manifold's degrees of freedom (DOF)
27///
28/// # Residual Dimensions by Manifold Type
29///
30/// - **SE(3)**: 6D residual `[v_x, v_y, v_z, ω_x, ω_y, ω_z]` - translation + rotation
31/// - **SE(2)**: 3D residual `[dx, dy, dθ]` - 2D translation + rotation
32/// - **SO(3)**: 3D residual `[ω_x, ω_y, ω_z]` - 3D rotation only
33/// - **SO(2)**: 1D residual `[dθ]` - 2D rotation only
34/// - **Rⁿ**: nD residual - Euclidean space
35///
36/// # Jacobian Computation
37///
38/// The Jacobian is computed analytically using the chain rule and Lie group derivatives:
39///
40/// ```text
41/// J = ∂r/∂[T_i, T_j]
42/// ```
43///
44/// The Jacobian dimensions are `DOF × (2 × DOF)` where DOF is the manifold's degrees of freedom:
45/// - **SE(3)**: 6×12 matrix
46/// - **SE(2)**: 3×6 matrix
47/// - **SO(3)**: 3×6 matrix
48/// - **SO(2)**: 1×2 matrix
49///
50/// # Use Cases
51///
52/// - **3D SLAM**: Visual odometry, loop closure constraints (SE3)
53/// - **2D SLAM**: Robot navigation, mapping (SE2)
54/// - **Pose graph optimization**: Relative pose constraints (SE2, SE3)
55/// - **Orientation tracking**: IMU fusion, attitude estimation (SO2, SO3)
56/// - **General manifold optimization**: Custom manifolds (Rⁿ)
57///
58/// # Examples
59///
60/// ## SE(3) - 3D Pose Graph
61///
62/// ```
63/// use apex_solver::factors::{Factor, BetweenFactor};
64/// use apex_solver::manifold::se3::SE3;
65/// use nalgebra::{Vector3, Quaternion, DVector};
66///
67/// let relative_pose = SE3::from_translation_quaternion(
68///     Vector3::new(1.0, 0.0, 0.0),
69///     Quaternion::new(1.0, 0.0, 0.0, 0.0),
70/// );
71/// let between = BetweenFactor::new(relative_pose);
72///
73/// let pose_i = DVector::from_vec(vec![0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0]);
74/// let pose_j = DVector::from_vec(vec![0.95, 0.05, 0.0, 1.0, 0.0, 0.0, 0.0]);
75///
76/// let mut residual = vec![0.0f64; between.residual_dim()];
77/// let (rows, cols) = between.jacobian_shape();
78/// let mut jac_buf = vec![0.0f64; rows * cols];
79/// let jac_mut = faer::mat::MatMut::from_column_major_slice_mut(&mut jac_buf, rows, cols);
80/// between.linearize(&[pose_i.as_slice(), pose_j.as_slice()], &mut residual, Some(jac_mut));
81/// ```
82///
83/// ## SE(2) - 2D Pose Graph
84///
85/// ```
86/// use apex_solver::factors::{Factor, BetweenFactor};
87/// use apex_solver::manifold::se2::SE2;
88/// use nalgebra::DVector;
89///
90/// let relative_pose = SE2::from_xy_angle(1.0, 0.0, 0.1);
91/// let between = BetweenFactor::new(relative_pose);
92///
93/// let pose_i = DVector::from_vec(vec![0.0, 0.0, 0.0]);
94/// let pose_j = DVector::from_vec(vec![0.95, 0.05, 0.12]);
95///
96/// let mut residual = vec![0.0f64; between.residual_dim()];
97/// between.linearize(&[pose_i.as_slice(), pose_j.as_slice()], &mut residual, None);
98/// ```
99///
100/// # Performance
101///
102/// This generic implementation uses static dispatch (monomorphization), meaning:
103/// - **Zero runtime overhead** compared to type-specific implementations
104/// - Compiler optimizes each instantiation (`BetweenFactor<SE3>`, `BetweenFactor<SE2>`, etc.)
105/// - All type checking happens at compile time
106/// - No dynamic dispatch or virtual function calls
107#[derive(Clone, PartialEq)]
108pub struct BetweenFactor<T>
109where
110    T: LieGroup + Clone + Send + Sync,
111{
112    /// The measured relative pose transformation between the two connected poses
113    pub relative_pose: T,
114}
115
116impl<T> BetweenFactor<T>
117where
118    T: LieGroup + Clone + Send + Sync,
119{
120    /// Create a new between factor from a relative pose measurement.
121    ///
122    /// This is a generic constructor that works with any Lie group manifold type.
123    /// The type parameter `T` is typically inferred from the `relative_pose` argument.
124    ///
125    /// # Arguments
126    ///
127    /// * `relative_pose` - The measured relative transformation between two poses
128    ///
129    /// # Returns
130    ///
131    /// A new `BetweenFactor<T>` instance
132    ///
133    /// # Examples
134    ///
135    /// ## SE(3) Between Factor
136    ///
137    /// ```
138    /// use apex_solver::factors::BetweenFactor;
139    /// use apex_solver::manifold::se3::SE3;
140    ///
141    /// // Create relative pose: move 2m in x, rotate 90° around z-axis
142    /// let relative = SE3::from_translation_euler(
143    ///     2.0, 0.0, 0.0,                      // translation (x, y, z)
144    ///     0.0, 0.0, std::f64::consts::FRAC_PI_2  // rotation (roll, pitch, yaw)
145    /// );
146    ///
147    /// // Type is inferred as BetweenFactor<SE3>
148    /// let factor = BetweenFactor::new(relative);
149    /// ```
150    ///
151    /// ## SE(2) Between Factor
152    ///
153    /// ```
154    /// use apex_solver::factors::BetweenFactor;
155    /// use apex_solver::manifold::se2::SE2;
156    ///
157    /// // Create relative 2D pose
158    /// let relative = SE2::from_xy_angle(1.0, 0.5, 0.1);
159    ///
160    /// // Type is inferred as BetweenFactor<SE2>
161    /// let factor = BetweenFactor::new(relative);
162    /// ```
163    pub fn new(relative_pose: T) -> Self {
164        Self { relative_pose }
165    }
166}
167
168impl<T> Factor for BetweenFactor<T>
169where
170    T: LieGroup + Clone + Send + Sync,
171{
172    fn linearize(
173        &self,
174        params: &[&[f64]],
175        residual: &mut [f64],
176        jacobian: Option<faer::mat::MatMut<'_, f64>>,
177    ) {
178        let se3_origin_k0 = T::from_param_slice(params[0]);
179        let se3_origin_k1 = T::from_param_slice(params[1]);
180        let se3_k0_k1_measured = &self.relative_pose;
181
182        // Step 1: se3_origin_k1.between(se3_origin_k0) = k1⁻¹ * k0
183        let mut j_k1_k0_wrt_k1 = T::zero_jacobian();
184        let mut j_k1_k0_wrt_k0 = T::zero_jacobian();
185        let se3_k1_k0 = se3_origin_k1.between(
186            &se3_origin_k0,
187            Some(&mut j_k1_k0_wrt_k1),
188            Some(&mut j_k1_k0_wrt_k0),
189        );
190
191        // Step 2: se3_k1_k0 * se3_k0_k1_measured
192        let mut j_diff_wrt_k1_k0 = T::zero_jacobian();
193        let se3_diff = se3_k1_k0.compose(se3_k0_k1_measured, Some(&mut j_diff_wrt_k1_k0), None);
194
195        // Step 3: se3_diff.log()
196        let mut j_log_wrt_diff = T::zero_jacobian();
197        let tangent = se3_diff.log(Some(&mut j_log_wrt_diff));
198        let tangent_slice = tangent.as_slice();
199        let dof = tangent_slice.len();
200
201        residual[..dof].copy_from_slice(tangent_slice);
202
203        if let Some(mut jac) = jacobian {
204            let j_diff_wrt_k0 = j_diff_wrt_k1_k0.clone() * j_k1_k0_wrt_k0;
205            let j_diff_wrt_k1 = j_diff_wrt_k1_k0 * j_k1_k0_wrt_k1;
206            let jacobian_wrt_k0 = j_log_wrt_diff.clone() * j_diff_wrt_k0;
207            let jacobian_wrt_k1 = j_log_wrt_diff * j_diff_wrt_k1;
208
209            for i in 0..dof {
210                for j in 0..dof {
211                    *jac.rb_mut().get_mut(i, j) = jacobian_wrt_k0[(i, j)];
212                    *jac.rb_mut().get_mut(i, j + dof) = jacobian_wrt_k1[(i, j)];
213                }
214            }
215        }
216    }
217
218    fn residual_dim(&self) -> usize {
219        self.relative_pose.tangent_dim()
220    }
221
222    fn jacobian_shape(&self) -> (usize, usize) {
223        let dof = self.relative_pose.tangent_dim();
224        (dof, 2 * dof)
225    }
226}
227
228#[cfg(test)]
229mod tests {
230    use super::*;
231    use apex_manifolds::se2::{SE2, SE2Tangent};
232    use apex_manifolds::se3::SE3;
233    use apex_manifolds::so2::SO2;
234    use apex_manifolds::so3::SO3;
235    use nalgebra::{DMatrix, DVector, Quaternion, Vector3};
236
237    const TOLERANCE: f64 = 1e-9;
238    const FD_EPSILON: f64 = 1e-6;
239    type TestResult = Result<(), Box<dyn std::error::Error>>;
240
241    fn compute_residual<T>(
242        factor: &BetweenFactor<T>,
243        pose_i: &DVector<f64>,
244        pose_j: &DVector<f64>,
245    ) -> Vec<f64>
246    where
247        T: LieGroup + Clone + Send + Sync,
248    {
249        let mut residual = vec![0.0f64; factor.residual_dim()];
250        factor.linearize(&[pose_i.as_slice(), pose_j.as_slice()], &mut residual, None);
251        residual
252    }
253
254    fn compute_with_jacobian<T>(
255        factor: &BetweenFactor<T>,
256        pose_i: &DVector<f64>,
257        pose_j: &DVector<f64>,
258    ) -> (Vec<f64>, DMatrix<f64>)
259    where
260        T: LieGroup + Clone + Send + Sync,
261    {
262        let (rows, cols) = factor.jacobian_shape();
263        let mut residual = vec![0.0f64; rows];
264        let mut jac_buf = vec![0.0f64; rows * cols];
265        let jac_mut = faer::mat::MatMut::from_column_major_slice_mut(&mut jac_buf, rows, cols);
266        factor.linearize(
267            &[pose_i.as_slice(), pose_j.as_slice()],
268            &mut residual,
269            Some(jac_mut),
270        );
271        let jacobian = DMatrix::from_column_slice(rows, cols, &jac_buf);
272        (residual, jacobian)
273    }
274
275    #[test]
276    fn test_between_factor_se2_identity() {
277        let relative = SE2::identity();
278        let factor = BetweenFactor::new(relative);
279
280        let pose_i = DVector::from_vec(vec![0.0, 0.0, 0.0]);
281        let pose_j = DVector::from_vec(vec![0.0, 0.0, 0.0]);
282
283        let residual = compute_residual(&factor, &pose_i, &pose_j);
284
285        assert_eq!(residual.len(), 3);
286        let norm: f64 = residual.iter().map(|x| x * x).sum::<f64>().sqrt();
287        assert!(norm < TOLERANCE, "Residual norm: {}", norm);
288    }
289
290    #[test]
291    fn test_between_factor_se3_identity() {
292        let relative = SE3::identity();
293        let factor = BetweenFactor::new(relative);
294
295        let pose_i = DVector::from_vec(vec![0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0]);
296        let pose_j = DVector::from_vec(vec![0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0]);
297
298        let residual = compute_residual(&factor, &pose_i, &pose_j);
299
300        assert_eq!(residual.len(), 6);
301        let norm: f64 = residual.iter().map(|x| x * x).sum::<f64>().sqrt();
302        assert!(norm < TOLERANCE, "Residual norm: {}", norm);
303    }
304
305    #[test]
306    fn test_between_factor_se2_jacobian_numerical() -> TestResult {
307        let relative = SE2::from_xy_angle(1.0, 0.0, 0.1);
308        let factor = BetweenFactor::new(relative);
309
310        let pose_i = DVector::from_vec(vec![0.0, 0.0, 0.0]);
311        let pose_j = DVector::from_vec(vec![0.95, 0.05, 0.12]);
312
313        let (residual, jacobian) = compute_with_jacobian(&factor, &pose_i, &pose_j);
314
315        assert_eq!(jacobian.nrows(), 3);
316        assert_eq!(jacobian.ncols(), 6);
317
318        let mut jacobian_fd = DMatrix::<f64>::zeros(3, 6);
319        let se2_i = SE2::from_param_slice(pose_i.as_slice());
320        let se2_j = SE2::from_param_slice(pose_j.as_slice());
321
322        for i in 0..3 {
323            let delta = match i {
324                0 => SE2Tangent::new(FD_EPSILON, 0.0, 0.0),
325                1 => SE2Tangent::new(0.0, FD_EPSILON, 0.0),
326                2 => SE2Tangent::new(0.0, 0.0, FD_EPSILON),
327                _ => unreachable!(),
328            };
329            let pose_i_p =
330                DVector::from_column_slice(se2_i.plus(&delta, None, None).as_param_slice());
331            let residual_p = compute_residual(&factor, &pose_i_p, &pose_j);
332            for j in 0..3 {
333                jacobian_fd[(j, i)] = (residual_p[j] - residual[j]) / FD_EPSILON;
334            }
335        }
336
337        for i in 0..3 {
338            let delta = match i {
339                0 => SE2Tangent::new(FD_EPSILON, 0.0, 0.0),
340                1 => SE2Tangent::new(0.0, FD_EPSILON, 0.0),
341                2 => SE2Tangent::new(0.0, 0.0, FD_EPSILON),
342                _ => unreachable!(),
343            };
344            let pose_j_p =
345                DVector::from_column_slice(se2_j.plus(&delta, None, None).as_param_slice());
346            let residual_p = compute_residual(&factor, &pose_i, &pose_j_p);
347            for j in 0..3 {
348                jacobian_fd[(j, i + 3)] = (residual_p[j] - residual[j]) / FD_EPSILON;
349            }
350        }
351
352        let diff_norm = (jacobian - jacobian_fd).norm();
353        assert!(diff_norm < 1e-5, "Jacobian difference norm: {}", diff_norm);
354        Ok(())
355    }
356
357    #[test]
358    fn test_between_factor_se3_jacobian_numerical() -> TestResult {
359        let relative = SE3::from_translation_quaternion(
360            Vector3::new(1.0, 0.0, 0.0),
361            Quaternion::new(1.0, 0.0, 0.0, 0.0),
362        );
363        let factor = BetweenFactor::new(relative);
364
365        let pose_i = DVector::from_vec(vec![0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0]);
366        let pose_j = DVector::from_vec(vec![0.95, 0.05, 0.0, 1.0, 0.0, 0.0, 0.0]);
367
368        let (residual, jacobian) = compute_with_jacobian(&factor, &pose_i, &pose_j);
369
370        assert_eq!(jacobian.nrows(), 6);
371        assert_eq!(jacobian.ncols(), 12);
372
373        let mut jacobian_fd = DMatrix::<f64>::zeros(6, 12);
374
375        for i in 0..3 {
376            let mut pose_i_p = pose_i.clone();
377            pose_i_p[i] += FD_EPSILON;
378            let residual_p = compute_residual(&factor, &pose_i_p, &pose_j);
379            for j in 0..6 {
380                jacobian_fd[(j, i)] = (residual_p[j] - residual[j]) / FD_EPSILON;
381            }
382        }
383
384        for i in 0..3 {
385            let mut pose_j_p = pose_j.clone();
386            pose_j_p[i] += FD_EPSILON;
387            let residual_p = compute_residual(&factor, &pose_i, &pose_j_p);
388            for j in 0..6 {
389                jacobian_fd[(j, i + 6)] = (residual_p[j] - residual[j]) / FD_EPSILON;
390            }
391        }
392
393        let diff_norm_trans = (jacobian.columns(0, 3) - jacobian_fd.columns(0, 3)).norm();
394        assert!(
395            diff_norm_trans < 1e-5,
396            "Jacobian difference norm (translation): {}",
397            diff_norm_trans
398        );
399        Ok(())
400    }
401
402    #[test]
403    fn test_between_factor_dimension_se2() -> TestResult {
404        let relative = SE2::from_xy_angle(1.0, 0.5, 0.1);
405        let factor = BetweenFactor::new(relative);
406
407        assert_eq!(factor.residual_dim(), 3);
408        assert_eq!(factor.jacobian_shape(), (3, 6));
409
410        let pose_i = DVector::from_vec(vec![0.0, 0.0, 0.0]);
411        let pose_j = DVector::from_vec(vec![1.0, 0.0, 0.0]);
412        let (residual, jacobian) = compute_with_jacobian(&factor, &pose_i, &pose_j);
413
414        assert_eq!(residual.len(), 3);
415        assert_eq!(jacobian.nrows(), 3);
416        assert_eq!(jacobian.ncols(), 6);
417        Ok(())
418    }
419
420    #[test]
421    fn test_between_factor_dimension_se3() -> TestResult {
422        let relative = SE3::identity();
423        let factor = BetweenFactor::new(relative);
424
425        assert_eq!(factor.residual_dim(), 6);
426        assert_eq!(factor.jacobian_shape(), (6, 12));
427
428        let pose_i = DVector::from_vec(vec![0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0]);
429        let pose_j = DVector::from_vec(vec![1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0]);
430        let (residual, jacobian) = compute_with_jacobian(&factor, &pose_i, &pose_j);
431
432        assert_eq!(residual.len(), 6);
433        assert_eq!(jacobian.nrows(), 6);
434        assert_eq!(jacobian.ncols(), 12);
435        Ok(())
436    }
437
438    #[test]
439    fn test_between_factor_so2_so3() -> TestResult {
440        let so2_relative = SO2::from_angle(0.1);
441        let so2_factor = BetweenFactor::new(so2_relative);
442
443        assert_eq!(so2_factor.residual_dim(), 1);
444        assert_eq!(so2_factor.jacobian_shape(), (1, 2));
445
446        let so2_i = DVector::from_vec(vec![0.0]);
447        let so2_j = DVector::from_vec(vec![0.12]);
448        let (res_so2, jac_so2) = compute_with_jacobian(&so2_factor, &so2_i, &so2_j);
449        assert_eq!(res_so2.len(), 1);
450        assert_eq!(jac_so2.nrows(), 1);
451        assert_eq!(jac_so2.ncols(), 2);
452
453        let so3_relative = SO3::identity();
454        let so3_factor = BetweenFactor::new(so3_relative);
455
456        let so3_i = DVector::from_vec(vec![1.0, 0.0, 0.0, 0.0]);
457        let so3_j = DVector::from_vec(vec![1.0, 0.0, 0.0, 0.0]);
458        let (res_so3, jac_so3) = compute_with_jacobian(&so3_factor, &so3_i, &so3_j);
459        assert_eq!(res_so3.len(), 3);
460        assert_eq!(jac_so3.nrows(), 3);
461        assert_eq!(jac_so3.ncols(), 6);
462        Ok(())
463    }
464
465    #[test]
466    fn test_between_factor_finiteness() -> TestResult {
467        let relative = SE2::from_xy_angle(100.0, -200.0, std::f64::consts::PI);
468        let factor = BetweenFactor::new(relative);
469
470        let pose_i = DVector::from_vec(vec![50.0, -100.0, 1.5]);
471        let pose_j = DVector::from_vec(vec![150.0, -300.0, -1.5]);
472
473        let (residual, jacobian) = compute_with_jacobian(&factor, &pose_i, &pose_j);
474
475        assert!(residual.iter().all(|x| x.is_finite()));
476        assert!(jacobian.iter().all(|x| x.is_finite()));
477        Ok(())
478    }
479
480    #[test]
481    fn test_between_factor_clone() {
482        let relative = SE3::identity();
483        let factor = BetweenFactor::new(relative);
484        let factor_clone = factor.clone();
485
486        let pose_i = DVector::from_vec(vec![0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0]);
487        let pose_j = DVector::from_vec(vec![1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0]);
488
489        let r1 = compute_residual(&factor, &pose_i, &pose_j);
490        let r2 = compute_residual(&factor_clone, &pose_i, &pose_j);
491
492        let diff: f64 = r1.iter().zip(r2.iter()).map(|(a, b)| (a - b).abs()).sum();
493        assert!(diff < TOLERANCE);
494    }
495}