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}