1use crate::error::{SparseError, SparseResult};
29use crate::host_csr::HostCsr;
30
31#[derive(Debug, Clone, PartialEq, Eq)]
36pub struct SymbolicPattern {
37 pub nrows: usize,
39 pub ncols: usize,
41 pub row_ptr: Vec<usize>,
44 pub col_indices: Vec<usize>,
47}
48
49impl SymbolicPattern {
50 #[inline]
52 pub fn nnz(&self) -> usize {
53 self.col_indices.len()
54 }
55
56 #[inline]
63 pub fn row(&self, row: usize) -> &[usize] {
64 &self.col_indices[self.row_ptr[row]..self.row_ptr[row + 1]]
65 }
66}
67
68pub fn spgemm_symbolic_pattern(a: &HostCsr, b: &HostCsr) -> SparseResult<SymbolicPattern> {
79 if a.ncols != b.nrows {
80 return Err(SparseError::DimensionMismatch(format!(
81 "A.ncols ({}) != B.nrows ({})",
82 a.ncols, b.nrows
83 )));
84 }
85
86 let out_cols = b.ncols;
87 let mut row_ptr = vec![0usize; a.nrows + 1];
88 let mut col_indices: Vec<usize> = Vec::new();
89
90 let mut mask = vec![usize::MAX; out_cols];
94 let mut row_cols: Vec<usize> = Vec::new();
95
96 for i in 0..a.nrows {
97 row_cols.clear();
98 let a_start = a.row_ptr[i];
99 let a_end = a.row_ptr[i + 1];
100 for &a_col in &a.col_indices[a_start..a_end] {
101 let b_start = b.row_ptr[a_col];
102 let b_end = b.row_ptr[a_col + 1];
103 for &b_col in &b.col_indices[b_start..b_end] {
104 if mask[b_col] != i {
105 mask[b_col] = i;
106 row_cols.push(b_col);
107 }
108 }
109 }
110 row_cols.sort_unstable();
111 col_indices.extend_from_slice(&row_cols);
112 row_ptr[i + 1] = col_indices.len();
113 }
114
115 Ok(SymbolicPattern {
116 nrows: a.nrows,
117 ncols: out_cols,
118 row_ptr,
119 col_indices,
120 })
121}
122
123#[cfg(test)]
124mod tests {
125 use super::*;
126
127 struct Lcg {
129 state: u64,
130 }
131
132 impl Lcg {
133 fn new(seed: u64) -> Self {
134 Self {
135 state: seed.wrapping_add(0x9e37_79b9_7f4a_7c15),
136 }
137 }
138 fn next_u32(&mut self) -> u32 {
139 self.state = self
140 .state
141 .wrapping_mul(6_364_136_223_846_793_005)
142 .wrapping_add(1_442_695_040_888_963_407);
143 (self.state >> 32) as u32
144 }
145 fn next_pos(&mut self) -> f64 {
147 1.0 + (self.next_u32() as f64 / u32::MAX as f64) * 3.0
148 }
149 fn next_bool(&mut self, num: u32, den: u32) -> bool {
150 self.next_u32() % den < num
151 }
152 }
153
154 fn random_positive(nrows: usize, ncols: usize, num: u32, den: u32, seed: u64) -> HostCsr {
157 let mut rng = Lcg::new(seed);
158 let mut row_ptr = vec![0usize; nrows + 1];
159 let mut col_indices = Vec::new();
160 let mut values = Vec::new();
161 for i in 0..nrows {
162 for j in 0..ncols {
163 if rng.next_bool(num, den) {
164 col_indices.push(j);
165 values.push(rng.next_pos());
166 }
167 }
168 row_ptr[i + 1] = col_indices.len();
169 }
170 HostCsr::new(nrows, ncols, row_ptr, col_indices, values).expect("valid random csr")
171 }
172
173 fn numeric_pattern(m: &HostCsr) -> Vec<Vec<usize>> {
175 (0..m.nrows)
176 .map(|i| m.col_indices[m.row_ptr[i]..m.row_ptr[i + 1]].to_vec())
177 .collect()
178 }
179
180 fn assert_matches_numeric(a: &HostCsr, b: &HostCsr) {
182 let sym = spgemm_symbolic_pattern(a, b).expect("symbolic");
183 let numeric = a.matmul(b).expect("numeric");
184
185 assert_eq!(sym.row_ptr, numeric.row_ptr, "row_ptr mismatch");
187 assert_eq!(sym.nnz(), numeric.nnz(), "nnz mismatch");
188
189 let num_sets = numeric_pattern(&numeric);
191 for (i, num_row) in num_sets.iter().enumerate() {
192 assert_eq!(sym.row(i), num_row.as_slice(), "row {i} columns differ");
193 }
194
195 assert_eq!(sym.row_ptr[0], 0);
197 assert_eq!(*sym.row_ptr.last().expect("nonempty"), sym.nnz());
198 for w in sym.row_ptr.windows(2) {
199 assert!(w[1] >= w[0], "row_ptr not monotone");
200 }
201 for &c in &sym.col_indices {
202 assert!(c < sym.ncols, "column {c} out of range");
203 }
204 }
205
206 #[test]
207 fn matches_dense_handbuilt() {
208 let a = HostCsr::new(
210 3,
211 3,
212 vec![0, 2, 4, 6],
213 vec![0, 1, 1, 2, 0, 2],
214 vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0],
215 )
216 .expect("a");
217 let b =
218 HostCsr::new(3, 2, vec![0, 1, 2, 3], vec![0, 1, 0], vec![7.0, 8.0, 9.0]).expect("b");
219 assert_matches_numeric(&a, &b);
220
221 let sym = spgemm_symbolic_pattern(&a, &b).expect("sym");
223 assert_eq!(sym.row_ptr, vec![0, 2, 4, 5]);
224 assert_eq!(sym.row(0), &[0, 1]);
225 assert_eq!(sym.row(1), &[0, 1]);
226 assert_eq!(sym.row(2), &[0]); }
228
229 #[test]
230 fn identity_preserves_pattern() {
231 let a = random_positive(6, 6, 1, 3, 0xabc);
232 let eye = HostCsr::new(
233 6,
234 6,
235 vec![0, 1, 2, 3, 4, 5, 6],
236 vec![0, 1, 2, 3, 4, 5],
237 vec![1.0; 6],
238 )
239 .expect("eye");
240 assert_matches_numeric(&a, &eye);
242 assert_matches_numeric(&eye, &a);
243 }
244
245 #[test]
246 fn handles_empty_rows() {
247 let a = HostCsr::new(3, 3, vec![0, 1, 1, 2], vec![2, 0], vec![1.0, 1.0]).expect("a");
249 let b = random_positive(3, 4, 1, 2, 0x55);
250 let sym = spgemm_symbolic_pattern(&a, &b).expect("sym");
251 assert_eq!(sym.row_ptr[1], sym.row_ptr[2], "empty A row -> empty C row");
252 assert_matches_numeric(&a, &b);
253 }
254
255 #[test]
256 fn empty_b_column_block() {
257 let a = random_positive(4, 3, 2, 3, 0x99);
259 let b =
261 HostCsr::new(3, 3, vec![0, 0, 1, 3], vec![2, 0, 1], vec![1.0, 1.0, 1.0]).expect("b");
262 assert_matches_numeric(&a, &b);
263 }
264
265 #[test]
266 fn random_small_matrices() {
267 let cases = [
269 (5usize, 4usize, 6usize, 1u32, 2u32, 0x1u64),
270 (7, 7, 7, 1, 3, 0x2),
271 (3, 8, 5, 2, 3, 0x3),
272 (10, 6, 9, 1, 4, 0x4),
273 (6, 6, 6, 1, 1, 0x5), ];
275 for (m, k, n, num, den, seed) in cases {
276 let a = random_positive(m, k, num, den, seed);
277 let b = random_positive(k, n, num, den, seed ^ 0xdead);
278 assert_matches_numeric(&a, &b);
279 }
280 }
281
282 #[test]
283 fn dimension_mismatch_errors() {
284 let a = random_positive(3, 4, 1, 2, 1);
285 let b = random_positive(5, 2, 1, 2, 2); assert!(spgemm_symbolic_pattern(&a, &b).is_err());
287 }
288
289 #[test]
290 fn nnz_and_row_accessor() {
291 let a = random_positive(4, 4, 1, 2, 7);
292 let b = random_positive(4, 4, 1, 2, 8);
293 let sym = spgemm_symbolic_pattern(&a, &b).expect("sym");
294 let total: usize = (0..sym.nrows).map(|i| sym.row(i).len()).sum();
295 assert_eq!(total, sym.nnz());
296 }
297}