Skip to main content

problemreductions/models/misc/
flow_shop_scheduling.rs

1//! Flow Shop Scheduling problem implementation.
2//!
3//! Given m processors and a set of jobs, each consisting of m tasks (one per processor)
4//! that must be processed in processor order 1, 2, ..., m, determine if all jobs can
5//! be completed by a global deadline D.
6
7use crate::registry::{FieldInfo, ProblemSchemaEntry};
8use crate::traits::Problem;
9use serde::{Deserialize, Serialize};
10
11inventory::submit! {
12    ProblemSchemaEntry {
13        name: "FlowShopScheduling",
14        display_name: "Flow Shop Scheduling",
15        aliases: &[],
16        dimensions: &[],
17        module_path: module_path!(),
18        description: "Determine if a flow-shop schedule for jobs on m processors meets a deadline",
19        fields: &[
20            FieldInfo { name: "num_processors", type_name: "usize", description: "Number of machines m" },
21            FieldInfo { name: "task_lengths", type_name: "Vec<Vec<u64>>", description: "task_lengths[j][i] = length of job j's task on machine i" },
22            FieldInfo { name: "deadline", type_name: "u64", description: "Global deadline D" },
23        ],
24    }
25}
26
27/// The Flow Shop Scheduling problem.
28///
29/// Given `m` processors and a set of `n` jobs, each job `j` consists of `m` tasks
30/// `t_1[j], t_2[j], ..., t_m[j]` with specified lengths. Tasks must be processed
31/// in processor order: job `j` cannot start on machine `i+1` until its task on
32/// machine `i` is completed. The question is whether there exists a schedule such
33/// that all jobs complete by deadline `D`.
34///
35/// # Representation
36///
37/// Configurations use Lehmer code encoding with `dims() = [n, n-1, ..., 1]`.
38/// A config `[c_0, c_1, ..., c_{n-1}]` where `c_i < n - i` is decoded by
39/// maintaining a list of available jobs and picking the `c_i`-th element:
40///
41/// For 3 jobs, config `[2, 0, 0]`: available=`[0,1,2]`, pick index 2 → job 2;
42/// available=`[0,1]`, pick index 0 → job 0; available=`[1]`, pick index 0 → job 1.
43/// Result: job order `[2, 0, 1]`.
44///
45/// Given a job order, start times are determined greedily (as early as possible).
46///
47/// # Example
48///
49/// ```
50/// use problemreductions::models::misc::FlowShopScheduling;
51/// use problemreductions::{Problem, Solver, BruteForce};
52///
53/// // 2 machines, 3 jobs, deadline 10
54/// let problem = FlowShopScheduling::new(2, vec![vec![2, 3], vec![3, 2], vec![1, 4]], 10);
55/// let solver = BruteForce::new();
56/// let solution = solver.find_witness(&problem);
57/// assert!(solution.is_some());
58/// ```
59#[derive(Debug, Clone, Serialize, Deserialize)]
60pub struct FlowShopScheduling {
61    /// Number of processors (machines).
62    num_processors: usize,
63    /// Task lengths: `task_lengths[j][i]` is the processing time of job `j` on machine `i`.
64    task_lengths: Vec<Vec<u64>>,
65    /// Global deadline.
66    deadline: u64,
67}
68
69impl FlowShopScheduling {
70    /// Create a new Flow Shop Scheduling instance.
71    ///
72    /// # Arguments
73    /// * `num_processors` - Number of machines m
74    /// * `task_lengths` - task_lengths[j][i] = processing time of job j on machine i.
75    ///   Each inner Vec must have length `num_processors`.
76    /// * `deadline` - Global deadline D
77    ///
78    /// # Panics
79    /// Panics if any job does not have exactly `num_processors` tasks.
80    pub fn new(num_processors: usize, task_lengths: Vec<Vec<u64>>, deadline: u64) -> Self {
81        for (j, tasks) in task_lengths.iter().enumerate() {
82            assert_eq!(
83                tasks.len(),
84                num_processors,
85                "Job {} has {} tasks, expected {}",
86                j,
87                tasks.len(),
88                num_processors
89            );
90        }
91        Self {
92            num_processors,
93            task_lengths,
94            deadline,
95        }
96    }
97
98    /// Get the number of processors.
99    pub fn num_processors(&self) -> usize {
100        self.num_processors
101    }
102
103    /// Get the task lengths matrix.
104    pub fn task_lengths(&self) -> &[Vec<u64>] {
105        &self.task_lengths
106    }
107
108    /// Get the deadline.
109    pub fn deadline(&self) -> u64 {
110        self.deadline
111    }
112
113    /// Get the number of jobs.
114    pub fn num_jobs(&self) -> usize {
115        self.task_lengths.len()
116    }
117
118    /// Compute the makespan for a given job ordering.
119    ///
120    /// The job_order slice must be a permutation of `0..num_jobs`.
121    /// Returns the completion time of the last job on the last machine.
122    pub fn compute_makespan(&self, job_order: &[usize]) -> u64 {
123        let n = job_order.len();
124        let m = self.num_processors;
125        assert_eq!(
126            n,
127            self.task_lengths.len(),
128            "job_order length ({}) does not match num_jobs ({})",
129            n,
130            self.task_lengths.len()
131        );
132        for (k, &job) in job_order.iter().enumerate() {
133            assert!(
134                job < self.task_lengths.len(),
135                "job_order[{}] = {} is out of range (num_jobs = {})",
136                k,
137                job,
138                self.task_lengths.len()
139            );
140        }
141        if n == 0 || m == 0 {
142            return 0;
143        }
144
145        // completion[k][i] = completion time of the k-th job in sequence on machine i
146        let mut completion = vec![vec![0u64; m]; n];
147
148        for (k, &job) in job_order.iter().enumerate() {
149            for i in 0..m {
150                let prev_machine = if i == 0 { 0 } else { completion[k][i - 1] };
151                let prev_job = if k == 0 { 0 } else { completion[k - 1][i] };
152                let start = prev_machine.max(prev_job);
153                completion[k][i] = start + self.task_lengths[job][i];
154            }
155        }
156
157        completion[n - 1][m - 1]
158    }
159}
160
161impl Problem for FlowShopScheduling {
162    const NAME: &'static str = "FlowShopScheduling";
163    type Value = crate::types::Or;
164
165    fn variant() -> Vec<(&'static str, &'static str)> {
166        crate::variant_params![]
167    }
168
169    fn dims(&self) -> Vec<usize> {
170        super::lehmer_dims(self.num_jobs())
171    }
172
173    fn evaluate(&self, config: &[usize]) -> crate::types::Or {
174        crate::types::Or({
175            let Some(job_order) = super::decode_lehmer(config, self.num_jobs()) else {
176                return crate::types::Or(false);
177            };
178
179            let makespan = self.compute_makespan(&job_order);
180            makespan <= self.deadline
181        })
182    }
183}
184
185crate::declare_variants! {
186    default FlowShopScheduling => "factorial(num_jobs)",
187}
188
189#[cfg(feature = "example-db")]
190pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::ModelExampleSpec> {
191    vec![crate::example_db::specs::ModelExampleSpec {
192        id: "flow_shop_scheduling",
193        instance: Box::new(FlowShopScheduling::new(
194            3,
195            vec![
196                vec![3, 4, 2],
197                vec![2, 3, 5],
198                vec![4, 1, 3],
199                vec![1, 5, 4],
200                vec![3, 2, 3],
201            ],
202            25,
203        )),
204        // Job order [3,0,4,2,1] = Lehmer code [3,0,2,1,0], makespan 23
205        optimal_config: vec![3, 0, 2, 1, 0],
206        optimal_value: serde_json::json!(true),
207    }]
208}
209
210#[cfg(test)]
211#[path = "../../unit_tests/models/misc/flow_shop_scheduling.rs"]
212mod tests;