Bin Complexity
An isolated model exploring how stow behaviour, working limits, and item placement rules interact inside a single fulfilment bin.
Optimized for larger screens
Some simulations are best viewed on larger screens in landscape orientation, but they might work on your phone. I just don't optimise for them.
What this is
This is the bin-level model extracted from the Machine Bureaucracy simulator. It isolates a single Library bin (capacity 12) and lets you manually add items from the product catalogue, choosing between two stow behaviours:
- Rule-following — slower, cleans messiness, can access 2 extra headroom slots beyond the working limit.
- KPI-chasing — faster, increases messiness, capped at the working limit.
The goal of the simulation is to determine the “messiness” a bin is in when a random stower adds items over time. For example, one stower who is neat and follows the rules might add in 3 items, and then a messy, KPI-following stower might add in 6. This model lets you observe how overstuffing and blocking emerge from the interaction of placement rules, working limits, and stow behaviour — without the noise of the full warehouse simulation.
How it works
Every time you add or remove an item, a chain of calculations runs to update the bin’s state. This section walks through each step in order, with the actual code, the formula, and a plain-English explanation of what’s happening and why.
Step 1 — Item physics lookup
Step 1 — Item physics lookup
Before any messiness maths can happen, the model needs to know what kind of thing you’re putting in the bin. Instead of tuning 250+ individual products, every item maps to one of eight archetypes — broad physical categories like “flat and small” or “spherical”.
Each archetype defines four properties on a 0→1 scale:
| Property | What it means | Example |
|---|---|---|
| sizeWeight | How much physical space the item takes up (0.5 = tiny, 2.0 = bulky) | A phone case is 0.6, a laptop sleeve is 1.2 |
| irregularity | How awkward the shape is — hard to line up neatly (0 = cube, 1 = chaotic shape) | A boxed item is 0.1, a ball is 0.7 |
| fragility | How much extra care it needs (0 = robust, 1 = very fragile) | A battery pack is 0.15, jewellery is 0.8 |
| stackability | How well it nests or stacks on other items (0 = won’t stack, 1 = perfect) | A ball is 0.05, a flat book is 0.95 |
// item-archetypes.ts — every item maps to an archetype
const ARCHETYPES = {
"flat-small": { sizeWeight: 0.6, irregularity: 0.1, fragility: 0.1, stackability: 0.95 },
"flat-large": { sizeWeight: 1.2, irregularity: 0.15, fragility: 0.2, stackability: 0.85 },
"cylindrical": { sizeWeight: 0.9, irregularity: 0.45, fragility: 0.15, stackability: 0.3 },
"spherical": { sizeWeight: 0.8, irregularity: 0.7, fragility: 0.1, stackability: 0.05 },
"boxed": { sizeWeight: 1.0, irregularity: 0.1, fragility: 0.15, stackability: 0.9 },
"irregular": { sizeWeight: 1.0, irregularity: 0.75, fragility: 0.15, stackability: 0.15 },
"fragile-flat": { sizeWeight: 0.7, irregularity: 0.15, fragility: 0.8, stackability: 0.7 },
"fragile-irregular": { sizeWeight: 0.9, irregularity: 0.6, fragility: 0.75, stackability: 0.2 },
};
function getItemPhysics(itemName: string): ItemPhysics {
const archetypeId = ITEM_ARCHETYPE_MAP[itemName] ?? "boxed"; // fallback
return ARCHETYPES[archetypeId];
}Why this matters: A ball rolling around the bin creates more disorder than a flat book that stacks neatly. The physics profile controls how much messiness each item contributes. Without it, adding “Socks” and adding a “Massage Ball” would have the same effect — which isn’t realistic.
Step 2 — Check the placement cap
Step 2 — Check the placement cap
Before any item goes in, the model checks: is there room? The answer depends on the stow behaviour and the working limit.
The working limit is the number of slots that are considered “reachable” — a worker can easily grab items up to this depth. Beyond that, it takes extra effort to reorganise the bin and make space.
// bin-model-v2.ts — placement cap
function getEffectiveBinPlacementCap(
bin: { capacity: number },
behavior: "kpi-chasing" | "rule-following",
binWorkingLimit: number = 10,
): number {
const base = clamp(Math.round(binWorkingLimit), 1, 48);
if (behavior === "rule-following") {
return Math.min(bin.capacity, base + 2); // +2 headroom
}
return Math.min(bin.capacity, base);
}In plain English: A KPI-chasing worker stops placing items when the bin hits the working limit — they won’t take the time to reorganise. A rule-following worker will tidy things up to squeeze in 2 more items. With a working limit of 10 on a 12-slot bin, KPI-chasing maxes out at 10, rule-following can reach 12.
Why this matters: This is where blocking comes from. Once the cap is hit, no more items can be placed. If every bin on the floor is at its cap, stowers have nowhere to put incoming items. In the real world, they start breaking rules — wedging items in, stacking unsafely — which is what the model is designed to prevent.
Step 3 — Calculate messiness (the core)
Step 3 — Calculate messiness (the core)
This is the heart of the model. Messiness is not a single number — it’s decomposed into three dimensions, each representing a different kind of bin degradation:
| Dimension | What it captures | What it affects |
|---|---|---|
| Disorder | Items out of logical sequence — misplaced, jumbled | Pick time (worker has to search) |
| Density | Items physically wedged together, hard to reach | Ergonomic cost, reorganisation time |
| Obscurity | Labels hidden, items hard to identify visually | Counting confidence |
Each dimension runs from 0 (perfect) to 1 (maximum mess). When an item is stowed, all three update simultaneously based on the stow behaviour, the item’s physical properties, and how full the bin already is.
KPI-chasing stows — all three axes increase
When someone is rushing to hit their rate target, they make the bin worse on every axis. The increase is shaped by the item’s physical properties:
Where are the current disorder, density, and obscurity values, and is the fill pressure — how much the bin’s fullness amplifies the messiness increase.
// bin-model-v2.ts — KPI-chasing messiness increase
if (behavior === "kpi-chasing") {
const disorderDelta =
(1 - m.disorder) *
(0.04 + demand * 0.012 + fillPressure * 0.04 + itemPhysics.irregularity * 0.06);
const densityDelta =
(1 - m.density) *
(0.03 + demand * 0.01 + fillPressure * 0.05 + itemPhysics.sizeWeight * 0.03
- itemPhysics.stackability * 0.02);
const obscurityDelta =
(1 - m.obscurity) *
(0.025 + demand * 0.008 + fillPressure * 0.035 + itemPhysics.irregularity * 0.03
+ itemPhysics.sizeWeight * 0.02);
return {
disorder: clamp(m.disorder + disorderDelta, 0, 1),
density: clamp(m.density + densityDelta, 0, 1),
obscurity: clamp(m.obscurity + obscurityDelta, 0, 1),
};
}Reading the formula: The (1 - current_value) term is a diminishing returns brake — a nearly-perfect bin (disorder = 0) has plenty of room to get worse, but a bin that’s already chaotic (disorder = 0.9) can only get a little worse. This prevents any axis from jumping unrealistically.
What the item physics do:
- An irregularly shaped item (like a massage ball) adds extra disorder — it rolls around and disrupts the arrangement.
- A large item adds extra density — it physically crowds the space.
- A stackable item reduces the density penalty — it nests neatly on top of what’s already there.
- Fill pressure makes everything worse when the bin is more than half full. A half-empty bin absorbs mess easily; a nearly-full bin amplifies it.
Rule-following stows — mess goes down (mostly)
A rule-following stower takes extra time to reorganise the bin as they place items. This reduces disorder and obscurity, and slightly reduces density. But placing an item still generates some wear — you can’t add something without any impact.
// bin-model-v2.ts — rule-following messiness (net decrease)
const careMultiplier = 1 - itemPhysics.fragility * 0.3;
const disorderRecovery = (0.05 + m.disorder * 0.5) * careMultiplier;
const disorderWear = demand * 0.005 + fillPressure * 0.01;
const obscurityRecovery = (0.04 + m.obscurity * 0.45) * careMultiplier;
const obscurityWear = demand * 0.004 + fillPressure * 0.008;
const densityRecovery = 0.015 * careMultiplier;
const densityWear = demand * 0.006 + fillPressure * 0.015
+ itemPhysics.sizeWeight * 0.01;
return {
disorder: clamp(m.disorder + disorderWear - disorderRecovery, 0, 1),
density: clamp(m.density + densityWear - densityRecovery, 0, 1),
obscurity: clamp(m.obscurity + obscurityWear - obscurityRecovery, 0, 1),
};In plain English: Recovery (tidying) outweighs wear (adding an item), so the net effect is cleaning. But fragile items slow the cleaning down — the careMultiplier means if you’re placing glass (fragility = 0.8), you spend more of your time being careful with that item and less time reorganising the rest. Also, recovery scales with how messy the bin already is: a very messy bin gets cleaned faster because there’s more obvious tidying to do.
Step 4 — Aggregate messiness score
Step 4 — Aggregate messiness score
The three dimensions are combined into a single headline number. This is what drives the messiness % shown in the simulator.
// bin-model-v2.ts — weighted aggregate
function getAggregateMessiness(m: BinMessiness): number {
return clamp(m.disorder * 0.35 + m.density * 0.35 + m.obscurity * 0.3, 0, 1);
}In plain English: Disorder and density are weighted equally at 35% each, obscurity slightly less at 30%. This means a bin can have hidden labels (high obscurity) but still score okay if items are in order (low disorder) and not physically jammed (low density). The single number is used for the trend sparkline and the label thresholds.
The aggregate score maps to a human-readable label:
| Score range | Label |
|---|---|
| 0 – 0.23 | Neat |
| 0.24 – 0.39 | Mixed |
| 0.40 – 0.69 | Messy |
| 0.70 – 1.00 | Chaotic |
The threshold at 0.40 is important — once a bin crosses into “messy” territory, the capacity bar changes colour from green to amber. This is the NON_NEAT_MESSINESS_THRESHOLD constant.
Step 5 — Counting confidence
Step 5 — Counting confidence
When a bin is messy, the model predicts that a count audit would be less reliable. Items are hard to see, labels are buried, things are wedged together — all of which make it harder to accurately count what’s in the bin.
Where — the fill penalty only kicks in once the bin passes 50% full.
// bin-model-v2.ts — counting confidence
function getBinCountingConfidence(bin: { capacity; occupied; messiness }): number {
const m = bin.messiness;
const fillPenalty = clamp(Math.max(0, getBinFillRatio(bin) - 0.5), 0, 0.75);
return clamp(
0.96 - m.obscurity * 0.52 - m.density * 0.18 - fillPenalty * 0.22,
0.08,
0.99,
);
}In plain English: A clean, half-empty bin starts at 96% confidence — nearly perfect. Then three things drag it down:
- Obscurity (biggest penalty at 0.52 weight) — if labels are hidden or items are hard to identify, a counter will miscount.
- Density (moderate penalty at 0.18) — items wedged tightly together are harder to separate and count individually.
- Fullness (mild penalty at 0.22, only above 50%) — a full bin means more items to count and more chance of error.
The floor is 8% — even in the worst case, you can identify some of what’s there. The ceiling is 99% — no count is ever perfectly certain.
Why this matters: In the full Machine Bureaucracy simulation, low counting confidence causes downstream picker drag. If the system’s count says there are 8 items but a counter only finds 7, that triggers a recount workflow. Every recount takes a picker off their route.
Step 6 — Availability band
Step 6 — Availability band
The model classifies the bin into one of four availability bands based on how full it is relative to the working limit. This is the coloured badge shown on the bin card (green, amber, orange, or red).
// bin-model-v2.ts — availability bands
function getBinAvailabilitySnapshot(bin, binWorkingLimit = 10) {
const workingCapacity = getEffectiveBinPlacementCap(bin, "kpi-chasing", binWorkingLimit);
const remainingWorkingSlots = Math.max(0, workingCapacity - bin.occupied);
const workingPressure = workingCapacity <= 0 ? 0 : bin.occupied / workingCapacity;
if (bin.occupied >= bin.capacity) return { ..., band: "capped" };
if (workingPressure >= 0.8) return { ..., band: "tight" };
if (workingPressure >= 0.5) return { ..., band: "working" };
return { ..., band: "buffered" };
}In plain English: Working pressure measures how full the bin is against the KPI-chasing cap (the tighter of the two limits). This tells you how much room a fast-moving worker has:
| Band | Working Pressure | What it means |
|---|---|---|
| 🟢 Buffered | < 50% | Plenty of room. No placement stress. |
| 🟡 Working | 50% – 79% | Getting used. Some choices require thought. |
| 🟠 Tight | 80% – 99% | Almost full. Finding a correct slot is slow. |
| 🔴 Capped | At physical capacity | No items can be placed, regardless of behaviour. |
Note that a bin can be “capped” for KPI-chasing at 10/12 but still have room for rule-following stows — the badge always reflects the most restrictive scenario.
Putting it all together
Putting it all together
Here’s the full sequence that runs every time you click an item:
1. Look up item physics → "What kind of thing is this?"
2. Check placement cap → "Is there room under the current behaviour?"
3. Calculate fill pressure → "How full is the bin after this item?"
4. Project new messiness → "How does this item + behaviour change the 3 axes?"
5. Compute aggregate score → "Single headline number from the 3 axes"
6. Derive counting confidence → "How reliable is an inventory count now?"
7. Classify availability band → "Buffered / working / tight / capped?"
8. Record the event → "Log what happened for the event history"Every calculation feeds the next. The item’s shape affects messiness, which affects confidence, which in the full simulation affects pick drag, which increases pressure, which increases shortcuts — the feedback loop that the whole model is designed to reveal.