A network optimizer that decides which inbound containers to unload, on which day, and to which building, so that the most demand is covered at a target service level under a daily unloading limit. It makes the recommendation itself and explains it, rather than handing a human a dashboard to triage.
This is the inbound counterpart to the demand forecasting engine and the network decision system: the demand engine says how much will be needed, and this engine decides which arriving containers to open first to meet it.
- The problem, and what was replaced
- Architecture and the pipeline it sits in
- Nodes by function, and home assignment
- Probabilistic demand: covering a service level
- The optimizer: objective, constraints, and why greedy
- Mixed containers and visitors
- Evaluation and results
- Limitations
- Future work
- Using the engine
- Project structure
- References
Containers arrive at an import dock carrying many items bound for different buildings. Only so many can be unloaded per day, so the order and destination of unloading decides how much customer demand gets met and how soon. The original tool computed a per-container color category for a human to read and then choose from by hand. Two things were replaced. The color triage was never clearly defined, so it became an explicit recommendation with a stated reason. And the decision itself was moved from the human to the engine: this is now an optimizer that chooses the assignment to maximize demand coverage, not a report about containers.
The original was also tied to one company's building names. Those are gone. The network is described by function, and real site names live only in configuration.
The engine consumes three inputs, each produced upstream and never recomputed here, which keeps the whole system a straight line with no cycles:
demand forecast (mean, std) ---.
item home building -----------> container optimizer --> container -> (building, day)
inbound containers (ETA, CBM) --' + coverage achieved
daily unloading capacity ------' + visitors flagged
Home building and the demand forecast are inputs. The optimizer decides only the selection and scheduling of container unloads. Because it never recomputes its own inputs, it cannot create the circular dependency that a naive multi-echelon design would.
Nodes are described by the role they play, not by name: an inbound cross-dock where containers arrive, a storage and reserve campus, and forward fulfillment buildings that ship to customers. Forward buildings are distinguished by capability, the kind of item they are built to hold, so the model describes a two-building network or a ten-building one without code changes.
Each item has a single home building, assigned by a transparent match between item attributes (size class and parcel eligibility in this version) and building capability. This is a simplified, generalized form of a real Building Assignment by Item Designation workflow that used size, weight, parcel eligibility, fragility, demand velocity, and sales distribution. Home assignment is an input to the optimizer, computed once, not a decision the optimizer makes.
The engine does not cover a single forecast number. It covers demand at a target service level. Given a per-item forecast mean and standard deviation from the demand engine, the required coverage is the service-level quantile of the demand distribution, so a higher service level asks the network to hold more. At a 95 percent service level, an item with mean 50 and standard deviation 15 has a required coverage of about 75 units. This is the probabilistic meaning: the target is a quantile of the forecast, and the same demand engine that produces the forecast feeds this one.
The decision is to assign each container to one building and one day, or to defer it, within these priorities, applied per day:
- Deadline feasibility. A container must be unloaded on or before its deadline. Each day the engine schedules the minimum number needed so no future deadline becomes impossible, using earliest-deadline-first, the classic feasibility rule (Liu and Layland 1973). This is a hard constraint, above the objective.
- Coverage. Demand first, always: fill the remaining slots with the containers that cover the most service-level demand.
- Ease and demurrage. Among containers within a few percent of the best coverage, prefer higher purity, fewer SKUs, and the container already bleeding demurrage.
- Idle avoidance. Only if a slot would otherwise sit empty, use it on a container already past free time, to stop demurrage. Idle capacity is never spent on a container that neither covers demand nor is accruing.
ETAs are exogenous. They are assigned by the carrier, not chosen by the engine. A container cannot be unloaded before it arrives, so ETA enters only as an availability constraint; the engine decides the unload day and building, never the arrival. Deferral is bounded: a container cannot be held past its deadline, and holding it past its free time accrues demurrage per day. Cube is tracked as a capacity signal. In production this runs as a rolling plan, re-solved as new ETAs and forecasts arrive, rather than as one fixed horizon.
Demand coverage is monotone and submodular: a unit already covered cannot be covered again, so each additional container has diminishing value. Greedy selection under a per-day limit is submodular maximization under a matroid constraint, which carries the classical guarantee of reaching at least a (1 - 1/e) fraction of the best possible (Nemhauser, Wolsey, and Fisher 1978; Fisher, Nemhauser, and Wolsey 1978). Greedy is therefore fast, explainable, and provably close to optimal, not a stopgap.
Because assignment is container-level, a container whose items are not all homed at its assigned building leaves those off-home items as visitors at that building. They are not counted as coverage, because they are not where their demand is, and they are flagged as reverse-replenishment candidates. Visitor is therefore not a special label; it is what falls out whenever a pull places stock away from its home. A mixed container is assigned to its dominant building, the one its contents most cover.
The value of the engine is the demand it covers per scarce unload slot, so it is compared against a first-come baseline that uses the same daily capacity but unloads by earliest ETA, ignoring demand value when choosing. Both are scored on the same service-level target.
Results on the synthetic demo (120 items, 60 containers, 14-day horizon, 3 containers unloaded per day, 95 percent service level, 3 free days, a 9-day hard deadline). These are illustrative, from the included generator, not a claim about any real network:
| strategy | coverage rate | containers used | visitor units | demurrage cost | deadline violations | lift vs first-come |
|---|---|---|---|---|---|---|
| optimizer (greedy) | 0.576 | 40 | 1654 | 5227 | 0 | 1.073 |
| first-come baseline | 0.537 | 40 | 1916 | 3465 | 0 | 1.00 |
The optimizer covers about 7 percent more demand than first-come on the identical unload slots, meets every deadline, and creates fewer visitors. It does pay more demurrage, and that is the honest tradeoff rather than a defect: prioritizing the most valuable containers means deferring the cheap ones, which then sit accruing charges, while first-come unloads in arrival order and naturally minimizes dwell. The engine makes this tradeoff explicit and tunable through the coverage-tolerance band; a first-come rule makes it blindly. Whether the extra coverage is worth the extra demurrage depends on the value of an avoided stockout, which is the lever named in future work.
Greedy is near-optimal, not optimal. For a portfolio-grade claim of optimality on a given instance, the greedy result should be compared against an exact formulation on small cases, which is listed in future work.
Container-level assignment is a modeling choice. It matches an operation where a whole container is pulled to one building, and it makes the visitor concept fall out cleanly, but it cannot split a container across buildings even when that would cover more demand. That is deliberate and stated, not an oversight.
The demo runs on synthetic data so the project is inspectable without proprietary inputs. The home-assignment rule here uses size and parcel eligibility only; the real workflow used more attributes, which the scoring function is structured to accept.
- An exact mixed-integer formulation to bound the greedy gap on small instances.
- A demurrage-weighted objective: given a value per unit of covered demand, optimize net benefit (coverage value minus demurrage) rather than treating demurrage only as a bounded tiebreak, so the coverage-versus-demurrage tradeoff is made on economics.
- A tunable ease-of-unload metric and tier order, exposed as configuration.
- A rolling multi-day horizon that re-plans as new ETAs and forecasts arrive, which is the intended production shape given that ETAs are exogenous.
- Cube and truck constraints on the outbound leg, not only as a tiebreak signal.
Standalone, on the included synthetic network:
pip install -r requirements.txt
python scripts/run_demo.pyAs a module, consuming a forecast and containers from the rest of the system:
from container_engine import generate, plan, evaluate
data = generate() # or supply your own frames
result, required = plan(data, service_level=0.95)
for a in result.assignments: # container -> building, day, coverage, reason
...
comparison = evaluate(data) # optimizer vs first-come baselineThe inputs are a per-item demand forecast (item_id, mean, std), items with a home building, inbound containers (container_id, eta_day, cbm) and their contents (container_id, item_id, qty), and a daily capacity. Homes and the forecast are produced upstream; this engine consumes them.
container-network-engine/
README.md this document
requirements.txt
src/container_engine/
nodes.py functional node roles and building capability
assign_home.py attribute-driven home-building assignment (an input)
demand.py service-level required coverage from the forecast
data.py synthetic network generator
optimize.py the container-level greedy optimizer
evaluate.py optimizer vs first-come baseline, coverage by building
pipeline.py plan(): forecast and containers in, schedule out
scripts/run_demo.py end-to-end demo, produces the figure and metrics
tests/ home assignment, demand, capacity, ETA, coverage, lift
assets/ generated figure
Chopra, S., and Meindl, P. (2016). Supply Chain Management: Strategy, Planning, and Operation, 6th ed. Pearson. (Cycle service level and safety inventory.)
Fisher, M. L., Nemhauser, G. L., and Wolsey, L. A. (1978). An analysis of approximations for maximizing submodular set functions II. Mathematical Programming Study, 8, 73-87. (Greedy under matroid constraints.)
Liu, C. L., and Layland, J. W. (1973). Scheduling algorithms for multiprogramming in a hard-real-time environment. Journal of the ACM, 20(1), 46-61. (Earliest-deadline- first feasibility.)
Nemhauser, G. L., Wolsey, L. A., and Fisher, M. L. (1978). An analysis of approximations for maximizing submodular set functions I. Mathematical Programming, 14(1), 265-294. (The 1 - 1/e greedy guarantee.)
Wolsey, L. A. (1998). Integer Programming. Wiley. (Exact assignment and scheduling formulations, referenced for future work.)
