Skip to main content

formualizer_eval/engine/
csr_edges.rs

1use super::addr::VertexAddr;
2use super::vertex::VertexId;
3
4#[cfg(test)]
5mod tests {
6    use super::*;
7
8    use super::super::addr::GridAddr;
9
10    fn grid(row: u32, col: u32) -> VertexAddr {
11        VertexAddr::grid(GridAddr::new(row, col))
12    }
13
14    #[test]
15    fn test_csr_construction() {
16        let edges = vec![
17            (0u32, vec![1u32, 2u32]),
18            (1u32, vec![2u32, 3u32]),
19            (2u32, vec![3u32]),
20            (3u32, vec![]),
21        ];
22
23        let coords = vec![grid(0, 0), grid(0, 1), grid(1, 0), grid(1, 1)];
24
25        let csr = CsrEdges::from_adjacency(edges, &coords);
26
27        assert_eq!(csr.out_edges(VertexId(0)), &[VertexId(1), VertexId(2)]);
28        assert_eq!(csr.out_edges(VertexId(1)), &[VertexId(2), VertexId(3)]);
29        assert_eq!(csr.out_edges(VertexId(3)), &[]);
30    }
31
32    #[test]
33    fn test_csr_memory_efficiency() {
34        // 10K vertices, average 4 edges each
35        let mut edges = Vec::new();
36        let mut coords = Vec::new();
37
38        for i in 0..10_000u32 {
39            let targets: Vec<_> = (0..4).map(|j| (i + j + 1) % 10_000).collect();
40            edges.push((i, targets));
41            coords.push(grid(i, i));
42        }
43
44        let csr = CsrEdges::from_adjacency(edges, &coords);
45
46        // Should use ~200KB (40k edges × 4B + 10k vertices × 4B)
47        assert!(csr.memory_usage() < 410_000, "{}", csr.memory_usage());
48    }
49
50    #[test]
51    fn test_csr_edge_ordering() {
52        // Test that edges are sorted by (row, col, id) for determinism
53        let edges = vec![
54            (0u32, vec![3u32, 1u32, 2u32]), // Unsorted input
55        ];
56
57        let coords = vec![
58            grid(0, 0), // vertex 0
59            grid(0, 5), // vertex 1
60            grid(0, 3), // vertex 2
61            grid(1, 0), // vertex 3
62        ];
63
64        let csr = CsrEdges::from_adjacency(edges, &coords);
65
66        // Should be sorted by row first, then col: [1(0,5), 2(0,3), 3(1,0)]
67        // But row 0 comes before row 1, so order is: 2(0,3), 1(0,5), 3(1,0)
68        assert_eq!(
69            csr.out_edges(VertexId(0)),
70            &[VertexId(2), VertexId(1), VertexId(3)]
71        );
72    }
73
74    #[test]
75    fn test_csr_empty_graph() {
76        let edges: Vec<(u32, Vec<u32>)> = vec![];
77        let coords: Vec<VertexAddr> = vec![];
78
79        let csr = CsrEdges::from_adjacency(edges, &coords);
80
81        assert_eq!(csr.num_vertices(), 0);
82        assert_eq!(csr.num_edges(), 0);
83        // Empty graph has one offset entry (0) = 4 bytes
84        assert_eq!(csr.memory_usage(), 8);
85    }
86
87    #[test]
88    fn test_csr_single_vertex() {
89        let edges = vec![(0u32, vec![])];
90        let coords = vec![grid(0, 0)];
91
92        let csr = CsrEdges::from_adjacency(edges, &coords);
93
94        assert_eq!(csr.num_vertices(), 1);
95        assert_eq!(csr.num_edges(), 0);
96        assert_eq!(csr.out_edges(VertexId(0)), &[]);
97    }
98
99    #[test]
100    fn test_csr_self_loop() {
101        let edges = vec![(0u32, vec![0u32])]; // Self loop
102        let coords = vec![grid(0, 0)];
103
104        let csr = CsrEdges::from_adjacency(edges, &coords);
105
106        assert_eq!(csr.out_edges(VertexId(0)), &[VertexId(0)]);
107        assert_eq!(csr.num_edges(), 1);
108    }
109
110    #[test]
111    fn test_csr_duplicate_edges() {
112        // CSR should preserve duplicates (formulas can reference same cell multiple times)
113        let edges = vec![(0u32, vec![1u32, 1u32, 2u32, 1u32])];
114        let coords = vec![grid(0, 0), grid(0, 1), grid(0, 2)];
115
116        let csr = CsrEdges::from_adjacency(edges, &coords);
117
118        // Should preserve all edges, sorted by target coords
119        assert_eq!(
120            csr.out_edges(VertexId(0)),
121            &[VertexId(1), VertexId(1), VertexId(1), VertexId(2)]
122        );
123    }
124
125    #[test]
126    fn test_degree_calculation() {
127        let edges = vec![
128            (0u32, vec![1u32, 2u32, 3u32]),
129            (1u32, vec![2u32]),
130            (2u32, vec![]),
131            (3u32, vec![0u32, 1u32]),
132        ];
133
134        let coords = vec![grid(0, 0), grid(0, 1), grid(1, 0), grid(1, 1)];
135
136        let csr = CsrEdges::from_adjacency(edges, &coords);
137
138        assert_eq!(csr.out_degree(VertexId(0)), 3);
139        assert_eq!(csr.out_degree(VertexId(1)), 1);
140        assert_eq!(csr.out_degree(VertexId(2)), 0);
141        assert_eq!(csr.out_degree(VertexId(3)), 2);
142    }
143
144    #[test]
145    fn test_out_of_bounds_access() {
146        let edges = vec![(0u32, vec![])];
147        let coords = vec![grid(0, 0)];
148
149        let csr = CsrEdges::from_adjacency(edges, &coords);
150
151        // Should return empty slice - only vertex 0 exists
152        assert_eq!(csr.out_edges(VertexId(1)), &[]);
153    }
154
155    #[test]
156    fn test_csr_iterator() {
157        let edges = vec![
158            (0u32, vec![1u32, 2u32]),
159            (1u32, vec![3u32]),
160            (2u32, vec![1u32, 3u32]),
161            (3u32, vec![]),
162        ];
163
164        let coords = vec![grid(0, 0), grid(0, 1), grid(1, 0), grid(1, 1)];
165
166        let csr = CsrEdges::from_adjacency(edges, &coords);
167
168        let collected: Vec<_> = csr.iter().collect();
169        assert_eq!(collected.len(), 4);
170        assert_eq!(collected[0].0, VertexId(0));
171        assert_eq!(collected[0].1, &[VertexId(1), VertexId(2)]);
172        assert_eq!(collected[3].1, &[]);
173    }
174
175    #[test]
176    fn test_has_edge() {
177        let edges = vec![
178            (0u32, vec![1u32, 2u32]),
179            (1u32, vec![3u32]),
180            (2u32, vec![]),
181            (3u32, vec![0u32]), // Back edge
182        ];
183
184        let coords = vec![grid(0, 0), grid(0, 1), grid(1, 0), grid(1, 1)];
185
186        let csr = CsrEdges::from_adjacency(edges, &coords);
187
188        assert!(csr.has_edge(VertexId(0), VertexId(1)));
189        assert!(csr.has_edge(VertexId(0), VertexId(2)));
190        assert!(!csr.has_edge(VertexId(0), VertexId(3)));
191        assert!(csr.has_edge(VertexId(3), VertexId(0))); // Back edge exists
192        assert!(!csr.has_edge(VertexId(2), VertexId(0))); // No edge
193    }
194
195    #[test]
196    fn test_csr_with_offset_vertex_ids() {
197        // Test CSR with vertex IDs starting at 1024 (FIRST_NORMAL_VERTEX)
198        let base_id = 1024u32;
199        let edges = vec![
200            (base_id, vec![base_id + 1, base_id + 2]),
201            (base_id + 1, vec![base_id + 3]),
202            (base_id + 2, vec![base_id + 3]),
203            (base_id + 3, vec![]),
204        ];
205
206        let coords = vec![grid(0, 0), grid(0, 1), grid(1, 0), grid(1, 1)];
207
208        let csr = CsrEdges::from_adjacency(edges, &coords);
209
210        // Verify min vertex ID
211        assert_eq!(csr.min_vertex_id, base_id);
212
213        // Verify edges work with offset IDs
214        assert_eq!(
215            csr.out_edges(VertexId(base_id)),
216            &[VertexId(base_id + 1), VertexId(base_id + 2)]
217        );
218        assert_eq!(
219            csr.out_edges(VertexId(base_id + 1)),
220            &[VertexId(base_id + 3)]
221        );
222        assert_eq!(
223            csr.out_edges(VertexId(base_id + 2)),
224            &[VertexId(base_id + 3)]
225        );
226        assert_eq!(csr.out_edges(VertexId(base_id + 3)), &[]);
227
228        // Verify out of bounds returns empty
229        assert_eq!(csr.out_edges(VertexId(0)), &[]); // Before min
230        assert_eq!(csr.out_edges(VertexId(base_id + 100)), &[]); // After max
231    }
232
233    #[test]
234    fn test_csr_with_sparse_vertex_ids() {
235        // Test CSR with sparse vertex IDs (gaps in numbering)
236        let edges = vec![
237            (100u32, vec![300u32, 500u32]),
238            (300u32, vec![500u32]),
239            (500u32, vec![100u32]), // Back edge
240        ];
241
242        let coords = vec![
243            grid(0, 0), // For vertex 100 (index 0)
244            grid(0, 0), // Padding (index 100-199)
245            grid(1, 0), // For vertex 300 (index 200)
246            grid(0, 0), // Padding (index 300-399)
247            grid(2, 0), // For vertex 500 (index 400)
248        ];
249
250        let csr = CsrEdges::from_adjacency(edges, &coords);
251
252        // Verify min vertex ID
253        assert_eq!(csr.min_vertex_id, 100);
254
255        // Verify edges work
256        assert_eq!(
257            csr.out_edges(VertexId(100)),
258            &[VertexId(300), VertexId(500)]
259        );
260        assert_eq!(csr.out_edges(VertexId(300)), &[VertexId(500)]);
261        assert_eq!(csr.out_edges(VertexId(500)), &[VertexId(100)]);
262
263        // Non-existent vertices return empty
264        assert_eq!(csr.out_edges(VertexId(200)), &[]);
265        assert_eq!(csr.out_edges(VertexId(400)), &[]);
266    }
267}
268
269/// Compressed Sparse Row (CSR) format for edge storage
270///
271/// Replaces Vec<VertexId> per vertex with two arrays:
272/// - offsets: Start index for each vertex's edges
273/// - edges: All edges concatenated
274///
275/// Memory usage: O(V + E) instead of O(V * avg_degree * vec_overhead)
276#[derive(Debug, Clone)]
277pub struct CsrEdges {
278    /// Offsets into the edges array. Length = num_vertices + 1
279    /// offset[i] = start index of vertex i's edges
280    /// offset[i+1] - offset[i] = number of edges for vertex i
281    offsets: Vec<u32>,
282
283    /// All edges concatenated, sorted within each vertex's section
284    edges: Vec<VertexId>,
285
286    /// Reverse edges: offsets for incoming edges
287    reverse_offsets: Vec<u32>,
288
289    /// All incoming edges concatenated
290    reverse_edges: Vec<VertexId>,
291
292    /// Minimum vertex ID in the graph (for offset calculation)
293    min_vertex_id: u32,
294}
295
296impl CsrEdges {
297    /// Create CSR from adjacency list representation
298    ///
299    /// # Arguments
300    /// - adj: Vector of (vertex_id, outgoing_edges) where vertex_id is the actual VertexId value
301    /// - coords: Packed coordinates for each vertex (used for deterministic ordering)
302    ///
303    /// # Edge Ordering
304    /// Edges are sorted by (row, col, vertex_id) to ensure deterministic
305    /// evaluation order for formulas (important for functions with side effects)
306    pub fn from_adjacency(adj: Vec<(u32, Vec<u32>)>, coords: &[VertexAddr]) -> Self {
307        if adj.is_empty() {
308            return Self {
309                offsets: vec![0],
310                edges: Vec::new(),
311                reverse_offsets: vec![0],
312                reverse_edges: Vec::new(),
313                min_vertex_id: 0,
314            };
315        }
316
317        // Find min and max vertex IDs
318        let mut min_id = u32::MAX;
319        let mut max_id = 0;
320        for &(vid, ref targets) in &adj {
321            min_id = min_id.min(vid);
322            max_id = max_id.max(vid);
323            for &target in targets {
324                min_id = min_id.min(target);
325                max_id = max_id.max(target);
326            }
327        }
328
329        // If no vertices, return empty
330        if min_id == u32::MAX {
331            return Self {
332                offsets: vec![0],
333                edges: Vec::new(),
334                reverse_offsets: vec![0],
335                reverse_edges: Vec::new(),
336                min_vertex_id: 0,
337            };
338        }
339
340        let num_vertices = (max_id - min_id + 1) as usize;
341        let mut offsets = vec![0u32; num_vertices + 1];
342        let mut edges = Vec::new();
343
344        // Build adjacency data indexed by offset
345        let mut adj_by_offset: Vec<Vec<u32>> = vec![Vec::new(); num_vertices];
346        for (vid, targets) in adj {
347            let offset_idx = (vid - min_id) as usize;
348            adj_by_offset[offset_idx] = targets;
349        }
350
351        // Build forward edges
352        for (idx, mut targets) in adj_by_offset.clone().into_iter().enumerate() {
353            // Sort targets by their coordinates for deterministic ordering
354            targets.sort_by_key(|&t| {
355                // Convert vertex ID to index in coords array
356                let coord_idx = (t - min_id) as usize;
357                if coord_idx < coords.len() {
358                    let (major, minor) = coords[coord_idx].order_key();
359                    (major, minor, t)
360                } else {
361                    // Handle out-of-bounds gracefully for construction
362                    (u32::MAX, u32::MAX, t)
363                }
364            });
365
366            edges.extend(targets.into_iter().map(VertexId));
367            offsets[idx + 1] = edges.len() as u32;
368        }
369
370        // Build reverse edges (incoming edges for each vertex)
371        let mut reverse_offsets = vec![0u32; num_vertices + 1];
372        let mut reverse_edges = Vec::new();
373        let mut reverse_adj: Vec<Vec<u32>> = vec![Vec::new(); num_vertices];
374
375        // Collect reverse edges
376        for (idx, targets) in adj_by_offset.into_iter().enumerate() {
377            let source = min_id + idx as u32;
378            for target in targets {
379                let target_idx = (target - min_id) as usize;
380                if target_idx < num_vertices {
381                    reverse_adj[target_idx].push(source);
382                }
383            }
384        }
385
386        // Build reverse CSR
387        for (idx, mut sources) in reverse_adj.into_iter().enumerate() {
388            // Sort sources by their coordinates for deterministic ordering
389            sources.sort_by_key(|&s| {
390                let coord_idx = (s - min_id) as usize;
391                if coord_idx < coords.len() {
392                    let (major, minor) = coords[coord_idx].order_key();
393                    (major, minor, s)
394                } else {
395                    (u32::MAX, u32::MAX, s)
396                }
397            });
398
399            reverse_edges.extend(sources.into_iter().map(VertexId));
400            reverse_offsets[idx + 1] = reverse_edges.len() as u32;
401        }
402
403        Self {
404            offsets,
405            edges,
406            reverse_offsets,
407            reverse_edges,
408            min_vertex_id: min_id,
409        }
410    }
411
412    /// Get outgoing edges for a vertex
413    #[inline]
414    pub fn out_edges(&self, v: VertexId) -> &[VertexId] {
415        // Handle empty graph
416        if self.offsets.len() <= 1 {
417            return &[];
418        }
419
420        // Convert vertex ID to offset index
421        if v.0 < self.min_vertex_id {
422            return &[];
423        }
424
425        let idx = (v.0 - self.min_vertex_id) as usize;
426        if idx >= self.offsets.len() - 1 {
427            return &[];
428        }
429
430        let start = self.offsets[idx] as usize;
431        let end = self.offsets[idx + 1] as usize;
432        &self.edges[start..end]
433    }
434
435    /// Get incoming edges for a vertex (who depends on this vertex)
436    #[inline]
437    pub fn in_edges(&self, v: VertexId) -> &[VertexId] {
438        // Handle empty graph
439        if self.reverse_offsets.len() <= 1 {
440            return &[];
441        }
442
443        // Convert vertex ID to offset index
444        if v.0 < self.min_vertex_id {
445            return &[];
446        }
447
448        let idx = (v.0 - self.min_vertex_id) as usize;
449        if idx >= self.reverse_offsets.len() - 1 {
450            return &[];
451        }
452
453        let start = self.reverse_offsets[idx] as usize;
454        let end = self.reverse_offsets[idx + 1] as usize;
455        &self.reverse_edges[start..end]
456    }
457
458    /// Get the out-degree of a vertex
459    #[inline]
460    pub fn out_degree(&self, v: VertexId) -> usize {
461        // Handle empty graph
462        if self.offsets.len() <= 1 {
463            return 0;
464        }
465
466        // Convert vertex ID to offset index
467        if v.0 < self.min_vertex_id {
468            return 0;
469        }
470
471        let idx = (v.0 - self.min_vertex_id) as usize;
472        if idx >= self.offsets.len() - 1 {
473            return 0;
474        }
475
476        let start = self.offsets[idx];
477        let end = self.offsets[idx + 1];
478        (end - start) as usize
479    }
480
481    /// Get the in-degree of a vertex
482    #[inline]
483    pub fn in_degree(&self, v: VertexId) -> usize {
484        self.in_edges(v).len()
485    }
486
487    /// Number of vertices in the graph
488    #[inline]
489    pub fn num_vertices(&self) -> usize {
490        self.offsets.len().saturating_sub(1)
491    }
492
493    /// Total number of edges in the graph
494    #[inline]
495    pub fn num_edges(&self) -> usize {
496        self.edges.len()
497    }
498
499    /// Memory usage in bytes
500    pub fn memory_usage(&self) -> usize {
501        self.offsets.len() * std::mem::size_of::<u32>()
502            + self.edges.len() * std::mem::size_of::<VertexId>()
503            + self.reverse_offsets.len() * std::mem::size_of::<u32>()
504            + self.reverse_edges.len() * std::mem::size_of::<VertexId>()
505    }
506
507    /// Create an empty CSR graph
508    pub fn empty() -> Self {
509        Self {
510            offsets: vec![0],
511            edges: Vec::new(),
512            reverse_offsets: vec![0],
513            reverse_edges: Vec::new(),
514            min_vertex_id: 0,
515        }
516    }
517
518    /// Builder pattern for incremental construction
519    pub fn builder() -> CsrBuilder {
520        CsrBuilder::new()
521    }
522
523    /// Iterate over all vertices and their outgoing edges
524    pub fn iter(&'_ self) -> CsrIterator<'_> {
525        CsrIterator {
526            csr: self,
527            current_vertex: 0,
528        }
529    }
530
531    /// Check if the graph has a specific edge
532    pub fn has_edge(&self, from: VertexId, to: VertexId) -> bool {
533        self.out_edges(from).contains(&to)
534    }
535}
536
537/// Iterator over vertices and their edges
538pub struct CsrIterator<'a> {
539    csr: &'a CsrEdges,
540    current_vertex: usize,
541}
542
543impl<'a> Iterator for CsrIterator<'a> {
544    type Item = (VertexId, &'a [VertexId]);
545
546    fn next(&mut self) -> Option<Self::Item> {
547        if self.current_vertex >= self.csr.num_vertices() {
548            return None;
549        }
550
551        let vertex_id = VertexId(self.current_vertex as u32 + self.csr.min_vertex_id);
552        let edges = self.csr.out_edges(vertex_id);
553        self.current_vertex += 1;
554
555        Some((vertex_id, edges))
556    }
557}
558
559/// Builder for incremental CSR construction
560pub struct CsrBuilder {
561    adjacency: Vec<Vec<usize>>,
562    coords: Vec<VertexAddr>,
563}
564
565impl Default for CsrBuilder {
566    fn default() -> Self {
567        Self::new()
568    }
569}
570
571impl CsrBuilder {
572    pub fn new() -> Self {
573        Self {
574            adjacency: Vec::new(),
575            coords: Vec::new(),
576        }
577    }
578
579    /// Add a vertex with its coordinate
580    pub fn add_vertex(&mut self, addr: VertexAddr) -> usize {
581        let idx = self.adjacency.len();
582        self.adjacency.push(Vec::new());
583        self.coords.push(addr);
584        idx
585    }
586
587    /// Add an edge from source to target
588    pub fn add_edge(&mut self, from: usize, to: usize) {
589        if from < self.adjacency.len() {
590            self.adjacency[from].push(to);
591        }
592    }
593
594    /// Build the final CSR structure
595    pub fn build(self) -> CsrEdges {
596        // Convert to (vertex_id, edges) format starting from vertex ID 0
597        let adj: Vec<_> = self
598            .adjacency
599            .into_iter()
600            .enumerate()
601            .map(|(idx, edges)| (idx as u32, edges.into_iter().map(|e| e as u32).collect()))
602            .collect();
603        CsrEdges::from_adjacency(adj, &self.coords)
604    }
605}