
3D Bin Packer
A Python library that packs 3D cuboids into a container, and a Streamlit demo that scores it against how the same items were really packed by hand.
Stack: Python, NumPy, Plotly, Streamlit, pytest, ruff, mypy, hypothesis.
Why I built it
I work for WareBee on warehouse digital twin implementation, so naturally it was a matter of time before I discovered container packing as a real problem. Once I heard of it, I was immediately hooked, wondering if I could beat human performance with my own packing algorithm.
How it was built
Literature first. PROJECT_PLAN.md opens with a problem definition and a review of already published approaches before proposing anything, and the implementation followed from that rather than from an idea I had upfront. Working this way with AI agents is fast, but requires weighing the risks that come with it.
The algorithms
Packing cuboids optimally is NP-hard, so this uses a greedy heuristic with an optional search on top. Three modes:
- Heuristic: one fast extreme-point greedy pass.
- GA: a genetic search over placement order that refines the heuristic. Slower, usually better.
- Sequential: packs items strictly in the order they need to be picked.
Sequential exists for the warehouse comparison. A picker meets items in whatever order their route produces, so an algorithm free to reorder the job is solving an easier problem and would flatter the result.
Heuristic
Two ideas do the work here, and the other two modes both build on them.
Extreme points. Rather than searching every position in the container, the packer keeps a set of candidate corners generated by the boxes already placed and prunes them as they become unusable. Each incoming item is tried at those points in each of its six axis-aligned orientations.
Occupancy and support. Space is tracked as a voxel grid, which answers what fraction of a box's base area would be supported rather than only whether a cell is full. SUPPORT_THRESHOLD = 0.5 requires at least half the base to rest on the floor or on other boxes, rejecting arrangements that could never be built.
GA
A candidate is a chromosome of placement order plus an orientation preference per item, decoded by running the heuristic over it. So the search inherits everything above and only explores sequence and rotation.
Scoring each candidate needs one number - fitness
fitness = gamma * item_count
- alpha * unused_vol_fraction
- beta * max_height_fraction
item_count: how many items got placed, weighted atgamma = 100.unused_vol_fraction:1 - utilisation, the share of container volume left empty, penalised atalpha = 1. This is what pushes placements together.max_height_fraction: the tallest placement over the container height, penalised atbeta = 1. It prefers a low flat pack to a tall thin one.
How the three weights compare to each other matters more than the terms they scale. One extra item placed is worth gamma, while both penalties are fractions between 0 and 1, weighted at 1, and so can never subtract more than 2 points between them. A single extra box is therefore worth at least fifty times the largest penalty available (no matter how wasteful or tall), which makes the score effectively lexicographic: item count decides the winner, and volume and height only break ties between packings holding the same number of boxes. That is the behaviour I wanted, since, for a warehouse, a container taking one more item beats one that merely looks neater.
There was a fourth term, delta * support_quality, which I have since removed. It ended up being superfluous and did not measure what the name suggested.
Sequential
This mode uses the same placement machinery with the order fixed, so it chooses position and orientation but never sequence.
The warehouse benchmark
The demo replays anonymised real pick-job data, packs each job with the selected algorithm, and compares the outcome against how those items were really packed. On the sample dataset, with a 27×32×50 cm container in sequential mode, it reports 140 containers saved (a 50% reduction) and 22 percentage points of extra fill across 26 jobs. Individual jobs, as well as the aggregate, can be inspected through the dashboard views.
I do not fully trust this result. This is a suspiciously large improvement, leading me to theorise I have either miscalculated the workers' percentage fill or not taken a constraint into account. I have not yet investigated the flaw behind this number, so I treat it as a figure the demo produces rather than a claim about the real efficacy of my algorithm.
Final thoughts
What I would do differently
I would spend more time on validation and on understanding the algorithms and the code, rather than accepting what the agent produced as correct, would help me iterate on the algorithm's accuracy and my own understanding much more quickly
Future adjustments
A reinforcement-learning stage is specified in PROJECT_PLAN.md and wired up with Torch and Gymnasium, but not integrated. It would refine placement order beyond what the genetic search reaches.
What's in the repo
bin_packing/extreme_points.py: candidate placement points and their pruningbin_packing/occupancy.py: voxel grid and supported-area queriesbin_packing/heuristic.py: greedy pass and the support thresholdbin_packing/genetic.py,bin_packing/fitness.py: genetic search and its scoringbin_packing/sequential.py: order-preserving modebin_packing/orientations.py: the six axis-aligned rotationsbin_packing/warehouse_io.py,warehouse_algorithms.py,warehouse_compare.py: pick-job ingest and the comparisonbin_packing/visualisation.py: Plotly views and the drop animationPROJECT_PLAN.md,SEQUENTIAL_PACKING_PLAN.md: the literature review and the design that followed