apex_solver/linalg/dense/
qr.rs1use faer::{Mat, linalg::solvers::Solve};
2
3use crate::linalg::{Damping, DenseMode, LinAlgResult, LinearSolver};
4
5#[derive(Debug, Clone)]
11pub struct DenseQRSolver {
12 hessian: Option<Mat<f64>>,
14
15 gradient: Option<Mat<f64>>,
17}
18
19impl DenseQRSolver {
20 pub fn new() -> Self {
21 Self {
22 hessian: None,
23 gradient: None,
24 }
25 }
26
27 pub fn hessian(&self) -> Option<&Mat<f64>> {
28 self.hessian.as_ref()
29 }
30
31 pub fn gradient(&self) -> Option<&Mat<f64>> {
32 self.gradient.as_ref()
33 }
34
35 fn solve_dense_normal(
37 &mut self,
38 residuals: &Mat<f64>,
39 jacobian: &Mat<f64>,
40 ) -> LinAlgResult<Mat<f64>> {
41 let hessian = jacobian.transpose() * jacobian;
43 let gradient = jacobian.transpose() * residuals;
45
46 let qr = hessian.as_ref().col_piv_qr();
48
49 let dx = qr.solve(-&gradient);
51
52 self.hessian = Some(hessian);
53 self.gradient = Some(gradient);
54
55 Ok(dx)
56 }
57
58 fn solve_dense_augmented(
60 &mut self,
61 residuals: &Mat<f64>,
62 jacobian: &Mat<f64>,
63 damping: &Damping,
64 ) -> LinAlgResult<Mat<f64>> {
65 let hessian = jacobian.transpose() * jacobian;
67 let gradient = jacobian.transpose() * residuals;
69
70 let n = hessian.nrows();
72 let mut augmented = hessian.clone();
73 for i in 0..n {
74 augmented[(i, i)] += damping.diagonal_term(hessian[(i, i)]);
75 }
76
77 let qr = augmented.as_ref().col_piv_qr();
79
80 let dx = qr.solve(-&gradient);
82
83 self.hessian = Some(hessian);
85 self.gradient = Some(gradient);
86
87 Ok(dx)
88 }
89}
90
91impl Default for DenseQRSolver {
92 fn default() -> Self {
93 Self::new()
94 }
95}
96
97impl LinearSolver<DenseMode> for DenseQRSolver {
102 fn solve_normal_equation(
103 &mut self,
104 residuals: &Mat<f64>,
105 jacobian: &Mat<f64>,
106 ) -> LinAlgResult<Mat<f64>> {
107 self.solve_dense_normal(residuals, jacobian)
108 }
109
110 fn solve_augmented_equation(
111 &mut self,
112 residuals: &Mat<f64>,
113 jacobian: &Mat<f64>,
114 damping: &Damping,
115 ) -> LinAlgResult<Mat<f64>> {
116 self.solve_dense_augmented(residuals, jacobian, damping)
117 }
118
119 fn hessian_vec_product(&self, v: &Mat<f64>) -> Option<Mat<f64>> {
120 Some(
121 <DenseMode as crate::linearizer::AssemblyBackend>::hessian_vec_product(
122 self.hessian.as_ref()?,
123 v,
124 ),
125 )
126 }
127
128 fn get_hessian(&self) -> Option<&Mat<f64>> {
129 self.hessian.as_ref()
130 }
131
132 fn get_gradient(&self) -> Option<&Mat<f64>> {
133 self.gradient.as_ref()
134 }
135}
136
137#[cfg(test)]
138mod tests {
139 use super::*;
140
141 const TOLERANCE: f64 = 1e-10;
142
143 type TestResult = Result<(), Box<dyn std::error::Error>>;
144
145 fn create_test_data() -> (Mat<f64>, Mat<f64>) {
146 let mut j = Mat::zeros(4, 2);
148 j[(0, 0)] = 2.0;
149 j[(0, 1)] = 1.0;
150 j[(1, 0)] = 1.0;
151 j[(1, 1)] = 3.0;
152 j[(2, 0)] = 1.0;
153 j[(2, 1)] = 1.0;
154 j[(3, 0)] = 0.5;
155 j[(3, 1)] = 2.0;
156
157 let mut r = Mat::zeros(4, 1);
158 r[(0, 0)] = 1.0;
159 r[(1, 0)] = 2.0;
160 r[(2, 0)] = 0.5;
161 r[(3, 0)] = 1.5;
162
163 (j, r)
164 }
165
166 #[test]
167 fn test_dense_qr_solver_creation() {
168 let solver = DenseQRSolver::new();
169 assert!(solver.hessian.is_none());
170 assert!(solver.gradient.is_none());
171
172 let default_solver = DenseQRSolver::default();
173 assert!(default_solver.hessian.is_none());
174 assert!(default_solver.gradient.is_none());
175 }
176
177 #[test]
178 fn test_dense_qr_solve_normal_equation() -> TestResult {
179 let (j, r) = create_test_data();
180 let mut solver = DenseQRSolver::new();
181
182 let dx = LinearSolver::<DenseMode>::solve_normal_equation(&mut solver, &r, &j)?;
183
184 let jtj = j.transpose() * &j;
186 let jtr = j.transpose() * &r;
187 let residual = &jtj * &dx + &jtr;
188
189 for i in 0..dx.nrows() {
190 assert!(
191 residual[(i, 0)].abs() < TOLERANCE,
192 "Residual at index {i}: {}",
193 residual[(i, 0)]
194 );
195 }
196
197 assert!(solver.hessian.is_some());
198 assert!(solver.gradient.is_some());
199
200 Ok(())
201 }
202
203 #[test]
204 fn test_dense_qr_solve_augmented_equation() -> TestResult {
205 let (j, r) = create_test_data();
206 let lambda = 0.1;
207 let mut solver = DenseQRSolver::new();
208
209 let dx = LinearSolver::<DenseMode>::solve_augmented_equation(
210 &mut solver,
211 &r,
212 &j,
213 &Damping::identity(lambda),
214 )?;
215
216 let mut jtj = j.transpose() * &j;
218 let jtr = j.transpose() * &r;
219 for i in 0..jtj.nrows() {
220 jtj[(i, i)] += lambda;
221 }
222 let residual = &jtj * &dx + &jtr;
223
224 for i in 0..dx.nrows() {
225 assert!(
226 residual[(i, 0)].abs() < TOLERANCE,
227 "Residual at index {i}: {}",
228 residual[(i, 0)]
229 );
230 }
231
232 Ok(())
233 }
234
235 #[test]
236 fn test_dense_qr_augmented_different_lambdas() -> TestResult {
237 let (j, r) = create_test_data();
238 let mut solver = DenseQRSolver::new();
239
240 let dx1 = LinearSolver::<DenseMode>::solve_augmented_equation(
241 &mut solver,
242 &r,
243 &j,
244 &Damping::identity(0.01),
245 )?;
246 let dx2 = LinearSolver::<DenseMode>::solve_augmented_equation(
247 &mut solver,
248 &r,
249 &j,
250 &Damping::identity(1.0),
251 )?;
252
253 let mut different = false;
254 for i in 0..dx1.nrows() {
255 if (dx1[(i, 0)] - dx2[(i, 0)]).abs() > TOLERANCE {
256 different = true;
257 break;
258 }
259 }
260 assert!(
261 different,
262 "Solutions should differ with different lambda values"
263 );
264
265 Ok(())
266 }
267
268 #[test]
269 fn test_dense_qr_rank_deficient_matrix() -> TestResult {
270 let mut solver = DenseQRSolver::new();
271
272 let mut j = Mat::zeros(3, 3);
274 j[(0, 0)] = 1.0;
275 j[(0, 1)] = 2.0;
276 j[(0, 2)] = 3.0;
277 j[(1, 0)] = 2.0;
278 j[(1, 1)] = 4.0;
279 j[(1, 2)] = 6.0;
280 j[(2, 0)] = 0.0;
281 j[(2, 1)] = 0.0;
282 j[(2, 2)] = 1.0;
283
284 let mut r = Mat::zeros(3, 1);
285 r[(0, 0)] = 1.0;
286 r[(1, 0)] = 2.0;
287 r[(2, 0)] = 3.0;
288
289 let result = LinearSolver::<DenseMode>::solve_normal_equation(&mut solver, &r, &j);
290 assert!(result.is_ok(), "QR should handle rank-deficient matrices");
291
292 Ok(())
293 }
294
295 #[test]
296 fn test_dense_qr_numerical_accuracy() -> TestResult {
297 let mut solver = DenseQRSolver::new();
298
299 let mut j = Mat::zeros(3, 3);
301 j[(0, 0)] = 1.0;
302 j[(1, 1)] = 1.0;
303 j[(2, 2)] = 1.0;
304
305 let mut r = Mat::zeros(3, 1);
306 r[(0, 0)] = -1.0;
307 r[(1, 0)] = -2.0;
308 r[(2, 0)] = -3.0;
309
310 let dx = LinearSolver::<DenseMode>::solve_normal_equation(&mut solver, &r, &j)?;
311
312 for i in 0..3 {
313 let expected = (i + 1) as f64;
314 assert!(
315 (dx[(i, 0)] - expected).abs() < TOLERANCE,
316 "Expected {expected}, got {}",
317 dx[(i, 0)]
318 );
319 }
320
321 Ok(())
322 }
323
324 #[test]
325 fn test_dense_qr_solver_clone() {
326 let solver1 = DenseQRSolver::new();
327 let solver2 = solver1.clone();
328 assert!(solver2.hessian.is_none());
329 assert!(solver2.gradient.is_none());
330 }
331
332 #[test]
333 fn test_dense_qr_zero_lambda_augmented() -> TestResult {
334 let (j, r) = create_test_data();
335 let mut solver = DenseQRSolver::new();
336
337 let normal_dx = LinearSolver::<DenseMode>::solve_normal_equation(&mut solver, &r, &j)?;
338 let augmented_dx = LinearSolver::<DenseMode>::solve_augmented_equation(
339 &mut solver,
340 &r,
341 &j,
342 &Damping::identity(0.0),
343 )?;
344
345 for i in 0..normal_dx.nrows() {
346 assert!(
347 (normal_dx[(i, 0)] - augmented_dx[(i, 0)]).abs() < 1e-8,
348 "Zero-lambda augmented should match normal equation"
349 );
350 }
351
352 Ok(())
353 }
354}