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/// 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}