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}