problemreductions/models/misc/
job_shop_scheduling.rs1use crate::registry::{CreateSpec, ProblemSchemaEntry};
9use crate::traits::Problem;
10use crate::types::Min;
11use serde::{Deserialize, Serialize};
12use std::collections::VecDeque;
13
14inventory::submit! {
15 ProblemSchemaEntry {
16 name: "JobShopScheduling",
17 display_name: "Job-Shop Scheduling",
18 aliases: &[],
19 dimensions: &[],
20 category: crate::registry::ProblemCategory::Misc,
21 module_path: module_path!(),
22 description: "Minimize the makespan of a job-shop schedule",
23 fields: JobShopSchedulingCreateSpec::FIELDS,
24 }
25}
26
27#[derive(Debug, Clone, Serialize, Deserialize)]
28pub struct JobShopScheduling {
29 num_processors: usize,
30 jobs: Vec<Vec<(usize, i64)>>,
31}
32
33#[derive(Debug, Deserialize, crate::CreateSpec)]
34struct JobShopSchedulingCreateSpec {
35 #[create(codec = "semicolon-separated")]
37 jobs: Vec<Vec<(usize, i64)>>,
38 num_processors: Option<usize>,
40}
41
42impl TryFrom<JobShopSchedulingCreateSpec> for JobShopScheduling {
43 type Error = crate::registry::ConstructionError;
44
45 fn try_from(spec: JobShopSchedulingCreateSpec) -> Result<Self, Self::Error> {
46 let inferred_processors = spec
47 .jobs
48 .iter()
49 .flatten()
50 .map(|(processor, _)| *processor)
51 .max()
52 .map(|processor| {
53 processor
54 .checked_add(1)
55 .ok_or_else(|| "inferred processor count overflows usize".to_string())
56 })
57 .transpose()?;
58 let num_processors = spec.num_processors.or(inferred_processors).ok_or_else(|| {
59 "cannot infer processor count from an empty job list; provide num_processors"
60 .to_string()
61 })?;
62 if num_processors == 0 {
63 return Err("num_processors must be positive".to_string().into());
64 }
65
66 for (job_index, job) in spec.jobs.iter().enumerate() {
67 for (task_index, &(processor, _)) in job.iter().enumerate() {
68 if processor >= num_processors {
69 return Err(format!(
70 "job {job_index} task {task_index} uses processor {processor}, but num_processors is {num_processors}"
71 ).into());
72 }
73 }
74 for (task_index, pair) in job.windows(2).enumerate() {
75 if pair[0].0 == pair[1].0 {
76 return Err(format!(
77 "job {job_index} tasks {task_index} and {} must use different processors",
78 task_index + 1
79 )
80 .into());
81 }
82 }
83 }
84
85 Ok(Self {
86 num_processors,
87 jobs: spec.jobs,
88 })
89 }
90}
91
92struct FlattenedTasks {
93 job_task_ids: Vec<Vec<usize>>,
94 machine_task_ids: Vec<Vec<usize>>,
95 lengths: Vec<i64>,
96}
97
98impl JobShopScheduling {
99 pub fn new(num_processors: usize, jobs: Vec<Vec<(usize, i64)>>) -> Self {
100 let num_tasks: usize = jobs.iter().map(Vec::len).sum();
101 if num_tasks > 0 {
102 assert!(
103 num_processors > 0,
104 "num_processors must be positive when tasks are present"
105 );
106 }
107 assert!(
108 jobs.iter().flatten().all(|&(_, length)| length >= 0),
109 "operation lengths must be nonnegative"
110 );
111
112 for (job_index, job) in jobs.iter().enumerate() {
113 for (task_index, &(processor, _length)) in job.iter().enumerate() {
114 assert!(
115 processor < num_processors,
116 "job {job_index} task {task_index} uses processor {processor}, but num_processors = {num_processors}"
117 );
118 }
119
120 for (task_index, pair) in job.windows(2).enumerate() {
121 assert_ne!(
122 pair[0].0,
123 pair[1].0,
124 "job {job_index} tasks {task_index} and {} must use different processors",
125 task_index + 1
126 );
127 }
128 }
129
130 Self {
131 num_processors,
132 jobs,
133 }
134 }
135
136 pub fn num_processors(&self) -> usize {
137 self.num_processors
138 }
139
140 pub fn jobs(&self) -> &[Vec<(usize, i64)>] {
141 &self.jobs
142 }
143
144 pub fn num_jobs(&self) -> usize {
145 self.jobs.len()
146 }
147
148 pub fn num_tasks(&self) -> usize {
149 self.jobs.iter().map(Vec::len).sum()
150 }
151
152 fn flatten_tasks(&self) -> FlattenedTasks {
153 let mut job_task_ids = Vec::with_capacity(self.jobs.len());
154 let mut machine_task_ids = vec![Vec::new(); self.num_processors];
155 let mut lengths = Vec::with_capacity(self.num_tasks());
156 let mut task_id = 0usize;
157
158 for job in &self.jobs {
159 let mut ids = Vec::with_capacity(job.len());
160 for &(processor, length) in job {
161 ids.push(task_id);
162 machine_task_ids[processor].push(task_id);
163 lengths.push(length);
164 task_id += 1;
165 }
166 job_task_ids.push(ids);
167 }
168
169 FlattenedTasks {
170 job_task_ids,
171 machine_task_ids,
172 lengths,
173 }
174 }
175
176 fn decode_machine_orders(
177 &self,
178 config: &[usize],
179 flattened: &FlattenedTasks,
180 ) -> Option<Vec<Vec<usize>>> {
181 if config.len() != flattened.lengths.len() {
182 return None;
183 }
184
185 let mut offset = 0usize;
186 let mut orders = Vec::with_capacity(flattened.machine_task_ids.len());
187
188 for machine_tasks in &flattened.machine_task_ids {
189 let k = machine_tasks.len();
190 let perm = super::decode_lehmer(&config[offset..offset + k], k)?;
191 orders.push(perm.into_iter().map(|i| machine_tasks[i]).collect());
192 offset += k;
193 }
194
195 Some(orders)
196 }
197
198 pub fn schedule_from_config(&self, config: &[usize]) -> Option<Vec<i64>> {
201 self.schedule_from_config_inner(config, &self.flatten_tasks())
202 }
203
204 fn schedule_from_config_inner(
205 &self,
206 config: &[usize],
207 flattened: &FlattenedTasks,
208 ) -> Option<Vec<i64>> {
209 let machine_orders = self.decode_machine_orders(config, flattened)?;
210 let num_tasks = flattened.lengths.len();
211
212 if num_tasks == 0 {
213 return Some(Vec::new());
214 }
215
216 let mut adjacency = vec![Vec::<usize>::new(); num_tasks];
217 let mut indegree = vec![0usize; num_tasks];
218
219 for job_ids in &flattened.job_task_ids {
220 for pair in job_ids.windows(2) {
221 adjacency[pair[0]].push(pair[1]);
222 indegree[pair[1]] += 1;
223 }
224 }
225
226 for machine_order in &machine_orders {
227 for pair in machine_order.windows(2) {
228 adjacency[pair[0]].push(pair[1]);
229 indegree[pair[1]] += 1;
230 }
231 }
232
233 let mut queue = VecDeque::new();
234 for (task_id, °ree) in indegree.iter().enumerate() {
235 if degree == 0 {
236 queue.push_back(task_id);
237 }
238 }
239
240 let mut start_times = vec![0i64; num_tasks];
241 let mut processed = 0usize;
242
243 while let Some(task_id) = queue.pop_front() {
244 processed += 1;
245 let finish = start_times[task_id].checked_add(flattened.lengths[task_id])?;
246
247 for &next_task in &adjacency[task_id] {
248 start_times[next_task] = start_times[next_task].max(finish);
249 indegree[next_task] -= 1;
250 if indegree[next_task] == 0 {
251 queue.push_back(next_task);
252 }
253 }
254 }
255
256 if processed != num_tasks {
257 return None;
258 }
259
260 Some(start_times)
261 }
262}
263
264impl Problem for JobShopScheduling {
265 const NAME: &'static str = "JobShopScheduling";
266 type Solution = Vec<usize>;
267 type Value = Min<i64>;
268
269 crate::problem_parameters![
270 ("num_processors", num_processors),
271 ("num_jobs", num_jobs),
272 ("num_tasks", num_tasks),
273 ];
274
275 fn variant() -> Vec<(&'static str, &'static str)> {
276 crate::variant_params![]
277 }
278
279 fn evaluate(
280 &self,
281 config: &Self::Solution,
282 ) -> Result<Min<i64>, crate::traits::EvaluationError> {
283 let flattened = self.flatten_tasks();
284 if config.len() != flattened.lengths.len() {
285 return Err(crate::traits::EvaluationError::InvalidConfiguration(
286 "machine-order encoding length does not match the tasks".into(),
287 ));
288 }
289 let dimensions = flattened
290 .machine_task_ids
291 .iter()
292 .flat_map(|machine_tasks| super::lehmer_dims(machine_tasks.len()));
293 if config
294 .iter()
295 .zip(dimensions)
296 .any(|(&digit, radix)| digit >= radix)
297 {
298 return Err(crate::traits::EvaluationError::InvalidConfiguration(
299 "machine-order encoding contains an out-of-range digit".into(),
300 ));
301 }
302 Ok({
303 match self.schedule_from_config_inner(config, &flattened) {
304 Some(start_times) => {
305 let makespan = start_times
306 .iter()
307 .enumerate()
308 .map(|(i, &s)| s + flattened.lengths[i])
309 .max()
310 .unwrap_or(0);
311 Min(Some(makespan))
312 }
313 None => Min(None),
314 }
315 })
316 }
317}
318
319impl crate::solvers::BruteForceProblem for JobShopScheduling {
320 fn dimensions(&self) -> Vec<usize> {
321 self.flatten_tasks()
322 .machine_task_ids
323 .into_iter()
324 .flat_map(|machine_tasks| super::lehmer_dims(machine_tasks.len()))
325 .collect()
326 }
327}
328
329crate::declare_variants! {
330 default JobShopScheduling => "factorial(num_tasks)" create JobShopSchedulingCreateSpec,
331}
332
333crate::register_brute_force! {
334 JobShopScheduling,
335}
336
337#[cfg(feature = "example-db")]
338pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::ModelExampleSpec> {
339 vec![crate::example_db::specs::ModelExampleSpec {
340 id: "job_shop_scheduling",
341 instance: Box::new(JobShopScheduling::new(
342 2,
343 vec![
344 vec![(0, 3), (1, 4)],
345 vec![(1, 2), (0, 3), (1, 2)],
346 vec![(0, 4), (1, 3)],
347 vec![(1, 5), (0, 2)],
348 vec![(0, 2), (1, 3), (0, 1)],
349 ],
350 )),
351 optimal_config: serde_json::json!(vec![0, 0, 0, 0, 0, 0, 1, 3, 0, 1, 1, 0]),
354 optimal_value: serde_json::json!(19),
355 }]
356}
357
358#[cfg(test)]
359#[path = "../../unit_tests/models/misc/job_shop_scheduling.rs"]
360mod tests;