Location Hierarchy Mapping for Velocity-Driven Slotting
Location hierarchy mapping is the coordinate system every other slotting decision is measured against: it translates the physical facility into a machine-addressable graph so that a velocity tier can be resolved to an exact bin, and a bin can be resolved back to its travel cost, capacity, and equipment requirements. This guide is part of the Core Slotting Architecture & Velocity Taxonomies system, where it sits between the scoring engine that ranks SKUs and the optimizer that commits them to slots — without a rigorous hierarchy, velocity-driven placement degrades into arbitrary assignment, inflated travel, and orphaned inventory after every layout change.
What Location Hierarchy Mapping Is
A location hierarchy is a strictly parent-child model of storage positions, encoded as a directed acyclic graph (DAG) in which each node is a physical tier and each edge is a containment relationship. The canonical schema for discrete pick-and-pack facilities is six tiers: Site → Functional Zone → Aisle → Bay → Level → Bin. Every node carries three classes of metadata: an immutable identifier (never reused, even after a bin is decommissioned), dimensional and capacity constraints (weight, cube, height clearance, temperature band), and a derived travel-cost attribute that the optimizer reads at assignment time.
The DAG framing matters because it guarantees two properties the optimizer depends on. First, every bin resolves to exactly one path back to the site root, so a composite location key is unambiguous. Second, constraints propagate downward — a temperature band declared at the zone tier is inherited by every bay, level, and bin beneath it — which lets the constraint filter reject an incompatible sub-tree in one comparison instead of walking thousands of leaf nodes.
Two industry variants deviate from the six-tier default and must be modeled explicitly rather than forced into it:
- Cross-dock sub-hierarchies collapse the reserve tiers entirely, mapping inbound staging doors directly to outbound sortation lanes so freight flows without a put-away cycle. The graph still terminates in addressable positions, but the “bin” is a time-boxed staging slot rather than a permanent rack location.
- Multi-temperature hierarchies introduce environmental boundaries as hard edges: ambient, chilled, and frozen sub-trees never share a parent below the zone tier, so no velocity rule can ever place a frozen SKU in an ambient bin regardless of how attractive its travel cost looks.
Input Data Requirements
The hierarchy is assembled from three feeds — CAD/rack-layout exports, the WMS location master, and any IoT or slotting-audit sensor data — normalized into one record per addressable position. Enforce these preconditions before a single node enters the graph:
| Field | Type | Constraint / precondition |
|---|---|---|
site_id |
str |
Non-null; matches the facility registry |
zone_id |
str |
Non-null; resolves to a declared functional zone |
aisle_id |
str |
Non-null; unique within zone |
bay_id |
str |
Non-null; unique within aisle |
level_id |
int |
>= 0; ground level is 0 |
bin_id |
str |
Non-null; unique within level |
max_weight_kg |
float |
> 0; sourced from rack rating, not guessed |
max_volume_m3 |
float |
> 0; usable cube after honeycombing allowance |
temperature_band |
str | None |
One of ambient / chilled / frozen; inherited from zone if null |
is_active |
bool |
False bins are excluded from candidate generation |
Two quality gates catch the errors that silently corrupt a hierarchy: referential integrity (every child’s parent key must exist in the tier above it, or the record is an orphan) and uniqueness of the composite key (site-zone-aisle-bay-level-bin must be unique, or two physical bins will collide on one address). Feed shape and contract enforcement upstream of this stage belong to Schema Validation for Inventory Feeds; this layer assumes fields arrive typed and simply validates the topology.
Step-by-Step Implementation
The pipeline builds a validated registry, materializes the DAG, computes a travel-cost attribute per node, and emits a velocity-weighted candidate ranking ready for the optimizer. Each stage below is idempotent so a failed run replays against the same input window without corrupting the location ledger.
1. Validate and Normalize Location Records
Model each position with a typed schema so malformed rows are rejected — and logged — rather than propagated. A level_id that arrives as a string, or a max_weight_kg of zero, is a data defect that must never reach the optimizer.
from __future__ import annotations
import logging
from typing import Optional
import pandas as pd
from pydantic import BaseModel, Field, ValidationError
logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
logger = logging.getLogger("slotting.hierarchy")
class LocationNode(BaseModel):
site_id: str
zone_id: str
aisle_id: str
bay_id: str
level_id: int = Field(ge=0)
bin_id: str
max_weight_kg: float = Field(gt=0)
max_volume_m3: float = Field(gt=0)
temperature_band: Optional[str] = None
is_active: bool = True
@property
def composite_key(self) -> str:
return f"{self.site_id}-{self.zone_id}-{self.aisle_id}-{self.bay_id}-{self.level_id}-{self.bin_id}"
def validate_locations(locations_df: pd.DataFrame) -> list[LocationNode]:
"""Coerce a raw location export into validated nodes, dropping and logging bad rows."""
nodes: list[LocationNode] = []
rejected = 0
for _, row in locations_df.iterrows():
try:
nodes.append(LocationNode(**row.to_dict()))
except ValidationError as exc:
rejected += 1
logger.warning("rejected location row %s: %s", row.get("bin_id", "?"), exc.error_count())
logger.info("validated %d locations, rejected %d", len(nodes), rejected)
return nodes
2. Assemble the Hierarchy and Enforce Referential Integrity
Before scoring anything, prove the topology is sound: every composite key is unique and every child has a live parent. Orphan detection here is what prevents “phantom bins” surviving a racking retrofit or aisle reprofile.
from collections import defaultdict
def build_hierarchy(nodes: list[LocationNode]) -> dict[str, list[LocationNode]]:
"""Group validated nodes by their aisle parent and fail fast on duplicate keys."""
seen: set[str] = set()
by_aisle: dict[str, list[LocationNode]] = defaultdict(list)
for node in nodes:
key = node.composite_key
if key in seen:
raise ValueError(f"duplicate composite key collides on one address: {key}")
seen.add(key)
aisle_key = f"{node.site_id}-{node.zone_id}-{node.aisle_id}"
by_aisle[aisle_key].append(node)
logger.info("assembled %d aisles across %d bins", len(by_aisle), len(seen))
return by_aisle
3. Attach Velocity and Compute a Slotting Priority
A hierarchy earns its keep only when coupled to inventory velocity. Join the decayed velocity score produced by the SKU Velocity Taxonomy Design layer onto candidate bins, then rank each bin by the ratio of velocity to travel friction so high-velocity SKUs anchor to low-friction positions.
def rank_candidates(
nodes: list[LocationNode],
velocity_scores: dict[str, float],
) -> pd.DataFrame:
"""Merge velocity onto active bins and rank by velocity-per-friction (higher is better)."""
rows = []
aisle_codes = {a: i for i, a in enumerate(sorted({n.aisle_id for n in nodes}))}
for node in nodes:
if not node.is_active:
continue
# travel_friction: aisle depth + a level penalty (higher levels need lift equipment)
travel_friction = aisle_codes[node.aisle_id] + (node.level_id * 2)
velocity = velocity_scores.get(node.composite_key, 0.0)
rows.append({
"composite_key": node.composite_key,
"zone_id": node.zone_id,
"temperature_band": node.temperature_band,
"velocity_score": velocity,
"travel_friction": travel_friction,
"slotting_priority": velocity / (travel_friction + 1),
})
ranked = pd.DataFrame(rows).sort_values("slotting_priority", ascending=False).reset_index(drop=True)
logger.info("ranked %d active candidate bins", len(ranked))
return ranked
4. Emit the Composite Assignment Cost
The optimizer treats placement as a constrained bin-packing problem with a distance-minimization objective. Rather than call an external solver, expose a single composite cost per candidate so the assignment engine — and the pre-slot simulation in Pick Path Modeling Frameworks — reads one comparable number. The weighted form is:
cost = α · travel_distance + β · replenishment_frequency + γ · pick_path_interference
def composite_cost(
ranked: pd.DataFrame,
replenishment_freq: dict[str, float],
interference: dict[str, float],
alpha: float = 1.0,
beta: float = 0.4,
gamma: float = 0.6,
) -> pd.DataFrame:
"""Blend travel, replenishment, and congestion into one cost (lower is better)."""
df = ranked.copy()
df["replenishment_freq"] = df["composite_key"].map(replenishment_freq).fillna(0.0)
df["interference"] = df["composite_key"].map(interference).fillna(0.0)
df["assignment_cost"] = (
alpha * df["travel_friction"]
+ beta * df["replenishment_freq"]
+ gamma * df["interference"]
)
logger.info("computed assignment cost for %d candidates", len(df))
return df.sort_values("assignment_cost").reset_index(drop=True)
Tuning & Calibration
The behavior of the mapping is governed by a small set of tunables: the cost weights α/β/γ, the per-level friction penalty, and the recalculation cadence that re-indexes the DAG after structural changes. Externalize them so a facility can recalibrate without a redeploy. The weights are facility-specific — a high-throughput e-commerce site with short replenishment cycles weights travel (α) hardest, while a bulk-storage site prone to congestion at cross-aisles raises interference (γ).
hierarchy:
level_penalty: 2.0 # friction added per rack level above ground (0)
cost_weights:
alpha: 1.0 # travel distance weight
beta: 0.4 # replenishment frequency weight
gamma: 0.6 # pick-path interference weight
reindex_cron: "0 3 * * *" # nightly DAG reconciliation, 03:00 local
orphan_tolerance: 0 # abort publish if any orphan bin is detected
golden_zone_levels: [0, 1] # levels treated as low-friction "golden" positions
HIERARCHY_CONFIG = {
"level_penalty": 2.0,
"cost_weights": {"alpha": 1.0, "beta": 0.4, "gamma": 0.6},
"reindex_cron": "0 3 * * *",
"orphan_tolerance": 0,
"golden_zone_levels": [0, 1],
}
| Parameter | Default | Effect of increasing | Recalibrate when |
|---|---|---|---|
level_penalty |
2.0 |
Pushes fast movers to lower levels; empties top racks | Lift-equipment availability or ergonomics targets change |
cost_weights.alpha |
1.0 |
Prioritizes short travel over everything else | Pick-walk distance dominates labor cost |
cost_weights.gamma |
0.6 |
Avoids congested aisles even at longer travel | Peak-wave choke points appear in path modeling |
reindex_cron |
nightly | Fresher topology, more compute per cycle | Frequent racking retrofits or aisle reprofiles |
orphan_tolerance |
0 |
Allows publishing with dangling bins (risky) | Never raise above 0 in production |
Weight and cube edge cases — heavy SKUs that a rack level cannot physically bear regardless of velocity — are not tuned here; they are resolved by Weight & Volume Constraint Modeling, which this layer treats as a hard pre-filter before priority ranking runs.
Validation & Testing
Topology correctness is non-negotiable, so gate the pipeline with assert-based checks that run on every build. These verify uniqueness, orphan-freedom, and that the ranking honors the friction model.
def test_composite_keys_are_unique() -> None:
nodes = validate_locations(_sample_frame())
keys = [n.composite_key for n in nodes]
assert len(keys) == len(set(keys)), "composite keys must be globally unique"
def test_no_orphan_bins() -> None:
nodes = validate_locations(_sample_frame())
by_aisle = build_hierarchy(nodes) # raises ValueError on duplicate/collision
assert all(len(bins) > 0 for bins in by_aisle.values()), "every aisle must hold >=1 bin"
def test_lower_friction_bins_rank_higher() -> None:
nodes = validate_locations(_sample_frame())
scores = {n.composite_key: 100.0 for n in nodes} # equal velocity isolates friction
ranked = rank_candidates(nodes, scores)
top, bottom = ranked.iloc[0], ranked.iloc[-1]
assert top["travel_friction"] <= bottom["travel_friction"], "top rank must be lowest friction"
def test_inactive_bins_excluded() -> None:
nodes = validate_locations(_sample_frame())
ranked = rank_candidates(nodes, {})
assert "inactive" not in ranked["composite_key"].str.lower().tolist()
Run these in CI against synthetic warehouse topologies before any change to the cost model reaches production. A regression that reorders the ranking is a slotting-thrash event waiting to happen.
Integration Points
Location hierarchy mapping is a hub the rest of the architecture reads from and writes back to:
- Upstream — velocity tiers. The SKU Velocity Taxonomy Design layer supplies the decayed
velocity_scorejoined onto each bin in step 3; the hierarchy contributes thetravel_frictionthat turns a raw score into a placement priority. - Sibling — aisle-to-zone translation. The concrete rules for grouping physical aisles into the functional zones this DAG branches on live in Mapping Warehouse Aisles to Logical Zones, the detailed companion to this overview.
- Downstream — path simulation. The
assignment_costfrom step 4 feeds directly into Pick Path Modeling Frameworks, which replays historical order profiles against a candidate layout to produce a projected travel-time delta before any move is committed. - Governance — who may mutate the graph. Hierarchy edits (deactivating a bin, re-rating a rack, reprofiling an aisle) are privileged operations. Security & Access Boundaries for Slotting enforces role-based access so a mistuned job cannot silently rewrite the location ledger.
Failure Modes & Edge Cases
- Orphaned bins after a layout change. A racking retrofit deactivates a bay but leaves its child bins keyed to a now-missing parent. Remediation: run
build_hierarchywithorphan_tolerance: 0in the nightly reindex and refuse to publish candidates until the topology reconciles. - Composite-key collision. Two physical bins normalize to the same
site-zone-aisle-bay-level-binstring after an aisle renumbering, so one silently shadows the other. Remediation: the uniqueness assert fails the build; never merge feeds without re-validating keys. - Stale travel friction. Aisle codes are cached from an old layout, so the friction attribute no longer matches the physical walk. Remediation: recompute
aisle_codeson every reindex, not once at bootstrap, and alert when the aisle set changes. - Temperature-band leakage. A null
temperature_bandat the bin tier fails to inherit its zone’s band, letting a chilled SKU rank against an ambient slot. Remediation: resolve inheritance during validation and hard-fail any bin whose band cannot be determined. - Level penalty masking a hard limit. A high-velocity SKU earns a low-friction ground slot that cannot bear its weight; the friction model ranked it but never checked capacity. Remediation: apply Weight & Volume Constraint Modeling as a pre-filter so infeasible bins never enter the ranking.
FAQ
How many tiers should a location hierarchy have?
Six (Site → Zone → Aisle → Bay → Level → Bin) covers most discrete pick-and-pack facilities. Add tiers only when a real containment relationship exists — for example, a sub-bin tier for divided carton-flow lanes. Cross-dock and bulk-storage operations often use fewer, collapsing the reserve tiers; model the actual containment, never a tier that carries no constraint.
Should the hierarchy be a tree or a graph?
Model it as a directed acyclic graph that happens to be a tree for containment, so every bin has exactly one parent path. Keep valid traversal edges (aisle-to-aisle adjacency for path cost) in a separate adjacency structure — do not overload the containment DAG with routing edges, or reindexing one corrupts the other.
How often should the DAG be re-indexed?
Nightly reconciliation against WMS transaction logs is the baseline (reindex_cron: "0 3 * * *"). Trigger an immediate out-of-band reindex on any structural event — aisle reprofile, racking retrofit, bin decommission — and gate directive publishing on a clean, orphan-free topology.
How do I stop dead bins from breaking assignments after a retrofit?
Set is_active: False on decommissioned positions rather than deleting them, keep the immutable identifier reserved, and exclude inactive bins at candidate generation (step 3). During a retrofit, temporarily decouple velocity constraints from physical availability until the DAG re-indexes cleanly, then resume.
Related
- Mapping Warehouse Aisles to Logical Zones — the concrete aisle-to-zone translation beneath this DAG.
- SKU Velocity Taxonomy Design — the scoring layer that supplies each bin’s velocity score.
- Pick Path Modeling Frameworks — consumes the assignment cost to simulate travel before committing a re-slot.
- Weight & Volume Constraint Modeling — the hard capacity pre-filter that runs before priority ranking.
- Core Slotting Architecture & Velocity Taxonomies — the parent architecture this mapping anchors.