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;