1use faer::{
2 Mat,
3 linalg::solvers::Solve,
4 sparse::linalg::solvers::{Qr, SymbolicQr},
5 sparse::{SparseColMat, Triplet},
6};
7use std::ops::Mul;
8
9use crate::linalg::{LinAlgError, LinAlgResult, SparseLinearSolver};
10
11#[derive(Debug, Clone)]
12pub struct SparseQRSolver {
13 factorizer: Option<Qr<usize, f64>>,
14
15 symbolic_factorization: Option<SymbolicQr<usize>>,
22
23 hessian: Option<SparseColMat<usize, f64>>,
27
28 gradient: Option<Mat<f64>>,
32
33 covariance_matrix: Option<Mat<f64>>,
37 standard_errors: Option<Mat<f64>>,
43}
44
45impl SparseQRSolver {
46 pub fn new() -> Self {
47 SparseQRSolver {
48 factorizer: None,
49 symbolic_factorization: None,
50 hessian: None,
51 gradient: None,
52 covariance_matrix: None,
53 standard_errors: None,
54 }
55 }
56
57 pub fn hessian(&self) -> Option<&SparseColMat<usize, f64>> {
58 self.hessian.as_ref()
59 }
60
61 pub fn gradient(&self) -> Option<&Mat<f64>> {
62 self.gradient.as_ref()
63 }
64
65 pub fn compute_standard_errors(&mut self) -> Option<&Mat<f64>> {
66 if self.covariance_matrix.is_none() {
68 self.compute_covariance_matrix();
69 }
70
71 let hessian = self.hessian.as_ref()?;
73 let n = hessian.ncols();
74 if let Some(cov) = &self.covariance_matrix {
76 let mut std_errors = Mat::zeros(n, 1);
77 for i in 0..n {
78 let diag_val = cov[(i, i)];
79 if diag_val >= 0.0 {
80 std_errors[(i, 0)] = diag_val.sqrt();
81 } else {
82 return None;
84 }
85 }
86 self.standard_errors = Some(std_errors);
87 }
88 self.standard_errors.as_ref()
89 }
90
91 pub fn reset_covariance(&mut self) {
93 self.covariance_matrix = None;
94 self.standard_errors = None;
95 }
96}
97
98impl Default for SparseQRSolver {
99 fn default() -> Self {
100 Self::new()
101 }
102}
103
104impl SparseLinearSolver for SparseQRSolver {
105 fn solve_normal_equation(
106 &mut self,
107 residuals: &Mat<f64>,
108 jacobians: &SparseColMat<usize, f64>,
109 ) -> LinAlgResult<Mat<f64>> {
110 let jt = jacobians.as_ref().transpose();
112 let hessian = jt
113 .to_col_major()
114 .map_err(|e| {
115 LinAlgError::MatrixConversion(
116 "Failed to convert transposed Jacobian to column-major format".to_string(),
117 )
118 .log_with_source(e)
119 })?
120 .mul(jacobians.as_ref());
121
122 let gradient = jacobians.as_ref().transpose().mul(residuals);
124
125 let sym = if let Some(ref cached_sym) = self.symbolic_factorization {
128 cached_sym.clone()
133 } else {
134 let new_sym = SymbolicQr::try_new(hessian.symbolic()).map_err(|e| {
136 LinAlgError::FactorizationFailed("Symbolic QR decomposition failed".to_string())
137 .log_with_source(e)
138 })?;
139 self.symbolic_factorization = Some(new_sym.clone());
141 new_sym
142 };
143
144 let qr = Qr::try_new_with_symbolic(sym, hessian.as_ref()).map_err(|e| {
146 LinAlgError::SingularMatrix(
147 "QR factorization failed (matrix may be singular)".to_string(),
148 )
149 .log_with_source(e)
150 })?;
151
152 let dx = qr.solve(-&gradient);
154 self.hessian = Some(hessian);
155 self.gradient = Some(gradient);
156 self.factorizer = Some(qr);
157
158 Ok(dx)
159 }
160
161 fn solve_augmented_equation(
162 &mut self,
163 residuals: &Mat<f64>,
164 jacobians: &SparseColMat<usize, f64>,
165 lambda: f64,
166 ) -> LinAlgResult<Mat<f64>> {
167 let n = jacobians.ncols();
168
169 let jt = jacobians.as_ref().transpose();
171 let hessian = jt
172 .to_col_major()
173 .map_err(|e| {
174 LinAlgError::MatrixConversion(
175 "Failed to convert transposed Jacobian to column-major format".to_string(),
176 )
177 .log_with_source(e)
178 })?
179 .mul(jacobians.as_ref());
180
181 let gradient = jacobians.as_ref().transpose().mul(residuals);
183
184 let mut lambda_i_triplets = Vec::with_capacity(n);
186 for i in 0..n {
187 lambda_i_triplets.push(Triplet::new(i, i, lambda));
188 }
189 let lambda_i =
190 SparseColMat::try_new_from_triplets(n, n, &lambda_i_triplets).map_err(|e| {
191 LinAlgError::SparseMatrixCreation("Failed to create lambda*I matrix".to_string())
192 .log_with_source(e)
193 })?;
194
195 let augmented_hessian = hessian.as_ref() + lambda_i;
196
197 let sym = if let Some(ref cached_sym) = self.symbolic_factorization {
202 cached_sym.clone()
203 } else {
204 let new_sym = SymbolicQr::try_new(augmented_hessian.symbolic()).map_err(|e| {
206 LinAlgError::FactorizationFailed(
207 "Symbolic QR decomposition failed for augmented system".to_string(),
208 )
209 .log_with_source(e)
210 })?;
211 self.symbolic_factorization = Some(new_sym.clone());
213 new_sym
214 };
215
216 let qr = Qr::try_new_with_symbolic(sym, augmented_hessian.as_ref()).map_err(|e| {
218 LinAlgError::SingularMatrix(
219 "QR factorization failed (matrix may be singular)".to_string(),
220 )
221 .log_with_source(e)
222 })?;
223
224 let dx = qr.solve(-&gradient);
225 self.hessian = Some(hessian);
226 self.gradient = Some(gradient);
227 self.factorizer = Some(qr);
228
229 Ok(dx)
230 }
231
232 fn get_hessian(&self) -> Option<&SparseColMat<usize, f64>> {
233 self.hessian.as_ref()
234 }
235
236 fn get_gradient(&self) -> Option<&Mat<f64>> {
237 self.gradient.as_ref()
238 }
239
240 fn compute_covariance_matrix(&mut self) -> Option<&Mat<f64>> {
241 if self.factorizer.is_some()
243 && self.hessian.is_some()
244 && self.covariance_matrix.is_none()
245 && let (Some(factorizer), Some(hessian)) = (&self.factorizer, &self.hessian)
246 {
247 let n = hessian.ncols();
248 let identity = Mat::identity(n, n);
250
251 let cov_matrix = factorizer.solve(&identity);
253 self.covariance_matrix = Some(cov_matrix);
254 }
255 self.covariance_matrix.as_ref()
256 }
257
258 fn get_covariance_matrix(&self) -> Option<&Mat<f64>> {
259 self.covariance_matrix.as_ref()
260 }
261}
262
263#[cfg(test)]
264mod tests {
265 use super::*;
266
267 const TOLERANCE: f64 = 1e-10;
268
269 type TestResult = Result<(), Box<dyn std::error::Error>>;
270
271 fn create_test_data()
273 -> Result<(SparseColMat<usize, f64>, Mat<f64>), faer::sparse::CreationError> {
274 let triplets = vec![
276 Triplet::new(0, 0, 1.0),
277 Triplet::new(0, 1, 0.0),
278 Triplet::new(0, 2, 1.0),
279 Triplet::new(1, 0, 0.0),
280 Triplet::new(1, 1, 1.0),
281 Triplet::new(1, 2, 1.0),
282 Triplet::new(2, 0, 1.0),
283 Triplet::new(2, 1, 1.0),
284 Triplet::new(2, 2, 0.0),
285 Triplet::new(3, 0, 1.0),
286 Triplet::new(3, 1, 0.0),
287 Triplet::new(3, 2, 0.0),
288 ];
289 let jacobian = SparseColMat::try_new_from_triplets(4, 3, &triplets)?;
290
291 let residuals = Mat::from_fn(4, 1, |i, _| (i + 1) as f64);
292
293 Ok((jacobian, residuals))
294 }
295
296 #[test]
298 fn test_qr_solver_creation() {
299 let solver = SparseQRSolver::new();
300 assert!(solver.factorizer.is_none());
301
302 let default_solver = SparseQRSolver::default();
303 assert!(default_solver.factorizer.is_none());
304 }
305
306 #[test]
308 fn test_qr_solve_normal_equation() -> TestResult {
309 let mut solver = SparseQRSolver::new();
310 let (jacobian, residuals) = create_test_data()?;
311
312 let solution = solver.solve_normal_equation(&residuals, &jacobian)?;
313 assert_eq!(solution.nrows(), 3); assert_eq!(solution.ncols(), 1);
315
316 assert!(solver.factorizer.is_some());
318 Ok(())
319 }
320
321 #[test]
323 fn test_qr_factorizer_caching() -> TestResult {
324 let mut solver = SparseQRSolver::new();
325 let (jacobian, residuals) = create_test_data()?;
326
327 let sol1 = solver.solve_normal_equation(&residuals, &jacobian)?;
329 assert!(solver.factorizer.is_some());
330
331 let sol2 = solver.solve_normal_equation(&residuals, &jacobian)?;
333
334 for i in 0..sol1.nrows() {
336 assert!((sol1[(i, 0)] - sol2[(i, 0)]).abs() < TOLERANCE);
337 }
338 Ok(())
339 }
340
341 #[test]
343 fn test_qr_solve_augmented_equation() -> TestResult {
344 let mut solver = SparseQRSolver::new();
345 let (jacobian, residuals) = create_test_data()?;
346 let lambda = 0.1;
347
348 let solution = solver.solve_augmented_equation(&residuals, &jacobian, lambda)?;
349 assert_eq!(solution.nrows(), 3); assert_eq!(solution.ncols(), 1);
351 Ok(())
352 }
353
354 #[test]
356 fn test_qr_augmented_different_lambdas() -> TestResult {
357 let mut solver = SparseQRSolver::new();
358 let (jacobian, residuals) = create_test_data()?;
359
360 let lambda1 = 0.01;
361 let lambda2 = 1.0;
362
363 let sol1 = solver.solve_augmented_equation(&residuals, &jacobian, lambda1)?;
364 let sol2 = solver.solve_augmented_equation(&residuals, &jacobian, lambda2)?;
365
366 let mut different = false;
368 for i in 0..sol1.nrows() {
369 if (sol1[(i, 0)] - sol2[(i, 0)]).abs() > TOLERANCE {
370 different = true;
371 break;
372 }
373 }
374 assert!(
375 different,
376 "Solutions should differ with different lambda values"
377 );
378 Ok(())
379 }
380
381 #[test]
383 fn test_qr_rank_deficient_matrix() -> TestResult {
384 let mut solver = SparseQRSolver::new();
385
386 let triplets = vec![
388 Triplet::new(0, 0, 1.0),
389 Triplet::new(0, 1, 2.0),
390 Triplet::new(0, 2, 3.0),
391 Triplet::new(1, 0, 2.0),
392 Triplet::new(1, 1, 4.0),
393 Triplet::new(1, 2, 6.0), Triplet::new(2, 0, 0.0),
395 Triplet::new(2, 1, 0.0),
396 Triplet::new(2, 2, 1.0),
397 ];
398 let jacobian = SparseColMat::try_new_from_triplets(3, 3, &triplets)?;
399 let residuals = Mat::from_fn(3, 1, |i, _| i as f64);
400
401 let result = solver.solve_normal_equation(&residuals, &jacobian);
403 assert!(result.is_ok());
404 Ok(())
405 }
406
407 #[test]
409 fn test_qr_augmented_system_structure() -> TestResult {
410 let mut solver = SparseQRSolver::new();
411
412 let triplets = vec![
414 Triplet::new(0, 0, 1.0),
415 Triplet::new(0, 1, 0.0),
416 Triplet::new(1, 0, 0.0),
417 Triplet::new(1, 1, 1.0),
418 ];
419 let jacobian = SparseColMat::try_new_from_triplets(2, 2, &triplets)?;
420 let residuals = Mat::from_fn(2, 1, |i, _| (i + 1) as f64);
421 let lambda = 0.5;
422
423 let solution = solver.solve_augmented_equation(&residuals, &jacobian, lambda)?;
424 assert_eq!(solution.nrows(), 2); assert_eq!(solution.ncols(), 1);
426 Ok(())
427 }
428
429 #[test]
431 fn test_qr_numerical_accuracy() -> TestResult {
432 let mut solver = SparseQRSolver::new();
433
434 let triplets = vec![
436 Triplet::new(0, 0, 1.0),
437 Triplet::new(1, 1, 1.0),
438 Triplet::new(2, 2, 1.0),
439 ];
440 let jacobian = SparseColMat::try_new_from_triplets(3, 3, &triplets)?;
441
442 let residuals = Mat::from_fn(3, 1, |i, _| -((i + 1) as f64)); let solution = solver.solve_normal_equation(&residuals, &jacobian)?;
445 for i in 0..3 {
447 let expected = (i + 1) as f64;
448 assert!(
449 (solution[(i, 0)] - expected).abs() < TOLERANCE,
450 "Expected {}, got {}",
451 expected,
452 solution[(i, 0)]
453 );
454 }
455 Ok(())
456 }
457
458 #[test]
460 fn test_qr_solver_clone() {
461 let solver1 = SparseQRSolver::new();
462 let solver2 = solver1.clone();
463
464 assert!(solver1.factorizer.is_none());
465 assert!(solver2.factorizer.is_none());
466 }
467
468 #[test]
470 fn test_qr_zero_lambda_augmented() -> TestResult {
471 let mut solver = SparseQRSolver::new();
472 let (jacobian, residuals) = create_test_data()?;
473
474 let normal_sol = solver.solve_normal_equation(&residuals, &jacobian)?;
475 let augmented_sol = solver.solve_augmented_equation(&residuals, &jacobian, 0.0)?;
476
477 for i in 0..normal_sol.nrows() {
479 assert!(
480 (normal_sol[(i, 0)] - augmented_sol[(i, 0)]).abs() < 1e-8,
481 "Zero lambda augmented should match normal equation"
482 );
483 }
484 Ok(())
485 }
486
487 #[test]
489 fn test_qr_covariance_computation() -> TestResult {
490 let mut solver = SparseQRSolver::new();
491 let (jacobian, residuals) = create_test_data()?;
492
493 solver.solve_normal_equation(&residuals, &jacobian)?;
495
496 let cov_matrix = solver.compute_covariance_matrix();
498 assert!(cov_matrix.is_some());
499
500 if let Some(cov) = cov_matrix {
501 assert_eq!(cov.nrows(), 3); assert_eq!(cov.ncols(), 3);
503
504 for i in 0..3 {
506 for j in 0..3 {
507 assert!(
508 (cov[(i, j)] - cov[(j, i)]).abs() < TOLERANCE,
509 "Covariance matrix should be symmetric"
510 );
511 }
512 }
513
514 for i in 0..3 {
516 assert!(
517 cov[(i, i)] > 0.0,
518 "Diagonal elements (variances) should be positive"
519 );
520 }
521 }
522 Ok(())
523 }
524
525 #[test]
527 fn test_qr_standard_errors_computation() -> TestResult {
528 let mut solver = SparseQRSolver::new();
529 let (jacobian, residuals) = create_test_data()?;
530
531 solver.solve_normal_equation(&residuals, &jacobian)?;
533
534 solver.compute_standard_errors();
536
537 assert!(solver.covariance_matrix.is_some());
539 assert!(solver.standard_errors.is_some());
540
541 if let (Some(cov), Some(errors)) = (&solver.covariance_matrix, &solver.standard_errors) {
542 assert_eq!(errors.nrows(), 3); assert_eq!(errors.ncols(), 1);
544
545 for i in 0..3 {
547 assert!(errors[(i, 0)] > 0.0, "Standard errors should be positive");
548 }
549
550 for i in 0..3 {
552 let expected_std_error = cov[(i, i)].sqrt();
553 assert!(
554 (errors[(i, 0)] - expected_std_error).abs() < TOLERANCE,
555 "Standard error should equal sqrt of covariance diagonal"
556 );
557 }
558 }
559 Ok(())
560 }
561
562 #[test]
564 fn test_qr_covariance_well_conditioned() -> TestResult {
565 let mut solver = SparseQRSolver::new();
566
567 let triplets = vec![
569 Triplet::new(0, 0, 2.0),
570 Triplet::new(0, 1, 0.0),
571 Triplet::new(1, 0, 0.0),
572 Triplet::new(1, 1, 3.0),
573 ];
574 let jacobian = SparseColMat::try_new_from_triplets(2, 2, &triplets)?;
575 let residuals = Mat::from_fn(2, 1, |i, _| (i + 1) as f64);
576
577 solver.solve_normal_equation(&residuals, &jacobian)?;
578
579 let cov_matrix = solver.compute_covariance_matrix();
580 assert!(cov_matrix.is_some());
581
582 if let Some(cov) = cov_matrix {
583 assert!((cov[(0, 0)] - 0.25).abs() < TOLERANCE);
586 assert!((cov[(1, 1)] - 1.0 / 9.0).abs() < TOLERANCE);
587 assert!(cov[(0, 1)].abs() < TOLERANCE);
588 assert!(cov[(1, 0)].abs() < TOLERANCE);
589 }
590 Ok(())
591 }
592
593 #[test]
595 fn test_qr_covariance_caching() -> TestResult {
596 let mut solver = SparseQRSolver::new();
597 let (jacobian, residuals) = create_test_data()?;
598
599 solver.solve_normal_equation(&residuals, &jacobian)?;
601
602 solver.compute_covariance_matrix();
604 assert!(solver.covariance_matrix.is_some());
605
606 if let Some(cov1) = &solver.covariance_matrix {
608 let cov1_ptr = cov1.as_ptr();
609
610 solver.compute_covariance_matrix();
612 assert!(solver.covariance_matrix.is_some());
613
614 if let Some(cov2) = &solver.covariance_matrix {
616 let cov2_ptr = cov2.as_ptr();
617
618 assert_eq!(cov1_ptr, cov2_ptr, "Covariance matrix should be cached");
620 }
621 }
622 Ok(())
623 }
624
625 #[test]
627 fn test_qr_covariance_singular_system() -> TestResult {
628 let mut solver = SparseQRSolver::new();
629
630 let triplets = vec![
632 Triplet::new(0, 0, 1.0),
633 Triplet::new(0, 1, 2.0),
634 Triplet::new(1, 0, 2.0),
635 Triplet::new(1, 1, 4.0), ];
637 let jacobian = SparseColMat::try_new_from_triplets(2, 2, &triplets)?;
638 let residuals = Mat::from_fn(2, 1, |i, _| i as f64);
639
640 let result = solver.solve_normal_equation(&residuals, &jacobian);
642 if result.is_ok() {
643 let cov_matrix = solver.compute_covariance_matrix();
645 if let Some(cov) = cov_matrix {
647 assert!(cov.nrows() == 2);
649 assert!(cov.ncols() == 2);
650 }
651 }
652 Ok(())
653 }
654}