This post is about what sometimes is called a 2d knapsack problem. It can also be considered as a bin-packing problem. These are famously difficult problems, so let's have a look at a relatively small instance.
So, we have one 2d container of a given size. Furthermore, we have a collection of items: rectangles of a given size, with a value. The goal is to pack items in our container without overlapping each other such that the total value is maximized.
To make things concrete, let's have a look at a small data set (randomly generated):
---- 18 PARAMETER data problem data width height available value value/size k1 20.000 4.000 2.000 338.984 4.237 k2 12.000 17.000 6.000 849.246 4.163 k3 20.000 12.000 2.000 524.022 2.183 k4 16.000 7.000 9.000 263.303 2.351 k5 3.000 6.000 3.000 113.436 6.302 k6 13.000 5.000 3.000 551.072 8.478 k7 4.000 7.000 6.000 86.166 3.077 k8 6.000 18.000 8.000 755.094 6.992 k9 14.000 2.000 7.000 223.516 7.983 k10 9.000 11.000 5.000 369.560 3.733 container 30.000 20.000
Our container is \(30\times 20\). We have 10 different types of items. The column available indicates the number of items of the current type. This means we have actually 51 items. Instead of value, it may be better to look at the value/size column. This is the relative value, normalized for size (i.e. height times width). The four most valuable items are k6, k9, k8, and k5. Hopefully, we will see them back in the solution.
An interesting wrinkle is: the items can be rotated. There are different ways of handling this. We can duplicate all items and add a rotated version. However, we need to make sure that the availability limit is applied to both. In the first MIP model below, I decided to add a rotation index \(r\) to indicate whether an item is rotated. This is almost the same as duplicating items. In the second model, I introduce an explicit binary variable to indicate whether the item is rotated.
Our solution will consist of a subset of the items plus a 2d no-overlapping configuration of those items. There is no way to just ask for the optimal selection of items: we always need to figure out the actual layout.
In this post, I'll discuss three different models:
- A covering MIP model that works with integer widths, heights, and locations,
- a continuous size MIP model where the sizes, and locations, can be fractional,
- and a CP-SAT model where all coefficients need to be integer-valued.
These three models are very different from each other. They share almost nothing!
A covering model
For this model, we assume that the container is a grid of cells, and each item occupies a number of cells. In other words, I assume the width and height are integer-valued. The main decision variable is: \[\color{darkred}x_{k,r,i,j} = \begin{cases} 1 & \text{if an item of type $k$ and direction $r$ is placed at cell \((i,j)\)} \\ 0 & \text{otherwise}\end{cases}\] The index \(r\) has two elements: \({r,nr}\) indicating if the corresponding item \(k\) is rotated or not. My convention is: if \(\color{darkred}x_{k,r,i,j}=1\) for a non-rotated item, then cells \((i',j')\), with \(i'=i,..,i+\color{darkblue}{\mathit{width}}_k-1\) and \(j'=j,..,j+\color{darkblue}{\mathit{height}}_k-1\) are occupied by this item. We need some rule to precisely state what it means if an item \(k\) is placed at \((i,j)\).
The first thing I did was creating two sets: \(\color{darkblue}{\mathit{ok}}\) and \(\color{darkblue}{\mathit{cover}}\).
The first set is \(\color{darkblue}{\mathit{ok}}_{k,r,i,j}\) indicating the allowed cells \((i,j)\) where we can place an item of type \(k\) (rotated or not). Below we see the allowed assignments of item k3:
ok(k,r,i,j) |
The second set is \(\color{darkblue}{\mathit{cover}}_{k,r,i,j,i',j'}\) indicating which cells \((i',j')\) are covered when placing item \(k\) on cell \((i,j)\). This is a large 6 dimensional set. It has 282,944 elements.
cover(k,r,i,j,ii,jj) |
Here we see the coverage when we place item k1 at (r4,c6) (not rotated).
With this we can write our covering model:
Covering Model |
---|
\[\begin{align}\max&\>\color{darkred}{\mathit{totalValue}} = \sum_{k,r,i,j|\color{darkblue}{\mathit{ok}}(k,r,i,j)} \color{darkblue}{\mathit{value}}_k \cdot \color{darkred}x_{k,r,i,j} && &&\text{Maximize total value}\\ & \sum_{k,r,i,j|\color{darkblue}{\mathit{cover}}(k,r,i,j,i',j')}\color{darkred}x_{k,r,i,j} \le 1 && \forall i',j' && \text{No overlap} \\ & \sum_{r,i,j|\color{darkblue}{\mathit{ok}}(k,r,i,j)} \color{darkred}x_{k,r,i,j} \le \color{darkblue}{\mathit{avail}}_k &&\forall k && \text{Availability} \\ & \color{darkred}x_{k,r,i,j} \in \{0,1\} \end{align}\] |
I considered adding the constraint: \[\sum_{k,r,i,j|\color{darkblue}{\mathit{ok}}(k,r,i,j)} \color{darkblue}{\mathit{area}}_k \cdot \color{darkred}x_{k,r,i,j} \le \color{darkblue}{\mathit{containerSize}}\] This did not seem to be much of a help.
The model is not very small:
MODEL STATISTICS BLOCKS OF EQUATIONS 3 SINGLE EQUATIONS 611 BLOCKS OF VARIABLES 2 SINGLE VARIABLES 4,273 NON ZERO ELEMENTS 291,489 DISCRETE VARIABLES 4,272
However, this model solves quickly: just 63 nodes were needed. The results are:
---- 83 VARIABLE x.L item k is placed at (i,j) r1.c1 r1.c14 r1.c28 r3.c14 r5.c14 r6.c1 r6.c6 r7.c11 r7.c29 k5 .nr 1 k6 .nr 1 k6 .r 1 1 k8 .r 1 k9 .nr 1 1 1 k9 .r 1 + r13.c11 r19.c1 r19.c15 k8 .r 1 k9 .nr 1 1 ---- 83 VARIABLE totalValue.L = 4617.938 objective (maximized)
A continuous size model
Intermezzo: pairwise no-overlap constraints. These constraints are: \[\begin{align} \color{darkred}x_a &\ge \color{darkred}x_b + \color{darkblue}w_b \\ {\bf or}\>\color{darkred}x_b &\ge \color{darkred}x_a + \color{darkblue}w_a \\ {\bf or}\>\color{darkred}y_a &\ge \color{darkred}y_b + \color{darkblue}h_b \\ {\bf or}\>\color{darkred}y_b &\ge \color{darkred}y_a + \color{darkblue}h_a \end{align}\] The or conditions can be implemented with additional binary variable and big-M constraints: \[\begin{align} & \color{darkred}x_a \ge \color{darkred}x_b + \color{darkblue}w_b - \color{darkblue}M \cdot (1-\color{darkred} \delta_{a,b,1})\\ & \color{darkred}x_b \ge \color{darkred}x_a + \color{darkblue}w_a - \color{darkblue}M \cdot (1-\color{darkred} \delta_{a,b,2})\\ &\color{darkred}y_a \ge \color{darkred}y_b + \color{darkblue}h_b - \color{darkblue}M \cdot (1-\color{darkred} \delta_{a,b,3})\\ & \color{darkred}y_b \ge \color{darkred}y_a + \color{darkblue}h_a - \color{darkblue}M \cdot (1-\color{darkred} \delta_{a,b,4}) \\ & \sum_k \color{darkred}\delta_{a,b,k} \ge 1 \end{align}\] We need to add one more detail: we want to have these constraints only active if both items a and b are selected to be in our container. So our constraints become: \[\begin{align} & \color{darkred}x_a \ge \color{darkred}x_b + \color{darkblue}w_b - \color{darkblue}M \cdot (1-\color{darkred} \delta_{a,b,1}) - \color{darkblue}M\cdot (1-\color{darkred}{\mathit{selected}}_a) - \color{darkblue}M\cdot (1-\color{darkred}{\mathit{selected}}_b)\\ & \color{darkred}x_b \ge \color{darkred}x_a + \color{darkblue}w_a - \color{darkblue}M \cdot (1-\color{darkred} \delta_{a,b,2}) - \color{darkblue}M\cdot (1-\color{darkred}{\mathit{selected}}_a) - \color{darkblue}M\cdot (1-\color{darkred}{\mathit{selected}}_b)\\ &\color{darkred}y_a \ge \color{darkred}y_b + \color{darkblue}h_b - \color{darkblue}M \cdot (1-\color{darkred} \delta_{a,b,3})- \color{darkblue}M\cdot (1-\color{darkred}{\mathit{selected}}_a) - \color{darkblue}M\cdot (1-\color{darkred}{\mathit{selected}}_b) \\ & \color{darkred}y_b \ge \color{darkred}y_a + \color{darkblue}h_a - \color{darkblue}M \cdot (1-\color{darkred} \delta_{a,b,4}) - \color{darkblue}M\cdot (1-\color{darkred}{\mathit{selected}}_a) - \color{darkblue}M\cdot (1-\color{darkred}{\mathit{selected}}_b)\\ & \sum_k \color{darkred}\delta_{a,b,k} \ge 1 \end{align}\] The final complication is that we can rotate items. So the width can become the hight and vice versa.
---- 56 SET kn (k,n) combinations n1 n2 n3 n4 n5 n6 n7 n8 n9 k1 YES YES k2 YES YES YES YES YES YES k3 YES YES k4 YES YES YES YES YES YES YES YES YES k5 YES YES YES k6 YES YES YES k7 YES YES YES YES YES YES k8 YES YES YES YES YES YES YES YES k9 YES YES YES YES YES YES YES k10 YES YES YES YES YES ---- 56 PARAMETER num sequence numbers for (k,n) n1 n2 n3 n4 n5 n6 n7 n8 n9 k1 1 2 k2 3 4 5 6 7 8 k3 9 10 k4 11 12 13 14 15 16 17 18 19 k5 20 21 22 k6 23 24 25 k7 26 27 28 29 30 31 k8 32 33 34 35 36 37 38 39 k9 40 41 42 43 44 45 46 k10 47 48 49 50 51
Items to compare |
Continuous size model |
---|
\[\begin{align} \max\> & \color{darkred}{\mathit{totalValue}} = \sum_{\color{darkblue}{\mathit{kn}}(k,n)} \color{darkblue}{\mathit{value}}_k \cdot \color{darkred}x_{k,n} && && \text{Maximize total value}\\ & \color{darkred}{\mathit{pos2}}_{k,n,xy} = \color{darkred}{\mathit{pos}}_{k,n,xy} + \color{darkblue}{\mathit{wh}}_{k,R,xy}\cdot \color{darkred}{\mathit{rot}}_{k,n} + \color{darkblue}{\mathit{wh}}_{k,{\mathit{NR}},xy}\cdot (1-\color{darkred}{\mathit{rot}}_{k,n}) &&\forall \color{darkblue}{\mathit{kn}}_{k,n},xy&&\text{End-point of item} \\ & \color{darkred}{\mathit{pos}}_{k,n,xy} \ge \color{darkred}{\mathit{pos2}}_{k',n',xy} - \color{darkblue}M \cdot (3-\color{darkred}\delta_{k,n,k',n',xy,1}-\color{darkred}x_{k,n}-\color{darkred}x_{k',n'})&&\forall \color{darkblue}{\mathit{compare}}_{n,k,n',k'}, xy && \text{No overlap}\\ &\color{darkred}{\mathit{pos}}_{k',n',xy} \ge \color{darkred}{\mathit{pos2}}_{k,n,xy} - \color{darkblue}M \cdot (3-\color{darkred}\delta_{k,n,k',n',xy,2}-\color{darkred}x_{k,n}-\color{darkred}x_{k',n'})&&\forall \color{darkblue}{\mathit{compare}}_{n,k,n',k'},xy && \text{No overlap} \\ & \sum_{xy} \left( \color{darkred}\delta_{k,n,k',n',xy,1}+\color{darkred}\delta_{k,n,k',n',xy,2}\right)\ge 1 && \forall \color{darkblue}{\mathit{compare}}_{n,k,n',k'} && \text{No overlap}\\ &\color{darkred}x_{k,n} \in \{0,1\}\\ &\color{darkred}{\mathit{rot}}_{k,n} \in \{0,1\}\\ &\color{darkred}{\mathit{pos}}_{k,n,xy} \ge 0 \\ &\color{darkred}{\mathit{pos2}}_{k,n,xy} \in [0,\color{darkblue}{\mathit{WH}}_{xy}] \end{align}\] |
- We can order the variable \(\color{darkred}x_{k,n}\) by \(n\): \[\color{darkred}x_{k,n-1}\ge\color{darkred}x_{k,n}\] This makes the solution easier to read.
- We can order variables \(\color{darkred}{\mathit{pos}}\) of the same type by their \(x\)-coordinate: \[\color{darkred}{\mathit{pos}}_{k,n-1,x}\le \color{darkred}{\mathit{pos}}_{k,n,x}\]
- We know that the total area of items cannot exceed the area of the container: \[\sum_{\color{darkblue}{\mathit{kn}}(k,n)} \color{darkblue}{\mathit{area}}_k \cdot \color{darkred}x_{k,n} \le \color{darkblue}{\mathit{containerArea}}\]
---- 127 VARIABLE x.L item (k,n) is placed (rotated/non-rotated) n1 n2 n3 n4 n5 n6 k5 1 k6 1 1 1 k8 1 1 k9 1 1 1 1 1 1 ---- 127 PARAMETER positions rectangles (x1,y1)-(x2,y2) x1 y1 x2 y2 rotated k5.n1 3.000 6.000 k6.n1 17.000 30.000 5.000 k6.n2 20.000 5.000 25.000 18.000 1.000 k6.n3 25.000 5.000 30.000 18.000 1.000 k8.n1 2.000 12.000 20.000 18.000 1.000 k8.n2 2.000 6.000 20.000 12.000 1.000 k9.n1 6.000 2.000 20.000 1.000 k9.n2 2.000 18.000 16.000 20.000 k9.n3 3.000 17.000 2.000 k9.n4 3.000 4.000 17.000 6.000 k9.n5 3.000 2.000 17.000 4.000 k9.n6 16.000 18.000 30.000 20.000 ---- 127 VARIABLE totalValue.L = 4617.938 objective (maximized)
Solution by continuous size model |
Notes
- Slightly different data can cause very different performance.
- The covering model is a bit faster than the continuous size model.
- The problem is very simple. The MIP models not so much.
- I did not try to physically duplicate data items to model rotation. Just have two items: one not rotated and one rotated. Impose a constraint that says we can only pick one. That may be a bit simpler than what I tried, especially in the second model. The Python code in the next section is exactly doing this.
Covering model | Continuous size model | |
---|---|---|
Equations | 611 | 6,561 |
Variables | 4,273 | 5,407 |
Discrete Variables | 4,272 | 5,202 |
Objective | 4617.9378 | 4617.9378 |
Status | Optimal | Optimal |
Time (sec.) | 40 | 232 |
Iterations | 26,066 | 9,598,476 |
Nodes | 63 | 157,747 |
Options | Defaults | Defaults |
OR-Tools CP-SAT implementation
CP-SAT Model |
---|
\[\begin{align}\max & \sum_i {\bf int}(1000 \cdot\color{darkblue}{\mathit{value}}_i)\cdot\color{darkred}u_i && && \text{integer coefficients}\\ & \color{darkred}w'_i = \color{darkblue}w_i \cdot \color{darkred}u_i && \forall i && \text{zero or }\color{darkblue}w_i\\ & \color{darkred}h'_i = \color{darkblue}h_i \cdot \color{darkred}u_i && \forall i && \text{zero or }\color{darkblue}h_i \\ & \color{darkred}u_i + \color{darkred}u_{i+51} \le 1 && i \in 1,\dots,51 && \text{rotated or not} \\ & {\bf NoOverlap2D}([\color{darkred}{x1},\color{darkred}w',\color{darkred}{x2}],[\color{darkred}{y1},\color{darkred}h',\color{darkred}{y2}]) && && \text{uses interval variables} \\ & \sum_i \color{darkblue}h_i \cdot \color{darkblue}w_i \cdot \color{darkred}u_i \le \color{darkblue}H \cdot \color{darkblue}W && && \text{extra} \\ & \color{darkred}{x1}_i,\color{darkred}{x2}_i \in \{0,\dots,\color{darkblue}H\} \\ & \color{darkred}{y1}_i,\color{darkred}{y2}_i \in \{0,\dots,\color{darkblue}W\} \\ &\color{darkred}w'_i \in \{0,\dots,\color{darkblue}w_i\} \\ &\color{darkred}h'_i \in \{0,\dots,\color{darkblue}h_i\}\\ & \color{darkred}u_i \in \{0,1\} \end{align}\] |
return code:4 status:OPTIMAL CpSolverResponse summary: status: OPTIMAL objective: 4617934 best_bound: 4617934 booleans: 432 conflicts: 87347 branches: 125277 propagations: 362526 integer_propagations: 1855211 restarts: 269 lp_iterations: 209669 walltime: 142.573 usertime: 142.573 deterministic_time: 51.1133 primal_integral: 93.1079 x y w h x2 y2 v 0 0 14 14 2 14 16 223516 1 0 16 14 2 14 18 223516 2 0 18 14 2 14 20 223516 3 2 0 14 2 16 2 223516 4 16 0 14 2 30 2 223516 5 20 2 5 13 25 15 551072 6 25 2 5 13 30 15 551072 7 2 2 18 6 20 8 755093 8 2 8 18 6 20 14 755093 9 14 14 3 6 17 20 113436 10 17 15 13 5 30 20 551072 11 0 0 2 14 2 14 223516
Appendix 1: GAMS covering model
$ontext |
Appendix 2: GAMS continuous size model
$ontext |
Appendix 3: Python code for Or-tools CP-SAT model
from ortools.sat.python import cp_model |
Erwin, I looked at a similar model years ago. Despite the larger size, the covering MIP far outperformed the "continuous size" model. I believe this is because the continuous size model depends on disjunctive constraints; in the LP relaxation, disjunctive constraints tend to spread the values to 1/n, so the LP relaxation gives poor information. If necessary, I believe you could improve the performance of the covering MIP via symmetry reduction and/or column generation.
ReplyDeleteThat is certainly confirmed for these models. Sometimes the covering formulation can lead to really, really big models.
DeleteHi,
ReplyDeleteI tweaked a little the model (not sure it helps)
```for i in range(nr):
model.Add(xw[i] == wr[i]).OnlyEnforceIf(u[i])
model.Add(xw[i] == 0).OnlyEnforceIf(u[i].Not())
model.Add(yh[i] == hr[i]).OnlyEnforceIf(u[i])
model.Add(yh[i] == 0).OnlyEnforceIf(u[i].Not())
```
On my machine, it solves quite fast
CpSolverResponse summary:
status: OPTIMAL
objective: 4617936
best_bound: 4617936
booleans: 592
conflicts: 30049
branches: 51905
propagations: 627691
integer_propagations: 670265
restarts: 237
lp_iterations: 24505
walltime: 6.20536
usertime: 6.20536
deterministic_time: 13.1242
primal_integral: 106.057
return code:4
status:OPTIMAL
CpSolverResponse summary:
status: OPTIMAL
objective: 4617936
best_bound: 4617936
booleans: 592
conflicts: 30049
branches: 51905
propagations: 627691
integer_propagations: 670265
restarts: 237
lp_iterations: 24505
walltime: 6.20536
usertime: 6.20536
deterministic_time: 13.1242
primal_integral: 106.057
x y w h x2 y2 v
0 0 0 14 2 14 2 223516
1 0 2 14 2 14 4 223516
2 0 4 14 2 14 6 223516
3 14 18 14 2 28 20 223516
4 18 5 5 13 23 18 551072
5 23 5 5 13 28 18 551072
6 0 18 14 2 14 20 223516
7 0 6 18 6 18 12 755094
8 14 0 3 6 17 6 113436
9 17 0 13 5 30 5 551072
10 0 12 18 6 18 18 755094
11 28 5 2 14 30 19 223516
Hi Laurent: that was my first formulation. I replaced it with a linear expression just because it was a little bit more compact. I ran it on colab.google.com. That may be a less "stable" environment (a small VM) than running it locally on a real machine.
DeleteIn the morning it seems a bit slower:
Deletereturn code:4
status:OPTIMAL
CpSolverResponse summary:
status: OPTIMAL
objective: 4617934
best_bound: 4617934
booleans: 444
conflicts: 1608380
branches: 1653210
propagations: 11072083
integer_propagations: 38767100
restarts: 1714
lp_iterations: 0
walltime: 331.133
usertime: 331.133
deterministic_time: 191.74
primal_integral: 347.965
Using the old formulation does not change performance. Which version of or-tools did you use ?
ReplyDeleteI did not print the version, but I see it installed: ortools-9.1.9490-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
DeleteSo, the jupyter backend seems to be mono-core, or close to.
ReplyDeleteI have rewritten the example, added a second model where the dimensions of the item depends on its state(not_selected, no_rotation, rotated).
ReplyDeleteIn the meantime, I have added a linear relaxation to the no_overlap_2d constraint that makes your equation obsolete.
I have also implemented detection of possible linearization of the product of two affine expression, which is necessary to rebuild the linear term in the energetic redundant equation.
This currently requires using the master branch of or-tools, while waiting for 9.2 to be released (hopefully this month).
With this version, I solve the problem much faster than the previous code.
The example is available at: https://github.com/google/or-tools/blob/master/examples/python/packing_2d_sat.py
Last update. I have changed the name of the example to knapsack_2d_sat.py. And we have implemented support for floating point coefficients and offsets in the objective. See https://github.com/google/or-tools/blob/master/examples/python/knapsack_2d_sat.py
DeleteProblem: a timber warehouse has a bundle of assorted widths and lengths, thickness is uniform. A customer places an order for a number of boards of varying widths and lengths, thickness is uniform. Is there a mathematical solution or software which identifies the most efficient way of meeting the order from the available timber?
ReplyDeleteThis is a two-dimensional cutting stock problem. There is a body of literature about this problem.
Deletecan you provide the code to display the output in container form for the 3rd case(using cp-sat solver)?
ReplyDelete