How to Assign Heavy SKUs to Ground-Level Slots
A 900 kg drum of adhesive does not belong at 78 inches, and no picker should ever lift it there — but a velocity-only optimizer does not know that. Heavy and bulky SKUs need to be routed to the lowest rack levels, both for the safety of the person retrieving them and for heavy-on-bottom load stacking, where the center of gravity of the whole bay stays low and a top level never carries more than it is rated for. This page shows you how to enforce a weight-by-level ceiling — a monotonically decreasing cap as you climb the rack — filter the feasible ground slots for a given SKU, and reconcile the result with velocity, because a heavy A-mover still cannot go overhead just because it is picked often. It is the ground-level-placement case of the Weight & Volume Constraint Modeling cluster inside the wider Location Assignment & ABC Classification Algorithms system.
The weight-by-level ceiling is the mechanical complement to the ergonomic scoring in How to Model Golden-Zone Ergonomic Limits: ergonomics asks whether a human can safely reach and lift at a height; this rule asks whether the rack level can carry the mass and whether placing it low keeps the stack stable. When a SKU is both heavy and fast, the two rules collide, and the reconciliation is always the same — safety wins, and the fast-but-heavy item takes the best ground slot rather than the best slot overall. Which ground slot it earns, and how full the forward zone already is, ties directly into Space & Cube Utilization Scoring.
Prerequisites
Confirm each of these before running the ranker against a live location set:
- Python 3.10+ — the implementation uses
list[...]generics,X | Noneunions, and adataclassfor candidate slots. - A rack-level index per location — every candidate slot must carry an integer level (
0= ground) and its pick-face height. The level topology comes from Location Hierarchy Mapping. - A per-level weight ceiling table — the maximum resting mass each level may carry, traced to the rack engineering certificate and derated the same way the Weight & Volume Constraint Modeling cluster derates beam ratings.
- Normalized SKU gross weight in kilograms — including packaging, from the ingestion frame.
- A velocity tier per SKU — so ties among feasible ground slots break toward the shortest travel for faster movers, using the tiers from ABC Classification Tuning.
Configuration Block
Every tunable lives in one externalized profile. The two levers that decide behavior are max_weight_kg_by_level (the ceiling that grows as you descend) and ground_levels (which levels count as “ground” for a heavy item that must stay low even when a higher level could technically bear it).
# heavy_placement.yaml — weight-by-level tunables, one profile per rack type
heavy_placement:
max_weight_kg_by_level: # ceiling per level index; must be non-increasing with height
0: 1200.0 # ground / floor position
1: 600.0
2: 250.0
3: 120.0
ground_levels: [0, 1] # levels a heavy SKU is allowed to occupy
heavy_threshold_kg: 250.0 # at/above this mass a SKU is "heavy" and floor-preferred
bulky_threshold_m3: 1.5 # oversized cube is also floor-preferred, independent of mass
safety_factor: 1.5 # derate applied to each raw level rating
# Equivalent Python config dict consumed by HeavyPlacementConfig
HEAVY_PLACEMENT = {
"max_weight_kg_by_level": {0: 1200.0, 1: 600.0, 2: 250.0, 3: 120.0},
"ground_levels": [0, 1],
"heavy_threshold_kg": 250.0,
"bulky_threshold_m3": 1.5,
"safety_factor": 1.5,
}
Implementation
The ranker takes a SKU and a list of candidate slots and returns the feasible ones, best first. Feasibility is two gates: the level’s derated ceiling must cover the SKU mass, and a heavy or bulky SKU is additionally confined to ground_levels. Among feasible slots it ranks by travel cost so a fast heavy mover still gets the closest legal ground bin. The function logs the moment a heavy SKU is confined and when nothing feasible remains.
from __future__ import annotations
import logging
from dataclasses import dataclass
logger = logging.getLogger("slotting.heavy")
@dataclass(frozen=True)
class HeavyPlacementConfig:
max_weight_kg_by_level: dict[int, float]
ground_levels: tuple[int, ...] = (0, 1)
heavy_threshold_kg: float = 250.0
bulky_threshold_m3: float = 1.5
safety_factor: float = 1.5
@dataclass(frozen=True)
class Slot:
location_id: str
level: int
height_in: float
travel_cost: float
def rank_ground_slots(
sku_id: str,
weight_kg: float,
volume_m3: float,
slots: list[Slot],
cfg: HeavyPlacementConfig,
) -> list[Slot]:
"""Return feasible slots for a (possibly heavy) SKU, cheapest travel first.
A slot is feasible when its derated level ceiling covers the SKU mass. A SKU
at or above heavy_threshold_kg (or bulky by cube) is additionally confined to
ground_levels, so a heavy fast-mover can never be ranked into an upper level.
"""
is_heavy = (
weight_kg >= cfg.heavy_threshold_kg or volume_m3 >= cfg.bulky_threshold_m3
)
if is_heavy:
logger.info(
"SKU %s heavy/bulky (%.0fkg, %.2fm3) - confined to levels %s",
sku_id, weight_kg, volume_m3, list(cfg.ground_levels),
)
feasible: list[Slot] = []
for slot in slots:
ceiling = cfg.max_weight_kg_by_level.get(slot.level)
if ceiling is None:
continue
eff_ceiling = ceiling / cfg.safety_factor
if weight_kg > eff_ceiling:
continue # level cannot bear the mass
if is_heavy and slot.level not in cfg.ground_levels:
continue # heavy item barred from height
feasible.append(slot)
feasible.sort(key=lambda s: (s.level, s.travel_cost))
if not feasible:
logger.warning(
"SKU %s (%.0fkg) has NO feasible ground slot - escalate to bulk zone",
sku_id, weight_kg,
)
else:
logger.info(
"SKU %s: %d feasible slots, best=%s (level %d)",
sku_id, len(feasible), feasible[0].location_id, feasible[0].level,
)
return feasible
Step-by-Step Walkthrough
- Classify heavy or bulky first. A SKU is floor-preferred if its mass clears
heavy_threshold_kgor its cube clearsbulky_threshold_m3. Bulk matters independently of weight — a large light carton still destabilizes an upper level and blocks reach — so the check is anor, not anand. - Derate every level ceiling. Each
max_weight_kg_by_levelentry is divided bysafety_factorbefore comparison, so the ranker enforces the same margin the beam-rating derate uses. A raw 1200 kg ground rating becomes an 800 kg effective ceiling at a 1.5 factor. - Gate on mass, then on height. A slot survives only if its derated ceiling covers the SKU mass and — for a heavy SKU — the level is in
ground_levels. The two gates are independent: a light SKU may sit anywhere its level can bear, but a heavy one is confined even when an upper level is technically rated for it. - Rank by
(level, travel_cost). The sort key puts the lowest level first, then the shortest travel within that level. This is the reconciliation with velocity: among legal ground slots the fast mover still wins the closest bin, but it can never jump a level to shave travel. - Escalate an empty result. If nothing is feasible, the SKU is heavier than any ground level’s derated ceiling — a real signal to route it to a floor-block or bulk-storage zone, logged at
WARNINGrather than silently dropped.
Verification
Assert the invariants directly — a heavy SKU never ranks into an upper level, the ground slot is preferred, and an over-ceiling SKU returns empty. These checks run without a WMS.
import logging
logging.basicConfig(level=logging.INFO)
cfg = HeavyPlacementConfig(
max_weight_kg_by_level={0: 1200.0, 1: 600.0, 2: 250.0, 3: 120.0},
)
slots = [
Slot("A-01-0", level=0, height_in=8.0, travel_cost=12.0),
Slot("A-01-2", level=2, height_in=54.0, travel_cost=3.0), # closer but high
]
# A 700 kg SKU: only level 0 bears it (600/1.5=400 at L1), and it is confined low.
ranked = rank_ground_slots("DRUM", 700.0, 0.9, slots, cfg)
assert [s.location_id for s in ranked] == ["A-01-0"]
# A 3000 kg SKU exceeds even the ground derated ceiling -> escalate.
assert rank_ground_slots("SLAB", 3000.0, 2.0, slots, cfg) == []
print("OK - heavy SKU stays on the ground, over-ceiling SKU escalates")
Sample expected output:
INFO:slotting.heavy:SKU DRUM heavy/bulky (700kg, 0.90m3) - confined to levels [0, 1]
INFO:slotting.heavy:SKU DRUM: 1 feasible slots, best=A-01-0 (level 0)
INFO:slotting.heavy:SKU SLAB heavy/bulky (3000kg, 2.00m3) - confined to levels [0, 1]
WARNING:slotting.heavy:SKU SLAB (3000kg) has NO feasible ground slot - escalate to bulk zone
OK - heavy SKU stays on the ground, over-ceiling SKU escalates
Common Pitfalls
- Ignoring the crush limit under mixed-weight stacking. A ground level rated 1200 kg does not mean any single carton can bear that — when heavy cases stack, the bottom carton carries everything above it. Enforce a per-column crush ceiling set by the weakest carton’s compression rating, separate from the level’s structural ceiling, or the rack survives while the product is crushed.
- A non-monotonic ceiling table.
max_weight_kg_by_levelmust be non-increasing as the level rises. If a data-entry slip rates level 2 higher than level 1, the ranker will happily send mass upward and defeat the whole heavy-on-bottom intent. Assert monotonicity when you load the config. - Blocking ground slots with slow heavy items. A slow-moving 400 kg SKU parked in your closest ground bin evicts a fast heavy mover to a distant one. Ground capacity is scarce; reconcile with velocity so the nearest ground slots go to the heavy items that are also picked often, and cascade slow-and-heavy stock to back-of-house ground positions. The trade-off between ground occupancy and pick travel is exactly what Space & Cube Utilization Scoring quantifies.
- Confusing “rated for it” with “safe to lift there”. A level can be structurally rated for a mass a human should never manually retrieve at that height. Pair this ceiling with the ergonomic score from How to Model Golden-Zone Ergonomic Limits so structural feasibility never overrides manual-handling safety.
Related
- Weight & Volume Constraint Modeling — the parent feasibility layer that derates ratings and turns these ground rules into hard solver constraints.
- How to Model Golden-Zone Ergonomic Limits — the sibling ergonomic score that decides safe lift height once a level can bear the mass.
- Space & Cube Utilization Scoring — quantifies the scarce ground-slot occupancy this rule competes for.
- Location Assignment & ABC Classification Algorithms — the parent architecture this ground-placement rule feeds.