Skip to main content

problemreductions/models/misc/
job_shop_scheduling.rs

1//! Job Shop Scheduling problem implementation.
2//!
3//! Given `m` processors and a set of jobs, each job consisting of an ordered
4//! sequence of processor-length tasks, find a schedule that minimizes the
5//! makespan (completion time of the last task) while respecting both within-job
6//! precedence and single-processor capacity constraints.
7
8use 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    /// Jobs expressed as ordered processor-duration operations.
51    #[create(codec = "semicolon-separated")]
52    jobs: Vec<Vec<(usize, i64)>>,
53    /// Optional processor count; omitted values are inferred from the jobs.
54    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    /// Compute start times from a Lehmer-code config. Returns `None` if the
194    /// config is invalid or induces a cycle in the precedence DAG.
195    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, &degree) 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        // Machine 0 order [0,3,5,8,9,11] => [0,0,0,0,0,0]
347        // Machine 1 order [2,7,1,6,10,4] => [1,3,0,1,1,0]
348        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;