1use crate::core::problem::VariableEnum;
44use crate::linalg::{
45 LinAlgError, LinAlgResult, StructuredSparseLinearSolver, implicit_schur::IterativeSchurSolver,
46};
47use crate::manifold::ManifoldType;
48use faer::sparse::{SparseColMat, Triplet};
49use faer::{
50 Mat, Side,
51 linalg::solvers::Solve,
52 sparse::linalg::solvers::{Llt, SymbolicLlt},
53};
54use nalgebra::Matrix3;
55use std::collections::HashMap;
56use tracing::{debug, info};
57
58#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
60pub enum SchurVariant {
61 #[default]
63 Sparse,
64 Iterative,
66}
67
68#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
70pub enum SchurPreconditioner {
71 None,
73 BlockDiagonal,
75 #[default]
78 SchurJacobi,
79}
80
81#[derive(Debug, Clone)]
83pub struct SchurOrdering {
84 pub eliminate_types: Vec<ManifoldType>,
85 pub eliminate_rn_size: Option<usize>,
88}
89
90impl Default for SchurOrdering {
91 fn default() -> Self {
92 Self {
93 eliminate_types: vec![ManifoldType::RN],
94 eliminate_rn_size: Some(3), }
96 }
97}
98
99impl SchurOrdering {
100 pub fn new() -> Self {
101 Self::default()
102 }
103
104 pub fn should_eliminate(&self, name: &str, manifold_type: &ManifoldType, size: usize) -> bool {
113 if name.starts_with("pt_") {
115 if !self.eliminate_types.contains(manifold_type) {
117 panic!(
118 "Landmark {} has manifold type {:?}, expected RN",
119 name, manifold_type
120 );
121 }
122
123 if self
125 .eliminate_rn_size
126 .is_some_and(|required_size| size != required_size)
127 {
128 panic!(
129 "Landmark {} has {} DOF, expected {}",
130 name,
131 size,
132 self.eliminate_rn_size.unwrap_or(0)
133 );
134 }
135 true
136 } else {
137 false
139 }
140 }
141}
142
143#[derive(Debug, Clone)]
145pub struct SchurBlockStructure {
146 pub camera_blocks: Vec<(String, usize, usize)>,
147 pub landmark_blocks: Vec<(String, usize, usize)>,
148 pub camera_dof: usize,
149 pub landmark_dof: usize,
150 pub num_landmarks: usize,
151}
152
153impl SchurBlockStructure {
154 pub fn new() -> Self {
155 Self {
156 camera_blocks: Vec::new(),
157 landmark_blocks: Vec::new(),
158 camera_dof: 0,
159 landmark_dof: 0,
160 num_landmarks: 0,
161 }
162 }
163
164 pub fn camera_col_range(&self) -> (usize, usize) {
165 if self.camera_blocks.is_empty() {
166 (0, 0)
167 } else {
168 let start = self.camera_blocks.first().map(|b| b.1).unwrap_or(0);
170 (start, start + self.camera_dof)
171 }
172 }
173
174 pub fn landmark_col_range(&self) -> (usize, usize) {
175 if self.landmark_blocks.is_empty() {
176 (0, 0)
177 } else {
178 let start = self.landmark_blocks.first().map(|b| b.1).unwrap_or(0);
180 (start, start + self.landmark_dof)
181 }
182 }
183}
184
185impl Default for SchurBlockStructure {
186 fn default() -> Self {
187 Self::new()
188 }
189}
190
191#[derive(Debug, Clone)]
193pub struct SparseSchurComplementSolver {
194 block_structure: Option<SchurBlockStructure>,
195 ordering: SchurOrdering,
196 variant: SchurVariant,
197 preconditioner: SchurPreconditioner,
198
199 cg_max_iterations: usize,
201 cg_tolerance: f64,
202
203 hessian: Option<SparseColMat<usize, f64>>,
205 gradient: Option<Mat<f64>>,
206
207 iterative_solver: Option<IterativeSchurSolver>,
209}
210
211impl SparseSchurComplementSolver {
212 pub fn new() -> Self {
213 Self {
214 block_structure: None,
215 ordering: SchurOrdering::default(),
216 variant: SchurVariant::default(),
217 preconditioner: SchurPreconditioner::default(),
218 cg_max_iterations: 200, cg_tolerance: 1e-6, hessian: None,
221 gradient: None,
222 iterative_solver: None,
223 }
224 }
225
226 pub fn with_ordering(mut self, ordering: SchurOrdering) -> Self {
227 self.ordering = ordering;
228 self
229 }
230
231 pub fn with_variant(mut self, variant: SchurVariant) -> Self {
232 self.variant = variant;
233 self
234 }
235
236 pub fn with_preconditioner(mut self, preconditioner: SchurPreconditioner) -> Self {
237 self.preconditioner = preconditioner;
238 self
239 }
240
241 pub fn with_cg_params(mut self, max_iter: usize, tol: f64) -> Self {
242 self.cg_max_iterations = max_iter;
243 self.cg_tolerance = tol;
244 self
245 }
246
247 pub fn block_structure(&self) -> Option<&SchurBlockStructure> {
248 self.block_structure.as_ref()
249 }
250
251 fn build_block_structure(
252 &mut self,
253 variables: &HashMap<String, VariableEnum>,
254 variable_index_map: &HashMap<String, usize>,
255 ) -> LinAlgResult<()> {
256 let mut structure = SchurBlockStructure::new();
257
258 for (name, variable) in variables {
259 let start_col = *variable_index_map.get(name).ok_or_else(|| {
260 LinAlgError::InvalidInput(format!("Variable {} not found in index map", name))
261 })?;
262 let size = variable.get_size();
263 let manifold_type = match variable {
264 VariableEnum::SE3(_) => ManifoldType::SE3,
265 VariableEnum::SE2(_) => ManifoldType::SE2,
266 VariableEnum::SO3(_) => ManifoldType::SO3,
267 VariableEnum::SO2(_) => ManifoldType::SO2,
268 VariableEnum::Rn(_) => ManifoldType::RN,
269 };
270
271 if self.ordering.should_eliminate(name, &manifold_type, size) {
273 structure
275 .landmark_blocks
276 .push((name.clone(), start_col, size));
277 structure.landmark_dof += size;
278 structure.num_landmarks += 1;
279 } else {
280 structure
282 .camera_blocks
283 .push((name.clone(), start_col, size));
284 structure.camera_dof += size;
285 }
286 }
287
288 structure.camera_blocks.sort_by_key(|(_, col, _)| *col);
289 structure.landmark_blocks.sort_by_key(|(_, col, _)| *col);
290
291 if structure.camera_blocks.is_empty() {
292 return Err(LinAlgError::InvalidInput(
293 "No camera variables found".to_string(),
294 ));
295 }
296 if structure.landmark_blocks.is_empty() {
297 return Err(LinAlgError::InvalidInput(
298 "No landmark variables found".to_string(),
299 ));
300 }
301
302 info!("Schur complement block structure:");
304 info!(
305 " Camera blocks: {} variables, {} total DOF",
306 structure.camera_blocks.len(),
307 structure.camera_dof
308 );
309 info!(
310 " Landmark blocks: {} variables, {} total DOF",
311 structure.landmark_blocks.len(),
312 structure.landmark_dof
313 );
314 debug!(" Camera column range: {:?}", structure.camera_col_range());
315 debug!(
316 " Landmark column range: {:?}",
317 structure.landmark_col_range()
318 );
319 info!(
320 " Schur complement S size: {} × {}",
321 structure.camera_dof, structure.camera_dof
322 );
323
324 let (_cam_start, cam_end) = structure.camera_col_range();
326 let (land_start, _land_end) = structure.landmark_col_range();
327 if cam_end != land_start {
328 debug!(
329 "WARNING: Gap between camera and landmark blocks! cam_end={}, land_start={}",
330 cam_end, land_start
331 );
332 }
333
334 self.block_structure = Some(structure);
335 Ok(())
336 }
337
338 fn extract_landmark_blocks(
340 &self,
341 hessian: &SparseColMat<usize, f64>,
342 ) -> LinAlgResult<Vec<Matrix3<f64>>> {
343 let structure = self
344 .block_structure
345 .as_ref()
346 .ok_or_else(|| LinAlgError::InvalidInput("Block structure not built".to_string()))?;
347
348 let mut blocks = Vec::with_capacity(structure.num_landmarks);
349 let symbolic = hessian.symbolic();
350
351 for (_, start_col, _) in &structure.landmark_blocks {
352 let mut block = Matrix3::<f64>::zeros();
353
354 for local_col in 0..3 {
355 let global_col = start_col + local_col;
356 let row_indices = symbolic.row_idx_of_col_raw(global_col);
357 let col_values = hessian.val_of_col(global_col);
358
359 for (idx, &row) in row_indices.iter().enumerate() {
360 if row >= *start_col && row < start_col + 3 {
361 let local_row = row - start_col;
362 block[(local_row, local_col)] = col_values[idx];
363 }
364 }
365 }
366
367 blocks.push(block);
368 }
369
370 Ok(blocks)
371 }
372
373 fn invert_landmark_blocks(blocks: &[Matrix3<f64>]) -> LinAlgResult<Vec<Matrix3<f64>>> {
379 Self::invert_landmark_blocks_with_lambda(blocks, 0.0)
380 }
381
382 fn invert_landmark_blocks_with_lambda(
391 blocks: &[Matrix3<f64>],
392 lambda: f64,
393 ) -> LinAlgResult<Vec<Matrix3<f64>>> {
394 const CONDITION_THRESHOLD: f64 = 1e10; const MIN_EIGENVALUE_THRESHOLD: f64 = 1e-12; const REGULARIZATION_SCALE: f64 = 1e-6; let mut ill_conditioned_count = 0;
400 let mut regularized_count = 0;
401
402 let result: LinAlgResult<Vec<Matrix3<f64>>> = blocks
403 .iter()
404 .enumerate()
405 .map(|(i, block)| {
406 let eigenvalues = block.symmetric_eigenvalues();
409 let min_ev = eigenvalues.min();
410 let max_ev = eigenvalues.max();
411
412 if min_ev < MIN_EIGENVALUE_THRESHOLD {
413 regularized_count += 1;
415 let reg = lambda.max(REGULARIZATION_SCALE) + max_ev * REGULARIZATION_SCALE;
416 let regularized = block + Matrix3::identity() * reg;
417 regularized.try_inverse().ok_or_else(|| {
418 LinAlgError::SingularMatrix(format!(
419 "Landmark block {} singular even with regularization (min_ev={:.2e})",
420 i, min_ev
421 ))
422 })
423 } else if max_ev / min_ev > CONDITION_THRESHOLD {
424 ill_conditioned_count += 1;
426 let extra_reg = max_ev * REGULARIZATION_SCALE;
427 let regularized = block + Matrix3::identity() * extra_reg;
428 regularized.try_inverse().ok_or_else(|| {
429 LinAlgError::SingularMatrix(format!(
430 "Landmark block {} ill-conditioned (cond={:.2e})",
431 i,
432 max_ev / min_ev
433 ))
434 })
435 } else {
436 block.try_inverse().ok_or_else(|| {
438 LinAlgError::SingularMatrix(format!("Landmark block {} is singular", i))
439 })
440 }
441 })
442 .collect();
443
444 if ill_conditioned_count > 0 || regularized_count > 0 {
446 debug!(
447 "Landmark block conditioning: {} ill-conditioned, {} regularized out of {}",
448 ill_conditioned_count,
449 regularized_count,
450 blocks.len()
451 );
452 }
453
454 result
455 }
456
457 fn extract_camera_block(
459 &self,
460 hessian: &SparseColMat<usize, f64>,
461 ) -> LinAlgResult<SparseColMat<usize, f64>> {
462 let structure = self
463 .block_structure
464 .as_ref()
465 .ok_or_else(|| LinAlgError::InvalidInput("Block structure not built".to_string()))?;
466
467 let (cam_start, cam_end) = structure.camera_col_range();
468 let cam_size = structure.camera_dof;
469 let symbolic = hessian.symbolic();
470
471 let mut triplets = Vec::new();
472
473 for global_col in cam_start..cam_end {
474 let local_col = global_col - cam_start;
475 let row_indices = symbolic.row_idx_of_col_raw(global_col);
476 let col_values = hessian.val_of_col(global_col);
477
478 for (idx, &global_row) in row_indices.iter().enumerate() {
479 if global_row >= cam_start && global_row < cam_end {
480 let local_row = global_row - cam_start;
481 triplets.push(Triplet::new(local_row, local_col, col_values[idx]));
482 }
483 }
484 }
485
486 SparseColMat::try_new_from_triplets(cam_size, cam_size, &triplets)
487 .map_err(|e| LinAlgError::SparseMatrixCreation(format!("H_cc: {:?}", e)))
488 }
489
490 fn extract_coupling_block(
492 &self,
493 hessian: &SparseColMat<usize, f64>,
494 ) -> LinAlgResult<SparseColMat<usize, f64>> {
495 let structure = self
496 .block_structure
497 .as_ref()
498 .ok_or_else(|| LinAlgError::InvalidInput("Block structure not built".to_string()))?;
499
500 let (cam_start, cam_end) = structure.camera_col_range();
501 let (land_start, land_end) = structure.landmark_col_range();
502 let cam_size = structure.camera_dof;
503 let land_size = structure.landmark_dof;
504 let symbolic = hessian.symbolic();
505
506 let mut triplets = Vec::new();
507
508 for global_col in land_start..land_end {
509 let local_col = global_col - land_start;
510 let row_indices = symbolic.row_idx_of_col_raw(global_col);
511 let col_values = hessian.val_of_col(global_col);
512
513 for (idx, &global_row) in row_indices.iter().enumerate() {
514 if global_row >= cam_start && global_row < cam_end {
515 let local_row = global_row - cam_start;
516 triplets.push(Triplet::new(local_row, local_col, col_values[idx]));
517 }
518 }
519 }
520
521 SparseColMat::try_new_from_triplets(cam_size, land_size, &triplets)
522 .map_err(|e| LinAlgError::SparseMatrixCreation(format!("H_cp: {:?}", e)))
523 }
524
525 fn extract_gradient_blocks(&self, gradient: &Mat<f64>) -> LinAlgResult<(Mat<f64>, Mat<f64>)> {
527 let structure = self
528 .block_structure
529 .as_ref()
530 .ok_or_else(|| LinAlgError::InvalidInput("Block structure not built".to_string()))?;
531
532 let (cam_start, cam_end) = structure.camera_col_range();
533 let (land_start, land_end) = structure.landmark_col_range();
534
535 let mut g_c = Mat::zeros(structure.camera_dof, 1);
536 for i in 0..(cam_end - cam_start) {
537 g_c[(i, 0)] = gradient[(cam_start + i, 0)];
538 }
539
540 let mut g_p = Mat::zeros(structure.landmark_dof, 1);
541 for i in 0..(land_end - land_start) {
542 g_p[(i, 0)] = gradient[(land_start + i, 0)];
543 }
544
545 Ok((g_c, g_p))
546 }
547
548 fn solve_with_cholesky(
553 &self,
554 a: &SparseColMat<usize, f64>,
555 b: &Mat<f64>,
556 ) -> LinAlgResult<Mat<f64>> {
557 let sym = SymbolicLlt::try_new(a.symbolic(), Side::Lower).map_err(|e| {
558 LinAlgError::FactorizationFailed(format!("Symbolic Cholesky failed: {:?}", e))
559 })?;
560
561 match Llt::try_new_with_symbolic(sym.clone(), a.as_ref(), Side::Lower) {
563 Ok(cholesky) => return Ok(cholesky.solve(b)),
564 Err(e) => {
565 debug!(
566 "Cholesky factorization failed: {:?}. Applying regularization.",
567 e
568 );
569 }
570 }
571
572 let n = a.nrows();
574 let symbolic = a.symbolic();
575
576 let mut trace = 0.0;
578 let mut max_diag = 0.0f64;
579 for col in 0..n {
580 let row_indices = symbolic.row_idx_of_col_raw(col);
581 let col_values = a.val_of_col(col);
582 for (idx, &row) in row_indices.iter().enumerate() {
583 if row == col {
584 trace += col_values[idx];
585 max_diag = max_diag.max(col_values[idx].abs());
586 }
587 }
588 }
589
590 let avg_diag = trace / n as f64;
592 let base_reg = avg_diag.max(max_diag).max(1.0);
593
594 for attempt in 0..5 {
595 let reg = base_reg * 10.0f64.powi(attempt - 4); debug!(
597 "Cholesky attempt {}: regularization = {:.2e}",
598 attempt + 2,
599 reg
600 );
601
602 let mut triplets = Vec::with_capacity(n * 10);
603 for col in 0..n {
604 let row_indices = symbolic.row_idx_of_col_raw(col);
605 let col_values = a.val_of_col(col);
606 for (idx, &row) in row_indices.iter().enumerate() {
607 triplets.push(Triplet::new(row, col, col_values[idx]));
608 }
609 }
610
611 for i in 0..n {
612 triplets.push(Triplet::new(i, i, reg));
613 }
614
615 let a_reg = match SparseColMat::try_new_from_triplets(n, n, &triplets) {
616 Ok(m) => m,
617 Err(e) => {
618 debug!("Failed to create regularized matrix: {:?}", e);
619 continue;
620 }
621 };
622
623 let sym_reg = match SymbolicLlt::try_new(a_reg.symbolic(), Side::Lower) {
625 Ok(s) => s,
626 Err(e) => {
627 debug!("Symbolic factorization failed: {:?}", e);
628 continue;
629 }
630 };
631
632 match Llt::try_new_with_symbolic(sym_reg, a_reg.as_ref(), Side::Lower) {
633 Ok(cholesky) => {
634 debug!("Cholesky succeeded with regularization {:.2e}", reg);
635 return Ok(cholesky.solve(b));
636 }
637 Err(e) => {
638 debug!("Cholesky failed with reg {:.2e}: {:?}", reg, e);
639 }
640 }
641 }
642
643 Err(LinAlgError::SingularMatrix(format!(
644 "Schur complement singular after 5 regularization attempts (max reg = {:.2e})",
645 base_reg
646 )))
647 }
648
649 fn solve_with_pcg(&self, a: &SparseColMat<usize, f64>, b: &Mat<f64>) -> LinAlgResult<Mat<f64>> {
653 let n = b.nrows();
654 let max_iterations = self.cg_max_iterations;
655 let tolerance = self.cg_tolerance;
656
657 let symbolic = a.symbolic();
659 let mut precond = vec![1.0; n];
660 for (col, precond_val) in precond.iter_mut().enumerate().take(n) {
661 let row_indices = symbolic.row_idx_of_col_raw(col);
662 let col_values = a.val_of_col(col);
663 for (idx, &row) in row_indices.iter().enumerate() {
664 if row == col {
665 let diag = col_values[idx];
666 *precond_val = if diag.abs() > 1e-12 { 1.0 / diag } else { 1.0 };
667 break;
668 }
669 }
670 }
671
672 let mut x = Mat::<f64>::zeros(n, 1);
674
675 let mut r = b.clone();
677
678 let mut z = Mat::<f64>::zeros(n, 1);
680 for i in 0..n {
681 z[(i, 0)] = precond[i] * r[(i, 0)];
682 }
683
684 let mut p = z.clone();
685
686 let mut rz_old = 0.0;
687 for i in 0..n {
688 rz_old += r[(i, 0)] * z[(i, 0)];
689 }
690
691 let mut r_norm_init = 0.0;
693 for i in 0..n {
694 r_norm_init += r[(i, 0)] * r[(i, 0)];
695 }
696 r_norm_init = r_norm_init.sqrt();
697 let abs_tol = tolerance * r_norm_init.max(1.0);
698
699 for _iter in 0..max_iterations {
700 let mut ap = Mat::<f64>::zeros(n, 1);
702 for col in 0..n {
703 let row_indices = symbolic.row_idx_of_col_raw(col);
704 let col_values = a.val_of_col(col);
705 for (idx, &row) in row_indices.iter().enumerate() {
706 ap[(row, 0)] += col_values[idx] * p[(col, 0)];
707 }
708 }
709
710 let mut p_ap = 0.0;
712 for i in 0..n {
713 p_ap += p[(i, 0)] * ap[(i, 0)];
714 }
715
716 if p_ap.abs() < 1e-30 {
717 break;
718 }
719
720 let alpha = rz_old / p_ap;
721
722 for i in 0..n {
724 x[(i, 0)] += alpha * p[(i, 0)];
725 }
726
727 for i in 0..n {
729 r[(i, 0)] -= alpha * ap[(i, 0)];
730 }
731
732 let mut r_norm = 0.0;
734 for i in 0..n {
735 r_norm += r[(i, 0)] * r[(i, 0)];
736 }
737 r_norm = r_norm.sqrt();
738
739 if r_norm < abs_tol {
740 break;
741 }
742
743 for i in 0..n {
745 z[(i, 0)] = precond[i] * r[(i, 0)];
746 }
747
748 let mut rz_new = 0.0;
750 for i in 0..n {
751 rz_new += r[(i, 0)] * z[(i, 0)];
752 }
753
754 if rz_old.abs() < 1e-30 {
755 break;
756 }
757
758 let beta = rz_new / rz_old;
759
760 for i in 0..n {
762 p[(i, 0)] = z[(i, 0)] + beta * p[(i, 0)];
763 }
764
765 rz_old = rz_new;
766 }
767
768 Ok(x)
769 }
770
771 fn compute_schur_complement(
785 &self,
786 h_cc: &SparseColMat<usize, f64>,
787 h_cp: &SparseColMat<usize, f64>,
788 hpp_inv_blocks: &[Matrix3<f64>],
789 ) -> LinAlgResult<SparseColMat<usize, f64>> {
790 let cam_size = h_cc.nrows();
791 let h_cp_symbolic = h_cp.symbolic();
792
793 let mut s_dense = vec![0.0f64; cam_size * cam_size];
796
797 let h_cc_symbolic = h_cc.symbolic();
799 for col in 0..h_cc.ncols() {
800 let row_indices = h_cc_symbolic.row_idx_of_col_raw(col);
801 let col_values = h_cc.val_of_col(col);
802 for (idx, &row) in row_indices.iter().enumerate() {
803 s_dense[row * cam_size + col] += col_values[idx];
804 }
805 }
806
807 let mut cam_rows: Vec<usize> = Vec::with_capacity(32);
810 let mut h_cp_block: Vec<[f64; 3]> = Vec::with_capacity(32);
811 let mut contrib_block: Vec<[f64; 3]> = Vec::with_capacity(32);
812
813 for (block_idx, hpp_inv_block) in hpp_inv_blocks.iter().enumerate() {
815 let col_start = block_idx * 3;
816
817 cam_rows.clear();
818 h_cp_block.clear();
819
820 if col_start + 2 >= h_cp.ncols() {
821 continue;
822 }
823
824 let row_indices_0 = h_cp_symbolic.row_idx_of_col_raw(col_start);
825 let col_values_0 = h_cp.val_of_col(col_start);
826 let row_indices_1 = h_cp_symbolic.row_idx_of_col_raw(col_start + 1);
827 let col_values_1 = h_cp.val_of_col(col_start + 1);
828 let row_indices_2 = h_cp_symbolic.row_idx_of_col_raw(col_start + 2);
829 let col_values_2 = h_cp.val_of_col(col_start + 2);
830
831 let mut i0 = 0;
832 let mut i1 = 0;
833 let mut i2 = 0;
834
835 while i0 < row_indices_0.len() || i1 < row_indices_1.len() || i2 < row_indices_2.len() {
836 let r0 = if i0 < row_indices_0.len() {
837 row_indices_0[i0]
838 } else {
839 usize::MAX
840 };
841 let r1 = if i1 < row_indices_1.len() {
842 row_indices_1[i1]
843 } else {
844 usize::MAX
845 };
846 let r2 = if i2 < row_indices_2.len() {
847 row_indices_2[i2]
848 } else {
849 usize::MAX
850 };
851
852 let min_row = r0.min(r1).min(r2);
853 if min_row == usize::MAX {
854 break;
855 }
856
857 let v0 = if r0 == min_row {
858 i0 += 1;
859 col_values_0[i0 - 1]
860 } else {
861 0.0
862 };
863 let v1 = if r1 == min_row {
864 i1 += 1;
865 col_values_1[i1 - 1]
866 } else {
867 0.0
868 };
869 let v2 = if r2 == min_row {
870 i2 += 1;
871 col_values_2[i2 - 1]
872 } else {
873 0.0
874 };
875
876 cam_rows.push(min_row);
877 h_cp_block.push([v0, v1, v2]);
878 }
879
880 if cam_rows.is_empty() {
881 continue;
882 }
883
884 contrib_block.clear();
885 for h_cp_row in &h_cp_block {
886 let c0 = h_cp_row[0] * hpp_inv_block[(0, 0)]
887 + h_cp_row[1] * hpp_inv_block[(1, 0)]
888 + h_cp_row[2] * hpp_inv_block[(2, 0)];
889 let c1 = h_cp_row[0] * hpp_inv_block[(0, 1)]
890 + h_cp_row[1] * hpp_inv_block[(1, 1)]
891 + h_cp_row[2] * hpp_inv_block[(2, 1)];
892 let c2 = h_cp_row[0] * hpp_inv_block[(0, 2)]
893 + h_cp_row[1] * hpp_inv_block[(1, 2)]
894 + h_cp_row[2] * hpp_inv_block[(2, 2)];
895 contrib_block.push([c0, c1, c2]);
896 }
897
898 let n_cams = cam_rows.len();
899 for i in 0..n_cams {
900 let cam_i = cam_rows[i];
901 let contrib_i = &contrib_block[i];
902 for j in 0..n_cams {
903 let cam_j = cam_rows[j];
904 let h_cp_j = &h_cp_block[j];
905 let dot = contrib_i[0] * h_cp_j[0]
906 + contrib_i[1] * h_cp_j[1]
907 + contrib_i[2] * h_cp_j[2];
908 s_dense[cam_i * cam_size + cam_j] -= dot;
909 }
910 }
911 }
912
913 for i in 0..cam_size {
917 for j in (i + 1)..cam_size {
918 let avg = (s_dense[i * cam_size + j] + s_dense[j * cam_size + i]) * 0.5;
919 s_dense[i * cam_size + j] = avg;
920 s_dense[j * cam_size + i] = avg;
921 }
922 }
923
924 let mut s_triplets: Vec<Triplet<usize, usize, f64>> = Vec::new();
927 for col in 0..cam_size {
928 for row in 0..cam_size {
929 let val = s_dense[row * cam_size + col];
930 if val.abs() > 1e-12 {
931 s_triplets.push(Triplet::new(row, col, val));
932 }
933 }
934 }
935
936 SparseColMat::try_new_from_triplets(cam_size, cam_size, &s_triplets)
937 .map_err(|e| LinAlgError::SparseMatrixCreation(format!("Schur S: {:?}", e)))
938 }
939
940 fn compute_reduced_gradient(
942 &self,
943 g_c: &Mat<f64>,
944 g_p: &Mat<f64>,
945 h_cp: &SparseColMat<usize, f64>,
946 hpp_inv_blocks: &[Matrix3<f64>],
947 ) -> LinAlgResult<Mat<f64>> {
948 let land_size = g_p.nrows();
950 let cam_size = g_c.nrows();
951
952 let mut hpp_inv_gp = Mat::zeros(land_size, 1);
954
955 for (block_idx, hpp_inv_block) in hpp_inv_blocks.iter().enumerate() {
956 let row_start = block_idx * 3;
957
958 let gp_block = nalgebra::Vector3::new(
959 g_p[(row_start, 0)],
960 g_p[(row_start + 1, 0)],
961 g_p[(row_start + 2, 0)],
962 );
963
964 let result = hpp_inv_block * gp_block;
965 hpp_inv_gp[(row_start, 0)] = result[0];
966 hpp_inv_gp[(row_start + 1, 0)] = result[1];
967 hpp_inv_gp[(row_start + 2, 0)] = result[2];
968 }
969
970 let mut h_cp_hpp_inv_gp = Mat::<f64>::zeros(cam_size, 1);
972 let symbolic = h_cp.symbolic();
973
974 for col in 0..h_cp.ncols() {
975 let row_indices = symbolic.row_idx_of_col_raw(col);
976 let col_values = h_cp.val_of_col(col);
977
978 for (idx, &row) in row_indices.iter().enumerate() {
979 h_cp_hpp_inv_gp[(row, 0)] += col_values[idx] * hpp_inv_gp[(col, 0)];
980 }
981 }
982
983 let mut g_reduced = Mat::zeros(cam_size, 1);
985 for i in 0..cam_size {
986 g_reduced[(i, 0)] = g_c[(i, 0)] - h_cp_hpp_inv_gp[(i, 0)];
987 }
988
989 Ok(g_reduced)
990 }
991
992 fn back_substitute(
994 &self,
995 delta_c: &Mat<f64>,
996 g_p: &Mat<f64>,
997 h_cp: &SparseColMat<usize, f64>,
998 hpp_inv_blocks: &[Matrix3<f64>],
999 ) -> LinAlgResult<Mat<f64>> {
1000 let land_size = g_p.nrows();
1003
1004 let mut h_cp_t_delta_c = Mat::<f64>::zeros(land_size, 1);
1006 let symbolic = h_cp.symbolic();
1007
1008 for col in 0..h_cp.ncols() {
1009 let row_indices = symbolic.row_idx_of_col_raw(col);
1010 let col_values = h_cp.val_of_col(col);
1011
1012 for (idx, &row) in row_indices.iter().enumerate() {
1013 h_cp_t_delta_c[(col, 0)] += col_values[idx] * delta_c[(row, 0)];
1014 }
1015 }
1016
1017 let mut rhs = Mat::zeros(land_size, 1);
1019 for i in 0..land_size {
1020 rhs[(i, 0)] = g_p[(i, 0)] - h_cp_t_delta_c[(i, 0)];
1021 }
1022
1023 let mut delta_p = Mat::zeros(land_size, 1);
1025
1026 for (block_idx, hpp_inv_block) in hpp_inv_blocks.iter().enumerate() {
1027 let row_start = block_idx * 3;
1028
1029 let rhs_block = nalgebra::Vector3::new(
1030 rhs[(row_start, 0)],
1031 rhs[(row_start + 1, 0)],
1032 rhs[(row_start + 2, 0)],
1033 );
1034
1035 let result = hpp_inv_block * rhs_block;
1036 delta_p[(row_start, 0)] = result[0];
1037 delta_p[(row_start + 1, 0)] = result[1];
1038 delta_p[(row_start + 2, 0)] = result[2];
1039 }
1040
1041 Ok(delta_p)
1042 }
1043}
1044
1045impl Default for SparseSchurComplementSolver {
1046 fn default() -> Self {
1047 Self::new()
1048 }
1049}
1050
1051impl StructuredSparseLinearSolver for SparseSchurComplementSolver {
1052 fn initialize_structure(
1053 &mut self,
1054 variables: &HashMap<String, VariableEnum>,
1055 variable_index_map: &HashMap<String, usize>,
1056 ) -> LinAlgResult<()> {
1057 self.build_block_structure(variables, variable_index_map)?;
1059
1060 match self.variant {
1062 SchurVariant::Iterative => {
1063 let mut solver =
1064 IterativeSchurSolver::with_cg_params(self.cg_max_iterations, self.cg_tolerance);
1065 solver.initialize_structure(variables, variable_index_map)?;
1066 self.iterative_solver = Some(solver);
1067 }
1068 SchurVariant::Sparse => {
1069 }
1071 }
1072
1073 Ok(())
1074 }
1075
1076 fn solve_normal_equation(
1077 &mut self,
1078 residuals: &Mat<f64>,
1079 jacobians: &SparseColMat<usize, f64>,
1080 ) -> LinAlgResult<Mat<f64>> {
1081 use std::ops::Mul;
1082
1083 if self.block_structure.is_none() {
1084 return Err(LinAlgError::InvalidInput(
1085 "Block structure not built. Call initialize_structure() first.".to_string(),
1086 ));
1087 }
1088
1089 let jt = jacobians
1096 .transpose()
1097 .to_col_major()
1098 .map_err(|e| LinAlgError::MatrixConversion(format!("Transpose failed: {:?}", e)))?;
1099 let hessian = jt.mul(jacobians);
1100 let gradient = jacobians.transpose().mul(residuals);
1101 let mut neg_gradient = Mat::zeros(gradient.nrows(), 1);
1102 for i in 0..gradient.nrows() {
1103 neg_gradient[(i, 0)] = -gradient[(i, 0)];
1104 }
1105
1106 self.hessian = Some(hessian.clone());
1107 self.gradient = Some(gradient.clone());
1110
1111 let h_cc = self.extract_camera_block(&hessian)?;
1113 let h_cp = self.extract_coupling_block(&hessian)?;
1114 let hpp_blocks = self.extract_landmark_blocks(&hessian)?;
1115 let (g_c, g_p) = self.extract_gradient_blocks(&neg_gradient)?;
1116
1117 let hpp_inv_blocks = Self::invert_landmark_blocks(&hpp_blocks)?;
1119
1120 let s = self.compute_schur_complement(&h_cc, &h_cp, &hpp_inv_blocks)?;
1122
1123 let g_reduced = self.compute_reduced_gradient(&g_c, &g_p, &h_cp, &hpp_inv_blocks)?;
1125
1126 let delta_c = match self.variant {
1128 SchurVariant::Iterative => self.solve_with_pcg(&s, &g_reduced)?,
1129 _ => self.solve_with_cholesky(&s, &g_reduced)?,
1130 };
1131
1132 let delta_p = self.back_substitute(&delta_c, &g_p, &h_cp, &hpp_inv_blocks)?;
1134
1135 self.combine_updates(&delta_c, &delta_p)
1137 }
1138
1139 fn solve_augmented_equation(
1140 &mut self,
1141 residuals: &Mat<f64>,
1142 jacobians: &SparseColMat<usize, f64>,
1143 lambda: f64,
1144 ) -> LinAlgResult<Mat<f64>> {
1145 use std::ops::Mul;
1146
1147 if self.block_structure.is_none() {
1148 return Err(LinAlgError::InvalidInput(
1149 "Block structure not built. Call initialize_structure() first.".to_string(),
1150 ));
1151 }
1152
1153 let jt = jacobians
1156 .transpose()
1157 .to_col_major()
1158 .map_err(|e| LinAlgError::MatrixConversion(format!("Transpose failed: {:?}", e)))?;
1159 let hessian = jt.mul(jacobians);
1160 let gradient = jacobians.transpose().mul(residuals);
1161 let mut neg_gradient = Mat::zeros(gradient.nrows(), 1);
1162 for i in 0..gradient.nrows() {
1163 neg_gradient[(i, 0)] = -gradient[(i, 0)];
1164 }
1165
1166 self.hessian = Some(hessian.clone());
1167 self.gradient = Some(gradient.clone());
1170
1171 let h_cc = self.extract_camera_block(&hessian)?;
1173 let h_cp = self.extract_coupling_block(&hessian)?;
1174 let mut hpp_blocks = self.extract_landmark_blocks(&hessian)?;
1175 let (g_c, g_p) = self.extract_gradient_blocks(&neg_gradient)?;
1176
1177 debug!("Iteration matrices:");
1179 debug!(
1180 " Hessian (J^T*J): {} × {}",
1181 hessian.nrows(),
1182 hessian.ncols()
1183 );
1184 debug!(" H_cc (camera): {} × {}", h_cc.nrows(), h_cc.ncols());
1185 debug!(" H_cp (coupling): {} × {}", h_cp.nrows(), h_cp.ncols());
1186 debug!(" H_pp blocks: {} (3×3 each)", hpp_blocks.len());
1187
1188 let structure = self
1190 .block_structure
1191 .as_ref()
1192 .ok_or_else(|| LinAlgError::InvalidInput("Block structure not initialized".into()))?;
1193 let cam_size = structure.camera_dof;
1194
1195 let mut h_cc_triplets = Vec::new();
1197 let h_cc_symbolic = h_cc.symbolic();
1198 for col in 0..h_cc.ncols() {
1199 let row_indices = h_cc_symbolic.row_idx_of_col_raw(col);
1200 let col_values = h_cc.val_of_col(col);
1201 for (idx, &row) in row_indices.iter().enumerate() {
1202 h_cc_triplets.push(Triplet::new(row, col, col_values[idx]));
1203 }
1204 }
1205 for i in 0..cam_size {
1206 if let Some(entry) = h_cc_triplets.iter_mut().find(|t| t.row == i && t.col == i) {
1207 *entry = Triplet::new(i, i, entry.val + lambda);
1208 } else {
1209 h_cc_triplets.push(Triplet::new(i, i, lambda));
1210 }
1211 }
1212 let h_cc_damped =
1213 SparseColMat::try_new_from_triplets(cam_size, cam_size, &h_cc_triplets)
1214 .map_err(|e| LinAlgError::SparseMatrixCreation(format!("Damped H_cc: {:?}", e)))?;
1215
1216 for block in &mut hpp_blocks {
1218 block[(0, 0)] += lambda;
1219 block[(1, 1)] += lambda;
1220 block[(2, 2)] += lambda;
1221 }
1222
1223 let hpp_inv_blocks = Self::invert_landmark_blocks(&hpp_blocks)?;
1225
1226 let s = self.compute_schur_complement(&h_cc_damped, &h_cp, &hpp_inv_blocks)?;
1228
1229 let g_reduced = self.compute_reduced_gradient(&g_c, &g_p, &h_cp, &hpp_inv_blocks)?;
1231
1232 let delta_c = match self.variant {
1234 SchurVariant::Iterative => self.solve_with_pcg(&s, &g_reduced)?,
1235 _ => self.solve_with_cholesky(&s, &g_reduced)?,
1236 };
1237
1238 let delta_p = self.back_substitute(&delta_c, &g_p, &h_cp, &hpp_inv_blocks)?;
1240
1241 self.combine_updates(&delta_c, &delta_p)
1243 }
1244
1245 fn get_hessian(&self) -> Option<&SparseColMat<usize, f64>> {
1246 self.hessian.as_ref()
1247 }
1248
1249 fn get_gradient(&self) -> Option<&Mat<f64>> {
1250 self.gradient.as_ref()
1251 }
1252}
1253
1254impl SparseSchurComplementSolver {
1256 fn combine_updates(&self, delta_c: &Mat<f64>, delta_p: &Mat<f64>) -> LinAlgResult<Mat<f64>> {
1258 let structure = self
1259 .block_structure
1260 .as_ref()
1261 .ok_or_else(|| LinAlgError::InvalidInput("Block structure not built".to_string()))?;
1262
1263 let total_dof = structure.camera_dof + structure.landmark_dof;
1264 let mut delta = Mat::zeros(total_dof, 1);
1265
1266 let (cam_start, cam_end) = structure.camera_col_range();
1267 let (land_start, land_end) = structure.landmark_col_range();
1268
1269 for i in 0..(cam_end - cam_start) {
1271 delta[(cam_start + i, 0)] = delta_c[(i, 0)];
1272 }
1273
1274 for i in 0..(land_end - land_start) {
1276 delta[(land_start + i, 0)] = delta_p[(i, 0)];
1277 }
1278
1279 debug!(
1281 "Update norms: delta_c={:.6e}, delta_p={:.6e}, combined={:.6e}",
1282 delta_c.norm_l2(),
1283 delta_p.norm_l2(),
1284 delta.norm_l2()
1285 );
1286
1287 Ok(delta)
1288 }
1289}
1290
1291pub struct SchurSolverAdapter {
1296 schur_solver: SparseSchurComplementSolver,
1297 initialized: bool,
1298}
1299
1300impl SchurSolverAdapter {
1301 pub fn new_with_structure(
1312 variables: &std::collections::HashMap<String, crate::core::problem::VariableEnum>,
1313 variable_index_map: &std::collections::HashMap<String, usize>,
1314 ) -> LinAlgResult<Self> {
1315 let mut solver = SparseSchurComplementSolver::new();
1316 solver.initialize_structure(variables, variable_index_map)?;
1317 Ok(Self {
1318 schur_solver: solver,
1319 initialized: true,
1320 })
1321 }
1322
1323 pub fn new_with_structure_and_config(
1325 variables: &std::collections::HashMap<String, crate::core::problem::VariableEnum>,
1326 variable_index_map: &std::collections::HashMap<String, usize>,
1327 variant: SchurVariant,
1328 preconditioner: SchurPreconditioner,
1329 ) -> LinAlgResult<Self> {
1330 let mut solver = SparseSchurComplementSolver::new()
1331 .with_variant(variant)
1332 .with_preconditioner(preconditioner);
1333 solver.initialize_structure(variables, variable_index_map)?;
1334 Ok(Self {
1335 schur_solver: solver,
1336 initialized: true,
1337 })
1338 }
1339}
1340
1341impl crate::linalg::SparseLinearSolver for SchurSolverAdapter {
1342 fn solve_normal_equation(
1343 &mut self,
1344 residuals: &Mat<f64>,
1345 jacobian: &SparseColMat<usize, f64>,
1346 ) -> LinAlgResult<Mat<f64>> {
1347 if !self.initialized {
1348 return Err(LinAlgError::InvalidInput(
1349 "Schur solver adapter not initialized with structure".to_string(),
1350 ));
1351 }
1352 self.schur_solver.solve_normal_equation(residuals, jacobian)
1353 }
1354
1355 fn solve_augmented_equation(
1356 &mut self,
1357 residuals: &Mat<f64>,
1358 jacobian: &SparseColMat<usize, f64>,
1359 lambda: f64,
1360 ) -> LinAlgResult<Mat<f64>> {
1361 if !self.initialized {
1362 return Err(LinAlgError::InvalidInput(
1363 "Schur solver adapter not initialized with structure".to_string(),
1364 ));
1365 }
1366 self.schur_solver
1367 .solve_augmented_equation(residuals, jacobian, lambda)
1368 }
1369
1370 fn get_hessian(&self) -> Option<&SparseColMat<usize, f64>> {
1371 self.schur_solver.get_hessian()
1372 }
1373
1374 fn get_gradient(&self) -> Option<&Mat<f64>> {
1375 self.schur_solver.get_gradient()
1376 }
1377
1378 fn compute_covariance_matrix(&mut self) -> Option<&Mat<f64>> {
1379 None
1383 }
1384
1385 fn get_covariance_matrix(&self) -> Option<&Mat<f64>> {
1386 None
1388 }
1389}
1390
1391#[cfg(test)]
1392#[allow(clippy::unwrap_used, clippy::expect_used)]
1393mod tests {
1394 use super::*;
1395
1396 #[test]
1397 fn test_schur_ordering_shared_intrinsics() {
1398 let ordering = SchurOrdering::default();
1399
1400 assert!(ordering.should_eliminate("pt_00000", &ManifoldType::RN, 3));
1402 assert!(ordering.should_eliminate("pt_12345", &ManifoldType::RN, 3));
1403
1404 assert!(!ordering.should_eliminate("cam_0000", &ManifoldType::SE3, 6));
1406 assert!(!ordering.should_eliminate("cam_0042", &ManifoldType::SE3, 6));
1407
1408 assert!(!ordering.should_eliminate("shared_intrinsics", &ManifoldType::RN, 3));
1410
1411 assert!(!ordering.should_eliminate("intr_0000", &ManifoldType::RN, 3));
1413 assert!(!ordering.should_eliminate("intr_0042", &ManifoldType::RN, 3));
1414 }
1415
1416 #[test]
1417 fn test_schur_ordering_multiple_intrinsic_groups() {
1418 let ordering = SchurOrdering::default();
1419
1420 assert!(
1422 !ordering.should_eliminate("intr_group_0000", &ManifoldType::RN, 3),
1423 "Intrinsic group 0 should be camera parameter (not eliminated)"
1424 );
1425 assert!(
1426 !ordering.should_eliminate("intr_group_0001", &ManifoldType::RN, 3),
1427 "Intrinsic group 1 should be camera parameter (not eliminated)"
1428 );
1429 assert!(
1430 !ordering.should_eliminate("intr_group_0005", &ManifoldType::RN, 3),
1431 "Intrinsic group 5 should be camera parameter (not eliminated)"
1432 );
1433 assert!(
1434 !ordering.should_eliminate("intr_group_0042", &ManifoldType::RN, 3),
1435 "Intrinsic group 42 should be camera parameter (not eliminated)"
1436 );
1437
1438 assert!(
1440 ordering.should_eliminate("pt_00000", &ManifoldType::RN, 3),
1441 "Landmarks should still be eliminated"
1442 );
1443
1444 assert!(
1446 !ordering.should_eliminate("cam_0000", &ManifoldType::SE3, 6),
1447 "Camera poses should not be eliminated"
1448 );
1449 }
1450
1451 #[test]
1452 #[should_panic(expected = "has manifold type")]
1453 fn test_schur_ordering_invalid_landmark_type() {
1454 let ordering = SchurOrdering::default();
1455 ordering.should_eliminate("pt_00000", &ManifoldType::SE3, 6);
1457 }
1458
1459 #[test]
1460 #[should_panic(expected = "has 6 DOF")]
1461 fn test_schur_ordering_invalid_landmark_size() {
1462 let ordering = SchurOrdering::default();
1463 ordering.should_eliminate("pt_00000", &ManifoldType::RN, 6);
1465 }
1466
1467 #[test]
1468 fn test_block_structure_creation() {
1469 let structure = SchurBlockStructure::new();
1470 assert_eq!(structure.camera_dof, 0);
1471 assert_eq!(structure.landmark_dof, 0);
1472 }
1473
1474 #[test]
1475 fn test_solver_creation() {
1476 let solver = SparseSchurComplementSolver::new();
1477 assert!(solver.block_structure.is_none());
1478 }
1479
1480 #[test]
1481 fn test_3x3_block_inversion() {
1482 let block = Matrix3::new(2.0, 0.0, 0.0, 0.0, 3.0, 0.0, 0.0, 0.0, 4.0);
1483 let inv = SparseSchurComplementSolver::invert_landmark_blocks(&[block])
1484 .expect("Test: Block inversion should succeed");
1485 assert!((inv[0][(0, 0)] - 0.5).abs() < 1e-10);
1486 }
1487
1488 #[test]
1489 fn test_schur_variants() {
1490 let solver = SparseSchurComplementSolver::new()
1491 .with_variant(SchurVariant::Iterative)
1492 .with_preconditioner(SchurPreconditioner::BlockDiagonal)
1493 .with_cg_params(100, 1e-8);
1494
1495 assert_eq!(solver.cg_max_iterations, 100);
1496 assert!((solver.cg_tolerance - 1e-8).abs() < 1e-12);
1497 }
1498
1499 #[test]
1500 fn test_compute_schur_complement_known_matrix() {
1501 use faer::sparse::Triplet;
1502
1503 let solver = SparseSchurComplementSolver::new();
1504
1505 let h_cc_triplets = vec![Triplet::new(0, 0, 4.0), Triplet::new(1, 1, 5.0)];
1507 let h_cc = SparseColMat::try_new_from_triplets(2, 2, &h_cc_triplets)
1508 .expect("Test: H_cc matrix creation should succeed");
1509
1510 let h_cp_triplets = vec![Triplet::new(0, 0, 1.0), Triplet::new(1, 1, 2.0)];
1512 let h_cp = SparseColMat::try_new_from_triplets(2, 3, &h_cp_triplets)
1513 .expect("Test: H_cp matrix creation should succeed");
1514
1515 let hpp_inv = vec![Matrix3::new(0.5, 0.0, 0.0, 0.0, 0.5, 0.0, 0.0, 0.0, 0.5)];
1517
1518 let s = solver
1529 .compute_schur_complement(&h_cc, &h_cp, &hpp_inv)
1530 .expect("Test: Schur complement computation should succeed");
1531
1532 assert_eq!(s.nrows(), 2);
1533 assert_eq!(s.ncols(), 2);
1534 assert!((s[(0, 0)] - 3.5).abs() < 1e-10, "S(0,0) = {}", s[(0, 0)]);
1542 assert!((s[(1, 1)] - 3.0).abs() < 1e-10, "S(1,1) = {}", s[(1, 1)]);
1543 }
1544
1545 #[test]
1546 fn test_back_substitute() {
1547 use faer::sparse::Triplet;
1548
1549 let solver = SparseSchurComplementSolver::new();
1550
1551 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)];
1557 let h_cp = SparseColMat::try_new_from_triplets(2, 3, &h_cp_triplets)
1558 .expect("Test: H_cp matrix creation should succeed");
1559
1560 let hpp_inv = vec![Matrix3::identity()];
1562
1563 let delta_p = solver
1568 .back_substitute(&delta_c, &g_p, &h_cp, &hpp_inv)
1569 .expect("Test: Back substitution should succeed");
1570
1571 assert_eq!(delta_p.nrows(), 3);
1572 assert!((delta_p[(0, 0)]).abs() < 1e-10);
1573 assert!((delta_p[(1, 0)]).abs() < 1e-10);
1574 assert!((delta_p[(2, 0)] - 3.0).abs() < 1e-10);
1575 }
1576
1577 #[test]
1578 fn test_compute_reduced_gradient() {
1579 use faer::sparse::Triplet;
1580
1581 let solver = SparseSchurComplementSolver::new();
1582
1583 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)];
1589 let h_cp = SparseColMat::try_new_from_triplets(2, 3, &h_cp_triplets)
1590 .expect("Test: H_cp matrix creation should succeed");
1591
1592 let hpp_inv = vec![Matrix3::new(2.0, 0.0, 0.0, 0.0, 2.0, 0.0, 0.0, 0.0, 2.0)];
1594
1595 let g_reduced = solver
1600 .compute_reduced_gradient(&g_c, &g_p, &h_cp, &hpp_inv)
1601 .expect("Test: Reduced gradient computation should succeed");
1602
1603 assert_eq!(g_reduced.nrows(), 2);
1604 assert!((g_reduced[(0, 0)] + 1.0).abs() < 1e-10);
1605 assert!((g_reduced[(1, 0)] + 2.0).abs() < 1e-10);
1606 }
1607}