From 56825086bcd5f0c5342d24ecf3d237339208129f Mon Sep 17 00:00:00 2001 From: Leon Lan Date: Sat, 15 Jun 2024 19:26:50 +0200 Subject: [PATCH] Permutation flow shop example --- examples/contrib/permutation_flow_shop.py | 177 ++++++++++++++++++++++ 1 file changed, 177 insertions(+) create mode 100644 examples/contrib/permutation_flow_shop.py diff --git a/examples/contrib/permutation_flow_shop.py b/examples/contrib/permutation_flow_shop.py new file mode 100644 index 0000000000..2299f208aa --- /dev/null +++ b/examples/contrib/permutation_flow_shop.py @@ -0,0 +1,177 @@ +#!/usr/bin/env python3 +# Copyright 2010-2024 Google LLC +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""This model implements the permutation flow shop problem (PFSP). + +In the PFSP, a set of jobs has to be processed on a set of machines. Each job +must be processed on each machine in sequence and all jobs have to be processed +in the same order on every machine. The objective is to minimize the makespan. +""" + +import argparse +from dataclasses import dataclass +from itertools import product + +import numpy as np +from ortools.sat.python import cp_model + + +@dataclass +class TaskType: + """ + Small wrapper to hold the start, end, and interval variables of a task. + """ + + start: cp_model.IntVar + end: cp_model.IntVar + interval: cp_model.IntervalVar + + +def permutation_flow_shop( + processing_times: np.ndarray, + time_limit: float = float("inf"), + log: bool = False, +): + """ + Solves the given permutation flow shop problem instance with OR-Tools. + + Parameters + ---------- + processing_times + An n-by-m matrix of processing times of the jobs on the machines. + time_limit + The time limit in seconds. If not set, the solver runs until an + optimal solution is found. + log + Whether to log the solver output. Default is False. + + Raises + ------ + ValueError + If the number of lines is greater than 1, i.e., the instance is a + distributed permutation flow shop problem. + """ + m = cp_model.CpModel() + num_jobs, num_machines = processing_times.shape + horizon = processing_times.sum() + + # Create interval variables for all tasks (each job/machine pair). + tasks = {} + for job, machine in product(range(num_jobs), range(num_machines)): + start = m.new_int_var(0, horizon, "") + end = m.new_int_var(0, horizon, "") + duration = processing_times[job][machine] + interval = m.new_interval_var(start, duration, end, "") + tasks[job, machine] = TaskType(start, end, interval) + + # No overlap for all job intervals on this machine. + for machine in range(num_machines): + intervals = [tasks[job, machine].interval for job in range(num_jobs)] + m.add_no_overlap(intervals) + + # Add precedence constraints between tasks of the same job. + for job, machine in product(range(num_jobs), range(num_machines - 1)): + pred = tasks[job, machine] + succ = tasks[job, machine + 1] + m.add(pred.end <= succ.start) + + # Create arcs for circuit constraints. + arcs = [] + for idx1 in range(num_jobs): + arcs.append((0, idx1 + 1, m.new_bool_var("start"))) + arcs.append((idx1 + 1, 0, m.new_bool_var("end"))) + + lits = {} + for idx1, idx2 in product(range(num_jobs), repeat=2): + if idx1 != idx2: + lit = m.new_bool_var(f"{idx1} -> {idx2}") + lits[idx1, idx2] = lit + arcs.append((idx1 + 1, idx2 + 1, lit)) + + m.add_circuit(arcs) + + # Enforce that the permutation of jobs is the same on all machines. + for machine in range(num_machines): + starts = [tasks[job, machine].start for job in range(num_jobs)] + ends = [tasks[job, machine].end for job in range(num_jobs)] + + for idx1, idx2 in product(range(num_jobs), repeat=2): + if idx1 == idx2: + continue + + # Since all machines share the same arc literals, if the literal + # i -> j is True, this enforces that job i is always scheduled + # before job j on all machines. + lit = lits[idx1, idx2] + m.add(ends[idx1] <= starts[idx2]).only_enforce_if(lit) + + # Set minimizing makespan as objective. + obj_var = m.new_int_var(0, horizon, "makespan") + completion_times = [ + tasks[(job, num_machines - 1)].end for job in range(num_jobs) + ] + m.add_max_equality(obj_var, completion_times) + m.minimize(obj_var) + + solver = cp_model.CpSolver() + solver.parameters.log_search_progress = log + solver.parameters.max_time_in_seconds = time_limit + + status_code = solver.Solve(m) + status = solver.StatusName(status_code) + + print(f"Status: {status}") + print(f"Makespan: {solver.ObjectiveValue()}") + + if status in ["OPTIMAL", "FEASIBLE"]: + start = [solver.Value(tasks[job, 0].start) for job in range(num_jobs)] + solution = np.argsort(start) + 1 + print(f"Solution: {solution}") + + +def parse_args(): + parser = argparse.ArgumentParser("Solve a permutation flow shop problem.") + + msg = "Time limit in seconds. Default is no time limit." + parser.add_argument( + "--time-limit", type=float, default=float("inf"), help=msg + ) + + msg = "Whether to log the solver output." + parser.add_argument("--log", action="store_true", help=msg) + + return parser.parse_args() + + +if __name__ == "__main__": + # VRF_10_5_2 instance from http://soa.iti.es/problem-instances. + # Optimal makespan is 698. + PROCESSING_TIMES = [ + [79, 67, 10, 48, 52], + [40, 40, 57, 21, 54], + [48, 93, 49, 11, 79], + [16, 23, 19, 2, 38], + [38, 90, 57, 73, 3], + [76, 13, 99, 98, 55], + [73, 85, 40, 20, 85], + [34, 6, 27, 53, 21], + [38, 6, 35, 28, 44], + [32, 11, 11, 34, 27], + ] + + args = parse_args() + + permutation_flow_shop( + np.array(PROCESSING_TIMES), args.time_limit, args.log + )