Skip to main content

problemreductions/rules/
rootedtreestorageassignment_ilp.rs

1//! Reduction from RootedTreeStorageAssignment to ILP (Integer Linear Programming).
2//!
3//! Uses parent indicators p_{v,u}, depth variables d_v, ancestor indicators
4//! a_{u,v}, transitive-closure helpers h_{u,v,w}, and per-subset gadgets
5//! (top/bottom selectors, pair selectors, endpoint depths, extension costs).
6
7use crate::models::algebraic::{Bounded, IntegerVariable, LinearConstraint, ObjectiveSense, ILP};
8use crate::models::set::RootedTreeStorageAssignment;
9use crate::reduction;
10use crate::rules::ilp_helpers::{mccormick_product, one_hot_decode_rows};
11use crate::rules::traits::{ReduceTo, ReductionResult};
12
13// Index helpers
14
15fn idx_p(n: usize, v: usize, u: usize) -> usize {
16    v * n + u
17}
18
19fn idx_d(n: usize, v: usize) -> usize {
20    n * n + v
21}
22
23fn idx_a(n: usize, u: usize, v: usize) -> usize {
24    n * n + n + u * n + v
25}
26
27fn idx_h(n: usize, u: usize, v: usize, w: usize) -> usize {
28    2 * n * n + n + (u * n + v) * n + w
29}
30
31fn idx_t(n: usize, r: usize, s: usize, u: usize) -> usize {
32    let _ = r;
33    n * n * n + 2 * n * n + n + s * n + u
34}
35
36fn idx_b(n: usize, r: usize, s: usize, v: usize) -> usize {
37    n * n * n + 2 * n * n + n + r * n + s * n + v
38}
39
40fn idx_m(n: usize, r: usize, s: usize, u: usize, v: usize) -> usize {
41    n * n * n + 2 * n * n + n + 2 * r * n + s * n * n + u * n + v
42}
43
44fn idx_big_t(n: usize, r: usize, s: usize) -> usize {
45    n * n * n + 2 * n * n + n + 2 * r * n + r * n * n + s
46}
47
48fn idx_big_b(n: usize, r: usize, s: usize) -> usize {
49    n * n * n + 2 * n * n + n + 2 * r * n + r * n * n + r + s
50}
51
52fn idx_c(n: usize, r: usize, s: usize) -> usize {
53    n * n * n + 2 * n * n + n + 2 * r * n + r * n * n + 2 * r + s
54}
55
56fn total_vars(n: usize, r: usize) -> usize {
57    n * n * n + 2 * n * n + n + r * (n * n + 2 * n + 3)
58}
59
60#[derive(Debug, Clone)]
61pub struct ReductionRTSAToILP {
62    target: ILP<i64, i64, Bounded>,
63    n: usize,
64}
65
66impl ReductionResult for ReductionRTSAToILP {
67    type Source = RootedTreeStorageAssignment;
68    type Target = ILP<i64, i64, Bounded>;
69
70    fn target_problem(&self) -> &ILP<i64, i64, Bounded> {
71        &self.target
72    }
73
74    /// Decode parent array from one-hot parent indicators p_{v,u}.
75    fn extract_solution(
76        &self,
77        target_solution: &<Self::Target as crate::traits::Problem>::Solution,
78    ) -> crate::rules::ExtractionResult<<Self::Source as crate::traits::Problem>::Solution> {
79        crate::rules::traits::validate_target_witness(
80            self.target_problem(),
81            target_solution,
82            |value| value.value.is_some(),
83            "target ILP assignment is infeasible",
84        )?;
85
86        one_hot_decode_rows(target_solution, self.n, self.n, 0)
87    }
88}
89
90#[crate::aggregate_reduction(ilp_feasibility)]
91impl crate::rules::AggregateReductionResult for ReductionRTSAToILP {}
92
93// Tree rows contribute <=9n^3+3n^2-n terms. Each nontrivial subset of
94// size s contributes 10n^2+4sn^2+4n+12s+8, plus one budget term.
95// Use s<=n and r<=num_subsets and drop -n. Magnitudes are bounded by
96// n+1 and r(n-1); n+num_subsets+1 bits also cover n=0 and clamped budgets.
97#[reduction(
98    transform = upper_bound {
99        max_constraint_magnitude_bits = "universe_size + num_subsets + 1",
100        num_vars = "universe_size * universe_size * universe_size + 2 * universe_size * universe_size + universe_size + num_subsets * (universe_size * universe_size + 2 * universe_size + 3)",
101        num_constraints = "4 * universe_size^3 + 6 * universe_size^2 + 5 * universe_size + 2 + num_subsets * (2 * universe_size^3 + 5 * universe_size^2 + 8 * universe_size + 8)",
102        num_nonzeros = "9 * universe_size^3 + 3 * universe_size^2 + num_subsets * (4 * universe_size^3 + 10 * universe_size^2 + 16 * universe_size + 9)",
103    },
104)]
105impl ReduceTo<ILP<i64, i64, Bounded>> for RootedTreeStorageAssignment {
106    type Result = ReductionRTSAToILP;
107
108    fn reduce_to(&self) -> Result<Self::Result, crate::rules::ReductionError> {
109        let n = self.universe_size();
110        let subsets = self.subsets();
111
112        // Nontrivial subsets (size >= 2)
113        let nontrivial: Vec<usize> = (0..subsets.len())
114            .filter(|&k| subsets[k].len() >= 2)
115            .collect();
116        let r = nontrivial.len();
117        // Each extension cost is between 0 and n-1; negative budgets are infeasible.
118        // Saturation is safe: the original i64 budget cannot exceed i64::MAX.
119        let max_cost = Self::exact_i64(r, "encoding the subset count")?.saturating_mul(
120            Self::exact_i64(n.saturating_sub(1), "encoding the maximum extension cost")?,
121        );
122        let bound = self.bound().clamp(-1, max_cost);
123
124        if n == 0 {
125            return Ok(ReductionRTSAToILP {
126                target: ILP::new(
127                    0,
128                    vec![LinearConstraint::le(vec![], bound)],
129                    vec![],
130                    ObjectiveSense::Minimize,
131                )
132                .map_err(Self::target_construction)?,
133                n,
134            });
135        }
136
137        let nv = total_vars(n, r);
138        let big_m = Self::exact_i64(n, "representing the vertex count in ILP rows")?;
139        let big_m_depth =
140            Self::exact_i64(n - 1, "representing the maximum tree depth in ILP rows")?;
141
142        let mut constraints = Vec::new();
143
144        // === Rooted-tree constraints ===
145
146        // Σ_u p_{v,u} = 1  ∀ v
147        for v in 0..n {
148            let terms: Vec<(usize, i64)> = (0..n).map(|u| (idx_p(n, v, u), 1)).collect();
149            constraints.push(LinearConstraint::eq(terms, 1));
150        }
151
152        // Σ_v p_{v,v} = 1 (exactly one root)
153        let root_terms: Vec<(usize, i64)> = (0..n).map(|v| (idx_p(n, v, v), 1)).collect();
154        constraints.push(LinearConstraint::eq(root_terms, 1));
155
156        // p_{v,u} binary: upper bound p_{v,u} <= 1
157        for v in 0..n {
158            for u in 0..n {
159                constraints.push(LinearConstraint::le(vec![(idx_p(n, v, u), 1)], 1));
160            }
161        }
162
163        // d_v <= (n-1)(1 - p_{v,v})  ∀ v  (root has depth 0)
164        for v in 0..n {
165            constraints.push(LinearConstraint::le(
166                vec![(idx_d(n, v), 1), (idx_p(n, v, v), big_m_depth)],
167                big_m_depth,
168            ));
169        }
170
171        // d_v >= 0  ∀ v
172        for v in 0..n {
173            constraints.push(LinearConstraint::ge(vec![(idx_d(n, v), 1)], 0));
174        }
175
176        // d_v <= n-1  ∀ v
177        for v in 0..n {
178            constraints.push(LinearConstraint::le(vec![(idx_d(n, v), 1)], big_m_depth));
179        }
180
181        // For u != v: d_v - d_u >= 1 - n(1 - p_{v,u})
182        //             d_v - d_u <= 1 + n(1 - p_{v,u})
183        for v in 0..n {
184            for u in 0..n {
185                if u != v {
186                    // d_v - d_u - n*p_{v,u} >= 1 - n
187                    constraints.push(LinearConstraint::ge(
188                        vec![
189                            (idx_d(n, v), 1),
190                            (idx_d(n, u), -1),
191                            (idx_p(n, v, u), -big_m),
192                        ],
193                        1 - big_m,
194                    ));
195
196                    // d_v - d_u <= 1 + n(1 - p_{v,u})
197                    // => d_v - d_u - n + n*p_{v,u} <= 1
198                    // => d_v - d_u + n*p_{v,u} <= 1 + n
199                    constraints.push(LinearConstraint::le(
200                        vec![(idx_d(n, v), 1), (idx_d(n, u), -1), (idx_p(n, v, u), big_m)],
201                        1 + big_m,
202                    ));
203                }
204            }
205        }
206
207        // === Ancestor relation ===
208
209        // a_{v,v} = 1  ∀ v
210        for v in 0..n {
211            constraints.push(LinearConstraint::eq(vec![(idx_a(n, v, v), 1)], 1));
212        }
213
214        // h_{u,v,v} = 0  ∀ u,v
215        for u in 0..n {
216            for v in 0..n {
217                constraints.push(LinearConstraint::eq(vec![(idx_h(n, u, v, v), 1)], 0));
218            }
219        }
220
221        // For u != v: a_{u,v} = Σ_w h_{u,v,w}
222        for u in 0..n {
223            for v in 0..n {
224                if u != v {
225                    let mut terms = vec![(idx_a(n, u, v), -1)];
226                    for w in 0..n {
227                        terms.push((idx_h(n, u, v, w), 1));
228                    }
229                    constraints.push(LinearConstraint::eq(terms, 0));
230                }
231            }
232        }
233
234        // h_{u,v,w} <= p_{v,w}  ∀ u,v,w with w != v
235        // h_{u,v,w} <= a_{u,w}  ∀ u,v,w with w != v
236        // h_{u,v,w} >= p_{v,w} + a_{u,w} - 1  ∀ u,v,w with w != v
237        for u in 0..n {
238            for v in 0..n {
239                for w in 0..n {
240                    if w != v {
241                        constraints.extend(mccormick_product(
242                            idx_h(n, u, v, w),
243                            idx_p(n, v, w),
244                            idx_a(n, u, w),
245                        ));
246                    }
247                }
248            }
249        }
250
251        // Binary bounds for a, h
252        for u in 0..n {
253            for v in 0..n {
254                constraints.push(LinearConstraint::le(vec![(idx_a(n, u, v), 1)], 1));
255                for w in 0..n {
256                    constraints.push(LinearConstraint::le(vec![(idx_h(n, u, v, w), 1)], 1));
257                }
258            }
259        }
260
261        // === Subset gadgets ===
262        for (s, &orig_k) in nontrivial.iter().enumerate() {
263            let subset = &subsets[orig_k];
264            let subset_size = subset.len();
265
266            // Top selectors: Σ_{u ∈ S} t_{s,u} = 1, t_{s,u} = 0 for u ∉ S
267            let top_terms: Vec<(usize, i64)> =
268                subset.iter().map(|&u| (idx_t(n, r, s, u), 1)).collect();
269            constraints.push(LinearConstraint::eq(top_terms, 1));
270            for u in 0..n {
271                if !subset.contains(&u) {
272                    constraints.push(LinearConstraint::eq(vec![(idx_t(n, r, s, u), 1)], 0));
273                }
274                // Binary bound
275                constraints.push(LinearConstraint::le(vec![(idx_t(n, r, s, u), 1)], 1));
276            }
277
278            // Bottom selectors: Σ_{v ∈ S} b_{s,v} = 1, b_{s,v} = 0 for v ∉ S
279            let bot_terms: Vec<(usize, i64)> =
280                subset.iter().map(|&v| (idx_b(n, r, s, v), 1)).collect();
281            constraints.push(LinearConstraint::eq(bot_terms, 1));
282            for v in 0..n {
283                if !subset.contains(&v) {
284                    constraints.push(LinearConstraint::eq(vec![(idx_b(n, r, s, v), 1)], 0));
285                }
286                constraints.push(LinearConstraint::le(vec![(idx_b(n, r, s, v), 1)], 1));
287            }
288
289            // Pair selectors (McCormick): m_{s,u,v} = t_{s,u} * b_{s,v}
290            for u in 0..n {
291                for v in 0..n {
292                    constraints.extend(mccormick_product(
293                        idx_m(n, r, s, u, v),
294                        idx_t(n, r, s, u),
295                        idx_b(n, r, s, v),
296                    ));
297                    constraints.push(LinearConstraint::le(vec![(idx_m(n, r, s, u, v), 1)], 1));
298                }
299            }
300
301            // Path condition: m_{s,u,v} <= a_{u,v} (top is ancestor of bottom)
302            for u in 0..n {
303                for v in 0..n {
304                    constraints.push(LinearConstraint::le(
305                        vec![(idx_m(n, r, s, u, v), 1), (idx_a(n, u, v), -1)],
306                        0,
307                    ));
308                }
309            }
310
311            // Every subset element w lies on the chain:
312            // m_{s,u,v} <= a_{u,w} and m_{s,u,v} <= a_{w,v}  ∀ w ∈ S, u, v
313            for &w in subset {
314                for u in 0..n {
315                    for v in 0..n {
316                        constraints.push(LinearConstraint::le(
317                            vec![(idx_m(n, r, s, u, v), 1), (idx_a(n, u, w), -1)],
318                            0,
319                        ));
320                        constraints.push(LinearConstraint::le(
321                            vec![(idx_m(n, r, s, u, v), 1), (idx_a(n, w, v), -1)],
322                            0,
323                        ));
324                    }
325                }
326            }
327
328            // Endpoint depths: T_s, B_s
329            // T_s - d_u <= (n-1)(1 - t_{s,u})  and  d_u - T_s <= (n-1)(1 - t_{s,u})
330            for &u in subset {
331                constraints.push(LinearConstraint::le(
332                    vec![
333                        (idx_big_t(n, r, s), 1),
334                        (idx_d(n, u), -1),
335                        (idx_t(n, r, s, u), big_m_depth),
336                    ],
337                    big_m_depth,
338                ));
339                constraints.push(LinearConstraint::le(
340                    vec![
341                        (idx_d(n, u), 1),
342                        (idx_big_t(n, r, s), -1),
343                        (idx_t(n, r, s, u), big_m_depth),
344                    ],
345                    big_m_depth,
346                ));
347            }
348            // B_s - d_v <= (n-1)(1 - b_{s,v})  and  d_v - B_s <= (n-1)(1 - b_{s,v})
349            for &v in subset {
350                constraints.push(LinearConstraint::le(
351                    vec![
352                        (idx_big_b(n, r, s), 1),
353                        (idx_d(n, v), -1),
354                        (idx_b(n, r, s, v), big_m_depth),
355                    ],
356                    big_m_depth,
357                ));
358                constraints.push(LinearConstraint::le(
359                    vec![
360                        (idx_d(n, v), 1),
361                        (idx_big_b(n, r, s), -1),
362                        (idx_b(n, r, s, v), big_m_depth),
363                    ],
364                    big_m_depth,
365                ));
366            }
367
368            // Depth bounds for T_s, B_s
369            constraints.push(LinearConstraint::ge(vec![(idx_big_t(n, r, s), 1)], 0));
370            constraints.push(LinearConstraint::le(
371                vec![(idx_big_t(n, r, s), 1)],
372                big_m_depth,
373            ));
374            constraints.push(LinearConstraint::ge(vec![(idx_big_b(n, r, s), 1)], 0));
375            constraints.push(LinearConstraint::le(
376                vec![(idx_big_b(n, r, s), 1)],
377                big_m_depth,
378            ));
379
380            // Extension cost: c_s = B_s - T_s + 1 - |S|
381            // => c_s - B_s + T_s = 1 - |S|
382            constraints.push(LinearConstraint::eq(
383                vec![
384                    (idx_c(n, r, s), 1),
385                    (idx_big_b(n, r, s), -1),
386                    (idx_big_t(n, r, s), 1),
387                ],
388                1 - Self::exact_i64(
389                    subset_size,
390                    "representing a subset cardinality in an ILP row",
391                )?,
392            ));
393
394            // c_s >= 0
395            constraints.push(LinearConstraint::ge(vec![(idx_c(n, r, s), 1)], 0));
396        }
397
398        // Total cost bound: Σ c_s <= K
399        let cost_terms: Vec<(usize, i64)> = (0..r).map(|s| (idx_c(n, r, s), 1)).collect();
400        constraints.push(LinearConstraint::le(cost_terms, bound));
401
402        let mut variables = vec![IntegerVariable::binary(); nv];
403        let depth_domain =
404            IntegerVariable::new(Some(0), Some(big_m_depth)).map_err(Self::target_construction)?;
405        variables[n * n..n * n + n].fill(depth_domain);
406        variables[idx_big_t(n, r, 0)..].fill(depth_domain);
407
408        let target = ILP::with_variables(variables, constraints, vec![], ObjectiveSense::Minimize)
409            .map_err(Self::target_construction)?;
410        Ok(ReductionRTSAToILP { target, n })
411    }
412}
413
414#[cfg(feature = "example-db")]
415pub(crate) fn canonical_rule_example_specs() -> Vec<crate::example_db::specs::RuleExampleSpec> {
416    use crate::export::SolutionPair;
417    vec![crate::example_db::specs::RuleExampleSpec {
418        id: "rootedtreestorageassignment_to_ilp",
419        build: || {
420            let source = RootedTreeStorageAssignment::new(3, vec![vec![0, 1], vec![1, 2]], 1);
421            let reduction: ReductionRTSAToILP =
422                ReduceTo::<ILP<i64, i64, Bounded>>::reduce_to(&source)
423                    .expect("reduction should succeed");
424            let target_config = {
425                let ilp_solver = crate::solvers::ILPSolver::new();
426                ilp_solver
427                    .solve(reduction.target_problem())
428                    .expect("ILP should be solvable")
429            };
430            let source_config = reduction.extract_solution(&target_config).unwrap();
431            crate::example_db::specs::rule_example_with_witness::<_, ILP<i64, i64, Bounded>>(
432                source,
433                SolutionPair {
434                    source_config: serde_json::to_value(source_config)
435                        .expect("solution serialization must succeed"),
436                    target_config: serde_json::to_value(target_config)
437                        .expect("solution serialization must succeed"),
438                },
439            )
440        },
441    }]
442}
443
444#[cfg(test)]
445#[path = "../unit_tests/rules/rootedtreestorageassignment_ilp.rs"]
446mod tests;