forked from sabbadino/container-optimizations
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmodel_optimizations.py
More file actions
269 lines (229 loc) · 13.4 KB
/
model_optimizations.py
File metadata and controls
269 lines (229 loc) · 13.4 KB
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
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
def prefer_put_boxes_by_volume_lower_z(model, n, z, l_eff, w_eff, h_eff, container):
from ortools.sat.python.cp_model import IntVar
from typing import List
"""
Returns a list of terms, one for each box, representing (box volume) * (height from bottom),
where box volume is l_eff[i] * w_eff[i] * h_eff[i] in its current orientation, and
height from bottom is the distance from the bottom of the container to the lower face of the box.
This is used as a soft constraint to encourage boxes with larger volume to be placed lower
in the container, improving stability. For boxes with free rotation, the volume is determined
by the chosen orientation (l_eff[i], w_eff[i], h_eff[i]).
Args:
model: The CpModel instance.
n: Number of boxes.
z: List of IntVar, z[i] is the z-coordinate of the lower face of box i.
l_eff, w_eff, h_eff: Lists of IntVar, effective length, width, and height of box i (depends on orientation).
container: Tuple/list of container dimensions (length, width, height).
Returns:
weighted_terms: List of IntVar, each representing (l_eff[i] * w_eff[i] * h_eff[i]) * (container[2] - z[i])
"""
weighted_terms: List[IntVar] = []
max_volume: int = container[0] * container[1] * container[2]
for i in range(n):
box_volume: IntVar = model.NewIntVar(0, max_volume, f'box_volume_{i}')
model.AddMultiplicationEquality(box_volume, [l_eff[i], w_eff[i], h_eff[i]])
height_from_bottom: IntVar = model.NewIntVar(0, container[2], f'height_from_bottom_vol_{i}')
model.Add(height_from_bottom == container[2] - z[i])
weighted: IntVar = model.NewIntVar(0, max_volume * container[2], f'weighted_vol_{i}')
model.AddMultiplicationEquality(weighted, [box_volume, height_from_bottom])
weighted_terms.append(weighted)
return weighted_terms
def prefer_put_boxes_lower_z_non_linear(model, n, z, l_eff, w_eff, container):
from ortools.sat.python.cp_model import CpModel, IntVar
from typing import List, Tuple, Any
"""
Returns a list of terms, one for each box, representing (base area) * (height from bottom)^2,
where base area is the area of the lower face of the box in its current orientation, and
height from bottom is the distance from the bottom of the container to the lower face of the box.
This is a non-linear (quadratic) version: it strongly rewards placing large-base boxes lower in the container.
For boxes with free rotation, the base area is determined by the chosen orientation (l_eff[i], w_eff[i]).
Args:
model: The CpModel instance.
n: Number of boxes.
z: List of IntVar, z[i] is the z-coordinate of the lower face of box i.
l_eff, w_eff: Lists of IntVar, effective length and width of box i (depends on orientation).
container: Tuple/list of container dimensions (length, width, height).
Returns:
weighted_terms: List of IntVar, each representing (l_eff[i] * w_eff[i]) * (container[2] - z[i]) ** 2
"""
weighted_terms: List[IntVar] = []
max_base_area: int = container[0] * container[1]
max_height: int = container[2]
for i in range(n):
base_area: IntVar = model.NewIntVar(0, max_base_area, f'base_area_{i}_ex')
model.AddMultiplicationEquality(base_area, [l_eff[i], w_eff[i]])
height_from_bottom: IntVar = model.NewIntVar(0, max_height, f'height_from_bottom_{i}_ex')
model.Add(height_from_bottom == max_height - z[i])
height_from_bottom_sq: IntVar = model.NewIntVar(0, max_height * max_height, f'height_from_bottom_sq_{i}_ex')
model.AddMultiplicationEquality(height_from_bottom_sq, [height_from_bottom, height_from_bottom])
weighted: IntVar = model.NewIntVar(0, max_base_area * max_height * max_height, f'weighted_{i}_ex')
model.AddMultiplicationEquality(weighted, [base_area, height_from_bottom_sq])
weighted_terms.append(weighted)
return weighted_terms
def prefer_put_boxes_lower_z(model, n, z, l_eff, w_eff, container):
from ortools.sat.python.cp_model import CpModel, IntVar
from typing import List, Tuple, Any
"""
Returns a list of terms, one for each box, representing (base area) * (height from bottom),
where base area is the area of the lower face of the box in its current orientation, and
height from bottom is the distance from the bottom of the container to the lower face of the box.
This is used as a soft constraint to encourage boxes with larger lower surfaces to be placed lower
in the container, improving stability. For boxes with free rotation, the base area is determined
by the chosen orientation (l_eff[i], w_eff[i]).
Args:
model: The CpModel instance.
n: Number of boxes.
z: List of IntVar, z[i] is the z-coordinate of the lower face of box i.
l_eff, w_eff: Lists of IntVar, effective length and width of box i (depends on orientation).
container: Tuple/list of container dimensions (length, width, height).
Returns:
weighted_terms: List of IntVar, each representing (l_eff[i] * w_eff[i]) * (container[2] - z[i])
"""
weighted_terms: List[IntVar] = []
for i in range(n):
base_area: IntVar = model.NewIntVar(0, container[0] * container[1], f'base_area_{i}')
model.AddMultiplicationEquality(base_area, [l_eff[i], w_eff[i]])
height_from_bottom: IntVar = model.NewIntVar(0, container[2], f'height_from_bottom_{i}')
model.Add(height_from_bottom == container[2] - z[i])
weighted: IntVar = model.NewIntVar(0, container[0] * container[1] * container[2], f'weighted_{i}')
model.AddMultiplicationEquality(weighted, [base_area, height_from_bottom])
weighted_terms.append(weighted)
return weighted_terms
def prefer_maximize_surface_contact(model, n, x, y, z, l_eff, w_eff, h_eff,container):
from ortools.sat.python.cp_model import CpModel, IntVar, BoolVarT
from typing import List
"""
Returns a list of contact area variables for each box (except those on the floor):
the area of the lower x-y surface in contact with the box below (if any).
The area is l_eff[i] * w_eff[i] if the box is exactly on top of another and overlaps in x/y, else 0.
This function is used as a soft constraint to encourage boxes to maximize their contact area with boxes below,
improving stability. For each box i, it sums the contact area with all possible supporting boxes j (j != i),
where box i is directly on top of box j (z[i] == z[j] + h_eff[j]) and their projections in x and y overlap.
The contact area is only counted if these conditions are met.
Args:
model: The CpModel instance.
n: Number of boxes.
x, y, z: Lists of IntVar, coordinates of the lower corner of each box.
l_eff, w_eff, h_eff: Lists of IntVar, effective dimensions of each box (depends on orientation).
container: Tuple/list of container dimensions (length, width, height).
Returns:
contact_area_vars: List of IntVar, each representing the total contact area of box i with boxes below.
"""
contact_area_vars: List[IntVar] = []
max_area: int = container[0] * container[1]
for i in range(n):
contact_with_any: List[IntVar] = []
for j in range(n):
if i == j:
continue
is_on_j: BoolVarT = model.NewBoolVar(f'is_on_{i}_on_{j}')
model.Add(z[i] == z[j] + h_eff[j]).OnlyEnforceIf(is_on_j)
model.Add(x[i] < x[j] + l_eff[j]).OnlyEnforceIf(is_on_j)
model.Add(x[i] + l_eff[i] > x[j]).OnlyEnforceIf(is_on_j)
model.Add(y[i] < y[j] + w_eff[j]).OnlyEnforceIf(is_on_j)
model.Add(y[i] + w_eff[i] > y[j]).OnlyEnforceIf(is_on_j)
area_ij: IntVar = model.NewIntVar(0, max_area, f'contact_area_{i}_on_{j}')
tmp_area: IntVar = model.NewIntVar(0, max_area, f'tmp_contact_area_{i}_on_{j}')
model.AddMultiplicationEquality(tmp_area, [l_eff[i], w_eff[i]])
model.AddMultiplicationEquality(area_ij, [is_on_j, tmp_area])
contact_with_any.append(area_ij)
if contact_with_any:
contact_area_i: IntVar = model.NewIntVar(0, max_area, f'contact_area_{i}')
model.Add(contact_area_i == sum(contact_with_any))
contact_area_vars.append(contact_area_i)
else:
contact_area_vars.append(model.NewIntVar(0, 0, f'contact_area_{i}_none'))
return contact_area_vars
def add_symmetry_breaking_for_identical_boxes(model, boxes, x, y, z, symmetry_mode, container):
from ortools.sat.python.cp_model import CpModel, IntVar, BoolVarT
from typing import List, Dict, Any
"""
Adds symmetry breaking constraints for identical boxes (same size and allowed rotations).
This function helps reduce the search space by enforcing an ordering among identical boxes, so that
equivalent solutions are not explored multiple times. Two modes are supported:
- 'simple': Orders boxes along the axis with the largest container dimension.
- 'full': Enforces full lexicographical ordering on (x, y, z) coordinates.
Args:
model: The CpModel instance.
boxes: List of box dicts, each with 'size' and 'rotation' keys.
x, y, z: Lists of IntVar, coordinates of the lower corner of each box.
symmetry_mode: 'simple' or 'full'. If None or other, no constraints are added.
container: Tuple/list of container dimensions (length, width, height).
"""
# Only apply symmetry breaking if a valid mode is specified
if symmetry_mode not in ['simple', 'full']:
return
# Group identical boxes and apply constraints
from collections import defaultdict
identical_boxes_map = defaultdict(list)
for i, box in enumerate(boxes):
# Create a stable key for grouping: size tuple + rotation type
key = (tuple(box['size']), box.get('rotation', 'free'))
identical_boxes_map[key].append(i)
axis_vars: List[List[IntVar]] = [x, y, z]
max_axis: int = max(enumerate(container), key=lambda t: t[1])[0] # 0=x, 1=y, 2=z
for group in identical_boxes_map.values():
if len(group) < 2:
continue
# Apply ordering constraints for each pair within the group
for i_idx in range(len(group)):
for j_idx in range(i_idx + 1, len(group)):
i = group[i_idx]
j = group[j_idx]
if symmetry_mode == 'simple':
# Simple ordering on one axis
model.Add(axis_vars[max_axis][i] <= axis_vars[max_axis][j])
elif symmetry_mode == 'full':
# Full lexicographical ordering using the efficient built-in method
model.AddLexicographicalComparison([x[i], y[i], z[i]], [x[j], y[j], z[j]])
def get_total_floor_area_covered(model, n, on_floor_vars, l_eff, w_eff, container):
from ortools.sat.python.cp_model import CpModel, IntVar, BoolVarT
from typing import List
"""
Returns a list of area variables for each box: on_floor[i] * l_eff[i] * w_eff[i].
For each box, computes the area of its lower face if it is on the floor (on_floor[i] == 1),
otherwise 0. These can be summed for the total covered floor area.
Args:
model: The CpModel instance.
n: Number of boxes.
on_floor_vars: List of BoolVar, 1 if box i is on the floor, 0 otherwise.
l_eff, w_eff: Lists of IntVar, effective length and width of each box (depends on orientation).
container: Tuple/list of container dimensions (length, width, height).
Returns:
area_vars: List of IntVar, each representing the area of box i on the floor (0 if not on floor).
"""
max_area: int = container[0] * container[1]
area_vars: List[IntVar] = []
for i in range(n):
area_i: IntVar = model.NewIntVar(0, max_area, f'area_on_floor_{i}')
tmp: IntVar = model.NewIntVar(0, max_area, f'tmp_{i}')
model.AddMultiplicationEquality(tmp, [l_eff[i], w_eff[i]])
model.AddMultiplicationEquality(area_i, [on_floor_vars[i], tmp])
area_vars.append(area_i)
return area_vars
def prefer_orientation_where_side_with_biggest_surface_is_at_the_bottom(perms_list, orient, boxes):
from ortools.sat.python.cp_model import BoolVarT
from typing import List, Tuple, Dict, Any
"""
Returns a list of orientation variables where the largest face is on the bottom for each box,
but only for boxes with free rotation.
For each box with free rotation, identifies the orientation(s) where the bottom face (l, w)
has the largest possible area. Returns the corresponding orientation variables, which can be
used in a soft constraint to encourage these orientations.
Args:
perms_list: List of lists of (l, w, h) tuples for each box's allowed orientations.
orient: List of lists of BoolVar, orient[i][k] is 1 if box i uses orientation k.
boxes: List of box dicts (to check rotation type).
Returns:
preferred_orient_vars: List of BoolVar, one for each preferred orientation (largest face down).
"""
preferred_orient_vars: List[BoolVarT] = []
for i, perms in enumerate(perms_list):
if boxes[i].get('rotation', 'free') != 'free':
continue
bottom_areas: List[int] = [l * w for (l, w, h) in perms]
max_area: int = max(bottom_areas)
for k, area in enumerate(bottom_areas):
if area == max_area:
preferred_orient_vars.append(orient[i][k])
return preferred_orient_vars