1use super::implicit_schur::IterativeSchurSolver;
48use crate::core::VarKey;
49use crate::core::variable::ManifoldVariable;
50use crate::linalg::{LinAlgError, LinAlgResult, LinearSolver, SparseMode, StructureAware};
51use apex_manifolds::ManifoldType;
52use faer::sparse::{SparseColMat, Triplet};
53use faer::{
54 Mat, Side,
55 linalg::solvers::Solve,
56 sparse::linalg::solvers::{Llt, SymbolicLlt},
57};
58use nalgebra::Matrix3;
59use slotmap::{SecondaryMap, SlotMap};
60use tracing::debug;
61
62#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
64pub enum SchurVariant {
65 #[default]
67 Sparse,
68 Iterative,
70}
71
72#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
74pub enum SchurPreconditioner {
75 None,
77 BlockDiagonal,
79 #[default]
82 SchurJacobi,
83}
84
85#[derive(Debug, Clone)]
87pub struct SchurOrdering {
88 pub eliminate_types: Vec<ManifoldType>,
89 pub eliminate_rn_size: Option<usize>,
92}
93
94impl Default for SchurOrdering {
95 fn default() -> Self {
96 Self {
97 eliminate_types: vec![ManifoldType::RN],
98 eliminate_rn_size: Some(3), }
100 }
101}
102
103impl SchurOrdering {
104 pub fn new() -> Self {
105 Self::default()
106 }
107
108 pub fn should_eliminate(&self, manifold_type: &ManifoldType, size: usize) -> bool {
113 if !self.eliminate_types.contains(manifold_type) {
114 return false;
115 }
116 if let Some(required_size) = self.eliminate_rn_size {
117 if size != required_size {
118 return false;
119 }
120 }
121 true
122 }
123}
124
125#[derive(Debug, Clone)]
127pub struct SchurBlockStructure {
128 pub camera_blocks: Vec<(VarKey, usize, usize)>,
129 pub landmark_blocks: Vec<(VarKey, usize, usize)>,
130 pub camera_dof: usize,
131 pub landmark_dof: usize,
132 pub num_landmarks: usize,
133}
134
135impl SchurBlockStructure {
136 pub fn new() -> Self {
137 Self {
138 camera_blocks: Vec::new(),
139 landmark_blocks: Vec::new(),
140 camera_dof: 0,
141 landmark_dof: 0,
142 num_landmarks: 0,
143 }
144 }
145
146 pub fn camera_col_range(&self) -> (usize, usize) {
147 if self.camera_blocks.is_empty() {
148 (0, 0)
149 } else {
150 let start = self.camera_blocks.first().map(|b| b.1).unwrap_or(0);
152 (start, start + self.camera_dof)
153 }
154 }
155
156 pub fn landmark_col_range(&self) -> (usize, usize) {
157 if self.landmark_blocks.is_empty() {
158 (0, 0)
159 } else {
160 let start = self.landmark_blocks.first().map(|b| b.1).unwrap_or(0);
162 (start, start + self.landmark_dof)
163 }
164 }
165}
166
167impl Default for SchurBlockStructure {
168 fn default() -> Self {
169 Self::new()
170 }
171}
172
173#[derive(Debug, Clone)]
175pub struct SparseSchurComplementSolver {
176 block_structure: Option<SchurBlockStructure>,
177 ordering: SchurOrdering,
178 variant: SchurVariant,
179 preconditioner: SchurPreconditioner,
180
181 cg_max_iterations: usize,
183 cg_tolerance: f64,
184
185 hessian: Option<SparseColMat<usize, f64>>,
187 gradient: Option<Mat<f64>>,
188
189 iterative_solver: Option<IterativeSchurSolver>,
191}
192
193impl SparseSchurComplementSolver {
194 pub fn new() -> Self {
195 Self {
196 block_structure: None,
197 ordering: SchurOrdering::default(),
198 variant: SchurVariant::default(),
199 preconditioner: SchurPreconditioner::default(),
200 cg_max_iterations: 200, cg_tolerance: 1e-6, hessian: None,
203 gradient: None,
204 iterative_solver: None,
205 }
206 }
207
208 pub fn with_ordering(mut self, ordering: SchurOrdering) -> Self {
209 self.ordering = ordering;
210 self
211 }
212
213 pub fn with_variant(mut self, variant: SchurVariant) -> Self {
214 self.variant = variant;
215 self
216 }
217
218 pub fn with_preconditioner(mut self, preconditioner: SchurPreconditioner) -> Self {
219 self.preconditioner = preconditioner;
220 self
221 }
222
223 pub fn with_cg_params(mut self, max_iter: usize, tol: f64) -> Self {
224 self.cg_max_iterations = max_iter;
225 self.cg_tolerance = tol;
226 self
227 }
228
229 pub fn block_structure(&self) -> Option<&SchurBlockStructure> {
230 self.block_structure.as_ref()
231 }
232
233 fn build_block_structure(
234 &mut self,
235 variables: &SlotMap<VarKey, Box<dyn ManifoldVariable>>,
236 variable_index_map: &SecondaryMap<VarKey, usize>,
237 schur_landmark_keys: &std::collections::HashSet<VarKey>,
238 ) -> LinAlgResult<()> {
239 let mut structure = SchurBlockStructure::new();
240
241 for (key, variable) in variables {
242 let start_col = *variable_index_map.get(key).ok_or_else(|| {
243 LinAlgError::InvalidInput(format!("VarKey {:?} not found in index map", key))
244 })?;
245 let size = variable.dof();
246
247 if schur_landmark_keys.contains(&key) {
248 structure.landmark_blocks.push((key, start_col, size));
249 structure.landmark_dof += size;
250 structure.num_landmarks += 1;
251 } else {
252 structure.camera_blocks.push((key, start_col, size));
253 structure.camera_dof += size;
254 }
255 }
256
257 structure.camera_blocks.sort_by_key(|(_, col, _)| *col);
258 structure.landmark_blocks.sort_by_key(|(_, col, _)| *col);
259
260 if structure.camera_blocks.is_empty() {
261 return Err(LinAlgError::InvalidInput(
262 "No camera variables found".to_string(),
263 ));
264 }
265 if structure.landmark_blocks.is_empty() {
266 return Err(LinAlgError::InvalidInput(
267 "No landmark variables found".to_string(),
268 ));
269 }
270
271 debug!("Schur complement block structure:");
273 debug!(
274 " Camera blocks: {} variables, {} total DOF",
275 structure.camera_blocks.len(),
276 structure.camera_dof
277 );
278 debug!(
279 " Landmark blocks: {} variables, {} total DOF",
280 structure.landmark_blocks.len(),
281 structure.landmark_dof
282 );
283 debug!(" Camera column range: {:?}", structure.camera_col_range());
284 debug!(
285 " Landmark column range: {:?}",
286 structure.landmark_col_range()
287 );
288 debug!(
289 " Schur complement S size: {} × {}",
290 structure.camera_dof, structure.camera_dof
291 );
292
293 let (_cam_start, cam_end) = structure.camera_col_range();
295 let (land_start, _land_end) = structure.landmark_col_range();
296 if cam_end != land_start {
297 debug!(
298 "WARNING: Gap between camera and landmark blocks! cam_end={}, land_start={}",
299 cam_end, land_start
300 );
301 }
302
303 self.block_structure = Some(structure);
304 Ok(())
305 }
306
307 fn extract_landmark_blocks(
309 &self,
310 hessian: &SparseColMat<usize, f64>,
311 ) -> LinAlgResult<Vec<Matrix3<f64>>> {
312 let structure = self
313 .block_structure
314 .as_ref()
315 .ok_or_else(|| LinAlgError::InvalidInput("Block structure not built".to_string()))?;
316
317 let mut blocks = Vec::with_capacity(structure.num_landmarks);
318 let symbolic = hessian.symbolic();
319
320 for (_, start_col, _) in &structure.landmark_blocks {
321 let mut block = Matrix3::<f64>::zeros();
322
323 for local_col in 0..3 {
324 let global_col = start_col + local_col;
325 let row_indices = symbolic.row_idx_of_col_raw(global_col);
326 let col_values = hessian.val_of_col(global_col);
327
328 for (idx, &row) in row_indices.iter().enumerate() {
329 if row >= *start_col && row < start_col + 3 {
330 let local_row = row - start_col;
331 block[(local_row, local_col)] = col_values[idx];
332 }
333 }
334 }
335
336 blocks.push(block);
337 }
338
339 Ok(blocks)
340 }
341
342 fn invert_landmark_blocks(blocks: &[Matrix3<f64>]) -> LinAlgResult<Vec<Matrix3<f64>>> {
348 Self::invert_landmark_blocks_with_lambda(blocks, 0.0)
349 }
350
351 fn invert_landmark_blocks_with_lambda(
360 blocks: &[Matrix3<f64>],
361 lambda: f64,
362 ) -> LinAlgResult<Vec<Matrix3<f64>>> {
363 const CONDITION_THRESHOLD: f64 = 1e10; const MIN_EIGENVALUE_THRESHOLD: f64 = 1e-12; const REGULARIZATION_SCALE: f64 = 1e-6; let mut ill_conditioned_count = 0;
369 let mut regularized_count = 0;
370
371 let result: LinAlgResult<Vec<Matrix3<f64>>> = blocks
372 .iter()
373 .enumerate()
374 .map(|(i, block)| {
375 let eigenvalues = block.symmetric_eigenvalues();
378 let min_ev = eigenvalues.min();
379 let max_ev = eigenvalues.max();
380
381 if min_ev < MIN_EIGENVALUE_THRESHOLD {
382 regularized_count += 1;
384 let reg = lambda.max(REGULARIZATION_SCALE) + max_ev * REGULARIZATION_SCALE;
385 let regularized = block + Matrix3::identity() * reg;
386 regularized.try_inverse().ok_or_else(|| {
387 LinAlgError::SingularMatrix(format!(
388 "Landmark block {} singular even with regularization (min_ev={:.2e})",
389 i, min_ev
390 ))
391 })
392 } else if max_ev / min_ev > CONDITION_THRESHOLD {
393 ill_conditioned_count += 1;
395 let extra_reg = max_ev * REGULARIZATION_SCALE;
396 let regularized = block + Matrix3::identity() * extra_reg;
397 regularized.try_inverse().ok_or_else(|| {
398 LinAlgError::SingularMatrix(format!(
399 "Landmark block {} ill-conditioned (cond={:.2e})",
400 i,
401 max_ev / min_ev
402 ))
403 })
404 } else {
405 block.try_inverse().ok_or_else(|| {
407 LinAlgError::SingularMatrix(format!("Landmark block {} is singular", i))
408 })
409 }
410 })
411 .collect();
412
413 if ill_conditioned_count > 0 || regularized_count > 0 {
415 debug!(
416 "Landmark block conditioning: {} ill-conditioned, {} regularized out of {}",
417 ill_conditioned_count,
418 regularized_count,
419 blocks.len()
420 );
421 }
422
423 result
424 }
425
426 fn extract_camera_block(
428 &self,
429 hessian: &SparseColMat<usize, f64>,
430 ) -> LinAlgResult<SparseColMat<usize, f64>> {
431 let structure = self
432 .block_structure
433 .as_ref()
434 .ok_or_else(|| LinAlgError::InvalidInput("Block structure not built".to_string()))?;
435
436 let (cam_start, cam_end) = structure.camera_col_range();
437 let cam_size = structure.camera_dof;
438 let symbolic = hessian.symbolic();
439
440 let mut triplets = Vec::new();
441
442 for global_col in cam_start..cam_end {
443 let local_col = global_col - cam_start;
444 let row_indices = symbolic.row_idx_of_col_raw(global_col);
445 let col_values = hessian.val_of_col(global_col);
446
447 for (idx, &global_row) in row_indices.iter().enumerate() {
448 if global_row >= cam_start && global_row < cam_end {
449 let local_row = global_row - cam_start;
450 triplets.push(Triplet::new(local_row, local_col, col_values[idx]));
451 }
452 }
453 }
454
455 SparseColMat::try_new_from_triplets(cam_size, cam_size, &triplets)
456 .map_err(|e| LinAlgError::SparseMatrixCreation(format!("H_cc: {:?}", e)))
457 }
458
459 fn extract_coupling_block(
461 &self,
462 hessian: &SparseColMat<usize, f64>,
463 ) -> LinAlgResult<SparseColMat<usize, f64>> {
464 let structure = self
465 .block_structure
466 .as_ref()
467 .ok_or_else(|| LinAlgError::InvalidInput("Block structure not built".to_string()))?;
468
469 let (cam_start, cam_end) = structure.camera_col_range();
470 let (land_start, land_end) = structure.landmark_col_range();
471 let cam_size = structure.camera_dof;
472 let land_size = structure.landmark_dof;
473 let symbolic = hessian.symbolic();
474
475 let mut triplets = Vec::new();
476
477 for global_col in land_start..land_end {
478 let local_col = global_col - land_start;
479 let row_indices = symbolic.row_idx_of_col_raw(global_col);
480 let col_values = hessian.val_of_col(global_col);
481
482 for (idx, &global_row) in row_indices.iter().enumerate() {
483 if global_row >= cam_start && global_row < cam_end {
484 let local_row = global_row - cam_start;
485 triplets.push(Triplet::new(local_row, local_col, col_values[idx]));
486 }
487 }
488 }
489
490 SparseColMat::try_new_from_triplets(cam_size, land_size, &triplets)
491 .map_err(|e| LinAlgError::SparseMatrixCreation(format!("H_cp: {:?}", e)))
492 }
493
494 fn extract_gradient_blocks(&self, gradient: &Mat<f64>) -> LinAlgResult<(Mat<f64>, Mat<f64>)> {
496 let structure = self
497 .block_structure
498 .as_ref()
499 .ok_or_else(|| LinAlgError::InvalidInput("Block structure not built".to_string()))?;
500
501 let (cam_start, cam_end) = structure.camera_col_range();
502 let (land_start, land_end) = structure.landmark_col_range();
503
504 let mut g_c = Mat::zeros(structure.camera_dof, 1);
505 for i in 0..(cam_end - cam_start) {
506 g_c[(i, 0)] = gradient[(cam_start + i, 0)];
507 }
508
509 let mut g_p = Mat::zeros(structure.landmark_dof, 1);
510 for i in 0..(land_end - land_start) {
511 g_p[(i, 0)] = gradient[(land_start + i, 0)];
512 }
513
514 Ok((g_c, g_p))
515 }
516
517 fn solve_with_cholesky(
522 &self,
523 a: &SparseColMat<usize, f64>,
524 b: &Mat<f64>,
525 ) -> LinAlgResult<Mat<f64>> {
526 let sym = SymbolicLlt::try_new(a.symbolic(), Side::Lower).map_err(|e| {
527 LinAlgError::FactorizationFailed(format!("Symbolic Cholesky failed: {:?}", e))
528 })?;
529
530 match Llt::try_new_with_symbolic(sym.clone(), a.as_ref(), Side::Lower) {
532 Ok(cholesky) => return Ok(cholesky.solve(b)),
533 Err(e) => {
534 debug!(
535 "Cholesky factorization failed: {:?}. Applying regularization.",
536 e
537 );
538 }
539 }
540
541 let n = a.nrows();
543 let symbolic = a.symbolic();
544
545 let mut trace = 0.0;
547 let mut max_diag = 0.0f64;
548 for col in 0..n {
549 let row_indices = symbolic.row_idx_of_col_raw(col);
550 let col_values = a.val_of_col(col);
551 for (idx, &row) in row_indices.iter().enumerate() {
552 if row == col {
553 trace += col_values[idx];
554 max_diag = max_diag.max(col_values[idx].abs());
555 }
556 }
557 }
558
559 let avg_diag = trace / n as f64;
561 let base_reg = avg_diag.max(max_diag).max(1.0);
562
563 for attempt in 0..5 {
564 let reg = base_reg * 10.0f64.powi(attempt - 4); debug!(
566 "Cholesky attempt {}: regularization = {:.2e}",
567 attempt + 2,
568 reg
569 );
570
571 let mut triplets = Vec::with_capacity(n * 10);
572 for col in 0..n {
573 let row_indices = symbolic.row_idx_of_col_raw(col);
574 let col_values = a.val_of_col(col);
575 for (idx, &row) in row_indices.iter().enumerate() {
576 triplets.push(Triplet::new(row, col, col_values[idx]));
577 }
578 }
579
580 for i in 0..n {
581 triplets.push(Triplet::new(i, i, reg));
582 }
583
584 let a_reg = match SparseColMat::try_new_from_triplets(n, n, &triplets) {
585 Ok(m) => m,
586 Err(e) => {
587 debug!("Failed to create regularized matrix: {:?}", e);
588 continue;
589 }
590 };
591
592 let sym_reg = match SymbolicLlt::try_new(a_reg.symbolic(), Side::Lower) {
594 Ok(s) => s,
595 Err(e) => {
596 debug!("Symbolic factorization failed: {:?}", e);
597 continue;
598 }
599 };
600
601 match Llt::try_new_with_symbolic(sym_reg, a_reg.as_ref(), Side::Lower) {
602 Ok(cholesky) => {
603 debug!("Cholesky succeeded with regularization {:.2e}", reg);
604 return Ok(cholesky.solve(b));
605 }
606 Err(e) => {
607 debug!("Cholesky failed with reg {:.2e}: {:?}", reg, e);
608 }
609 }
610 }
611
612 Err(LinAlgError::SingularMatrix(format!(
613 "Schur complement singular after 5 regularization attempts (max reg = {:.2e})",
614 base_reg
615 )))
616 }
617
618 fn solve_with_pcg(&self, a: &SparseColMat<usize, f64>, b: &Mat<f64>) -> LinAlgResult<Mat<f64>> {
622 let n = b.nrows();
623 let max_iterations = self.cg_max_iterations;
624 let tolerance = self.cg_tolerance;
625
626 let symbolic = a.symbolic();
628 let mut precond = vec![1.0; n];
629 for (col, precond_val) in precond.iter_mut().enumerate().take(n) {
630 let row_indices = symbolic.row_idx_of_col_raw(col);
631 let col_values = a.val_of_col(col);
632 for (idx, &row) in row_indices.iter().enumerate() {
633 if row == col {
634 let diag = col_values[idx];
635 *precond_val = if diag.abs() > 1e-12 { 1.0 / diag } else { 1.0 };
636 break;
637 }
638 }
639 }
640
641 let mut x = Mat::<f64>::zeros(n, 1);
643
644 let mut r = b.clone();
646
647 let mut z = Mat::<f64>::zeros(n, 1);
649 for i in 0..n {
650 z[(i, 0)] = precond[i] * r[(i, 0)];
651 }
652
653 let mut p = z.clone();
654
655 let mut rz_old = 0.0;
656 for i in 0..n {
657 rz_old += r[(i, 0)] * z[(i, 0)];
658 }
659
660 let mut r_norm_init = 0.0;
662 for i in 0..n {
663 r_norm_init += r[(i, 0)] * r[(i, 0)];
664 }
665 r_norm_init = r_norm_init.sqrt();
666 let abs_tol = tolerance * r_norm_init.max(1.0);
667
668 for _iter in 0..max_iterations {
669 let mut ap = Mat::<f64>::zeros(n, 1);
671 for col in 0..n {
672 let row_indices = symbolic.row_idx_of_col_raw(col);
673 let col_values = a.val_of_col(col);
674 for (idx, &row) in row_indices.iter().enumerate() {
675 ap[(row, 0)] += col_values[idx] * p[(col, 0)];
676 }
677 }
678
679 let mut p_ap = 0.0;
681 for i in 0..n {
682 p_ap += p[(i, 0)] * ap[(i, 0)];
683 }
684
685 if p_ap.abs() < 1e-30 {
686 break;
687 }
688
689 let alpha = rz_old / p_ap;
690
691 for i in 0..n {
693 x[(i, 0)] += alpha * p[(i, 0)];
694 }
695
696 for i in 0..n {
698 r[(i, 0)] -= alpha * ap[(i, 0)];
699 }
700
701 let mut r_norm = 0.0;
703 for i in 0..n {
704 r_norm += r[(i, 0)] * r[(i, 0)];
705 }
706 r_norm = r_norm.sqrt();
707
708 if r_norm < abs_tol {
709 break;
710 }
711
712 for i in 0..n {
714 z[(i, 0)] = precond[i] * r[(i, 0)];
715 }
716
717 let mut rz_new = 0.0;
719 for i in 0..n {
720 rz_new += r[(i, 0)] * z[(i, 0)];
721 }
722
723 if rz_old.abs() < 1e-30 {
724 break;
725 }
726
727 let beta = rz_new / rz_old;
728
729 for i in 0..n {
731 p[(i, 0)] = z[(i, 0)] + beta * p[(i, 0)];
732 }
733
734 rz_old = rz_new;
735 }
736
737 Ok(x)
738 }
739
740 fn compute_schur_complement(
754 &self,
755 h_cc: &SparseColMat<usize, f64>,
756 h_cp: &SparseColMat<usize, f64>,
757 hpp_inv_blocks: &[Matrix3<f64>],
758 ) -> LinAlgResult<SparseColMat<usize, f64>> {
759 let cam_size = h_cc.nrows();
760 let h_cp_symbolic = h_cp.symbolic();
761
762 let mut s_dense = vec![0.0f64; cam_size * cam_size];
765
766 let h_cc_symbolic = h_cc.symbolic();
768 for col in 0..h_cc.ncols() {
769 let row_indices = h_cc_symbolic.row_idx_of_col_raw(col);
770 let col_values = h_cc.val_of_col(col);
771 for (idx, &row) in row_indices.iter().enumerate() {
772 s_dense[row * cam_size + col] += col_values[idx];
773 }
774 }
775
776 let mut cam_rows: Vec<usize> = Vec::with_capacity(32);
779 let mut h_cp_block: Vec<[f64; 3]> = Vec::with_capacity(32);
780 let mut contrib_block: Vec<[f64; 3]> = Vec::with_capacity(32);
781
782 for (block_idx, hpp_inv_block) in hpp_inv_blocks.iter().enumerate() {
784 let col_start = block_idx * 3;
785
786 cam_rows.clear();
787 h_cp_block.clear();
788
789 if col_start + 2 >= h_cp.ncols() {
790 continue;
791 }
792
793 let row_indices_0 = h_cp_symbolic.row_idx_of_col_raw(col_start);
794 let col_values_0 = h_cp.val_of_col(col_start);
795 let row_indices_1 = h_cp_symbolic.row_idx_of_col_raw(col_start + 1);
796 let col_values_1 = h_cp.val_of_col(col_start + 1);
797 let row_indices_2 = h_cp_symbolic.row_idx_of_col_raw(col_start + 2);
798 let col_values_2 = h_cp.val_of_col(col_start + 2);
799
800 let mut i0 = 0;
801 let mut i1 = 0;
802 let mut i2 = 0;
803
804 while i0 < row_indices_0.len() || i1 < row_indices_1.len() || i2 < row_indices_2.len() {
805 let r0 = if i0 < row_indices_0.len() {
806 row_indices_0[i0]
807 } else {
808 usize::MAX
809 };
810 let r1 = if i1 < row_indices_1.len() {
811 row_indices_1[i1]
812 } else {
813 usize::MAX
814 };
815 let r2 = if i2 < row_indices_2.len() {
816 row_indices_2[i2]
817 } else {
818 usize::MAX
819 };
820
821 let min_row = r0.min(r1).min(r2);
822 if min_row == usize::MAX {
823 break;
824 }
825
826 let v0 = if r0 == min_row {
827 i0 += 1;
828 col_values_0[i0 - 1]
829 } else {
830 0.0
831 };
832 let v1 = if r1 == min_row {
833 i1 += 1;
834 col_values_1[i1 - 1]
835 } else {
836 0.0
837 };
838 let v2 = if r2 == min_row {
839 i2 += 1;
840 col_values_2[i2 - 1]
841 } else {
842 0.0
843 };
844
845 cam_rows.push(min_row);
846 h_cp_block.push([v0, v1, v2]);
847 }
848
849 if cam_rows.is_empty() {
850 continue;
851 }
852
853 contrib_block.clear();
854 for h_cp_row in &h_cp_block {
855 let c0 = h_cp_row[0] * hpp_inv_block[(0, 0)]
856 + h_cp_row[1] * hpp_inv_block[(1, 0)]
857 + h_cp_row[2] * hpp_inv_block[(2, 0)];
858 let c1 = h_cp_row[0] * hpp_inv_block[(0, 1)]
859 + h_cp_row[1] * hpp_inv_block[(1, 1)]
860 + h_cp_row[2] * hpp_inv_block[(2, 1)];
861 let c2 = h_cp_row[0] * hpp_inv_block[(0, 2)]
862 + h_cp_row[1] * hpp_inv_block[(1, 2)]
863 + h_cp_row[2] * hpp_inv_block[(2, 2)];
864 contrib_block.push([c0, c1, c2]);
865 }
866
867 let n_cams = cam_rows.len();
868 for i in 0..n_cams {
869 let cam_i = cam_rows[i];
870 let contrib_i = &contrib_block[i];
871 for j in 0..n_cams {
872 let cam_j = cam_rows[j];
873 let h_cp_j = &h_cp_block[j];
874 let dot = contrib_i[0] * h_cp_j[0]
875 + contrib_i[1] * h_cp_j[1]
876 + contrib_i[2] * h_cp_j[2];
877 s_dense[cam_i * cam_size + cam_j] -= dot;
878 }
879 }
880 }
881
882 for i in 0..cam_size {
886 for j in (i + 1)..cam_size {
887 let avg = (s_dense[i * cam_size + j] + s_dense[j * cam_size + i]) * 0.5;
888 s_dense[i * cam_size + j] = avg;
889 s_dense[j * cam_size + i] = avg;
890 }
891 }
892
893 let mut s_triplets: Vec<Triplet<usize, usize, f64>> = Vec::new();
896 for col in 0..cam_size {
897 for row in 0..cam_size {
898 let val = s_dense[row * cam_size + col];
899 if val.abs() > 1e-12 {
900 s_triplets.push(Triplet::new(row, col, val));
901 }
902 }
903 }
904
905 SparseColMat::try_new_from_triplets(cam_size, cam_size, &s_triplets)
906 .map_err(|e| LinAlgError::SparseMatrixCreation(format!("Schur S: {:?}", e)))
907 }
908
909 fn compute_reduced_gradient(
911 &self,
912 g_c: &Mat<f64>,
913 g_p: &Mat<f64>,
914 h_cp: &SparseColMat<usize, f64>,
915 hpp_inv_blocks: &[Matrix3<f64>],
916 ) -> LinAlgResult<Mat<f64>> {
917 let land_size = g_p.nrows();
919 let cam_size = g_c.nrows();
920
921 let mut hpp_inv_gp = Mat::zeros(land_size, 1);
923
924 for (block_idx, hpp_inv_block) in hpp_inv_blocks.iter().enumerate() {
925 let row_start = block_idx * 3;
926
927 let gp_block = nalgebra::Vector3::new(
928 g_p[(row_start, 0)],
929 g_p[(row_start + 1, 0)],
930 g_p[(row_start + 2, 0)],
931 );
932
933 let result = hpp_inv_block * gp_block;
934 hpp_inv_gp[(row_start, 0)] = result[0];
935 hpp_inv_gp[(row_start + 1, 0)] = result[1];
936 hpp_inv_gp[(row_start + 2, 0)] = result[2];
937 }
938
939 let mut h_cp_hpp_inv_gp = Mat::<f64>::zeros(cam_size, 1);
941 let symbolic = h_cp.symbolic();
942
943 for col in 0..h_cp.ncols() {
944 let row_indices = symbolic.row_idx_of_col_raw(col);
945 let col_values = h_cp.val_of_col(col);
946
947 for (idx, &row) in row_indices.iter().enumerate() {
948 h_cp_hpp_inv_gp[(row, 0)] += col_values[idx] * hpp_inv_gp[(col, 0)];
949 }
950 }
951
952 let mut g_reduced = Mat::zeros(cam_size, 1);
954 for i in 0..cam_size {
955 g_reduced[(i, 0)] = g_c[(i, 0)] - h_cp_hpp_inv_gp[(i, 0)];
956 }
957
958 Ok(g_reduced)
959 }
960
961 fn back_substitute(
963 &self,
964 delta_c: &Mat<f64>,
965 g_p: &Mat<f64>,
966 h_cp: &SparseColMat<usize, f64>,
967 hpp_inv_blocks: &[Matrix3<f64>],
968 ) -> LinAlgResult<Mat<f64>> {
969 let land_size = g_p.nrows();
972
973 let mut h_cp_t_delta_c = Mat::<f64>::zeros(land_size, 1);
975 let symbolic = h_cp.symbolic();
976
977 for col in 0..h_cp.ncols() {
978 let row_indices = symbolic.row_idx_of_col_raw(col);
979 let col_values = h_cp.val_of_col(col);
980
981 for (idx, &row) in row_indices.iter().enumerate() {
982 h_cp_t_delta_c[(col, 0)] += col_values[idx] * delta_c[(row, 0)];
983 }
984 }
985
986 let mut rhs = Mat::zeros(land_size, 1);
988 for i in 0..land_size {
989 rhs[(i, 0)] = g_p[(i, 0)] - h_cp_t_delta_c[(i, 0)];
990 }
991
992 let mut delta_p = Mat::zeros(land_size, 1);
994
995 for (block_idx, hpp_inv_block) in hpp_inv_blocks.iter().enumerate() {
996 let row_start = block_idx * 3;
997
998 let rhs_block = nalgebra::Vector3::new(
999 rhs[(row_start, 0)],
1000 rhs[(row_start + 1, 0)],
1001 rhs[(row_start + 2, 0)],
1002 );
1003
1004 let result = hpp_inv_block * rhs_block;
1005 delta_p[(row_start, 0)] = result[0];
1006 delta_p[(row_start + 1, 0)] = result[1];
1007 delta_p[(row_start + 2, 0)] = result[2];
1008 }
1009
1010 Ok(delta_p)
1011 }
1012}
1013
1014impl Default for SparseSchurComplementSolver {
1015 fn default() -> Self {
1016 Self::new()
1017 }
1018}
1019
1020impl StructureAware for SparseSchurComplementSolver {
1021 fn initialize_structure(
1022 &mut self,
1023 variables: &SlotMap<VarKey, Box<dyn ManifoldVariable>>,
1024 variable_index_map: &SecondaryMap<VarKey, usize>,
1025 schur_landmark_keys: &std::collections::HashSet<VarKey>,
1026 ) -> LinAlgResult<()> {
1027 self.build_block_structure(variables, variable_index_map, schur_landmark_keys)?;
1029
1030 match self.variant {
1032 SchurVariant::Iterative => {
1033 let mut solver =
1034 IterativeSchurSolver::with_cg_params(self.cg_max_iterations, self.cg_tolerance);
1035 solver.initialize_structure(variables, variable_index_map, schur_landmark_keys)?;
1036 self.iterative_solver = Some(solver);
1037 }
1038 SchurVariant::Sparse => {
1039 }
1041 }
1042
1043 Ok(())
1044 }
1045}
1046
1047impl LinearSolver<SparseMode> for SparseSchurComplementSolver {
1048 fn solve_normal_equation(
1049 &mut self,
1050 residuals: &Mat<f64>,
1051 jacobian: &SparseColMat<usize, f64>,
1052 ) -> LinAlgResult<Mat<f64>> {
1053 use std::ops::Mul;
1054 let jacobians = jacobian;
1055
1056 if self.block_structure.is_none() {
1057 return Err(LinAlgError::InvalidInput(
1058 "Block structure not built. Call initialize_structure() first.".to_string(),
1059 ));
1060 }
1061
1062 let jt = jacobians
1069 .transpose()
1070 .to_col_major()
1071 .map_err(|e| LinAlgError::MatrixConversion(format!("Transpose failed: {:?}", e)))?;
1072 let hessian = jt.mul(jacobians);
1073 let gradient = jacobians.transpose().mul(residuals);
1074 let mut neg_gradient = Mat::zeros(gradient.nrows(), 1);
1075 for i in 0..gradient.nrows() {
1076 neg_gradient[(i, 0)] = -gradient[(i, 0)];
1077 }
1078
1079 self.hessian = Some(hessian.clone());
1080 self.gradient = Some(gradient.clone());
1083
1084 let h_cc = self.extract_camera_block(&hessian)?;
1086 let h_cp = self.extract_coupling_block(&hessian)?;
1087 let hpp_blocks = self.extract_landmark_blocks(&hessian)?;
1088 let (g_c, g_p) = self.extract_gradient_blocks(&neg_gradient)?;
1089
1090 let hpp_inv_blocks = Self::invert_landmark_blocks(&hpp_blocks)?;
1092
1093 let s = self.compute_schur_complement(&h_cc, &h_cp, &hpp_inv_blocks)?;
1095
1096 let g_reduced = self.compute_reduced_gradient(&g_c, &g_p, &h_cp, &hpp_inv_blocks)?;
1098
1099 let delta_c = match self.variant {
1101 SchurVariant::Iterative => self.solve_with_pcg(&s, &g_reduced)?,
1102 _ => self.solve_with_cholesky(&s, &g_reduced)?,
1103 };
1104
1105 let delta_p = self.back_substitute(&delta_c, &g_p, &h_cp, &hpp_inv_blocks)?;
1107
1108 self.combine_updates(&delta_c, &delta_p)
1110 }
1111
1112 fn solve_augmented_equation(
1113 &mut self,
1114 residuals: &Mat<f64>,
1115 jacobian: &SparseColMat<usize, f64>,
1116 lambda: f64,
1117 ) -> LinAlgResult<Mat<f64>> {
1118 use std::ops::Mul;
1119 let jacobians = jacobian;
1120
1121 if self.block_structure.is_none() {
1122 return Err(LinAlgError::InvalidInput(
1123 "Block structure not built. Call initialize_structure() first.".to_string(),
1124 ));
1125 }
1126
1127 let jt = jacobians
1130 .transpose()
1131 .to_col_major()
1132 .map_err(|e| LinAlgError::MatrixConversion(format!("Transpose failed: {:?}", e)))?;
1133 let hessian = jt.mul(jacobians);
1134 let gradient = jacobians.transpose().mul(residuals);
1135 let mut neg_gradient = Mat::zeros(gradient.nrows(), 1);
1136 for i in 0..gradient.nrows() {
1137 neg_gradient[(i, 0)] = -gradient[(i, 0)];
1138 }
1139
1140 self.hessian = Some(hessian.clone());
1141 self.gradient = Some(gradient.clone());
1144
1145 let h_cc = self.extract_camera_block(&hessian)?;
1147 let h_cp = self.extract_coupling_block(&hessian)?;
1148 let mut hpp_blocks = self.extract_landmark_blocks(&hessian)?;
1149 let (g_c, g_p) = self.extract_gradient_blocks(&neg_gradient)?;
1150
1151 debug!("Iteration matrices:");
1153 debug!(
1154 " Hessian (J^T*J): {} × {}",
1155 hessian.nrows(),
1156 hessian.ncols()
1157 );
1158 debug!(" H_cc (camera): {} × {}", h_cc.nrows(), h_cc.ncols());
1159 debug!(" H_cp (coupling): {} × {}", h_cp.nrows(), h_cp.ncols());
1160 debug!(" H_pp blocks: {} (3×3 each)", hpp_blocks.len());
1161
1162 let structure = self
1164 .block_structure
1165 .as_ref()
1166 .ok_or_else(|| LinAlgError::InvalidInput("Block structure not initialized".into()))?;
1167 let cam_size = structure.camera_dof;
1168
1169 let mut h_cc_triplets = Vec::new();
1171 let h_cc_symbolic = h_cc.symbolic();
1172 for col in 0..h_cc.ncols() {
1173 let row_indices = h_cc_symbolic.row_idx_of_col_raw(col);
1174 let col_values = h_cc.val_of_col(col);
1175 for (idx, &row) in row_indices.iter().enumerate() {
1176 h_cc_triplets.push(Triplet::new(row, col, col_values[idx]));
1177 }
1178 }
1179 for i in 0..cam_size {
1180 if let Some(entry) = h_cc_triplets.iter_mut().find(|t| t.row == i && t.col == i) {
1181 *entry = Triplet::new(i, i, entry.val + lambda);
1182 } else {
1183 h_cc_triplets.push(Triplet::new(i, i, lambda));
1184 }
1185 }
1186 let h_cc_damped =
1187 SparseColMat::try_new_from_triplets(cam_size, cam_size, &h_cc_triplets)
1188 .map_err(|e| LinAlgError::SparseMatrixCreation(format!("Damped H_cc: {:?}", e)))?;
1189
1190 for block in &mut hpp_blocks {
1192 block[(0, 0)] += lambda;
1193 block[(1, 1)] += lambda;
1194 block[(2, 2)] += lambda;
1195 }
1196
1197 let hpp_inv_blocks = Self::invert_landmark_blocks(&hpp_blocks)?;
1199
1200 let s = self.compute_schur_complement(&h_cc_damped, &h_cp, &hpp_inv_blocks)?;
1202
1203 let g_reduced = self.compute_reduced_gradient(&g_c, &g_p, &h_cp, &hpp_inv_blocks)?;
1205
1206 let delta_c = match self.variant {
1208 SchurVariant::Iterative => self.solve_with_pcg(&s, &g_reduced)?,
1209 _ => self.solve_with_cholesky(&s, &g_reduced)?,
1210 };
1211
1212 let delta_p = self.back_substitute(&delta_c, &g_p, &h_cp, &hpp_inv_blocks)?;
1214
1215 self.combine_updates(&delta_c, &delta_p)
1217 }
1218
1219 fn get_hessian(&self) -> Option<&SparseColMat<usize, f64>> {
1220 self.hessian.as_ref()
1221 }
1222
1223 fn get_gradient(&self) -> Option<&Mat<f64>> {
1224 self.gradient.as_ref()
1225 }
1226}
1227
1228impl SparseSchurComplementSolver {
1230 fn combine_updates(&self, delta_c: &Mat<f64>, delta_p: &Mat<f64>) -> LinAlgResult<Mat<f64>> {
1232 let structure = self
1233 .block_structure
1234 .as_ref()
1235 .ok_or_else(|| LinAlgError::InvalidInput("Block structure not built".to_string()))?;
1236
1237 let total_dof = structure.camera_dof + structure.landmark_dof;
1238 let mut delta = Mat::zeros(total_dof, 1);
1239
1240 let (cam_start, cam_end) = structure.camera_col_range();
1241 let (land_start, land_end) = structure.landmark_col_range();
1242
1243 for i in 0..(cam_end - cam_start) {
1245 delta[(cam_start + i, 0)] = delta_c[(i, 0)];
1246 }
1247
1248 for i in 0..(land_end - land_start) {
1250 delta[(land_start + i, 0)] = delta_p[(i, 0)];
1251 }
1252
1253 debug!(
1255 "Update norms: delta_c={:.6e}, delta_p={:.6e}, combined={:.6e}",
1256 delta_c.norm_l2(),
1257 delta_p.norm_l2(),
1258 delta.norm_l2()
1259 );
1260
1261 Ok(delta)
1262 }
1263}
1264
1265#[cfg(test)]
1266mod tests {
1267 use super::*;
1268 use crate::core::VarKey;
1269 use crate::core::variable::Variable;
1270 use apex_manifolds::{LieGroup, rn, se3};
1271 use nalgebra::DVector;
1272 use slotmap::{SecondaryMap, SlotMap};
1273
1274 type TestResult = Result<(), Box<dyn std::error::Error>>;
1275
1276 type TestSetup = (
1278 SlotMap<VarKey, Box<dyn ManifoldVariable>>,
1279 SecondaryMap<VarKey, usize>,
1280 SparseColMat<usize, f64>,
1281 Mat<f64>,
1282 std::collections::HashSet<VarKey>,
1283 );
1284
1285 fn create_schur_test_setup() -> Result<TestSetup, Box<dyn std::error::Error>> {
1291 let se3_id = DVector::from_vec(vec![1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]);
1292 let pt_zero = DVector::from_vec(vec![0.0, 0.0, 0.0]);
1293
1294 let mut variables: SlotMap<VarKey, Box<dyn ManifoldVariable>> = SlotMap::with_key();
1295 let cam0 = variables.insert(Box::new(Variable::new(se3::SE3::from_param_slice(
1296 se3_id.as_slice(),
1297 ))));
1298 let cam1 = variables.insert(Box::new(Variable::new(se3::SE3::from_param_slice(
1299 se3_id.as_slice(),
1300 ))));
1301 let pt0 = variables.insert(Box::new(Variable::new(rn::Rn::new(pt_zero.clone()))));
1302 let pt1 = variables.insert(Box::new(Variable::new(rn::Rn::new(pt_zero.clone()))));
1303 let pt2 = variables.insert(Box::new(Variable::new(rn::Rn::new(pt_zero.clone()))));
1304
1305 let mut variable_index_map: SecondaryMap<VarKey, usize> = SecondaryMap::new();
1307 variable_index_map.insert(cam0, 0);
1308 variable_index_map.insert(cam1, 6);
1309 variable_index_map.insert(pt0, 12);
1310 variable_index_map.insert(pt1, 15);
1311 variable_index_map.insert(pt2, 18);
1312
1313 let n_rows = 36;
1318 let n_cols = 21;
1319 let cam_cols = [0usize, 6];
1320 let lm_cols = [12usize, 15, 18];
1321
1322 let mut triplets: Vec<Triplet<usize, usize, f64>> = Vec::new();
1323 for (ci, &cam_col) in cam_cols.iter().enumerate() {
1324 for (li, &lm_col) in lm_cols.iter().enumerate() {
1325 let row_base = (ci * 3 + li) * 6;
1326 for k in 0..6 {
1327 triplets.push(Triplet::new(row_base + k, cam_col + k, 1.0));
1328 triplets.push(Triplet::new(row_base + k, lm_col + (k % 3), 1.0));
1329 }
1330 }
1331 }
1332
1333 let jacobian = SparseColMat::try_new_from_triplets(n_rows, n_cols, &triplets)?;
1334 let residuals = Mat::from_fn(n_rows, 1, |i, _| (i % 5) as f64 * 0.1);
1335
1336 let mut landmark_keys = std::collections::HashSet::new();
1337 landmark_keys.insert(pt0);
1338 landmark_keys.insert(pt1);
1339 landmark_keys.insert(pt2);
1340
1341 Ok((
1342 variables,
1343 variable_index_map,
1344 jacobian,
1345 residuals,
1346 landmark_keys,
1347 ))
1348 }
1349
1350 #[test]
1351 fn test_schur_ordering_rn3_eliminated() {
1352 let ordering = SchurOrdering::default();
1353 assert!(ordering.should_eliminate(&ManifoldType::RN, 3));
1355 }
1356
1357 #[test]
1358 fn test_schur_ordering_se3_not_eliminated() {
1359 let ordering = SchurOrdering::default();
1360 assert!(!ordering.should_eliminate(&ManifoldType::SE3, 6));
1362 }
1363
1364 #[test]
1365 fn test_schur_ordering_wrong_type_not_eliminated() {
1366 let ordering = SchurOrdering::default();
1367 assert!(!ordering.should_eliminate(&ManifoldType::SE3, 6));
1369 }
1370
1371 #[test]
1372 fn test_schur_ordering_wrong_size_not_eliminated() {
1373 let ordering = SchurOrdering::default();
1374 assert!(!ordering.should_eliminate(&ManifoldType::RN, 6));
1376 assert!(!ordering.should_eliminate(&ManifoldType::RN, 2));
1377 }
1378
1379 #[test]
1380 fn test_block_structure_creation() {
1381 let structure = SchurBlockStructure::new();
1382 assert_eq!(structure.camera_dof, 0);
1383 assert_eq!(structure.landmark_dof, 0);
1384 }
1385
1386 #[test]
1387 fn test_solver_creation() {
1388 let solver = SparseSchurComplementSolver::new();
1389 assert!(solver.block_structure.is_none());
1390 }
1391
1392 #[test]
1393 fn test_3x3_block_inversion() -> Result<(), LinAlgError> {
1394 let block = Matrix3::new(2.0, 0.0, 0.0, 0.0, 3.0, 0.0, 0.0, 0.0, 4.0);
1395 let inv = SparseSchurComplementSolver::invert_landmark_blocks(&[block])?;
1396 assert!((inv[0][(0, 0)] - 0.5).abs() < 1e-10);
1397 Ok(())
1398 }
1399
1400 #[test]
1401 fn test_schur_variants() {
1402 let solver = SparseSchurComplementSolver::new()
1403 .with_variant(SchurVariant::Iterative)
1404 .with_preconditioner(SchurPreconditioner::BlockDiagonal)
1405 .with_cg_params(100, 1e-8);
1406
1407 assert_eq!(solver.cg_max_iterations, 100);
1408 assert!((solver.cg_tolerance - 1e-8).abs() < 1e-12);
1409 }
1410
1411 #[test]
1412 fn test_compute_schur_complement_known_matrix() -> Result<(), LinAlgError> {
1413 use faer::sparse::Triplet;
1414
1415 let solver = SparseSchurComplementSolver::new();
1416
1417 let h_cc_triplets = vec![Triplet::new(0, 0, 4.0), Triplet::new(1, 1, 5.0)];
1419 let h_cc = SparseColMat::try_new_from_triplets(2, 2, &h_cc_triplets)
1420 .map_err(|e| LinAlgError::SparseMatrixCreation(format!("{e:?}")))?;
1421
1422 let h_cp_triplets = vec![Triplet::new(0, 0, 1.0), Triplet::new(1, 1, 2.0)];
1424 let h_cp = SparseColMat::try_new_from_triplets(2, 3, &h_cp_triplets)
1425 .map_err(|e| LinAlgError::SparseMatrixCreation(format!("{e:?}")))?;
1426
1427 let hpp_inv = vec![Matrix3::new(0.5, 0.0, 0.0, 0.0, 0.5, 0.0, 0.0, 0.0, 0.5)];
1429
1430 let s = solver.compute_schur_complement(&h_cc, &h_cp, &hpp_inv)?;
1441
1442 assert_eq!(s.nrows(), 2);
1443 assert_eq!(s.ncols(), 2);
1444 assert!((s[(0, 0)] - 3.5).abs() < 1e-10, "S(0,0) = {}", s[(0, 0)]);
1452 assert!((s[(1, 1)] - 3.0).abs() < 1e-10, "S(1,1) = {}", s[(1, 1)]);
1453 Ok(())
1454 }
1455
1456 #[test]
1457 fn test_back_substitute() -> Result<(), LinAlgError> {
1458 use faer::sparse::Triplet;
1459
1460 let solver = SparseSchurComplementSolver::new();
1461
1462 let delta_c = Mat::from_fn(2, 1, |i, _| (i + 1) as f64); let g_p = Mat::from_fn(3, 1, |i, _| (i + 1) as f64); let h_cp_triplets = vec![Triplet::new(0, 0, 1.0), Triplet::new(1, 1, 1.0)];
1468 let h_cp = SparseColMat::try_new_from_triplets(2, 3, &h_cp_triplets)
1469 .map_err(|e| LinAlgError::SparseMatrixCreation(format!("{e:?}")))?;
1470
1471 let hpp_inv = vec![Matrix3::identity()];
1473
1474 let delta_p = solver.back_substitute(&delta_c, &g_p, &h_cp, &hpp_inv)?;
1479
1480 assert_eq!(delta_p.nrows(), 3);
1481 assert!((delta_p[(0, 0)]).abs() < 1e-10);
1482 assert!((delta_p[(1, 0)]).abs() < 1e-10);
1483 assert!((delta_p[(2, 0)] - 3.0).abs() < 1e-10);
1484 Ok(())
1485 }
1486
1487 #[test]
1488 fn test_compute_reduced_gradient() -> Result<(), LinAlgError> {
1489 use faer::sparse::Triplet;
1490
1491 let solver = SparseSchurComplementSolver::new();
1492
1493 let g_c = Mat::from_fn(2, 1, |i, _| (i + 1) as f64); let g_p = Mat::from_fn(3, 1, |i, _| (i + 1) as f64); let h_cp_triplets = vec![Triplet::new(0, 0, 1.0), Triplet::new(1, 1, 1.0)];
1499 let h_cp = SparseColMat::try_new_from_triplets(2, 3, &h_cp_triplets)
1500 .map_err(|e| LinAlgError::SparseMatrixCreation(format!("{e:?}")))?;
1501
1502 let hpp_inv = vec![Matrix3::new(2.0, 0.0, 0.0, 0.0, 2.0, 0.0, 0.0, 0.0, 2.0)];
1504
1505 let g_reduced = solver.compute_reduced_gradient(&g_c, &g_p, &h_cp, &hpp_inv)?;
1510
1511 assert_eq!(g_reduced.nrows(), 2);
1512 assert!((g_reduced[(0, 0)] + 1.0).abs() < 1e-10);
1513 assert!((g_reduced[(1, 0)] + 2.0).abs() < 1e-10);
1514 Ok(())
1515 }
1516
1517 #[test]
1523 fn test_solver_default() {
1524 let solver = SparseSchurComplementSolver::default();
1525 assert!(solver.block_structure.is_none());
1526 assert!(solver.hessian.is_none());
1527 assert!(solver.gradient.is_none());
1528 }
1529
1530 #[test]
1532 fn test_block_structure_default() {
1533 let s = SchurBlockStructure::default();
1534 assert!(s.camera_blocks.is_empty());
1535 assert!(s.landmark_blocks.is_empty());
1536 assert_eq!(s.camera_dof, 0);
1537 assert_eq!(s.landmark_dof, 0);
1538 }
1539
1540 #[test]
1542 fn test_block_structure_col_ranges() {
1543 let mut s = SchurBlockStructure::new();
1544 assert_eq!(s.camera_col_range(), (0, 0));
1546 assert_eq!(s.landmark_col_range(), (0, 0));
1547
1548 let mut tmp: SlotMap<VarKey, ()> = SlotMap::with_key();
1550 let cam_key = tmp.insert(());
1551 let pt_key = tmp.insert(());
1552 s.camera_blocks.push((cam_key, 0, 6));
1553 s.camera_dof = 6;
1554 s.landmark_blocks.push((pt_key, 6, 3));
1555 s.landmark_dof = 3;
1556
1557 assert_eq!(s.camera_col_range(), (0, 6));
1558 assert_eq!(s.landmark_col_range(), (6, 9));
1559 }
1560
1561 #[test]
1563 fn test_block_structure_getter() -> TestResult {
1564 let (variables, variable_index_map, _, _, landmark_keys) = create_schur_test_setup()?;
1565 let mut solver = SparseSchurComplementSolver::new();
1566
1567 assert!(solver.block_structure().is_none());
1568 solver.initialize_structure(&variables, &variable_index_map, &landmark_keys)?;
1569 assert!(solver.block_structure().is_some());
1570 Ok(())
1571 }
1572
1573 #[test]
1575 fn test_with_ordering_builder() {
1576 let ordering = SchurOrdering {
1577 eliminate_types: vec![ManifoldType::RN],
1578 eliminate_rn_size: Some(3),
1579 };
1580 let solver = SparseSchurComplementSolver::new().with_ordering(ordering);
1581 assert_eq!(solver.ordering.eliminate_rn_size, Some(3));
1582 }
1583
1584 #[test]
1589 fn test_invert_landmark_blocks_with_lambda() -> TestResult {
1590 let block = Matrix3::new(2.0, 0.0, 0.0, 0.0, 3.0, 0.0, 0.0, 0.0, 4.0);
1592
1593 let inv = SparseSchurComplementSolver::invert_landmark_blocks_with_lambda(&[block], 0.0)?;
1595 assert_eq!(inv.len(), 1);
1596 assert!((inv[0][(0, 0)] - 0.5).abs() < 1e-10);
1597 assert!((inv[0][(1, 1)] - 1.0 / 3.0).abs() < 1e-10);
1598 assert!((inv[0][(2, 2)] - 0.25).abs() < 1e-10);
1599
1600 let inv_lam =
1602 SparseSchurComplementSolver::invert_landmark_blocks_with_lambda(&[block], 1.0)?;
1603 assert!((inv_lam[0][(0, 0)] - 0.5).abs() < 1e-10);
1604 Ok(())
1605 }
1606
1607 #[test]
1609 fn test_explicit_schur_initialize_structure() -> TestResult {
1610 let (variables, variable_index_map, _, _, landmark_keys) = create_schur_test_setup()?;
1611 let mut solver = SparseSchurComplementSolver::new();
1612 solver.initialize_structure(&variables, &variable_index_map, &landmark_keys)?;
1613
1614 let bs = solver.block_structure().ok_or("block_structure is None")?;
1615 assert_eq!(bs.camera_blocks.len(), 2);
1616 assert_eq!(bs.landmark_blocks.len(), 3);
1617 assert_eq!(bs.camera_dof, 12); assert_eq!(bs.landmark_dof, 9); Ok(())
1620 }
1621
1622 #[test]
1624 fn test_extract_gradient_blocks() -> TestResult {
1625 let (variables, variable_index_map, _, _, landmark_keys) = create_schur_test_setup()?;
1626 let mut solver = SparseSchurComplementSolver::new();
1627 solver.initialize_structure(&variables, &variable_index_map, &landmark_keys)?;
1628
1629 let gradient = Mat::from_fn(21, 1, |i, _| i as f64);
1631 let (g_c, g_p) = solver.extract_gradient_blocks(&gradient)?;
1632
1633 assert_eq!(g_c.nrows(), 12); assert_eq!(g_p.nrows(), 9); Ok(())
1636 }
1637
1638 #[test]
1640 fn test_explicit_schur_solve_normal_equation() -> TestResult {
1641 let (variables, variable_index_map, jacobian, residuals, landmark_keys) =
1642 create_schur_test_setup()?;
1643 let mut solver = SparseSchurComplementSolver::new().with_variant(SchurVariant::Sparse);
1644 solver.initialize_structure(&variables, &variable_index_map, &landmark_keys)?;
1645
1646 let delta =
1647 LinearSolver::<SparseMode>::solve_normal_equation(&mut solver, &residuals, &jacobian)?;
1648 assert_eq!(delta.nrows(), 21);
1649 assert_eq!(delta.ncols(), 1);
1650 Ok(())
1651 }
1652
1653 #[test]
1655 fn test_explicit_schur_solve_augmented_equation() -> TestResult {
1656 let (variables, variable_index_map, jacobian, residuals, landmark_keys) =
1657 create_schur_test_setup()?;
1658 let mut solver = SparseSchurComplementSolver::new().with_variant(SchurVariant::Sparse);
1659 solver.initialize_structure(&variables, &variable_index_map, &landmark_keys)?;
1660
1661 let delta = LinearSolver::<SparseMode>::solve_augmented_equation(
1662 &mut solver,
1663 &residuals,
1664 &jacobian,
1665 0.1,
1666 )?;
1667 assert_eq!(delta.nrows(), 21);
1668 Ok(())
1669 }
1670
1671 #[test]
1673 fn test_explicit_schur_solve_iterative_variant() -> TestResult {
1674 let (variables, variable_index_map, jacobian, residuals, landmark_keys) =
1675 create_schur_test_setup()?;
1676 let mut solver = SparseSchurComplementSolver::new()
1677 .with_variant(SchurVariant::Iterative)
1678 .with_cg_params(200, 1e-6);
1679 solver.initialize_structure(&variables, &variable_index_map, &landmark_keys)?;
1680
1681 let delta =
1682 LinearSolver::<SparseMode>::solve_normal_equation(&mut solver, &residuals, &jacobian)?;
1683 assert_eq!(delta.nrows(), 21);
1684 Ok(())
1685 }
1686
1687 #[test]
1689 fn test_explicit_schur_get_hessian_gradient() -> TestResult {
1690 let (variables, variable_index_map, jacobian, residuals, landmark_keys) =
1691 create_schur_test_setup()?;
1692 let mut solver = SparseSchurComplementSolver::new();
1693 solver.initialize_structure(&variables, &variable_index_map, &landmark_keys)?;
1694
1695 assert!(LinearSolver::<SparseMode>::get_hessian(&solver).is_none());
1696 assert!(LinearSolver::<SparseMode>::get_gradient(&solver).is_none());
1697
1698 LinearSolver::<SparseMode>::solve_normal_equation(&mut solver, &residuals, &jacobian)?;
1699
1700 let h = LinearSolver::<SparseMode>::get_hessian(&solver);
1701 let g = LinearSolver::<SparseMode>::get_gradient(&solver);
1702 assert!(h.is_some());
1703 assert!(g.is_some());
1704 let h = h.ok_or("hessian is None")?;
1705 let g = g.ok_or("gradient is None")?;
1706 assert_eq!(h.nrows(), 21);
1707 assert_eq!(g.nrows(), 21);
1708 Ok(())
1709 }
1710
1711 #[test]
1713 fn test_explicit_schur_augmented_lambda_effect() -> TestResult {
1714 let (variables, variable_index_map, jacobian, residuals, landmark_keys) =
1715 create_schur_test_setup()?;
1716
1717 let mut solver1 = SparseSchurComplementSolver::new();
1718 solver1.initialize_structure(&variables, &variable_index_map, &landmark_keys)?;
1719 let delta1 = LinearSolver::<SparseMode>::solve_augmented_equation(
1720 &mut solver1,
1721 &residuals,
1722 &jacobian,
1723 0.001,
1724 )?;
1725
1726 let mut solver2 = SparseSchurComplementSolver::new();
1727 solver2.initialize_structure(&variables, &variable_index_map, &landmark_keys)?;
1728 let delta2 = LinearSolver::<SparseMode>::solve_augmented_equation(
1729 &mut solver2,
1730 &residuals,
1731 &jacobian,
1732 100.0,
1733 )?;
1734
1735 let norm_diff: f64 = (0..21)
1737 .map(|i| (delta1[(i, 0)] - delta2[(i, 0)]).powi(2))
1738 .sum();
1739 assert!(
1740 norm_diff > 1e-10,
1741 "Different λ should yield different updates"
1742 );
1743 Ok(())
1744 }
1745
1746 #[test]
1748 fn test_combine_updates() -> TestResult {
1749 let (variables, variable_index_map, _, _, landmark_keys) = create_schur_test_setup()?;
1750 let mut solver = SparseSchurComplementSolver::new();
1751 solver.initialize_structure(&variables, &variable_index_map, &landmark_keys)?;
1752
1753 let delta_c = Mat::from_fn(12, 1, |_, _| 1.0);
1755 let delta_p = Mat::from_fn(9, 1, |_, _| 2.0);
1756
1757 let combined = solver.combine_updates(&delta_c, &delta_p)?;
1758 assert_eq!(combined.nrows(), 21);
1759
1760 for i in 0..12 {
1762 assert!((combined[(i, 0)] - 1.0).abs() < 1e-10);
1763 }
1764 for i in 12..21 {
1766 assert!((combined[(i, 0)] - 2.0).abs() < 1e-10);
1767 }
1768 Ok(())
1769 }
1770
1771 #[test]
1773 fn test_explicit_schur_solve_without_init_returns_error() -> TestResult {
1774 let triplets: Vec<Triplet<usize, usize, f64>> = vec![Triplet::new(0, 0, 1.0)];
1775 let jacobian =
1776 SparseColMat::try_new_from_triplets(1, 1, &triplets).map_err(|e| format!("{e:?}"))?;
1777 let residuals = Mat::from_fn(1, 1, |_, _| 1.0);
1778 let mut solver = SparseSchurComplementSolver::new();
1779
1780 let result =
1781 LinearSolver::<SparseMode>::solve_normal_equation(&mut solver, &residuals, &jacobian);
1782 assert!(result.is_err());
1783 Ok(())
1784 }
1785
1786 #[test]
1792 fn test_schur_ordering_new_equals_default() {
1793 let a = SchurOrdering::new();
1794 let b = SchurOrdering::default();
1795 assert_eq!(a.eliminate_rn_size, b.eliminate_rn_size);
1796 assert_eq!(a.eliminate_types.len(), b.eliminate_types.len());
1797 }
1798
1799 #[test]
1801 fn test_extract_camera_block() -> TestResult {
1802 let (variables, variable_index_map, jacobian, residuals, landmark_keys) =
1803 create_schur_test_setup()?;
1804 let mut solver = SparseSchurComplementSolver::new();
1805 solver.initialize_structure(&variables, &variable_index_map, &landmark_keys)?;
1806
1807 LinearSolver::<SparseMode>::solve_normal_equation(&mut solver, &residuals, &jacobian)?;
1809 let hessian = solver.hessian.clone().ok_or("hessian is None")?;
1810
1811 let mut fresh = SparseSchurComplementSolver::new();
1812 fresh.initialize_structure(&variables, &variable_index_map, &landmark_keys)?;
1813 let h_cc = fresh.extract_camera_block(&hessian)?;
1814
1815 assert_eq!(h_cc.nrows(), 12);
1817 assert_eq!(h_cc.ncols(), 12);
1818 Ok(())
1819 }
1820
1821 #[test]
1823 fn test_extract_coupling_block() -> TestResult {
1824 let (variables, variable_index_map, jacobian, residuals, landmark_keys) =
1825 create_schur_test_setup()?;
1826 let mut solver = SparseSchurComplementSolver::new();
1827 solver.initialize_structure(&variables, &variable_index_map, &landmark_keys)?;
1828
1829 LinearSolver::<SparseMode>::solve_normal_equation(&mut solver, &residuals, &jacobian)?;
1830 let hessian = solver.hessian.clone().ok_or("hessian is None")?;
1831
1832 let mut fresh = SparseSchurComplementSolver::new();
1833 fresh.initialize_structure(&variables, &variable_index_map, &landmark_keys)?;
1834 let h_cp = fresh.extract_coupling_block(&hessian)?;
1835
1836 assert_eq!(h_cp.nrows(), 12);
1838 assert_eq!(h_cp.ncols(), 9);
1839 Ok(())
1840 }
1841
1842 #[test]
1844 fn test_extract_landmark_blocks() -> TestResult {
1845 let (variables, variable_index_map, jacobian, residuals, landmark_keys) =
1846 create_schur_test_setup()?;
1847 let mut solver = SparseSchurComplementSolver::new();
1848 solver.initialize_structure(&variables, &variable_index_map, &landmark_keys)?;
1849
1850 LinearSolver::<SparseMode>::solve_normal_equation(&mut solver, &residuals, &jacobian)?;
1851 let hessian = solver.hessian.clone().ok_or("hessian is None")?;
1852
1853 let mut fresh = SparseSchurComplementSolver::new();
1854 fresh.initialize_structure(&variables, &variable_index_map, &landmark_keys)?;
1855 let blocks = fresh.extract_landmark_blocks(&hessian)?;
1856
1857 assert_eq!(blocks.len(), 3);
1859 Ok(())
1860 }
1861
1862 #[test]
1864 fn test_solve_with_cholesky_small_spd() -> TestResult {
1865 let solver = SparseSchurComplementSolver::new();
1866
1867 let triplets = vec![
1869 Triplet::new(0usize, 0usize, 4.0f64),
1870 Triplet::new(1usize, 0usize, 1.0f64),
1871 Triplet::new(0usize, 1usize, 1.0f64),
1872 Triplet::new(1usize, 1usize, 3.0f64),
1873 ];
1874 let a =
1875 SparseColMat::try_new_from_triplets(2, 2, &triplets).map_err(|e| format!("{e:?}"))?;
1876 let b = Mat::from_fn(2, 1, |i, _| (i + 1) as f64); let x = solver.solve_with_cholesky(&a, &b)?;
1879 assert_eq!(x.nrows(), 2);
1880
1881 let ax0 = 4.0 * x[(0, 0)] + 1.0 * x[(1, 0)];
1884 let ax1 = 1.0 * x[(0, 0)] + 3.0 * x[(1, 0)];
1885 assert!((ax0 - 1.0).abs() < 1e-8, "A·x[0] = {ax0}");
1886 assert!((ax1 - 2.0).abs() < 1e-8, "A·x[1] = {ax1}");
1887 Ok(())
1888 }
1889
1890 #[test]
1892 fn test_solve_with_pcg_diagonal_system() -> TestResult {
1893 let solver = SparseSchurComplementSolver::new();
1894
1895 let triplets = vec![
1897 Triplet::new(0usize, 0usize, 2.0f64),
1898 Triplet::new(1usize, 1usize, 3.0f64),
1899 ];
1900 let a =
1901 SparseColMat::try_new_from_triplets(2, 2, &triplets).map_err(|e| format!("{e:?}"))?;
1902 let b = Mat::from_fn(2, 1, |i, _| (i + 1) as f64); let x = solver.solve_with_pcg(&a, &b)?;
1905 assert!((x[(0, 0)] - 0.5).abs() < 1e-6, "x[0] = {}", x[(0, 0)]);
1907 assert!((x[(1, 0)] - 2.0 / 3.0).abs() < 1e-6, "x[1] = {}", x[(1, 0)]);
1908 Ok(())
1909 }
1910
1911 #[test]
1913 fn test_initialize_structure_no_cameras_returns_error() {
1914 use crate::core::variable::Variable;
1915 use apex_manifolds::rn;
1916 use nalgebra::DVector;
1917
1918 let mut variables: SlotMap<VarKey, Box<dyn ManifoldVariable>> = SlotMap::with_key();
1919 let k = variables.insert(Box::new(Variable::new(rn::Rn::new(DVector::zeros(3)))));
1920 let mut variable_index_map: SecondaryMap<VarKey, usize> = SecondaryMap::new();
1921 variable_index_map.insert(k, 0);
1922 let mut landmark_keys = std::collections::HashSet::new();
1923 landmark_keys.insert(k);
1924
1925 let mut solver = SparseSchurComplementSolver::new();
1926 let result = solver.initialize_structure(&variables, &variable_index_map, &landmark_keys);
1927 assert!(
1928 result.is_err(),
1929 "Expected Err when no camera variables present"
1930 );
1931 }
1932
1933 #[test]
1935 fn test_initialize_structure_no_landmarks_returns_error() {
1936 use crate::core::variable::Variable;
1937 use apex_manifolds::se3;
1938
1939 let mut variables: SlotMap<VarKey, Box<dyn ManifoldVariable>> = SlotMap::with_key();
1940 let _k = variables.insert(Box::new(Variable::new(se3::SE3::from_param_slice(&[
1941 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
1942 ]))));
1943 let mut variable_index_map: SecondaryMap<VarKey, usize> = SecondaryMap::new();
1944 variable_index_map.insert(_k, 0);
1945 let landmark_keys = std::collections::HashSet::<VarKey>::new(); let mut solver = SparseSchurComplementSolver::new();
1948 let result = solver.initialize_structure(&variables, &variable_index_map, &landmark_keys);
1949 assert!(
1950 result.is_err(),
1951 "Expected Err when no landmark variables present"
1952 );
1953 }
1954}