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// P=sum_{l=1}^n(n-l+1)^2=n(n+1)(2n+1)/6 pointers;
217// M=sum_{l=1}^n l(n-l+1)^2=n(n+1)^2(n+2)/12 matching rows.
218// Variables=nk+2n+P; rows=3n+nk+M (also zero at n=0).
219// Terms=3nk+4n-2+2P+2M for n>0; dropping -2 covers the empty return.
220#[reduction(transform = {
221    exact {
222        max_constraint_magnitude_bits = "1",
223        num_vars = "string_length * alphabet_size + 2 * string_length + string_length * (string_length + 1) * (2 * string_length + 1) / 6",
224        num_constraints = "3 * string_length + string_length * alphabet_size + string_length * (string_length + 1)^2 * (string_length + 2) / 12",
225    },
226    upper_bound {
227        num_nonzeros = "3 * string_length * alphabet_size + 4 * string_length + string_length * (string_length + 1) * (2 * string_length + 1) / 3 + string_length * (string_length + 1)^2 * (string_length + 2) / 6",
228    },
229})]
230impl ReduceTo<ILP<bool>> for MinimumExternalMacroDataCompression {
231    type Result = ReductionEMDCToILP;
232
233    fn reduce_to(&self) -> Result<Self::Result, crate::rules::ReductionError> {
234        let n = self.string_length();
235        let k = self.alphabet_size();
236        let h = self.pointer_cost();
237        let s = self.string();
238
239        // Handle empty string
240        if n == 0 {
241            let layout = VarLayout::new(0, k);
242            let target = ILP::new(0, vec![], vec![], ObjectiveSense::Minimize)
243                .map_err(Self::target_construction)?;
244            return Ok(ReductionEMDCToILP {
245                target,
246                layout,
247                source_string: vec![],
248                alphabet_size: k,
249            });
250        }
251
252        let layout = VarLayout::new(n, k);
253        let num_vars = layout.total_vars;
254        let mut constraints = Vec::new();
255
256        // 1. Dictionary one-hot: for each j, sum_c d[j][c] <= 1
257        for j in 0..n {
258            let terms: Vec<(usize, i64)> = (0..k).map(|c| (layout.d_var(j, c), 1)).collect();
259            constraints.push(LinearConstraint::le(terms, 1));
260        }
261
262        // 2. Dictionary linking: d[j][c] <= d_used[j] for all j, c
263        for j in 0..n {
264            for c in 0..k {
265                constraints.push(LinearConstraint::le(
266                    vec![(layout.d_var(j, c), 1), (layout.d_used_var(j), -1)],
267                    0,
268                ));
269            }
270        }
271
272        // 3. Dictionary contiguous: d_used[j+1] <= d_used[j] for j=0..n-2
273        for j in 0..n.saturating_sub(1) {
274            constraints.push(LinearConstraint::le(
275                vec![(layout.d_used_var(j + 1), 1), (layout.d_used_var(j), -1)],
276                0,
277            ));
278        }
279
280        // 4. Flow conservation on DAG: positions 0..n are nodes.
281        // A segment (i, l) contributes to outgoing flow at node i and incoming flow at node i+l.
282        // For segment (i, l):
283        //   - if l == 1: flow = lit[i] + sum_{d_start} ptr[i][1][d_start]
284        //   - if l >= 2: flow = sum_{d_start} ptr[i][l][d_start]
285        //
286        // Flow constraints:
287        // At node 0: sum of outgoing segments = 1
288        // At node i (1..n-1): sum of incoming = sum of outgoing
289        // At node n: sum of incoming = 1
290
291        // Helper: get all terms for "segment flow" at (i, l)
292        // Returns the variable indices with coefficient 1
293        let segment_terms = |i: usize, l: usize| -> Vec<(usize, i64)> {
294            let mut terms = Vec::new();
295            if l == 1 {
296                terms.push((layout.lit_var(i), 1));
297            }
298            for &var in &layout.ptr_vars_for_segment(i, l) {
299                terms.push((var, 1));
300            }
301            terms
302        };
303
304        // For each node, compute outgoing and incoming segment terms
305        for node in 0..=n {
306            let mut all_terms: Vec<(usize, i64)> = Vec::new();
307
308            if node == 0 {
309                // sum of outgoing(0, l) = 1
310                for l in 1..=n {
311                    all_terms.extend(segment_terms(0, l));
312                }
313                constraints.push(LinearConstraint::eq(all_terms, 1));
314            } else if node == n {
315                // sum of incoming(n) = 1
316                // incoming at node n: segments (j, l) where j + l = n
317                for j in 0..n {
318                    let l = n - j;
319                    all_terms.extend(segment_terms(j, l));
320                }
321                constraints.push(LinearConstraint::eq(all_terms, 1));
322            } else {
323                // node 1..n-1: incoming = outgoing
324                // incoming: segments (j, l) where j + l = node
325                let mut incoming = Vec::new();
326                for j in 0..node {
327                    let l = node - j;
328                    incoming.extend(segment_terms(j, l));
329                }
330                // outgoing: segments (node, l) for valid l
331                let mut outgoing = Vec::new();
332                for l in 1..=(n - node) {
333                    outgoing.extend(segment_terms(node, l));
334                }
335                // incoming - outgoing = 0
336                for (var, coef) in incoming {
337                    all_terms.push((var, coef));
338                }
339                for (var, coef) in outgoing {
340                    all_terms.push((var, -coef));
341                }
342                constraints.push(LinearConstraint::eq(all_terms, 0));
343            }
344        }
345
346        // 5. Pointer matching: ptr[i][l][d_start] <= d[d_start+offset][s[i+offset]]
347        // for all offset=0..l-1
348        for (idx, &(i, l, d_start)) in layout.ptr_triples.iter().enumerate() {
349            let ptr_idx = layout.ptr_offset + idx;
350            for offset in 0..l {
351                let symbol = s[i + offset];
352                // ptr[i][l][d_start] <= d[d_start + offset][symbol]
353                constraints.push(LinearConstraint::le(
354                    vec![(ptr_idx, 1), (layout.d_var(d_start + offset, symbol), -1)],
355                    0,
356                ));
357            }
358        }
359
360        // 6. Literal matching: lit[i] can only be active if position i exists
361        // (this is always true for i < n, so no constraint needed).
362        // But we do need: if lit[i] = 1, the literal is s[i], which is automatic
363        // in the extract_solution. No additional constraint needed because the
364        // objective already penalizes literals.
365
366        // Objective: minimize sum d_used[j] + sum lit[i] + h * sum ptr[i][l][d_start]
367        let mut objective: Vec<(usize, i64)> = Vec::new();
368        for j in 0..n {
369            objective.push((layout.d_used_var(j), 1));
370        }
371        for i in 0..n {
372            objective.push((layout.lit_var(i), 1));
373        }
374        for (idx, _) in layout.ptr_triples.iter().enumerate() {
375            objective.push((layout.ptr_offset + idx, h));
376        }
377
378        let target = ILP::new(num_vars, constraints, objective, ObjectiveSense::Minimize)
379            .map_err(Self::target_construction)?;
380
381        Ok(ReductionEMDCToILP {
382            target,
383            layout,
384            source_string: s.to_vec(),
385            alphabet_size: k,
386        })
387    }
388}
389
390#[cfg(feature = "example-db")]
391pub(crate) fn canonical_rule_example_specs() -> Vec<crate::example_db::specs::RuleExampleSpec> {
392    use crate::export::SolutionPair;
393
394    // s = "ab" (len 2), alphabet {a,b} (size 2), h=2
395    // Optimal: uncompressed, D="" C="ab", cost = 0+2+0 = 2
396    // Config: D-slots=[2,2], C-slots=[0,1]
397    // ILP target_config: all d and d_used = 0, lit[0]=1, lit[1]=1, all ptr = 0
398    vec![crate::example_db::specs::RuleExampleSpec {
399        id: "minimumexternalmacrodatacompression_to_ilp",
400        build: || {
401            let source = MinimumExternalMacroDataCompression::new(2, vec![0, 1], 2);
402            let reduction =
403                ReduceTo::<ILP<bool>>::reduce_to(&source).expect("reduction should succeed");
404            let layout = &reduction.layout;
405            let n = 2;
406            let k = 2;
407
408            // Build target config: all zeros, then set lit[0]=1, lit[1]=1
409            let mut target_config = vec![0_i64; layout.total_vars];
410            target_config[layout.lit_var(0)] = 1;
411            target_config[layout.lit_var(1)] = 1;
412
413            // Verify this is correct
414            let source_config = reduction.extract_solution(&target_config).unwrap();
415            debug_assert_eq!(source_config[..n], [k, k]); // D empty
416            debug_assert_eq!(source_config[n..], [0, 1]); // C = "ab"
417
418            crate::example_db::specs::rule_example_with_witness::<_, ILP<bool>>(
419                source,
420                SolutionPair {
421                    source_config: serde_json::to_value(source_config)
422                        .expect("solution serialization must succeed"),
423                    target_config: serde_json::to_value(target_config)
424                        .expect("solution serialization must succeed"),
425                },
426            )
427        },
428    }]
429}
430
431#[cfg(test)]
432#[path = "../unit_tests/rules/minimumexternalmacrodatacompression_ilp.rs"]
433mod tests;