1use faer::{
2 Mat, Side,
3 linalg::solvers::Solve,
4 sparse::SparseColMat,
5 sparse::linalg::solvers::{Llt, SymbolicLlt},
6};
7
8use crate::error::ErrorLogging;
9use crate::linalg::sparse::normal_eq::{LazyNormalEquations, NormalEquations};
10use crate::linalg::{Damping, LinAlgError, LinAlgResult, LinearSolver, SparseMode};
11
12#[derive(Debug, Clone)]
13pub struct SparseCholeskySolver {
14 symbolic_factorization: Option<SymbolicLlt<usize>>,
19
20 ne_cache: LazyNormalEquations,
22
23 hessian: Option<SparseColMat<usize, f64>>,
27
28 gradient: Option<Mat<f64>>,
32}
33
34impl SparseCholeskySolver {
35 pub fn new() -> Self {
36 SparseCholeskySolver {
37 symbolic_factorization: None,
38 ne_cache: LazyNormalEquations::default(),
39 hessian: None,
40 gradient: None,
41 }
42 }
43
44 pub fn hessian(&self) -> Option<&SparseColMat<usize, f64>> {
45 self.hessian.as_ref()
46 }
47
48 pub fn gradient(&self) -> Option<&Mat<f64>> {
49 self.gradient.as_ref()
50 }
51}
52
53impl Default for SparseCholeskySolver {
54 fn default() -> Self {
55 Self::new()
56 }
57}
58impl LinearSolver<SparseMode> for SparseCholeskySolver {
59 fn solve_normal_equation(
60 &mut self,
61 residuals: &Mat<f64>,
62 jacobians: &SparseColMat<usize, f64>,
63 ) -> LinAlgResult<Mat<f64>> {
64 let NormalEquations { hessian, gradient } = self.ne_cache.compute(residuals, jacobians)?;
67
68 let sym = if let Some(ref cached_sym) = self.symbolic_factorization {
69 cached_sym.clone()
74 } else {
75 let new_sym = SymbolicLlt::try_new(hessian.symbolic(), Side::Lower).map_err(|e| {
77 LinAlgError::FactorizationFailed(
78 "Symbolic Cholesky decomposition failed".to_string(),
79 )
80 .log_with_source(e)
81 })?;
82 self.symbolic_factorization = Some(new_sym.clone());
84 new_sym
85 };
86
87 let cholesky =
89 Llt::try_new_with_symbolic(sym, hessian.as_ref(), Side::Lower).map_err(|e| {
90 LinAlgError::SingularMatrix(
91 "Cholesky factorization failed (matrix may be singular)".to_string(),
92 )
93 .log_with_source(e)
94 })?;
95
96 let dx = cholesky.solve(-&gradient);
97 self.hessian = Some(hessian);
98 self.gradient = Some(gradient);
99
100 Ok(dx)
101 }
102
103 fn solve_augmented_equation(
104 &mut self,
105 residuals: &Mat<f64>,
106 jacobians: &SparseColMat<usize, f64>,
107 damping: &Damping,
108 ) -> LinAlgResult<Mat<f64>> {
109 let NormalEquations { hessian, gradient } = self.ne_cache.compute(residuals, jacobians)?;
111
112 let augmented_hessian = self.ne_cache.damped_hessian(damping)?;
114
115 let sym = if let Some(ref cached_sym) = self.symbolic_factorization {
116 cached_sym.clone()
121 } else {
122 let new_sym =
124 SymbolicLlt::try_new(augmented_hessian.symbolic(), Side::Lower).map_err(|e| {
125 LinAlgError::FactorizationFailed(
126 "Symbolic Cholesky decomposition failed for augmented system".to_string(),
127 )
128 .log_with_source(e)
129 })?;
130 self.symbolic_factorization = Some(new_sym.clone());
132 new_sym
133 };
134
135 let cholesky = Llt::try_new_with_symbolic(sym, augmented_hessian.as_ref(), Side::Lower)
137 .map_err(|e| {
138 LinAlgError::SingularMatrix(
139 "Cholesky factorization failed (matrix may be singular)".to_string(),
140 )
141 .log_with_source(e)
142 })?;
143
144 let dx = cholesky.solve(-&gradient);
145 self.hessian = Some(hessian);
146 self.gradient = Some(gradient);
147
148 Ok(dx)
149 }
150
151 fn hessian_vec_product(&self, v: &Mat<f64>) -> Option<Mat<f64>> {
152 Some(
153 <SparseMode as crate::linearizer::AssemblyBackend>::hessian_vec_product(
154 self.hessian.as_ref()?,
155 v,
156 ),
157 )
158 }
159
160 fn get_hessian(&self) -> Option<&SparseColMat<usize, f64>> {
161 self.hessian.as_ref()
162 }
163
164 fn get_gradient(&self) -> Option<&Mat<f64>> {
165 self.gradient.as_ref()
166 }
167}
168
169#[cfg(test)]
170mod tests {
171 use super::*;
172 use faer::sparse::Triplet;
173
174 const TOLERANCE: f64 = 1e-10;
175
176 type TestResult = Result<(), Box<dyn std::error::Error>>;
177
178 fn create_test_data()
180 -> Result<(SparseColMat<usize, f64>, Mat<f64>), faer::sparse::CreationError> {
181 let triplets = vec![
183 Triplet::new(0, 0, 2.0),
184 Triplet::new(0, 1, 1.0),
185 Triplet::new(1, 0, 1.0),
186 Triplet::new(1, 1, 3.0),
187 Triplet::new(1, 2, 1.0),
188 Triplet::new(2, 1, 1.0),
189 Triplet::new(2, 2, 2.0),
190 Triplet::new(3, 0, 1.5), Triplet::new(3, 2, 0.5),
192 ];
193 let jacobian = SparseColMat::try_new_from_triplets(4, 3, &triplets)?;
194
195 let residuals = Mat::from_fn(4, 1, |i, _| match i {
196 0 => 1.0,
197 1 => -2.0,
198 2 => 0.5,
199 3 => 1.2,
200 _ => 0.0,
201 });
202
203 Ok((jacobian, residuals))
204 }
205
206 #[test]
208 fn test_solver_creation() {
209 let solver = SparseCholeskySolver::new();
210 assert!(solver.hessian.is_none());
211 assert!(solver.gradient.is_none());
212
213 let default_solver = SparseCholeskySolver::default();
214 assert!(default_solver.hessian.is_none());
215 assert!(default_solver.gradient.is_none());
216 }
217
218 #[test]
220 fn test_solve_normal_equation_well_conditioned() -> TestResult {
221 let mut solver = SparseCholeskySolver::new();
222 let (jacobian, residuals) = create_test_data()?;
223
224 let solution =
225 LinearSolver::<SparseMode>::solve_normal_equation(&mut solver, &residuals, &jacobian)?;
226 assert_eq!(solution.nrows(), 3);
227 assert_eq!(solution.ncols(), 1);
228
229 Ok(())
231 }
232
233 #[test]
235 fn test_symbolic_pattern_caching() -> TestResult {
236 let mut solver = SparseCholeskySolver::new();
237 let (jacobian, residuals) = create_test_data()?;
238
239 let sol1 =
241 LinearSolver::<SparseMode>::solve_normal_equation(&mut solver, &residuals, &jacobian)?;
242
243 let sol2 =
245 LinearSolver::<SparseMode>::solve_normal_equation(&mut solver, &residuals, &jacobian)?;
246
247 for i in 0..sol1.nrows() {
249 assert!((sol1[(i, 0)] - sol2[(i, 0)]).abs() < TOLERANCE);
250 }
251 Ok(())
252 }
253
254 #[test]
256 fn test_solve_augmented_equation() -> TestResult {
257 let mut solver = SparseCholeskySolver::new();
258 let (jacobian, residuals) = create_test_data()?;
259 let lambda = 0.1;
260
261 let solution = LinearSolver::<SparseMode>::solve_augmented_equation(
262 &mut solver,
263 &residuals,
264 &jacobian,
265 &Damping::identity(lambda),
266 )?;
267 assert_eq!(solution.nrows(), 3);
268 assert_eq!(solution.ncols(), 1);
269 Ok(())
270 }
271
272 #[test]
274 fn test_augmented_equation_different_lambdas() -> TestResult {
275 let mut solver = SparseCholeskySolver::new();
276 let (jacobian, residuals) = create_test_data()?;
277
278 let lambda1 = 0.01;
279 let lambda2 = 1.0;
280
281 let sol1 = LinearSolver::<SparseMode>::solve_augmented_equation(
282 &mut solver,
283 &residuals,
284 &jacobian,
285 &Damping::identity(lambda1),
286 )?;
287 let sol2 = LinearSolver::<SparseMode>::solve_augmented_equation(
288 &mut solver,
289 &residuals,
290 &jacobian,
291 &Damping::identity(lambda2),
292 )?;
293
294 let mut different = false;
296 for i in 0..sol1.nrows() {
297 if (sol1[(i, 0)] - sol2[(i, 0)]).abs() > TOLERANCE {
298 different = true;
299 break;
300 }
301 }
302 assert!(
303 different,
304 "Solutions should differ with different lambda values"
305 );
306 Ok(())
307 }
308
309 #[test]
311 fn test_singular_matrix() -> TestResult {
312 let mut solver = SparseCholeskySolver::new();
313
314 let triplets = vec![
316 Triplet::new(0, 0, 1.0),
317 Triplet::new(0, 1, 2.0),
318 Triplet::new(1, 0, 2.0),
319 Triplet::new(1, 1, 4.0), ];
321 let singular_jacobian = SparseColMat::try_new_from_triplets(2, 2, &triplets)?;
322 let residuals = Mat::from_fn(2, 1, |i, _| i as f64);
323
324 let result = LinearSolver::<SparseMode>::solve_normal_equation(
325 &mut solver,
326 &residuals,
327 &singular_jacobian,
328 );
329 assert!(result.is_err(), "Singular matrix should return Err");
331 Ok(())
332 }
333
334 #[test]
336 fn test_empty_matrix() -> TestResult {
337 let mut solver = SparseCholeskySolver::new();
338
339 let empty_jacobian = SparseColMat::try_new_from_triplets(0, 0, &[])?;
340 let empty_residuals = Mat::zeros(0, 1);
341
342 let result = LinearSolver::<SparseMode>::solve_normal_equation(
343 &mut solver,
344 &empty_residuals,
345 &empty_jacobian,
346 );
347 if let Ok(solution) = result {
348 assert_eq!(solution.nrows(), 0);
349 }
350 Ok(())
351 }
352
353 #[test]
355 fn test_numerical_accuracy() -> TestResult {
356 let mut solver = SparseCholeskySolver::new();
357
358 let triplets = vec![
360 Triplet::new(0, 0, 1.0),
361 Triplet::new(0, 1, 0.0),
362 Triplet::new(1, 0, 0.0),
363 Triplet::new(1, 1, 1.0),
364 ];
365 let jacobian = SparseColMat::try_new_from_triplets(2, 2, &triplets)?;
366 let residuals = Mat::from_fn(2, 1, |i, _| -((i + 1) as f64)); let solution =
369 LinearSolver::<SparseMode>::solve_normal_equation(&mut solver, &residuals, &jacobian)?;
370 assert!((solution[(0, 0)] - 1.0).abs() < TOLERANCE);
372 assert!((solution[(1, 0)] - 2.0).abs() < TOLERANCE);
373 Ok(())
374 }
375
376 #[test]
378 fn test_solver_clone() {
379 let solver1 = SparseCholeskySolver::new();
380 let solver2 = solver1.clone();
381 assert!(solver2.hessian.is_none());
382 assert!(solver2.gradient.is_none());
383 }
384
385 #[test]
387 fn test_cholesky_decomposition_properties() -> TestResult {
388 let mut solver = SparseCholeskySolver::new();
389
390 let triplets = vec![Triplet::new(0, 0, 2.0), Triplet::new(1, 1, 3.0)];
392 let jacobian = SparseColMat::try_new_from_triplets(2, 2, &triplets)?;
393 let residuals = Mat::from_fn(2, 1, |i, _| (i + 1) as f64);
394
395 LinearSolver::<SparseMode>::solve_normal_equation(&mut solver, &residuals, &jacobian)?;
396
397 assert!(solver.hessian.is_some());
399
400 if let Some(hessian) = &solver.hessian {
402 assert_eq!(hessian.nrows(), 2);
403 assert_eq!(hessian.ncols(), 2);
404 }
405 Ok(())
406 }
407
408 #[test]
410 fn test_cholesky_numerical_stability() -> TestResult {
411 let mut solver = SparseCholeskySolver::new();
412
413 let triplets = vec![
415 Triplet::new(0, 0, 1.0),
416 Triplet::new(1, 1, 1.0),
417 Triplet::new(2, 2, 1.0),
418 ];
419 let jacobian = SparseColMat::try_new_from_triplets(3, 3, &triplets)?;
420 let residuals = Mat::from_fn(3, 1, |i, _| -((i + 1) as f64)); let solution =
423 LinearSolver::<SparseMode>::solve_normal_equation(&mut solver, &residuals, &jacobian)?;
424 for i in 0..3 {
426 let expected = (i + 1) as f64;
427 assert!(
428 (solution[(i, 0)] - expected).abs() < TOLERANCE,
429 "Expected {}, got {}",
430 expected,
431 solution[(i, 0)]
432 );
433 }
434
435 Ok(())
436 }
437
438 #[test]
440 fn test_cholesky_hessian_getter() -> TestResult {
441 let mut solver = SparseCholeskySolver::new();
442 assert!(solver.hessian().is_none());
443
444 let (jacobian, residuals) = create_test_data()?;
445 LinearSolver::<SparseMode>::solve_normal_equation(&mut solver, &residuals, &jacobian)?;
446
447 assert!(solver.hessian().is_some());
448 Ok(())
449 }
450
451 #[test]
453 fn test_cholesky_gradient_getter() -> TestResult {
454 let mut solver = SparseCholeskySolver::new();
455 assert!(solver.gradient().is_none());
456
457 let (jacobian, residuals) = create_test_data()?;
458 LinearSolver::<SparseMode>::solve_normal_equation(&mut solver, &residuals, &jacobian)?;
459
460 assert!(solver.gradient().is_some());
461 Ok(())
462 }
463
464 #[test]
466 fn test_cholesky_get_hessian_trait() -> TestResult {
467 let mut solver = SparseCholeskySolver::new();
468 assert!(LinearSolver::<SparseMode>::get_hessian(&solver).is_none());
469
470 let (jacobian, residuals) = create_test_data()?;
471 LinearSolver::<SparseMode>::solve_normal_equation(&mut solver, &residuals, &jacobian)?;
472
473 assert!(LinearSolver::<SparseMode>::get_hessian(&solver).is_some());
474 Ok(())
475 }
476
477 #[test]
479 fn test_cholesky_get_gradient_trait() -> TestResult {
480 let mut solver = SparseCholeskySolver::new();
481 assert!(LinearSolver::<SparseMode>::get_gradient(&solver).is_none());
482
483 let (jacobian, residuals) = create_test_data()?;
484 LinearSolver::<SparseMode>::solve_normal_equation(&mut solver, &residuals, &jacobian)?;
485
486 assert!(LinearSolver::<SparseMode>::get_gradient(&solver).is_some());
487 Ok(())
488 }
489}