Skip to content

abedford37/home-assignment-engine

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Home Assignment Engine

Decides where every item lives. Active items get a primary home building and ranked fallback homes across the forward network, placed to minimize demand-weighted fulfillment cost while respecting each building's cube capacity. Dead and slow items, the ones with little or no demand, are pulled out of that competition and parked in storage with no policy, so scarce forward space is fought over only by items that actually move. This is the slotting layer of the network system.

It sits between the policy engine and the routing engines. The policy engine sizes each active item's footprint from its OPHQ; this engine places that footprint optimally; and the container and transfer engines then route inbound to those homes and repatriate misplaced stock back to them.

A note on what this is not. The container engine assigned containers to buildings to maximize demand coverage, a daily service decision. This engine assigns items to buildings to minimize cost under space, a strategic slotting decision. Different question, different objective, different math.


Contents

  1. The decision and the pipeline role
  2. The cost model
  3. The assignment heuristic
  4. Evaluation and results
  5. Limitations
  6. Future work
  7. Using the engine
  8. Project structure
  9. References

1. The decision and the pipeline role

Every item needs a home building: the place its stock lives and ships from. The home determines fulfillment cost, because a building that is far from an item's demand or expensive to pick from costs more on every order. Buildings are finite, so not every item can live in its ideal home. The problem is to place the whole catalog at least total cost without overfilling any building.

Formally, for the active items this is a generalized assignment problem: assign each active item to exactly one forward building, respect each building's cube capacity, minimize total cost. It is NP-hard, so the engine uses a strong heuristic rather than claiming an exact optimum, and brackets its answer against a provable lower bound so the gap is visible.

Inputs come from upstream engines: the demand rate and its geography from the demand side, and the OPHQ footprint from the policy engine. The output, a home plus two fallback homes per item, is what the container and transfer engines route against.

The movement gate

Before any optimization, items are classified by movement, using the demand engine's own scheme rather than a reinvented one. The demand engine places each item in a Syntetos-Boylan quadrant from how often it sells (average demand interval) and how variable its sale sizes are (squared coefficient of variation), with the standard cuts at 1.32 and 0.49: smooth, erratic, intermittent, or lumpy. It also draws a distinction this engine depends on, between an item that has never sold and one that sold before but not lately. Both show zero recent demand, but they are not the same thing.

The gate maps that classification to a placement:

  • active, real ongoing demand, goes to a forward home with a full policy.
  • new, never sold and so no history yet, goes to a forward home on a provisional cold-start policy, because a brand-new item is expected to move, not sit dead.
  • slow, sells rarely and in low volume (intermittent or lumpy under a volume cut), goes to storage with no policy.
  • dead, has history but no recent movement, goes to storage with no policy.

Active and new items compete for forward cube. Slow and dead items are parked in storage. The key correction over a naive demand cutoff is that a never-sold item is new, not dead: it earns a provisional forward slot rather than being garaged, and only genuinely dead stock goes to storage. The volume cuts that separate dead, slow, and active are adjustable per operation. Removing the non-moving tail also sharpens the active placement, since the optimizer is no longer distracted by items that do not move.


2. The cost model

Homing item i in building b costs

cost[i, b] = demand[i] * handling[b] + demand[i] * expected_ship_distance[i, b]

The first term is internal: a busier or less automated building has a higher handling rate per unit picked. The second term is external: each item's demand is spread across regions, and the expected ship distance is that demand-share-weighted distance from the building to those regions. Homing an item near its demand, in an efficient building, is cheap. Distances are Euclidean on a campus plane here; a real deployment would substitute actual lane rates without changing anything else.

Because the cost scales with demand, a high-velocity item shows a large cost difference between a good and a bad home. That difference is what the engine prioritizes, so velocity-based slotting is not a separate rule, it falls out of the objective.


3. The assignment heuristic

The engine starts from a regret-ordered greedy pass. An item's regret is how much worse its second-best building is than its best, so items with high regret are the ones that would lose the most if displaced. Placing them first hands the scarce prime capacity to the items that value it most. Each item takes its cheapest building with room; if its best is full it falls to its next-best, and so on.

Local search then improves the start. It repeatedly applies two capacity-respecting moves: shifting a single item to a cheaper building that has room, and swapping two items between buildings when the swap lowers total cost and both still fit. It runs until no move helps.

Fallback ranks are the next-best buildings by cost, excluding the assigned home. They are contingency targets, where to look when the primary cannot serve, which is what the transfer engine's repatriation and the container engine's diversion both need.


4. Evaluation and results

The engine is bracketed between a floor and a feasible baseline. The unconstrained floor puts every item in its cheapest building ignoring capacity: the least cost any assignment could achieve, but it overloads buildings, so it is not a legal plan. The balanced baseline is cost-blind, placing each item where there is the most room: legal but ignorant of demand geography. A good engine sits just above the floor while staying feasible, and well below the baseline.

