Skip to main content

apex_solver/factors/pose/
between.rs

1use crate::core::variable::ManifoldVariable;
2use crate::factors::Factor;
3use crate::factors::common::validate::expect_uniform_blocks;
4use apex_manifolds::{LieGroup, Tangent};
5use faer::prelude::ReborrowMut;
6
7/// The two Jacobian blocks of a between residual: `(∂r/∂k0, ∂r/∂k1)`.
8type BetweenBlocks<T> = (
9    <T as LieGroup>::JacobianMatrix,
10    <T as LieGroup>::JacobianMatrix,
11);
12
13/// Generic between factor for Lie group pose constraints.
14///
15/// Represents a relative pose measurement between two poses of any Lie group manifold type.
16/// This is a generic implementation that works with SE(2), SE(3), SO(2), SO(3), and Rⁿ
17/// using static dispatch for zero runtime overhead.
18///
19/// # Type Parameter
20///
21/// * `T` - The Lie group manifold type (e.g., SE2, SE3, SO2, SO3, Rn)
22///
23/// # Mathematical Formulation
24///
25/// Given two poses `T_i` and `T_j` in a Lie group, and a measurement `T_ij`, the residual is:
26///
27/// ```text
28/// r = log(T_ij⁻¹ ⊕ T_i⁻¹ ⊕ T_j)
29/// ```
30///
31/// where:
32/// - `⊕` is the Lie group composition operation
33/// - `log` is the logarithm map (converts from manifold to tangent space)
34/// - The residual dimensionality depends on the manifold's degrees of freedom (DOF)
35///
36/// # Residual Dimensions by Manifold Type
37///
38/// - **SE(3)**: 6D residual `[v_x, v_y, v_z, ω_x, ω_y, ω_z]` - translation + rotation
39/// - **SE(2)**: 3D residual `[dx, dy, dθ]` - 2D translation + rotation
40/// - **SO(3)**: 3D residual `[ω_x, ω_y, ω_z]` - 3D rotation only
41/// - **SO(2)**: 1D residual `[dθ]` - 2D rotation only
42/// - **Rⁿ**: nD residual - Euclidean space
43///
44/// # Jacobian Computation
45///
46/// The Jacobian is computed analytically using the chain rule and Lie group derivatives:
47///
48/// ```text
49/// J = ∂r/∂[T_i, T_j]
50/// ```
51///
52/// The Jacobian dimensions are `DOF × (2 × DOF)` where DOF is the manifold's degrees of freedom:
53/// - **SE(3)**: 6×12 matrix
54/// - **SE(2)**: 3×6 matrix
55/// - **SO(3)**: 3×6 matrix
56/// - **SO(2)**: 1×2 matrix
57///
58/// # Use Cases
59///
60/// - **3D SLAM**: Visual odometry, loop closure constraints (SE3)
61/// - **2D SLAM**: Robot navigation, mapping (SE2)
62/// - **Pose graph optimization**: Relative pose constraints (SE2, SE3)
63/// - **Orientation tracking**: IMU fusion, attitude estimation (SO2, SO3)
64/// - **General manifold optimization**: Custom manifolds (Rⁿ)
65///
66/// # Examples
67///
68/// ## SE(3) - 3D Pose Graph
69///
70/// ```
71/// use apex_solver::factors::Factor;
72/// use apex_solver::factors::pose::BetweenFactor;
73/// use apex_solver::manifold::se3::SE3;
74/// use nalgebra::{Vector3, Quaternion, DVector};
75///
76/// let relative_pose = SE3::from_translation_quaternion(
77///     Vector3::new(1.0, 0.0, 0.0),
78///     Quaternion::new(1.0, 0.0, 0.0, 0.0),
79/// );
80/// let between = BetweenFactor::new(relative_pose);
81///
82/// let pose_i = DVector::from_vec(vec![0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0]);
83/// let pose_j = DVector::from_vec(vec![0.95, 0.05, 0.0, 1.0, 0.0, 0.0, 0.0]);
84///
85/// let mut residual = vec![0.0f64; between.residual_dim()];
86/// let (rows, cols) = between.jacobian_shape();
87/// let mut jac_buf = vec![0.0f64; rows * cols];
88/// let jac_mut = faer::mat::MatMut::from_column_major_slice_mut(&mut jac_buf, rows, cols);
89/// between.linearize(&[pose_i.as_slice(), pose_j.as_slice()], &mut residual, Some(jac_mut));
90/// ```
91///
92/// ## SE(2) - 2D Pose Graph
93///
94/// ```
95/// use apex_solver::factors::Factor;
96/// use apex_solver::factors::pose::BetweenFactor;
97/// use apex_solver::manifold::se2::SE2;
98/// use nalgebra::DVector;
99///
100/// let relative_pose = SE2::from_xy_angle(1.0, 0.0, 0.1);
101/// let between = BetweenFactor::new(relative_pose);
102///
103/// let pose_i = DVector::from_vec(vec![0.0, 0.0, 0.0]);
104/// let pose_j = DVector::from_vec(vec![0.95, 0.05, 0.12]);
105///
106/// let mut residual = vec![0.0f64; between.residual_dim()];
107/// between.linearize(&[pose_i.as_slice(), pose_j.as_slice()], &mut residual, None);
108/// ```
109///
110/// # Performance
111///
112/// This generic implementation uses static dispatch (monomorphization), meaning:
113/// - **Zero runtime overhead** compared to type-specific implementations
114/// - Compiler optimizes each instantiation (`BetweenFactor<SE3>`, `BetweenFactor<SE2>`, etc.)
115/// - All type checking happens at compile time
116/// - No dynamic dispatch or virtual function calls
117#[derive(Debug, Clone, PartialEq)]
118pub struct BetweenFactor<T>
119where
120    T: LieGroup + Clone + Send + Sync,
121{
122    /// The measured relative pose transformation between the two connected poses
123    pub relative_pose: T,
124}
125
126impl<T> BetweenFactor<T>
127where
128    T: LieGroup + Clone + Send + Sync,
129{
130    /// Create a new between factor from a relative pose measurement.
131    ///
132    /// This is a generic constructor that works with any Lie group manifold type.
133    /// The type parameter `T` is typically inferred from the `relative_pose` argument.
134    ///
135    /// # Arguments
136    ///
137    /// * `relative_pose` - The measured relative transformation between two poses
138    ///
139    /// # Returns
140    ///
141    /// A new `BetweenFactor<T>` instance
142    ///
143    /// # Examples
144    ///
145    /// ## SE(3) Between Factor
146    ///
147    /// ```
148    /// use apex_solver::factors::pose::BetweenFactor;
149    /// use apex_solver::manifold::se3::SE3;
150    ///
151    /// // Create relative pose: move 2m in x, rotate 90° around z-axis
152    /// let relative = SE3::from_translation_euler(
153    ///     2.0, 0.0, 0.0,                      // translation (x, y, z)
154    ///     0.0, 0.0, std::f64::consts::FRAC_PI_2  // rotation (roll, pitch, yaw)
155    /// );
156    ///
157    /// // Type is inferred as BetweenFactor<SE3>
158    /// let factor = BetweenFactor::new(relative);
159    /// ```
160    ///
161    /// ## SE(2) Between Factor
162    ///
163    /// ```
164    /// use apex_solver::factors::pose::BetweenFactor;
165    /// use apex_solver::manifold::se2::SE2;
166    ///
167    /// // Create relative 2D pose
168    /// let relative = SE2::from_xy_angle(1.0, 0.5, 0.1);
169    ///
170    /// // Type is inferred as BetweenFactor<SE2>
171    /// let factor = BetweenFactor::new(relative);
172    /// ```
173    pub fn new(relative_pose: T) -> Self {
174        Self { relative_pose }
175    }
176
177    /// Shared residual/Jacobian chain: writes the residual and, when
178    /// `want_jacobian`, returns the `(∂r/∂k0, ∂r/∂k1)` blocks.
179    ///
180    /// Both entry points go through here so the prior and the between
181    /// constraint stay bit-for-bit consistent — same chain, same conventions.
182    fn evaluate(
183        &self,
184        k0: &[f64],
185        k1: &[f64],
186        residual: &mut [f64],
187        want_jacobian: bool,
188    ) -> (usize, Option<BetweenBlocks<T>>) {
189        let se3_origin_k0 = T::from_param_slice(k0);
190        let se3_origin_k1 = T::from_param_slice(k1);
191        let se3_k0_k1_measured = &self.relative_pose;
192
193        // Step 1: se3_origin_k1.between(se3_origin_k0) = k1⁻¹ * k0
194        let mut j_k1_k0_wrt_k1 = T::zero_jacobian();
195        let mut j_k1_k0_wrt_k0 = T::zero_jacobian();
196        let se3_k1_k0 = se3_origin_k1.between(
197            &se3_origin_k0,
198            Some(&mut j_k1_k0_wrt_k1),
199            Some(&mut j_k1_k0_wrt_k0),
200        );
201
202        // Step 2: se3_k1_k0 * se3_k0_k1_measured
203        let mut j_diff_wrt_k1_k0 = T::zero_jacobian();
204        let se3_diff = se3_k1_k0.compose(se3_k0_k1_measured, Some(&mut j_diff_wrt_k1_k0), None);
205
206        // Step 3: se3_diff.log()
207        let mut j_log_wrt_diff = T::zero_jacobian();
208        let tangent = se3_diff.log(Some(&mut j_log_wrt_diff));
209        let tangent_slice = tangent.as_slice();
210        let dof = tangent_slice.len();
211
212        residual[..dof].copy_from_slice(tangent_slice);
213
214        if !want_jacobian {
215            return (dof, None);
216        }
217
218        let j_diff_wrt_k0 = j_diff_wrt_k1_k0.clone() * j_k1_k0_wrt_k0;
219        let j_diff_wrt_k1 = j_diff_wrt_k1_k0 * j_k1_k0_wrt_k1;
220        (
221            dof,
222            Some((
223                j_log_wrt_diff.clone() * j_diff_wrt_k0,
224                j_log_wrt_diff * j_diff_wrt_k1,
225            )),
226        )
227    }
228
229    /// Residual, plus only the Jacobian block with respect to the **second**
230    /// variable, written straight into a `dof × dof` output.
231    ///
232    /// A prior anchors one variable against a fixed value, which is a between
233    /// constraint whose first argument is constant. Sharing the chain keeps the
234    /// conventions identical while letting the prior skip the `dof × 2·dof`
235    /// scratch buffer whose first half it would discard — `linearize` is on the
236    /// hot path of every solve and is documented as allocation-free.
237    pub(crate) fn linearize_wrt_second(
238        &self,
239        first: &[f64],
240        second: &[f64],
241        residual: &mut [f64],
242        jacobian: Option<faer::mat::MatMut<'_, f64>>,
243    ) {
244        let (dof, blocks) = self.evaluate(first, second, residual, jacobian.is_some());
245        if let (Some(mut jac), Some((_, jacobian_wrt_k1))) = (jacobian, blocks) {
246            for i in 0..dof {
247                for j in 0..dof {
248                    *jac.rb_mut().get_mut(i, j) = jacobian_wrt_k1[(i, j)];
249                }
250            }
251        }
252    }
253}
254
255impl<T> Factor for BetweenFactor<T>
256where
257    T: LieGroup + Clone + Send + Sync,
258{
259    fn linearize(
260        &self,
261        params: &[&[f64]],
262        residual: &mut [f64],
263        jacobian: Option<faer::mat::MatMut<'_, f64>>,
264    ) {
265        let (dof, blocks) = self.evaluate(params[0], params[1], residual, jacobian.is_some());
266
267        if let (Some(mut jac), Some((jacobian_wrt_k0, jacobian_wrt_k1))) = (jacobian, blocks) {
268            for i in 0..dof {
269                for j in 0..dof {
270                    *jac.rb_mut().get_mut(i, j) = jacobian_wrt_k0[(i, j)];
271                    *jac.rb_mut().get_mut(i, j + dof) = jacobian_wrt_k1[(i, j)];
272                }
273            }
274        }
275    }
276
277    fn residual_dim(&self) -> usize {
278        self.relative_pose.tangent_dim()
279    }
280
281    fn jacobian_shape(&self) -> (usize, usize) {
282        let dof = self.relative_pose.tangent_dim();
283        (dof, 2 * dof)
284    }
285
286    fn validate_variables(&self, variables: &[&dyn ManifoldVariable]) -> Result<(), String> {
287        // Both variables must be the same group as the measurement — the
288        // parameter length is what distinguishes e.g. SE3 (7) from SE23 (10).
289        let size = self.relative_pose.as_param_slice().len();
290        expect_uniform_blocks(
291            variables,
292            2,
293            size,
294            "BetweenFactor expects two variables of the measurement's own group",
295        )
296    }
297}
298
299#[cfg(test)]
300mod tests {
301    use super::*;
302    use apex_manifolds::rn::Rn;
303    use apex_manifolds::se2::{SE2, SE2Tangent};
304    use apex_manifolds::se3::SE3;
305    use apex_manifolds::so2::SO2;
306    use apex_manifolds::so3::SO3;
307    use nalgebra::{DMatrix, DVector, Quaternion, Vector3};
308
309    const TOLERANCE: f64 = 1e-9;
310    const FD_EPSILON: f64 = 1e-6;
311    type TestResult = Result<(), Box<dyn std::error::Error>>;
312
313    fn compute_residual<T>(
314        factor: &BetweenFactor<T>,
315        pose_i: &DVector<f64>,
316        pose_j: &DVector<f64>,
317    ) -> Vec<f64>
318    where
319        T: LieGroup + Clone + Send + Sync,
320    {
321        let mut residual = vec![0.0f64; factor.residual_dim()];
322        factor.linearize(&[pose_i.as_slice(), pose_j.as_slice()], &mut residual, None);
323        residual
324    }
325
326    fn compute_with_jacobian<T>(
327        factor: &BetweenFactor<T>,
328        pose_i: &DVector<f64>,
329        pose_j: &DVector<f64>,
330    ) -> (Vec<f64>, DMatrix<f64>)
331    where
332        T: LieGroup + Clone + Send + Sync,
333    {
334        let (rows, cols) = factor.jacobian_shape();
335        let mut residual = vec![0.0f64; rows];
336        let mut jac_buf = vec![0.0f64; rows * cols];
337        let jac_mut = faer::mat::MatMut::from_column_major_slice_mut(&mut jac_buf, rows, cols);
338        factor.linearize(
339            &[pose_i.as_slice(), pose_j.as_slice()],
340            &mut residual,
341            Some(jac_mut),
342        );
343        let jacobian = DMatrix::from_column_slice(rows, cols, &jac_buf);
344        (residual, jacobian)
345    }
346
347    /// `BetweenFactor<Rn>` is a documented supported combination, but `Rn` is
348    /// dynamically sized, so its Jacobian blocks must follow the element's
349    /// runtime dimension rather than a hardcoded 3×3.
350    ///
351    /// Regression for the dimension-mismatch panic inside `linearize`.
352    #[test]
353    fn test_between_factor_rn_jacobian_matches_dimension() -> TestResult {
354        for dim in [1usize, 2, 3, 4, 7] {
355            let relative = Rn::from_slice(&vec![0.25f64; dim]);
356            let factor = BetweenFactor::new(relative);
357
358            let pose_i = DVector::from_vec(vec![0.0f64; dim]);
359            let pose_j = DVector::from_vec(vec![1.0f64; dim]);
360
361            let (rows, cols) = factor.jacobian_shape();
362            assert_eq!(rows, dim, "residual rows for Rn({dim})");
363            assert_eq!(cols, 2 * dim, "jacobian cols for Rn({dim})");
364
365            let (residual, jacobian) = compute_with_jacobian(&factor, &pose_i, &pose_j);
366            assert_eq!(residual.len(), dim);
367            assert_eq!(jacobian.nrows(), dim);
368            assert_eq!(jacobian.ncols(), 2 * dim);
369            assert!(
370                jacobian.iter().all(|v| v.is_finite()),
371                "non-finite Jacobian entry for Rn({dim})"
372            );
373        }
374        Ok(())
375    }
376
377    /// The Rⁿ between-factor residual is `(x_i - x_j) + measurement`, and the
378    /// Jacobian blocks are ∓I. Verified against finite differences so the
379    /// dimension fix cannot silently produce a wrong-but-well-shaped block.
380    #[test]
381    fn test_between_factor_rn_jacobian_matches_finite_differences() -> TestResult {
382        for dim in [2usize, 4] {
383            let relative = Rn::from_slice(&vec![0.5f64; dim]);
384            let factor = BetweenFactor::new(relative);
385
386            let pose_i = DVector::from_vec((0..dim).map(|k| 0.1 * k as f64).collect::<Vec<_>>());
387            let pose_j =
388                DVector::from_vec((0..dim).map(|k| 1.0 - 0.2 * k as f64).collect::<Vec<_>>());
389
390            let (_, analytic) = compute_with_jacobian(&factor, &pose_i, &pose_j);
391
392            for (block, pose) in [(0usize, &pose_i), (1usize, &pose_j)] {
393                for c in 0..dim {
394                    let mut plus = pose.clone();
395                    let mut minus = pose.clone();
396                    plus[c] += FD_EPSILON;
397                    minus[c] -= FD_EPSILON;
398
399                    let (rp, rm) = if block == 0 {
400                        (
401                            compute_residual(&factor, &plus, &pose_j),
402                            compute_residual(&factor, &minus, &pose_j),
403                        )
404                    } else {
405                        (
406                            compute_residual(&factor, &pose_i, &plus),
407                            compute_residual(&factor, &pose_i, &minus),
408                        )
409                    };
410
411                    for r in 0..dim {
412                        let fd = (rp[r] - rm[r]) / (2.0 * FD_EPSILON);
413                        let an = analytic[(r, block * dim + c)];
414                        assert!(
415                            (fd - an).abs() < 1e-6,
416                            "Rn({dim}) block {block} entry ({r},{c}): analytic {an}, fd {fd}"
417                        );
418                    }
419                }
420            }
421        }
422        Ok(())
423    }
424
425    #[test]
426    fn test_between_factor_se2_identity() {
427        let relative = SE2::identity();
428        let factor = BetweenFactor::new(relative);
429
430        let pose_i = DVector::from_vec(vec![0.0, 0.0, 0.0]);
431        let pose_j = DVector::from_vec(vec![0.0, 0.0, 0.0]);
432
433        let residual = compute_residual(&factor, &pose_i, &pose_j);
434
435        assert_eq!(residual.len(), 3);
436        let norm: f64 = residual.iter().map(|x| x * x).sum::<f64>().sqrt();
437        assert!(norm < TOLERANCE, "Residual norm: {}", norm);
438    }
439
440    #[test]
441    fn test_between_factor_se3_identity() {
442        let relative = SE3::identity();
443        let factor = BetweenFactor::new(relative);
444
445        let pose_i = DVector::from_vec(vec![0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0]);
446        let pose_j = DVector::from_vec(vec![0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0]);
447
448        let residual = compute_residual(&factor, &pose_i, &pose_j);
449
450        assert_eq!(residual.len(), 6);
451        let norm: f64 = residual.iter().map(|x| x * x).sum::<f64>().sqrt();
452        assert!(norm < TOLERANCE, "Residual norm: {}", norm);
453    }
454
455    #[test]
456    fn test_between_factor_se2_jacobian_numerical() -> TestResult {
457        let relative = SE2::from_xy_angle(1.0, 0.0, 0.1);
458        let factor = BetweenFactor::new(relative);
459
460        let pose_i = DVector::from_vec(vec![0.0, 0.0, 0.0]);
461        let pose_j = DVector::from_vec(vec![0.95, 0.05, 0.12]);
462
463        let (residual, jacobian) = compute_with_jacobian(&factor, &pose_i, &pose_j);
464
465        assert_eq!(jacobian.nrows(), 3);
466        assert_eq!(jacobian.ncols(), 6);
467
468        let mut jacobian_fd = DMatrix::<f64>::zeros(3, 6);
469        let se2_i = SE2::from_param_slice(pose_i.as_slice());
470        let se2_j = SE2::from_param_slice(pose_j.as_slice());
471
472        for i in 0..3 {
473            let delta = match i {
474                0 => SE2Tangent::new(FD_EPSILON, 0.0, 0.0),
475                1 => SE2Tangent::new(0.0, FD_EPSILON, 0.0),
476                2 => SE2Tangent::new(0.0, 0.0, FD_EPSILON),
477                _ => unreachable!(),
478            };
479            let pose_i_p =
480                DVector::from_column_slice(se2_i.plus(&delta, None, None).as_param_slice());
481            let residual_p = compute_residual(&factor, &pose_i_p, &pose_j);
482            for j in 0..3 {
483                jacobian_fd[(j, i)] = (residual_p[j] - residual[j]) / FD_EPSILON;
484            }
485        }
486
487        for i in 0..3 {
488            let delta = match i {
489                0 => SE2Tangent::new(FD_EPSILON, 0.0, 0.0),
490                1 => SE2Tangent::new(0.0, FD_EPSILON, 0.0),
491                2 => SE2Tangent::new(0.0, 0.0, FD_EPSILON),
492                _ => unreachable!(),
493            };
494            let pose_j_p =
495                DVector::from_column_slice(se2_j.plus(&delta, None, None).as_param_slice());
496            let residual_p = compute_residual(&factor, &pose_i, &pose_j_p);
497            for j in 0..3 {
498                jacobian_fd[(j, i + 3)] = (residual_p[j] - residual[j]) / FD_EPSILON;
499            }
500        }
501
502        let diff_norm = (jacobian - jacobian_fd).norm();
503        assert!(diff_norm < 1e-5, "Jacobian difference norm: {}", diff_norm);
504        Ok(())
505    }
506
507    #[test]
508    fn test_between_factor_se3_jacobian_numerical() -> TestResult {
509        let relative = SE3::from_translation_quaternion(
510            Vector3::new(1.0, 0.0, 0.0),
511            Quaternion::new(1.0, 0.0, 0.0, 0.0),
512        );
513        let factor = BetweenFactor::new(relative);
514
515        let pose_i = DVector::from_vec(vec![0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0]);
516        let pose_j = DVector::from_vec(vec![0.95, 0.05, 0.0, 1.0, 0.0, 0.0, 0.0]);
517
518        let (residual, jacobian) = compute_with_jacobian(&factor, &pose_i, &pose_j);
519
520        assert_eq!(jacobian.nrows(), 6);
521        assert_eq!(jacobian.ncols(), 12);
522
523        let mut jacobian_fd = DMatrix::<f64>::zeros(6, 12);
524
525        for i in 0..3 {
526            let mut pose_i_p = pose_i.clone();
527            pose_i_p[i] += FD_EPSILON;
528            let residual_p = compute_residual(&factor, &pose_i_p, &pose_j);
529            for j in 0..6 {
530                jacobian_fd[(j, i)] = (residual_p[j] - residual[j]) / FD_EPSILON;
531            }
532        }
533
534        for i in 0..3 {
535            let mut pose_j_p = pose_j.clone();
536            pose_j_p[i] += FD_EPSILON;
537            let residual_p = compute_residual(&factor, &pose_i, &pose_j_p);
538            for j in 0..6 {
539                jacobian_fd[(j, i + 6)] = (residual_p[j] - residual[j]) / FD_EPSILON;
540            }
541        }
542
543        let diff_norm_trans = (jacobian.columns(0, 3) - jacobian_fd.columns(0, 3)).norm();
544        assert!(
545            diff_norm_trans < 1e-5,
546            "Jacobian difference norm (translation): {}",
547            diff_norm_trans
548        );
549        Ok(())
550    }
551
552    #[test]
553    fn test_between_factor_dimension_se2() -> TestResult {
554        let relative = SE2::from_xy_angle(1.0, 0.5, 0.1);
555        let factor = BetweenFactor::new(relative);
556
557        assert_eq!(factor.residual_dim(), 3);
558        assert_eq!(factor.jacobian_shape(), (3, 6));
559
560        let pose_i = DVector::from_vec(vec![0.0, 0.0, 0.0]);
561        let pose_j = DVector::from_vec(vec![1.0, 0.0, 0.0]);
562        let (residual, jacobian) = compute_with_jacobian(&factor, &pose_i, &pose_j);
563
564        assert_eq!(residual.len(), 3);
565        assert_eq!(jacobian.nrows(), 3);
566        assert_eq!(jacobian.ncols(), 6);
567        Ok(())
568    }
569
570    #[test]
571    fn test_between_factor_dimension_se3() -> TestResult {
572        let relative = SE3::identity();
573        let factor = BetweenFactor::new(relative);
574
575        assert_eq!(factor.residual_dim(), 6);
576        assert_eq!(factor.jacobian_shape(), (6, 12));
577
578        let pose_i = DVector::from_vec(vec![0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0]);
579        let pose_j = DVector::from_vec(vec![1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0]);
580        let (residual, jacobian) = compute_with_jacobian(&factor, &pose_i, &pose_j);
581
582        assert_eq!(residual.len(), 6);
583        assert_eq!(jacobian.nrows(), 6);
584        assert_eq!(jacobian.ncols(), 12);
585        Ok(())
586    }
587
588    #[test]
589    fn test_between_factor_so2_so3() -> TestResult {
590        let so2_relative = SO2::from_angle(0.1);
591        let so2_factor = BetweenFactor::new(so2_relative);
592
593        assert_eq!(so2_factor.residual_dim(), 1);
594        assert_eq!(so2_factor.jacobian_shape(), (1, 2));
595
596        let so2_i = DVector::from_vec(vec![0.0]);
597        let so2_j = DVector::from_vec(vec![0.12]);
598        let (res_so2, jac_so2) = compute_with_jacobian(&so2_factor, &so2_i, &so2_j);
599        assert_eq!(res_so2.len(), 1);
600        assert_eq!(jac_so2.nrows(), 1);
601        assert_eq!(jac_so2.ncols(), 2);
602
603        let so3_relative = SO3::identity();
604        let so3_factor = BetweenFactor::new(so3_relative);
605
606        let so3_i = DVector::from_vec(vec![1.0, 0.0, 0.0, 0.0]);
607        let so3_j = DVector::from_vec(vec![1.0, 0.0, 0.0, 0.0]);
608        let (res_so3, jac_so3) = compute_with_jacobian(&so3_factor, &so3_i, &so3_j);
609        assert_eq!(res_so3.len(), 3);
610        assert_eq!(jac_so3.nrows(), 3);
611        assert_eq!(jac_so3.ncols(), 6);
612        Ok(())
613    }
614
615    #[test]
616    fn test_between_factor_finiteness() -> TestResult {
617        let relative = SE2::from_xy_angle(100.0, -200.0, std::f64::consts::PI);
618        let factor = BetweenFactor::new(relative);
619
620        let pose_i = DVector::from_vec(vec![50.0, -100.0, 1.5]);
621        let pose_j = DVector::from_vec(vec![150.0, -300.0, -1.5]);
622
623        let (residual, jacobian) = compute_with_jacobian(&factor, &pose_i, &pose_j);
624
625        assert!(residual.iter().all(|x| x.is_finite()));
626        assert!(jacobian.iter().all(|x| x.is_finite()));
627        Ok(())
628    }
629
630    #[test]
631    fn test_between_factor_clone() {
632        let relative = SE3::identity();
633        let factor = BetweenFactor::new(relative);
634        let factor_clone = factor.clone();
635
636        let pose_i = DVector::from_vec(vec![0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0]);
637        let pose_j = DVector::from_vec(vec![1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0]);
638
639        let r1 = compute_residual(&factor, &pose_i, &pose_j);
640        let r2 = compute_residual(&factor_clone, &pose_i, &pose_j);
641
642        let diff: f64 = r1.iter().zip(r2.iter()).map(|(a, b)| (a - b).abs()).sum();
643        assert!(diff < TOLERANCE);
644    }
645}