1pub mod cpu;
26
27use rayon::prelude::*;
28use slotmap::{SecondaryMap, SlotMap};
29
30use faer::Mat;
31use faer::sparse::SparseColMat;
32use smallvec::SmallVec;
33use thiserror::Error;
34
35use crate::core::problem::Problem;
36use crate::core::variable::ManifoldVariable;
37use crate::core::{FactorKey, VarKey};
38use crate::error::ErrorLogging;
39use crate::{
40 core::{corrector::Corrector, residual_block::ResidualBlock},
41 linearizer::cpu::{DenseMode, LinearizationMode, SparseMode},
42};
43
44pub use cpu::sparse::SymbolicStructure;
45
46#[derive(Debug, Clone, Error)]
64pub enum LinearizerError {
65 #[error("Symbolic structure error: {0}")]
67 SymbolicStructure(String),
68
69 #[error("Parallel computation error: {0}")]
71 ParallelComputation(String),
72
73 #[error("Variable error: {0}")]
75 Variable(String),
76
77 #[error("Factor linearization failed: {0}")]
79 FactorLinearization(String),
80
81 #[error("Invalid input: {0}")]
83 InvalidInput(String),
84}
85
86pub type LinearizerResult<T> = Result<T, LinearizerError>;
88
89pub(crate) struct BlockLinearization {
99 pub variable_local_idx_size_list: SmallVec<[(usize, usize); 8]>,
102 pub residual_row_start_idx: usize,
104 pub residual_dim: usize,
106}
107
108pub(crate) fn split_by_row_offsets_mut<'a>(
113 buf: &'a mut [f64],
114 sorted_offsets_lens: &[(usize, usize)],
115) -> Vec<&'a mut [f64]> {
116 let mut remaining = buf;
117 let mut result = Vec::with_capacity(sorted_offsets_lens.len());
118 let mut current = 0usize;
119 for &(start, len) in sorted_offsets_lens {
120 let gap = start - current;
121 let (_, rest) = remaining.split_at_mut(gap);
122 let (slice, rest2) = rest.split_at_mut(len);
123 result.push(slice);
124 remaining = rest2;
125 current = start + len;
126 }
127 result
128}
129
130pub struct AssemblyWorkspace {
137 pub(crate) block_order: Vec<FactorKey>,
139 pub(crate) offsets_lens: Vec<(usize, usize)>,
141 pub(crate) jac_arena: Vec<f64>,
143 pub(crate) jac_offsets: Vec<(usize, usize)>,
145 pub(crate) residual_buf: Vec<f64>,
147 pub(crate) jacobian_values: Vec<f64>,
149}
150
151impl AssemblyWorkspace {
152 pub(crate) fn build(problem: &Problem) -> Self {
154 let mut blocks: Vec<(FactorKey, &ResidualBlock)> =
155 problem.residual_blocks().iter().collect();
156 blocks.sort_by_key(|(_, b)| b.residual_row_start_idx);
157
158 let mut offsets_lens = Vec::with_capacity(blocks.len());
159 let mut jac_offsets = Vec::with_capacity(blocks.len());
160 let mut total_jac_len = 0usize;
161 for (_, block) in &blocks {
162 offsets_lens.push((block.residual_row_start_idx, block.factor.residual_dim()));
163 let (r, c) = block.factor.jacobian_shape();
164 jac_offsets.push((total_jac_len, r * c));
165 total_jac_len += r * c;
166 }
167
168 AssemblyWorkspace {
169 block_order: blocks.into_iter().map(|(k, _)| k).collect(),
170 offsets_lens,
171 jac_arena: vec![0.0; total_jac_len],
172 jac_offsets,
173 residual_buf: vec![0.0; problem.total_residual_dimension],
174 jacobian_values: Vec::new(),
177 }
178 }
179
180 #[cfg(test)]
182 pub(crate) fn empty() -> Self {
183 AssemblyWorkspace {
184 block_order: Vec::new(),
185 offsets_lens: Vec::new(),
186 jac_arena: Vec::new(),
187 jac_offsets: Vec::new(),
188 residual_buf: Vec::new(),
189 jacobian_values: Vec::new(),
190 }
191 }
192}
193
194pub(crate) fn compute_block_into(
203 residual_block: &ResidualBlock,
204 variables: &SlotMap<VarKey, Box<dyn ManifoldVariable>>,
205 residual_slice: &mut [f64],
206 mut jacobian_buf: Option<&mut [f64]>,
207) -> LinearizerResult<(BlockLinearization, f64)> {
208 let mut param_slices: SmallVec<[&[f64]; 8]> = SmallVec::new();
209 let mut variable_local_idx_size_list: SmallVec<[(usize, usize); 8]> = SmallVec::new();
210 let mut count_variable_local_idx: usize = 0;
211
212 for &var_key in &residual_block.variable_keys {
213 let variable = variables.get(var_key).ok_or_else(|| {
218 LinearizerError::Variable(format!(
219 "residual block references variable key {var_key:?}, which is not in the \
220 variable map"
221 ))
222 .log()
223 })?;
224 param_slices.push(variable.as_param_slice());
225 let var_size = variable.dof();
226 variable_local_idx_size_list.push((count_variable_local_idx, var_size));
227 count_variable_local_idx += var_size;
228 }
229
230 let (rows, cols) = residual_block.factor.jacobian_shape();
231 match jacobian_buf.as_deref_mut() {
232 Some(buf) => {
233 debug_assert_eq!(buf.len(), rows * cols);
234 let jac_mut = faer::mat::MatMut::from_column_major_slice_mut(buf, rows, cols);
235 residual_block
236 .factor
237 .linearize(¶m_slices, residual_slice, Some(jac_mut));
238 }
239 None => residual_block
240 .factor
241 .linearize(¶m_slices, residual_slice, None),
242 }
243
244 residual_block.noise.whiten_residual(residual_slice);
248 if let Some(buf) = jacobian_buf.as_deref_mut() {
249 residual_block.noise.whiten_jacobian(buf, rows, cols);
250 }
251
252 let squared_norm: f64 = residual_slice.iter().map(|x| x * x).sum();
257 let cost = if let Some(loss_func) = &residual_block.loss_func {
258 let corrector = Corrector::new(loss_func.as_ref(), squared_norm);
259 if let Some(buf) = &mut jacobian_buf {
261 corrector.correct_jacobian_in_place(residual_slice, buf, rows, cols);
262 }
263 corrector.correct_residual_in_place(residual_slice);
264 corrector.robust_cost()
265 } else {
266 0.5 * squared_norm
267 };
268
269 Ok((
270 BlockLinearization {
271 variable_local_idx_size_list,
272 residual_row_start_idx: residual_block.residual_row_start_idx,
273 residual_dim: rows,
274 },
275 cost,
276 ))
277}
278
279pub trait AssemblyBackend: LinearizationMode {
294 fn assemble(
299 problem: &Problem,
300 variables: &SlotMap<VarKey, Box<dyn ManifoldVariable>>,
301 variable_index_map: &SecondaryMap<VarKey, usize>,
302 symbolic_structure: Option<&SymbolicStructure>,
303 total_dof: usize,
304 workspace: &mut AssemblyWorkspace,
305 ) -> LinearizerResult<(Mat<f64>, Self::Jacobian)>;
306
307 fn compute_column_norms(jacobian: &Self::Jacobian) -> Vec<f64>;
309
310 fn apply_column_scaling(jacobian: &Self::Jacobian, scaling: &[f64]) -> Self::Jacobian;
313
314 fn apply_inverse_scaling(step: &Mat<f64>, scaling: &[f64]) -> Mat<f64>;
316
317 fn hessian_vec_product(hessian: &Self::Hessian, vec: &Mat<f64>) -> Mat<f64>;
319}
320
321impl AssemblyBackend for SparseMode {
322 fn assemble(
323 problem: &Problem,
324 variables: &SlotMap<VarKey, Box<dyn ManifoldVariable>>,
325 variable_index_map: &SecondaryMap<VarKey, usize>,
326 symbolic_structure: Option<&SymbolicStructure>,
327 _total_dof: usize,
328 workspace: &mut AssemblyWorkspace,
329 ) -> LinearizerResult<(Mat<f64>, SparseColMat<usize, f64>)> {
330 let sym = symbolic_structure.ok_or_else(|| {
331 LinearizerError::InvalidInput("SparseMode requires symbolic structure".to_string())
332 })?;
333 crate::linearizer::cpu::sparse::assemble_sparse(
334 problem,
335 variables,
336 variable_index_map,
337 sym,
338 workspace,
339 )
340 }
341
342 fn compute_column_norms(jacobian: &SparseColMat<usize, f64>) -> Vec<f64> {
343 let ncols = jacobian.ncols();
344 let sparse_ref = jacobian.as_ref();
345 (0..ncols)
346 .into_par_iter()
347 .map(|c| {
348 let col_norm_squared: f64 =
349 sparse_ref.val_of_col(c).iter().map(|&val| val * val).sum();
350 col_norm_squared.sqrt()
351 })
352 .collect()
353 }
354
355 fn apply_column_scaling(
356 jacobian: &SparseColMat<usize, f64>,
357 scaling: &[f64],
358 ) -> SparseColMat<usize, f64> {
359 let ncols = jacobian.ncols();
363 let col_ptr = jacobian.symbolic().col_ptr();
364 let mut values = jacobian.as_ref().val().to_vec();
365 let offsets_lens: Vec<(usize, usize)> = (0..ncols)
366 .map(|c| (col_ptr[c], col_ptr[c + 1] - col_ptr[c]))
367 .collect();
368
369 let columns = split_by_row_offsets_mut(&mut values, &offsets_lens);
370 columns
371 .into_par_iter()
372 .zip((0..ncols).into_par_iter())
373 .for_each(|(column, c)| {
374 let s = scaling[c];
375 for v in column {
376 *v *= s;
377 }
378 });
379
380 let symbolic = match jacobian.symbolic().to_owned() {
381 Ok(symbolic) => symbolic,
382 Err(_) => return jacobian.clone(),
385 };
386 SparseColMat::new(symbolic, values)
387 }
388
389 fn apply_inverse_scaling(step: &Mat<f64>, scaling: &[f64]) -> Mat<f64> {
390 let mut result = step.clone();
391 for i in 0..step.nrows() {
392 result[(i, 0)] *= scaling[i];
393 }
394 result
395 }
396
397 fn hessian_vec_product(hessian: &SparseColMat<usize, f64>, vec: &Mat<f64>) -> Mat<f64> {
398 use std::ops::Mul;
399 hessian.as_ref().mul(vec)
400 }
401}
402
403impl AssemblyBackend for DenseMode {
404 fn assemble(
405 problem: &Problem,
406 variables: &SlotMap<VarKey, Box<dyn ManifoldVariable>>,
407 variable_index_map: &SecondaryMap<VarKey, usize>,
408 _symbolic_structure: Option<&SymbolicStructure>,
409 total_dof: usize,
410 workspace: &mut AssemblyWorkspace,
411 ) -> LinearizerResult<(Mat<f64>, Mat<f64>)> {
412 crate::linearizer::cpu::dense::assemble_dense(
413 problem,
414 variables,
415 variable_index_map,
416 total_dof,
417 workspace,
418 )
419 }
420
421 fn compute_column_norms(jacobian: &Mat<f64>) -> Vec<f64> {
422 let ncols = jacobian.ncols();
423 (0..ncols)
424 .map(|c| {
425 let mut norm_sq = 0.0;
426 for r in 0..jacobian.nrows() {
427 let v = jacobian[(r, c)];
428 norm_sq += v * v;
429 }
430 norm_sq.sqrt()
431 })
432 .collect()
433 }
434
435 fn apply_column_scaling(jacobian: &Mat<f64>, scaling: &[f64]) -> Mat<f64> {
436 let mut result = jacobian.clone();
437 for c in 0..jacobian.ncols() {
438 for r in 0..jacobian.nrows() {
439 result[(r, c)] *= scaling[c];
440 }
441 }
442 result
443 }
444
445 fn apply_inverse_scaling(step: &Mat<f64>, scaling: &[f64]) -> Mat<f64> {
446 let mut result = step.clone();
447 for i in 0..step.nrows() {
448 result[(i, 0)] *= scaling[i];
449 }
450 result
451 }
452
453 fn hessian_vec_product(hessian: &Mat<f64>, vec: &Mat<f64>) -> Mat<f64> {
454 hessian * vec
455 }
456}
457
458#[cfg(test)]
459mod tests {
460 use super::*;
461 use crate::{
462 core::{VarKey, problem::Problem},
463 factors,
464 linalg::JacobianMode,
465 };
466 use apex_manifolds::ManifoldType;
467 use faer::prelude::ReborrowMut;
468 use nalgebra::dvector;
469 use slotmap::{SecondaryMap, SlotMap};
470
471 type TestResult = Result<(), Box<dyn std::error::Error>>;
472
473 struct LinearFactor {
474 target: f64,
475 }
476
477 impl factors::Factor for LinearFactor {
478 fn linearize(
479 &self,
480 params: &[&[f64]],
481 residual: &mut [f64],
482 jacobian: Option<faer::mat::MatMut<'_, f64>>,
483 ) {
484 residual[0] = params[0][0] - self.target;
485 if let Some(mut jac) = jacobian {
486 *jac.rb_mut().get_mut(0, 0) = 1.0;
487 }
488 }
489 fn residual_dim(&self) -> usize {
490 1
491 }
492 fn jacobian_shape(&self) -> (usize, usize) {
493 (1, 1)
494 }
495 }
496
497 fn one_var_problem() -> (Problem, VarKey) {
498 let mut problem = Problem::new(JacobianMode::Sparse);
499 let k = problem.add_variable(ManifoldType::RN, dvector![5.0]);
500 problem.add_residual_block(&[k], Box::new(LinearFactor { target: 0.0 }), None);
501 (problem, k)
502 }
503
504 #[allow(clippy::type_complexity)]
505 fn make_index_map(
506 problem: &Problem,
507 ) -> (
508 SlotMap<VarKey, Box<dyn crate::core::variable::ManifoldVariable>>,
509 SecondaryMap<VarKey, usize>,
510 usize,
511 ) {
512 let variables = problem.variables.clone();
513 let mut index_map: SecondaryMap<VarKey, usize> = SecondaryMap::new();
514 let mut offset = 0;
515 for (k, v) in &variables {
516 index_map.insert(k, offset);
517 offset += v.dof();
518 }
519 (variables, index_map, offset)
520 }
521
522 #[test]
534 fn test_compute_block_into_rejects_missing_variable() -> TestResult {
535 let (problem, _k) = one_var_problem();
536 let block = problem
537 .residual_blocks()
538 .values()
539 .next()
540 .ok_or("no blocks")?;
541
542 let variables: SlotMap<VarKey, Box<dyn ManifoldVariable>> = SlotMap::with_key();
544 let mut residual_slice = vec![0.0f64; 1];
545 let mut jac_buf = vec![0.0f64; 1];
546
547 let Err(err) =
548 compute_block_into(block, &variables, &mut residual_slice, Some(&mut jac_buf))
549 else {
550 panic!("missing variable key must be an error, not a silent skip");
551 };
552 assert!(
553 matches!(err, LinearizerError::Variable(_)),
554 "expected a Variable error, got {err:?}"
555 );
556 Ok(())
557 }
558
559 #[test]
560 fn test_compute_block_into_residual_value() -> TestResult {
561 let (problem, _k) = one_var_problem();
562 let (variables, _, _) = make_index_map(&problem);
563 let block = problem
564 .residual_blocks()
565 .values()
566 .next()
567 .ok_or("no blocks")?;
568 let mut residual_slice = vec![0.0f64; 1];
569 let mut jac_buf = vec![0.0f64; 1];
570 compute_block_into(block, &variables, &mut residual_slice, Some(&mut jac_buf))?;
571 assert!((residual_slice[0] - 5.0).abs() < 1e-12);
572 Ok(())
573 }
574
575 #[test]
576 fn test_compute_block_into_jacobian_shape() -> TestResult {
577 let (problem, _k) = one_var_problem();
578 let (variables, _, _) = make_index_map(&problem);
579 let block = problem
580 .residual_blocks()
581 .values()
582 .next()
583 .ok_or("no blocks")?;
584 let mut residual_slice = vec![0.0f64; 1];
585 let mut jac_buf = vec![0.0f64; 1];
586 let (result, _cost) =
587 compute_block_into(block, &variables, &mut residual_slice, Some(&mut jac_buf))?;
588 assert_eq!(result.residual_dim, 1);
589 assert_eq!(jac_buf.len(), 1); Ok(())
591 }
592
593 #[test]
594 fn test_compute_block_into_variable_local_idx() -> TestResult {
595 let (problem, _k) = one_var_problem();
596 let (variables, _, _) = make_index_map(&problem);
597 let block = problem
598 .residual_blocks()
599 .values()
600 .next()
601 .ok_or("no blocks")?;
602 let mut residual_slice = vec![0.0f64; 1];
603 let mut jac_buf = vec![0.0f64; 1];
604 let (result, _cost) =
605 compute_block_into(block, &variables, &mut residual_slice, Some(&mut jac_buf))?;
606 assert_eq!(result.variable_local_idx_size_list.len(), 1);
607 let (local_idx, size) = result.variable_local_idx_size_list[0];
608 assert_eq!(local_idx, 0);
609 assert_eq!(size, 1);
610 Ok(())
611 }
612
613 #[test]
618 fn test_split_by_row_offsets_mut_basic() {
619 let mut buf = vec![1.0f64, 2.0, 3.0, 4.0, 5.0];
620 let offsets = vec![(0, 2), (3, 2)];
621 let slices = split_by_row_offsets_mut(&mut buf, &offsets);
622 assert_eq!(slices.len(), 2);
623 assert_eq!(slices[0], &[1.0, 2.0]);
624 assert_eq!(slices[1], &[4.0, 5.0]);
625 }
626
627 #[test]
628 fn test_split_by_row_offsets_mut_write() {
629 let mut buf = vec![0.0f64; 4];
630 let offsets = vec![(0, 2), (2, 2)];
631 {
632 let mut slices = split_by_row_offsets_mut(&mut buf, &offsets);
633 slices[0][0] = 1.0;
634 slices[0][1] = 2.0;
635 slices[1][0] = 3.0;
636 slices[1][1] = 4.0;
637 }
638 assert_eq!(buf, vec![1.0, 2.0, 3.0, 4.0]);
639 }
640
641 #[test]
646 fn test_sparse_backend_assemble() -> TestResult {
647 let (problem, _k) = one_var_problem();
648 let (variables, index_map, total_dof) = make_index_map(&problem);
649 let sym = crate::linearizer::cpu::sparse::build_symbolic_structure(
650 &problem, &variables, &index_map, total_dof,
651 )?;
652 let (residual, _) = SparseMode::assemble(
653 &problem,
654 &variables,
655 &index_map,
656 Some(&sym),
657 total_dof,
658 &mut AssemblyWorkspace::build(&problem),
659 )?;
660 assert!((residual[(0, 0)] - 5.0).abs() < 1e-12);
661 Ok(())
662 }
663
664 #[test]
665 fn test_sparse_backend_assemble_no_symbolic_returns_error() -> TestResult {
666 let (problem, _k) = one_var_problem();
667 let (variables, index_map, total_dof) = make_index_map(&problem);
668 let result = SparseMode::assemble(
669 &problem,
670 &variables,
671 &index_map,
672 None,
673 total_dof,
674 &mut AssemblyWorkspace::build(&problem),
675 );
676 assert!(result.is_err());
677 Ok(())
678 }
679
680 #[test]
681 fn test_sparse_backend_compute_column_norms() -> TestResult {
682 let (problem, _k) = one_var_problem();
683 let (variables, index_map, total_dof) = make_index_map(&problem);
684 let sym = crate::linearizer::cpu::sparse::build_symbolic_structure(
685 &problem, &variables, &index_map, total_dof,
686 )?;
687 let (_, jacobian) = SparseMode::assemble(
688 &problem,
689 &variables,
690 &index_map,
691 Some(&sym),
692 total_dof,
693 &mut AssemblyWorkspace::build(&problem),
694 )?;
695 let norms = SparseMode::compute_column_norms(&jacobian);
696 assert_eq!(norms.len(), 1);
697 assert!((norms[0] - 1.0).abs() < 1e-12);
698 Ok(())
699 }
700
701 #[test]
702 fn test_sparse_backend_apply_column_scaling() -> TestResult {
703 let (problem, _k) = one_var_problem();
704 let (variables, index_map, total_dof) = make_index_map(&problem);
705 let sym = crate::linearizer::cpu::sparse::build_symbolic_structure(
706 &problem, &variables, &index_map, total_dof,
707 )?;
708 let (_, jacobian) = SparseMode::assemble(
709 &problem,
710 &variables,
711 &index_map,
712 Some(&sym),
713 total_dof,
714 &mut AssemblyWorkspace::build(&problem),
715 )?;
716 let scaling = vec![0.5_f64];
717 let scaled = SparseMode::apply_column_scaling(&jacobian, &scaling);
718 let val = scaled.as_ref().val_of_col(0)[0];
719 assert!((val - 0.5).abs() < 1e-12);
720 Ok(())
721 }
722
723 #[test]
724 fn test_sparse_backend_apply_inverse_scaling() {
725 let step = Mat::from_fn(1, 1, |_, _| 1.0_f64);
726 let scaling = vec![2.0_f64];
727 let result = SparseMode::apply_inverse_scaling(&step, &scaling);
728 assert!((result[(0, 0)] - 2.0).abs() < 1e-12);
729 }
730
731 #[test]
732 fn test_sparse_backend_hessian_vec_product() -> TestResult {
733 let triplets = vec![faer::sparse::Triplet::new(0usize, 0usize, 4.0_f64)];
734 let h = SparseColMat::try_new_from_triplets(1, 1, &triplets)?;
735 let v = Mat::from_fn(1, 1, |_, _| 2.0_f64);
736 let result = SparseMode::hessian_vec_product(&h, &v);
737 assert!((result[(0, 0)] - 8.0).abs() < 1e-12);
738 Ok(())
739 }
740
741 #[test]
746 fn test_dense_backend_assemble() -> TestResult {
747 let mut problem = Problem::new(JacobianMode::Dense);
748 let k = problem.add_variable(ManifoldType::RN, dvector![5.0]);
749 problem.add_residual_block(&[k], Box::new(LinearFactor { target: 0.0 }), None);
750 let (variables, index_map, total_dof) = make_index_map(&problem);
751 let (residual, _) = DenseMode::assemble(
752 &problem,
753 &variables,
754 &index_map,
755 None,
756 total_dof,
757 &mut AssemblyWorkspace::build(&problem),
758 )?;
759 assert!((residual[(0, 0)] - 5.0).abs() < 1e-12);
760 Ok(())
761 }
762
763 #[test]
764 fn test_dense_backend_compute_column_norms() {
765 let jacobian = Mat::from_fn(1, 1, |_, _| 1.0_f64);
766 let norms = DenseMode::compute_column_norms(&jacobian);
767 assert_eq!(norms.len(), 1);
768 assert!((norms[0] - 1.0).abs() < 1e-12);
769 }
770
771 #[test]
772 fn test_dense_backend_apply_column_scaling() {
773 let jacobian = Mat::from_fn(1, 1, |_, _| 1.0_f64);
774 let scaling = vec![0.5_f64];
775 let scaled = DenseMode::apply_column_scaling(&jacobian, &scaling);
776 assert!((scaled[(0, 0)] - 0.5).abs() < 1e-12);
777 }
778
779 #[test]
780 fn test_dense_backend_apply_inverse_scaling() {
781 let step = Mat::from_fn(1, 1, |_, _| 1.0_f64);
782 let scaling = vec![2.0_f64];
783 let result = DenseMode::apply_inverse_scaling(&step, &scaling);
784 assert!((result[(0, 0)] - 2.0).abs() < 1e-12);
785 }
786
787 #[test]
788 fn test_dense_backend_hessian_vec_product() {
789 let h = Mat::from_fn(1, 1, |_, _| 4.0_f64);
790 let v = Mat::from_fn(1, 1, |_, _| 2.0_f64);
791 let result = DenseMode::hessian_vec_product(&h, &v);
792 assert!((result[(0, 0)] - 8.0).abs() < 1e-12);
793 }
794}