Skip to main content

uni_common/graph/
simple_graph.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2024-2026 Dragonscale Team
3
4//! Simple adjacency list graph for L0Buffer.
5//!
6//! This replaces gryf's DeltaGraph with a minimal implementation that provides:
7//! - O(1) vertex lookup
8//! - O(1) edge lookup by Eid
9//! - O(degree) neighbor iteration
10//! - Safe edge deletion without panics
11
12use crate::core::id::{Eid, Vid};
13use fxhash::FxBuildHasher;
14use std::collections::HashMap;
15
16/// Edge entry stored in adjacency lists.
17///
18/// The `edge_type` field uses the [`EdgeTypeId`](crate::core::edge_type::EdgeTypeId) encoding.
19#[derive(Clone, Copy, Debug)]
20pub struct EdgeEntry {
21    pub eid: Eid,
22    pub src_vid: Vid,
23    pub dst_vid: Vid,
24    pub edge_type: u32,
25}
26
27/// Type alias for FxHashMap (faster hashing for integer keys)
28type FxHashMap<K, V> = HashMap<K, V, FxBuildHasher>;
29
30/// Simple directed graph with adjacency lists.
31///
32/// Stores outgoing and incoming edge lists per vertex for O(degree) traversal,
33/// plus a direct Eid-to-edge map for O(1) edge lookup.
34#[derive(Debug)]
35pub struct SimpleGraph {
36    /// Set of vertices in the graph
37    vertices: FxHashMap<Vid, ()>,
38    /// Outgoing edges per vertex: src_vid -> [EdgeEntry]
39    outgoing: FxHashMap<Vid, Vec<EdgeEntry>>,
40    /// Incoming edges per vertex: dst_vid -> [EdgeEntry]
41    incoming: FxHashMap<Vid, Vec<EdgeEntry>>,
42    /// Direct edge lookup by Eid for O(1) access
43    edge_map: FxHashMap<Eid, EdgeEntry>,
44}
45
46/// Direction for neighbor traversal.
47#[derive(Clone, Copy, Debug, PartialEq, Eq)]
48pub enum Direction {
49    Outgoing,
50    Incoming,
51}
52
53impl Default for SimpleGraph {
54    fn default() -> Self {
55        Self {
56            vertices: HashMap::with_hasher(FxBuildHasher::default()),
57            outgoing: HashMap::with_hasher(FxBuildHasher::default()),
58            incoming: HashMap::with_hasher(FxBuildHasher::default()),
59            edge_map: HashMap::with_hasher(FxBuildHasher::default()),
60        }
61    }
62}
63
64impl SimpleGraph {
65    /// Creates a new empty graph.
66    pub fn new() -> Self {
67        Self::default()
68    }
69
70    /// Creates a new graph with pre-allocated capacity.
71    pub fn with_capacity(vertices: usize, edges: usize) -> Self {
72        Self {
73            vertices: HashMap::with_capacity_and_hasher(vertices, FxBuildHasher::default()),
74            outgoing: HashMap::with_capacity_and_hasher(vertices, FxBuildHasher::default()),
75            incoming: HashMap::with_capacity_and_hasher(vertices, FxBuildHasher::default()),
76            edge_map: HashMap::with_capacity_and_hasher(edges, FxBuildHasher::default()),
77        }
78    }
79
80    /// Adds a vertex to the graph. Returns true if the vertex was newly added.
81    pub fn add_vertex(&mut self, vid: Vid) -> bool {
82        if self.vertices.contains_key(&vid) {
83            return false;
84        }
85        self.vertices.insert(vid, ());
86        true
87    }
88
89    /// Removes a vertex and all its edges from the graph.
90    pub fn remove_vertex(&mut self, vid: Vid) {
91        self.vertices.remove(&vid);
92
93        // Remove outgoing edges from this vertex
94        if let Some(edges) = self.outgoing.remove(&vid) {
95            // Also remove from incoming lists of neighbors and edge_map
96            for edge in edges {
97                self.edge_map.remove(&edge.eid);
98                if let Some(incoming_list) = self.incoming.get_mut(&edge.dst_vid) {
99                    incoming_list.retain(|e| e.eid != edge.eid);
100                }
101            }
102        }
103
104        // Remove incoming edges to this vertex
105        if let Some(edges) = self.incoming.remove(&vid) {
106            // Also remove from outgoing lists of neighbors and edge_map
107            for edge in edges {
108                self.edge_map.remove(&edge.eid);
109                if let Some(outgoing_list) = self.outgoing.get_mut(&edge.src_vid) {
110                    outgoing_list.retain(|e| e.eid != edge.eid);
111                }
112            }
113        }
114    }
115
116    /// Checks if a vertex exists in the graph.
117    pub fn contains_vertex(&self, vid: Vid) -> bool {
118        self.vertices.contains_key(&vid)
119    }
120
121    /// Returns the number of vertices in the graph.
122    pub fn vertex_count(&self) -> usize {
123        self.vertices.len()
124    }
125
126    /// Adds an edge to the graph. Vertices are implicitly created if they don't exist.
127    pub fn add_edge(&mut self, src_vid: Vid, dst_vid: Vid, eid: Eid, edge_type: u32) {
128        // Ensure vertices exist
129        self.add_vertex(src_vid);
130        self.add_vertex(dst_vid);
131
132        self.add_edge_unchecked(src_vid, dst_vid, eid, edge_type);
133    }
134
135    /// Adds an edge without checking if vertices exist. Use when vertices are pre-added.
136    #[inline]
137    pub fn add_edge_unchecked(&mut self, src_vid: Vid, dst_vid: Vid, eid: Eid, edge_type: u32) {
138        let entry = EdgeEntry {
139            eid,
140            src_vid,
141            dst_vid,
142            edge_type,
143        };
144
145        // Add to outgoing list
146        self.outgoing.entry(src_vid).or_default().push(entry);
147
148        // Add to incoming list
149        self.incoming.entry(dst_vid).or_default().push(entry);
150
151        // Add to edge map for O(1) lookup
152        self.edge_map.insert(eid, entry);
153    }
154
155    /// Removes an edge by its ID. Returns the removed edge entry if found.
156    ///
157    /// Uses O(1) lookup via edge_map, then O(degree) removal from adjacency lists.
158    pub fn remove_edge(&mut self, eid: Eid) -> Option<EdgeEntry> {
159        // O(1) lookup via edge_map
160        let entry = self.edge_map.remove(&eid)?;
161
162        // Remove from outgoing list
163        if let Some(outgoing_list) = self.outgoing.get_mut(&entry.src_vid) {
164            outgoing_list.retain(|e| e.eid != eid);
165        }
166
167        // Remove from incoming list
168        if let Some(incoming_list) = self.incoming.get_mut(&entry.dst_vid) {
169            incoming_list.retain(|e| e.eid != eid);
170        }
171
172        Some(entry)
173    }
174
175    /// Returns neighbors in the specified direction.
176    /// O(degree) complexity.
177    pub fn neighbors(&self, vid: Vid, direction: Direction) -> &[EdgeEntry] {
178        let map = match direction {
179            Direction::Outgoing => &self.outgoing,
180            Direction::Incoming => &self.incoming,
181        };
182        map.get(&vid).map(|v| v.as_slice()).unwrap_or(&[])
183    }
184
185    /// Returns an iterator over all edges in the graph.
186    pub fn edges(&self) -> impl Iterator<Item = &EdgeEntry> {
187        self.outgoing.values().flat_map(|v| v.iter())
188    }
189
190    /// Returns the edge with the given Eid in O(1) time.
191    pub fn edge(&self, eid: Eid) -> Option<&EdgeEntry> {
192        self.edge_map.get(&eid)
193    }
194
195    /// Checks if a vertex exists and returns it (identity).
196    pub fn vertex(&self, vid: Vid) -> Option<Vid> {
197        if self.vertices.contains_key(&vid) {
198            Some(vid)
199        } else {
200            None
201        }
202    }
203
204    /// Returns the total number of edges in the graph.
205    pub fn edge_count(&self) -> usize {
206        self.outgoing.values().map(|v| v.len()).sum()
207    }
208
209    /// Returns an iterator over all vertices in the graph.
210    pub fn vertices(&self) -> impl Iterator<Item = Vid> + '_ {
211        self.vertices.keys().copied()
212    }
213
214    /// Clears the graph, removing all vertices and edges.
215    pub fn clear(&mut self) {
216        self.vertices.clear();
217        self.outgoing.clear();
218        self.incoming.clear();
219        self.edge_map.clear();
220    }
221}
222
223#[cfg(test)]
224mod tests {
225    use super::*;
226
227    #[test]
228    fn test_add_remove_vertex() {
229        let mut g = SimpleGraph::new();
230        let v1 = Vid::new(1);
231        let v2 = Vid::new(2);
232
233        assert!(g.add_vertex(v1));
234        assert!(!g.add_vertex(v1)); // Already exists
235        assert!(g.add_vertex(v2));
236
237        assert_eq!(g.vertex_count(), 2);
238        assert!(g.contains_vertex(v1));
239
240        g.remove_vertex(v1);
241        assert_eq!(g.vertex_count(), 1);
242        assert!(!g.contains_vertex(v1));
243    }
244
245    #[test]
246    fn test_add_remove_edge() {
247        let mut g = SimpleGraph::new();
248        let v1 = Vid::new(1);
249        let v2 = Vid::new(2);
250        let v3 = Vid::new(3);
251        let e1 = Eid::new(101);
252        let e2 = Eid::new(102);
253
254        g.add_edge(v1, v2, e1, 1);
255        g.add_edge(v1, v3, e2, 1);
256
257        assert_eq!(g.edge_count(), 2);
258
259        // Check outgoing neighbors of v1
260        let out = g.neighbors(v1, Direction::Outgoing);
261        assert_eq!(out.len(), 2);
262
263        // Check incoming neighbors of v2
264        let inc = g.neighbors(v2, Direction::Incoming);
265        assert_eq!(inc.len(), 1);
266        assert_eq!(inc[0].src_vid, v1);
267
268        // Remove edge
269        let removed = g.remove_edge(e1);
270        assert!(removed.is_some());
271        assert_eq!(removed.unwrap().dst_vid, v2);
272
273        assert_eq!(g.edge_count(), 1);
274        assert_eq!(g.neighbors(v1, Direction::Outgoing).len(), 1);
275        assert_eq!(g.neighbors(v2, Direction::Incoming).len(), 0);
276    }
277
278    #[test]
279    fn test_remove_vertex_with_edges() {
280        let mut g = SimpleGraph::new();
281        let v1 = Vid::new(1);
282        let v2 = Vid::new(2);
283        let v3 = Vid::new(3);
284        let e1 = Eid::new(101);
285        let e2 = Eid::new(102);
286
287        g.add_edge(v1, v2, e1, 1);
288        g.add_edge(v2, v3, e2, 1);
289
290        // Remove v2, should clean up edges to/from it
291        g.remove_vertex(v2);
292
293        assert_eq!(g.vertex_count(), 2);
294        assert_eq!(g.edge_count(), 0);
295        assert_eq!(g.neighbors(v1, Direction::Outgoing).len(), 0);
296        assert_eq!(g.neighbors(v3, Direction::Incoming).len(), 0);
297    }
298
299    #[test]
300    fn test_edge_iteration() {
301        let mut g = SimpleGraph::new();
302        let v1 = Vid::new(1);
303        let v2 = Vid::new(2);
304        let v3 = Vid::new(3);
305        let e1 = Eid::new(101);
306        let e2 = Eid::new(102);
307
308        g.add_edge(v1, v2, e1, 1);
309        g.add_edge(v2, v3, e2, 2);
310
311        let edges: Vec<_> = g.edges().collect();
312        assert_eq!(edges.len(), 2);
313    }
314
315    #[test]
316    fn test_neighbor_iteration_after_deletion() {
317        // This is the key test - ensure we don't panic after edge deletion
318        let mut g = SimpleGraph::new();
319        let v1 = Vid::new(1);
320        let v2 = Vid::new(2);
321        let v3 = Vid::new(3);
322        let e1 = Eid::new(101);
323        let e2 = Eid::new(102);
324
325        g.add_edge(v1, v2, e1, 1);
326        g.add_edge(v1, v3, e2, 1);
327
328        // Delete one edge
329        g.remove_edge(e1);
330
331        // Iterate neighbors - should not panic (unlike gryf)
332        let neighbors = g.neighbors(v1, Direction::Outgoing);
333        assert_eq!(neighbors.len(), 1);
334        assert_eq!(neighbors[0].dst_vid, v3);
335    }
336
337    #[test]
338    fn test_edge_lookup_o1() {
339        // Test O(1) edge lookup via edge_map
340        let mut g = SimpleGraph::new();
341        let v1 = Vid::new(1);
342        let v2 = Vid::new(2);
343        let v3 = Vid::new(3);
344        let e1 = Eid::new(101);
345        let e2 = Eid::new(102);
346
347        g.add_edge(v1, v2, e1, 1);
348        g.add_edge(v2, v3, e2, 2);
349
350        // O(1) lookup should work
351        let edge = g.edge(e1);
352        assert!(edge.is_some());
353        let edge = edge.unwrap();
354        assert_eq!(edge.src_vid, v1);
355        assert_eq!(edge.dst_vid, v2);
356        assert_eq!(edge.edge_type, 1);
357
358        // Lookup non-existent edge
359        let non_existent = Eid::new(999);
360        assert!(g.edge(non_existent).is_none());
361
362        // After removal, lookup should return None
363        g.remove_edge(e1);
364        assert!(g.edge(e1).is_none());
365        assert!(g.edge(e2).is_some()); // e2 should still exist
366    }
367}