1use std::{
2 collections::{HashMap, HashSet},
3 fs::File,
4 io::{Error, Write},
5};
6
7use faer::{Mat, sparse::SparseColMat};
8use nalgebra::DVector;
9use rayon::prelude::*;
10use slotmap::{SecondaryMap, SlotMap};
11
12use crate::{
13 core::CoreResult,
14 core::{
15 CoreError, FactorKey, VarKey,
16 loss_functions::LossFunction,
17 noise::NoiseModel,
18 residual_block::ResidualBlock,
19 variable::{ManifoldVariable, Variable},
20 },
21 factors::Factor,
22 linalg::JacobianMode,
23};
24use apex_manifolds::{LieGroup, ManifoldType, rn, se2, se3, se23, sgal3, sim3, so2, so3};
25
26pub use crate::linearizer::cpu::sparse::SymbolicStructure;
27
28pub struct Problem {
29 pub(crate) total_residual_dimension: usize,
30 pub(crate) jacobian_mode: JacobianMode,
31 pub(crate) variables: SlotMap<VarKey, Box<dyn ManifoldVariable>>,
32 residual_blocks: SlotMap<FactorKey, ResidualBlock>,
33 pub(crate) fixed_variable_indexes: SecondaryMap<VarKey, HashSet<usize>>,
34 pub(crate) variable_bounds: SecondaryMap<VarKey, HashMap<usize, (f64, f64)>>,
35 pub(crate) schur_landmark_keys: HashSet<VarKey>,
36}
37
38impl Default for Problem {
39 fn default() -> Self {
40 Self::new(JacobianMode::Sparse)
41 }
42}
43
44impl Problem {
45 pub fn new(jacobian_mode: JacobianMode) -> Self {
46 Self {
47 total_residual_dimension: 0,
48 jacobian_mode,
49 variables: SlotMap::with_key(),
50 residual_blocks: SlotMap::with_key(),
51 fixed_variable_indexes: SecondaryMap::new(),
52 variable_bounds: SecondaryMap::new(),
53 schur_landmark_keys: HashSet::new(),
54 }
55 }
56
57 pub fn mark_for_elimination(&mut self, key: VarKey) {
78 self.schur_landmark_keys.insert(key);
79 }
80
81 #[deprecated(
83 since = "1.6.0",
84 note = "renamed to `mark_for_elimination`: elimination is not restricted to landmarks"
85 )]
86 pub fn mark_as_schur_landmark(&mut self, key: VarKey) {
87 self.mark_for_elimination(key);
88 }
89
90 pub fn add_variable(&mut self, manifold_type: ManifoldType, params: DVector<f64>) -> VarKey {
94 let var = Self::create_variable(&manifold_type, ¶ms);
95 self.variables.insert(var)
96 }
97
98 pub fn add_residual_block(
102 &mut self,
103 variable_keys: &[VarKey],
104 factor: Box<dyn Factor + Send + Sync>,
105 loss_func: Option<Box<dyn LossFunction + Send + Sync>>,
106 ) -> FactorKey {
107 self.add_residual_block_with_noise(variable_keys, factor, loss_func, NoiseModel::null())
108 }
109
110 pub fn add_residual_block_with_noise(
119 &mut self,
120 variable_keys: &[VarKey],
121 factor: Box<dyn Factor + Send + Sync>,
122 loss_func: Option<Box<dyn LossFunction + Send + Sync>>,
123 noise: NoiseModel,
124 ) -> FactorKey {
125 self.try_add_residual_block_with_noise(variable_keys, factor, loss_func, noise)
126 .unwrap_or_else(|e| panic!("invalid residual block: {e}"))
127 }
128
129 pub fn try_add_residual_block_with_noise(
131 &mut self,
132 variable_keys: &[VarKey],
133 factor: Box<dyn Factor + Send + Sync>,
134 loss_func: Option<Box<dyn LossFunction + Send + Sync>>,
135 noise: NoiseModel,
136 ) -> CoreResult<FactorKey> {
137 if noise.dim() != 0 && noise.dim() != factor.residual_dim() {
138 return Err(CoreError::DimensionMismatch(format!(
139 "noise model covers {} residual rows but the factor produces {}",
140 noise.dim(),
141 factor.residual_dim()
142 )));
143 }
144 if factor.whitens_internally() && !matches!(noise, NoiseModel::Null) {
145 return Err(CoreError::InvalidInput(
146 "this factor applies its own square-root information; register it with \
147 NoiseModel::null(). Supplying a noise model as well would whiten the \
148 residual twice."
149 .into(),
150 ));
151 }
152 self.try_add_residual_block_impl(variable_keys, factor, loss_func, noise)
153 }
154
155 pub fn try_add_residual_block(
165 &mut self,
166 variable_keys: &[VarKey],
167 factor: Box<dyn Factor + Send + Sync>,
168 loss_func: Option<Box<dyn LossFunction + Send + Sync>>,
169 ) -> CoreResult<FactorKey> {
170 self.try_add_residual_block_impl(variable_keys, factor, loss_func, NoiseModel::Null)
171 }
172
173 fn try_add_residual_block_impl(
174 &mut self,
175 variable_keys: &[VarKey],
176 factor: Box<dyn Factor + Send + Sync>,
177 loss_func: Option<Box<dyn LossFunction + Send + Sync>>,
178 noise: NoiseModel,
179 ) -> CoreResult<FactorKey> {
180 let mut variables: Vec<&dyn ManifoldVariable> = Vec::with_capacity(variable_keys.len());
181 for &key in variable_keys {
182 let variable = self
183 .variables
184 .get(key)
185 .ok_or_else(|| {
186 CoreError::Variable(format!(
187 "residual block references unknown variable key {key:?}"
188 ))
189 })?
190 .as_ref();
191 variables.push(variable);
192 }
193 factor
194 .validate_variables(&variables)
195 .map_err(CoreError::DimensionMismatch)?;
196
197 let (jac_rows, jac_cols) = factor.jacobian_shape();
204 let residual_dim = factor.residual_dim();
205 if jac_rows != residual_dim {
206 return Err(CoreError::DimensionMismatch(format!(
207 "factor declares a {jac_rows}×{jac_cols} Jacobian but residual_dim() is \
208 {residual_dim}; jacobian_shape().0 must equal residual_dim()"
209 )));
210 }
211 let total_dof: usize = variables.iter().map(|v| v.dof()).sum();
212 if jac_cols != total_dof {
213 return Err(CoreError::DimensionMismatch(format!(
214 "factor declares a {jac_rows}×{jac_cols} Jacobian but the registered \
215 variables have {total_dof} dofs in total; jacobian_shape().1 must equal \
216 the sum of the connected variables' dofs"
217 )));
218 }
219
220 let new_residual_dimension = residual_dim;
221 let row_start = self.total_residual_dimension;
222 let fk = self.residual_blocks.insert_with_key(|fk| {
223 ResidualBlock::with_noise(fk, row_start, variable_keys, factor, loss_func, noise)
224 });
225 self.total_residual_dimension += new_residual_dimension;
226 Ok(fk)
227 }
228
229 pub(crate) fn group_rows_for_elimination(&mut self) -> bool {
249 if self.schur_landmark_keys.is_empty() {
250 return false;
251 }
252
253 let rank: std::collections::HashMap<VarKey, usize> = self
266 .variables
267 .keys()
268 .enumerate()
269 .map(|(rank, key)| (key, rank))
270 .collect();
271 let mut ordered: Vec<(Option<usize>, usize, FactorKey)> = self
272 .residual_blocks
273 .iter()
274 .map(|(key, block)| {
275 let eliminated = block
276 .variable_keys
277 .iter()
278 .find(|k| self.schur_landmark_keys.contains(k))
279 .copied()
280 .and_then(|k| rank.get(&k).copied());
281 (eliminated, block.residual_row_start_idx, key)
282 })
283 .collect();
284 ordered.sort_by_key(|(eliminated, offset, _)| (*eliminated, *offset));
286
287 let mut moved = false;
288 let mut row = 0usize;
289 for (_, _, key) in &ordered {
290 if let Some(block) = self.residual_blocks.get_mut(*key) {
291 if block.residual_row_start_idx != row {
292 block.residual_row_start_idx = row;
293 moved = true;
294 }
295 row += block.factor.residual_dim();
296 }
297 }
298 debug_assert_eq!(
299 row, self.total_residual_dimension,
300 "regrouping must preserve the total residual dimension"
301 );
302 moved
303 }
304
305 pub fn remove_residual_block(&mut self, block_id: FactorKey) -> Option<ResidualBlock> {
306 if let Some(block) = self.residual_blocks.remove(block_id) {
307 self.total_residual_dimension -= block.factor.residual_dim();
308 Some(block)
309 } else {
310 None
311 }
312 }
313
314 pub fn fix_variable(&mut self, var_key: VarKey, idx: usize) {
320 self.try_fix_variable(var_key, idx)
321 .unwrap_or_else(|e| panic!("invalid variable constraint: {e}"))
322 }
323
324 pub fn try_fix_variable(&mut self, var_key: VarKey, idx: usize) -> CoreResult<()> {
330 let dof = self.variable_dof(var_key)?;
331 if idx >= dof {
332 return Err(CoreError::Variable(format!(
333 "cannot fix component {idx} of a {dof}-DOF variable"
334 )));
335 }
336 if let Some(set) = self.fixed_variable_indexes.get_mut(var_key) {
337 set.insert(idx);
338 } else {
339 self.fixed_variable_indexes
340 .insert(var_key, HashSet::from([idx]));
341 }
342 Ok(())
343 }
344
345 fn variable_dof(&self, var_key: VarKey) -> CoreResult<usize> {
347 self.variables
348 .get(var_key)
349 .map(|v| v.dof())
350 .ok_or_else(|| CoreError::Variable(format!("unknown variable key {var_key:?}")))
351 }
352
353 pub fn unfix_variable(&mut self, var_key: VarKey) {
354 self.fixed_variable_indexes.remove(var_key);
355 }
356
357 pub fn set_variable_bounds(
364 &mut self,
365 var_key: VarKey,
366 idx: usize,
367 lower_bound: f64,
368 upper_bound: f64,
369 ) {
370 self.try_set_variable_bounds(var_key, idx, lower_bound, upper_bound)
371 .unwrap_or_else(|e| panic!("invalid variable bounds: {e}"))
372 }
373
374 pub fn try_set_variable_bounds(
379 &mut self,
380 var_key: VarKey,
381 idx: usize,
382 lower_bound: f64,
383 upper_bound: f64,
384 ) -> CoreResult<()> {
385 let dof = self.variable_dof(var_key)?;
386 if idx >= dof {
387 return Err(CoreError::Variable(format!(
388 "cannot bound component {idx} of a {dof}-DOF variable"
389 )));
390 }
391 if lower_bound.is_nan() || upper_bound.is_nan() {
395 return Err(CoreError::Variable(format!(
396 "bound [{lower_bound}, {upper_bound}] must not be NaN"
397 )));
398 }
399 if lower_bound > upper_bound {
400 return Err(CoreError::Variable(format!(
401 "lower bound {lower_bound} exceeds upper bound {upper_bound}"
402 )));
403 }
404 if let Some(map) = self.variable_bounds.get_mut(var_key) {
405 map.insert(idx, (lower_bound, upper_bound));
406 } else {
407 self.variable_bounds
408 .insert(var_key, HashMap::from([(idx, (lower_bound, upper_bound))]));
409 }
410 Ok(())
411 }
412
413 pub fn remove_variable_bounds(&mut self, var_key: VarKey) {
414 self.variable_bounds.remove(var_key);
415 }
416
417 fn create_variable(
418 manifold_type: &ManifoldType,
419 params: &DVector<f64>,
420 ) -> Box<dyn ManifoldVariable> {
421 match manifold_type {
422 ManifoldType::SO2 => {
423 Box::new(Variable::new(so2::SO2::from_param_slice(params.as_slice())))
424 }
425 ManifoldType::SO3 => {
426 Box::new(Variable::new(so3::SO3::from_param_slice(params.as_slice())))
427 }
428 ManifoldType::SE2 => {
429 Box::new(Variable::new(se2::SE2::from_param_slice(params.as_slice())))
430 }
431 ManifoldType::SE3 => {
432 Box::new(Variable::new(se3::SE3::from_param_slice(params.as_slice())))
433 }
434 ManifoldType::RN => Box::new(Variable::new(rn::Rn::new(params.clone()))),
435 ManifoldType::SE23 => Box::new(Variable::new(se23::SE23::from_param_slice(
436 params.as_slice(),
437 ))),
438 ManifoldType::SGal3 => Box::new(Variable::new(sgal3::SGal3::from_param_slice(
439 params.as_slice(),
440 ))),
441 ManifoldType::Sim3 => Box::new(Variable::new(sim3::Sim3::from_param_slice(
442 params.as_slice(),
443 ))),
444 }
445 }
446
447 pub fn apply_constraints_to_variables(
451 &self,
452 variables: &mut SlotMap<VarKey, Box<dyn ManifoldVariable>>,
453 ) {
454 for (key, var) in variables.iter_mut() {
455 if let Some(indexes) = self.fixed_variable_indexes.get(key) {
456 var.set_fixed_indices(indexes.clone());
457 }
458 if let Some(bounds) = self.variable_bounds.get(key) {
459 var.set_bounds(bounds.clone());
460 }
461 }
462 }
463
464 pub fn num_residual_blocks(&self) -> usize {
465 self.residual_blocks.len()
466 }
467
468 pub fn total_residual_dimension(&self) -> usize {
472 self.total_residual_dimension
473 }
474
475 pub(crate) fn residual_blocks(&self) -> &SlotMap<FactorKey, ResidualBlock> {
476 &self.residual_blocks
477 }
478
479 pub fn compute_residual_sparse(
481 &self,
482 variables: &SlotMap<VarKey, Box<dyn ManifoldVariable>>,
483 ) -> CoreResult<Mat<f64>> {
484 Ok(self.compute_residual_and_cost_sparse(variables)?.0)
485 }
486
487 pub fn compute_residual_and_cost_sparse(
498 &self,
499 variables: &SlotMap<VarKey, Box<dyn ManifoldVariable>>,
500 ) -> CoreResult<(Mat<f64>, f64)> {
501 let mut workspace = crate::linearizer::AssemblyWorkspace::build(self);
502 self.compute_residual_and_cost_sparse_with_workspace(variables, &mut workspace)
503 }
504
505 pub(crate) fn compute_residual_and_cost_sparse_with_workspace(
511 &self,
512 variables: &SlotMap<VarKey, Box<dyn ManifoldVariable>>,
513 workspace: &mut crate::linearizer::AssemblyWorkspace,
514 ) -> CoreResult<(Mat<f64>, f64)> {
515 use crate::linearizer::split_by_row_offsets_mut;
516
517 workspace.residual_buf.fill(0.0);
518 let residual_slices =
519 split_by_row_offsets_mut(&mut workspace.residual_buf, &workspace.offsets_lens);
520
521 let results: Vec<CoreResult<f64>> = residual_slices
524 .into_par_iter()
525 .zip(workspace.block_order.par_iter())
526 .map(|(slice, key)| {
527 let block = &self.residual_blocks[*key];
528 self.compute_residual_block(block, variables, slice)
529 })
530 .collect();
531 let cost: f64 = results.into_iter().sum::<CoreResult<f64>>()?;
532
533 let n = self.total_residual_dimension;
534 Ok((Mat::from_fn(n, 1, |i, _| workspace.residual_buf[i]), cost))
535 }
536
537 pub fn compute_residual_and_jacobian_sparse(
539 &self,
540 variables: &SlotMap<VarKey, Box<dyn ManifoldVariable>>,
541 variable_index_map: &SecondaryMap<VarKey, usize>,
542 symbolic_structure: &SymbolicStructure,
543 ) -> CoreResult<(Mat<f64>, SparseColMat<usize, f64>)> {
544 let mut workspace = crate::linearizer::AssemblyWorkspace::build(self);
545 Ok(crate::linearizer::cpu::sparse::assemble_sparse(
546 self,
547 variables,
548 variable_index_map,
549 symbolic_structure,
550 &mut workspace,
551 )?)
552 }
553
554 pub fn compute_residual_and_jacobian_dense(
556 &self,
557 variables: &SlotMap<VarKey, Box<dyn ManifoldVariable>>,
558 variable_index_map: &SecondaryMap<VarKey, usize>,
559 total_dof: usize,
560 ) -> CoreResult<(Mat<f64>, Mat<f64>)> {
561 let mut workspace = crate::linearizer::AssemblyWorkspace::build(self);
562 Ok(crate::linearizer::cpu::dense::assemble_dense(
563 self,
564 variables,
565 variable_index_map,
566 total_dof,
567 &mut workspace,
568 )?)
569 }
570
571 fn compute_residual_block(
577 &self,
578 residual_block: &ResidualBlock,
579 variables: &SlotMap<VarKey, Box<dyn ManifoldVariable>>,
580 residual_slice: &mut [f64],
581 ) -> CoreResult<f64> {
582 let (_, cost) =
585 crate::linearizer::compute_block_into(residual_block, variables, residual_slice, None)?;
586 Ok(cost)
587 }
588
589 pub fn log_residual_to_file(
590 &self,
591 residual: &nalgebra::DVector<f64>,
592 filename: &str,
593 ) -> Result<(), Error> {
594 let mut file = File::create(filename)?;
595 writeln!(file, "# Residual vector - {} elements", residual.len())?;
596 for (i, &value) in residual.iter().enumerate() {
597 writeln!(file, "{}: {:.12}", i, value)?;
598 }
599 Ok(())
600 }
601
602 pub fn log_sparse_jacobian_to_file(
603 &self,
604 jacobian: &SparseColMat<usize, f64>,
605 filename: &str,
606 ) -> Result<(), Error> {
607 let mut file = File::create(filename)?;
608 writeln!(
609 file,
610 "# Sparse Jacobian matrix - {} x {} ({} non-zeros)",
611 jacobian.nrows(),
612 jacobian.ncols(),
613 jacobian.compute_nnz()
614 )?;
615 writeln!(file, "# Matrix saved as dimensions and non-zero count only")?;
616 Ok(())
617 }
618
619 pub fn log_variables_to_file(
620 &self,
621 variables: &SlotMap<VarKey, Box<dyn ManifoldVariable>>,
622 filename: &str,
623 ) -> Result<(), Error> {
624 let mut file = File::create(filename)?;
625 writeln!(file, "# Variables - {} total", variables.len())?;
626 for (_, var) in variables {
627 let vec = var.to_dvector();
628 write!(file, "[")?;
629 for (i, &v) in vec.iter().enumerate() {
630 write!(file, "{:.12}", v)?;
631 if i < vec.len() - 1 {
632 write!(file, ", ")?;
633 }
634 }
635 writeln!(file, "]")?;
636 }
637 Ok(())
638 }
639
640 pub fn compute_and_set_covariances(
659 &self,
660 variables: &mut SlotMap<VarKey, Box<dyn ManifoldVariable>>,
661 options: crate::linalg::covariance::CovarianceOptions,
662 ) -> Option<SecondaryMap<VarKey, Mat<f64>>> {
663 let covariance =
664 match crate::linalg::covariance::Covariance::compute(options, self, variables) {
665 Ok(covariance) => covariance,
666 Err(e) => {
667 tracing::error!("Covariance estimation failed: {e}");
668 return None;
669 }
670 };
671
672 let per_var = covariance.per_variable();
673 for (key, cov) in &per_var {
674 if let Some(var) = variables.get_mut(key) {
675 var.set_covariance(cov.clone());
676 }
677 }
678 Some(per_var)
679 }
680}
681
682#[cfg(test)]
683mod tests {
684 use super::*;
685 use crate::core::loss_functions::HuberLoss;
686 use crate::factors::pose::{BetweenFactor, PriorFactor};
687 use apex_manifolds::{ManifoldType, se2::SE2, se3::SE3};
688 use nalgebra::{Quaternion, Vector3, dvector};
689
690 type TestResult = Result<(), Box<dyn std::error::Error>>;
691
692 fn create_se2_test_problem() -> Result<(Problem, Vec<VarKey>), Box<dyn std::error::Error>> {
693 let mut problem = Problem::new(JacobianMode::Sparse);
694
695 let poses = [
696 (0.0_f64, 0.0, 0.0),
697 (1.0, 0.0, 0.1),
698 (1.5, 1.0, 0.5),
699 (1.0, 2.0, 1.0),
700 (0.0, 2.5, 1.5),
701 (-1.0, 2.0, 2.0),
702 (-1.5, 1.0, 2.5),
703 (-1.0, 0.0, 3.0),
704 (-0.5, -0.5, -2.8),
705 (0.5, -0.5, -2.3),
706 ];
707
708 let keys: Vec<VarKey> = poses
709 .iter()
710 .map(|&(x, y, t)| problem.add_variable(ManifoldType::SE2, dvector![x, y, t]))
711 .collect();
712
713 for i in 0..9 {
714 let (fx, fy, ft) = poses[i];
715 let (tx, ty, tt) = poses[i + 1];
716 problem.add_residual_block(
717 &[keys[i], keys[i + 1]],
718 Box::new(BetweenFactor::new(SE2::from_xy_angle(
719 tx - fx,
720 ty - fy,
721 tt - ft,
722 ))),
723 Some(Box::new(HuberLoss::new(1.0)?)),
724 );
725 }
726
727 let (fx, fy, ft) = poses[9];
728 let (tx, ty, tt) = poses[0];
729 problem.add_residual_block(
730 &[keys[9], keys[0]],
731 Box::new(BetweenFactor::new(SE2::from_xy_angle(
732 tx - fx,
733 ty - fy,
734 tt - ft,
735 ))),
736 Some(Box::new(HuberLoss::new(1.0)?)),
737 );
738
739 problem.add_residual_block(
740 &[keys[0]],
741 Box::new(PriorFactor::<SE2>::new(SE2::identity())),
742 None,
743 );
744
745 Ok((problem, keys))
746 }
747
748 fn create_se3_test_problem() -> Result<(Problem, Vec<VarKey>), Box<dyn std::error::Error>> {
749 let mut problem = Problem::new(JacobianMode::Sparse);
750
751 let poses = [
753 (0.0_f64, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0),
754 (1.0, 0.0, 0.0, 0.0, 0.0, 0.1, 0.995),
755 (1.0, 1.0, 0.0, 0.0, 0.0, 0.2, 0.98),
756 (0.0, 1.0, 0.0, 0.0, 0.0, 0.3, 0.955),
757 (0.0, 0.0, 1.0, 0.1, 0.0, 0.0, 0.995),
758 (1.0, 0.0, 1.0, 0.1, 0.0, 0.1, 0.99),
759 (1.0, 1.0, 1.0, 0.1, 0.0, 0.2, 0.975),
760 (0.0, 1.0, 1.0, 0.1, 0.0, 0.3, 0.95),
761 ];
762
763 let keys: Vec<VarKey> = poses
764 .iter()
765 .map(|&(tx, ty, tz, qx, qy, qz, qw)| {
766 problem.add_variable(ManifoldType::SE3, dvector![tx, ty, tz, qw, qx, qy, qz])
767 })
768 .collect();
769
770 let edges = [
771 (0, 1),
772 (1, 2),
773 (2, 3),
774 (3, 0),
775 (4, 5),
776 (5, 6),
777 (6, 7),
778 (7, 4),
779 (0, 4),
780 (1, 5),
781 (2, 6),
782 (3, 7),
783 ];
784
785 for (f, t) in edges {
786 let fp = poses[f];
787 let tp = poses[t];
788 let rel = SE3::from_translation_quaternion(
789 Vector3::new(tp.0 - fp.0, tp.1 - fp.1, tp.2 - fp.2),
790 Quaternion::new(1.0, 0.0, 0.0, 0.0),
791 );
792 problem.add_residual_block(
793 &[keys[f], keys[t]],
794 Box::new(BetweenFactor::new(rel)),
795 Some(Box::new(HuberLoss::new(1.0)?)),
796 );
797 }
798
799 problem.add_residual_block(
800 &[keys[0]],
801 Box::new(PriorFactor::<SE3>::new(SE3::identity())),
802 None,
803 );
804
805 Ok((problem, keys))
806 }
807
808 #[test]
809 fn test_problem_construction_se2() -> TestResult {
810 let (problem, keys) = create_se2_test_problem()?;
811 assert_eq!(problem.num_residual_blocks(), 11);
812 assert_eq!(problem.total_residual_dimension, 33);
813 assert_eq!(keys.len(), 10);
814 assert_eq!(problem.variables.len(), 10);
815 Ok(())
816 }
817
818 #[test]
819 fn test_problem_construction_se3() -> TestResult {
820 let (problem, keys) = create_se3_test_problem()?;
821 assert_eq!(problem.num_residual_blocks(), 13);
822 assert_eq!(keys.len(), 8);
823 assert_eq!(problem.variables.len(), 8);
824 Ok(())
825 }
826
827 #[test]
828 fn test_add_variable_returns_distinct_keys() {
829 let mut problem = Problem::new(JacobianMode::Sparse);
830 let k0 = problem.add_variable(ManifoldType::SE2, dvector![0.0, 0.0, 0.0]);
831 let k1 = problem.add_variable(ManifoldType::SE2, dvector![1.0, 0.0, 0.1]);
832 assert_ne!(k0, k1);
833 assert_eq!(problem.variables.len(), 2);
834 assert_eq!(problem.variables[k0].dof(), 3);
835 }
836
837 #[test]
838 fn test_variable_all_manifold_types() {
839 let mut p = Problem::new(JacobianMode::Sparse);
840 let so2 = p.add_variable(ManifoldType::SO2, dvector![0.5]);
841 let so3 = p.add_variable(ManifoldType::SO3, dvector![1.0, 0.0, 0.0, 0.0]);
842 let se2 = p.add_variable(ManifoldType::SE2, dvector![1.0, 2.0, 0.5]);
843 let se3 = p.add_variable(
844 ManifoldType::SE3,
845 dvector![1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0],
846 );
847 let rn = p.add_variable(ManifoldType::RN, dvector![5.0, 6.0]);
848
849 assert_eq!(p.variables[so2].manifold_type_name(), "SO2");
850 assert_eq!(p.variables[so3].manifold_type_name(), "SO3");
851 assert_eq!(p.variables[se2].manifold_type_name(), "SE2");
852 assert_eq!(p.variables[se3].manifold_type_name(), "SE3");
853 assert_eq!(p.variables[rn].manifold_type_name(), "Rn");
854 }
855
856 #[test]
857 fn test_residual_block_add_remove() -> TestResult {
858 let mut p = Problem::new(JacobianMode::Sparse);
859 let k0 = p.add_variable(ManifoldType::SE2, dvector![0.0, 0.0, 0.0]);
860 let k1 = p.add_variable(ManifoldType::SE2, dvector![1.0, 0.0, 0.1]);
861
862 let fk1 = p.add_residual_block(
863 &[k0, k1],
864 Box::new(BetweenFactor::new(SE2::from_xy_angle(1.0, 0.0, 0.1))),
865 Some(Box::new(HuberLoss::new(1.0)?)),
866 );
867 let fk2 = p.add_residual_block(
868 &[k0],
869 Box::new(PriorFactor::<SE2>::new(SE2::identity())),
870 None,
871 );
872
873 assert_ne!(fk1, fk2);
874 assert_eq!(p.num_residual_blocks(), 2);
875 assert_eq!(p.total_residual_dimension, 6);
876
877 let removed = p.remove_residual_block(fk1);
878 assert!(removed.is_some());
879 assert_eq!(p.num_residual_blocks(), 1);
880 assert_eq!(p.total_residual_dimension, 3);
881
882 assert!(p.remove_residual_block(fk1).is_none());
883 Ok(())
884 }
885
886 #[test]
887 fn test_fix_unfix_variable() {
888 let mut p = Problem::new(JacobianMode::Sparse);
889 let k0 = p.add_variable(ManifoldType::SE2, dvector![0.0, 0.0, 0.0]);
890 let k1 = p.add_variable(ManifoldType::SE2, dvector![1.0, 0.0, 0.1]);
891
892 p.fix_variable(k0, 0);
893 p.fix_variable(k0, 1);
894 p.fix_variable(k1, 2);
895
896 assert_eq!(p.fixed_variable_indexes[k0].len(), 2);
897 assert_eq!(p.fixed_variable_indexes[k1].len(), 1);
898
899 p.unfix_variable(k0);
900 assert!(!p.fixed_variable_indexes.contains_key(k0));
901 assert!(p.fixed_variable_indexes.contains_key(k1));
902 }
903
904 #[test]
909 fn test_group_rows_for_elimination_preserves_cost() -> TestResult {
910 use crate::linearizer::AssemblyWorkspace;
911
912 let mut p = Problem::new(JacobianMode::Sparse);
913 let a = p.add_variable(ManifoldType::SE2, dvector![0.1, 0.2, 0.05]);
914 let b = p.add_variable(ManifoldType::SE2, dvector![1.0, 0.1, 0.02]);
915 let p0 = p.add_variable(ManifoldType::RN, dvector![0.3, 0.4, 0.5]);
916 let p1 = p.add_variable(ManifoldType::RN, dvector![0.6, 0.7, 0.8]);
917
918 for (x, y) in [(a, p0), (a, p1), (b, p0), (b, p1)] {
920 p.add_residual_block(
921 &[x, y],
922 Box::new(crate::factors::pose::BetweenFactor::new(
923 apex_manifolds::se2::SE2::from_xy_angle(0.5, 0.1, 0.01),
924 )),
925 None,
926 );
927 }
928 p.mark_for_elimination(p0);
929 p.mark_for_elimination(p1);
930
931 let variables = p.variables.clone();
932 let mut ws = AssemblyWorkspace::build(&p);
933 let (_, cost_before) =
934 p.compute_residual_and_cost_sparse_with_workspace(&variables, &mut ws)?;
935
936 assert!(
937 p.group_rows_for_elimination(),
938 "camera-major rows must move"
939 );
940
941 let mut ws = AssemblyWorkspace::build(&p);
942 let (_, cost_after) =
943 p.compute_residual_and_cost_sparse_with_workspace(&variables, &mut ws)?;
944 assert!(
945 (cost_before - cost_after).abs() < 1e-12,
946 "cost changed: {cost_before} -> {cost_after}"
947 );
948
949 for landmark in [p0, p1] {
951 let mut rows: Vec<usize> = p
952 .residual_blocks()
953 .values()
954 .filter(|b| b.variable_keys.contains(&landmark))
955 .flat_map(|b| {
956 b.residual_row_start_idx..b.residual_row_start_idx + b.factor.residual_dim()
957 })
958 .collect();
959 rows.sort_unstable();
960 let contiguous = rows.windows(2).all(|w| w[1] == w[0] + 1);
961 assert!(
962 contiguous,
963 "rows for {landmark:?} are not contiguous: {rows:?}"
964 );
965 }
966
967 assert!(
969 !p.group_rows_for_elimination(),
970 "a second call must be a no-op"
971 );
972 Ok(())
973 }
974
975 #[test]
983 fn test_group_rows_after_mutation_restores_layout() -> TestResult {
984 use crate::linearizer::AssemblyWorkspace;
985
986 let mut p = Problem::new(JacobianMode::Sparse);
987 let a = p.add_variable(ManifoldType::SE2, dvector![0.1, 0.2, 0.05]);
988 let b = p.add_variable(ManifoldType::SE2, dvector![1.0, 0.1, 0.02]);
989 let p0 = p.add_variable(ManifoldType::RN, dvector![0.3, 0.4, 0.5]);
990 let p1 = p.add_variable(ManifoldType::RN, dvector![0.6, 0.7, 0.8]);
991
992 for (x, y) in [(a, p0), (a, p1), (b, p0), (b, p1)] {
994 p.add_residual_block(
995 &[x, y],
996 Box::new(crate::factors::pose::BetweenFactor::new(
997 apex_manifolds::se2::SE2::from_xy_angle(0.5, 0.1, 0.01),
998 )),
999 None,
1000 );
1001 }
1002 p.mark_for_elimination(p0);
1003 p.mark_for_elimination(p1);
1004 assert!(
1005 p.group_rows_for_elimination(),
1006 "camera-major rows must move"
1007 );
1008 assert!(
1009 !p.group_rows_for_elimination(),
1010 "grouped layout must be stable"
1011 );
1012
1013 p.add_residual_block(
1015 &[a, b],
1016 Box::new(crate::factors::pose::BetweenFactor::new(
1017 apex_manifolds::se2::SE2::from_xy_angle(0.2, 0.0, 0.0),
1018 )),
1019 None,
1020 );
1021 assert!(
1022 p.group_rows_for_elimination(),
1023 "the appended unchunked block must move ahead of the chunks"
1024 );
1025
1026 let total = p.total_residual_dimension;
1028 let mut covered = vec![false; total];
1029 for block in p.residual_blocks().values() {
1030 let start = block.residual_row_start_idx;
1031 for (row, slot) in covered
1032 .iter_mut()
1033 .enumerate()
1034 .skip(start)
1035 .take(block.factor.residual_dim())
1036 {
1037 assert!(row < total, "row {row} out of range (total {total})");
1038 assert!(!*slot, "row {row} covered twice");
1039 *slot = true;
1040 }
1041 }
1042 assert!(covered.iter().all(|&c| c), "layout has gaps: {covered:?}");
1043
1044 let touches_eliminated = |keys: &[VarKey]| keys.contains(&p0) || keys.contains(&p1);
1046 let first_chunk_row = p
1047 .residual_blocks()
1048 .values()
1049 .filter(|b| touches_eliminated(&b.variable_keys))
1050 .map(|b| b.residual_row_start_idx)
1051 .min()
1052 .ok_or_else(|| CoreError::Variable("eliminated block missing".to_string()))?;
1053 for block in p.residual_blocks().values() {
1054 if !touches_eliminated(&block.variable_keys) {
1055 assert!(
1056 block.residual_row_start_idx < first_chunk_row,
1057 "unchunked block at {} is inside the chunked region (starts at {first_chunk_row})",
1058 block.residual_row_start_idx
1059 );
1060 }
1061 }
1062
1063 let variables = p.variables.clone();
1065 let mut ws = AssemblyWorkspace::build(&p);
1066 let (_, cost) = p.compute_residual_and_cost_sparse_with_workspace(&variables, &mut ws)?;
1067 assert!(cost.is_finite(), "cost must stay finite, got {cost}");
1068 assert!(!p.group_rows_for_elimination(), "layout must be stable now");
1069 Ok(())
1070 }
1071
1072 #[test]
1073 fn test_variable_bounds_set_remove() {
1074 let mut p = Problem::new(JacobianMode::Sparse);
1075 let k0 = p.add_variable(ManifoldType::SE2, dvector![0.0, 0.0, 0.0]);
1076 let k1 = p.add_variable(ManifoldType::SE2, dvector![0.0, 0.0, 0.0]);
1077
1078 p.set_variable_bounds(k0, 0, -1.0, 1.0);
1079 p.set_variable_bounds(k0, 1, -2.0, 2.0);
1080 p.set_variable_bounds(k1, 0, 0.0, 5.0);
1081
1082 assert_eq!(p.variable_bounds[k0].len(), 2);
1083 assert_eq!(p.variable_bounds[k1].len(), 1);
1084
1085 p.remove_variable_bounds(k0);
1086 assert!(!p.variable_bounds.contains_key(k0));
1087 assert!(p.variable_bounds.contains_key(k1));
1088 }
1089
1090 #[test]
1091 fn test_set_variable_bounds_invalid_order() {
1092 let mut p = Problem::new(JacobianMode::Sparse);
1093 let k = p.add_variable(ManifoldType::SE2, dvector![0.0, 0.0, 0.0]);
1094
1095 let Err(err) = p.try_set_variable_bounds(k, 0, 5.0, 1.0) else {
1098 panic!("inverted range must be rejected");
1099 };
1100 assert!(err.to_string().contains("exceeds upper bound"), "{err}");
1101 assert!(!p.variable_bounds.contains_key(k));
1102 }
1103
1104 #[test]
1108 fn test_set_variable_bounds_rejects_nan() {
1109 let mut p = Problem::new(JacobianMode::Sparse);
1110 let k = p.add_variable(ManifoldType::SE2, dvector![0.0, 0.0, 0.0]);
1111
1112 let Err(err) = p.try_set_variable_bounds(k, 0, f64::NAN, 1.0) else {
1113 panic!("NaN lower bound must be rejected");
1114 };
1115 assert!(err.to_string().contains("NaN"), "{err}");
1116
1117 let Err(err) = p.try_set_variable_bounds(k, 0, -1.0, f64::NAN) else {
1118 panic!("NaN upper bound must be rejected");
1119 };
1120 assert!(err.to_string().contains("NaN"), "{err}");
1121
1122 assert!(!p.variable_bounds.contains_key(k));
1123 assert!(
1124 p.try_set_variable_bounds(k, 0, f64::NEG_INFINITY, f64::INFINITY)
1125 .is_ok()
1126 );
1127 }
1128
1129 #[test]
1132 fn test_variable_constraints_reject_out_of_range_index() {
1133 let mut p = Problem::new(JacobianMode::Sparse);
1134 let k = p.add_variable(ManifoldType::SE2, dvector![0.0, 0.0, 0.0]); let Err(err) = p.try_fix_variable(k, 3) else {
1137 panic!("index 3 is out of range for a 3-DOF variable");
1138 };
1139 assert!(err.to_string().contains("3-DOF"), "{err}");
1140
1141 let Err(err) = p.try_set_variable_bounds(k, 7, -1.0, 1.0) else {
1142 panic!("index 7 is out of range for a 3-DOF variable");
1143 };
1144 assert!(err.to_string().contains("3-DOF"), "{err}");
1145
1146 assert!(!p.fixed_variable_indexes.contains_key(k));
1147 assert!(!p.variable_bounds.contains_key(k));
1148 }
1149
1150 #[test]
1151 fn test_problem_default_equals_new_sparse() {
1152 let d = Problem::default();
1153 let n = Problem::new(JacobianMode::Sparse);
1154 assert_eq!(d.jacobian_mode, n.jacobian_mode);
1155 assert_eq!(d.num_residual_blocks(), 0);
1156 }
1157
1158 #[test]
1159 fn test_compute_residual_sparse_smoke() -> TestResult {
1160 let (problem, _) = create_se2_test_problem()?;
1161 let residual = problem.compute_residual_sparse(&problem.variables)?;
1162 let norm_sq: f64 = (0..residual.nrows())
1163 .map(|i| residual[(i, 0)].powi(2))
1164 .sum();
1165 assert!(norm_sq >= 0.0);
1166 assert_eq!(residual.nrows(), problem.total_residual_dimension);
1167 Ok(())
1168 }
1169
1170 struct IdentityFactor;
1176
1177 impl crate::factors::Factor for IdentityFactor {
1178 fn linearize(
1179 &self,
1180 params: &[&[f64]],
1181 residual: &mut [f64],
1182 jacobian: Option<faer::mat::MatMut<'_, f64>>,
1183 ) {
1184 residual[0] = params[0][0];
1185 if let Some(mut jac) = jacobian {
1186 use faer::prelude::ReborrowMut;
1187 *jac.rb_mut().get_mut(0, 0) = 1.0;
1188 }
1189 }
1190 fn residual_dim(&self) -> usize {
1191 1
1192 }
1193 fn jacobian_shape(&self) -> (usize, usize) {
1194 (1, 1)
1195 }
1196 }
1197
1198 #[test]
1204 fn robust_cost_is_half_rho_not_half_corrected_residual_squared() -> TestResult {
1205 let mut problem = Problem::new(JacobianMode::Sparse);
1206 let x = problem.add_variable(ManifoldType::RN, dvector![2.0]);
1207 problem.add_residual_block(
1208 &[x],
1209 Box::new(IdentityFactor),
1210 Some(Box::new(HuberLoss::new(1.0)?)),
1211 );
1212
1213 let (residual, cost) = problem.compute_residual_and_cost_sparse(&problem.variables)?;
1214
1215 assert!(
1216 (cost - 1.5).abs() < 1e-12,
1217 "robust cost should be 0.5·ρ(4) = 1.5, got {cost}"
1218 );
1219
1220 let from_corrected_residual = 0.5 * residual.squared_norm_l2();
1223 assert!(
1224 (from_corrected_residual - 1.0).abs() < 1e-12,
1225 "corrected-residual cost should be 1.0, got {from_corrected_residual}"
1226 );
1227 Ok(())
1228 }
1229
1230 #[test]
1233 fn cost_without_loss_is_half_squared_norm() -> TestResult {
1234 let mut problem = Problem::new(JacobianMode::Sparse);
1235 let x = problem.add_variable(ManifoldType::RN, dvector![3.0]);
1236 problem.add_residual_block(&[x], Box::new(IdentityFactor), None);
1237
1238 let (residual, cost) = problem.compute_residual_and_cost_sparse(&problem.variables)?;
1239 assert!(
1240 (cost - 4.5).abs() < 1e-12,
1241 "expected 0.5·3² = 4.5, got {cost}"
1242 );
1243 assert!((cost - 0.5 * residual.squared_norm_l2()).abs() < 1e-15);
1244 Ok(())
1245 }
1246
1247 #[test]
1250 fn robust_cost_matches_loss_function_rho() -> TestResult {
1251 use crate::core::loss_functions::{CauchyLoss, LossFunction};
1252
1253 let value = 2.5;
1254 let mut problem = Problem::new(JacobianMode::Sparse);
1255 let x = problem.add_variable(ManifoldType::RN, dvector![value]);
1256 problem.add_residual_block(
1257 &[x],
1258 Box::new(IdentityFactor),
1259 Some(Box::new(CauchyLoss::new(1.0)?)),
1260 );
1261
1262 let (_, cost) = problem.compute_residual_and_cost_sparse(&problem.variables)?;
1263 let expected = 0.5 * CauchyLoss::new(1.0)?.evaluate(value * value)[0];
1264 assert!(
1265 (cost - expected).abs() < 1e-12,
1266 "expected 0.5·ρ(s) = {expected}, got {cost}"
1267 );
1268 Ok(())
1269 }
1270
1271 #[test]
1272 fn test_variable_covariance_lifecycle() -> TestResult {
1273 use faer::Mat;
1274 let mut p = Problem::new(JacobianMode::Sparse);
1275 let k = p.add_variable(ManifoldType::SE2, dvector![0.0, 0.0, 0.0]);
1276
1277 assert!(p.variables[k].covariance().is_none());
1278 p.variables[k].set_covariance(Mat::identity(3, 3));
1279 let cov = p.variables[k].covariance().ok_or("no cov")?;
1280 assert_eq!(cov.nrows(), 3);
1281 p.variables[k].clear_covariance();
1282 assert!(p.variables[k].covariance().is_none());
1283 Ok(())
1284 }
1285
1286 #[test]
1287 fn test_fixed_indices_stored_correctly() {
1288 let mut p = Problem::new(JacobianMode::Sparse);
1289 let k = p.add_variable(ManifoldType::SE2, dvector![0.0, 0.0, 0.0]);
1290 p.fix_variable(k, 0);
1291 p.fix_variable(k, 2);
1292 assert_eq!(p.fixed_variable_indexes[k].len(), 2);
1293 assert!(p.fixed_variable_indexes[k].contains(&0));
1294 assert!(p.fixed_variable_indexes[k].contains(&2));
1295 }
1296
1297 #[test]
1298 fn test_log_residual_to_file() -> TestResult {
1299 let p = Problem::new(JacobianMode::Sparse);
1300 let res = nalgebra::dvector![1.0, 2.0, 3.0];
1301 let path = std::env::temp_dir().join("apex_test_residual.txt");
1302 p.log_residual_to_file(&res, path.to_str().ok_or("bad path")?)?;
1303 assert!(path.exists());
1304 Ok(())
1305 }
1306
1307 #[test]
1308 fn test_log_variables_to_file() -> TestResult {
1309 let mut p = Problem::new(JacobianMode::Sparse);
1310 p.add_variable(ManifoldType::SE2, dvector![1.0, 2.0, 0.3]);
1311 let vars = p.variables.clone();
1312 let path = std::env::temp_dir().join("apex_test_variables.txt");
1313 p.log_variables_to_file(&vars, path.to_str().ok_or("bad path")?)?;
1314 assert!(path.exists());
1315 Ok(())
1316 }
1317
1318 #[test]
1319 fn test_log_sparse_jacobian_to_file() -> TestResult {
1320 use faer::sparse::SparseColMat;
1321 let p = Problem::new(JacobianMode::Sparse);
1322 let triplets = vec![faer::sparse::Triplet::new(0usize, 0usize, 1.0f64)];
1323 let jac =
1324 SparseColMat::try_new_from_triplets(1, 1, &triplets).map_err(|e| format!("{e:?}"))?;
1325 let path = std::env::temp_dir().join("apex_test_jacobian.txt");
1326 p.log_sparse_jacobian_to_file(&jac, path.to_str().ok_or("bad path")?)?;
1327 assert!(path.exists());
1328 Ok(())
1329 }
1330
1331 #[test]
1332 fn test_apply_tangent_step_se2() -> TestResult {
1333 let mut p = Problem::new(JacobianMode::Sparse);
1334 let k = p.add_variable(ManifoldType::SE2, dvector![0.0, 0.0, 0.0]);
1335 p.variables[k].apply_tangent_step(&[1.0, 2.0, 3.0]);
1336 assert_eq!(p.variables[k].dof(), 3);
1337 Ok(())
1338 }
1339
1340 #[test]
1341 fn test_variable_rn_values() -> TestResult {
1342 let mut p = Problem::new(JacobianMode::Sparse);
1343 let k = p.add_variable(ManifoldType::RN, dvector![5.0, 6.0]);
1344 let vec = p.variables[k].to_dvector();
1345 assert!((vec[0] - 5.0).abs() < 1e-10);
1346 assert!((vec[1] - 6.0).abs() < 1e-10);
1347 Ok(())
1348 }
1349
1350 mod validation {
1355 use super::*;
1356 use crate::factors::visual::projection::ProjectionFactor;
1357 use crate::factors::{BundleAdjustment, Factor};
1358 use apex_camera_models::PinholeCamera;
1359 use nalgebra::Matrix2xX;
1360 use rn::Rn;
1361
1362 fn ba_factor(n_landmarks: usize) -> Box<dyn Factor + Send + Sync> {
1363 let observations = Matrix2xX::from_fn(n_landmarks, |r, c| (r + c) as f64);
1364 Box::new(ProjectionFactor::<PinholeCamera, BundleAdjustment>::new(
1365 observations,
1366 PinholeCamera::from([500.0, 500.0, 320.0, 240.0]),
1367 ))
1368 }
1369
1370 fn pose_and_landmarks(problem: &mut Problem, n_landmark_params: usize) -> (VarKey, VarKey) {
1371 let pose = problem.add_variable(
1372 ManifoldType::SE3,
1373 dvector![0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0],
1374 );
1375 let landmarks = problem.add_variable(
1376 ManifoldType::RN,
1377 DVector::from_element(n_landmark_params, 1.0),
1378 );
1379 (pose, landmarks)
1380 }
1381
1382 #[test]
1383 fn try_add_accepts_matching_landmark_variable() -> TestResult {
1384 let mut problem = Problem::new(JacobianMode::Sparse);
1385 let (pose, landmarks) = pose_and_landmarks(&mut problem, 9);
1386 let key = problem.try_add_residual_block(&[pose, landmarks], ba_factor(3), None)?;
1387 assert_eq!(problem.residual_blocks().len(), 1);
1388 assert_eq!(problem.total_residual_dimension(), 6);
1389 let _ = key;
1390 Ok(())
1391 }
1392
1393 #[test]
1394 fn try_add_rejects_mismatched_landmark_variable() -> TestResult {
1395 let mut problem = Problem::new(JacobianMode::Sparse);
1396 let (pose, landmarks) = pose_and_landmarks(&mut problem, 12);
1397 let err = problem
1398 .try_add_residual_block(&[pose, landmarks], ba_factor(3), None)
1399 .err()
1400 .ok_or("mismatched landmark count must be rejected")?;
1401 assert!(matches!(err, CoreError::DimensionMismatch(_)), "{err}");
1402 Ok(())
1403 }
1404
1405 #[test]
1406 fn add_residual_block_panics_on_invalid_registration() {
1407 let mut problem = Problem::new(JacobianMode::Sparse);
1408 let (pose, landmarks) = pose_and_landmarks(&mut problem, 12);
1409 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1410 problem.add_residual_block(&[pose, landmarks], ba_factor(3), None);
1411 }));
1412 assert!(result.is_err(), "registration panic expected");
1413 }
1414
1415 #[test]
1416 fn try_add_rejects_unknown_variable_key() -> TestResult {
1417 let mut problem = Problem::new(JacobianMode::Sparse);
1418 let (pose, _landmarks) = pose_and_landmarks(&mut problem, 9);
1419
1420 let mut other_store: SlotMap<VarKey, Box<dyn ManifoldVariable>> = SlotMap::with_key();
1423 let mut foreign_key = None;
1424 for i in 0..5 {
1425 foreign_key =
1426 Some(other_store.insert(Box::new(Variable::new(Rn::new(dvector![i as f64])))));
1427 }
1428 let foreign_key = foreign_key.ok_or("inserted five keys")?;
1429
1430 let err = problem
1431 .try_add_residual_block(&[pose, foreign_key], ba_factor(3), None)
1432 .err()
1433 .ok_or("unknown variable key must be rejected")?;
1434 assert!(matches!(err, CoreError::Variable(_)), "{err}");
1435 Ok(())
1436 }
1437
1438 struct WrongRowFactor;
1446
1447 impl Factor for WrongRowFactor {
1448 fn linearize(
1449 &self,
1450 _params: &[&[f64]],
1451 _residual: &mut [f64],
1452 _jacobian: Option<faer::mat::MatMut<'_, f64>>,
1453 ) {
1454 }
1455 fn residual_dim(&self) -> usize {
1456 2
1457 }
1458 fn jacobian_shape(&self) -> (usize, usize) {
1459 (3, 1)
1460 }
1461 }
1462
1463 struct WrongColFactor;
1467
1468 impl Factor for WrongColFactor {
1469 fn linearize(
1470 &self,
1471 _params: &[&[f64]],
1472 _residual: &mut [f64],
1473 _jacobian: Option<faer::mat::MatMut<'_, f64>>,
1474 ) {
1475 }
1476 fn residual_dim(&self) -> usize {
1477 1
1478 }
1479 fn jacobian_shape(&self) -> (usize, usize) {
1480 (1, 2)
1481 }
1482 }
1483
1484 #[test]
1485 fn try_add_rejects_jacobian_rows_not_equal_to_residual_dim() -> TestResult {
1486 let mut problem = Problem::new(JacobianMode::Sparse);
1487 let x = problem.add_variable(ManifoldType::RN, dvector![1.0]);
1488 let err = problem
1489 .try_add_residual_block(&[x], Box::new(WrongRowFactor), None)
1490 .err()
1491 .ok_or("row mismatch must be rejected")?;
1492 assert!(matches!(err, CoreError::DimensionMismatch(_)), "{err}");
1493 Ok(())
1494 }
1495
1496 #[test]
1497 fn try_add_rejects_jacobian_cols_not_matching_total_dof() -> TestResult {
1498 let mut problem = Problem::new(JacobianMode::Sparse);
1499 let x = problem.add_variable(ManifoldType::RN, dvector![1.0]);
1500 let err = problem
1501 .try_add_residual_block(&[x], Box::new(WrongColFactor), None)
1502 .err()
1503 .ok_or("column mismatch must be rejected")?;
1504 assert!(matches!(err, CoreError::DimensionMismatch(_)), "{err}");
1505 Ok(())
1506 }
1507
1508 fn scalar_marginal() -> Box<dyn Factor + Send + Sync> {
1514 Box::new(
1515 crate::factors::marginal::MarginalPriorFactor::new(
1516 vec![1],
1517 nalgebra::DMatrix::identity(1, 1),
1518 nalgebra::DVector::zeros(1),
1519 Box::new(|params: &[&[f64]], out: &mut [f64]| out[0] = params[0][0]),
1520 )
1521 .unwrap_or_else(|e| panic!("marginal prior construction: {e}")),
1522 )
1523 }
1524
1525 #[test]
1529 fn try_add_rejects_noise_model_on_self_whitening_marginal_prior() -> TestResult {
1530 let mut problem = Problem::new(JacobianMode::Sparse);
1531 let x = problem.add_variable(ManifoldType::RN, dvector![1.0]);
1532 let noise = NoiseModel::from_sigmas(&[0.5]).map_err(|e| format!("noise model: {e}"))?;
1533 let err = problem
1534 .try_add_residual_block_with_noise(&[x], scalar_marginal(), None, noise)
1535 .err()
1536 .ok_or("noise model on a self-whitening factor must be rejected")?;
1537 assert!(matches!(err, CoreError::InvalidInput(_)), "{err}");
1538
1539 problem.try_add_residual_block_with_noise(
1541 &[x],
1542 scalar_marginal(),
1543 None,
1544 NoiseModel::Null,
1545 )?;
1546 Ok(())
1547 }
1548 }
1549}