Files
ortools-clone/examples/python/jobshop_with_maintenance_sat.py

163 lines
5.7 KiB
Python
Raw Permalink Normal View History

#!/usr/bin/env python3
2025-01-10 11:35:44 +01:00
# Copyright 2010-2025 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.
2023-07-01 06:06:53 +02:00
"""Jobshop with maintenance tasks using the CP-SAT solver."""
import collections
from typing import Sequence
from absl import app
from ortools.sat.python import cp_model
2020-12-03 16:56:36 +01:00
class SolutionPrinter(cp_model.CpSolverSolutionCallback):
"""Print intermediate solutions."""
def __init__(self) -> None:
2020-12-03 16:56:36 +01:00
cp_model.CpSolverSolutionCallback.__init__(self)
self.__solution_count = 0
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(
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
def jobshop_with_maintenance() -> None:
"""Solves a jobshop with maintenance on one machine."""
2018-12-30 12:32:20 +01:00
# Create the model.
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-30 10:26:39 +01:00
machines_count = 1 + max(task[0] for job in jobs_data for task in job)
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)
# 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"
)
# 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
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-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-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])
# 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
)
# 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)
# 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)
# 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]
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-30 12:21:57 +01:00
# Create per machine output lines.
2023-07-01 06:06:53 +02:00
output = ""
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 = " "
for assigned_task in assigned_jobs[machine]:
name = f"job_{assigned_task.job}_{assigned_task.index}"
2023-11-16 19:46:56 +01:00
# add spaces to output to align columns.
sol_line_tasks += f"{name:>10}"
start = assigned_task.start
2018-12-30 12:08:08 +01:00
duration = assigned_task.duration
2018-12-30 12:03:43 +01:00
sol_tmp = f"[{start}, {start + duration}]"
2023-11-16 19:46:56 +01:00
# add spaces to output to align columns.
sol_line += f"{sol_tmp:>10}"
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
# Finally print the solution found.
print(f"Optimal Schedule Length: {solver.objective_value}")
2018-12-30 12:03:43 +01:00
print(output)
print(solver.response_stats())
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.")
jobshop_with_maintenance()
2023-07-01 06:06:53 +02:00
if __name__ == "__main__":
app.run(main)