Skip to main content

netoptim_rs/
neg_cycle.rs

1use petgraph::graph::{EdgeReference, NodeIndex};
2use petgraph::prelude::*;
3use petgraph::visit::EdgeRef;
4use petgraph::visit::IntoNodeIdentifiers;
5
6// use petgraph::visit::IntoNeighborsDirected;
7
8/// The `NegCycleFinder` struct is used to find negative cycles in a directed graph.
9///
10/// Properties:
11///
12/// * `digraph`: The `digraph` property is a reference to a directed graph (`DiGraph`) that the
13///   `NegCycleFinder` is operating on. It is annotated with a lifetime `'a`, indicating that the
14///   reference is valid for a certain scope.
15/// * `pred`: The `pred` property is a `HashMap` that maps a `NodeIndex` to a tuple containing the
16///   previous node index and an `EdgeReference`. This is used to keep track of the predecessor node and
17///   the edge that leads to that node during the process of finding negative cycles in a directed graph
18#[derive(Debug, Clone)]
19pub struct NegCycleFinder<'a, V, D> {
20    pub digraph: &'a DiGraph<V, D>,
21    pub pred: std::collections::HashMap<NodeIndex, (NodeIndex, EdgeReference<'a, D>)>,
22}
23
24impl<'a, V, D> NegCycleFinder<'a, V, D>
25where
26    D: std::ops::Add<Output = D> + std::cmp::PartialOrd + Copy,
27{
28    /// The `new` function creates a new `NegCycleFinder` object with an empty predecessor map.
29    ///
30    /// Arguments:
31    ///
32    /// * `digraph`: A reference to a directed graph (`DiGraph`) that the `NegCycleFinder` will operate on.
33    ///
34    /// Returns:
35    ///
36    /// The `new` function is returning an instance of the `NegCycleFinder<V, D>` struct.
37    /// Creates a new [`NegCycleFinder<V, D>`].
38    pub fn new(digraph: &'a DiGraph<V, D>) -> Self {
39        Self {
40            digraph,
41            pred: std::collections::HashMap::new(),
42        }
43    }
44
45    /// The `find_cycle` function in Rust returns the first node in a cycle found in a directed graph.
46    ///
47    /// Returns:
48    ///
49    /// The function `find_cycle` returns an `Option<NodeIndex>`.
50    pub fn find_cycle(&self) -> Option<NodeIndex> {
51        let mut visited = std::collections::HashMap::new();
52        for vtx in self.digraph.node_identifiers() {
53            if visited.contains_key(&vtx) {
54                continue;
55            }
56            let mut utx = vtx;
57            while !visited.contains_key(&utx) {
58                visited.insert(utx, vtx);
59                if !self.pred.contains_key(&utx) {
60                    break;
61                }
62                let result = *self.pred.get(&utx).unwrap();
63                utx = result.0;
64                if visited.contains_key(&utx) {
65                    if visited[&utx] == vtx {
66                        return Some(utx);
67                    }
68                    break;
69                }
70            }
71        }
72        None
73    }
74
75    /// The `relax` function updates the distances between nodes in a graph based on the weights of the
76    /// edges, and returns a boolean indicating whether any distances were changed.
77    ///
78    /// Arguments:
79    ///
80    /// * `dist`: `dist` is a mutable reference to a slice of type `D`. It represents the distances from
81    ///   a source node to each node in a graph.
82    /// * `get_weight`: The `get_weight` parameter is a closure that takes an `EdgeReference<D>` as
83    ///   input and returns a value of type `D`. This closure is used to calculate the weight of each edge
84    ///   in the graph. The `EdgeReference<D>` represents a reference to an edge in the graph, and
85    ///
86    /// Returns:
87    ///
88    /// a boolean value.
89    pub fn relax<F>(&mut self, dist: &mut [D], get_weight: F) -> bool
90    where
91        F: Fn(EdgeReference<D>) -> D,
92    {
93        let mut changed = false;
94        for utx in self.digraph.node_identifiers() {
95            for edge in self.digraph.edges(utx) {
96                let vtx = edge.target();
97                let weight = get_weight(edge);
98                // for utx in self.digraph.node_indices() {
99                //     for vtx in self
100                //         .digraph
101                //         .neighbors_directed(utx, petgraph::Direction::Outgoing)
102                //     {
103                // let weight = get_weight((utx, vtx));
104                let distance = dist[utx.index()] + weight;
105                if dist[vtx.index()] > distance {
106                    dist[vtx.index()] = distance;
107                    self.pred.insert(vtx, (utx, edge));
108                    changed = true;
109                }
110            }
111        }
112        changed
113    }
114
115    /// The `howard` function implements Howard's algorithm for finding negative cycles in a directed
116    /// graph.
117    ///
118    /// Arguments:
119    ///
120    /// * `dist`: `dist` is a mutable reference to an array of type `D`. This array is used to store the
121    ///   distances from the source vertex to each vertex in the graph. The algorithm will update the
122    ///   distances during the execution.
123    /// * `get_weight`: `get_weight` is a closure that takes an `EdgeReference<D>` and returns the
124    ///   weight of that edge. The `howard` function uses this closure to get the weight of each edge in
125    ///   the graph.
126    ///
127    /// Returns:
128    ///
129    /// The `howard` function returns an `Option<Vec<EdgeReference<'a, D>>>`.
130    /// Howard's algorithm for finding negative cycles
131    ///
132    /// # Examples
133    ///
134    /// ```
135    /// use petgraph::prelude::*;
136    /// use netoptim_rs::neg_cycle::NegCycleFinder;
137    /// let digraph = DiGraph::<(), i32>::from_edges([
138    ///     (0, 1, 1),
139    ///     (0, 2, 1),
140    ///     (0, 3, 1),
141    ///     (1, 3, 1),
142    ///     (2, 1, 1),
143    ///     (3, 2, -3),
144    /// ]);
145    /// let mut ncf = NegCycleFinder::new(&digraph);
146    /// let mut dist = [0, 0, 0, 0];
147    /// let result = ncf.howard(&mut dist, |e| { *e.weight()});
148    /// assert!(result.is_some());
149    /// ```
150    /// # Example: Graph with no negative cycle
151    /// ```rust
152    /// use petgraph::prelude::*;
153    /// use netoptim_rs::neg_cycle::NegCycleFinder;
154    /// use num::rational::Ratio;
155    /// let digraph = DiGraph::<(), Ratio<i32>>::from_edges([
156    ///     (0, 1, Ratio::new(1, 1)),
157    ///     (1, 2, Ratio::new(1, 1)),
158    ///     (2, 3, Ratio::new(1, 1)),
159    /// ]);
160    /// let mut ncf = NegCycleFinder::new(&digraph);
161    /// let mut dist = [
162    ///     Ratio::new(0, 1),
163    ///     Ratio::new(0, 1),
164    ///     Ratio::new(0, 1),
165    ///     Ratio::new(0, 1),
166    /// ];
167    /// let result = ncf.howard(&mut dist, |e| { *e.weight()});
168    /// assert!(result.is_none());
169    /// ```
170    pub fn howard<F>(&mut self, dist: &mut [D], get_weight: F) -> Option<Vec<EdgeReference<'a, D>>>
171    where
172        F: Fn(EdgeReference<D>) -> D,
173    {
174        self.pred.clear();
175        while self.relax(dist, &get_weight) {
176            let v_opt = self.find_cycle();
177            if let Some(vtx) = v_opt {
178                return Some(self.cycle_list(vtx));
179            }
180        }
181        None
182    }
183
184    /// The function `cycle_list` takes a node index as input and returns a vector of edge references
185    /// that form a cycle in a graph.
186    ///
187    /// Arguments:
188    ///
189    /// * `handle`: The `handle` parameter is of type `NodeIndex`. It represents the starting node index
190    ///   from which the cycle traversal will begin.
191    ///
192    /// Returns:
193    ///
194    /// The function `cycle_list` returns a vector of `EdgeReference` objects.
195    fn cycle_list(&self, handle: NodeIndex) -> Vec<EdgeReference<'a, D>> {
196        let mut vtx = handle;
197        let mut cycle = Vec::new();
198        loop {
199            let (utx, edge) = self.pred[&vtx];
200            cycle.push(edge);
201            vtx = utx;
202            if vtx == handle {
203                break;
204            }
205        }
206        cycle
207    }
208}
209
210#[cfg(test)]
211mod tests {
212    use super::*;
213    use num::rational::Ratio;
214
215    #[test]
216    fn it_works() {
217        let result = 2 + 2;
218        assert_eq!(result, 4);
219    }
220
221    #[test]
222    fn test_neg_cycle1() {
223        let digraph = DiGraph::<(), Ratio<i32>>::from_edges([
224            (0, 1, Ratio::new(1, 1)),
225            (0, 2, Ratio::new(1, 1)),
226            (0, 3, Ratio::new(1, 1)),
227            (1, 3, Ratio::new(1, 1)),
228            (2, 1, Ratio::new(1, 1)),
229            (3, 2, Ratio::new(-3, 1)),
230        ]);
231
232        let mut ncf = NegCycleFinder::new(&digraph);
233        let mut dist = [
234            Ratio::new(0, 1),
235            Ratio::new(0, 1),
236            Ratio::new(0, 1),
237            Ratio::new(0, 1),
238        ];
239        let result = ncf.howard(&mut dist, |e| *e.weight());
240        assert!(result.is_some());
241    }
242
243    #[test]
244    fn test_neg_cycle2() {
245        let mut graph = DiGraph::new();
246        let a = graph.add_node("a");
247        let b = graph.add_node("b");
248        let c = graph.add_node("c");
249        let d = graph.add_node("d");
250        let e = graph.add_node("e");
251        let f = graph.add_node("f");
252        let g = graph.add_node("g");
253        let h = graph.add_node("h");
254        let i = graph.add_node("i");
255        graph.add_edge(a, b, Ratio::new(1, 1));
256        graph.add_edge(a, c, Ratio::new(1, 1));
257        graph.add_edge(b, d, Ratio::new(1, 1));
258        graph.add_edge(c, d, Ratio::new(1, 1));
259        graph.add_edge(d, e, Ratio::new(-3, 1));
260        graph.add_edge(d, f, Ratio::new(1, 1));
261        graph.add_edge(e, g, Ratio::new(1, 1));
262        graph.add_edge(f, g, Ratio::new(1, 1));
263        graph.add_edge(g, h, Ratio::new(1, 1));
264        graph.add_edge(h, i, Ratio::new(1, 1));
265        graph.add_edge(i, f, Ratio::new(1, 1));
266
267        let mut ncf = NegCycleFinder::new(&graph);
268        let mut dist = [
269            Ratio::new(0, 1),
270            Ratio::new(0, 1),
271            Ratio::new(0, 1),
272            Ratio::new(0, 1),
273            Ratio::new(0, 1),
274            Ratio::new(0, 1),
275            Ratio::new(0, 1),
276            Ratio::new(0, 1),
277            Ratio::new(0, 1),
278        ];
279        let result = ncf.howard(&mut dist, |e| *e.weight());
280        assert!(result.is_none());
281    }
282
283    #[test]
284    fn test_neg_cycle_no_edges() {
285        let digraph = DiGraph::<(), Ratio<i32>>::new();
286        let mut ncf = NegCycleFinder::new(&digraph);
287        let mut dist = [];
288        let result = ncf.howard(&mut dist, |e| *e.weight());
289        assert!(result.is_none());
290    }
291
292    #[test]
293    fn test_neg_cycle_self_loop() {
294        let mut digraph = DiGraph::<(), Ratio<i32>>::new();
295        let n0 = digraph.add_node(());
296        digraph.add_edge(n0, n0, Ratio::new(-1, 1));
297
298        let mut ncf = NegCycleFinder::new(&digraph);
299        let mut dist = [Ratio::new(0, 1)];
300        let result = ncf.howard(&mut dist, |e| *e.weight());
301        assert!(result.is_some());
302        let cycle = result.unwrap();
303        assert_eq!(cycle.len(), 1);
304        assert_eq!(cycle[0].source(), n0);
305        assert_eq!(cycle[0].target(), n0);
306    }
307
308    #[test]
309    fn test_neg_cycle_multiple_cycles() {
310        let digraph = DiGraph::<(), Ratio<i32>>::from_edges([
311            (0, 1, Ratio::new(1, 1)),
312            (1, 0, Ratio::new(-2, 1)), // Cycle 1: 0 -> 1 -> 0 (weight -1)
313            (2, 3, Ratio::new(1, 1)),
314            (3, 2, Ratio::new(-2, 1)), // Cycle 2: 2 -> 3 -> 2 (weight -1)
315            (0, 2, Ratio::new(1, 1)),
316        ]);
317
318        let mut ncf = NegCycleFinder::new(&digraph);
319        let mut dist = [
320            Ratio::new(0, 1),
321            Ratio::new(0, 1),
322            Ratio::new(0, 1),
323            Ratio::new(0, 1),
324        ];
325        let result = ncf.howard(&mut dist, |e| *e.weight());
326        assert!(result.is_some());
327        // The algorithm finds one of the negative cycles.
328        // We can't assert which one, but we can assert it's a negative cycle.
329        let cycle = result.unwrap();
330        let cycle_weight: Ratio<i32> = cycle.iter().map(|e| *e.weight()).sum();
331        assert!(cycle_weight < Ratio::new(0, 1));
332    }
333
334    #[test]
335    fn test_neg_cycle_unreachable_cycle() {
336        let digraph = DiGraph::<(), Ratio<i32>>::from_edges([
337            (0, 1, Ratio::new(1, 1)),
338            (1, 2, Ratio::new(1, 1)),
339            (2, 0, Ratio::new(-3, 1)), // Cycle 1: 0 -> 1 -> 2 -> 0 (weight -1)
340            (3, 4, Ratio::new(1, 1)),
341            (4, 3, Ratio::new(-2, 1)), // Cycle 2: 3 -> 4 -> 3 (weight -1) - unreachable from 0
342        ]);
343
344        let mut ncf = NegCycleFinder::new(&digraph);
345        let mut dist = [
346            Ratio::new(0, 1),
347            Ratio::new(0, 1),
348            Ratio::new(0, 1),
349            Ratio::new(0, 1),
350            Ratio::new(0, 1),
351        ];
352        let result = ncf.howard(&mut dist, |e| *e.weight());
353        assert!(result.is_some());
354        let cycle = result.unwrap();
355        let cycle_weight: Ratio<i32> = cycle.iter().map(|e| *e.weight()).sum();
356        assert!(cycle_weight < Ratio::new(0, 1));
357        // The found cycle should be the one reachable from the initial dist (all zeros, effectively reachable from all nodes)
358        // In this case, it should find the 0->1->2->0 cycle.
359        let expected_cycle_nodes: Vec<NodeIndex> = cycle.iter().map(|e| e.source()).collect();
360        assert!(expected_cycle_nodes.contains(&NodeIndex::new(0)));
361        assert!(expected_cycle_nodes.contains(&NodeIndex::new(1)));
362        assert!(expected_cycle_nodes.contains(&NodeIndex::new(2)));
363        assert!(!expected_cycle_nodes.contains(&NodeIndex::new(3)));
364        assert!(!expected_cycle_nodes.contains(&NodeIndex::new(4)));
365    }
366}