Skip to main content

problemreductions/rules/
sequencingwithdeadlinesandsetuptimes_ilp.rs

1//! Reduction from SequencingWithDeadlinesAndSetUpTimes to `ILP<bool>`.
2//!
3//! Position-assignment ILP with compiler-switch detection.
4//!
5//! Variables:
6//! - `x_{j,p}` binary: task j occupies position p  (n*n variables)
7//! - `sw_p`    binary: a compiler switch occurs before position p (n-1 variables, p >= 1)
8//! - `a_{j,p}` binary: x_{j,p} = 1 AND sw_p = 1   (n*(n-1) variables, p >= 1)
9//!
10//! The completion time of task j at position p equals the sum of all task
11//! lengths up to and including position p, plus the setup times for switches
12//! at each position 1..=p. Using the `a_{j,p}` linearisation, the setup
13//! contribution at position p is `sum_j s[k(j)] * a_{j,p}`.
14//!
15//! Deadline enforcement uses the standard big-M trick: for each (j, p),
16//! if `x_{j,p}=1` then the completion time at p must not exceed `d[j]`.
17
18use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP};
19use crate::models::misc::SequencingWithDeadlinesAndSetUpTimes;
20use crate::reduction;
21use crate::rules::ilp_helpers::one_hot_decode;
22use crate::rules::traits::{ReduceTo, ReductionResult};
23
24/// Result of reducing SequencingWithDeadlinesAndSetUpTimes to `ILP<bool>`.
25#[derive(Debug, Clone)]
26pub struct ReductionSWDSTToILP {
27    target: ILP<bool>,
28    num_tasks: usize,
29}
30
31impl ReductionResult for ReductionSWDSTToILP {
32    type Source = SequencingWithDeadlinesAndSetUpTimes;
33    type Target = ILP<bool>;
34
35    fn target_problem(&self) -> &ILP<bool> {
36        &self.target
37    }
38
39    fn extract_solution(
40        &self,
41        target_solution: &<Self::Target as crate::traits::Problem>::Solution,
42    ) -> crate::rules::ExtractionResult<<Self::Source as crate::traits::Problem>::Solution> {
43        crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?;
44
45        Ok({
46            let n = self.num_tasks;
47            // x_{j,p} occupies the first n*n variables: decode the permutation.
48            one_hot_decode(target_solution, n, n, 0)?
49        })
50    }
51}
52
53#[reduction(transform = upper_bound {
54    num_vars = "2 * num_tasks^2 + num_tasks",
55    num_constraints = "2 * num_tasks + num_tasks^2 * (num_tasks - 1) + 3 * num_tasks * (num_tasks - 1) + num_tasks * num_tasks",
56},
57    unavailable = {
58        num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform",
59    }
60)]
61impl ReduceTo<ILP<bool>> for SequencingWithDeadlinesAndSetUpTimes {
62    type Result = ReductionSWDSTToILP;
63
64    fn reduce_to(&self) -> Result<Self::Result, crate::rules::ReductionError> {
65        let n = self.num_tasks();
66
67        // Handle empty case.
68        if n == 0 {
69            return Ok(ReductionSWDSTToILP {
70                target: ILP::new(0, vec![], vec![], ObjectiveSense::Minimize)
71                    .map_err(Self::target_construction)?,
72                num_tasks: 0,
73            });
74        }
75
76        // Variable layout:
77        //   x_{j,p}   = j*n + p          for j,p in 0..n        → indices 0..n*n
78        //   sw_p       = n*n + (p-1)      for p in 1..n          → indices n*n .. n*n+(n-1)
79        //   a_{j,p}    = n*n+(n-1)+j*(n-1)+(p-1)  for j in 0..n, p in 1..n
80        //                                           → indices n*n+(n-1) .. n*n+(n-1)+n*(n-1)
81        let num_x = n * n;
82        let sw_offset = num_x;
83        let a_offset = sw_offset + (n - 1);
84        let num_vars = a_offset + n * (n - 1);
85
86        let x_var = |j: usize, p: usize| -> usize { j * n + p };
87        let sw_var = |p: usize| -> usize { sw_offset + (p - 1) }; // p >= 1
88        let a_var = |j: usize, p: usize| -> usize { a_offset + j * (n - 1) + (p - 1) }; // p >= 1
89
90        let lengths = self.lengths();
91        let deadlines = self.deadlines();
92        let compilers = self.compilers();
93        let setup_times = self.setup_times();
94
95        // Big-M: total processing time + worst-case total setup overhead.
96        let total_length = lengths.iter().try_fold(0_i64, |total, &length| {
97            total.checked_add(length).ok_or_else(|| {
98                crate::rules::ReductionError::integer_overflow::<
99                    SequencingWithDeadlinesAndSetUpTimes,
100                    ILP<bool>,
101                >("summing task lengths")
102            })
103        })?;
104        let max_setup: i64 = setup_times.iter().copied().max().unwrap_or(0);
105        let transition_count = Self::exact_i64(
106            n - 1,
107            "converting the number of compiler transitions to i64",
108        )?;
109        let big_m = max_setup
110            .checked_mul(transition_count)
111            .and_then(|setup_total| total_length.checked_add(setup_total))
112            .ok_or_else(|| {
113                crate::rules::ReductionError::integer_overflow::<
114                    SequencingWithDeadlinesAndSetUpTimes,
115                    ILP<bool>,
116                >("computing the scheduling big-M bound")
117            })?;
118        let mut constraints = Vec::new();
119
120        // 1. Each task assigned to exactly one position: sum_p x_{j,p} = 1 for all j.
121        for j in 0..n {
122            let terms: Vec<(usize, i64)> = (0..n).map(|p| (x_var(j, p), 1)).collect();
123            constraints.push(LinearConstraint::eq(terms, 1));
124        }
125
126        // 2. Each position has exactly one task: sum_j x_{j,p} = 1 for all p.
127        for p in 0..n {
128            let terms: Vec<(usize, i64)> = (0..n).map(|j| (x_var(j, p), 1)).collect();
129            constraints.push(LinearConstraint::eq(terms, 1));
130        }
131
132        // For each position p >= 1:
133        for p in 1..n {
134            // 3. Switch detection: sw_p >= x_{j,p} + x_{j',p-1} - 1
135            //    whenever k(j) != k(j').
136            //    This forces sw_p = 1 whenever the tasks at p-1 and p differ.
137            for j in 0..n {
138                for j_prev in 0..n {
139                    if compilers[j] != compilers[j_prev] {
140                        // sw_p - x_{j,p} - x_{j',p-1} >= -1
141                        // i.e., x_{j,p} + x_{j',p-1} - sw_p <= 1
142                        constraints.push(LinearConstraint::le(
143                            vec![(x_var(j, p), 1), (x_var(j_prev, p - 1), 1), (sw_var(p), -1)],
144                            1,
145                        ));
146                    }
147                }
148            }
149
150            // 4. Linearisation of a_{j,p} = x_{j,p} * sw_p for each j:
151            //    a_{j,p} <= x_{j,p}
152            //    a_{j,p} <= sw_p
153            //    a_{j,p} >= x_{j,p} + sw_p - 1
154            for j in 0..n {
155                // a_{j,p} <= x_{j,p}
156                constraints.push(LinearConstraint::le(
157                    vec![(a_var(j, p), 1), (x_var(j, p), -1)],
158                    0,
159                ));
160                // a_{j,p} <= sw_p
161                constraints.push(LinearConstraint::le(
162                    vec![(a_var(j, p), 1), (sw_var(p), -1)],
163                    0,
164                ));
165                // a_{j,p} >= x_{j,p} + sw_p - 1
166                // i.e. x_{j,p} + sw_p - a_{j,p} <= 1
167                constraints.push(LinearConstraint::le(
168                    vec![(x_var(j, p), 1), (sw_var(p), 1), (a_var(j, p), -1)],
169                    1,
170                ));
171            }
172        }
173
174        // 5. Deadline constraints: for each (j, p), if x_{j,p}=1, then
175        //    the completion time at position p must be <= d[j].
176        //
177        //    Completion time at position p =
178        //      sum_{p'<=p} sum_{j''} l_{j''} * x_{j'',p'}
179        //      + sum_{p'=1..=p} sum_{j''} s[k(j'')] * a_{j'',p'}
180        //
181        //    Big-M form (only active when x_{j,p}=1):
182        //    M * x_{j,p}
183        //      + sum_{p'<p} sum_{j''} l_{j''} * x_{j'',p'}
184        //      + sum_{p'=1..=p} sum_{j''} s[k(j'')] * a_{j'',p'}
185        //      - M * x_{j,p}   (cancels the activation term)
186        //      <= d[j] - l[j] + M
187        //
188        //    Simplifying (M * x_{j,p} - M * x_{j,p} vanishes):
189        //      sum_{p'<p} sum_{j''} l_{j''} * x_{j'',p'}
190        //      + sum_{p'=1..=p} sum_{j''} s[k(j'')] * a_{j'',p'}
191        //      + M * x_{j,p}
192        //      - M
193        //      <= d[j] - l[j]
194        //
195        //    i.e.:
196        //      M * x_{j,p}
197        //      + sum_{p'<p} sum_{j''} l_{j''} * x_{j'',p'}
198        //      + sum_{p'=1..=p} sum_{j''} s[k(j'')] * a_{j'',p'}
199        //      <= d[j] - l[j] + M
200        for j in 0..n {
201            for p in 0..n {
202                let mut terms: Vec<(usize, i64)> = Vec::new();
203                // Big-M activation term
204                terms.push((x_var(j, p), big_m));
205                // Processing time for positions 0..p (not including p itself)
206                for pp in 0..p {
207                    for (jj, &length) in lengths.iter().enumerate() {
208                        terms.push((x_var(jj, pp), length));
209                    }
210                }
211                // Setup time for positions 1..=p
212                for pp in 1..=p {
213                    for jj in 0..n {
214                        let s = setup_times[compilers[jj]];
215                        if s > 0 {
216                            terms.push((a_var(jj, pp), s));
217                        }
218                    }
219                }
220                let rhs = deadlines[j]
221                    .checked_sub(lengths[j])
222                    .and_then(|value| value.checked_add(big_m))
223                    .ok_or_else(|| {
224                        crate::rules::ReductionError::integer_overflow::<
225                            SequencingWithDeadlinesAndSetUpTimes,
226                            ILP<bool>,
227                        >("computing a deadline constraint bound")
228                    })?;
229                constraints.push(LinearConstraint::le(terms, rhs));
230            }
231        }
232
233        Ok(ReductionSWDSTToILP {
234            target: ILP::new(num_vars, constraints, vec![], ObjectiveSense::Minimize)
235                .map_err(Self::target_construction)?,
236            num_tasks: n,
237        })
238    }
239}
240
241#[cfg(feature = "example-db")]
242pub(crate) fn canonical_rule_example_specs() -> Vec<crate::example_db::specs::RuleExampleSpec> {
243    vec![crate::example_db::specs::RuleExampleSpec {
244        id: "sequencingwithdeadlinesandsetuptimes_to_ilp",
245        build: || {
246            let source = SequencingWithDeadlinesAndSetUpTimes::new(
247                vec![2, 3, 1, 2, 2],
248                vec![4, 11, 3, 16, 7],
249                vec![0, 1, 0, 1, 0],
250                vec![1, 2],
251            );
252            crate::example_db::specs::rule_example_via_ilp::<_, bool>(source)
253        },
254    }]
255}
256
257#[cfg(test)]
258#[path = "../unit_tests/rules/sequencingwithdeadlinesandsetuptimes_ilp.rs"]
259mod tests;