Skip to main content

problemreductions/rules/
acyclicpartition_ilp.rs

1//! Bounded partition labels with exact crossing flags and occupied-part budgets.
2
3use 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    /// Decode one-hot memberships or return the direct binary part labels.
24    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
46// Two separated heavy anchors, with a two-arc path through every other
47// vertex, give a lower bound on crossing cost. A third part would cross both
48// arcs of one path, rather than at least one, and pay the indicated extra.
49fn 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
106// With two certified occupied parts, one binary label per vertex suffices.
107// Substitute fixed anchor labels before summing weight and crossing rows.
108fn 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        // Exactly k unit-weight vertices belong to the root part. A zero-weight
175        // item with two such parents can be selected only with both parents.
176        // Thus each selected parent has at most k-1 distinct selected neighbors.
177        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        // Assignment: Σ_c x_{v,c} = 1 for each vertex v.
265        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        // Only occupied classes must meet the weight bound, which can be negative.
279        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            // Keep the bound on the RHS to preserve representable source sums.
295            terms.push((empty_idx(c), weight_bound.min(0)));
296            constraints.push(LinearConstraint::le(terms, weight_bound));
297        }
298
299        // A crossing arc increases its part label by at least one; an internal
300        // arc has equal labels. This equivalence also handles negative costs.
301        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        // Cost bound: Σ_t cost(a_t) * y_t ≤ K.
317        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;