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 every edge endpoint is within the node range.
122    pub fn validate(&self) -> Result<(), GraphError> {
123        let n = self.nodes.len();
124        for e in &self.edges {
125            for node in [e.source, e.target] {
126                if node >= n {
127                    return Err(GraphError::InvalidEndpoint {
128                        edge: e.id,
129                        node,
130                        len: n,
131                    });
132                }
133            }
134        }
135        Ok(())
136    }
137
138    /// Outgoing adjacencies of `node`, in deterministic ascending neighbor order
139    /// (ties broken by edge id). For undirected graphs both directions expand.
140    pub fn neighbors(&self, node: usize) -> Result<Vec<Neighbor<'_, W>>, GraphError> {
141        if node >= self.nodes.len() {
142            return Err(GraphError::NodeOutOfRange {
143                node,
144                count: self.nodes.len(),
145            });
146        }
147        let directed = self.is_directed();
148        let mut out = Vec::new();
149        for e in &self.edges {
150            if e.source == node {
151                out.push(Neighbor {
152                    edge_id: e.id,
153                    node: e.target,
154                    weight: &e.weight,
155                });
156            } else if !directed && e.target == node {
157                out.push(Neighbor {
158                    edge_id: e.id,
159                    node: e.source,
160                    weight: &e.weight,
161                });
162            }
163        }
164        out.sort_by_key(|adj| (adj.node, adj.edge_id));
165        Ok(out)
166    }
167}
168
169#[cfg(test)]
170mod tests {
171    use super::*;
172
173    #[test]
174    fn empty_graph_is_valid() {
175        let g: Graph<(), ()> = Graph::new(Directedness::Undirected);
176        assert_eq!(g.node_count(), 0);
177        assert_eq!(g.edge_count(), 0);
178        assert!(g.validate().is_ok());
179    }
180
181    #[test]
182    fn add_edge_rejects_invalid_endpoint() {
183        let mut g: Graph<&str, u64> = Graph::with_nodes(vec!["a", "b"], Directedness::Directed);
184        let r = g.add_edge(0, 5, 1);
185        assert!(matches!(
186            r,
187            Err(GraphError::InvalidEndpoint { node: 5, .. })
188        ));
189    }
190
191    #[test]
192    fn self_loop_and_multiedge_preserved() {
193        let mut g: Graph<u8, u64> = Graph::with_nodes(vec![0, 1], Directedness::Undirected);
194        g.add_edge(0, 0, 1).unwrap(); // self-loop
195        g.add_edge(0, 1, 2).unwrap();
196        g.add_edge(0, 1, 3).unwrap(); // parallel edge
197        assert_eq!(g.edge_count(), 3);
198        assert!(g.edges[0].is_self_loop());
199    }
200
201    #[test]
202    fn undirected_neighbors_expand_both_ways() {
203        let mut g: Graph<u8, u64> = Graph::with_nodes(vec![0, 1, 2], Directedness::Undirected);
204        g.add_edge(0, 1, 10).unwrap();
205        g.add_edge(2, 1, 20).unwrap();
206        let n1: Vec<usize> = g.neighbors(1).unwrap().iter().map(|a| a.node).collect();
207        assert_eq!(n1, vec![0, 2]); // sorted ascending
208    }
209
210    #[test]
211    fn directed_neighbors_are_outgoing_only() {
212        let mut g: Graph<u8, u64> = Graph::with_nodes(vec![0, 1], Directedness::Directed);
213        g.add_edge(0, 1, 1).unwrap();
214        assert_eq!(g.neighbors(0).unwrap().len(), 1);
215        assert_eq!(g.neighbors(1).unwrap().len(), 0);
216    }
217}