new example

This commit is contained in:
Laurent Perron
2021-11-14 17:10:05 +01:00
parent f76c909bc4
commit 65dc9c4b6b

View File

@@ -0,0 +1,272 @@
#!/usr/bin/env python3
# Copyright 2010-2021 Google LLC
# 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.
"""Solver a 2D rectangle knapsack problem.
This code is adapted from
https://yetanothermathprogrammingconsultant.blogspot.com/2021/02/2d-bin-packing.html
"""
from io import StringIO
from absl import app
from absl import flags
from google.protobuf import text_format
import numpy as np
import pandas as pd
from ortools.sat.python import cp_model
FLAGS = flags.FLAGS
flags.DEFINE_string('output_proto', '',
'Output file to write the cp_model proto to.')
flags.DEFINE_string('params', 'num_search_workers:16,log_search_progress:true',
'Sat solver parameters.')
flags.DEFINE_string('model', 'rotation', '\'duplicate\' or \'rotation\'')
def build_data():
"""Build the data frame."""
data = """
item width height available value color
k1 20 4 2 338.984 blue
k2 12 17 6 849.246 orange
k3 20 12 2 524.022 green
k4 16 7 9 263.303 red
k5 3 6 3 113.436 purple
k6 13 5 3 551.072 brown
k7 4 7 6 86.166 pink
k8 6 18 8 755.094 grey
k9 14 2 7 223.516 olive
k10 9 11 5 369.560 cyan
"""
data = pd.read_table(StringIO(data), sep='\s+')
print('Input data')
print(data)
height = 20
width = 30
print(f'Container width:{width} height:{height}')
print(f'#Items: {len(data.index)}')
return (data, height, width)
def solve_with_duplicate_items(data, height, width):
"""Solve the problem by building 2 items (rotated or not) for each item."""
# Derived data (expanded to individual items).
data_weights = data['width'].to_numpy()
data_heights = data['height'].to_numpy()
data_availability = data['available'].to_numpy()
data_values = data['value'].to_numpy()
# Non duplicated items data.
base_item_weights = np.repeat(data_weights, data_availability)
base_item_heights = np.repeat(data_heights, data_availability)
base_item_values = np.repeat(data_values, data_availability)
num_data_items = len(base_item_values)
# Create rotated items by duplicating.
item_weights = np.concatenate((base_item_weights, base_item_heights))
item_heights = np.concatenate((base_item_heights, base_item_weights))
item_values = np.concatenate((base_item_values, base_item_values))
# Scale values to become integers.
item_values = (item_values * 1000.0).astype('int')
num_items = len(item_values)
# OR-Tools model
model = cp_model.CpModel()
# Variables
## u[i] : item i is used
is_used = [model.NewBoolVar(f'u{i}') for i in range(num_items)]
## x_start[i],y_start[i] : location of item i
x_start = [model.NewIntVar(0, width, f'x_start{i}') for i in range(num_items)]
y_start = [model.NewIntVar(0, height, f'y_start{i}') for i in range(num_items)]
## x_end[i],y_end[i] : upper limit of interval variable
x_end = [model.NewIntVar(0, width, f'x_end{i}') for i in range(num_items)]
y_end = [model.NewIntVar(0, height, f'y_end{i}') for i in range(num_items)]
## interval variables
x_intervals = [
model.NewIntervalVar(x_start[i], item_weights[i] * is_used[i], x_end[i],
f'xival{i}') for i in range(num_items)
]
y_intervals = [
model.NewIntervalVar(y_start[i], item_heights[i] * is_used[i], y_end[i],
f'yival{i}') for i in range(num_items)
]
# only one of non-rotated/rotated pair can be used
for i in range(num_data_items):
model.Add(is_used[i] + is_used[i + num_data_items] <= 1)
# Constraints.
## 2D no overlap.
model.AddNoOverlap2D(x_intervals, y_intervals)
## Objective.
model.Maximize(sum([is_used[i] * item_values[i] for i in range(num_items)]))
# Output proto to file.
if FLAGS.output_proto:
print('Writing proto to %s' % FLAGS.output_proto)
with open(FLAGS.output_proto, 'w') as text_file:
text_file.write(str(model))
# Solve model.
solver = cp_model.CpSolver()
if FLAGS.params:
text_format.Parse(FLAGS.params, solver.parameters)
status = solver.Solve(model)
# Report solution.
if status == cp_model.OPTIMAL:
used = {i for i in range(num_items) if solver.BooleanValue(is_used[i])}
data = pd.DataFrame({
'x_start': [solver.Value(x_start[i]) for i in used],
'y_start': [solver.Value(y_start[i]) for i in used],
'item_weight': [item_weights[i] for i in used],
'item_height': [item_heights[i] for i in used],
'x_end': [solver.Value(x_end[i]) for i in used],
'y_end': [solver.Value(y_end[i]) for i in used],
'item_value': [item_values[i] for i in used]
})
print(data)
def solve_with_rotations(data, height, width):
"""Solve the problem by rotating items."""
# Derived data (expanded to individual items).
data_weights = data['width'].to_numpy()
data_heights = data['height'].to_numpy()
data_availability = data['available'].to_numpy()
data_values = data['value'].to_numpy()
item_weights = np.repeat(data_weights, data_availability).astype('int')
item_heights = np.repeat(data_heights, data_availability).astype('int')
item_values = np.repeat(data_values, data_availability)
num_items = len(item_weights)
# Scale values to become integers.
item_values = (item_values * 1000.0).astype('int')
# OR-Tools model.
model = cp_model.CpModel()
# Variables.
## x_start[i],y_start[i] : location of item i.
x_start = [model.NewIntVar(0, width, f'x_start{i}') for i in range(num_items)]
y_start = [model.NewIntVar(0, height, f'y_start{i}') for i in range(num_items)]
## x_size[i],y_size[i] : sizes of item i.
x_size = [
model.NewIntVarFromDomain(
cp_model.Domain.FromValues([0, item_weights[i], item_heights[i]]),
f'x_size{i}') for i in range(num_items)
]
y_size = [
model.NewIntVarFromDomain(
cp_model.Domain.FromValues([0, item_weights[i], item_heights[i]]),
f'y_size{i}') for i in range(num_items)
]
## x_end[i],y_end[i] : upper limit of interval variable.
x_end = [model.NewIntVar(0, width, f'x_end{i}') for i in range(num_items)]
y_end = [model.NewIntVar(0, height, f'y_end{i}') for i in range(num_items)]
## Interval variables
x_intervals = [
model.NewIntervalVar(x_start[i], x_size[i], x_end[i], f'xival{i}')
for i in range(num_items)
]
y_intervals = [
model.NewIntervalVar(y_start[i], y_size[i], y_end[i], f'yival{i}')
for i in range(num_items)
]
is_used = []
# Constraints.
## for each item, decide is unselected, no_rotation, rotated.
for i in range(num_items):
not_selected = model.NewBoolVar(f'not_selected_{i}')
no_rotation = model.NewBoolVar(f'no_rotation{i}')
rotation = model.NewBoolVar(f'rotation{i}')
### Only one state can be chosen.
model.Add(not_selected + no_rotation + rotation == 1)
### Define height and width.
model.Add(x_size[i] == 0).OnlyEnforceIf(not_selected)
model.Add(y_size[i] == 0).OnlyEnforceIf(not_selected)
model.Add(x_size[i] == item_weights[i]).OnlyEnforceIf(no_rotation)
model.Add(y_size[i] == item_heights[i]).OnlyEnforceIf(no_rotation)
model.Add(x_size[i] == item_heights[i]).OnlyEnforceIf(rotation)
model.Add(y_size[i] == item_weights[i]).OnlyEnforceIf(rotation)
is_used.append(not_selected.Not())
## 2D no overlap.
model.AddNoOverlap2D(x_intervals, y_intervals)
# Objective.
model.Maximize(sum(is_used[i] * item_values[i] for i in range(num_items)))
# Output proto to file.
if FLAGS.output_proto:
print('Writing proto to %s' % FLAGS.output_proto)
with open(FLAGS.output_proto, 'w') as text_file:
text_file.write(str(model))
# Solve model.
solver = cp_model.CpSolver()
if FLAGS.params:
text_format.Parse(FLAGS.params, solver.parameters)
status = solver.Solve(model)
# Report solution.
if status == cp_model.OPTIMAL:
used = {i for i in range(num_items) if solver.BooleanValue(is_used[i])}
data = pd.DataFrame({
'x_start': [solver.Value(x_start[i]) for i in used],
'y_start': [solver.Value(y_start[i]) for i in used],
'item_weight': [solver.Value(x_size[i]) for i in used],
'item_height': [solver.Value(y_size[i]) for i in used],
'x_end': [solver.Value(x_end[i]) for i in used],
'y_end': [solver.Value(y_end[i]) for i in used],
'item_value': [item_values[i] for i in used]
})
print(data)
def main(_):
"""Solve the problem with all models."""
data, height, width = build_data()
if FLAGS.model == 'duplicate':
solve_with_duplicate_items(data, height, width)
else:
solve_with_rotations(data, height, width)
if __name__ == '__main__':
app.run(main)