Rectangle Packaging Problem / Efficient 2D Packing
4 minute read
Summary
Programmatically pack 2D rectangular parts as efficiently as possible.
Background
I need to produce a bunch of 2D parts out of the same material. The parts are roughly rectangular and I need to produce an undefined large number (test jig stands for TheJigsApp.com). There are a ton of available panel sizes but the CNC I’m using can fit panels of 25x36" max. So our problem is to fit different size rectangles into a given rectangle [Wikipedia: Rectangle Packing].
One interesting paper on the topic is available here. The author’s original study code is up on Github here.
rectpack Library & Code Design
There’s a python library available that implements a few of the more efficient algorithms available [here].
Inputs
- Panel Size
- List of rectangle sizes
Outputs
- Optimal number of panels of a given size to minimize the waste.
- Total wasted area
- Percentage of total unused area
- Number of sets built
- Spreadsheet of rectangle orientations and rotations
I’ll check the placement visually using openscad.
Code
from rectpack import newPacker
import pandas
import jinja2
import numpy as np
import pprint
import click
def main(rects, width, length, bins=1):
packer = newPacker()
for rect in rects:
packer.add_rect(*rect)
for i in range(bins):
packer.add_bin(width, length)
packer.pack()
all_rects = packer.rect_list()
if len(all_rects) != len(rects):
raise UserWarning("Cant fit rectangles %d/%d", len(all_rects), len(rects))
dfout = {"bin": [], "x": [], "y":[], "w":[], "h":[], "area":[], "rotation":[]}
for rect, transform in zip(rects, all_rects):
b, x, y, w, h, rid = transform
dfout["bin"].append(b)
dfout["x"].append(x)
dfout["y"].append(y)
dfout["w"].append(w)
dfout["h"].append(h)
dfout["area"].append(h*w)
for rect in rects:
if (rect[0] == w and rect[1] == h):
dfout["rotation"].append(0)
break
elif (rect[0] == h and rect[1] == w):
dfout["rotation"].append(90)
break
else:
print("error ", rect, transform)
assert(0, "part not in expected order")
return dfout
def to_scad(df):
'''
Return scad as a string
'''
template = '''function get_shapes() = [
{% for _, l in df.iterrows() -%}
[{{l['bin']}}, {{l['x']}}, {{l['y']}}, {{l['w']}}, {{l['h']}}],
{% endfor %}
];
'''
return jinja2.Template(template).render(df=df)
def optimize(panels, max_bins):
config = {"nbins": [], "panel width": [], "panel length": [], "packing": [], "% used": []}
dfset = pandas.read_csv(f"rectangles.csv")
rect_set = [[line["width"], line["length"], line["name"]] for _, line in dfset.iterrows()]
for panel in panels:
for nbin in range(1, max_bins+1):
nsets = 1
while True:
try:
d = main(list(rect_set)*nsets, width=panel[0], length=panel[1], bins=nbin)
df = pandas.DataFrame(d)
config["packing"].append(df)
config["nbins"].append(nbin)
config["panel width"].append(panel[0])
config["panel length"].append(panel[1])
config["% used"].append(sum(df["w"]*df["h"])/(nbin*panel[0]*panel[1]))
nsets+=1
except UserWarning:
break
return pandas.DataFrame(config)
@click.command()
@click.option("--bins", "-n", type=int, default=1)
@click.option("--design", required=True)
@click.option("--output-scad", default="shapes.scad")
@click.option("--output-xlsx", default="shapes.xlsx")
def click_main(bins, design, output_scad, output_xlsx):
panels = [
[11.5, 35.5],
[23.5, 35.5],
]
config_df = optimize(panels=panels, max_bins=bins)
max_percent = np.max(config_df["% used"])
i = list(config_df["% used"]).index(max_percent)
df = config_df.iloc[i]
df_shapes = df["packing"]
df_shapes.to_excel(output_xlsx)
with open(output_scad, "w") as f:
f.write(to_scad(df_shapes))
pprint.pprint(list(config_df["% used"]))
pprint.pprint(config_df)
print(f"Min Error: {max_percent}\n Panel Size: {df['panel width']}x{df['panel length']}. nbins: {df['nbins']}")
click_main()
Our design file:
name | width | length |
---|---|---|
upright 1 | 1.5 | 13.5 |
upright 2 | 1.5 | 13.5 |
short side 1 | 2 | 8 |
short side 2 | 2 | 8 |
long side 1 | 2 | 9 |
long side 2 | 2 | 9 |
top | 7.5 | 9 |
button | 1.5 | 9 |
cross | 4.5 | 8.5 |
Finally the OpenScad
use <./shapes.scad>;
/*
# b - Bin index
# x - Rectangle bottom-left corner x coordinate
# y - Rectangle bottom-left corner y coordinate
# w - Rectangle width
# h - Rectangle height
# rid - User asigned rectangle id or None
*/
bins = 8;
width = 24;
gap=0.5;
length=36;
space = 25;
difference() {
for (i=[0:bins-1]) {
translate([(width+space)*i, 0])square([width,length]);
};
union() {
for (shape=get_shapes()) {
translate([shape[1]+(width+space)*shape[0]+gap/2, shape[2]+gap/2])square([shape[3], shape[4]], center=false);
};
};
};
echo(len(get_shapes()));
python3 pack.py --design rectangles.csv --bins 8
Min Error: 0.9099990010987913
Panel Size: 23.5x35.5. nbins: 3
We’re left with the following wasted outline from panels of 24"x36" (including a 1/4" keep out around the panel).
Expanding the space around the parts a bit shows how the packing is done over the three panels:
This approach gives the arrangement of the rectangles so I will need to go and match the size of each object to a suitable slot. You can do this in a cad program of choice or with openscad as long as you’re careful with the import/export formats. DXF works well in both directions but you need to worry about document size and loine thickness if you’re using dxf. Seems to work well and this definitely speeds up the process!