#![cfg(feature = "alloc")]
use proptest::prelude::*;
use rustebra::sparse::{CsrMatrix, add_csr, spmm_csr};
fn approx_eq(a: f64, b: f64, tol: f64) -> bool {
(a - b).abs() <= tol
}
fn dense_matrix(max_dim: usize) -> impl Strategy<Value = (usize, usize, Vec<f64>)> {
(1usize..=max_dim, 1usize..=max_dim).prop_flat_map(|(rows, cols)| {
prop::collection::vec(prop_oneof![Just(0.0f64), -20.0..20.0f64], rows * cols)
.prop_map(move |dense| (rows, cols, dense))
})
}
fn csr_from_dense(rows: usize, cols: usize, dense: &[f64]) -> CsrMatrix<f64> {
let mut row_ptr = vec![0u32; rows + 1];
let mut col_indices = Vec::new();
let mut values = Vec::new();
for r in 0..rows {
for c in 0..cols {
let v = dense[r * cols + c];
if v != 0.0 {
col_indices.push(c as u32);
values.push(v);
}
}
row_ptr[r + 1] = col_indices.len() as u32;
}
CsrMatrix::new(rows, cols, row_ptr, col_indices, values).expect("built from a valid dense grid")
}
fn dense_from_csr(m: &CsrMatrix<f64>) -> Vec<f64> {
let mut dense = vec![0.0f64; m.rows() * m.cols()];
for r in 0..m.rows() {
for k in m.row_range(r).expect("row < rows") {
let c = m.col_indices()[k] as usize;
dense[r * m.cols() + c] = m.values()[k];
}
}
dense
}
proptest! {
#[test]
fn add_csr_matches_dense_addition(
(rows, cols, a_dense) in dense_matrix(8),
b_entries in prop::collection::vec(prop_oneof![Just(0.0f64), -20.0..20.0f64], 0..64),
) {
let b_dense: Vec<f64> = (0..rows * cols)
.map(|i| b_entries.get(i).copied().unwrap_or(0.0))
.collect();
let a = csr_from_dense(rows, cols, &a_dense);
let b = csr_from_dense(rows, cols, &b_dense);
let c = add_csr(&a, &b).expect("same shape by construction");
let expected: Vec<f64> = a_dense.iter().zip(b_dense.iter()).map(|(x, y)| x + y).collect();
let actual = dense_from_csr(&c);
for (act, exp) in actual.iter().zip(expected.iter()) {
prop_assert!(approx_eq(*act, *exp, 1e-9));
}
for &v in c.values() {
prop_assert_ne!(v, 0.0);
}
}
#[test]
fn spmm_csr_matches_dense_matmul(
m in 1usize..=6,
k in 1usize..=6,
n in 1usize..=6,
a_entries in prop::collection::vec(prop_oneof![Just(0.0f64), -10.0..10.0f64], 0..64),
b_entries in prop::collection::vec(prop_oneof![Just(0.0f64), -10.0..10.0f64], 0..64),
) {
let a_dense: Vec<f64> = (0..m * k).map(|i| a_entries.get(i).copied().unwrap_or(0.0)).collect();
let b_dense: Vec<f64> = (0..k * n).map(|i| b_entries.get(i).copied().unwrap_or(0.0)).collect();
let a = csr_from_dense(m, k, &a_dense);
let b = csr_from_dense(k, n, &b_dense);
let c = spmm_csr(&a, &b).expect("a.cols() == b.rows() by construction");
let mut expected = vec![0.0f64; m * n];
for i in 0..m {
for p in 0..k {
let av = a_dense[i * k + p];
if av == 0.0 {
continue;
}
for j in 0..n {
expected[i * n + j] += av * b_dense[p * n + j];
}
}
}
let actual = dense_from_csr(&c);
for (act, exp) in actual.iter().zip(expected.iter()) {
prop_assert!(approx_eq(*act, *exp, 1e-6));
}
for &v in c.values() {
prop_assert_ne!(v, 0.0);
}
}
#[test]
fn add_csr_cancels_to_empty((rows, cols, dense) in dense_matrix(8)) {
let a = csr_from_dense(rows, cols, &dense);
let negated: Vec<f64> = dense.iter().map(|v| -v).collect();
let b = csr_from_dense(rows, cols, &negated);
let c = add_csr(&a, &b).expect("same shape by construction");
prop_assert_eq!(c.nnz(), 0);
}
#[test]
fn spmm_csr_cancels_accumulated_product(v in -20.0..20.0f64, w in -20.0..20.0f64) {
prop_assume!(v != 0.0 && w != 0.0);
let a = CsrMatrix::new(1, 2, vec![0, 2], vec![0, 1], vec![v, -v]).unwrap();
let b = CsrMatrix::new(2, 1, vec![0, 1, 2], vec![0, 0], vec![w, w]).unwrap();
let c = spmm_csr(&a, &b).expect("a.cols() == b.rows() by construction");
prop_assert_eq!(c.nnz(), 0);
}
}