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::{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>`):
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>,
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>;
38
39    fn target_problem(&self) -> &ILP<i64> {
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(
61    transform = exact {
62        num_vars = "2 * num_edges + num_vertices",
63        num_constraints = "5 * num_edges + 4 * num_vertices + 2",
64    },
65    unavailable = {
66        num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform",
67    }
68)]
69impl ReduceTo<ILP<i64>> for ShortestWeightConstrainedPath<SimpleGraph, i64> {
70    type Result = ReductionSWCPToILP;
71
72    fn reduce_to(&self) -> Result<Self::Result, crate::rules::ReductionError> {
73        let edges = self.graph().edges();
74        let num_vertices = self.num_vertices();
75        let num_edges = self.num_edges();
76        let num_vars = 2 * num_edges + num_vertices;
77        let source = self.source_vertex();
78        let target = self.target_vertex();
79        let big_m = Self::exact_i64(num_vertices, "encoding the vertex order")?;
80
81        let order_var = |vertex: usize| 2 * num_edges + vertex;
82
83        // Build adjacency: outgoing[v] and incoming[v] collect arc variable
84        // references for arcs leaving / entering vertex v.
85        let mut outgoing: Vec<Vec<(usize, i64)>> = vec![Vec::new(); num_vertices];
86        let mut incoming: Vec<Vec<(usize, i64)>> = vec![Vec::new(); num_vertices];
87
88        for (edge_idx, &(u, v)) in edges.iter().enumerate() {
89            let forward = ReductionSWCPToILP::arc_var(edge_idx, 0); // u -> v
90            let reverse = ReductionSWCPToILP::arc_var(edge_idx, 1); // v -> u
91            outgoing[u].push((forward, 1));
92            incoming[v].push((forward, 1));
93            outgoing[v].push((reverse, 1));
94            incoming[u].push((reverse, 1));
95        }
96
97        let mut constraints = Vec::new();
98
99        // --- Arc variables are binary within `ILP<i64>`: 0 <= a_{e,d} <= 1 ---
100        for edge_idx in 0..num_edges {
101            constraints.push(LinearConstraint::le(
102                vec![(ReductionSWCPToILP::arc_var(edge_idx, 0), 1)],
103                1,
104            ));
105            constraints.push(LinearConstraint::le(
106                vec![(ReductionSWCPToILP::arc_var(edge_idx, 1), 1)],
107                1,
108            ));
109        }
110
111        // --- Order variables stay within [0, |V|-1] ---
112        let max_order = if num_vertices == 0 { 0 } else { big_m - 1 };
113        for vertex in 0..num_vertices {
114            constraints.push(LinearConstraint::le(
115                vec![(order_var(vertex), 1)],
116                max_order,
117            ));
118        }
119
120        // --- Flow balance and degree bounds ---
121        for vertex in 0..num_vertices {
122            // net flow: out - in
123            let mut balance_terms = outgoing[vertex].clone();
124            for &(var, coef) in &incoming[vertex] {
125                balance_terms.push((var, -coef));
126            }
127
128            let rhs = if source != target {
129                if vertex == source {
130                    1
131                } else if vertex == target {
132                    -1
133                } else {
134                    0
135                }
136            } else {
137                0
138            };
139            constraints.push(LinearConstraint::eq(balance_terms, rhs));
140            constraints.push(LinearConstraint::le(outgoing[vertex].clone(), 1));
141            constraints.push(LinearConstraint::le(incoming[vertex].clone(), 1));
142        }
143
144        // --- At most one direction per undirected edge ---
145        for edge_idx in 0..num_edges {
146            constraints.push(LinearConstraint::le(
147                vec![
148                    (ReductionSWCPToILP::arc_var(edge_idx, 0), 1),
149                    (ReductionSWCPToILP::arc_var(edge_idx, 1), 1),
150                ],
151                1,
152            ));
153        }
154
155        // --- MTZ ordering: if arc u->v is selected then order(v) >= order(u) + 1 ---
156        for (edge_idx, &(u, v)) in edges.iter().enumerate() {
157            // o_v - o_u - M * a_{e,0} >= 1 - M
158            constraints.push(LinearConstraint::ge(
159                vec![
160                    (order_var(v), 1),
161                    (order_var(u), -1),
162                    (ReductionSWCPToILP::arc_var(edge_idx, 0), -big_m),
163                ],
164                1 - big_m,
165            ));
166            // o_u - o_v - M * a_{e,1} >= 1 - M
167            constraints.push(LinearConstraint::ge(
168                vec![
169                    (order_var(u), 1),
170                    (order_var(v), -1),
171                    (ReductionSWCPToILP::arc_var(edge_idx, 1), -big_m),
172                ],
173                1 - big_m,
174            ));
175        }
176
177        // --- Fix source order to 0 ---
178        constraints.push(LinearConstraint::eq(vec![(order_var(source), 1)], 0));
179
180        // --- Weight bound: Σ wt_e * (a_{e,0} + a_{e,1}) <= weight_bound ---
181        let edge_weights: Vec<i64> = self
182            .edge_weights()
183            .iter()
184            .map(WeightElement::to_sum)
185            .collect();
186        let weight_terms: Vec<(usize, i64)> = edges
187            .iter()
188            .enumerate()
189            .flat_map(|(edge_idx, _)| {
190                let coeff = edge_weights[edge_idx];
191                [
192                    (ReductionSWCPToILP::arc_var(edge_idx, 0), coeff),
193                    (ReductionSWCPToILP::arc_var(edge_idx, 1), coeff),
194                ]
195            })
196            .collect();
197        constraints.push(LinearConstraint::le(weight_terms, *self.weight_bound()));
198
199        // --- Objective: minimize total path length ---
200        let edge_lengths: Vec<i64> = self
201            .edge_lengths()
202            .iter()
203            .map(WeightElement::to_sum)
204            .collect();
205        let objective: Vec<(usize, i64)> = edges
206            .iter()
207            .enumerate()
208            .flat_map(|(edge_idx, _)| {
209                let coeff = edge_lengths[edge_idx];
210                [
211                    (ReductionSWCPToILP::arc_var(edge_idx, 0), coeff),
212                    (ReductionSWCPToILP::arc_var(edge_idx, 1), coeff),
213                ]
214            })
215            .collect();
216        let target_ilp = ILP::new(num_vars, constraints, objective, ObjectiveSense::Minimize)
217            .map_err(Self::target_construction)?;
218
219        Ok(ReductionSWCPToILP {
220            target: target_ilp,
221            num_edges,
222        })
223    }
224}
225
226#[cfg(feature = "example-db")]
227pub(crate) fn canonical_rule_example_specs() -> Vec<crate::example_db::specs::RuleExampleSpec> {
228    vec![crate::example_db::specs::RuleExampleSpec {
229        id: "shortestweightconstrainedpath_to_ilp",
230        build: || {
231            // 3-vertex path: 0 -- 1 -- 2, s=0, t=2
232            // edge_lengths = [2, 3], edge_weights = [1, 2]
233            // weight_bound = 4
234            // The only s-t path uses both edges: length=5, weight=3 <= 4 => feasible
235            let source = ShortestWeightConstrainedPath::new(
236                SimpleGraph::new(3, vec![(0, 1), (1, 2)]),
237                vec![2, 3],
238                vec![1, 2],
239                0,
240                2,
241                4,
242            );
243            crate::example_db::specs::rule_example_via_ilp::<_, i64>(source)
244        },
245    }]
246}
247
248#[cfg(test)]
249#[path = "../unit_tests/rules/shortestweightconstrainedpath_ilp.rs"]
250mod tests;