Skip to main content

problemreductions/rules/
strongconnectivityaugmentation_ilp.rs

1//! Reduction from StrongConnectivityAugmentation to ILP<i32>.
2//!
3//! Select candidate arcs under the budget and certify strong connectivity by
4//! sending flow both from a root to every vertex and back again.
5//! See the paper entry for the full formulation.
6
7use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP};
8use crate::models::graph::StrongConnectivityAugmentation;
9use crate::reduction;
10use crate::rules::traits::{ReduceTo, ReductionResult};
11
12#[derive(Debug, Clone)]
13pub struct ReductionSCAToILP {
14    target: ILP<i32>,
15    num_candidates: usize,
16}
17
18impl ReductionResult for ReductionSCAToILP {
19    type Source = StrongConnectivityAugmentation<i32>;
20    type Target = ILP<i32>;
21
22    fn target_problem(&self) -> &ILP<i32> {
23        &self.target
24    }
25
26    fn extract_solution(&self, target_solution: &[usize]) -> Vec<usize> {
27        target_solution[..self.num_candidates].to_vec()
28    }
29}
30
31#[reduction(
32    overhead = {
33        num_vars = "num_potential_arcs + 2 * num_vertices * (num_arcs + num_potential_arcs)",
34        num_constraints = "1 + 2 * num_vertices * num_potential_arcs + 2 * num_vertices * num_vertices",
35    }
36)]
37impl ReduceTo<ILP<i32>> for StrongConnectivityAugmentation<i32> {
38    type Result = ReductionSCAToILP;
39
40    fn reduce_to(&self) -> Self::Result {
41        let n = self.num_vertices();
42        let p = self.num_potential_arcs();
43
44        let base_arcs = self.graph().arcs();
45        let m = base_arcs.len();
46        let root = 0;
47
48        // Variable layout per paper:
49        // y_j:              j                          [0, p)
50        // f^t_i (fwd base): p + t*m + i                [p, p + n*m)
51        // f_bar^t_j (fwd cand): p + n*m + t*p + j      [p+nm, p+nm+np)
52        // g^t_i (bwd base): p + n*(m+p) + t*m + i      [p+n(m+p), p+n(2m+p))
53        // g_bar^t_j (bwd cand): p + n*(2m+p) + t*p + j [p+n(2m+p), p+2n(m+p))
54        let num_vars = p + 2 * n * (m + p);
55        let f_base = |t: usize, i: usize| -> usize { p + t * m + i };
56        let f_cand = |t: usize, j: usize| -> usize { p + n * m + t * p + j };
57        let g_base = |t: usize, i: usize| -> usize { p + n * (m + p) + t * m + i };
58        let g_cand = |t: usize, j: usize| -> usize { p + n * (2 * m + p) + t * p + j };
59
60        let mut constraints = Vec::new();
61
62        // Binary bounds: y_j ≤ 1
63        for j in 0..p {
64            constraints.push(LinearConstraint::le(vec![(j, 1.0)], 1.0));
65        }
66
67        // Budget: Σ w_j * y_j ≤ B
68        let budget_terms: Vec<(usize, f64)> = self
69            .candidate_arcs()
70            .iter()
71            .enumerate()
72            .map(|(j, &(_, _, w))| (j, w as f64))
73            .collect();
74        constraints.push(LinearConstraint::le(budget_terms, *self.bound() as f64));
75
76        for t in 0..n {
77            if t == root {
78                // Pin all flow vars to 0 for dummy commodity t = root
79                for i in 0..m {
80                    constraints.push(LinearConstraint::eq(vec![(f_base(t, i), 1.0)], 0.0));
81                    constraints.push(LinearConstraint::eq(vec![(g_base(t, i), 1.0)], 0.0));
82                }
83                for j in 0..p {
84                    constraints.push(LinearConstraint::eq(vec![(f_cand(t, j), 1.0)], 0.0));
85                    constraints.push(LinearConstraint::eq(vec![(g_cand(t, j), 1.0)], 0.0));
86                }
87                continue;
88            }
89
90            // Activation: f_bar^t_j ≤ y_j and g_bar^t_j ≤ y_j
91            for j in 0..p {
92                constraints.push(LinearConstraint::le(
93                    vec![(f_cand(t, j), 1.0), (j, -1.0)],
94                    0.0,
95                ));
96                constraints.push(LinearConstraint::le(
97                    vec![(g_cand(t, j), 1.0), (j, -1.0)],
98                    0.0,
99                ));
100            }
101
102            // Forward flow conservation (root → t): for each vertex v
103            for v in 0..n {
104                let mut terms: Vec<(usize, f64)> = Vec::new();
105
106                // Base arcs
107                for (i, &(u_a, v_a)) in base_arcs.iter().enumerate() {
108                    if u_a == v {
109                        terms.push((f_base(t, i), 1.0)); // outgoing
110                    }
111                    if v_a == v {
112                        terms.push((f_base(t, i), -1.0)); // incoming
113                    }
114                }
115
116                // Candidate arcs
117                for (j, &(sj, tj, _)) in self.candidate_arcs().iter().enumerate() {
118                    if sj == v {
119                        terms.push((f_cand(t, j), 1.0)); // outgoing
120                    }
121                    if tj == v {
122                        terms.push((f_cand(t, j), -1.0)); // incoming
123                    }
124                }
125
126                let rhs = if v == root {
127                    1.0
128                } else if v == t {
129                    -1.0
130                } else {
131                    0.0
132                };
133                constraints.push(LinearConstraint::eq(terms, rhs));
134            }
135
136            // Backward flow conservation (t → root): for each vertex v
137            for v in 0..n {
138                let mut terms: Vec<(usize, f64)> = Vec::new();
139
140                // Base arcs
141                for (i, &(u_a, v_a)) in base_arcs.iter().enumerate() {
142                    if u_a == v {
143                        terms.push((g_base(t, i), 1.0));
144                    }
145                    if v_a == v {
146                        terms.push((g_base(t, i), -1.0));
147                    }
148                }
149
150                // Candidate arcs
151                for (j, &(sj, tj, _)) in self.candidate_arcs().iter().enumerate() {
152                    if sj == v {
153                        terms.push((g_cand(t, j), 1.0));
154                    }
155                    if tj == v {
156                        terms.push((g_cand(t, j), -1.0));
157                    }
158                }
159
160                let rhs = if v == t {
161                    1.0 // source of backward flow
162                } else if v == root {
163                    -1.0 // sink of backward flow
164                } else {
165                    0.0
166                };
167                constraints.push(LinearConstraint::eq(terms, rhs));
168            }
169        }
170
171        let target = ILP::new(num_vars, constraints, vec![], ObjectiveSense::Minimize);
172        ReductionSCAToILP {
173            target,
174            num_candidates: p,
175        }
176    }
177}
178
179#[cfg(feature = "example-db")]
180pub(crate) fn canonical_rule_example_specs() -> Vec<crate::example_db::specs::RuleExampleSpec> {
181    use crate::export::SolutionPair;
182    use crate::topology::DirectedGraph;
183    vec![crate::example_db::specs::RuleExampleSpec {
184        id: "strongconnectivityaugmentation_to_ilp",
185        build: || {
186            // Path 0→1→2, candidates: (2,0,1),(1,0,2), bound=2
187            let source = StrongConnectivityAugmentation::new(
188                DirectedGraph::new(3, vec![(0, 1), (1, 2)]),
189                vec![(2, 0, 1), (1, 0, 2)],
190                2,
191            );
192            let reduction: ReductionSCAToILP =
193                crate::rules::ReduceTo::<ILP<i32>>::reduce_to(&source);
194            let ilp_sol = crate::solvers::ILPSolver::new()
195                .solve(reduction.target_problem())
196                .expect("ILP should be solvable");
197            let extracted = reduction.extract_solution(&ilp_sol);
198            crate::example_db::specs::rule_example_with_witness::<_, ILP<i32>>(
199                source,
200                SolutionPair {
201                    source_config: extracted,
202                    target_config: ilp_sol,
203                },
204            )
205        },
206    }]
207}
208
209#[cfg(test)]
210#[path = "../unit_tests/rules/strongconnectivityaugmentation_ilp.rs"]
211mod tests;