Results on the synthetic instance (200 items, of which about 169 are forward-eligible, 20 of those brand-new, and 31 are dead or slow, four forward buildings, seven demand regions, forward footprint at about 85 percent of forward capacity). Illustrative, from the included generator:

plan total cost gap to floor feasible
unconstrained floor 5577 0.0% no
engine 5615 0.7% yes
balanced baseline 9162 64.3% yes

The 31 dead and slow items go straight to storage with no policy and never touch forward cube, while the 20 never-sold items get provisional forward homes rather than being mistaken for dead stock. Among the forward-eligible items, the engine lands 0.7 percent above a floor that is itself infeasible, so the gap to the best legal plan is smaller still, while the cost-blind baseline costs 64 percent more. About 90 percent of forward items get their first-choice building; the rest are the lowest-regret items on the oversubscribed buildings, displaced to the ones with slack. The prime buildings end near 100 percent utilization.

Cost comparison and the spatial assignment

The map makes the logic visible: forward items cluster to the building nearest their demand, new items are ringed to mark their provisional placement, and where a prime building fills up you can see a few items in a neighbor's color, the capacity displacement the cost table summarizes. The dead and slow tail is noted off to the side, since it has no demand geography to place it by.


5. Limitations

Capacity is modeled as cube only; a real building also limits SKU count and weight. Distance is Euclidean, a stand-in for lane rates. The instance is single-period and static: it slots the catalog once rather than re-slotting as demand drifts. Greedy plus local search is near-optimal, not exact, though the floor shows the gap is small. Items ordered together are not yet co-located. New items enter on a flat provisional demand estimate rather than a category prior, so their first placement is a best guess that should be revised once real history accrues. The volume cuts that separate dead, slow, and active are static thresholds; a demand-drift-aware version would revisit them as patterns change.


6. Future work

  • Co-location, homing items that ship together in the same building to cut multi-line order handling, which turns the objective from separable to affinity-based.
  • Multi-dimensional capacity: cube, SKU slots, and weight together.
  • Real lane rates in place of Euclidean distance.
  • Periodic re-slotting with a churn penalty, so homes track demand drift without thrashing the network with moves.
  • An exact formulation on small instances to certify the heuristic gap.

7. Using the engine

Standalone, on the included synthetic instance:

pip install -r requirements.txt
python scripts/run_demo.py

As a module:

from home_engine import generate, assign_homes, compare

state = generate()                 # or supply your own instance
result = assign_homes(state)
result["table"]                    # item, primary_home, home_2, home_3, cost, footprint
result["loads"]                    # per-building utilization
comparison, _, first_choice = compare(state)

Inputs are items with a demand rate, a cube per unit, an OPHQ footprint, and a demand split across regions, plus buildings with a location, handling rate, and cube capacity, and region locations. Demand and OPHQ come from the demand and policy engines.


8. Project structure

home-assignment-engine/
  README.md                 this document
  requirements.txt
  src/home_engine/
    nodes.py                forward buildings and the storage building
    classify.py             movement gate: Syntetos-Boylan buckets, new vs dead, placement
    regions.py              demand regions and the building-to-region distance model
    data.py                 synthetic instance with a full demand-pattern spread
    cost.py                 demand-weighted cost matrix, active items over forward buildings
    assign.py               storage preassignment, regret-greedy start, local search, fallbacks
    evaluate.py             floor versus engine versus balanced baseline
    pipeline.py             assign_homes(): instance in, home table out
  scripts/run_demo.py       end-to-end demo, figure and exports
  tests/                    capacity, feasibility, near-floor, local-search, fallbacks
  assets/                   generated figure
  data/                     sample home table and building loads

9. References

Chopra, S., and Meindl, P. (2016). Supply Chain Management: Strategy, Planning, and Operation, 6th ed. Pearson. (Facility location and network design.)

Fisher, M. L., Jaikumar, R., and Van Wassenhove, L. N. (1986). A multiplier adjustment method for the generalized assignment problem. Management Science, 32(9), 1095-1103. (The generalized assignment problem and its heuristics.)

Martello, S., and Toth, P. (1990). Knapsack Problems: Algorithms and Computer Implementations. Wiley. (Regret and greedy heuristics for assignment under capacity.)

Bartholdi, J. J., and Hackman, S. T. (2019). Warehouse and Distribution Science. (Velocity-based slotting and storage assignment.)

Syntetos, A. A., and Boylan, J. E. (2005). The accuracy of intermittent demand estimates. International Journal of Forecasting, 21(2), 303-314. (The ADI and CV2 demand-pattern classification the movement gate consumes.)

About

Slots items to buildings to minimize demand-weighted fulfillment cost under cube capacity, and parks the non-moving tail in storage.

Topics

Resources

License

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages