14. Scheduling
14.1. Setup
We start by downloading the unified planning library.
[ ]:
%pip install unified-planning[aries]
14.2. A scheduling primer
unified-planning
provides support for modeling scheduling problems. It heavily relies on all the building blocks of planning problems and in particular:
reuse state definition (
Type
,Fluent
, initial state, (timed) goals, …)replace
Action
withActivity
add syntactic sugar for common patterns in scheduling problems (resources, …)
[13]:
from unified_planning.shortcuts import *
from unified_planning.model.scheduling import SchedulingProblem
# Create an empty problem called factory
problem = SchedulingProblem("factory")
problem
[13]:
problem name = factory
fluents = [
]
initial fluents default = [
]
initial values = [
]
BASE: {
}
Activities:
14.3. Resources as numeric fluents
A SchedulingProblem
allows boolean, symbolic and numeric fluents to state representation, just like a regular planning problem.
In addition, it exposes an `add_resource
<https://unified-planning.readthedocs.io/en/latest/api/model/scheduling/SchedulingProblem.html#unified_planning.model.scheduling.SchedulingProblem.add_resource>`__ method that eases the definition of reusable resources:
[14]:
# Create two unary resources, one for each machine
machine1 = problem.add_resource("machine1", capacity=1)
machine2 = problem.add_resource("machine2", capacity=1)
# Create a resource with capacity 4 representing the available operators
operators = problem.add_resource("operators", capacity=4)
print(problem)
problem name = factory
fluents = [
integer[0, 1] machine1
integer[0, 1] machine2
integer[0, 4] operators
]
initial fluents default = [
integer[0, 1] machine1 := 1
integer[0, 1] machine2 := 1
integer[0, 4] operators := 4
]
initial values = [
]
BASE: {
}
Activities:
14.4. Activities
An `Activity
<https://unified-planning.readthedocs.io/en/latest/api/model/scheduling/Activity.html>`__ is essentially a durative action that appears exactly once in the solution.
[15]:
# Create an activity a1 that has a duration of 3 time units that uses the machine 1
a1 = problem.add_activity("a1", duration=3)
a1.uses(machine1, amount=1)
print(a1)
a1 {
duration = [3, 3]
effects = [
start(a1):
machine1 -= 1:
end(a1):
machine1 += 1:
]
}
[16]:
# Specify that activity a1 requires 2 operators to be executed
a1.uses(operators, amount=2)
print(a1)
a1 {
duration = [3, 3]
effects = [
start(a1):
machine1 -= 1:
operators -= 2:
end(a1):
machine1 += 1:
operators += 2:
]
}
[17]:
# Create a new activity a2 that lasts 6 time units and require machine2 and 1 operator
a2 = problem.add_activity("a2", duration=6)
a2.uses(operators) # default usage is 1
a2.uses(machine2)
# Require that activity be finished by time unit 14
a2.add_deadline(14)
a2
[17]:
a2 {
duration = [6, 6]
constraints = [
(end(a2) <= 14)
]
effects = [
start(a2):
operators -= 1:
machine2 -= 1:
end(a2):
operators += 1:
machine2 += 1:
]
}
[23]:
# finish a2 strictly before starting a1
problem.add_constraint(LT(a2.end, a1.start))
# One worker is unavailable over [17, 25)
problem.add_decrease_effect(17, operators, 1)
problem.add_increase_effect(25, operators, 1)
problem
[23]:
problem name = factory
fluents = [
integer[0, 1] machine1
integer[0, 1] machine2
integer[0, 4] operators
]
initial fluents default = [
integer[0, 1] machine1 := 1
integer[0, 1] machine2 := 1
integer[0, 4] operators := 4
]
initial values = [
machine1 := 1
machine2 := 1
operators := 4
]
quality metrics = [
minimize makespan
]
BASE: {
constraints = [
(end(a2) < start(a1))
]
effects = [
start + 17:
operators -= 1:
operators -= 1:
start + 25:
operators += 1:
operators += 1:
]
}
Activities:
a1 {
duration = [3, 3]
effects = [
start(a1):
machine1 -= 1:
operators -= 2:
end(a1):
machine1 += 1:
operators += 2:
]
}
a2 {
duration = [6, 6]
constraints = [
(end(a2) <= 14)
]
effects = [
start(a2):
operators -= 1:
machine2 -= 1:
end(a2):
operators += 1:
machine2 += 1:
]
}
As a last step, lets just specify that we want the makespan to be minimized.
[19]:
problem.add_quality_metric(MinimizeMakespan())
14.5. Solving scheduling problems
Like all problems in the UP, we can access the kind
field that is automatically computed to reflect the features in the problem.
[20]:
print(problem.kind)
PROBLEM_CLASS: ['SCHEDULING']
PROBLEM_TYPE: ['SIMPLE_NUMERIC_PLANNING']
TIME: ['TIMED_EFFECTS', 'DISCRETE_TIME']
EXPRESSION_DURATION: ['INT_TYPE_DURATIONS']
NUMBERS: ['BOUNDED_TYPES']
EFFECTS_KIND: ['DECREASE_EFFECTS', 'INCREASE_EFFECTS']
FLUENTS_TYPE: ['INT_FLUENTS']
QUALITY_METRICS: ['MAKESPAN']
Currently, the only solver supporting scheduling problems is aries. When asking for a oneshot solver, it would automatically be selected to solve the problem.
[21]:
with OneshotPlanner(problem_kind=problem.kind) as planner:
res = planner.solve(problem)
print(res)
status: SOLVED_OPTIMALLY
engine: aries
plan: Schedule:
[0, 6] a2
[7, 10] a1
As you can see, all activities are present in the solution, with a2
finishing strictly before starting a1
as imposed in the problem’s constraints.
14.6. Going Further
Reference: Complete parser for jobshop (with operators) problems