2021-05-03 12:11:39 +02:00
|
|
|
#!/usr/bin/env python3
|
2024-01-04 13:43:15 +01:00
|
|
|
# Copyright 2010-2024 Google LLC
|
2017-11-22 14:55:44 +01: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.
|
2023-07-01 06:06:53 +02:00
|
|
|
|
2018-12-30 18:12:26 +01:00
|
|
|
"""Solves the Stell Mill Slab problem with 4 different techniques."""
|
2017-11-22 14:55:44 +01:00
|
|
|
|
2020-11-30 11:59:56 +01:00
|
|
|
# overloaded sum() clashes with pytype.
|
2018-12-30 21:17:38 +01:00
|
|
|
|
|
|
|
|
import collections
|
2018-08-28 18:34:20 +02:00
|
|
|
import time
|
2018-12-30 21:17:38 +01:00
|
|
|
|
2020-11-30 11:59:56 +01:00
|
|
|
from absl import app
|
|
|
|
|
from absl import flags
|
2023-07-01 06:06:53 +02:00
|
|
|
from google.protobuf import text_format
|
2020-11-30 11:59:56 +01:00
|
|
|
from ortools.sat.python import cp_model
|
|
|
|
|
|
2023-07-01 06:06:53 +02:00
|
|
|
|
|
|
|
|
_PROBLEM = flags.DEFINE_integer("problem", 2, "Problem id to solve.")
|
2022-10-07 18:21:23 +02:00
|
|
|
_BREAK_SYMMETRIES = flags.DEFINE_boolean(
|
2023-07-01 06:06:53 +02:00
|
|
|
"break_symmetries", True, "Break symmetries between equivalent orders."
|
|
|
|
|
)
|
2022-10-07 18:21:23 +02:00
|
|
|
_SOLVER = flags.DEFINE_string(
|
2023-07-01 06:06:53 +02:00
|
|
|
"solver", "sat_column", "Method used to solve: sat, sat_table, sat_column."
|
|
|
|
|
)
|
|
|
|
|
_PARAMS = flags.DEFINE_string(
|
|
|
|
|
"params",
|
|
|
|
|
"max_time_in_seconds:20,num_workers:8,log_search_progress:true",
|
|
|
|
|
"CP-SAT parameters.",
|
|
|
|
|
)
|
2017-10-25 14:20:56 +02:00
|
|
|
|
|
|
|
|
|
2018-11-20 05:44:21 -08:00
|
|
|
def build_problem(problem_id):
|
2018-11-11 09:39:59 +01:00
|
|
|
"""Build problem data."""
|
2023-07-01 06:06:53 +02:00
|
|
|
capacities = None
|
|
|
|
|
num_colors = None
|
|
|
|
|
num_slabs = None
|
|
|
|
|
orders = None
|
|
|
|
|
|
2018-11-11 09:39:59 +01:00
|
|
|
if problem_id == 0:
|
|
|
|
|
capacities = [
|
2023-07-13 14:28:36 +02:00
|
|
|
# fmt:off
|
|
|
|
|
0, 12, 14, 17, 18, 19, 20, 23, 24, 25, 26, 27, 28, 29, 30, 32, 35, 39, 42, 43, 44,
|
|
|
|
|
# fmt:on
|
2018-11-11 09:39:59 +01:00
|
|
|
]
|
|
|
|
|
num_colors = 88
|
|
|
|
|
num_slabs = 111
|
2023-07-13 14:28:36 +02:00
|
|
|
orders = [ # (size, color)
|
|
|
|
|
# fmt:off
|
|
|
|
|
(4, 1), (22, 2), (9, 3), (5, 4), (8, 5), (3, 6), (3, 4), (4, 7),
|
|
|
|
|
(7, 4), (7, 8), (3, 6), (2, 6), (2, 4), (8, 9), (5, 10), (7, 11),
|
|
|
|
|
(4, 7), (7, 11), (5, 10), (7, 11), (8, 9), (3, 1), (25, 12), (14, 13),
|
|
|
|
|
(3, 6), (22, 14), (19, 15), (19, 15), (22, 16), (22, 17), (22, 18),
|
|
|
|
|
(20, 19), (22, 20), (5, 21), (4, 22), (10, 23), (26, 24), (17, 25),
|
|
|
|
|
(20, 26), (16, 27), (10, 28), (19, 29), (10, 30), (10, 31), (23, 32),
|
|
|
|
|
(22, 33), (26, 34), (27, 35), (22, 36), (27, 37), (22, 38), (22, 39),
|
|
|
|
|
(13, 40), (14, 41), (16, 27), (26, 34), (26, 42), (27, 35), (22, 36),
|
|
|
|
|
(20, 43), (26, 24), (22, 44), (13, 45), (19, 46), (20, 47), (16, 48),
|
|
|
|
|
(15, 49), (17, 50), (10, 28), (20, 51), (5, 52), (26, 24), (19, 53),
|
|
|
|
|
(15, 54), (10, 55), (10, 56), (13, 57), (13, 58), (13, 59), (12, 60),
|
|
|
|
|
(12, 61), (18, 62), (10, 63), (18, 64), (16, 65), (20, 66), (12, 67),
|
|
|
|
|
(6, 68), (6, 68), (15, 69), (15, 70), (15, 70), (21, 71), (30, 72),
|
|
|
|
|
(30, 73), (30, 74), (30, 75), (23, 76), (15, 77), (15, 78), (27, 79),
|
|
|
|
|
(27, 80), (27, 81), (27, 82), (27, 83), (27, 84), (27, 79), (27, 85),
|
|
|
|
|
(27, 86), (10, 87), (3, 88),
|
|
|
|
|
# fmt:on
|
2018-11-11 09:39:59 +01:00
|
|
|
]
|
|
|
|
|
elif problem_id == 1:
|
|
|
|
|
capacities = [0, 17, 44]
|
|
|
|
|
num_colors = 23
|
|
|
|
|
num_slabs = 30
|
2023-07-13 14:28:36 +02:00
|
|
|
orders = [ # (size, color)
|
|
|
|
|
# fmt:off
|
|
|
|
|
(4, 1), (22, 2), (9, 3), (5, 4), (8, 5), (3, 6), (3, 4), (4, 7), (7, 4),
|
|
|
|
|
(7, 8), (3, 6), (2, 6), (2, 4), (8, 9), (5, 10), (7, 11), (4, 7), (7, 11),
|
|
|
|
|
(5, 10), (7, 11), (8, 9), (3, 1), (25, 12), (14, 13), (3, 6), (22, 14),
|
|
|
|
|
(19, 15), (19, 15), (22, 16), (22, 17), (22, 18), (20, 19), (22, 20),
|
|
|
|
|
(5, 21), (4, 22), (10, 23),
|
|
|
|
|
# fmt:on
|
2018-11-11 09:39:59 +01:00
|
|
|
]
|
|
|
|
|
elif problem_id == 2:
|
|
|
|
|
capacities = [0, 17, 44]
|
|
|
|
|
num_colors = 15
|
|
|
|
|
num_slabs = 20
|
2023-07-13 14:28:36 +02:00
|
|
|
orders = [ # (size, color)
|
|
|
|
|
# fmt:off
|
|
|
|
|
(4, 1), (22, 2), (9, 3), (5, 4), (8, 5), (3, 6), (3, 4), (4, 7), (7, 4),
|
|
|
|
|
(7, 8), (3, 6), (2, 6), (2, 4), (8, 9), (5, 10), (7, 11), (4, 7), (7, 11),
|
|
|
|
|
(5, 10), (7, 11), (8, 9), (3, 1), (25, 12), (14, 13), (3, 6), (22, 14),
|
|
|
|
|
(19, 15), (19, 15),
|
|
|
|
|
# fmt:on
|
2018-11-11 09:39:59 +01:00
|
|
|
]
|
|
|
|
|
|
|
|
|
|
elif problem_id == 3:
|
|
|
|
|
capacities = [0, 17, 44]
|
|
|
|
|
num_colors = 8
|
|
|
|
|
num_slabs = 10
|
2023-07-13 14:28:36 +02:00
|
|
|
orders = [ # (size, color)
|
|
|
|
|
# fmt:off
|
|
|
|
|
(4, 1), (22, 2), (9, 3), (5, 4), (8, 5), (3, 6), (3, 4), (4, 7),
|
|
|
|
|
(7, 4), (7, 8), (3, 6),
|
|
|
|
|
# fmt:on
|
2018-11-11 09:39:59 +01:00
|
|
|
]
|
|
|
|
|
|
|
|
|
|
return (num_slabs, capacities, num_colors, orders)
|
2017-10-25 14:20:56 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class SteelMillSlabSolutionPrinter(cp_model.CpSolverSolutionCallback):
|
2018-11-11 09:39:59 +01:00
|
|
|
"""Print intermediate solutions."""
|
|
|
|
|
|
|
|
|
|
def __init__(self, orders, assign, load, loss):
|
|
|
|
|
cp_model.CpSolverSolutionCallback.__init__(self)
|
|
|
|
|
self.__orders = orders
|
|
|
|
|
self.__assign = assign
|
|
|
|
|
self.__load = load
|
|
|
|
|
self.__loss = loss
|
|
|
|
|
self.__solution_count = 0
|
|
|
|
|
self.__all_orders = range(len(orders))
|
|
|
|
|
self.__all_slabs = range(len(assign[0]))
|
|
|
|
|
self.__start_time = time.time()
|
|
|
|
|
|
2018-11-20 05:44:21 -08:00
|
|
|
def on_solution_callback(self):
|
2018-12-30 21:17:38 +01:00
|
|
|
"""Called on each new solution."""
|
2018-11-11 09:39:59 +01:00
|
|
|
current_time = time.time()
|
2023-11-16 19:46:56 +01:00
|
|
|
objective = sum(self.value(l) for l in self.__loss)
|
2023-07-01 06:06:53 +02:00
|
|
|
print(
|
|
|
|
|
"Solution %i, time = %f s, objective = %i"
|
|
|
|
|
% (self.__solution_count, current_time - self.__start_time, objective)
|
|
|
|
|
)
|
2018-11-11 09:39:59 +01:00
|
|
|
self.__solution_count += 1
|
2023-07-01 06:06:53 +02:00
|
|
|
orders_in_slab = [
|
2023-11-16 19:46:56 +01:00
|
|
|
[o for o in self.__all_orders if self.value(self.__assign[o][s])]
|
2023-07-01 06:06:53 +02:00
|
|
|
for s in self.__all_slabs
|
|
|
|
|
]
|
2018-11-11 09:39:59 +01:00
|
|
|
for s in self.__all_slabs:
|
|
|
|
|
if orders_in_slab[s]:
|
2023-07-01 06:06:53 +02:00
|
|
|
line = " - slab %i, load = %i, loss = %i, orders = [" % (
|
|
|
|
|
s,
|
2023-11-16 19:46:56 +01:00
|
|
|
self.value(self.__load[s]),
|
|
|
|
|
self.value(self.__loss[s]),
|
2023-07-01 06:06:53 +02:00
|
|
|
)
|
2018-11-11 09:39:59 +01:00
|
|
|
for o in orders_in_slab[s]:
|
2023-07-01 06:06:53 +02:00
|
|
|
line += "#%i(w%i, c%i) " % (
|
|
|
|
|
o,
|
|
|
|
|
self.__orders[o][0],
|
|
|
|
|
self.__orders[o][1],
|
|
|
|
|
)
|
|
|
|
|
line += "]"
|
2018-11-11 09:39:59 +01:00
|
|
|
print(line)
|
|
|
|
|
|
2017-10-25 14:20:56 +02:00
|
|
|
|
2024-07-23 14:07:41 +02:00
|
|
|
def steel_mill_slab(problem, break_symmetries) -> None:
|
2018-11-11 09:39:59 +01:00
|
|
|
"""Solves the Steel Mill Slab Problem."""
|
|
|
|
|
### Load problem.
|
2018-11-20 05:44:21 -08:00
|
|
|
(num_slabs, capacities, num_colors, orders) = build_problem(problem)
|
2018-11-11 09:39:59 +01:00
|
|
|
|
|
|
|
|
num_orders = len(orders)
|
|
|
|
|
num_capacities = len(capacities)
|
|
|
|
|
all_slabs = range(num_slabs)
|
|
|
|
|
all_colors = range(num_colors)
|
|
|
|
|
all_orders = range(len(orders))
|
2023-07-01 06:06:53 +02:00
|
|
|
print(
|
|
|
|
|
"Solving steel mill with %i orders, %i slabs, and %i capacities"
|
|
|
|
|
% (num_orders, num_slabs, num_capacities - 1)
|
|
|
|
|
)
|
2018-11-11 09:39:59 +01:00
|
|
|
|
2020-11-30 11:59:56 +01:00
|
|
|
# Compute auxiliary data.
|
2018-11-11 09:39:59 +01:00
|
|
|
widths = [x[0] for x in orders]
|
|
|
|
|
colors = [x[1] for x in orders]
|
|
|
|
|
max_capacity = max(capacities)
|
|
|
|
|
loss_array = [
|
2023-07-01 06:06:53 +02:00
|
|
|
min(x for x in capacities if x >= c) - c for c in range(max_capacity + 1)
|
2018-11-11 09:39:59 +01:00
|
|
|
]
|
|
|
|
|
max_loss = max(loss_array)
|
2020-11-30 11:59:56 +01:00
|
|
|
orders_per_color = [
|
|
|
|
|
[o for o in all_orders if colors[o] == c + 1] for c in all_colors
|
|
|
|
|
]
|
2018-11-11 09:39:59 +01:00
|
|
|
unique_color_orders = [
|
|
|
|
|
o for o in all_orders if len(orders_per_color[colors[o] - 1]) == 1
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
### Model problem.
|
|
|
|
|
|
|
|
|
|
# Create the model and the decision variables.
|
|
|
|
|
model = cp_model.CpModel()
|
2023-07-01 06:06:53 +02:00
|
|
|
assign = [
|
2023-11-16 19:46:56 +01:00
|
|
|
[model.new_bool_var("assign_%i_to_slab_%i" % (o, s)) for s in all_slabs]
|
2023-07-01 06:06:53 +02:00
|
|
|
for o in all_orders
|
|
|
|
|
]
|
2023-11-16 19:46:56 +01:00
|
|
|
loads = [
|
|
|
|
|
model.new_int_var(0, max_capacity, "load_of_slab_%i" % s) for s in all_slabs
|
|
|
|
|
]
|
2023-07-01 06:06:53 +02:00
|
|
|
color_is_in_slab = [
|
2023-11-16 19:46:56 +01:00
|
|
|
[model.new_bool_var("color_%i_in_slab_%i" % (c + 1, s)) for c in all_colors]
|
2018-11-11 09:39:59 +01:00
|
|
|
for s in all_slabs
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
# Compute load of all slabs.
|
2017-10-25 14:20:56 +02:00
|
|
|
for s in all_slabs:
|
2023-11-16 19:46:56 +01:00
|
|
|
model.add(sum(assign[o][s] * widths[o] for o in all_orders) == loads[s])
|
2017-10-25 14:20:56 +02:00
|
|
|
|
2018-11-11 09:39:59 +01:00
|
|
|
# Orders are assigned to one slab.
|
|
|
|
|
for o in all_orders:
|
2023-11-16 19:46:56 +01:00
|
|
|
model.add_exactly_one(assign[o])
|
2017-10-25 14:20:56 +02:00
|
|
|
|
2018-11-11 09:39:59 +01:00
|
|
|
# Redundant constraint (sum of loads == sum of widths).
|
2023-11-16 19:46:56 +01:00
|
|
|
model.add(sum(loads) == sum(widths))
|
2017-10-25 14:20:56 +02:00
|
|
|
|
2018-11-11 09:39:59 +01:00
|
|
|
# Link present_colors and assign.
|
|
|
|
|
for c in all_colors:
|
|
|
|
|
for s in all_slabs:
|
|
|
|
|
for o in orders_per_color[c]:
|
2023-11-16 19:46:56 +01:00
|
|
|
model.add_implication(assign[o][s], color_is_in_slab[s][c])
|
2023-12-15 14:10:44 +01:00
|
|
|
model.add_implication(~color_is_in_slab[s][c], ~assign[o][s])
|
2017-10-25 14:20:56 +02:00
|
|
|
|
2018-11-11 09:39:59 +01:00
|
|
|
# At most two colors per slab.
|
|
|
|
|
for s in all_slabs:
|
2023-11-16 19:46:56 +01:00
|
|
|
model.add(sum(color_is_in_slab[s]) <= 2)
|
2017-10-25 14:20:56 +02:00
|
|
|
|
2018-11-11 09:39:59 +01:00
|
|
|
# Project previous constraint on unique_color_orders
|
|
|
|
|
for s in all_slabs:
|
2023-11-16 19:46:56 +01:00
|
|
|
model.add(sum(assign[o][s] for o in unique_color_orders) <= 2)
|
2017-10-25 14:20:56 +02:00
|
|
|
|
2018-11-11 09:39:59 +01:00
|
|
|
# Symmetry breaking.
|
|
|
|
|
for s in range(num_slabs - 1):
|
2023-11-16 19:46:56 +01:00
|
|
|
model.add(loads[s] >= loads[s + 1])
|
2018-11-11 09:39:59 +01:00
|
|
|
|
|
|
|
|
# Collect equivalent orders.
|
2017-10-26 11:49:22 +02:00
|
|
|
width_to_unique_color_order = {}
|
|
|
|
|
ordered_equivalent_orders = []
|
|
|
|
|
for c in all_colors:
|
2018-11-11 09:39:59 +01:00
|
|
|
colored_orders = orders_per_color[c]
|
|
|
|
|
if not colored_orders:
|
|
|
|
|
continue
|
|
|
|
|
if len(colored_orders) == 1:
|
|
|
|
|
o = colored_orders[0]
|
|
|
|
|
w = widths[o]
|
|
|
|
|
if w not in width_to_unique_color_order:
|
|
|
|
|
width_to_unique_color_order[w] = [o]
|
|
|
|
|
else:
|
|
|
|
|
width_to_unique_color_order[w].append(o)
|
2017-10-26 11:49:22 +02:00
|
|
|
else:
|
2018-11-11 09:39:59 +01:00
|
|
|
local_width_to_order = {}
|
|
|
|
|
for o in colored_orders:
|
|
|
|
|
w = widths[o]
|
|
|
|
|
if w not in local_width_to_order:
|
|
|
|
|
local_width_to_order[w] = []
|
|
|
|
|
local_width_to_order[w].append(o)
|
2023-07-01 06:06:53 +02:00
|
|
|
for _, os in local_width_to_order.items():
|
2018-11-11 09:39:59 +01:00
|
|
|
if len(os) > 1:
|
|
|
|
|
for p in range(len(os) - 1):
|
|
|
|
|
ordered_equivalent_orders.append((os[p], os[p + 1]))
|
2023-07-01 06:06:53 +02:00
|
|
|
for _, os in width_to_unique_color_order.items():
|
2018-11-11 09:39:59 +01:00
|
|
|
if len(os) > 1:
|
|
|
|
|
for p in range(len(os) - 1):
|
|
|
|
|
ordered_equivalent_orders.append((os[p], os[p + 1]))
|
2017-10-26 11:49:22 +02:00
|
|
|
|
|
|
|
|
# Create position variables if there are symmetries to be broken.
|
2018-11-11 09:39:59 +01:00
|
|
|
if break_symmetries and ordered_equivalent_orders:
|
2023-07-01 06:06:53 +02:00
|
|
|
print(
|
|
|
|
|
" - creating %i symmetry breaking constraints"
|
|
|
|
|
% len(ordered_equivalent_orders)
|
|
|
|
|
)
|
2018-11-11 09:39:59 +01:00
|
|
|
positions = {}
|
|
|
|
|
for p in ordered_equivalent_orders:
|
|
|
|
|
if p[0] not in positions:
|
2023-11-16 19:46:56 +01:00
|
|
|
positions[p[0]] = model.new_int_var(
|
2023-07-01 06:06:53 +02:00
|
|
|
0, num_slabs - 1, "position_of_slab_%i" % p[0]
|
|
|
|
|
)
|
2023-11-16 19:46:56 +01:00
|
|
|
model.add_map_domain(positions[p[0]], assign[p[0]])
|
2018-11-11 09:39:59 +01:00
|
|
|
if p[1] not in positions:
|
2023-11-16 19:46:56 +01:00
|
|
|
positions[p[1]] = model.new_int_var(
|
2023-07-01 06:06:53 +02:00
|
|
|
0, num_slabs - 1, "position_of_slab_%i" % p[1]
|
|
|
|
|
)
|
2023-11-16 19:46:56 +01:00
|
|
|
model.add_map_domain(positions[p[1]], assign[p[1]])
|
2018-11-11 09:39:59 +01:00
|
|
|
# Finally add the symmetry breaking constraint.
|
2023-11-16 19:46:56 +01:00
|
|
|
model.add(positions[p[0]] <= positions[p[1]])
|
2018-11-11 09:39:59 +01:00
|
|
|
|
|
|
|
|
# Objective.
|
2023-11-16 19:46:56 +01:00
|
|
|
obj = model.new_int_var(0, num_slabs * max_loss, "obj")
|
|
|
|
|
losses = [model.new_int_var(0, max_loss, "loss_%i" % s) for s in all_slabs]
|
2018-11-11 09:39:59 +01:00
|
|
|
for s in all_slabs:
|
2023-11-16 19:46:56 +01:00
|
|
|
model.add_element(loads[s], loss_array, losses[s])
|
|
|
|
|
model.add(obj == sum(losses))
|
|
|
|
|
model.minimize(obj)
|
2018-11-11 09:39:59 +01:00
|
|
|
|
|
|
|
|
### Solve model.
|
|
|
|
|
solver = cp_model.CpSolver()
|
2023-07-01 06:06:53 +02:00
|
|
|
if _PARAMS.value:
|
|
|
|
|
text_format.Parse(_PARAMS.value, solver.parameters)
|
2018-12-30 18:12:26 +01:00
|
|
|
objective_printer = cp_model.ObjectiveSolutionPrinter()
|
2023-11-16 19:46:56 +01:00
|
|
|
status = solver.solve(model, objective_printer)
|
2018-11-11 09:39:59 +01:00
|
|
|
|
|
|
|
|
### Output the solution.
|
2018-12-30 23:08:34 +01:00
|
|
|
if status in (cp_model.OPTIMAL, cp_model.FEASIBLE):
|
2020-11-30 11:59:56 +01:00
|
|
|
print(
|
2023-07-01 06:06:53 +02:00
|
|
|
"Loss = %i, time = %f s, %i conflicts"
|
2023-11-16 19:46:56 +01:00
|
|
|
% (solver.objective_value, solver.wall_time, solver.num_conflicts)
|
2023-07-01 06:06:53 +02:00
|
|
|
)
|
2018-11-11 09:39:59 +01:00
|
|
|
else:
|
2023-07-01 06:06:53 +02:00
|
|
|
print("No solution")
|
2018-11-11 09:39:59 +01:00
|
|
|
|
|
|
|
|
|
2018-12-30 21:17:38 +01:00
|
|
|
def collect_valid_slabs_dp(capacities, colors, widths, loss_array):
|
2018-11-11 09:39:59 +01:00
|
|
|
"""Collect valid columns (assign, loss) for one slab."""
|
2018-12-30 18:12:26 +01:00
|
|
|
start_time = time.time()
|
|
|
|
|
|
2018-12-30 21:17:38 +01:00
|
|
|
max_capacity = max(capacities)
|
2018-12-30 18:12:26 +01:00
|
|
|
|
2023-07-01 06:06:53 +02:00
|
|
|
valid_assignment = collections.namedtuple("valid_assignment", "orders load colors")
|
2018-12-30 21:17:38 +01:00
|
|
|
all_valid_assignments = [valid_assignment(orders=[], load=0, colors=[])]
|
2018-12-30 18:12:26 +01:00
|
|
|
|
2023-07-01 06:06:53 +02:00
|
|
|
for order_id, new_color in enumerate(colors):
|
2018-12-30 21:17:38 +01:00
|
|
|
new_width = widths[order_id]
|
|
|
|
|
new_assignments = []
|
|
|
|
|
for assignment in all_valid_assignments:
|
|
|
|
|
if assignment.load + new_width > max_capacity:
|
|
|
|
|
continue
|
2019-01-02 13:38:56 +01:00
|
|
|
new_colors = list(assignment.colors)
|
2020-11-30 11:59:56 +01:00
|
|
|
if new_color not in new_colors:
|
2018-12-30 21:17:38 +01:00
|
|
|
new_colors.append(new_color)
|
2018-12-30 22:54:56 +01:00
|
|
|
if len(new_colors) > 2:
|
|
|
|
|
continue
|
2023-07-01 06:06:53 +02:00
|
|
|
new_assignment = valid_assignment(
|
|
|
|
|
orders=assignment.orders + [order_id],
|
|
|
|
|
load=assignment.load + new_width,
|
|
|
|
|
colors=new_colors,
|
|
|
|
|
)
|
2018-12-30 21:17:38 +01:00
|
|
|
new_assignments.append(new_assignment)
|
|
|
|
|
all_valid_assignments.extend(new_assignments)
|
|
|
|
|
|
2023-07-01 06:06:53 +02:00
|
|
|
print(
|
|
|
|
|
"%i assignments created in %.2f s"
|
|
|
|
|
% (len(all_valid_assignments), time.time() - start_time)
|
|
|
|
|
)
|
2018-12-30 21:17:38 +01:00
|
|
|
tuples = []
|
|
|
|
|
for assignment in all_valid_assignments:
|
2023-07-01 06:06:53 +02:00
|
|
|
solution = [0] * len(colors)
|
2018-12-30 21:17:38 +01:00
|
|
|
for i in assignment.orders:
|
|
|
|
|
solution[i] = 1
|
|
|
|
|
solution.append(loss_array[assignment.load])
|
|
|
|
|
solution.append(assignment.load)
|
|
|
|
|
tuples.append(solution)
|
|
|
|
|
|
|
|
|
|
return tuples
|
2018-11-11 09:39:59 +01:00
|
|
|
|
|
|
|
|
|
2020-11-30 11:59:56 +01:00
|
|
|
def steel_mill_slab_with_valid_slabs(problem, break_symmetries):
|
2018-11-11 09:39:59 +01:00
|
|
|
"""Solves the Steel Mill Slab Problem."""
|
|
|
|
|
### Load problem.
|
2018-11-20 05:44:21 -08:00
|
|
|
(num_slabs, capacities, num_colors, orders) = build_problem(problem)
|
2018-11-11 09:39:59 +01:00
|
|
|
|
|
|
|
|
num_orders = len(orders)
|
|
|
|
|
num_capacities = len(capacities)
|
|
|
|
|
all_slabs = range(num_slabs)
|
|
|
|
|
all_colors = range(num_colors)
|
|
|
|
|
all_orders = range(len(orders))
|
2023-07-01 06:06:53 +02:00
|
|
|
print(
|
|
|
|
|
"Solving steel mill with %i orders, %i slabs, and %i capacities"
|
|
|
|
|
% (num_orders, num_slabs, num_capacities - 1)
|
|
|
|
|
)
|
2018-11-11 09:39:59 +01:00
|
|
|
|
2020-11-30 11:59:56 +01:00
|
|
|
# Compute auxiliary data.
|
2018-11-11 09:39:59 +01:00
|
|
|
widths = [x[0] for x in orders]
|
|
|
|
|
colors = [x[1] for x in orders]
|
|
|
|
|
max_capacity = max(capacities)
|
|
|
|
|
loss_array = [
|
2023-07-01 06:06:53 +02:00
|
|
|
min(x for x in capacities if x >= c) - c for c in range(max_capacity + 1)
|
2018-11-11 09:39:59 +01:00
|
|
|
]
|
|
|
|
|
max_loss = max(loss_array)
|
|
|
|
|
|
|
|
|
|
### Model problem.
|
|
|
|
|
|
|
|
|
|
# Create the model and the decision variables.
|
|
|
|
|
model = cp_model.CpModel()
|
2023-07-01 06:06:53 +02:00
|
|
|
assign = [
|
2023-11-16 19:46:56 +01:00
|
|
|
[model.new_bool_var("assign_%i_to_slab_%i" % (o, s)) for s in all_slabs]
|
2023-07-01 06:06:53 +02:00
|
|
|
for o in all_orders
|
|
|
|
|
]
|
2023-11-16 19:46:56 +01:00
|
|
|
loads = [model.new_int_var(0, max_capacity, "load_%i" % s) for s in all_slabs]
|
|
|
|
|
losses = [model.new_int_var(0, max_loss, "loss_%i" % s) for s in all_slabs]
|
2023-07-01 06:06:53 +02:00
|
|
|
|
|
|
|
|
unsorted_valid_slabs = collect_valid_slabs_dp(
|
|
|
|
|
capacities, colors, widths, loss_array
|
|
|
|
|
)
|
2018-11-11 09:39:59 +01:00
|
|
|
# Sort slab by descending load/loss. Remove duplicates.
|
2023-07-01 06:06:53 +02:00
|
|
|
valid_slabs = sorted(unsorted_valid_slabs, key=lambda c: 1000 * c[-1] + c[-2])
|
2018-11-11 09:39:59 +01:00
|
|
|
|
|
|
|
|
for s in all_slabs:
|
2023-11-16 19:46:56 +01:00
|
|
|
model.add_allowed_assignments(
|
2023-07-01 06:06:53 +02:00
|
|
|
[assign[o][s] for o in all_orders] + [losses[s], loads[s]], valid_slabs
|
|
|
|
|
)
|
2018-11-11 09:39:59 +01:00
|
|
|
|
|
|
|
|
# Orders are assigned to one slab.
|
|
|
|
|
for o in all_orders:
|
2023-11-16 19:46:56 +01:00
|
|
|
model.add_exactly_one(assign[o])
|
2018-11-11 09:39:59 +01:00
|
|
|
|
|
|
|
|
# Redundant constraint (sum of loads == sum of widths).
|
2023-11-16 19:46:56 +01:00
|
|
|
model.add(sum(loads) == sum(widths))
|
2018-11-11 09:39:59 +01:00
|
|
|
|
|
|
|
|
# Symmetry breaking.
|
|
|
|
|
for s in range(num_slabs - 1):
|
2023-11-16 19:46:56 +01:00
|
|
|
model.add(loads[s] >= loads[s + 1])
|
2018-11-11 09:39:59 +01:00
|
|
|
|
|
|
|
|
# Collect equivalent orders.
|
|
|
|
|
if break_symmetries:
|
2023-07-01 06:06:53 +02:00
|
|
|
print("Breaking symmetries")
|
2018-11-11 09:39:59 +01:00
|
|
|
width_to_unique_color_order = {}
|
|
|
|
|
ordered_equivalent_orders = []
|
2020-11-30 11:59:56 +01:00
|
|
|
orders_per_color = [
|
|
|
|
|
[o for o in all_orders if colors[o] == c + 1] for c in all_colors
|
|
|
|
|
]
|
2018-11-11 09:39:59 +01:00
|
|
|
for c in all_colors:
|
|
|
|
|
colored_orders = orders_per_color[c]
|
|
|
|
|
if not colored_orders:
|
|
|
|
|
continue
|
|
|
|
|
if len(colored_orders) == 1:
|
|
|
|
|
o = colored_orders[0]
|
|
|
|
|
w = widths[o]
|
|
|
|
|
if w not in width_to_unique_color_order:
|
|
|
|
|
width_to_unique_color_order[w] = [o]
|
|
|
|
|
else:
|
|
|
|
|
width_to_unique_color_order[w].append(o)
|
|
|
|
|
else:
|
|
|
|
|
local_width_to_order = {}
|
|
|
|
|
for o in colored_orders:
|
|
|
|
|
w = widths[o]
|
|
|
|
|
if w not in local_width_to_order:
|
|
|
|
|
local_width_to_order[w] = []
|
|
|
|
|
local_width_to_order[w].append(o)
|
2023-07-01 06:06:53 +02:00
|
|
|
for _, os in local_width_to_order.items():
|
2018-11-11 09:39:59 +01:00
|
|
|
if len(os) > 1:
|
|
|
|
|
for p in range(len(os) - 1):
|
2020-11-30 11:59:56 +01:00
|
|
|
ordered_equivalent_orders.append((os[p], os[p + 1]))
|
2023-07-01 06:06:53 +02:00
|
|
|
for _, os in width_to_unique_color_order.items():
|
2018-11-11 09:39:59 +01:00
|
|
|
if len(os) > 1:
|
|
|
|
|
for p in range(len(os) - 1):
|
|
|
|
|
ordered_equivalent_orders.append((os[p], os[p + 1]))
|
|
|
|
|
|
|
|
|
|
# Create position variables if there are symmetries to be broken.
|
|
|
|
|
if ordered_equivalent_orders:
|
2023-07-01 06:06:53 +02:00
|
|
|
print(
|
|
|
|
|
" - creating %i symmetry breaking constraints"
|
|
|
|
|
% len(ordered_equivalent_orders)
|
|
|
|
|
)
|
2018-11-11 09:39:59 +01:00
|
|
|
positions = {}
|
|
|
|
|
for p in ordered_equivalent_orders:
|
|
|
|
|
if p[0] not in positions:
|
2023-11-16 19:46:56 +01:00
|
|
|
positions[p[0]] = model.new_int_var(
|
2023-07-01 06:06:53 +02:00
|
|
|
0, num_slabs - 1, "position_of_slab_%i" % p[0]
|
|
|
|
|
)
|
2023-11-16 19:46:56 +01:00
|
|
|
model.add_map_domain(positions[p[0]], assign[p[0]])
|
2018-11-11 09:39:59 +01:00
|
|
|
if p[1] not in positions:
|
2023-11-16 19:46:56 +01:00
|
|
|
positions[p[1]] = model.new_int_var(
|
2023-07-01 06:06:53 +02:00
|
|
|
0, num_slabs - 1, "position_of_slab_%i" % p[1]
|
|
|
|
|
)
|
2023-11-16 19:46:56 +01:00
|
|
|
model.add_map_domain(positions[p[1]], assign[p[1]])
|
2018-11-11 09:39:59 +01:00
|
|
|
# Finally add the symmetry breaking constraint.
|
2023-11-16 19:46:56 +01:00
|
|
|
model.add(positions[p[0]] <= positions[p[1]])
|
2018-11-11 09:39:59 +01:00
|
|
|
|
|
|
|
|
# Objective.
|
2023-11-16 19:46:56 +01:00
|
|
|
model.minimize(sum(losses))
|
2018-11-11 09:39:59 +01:00
|
|
|
|
2023-07-01 06:06:53 +02:00
|
|
|
print("Model created")
|
2018-11-11 09:39:59 +01:00
|
|
|
|
|
|
|
|
### Solve model.
|
|
|
|
|
solver = cp_model.CpSolver()
|
2023-07-01 06:06:53 +02:00
|
|
|
if _PARAMS.value:
|
|
|
|
|
text_format.Parse(_PARAMS.value, solver.parameters)
|
|
|
|
|
|
|
|
|
|
solution_printer = SteelMillSlabSolutionPrinter(orders, assign, loads, losses)
|
2023-11-16 19:46:56 +01:00
|
|
|
status = solver.solve(model, solution_printer)
|
2018-11-11 09:39:59 +01:00
|
|
|
|
|
|
|
|
### Output the solution.
|
|
|
|
|
if status == cp_model.OPTIMAL:
|
2020-11-30 11:59:56 +01:00
|
|
|
print(
|
2023-07-01 06:06:53 +02:00
|
|
|
"Loss = %i, time = %.2f s, %i conflicts"
|
2023-11-16 19:46:56 +01:00
|
|
|
% (solver.objective_value, solver.wall_time, solver.num_conflicts)
|
2023-07-01 06:06:53 +02:00
|
|
|
)
|
2018-11-11 09:39:59 +01:00
|
|
|
else:
|
2023-07-01 06:06:53 +02:00
|
|
|
print("No solution")
|
2018-11-11 09:39:59 +01:00
|
|
|
|
2017-10-25 14:20:56 +02:00
|
|
|
|
2020-11-30 11:59:56 +01:00
|
|
|
def steel_mill_slab_with_column_generation(problem):
|
2018-11-11 09:39:59 +01:00
|
|
|
"""Solves the Steel Mill Slab Problem."""
|
|
|
|
|
### Load problem.
|
2018-12-30 22:54:56 +01:00
|
|
|
(num_slabs, capacities, _, orders) = build_problem(problem)
|
2018-11-11 09:39:59 +01:00
|
|
|
|
|
|
|
|
num_orders = len(orders)
|
|
|
|
|
num_capacities = len(capacities)
|
|
|
|
|
all_orders = range(len(orders))
|
2023-07-01 06:06:53 +02:00
|
|
|
print(
|
|
|
|
|
"Solving steel mill with %i orders, %i slabs, and %i capacities"
|
|
|
|
|
% (num_orders, num_slabs, num_capacities - 1)
|
|
|
|
|
)
|
2018-11-11 09:39:59 +01:00
|
|
|
|
2020-11-30 11:59:56 +01:00
|
|
|
# Compute auxiliary data.
|
2018-11-11 09:39:59 +01:00
|
|
|
widths = [x[0] for x in orders]
|
|
|
|
|
colors = [x[1] for x in orders]
|
|
|
|
|
max_capacity = max(capacities)
|
|
|
|
|
loss_array = [
|
2023-07-01 06:06:53 +02:00
|
|
|
min(x for x in capacities if x >= c) - c for c in range(max_capacity + 1)
|
2018-11-11 09:39:59 +01:00
|
|
|
]
|
|
|
|
|
|
|
|
|
|
### Model problem.
|
|
|
|
|
|
|
|
|
|
# Generate all valid slabs (columns)
|
2023-07-01 06:06:53 +02:00
|
|
|
unsorted_valid_slabs = collect_valid_slabs_dp(
|
|
|
|
|
capacities, colors, widths, loss_array
|
|
|
|
|
)
|
2018-12-30 21:17:38 +01:00
|
|
|
|
2018-11-11 09:39:59 +01:00
|
|
|
# Sort slab by descending load/loss. Remove duplicates.
|
2023-07-01 06:06:53 +02:00
|
|
|
valid_slabs = sorted(unsorted_valid_slabs, key=lambda c: 1000 * c[-1] + c[-2])
|
2018-12-30 22:54:56 +01:00
|
|
|
all_valid_slabs = range(len(valid_slabs))
|
2018-11-11 09:39:59 +01:00
|
|
|
|
|
|
|
|
# create model and decision variables.
|
|
|
|
|
model = cp_model.CpModel()
|
2023-11-16 19:46:56 +01:00
|
|
|
selected = [model.new_bool_var("selected_%i" % i) for i in all_valid_slabs]
|
2018-11-11 09:39:59 +01:00
|
|
|
|
2018-12-30 22:54:56 +01:00
|
|
|
for order_id in all_orders:
|
2023-11-16 19:46:56 +01:00
|
|
|
model.add(
|
2023-07-01 06:06:53 +02:00
|
|
|
sum(selected[i] for i, slab in enumerate(valid_slabs) if slab[order_id])
|
|
|
|
|
== 1
|
|
|
|
|
)
|
2018-11-11 09:39:59 +01:00
|
|
|
|
|
|
|
|
# Redundant constraint (sum of loads == sum of widths).
|
2023-11-16 19:46:56 +01:00
|
|
|
model.add(
|
2023-07-01 06:06:53 +02:00
|
|
|
sum(selected[i] * valid_slabs[i][-1] for i in all_valid_slabs) == sum(widths)
|
|
|
|
|
)
|
2018-11-11 09:39:59 +01:00
|
|
|
|
|
|
|
|
# Objective.
|
2023-11-16 19:46:56 +01:00
|
|
|
model.minimize(sum(selected[i] * valid_slabs[i][-2] for i in all_valid_slabs))
|
2018-11-11 09:39:59 +01:00
|
|
|
|
2023-07-01 06:06:53 +02:00
|
|
|
print("Model created")
|
2018-11-11 09:39:59 +01:00
|
|
|
|
|
|
|
|
### Solve model.
|
|
|
|
|
solver = cp_model.CpSolver()
|
2023-07-01 06:06:53 +02:00
|
|
|
if _PARAMS.value:
|
|
|
|
|
text_format.Parse(_PARAMS.value, solver.parameters)
|
2018-12-30 18:12:26 +01:00
|
|
|
solution_printer = cp_model.ObjectiveSolutionPrinter()
|
2023-11-16 19:46:56 +01:00
|
|
|
status = solver.solve(model, solution_printer)
|
2018-11-11 09:39:59 +01:00
|
|
|
|
|
|
|
|
### Output the solution.
|
2018-12-30 18:12:26 +01:00
|
|
|
if status in (cp_model.OPTIMAL, cp_model.FEASIBLE):
|
2020-11-30 11:59:56 +01:00
|
|
|
print(
|
2023-07-01 06:06:53 +02:00
|
|
|
"Loss = %i, time = %.2f s, %i conflicts"
|
2023-11-16 19:46:56 +01:00
|
|
|
% (solver.objective_value, solver.wall_time, solver.num_conflicts)
|
2023-07-01 06:06:53 +02:00
|
|
|
)
|
2018-11-11 09:39:59 +01:00
|
|
|
else:
|
2023-07-01 06:06:53 +02:00
|
|
|
print("No solution")
|
2018-11-11 09:39:59 +01:00
|
|
|
|
2017-10-27 08:52:42 +02:00
|
|
|
|
2022-10-07 18:21:23 +02:00
|
|
|
def main(_):
|
2023-07-01 06:06:53 +02:00
|
|
|
if _SOLVER.value == "sat":
|
2022-10-07 18:21:23 +02:00
|
|
|
steel_mill_slab(_PROBLEM.value, _BREAK_SYMMETRIES.value)
|
2023-07-01 06:06:53 +02:00
|
|
|
elif _SOLVER.value == "sat_table":
|
|
|
|
|
steel_mill_slab_with_valid_slabs(_PROBLEM.value, _BREAK_SYMMETRIES.value)
|
|
|
|
|
elif _SOLVER.value == "sat_column":
|
2022-10-07 18:21:23 +02:00
|
|
|
steel_mill_slab_with_column_generation(_PROBLEM.value)
|
2023-03-03 18:55:16 +04:00
|
|
|
else:
|
2023-07-01 06:06:53 +02:00
|
|
|
print(f"Unknown model {_SOLVER.value}")
|
2018-11-11 09:39:59 +01:00
|
|
|
|
2017-10-25 14:20:56 +02:00
|
|
|
|
2023-07-01 06:06:53 +02:00
|
|
|
if __name__ == "__main__":
|
2020-11-30 11:59:56 +01:00
|
|
|
app.run(main)
|