Slotting Architecture · 19 min read

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.

Utilization-versus-velocity action quadrant A two-by-two decision grid. The horizontal axis is location cube utilization, increasing left to right, split at a target band of 0.55 to 0.90; left of the band is under-utilized, right is over-utilized. The vertical axis is SKU velocity tier, with A and B tiers at the top and C tier at the bottom. Top-left, an under-filled fast mover is a honeycombing risk that should be right-sized to a smaller slot. Top-right, a well-slotted A-mover sits at capacity and should be watched for overflow or split across faces. Bottom-left, an under-filled slow mover should be consolidated to free the slot for reserve. Bottom-right, an over-packed slow mover is on the wrong real estate and should be downsized or pushed to bulk reserve. Utilization score interacts with velocity tier target band 0.55–0.90 Under-filled fast mover honeycombing risk — right-size to a smaller slot Well-slotted A-mover at capacity — watch for overflow / split face Under-filled slow mover consolidate — free the slot for reserve Over-packed slow mover wrong real estate — downsize / push to bulk reserve Location cube utilization → under-utilized over-utilized A / B tier C tier
The same utilization number means opposite things depending on velocity: an under-filled A-mover is a honeycombing signal to right-size down, while an over-packed C-mover is squatting on prime cube it should surrender. The scorer produces the axis value; velocity tier decides which quadrant — and which action — applies.

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_ft3 and recommend_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_ft3 includes 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 carry clearance_factor explicitly; assert 0 ≤ clearance_factor < 1 at 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.