use crate::compressed::{check_compressed, transpose};
use crate::index::{Col, Row, dim_to_u32};
use crate::real::{Real, is_exact_zero};
use crate::{CsrMatrix, SparseError};
#[derive(Clone, Debug, PartialEq)]
pub struct CscMatrix<T: Real = f64> {
pub n_rows: usize,
pub col_ptr: Vec<usize>,
pub row_idx: Vec<u32>,
pub val: Vec<T>,
}
impl<T: Real> CscMatrix<T> {
pub fn new(n_rows: usize, n_cols: usize) -> Self {
Self::zeros(n_rows, n_cols)
}
pub fn zeros(n_rows: usize, n_cols: usize) -> Self {
assert!(
dim_to_u32("rows", n_rows).is_ok(),
"row count exceeds u32::MAX"
);
assert!(
dim_to_u32("columns", n_cols).is_ok(),
"column count exceeds u32::MAX"
);
Self {
n_rows,
col_ptr: vec![0; n_cols + 1],
row_idx: Vec::new(),
val: Vec::new(),
}
}
pub fn from_raw_parts(
n_rows: usize,
col_ptr: Vec<usize>,
row_idx: Vec<u32>,
val: Vec<T>,
) -> Result<Self, SparseError> {
let a = Self {
n_rows,
col_ptr,
row_idx,
val,
};
a.check_invariants()?;
Ok(a)
}
pub fn from_triplets(
n_rows: usize,
n_cols: usize,
triplets: &[(Row, Col, T)],
) -> Result<Self, SparseError> {
dim_to_u32("rows", n_rows)?;
dim_to_u32("columns", n_cols)?;
for (k, &(i, j, v)) in triplets.iter().enumerate() {
if i.index() >= n_rows {
return Err(SparseError::IndexOutOfRange {
what: "row",
index: i.index(),
bound: n_rows,
});
}
if j.index() >= n_cols {
return Err(SparseError::IndexOutOfRange {
what: "column",
index: j.index(),
bound: n_cols,
});
}
if !v.is_finite() {
return Err(SparseError::InvalidValue {
what: "triplet value",
index: k,
value: v.to_f64(),
});
}
}
let mut rptr = vec![0_usize; n_rows + 1];
for &(i, _, _) in triplets {
rptr[i.index() + 1] += 1;
}
for i in 0..n_rows {
rptr[i + 1] += rptr[i];
}
let mut next = rptr[..n_rows].to_vec();
let mut cidx = vec![0_u32; triplets.len()];
let mut cval = vec![T::ZERO; triplets.len()];
for &(i, j, v) in triplets {
let q = next[i.index()];
next[i.index()] += 1;
cidx[q] = j.0;
cval[q] = v;
}
let (col_ptr, mut row_idx, mut val) = transpose(n_cols, &rptr, &cidx, &cval);
let mut out_ptr = Vec::with_capacity(n_cols + 1);
out_ptr.push(0);
let mut w = 0;
for (j, win) in col_ptr.windows(2).enumerate() {
let (mut p, end) = (win[0], win[1]);
while p < end {
let i = row_idx[p];
let mut s = val[p];
p += 1;
while p < end && row_idx[p] == i {
s += val[p];
p += 1;
}
if !s.is_finite() {
return Err(SparseError::InvalidValue {
what: "sum of duplicate entries in column",
index: j,
value: s.to_f64(),
});
}
if !is_exact_zero(s) {
row_idx[w] = i;
val[w] = s;
w += 1;
}
}
out_ptr.push(w);
}
row_idx.truncate(w);
val.truncate(w);
let a = Self {
n_rows,
col_ptr: out_ptr,
row_idx,
val,
};
debug_assert!(a.check_invariants().is_ok(), "{:?}", a.check_invariants());
Ok(a)
}
#[inline]
pub fn n_rows(&self) -> usize {
self.n_rows
}
#[inline]
pub fn n_cols(&self) -> usize {
self.col_ptr.len() - 1
}
#[inline]
pub fn nnz(&self) -> usize {
self.val.len()
}
#[inline]
pub fn col(&self, j: Col) -> (&[u32], &[T]) {
let r = self.col_ptr[j.index()]..self.col_ptr[j.index() + 1];
(&self.row_idx[r.clone()], &self.val[r])
}
pub fn get(&self, i: Row, j: Col) -> T {
let (rows, vals) = self.col(j);
rows.binary_search(&i.0).map_or(T::ZERO, |p| vals[p])
}
pub fn gaxpy(&self, alpha: T, x: &[T], y: &mut [T]) {
assert_eq!(x.len(), self.n_cols(), "gaxpy: x has the wrong length");
assert_eq!(y.len(), self.n_rows, "gaxpy: y has the wrong length");
for (j, &xj) in x.iter().enumerate() {
if is_exact_zero(xj) {
continue;
}
let t = alpha * xj;
let (rows, vals) = self.col(Col::from_usize(j));
for (&i, &v) in rows.iter().zip(vals) {
y[i as usize] += v * t;
}
}
}
pub fn to_csr(&self) -> CsrMatrix<T> {
let (row_ptr, col_idx, val) =
transpose(self.n_rows, &self.col_ptr, &self.row_idx, &self.val);
let r = CsrMatrix {
n_cols: self.n_cols(),
row_ptr,
col_idx,
val,
};
debug_assert!(r.check_invariants().is_ok(), "{:?}", r.check_invariants());
r
}
#[must_use]
pub fn transpose(&self) -> CscMatrix<T> {
let (col_ptr, row_idx, val) =
transpose(self.n_rows, &self.col_ptr, &self.row_idx, &self.val);
let t = CscMatrix {
n_rows: self.n_cols(),
col_ptr,
row_idx,
val,
};
debug_assert!(t.check_invariants().is_ok(), "{:?}", t.check_invariants());
t
}
pub fn check_invariants(&self) -> Result<(), SparseError> {
check_compressed(
"column",
"row",
self.n_rows,
&self.col_ptr,
&self.row_idx,
&self.val,
)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::real::approx_eq;
use proptest::prelude::*;
fn dense_reference(m: usize, n: usize, t: &[(Row, Col, f64)]) -> Vec<Vec<f64>> {
let mut d = vec![vec![0.0; n]; m];
for &(i, j, v) in t {
d[i.index()][j.index()] += v;
}
d
}
fn triplets() -> impl Strategy<Value = (usize, usize, Vec<(Row, Col, f64)>)> {
(0_usize..8, 0_usize..8).prop_flat_map(|(m, n)| {
let entries = if m == 0 || n == 0 {
Just(Vec::new()).boxed()
} else {
proptest::collection::vec(
(0..m, 0..n, -3_i8..=3).prop_map(|(i, j, v)| {
(Row::from_usize(i), Col::from_usize(j), f64::from(v))
}),
0..40,
)
.boxed()
};
(Just(m), Just(n), entries)
})
}
#[test]
fn ex_1_1_small_example() {
let t = [
(Row(1), Col(2), 3.0),
(Row(0), Col(2), 0.5),
(Row(0), Col(0), 1.0),
(Row(1), Col(0), 0.0),
(Row(0), Col(2), 1.5),
];
let a = CscMatrix::from_triplets(2, 3, &t).unwrap();
assert_eq!(a.col_ptr, vec![0, 1, 1, 3]);
assert_eq!(a.row_idx, vec![0, 0, 1]);
assert_eq!(a.val, vec![1.0, 2.0, 3.0]);
assert!(approx_eq(a.get(Row(0), Col(2)), 2.0, 0.0));
assert!(is_exact_zero(a.get(Row(1), Col(1))));
}
#[test]
fn rejects_bad_input() {
let oob = CscMatrix::from_triplets(1, 1, &[(Row(1), Col(0), 1.0)]);
assert!(matches!(
oob,
Err(SparseError::IndexOutOfRange { what: "row", .. })
));
let nan = CscMatrix::from_triplets(1, 1, &[(Row(0), Col(0), f64::NAN)]);
assert!(matches!(nan, Err(SparseError::InvalidValue { .. })));
let ovf = CscMatrix::from_triplets(
1,
1,
&[(Row(0), Col(0), f64::MAX), (Row(0), Col(0), f64::MAX)],
);
assert!(matches!(ovf, Err(SparseError::InvalidValue { .. })));
}
#[test]
fn raw_parts_are_validated() {
assert!(CscMatrix::from_raw_parts(2, vec![0, 2], vec![0, 1], vec![1.0, 2.0]).is_ok());
assert!(CscMatrix::from_raw_parts(2, vec![0, 2], vec![1, 0], vec![1.0, 2.0]).is_err());
assert!(CscMatrix::from_raw_parts(2, vec![0, 2], vec![0, 0], vec![1.0, 2.0]).is_err());
assert!(CscMatrix::from_raw_parts(2, vec![0, 1], vec![0], vec![0.0]).is_err());
assert!(CscMatrix::from_raw_parts(1, vec![0, 1], vec![1], vec![1.0]).is_err());
assert!(CscMatrix::<f64>::from_raw_parts(2, vec![1, 1], vec![], vec![]).is_err());
assert!(CscMatrix::from_raw_parts(2, vec![0, 1, 0], vec![0], vec![1.0]).is_err());
assert!(CscMatrix::from_raw_parts(2, vec![0, 2], vec![0], vec![1.0]).is_err());
assert!(CscMatrix::<f64>::from_raw_parts(2, vec![], vec![], vec![]).is_err());
}
#[test]
fn gaxpy_matches_the_dense_product() {
let a = CscMatrix::from_raw_parts(2, vec![0, 1, 1, 3], vec![0, 0, 1], vec![1.0, 2.0, 3.0])
.unwrap();
let mut y = vec![1.0, -1.0];
a.gaxpy(2.0, &[1.0, 5.0, -1.0], &mut y);
assert!(approx_eq(y[0], 1.0 + 2.0 * (1.0 - 2.0), 0.0));
assert!(approx_eq(y[1], -1.0 + 2.0 * (-3.0), 0.0));
assert_eq!(CscMatrix::<f64>::new(2, 3), CscMatrix::zeros(2, 3));
}
#[test]
fn zeros_is_valid() {
let z = CscMatrix::<f64>::zeros(3, 4);
assert!(z.check_invariants().is_ok());
assert_eq!((z.n_rows(), z.n_cols(), z.nnz()), (3, 4, 0));
assert_eq!(z.to_csr().to_csc(), z);
}
proptest! {
#[test]
fn triplets_match_dense_reference((m, n, t) in triplets()) {
let a = CscMatrix::from_triplets(m, n, &t).unwrap();
prop_assert!(a.check_invariants().is_ok());
let d = dense_reference(m, n, &t);
let mut nnz = 0;
for (i, row) in d.iter().enumerate() {
for (j, &dij) in row.iter().enumerate() {
let aij = a.get(Row::from_usize(i), Col::from_usize(j));
prop_assert!(approx_eq(aij, dij, 0.0));
nnz += usize::from(!is_exact_zero(dij));
}
}
prop_assert_eq!(a.nnz(), nnz);
}
#[test]
fn csc_csr_round_trip((m, n, t) in triplets()) {
let a = CscMatrix::from_triplets(m, n, &t).unwrap();
let r = a.to_csr();
prop_assert!(r.check_invariants().is_ok());
prop_assert_eq!((r.n_rows(), r.n_cols(), r.nnz()), (m, n, a.nnz()));
for i in 0..m {
for j in 0..n {
let (i, j) = (Row::from_usize(i), Col::from_usize(j));
prop_assert!(approx_eq(r.get(i, j), a.get(i, j), 0.0));
}
}
prop_assert_eq!(r.to_csc(), a.clone());
prop_assert_eq!(a.transpose().transpose(), a);
}
}
}