Skip to main content

problemreductions/models/misc/
multiprocessor_scheduling.rs

1//! Multiprocessor Scheduling problem implementation.
2//!
3//! The Multiprocessor Scheduling problem asks whether a set of tasks
4//! can be assigned to identical processors such that no processor's
5//! total load exceeds a given deadline.
6
7use crate::registry::{CreateSpec, ProblemSchemaEntry};
8use crate::traits::Problem;
9use serde::{Deserialize, Serialize};
10
11inventory::submit! {
12    ProblemSchemaEntry {
13        name: "MultiprocessorScheduling",
14        display_name: "Multiprocessor Scheduling",
15        aliases: &[],
16        dimensions: &[],
17        category: crate::registry::ProblemCategory::Misc,
18        module_path: module_path!(),
19        description: "Assign tasks to processors so that no processor's load exceeds a deadline",
20        fields: MultiprocessorSchedulingCreateSpec::FIELDS,
21    }
22}
23
24/// The Multiprocessor Scheduling problem.
25///
26/// Given a set T of tasks with processing times, a number m of identical
27/// processors, and a deadline D, determine whether there exists an assignment
28/// of tasks to processors such that the total load on each processor does
29/// not exceed D.
30///
31/// Because tasks are independent and processors are identical, any feasible
32/// schedule can be packed processor-by-processor without idle gaps. This makes
33/// the scheduling question equivalent to partitioning tasks among processors
34/// with per-processor load at most `D`.
35///
36/// # Representation
37///
38/// Each task has a variable in `{0, ..., m-1}` representing its processor assignment.
39///
40/// # Example
41///
42/// ```
43/// use problemreductions::models::misc::MultiprocessorScheduling;
44/// use problemreductions::{Problem, BruteForce};
45///
46/// // 5 tasks with lengths [4, 5, 3, 2, 6], 2 processors, deadline 10
47/// let problem = MultiprocessorScheduling::new(vec![4, 5, 3, 2, 6], 2, 10);
48/// let solver = BruteForce::new();
49/// let solution = solver.solve(&problem).unwrap();
50/// assert!(solution.is_some());
51/// ```
52#[derive(Debug, Clone, Serialize, Deserialize)]
53pub struct MultiprocessorScheduling {
54    /// Processing time for each task.
55    lengths: Vec<i64>,
56    /// Number of identical processors.
57    #[serde(deserialize_with = "positive_usize::deserialize")]
58    num_processors: usize,
59    /// Global deadline.
60    deadline: i64,
61}
62
63#[derive(Debug, Deserialize, crate::CreateSpec)]
64struct MultiprocessorSchedulingCreateSpec {
65    /// Processing time for each task.
66    lengths: Vec<i64>,
67    /// Number of identical processors.
68    num_processors: usize,
69    /// Global deadline.
70    deadline: i64,
71}
72impl TryFrom<MultiprocessorSchedulingCreateSpec> for MultiprocessorScheduling {
73    type Error = crate::registry::ConstructionError;
74    fn try_from(spec: MultiprocessorSchedulingCreateSpec) -> Result<Self, Self::Error> {
75        if spec.num_processors == 0 {
76            return Err("num_processors must be positive".to_string().into());
77        }
78        Ok(Self::new(spec.lengths, spec.num_processors, spec.deadline))
79    }
80}
81
82impl MultiprocessorScheduling {
83    /// Create a new Multiprocessor Scheduling instance.
84    ///
85    /// # Panics
86    /// Panics if `num_processors` is zero.
87    pub fn new(lengths: Vec<i64>, num_processors: usize, deadline: i64) -> Self {
88        assert!(num_processors > 0, "num_processors must be positive");
89        assert!(
90            lengths.iter().all(|&length| length >= 0),
91            "task lengths must be nonnegative"
92        );
93        assert!(deadline >= 0, "deadline must be nonnegative");
94        Self {
95            lengths,
96            num_processors,
97            deadline,
98        }
99    }
100
101    /// Returns the processing times for each task.
102    pub fn lengths(&self) -> &[i64] {
103        &self.lengths
104    }
105
106    /// Returns the number of processors.
107    pub fn num_processors(&self) -> usize {
108        self.num_processors
109    }
110
111    /// Returns the deadline.
112    pub fn deadline(&self) -> i64 {
113        self.deadline
114    }
115
116    /// Returns the number of tasks.
117    pub fn num_tasks(&self) -> usize {
118        self.lengths.len()
119    }
120
121    /// Returns the total processing time of all tasks.
122    pub fn total_length(&self) -> i64 {
123        self.lengths.iter().sum()
124    }
125}
126
127impl Problem for MultiprocessorScheduling {
128    const NAME: &'static str = "MultiprocessorScheduling";
129    type Solution = Vec<usize>;
130    type Value = crate::types::Or;
131
132    crate::problem_parameters![("num_processors", num_processors), ("num_tasks", num_tasks),];
133
134    fn variant() -> Vec<(&'static str, &'static str)> {
135        crate::variant_params![]
136    }
137
138    fn evaluate(
139        &self,
140        config: &Self::Solution,
141    ) -> Result<crate::types::Or, crate::traits::EvaluationError> {
142        Ok({
143            crate::types::Or({
144                if config.len() != self.num_tasks() {
145                    return Err(crate::traits::EvaluationError::InvalidConfiguration(
146                        "processor assignment length does not match the tasks".into(),
147                    ));
148                }
149                let m = self.num_processors;
150                if config.iter().any(|&processor| processor >= m) {
151                    return Err(crate::traits::EvaluationError::InvalidConfiguration(
152                        "assignment contains an out-of-range processor".into(),
153                    ));
154                }
155                let mut loads = vec![0i64; m];
156                for (i, &processor) in config.iter().enumerate() {
157                    loads[processor] =
158                        loads[processor]
159                            .checked_add(self.lengths[i])
160                            .ok_or_else(|| {
161                                crate::traits::EvaluationError::IntegerOverflow(
162                                    "summing multiprocessor load".into(),
163                                )
164                            })?;
165                }
166                loads.iter().all(|&load| load <= self.deadline)
167            })
168        })
169    }
170}
171
172impl crate::solvers::BruteForceProblem for MultiprocessorScheduling {
173    fn dimensions(&self) -> Vec<usize> {
174        vec![self.num_processors; self.num_tasks()]
175    }
176}
177
178crate::declare_variants! {
179    default MultiprocessorScheduling => "2^num_tasks" create MultiprocessorSchedulingCreateSpec,
180}
181
182crate::register_brute_force! {
183    MultiprocessorScheduling,
184}
185
186#[cfg(feature = "example-db")]
187pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::ModelExampleSpec> {
188    vec![crate::example_db::specs::ModelExampleSpec {
189        id: "multiprocessor_scheduling",
190        instance: Box::new(MultiprocessorScheduling::new(vec![4, 5, 3, 2, 6], 2, 10)),
191        optimal_config: serde_json::json!(vec![0, 1, 1, 1, 0]),
192        optimal_value: serde_json::json!(true),
193    }]
194}
195
196mod positive_usize {
197    use serde::de::Error;
198    use serde::{Deserialize, Deserializer};
199
200    pub fn deserialize<'de, D>(deserializer: D) -> Result<usize, D::Error>
201    where
202        D: Deserializer<'de>,
203    {
204        let value = usize::deserialize(deserializer)?;
205        if value == 0 {
206            return Err(D::Error::custom("expected positive integer, got 0"));
207        }
208        Ok(value)
209    }
210}
211
212#[cfg(test)]
213#[path = "../../unit_tests/models/misc/multiprocessor_scheduling.rs"]
214mod tests;