Skip to main content

problemreductions/models/misc/
resource_constrained_scheduling.rs

1//! Resource Constrained Scheduling problem implementation.
2//!
3//! A classical NP-complete scheduling problem (Garey & Johnson A5 SS10) where
4//! unit-length tasks must be assigned to identical processors under both a
5//! processor capacity limit and resource usage constraints per time slot.
6
7use crate::registry::{ConstructionError, FieldInfo, ProblemSchemaEntry};
8use crate::traits::Problem;
9use serde::{Deserialize, Serialize};
10
11inventory::submit! {
12    ProblemSchemaEntry {
13        name: "ResourceConstrainedScheduling",
14        display_name: "Resource Constrained Scheduling",
15        aliases: &[],
16        dimensions: &[],
17        category: crate::registry::ProblemCategory::Misc,
18        module_path: module_path!(),
19        description: "Schedule unit-length tasks on m processors with resource constraints and a deadline",
20        fields: &[
21            FieldInfo { name: "num_processors", type_name: "usize", description: "Number of identical processors m" },
22            FieldInfo { name: "resource_bounds", type_name: "Vec<i64>", description: "Resource bound B_i for each resource i" },
23            FieldInfo { name: "resource_requirements", type_name: "Vec<Vec<i64>>", description: "R_i(t) for each task t and resource i (n x r matrix)" },
24            FieldInfo { name: "deadline", type_name: "i64", description: "Overall deadline D" },
25        ],
26    }
27}
28
29/// The Resource Constrained Scheduling problem.
30///
31/// Given `n` unit-length tasks, `m` identical processors, `r` resources with
32/// bounds `B_i`, resource requirements `R_i(t)` for each task `t` and resource `i`,
33/// and an overall deadline `D`, determine whether there exists a schedule
34/// `σ: T → {0, ..., D-1}` such that:
35/// - At each time slot `u`, at most `m` tasks are scheduled (processor capacity)
36/// - At each time slot `u` and for each resource `i`, the sum of `R_i(t)` over
37///   all tasks `t` scheduled at `u` does not exceed `B_i`
38///
39/// # Representation
40///
41/// Each task has a variable in `{0, ..., D-1}` representing its assigned time slot.
42///
43/// # Example
44///
45/// ```
46/// use problemreductions::models::misc::ResourceConstrainedScheduling;
47/// use problemreductions::{Problem, BruteForce};
48///
49/// // 6 tasks, 3 processors, 1 resource with bound 20, deadline 2
50/// let problem = ResourceConstrainedScheduling::new(
51///     3,
52///     vec![20],
53///     vec![vec![6], vec![7], vec![7], vec![6], vec![8], vec![6]],
54///     2,
55/// ).unwrap();
56/// let solver = BruteForce::new();
57/// let solution = solver.solve(&problem).unwrap();
58/// assert!(solution.is_some());
59/// ```
60#[derive(Debug, Clone, Serialize)]
61pub struct ResourceConstrainedScheduling {
62    /// Number of identical processors.
63    num_processors: usize,
64    /// Resource bounds B_i for each resource.
65    resource_bounds: Vec<i64>,
66    /// Resource requirements R_i(t) for each task t and resource i (n x r matrix).
67    resource_requirements: Vec<Vec<i64>>,
68    /// Overall deadline D.
69    deadline: i64,
70}
71
72impl ResourceConstrainedScheduling {
73    /// Create a new Resource Constrained Scheduling instance.
74    ///
75    /// # Arguments
76    /// * `num_processors` - Number of identical processors `m`
77    /// * `resource_bounds` - Resource bound `B_i` for each resource `i` (length = r)
78    /// * `resource_requirements` - `R_i(t)` for each task `t` and resource `i` (n x r matrix)
79    /// * `deadline` - Overall deadline `D`
80    pub fn new(
81        num_processors: usize,
82        resource_bounds: Vec<i64>,
83        resource_requirements: Vec<Vec<i64>>,
84        deadline: i64,
85    ) -> Result<Self, ConstructionError> {
86        if deadline <= 0 {
87            return Err(ConstructionError::Conversion(
88                "deadline must be positive".into(),
89            ));
90        }
91        usize::try_from(deadline).map_err(|_| {
92            ConstructionError::IntegerOverflow("deadline does not fit usize".into())
93        })?;
94        if resource_bounds.iter().any(|&bound| bound < 0) {
95            return Err(ConstructionError::Conversion(
96                "resource bounds must be nonnegative".into(),
97            ));
98        }
99        let r = resource_bounds.len();
100        for (t, row) in resource_requirements.iter().enumerate() {
101            if row.len() != r {
102                return Err(ConstructionError::Conversion(format!(
103                    "task {t} has {} resource requirements, expected {r}",
104                    row.len()
105                )));
106            }
107            if row.iter().any(|&requirement| requirement < 0) {
108                return Err(ConstructionError::Conversion(format!(
109                    "task {t} resource requirements must be nonnegative"
110                )));
111            }
112        }
113        Ok(Self {
114            num_processors,
115            resource_bounds,
116            resource_requirements,
117            deadline,
118        })
119    }
120
121    /// Smallest h >= 1 with every resource requirement and bound strictly below 2^h.
122    pub fn max_resource_bits(&self) -> u64 {
123        crate::types::max_numeric_magnitude_bits(
124            self.resource_requirements
125                .iter()
126                .flatten()
127                .copied()
128                .chain(self.resource_bounds.iter().copied()),
129        )
130    }
131
132    /// Get the number of tasks.
133    pub fn num_tasks(&self) -> usize {
134        self.resource_requirements.len()
135    }
136
137    /// Get the number of processors.
138    pub fn num_processors(&self) -> usize {
139        self.num_processors
140    }
141
142    /// Get the resource bounds.
143    pub fn resource_bounds(&self) -> &[i64] {
144        &self.resource_bounds
145    }
146
147    /// Get the resource requirements matrix.
148    pub fn resource_requirements(&self) -> &[Vec<i64>] {
149        &self.resource_requirements
150    }
151
152    /// Get the deadline.
153    pub fn deadline(&self) -> i64 {
154        self.deadline
155    }
156
157    /// Get the number of resources.
158    pub fn num_resources(&self) -> usize {
159        self.resource_bounds.len()
160    }
161}
162
163impl<'de> Deserialize<'de> for ResourceConstrainedScheduling {
164    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
165    where
166        D: serde::Deserializer<'de>,
167    {
168        #[derive(Deserialize)]
169        struct Raw {
170            num_processors: usize,
171            resource_bounds: Vec<i64>,
172            resource_requirements: Vec<Vec<i64>>,
173            deadline: i64,
174        }
175
176        let raw = Raw::deserialize(deserializer)?;
177        Self::new(
178            raw.num_processors,
179            raw.resource_bounds,
180            raw.resource_requirements,
181            raw.deadline,
182        )
183        .map_err(serde::de::Error::custom)
184    }
185}
186
187impl Problem for ResourceConstrainedScheduling {
188    const NAME: &'static str = "ResourceConstrainedScheduling";
189    type Solution = Vec<usize>;
190    type Value = crate::types::Or;
191
192    crate::problem_parameters![
193        ("max_resource_bits", max_resource_bits),
194        ("deadline", deadline),
195        ("num_resources", num_resources),
196        ("num_tasks", num_tasks),
197    ];
198
199    fn variant() -> Vec<(&'static str, &'static str)> {
200        crate::variant_params![]
201    }
202
203    fn evaluate(
204        &self,
205        config: &Self::Solution,
206    ) -> Result<crate::types::Or, crate::traits::EvaluationError> {
207        Ok({
208            crate::types::Or({
209                let n = self.num_tasks();
210                let d = self.deadline as usize;
211                let r = self.num_resources();
212
213                // Check config length
214                if config.len() != n {
215                    return Err(crate::traits::EvaluationError::InvalidConfiguration(
216                        "schedule length does not match the tasks".into(),
217                    ));
218                }
219
220                if config.iter().any(|&start| start >= d) {
221                    return Err(crate::traits::EvaluationError::InvalidConfiguration(
222                        "schedule contains an out-of-range start time".into(),
223                    ));
224                }
225
226                // Empty slots consume no resources. Keep ascending slot order and
227                // task order within each slot for checked accumulation.
228                let mut tasks: Vec<_> = (0..n).collect();
229                tasks.sort_unstable_by_key(|&task| (config[task], task));
230                for slot_tasks in tasks.chunk_by(|&a, &b| config[a] == config[b]) {
231                    let mut resource_usage = vec![0i64; r];
232                    for &task in slot_tasks {
233                        for (usage, &req) in resource_usage
234                            .iter_mut()
235                            .zip(self.resource_requirements[task].iter())
236                        {
237                            *usage = usage.checked_add(req).ok_or_else(|| {
238                                crate::traits::EvaluationError::IntegerOverflow(
239                                    "summing scheduled resource usage".to_string(),
240                                )
241                            })?;
242                        }
243                    }
244
245                    // Check processor capacity
246                    if slot_tasks.len() > self.num_processors {
247                        return Ok(crate::types::Or(false));
248                    }
249
250                    // Check resource bounds
251                    for (usage, bound) in resource_usage.iter().zip(self.resource_bounds.iter()) {
252                        if usage > bound {
253                            return Ok(crate::types::Or(false));
254                        }
255                    }
256                }
257
258                true
259            })
260        })
261    }
262}
263
264impl crate::solvers::BruteForceProblem for ResourceConstrainedScheduling {
265    fn dimensions(&self) -> Vec<usize> {
266        vec![self.deadline as usize; self.num_tasks()]
267    }
268}
269
270crate::declare_variants! {
271    default ResourceConstrainedScheduling => "deadline ^ num_tasks",
272}
273
274crate::register_brute_force! {
275    ResourceConstrainedScheduling,
276}
277
278#[cfg(feature = "example-db")]
279pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::ModelExampleSpec> {
280    vec![crate::example_db::specs::ModelExampleSpec {
281        id: "resource_constrained_scheduling",
282        // 6 tasks, 3 processors, 1 resource B_1=20, deadline 2
283        instance: Box::new(
284            ResourceConstrainedScheduling::new(
285                3,
286                vec![20],
287                vec![vec![6], vec![7], vec![7], vec![6], vec![8], vec![6]],
288                2,
289            )
290            .expect("canonical resource-constrained-scheduling instance must be valid"),
291        ),
292        optimal_config: serde_json::json!(vec![0, 0, 0, 1, 1, 1]),
293        optimal_value: serde_json::json!(true),
294    }]
295}
296
297#[cfg(test)]
298#[path = "../../unit_tests/models/misc/resource_constrained_scheduling.rs"]
299mod tests;