use alloc::vec::Vec;
use core::fmt;
use core::ops::Range;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CsrError {
LengthMismatch,
RowPtrLengthMismatch,
RowPtrInvalid,
ColIndexOutOfBounds,
DimensionOverflow,
}
pub struct CsrMatrix<T> {
rows: usize,
cols: usize,
row_ptr: Vec<u32>,
col_indices: Vec<u32>,
values: Vec<T>,
}
impl<T> CsrMatrix<T> {
pub(super) fn new_raw(
rows: usize,
cols: usize,
row_ptr: Vec<u32>,
col_indices: Vec<u32>,
values: Vec<T>,
) -> Self {
Self {
rows,
cols,
row_ptr,
col_indices,
values,
}
}
pub(super) fn into_raw_parts(self) -> (usize, usize, Vec<u32>, Vec<u32>, Vec<T>) {
(
self.rows,
self.cols,
self.row_ptr,
self.col_indices,
self.values,
)
}
pub fn new(
rows: usize,
cols: usize,
row_ptr: Vec<u32>,
col_indices: Vec<u32>,
values: Vec<T>,
) -> Result<Self, CsrError> {
if col_indices.len() != values.len() {
return Err(CsrError::LengthMismatch);
}
let expected_len = rows.checked_add(1).ok_or(CsrError::DimensionOverflow)?;
if row_ptr.len() != expected_len {
return Err(CsrError::RowPtrLengthMismatch);
}
let nnz = col_indices.len();
if row_ptr[0] != 0 || row_ptr[rows] as usize != nnz {
return Err(CsrError::RowPtrInvalid);
}
for i in 1..=rows {
if row_ptr[i] < row_ptr[i - 1] {
return Err(CsrError::RowPtrInvalid);
}
}
for &c in &col_indices {
if c as usize >= cols {
return Err(CsrError::ColIndexOutOfBounds);
}
}
Ok(Self {
rows,
cols,
row_ptr,
col_indices,
values,
})
}
pub fn rows(&self) -> usize {
self.rows
}
pub fn cols(&self) -> usize {
self.cols
}
pub fn nnz(&self) -> usize {
self.values.len()
}
pub fn row_ptr(&self) -> &[u32] {
&self.row_ptr
}
pub fn col_indices(&self) -> &[u32] {
&self.col_indices
}
pub fn values(&self) -> &[T] {
&self.values
}
pub fn row_range(&self, row: usize) -> Option<Range<usize>> {
if row >= self.rows {
None
} else {
Some(self.row_ptr[row] as usize..self.row_ptr[row + 1] as usize)
}
}
}
impl<T: PartialEq> PartialEq for CsrMatrix<T> {
fn eq(&self, other: &Self) -> bool {
self.rows == other.rows
&& self.cols == other.cols
&& self.row_ptr == other.row_ptr
&& self.col_indices == other.col_indices
&& self.values == other.values
}
}
impl<T: fmt::Debug> fmt::Debug for CsrMatrix<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("CsrMatrix")
.field("rows", &self.rows)
.field("cols", &self.cols)
.field("nnz", &self.values.len())
.finish()
}
}
#[cfg(test)]
mod tests {
use super::{CsrError, CsrMatrix};
#[test]
fn constructs_empty_matrix() {
let m = CsrMatrix::<f64>::new(3, 4, vec![0, 0, 0, 0], vec![], vec![]).unwrap();
assert_eq!(m.rows(), 3);
assert_eq!(m.cols(), 4);
assert_eq!(m.nnz(), 0);
assert_eq!(m.row_ptr(), &[0, 0, 0, 0]);
}
#[test]
fn constructs_identity_matrix() {
let m = CsrMatrix::new(
3,
3,
vec![0, 1, 2, 3],
vec![0, 1, 2],
vec![1.0_f64, 1.0, 1.0],
)
.unwrap();
assert_eq!(m.rows(), 3);
assert_eq!(m.cols(), 3);
assert_eq!(m.nnz(), 3);
assert_eq!(m.col_indices(), &[0, 1, 2]);
assert_eq!(m.values(), &[1.0, 1.0, 1.0]);
assert_eq!(m.row_range(0), Some(0..1));
assert_eq!(m.row_range(1), Some(1..2));
assert_eq!(m.row_range(2), Some(2..3));
assert_eq!(m.row_range(3), None);
}
#[test]
fn constructs_matrix_with_empty_rows() {
let m = CsrMatrix::new(
3,
3,
vec![0, 0, 1, 3],
vec![0, 1, 2],
vec![1.0_f64, 2.0, 3.0],
)
.unwrap();
assert_eq!(m.row_range(0), Some(0..0));
assert_eq!(m.row_range(1), Some(0..1));
assert_eq!(m.row_range(2), Some(1..3));
}
#[test]
fn length_mismatch_is_an_error_not_a_panic() {
let err = CsrMatrix::<f64>::new(2, 2, vec![0, 1, 2], vec![0, 1], vec![1.0]);
assert_eq!(err, Err(CsrError::LengthMismatch));
}
#[test]
fn row_ptr_length_mismatch_is_an_error_not_a_panic() {
let err = CsrMatrix::<f64>::new(3, 3, vec![0, 1], vec![], vec![]);
assert_eq!(err, Err(CsrError::RowPtrLengthMismatch));
}
#[test]
fn row_ptr_not_starting_at_zero_is_an_error_not_a_panic() {
let err = CsrMatrix::<f64>::new(2, 2, vec![1, 1, 1], vec![], vec![]);
assert_eq!(err, Err(CsrError::RowPtrInvalid));
}
#[test]
fn row_ptr_not_monotone_is_an_error_not_a_panic() {
let err = CsrMatrix::<f64>::new(3, 3, vec![0, 2, 1, 3], vec![0, 1, 2], vec![1.0, 2.0, 3.0]);
assert_eq!(err, Err(CsrError::RowPtrInvalid));
}
#[test]
fn row_ptr_last_entry_mismatch_is_an_error_not_a_panic() {
let err = CsrMatrix::<f64>::new(2, 2, vec![0, 1, 5], vec![0, 1], vec![1.0, 2.0]);
assert_eq!(err, Err(CsrError::RowPtrInvalid));
}
#[test]
fn col_index_out_of_bounds_is_an_error_not_a_panic() {
let err = CsrMatrix::<f64>::new(2, 2, vec![0, 1, 2], vec![0, 3], vec![1.0, 2.0]);
assert_eq!(err, Err(CsrError::ColIndexOutOfBounds));
}
#[test]
fn partial_eq_compares_all_fields() {
let a = CsrMatrix::new(2, 2, vec![0, 1, 2], vec![0, 1], vec![1.0_f64, 2.0]).unwrap();
let b = CsrMatrix::new(2, 2, vec![0, 1, 2], vec![0, 1], vec![1.0_f64, 2.0]).unwrap();
let c = CsrMatrix::new(2, 2, vec![0, 1, 2], vec![0, 1], vec![1.0_f64, 9.0]).unwrap();
assert_eq!(a, b);
assert_ne!(a, c);
}
#[test]
fn rows_plus_one_overflow_is_an_error_not_a_panic() {
let err = CsrMatrix::<f64>::new(usize::MAX, 1, vec![], vec![], vec![]);
assert_eq!(err, Err(CsrError::DimensionOverflow));
}
#[test]
fn zero_row_matrix_is_valid() {
let m = CsrMatrix::<f64>::new(0, 5, vec![0], vec![], vec![]).unwrap();
assert_eq!(m.rows(), 0);
assert_eq!(m.nnz(), 0);
assert_eq!(m.row_range(0), None);
}
}