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