2023-01-29 21:20:58 +01:00
|
|
|
#!/usr/bin/env python3
|
2025-01-10 11:35:44 +01:00
|
|
|
# Copyright 2010-2025 Google LLC
|
2018-12-29 23:44:21 +01: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-12-29 23:44:21 +01:00
|
|
|
"""Jobshop with maintenance tasks using the CP-SAT solver."""
|
|
|
|
|
|
|
|
|
|
import collections
|
2023-01-29 21:20:58 +01:00
|
|
|
from typing import Sequence
|
|
|
|
|
from absl import app
|
2018-12-29 23:44:21 +01:00
|
|
|
from ortools.sat.python import cp_model
|
|
|
|
|
|
|
|
|
|
|
2020-12-03 16:56:36 +01:00
|
|
|
class SolutionPrinter(cp_model.CpSolverSolutionCallback):
|
|
|
|
|
"""Print intermediate solutions."""
|
|
|
|
|
|
2024-07-25 15:46:57 -07:00
|
|
|
def __init__(self) -> None:
|
2020-12-03 16:56:36 +01:00
|
|
|
cp_model.CpSolverSolutionCallback.__init__(self)
|
|
|
|
|
self.__solution_count = 0
|
|
|
|
|
|
2024-07-25 15:46:57 -07:00
|
|
|
def on_solution_callback(self) -> None:
|
2020-12-03 16:56:36 +01:00
|
|
|
"""Called at each new solution."""
|
2023-07-01 06:06:53 +02:00
|
|
|
print(
|
2024-07-25 15:46:57 -07:00
|
|
|
f"Solution {self.__solution_count}, time = {self.wall_time} s,"
|
|
|
|
|
f" objective = {self.objective_value}"
|
2023-07-01 06:06:53 +02:00
|
|
|
)
|
2020-12-03 16:56:36 +01:00
|
|
|
self.__solution_count += 1
|
|
|
|
|
|
|
|
|
|
|
2024-07-23 14:07:41 +02:00
|
|
|
def jobshop_with_maintenance() -> None:
|
2018-12-29 23:44:21 +01:00
|
|
|
"""Solves a jobshop with maintenance on one machine."""
|
2018-12-30 12:32:20 +01:00
|
|
|
# Create the model.
|
2018-12-29 23:44:21 +01:00
|
|
|
model = cp_model.CpModel()
|
|
|
|
|
|
2018-12-30 10:26:39 +01:00
|
|
|
jobs_data = [ # task = (machine_id, processing_time).
|
|
|
|
|
[(0, 3), (1, 2), (2, 2)], # Job0
|
|
|
|
|
[(0, 2), (2, 1), (1, 4)], # Job1
|
2020-12-03 16:56:36 +01:00
|
|
|
[(1, 4), (2, 3)], # Job2
|
2018-12-30 10:26:39 +01:00
|
|
|
]
|
2018-12-29 23:44:21 +01:00
|
|
|
|
2018-12-30 10:26:39 +01:00
|
|
|
machines_count = 1 + max(task[0] for job in jobs_data for task in job)
|
2018-12-29 23:44:21 +01:00
|
|
|
all_machines = range(machines_count)
|
|
|
|
|
|
|
|
|
|
# Computes horizon dynamically as the sum of all durations.
|
2018-12-30 10:26:39 +01:00
|
|
|
horizon = sum(task[1] for job in jobs_data for task in job)
|
2018-12-29 23:44:21 +01:00
|
|
|
|
|
|
|
|
# Named tuple to store information about created variables.
|
2024-07-24 14:54:57 -07:00
|
|
|
task_type = collections.namedtuple("task_type", "start end interval")
|
2018-12-30 12:30:29 +01:00
|
|
|
# Named tuple to manipulate solution information.
|
2023-07-01 06:06:53 +02:00
|
|
|
assigned_task_type = collections.namedtuple(
|
|
|
|
|
"assigned_task_type", "start job index duration"
|
|
|
|
|
)
|
2018-12-29 23:44:21 +01:00
|
|
|
|
|
|
|
|
# Creates job intervals and add to the corresponding machine lists.
|
|
|
|
|
all_tasks = {}
|
|
|
|
|
machine_to_intervals = collections.defaultdict(list)
|
|
|
|
|
|
2018-12-30 12:21:57 +01:00
|
|
|
for job_id, job in enumerate(jobs_data):
|
2023-11-16 19:46:56 +01:00
|
|
|
for entry in enumerate(job):
|
|
|
|
|
task_id, task = entry
|
2024-07-25 15:46:57 -07:00
|
|
|
machine, duration = task
|
2024-07-24 14:54:57 -07:00
|
|
|
suffix = f"_{job_id}_{task_id}"
|
2023-11-16 19:46:56 +01:00
|
|
|
start_var = model.new_int_var(0, horizon, "start" + suffix)
|
|
|
|
|
end_var = model.new_int_var(0, horizon, "end" + suffix)
|
|
|
|
|
interval_var = model.new_interval_var(
|
2023-07-01 06:06:53 +02:00
|
|
|
start_var, duration, end_var, "interval" + suffix
|
|
|
|
|
)
|
|
|
|
|
all_tasks[job_id, task_id] = task_type(
|
|
|
|
|
start=start_var, end=end_var, interval=interval_var
|
|
|
|
|
)
|
2018-12-30 12:21:57 +01:00
|
|
|
machine_to_intervals[machine].append(interval_var)
|
2018-12-29 23:44:21 +01:00
|
|
|
|
2018-12-30 12:21:57 +01:00
|
|
|
# Add maintenance interval (machine 0 is not available on time {4, 5, 6, 7}).
|
2023-11-16 19:46:56 +01:00
|
|
|
machine_to_intervals[0].append(model.new_interval_var(4, 4, 8, "weekend_0"))
|
2018-12-29 23:44:21 +01:00
|
|
|
|
2018-12-30 12:30:29 +01:00
|
|
|
# Create and add disjunctive constraints.
|
2018-12-30 12:21:57 +01:00
|
|
|
for machine in all_machines:
|
2023-11-16 19:46:56 +01:00
|
|
|
model.add_no_overlap(machine_to_intervals[machine])
|
2018-12-29 23:44:21 +01:00
|
|
|
|
|
|
|
|
# Precedences inside a job.
|
2018-12-30 12:21:57 +01:00
|
|
|
for job_id, job in enumerate(jobs_data):
|
|
|
|
|
for task_id in range(len(job) - 1):
|
2023-11-16 19:46:56 +01:00
|
|
|
model.add(
|
2023-07-01 06:06:53 +02:00
|
|
|
all_tasks[job_id, task_id + 1].start >= all_tasks[job_id, task_id].end
|
|
|
|
|
)
|
2018-12-29 23:44:21 +01: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[job_id, len(job) - 1].end for job_id, job in enumerate(jobs_data)],
|
|
|
|
|
)
|
2023-11-16 19:46:56 +01:00
|
|
|
model.minimize(obj_var)
|
2018-12-29 23:44:21 +01:00
|
|
|
|
|
|
|
|
# Solve model.
|
|
|
|
|
solver = cp_model.CpSolver()
|
2020-12-03 16:56:36 +01:00
|
|
|
solution_printer = SolutionPrinter()
|
2023-11-16 19:46:56 +01:00
|
|
|
status = solver.solve(model, solution_printer)
|
2018-12-29 23:44:21 +01:00
|
|
|
|
|
|
|
|
# Output solution.
|
|
|
|
|
if status == cp_model.OPTIMAL:
|
|
|
|
|
# Create one list of assigned tasks per machine.
|
|
|
|
|
assigned_jobs = collections.defaultdict(list)
|
2018-12-30 12:21:57 +01:00
|
|
|
for job_id, job in enumerate(jobs_data):
|
|
|
|
|
for task_id, task in enumerate(job):
|
2018-12-30 10:26:39 +01:00
|
|
|
machine = task[0]
|
2018-12-29 23:44:21 +01:00
|
|
|
assigned_jobs[machine].append(
|
2023-07-01 06:06:53 +02:00
|
|
|
assigned_task_type(
|
2023-11-16 19:46:56 +01:00
|
|
|
start=solver.value(all_tasks[job_id, task_id].start),
|
2023-07-01 06:06:53 +02:00
|
|
|
job=job_id,
|
|
|
|
|
index=task_id,
|
|
|
|
|
duration=task[1],
|
|
|
|
|
)
|
|
|
|
|
)
|
2018-12-29 23:44:21 +01:00
|
|
|
|
2018-12-30 12:21:57 +01:00
|
|
|
# Create per machine output lines.
|
2023-07-01 06:06:53 +02:00
|
|
|
output = ""
|
2018-12-29 23:44:21 +01:00
|
|
|
for machine in all_machines:
|
|
|
|
|
# Sort by starting time.
|
|
|
|
|
assigned_jobs[machine].sort()
|
2023-07-01 06:06:53 +02:00
|
|
|
sol_line_tasks = "Machine " + str(machine) + ": "
|
|
|
|
|
sol_line = " "
|
2018-12-29 23:44:21 +01:00
|
|
|
|
|
|
|
|
for assigned_task in assigned_jobs[machine]:
|
2024-07-25 15:46:57 -07:00
|
|
|
name = f"job_{assigned_task.job}_{assigned_task.index}"
|
2023-11-16 19:46:56 +01:00
|
|
|
# add spaces to output to align columns.
|
2024-07-25 15:46:57 -07:00
|
|
|
sol_line_tasks += f"{name:>10}"
|
2018-12-29 23:44:21 +01:00
|
|
|
start = assigned_task.start
|
2018-12-30 12:08:08 +01:00
|
|
|
duration = assigned_task.duration
|
2018-12-30 12:03:43 +01:00
|
|
|
|
2024-07-25 15:46:57 -07:00
|
|
|
sol_tmp = f"[{start}, {start + duration}]"
|
2023-11-16 19:46:56 +01:00
|
|
|
# add spaces to output to align columns.
|
2024-07-25 15:46:57 -07:00
|
|
|
sol_line += f"{sol_tmp:>10}"
|
2018-12-29 23:44:21 +01:00
|
|
|
|
2023-07-01 06:06:53 +02:00
|
|
|
sol_line += "\n"
|
|
|
|
|
sol_line_tasks += "\n"
|
2018-12-30 12:03:43 +01:00
|
|
|
output += sol_line_tasks
|
|
|
|
|
output += sol_line
|
2018-12-29 23:44:21 +01:00
|
|
|
|
2018-12-29 23:53:58 +01:00
|
|
|
# Finally print the solution found.
|
2024-07-25 15:46:57 -07:00
|
|
|
print(f"Optimal Schedule Length: {solver.objective_value}")
|
2018-12-30 12:03:43 +01:00
|
|
|
print(output)
|
2024-07-25 15:46:57 -07:00
|
|
|
print(solver.response_stats())
|
2018-12-29 23:44:21 +01:00
|
|
|
|
|
|
|
|
|
2023-01-29 21:20:58 +01:00
|
|
|
def main(argv: Sequence[str]) -> None:
|
|
|
|
|
if len(argv) > 1:
|
2023-07-01 06:06:53 +02:00
|
|
|
raise app.UsageError("Too many command-line arguments.")
|
2023-01-29 21:20:58 +01:00
|
|
|
jobshop_with_maintenance()
|
|
|
|
|
|
|
|
|
|
|
2023-07-01 06:06:53 +02:00
|
|
|
if __name__ == "__main__":
|
2023-01-29 21:20:58 +01:00
|
|
|
app.run(main)
|