use oxicuda_blas::GpuFloat;
use oxicuda_memory::DeviceBuffer;
use crate::error::{SparseError, SparseResult};
pub struct CsrMatrix<T: GpuFloat> {
rows: u32,
cols: u32,
nnz: u32,
row_ptr: DeviceBuffer<i32>,
col_idx: DeviceBuffer<i32>,
values: DeviceBuffer<T>,
}
impl<T: GpuFloat> CsrMatrix<T> {
pub fn from_host(
rows: u32,
cols: u32,
row_ptr: &[i32],
col_idx: &[i32],
values: &[T],
) -> SparseResult<Self> {
if rows == 0 || cols == 0 {
return Err(SparseError::InvalidFormat(
"rows and cols must be non-zero".to_string(),
));
}
let expected_row_ptr_len = rows as usize + 1;
if row_ptr.len() != expected_row_ptr_len {
return Err(SparseError::InvalidFormat(format!(
"row_ptr length ({}) must be rows + 1 ({})",
row_ptr.len(),
expected_row_ptr_len
)));
}
let nnz = values.len();
if nnz == 0 {
return Err(SparseError::ZeroNnz);
}
if col_idx.len() != nnz {
return Err(SparseError::InvalidFormat(format!(
"col_idx length ({}) must equal values length ({})",
col_idx.len(),
nnz
)));
}
if row_ptr[0] != 0 {
return Err(SparseError::InvalidFormat(
"row_ptr[0] must be 0".to_string(),
));
}
if row_ptr[rows as usize] != nnz as i32 {
return Err(SparseError::InvalidFormat(format!(
"row_ptr[rows] ({}) must equal nnz ({})",
row_ptr[rows as usize], nnz
)));
}
for i in 0..rows as usize {
if row_ptr[i] > row_ptr[i + 1] {
return Err(SparseError::InvalidFormat(format!(
"row_ptr must be non-decreasing: row_ptr[{}]={} > row_ptr[{}]={}",
i,
row_ptr[i],
i + 1,
row_ptr[i + 1]
)));
}
}
for (k, &c) in col_idx.iter().enumerate() {
if c < 0 || c as u32 >= cols {
return Err(SparseError::InvalidFormat(format!(
"col_idx[{k}] = {c} out of range [0, {cols})"
)));
}
}
let d_row_ptr = DeviceBuffer::from_host(row_ptr)?;
let d_col_idx = DeviceBuffer::from_host(col_idx)?;
let d_values = DeviceBuffer::from_host(values)?;
Ok(Self {
rows,
cols,
nnz: nnz as u32,
row_ptr: d_row_ptr,
col_idx: d_col_idx,
values: d_values,
})
}
pub fn from_device(
rows: u32,
cols: u32,
nnz: u32,
row_ptr: DeviceBuffer<i32>,
col_idx: DeviceBuffer<i32>,
values: DeviceBuffer<T>,
) -> SparseResult<Self> {
if row_ptr.len() != (rows as usize + 1) {
return Err(SparseError::InvalidFormat(format!(
"row_ptr length ({}) must be rows + 1 ({})",
row_ptr.len(),
rows as usize + 1
)));
}
if col_idx.len() != nnz as usize {
return Err(SparseError::InvalidFormat(format!(
"col_idx length ({}) must equal nnz ({})",
col_idx.len(),
nnz
)));
}
if values.len() != nnz as usize {
return Err(SparseError::InvalidFormat(format!(
"values length ({}) must equal nnz ({})",
values.len(),
nnz
)));
}
Ok(Self {
rows,
cols,
nnz,
row_ptr,
col_idx,
values,
})
}
pub fn to_host(&self) -> SparseResult<(Vec<i32>, Vec<i32>, Vec<T>)> {
let mut h_row_ptr = vec![0i32; self.row_ptr.len()];
let mut h_col_idx = vec![0i32; self.col_idx.len()];
let mut h_values = vec![T::gpu_zero(); self.values.len()];
self.row_ptr.copy_to_host(&mut h_row_ptr)?;
self.col_idx.copy_to_host(&mut h_col_idx)?;
self.values.copy_to_host(&mut h_values)?;
Ok((h_row_ptr, h_col_idx, h_values))
}
#[inline]
pub fn rows(&self) -> u32 {
self.rows
}
#[inline]
pub fn cols(&self) -> u32 {
self.cols
}
#[inline]
pub fn nnz(&self) -> u32 {
self.nnz
}
#[inline]
pub fn density(&self) -> f64 {
let total = self.rows as f64 * self.cols as f64;
if total == 0.0 {
return 0.0;
}
self.nnz as f64 / total
}
#[inline]
pub fn is_valid(&self) -> bool {
self.row_ptr.len() == (self.rows as usize + 1)
&& self.col_idx.len() == self.nnz as usize
&& self.values.len() == self.nnz as usize
}
#[inline]
pub fn row_ptr(&self) -> &DeviceBuffer<i32> {
&self.row_ptr
}
#[inline]
pub fn col_idx(&self) -> &DeviceBuffer<i32> {
&self.col_idx
}
#[inline]
pub fn values(&self) -> &DeviceBuffer<T> {
&self.values
}
#[inline]
pub fn values_mut(&mut self) -> &mut DeviceBuffer<T> {
&mut self.values
}
#[inline]
pub fn avg_nnz_per_row(&self) -> f64 {
if self.rows == 0 {
return 0.0;
}
self.nnz as f64 / self.rows as f64
}
pub fn to_coo(&self) -> SparseResult<super::CooMatrix<T>> {
let (h_row_ptr, h_col_idx, h_values) = self.to_host()?;
let mut h_row_idx = Vec::with_capacity(self.nnz as usize);
for row in 0..self.rows {
let start = h_row_ptr[row as usize];
let end = h_row_ptr[row as usize + 1];
for _ in start..end {
h_row_idx.push(row as i32);
}
}
super::CooMatrix::from_host(self.rows, self.cols, &h_row_idx, &h_col_idx, &h_values)
}
pub fn to_csc(&self) -> SparseResult<super::CscMatrix<T>> {
let (h_row_ptr, h_col_idx, h_values) = self.to_host()?;
let mut col_counts = vec![0i32; self.cols as usize];
for &c in &h_col_idx {
col_counts[c as usize] += 1;
}
let mut h_col_ptr = vec![0i32; self.cols as usize + 1];
for i in 0..self.cols as usize {
h_col_ptr[i + 1] = h_col_ptr[i] + col_counts[i];
}
let mut h_csc_row_idx = vec![0i32; self.nnz as usize];
let mut h_csc_values = vec![T::gpu_zero(); self.nnz as usize];
let mut write_pos = h_col_ptr.clone();
for row in 0..self.rows as usize {
let start = h_row_ptr[row] as usize;
let end = h_row_ptr[row + 1] as usize;
for j in start..end {
let col = h_col_idx[j] as usize;
let dest = write_pos[col] as usize;
h_csc_row_idx[dest] = row as i32;
h_csc_values[dest] = h_values[j];
write_pos[col] += 1;
}
}
super::CscMatrix::from_host(
self.rows,
self.cols,
&h_col_ptr,
&h_csc_row_idx,
&h_csc_values,
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn csr_validation_row_ptr_length() {
let result = CsrMatrix::<f32>::from_host(3, 3, &[0, 2, 4], &[0, 1, 0, 2], &[1.0; 4]);
assert!(result.is_err());
}
#[test]
fn csr_validation_zero_nnz() {
let result = CsrMatrix::<f32>::from_host(2, 2, &[0, 0, 0], &[], &[]);
assert!(matches!(result, Err(SparseError::ZeroNnz)));
}
#[test]
fn csr_validation_mismatched_col_idx() {
let result = CsrMatrix::<f32>::from_host(2, 2, &[0, 1, 2], &[0], &[1.0, 2.0]);
assert!(result.is_err());
}
#[test]
fn csr_validation_col_idx_out_of_range() {
let result = CsrMatrix::<f32>::from_host(2, 2, &[0, 1, 2], &[0, 2], &[1.0, 2.0]);
assert!(matches!(result, Err(SparseError::InvalidFormat(_))));
}
#[test]
fn csr_validation_negative_col_idx() {
let result = CsrMatrix::<f32>::from_host(2, 2, &[0, 1, 2], &[0, -1], &[1.0, 2.0]);
assert!(matches!(result, Err(SparseError::InvalidFormat(_))));
}
#[test]
fn csr_density() {
assert!((4.0_f64 / 16.0 - 0.25).abs() < 1e-10);
}
}