Skip to content

Simulation engine

Run simulations and read their results. Guide: The simulation engine.

SimulationEngine

SimulationEngine(
    verbose: int = 0,
    *,
    sku_column: Optional[str] = None,
    date_column: str = "date",
    period_column: str = "period",
    demand_column: str = "y",
)

Run inventory simulations: the clock and the only place stock changes.

Each period the engine receives due deliveries, lets the policy decide on scheduled periods (then applies callbacks, ordering constraints and the supply model), serves demand, applies inventory processes and after-demand callbacks, and records one checked event table row per SKU. Everything that can be validated is checked before the first period.

Example
result = SimulationEngine().run(
    policy=policy, demand_source=demand, inventory=inventory,
    warmup_periods=7,                 # optional: exclude a burn-in from scoring
)

Parameters:

Name Type Description Default
verbose int

0 silent (default), 1 start/end/milestones, 2 one line per period.

0
sku_column Optional[str]

SKU column of the demand table; None (default) uses the inventory's SKU column.

None
date_column str

Date column of the demand table (default "date").

'date'
period_column str

Period column of the demand table (default "period").

'period'
demand_column str

Demand column of the demand table (default "y").

'y'

A column named other than its default must exist in the demand table, and the table must not also hold the default name. Results and event tables use the default names.

run

run(
    policy: BasePolicy,
    demand_source: Union[DataFrame, Callable],
    inventory: InventoryStateDataFrame,
    n_periods: Optional[int] = None,
    *,
    freq: Optional[str] = None,
    warmup_periods: int = 0,
    scoring_periods: Optional[int] = None,
    settlement_periods: int = 0,
    order_during_settlement: Optional[bool] = None,
    demand_source_name: Optional[str] = None,
    random_seed: Optional[int] = None,
    policy_schedule: Optional[
        Mapping[int, BasePolicy]
    ] = None,
    order_constraints: Optional[
        Union[OrderingConstraints, Sequence]
    ] = None,
    callbacks: Optional[
        Sequence[SimulationCallback]
    ] = None,
    supply: Optional[SupplyModel] = None,
    processes: Optional[Sequence[InventoryProcess]] = None,
    allow_target_reuse: bool = True,
    missing_targets: str = "raise",
) -> SimulationResult

Simulate a policy against a demand path.

The engine copies inventory and policy, validates the whole experiment, then plays n_periods demand periods: receive, decide (on scheduled periods), meet demand, record. Your input objects are not changed.

Parameters:

Name Type Description Default
policy BasePolicy

A fitted policy.

required
demand_source Union[DataFrame, Callable]

A DataFrame with unique_id, y and date and/or period, covering every SKU and period, or a callable period -> DataFrame (called once per period before the run). Periods count from the inventory's opening date: period 0 is dated one period after it. With a date column the dates set the periods, and period may keep your own numbering (such as 12, 13, ...) as long as it advances one per period.

required
inventory InventoryStateDataFrame

The opening InventoryStateDataFrame. Its max_lead_time must cover the policy's lead time and the supply model's longest delivery. Its allow_backorders must match the policy's, or be unset to take the policy's.

required
n_periods Optional[int]

Number of demand periods. Inferred from a DataFrame; required for a callable.

None
freq Optional[str]

Length of one period, a pandas frequency such as "D". Period p is dated opening date + (p + 1) periods. Defaults to the policy's freq (given or read at fit); required when the policy has none. If given, it must match the policy's.

None
warmup_periods int

Leading periods excluded from scoring (default 0).

0
scoring_periods Optional[int]

Periods that metrics describe by default (>= 1). Defaults to the periods left after warmup and settlement. The three windows must add up to n_periods.

None
settlement_periods int

Trailing periods excluded from scoring (default 0).

0
order_during_settlement Optional[bool]

Whether the policy may order in settlement. Required when settlement_periods > 0.

None
demand_source_name Optional[str]

Optional label for the demand, stored in the manifest.

None
random_seed Optional[int]

Optional seed behind the demand, stored in the manifest.

None
policy_schedule Optional[Mapping[int, BasePolicy]]

{decision_period: fitted_policy} with refitted policies for later decisions. Each must match policy's class and configuration. A dated snapshot's forecast origin must equal the decision's information date; when policy is dated, every snapshot must be dated too.

