2018-11-10 18:00:53 +01:00
|
|
|
# Copyright 2010-2018 Google LLC
|
2014-01-29 15:21:07 +00:00
|
|
|
# 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.
|
2018-11-20 04:35:48 -08:00
|
|
|
"""Generates possible daily schedules for workers."""
|
2014-01-29 15:21:07 +00:00
|
|
|
|
2018-08-30 07:59:49 +02:00
|
|
|
|
2015-12-09 14:49:52 +01:00
|
|
|
import argparse
|
2018-11-20 04:35:48 -08:00
|
|
|
from ortools.sat.python import cp_model
|
2014-01-29 15:21:07 +00:00
|
|
|
from ortools.linear_solver import pywraplp
|
|
|
|
|
|
2018-08-28 18:34:20 +02:00
|
|
|
PARSER = argparse.ArgumentParser()
|
|
|
|
|
PARSER.add_argument(
|
2018-06-11 11:51:18 +02:00
|
|
|
'--load_min', default=480, type=int, help='Minimum load in minutes')
|
2018-08-28 18:34:20 +02:00
|
|
|
PARSER.add_argument(
|
2018-06-11 11:51:18 +02:00
|
|
|
'--load_max', default=540, type=int, help='Maximum load in minutes')
|
2018-08-28 18:34:20 +02:00
|
|
|
PARSER.add_argument(
|
2018-06-11 11:51:18 +02:00
|
|
|
'--commute_time', default=30, type=int, help='Commute time in minutes')
|
2018-08-28 18:34:20 +02:00
|
|
|
PARSER.add_argument(
|
2018-06-11 11:51:18 +02:00
|
|
|
'--num_workers', default=98, type=int, help='Maximum number of workers.')
|
2014-01-29 15:21:07 +00:00
|
|
|
|
2018-09-06 15:09:32 +02:00
|
|
|
|
2018-11-20 04:35:48 -08:00
|
|
|
class AllSolutionCollector(cp_model.CpSolverSolutionCallback):
|
|
|
|
|
"""Stores all solutions."""
|
|
|
|
|
|
|
|
|
|
def __init__(self, variables):
|
|
|
|
|
cp_model.CpSolverSolutionCallback.__init__(self)
|
|
|
|
|
self.__variables = variables
|
|
|
|
|
self.__collect = []
|
|
|
|
|
|
2018-11-20 05:44:21 -08:00
|
|
|
def on_solution_callback(self):
|
2018-11-20 04:35:48 -08:00
|
|
|
"""Collect a new combination."""
|
|
|
|
|
combination = [self.Value(v) for v in self.__variables]
|
|
|
|
|
self.__collect.append(combination)
|
|
|
|
|
|
|
|
|
|
def combinations(self):
|
|
|
|
|
"""Returns all collected combinations."""
|
|
|
|
|
return self.__collect
|
|
|
|
|
|
|
|
|
|
|
2018-08-28 18:34:20 +02:00
|
|
|
def find_combinations(durations, load_min, load_max, commute_time):
|
2018-11-11 09:39:59 +01:00
|
|
|
"""This methods find all valid combinations of appointments.
|
2014-01-29 15:21:07 +00:00
|
|
|
|
2018-11-20 04:35:48 -08:00
|
|
|
This methods find all combinations of appointments such that the sum of
|
|
|
|
|
durations + commute times is between load_min and load_max.
|
2014-01-29 15:21:07 +00:00
|
|
|
|
2018-11-20 04:35:48 -08:00
|
|
|
Args:
|
|
|
|
|
durations: The durations of all appointments.
|
|
|
|
|
load_min: The min number of worked minutes for a valid selection.
|
|
|
|
|
load_max: The max number of worked minutes for a valid selection.
|
|
|
|
|
commute_time: The commute time between two appointments in minutes.
|
2018-09-06 15:09:32 +02:00
|
|
|
|
2018-11-20 04:35:48 -08:00
|
|
|
Returns:
|
|
|
|
|
A matrix where each line is a valid combinations of appointments.
|
|
|
|
|
"""
|
|
|
|
|
model = cp_model.CpModel()
|
2018-11-11 09:39:59 +01:00
|
|
|
variables = [
|
2018-11-20 04:35:48 -08:00
|
|
|
model.NewIntVar(0, load_max // (duration + commute_time), '')
|
|
|
|
|
for duration in durations
|
2018-11-11 09:39:59 +01:00
|
|
|
]
|
2019-05-06 10:12:44 +02:00
|
|
|
terms = sum(variables[i] * (duration + commute_time)
|
|
|
|
|
for i, duration in enumerate(durations))
|
2019-05-03 18:15:37 +02:00
|
|
|
model.AddLinearConstraint(terms, load_min, load_max)
|
2018-11-20 04:35:48 -08:00
|
|
|
|
|
|
|
|
solver = cp_model.CpSolver()
|
|
|
|
|
solution_collector = AllSolutionCollector(variables)
|
|
|
|
|
solver.SearchForAllSolutions(model, solution_collector)
|
|
|
|
|
return solution_collector.combinations()
|
2014-01-29 15:21:07 +00:00
|
|
|
|
2018-09-06 15:09:32 +02:00
|
|
|
|
2018-08-28 18:34:20 +02:00
|
|
|
def select(combinations, loads, max_number_of_workers):
|
2018-11-11 09:39:59 +01:00
|
|
|
"""This method selects the optimal combination of appointments.
|
2014-01-29 15:21:07 +00:00
|
|
|
|
|
|
|
|
This method uses Mixed Integer Programming to select the optimal mix of
|
|
|
|
|
appointments.
|
|
|
|
|
"""
|
2018-11-11 09:39:59 +01:00
|
|
|
solver = pywraplp.Solver('Select',
|
|
|
|
|
pywraplp.Solver.CBC_MIXED_INTEGER_PROGRAMMING)
|
|
|
|
|
num_vars = len(loads)
|
|
|
|
|
num_combinations = len(combinations)
|
|
|
|
|
variables = [
|
|
|
|
|
solver.IntVar(0, max_number_of_workers, 's[%d]' % i)
|
|
|
|
|
for i in range(num_combinations)
|
|
|
|
|
]
|
|
|
|
|
achieved = [
|
|
|
|
|
solver.IntVar(0, 1000, 'achieved[%d]' % i) for i in range(num_vars)
|
|
|
|
|
]
|
|
|
|
|
transposed = [[
|
|
|
|
|
combinations[type][index] for type in range(num_combinations)
|
|
|
|
|
] for index in range(num_vars)]
|
|
|
|
|
|
|
|
|
|
# Maintain the achieved variables.
|
2018-11-20 04:35:48 -08:00
|
|
|
for i, coefs in enumerate(transposed):
|
2018-11-11 09:39:59 +01:00
|
|
|
ct = solver.Constraint(0.0, 0.0)
|
|
|
|
|
ct.SetCoefficient(achieved[i], -1)
|
2018-11-20 04:35:48 -08:00
|
|
|
for j, coef in enumerate(coefs):
|
|
|
|
|
ct.SetCoefficient(variables[j], coef)
|
2018-11-11 09:39:59 +01:00
|
|
|
|
|
|
|
|
# Simple bound.
|
|
|
|
|
solver.Add(solver.Sum(variables) <= max_number_of_workers)
|
|
|
|
|
|
|
|
|
|
obj_vars = [
|
|
|
|
|
solver.IntVar(0, 1000, 'obj_vars[%d]' % i) for i in range(num_vars)
|
2018-06-11 11:51:18 +02:00
|
|
|
]
|
2018-11-11 09:39:59 +01:00
|
|
|
for i in range(num_vars):
|
|
|
|
|
solver.Add(obj_vars[i] >= achieved[i] - loads[i])
|
|
|
|
|
solver.Add(obj_vars[i] >= loads[i] - achieved[i])
|
|
|
|
|
|
|
|
|
|
solver.Minimize(solver.Sum(obj_vars))
|
|
|
|
|
|
|
|
|
|
result_status = solver.Solve()
|
|
|
|
|
|
|
|
|
|
# The problem has an optimal solution.
|
|
|
|
|
if result_status == pywraplp.Solver.OPTIMAL:
|
|
|
|
|
print('Problem solved in %f milliseconds' % solver.WallTime())
|
|
|
|
|
return solver.Objective().Value(), [
|
|
|
|
|
int(v.SolutionValue()) for v in variables
|
|
|
|
|
]
|
|
|
|
|
return -1, []
|
2014-01-29 15:21:07 +00:00
|
|
|
|
2018-09-06 15:09:32 +02:00
|
|
|
|
2018-08-28 18:34:20 +02:00
|
|
|
def get_optimal_schedule(demand, args):
|
2018-11-11 09:39:59 +01:00
|
|
|
"""Computes the optimal schedule for the appointment selection problem."""
|
|
|
|
|
combinations = find_combinations([a[2] for a in demand], args.load_min,
|
|
|
|
|
args.load_max, args.commute_time)
|
2018-11-28 10:56:33 +01:00
|
|
|
print('found %d possible combinations of appointements' % len(combinations))
|
2014-01-29 15:21:07 +00:00
|
|
|
|
2018-11-28 10:56:33 +01:00
|
|
|
cost, selection = select(combinations, [a[0]
|
|
|
|
|
for a in demand], args.num_workers)
|
2018-11-11 09:39:59 +01:00
|
|
|
output = [(selection[i], [(combinations[i][t], demand[t][1])
|
|
|
|
|
for t in range(len(demand))
|
|
|
|
|
if combinations[i][t] != 0])
|
|
|
|
|
for i in range(len(selection)) if selection[i] != 0]
|
|
|
|
|
return cost, output
|
2014-01-29 15:21:07 +00:00
|
|
|
|
2018-09-06 15:09:32 +02:00
|
|
|
|
2015-12-09 14:49:52 +01:00
|
|
|
def main(args):
|
2018-11-20 04:35:48 -08:00
|
|
|
"""Solve the assignment problem."""
|
2018-11-11 09:39:59 +01:00
|
|
|
demand = [(40, 'A1', 90), (30, 'A2', 120), (25, 'A3', 180)]
|
|
|
|
|
print('appointments: ')
|
|
|
|
|
for a in demand:
|
|
|
|
|
print(' %d * %s : %d min' % (a[0], a[1], a[2]))
|
|
|
|
|
print('commute time = %d' % args.commute_time)
|
2018-11-28 10:56:33 +01:00
|
|
|
print('accepted total duration = [%d..%d]' % (args.load_min, args.load_max))
|
2018-11-11 09:39:59 +01:00
|
|
|
print('%d workers' % args.num_workers)
|
|
|
|
|
cost, selection = get_optimal_schedule(demand, args)
|
|
|
|
|
print('Optimal solution as a cost of %d' % cost)
|
|
|
|
|
for template in selection:
|
|
|
|
|
print('%d schedules with ' % template[0])
|
|
|
|
|
for t in template[1]:
|
|
|
|
|
print(' %d installation of type %s' % (t[0], t[1]))
|
2014-01-29 15:21:07 +00:00
|
|
|
|
2018-09-06 15:09:32 +02:00
|
|
|
|
2014-01-29 15:21:07 +00:00
|
|
|
if __name__ == '__main__':
|
2018-11-11 09:39:59 +01:00
|
|
|
main(PARSER.parse_args())
|