From 425da9ed290c75af8a5541c6959799a59d8f4826 Mon Sep 17 00:00:00 2001 From: Laurent Perron Date: Mon, 30 Nov 2020 11:59:56 +0100 Subject: [PATCH] sync python examples --- examples/python/appointments.py | 259 +++++++++++++++--------- examples/python/gate_scheduling_sat.py | 51 +++-- examples/python/golomb8.py | 4 +- examples/python/shift_scheduling_sat.py | 11 +- examples/python/steel_mill_slab_sat.py | 171 +++++++--------- 5 files changed, 285 insertions(+), 211 deletions(-) diff --git a/examples/python/appointments.py b/examples/python/appointments.py index c17579cb9b..0d05755f80 100644 --- a/examples/python/appointments.py +++ b/examples/python/appointments.py @@ -10,22 +10,26 @@ # 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. -"""Generates possible daily schedules for workers.""" +"""Appointment selection. +This module maximizes the number of appointments that can +be fulfilled by a crew of installers while staying close to ideal +ratio of appointment types. +""" -import argparse -from ortools.sat.python import cp_model +# overloaded sum() clashes with pytype. +# pytype: disable=wrong-arg-types + +from absl import app +from absl import flags from ortools.linear_solver import pywraplp +from ortools.sat.python import cp_model -PARSER = argparse.ArgumentParser() -PARSER.add_argument( - '--load_min', default=480, type=int, help='Minimum load in minutes') -PARSER.add_argument( - '--load_max', default=540, type=int, help='Maximum load in minutes') -PARSER.add_argument( - '--commute_time', default=30, type=int, help='Commute time in minutes') -PARSER.add_argument( - '--num_workers', default=98, type=int, help='Maximum number of workers.') +FLAGS = flags.FLAGS +flags.DEFINE_integer('load_min', 480, 'Minimum load in minutes.') +flags.DEFINE_integer('load_max', 540, 'Maximum load in minutes.') +flags.DEFINE_integer('commute_time', 30, 'Commute time in minutes.') +flags.DEFINE_integer('num_workers', 98, 'Maximum number of workers.') class AllSolutionCollector(cp_model.CpSolverSolutionCallback): @@ -46,29 +50,26 @@ class AllSolutionCollector(cp_model.CpSolverSolutionCallback): return self.__collect -def find_combinations(durations, load_min, load_max, commute_time): - """This methods find all valid combinations of appointments. +def EnumerateAllKnapsacksWithRepetition(item_sizes, total_size_min, + total_size_max): + """Enumerate all possible knapsacks with total size in the given range. - This methods find all combinations of appointments such that the sum of - durations + commute times is between load_min and load_max. + Args: + item_sizes: a list of integers. item_sizes[i] is the size of item #i. + total_size_min: an integer, the minimum total size. + total_size_max: an integer, the maximum total size. - 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. - - Returns: - A matrix where each line is a valid combinations of appointments. - """ + Returns: + The list of all the knapsacks whose total size is in the given inclusive + range. Each knapsack is a list [#item0, #item1, ... ], where #itemK is an + nonnegative integer: the number of times we put item #K in the knapsack. + """ model = cp_model.CpModel() variables = [ - model.NewIntVar(0, load_max // (duration + commute_time), '') - for duration in durations + model.NewIntVar(0, total_size_max // size, '') for size in item_sizes ] - terms = sum(variables[i] * (duration + commute_time) - for i, duration in enumerate(durations)) - model.AddLinearConstraint(terms, load_min, load_max) + load = sum(variables[i] * size for i, size in enumerate(item_sizes)) + model.AddLinearConstraint(load, total_size_min, total_size_max) solver = cp_model.CpSolver() solution_collector = AllSolutionCollector(variables) @@ -76,88 +77,164 @@ def find_combinations(durations, load_min, load_max, commute_time): return solution_collector.combinations() -def select(combinations, loads, max_number_of_workers): - """This method selects the optimal combination of appointments. +def AggregateItemCollectionsOptimally(item_collections, max_num_collections, + ideal_item_ratios): + """Selects a set (with repetition) of combination of items optimally. - This method uses Mixed Integer Programming to select the optimal mix of - appointments. + Given a set of collections of N possible items (in each collection, an item + may appear multiple times), a given "ideal breakdown of items", and a + maximum number of collections, this method finds the optimal way to + aggregate the collections in order to: + - maximize the overall number of items + - while keeping the ratio of each item, among the overall selection, as close + as possible to a given input ratio (which depends on the item). + Each collection may be selected more than one time. + + Args: + item_collections: a list of item collections. Each item collection is a + list of integers [#item0, ..., #itemN-1], where #itemK is the number + of times item #K appears in the collection, and N is the number of + distinct items. + max_num_collections: an integer, the maximum number of item collections + that may be selected (counting repetitions of the same collection). + ideal_item_ratios: A list of N float which sums to 1.0: the K-th element is + the ideal ratio of item #K in the whole aggregated selection. + + Returns: + A pair (objective value, list of pairs (item collection, num_selections)), + where: + - "objective value" is the value of the internal objective function used + by the MIP Solver + - Each "item collection" is an element of the input item_collections + - and its associated "num_selections" is the number of times it was + selected. """ 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)] + pywraplp.Solver.SCIP_MIXED_INTEGER_PROGRAMMING) + n = len(ideal_item_ratios) + num_distinct_collections = len(item_collections) + max_num_items_per_collection = 0 + for template in item_collections: + max_num_items_per_collection = max(max_num_items_per_collection, + sum(template)) + upper_bound = max_num_items_per_collection * max_num_collections - # Maintain the achieved variables. - for i, coefs in enumerate(transposed): + # num_selections_of_collection[i] is an IntVar that represents the number + # of times that we will use collection #i in our global selection. + num_selections_of_collection = [ + solver.IntVar(0, max_num_collections, 's[%d]' % i) + for i in range(num_distinct_collections) + ] + + # num_overall_item[i] is an IntVar that represents the total count of item #i, + # aggregated over all selected collections. This is enforced with dedicated + # constraints that bind them with the num_selections_of_collection vars. + num_overall_item = [ + solver.IntVar(0, upper_bound, 'num_overall_item[%d]' % i) + for i in range(n) + ] + for i in range(n): ct = solver.Constraint(0.0, 0.0) - ct.SetCoefficient(achieved[i], -1) - for j, coef in enumerate(coefs): - ct.SetCoefficient(variables[j], coef) + ct.SetCoefficient(num_overall_item[i], -1) + for j in range(num_distinct_collections): + ct.SetCoefficient(num_selections_of_collection[j], + item_collections[j][i]) - # Simple bound. - solver.Add(solver.Sum(variables) <= max_number_of_workers) + # Maintain the num_all_item variable as the sum of all num_overall_item + # variables. + num_all_items = solver.IntVar(0, upper_bound, 'num_all_items') + solver.Add(solver.Sum(num_overall_item) == num_all_items) - obj_vars = [ - solver.IntVar(0, 1000, 'obj_vars[%d]' % i) for i in range(num_vars) + # Sets the total number of workers. + solver.Add(solver.Sum(num_selections_of_collection) == max_num_collections) + + # Objective variables. + deviation_vars = [ + solver.NumVar(0, upper_bound, 'deviation_vars[%d]' % i) + for i in range(n) ] - for i in range(num_vars): - solver.Add(obj_vars[i] >= achieved[i] - loads[i]) - solver.Add(obj_vars[i] >= loads[i] - achieved[i]) + for i in range(n): + deviation = deviation_vars[i] + solver.Add(deviation >= num_overall_item[i] - + ideal_item_ratios[i] * num_all_items) + solver.Add(deviation >= ideal_item_ratios[i] * num_all_items - + num_overall_item[i]) - solver.Minimize(solver.Sum(obj_vars)) + solver.Maximize(num_all_items - solver.Sum(deviation_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, [] + # The problem has an optimal solution. + return [int(v.solution_value()) for v in num_selections_of_collection] + return [] -def get_optimal_schedule(demand, args): - """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) - print('found %d possible combinations of appointements' % len(combinations)) +def GetOptimalSchedule(demand): + """Computes the optimal schedule for the installation input. - cost, selection = select(combinations, [a[0] - for a in demand], args.num_workers) - 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 + Args: + demand: a list of "appointment types". Each "appointment type" is + a triple (ideal_ratio_pct, name, duration_minutes), where + ideal_ratio_pct is the ideal percentage (in [0..100.0]) of that + type of appointment among all appointments scheduled. + + Returns: + The same output type as EnumerateAllKnapsacksWithRepetition. + """ + combinations = EnumerateAllKnapsacksWithRepetition( + [a[2] + FLAGS.commute_time for a in demand], FLAGS.load_min, + FLAGS.load_max) + print(('Found %d possible day schedules ' % len(combinations) + + '(i.e. combination of appointments filling up one worker\'s day)')) + + selection = AggregateItemCollectionsOptimally( + combinations, FLAGS.num_workers, [a[0] / 100.0 for a in demand]) + output = [] + for i in range(len(selection)): + if selection[i] != 0: + output.append((selection[i], [(combinations[i][t], demand[t][1]) + for t in range(len(demand)) + if combinations[i][t] != 0])) + + return output -def main(args): - """Solve the assignment problem.""" - demand = [(40, 'A1', 90), (30, 'A2', 120), (25, 'A3', 180)] - print('appointments: ') +def main(_): + demand = [(45.0, 'Type1', 90), (30.0, 'Type2', 120), (25.0, 'Type3', 180)] + print('*** input problem ***') + print('Appointments: ') for a in demand: - print(' %d * %s : %d min' % (a[0], a[1], a[2])) - print('commute time = %d' % args.commute_time) - print('accepted total duration = [%d..%d]' % (args.load_min, args.load_max)) - print('%d workers' % args.num_workers) - cost, selection = get_optimal_schedule(demand, args) - print('Optimal solution as a cost of %d' % cost) + print(' %.2f%% of %s : %d min' % (a[0], a[1], a[2])) + print('Commute time = %d' % FLAGS.commute_time) + print('Acceptable duration of a work day = [%d..%d]' % + (FLAGS.load_min, FLAGS.load_max)) + print('%d workers' % FLAGS.num_workers) + selection = GetOptimalSchedule(demand) + print() + installed = 0 + installed_per_type = {} + for a in demand: + installed_per_type[a[1]] = 0 + + print('*** output solution ***') for template in selection: - print('%d schedules with ' % template[0]) + num_instances = template[0] + print('%d schedules with ' % num_instances) for t in template[1]: - print(' %d installation of type %s' % (t[0], t[1])) + mult = t[0] + print(' %d installation of type %s' % (mult, t[1])) + installed += num_instances * mult + installed_per_type[t[1]] += num_instances * mult + + print() + print('%d installations planned' % installed) + for a in demand: + name = a[1] + per_type = installed_per_type[name] + print((' %d (%.2f%%) installations of type %s planned' % + (per_type, per_type * 100.0 / installed, name))) if __name__ == '__main__': - main(PARSER.parse_args()) + app.run(main) diff --git a/examples/python/gate_scheduling_sat.py b/examples/python/gate_scheduling_sat.py index d1f271b0a1..a6cf645d3e 100644 --- a/examples/python/gate_scheduling_sat.py +++ b/examples/python/gate_scheduling_sat.py @@ -16,23 +16,40 @@ We have a set of jobs to perform (duration, width). We have two parallel machines that can perform this job. One machine can only perform one job at a time. At any point in time, the sum of the width of the two active jobs does not -exceed a max_length. +exceed a max_width. The objective is to minimize the max end time of all jobs. """ -from ortools.sat.python import cp_model +from absl import app + from ortools.sat.python import visualization +from ortools.sat.python import cp_model -def main(): +def main(_): """Solves the gate scheduling problem.""" model = cp_model.CpModel() - jobs = [[3, 3], [2, 5], [1, 3], [3, 7], [7, 3], [2, 2], [2, 2], [5, 5], - [10, 2], [4, 3], [2, 6], [1, 2], [6, 8], [4, 5], [3, 7]] + jobs = [ + [3, 3], # [duration, width] + [2, 5], + [1, 3], + [3, 7], + [7, 3], + [2, 2], + [2, 2], + [5, 5], + [10, 2], + [4, 3], + [2, 6], + [1, 2], + [6, 8], + [4, 5], + [3, 7] + ] - max_length = 10 + max_width = 10 horizon = sum(t[0] for t in jobs) num_jobs = len(jobs) @@ -57,14 +74,14 @@ def main(): ends.append(end) demands.append(jobs[i][1]) + # Create an optional copy of interval to be executed on machine 0. performed_on_m0 = model.NewBoolVar('perform_%i_on_m0' % i) performed.append(performed_on_m0) - - # Create an optional copy of interval to be executed on machine 0. start0 = model.NewIntVar(0, horizon, 'start_%i_on_m0' % i) end0 = model.NewIntVar(0, horizon, 'end_%i_on_m0' % i) - interval0 = model.NewOptionalIntervalVar( - start0, duration, end0, performed_on_m0, 'interval_%i_on_m0' % i) + interval0 = model.NewOptionalIntervalVar(start0, duration, end0, + performed_on_m0, + 'interval_%i_on_m0' % i) intervals0.append(interval0) # Create an optional copy of interval to be executed on machine 1. @@ -79,8 +96,8 @@ def main(): model.Add(start0 == start).OnlyEnforceIf(performed_on_m0) model.Add(start1 == start).OnlyEnforceIf(performed_on_m0.Not()) - # Max Length constraint (modeled as a cumulative) - model.AddCumulative(intervals, demands, max_length) + # Width constraint (modeled as a cumulative) + model.AddCumulative(intervals, demands, max_width) # Choose which machine to perform the jobs on. model.AddNoOverlap(intervals0) @@ -100,7 +117,7 @@ def main(): # Output solution. if visualization.RunFromIPython(): - output = visualization.SvgWrapper(solver.ObjectiveValue(), max_length, + output = visualization.SvgWrapper(solver.ObjectiveValue(), max_width, 40.0) output.AddTitle('Makespan = %i' % solver.ObjectiveValue()) color_manager = visualization.ColorManager() @@ -111,7 +128,7 @@ def main(): start = solver.Value(starts[i]) d_x = jobs[i][0] d_y = jobs[i][1] - s_y = performed_machine * (max_length - d_y) + s_y = performed_machine * (max_width - d_y) output.AddRectangle(start, s_y, d_x, d_y, color_manager.RandomColor(), 'black', 'j%i' % i) @@ -124,8 +141,8 @@ def main(): for i in all_jobs: performed_machine = 1 - solver.Value(performed[i]) start = solver.Value(starts[i]) - print(' - Job %i starts at %i on machine %i' % (i, start, - performed_machine)) + print(' - Job %i starts at %i on machine %i' % + (i, start, performed_machine)) print('Statistics') print(' - conflicts : %i' % solver.NumConflicts()) print(' - branches : %i' % solver.NumBranches()) @@ -133,4 +150,4 @@ def main(): if __name__ == '__main__': - main() + app.run(main) diff --git a/examples/python/golomb8.py b/examples/python/golomb8.py index fda39ac722..64f28575e6 100644 --- a/examples/python/golomb8.py +++ b/examples/python/golomb8.py @@ -47,13 +47,13 @@ def main(_): # We expand the creation of the diff array to avoid a pylint warning. diffs = [] - for i in range(0, size - 1): + for i in range(size - 1): for j in range(i + 1, size): diffs.append(marks[j] - marks[i]) solver.Add(solver.AllDifferent(diffs)) solver.Add(marks[size - 1] - marks[size - 2] > marks[1] - marks[0]) - for i in range(0, size - 2): + for i in range(size - 2): solver.Add(marks[i + 1] > marks[i]) solution = solver.Assignment() diff --git a/examples/python/shift_scheduling_sat.py b/examples/python/shift_scheduling_sat.py index 4864cd450d..cd2a0ee3e4 100644 --- a/examples/python/shift_scheduling_sat.py +++ b/examples/python/shift_scheduling_sat.py @@ -12,17 +12,18 @@ # limitations under the License. """Creates a shift scheduling problem and solves it.""" -from ortools.sat.python import cp_model - -from google.protobuf import text_format from absl import app from absl import flags +from ortools.sat.python import cp_model +from google.protobuf import text_format + FLAGS = flags.FLAGS flags.DEFINE_string('output_proto', '', 'Output file to write the cp_model proto to.') -flags.DEFINE_string('params', '', 'Sat solver parameters.') +flags.DEFINE_string('params', 'max_time_in_seconds:10.0', + 'Sat solver parameters.') def negated_bounded_span(works, start, length): @@ -376,8 +377,6 @@ def solve_shift_scheduling(params, output_proto): solver = cp_model.CpSolver() if params: text_format.Parse(params, solver.parameters) - else: - text_format.Parse(r'max_time_in_seconds:10.0', solver.parameters) solution_printer = cp_model.ObjectiveSolutionPrinter() status = solver.SolveWithSolutionCallback(model, solution_printer) diff --git a/examples/python/steel_mill_slab_sat.py b/examples/python/steel_mill_slab_sat.py index f7930772e3..4f90d5fdbc 100644 --- a/examples/python/steel_mill_slab_sat.py +++ b/examples/python/steel_mill_slab_sat.py @@ -12,31 +12,25 @@ # limitations under the License. """Solves the Stell Mill Slab problem with 4 different techniques.""" +# overloaded sum() clashes with pytype. +# pytype: disable=wrong-arg-types -import argparse import collections import time -from ortools.sat.python import cp_model +from absl import app +from absl import flags from ortools.linear_solver import pywraplp +from ortools.sat.python import cp_model -PARSER = argparse.ArgumentParser() +FLAGS = flags.FLAGS -PARSER.add_argument( - '--problem', default=2, type=int, help='Problem id to solve.') -PARSER.add_argument( - '--break_symmetries', - default=True, - type=bool, - help='Break symmetries between equivalent orders.') -PARSER.add_argument( - '--solver', - default='sat_table', - help='Method used to solve: sat, sat_table, sat_column, mip_column.') -PARSER.add_argument( - '--output_proto', - default='', - help='Output file to write the cp_model proto to.') +flags.DEFINE_integer('problem', 2, 'Problem id to solve.') +flags.DEFINE_boolean('break_symmetries', True, + 'Break symmetries between equivalent orders.') +flags.DEFINE_string( + 'solver', 'mip_column', 'Method used to solve: sat, sat_table, sat_column, ' + 'mip_column.') def build_problem(problem_id): @@ -295,7 +289,7 @@ class SteelMillSlabSolutionPrinter(cp_model.CpSolverSolutionCallback): print(line) -def steel_mill_slab(problem, break_symmetries, output_proto): +def steel_mill_slab(problem, break_symmetries): """Solves the Steel Mill Slab Problem.""" ### Load problem. (num_slabs, capacities, num_colors, orders) = build_problem(problem) @@ -308,17 +302,18 @@ def steel_mill_slab(problem, break_symmetries, output_proto): print('Solving steel mill with %i orders, %i slabs, and %i capacities' % (num_orders, num_slabs, num_capacities - 1)) - # Compute auxilliary data. + # Compute auxiliary data. widths = [x[0] for x in orders] colors = [x[1] for x in orders] max_capacity = max(capacities) loss_array = [ - min(x for x in capacities if x >= c) - c - for c in range(max_capacity + 1) + min(x for x in capacities if x >= c) - c for c in range(max_capacity + + 1) ] max_loss = max(loss_array) - orders_per_color = [[o for o in all_orders if colors[o] == c + 1] - for c in all_colors] + orders_per_color = [ + [o for o in all_orders if colors[o] == c + 1] for c in all_colors + ] unique_color_orders = [ o for o in all_orders if len(orders_per_color[colors[o] - 1]) == 1 ] @@ -335,14 +330,12 @@ def steel_mill_slab(problem, break_symmetries, output_proto): for s in all_slabs ] color_is_in_slab = [[ - model.NewBoolVar('color_%i_in_slab_%i' % (c + 1, s)) - for c in all_colors + model.NewBoolVar('color_%i_in_slab_%i' % (c + 1, s)) for c in all_colors ] for s in all_slabs] # Compute load of all slabs. for s in all_slabs: - model.Add( - sum(assign[o][s] * widths[o] for o in all_orders) == loads[s]) + model.Add(sum(assign[o][s] * widths[o] for o in all_orders) == loads[s]) # Orders are assigned to one slab. for o in all_orders: @@ -434,8 +427,9 @@ def steel_mill_slab(problem, break_symmetries, output_proto): ### Output the solution. if status in (cp_model.OPTIMAL, cp_model.FEASIBLE): - print('Loss = %i, time = %f s, %i conflicts' % ( - solver.ObjectiveValue(), solver.WallTime(), solver.NumConflicts())) + print( + 'Loss = %i, time = %f s, %i conflicts' % + (solver.ObjectiveValue(), solver.WallTime(), solver.NumConflicts())) else: print('No solution') @@ -458,19 +452,19 @@ def collect_valid_slabs_dp(capacities, colors, widths, loss_array): if assignment.load + new_width > max_capacity: continue new_colors = list(assignment.colors) - if not new_color in new_colors: + if new_color not in new_colors: new_colors.append(new_color) if len(new_colors) > 2: continue - new_assignment = valid_assignment( - orders=assignment.orders + [order_id], - load=assignment.load + new_width, - colors=new_colors) + new_assignment = valid_assignment(orders=assignment.orders + + [order_id], + load=assignment.load + new_width, + colors=new_colors) new_assignments.append(new_assignment) all_valid_assignments.extend(new_assignments) - print('%i assignments created in %.2f s' % (len(all_valid_assignments), - time.time() - start_time)) + print('%i assignments created in %.2f s' % + (len(all_valid_assignments), time.time() - start_time)) tuples = [] for assignment in all_valid_assignments: solution = [0 for _ in range(len(colors))] @@ -483,7 +477,7 @@ def collect_valid_slabs_dp(capacities, colors, widths, loss_array): return tuples -def steel_mill_slab_with_valid_slabs(problem, break_symmetries, output_proto): +def steel_mill_slab_with_valid_slabs(problem, break_symmetries): """Solves the Steel Mill Slab Problem.""" ### Load problem. (num_slabs, capacities, num_colors, orders) = build_problem(problem) @@ -496,13 +490,13 @@ def steel_mill_slab_with_valid_slabs(problem, break_symmetries, output_proto): print('Solving steel mill with %i orders, %i slabs, and %i capacities' % (num_orders, num_slabs, num_capacities - 1)) - # Compute auxilliary data. + # Compute auxiliary data. widths = [x[0] for x in orders] colors = [x[1] for x in orders] max_capacity = max(capacities) loss_array = [ - min(x for x in capacities if x >= c) - c - for c in range(max_capacity + 1) + min(x for x in capacities if x >= c) - c for c in range(max_capacity + + 1) ] max_loss = max(loss_array) @@ -513,21 +507,18 @@ def steel_mill_slab_with_valid_slabs(problem, break_symmetries, output_proto): assign = [[ model.NewBoolVar('assign_%i_to_slab_%i' % (o, s)) for s in all_slabs ] for o in all_orders] - loads = [ - model.NewIntVar(0, max_capacity, 'load_%i' % s) for s in all_slabs - ] + loads = [model.NewIntVar(0, max_capacity, 'load_%i' % s) for s in all_slabs] losses = [model.NewIntVar(0, max_loss, 'loss_%i' % s) for s in all_slabs] unsorted_valid_slabs = collect_valid_slabs_dp(capacities, colors, widths, loss_array) # Sort slab by descending load/loss. Remove duplicates. - valid_slabs = sorted( - unsorted_valid_slabs, key=lambda c: 1000 * c[-1] + c[-2]) + valid_slabs = sorted(unsorted_valid_slabs, + key=lambda c: 1000 * c[-1] + c[-2]) for s in all_slabs: - model.AddAllowedAssignments( - [assign[o][s] for o in all_orders] + [losses[s], loads[s]], - valid_slabs) + model.AddAllowedAssignments([assign[o][s] for o in all_orders] + + [losses[s], loads[s]], valid_slabs) # Orders are assigned to one slab. for o in all_orders: @@ -545,8 +536,9 @@ def steel_mill_slab_with_valid_slabs(problem, break_symmetries, output_proto): print('Breaking symmetries') width_to_unique_color_order = {} ordered_equivalent_orders = [] - orders_per_color = [[o for o in all_orders if colors[o] == c + 1] - for c in all_colors] + orders_per_color = [ + [o for o in all_orders if colors[o] == c + 1] for c in all_colors + ] for c in all_colors: colored_orders = orders_per_color[c] if not colored_orders: @@ -568,8 +560,7 @@ def steel_mill_slab_with_valid_slabs(problem, break_symmetries, output_proto): for w, os in local_width_to_order.items(): if len(os) > 1: for p in range(len(os) - 1): - ordered_equivalent_orders.append((os[p], - os[p + 1])) + ordered_equivalent_orders.append((os[p], os[p + 1])) for w, os in width_to_unique_color_order.items(): if len(os) > 1: for p in range(len(os) - 1): @@ -597,12 +588,6 @@ def steel_mill_slab_with_valid_slabs(problem, break_symmetries, output_proto): print('Model created') - # Output model proto to file. - if output_proto: - output_file = open(output_proto, 'w') - output_file.write(str(model.Proto())) - output_file.close() - ### Solve model. solver = cp_model.CpSolver() solver.num_search_workers = 8 @@ -612,13 +597,14 @@ def steel_mill_slab_with_valid_slabs(problem, break_symmetries, output_proto): ### Output the solution. if status == cp_model.OPTIMAL: - print('Loss = %i, time = %.2f s, %i conflicts' % ( - solver.ObjectiveValue(), solver.WallTime(), solver.NumConflicts())) + print( + 'Loss = %i, time = %.2f s, %i conflicts' % + (solver.ObjectiveValue(), solver.WallTime(), solver.NumConflicts())) else: print('No solution') -def steel_mill_slab_with_column_generation(problem, output_proto): +def steel_mill_slab_with_column_generation(problem): """Solves the Steel Mill Slab Problem.""" ### Load problem. (num_slabs, capacities, _, orders) = build_problem(problem) @@ -629,13 +615,13 @@ def steel_mill_slab_with_column_generation(problem, output_proto): print('Solving steel mill with %i orders, %i slabs, and %i capacities' % (num_orders, num_slabs, num_capacities - 1)) - # Compute auxilliary data. + # Compute auxiliary data. widths = [x[0] for x in orders] colors = [x[1] for x in orders] max_capacity = max(capacities) loss_array = [ - min(x for x in capacities if x >= c) - c - for c in range(max_capacity + 1) + min(x for x in capacities if x >= c) - c for c in range(max_capacity + + 1) ] ### Model problem. @@ -645,8 +631,8 @@ def steel_mill_slab_with_column_generation(problem, output_proto): loss_array) # Sort slab by descending load/loss. Remove duplicates. - valid_slabs = sorted( - unsorted_valid_slabs, key=lambda c: 1000 * c[-1] + c[-2]) + valid_slabs = sorted(unsorted_valid_slabs, + key=lambda c: 1000 * c[-1] + c[-2]) all_valid_slabs = range(len(valid_slabs)) # create model and decision variables. @@ -655,7 +641,8 @@ def steel_mill_slab_with_column_generation(problem, output_proto): for order_id in all_orders: model.Add( - sum(selected[i] for i, slab in enumerate(valid_slabs) + sum(selected[i] + for i, slab in enumerate(valid_slabs) if slab[order_id]) == 1) # Redundant constraint (sum of loads == sum of widths). @@ -669,22 +656,18 @@ def steel_mill_slab_with_column_generation(problem, output_proto): print('Model created') - # Output model proto to file. - if output_proto: - output_file = open(output_proto, 'w') - output_file.write(str(model.Proto())) - output_file.close() - ### Solve model. solver = cp_model.CpSolver() solver.parameters.num_search_workers = 8 + solver.parameters.log_search_progress = True solution_printer = cp_model.ObjectiveSolutionPrinter() status = solver.SolveWithSolutionCallback(model, solution_printer) ### Output the solution. if status in (cp_model.OPTIMAL, cp_model.FEASIBLE): - print('Loss = %i, time = %.2f s, %i conflicts' % ( - solver.ObjectiveValue(), solver.WallTime(), solver.NumConflicts())) + print( + 'Loss = %i, time = %.2f s, %i conflicts' % + (solver.ObjectiveValue(), solver.WallTime(), solver.NumConflicts())) else: print('No solution') @@ -700,13 +683,13 @@ def steel_mill_slab_with_mip_column_generation(problem): print('Solving steel mill with %i orders, %i slabs, and %i capacities' % (num_orders, num_slabs, num_capacities - 1)) - # Compute auxilliary data. + # Compute auxiliary data. widths = [x[0] for x in orders] colors = [x[1] for x in orders] max_capacity = max(capacities) loss_array = [ - min(x for x in capacities if x >= c) - c - for c in range(max_capacity + 1) + min(x for x in capacities if x >= c) - c for c in range(max_capacity + + 1) ] ### Model problem. @@ -715,21 +698,22 @@ def steel_mill_slab_with_mip_column_generation(problem): unsorted_valid_slabs = collect_valid_slabs_dp(capacities, colors, widths, loss_array) # Sort slab by descending load/loss. Remove duplicates. - valid_slabs = sorted( - unsorted_valid_slabs, key=lambda c: 1000 * c[-1] + c[-2]) + valid_slabs = sorted(unsorted_valid_slabs, + key=lambda c: 1000 * c[-1] + c[-2]) all_valid_slabs = range(len(valid_slabs)) # create model and decision variables. start_time = time.time() solver = pywraplp.Solver('Steel', - pywraplp.Solver.CBC_MIXED_INTEGER_PROGRAMMING) + pywraplp.Solver.SCIP_MIXED_INTEGER_PROGRAMMING) selected = [ solver.IntVar(0.0, 1.0, 'selected_%i' % i) for i in all_valid_slabs ] for order in all_orders: solver.Add( - sum(selected[i] for i in all_valid_slabs + sum(selected[i] + for i in all_valid_slabs if valid_slabs[i][order]) == 1) # Redundant constraint (sum of loads == sum of widths). @@ -751,19 +735,16 @@ def steel_mill_slab_with_mip_column_generation(problem): print('No solution') -def main(args): - '''Main function''' - if args.solver == 'sat': - steel_mill_slab(args.problem, args.break_symmetries, args.output_proto) - elif args.solver == 'sat_table': - steel_mill_slab_with_valid_slabs(args.problem, args.break_symmetries, - args.output_proto) - elif args.solver == 'sat_column': - steel_mill_slab_with_column_generation(args.problem, args.output_proto) +def main(_): + if FLAGS.solver == 'sat': + steel_mill_slab(FLAGS.problem, FLAGS.break_symmetries) + elif FLAGS.solver == 'sat_table': + steel_mill_slab_with_valid_slabs(FLAGS.problem, FLAGS.break_symmetries) + elif FLAGS.solver == 'sat_column': + steel_mill_slab_with_column_generation(FLAGS.problem) else: # 'mip_column' - steel_mill_slab_with_mip_column_generation(args.problem) + steel_mill_slab_with_mip_column_generation(FLAGS.problem) if __name__ == '__main__': - main(PARSER.parse_args()) -# vim: set tw=2 ts=2 sw=2 expandtab: + app.run(main)