apex_solver/linalg/dense/
cholesky.rs1use faer::{
2 Mat, Side,
3 linalg::solvers::{LltError, Solve},
4};
5
6use crate::error::ErrorLogging;
7use crate::linalg::{Damping, 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
23impl DenseCholeskySolver {
24 pub fn new() -> Self {
25 Self {
26 hessian: None,
27 gradient: None,
28 }
29 }
30
31 fn solve_dense_normal(
33 &mut self,
34 residuals: &Mat<f64>,
35 jacobian: &Mat<f64>,
36 ) -> LinAlgResult<Mat<f64>> {
37 let hessian = jacobian.transpose() * jacobian;
39 let gradient = jacobian.transpose() * residuals;
41
42 let dx = solve_spd(
44 &hessian,
45 &(-&gradient),
46 "Dense Cholesky factorization failed",
47 )?;
48
49 self.hessian = Some(hessian);
50 self.gradient = Some(gradient);
51
52 Ok(dx)
53 }
54
55 fn solve_dense_augmented(
57 &mut self,
58 residuals: &Mat<f64>,
59 jacobian: &Mat<f64>,
60 damping: &Damping,
61 ) -> LinAlgResult<Mat<f64>> {
62 let hessian = jacobian.transpose() * jacobian;
64 let gradient = jacobian.transpose() * residuals;
66
67 let n = hessian.nrows();
69 let mut augmented = hessian.clone();
70 for i in 0..n {
71 augmented[(i, i)] += damping.diagonal_term(hessian[(i, i)]);
72 }
73
74 let dx = solve_spd(
76 &augmented,
77 &(-&gradient),
78 "Augmented dense Cholesky factorization failed",
79 )?;
80
81 self.hessian = Some(hessian);
83 self.gradient = Some(gradient);
84
85 Ok(dx)
86 }
87}
88
89pub(crate) fn solve_spd(a: &Mat<f64>, b: &Mat<f64>, context: &str) -> LinAlgResult<Mat<f64>> {
95 let llt = a
96 .as_ref()
97 .llt(Side::Lower)
98 .map_err(|e| map_llt_error(e, context))?;
99 Ok(llt.solve(b))
100}
101
102impl Default for DenseCholeskySolver {
103 fn default() -> Self {
104 Self::new()
105 }
106}
107
108impl LinearSolver<DenseMode> for DenseCholeskySolver {
113 fn solve_normal_equation(
114 &mut self,
115 residuals: &Mat<f64>,
116 jacobian: &Mat<f64>,
117 ) -> LinAlgResult<Mat<f64>> {
118 self.solve_dense_normal(residuals, jacobian)
119 }
120
121 fn solve_augmented_equation(
122 &mut self,
123 residuals: &Mat<f64>,
124 jacobian: &Mat<f64>,
125 damping: &Damping,
126 ) -> LinAlgResult<Mat<f64>> {
127 self.solve_dense_augmented(residuals, jacobian, damping)
128 }
129
130 fn hessian_vec_product(&self, v: &Mat<f64>) -> Option<Mat<f64>> {
131 Some(
132 <DenseMode as crate::linearizer::AssemblyBackend>::hessian_vec_product(
133 self.hessian.as_ref()?,
134 v,
135 ),
136 )
137 }
138
139 fn get_hessian(&self) -> Option<&Mat<f64>> {
140 self.hessian.as_ref()
141 }
142
143 fn get_gradient(&self) -> Option<&Mat<f64>> {
144 self.gradient.as_ref()
145 }
146}
147
148fn map_llt_error(e: LltError, context: &str) -> LinAlgError {
150 LinAlgError::FactorizationFailed(format!("{context}: {e:?}")).log()
151}
152
153#[cfg(test)]
154mod tests {
155 use super::*;
156
157 const TOLERANCE: f64 = 1e-10;
158
159 type TestResult = Result<(), Box<dyn std::error::Error>>;
160
161 fn create_test_data() -> (Mat<f64>, Mat<f64>) {
162 let mut j = Mat::zeros(4, 2);
164 j[(0, 0)] = 2.0;
165 j[(0, 1)] = 1.0;
166 j[(1, 0)] = 1.0;
167 j[(1, 1)] = 3.0;
168 j[(2, 0)] = 1.0;
169 j[(2, 1)] = 1.0;
170 j[(3, 0)] = 0.5;
171 j[(3, 1)] = 2.0;
172
173 let mut r = Mat::zeros(4, 1);
174 r[(0, 0)] = 1.0;
175 r[(1, 0)] = 2.0;
176 r[(2, 0)] = 0.5;
177 r[(3, 0)] = 1.5;
178
179 (j, r)
180 }
181
182 #[test]
183 fn test_dense_cholesky_solve_normal() -> TestResult {
184 let (j, r) = create_test_data();
185 let mut solver = DenseCholeskySolver::new();
186
187 let dx = LinearSolver::<DenseMode>::solve_normal_equation(&mut solver, &r, &j)?;
188
189 let jtj = j.transpose() * &j;
191 let jtr = j.transpose() * &r;
192 let residual = &jtj * &dx + &jtr;
193
194 for i in 0..dx.nrows() {
195 assert!(
196 residual[(i, 0)].abs() < TOLERANCE,
197 "Residual at index {}: {}",
198 i,
199 residual[(i, 0)]
200 );
201 }
202
203 assert!(solver.hessian.is_some());
204 assert!(solver.gradient.is_some());
205
206 Ok(())
207 }
208
209 #[test]
210 fn test_dense_cholesky_solve_augmented() -> TestResult {
211 let (j, r) = create_test_data();
212 let lambda = 0.1;
213 let mut solver = DenseCholeskySolver::new();
214
215 let dx = LinearSolver::<DenseMode>::solve_augmented_equation(
216 &mut solver,
217 &r,
218 &j,
219 &Damping::identity(lambda),
220 )?;
221
222 let mut jtj = j.transpose() * &j;
224 let jtr = j.transpose() * &r;
225 for i in 0..jtj.nrows() {
226 jtj[(i, i)] += lambda;
227 }
228 let residual = &jtj * &dx + &jtr;
229
230 for i in 0..dx.nrows() {
231 assert!(
232 residual[(i, 0)].abs() < TOLERANCE,
233 "Residual at index {}: {}",
234 i,
235 residual[(i, 0)]
236 );
237 }
238
239 Ok(())
240 }
241
242 #[test]
248 fn test_accessors_before_solve() {
249 let solver = DenseCholeskySolver::new();
250 assert!(
251 LinearSolver::<DenseMode>::get_hessian(&solver).is_none(),
252 "hessian should be None before solve"
253 );
254 assert!(
255 LinearSolver::<DenseMode>::get_gradient(&solver).is_none(),
256 "gradient should be None before solve"
257 );
258 }
259
260 #[test]
262 fn test_default_equals_new() {
263 let solver = DenseCholeskySolver::default();
264 assert!(solver.hessian.is_none());
265 assert!(solver.gradient.is_none());
266 }
267
268 #[test]
270 fn test_singular_jacobian_returns_error() {
271 let mut solver = DenseCholeskySolver::new();
272
273 let mut j = Mat::zeros(3, 2);
275 j[(0, 0)] = 1.0;
276 j[(1, 0)] = 2.0;
277 j[(2, 0)] = 3.0;
278 let mut r = Mat::zeros(3, 1);
281 r[(0, 0)] = 1.0;
282
283 let result = LinearSolver::<DenseMode>::solve_normal_equation(&mut solver, &r, &j);
284 assert!(
285 result.is_err(),
286 "Singular Jacobian must produce a factorization error"
287 );
288 }
289
290 #[test]
292 fn test_augmented_with_zero_lambda_matches_normal() -> TestResult {
293 let (j, r) = create_test_data();
294 let mut solver_n = DenseCholeskySolver::new();
295 let mut solver_a = DenseCholeskySolver::new();
296
297 let dx_normal = LinearSolver::<DenseMode>::solve_normal_equation(&mut solver_n, &r, &j)?;
298 let dx_augmented = LinearSolver::<DenseMode>::solve_augmented_equation(
299 &mut solver_a,
300 &r,
301 &j,
302 &Damping::identity(0.0),
303 )?;
304
305 for i in 0..dx_normal.nrows() {
306 assert!(
307 (dx_normal[(i, 0)] - dx_augmented[(i, 0)]).abs() < TOLERANCE,
308 "Element {} differs: normal={}, augmented(λ=0)={}",
309 i,
310 dx_normal[(i, 0)],
311 dx_augmented[(i, 0)]
312 );
313 }
314 Ok(())
315 }
316}