Skip to main content

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(&params, &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(&params, &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}