use num_traits::Float;
use rayon::prelude::*;
use std::ops::{Add, AddAssign, Mul};
use crate::errors::SparseErrors;
use crate::traits::BixverseNumeric;
pub fn thread_chunks(n: usize) -> Vec<(usize, usize)> {
let n_threads = rayon::current_num_threads().max(1);
let chunk = n.div_ceil(n_threads).max(1);
(0..n)
.step_by(chunk)
.map(|start| (start, (start + chunk).min(n)))
.collect()
}
#[derive(Debug, Clone, Copy)]
pub enum CompressedSparseFormat {
Csc,
Csr,
}
impl std::fmt::Display for CompressedSparseFormat {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
CompressedSparseFormat::Csc => write!(f, "CSC"),
CompressedSparseFormat::Csr => write!(f, "CSR"),
}
}
}
impl CompressedSparseFormat {
#[inline(always)]
pub fn is_csc(&self) -> bool {
matches!(self, CompressedSparseFormat::Csc)
}
#[inline(always)]
pub fn is_csr(&self) -> bool {
matches!(self, CompressedSparseFormat::Csr)
}
}
pub struct SparseAxis<T, U = T> {
pub indices: Vec<usize>,
pub data: Vec<T>,
pub data_2: Option<Vec<U>>,
pub cs_type: CompressedSparseFormat,
pub len: usize,
}
impl<T, U> SparseAxis<T, U>
where
T: BixverseNumeric,
U: BixverseNumeric,
{
pub fn new_csc(indices: Vec<usize>, data: Vec<T>, data_2: Option<Vec<U>>, len: usize) -> Self {
SparseAxis {
indices,
data,
data_2,
cs_type: CompressedSparseFormat::Csc,
len,
}
}
pub fn get_indices_data_2(&self) -> Result<(&[usize], &[U]), SparseErrors> {
let indices = &self.indices;
let data_2 = self
.data_2
.as_ref()
.ok_or(SparseErrors::Data2NotAvailable)?;
Ok((indices, data_2))
}
pub fn from_vecs_to_csc(indices: Vec<T>, values: Vec<U>) -> Result<Self, SparseErrors>
where
T: Into<usize>,
{
if indices.len() != values.len() {
return Err(SparseErrors::DimensionMisMatchSparse {
indices_len: indices.len(),
data_len: values.len(),
});
}
let usize_indices: Vec<usize> = indices.into_iter().map(Into::into).collect();
let len = usize_indices.last().map_or(0, |&i| i + 1);
Ok(Self {
indices: usize_indices,
data: Vec::new(),
data_2: Some(values),
cs_type: CompressedSparseFormat::Csc,
len,
})
}
}
#[derive(Debug, Clone)]
pub struct CompressedSparseData2<T, U = T>
where
T: Clone,
U: Clone,
{
pub data: Vec<T>,
pub indices: Vec<u32>,
pub indptr: Vec<u32>,
pub cs_type: CompressedSparseFormat,
pub data_2: Option<Vec<U>>,
pub shape: (usize, usize),
}
impl<T, U> CompressedSparseData2<T, U>
where
T: BixverseNumeric,
U: BixverseNumeric,
{
pub fn new_csc(
data: &[T],
indices: &[u32],
indptr: &[u32],
data2: Option<&[U]>,
shape: (usize, usize),
) -> Self {
Self::from_parts(
data.to_vec(),
indices.to_vec(),
indptr.to_vec(),
data2.map(|d| d.to_vec()),
CompressedSparseFormat::Csc,
shape,
)
}
pub fn new_csr(
data: &[T],
indices: &[u32],
indptr: &[u32],
data2: Option<&[U]>,
shape: (usize, usize),
) -> Self {
Self::from_parts(
data.to_vec(),
indices.to_vec(),
indptr.to_vec(),
data2.map(|d| d.to_vec()),
CompressedSparseFormat::Csr,
shape,
)
}
pub fn from_parts(
data: Vec<T>,
indices: Vec<u32>,
indptr: Vec<u32>,
data2: Option<Vec<U>>,
cs_type: CompressedSparseFormat,
shape: (usize, usize),
) -> Self {
Self {
data,
indices,
indptr,
cs_type,
data_2: data2,
shape,
}
}
pub fn transform(&self) -> Self {
transpose_sparse(self)
}
pub fn transform_single_layer(&self, use_second_layer: bool) -> Result<Self, SparseErrors> {
transpose_sparse_single_layer(self, use_second_layer)
}
pub fn transpose_and_convert(&self) -> Self {
let mut out = match self.cs_type {
CompressedSparseFormat::Csr => transpose_sparse(self),
CompressedSparseFormat::Csc => self.clone(),
};
out.cs_type = CompressedSparseFormat::Csr;
out.shape = (self.shape.1, self.shape.0);
out
}
pub fn from_upper_triangle_sym(
upper_triangle: &[T],
n: usize,
include_diagonal: bool,
format: CompressedSparseFormat,
) -> Self
where
T: BixverseNumeric + Float,
{
let get_value = |row: usize, col: usize| -> T {
if row == col {
if include_diagonal {
let offset = row * n - row * (row + 1) / 2 + col;
upper_triangle[offset]
} else {
T::one()
}
} else if row < col {
let offset = if include_diagonal {
row * n - row * (row + 1) / 2 + col
} else {
row * (n - 1) - row * (row + 1) / 2 + col - 1
};
upper_triangle[offset]
} else {
let offset = if include_diagonal {
col * n - col * (col + 1) / 2 + row
} else {
col * (n - 1) - col * (col + 1) / 2 + row - 1
};
upper_triangle[offset]
}
};
Self::from_entry_fn(n, n, format, get_value)
}
pub fn shape(&self) -> (usize, usize) {
self.shape
}
pub fn nrows(&self) -> usize {
self.shape.0
}
pub fn ncols(&self) -> usize {
self.shape.1
}
pub fn get_nnz(&self) -> usize {
self.indices.len()
}
pub fn get_data2_unsafe(&self) -> Vec<U> {
self.data_2.clone().unwrap()
}
pub fn to_sparse_axes(&self) -> Vec<SparseAxis<T, U>> {
let len = self.minor_dim();
let has_data = self.data.len() == self.indices.len();
self.indptr
.par_windows(2)
.map(|w| {
let (lo, hi) = (w[0] as usize, w[1] as usize);
SparseAxis {
indices: self.indices[lo..hi].iter().map(|&i| i as usize).collect(),
data: if has_data {
self.data[lo..hi].to_vec()
} else {
Vec::new()
},
data_2: self.data_2.as_ref().map(|d| d[lo..hi].to_vec()),
cs_type: self.cs_type,
len,
}
})
.collect()
}
fn slice_major(&self, keep: &[usize]) -> Result<Self, SparseErrors> {
let major = self.major_dim();
let mut seen = vec![false; major];
let mut indptr = Vec::with_capacity(keep.len() + 1);
indptr.push(0u32);
let mut indices = Vec::new();
let mut data = Vec::new();
let mut data_2 = self.data_2.as_ref().map(|_| Vec::new());
for &m in keep {
if m >= major {
return Err(SparseErrors::SliceIndexOutOfBounds {
index: m,
len: major,
});
}
if seen[m] {
return Err(SparseErrors::DuplicateSliceIndex(m));
}
seen[m] = true;
let s = self.indptr[m] as usize;
let e = self.indptr[m + 1] as usize;
indices.extend_from_slice(&self.indices[s..e]);
data.extend_from_slice(&self.data[s..e]);
if let (Some(dst), Some(src)) = (data_2.as_mut(), self.data_2.as_ref()) {
dst.extend_from_slice(&src[s..e]);
}
indptr.push(indices.len() as u32);
}
let shape = match self.cs_type {
CompressedSparseFormat::Csr => (keep.len(), self.shape.1),
CompressedSparseFormat::Csc => (self.shape.0, keep.len()),
};
let out = Self {
data,
indices,
indptr,
cs_type: self.cs_type,
data_2,
shape,
};
out.assert_invariants();
Ok(out)
}
fn slice_minor(&self, keep: &[usize]) -> Result<Self, SparseErrors> {
let minor = self.minor_dim();
let mut map: Vec<Option<usize>> = vec![None; minor];
for (new_idx, &old) in keep.iter().enumerate() {
if old >= minor {
return Err(SparseErrors::SliceIndexOutOfBounds {
index: old,
len: minor,
});
}
if map[old].is_some() {
return Err(SparseErrors::DuplicateSliceIndex(old));
}
map[old] = Some(new_idx);
}
let major = self.major_dim();
let mut indptr = Vec::with_capacity(major + 1);
indptr.push(0u32);
let mut indices = Vec::new();
let mut data = Vec::new();
let mut data_2 = self.data_2.as_ref().map(|_| Vec::new());
let mut kept: Vec<(usize, usize)> = Vec::new(); for m in 0..major {
let s = self.indptr[m];
let e = self.indptr[m + 1];
kept.clear();
for p in s..e {
if let Some(nm) = map[self.indices[p as usize] as usize] {
kept.push((nm, p as usize));
}
}
kept.sort_unstable_by_key(|&(nm, _)| nm);
for &(nm, p) in &kept {
indices.push(nm as u32);
data.push(self.data[p]);
if let (Some(dst), Some(src)) = (data_2.as_mut(), self.data_2.as_ref()) {
dst.push(src[p]);
}
}
indptr.push(indices.len() as u32);
}
let shape = match self.cs_type {
CompressedSparseFormat::Csr => (self.shape.0, keep.len()),
CompressedSparseFormat::Csc => (keep.len(), self.shape.1),
};
let out = Self {
data,
indices,
indptr,
cs_type: self.cs_type,
data_2,
shape,
};
out.assert_invariants();
Ok(out)
}
pub fn slice_rows(&self, rows: &[usize]) -> Result<Self, SparseErrors> {
match self.cs_type {
CompressedSparseFormat::Csr => self.slice_major(rows),
CompressedSparseFormat::Csc => self.slice_minor(rows),
}
}
pub fn slice_cols(&self, cols: &[usize]) -> Result<Self, SparseErrors> {
match self.cs_type {
CompressedSparseFormat::Csr => self.slice_minor(cols),
CompressedSparseFormat::Csc => self.slice_major(cols),
}
}
pub fn slice(&self, rows: &[usize], cols: &[usize]) -> Result<Self, SparseErrors> {
self.slice_rows(rows)?.slice_cols(cols)
}
pub fn from_entry_fn<G>(
nrows: usize,
ncols: usize,
format: CompressedSparseFormat,
get: G,
) -> Self
where
T: BixverseNumeric + Float,
G: Fn(usize, usize) -> T,
{
let (major, minor) = match format {
CompressedSparseFormat::Csr => (nrows, ncols),
CompressedSparseFormat::Csc => (ncols, nrows),
};
let mut data = Vec::new();
let mut indices: Vec<u32> = Vec::new();
let mut indptr: Vec<u32> = Vec::with_capacity(major + 1);
indptr.push(0);
for outer in 0..major {
for inner in 0..minor {
let val = match format {
CompressedSparseFormat::Csr => get(outer, inner),
CompressedSparseFormat::Csc => get(inner, outer),
};
if val != T::zero() {
data.push(val);
indices.push(inner as u32);
}
}
indptr.push(data.len() as u32);
}
Self::from_parts(data, indices, indptr, None, format, (nrows, ncols))
}
fn major_dim(&self) -> usize {
self.indptr.len() - 1
}
fn expected_major(&self) -> usize {
match self.cs_type {
CompressedSparseFormat::Csr => self.shape.0,
CompressedSparseFormat::Csc => self.shape.1,
}
}
fn minor_dim(&self) -> usize {
match self.cs_type {
CompressedSparseFormat::Csr => self.shape.1,
CompressedSparseFormat::Csc => self.shape.0,
}
}
pub fn validate(&self) -> Result<(), SparseErrors> {
let expected_major = self.expected_major();
if self.indptr.len() != expected_major + 1 {
return Err(SparseErrors::SparseIndptrInvalid {
detail: "length against the declared major axis",
expected: expected_major + 1,
got: self.indptr.len(),
});
}
let nnz = *self.indptr.last().expect("indptr length checked above") as usize;
if nnz != self.indices.len() {
return Err(SparseErrors::SparseIndptrInvalid {
detail: "final offset against the stored indices",
expected: self.indices.len(),
got: nnz,
});
}
if !self.indptr.windows(2).all(|w| w[0] <= w[1]) {
return Err(SparseErrors::SparseIndptrInvalid {
detail: "offsets must be non-decreasing",
expected: nnz,
got: nnz,
});
}
if !self.data.is_empty() && self.data.len() != self.indices.len() {
return Err(SparseErrors::DimensionMisMatchSparse {
indices_len: self.indices.len(),
data_len: self.data.len(),
});
}
if let Some(data_2) = &self.data_2
&& !data_2.is_empty()
&& data_2.len() != self.indices.len()
{
return Err(SparseErrors::DimensionMisMatchSparse {
indices_len: self.indices.len(),
data_len: data_2.len(),
});
}
let minor = self.minor_dim();
if let Some(&bad) = self
.indices
.par_iter()
.find_any(|&&idx| idx as usize >= minor)
{
return Err(SparseErrors::SliceIndexOutOfBounds {
index: bad as usize,
len: minor,
});
}
Ok(())
}
pub fn assert_invariants(&self) {
debug_assert_eq!(self.indices.len(), self.data.len());
debug_assert_eq!(self.indptr.len(), self.expected_major() + 1);
debug_assert_eq!(*self.indptr.last().unwrap() as usize, self.data.len());
if let Some(d2) = &self.data_2 {
debug_assert_eq!(d2.len(), self.data.len());
}
}
}
pub fn require_csr<T, U>(mat: &CompressedSparseData2<T, U>) -> Result<(), SparseErrors>
where
T: Clone,
U: Clone,
{
if mat.cs_type.is_csr() {
Ok(())
} else {
Err(SparseErrors::SparseMatrixMustBeCsr)
}
}
pub fn require_csc<T, U>(mat: &CompressedSparseData2<T, U>) -> Result<(), SparseErrors>
where
T: Clone,
U: Clone,
{
if mat.cs_type.is_csc() {
Ok(())
} else {
Err(SparseErrors::SparseMatrixMustBeCsc)
}
}
fn require_csr_pair_same_shape<T>(
a: &CompressedSparseData2<T>,
b: &CompressedSparseData2<T>,
) -> Result<(), SparseErrors>
where
T: Clone,
{
require_csr(a)?;
require_csr(b)?;
if a.shape != b.shape {
return Err(SparseErrors::ShapeMismatchSparse);
}
Ok(())
}
fn cumulate_indptr(indptr: &mut [u32]) {
for i in 1..indptr.len() {
indptr[i] += indptr[i - 1];
}
}
pub fn parse_compressed_sparse_format(s: &str) -> Option<CompressedSparseFormat> {
match s.to_lowercase().as_str() {
"csr" => Some(CompressedSparseFormat::Csr),
"csc" => Some(CompressedSparseFormat::Csc),
_ => None,
}
}
pub fn validate_square_csr<T>(graph: &CompressedSparseData2<T>) -> Result<usize, SparseErrors>
where
T: Clone,
{
if !graph.cs_type.is_csr() {
return Err(SparseErrors::SparseMatrixMustBeCsr);
}
let (rows, cols) = graph.shape;
if rows != cols {
return Err(SparseErrors::ShapeMismatch {
expected: (rows, rows),
got: (rows, cols),
});
}
if graph.indptr.len() != rows + 1 {
return Err(SparseErrors::MalformedCsr(
"indptr length must be the row count plus one",
));
}
if graph.indices.len() != graph.data.len() {
return Err(SparseErrors::MalformedCsr(
"indices and data must have the same length",
));
}
if graph.indptr.windows(2).any(|w| w[0] > w[1]) {
return Err(SparseErrors::MalformedCsr("indptr must be non-decreasing"));
}
if graph.indptr[rows] as usize != graph.indices.len() {
return Err(SparseErrors::MalformedCsr(
"the last indptr entry must equal the number of stored values",
));
}
if graph.indices.iter().any(|&j| j as usize >= rows) {
return Err(SparseErrors::MalformedCsr(
"a column index sits outside the matrix",
));
}
Ok(rows)
}
const PARALLEL_TRANSPOSE_MIN_NNZ: usize = 1 << 20;
const TRANSPOSE_MINOR_BAND: usize = 1024;
pub struct ScatterPtr<T>(pub *mut T);
unsafe impl<T: Send> Send for ScatterPtr<T> {}
unsafe impl<T: Send> Sync for ScatterPtr<T> {}
impl<T> ScatterPtr<T> {
#[inline(always)]
pub unsafe fn write(&self, pos: usize, value: T) {
unsafe { *self.0.add(pos) = value }
}
}
fn converted_layout<T, U>(
sparse_data: &CompressedSparseData2<T, U>,
) -> (usize, CompressedSparseFormat)
where
T: BixverseNumeric,
U: BixverseNumeric,
{
let new_type = match sparse_data.cs_type {
CompressedSparseFormat::Csc => CompressedSparseFormat::Csr,
CompressedSparseFormat::Csr => CompressedSparseFormat::Csc,
};
(sparse_data.minor_dim(), new_type)
}
fn transpose_cursors<T, U>(
sparse_data: &CompressedSparseData2<T, U>,
chunks: &[(usize, usize)],
new_major: usize,
) -> (Vec<u32>, Vec<u32>, bool)
where
T: BixverseNumeric,
U: BixverseNumeric,
{
let mut cursors = vec![0u32; chunks.len() * new_major];
let sorted = chunks
.par_iter()
.zip(cursors.par_chunks_mut(new_major.max(1)))
.map(|(&(start, end), counts)| {
let mut sorted = true;
for major in start..end {
let lo = sparse_data.indptr[major] as usize;
let hi = sparse_data.indptr[major + 1] as usize;
let run = &sparse_data.indices[lo..hi];
for &minor in run {
counts[minor as usize] += 1;
}
sorted &= run.windows(2).all(|w| w[0] < w[1]);
}
sorted
})
.reduce(|| true, |a, b| a && b);
let mut new_indptr = vec![0u32; new_major + 1];
for counts in cursors.chunks(new_major.max(1)) {
for (total, &count) in new_indptr[1..].iter_mut().zip(counts.iter()) {
*total += count;
}
}
cumulate_indptr(&mut new_indptr);
let mut running: Vec<u32> = new_indptr[..new_major].to_vec();
for counts in cursors.chunks_mut(new_major.max(1)) {
for (count, offset) in counts.iter_mut().zip(running.iter_mut()) {
let start = *offset;
*offset += *count;
*count = start;
}
}
(new_indptr, cursors, sorted)
}
#[allow(clippy::too_many_arguments)]
fn transpose_scatter<T, U>(
sparse_data: &CompressedSparseData2<T, U>,
chunks: &[(usize, usize)],
cursors: &mut [u32],
new_major: usize,
band: Option<usize>,
out_indices: &mut [u32],
out_data: Option<(&[T], &mut [T])>,
out_data2: Option<(&[U], &mut [U])>,
) where
T: BixverseNumeric,
U: BixverseNumeric,
{
let indices_ptr = ScatterPtr(out_indices.as_mut_ptr());
let (data_src, data_ptr) = match out_data {
Some((src, dst)) => (Some(src), Some(ScatterPtr(dst.as_mut_ptr()))),
None => (None, None),
};
let (data2_src, data2_ptr) = match out_data2 {
Some((src, dst)) => (Some(src), Some(ScatterPtr(dst.as_mut_ptr()))),
None => (None, None),
};
chunks
.par_iter()
.zip(cursors.par_chunks_mut(new_major.max(1)))
.for_each(|(&(start, end), cursor)| {
let mut read: Vec<usize> = (start..end)
.map(|major| sparse_data.indptr[major] as usize)
.collect();
let band = band.unwrap_or(usize::MAX).max(1);
let mut band_lo = 0usize;
while band_lo < new_major.max(1) {
let band_hi = band_lo.saturating_add(band);
for (major, rd) in (start..end).zip(read.iter_mut()) {
let hi = sparse_data.indptr[major + 1] as usize;
while *rd < hi {
let idx = *rd;
let minor = sparse_data.indices[idx] as usize;
if minor >= band_hi {
break;
}
*rd += 1;
let pos = cursor[minor] as usize;
cursor[minor] += 1;
unsafe {
indices_ptr.write(pos, major as u32);
if let (Some(src), Some(dst)) = (data_src, &data_ptr) {
dst.write(pos, src[idx]);
}
if let (Some(src), Some(dst)) = (data2_src, &data2_ptr) {
dst.write(pos, src[idx]);
}
}
}
}
band_lo = band_hi;
}
});
}
fn major_axis_chunks<T, U>(sparse_data: &CompressedSparseData2<T, U>) -> Vec<(usize, usize)>
where
T: BixverseNumeric,
U: BixverseNumeric,
{
let old_major_len = sparse_data.indptr.len() - 1;
if sparse_data.get_nnz() >= PARALLEL_TRANSPOSE_MIN_NNZ {
thread_chunks(old_major_len)
} else {
vec![(0, old_major_len)]
}
}
fn transpose_sparse_chunked<T, U>(
sparse_data: &CompressedSparseData2<T, U>,
chunks: &[(usize, usize)],
band: usize,
) -> CompressedSparseData2<T, U>
where
T: BixverseNumeric,
U: BixverseNumeric,
{
let nnz = sparse_data.get_nnz();
let (nrow, ncol) = sparse_data.shape();
let (new_major, new_type) = converted_layout(sparse_data);
let mut new_data: Vec<T> = vec![T::default(); nnz];
let mut new_indices: Vec<u32> = vec![0u32; nnz];
let mut new_data2: Option<Vec<U>> =
sparse_data.data_2.as_ref().map(|_| vec![U::default(); nnz]);
let (new_indptr, mut cursors, sorted) = transpose_cursors(sparse_data, chunks, new_major);
let data_pair = Some((sparse_data.data.as_slice(), new_data.as_mut_slice()));
let data2_pair = match (&sparse_data.data_2, &mut new_data2) {
(Some(src), Some(dst)) => Some((src.as_slice(), dst.as_mut_slice())),
_ => None,
};
transpose_scatter(
sparse_data,
chunks,
&mut cursors,
new_major,
sorted.then_some(band),
&mut new_indices,
data_pair,
data2_pair,
);
CompressedSparseData2 {
data: new_data,
indices: new_indices,
indptr: new_indptr,
cs_type: new_type,
data_2: new_data2,
shape: (nrow, ncol),
}
}
pub fn transpose_sparse<T, U>(
sparse_data: &CompressedSparseData2<T, U>,
) -> CompressedSparseData2<T, U>
where
T: BixverseNumeric,
U: BixverseNumeric,
{
transpose_sparse_chunked(
sparse_data,
&major_axis_chunks(sparse_data),
TRANSPOSE_MINOR_BAND,
)
}
pub fn transpose_sparse_single_layer<T, U>(
sparse_data: &CompressedSparseData2<T, U>,
use_second_layer: bool,
) -> Result<CompressedSparseData2<T, U>, SparseErrors>
where
T: BixverseNumeric,
U: BixverseNumeric,
{
let nnz = sparse_data.get_nnz();
let (nrow, ncol) = sparse_data.shape();
let (new_major, new_type) = converted_layout(sparse_data);
let mut new_indices: Vec<u32> = vec![0u32; nnz];
let (mut new_data, mut new_data2): (Vec<T>, Option<Vec<U>>) = if use_second_layer {
(Vec::new(), Some(vec![U::default(); nnz]))
} else {
(vec![T::default(); nnz], None)
};
let chunks = major_axis_chunks(sparse_data);
let (new_indptr, mut cursors, sorted) = transpose_cursors(sparse_data, &chunks, new_major);
let (data_pair, data2_pair) = if use_second_layer {
let src = sparse_data
.data_2
.as_ref()
.ok_or(SparseErrors::Data2NotAvailable)?
.as_slice();
let dst = new_data2.as_mut().unwrap().as_mut_slice();
(None, Some((src, dst)))
} else {
(
Some((sparse_data.data.as_slice(), new_data.as_mut_slice())),
None,
)
};
transpose_scatter(
sparse_data,
&chunks,
&mut cursors,
new_major,
sorted.then_some(TRANSPOSE_MINOR_BAND),
&mut new_indices,
data_pair,
data2_pair,
);
Ok(CompressedSparseData2 {
data: new_data,
indices: new_indices,
indptr: new_indptr,
cs_type: new_type,
data_2: new_data2,
shape: (nrow, ncol),
})
}
pub fn coo_to_csr<T>(
rows: &[u32],
cols: &[u32],
vals: &[T],
shape: (usize, usize),
) -> CompressedSparseData2<T>
where
T: BixverseNumeric,
{
let n_rows = shape.0;
let mut entries: Vec<(u32, u32, T)> = rows
.iter()
.zip(cols.iter())
.zip(vals.iter())
.map(|((&r, &c), &v)| (r, c, v))
.collect();
entries.sort_unstable_by_key(|&(r, c, _)| (r, c));
let mut merged_entries = Vec::new();
if !entries.is_empty() {
let mut current = entries[0];
for &(r, c, v) in &entries[1..] {
if r == current.0 && c == current.1 {
current.2 += v;
} else {
if current.2 != T::default() {
merged_entries.push(current);
}
current = (r, c, v);
}
}
if current.2 != T::default() {
merged_entries.push(current);
}
}
let final_nnz = merged_entries.len();
let mut data = Vec::with_capacity(final_nnz);
let mut indices = Vec::with_capacity(final_nnz);
let mut indptr = vec![0u32; n_rows + 1];
for &(row, col, val) in &merged_entries {
data.push(val);
indices.push(col);
indptr[(row + 1) as usize] += 1;
}
cumulate_indptr(&mut indptr);
CompressedSparseData2::new_csr(&data, &indices, &indptr, None, shape)
}
pub fn coo_to_csr_presorted<T>(
rows: &[u32],
cols: &[u32],
vals: &[T],
shape: (usize, usize),
) -> CompressedSparseData2<T>
where
T: BixverseNumeric,
{
let n_rows = shape.0;
let nnz = rows.len();
let mut data = Vec::with_capacity(nnz);
let mut indices: Vec<u32> = Vec::with_capacity(nnz);
let mut indptr = vec![0u32; n_rows + 1];
unsafe {
data.set_len(nnz);
indices.set_len(nnz);
let data_ptr: *mut T = data.as_mut_ptr();
let indices_ptr: *mut u32 = indices.as_mut_ptr();
let indptr_ptr: *mut u32 = indptr.as_mut_ptr();
for i in 0..nnz {
*data_ptr.add(i) = *vals.get_unchecked(i);
*indices_ptr.add(i) = *cols.get_unchecked(i);
let row = *rows.get_unchecked(i) as usize;
*indptr_ptr.add(row + 1) += 1;
}
}
cumulate_indptr(&mut indptr);
CompressedSparseData2::new_csr(&data, &indices, &indptr, None, shape)
}
pub fn undirected_edges_to_csr<T>(n: usize, edges: &[(u32, u32, T)]) -> CompressedSparseData2<T>
where
T: BixverseNumeric,
{
debug_assert!(
edges
.iter()
.all(|&(lo, hi, _)| lo < hi && (hi as usize) < n),
"undirected_edges_to_csr wants lo < hi < n"
);
debug_assert!(
edges
.windows(2)
.all(|w| (w[0].0, w[0].1) < (w[1].0, w[1].1)),
"undirected_edges_to_csr wants strictly ascending, deduplicated (lo, hi)"
);
let mut indptr = vec![0u32; n + 1];
for &(lo, hi, _) in edges {
indptr[lo as usize + 1] += 1;
indptr[hi as usize + 1] += 1;
}
cumulate_indptr(&mut indptr);
let nnz = 2 * edges.len();
let mut indices = vec![0u32; nnz];
let mut data = vec![T::default(); nnz];
let mut cursor: Vec<u32> = indptr[..n].to_vec();
for &(lo, hi, v) in edges {
let pos = cursor[hi as usize] as usize;
indices[pos] = lo;
data[pos] = v;
cursor[hi as usize] += 1;
}
for &(lo, hi, v) in edges {
let pos = cursor[lo as usize] as usize;
indices[pos] = hi;
data[pos] = v;
cursor[lo as usize] += 1;
}
CompressedSparseData2::from_parts(
data,
indices,
indptr,
None,
CompressedSparseFormat::Csr,
(n, n),
)
}
const SPARSE_MERGE_ROW_CHUNK: usize = 1024;
const SPARSE_MERGE_EPSILON: f32 = 1e-9;
const SPARSE_MATMUL_ROW_CHUNK: usize = 256;
const NORMALISE_MIN_SUM: f64 = 1e-15;
struct SparseAccumulator<T>
where
T: Copy + Default + AddAssign,
{
values: Vec<T>,
indices: Vec<usize>,
flags: Vec<bool>,
}
impl<T> SparseAccumulator<T>
where
T: Copy + Default + AddAssign,
{
fn new(size: usize) -> Self {
Self {
values: vec![T::default(); size],
indices: Vec::with_capacity(size / 10),
flags: vec![false; size],
}
}
#[inline]
unsafe fn add(&mut self, idx: usize, val: T) {
unsafe {
if !*self.flags.get_unchecked(idx) {
*self.flags.get_unchecked_mut(idx) = true;
self.indices.push(idx);
*self.values.get_unchecked_mut(idx) = val;
} else {
*self.values.get_unchecked_mut(idx) += val;
}
}
}
#[inline]
fn extract_into(&mut self, indices_out: &mut Vec<usize>, data_out: &mut Vec<T>) -> usize {
self.indices.sort_unstable();
let n = self.indices.len();
unsafe {
for &i in &self.indices {
indices_out.push(i);
data_out.push(*self.values.get_unchecked(i));
*self.flags.get_unchecked_mut(i) = false;
*self.values.get_unchecked_mut(i) = T::default();
}
}
self.indices.clear();
n
}
}
pub fn require_matmul_operands<T>(
a: &CompressedSparseData2<T>,
b: &CompressedSparseData2<T>,
) -> Result<(), SparseErrors>
where
T: Clone,
{
require_csr(a)?;
require_csr(b)?;
let (ncol_a, nrow_b) = (a.shape.1, b.shape.0);
if ncol_a != nrow_b {
return Err(SparseErrors::SparseMatrixMultiplication {
n_col_a: ncol_a,
n_row_b: nrow_b,
});
}
Ok(())
}
fn merge_csr_rows<T, C, N>(
a: &CompressedSparseData2<T>,
b: &CompressedSparseData2<T>,
combine: C,
lone_b: N,
) -> CompressedSparseData2<T>
where
T: BixverseNumeric + Into<f64>,
C: Fn(T, T) -> T + Sync,
N: Fn(T) -> T + Sync,
{
let n_rows = a.shape.0;
let chunks: Vec<(Vec<u32>, Vec<T>, Vec<u32>)> = (0..n_rows.div_ceil(SPARSE_MERGE_ROW_CHUNK))
.into_par_iter()
.map(|chunk_idx| {
let start = chunk_idx * SPARSE_MERGE_ROW_CHUNK;
let end = (start + SPARSE_MERGE_ROW_CHUNK).min(n_rows);
let cap = (a.indptr[end] - a.indptr[start] + b.indptr[end] - b.indptr[start]) as usize;
let mut cols: Vec<u32> = Vec::with_capacity(cap);
let mut vals: Vec<T> = Vec::with_capacity(cap);
let mut lengths: Vec<u32> = Vec::with_capacity(end - start);
for i in start..end {
let before = cols.len();
let a_end = a.indptr[i + 1] as usize;
let b_end = b.indptr[i + 1] as usize;
let mut a_idx = a.indptr[i] as usize;
let mut b_idx = b.indptr[i] as usize;
while a_idx < a_end && b_idx < b_end {
let a_col = a.indices[a_idx];
let b_col = b.indices[b_idx];
if a_col < b_col {
cols.push(a_col);
vals.push(a.data[a_idx]);
a_idx += 1;
} else if b_col < a_col {
cols.push(b_col);
vals.push(lone_b(b.data[b_idx]));
b_idx += 1;
} else {
let val = combine(a.data[a_idx], b.data[b_idx]);
if val.into().abs() > SPARSE_MERGE_EPSILON as f64 {
cols.push(a_col);
vals.push(val);
}
a_idx += 1;
b_idx += 1;
}
}
cols.extend_from_slice(&a.indices[a_idx..a_end]);
vals.extend_from_slice(&a.data[a_idx..a_end]);
cols.extend_from_slice(&b.indices[b_idx..b_end]);
vals.extend(b.data[b_idx..b_end].iter().map(|&v| lone_b(v)));
lengths.push((cols.len() - before) as u32);
}
(cols, vals, lengths)
})
.collect();
let nnz: usize = chunks.iter().map(|(c, _, _)| c.len()).sum();
let mut indptr: Vec<u32> = Vec::with_capacity(n_rows + 1);
let mut indices: Vec<u32> = Vec::with_capacity(nnz);
let mut data: Vec<T> = Vec::with_capacity(nnz);
indptr.push(0);
for (cols, vals, lengths) in chunks {
let mut running = *indptr.last().unwrap_or(&0);
for len in lengths {
running += len;
indptr.push(running);
}
indices.extend_from_slice(&cols);
data.extend_from_slice(&vals);
}
CompressedSparseData2::from_parts(
data,
indices,
indptr,
None,
CompressedSparseFormat::Csr,
a.shape,
)
}
pub fn sparse_add_csr<T>(
a: &CompressedSparseData2<T>,
b: &CompressedSparseData2<T>,
) -> Result<CompressedSparseData2<T>, SparseErrors>
where
T: BixverseNumeric + Into<f64> + Add<Output = T>,
{
require_csr_pair_same_shape(a, b)?;
Ok(merge_csr_rows(a, b, |x, y| x + y, |y| y))
}
pub fn sparse_scalar_multiply_csr<T>(
a: &CompressedSparseData2<T>,
scalar: T,
) -> CompressedSparseData2<T>
where
T: BixverseNumeric,
<T as Mul>::Output: Send,
Vec<T>: FromParallelIterator<<T as Mul>::Output>,
{
let data: Vec<T> = a.data.par_iter().map(|&v| v * scalar).collect();
CompressedSparseData2::new_csr(&data, &a.indices, &a.indptr, None, a.shape)
}
pub fn sparse_subtract_csr<T>(
a: &CompressedSparseData2<T>,
b: &CompressedSparseData2<T>,
) -> Result<CompressedSparseData2<T>, SparseErrors>
where
T: BixverseNumeric + Into<f64>,
{
require_csr_pair_same_shape(a, b)?;
Ok(merge_csr_rows(a, b, |x, y| x - y, |y| T::default() - y))
}
pub fn sparse_multiply_elementwise_csr<T>(
a: &CompressedSparseData2<T>,
b: &CompressedSparseData2<T>,
) -> Result<CompressedSparseData2<T>, SparseErrors>
where
T: BixverseNumeric,
<T as std::ops::Add>::Output: std::cmp::PartialEq<T>,
{
require_csr_pair_same_shape(a, b)?;
let n_rows = a.shape.0;
let mut rows: Vec<u32> = Vec::new();
let mut cols: Vec<u32> = Vec::new();
let mut vals = Vec::new();
for i in 0..n_rows {
let a_start = a.indptr[i] as usize;
let a_end = a.indptr[i + 1] as usize;
let b_start = b.indptr[i] as usize;
let b_end = b.indptr[i + 1] as usize;
let mut a_idx = a_start;
let mut b_idx = b_start;
while a_idx < a_end && b_idx < b_end {
match a.indices[a_idx].cmp(&b.indices[b_idx]) {
std::cmp::Ordering::Less => {
a_idx += 1;
}
std::cmp::Ordering::Greater => {
b_idx += 1;
}
std::cmp::Ordering::Equal => {
let val = a.data[a_idx] * b.data[b_idx];
if val != T::default() {
rows.push(i as u32);
cols.push(a.indices[a_idx]);
vals.push(val);
}
a_idx += 1;
b_idx += 1;
}
}
}
}
Ok(coo_to_csr(&rows, &cols, &vals, a.shape))
}
pub fn normalise_csr_columns_l1<T>(csr: &mut CompressedSparseData2<T>)
where
T: BixverseNumeric + Into<f64>,
<T as std::ops::Add>::Output: std::cmp::PartialEq<T>,
{
assert!(csr.cs_type.is_csr(), "Matrix must be in CSR format");
let ncols = csr.shape.1;
let mut col_sums = vec![T::default(); ncols];
for (idx, &col) in csr.indices.iter().enumerate() {
col_sums[col as usize] += csr.data[idx]
}
for (idx, &col) in csr.indices.iter().enumerate() {
let sum = col_sums[col as usize];
if sum.into() > NORMALISE_MIN_SUM {
csr.data[idx] /= sum;
}
}
}
pub fn normalise_csr_rows_l1<T>(csr: &mut CompressedSparseData2<T>) -> Result<(), SparseErrors>
where
T: BixverseNumeric + Into<f64>,
T: std::iter::Sum<T>,
{
require_csr(csr)?;
let nrows = csr.shape.0;
for i in 0..nrows {
let start = csr.indptr[i] as usize;
let end = csr.indptr[i + 1] as usize;
let row_data_slice = &mut csr.data[start..end];
let row_sum: T = row_data_slice.iter().copied().sum();
if row_sum.into() <= NORMALISE_MIN_SUM {
return Err(SparseErrors::SparseMatrixIsolatedRow {
row: i,
row_sum: row_sum.into(),
});
}
for val in row_data_slice.iter_mut() {
*val /= row_sum;
}
}
Ok(())
}
pub fn eliminate_zeros_csr<T>(mat: CompressedSparseData2<T>) -> CompressedSparseData2<T>
where
T: BixverseNumeric,
<T as std::ops::Add>::Output: std::cmp::PartialEq<T>,
{
let mut rows: Vec<u32> = Vec::new();
let mut cols: Vec<u32> = Vec::new();
let mut vals = Vec::new();
let n_rows = mat.shape.0;
for i in 0..n_rows {
let start = mat.indptr[i] as usize;
let end = mat.indptr[i + 1] as usize;
for j in start..end {
if mat.data[j] != T::default() {
rows.push(i as u32);
cols.push(mat.indices[j]);
vals.push(mat.data[j]);
}
}
}
coo_to_csr(&rows, &cols, &vals, mat.shape)
}
pub fn csr_matvec<T>(mat: &CompressedSparseData2<T>, vec: &[T]) -> Result<Vec<T>, SparseErrors>
where
T: BixverseNumeric,
<T as std::ops::Add>::Output: std::cmp::PartialEq<T>,
{
require_csr(mat)?;
let mut result = vec![T::default(); mat.shape.0];
for i in 0..mat.shape.0 {
let row_start = mat.indptr[i] as usize;
let row_end = mat.indptr[i + 1] as usize;
let data = &mat.data[row_start..row_end];
let cols = &mat.indices[row_start..row_end];
let mut acc = [T::default(); 4];
let mut data_chunks = data.chunks_exact(4);
let mut col_chunks = cols.chunks_exact(4);
for (d, c) in (&mut data_chunks).zip(&mut col_chunks) {
for l in 0..4 {
acc[l] += d[l] * vec[c[l] as usize];
}
}
let mut sum = (acc[0] + acc[1]) + (acc[2] + acc[3]);
for (&d, &c) in data_chunks.remainder().iter().zip(col_chunks.remainder()) {
sum += d * vec[c as usize];
}
result[i] = sum;
}
Ok(result)
}
pub fn csr_vecmat<T>(vec: &[T], mat: &CompressedSparseData2<T>) -> Result<Vec<T>, SparseErrors>
where
T: BixverseNumeric,
{
require_csr(mat)?;
let n_cols = mat.ncols();
let mut out = vec![T::default(); n_cols];
for (j, &vj) in vec.iter().enumerate() {
if vj == T::default() {
continue;
}
let gs = mat.indptr[j] as usize;
let ge = mat.indptr[j + 1] as usize;
for q in gs..ge {
let t = mat.indices[q] as usize;
out[t] += vj * mat.data[q];
}
}
Ok(out)
}
pub fn csr_matmul_csr<T>(
a: &CompressedSparseData2<T>,
b: &CompressedSparseData2<T>,
) -> Result<CompressedSparseData2<T>, SparseErrors>
where
T: BixverseNumeric,
{
require_matmul_operands(a, b)?;
let nrows = a.shape.0;
let ncols = b.shape.1;
let n_chunks = nrows.div_ceil(SPARSE_MATMUL_ROW_CHUNK);
let chunks: Vec<(Vec<usize>, Vec<T>, Vec<usize>)> = (0..n_chunks)
.into_par_iter()
.map_init(
|| SparseAccumulator::new(ncols),
|acc, chunk_idx| {
let start = chunk_idx * SPARSE_MATMUL_ROW_CHUNK;
let end = ((chunk_idx + 1) * SPARSE_MATMUL_ROW_CHUNK).min(nrows);
let mut idx_buf = Vec::new();
let mut data_buf = Vec::new();
let mut lengths = Vec::with_capacity(end - start);
for i in start..end {
for a_idx in a.indptr[i] as usize..a.indptr[i + 1] as usize {
let k = a.indices[a_idx] as usize;
let a_val = a.data[a_idx];
for b_idx in b.indptr[k] as usize..b.indptr[k + 1] as usize {
unsafe {
acc.add(b.indices[b_idx] as usize, a_val * b.data[b_idx]);
}
}
}
lengths.push(acc.extract_into(&mut idx_buf, &mut data_buf));
}
(idx_buf, data_buf, lengths)
},
)
.collect();
let total_nnz: usize = chunks.iter().map(|(i, _, _)| i.len()).sum();
let mut indptr: Vec<u32> = Vec::with_capacity(nrows + 1);
indptr.push(0);
let mut running = 0usize;
for (_, _, lengths) in &chunks {
for &len in lengths {
running += len;
indptr.push(running as u32);
}
}
let mut indices: Vec<u32> = Vec::with_capacity(total_nnz);
let mut data = Vec::with_capacity(total_nnz);
for (idx_buf, data_buf, _) in chunks {
indices.extend(idx_buf.into_iter().map(|x| x as u32));
data.extend(data_buf);
}
Ok(CompressedSparseData2 {
data,
indices,
indptr,
cs_type: CompressedSparseFormat::Csr,
data_2: None,
shape: (nrows, ncols),
})
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
fn validatable_csr() -> CompressedSparseData2<f32, f32> {
CompressedSparseData2 {
data: vec![1.0, 2.0, 3.0, 4.0],
indices: vec![0, 3, 1, 2],
indptr: vec![0, 2, 2, 4],
cs_type: CompressedSparseFormat::Csr,
data_2: Some(vec![10.0, 20.0, 30.0, 40.0]),
shape: (3, 4),
}
}
#[test]
fn test_validate_accepts_a_well_formed_matrix() {
assert!(validatable_csr().validate().is_ok());
assert!(validatable_csr().transform().validate().is_ok());
}
#[test]
fn test_validate_accepts_an_empty_unused_layer() {
let single = validatable_csr().transform_single_layer(true).unwrap();
assert!(single.data.is_empty());
assert!(single.validate().is_ok());
}
#[test]
fn test_validate_rejects_a_short_indptr() {
let mut m = validatable_csr();
m.indptr = vec![0, 2, 4];
assert!(matches!(
m.validate(),
Err(SparseErrors::SparseIndptrInvalid {
expected: 4,
got: 3,
..
})
));
}
#[test]
fn test_validate_rejects_a_final_offset_past_the_indices() {
let mut m = validatable_csr();
m.indptr = vec![0, 2, 2, 9];
assert!(matches!(
m.validate(),
Err(SparseErrors::SparseIndptrInvalid {
expected: 4,
got: 9,
..
})
));
}
#[test]
fn test_validate_rejects_decreasing_offsets() {
let mut m = validatable_csr();
m.indptr = vec![0, 3, 1, 4];
assert!(matches!(
m.validate(),
Err(SparseErrors::SparseIndptrInvalid { .. })
));
}
#[test]
fn test_validate_rejects_an_index_past_the_minor_axis() {
let mut m = validatable_csr();
m.indices = vec![0, 3, 1, 4];
assert!(matches!(
m.validate(),
Err(SparseErrors::SliceIndexOutOfBounds { index: 4, len: 4 })
));
}
#[test]
fn test_validate_bounds_csc_by_the_row_count() {
let mut m = validatable_csr();
m.cs_type = CompressedSparseFormat::Csc;
m.shape = (4, 3);
assert!(m.validate().is_ok());
m.shape = (3, 3);
assert!(matches!(
m.validate(),
Err(SparseErrors::SliceIndexOutOfBounds { index: 3, len: 3 })
));
}
#[test]
fn test_validate_rejects_a_truncated_second_layer() {
let mut m = validatable_csr();
m.data_2 = Some(vec![10.0, 20.0]);
assert!(matches!(
m.validate(),
Err(SparseErrors::DimensionMisMatchSparse {
indices_len: 4,
data_len: 2
})
));
}
#[test]
fn test_parse_sparse_format() {
assert!(parse_compressed_sparse_format("csr").unwrap().is_csr());
assert!(parse_compressed_sparse_format("CSC").unwrap().is_csc());
assert!(parse_compressed_sparse_format("dense").is_none());
}
fn transpose_fixture() -> CompressedSparseData2<f64, f64> {
let data: Vec<f64> = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0];
let data2: Vec<f64> = data.iter().map(|v| v * 10.0).collect();
let indices: Vec<u32> = vec![0, 2, 1, 0, 1, 3, 2, 3];
let indptr: Vec<u32> = vec![0, 2, 3, 3, 6, 8];
CompressedSparseData2::new_csr(&data, &indices, &indptr, Some(&data2), (5, 4))
}
#[test]
fn test_transpose_chunked_matches_single_chunk() {
let csr = transpose_fixture();
let serial = transpose_sparse_chunked(&csr, &[(0, 5)], usize::MAX);
for (chunks, band) in [
(vec![(0, 1), (1, 3), (3, 5)], usize::MAX),
(vec![(0, 2), (2, 5)], usize::MAX),
(vec![(0, 1), (1, 2), (2, 3), (3, 4), (4, 5)], usize::MAX),
(vec![(0, 5)], 1),
(vec![(0, 2), (2, 5)], 1),
(vec![(0, 1), (1, 3), (3, 5)], 2),
(vec![(0, 2), (2, 5)], 3),
(vec![(0, 2), (2, 5)], 4),
] {
let parallel = transpose_sparse_chunked(&csr, &chunks, band);
assert_eq!(parallel.indptr, serial.indptr);
assert_eq!(parallel.indices, serial.indices);
assert_eq!(parallel.data, serial.data);
assert_eq!(parallel.data_2, serial.data_2);
assert_eq!(parallel.cs_type.is_csc(), serial.cs_type.is_csc());
assert_eq!(parallel.shape, serial.shape);
}
}
#[test]
fn test_transpose_unsorted_runs_ignore_band() {
let data: Vec<f64> = vec![1.0, 2.0, 3.0, 4.0, 5.0];
let indices: Vec<u32> = vec![2, 0, 3, 1, 0];
let indptr: Vec<u32> = vec![0, 2, 4, 5];
let csr =
CompressedSparseData2::<f64, f64>::new_csr(&data, &indices, &indptr, None, (3, 4));
let csc = transpose_sparse_chunked(&csr, &[(0, 1), (1, 3)], 1);
assert_eq!(csc.indptr, vec![0, 2, 3, 4, 5]);
assert_eq!(csc.indices, vec![0, 2, 1, 0, 1]);
assert_eq!(csc.data, vec![2.0, 5.0, 4.0, 1.0, 3.0]);
}
#[test]
fn test_transpose_round_trips_and_stays_sorted() {
let csr = transpose_fixture();
let csc = transpose_sparse(&csr);
assert!(csc.cs_type.is_csc());
for major in 0..csc.indptr.len() - 1 {
let lo = csc.indptr[major] as usize;
let hi = csc.indptr[major + 1] as usize;
assert!(csc.indices[lo..hi].windows(2).all(|w| w[0] < w[1]));
}
let round_trip = transpose_sparse(&csc);
assert_eq!(round_trip.indptr, csr.indptr);
assert_eq!(round_trip.indices, csr.indices);
assert_eq!(round_trip.data, csr.data);
assert_eq!(round_trip.data_2, csr.data_2);
}
#[test]
fn test_transpose_single_layer_matches_full_transpose() {
let csr = transpose_fixture();
let full = transpose_sparse(&csr);
let counts_only = csr.transform_single_layer(false).unwrap();
assert_eq!(counts_only.data, full.data);
assert!(counts_only.data_2.is_none());
let norm_only = csr.transform_single_layer(true).unwrap();
assert_eq!(norm_only.data_2, full.data_2);
assert!(norm_only.data.is_empty());
assert_eq!(counts_only.indptr, full.indptr);
assert_eq!(norm_only.indices, full.indices);
}
#[test]
fn test_undirected_edges_to_csr_is_symmetric_and_row_sorted() {
let edges = vec![(0u32, 1u32, 1.0f64), (0, 3, 2.0), (1, 2, 3.0), (2, 3, 4.0)];
let csr = undirected_edges_to_csr(4, &edges);
assert_eq!(csr.shape, (4, 4));
assert_eq!(csr.indptr, vec![0, 2, 4, 6, 8]);
assert_eq!(csr.indices, vec![1, 3, 0, 2, 1, 3, 0, 2]);
assert_eq!(csr.data, vec![1.0, 2.0, 1.0, 3.0, 3.0, 4.0, 2.0, 4.0]);
for i in 0..4 {
let lo = csr.indptr[i] as usize;
let hi = csr.indptr[i + 1] as usize;
assert!(csr.indices[lo..hi].windows(2).all(|w| w[0] < w[1]));
}
}
#[test]
fn test_undirected_edges_to_csr_handles_empty_input() {
let csr = undirected_edges_to_csr::<f64>(3, &[]);
assert_eq!(csr.shape, (3, 3));
assert_eq!(csr.indptr, vec![0, 0, 0, 0]);
assert!(csr.indices.is_empty());
}
#[test]
fn test_undirected_edges_to_csr_keeps_isolated_nodes() {
let edges = vec![(1u32, 2u32, 7.0f64)];
let csr = undirected_edges_to_csr(3, &edges);
assert_eq!(csr.indptr, vec![0, 0, 1, 2]);
assert_eq!(csr.indices, vec![2, 1]);
}
#[test]
fn test_validate_square_csr_accepts_a_sound_matrix() {
let csr = undirected_edges_to_csr(3, &[(0u32, 1u32, 1.0f64), (1, 2, 2.0)]);
assert_eq!(validate_square_csr(&csr).unwrap(), 3);
}
#[test]
fn test_validate_square_csr_catches_structural_faults() {
let bad_indptr = CompressedSparseData2::new_csr(
&[1.0f64; 7],
&[1u32, 2, 0, 2, 0, 0, 1],
&[0u32, 5, 2, 7],
None,
(3, 3),
);
assert!(matches!(
validate_square_csr(&bad_indptr),
Err(SparseErrors::MalformedCsr(_))
));
let bad_index = CompressedSparseData2::new_csr(
&[1.0f64, 1.0],
&[1u32, 9],
&[0u32, 1, 2, 2],
None,
(3, 3),
);
assert!(matches!(
validate_square_csr(&bad_index),
Err(SparseErrors::MalformedCsr(_))
));
let short_indptr =
CompressedSparseData2::new_csr(&[1.0f64], &[1u32], &[0u32, 1], None, (3, 3));
assert!(matches!(
validate_square_csr(&short_indptr),
Err(SparseErrors::MalformedCsr(_))
));
let short_tail = CompressedSparseData2::new_csr(
&[1.0f64, 1.0],
&[1u32, 0],
&[0u32, 1, 1, 1],
None,
(3, 3),
);
assert!(matches!(
validate_square_csr(&short_tail),
Err(SparseErrors::MalformedCsr(_))
));
}
#[test]
fn test_sparse_add_csr() {
let shape = (2, 2);
let a = CompressedSparseData2::<f64, f64>::new_csr(
&[1.0, 2.0],
&[0, 1],
&[0, 1, 2],
None,
shape,
);
let b = CompressedSparseData2::<f64, f64>::new_csr(
&[3.0, 4.0],
&[1, 1],
&[0, 1, 2],
None,
shape,
);
let c = sparse_add_csr(&a, &b).unwrap();
assert_eq!(c.data, vec![1.0, 3.0, 6.0]);
assert_eq!(c.indices, vec![0, 1, 1]);
assert_eq!(c.indptr, vec![0, 2, 3]);
}
#[test]
fn test_csr_matvec() {
let a = CompressedSparseData2::<f64, f64>::new_csr(
&[1.0, 2.0, 3.0],
&[0, 1, 1],
&[0, 2, 3],
None,
(2, 2),
);
let vec = vec![2.0, 1.0];
let result = csr_matvec(&a, &vec).unwrap();
assert_eq!(result, vec![4.0, 3.0]);
}
#[test]
fn test_normalise_csr_rows_l1_isolated_row_errors() {
let data = vec![1.0f64, 3.0, 0.0];
let indices = vec![0u32, 2, 1];
let indptr = vec![0u32, 2, 2, 3];
let mut csr = CompressedSparseData2::new_csr(&data, &indices, &indptr, None, (3, 3));
match normalise_csr_rows_l1(&mut csr) {
Err(SparseErrors::SparseMatrixIsolatedRow { row, row_sum }) => {
assert_eq!(row, 1);
assert_eq!(row_sum, 0.0);
}
other => panic!("expected SparseMatrixIsolatedRow, got {:?}", other),
}
}
#[test]
fn test_normalise_csr_rows_l1_rows_sum_to_one() {
let data = vec![1.0f64, 3.0, 2.0, 2.0, 5.0];
let indices = vec![0u32, 2, 1, 2, 0];
let indptr = vec![0u32, 2, 4, 5];
let mut csr = CompressedSparseData2::new_csr(&data, &indices, &indptr, None, (3, 3));
normalise_csr_rows_l1(&mut csr).unwrap();
for i in 0..3 {
let lo = csr.indptr[i] as usize;
let hi = csr.indptr[i + 1] as usize;
let sum: f64 = csr.data[lo..hi].iter().sum();
assert_relative_eq!(sum, 1.0, epsilon = 1e-12);
}
assert_relative_eq!(csr.data[0], 0.25, epsilon = 1e-12);
assert_relative_eq!(csr.data[1], 0.75, epsilon = 1e-12);
}
}