pub mod types;
mod btf;
mod amd;
mod analyze;
mod kernel;
mod factor;
mod scale;
mod refactor;
mod solve;
#[cfg(all(test, feature = "klu"))]
mod ffi_oracle;
fn build_csc_structure(n: usize, pairs: &[(usize, usize)]) -> (Vec<i64>, Vec<i64>, Vec<Vec<usize>>) {
let mut order: Vec<usize> = (0..pairs.len()).collect();
order.sort_by_key(|&i| (pairs[i].1, pairs[i].0));
let mut col_ptr = vec![0i64; n + 1];
let mut row_idx: Vec<i64> = Vec::new();
let mut groups: Vec<Vec<usize>> = Vec::new();
let mut idx = 0;
while idx < order.len() {
let (row, col) = pairs[order[idx]];
let mut group = vec![order[idx]];
let mut k = idx + 1;
while k < order.len() && pairs[order[k]] == (row, col) {
group.push(order[k]);
k += 1;
}
row_idx.push(row as i64);
groups.push(group);
col_ptr[col + 1] += 1;
idx = k;
}
for c in 0..n {
col_ptr[c + 1] += col_ptr[c];
}
(col_ptr, row_idx, groups)
}
fn pack_values(entries: &[(usize, usize, f64)], groups: &[Vec<usize>]) -> Vec<f64> {
groups.iter().map(|g| g.iter().map(|&i| entries[i].2).sum()).collect()
}
fn pack_values_slice(values: &[f64], groups: &[Vec<usize>]) -> Vec<f64> {
groups.iter().map(|g| g.iter().map(|&i| values[i]).sum()).collect()
}
pub struct KluNativeSystem {
n: usize,
col_ptr: Vec<i64>,
row_idx: Vec<i64>,
groups: Vec<Vec<usize>>,
sym: analyze::Symbolic,
num: factor::Numeric,
scratch: refactor::RefactorScratch,
}
impl KluNativeSystem {
pub fn new(n: usize, entries: &[(usize, usize, f64)]) -> Option<Self> {
let pairs: Vec<(usize, usize)> = entries.iter().map(|&(r, c, _)| (r, c)).collect();
let (col_ptr, row_idx, groups) = build_csc_structure(n, &pairs);
let values = pack_values(entries, &groups);
let sym = analyze::analyze(n, &col_ptr, &row_idx);
let num = factor::factor(n, &col_ptr, &row_idx, &values, &sym, types::Options::default().tol)?;
let scratch = refactor::RefactorScratch::new(n);
Some(Self { n, col_ptr, row_idx, groups, sym, num, scratch })
}
pub fn factor_and_solve(&mut self, entries: &[(usize, usize, f64)], rhs: &[f64]) -> Option<Vec<f64>> {
let values = pack_values(entries, &self.groups);
self.solve_packed(values, rhs)
}
fn solve_packed(&mut self, values: Vec<f64>, rhs: &[f64]) -> Option<Vec<f64>> {
let ok = refactor::refactor(self.n, &self.col_ptr, &self.row_idx, &values, &self.sym, &mut self.num, &mut self.scratch);
if !ok {
return None;
}
let x = solve::solve(&self.sym, &self.num, None, rhs);
if x.iter().any(|v| !v.is_finite()) {
return None;
}
Some(x)
}
}
impl crate::solver::LinearSolver for KluNativeSystem {
fn new(n: usize, entries: &[(usize, usize, f64)]) -> Option<Self> {
KluNativeSystem::new(n, entries)
}
fn factor_and_solve(&mut self, entries: &[(usize, usize, f64)], rhs: &[f64]) -> Option<Vec<f64>> {
KluNativeSystem::factor_and_solve(self, entries, rhs)
}
fn factor_and_solve_values(&mut self, values: &[f64], rhs: &[f64]) -> Option<Vec<f64>> {
self.solve_packed(pack_values_slice(values, &self.groups), rhs)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn real_sparse_system_refactor_reuses_symbolic() {
let entries_a = vec![(0, 0, 2.0), (0, 1, 1.0), (1, 0, 1.0), (1, 1, 3.0)];
let mut sys = KluNativeSystem::new(2, &entries_a).unwrap();
let x = sys.factor_and_solve(&entries_a, &[5.0, 10.0]).unwrap();
assert!((x[0] - 1.0).abs() < 1e-10);
assert!((x[1] - 3.0).abs() < 1e-10);
let entries_b = vec![(0, 0, 4.0), (0, 1, 1.0), (1, 0, 1.0), (1, 1, 2.0)];
let x2 = sys.factor_and_solve(&entries_b, &[6.0, 5.0]).unwrap();
assert!((x2[0] - 1.0).abs() < 1e-10);
assert!((x2[1] - 2.0).abs() < 1e-10);
}
#[test]
fn singular_matrix_returns_none() {
let entries = vec![(0, 0, 1.0), (0, 1, 1.0), (1, 0, 2.0), (1, 1, 2.0)];
assert!(KluNativeSystem::new(2, &entries).is_none());
}
#[cfg(feature = "klu")]
#[test]
fn matches_real_klu_end_to_end() {
let mut seed: u64 = 0x9F1D4A2E7C6B0835;
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 = 4 + (trial % 14);
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 native = KluNativeSystem::new(n, &entries)
.unwrap_or_else(|| panic!("trial {trial} (n={n}): unexpectedly singular"));
let mut real = crate::sparse_klu::KluRealSystem::new(n, &entries).unwrap();
for pass in 0..3 {
let b: Vec<f64> = (0..n).map(|i| 1.0 + i as f64 * 0.3 + pass as f64).collect();
let native_x = native.factor_and_solve(&entries, &b).unwrap();
let real_x = real.factor_and_solve(&entries, &b).unwrap();
for i in 0..n {
assert!(
(native_x[i] - real_x[i]).abs() < 1e-8,
"trial {trial} pass {pass} (n={n}), index {i}: native={} real={}",
native_x[i],
real_x[i]
);
}
}
}
}
}