Skip to content

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
True

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.