1use crate::models::algebraic::{Bounded, IntegerVariable, LinearConstraint, ObjectiveSense, ILP};
4use crate::models::graph::AcyclicPartition;
5use crate::reduction;
6use crate::rules::traits::{ReduceTo, ReductionResult};
7
8#[derive(Debug, Clone)]
9pub struct ReductionAcyclicPartitionToILP {
10 target: ILP<i64, i64, Bounded>,
11 n: usize,
12 one_hot: bool,
13}
14
15impl ReductionResult for ReductionAcyclicPartitionToILP {
16 type Source = AcyclicPartition<i64>;
17 type Target = ILP<i64, i64, Bounded>;
18
19 fn target_problem(&self) -> &ILP<i64, i64, Bounded> {
20 &self.target
21 }
22
23 fn extract_solution(
25 &self,
26 target_solution: &<Self::Target as crate::traits::Problem>::Solution,
27 ) -> crate::rules::ExtractionResult<<Self::Source as crate::traits::Problem>::Solution> {
28 crate::rules::traits::validate_target_witness(
29 self.target_problem(),
30 target_solution,
31 |value| value.value.is_some(),
32 "target ILP assignment is infeasible",
33 )?;
34
35 if self.one_hot {
36 crate::rules::ilp_helpers::one_hot_decode_rows(target_solution, self.n, self.n, 0)
37 } else {
38 crate::rules::ilp_helpers::decode_usize_values(&target_solution[..self.n])
39 }
40 }
41}
42
43#[crate::aggregate_reduction(ilp_feasibility)]
44impl crate::rules::AggregateReductionResult for ReductionAcyclicPartitionToILP {}
45
46fn two_part_anchors(problem: &AcyclicPartition<i64>) -> Option<(usize, usize)> {
50 let n = problem.num_vertices();
51 if n < 3
52 || *problem.weight_bound() < 0
53 || problem.vertex_weights().iter().any(|&weight| weight < 0)
54 || problem.arc_costs().iter().any(|&cost| cost < 0)
55 {
56 return None;
57 }
58 let arcs = problem.graph().arcs();
59 let mut incoming = vec![false; n];
60 let mut outgoing = vec![false; n];
61 for &(u, v) in &arcs {
62 if u != v {
63 outgoing[u] = true;
64 incoming[v] = true;
65 }
66 }
67 for root in (0..n).filter(|&v| !incoming[v]) {
68 for sink in (0..n).filter(|&v| v != root && !outgoing[v]) {
69 if i128::from(problem.vertex_weights()[root])
70 + i128::from(problem.vertex_weights()[sink])
71 <= i128::from(*problem.weight_bound())
72 {
73 continue;
74 }
75 let mut left = vec![None; n];
76 let mut right = vec![None; n];
77 let mut baseline = 0_i128;
78 for (&(u, v), &cost) in arcs.iter().zip(problem.arc_costs()) {
79 let cost = i128::from(cost);
80 if u == root && v == sink {
81 baseline += cost;
82 } else if u == root && v != root {
83 left[v] = Some(left[v].unwrap_or(0) + cost);
84 } else if v == sink && u != sink {
85 right[u] = Some(right[u].unwrap_or(0) + cost);
86 }
87 }
88 let mut extra = i128::MAX;
89 for v in (0..n).filter(|&v| v != root && v != sink) {
90 if let (Some(a), Some(b)) = (left[v], right[v]) {
91 baseline += a.min(b);
92 extra = extra.min(a.max(b));
93 } else {
94 extra = 0;
95 break;
96 }
97 }
98 if extra > 0 && i128::from(*problem.cost_bound()) - baseline < extra {
99 return Some((root, sink));
100 }
101 }
102 }
103 None
104}
105
106fn two_part_reduction(
109 problem: &AcyclicPartition<i64>,
110 root: usize,
111 sink: usize,
112) -> Result<ReductionAcyclicPartitionToILP, crate::rules::ReductionError> {
113 type Source = AcyclicPartition<i64>;
114 type Target = ILP<i64, i64, Bounded>;
115 let n = problem.num_vertices();
116 let exact = |value, operation| {
117 i64::try_from(value).map_err(|_| {
118 crate::rules::ReductionError::integer_overflow::<Source, Target>(operation)
119 })
120 };
121 let mut variables = vec![IntegerVariable::binary(); n];
122 variables[root] = IntegerVariable::new(Some(0), Some(0))
123 .map_err(<Source as ReduceTo<Target>>::target_construction)?;
124 variables[sink] = IntegerVariable::new(Some(1), Some(1))
125 .map_err(<Source as ReduceTo<Target>>::target_construction)?;
126 let mut constraints = Vec::new();
127 let mut costs = vec![0_i128; n];
128 for (&(u, v), &cost) in problem.graph().arcs().iter().zip(problem.arc_costs()) {
129 constraints.push(LinearConstraint::le(vec![(u, 1), (v, -1)], 0));
130 costs[u] -= i128::from(cost);
131 costs[v] += i128::from(cost);
132 }
133 let interior = |v: &usize| *v != root && *v != sink;
134 let weights: Vec<_> = (0..n)
135 .filter(interior)
136 .map(|v| (v, problem.vertex_weights()[v]))
137 .collect();
138 let total_weight: i128 = weights.iter().map(|&(_, weight)| i128::from(weight)).sum();
139 constraints.push(LinearConstraint::le(
140 weights.clone(),
141 exact(
142 i128::from(*problem.weight_bound()) - i128::from(problem.vertex_weights()[sink]),
143 "bounding the sink part weight",
144 )?,
145 ));
146 constraints.push(LinearConstraint::ge(
147 weights,
148 exact(
149 total_weight - i128::from(*problem.weight_bound())
150 + i128::from(problem.vertex_weights()[root]),
151 "bounding the root part weight",
152 )?,
153 ));
154 let cost_terms = (0..n)
155 .filter(interior)
156 .map(|v| Ok((v, exact(costs[v], "summing crossing cost coefficients")?)))
157 .collect::<Result<_, crate::rules::ReductionError>>()?;
158 constraints.push(LinearConstraint::le(
159 cost_terms,
160 exact(
161 i128::from(*problem.cost_bound()) - costs[sink],
162 "substituting the sink crossing cost",
163 )?,
164 ));
165 let selected = i128::from(*problem.weight_bound()) - i128::from(problem.vertex_weights()[root]);
166 let unselected =
167 i128::from(*problem.weight_bound()) - i128::from(problem.vertex_weights()[sink]);
168 if selected > 0
169 && selected + unselected == total_weight
170 && (0..n)
171 .filter(interior)
172 .all(|v| problem.vertex_weights()[v] <= 1)
173 {
174 let mut parents = vec![Vec::new(); n];
178 for (u, v) in problem.graph().arcs() {
179 if interior(&u) && problem.vertex_weights()[u] == 1 {
180 parents[v].push(u);
181 }
182 }
183 let mut pairs = std::collections::BTreeMap::new();
184 for v in (0..n).filter(interior) {
185 parents[v].sort_unstable();
186 parents[v].dedup();
187 if problem.vertex_weights()[v] == 0 && parents[v].len() == 2 {
188 pairs.entry((parents[v][0], parents[v][1])).or_insert(v);
189 }
190 }
191 let mut incident = vec![Vec::new(); n];
192 for ((u, v), item) in pairs {
193 incident[u].push(item);
194 incident[v].push(item);
195 }
196 let limit = exact(selected - 1, "bounding distinct selected neighbors")?;
197 for (v, neighbors) in incident
198 .into_iter()
199 .enumerate()
200 .filter(|(_, neighbors)| !neighbors.is_empty())
201 {
202 let degree = <Source as ReduceTo<Target>>::exact_i64(
203 neighbors.len(),
204 "counting distinct neighbors",
205 )?;
206 let mut terms: Vec<_> = neighbors.into_iter().map(|item| (item, 1)).collect();
207 terms.push((v, -limit));
208 constraints.push(LinearConstraint::ge(
209 terms,
210 exact(
211 i128::from(degree) - i128::from(limit),
212 "bounding selected incidence items",
213 )?,
214 ));
215 }
216 }
217 let target = Target::with_variables(variables, constraints, vec![], ObjectiveSense::Minimize)
218 .map_err(<Source as ReduceTo<Target>>::target_construction)?;
219 Ok(ReductionAcyclicPartitionToILP {
220 target,
221 n,
222 one_hot: false,
223 })
224}
225
226#[reduction(transform = upper_bound {
227 num_vars = "num_vertices^2 + 2 * num_vertices + num_arcs",
228 num_constraints = "num_vertices^2 + 4 * num_vertices + 2 * num_arcs + 1",
229 max_constraint_magnitude_bits = "max_numeric_magnitude_bits + num_vertices + num_arcs + 1",
230 num_nonzeros = "6 * num_vertices^2 + 2 * num_vertices + 7 * num_arcs",
231})]
232impl ReduceTo<ILP<i64, i64, Bounded>> for AcyclicPartition<i64> {
233 type Result = ReductionAcyclicPartitionToILP;
234
235 fn reduce_to(&self) -> Result<Self::Result, crate::rules::ReductionError> {
236 let n = self.num_vertices();
237 let arcs = self.graph().arcs();
238 let m = arcs.len();
239 if let Some((root, sink)) = two_part_anchors(self) {
240 return two_part_reduction(self, root, sink);
241 }
242 let parts = n;
243
244 let overflow = || {
245 crate::rules::ReductionError::integer_overflow::<Self, ILP<i64, i64, Bounded>>(
246 "counting acyclic partition variables",
247 )
248 };
249 let square = n.checked_mul(parts).ok_or_else(overflow)?;
250 let labels = square.checked_add(parts).ok_or_else(overflow)?;
251 let crossing = labels.checked_add(n).ok_or_else(overflow)?;
252 let num_vars = crossing.checked_add(m).ok_or_else(overflow)?;
253 let last_label = Self::exact_i64(parts.saturating_sub(1), "bounding partition labels")?;
254 let x_idx = |v: usize, c: usize| v * parts + c;
255 let empty_idx = |c: usize| square + c;
256 let label_idx = |v: usize| labels + v;
257 let y_idx = |t: usize| crossing + t;
258 let mut constraints = Vec::new();
259 let vertex_weights = self.vertex_weights();
260 let arc_costs = self.arc_costs();
261 let weight_bound = *self.weight_bound();
262 let cost_bound = *self.cost_bound();
263
264 for v in 0..n {
266 let terms: Vec<(usize, i64)> = (0..parts).map(|c| (x_idx(v, c), 1)).collect();
267 constraints.push(LinearConstraint::eq(terms, 1));
268 let mut label = vec![(label_idx(v), 1)];
269 for c in 1..parts {
270 label.push((
271 x_idx(v, c),
272 -Self::exact_i64(c, "representing a partition label")?,
273 ));
274 }
275 constraints.push(LinearConstraint::eq(label, 0));
276 }
277
278 for c in 0..parts {
280 let mut membership = vec![(empty_idx(c), 1)];
281 for v in 0..n {
282 constraints.push(LinearConstraint::le(
283 vec![(x_idx(v, c), 1), (empty_idx(c), 1)],
284 1,
285 ));
286 membership.push((x_idx(v, c), 1));
287 }
288 constraints.push(LinearConstraint::ge(membership, 1));
289 let mut terms: Vec<(usize, i64)> = vertex_weights
290 .iter()
291 .enumerate()
292 .map(|(vertex, &weight)| (x_idx(vertex, c), weight))
293 .collect();
294 terms.push((empty_idx(c), weight_bound.min(0)));
296 constraints.push(LinearConstraint::le(terms, weight_bound));
297 }
298
299 for (t, &(u, v)) in arcs.iter().enumerate() {
302 constraints.push(LinearConstraint::ge(
303 vec![(label_idx(v), 1), (label_idx(u), -1), (y_idx(t), -1)],
304 0,
305 ));
306 constraints.push(LinearConstraint::le(
307 vec![
308 (label_idx(v), 1),
309 (label_idx(u), -1),
310 (y_idx(t), -last_label),
311 ],
312 0,
313 ));
314 }
315
316 let cost_terms: Vec<(usize, i64)> = arc_costs
318 .iter()
319 .enumerate()
320 .map(|(arc, &cost)| (y_idx(arc), cost))
321 .collect();
322 constraints.push(LinearConstraint::le(cost_terms, cost_bound));
323
324 let mut variables = vec![IntegerVariable::binary(); num_vars];
325 variables[labels..crossing].fill(
326 IntegerVariable::new(Some(0), Some(last_label)).map_err(Self::target_construction)?,
327 );
328 let target = ILP::with_variables(variables, constraints, vec![], ObjectiveSense::Minimize)
329 .map_err(Self::target_construction)?;
330
331 Ok(ReductionAcyclicPartitionToILP {
332 target,
333 n,
334 one_hot: true,
335 })
336 }
337}
338
339#[cfg(feature = "example-db")]
340pub(crate) fn canonical_rule_example_specs() -> Vec<crate::example_db::specs::RuleExampleSpec> {
341 use crate::topology::DirectedGraph;
342 vec![crate::example_db::specs::RuleExampleSpec {
343 id: "acyclicpartition_to_ilp",
344 build: || {
345 crate::example_db::specs::rule_example_via_bounded_ilp(AcyclicPartition::new(
346 DirectedGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]),
347 vec![1, 1, 1, 1],
348 vec![1, 1, 1],
349 3,
350 2,
351 ))
352 },
353 }]
354}
355
356#[cfg(test)]
357#[path = "../unit_tests/rules/acyclicpartition_ilp.rs"]
358mod tests;