Ariel Fishgang
3D Bin Packer live preview

3D Bin Packer

PythonNumPyPlotlyGenetic AlgorithmsOptimisationLogisticsData Processing

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 at gamma = 100.
  • unused_vol_fraction: 1 - utilisation, the share of container volume left empty, penalised at alpha = 1. This is what pushes placements together.
  • max_height_fraction: the tallest placement over the container height, penalised at beta = 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 pruning
  • bin_packing/occupancy.py: voxel grid and supported-area queries
  • bin_packing/heuristic.py: greedy pass and the support threshold
  • bin_packing/genetic.py, bin_packing/fitness.py: genetic search and its scoring
  • bin_packing/sequential.py: order-preserving mode
  • bin_packing/orientations.py: the six axis-aligned rotations
  • bin_packing/warehouse_io.py, warehouse_algorithms.py, warehouse_compare.py: pick-job ingest and the comparison
  • bin_packing/visualisation.py: Plotly views and the drop animation
  • PROJECT_PLAN.md, SEQUENTIAL_PACKING_PLAN.md: the literature review and the design that followed