Skip to main content

apex_solver/linearizer/
mod.rs

1//! Jacobian linearization — the bridge between the nonlinear factor graph
2//! and the linear system solved each iteration.
3//!
4//! This is the central module for all linearization concerns:
5//! - `linearize_block()`: Shared factor evaluation (loss correction, residual accumulation)
6//! - [`cpu::sparse`]: Sparse Jacobian assembly using `SparseColMat` and symbolic structure
7//! - [`cpu::dense`]: Dense Jacobian assembly using `Mat<f64>`
8//! - [`AssemblyBackend`]: Trait bridging linearization with the optimizer's solver types
9//!
10//! # Architecture
11//!
12//! ```text
13//! Problem (factor graph)
14//!     │  AssemblyBackend::assemble()
15//!     ▼
16//! (r: Mat<f64>, J: M::Jacobian)   ← M: LinearizationMode
17//!     │
18//!     ▼
19//! LinearSolver<M>   (linalg/)
20//!     │
21//!     ▼
22//! dx: Mat<f64>  → manifold update
23//! ```
24
25pub 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// ============================================================================
47// Linearizer error types
48// ============================================================================
49
50/// Linearizer-specific error types for Jacobian assembly and symbolic structure operations.
51///
52/// These errors occur during the linearization phase of optimization, where
53/// the nonlinear factor graph is converted into a linear system (residual vector
54/// and Jacobian matrix).
55///
56/// # Error Hierarchy
57///
58/// `LinearizerError` is a Layer C (deep/module) error. It propagates up through:
59/// - `CoreError::Linearizer(LinearizerError)` → for core module callers
60/// - `ApexSolverError::Linearizer(LinearizerError)` → for direct API callers
61///
62
63#[derive(Debug, Clone, Error)]
64pub enum LinearizerError {
65    /// Symbolic structure construction or usage failed
66    #[error("Symbolic structure error: {0}")]
67    SymbolicStructure(String),
68
69    /// Parallel computation error (thread/mutex failures during assembly)
70    #[error("Parallel computation error: {0}")]
71    ParallelComputation(String),
72
73    /// Variable key missing in index mapping during Jacobian scatter
74    #[error("Variable error: {0}")]
75    Variable(String),
76
77    /// Factor linearization returned no Jacobian when expected
78    #[error("Factor linearization failed: {0}")]
79    FactorLinearization(String),
80
81    /// Invalid input (e.g., SparseMode requires symbolic structure)
82    #[error("Invalid input: {0}")]
83    InvalidInput(String),
84}
85
86/// Result type for linearizer module operations
87pub type LinearizerResult<T> = Result<T, LinearizerError>;
88
89// ============================================================================
90// Block linearization (shared by sparse and dense paths)
91// ============================================================================
92
93/// Metadata produced by evaluating a single residual block.
94///
95/// The corrected Jacobian itself lives in a caller-provided buffer (see
96/// [`compute_block_into`]) — this struct only describes how to scatter it
97/// into the global Jacobian.
98pub(crate) struct BlockLinearization {
99    /// Maps each variable to (local_col_offset, dof_size) within the block Jacobian.
100    /// SmallVec inline storage covers all current factor types (≤ 4 variables per factor).
101    pub variable_local_idx_size_list: SmallVec<[(usize, usize); 8]>,
102    /// Starting row index in the global residual/Jacobian
103    pub residual_row_start_idx: usize,
104    /// Residual dimension for this block
105    pub residual_dim: usize,
106}
107
108/// Split a mutable buffer into non-overlapping slices at the given (start, len) offsets.
109///
110/// `sorted_offsets_lens` must be sorted ascending by `start` and non-overlapping.
111/// Uses chained `split_at_mut` calls — no unsafe code needed.
112pub(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
130/// Static per-solve assembly data, built once and reused by every iteration.
131///
132/// The set of residual blocks and their layout is fixed for the lifetime of a
133/// solve, so the block ordering, slice offsets and scratch buffers are computed
134/// a single time instead of being rebuilt (collected, sorted, allocated) on
135/// each linearization.
136pub struct AssemblyWorkspace {
137    /// Residual block keys ordered by `residual_row_start_idx`.
138    pub(crate) block_order: Vec<FactorKey>,
139    /// `(row_start, len)` per ordered block — drives residual slice splitting.
140    pub(crate) offsets_lens: Vec<(usize, usize)>,
141    /// Flat per-block Jacobian scratch arena, sized `Σ rows·cols`.
142    pub(crate) jac_arena: Vec<f64>,
143    /// `(start, len)` per ordered block into `jac_arena` — contiguous.
144    pub(crate) jac_offsets: Vec<(usize, usize)>,
145    /// Reusable global residual buffer.
146    pub(crate) residual_buf: Vec<f64>,
147    /// Reusable CSC value array the Jacobian blocks scatter into.
148    pub(crate) jacobian_values: Vec<f64>,
149}
150
151impl AssemblyWorkspace {
152    /// Build the workspace for `problem`. Call once per solve.
153    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            // Sized on first use: nnz comes from the symbolic structure, which
175            // the workspace does not carry.
176            jacobian_values: Vec::new(),
177        }
178    }
179
180    /// Empty workspace for degenerate problems and tests.
181    #[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
194/// Evaluate a single residual block: call `factor.linearize()`, apply loss correction,
195/// write the corrected residual into the provided slice, return the Jacobian buffer.
196///
197/// This is the shared core used by both sparse and dense assembly. It:
198/// 1. Gathers `&[f64]` parameter slices for each variable (zero copy from manifold storage)
199/// 2. Calls `factor.linearize()` — writes into `residual_slice` and a local Jacobian buffer
200/// 3. Applies the robust loss function correction (if any)
201/// 4. Returns the corrected Jacobian buffer and metadata for the caller to scatter
202pub(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        // Skipping an unresolved key would silently shorten `param_slices`, and
214        // the factor then indexes past its end deep inside `linearize`. The
215        // scatter phase already reports this condition as an error; report it
216        // here too rather than turning it into a panic downstream.
217        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(&param_slices, residual_slice, Some(jac_mut));
238        }
239        None => residual_block
240            .factor
241            .linearize(&param_slices, residual_slice, None),
242    }
243
244    // Whiten by the block's noise model (r̃ = S·r, J̃ = S·J) BEFORE the robust
245    // corrector, so Triggs math, cost accounting and covariance all see the
246    // weighted problem. Null is a zero-cost no-op.
247    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    // Apply robust-loss correction in-place: no heap allocation.
253    //
254    // The corrected residual/Jacobian drive the linear system; the returned
255    // cost is the true robust cost `0.5·ρ(‖S·r‖²)`, not the corrected norm.
256    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        // Jacobian correction must read the original (un-corrected) residual.
260        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
279// ============================================================================
280// AssemblyBackend trait (bridges linearizer output with optimizer solver types)
281// ============================================================================
282
283/// Type-level backend for assembling (residuals, Jacobian) and performing
284/// matrix operations. Implemented by [`SparseMode`] and [`DenseMode`].
285///
286/// All methods are static — this trait is used as a compile-time strategy
287/// selector, not as an object interface. Extends [`LinearizationMode`] with
288/// the five operations an optimizer needs each iteration: building `(r, J)`,
289/// scaling `J`, unscaling `dx`, and `H·v`.
290///
291/// All three optimizers (LM, GN, DogLeg) are generic over `M: AssemblyBackend`,
292/// giving zero-cost static dispatch through the entire pipeline.
293pub trait AssemblyBackend: LinearizationMode {
294    /// Assemble residuals and Jacobian from the problem.
295    ///
296    /// Reuses the per-solve [`AssemblyWorkspace`] scratch buffers; the workspace
297    /// must have been built from the same `problem`.
298    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    /// Compute column norms of the Jacobian (for Jacobi scaling).
308    fn compute_column_norms(jacobian: &Self::Jacobian) -> Vec<f64>;
309
310    /// Apply diagonal column scaling to the Jacobian.
311    /// Returns a new Jacobian with columns scaled by `1 / (1 + norm)`.
312    fn apply_column_scaling(jacobian: &Self::Jacobian, scaling: &[f64]) -> Self::Jacobian;
313
314    /// Apply inverse scaling to a step vector: step_i *= scaling_i
315    fn apply_inverse_scaling(step: &Mat<f64>, scaling: &[f64]) -> Mat<f64>;
316
317    /// Hessian-vector product: H * v (needed by DogLeg for Cauchy point)
318    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        // Scale the value array column-by-column in parallel. The sparsity
360        // pattern is unchanged, so no triplet build, sort or sparse product is
361        // needed — the previous diagonal-matrix product did exactly that.
362        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            // Fall back to the unscaled Jacobian — mirrors the old behavior
383            // when the diagonal scaling matrix could not be built.
384            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    // -------------------------------------------------------------------------
523    // compute_block_into
524    // -------------------------------------------------------------------------
525
526    /// An unresolved variable key must surface as a typed error.
527    ///
528    /// Skipping it silently shortens `param_slices`, and the factor then indexes
529    /// past its end inside `linearize` — a panic deep in parallel assembly
530    /// instead of an error at the boundary. `Problem` rejects unknown keys at
531    /// registration today, so this is defence for the paths that will remove
532    /// variables (sliding-window marginalization).
533    #[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        // An empty variable map cannot resolve the block's key.
543        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); // 1×1
590        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    // -------------------------------------------------------------------------
614    // split_by_row_offsets_mut
615    // -------------------------------------------------------------------------
616
617    #[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    // -------------------------------------------------------------------------
642    // SparseMode AssemblyBackend
643    // -------------------------------------------------------------------------
644
645    #[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    // -------------------------------------------------------------------------
742    // DenseMode AssemblyBackend
743    // -------------------------------------------------------------------------
744
745    #[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}