Skip to main content

problemreductions/rules/
minimumexternalmacrodatacompression_ilp.rs

1//! Reduction from MinimumExternalMacroDataCompression to ILP (Integer Linear Programming).
2//!
3//! The EMDC problem is formulated as a binary ILP using a flow-on-DAG partition
4//! model for the compressed string, combined with dictionary assignment variables.
5//!
6//! **Variables (all binary):**
7//! - `d[j][c]`: D-slot j contains symbol c (j=0..n-1, c=0..k-1)
8//! - `d_used[j]`: D-slot j is used (j=0..n-1)
9//! - `lit[i]`: position i is covered by a literal in C (i=0..n-1)
10//! - `ptr[i][l][d_start]`: segment [i, i+l) is a pointer referencing D[d_start..d_start+l]
11//! - Flow conservation ensures positions 0..n are partitioned into segments.
12//!
13//! **Objective:** minimize |D| + |C_literals| + h * |C_pointers|
14//! = sum d_used[j] + sum lit[i] + h * sum ptr[i][l][d_start]
15
16use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP};
17use crate::models::misc::MinimumExternalMacroDataCompression;
18use crate::reduction;
19use crate::rules::traits::{ReduceTo, ReductionResult};
20
21/// Index layout for ILP variables.
22#[derive(Debug, Clone)]
23struct VarLayout {
24    n: usize,
25    k: usize,
26    /// Offset of d[j][c] block: index = d_offset + j * k + c
27    d_offset: usize,
28    /// Offset of d_used[j] block: index = d_used_offset + j
29    d_used_offset: usize,
30    /// Offset of lit[i] block: index = lit_offset + i
31    lit_offset: usize,
32    /// Offset of ptr variables, stored as a flat list.
33    /// Each entry in `ptr_triples` is (i, l, d_start) and its var index = ptr_offset + idx.
34    ptr_offset: usize,
35    /// The (i, l, d_start) triples in order.
36    ptr_triples: Vec<(usize, usize, usize)>,
37    /// Total number of variables.
38    total_vars: usize,
39}
40
41impl VarLayout {
42    fn new(n: usize, k: usize) -> Self {
43        let d_offset = 0;
44        let d_used_offset = d_offset + n * k;
45        let lit_offset = d_used_offset + n;
46        let ptr_offset = lit_offset + n;
47
48        // Enumerate all valid (i, l, d_start) triples
49        let mut ptr_triples = Vec::new();
50        for i in 0..n {
51            for l in 1..=(n - i) {
52                for d_start in 0..=(n - l) {
53                    ptr_triples.push((i, l, d_start));
54                }
55            }
56        }
57
58        let total_vars = ptr_offset + ptr_triples.len();
59        Self {
60            n,
61            k,
62            d_offset,
63            d_used_offset,
64            lit_offset,
65            ptr_offset,
66            ptr_triples,
67            total_vars,
68        }
69    }
70
71    fn d_var(&self, j: usize, c: usize) -> usize {
72        self.d_offset + j * self.k + c
73    }
74
75    fn d_used_var(&self, j: usize) -> usize {
76        self.d_used_offset + j
77    }
78
79    fn lit_var(&self, i: usize) -> usize {
80        self.lit_offset + i
81    }
82
83    fn ptr_var(&self, i: usize, l: usize, d_start: usize) -> usize {
84        // Find the index of (i, l, d_start) in ptr_triples
85        let idx = self
86            .ptr_triples
87            .iter()
88            .position(|&(pi, pl, pd)| pi == i && pl == l && pd == d_start)
89            .expect("invalid ptr triple");
90        self.ptr_offset + idx
91    }
92
93    /// Get all ptr variable indices for segments starting at position i with length l.
94    fn ptr_vars_for_segment(&self, i: usize, l: usize) -> Vec<usize> {
95        self.ptr_triples
96            .iter()
97            .enumerate()
98            .filter(|(_, &(pi, pl, _))| pi == i && pl == l)
99            .map(|(idx, _)| self.ptr_offset + idx)
100            .collect()
101    }
102}
103
104/// Result of reducing MinimumExternalMacroDataCompression to ILP.
105#[derive(Debug, Clone)]
106pub struct ReductionEMDCToILP {
107    target: ILP<bool>,
108    /// Variable layout for solution extraction.
109    layout: VarLayout,
110    /// The source string (needed for extract_solution).
111    source_string: Vec<usize>,
112    /// Alphabet size.
113    alphabet_size: usize,
114}
115
116impl ReductionResult for ReductionEMDCToILP {
117    type Source = MinimumExternalMacroDataCompression;
118    type Target = ILP<bool>;
119
120    fn target_problem(&self) -> &ILP<bool> {
121        &self.target
122    }
123
124    fn extract_solution(
125        &self,
126        target_solution: &<Self::Target as crate::traits::Problem>::Solution,
127    ) -> crate::rules::ExtractionResult<<Self::Source as crate::traits::Problem>::Solution> {
128        crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?;
129
130        Ok({
131            let n = self.layout.n;
132            let k = self.alphabet_size;
133            let empty = k; // empty marker
134
135            // Build D-slots
136            let mut d_slots = vec![empty; n];
137            for j in 0..n {
138                let symbols: Vec<_> = (0..k)
139                    .filter(|&c| target_solution[self.layout.d_var(j, c)] == 1)
140                    .collect();
141                if target_solution[self.layout.d_used_var(j)] == 1 {
142                    match symbols.as_slice() {
143                        [symbol] => d_slots[j] = *symbol,
144                        [] => {
145                            return Err(crate::rules::ExtractionError::invalid(format!(
146                                "dictionary slot {j} is active without a symbol"
147                            )))
148                        }
149                        _ => {
150                            return Err(crate::rules::ExtractionError::invalid(format!(
151                                "dictionary slot {j} selects multiple symbols"
152                            )))
153                        }
154                    }
155                } else if !symbols.is_empty() {
156                    return Err(crate::rules::ExtractionError::invalid(format!(
157                        "inactive dictionary slot {j} selects a symbol"
158                    )));
159                }
160            }
161
162            // Walk through active segments to build C-slots
163            let mut c_slots = vec![empty; n];
164            let mut c_pos = 0;
165            let mut pos = 0;
166            while pos < n {
167                let pointers: Vec<_> = (1..=(n - pos))
168                    .flat_map(|length| {
169                        (0..=(n - length)).filter_map(move |start| {
170                            (target_solution[self.layout.ptr_var(pos, length, start)] == 1)
171                                .then_some((start, length))
172                        })
173                    })
174                    .collect();
175                if target_solution[self.layout.lit_var(pos)] == 1 {
176                    if !pointers.is_empty() {
177                        return Err(crate::rules::ExtractionError::invalid(format!(
178                            "position {pos} selects both a literal and a pointer"
179                        )));
180                    }
181                    // Literal at position pos
182                    c_slots[c_pos] = self.source_string[pos];
183                    c_pos += 1;
184                    pos += 1;
185                    continue;
186                }
187                let [(d_start, length)] = pointers.as_slice() else {
188                    return Err(crate::rules::ExtractionError::invalid(format!(
189                        "position {pos} must select exactly one pointer"
190                    )));
191                };
192                let ptr_idx = encode_pointer(n, *d_start, *length);
193                c_slots[c_pos] = k + 1 + ptr_idx;
194                c_pos += 1;
195                pos += length;
196            }
197
198            // Combine D-slots and C-slots
199            let mut config = d_slots;
200            config.extend(c_slots);
201            config
202        })
203    }
204}
205
206/// Encode a pointer (start, len) into the EMDC pointer index.
207/// Pointers enumerate: (0,1),(0,2),...,(0,n), (1,1),(1,2),...,(1,n-1), ...
208fn encode_pointer(n: usize, start: usize, len: usize) -> usize {
209    let mut idx = 0;
210    for s in 0..start {
211        idx += n - s;
212    }
213    idx + len - 1
214}
215
216#[reduction(
217    transform = upper_bound {
218        num_vars = "string_length * alphabet_size + 2 * string_length + string_length ^ 3",
219        num_constraints = "string_length + string_length * alphabet_size + string_length + string_length + 1 + string_length ^ 3 * string_length",
220    },
221    unavailable = {
222        num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform",
223    }
224)]
225impl ReduceTo<ILP<bool>> for MinimumExternalMacroDataCompression {
226    type Result = ReductionEMDCToILP;
227
228    fn reduce_to(&self) -> Result<Self::Result, crate::rules::ReductionError> {
229        let n = self.string_length();
230        let k = self.alphabet_size();
231        let h = self.pointer_cost();
232        let s = self.string();
233
234        // Handle empty string
235        if n == 0 {
236            let layout = VarLayout::new(0, k);
237            let target = ILP::new(0, vec![], vec![], ObjectiveSense::Minimize)
238                .map_err(Self::target_construction)?;
239            return Ok(ReductionEMDCToILP {
240                target,
241                layout,
242                source_string: vec![],
243                alphabet_size: k,
244            });
245        }
246
247        let layout = VarLayout::new(n, k);
248        let num_vars = layout.total_vars;
249        let mut constraints = Vec::new();
250
251        // 1. Dictionary one-hot: for each j, sum_c d[j][c] <= 1
252        for j in 0..n {
253            let terms: Vec<(usize, i64)> = (0..k).map(|c| (layout.d_var(j, c), 1)).collect();
254            constraints.push(LinearConstraint::le(terms, 1));
255        }
256
257        // 2. Dictionary linking: d[j][c] <= d_used[j] for all j, c
258        for j in 0..n {
259            for c in 0..k {
260                constraints.push(LinearConstraint::le(
261                    vec![(layout.d_var(j, c), 1), (layout.d_used_var(j), -1)],
262                    0,
263                ));
264            }
265        }
266
267        // 3. Dictionary contiguous: d_used[j+1] <= d_used[j] for j=0..n-2
268        for j in 0..n.saturating_sub(1) {
269            constraints.push(LinearConstraint::le(
270                vec![(layout.d_used_var(j + 1), 1), (layout.d_used_var(j), -1)],
271                0,
272            ));
273        }
274
275        // 4. Flow conservation on DAG: positions 0..n are nodes.
276        // A segment (i, l) contributes to outgoing flow at node i and incoming flow at node i+l.
277        // For segment (i, l):
278        //   - if l == 1: flow = lit[i] + sum_{d_start} ptr[i][1][d_start]
279        //   - if l >= 2: flow = sum_{d_start} ptr[i][l][d_start]
280        //
281        // Flow constraints:
282        // At node 0: sum of outgoing segments = 1
283        // At node i (1..n-1): sum of incoming = sum of outgoing
284        // At node n: sum of incoming = 1
285
286        // Helper: get all terms for "segment flow" at (i, l)
287        // Returns the variable indices with coefficient 1
288        let segment_terms = |i: usize, l: usize| -> Vec<(usize, i64)> {
289            let mut terms = Vec::new();
290            if l == 1 {
291                terms.push((layout.lit_var(i), 1));
292            }
293            for &var in &layout.ptr_vars_for_segment(i, l) {
294                terms.push((var, 1));
295            }
296            terms
297        };
298
299        // For each node, compute outgoing and incoming segment terms
300        for node in 0..=n {
301            let mut all_terms: Vec<(usize, i64)> = Vec::new();
302
303            if node == 0 {
304                // sum of outgoing(0, l) = 1
305                for l in 1..=n {
306                    all_terms.extend(segment_terms(0, l));
307                }
308                constraints.push(LinearConstraint::eq(all_terms, 1));
309            } else if node == n {
310                // sum of incoming(n) = 1
311                // incoming at node n: segments (j, l) where j + l = n
312                for j in 0..n {
313                    let l = n - j;
314                    all_terms.extend(segment_terms(j, l));
315                }
316                constraints.push(LinearConstraint::eq(all_terms, 1));
317            } else {
318                // node 1..n-1: incoming = outgoing
319                // incoming: segments (j, l) where j + l = node
320                let mut incoming = Vec::new();
321                for j in 0..node {
322                    let l = node - j;
323                    incoming.extend(segment_terms(j, l));
324                }
325                // outgoing: segments (node, l) for valid l
326                let mut outgoing = Vec::new();
327                for l in 1..=(n - node) {
328                    outgoing.extend(segment_terms(node, l));
329                }
330                // incoming - outgoing = 0
331                for (var, coef) in incoming {
332                    all_terms.push((var, coef));
333                }
334                for (var, coef) in outgoing {
335                    all_terms.push((var, -coef));
336                }
337                constraints.push(LinearConstraint::eq(all_terms, 0));
338            }
339        }
340
341        // 5. Pointer matching: ptr[i][l][d_start] <= d[d_start+offset][s[i+offset]]
342        // for all offset=0..l-1
343        for (idx, &(i, l, d_start)) in layout.ptr_triples.iter().enumerate() {
344            let ptr_idx = layout.ptr_offset + idx;
345            for offset in 0..l {
346                let symbol = s[i + offset];
347                // ptr[i][l][d_start] <= d[d_start + offset][symbol]
348                constraints.push(LinearConstraint::le(
349                    vec![(ptr_idx, 1), (layout.d_var(d_start + offset, symbol), -1)],
350                    0,
351                ));
352            }
353        }
354
355        // 6. Literal matching: lit[i] can only be active if position i exists
356        // (this is always true for i < n, so no constraint needed).
357        // But we do need: if lit[i] = 1, the literal is s[i], which is automatic
358        // in the extract_solution. No additional constraint needed because the
359        // objective already penalizes literals.
360
361        // Objective: minimize sum d_used[j] + sum lit[i] + h * sum ptr[i][l][d_start]
362        let mut objective: Vec<(usize, i64)> = Vec::new();
363        for j in 0..n {
364            objective.push((layout.d_used_var(j), 1));
365        }
366        for i in 0..n {
367            objective.push((layout.lit_var(i), 1));
368        }
369        for (idx, _) in layout.ptr_triples.iter().enumerate() {
370            objective.push((layout.ptr_offset + idx, h));
371        }
372
373        let target = ILP::new(num_vars, constraints, objective, ObjectiveSense::Minimize)
374            .map_err(Self::target_construction)?;
375
376        Ok(ReductionEMDCToILP {
377            target,
378            layout,
379            source_string: s.to_vec(),
380            alphabet_size: k,
381        })
382    }
383}
384
385#[cfg(feature = "example-db")]
386pub(crate) fn canonical_rule_example_specs() -> Vec<crate::example_db::specs::RuleExampleSpec> {
387    use crate::export::SolutionPair;
388
389    // s = "ab" (len 2), alphabet {a,b} (size 2), h=2
390    // Optimal: uncompressed, D="" C="ab", cost = 0+2+0 = 2
391    // Config: D-slots=[2,2], C-slots=[0,1]
392    // ILP target_config: all d and d_used = 0, lit[0]=1, lit[1]=1, all ptr = 0
393    vec![crate::example_db::specs::RuleExampleSpec {
394        id: "minimumexternalmacrodatacompression_to_ilp",
395        build: || {
396            let source = MinimumExternalMacroDataCompression::new(2, vec![0, 1], 2);
397            let reduction =
398                ReduceTo::<ILP<bool>>::reduce_to(&source).expect("reduction should succeed");
399            let layout = &reduction.layout;
400            let n = 2;
401            let k = 2;
402
403            // Build target config: all zeros, then set lit[0]=1, lit[1]=1
404            let mut target_config = vec![0_i64; layout.total_vars];
405            target_config[layout.lit_var(0)] = 1;
406            target_config[layout.lit_var(1)] = 1;
407
408            // Verify this is correct
409            let source_config = reduction.extract_solution(&target_config).unwrap();
410            debug_assert_eq!(source_config[..n], [k, k]); // D empty
411            debug_assert_eq!(source_config[n..], [0, 1]); // C = "ab"
412
413            crate::example_db::specs::rule_example_with_witness::<_, ILP<bool>>(
414                source,
415                SolutionPair {
416                    source_config: serde_json::to_value(source_config)
417                        .expect("solution serialization must succeed"),
418                    target_config: serde_json::to_value(target_config)
419                        .expect("solution serialization must succeed"),
420                },
421            )
422        },
423    }]
424}
425
426#[cfg(test)]
427#[path = "../unit_tests/rules/minimumexternalmacrodatacompression_ilp.rs"]
428mod tests;