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}