Skip to main content

trueno_graph/storage/
csr.rs

1//! CSR (Compressed Sparse Row) graph representation
2//!
3//! Based on `GraphBLAST` (Yang et al., ACM `ToMS` 2022) for GPU-optimized sparse matrix operations.
4//!
5//! # CSR Format
6//!
7//! ```text
8//! Graph: 0 → 1, 0 → 2, 1 → 2
9//!
10//! CSR:
11//!   row_offsets: [0, 2, 3, 3]  // Node 0: edges [0..2), Node 1: [2..3), Node 2: [3..3)
12//!   col_indices: [1, 2, 2]      // Edge 0 → node 1, edge 1 → node 2, edge 2 → node 2
13//!   edge_weights: [1.0, 1.0, 1.0]
14//! ```
15
16use anyhow::{anyhow, Result};
17use std::collections::HashMap;
18
19/// Node identifier (zero-indexed)
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
21pub struct NodeId(pub u32);
22
23/// CSR (Compressed Sparse Row) graph
24///
25/// Optimized for:
26/// - O(1) access to outgoing edges (via forward CSR)
27/// - O(1) access to incoming edges (via reverse CSR)
28/// - GPU-friendly memory layout
29/// - Sparse matrix operations (via aprender)
30///
31/// # Example
32///
33/// ```
34/// use trueno_graph::{CsrGraph, NodeId};
35///
36/// let mut graph = CsrGraph::new();
37/// graph.add_edge(NodeId(0), NodeId(1), 1.0).unwrap();
38/// graph.add_edge(NodeId(0), NodeId(2), 1.0).unwrap();
39///
40/// let neighbors = graph.outgoing_neighbors(NodeId(0)).unwrap();
41/// assert_eq!(neighbors.len(), 2);
42/// ```
43#[derive(Debug, Clone)]
44pub struct CsrGraph {
45    /// Forward CSR: Row offsets for outgoing edges
46    /// node i's edges start at `row_offsets`[i]
47    /// Length: `num_nodes` + 1
48    row_offsets: Vec<u32>,
49
50    /// Forward CSR: Column indices (edge targets)
51    /// Length: `num_edges`
52    col_indices: Vec<u32>,
53
54    /// Forward CSR: Edge weights
55    /// Length: `num_edges`
56    edge_weights: Vec<f32>,
57
58    /// Reverse CSR: Row offsets for incoming edges
59    /// node i's incoming edges start at `rev_row_offsets`[i]
60    /// Length: `num_nodes` + 1
61    rev_row_offsets: Vec<u32>,
62
63    /// Reverse CSR: Column indices (edge sources)
64    /// Length: `num_edges`
65    rev_col_indices: Vec<u32>,
66
67    /// Reverse CSR: Edge weights (same as forward, but reordered)
68    /// Length: `num_edges`
69    rev_edge_weights: Vec<f32>,
70
71    /// Node names (for debugging/export)
72    node_names: HashMap<NodeId, String>,
73
74    /// Number of nodes
75    num_nodes: usize,
76}
77
78impl CsrGraph {
79    /// Create new empty graph
80    #[must_use]
81    pub fn new() -> Self {
82        Self {
83            row_offsets: vec![0], // Start with single offset
84            col_indices: Vec::new(),
85            edge_weights: Vec::new(),
86            rev_row_offsets: vec![0], // Reverse CSR offsets
87            rev_col_indices: Vec::new(),
88            rev_edge_weights: Vec::new(),
89            node_names: HashMap::new(),
90            num_nodes: 0,
91        }
92    }
93
94    /// Create graph from edge list
95    ///
96    /// # Arguments
97    ///
98    /// * `edges` - List of (source, target, weight) tuples
99    ///
100    /// # Errors
101    ///
102    /// Returns error if edge list is invalid (e.g., negative node IDs)
103    pub fn from_edge_list(edges: &[(NodeId, NodeId, f32)]) -> Result<Self> {
104        if edges.is_empty() {
105            return Ok(Self::new());
106        }
107
108        // Find max node ID to determine graph size
109        let max_node = edges
110            .iter()
111            .flat_map(|(src, dst, _)| [src.0, dst.0])
112            .max()
113            .ok_or_else(|| anyhow!("Empty edge list"))?;
114
115        let num_nodes = (max_node + 1) as usize;
116
117        // Build intermediate adjacency lists for both forward and reverse directions
118        let mut adj_list: Vec<Vec<(u32, f32)>> = vec![Vec::new(); num_nodes];
119        let mut rev_adj_list: Vec<Vec<(u32, f32)>> = vec![Vec::new(); num_nodes];
120
121        for (src, dst, weight) in edges {
122            adj_list[src.0 as usize].push((dst.0, *weight));
123            rev_adj_list[dst.0 as usize].push((src.0, *weight)); // Reverse: dst ← src
124        }
125
126        // Build forward CSR
127        let mut row_offsets = Vec::with_capacity(num_nodes + 1);
128        let mut col_indices = Vec::new();
129        let mut edge_weights_vec = Vec::new();
130
131        let mut offset = 0_u32;
132        row_offsets.push(offset);
133
134        for neighbors in &adj_list {
135            #[allow(clippy::cast_possible_truncation)] // Graphs >4B nodes not supported yet
136            let len_u32 = neighbors.len() as u32;
137            offset += len_u32;
138            row_offsets.push(offset);
139
140            for (target, weight) in neighbors {
141                col_indices.push(*target);
142                edge_weights_vec.push(*weight);
143            }
144        }
145
146        // Build reverse CSR
147        let mut rev_row_offsets = Vec::with_capacity(num_nodes + 1);
148        let mut rev_col_indices = Vec::new();
149        let mut rev_edge_weights_vec = Vec::new();
150
151        let mut rev_offset = 0_u32;
152        rev_row_offsets.push(rev_offset);
153
154        for rev_neighbors in &rev_adj_list {
155            #[allow(clippy::cast_possible_truncation)]
156            let len_u32 = rev_neighbors.len() as u32;
157            rev_offset += len_u32;
158            rev_row_offsets.push(rev_offset);
159
160            for (source, weight) in rev_neighbors {
161                rev_col_indices.push(*source);
162                rev_edge_weights_vec.push(*weight);
163            }
164        }
165
166        Ok(Self {
167            row_offsets,
168            col_indices,
169            edge_weights: edge_weights_vec,
170            rev_row_offsets,
171            rev_col_indices,
172            rev_edge_weights: rev_edge_weights_vec,
173            node_names: HashMap::new(),
174            num_nodes,
175        })
176    }
177
178    /// Add edge to graph (dynamic insertion)
179    ///
180    /// Note: For large graphs, use `from_edge_list` for better performance.
181    ///
182    /// # Errors
183    ///
184    /// Returns error if graph is already finalized (immutable CSR)
185    pub fn add_edge(&mut self, src: NodeId, dst: NodeId, weight: f32) -> Result<()> {
186        // Expand graph if needed
187        let max_node = src.0.max(dst.0) as usize;
188        if max_node >= self.num_nodes {
189            self.expand_to(max_node + 1);
190        }
191
192        // Insert forward edge (src → dst)
193        let src_idx = src.0 as usize;
194        let end = self.row_offsets[src_idx + 1] as usize;
195
196        self.col_indices.insert(end, dst.0);
197        self.edge_weights.insert(end, weight);
198
199        // Update row offsets for all nodes after src
200        for offset in &mut self.row_offsets[src_idx + 1..] {
201            *offset += 1;
202        }
203
204        // Insert reverse edge (dst ← src)
205        let dst_idx = dst.0 as usize;
206        let rev_end = self.rev_row_offsets[dst_idx + 1] as usize;
207
208        self.rev_col_indices.insert(rev_end, src.0);
209        self.rev_edge_weights.insert(rev_end, weight);
210
211        // Update reverse row offsets for all nodes after dst
212        for offset in &mut self.rev_row_offsets[dst_idx + 1..] {
213            *offset += 1;
214        }
215
216        Ok(())
217    }
218
219    /// Get outgoing neighbors (callees) of a node
220    ///
221    /// # Errors
222    ///
223    /// Returns error if node ID is out of bounds
224    pub fn outgoing_neighbors(&self, node: NodeId) -> Result<&[u32]> {
225        if (node.0 as usize) >= self.num_nodes {
226            return Err(anyhow!("Node ID {} out of bounds", node.0));
227        }
228
229        let idx = node.0 as usize;
230        let start = self.row_offsets[idx] as usize;
231        let end = self.row_offsets[idx + 1] as usize;
232
233        Ok(&self.col_indices[start..end])
234    }
235
236    /// Get incoming neighbors (callers) of a node
237    ///
238    /// Returns O(1) access to incoming edges via reverse CSR.
239    ///
240    /// # Errors
241    ///
242    /// Returns error if node ID is out of bounds
243    pub fn incoming_neighbors(&self, target: NodeId) -> Result<&[u32]> {
244        if (target.0 as usize) >= self.num_nodes {
245            return Err(anyhow!("Node ID {} out of bounds", target.0));
246        }
247
248        let idx = target.0 as usize;
249        let start = self.rev_row_offsets[idx] as usize;
250        let end = self.rev_row_offsets[idx + 1] as usize;
251
252        Ok(&self.rev_col_indices[start..end])
253    }
254
255    /// Set node name (for debugging/export)
256    pub fn set_node_name(&mut self, node: NodeId, name: String) {
257        self.node_names.insert(node, name);
258    }
259
260    /// Get node name
261    #[must_use]
262    pub fn get_node_name(&self, node: NodeId) -> Option<&str> {
263        self.node_names.get(&node).map(String::as_str)
264    }
265
266    /// Get number of nodes
267    #[must_use]
268    pub const fn num_nodes(&self) -> usize {
269        self.num_nodes
270    }
271
272    /// Get number of edges
273    #[must_use]
274    pub fn num_edges(&self) -> usize {
275        self.col_indices.len()
276    }
277
278    /// Get row offsets as slice (for GPU upload)
279    #[must_use]
280    pub fn row_offsets_slice(&self) -> &[u32] {
281        &self.row_offsets
282    }
283
284    /// Get column indices as slice (for GPU upload)
285    #[must_use]
286    pub fn col_indices_slice(&self) -> &[u32] {
287        &self.col_indices
288    }
289
290    /// Get edge weights as slice (for GPU upload)
291    #[must_use]
292    pub fn edge_weights_slice(&self) -> &[f32] {
293        &self.edge_weights
294    }
295
296    /// Get adjacency list for a specific node
297    ///
298    /// Returns (targets, weights) slices for the node's outgoing edges
299    #[must_use]
300    pub fn adjacency(&self, node_id: NodeId) -> (&[u32], &[f32]) {
301        let idx = node_id.0 as usize;
302        if idx >= self.num_nodes {
303            return (&[], &[]);
304        }
305
306        let start = self.row_offsets[idx] as usize;
307        let end = self.row_offsets[idx + 1] as usize;
308
309        (&self.col_indices[start..end], &self.edge_weights[start..end])
310    }
311
312    /// Iterate over all nodes and their adjacency lists
313    ///
314    /// Yields (`node_id`, `targets`, `weights`) tuples
315    pub fn iter_adjacency(&self) -> impl Iterator<Item = (NodeId, &[u32], &[f32])> + '_ {
316        (0..self.num_nodes).map(move |node_id| {
317            let start = self.row_offsets[node_id] as usize;
318            let end = self.row_offsets[node_id + 1] as usize;
319
320            #[allow(clippy::cast_possible_truncation)]
321            (NodeId(node_id as u32), &self.col_indices[start..end], &self.edge_weights[start..end])
322        })
323    }
324
325    /// Expand graph to accommodate new nodes
326    fn expand_to(&mut self, new_size: usize) {
327        if new_size <= self.num_nodes {
328            return;
329        }
330
331        // Add row offsets for new nodes (all point to same offset = no edges)
332        let last_offset = *self.row_offsets.last().unwrap_or(&0);
333        for _ in self.num_nodes..new_size {
334            self.row_offsets.push(last_offset);
335        }
336
337        // Add reverse row offsets for new nodes
338        let rev_last_offset = *self.rev_row_offsets.last().unwrap_or(&0);
339        for _ in self.num_nodes..new_size {
340            self.rev_row_offsets.push(rev_last_offset);
341        }
342
343        self.num_nodes = new_size;
344    }
345
346    /// Get CSR components (for aprender integration)
347    #[must_use]
348    pub fn csr_components(&self) -> (&[u32], &[u32], &[f32]) {
349        (&self.row_offsets, &self.col_indices, &self.edge_weights)
350    }
351}
352
353impl Default for CsrGraph {
354    fn default() -> Self {
355        Self::new()
356    }
357}
358
359#[cfg(test)]
360mod tests {
361    use super::*;
362
363    #[test]
364    fn test_empty_graph() {
365        let graph = CsrGraph::new();
366        assert_eq!(graph.num_nodes(), 0);
367        assert_eq!(graph.num_edges(), 0);
368    }
369
370    #[test]
371    fn test_from_edge_list_simple() {
372        let edges = vec![
373            (NodeId(0), NodeId(1), 1.0),
374            (NodeId(0), NodeId(2), 1.0),
375            (NodeId(1), NodeId(2), 1.0),
376        ];
377
378        let graph = CsrGraph::from_edge_list(&edges).unwrap();
379
380        assert_eq!(graph.num_nodes(), 3);
381        assert_eq!(graph.num_edges(), 3);
382
383        // Check CSR structure
384        assert_eq!(graph.row_offsets, vec![0, 2, 3, 3]);
385        assert_eq!(graph.col_indices, vec![1, 2, 2]);
386        assert_eq!(graph.edge_weights, vec![1.0, 1.0, 1.0]);
387    }
388
389    #[test]
390    fn test_outgoing_neighbors() {
391        let edges = vec![(NodeId(0), NodeId(1), 1.0), (NodeId(0), NodeId(2), 2.0)];
392
393        let graph = CsrGraph::from_edge_list(&edges).unwrap();
394
395        let neighbors = graph.outgoing_neighbors(NodeId(0)).unwrap();
396        assert_eq!(neighbors, &[1, 2]);
397
398        let neighbors = graph.outgoing_neighbors(NodeId(1)).unwrap();
399        let empty: &[u32] = &[];
400        assert_eq!(neighbors, empty);
401    }
402
403    #[test]
404    fn test_incoming_neighbors() {
405        let edges = vec![(NodeId(0), NodeId(2), 1.0), (NodeId(1), NodeId(2), 1.0)];
406
407        let graph = CsrGraph::from_edge_list(&edges).unwrap();
408
409        let callers = graph.incoming_neighbors(NodeId(2)).unwrap();
410        assert_eq!(callers.len(), 2);
411        assert!(callers.contains(&0));
412        assert!(callers.contains(&1));
413    }
414
415    #[test]
416    fn test_reverse_csr_structure() {
417        // Build a simple graph to verify reverse CSR structure
418        let edges = vec![
419            (NodeId(0), NodeId(1), 1.0), // 0 → 1
420            (NodeId(0), NodeId(2), 2.0), // 0 → 2
421            (NodeId(1), NodeId(2), 3.0), // 1 → 2
422        ];
423
424        let graph = CsrGraph::from_edge_list(&edges).unwrap();
425
426        // Node 0: no incoming edges
427        let empty: &[u32] = &[];
428        assert_eq!(graph.incoming_neighbors(NodeId(0)).unwrap(), empty);
429
430        // Node 1: incoming from 0
431        assert_eq!(graph.incoming_neighbors(NodeId(1)).unwrap(), &[0]);
432
433        // Node 2: incoming from 0 and 1
434        let node2_incoming = graph.incoming_neighbors(NodeId(2)).unwrap();
435        assert_eq!(node2_incoming.len(), 2);
436        assert!(node2_incoming.contains(&0));
437        assert!(node2_incoming.contains(&1));
438    }
439
440    #[test]
441    fn test_reverse_csr_multi_edges() {
442        // Test that reverse CSR correctly handles multi-edges (duplicate edges)
443        let edges = vec![
444            (NodeId(0), NodeId(1), 1.0),
445            (NodeId(0), NodeId(1), 2.0), // Duplicate edge with different weight
446            (NodeId(2), NodeId(1), 3.0),
447        ];
448
449        let graph = CsrGraph::from_edge_list(&edges).unwrap();
450
451        // Node 1 should have 3 incoming edges (2 from node 0, 1 from node 2)
452        let incoming = graph.incoming_neighbors(NodeId(1)).unwrap();
453        assert_eq!(incoming.len(), 3);
454
455        // Count occurrences
456        let count_0 = incoming.iter().filter(|&&x| x == 0).count();
457        let count_2 = incoming.iter().filter(|&&x| x == 2).count();
458
459        assert_eq!(count_0, 2, "Should have 2 edges from node 0");
460        assert_eq!(count_2, 1, "Should have 1 edge from node 2");
461    }
462
463    #[test]
464    fn test_reverse_csr_with_add_edge() {
465        // Test that reverse CSR is correctly updated when using add_edge
466        let mut graph = CsrGraph::new();
467
468        graph.add_edge(NodeId(0), NodeId(1), 1.0).unwrap();
469        graph.add_edge(NodeId(2), NodeId(1), 2.0).unwrap();
470
471        // Node 1 should have incoming edges from 0 and 2
472        let incoming = graph.incoming_neighbors(NodeId(1)).unwrap();
473        assert_eq!(incoming.len(), 2);
474        assert!(incoming.contains(&0));
475        assert!(incoming.contains(&2));
476
477        // Add another edge
478        graph.add_edge(NodeId(3), NodeId(1), 3.0).unwrap();
479
480        // Now node 1 should have 3 incoming edges
481        let incoming = graph.incoming_neighbors(NodeId(1)).unwrap();
482        assert_eq!(incoming.len(), 3);
483        assert!(incoming.contains(&0));
484        assert!(incoming.contains(&2));
485        assert!(incoming.contains(&3));
486    }
487
488    #[test]
489    fn test_add_edge_dynamic() {
490        let mut graph = CsrGraph::new();
491
492        graph.add_edge(NodeId(0), NodeId(1), 1.0).unwrap();
493        graph.add_edge(NodeId(0), NodeId(2), 1.0).unwrap();
494
495        assert_eq!(graph.num_nodes(), 3);
496        assert_eq!(graph.num_edges(), 2);
497
498        let neighbors = graph.outgoing_neighbors(NodeId(0)).unwrap();
499        assert_eq!(neighbors, &[1, 2]);
500    }
501
502    #[test]
503    fn test_node_names() {
504        let mut graph = CsrGraph::new();
505        graph.add_edge(NodeId(0), NodeId(1), 1.0).unwrap();
506
507        graph.set_node_name(NodeId(0), "main".to_string());
508        graph.set_node_name(NodeId(1), "parse_args".to_string());
509
510        assert_eq!(graph.get_node_name(NodeId(0)), Some("main"));
511        assert_eq!(graph.get_node_name(NodeId(1)), Some("parse_args"));
512    }
513
514    #[test]
515    fn test_csr_components() {
516        let edges = vec![(NodeId(0), NodeId(1), 1.0), (NodeId(0), NodeId(2), 2.0)];
517
518        let graph = CsrGraph::from_edge_list(&edges).unwrap();
519        let (row_offsets, col_indices, weights) = graph.csr_components();
520
521        assert_eq!(row_offsets, &[0, 2, 2, 2]);
522        assert_eq!(col_indices, &[1, 2]);
523        assert_eq!(weights, &[1.0, 2.0]);
524    }
525
526    #[test]
527    fn test_adjacency() {
528        let edges = vec![
529            (NodeId(0), NodeId(1), 1.5),
530            (NodeId(0), NodeId(2), 2.5),
531            (NodeId(1), NodeId(2), 3.5),
532        ];
533
534        let graph = CsrGraph::from_edge_list(&edges).unwrap();
535
536        // Node 0 has two outgoing edges
537        let (targets, weights) = graph.adjacency(NodeId(0));
538        assert_eq!(targets, &[1, 2]);
539        assert_eq!(weights, &[1.5, 2.5]);
540
541        // Node 1 has one outgoing edge
542        let (targets, weights) = graph.adjacency(NodeId(1));
543        assert_eq!(targets, &[2]);
544        assert_eq!(weights, &[3.5]);
545
546        // Node 2 has no outgoing edges
547        let (targets, weights) = graph.adjacency(NodeId(2));
548        let empty_u32: &[u32] = &[];
549        let empty_f32: &[f32] = &[];
550        assert_eq!(targets, empty_u32);
551        assert_eq!(weights, empty_f32);
552    }
553
554    #[test]
555    fn test_adjacency_out_of_bounds() {
556        let edges = vec![(NodeId(0), NodeId(1), 1.0)];
557        let graph = CsrGraph::from_edge_list(&edges).unwrap();
558
559        // Out of bounds node should return empty slices
560        let (targets, weights) = graph.adjacency(NodeId(999));
561        let empty_u32: &[u32] = &[];
562        let empty_f32: &[f32] = &[];
563        assert_eq!(targets, empty_u32);
564        assert_eq!(weights, empty_f32);
565    }
566
567    #[test]
568    fn test_iter_adjacency() {
569        let edges = vec![
570            (NodeId(0), NodeId(1), 1.0),
571            (NodeId(0), NodeId(2), 2.0),
572            (NodeId(1), NodeId(2), 3.0),
573        ];
574
575        let graph = CsrGraph::from_edge_list(&edges).unwrap();
576
577        let adjacencies: Vec<_> = graph.iter_adjacency().collect();
578
579        assert_eq!(adjacencies.len(), 3);
580
581        // Node 0
582        assert_eq!(adjacencies[0].0, NodeId(0));
583        assert_eq!(adjacencies[0].1, &[1, 2]);
584        assert_eq!(adjacencies[0].2, &[1.0, 2.0]);
585
586        // Node 1
587        assert_eq!(adjacencies[1].0, NodeId(1));
588        assert_eq!(adjacencies[1].1, &[2]);
589        assert_eq!(adjacencies[1].2, &[3.0]);
590
591        // Node 2
592        assert_eq!(adjacencies[2].0, NodeId(2));
593        let empty_u32: &[u32] = &[];
594        let empty_f32: &[f32] = &[];
595        assert_eq!(adjacencies[2].1, empty_u32);
596        assert_eq!(adjacencies[2].2, empty_f32);
597    }
598
599    #[test]
600    fn test_slice_methods() {
601        let edges = vec![(NodeId(0), NodeId(1), 1.0), (NodeId(0), NodeId(2), 2.0)];
602        let graph = CsrGraph::from_edge_list(&edges).unwrap();
603
604        // Test slice methods
605        assert_eq!(graph.row_offsets_slice(), &[0, 2, 2, 2]);
606        assert_eq!(graph.col_indices_slice(), &[1, 2]);
607        assert_eq!(graph.edge_weights_slice(), &[1.0, 2.0]);
608    }
609
610    #[test]
611    fn test_get_node_name_nonexistent() {
612        let graph = CsrGraph::new();
613        assert_eq!(graph.get_node_name(NodeId(0)), None);
614    }
615
616    #[test]
617    fn test_empty_adjacency_iterator() {
618        let graph = CsrGraph::new();
619        let adjacencies: Vec<_> = graph.iter_adjacency().collect();
620        assert_eq!(adjacencies.len(), 0);
621    }
622}