1use faer::{
2 Mat,
3 linalg::solvers::Solve,
4 sparse::SparseColMat,
5 sparse::linalg::solvers::{Qr, SymbolicQr},
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 SparseQRSolver {
14 symbolic_factorization: Option<SymbolicQr<usize>>,
21
22 ne_cache: LazyNormalEquations,
24
25 hessian: Option<SparseColMat<usize, f64>>,
29
30 gradient: Option<Mat<f64>>,
34}
35
36impl SparseQRSolver {
37 pub fn new() -> Self {
38 SparseQRSolver {
39 symbolic_factorization: None,
40 ne_cache: LazyNormalEquations::default(),
41 hessian: None,
42 gradient: None,
43 }
44 }
45
46 pub fn hessian(&self) -> Option<&SparseColMat<usize, f64>> {
47 self.hessian.as_ref()
48 }
49
50 pub fn gradient(&self) -> Option<&Mat<f64>> {
51 self.gradient.as_ref()
52 }
53}
54
55impl Default for SparseQRSolver {
56 fn default() -> Self {
57 Self::new()
58 }
59}
60
61impl LinearSolver<SparseMode> for SparseQRSolver {
62 fn solve_normal_equation(
63 &mut self,
64 residuals: &Mat<f64>,
65 jacobians: &SparseColMat<usize, f64>,
66 ) -> LinAlgResult<Mat<f64>> {
67 let NormalEquations { hessian, gradient } = self.ne_cache.compute(residuals, jacobians)?;
70
71 let sym = if let Some(ref cached_sym) = self.symbolic_factorization {
74 cached_sym.clone()
79 } else {
80 let new_sym = SymbolicQr::try_new(hessian.symbolic()).map_err(|e| {
82 LinAlgError::FactorizationFailed("Symbolic QR decomposition failed".to_string())
83 .log_with_source(e)
84 })?;
85 self.symbolic_factorization = Some(new_sym.clone());
87 new_sym
88 };
89
90 let qr = Qr::try_new_with_symbolic(sym, hessian.as_ref()).map_err(|e| {
92 LinAlgError::SingularMatrix(
93 "QR factorization failed (matrix may be singular)".to_string(),
94 )
95 .log_with_source(e)
96 })?;
97
98 let dx = qr.solve(-&gradient);
100 self.hessian = Some(hessian);
101 self.gradient = Some(gradient);
102
103 Ok(dx)
104 }
105
106 fn solve_augmented_equation(
107 &mut self,
108 residuals: &Mat<f64>,
109 jacobians: &SparseColMat<usize, f64>,
110 damping: &Damping,
111 ) -> LinAlgResult<Mat<f64>> {
112 let NormalEquations { hessian, gradient } = self.ne_cache.compute(residuals, jacobians)?;
114
115 let augmented_hessian = self.ne_cache.damped_hessian(damping)?;
117
118 let sym = if let Some(ref cached_sym) = self.symbolic_factorization {
123 cached_sym.clone()
124 } else {
125 let new_sym = SymbolicQr::try_new(augmented_hessian.symbolic()).map_err(|e| {
127 LinAlgError::FactorizationFailed(
128 "Symbolic QR decomposition failed for augmented system".to_string(),
129 )
130 .log_with_source(e)
131 })?;
132 self.symbolic_factorization = Some(new_sym.clone());
134 new_sym
135 };
136
137 let qr = Qr::try_new_with_symbolic(sym, augmented_hessian.as_ref()).map_err(|e| {
139 LinAlgError::SingularMatrix(
140 "QR factorization failed (matrix may be singular)".to_string(),
141 )
142 .log_with_source(e)
143 })?;
144
145 let dx = qr.solve(-&gradient);
146 self.hessian = Some(hessian);
147 self.gradient = Some(gradient);
148
149 Ok(dx)
150 }
151
152 fn hessian_vec_product(&self, v: &Mat<f64>) -> Option<Mat<f64>> {
153 Some(
154 <SparseMode as crate::linearizer::AssemblyBackend>::hessian_vec_product(
155 self.hessian.as_ref()?,
156 v,
157 ),
158 )
159 }
160
161 fn get_hessian(&self) -> Option<&SparseColMat<usize, f64>> {
162 self.hessian.as_ref()
163 }
164
165 fn get_gradient(&self) -> Option<&Mat<f64>> {
166 self.gradient.as_ref()
167 }
168}
169
170#[cfg(test)]
171mod tests {
172 use super::*;
173 use faer::sparse::Triplet;
174
175 const TOLERANCE: f64 = 1e-10;
176
177 type TestResult = Result<(), Box<dyn std::error::Error>>;
178
179 fn create_test_data()
181 -> Result<(SparseColMat<usize, f64>, Mat<f64>), faer::sparse::CreationError> {
182 let triplets = vec![
184 Triplet::new(0, 0, 1.0),
185 Triplet::new(0, 1, 0.0),
186 Triplet::new(0, 2, 1.0),
187 Triplet::new(1, 0, 0.0),
188 Triplet::new(1, 1, 1.0),
189 Triplet::new(1, 2, 1.0),
190 Triplet::new(2, 0, 1.0),
191 Triplet::new(2, 1, 1.0),
192 Triplet::new(2, 2, 0.0),
193 Triplet::new(3, 0, 1.0),
194 Triplet::new(3, 1, 0.0),
195 Triplet::new(3, 2, 0.0),
196 ];
197 let jacobian = SparseColMat::try_new_from_triplets(4, 3, &triplets)?;
198
199 let residuals = Mat::from_fn(4, 1, |i, _| (i + 1) as f64);
200
201 Ok((jacobian, residuals))
202 }
203
204 #[test]
206 fn test_qr_solver_creation() {
207 let solver = SparseQRSolver::new();
208 assert!(solver.hessian.is_none());
209 assert!(solver.gradient.is_none());
210
211 let default_solver = SparseQRSolver::default();
212 assert!(default_solver.hessian.is_none());
213 assert!(default_solver.gradient.is_none());
214 }
215
216 #[test]
218 fn test_qr_solve_normal_equation() -> TestResult {
219 let mut solver = SparseQRSolver::new();
220 let (jacobian, residuals) = create_test_data()?;
221
222 let solution =
223 LinearSolver::<SparseMode>::solve_normal_equation(&mut solver, &residuals, &jacobian)?;
224 assert_eq!(solution.nrows(), 3); assert_eq!(solution.ncols(), 1);
226
227 Ok(())
229 }
230
231 #[test]
233 fn test_qr_factorizer_caching() -> TestResult {
234 let mut solver = SparseQRSolver::new();
235 let (jacobian, residuals) = create_test_data()?;
236
237 let sol1 =
239 LinearSolver::<SparseMode>::solve_normal_equation(&mut solver, &residuals, &jacobian)?;
240
241 let sol2 =
243 LinearSolver::<SparseMode>::solve_normal_equation(&mut solver, &residuals, &jacobian)?;
244
245 for i in 0..sol1.nrows() {
247 assert!((sol1[(i, 0)] - sol2[(i, 0)]).abs() < TOLERANCE);
248 }
249 Ok(())
250 }
251
252 #[test]
254 fn test_qr_solve_augmented_equation() -> TestResult {
255 let mut solver = SparseQRSolver::new();
256 let (jacobian, residuals) = create_test_data()?;
257 let lambda = 0.1;
258
259 let solution = LinearSolver::<SparseMode>::solve_augmented_equation(
260 &mut solver,
261 &residuals,
262 &jacobian,
263 &Damping::identity(lambda),
264 )?;
265 assert_eq!(solution.nrows(), 3); assert_eq!(solution.ncols(), 1);
267 Ok(())
268 }
269
270 #[test]
272 fn test_qr_augmented_different_lambdas() -> TestResult {
273 let mut solver = SparseQRSolver::new();
274 let (jacobian, residuals) = create_test_data()?;
275
276 let lambda1 = 0.01;
277 let lambda2 = 1.0;
278
279 let sol1 = LinearSolver::<SparseMode>::solve_augmented_equation(
280 &mut solver,
281 &residuals,
282 &jacobian,
283 &Damping::identity(lambda1),
284 )?;
285 let sol2 = LinearSolver::<SparseMode>::solve_augmented_equation(
286 &mut solver,
287 &residuals,
288 &jacobian,
289 &Damping::identity(lambda2),
290 )?;
291
292 let mut different = false;
294 for i in 0..sol1.nrows() {
295 if (sol1[(i, 0)] - sol2[(i, 0)]).abs() > TOLERANCE {
296 different = true;
297 break;
298 }
299 }
300 assert!(
301 different,
302 "Solutions should differ with different lambda values"
303 );
304 Ok(())
305 }
306
307 #[test]
309 fn test_qr_rank_deficient_matrix() -> TestResult {
310 let mut solver = SparseQRSolver::new();
311
312 let triplets = vec![
314 Triplet::new(0, 0, 1.0),
315 Triplet::new(0, 1, 2.0),
316 Triplet::new(0, 2, 3.0),
317 Triplet::new(1, 0, 2.0),
318 Triplet::new(1, 1, 4.0),
319 Triplet::new(1, 2, 6.0), Triplet::new(2, 0, 0.0),
321 Triplet::new(2, 1, 0.0),
322 Triplet::new(2, 2, 1.0),
323 ];
324 let jacobian = SparseColMat::try_new_from_triplets(3, 3, &triplets)?;
325 let residuals = Mat::from_fn(3, 1, |i, _| i as f64);
326
327 let result =
329 LinearSolver::<SparseMode>::solve_normal_equation(&mut solver, &residuals, &jacobian);
330 assert!(result.is_ok());
331 Ok(())
332 }
333
334 #[test]
336 fn test_qr_augmented_system_structure() -> TestResult {
337 let mut solver = SparseQRSolver::new();
338
339 let triplets = vec![
341 Triplet::new(0, 0, 1.0),
342 Triplet::new(0, 1, 0.0),
343 Triplet::new(1, 0, 0.0),
344 Triplet::new(1, 1, 1.0),
345 ];
346 let jacobian = SparseColMat::try_new_from_triplets(2, 2, &triplets)?;
347 let residuals = Mat::from_fn(2, 1, |i, _| (i + 1) as f64);
348 let lambda = 0.5;
349
350 let solution = LinearSolver::<SparseMode>::solve_augmented_equation(
351 &mut solver,
352 &residuals,
353 &jacobian,
354 &Damping::identity(lambda),
355 )?;
356 assert_eq!(solution.nrows(), 2); assert_eq!(solution.ncols(), 1);
358 Ok(())
359 }
360
361 #[test]
363 fn test_qr_numerical_accuracy() -> TestResult {
364 let mut solver = SparseQRSolver::new();
365
366 let triplets = vec![
368 Triplet::new(0, 0, 1.0),
369 Triplet::new(1, 1, 1.0),
370 Triplet::new(2, 2, 1.0),
371 ];
372 let jacobian = SparseColMat::try_new_from_triplets(3, 3, &triplets)?;
373
374 let residuals = Mat::from_fn(3, 1, |i, _| -((i + 1) as f64)); let solution =
377 LinearSolver::<SparseMode>::solve_normal_equation(&mut solver, &residuals, &jacobian)?;
378 for i in 0..3 {
380 let expected = (i + 1) as f64;
381 assert!(
382 (solution[(i, 0)] - expected).abs() < TOLERANCE,
383 "Expected {}, got {}",
384 expected,
385 solution[(i, 0)]
386 );
387 }
388 Ok(())
389 }
390
391 #[test]
393 fn test_qr_solver_clone() {
394 let solver1 = SparseQRSolver::new();
395 let solver2 = solver1.clone();
396 assert!(solver2.hessian.is_none());
397 assert!(solver2.gradient.is_none());
398 }
399
400 #[test]
402 fn test_qr_zero_lambda_augmented() -> TestResult {
403 let mut solver = SparseQRSolver::new();
404 let (jacobian, residuals) = create_test_data()?;
405
406 let normal_sol =
407 LinearSolver::<SparseMode>::solve_normal_equation(&mut solver, &residuals, &jacobian)?;
408 let augmented_sol = LinearSolver::<SparseMode>::solve_augmented_equation(
409 &mut solver,
410 &residuals,
411 &jacobian,
412 &Damping::identity(0.0),
413 )?;
414
415 for i in 0..normal_sol.nrows() {
417 assert!(
418 (normal_sol[(i, 0)] - augmented_sol[(i, 0)]).abs() < 1e-8,
419 "Zero lambda augmented should match normal equation"
420 );
421 }
422 Ok(())
423 }
424
425 #[test]
427 fn test_qr_hessian_getter() -> TestResult {
428 let mut solver = SparseQRSolver::new();
429 assert!(solver.hessian().is_none());
430
431 let (jacobian, residuals) = create_test_data()?;
432 LinearSolver::<SparseMode>::solve_normal_equation(&mut solver, &residuals, &jacobian)?;
433
434 assert!(solver.hessian().is_some());
435 Ok(())
436 }
437
438 #[test]
440 fn test_qr_gradient_getter() -> TestResult {
441 let mut solver = SparseQRSolver::new();
442 assert!(solver.gradient().is_none());
443
444 let (jacobian, residuals) = create_test_data()?;
445 LinearSolver::<SparseMode>::solve_normal_equation(&mut solver, &residuals, &jacobian)?;
446
447 assert!(solver.gradient().is_some());
448 Ok(())
449 }
450
451 #[test]
453 fn test_qr_get_hessian_trait() -> TestResult {
454 let mut solver = SparseQRSolver::new();
455 assert!(LinearSolver::<SparseMode>::get_hessian(&solver).is_none());
456
457 let (jacobian, residuals) = create_test_data()?;
458 LinearSolver::<SparseMode>::solve_normal_equation(&mut solver, &residuals, &jacobian)?;
459
460 assert!(LinearSolver::<SparseMode>::get_hessian(&solver).is_some());
461 Ok(())
462 }
463
464 #[test]
466 fn test_qr_get_gradient_trait() -> TestResult {
467 let mut solver = SparseQRSolver::new();
468 assert!(LinearSolver::<SparseMode>::get_gradient(&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_gradient(&solver).is_some());
474 Ok(())
475 }
476}