apex_solver/optimizer/mod.rs
1//! Optimization solvers for nonlinear least squares problems.
2//!
3//! This module provides various optimization algorithms specifically designed
4//! for nonlinear least squares problems commonly found in computer vision:
5//! - Levenberg-Marquardt algorithm
6//! - Gauss-Newton algorithm
7//! - Dog Leg algorithm
8
9use crate::core::problem::{Problem, VariableEnum};
10use crate::linalg;
11use crate::manifold::ManifoldType;
12use faer::{Mat, MatRef};
13use nalgebra::DVector;
14use std::collections::HashMap;
15use std::time;
16use std::{
17 fmt,
18 fmt::{Display, Formatter},
19};
20use thiserror::Error;
21use tracing::error;
22
23pub mod dog_leg;
24pub mod gauss_newton;
25pub mod levenberg_marquardt;
26
27pub use dog_leg::DogLeg;
28pub use gauss_newton::GaussNewton;
29pub use levenberg_marquardt::LevenbergMarquardt;
30
31// Re-export observer types from the observers module
32pub use crate::observers::{OptObserver, OptObserverVec};
33
34/// Type of optimization solver algorithm to use
35#[derive(Default, Clone, Copy, PartialEq, Eq)]
36pub enum OptimizerType {
37 /// Levenberg-Marquardt algorithm (robust, adaptive damping)
38 #[default]
39 LevenbergMarquardt,
40 /// Gauss-Newton algorithm (fast convergence, may be unstable)
41 GaussNewton,
42 /// Dog Leg algorithm (trust region method)
43 DogLeg,
44}
45
46impl Display for OptimizerType {
47 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
48 match self {
49 OptimizerType::LevenbergMarquardt => write!(f, "Levenberg-Marquardt"),
50 OptimizerType::GaussNewton => write!(f, "Gauss-Newton"),
51 OptimizerType::DogLeg => write!(f, "Dog Leg"),
52 }
53 }
54}
55
56/// Optimizer-specific error types for apex-solver
57#[derive(Debug, Clone, Error)]
58pub enum OptimizerError {
59 /// Linear system solve failed during optimization
60 #[error("Linear system solve failed: {0}")]
61 LinearSolveFailed(String),
62
63 /// Maximum iterations reached without achieving convergence
64 #[error("Maximum iterations ({max_iters}) reached without convergence")]
65 MaxIterationsReached { max_iters: usize },
66
67 /// Trust region radius became too small
68 #[error("Trust region radius became too small: {radius:.6e} < {min_radius:.6e}")]
69 TrustRegionFailure { radius: f64, min_radius: f64 },
70
71 /// Damping parameter became too large (LM-specific)
72 #[error("Damping parameter became too large: {damping:.6e} > {max_damping:.6e}")]
73 DampingFailure { damping: f64, max_damping: f64 },
74
75 /// Cost increased unexpectedly when it should decrease
76 #[error("Cost increased unexpectedly: {old_cost:.6e} -> {new_cost:.6e}")]
77 CostIncrease { old_cost: f64, new_cost: f64 },
78
79 /// Jacobian computation failed
80 #[error("Jacobian computation failed: {0}")]
81 JacobianFailed(String),
82
83 /// Invalid optimization parameters provided
84 #[error("Invalid optimization parameters: {0}")]
85 InvalidParameters(String),
86
87 /// Numerical instability detected (NaN, Inf in cost, gradient, or parameters)
88 #[error("Numerical instability detected: {0}")]
89 NumericalInstability(String),
90
91 /// Linear algebra operation failed
92 #[error("Linear algebra error: {0}")]
93 LinAlg(#[from] linalg::LinAlgError),
94
95 /// Problem has no variables to optimize
96 #[error("Problem has no variables to optimize")]
97 EmptyProblem,
98
99 /// Problem has no residual blocks
100 #[error("Problem has no residual blocks")]
101 NoResidualBlocks,
102
103 /// Jacobi scaling matrix creation failed
104 #[error("Failed to create Jacobi scaling matrix: {0}")]
105 JacobiScalingCreation(String),
106
107 /// Jacobi scaling not initialized when expected
108 #[error("Jacobi scaling not initialized")]
109 JacobiScalingNotInitialized,
110
111 /// Unknown or unsupported linear solver type
112 #[error("Unknown linear solver type: {0}")]
113 UnknownLinearSolver(String),
114}
115
116impl OptimizerError {
117 /// Log the error with tracing::error and return self for chaining
118 ///
119 /// This method allows for a consistent error logging pattern throughout
120 /// the optimizer module, ensuring all errors are properly recorded.
121 ///
122 /// # Example
123 /// ```ignore
124 /// operation()
125 /// .map_err(|e| OptimizerError::from(e).log())?;
126 /// ```
127 #[must_use]
128 pub fn log(self) -> Self {
129 error!("{}", self);
130 self
131 }
132
133 /// Log the error with the original source error from a third-party library
134 ///
135 /// This method logs both the OptimizerError and the underlying error
136 /// from external libraries. This provides full debugging context when
137 /// errors occur in third-party code.
138 ///
139 /// # Arguments
140 /// * `source_error` - The original error from the third-party library (must implement Debug)
141 ///
142 /// # Example
143 /// ```ignore
144 /// SparseColMat::try_new_from_triplets(cols, cols, &triplets)
145 /// .map_err(|e| {
146 /// OptimizerError::JacobiScalingCreation(e.to_string())
147 /// .log_with_source(e)
148 /// })?;
149 /// ```
150 #[must_use]
151 pub fn log_with_source<E: std::fmt::Debug>(self, source_error: E) -> Self {
152 error!("{} | Source: {:?}", self, source_error);
153 self
154 }
155}
156
157/// Result type for optimizer operations
158pub type OptimizerResult<T> = Result<T, OptimizerError>;
159
160// State information during iterative optimization.
161// #[derive(Debug, Clone)]
162// pub struct IterativeState {
163// /// Current iteration number
164// pub iteration: usize,
165// /// Current cost value
166// pub cost: f64,
167// /// Current gradient norm
168// pub gradient_norm: f64,
169// /// Current parameter update norm
170// pub parameter_update_norm: f64,
171// /// Time elapsed since start
172// pub elapsed_time: Duration,
173// }
174
175/// Detailed convergence information.
176#[derive(Debug, Clone)]
177pub struct ConvergenceInfo {
178 /// Final gradient norm
179 pub final_gradient_norm: f64,
180 /// Final parameter update norm
181 pub final_parameter_update_norm: f64,
182 /// Cost function evaluation count
183 pub cost_evaluations: usize,
184 /// Jacobian evaluation count
185 pub jacobian_evaluations: usize,
186}
187
188impl Display for ConvergenceInfo {
189 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
190 write!(
191 f,
192 "Final gradient norm: {:.2e}, Final parameter update norm: {:.2e}, Cost evaluations: {}, Jacobian evaluations: {}",
193 self.final_gradient_norm,
194 self.final_parameter_update_norm,
195 self.cost_evaluations,
196 self.jacobian_evaluations
197 )
198 }
199}
200
201/// Status of an optimization process
202#[derive(Debug, Clone, PartialEq, Eq)]
203pub enum OptimizationStatus {
204 /// Optimization converged successfully
205 Converged,
206 /// Maximum number of iterations reached
207 MaxIterationsReached,
208 /// Cost function tolerance reached
209 CostToleranceReached,
210 /// Parameter tolerance reached
211 ParameterToleranceReached,
212 /// Gradient tolerance reached
213 GradientToleranceReached,
214 /// Optimization failed due to numerical issues
215 NumericalFailure,
216 /// User requested termination
217 UserTerminated,
218 /// Timeout reached
219 Timeout,
220 /// Trust region radius fell below minimum threshold
221 TrustRegionRadiusTooSmall,
222 /// Objective function fell below user-specified cutoff
223 MinCostThresholdReached,
224 /// Jacobian matrix is singular or ill-conditioned
225 IllConditionedJacobian,
226 /// NaN or Inf detected in cost or parameters
227 InvalidNumericalValues,
228 /// Other failure
229 Failed(String),
230}
231
232impl Display for OptimizationStatus {
233 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
234 match self {
235 OptimizationStatus::Converged => write!(f, "Converged"),
236 OptimizationStatus::MaxIterationsReached => write!(f, "Maximum iterations reached"),
237 OptimizationStatus::CostToleranceReached => write!(f, "Cost tolerance reached"),
238 OptimizationStatus::ParameterToleranceReached => {
239 write!(f, "Parameter tolerance reached")
240 }
241 OptimizationStatus::GradientToleranceReached => write!(f, "Gradient tolerance reached"),
242 OptimizationStatus::NumericalFailure => write!(f, "Numerical failure"),
243 OptimizationStatus::UserTerminated => write!(f, "User terminated"),
244 OptimizationStatus::Timeout => write!(f, "Timeout"),
245 OptimizationStatus::TrustRegionRadiusTooSmall => {
246 write!(f, "Trust region radius too small")
247 }
248 OptimizationStatus::MinCostThresholdReached => {
249 write!(f, "Minimum cost threshold reached")
250 }
251 OptimizationStatus::IllConditionedJacobian => {
252 write!(f, "Ill-conditioned Jacobian matrix")
253 }
254 OptimizationStatus::InvalidNumericalValues => {
255 write!(f, "Invalid numerical values (NaN/Inf) detected")
256 }
257 OptimizationStatus::Failed(msg) => write!(f, "Failed: {msg}"),
258 }
259 }
260}
261
262/// Result of a solver execution.
263#[derive(Clone)]
264pub struct SolverResult<T> {
265 /// Final parameters
266 pub parameters: T,
267 /// Final optimization status
268 pub status: OptimizationStatus,
269 /// Initial cost value
270 pub initial_cost: f64,
271 /// Final cost value
272 pub final_cost: f64,
273 /// Number of iterations performed
274 pub iterations: usize,
275 /// Total time elapsed
276 pub elapsed_time: time::Duration,
277 /// Convergence statistics
278 pub convergence_info: Option<ConvergenceInfo>,
279 /// Per-variable covariance matrices (uncertainty estimation)
280 ///
281 /// This is `None` if covariance computation was not enabled in the solver configuration.
282 /// When present, it contains a mapping from variable names to their covariance matrices
283 /// in tangent space. For example, for SE3 variables this would be 6×6 matrices.
284 ///
285 /// Enable covariance computation by setting `compute_covariances: true` in the optimizer config.
286 pub covariances: Option<HashMap<String, Mat<f64>>>,
287}
288
289/// Core trait for optimization solvers.
290pub trait Solver {
291 /// Configuration type for this solver
292 type Config;
293 /// Error type
294 type Error;
295
296 /// Create a new solver with the given configuration
297 fn new() -> Self;
298
299 /// Optimize the problem to minimize the cost function
300 fn optimize(
301 &mut self,
302 problem: &Problem,
303 initial_params: &HashMap<String, (ManifoldType, DVector<f64>)>,
304 ) -> Result<SolverResult<HashMap<String, VariableEnum>>, Self::Error>;
305}
306
307/// Apply parameter update step to all variables.
308///
309/// This is a common operation used by all optimizers (Levenberg-Marquardt, Gauss-Newton, Dog Leg).
310/// It applies a tangent space perturbation to each variable using the proper manifold plus operation.
311///
312/// # Arguments
313/// * `variables` - Mutable map of variables to update
314/// * `step` - Full step vector in tangent space (faer matrix view)
315/// * `variable_order` - Ordered list of variable names (defines indexing into step vector)
316///
317/// # Returns
318/// * Step norm (L2 norm) for convergence checking
319///
320/// # Implementation Notes
321/// The step vector contains concatenated tangent vectors for all variables in the order
322/// specified by `variable_order`. Each variable's tangent space dimension determines
323/// how many elements it occupies in the step vector.
324///
325pub fn apply_parameter_step(
326 variables: &mut HashMap<String, VariableEnum>,
327 step: MatRef<f64>,
328 variable_order: &[String],
329) -> f64 {
330 let mut step_offset = 0;
331
332 for var_name in variable_order {
333 if let Some(var) = variables.get_mut(var_name) {
334 let var_size = var.get_size();
335 let var_step = step.subrows(step_offset, var_size);
336
337 // Delegate to VariableEnum's apply_tangent_step method
338 // This handles all manifold types (SE2, SE3, SO2, SO3, Rn)
339 var.apply_tangent_step(var_step);
340
341 step_offset += var_size;
342 }
343 }
344
345 // Compute and return step norm for convergence checking
346 step.norm_l2()
347}
348
349/// Apply negative parameter step to rollback variables.
350///
351/// This is used when an optimization step is rejected (e.g., in trust region methods).
352/// It applies the negative of a tangent space perturbation to revert the previous update.
353///
354/// # Arguments
355/// * `variables` - Mutable map of variables to revert
356/// * `step` - Full step vector in tangent space (faer matrix view) to negate
357/// * `variable_order` - Ordered list of variable names (defines indexing into step vector)
358///
359pub fn apply_negative_parameter_step(
360 variables: &mut HashMap<String, VariableEnum>,
361 step: MatRef<f64>,
362 variable_order: &[String],
363) {
364 // Create a negated version of the step vector
365 let mut negative_step = Mat::zeros(step.nrows(), 1);
366 for i in 0..step.nrows() {
367 negative_step[(i, 0)] = -step[(i, 0)];
368 }
369
370 // Apply the negative step using the standard apply_parameter_step function
371 apply_parameter_step(variables, negative_step.as_ref(), variable_order);
372}
373
374pub fn compute_cost(residual: &Mat<f64>) -> f64 {
375 let cost = residual.norm_l2();
376 0.5 * cost * cost
377}