use data_beans::sparse_io::*;
type Shape = (usize, usize, usize);
fn reference() -> (Vec<(u64, u64, f32)>, Shape) {
let triplets = vec![
(0u64, 0u64, 1.0f32),
(2, 0, 2.0),
(1, 1, 3.0),
(0, 3, 4.0),
(1, 3, 5.0),
(3, 3, 6.0),
(2, 4, 7.0),
];
(triplets, (4, 5, 7))
}
fn slab(triplets: &[(u64, u64, f32)], col_lo: u64, col_hi: u64) -> (Vec<u64>, Vec<u64>, Vec<f32>) {
let band: Vec<_> = triplets
.iter()
.filter(|(_, j, _)| (col_lo..col_hi).contains(j))
.collect();
let mut colptr = Vec::new();
let mut rows = Vec::new();
let mut vals = Vec::new();
for c in col_lo..col_hi {
colptr.push(rows.len() as u64);
for &&(i, j, x) in &band {
if j == c {
rows.push(i);
vals.push(x);
}
}
}
(colptr, rows, vals)
}
fn stream_write(
path: &str,
shape: Shape,
bands: &[(u64, u64)],
triplets: &[(u64, u64, f32)],
) -> anyhow::Result<Box<dyn SparseIo<IndexIter = Vec<usize>>>> {
let mut out = create_sparse_streaming_empty(Some(path), Some(&SparseIoBackend::Zarr))?;
out.begin_streaming_csc(shape)?;
let mut nnz_offset = 0u64;
for &(lo, hi) in bands {
let (colptr, rows, vals) = slab(triplets, lo, hi);
out.append_csc_slab(lo, nnz_offset, &colptr, &rows, &vals)?;
nnz_offset += vals.len() as u64;
}
out.finalize_streaming_csc()?;
out.build_csr_from_csc_streaming()?;
Ok(out)
}
fn sorted(mut t: Vec<(u64, u64, f32)>) -> Vec<(u64, u64, f32)> {
t.sort_by(|a, b| (a.1, a.0).cmp(&(b.1, b.0)));
t
}
#[test]
fn a_streamed_matrix_equals_the_triplet_path() -> anyhow::Result<()> {
let (triplets, shape) = reference();
let dir = tempfile::tempdir()?;
let streamed_path = dir.path().join("s.zarr");
let streamed = stream_write(
streamed_path.to_str().expect("utf8"),
shape,
&[(0, 2), (2, 5)],
&triplets,
)?;
let reference_backend = create_sparse_from_triplets(
&triplets,
shape,
Some(dir.path().join("t.zarr").to_str().expect("utf8")),
Some(&SparseIoBackend::Zarr),
)?;
for backend in [&streamed, &reference_backend] {
assert_eq!(backend.num_rows(), Some(4));
assert_eq!(backend.num_columns(), Some(5));
assert_eq!(backend.num_non_zeros(), Some(7));
}
let (_, _, a) = streamed.read_triplets_by_columns((0..5).collect())?;
let (_, _, b) = reference_backend.read_triplets_by_columns((0..5).collect())?;
assert_eq!(
sorted(a),
sorted(b),
"the two write paths must agree entry-for-entry"
);
let (_, _, ar) = streamed.read_triplets_by_rows((0..4).collect())?;
assert_eq!(
sorted(ar),
sorted(triplets),
"CSR disagrees with what was written"
);
Ok(())
}
#[test]
fn an_nnz_gap_is_an_error_not_a_poisoned_file() -> anyhow::Result<()> {
let (triplets, shape) = reference();
let dir = tempfile::tempdir()?;
let mut out = create_sparse_streaming_empty(
Some(dir.path().join("g.zarr").to_str().expect("utf8")),
Some(&SparseIoBackend::Zarr),
)?;
out.begin_streaming_csc(shape)?;
let (colptr, rows, vals) = slab(&triplets, 0, 2);
out.append_csc_slab(0, 0, &colptr, &rows, &vals)?;
let (colptr, rows, vals) = slab(&triplets, 2, 5);
let late = out
.append_csc_slab(2, vals.len() as u64 + 5, &colptr, &rows, &vals)
.and_then(|()| out.finalize_streaming_csc());
assert!(late.is_err(), "a gap in the nnz tiling must be refused");
Ok(())
}
#[test]
fn an_overdeclared_nnz_is_an_error_at_finalize() -> anyhow::Result<()> {
let (triplets, (nrow, ncol, _)) = reference();
let dir = tempfile::tempdir()?;
let mut out = create_sparse_streaming_empty(
Some(dir.path().join("o.zarr").to_str().expect("utf8")),
Some(&SparseIoBackend::Zarr),
)?;
out.begin_streaming_csc((nrow, ncol, 9))?;
let (colptr, rows, vals) = slab(&triplets, 0, 5);
out.append_csc_slab(0, 0, &colptr, &rows, &vals)?;
assert!(
out.finalize_streaming_csc().is_err(),
"declared nnz 9 but wrote 7; the phantom tail must be refused"
);
Ok(())
}
#[test]
fn unsorted_rows_within_a_column_are_refused() -> anyhow::Result<()> {
let dir = tempfile::tempdir()?;
let mut out = create_sparse_streaming_empty(
Some(dir.path().join("u.zarr").to_str().expect("utf8")),
Some(&SparseIoBackend::Zarr),
)?;
out.begin_streaming_csc((4, 2, 3))?;
let refused = out.append_csc_slab(0, 0, &[0, 2], &[2, 0, 1], &[1.0, 2.0, 3.0]);
assert!(
refused.is_err(),
"readers document ascending rows as an invariant"
);
Ok(())
}
#[test]
fn a_row_index_at_or_past_nrow_is_refused() -> anyhow::Result<()> {
let dir = tempfile::tempdir()?;
let mut out = create_sparse_streaming_empty(
Some(dir.path().join("r.zarr").to_str().expect("utf8")),
Some(&SparseIoBackend::Zarr),
)?;
out.begin_streaming_csc((3, 1, 1))?;
let refused = out.append_csc_slab(0, 0, &[0], &[3], &[1.0]);
assert!(
refused.is_err(),
"row 3 in a 3-row matrix lands outside the CSR"
);
Ok(())
}
#[test]
fn a_nonmonotone_colptr_is_refused() -> anyhow::Result<()> {
let dir = tempfile::tempdir()?;
let mut out = create_sparse_streaming_empty(
Some(dir.path().join("m.zarr").to_str().expect("utf8")),
Some(&SparseIoBackend::Zarr),
)?;
out.begin_streaming_csc((4, 3, 4))?;
let refused = out.append_csc_slab(0, 0, &[0, 3, 2], &[0, 1, 2, 3], &[1.0; 4]);
assert!(
refused.is_err(),
"a decreasing colptr makes a column claim a negative span"
);
Ok(())
}