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    /// Compute `D_max = sum of all task lengths` (worst-case makespan).
177    pub fn d_max(&self) -> usize {
178        let total = self
179            .lengths
180            .iter()
181            .try_fold(0_i64, |total, &length| total.checked_add(length))
182            .expect("construction validates the task horizon");
183        usize::try_from(total).expect("validated task horizon fits usize")
184    }
185}
186
187impl TryFrom<PreemptiveSchedulingSerde> for PreemptiveScheduling {
188    type Error = ConstructionError;
189
190    fn try_from(value: PreemptiveSchedulingSerde) -> Result<Self, Self::Error> {
191        Self::new(value.lengths, value.num_processors, value.precedences)
192    }
193}
194
195impl<'de> Deserialize<'de> for PreemptiveScheduling {
196    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
197    where
198        D: serde::Deserializer<'de>,
199    {
200        let value = PreemptiveSchedulingSerde::deserialize(deserializer)?;
201        Self::try_from(value).map_err(serde::de::Error::custom)
202    }
203}
204
205impl Problem for PreemptiveScheduling {
206    const NAME: &'static str = "PreemptiveScheduling";
207    type Solution = Vec<Vec<bool>>;
208    type Value = Min<i64>;
209
210    crate::problem_parameters![
211        ("d_max", d_max),
212        ("num_precedences", num_precedences),
213        ("num_processors", num_processors),
214        ("num_tasks", num_tasks),
215    ];
216
217    fn variant() -> Vec<(&'static str, &'static str)> {
218        crate::variant_params![]
219    }
220
221    fn evaluate(
222        &self,
223        solution: &Self::Solution,
224    ) -> Result<Min<i64>, crate::traits::EvaluationError> {
225        let n = self.num_tasks();
226        let d = self.d_max();
227        if solution.len() != n || solution.iter().any(|task| task.len() != d) {
228            return Err(crate::traits::EvaluationError::InvalidConfiguration(
229                "preemptive schedule dimensions do not match the instance".into(),
230            ));
231        }
232        Ok({
233            // Check each task t is active in exactly l(t) slots
234            for (task, &length) in solution.iter().zip(&self.lengths) {
235                let active = task.iter().filter(|&&active| active).count();
236                if i64::try_from(active).expect("active slots fit the validated horizon") != length
237                {
238                    return Ok(Min(None));
239                }
240            }
241
242            // Check processor capacity at each time slot
243            for u in 0..d {
244                let active_count = solution.iter().filter(|task| task[u]).count();
245                if active_count > self.num_processors {
246                    return Ok(Min(None));
247                }
248            }
249
250            // Check precedence constraints:
251            // last active slot of pred < first active slot of succ
252            for &(pred, succ) in &self.precedences {
253                let last_pred = (0..d).rev().find(|&u| solution[pred][u]);
254                let first_succ = (0..d).find(|&u| solution[succ][u]);
255                if let (Some(lp), Some(fs)) = (last_pred, first_succ) {
256                    if lp >= fs {
257                        return Ok(Min(None));
258                    }
259                }
260            }
261
262            // Compute makespan: max over all t of (last active slot + 1)
263            let makespan = solution
264                .iter()
265                .filter_map(|task| (0..d).rev().find(|&u| task[u]))
266                .map(|last| last + 1)
267                .max()
268                .unwrap_or(0);
269
270            Min(Some(
271                i64::try_from(makespan).expect("makespan fits the validated horizon"),
272            ))
273        })
274    }
275}
276
277impl crate::solvers::BruteForceProblem for PreemptiveScheduling {
278    fn dimensions(&self) -> Vec<usize> {
279        let d = self.d_max();
280        vec![2; self.num_tasks() * d]
281    }
282}
283
284crate::declare_variants! {
285    default PreemptiveScheduling => "2^(num_tasks * num_tasks)" create PreemptiveSchedulingCreateSpec,
286}
287
288crate::register_brute_force! {
289    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() },
290}
291
292#[cfg(feature = "example-db")]
293pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::ModelExampleSpec> {
294    // 5 tasks, lengths [2,1,3,2,1], 2 processors, precedences [(0,2),(1,3)]
295    // D_max = 2+1+3+2+1 = 9
296    // Optimal schedule (makespan 5):
297    //   t0: slots 0,1         → t0*9+0=1, t0*9+1=1
298    //   t1: slot 0            → t1*9+0=1
299    //   t2: slots 2,3,4       → t2*9+2=1, t2*9+3=1, t2*9+4=1
300    //   t3: slots 2,3         → t3*9+2=1, t3*9+3=1
301    //   t4: slot 1            → t4*9+1=1
302    // config indices (length 45):
303    //   t0 (0..9):  [1,1,0,0,0,0,0,0,0]
304    //   t1 (9..18): [1,0,0,0,0,0,0,0,0]
305    //   t2 (18..27):[0,0,1,1,1,0,0,0,0]
306    //   t3 (27..36):[0,0,1,1,0,0,0,0,0]
307    //   t4 (36..45):[0,1,0,0,0,0,0,0,0]
308    let mut config = vec![vec![false; 9]; 5];
309    config[0][0] = true;
310    config[0][1] = true;
311    config[1][0] = true;
312    config[2][2] = true;
313    config[2][3] = true;
314    config[2][4] = true;
315    config[3][2] = true;
316    config[3][3] = true;
317    config[4][1] = true;
318    vec![crate::example_db::specs::ModelExampleSpec {
319        id: "preemptive_scheduling",
320        instance: Box::new(
321            PreemptiveScheduling::new(vec![2, 1, 3, 2, 1], 2, vec![(0, 2), (1, 3)]).unwrap(),
322        ),
323        optimal_config: serde_json::json!(config),
324        optimal_value: serde_json::json!(5),
325    }]
326}
327
328#[cfg(test)]
329#[path = "../../unit_tests/models/misc/preemptive_scheduling.rs"]
330mod tests;