Skip to main content

problemreductions/models/misc/
precedence_constrained_scheduling.rs

1//! Precedence Constrained Scheduling problem implementation.
2//!
3//! Given unit-length tasks with precedence constraints, m processors, and a
4//! deadline D, determine whether all tasks can be scheduled to meet D while
5//! respecting precedences. NP-complete via reduction from 3SAT (Ullman, 1975).
6
7use crate::registry::{FieldInfo, ProblemSchemaEntry};
8use crate::traits::Problem;
9use serde::{Deserialize, Serialize};
10
11inventory::submit! {
12    ProblemSchemaEntry {
13        name: "PrecedenceConstrainedScheduling",
14        display_name: "Precedence Constrained Scheduling",
15        aliases: &[],
16        dimensions: &[],
17        module_path: module_path!(),
18        description: "Schedule unit-length tasks on m processors by deadline D respecting precedence constraints",
19        fields: &[
20            FieldInfo { name: "num_tasks", type_name: "usize", description: "Number of tasks n = |T|" },
21            FieldInfo { name: "num_processors", type_name: "usize", description: "Number of processors m" },
22            FieldInfo { name: "deadline", type_name: "usize", description: "Global deadline D" },
23            FieldInfo { name: "precedences", type_name: "Vec<(usize, usize)>", description: "Precedence pairs (i, j) meaning task i must finish before task j starts" },
24        ],
25    }
26}
27
28/// The Precedence Constrained Scheduling problem.
29///
30/// Given `n` unit-length tasks with precedence constraints (a partial order),
31/// `m` processors, and a deadline `D`, determine whether there exists a schedule
32/// assigning each task to a time slot in `{0, ..., D-1}` such that:
33/// - At most `m` tasks are assigned to any single time slot
34/// - For each precedence `(i, j)`: task `j` starts after task `i` completes,
35///   i.e., `slot(j) >= slot(i) + 1`
36///
37/// # Representation
38///
39/// Each task has a variable in `{0, ..., D-1}` representing its assigned time slot.
40///
41/// # Example
42///
43/// ```
44/// use problemreductions::models::misc::PrecedenceConstrainedScheduling;
45/// use problemreductions::{Problem, Solver, BruteForce};
46///
47/// // 4 tasks, 2 processors, deadline 3, with t0 < t2 and t1 < t3
48/// let problem = PrecedenceConstrainedScheduling::new(4, 2, 3, vec![(0, 2), (1, 3)]);
49/// let solver = BruteForce::new();
50/// let solution = solver.find_witness(&problem);
51/// assert!(solution.is_some());
52/// ```
53#[derive(Debug, Clone, Serialize, Deserialize)]
54pub struct PrecedenceConstrainedScheduling {
55    num_tasks: usize,
56    num_processors: usize,
57    deadline: usize,
58    precedences: Vec<(usize, usize)>,
59}
60
61impl PrecedenceConstrainedScheduling {
62    /// Create a new Precedence Constrained Scheduling instance.
63    ///
64    /// # Panics
65    ///
66    /// Panics if `num_processors` or `deadline` is zero (when `num_tasks > 0`),
67    /// or if any precedence index is out of bounds (>= num_tasks).
68    pub fn new(
69        num_tasks: usize,
70        num_processors: usize,
71        deadline: usize,
72        precedences: Vec<(usize, usize)>,
73    ) -> Self {
74        if num_tasks > 0 {
75            assert!(
76                num_processors > 0,
77                "num_processors must be > 0 when there are tasks"
78            );
79            assert!(deadline > 0, "deadline must be > 0 when there are tasks");
80        }
81        for &(i, j) in &precedences {
82            assert!(
83                i < num_tasks && j < num_tasks,
84                "Precedence ({}, {}) out of bounds for {} tasks",
85                i,
86                j,
87                num_tasks
88            );
89        }
90        Self {
91            num_tasks,
92            num_processors,
93            deadline,
94            precedences,
95        }
96    }
97
98    /// Get the number of tasks.
99    pub fn num_tasks(&self) -> usize {
100        self.num_tasks
101    }
102
103    /// Get the number of processors.
104    pub fn num_processors(&self) -> usize {
105        self.num_processors
106    }
107
108    /// Get the deadline.
109    pub fn deadline(&self) -> usize {
110        self.deadline
111    }
112
113    /// Get the precedence constraints.
114    pub fn precedences(&self) -> &[(usize, usize)] {
115        &self.precedences
116    }
117}
118
119impl Problem for PrecedenceConstrainedScheduling {
120    const NAME: &'static str = "PrecedenceConstrainedScheduling";
121    type Value = crate::types::Or;
122
123    fn variant() -> Vec<(&'static str, &'static str)> {
124        crate::variant_params![]
125    }
126
127    fn dims(&self) -> Vec<usize> {
128        vec![self.deadline; self.num_tasks]
129    }
130
131    fn evaluate(&self, config: &[usize]) -> crate::types::Or {
132        crate::types::Or({
133            if config.len() != self.num_tasks {
134                return crate::types::Or(false);
135            }
136            // Check all values are valid time slots
137            if config.iter().any(|&v| v >= self.deadline) {
138                return crate::types::Or(false);
139            }
140            // Check processor capacity: at most num_processors tasks per time slot
141            let mut slot_count = vec![0usize; self.deadline];
142            for &slot in config {
143                slot_count[slot] += 1;
144                if slot_count[slot] > self.num_processors {
145                    return crate::types::Or(false);
146                }
147            }
148            // Check precedence constraints: for (i, j), slot[j] >= slot[i] + 1
149            for &(i, j) in &self.precedences {
150                if config[j] < config[i] + 1 {
151                    return crate::types::Or(false);
152                }
153            }
154            true
155        })
156    }
157}
158
159crate::declare_variants! {
160    default PrecedenceConstrainedScheduling => "2^num_tasks",
161}
162
163#[cfg(feature = "example-db")]
164pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::ModelExampleSpec> {
165    vec![crate::example_db::specs::ModelExampleSpec {
166        id: "precedence_constrained_scheduling",
167        // Issue #501 example: 8 tasks, 3 processors, deadline 4
168        instance: Box::new(PrecedenceConstrainedScheduling::new(
169            8,
170            3,
171            4,
172            vec![
173                (0, 2),
174                (0, 3),
175                (1, 3),
176                (1, 4),
177                (2, 5),
178                (3, 6),
179                (4, 6),
180                (5, 7),
181                (6, 7),
182            ],
183        )),
184        // Valid schedule: slot 0: {t0,t1}, slot 1: {t2,t3,t4}, slot 2: {t5,t6}, slot 3: {t7}
185        optimal_config: vec![0, 0, 1, 1, 1, 2, 2, 3],
186        optimal_value: serde_json::json!(true),
187    }]
188}
189
190#[cfg(test)]
191#[path = "../../unit_tests/models/misc/precedence_constrained_scheduling.rs"]
192mod tests;