Skip to main content

netoptim_rs/
dijkstra.rs

1//! Dijkstra's shortest path algorithm implementation.
2
3use petgraph::algo::FloatMeasure;
4#[allow(unused_imports)]
5use petgraph::graph::NodeIndex;
6use petgraph::visit::{
7    EdgeRef, IntoEdges, IntoNodeIdentifiers, NodeCount, NodeIndexable, VisitMap, Visitable,
8};
9use std::cmp::Ordering;
10use std::collections::BinaryHeap;
11
12/// Result of Dijkstra's shortest path algorithm.
13///
14/// Contains the distances from the source node to all other nodes,
15/// and the predecessor of each node along the shortest path.
16#[derive(Debug, Clone)]
17pub struct DijkstraResult<NodeId, EdgeWeight> {
18    pub distances: Vec<EdgeWeight>,
19    pub predecessors: Vec<Option<NodeId>>,
20}
21
22/// State for the priority queue in Dijkstra's algorithm.
23/// Contains a node and its current cost from the source.
24#[derive(Clone, Debug)]
25struct State<NodeId, Cost> {
26    node: NodeId,
27    cost: Cost,
28}
29
30impl<NodeId: PartialEq, Cost: PartialEq> PartialEq for State<NodeId, Cost> {
31    fn eq(&self, other: &Self) -> bool {
32        self.node == other.node && self.cost == other.cost
33    }
34}
35
36impl<NodeId: PartialEq, Cost: PartialEq> Eq for State<NodeId, Cost> {}
37
38impl<NodeId: PartialEq, Cost: FloatMeasure> Ord for State<NodeId, Cost> {
39    fn cmp(&self, other: &Self) -> Ordering {
40        // Use partial_cmp and unwrap since FloatMeasure guarantees total ordering via NaN handling
41        other
42            .cost
43            .partial_cmp(&self.cost)
44            .unwrap_or(Ordering::Equal)
45    }
46}
47
48impl<NodeId: PartialEq, Cost: FloatMeasure> PartialOrd for State<NodeId, Cost> {
49    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
50        Some(self.cmp(other))
51    }
52}
53
54/// \[Generic\] Compute shortest paths from node `source` to all other nodes using Dijkstra's algorithm.
55///
56/// $$ \text{dist}\[v\] = \min_{(u,v) \in E} (\text{dist}\[u\] + w(u,v)) $$
57///
58/// This implementation uses a binary heap for efficient priority queue operations.
59/// The algorithm requires non-negative edge weights.
60///
61/// # Arguments
62/// * `g` - The graph to compute shortest paths on
63/// * `source` - The source node index
64///
65/// # Returns
66/// * `Ok(DijkstraResult)` - Contains distances and predecessors for each node
67/// * `Err` - If a negative edge weight is found
68///
69/// # Example
70/// ```rust
71/// use petgraph::Graph;
72/// use petgraph::prelude::*;
73/// use netoptim_rs::dijkstra::dijkstra;
74///
75/// let mut g = Graph::new();
76/// let a = g.add_node(()); // node with no weight
77/// let b = g.add_node(());
78/// let c = g.add_node(());
79/// let d = g.add_node(());
80/// g.extend_with_edges(&[
81///     (0, 1, 2.0),
82///     (0, 3, 4.0),
83///     (1, 2, 1.0),
84///     (2, 4, 5.0),
85///     (3, 4, 1.0),
86/// ]);
87///
88/// let result = dijkstra(&g, a);
89/// assert!(result.is_ok());
90/// let paths = result.unwrap();
91/// assert_eq!(paths.distances[a.index()], 0.0);
92/// assert_eq!(paths.distances[b.index()], 2.0);
93/// assert_eq!(paths.distances[c.index()], 3.0);
94/// ```
95pub fn dijkstra<G>(
96    g: G,
97    source: G::NodeId,
98) -> Result<DijkstraResult<G::NodeId, G::EdgeWeight>, String>
99where
100    G: NodeCount + IntoNodeIdentifiers + IntoEdges + NodeIndexable + Visitable,
101    G::EdgeWeight: FloatMeasure,
102{
103    let ix = |i| g.to_index(i);
104    let node_count = g.node_count();
105
106    let mut distances = vec![<_>::infinite(); node_count];
107    let mut predecessors = vec![None; node_count];
108    let mut visited = g.visit_map();
109
110    distances[ix(source)] = <_>::zero();
111
112    let mut heap = BinaryHeap::new();
113    heap.push(State {
114        node: source,
115        cost: <_>::zero(),
116    });
117
118    while let Some(State { node, cost }) = heap.pop() {
119        if visited.is_visited(&node) {
120            continue;
121        }
122
123        visited.visit(node);
124
125        for edge in g.edges(node) {
126            let target = edge.target();
127            let weight = *edge.weight();
128
129            if weight < <_>::zero() {
130                return Err("Dijkstra's algorithm requires non-negative edge weights".to_string());
131            }
132
133            let new_cost = cost + weight;
134            if new_cost < distances[ix(target)] {
135                distances[ix(target)] = new_cost;
136                predecessors[ix(target)] = Some(node);
137                heap.push(State {
138                    node: target,
139                    cost: new_cost,
140                });
141            }
142        }
143    }
144
145    Ok(DijkstraResult {
146        distances,
147        predecessors,
148    })
149}
150
151/// \[Generic\] Compute shortest path from `source` to `target` using Dijkstra's algorithm.
152///
153/// $$ \text{dist}(t) = \min_{P \in \text{paths}(s,t)} \sum_{(u,v) \in P} w(u,v) $$
154///
155/// # Arguments
156/// * `g` - The graph to compute shortest path on
157/// * `source` - The source node index
158/// * `target` - The target node index
159///
160/// # Returns
161/// * `Some(path)` - A vector of node indices representing the shortest path from source to target
162/// * `None` - If no path exists
163///
164/// # Example
165/// ```rust
166/// use petgraph::Graph;
167/// use petgraph::prelude::*;
168/// use netoptim_rs::dijkstra::dijkstra_path;
169///
170/// let mut g = Graph::new();
171/// let a = g.add_node(());
172/// let b = g.add_node(());
173/// let c = g.add_node(());
174/// g.extend_with_edges(&[(0, 1, 2.0), (1, 2, 3.0)]);
175///
176/// let path = dijkstra_path(&g, a, c);
177/// assert_eq!(path, Some(vec![a, b, c]));
178/// ```
179pub fn dijkstra_path<G>(g: G, source: G::NodeId, target: G::NodeId) -> Option<Vec<G::NodeId>>
180where
181    G: NodeCount + IntoNodeIdentifiers + IntoEdges + NodeIndexable + Visitable,
182    G::EdgeWeight: FloatMeasure,
183    G::NodeId: PartialEq,
184{
185    let ix = |i| g.to_index(i);
186
187    let result = dijkstra(g, source).ok()?;
188
189    if result.predecessors[ix(target)].is_none() && source != target {
190        return None;
191    }
192
193    let mut path = vec![target];
194    let mut current = target;
195
196    while current != source {
197        let pred = result.predecessors[ix(current)]?;
198        path.push(pred);
199        current = pred;
200    }
201
202    path.reverse();
203    Some(path)
204}
205
206#[cfg(test)]
207mod tests {
208    use super::*;
209    use petgraph::Graph;
210
211    #[test]
212    fn test_dijkstra_simple() {
213        let mut g = Graph::new();
214        let a = g.add_node(());
215        let _b = g.add_node(());
216        let _c = g.add_node(());
217        let _d = g.add_node(());
218        g.extend_with_edges([(0, 1, 2.0), (0, 3, 4.0), (1, 2, 1.0), (3, 4, 1.0)]);
219
220        let result = dijkstra(&g, a);
221        assert!(result.is_ok());
222        let paths = result.unwrap();
223        assert_eq!(paths.distances[0], 0.0);
224        assert_eq!(paths.distances[1], 2.0);
225        assert_eq!(paths.distances[2], 3.0);
226        assert_eq!(paths.distances[3], 4.0);
227    }
228
229    #[test]
230    fn test_dijkstra_negative_weight() {
231        let g: Graph<(), f32> = Graph::<(), f32>::from_edges([(0, 1, -1.0)]);
232        let result = dijkstra(&g, NodeIndex::new(0));
233        assert!(result.is_err());
234    }
235
236    #[test]
237    fn test_dijkstra_path() {
238        let mut g = Graph::new();
239        let a = g.add_node(());
240        let b = g.add_node(());
241        let c = g.add_node(());
242        g.extend_with_edges([(0, 1, 2.0), (1, 2, 3.0)]);
243
244        let path = dijkstra_path(&g, a, c);
245        assert_eq!(path, Some(vec![a, b, c]));
246    }
247
248    #[test]
249    fn test_dijkstra_path_no_path() {
250        let mut g = Graph::new();
251        let a = g.add_node(());
252        let b = g.add_node(());
253        let c = g.add_node(());
254        g.add_edge(a, b, 1.0);
255
256        let path = dijkstra_path(&g, a, c);
257        assert_eq!(path, None);
258    }
259
260    #[test]
261    fn test_dijkstra_disconnected() {
262        let mut g: Graph<(), f64> = Graph::new();
263        let a = g.add_node(());
264        let b = g.add_node(());
265        let c = g.add_node(());
266        g.add_edge(a, b, 1.0);
267
268        let result = dijkstra(&g, a);
269        assert!(result.is_ok());
270        let paths = result.unwrap();
271        assert_eq!(paths.distances[a.index()], 0.0);
272        assert_eq!(paths.distances[b.index()], 1.0);
273        assert!(paths.distances[c.index()].is_infinite());
274    }
275
276    #[test]
277    fn test_dijkstra_same_node() {
278        let mut g: Graph<(), f64> = Graph::new();
279        let a = g.add_node(());
280
281        let result = dijkstra(&g, a);
282        assert!(result.is_ok());
283        let paths = result.unwrap();
284        assert_eq!(paths.distances[a.index()], 0.0);
285    }
286
287    #[test]
288    fn test_dijkstra_path_same_node() {
289        let mut g: Graph<(), f64> = Graph::new();
290        let a = g.add_node(());
291
292        let path = dijkstra_path(&g, a, a);
293        assert_eq!(path, Some(vec![a]));
294    }
295}