use crate::prelude::*;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum SparseFormat {
Csr,
Csc,
}
impl SparseFormat {
#[inline]
pub fn flipped(&self) -> Self {
match self {
SparseFormat::Csr => SparseFormat::Csc,
SparseFormat::Csc => SparseFormat::Csr,
}
}
}
#[derive(Clone, Debug)]
pub struct CompressedSparse<T> {
pub data: Vec<T>,
pub indices: Vec<u32>,
pub indptr: Vec<u32>,
pub format: SparseFormat,
pub shape: (usize, usize),
}
impl<T: Copy> CompressedSparse<T> {
pub fn from_parts(
data: Vec<T>,
indices: Vec<u32>,
indptr: Vec<u32>,
format: SparseFormat,
shape: (usize, usize),
) -> Result<Self, EdgeErrors> {
let out = Self {
data,
indices,
indptr,
format,
shape,
};
out.validate()?;
Ok(out)
}
pub fn validate(&self) -> Result<(), EdgeErrors> {
if self.data.len() != self.indices.len() {
return Err(EdgeErrors::MalformedSparse(format!(
"data has {} values but indices has {}",
self.data.len(),
self.indices.len()
)));
}
let expected_indptr = self.outer_len() + 1;
if self.indptr.len() != expected_indptr {
return Err(EdgeErrors::MalformedSparse(format!(
"indptr has length {} but the {:?} layout of a {:?} matrix needs {}",
self.indptr.len(),
self.format,
self.shape,
expected_indptr
)));
}
if self.indptr[0] != 0 {
return Err(EdgeErrors::MalformedSparse(format!(
"indptr must start at 0, starts at {}",
self.indptr[0]
)));
}
for w in self.indptr.windows(2) {
if w[1] < w[0] {
return Err(EdgeErrors::MalformedSparse(format!(
"indptr is not non-decreasing: {} follows {}",
w[1], w[0]
)));
}
}
let nnz = *self.indptr.last().unwrap_or(&0) as usize;
if nnz != self.data.len() {
return Err(EdgeErrors::MalformedSparse(format!(
"indptr ends at {} but data has {} values",
nnz,
self.data.len()
)));
}
let inner = self.inner_len();
if let Some(&bad) = self.indices.iter().find(|&&i| i as usize >= inner) {
return Err(EdgeErrors::MalformedSparse(format!(
"index {bad} is out of range for a non-compressed axis of length {inner}"
)));
}
Ok(())
}
#[inline]
pub fn nrows(&self) -> usize {
self.shape.0
}
#[inline]
pub fn ncols(&self) -> usize {
self.shape.1
}
#[inline]
pub fn nnz(&self) -> usize {
self.data.len()
}
#[inline]
pub fn outer_len(&self) -> usize {
match self.format {
SparseFormat::Csr => self.shape.0,
SparseFormat::Csc => self.shape.1,
}
}
#[inline]
pub fn inner_len(&self) -> usize {
match self.format {
SparseFormat::Csr => self.shape.1,
SparseFormat::Csc => self.shape.0,
}
}
#[inline]
pub fn outer(&self, outer: usize) -> (&[u32], &[T]) {
let start = self.indptr[outer] as usize;
let end = self.indptr[outer + 1] as usize;
(&self.indices[start..end], &self.data[start..end])
}
pub fn transpose(self) -> Self {
Self {
data: self.data,
indices: self.indices,
indptr: self.indptr,
format: self.format.flipped(),
shape: (self.shape.1, self.shape.0),
}
}
pub fn convert(&self) -> Self
where
T: Default,
{
let outer_new = self.inner_len();
let nnz = self.nnz();
let mut counts = vec![0u32; outer_new + 1];
for &idx in &self.indices {
counts[idx as usize + 1] += 1;
}
for i in 0..outer_new {
counts[i + 1] += counts[i];
}
let indptr_new = counts.clone();
let mut data_new = vec![T::default(); nnz];
let mut indices_new = vec![0u32; nnz];
let mut cursor = counts;
for outer in 0..self.outer_len() {
let start = self.indptr[outer] as usize;
let end = self.indptr[outer + 1] as usize;
for k in start..end {
let inner = self.indices[k] as usize;
let dst = cursor[inner] as usize;
data_new[dst] = self.data[k];
indices_new[dst] = outer as u32;
cursor[inner] += 1;
}
}
Self {
data: data_new,
indices: indices_new,
indptr: indptr_new,
format: self.format.flipped(),
shape: self.shape,
}
}
pub fn from_dense<F>(
dense: &[T],
n_rows: usize,
n_cols: usize,
format: SparseFormat,
is_zero: F,
) -> Result<Self, EdgeErrors>
where
F: Fn(&T) -> bool,
{
if dense.len() != n_rows * n_cols {
return Err(EdgeErrors::LengthMismatch {
name: "dense",
expected: n_rows * n_cols,
got: dense.len(),
});
}
let (outer_len, inner_len) = match format {
SparseFormat::Csr => (n_rows, n_cols),
SparseFormat::Csc => (n_cols, n_rows),
};
let mut data = Vec::new();
let mut indices = Vec::new();
let mut indptr = Vec::with_capacity(outer_len + 1);
indptr.push(0u32);
for o in 0..outer_len {
for i in 0..inner_len {
let flat = match format {
SparseFormat::Csr => o * n_cols + i,
SparseFormat::Csc => i * n_cols + o,
};
let v = dense[flat];
if !is_zero(&v) {
data.push(v);
indices.push(i as u32);
}
}
indptr.push(data.len() as u32);
}
Self::from_parts(data, indices, indptr, format, (n_rows, n_cols))
}
pub fn to_dense(&self, zero: T) -> Vec<T> {
let mut out = vec![zero; self.nrows() * self.ncols()];
for o in 0..self.outer_len() {
let (idx, vals) = self.outer(o);
for (&i, &v) in idx.iter().zip(vals.iter()) {
let flat = match self.format {
SparseFormat::Csr => o * self.ncols() + i as usize,
SparseFormat::Csc => i as usize * self.ncols() + o,
};
out[flat] = v;
}
}
out
}
}
#[cfg(test)]
mod tests {
use super::*;
fn dense_fixture() -> (Vec<f64>, usize, usize) {
let dense = vec![
1.0, 0.0, 2.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 3.0, 4.0, 5.0, ];
(dense, 3, 4)
}
#[test]
fn test_from_dense_csr_round_trips() {
let (dense, r, c) = dense_fixture();
let m =
CompressedSparse::from_dense(&dense, r, c, SparseFormat::Csr, |v| *v == 0.0).unwrap();
assert_eq!(m.nnz(), 5);
assert_eq!(m.to_dense(0.0), dense);
}
#[test]
fn test_from_dense_csc_round_trips() {
let (dense, r, c) = dense_fixture();
let m =
CompressedSparse::from_dense(&dense, r, c, SparseFormat::Csc, |v| *v == 0.0).unwrap();
assert_eq!(m.nnz(), 5);
assert_eq!(m.to_dense(0.0), dense);
}
#[test]
fn test_convert_preserves_the_matrix() {
let (dense, r, c) = dense_fixture();
let csr =
CompressedSparse::from_dense(&dense, r, c, SparseFormat::Csr, |v| *v == 0.0).unwrap();
let csc = csr.convert();
assert_eq!(csc.format, SparseFormat::Csc);
assert_eq!(csc.shape, (r, c));
assert_eq!(csc.to_dense(0.0), dense);
assert_eq!(csc.convert().to_dense(0.0), dense);
}
#[test]
fn test_transpose_is_a_relabel_not_a_copy() {
let (dense, r, c) = dense_fixture();
let csr =
CompressedSparse::from_dense(&dense, r, c, SparseFormat::Csr, |v| *v == 0.0).unwrap();
let data_before = csr.data.clone();
let t = csr.transpose();
assert_eq!(t.format, SparseFormat::Csc);
assert_eq!(t.shape, (c, r));
assert_eq!(t.data, data_before);
let dense_t = t.to_dense(0.0);
for i in 0..c {
for j in 0..r {
assert_eq!(dense_t[i * r + j], dense[j * c + i]);
}
}
}
#[test]
fn test_outer_borrows_a_gene_slice() {
let (dense, r, c) = dense_fixture();
let csr =
CompressedSparse::from_dense(&dense, r, c, SparseFormat::Csr, |v| *v == 0.0).unwrap();
let (idx, vals) = csr.outer(0);
assert_eq!(idx, &[0, 2]);
assert_eq!(vals, &[1.0, 2.0]);
let (idx, vals) = csr.outer(1);
assert!(idx.is_empty());
assert!(vals.is_empty());
let (idx, vals) = csr.outer(2);
assert_eq!(idx, &[1, 2, 3]);
assert_eq!(vals, &[3.0, 4.0, 5.0]);
}
#[test]
fn test_validate_rejects_out_of_range_index() {
let err = CompressedSparse::from_parts(
vec![1.0_f64],
vec![99],
vec![0, 1, 1, 1],
SparseFormat::Csr,
(3, 4),
)
.unwrap_err();
assert!(matches!(err, EdgeErrors::MalformedSparse(_)));
}
#[test]
fn test_validate_rejects_bad_indptr_length() {
let err = CompressedSparse::from_parts(
vec![1.0_f64],
vec![0],
vec![0, 1],
SparseFormat::Csr,
(3, 4),
)
.unwrap_err();
assert!(matches!(err, EdgeErrors::MalformedSparse(_)));
}
#[test]
fn test_convert_handles_empty_matrix() {
let m: CompressedSparse<f64> =
CompressedSparse::from_parts(vec![], vec![], vec![0; 4], SparseFormat::Csr, (3, 4))
.unwrap();
let c = m.convert();
assert_eq!(c.nnz(), 0);
assert_eq!(c.indptr.len(), 5);
assert_eq!(c.to_dense(0.0), vec![0.0; 12]);
}
}