use super::types::{flip, EMPTY};
#[allow(clippy::too_many_arguments)]
fn dfs(
j0: usize,
k: i64,
pinv: &[i64],
l_cols: &[Vec<(usize, f64)>],
stack: &mut [usize],
flag: &mut [i64],
lpend: &[i64],
mut top: usize,
lik: &mut Vec<usize>,
ap_pos: &mut [i64],
) -> usize {
let mut head: i64 = 0;
stack[0] = j0;
debug_assert_ne!(flag[j0], k);
while head >= 0 {
let hd = head as usize;
let j = stack[hd];
let jnew = pinv[j] as usize;
if flag[j] != k {
flag[j] = k;
ap_pos[hd] = if lpend[jnew] == EMPTY { l_cols[jnew].len() as i64 } else { lpend[jnew] };
}
ap_pos[hd] -= 1;
let mut pos = ap_pos[hd];
let mut pushed_child = false;
while pos >= 0 {
let i = l_cols[jnew][pos as usize].0;
if flag[i] != k {
if pinv[i] >= 0 {
ap_pos[hd] = pos;
head += 1;
stack[head as usize] = i;
pushed_child = true;
break;
} else {
flag[i] = k;
lik.push(i);
}
}
pos -= 1;
}
if !pushed_child {
head -= 1;
top -= 1;
stack[top] = j;
}
}
top
}
#[allow(clippy::too_many_arguments)]
fn lsolve_symbolic(
n: usize,
k: usize,
col_entries: &[usize],
pinv: &[i64],
l_cols: &[Vec<(usize, f64)>],
stack: &mut [usize],
flag: &mut [i64],
lpend: &[i64],
ap_pos: &mut [i64],
) -> (usize, Vec<usize>) {
let mut top = n;
let mut lik: Vec<usize> = Vec::new();
let kk = k as i64;
for &i in col_entries {
if flag[i] != kk {
if pinv[i] >= 0 {
top = dfs(i, kk, pinv, l_cols, stack, flag, lpend, top, &mut lik, ap_pos);
} else {
flag[i] = kk;
lik.push(i);
}
}
}
(top, lik)
}
fn construct_column(x: &mut [f64], col_entries: &[(i64, f64)], off_diagonal: &mut Vec<(i64, f64)>) {
for &(row_or_off, value) in col_entries {
if row_or_off < 0 {
off_diagonal.push((flip(row_or_off), value)); } else {
x[row_or_off as usize] = value;
}
}
}
fn lsolve_numeric(pinv: &[i64], l_cols: &[Vec<(usize, f64)>], stack: &[usize], top: usize, x: &mut [f64]) {
for &j in &stack[top..] {
let jnew = pinv[j] as usize;
let xj = x[j];
for &(row, lij) in &l_cols[jnew] {
x[row] -= lij * xj;
}
}
}
type LpivotResult = (i64, f64, f64, Vec<(usize, f64)>);
fn lpivot(diagrow: i64, tol: f64, x: &mut [f64], lik: Vec<usize>) -> Option<LpivotResult> {
if lik.is_empty() {
return None;
}
let mut l_values: Vec<(usize, f64)> = lik.iter().map(|&i| (i, x[i])).collect();
for &(row, _) in &l_values {
x[row] = 0.0;
}
let last_idx = l_values.len() - 1;
let last_row_index = l_values[last_idx].0;
let candidates = &l_values[..last_idx];
let mut pdiag: Option<usize> = None; let mut ppivrow: Option<usize> = None;
let mut abs_pivot = -1.0f64;
for (p, &(row, value)) in candidates.iter().enumerate() {
let xabs = value.abs();
if row as i64 == diagrow {
pdiag = Some(p);
}
if xabs > abs_pivot {
abs_pivot = xabs;
ppivrow = Some(p);
}
}
let last_value = l_values[last_idx].1;
let mut last_xabs = last_value.abs();
if last_xabs > abs_pivot {
abs_pivot = last_xabs;
ppivrow = None; }
if last_row_index as i64 == diagrow {
if last_xabs >= tol * abs_pivot {
abs_pivot = last_xabs;
ppivrow = None;
}
} else if let Some(pd) = pdiag {
let dabs = candidates[pd].1.abs();
if dabs >= tol * abs_pivot {
abs_pivot = dabs;
ppivrow = Some(pd);
}
}
let (pivrow, pivot): (usize, f64) = if let Some(pp) = ppivrow {
let (prow, pval) = candidates[pp];
l_values[pp] = (last_row_index, last_value);
(prow, pval)
} else {
(last_row_index, last_value)
};
let _ = &mut last_xabs; l_values.truncate(last_idx);
if pivot == 0.0 {
return None;
}
for v in l_values.iter_mut() {
v.1 /= pivot;
}
Some((pivrow as i64, pivot, abs_pivot, l_values))
}
fn prune(lpend: &mut [i64], pinv: &[i64], u_col_k: &[(usize, f64)], pivrow: usize, l_cols: &mut [Vec<(usize, f64)>]) {
for &(j, _) in u_col_k {
if lpend[j] == EMPTY {
let lj = &mut l_cols[j];
if let Some(found_pos) = lj.iter().position(|&(row, _)| row == pivrow) {
let _ = found_pos;
let mut phead = 0usize;
let mut ptail = lj.len();
while phead < ptail {
let (i, _) = lj[phead];
if pinv[i] >= 0 {
phead += 1;
} else {
ptail -= 1;
lj.swap(phead, ptail);
}
}
lpend[j] = ptail as i64;
}
}
}
}
#[derive(Clone)]
pub struct BlockFactor {
pub l_cols: Vec<Vec<(usize, f64)>>,
pub u_cols: Vec<Vec<(usize, f64)>>,
pub udiag: Vec<f64>,
pub p: Vec<i64>,
}
pub fn factor_block(
nk: usize,
tol: f64,
mut column_entries: impl FnMut(usize) -> Vec<(i64, f64)>,
off_diagonal_out: &mut [Vec<(i64, f64)>], ) -> Option<BlockFactor> {
let mut l_cols: Vec<Vec<(usize, f64)>> = Vec::with_capacity(nk);
let mut u_cols: Vec<Vec<(usize, f64)>> = Vec::with_capacity(nk);
let mut udiag = vec![0.0f64; nk];
let mut x = vec![0.0f64; nk];
let mut flag = vec![EMPTY; nk];
let mut lpend = vec![EMPTY; nk];
let mut stack = vec![0usize; nk];
let mut ap_pos = vec![0i64; nk];
let mut p: Vec<i64> = (0..nk as i64).collect();
let mut pinv: Vec<i64> = (0..nk as i64).map(flip).collect();
for k in 0..nk {
let raw_entries = column_entries(k);
let col_rows: Vec<usize> = raw_entries.iter().filter(|&&(r, _)| r >= 0).map(|&(r, _)| r as usize).collect();
let (top, lik) =
lsolve_symbolic(nk, k, &col_rows, &pinv, &l_cols, &mut stack, &mut flag, &lpend, &mut ap_pos);
let mut off_diag = std::mem::take(&mut off_diagonal_out[k]);
construct_column(&mut x, &raw_entries, &mut off_diag);
off_diagonal_out[k] = off_diag;
lsolve_numeric(&pinv, &l_cols, &stack, top, &mut x);
let diagrow = p[k]; let (pivrow, pivot, _abs_pivot, l_col_k) = lpivot(diagrow, tol, &mut x, lik)?;
let mut u_col_k: Vec<(usize, f64)> = Vec::with_capacity(nk - top);
for &j in &stack[top..] {
let jnew = pinv[j] as usize;
u_col_k.push((jnew, x[j]));
x[j] = 0.0;
}
udiag[k] = pivot;
if pivrow != diagrow && pinv[diagrow as usize] < 0 {
let kbar = flip(pinv[pivrow as usize]);
p[kbar as usize] = diagrow;
pinv[diagrow as usize] = flip(kbar);
}
p[k] = pivrow;
pinv[pivrow as usize] = k as i64;
prune(&mut lpend, &pinv, &u_col_k, pivrow as usize, &mut l_cols);
l_cols.push(l_col_k);
u_cols.push(u_col_k);
}
for col in l_cols.iter_mut() {
for entry in col.iter_mut() {
entry.0 = pinv[entry.0] as usize;
}
}
Some(BlockFactor { l_cols, u_cols, udiag, p })
}
pub fn refactor_block_in_place(
nk: usize,
block: &mut BlockFactor,
x: &mut [f64],
columns: &[Vec<(i64, f64)>],
off_diagonal_out: &mut [Vec<(i64, f64)>],
) -> bool {
let BlockFactor { l_cols, u_cols, udiag, .. } = block;
for k in 0..nk {
let mut off_diag = std::mem::take(&mut off_diagonal_out[k]);
construct_column(x, &columns[k], &mut off_diag);
off_diagonal_out[k] = off_diag;
for entry in u_cols[k].iter_mut() {
let j = entry.0;
let ujk = x[j];
x[j] = 0.0;
entry.1 = ujk;
for &(row, lij) in l_cols[j].iter() {
x[row] -= lij * ujk;
}
}
let ukk = x[k];
x[k] = 0.0;
if ukk == 0.0 {
return false;
}
udiag[k] = ukk;
for entry in l_cols[k].iter_mut() {
let row = entry.0;
entry.1 = x[row] / ukk;
x[row] = 0.0;
}
}
true
}
#[cfg(test)]
mod tests {
use super::*;
fn dense_solve(a: &[Vec<f64>], b: &[f64]) -> Vec<f64> {
let n = a.len();
let mut a: Vec<Vec<f64>> = a.to_vec();
let mut rhs = b.to_vec();
for col in 0..n {
let pivot_row = (col..n).max_by(|&r1, &r2| a[r1][col].abs().total_cmp(&a[r2][col].abs())).unwrap();
a.swap(col, pivot_row);
rhs.swap(col, pivot_row);
for row in (col + 1)..n {
let factor = a[row][col] / a[col][col];
#[allow(clippy::needless_range_loop)] for k in col..n {
a[row][k] -= factor * a[col][k];
}
rhs[row] -= factor * rhs[col];
}
}
let mut x = vec![0.0; n];
for row in (0..n).rev() {
let mut sum = rhs[row];
for k in (row + 1)..n {
sum -= a[row][k] * x[k];
}
x[row] = sum / a[row][row];
}
x
}
fn solve_with_factor(bf: &BlockFactor, b: &[f64]) -> Vec<f64> {
let n = bf.p.len();
let mut y = vec![0.0; n];
for k in 0..n {
y[k] = b[bf.p[k] as usize];
}
for k in 0..n {
let yk = y[k];
for &(row, lij) in &bf.l_cols[k] {
y[row] -= lij * yk;
}
}
let mut x = y;
for k in (0..n).rev() {
x[k] /= bf.udiag[k];
let xk = x[k];
for &(row, uij) in &bf.u_cols[k] {
x[row] -= uij * xk;
}
}
x
}
fn dense_from_entries(n: usize, entries: &[(usize, usize, f64)]) -> Vec<Vec<f64>> {
let mut a = vec![vec![0.0; n]; n];
for &(r, c, v) in entries {
a[r][c] += v;
}
a
}
fn factor_dense(n: usize, entries: &[(usize, usize, f64)]) -> BlockFactor {
let mut by_col: Vec<Vec<(i64, f64)>> = vec![Vec::new(); n];
for &(r, c, v) in entries {
by_col[c].push((r as i64, v));
}
let mut off = vec![Vec::new(); n];
factor_block(n, 1e-3, |k| by_col[k].clone(), &mut off).expect("should not be singular")
}
#[test]
fn solve_diagonal_matches_dense() {
let n = 3;
let entries = vec![(0, 0, 4.0), (1, 1, 5.0), (2, 2, 6.0)];
let bf = factor_dense(n, &entries);
let a = dense_from_entries(n, &entries);
let b = vec![8.0, 10.0, 12.0];
let x = solve_with_factor(&bf, &b);
let expected = dense_solve(&a, &b);
for i in 0..n {
assert!((x[i] - expected[i]).abs() < 1e-10, "index {i}: {} vs {}", x[i], expected[i]);
}
}
#[test]
fn solve_needs_pivoting() {
let n = 2;
let entries = vec![(0, 1, 2.0), (1, 0, 3.0), (1, 1, 1.0)];
let bf = factor_dense(n, &entries);
let a = dense_from_entries(n, &entries);
let b = vec![4.0, 5.0];
let x = solve_with_factor(&bf, &b);
let expected = dense_solve(&a, &b);
for i in 0..n {
assert!((x[i] - expected[i]).abs() < 1e-10, "index {i}: {} vs {}", x[i], expected[i]);
}
}
#[test]
fn solve_chain_matches_dense() {
let n = 6;
let mut entries = Vec::new();
for i in 0..n {
entries.push((i, i, 3.0 + i as f64 * 0.3));
}
for i in 0..n - 1 {
entries.push((i, i + 1, -0.8));
entries.push((i + 1, i, -0.7));
}
let bf = factor_dense(n, &entries);
let a = dense_from_entries(n, &entries);
let b: Vec<f64> = (0..n).map(|i| 1.0 + i as f64).collect();
let x = solve_with_factor(&bf, &b);
let expected = dense_solve(&a, &b);
for i in 0..n {
assert!((x[i] - expected[i]).abs() < 1e-8, "index {i}: {} vs {}", x[i], expected[i]);
}
}
#[test]
fn solve_asymmetric_matches_dense() {
let n = 4;
let entries = vec![
(0, 0, 6.0),
(1, 1, 7.0),
(2, 2, 8.0),
(3, 3, 5.0),
(0, 1, -0.9),
(1, 0, -0.5),
(1, 2, -0.6),
(2, 1, -0.3),
(2, 3, 0.4),
(3, 2, -0.2),
(0, 3, 0.1),
(3, 0, 0.15),
];
let bf = factor_dense(n, &entries);
let a = dense_from_entries(n, &entries);
let b = vec![1.0, 2.0, -1.0, 0.5];
let x = solve_with_factor(&bf, &b);
let expected = dense_solve(&a, &b);
for i in 0..n {
assert!((x[i] - expected[i]).abs() < 1e-8, "index {i}: {} vs {}", x[i], expected[i]);
}
}
#[test]
fn structurally_singular_returns_none() {
let n = 2;
let entries = vec![(0, 1, 1.0), (1, 1, 1.0)]; let mut by_col: Vec<Vec<(i64, f64)>> = vec![Vec::new(); n];
for &(r, c, v) in &entries {
by_col[c].push((r as i64, v));
}
let mut off = vec![Vec::new(); n];
assert!(factor_block(n, 1e-3, |k| by_col[k].clone(), &mut off).is_none());
}
fn refactor_dense(n: usize, prev: &BlockFactor, entries: &[(usize, usize, f64)]) -> BlockFactor {
let mut by_col: Vec<Vec<(i64, f64)>> = vec![Vec::new(); n];
for &(r, c, v) in entries {
by_col[c].push((r as i64, v));
}
let mut off = vec![Vec::new(); n];
let mut x = vec![0.0; n];
let mut block = prev.clone();
assert!(refactor_block_in_place(n, &mut block, &mut x, &by_col, &mut off), "should not be singular");
block
}
#[test]
fn refactor_with_identical_values_matches_original() {
let n = 4;
let entries = vec![
(0, 0, 6.0),
(1, 1, 7.0),
(2, 2, 8.0),
(3, 3, 5.0),
(0, 1, -0.9),
(1, 0, -0.5),
(1, 2, -0.6),
(2, 1, -0.3),
(2, 3, 0.4),
(3, 2, -0.2),
(0, 3, 0.1),
(3, 0, 0.15),
];
let bf = factor_dense(n, &entries);
let refactored = refactor_dense(n, &bf, &entries);
assert_eq!(refactored.p, bf.p);
for k in 0..n {
assert_eq!(refactored.l_cols[k], bf.l_cols[k], "L column {k}");
assert_eq!(refactored.u_cols[k], bf.u_cols[k], "U column {k}");
}
assert_eq!(refactored.udiag, bf.udiag);
}
#[test]
fn refactor_with_new_values_matches_independent_dense_solve() {
let n = 4;
let original = vec![
(0, 0, 6.0),
(1, 1, 7.0),
(2, 2, 8.0),
(3, 3, 5.0),
(0, 1, -0.9),
(1, 0, -0.5),
(1, 2, -0.6),
(2, 1, -0.3),
(2, 3, 0.4),
(3, 2, -0.2),
(0, 3, 0.1),
(3, 0, 0.15),
];
let bf = factor_dense(n, &original);
let updated = vec![
(0, 0, 9.0),
(1, 1, 4.0),
(2, 2, 6.5),
(3, 3, 3.0),
(0, 1, -1.2),
(1, 0, -0.2),
(1, 2, -0.4),
(2, 1, -0.1),
(2, 3, 0.9),
(3, 2, -0.3),
(0, 3, 0.05),
(3, 0, 0.4),
];
let refactored = refactor_dense(n, &bf, &updated);
let a = dense_from_entries(n, &updated);
let b = vec![1.0, 2.0, -1.0, 0.5];
let x = solve_with_factor(&refactored, &b);
let expected = dense_solve(&a, &b);
for i in 0..n {
assert!((x[i] - expected[i]).abs() < 1e-8, "index {i}: {} vs {}", x[i], expected[i]);
}
}
#[test]
fn refactor_detects_new_singularity() {
let n = 2;
let entries = vec![(0, 0, 4.0), (0, 1, 2.0), (1, 0, 3.0), (1, 1, 1.0)];
let bf = factor_dense(n, &entries);
let singular = vec![(0, 0, 0.0), (0, 1, 2.0), (1, 0, 3.0), (1, 1, 0.0)];
let mut by_col: Vec<Vec<(i64, f64)>> = vec![Vec::new(); n];
for &(r, c, v) in &singular {
by_col[c].push((r as i64, v));
}
let mut off = vec![Vec::new(); n];
let mut x = vec![0.0; n];
let mut block = bf.clone();
assert!(!refactor_block_in_place(n, &mut block, &mut x, &by_col, &mut off));
}
#[cfg(feature = "klu")]
#[test]
fn matches_real_klu_on_random_matrices() {
let mut seed: u64 = 0xA24BAED4963EE407;
let mut next_f64 = move || {
seed ^= seed << 13;
seed ^= seed >> 7;
seed ^= seed << 17;
(seed >> 11) as f64 / (1u64 << 53) as f64
};
for trial in 0..40 {
let n = 3 + (trial % 12);
let mut entries: Vec<(usize, usize, f64)> = Vec::new();
let mut row_sums = vec![0.0f64; n];
let mut off_entries: Vec<(usize, usize, f64)> = Vec::new();
#[allow(clippy::needless_range_loop)] for i in 0..n {
let mut js: std::collections::BTreeSet<usize> = Default::default();
let degree = 1 + (next_f64() * 3.0) as usize;
for _ in 0..degree {
let j = (next_f64() * n as f64) as usize;
if j != i {
js.insert(j);
}
}
for j in js {
let v = next_f64() * 2.0 - 1.0;
off_entries.push((i, j, v));
row_sums[i] += v.abs();
}
}
for (i, &rs) in row_sums.iter().enumerate() {
entries.push((i, i, rs + 1.0 + next_f64()));
}
entries.extend(off_entries);
let mut by_col: Vec<Vec<(i64, f64)>> = vec![Vec::new(); n];
for &(r, c, v) in &entries {
by_col[c].push((r as i64, v));
}
let mut off = vec![Vec::new(); n];
let bf = factor_block(n, 1e-3, |k| by_col[k].clone(), &mut off)
.unwrap_or_else(|| panic!("trial {trial} (n={n}): unexpectedly singular"));
let b: Vec<f64> = (0..n).map(|i| 1.0 + i as f64 * 0.3).collect();
let rust_x = solve_with_factor(&bf, &b);
let mut klu_sys = crate::sparse_klu::KluRealSystem::new(n, &entries).unwrap();
let klu_x = klu_sys.factor_and_solve(&entries, &b).unwrap();
for i in 0..n {
assert!(
(rust_x[i] - klu_x[i]).abs() < 1e-8,
"trial {trial} (n={n}), index {i}: rust={} klu={}",
rust_x[i],
klu_x[i]
);
}
}
}
}