Run your own simulation loop¶
SimulationEngine is the complete, checked way to run a simulation. Sometimes
you want the steps in your own hands: to plug Stockcast into another
simulator, to step through a period while teaching, or to drive a live
process one day at a time. The same building blocks the engine uses are
public.
The three primitives¶
| Step | Call | Does |
|---|---|---|
| 1. Receive | state.advance_period(freq=..., is_review_period=...) |
moves to the next period, receives due deliveries, serves old backorders first |
| 2. Decide | policy.predict(state, current_period=...), then update_inventory_with_orders(state, decision, policy=policy) |
places the order: into the pipeline, or received now if \(L = 0\) |
| 3. Meet demand | state.fulfill_demand(demand_rows) |
serves this period's demand; lost sales or backorders |
Each call returns a new state, so earlier states stay available.
Example loop¶
Setup: the tea shop from the Walkthrough
import numpy as np
import pandas as pd
from stockcast.core import InventoryStateDataFrame, SimulationEngine
from stockcast.policies import OrderUpToPolicy
from stockcast.utils import DemandGenerator
sku = "tea_250g"
opening_date = pd.Timestamp("2026-01-05")
lead_time, review_period = 2, 4
horizon = lead_time + review_period
demand = DemandGenerator(
[sku], first_date=opening_date + pd.Timedelta(days=1), freq="D",
random_seed=3, negative_demand_handling="clip_zero",
).seasonal(n_periods=56, base=6.0, amplitude=2.0, season_length=7, std=2.0)
demand["y"] = demand["y"].round()
inventory = InventoryStateDataFrame.from_observed(
pd.DataFrame({"unique_id": [sku], "date": [opening_date], "on_hand": [30.0]}),
allow_backorders=False, # some pages step this state by hand
)
paths = np.random.default_rng(42).poisson(6.0, size=(10_000, horizon))
def tea_policy(probability):
"""Order-up-to policy whose target is the given quantile of 6-day demand."""
target = pd.DataFrame({
"unique_id": [sku],
"target": [np.quantile(paths.sum(axis=1), probability)],
"date": [opening_date + pd.Timedelta(days=horizon)], # the last day it covers
})
return OrderUpToPolicy(
lead_time=lead_time, review_period=review_period, freq="D",
service_level=probability, allow_backorders=False,
).fit(target, target_column="target")
policy = tea_policy(0.95)
run_settings = dict(random_seed=3)
result = SimulationEngine().run(
policy=policy, demand_source=demand, inventory=inventory, **run_settings,
)
from stockcast.utils import update_inventory_with_orders
state = inventory
ending_on_hand = []
for p in range(56):
review = p % review_period == 0
state = state.advance_period(freq="D", is_review_period=review) # 1
if review:
now = int(state.get_dataframe()["period"].iloc[0])
decision = policy.predict(state, current_period=now) # 2
state = update_inventory_with_orders(state, decision, policy=policy)
state = state.fulfill_demand(demand[demand["period"] == p]) # 3
ending_on_hand.append(float(state.get_dataframe()["on_hand"].iloc[0]))
engine_on_hand = result.to_event_frame()["ending_on_hand"].tolist()
ending_on_hand == engine_on_hand
The loop reproduces the engine's run exactly. Note that current_period is
the state's period (the opening state is period 0, so the first demand period
is state period 1).
What the engine adds¶
Your loop is responsible for everything the engine otherwise does for you:
| The engine… | In your loop, you… |
|---|---|
| validates the demand calendar, targets, and settings before starting | check inputs yourself |
| copies the opening state and policy | keep track of which objects you change |
| writes and checks one event table row per SKU and period | record what you need |
| applies callbacks, constraints, suppliers, and processes | apply them yourself, or leave them out |
| builds the run manifest | record your settings |
Use the engine for experiments and comparisons, and the primitives when you need control of each step.
Order lines¶
place_order_lines(state, OrderLines(...)) is the order-level version of step
2: explicit deliveries per supplier, with their own due periods and partial
deliveries. See Suppliers and open orders.
Notebook 03 builds this loop step by step and compares it with the engine.