Skip to main content

problemreductions/rules/
lengthboundeddisjointpaths_ilp.rs

1//! Reduction from LengthBoundedDisjointPaths to ILP.
2//!
3//! Binary flow variables per commodity per directed edge orientation.
4//! Conservation, edge/vertex disjointness, and length bound.
5
6use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP};
7use crate::models::graph::LengthBoundedDisjointPaths;
8use crate::reduction;
9use crate::rules::traits::{ReduceTo, ReductionResult};
10use crate::topology::{Graph, SimpleGraph};
11
12/// Result of reducing LengthBoundedDisjointPaths to ILP.
13///
14/// Variable layout (all binary):
15/// - Flow: `f^k_{e,dir}` at index `k * 2m + 2e + dir`
16///
17/// Total: `J * 2m` variables.
18#[derive(Debug, Clone)]
19pub struct ReductionLBDPToILP {
20    target: ILP<bool>,
21    /// Canonical sorted edges.
22    edges: Vec<(usize, usize)>,
23    num_vertices: usize,
24    num_paths: usize,
25}
26
27impl ReductionResult for ReductionLBDPToILP {
28    type Source = LengthBoundedDisjointPaths<SimpleGraph>;
29    type Target = ILP<bool>;
30
31    fn target_problem(&self) -> &ILP<bool> {
32        &self.target
33    }
34
35    fn extract_solution(&self, target_solution: &[usize]) -> Vec<usize> {
36        // For each path slot k, set the source vertex-indicator block to 1
37        // exactly on the vertices incident to the commodity-k path, including s and t.
38        let m = self.edges.len();
39        let n = self.num_vertices;
40        let j = self.num_paths;
41        let flow_vars_per_k = 2 * m;
42
43        let mut result = vec![0usize; j * n];
44        for k in 0..j {
45            // Find which vertices are on the path for commodity k
46            let mut on_path = vec![false; n];
47            for e in 0..m {
48                let (u, v) = self.edges[e];
49                let fwd = target_solution[k * flow_vars_per_k + 2 * e];
50                let rev = target_solution[k * flow_vars_per_k + 2 * e + 1];
51                if fwd == 1 {
52                    on_path[u] = true;
53                    on_path[v] = true;
54                }
55                if rev == 1 {
56                    on_path[u] = true;
57                    on_path[v] = true;
58                }
59            }
60            for v in 0..n {
61                if on_path[v] {
62                    result[k * n + v] = 1;
63                }
64            }
65        }
66        result
67    }
68}
69
70#[reduction(
71    overhead = {
72        num_vars = "max_paths * 2 * num_edges + max_paths",
73        num_constraints = "max_paths * num_vertices + max_paths * num_edges + max_paths + num_edges + num_vertices + max_paths",
74    }
75)]
76impl ReduceTo<ILP<bool>> for LengthBoundedDisjointPaths<SimpleGraph> {
77    type Result = ReductionLBDPToILP;
78
79    #[allow(clippy::needless_range_loop)]
80    fn reduce_to(&self) -> Self::Result {
81        let mut edges: Vec<(usize, usize)> = self
82            .graph()
83            .edges()
84            .into_iter()
85            .map(|(u, v)| if u <= v { (u, v) } else { (v, u) })
86            .collect();
87        edges.sort_unstable();
88
89        let m = edges.len();
90        let n = self.num_vertices();
91        let j = self.max_paths();
92        let max_len = self.max_length();
93        let s = self.source();
94        let t = self.sink();
95
96        // Variable layout: flow variables + activation variables a_k
97        let flow_vars_per_k = 2 * m;
98        let num_flow = j * flow_vars_per_k;
99        let a_var = |k: usize| num_flow + k;
100        let num_vars = num_flow + j;
101
102        let flow_var = |k: usize, e: usize, dir: usize| k * flow_vars_per_k + 2 * e + dir;
103
104        // Build vertex-to-edge adjacency
105        let mut vertex_edges: Vec<Vec<usize>> = vec![Vec::new(); n];
106        for (e, &(u, v)) in edges.iter().enumerate() {
107            vertex_edges[u].push(e);
108            vertex_edges[v].push(e);
109        }
110
111        let mut constraints = Vec::new();
112
113        for k in 0..j {
114            // Flow conservation: outflow - inflow = a_k at source, -a_k at sink, 0 elsewhere
115            for vertex in 0..n {
116                let mut terms = Vec::new();
117                for &e in &vertex_edges[vertex] {
118                    let (eu, _) = edges[e];
119                    if vertex == eu {
120                        terms.push((flow_var(k, e, 0), 1.0)); // outgoing
121                        terms.push((flow_var(k, e, 1), -1.0)); // incoming
122                    } else {
123                        terms.push((flow_var(k, e, 1), 1.0)); // outgoing
124                        terms.push((flow_var(k, e, 0), -1.0)); // incoming
125                    }
126                }
127                if vertex == s {
128                    // outflow - inflow = a_k  =>  outflow - inflow - a_k = 0
129                    terms.push((a_var(k), -1.0));
130                    constraints.push(LinearConstraint::eq(terms, 0.0));
131                } else if vertex == t {
132                    // outflow - inflow = -a_k  =>  outflow - inflow + a_k = 0
133                    terms.push((a_var(k), 1.0));
134                    constraints.push(LinearConstraint::eq(terms, 0.0));
135                } else {
136                    constraints.push(LinearConstraint::eq(terms, 0.0));
137                }
138            }
139
140            // Anti-parallel
141            for e in 0..m {
142                constraints.push(LinearConstraint::le(
143                    vec![(flow_var(k, e, 0), 1.0), (flow_var(k, e, 1), 1.0)],
144                    1.0,
145                ));
146            }
147
148            // Length bound: total flow for commodity k <= max_length * a_k
149            let mut len_terms = Vec::new();
150            for e in 0..m {
151                len_terms.push((flow_var(k, e, 0), 1.0));
152                len_terms.push((flow_var(k, e, 1), 1.0));
153            }
154            len_terms.push((a_var(k), -(max_len as f64)));
155            constraints.push(LinearConstraint::le(len_terms, 0.0));
156        }
157
158        // Edge disjointness: each edge used by at most one commodity
159        for e in 0..m {
160            let mut terms = Vec::new();
161            for k in 0..j {
162                terms.push((flow_var(k, e, 0), 1.0));
163                terms.push((flow_var(k, e, 1), 1.0));
164            }
165            constraints.push(LinearConstraint::le(terms, 1.0));
166        }
167
168        // Vertex disjointness for non-terminal vertices
169        for v in 0..n {
170            if v == s || v == t {
171                continue;
172            }
173            let mut terms = Vec::new();
174            for k in 0..j {
175                for &e in &vertex_edges[v] {
176                    let (eu, _) = edges[e];
177                    if v == eu {
178                        terms.push((flow_var(k, e, 0), 1.0));
179                    } else {
180                        terms.push((flow_var(k, e, 1), 1.0));
181                    }
182                }
183            }
184            constraints.push(LinearConstraint::le(terms, 1.0));
185        }
186
187        // Objective: maximize number of active path slots
188        let objective: Vec<(usize, f64)> = (0..j).map(|k| (a_var(k), 1.0)).collect();
189        let target = ILP::new(num_vars, constraints, objective, ObjectiveSense::Maximize);
190
191        ReductionLBDPToILP {
192            target,
193            edges,
194            num_vertices: n,
195            num_paths: j,
196        }
197    }
198}
199
200#[cfg(feature = "example-db")]
201pub(crate) fn canonical_rule_example_specs() -> Vec<crate::example_db::specs::RuleExampleSpec> {
202    vec![crate::example_db::specs::RuleExampleSpec {
203        id: "lengthboundeddisjointpaths_to_ilp",
204        build: || {
205            // 4-vertex diamond: s=0, t=3, K=2
206            let source = LengthBoundedDisjointPaths::new(
207                SimpleGraph::new(4, vec![(0, 1), (0, 2), (1, 3), (2, 3)]),
208                0,
209                3,
210                2,
211            );
212            crate::example_db::specs::rule_example_via_ilp::<_, bool>(source)
213        },
214    }]
215}
216
217#[cfg(test)]
218#[path = "../unit_tests/rules/lengthboundeddisjointpaths_ilp.rs"]
219mod tests;