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}