use crate::compressed::transpose;
use crate::index::dim_to_u32;
use crate::real::{Real, is_exact_zero};
use crate::{CscMatrix, SparseError};
#[derive(Clone, Debug, Default)]
pub struct Triplets<T: Real = f64> {
pub n_rows: usize,
pub n_cols: usize,
pub row: Vec<u32>,
pub col: Vec<u32>,
pub val: Vec<T>,
}
impl<T: Real> Triplets<T> {
pub fn new(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,
n_cols,
row: Vec::new(),
col: Vec::new(),
val: Vec::new(),
}
}
pub fn from_csc(a: &CscMatrix<T>) -> Self {
let mut t = Self::new(a.n_rows(), a.n_cols());
t.reserve(a.nnz());
for (j, w) in a.col_ptr.windows(2).enumerate() {
let jj = u32::try_from(j).expect("column count exceeds u32::MAX");
for p in w[0]..w[1] {
t.row.push(a.row_idx[p]);
t.col.push(jj);
t.val.push(a.val[p]);
}
}
t
}
#[inline]
pub fn push(&mut self, i: u32, j: u32, v: T) {
self.row.push(i);
self.col.push(j);
self.val.push(v);
}
#[inline]
pub fn len(&self) -> usize {
self.val.len()
}
#[inline]
pub fn is_empty(&self) -> bool {
self.val.is_empty()
}
pub fn clear(&mut self) {
self.row.clear();
self.col.clear();
self.val.clear();
}
pub fn reserve(&mut self, additional: usize) {
self.row.reserve(additional);
self.col.reserve(additional);
self.val.reserve(additional);
}
pub fn check_invariants(&self) -> Result<(), SparseError> {
let k = self.row.len();
if self.col.len() != k {
return Err(SparseError::Dimension {
what: "triplet column array",
expected: k,
found: self.col.len(),
});
}
if self.val.len() != k {
return Err(SparseError::Dimension {
what: "triplet value array",
expected: k,
found: self.val.len(),
});
}
dim_to_u32("rows", self.n_rows)?;
dim_to_u32("columns", self.n_cols)?;
for (k, (&i, &j)) in self.row.iter().zip(&self.col).enumerate() {
if i as usize >= self.n_rows {
return Err(SparseError::IndexOutOfRange {
what: "row",
index: i as usize,
bound: self.n_rows,
});
}
if j as usize >= self.n_cols {
return Err(SparseError::IndexOutOfRange {
what: "column",
index: j as usize,
bound: self.n_cols,
});
}
let v = self.val[k];
if !v.is_finite() {
return Err(SparseError::InvalidValue {
what: "triplet value",
index: k,
value: v.to_f64(),
});
}
}
Ok(())
}
pub fn to_csc(&self) -> Result<CscMatrix<T>, SparseError> {
self.check_invariants()?;
let (m, n, k) = (self.n_rows, self.n_cols, self.len());
let mut rptr = vec![0_usize; m + 1];
for &i in &self.row {
rptr[i as usize + 1] += 1;
}
for i in 0..m {
rptr[i + 1] += rptr[i];
}
let mut next = rptr[..m].to_vec();
let mut cidx = vec![0_u32; k];
let mut cval = vec![T::ZERO; k];
for ((&i, &j), &v) in self.row.iter().zip(&self.col).zip(&self.val) {
let q = next[i as usize];
next[i as usize] += 1;
cidx[q] = j;
cval[q] = v;
}
let (col_ptr, mut row_idx, mut val) = transpose(n, &rptr, &cidx, &cval);
let mut out_ptr = Vec::with_capacity(n + 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 = CscMatrix {
n_rows: m,
col_ptr: out_ptr,
row_idx,
val,
};
debug_assert!(a.check_invariants().is_ok(), "{:?}", a.check_invariants());
Ok(a)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::real::approx_eq;
use crate::{Col, Row};
use proptest::prelude::*;
#[test]
fn ex_5_8_4_small_example() {
let mut t = Triplets::new(2, 3);
t.push(1, 2, 3.0);
t.push(0, 2, 0.5);
t.push(0, 0, 1.0);
t.push(1, 0, 0.0);
t.push(0, 2, 1.5);
assert_eq!((t.len(), t.is_empty()), (5, false));
assert!(t.check_invariants().is_ok());
let a = t.to_csc().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]);
let c = CscMatrix::from_triplets(
2,
3,
&[
(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),
],
)
.unwrap();
assert_eq!(a, c);
t.clear();
assert!(t.is_empty());
assert_eq!(t.to_csc().unwrap(), CscMatrix::zeros(2, 3));
t.reserve(10);
assert!(t.row.capacity() >= 10 && t.col.capacity() >= 10 && t.val.capacity() >= 10);
}
#[test]
fn duplicates_are_summed_in_insertion_order_without_reassociation() {
let mut t = Triplets::new(1, 1);
t.push(0, 0, 1e16);
t.push(0, 0, 1.0);
t.push(0, 0, -1e16);
let a = t.to_csc().unwrap();
assert_eq!(a.nnz(), 0);
assert!(a.check_invariants().is_ok());
let mut t = Triplets::new(1, 1);
t.push(0, 0, 1e16);
t.push(0, 0, -1e16);
t.push(0, 0, 1.0);
let a = t.to_csc().unwrap();
assert_eq!(a.val, vec![1.0]);
}
#[test]
fn zeros_are_accepted_and_not_stored() {
let mut t = Triplets::new(2, 2);
t.push(0, 0, 0.0);
t.push(1, 1, 2.0);
t.push(1, 1, 0.0);
t.push(0, 1, 1.0);
t.push(0, 1, -1.0);
let a = t.to_csc().unwrap();
assert_eq!(a.col_ptr, vec![0, 0, 1]);
assert_eq!(a.row_idx, vec![1]);
assert_eq!(a.val, vec![2.0]);
}
#[test]
fn rejects_bad_input() {
let mut t = Triplets::new(1, 1);
t.push(1, 0, 1.0);
assert!(matches!(
t.to_csc(),
Err(SparseError::IndexOutOfRange {
what: "row",
index: 1,
bound: 1
})
));
let mut t = Triplets::new(1, 1);
t.push(0, 3, 1.0);
assert!(matches!(
t.to_csc(),
Err(SparseError::IndexOutOfRange {
what: "column",
index: 3,
bound: 1
})
));
let mut t = Triplets::new(1, 1);
t.push(0, 0, f64::NAN);
assert!(matches!(
t.to_csc(),
Err(SparseError::InvalidValue {
what: "triplet value",
index: 0,
..
})
));
let mut t = Triplets::new(1, 1);
t.push(0, 0, f64::MAX);
t.push(0, 0, f64::MAX);
assert!(matches!(
t.to_csc(),
Err(SparseError::InvalidValue {
what: "sum of duplicate entries in column",
index: 0,
..
})
));
let t = Triplets {
n_rows: 1,
n_cols: 1,
row: vec![0],
col: vec![],
val: vec![1.0],
};
assert!(matches!(
t.check_invariants(),
Err(SparseError::Dimension {
what: "triplet column array",
expected: 1,
found: 0
})
));
let t = Triplets {
n_rows: 1,
n_cols: 1,
row: vec![0],
col: vec![0],
val: Vec::<f64>::new(),
};
assert!(matches!(
t.to_csc(),
Err(SparseError::Dimension {
what: "triplet value array",
..
})
));
#[cfg(target_pointer_width = "64")]
{
let t = Triplets::<f64> {
n_rows: 1 << 33,
..Triplets::new(0, 0)
};
assert!(matches!(t.to_csc(), Err(SparseError::TooLarge { .. })));
}
}
#[test]
#[cfg(target_pointer_width = "64")]
#[should_panic(expected = "exceeds u32::MAX")]
fn new_rejects_a_dimension_beyond_u32() {
let _ = Triplets::<f64>::new(1 << 33, 1);
}
#[test]
fn from_csc_lists_the_entries_column_by_column() {
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 t = Triplets::from_csc(&a);
assert_eq!((t.n_rows, t.n_cols, t.len()), (2, 3, 3));
assert_eq!(t.row, vec![0, 0, 1]);
assert_eq!(t.col, vec![0, 2, 2]);
assert_eq!(t.val, vec![1.0, 2.0, 3.0]);
assert_eq!(t.to_csc().unwrap(), a);
}
fn triplets() -> impl Strategy<Value = (usize, usize, Vec<(u32, u32, 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)| {
(
u32::try_from(i).unwrap(),
u32::try_from(j).unwrap(),
f64::from(v),
)
}),
0..40,
)
.boxed()
};
(Just(m), Just(n), entries)
})
}
fn csc_and_shuffle() -> impl Strategy<Value = (CscMatrix, Vec<usize>, Vec<(u32, u32)>)> {
triplets().prop_flat_map(|(m, n, t)| {
let mut b = Triplets::new(m, n);
for &(i, j, v) in &t {
b.push(i, j, v);
}
let a = b.to_csc().unwrap();
let extra = if m == 0 || n == 0 {
Just(Vec::new()).boxed()
} else {
proptest::collection::vec(
(0..m, 0..n)
.prop_map(|(i, j)| (u32::try_from(i).unwrap(), u32::try_from(j).unwrap())),
0..6,
)
.boxed()
};
let nnz = a.nnz();
(Just(a), extra).prop_flat_map(move |(a, extra)| {
let order: Vec<usize> = (0..nnz + extra.len()).collect();
(Just(a), Just(order).prop_shuffle(), Just(extra))
})
})
}
proptest! {
#[test]
fn c_prime_agrees_with_c((m, n, t) in triplets()) {
let mut b = Triplets::new(m, n);
for &(i, j, v) in &t {
b.push(i, j, v);
}
prop_assert_eq!(b.len(), t.len());
prop_assert!(b.check_invariants().is_ok());
let a = b.to_csc().unwrap();
prop_assert!(a.check_invariants().is_ok());
let tuples: Vec<(Row, Col, f64)> =
t.iter().map(|&(i, j, v)| (Row(i), Col(j), v)).collect();
let c = CscMatrix::from_triplets(m, n, &tuples).unwrap();
prop_assert_eq!(a, c);
}
#[test]
fn round_trip_in_any_order_with_zeros((a, order, extra) in csc_and_shuffle()) {
let t = Triplets::from_csc(&a);
prop_assert_eq!(t.to_csc().unwrap(), a.clone());
let nnz = a.nnz();
let mut shuffled = Triplets::new(a.n_rows(), a.n_cols());
for &k in &order {
if k < nnz {
shuffled.push(t.row[k], t.col[k], t.val[k]);
} else {
let (i, j) = extra[k - nnz];
shuffled.push(i, j, 0.0);
}
}
prop_assert_eq!(shuffled.len(), nnz + extra.len());
let back = shuffled.to_csc().unwrap();
prop_assert!(back.check_invariants().is_ok());
prop_assert_eq!(back, a);
}
#[test]
fn matches_the_dense_reference((m, n, t) in triplets()) {
let mut d = vec![vec![0.0; n]; m];
let mut b = Triplets::new(m, n);
for &(i, j, v) in &t {
d[i as usize][j as usize] += v;
b.push(i, j, v);
}
let a = b.to_csc().unwrap();
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);
}
}
}