-
Notifications
You must be signed in to change notification settings - Fork 1
/
boxes_in_trucks.py
216 lines (181 loc) · 9.85 KB
/
boxes_in_trucks.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
# -*- coding: utf-8 -*-
"""
Example of integrating integer optimization with constraint programming.
Packing boxes into trucks: IP is to minimize costs of assignment of boxes to trucks,
and CP is to ensure boxes fit (in 3D space) into trucks.
Assumes boxes can be oriented in one of two dimensions (up/down, left/right), but not turned on side.
Uses lazy constraint callbacks.
Requirements:
- CPLEX (or other Pyomo-compatible solver)
"""
# Import
from pyomo.environ import *
from pyomo.opt import *
from pyomo.core import *
import cplex
from cplex.callbacks import LazyConstraintCallback
from packing_subproblem import * # For running packing CSP using constraint programming
# Global variables, to be able to access this data in the lazy constraint callback
numBoxes = 0 # Total number of boxes
numContainers = 0 # Number of containers (trucks)
boxSize = {} # Dict of box sizes, by (box, dim, orientation)
containerSize = {} # Dict of container (truck) sizes, in each of three dimensions [d1, d2, d3]
numLazyConstraints = 0 # Number of lazy constraints added
def generateRandomData(numBoxes, numContainers):
# Generate random data and return data structures
from random import randrange, seed
seed(1) # Set random seed
# Fixed values for random ranges
costLB = 1
costUB = 10
boxSizeLB = 1
boxSizeUB = 5
contSizeLB = 3
contSizeUB = 10
print("Generating random input data...")
boxSize = {} # Dict of box sizes, by (box, dim, orientation)
costs = {} # Dist of costs of assigning a box to a container (b,c):cost
containerSize = {} # Dict of container (truck) sizes, in each of three dimensions truck:[d1, d2, d3]
for b in range(1, numBoxes+1):
# Generate box size
theLength = randrange(boxSizeLB, boxSizeUB)
theWidth = randrange(boxSizeLB, boxSizeUB)
theHeight = randrange(boxSizeLB, boxSizeUB)
boxSize[b,1,1] = theLength
boxSize[b,2,2] = theLength
boxSize[b,2,1] = theWidth
boxSize[b,1,2] = theWidth
boxSize[b,3,1] = theHeight
boxSize[b,3,2] = theHeight
# Generate box costs
for c in range(1, numContainers+1):
costs[(b,c)] = randrange(costLB, costUB)
# Generate box costs
for c in range(1, numContainers+1):
containerSize[c] = [randrange(contSizeLB, contSizeUB), randrange(contSizeLB, contSizeUB),randrange(contSizeLB, contSizeUB)]
return costs, boxSize, containerSize
def objective_rule(model):
# Create objective function
return sum(model.cost[b,t] * model.ASSIGN[b,t] for b in model.b for t in model.t)
def assignment_rule(model,b):
# Each box must be assigned to exactly one truck
return sum(model.ASSIGN[b,t] for t in model.t) == 1
class cplexLazyConstraintCallback(LazyConstraintCallback):
# CPLEX lazy constraint callback (this is only enabled when solving using .lp file interface).
# For each worksite, need to read in current solution, create and solve CSP, and add cuts if current assignment is not feasible.
def __call__(self):
print("This is the lazy constraint callback code")
# Reference global values (otherwise, can't access these data inside this callback code)
global numBoxes
global numContainers
global boxSize
global containerSize
global numLazyConstraints
# Read in values of current assignments
for t in range(1,numContainers+1):
theBoxes = []
variableList = [] # # Create list of string names of the active variables, if needed in lazy constraint
theContainerSize = containerSize[t] # Get list of dimensions for this container
for b in range(1, numBoxes+1):
if self.get_values('ASSIGN(' + str(b) + '_' + str(t) + ')') > 0: # If this box assigned to this container
theBoxes.append(b)
variableList.append('ASSIGN(' + str(b) + '_' + str(t) + ')')
# Run constraint programming problem to determine if these boxes fit in this container
isFeasible = solvePackingSubproblem(theBoxes, boxSize, theContainerSize)
if not isFeasible: # Scheduling problem was infeasible; add cut
print("Infeasible assignment in container " + str(t) + "; adding cut...")
coefficientList = [1] * len(variableList) # Create list of the coefficients
self.add([variableList,coefficientList], "L", len(variableList)-1) # Add a cut that says at least one of these boxes can't be assigned to this box
numLazyConstraints += 1
print("The variableList is " + str(variableList))
else:
print("Feasible assignment of boxes to container " + str(t))
def SolveUsingPyomoCPLEX_LP(theNumBoxes, theNumContainers, costs, theBoxSize, theContainerSize):
''' Create and solve a concrete Pyomo model and CPLEX interface (to allow lazy constraint callbacks)
theNumBoxes: Number of boxes (int)
theNumContainers: Number of containers (trucks) (int)
costs: Dict of costs of assigning a box to a container (b,t):cost
theBoxSize: Dict of box sizes, by (box, dim, orientation)
theContainerSize: Dict of container (truck) sizes, in each of three dimensions [d1, d2, d3]
'''
# Initialize data structures (globally, for use in callback)
global numBoxes
numBoxes = theNumBoxes
global numContainers
numContainers = theNumContainers
global boxSize
boxSize = theBoxSize
global containerSize
containerSize = theContainerSize
global numLazyConstraints
# Create a concrete Pyomo model
print("Building Pyomo model...")
model = ConcreteModel()
# Define indices and sets
print("Creating indices and sets...")
model.b = Set(initialize=[b for b in range(1, numBoxes+1)], ordered=True) # Index on each box
model.t = Set(initialize=[t for t in range(1, numContainers+1)], ordered=True) # Index on each container/truck
# Define variables
print("Creating variables...")
model.ASSIGN = Var(model.b * model.t, domain=Binary, initialize = 0) # Indicates the assignment of a box to a truck
# Create parameters (i.e., data)
print("Creating parameters...")
model.cost = Param(model.b * model.t, initialize = costs)
# Also box and truck size, by dim / orientation
# Create objective function
print("Creating objective function...")
model.objective = Objective(rule=objective_rule, sense = minimize)
print("Creating assignment constraints ...")
model.assignmentConstraint = Constraint(model.b, rule=assignment_rule)
print("Adding (optional) constraints disallowing individual boxes in containers...")
# Check if a box can't possibly fit in a container.
# This will reduce number of lazy constraints, but might not reduce overall runtime.
model.noFitConstraints = ConstraintList() # For adding an array of constraints
numNoFitConstraints = 0
for t in model.t:
for b in model.b:
# If it definitely doesn't fit
if (boxSize[b,1,1] > containerSize[t][0] and boxSize[b,1,2] > containerSize[t][0]) or \
(boxSize[b,2,1] > containerSize[t][1] and boxSize[b,2,2] > containerSize[t][1]) or \
(boxSize[b,3,1] > containerSize[t][2]):
numNoFitConstraints += 1
model.noFitConstraints.add(model.ASSIGN[b,t] == 0)
if numNoFitConstraints > 0: print("Added " + str(numNoFitConstraints) + " constraints to prevent selection of items that don't fit.")
print("Done.")
print("Pyomo model created. Saving as LP file and setting up CPLEX interface...")
model.write('pyomoModel.lp', io_options={'symbolic_solver_labels':True}) # Write Pyomo model as an LP file
modelCPLEX = cplex.Cplex('pyomoModel.lp')
theCPLEXLazyCallback = modelCPLEX.register_callback(cplexLazyConstraintCallback) # Register the lazy constraint callback
# Note using control callbacks (like lazy constraints) means you can only use opportunistic parallel processing, and disables some reductions.
print("Running solver...")
modelCPLEX.solve()
print("Done. Loading solution from CPLEX back into Pyomo object and saving results...")
results = modelCPLEX.solution
# Print results (this is hard-coded to be specific to this problem)
print("The total number of lazy constraints is: " + str(numLazyConstraints))
print("The objective value is: " + str(results.get_objective_value()))
for b in model.b:
for t in model.t:
if(results.get_values("ASSIGN(" + str(b) + "_" + str(t) + ")")) > 0:
print("Box " + str(b) + " is assigned to container " + str(t) + ", at a cost of " + str(model.cost[b,t]))
# Small dataset, for testing
numBoxes = 5 # Number of boxes
numContainers = 3 # Number of containers (trucks)
costs = {(1,1):1, (1,2):2, (1,3):3, # Costs of assigning a box to a container
(2,1):1, (2,2):2, (2,3):3,
(3,1):1, (3,2):2, (3,3):3,
(4,1):1, (4,2):2, (4,3):3,
(5,1):1, (5,2):2, (5,3):3 }
# Dict of box sizes, by (box, dim, orientation)
boxSize = { (1,1,1):1, (1,1,2):1, (1,2,1):1, (1,2,2):1, (1,3,1):1, (1,3,2):1,
(2,1,1):2, (2,1,2):2, (2,2,1):2, (2,2,2):2, (2,3,1):2, (2,3,2):2,
(3,1,1):1, (3,1,2):4, (3,2,1):4, (3,2,2):1, (3,3,1):1, (3,3,2):1,
(4,1,1):2, (4,1,2):5, (4,2,1):5, (4,2,2):2, (4,3,1):2, (4,3,2):2,
(5,1,1):1, (5,1,2):3, (5,2,1):3, (5,2,2):1, (5,3,1):3, (5,3,2):3 }
# Dict of container (truck) sizes, in each of three dimensions [d1, d2, d3]
containerSize = {1:[3,6,5], 2:[4,4,4], 3:[5,8,5]}
# Use this to generate random data. Otherwise, just comment this to use above test data.
numBoxes = 20
numContainers = 8 # 8 works, 7 works but takes about 60 sec
costs, boxSize, containerSize = generateRandomData(numBoxes, numContainers)
SolveUsingPyomoCPLEX_LP(numBoxes, numContainers, costs, boxSize, containerSize) # Run above code