1use crate::core::VarKey;
50use crate::core::variable::ManifoldVariable;
51use crate::error::ErrorLogging;
52use crate::linalg::regularization::invert_with_retry_dyn;
53use crate::linalg::schur::{BlockSpan, EliminatedBlocks, SchurOrdering, SchurPartition};
54use crate::linalg::schur::{PcgParams, SchurPreconditioner, solve_pcg};
55use crate::linalg::sparse::normal_eq::{LazyNormalEquations, NormalEquations};
56use crate::linalg::sparse::pattern;
57use crate::linalg::{Damping, LinAlgError, LinAlgResult, LinearSolver, SparseMode, StructureAware};
58use faer::sparse::{SparseColMat, SymbolicSparseColMat, Triplet};
59use faer::{
60 Accum, Mat, Side,
61 linalg::solvers::Solve,
62 sparse::linalg::solvers::{Llt, SymbolicLlt},
63};
64use nalgebra::DMatrix;
65use rayon::prelude::*;
66use slotmap::{SecondaryMap, SlotMap};
67use tracing::debug;
68
69#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
72pub enum ExplicitSchurVariant {
73 #[default]
79 Sparse,
80 Iterative,
93 Chunked,
116}
117
118#[derive(Debug, Clone)]
121pub struct ExplicitSparseSchur {
122 partition: Option<SchurPartition>,
123 eliminated: EliminatedBlocks,
125 chunked: Option<super::chunk_eliminator::ChunkedSchurEliminator>,
127 chunked_jacobian: Option<SparseColMat<usize, f64>>,
134 verified_pattern: Option<pattern::PatternFingerprint>,
142 ordering: SchurOrdering,
143 variant: ExplicitSchurVariant,
144 preconditioner: SchurPreconditioner,
145
146 cg_q_tolerance: f64,
149 cg_max_iterations: usize,
150 cg_tolerance: f64,
151
152 ne_cache: LazyNormalEquations,
154
155 extraction: Option<ExtractionCache>,
158
159 s_bitmap: Option<(pattern::PatternFingerprint, Vec<u64>)>,
165
166 hessian: Option<SparseColMat<usize, f64>>,
168 gradient: Option<Mat<f64>>,
169 s_llt_cache: Option<(u64, SymbolicLlt<usize>)>,
173}
174
175#[derive(Debug, Clone)]
187struct ExtractionCache {
188 fingerprint: pattern::PatternFingerprint,
189 h_kk_symbolic: SymbolicSparseColMat<usize>,
190 h_ke_symbolic: SymbolicSparseColMat<usize>,
191 h_kk_src: Vec<u32>,
193 h_ke_src: Vec<u32>,
195}
196
197impl ExtractionCache {
198 fn build(partition: &SchurPartition, hessian: &SparseColMat<usize, f64>) -> LinAlgResult<Self> {
203 let symbolic = hessian.symbolic();
204
205 let mut kept_cols = vec![u32::MAX; hessian.ncols()];
208 for block in partition.kept_blocks() {
209 for offset in 0..block.dof {
210 let global = block.col_start + offset;
211 if let Some(local) = partition.kept_local(global) {
212 kept_cols[global] = local as u32;
213 }
214 }
215 }
216 let mut elim_cols = vec![u32::MAX; hessian.ncols()];
217 for (block_idx, block) in partition.eliminated_blocks().iter().enumerate() {
218 let base = partition.eliminated_offset(block_idx);
219 for offset in 0..block.dof {
220 elim_cols[block.col_start + offset] = (base + offset) as u32;
221 }
222 }
223
224 let mut kk_triplets: Vec<(usize, usize, u32)> = Vec::new();
228 let mut ke_triplets: Vec<(usize, usize, u32)> = Vec::new();
229 for col in 0..hessian.ncols() {
230 let col_start = symbolic.col_range(col).start;
231 let rows = symbolic.row_idx_of_col_raw(col);
232 let c_kept = kept_cols[col];
233 let c_elim = elim_cols[col];
234 for (k, &row) in rows.iter().enumerate() {
235 let src = (col_start + k) as u32;
236 if c_kept != u32::MAX {
237 if let Some(r) = partition.kept_local(row) {
238 kk_triplets.push((r, c_kept as usize, src));
239 }
240 } else if c_elim != u32::MAX
241 && let Some(r) = partition.kept_local(row)
242 {
243 ke_triplets.push((r, c_elim as usize, src));
244 }
245 }
246 }
247
248 let (h_kk_symbolic, h_kk_src) =
249 Self::symbolic_and_src(kk_triplets, partition.kept_dof(), partition.kept_dof())?;
250 let (h_ke_symbolic, h_ke_src) = Self::symbolic_and_src(
251 ke_triplets,
252 partition.kept_dof(),
253 partition.eliminated_dof(),
254 )?;
255
256 Ok(Self {
257 fingerprint: pattern::PatternFingerprint::of(hessian),
258 h_kk_symbolic,
259 h_ke_symbolic,
260 h_kk_src,
261 h_ke_src,
262 })
263 }
264
265 fn symbolic_and_src(
271 mut triplets: Vec<(usize, usize, u32)>,
272 nrows: usize,
273 ncols: usize,
274 ) -> LinAlgResult<(SymbolicSparseColMat<usize>, Vec<u32>)> {
275 triplets.sort_unstable_by_key(|&(r, c, _)| (c, r));
276
277 let nnz = triplets.len();
278 let mut col_ptr = vec![0usize; ncols + 1];
279 let mut row_idx = Vec::with_capacity(nnz);
280 let mut src = Vec::with_capacity(nnz);
281 for &(r, c, s) in &triplets {
282 col_ptr[c + 1] += 1;
283 row_idx.push(r);
284 src.push(s);
285 }
286 for c in 0..ncols {
287 col_ptr[c + 1] += col_ptr[c];
288 }
289
290 let symbolic = SymbolicSparseColMat::new_checked(nrows, ncols, col_ptr, None, row_idx);
291 Ok((symbolic, src))
292 }
293
294 fn extract(
300 &self,
301 hessian: &SparseColMat<usize, f64>,
302 ) -> (SparseColMat<usize, f64>, SparseColMat<usize, f64>) {
303 let values = hessian.as_ref().val();
304 let kk: Vec<f64> = self
305 .h_kk_src
306 .par_iter()
307 .map(|&s| values[s as usize])
308 .collect();
309 let ke: Vec<f64> = self
310 .h_ke_src
311 .par_iter()
312 .map(|&s| values[s as usize])
313 .collect();
314 (
315 SparseColMat::new(self.h_kk_symbolic.clone(), kk),
316 SparseColMat::new(self.h_ke_symbolic.clone(), ke),
317 )
318 }
319}
320
321#[inline]
324fn set_dense_bit(bits: &mut [u64], wpc: usize, row: usize, col: usize) {
325 bits[col * wpc + (row >> 6)] |= 1u64 << (row & 63);
326}
327
328fn build_schur_output(
336 kept_dof: usize,
337 wpc: usize,
338 bits: &[u64],
339 s_dense: &[f64],
340) -> (SymbolicSparseColMat<usize>, Vec<f64>, u64) {
341 const FNV_OFFSET: u64 = 0xcbf29ce484222325;
344 const FNV_PRIME: u64 = 0x100000001b3;
345 let mut pattern_hash = FNV_OFFSET;
346
347 let mut col_counts = vec![0usize; kept_dof];
349 let mut nnz = 0usize;
350 for c in 0..kept_dof {
351 let base = c * wpc;
352 for w in 0..wpc {
353 let mut word = bits[base + w];
354 while word != 0 {
355 let bit = word.trailing_zeros() as usize;
356 word &= word - 1;
357 let r = (w << 6) | bit;
358 if r < kept_dof {
359 let avg = (s_dense[r * kept_dof + c] + s_dense[c * kept_dof + r]) * 0.5;
360 if avg.abs() > 1e-12 {
361 col_counts[c] += 1;
362 nnz += 1;
363 }
364 }
365 }
366 }
367 }
368
369 let mut col_ptr = Vec::with_capacity(kept_dof + 1);
370 col_ptr.push(0usize);
371 for c in 0..kept_dof {
372 col_ptr.push(col_ptr[c] + col_counts[c]);
373 }
374
375 let mut row_idx = vec![0usize; nnz];
377 let mut s_values = vec![0.0f64; nnz];
378 let mut offsets = col_ptr[..kept_dof].to_vec();
379 for c in 0..kept_dof {
380 let base = c * wpc;
381 pattern_hash = (pattern_hash ^ (c as u64)).wrapping_mul(FNV_PRIME);
382 pattern_hash = (pattern_hash ^ (col_counts[c] as u64)).wrapping_mul(FNV_PRIME);
383 for w in 0..wpc {
384 let mut word = bits[base + w];
385 while word != 0 {
386 let bit = word.trailing_zeros() as usize;
387 word &= word - 1;
388 let r = (w << 6) | bit;
389 if r < kept_dof {
390 let avg = (s_dense[r * kept_dof + c] + s_dense[c * kept_dof + r]) * 0.5;
391 if avg.abs() > 1e-12 {
392 let slot = offsets[c];
393 offsets[c] += 1;
394 row_idx[slot] = r;
395 s_values[slot] = avg;
396 pattern_hash = (pattern_hash ^ (r as u64)).wrapping_mul(FNV_PRIME);
397 }
398 }
399 }
400 }
401 }
402
403 let symbolic = SymbolicSparseColMat::new_checked(kept_dof, kept_dof, col_ptr, None, row_idx);
404 (symbolic, s_values, pattern_hash)
405}
406
407impl ExplicitSparseSchur {
408 pub fn new() -> Self {
409 Self {
410 partition: None,
411 eliminated: EliminatedBlocks::default(),
412 chunked: None,
413 chunked_jacobian: None,
414 verified_pattern: None,
415 ordering: SchurOrdering::default(),
416 variant: ExplicitSchurVariant::default(),
417 preconditioner: SchurPreconditioner::default(),
418 cg_q_tolerance: crate::linalg::schur::DEFAULT_ETA,
419 cg_max_iterations: 200, cg_tolerance: 1e-6, ne_cache: LazyNormalEquations::default(),
422 extraction: None,
423 s_bitmap: None,
424 hessian: None,
425 gradient: None,
426 s_llt_cache: None,
427 }
428 }
429
430 pub fn with_ordering(mut self, ordering: SchurOrdering) -> Self {
431 self.ordering = ordering;
432 self
433 }
434
435 pub fn with_variant(mut self, variant: ExplicitSchurVariant) -> Self {
436 self.variant = variant;
437 self
438 }
439
440 pub fn with_preconditioner(mut self, preconditioner: SchurPreconditioner) -> Self {
441 self.preconditioner = preconditioner;
442 self
443 }
444
445 pub fn with_cg_q_tolerance(mut self, q_tolerance: f64) -> Self {
448 self.cg_q_tolerance = q_tolerance;
449 self
450 }
451
452 pub fn with_cg_params(mut self, max_iter: usize, tol: f64) -> Self {
453 self.cg_max_iterations = max_iter;
454 self.cg_tolerance = tol;
455 self
456 }
457 pub fn partition(&self) -> Option<&SchurPartition> {
459 self.partition.as_ref()
460 }
461
462 fn ensure_block_diagonal(&mut self, hessian: &SparseColMat<usize, f64>) -> LinAlgResult<()> {
468 let fingerprint = pattern::PatternFingerprint::of(hessian);
469 if self.verified_pattern == Some(fingerprint) {
470 return Ok(());
471 }
472 self.require_partition()?.verify_block_diagonal(hessian)?;
473 self.verified_pattern = Some(fingerprint);
474 Ok(())
475 }
476
477 fn require_partition(&self) -> LinAlgResult<&SchurPartition> {
479 self.partition.as_ref().ok_or_else(|| {
480 LinAlgError::InvalidInput(
481 "Block structure not built. Call initialize_structure() first.".to_string(),
482 )
483 .log()
484 })
485 }
486
487 fn build_partition(
493 &mut self,
494 variables: &SlotMap<VarKey, Box<dyn ManifoldVariable>>,
495 variable_index_map: &SecondaryMap<VarKey, usize>,
496 eliminate_keys: &std::collections::HashSet<VarKey>,
497 ) -> LinAlgResult<()> {
498 let mut kept = Vec::new();
499 let mut eliminated = Vec::new();
500
501 for (key, variable) in variables {
502 let col_start = *variable_index_map.get(key).ok_or_else(|| {
503 LinAlgError::InvalidInput(format!("VarKey {:?} not found in index map", key)).log()
504 })?;
505 let span = BlockSpan {
506 key,
507 col_start,
508 dof: variable.free_dof(),
511 };
512 if eliminate_keys.contains(&key) {
513 eliminated.push(span);
514 } else {
515 kept.push(span);
516 }
517 }
518
519 let partition = SchurPartition::new(kept, eliminated)?;
520 self.eliminated = EliminatedBlocks::new(&partition);
521 self.partition = Some(partition);
522 self.verified_pattern = None;
523 Ok(())
524 }
525
526 fn extract_kept_and_coupling(
535 &mut self,
536 hessian: &SparseColMat<usize, f64>,
537 ) -> LinAlgResult<(SparseColMat<usize, f64>, SparseColMat<usize, f64>)> {
538 let fingerprint = pattern::PatternFingerprint::of(hessian);
539 let cache = match self.extraction.take() {
540 Some(cache) if cache.fingerprint == fingerprint => cache,
541 _ => ExtractionCache::build(self.require_partition()?, hessian)?,
542 };
543 let out = cache.extract(hessian);
544 self.extraction = Some(cache);
545 Ok(out)
546 }
547
548 #[cfg_attr(not(test), allow(dead_code))]
557 fn extract_kept_block(
558 &self,
559 hessian: &SparseColMat<usize, f64>,
560 ) -> LinAlgResult<SparseColMat<usize, f64>> {
561 let partition = self.require_partition()?;
562 let kept_dof = partition.kept_dof();
563 let symbolic = hessian.symbolic();
564
565 let mut triplets = Vec::new();
566 for block in partition.kept_blocks() {
567 for offset in 0..block.dof {
568 let global_col = block.col_start + offset;
569 let Some(local_col) = partition.kept_local(global_col) else {
570 continue;
571 };
572 let rows = symbolic.row_idx_of_col_raw(global_col);
573 let vals = hessian.val_of_col(global_col);
574 for (idx, &global_row) in rows.iter().enumerate() {
575 if let Some(local_row) = partition.kept_local(global_row) {
576 triplets.push(Triplet::new(local_row, local_col, vals[idx]));
577 }
578 }
579 }
580 }
581
582 SparseColMat::try_new_from_triplets(kept_dof, kept_dof, &triplets)
583 .map_err(|e| LinAlgError::SparseMatrixCreation(format!("H_kk: {:?}", e)))
584 }
585 #[cfg_attr(not(test), allow(dead_code))]
593 fn extract_coupling_block(
594 &self,
595 hessian: &SparseColMat<usize, f64>,
596 ) -> LinAlgResult<SparseColMat<usize, f64>> {
597 let partition = self.require_partition()?;
598 let symbolic = hessian.symbolic();
599
600 let mut triplets = Vec::new();
601 for (block_idx, block) in partition.eliminated_blocks().iter().enumerate() {
602 let col_base = partition.eliminated_offset(block_idx);
603 for offset in 0..block.dof {
604 let global_col = block.col_start + offset;
605 let rows = symbolic.row_idx_of_col_raw(global_col);
606 let vals = hessian.val_of_col(global_col);
607 for (idx, &global_row) in rows.iter().enumerate() {
608 if let Some(local_row) = partition.kept_local(global_row) {
609 triplets.push(Triplet::new(local_row, col_base + offset, vals[idx]));
610 }
611 }
612 }
613 }
614
615 SparseColMat::try_new_from_triplets(
616 partition.kept_dof(),
617 partition.eliminated_dof(),
618 &triplets,
619 )
620 .map_err(|e| LinAlgError::SparseMatrixCreation(format!("H_ke: {:?}", e)))
621 }
622 fn extract_gradient_blocks(&self, gradient: &Mat<f64>) -> LinAlgResult<(Mat<f64>, Mat<f64>)> {
624 let partition = self.require_partition()?;
625
626 let mut g_k = Mat::zeros(partition.kept_dof(), 1);
627 let mut g_e = Mat::zeros(partition.eliminated_dof(), 1);
628
629 for block in partition.kept_blocks() {
630 for offset in 0..block.dof {
631 let global = block.col_start + offset;
632 if let Some(local) = partition.kept_local(global) {
633 g_k[(local, 0)] = gradient[(global, 0)];
634 }
635 }
636 }
637 for (block_idx, block) in partition.eliminated_blocks().iter().enumerate() {
638 let base = partition.eliminated_offset(block_idx);
639 for offset in 0..block.dof {
640 g_e[(base + offset, 0)] = gradient[(block.col_start + offset, 0)];
641 }
642 }
643
644 Ok((g_k, g_e))
645 }
646
647 fn solve_with_cholesky(
652 &mut self,
653 a: &SparseColMat<usize, f64>,
654 b: &Mat<f64>,
655 pattern_hash: Option<u64>,
656 ) -> LinAlgResult<Mat<f64>> {
657 if let (Some(hash), Some((cached_hash, sym))) = (pattern_hash, self.s_llt_cache.as_ref())
663 && *cached_hash == hash
664 {
665 if let Ok(cholesky) = Llt::try_new_with_symbolic(sym.clone(), a.as_ref(), Side::Lower) {
666 return Ok(cholesky.solve(b));
667 }
668 self.s_llt_cache = None;
672 }
673
674 let sym = SymbolicLlt::try_new(a.symbolic(), Side::Lower).map_err(|e| {
675 LinAlgError::FactorizationFailed(format!("Symbolic Cholesky failed: {:?}", e))
676 })?;
677 self.s_llt_cache = pattern_hash.map(|hash| (hash, sym.clone()));
681
682 match Llt::try_new_with_symbolic(sym.clone(), a.as_ref(), Side::Lower) {
684 Ok(cholesky) => return Ok(cholesky.solve(b)),
685 Err(e) => {
686 debug!(
687 "Cholesky factorization failed: {:?}. Applying regularization.",
688 e
689 );
690 }
691 }
692
693 let n = a.nrows();
695 let symbolic = a.symbolic();
696
697 let mut trace = 0.0;
699 let mut max_diag = 0.0f64;
700 for col in 0..n {
701 let row_indices = symbolic.row_idx_of_col_raw(col);
702 let col_values = a.val_of_col(col);
703 for (idx, &row) in row_indices.iter().enumerate() {
704 if row == col {
705 trace += col_values[idx];
706 max_diag = max_diag.max(col_values[idx].abs());
707 }
708 }
709 }
710
711 let avg_diag = trace / n as f64;
713 let base_reg = avg_diag.max(max_diag).max(1.0);
714
715 for attempt in 0..5 {
716 let reg = base_reg * 10.0f64.powi(attempt - 4); debug!(
718 "Cholesky attempt {}: regularization = {:.2e}",
719 attempt + 2,
720 reg
721 );
722
723 let mut triplets = Vec::with_capacity(n * 10);
724 for col in 0..n {
725 let row_indices = symbolic.row_idx_of_col_raw(col);
726 let col_values = a.val_of_col(col);
727 for (idx, &row) in row_indices.iter().enumerate() {
728 triplets.push(Triplet::new(row, col, col_values[idx]));
729 }
730 }
731
732 for i in 0..n {
733 triplets.push(Triplet::new(i, i, reg));
734 }
735
736 let a_reg = match SparseColMat::try_new_from_triplets(n, n, &triplets) {
737 Ok(m) => m,
738 Err(e) => {
739 debug!("Failed to create regularized matrix: {:?}", e);
740 continue;
741 }
742 };
743
744 let sym_reg = match SymbolicLlt::try_new(a_reg.symbolic(), Side::Lower) {
746 Ok(s) => s,
747 Err(e) => {
748 debug!("Symbolic factorization failed: {:?}", e);
749 continue;
750 }
751 };
752
753 match Llt::try_new_with_symbolic(sym_reg, a_reg.as_ref(), Side::Lower) {
754 Ok(cholesky) => {
755 debug!("Cholesky succeeded with regularization {:.2e}", reg);
756 return Ok(cholesky.solve(b));
757 }
758 Err(e) => {
759 debug!("Cholesky failed with reg {:.2e}: {:?}", reg, e);
760 }
761 }
762 }
763
764 Err(LinAlgError::SingularMatrix(format!(
765 "Schur complement singular after 5 regularization attempts (max reg = {:.2e})",
766 base_reg
767 )))
768 }
769
770 fn solve_with_pcg(
777 &self,
778 a: &SparseColMat<usize, f64>,
779 b: &Mat<f64>,
780 h_kk: &SparseColMat<usize, f64>,
781 ) -> LinAlgResult<Mat<f64>> {
782 let precond = self.build_preconditioner(a, h_kk)?;
783
784 let result = solve_pcg(
785 b,
786 &PcgParams::new(self.cg_max_iterations, self.cg_tolerance)
787 .with_q_tolerance(self.cg_q_tolerance),
788 |p, ap| {
789 faer::sparse::linalg::matmul::sparse_dense_matmul(
791 ap.as_mut(),
792 Accum::Replace,
793 a.as_ref(),
794 p.as_ref(),
795 1.0,
796 faer::get_global_parallelism(),
797 );
798 },
799 |r, z| apply_block_preconditioner(precond.as_deref(), r, z),
800 );
801 Ok(result.x)
802 }
803
804 fn build_preconditioner(
821 &self,
822 s: &SparseColMat<usize, f64>,
823 h_kk: &SparseColMat<usize, f64>,
824 ) -> LinAlgResult<Option<Vec<DMatrix<f64>>>> {
825 let source = match self.preconditioner {
826 SchurPreconditioner::None => return Ok(None),
827 SchurPreconditioner::SchurJacobi => s,
828 SchurPreconditioner::BlockDiagonal => h_kk,
829 };
830 let partition = self.require_partition()?;
831
832 let blocks = partition
833 .kept_blocks()
834 .par_iter()
835 .enumerate()
836 .map(|(idx, block)| {
837 let offset = partition.kept_offset(idx);
838 let dense = diagonal_block(source, offset, block.dof);
839 invert_with_retry_dyn(&dense)
840 .unwrap_or_else(|| DMatrix::identity(block.dof, block.dof))
841 })
842 .collect();
843 Ok(Some(blocks))
844 }
845 fn compute_schur_complement(
856 &mut self,
857 h_kk: &SparseColMat<usize, f64>,
858 h_ke: &SparseColMat<usize, f64>,
859 h_ee_inv: &EliminatedBlocks,
860 ) -> LinAlgResult<(SparseColMat<usize, f64>, Option<u64>)> {
861 let partition = self.require_partition()?.clone();
862 let kept_dof = h_kk.nrows();
863 let h_ke_symbolic = h_ke.symbolic();
864 let wpc = kept_dof.div_ceil(64);
865
866 let mut s_dense = vec![0.0f64; kept_dof * kept_dof];
867
868 let extraction_fp = self.extraction.as_ref().map(|c| c.fingerprint);
875 let words = kept_dof * wpc;
876 let mut bits = match self.s_bitmap.take() {
877 Some((fp, b)) if Some(fp) == extraction_fp && b.len() == words => b,
878 _ => vec![0u64; words],
879 };
880
881 let h_kk_symbolic = h_kk.symbolic();
883 for col in 0..h_kk.ncols() {
884 let rows = h_kk_symbolic.row_idx_of_col_raw(col);
885 let vals = h_kk.val_of_col(col);
886 for (idx, &row) in rows.iter().enumerate() {
887 s_dense[row * kept_dof + col] += vals[idx];
888 set_dense_bit(&mut bits, wpc, row, col);
889 }
890 }
891
892 let max_dof = partition
893 .eliminated_blocks()
894 .iter()
895 .map(|b| b.dof)
896 .max()
897 .unwrap_or(0)
898 .max(1);
899 let mut kept_rows: Vec<usize> = Vec::with_capacity(32);
902 let mut h_ke_rows: Vec<f64> = Vec::with_capacity(32 * max_dof);
903 let mut contrib: Vec<f64> = Vec::with_capacity(32 * max_dof);
904 let mut cursors: Vec<usize> = Vec::with_capacity(max_dof);
905 let mut col_rows: Vec<&[usize]> = Vec::with_capacity(max_dof);
906 let mut col_vals: Vec<&[f64]> = Vec::with_capacity(max_dof);
907
908 for block_idx in 0..h_ee_inv.len() {
909 let dof = h_ee_inv.dof(block_idx);
910 if dof == 0 {
911 continue;
912 }
913 let col_base = partition.eliminated_offset(block_idx);
914
915 kept_rows.clear();
916 h_ke_rows.clear();
917 cursors.clear();
918 cursors.resize(dof, 0);
919
920 col_rows.clear();
923 col_vals.clear();
924 for local in 0..dof {
925 let col = col_base + local;
926 col_rows.push(h_ke_symbolic.row_idx_of_col_raw(col));
927 col_vals.push(h_ke.val_of_col(col));
928 }
929
930 loop {
934 let mut min_row = usize::MAX;
935 for local in 0..dof {
936 if let Some(&row) = col_rows[local].get(cursors[local]) {
937 min_row = min_row.min(row);
938 }
939 }
940 if min_row == usize::MAX {
941 break;
942 }
943
944 let strip = h_ke_rows.len();
945 h_ke_rows.resize(strip + dof, 0.0);
946 for local in 0..dof {
947 if col_rows[local].get(cursors[local]) == Some(&min_row) {
948 h_ke_rows[strip + local] = col_vals[local][cursors[local]];
949 cursors[local] += 1;
950 }
951 }
952 kept_rows.push(min_row);
953 }
954
955 if kept_rows.is_empty() {
956 continue;
957 }
958
959 let inv = h_ee_inv.block(block_idx);
962 contrib.clear();
963 contrib.resize(kept_rows.len() * dof, 0.0);
964 for r in 0..kept_rows.len() {
965 let row = &h_ke_rows[r * dof..(r + 1) * dof];
966 for c in 0..dof {
967 let inv_col = &inv[c * dof..(c + 1) * dof];
968 let mut acc = 0.0;
969 for k in 0..dof {
970 acc += row[k] * inv_col[k];
971 }
972 contrib[r * dof + c] = acc;
973 }
974 }
975
976 if dof == 3 {
983 for (i, &row_i) in kept_rows.iter().enumerate() {
984 let (c0, c1, c2) = (contrib[i * 3], contrib[i * 3 + 1], contrib[i * 3 + 2]);
985 let base = row_i * kept_dof;
986 for (j, &row_j) in kept_rows.iter().enumerate() {
987 let dot = c0 * h_ke_rows[j * 3]
988 + c1 * h_ke_rows[j * 3 + 1]
989 + c2 * h_ke_rows[j * 3 + 2];
990 s_dense[base + row_j] -= dot;
991 set_dense_bit(&mut bits, wpc, row_i, row_j);
992 }
993 }
994 } else {
995 for (i, &row_i) in kept_rows.iter().enumerate() {
996 let contrib_i = &contrib[i * dof..(i + 1) * dof];
997 let base = row_i * kept_dof;
998 for (j, &row_j) in kept_rows.iter().enumerate() {
999 let h_ke_j = &h_ke_rows[j * dof..(j + 1) * dof];
1000 let mut dot = 0.0;
1001 for k in 0..dof {
1002 dot += contrib_i[k] * h_ke_j[k];
1003 }
1004 s_dense[base + row_j] -= dot;
1005 set_dense_bit(&mut bits, wpc, row_i, row_j);
1006 }
1007 }
1008 }
1009 }
1010
1011 let output = if self.extraction.is_some() {
1019 let (symbolic, s_values, pattern_hash) =
1020 build_schur_output(kept_dof, wpc, &bits, &s_dense);
1021 let s = SparseColMat::new(symbolic, s_values);
1022 return Ok((s, Some(pattern_hash)));
1023 } else {
1024 for i in 0..kept_dof {
1026 for j in (i + 1)..kept_dof {
1027 let avg = (s_dense[i * kept_dof + j] + s_dense[j * kept_dof + i]) * 0.5;
1028 s_dense[i * kept_dof + j] = avg;
1029 s_dense[j * kept_dof + i] = avg;
1030 }
1031 }
1032 let mut s_triplets: Vec<Triplet<usize, usize, f64>> =
1033 Vec::with_capacity(kept_dof.saturating_mul(8));
1034 for row in 0..kept_dof {
1035 let row_base = row * kept_dof;
1036 for col in 0..kept_dof {
1037 let val = s_dense[row_base + col];
1038 if val.abs() > 1e-12 {
1039 s_triplets.push(Triplet::new(row, col, val));
1040 }
1041 }
1042 }
1043 SparseColMat::try_new_from_triplets(kept_dof, kept_dof, &s_triplets)
1044 .map_err(|e| LinAlgError::SparseMatrixCreation(format!("Schur S: {:?}", e)))?
1045 };
1046
1047 self.s_bitmap = Some((extraction_fp.unwrap_or_default(), bits));
1048 Ok((output, None))
1049 }
1050 fn compute_reduced_gradient(
1052 &self,
1053 g_k: &Mat<f64>,
1054 g_e: &Mat<f64>,
1055 h_ke: &SparseColMat<usize, f64>,
1056 h_ee_inv: &EliminatedBlocks,
1057 ) -> LinAlgResult<Mat<f64>> {
1058 let partition = self.require_partition()?;
1059 let kept_dof = g_k.nrows();
1060
1061 let mut hee_inv_ge = Mat::zeros(g_e.nrows(), 1);
1063 for block_idx in 0..h_ee_inv.len() {
1064 let dof = h_ee_inv.dof(block_idx);
1065 let base = partition.eliminated_offset(block_idx);
1066 let inv = h_ee_inv.block(block_idx);
1067 for r in 0..dof {
1068 let mut acc = 0.0;
1069 for c in 0..dof {
1070 acc += inv[c * dof + r] * g_e[(base + c, 0)];
1071 }
1072 hee_inv_ge[(base + r, 0)] = acc;
1073 }
1074 }
1075
1076 let mut correction = Mat::<f64>::zeros(kept_dof, 1);
1078 let symbolic = h_ke.symbolic();
1079 for col in 0..h_ke.ncols() {
1080 let rows = symbolic.row_idx_of_col_raw(col);
1081 let vals = h_ke.val_of_col(col);
1082 for (idx, &row) in rows.iter().enumerate() {
1083 correction[(row, 0)] += vals[idx] * hee_inv_ge[(col, 0)];
1084 }
1085 }
1086
1087 let mut g_reduced = Mat::zeros(kept_dof, 1);
1088 for i in 0..kept_dof {
1089 g_reduced[(i, 0)] = g_k[(i, 0)] - correction[(i, 0)];
1090 }
1091 Ok(g_reduced)
1092 }
1093 fn back_substitute(
1095 &self,
1096 delta_k: &Mat<f64>,
1097 g_e: &Mat<f64>,
1098 h_ke: &SparseColMat<usize, f64>,
1099 h_ee_inv: &EliminatedBlocks,
1100 ) -> LinAlgResult<Mat<f64>> {
1101 let partition = self.require_partition()?;
1102 let eliminated_dof = g_e.nrows();
1103
1104 let mut hke_t_delta = Mat::<f64>::zeros(eliminated_dof, 1);
1106 let symbolic = h_ke.symbolic();
1107 for col in 0..h_ke.ncols() {
1108 let rows = symbolic.row_idx_of_col_raw(col);
1109 let vals = h_ke.val_of_col(col);
1110 let mut acc = 0.0;
1111 for (idx, &row) in rows.iter().enumerate() {
1112 acc += vals[idx] * delta_k[(row, 0)];
1113 }
1114 hke_t_delta[(col, 0)] = acc;
1115 }
1116
1117 let mut delta_e = Mat::zeros(eliminated_dof, 1);
1118 for block_idx in 0..h_ee_inv.len() {
1119 let dof = h_ee_inv.dof(block_idx);
1120 let base = partition.eliminated_offset(block_idx);
1121 let inv = h_ee_inv.block(block_idx);
1122 for r in 0..dof {
1123 let mut acc = 0.0;
1124 for c in 0..dof {
1125 let rhs = g_e[(base + c, 0)] - hke_t_delta[(base + c, 0)];
1126 acc += inv[c * dof + r] * rhs;
1127 }
1128 delta_e[(base + r, 0)] = acc;
1129 }
1130 }
1131
1132 Ok(delta_e)
1133 }
1134}
1135
1136impl Default for ExplicitSparseSchur {
1137 fn default() -> Self {
1138 Self::new()
1139 }
1140}
1141
1142impl StructureAware for ExplicitSparseSchur {
1143 fn initialize_structure(
1144 &mut self,
1145 variables: &SlotMap<VarKey, Box<dyn ManifoldVariable>>,
1146 variable_index_map: &SecondaryMap<VarKey, usize>,
1147 schur_landmark_keys: &std::collections::HashSet<VarKey>,
1148 ) -> LinAlgResult<()> {
1149 let effective_keys = crate::linalg::schur::effective_landmark_keys(
1153 variables,
1154 schur_landmark_keys,
1155 &self.ordering,
1156 );
1157
1158 self.build_partition(variables, variable_index_map, &effective_keys)?;
1159
1160 Ok(())
1163 }
1164}
1165
1166impl LinearSolver<SparseMode> for ExplicitSparseSchur {
1167 fn solve_normal_equation(
1168 &mut self,
1169 residuals: &Mat<f64>,
1170 jacobian: &SparseColMat<usize, f64>,
1171 ) -> LinAlgResult<Mat<f64>> {
1172 self.require_partition()?;
1173 if self.variant == ExplicitSchurVariant::Chunked {
1174 return self.solve_chunked(residuals, jacobian, None);
1175 }
1176
1177 let NormalEquations { hessian, gradient } = self.ne_cache.compute(residuals, jacobian)?;
1179 let mut neg_gradient = Mat::zeros(gradient.nrows(), 1);
1180 for i in 0..gradient.nrows() {
1181 neg_gradient[(i, 0)] = -gradient[(i, 0)];
1182 }
1183
1184 self.ensure_block_diagonal(&hessian)?;
1187 let (h_kk, h_ke) = self.extract_kept_and_coupling(&hessian)?;
1188 let (g_k, g_e) = self.extract_gradient_blocks(&neg_gradient)?;
1189
1190 let mut eliminated = std::mem::take(&mut self.eliminated);
1192 let inversion = (|| -> LinAlgResult<()> {
1193 let partition = self.require_partition()?;
1194 eliminated.gather(&hessian, partition);
1195 eliminated.invert_in_place(partition)
1196 })();
1197 if let Err(e) = inversion {
1198 self.eliminated = eliminated;
1199 return Err(e);
1200 }
1201
1202 let result = self.solve_reduced_system(&h_kk, &h_ke, &g_k, &g_e, &eliminated);
1207 self.eliminated = eliminated;
1208 if result.is_ok() {
1209 self.hessian = Some(hessian);
1210 self.gradient = Some(gradient);
1211 }
1212 result
1213 }
1214 fn solve_augmented_equation(
1215 &mut self,
1216 residuals: &Mat<f64>,
1217 jacobian: &SparseColMat<usize, f64>,
1218 damping: &Damping,
1219 ) -> LinAlgResult<Mat<f64>> {
1220 self.require_partition()?;
1221 if self.variant == ExplicitSchurVariant::Chunked {
1222 return self.solve_chunked(residuals, jacobian, Some(damping));
1223 }
1224
1225 let NormalEquations { hessian, gradient } = self.ne_cache.compute(residuals, jacobian)?;
1227 let mut neg_gradient = Mat::zeros(gradient.nrows(), 1);
1228 for i in 0..gradient.nrows() {
1229 neg_gradient[(i, 0)] = -gradient[(i, 0)];
1230 }
1231
1232 self.ensure_block_diagonal(&hessian)?;
1234 let partition = self.require_partition()?;
1235 let kept_dof = partition.kept_dof();
1236 let num_eliminated_blocks = partition.eliminated_blocks().len();
1237 let (h_kk, h_ke) = self.extract_kept_and_coupling(&hessian)?;
1238 let (g_k, g_e) = self.extract_gradient_blocks(&neg_gradient)?;
1239
1240 debug!(
1241 "Iteration matrices: H {}×{}, H_kk {}×{}, H_ke {}×{}, {} eliminated blocks",
1242 hessian.nrows(),
1243 hessian.ncols(),
1244 h_kk.nrows(),
1245 h_kk.ncols(),
1246 h_ke.nrows(),
1247 h_ke.ncols(),
1248 num_eliminated_blocks
1249 );
1250
1251 let h_kk_damped = damp_camera_block(&h_kk, kept_dof, damping)?;
1254
1255 let mut eliminated = std::mem::take(&mut self.eliminated);
1256 let prepared = (|| -> LinAlgResult<()> {
1257 let partition = self.require_partition()?;
1258 eliminated.gather(&hessian, partition);
1259 eliminated.damp(damping);
1260 eliminated.invert_in_place(partition)
1261 })();
1262 if let Err(e) = prepared {
1263 self.eliminated = eliminated;
1264 return Err(e);
1265 }
1266
1267 let result = self.solve_reduced_system(&h_kk_damped, &h_ke, &g_k, &g_e, &eliminated);
1272 self.eliminated = eliminated;
1273 if result.is_ok() {
1274 self.hessian = Some(hessian);
1275 self.gradient = Some(gradient);
1276 }
1277 result
1278 }
1279
1280 fn hessian_vec_product(&self, v: &Mat<f64>) -> Option<Mat<f64>> {
1281 if let Some(h) = self.hessian.as_ref() {
1282 return Some(
1283 <SparseMode as crate::linearizer::AssemblyBackend>::hessian_vec_product(h, v),
1284 );
1285 }
1286 let j = self.chunked_jacobian.as_ref()?;
1288 let symbolic = j.symbolic();
1289
1290 let mut jv = Mat::<f64>::zeros(j.nrows(), 1);
1291 for col in 0..j.ncols() {
1292 let x = v[(col, 0)];
1293 if x == 0.0 {
1294 continue;
1295 }
1296 let rows = symbolic.row_idx_of_col_raw(col);
1297 let vals = j.val_of_col(col);
1298 for (i, &row) in rows.iter().enumerate() {
1299 jv[(row, 0)] += vals[i] * x;
1300 }
1301 }
1302
1303 let mut out = Mat::<f64>::zeros(j.ncols(), 1);
1304 for col in 0..j.ncols() {
1305 let rows = symbolic.row_idx_of_col_raw(col);
1306 let vals = j.val_of_col(col);
1307 let mut acc = 0.0;
1308 for (i, &row) in rows.iter().enumerate() {
1309 acc += vals[i] * jv[(row, 0)];
1310 }
1311 out[(col, 0)] = acc;
1312 }
1313 Some(out)
1314 }
1315
1316 fn get_hessian(&self) -> Option<&SparseColMat<usize, f64>> {
1317 self.hessian.as_ref()
1318 }
1319
1320 fn get_gradient(&self) -> Option<&Mat<f64>> {
1321 self.gradient.as_ref()
1322 }
1323}
1324
1325impl ExplicitSparseSchur {
1327 fn solve_chunked(
1335 &mut self,
1336 residuals: &Mat<f64>,
1337 jacobian: &SparseColMat<usize, f64>,
1338 damping: Option<&Damping>,
1339 ) -> LinAlgResult<Mat<f64>> {
1340 if self.chunked.as_ref().is_none_or(|c| !c.matches(jacobian)) {
1342 let partition = self.require_partition()?;
1343 self.chunked = Some(super::chunk_eliminator::ChunkedSchurEliminator::new(
1344 jacobian, partition,
1345 )?);
1346 }
1347
1348 let mut eliminator = match self.chunked.take() {
1349 Some(e) => e,
1350 None => {
1351 return Err(LinAlgError::InvalidState(
1352 "chunk eliminator not initialized".to_string(),
1353 )
1354 .log());
1355 }
1356 };
1357
1358 let outcome = (|| -> LinAlgResult<Mat<f64>> {
1363 let partition = self.require_partition()?.clone();
1364 let reduced = eliminator.eliminate(jacobian, residuals, &partition, damping)?;
1365
1366 let kept_dof = reduced.kept_dof;
1368 let mut triplets: Vec<Triplet<usize, usize, f64>> =
1371 Vec::with_capacity(kept_dof.saturating_mul(8));
1372 for col in 0..kept_dof {
1373 for row in 0..kept_dof {
1374 let v = reduced.s[(row, col)];
1375 if v.abs() > 1e-12 {
1376 triplets.push(Triplet::new(row, col, v));
1377 }
1378 }
1379 }
1380 let s = SparseColMat::try_new_from_triplets(kept_dof, kept_dof, &triplets)
1381 .map_err(|e| LinAlgError::SparseMatrixCreation(format!("Schur S: {e:?}")))?;
1382
1383 let mut rhs = Mat::<f64>::zeros(kept_dof, 1);
1385 for i in 0..kept_dof {
1386 rhs[(i, 0)] = -reduced.g_reduced[(i, 0)];
1387 }
1388 let delta_k = self.solve_with_cholesky(&s, &rhs, None)?;
1391
1392 let delta_e = self.back_substitute_chunked(&delta_k, &reduced, jacobian, &partition)?;
1395 self.combine_updates(&delta_k, &delta_e)
1396 })();
1397
1398 self.chunked = Some(eliminator);
1399 if outcome.is_ok() {
1400 self.gradient = Some(Self::full_gradient(jacobian, residuals));
1401 self.hessian = None;
1402 self.chunked_jacobian = Some(jacobian.clone());
1403 }
1404 outcome
1405 }
1406
1407 fn full_gradient(jacobian: &SparseColMat<usize, f64>, residuals: &Mat<f64>) -> Mat<f64> {
1412 let symbolic = jacobian.symbolic();
1413 let per_col: Vec<f64> = (0..jacobian.ncols())
1414 .into_par_iter()
1415 .map(|col| {
1416 let rows = symbolic.row_idx_of_col_raw(col);
1417 let vals = jacobian.val_of_col(col);
1418 let mut acc = 0.0;
1419 for (i, &row) in rows.iter().enumerate() {
1420 acc += vals[i] * residuals[(row, 0)];
1421 }
1422 acc
1423 })
1424 .collect();
1425 Mat::from_fn(jacobian.ncols(), 1, |r, _| per_col[r])
1426 }
1427
1428 fn back_substitute_chunked(
1430 &self,
1431 delta_k: &Mat<f64>,
1432 reduced: &super::chunk_eliminator::ReducedSystem,
1433 jacobian: &SparseColMat<usize, f64>,
1434 partition: &SchurPartition,
1435 ) -> LinAlgResult<Mat<f64>> {
1436 let symbolic = jacobian.symbolic();
1437
1438 let mut j_delta = Mat::<f64>::zeros(jacobian.nrows(), 1);
1441 let mut local = 0usize;
1442 for block in partition.kept_blocks() {
1443 for offset in 0..block.dof {
1444 let col = block.col_start + offset;
1445 let rows = symbolic.row_idx_of_col_raw(col);
1446 let vals = jacobian.val_of_col(col);
1447 let x = delta_k[(local, 0)];
1448 if x != 0.0 {
1449 for (i, &row) in rows.iter().enumerate() {
1450 j_delta[(row, 0)] += vals[i] * x;
1451 }
1452 }
1453 local += 1;
1454 }
1455 }
1456
1457 let mut delta_e = Mat::<f64>::zeros(partition.eliminated_dof(), 1);
1458 let max_dof = partition
1462 .eliminated_blocks()
1463 .iter()
1464 .map(|block| block.dof)
1465 .max()
1466 .unwrap_or(0);
1467 let mut etjd = vec![0.0f64; max_dof];
1468 for (block_idx, block) in partition.eliminated_blocks().iter().enumerate() {
1469 let dof = block.dof;
1470 let base = partition.eliminated_offset(block_idx);
1471 let inv = reduced.eliminated_inverse.block(block_idx);
1472
1473 for (a, slot) in etjd[..dof].iter_mut().enumerate() {
1475 let col = block.col_start + a;
1476 let rows = symbolic.row_idx_of_col_raw(col);
1477 let vals = jacobian.val_of_col(col);
1478 let mut acc = 0.0;
1479 for (i, &row) in rows.iter().enumerate() {
1480 acc += vals[i] * j_delta[(row, 0)];
1481 }
1482 *slot = acc;
1483 }
1484
1485 for r in 0..dof {
1487 let mut acc = -reduced.eliminated_rhs[(base + r, 0)];
1488 for (c, &term) in etjd[..dof].iter().enumerate() {
1489 acc -= inv[c * dof + r] * term;
1490 }
1491 delta_e[(base + r, 0)] = acc;
1492 }
1493 }
1494
1495 Ok(delta_e)
1496 }
1497
1498 fn solve_reduced_system(
1503 &mut self,
1504 h_kk: &SparseColMat<usize, f64>,
1505 h_ke: &SparseColMat<usize, f64>,
1506 g_k: &Mat<f64>,
1507 g_e: &Mat<f64>,
1508 h_ee_inv: &EliminatedBlocks,
1509 ) -> LinAlgResult<Mat<f64>> {
1510 let (s, s_pattern_hash) = self.compute_schur_complement(h_kk, h_ke, h_ee_inv)?;
1511 let g_reduced = self.compute_reduced_gradient(g_k, g_e, h_ke, h_ee_inv)?;
1512
1513 let delta_k = match self.variant {
1514 ExplicitSchurVariant::Iterative => self.solve_with_pcg(&s, &g_reduced, h_kk)?,
1515 ExplicitSchurVariant::Sparse | ExplicitSchurVariant::Chunked => {
1516 self.solve_with_cholesky(&s, &g_reduced, s_pattern_hash)?
1517 }
1518 };
1519
1520 let delta_e = self.back_substitute(&delta_k, g_e, h_ke, h_ee_inv)?;
1521 self.combine_updates(&delta_k, &delta_e)
1522 }
1523
1524 fn combine_updates(&self, delta_k: &Mat<f64>, delta_e: &Mat<f64>) -> LinAlgResult<Mat<f64>> {
1529 let partition = self.require_partition()?;
1530 let mut delta = Mat::zeros(partition.total_dof(), 1);
1531
1532 for block in partition.kept_blocks() {
1533 for offset in 0..block.dof {
1534 let global = block.col_start + offset;
1535 if let Some(local) = partition.kept_local(global) {
1536 delta[(global, 0)] = delta_k[(local, 0)];
1537 }
1538 }
1539 }
1540 for (block_idx, block) in partition.eliminated_blocks().iter().enumerate() {
1541 let base = partition.eliminated_offset(block_idx);
1542 for offset in 0..block.dof {
1543 delta[(block.col_start + offset, 0)] = delta_e[(base + offset, 0)];
1544 }
1545 }
1546
1547 debug!(
1548 "Update norms: delta_kept={:.6e}, delta_eliminated={:.6e}, combined={:.6e}",
1549 delta_k.norm_l2(),
1550 delta_e.norm_l2(),
1551 delta.norm_l2()
1552 );
1553
1554 Ok(delta)
1555 }
1556}
1557
1558fn diagonal_block(m: &SparseColMat<usize, f64>, offset: usize, dof: usize) -> DMatrix<f64> {
1561 let symbolic = m.symbolic();
1562 let mut block = DMatrix::zeros(dof, dof);
1563 for local_col in 0..dof {
1564 let col = offset + local_col;
1565 let rows = symbolic.row_idx_of_col_raw(col);
1566 let vals = m.val_of_col(col);
1567 for (idx, &row) in rows.iter().enumerate() {
1568 if row >= offset && row < offset + dof {
1569 block[(row - offset, local_col)] = vals[idx];
1570 }
1571 }
1572 }
1573 block
1574}
1575
1576fn apply_block_preconditioner(blocks: Option<&[DMatrix<f64>]>, r: &Mat<f64>, z: &mut Mat<f64>) {
1581 let Some(blocks) = blocks else {
1582 faer::zip!(z, r).for_each(|faer::unzip!(z, r)| *z = *r);
1583 return;
1584 };
1585
1586 let mut offset = 0usize;
1587 for inv in blocks {
1588 let dof = inv.nrows();
1589 for row in 0..dof {
1590 let mut acc = 0.0;
1591 for col in 0..dof {
1592 acc += inv[(row, col)] * r[(offset + col, 0)];
1593 }
1594 z[(offset + row, 0)] = acc;
1595 }
1596 offset += dof;
1597 }
1598}
1599
1600fn damp_camera_block(
1616 h_cc: &SparseColMat<usize, f64>,
1617 cam_size: usize,
1618 damping: &Damping,
1619) -> LinAlgResult<SparseColMat<usize, f64>> {
1620 let symbolic = h_cc.symbolic();
1621
1622 let diag_pos: Vec<Option<usize>> = (0..h_cc.ncols())
1624 .map(|col| {
1625 let col_start = symbolic.col_range(col).start;
1626 symbolic
1627 .row_idx_of_col_raw(col)
1628 .iter()
1629 .position(|&row| row == col)
1630 .map(|local| col_start + local)
1631 })
1632 .collect();
1633
1634 if diag_pos.iter().all(Option::is_some) {
1635 let owned = symbolic.to_owned().map_err(|e| {
1636 LinAlgError::SparseMatrixCreation(format!("Damped H_cc symbolic: {e:?}"))
1637 })?;
1638 let mut values = h_cc.as_ref().val().to_vec();
1639 for pos in diag_pos.iter().flatten() {
1640 values[*pos] += damping.diagonal_term(values[*pos]);
1641 }
1642 return Ok(SparseColMat::new(owned, values));
1643 }
1644
1645 let mut triplets = Vec::with_capacity(h_cc.compute_nnz() + cam_size);
1647 for col in 0..h_cc.ncols() {
1648 let row_indices = symbolic.row_idx_of_col_raw(col);
1649 let col_values = h_cc.val_of_col(col);
1650 for (idx, &row) in row_indices.iter().enumerate() {
1651 let val = col_values[idx];
1652 let val = if row == col {
1653 val + damping.diagonal_term(val)
1654 } else {
1655 val
1656 };
1657 triplets.push(Triplet::new(row, col, val));
1658 }
1659 }
1660 for (i, pos) in diag_pos.iter().enumerate().take(cam_size) {
1661 if pos.is_none() {
1662 triplets.push(Triplet::new(i, i, damping.diagonal_term(0.0)));
1665 }
1666 }
1667 SparseColMat::try_new_from_triplets(cam_size, cam_size, &triplets)
1668 .map_err(|e| LinAlgError::SparseMatrixCreation(format!("Damped H_cc: {e:?}")))
1669}
1670
1671#[cfg(test)]
1672mod tests {
1673 use super::*;
1674 use crate::core::VarKey;
1675 use crate::core::variable::Variable;
1676 use apex_manifolds::{LieGroup, ManifoldType, rn, se3};
1677 use nalgebra::DVector;
1678 use slotmap::{SecondaryMap, SlotMap};
1679
1680 type TestResult = Result<(), Box<dyn std::error::Error>>;
1681
1682 type TestSetup = (
1684 SlotMap<VarKey, Box<dyn ManifoldVariable>>,
1685 SecondaryMap<VarKey, usize>,
1686 SparseColMat<usize, f64>,
1687 Mat<f64>,
1688 std::collections::HashSet<VarKey>,
1689 );
1690
1691 fn create_schur_test_setup() -> Result<TestSetup, Box<dyn std::error::Error>> {
1697 let se3_id = DVector::from_vec(vec![1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]);
1698 let pt_zero = DVector::from_vec(vec![0.0, 0.0, 0.0]);
1699
1700 let mut variables: SlotMap<VarKey, Box<dyn ManifoldVariable>> = SlotMap::with_key();
1701 let cam0 = variables.insert(Box::new(Variable::new(se3::SE3::from_param_slice(
1702 se3_id.as_slice(),
1703 ))));
1704 let cam1 = variables.insert(Box::new(Variable::new(se3::SE3::from_param_slice(
1705 se3_id.as_slice(),
1706 ))));
1707 let pt0 = variables.insert(Box::new(Variable::new(rn::Rn::new(pt_zero.clone()))));
1708 let pt1 = variables.insert(Box::new(Variable::new(rn::Rn::new(pt_zero.clone()))));
1709 let pt2 = variables.insert(Box::new(Variable::new(rn::Rn::new(pt_zero.clone()))));
1710
1711 let mut variable_index_map: SecondaryMap<VarKey, usize> = SecondaryMap::new();
1713 variable_index_map.insert(cam0, 0);
1714 variable_index_map.insert(cam1, 6);
1715 variable_index_map.insert(pt0, 12);
1716 variable_index_map.insert(pt1, 15);
1717 variable_index_map.insert(pt2, 18);
1718
1719 let n_rows = 36;
1724 let n_cols = 21;
1725 let cam_cols = [0usize, 6];
1726 let lm_cols = [12usize, 15, 18];
1727
1728 let mut triplets: Vec<Triplet<usize, usize, f64>> = Vec::new();
1729 for (ci, &cam_col) in cam_cols.iter().enumerate() {
1730 for (li, &lm_col) in lm_cols.iter().enumerate() {
1731 let row_base = (ci * 3 + li) * 6;
1732 for k in 0..6 {
1733 triplets.push(Triplet::new(row_base + k, cam_col + k, 1.0));
1734 triplets.push(Triplet::new(row_base + k, lm_col + (k % 3), 1.0));
1735 }
1736 }
1737 }
1738
1739 let jacobian = SparseColMat::try_new_from_triplets(n_rows, n_cols, &triplets)?;
1740 let residuals = Mat::from_fn(n_rows, 1, |i, _| (i % 5) as f64 * 0.1);
1741
1742 let mut landmark_keys = std::collections::HashSet::new();
1743 landmark_keys.insert(pt0);
1744 landmark_keys.insert(pt1);
1745 landmark_keys.insert(pt2);
1746
1747 Ok((
1748 variables,
1749 variable_index_map,
1750 jacobian,
1751 residuals,
1752 landmark_keys,
1753 ))
1754 }
1755
1756 #[test]
1760 fn cached_extraction_matches_direct_reference() -> TestResult {
1761 let (variables, variable_index_map, jacobian, _residuals, landmark_keys) =
1762 create_schur_test_setup()?;
1763 let mut solver = ExplicitSparseSchur::new();
1764 solver.initialize_structure(&variables, &variable_index_map, &landmark_keys)?;
1765
1766 let h_full = {
1767 let mut cache =
1768 crate::linalg::sparse::normal_eq::NormalEquationsCache::try_new(&jacobian)?;
1769 let residuals = Mat::zeros(jacobian.nrows(), 1);
1770 cache.compute(&residuals, &jacobian)?.hessian
1771 };
1772
1773 let kk_ref = solver.extract_kept_block(&h_full)?;
1774 let ke_ref = solver.extract_coupling_block(&h_full)?;
1775 let (kk_first, ke_first) = solver.extract_kept_and_coupling(&h_full)?;
1776 let (kk_second, ke_second) = solver.extract_kept_and_coupling(&h_full)?;
1777
1778 for (cached, direct, what) in [
1779 (&kk_first, &kk_ref, "H_kk first call"),
1780 (&kk_second, &kk_ref, "H_kk cached call"),
1781 (&ke_first, &ke_ref, "H_ke first call"),
1782 (&ke_second, &ke_ref, "H_ke cached call"),
1783 ] {
1784 assert_eq!(
1785 (cached.nrows(), cached.ncols()),
1786 (direct.nrows(), direct.ncols()),
1787 "{what}: shape mismatch"
1788 );
1789 assert_eq!(cached.compute_nnz(), direct.compute_nnz(), "{what}: nnz");
1790 for col in 0..direct.ncols() {
1791 assert_eq!(
1792 cached.symbolic().row_idx_of_col_raw(col),
1793 direct.symbolic().row_idx_of_col_raw(col),
1794 "{what}: pattern mismatch in column {col}"
1795 );
1796 assert_eq!(
1797 cached.val_of_col(col),
1798 direct.val_of_col(col),
1799 "{what}: value mismatch in column {col}"
1800 );
1801 }
1802 }
1803 Ok(())
1804 }
1805
1806 #[test]
1814 fn schur_complement_matches_naive_dense() -> TestResult {
1815 let (variables, variable_index_map, jacobian, _residuals, landmark_keys) =
1816 create_schur_test_setup()?;
1817 let mut solver = ExplicitSparseSchur::new();
1818 solver.initialize_structure(&variables, &variable_index_map, &landmark_keys)?;
1819
1820 let nrows = jacobian.nrows();
1822 let ncols = jacobian.ncols();
1823 let mut dense_j = nalgebra::DMatrix::zeros(nrows, ncols);
1824 for col in 0..ncols {
1825 let rows = jacobian.symbolic().row_idx_of_col_raw(col);
1826 let vals = jacobian.val_of_col(col);
1827 for (i, &row) in rows.iter().enumerate() {
1828 dense_j[(row, col)] = vals[i];
1829 }
1830 }
1831 let h = dense_j.transpose() * &dense_j;
1832
1833 let mut elim_cols = Vec::new();
1835 for key in &landmark_keys {
1836 let start = variable_index_map.get(*key).ok_or("elim key missing")?;
1837 let dof = variables.get(*key).ok_or("elim var missing")?.dof();
1838 elim_cols.extend(*start..*start + dof);
1839 }
1840 elim_cols.sort_unstable();
1841 let kept_cols: Vec<usize> = (0..ncols).filter(|c| !elim_cols.contains(c)).collect();
1842
1843 let submatrix = |rows: &[usize], cols: &[usize]| -> nalgebra::DMatrix<f64> {
1844 nalgebra::DMatrix::from_fn(rows.len(), cols.len(), |r, c| h[(rows[r], cols[c])])
1845 };
1846 let h_kk = submatrix(&kept_cols, &kept_cols);
1847 let h_ke = submatrix(&kept_cols, &elim_cols);
1848 let h_ee = submatrix(&elim_cols, &elim_cols);
1849 let h_ee_inv = h_ee.try_inverse().ok_or("naive H_ee must invert")?;
1850 let s_naive = h_kk - &h_ke * h_ee_inv * h_ke.transpose();
1851
1852 let h_full = {
1855 let mut cache =
1856 crate::linalg::sparse::normal_eq::NormalEquationsCache::try_new(&jacobian)?;
1857 let residuals = Mat::zeros(nrows, 1);
1858 cache.compute(&residuals, &jacobian)?.hessian
1859 };
1860 let h_kk2 = solver.extract_kept_block(&h_full)?;
1861 let h_ke2 = solver.extract_coupling_block(&h_full)?;
1862 let mut blocks = EliminatedBlocks::new(solver.partition().ok_or("partition missing")?);
1863 blocks.gather(&h_full, solver.partition().ok_or("partition missing")?);
1864 blocks.invert_in_place(solver.partition().ok_or("partition missing")?)?;
1865 let (s, _) = solver.compute_schur_complement(&h_kk2, &h_ke2, &blocks)?;
1866
1867 let s_val = |i: usize, j: usize| -> f64 {
1871 let rows = s.symbolic().row_idx_of_col_raw(j);
1872 let vals = s.val_of_col(j);
1873 rows.iter()
1874 .position(|&r| r == i)
1875 .map(|p| vals[p])
1876 .unwrap_or(0.0)
1877 };
1878 let k = kept_cols.len();
1879 assert_eq!((s.nrows(), s.ncols()), (k, k));
1880 for i in 0..k {
1881 for j in 0..k {
1882 let (a, b) = (s_val(i, j), s_val(j, i));
1883 assert!(
1884 (a - b).abs() < 1e-12,
1885 "S not symmetric at ({i},{j}): {a} vs {b}"
1886 );
1887 assert!(
1888 (a - s_naive[(i, j)]).abs() < 1e-9,
1889 "S[{i},{j}] = {a}, naive dense = {}",
1890 s_naive[(i, j)]
1891 );
1892 }
1893 }
1894 Ok(())
1895 }
1896
1897 #[test]
1898 fn test_schur_ordering_rn3_eliminated() {
1899 let ordering = SchurOrdering::default();
1900 assert!(ordering.should_eliminate(&ManifoldType::RN, 3));
1902 }
1903
1904 #[test]
1905 fn test_schur_ordering_se3_not_eliminated() {
1906 let ordering = SchurOrdering::default();
1907 assert!(!ordering.should_eliminate(&ManifoldType::SE3, 6));
1909 }
1910
1911 #[test]
1912 fn test_schur_ordering_wrong_type_not_eliminated() {
1913 let ordering = SchurOrdering::default();
1914 assert!(!ordering.should_eliminate(&ManifoldType::SE3, 6));
1916 }
1917
1918 #[test]
1919 fn test_schur_ordering_wrong_size_not_eliminated() {
1920 let ordering = SchurOrdering::default();
1921 assert!(!ordering.should_eliminate(&ManifoldType::RN, 6));
1923 assert!(!ordering.should_eliminate(&ManifoldType::RN, 2));
1924 }
1925
1926 #[test]
1927 fn test_schur_ordering_by_name_matches_type() {
1928 let ordering = SchurOrdering::default();
1929 assert!(ordering.should_eliminate_by_name("Rn", 3));
1930 assert!(!ordering.should_eliminate_by_name("SE3", 6));
1931 assert!(!ordering.should_eliminate_by_name("NotAManifold", 3));
1932 }
1933
1934 #[test]
1935 fn test_auto_elimination_is_opt_in() -> Result<(), Box<dyn std::error::Error>> {
1936 use crate::core::variable::Variable;
1937 use apex_manifolds::{rn, se3};
1938 use nalgebra::DVector;
1939 use slotmap::SlotMap;
1940
1941 let mut variables: SlotMap<VarKey, Box<dyn ManifoldVariable>> = SlotMap::with_key();
1943 let se3_data = DVector::from_vec(vec![1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]);
1944 let cam0 = variables.insert(Box::new(Variable::new(se3::SE3::from_param_slice(
1945 se3_data.as_slice(),
1946 ))));
1947 let cam1 = variables.insert(Box::new(Variable::new(se3::SE3::from_param_slice(
1948 se3_data.as_slice(),
1949 ))));
1950 let pt_data = DVector::from_vec(vec![0.0, 0.0, 0.0]);
1951 let pt0 = variables.insert(Box::new(Variable::new(rn::Rn::new(pt_data.clone()))));
1952 let pt1 = variables.insert(Box::new(Variable::new(rn::Rn::new(pt_data.clone()))));
1953
1954 let mut index_map = SecondaryMap::new();
1955 index_map.insert(cam0, 0);
1956 index_map.insert(cam1, 6);
1957 index_map.insert(pt0, 12);
1958 index_map.insert(pt1, 15);
1959
1960 let empty_marks = std::collections::HashSet::new();
1961
1962 let mut marks = std::collections::HashSet::new();
1966 marks.insert(pt0);
1967 let mut solver = ExplicitSparseSchur::new();
1968 solver.initialize_structure(&variables, &index_map, &marks)?;
1969 let structure = solver.partition().ok_or("partition missing")?;
1970 assert_eq!(
1971 structure.eliminated_blocks().len(),
1972 1,
1973 "auto-detection must be off by default"
1974 );
1975 assert_eq!(structure.kept_blocks().len(), 3);
1976 assert!(structure.kept_blocks().iter().any(|b| b.key == pt1));
1977
1978 let ordering = SchurOrdering::default().with_auto_detect(true);
1980 let mut solver = ExplicitSparseSchur::new().with_ordering(ordering);
1981 solver.initialize_structure(&variables, &index_map, &empty_marks)?;
1982 let structure = solver.partition().ok_or("partition missing")?;
1983 assert_eq!(
1984 structure.eliminated_blocks().len(),
1985 2,
1986 "Rn(3) variables must auto-eliminate"
1987 );
1988 assert_eq!(
1989 structure.kept_blocks().len(),
1990 2,
1991 "SE3 variables must stay cameras"
1992 );
1993
1994 let mut marks = std::collections::HashSet::new();
1996 marks.insert(cam1);
1997 let mut solver = ExplicitSparseSchur::new();
1998 solver.initialize_structure(&variables, &index_map, &marks)?;
1999 let structure = solver.partition().ok_or("partition missing")?;
2000 assert!(structure.eliminated_blocks().iter().any(|b| b.key == cam1));
2001
2002 Ok(())
2003 }
2004 #[test]
2007 fn test_partition_requires_both_sides() {
2008 assert!(SchurPartition::new(Vec::new(), Vec::new()).is_err());
2009 }
2010
2011 #[test]
2012 fn test_solver_creation() {
2013 let solver = ExplicitSparseSchur::new();
2014 assert!(solver.partition().is_none());
2015 }
2016
2017 #[test]
2018 fn test_schur_variants() {
2019 let solver = ExplicitSparseSchur::new()
2020 .with_variant(ExplicitSchurVariant::Iterative)
2021 .with_preconditioner(SchurPreconditioner::BlockDiagonal)
2022 .with_cg_params(100, 1e-8);
2023
2024 assert_eq!(solver.cg_max_iterations, 100);
2025 assert!((solver.cg_tolerance - 1e-8).abs() < 1e-12);
2026 assert_eq!(solver.variant, ExplicitSchurVariant::Iterative);
2027 assert_eq!(solver.preconditioner, SchurPreconditioner::BlockDiagonal);
2028 }
2029
2030 #[test]
2038 fn preconditioner_selection_reaches_pcg() -> Result<(), LinAlgError> {
2039 use faer::sparse::Triplet;
2040
2041 let kept = vec![BlockSpan {
2044 key: VarKey::default(),
2045 col_start: 0,
2046 dof: 2,
2047 }];
2048 let eliminated = vec![BlockSpan {
2049 key: VarKey::default(),
2050 col_start: 2,
2051 dof: 1,
2052 }];
2053 let partition = SchurPartition::new(kept, eliminated)?;
2054 let mut solver = ExplicitSparseSchur::new();
2055 solver.partition = Some(partition);
2056
2057 let mk = |vals: [f64; 4]| -> Result<SparseColMat<usize, f64>, LinAlgError> {
2058 SparseColMat::try_new_from_triplets(
2059 2,
2060 2,
2061 &[
2062 Triplet::new(0, 0, vals[0]),
2063 Triplet::new(1, 0, vals[1]),
2064 Triplet::new(0, 1, vals[2]),
2065 Triplet::new(1, 1, vals[3]),
2066 ],
2067 )
2068 .map_err(|e| LinAlgError::SparseMatrixCreation(format!("{e:?}")))
2069 };
2070 let h_kk = mk([4.0, 1.0, 1.0, 5.0])?;
2071 let s = mk([2.0, 0.5, 0.5, 3.0])?;
2073
2074 solver.preconditioner = SchurPreconditioner::None;
2075 assert!(
2076 solver.build_preconditioner(&s, &h_kk)?.is_none(),
2077 "None must leave PCG unpreconditioned"
2078 );
2079
2080 solver.preconditioner = SchurPreconditioner::SchurJacobi;
2081 let blocks_missing = || LinAlgError::InvalidInput("expected blocks".into());
2082 let schur = solver
2083 .build_preconditioner(&s, &h_kk)?
2084 .ok_or_else(blocks_missing)?;
2085 solver.preconditioner = SchurPreconditioner::BlockDiagonal;
2086 let plain = solver
2087 .build_preconditioner(&s, &h_kk)?
2088 .ok_or_else(blocks_missing)?;
2089
2090 let expect_schur = DMatrix::from_row_slice(2, 2, &[2.0, 0.5, 0.5, 3.0])
2092 .try_inverse()
2093 .ok_or_else(|| LinAlgError::InvalidInput("S block must invert".into()))?;
2094 for r in 0..2 {
2095 for c in 0..2 {
2096 assert!(
2097 (schur[0][(r, c)] - expect_schur[(r, c)]).abs() < 1e-12,
2098 "SchurJacobi must invert S's diagonal block"
2099 );
2100 assert!(
2101 (schur[0][(r, c)] - plain[0][(r, c)]).abs() > 1e-9,
2102 "SchurJacobi and BlockDiagonal must not coincide at ({r},{c})"
2103 );
2104 }
2105 }
2106 Ok(())
2107 }
2108 fn solver_with_block(
2111 kept_dof: usize,
2112 dof: usize,
2113 inverse: &[f64],
2114 ) -> Result<(ExplicitSparseSchur, EliminatedBlocks), LinAlgError> {
2115 let kept = (0..kept_dof)
2116 .map(|i| BlockSpan {
2117 key: VarKey::default(),
2118 col_start: i,
2119 dof: 1,
2120 })
2121 .collect();
2122 let eliminated = vec![BlockSpan {
2123 key: VarKey::default(),
2124 col_start: kept_dof,
2125 dof,
2126 }];
2127 let partition = SchurPartition::new(kept, eliminated)?;
2128 let mut blocks = EliminatedBlocks::new(&partition);
2129 blocks.block_mut(0).copy_from_slice(inverse);
2130 let mut solver = ExplicitSparseSchur::new();
2131 solver.partition = Some(partition);
2132 Ok((solver, blocks))
2133 }
2134
2135 #[test]
2138 fn test_compute_schur_complement_known_matrix() -> Result<(), LinAlgError> {
2139 use faer::sparse::Triplet;
2140
2141 let inv = [0.5, 0.0, 0.0, 0.0, 0.5, 0.0, 0.0, 0.0, 0.5];
2143 let (mut solver, blocks) = solver_with_block(2, 3, &inv)?;
2144
2145 let h_kk = SparseColMat::try_new_from_triplets(
2146 2,
2147 2,
2148 &[Triplet::new(0, 0, 4.0), Triplet::new(1, 1, 5.0)],
2149 )
2150 .map_err(|e| LinAlgError::SparseMatrixCreation(format!("{e:?}")))?;
2151 let h_ke = SparseColMat::try_new_from_triplets(
2152 2,
2153 3,
2154 &[Triplet::new(0, 0, 1.0), Triplet::new(1, 1, 2.0)],
2155 )
2156 .map_err(|e| LinAlgError::SparseMatrixCreation(format!("{e:?}")))?;
2157
2158 let (s, _) = solver.compute_schur_complement(&h_kk, &h_ke, &blocks)?;
2159
2160 assert_eq!(s.nrows(), 2);
2161 assert_eq!(s.ncols(), 2);
2162
2163 let dense = dense_of(&s);
2165 assert!((dense[0][0] - 3.5).abs() < 1e-12, "got {}", dense[0][0]);
2166 assert!((dense[1][1] - 3.0).abs() < 1e-12, "got {}", dense[1][1]);
2167 Ok(())
2168 }
2169
2170 #[test]
2171 fn test_back_substitute() -> Result<(), LinAlgError> {
2172 use faer::sparse::Triplet;
2173
2174 let inv = [1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0];
2176 let (solver, blocks) = solver_with_block(2, 3, &inv)?;
2177
2178 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 = SparseColMat::try_new_from_triplets(
2182 2,
2183 3,
2184 &[Triplet::new(0, 0, 1.0), Triplet::new(1, 1, 1.0)],
2185 )
2186 .map_err(|e| LinAlgError::SparseMatrixCreation(format!("{e:?}")))?;
2187
2188 let delta_p = solver.back_substitute(&delta_c, &g_p, &h_cp, &blocks)?;
2193
2194 assert_eq!(delta_p.nrows(), 3);
2195 assert!((delta_p[(0, 0)]).abs() < 1e-10);
2196 assert!((delta_p[(1, 0)]).abs() < 1e-10);
2197 assert!((delta_p[(2, 0)] - 3.0).abs() < 1e-10);
2198 Ok(())
2199 }
2200
2201 #[test]
2202 fn test_compute_reduced_gradient() -> Result<(), LinAlgError> {
2203 use faer::sparse::Triplet;
2204
2205 let inv = [2.0, 0.0, 0.0, 0.0, 2.0, 0.0, 0.0, 0.0, 2.0];
2207 let (solver, blocks) = solver_with_block(2, 3, &inv)?;
2208
2209 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 = SparseColMat::try_new_from_triplets(
2213 2,
2214 3,
2215 &[Triplet::new(0, 0, 1.0), Triplet::new(1, 1, 1.0)],
2216 )
2217 .map_err(|e| LinAlgError::SparseMatrixCreation(format!("{e:?}")))?;
2218
2219 let g_reduced = solver.compute_reduced_gradient(&g_c, &g_p, &h_cp, &blocks)?;
2224
2225 assert_eq!(g_reduced.nrows(), 2);
2226 assert!((g_reduced[(0, 0)] + 1.0).abs() < 1e-10);
2227 assert!((g_reduced[(1, 0)] + 2.0).abs() < 1e-10);
2228 Ok(())
2229 }
2230
2231 #[test]
2237 fn test_solver_default() {
2238 let solver = ExplicitSparseSchur::default();
2239 assert!(solver.partition().is_none());
2240 assert!(solver.hessian.is_none());
2241 assert!(solver.gradient.is_none());
2242 }
2243
2244 #[test]
2246 fn test_partition_getter() -> TestResult {
2247 let (variables, variable_index_map, _, _, landmark_keys) = create_schur_test_setup()?;
2248 let mut solver = ExplicitSparseSchur::new();
2249
2250 assert!(solver.partition().is_none());
2251 solver.initialize_structure(&variables, &variable_index_map, &landmark_keys)?;
2252 assert!(solver.partition().is_some());
2253 Ok(())
2254 }
2255
2256 #[test]
2258 fn test_with_ordering_builder() {
2259 let ordering = SchurOrdering {
2260 eliminate_types: vec![ManifoldType::RN],
2261 eliminate_rn_size: Some(3),
2262 auto_detect: false,
2263 };
2264 let solver = ExplicitSparseSchur::new().with_ordering(ordering);
2265 assert_eq!(solver.ordering.eliminate_rn_size, Some(3));
2266 }
2267 #[test]
2270 fn test_eliminated_block_inversion_3dof() -> TestResult {
2271 let inv_seed = [2.0, 0.0, 0.0, 0.0, 3.0, 0.0, 0.0, 0.0, 4.0];
2273 let kept = vec![BlockSpan {
2274 key: VarKey::default(),
2275 col_start: 0,
2276 dof: 1,
2277 }];
2278 let eliminated = vec![BlockSpan {
2279 key: VarKey::default(),
2280 col_start: 1,
2281 dof: 3,
2282 }];
2283 let partition = SchurPartition::new(kept, eliminated)?;
2284 let mut blocks = EliminatedBlocks::new(&partition);
2285 blocks.block_mut(0).copy_from_slice(&inv_seed);
2286 blocks.invert_in_place(&partition)?;
2287
2288 assert!((blocks.at(0, 0, 0) - 0.5).abs() < 1e-10);
2289 assert!((blocks.at(0, 1, 1) - 1.0 / 3.0).abs() < 1e-10);
2290 assert!((blocks.at(0, 2, 2) - 0.25).abs() < 1e-10);
2291 Ok(())
2292 }
2293
2294 #[test]
2296 fn test_explicit_schur_initialize_structure() -> TestResult {
2297 let (variables, variable_index_map, _, _, landmark_keys) = create_schur_test_setup()?;
2298 let mut solver = ExplicitSparseSchur::new();
2299 solver.initialize_structure(&variables, &variable_index_map, &landmark_keys)?;
2300
2301 let bs = solver.partition().ok_or("partition is None")?;
2302 assert_eq!(bs.kept_blocks().len(), 2);
2303 assert_eq!(bs.eliminated_blocks().len(), 3);
2304 assert_eq!(bs.kept_dof(), 12); assert_eq!(bs.eliminated_dof(), 9); Ok(())
2307 }
2308
2309 #[test]
2311 fn test_extract_gradient_blocks() -> TestResult {
2312 let (variables, variable_index_map, _, _, landmark_keys) = create_schur_test_setup()?;
2313 let mut solver = ExplicitSparseSchur::new();
2314 solver.initialize_structure(&variables, &variable_index_map, &landmark_keys)?;
2315
2316 let gradient = Mat::from_fn(21, 1, |i, _| i as f64);
2318 let (g_c, g_p) = solver.extract_gradient_blocks(&gradient)?;
2319
2320 assert_eq!(g_c.nrows(), 12); assert_eq!(g_p.nrows(), 9); Ok(())
2323 }
2324
2325 #[test]
2327 fn test_explicit_schur_solve_normal_equation() -> TestResult {
2328 let (variables, variable_index_map, jacobian, residuals, landmark_keys) =
2329 create_schur_test_setup()?;
2330 let mut solver = ExplicitSparseSchur::new().with_variant(ExplicitSchurVariant::Sparse);
2331 solver.initialize_structure(&variables, &variable_index_map, &landmark_keys)?;
2332
2333 let delta =
2334 LinearSolver::<SparseMode>::solve_normal_equation(&mut solver, &residuals, &jacobian)?;
2335 assert_eq!(delta.nrows(), 21);
2336 assert_eq!(delta.ncols(), 1);
2337 Ok(())
2338 }
2339
2340 #[test]
2342 fn test_explicit_schur_solve_augmented_equation() -> TestResult {
2343 let (variables, variable_index_map, jacobian, residuals, landmark_keys) =
2344 create_schur_test_setup()?;
2345 let mut solver = ExplicitSparseSchur::new().with_variant(ExplicitSchurVariant::Sparse);
2346 solver.initialize_structure(&variables, &variable_index_map, &landmark_keys)?;
2347
2348 let delta = LinearSolver::<SparseMode>::solve_augmented_equation(
2349 &mut solver,
2350 &residuals,
2351 &jacobian,
2352 &Damping::identity(0.1),
2353 )?;
2354 assert_eq!(delta.nrows(), 21);
2355 Ok(())
2356 }
2357
2358 #[test]
2362 fn test_explicit_schur_solve_iterative_variant() -> TestResult {
2363 let (variables, variable_index_map, jacobian, residuals, landmark_keys) =
2364 create_schur_test_setup()?;
2365 let mut solver = ExplicitSparseSchur::new()
2366 .with_variant(ExplicitSchurVariant::Iterative)
2367 .with_cg_params(200, 1e-10);
2368 solver.initialize_structure(&variables, &variable_index_map, &landmark_keys)?;
2369
2370 let delta =
2371 LinearSolver::<SparseMode>::solve_normal_equation(&mut solver, &residuals, &jacobian)?;
2372 assert_eq!(delta.nrows(), 21);
2373
2374 let mut cholesky_solver =
2375 ExplicitSparseSchur::new().with_variant(ExplicitSchurVariant::Sparse);
2376 cholesky_solver.initialize_structure(&variables, &variable_index_map, &landmark_keys)?;
2377 let cholesky_delta = LinearSolver::<SparseMode>::solve_normal_equation(
2378 &mut cholesky_solver,
2379 &residuals,
2380 &jacobian,
2381 )?;
2382
2383 for i in 0..21 {
2384 assert!(
2385 (delta[(i, 0)] - cholesky_delta[(i, 0)]).abs() < 1e-4,
2386 "PCG-on-S must agree with Cholesky-on-S at {i}: {} vs {}",
2387 delta[(i, 0)],
2388 cholesky_delta[(i, 0)]
2389 );
2390 }
2391 Ok(())
2392 }
2393
2394 #[test]
2396 fn test_explicit_schur_get_hessian_gradient() -> TestResult {
2397 let (variables, variable_index_map, jacobian, residuals, landmark_keys) =
2398 create_schur_test_setup()?;
2399 let mut solver = ExplicitSparseSchur::new();
2400 solver.initialize_structure(&variables, &variable_index_map, &landmark_keys)?;
2401
2402 assert!(LinearSolver::<SparseMode>::get_hessian(&solver).is_none());
2403 assert!(LinearSolver::<SparseMode>::get_gradient(&solver).is_none());
2404
2405 LinearSolver::<SparseMode>::solve_normal_equation(&mut solver, &residuals, &jacobian)?;
2406
2407 let h = LinearSolver::<SparseMode>::get_hessian(&solver);
2408 let g = LinearSolver::<SparseMode>::get_gradient(&solver);
2409 assert!(h.is_some());
2410 assert!(g.is_some());
2411 let h = h.ok_or("hessian is None")?;
2412 let g = g.ok_or("gradient is None")?;
2413 assert_eq!(h.nrows(), 21);
2414 assert_eq!(g.nrows(), 21);
2415 Ok(())
2416 }
2417
2418 #[test]
2420 fn test_explicit_schur_augmented_lambda_effect() -> TestResult {
2421 let (variables, variable_index_map, jacobian, residuals, landmark_keys) =
2422 create_schur_test_setup()?;
2423
2424 let mut solver1 = ExplicitSparseSchur::new();
2425 solver1.initialize_structure(&variables, &variable_index_map, &landmark_keys)?;
2426 let delta1 = LinearSolver::<SparseMode>::solve_augmented_equation(
2427 &mut solver1,
2428 &residuals,
2429 &jacobian,
2430 &Damping::identity(0.001),
2431 )?;
2432
2433 let mut solver2 = ExplicitSparseSchur::new();
2434 solver2.initialize_structure(&variables, &variable_index_map, &landmark_keys)?;
2435 let delta2 = LinearSolver::<SparseMode>::solve_augmented_equation(
2436 &mut solver2,
2437 &residuals,
2438 &jacobian,
2439 &Damping::identity(100.0),
2440 )?;
2441
2442 let norm_diff: f64 = (0..21)
2444 .map(|i| (delta1[(i, 0)] - delta2[(i, 0)]).powi(2))
2445 .sum();
2446 assert!(
2447 norm_diff > 1e-10,
2448 "Different λ should yield different updates"
2449 );
2450 Ok(())
2451 }
2452
2453 #[test]
2455 fn test_combine_updates() -> TestResult {
2456 let (variables, variable_index_map, _, _, landmark_keys) = create_schur_test_setup()?;
2457 let mut solver = ExplicitSparseSchur::new();
2458 solver.initialize_structure(&variables, &variable_index_map, &landmark_keys)?;
2459
2460 let delta_c = Mat::from_fn(12, 1, |_, _| 1.0);
2462 let delta_p = Mat::from_fn(9, 1, |_, _| 2.0);
2463
2464 let combined = solver.combine_updates(&delta_c, &delta_p)?;
2465 assert_eq!(combined.nrows(), 21);
2466
2467 for i in 0..12 {
2469 assert!((combined[(i, 0)] - 1.0).abs() < 1e-10);
2470 }
2471 for i in 12..21 {
2473 assert!((combined[(i, 0)] - 2.0).abs() < 1e-10);
2474 }
2475 Ok(())
2476 }
2477
2478 #[test]
2480 fn test_explicit_schur_solve_without_init_returns_error() -> TestResult {
2481 let triplets: Vec<Triplet<usize, usize, f64>> = vec![Triplet::new(0, 0, 1.0)];
2482 let jacobian =
2483 SparseColMat::try_new_from_triplets(1, 1, &triplets).map_err(|e| format!("{e:?}"))?;
2484 let residuals = Mat::from_fn(1, 1, |_, _| 1.0);
2485 let mut solver = ExplicitSparseSchur::new();
2486
2487 let result =
2488 LinearSolver::<SparseMode>::solve_normal_equation(&mut solver, &residuals, &jacobian);
2489 assert!(result.is_err());
2490 Ok(())
2491 }
2492
2493 #[test]
2499 fn test_schur_ordering_new_equals_default() {
2500 let a = SchurOrdering::new();
2501 let b = SchurOrdering::default();
2502 assert_eq!(a.eliminate_rn_size, b.eliminate_rn_size);
2503 assert_eq!(a.eliminate_types.len(), b.eliminate_types.len());
2504 }
2505
2506 #[test]
2508 fn test_extract_camera_block() -> TestResult {
2509 let (variables, variable_index_map, jacobian, residuals, landmark_keys) =
2510 create_schur_test_setup()?;
2511 let mut solver = ExplicitSparseSchur::new();
2512 solver.initialize_structure(&variables, &variable_index_map, &landmark_keys)?;
2513
2514 LinearSolver::<SparseMode>::solve_normal_equation(&mut solver, &residuals, &jacobian)?;
2516 let hessian = solver.hessian.clone().ok_or("hessian is None")?;
2517
2518 let mut fresh = ExplicitSparseSchur::new();
2519 fresh.initialize_structure(&variables, &variable_index_map, &landmark_keys)?;
2520 let h_cc = fresh.extract_kept_block(&hessian)?;
2521
2522 assert_eq!(h_cc.nrows(), 12);
2524 assert_eq!(h_cc.ncols(), 12);
2525 Ok(())
2526 }
2527
2528 #[test]
2530 fn test_extract_coupling_block() -> TestResult {
2531 let (variables, variable_index_map, jacobian, residuals, landmark_keys) =
2532 create_schur_test_setup()?;
2533 let mut solver = ExplicitSparseSchur::new();
2534 solver.initialize_structure(&variables, &variable_index_map, &landmark_keys)?;
2535
2536 LinearSolver::<SparseMode>::solve_normal_equation(&mut solver, &residuals, &jacobian)?;
2537 let hessian = solver.hessian.clone().ok_or("hessian is None")?;
2538
2539 let mut fresh = ExplicitSparseSchur::new();
2540 fresh.initialize_structure(&variables, &variable_index_map, &landmark_keys)?;
2541 let h_cp = fresh.extract_coupling_block(&hessian)?;
2542
2543 assert_eq!(h_cp.nrows(), 12);
2545 assert_eq!(h_cp.ncols(), 9);
2546 Ok(())
2547 }
2548
2549 #[test]
2551 fn test_gather_eliminated_blocks() -> TestResult {
2552 let (variables, variable_index_map, jacobian, residuals, landmark_keys) =
2553 create_schur_test_setup()?;
2554 let mut solver = ExplicitSparseSchur::new();
2555 solver.initialize_structure(&variables, &variable_index_map, &landmark_keys)?;
2556
2557 LinearSolver::<SparseMode>::solve_normal_equation(&mut solver, &residuals, &jacobian)?;
2558 let hessian = solver.hessian.clone().ok_or("hessian is None")?;
2559
2560 let mut fresh = ExplicitSparseSchur::new();
2561 fresh.initialize_structure(&variables, &variable_index_map, &landmark_keys)?;
2562 let partition = fresh.partition().ok_or("partition is None")?;
2563 let mut blocks = EliminatedBlocks::new(partition);
2564 blocks.gather(&hessian, partition);
2565
2566 assert_eq!(blocks.len(), 3);
2568 for i in 0..blocks.len() {
2569 assert_eq!(blocks.dof(i), 3);
2570 }
2571 Ok(())
2572 }
2573
2574 #[test]
2582 fn schur_matches_cholesky_on_assembled_pose_chain() -> TestResult {
2583 use crate::core::problem::Problem;
2584 use crate::factors::pose::{BetweenFactor, PriorFactor};
2585 use crate::linalg::JacobianMode;
2586 use crate::linalg::sparse::cholesky::SparseCholeskySolver;
2587 use apex_manifolds::{ManifoldType, se2::SE2};
2588 use nalgebra::dvector;
2589
2590 let mut problem = Problem::new(JacobianMode::Sparse);
2591 let mut keys = Vec::new();
2592 for i in 0..5 {
2593 keys.push(
2594 problem.add_variable(ManifoldType::SE2, dvector![i as f64 + 0.1, 0.05, 0.02]),
2595 );
2596 }
2597 for w in keys.windows(2) {
2598 problem.add_residual_block(
2599 &[w[0], w[1]],
2600 Box::new(BetweenFactor::new(SE2::from_xy_angle(1.0, 0.0, 0.0))),
2601 None,
2602 );
2603 }
2604 problem.add_residual_block(
2605 &[keys[0]],
2606 Box::new(PriorFactor::new(SE2::from_xy_angle(0.0, 0.0, 0.0))),
2607 None,
2608 );
2609 problem.mark_for_elimination(keys[1]);
2610 problem.mark_for_elimination(keys[3]);
2611 problem.group_rows_for_elimination();
2612
2613 let state = crate::optimizer::initialize_optimization_state(&mut problem)?;
2614 let symbolic = state.symbolic_structure.as_ref().ok_or("sparse symbolic")?;
2615 let mut workspace = crate::linearizer::AssemblyWorkspace::build(&problem);
2616 let (residuals, jacobian) = crate::linearizer::cpu::sparse::assemble_sparse(
2617 &problem,
2618 &state.variables,
2619 &state.variable_index_map,
2620 symbolic,
2621 &mut workspace,
2622 )?;
2623
2624 let mut eliminate = std::collections::HashSet::new();
2625 eliminate.insert(keys[1]);
2626 eliminate.insert(keys[3]);
2627
2628 let mut cholesky = SparseCholeskySolver::new();
2629 let reference =
2630 crate::linalg::LinearSolver::<crate::linalg::SparseMode>::solve_normal_equation(
2631 &mut cholesky,
2632 &residuals,
2633 &jacobian,
2634 )?;
2635
2636 let mut schur = ExplicitSparseSchur::new();
2637 schur.initialize_structure(&state.variables, &state.variable_index_map, &eliminate)?;
2638 let first =
2639 crate::linalg::LinearSolver::<crate::linalg::SparseMode>::solve_normal_equation(
2640 &mut schur, &residuals, &jacobian,
2641 )?;
2642 let second =
2643 crate::linalg::LinearSolver::<crate::linalg::SparseMode>::solve_normal_equation(
2644 &mut schur, &residuals, &jacobian,
2645 )?;
2646
2647 let scale = reference.norm_l2().max(1.0);
2648 for (label, step) in [("first", &first), ("second", &second)] {
2649 assert_eq!(reference.nrows(), step.nrows(), "{label}: length");
2650 for i in 0..reference.nrows() {
2651 let diff = (reference[(i, 0)] - step[(i, 0)]).abs();
2652 assert!(
2653 diff / scale < 1e-9,
2654 "{label}: component {i} differs — cholesky {}, schur {} (rel {:.3e})",
2655 reference[(i, 0)],
2656 step[(i, 0)],
2657 diff / scale
2658 );
2659 }
2660 }
2661 Ok(())
2662 }
2663
2664 #[test]
2669 fn schur_tracks_cholesky_across_iterations() -> TestResult {
2670 use crate::core::problem::Problem;
2671 use crate::factors::pose::{BetweenFactor, PriorFactor};
2672 use crate::linalg::JacobianMode;
2673 use crate::linalg::sparse::cholesky::SparseCholeskySolver;
2674 use apex_manifolds::{ManifoldType, se2::SE2};
2675 use nalgebra::dvector;
2676
2677 let mut problem = Problem::new(JacobianMode::Sparse);
2678 let mut keys = Vec::new();
2679 for i in 0..5 {
2680 keys.push(
2681 problem.add_variable(ManifoldType::SE2, dvector![i as f64 + 0.1, 0.05, 0.02]),
2682 );
2683 }
2684 for w in keys.windows(2) {
2685 problem.add_residual_block(
2686 &[w[0], w[1]],
2687 Box::new(BetweenFactor::new(SE2::from_xy_angle(1.0, 0.0, 0.0))),
2688 None,
2689 );
2690 }
2691 problem.add_residual_block(
2692 &[keys[0]],
2693 Box::new(PriorFactor::new(SE2::from_xy_angle(0.0, 0.0, 0.0))),
2694 None,
2695 );
2696 problem.mark_for_elimination(keys[1]);
2697 problem.mark_for_elimination(keys[3]);
2698 problem.group_rows_for_elimination();
2699
2700 let mut state = crate::optimizer::initialize_optimization_state(&mut problem)?;
2701 let mut eliminate = std::collections::HashSet::new();
2702 eliminate.insert(keys[1]);
2703 eliminate.insert(keys[3]);
2704
2705 let mut cholesky = SparseCholeskySolver::new();
2706 let mut schur = ExplicitSparseSchur::new();
2707 schur.initialize_structure(&state.variables, &state.variable_index_map, &eliminate)?;
2708
2709 for iter in 0..3 {
2710 let symbolic = state.symbolic_structure.as_ref().ok_or("sparse symbolic")?;
2711 let mut workspace = crate::linearizer::AssemblyWorkspace::build(&problem);
2712 let (residuals, jacobian) = crate::linearizer::cpu::sparse::assemble_sparse(
2713 &problem,
2714 &state.variables,
2715 &state.variable_index_map,
2716 symbolic,
2717 &mut workspace,
2718 )?;
2719 let reference =
2720 crate::linalg::LinearSolver::<crate::linalg::SparseMode>::solve_normal_equation(
2721 &mut cholesky,
2722 &residuals,
2723 &jacobian,
2724 )?;
2725 let step =
2726 crate::linalg::LinearSolver::<crate::linalg::SparseMode>::solve_normal_equation(
2727 &mut schur, &residuals, &jacobian,
2728 )?;
2729 let scale = reference.norm_l2().max(1.0);
2730 let mut max_rel: f64 = 0.0;
2731 for i in 0..reference.nrows() {
2732 max_rel = max_rel.max((reference[(i, 0)] - step[(i, 0)]).abs() / scale);
2733 }
2734 assert!(
2735 max_rel < 1e-9,
2736 "iteration {iter}: Schur step diverges from Cholesky (max rel {max_rel:.3e})"
2737 );
2738 crate::optimizer::apply_parameter_step(
2739 &mut state.variables,
2740 step.as_ref(),
2741 &state.sorted_vars,
2742 );
2743 }
2744 Ok(())
2745 }
2746
2747 #[test]
2752 fn schur_tracks_cholesky_with_shared_workspace() -> TestResult {
2753 use crate::core::problem::Problem;
2754 use crate::factors::pose::{BetweenFactor, PriorFactor};
2755 use crate::linalg::JacobianMode;
2756 use crate::linalg::sparse::cholesky::SparseCholeskySolver;
2757 use apex_manifolds::{ManifoldType, se2::SE2};
2758 use nalgebra::dvector;
2759
2760 let mut problem = Problem::new(JacobianMode::Sparse);
2761 let mut keys = Vec::new();
2762 for i in 0..5 {
2763 keys.push(
2764 problem.add_variable(ManifoldType::SE2, dvector![i as f64 + 0.1, 0.05, 0.02]),
2765 );
2766 }
2767 for w in keys.windows(2) {
2768 problem.add_residual_block(
2769 &[w[0], w[1]],
2770 Box::new(BetweenFactor::new(SE2::from_xy_angle(1.0, 0.0, 0.0))),
2771 None,
2772 );
2773 }
2774 problem.add_residual_block(
2775 &[keys[0]],
2776 Box::new(PriorFactor::new(SE2::from_xy_angle(0.0, 0.0, 0.0))),
2777 None,
2778 );
2779 problem.mark_for_elimination(keys[1]);
2780 problem.mark_for_elimination(keys[3]);
2781 problem.group_rows_for_elimination();
2782
2783 let mut state = crate::optimizer::initialize_optimization_state(&mut problem)?;
2784 let mut eliminate = std::collections::HashSet::new();
2785 eliminate.insert(keys[1]);
2786 eliminate.insert(keys[3]);
2787
2788 let costs = |variables: &slotmap::SlotMap<
2791 crate::core::VarKey,
2792 Box<dyn crate::core::variable::ManifoldVariable>,
2793 >|
2794 -> Result<f64, Box<dyn std::error::Error>> {
2795 Ok(problem.compute_residual_and_cost_sparse(variables)?.1)
2796 };
2797
2798 let mut cholesky = SparseCholeskySolver::new();
2799 let mut schur = ExplicitSparseSchur::new();
2800 schur.initialize_structure(&state.variables, &state.variable_index_map, &eliminate)?;
2801
2802 for iter in 0..3 {
2803 let symbolic = state.symbolic_structure.as_ref().ok_or("sparse symbolic")?;
2804 let (residuals, jacobian) = crate::linearizer::cpu::sparse::assemble_sparse(
2805 &problem,
2806 &state.variables,
2807 &state.variable_index_map,
2808 symbolic,
2809 &mut state.workspace,
2810 )?;
2811 let reference =
2812 crate::linalg::LinearSolver::<crate::linalg::SparseMode>::solve_normal_equation(
2813 &mut cholesky,
2814 &residuals,
2815 &jacobian,
2816 )?;
2817 let step =
2818 crate::linalg::LinearSolver::<crate::linalg::SparseMode>::solve_normal_equation(
2819 &mut schur, &residuals, &jacobian,
2820 )?;
2821 let scale = reference.norm_l2().max(1.0);
2822 let mut max_rel: f64 = 0.0;
2823 for i in 0..reference.nrows() {
2824 max_rel = max_rel.max((reference[(i, 0)] - step[(i, 0)]).abs() / scale);
2825 }
2826 assert!(
2827 max_rel < 1e-9,
2828 "iteration {iter}: Schur step diverges from Cholesky (max rel {max_rel:.3e})"
2829 );
2830 crate::optimizer::apply_parameter_step(
2831 &mut state.variables,
2832 reference.as_ref(),
2833 &state.sorted_vars,
2834 );
2835 let _ = costs(&state.variables)?;
2836 }
2837 Ok(())
2838 }
2839
2840 #[test]
2841 fn test_solve_with_cholesky_small_spd() -> TestResult {
2842 let mut solver = ExplicitSparseSchur::new();
2843
2844 let triplets = vec![
2846 Triplet::new(0usize, 0usize, 4.0f64),
2847 Triplet::new(1usize, 0usize, 1.0f64),
2848 Triplet::new(0usize, 1usize, 1.0f64),
2849 Triplet::new(1usize, 1usize, 3.0f64),
2850 ];
2851 let a =
2852 SparseColMat::try_new_from_triplets(2, 2, &triplets).map_err(|e| format!("{e:?}"))?;
2853 let b = Mat::from_fn(2, 1, |i, _| (i + 1) as f64); let x = solver.solve_with_cholesky(&a, &b, None)?;
2856 assert_eq!(x.nrows(), 2);
2857
2858 let ax0 = 4.0 * x[(0, 0)] + 1.0 * x[(1, 0)];
2861 let ax1 = 1.0 * x[(0, 0)] + 3.0 * x[(1, 0)];
2862 assert!((ax0 - 1.0).abs() < 1e-8, "A·x[0] = {ax0}");
2863 assert!((ax1 - 2.0).abs() < 1e-8, "A·x[1] = {ax1}");
2864 Ok(())
2865 }
2866
2867 #[test]
2869 fn test_solve_with_pcg_diagonal_system() -> TestResult {
2870 let inv = [1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0];
2873 let (solver, _) = solver_with_block(2, 3, &inv)?;
2874
2875 let triplets = vec![
2877 Triplet::new(0usize, 0usize, 2.0f64),
2878 Triplet::new(1usize, 1usize, 3.0f64),
2879 ];
2880 let a =
2881 SparseColMat::try_new_from_triplets(2, 2, &triplets).map_err(|e| format!("{e:?}"))?;
2882 let b = Mat::from_fn(2, 1, |i, _| (i + 1) as f64); let x = solver.solve_with_pcg(&a, &b, &a)?;
2885 assert!((x[(0, 0)] - 0.5).abs() < 1e-6, "x[0] = {}", x[(0, 0)]);
2887 assert!((x[(1, 0)] - 2.0 / 3.0).abs() < 1e-6, "x[1] = {}", x[(1, 0)]);
2888 Ok(())
2889 }
2890
2891 #[test]
2893 fn test_initialize_structure_no_cameras_returns_error() {
2894 use crate::core::variable::Variable;
2895 use apex_manifolds::rn;
2896 use nalgebra::DVector;
2897
2898 let mut variables: SlotMap<VarKey, Box<dyn ManifoldVariable>> = SlotMap::with_key();
2899 let k = variables.insert(Box::new(Variable::new(rn::Rn::new(DVector::zeros(3)))));
2900 let mut variable_index_map: SecondaryMap<VarKey, usize> = SecondaryMap::new();
2901 variable_index_map.insert(k, 0);
2902 let mut landmark_keys = std::collections::HashSet::new();
2903 landmark_keys.insert(k);
2904
2905 let mut solver = ExplicitSparseSchur::new();
2906 let result = solver.initialize_structure(&variables, &variable_index_map, &landmark_keys);
2907 assert!(
2908 result.is_err(),
2909 "Expected Err when no camera variables present"
2910 );
2911 }
2912
2913 #[test]
2915 fn test_initialize_structure_no_landmarks_returns_error() {
2916 use crate::core::variable::Variable;
2917 use apex_manifolds::se3;
2918
2919 let mut variables: SlotMap<VarKey, Box<dyn ManifoldVariable>> = SlotMap::with_key();
2920 let _k = variables.insert(Box::new(Variable::new(se3::SE3::from_param_slice(&[
2921 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
2922 ]))));
2923 let mut variable_index_map: SecondaryMap<VarKey, usize> = SecondaryMap::new();
2924 variable_index_map.insert(_k, 0);
2925 let landmark_keys = std::collections::HashSet::<VarKey>::new(); let mut solver = ExplicitSparseSchur::new();
2928 let result = solver.initialize_structure(&variables, &variable_index_map, &landmark_keys);
2929 assert!(
2930 result.is_err(),
2931 "Expected Err when no landmark variables present"
2932 );
2933 }
2934
2935 fn dense_of(m: &SparseColMat<usize, f64>) -> Vec<Vec<f64>> {
2937 let mut out = vec![vec![0.0; m.ncols()]; m.nrows()];
2938 for (col, out_col) in (0..m.ncols()).zip(0..m.ncols()) {
2939 let rows = m.symbolic().row_idx_of_col_raw(col);
2940 let vals = m.val_of_col(col);
2941 for (i, &r) in rows.iter().enumerate() {
2942 out[r][out_col] = vals[i];
2943 }
2944 }
2945 out
2946 }
2947
2948 #[test]
2951 fn test_damp_camera_block_full_diagonal() -> TestResult {
2952 let triplets = vec![
2953 Triplet::new(0usize, 0usize, 4.0_f64),
2954 Triplet::new(1, 0, 1.0),
2955 Triplet::new(0, 1, 1.0),
2956 Triplet::new(1, 1, 9.0),
2957 ];
2958 let h_cc = SparseColMat::try_new_from_triplets(2, 2, &triplets)?;
2959 let damping = Damping::new(0.5, 1e-6, 1e32)?;
2960
2961 let damped = dense_of(&damp_camera_block(&h_cc, 2, &damping)?);
2962
2963 assert!((damped[0][0] - (4.0 + 0.5 * 4.0)).abs() < 1e-12);
2964 assert!((damped[1][1] - (9.0 + 0.5 * 9.0)).abs() < 1e-12);
2965 assert!((damped[0][1] - 1.0).abs() < 1e-12, "off-diagonal changed");
2966 assert!((damped[1][0] - 1.0).abs() < 1e-12, "off-diagonal changed");
2967 Ok(())
2968 }
2969
2970 #[test]
2974 fn test_damp_camera_block_missing_diagonal() -> TestResult {
2975 let triplets = vec![
2977 Triplet::new(0usize, 0usize, 4.0_f64),
2978 Triplet::new(0, 1, 2.0),
2979 ];
2980 let h_cc = SparseColMat::try_new_from_triplets(2, 2, &triplets)?;
2981 let damping = Damping::new(0.5, 1e-3, 1e32)?;
2982
2983 let damped = dense_of(&damp_camera_block(&h_cc, 2, &damping)?);
2984
2985 assert!((damped[0][0] - (4.0 + 0.5 * 4.0)).abs() < 1e-12);
2986 assert!((damped[1][1] - 0.5 * 1e-3).abs() < 1e-12);
2988 assert!((damped[0][1] - 2.0).abs() < 1e-12, "off-diagonal changed");
2989 Ok(())
2990 }
2991}