Space & Cube Utilization Scoring
Floor-plan dashboards lie. A rack aisle can read 95% “full” on a two-dimensional slot-occupancy report while more than a third of its actual cube sits empty above short cartons and behind half-depleted deep lanes. That gap between locations assigned and cube used is where a facility quietly loses storage capacity, triggers premature “we’re out of room” expansion projects, and forces fast-movers into overflow. This guide is part of the Pick-Path Optimization & Space Utilization architecture, and it covers the measurement layer specifically: how you score real cube utilization per location, separate honeycombing void from genuine fill, right-size slots to their occupants, and roll thousands of location scores up into a single facility index you can trend and alert on.
The thesis is that utilization is a volumetric signal, not an occupancy flag, and that a raw fill fraction is only half the story — a location can be under-filled (wasting committed cube) or over-packed (starved of handling headroom), and which failure matters depends entirely on the velocity of what sits there.
What Cube Utilization Scoring Is
Cube utilization is the ratio of occupied product volume to the usable volume of a location. It answers the only capacity question that maps to reality: of the cubic feet this slot can actually hold, how many are carrying inventory right now? That is a fundamentally different measurement from the two metrics it is routinely confused with:
- Area / floor utilization measures the two-dimensional footprint consumed — square feet of slab under rack, or the fraction of pick faces assigned. It ignores vertical space entirely, so a facility storing knee-high cartons in ten-foot bays can post 90% floor utilization while wasting more than half its cube.
- Location (slot) utilization counts occupied versus empty locations. A slot holding a single loose eaches carton reads as 100% occupied and 0% available even though it is 8% full by volume.
- Cube utilization divides occupied cubic volume by usable cubic volume. It is the metric that predicts when you genuinely run out of storage, and the only one that exposes honeycombing.
Honeycombing is the committed-but-empty cube that appears when a dedicated, deep, or full-pallet location is partially depleted and cannot be refilled with anything else until it clears. Picture a five-deep pallet lane assigned to one SKU: once two positions are pulled, those two positions are void, but the lane stays reserved to that SKU, so the cube is unusable to the rest of the catalog. Honeycombing is the single largest source of the “full but empty” paradox, and unlike ordinary low fill it cannot be fixed by adding stock — only by right-sizing the storage medium or changing the slotting rule.
Fill-rate and slot right-sizing close the loop. Right-sizing matches the cube of a slot to the cube profile of its assigned SKU: a slow C-mover with a two-carton on-hand does not belong in a full-pallet position, and a hyper-velocity A-mover does not belong in a shelf bin it overflows twice a shift. The utilization score is the evidence that drives those moves — but the decision to move belongs to the re-slotting economics, not to the scorer.
Input Data Requirements
Scoring consumes one snapshot row per storage location, joined to the velocity tier the Location Assignment & ABC Classification Algorithms layer already computed for the SKU sitting there. The single most common cause of a wrong utilization number is a gross-cube figure that includes structural clearance the location cannot actually use, so the clearance factor is a hard input, not an optional refinement.
| Field | Type | Precondition |
|---|---|---|
location_id |
str |
Non-null, unique per storage position; matches the WMS location master |
gross_cube_ft3 |
float |
> 0; rated interior cube of the location before clearance |
clearance_factor |
float |
0 ≤ f < 1; fraction of gross lost to beam, flue, and handling clearance |
occupied_cube_ft3 |
float |
≥ 0; summed cube of product currently in the location |
lane_depth |
int |
≥ 1; pallet positions deep (1 = single-deep/shared, > 1 = deep lane) |
velocity_tier |
str |
One of A/B/C; inherited from the classification layer |
sku_id |
str | None |
The occupant SKU, or null for an empty location |
from __future__ import annotations
from dataclasses import dataclass
from typing import Optional
@dataclass(frozen=True)
class SlotSnapshot:
"""One storage location at a point in time — the atomic input to scoring."""
location_id: str
gross_cube_ft3: float # rated interior cube before clearance
clearance_factor: float # fraction of gross lost to flue/beam/handling
occupied_cube_ft3: float # cube of product currently in the location
lane_depth: int # 1 = single-deep/shared, >1 = dedicated deep lane
velocity_tier: str # A/B/C inherited from the classification layer
sku_id: Optional[str] = None
The quality precondition that matters most is consistency of the cube basis. If occupied_cube_ft3 is derived from case dimensions but gross_cube_ft3 is measured to the rack rails, your utilization will read systematically low and every location will look like it needs consolidating. Measure both to the same envelope — inside usable faces — and validate that no row reports occupied cube greater than gross cube before scoring runs.
Step-by-Step Implementation
The pipeline is three pure passes: score each location’s cube utilization, classify honeycombing on the dedicated deep lanes, then roll every location up into a facility index with the health distribution attached. Each pass is a pure function so the whole thing is deterministic and testable.
1. Score Cube Utilization Per Location
Usable cube is gross cube net of the clearance factor — never the raw rated volume. Dividing occupied cube by usable cube yields the 0–1 utilization score, clamped at 1.0 so a data glitch that reports an over-stuffed location cannot poison the roll-up. The single-location scorer here is the same primitive covered in depth, with the packing-efficiency refinement, in How to Score Cube Utilization Per Location.
import logging
logger = logging.getLogger("space.utilization")
def cube_utilization(slot: SlotSnapshot) -> float:
"""Occupied cube as a fraction of usable cube (gross net of clearance), clamped to [0, 1]."""
usable = slot.gross_cube_ft3 * (1.0 - slot.clearance_factor)
if usable <= 0.0:
logger.warning("Non-positive usable cube at %s; scoring 0.0", slot.location_id)
return 0.0
score = slot.occupied_cube_ft3 / usable
if score > 1.0:
logger.warning("Over-cube at %s (%.2f); clamping to 1.0", slot.location_id, score)
return 1.0
return score
2. Detect Honeycombing on Dedicated Deep Lanes
Only dedicated deep lanes honeycomb — a single-deep or freely shareable location that is half-empty is simply available, because the next putaway can co-mingle into it. So the detector gates on lane_depth > 1 first, then measures the committed-but-empty cube and grades severity against two thresholds. The wasted cube it reports is the real prize: it is capacity you can reclaim by right-sizing the lane, and it feeds directly into the re-slotting break-even.
from dataclasses import dataclass
@dataclass
class HoneycombFinding:
location_id: str
utilization: float
wasted_cube_ft3: float
severity: str # none | minor | severe
def detect_honeycombing(slot: SlotSnapshot,
minor_below: float = 0.75,
severe_below: float = 0.40) -> HoneycombFinding:
"""Grade committed-but-empty cube in dedicated deep-lane locations."""
usable = slot.gross_cube_ft3 * (1.0 - slot.clearance_factor)
util = cube_utilization(slot)
wasted = max(usable - slot.occupied_cube_ft3, 0.0)
if slot.lane_depth <= 1:
severity = "none" # shared/single-deep slots re-mingle; no honeycomb
elif util < severe_below:
severity = "severe"
elif util < minor_below:
severity = "minor"
else:
severity = "none"
if severity != "none":
logger.info("Honeycomb %s at %s: %.0f ft3 committed-empty (U=%.2f, depth=%d)",
severity, slot.location_id, wasted, util, slot.lane_depth)
return HoneycombFinding(slot.location_id, util, wasted, severity)
3. Roll Up to a Facility Index
The facility index is deliberately cube-weighted, not a simple mean of location scores. A plain average lets ten thousand tiny empty shelf bins drag the number down and hide the fact that the pallet reserve — where the cube actually lives — is packed. Weighting each location by its usable cube gives you the number that answers “what fraction of my building’s storage volume is working?” alongside the honeycomb total and the share of locations sitting outside the healthy band.
from dataclasses import dataclass
@dataclass
class FacilityScore:
cube_weighted_utilization: float
honeycomb_cube_ft3: float
under_utilized_pct: float
over_utilized_pct: float
n_locations: int
def roll_up(slots: list[SlotSnapshot],
target_low: float = 0.55,
target_high: float = 0.90,
minor_below: float = 0.75,
severe_below: float = 0.40) -> FacilityScore:
"""Aggregate location scores into a cube-weighted facility index with a health split."""
if not slots:
logger.warning("roll_up received zero locations; returning empty score")
return FacilityScore(0.0, 0.0, 0.0, 0.0, 0)
total_usable = 0.0
total_occupied = 0.0
honeycomb = 0.0
under = over = 0
for slot in slots:
usable = slot.gross_cube_ft3 * (1.0 - slot.clearance_factor)
util = cube_utilization(slot)
total_usable += usable
total_occupied += min(slot.occupied_cube_ft3, usable)
honeycomb += detect_honeycombing(slot, minor_below, severe_below).wasted_cube_ft3 \
if slot.lane_depth > 1 and util < minor_below else 0.0
if util < target_low:
under += 1
elif util > target_high:
over += 1
n = len(slots)
score = FacilityScore(
cube_weighted_utilization=total_occupied / total_usable if total_usable else 0.0,
honeycomb_cube_ft3=honeycomb,
under_utilized_pct=under / n,
over_utilized_pct=over / n,
n_locations=n,
)
logger.info("Facility index: U=%.3f, honeycomb=%.0f ft3, under=%.0f%%, over=%.0f%%",
score.cube_weighted_utilization, score.honeycomb_cube_ft3,
100 * score.under_utilized_pct, 100 * score.over_utilized_pct)
return score
The action recommendation that turns a score into a slotting move is the quadrant from the diagram above, expressed as a small function. It never moves anything itself — it emits a label the re-slotting layer can act on or ignore based on economics.
def recommend_action(slot: SlotSnapshot,
target_low: float = 0.55,
target_high: float = 0.90) -> str:
"""Map a location's utilization and velocity tier to a right-sizing recommendation."""
util = cube_utilization(slot)
fast = slot.velocity_tier in ("A", "B")
if util > target_high:
return "WATCH_OVERFLOW" if fast else "DOWNSIZE_TO_RESERVE"
if util < target_low:
return "RIGHT_SIZE_DOWN" if fast else "CONSOLIDATE"
return "OK"
Tuning & Calibration
Every threshold above is a facility-specific lever, and the two that move outcomes most are the target band and the honeycomb severity cuts. Set target_high too low and the scorer nags you to compress locations that are healthily full; set it above ~0.92 and you erase the handling headroom pickers need to work a face without restacking. The target low is where consolidation candidates begin — pushing it up surfaces more reclaimable cube but also more noise. Recompute weekly against a fresh snapshot; utilization drifts far faster than velocity tiers do.
# space_scoring.yaml — one profile per facility
clearance_factor_default: 0.12 # fallback flue/beam/handling clearance if not measured
target_utilization:
low: 0.55 # below this a location is a consolidation candidate
high: 0.90 # above this a location is over-packed / no headroom
honeycomb:
min_lane_depth: 2 # only lanes this deep or deeper can honeycomb
minor_below: 0.75 # dedicated lane under this fill = minor honeycomb
severe_below: 0.40 # dedicated lane under this fill = severe honeycomb
rollup:
weight_by: cube # cube | count — cube weighting is the default
recalc_cadence: weekly # snapshot + rescore frequency
# Equivalent Python config dict consumed by the scorer
SPACE_SCORING = {
"clearance_factor_default": 0.12,
"target_utilization": {"low": 0.55, "high": 0.90},
"honeycomb": {"min_lane_depth": 2, "minor_below": 0.75, "severe_below": 0.40},
"rollup": {"weight_by": "cube"},
"recalc_cadence": "weekly",
}
Parameter sensitivity is not uniform. clearance_factor is a physical constant per rack type — measure it once and treat it as ground truth. The target band is an operating-policy lever tied to how much surge headroom you carry. The honeycomb cuts are the levers you actually tune against results: if right-sizing lanes flagged minor yields little reclaimed cube, raise severe_below and act only on the severe tail first.
Validation & Testing
Never trend a facility index you have not asserted. Three invariants must hold on every run: every location score lands in [0, 1], a single-deep location never registers as honeycombing, and the cube-weighted roll-up equals the analytic value on a hand-checked fixture. These pytest checks run in the scoring job before the index is written.
def _lane(loc: str, gross: float, occ: float, depth: int, tier: str = "A") -> SlotSnapshot:
return SlotSnapshot(loc, gross, 0.10, occ, depth, tier, sku_id="SKU-" + loc)
def test_utilization_bounded() -> None:
slots = [_lane("L1", 100.0, 5.0, 1), _lane("L2", 100.0, 500.0, 1)]
assert all(0.0 <= cube_utilization(s) <= 1.0 for s in slots)
def test_single_deep_never_honeycombs() -> None:
shallow = _lane("L3", 100.0, 5.0, depth=1) # 5% full but single-deep
assert detect_honeycombing(shallow).severity == "none"
def test_deep_lane_flags_severe() -> None:
deep = _lane("L4", 100.0, 20.0, depth=4) # 22% of usable cube
assert detect_honeycombing(deep).severity == "severe"
def test_rollup_is_cube_weighted() -> None:
# 90 usable @ full + 90 usable @ empty -> 50% weighted, not the 50% simple mean by luck
slots = [_lane("A", 100.0, 90.0, 1), _lane("B", 100.0, 0.0, 1)]
out = roll_up(slots)
assert abs(out.cube_weighted_utilization - 0.5) < 1e-9
assert out.under_utilized_pct == 0.5
A sample healthy run logs Facility index: U=0.712, honeycomb=1840 ft3, under=18%, over=6%. A cube-weighted utilization in the high-0.60s to low-0.70s with honeycomb trending down week over week is the target; an index above 0.90 is not “efficient” but a warning that you have no surge capacity left.
Integration Points
The utilization index is a diagnostic input to two sibling systems, and it is authoritative in neither — the scorer measures, the downstream layers decide.
- Re-slotting thresholds. A low utilization score or a severe honeycomb finding is a candidate for a move, never a command to execute one. Whether reclaiming that cube is worth the relocation labor is decided by the break-even math in Threshold Optimization for Re-slotting: a 40-cubic-foot honeycomb void deep in slow-moving reserve rarely repays the move, while the same void on a golden-zone A-face almost always does. The scorer hands that layer
wasted_cube_ft3andrecommend_action; the economics gate the trigger. - The travel-cost model. Utilization and travel cost are coupled. Compressing under-filled slots pulls active inventory into fewer, denser faces and shortens pick paths, so a right-sizing pass changes the distance matrix that Travel-Distance & Pick-Path Cost Modeling consumes. Feed the post-consolidation location map back into that model rather than scoring travel against a stale layout, or you will credit savings the floor never realizes.
Denser slotting also concentrates picker traffic, which is exactly where utilization work can backfire — squeezing everything into the shortest aisles raises the odds of queueing. Pair a consolidation pass with the load-spreading logic in Congestion Modeling & Zone Routing so you trade honeycomb void for density without minting a new hotspot. Upstream, the velocity tier every recommendation depends on is produced by the Location Assignment & ABC Classification Algorithms layer; a mis-tiered SKU sends the quadrant the wrong action.
Failure Modes & Edge Cases
- Occupancy mistaken for utilization. Reporting “97% of locations occupied” as a capacity metric hides catastrophic cube loss. Remediation: publish the cube-weighted index as the headline number and demote slot-occupancy to a secondary count.
- Clearance folded into gross cube. If
gross_cube_ft3includes flue and beam clearance the location cannot fill, every score reads high and honeycombing vanishes from the report. Remediation: measure gross to the usable envelope and carryclearance_factorexplicitly; assert0 ≤ clearance_factor < 1at load. - Honeycomb flagged on shareable slots. Grading a half-empty single-deep bin as honeycombing generates phantom right-sizing tasks. Remediation: gate detection on
lane_depth >= min_lane_depth; shared locations are available, not wasted. - Simple-mean roll-up. Averaging location scores lets a swarm of tiny empty bins mask a packed reserve. Remediation: weight by usable cube so the index reflects where volume actually sits.
- Chasing every under-filled slot. Right-sizing a deep C-mover lane for 30 reclaimed cubic feet can cost more labor than it saves. Remediation: route every finding through the re-slotting break-even and act on the severe, high-velocity tail first.
FAQ
What is the difference between cube utilization and floor utilization?
Floor utilization measures the two-dimensional footprint consumed — square feet under rack or the fraction of pick faces assigned — and ignores height entirely. Cube utilization divides occupied product volume by usable location volume, so it captures the vertical space above short cartons and the void behind depleted deep lanes. A facility can post 90% floor utilization while running below 60% cube utilization; the cube number is the one that predicts when you genuinely run out of room, so treat floor and slot-occupancy as secondary and headline the cube-weighted index.
How is honeycombing loss different from ordinary low fill?
Ordinary low fill is a shareable location that simply has room for more — the next putaway can co-mingle into it, so it is available capacity, not waste. Honeycombing is committed-but-empty cube in a dedicated or deep location that cannot accept anything else until the assigned SKU fully depletes. You cannot fix honeycombing by adding stock, because nothing else is allowed in; you fix it by right-sizing the storage medium (a shallower lane, a smaller slot) or changing the slotting rule. That is why the detector gates on lane_depth and reports reclaimable wasted_cube_ft3 separately from the fill score.
What is a good target cube utilization?
For active pick faces, a healthy band is roughly 0.55 to 0.90. Below the low edge you are carrying consolidation candidates; above the high edge you have squeezed out the handling headroom pickers need to work a face without restacking, and you have no surge capacity for a demand spike. Reserve and bulk storage can run tighter, toward 0.90–0.95, because they are replenishment buffers, not work faces. The exact edges are a policy choice tied to how much surge you plan for — set them in the config and revisit them seasonally, not per shift.
Should a low utilization score automatically trigger a re-slot?
No. The scorer produces evidence, not decisions. A low score or a honeycomb finding is a candidate that must clear the relocation break-even in the re-slotting layer before any move is emitted — reclaiming 40 cubic feet deep in slow reserve rarely repays the labor, while the same void on a golden-zone A-face usually does. Keeping measurement and economics separate is what stops utilization scoring from generating a churn of low-value moves that cost more than the cube they recover.
How often should utilization be scored?
Weekly is the working default for the facility index, because utilization drifts far faster than velocity tiers — a single large receipt or a promotional draw-down reshapes fill within days. Score against a fresh point-in-time snapshot rather than a rolling average so the number reflects the layout as it stands now. Trend the weekly index and the honeycomb total; a rising index with falling honeycomb is a healthy consolidation, while a rising index with flat honeycomb usually means you are simply filling up and should plan capacity.
Related
- How to Score Cube Utilization Per Location — the single-location scorer with the packing-efficiency refinement, worked end to end.
- Travel-Distance & Pick-Path Cost Modeling — the distance model that a consolidation pass reshapes; rescore travel against the new layout.
- Congestion Modeling & Zone Routing — spread the picker load a denser layout concentrates so consolidation does not mint a hotspot.
- Threshold Optimization for Re-slotting — the move-cost break-even that decides whether a reclaimable void is worth acting on.
- Pick-Path Optimization & Space Utilization — the parent architecture this scoring layer feeds.