use std::cell::Cell;
use oxicuda_blas::{DiagType, FillMode};
use crate::error::{SparseError, SparseResult};
use crate::host_csr::HostCsr;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum CusparsePointerMode {
#[default]
Host,
Device,
}
#[derive(Debug)]
pub struct CusparseCompatHandle {
pointer_mode: CusparsePointerMode,
op_count: Cell<u64>,
}
impl CusparseCompatHandle {
pub fn create() -> SparseResult<Self> {
Ok(Self {
pointer_mode: CusparsePointerMode::default(),
op_count: Cell::new(0),
})
}
pub fn destroy(self) -> SparseResult<()> {
Ok(())
}
#[inline]
pub fn pointer_mode(&self) -> CusparsePointerMode {
self.pointer_mode
}
#[inline]
pub fn set_pointer_mode(&mut self, mode: CusparsePointerMode) {
self.pointer_mode = mode;
}
#[inline]
pub fn op_count(&self) -> u64 {
self.op_count.get()
}
#[inline]
fn record_op(&self) {
self.op_count.set(self.op_count.get() + 1);
}
}
pub fn cusparse_spmv(
handle: &CusparseCompatHandle,
alpha: f64,
a: &HostCsr,
x: &[f64],
beta: f64,
y: &mut [f64],
) -> SparseResult<()> {
handle.record_op();
if x.len() != a.ncols {
return Err(SparseError::DimensionMismatch(format!(
"SpMV: x length ({}) must equal A.ncols ({})",
x.len(),
a.ncols
)));
}
if y.len() != a.nrows {
return Err(SparseError::DimensionMismatch(format!(
"SpMV: y length ({}) must equal A.nrows ({})",
y.len(),
a.nrows
)));
}
let ax = a.matvec(x);
for (yi, &ax_i) in y.iter_mut().zip(ax.iter()) {
let scaled = if beta == 0.0 { 0.0 } else { beta * *yi };
*yi = alpha * ax_i + scaled;
}
Ok(())
}
pub fn cusparse_spgemm(
handle: &CusparseCompatHandle,
a: &HostCsr,
b: &HostCsr,
) -> SparseResult<HostCsr> {
handle.record_op();
a.matmul(b)
}
pub fn cusparse_spsv(
handle: &CusparseCompatHandle,
fill_mode: FillMode,
diag_type: DiagType,
alpha: f64,
a: &HostCsr,
b: &[f64],
) -> SparseResult<Vec<f64>> {
handle.record_op();
if a.nrows != a.ncols {
return Err(SparseError::DimensionMismatch(format!(
"SpSV requires a square matrix, got {}x{}",
a.nrows, a.ncols
)));
}
if b.len() != a.nrows {
return Err(SparseError::DimensionMismatch(format!(
"SpSV: b length ({}) must equal A.nrows ({})",
b.len(),
a.nrows
)));
}
let n = a.nrows;
let mut x = vec![0.0f64; n];
match fill_mode {
FillMode::Lower => {
for i in 0..n {
let (acc, diag) = substitution_row(a, i, alpha * b[i], &x, c_below(i));
x[i] = finish_row(acc, diag, diag_type)?;
}
}
FillMode::Upper => {
for i in (0..n).rev() {
let (acc, diag) = substitution_row(a, i, alpha * b[i], &x, c_above(i));
x[i] = finish_row(acc, diag, diag_type)?;
}
}
FillMode::Full => {
return Err(SparseError::InvalidArgument(
"SpSV requires a triangular fill mode (Lower or Upper), got Full".to_string(),
));
}
}
Ok(x)
}
#[inline]
fn c_below(i: usize) -> impl Fn(usize) -> bool {
move |c| c < i
}
#[inline]
fn c_above(i: usize) -> impl Fn(usize) -> bool {
move |c| c > i
}
#[inline]
fn substitution_row(
a: &HostCsr,
i: usize,
rhs: f64,
x: &[f64],
is_solved: impl Fn(usize) -> bool,
) -> (f64, Option<f64>) {
let mut acc = rhs;
let mut diag: Option<f64> = None;
let start = a.row_ptr[i];
let end = a.row_ptr[i + 1];
for k in start..end {
let c = a.col_indices[k];
if c == i {
diag = Some(a.values[k]);
} else if is_solved(c) {
acc -= a.values[k] * x[c];
}
}
(acc, diag)
}
#[inline]
fn finish_row(acc: f64, diag: Option<f64>, diag_type: DiagType) -> SparseResult<f64> {
match diag_type {
DiagType::Unit => Ok(acc),
DiagType::NonUnit => match diag {
Some(d) if d.abs() > 1e-300 => Ok(acc / d),
_ => Err(SparseError::SingularMatrix),
},
}
}
#[cfg(test)]
mod tests {
use super::*;
fn dense_matvec(dense: &[f64], nrows: usize, ncols: usize, x: &[f64]) -> Vec<f64> {
(0..nrows)
.map(|i| {
dense[i * ncols..(i + 1) * ncols]
.iter()
.zip(x.iter())
.map(|(&a, &xj)| a * xj)
.sum()
})
.collect()
}
fn sample_matrix() -> HostCsr {
HostCsr::new(
3,
3,
vec![0, 2, 3, 5],
vec![0, 2, 1, 0, 2],
vec![1.0, 2.0, 3.0, 4.0, 5.0],
)
.expect("sample")
}
#[test]
fn spmv_alpha1_beta0_matches_dense() {
let h = CusparseCompatHandle::create().expect("handle");
let a = sample_matrix();
let x = [1.0, 2.0, 3.0];
let mut y = vec![0.0; 3];
cusparse_spmv(&h, 1.0, &a, &x, 0.0, &mut y).expect("spmv");
let reference = dense_matvec(&a.to_dense(), 3, 3, &x);
for (got, want) in y.iter().zip(reference.iter()) {
assert!((got - want).abs() < 1e-12, "got {got} want {want}");
}
assert!(y.iter().all(|v| v.is_finite()));
}
#[test]
fn spmv_general_alpha_beta() {
let h = CusparseCompatHandle::create().expect("handle");
let a = sample_matrix();
let x = [2.0, -1.0, 0.5];
let y0 = [10.0, 20.0, 30.0];
let alpha = 1.5;
let beta = -2.0;
let mut y = y0.to_vec();
cusparse_spmv(&h, alpha, &a, &x, beta, &mut y).expect("spmv");
let ax = dense_matvec(&a.to_dense(), 3, 3, &x);
for i in 0..3 {
let want = alpha * ax[i] + beta * y0[i];
assert!((y[i] - want).abs() < 1e-12, "row {i}: {} vs {}", y[i], want);
}
}
#[test]
fn spmv_beta0_ignores_prior_nonfinite_y() {
let h = CusparseCompatHandle::create().expect("handle");
let a = sample_matrix();
let x = [1.0, 1.0, 1.0];
let mut y = vec![f64::NAN, f64::INFINITY, -f64::INFINITY];
cusparse_spmv(&h, 1.0, &a, &x, 0.0, &mut y).expect("spmv");
assert!(y.iter().all(|v| v.is_finite()), "beta=0 must overwrite y");
}
#[test]
fn spgemm_matches_dense() {
let h = CusparseCompatHandle::create().expect("handle");
let a = sample_matrix();
let b = HostCsr::new(
3,
2,
vec![0, 1, 2, 4],
vec![0, 1, 0, 1],
vec![1.0, 2.0, 3.0, 4.0],
)
.expect("b");
let c = cusparse_spgemm(&h, &a, &b).expect("spgemm");
let da = a.to_dense();
let db = b.to_dense();
let mut dc = [0.0f64; 3 * 2];
for i in 0..3 {
for j in 0..2 {
let mut acc = 0.0;
for k in 0..3 {
acc += da[i * 3 + k] * db[k * 2 + j];
}
dc[i * 2 + j] = acc;
}
}
for i in 0..3 {
for j in 0..2 {
let got = c.get(i, j).unwrap_or(0.0);
assert!((got - dc[i * 2 + j]).abs() < 1e-12, "C[{i},{j}]");
}
}
}
#[test]
fn spsv_lower_solves_system() {
let h = CusparseCompatHandle::create().expect("handle");
let l = HostCsr::new(
3,
3,
vec![0, 1, 3, 6],
vec![0, 0, 1, 0, 1, 2],
vec![2.0, 1.0, 3.0, 4.0, 5.0, 6.0],
)
.expect("l");
let x_true = [1.0, 2.0, 3.0];
let b = l.matvec(&x_true);
let x = cusparse_spsv(&h, FillMode::Lower, DiagType::NonUnit, 1.0, &l, &b).expect("spsv");
for (got, want) in x.iter().zip(x_true.iter()) {
assert!((got - want).abs() < 1e-12, "got {got} want {want}");
}
let check = l.matvec(&x);
for (c, bi) in check.iter().zip(b.iter()) {
assert!((c - bi).abs() < 1e-12);
}
assert!(x.iter().all(|v| v.is_finite()));
}
#[test]
fn spsv_upper_solves_system() {
let h = CusparseCompatHandle::create().expect("handle");
let u = HostCsr::new(
3,
3,
vec![0, 3, 5, 6],
vec![0, 1, 2, 1, 2, 2],
vec![2.0, 1.0, 1.0, 3.0, 2.0, 4.0],
)
.expect("u");
let x_true = [-1.0, 2.0, 0.5];
let b = u.matvec(&x_true);
let x = cusparse_spsv(&h, FillMode::Upper, DiagType::NonUnit, 1.0, &u, &b).expect("spsv");
for (got, want) in x.iter().zip(x_true.iter()) {
assert!((got - want).abs() < 1e-12, "got {got} want {want}");
}
}
#[test]
fn spsv_unit_diagonal() {
let h = CusparseCompatHandle::create().expect("handle");
let l =
HostCsr::new(3, 3, vec![0, 0, 1, 3], vec![0, 0, 1], vec![2.0, 3.0, 4.0]).expect("l");
let x_true = [5.0, -1.0, 2.0];
let b = [
x_true[0],
2.0 * x_true[0] + x_true[1],
3.0 * x_true[0] + 4.0 * x_true[1] + x_true[2],
];
let x = cusparse_spsv(&h, FillMode::Lower, DiagType::Unit, 1.0, &l, &b).expect("spsv");
for (got, want) in x.iter().zip(x_true.iter()) {
assert!((got - want).abs() < 1e-12, "got {got} want {want}");
}
}
#[test]
fn spsv_alpha_scales_rhs() {
let h = CusparseCompatHandle::create().expect("handle");
let l = HostCsr::new(2, 2, vec![0, 1, 3], vec![0, 0, 1], vec![2.0, 1.0, 4.0]).expect("l");
let b = [3.0, 7.0];
let alpha = 2.0;
let x = cusparse_spsv(&h, FillMode::Lower, DiagType::NonUnit, alpha, &l, &b).expect("spsv");
let ax = l.matvec(&x);
for (axi, bi) in ax.iter().zip(b.iter()) {
assert!((axi - alpha * bi).abs() < 1e-12);
}
}
#[test]
fn handle_lifecycle_and_op_count() {
let mut h = CusparseCompatHandle::create().expect("create");
assert_eq!(h.pointer_mode(), CusparsePointerMode::Host);
assert_eq!(h.op_count(), 0);
let a = sample_matrix();
let x = [1.0, 1.0, 1.0];
let mut y = vec![0.0; 3];
cusparse_spmv(&h, 1.0, &a, &x, 0.0, &mut y).expect("spmv");
assert_eq!(h.op_count(), 1);
h.set_pointer_mode(CusparsePointerMode::Device);
assert_eq!(h.pointer_mode(), CusparsePointerMode::Device);
let _ = cusparse_spgemm(&h, &a, &a).expect("spgemm");
assert_eq!(h.op_count(), 2);
h.destroy().expect("destroy");
}
#[test]
fn spmv_dimension_mismatch_errors() {
let h = CusparseCompatHandle::create().expect("handle");
let a = sample_matrix();
let bad_x = [1.0, 2.0]; let mut y = vec![0.0; 3];
assert!(cusparse_spmv(&h, 1.0, &a, &bad_x, 0.0, &mut y).is_err());
let x = [1.0, 2.0, 3.0];
let mut bad_y = vec![0.0; 2]; assert!(cusparse_spmv(&h, 1.0, &a, &x, 0.0, &mut bad_y).is_err());
}
#[test]
fn spgemm_dimension_mismatch_errors() {
let h = CusparseCompatHandle::create().expect("handle");
let a = sample_matrix(); let b = HostCsr::new(2, 2, vec![0, 1, 2], vec![0, 1], vec![1.0, 1.0]).expect("b"); assert!(cusparse_spgemm(&h, &a, &b).is_err());
}
#[test]
fn spsv_rejects_non_square_and_full() {
let h = CusparseCompatHandle::create().expect("handle");
let rect = HostCsr::new(2, 3, vec![0, 1, 2], vec![0, 1], vec![1.0, 1.0]).expect("rect");
let b = [1.0, 2.0];
assert!(cusparse_spsv(&h, FillMode::Lower, DiagType::NonUnit, 1.0, &rect, &b).is_err());
let sq = sample_matrix();
let b3 = [1.0, 2.0, 3.0];
assert!(
cusparse_spsv(&h, FillMode::Full, DiagType::NonUnit, 1.0, &sq, &b3).is_err(),
"Full fill mode must be rejected"
);
}
#[test]
fn spsv_singular_diagonal_errors() {
let h = CusparseCompatHandle::create().expect("handle");
let l = HostCsr::new(2, 2, vec![0, 1, 2], vec![0, 0], vec![2.0, 5.0]).expect("l");
let b = [2.0, 5.0];
assert!(cusparse_spsv(&h, FillMode::Lower, DiagType::NonUnit, 1.0, &l, &b).is_err());
}
}