apex_solver/linalg/mod.rs
1pub mod cholesky;
2pub mod qr;
3
4use crate::core::problem::VariableEnum;
5use faer::{Mat, sparse::SparseColMat};
6use std::{
7 collections::HashMap,
8 fmt,
9 fmt::{Display, Formatter},
10};
11use thiserror::Error;
12use tracing::error;
13
14#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
15pub enum LinearSolverType {
16 #[default]
17 SparseCholesky,
18 SparseQR,
19}
20
21impl Display for LinearSolverType {
22 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
23 match self {
24 LinearSolverType::SparseCholesky => write!(f, "Sparse Cholesky"),
25 LinearSolverType::SparseQR => write!(f, "Sparse QR"),
26 }
27 }
28}
29
30/// Linear algebra specific error types for apex-solver
31#[derive(Debug, Clone, Error)]
32pub enum LinAlgError {
33 /// Matrix factorization failed (Cholesky, QR, etc.)
34 #[error("Matrix factorization failed: {0}")]
35 FactorizationFailed(String),
36
37 /// Singular or near-singular matrix detected
38 #[error("Singular matrix detected (matrix is not invertible)")]
39 SingularMatrix,
40
41 /// Failed to create sparse matrix from triplets
42 #[error("Failed to create sparse matrix: {0}")]
43 SparseMatrixCreation(String),
44
45 /// Matrix format conversion failed
46 #[error("Matrix conversion failed: {0}")]
47 MatrixConversion(String),
48}
49
50impl LinAlgError {
51 /// Log the error with tracing::error and return self for chaining
52 ///
53 /// This method allows for a consistent error logging pattern throughout
54 /// the linalg module, ensuring all errors are properly recorded.
55 ///
56 /// # Example
57 /// ```ignore
58 /// operation()
59 /// .map_err(|e| LinAlgError::from(e).log())?;
60 /// ```
61 #[must_use]
62 pub fn log(self) -> Self {
63 error!("{}", self);
64 self
65 }
66
67 /// Log the error with the original source error from a third-party library
68 ///
69 /// This method logs both the LinAlgError and the underlying error
70 /// from external libraries (e.g., faer's FaerError, LltError, CreationError).
71 /// This provides full debugging context when errors occur in third-party code.
72 ///
73 /// # Arguments
74 /// * `source_error` - The original error from the third-party library (must implement Debug)
75 ///
76 /// # Example
77 /// ```ignore
78 /// SymbolicLlt::try_new(matrix.symbolic(), Side::Lower)
79 /// .map_err(|e| {
80 /// LinAlgError::FactorizationFailed(
81 /// "Symbolic Cholesky decomposition failed".to_string()
82 /// )
83 /// .log_with_source(e)
84 /// })?;
85 /// ```
86 #[must_use]
87 pub fn log_with_source<E: std::fmt::Debug>(self, source_error: E) -> Self {
88 error!("{} | Source: {:?}", self, source_error);
89 self
90 }
91}
92
93/// Result type for linear algebra operations
94pub type LinAlgResult<T> = Result<T, LinAlgError>;
95
96// /// Contains statistical information about the quality of the optimization solution.
97// #[derive(Debug, Clone)]
98// pub struct SolverElement {
99// /// The Hessian matrix, computed as `(J^T * W * J)`.
100// ///
101// /// This is `None` if the Hessian could not be computed.
102// pub hessian: Option<sparse::SparseColMat<usize, f64>>,
103
104// /// The gradient vector, computed as `J^T * W * r`.
105// ///
106// /// This is `None` if the gradient could not be computed.
107// pub gradient: Option<Mat<f64>>,
108
109// /// The parameter covariance matrix, computed as `(J^T * W * J)^-1`.
110// ///
111// /// This is `None` if the Hessian is singular or ill-conditioned.
112// pub covariance_matrix: Option<Mat<f64>>,
113// /// Asymptotic standard errors of the parameters.
114// ///
115// /// This is `None` if the covariance matrix could not be computed.
116// /// Each error is the square root of the corresponding diagonal element
117// /// of the covariance matrix.
118// pub standard_errors: Option<Mat<f64>>,
119// }
120
121/// Trait for sparse linear solvers that can solve both normal and augmented equations
122pub trait SparseLinearSolver {
123 /// Solve the normal equation: (J^T * J) * dx = -J^T * r
124 ///
125 /// # Errors
126 /// Returns `LinAlgError` if:
127 /// - Matrix factorization fails
128 /// - Matrix is singular or ill-conditioned
129 /// - Numerical instability is detected
130 fn solve_normal_equation(
131 &mut self,
132 residuals: &Mat<f64>,
133 jacobians: &SparseColMat<usize, f64>,
134 ) -> LinAlgResult<Mat<f64>>;
135
136 /// Solve the augmented equation: (J^T * J + λI) * dx = -J^T * r
137 ///
138 /// # Errors
139 /// Returns `LinAlgError` if:
140 /// - Matrix factorization fails
141 /// - Matrix is singular or ill-conditioned
142 /// - Numerical instability is detected
143 fn solve_augmented_equation(
144 &mut self,
145 residuals: &Mat<f64>,
146 jacobians: &SparseColMat<usize, f64>,
147 lambda: f64,
148 ) -> LinAlgResult<Mat<f64>>;
149
150 /// Get the cached Hessian matrix (J^T * J) from the last solve
151 fn get_hessian(&self) -> Option<&SparseColMat<usize, f64>>;
152
153 /// Get the cached gradient vector (J^T * r) from the last solve
154 fn get_gradient(&self) -> Option<&Mat<f64>>;
155
156 /// Compute the covariance matrix (H^{-1}) by inverting the cached Hessian
157 ///
158 /// Returns a reference to the covariance matrix if successful, None otherwise.
159 /// The covariance matrix represents parameter uncertainty in the tangent space.
160 fn compute_covariance_matrix(&mut self) -> Option<&Mat<f64>>;
161
162 /// Get the cached covariance matrix (H^{-1}) computed from the Hessian
163 ///
164 /// Returns None if covariance has not been computed yet.
165 fn get_covariance_matrix(&self) -> Option<&Mat<f64>>;
166}
167
168pub use cholesky::SparseCholeskySolver;
169pub use qr::SparseQRSolver;
170
171/// Extract per-variable covariance blocks from the full covariance matrix.
172///
173/// Given the full covariance matrix H^{-1} (inverse of information matrix),
174/// this function extracts the diagonal blocks corresponding to each individual variable.
175///
176/// # Arguments
177/// * `full_covariance` - Full covariance matrix of size n×n (from H^{-1})
178/// * `variables` - Map of variable names to their Variable objects
179/// * `variable_index_map` - Map from variable names to their starting column index in the full matrix
180///
181/// # Returns
182/// HashMap mapping variable names to their covariance matrices in tangent space.
183/// For SE3 variables, this would be 6×6 matrices; for SE2, 3×3; etc.
184///
185pub fn extract_variable_covariances(
186 full_covariance: &Mat<f64>,
187 variables: &HashMap<String, VariableEnum>,
188 variable_index_map: &HashMap<String, usize>,
189) -> HashMap<String, Mat<f64>> {
190 let mut result = HashMap::new();
191
192 for (var_name, var) in variables {
193 // Get the starting column/row index for this variable
194 if let Some(&start_idx) = variable_index_map.get(var_name) {
195 // Get the tangent space dimension for this variable
196 let dim = var.get_size();
197
198 // Extract the block diagonal covariance for this variable
199 // This is the block [start_idx:start_idx+dim, start_idx:start_idx+dim]
200 let mut var_cov = Mat::zeros(dim, dim);
201
202 for i in 0..dim {
203 for j in 0..dim {
204 var_cov[(i, j)] = full_covariance[(start_idx + i, start_idx + j)];
205 }
206 }
207
208 result.insert(var_name.clone(), var_cov);
209 }
210 }
211
212 result
213}