Skip to main content

sim_lib_discrete_graph/
graph.rs

1//! The weighted graph value type and its adjacency expansion.
2
3use crate::edge::{Directedness, Edge};
4use crate::error::GraphError;
5
6/// A weighted graph over node labels `N` and edge weights `W`.
7///
8/// Algorithm-level node identity is the `usize` index into `nodes`; labels are
9/// payload and never affect correctness. Edge ids are stable. Multiedges and
10/// self-loops are representable. Undirected graphs store one record per edge;
11/// [`Graph::neighbors`] expands both directions.
12///
13/// # Examples
14///
15/// Build a small directed graph and read a node's outgoing adjacencies:
16///
17/// ```
18/// use sim_lib_discrete_graph::{Directedness, Graph};
19///
20/// let mut g: Graph<&str, u64> = Graph::new(Directedness::Directed);
21/// let a = g.add_node("a");
22/// let b = g.add_node("b");
23/// let c = g.add_node("c");
24/// g.add_edge(a, b, 1).unwrap();
25/// g.add_edge(a, c, 2).unwrap();
26///
27/// assert_eq!(g.node_count(), 3);
28/// assert_eq!(g.edge_count(), 2);
29///
30/// let neighbors: Vec<usize> = g.neighbors(a).unwrap().iter().map(|n| n.node).collect();
31/// assert_eq!(neighbors, vec![b, c]);
32/// ```
33#[derive(Debug, Clone, PartialEq)]
34pub struct Graph<N, W> {
35    /// Node labels, indexed by node id.
36    pub nodes: Vec<N>,
37    /// Edge records.
38    pub edges: Vec<Edge<W>>,
39    /// Whether edges are directed.
40    pub directedness: Directedness,
41}
42
43/// One adjacency: the edge taken and the neighbor reached.
44#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45pub struct Neighbor<'a, W> {
46    /// The traversed edge id.
47    pub edge_id: usize,
48    /// The neighbor node index.
49    pub node: usize,
50    /// The traversed edge weight.
51    pub weight: &'a W,
52}
53
54impl<N, W> Graph<N, W> {
55    /// An empty graph with the given directedness.
56    pub fn new(directedness: Directedness) -> Self {
57        Graph {
58            nodes: Vec::new(),
59            edges: Vec::new(),
60            directedness,
61        }
62    }
63
64    /// A graph seeded with `nodes` and no edges.
65    pub fn with_nodes(nodes: Vec<N>, directedness: Directedness) -> Self {
66        Graph {
67            nodes,
68            edges: Vec::new(),
69            directedness,
70        }
71    }
72
73    /// Number of nodes.
74    pub fn node_count(&self) -> usize {
75        self.nodes.len()
76    }
77
78    /// Number of edge records (one per undirected edge).
79    pub fn edge_count(&self) -> usize {
80        self.edges.len()
81    }
82
83    /// Whether the graph is directed.
84    pub fn is_directed(&self) -> bool {
85        matches!(self.directedness, Directedness::Directed)
86    }
87
88    /// Append a node label, returning its index.
89    pub fn add_node(&mut self, label: N) -> usize {
90        self.nodes.push(label);
91        self.nodes.len() - 1
92    }
93
94    /// Append an edge, validating endpoints and assigning a fresh id.
95    pub fn add_edge(
96        &mut self,
97        source: usize,
98        target: usize,
99        weight: W,
100    ) -> Result<usize, GraphError> {
101        let n = self.nodes.len();
102        let id = self.edges.len();
103        for node in [source, target] {
104            if node >= n {
105                return Err(GraphError::InvalidEndpoint {
106                    edge: id,
107                    node,
108                    len: n,
109                });
110            }
111        }
112        self.edges.push(Edge {
113            id,
114            source,
115            target,
116            weight,
117        });
118        Ok(id)
119    }
120
121    /// Validate that edge ids match storage order and every endpoint is in range.
122    ///
123    /// Public fields keep the value easy to encode, but id-indexed consumers
124    /// require `edges[i].id == i`. Sparse, duplicate, or shuffled ids are
125    /// rejected before bridge and certificate code indexes by edge id.
126    pub fn validate(&self) -> Result<(), GraphError> {
127        let n = self.nodes.len();
128        let edge_count = self.edges.len();
129        for (index, e) in self.edges.iter().enumerate() {
130            if e.id != index {
131                return Err(GraphError::InvalidEdgeId {
132                    index,
133                    id: e.id,
134                    len: edge_count,
135                });
136            }
137            for node in [e.source, e.target] {
138                if node >= n {
139                    return Err(GraphError::InvalidEndpoint {
140                        edge: e.id,
141                        node,
142                        len: n,
143                    });
144                }
145            }
146        }
147        Ok(())
148    }
149
150    /// Outgoing adjacencies of `node`, in deterministic ascending neighbor order
151    /// (ties broken by edge id). For undirected graphs both directions expand.
152    pub fn neighbors(&self, node: usize) -> Result<Vec<Neighbor<'_, W>>, GraphError> {
153        if node >= self.nodes.len() {
154            return Err(GraphError::NodeOutOfRange {
155                node,
156                count: self.nodes.len(),
157            });
158        }
159        let directed = self.is_directed();
160        let mut out = Vec::new();
161        for e in &self.edges {
162            if e.source == node {
163                out.push(Neighbor {
164                    edge_id: e.id,
165                    node: e.target,
166                    weight: &e.weight,
167                });
168            } else if !directed && e.target == node {
169                out.push(Neighbor {
170                    edge_id: e.id,
171                    node: e.source,
172                    weight: &e.weight,
173                });
174            }
175        }
176        out.sort_by_key(|adj| (adj.node, adj.edge_id));
177        Ok(out)
178    }
179}
180
181#[cfg(test)]
182mod tests {
183    use super::*;
184
185    #[test]
186    fn empty_graph_is_valid() {
187        let g: Graph<(), ()> = Graph::new(Directedness::Undirected);
188        assert_eq!(g.node_count(), 0);
189        assert_eq!(g.edge_count(), 0);
190        assert!(g.validate().is_ok());
191    }
192
193    #[test]
194    fn add_edge_rejects_invalid_endpoint() {
195        let mut g: Graph<&str, u64> = Graph::with_nodes(vec!["a", "b"], Directedness::Directed);
196        let r = g.add_edge(0, 5, 1);
197        assert!(matches!(
198            r,
199            Err(GraphError::InvalidEndpoint { node: 5, .. })
200        ));
201    }
202
203    #[test]
204    fn validate_rejects_sparse_edge_id() {
205        let g = Graph {
206            nodes: vec![0, 1],
207            edges: vec![Edge {
208                id: 2,
209                source: 0,
210                target: 1,
211                weight: 7,
212            }],
213            directedness: Directedness::Directed,
214        };
215
216        assert!(matches!(
217            g.validate(),
218            Err(GraphError::InvalidEdgeId {
219                index: 0,
220                id: 2,
221                len: 1,
222            })
223        ));
224    }
225
226    #[test]
227    fn validate_rejects_duplicate_edge_id() {
228        let g = Graph {
229            nodes: vec![0, 1, 2],
230            edges: vec![
231                Edge {
232                    id: 0,
233                    source: 0,
234                    target: 1,
235                    weight: 7,
236                },
237                Edge {
238                    id: 0,
239                    source: 1,
240                    target: 2,
241                    weight: 9,
242                },
243            ],
244            directedness: Directedness::Directed,
245        };
246
247        assert!(matches!(
248            g.validate(),
249            Err(GraphError::InvalidEdgeId {
250                index: 1,
251                id: 0,
252                len: 2,
253            })
254        ));
255    }
256
257    #[test]
258    fn self_loop_and_multiedge_preserved() {
259        let mut g: Graph<u8, u64> = Graph::with_nodes(vec![0, 1], Directedness::Undirected);
260        g.add_edge(0, 0, 1).unwrap(); // self-loop
261        g.add_edge(0, 1, 2).unwrap();
262        g.add_edge(0, 1, 3).unwrap(); // parallel edge
263        assert_eq!(g.edge_count(), 3);
264        assert!(g.edges[0].is_self_loop());
265    }
266
267    #[test]
268    fn undirected_neighbors_expand_both_ways() {
269        let mut g: Graph<u8, u64> = Graph::with_nodes(vec![0, 1, 2], Directedness::Undirected);
270        g.add_edge(0, 1, 10).unwrap();
271        g.add_edge(2, 1, 20).unwrap();
272        let n1: Vec<usize> = g.neighbors(1).unwrap().iter().map(|a| a.node).collect();
273        assert_eq!(n1, vec![0, 2]); // sorted ascending
274    }
275
276    #[test]
277    fn directed_neighbors_are_outgoing_only() {
278        let mut g: Graph<u8, u64> = Graph::with_nodes(vec![0, 1], Directedness::Directed);
279        g.add_edge(0, 1, 1).unwrap();
280        assert_eq!(g.neighbors(0).unwrap().len(), 1);
281        assert_eq!(g.neighbors(1).unwrap().len(), 0);
282    }
283}