apex_solver/factors/mod.rs
1//! Factor implementations for graph-based optimization problems.
2//!
3//! Factors (also called constraints or error functions) represent measurements or relationships
4//! between variables in a factor graph. Each factor computes a residual (error) vector and its
5//! Jacobian with respect to the connected variables.
6//!
7//! # Factor Graph Formulation
8//!
9//! In graph-based SLAM and bundle adjustment, the optimization problem is represented as:
10//!
11//! ```text
12//! minimize Σ_i ||r_i(x)||²
13//! ```
14//!
15//! where:
16//! - `x` is the set of variables (poses, landmarks, etc.)
17//! - `r_i(x)` is the residual function for factor i
18//! - Each factor connects one or more variables
19//!
20//! # Factor Types
21//!
22//! ## Pose Factors
23//! - **Between factors**: Relative pose constraints (SE2, SE3)
24//! - **Prior factors**: Unary constraints on single variables
25//!
26//! ## Camera Projection Factors
27//!
28//! Use [`ProjectionFactor`] with a specific
29//! [`CameraModel`](apex_camera_models::CameraModel).
30//!
31//! Supported camera models:
32//! - [`PinholeCamera`](camera::PinholeCamera)
33//! - [`DoubleSphereCamera`](camera::DoubleSphereCamera)
34//! - [`EucmCamera`](camera::EucmCamera)
35//! - [`FovCamera`](camera::FovCamera)
36//! - [`KannalaBrandtCamera`](camera::KannalaBrandtCamera)
37//! - [`RadTanCamera`](camera::RadTanCamera)
38//! - [`UcmCamera`](camera::UcmCamera)
39//!
40//! # Linearization
41//!
42//! Each factor must provide a `linearize` method that writes the residual and Jacobian
43//! directly into caller-provided buffers — no heap allocation for the output.
44//!
45//! This information is used by the optimizer to compute parameter updates via Newton-type methods.
46
47use thiserror::Error;
48
49// Pose factors
50pub mod between_factor;
51pub mod prior_factor;
52pub mod projection_factor;
53
54pub use between_factor::BetweenFactor;
55pub use prior_factor::PriorFactor;
56pub use projection_factor::ProjectionFactor;
57
58// Optimization configuration types
59
60/// Configuration for which parameters to optimize.
61///
62/// Uses const generic booleans for compile-time optimization selection.
63///
64/// # Type Parameters
65///
66/// - `POSE`: Whether to optimize camera pose (SE3 transformation)
67/// - `LANDMARK`: Whether to optimize 3D landmark positions
68/// - `INTRINSIC`: Whether to optimize camera intrinsic parameters
69#[derive(Debug, Clone, Copy, Default)]
70pub struct OptimizeParams<const POSE: bool, const LANDMARK: bool, const INTRINSIC: bool>;
71
72impl<const P: bool, const L: bool, const I: bool> OptimizeParams<P, L, I> {
73 /// Whether to optimize camera pose
74 pub const POSE: bool = P;
75 /// Whether to optimize 3D landmarks
76 pub const LANDMARK: bool = L;
77 /// Whether to optimize camera intrinsics
78 pub const INTRINSIC: bool = I;
79}
80
81/// Bundle Adjustment: optimize pose + landmarks (intrinsics fixed).
82pub type BundleAdjustment = OptimizeParams<true, true, false>;
83
84/// Self-Calibration: optimize pose + landmarks + intrinsics.
85pub type SelfCalibration = OptimizeParams<true, true, true>;
86
87/// Only Intrinsics: optimize intrinsics (pose and landmarks fixed).
88pub type OnlyIntrinsics = OptimizeParams<false, false, true>;
89
90/// Only Pose: optimize pose (landmarks and intrinsics fixed).
91pub type OnlyPose = OptimizeParams<true, false, false>;
92
93/// Only Landmarks: optimize landmarks (pose and intrinsics fixed).
94pub type OnlyLandmarks = OptimizeParams<false, true, false>;
95
96/// Pose and Intrinsics: optimize pose + intrinsics (landmarks fixed).
97pub type PoseAndIntrinsics = OptimizeParams<true, false, true>;
98
99/// Landmarks and Intrinsics: optimize landmarks + intrinsics (pose fixed).
100pub type LandmarksAndIntrinsics = OptimizeParams<false, true, true>;
101
102// Camera module alias for backward compatibility
103// Re-exports the apex-camera-models crate as `camera` module
104pub mod camera {
105 pub use apex_camera_models::*;
106}
107
108/// Factor-specific error types for apex-solver
109#[derive(Debug, Clone, Error)]
110pub enum FactorError {
111 /// Invalid dimension mismatch between expected and actual
112 #[error("Invalid dimension: expected {expected}, got {actual}")]
113 InvalidDimension { expected: usize, actual: usize },
114
115 /// Invalid projection (point behind camera or outside valid range)
116 #[error("Invalid projection: {0}")]
117 InvalidProjection(String),
118
119 /// Jacobian computation failed
120 #[error("Jacobian computation failed: {0}")]
121 JacobianFailed(String),
122
123 /// Invalid parameter values
124 #[error("Invalid parameter values: {0}")]
125 InvalidParameters(String),
126
127 /// Numerical instability detected
128 #[error("Numerical instability: {0}")]
129 NumericalInstability(String),
130}
131
132/// Result type for factor operations
133pub type FactorResult<T> = Result<T, FactorError>;
134
135/// Trait for factor (constraint) implementations in factor graph optimization.
136///
137/// A factor represents a measurement or constraint connecting one or more variables.
138/// It writes the residual and Jacobian directly into caller-provided buffers — no heap
139/// allocation for the output. Parameters arrive as zero-copy `&[f64]` slices from
140/// manifold storage.
141///
142/// # Thread Safety
143///
144/// Factors must be `Send + Sync` to enable parallel residual/Jacobian evaluation.
145///
146/// # Example
147///
148/// ```
149/// use apex_solver::factors::Factor;
150/// use faer::prelude::ReborrowMut;
151///
152/// // Simple 1D range measurement factor
153/// struct RangeFactor {
154/// measurement: f64,
155/// }
156///
157/// impl Factor for RangeFactor {
158/// fn linearize(
159/// &self,
160/// params: &[&[f64]],
161/// residual: &mut [f64],
162/// jacobian: Option<faer::mat::MatMut<'_, f64>>,
163/// ) {
164/// let x = params[0][0];
165/// let y = params[0][1];
166/// let dist = (x * x + y * y).sqrt();
167/// residual[0] = self.measurement - dist;
168/// if let Some(mut jac) = jacobian {
169/// *jac.rb_mut().get_mut(0, 0) = -x / dist;
170/// *jac.rb_mut().get_mut(0, 1) = -y / dist;
171/// }
172/// }
173/// fn residual_dim(&self) -> usize { 1 }
174/// fn jacobian_shape(&self) -> (usize, usize) { (1, 2) }
175/// }
176/// ```
177pub trait Factor: Send + Sync {
178 /// Write residual and (optionally) Jacobian into pre-allocated buffers.
179 ///
180 /// - `params`: one `&[f64]` slice per connected variable (from `ManifoldVariable::as_param_slice`)
181 /// - `residual`: pre-allocated output buffer of length `residual_dim()`
182 /// - `jacobian`: optional column-major `MatMut` of shape `jacobian_shape()`
183 fn linearize(
184 &self,
185 params: &[&[f64]],
186 residual: &mut [f64],
187 jacobian: Option<faer::mat::MatMut<'_, f64>>,
188 );
189
190 /// Number of residual rows (length of the `residual` buffer).
191 fn residual_dim(&self) -> usize;
192
193 /// `(rows, cols)` of the Jacobian — `rows == residual_dim()`, `cols == sum of variable DOFs`.
194 fn jacobian_shape(&self) -> (usize, usize);
195}
196
197#[cfg(test)]
198mod tests {
199 use super::*;
200 use crate::error::ErrorLogging;
201 use faer::prelude::ReborrowMut;
202 use nalgebra::dvector;
203
204 // -------------------------------------------------------------------------
205 // OptimizeParams const generic flags — all 7 type aliases
206 // -------------------------------------------------------------------------
207
208 #[test]
209 fn test_optimize_params_bundle_adjustment_flags() {
210 const { assert!(BundleAdjustment::POSE) };
211 const { assert!(BundleAdjustment::LANDMARK) };
212 const { assert!(!BundleAdjustment::INTRINSIC) };
213 }
214
215 #[test]
216 fn test_optimize_params_self_calibration_flags() {
217 const { assert!(SelfCalibration::POSE) };
218 const { assert!(SelfCalibration::LANDMARK) };
219 const { assert!(SelfCalibration::INTRINSIC) };
220 }
221
222 #[test]
223 fn test_optimize_params_only_intrinsics_flags() {
224 const { assert!(!OnlyIntrinsics::POSE) };
225 const { assert!(!OnlyIntrinsics::LANDMARK) };
226 const { assert!(OnlyIntrinsics::INTRINSIC) };
227 }
228
229 #[test]
230 fn test_optimize_params_only_pose_flags() {
231 const { assert!(OnlyPose::POSE) };
232 const { assert!(!OnlyPose::LANDMARK) };
233 const { assert!(!OnlyPose::INTRINSIC) };
234 }
235
236 #[test]
237 fn test_optimize_params_only_landmarks_flags() {
238 const { assert!(!OnlyLandmarks::POSE) };
239 const { assert!(OnlyLandmarks::LANDMARK) };
240 const { assert!(!OnlyLandmarks::INTRINSIC) };
241 }
242
243 #[test]
244 fn test_optimize_params_pose_and_intrinsics_flags() {
245 const { assert!(PoseAndIntrinsics::POSE) };
246 const { assert!(!PoseAndIntrinsics::LANDMARK) };
247 const { assert!(PoseAndIntrinsics::INTRINSIC) };
248 }
249
250 #[test]
251 fn test_optimize_params_landmarks_and_intrinsics_flags() {
252 const { assert!(!LandmarksAndIntrinsics::POSE) };
253 const { assert!(LandmarksAndIntrinsics::LANDMARK) };
254 const { assert!(LandmarksAndIntrinsics::INTRINSIC) };
255 }
256
257 // -------------------------------------------------------------------------
258 // FactorError Display — one per variant
259 // -------------------------------------------------------------------------
260
261 #[test]
262 fn test_factor_error_invalid_dimension_display() {
263 let e = FactorError::InvalidDimension {
264 expected: 3,
265 actual: 6,
266 };
267 let s = e.to_string();
268 assert!(s.contains("3"), "{s}");
269 assert!(s.contains("6"), "{s}");
270 }
271
272 #[test]
273 fn test_factor_error_invalid_projection_display() {
274 let e = FactorError::InvalidProjection("behind camera".into());
275 assert!(e.to_string().contains("behind camera"));
276 }
277
278 #[test]
279 fn test_factor_error_jacobian_failed_display() {
280 let e = FactorError::JacobianFailed("singular".into());
281 assert!(e.to_string().contains("singular"));
282 }
283
284 #[test]
285 fn test_factor_error_invalid_parameters_display() {
286 let e = FactorError::InvalidParameters("nan detected".into());
287 assert!(e.to_string().contains("nan detected"));
288 }
289
290 #[test]
291 fn test_factor_error_numerical_instability_display() {
292 let e = FactorError::NumericalInstability("overflow".into());
293 assert!(e.to_string().contains("overflow"));
294 }
295
296 // -------------------------------------------------------------------------
297 // log() / log_with_source() return self
298 // -------------------------------------------------------------------------
299
300 #[test]
301 fn test_factor_error_log_returns_self() {
302 let e = FactorError::JacobianFailed("test_log".into());
303 let returned = e.log();
304 assert!(returned.to_string().contains("test_log"));
305 }
306
307 #[test]
308 fn test_factor_error_log_with_source_returns_self() {
309 let e = FactorError::InvalidProjection("proj_log".into());
310 let source = std::io::Error::other("src");
311 let returned = e.log_with_source(source);
312 assert!(returned.to_string().contains("proj_log"));
313 }
314
315 // -------------------------------------------------------------------------
316 // Factor trait — local implementation
317 // -------------------------------------------------------------------------
318
319 struct ConstantFactor {
320 value: f64,
321 }
322
323 impl Factor for ConstantFactor {
324 fn linearize(
325 &self,
326 params: &[&[f64]],
327 residual: &mut [f64],
328 jacobian: Option<faer::mat::MatMut<'_, f64>>,
329 ) {
330 residual[0] = params[0][0] - self.value;
331 if let Some(mut jac) = jacobian {
332 *jac.rb_mut().get_mut(0, 0) = 1.0;
333 }
334 }
335
336 fn residual_dim(&self) -> usize {
337 1
338 }
339
340 fn jacobian_shape(&self) -> (usize, usize) {
341 (1, 1)
342 }
343 }
344
345 #[test]
346 fn test_factor_compute_with_jacobian() {
347 let f = ConstantFactor { value: 3.0 };
348 let p = dvector![5.0];
349 let params: Vec<&[f64]> = vec![p.as_slice()];
350 let mut residual = vec![0.0f64; 1];
351 let mut jac_buf = vec![0.0f64; 1];
352 let jac_mut = faer::mat::MatMut::from_column_major_slice_mut(&mut jac_buf, 1, 1);
353 f.linearize(¶ms, &mut residual, Some(jac_mut));
354 assert!((residual[0] - 2.0).abs() < 1e-12);
355 assert!((jac_buf[0] - 1.0).abs() < 1e-12);
356 }
357
358 #[test]
359 fn test_factor_compute_without_jacobian() {
360 let f = ConstantFactor { value: 3.0 };
361 let p = dvector![5.0];
362 let params: Vec<&[f64]> = vec![p.as_slice()];
363 let mut residual = vec![0.0f64; 1];
364 f.linearize(¶ms, &mut residual, None);
365 assert!((residual[0] - 2.0).abs() < 1e-12);
366 }
367
368 #[test]
369 fn test_factor_residual_dim() {
370 let f = ConstantFactor { value: 0.0 };
371 assert_eq!(f.residual_dim(), 1);
372 }
373
374 // -------------------------------------------------------------------------
375 // FactorResult type alias
376 // -------------------------------------------------------------------------
377
378 #[test]
379 fn test_factor_result_ok() {
380 let r: FactorResult<f64> = Ok(1.0);
381 assert!(r.is_ok());
382 }
383
384 #[test]
385 fn test_factor_result_err() {
386 let r: FactorResult<f64> = Err(FactorError::InvalidParameters("bad".into()));
387 assert!(r.is_err());
388 }
389}