1use std::{
8 collections::HashMap,
9 sync::{Arc, Mutex},
10};
11
12use faer::{Col, Mat};
13use rayon::prelude::*;
14
15use crate::error::ErrorLogging;
16use crate::linearizer::{LinearizerError, LinearizerResult};
17
18use super::super::linearize_block;
19use crate::core::problem::{Problem, VariableEnum};
20
21pub fn assemble_dense(
33 problem: &Problem,
34 variables: &HashMap<String, VariableEnum>,
35 variable_index_map: &HashMap<String, usize>,
36 total_dof: usize,
37) -> LinearizerResult<(Mat<f64>, Mat<f64>)> {
38 let total_residual = Arc::new(Mutex::new(Col::<f64>::zeros(
39 problem.total_residual_dimension,
40 )));
41 let jacobian_dense = Arc::new(Mutex::new(Mat::<f64>::zeros(
42 problem.total_residual_dimension,
43 total_dof,
44 )));
45
46 problem.residual_blocks().par_iter().try_for_each(
48 |(_, residual_block)| -> Result<(), LinearizerError> {
49 let block = linearize_block(residual_block, variables, &total_residual)?;
50
51 let mut jac_dense = jacobian_dense.lock().map_err(|e| {
53 LinearizerError::ParallelComputation(
54 "Failed to acquire lock on dense Jacobian".to_string(),
55 )
56 .log_with_source(e)
57 })?;
58
59 for (i, var_key) in residual_block.variable_key_list.iter().enumerate() {
60 let col_offset = *variable_index_map.get(var_key).ok_or_else(|| {
61 LinearizerError::Variable(format!(
62 "Missing key {} in variable-to-column-index mapping",
63 var_key
64 ))
65 .log()
66 })?;
67 let (variable_local_idx, var_size) = block.variable_local_idx_size_list[i];
68 let variable_jac = block
69 .jacobian
70 .view((0, variable_local_idx), (block.residual_dim, var_size));
71
72 for row in 0..block.residual_dim {
73 for col in 0..var_size {
74 jac_dense[(block.residual_row_start_idx + row, col_offset + col)] =
75 variable_jac[(row, col)];
76 }
77 }
78 }
79
80 Ok(())
81 },
82 )?;
83
84 let total_residual = Arc::try_unwrap(total_residual)
85 .map_err(|_| {
86 LinearizerError::ParallelComputation(
87 "Failed to unwrap Arc for total residual".to_string(),
88 )
89 .log()
90 })?
91 .into_inner()
92 .map_err(|e| {
93 LinearizerError::ParallelComputation(
94 "Failed to extract mutex inner value for total residual".to_string(),
95 )
96 .log_with_source(e)
97 })?;
98
99 let jacobian_dense = Arc::try_unwrap(jacobian_dense)
100 .map_err(|_| {
101 LinearizerError::ParallelComputation(
102 "Failed to unwrap Arc for dense Jacobian".to_string(),
103 )
104 .log()
105 })?
106 .into_inner()
107 .map_err(|e| {
108 LinearizerError::ParallelComputation(
109 "Failed to extract mutex inner value for dense Jacobian".to_string(),
110 )
111 .log_with_source(e)
112 })?;
113
114 let residual_faer = total_residual.as_ref().as_mat().to_owned();
115 Ok((residual_faer, jacobian_dense))
116}
117
118#[cfg(test)]
119mod tests {
120 use super::*;
121 use crate::{core::problem::Problem, factors, linalg::JacobianMode, optimizer};
122 use apex_manifolds::ManifoldType;
123 use nalgebra::{DMatrix, DVector, dvector};
124 use std::collections::HashMap;
125
126 type TestResult = Result<(), Box<dyn std::error::Error>>;
127
128 struct LinearFactor {
129 target: f64,
130 }
131
132 impl factors::Factor for LinearFactor {
133 fn linearize(
134 &self,
135 params: &[DVector<f64>],
136 compute_jacobian: bool,
137 ) -> (DVector<f64>, Option<DMatrix<f64>>) {
138 let residual = dvector![params[0][0] - self.target];
139 let jacobian = if compute_jacobian {
140 Some(DMatrix::from_element(1, 1, 1.0))
141 } else {
142 None
143 };
144 (residual, jacobian)
145 }
146
147 fn get_dimension(&self) -> usize {
148 1
149 }
150 }
151
152 fn one_var_dense_problem() -> (Problem, HashMap<String, (ManifoldType, DVector<f64>)>) {
153 let mut problem = Problem::new(JacobianMode::Dense);
154 problem.add_residual_block(&["x"], Box::new(LinearFactor { target: 0.0 }), None);
155 let mut init = HashMap::new();
156 init.insert("x".to_string(), (ManifoldType::RN, dvector![5.0]));
157 (problem, init)
158 }
159
160 #[test]
161 fn test_assemble_dense_basic() -> TestResult {
162 let (problem, init) = one_var_dense_problem();
163 let state = optimizer::initialize_optimization_state(&problem, &init)?;
164 let (residual, jacobian) = assemble_dense(
165 &problem,
166 &state.variables,
167 &state.variable_index_map,
168 state.total_dof,
169 )?;
170 assert!((residual[(0, 0)] - 5.0).abs() < 1e-12);
171 assert!((jacobian[(0, 0)] - 1.0).abs() < 1e-12);
172 Ok(())
173 }
174
175 #[test]
176 fn test_assemble_dense_jacobian_dimensions() -> TestResult {
177 let (problem, init) = one_var_dense_problem();
178 let state = optimizer::initialize_optimization_state(&problem, &init)?;
179 let (residual, jacobian) = assemble_dense(
180 &problem,
181 &state.variables,
182 &state.variable_index_map,
183 state.total_dof,
184 )?;
185 assert_eq!(residual.nrows(), problem.total_residual_dimension);
186 assert_eq!(jacobian.nrows(), problem.total_residual_dimension);
187 assert_eq!(jacobian.ncols(), state.total_dof);
188 Ok(())
189 }
190
191 #[test]
192 fn test_assemble_dense_zero_residual() -> TestResult {
193 let mut problem = Problem::new(JacobianMode::Dense);
194 problem.add_residual_block(&["x"], Box::new(LinearFactor { target: 3.0 }), None);
195 let mut init = HashMap::new();
196 init.insert("x".to_string(), (ManifoldType::RN, dvector![3.0]));
197 let state = optimizer::initialize_optimization_state(&problem, &init)?;
198 let (residual, _) = assemble_dense(
199 &problem,
200 &state.variables,
201 &state.variable_index_map,
202 state.total_dof,
203 )?;
204 assert!(residual[(0, 0)].abs() < 1e-12);
205 Ok(())
206 }
207
208 #[test]
209 fn test_assemble_dense_two_variables() -> TestResult {
210 let mut problem = Problem::new(JacobianMode::Dense);
211 problem.add_residual_block(&["x"], Box::new(LinearFactor { target: 0.0 }), None);
212 problem.add_residual_block(&["y"], Box::new(LinearFactor { target: 0.0 }), None);
213 let mut init = HashMap::new();
214 init.insert("x".to_string(), (ManifoldType::RN, dvector![2.0]));
215 init.insert("y".to_string(), (ManifoldType::RN, dvector![7.0]));
216 let state = optimizer::initialize_optimization_state(&problem, &init)?;
217 let (residual, jacobian) = assemble_dense(
218 &problem,
219 &state.variables,
220 &state.variable_index_map,
221 state.total_dof,
222 )?;
223 assert_eq!(jacobian.nrows(), 2);
224 assert_eq!(jacobian.ncols(), 2);
225 let rsum = residual[(0, 0)].abs() + residual[(1, 0)].abs();
226 assert!((rsum - 9.0).abs() < 1e-12);
227 Ok(())
228 }
229
230 #[test]
231 fn test_assemble_dense_residual_faer_shape() -> TestResult {
232 let (problem, init) = one_var_dense_problem();
233 let state = optimizer::initialize_optimization_state(&problem, &init)?;
234 let (residual, _) = assemble_dense(
235 &problem,
236 &state.variables,
237 &state.variable_index_map,
238 state.total_dof,
239 )?;
240 assert_eq!(residual.nrows(), 1);
241 assert_eq!(residual.ncols(), 1);
242 Ok(())
243 }
244
245 struct BinaryLinearFactor {
251 target_x: f64,
252 target_y: f64,
253 }
254
255 impl factors::Factor for BinaryLinearFactor {
256 fn linearize(
257 &self,
258 params: &[DVector<f64>],
259 compute_jacobian: bool,
260 ) -> (DVector<f64>, Option<DMatrix<f64>>) {
261 let residual =
262 nalgebra::dvector![params[0][0] - self.target_x, params[1][0] - self.target_y];
263 let jacobian = if compute_jacobian {
264 let mut j = DMatrix::zeros(2, 2);
266 j[(0, 0)] = 1.0;
267 j[(1, 1)] = 1.0;
268 Some(j)
269 } else {
270 None
271 };
272 (residual, jacobian)
273 }
274
275 fn get_dimension(&self) -> usize {
276 2
277 }
278 }
279
280 #[test]
282 fn test_assemble_dense_binary_factor() -> TestResult {
283 let mut problem = Problem::new(JacobianMode::Dense);
284 problem.add_residual_block(
285 &["x", "y"],
286 Box::new(BinaryLinearFactor {
287 target_x: 0.0,
288 target_y: 0.0,
289 }),
290 None,
291 );
292 let mut init = HashMap::new();
293 init.insert("x".to_string(), (ManifoldType::RN, nalgebra::dvector![3.0]));
294 init.insert("y".to_string(), (ManifoldType::RN, nalgebra::dvector![5.0]));
295 let state = optimizer::initialize_optimization_state(&problem, &init)?;
296
297 let (residual, jacobian) = assemble_dense(
298 &problem,
299 &state.variables,
300 &state.variable_index_map,
301 state.total_dof,
302 )?;
303
304 assert_eq!(residual.nrows(), 2);
306 assert_eq!(jacobian.nrows(), 2);
307 assert_eq!(jacobian.ncols(), 2);
308
309 let r_sum = residual[(0, 0)].abs() + residual[(1, 0)].abs();
311 assert!((r_sum - 8.0).abs() < 1e-10, "residual sum = {r_sum}");
312
313 let jac_sum: f64 = (0..2)
316 .map(|c| (0..2).map(|r| jacobian[(r, c)]).sum::<f64>())
317 .sum();
318 assert!(
319 (jac_sum - 2.0).abs() < 1e-10,
320 "sum of jacobian entries = {jac_sum}"
321 );
322 Ok(())
323 }
324
325 #[test]
327 fn test_assemble_dense_missing_variable_key_returns_error() -> TestResult {
328 let (problem, init) = one_var_dense_problem();
329 let state = optimizer::initialize_optimization_state(&problem, &init)?;
330
331 let empty_map: HashMap<String, usize> = HashMap::new();
333 let result = assemble_dense(&problem, &state.variables, &empty_map, state.total_dof);
334 assert!(
335 result.is_err(),
336 "missing variable key should produce an Err"
337 );
338 Ok(())
339 }
340
341 #[test]
343 fn test_assemble_dense_multi_block_residual_values() -> TestResult {
344 let mut problem = Problem::new(JacobianMode::Dense);
345 problem.add_residual_block(&["x"], Box::new(LinearFactor { target: 1.0 }), None);
346 problem.add_residual_block(&["x"], Box::new(LinearFactor { target: 4.0 }), None);
347 let mut init = HashMap::new();
348 init.insert("x".to_string(), (ManifoldType::RN, dvector![3.0]));
349 let state = optimizer::initialize_optimization_state(&problem, &init)?;
350
351 let (residual, jacobian) = assemble_dense(
352 &problem,
353 &state.variables,
354 &state.variable_index_map,
355 state.total_dof,
356 )?;
357
358 assert_eq!(residual.nrows(), 2);
360
361 let vals: std::collections::HashSet<i64> =
363 (0..2).map(|i| (residual[(i, 0)] * 1e6) as i64).collect();
364 assert!(vals.contains(&2_000_000), "Missing residual entry 2.0");
365 assert!(vals.contains(&-1_000_000), "Missing residual entry -1.0");
366
367 assert!((jacobian[(0, 0)] - 1.0).abs() < 1e-10);
369 assert!((jacobian[(1, 0)] - 1.0).abs() < 1e-10);
370 Ok(())
371 }
372}