1use std::{
8 collections::HashMap,
9 sync::{Arc, Mutex},
10};
11
12use faer::{
13 Col, Mat,
14 sparse::{Argsort, Pair, SparseColMat, SymbolicSparseColMat},
15};
16use rayon::prelude::*;
17
18use crate::error::ErrorLogging;
19use crate::linearizer::{LinearizerError, LinearizerResult};
20
21use super::super::linearize_block;
22use crate::core::problem::{Problem, VariableEnum};
23
24pub struct SymbolicStructure {
35 pub pattern: SymbolicSparseColMat<usize>,
36 pub order: Argsort<usize>,
37}
38
39pub fn build_symbolic_structure(
55 problem: &Problem,
56 variables: &HashMap<String, VariableEnum>,
57 variable_index_map: &HashMap<String, usize>,
58 total_dof: usize,
59) -> LinearizerResult<SymbolicStructure> {
60 let mut indices = Vec::<Pair<usize, usize>>::new();
61
62 problem
63 .residual_blocks()
64 .iter()
65 .for_each(|(_, residual_block)| {
66 let mut variable_local_idx_size_list = Vec::<(usize, usize)>::new();
67 let mut count_variable_local_idx: usize = 0;
68
69 for var_key in &residual_block.variable_key_list {
70 if let Some(variable) = variables.get(var_key) {
71 variable_local_idx_size_list
72 .push((count_variable_local_idx, variable.get_size()));
73 count_variable_local_idx += variable.get_size();
74 }
75 }
76
77 for (i, var_key) in residual_block.variable_key_list.iter().enumerate() {
78 if let Some(variable_global_idx) = variable_index_map.get(var_key) {
79 let (_, var_size) = variable_local_idx_size_list[i];
80
81 for row_idx in 0..residual_block.factor.get_dimension() {
82 for col_idx in 0..var_size {
83 let global_row_idx = residual_block.residual_row_start_idx + row_idx;
84 let global_col_idx = variable_global_idx + col_idx;
85 indices.push(Pair::new(global_row_idx, global_col_idx));
86 }
87 }
88 }
89 }
90 });
91
92 let (pattern, order) = SymbolicSparseColMat::try_new_from_indices(
93 problem.total_residual_dimension,
94 total_dof,
95 &indices,
96 )
97 .map_err(|e| {
98 LinearizerError::SymbolicStructure(
99 "Failed to build symbolic sparse matrix structure".to_string(),
100 )
101 .log_with_source(e)
102 })?;
103
104 Ok(SymbolicStructure { pattern, order })
105}
106
107pub fn assemble_sparse(
120 problem: &Problem,
121 variables: &HashMap<String, VariableEnum>,
122 variable_index_map: &HashMap<String, usize>,
123 symbolic_structure: &SymbolicStructure,
124) -> LinearizerResult<(Mat<f64>, SparseColMat<usize, f64>)> {
125 let total_residual = Arc::new(Mutex::new(Col::<f64>::zeros(
126 problem.total_residual_dimension,
127 )));
128
129 let total_nnz = symbolic_structure.pattern.compute_nnz();
130
131 let jacobian_blocks: Result<Vec<(usize, Vec<f64>)>, LinearizerError> = problem
133 .residual_blocks()
134 .par_iter()
135 .map(|(_, residual_block)| {
136 let values = scatter_sparse_block(
137 residual_block,
138 variables,
139 variable_index_map,
140 &total_residual,
141 )?;
142 let size = values.len();
143 Ok((size, values))
144 })
145 .collect();
146
147 let jacobian_blocks = jacobian_blocks?;
148
149 let mut jacobian_values = Vec::with_capacity(total_nnz);
151 for (_size, mut block_values) in jacobian_blocks {
152 jacobian_values.append(&mut block_values);
153 }
154
155 let total_residual = Arc::try_unwrap(total_residual)
156 .map_err(|_| {
157 LinearizerError::ParallelComputation(
158 "Failed to unwrap Arc for total residual".to_string(),
159 )
160 .log()
161 })?
162 .into_inner()
163 .map_err(|e| {
164 LinearizerError::ParallelComputation(
165 "Failed to extract mutex inner value for total residual".to_string(),
166 )
167 .log_with_source(e)
168 })?;
169
170 let residual_faer = total_residual.as_ref().as_mat().to_owned();
171 let jacobian_sparse = SparseColMat::new_from_argsort(
172 symbolic_structure.pattern.clone(),
173 &symbolic_structure.order,
174 jacobian_values.as_slice(),
175 )
176 .map_err(|e| {
177 LinearizerError::SymbolicStructure(
178 "Failed to create sparse Jacobian from argsort".to_string(),
179 )
180 .log_with_source(e)
181 })?;
182
183 Ok((residual_faer, jacobian_sparse))
184}
185
186fn scatter_sparse_block(
191 residual_block: &crate::core::residual_block::ResidualBlock,
192 variables: &HashMap<String, VariableEnum>,
193 variable_index_map: &HashMap<String, usize>,
194 total_residual: &Arc<Mutex<Col<f64>>>,
195) -> LinearizerResult<Vec<f64>> {
196 let block = linearize_block(residual_block, variables, total_residual)?;
197
198 let mut local_jacobian_values = Vec::new();
199 for (i, var_key) in residual_block.variable_key_list.iter().enumerate() {
200 if variable_index_map.contains_key(var_key) {
201 let (variable_local_idx, var_size) = block.variable_local_idx_size_list[i];
202 let variable_jac = block
203 .jacobian
204 .view((0, variable_local_idx), (block.residual_dim, var_size));
205
206 for row_idx in 0..block.residual_dim {
207 for col_idx in 0..var_size {
208 local_jacobian_values.push(variable_jac[(row_idx, col_idx)]);
209 }
210 }
211 } else {
212 return Err(LinearizerError::Variable(format!(
213 "Missing key {} in variable-to-column-index mapping",
214 var_key
215 ))
216 .log());
217 }
218 }
219
220 Ok(local_jacobian_values)
221}
222
223#[cfg(test)]
224mod tests {
225 use super::*;
226 use crate::{core::problem::Problem, factors, linalg::JacobianMode, optimizer};
227 use apex_manifolds::ManifoldType;
228 use nalgebra::{DMatrix, DVector, dvector};
229 use std::collections::HashMap;
230
231 type TestResult = Result<(), Box<dyn std::error::Error>>;
232
233 struct LinearFactor {
234 target: f64,
235 }
236
237 impl factors::Factor for LinearFactor {
238 fn linearize(
239 &self,
240 params: &[DVector<f64>],
241 compute_jacobian: bool,
242 ) -> (DVector<f64>, Option<DMatrix<f64>>) {
243 let residual = dvector![params[0][0] - self.target];
244 let jacobian = if compute_jacobian {
245 Some(DMatrix::from_element(1, 1, 1.0))
246 } else {
247 None
248 };
249 (residual, jacobian)
250 }
251
252 fn get_dimension(&self) -> usize {
253 1
254 }
255 }
256
257 fn one_var_problem() -> (Problem, HashMap<String, (ManifoldType, DVector<f64>)>) {
258 let mut problem = Problem::new(JacobianMode::Sparse);
259 problem.add_residual_block(&["x"], Box::new(LinearFactor { target: 0.0 }), None);
260 let mut init = HashMap::new();
261 init.insert("x".to_string(), (ManifoldType::RN, dvector![5.0]));
262 (problem, init)
263 }
264
265 #[test]
270 fn test_build_symbolic_structure_nnz() -> TestResult {
271 let (problem, init) = one_var_problem();
272 let state = optimizer::initialize_optimization_state(&problem, &init)?;
273 let sym = build_symbolic_structure(
274 &problem,
275 &state.variables,
276 &state.variable_index_map,
277 state.total_dof,
278 )?;
279 assert_eq!(sym.pattern.compute_nnz(), 1);
280 Ok(())
281 }
282
283 #[test]
284 fn test_build_symbolic_structure_dimensions() -> TestResult {
285 let (problem, init) = one_var_problem();
286 let state = optimizer::initialize_optimization_state(&problem, &init)?;
287 let sym = build_symbolic_structure(
288 &problem,
289 &state.variables,
290 &state.variable_index_map,
291 state.total_dof,
292 )?;
293 assert_eq!(sym.pattern.nrows(), 1);
294 assert_eq!(sym.pattern.ncols(), 1);
295 Ok(())
296 }
297
298 #[test]
299 fn test_build_symbolic_structure_two_factors() -> TestResult {
300 let mut problem = Problem::new(JacobianMode::Sparse);
301 problem.add_residual_block(&["x"], Box::new(LinearFactor { target: 0.0 }), None);
302 problem.add_residual_block(&["x"], Box::new(LinearFactor { target: 1.0 }), None);
303 let mut init = HashMap::new();
304 init.insert("x".to_string(), (ManifoldType::RN, dvector![5.0]));
305 let state = optimizer::initialize_optimization_state(&problem, &init)?;
306 let sym = build_symbolic_structure(
307 &problem,
308 &state.variables,
309 &state.variable_index_map,
310 state.total_dof,
311 )?;
312 assert_eq!(sym.pattern.compute_nnz(), 2);
313 Ok(())
314 }
315
316 #[test]
321 fn test_assemble_sparse_basic() -> TestResult {
322 let (problem, init) = one_var_problem();
323 let state = optimizer::initialize_optimization_state(&problem, &init)?;
324 let sym = state
325 .symbolic_structure
326 .ok_or("symbolic_structure is None")?;
327 let (residual, _) =
328 assemble_sparse(&problem, &state.variables, &state.variable_index_map, &sym)?;
329 assert!((residual[(0, 0)] - 5.0).abs() < 1e-12);
330 Ok(())
331 }
332
333 #[test]
334 fn test_assemble_sparse_jacobian_value() -> TestResult {
335 let (problem, init) = one_var_problem();
336 let state = optimizer::initialize_optimization_state(&problem, &init)?;
337 let sym = state
338 .symbolic_structure
339 .ok_or("symbolic_structure is None")?;
340 let (_, jacobian) =
341 assemble_sparse(&problem, &state.variables, &state.variable_index_map, &sym)?;
342 let val = jacobian.as_ref().val_of_col(0)[0];
343 assert!((val - 1.0).abs() < 1e-12);
344 Ok(())
345 }
346
347 #[test]
348 fn test_assemble_sparse_zero_residual() -> TestResult {
349 let mut problem = Problem::new(JacobianMode::Sparse);
350 problem.add_residual_block(&["x"], Box::new(LinearFactor { target: 3.0 }), None);
351 let mut init = HashMap::new();
352 init.insert("x".to_string(), (ManifoldType::RN, dvector![3.0]));
353 let state = optimizer::initialize_optimization_state(&problem, &init)?;
354 let sym = state
355 .symbolic_structure
356 .ok_or("symbolic_structure is None")?;
357 let (residual, _) =
358 assemble_sparse(&problem, &state.variables, &state.variable_index_map, &sym)?;
359 assert!(residual[(0, 0)].abs() < 1e-12);
360 Ok(())
361 }
362
363 #[test]
364 fn test_assemble_sparse_dimensions() -> TestResult {
365 let (problem, init) = one_var_problem();
366 let state = optimizer::initialize_optimization_state(&problem, &init)?;
367 let sym = state
368 .symbolic_structure
369 .ok_or("symbolic_structure is None")?;
370 let (residual, jacobian) =
371 assemble_sparse(&problem, &state.variables, &state.variable_index_map, &sym)?;
372 assert_eq!(residual.nrows(), 1);
373 assert_eq!(residual.ncols(), 1);
374 assert_eq!(jacobian.nrows(), 1);
375 assert_eq!(jacobian.ncols(), 1);
376 Ok(())
377 }
378
379 #[test]
380 fn test_assemble_sparse_two_variables() -> TestResult {
381 let mut problem = Problem::new(JacobianMode::Sparse);
382 problem.add_residual_block(&["x"], Box::new(LinearFactor { target: 0.0 }), None);
383 problem.add_residual_block(&["y"], Box::new(LinearFactor { target: 0.0 }), None);
384 let mut init = HashMap::new();
385 init.insert("x".to_string(), (ManifoldType::RN, dvector![2.0]));
386 init.insert("y".to_string(), (ManifoldType::RN, dvector![7.0]));
387 let state = optimizer::initialize_optimization_state(&problem, &init)?;
388 let sym = state
389 .symbolic_structure
390 .ok_or("symbolic_structure is None")?;
391 let (residual, _) =
392 assemble_sparse(&problem, &state.variables, &state.variable_index_map, &sym)?;
393 assert_eq!(residual.nrows(), 2);
394 let rsum = residual[(0, 0)].abs() + residual[(1, 0)].abs();
395 assert!((rsum - 9.0).abs() < 1e-12);
396 Ok(())
397 }
398
399 #[test]
405 fn test_assemble_sparse_missing_variable_key_returns_error() -> TestResult {
406 let (problem, init) = one_var_problem();
407 let state = optimizer::initialize_optimization_state(&problem, &init)?;
408 let sym = state
409 .symbolic_structure
410 .ok_or("symbolic_structure is None")?;
411
412 let empty_map = HashMap::new();
414 let result = assemble_sparse(&problem, &state.variables, &empty_map, &sym);
415 assert!(
416 result.is_err(),
417 "assemble_sparse with missing variable key should return Err"
418 );
419 Ok(())
420 }
421}