1use faer::Mat;
41
42use crate::core::VarKey;
43use crate::core::variable::ManifoldVariable;
44use crate::linalg::dense::cholesky::solve_spd;
45use crate::linalg::dense::schur::gather::{gather_dense, verify_block_diagonal_dense};
46use crate::linalg::schur::{
47 BlockSpan, EliminatedBlocks, SchurOrdering, SchurPartition, effective_landmark_keys,
48};
49use crate::linalg::{Damping, DenseMode, LinAlgError, LinAlgResult, LinearSolver, StructureAware};
50use slotmap::{SecondaryMap, SlotMap};
51
52#[derive(Debug, Clone)]
55pub struct ExplicitDenseSchur {
56 partition: Option<SchurPartition>,
57 eliminated: EliminatedBlocks,
59 ordering: SchurOrdering,
60
61 hessian: Option<Mat<f64>>,
62 gradient: Option<Mat<f64>>,
63}
64
65impl ExplicitDenseSchur {
66 pub fn new() -> Self {
67 Self {
68 partition: None,
69 eliminated: EliminatedBlocks::default(),
70 ordering: SchurOrdering::default(),
71 hessian: None,
72 gradient: None,
73 }
74 }
75
76 pub fn with_ordering(mut self, ordering: SchurOrdering) -> Self {
78 self.ordering = ordering;
79 self
80 }
81
82 pub fn partition(&self) -> Option<&SchurPartition> {
84 self.partition.as_ref()
85 }
86
87 fn require_partition(&self) -> LinAlgResult<&SchurPartition> {
88 self.partition.as_ref().ok_or_else(|| {
89 LinAlgError::InvalidInput(
90 "Block structure not built. Call initialize_structure() first.".to_string(),
91 )
92 })
93 }
94
95 fn column_orderings(partition: &SchurPartition) -> (Vec<usize>, Vec<usize>) {
100 let kept: Vec<usize> = partition
101 .kept_blocks()
102 .iter()
103 .flat_map(|b| (0..b.dof).map(move |o| b.col_start + o))
104 .collect();
105 let eliminated: Vec<usize> = partition
106 .eliminated_blocks()
107 .iter()
108 .flat_map(|b| (0..b.dof).map(move |o| b.col_start + o))
109 .collect();
110 (kept, eliminated)
111 }
112
113 fn solve_with_hessian(
115 &mut self,
116 hessian: &Mat<f64>,
117 neg_gradient: &Mat<f64>,
118 damping: Option<&Damping>,
119 ) -> LinAlgResult<Mat<f64>> {
120 let partition = self.require_partition()?;
121 verify_block_diagonal_dense(partition, hessian)?;
122
123 let kept_dof = partition.kept_dof();
124 let eliminated_dof = partition.eliminated_dof();
125 let (kept_global, elim_global) = Self::column_orderings(partition);
126
127 let mut h_kk = Mat::<f64>::zeros(kept_dof, kept_dof);
129 for (lr, &gr) in kept_global.iter().enumerate() {
130 for (lc, &gc) in kept_global.iter().enumerate() {
131 h_kk[(lr, lc)] = hessian[(gr, gc)];
132 }
133 }
134 let mut h_ke = Mat::<f64>::zeros(kept_dof, eliminated_dof);
135 for (lr, &gr) in kept_global.iter().enumerate() {
136 for (lc, &gc) in elim_global.iter().enumerate() {
137 h_ke[(lr, lc)] = hessian[(gr, gc)];
138 }
139 }
140
141 let mut g_k = Mat::<f64>::zeros(kept_dof, 1);
142 for (lr, &gr) in kept_global.iter().enumerate() {
143 g_k[(lr, 0)] = neg_gradient[(gr, 0)];
144 }
145 let mut g_e = Mat::<f64>::zeros(eliminated_dof, 1);
146 for (lr, &gr) in elim_global.iter().enumerate() {
147 g_e[(lr, 0)] = neg_gradient[(gr, 0)];
148 }
149
150 if let Some(damping) = damping {
154 for i in 0..kept_dof {
155 h_kk[(i, i)] += damping.diagonal_term(h_kk[(i, i)]);
156 }
157 }
158
159 let mut eliminated = std::mem::take(&mut self.eliminated);
163 let prepared = (|| -> LinAlgResult<()> {
164 let partition = self.require_partition()?;
165 gather_dense(&mut eliminated, partition, hessian);
166 if let Some(damping) = damping {
167 eliminated.damp(damping);
168 }
169 eliminated.invert_in_place(partition)
170 })();
171 self.eliminated = eliminated;
172 prepared?;
173
174 let partition = self.require_partition()?;
175
176 let mut h_ee_inv = Mat::<f64>::zeros(eliminated_dof, eliminated_dof);
179 for (block_idx, block) in partition.eliminated_blocks().iter().enumerate() {
180 let dof = block.dof;
181 let base = partition.eliminated_offset(block_idx);
182 let inv = self.eliminated.block(block_idx);
183 for c in 0..dof {
184 for r in 0..dof {
185 h_ee_inv[(base + r, base + c)] = inv[c * dof + r];
186 }
187 }
188 }
189
190 let coupling = &h_ke * &h_ee_inv;
191 let s = &h_kk - &coupling * h_ke.transpose();
192 let g_reduced = &g_k - &coupling * &g_e;
193
194 let delta_k = solve_spd(
195 &s,
196 &g_reduced,
197 "Dense Schur complement factorization failed",
198 )?;
199
200 let hke_t_delta = h_ke.transpose() * &delta_k;
202 let mut delta_e = Mat::<f64>::zeros(eliminated_dof, 1);
203 for (block_idx, block) in partition.eliminated_blocks().iter().enumerate() {
204 let dof = block.dof;
205 let base = partition.eliminated_offset(block_idx);
206 let inv = self.eliminated.block(block_idx);
207 for r in 0..dof {
208 let mut acc = 0.0;
209 for c in 0..dof {
210 let rhs = g_e[(base + c, 0)] - hke_t_delta[(base + c, 0)];
211 acc += inv[c * dof + r] * rhs;
212 }
213 delta_e[(base + r, 0)] = acc;
214 }
215 }
216
217 let mut delta = Mat::<f64>::zeros(partition.total_dof(), 1);
218 for (lr, &gr) in kept_global.iter().enumerate() {
219 delta[(gr, 0)] = delta_k[(lr, 0)];
220 }
221 for (lr, &gr) in elim_global.iter().enumerate() {
222 delta[(gr, 0)] = delta_e[(lr, 0)];
223 }
224 Ok(delta)
225 }
226}
227
228impl Default for ExplicitDenseSchur {
229 fn default() -> Self {
230 Self::new()
231 }
232}
233
234impl StructureAware for ExplicitDenseSchur {
235 fn initialize_structure(
236 &mut self,
237 variables: &SlotMap<VarKey, Box<dyn ManifoldVariable>>,
238 variable_index_map: &SecondaryMap<VarKey, usize>,
239 schur_landmark_keys: &std::collections::HashSet<VarKey>,
240 ) -> LinAlgResult<()> {
241 let effective_keys =
242 effective_landmark_keys(variables, schur_landmark_keys, &self.ordering);
243
244 let mut kept = Vec::new();
245 let mut eliminated = Vec::new();
246 for (key, variable) in variables {
247 let col_start = *variable_index_map.get(key).ok_or_else(|| {
248 LinAlgError::InvalidInput(format!("VarKey {:?} not found in index map", key))
249 })?;
250 let span = BlockSpan {
251 key,
252 col_start,
253 dof: variable.free_dof(),
256 };
257 if effective_keys.contains(&key) {
258 eliminated.push(span);
259 } else {
260 kept.push(span);
261 }
262 }
263
264 let partition = SchurPartition::new(kept, eliminated)?;
265 self.eliminated = EliminatedBlocks::new(&partition);
266 self.partition = Some(partition);
267 Ok(())
268 }
269}
270
271impl LinearSolver<DenseMode> for ExplicitDenseSchur {
272 fn solve_normal_equation(
273 &mut self,
274 residuals: &Mat<f64>,
275 jacobian: &Mat<f64>,
276 ) -> LinAlgResult<Mat<f64>> {
277 self.require_partition()?;
278 let hessian = jacobian.transpose() * jacobian;
279 let gradient = jacobian.transpose() * residuals;
280 let mut neg_gradient = Mat::<f64>::zeros(gradient.nrows(), 1);
281 for i in 0..gradient.nrows() {
282 neg_gradient[(i, 0)] = -gradient[(i, 0)];
283 }
284
285 let result = self.solve_with_hessian(&hessian, &neg_gradient, None);
286 if result.is_ok() {
287 self.hessian = Some(hessian);
288 self.gradient = Some(gradient);
289 }
290 result
291 }
292
293 fn solve_augmented_equation(
294 &mut self,
295 residuals: &Mat<f64>,
296 jacobian: &Mat<f64>,
297 damping: &Damping,
298 ) -> LinAlgResult<Mat<f64>> {
299 self.require_partition()?;
300 let hessian = jacobian.transpose() * jacobian;
301 let gradient = jacobian.transpose() * residuals;
302 let mut neg_gradient = Mat::<f64>::zeros(gradient.nrows(), 1);
303 for i in 0..gradient.nrows() {
304 neg_gradient[(i, 0)] = -gradient[(i, 0)];
305 }
306
307 let result = self.solve_with_hessian(&hessian, &neg_gradient, Some(damping));
308 if result.is_ok() {
309 self.hessian = Some(hessian);
310 self.gradient = Some(gradient);
311 }
312 result
313 }
314
315 fn hessian_vec_product(&self, v: &Mat<f64>) -> Option<Mat<f64>> {
316 Some(
317 <DenseMode as crate::linearizer::AssemblyBackend>::hessian_vec_product(
318 self.hessian.as_ref()?,
319 v,
320 ),
321 )
322 }
323
324 fn get_hessian(&self) -> Option<&Mat<f64>> {
325 self.hessian.as_ref()
326 }
327
328 fn get_gradient(&self) -> Option<&Mat<f64>> {
329 self.gradient.as_ref()
330 }
331}
332
333#[cfg(test)]
334mod tests {
335 use super::*;
336 use crate::core::VarKey;
337 use crate::core::variable::Variable;
338 use apex_manifolds::{LieGroup, rn, se3};
339 use nalgebra::DVector;
340 use slotmap::{SecondaryMap, SlotMap};
341
342 type TestResult = Result<(), Box<dyn std::error::Error>>;
343
344 type TestSetup = (
345 SlotMap<VarKey, Box<dyn ManifoldVariable>>,
346 SecondaryMap<VarKey, usize>,
347 Mat<f64>,
348 Mat<f64>,
349 std::collections::HashSet<VarKey>,
350 );
351
352 fn create_schur_test_setup() -> Result<TestSetup, Box<dyn std::error::Error>> {
355 let se3_id = DVector::from_vec(vec![1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]);
356 let pt_zero = DVector::from_vec(vec![0.0, 0.0, 0.0]);
357
358 let mut variables: SlotMap<VarKey, Box<dyn ManifoldVariable>> = SlotMap::with_key();
359 let cam0 = variables.insert(Box::new(Variable::new(se3::SE3::from_param_slice(
360 se3_id.as_slice(),
361 ))));
362 let cam1 = variables.insert(Box::new(Variable::new(se3::SE3::from_param_slice(
363 se3_id.as_slice(),
364 ))));
365 let pt0 = variables.insert(Box::new(Variable::new(rn::Rn::new(pt_zero.clone()))));
366 let pt1 = variables.insert(Box::new(Variable::new(rn::Rn::new(pt_zero.clone()))));
367 let pt2 = variables.insert(Box::new(Variable::new(rn::Rn::new(pt_zero.clone()))));
368
369 let mut variable_index_map: SecondaryMap<VarKey, usize> = SecondaryMap::new();
370 variable_index_map.insert(cam0, 0);
371 variable_index_map.insert(cam1, 6);
372 variable_index_map.insert(pt0, 12);
373 variable_index_map.insert(pt1, 15);
374 variable_index_map.insert(pt2, 18);
375
376 let cam_cols = [0usize, 6];
377 let lm_cols = [12usize, 15, 18];
378 let mut jacobian = Mat::<f64>::zeros(36, 21);
379 for (ci, &cam_col) in cam_cols.iter().enumerate() {
387 for (li, &lm_col) in lm_cols.iter().enumerate() {
388 let row_base = (ci * 3 + li) * 6;
389 for k in 0..6 {
390 let cam_val = 1.0 + 0.1 * (ci as f64 + 1.0);
391 let lm_val = 0.3 + 0.05 * ((k + li + 2 * ci) % 5) as f64;
392 jacobian[(row_base + k, cam_col + k)] = cam_val;
393 jacobian[(row_base + k, lm_col + (k % 3))] = lm_val;
394 }
395 }
396 }
397 let residuals = Mat::from_fn(36, 1, |i, _| (i % 5) as f64 * 0.1);
398
399 let mut landmark_keys = std::collections::HashSet::new();
400 landmark_keys.insert(pt0);
401 landmark_keys.insert(pt1);
402 landmark_keys.insert(pt2);
403
404 Ok((
405 variables,
406 variable_index_map,
407 jacobian,
408 residuals,
409 landmark_keys,
410 ))
411 }
412
413 #[test]
414 fn test_dense_schur_initialize_structure() -> TestResult {
415 let (variables, variable_index_map, _, _, landmark_keys) = create_schur_test_setup()?;
416 let mut solver = ExplicitDenseSchur::new();
417 solver.initialize_structure(&variables, &variable_index_map, &landmark_keys)?;
418
419 let partition = solver.partition().ok_or("partition is None")?;
420 assert_eq!(partition.kept_dof(), 12);
421 assert_eq!(partition.eliminated_dof(), 9);
422 Ok(())
423 }
424
425 #[test]
426 fn test_dense_schur_solve_normal_equation() -> TestResult {
427 let (variables, variable_index_map, jacobian, residuals, landmark_keys) =
428 create_schur_test_setup()?;
429 let mut solver = ExplicitDenseSchur::new();
430 solver.initialize_structure(&variables, &variable_index_map, &landmark_keys)?;
431
432 let delta =
433 LinearSolver::<DenseMode>::solve_normal_equation(&mut solver, &residuals, &jacobian)?;
434 assert_eq!(delta.nrows(), 21);
435 Ok(())
436 }
437
438 #[test]
439 fn test_dense_schur_solve_augmented_equation() -> TestResult {
440 let (variables, variable_index_map, jacobian, residuals, landmark_keys) =
441 create_schur_test_setup()?;
442 let mut solver = ExplicitDenseSchur::new();
443 solver.initialize_structure(&variables, &variable_index_map, &landmark_keys)?;
444
445 let delta = LinearSolver::<DenseMode>::solve_augmented_equation(
446 &mut solver,
447 &residuals,
448 &jacobian,
449 &Damping::identity(0.1),
450 )?;
451 assert_eq!(delta.nrows(), 21);
452 Ok(())
453 }
454
455 #[test]
456 fn test_dense_schur_matches_dense_cholesky_on_full_system() -> TestResult {
457 use crate::linalg::dense::cholesky::DenseCholeskySolver;
458
459 let (variables, variable_index_map, jacobian, residuals, landmark_keys) =
460 create_schur_test_setup()?;
461
462 let mut cholesky = DenseCholeskySolver::new();
463 let reference =
464 LinearSolver::<DenseMode>::solve_normal_equation(&mut cholesky, &residuals, &jacobian)?;
465
466 let mut schur = ExplicitDenseSchur::new();
467 schur.initialize_structure(&variables, &variable_index_map, &landmark_keys)?;
468 let step =
469 LinearSolver::<DenseMode>::solve_normal_equation(&mut schur, &residuals, &jacobian)?;
470
471 for i in 0..21 {
472 assert!(
473 (reference[(i, 0)] - step[(i, 0)]).abs() < 1e-9,
474 "component {i}: cholesky {}, schur {}",
475 reference[(i, 0)],
476 step[(i, 0)]
477 );
478 }
479 Ok(())
480 }
481
482 #[test]
483 fn test_dense_schur_get_hessian_gradient() -> TestResult {
484 let (variables, variable_index_map, jacobian, residuals, landmark_keys) =
485 create_schur_test_setup()?;
486 let mut solver = ExplicitDenseSchur::new();
487 solver.initialize_structure(&variables, &variable_index_map, &landmark_keys)?;
488
489 assert!(LinearSolver::<DenseMode>::get_hessian(&solver).is_none());
490 assert!(LinearSolver::<DenseMode>::get_gradient(&solver).is_none());
491
492 LinearSolver::<DenseMode>::solve_normal_equation(&mut solver, &residuals, &jacobian)?;
493
494 let h = LinearSolver::<DenseMode>::get_hessian(&solver).ok_or("hessian is None")?;
495 let g = LinearSolver::<DenseMode>::get_gradient(&solver).ok_or("gradient is None")?;
496 assert_eq!(h.nrows(), 21);
497 assert_eq!(g.nrows(), 21);
498 Ok(())
499 }
500
501 #[test]
502 fn test_dense_schur_solve_without_init_returns_error() {
503 let jacobian = Mat::<f64>::zeros(1, 1);
504 let residuals = Mat::<f64>::zeros(1, 1);
505 let mut solver = ExplicitDenseSchur::new();
506 let result =
507 LinearSolver::<DenseMode>::solve_normal_equation(&mut solver, &residuals, &jacobian);
508 assert!(result.is_err());
509 }
510
511 #[test]
514 fn test_dense_schur_supports_non_contiguous_mixed_dof_partition() -> TestResult {
515 let a = Variable::new(se3::SE3::from_param_slice(&[
516 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
517 ]));
518 let b = Variable::new(se3::SE3::from_param_slice(&[
519 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
520 ]));
521 let p = Variable::new(rn::Rn::new(DVector::from_vec(vec![0.0])));
522 let q = Variable::new(rn::Rn::new(DVector::from_vec(vec![0.0])));
523
524 let mut variables: SlotMap<VarKey, Box<dyn ManifoldVariable>> = SlotMap::with_key();
525 let key_a = variables.insert(Box::new(a));
526 let key_p = variables.insert(Box::new(p));
527 let key_b = variables.insert(Box::new(b));
528 let key_q = variables.insert(Box::new(q));
529
530 let mut variable_index_map: SecondaryMap<VarKey, usize> = SecondaryMap::new();
531 variable_index_map.insert(key_a, 0);
532 variable_index_map.insert(key_p, 6);
533 variable_index_map.insert(key_b, 7);
534 variable_index_map.insert(key_q, 13);
535
536 let mut landmark_keys = std::collections::HashSet::new();
537 landmark_keys.insert(key_p);
538 landmark_keys.insert(key_q);
539
540 let mut jacobian = Mat::<f64>::zeros(4 + 14, 14);
541 let mut row = 0usize;
542 for &cam_col in &[0usize, 7] {
543 for &lm_col in &[6usize, 13] {
544 jacobian[(row, cam_col)] = 1.0;
545 jacobian[(row, lm_col)] = 1.0;
546 row += 1;
547 }
548 }
549 for col in 0..14 {
550 jacobian[(row, col)] = 0.5;
551 row += 1;
552 }
553 let residuals = Mat::from_fn(row, 1, |i, _| 0.1 * (i as f64 + 1.0));
554
555 let mut cholesky = crate::linalg::dense::cholesky::DenseCholeskySolver::new();
556 let reference =
557 LinearSolver::<DenseMode>::solve_normal_equation(&mut cholesky, &residuals, &jacobian)?;
558
559 let mut solver = ExplicitDenseSchur::new();
560 solver.initialize_structure(&variables, &variable_index_map, &landmark_keys)?;
561 let step =
562 LinearSolver::<DenseMode>::solve_normal_equation(&mut solver, &residuals, &jacobian)?;
563
564 for i in 0..14 {
565 assert!(
566 (reference[(i, 0)] - step[(i, 0)]).abs() < 1e-9,
567 "component {i}: cholesky {}, schur {}",
568 reference[(i, 0)],
569 step[(i, 0)]
570 );
571 }
572 Ok(())
573 }
574}