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

303 lines
11 KiB
Python
Raw Normal View History

2022-03-28 16:42:35 +02:00
#!/usr/bin/env python3
# Copyright 2010-2021 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.
"""Scheduling cooking tasks in a bakery using no-wait jobshop scheduling.
We are scheduling a full day of baking:
- Work starts at 4am.
- The shop closes at 10PM.
- Durations are in minutes. Time starts at midnight.
"""
import collections
from typing import Sequence
from absl import app
from absl import flags
from google.protobuf import text_format
from ortools.sat.python import cp_model
FLAGS = flags.FLAGS
2022-03-28 17:44:33 +02:00
flags.DEFINE_string('params', 'num_search_workers:16, max_time_in_seconds:30',
2022-03-28 16:42:35 +02:00
'Sat solver parameters.')
flags.DEFINE_string('proto_file', '',
'If not empty, output the proto to this file.')
# Recipes
CROISSANT = 'croissant'
APPLE_PIE = 'apple pie'
BRIOCHE = 'brioche'
CHOCOLATE_CAKE = 'chocolate cake'
# Skills
BAKING = 'baking'
PROOFING = 'proofing'
COOKING = 'cooking'
COOLING = 'cooling'
DECORATING = 'decorating'
DISPLAY = 'display'
class Task(object):
"""A unit baking task.
- Simple baking tasks have a fixed duration. They are performed by workers.
- Waiting/cooling/proofing tasks have a min and a max duration.
2022-03-28 17:44:33 +02:00
They are performed by machine or they use space resources.
2022-03-28 16:42:35 +02:00
"""
def __init__(self, name, min_duration, max_duration):
self.name = name
self.min_duration = min_duration
self.max_duration = max_duration
class Skill(object):
2022-03-28 17:44:33 +02:00
"""The skill of a worker or the capability of a machine."""
2022-03-28 16:42:35 +02:00
def __init__(self, name, efficiency):
self.name = name
2022-03-28 17:44:33 +02:00
# Efficiency is currently not used.
2022-03-28 16:42:35 +02:00
self.efficiency = efficiency
class Recipe(object):
2022-03-28 17:44:33 +02:00
"""A recipe is a sequence of cooking tasks."""
2022-03-28 16:42:35 +02:00
def __init__(self, name):
self.name = name
self.tasks = []
def add_task(self, resource_name, min_duration, max_duration):
self.tasks.append(Task(resource_name, min_duration, max_duration))
return self
class Resource(object):
"""A resource is a worker, a machine, or just some space for cakes to rest.
- Workers have a capacity of 1 and can have variable efficiency.
- Machines and spaces have a capacity greater or equal to one, but the
efficiency is fixed to 100.
For a worker with efficiency k and a task of duration t, the resulting
work will have a duration `ceil(t * k)`.
"""
def __init__(self, name, capacity):
self.name = name
self.capacity = capacity
self.skills = []
def add_skill(self, skill_name, efficiency):
self.skills.append(Skill(skill_name, efficiency))
return self
class Order(object):
"""An order is a recipe that should be delivered at a given due date."""
def __init__(self, unique_id, recipe_name, due_date, quantity):
"""Builds an order.
Args:
unique_id: A unique identifier for the order. Used to display the result.
2022-03-28 17:44:33 +02:00
recipe_name: The name of the recipe. It must match one of the recipes.
2022-03-28 16:42:35 +02:00
due_date: The due date in minutes since midnight.
quantity: How many cakes to prepare.
"""
self.unique_id = unique_id
self.recipe_name = recipe_name
self.due_date = due_date
self.quantity = quantity
def set_up_data():
"""Set up the bakery problem data."""
# Recipes.
croissant_recipe = Recipe(CROISSANT)
croissant_recipe.add_task(BAKING, 15, 15)
croissant_recipe.add_task(PROOFING, 60, 90)
croissant_recipe.add_task(COOKING, 20, 20)
croissant_recipe.add_task(DISPLAY, 5, 5 * 60)
apple_pie_recipe = Recipe(APPLE_PIE)
apple_pie_recipe.add_task(BAKING, 25, 25)
apple_pie_recipe.add_task(PROOFING, 15, 60)
apple_pie_recipe.add_task(COOKING, 30, 30)
apple_pie_recipe.add_task(DECORATING, 5, 5)
apple_pie_recipe.add_task(DISPLAY, 5, 5 * 60)
brioche_recipe = Recipe(BRIOCHE)
brioche_recipe.add_task(BAKING, 20, 20)
brioche_recipe.add_task(PROOFING, 60, 90)
brioche_recipe.add_task(COOKING, 30, 30)
brioche_recipe.add_task(DISPLAY, 5, 5 * 60)
chocolate_cake_recipe = Recipe(CHOCOLATE_CAKE)
chocolate_cake_recipe.add_task(BAKING, 15, 15)
chocolate_cake_recipe.add_task(COOKING, 25, 25)
chocolate_cake_recipe.add_task(DECORATING, 15, 15)
chocolate_cake_recipe.add_task(DISPLAY, 5, 5 * 60)
recipes = [
croissant_recipe, apple_pie_recipe, brioche_recipe,
chocolate_cake_recipe
]
# Resources.
baker1 = Resource('baker1', 1).add_skill(BAKING, 1.0)
baker2 = Resource('baker2', 1).add_skill(BAKING, 1.0)
decorator1 = Resource('decorator1', 1).add_skill(DECORATING, 1.0)
2022-03-28 17:44:33 +02:00
waiting_space = Resource('waiting_space', 4).add_skill(PROOFING, 1.0)
oven = Resource('oven', 4).add_skill(COOKING, 1.0)
display_space = Resource('display_space', 12).add_skill(DISPLAY, 1.0)
2022-03-28 16:42:35 +02:00
resources = [baker1, baker2, decorator1, waiting_space, oven, display_space]
# Orders
croissant_7am = Order('croissant_7am', CROISSANT, 7 * 60, 3)
croissant_8am = Order('croissant_8am', CROISSANT, 8 * 60, 3)
croissant_9am = Order('croissant_9am', CROISSANT, 9 * 60, 2)
croissant_10am = Order('croissant_10am', CROISSANT, 10 * 60, 1)
croissant_11am = Order('croissant_11am', CROISSANT, 11 * 60, 1)
2022-03-28 17:44:33 +02:00
brioche_10am = Order('brioche_10am', BRIOCHE, 10 * 60, 8)
brioche_12pm = Order('brioche_12pm', BRIOCHE, 12 * 60, 8)
2022-03-28 16:42:35 +02:00
apple_pie_1pm = Order('apple_pie_1pm', APPLE_PIE, 13 * 60, 10)
2022-03-28 17:44:33 +02:00
chocolate_4pm = Order('chocolate_4pm', CHOCOLATE_CAKE, 16 * 60, 10)
2022-03-28 16:42:35 +02:00
orders = [
croissant_7am, croissant_8am, croissant_9am, croissant_10am,
croissant_11am, brioche_10am, brioche_12pm, apple_pie_1pm, chocolate_4pm
]
return recipes, resources, orders
def solve_with_cp_sat(recipes, resources, orders):
"""Build the optimization model, and solve the problem."""
model = cp_model.CpModel()
horizon = 22 * 60 # 10PM.
start_work = 4 * 60 # 4am.
# Parse recipes.
recipe_by_name = {}
for recipe in recipes:
recipe_by_name[recipe.name] = recipe
# Parse resources.
resource_by_name = {}
resource_list_by_skill_name = collections.defaultdict(list)
for resource in resources:
resource_by_name[resource.name] = resource
for skill in resource.skills:
resource_list_by_skill_name[skill.name].append(resource)
# Parse orders and create one optional copy per eligible resource and per
# task.
interval_list_by_resource_name = collections.defaultdict(list)
2022-03-28 17:44:33 +02:00
orders_sequence_of_events = collections.defaultdict(list)
2022-03-28 16:42:35 +02:00
sorted_orders = []
2022-03-28 17:44:33 +02:00
tardiness_vars = []
2022-03-28 16:42:35 +02:00
for order in orders:
for batch in range(order.quantity):
order_id = f'{order.unique_id}_{batch}'
sorted_orders.append(order_id)
previous_end = None
due_date = order.due_date
recipe = recipe_by_name[order.recipe_name]
for task in recipe.tasks:
skill_name = task.name
suffix = f'_{order.unique_id}_batch{batch}_{skill_name}'
start = None
if previous_end is None:
start = model.NewIntVar(start_work, horizon,
f'start{suffix}')
2022-03-28 17:44:33 +02:00
orders_sequence_of_events[order_id].append(
(start, f'start{suffix}'))
2022-03-28 16:42:35 +02:00
else:
start = previous_end
size = model.NewIntVar(task.min_duration, task.max_duration,
f'size{suffix}')
end = None
if task == recipe.tasks[-1]:
2022-03-28 17:44:33 +02:00
# The order must end after the due_date. Ideally, exactly at the
# due_date.
tardiness = model.NewIntVar(0, horizon - due_date,
f'end{suffix}')
end = tardiness + due_date
# Store the end_var for the objective.
tardiness_vars.append(tardiness)
2022-03-28 16:42:35 +02:00
else:
end = model.NewIntVar(start_work, horizon, f'end{suffix}')
2022-03-28 17:44:33 +02:00
orders_sequence_of_events[order_id].append(
(end, f'end{suffix}'))
2022-03-28 16:42:35 +02:00
previous_end = end
# Per resource copy.
presence_literals = []
for resource in resource_list_by_skill_name[skill_name]:
presence = model.NewBoolVar(
f'presence{suffix}_{resource.name}')
copy = model.NewOptionalIntervalVar(
start, size, end, presence,
f'interval{suffix}_{resource.name}')
interval_list_by_resource_name[resource.name].append(copy)
presence_literals.append(presence)
# Only one copy will be performed.
model.AddExactlyOne(presence_literals)
# Create resource constraints.
for resource in resources:
intervals = interval_list_by_resource_name[resource.name]
if resource.capacity == 1:
model.AddNoOverlap(intervals)
else:
model.AddCumulative(intervals, [1] * len(intervals),
resource.capacity)
# The objective is to minimize the sum of the tardiness values of each jobs.
# The tardiness is difference between the end time of an order and its
# due date.
2022-03-28 17:44:33 +02:00
model.Minimize(sum(tardiness_vars))
2022-03-28 16:42:35 +02:00
# Solve model.
solver = cp_model.CpSolver()
if FLAGS.params:
text_format.Parse(FLAGS.params, solver.parameters)
solver.parameters.log_search_progress = True
status = solver.Solve(model)
2022-03-28 17:44:33 +02:00
if status == cp_model.OPTIMAL or status == cp_model.FEASIBLE:
2022-03-28 16:42:35 +02:00
for order_id in sorted_orders:
print(f'{order_id}:')
2022-03-28 17:44:33 +02:00
for time_expr, event_id in orders_sequence_of_events[order_id]:
time = solver.Value(time_expr)
print(f' {event_id} at {time // 60}:{time % 60:02}')
2022-03-28 16:42:35 +02:00
def main(argv: Sequence[str]) -> None:
if len(argv) > 1:
raise app.UsageError('Too many command-line arguments.')
recipes, resources, orders = set_up_data()
solve_with_cp_sat(recipes, resources, orders)
if __name__ == '__main__':
app.run(main)