Skip to main content

apex_solver/linearizer/cpu/
sparse.rs

1//! Sparse Jacobian assembly using symbolic sparsity patterns.
2
3use faer::{
4    Mat,
5    sparse::{Argsort, Pair, SparseColMat, SymbolicSparseColMat},
6};
7use rayon::prelude::*;
8use slotmap::{SecondaryMap, SlotMap};
9
10use crate::core::VarKey;
11use crate::error::ErrorLogging;
12use crate::linearizer::{
13    BlockLinearization, LinearizerError, LinearizerResult, compute_block_into,
14    split_by_row_offsets_mut,
15};
16
17use crate::core::problem::Problem;
18use crate::core::variable::ManifoldVariable;
19
20/// Symbolic structure for sparse matrix operations.
21pub struct SymbolicStructure {
22    pub pattern: SymbolicSparseColMat<usize>,
23    pub order: Argsort<usize>,
24}
25
26/// Build the symbolic sparsity structure for the Jacobian matrix.
27pub fn build_symbolic_structure(
28    problem: &Problem,
29    variables: &SlotMap<VarKey, Box<dyn ManifoldVariable>>,
30    variable_index_map: &SecondaryMap<VarKey, usize>,
31    total_dof: usize,
32) -> LinearizerResult<SymbolicStructure> {
33    let mut indices = Vec::<Pair<usize, usize>>::new();
34
35    problem.residual_blocks().iter().for_each(|(_, block)| {
36        let mut var_local_sizes = Vec::<(usize, usize)>::new();
37        let mut local_offset = 0;
38
39        for &var_key in &block.variable_keys {
40            if let Some(variable) = variables.get(var_key) {
41                var_local_sizes.push((local_offset, variable.dof()));
42                local_offset += variable.dof();
43            }
44        }
45
46        for (i, &var_key) in block.variable_keys.iter().enumerate() {
47            if let Some(&global_col) = variable_index_map.get(var_key) {
48                if let Some((_, var_size)) = var_local_sizes.get(i) {
49                    for row in 0..block.factor.residual_dim() {
50                        for col in 0..*var_size {
51                            indices.push(Pair::new(
52                                block.residual_row_start_idx + row,
53                                global_col + col,
54                            ));
55                        }
56                    }
57                }
58            }
59        }
60    });
61
62    let (pattern, order) = SymbolicSparseColMat::try_new_from_indices(
63        problem.total_residual_dimension,
64        total_dof,
65        &indices,
66    )
67    .map_err(|e| {
68        LinearizerError::SymbolicStructure(
69            "Failed to build symbolic sparse matrix structure".to_string(),
70        )
71        .log_with_source(e)
72    })?;
73
74    Ok(SymbolicStructure { pattern, order })
75}
76
77/// Assemble residuals and sparse Jacobian from the current variable values.
78pub fn assemble_sparse(
79    problem: &Problem,
80    variables: &SlotMap<VarKey, Box<dyn ManifoldVariable>>,
81    variable_index_map: &SecondaryMap<VarKey, usize>,
82    symbolic_structure: &SymbolicStructure,
83) -> LinearizerResult<(Mat<f64>, SparseColMat<usize, f64>)> {
84    let total_nnz = symbolic_structure.pattern.compute_nnz();
85
86    // Collect and sort blocks by residual_row_start_idx so pre-split is contiguous
87    let mut blocks: Vec<&crate::core::residual_block::ResidualBlock> =
88        problem.residual_blocks().values().collect();
89    blocks.sort_by_key(|b| b.residual_row_start_idx);
90
91    // Pre-allocate the global residual buffer and split into non-overlapping slices
92    let mut residual_buf = vec![0.0f64; problem.total_residual_dimension];
93    let offsets_lens: Vec<(usize, usize)> = blocks
94        .iter()
95        .map(|b| (b.residual_row_start_idx, b.factor.residual_dim()))
96        .collect();
97    let residual_slices = split_by_row_offsets_mut(&mut residual_buf, &offsets_lens);
98
99    // Pre-allocate one Jacobian buffer per block (sized to factor.jacobian_shape).
100    // These buffers are reused as scratch for the in-place factor write and the
101    // in-place loss correction — no per-block allocation inside the parallel loop.
102    let mut jacobian_buffers: Vec<Vec<f64>> = blocks
103        .iter()
104        .map(|b| {
105            let (r, c) = b.factor.jacobian_shape();
106            vec![0.0f64; r * c]
107        })
108        .collect();
109
110    // Parallel evaluation: each task gets a unique residual slice and a unique
111    // Jacobian buffer (mutable, non-aliasing) — pure zero-copy through factor.linearize.
112    let block_results: Vec<LinearizerResult<BlockLinearization>> = jacobian_buffers
113        .par_iter_mut()
114        .zip(residual_slices.into_par_iter())
115        .zip(blocks.par_iter())
116        .map(|((jac_buf, res_slice), block)| {
117            jac_buf.fill(0.0);
118            compute_block_into(block, variables, res_slice, jac_buf.as_mut_slice())
119        })
120        .collect();
121
122    let block_results = block_results
123        .into_iter()
124        .collect::<LinearizerResult<Vec<_>>>()?;
125
126    // Scatter Jacobian blocks into CSC value array (serial, pre-computed positions).
127    let mut jacobian_values = Vec::with_capacity(total_nnz);
128    for ((bl, block), jac_buf) in block_results
129        .iter()
130        .zip(blocks.iter())
131        .zip(jacobian_buffers.iter())
132    {
133        scatter_sparse_block(bl, block, variable_index_map, jac_buf, &mut jacobian_values)?;
134    }
135
136    // Convert residual buffer to faer Mat
137    let n = problem.total_residual_dimension;
138    let residual_faer = faer::Mat::from_fn(n, 1, |i, _| residual_buf[i]);
139
140    let jacobian_sparse = SparseColMat::new_from_argsort(
141        symbolic_structure.pattern.clone(),
142        &symbolic_structure.order,
143        jacobian_values.as_slice(),
144    )
145    .map_err(|e| {
146        LinearizerError::SymbolicStructure(
147            "Failed to create sparse Jacobian from argsort".to_string(),
148        )
149        .log_with_source(e)
150    })?;
151
152    Ok((residual_faer, jacobian_sparse))
153}
154
155fn scatter_sparse_block(
156    bl: &BlockLinearization,
157    residual_block: &crate::core::residual_block::ResidualBlock,
158    variable_index_map: &SecondaryMap<VarKey, usize>,
159    jacobian_buf: &[f64],
160    jacobian_values: &mut Vec<f64>,
161) -> LinearizerResult<()> {
162    for (i, &var_key) in residual_block.variable_keys.iter().enumerate() {
163        if variable_index_map.contains_key(var_key) {
164            let (local_col, var_size) = bl.variable_local_idx_size_list[i];
165            // symbolic indices are pushed row-major: (row outer, col inner)
166            // jacobian_buf is column-major: buf[(local_col + col) * residual_dim + row]
167            for row in 0..bl.residual_dim {
168                for col in 0..var_size {
169                    jacobian_values.push(jacobian_buf[(local_col + col) * bl.residual_dim + row]);
170                }
171            }
172        } else {
173            return Err(LinearizerError::Variable(format!(
174                "VarKey {:?} missing in variable-to-column-index mapping",
175                var_key
176            ))
177            .log());
178        }
179    }
180    Ok(())
181}
182
183#[cfg(test)]
184mod tests {
185    use super::*;
186    use crate::{core::problem::Problem, factors, linalg::JacobianMode};
187    use apex_manifolds::ManifoldType;
188    use faer::prelude::ReborrowMut;
189    use nalgebra::dvector;
190
191    type TestResult = Result<(), Box<dyn std::error::Error>>;
192
193    struct LinearFactor {
194        target: f64,
195    }
196
197    impl factors::Factor for LinearFactor {
198        fn linearize(
199            &self,
200            params: &[&[f64]],
201            residual: &mut [f64],
202            jacobian: Option<faer::mat::MatMut<'_, f64>>,
203        ) {
204            residual[0] = params[0][0] - self.target;
205            if let Some(mut jac) = jacobian {
206                *jac.rb_mut().get_mut(0, 0) = 1.0;
207            }
208        }
209        fn residual_dim(&self) -> usize {
210            1
211        }
212        fn jacobian_shape(&self) -> (usize, usize) {
213            (1, 1)
214        }
215    }
216
217    fn one_var_problem() -> (Problem, VarKey) {
218        let mut problem = Problem::new(JacobianMode::Sparse);
219        let k = problem.add_variable(ManifoldType::RN, dvector![5.0]);
220        problem.add_residual_block(&[k], Box::new(LinearFactor { target: 0.0 }), None);
221        (problem, k)
222    }
223
224    fn build_index_map(problem: &Problem) -> (SecondaryMap<VarKey, usize>, usize) {
225        let mut map = SecondaryMap::new();
226        let mut offset = 0;
227        for (k, v) in &problem.variables {
228            map.insert(k, offset);
229            offset += v.dof();
230        }
231        (map, offset)
232    }
233
234    #[test]
235    fn test_build_symbolic_structure_nnz() -> TestResult {
236        let (problem, _) = one_var_problem();
237        let (index_map, total_dof) = build_index_map(&problem);
238        let sym = build_symbolic_structure(&problem, &problem.variables, &index_map, total_dof)?;
239        assert_eq!(sym.pattern.compute_nnz(), 1);
240        Ok(())
241    }
242
243    #[test]
244    fn test_build_symbolic_structure_dimensions() -> TestResult {
245        let (problem, _) = one_var_problem();
246        let (index_map, total_dof) = build_index_map(&problem);
247        let sym = build_symbolic_structure(&problem, &problem.variables, &index_map, total_dof)?;
248        assert_eq!(sym.pattern.nrows(), 1);
249        assert_eq!(sym.pattern.ncols(), 1);
250        Ok(())
251    }
252
253    #[test]
254    fn test_build_symbolic_structure_two_factors() -> TestResult {
255        let mut problem = Problem::new(JacobianMode::Sparse);
256        let k = problem.add_variable(ManifoldType::RN, dvector![5.0]);
257        problem.add_residual_block(&[k], Box::new(LinearFactor { target: 0.0 }), None);
258        problem.add_residual_block(&[k], Box::new(LinearFactor { target: 1.0 }), None);
259        let (index_map, total_dof) = build_index_map(&problem);
260        let sym = build_symbolic_structure(&problem, &problem.variables, &index_map, total_dof)?;
261        assert_eq!(sym.pattern.compute_nnz(), 2);
262        Ok(())
263    }
264
265    #[test]
266    fn test_assemble_sparse_basic() -> TestResult {
267        let (problem, _) = one_var_problem();
268        let (index_map, total_dof) = build_index_map(&problem);
269        let sym = build_symbolic_structure(&problem, &problem.variables, &index_map, total_dof)?;
270        let (residual, _) = assemble_sparse(&problem, &problem.variables, &index_map, &sym)?;
271        assert!((residual[(0, 0)] - 5.0).abs() < 1e-12);
272        Ok(())
273    }
274
275    #[test]
276    fn test_assemble_sparse_jacobian_value() -> TestResult {
277        let (problem, _) = one_var_problem();
278        let (index_map, total_dof) = build_index_map(&problem);
279        let sym = build_symbolic_structure(&problem, &problem.variables, &index_map, total_dof)?;
280        let (_, jacobian) = assemble_sparse(&problem, &problem.variables, &index_map, &sym)?;
281        let val = jacobian.as_ref().val_of_col(0)[0];
282        assert!((val - 1.0).abs() < 1e-12);
283        Ok(())
284    }
285
286    #[test]
287    fn test_assemble_sparse_zero_residual() -> TestResult {
288        let mut problem = Problem::new(JacobianMode::Sparse);
289        let k = problem.add_variable(ManifoldType::RN, dvector![3.0]);
290        problem.add_residual_block(&[k], Box::new(LinearFactor { target: 3.0 }), None);
291        let (index_map, total_dof) = build_index_map(&problem);
292        let sym = build_symbolic_structure(&problem, &problem.variables, &index_map, total_dof)?;
293        let (residual, _) = assemble_sparse(&problem, &problem.variables, &index_map, &sym)?;
294        assert!(residual[(0, 0)].abs() < 1e-12);
295        Ok(())
296    }
297
298    #[test]
299    fn test_assemble_sparse_dimensions() -> TestResult {
300        let (problem, _) = one_var_problem();
301        let (index_map, total_dof) = build_index_map(&problem);
302        let sym = build_symbolic_structure(&problem, &problem.variables, &index_map, total_dof)?;
303        let (residual, jacobian) = assemble_sparse(&problem, &problem.variables, &index_map, &sym)?;
304        assert_eq!(residual.nrows(), 1);
305        assert_eq!(residual.ncols(), 1);
306        assert_eq!(jacobian.nrows(), 1);
307        assert_eq!(jacobian.ncols(), 1);
308        Ok(())
309    }
310
311    #[test]
312    fn test_assemble_sparse_two_variables() -> TestResult {
313        let mut problem = Problem::new(JacobianMode::Sparse);
314        let kx = problem.add_variable(ManifoldType::RN, dvector![2.0]);
315        let ky = problem.add_variable(ManifoldType::RN, dvector![7.0]);
316        problem.add_residual_block(&[kx], Box::new(LinearFactor { target: 0.0 }), None);
317        problem.add_residual_block(&[ky], Box::new(LinearFactor { target: 0.0 }), None);
318        let (index_map, total_dof) = build_index_map(&problem);
319        let sym = build_symbolic_structure(&problem, &problem.variables, &index_map, total_dof)?;
320        let (residual, _) = assemble_sparse(&problem, &problem.variables, &index_map, &sym)?;
321        assert_eq!(residual.nrows(), 2);
322        let rsum = residual[(0, 0)].abs() + residual[(1, 0)].abs();
323        assert!((rsum - 9.0).abs() < 1e-12);
324        Ok(())
325    }
326
327    #[test]
328    fn test_assemble_sparse_missing_variable_key_returns_error() -> TestResult {
329        let (problem, _) = one_var_problem();
330        let (_, total_dof) = build_index_map(&problem);
331        let (index_map_full, _) = build_index_map(&problem);
332        let sym =
333            build_symbolic_structure(&problem, &problem.variables, &index_map_full, total_dof)?;
334
335        let empty: SecondaryMap<VarKey, usize> = SecondaryMap::new();
336        let result = assemble_sparse(&problem, &problem.variables, &empty, &sym);
337        assert!(result.is_err(), "expected Err for missing variable key");
338        Ok(())
339    }
340}