netoptim_rs/parametric.rs
1// use std::collections::HashMap;
2// use std::cmp::Ordering;
3use std::hash::Hash;
4use std::ops::Add;
5use std::ops::Div;
6use std::ops::Mul;
7use std::ops::Neg;
8use std::ops::Sub;
9
10use petgraph::graph::{DiGraph, EdgeReference};
11
12// use petgraph::visit::EdgeRef;
13// use petgraph::visit::IntoNodeIdentifiers;
14// use petgraph::Direction;
15
16// use num::traits::Float;
17use num::traits::Inv;
18use num::traits::One;
19use num::traits::Zero;
20
21use crate::neg_cycle::NegCycleFinder;
22
23/// API trait for parametric shortest path problems.
24///
25/// The parametric shortest path problem finds the maximum ratio $r$ such that:
26///
27/// $$ d_v - d_u \le w_{uv} - r \quad \forall (u,v) \in E $$
28///
29/// Implement this trait to define how distances are computed and how
30/// to find the ratio that cancels a cycle.
31pub trait ParametricAPI<E, R>
32where
33 R: Copy + PartialOrd,
34 E: Clone,
35{
36 /// Compute the effective edge distance $d'(w, r) = w - r$.
37 fn distance(&self, ratio: &R, edge: &EdgeReference<R>) -> R;
38 /// Compute $r^* = \frac{\sum_{(u,v) \in C} w_{uv}}{|C|}$ such that the cycle's total distance is zero.
39 fn zero_cancel(&self, cycle: &[EdgeReference<R>]) -> R;
40}
41
42/// Maximum parametric shortest path solver.
43///
44/// $$ d_j(\lambda) = \min_{k} \big( d_k(\lambda) + \text{cost}(k, j, \lambda) \big) $$
45///
46/// Finds the minimum ratio cycle in a directed graph using Howard's algorithm
47/// for negative cycle detection.
48#[derive(Debug)]
49pub struct MaxParametricSolver<'a, V, R, P>
50where
51 R: Copy
52 + PartialOrd
53 + Add<Output = R>
54 + Sub<Output = R>
55 + Mul<Output = R>
56 + Div<Output = R>
57 + Neg<Output = R>
58 + Inv<Output = R>,
59 V: Eq + Hash + Clone,
60 P: ParametricAPI<V, R>,
61{
62 ncf: NegCycleFinder<'a, V, R>,
63 omega: P,
64}
65
66impl<'a, V, R, P> MaxParametricSolver<'a, V, R, P>
67where
68 R: Copy
69 + PartialOrd
70 + Zero
71 + One
72 + Add<Output = R>
73 + Sub<Output = R>
74 + Mul<Output = R>
75 + Div<Output = R>
76 + Neg<Output = R>
77 + Inv<Output = R>,
78 V: Eq + Hash + Clone,
79 P: ParametricAPI<V, R>,
80{
81 pub fn new(gra: &'a DiGraph<V, R>, omega: P) -> Self {
82 Self {
83 ncf: NegCycleFinder::new(gra),
84 omega,
85 }
86 }
87
88 /// The function `run` finds the minimum ratio and corresponding cycle in a given graph.
89 ///
90 /// $$ r^* = \min_{C \in \text{cycles}} \frac{\sum_{(u,v) \in C} w_{uv}}{|C|} $$
91 ///
92 /// Arguments:
93 ///
94 /// * `dist`: `dist` is a mutable reference to a slice of type `R`. It represents a distance matrix
95 /// or array, where `R` is the type of the elements in the matrix.
96 /// * `ratio`: The `ratio` parameter is a mutable reference to a value of type `R`. It represents
97 /// the current ratio value that is being used in the algorithm. The algorithm will update this
98 /// value if it finds a smaller ratio during its execution.
99 ///
100 /// Returns:
101 ///
102 /// a vector of `EdgeReference<R>`.
103 /// # Example
104 /// ```rust
105 /// use petgraph::graph::DiGraph;
106 /// use petgraph::prelude::*;
107 /// use num::rational::Ratio;
108 /// use netoptim_rs::parametric::{MaxParametricSolver, ParametricAPI};
109 /// use petgraph::graph::EdgeReference;
110 ///
111 /// struct TestParametricAPI;
112 ///
113 /// impl ParametricAPI<(), Ratio<i32>> for TestParametricAPI {
114 /// fn distance(&self, ratio: &Ratio<i32>, edge: &EdgeReference<Ratio<i32>>) -> Ratio<i32> {
115 /// *edge.weight() - *ratio
116 /// }
117 ///
118 /// fn zero_cancel(&self, cycle: &[EdgeReference<Ratio<i32>>]) -> Ratio<i32> {
119 /// let mut sum_a = Ratio::new(0, 1);
120 /// let mut sum_b = Ratio::new(0, 1);
121 /// for edge in cycle {
122 /// sum_a += *edge.weight();
123 /// sum_b += Ratio::new(1, 1);
124 /// }
125 /// sum_a / sum_b
126 /// }
127 /// }
128 ///
129 /// let digraph = DiGraph::<(), Ratio<i32>>::from_edges(&[
130 /// (0, 1, Ratio::new(1, 1)),
131 /// (1, 2, Ratio::new(1, 1)),
132 /// (2, 0, Ratio::new(-3, 1)),
133 /// ]);
134 ///
135 /// let mut solver = MaxParametricSolver::new(&digraph, TestParametricAPI);
136 /// let mut dist = [Ratio::new(0, 1), Ratio::new(0, 1), Ratio::new(0, 1)];
137 /// let mut ratio = Ratio::new(0, 1);
138 ///
139 /// let cycle = solver.run(&mut dist, &mut ratio);
140 /// assert!(!cycle.is_empty());
141 /// assert_eq!(ratio, Ratio::new(-1, 3));
142 /// ```
143 pub fn run(&mut self, dist: &mut [R], ratio: &mut R) -> Vec<EdgeReference<'a, R>> {
144 let mut r_min = *ratio;
145 let mut c_min = Vec::<EdgeReference<R>>::new();
146 let mut cycle = Vec::<EdgeReference<R>>::new();
147 loop {
148 if let Some(ci) = self.ncf.howard(dist, |e| self.omega.distance(ratio, &e)) {
149 let ri = self.omega.zero_cancel(&ci);
150 if r_min > ri {
151 r_min = ri;
152 c_min = ci;
153 }
154 }
155 if r_min >= *ratio {
156 break;
157 }
158 cycle.clone_from(&c_min);
159 *ratio = r_min;
160 }
161 cycle
162 }
163}
164
165#[cfg(test)]
166mod tests {
167 use super::*;
168 use petgraph::graph::DiGraph;
169
170 use num::rational::Ratio;
171
172 struct TestParametricAPI;
173
174 impl ParametricAPI<(), Ratio<i32>> for TestParametricAPI {
175 fn distance(&self, ratio: &Ratio<i32>, edge: &EdgeReference<Ratio<i32>>) -> Ratio<i32> {
176 *edge.weight() - *ratio
177 }
178
179 fn zero_cancel(&self, cycle: &[EdgeReference<Ratio<i32>>]) -> Ratio<i32> {
180 let mut sum_a = Ratio::new(0, 1);
181 let mut sum_b = Ratio::new(0, 1);
182 for edge in cycle {
183 sum_a += *edge.weight();
184 sum_b += Ratio::new(1, 1);
185 }
186 sum_a / sum_b
187 }
188 }
189
190 #[test]
191 fn test_max_parametric_solver_simple_cycle() {
192 let digraph = DiGraph::<(), Ratio<i32>>::from_edges([
193 (0, 1, Ratio::new(1, 1)),
194 (1, 2, Ratio::new(1, 1)),
195 (2, 0, Ratio::new(-3, 1)),
196 ]);
197
198 let mut solver = MaxParametricSolver::new(&digraph, TestParametricAPI);
199 let mut dist = [Ratio::new(0, 1), Ratio::new(0, 1), Ratio::new(0, 1)];
200 let mut ratio = Ratio::new(0, 1);
201
202 let cycle = solver.run(&mut dist, &mut ratio);
203 assert!(!cycle.is_empty());
204 assert_eq!(ratio, Ratio::new(-1, 3));
205 }
206
207 #[test]
208 fn test_max_parametric_solver_no_cycle() {
209 let digraph = DiGraph::<(), Ratio<i32>>::from_edges([
210 (0, 1, Ratio::new(1, 1)),
211 (1, 2, Ratio::new(1, 1)),
212 (0, 2, Ratio::new(3, 1)),
213 ]);
214
215 let mut solver = MaxParametricSolver::new(&digraph, TestParametricAPI);
216 let mut dist = [Ratio::new(0, 1), Ratio::new(0, 1), Ratio::new(0, 1)];
217 let mut ratio = Ratio::new(0, 1);
218
219 let cycle = solver.run(&mut dist, &mut ratio);
220 assert!(cycle.is_empty());
221 assert_eq!(ratio, Ratio::new(0, 1)); // Should remain initial ratio
222 }
223
224 #[test]
225 fn test_max_parametric_solver_multiple_cycles() {
226 let digraph = DiGraph::<(), Ratio<i32>>::from_edges([
227 (0, 1, Ratio::new(1, 1)),
228 (1, 0, Ratio::new(-2, 1)), // Cycle 1: ratio -1/1
229 (2, 3, Ratio::new(1, 1)),
230 (3, 2, Ratio::new(-4, 1)), // Cycle 2: ratio -3/1
231 (0, 2, Ratio::new(1, 1)),
232 ]);
233
234 let mut solver = MaxParametricSolver::new(&digraph, TestParametricAPI);
235 let mut dist = [
236 Ratio::new(0, 1),
237 Ratio::new(0, 1),
238 Ratio::new(0, 1),
239 Ratio::new(0, 1),
240 ];
241 let mut ratio = Ratio::new(0, 1);
242
243 let cycle = solver.run(&mut dist, &mut ratio);
244 assert!(!cycle.is_empty());
245 assert_eq!(ratio, Ratio::new(-3, 2)); // Should find the cycle with ratio -3/1
246 }
247}