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