apex_solver/linalg/dense/
cholesky.rs1use faer::{
2 Mat, Side,
3 linalg::solvers::{Llt, LltError, Solve},
4};
5
6use crate::error::ErrorLogging;
7use crate::linalg::{DenseMode, LinAlgError, LinAlgResult, LinearSolver};
8
9#[derive(Debug, Clone)]
15pub struct DenseCholeskySolver {
16 hessian: Option<Mat<f64>>,
18
19 gradient: Option<Mat<f64>>,
21
22 factorizer: Option<Llt<f64>>,
24
25 covariance_matrix: Option<Mat<f64>>,
27}
28
29impl DenseCholeskySolver {
30 pub fn new() -> Self {
31 Self {
32 hessian: None,
33 gradient: None,
34 factorizer: None,
35 covariance_matrix: None,
36 }
37 }
38
39 fn solve_dense_normal(
41 &mut self,
42 residuals: &Mat<f64>,
43 jacobian: &Mat<f64>,
44 ) -> LinAlgResult<Mat<f64>> {
45 let hessian = jacobian.transpose() * jacobian;
47 let gradient = jacobian.transpose() * residuals;
49
50 let llt = hessian
52 .as_ref()
53 .llt(Side::Lower)
54 .map_err(|e| map_llt_error(e, "Dense Cholesky factorization failed"))?;
55
56 let dx = llt.solve(-&gradient);
58
59 self.factorizer = Some(llt);
60 self.hessian = Some(hessian);
61 self.gradient = Some(gradient);
62 self.covariance_matrix = None;
63
64 Ok(dx)
65 }
66
67 fn solve_dense_augmented(
69 &mut self,
70 residuals: &Mat<f64>,
71 jacobian: &Mat<f64>,
72 lambda: f64,
73 ) -> LinAlgResult<Mat<f64>> {
74 let hessian = jacobian.transpose() * jacobian;
76 let gradient = jacobian.transpose() * residuals;
78
79 let n = hessian.nrows();
81 let mut augmented = hessian.clone();
82 for i in 0..n {
83 augmented[(i, i)] += lambda;
84 }
85
86 let llt = augmented
88 .as_ref()
89 .llt(Side::Lower)
90 .map_err(|e| map_llt_error(e, "Augmented dense Cholesky factorization failed"))?;
91
92 let dx = llt.solve(-&gradient);
94
95 self.factorizer = Some(llt);
97 self.hessian = Some(hessian);
98 self.gradient = Some(gradient);
99 self.covariance_matrix = None;
100
101 Ok(dx)
102 }
103}
104
105impl Default for DenseCholeskySolver {
106 fn default() -> Self {
107 Self::new()
108 }
109}
110
111impl LinearSolver<DenseMode> for DenseCholeskySolver {
116 fn solve_normal_equation(
117 &mut self,
118 residuals: &Mat<f64>,
119 jacobian: &Mat<f64>,
120 ) -> LinAlgResult<Mat<f64>> {
121 self.solve_dense_normal(residuals, jacobian)
122 }
123
124 fn solve_augmented_equation(
125 &mut self,
126 residuals: &Mat<f64>,
127 jacobian: &Mat<f64>,
128 lambda: f64,
129 ) -> LinAlgResult<Mat<f64>> {
130 self.solve_dense_augmented(residuals, jacobian, lambda)
131 }
132
133 fn get_hessian(&self) -> Option<&Mat<f64>> {
134 self.hessian.as_ref()
135 }
136
137 fn get_gradient(&self) -> Option<&Mat<f64>> {
138 self.gradient.as_ref()
139 }
140
141 fn compute_covariance_matrix(&mut self) -> Option<&Mat<f64>> {
142 if self.covariance_matrix.is_none()
143 && let Some(hessian) = &self.hessian
144 && let Some(factorizer) = &self.factorizer
145 {
146 let n = hessian.nrows();
147 let identity = Mat::identity(n, n);
148 let cov = factorizer.solve(&identity);
149 self.covariance_matrix = Some(cov);
150 }
151 self.covariance_matrix.as_ref()
152 }
153
154 fn get_covariance_matrix(&self) -> Option<&Mat<f64>> {
155 self.covariance_matrix.as_ref()
156 }
157}
158
159fn map_llt_error(e: LltError, context: &str) -> LinAlgError {
161 LinAlgError::FactorizationFailed(format!("{context}: {e:?}")).log()
162}
163
164#[cfg(test)]
165mod tests {
166 use super::*;
167
168 const TOLERANCE: f64 = 1e-10;
169
170 type TestResult = Result<(), Box<dyn std::error::Error>>;
171
172 fn create_test_data() -> (Mat<f64>, Mat<f64>) {
173 let mut j = Mat::zeros(4, 2);
175 j[(0, 0)] = 2.0;
176 j[(0, 1)] = 1.0;
177 j[(1, 0)] = 1.0;
178 j[(1, 1)] = 3.0;
179 j[(2, 0)] = 1.0;
180 j[(2, 1)] = 1.0;
181 j[(3, 0)] = 0.5;
182 j[(3, 1)] = 2.0;
183
184 let mut r = Mat::zeros(4, 1);
185 r[(0, 0)] = 1.0;
186 r[(1, 0)] = 2.0;
187 r[(2, 0)] = 0.5;
188 r[(3, 0)] = 1.5;
189
190 (j, r)
191 }
192
193 #[test]
194 fn test_dense_cholesky_solve_normal() -> TestResult {
195 let (j, r) = create_test_data();
196 let mut solver = DenseCholeskySolver::new();
197
198 let dx = LinearSolver::<DenseMode>::solve_normal_equation(&mut solver, &r, &j)?;
199
200 let jtj = j.transpose() * &j;
202 let jtr = j.transpose() * &r;
203 let residual = &jtj * &dx + &jtr;
204
205 for i in 0..dx.nrows() {
206 assert!(
207 residual[(i, 0)].abs() < TOLERANCE,
208 "Residual at index {}: {}",
209 i,
210 residual[(i, 0)]
211 );
212 }
213
214 assert!(solver.hessian.is_some());
215 assert!(solver.gradient.is_some());
216
217 Ok(())
218 }
219
220 #[test]
221 fn test_dense_cholesky_solve_augmented() -> TestResult {
222 let (j, r) = create_test_data();
223 let lambda = 0.1;
224 let mut solver = DenseCholeskySolver::new();
225
226 let dx = LinearSolver::<DenseMode>::solve_augmented_equation(&mut solver, &r, &j, lambda)?;
227
228 let mut jtj = j.transpose() * &j;
230 let jtr = j.transpose() * &r;
231 for i in 0..jtj.nrows() {
232 jtj[(i, i)] += lambda;
233 }
234 let residual = &jtj * &dx + &jtr;
235
236 for i in 0..dx.nrows() {
237 assert!(
238 residual[(i, 0)].abs() < TOLERANCE,
239 "Residual at index {}: {}",
240 i,
241 residual[(i, 0)]
242 );
243 }
244
245 Ok(())
246 }
247
248 #[test]
249 fn test_dense_cholesky_covariance_computation() -> TestResult {
250 let (j, r) = create_test_data();
251 let mut solver = DenseCholeskySolver::new();
252
253 LinearSolver::<DenseMode>::solve_normal_equation(&mut solver, &r, &j)?;
254
255 let cov = LinearSolver::<DenseMode>::compute_covariance_matrix(&mut solver)
256 .ok_or("covariance should be computable")?;
257 let n = cov.nrows();
258
259 for i in 0..n {
261 for k in 0..n {
262 assert!(
263 (cov[(i, k)] - cov[(k, i)]).abs() < TOLERANCE,
264 "Covariance not symmetric at ({i}, {k})"
265 );
266 }
267 }
268
269 for i in 0..n {
271 assert!(cov[(i, i)] > 0.0, "Diagonal entry {i} should be positive");
272 }
273
274 Ok(())
275 }
276
277 #[test]
278 fn test_dense_cholesky_covariance_well_conditioned() -> TestResult {
279 let mut solver = DenseCholeskySolver::new();
280
281 let mut j = Mat::zeros(2, 2);
283 j[(0, 0)] = 2.0;
284 j[(1, 1)] = 3.0;
285
286 let mut r = Mat::zeros(2, 1);
287 r[(0, 0)] = 1.0;
288 r[(1, 0)] = 2.0;
289
290 LinearSolver::<DenseMode>::solve_normal_equation(&mut solver, &r, &j)?;
291
292 let cov = LinearSolver::<DenseMode>::compute_covariance_matrix(&mut solver)
293 .ok_or("covariance computation failed")?;
294 assert!(
295 (cov[(0, 0)] - 0.25).abs() < TOLERANCE,
296 "cov[0,0] should be 0.25"
297 );
298 assert!(
299 (cov[(1, 1)] - 1.0 / 9.0).abs() < TOLERANCE,
300 "cov[1,1] should be 1/9"
301 );
302 assert!(cov[(0, 1)].abs() < TOLERANCE);
303 assert!(cov[(1, 0)].abs() < TOLERANCE);
304
305 Ok(())
306 }
307
308 #[test]
309 fn test_dense_cholesky_covariance_caching() -> TestResult {
310 let (j, r) = create_test_data();
311 let mut solver = DenseCholeskySolver::new();
312
313 LinearSolver::<DenseMode>::solve_normal_equation(&mut solver, &r, &j)?;
314
315 LinearSolver::<DenseMode>::compute_covariance_matrix(&mut solver);
316 let ptr1 = solver
317 .covariance_matrix
318 .as_ref()
319 .ok_or("covariance not cached after first call")?
320 .as_ptr();
321
322 LinearSolver::<DenseMode>::compute_covariance_matrix(&mut solver);
324 let ptr2 = solver
325 .covariance_matrix
326 .as_ref()
327 .ok_or("covariance not cached after second call")?
328 .as_ptr();
329
330 assert_eq!(ptr1, ptr2, "Covariance matrix should be cached");
331
332 Ok(())
333 }
334
335 #[test]
341 fn test_accessors_before_solve() {
342 let solver = DenseCholeskySolver::new();
343 assert!(
344 LinearSolver::<DenseMode>::get_hessian(&solver).is_none(),
345 "hessian should be None before solve"
346 );
347 assert!(
348 LinearSolver::<DenseMode>::get_gradient(&solver).is_none(),
349 "gradient should be None before solve"
350 );
351 }
352
353 #[test]
355 fn test_get_covariance_before_solve() {
356 let mut solver = DenseCholeskySolver::new();
357 assert!(
359 LinearSolver::<DenseMode>::compute_covariance_matrix(&mut solver).is_none(),
360 "covariance should be None when no factorizer is cached"
361 );
362 assert!(
363 LinearSolver::<DenseMode>::get_covariance_matrix(&solver).is_none(),
364 "get_covariance_matrix should be None before compute"
365 );
366 }
367
368 #[test]
370 fn test_default_equals_new() {
371 let solver = DenseCholeskySolver::default();
372 assert!(solver.hessian.is_none());
373 assert!(solver.gradient.is_none());
374 assert!(solver.factorizer.is_none());
375 assert!(solver.covariance_matrix.is_none());
376 }
377
378 #[test]
380 fn test_singular_jacobian_returns_error() {
381 let mut solver = DenseCholeskySolver::new();
382
383 let mut j = Mat::zeros(3, 2);
385 j[(0, 0)] = 1.0;
386 j[(1, 0)] = 2.0;
387 j[(2, 0)] = 3.0;
388 let mut r = Mat::zeros(3, 1);
391 r[(0, 0)] = 1.0;
392
393 let result = LinearSolver::<DenseMode>::solve_normal_equation(&mut solver, &r, &j);
394 assert!(
395 result.is_err(),
396 "Singular Jacobian must produce a factorization error"
397 );
398 }
399
400 #[test]
402 fn test_covariance_reset_after_second_solve() -> TestResult {
403 let (j, r) = create_test_data();
404 let mut solver = DenseCholeskySolver::new();
405
406 LinearSolver::<DenseMode>::solve_normal_equation(&mut solver, &r, &j)?;
408 LinearSolver::<DenseMode>::compute_covariance_matrix(&mut solver);
409 assert!(
410 solver.covariance_matrix.is_some(),
411 "covariance should exist after first compute"
412 );
413
414 LinearSolver::<DenseMode>::solve_normal_equation(&mut solver, &r, &j)?;
416 assert!(
417 solver.covariance_matrix.is_none(),
418 "covariance should be None after a new solve"
419 );
420 Ok(())
421 }
422
423 #[test]
425 fn test_augmented_with_zero_lambda_matches_normal() -> TestResult {
426 let (j, r) = create_test_data();
427 let mut solver_n = DenseCholeskySolver::new();
428 let mut solver_a = DenseCholeskySolver::new();
429
430 let dx_normal = LinearSolver::<DenseMode>::solve_normal_equation(&mut solver_n, &r, &j)?;
431 let dx_augmented =
432 LinearSolver::<DenseMode>::solve_augmented_equation(&mut solver_a, &r, &j, 0.0)?;
433
434 for i in 0..dx_normal.nrows() {
435 assert!(
436 (dx_normal[(i, 0)] - dx_augmented[(i, 0)]).abs() < TOLERANCE,
437 "Element {} differs: normal={}, augmented(λ=0)={}",
438 i,
439 dx_normal[(i, 0)],
440 dx_augmented[(i, 0)]
441 );
442 }
443 Ok(())
444 }
445}