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    /// Get the number of tasks.
122    pub fn num_tasks(&self) -> usize {
123        self.resource_requirements.len()
124    }
125
126    /// Get the number of processors.
127    pub fn num_processors(&self) -> usize {
128        self.num_processors
129    }
130
131    /// Get the resource bounds.
132    pub fn resource_bounds(&self) -> &[i64] {
133        &self.resource_bounds
134    }
135
136    /// Get the resource requirements matrix.
137    pub fn resource_requirements(&self) -> &[Vec<i64>] {
138        &self.resource_requirements
139    }
140
141    /// Get the deadline.
142    pub fn deadline(&self) -> i64 {
143        self.deadline
144    }
145
146    /// Get the number of resources.
147    pub fn num_resources(&self) -> usize {
148        self.resource_bounds.len()
149    }
150}
151
152impl<'de> Deserialize<'de> for ResourceConstrainedScheduling {
153    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
154    where
155        D: serde::Deserializer<'de>,
156    {
157        #[derive(Deserialize)]
158        struct Raw {
159            num_processors: usize,
160            resource_bounds: Vec<i64>,
161            resource_requirements: Vec<Vec<i64>>,
162            deadline: i64,
163        }
164
165        let raw = Raw::deserialize(deserializer)?;
166        Self::new(
167            raw.num_processors,
168            raw.resource_bounds,
169            raw.resource_requirements,
170            raw.deadline,
171        )
172        .map_err(serde::de::Error::custom)
173    }
174}
175
176impl Problem for ResourceConstrainedScheduling {
177    const NAME: &'static str = "ResourceConstrainedScheduling";
178    type Solution = Vec<usize>;
179    type Value = crate::types::Or;
180
181    crate::problem_parameters![
182        ("deadline", deadline),
183        ("num_resources", num_resources),
184        ("num_tasks", num_tasks),
185    ];
186
187    fn variant() -> Vec<(&'static str, &'static str)> {
188        crate::variant_params![]
189    }
190
191    fn evaluate(
192        &self,
193        config: &Self::Solution,
194    ) -> Result<crate::types::Or, crate::traits::EvaluationError> {
195        Ok({
196            crate::types::Or({
197                let n = self.num_tasks();
198                let d = self.deadline as usize;
199                let r = self.num_resources();
200
201                // Check config length
202                if config.len() != n {
203                    return Err(crate::traits::EvaluationError::InvalidConfiguration(
204                        "schedule length does not match the tasks".into(),
205                    ));
206                }
207
208                if config.iter().any(|&start| start >= d) {
209                    return Err(crate::traits::EvaluationError::InvalidConfiguration(
210                        "schedule contains an out-of-range start time".into(),
211                    ));
212                }
213
214                // Check processor capacity and resource constraints at each time slot
215                for u in 0..d {
216                    // Collect tasks scheduled at time slot u
217                    let mut task_count = 0usize;
218                    let mut resource_usage = vec![0i64; r];
219
220                    for (t, &slot) in config.iter().enumerate() {
221                        if slot == u {
222                            task_count += 1;
223                            // Accumulate resource usage
224                            for (usage, &req) in resource_usage
225                                .iter_mut()
226                                .zip(self.resource_requirements[t].iter())
227                            {
228                                *usage = usage.checked_add(req).ok_or_else(|| {
229                                    crate::traits::EvaluationError::IntegerOverflow(
230                                        "summing scheduled resource usage".to_string(),
231                                    )
232                                })?;
233                            }
234                        }
235                    }
236
237                    // Check processor capacity
238                    if task_count > self.num_processors {
239                        return Ok(crate::types::Or(false));
240                    }
241
242                    // Check resource bounds
243                    for (usage, bound) in resource_usage.iter().zip(self.resource_bounds.iter()) {
244                        if usage > bound {
245                            return Ok(crate::types::Or(false));
246                        }
247                    }
248                }
249
250                true
251            })
252        })
253    }
254}
255
256impl crate::solvers::BruteForceProblem for ResourceConstrainedScheduling {
257    fn dimensions(&self) -> Vec<usize> {
258        vec![self.deadline as usize; self.num_tasks()]
259    }
260}
261
262crate::declare_variants! {
263    default ResourceConstrainedScheduling => "deadline ^ num_tasks",
264}
265
266crate::register_brute_force! {
267    ResourceConstrainedScheduling,
268}
269
270#[cfg(feature = "example-db")]
271pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::ModelExampleSpec> {
272    vec![crate::example_db::specs::ModelExampleSpec {
273        id: "resource_constrained_scheduling",
274        // 6 tasks, 3 processors, 1 resource B_1=20, deadline 2
275        instance: Box::new(
276            ResourceConstrainedScheduling::new(
277                3,
278                vec![20],
279                vec![vec![6], vec![7], vec![7], vec![6], vec![8], vec![6]],
280                2,
281            )
282            .expect("canonical resource-constrained-scheduling instance must be valid"),
283        ),
284        optimal_config: serde_json::json!(vec![0, 0, 0, 1, 1, 1]),
285        optimal_value: serde_json::json!(true),
286    }]
287}
288
289#[cfg(test)]
290#[path = "../../unit_tests/models/misc/resource_constrained_scheduling.rs"]
291mod tests;