use crate::error::{SparseError, SparseResult};
use crate::host_csr::HostCsr;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SymbolicPattern {
pub nrows: usize,
pub ncols: usize,
pub row_ptr: Vec<usize>,
pub col_indices: Vec<usize>,
}
impl SymbolicPattern {
#[inline]
pub fn nnz(&self) -> usize {
self.col_indices.len()
}
#[inline]
pub fn row(&self, row: usize) -> &[usize] {
&self.col_indices[self.row_ptr[row]..self.row_ptr[row + 1]]
}
}
pub fn spgemm_symbolic_pattern(a: &HostCsr, b: &HostCsr) -> SparseResult<SymbolicPattern> {
if a.ncols != b.nrows {
return Err(SparseError::DimensionMismatch(format!(
"A.ncols ({}) != B.nrows ({})",
a.ncols, b.nrows
)));
}
let out_cols = b.ncols;
let mut row_ptr = vec![0usize; a.nrows + 1];
let mut col_indices: Vec<usize> = Vec::new();
let mut mask = vec![usize::MAX; out_cols];
let mut row_cols: Vec<usize> = Vec::new();
for i in 0..a.nrows {
row_cols.clear();
let a_start = a.row_ptr[i];
let a_end = a.row_ptr[i + 1];
for &a_col in &a.col_indices[a_start..a_end] {
let b_start = b.row_ptr[a_col];
let b_end = b.row_ptr[a_col + 1];
for &b_col in &b.col_indices[b_start..b_end] {
if mask[b_col] != i {
mask[b_col] = i;
row_cols.push(b_col);
}
}
}
row_cols.sort_unstable();
col_indices.extend_from_slice(&row_cols);
row_ptr[i + 1] = col_indices.len();
}
Ok(SymbolicPattern {
nrows: a.nrows,
ncols: out_cols,
row_ptr,
col_indices,
})
}
#[cfg(test)]
mod tests {
use super::*;
struct Lcg {
state: u64,
}
impl Lcg {
fn new(seed: u64) -> Self {
Self {
state: seed.wrapping_add(0x9e37_79b9_7f4a_7c15),
}
}
fn next_u32(&mut self) -> u32 {
self.state = self
.state
.wrapping_mul(6_364_136_223_846_793_005)
.wrapping_add(1_442_695_040_888_963_407);
(self.state >> 32) as u32
}
fn next_pos(&mut self) -> f64 {
1.0 + (self.next_u32() as f64 / u32::MAX as f64) * 3.0
}
fn next_bool(&mut self, num: u32, den: u32) -> bool {
self.next_u32() % den < num
}
}
fn random_positive(nrows: usize, ncols: usize, num: u32, den: u32, seed: u64) -> HostCsr {
let mut rng = Lcg::new(seed);
let mut row_ptr = vec![0usize; nrows + 1];
let mut col_indices = Vec::new();
let mut values = Vec::new();
for i in 0..nrows {
for j in 0..ncols {
if rng.next_bool(num, den) {
col_indices.push(j);
values.push(rng.next_pos());
}
}
row_ptr[i + 1] = col_indices.len();
}
HostCsr::new(nrows, ncols, row_ptr, col_indices, values).expect("valid random csr")
}
fn numeric_pattern(m: &HostCsr) -> Vec<Vec<usize>> {
(0..m.nrows)
.map(|i| m.col_indices[m.row_ptr[i]..m.row_ptr[i + 1]].to_vec())
.collect()
}
fn assert_matches_numeric(a: &HostCsr, b: &HostCsr) {
let sym = spgemm_symbolic_pattern(a, b).expect("symbolic");
let numeric = a.matmul(b).expect("numeric");
assert_eq!(sym.row_ptr, numeric.row_ptr, "row_ptr mismatch");
assert_eq!(sym.nnz(), numeric.nnz(), "nnz mismatch");
let num_sets = numeric_pattern(&numeric);
for (i, num_row) in num_sets.iter().enumerate() {
assert_eq!(sym.row(i), num_row.as_slice(), "row {i} columns differ");
}
assert_eq!(sym.row_ptr[0], 0);
assert_eq!(*sym.row_ptr.last().expect("nonempty"), sym.nnz());
for w in sym.row_ptr.windows(2) {
assert!(w[1] >= w[0], "row_ptr not monotone");
}
for &c in &sym.col_indices {
assert!(c < sym.ncols, "column {c} out of range");
}
}
#[test]
fn matches_dense_handbuilt() {
let a = HostCsr::new(
3,
3,
vec![0, 2, 4, 6],
vec![0, 1, 1, 2, 0, 2],
vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0],
)
.expect("a");
let b =
HostCsr::new(3, 2, vec![0, 1, 2, 3], vec![0, 1, 0], vec![7.0, 8.0, 9.0]).expect("b");
assert_matches_numeric(&a, &b);
let sym = spgemm_symbolic_pattern(&a, &b).expect("sym");
assert_eq!(sym.row_ptr, vec![0, 2, 4, 5]);
assert_eq!(sym.row(0), &[0, 1]);
assert_eq!(sym.row(1), &[0, 1]);
assert_eq!(sym.row(2), &[0]); }
#[test]
fn identity_preserves_pattern() {
let a = random_positive(6, 6, 1, 3, 0xabc);
let eye = HostCsr::new(
6,
6,
vec![0, 1, 2, 3, 4, 5, 6],
vec![0, 1, 2, 3, 4, 5],
vec![1.0; 6],
)
.expect("eye");
assert_matches_numeric(&a, &eye);
assert_matches_numeric(&eye, &a);
}
#[test]
fn handles_empty_rows() {
let a = HostCsr::new(3, 3, vec![0, 1, 1, 2], vec![2, 0], vec![1.0, 1.0]).expect("a");
let b = random_positive(3, 4, 1, 2, 0x55);
let sym = spgemm_symbolic_pattern(&a, &b).expect("sym");
assert_eq!(sym.row_ptr[1], sym.row_ptr[2], "empty A row -> empty C row");
assert_matches_numeric(&a, &b);
}
#[test]
fn empty_b_column_block() {
let a = random_positive(4, 3, 2, 3, 0x99);
let b =
HostCsr::new(3, 3, vec![0, 0, 1, 3], vec![2, 0, 1], vec![1.0, 1.0, 1.0]).expect("b");
assert_matches_numeric(&a, &b);
}
#[test]
fn random_small_matrices() {
let cases = [
(5usize, 4usize, 6usize, 1u32, 2u32, 0x1u64),
(7, 7, 7, 1, 3, 0x2),
(3, 8, 5, 2, 3, 0x3),
(10, 6, 9, 1, 4, 0x4),
(6, 6, 6, 1, 1, 0x5), ];
for (m, k, n, num, den, seed) in cases {
let a = random_positive(m, k, num, den, seed);
let b = random_positive(k, n, num, den, seed ^ 0xdead);
assert_matches_numeric(&a, &b);
}
}
#[test]
fn dimension_mismatch_errors() {
let a = random_positive(3, 4, 1, 2, 1);
let b = random_positive(5, 2, 1, 2, 2); assert!(spgemm_symbolic_pattern(&a, &b).is_err());
}
#[test]
fn nnz_and_row_accessor() {
let a = random_positive(4, 4, 1, 2, 7);
let b = random_positive(4, 4, 1, 2, 8);
let sym = spgemm_symbolic_pattern(&a, &b).expect("sym");
let total: usize = (0..sym.nrows).map(|i| sym.row(i).len()).sum();
assert_eq!(total, sym.nnz());
}
}