use faer::Mat;
use faer::sparse::SparseColMat;
use crate::error::ErrorLogging;
use crate::linalg::schur::{EliminatedBlocks, SchurPartition};
use crate::linalg::sparse::pattern;
use crate::linalg::{Damping, LinAlgError, LinAlgResult};
#[derive(Debug, Clone, Default)]
pub struct ChunkLayout {
ranges: Vec<(usize, usize)>,
chunk_cols: Vec<u32>,
col_spans: Vec<(usize, usize)>,
pattern: pattern::PatternFingerprint,
}
impl ChunkLayout {
pub fn build(
jacobian: &SparseColMat<usize, f64>,
partition: &SchurPartition,
) -> LinAlgResult<Self> {
let symbolic = jacobian.symbolic();
let mut ranges = Vec::with_capacity(partition.eliminated_blocks().len());
let mut row_owner: Vec<usize> = vec![usize::MAX; jacobian.nrows()];
for (block_idx, block) in partition.eliminated_blocks().iter().enumerate() {
let mut lo = usize::MAX;
let mut hi = 0usize;
let mut count = 0usize;
for offset in 0..block.dof {
let rows = symbolic.row_idx_of_col_raw(block.col_start + offset);
if let (Some(&first), Some(&last)) = (rows.first(), rows.last()) {
lo = lo.min(first);
hi = hi.max(last + 1);
}
for &row in rows {
match row_owner[row] {
usize::MAX => row_owner[row] = block_idx,
owned if owned != block_idx => {
let other = partition.eliminated_blocks()[owned];
return Err(LinAlgError::InvalidInput(format!(
"variables {:?} and {:?} are both marked for elimination but \
share residual row {row}, so H_ee is not block-diagonal and \
chunk-wise elimination would give a wrong step; eliminate only \
mutually unconnected variables",
block.key, other.key
))
.log());
}
_ => {}
}
}
count += rows.len();
}
if count == 0 {
return Err(LinAlgError::InvalidInput(format!(
"variable {:?} is marked for elimination but appears in no residual; \
it cannot be eliminated",
block.key
))
.log());
}
ranges.push((lo, hi));
}
let mut previous_end = 0usize;
for (idx, &(lo, hi)) in ranges.iter().enumerate() {
if lo < previous_end {
return Err(LinAlgError::InvalidInput(format!(
"chunk {idx} starts at row {lo}, before the previous chunk ended at \
{previous_end}; chunk-wise elimination needs each eliminated variable's \
rows to form a contiguous, increasing range"
))
.log());
}
if idx > 0 && lo > previous_end {
return Err(LinAlgError::InvalidInput(format!(
"chunk {idx} starts at row {lo}, leaving a gap of rows \
{previous_end}..{lo} outside every chunk; the sweep would silently drop \
them — usually a factor couples two eliminated variables, or the rows \
are not grouped by eliminated variable"
))
.log());
}
previous_end = hi;
}
let mut per_chunk: Vec<Vec<u32>> = vec![Vec::new(); ranges.len()];
let mut local = 0u32;
for block in partition.kept_blocks() {
for offset in 0..block.dof {
let rows = symbolic.row_idx_of_col_raw(block.col_start + offset);
let mut last: Option<usize> = None;
for &row in rows {
let found = ranges.partition_point(|&(_, hi)| hi <= row);
if found < ranges.len() && row >= ranges[found].0 && last != Some(found) {
per_chunk[found].push(local);
last = Some(found);
}
}
local += 1;
}
}
let mut chunk_cols = Vec::new();
let mut col_spans = Vec::with_capacity(ranges.len());
for cols in &per_chunk {
col_spans.push((chunk_cols.len(), cols.len()));
chunk_cols.extend_from_slice(cols);
}
Ok(Self {
ranges,
chunk_cols,
col_spans,
pattern: pattern::PatternFingerprint::of(jacobian),
})
}
#[inline]
pub fn cols(&self, idx: usize) -> &[u32] {
let (start, len) = self.col_spans[idx];
&self.chunk_cols[start..start + len]
}
pub fn len(&self) -> usize {
self.ranges.len()
}
pub fn is_empty(&self) -> bool {
self.ranges.is_empty()
}
#[inline]
pub fn range(&self, idx: usize) -> (usize, usize) {
self.ranges[idx]
}
pub fn nrows(&self) -> usize {
self.pattern.nrows()
}
pub fn ncols(&self) -> usize {
self.pattern.ncols()
}
pub fn nnz(&self) -> usize {
self.pattern.nnz()
}
pub fn matches(&self, jacobian: &SparseColMat<usize, f64>) -> bool {
self.pattern == pattern::PatternFingerprint::of(jacobian)
}
}
pub struct ReducedSystem {
pub s: Mat<f64>,
pub g_reduced: Mat<f64>,
pub eliminated_inverse: EliminatedBlocks,
pub eliminated_rhs: Mat<f64>,
pub kept_dof: usize,
}
#[derive(Debug, Clone, Default)]
pub struct ChunkedSchurEliminator {
layout: ChunkLayout,
cursors: Vec<usize>,
}
impl ChunkedSchurEliminator {
pub fn new(
jacobian: &SparseColMat<usize, f64>,
partition: &SchurPartition,
) -> LinAlgResult<Self> {
Ok(Self {
layout: ChunkLayout::build(jacobian, partition)?,
cursors: vec![0; partition.kept_dof()],
})
}
pub fn matches(&self, jacobian: &SparseColMat<usize, f64>) -> bool {
self.layout.matches(jacobian)
}
pub fn eliminate(
&mut self,
jacobian: &SparseColMat<usize, f64>,
residuals: &Mat<f64>,
partition: &SchurPartition,
damping: Option<&Damping>,
) -> LinAlgResult<ReducedSystem> {
let kept_dof = partition.kept_dof();
let symbolic = jacobian.symbolic();
let mut s = Mat::<f64>::zeros(kept_dof, kept_dof);
let mut g_k = Mat::<f64>::zeros(kept_dof, 1);
let mut eliminated_inverse = EliminatedBlocks::new(partition);
let mut eliminated_rhs = Mat::<f64>::zeros(partition.eliminated_dof(), 1);
let kept_global: Vec<usize> = partition
.kept_blocks()
.iter()
.flat_map(|b| (0..b.dof).map(move |o| b.col_start + o))
.collect();
let all_kept: Vec<u32> = (0..kept_dof as u32).collect();
self.cursors.clear();
self.cursors.resize(kept_dof, 0);
let leading_end = if self.layout.is_empty() {
self.layout.nrows()
} else {
self.layout.range(0).0
};
if leading_end > 0 {
Self::sweep_rows(
&mut self.cursors,
jacobian,
residuals,
&kept_global,
&all_kept,
0,
leading_end,
&mut s,
&mut g_k,
);
}
let mut chunk_data: Vec<ChunkData> = Vec::with_capacity(self.layout.len());
for chunk in 0..self.layout.len() {
let (row_start, row_end) = self.layout.range(chunk);
let dof = eliminated_inverse.dof(chunk);
if dof == 0 || row_start >= row_end {
chunk_data.push(ChunkData::empty());
continue;
}
let block = partition.eliminated_blocks()[chunk];
let (start, len) = self.layout.col_spans[chunk];
let chunk_cols = &self.layout.chunk_cols[start..start + len];
let (cols, strip) = Self::gather_strip(
&mut self.cursors,
jacobian,
residuals,
&kept_global,
row_start,
row_end,
chunk_cols,
&mut s,
&mut g_k,
);
let ete = eliminated_inverse.block_mut(chunk);
ete.fill(0.0);
let mut etr = Mat::<f64>::zeros(dof, 1);
let mut etf = Mat::<f64>::zeros(dof, cols.len());
let e_cols: Vec<(&[usize], &[f64])> = (0..dof)
.map(|b| {
let col_b = block.col_start + b;
(
symbolic.row_idx_of_col_raw(col_b),
jacobian.val_of_col(col_b),
)
})
.collect();
for a in 0..dof {
let col_a = block.col_start + a;
let rows_a = symbolic.row_idx_of_col_raw(col_a);
let vals_a = jacobian.val_of_col(col_a);
for (i, &row) in rows_a.iter().enumerate() {
if row < row_start || row >= row_end {
continue;
}
let e = vals_a[i];
etr[(a, 0)] += e * residuals[(row, 0)];
let local_row = row - row_start;
for c in 0..cols.len() {
etf[(a, c)] += e * strip[(local_row, c)];
}
for (b, &(rows_b, vals_b)) in e_cols.iter().enumerate() {
if let Ok(pos) = rows_b.binary_search(&row) {
ete[b * dof + a] += e * vals_b[pos];
}
}
}
}
chunk_data.push(ChunkData {
cols,
etf,
etr,
dof,
});
}
if !self.layout.is_empty() {
let last_end = self.layout.range(self.layout.len() - 1).1;
if last_end < self.layout.nrows() {
Self::sweep_rows(
&mut self.cursors,
jacobian,
residuals,
&kept_global,
&all_kept,
last_end,
self.layout.nrows(),
&mut s,
&mut g_k,
);
}
}
if let Some(damping) = damping {
for i in 0..kept_dof {
s[(i, i)] += damping.diagonal_term(s[(i, i)]);
}
for chunk in 0..eliminated_inverse.len() {
let dof = eliminated_inverse.dof(chunk);
let ete = eliminated_inverse.block_mut(chunk);
for k in 0..dof {
let pos = k * dof + k;
ete[pos] += damping.diagonal_term(ete[pos]);
}
}
}
let mut g_reduced = g_k;
for (chunk, data) in chunk_data.iter().enumerate() {
let dof = data.dof;
if dof == 0 {
continue;
}
let block = partition.eliminated_blocks()[chunk];
eliminated_inverse.invert_one(chunk, block.key)?;
let inv = eliminated_inverse.block(chunk);
let base = partition.eliminated_offset(chunk);
for r in 0..dof {
let mut acc = 0.0;
for c in 0..dof {
acc += inv[c * dof + r] * data.etr[(c, 0)];
}
eliminated_rhs[(base + r, 0)] = acc;
}
if data.cols.is_empty() {
continue;
}
let mut contrib = Mat::<f64>::zeros(dof, 1);
for (i, &col_i) in data.cols.iter().enumerate() {
for c in 0..dof {
let inv_col = &inv[c * dof..(c + 1) * dof];
let mut acc = 0.0;
for (k, &inv_k) in inv_col.iter().enumerate().take(dof) {
acc += data.etf[(k, i)] * inv_k;
}
contrib[(c, 0)] = acc;
}
let mut g_term = 0.0;
for c in 0..dof {
g_term += contrib[(c, 0)] * data.etr[(c, 0)];
}
g_reduced[(col_i, 0)] -= g_term;
for (j, &col_j) in data.cols.iter().enumerate() {
let mut acc = 0.0;
for k in 0..dof {
acc += contrib[(k, 0)] * data.etf[(k, j)];
}
s[(col_i, col_j)] -= acc;
}
}
}
Ok(ReducedSystem {
s,
g_reduced,
eliminated_inverse,
eliminated_rhs,
kept_dof,
})
}
#[allow(clippy::too_many_arguments)]
fn gather_strip(
cursors: &mut [usize],
jacobian: &SparseColMat<usize, f64>,
residuals: &Mat<f64>,
kept_global: &[usize],
row_start: usize,
row_end: usize,
candidate_cols: &[u32],
s: &mut Mat<f64>,
g_k: &mut Mat<f64>,
) -> (Vec<usize>, Mat<f64>) {
let symbolic = jacobian.symbolic();
let n_rows = row_end - row_start;
let candidates = candidate_cols;
let mut cols = Vec::with_capacity(candidates.len());
let mut spans = Vec::with_capacity(candidates.len());
for &local_u32 in candidates {
let local = local_u32 as usize;
let cursor = &mut cursors[local];
let rows = symbolic.row_idx_of_col_raw(kept_global[local]);
while *cursor < rows.len() && rows[*cursor] < row_start {
*cursor += 1;
}
let begin = *cursor;
let mut probe = begin;
while probe < rows.len() && rows[probe] < row_end {
probe += 1;
}
if probe > begin {
cols.push(local);
spans.push((begin, probe));
*cursor = probe;
}
}
let width = cols.len();
let mut strip = Mat::<f64>::zeros(n_rows, width);
for (c, (&local, &(begin, end))) in cols.iter().zip(spans.iter()).enumerate() {
let global = kept_global[local];
let rows = symbolic.row_idx_of_col_raw(global);
let vals = jacobian.val_of_col(global);
for k in begin..end {
strip[(rows[k] - row_start, c)] = vals[k];
}
}
for r in 0..n_rows {
let residual = residuals[(row_start + r, 0)];
for i in 0..width {
let vi = strip[(r, i)];
if vi == 0.0 {
continue;
}
g_k[(cols[i], 0)] += vi * residual;
for j in 0..width {
let vj = strip[(r, j)];
if vj != 0.0 {
s[(cols[i], cols[j])] += vi * vj;
}
}
}
}
(cols, strip)
}
#[allow(clippy::too_many_arguments)]
fn sweep_rows(
cursors: &mut [usize],
jacobian: &SparseColMat<usize, f64>,
residuals: &Mat<f64>,
kept_global: &[usize],
candidates: &[u32],
row_start: usize,
row_end: usize,
s: &mut Mat<f64>,
g_k: &mut Mat<f64>,
) {
let _ = Self::gather_strip(
cursors,
jacobian,
residuals,
kept_global,
row_start,
row_end,
candidates,
s,
g_k,
);
}
}
#[derive(Debug)]
struct ChunkData {
cols: Vec<usize>,
etf: Mat<f64>,
etr: Mat<f64>,
dof: usize,
}
impl ChunkData {
fn empty() -> Self {
Self {
cols: Vec::new(),
etf: Mat::zeros(0, 0),
etr: Mat::zeros(0, 0),
dof: 0,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::linalg::schur::BlockSpan;
use faer::sparse::Triplet;
use slotmap::KeyData;
type TestResult = Result<(), Box<dyn std::error::Error>>;
fn key(i: u64) -> crate::core::VarKey {
crate::core::VarKey::from(KeyData::from_ffi((1u64 << 32) | i))
}
type Fixture = (SparseColMat<usize, f64>, Mat<f64>, SchurPartition);
fn tiny_system() -> Result<Fixture, Box<dyn std::error::Error>> {
let t = vec![
Triplet::new(0usize, 0usize, 1.0f64),
Triplet::new(0, 1, 0.5),
Triplet::new(0, 4, 2.0),
Triplet::new(1, 0, 0.3),
Triplet::new(1, 2, 1.1),
Triplet::new(1, 4, 1.5),
Triplet::new(2, 2, 0.9),
Triplet::new(2, 3, 1.3),
Triplet::new(2, 5, 1.7),
Triplet::new(3, 0, 0.4),
Triplet::new(3, 3, 0.8),
Triplet::new(3, 5, 2.1),
Triplet::new(4, 0, 0.7),
Triplet::new(5, 1, 0.6),
Triplet::new(6, 2, 0.9),
Triplet::new(7, 3, 1.2),
];
let j = SparseColMat::try_new_from_triplets(8, 6, &t)?;
let r = Mat::from_fn(8, 1, |i, _| 0.2 + (i % 3) as f64 * 0.4);
let partition = SchurPartition::new(
vec![
BlockSpan {
key: key(0),
col_start: 0,
dof: 2,
},
BlockSpan {
key: key(1),
col_start: 2,
dof: 2,
},
],
vec![
BlockSpan {
key: key(2),
col_start: 4,
dof: 1,
},
BlockSpan {
key: key(3),
col_start: 5,
dof: 1,
},
],
)?;
Ok((j, r, partition))
}
fn dense_reference(
j: &SparseColMat<usize, f64>,
r: &Mat<f64>,
partition: &SchurPartition,
) -> (Vec<f64>, Vec<f64>) {
let n = j.ncols();
let m = j.nrows();
let mut dense = vec![vec![0.0f64; n]; m];
for (col, _) in (0..n).map(|c| (c, ())) {
let rows = j.symbolic().row_idx_of_col_raw(col);
let vals = j.val_of_col(col);
for (i, &row) in rows.iter().enumerate() {
dense[row][col] = vals[i];
}
}
let mut h = vec![vec![0.0f64; n]; n];
let mut g = vec![0.0f64; n];
for a in 0..n {
for b in 0..n {
let mut acc = 0.0;
for row in dense.iter() {
acc += row[a] * row[b];
}
h[a][b] = acc;
}
let mut acc = 0.0;
for (row_idx, row) in dense.iter().enumerate() {
acc += row[a] * r[(row_idx, 0)];
}
g[a] = acc;
}
let kept: Vec<usize> = partition
.kept_blocks()
.iter()
.flat_map(|b| b.col_start..b.col_start + b.dof)
.collect();
let elim: Vec<usize> = partition
.eliminated_blocks()
.iter()
.flat_map(|b| b.col_start..b.col_start + b.dof)
.collect();
let k = kept.len();
let mut s = vec![0.0f64; k * k];
let mut g_red = vec![0.0f64; k];
for (li, &ci) in kept.iter().enumerate() {
g_red[li] = g[ci];
for (lj, &cj) in kept.iter().enumerate() {
s[li * k + lj] = h[ci][cj];
}
}
for &e in &elim {
let inv = 1.0 / h[e][e];
for (li, &ci) in kept.iter().enumerate() {
g_red[li] -= h[ci][e] * inv * g[e];
for (lj, &cj) in kept.iter().enumerate() {
s[li * k + lj] -= h[ci][e] * inv * h[e][cj];
}
}
}
(s, g_red)
}
#[test]
fn chunked_elimination_matches_dense_reference() -> TestResult {
let (j, r, partition) = tiny_system()?;
let (want_s, want_g) = dense_reference(&j, &r, &partition);
let mut elim = ChunkedSchurEliminator::new(&j, &partition)?;
let got = elim.eliminate(&j, &r, &partition, None)?;
assert_eq!(got.kept_dof, 4);
for i in 0..4 {
assert!(
(got.g_reduced[(i, 0)] - want_g[i]).abs() < 1e-10,
"g_reduced[{i}]: got {}, want {}",
got.g_reduced[(i, 0)],
want_g[i]
);
for k in 0..4 {
assert!(
(got.s[(i, k)] - want_s[i * 4 + k]).abs() < 1e-10,
"S[{i},{k}]: got {}, want {}",
got.s[(i, k)],
want_s[i * 4 + k]
);
}
}
Ok(())
}
#[test]
fn chunked_elimination_handles_zero_dof_kept_block() -> TestResult {
let (j, r, _) = tiny_system()?;
let partition = SchurPartition::new(
vec![
BlockSpan {
key: key(4),
col_start: 0,
dof: 0,
},
BlockSpan {
key: key(0),
col_start: 0,
dof: 2,
},
BlockSpan {
key: key(1),
col_start: 2,
dof: 2,
},
],
vec![
BlockSpan {
key: key(2),
col_start: 4,
dof: 1,
},
BlockSpan {
key: key(3),
col_start: 5,
dof: 1,
},
],
)?;
let (want_s, want_g) = dense_reference(&j, &r, &partition);
let mut elim = ChunkedSchurEliminator::new(&j, &partition)?;
let got = elim.eliminate(&j, &r, &partition, None)?;
assert_eq!(got.kept_dof, 4);
for i in 0..4 {
assert!(
(got.g_reduced[(i, 0)] - want_g[i]).abs() < 1e-10,
"g_reduced[{i}]: got {}, want {}",
got.g_reduced[(i, 0)],
want_g[i]
);
for k in 0..4 {
assert!(
(got.s[(i, k)] - want_s[i * 4 + k]).abs() < 1e-10,
"S[{i},{k}]: got {}, want {}",
got.s[(i, k)],
want_s[i * 4 + k]
);
}
}
Ok(())
}
#[test]
fn chunked_elimination_handles_mixed_zero_dof_row_in_chunk() -> TestResult {
let t = vec![
Triplet::new(0usize, 3usize, 2.0f64),
Triplet::new(1, 0, 1.0),
Triplet::new(1, 1, 0.5),
Triplet::new(1, 2, 0.3),
Triplet::new(1, 3, 1.5),
];
let j = SparseColMat::try_new_from_triplets(2, 4, &t)?;
let r = Mat::from_fn(2, 1, |i, _| 0.3 + i as f64 * 0.2);
let partition = SchurPartition::new(
vec![
BlockSpan {
key: key(4),
col_start: 0,
dof: 0,
},
BlockSpan {
key: key(0),
col_start: 0,
dof: 3,
},
],
vec![BlockSpan {
key: key(2),
col_start: 3,
dof: 1,
}],
)?;
let (want_s, want_g) = dense_reference(&j, &r, &partition);
let mut elim = ChunkedSchurEliminator::new(&j, &partition)?;
let got = elim.eliminate(&j, &r, &partition, None)?;
assert_eq!(got.kept_dof, 3);
for i in 0..3 {
assert!(
(got.g_reduced[(i, 0)] - want_g[i]).abs() < 1e-10,
"g_reduced[{i}]: got {}, want {}",
got.g_reduced[(i, 0)],
want_g[i]
);
for k in 0..3 {
assert!(
(got.s[(i, k)] - want_s[i * 3 + k]).abs() < 1e-10,
"S[{i},{k}]: got {}, want {}",
got.s[(i, k)],
want_s[i * 3 + k]
);
}
}
Ok(())
}
#[test]
fn chunked_elimination_computes_delta_e_for_chunk_with_no_kept_coupling() -> TestResult {
let t = vec![Triplet::new(0usize, 0usize, 2.0f64)];
let j = SparseColMat::try_new_from_triplets(1, 1, &t)?;
let r = Mat::from_fn(1, 1, |_, _| 0.7);
let partition = SchurPartition::new(
vec![BlockSpan {
key: key(4),
col_start: 0,
dof: 0,
}],
vec![BlockSpan {
key: key(2),
col_start: 0,
dof: 1,
}],
)?;
let mut elim = ChunkedSchurEliminator::new(&j, &partition)?;
let got = elim.eliminate(&j, &r, &partition, None)?;
assert!(
(got.eliminated_rhs[(0, 0)] - 0.35).abs() < 1e-10,
"eliminated_rhs must be H_ee⁻¹·Eᵀr even with no kept coupling, got {}",
got.eliminated_rhs[(0, 0)]
);
Ok(())
}
#[test]
fn damping_reaches_both_sides() -> TestResult {
let (j, r, partition) = tiny_system()?;
let mut elim = ChunkedSchurEliminator::new(&j, &partition)?;
let plain = elim.eliminate(&j, &r, &partition, None)?;
let damping = Damping::new(0.5, 1e-6, 1e32)?;
let damped = elim.eliminate(&j, &r, &partition, Some(&damping))?;
for i in 0..plain.kept_dof {
assert!(
damped.s[(i, i)] > plain.s[(i, i)],
"damping must raise S's diagonal at {i}"
);
}
assert!(
(damped.s[(0, 1)] - plain.s[(0, 1)]).abs() > 1e-12,
"damping H_ee must change the Schur complement off-diagonal"
);
Ok(())
}
#[test]
fn interleaved_chunks_are_rejected() -> TestResult {
let t = vec![
Triplet::new(0usize, 0usize, 1.0f64),
Triplet::new(0, 1, 2.0),
Triplet::new(1, 0, 1.0),
Triplet::new(1, 2, 1.5),
Triplet::new(2, 0, 1.0),
Triplet::new(2, 1, 1.1),
];
let j = SparseColMat::try_new_from_triplets(3, 3, &t)?;
let partition = SchurPartition::new(
vec![BlockSpan {
key: key(0),
col_start: 0,
dof: 1,
}],
vec![
BlockSpan {
key: key(1),
col_start: 1,
dof: 1,
},
BlockSpan {
key: key(2),
col_start: 2,
dof: 1,
},
],
)?;
let Err(err) = ChunkedSchurEliminator::new(&j, &partition) else {
panic!("interleaved chunk rows must be rejected");
};
assert!(err.to_string().contains("contiguous"), "{err}");
Ok(())
}
#[test]
fn layout_matches_rejects_pattern_change_at_equal_row_count() -> TestResult {
let (j, _r, partition) = tiny_system()?;
let layout = ChunkLayout::build(&j, &partition)?;
assert!(layout.matches(&j));
let t = vec![Triplet::new(0usize, 0usize, 1.0f64)];
let other = SparseColMat::try_new_from_triplets(8, 6, &t)?;
assert!(
!layout.matches(&other),
"a pattern change at equal row count must not match"
);
let moved = vec![
Triplet::new(0usize, 0usize, 1.0f64),
Triplet::new(0, 1, 0.5),
Triplet::new(0, 4, 2.0),
Triplet::new(1, 0, 0.3),
Triplet::new(1, 2, 1.1),
Triplet::new(1, 4, 1.5),
Triplet::new(2, 0, 0.4),
Triplet::new(2, 2, 0.9),
Triplet::new(2, 3, 1.3),
Triplet::new(2, 5, 1.7),
Triplet::new(3, 3, 0.8),
Triplet::new(3, 5, 2.1),
Triplet::new(4, 0, 0.7),
Triplet::new(5, 1, 0.6),
Triplet::new(6, 2, 0.9),
Triplet::new(7, 3, 1.2),
];
let permuted = SparseColMat::try_new_from_triplets(8, 6, &moved)?;
assert_eq!(
permuted.as_ref().compute_nnz(),
j.as_ref().compute_nnz(),
"relocation must preserve nnz for the regression to be meaningful"
);
assert!(
!layout.matches(&permuted),
"an equal-nnz permutation must invalidate the layout"
);
Ok(())
}
#[test]
fn gap_between_chunks_is_rejected() -> TestResult {
let t = vec![
Triplet::new(0usize, 0usize, 1.0f64),
Triplet::new(0, 1, 2.0),
Triplet::new(0, 2, 1.5),
Triplet::new(1, 0, 1.0),
Triplet::new(1, 2, 1.1),
Triplet::new(2, 0, 0.8),
Triplet::new(3, 1, 0.9),
Triplet::new(4, 0, 1.2),
Triplet::new(4, 3, 1.4),
Triplet::new(5, 0, 1.0),
Triplet::new(5, 3, 1.3),
];
let j = SparseColMat::try_new_from_triplets(6, 4, &t)?;
let partition = SchurPartition::new(
vec![
BlockSpan {
key: key(0),
col_start: 0,
dof: 1,
},
BlockSpan {
key: key(1),
col_start: 1,
dof: 1,
},
],
vec![
BlockSpan {
key: key(2),
col_start: 2,
dof: 1,
},
BlockSpan {
key: key(3),
col_start: 3,
dof: 1,
},
],
)?;
let Err(err) = ChunkedSchurEliminator::new(&j, &partition) else {
panic!("a gap between chunk row ranges must be rejected");
};
assert!(err.to_string().contains("gap"), "{err}");
Ok(())
}
#[test]
fn unobserved_eliminated_variable_is_reported() -> TestResult {
let t = vec![
Triplet::new(0usize, 0usize, 1.0f64),
Triplet::new(1, 0, 0.5),
];
let j = SparseColMat::try_new_from_triplets(2, 2, &t)?;
let partition = SchurPartition::new(
vec![BlockSpan {
key: key(0),
col_start: 0,
dof: 1,
}],
vec![BlockSpan {
key: key(1),
col_start: 1,
dof: 1,
}],
)?;
let Err(err) = ChunkedSchurEliminator::new(&j, &partition) else {
panic!("an unobserved eliminated variable must be reported");
};
assert!(err.to_string().contains("no residual"), "{err}");
Ok(())
}
}