Skip to main content

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}