287 lines
10 KiB
Python
287 lines
10 KiB
Python
# Copyright 2010-2017 Google
|
|
# 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.
|
|
"""Sat based solver for the RCPSP problems (see rcpsp.proto)."""
|
|
|
|
from __future__ import absolute_import
|
|
from __future__ import division
|
|
from __future__ import print_function
|
|
|
|
import argparse
|
|
import collections
|
|
import time
|
|
|
|
from google.protobuf import text_format
|
|
from ortools.data import pywraprcpsp
|
|
from ortools.sat.python import cp_model
|
|
|
|
Parser = argparse.ArgumentParser()
|
|
Parser.add_argument('--input', default = "",
|
|
help = 'Input file to parse and solve.')
|
|
Parser.add_argument('--output_proto', default = "",
|
|
help = 'Output file to write the cp_model'
|
|
'proto to.')
|
|
Parser.add_argument('--params', default = "",
|
|
help = 'Sat solver parameters.')
|
|
|
|
|
|
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 OnSolutionCallback(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."""
|
|
|
|
# Determine problem type.
|
|
problem_type = ('Resource Investment Problem'
|
|
if problem.is_resource_investment else 'RCPSP')
|
|
|
|
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))
|
|
else:
|
|
print('Solving %s with %i resources and %i tasks' %
|
|
(problem_type, len(problem.resources), len(problem.tasks) - 2))
|
|
|
|
# Create the model.
|
|
model = cp_model.CpModel()
|
|
|
|
num_tasks = len(problem.tasks)
|
|
num_resources = len(problem.resources)
|
|
|
|
all_active_tasks = range(1, num_tasks - 1)
|
|
all_resources = range(num_resources)
|
|
|
|
horizon = problem.deadline if problem.deadline != -1 else problem.horizon
|
|
if horizon == -1: # Naive computation.
|
|
horizon = sum(max(r.duration for r in t.recipes) for t in problem.tasks)
|
|
if problem.is_rcpsp_max:
|
|
for t in problem.tasks:
|
|
for sd in t.successor_delays:
|
|
for rd in sd.recipe_delays:
|
|
for d in rd.min_delays:
|
|
horizon += abs(d)
|
|
print(' - horizon = %i' % horizon)
|
|
|
|
# Containers used to build resources.
|
|
intervals_per_resource = collections.defaultdict(list)
|
|
demands_per_resource = collections.defaultdict(list)
|
|
presences_per_resource = collections.defaultdict(list)
|
|
starts_per_resource = collections.defaultdict(list)
|
|
|
|
# Starts and ends for master interval variables.
|
|
task_starts = {}
|
|
task_ends = {}
|
|
|
|
# Containers for per-recipe per task variables.
|
|
alternatives_per_task = collections.defaultdict(list)
|
|
presences_per_task = collections.defaultdict(list)
|
|
starts_per_task = collections.defaultdict(list)
|
|
ends_per_task = collections.defaultdict(list)
|
|
|
|
one = model.NewIntVar(1, 1, 'one')
|
|
|
|
# Create tasks.
|
|
for t in all_active_tasks:
|
|
task = problem.tasks[t]
|
|
|
|
if len(task.recipes) == 1:
|
|
# Create 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)
|
|
interval = model.NewIntervalVar(task_starts[t], recipe.duration,
|
|
task_ends[t], 'interval_%i' % 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])
|
|
presences_per_task[t].append(one)
|
|
|
|
# Register for resources.
|
|
for i in range(len(recipe.demands)):
|
|
demand = recipe.demands[i]
|
|
res = recipe.resources[i]
|
|
demands_per_resource[res].append(demand)
|
|
if problem.resources[res].renewable:
|
|
intervals_per_resource[res].append(interval)
|
|
else:
|
|
starts_per_resource[res].append(task_starts[t])
|
|
presences_per_resource[res].append(1)
|
|
else:
|
|
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)
|
|
|
|
# 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))
|
|
|
|
# Store variables.
|
|
alternatives_per_task[t].append(interval)
|
|
starts_per_task[t].append(start)
|
|
ends_per_task[t].append(end)
|
|
presences_per_task[t].append(is_present)
|
|
|
|
# Register intervals in resources.
|
|
for i in range(len(recipe.demands)):
|
|
demand = recipe.demands[i]
|
|
res = recipe.resources[i]
|
|
demands_per_resource[res].append(demand)
|
|
if problem.resources[res].renewable:
|
|
intervals_per_resource[res].append(interval)
|
|
else:
|
|
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)
|
|
model.Add(sum(presences_per_task[t]) == 1)
|
|
|
|
# Create makespan variable
|
|
makespan = model.NewIntVar(0, horizon, 'makespan')
|
|
|
|
# Add precedences.
|
|
if problem.is_rcpsp_max:
|
|
for task_id in all_active_tasks:
|
|
task = problem.tasks[task_id]
|
|
num_modes = len(task.recipes)
|
|
|
|
for successor_index in range(len(task.successors)):
|
|
next_id = task.successors[successor_index]
|
|
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]
|
|
p1 = presences_per_task[task_id][m1]
|
|
if next_id == num_tasks - 1:
|
|
delay = delay_matrix.recipe_delays[m1].min_delays[0]
|
|
model.Add(s1 + delay <= makespan).OnlyEnforceIf(p1)
|
|
else:
|
|
for m2 in range(num_next_modes):
|
|
delay = delay_matrix.recipe_delays[m1].min_delays[m2]
|
|
s2 = starts_per_task[next_id][m2]
|
|
p2 = presences_per_task[next_id][m2]
|
|
# Create p == (p1 and p2).
|
|
p = model.NewBoolVar('(%s and %s)' % (p1, p2))
|
|
model.AddImplication(p, p1)
|
|
model.AddImplication(p, p2)
|
|
model.AddBoolOr([p1.Not(), p2.Not(), p])
|
|
model.Add(s1 + delay <= s2).OnlyEnforceIf(p)
|
|
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:
|
|
model.Add(task_ends[t] <= makespan)
|
|
else:
|
|
model.Add(task_ends[t] <= task_starts[n])
|
|
|
|
# Containers for resource investment problems.
|
|
capacities = []
|
|
max_cost = 0
|
|
|
|
# Create resources.
|
|
for r in all_resources:
|
|
resource = problem.resources[r]
|
|
c = resource.max_capacity
|
|
if c == -1:
|
|
c = sum(demands_per_resource[r])
|
|
|
|
if problem.is_resource_investment:
|
|
# RIP problems have only renewable resources.
|
|
capacity = model.NewIntVar(0, c, 'capacity_of_%i' % r)
|
|
model.AddCumulative(intervals_per_resource[r], demands_per_resource[r],
|
|
capacity)
|
|
capacities.append(capacity)
|
|
max_cost += c * resource.unit_cost
|
|
elif resource.renewable:
|
|
if intervals_per_resource[r]:
|
|
model.AddCumulative(intervals_per_resource[r], demands_per_resource[r],
|
|
c)
|
|
elif presences_per_resource[r]: # Non empty non renewable resource.
|
|
if problem.is_consumer_producer:
|
|
model.AddReservoirConstraint(
|
|
starts_per_resource[r], demands_per_resource[r],
|
|
resource.min_capacity, resource.max_capacity)
|
|
else:
|
|
model.Add(
|
|
sum(presences_per_resource[r][i] * demands_per_resource[r][i]
|
|
for i in range(len(presences_per_resource[r]))) <= c)
|
|
|
|
# Objective.
|
|
if problem.is_resource_investment:
|
|
objective = model.NewIntVar(0, max_cost, 'capacity_costs')
|
|
model.Add(objective == sum(problem.resources[i].unit_cost * capacities[i]
|
|
for i in range(len(capacities))))
|
|
else:
|
|
objective = makespan
|
|
|
|
model.Minimize(objective)
|
|
|
|
if proto_file:
|
|
print('Writing proto to %s' % proto_file)
|
|
with open(proto_file, 'w') as text_file:
|
|
text_file.write(str(model))
|
|
|
|
# Solve model.
|
|
solver = cp_model.CpSolver()
|
|
if params:
|
|
text_format.Merge(params, solver.parameters)
|
|
solution_printer = SolutionPrinter()
|
|
solver.SolveWithSolutionCallback(model, solution_printer)
|
|
print(solver.ResponseStats())
|
|
|
|
|
|
def main(args):
|
|
rcpsp_parser = pywraprcpsp.RcpspParser()
|
|
rcpsp_parser.ParseFile(args.input)
|
|
SolveRcpsp(rcpsp_parser.Problem(), args.output_proto, args.params)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main(Parser.parse_args())
|