problemreductions/models/misc/
preemptive_scheduling.rs1use crate::registry::{ConstructionError, CreateSpec, ProblemSchemaEntry};
9use crate::traits::Problem;
10use crate::types::Min;
11use serde::{Deserialize, Serialize};
12
13inventory::submit! {
14 ProblemSchemaEntry {
15 name: "PreemptiveScheduling",
16 display_name: "Preemptive Scheduling",
17 aliases: &[],
18 dimensions: &[],
19 category: crate::registry::ProblemCategory::Misc,
20 module_path: module_path!(),
21 description: "Minimize makespan for preemptive parallel-processor scheduling with precedence constraints",
22 fields: PreemptiveSchedulingCreateSpec::FIELDS,
23 }
24}
25
26#[derive(Debug, Clone, Serialize)]
62pub struct PreemptiveScheduling {
63 lengths: Vec<i64>,
65 num_processors: usize,
67 precedences: Vec<(usize, usize)>,
69}
70
71#[derive(Debug, Deserialize, crate::CreateSpec)]
72struct PreemptiveSchedulingCreateSpec {
73 lengths: Vec<i64>,
74 num_processors: usize,
75 precedences: Option<Vec<(usize, usize)>>,
76}
77
78impl TryFrom<PreemptiveSchedulingCreateSpec> for PreemptiveScheduling {
79 type Error = ConstructionError;
80
81 fn try_from(spec: PreemptiveSchedulingCreateSpec) -> Result<Self, Self::Error> {
82 let precedences = spec.precedences.unwrap_or_default();
83 Self::new(spec.lengths, spec.num_processors, precedences)
84 }
85}
86
87#[derive(Deserialize)]
88struct PreemptiveSchedulingSerde {
89 lengths: Vec<i64>,
90 num_processors: usize,
91 precedences: Vec<(usize, usize)>,
92}
93
94impl PreemptiveScheduling {
95 fn validate(
96 lengths: &[i64],
97 num_processors: usize,
98 precedences: &[(usize, usize)],
99 ) -> Result<(), ConstructionError> {
100 if lengths.iter().any(|&length| length <= 0) {
101 return Err(ConstructionError::Conversion(
102 "task lengths must be positive".into(),
103 ));
104 }
105 if num_processors == 0 {
106 return Err(ConstructionError::Conversion(
107 "num_processors must be positive".into(),
108 ));
109 }
110 let n = lengths.len();
111 let total_length = lengths
112 .iter()
113 .try_fold(0_i64, |total, &length| total.checked_add(length))
114 .ok_or_else(|| ConstructionError::IntegerOverflow("summing task lengths".into()))?;
115 let horizon = usize::try_from(total_length).map_err(|_| {
116 ConstructionError::IntegerOverflow("task horizon does not fit usize".into())
117 })?;
118 n.checked_mul(horizon).ok_or_else(|| {
119 ConstructionError::IntegerOverflow("configuration size does not fit usize".into())
120 })?;
121 for &(pred, succ) in precedences {
122 if pred >= n || succ >= n {
123 return Err(ConstructionError::Conversion(format!(
124 "precedence index out of range: ({pred}, {succ}) but num_tasks = {n}"
125 )));
126 }
127 }
128 Ok(())
129 }
130
131 pub fn new(
139 lengths: Vec<i64>,
140 num_processors: usize,
141 precedences: Vec<(usize, usize)>,
142 ) -> Result<Self, ConstructionError> {
143 Self::validate(&lengths, num_processors, &precedences)?;
144 Ok(Self {
145 lengths,
146 num_processors,
147 precedences,
148 })
149 }
150
151 pub fn num_tasks(&self) -> usize {
153 self.lengths.len()
154 }
155
156 pub fn num_processors(&self) -> usize {
158 self.num_processors
159 }
160
161 pub fn num_precedences(&self) -> usize {
163 self.precedences.len()
164 }
165
166 pub fn lengths(&self) -> &[i64] {
168 &self.lengths
169 }
170
171 pub fn precedences(&self) -> &[(usize, usize)] {
173 &self.precedences
174 }
175
176 pub fn max_schedule_magnitude_bits(&self) -> u64 {
178 crate::types::max_numeric_magnitude_bits([self.d_max(), self.num_processors()])
179 }
180
181 pub(crate) fn scheduling_windows(&self) -> (usize, Vec<std::ops::Range<usize>>) {
182 use std::cmp::Reverse;
183 use std::collections::BinaryHeap;
184 let n = self.num_tasks();
185 let lengths: Vec<_> = self
186 .lengths
187 .iter()
188 .map(|&p| usize::try_from(p).expect("validated positive task length fits the horizon"))
189 .collect();
190 let mut successors = vec![Vec::new(); n];
191 let mut indegree = vec![0; n];
192 for &(a, b) in &self.precedences {
193 successors[a].push(b);
194 indegree[b] += 1;
195 }
196 let mut pending = indegree.clone();
197 let mut order: Vec<_> = (0..n).filter(|&j| pending[j] == 0).collect();
198 let mut earliest = vec![0; n];
199 let mut cursor = 0;
200 while cursor < order.len() {
201 let j = order[cursor];
202 cursor += 1;
203 for &k in &successors[j] {
204 earliest[k] = earliest[k].max(earliest[j] + lengths[j]);
205 pending[k] -= 1;
206 if pending[k] == 0 {
207 order.push(k);
208 }
209 }
210 }
211 if order.len() != n {
212 return (0, vec![0..0; n]);
213 }
214 let mut tail = lengths.clone();
215 for &j in order.iter().rev() {
216 tail[j] += successors[j].iter().map(|&k| tail[k]).max().unwrap_or(0);
217 }
218 let mut ready: BinaryHeap<_> = (0..n)
219 .filter(|&j| indegree[j] == 0)
220 .map(|j| (tail[j], Reverse(j)))
221 .collect();
222 let mut running = BinaryHeap::new();
223 let mut time = 0;
224 loop {
225 while running.len() < self.num_processors {
226 let Some((_, Reverse(j))) = ready.pop() else {
227 break;
228 };
229 running.push(Reverse((time + lengths[j], j)));
230 }
231 let Some(&Reverse((finish, _))) = running.peek() else {
232 break;
233 };
234 time = finish;
235 while running.peek().is_some_and(|Reverse((end, _))| *end == time) {
236 let Reverse((_, j)) = running.pop().expect("finish event exists");
237 for &k in &successors[j] {
238 indegree[k] -= 1;
239 if indegree[k] == 0 {
240 ready.push((tail[k], Reverse(k)));
241 }
242 }
243 }
244 }
245 (
248 time,
249 (0..n)
250 .map(|j| earliest[j]..time - tail[j] + lengths[j])
251 .collect(),
252 )
253 }
254
255 pub fn d_max(&self) -> usize {
257 let total = self
258 .lengths
259 .iter()
260 .try_fold(0_i64, |total, &length| total.checked_add(length))
261 .expect("construction validates the task horizon");
262 usize::try_from(total).expect("validated task horizon fits usize")
263 }
264}
265
266impl TryFrom<PreemptiveSchedulingSerde> for PreemptiveScheduling {
267 type Error = ConstructionError;
268
269 fn try_from(value: PreemptiveSchedulingSerde) -> Result<Self, Self::Error> {
270 Self::new(value.lengths, value.num_processors, value.precedences)
271 }
272}
273
274impl<'de> Deserialize<'de> for PreemptiveScheduling {
275 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
276 where
277 D: serde::Deserializer<'de>,
278 {
279 let value = PreemptiveSchedulingSerde::deserialize(deserializer)?;
280 Self::try_from(value).map_err(serde::de::Error::custom)
281 }
282}
283
284impl Problem for PreemptiveScheduling {
285 const NAME: &'static str = "PreemptiveScheduling";
286 type Solution = Vec<Vec<bool>>;
287 type Value = Min<i64>;
288
289 crate::problem_parameters![
290 ("d_max", d_max),
291 ("max_schedule_magnitude_bits", max_schedule_magnitude_bits),
292 ("num_precedences", num_precedences),
293 ("num_processors", num_processors),
294 ("num_tasks", num_tasks),
295 ];
296
297 fn variant() -> Vec<(&'static str, &'static str)> {
298 crate::variant_params![]
299 }
300
301 fn evaluate(
302 &self,
303 solution: &Self::Solution,
304 ) -> Result<Min<i64>, crate::traits::EvaluationError> {
305 let n = self.num_tasks();
306 let d = self.d_max();
307 if solution.len() != n || solution.iter().any(|task| task.len() != d) {
308 return Err(crate::traits::EvaluationError::InvalidConfiguration(
309 "preemptive schedule dimensions do not match the instance".into(),
310 ));
311 }
312 Ok({
313 for (task, &length) in solution.iter().zip(&self.lengths) {
315 let active = task.iter().filter(|&&active| active).count();
316 if i64::try_from(active).expect("active slots fit the validated horizon") != length
317 {
318 return Ok(Min(None));
319 }
320 }
321
322 for u in 0..d {
324 let active_count = solution.iter().filter(|task| task[u]).count();
325 if active_count > self.num_processors {
326 return Ok(Min(None));
327 }
328 }
329
330 for &(pred, succ) in &self.precedences {
333 let last_pred = (0..d).rev().find(|&u| solution[pred][u]);
334 let first_succ = (0..d).find(|&u| solution[succ][u]);
335 if let (Some(lp), Some(fs)) = (last_pred, first_succ) {
336 if lp >= fs {
337 return Ok(Min(None));
338 }
339 }
340 }
341
342 let makespan = solution
344 .iter()
345 .filter_map(|task| (0..d).rev().find(|&u| task[u]))
346 .map(|last| last + 1)
347 .max()
348 .unwrap_or(0);
349
350 Min(Some(
351 i64::try_from(makespan).expect("makespan fits the validated horizon"),
352 ))
353 })
354 }
355}
356
357impl crate::solvers::BruteForceProblem for PreemptiveScheduling {
358 fn dimensions(&self) -> Vec<usize> {
359 let d = self.d_max();
360 vec![2; self.num_tasks() * d]
361 }
362}
363
364crate::declare_variants! {
365 default PreemptiveScheduling => "2^(num_tasks * num_tasks)" create PreemptiveSchedulingCreateSpec,
366}
367
368crate::register_brute_force! {
369 PreemptiveScheduling decode |problem: &PreemptiveScheduling, indices: Vec<usize>| if problem.d_max() == 0 { vec![Vec::new(); problem.num_tasks()] } else { indices.chunks(problem.d_max()).map(crate::config::config_to_bits).collect() },
370}
371
372#[cfg(feature = "example-db")]
373pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::ModelExampleSpec> {
374 let mut config = vec![vec![false; 9]; 5];
389 config[0][0] = true;
390 config[0][1] = true;
391 config[1][0] = true;
392 config[2][2] = true;
393 config[2][3] = true;
394 config[2][4] = true;
395 config[3][2] = true;
396 config[3][3] = true;
397 config[4][1] = true;
398 vec![crate::example_db::specs::ModelExampleSpec {
399 id: "preemptive_scheduling",
400 instance: Box::new(
401 PreemptiveScheduling::new(vec![2, 1, 3, 2, 1], 2, vec![(0, 2), (1, 3)]).unwrap(),
402 ),
403 optimal_config: serde_json::json!(config),
404 optimal_value: serde_json::json!(5),
405 }]
406}
407
408#[cfg(test)]
409#[path = "../../unit_tests/models/misc/preemptive_scheduling.rs"]
410mod tests;