1use rand::{RngExt, SeedableRng};
15use rand_chacha::ChaCha8Rng;
16use rayon::prelude::*;
17
18#[derive(Clone, Debug)]
20pub struct CsrMatrix {
21 pub row_offsets: Vec<usize>,
23 pub col_indices: Vec<usize>,
24 pub values: Vec<f64>,
25 pub n_rows: usize,
26 pub n_cols: usize,
27}
28
29impl CsrMatrix {
30 pub fn new(
31 row_offsets: Vec<usize>,
32 col_indices: Vec<usize>,
33 values: Vec<f64>,
34 n_rows: usize,
35 n_cols: usize,
36 ) -> Result<Self, String> {
37 if row_offsets.len() != n_rows + 1 {
38 return Err(format!(
39 "row_offsets length {} != n_rows + 1 = {}",
40 row_offsets.len(),
41 n_rows + 1
42 ));
43 }
44 if col_indices.len() != values.len() {
45 return Err(format!(
46 "col_indices length {} != values length {}",
47 col_indices.len(),
48 values.len()
49 ));
50 }
51 let nnz = *row_offsets.last().ok_or("row_offsets must not be empty")?;
52 if col_indices.len() != nnz {
53 return Err(format!(
54 "col_indices length {} != nnz from row_offsets {}",
55 col_indices.len(),
56 nnz
57 ));
58 }
59 if row_offsets[0] != 0 {
62 return Err(format!(
63 "row_offsets must start at 0, got {}",
64 row_offsets[0]
65 ));
66 }
67 if let Some(pair) = row_offsets.windows(2).find(|w| w[0] > w[1]) {
68 return Err(format!(
69 "row_offsets must be non-decreasing, got {} then {}",
70 pair[0], pair[1]
71 ));
72 }
73 if let Some(&col) = col_indices.iter().find(|&&c| c >= n_cols) {
76 return Err(format!("col_index {col} out of range for n_cols {n_cols}"));
77 }
78 Ok(Self {
79 row_offsets,
80 col_indices,
81 values,
82 n_rows,
83 n_cols,
84 })
85 }
86
87 pub fn nnz(&self) -> usize {
88 self.values.len()
89 }
90
91 pub fn from_dense(dense: &[f64], n_rows: usize, n_cols: usize, threshold: f64) -> Self {
93 let mut row_offsets = Vec::with_capacity(n_rows + 1);
94 let mut col_indices = Vec::new();
95 let mut values = Vec::new();
96
97 row_offsets.push(0);
98 for i in 0..n_rows {
99 for j in 0..n_cols {
100 let v = dense[i * n_cols + j];
101 if v.abs() > threshold {
102 col_indices.push(j);
103 values.push(v);
104 }
105 }
106 row_offsets.push(col_indices.len());
107 }
108
109 Self {
110 row_offsets,
111 col_indices,
112 values,
113 n_rows,
114 n_cols,
115 }
116 }
117
118 pub fn to_dense(&self) -> Vec<f64> {
120 let mut dense = vec![0.0_f64; self.n_rows * self.n_cols];
121 for i in 0..self.n_rows {
122 for idx in self.row_offsets[i]..self.row_offsets[i + 1] {
123 dense[i * self.n_cols + self.col_indices[idx]] = self.values[idx];
124 }
125 }
126 dense
127 }
128
129 fn row_sum(&self, i: usize) -> f64 {
131 let mut s = 0.0_f64;
132 for idx in self.row_offsets[i]..self.row_offsets[i + 1] {
133 s += self.values[idx];
134 }
135 s
136 }
137}
138
139pub enum AdjStorage {
141 Dense { adj: Vec<f64> },
142 Sparse { csr: CsrMatrix },
143}
144
145pub struct StochasticGraphLayer {
146 pub n_nodes: usize,
147 pub n_features: usize,
148 pub storage: AdjStorage,
149 pub weights: Vec<f64>,
150 pub degrees: Vec<f64>,
151}
152
153fn random_weights(n_features: usize, seed: u64) -> Vec<f64> {
154 let mut rng = ChaCha8Rng::seed_from_u64(seed);
155 let mut weights = vec![0.0_f64; n_features * n_features];
156 for w in &mut weights {
157 *w = rng.random::<f64>();
158 }
159 weights
160}
161
162fn dense_degrees(adj: &[f64], n: usize) -> Vec<f64> {
163 let mut degrees = vec![0.0_f64; n];
164 for i in 0..n {
165 let mut sum = 0.0_f64;
166 for j in 0..n {
167 sum += adj[i * n + j];
168 }
169 degrees[i] = sum;
170 }
171 degrees
172}
173
174fn csr_degrees(csr: &CsrMatrix) -> Vec<f64> {
175 (0..csr.n_rows).map(|i| csr.row_sum(i)).collect()
176}
177
178impl StochasticGraphLayer {
179 pub fn new(adj_flat: Vec<f64>, n_nodes: usize, n_features: usize, seed: u64) -> Self {
181 assert_eq!(
182 adj_flat.len(),
183 n_nodes * n_nodes,
184 "adj_flat must have length n_nodes * n_nodes",
185 );
186 let degrees = dense_degrees(&adj_flat, n_nodes);
187 Self {
188 n_nodes,
189 n_features,
190 storage: AdjStorage::Dense { adj: adj_flat },
191 weights: random_weights(n_features, seed),
192 degrees,
193 }
194 }
195
196 pub fn new_sparse(csr: CsrMatrix, n_features: usize, seed: u64) -> Result<Self, String> {
198 if csr.n_rows != csr.n_cols {
199 return Err(format!(
200 "CSR must be square, got {}x{}",
201 csr.n_rows, csr.n_cols
202 ));
203 }
204 let n_nodes = csr.n_rows;
205 let degrees = csr_degrees(&csr);
206 Ok(Self {
207 n_nodes,
208 n_features,
209 storage: AdjStorage::Sparse { csr },
210 weights: random_weights(n_features, seed),
211 degrees,
212 })
213 }
214
215 pub fn from_dense_auto(
217 adj_flat: Vec<f64>,
218 n_nodes: usize,
219 n_features: usize,
220 seed: u64,
221 density_threshold: f64,
222 ) -> Self {
223 assert_eq!(adj_flat.len(), n_nodes * n_nodes);
224 let total = (n_nodes * n_nodes) as f64;
225 let nnz = adj_flat.iter().filter(|v| v.abs() > 1e-15).count() as f64;
226 let density = nnz / total;
227
228 if density < density_threshold {
229 let csr = CsrMatrix::from_dense(&adj_flat, n_nodes, n_nodes, 1e-15);
230 let degrees = csr_degrees(&csr);
231 Self {
232 n_nodes,
233 n_features,
234 storage: AdjStorage::Sparse { csr },
235 weights: random_weights(n_features, seed),
236 degrees,
237 }
238 } else {
239 Self::new(adj_flat, n_nodes, n_features, seed)
240 }
241 }
242
243 pub fn is_sparse(&self) -> bool {
245 matches!(self.storage, AdjStorage::Sparse { .. })
246 }
247
248 fn validate_features(&self, node_features: &[f64]) -> Result<(), String> {
249 if node_features.len() != self.n_nodes * self.n_features {
250 return Err(format!(
251 "node_features length mismatch: got {}, expected {}.",
252 node_features.len(),
253 self.n_nodes * self.n_features
254 ));
255 }
256 Ok(())
257 }
258
259 fn aggregate_and_transform(&self, agg_flat: &[f64]) -> Vec<f64> {
261 let out_rows: Vec<Vec<f64>> = (0..self.n_nodes)
262 .into_par_iter()
263 .map(|i| {
264 let agg = &agg_flat[i * self.n_features..(i + 1) * self.n_features];
265 let mut out = vec![0.0_f64; self.n_features];
266 for (f_out, out_val) in out.iter_mut().enumerate().take(self.n_features) {
267 let mut acc = 0.0_f64;
268 for (g, agg_val) in agg.iter().enumerate().take(self.n_features) {
269 acc += *agg_val * self.weights[g * self.n_features + f_out];
270 }
271 *out_val = acc.tanh();
272 }
273 out
274 })
275 .collect();
276 let mut flat = Vec::with_capacity(self.n_nodes * self.n_features);
277 for row in out_rows {
278 flat.extend(row);
279 }
280 flat
281 }
282
283 pub fn forward(&self, node_features: &[f64]) -> Result<Vec<f64>, String> {
285 self.validate_features(node_features)?;
286
287 let mut agg = vec![0.0_f64; self.n_nodes * self.n_features];
288
289 match &self.storage {
290 AdjStorage::Dense { adj } => {
291 let agg_rows: Vec<Vec<f64>> = (0..self.n_nodes)
292 .into_par_iter()
293 .map(|i| {
294 let mut row = vec![0.0_f64; self.n_features];
295 for f in 0..self.n_features {
296 let mut acc = 0.0_f64;
297 for j in 0..self.n_nodes {
298 acc += adj[i * self.n_nodes + j]
299 * node_features[j * self.n_features + f];
300 }
301 row[f] = acc;
302 }
303 if self.degrees[i] != 0.0 {
304 for x in &mut row {
305 *x /= self.degrees[i];
306 }
307 }
308 row
309 })
310 .collect();
311 for (i, row) in agg_rows.into_iter().enumerate() {
312 agg[i * self.n_features..(i + 1) * self.n_features].copy_from_slice(&row);
313 }
314 }
315 AdjStorage::Sparse { csr } => {
316 let agg_rows: Vec<Vec<f64>> = (0..self.n_nodes)
317 .into_par_iter()
318 .map(|i| {
319 let mut row = vec![0.0_f64; self.n_features];
320 for idx in csr.row_offsets[i]..csr.row_offsets[i + 1] {
321 let j = csr.col_indices[idx];
322 let a_ij = csr.values[idx];
323 for f in 0..self.n_features {
324 row[f] += a_ij * node_features[j * self.n_features + f];
325 }
326 }
327 if self.degrees[i] != 0.0 {
328 for x in &mut row {
329 *x /= self.degrees[i];
330 }
331 }
332 row
333 })
334 .collect();
335 for (i, row) in agg_rows.into_iter().enumerate() {
336 agg[i * self.n_features..(i + 1) * self.n_features].copy_from_slice(&row);
337 }
338 }
339 }
340
341 Ok(self.aggregate_and_transform(&agg))
342 }
343
344 pub fn forward_sc(
349 &self,
350 node_features: &[f64],
351 length: usize,
352 seed: u64,
353 ) -> Result<Vec<f64>, String> {
354 self.validate_features(node_features)?;
355 if length == 0 {
356 return Err("length must be > 0 for SC mode.".to_string());
357 }
358
359 let mut rng = ChaCha8Rng::seed_from_u64(seed);
360 let words = length.div_ceil(64);
361
362 let feat_packed = crate::bitstream::encode_matrix_prob_to_packed(
363 node_features,
364 self.n_nodes,
365 self.n_features,
366 length,
367 words,
368 &mut rng,
369 );
370
371 let mut agg = vec![0.0_f64; self.n_nodes * self.n_features];
372
373 match &self.storage {
374 AdjStorage::Dense { adj } => {
375 let adj_packed = crate::bitstream::encode_matrix_prob_to_packed(
376 adj,
377 self.n_nodes,
378 self.n_nodes,
379 length,
380 words,
381 &mut rng,
382 );
383 for i in 0..self.n_nodes {
384 for f in 0..self.n_features {
385 let mut pop_total = 0_u64;
386 for j in 0..self.n_nodes {
387 let a = &adj_packed[i * self.n_nodes + j];
388 let b = &feat_packed[j * self.n_features + f];
389 for w in 0..words {
390 pop_total += crate::bitstream::swar_popcount_word(a[w] & b[w]);
391 }
392 }
393 agg[i * self.n_features + f] = pop_total as f64 / length as f64;
394 }
395 }
396 }
397 AdjStorage::Sparse { csr } => {
398 let nnz = csr.nnz();
400 let adj_vals_clamped: Vec<f64> =
401 csr.values.iter().map(|v| v.clamp(0.0, 1.0)).collect();
402 let adj_packed = crate::bitstream::encode_matrix_prob_to_packed(
403 &adj_vals_clamped,
404 1,
405 nnz,
406 length,
407 words,
408 &mut rng,
409 );
410 for i in 0..self.n_nodes {
411 #[allow(clippy::needless_range_loop)]
412 for idx in csr.row_offsets[i]..csr.row_offsets[i + 1] {
413 let j = csr.col_indices[idx];
414 let a = &adj_packed[idx];
415 for f in 0..self.n_features {
416 let b = &feat_packed[j * self.n_features + f];
417 let mut pop = 0_u64;
418 for w in 0..words {
419 pop += crate::bitstream::swar_popcount_word(a[w] & b[w]);
420 }
421 agg[i * self.n_features + f] += pop as f64 / length as f64;
422 }
423 }
424 }
425 }
426 }
427
428 for i in 0..self.n_nodes {
429 if self.degrees[i] != 0.0 {
430 for f in 0..self.n_features {
431 agg[i * self.n_features + f] /= self.degrees[i];
432 }
433 }
434 }
435
436 let agg_packed = crate::bitstream::encode_matrix_prob_to_packed(
437 &agg,
438 self.n_nodes,
439 self.n_features,
440 length,
441 words,
442 &mut rng,
443 );
444 let w_clamped: Vec<f64> = self.weights.iter().map(|w| w.clamp(0.0, 1.0)).collect();
445 let w_packed = crate::bitstream::encode_matrix_prob_to_packed(
446 &w_clamped,
447 self.n_features,
448 self.n_features,
449 length,
450 words,
451 &mut rng,
452 );
453
454 let mut out = Vec::with_capacity(self.n_nodes * self.n_features);
455 for i in 0..self.n_nodes {
456 for f_out in 0..self.n_features {
457 let mut pop_total = 0_u64;
458 for g in 0..self.n_features {
459 let a = &agg_packed[i * self.n_features + g];
460 let b = &w_packed[g * self.n_features + f_out];
461 for w in 0..words {
462 pop_total += crate::bitstream::swar_popcount_word(a[w] & b[w]);
463 }
464 }
465 out.push((pop_total as f64 / length as f64).tanh());
466 }
467 }
468
469 Ok(out)
470 }
471
472 pub fn get_weights(&self) -> Vec<f64> {
473 self.weights.clone()
474 }
475
476 pub fn set_weights(&mut self, weights: Vec<f64>) -> Result<(), String> {
477 if weights.len() != self.n_features * self.n_features {
478 return Err(format!(
479 "weights length mismatch: got {}, expected {}.",
480 weights.len(),
481 self.n_features * self.n_features
482 ));
483 }
484 self.weights = weights;
485 Ok(())
486 }
487}