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