Skip to main content

problemreductions/models/misc/
preemptive_scheduling.rs

1//! Preemptive Scheduling problem implementation.
2//!
3//! A classical NP-hard scheduling problem (Garey & Johnson A5 SS6) where
4//! variable-length tasks may be split across non-contiguous time slots on
5//! `m` identical processors, subject to precedence constraints.
6//! The goal is to minimize the makespan (latest completion time).
7
8use 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/// The Preemptive Scheduling problem.
27///
28/// Given `n` tasks with processing lengths `l(0), ..., l(n-1)`, `m` identical
29/// processors, and a set of precedence constraints, find a preemptive schedule
30/// that minimizes the makespan.
31///
32/// Tasks may be interrupted and resumed at later time slots (preemption).
33/// A configuration is a binary vector of length `n × D_max` where
34/// `D_max = sum of all lengths` is the worst-case makespan.
35///
36/// `solution[t][u] = true` means task `t` is processed at time slot `u`.
37///
38/// A valid schedule satisfies:
39/// - Each task `t` is active in exactly `l(t)` time slots.
40/// - At most `m` tasks are active at any time slot.
41/// - For each precedence `(pred, succ)`, the last active slot of `pred` is
42///   strictly less than the first active slot of `succ`.
43///
44/// The makespan is `max_t (last active slot of t + 1)`.
45///
46/// # Example
47///
48/// ```
49/// use problemreductions::models::misc::PreemptiveScheduling;
50/// use problemreductions::Problem;
51///
52/// let problem = PreemptiveScheduling::new(vec![2, 1], 2, vec![]).unwrap();
53/// // D_max = 3, so the solution is a 2 × 3 task-by-time matrix.
54/// // task 0 active at slots 0,1; task 1 active at slot 0
55/// let solution = vec![
56///     vec![true, true, false],
57///     vec![true, false, false],
58/// ];
59/// assert_eq!(problem.evaluate(&solution).unwrap(), problemreductions::types::Min(Some(2)));
60/// ```
61#[derive(Debug, Clone, Serialize)]
62pub struct PreemptiveScheduling {
63    /// Processing length for each task.
64    lengths: Vec<i64>,
65    /// Number of identical processors.
66    num_processors: usize,
67    /// Precedence constraints: (pred, succ) means pred must finish before succ starts.
68    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    /// Create a new Preemptive Scheduling instance.
132    ///
133    /// # Arguments
134    /// * `lengths` - Processing length `l(t)` for each task (must be positive)
135    /// * `num_processors` - Number of identical processors `m` (must be positive)
136    /// * `precedences` - Pairs `(pred, succ)`: task `pred` must finish before task `succ` starts
137    ///
138    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    /// Get the number of tasks.
152    pub fn num_tasks(&self) -> usize {
153        self.lengths.len()
154    }
155
156    /// Get the number of processors.
157    pub fn num_processors(&self) -> usize {
158        self.num_processors
159    }
160
161    /// Get the number of precedence constraints.
162    pub fn num_precedences(&self) -> usize {
163        self.precedences.len()
164    }
165
166    /// Get the processing lengths.
167    pub fn lengths(&self) -> &[i64] {
168        &self.lengths
169    }
170
171    /// Get the precedence constraints.
172    pub fn precedences(&self) -> &[(usize, usize)] {
173        &self.precedences
174    }
175
176    /// Bit bound for work, capacity and endpoint coefficients.
177    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        // Every addition above lies on a path or a feasible serial schedule,
246        // hence fits the constructor-validated sum of processing lengths.
247        (
248            time,
249            (0..n)
250                .map(|j| earliest[j]..time - tail[j] + lengths[j])
251                .collect(),
252        )
253    }
254
255    /// Compute `D_max = sum of all task lengths` (worst-case makespan).
256    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            // Check each task t is active in exactly l(t) slots
314            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            // Check processor capacity at each time slot
323            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            // Check precedence constraints:
331            // last active slot of pred < first active slot of succ
332            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            // Compute makespan: max over all t of (last active slot + 1)
343            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    // 5 tasks, lengths [2,1,3,2,1], 2 processors, precedences [(0,2),(1,3)]
375    // D_max = 2+1+3+2+1 = 9
376    // Optimal schedule (makespan 5):
377    //   t0: slots 0,1         → t0*9+0=1, t0*9+1=1
378    //   t1: slot 0            → t1*9+0=1
379    //   t2: slots 2,3,4       → t2*9+2=1, t2*9+3=1, t2*9+4=1
380    //   t3: slots 2,3         → t3*9+2=1, t3*9+3=1
381    //   t4: slot 1            → t4*9+1=1
382    // config indices (length 45):
383    //   t0 (0..9):  [1,1,0,0,0,0,0,0,0]
384    //   t1 (9..18): [1,0,0,0,0,0,0,0,0]
385    //   t2 (18..27):[0,0,1,1,1,0,0,0,0]
386    //   t3 (27..36):[0,0,1,1,0,0,0,0,0]
387    //   t4 (36..45):[0,1,0,0,0,0,0,0,0]
388    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;