2021-05-03 12:11:39 +02:00
|
|
|
#!/usr/bin/env python3
|
2025-01-10 11:35:44 +01:00
|
|
|
# Copyright 2010-2025 Google LLC
|
2018-11-19 20:42:23 -08:00
|
|
|
# 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.
|
2023-07-01 06:06:53 +02:00
|
|
|
|
2018-11-19 20:42:23 -08:00
|
|
|
"""This model implements a variation of the ft06 jobshop.
|
|
|
|
|
|
|
|
|
|
A jobshop is a standard scheduling problem when you must sequence a
|
|
|
|
|
series of tasks on a set of machines. Each job contains one task per
|
|
|
|
|
machine. The order of execution and the length of each job on each
|
|
|
|
|
machine is task dependent.
|
|
|
|
|
|
|
|
|
|
The objective is to minimize the maximum completion time of all
|
|
|
|
|
jobs. This is called the makespan.
|
|
|
|
|
|
|
|
|
|
This variation introduces a minimum distance between all the jobs on each
|
|
|
|
|
machine.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
import collections
|
|
|
|
|
|
|
|
|
|
from ortools.sat.python import cp_model
|
|
|
|
|
|
|
|
|
|
|
2023-11-16 19:46:56 +01:00
|
|
|
def distance_between_jobs(x: int, y: int) -> int:
|
2018-11-19 20:42:23 -08:00
|
|
|
"""Returns the distance between tasks of job x and tasks of job y."""
|
|
|
|
|
return abs(x - y)
|
|
|
|
|
|
|
|
|
|
|
2024-07-23 14:07:41 +02:00
|
|
|
def jobshop_ft06_distance() -> None:
|
2018-11-19 20:42:23 -08:00
|
|
|
"""Solves the ft06 jobshop with distances between tasks."""
|
|
|
|
|
# Creates the model.
|
|
|
|
|
model = cp_model.CpModel()
|
|
|
|
|
|
|
|
|
|
machines_count = 6
|
|
|
|
|
jobs_count = 6
|
|
|
|
|
all_machines = range(0, machines_count)
|
|
|
|
|
all_jobs = range(0, jobs_count)
|
|
|
|
|
|
2023-07-01 06:06:53 +02:00
|
|
|
durations = [
|
|
|
|
|
[1, 3, 6, 7, 3, 6],
|
|
|
|
|
[8, 5, 10, 10, 10, 4],
|
|
|
|
|
[5, 4, 8, 9, 1, 7],
|
|
|
|
|
[5, 5, 5, 3, 8, 9],
|
|
|
|
|
[9, 3, 5, 4, 3, 1],
|
|
|
|
|
[3, 3, 9, 10, 4, 1],
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
machines = [
|
|
|
|
|
[2, 0, 1, 3, 5, 4],
|
|
|
|
|
[1, 2, 4, 5, 0, 3],
|
|
|
|
|
[2, 3, 5, 0, 1, 4],
|
|
|
|
|
[1, 0, 2, 3, 4, 5],
|
|
|
|
|
[2, 1, 4, 5, 0, 3],
|
|
|
|
|
[1, 3, 5, 0, 4, 2],
|
|
|
|
|
]
|
2018-11-19 20:42:23 -08:00
|
|
|
|
|
|
|
|
# Computes horizon statically.
|
|
|
|
|
horizon = 150
|
|
|
|
|
|
2023-07-01 06:06:53 +02:00
|
|
|
task_type = collections.namedtuple("task_type", "start end interval")
|
2018-11-19 20:42:23 -08:00
|
|
|
|
|
|
|
|
# Creates jobs.
|
|
|
|
|
all_tasks = {}
|
|
|
|
|
for i in all_jobs:
|
|
|
|
|
for j in all_machines:
|
2024-07-25 15:46:57 -07:00
|
|
|
start_var = model.new_int_var(0, horizon, f"start_{i}_{j}")
|
2018-11-19 20:42:23 -08:00
|
|
|
duration = durations[i][j]
|
2024-07-25 15:46:57 -07:00
|
|
|
end_var = model.new_int_var(0, horizon, f"end_{i}_{j}")
|
2023-11-16 19:46:56 +01:00
|
|
|
interval_var = model.new_interval_var(
|
2024-07-25 15:46:57 -07:00
|
|
|
start_var, duration, end_var, f"interval_{i}_{j}"
|
2023-07-01 06:06:53 +02:00
|
|
|
)
|
|
|
|
|
all_tasks[(i, j)] = task_type(
|
|
|
|
|
start=start_var, end=end_var, interval=interval_var
|
|
|
|
|
)
|
2018-11-19 20:42:23 -08:00
|
|
|
|
|
|
|
|
# Create disjuctive constraints.
|
|
|
|
|
for i in all_machines:
|
|
|
|
|
job_intervals = []
|
|
|
|
|
job_indices = []
|
|
|
|
|
job_starts = []
|
|
|
|
|
job_ends = []
|
|
|
|
|
for j in all_jobs:
|
|
|
|
|
for k in all_machines:
|
|
|
|
|
if machines[j][k] == i:
|
|
|
|
|
job_intervals.append(all_tasks[(j, k)].interval)
|
|
|
|
|
job_indices.append(j)
|
|
|
|
|
job_starts.append(all_tasks[(j, k)].start)
|
|
|
|
|
job_ends.append(all_tasks[(j, k)].end)
|
2023-11-16 19:46:56 +01:00
|
|
|
model.add_no_overlap(job_intervals)
|
2018-11-19 20:42:23 -08:00
|
|
|
|
|
|
|
|
arcs = []
|
|
|
|
|
for j1 in range(len(job_intervals)):
|
|
|
|
|
# Initial arc from the dummy node (0) to a task.
|
2024-07-25 15:46:57 -07:00
|
|
|
start_lit = model.new_bool_var(f"{j1} is first job")
|
2023-07-10 14:38:58 +02:00
|
|
|
arcs.append((0, j1 + 1, start_lit))
|
2018-11-19 20:42:23 -08:00
|
|
|
# Final arc from an arc to the dummy node.
|
2024-07-25 15:46:57 -07:00
|
|
|
arcs.append((j1 + 1, 0, model.new_bool_var(f"{j1} is last job")))
|
2018-11-19 20:42:23 -08:00
|
|
|
|
|
|
|
|
for j2 in range(len(job_intervals)):
|
|
|
|
|
if j1 == j2:
|
|
|
|
|
continue
|
|
|
|
|
|
2024-07-25 15:46:57 -07:00
|
|
|
lit = model.new_bool_var(f"{j2} follows {j1}")
|
2023-07-10 14:38:58 +02:00
|
|
|
arcs.append((j1 + 1, j2 + 1, lit))
|
2018-11-19 20:42:23 -08:00
|
|
|
|
|
|
|
|
# We add the reified precedence to link the literal with the
|
|
|
|
|
# times of the two tasks.
|
|
|
|
|
min_distance = distance_between_jobs(j1, j2)
|
2023-11-16 19:46:56 +01:00
|
|
|
model.add(
|
|
|
|
|
job_starts[j2] >= job_ends[j1] + min_distance
|
|
|
|
|
).only_enforce_if(lit)
|
2018-11-19 20:42:23 -08:00
|
|
|
|
2023-11-16 19:46:56 +01:00
|
|
|
model.add_circuit(arcs)
|
2018-11-19 20:42:23 -08:00
|
|
|
|
|
|
|
|
# Precedences inside a job.
|
|
|
|
|
for i in all_jobs:
|
|
|
|
|
for j in range(0, machines_count - 1):
|
2023-11-16 19:46:56 +01:00
|
|
|
model.add(all_tasks[(i, j + 1)].start >= all_tasks[(i, j)].end)
|
2018-11-19 20:42:23 -08:00
|
|
|
|
|
|
|
|
# Makespan objective.
|
2023-11-16 19:46:56 +01:00
|
|
|
obj_var = model.new_int_var(0, horizon, "makespan")
|
|
|
|
|
model.add_max_equality(
|
2023-07-01 06:06:53 +02:00
|
|
|
obj_var, [all_tasks[(i, machines_count - 1)].end for i in all_jobs]
|
|
|
|
|
)
|
2023-11-16 19:46:56 +01:00
|
|
|
model.minimize(obj_var)
|
2018-11-19 20:42:23 -08:00
|
|
|
|
|
|
|
|
# Solve model.
|
|
|
|
|
solver = cp_model.CpSolver()
|
2023-11-16 19:46:56 +01:00
|
|
|
status = solver.solve(model)
|
2018-11-19 20:42:23 -08:00
|
|
|
|
|
|
|
|
# Output solution.
|
|
|
|
|
if status == cp_model.OPTIMAL:
|
2024-07-25 15:46:57 -07:00
|
|
|
print(f"Optimal makespan: {solver.objective_value}")
|
|
|
|
|
print(solver.response_stats())
|
2018-11-19 20:42:23 -08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
jobshop_ft06_distance()
|