apex_solver/factors/between_factor.rs
1use super::Factor;
2use crate::manifold::LieGroup;
3use nalgebra::{DMatrix, DVector};
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/// // Measurement: relative 3D transformation between two poses
68/// let relative_pose = SE3::from_translation_quaternion(
69/// Vector3::new(1.0, 0.0, 0.0), // 1m forward
70/// Quaternion::new(1.0, 0.0, 0.0, 0.0) // No rotation
71/// );
72/// let between = BetweenFactor::new(relative_pose);
73///
74/// // Current pose estimates (in [tx, ty, tz, qw, qx, qy, qz] format)
75/// let pose_i = DVector::from_vec(vec![0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0]);
76/// let pose_j = DVector::from_vec(vec![0.95, 0.05, 0.0, 1.0, 0.0, 0.0, 0.0]);
77///
78/// // Compute residual (dimension 6) and Jacobian (6×12)
79/// let (residual, jacobian) = between.linearize(&[pose_i, pose_j], true);
80/// ```
81///
82/// ## SE(2) - 2D Pose Graph
83///
84/// ```
85/// use apex_solver::factors::{Factor, BetweenFactor};
86/// use apex_solver::manifold::se2::SE2;
87/// use nalgebra::DVector;
88///
89/// // Measurement: robot moved 1m forward and rotated 0.1 rad
90/// let relative_pose = SE2::from_xy_angle(1.0, 0.0, 0.1);
91/// let between = BetweenFactor::new(relative_pose);
92///
93/// // Current pose estimates (in [x, y, theta] format)
94/// let pose_i = DVector::from_vec(vec![0.0, 0.0, 0.0]);
95/// let pose_j = DVector::from_vec(vec![0.95, 0.05, 0.12]);
96///
97/// // Compute residual (dimension 3) and Jacobian (3×6)
98/// let (residual, jacobian) = between.linearize(&[pose_i, pose_j], true);
99/// ```
100///
101/// # Performance
102///
103/// This generic implementation uses static dispatch (monomorphization), meaning:
104/// - **Zero runtime overhead** compared to type-specific implementations
105/// - Compiler optimizes each instantiation (`BetweenFactor<SE3>`, `BetweenFactor<SE2>`, etc.)
106/// - All type checking happens at compile time
107/// - No dynamic dispatch or virtual function calls
108#[derive(Clone, PartialEq)]
109pub struct BetweenFactor<T>
110where
111 T: LieGroup + Clone + Send + Sync,
112 T::TangentVector: Into<DVector<f64>>,
113{
114 /// The measured relative pose transformation between the two connected poses
115 pub relative_pose: T,
116}
117
118impl<T> BetweenFactor<T>
119where
120 T: LieGroup + Clone + Send + Sync,
121 T::TangentVector: Into<DVector<f64>>,
122{
123 /// Create a new between factor from a relative pose measurement.
124 ///
125 /// This is a generic constructor that works with any Lie group manifold type.
126 /// The type parameter `T` is typically inferred from the `relative_pose` argument.
127 ///
128 /// # Arguments
129 ///
130 /// * `relative_pose` - The measured relative transformation between two poses
131 ///
132 /// # Returns
133 ///
134 /// A new `BetweenFactor<T>` instance
135 ///
136 /// # Examples
137 ///
138 /// ## SE(3) Between Factor
139 ///
140 /// ```
141 /// use apex_solver::factors::BetweenFactor;
142 /// use apex_solver::manifold::se3::SE3;
143 ///
144 /// // Create relative pose: move 2m in x, rotate 90° around z-axis
145 /// let relative = SE3::from_translation_euler(
146 /// 2.0, 0.0, 0.0, // translation (x, y, z)
147 /// 0.0, 0.0, std::f64::consts::FRAC_PI_2 // rotation (roll, pitch, yaw)
148 /// );
149 ///
150 /// // Type is inferred as BetweenFactor<SE3>
151 /// let factor = BetweenFactor::new(relative);
152 /// ```
153 ///
154 /// ## SE(2) Between Factor
155 ///
156 /// ```
157 /// use apex_solver::factors::BetweenFactor;
158 /// use apex_solver::manifold::se2::SE2;
159 ///
160 /// // Create relative 2D pose
161 /// let relative = SE2::from_xy_angle(1.0, 0.5, 0.1);
162 ///
163 /// // Type is inferred as BetweenFactor<SE2>
164 /// let factor = BetweenFactor::new(relative);
165 /// ```
166 pub fn new(relative_pose: T) -> Self {
167 Self { relative_pose }
168 }
169}
170
171impl<T> Factor for BetweenFactor<T>
172where
173 T: LieGroup + Clone + Send + Sync + From<DVector<f64>>,
174 T::TangentVector: Into<DVector<f64>>,
175{
176 /// Compute residual and Jacobian for a generic between factor.
177 ///
178 /// This method works with any Lie group manifold type, automatically adapting to
179 /// the manifold's degrees of freedom. The residual and Jacobian dimensions are
180 /// determined at runtime based on the manifold type.
181 ///
182 /// # Arguments
183 ///
184 /// * `params` - Two poses as `DVector<f64>` in the manifold's representation format:
185 /// - **SE(3)**: `[tx, ty, tz, qw, qx, qy, qz]` (7 parameters, 6 DOF)
186 /// - **SE(2)**: `[x, y, theta]` (3 parameters, 3 DOF)
187 /// - **SO(3)**: `[qw, qx, qy, qz]` (4 parameters, 3 DOF)
188 /// - **SO(2)**: `[angle]` (1 parameter, 1 DOF)
189 /// * `compute_jacobian` - Whether to compute the analytical Jacobian matrix
190 ///
191 /// # Returns
192 ///
193 /// A tuple `(residual, jacobian)` where:
194 /// - **Residual**: `DVector<f64>` with dimension = manifold DOF
195 /// - SE(3): 6×1 vector `[v_x, v_y, v_z, ω_x, ω_y, ω_z]`
196 /// - SE(2): 3×1 vector `[dx, dy, dθ]`
197 /// - SO(3): 3×1 vector `[ω_x, ω_y, ω_z]`
198 /// - SO(2): 1×1 vector `[dθ]`
199 /// - **Jacobian**: `Option<DMatrix<f64>>` with dimension = (DOF, 2×DOF)
200 /// - SE(3): 6×12 matrix `[∂r/∂pose_i | ∂r/∂pose_j]`
201 /// - SE(2): 3×6 matrix `[∂r/∂pose_i | ∂r/∂pose_j]`
202 /// - SO(3): 3×6 matrix `[∂r/∂pose_i | ∂r/∂pose_j]`
203 /// - SO(2): 1×2 matrix `[∂r/∂pose_i | ∂r/∂pose_j]`
204 ///
205 /// # Algorithm
206 ///
207 /// Uses analytical Jacobians computed via chain rule through three steps:
208 /// 1. **Between**: `T_j.between(T_i) = T_j⁻¹ ⊕ T_i` with Jacobians ∂/∂T_j and ∂/∂T_i
209 /// 2. **Composition**: `(T_j⁻¹ ⊕ T_i) ⊕ T_ij` with Jacobian ∂/∂(T_j⁻¹ ⊕ T_i)
210 /// 3. **Logarithm**: `log(...)` with Jacobian ∂log/∂(...)
211 ///
212 /// The final Jacobian is computed using the chain rule:
213 /// ```text
214 /// J = ∂log/∂diff · ∂diff/∂between · ∂between/∂poses
215 /// ```
216 ///
217 /// This approach reduces the number of matrix operations compared to computing
218 /// inverse and compose separately, resulting in both clearer code and better performance.
219 ///
220 /// # Performance
221 ///
222 /// - **Static dispatch**: All operations are monomorphized at compile time
223 /// - **Zero overhead**: Same performance as type-specific implementations
224 /// - **Parallel-safe**: Marked `Send + Sync` for use in parallel optimization
225 ///
226 /// # Examples
227 ///
228 /// ## SE(3) Linearization
229 ///
230 /// ```
231 /// use apex_solver::factors::{Factor, BetweenFactor};
232 /// use apex_solver::manifold::se3::SE3;
233 /// use nalgebra::DVector;
234 ///
235 /// let relative = SE3::identity();
236 /// let factor = BetweenFactor::new(relative);
237 ///
238 /// let pose_i = DVector::from_vec(vec![0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0]);
239 /// let pose_j = DVector::from_vec(vec![1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0]);
240 ///
241 /// let (residual, jacobian) = factor.linearize(&[pose_i, pose_j], true);
242 /// assert_eq!(residual.len(), 6); // 6 DOF
243 /// assert!(jacobian.is_some());
244 /// let jac = jacobian.unwrap();
245 /// assert_eq!(jac.nrows(), 6); // Residual dimension
246 /// assert_eq!(jac.ncols(), 12); // 2 × DOF
247 /// ```
248 ///
249 /// ## SE(2) Linearization
250 ///
251 /// ```
252 /// use apex_solver::factors::{Factor, BetweenFactor};
253 /// use apex_solver::manifold::se2::SE2;
254 /// use nalgebra::DVector;
255 ///
256 /// let relative = SE2::from_xy_angle(1.0, 0.0, 0.0);
257 /// let factor = BetweenFactor::new(relative);
258 ///
259 /// let pose_i = DVector::from_vec(vec![0.0, 0.0, 0.0]);
260 /// let pose_j = DVector::from_vec(vec![1.0, 0.0, 0.0]);
261 ///
262 /// let (residual, jacobian) = factor.linearize(&[pose_i, pose_j], true);
263 /// assert_eq!(residual.len(), 3); // 3 DOF
264 /// let jac = jacobian.unwrap();
265 /// assert_eq!(jac.nrows(), 3); // Residual dimension
266 /// assert_eq!(jac.ncols(), 6); // 2 × DOF
267 /// ```
268 fn linearize(
269 &self,
270 params: &[DVector<f64>],
271 compute_jacobian: bool,
272 ) -> (DVector<f64>, Option<DMatrix<f64>>) {
273 let se3_origin_k0 = T::from(params[0].clone());
274 let se3_origin_k1 = T::from(params[1].clone());
275 let se3_k0_k1_measured = &self.relative_pose;
276
277 // Step 1: se3_origin_k1.between(se3_origin_k0) = k1⁻¹ * k0
278 let mut j_k1_k0_wrt_k1 = T::zero_jacobian();
279 let mut j_k1_k0_wrt_k0 = T::zero_jacobian();
280 let se3_k1_k0 = se3_origin_k1.between(
281 &se3_origin_k0,
282 Some(&mut j_k1_k0_wrt_k1),
283 Some(&mut j_k1_k0_wrt_k0),
284 );
285
286 // Step 2: se3_k1_k0 * se3_k0_k1_measured
287 let mut j_diff_wrt_k1_k0 = T::zero_jacobian();
288 let se3_diff = se3_k1_k0.compose(se3_k0_k1_measured, Some(&mut j_diff_wrt_k1_k0), None);
289
290 // Step 3: se3_diff.log()
291 let mut j_log_wrt_diff = T::zero_jacobian();
292 let residual = se3_diff.log(Some(&mut j_log_wrt_diff));
293
294 let jacobian = if compute_jacobian {
295 // Calculate dimensions dynamically based on manifold DOF
296 let dof = se3_origin_k0.tangent_dim();
297
298 // Chain rule: d(residual)/d(k0) and d(residual)/d(k1)
299 let j_diff_wrt_k0 = j_diff_wrt_k1_k0.clone() * j_k1_k0_wrt_k0;
300 let j_diff_wrt_k1 = j_diff_wrt_k1_k0 * j_k1_k0_wrt_k1;
301
302 let jacobian_wrt_k0 = j_log_wrt_diff.clone() * j_diff_wrt_k0;
303 let jacobian_wrt_k1 = j_log_wrt_diff * j_diff_wrt_k1;
304
305 // Assemble full Jacobian: [∂r/∂pose_i | ∂r/∂pose_j]
306 let mut jacobian = DMatrix::<f64>::zeros(dof, 2 * dof);
307
308 // Copy element-wise from JacobianMatrix to DMatrix
309 // This works for all Matrix types (fixed-size and dynamic)
310 for i in 0..dof {
311 for j in 0..dof {
312 jacobian[(i, j)] = jacobian_wrt_k0[(i, j)];
313 jacobian[(i, j + dof)] = jacobian_wrt_k1[(i, j)];
314 }
315 }
316
317 Some(jacobian)
318 } else {
319 None
320 };
321 (residual.into(), jacobian)
322 }
323
324 fn get_dimension(&self) -> usize {
325 self.relative_pose.tangent_dim()
326 }
327}
328
329#[cfg(test)]
330mod tests {
331 use super::*;
332 use crate::manifold::se2::{SE2, SE2Tangent};
333 use crate::manifold::se3::SE3;
334 use crate::manifold::so2::SO2;
335 use crate::manifold::so3::SO3;
336 use nalgebra::{DVector, Quaternion, Vector3};
337
338 const TOLERANCE: f64 = 1e-9;
339 const FD_EPSILON: f64 = 1e-6;
340
341 #[test]
342 fn test_between_factor_se2_identity() {
343 // Test that identity measurement yields zero residual
344 let relative = SE2::identity();
345 let factor = BetweenFactor::new(relative);
346
347 let pose_i = DVector::from_vec(vec![0.0, 0.0, 0.0]);
348 let pose_j = DVector::from_vec(vec![0.0, 0.0, 0.0]);
349
350 let (residual, _) = factor.linearize(&[pose_i, pose_j], false);
351
352 assert_eq!(residual.len(), 3);
353 assert!(
354 residual.norm() < TOLERANCE,
355 "Residual norm: {}",
356 residual.norm()
357 );
358 }
359
360 #[test]
361 fn test_between_factor_se3_identity() {
362 // Test that identity measurement yields zero residual
363 let relative = SE3::identity();
364 let factor = BetweenFactor::new(relative);
365
366 let pose_i = DVector::from_vec(vec![0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0]);
367 let pose_j = DVector::from_vec(vec![0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0]);
368
369 let (residual, _) = factor.linearize(&[pose_i, pose_j], false);
370
371 assert_eq!(residual.len(), 6);
372 assert!(
373 residual.norm() < TOLERANCE,
374 "Residual norm: {}",
375 residual.norm()
376 );
377 }
378
379 #[test]
380 fn test_between_factor_se2_jacobian_numerical() -> Result<(), Box<dyn std::error::Error>> {
381 // Verify Jacobian using finite differences with manifold perturbations
382 let relative = SE2::from_xy_angle(1.0, 0.0, 0.1);
383 let factor = BetweenFactor::new(relative);
384
385 let pose_i = DVector::from_vec(vec![0.0, 0.0, 0.0]);
386 let pose_j = DVector::from_vec(vec![0.95, 0.05, 0.12]);
387
388 let (residual, jacobian_opt) = factor.linearize(&[pose_i.clone(), pose_j.clone()], true);
389 let jacobian = jacobian_opt.ok_or("Jacobian should be Some when compute_jacobians=true")?;
390
391 assert_eq!(jacobian.nrows(), 3);
392 assert_eq!(jacobian.ncols(), 6);
393
394 // Finite difference validation using manifold plus operation
395 let mut jacobian_fd = DMatrix::<f64>::zeros(3, 6);
396 let se2_i = SE2::from(pose_i.clone());
397 let se2_j = SE2::from(pose_j.clone());
398
399 // Perturb pose_i in tangent space
400 for i in 0..3 {
401 let delta = match i {
402 0 => SE2Tangent::new(FD_EPSILON, 0.0, 0.0),
403 1 => SE2Tangent::new(0.0, FD_EPSILON, 0.0),
404 2 => SE2Tangent::new(0.0, 0.0, FD_EPSILON),
405 _ => unreachable!(),
406 };
407 let se2_i_perturbed = se2_i.plus(&delta, None, None);
408 let pose_i_perturbed = DVector::<f64>::from(se2_i_perturbed);
409 let (residual_perturbed, _) =
410 factor.linearize(&[pose_i_perturbed, pose_j.clone()], false);
411
412 for j in 0..3 {
413 jacobian_fd[(j, i)] = (residual_perturbed[j] - residual[j]) / FD_EPSILON;
414 }
415 }
416
417 // Perturb pose_j in tangent space
418 for i in 0..3 {
419 let delta = match i {
420 0 => SE2Tangent::new(FD_EPSILON, 0.0, 0.0),
421 1 => SE2Tangent::new(0.0, FD_EPSILON, 0.0),
422 2 => SE2Tangent::new(0.0, 0.0, FD_EPSILON),
423 _ => unreachable!(),
424 };
425 let se2_j_perturbed = se2_j.plus(&delta, None, None);
426 let pose_j_perturbed = DVector::<f64>::from(se2_j_perturbed);
427 let (residual_perturbed, _) =
428 factor.linearize(&[pose_i.clone(), pose_j_perturbed], false);
429
430 for j in 0..3 {
431 jacobian_fd[(j, i + 3)] = (residual_perturbed[j] - residual[j]) / FD_EPSILON;
432 }
433 }
434
435 let diff_norm = (jacobian - jacobian_fd).norm();
436 assert!(diff_norm < 1e-5, "Jacobian difference norm: {}", diff_norm);
437 Ok(())
438 }
439
440 #[test]
441 fn test_between_factor_se3_jacobian_numerical() -> Result<(), Box<dyn std::error::Error>> {
442 // Verify Jacobian using finite differences for SE3
443 let relative = SE3::from_translation_quaternion(
444 Vector3::new(1.0, 0.0, 0.0),
445 Quaternion::new(1.0, 0.0, 0.0, 0.0),
446 );
447 let factor = BetweenFactor::new(relative);
448
449 let pose_i = DVector::from_vec(vec![0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0]);
450 let pose_j = DVector::from_vec(vec![0.95, 0.05, 0.0, 1.0, 0.0, 0.0, 0.0]);
451
452 let (residual, jacobian_opt) = factor.linearize(&[pose_i.clone(), pose_j.clone()], true);
453 let jacobian = jacobian_opt.ok_or("Jacobian should be Some when compute_jacobians=true")?;
454
455 assert_eq!(jacobian.nrows(), 6);
456 assert_eq!(jacobian.ncols(), 12);
457
458 // Finite difference validation (only check translation part for simplicity)
459 let mut jacobian_fd = DMatrix::<f64>::zeros(6, 12);
460
461 // Perturb pose_i translation
462 for i in 0..3 {
463 let mut pose_i_perturbed = pose_i.clone();
464 pose_i_perturbed[i] += FD_EPSILON;
465 let (residual_perturbed, _) =
466 factor.linearize(&[pose_i_perturbed, pose_j.clone()], false);
467
468 for j in 0..6 {
469 jacobian_fd[(j, i)] = (residual_perturbed[j] - residual[j]) / FD_EPSILON;
470 }
471 }
472
473 // Perturb pose_j translation
474 for i in 0..3 {
475 let mut pose_j_perturbed = pose_j.clone();
476 pose_j_perturbed[i] += FD_EPSILON;
477 let (residual_perturbed, _) =
478 factor.linearize(&[pose_i.clone(), pose_j_perturbed], false);
479
480 for j in 0..6 {
481 jacobian_fd[(j, i + 6)] = (residual_perturbed[j] - residual[j]) / FD_EPSILON;
482 }
483 }
484
485 // Check translation part only (more robust for FD)
486 let diff_norm_trans = (jacobian.columns(0, 3) - jacobian_fd.columns(0, 3)).norm();
487 assert!(
488 diff_norm_trans < 1e-5,
489 "Jacobian difference norm (translation): {}",
490 diff_norm_trans
491 );
492 Ok(())
493 }
494
495 #[test]
496 fn test_between_factor_dimension_se2() -> Result<(), Box<dyn std::error::Error>> {
497 let relative = SE2::from_xy_angle(1.0, 0.5, 0.1);
498 let factor = BetweenFactor::new(relative);
499
500 let pose_i = DVector::from_vec(vec![0.0, 0.0, 0.0]);
501 let pose_j = DVector::from_vec(vec![1.0, 0.0, 0.0]);
502
503 let (residual, jacobian) = factor.linearize(&[pose_i, pose_j], true);
504
505 assert_eq!(residual.len(), 3);
506 assert_eq!(factor.get_dimension(), 3);
507
508 let jac = jacobian.ok_or("Jacobian should be Some when compute_jacobians=true")?;
509 assert_eq!(jac.nrows(), 3);
510 assert_eq!(jac.ncols(), 6);
511 Ok(())
512 }
513
514 #[test]
515 fn test_between_factor_dimension_se3() -> Result<(), Box<dyn std::error::Error>> {
516 let relative = SE3::identity();
517 let factor = BetweenFactor::new(relative);
518
519 let pose_i = DVector::from_vec(vec![0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0]);
520 let pose_j = DVector::from_vec(vec![1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0]);
521
522 let (residual, jacobian) = factor.linearize(&[pose_i, pose_j], true);
523
524 assert_eq!(residual.len(), 6);
525 assert_eq!(factor.get_dimension(), 6);
526
527 let jac = jacobian.ok_or("Jacobian should be Some when compute_jacobians=true")?;
528 assert_eq!(jac.nrows(), 6);
529 assert_eq!(jac.ncols(), 12);
530 Ok(())
531 }
532
533 #[test]
534 fn test_between_factor_so2_so3() -> Result<(), Box<dyn std::error::Error>> {
535 // Test SO2 (rotation-only in 2D)
536 let so2_relative = SO2::from_angle(0.1);
537 let so2_factor = BetweenFactor::new(so2_relative);
538
539 let so2_i = DVector::from_vec(vec![0.0]);
540 let so2_j = DVector::from_vec(vec![0.12]);
541
542 let (residual_so2, jacobian_so2) = so2_factor.linearize(&[so2_i, so2_j], true);
543 assert_eq!(residual_so2.len(), 1);
544 assert_eq!(so2_factor.get_dimension(), 1);
545
546 let jac_so2 = jacobian_so2.ok_or("Jacobian should be Some when compute_jacobians=true")?;
547 assert_eq!(jac_so2.nrows(), 1);
548 assert_eq!(jac_so2.ncols(), 2);
549
550 // Test SO3 (rotation-only in 3D)
551 let so3_relative = SO3::identity();
552 let so3_factor = BetweenFactor::new(so3_relative);
553
554 let so3_i = DVector::from_vec(vec![1.0, 0.0, 0.0, 0.0]);
555 let so3_j = DVector::from_vec(vec![1.0, 0.0, 0.0, 0.0]);
556
557 let (residual_so3, jacobian_so3) = so3_factor.linearize(&[so3_i, so3_j], true);
558 assert_eq!(residual_so3.len(), 3);
559 assert_eq!(so3_factor.get_dimension(), 3);
560
561 let jac_so3 = jacobian_so3.ok_or("Jacobian should be Some when compute_jacobians=true")?;
562 assert_eq!(jac_so3.nrows(), 3);
563 assert_eq!(jac_so3.ncols(), 6);
564 Ok(())
565 }
566
567 #[test]
568 fn test_between_factor_finiteness() -> Result<(), Box<dyn std::error::Error>> {
569 // Test numerical stability with various inputs
570 let relative = SE2::from_xy_angle(100.0, -200.0, std::f64::consts::PI);
571 let factor = BetweenFactor::new(relative);
572
573 let pose_i = DVector::from_vec(vec![50.0, -100.0, 1.5]);
574 let pose_j = DVector::from_vec(vec![150.0, -300.0, -1.5]);
575
576 let (residual, jacobian) = factor.linearize(&[pose_i, pose_j], true);
577
578 assert!(residual.iter().all(|&x| x.is_finite()));
579 let jac = jacobian.ok_or("Jacobian should be Some when compute_jacobians=true")?;
580 assert!(jac.iter().all(|&x| x.is_finite()));
581 Ok(())
582 }
583
584 #[test]
585 fn test_between_factor_clone() {
586 let relative = SE3::identity();
587 let factor = BetweenFactor::new(relative);
588 let factor_clone = factor.clone();
589
590 let pose_i = DVector::from_vec(vec![0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0]);
591 let pose_j = DVector::from_vec(vec![1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0]);
592
593 let (residual1, _) = factor.linearize(&[pose_i.clone(), pose_j.clone()], false);
594 let (residual2, _) = factor_clone.linearize(&[pose_i, pose_j], false);
595
596 assert!((residual1 - residual2).norm() < TOLERANCE);
597 }
598}