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        category: crate::registry::ProblemCategory::Misc,
18        module_path: module_path!(),
19        description: "Determine if a flow-shop schedule for jobs on m processors meets a deadline",
20        fields: &[
21            FieldInfo { name: "num_processors", type_name: "usize", description: "Number of machines m" },
22            FieldInfo { name: "task_lengths", type_name: "Vec<Vec<i64>>", description: "task_lengths[j][i] = length of job j's task on machine i" },
23            FieldInfo { name: "deadline", type_name: "i64", description: "Global deadline D" },
24        ],
25    }
26}
27
28/// The Flow Shop Scheduling problem.
29///
30/// Given `m` processors and a set of `n` jobs, each job `j` consists of `m` tasks
31/// `t_1[j], t_2[j], ..., t_m[j]` with specified lengths. Tasks must be processed
32/// in processor order: job `j` cannot start on machine `i+1` until its task on
33/// machine `i` is completed. The question is whether there exists a schedule such
34/// that all jobs complete by deadline `D`.
35///
36/// # Representation
37///
38/// Configurations use Lehmer code encoding with `dims() = [n, n-1, ..., 1]`.
39/// A config `[c_0, c_1, ..., c_{n-1}]` where `c_i < n - i` is decoded by
40/// maintaining a list of available jobs and picking the `c_i`-th element:
41///
42/// For 3 jobs, config `[2, 0, 0]`: available=`[0,1,2]`, pick index 2 → job 2;
43/// available=`[0,1]`, pick index 0 → job 0; available=`[1]`, pick index 0 → job 1.
44/// Result: job order `[2, 0, 1]`.
45///
46/// Given a job order, start times are determined greedily (as early as possible).
47///
48/// # Example
49///
50/// ```
51/// use problemreductions::models::misc::FlowShopScheduling;
52/// use problemreductions::{Problem, BruteForce};
53///
54/// // 2 machines, 3 jobs, deadline 10
55/// let problem = FlowShopScheduling::new(2, vec![vec![2, 3], vec![3, 2], vec![1, 4]], 10);
56/// let solver = BruteForce::new();
57/// let solution = solver.solve(&problem).unwrap();
58/// assert!(solution.is_some());
59/// ```
60#[derive(Debug, Clone, Serialize, Deserialize)]
61#[serde(try_from = "FlowShopSchedulingData")]
62pub struct FlowShopScheduling {
63    /// Number of processors (machines).
64    num_processors: usize,
65    /// Task lengths: `task_lengths[j][i]` is the processing time of job `j` on machine `i`.
66    task_lengths: Vec<Vec<i64>>,
67    /// Global deadline.
68    deadline: i64,
69}
70
71#[derive(Deserialize)]
72struct FlowShopSchedulingData {
73    num_processors: usize,
74    task_lengths: Vec<Vec<i64>>,
75    deadline: i64,
76}
77
78impl TryFrom<FlowShopSchedulingData> for FlowShopScheduling {
79    type Error = crate::registry::ConstructionError;
80
81    fn try_from(data: FlowShopSchedulingData) -> Result<Self, Self::Error> {
82        Self::try_new(data.num_processors, data.task_lengths, data.deadline)
83    }
84}
85
86impl FlowShopScheduling {
87    /// Create a new Flow Shop Scheduling instance.
88    ///
89    /// # Arguments
90    /// * `num_processors` - Number of machines m
91    /// * `task_lengths` - `task_lengths[j][i]` = processing time of job j on machine i.
92    ///   Each inner Vec must have length `num_processors`.
93    /// * `deadline` - Global deadline D
94    ///
95    /// # Panics
96    /// Panics if any job does not have exactly `num_processors` tasks.
97    pub fn new(num_processors: usize, task_lengths: Vec<Vec<i64>>, deadline: i64) -> Self {
98        Self::try_new(num_processors, task_lengths, deadline)
99            .unwrap_or_else(|error| panic!("{error}"))
100    }
101
102    fn try_new(
103        num_processors: usize,
104        task_lengths: Vec<Vec<i64>>,
105        deadline: i64,
106    ) -> Result<Self, crate::registry::ConstructionError> {
107        for (j, tasks) in task_lengths.iter().enumerate() {
108            if tasks.len() != num_processors {
109                return Err(format!(
110                    "Job {} has {} tasks, expected {}",
111                    j,
112                    tasks.len(),
113                    num_processors
114                )
115                .into());
116            }
117        }
118        if task_lengths.iter().flatten().any(|&length| length < 0) {
119            return Err("task lengths must be nonnegative".into());
120        }
121        if deadline < 0 {
122            return Err("deadline must be nonnegative".into());
123        }
124        Ok(Self {
125            num_processors,
126            task_lengths,
127            deadline,
128        })
129    }
130
131    /// Get the number of processors.
132    pub fn num_processors(&self) -> usize {
133        self.num_processors
134    }
135
136    /// Get the task lengths matrix.
137    pub fn task_lengths(&self) -> &[Vec<i64>] {
138        &self.task_lengths
139    }
140
141    /// Get the deadline.
142    pub fn deadline(&self) -> i64 {
143        self.deadline
144    }
145
146    /// Smallest h >= 1 with every processing time and the deadline strictly below 2^h.
147    pub fn max_time_bits(&self) -> u64 {
148        crate::types::max_numeric_magnitude_bits(
149            self.task_lengths
150                .iter()
151                .flatten()
152                .copied()
153                .chain(std::iter::once(self.deadline)),
154        )
155    }
156
157    /// Get the number of jobs.
158    pub fn num_jobs(&self) -> usize {
159        self.task_lengths.len()
160    }
161
162    /// Compute the makespan for a given job ordering.
163    ///
164    /// The job_order slice must be a permutation of `0..num_jobs`.
165    /// Returns the completion time of the last job on the last machine.
166    pub fn compute_makespan(
167        &self,
168        job_order: &[usize],
169    ) -> Result<i64, crate::traits::EvaluationError> {
170        let n = job_order.len();
171        let m = self.num_processors;
172        assert_eq!(
173            n,
174            self.task_lengths.len(),
175            "job_order length ({}) does not match num_jobs ({})",
176            n,
177            self.task_lengths.len()
178        );
179        for (k, &job) in job_order.iter().enumerate() {
180            assert!(
181                job < self.task_lengths.len(),
182                "job_order[{}] = {} is out of range (num_jobs = {})",
183                k,
184                job,
185                self.task_lengths.len()
186            );
187        }
188        if n == 0 || m == 0 {
189            return Ok(0);
190        }
191
192        // completion[k][i] = completion time of the k-th job in sequence on machine i
193        let mut completion = vec![vec![0i64; m]; n];
194
195        for (k, &job) in job_order.iter().enumerate() {
196            for i in 0..m {
197                let prev_machine = if i == 0 { 0 } else { completion[k][i - 1] };
198                let prev_job = if k == 0 { 0 } else { completion[k - 1][i] };
199                let start = prev_machine.max(prev_job);
200                completion[k][i] =
201                    start
202                        .checked_add(self.task_lengths[job][i])
203                        .ok_or_else(|| {
204                            crate::traits::EvaluationError::IntegerOverflow(
205                                "computing flow-shop completion time".to_string(),
206                            )
207                        })?;
208            }
209        }
210
211        Ok(completion[n - 1][m - 1])
212    }
213}
214
215impl Problem for FlowShopScheduling {
216    const NAME: &'static str = "FlowShopScheduling";
217    type Solution = Vec<usize>;
218    type Value = crate::types::Or;
219
220    crate::problem_parameters![
221        ("max_time_bits", max_time_bits),
222        ("num_jobs", num_jobs),
223        ("num_processors", num_processors),
224    ];
225
226    fn variant() -> Vec<(&'static str, &'static str)> {
227        crate::variant_params![]
228    }
229
230    fn evaluate(
231        &self,
232        config: &Self::Solution,
233    ) -> Result<crate::types::Or, crate::traits::EvaluationError> {
234        let n = self.num_jobs();
235        if config.len() != n {
236            return Err(crate::traits::EvaluationError::InvalidConfiguration(
237                "job ordering length does not match the jobs".into(),
238            ));
239        }
240        if config.iter().any(|&job| job >= n) {
241            return Err(crate::traits::EvaluationError::InvalidConfiguration(
242                "job ordering contains an out-of-range job".into(),
243            ));
244        }
245        Ok({
246            crate::types::Or({
247                let Some(job_order) = super::decode_permutation(config, self.num_jobs()) else {
248                    return Ok(crate::types::Or(false));
249                };
250
251                let makespan = self.compute_makespan(&job_order)?;
252                makespan <= self.deadline
253            })
254        })
255    }
256}
257
258impl crate::solvers::BruteForceProblem for FlowShopScheduling {
259    fn dimensions(&self) -> Vec<usize> {
260        super::lehmer_dims(self.num_jobs())
261    }
262}
263
264crate::declare_variants! {
265    default FlowShopScheduling => "factorial(num_jobs)",
266}
267
268crate::register_brute_force! {
269    FlowShopScheduling decode |problem: &FlowShopScheduling, indices: Vec<usize>| super::decode_lehmer(&indices, problem.num_jobs()).expect("enumerated Lehmer digits are valid"),
270}
271
272#[cfg(feature = "example-db")]
273pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::ModelExampleSpec> {
274    vec![crate::example_db::specs::ModelExampleSpec {
275        id: "flow_shop_scheduling",
276        instance: Box::new(FlowShopScheduling::new(
277            3,
278            vec![
279                vec![3, 4, 2],
280                vec![2, 3, 5],
281                vec![4, 1, 3],
282                vec![1, 5, 4],
283                vec![3, 2, 3],
284            ],
285            25,
286        )),
287        // Job order [3,0,4,2,1] = Lehmer code [3,0,2,1,0], makespan 23
288        optimal_config: serde_json::json!(vec![3, 0, 4, 2, 1]),
289        optimal_value: serde_json::json!(true),
290    }]
291}
292
293#[cfg(test)]
294#[path = "../../unit_tests/models/misc/flow_shop_scheduling.rs"]
295mod tests;