Skip to main content

netoptim_rs/
lib.rs

1//! Network optimization algorithms in Rust.
2
3/// Dijkstra's shortest path algorithm implementation.
4pub mod dijkstra;
5
6/// Error types for network optimization.
7pub mod error;
8
9/// Negative cycle detection using Howard's algorithm.
10pub mod neg_cycle;
11
12/// Maximum parametric optimization.
13pub mod parametric;
14
15/// Graph utility functions.
16pub mod utils;
17
18pub use error::NetOptimError;
19pub use utils::*;
20
21#[cfg(test)]
22mod integration_tests;
23
24// Logging module - only available with std feature
25#[cfg(feature = "std")]
26pub mod logging;
27
28use petgraph::prelude::*;
29
30use petgraph::algo::{FloatMeasure, NegativeCycle};
31use petgraph::visit::{
32    IntoEdges, IntoNodeIdentifiers, NodeCount, NodeIndexable, VisitMap, Visitable,
33};
34
35/// Result of shortest path algorithms.
36///
37/// Contains the distances from the source node to all other nodes,
38/// and the predecessor of each node along the shortest path.
39#[derive(Debug, Clone)]
40pub struct Paths<NodeId, EdgeWeight> {
41    pub distances: Vec<EdgeWeight>,
42    pub predecessors: Vec<Option<NodeId>>,
43}
44
45/// \[Generic\] Compute shortest paths from node `source` to all other.
46///
47/// Using the [Bellman–Ford algorithm][bf]; negative edge costs are
48/// permitted, but the graph must not have a cycle of negative weights
49/// (in that case it will return an error).
50///
51/// On success, return one vec with path costs, and another one which points
52/// out the predecessor of a node along a shortest path. The vectors
53/// are indexed by the graph's node indices.
54///
55/// [bf]: https://en.wikipedia.org/wiki/Bellman%E2%80%93Ford_algorithm
56///
57/// # Example
58/// ```rust
59/// use petgraph::Graph;
60/// use petgraph::algo::bellman_ford;
61/// use petgraph::prelude::*;
62///
63/// let mut g = Graph::new();
64/// let a = g.add_node(()); // node with no weight
65/// let b = g.add_node(());
66/// let c = g.add_node(());
67/// let d = g.add_node(());
68/// let edge = g.add_node(());
69/// let f = g.add_node(());
70/// g.extend_with_edges(&[
71///     (0, 1, 2.0),
72///     (0, 3, 4.0),
73///     (1, 2, 1.0),
74///     (1, 5, 7.0),
75///     (2, 4, 5.0),
76///     (4, 5, 1.0),
77///     (3, 4, 1.0),
78/// ]);
79///
80/// // Graph represented with the weight of each edge
81/// //
82/// //     2       1
83/// // a ----- b ----- c
84/// // | 4     | 7     |
85/// // d       f       | 5
86/// // | 1     | 1     |
87/// // \------ edge ------/
88///
89/// let path = bellman_ford(&g, a);
90/// assert!(path.is_ok());
91/// let path = path.unwrap();
92/// assert_eq!(path.distances, vec![    0.0,     2.0,    3.0,    4.0,     5.0,     6.0]);
93/// assert_eq!(path.predecessors, vec![None, Some(a),Some(b),Some(a), Some(d), Some(edge)]);
94///
95/// // Node f (indice 5) can be reach from a with a path costing 6.
96/// // Predecessor of f is Some(edge) which predecessor is Some(d) which predecessor is Some(a).
97/// // Thus the path from a to f is a <-> d <-> edge <-> f
98///
99/// let graph_with_neg_cycle = Graph::<(), f32, Undirected>::from_edges(&[
100///         (0, 1, -2.0),
101///         (0, 3, -4.0),
102///         (1, 2, -1.0),
103///         (1, 5, -25.0),
104///         (2, 4, -5.0),
105///         (4, 5, -25.0),
106///         (3, 4, -1.0),
107/// ]);
108///
109/// assert!(bellman_ford(&graph_with_neg_cycle, NodeIndex::new(0)).is_err());
110/// ```
111pub fn bellman_ford<G>(
112    g: G,
113    source: G::NodeId,
114) -> Result<Paths<G::NodeId, G::EdgeWeight>, NegativeCycle>
115where
116    G: NodeCount + IntoNodeIdentifiers + IntoEdges + NodeIndexable,
117    G::EdgeWeight: FloatMeasure,
118{
119    let ix = |i| g.to_index(i);
120
121    // Step 1 and Step 2: initialize and relax
122    let (distances, predecessors) = bellman_ford_initialize_relax(g, source);
123
124    // Step 3: check for negative weight cycle
125    for i in g.node_identifiers() {
126        for edge in g.edges(i) {
127            let j = edge.target();
128            let w = *edge.weight();
129            if distances[ix(i)] + w < distances[ix(j)] {
130                return Err(NegativeCycle(()));
131            }
132        }
133    }
134
135    Ok(Paths {
136        distances,
137        predecessors,
138    })
139}
140
141/// \[Generic\] Find the path of a negative cycle reachable from node `source`.
142///
143/// Using the [find_negative_cycle][nc]; will search the Graph for negative cycles using
144/// [Bellman–Ford algorithm][bf]. If no negative cycle is found the function will return `None`.
145///
146/// If a negative cycle is found from source, return one vec with a path of `NodeId`s.
147///
148/// The time complexity of this algorithm should be the same as the Bellman-Ford (O(|V|·|E|)).
149///
150/// [nc]: https://blogs.asarkar.com/assets/docs/algorithms-curated/Negative-Weight%20Cycle%20Algorithms%20-%20Huang.pdf
151/// [bf]: https://en.wikipedia.org/wiki/Bellman%E2%80%93Ford_algorithm
152///
153/// # Example
154/// ```rust
155/// use petgraph::Graph;
156/// use petgraph::algo::find_negative_cycle;
157/// use petgraph::prelude::*;
158///
159/// let graph_with_neg_cycle = Graph::<(), f32, Directed>::from_edges(&[
160///         (0, 1, 1.),
161///         (0, 2, 1.),
162///         (0, 3, 1.),
163///         (1, 3, 1.),
164///         (2, 1, 1.),
165///         (3, 2, -3.),
166/// ]);
167///
168/// let path = find_negative_cycle(&graph_with_neg_cycle, NodeIndex::new(0));
169/// assert_eq!(
170///     path,
171///     Some([NodeIndex::new(1), NodeIndex::new(3), NodeIndex::new(2)].to_vec())
172/// );
173/// ```
174/// # Example: Graph with no negative cycle
175/// ```rust
176/// use petgraph::Graph;
177/// use petgraph::algo::find_negative_cycle;
178/// use petgraph::prelude::*;
179///
180/// let graph = Graph::<(), f32, Directed>::from_edges(&[
181///     (0, 1, 1.0),
182///     (1, 2, 1.0),
183///     (2, 3, 1.0),
184/// ]);
185/// let result = find_negative_cycle(&graph, NodeIndex::new(0));
186/// assert!(result.is_none());
187/// ```
188pub fn find_negative_cycle<G>(g: G, source: G::NodeId) -> Option<Vec<G::NodeId>>
189where
190    G: NodeCount + IntoNodeIdentifiers + IntoEdges + NodeIndexable + Visitable,
191    G::EdgeWeight: FloatMeasure,
192{
193    let ix = |i| g.to_index(i);
194    let mut path = Vec::<G::NodeId>::new();
195
196    // Step 1: initialize and relax
197    let (distance, predecessor) = bellman_ford_initialize_relax(g, source);
198
199    // Step 2: Check for negative weight cycle
200    'outer: for i in g.node_identifiers() {
201        for edge in g.edges(i) {
202            let j = edge.target();
203            let w = *edge.weight();
204            if distance[ix(i)] + w < distance[ix(j)] {
205                // Step 3: negative cycle found
206                let start = j;
207                let mut node = start;
208                let mut visited = g.visit_map();
209                // Go backward in the predecessor chain
210                loop {
211                    let ancestor = match predecessor[ix(node)] {
212                        Some(predecessor_node) => predecessor_node,
213                        None => node, // no predecessor, self cycle
214                    };
215                    // We have only 2 ways to find the cycle and break the loop:
216                    // 1. start is reached
217                    if ancestor == start {
218                        path.push(ancestor);
219                        break;
220                    }
221                    // 2. some node was reached twice
222                    else if visited.is_visited(&ancestor) {
223                        // Drop any node in path that is before the first ancestor
224                        let pos = path
225                            .iter()
226                            .position(|&p| p == ancestor)
227                            .expect("we should always have a position");
228                        path = path[pos..path.len()].to_vec();
229
230                        break;
231                    }
232
233                    // None of the above, some middle path node
234                    path.push(ancestor);
235                    visited.visit(ancestor);
236                    node = ancestor;
237                }
238                // We are done here
239                break 'outer;
240            }
241        }
242    }
243    if !path.is_empty() {
244        // Users will probably need to follow the path of the negative cycle
245        // so it should be in the reverse order than it was found by the algorithm.
246        path.reverse();
247        Some(path)
248    } else {
249        None
250    }
251}
252
253// Perform Step 1 and Step 2 of the Bellman-Ford algorithm.
254#[inline(always)]
255fn bellman_ford_initialize_relax<G>(
256    g: G,
257    source: G::NodeId,
258) -> (Vec<G::EdgeWeight>, Vec<Option<G::NodeId>>)
259where
260    G: NodeCount + IntoNodeIdentifiers + IntoEdges + NodeIndexable,
261    G::EdgeWeight: FloatMeasure,
262{
263    // Step 1: initialize graph
264    let mut predecessor = vec![None; g.node_bound()];
265    let mut distance = vec![<_>::infinite(); g.node_bound()];
266    let ix = |i| g.to_index(i);
267    distance[ix(source)] = <_>::zero();
268
269    // Step 2: relax edges repeatedly
270    for _ in 1..g.node_count() {
271        let mut did_update = false;
272        for i in g.node_identifiers() {
273            for edge in g.edges(i) {
274                let j = edge.target();
275                let w = *edge.weight();
276                if distance[ix(i)] + w < distance[ix(j)] {
277                    distance[ix(j)] = distance[ix(i)] + w;
278                    predecessor[ix(j)] = Some(i);
279                    did_update = true;
280                }
281            }
282        }
283        if !did_update {
284            break;
285        }
286    }
287    (distance, predecessor)
288}
289
290#[cfg(test)]
291mod tests {
292    use super::*;
293    use petgraph::Graph;
294
295    #[test]
296    fn test_bellman_ford_negative_cycle() {
297        let graph_with_neg_cycle =
298            Graph::<(), f32, Directed>::from_edges([(0, 1, 1.0), (1, 2, 1.0), (2, 0, -3.0)]);
299        let result = bellman_ford(&graph_with_neg_cycle, NodeIndex::new(0));
300        assert!(result.is_err());
301    }
302
303    #[test]
304    fn test_bellman_ford_no_edges() {
305        let mut graph = Graph::<(), f32, Directed>::new();
306        let n0 = graph.add_node(());
307        let result = bellman_ford(&graph, n0);
308        assert!(result.is_ok());
309        let paths = result.unwrap();
310        assert_eq!(paths.distances, vec![0.0]);
311        assert_eq!(paths.predecessors, vec![None]);
312    }
313
314    #[test]
315    fn test_bellman_ford_disconnected_components() {
316        let mut graph = Graph::<(), f32, Directed>::new();
317        let n0 = graph.add_node(());
318        let n1 = graph.add_node(());
319        let n2 = graph.add_node(());
320        graph.add_edge(n0, n1, 1.0);
321
322        let result = bellman_ford(&graph, n0);
323        assert!(result.is_ok());
324        let paths = result.unwrap();
325        // Node 2 is unreachable, so its distance should be infinite
326        assert_eq!(paths.distances.len(), 3);
327        assert_eq!(paths.distances[n0.index()], 0.0);
328        assert_eq!(paths.distances[n1.index()], 1.0);
329        assert!(paths.distances[n2.index()].is_infinite());
330        assert_eq!(paths.predecessors, vec![None, Some(n0), None]);
331    }
332
333    #[test]
334    fn test_find_negative_cycle_exists() {
335        let graph_with_neg_cycle =
336            Graph::<(), f32, Directed>::from_edges([(0, 1, 1.0), (1, 2, 1.0), (2, 0, -3.0)]);
337        let result = find_negative_cycle(&graph_with_neg_cycle, NodeIndex::new(0));
338        assert!(result.is_some());
339        let cycle = result.unwrap();
340        assert_eq!(cycle.len(), 3);
341        assert!(cycle.contains(&NodeIndex::new(0)));
342        assert!(cycle.contains(&NodeIndex::new(1)));
343        assert!(cycle.contains(&NodeIndex::new(2)));
344    }
345
346    #[test]
347    fn test_find_negative_cycle_none() {
348        let graph = Graph::<(), f32, Directed>::from_edges([(0, 1, 1.0), (1, 2, 1.0), (2, 3, 1.0)]);
349        let result = find_negative_cycle(&graph, NodeIndex::new(0));
350        assert!(result.is_none());
351    }
352
353    #[test]
354    fn test_find_negative_cycle_unreachable() {
355        let graph =
356            Graph::<(), f32, Directed>::from_edges([(0, 1, 1.0), (2, 3, -1.0), (3, 2, -1.0)]);
357        let result = find_negative_cycle(&graph, NodeIndex::new(0));
358        assert!(result.is_none());
359    }
360    use crate::neg_cycle::NegCycleFinder;
361    use crate::parametric::{MaxParametricSolver, ParametricAPI};
362    use num::rational::Ratio;
363    use petgraph::graph::{DiGraph, EdgeReference};
364
365    #[test]
366    fn test_neg_cycle_multiple_neg_cycles() {
367        let digraph = DiGraph::<(), Ratio<i32>>::from_edges([
368            (0, 1, Ratio::new(1, 1)),
369            (1, 0, Ratio::new(-2, 1)), // Cycle 1: 0 -> 1 -> 0 (weight -1)
370            (2, 3, Ratio::new(1, 1)),
371            (3, 2, Ratio::new(-3, 1)), // Cycle 2: 2 -> 3 -> 2 (weight -2)
372        ]);
373
374        let mut ncf = NegCycleFinder::new(&digraph);
375        let mut dist = [
376            Ratio::new(0, 1),
377            Ratio::new(0, 1),
378            Ratio::new(0, 1),
379            Ratio::new(0, 1),
380        ];
381        let result = ncf.howard(&mut dist, |e| *e.weight());
382        assert!(result.is_some());
383        let cycle = result.unwrap();
384        let cycle_weight: Ratio<i32> = cycle.iter().map(|e| *e.weight()).sum();
385        assert!(cycle_weight < Ratio::new(0, 1));
386    }
387
388    #[test]
389    fn test_neg_cycle_not_reachable() {
390        let digraph = DiGraph::<(), Ratio<i32>>::from_edges([
391            (0, 1, Ratio::new(1, 1)),
392            (2, 3, Ratio::new(-1, 1)),
393            (3, 2, Ratio::new(-1, 1)),
394        ]);
395
396        let mut ncf = NegCycleFinder::new(&digraph);
397        let mut dist = [
398            Ratio::new(0, 1),
399            Ratio::new(0, 1),
400            Ratio::new(0, 1),
401            Ratio::new(0, 1),
402        ];
403        let result = ncf.howard(&mut dist, |e| *e.weight());
404        assert!(result.is_some());
405    }
406
407    struct TestParametricAPI;
408
409    impl ParametricAPI<(), Ratio<i32>> for TestParametricAPI {
410        fn distance(&self, ratio: &Ratio<i32>, edge: &EdgeReference<Ratio<i32>>) -> Ratio<i32> {
411            *edge.weight() - *ratio
412        }
413
414        fn zero_cancel(&self, cycle: &[EdgeReference<Ratio<i32>>]) -> Ratio<i32> {
415            let mut sum_a = Ratio::new(0, 1);
416            let mut sum_b = Ratio::new(0, 1);
417            for edge in cycle {
418                sum_a += *edge.weight();
419                sum_b += Ratio::new(1, 1);
420            }
421            sum_a / sum_b
422        }
423    }
424
425    #[test]
426    fn test_max_parametric_solver_no_neg_cycle() {
427        let digraph = DiGraph::<(), Ratio<i32>>::from_edges([
428            (0, 1, Ratio::new(1, 1)),
429            (1, 2, Ratio::new(1, 1)),
430            (2, 0, Ratio::new(1, 1)),
431        ]);
432
433        let mut solver = MaxParametricSolver::new(&digraph, TestParametricAPI);
434        let mut dist = [Ratio::new(0, 1), Ratio::new(0, 1), Ratio::new(0, 1)];
435        let mut ratio = Ratio::new(0, 1);
436
437        let cycle = solver.run(&mut dist, &mut ratio);
438        assert!(cycle.is_empty());
439    }
440
441    #[test]
442    fn test_max_parametric_solver_multiple_neg_cycles() {
443        let digraph = DiGraph::<(), Ratio<i32>>::from_edges([
444            (0, 1, Ratio::new(1, 1)),
445            (1, 0, Ratio::new(-2, 1)), // Cycle 1: ratio -1/1
446            (2, 3, Ratio::new(1, 1)),
447            (3, 2, Ratio::new(-4, 1)), // Cycle 2: ratio -3/1
448        ]);
449
450        let mut solver = MaxParametricSolver::new(&digraph, TestParametricAPI);
451        let mut dist = [
452            Ratio::new(0, 1),
453            Ratio::new(0, 1),
454            Ratio::new(0, 1),
455            Ratio::new(0, 1),
456        ];
457        let mut ratio = Ratio::new(0, 1);
458
459        let cycle = solver.run(&mut dist, &mut ratio);
460        assert!(!cycle.is_empty());
461        assert_eq!(ratio, Ratio::new(-3, 2));
462    }
463
464    #[test]
465    fn test_bellman_ford_neg_cycle() {
466        let graph_with_neg_cycle =
467            Graph::<(), f32, Directed>::from_edges([(0, 1, 1.0), (1, 2, 1.0), (2, 0, -3.0)]);
468        let result = bellman_ford(&graph_with_neg_cycle, NodeIndex::new(0));
469        assert!(result.is_err());
470    }
471
472    #[test]
473    fn test_bellman_ford_no_edge() {
474        let mut graph = Graph::<(), f32, Directed>::new();
475        let n0 = graph.add_node(());
476        let result = bellman_ford(&graph, n0);
477        assert!(result.is_ok());
478        let paths = result.unwrap();
479        assert_eq!(paths.distances, vec![0.0]);
480        assert_eq!(paths.predecessors, vec![None]);
481    }
482
483    #[test]
484    fn test_bellman_ford_disconnected() {
485        let mut graph = Graph::<(), f32, Directed>::new();
486        let n0 = graph.add_node(());
487        let n1 = graph.add_node(());
488        let n2 = graph.add_node(());
489        graph.add_edge(n0, n1, 1.0);
490
491        let result = bellman_ford(&graph, n0);
492        assert!(result.is_ok());
493        let paths = result.unwrap();
494        // Node 2 is unreachable, so its distance should be infinite
495        assert_eq!(paths.distances.len(), 3);
496        assert_eq!(paths.distances[n0.index()], 0.0);
497        assert_eq!(paths.distances[n1.index()], 1.0);
498        assert!(paths.distances[n2.index()].is_infinite());
499        assert_eq!(paths.predecessors, vec![None, Some(n0), None]);
500    }
501
502    #[test]
503    fn test_find_negative_cycle_multiple() {
504        let graph_with_neg_cycle = Graph::<(), f32, Directed>::from_edges([
505            (0, 1, 1.0),
506            (1, 0, -2.0),
507            (2, 3, 1.0),
508            (3, 2, -3.0),
509        ]);
510        let result = find_negative_cycle(&graph_with_neg_cycle, NodeIndex::new(0));
511        assert!(result.is_some());
512    }
513
514    #[test]
515    fn test_find_negative_cycle_no_neg_cycle() {
516        let graph = Graph::<(), f32, Directed>::from_edges([(0, 1, 1.0), (1, 2, 1.0), (2, 3, 1.0)]);
517        let result = find_negative_cycle(&graph, NodeIndex::new(0));
518        assert!(result.is_none());
519    }
520
521    #[test]
522    fn test_find_negative_cycle_unreachable_neg_cycle() {
523        let graph =
524            Graph::<(), f32, Directed>::from_edges([(0, 1, 1.0), (2, 3, -1.0), (3, 2, -1.0)]);
525        let result = find_negative_cycle(&graph, NodeIndex::new(0));
526        assert!(result.is_none());
527    }
528}