#!/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/10/2d-knapsack-problem.html """ import io from absl import app from absl import flags import numpy as np import pandas as pd from google.protobuf import text_format 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 scale_double(value): return int(round(value * 1000.0)) 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(io.StringIO(data), sep=r'\s+') print('Input data') print(data) max_height = 20 max_width = 30 print(f'Container max_width:{max_width} max_height:{max_height}') print(f'#Items: {len(data.index)}') return (data, max_height, max_width) # pytype: disable=wrong-arg-types def solve_with_duplicate_items(data, max_height, max_width): """Solve the problem by building 2 items (rotated or not) for each item.""" # Derived data (expanded to individual items). data_widths = 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_widths = np.repeat(data_widths, 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_widths = np.concatenate((base_item_widths, base_item_heights)) item_heights = np.concatenate((base_item_heights, base_item_widths)) item_values = np.concatenate((base_item_values, base_item_values)) num_items = len(item_values) # OR-Tools model model = cp_model.CpModel() # Variables x_starts = [] x_ends = [] y_starts = [] y_ends = [] is_used = [] x_intervals = [] y_intervals = [] for i in range(num_items): ## Is the item used? is_used.append(model.NewBoolVar(f'is_used{i}')) ## Item coordinates. x_starts.append(model.NewIntVar(0, max_width, f'x_start{i}')) x_ends.append(model.NewIntVar(0, max_width, f'x_end{i}')) y_starts.append(model.NewIntVar(0, max_height, f'y_start{i}')) y_ends.append(model.NewIntVar(0, max_height, f'y_end{i}')) ## Interval variables. x_intervals.append( model.NewIntervalVar(x_starts[i], item_widths[i] * is_used[i], x_ends[i], f'x_interval{i}')) y_intervals.append( model.NewIntervalVar(y_starts[i], item_heights[i] * is_used[i], y_ends[i], f'y_interval{i}')) # Constraints. ## 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) ## 2D no overlap. model.AddNoOverlap2D(x_intervals, y_intervals) ## Objective. model.Maximize( sum(is_used[i] * scale_double(item_values[i]) for i in range(num_items))) model.SetObjectiveScaling(1e-3) # 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_starts[i]) for i in used], 'y_start': [solver.Value(y_starts[i]) for i in used], 'item_width': [item_widths[i] for i in used], 'item_height': [item_heights[i] for i in used], 'x_end': [solver.Value(x_ends[i]) for i in used], 'y_end': [solver.Value(y_ends[i]) for i in used], 'item_value': [item_values[i] for i in used] }) print(data) def solve_with_rotations(data, max_height, max_width): """Solve the problem by rotating items.""" # Derived data (expanded to individual items). data_widths = data['width'].to_numpy() data_heights = data['height'].to_numpy() data_availability = data['available'].to_numpy() data_values = data['value'].to_numpy() item_widths = np.repeat(data_widths, data_availability) item_heights = np.repeat(data_heights, data_availability) item_values = np.repeat(data_values, data_availability) num_items = len(item_widths) # OR-Tools model. model = cp_model.CpModel() # Variables. x_starts = [] x_sizes = [] x_ends = [] y_starts = [] y_sizes = [] y_ends = [] x_intervals = [] y_intervals = [] for i in range(num_items): # X coordinates. x_starts.append(model.NewIntVar(0, max_width, f'x_start{i}')) x_sizes.append( model.NewIntVarFromDomain( cp_model.Domain.FromValues( [0, int(item_widths[i]), int(item_heights[i])]), f'x_size{i}')) x_ends.append(model.NewIntVar(0, max_width, f'x_end{i}')) # Y coordinates. y_starts.append(model.NewIntVar(0, max_height, f'y_start{i}')) y_sizes.append( model.NewIntVarFromDomain( cp_model.Domain.FromValues( [0, int(item_widths[i]), int(item_heights[i])]), f'y_size{i}')) y_ends.append(model.NewIntVar(0, max_height, f'y_end{i}')) ## Interval variables x_intervals.append( model.NewIntervalVar(x_starts[i], x_sizes[i], x_ends[i], f'x_interval{i}')) y_intervals.append( model.NewIntervalVar(y_starts[i], y_sizes[i], y_ends[i], f'y_interval{i}')) 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 max_height and width. dim1 = int(item_widths[i]) dim2 = int(item_heights[i]) model.Add(x_sizes[i] == 0).OnlyEnforceIf(not_selected) model.Add(y_sizes[i] == 0).OnlyEnforceIf(not_selected) model.Add(x_sizes[i] == dim1).OnlyEnforceIf(no_rotation) model.Add(y_sizes[i] == dim2).OnlyEnforceIf(no_rotation) model.Add(x_sizes[i] == dim2).OnlyEnforceIf(rotation) model.Add(y_sizes[i] == dim1).OnlyEnforceIf(rotation) is_used.append(not_selected.Not()) ## 2D no overlap. model.AddNoOverlap2D(x_intervals, y_intervals) # Objective. model.Maximize( sum(is_used[i] * scale_double(item_values[i]) for i in range(num_items))) model.SetObjectiveScaling(1e-3) # 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_starts[i]) for i in used], 'y_start': [solver.Value(y_starts[i]) for i in used], 'item_width': [solver.Value(x_sizes[i]) for i in used], 'item_height': [solver.Value(y_sizes[i]) for i in used], 'x_end': [solver.Value(x_ends[i]) for i in used], 'y_end': [solver.Value(y_ends[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, max_height, max_width = build_data() if FLAGS.model == 'duplicate': solve_with_duplicate_items(data, max_height, max_width) else: solve_with_rotations(data, max_height, max_width) if __name__ == '__main__': app.run(main)