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 /// ```
124 /// # use apex_solver::optimizer::OptimizerError;
125 /// # fn operation() -> Result<(), OptimizerError> { Ok(()) }
126 /// # fn example() -> Result<(), OptimizerError> {
127 /// operation()
128 /// .map_err(|e| e.log())?;
129 /// # Ok(())
130 /// # }
131 /// ```
132 #[must_use]
133 pub fn log(self) -> Self {
134 error!("{}", self);
135 self
136 }
137
138 /// Log the error with the original source error from a third-party library
139 ///
140 /// This method logs both the OptimizerError and the underlying error
141 /// from external libraries. This provides full debugging context when
142 /// errors occur in third-party code.
143 ///
144 /// # Arguments
145 /// * `source_error` - The original error from the third-party library (must implement Debug)
146 ///
147 /// # Example
148 /// ```
149 /// # use apex_solver::optimizer::OptimizerError;
150 /// # fn sparse_matrix_op() -> Result<(), std::io::Error> { Ok(()) }
151 /// # fn example() -> Result<(), OptimizerError> {
152 /// sparse_matrix_op()
153 /// .map_err(|e| {
154 /// OptimizerError::JacobiScalingCreation(e.to_string())
155 /// .log_with_source(e)
156 /// })?;
157 /// # Ok(())
158 /// # }
159 /// ```
160 #[must_use]
161 pub fn log_with_source<E: std::fmt::Debug>(self, source_error: E) -> Self {
162 error!("{} | Source: {:?}", self, source_error);
163 self
164 }
165}
166
167/// Result type for optimizer operations
168pub type OptimizerResult<T> = Result<T, OptimizerError>;
169
170// State information during iterative optimization.
171// #[derive(Debug, Clone)]
172// pub struct IterativeState {
173// /// Current iteration number
174// pub iteration: usize,
175// /// Current cost value
176// pub cost: f64,
177// /// Current gradient norm
178// pub gradient_norm: f64,
179// /// Current parameter update norm
180// pub parameter_update_norm: f64,
181// /// Time elapsed since start
182// pub elapsed_time: Duration,
183// }
184
185/// Detailed convergence information.
186#[derive(Debug, Clone)]
187pub struct ConvergenceInfo {
188 /// Final gradient norm
189 pub final_gradient_norm: f64,
190 /// Final parameter update norm
191 pub final_parameter_update_norm: f64,
192 /// Cost function evaluation count
193 pub cost_evaluations: usize,
194 /// Jacobian evaluation count
195 pub jacobian_evaluations: usize,
196}
197
198impl Display for ConvergenceInfo {
199 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
200 write!(
201 f,
202 "Final gradient norm: {:.2e}, Final parameter update norm: {:.2e}, Cost evaluations: {}, Jacobian evaluations: {}",
203 self.final_gradient_norm,
204 self.final_parameter_update_norm,
205 self.cost_evaluations,
206 self.jacobian_evaluations
207 )
208 }
209}
210
211/// Status of an optimization process
212#[derive(Debug, Clone, PartialEq, Eq)]
213pub enum OptimizationStatus {
214 /// Optimization converged successfully
215 Converged,
216 /// Maximum number of iterations reached
217 MaxIterationsReached,
218 /// Cost function tolerance reached
219 CostToleranceReached,
220 /// Parameter tolerance reached
221 ParameterToleranceReached,
222 /// Gradient tolerance reached
223 GradientToleranceReached,
224 /// Optimization failed due to numerical issues
225 NumericalFailure,
226 /// User requested termination
227 UserTerminated,
228 /// Timeout reached
229 Timeout,
230 /// Trust region radius fell below minimum threshold
231 TrustRegionRadiusTooSmall,
232 /// Objective function fell below user-specified cutoff
233 MinCostThresholdReached,
234 /// Jacobian matrix is singular or ill-conditioned
235 IllConditionedJacobian,
236 /// NaN or Inf detected in cost or parameters
237 InvalidNumericalValues,
238 /// Other failure
239 Failed(String),
240}
241
242impl Display for OptimizationStatus {
243 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
244 match self {
245 OptimizationStatus::Converged => write!(f, "Converged"),
246 OptimizationStatus::MaxIterationsReached => write!(f, "Maximum iterations reached"),
247 OptimizationStatus::CostToleranceReached => write!(f, "Cost tolerance reached"),
248 OptimizationStatus::ParameterToleranceReached => {
249 write!(f, "Parameter tolerance reached")
250 }
251 OptimizationStatus::GradientToleranceReached => write!(f, "Gradient tolerance reached"),
252 OptimizationStatus::NumericalFailure => write!(f, "Numerical failure"),
253 OptimizationStatus::UserTerminated => write!(f, "User terminated"),
254 OptimizationStatus::Timeout => write!(f, "Timeout"),
255 OptimizationStatus::TrustRegionRadiusTooSmall => {
256 write!(f, "Trust region radius too small")
257 }
258 OptimizationStatus::MinCostThresholdReached => {
259 write!(f, "Minimum cost threshold reached")
260 }
261 OptimizationStatus::IllConditionedJacobian => {
262 write!(f, "Ill-conditioned Jacobian matrix")
263 }
264 OptimizationStatus::InvalidNumericalValues => {
265 write!(f, "Invalid numerical values (NaN/Inf) detected")
266 }
267 OptimizationStatus::Failed(msg) => write!(f, "Failed: {msg}"),
268 }
269 }
270}
271
272/// Result of a solver execution.
273#[derive(Clone)]
274pub struct SolverResult<T> {
275 /// Final parameters
276 pub parameters: T,
277 /// Final optimization status
278 pub status: OptimizationStatus,
279 /// Initial cost value
280 pub initial_cost: f64,
281 /// Final cost value
282 pub final_cost: f64,
283 /// Number of iterations performed
284 pub iterations: usize,
285 /// Total time elapsed
286 pub elapsed_time: time::Duration,
287 /// Convergence statistics
288 pub convergence_info: Option<ConvergenceInfo>,
289 /// Per-variable covariance matrices (uncertainty estimation)
290 ///
291 /// This is `None` if covariance computation was not enabled in the solver configuration.
292 /// When present, it contains a mapping from variable names to their covariance matrices
293 /// in tangent space. For example, for SE3 variables this would be 6×6 matrices.
294 ///
295 /// Enable covariance computation by setting `compute_covariances: true` in the optimizer config.
296 pub covariances: Option<HashMap<String, Mat<f64>>>,
297}
298
299/// Core trait for optimization solvers.
300pub trait Solver {
301 /// Configuration type for this solver
302 type Config;
303 /// Error type
304 type Error;
305
306 /// Create a new solver with the given configuration
307 fn new() -> Self;
308
309 /// Optimize the problem to minimize the cost function
310 fn optimize(
311 &mut self,
312 problem: &Problem,
313 initial_params: &HashMap<String, (ManifoldType, DVector<f64>)>,
314 ) -> Result<SolverResult<HashMap<String, VariableEnum>>, Self::Error>;
315}
316
317/// Apply parameter update step to all variables.
318///
319/// This is a common operation used by all optimizers (Levenberg-Marquardt, Gauss-Newton, Dog Leg).
320/// It applies a tangent space perturbation to each variable using the proper manifold plus operation.
321///
322/// # Arguments
323/// * `variables` - Mutable map of variables to update
324/// * `step` - Full step vector in tangent space (faer matrix view)
325/// * `variable_order` - Ordered list of variable names (defines indexing into step vector)
326///
327/// # Returns
328/// * Step norm (L2 norm) for convergence checking
329///
330/// # Implementation Notes
331/// The step vector contains concatenated tangent vectors for all variables in the order
332/// specified by `variable_order`. Each variable's tangent space dimension determines
333/// how many elements it occupies in the step vector.
334///
335pub fn apply_parameter_step(
336 variables: &mut HashMap<String, VariableEnum>,
337 step: MatRef<f64>,
338 variable_order: &[String],
339) -> f64 {
340 let mut step_offset = 0;
341
342 for var_name in variable_order {
343 if let Some(var) = variables.get_mut(var_name) {
344 let var_size = var.get_size();
345 let var_step = step.subrows(step_offset, var_size);
346
347 // Delegate to VariableEnum's apply_tangent_step method
348 // This handles all manifold types (SE2, SE3, SO2, SO3, Rn)
349 var.apply_tangent_step(var_step);
350
351 step_offset += var_size;
352 }
353 }
354
355 // Compute and return step norm for convergence checking
356 step.norm_l2()
357}
358
359/// Apply negative parameter step to rollback variables.
360///
361/// This is used when an optimization step is rejected (e.g., in trust region methods).
362/// It applies the negative of a tangent space perturbation to revert the previous update.
363///
364/// # Arguments
365/// * `variables` - Mutable map of variables to revert
366/// * `step` - Full step vector in tangent space (faer matrix view) to negate
367/// * `variable_order` - Ordered list of variable names (defines indexing into step vector)
368///
369pub fn apply_negative_parameter_step(
370 variables: &mut HashMap<String, VariableEnum>,
371 step: MatRef<f64>,
372 variable_order: &[String],
373) {
374 // Create a negated version of the step vector
375 let mut negative_step = Mat::zeros(step.nrows(), 1);
376 for i in 0..step.nrows() {
377 negative_step[(i, 0)] = -step[(i, 0)];
378 }
379
380 // Apply the negative step using the standard apply_parameter_step function
381 apply_parameter_step(variables, negative_step.as_ref(), variable_order);
382}
383
384pub fn compute_cost(residual: &Mat<f64>) -> f64 {
385 let cost = residual.norm_l2();
386 0.5 * cost * cost
387}