use crate::core::VarKey;
use crate::core::variable::ManifoldVariable;
use crate::error::ErrorLogging;
use crate::linalg::regularization::invert_with_retry_dyn;
use crate::linalg::schur::jacobian_ops::{column_dot, diag_jt_j};
use crate::linalg::schur::{
BlockSpan, EliminatedBlocks, PcgParams, SchurOrdering, SchurPartition, SchurPreconditioner,
effective_landmark_keys, jt_j_vec_product, jt_vec, solve_pcg,
};
use crate::linalg::sparse::pattern;
use crate::linalg::{Damping, LinAlgError, LinAlgResult, LinearSolver, SparseMode, StructureAware};
use faer::Mat;
use faer::sparse::SparseColMat;
use nalgebra::DMatrix;
use rayon::prelude::*;
use slotmap::{SecondaryMap, SlotMap};
use tracing::debug;
#[derive(Debug, Clone, Default)]
struct StructureCache {
kept_cols: Vec<usize>,
eliminated_cols: Vec<usize>,
eliminated_rows: Vec<Vec<usize>>,
visibility: Vec<Vec<usize>>,
chunk_rows: usize,
kept_spans: Vec<Vec<ColumnSpan>>,
eliminated_spans: Vec<Vec<ColumnSpan>>,
eliminated_block_of: Vec<u32>,
fingerprint: Option<pattern::PatternFingerprint>,
}
#[derive(Debug, Clone, Copy)]
struct ColumnSpan {
lo: u32,
hi: u32,
local: u32,
}
fn row_chunk_len(nrows: usize) -> usize {
const MIN_CHUNK_ROWS: usize = 4096;
let chunks = rayon::current_num_threads().max(1) * 8;
nrows.div_ceil(chunks).max(MIN_CHUNK_ROWS)
}
struct RowSpans {
kept: Vec<Vec<ColumnSpan>>,
eliminated: Vec<Vec<ColumnSpan>>,
block_of: Vec<u32>,
}
impl RowSpans {
fn build(
jacobian: &SparseColMat<usize, f64>,
partition: &SchurPartition,
chunk_rows: usize,
) -> LinAlgResult<Self> {
let chunks = jacobian.nrows().div_ceil(chunk_rows);
let mut kept = vec![Vec::new(); chunks];
for block in partition.kept_blocks() {
for offset in 0..block.dof {
let col = block.col_start + offset;
if let Some(local) = partition.kept_local(col) {
push_column_spans(jacobian, col, local, chunk_rows, &mut kept)?;
}
}
}
let mut eliminated = vec![Vec::new(); chunks];
let mut block_of = vec![0u32; partition.eliminated_dof()];
for (block_idx, block) in partition.eliminated_blocks().iter().enumerate() {
let owner = u32::try_from(block_idx).map_err(|_| {
LinAlgError::InvalidInput(format!(
"{block_idx} eliminated blocks exceed the implicit Schur operator's u32 range"
))
.log()
})?;
let base = partition.eliminated_offset(block_idx);
for offset in 0..block.dof {
block_of[base + offset] = owner;
push_column_spans(
jacobian,
block.col_start + offset,
base + offset,
chunk_rows,
&mut eliminated,
)?;
}
}
Ok(Self {
kept,
eliminated,
block_of,
})
}
}
fn push_column_spans(
jacobian: &SparseColMat<usize, f64>,
col: usize,
local: usize,
chunk_rows: usize,
spans: &mut [Vec<ColumnSpan>],
) -> LinAlgResult<()> {
let symbolic = jacobian.symbolic();
let start = symbolic.col_range(col).start;
let rows = symbolic.row_idx_of_col_raw(col);
let narrow = |x: usize| {
u32::try_from(x).map_err(|_| {
LinAlgError::InvalidInput(format!(
"Jacobian index {x} exceeds the implicit Schur operator's u32 range"
))
.log()
})
};
let local = narrow(local)?;
let mut k = 0;
while let Some(&first_row) = rows.get(k) {
let chunk = first_row / chunk_rows;
let chunk_end = (chunk + 1) * chunk_rows;
let hi = k + rows[k..].partition_point(|&r| r < chunk_end);
let slot = spans.get_mut(chunk).ok_or_else(|| {
LinAlgError::InvalidInput(format!("row {first_row} outside the Jacobian")).log()
})?;
slot.push(ColumnSpan {
lo: narrow(start + k)?,
hi: narrow(start + hi)?,
local,
});
k = hi;
}
Ok(())
}
#[derive(Debug, Clone)]
pub struct ImplicitSparseSchur {
partition: Option<SchurPartition>,
eliminated: EliminatedBlocks,
max_eliminated_dof: usize,
ordering: SchurOrdering,
max_cg_iterations: usize,
cg_tolerance: f64,
cg_q_tolerance: f64,
preconditioner_type: SchurPreconditioner,
jacobian: Option<SparseColMat<usize, f64>>,
jacobian_pattern: Option<pattern::PatternFingerprint>,
gradient: Option<Mat<f64>>,
workspace_rows: Vec<f64>, workspace_lm: Vec<f64>, workspace_cam: Vec<f64>, block_scratch: Vec<f64>,
structure: StructureCache,
}
impl ImplicitSparseSchur {
pub fn new() -> Self {
Self {
partition: None,
eliminated: EliminatedBlocks::default(),
max_eliminated_dof: 1,
ordering: SchurOrdering::default(),
max_cg_iterations: 500,
cg_tolerance: 1e-9,
cg_q_tolerance: crate::linalg::schur::DEFAULT_ETA,
preconditioner_type: SchurPreconditioner::default(),
jacobian: None,
jacobian_pattern: None,
gradient: None,
workspace_rows: Vec::new(),
workspace_lm: Vec::new(),
workspace_cam: Vec::new(),
block_scratch: Vec::new(),
structure: StructureCache::default(),
}
}
pub fn with_cg_config(mut self, max_iterations: usize, tolerance: f64) -> Self {
self.max_cg_iterations = max_iterations;
self.cg_tolerance = tolerance;
self
}
pub fn with_preconditioner(mut self, preconditioner: SchurPreconditioner) -> Self {
self.preconditioner_type = preconditioner;
self
}
pub fn with_cg_q_tolerance(mut self, q_tolerance: f64) -> Self {
self.cg_q_tolerance = q_tolerance;
self
}
pub fn with_ordering(mut self, ordering: SchurOrdering) -> Self {
self.ordering = ordering;
self
}
pub fn with_cg_params(max_iterations: usize, tolerance: f64) -> Self {
Self::new().with_cg_config(max_iterations, tolerance)
}
pub fn with_config(
max_iterations: usize,
tolerance: f64,
preconditioner: SchurPreconditioner,
) -> Self {
Self::new()
.with_cg_config(max_iterations, tolerance)
.with_preconditioner(preconditioner)
}
pub fn partition(&self) -> Option<&SchurPartition> {
self.partition.as_ref()
}
fn require_partition(&self) -> LinAlgResult<&SchurPartition> {
self.partition.as_ref().ok_or_else(|| {
LinAlgError::InvalidInput(
"Schur solver used before initialize_structure; the partition is unknown".into(),
)
.log()
})
}
fn ensure_structure(&mut self, jacobian: &SparseColMat<usize, f64>) -> LinAlgResult<()> {
let fingerprint = pattern::PatternFingerprint::of(jacobian);
if self.structure.fingerprint == Some(fingerprint) {
return Ok(());
}
let partition = self.require_partition()?;
let symbolic = jacobian.symbolic();
let mut row_owner = vec![usize::MAX; jacobian.nrows()];
let mut eliminated_rows = vec![Vec::new(); partition.eliminated_blocks().len()];
for (block_idx, block) in partition.eliminated_blocks().iter().enumerate() {
for offset in 0..block.dof {
for &row in symbolic.row_idx_of_col_raw(block.col_start + offset) {
match row_owner[row] {
usize::MAX => {
row_owner[row] = block_idx;
eliminated_rows[block_idx].push(row);
}
owned if owned == block_idx => {}
owned => {
let other = partition.eliminated_blocks()[owned];
return Err(LinAlgError::InvalidInput(format!(
"variables {:?} and {:?} are both marked for elimination but \
share residual row {row}, so EᵀE is not block-diagonal and \
the elimination would give a wrong step; eliminate only \
mutually unconnected variables",
block.key, other.key
))
.log());
}
}
}
}
}
for rows in &mut eliminated_rows {
rows.sort_unstable();
}
let visibility: Vec<Vec<usize>> = partition
.kept_blocks()
.par_iter()
.map(|block| {
let mut seen = Vec::new();
for offset in 0..block.dof {
for &row in symbolic.row_idx_of_col_raw(block.col_start + offset) {
let owner = row_owner[row];
if owner != usize::MAX {
seen.push(owner);
}
}
}
seen.sort_unstable();
seen.dedup();
seen
})
.collect();
let mut kept_cols = vec![0usize; partition.kept_dof()];
for block in partition.kept_blocks() {
for offset in 0..block.dof {
let col = block.col_start + offset;
if let Some(local) = partition.kept_local(col) {
kept_cols[local] = col;
}
}
}
let mut eliminated_cols = vec![0usize; partition.eliminated_dof()];
for (block_idx, block) in partition.eliminated_blocks().iter().enumerate() {
let base = partition.eliminated_offset(block_idx);
for offset in 0..block.dof {
eliminated_cols[base + offset] = block.col_start + offset;
}
}
let chunk_rows = row_chunk_len(jacobian.nrows());
let spans = RowSpans::build(jacobian, partition, chunk_rows)?;
self.structure = StructureCache {
kept_cols,
eliminated_cols,
eliminated_rows,
visibility,
chunk_rows,
kept_spans: spans.kept,
eliminated_spans: spans.eliminated,
eliminated_block_of: spans.block_of,
fingerprint: Some(fingerprint),
};
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn apply_schur_operator(
&self,
partition: &SchurPartition,
jacobian: &SparseColMat<usize, f64>,
damp_kept: &[f64],
v: &Mat<f64>,
out: &mut Mat<f64>,
out_buf: &mut [f64],
rows: &mut [f64],
temp_lm: &mut [f64],
scratch: &mut [f64],
) {
let symbolic = jacobian.symbolic();
let row_idx = symbolic.row_idx();
let values = jacobian.val();
let chunk_rows = self.structure.chunk_rows.max(1);
rows.par_chunks_mut(chunk_rows)
.zip(self.structure.kept_spans.par_iter())
.enumerate()
.for_each(|(chunk, (y, spans))| {
y.fill(0.0);
let first = chunk * chunk_rows;
for span in spans {
let x = v[(span.local as usize, 0)];
if x == 0.0 {
continue;
}
for k in span.lo as usize..span.hi as usize {
y[row_idx[k] - first] += values[k] * x;
}
}
});
let rows_ref: &[f64] = rows;
temp_lm
.par_iter_mut()
.zip(self.structure.eliminated_cols.par_iter())
.for_each(|(t, &col)| {
let idx = symbolic.row_idx_of_col_raw(col);
let vals = jacobian.val_of_col(col);
*t = idx
.iter()
.zip(vals)
.map(|(&row, val)| val * rows_ref[row])
.sum();
});
let t: &[f64] = temp_lm;
let eliminated = &self.eliminated;
let block_of = &self.structure.eliminated_block_of;
scratch.par_iter_mut().enumerate().for_each(|(i, u)| {
let block_idx = block_of[i] as usize;
let dof = eliminated.dof(block_idx);
let base = partition.eliminated_offset(block_idx);
let inv = eliminated.block(block_idx);
let r = i - base;
let mut acc = 0.0;
for c in 0..dof {
acc += inv[c * dof + r] * t[base + c];
}
*u = acc;
});
let u: &[f64] = scratch;
rows.par_chunks_mut(chunk_rows)
.zip(self.structure.eliminated_spans.par_iter())
.enumerate()
.for_each(|(chunk, (y, spans))| {
let first = chunk * chunk_rows;
for span in spans {
let u = u[span.local as usize];
if u == 0.0 {
continue;
}
for k in span.lo as usize..span.hi as usize {
y[row_idx[k] - first] -= values[k] * u;
}
}
});
let rows_ref: &[f64] = rows;
out_buf
.par_iter_mut()
.enumerate()
.zip(self.structure.kept_cols.par_iter())
.for_each(|((local, o), &col)| {
let idx = symbolic.row_idx_of_col_raw(col);
let vals = jacobian.val_of_col(col);
let mut acc: f64 = idx
.iter()
.zip(vals)
.map(|(&row, val)| val * rows_ref[row])
.sum();
if let Some(d) = damp_kept.get(local) {
acc += d * v[(local, 0)];
}
*o = acc;
});
for (local, value) in out_buf.iter().enumerate() {
out[(local, 0)] = *value;
}
}
fn gather_strip(
jacobian: &SparseColMat<usize, f64>,
block: &BlockSpan,
rows: &[usize],
) -> DMatrix<f64> {
let symbolic = jacobian.symbolic();
let mut strip = DMatrix::zeros(rows.len(), block.dof);
for offset in 0..block.dof {
let col = block.col_start + offset;
let idx = symbolic.row_idx_of_col_raw(col);
let vals = jacobian.val_of_col(col);
for (local_row, &row) in rows.iter().enumerate() {
if let Ok(k) = idx.binary_search(&row) {
strip[(local_row, offset)] = vals[k];
}
}
}
strip
}
fn build_preconditioner(
&self,
partition: &SchurPartition,
jacobian: &SparseColMat<usize, f64>,
damp_kept: &[f64],
) -> Option<Vec<DMatrix<f64>>> {
if self.preconditioner_type == SchurPreconditioner::None {
return None;
}
let blocks = partition
.kept_blocks()
.par_iter()
.enumerate()
.map(|(kept_idx, block)| {
let dof = block.dof;
let base = partition.kept_offset(kept_idx);
let mut s_ii = DMatrix::zeros(dof, dof);
for c in 0..dof {
for r in 0..dof {
s_ii[(r, c)] =
column_dot(jacobian, block.col_start + r, block.col_start + c);
}
if let Some(d) = damp_kept.get(base + c) {
s_ii[(c, c)] += d;
}
}
if self.preconditioner_type == SchurPreconditioner::SchurJacobi {
let visible = self
.structure
.visibility
.get(kept_idx)
.map_or(&[][..], Vec::as_slice);
for &elim_idx in visible {
let elim_block = partition.eliminated_blocks()[elim_idx];
let rows = &self.structure.eliminated_rows[elim_idx];
let f_strip = Self::gather_strip(jacobian, block, rows);
let e_strip = Self::gather_strip(jacobian, &elim_block, rows);
let m = f_strip.transpose() * e_strip;
let elim_dof = elim_block.dof;
let inv = DMatrix::from_column_slice(
elim_dof,
elim_dof,
self.eliminated.block(elim_idx),
);
s_ii -= &m * inv * m.transpose();
}
}
invert_with_retry_dyn(&s_ii).unwrap_or_else(|| DMatrix::identity(dof, dof))
})
.collect();
Some(blocks)
}
fn apply_preconditioner(blocks: Option<&[DMatrix<f64>]>, r: &Mat<f64>, z: &mut Mat<f64>) {
let Some(blocks) = blocks else {
faer::zip!(z, r).for_each(|faer::unzip!(z, r)| *z = *r);
return;
};
let mut offset = 0usize;
for inv in blocks {
let dof = inv.nrows();
for row in 0..dof {
let mut acc = 0.0;
for col in 0..dof {
acc += inv[(row, col)] * r[(offset + col, 0)];
}
z[(offset + row, 0)] = acc;
}
offset += dof;
}
}
fn solve_from_jacobian(
&mut self,
residuals: &Mat<f64>,
jacobian: &SparseColMat<usize, f64>,
damping: Option<&Damping>,
) -> LinAlgResult<Mat<f64>> {
self.ensure_structure(jacobian)?;
let partition = self.require_partition()?.clone();
if jacobian.ncols() != partition.total_dof() {
return Err(LinAlgError::InvalidInput(format!(
"Jacobian has {} columns but the partition covers {}",
jacobian.ncols(),
partition.total_dof()
))
.log());
}
let gradient = jt_vec(jacobian, residuals);
let kept_dof = partition.kept_dof();
let eliminated_dof = partition.eliminated_dof();
let damp_kept: Vec<f64> = match damping {
None => Vec::new(),
Some(d) => {
let diag = diag_jt_j(jacobian);
let mut per_kept = vec![0.0; kept_dof];
for block in partition.kept_blocks() {
for offset in 0..block.dof {
let col = block.col_start + offset;
if let Some(local) = partition.kept_local(col) {
per_kept[local] = d.diagonal_term(diag[col]);
}
}
}
per_kept
}
};
let mut eliminated = std::mem::take(&mut self.eliminated);
eliminated.gather_from_jacobian(jacobian, &partition);
if let Some(d) = damping {
eliminated.damp(d);
}
let inversion = eliminated.invert_in_place(&partition);
self.eliminated = eliminated;
inversion?;
let mut g_k = Mat::<f64>::zeros(kept_dof, 1);
for block in partition.kept_blocks() {
for offset in 0..block.dof {
let col = block.col_start + offset;
if let Some(local) = partition.kept_local(col) {
g_k[(local, 0)] = -gradient[(col, 0)];
}
}
}
let mut g_e = Mat::<f64>::zeros(eliminated_dof, 1);
for (block_idx, block) in partition.eliminated_blocks().iter().enumerate() {
let base = partition.eliminated_offset(block_idx);
for offset in 0..block.dof {
g_e[(base + offset, 0)] = -gradient[(block.col_start + offset, 0)];
}
}
let mut hee_ge = Mat::<f64>::zeros(eliminated_dof, 1);
self.apply_eliminated_inverse(&partition, &g_e, &mut hee_ge);
let correction = self.coupling_times(&partition, jacobian, &hee_ge);
let g_reduced = Mat::from_fn(kept_dof, 1, |i, _| g_k[(i, 0)] - correction[(i, 0)]);
let precond = self.build_preconditioner(&partition, jacobian, &damp_kept);
let delta_k = self.solve_pcg_block(
&partition,
jacobian,
&damp_kept,
&g_reduced,
precond.as_deref(),
);
let coupling_t = self.coupling_transpose_times(&partition, jacobian, &delta_k);
let rhs_e = Mat::from_fn(eliminated_dof, 1, |i, _| g_e[(i, 0)] - coupling_t[(i, 0)]);
let mut delta_e = Mat::<f64>::zeros(eliminated_dof, 1);
self.apply_eliminated_inverse(&partition, &rhs_e, &mut delta_e);
let delta = self.combine_updates(&partition, &delta_k, &delta_e);
self.gradient = Some(gradient);
self.retain_jacobian(jacobian);
Ok(delta)
}
fn retain_jacobian(&mut self, jacobian: &SparseColMat<usize, f64>) {
let fingerprint = self.structure.fingerprint;
let reusable = self.jacobian_pattern == fingerprint
&& self
.jacobian
.as_ref()
.is_some_and(|held| held.val().len() == jacobian.val().len());
match self.jacobian.as_mut() {
Some(held) if reusable => held.val_mut().copy_from_slice(jacobian.val()),
_ => {
self.jacobian = Some(jacobian.clone());
self.jacobian_pattern = fingerprint;
}
}
}
fn apply_eliminated_inverse(&self, partition: &SchurPartition, b: &Mat<f64>, x: &mut Mat<f64>) {
for block_idx in 0..partition.eliminated_blocks().len() {
let dof = self.eliminated.dof(block_idx);
let base = partition.eliminated_offset(block_idx);
let inv = self.eliminated.block(block_idx);
for r in 0..dof {
let mut acc = 0.0;
for c in 0..dof {
acc += inv[c * dof + r] * b[(base + c, 0)];
}
x[(base + r, 0)] = acc;
}
}
}
fn coupling_times(
&self,
partition: &SchurPartition,
jacobian: &SparseColMat<usize, f64>,
u: &Mat<f64>,
) -> Mat<f64> {
let symbolic = jacobian.symbolic();
let mut rows = vec![0.0; jacobian.nrows()];
for (block_idx, block) in partition.eliminated_blocks().iter().enumerate() {
let base = partition.eliminated_offset(block_idx);
for offset in 0..block.dof {
let x = u[(base + offset, 0)];
if x == 0.0 {
continue;
}
let col = block.col_start + offset;
let idx = symbolic.row_idx_of_col_raw(col);
let vals = jacobian.val_of_col(col);
for (k, &row) in idx.iter().enumerate() {
rows[row] += vals[k] * x;
}
}
}
let mut out = Mat::<f64>::zeros(partition.kept_dof(), 1);
for block in partition.kept_blocks() {
for offset in 0..block.dof {
let col = block.col_start + offset;
let Some(local) = partition.kept_local(col) else {
continue;
};
let idx = symbolic.row_idx_of_col_raw(col);
let vals = jacobian.val_of_col(col);
let mut acc = 0.0;
for (k, &row) in idx.iter().enumerate() {
acc += vals[k] * rows[row];
}
out[(local, 0)] = acc;
}
}
out
}
fn coupling_transpose_times(
&self,
partition: &SchurPartition,
jacobian: &SparseColMat<usize, f64>,
v: &Mat<f64>,
) -> Mat<f64> {
let symbolic = jacobian.symbolic();
let mut rows = vec![0.0; jacobian.nrows()];
for block in partition.kept_blocks() {
for offset in 0..block.dof {
let col = block.col_start + offset;
let Some(local) = partition.kept_local(col) else {
continue;
};
let x = v[(local, 0)];
if x == 0.0 {
continue;
}
let idx = symbolic.row_idx_of_col_raw(col);
let vals = jacobian.val_of_col(col);
for (k, &row) in idx.iter().enumerate() {
rows[row] += vals[k] * x;
}
}
}
let mut out = Mat::<f64>::zeros(partition.eliminated_dof(), 1);
for (block_idx, block) in partition.eliminated_blocks().iter().enumerate() {
let base = partition.eliminated_offset(block_idx);
for offset in 0..block.dof {
let col = block.col_start + offset;
let idx = symbolic.row_idx_of_col_raw(col);
let vals = jacobian.val_of_col(col);
let mut acc = 0.0;
for (k, &row) in idx.iter().enumerate() {
acc += vals[k] * rows[row];
}
out[(base + offset, 0)] = acc;
}
}
out
}
fn solve_pcg_block(
&mut self,
partition: &SchurPartition,
jacobian: &SparseColMat<usize, f64>,
damp_kept: &[f64],
b: &Mat<f64>,
precond: Option<&[DMatrix<f64>]>,
) -> Mat<f64> {
let mut rows = std::mem::take(&mut self.workspace_rows);
let mut temp_lm = std::mem::take(&mut self.workspace_lm);
let mut out_buf = std::mem::take(&mut self.workspace_cam);
let mut scratch = std::mem::take(&mut self.block_scratch);
out_buf.clear();
out_buf.resize(partition.kept_dof(), 0.0);
rows.clear();
rows.resize(jacobian.nrows(), 0.0);
temp_lm.clear();
temp_lm.resize(partition.eliminated_dof(), 0.0);
scratch.clear();
scratch.resize(partition.eliminated_dof(), 0.0);
let result = solve_pcg(
b,
&PcgParams::new(self.max_cg_iterations, self.cg_tolerance)
.with_q_tolerance(self.cg_q_tolerance),
|p, ap| {
self.apply_schur_operator(
partition,
jacobian,
damp_kept,
p,
ap,
&mut out_buf,
&mut rows,
&mut temp_lm,
&mut scratch,
);
},
|r, z| Self::apply_preconditioner(precond, r, z),
);
self.workspace_rows = rows;
self.workspace_lm = temp_lm;
self.workspace_cam = out_buf;
self.block_scratch = scratch;
debug!(
"PCG: {} iterations, residual {:.3e}, {:?} ({:?})",
result.iterations, result.final_residual, result.termination, self.preconditioner_type
);
result.x
}
fn combine_updates(
&self,
partition: &SchurPartition,
delta_k: &Mat<f64>,
delta_e: &Mat<f64>,
) -> Mat<f64> {
let mut delta = Mat::zeros(partition.total_dof(), 1);
for block in partition.kept_blocks() {
for offset in 0..block.dof {
let global = block.col_start + offset;
if let Some(local) = partition.kept_local(global) {
delta[(global, 0)] = delta_k[(local, 0)];
}
}
}
for (block_idx, block) in partition.eliminated_blocks().iter().enumerate() {
let base = partition.eliminated_offset(block_idx);
for offset in 0..block.dof {
delta[(block.col_start + offset, 0)] = delta_e[(base + offset, 0)];
}
}
delta
}
}
impl Default for ImplicitSparseSchur {
fn default() -> Self {
Self::new()
}
}
impl LinearSolver<SparseMode> for ImplicitSparseSchur {
fn solve_normal_equation(
&mut self,
residuals: &Mat<f64>,
jacobian: &SparseColMat<usize, f64>,
) -> LinAlgResult<Mat<f64>> {
self.solve_from_jacobian(residuals, jacobian, None)
}
fn solve_augmented_equation(
&mut self,
residuals: &Mat<f64>,
jacobian: &SparseColMat<usize, f64>,
damping: &Damping,
) -> LinAlgResult<Mat<f64>> {
self.solve_from_jacobian(residuals, jacobian, Some(damping))
}
fn hessian_vec_product(&self, v: &Mat<f64>) -> Option<Mat<f64>> {
Some(jt_j_vec_product(self.jacobian.as_ref()?, v))
}
fn get_hessian(&self) -> Option<&SparseColMat<usize, f64>> {
None
}
fn get_gradient(&self) -> Option<&Mat<f64>> {
self.gradient.as_ref()
}
}
impl StructureAware for ImplicitSparseSchur {
fn initialize_structure(
&mut self,
variables: &SlotMap<VarKey, Box<dyn ManifoldVariable>>,
variable_index_map: &SecondaryMap<VarKey, usize>,
schur_landmark_keys: &std::collections::HashSet<VarKey>,
) -> LinAlgResult<()> {
let effective_keys =
effective_landmark_keys(variables, schur_landmark_keys, &self.ordering);
let mut kept = Vec::new();
let mut eliminated = Vec::new();
for (key, variable) in variables {
let col_start = *variable_index_map.get(key).ok_or_else(|| {
LinAlgError::InvalidInput(format!("VarKey {:?} not in index map", key))
})?;
let span = BlockSpan {
key,
col_start,
dof: variable.free_dof(),
};
if effective_keys.contains(&key) {
eliminated.push(span);
} else {
kept.push(span);
}
}
let partition = SchurPartition::new(kept, eliminated)?;
self.max_eliminated_dof = partition
.eliminated_blocks()
.iter()
.map(|b| b.dof)
.max()
.unwrap_or(0)
.max(1);
self.eliminated = EliminatedBlocks::new(&partition);
self.workspace_lm = vec![0.0; partition.eliminated_dof()];
self.workspace_cam = vec![0.0; partition.kept_dof()];
self.block_scratch = vec![0.0; self.max_eliminated_dof];
self.workspace_rows.clear();
self.structure = StructureCache::default();
self.partition = Some(partition);
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::VarKey;
use crate::core::variable::Variable;
use apex_manifolds::{LieGroup, rn, se3};
use faer::sparse::Triplet;
use nalgebra::DVector;
use slotmap::{SecondaryMap, SlotMap};
type TestResult = Result<(), Box<dyn std::error::Error>>;
type TestSetup = (
SlotMap<VarKey, Box<dyn ManifoldVariable>>,
SecondaryMap<VarKey, usize>,
SparseColMat<usize, f64>,
faer::Mat<f64>,
std::collections::HashSet<VarKey>,
);
fn create_schur_test_setup() -> Result<TestSetup, Box<dyn std::error::Error>> {
let se3_id = DVector::from_vec(vec![1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]);
let pt_zero = DVector::from_vec(vec![0.0, 0.0, 0.0]);
let mut variables: SlotMap<VarKey, Box<dyn ManifoldVariable>> = SlotMap::with_key();
let cam0 = variables.insert(Box::new(Variable::new(se3::SE3::from_param_slice(
se3_id.as_slice(),
))));
let cam1 = variables.insert(Box::new(Variable::new(se3::SE3::from_param_slice(
se3_id.as_slice(),
))));
let pt0 = variables.insert(Box::new(Variable::new(rn::Rn::new(pt_zero.clone()))));
let pt1 = variables.insert(Box::new(Variable::new(rn::Rn::new(pt_zero.clone()))));
let pt2 = variables.insert(Box::new(Variable::new(rn::Rn::new(pt_zero.clone()))));
let mut variable_index_map: SecondaryMap<VarKey, usize> = SecondaryMap::new();
variable_index_map.insert(cam0, 0);
variable_index_map.insert(cam1, 6);
variable_index_map.insert(pt0, 12);
variable_index_map.insert(pt1, 15);
variable_index_map.insert(pt2, 18);
let cam_cols = [0usize, 6];
let lm_cols = [12usize, 15, 18];
let mut triplets: Vec<Triplet<usize, usize, f64>> = Vec::new();
for (ci, &cam_col) in cam_cols.iter().enumerate() {
for (li, &lm_col) in lm_cols.iter().enumerate() {
let row_base = (ci * 3 + li) * 6;
for k in 0..6 {
triplets.push(Triplet::new(row_base + k, cam_col + k, 1.0));
triplets.push(Triplet::new(row_base + k, lm_col + (k % 3), 1.0));
}
}
}
let jacobian = SparseColMat::try_new_from_triplets(36, 21, &triplets)?;
let residuals = faer::Mat::from_fn(36, 1, |i, _| (i % 5) as f64 * 0.1);
let mut landmark_keys = std::collections::HashSet::new();
landmark_keys.insert(pt0);
landmark_keys.insert(pt1);
landmark_keys.insert(pt2);
Ok((
variables,
variable_index_map,
jacobian,
residuals,
landmark_keys,
))
}
#[test]
fn test_implicit_schur_creation() {
let solver = ImplicitSparseSchur::new();
assert_eq!(solver.max_cg_iterations, 500);
assert_eq!(solver.cg_tolerance, 1e-9);
assert_eq!(solver.preconditioner_type, SchurPreconditioner::SchurJacobi);
}
#[test]
fn test_with_custom_params() {
let solver = ImplicitSparseSchur::with_cg_params(100, 1e-8);
assert_eq!(solver.max_cg_iterations, 100);
assert_eq!(solver.cg_tolerance, 1e-8);
assert_eq!(solver.preconditioner_type, SchurPreconditioner::SchurJacobi);
}
#[test]
fn test_with_full_config() {
let solver =
ImplicitSparseSchur::with_config(200, 1e-10, SchurPreconditioner::BlockDiagonal);
assert_eq!(solver.max_cg_iterations, 200);
assert_eq!(solver.cg_tolerance, 1e-10);
assert_eq!(
solver.preconditioner_type,
SchurPreconditioner::BlockDiagonal
);
}
#[test]
fn test_implicit_schur_default() {
let solver = ImplicitSparseSchur::default();
assert_eq!(solver.max_cg_iterations, 500);
assert_eq!(solver.cg_tolerance, 1e-9);
assert!(solver.partition.is_none());
assert!(solver.jacobian.is_none());
}
#[test]
fn test_implicit_schur_initialize_structure() -> TestResult {
let (variables, variable_index_map, _, _, landmark_keys) = create_schur_test_setup()?;
let mut solver = ImplicitSparseSchur::new();
solver.initialize_structure(&variables, &variable_index_map, &landmark_keys)?;
let partition = solver.partition.as_ref().ok_or("partition is None")?;
assert_eq!(partition.kept_blocks().len(), 2);
assert_eq!(partition.eliminated_blocks().len(), 3);
assert_eq!(partition.kept_dof(), 12);
assert_eq!(partition.eliminated_dof(), 9);
Ok(())
}
#[test]
fn test_implicit_schur_solve_normal_equation() -> TestResult {
let (variables, variable_index_map, jacobian, residuals, landmark_keys) =
create_schur_test_setup()?;
let mut solver = ImplicitSparseSchur::with_cg_params(500, 1e-6);
solver.initialize_structure(&variables, &variable_index_map, &landmark_keys)?;
let delta =
LinearSolver::<SparseMode>::solve_normal_equation(&mut solver, &residuals, &jacobian)?;
assert_eq!(delta.nrows(), 21);
assert_eq!(delta.ncols(), 1);
Ok(())
}
#[test]
fn test_implicit_schur_solve_augmented_equation() -> TestResult {
let (variables, variable_index_map, jacobian, residuals, landmark_keys) =
create_schur_test_setup()?;
let mut solver = ImplicitSparseSchur::with_cg_params(500, 1e-6);
solver.initialize_structure(&variables, &variable_index_map, &landmark_keys)?;
let delta = LinearSolver::<SparseMode>::solve_augmented_equation(
&mut solver,
&residuals,
&jacobian,
&Damping::identity(0.1),
)?;
assert_eq!(delta.nrows(), 21);
Ok(())
}
#[test]
fn test_implicit_schur_get_hessian_gradient() -> TestResult {
let (variables, variable_index_map, jacobian, residuals, landmark_keys) =
create_schur_test_setup()?;
let mut solver = ImplicitSparseSchur::with_cg_params(500, 1e-6);
solver.initialize_structure(&variables, &variable_index_map, &landmark_keys)?;
assert!(LinearSolver::<SparseMode>::get_hessian(&solver).is_none());
assert!(LinearSolver::<SparseMode>::get_gradient(&solver).is_none());
LinearSolver::<SparseMode>::solve_normal_equation(&mut solver, &residuals, &jacobian)?;
assert!(
LinearSolver::<SparseMode>::get_hessian(&solver).is_none(),
"matrix-free solver must not publish a Hessian"
);
let g = LinearSolver::<SparseMode>::get_gradient(&solver).ok_or("gradient is None")?;
assert_eq!(g.nrows(), 21);
let v = faer::Mat::from_fn(21, 1, |i, _| ((i % 3) as f64) - 1.0);
let hv = LinearSolver::<SparseMode>::hessian_vec_product(&solver, &v)
.ok_or("hessian_vec_product is None")?;
assert_eq!(hv.nrows(), 21);
Ok(())
}
#[test]
fn every_preconditioner_reaches_the_same_step() -> TestResult {
let mut steps = Vec::new();
for preconditioner in [
SchurPreconditioner::None,
SchurPreconditioner::BlockDiagonal,
SchurPreconditioner::SchurJacobi,
] {
let (variables, variable_index_map, jacobian, residuals, landmark_keys) =
create_schur_test_setup()?;
let mut solver = ImplicitSparseSchur::new()
.with_cg_config(1000, 1e-12)
.with_preconditioner(preconditioner);
solver.initialize_structure(&variables, &variable_index_map, &landmark_keys)?;
steps.push(LinearSolver::<SparseMode>::solve_normal_equation(
&mut solver,
&residuals,
&jacobian,
)?);
}
let reference = &steps[0];
for (idx, step) in steps.iter().enumerate().skip(1) {
for row in 0..reference.nrows() {
let scale = reference[(row, 0)].abs().max(1.0);
assert!(
(reference[(row, 0)] - step[(row, 0)]).abs() / scale < 1e-6,
"preconditioner {idx} diverged at row {row}: {} vs {}",
step[(row, 0)],
reference[(row, 0)]
);
}
}
Ok(())
}
#[test]
fn test_implicit_schur_block_diagonal_preconditioner() -> TestResult {
let (variables, variable_index_map, jacobian, residuals, landmark_keys) =
create_schur_test_setup()?;
let mut solver =
ImplicitSparseSchur::with_config(500, 1e-6, SchurPreconditioner::BlockDiagonal);
solver.initialize_structure(&variables, &variable_index_map, &landmark_keys)?;
let delta =
LinearSolver::<SparseMode>::solve_normal_equation(&mut solver, &residuals, &jacobian)?;
assert_eq!(delta.nrows(), 21);
Ok(())
}
#[test]
fn test_implicit_schur_schur_jacobi_preconditioner() -> TestResult {
let (variables, variable_index_map, jacobian, residuals, landmark_keys) =
create_schur_test_setup()?;
let mut solver =
ImplicitSparseSchur::with_config(500, 1e-6, SchurPreconditioner::SchurJacobi);
solver.initialize_structure(&variables, &variable_index_map, &landmark_keys)?;
let delta =
LinearSolver::<SparseMode>::solve_normal_equation(&mut solver, &residuals, &jacobian)?;
assert_eq!(delta.nrows(), 21);
Ok(())
}
#[test]
fn test_implicit_schur_no_preconditioner() -> TestResult {
let (variables, variable_index_map, jacobian, residuals, landmark_keys) =
create_schur_test_setup()?;
let mut solver = ImplicitSparseSchur::with_config(500, 1e-6, SchurPreconditioner::None);
solver.initialize_structure(&variables, &variable_index_map, &landmark_keys)?;
let delta =
LinearSolver::<SparseMode>::solve_normal_equation(&mut solver, &residuals, &jacobian)?;
assert_eq!(delta.nrows(), 21);
Ok(())
}
#[test]
fn test_implicit_schur_augmented_lambda_effect() -> TestResult {
let (variables, variable_index_map, jacobian, residuals, landmark_keys) =
create_schur_test_setup()?;
let mut s1 = ImplicitSparseSchur::with_cg_params(500, 1e-6);
s1.initialize_structure(&variables, &variable_index_map, &landmark_keys)?;
let d1 = LinearSolver::<SparseMode>::solve_augmented_equation(
&mut s1,
&residuals,
&jacobian,
&Damping::identity(0.001),
)?;
let mut s2 = ImplicitSparseSchur::with_cg_params(500, 1e-6);
s2.initialize_structure(&variables, &variable_index_map, &landmark_keys)?;
let d2 = LinearSolver::<SparseMode>::solve_augmented_equation(
&mut s2,
&residuals,
&jacobian,
&Damping::identity(100.0),
)?;
let norm_diff: f64 = (0..21).map(|i| (d1[(i, 0)] - d2[(i, 0)]).powi(2)).sum();
assert!(
norm_diff > 1e-10,
"Different λ should yield different updates"
);
Ok(())
}
#[test]
fn test_implicit_schur_solve_without_init_returns_error() -> TestResult {
let triplets: Vec<Triplet<usize, usize, f64>> = vec![Triplet::new(0, 0, 1.0)];
let jacobian =
SparseColMat::try_new_from_triplets(1, 1, &triplets).map_err(|e| format!("{e:?}"))?;
let residuals = faer::Mat::from_fn(1, 1, |_, _| 1.0);
let mut solver = ImplicitSparseSchur::new();
let result =
LinearSolver::<SparseMode>::solve_normal_equation(&mut solver, &residuals, &jacobian);
assert!(result.is_err());
Ok(())
}
#[test]
fn test_implicit_initialize_structure_no_cameras_returns_error() {
use crate::core::variable::Variable;
use apex_manifolds::rn;
let mut variables: SlotMap<VarKey, Box<dyn ManifoldVariable>> = SlotMap::with_key();
let k = variables.insert(Box::new(Variable::new(rn::Rn::new(DVector::zeros(3)))));
let mut variable_index_map: SecondaryMap<VarKey, usize> = SecondaryMap::new();
variable_index_map.insert(k, 0);
let mut landmark_keys = std::collections::HashSet::new();
landmark_keys.insert(k);
let mut solver = ImplicitSparseSchur::new();
let result = solver.initialize_structure(&variables, &variable_index_map, &landmark_keys);
assert!(
result.is_err(),
"Expected Err when no retained variables present"
);
}
#[test]
fn test_implicit_initialize_structure_no_landmarks_returns_error() {
use crate::core::variable::Variable;
use apex_manifolds::se3;
let mut variables: SlotMap<VarKey, Box<dyn ManifoldVariable>> = SlotMap::with_key();
let k = variables.insert(Box::new(Variable::new(se3::SE3::from_param_slice(&[
1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
]))));
let mut variable_index_map: SecondaryMap<VarKey, usize> = SecondaryMap::new();
variable_index_map.insert(k, 0);
let landmark_keys = std::collections::HashSet::<VarKey>::new();
let mut solver = ImplicitSparseSchur::new();
let result = solver.initialize_structure(&variables, &variable_index_map, &landmark_keys);
assert!(
result.is_err(),
"Expected Err when no eliminated variables present"
);
}
#[test]
fn test_implicit_schur_supports_non_contiguous_mixed_dof_partition() -> TestResult {
use crate::linalg::schur::BlockSpan;
let a = Variable::new(se3::SE3::from_param_slice(&[
1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
]));
let b = Variable::new(se3::SE3::from_param_slice(&[
1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
]));
let p = Variable::new(rn::Rn::new(DVector::from_vec(vec![0.0])));
let q = Variable::new(rn::Rn::new(DVector::from_vec(vec![0.0])));
let mut variables: SlotMap<VarKey, Box<dyn ManifoldVariable>> = SlotMap::with_key();
let key_a = variables.insert(Box::new(a));
let key_p = variables.insert(Box::new(p));
let key_b = variables.insert(Box::new(b));
let key_q = variables.insert(Box::new(q));
let mut variable_index_map: SecondaryMap<VarKey, usize> = SecondaryMap::new();
variable_index_map.insert(key_a, 0);
variable_index_map.insert(key_p, 6);
variable_index_map.insert(key_b, 7);
variable_index_map.insert(key_q, 13);
let mut landmark_keys = std::collections::HashSet::new();
landmark_keys.insert(key_p);
landmark_keys.insert(key_q);
let mut triplets: Vec<Triplet<usize, usize, f64>> = Vec::new();
let mut row = 0usize;
for &cam_col in &[0usize, 7] {
for &lm_col in &[6usize, 13] {
for k in 0..1 {
triplets.push(Triplet::new(row + k, cam_col + k, 1.0));
triplets.push(Triplet::new(row + k, lm_col, 1.0));
}
row += 1;
}
}
for col in 0..14 {
triplets.push(Triplet::new(row, col, 0.5));
row += 1;
}
let jacobian = SparseColMat::try_new_from_triplets(row, 14, &triplets)?;
let residuals = faer::Mat::from_fn(row, 1, |i, _| 0.1 * (i as f64 + 1.0));
let mut solver = ImplicitSparseSchur::with_cg_params(500, 1e-10);
solver.initialize_structure(&variables, &variable_index_map, &landmark_keys)?;
let delta =
LinearSolver::<SparseMode>::solve_normal_equation(&mut solver, &residuals, &jacobian)?;
assert_eq!(delta.nrows(), 14);
for i in 0..14 {
assert!(delta[(i, 0)].is_finite());
}
let _ = BlockSpan {
key: key_a,
col_start: 0,
dof: 6,
};
Ok(())
}
fn column_scatter_reference(
solver: &ImplicitSparseSchur,
partition: &SchurPartition,
jacobian: &SparseColMat<usize, f64>,
v: &Mat<f64>,
) -> Vec<f64> {
let symbolic = jacobian.symbolic();
let mut rows = vec![0.0; jacobian.nrows()];
for block in partition.kept_blocks() {
for offset in 0..block.dof {
let col = block.col_start + offset;
let Some(local) = partition.kept_local(col) else {
continue;
};
let x = v[(local, 0)];
if x == 0.0 {
continue;
}
let idx = symbolic.row_idx_of_col_raw(col);
let vals = jacobian.val_of_col(col);
for (k, &row) in idx.iter().enumerate() {
rows[row] += vals[k] * x;
}
}
}
let mut t = vec![0.0; partition.eliminated_dof()];
for (slot, &col) in t.iter_mut().zip(&solver.structure.eliminated_cols) {
let idx = symbolic.row_idx_of_col_raw(col);
let vals = jacobian.val_of_col(col);
*slot = idx
.iter()
.zip(vals)
.map(|(&row, val)| val * rows[row])
.sum();
}
for (block_idx, block) in partition.eliminated_blocks().iter().enumerate() {
let dof = solver.eliminated.dof(block_idx);
let base = partition.eliminated_offset(block_idx);
let inv = solver.eliminated.block(block_idx);
let mut u = vec![0.0; dof];
for (r, out) in u.iter_mut().enumerate() {
let mut acc = 0.0;
for c in 0..dof {
acc += inv[c * dof + r] * t[base + c];
}
*out = acc;
}
for (offset, &u) in u.iter().enumerate() {
if u == 0.0 {
continue;
}
let col = block.col_start + offset;
let idx = symbolic.row_idx_of_col_raw(col);
let vals = jacobian.val_of_col(col);
for (k, &row) in idx.iter().enumerate() {
rows[row] -= vals[k] * u;
}
}
}
solver
.structure
.kept_cols
.iter()
.map(|&col| {
let idx = symbolic.row_idx_of_col_raw(col);
let vals = jacobian.val_of_col(col);
idx.iter()
.zip(vals)
.map(|(&row, val)| val * rows[row])
.sum()
})
.collect()
}
#[test]
fn row_chunked_operator_is_bit_identical_to_column_scatter() -> TestResult {
let cam = || -> Box<dyn ManifoldVariable> {
Box::new(Variable::new(se3::SE3::from_param_slice(&[
1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
])))
};
let point = || -> Box<dyn ManifoldVariable> {
Box::new(Variable::new(rn::Rn::new(DVector::from_vec(vec![0.0; 3]))))
};
let mut variables: SlotMap<VarKey, Box<dyn ManifoldVariable>> = SlotMap::with_key();
let cams: Vec<VarKey> = (0..3).map(|_| variables.insert(cam())).collect();
let points: Vec<VarKey> = (0..4).map(|_| variables.insert(point())).collect();
let mut variable_index_map: SecondaryMap<VarKey, usize> = SecondaryMap::new();
for (i, &k) in cams.iter().enumerate() {
variable_index_map.insert(k, i * 6);
}
for (i, &k) in points.iter().enumerate() {
variable_index_map.insert(k, 18 + i * 3);
}
let landmark_keys: std::collections::HashSet<VarKey> = points.iter().copied().collect();
let value = |r: usize, c: usize| ((r * 31 + c * 17) as f64 * 0.618_033_988_7).sin() + 1.5;
let mut triplets: Vec<Triplet<usize, usize, f64>> = Vec::new();
let mut row = 0usize;
for p in 0..4 {
for c in 0..3 {
for _ in 0..2 {
for k in 0..6 {
triplets.push(Triplet::new(row, c * 6 + k, value(row, c * 6 + k)));
if row % 2 == 1 {
let other = ((c + 1) % 3) * 6 + k;
triplets.push(Triplet::new(row, other, value(row, other)));
}
}
for k in 0..3 {
let col = 18 + p * 3 + k;
triplets.push(Triplet::new(row, col, value(row, col)));
}
row += 1;
}
}
}
let jacobian = SparseColMat::try_new_from_triplets(row, 30, &triplets)?;
let mut solver = ImplicitSparseSchur::new();
solver.initialize_structure(&variables, &variable_index_map, &landmark_keys)?;
solver.ensure_structure(&jacobian)?;
let partition = solver.require_partition()?.clone();
solver
.eliminated
.gather_from_jacobian(&jacobian, &partition);
solver.eliminated.invert_in_place(&partition)?;
let v = Mat::from_fn(partition.kept_dof(), 1, |i, _| {
(i as f64 * 0.37).cos() - 0.2
});
let want = column_scatter_reference(&solver, &partition, &jacobian, &v);
for chunk_rows in [1usize, 2, 3, 5, 7, row, row + 4] {
let spans = RowSpans::build(&jacobian, &partition, chunk_rows)?;
solver.structure.chunk_rows = chunk_rows;
solver.structure.kept_spans = spans.kept;
solver.structure.eliminated_spans = spans.eliminated;
solver.structure.eliminated_block_of = spans.block_of;
let mut out = Mat::zeros(partition.kept_dof(), 1);
let mut out_buf = vec![0.0; partition.kept_dof()];
let mut rows = vec![0.0; jacobian.nrows()];
let mut temp_lm = vec![0.0; partition.eliminated_dof()];
let mut scratch = vec![0.0; partition.eliminated_dof()];
solver.apply_schur_operator(
&partition,
&jacobian,
&[],
&v,
&mut out,
&mut out_buf,
&mut rows,
&mut temp_lm,
&mut scratch,
);
for (i, w) in want.iter().enumerate() {
assert_eq!(
out[(i, 0)].to_bits(),
w.to_bits(),
"chunk_rows={chunk_rows}, entry {i}: {} vs {w}",
out[(i, 0)]
);
}
}
Ok(())
}
}