None
order_constraints Optional[Union[OrderingConstraints, Sequence]]

A list of OrderingConstraint objects, applied in order (or an OrderingConstraints holding them).

None
callbacks Optional[Sequence[SimulationCallback]]

SimulationCallback objects, applied in order.

None
supply Optional[SupplyModel]

A SupplyModel for suppliers, random lead times and split or unreliable deliveries. None: one delivery lead_time periods after each order.

None
processes Optional[Sequence[InventoryProcess]]

InventoryProcess objects (for example ShelfLife), applied in order.

None
allow_target_reuse bool

True (default): a policy keeps deciding until policy_schedule replaces it, so one target can serve many decisions (a static-target experiment). False: every decision needs a target fitted for it: each decision after the first is a key of policy_schedule, and a dated policy used at the first decision has that decision's information date as its forecast origin. Checked before the run.

True
missing_targets str

What a decision does for an inventory SKU its policy has no fitted level for. "raise" (default): the run fails before it starts. "no_order": no order for that SKU at that decision; its stock still moves, its target_level is NaN until its next decision with a target, and the event table flags the row in target_missing_flag. run_settings["missing_targets"] counts these decisions per SKU. Needs built-in policies (their fitted SKUs are known); a SKU without a target at any decision raises. The SKU is not in the decision callbacks see, and a callback or ordering constraint that gives it an order there stops the run.

'raise'

Returns:

Type Description
SimulationResult

A SimulationResult.

Raises:

Type Description
ValueError

If an input is incomplete or inconsistent, for example an unfitted policy, a demand calendar with gaps, a target for the wrong window, or windows that do not add up.

run_comparison

run_comparison(
    policies: Union[
        List[BasePolicy], Mapping[str, BasePolicy]
    ],
    demand_source: Union[DataFrame, Callable],
    inventory: InventoryStateDataFrame,
    n_periods: Optional[int] = None,
    *,
    freq: Optional[str] = None,
    warmup_periods: int = 0,
    scoring_periods: Optional[int] = None,
    settlement_periods: int = 0,
    order_during_settlement: Optional[bool] = None,
    demand_source_name: Optional[str] = None,
    random_seed: Optional[int] = None,
    labels: Optional[List[str]] = None,
    policy_schedules: Optional[
        Union[
            List[Optional[Mapping[int, BasePolicy]]],
            Mapping[str, Mapping[int, BasePolicy]],
        ]
    ] = None,
    order_constraints: Optional[
        Union[OrderingConstraints, Sequence]
    ] = None,
    callbacks: Optional[
        Sequence[SimulationCallback]
    ] = None,
    supply: Optional[SupplyModel] = None,
    processes: Optional[Sequence[InventoryProcess]] = None,
    allow_target_reuse: bool = True,
    missing_targets: str = "raise",
) -> ComparisonResult

Simulate several policies on the same experiment.

The demand is built once and shared; each policy starts from its own copy of inventory. Constraints, callbacks, supply and processes apply to every branch (callbacks and processes are reset between branches), and random supplier lead times use the same seeded draws per period and SKU. Seeded delivery outcomes follow each branch's own deliveries. Arguments not listed below are those of run. To compare policies with different allow_backorders, leave the inventory's allow_backorders unset.

Parameters:

Name Type Description Default
policies Union[List[BasePolicy], Mapping[str, BasePolicy]]

Fitted policies, as a list or as {label: policy}.

required
labels Optional[List[str]]

Names for the results of a list of policies; defaults to the policies' names. Not used with a dict, whose keys are the labels.

None
policy_schedules Optional[Union[List[Optional[Mapping[int, BasePolicy]]], Mapping[str, Mapping[int, BasePolicy]]]]

One policy_schedule (or None) per policy, in the same order; with a dict of policies, also {label: policy_schedule} for the labels that have one.

None

Returns:

Type Description
ComparisonResult

A ComparisonResult keyed by label.

Example
comparison = SimulationEngine().run_comparison(
    {"order-up-to": order_up_to, "reorder point": reorder_point},
    demand, inventory,
)
comparison["order-up-to"].to_event_frame()

SimulationResult

