problemreductions/models/misc/
open_shop_scheduling.rs1use crate::registry::{CreateSpec, ProblemSchemaEntry};
10use crate::traits::Problem;
11use crate::types::Min;
12use serde::{Deserialize, Serialize};
13
14inventory::submit! {
15 ProblemSchemaEntry {
16 name: "OpenShopScheduling",
17 display_name: "Open Shop Scheduling",
18 aliases: &[],
19 dimensions: &[],
20 category: crate::registry::ProblemCategory::Misc,
21 module_path: module_path!(),
22 description: "Minimize the makespan of an open-shop schedule",
23 fields: OpenShopSchedulingCreateSpec::FIELDS,
24 }
25}
26
27#[derive(Debug, Clone, Serialize, Deserialize)]
62#[serde(try_from = "OpenShopSchedulingSerde")]
63pub struct OpenShopScheduling {
64 num_machines: usize,
66 processing_times: Vec<Vec<i64>>,
69}
70
71#[derive(Deserialize)]
72struct OpenShopSchedulingSerde {
73 num_machines: usize,
74 processing_times: Vec<Vec<i64>>,
75}
76
77impl TryFrom<OpenShopSchedulingSerde> for OpenShopScheduling {
78 type Error = crate::registry::ConstructionError;
79
80 fn try_from(value: OpenShopSchedulingSerde) -> Result<Self, Self::Error> {
81 Self::try_new(value.num_machines, value.processing_times)
82 }
83}
84
85#[derive(Debug, Deserialize, crate::CreateSpec)]
86struct OpenShopSchedulingCreateSpec {
87 num_processors: usize,
89 processing_times: Vec<Vec<i64>>,
91}
92
93impl TryFrom<OpenShopSchedulingCreateSpec> for OpenShopScheduling {
94 type Error = crate::registry::ConstructionError;
95
96 fn try_from(spec: OpenShopSchedulingCreateSpec) -> Result<Self, Self::Error> {
97 Self::try_new(spec.num_processors, spec.processing_times)
98 }
99}
100
101impl OpenShopScheduling {
102 pub fn new(num_machines: usize, processing_times: Vec<Vec<i64>>) -> Self {
112 Self::try_new(num_machines, processing_times)
113 .expect("invalid open-shop scheduling instance")
114 }
115
116 pub fn try_new(
118 num_machines: usize,
119 processing_times: Vec<Vec<i64>>,
120 ) -> Result<Self, crate::registry::ConstructionError> {
121 for (job, times) in processing_times.iter().enumerate() {
122 if times.len() != num_machines {
123 return Err(format!(
124 "processing_times[{job}] has {} entries, expected {num_machines}",
125 times.len(),
126 )
127 .into());
128 }
129 if times.iter().any(|&time| time < 0) {
130 return Err(format!("processing_times[{job}] contains a negative duration").into());
131 }
132 }
133 processing_times
134 .len()
135 .checked_mul(num_machines)
136 .ok_or_else(|| {
137 crate::registry::ConstructionError::IntegerOverflow(
138 "operation count overflows usize".into(),
139 )
140 })?;
141 let horizon = processing_times
142 .iter()
143 .flatten()
144 .try_fold(0i64, |total, &time| total.checked_add(time))
145 .ok_or_else(|| {
146 crate::registry::ConstructionError::IntegerOverflow(
147 "schedule horizon overflows i64".into(),
148 )
149 })?;
150 usize::try_from(horizon)
151 .ok()
152 .and_then(|value| value.checked_add(1))
153 .ok_or_else(|| {
154 crate::registry::ConstructionError::IntegerOverflow(
155 "schedule horizon domain overflows usize".into(),
156 )
157 })?;
158 Ok(Self {
159 num_machines,
160 processing_times,
161 })
162 }
163
164 pub fn num_machines(&self) -> usize {
166 self.num_machines
167 }
168
169 pub fn num_jobs(&self) -> usize {
171 self.processing_times.len()
172 }
173
174 pub fn processing_times(&self) -> &[Vec<i64>] {
176 &self.processing_times
177 }
178
179 pub fn schedule_horizon(&self) -> usize {
181 self.processing_times
182 .iter()
183 .flatten()
184 .try_fold(0usize, |total, &time| {
185 usize::try_from(time)
186 .ok()
187 .and_then(|time| total.checked_add(time))
188 })
189 .expect("processing times must fit the brute-force schedule horizon")
190 }
191
192 fn finish_time(
193 &self,
194 config: &[usize],
195 job: usize,
196 machine: usize,
197 ) -> Result<i64, crate::traits::EvaluationError> {
198 let start = i64::try_from(config[job * self.num_machines + machine]).map_err(|_| {
199 crate::traits::EvaluationError::IntegerOverflow(
200 "converting an open-shop start time to i64".into(),
201 )
202 })?;
203 start
204 .checked_add(self.processing_times[job][machine])
205 .ok_or_else(|| {
206 crate::traits::EvaluationError::IntegerOverflow(
207 "computing an open-shop completion time".into(),
208 )
209 })
210 }
211
212 fn operations_overlap(
213 &self,
214 config: &[usize],
215 first: (usize, usize),
216 second: (usize, usize),
217 ) -> Result<bool, crate::traits::EvaluationError> {
218 let (j1, i1) = first;
219 let (j2, i2) = second;
220 let s1 = i64::try_from(config[j1 * self.num_machines + i1]).map_err(|_| {
221 crate::traits::EvaluationError::IntegerOverflow("converting start time to i64".into())
222 })?;
223 let s2 = i64::try_from(config[j2 * self.num_machines + i2]).map_err(|_| {
224 crate::traits::EvaluationError::IntegerOverflow("converting start time to i64".into())
225 })?;
226 let f1 = self.finish_time(config, j1, i1)?;
227 let f2 = self.finish_time(config, j2, i2)?;
228 Ok(s1 < f2 && s2 < f1)
229 }
230}
231
232impl Problem for OpenShopScheduling {
233 const NAME: &'static str = "OpenShopScheduling";
234 type Solution = Vec<usize>;
235 type Value = Min<i64>;
236
237 crate::problem_parameters![
238 ("num_jobs", num_jobs),
239 ("num_machines", num_machines),
240 ("schedule_horizon", schedule_horizon),
241 ];
242
243 fn variant() -> Vec<(&'static str, &'static str)> {
244 crate::variant_params![]
245 }
246
247 fn evaluate(
248 &self,
249 config: &Self::Solution,
250 ) -> Result<Min<i64>, crate::traits::EvaluationError> {
251 let n = self.num_jobs();
252 let m = self.num_machines;
253 if config.len() != n * m {
254 return Err(crate::traits::EvaluationError::InvalidConfiguration(
255 "start-time representation length does not match the instance".into(),
256 ));
257 }
258 for machine in 0..m {
259 for first in 0..n {
260 for second in (first + 1)..n {
261 if self.operations_overlap(config, (first, machine), (second, machine))? {
262 return Ok(Min(None));
263 }
264 }
265 }
266 }
267 for job in 0..n {
268 for first in 0..m {
269 for second in (first + 1)..m {
270 if self.operations_overlap(config, (job, first), (job, second))? {
271 return Ok(Min(None));
272 }
273 }
274 }
275 }
276 let mut makespan = 0;
277 for job in 0..n {
278 for machine in 0..m {
279 makespan = makespan.max(self.finish_time(config, job, machine)?);
280 }
281 }
282 Ok(Min(Some(makespan)))
283 }
284}
285
286impl crate::solvers::BruteForceProblem for OpenShopScheduling {
287 fn dimensions(&self) -> Vec<usize> {
288 let domain = self
289 .schedule_horizon()
290 .checked_add(1)
291 .expect("schedule horizon overflow");
292 vec![domain; self.num_jobs() * self.num_machines]
293 }
294}
295
296crate::declare_variants! {
297 default OpenShopScheduling => "(schedule_horizon + 1)^(num_jobs * num_machines)" create OpenShopSchedulingCreateSpec,
298}
299
300crate::register_brute_force! {
301 OpenShopScheduling,
302}
303
304#[cfg(feature = "example-db")]
305pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::ModelExampleSpec> {
306 vec![crate::example_db::specs::ModelExampleSpec {
319 id: "open_shop_scheduling",
320 instance: Box::new(OpenShopScheduling::new(
321 3,
322 vec![vec![3, 1, 2], vec![2, 3, 1], vec![1, 2, 3], vec![2, 2, 1]],
323 )),
324 optimal_config: serde_json::json!(vec![0, 3, 4, 3, 0, 6, 5, 6, 0, 6, 4, 3]),
325 optimal_value: serde_json::json!(8),
326 }]
327}
328
329#[cfg(test)]
330#[path = "../../unit_tests/models/misc/open_shop_scheduling.rs"]
331mod tests;