1use 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 pub fn schedule_horizon_bits(&self) -> u64 {
194 crate::types::max_numeric_magnitude_bits([self.schedule_horizon()])
195 }
196
197 fn finish_time(
198 &self,
199 config: &[usize],
200 job: usize,
201 machine: usize,
202 ) -> Result<i64, crate::traits::EvaluationError> {
203 let start = i64::try_from(config[job * self.num_machines + machine]).map_err(|_| {
204 crate::traits::EvaluationError::IntegerOverflow(
205 "converting an open-shop start time to i64".into(),
206 )
207 })?;
208 start
209 .checked_add(self.processing_times[job][machine])
210 .ok_or_else(|| {
211 crate::traits::EvaluationError::IntegerOverflow(
212 "computing an open-shop completion time".into(),
213 )
214 })
215 }
216
217 fn operations_overlap(
218 &self,
219 config: &[usize],
220 first: (usize, usize),
221 second: (usize, usize),
222 ) -> Result<bool, crate::traits::EvaluationError> {
223 let (j1, i1) = first;
224 let (j2, i2) = second;
225 let s1 = i64::try_from(config[j1 * self.num_machines + i1]).map_err(|_| {
226 crate::traits::EvaluationError::IntegerOverflow("converting start time to i64".into())
227 })?;
228 let s2 = i64::try_from(config[j2 * self.num_machines + i2]).map_err(|_| {
229 crate::traits::EvaluationError::IntegerOverflow("converting start time to i64".into())
230 })?;
231 let f1 = self.finish_time(config, j1, i1)?;
232 let f2 = self.finish_time(config, j2, i2)?;
233 Ok(s1 < f2 && s2 < f1)
234 }
235}
236
237impl Problem for OpenShopScheduling {
238 const NAME: &'static str = "OpenShopScheduling";
239 type Solution = Vec<usize>;
240 type Value = Min<i64>;
241
242 crate::problem_parameters![
243 ("num_jobs", num_jobs),
244 ("num_machines", num_machines),
245 ("schedule_horizon", schedule_horizon),
246 ("schedule_horizon_bits", schedule_horizon_bits),
247 ];
248
249 fn variant() -> Vec<(&'static str, &'static str)> {
250 crate::variant_params![]
251 }
252
253 fn evaluate(
254 &self,
255 config: &Self::Solution,
256 ) -> Result<Min<i64>, crate::traits::EvaluationError> {
257 let n = self.num_jobs();
258 let m = self.num_machines;
259 if config.len() != n * m {
260 return Err(crate::traits::EvaluationError::InvalidConfiguration(
261 "start-time representation length does not match the instance".into(),
262 ));
263 }
264 for machine in 0..m {
265 for first in 0..n {
266 for second in (first + 1)..n {
267 if self.operations_overlap(config, (first, machine), (second, machine))? {
268 return Ok(Min(None));
269 }
270 }
271 }
272 }
273 for job in 0..n {
274 for first in 0..m {
275 for second in (first + 1)..m {
276 if self.operations_overlap(config, (job, first), (job, second))? {
277 return Ok(Min(None));
278 }
279 }
280 }
281 }
282 let mut makespan = 0;
283 for job in 0..n {
284 for machine in 0..m {
285 makespan = makespan.max(self.finish_time(config, job, machine)?);
286 }
287 }
288 Ok(Min(Some(makespan)))
289 }
290}
291
292impl crate::solvers::BruteForceProblem for OpenShopScheduling {
293 fn dimensions(&self) -> Vec<usize> {
294 let domain = self
295 .schedule_horizon()
296 .checked_add(1)
297 .expect("schedule horizon overflow");
298 vec![domain; self.num_jobs() * self.num_machines]
299 }
300}
301
302crate::declare_variants! {
303 default OpenShopScheduling => "(schedule_horizon + 1)^(num_jobs * num_machines)" create OpenShopSchedulingCreateSpec,
304}
305
306crate::register_brute_force! {
307 OpenShopScheduling,
308}
309
310#[cfg(feature = "example-db")]
311pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::ModelExampleSpec> {
312 vec![crate::example_db::specs::ModelExampleSpec {
325 id: "open_shop_scheduling",
326 instance: Box::new(OpenShopScheduling::new(
327 3,
328 vec![vec![3, 1, 2], vec![2, 3, 1], vec![1, 2, 3], vec![2, 2, 1]],
329 )),
330 optimal_config: serde_json::json!(vec![0, 3, 4, 3, 0, 6, 5, 6, 0, 6, 4, 3]),
331 optimal_value: serde_json::json!(8),
332 }]
333}
334
335#[cfg(test)]
336#[path = "../../unit_tests/models/misc/open_shop_scheduling.rs"]
337mod tests;
338
339crate::decision_problem_meta!(OpenShopScheduling, "DecisionOpenShopScheduling");
340crate::register_decision_variant!(
341 OpenShopScheduling, "DecisionOpenShopScheduling", "(schedule_horizon + 1)^(num_jobs * num_machines)", &[],
342 "Does a feasible solution have objective value <= the bound?",
343 category: crate::registry::ProblemCategory::Misc,
344 dims: [],
345 fields: [
346 crate::registry::FieldInfo { name: "num_processors", type_name: "usize", description: "Number of machines m." },
347 crate::registry::FieldInfo { name: "processing_times", type_name: "Vec<Vec<i64>>", description: "Processing time of each job on each machine (n x m)." },
348 crate::registry::FieldInfo { name: "bound", type_name: "i64", description: "Accept objective values <= this bound" },
349 ],
350 decode: |_, indices: Vec<usize>| indices
351);
352
353#[cfg(feature = "example-db")]
354pub(crate) fn decision_canonical_rule_example_specs(
355) -> Vec<crate::example_db::specs::RuleExampleSpec> {
356 vec![crate::example_db::specs::RuleExampleSpec {
357 id: "decision_open_shop_scheduling_to_open_shop_scheduling",
358 build: || {
359 let source = crate::models::decision::Decision::new(
360 OpenShopScheduling::new(
361 3,
362 vec![vec![3, 1, 2], vec![2, 3, 1], vec![1, 2, 3], vec![2, 2, 1]],
363 ),
364 8,
365 );
366 let witness = serde_json::json!(vec![0, 3, 4, 3, 0, 6, 5, 6, 0, 6, 4, 3]);
367 crate::example_db::specs::rule_example_with_witness::<_, OpenShopScheduling>(
368 source,
369 crate::export::SolutionPair {
370 source_config: witness.clone(),
371 target_config: witness,
372 },
373 )
374 },
375 }]
376}