SimulationResult(
    history: DataFrame,
    inventory: InventoryStateDataFrame,
    n_periods: int,
    policy_name: str,
    event_frame: Optional[DataFrame] = None,
    run_settings: Optional[dict] = None,
    run_manifest: Optional[dict] = None,
    callback_audit: Optional[DataFrame] = None,
    order_frame: Optional[DataFrame] = None,
    process_flows: Optional[DataFrame] = None,
)

The outcome of one simulation run.

Returned by SimulationEngine.run. The event table is the main record; the other tables and the manifest add detail.

Attributes:

Name Type Description
inventory

The final InventoryStateDataFrame.

history

State snapshots after each period.

n_periods

Number of simulated demand periods.

policy_name

Name of the policy.

run_settings

The run's settings (also in run_manifest).

run_manifest

A JSON-friendly record of inputs, settings and versions, with the sections in RUN_MANIFEST_REQUIRED_SECTIONS.

Example
events = result.to_event_frame(window="scoring")
orders = result.to_order_frame()
result.summary()["fill_rate"]

to_event_frame

to_event_frame(window: Optional[str] = None) -> DataFrame

Return the event table: one row per SKU and period.

Every row satisfies the stock, pipeline and backorder balance identities. Columns are described in the output-tables reference (CANONICAL_EVENT_COLUMNS plus optional columns).

Parameters:

Name Type Description Default
window Optional[str]

"warmup", "scoring", "settlement", or "all" / None for every row.

None

Returns:

Type Description
DataFrame

A copy of the event table rows.

to_callback_audit_frame

to_callback_audit_frame() -> DataFrame

Return one row per accepted callback effect (CALLBACK_AUDIT_COLUMNS).

to_order_frame

to_order_frame() -> DataFrame

Return the order table: one row per scheduled delivery.

Rows cover the opening pipeline and every order placed during the run. Columns are stockcast.core.ORDER_FRAME_COLUMNS:

- order_id: order line identifier (deliveries of one line share it)
- unique_id, supplier_id: SKU and supplier (``None`` when unknown,
  for example a pipeline given only as ``in_transit`` or a run
  without ``supply``)
- source: ``"opening"`` for the opening pipeline, ``"placed"``
  for orders placed in the run
- order_period / order_date: when the line was ordered (missing
  for opening orders without a declared order period)
- due_period / due_date: when this delivery is received, before
  that period's demand
- lead_time: ``due_period - order_period`` (realized, per delivery)
- ordered_quantity: quantity of the whole order line
- delivery_quantity: quantity of this delivery
- status: ``"received"`` or ``"open"`` at the end of the run

Per SKU and period, received deliveries add up to the event table's received_units and open ones to on_order_end.

to_process_flow_frame

to_process_flow_frame() -> DataFrame

Return one row per nonzero process flow, SKU and period.

Columns are stockcast.core.PROCESS_FLOW_COLUMNS: the SKU, the event-table period/date/demand_period/run_window, the process and flow names, the flow's direction (inflow/outflow) and category (general/expiry), the phase it acted in, and its nonnegative quantity.

Per SKU and period, expiry flows add up to the event table's expired_units; general inflows and outflows add up to process_inflow_units and process_outflow_units. The frame is empty for a run without processes.

summary

summary() -> Dict

A few headline numbers for the scoring window.

Returns:

Type Description
Dict

dict with keys: - fill_rate: share of scoring demand served from stock in its own period, 1 - shortage / demand. - demand_period_service_level: share of SKU-period rows with demand and no shortage. - mean_ending_on_hand_per_sku_period: mean ending stock per row. - stockout_periods: periods in which at least one SKU was short. - total_order_units: units ordered. - order_event_count: decisions that placed a positive order. - sku_order_line_count: positive order lines.

ComparisonResult

ComparisonResult(results: Dict[str, SimulationResult])

Results of SimulationEngine.run_comparison, one per policy.

Behaves like a read-only mapping from label to SimulationResult.

Attributes:

Name Type Description
results

Dict of label to SimulationResult.

Example
comparison.summary()          # one row per policy
comparison["95% target"]      # one SimulationResult
list(comparison)              # the labels

summary

summary() -> DataFrame

SimulationResult.summary() of every run, one row per label.