improve rcpsp_sat model; fix python indent/comments
This commit is contained in:
@@ -25,9 +25,9 @@ Constraints:
|
||||
import collections
|
||||
import math
|
||||
|
||||
from google.protobuf import text_format
|
||||
from absl import app
|
||||
from absl import flags
|
||||
from google.protobuf import text_format
|
||||
from ortools.sat.python import cp_model
|
||||
|
||||
FLAGS = flags.FLAGS
|
||||
|
||||
@@ -10,9 +10,22 @@
|
||||
# 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.
|
||||
"""Solves a flexible jobshop problems with the CP-SAT solver."""
|
||||
"""Solves a flexible jobshop problems with the CP-SAT solver.
|
||||
|
||||
A jobshop is a standard scheduling problem when you must sequence a
|
||||
series of task_types on a set of machines. Each job contains one task_type per
|
||||
machine. The order of execution and the length of each job on each
|
||||
machine is task_type dependent.
|
||||
|
||||
The objective is to minimize the maximum completion time of all
|
||||
jobs. This is called the makespan.
|
||||
"""
|
||||
|
||||
# overloaded sum() clashes with pytype.
|
||||
# pytype: disable=wrong-arg-types
|
||||
|
||||
import collections
|
||||
|
||||
from ortools.sat.python import cp_model
|
||||
|
||||
|
||||
@@ -33,18 +46,18 @@ class SolutionPrinter(cp_model.CpSolverSolutionCallback):
|
||||
def flexible_jobshop():
|
||||
"""Solve a small flexible jobshop problem."""
|
||||
# Data part.
|
||||
jobs = [ # task = (processing_time, machine_id)
|
||||
[ # Job 0
|
||||
[(3, 0), (1, 1), (5, 2)], # task 0 with 3 alternatives
|
||||
[(2, 0), (4, 1), (6, 2)], # task 1 with 3 alternatives
|
||||
[(2, 0), (3, 1), (1, 2)], # task 2 with 3 alternatives
|
||||
jobs = [ # task = (processing_time, machine_id)
|
||||
[ # Job 0
|
||||
[(3, 0), (1, 1), (5, 2)], # task 0 with 3 alternatives
|
||||
[(2, 0), (4, 1), (6, 2)], # task 1 with 3 alternatives
|
||||
[(2, 0), (3, 1), (1, 2)], # task 2 with 3 alternatives
|
||||
],
|
||||
[ # Job 1
|
||||
[ # Job 1
|
||||
[(2, 0), (3, 1), (4, 2)],
|
||||
[(1, 0), (5, 1), (4, 2)],
|
||||
[(2, 0), (1, 1), (4, 2)],
|
||||
],
|
||||
[ # Job 2
|
||||
[ # Job 2
|
||||
[(2, 0), (1, 1), (4, 2)],
|
||||
[(2, 0), (3, 1), (4, 2)],
|
||||
[(3, 0), (1, 1), (5, 2)],
|
||||
|
||||
@@ -13,11 +13,10 @@
|
||||
"""Sat based solver for the RCPSP problems (see rcpsp.proto)."""
|
||||
|
||||
import collections
|
||||
import time
|
||||
|
||||
from google.protobuf import text_format
|
||||
from absl import app
|
||||
from absl import flags
|
||||
from google.protobuf import text_format
|
||||
from ortools.data import pywraprcpsp
|
||||
from ortools.sat.python import cp_model
|
||||
|
||||
@@ -30,42 +29,62 @@ flags.DEFINE_string('params', '', 'Sat solver parameters.')
|
||||
flags.DEFINE_bool('use_interval_makespan', True,
|
||||
'Whether we encode the makespan using an interval or not.')
|
||||
flags.DEFINE_integer('horizon', -1, 'Force horizon.')
|
||||
flags.DEFINE_bool(
|
||||
'use_main_interval_for_tasks', True,
|
||||
'Creates a main interval for each task, and use it in precedences')
|
||||
|
||||
|
||||
class SolutionPrinter(cp_model.CpSolverSolutionCallback):
|
||||
"""Print intermediate solutions."""
|
||||
|
||||
def __init__(self):
|
||||
cp_model.CpSolverSolutionCallback.__init__(self)
|
||||
self.__solution_count = 0
|
||||
self.__start_time = time.time()
|
||||
|
||||
def on_solution_callback(self):
|
||||
current_time = time.time()
|
||||
objective = self.ObjectiveValue()
|
||||
print('Solution %i, time = %f s, objective = %i' %
|
||||
(self.__solution_count, current_time - self.__start_time,
|
||||
objective))
|
||||
self.__solution_count += 1
|
||||
|
||||
|
||||
def SolveRcpsp(problem, proto_file, params):
|
||||
"""Parse and solve a given RCPSP problem in proto format."""
|
||||
def PrintProblemStatistics(problem):
|
||||
"""Display various statistics on the problem."""
|
||||
|
||||
# Determine problem type.
|
||||
problem_type = ('Resource Investment Problem'
|
||||
if problem.is_resource_investment else 'RCPSP')
|
||||
|
||||
num_resources = len(problem.resources)
|
||||
num_tasks = len(problem.tasks) - 2 # 2 sentinels.
|
||||
tasks_with_alternatives = 0
|
||||
variable_duration_tasks = 0
|
||||
tasks_with_delay = 0
|
||||
|
||||
for task in problem.tasks:
|
||||
if len(task.recipes) > 1:
|
||||
tasks_with_alternatives += 1
|
||||
duration_0 = task.recipes[0].duration
|
||||
for recipe in task.recipes:
|
||||
if recipe.duration != duration_0:
|
||||
variable_duration_tasks += 1
|
||||
break
|
||||
if task.successor_delays:
|
||||
tasks_with_delay += 1
|
||||
|
||||
if problem.is_rcpsp_max:
|
||||
problem_type += '/Max delay'
|
||||
# We print 2 less tasks as these are sentinel tasks that are not counted in
|
||||
# the description of the rcpsp models.
|
||||
if problem.is_consumer_producer:
|
||||
print('Solving %s with %i reservoir resources and %i tasks' %
|
||||
(problem_type, len(problem.resources), len(problem.tasks) - 2))
|
||||
print(f'Solving {problem_type} with:')
|
||||
print(f' - {num_resources} reservoir resources')
|
||||
print(f' - {num_tasks} tasks')
|
||||
else:
|
||||
print('Solving %s with %i resources and %i tasks' %
|
||||
(problem_type, len(problem.resources), len(problem.tasks) - 2))
|
||||
print(f'Solving {problem_type} with:')
|
||||
print(f' - {num_resources} renewable resources')
|
||||
print(f' - {num_tasks} tasks')
|
||||
if tasks_with_alternatives:
|
||||
print(
|
||||
f' - {tasks_with_alternatives} tasks with alternative resources'
|
||||
)
|
||||
if variable_duration_tasks:
|
||||
print(
|
||||
f' - {variable_duration_tasks} tasks with variable durations'
|
||||
)
|
||||
if tasks_with_delay:
|
||||
print(f' - {tasks_with_delay} tasks with successor delays')
|
||||
|
||||
|
||||
def SolveRcpsp(problem, proto_file, params):
|
||||
"""Parse and solve a given RCPSP problem in proto format."""
|
||||
PrintProblemStatistics(problem)
|
||||
|
||||
# Create the model.
|
||||
model = cp_model.CpModel()
|
||||
@@ -87,7 +106,7 @@ def SolveRcpsp(problem, proto_file, params):
|
||||
for rd in sd.recipe_delays:
|
||||
for d in rd.min_delays:
|
||||
horizon += abs(d)
|
||||
print(' - horizon = %i' % horizon)
|
||||
print(f' - horizon = {horizon}')
|
||||
|
||||
# Containers used to build resources.
|
||||
intervals_per_resource = collections.defaultdict(list)
|
||||
@@ -95,37 +114,33 @@ def SolveRcpsp(problem, proto_file, params):
|
||||
presences_per_resource = collections.defaultdict(list)
|
||||
starts_per_resource = collections.defaultdict(list)
|
||||
|
||||
# Starts and ends for master interval variables.
|
||||
# Starts and ends for each task (shared between all alternatives)
|
||||
task_starts = {}
|
||||
task_ends = {}
|
||||
|
||||
# Containers for per-recipe per task variables.
|
||||
alternatives_per_task = collections.defaultdict(list)
|
||||
# Containers for per-recipe per task alternatives variables.
|
||||
presences_per_task = collections.defaultdict(list)
|
||||
starts_per_task = collections.defaultdict(list)
|
||||
ends_per_task = collections.defaultdict(list)
|
||||
durations_per_task = collections.defaultdict(list)
|
||||
|
||||
one = model.NewIntVar(1, 1, 'one')
|
||||
one = model.NewConstant(1)
|
||||
|
||||
# Create tasks.
|
||||
# Create tasks variables.
|
||||
for t in all_active_tasks:
|
||||
task = problem.tasks[t]
|
||||
|
||||
if len(task.recipes) == 1:
|
||||
# Create interval.
|
||||
# Create main and unique interval.
|
||||
recipe = task.recipes[0]
|
||||
task_starts[t] = model.NewIntVar(0, horizon, 'start_of_task_%i' % t)
|
||||
task_ends[t] = model.NewIntVar(0, horizon, 'end_of_task_%i' % t)
|
||||
task_starts[t] = model.NewIntVar(0, horizon, f'start_of_task_{t}')
|
||||
task_ends[t] = model.NewIntVar(0, horizon, f'end_of_task_{t}')
|
||||
interval = model.NewIntervalVar(task_starts[t], recipe.duration,
|
||||
task_ends[t], 'interval_%i' % t)
|
||||
task_ends[t], f'interval_{t}')
|
||||
|
||||
# Store for later.
|
||||
alternatives_per_task[t].append(interval)
|
||||
starts_per_task[t].append(task_starts[t])
|
||||
ends_per_task[t].append(task_ends[t])
|
||||
# Store as a single alternative for later.
|
||||
presences_per_task[t].append(one)
|
||||
durations_per_task[t].append(recipe.duration)
|
||||
|
||||
# Register for resources.
|
||||
# Register the interval in resources specified by the demands.
|
||||
for i in range(len(recipe.demands)):
|
||||
demand = recipe.demands[i]
|
||||
res = recipe.resources[i]
|
||||
@@ -135,30 +150,29 @@ def SolveRcpsp(problem, proto_file, params):
|
||||
else:
|
||||
starts_per_resource[res].append(task_starts[t])
|
||||
presences_per_resource[res].append(1)
|
||||
else:
|
||||
else: # Multiple alternative recipes.
|
||||
all_recipes = range(len(task.recipes))
|
||||
|
||||
# Compute duration range.
|
||||
min_size = min(recipe.duration for recipe in task.recipes)
|
||||
max_size = max(recipe.duration for recipe in task.recipes)
|
||||
start = model.NewIntVar(0, horizon, f'start_of_task_{t}')
|
||||
end = model.NewIntVar(0, horizon, f'end_of_task_{t}')
|
||||
|
||||
# Store for precedences.
|
||||
task_starts[t] = start
|
||||
task_ends[t] = end
|
||||
|
||||
# Create one optional interval per recipe.
|
||||
for r in all_recipes:
|
||||
recipe = task.recipes[r]
|
||||
is_present = model.NewBoolVar('is_present_%i_r%i' % (t, r))
|
||||
start = model.NewIntVar(0, horizon, 'start_%i_r%i' % (t, r))
|
||||
end = model.NewIntVar(0, horizon, 'end_%i_r%i' % (t, r))
|
||||
interval = model.NewOptionalIntervalVar(
|
||||
start, recipe.duration, end, is_present,
|
||||
'interval_%i_r%i' % (t, r))
|
||||
is_present = model.NewBoolVar(f'is_present_{t}_{r}')
|
||||
interval = model.NewOptionalIntervalVar(start, recipe.duration,
|
||||
end, is_present,
|
||||
f'interval_{t}_{r}')
|
||||
|
||||
# Store variables.
|
||||
alternatives_per_task[t].append(interval)
|
||||
starts_per_task[t].append(start)
|
||||
ends_per_task[t].append(end)
|
||||
# Store alternative variables.
|
||||
presences_per_task[t].append(is_present)
|
||||
durations_per_task[t].append(recipe.duration)
|
||||
|
||||
# Register intervals in resources.
|
||||
# Register the interval in resources specified by the demands.
|
||||
for i in range(len(recipe.demands)):
|
||||
demand = recipe.demands[i]
|
||||
res = recipe.resources[i]
|
||||
@@ -169,23 +183,24 @@ def SolveRcpsp(problem, proto_file, params):
|
||||
starts_per_resource[res].append(start)
|
||||
presences_per_resource[res].append(is_present)
|
||||
|
||||
# Create the master interval for the task.
|
||||
task_starts[t] = model.NewIntVar(0, horizon, 'start_of_task_%i' % t)
|
||||
task_ends[t] = model.NewIntVar(0, horizon, 'end_of_task_%i' % t)
|
||||
duration = model.NewIntVar(min_size, max_size,
|
||||
'duration_of_task_%i' % t)
|
||||
interval = model.NewIntervalVar(task_starts[t], duration,
|
||||
task_ends[t], 'interval_%i' % t)
|
||||
|
||||
# Link with optional per-recipe copies.
|
||||
for r in all_recipes:
|
||||
p = presences_per_task[t][r]
|
||||
model.Add(
|
||||
task_starts[t] == starts_per_task[t][r]).OnlyEnforceIf(p)
|
||||
model.Add(task_ends[t] == ends_per_task[t][r]).OnlyEnforceIf(p)
|
||||
model.Add(duration == task.recipes[r].duration).OnlyEnforceIf(p)
|
||||
# Exactly one alternative must be performed.
|
||||
model.Add(sum(presences_per_task[t]) == 1)
|
||||
|
||||
# linear encoding of the duration.
|
||||
min_duration = min(durations_per_task[t])
|
||||
max_duration = max(durations_per_task[t])
|
||||
shifted = [x - min_duration for x in durations_per_task[t]]
|
||||
|
||||
duration = model.NewIntVar(min_duration, max_duration,
|
||||
f'duration_of_task_{t}')
|
||||
model.Add(
|
||||
duration == min_duration +
|
||||
cp_model.LinearExpr.ScalProd(presences_per_task[t], shifted))
|
||||
|
||||
# We do not create a 'main' interval. Instead, we link start, end, and
|
||||
# duration.
|
||||
model.Add(start + duration == end)
|
||||
|
||||
# Create makespan variable
|
||||
makespan = model.NewIntVar(0, horizon, 'makespan')
|
||||
interval_makespan = model.NewIntervalVar(
|
||||
@@ -194,6 +209,8 @@ def SolveRcpsp(problem, proto_file, params):
|
||||
|
||||
# Add precedences.
|
||||
if problem.is_rcpsp_max:
|
||||
# In RCPSP/Max problem, precedences are given and max delay (possible
|
||||
# negative) between the starts of two tasks.
|
||||
for task_id in all_active_tasks:
|
||||
task = problem.tasks[task_id]
|
||||
num_modes = len(task.recipes)
|
||||
@@ -203,7 +220,7 @@ def SolveRcpsp(problem, proto_file, params):
|
||||
delay_matrix = task.successor_delays[successor_index]
|
||||
num_next_modes = len(problem.tasks[next_id].recipes)
|
||||
for m1 in range(num_modes):
|
||||
s1 = starts_per_task[task_id][m1]
|
||||
s1 = task_starts[task_id]
|
||||
p1 = presences_per_task[task_id][m1]
|
||||
if next_id == num_tasks - 1:
|
||||
delay = delay_matrix.recipe_delays[m1].min_delays[0]
|
||||
@@ -212,22 +229,21 @@ def SolveRcpsp(problem, proto_file, params):
|
||||
for m2 in range(num_next_modes):
|
||||
delay = delay_matrix.recipe_delays[m1].min_delays[
|
||||
m2]
|
||||
s2 = starts_per_task[next_id][m2]
|
||||
s2 = task_starts[next_id]
|
||||
p2 = presences_per_task[next_id][m2]
|
||||
model.Add(s1 + delay <= s2).OnlyEnforceIf([p1, p2])
|
||||
else: # Normal dependencies (task ends before the start of successors).
|
||||
else:
|
||||
# Normal dependencies (task ends before the start of successors).
|
||||
for t in all_active_tasks:
|
||||
for n in problem.tasks[t].successors:
|
||||
if n == num_tasks - 1:
|
||||
# TODO(user): I guess these are still useful, but we might want to
|
||||
# experiment with removing them.
|
||||
model.Add(task_ends[t] <= makespan)
|
||||
else:
|
||||
model.Add(task_ends[t] <= task_starts[n])
|
||||
|
||||
# Containers for resource investment problems.
|
||||
capacities = []
|
||||
max_cost = 0
|
||||
capacities = [] # Capacity variables for all resources.
|
||||
max_cost = 0 # Upper bound on the investment cost.
|
||||
|
||||
# Create resources.
|
||||
for r in all_resources:
|
||||
@@ -238,7 +254,7 @@ def SolveRcpsp(problem, proto_file, params):
|
||||
|
||||
if problem.is_resource_investment:
|
||||
# RIP problems have only renewable resources.
|
||||
capacity = model.NewIntVar(0, c, 'capacity_of_%i' % r)
|
||||
capacity = model.NewIntVar(0, c, f'capacity_of_{r}')
|
||||
model.AddCumulative(intervals_per_resource[r],
|
||||
demands_per_resource[r], capacity)
|
||||
capacities.append(capacity)
|
||||
@@ -276,7 +292,7 @@ def SolveRcpsp(problem, proto_file, params):
|
||||
model.Minimize(objective)
|
||||
|
||||
if proto_file:
|
||||
print('Writing proto to %s' % proto_file)
|
||||
print(f'Writing proto to{proto_file}')
|
||||
with open(proto_file, 'w') as text_file:
|
||||
text_file.write(str(model))
|
||||
|
||||
@@ -284,9 +300,8 @@ def SolveRcpsp(problem, proto_file, params):
|
||||
solver = cp_model.CpSolver()
|
||||
if params:
|
||||
text_format.Parse(params, solver.parameters)
|
||||
solution_printer = SolutionPrinter()
|
||||
solver.SolveWithSolutionCallback(model, solution_printer)
|
||||
print(solver.ResponseStats())
|
||||
solver.parameters.log_search_progress = True
|
||||
solver.Solve(model)
|
||||
|
||||
|
||||
def main(_):
|
||||
|
||||
Reference in New Issue
Block a user