use crate::sparse::SparseError;
use crate::sparse::csc::CscPattern;
use crate::sparse::csc::CscView;
use crate::sparse::lu::{LuFactors, SparseScalar};
use crate::sparse::supernodal::{SupernodalLu, SymbolicSupernodal};
use crate::sparse::symbolic::SymbolicLu;
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum LuMethod {
Simplicial,
Supernodal,
Auto {
flop_ratio_threshold: f64,
},
}
impl LuMethod {
#[inline]
pub fn auto() -> Self {
Self::Auto { flop_ratio_threshold: 4.0 }
}
}
impl Default for LuMethod {
fn default() -> Self {
Self::Simplicial
}
}
#[derive(Clone, Debug)]
pub struct LuSymbolic {
n: usize,
nnz: usize,
requested: LuMethod,
supernodal: bool,
flop_ratio: f64,
simplicial: SymbolicLu,
supernodal_symbolic: Option<SymbolicSupernodal>,
}
impl LuSymbolic {
pub fn analyze(pattern: CscPattern<'_>, method: LuMethod) -> Result<Self, SparseError> {
let simplicial = SymbolicLu::analyze(pattern)?;
let supernodal_symbolic = SymbolicSupernodal::analyze_with_permutation(pattern, simplicial.col_perm(), simplicial.col_perm_inv())?;
let counts = supernodal_symbolic.col_counts();
let squares: f64 = counts.iter().map(|&c| (c as f64) * (c as f64)).sum();
let flop_ratio = if pattern.nnz() == 0 { 0.0 } else { squares / pattern.nnz() as f64 };
let supernodal = match method {
LuMethod::Simplicial => false,
LuMethod::Supernodal => true,
LuMethod::Auto { flop_ratio_threshold } => flop_ratio > flop_ratio_threshold,
};
Ok(Self {
n: pattern.ncols(),
nnz: pattern.nnz(),
requested: method,
supernodal,
flop_ratio,
simplicial,
supernodal_symbolic: supernodal.then_some(supernodal_symbolic),
})
}
#[inline]
pub fn requested(&self) -> LuMethod {
self.requested
}
#[inline]
pub fn is_supernodal(&self) -> bool {
self.supernodal
}
#[inline]
pub fn flop_ratio(&self) -> f64 {
self.flop_ratio
}
#[inline]
pub fn n(&self) -> usize {
self.n
}
#[inline]
pub fn nnz(&self) -> usize {
self.nnz
}
#[inline]
pub fn simplicial(&self) -> &SymbolicLu {
&self.simplicial
}
#[inline]
pub fn supernodal(&self) -> Option<&SymbolicSupernodal> {
self.supernodal_symbolic.as_ref()
}
pub fn factorize<T: SparseScalar>(&self, a: CscView<'_, T>) -> Result<LuSolver<T>, SparseError> {
if self.supernodal {
let symbolic = self.supernodal_symbolic.as_ref().ok_or(SparseError::InvalidStructure)?;
Ok(LuSolver {
inner: LuSolverInner::Supernodal(symbolic.factorize(a)?),
})
} else {
Ok(LuSolver {
inner: LuSolverInner::Simplicial(self.simplicial.factorize(a)?),
})
}
}
}
#[derive(Clone, Debug)]
pub struct LuSolver<T> {
inner: LuSolverInner<T>,
}
#[derive(Clone, Debug)]
enum LuSolverInner<T> {
Simplicial(LuFactors<T>),
Supernodal(SupernodalLu<T>),
}
impl<T> LuSolver<T> {
#[inline]
pub fn is_supernodal(&self) -> bool {
matches!(self.inner, LuSolverInner::Supernodal(_))
}
#[inline]
pub fn n(&self) -> usize {
match &self.inner {
LuSolverInner::Simplicial(f) => f.n(),
LuSolverInner::Supernodal(f) => f.n(),
}
}
#[inline]
pub fn nnz_l(&self) -> usize {
match &self.inner {
LuSolverInner::Simplicial(f) => f.nnz_l(),
LuSolverInner::Supernodal(f) => f.nnz_l(),
}
}
#[inline]
pub fn nnz_u(&self) -> usize {
match &self.inner {
LuSolverInner::Simplicial(f) => f.nnz_u(),
LuSolverInner::Supernodal(f) => f.nnz_u(),
}
}
#[inline]
pub fn as_simplicial(&self) -> Option<&LuFactors<T>> {
match &self.inner {
LuSolverInner::Simplicial(f) => Some(f),
LuSolverInner::Supernodal(_) => None,
}
}
#[inline]
pub fn as_supernodal(&self) -> Option<&SupernodalLu<T>> {
match &self.inner {
LuSolverInner::Simplicial(_) => None,
LuSolverInner::Supernodal(f) => Some(f),
}
}
}
impl<T: SparseScalar> LuSolver<T> {
pub fn solve(&self, b: &mut [T]) -> Result<(), SparseError> {
match &self.inner {
LuSolverInner::Simplicial(f) => f.solve(b),
LuSolverInner::Supernodal(f) => f.solve(b),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn tridiag(n: usize) -> (Vec<usize>, Vec<usize>) {
let mut col_ptr = vec![0];
let mut row_idx = Vec::new();
for j in 0..n {
if j > 0 {
row_idx.push(j - 1);
}
row_idx.push(j);
if j + 1 < n {
row_idx.push(j + 1);
}
col_ptr.push(row_idx.len());
}
(col_ptr, row_idx)
}
fn dense(n: usize) -> (Vec<usize>, Vec<usize>) {
let mut col_ptr = vec![0];
let mut row_idx = Vec::new();
for _j in 0..n {
for i in 0..n {
row_idx.push(i);
}
col_ptr.push(row_idx.len());
}
(col_ptr, row_idx)
}
fn values(n: usize, col_ptr: &[usize], row_idx: &[usize]) -> Vec<f64> {
let mut values = vec![0.0; row_idx.len()];
for j in 0..n {
for k in col_ptr[j]..col_ptr[j + 1] {
values[k] = if row_idx[k] == j { 5.0 } else { 1.0 };
}
}
values
}
fn solve(method: LuMethod, n: usize, col_ptr: &[usize], row_idx: &[usize], values: &[f64]) -> Vec<f64> {
let pattern = CscPattern::new(n, n, col_ptr, row_idx).unwrap();
let a = CscView::new(n, n, col_ptr, row_idx, values).unwrap();
let symbolic = LuSymbolic::analyze(pattern, method).unwrap();
let factors = symbolic.factorize(a).unwrap();
let mut b = vec![1.0; n];
factors.solve(&mut b).unwrap();
b
}
#[test]
fn both_methods_solve_the_same_system() {
let (cp, ri) = tridiag(9);
let v = values(9, &cp, &ri);
let s = solve(LuMethod::Simplicial, 9, &cp, &ri, &v);
let n = solve(LuMethod::Supernodal, 9, &cp, &ri, &v);
for i in 0..9 {
assert!((s[i] - n[i]).abs() < 1e-12, "entry {i}");
}
}
#[test]
fn automatic_selection_picks_by_density() {
let (cp, ri) = tridiag(9);
let v = values(9, &cp, &ri);
let s = LuSymbolic::analyze(CscPattern::new(9, 9, &cp, &ri).unwrap(), LuMethod::auto()).unwrap();
assert!(!s.is_supernodal(), "tridiagonal should stay simplicial");
let n = solve(LuMethod::auto(), 9, &cp, &ri, &v);
assert_eq!(n.len(), 9);
let (cp, ri) = dense(12);
let v = values(12, &cp, &ri);
let symbolic = LuSymbolic::analyze(CscPattern::new(12, 12, &cp, &ri).unwrap(), LuMethod::auto()).unwrap();
assert!(symbolic.is_supernodal(), "dense should be supernodal");
solve(LuMethod::auto(), 12, &cp, &ri, &v);
}
#[test]
fn forced_methods_are_respected() {
let (cp, ri) = dense(6);
let s = LuSymbolic::analyze(CscPattern::new(6, 6, &cp, &ri).unwrap(), LuMethod::Simplicial).unwrap();
assert!(!s.is_supernodal());
let n = LuSymbolic::analyze(CscPattern::new(6, 6, &cp, &ri).unwrap(), LuMethod::Supernodal).unwrap();
assert!(n.is_supernodal());
assert!(n.flop_ratio() > 0.0);
}
#[test]
fn rejects_non_finite_values_in_both_methods() {
let (cp, ri) = tridiag(6);
for method in [LuMethod::Simplicial, LuMethod::Supernodal] {
let symbolic = LuSymbolic::analyze(CscPattern::new(6, 6, &cp, &ri).unwrap(), method).unwrap();
let mut values = values(6, &cp, &ri);
values[0] = f64::NAN;
let view = CscView::new(6, 6, &cp, &ri, &values).unwrap();
assert_eq!(symbolic.factorize(view).unwrap_err(), SparseError::NotFinite);
}
}
#[test]
fn rejects_a_changed_pattern() {
let (cp, ri) = tridiag(6);
let symbolic = LuSymbolic::analyze(CscPattern::new(6, 6, &cp, &ri).unwrap(), LuMethod::Simplicial).unwrap();
let other = CscView::new(6, 6, &[0, 0, 0, 0, 0, 0, 0], &[], &[] as &[f64]).unwrap();
assert_eq!(symbolic.factorize(other).unwrap_err(), SparseError::StructureMismatch);
}
}