Skip to main content

problemreductions/rules/
shortestweightconstrainedpath_ilp.rs

1//! Reduction from ShortestWeightConstrainedPath to ILP (Integer Linear Programming).
2//!
3//! Uses directed-arc variables for each orientation of each undirected edge,
4//! together with integer order variables for MTZ-style subtour elimination.
5//! Flow-balance constraints force a single directed s-t path, the weight
6//! bound constraint enforces the weight limit, and the objective minimizes
7//! total path length.
8
9use crate::models::algebraic::{Bounded, IntegerVariable, LinearConstraint, ObjectiveSense, ILP};
10use crate::models::graph::ShortestWeightConstrainedPath;
11use crate::reduction;
12use crate::rules::traits::{ReduceTo, ReductionResult};
13use crate::topology::{Graph, SimpleGraph};
14use crate::types::WeightElement;
15
16/// Result of reducing ShortestWeightConstrainedPath to ILP.
17///
18/// Variable layout (within `ILP<i64, i64, Bounded>`):
19/// - Arc variables: `a_{e,0}` and `a_{e,1}` for each undirected edge `e`
20///   (indices `0..2m`), bounded to {0, 1}
21/// - Order variables: `o_v` for each vertex `v` (indices `2m..2m+n`),
22///   bounded to `[0, n-1]`
23#[derive(Debug, Clone)]
24pub struct ReductionSWCPToILP {
25    target: ILP<i64, i64, Bounded>,
26    num_edges: usize,
27}
28
29impl ReductionSWCPToILP {
30    fn arc_var(edge_idx: usize, dir: usize) -> usize {
31        2 * edge_idx + dir
32    }
33}
34
35impl ReductionResult for ReductionSWCPToILP {
36    type Source = ShortestWeightConstrainedPath<SimpleGraph, i64>;
37    type Target = ILP<i64, i64, Bounded>;
38
39    fn target_problem(&self) -> &ILP<i64, i64, Bounded> {
40        &self.target
41    }
42
43    fn extract_solution(
44        &self,
45        target_solution: &<Self::Target as crate::traits::Problem>::Solution,
46    ) -> crate::rules::ExtractionResult<<Self::Source as crate::traits::Problem>::Solution> {
47        crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?;
48
49        Ok({
50            (0..self.num_edges)
51                .map(|edge_idx| {
52                    target_solution[Self::arc_var(edge_idx, 0)] > 0
53                        || target_solution[Self::arc_var(edge_idx, 1)] > 0
54                })
55                .collect()
56        })
57    }
58}
59
60#[reduction(transform = {
61    exact {
62        num_vars = "2 * num_edges + num_vertices",
63        num_constraints = "5 * num_edges + 4 * num_vertices + 2",
64    },
65    upper_bound {
66        max_constraint_magnitude_bits = "max_weight_bits + num_vertices",
67        num_nonzeros = "(2 * num_edges + num_vertices) * (5 * num_edges + 4 * num_vertices + 2)",
68    },
69})]
70impl ReduceTo<ILP<i64, i64, Bounded>> for ShortestWeightConstrainedPath<SimpleGraph, i64> {
71    type Result = ReductionSWCPToILP;
72
73    fn reduce_to(&self) -> Result<Self::Result, crate::rules::ReductionError> {
74        let edges = self.graph().edges();
75        let num_vertices = self.num_vertices();
76        let num_edges = self.num_edges();
77        let num_vars = 2 * num_edges + num_vertices;
78        let source = self.source_vertex();
79        let target = self.target_vertex();
80        let big_m = Self::exact_i64(num_vertices, "encoding the vertex order")?;
81
82        let order_var = |vertex: usize| 2 * num_edges + vertex;
83
84        // Build adjacency: outgoing[v] and incoming[v] collect arc variable
85        // references for arcs leaving / entering vertex v.
86        let mut outgoing: Vec<Vec<(usize, i64)>> = vec![Vec::new(); num_vertices];
87        let mut incoming: Vec<Vec<(usize, i64)>> = vec![Vec::new(); num_vertices];
88
89        for (edge_idx, &(u, v)) in edges.iter().enumerate() {
90            let forward = ReductionSWCPToILP::arc_var(edge_idx, 0); // u -> v
91            let reverse = ReductionSWCPToILP::arc_var(edge_idx, 1); // v -> u
92            outgoing[u].push((forward, 1));
93            incoming[v].push((forward, 1));
94            outgoing[v].push((reverse, 1));
95            incoming[u].push((reverse, 1));
96        }
97
98        let mut constraints = Vec::new();
99
100        // --- Arc variables are binary within `ILP<i64, i64, Bounded>`: 0 <= a_{e,d} <= 1 ---
101        for edge_idx in 0..num_edges {
102            constraints.push(LinearConstraint::le(
103                vec![(ReductionSWCPToILP::arc_var(edge_idx, 0), 1)],
104                1,
105            ));
106            constraints.push(LinearConstraint::le(
107                vec![(ReductionSWCPToILP::arc_var(edge_idx, 1), 1)],
108                1,
109            ));
110        }
111
112        // --- Order variables stay within [0, |V|-1] ---
113        let max_order = if num_vertices == 0 { 0 } else { big_m - 1 };
114        for vertex in 0..num_vertices {
115            constraints.push(LinearConstraint::le(
116                vec![(order_var(vertex), 1)],
117                max_order,
118            ));
119        }
120
121        // --- Flow balance and degree bounds ---
122        for vertex in 0..num_vertices {
123            // net flow: out - in
124            let mut balance_terms = outgoing[vertex].clone();
125            for &(var, coef) in &incoming[vertex] {
126                balance_terms.push((var, -coef));
127            }
128
129            let rhs = if source != target {
130                if vertex == source {
131                    1
132                } else if vertex == target {
133                    -1
134                } else {
135                    0
136                }
137            } else {
138                0
139            };
140            constraints.push(LinearConstraint::eq(balance_terms, rhs));
141            constraints.push(LinearConstraint::le(outgoing[vertex].clone(), 1));
142            constraints.push(LinearConstraint::le(incoming[vertex].clone(), 1));
143        }
144
145        // --- At most one direction per undirected edge ---
146        for edge_idx in 0..num_edges {
147            constraints.push(LinearConstraint::le(
148                vec![
149                    (ReductionSWCPToILP::arc_var(edge_idx, 0), 1),
150                    (ReductionSWCPToILP::arc_var(edge_idx, 1), 1),
151                ],
152                1,
153            ));
154        }
155
156        // --- MTZ ordering: if arc u->v is selected then order(v) >= order(u) + 1 ---
157        for (edge_idx, &(u, v)) in edges.iter().enumerate() {
158            // o_v - o_u - M * a_{e,0} >= 1 - M
159            constraints.push(LinearConstraint::ge(
160                vec![
161                    (order_var(v), 1),
162                    (order_var(u), -1),
163                    (ReductionSWCPToILP::arc_var(edge_idx, 0), -big_m),
164                ],
165                1 - big_m,
166            ));
167            // o_u - o_v - M * a_{e,1} >= 1 - M
168            constraints.push(LinearConstraint::ge(
169                vec![
170                    (order_var(u), 1),
171                    (order_var(v), -1),
172                    (ReductionSWCPToILP::arc_var(edge_idx, 1), -big_m),
173                ],
174                1 - big_m,
175            ));
176        }
177
178        // --- Fix source order to 0 ---
179        constraints.push(LinearConstraint::eq(vec![(order_var(source), 1)], 0));
180
181        // --- Weight bound: Σ wt_e * (a_{e,0} + a_{e,1}) <= weight_bound ---
182        let edge_weights: Vec<i64> = self
183            .edge_weights()
184            .iter()
185            .map(WeightElement::to_sum)
186            .collect();
187        let weight_terms: Vec<(usize, i64)> = edges
188            .iter()
189            .enumerate()
190            .flat_map(|(edge_idx, _)| {
191                let coeff = edge_weights[edge_idx];
192                [
193                    (ReductionSWCPToILP::arc_var(edge_idx, 0), coeff),
194                    (ReductionSWCPToILP::arc_var(edge_idx, 1), coeff),
195                ]
196            })
197            .collect();
198        constraints.push(LinearConstraint::le(weight_terms, *self.weight_bound()));
199
200        // --- Objective: minimize total path length ---
201        let edge_lengths: Vec<i64> = self
202            .edge_lengths()
203            .iter()
204            .map(WeightElement::to_sum)
205            .collect();
206        let objective: Vec<(usize, i64)> = edges
207            .iter()
208            .enumerate()
209            .flat_map(|(edge_idx, _)| {
210                let coeff = edge_lengths[edge_idx];
211                [
212                    (ReductionSWCPToILP::arc_var(edge_idx, 0), coeff),
213                    (ReductionSWCPToILP::arc_var(edge_idx, 1), coeff),
214                ]
215            })
216            .collect();
217        let mut variables = vec![IntegerVariable::binary(); num_vars];
218        variables[2 * num_edges..].fill(
219            IntegerVariable::new(Some(0), Some(max_order)).map_err(Self::target_construction)?,
220        );
221
222        let target_ilp =
223            ILP::with_variables(variables, constraints, objective, ObjectiveSense::Minimize)
224                .map_err(Self::target_construction)?;
225
226        Ok(ReductionSWCPToILP {
227            target: target_ilp,
228            num_edges,
229        })
230    }
231}
232
233#[cfg(feature = "example-db")]
234pub(crate) fn canonical_rule_example_specs() -> Vec<crate::example_db::specs::RuleExampleSpec> {
235    vec![crate::example_db::specs::RuleExampleSpec {
236        id: "shortestweightconstrainedpath_to_ilp",
237        build: || {
238            // 3-vertex path: 0 -- 1 -- 2, s=0, t=2
239            // edge_lengths = [2, 3], edge_weights = [1, 2]
240            // weight_bound = 4
241            // The only s-t path uses both edges: length=5, weight=3 <= 4 => feasible
242            let source = ShortestWeightConstrainedPath::new(
243                SimpleGraph::new(3, vec![(0, 1), (1, 2)]),
244                vec![2, 3],
245                vec![1, 2],
246                0,
247                2,
248                4,
249            );
250            crate::example_db::specs::rule_example_via_bounded_ilp::<_>(source)
251        },
252    }]
253}
254
255#[cfg(test)]
256#[path = "../unit_tests/rules/shortestweightconstrainedpath_ilp.rs"]
257mod tests;