Skip to content

Models and scenarios

model

Models: reusable, parameterised simulation definitions.

A :class:Model couples a build function build(sim, params) - which creates resources and starts processes on a fresh :class:Simulation - with a parameter schema, a run length and an optional warm-up. Models are what experiments, Monte Carlo studies, sensitivity analysis, validation, the CLI and optimisers all operate on.

Parameter dataclass

Parameter(name: str, default: Any = REQUIRED, kind: ParameterKind = 'any', low: float | None = None, high: float | None = None, choices: tuple[Any, ...] | None = None, description: str = '', unit: str = '')

Schema for one model parameter.

coerce

coerce(value: Any) -> Any

Validate and normalise value; raises :class:ConfigError.

Params

Params(data: Mapping[str, Any])

Bases: Mapping[str, Any]

Read-only resolved parameters; values are reachable as keys or attributes.

Model

Model(build: BuildFn, *, name: str | None = None, duration: float | None = None, warmup: float = 0.0, parameters: Mapping[str, Any] | Sequence[Parameter] | None = None, version: str = '0', description: str = '', outputs: Sequence[str] | None = None, sim_options: Mapping[str, Any] | None = None, strict: bool = True, presets: Mapping[str, Mapping[str, Any]] | None = None)

A parameterised simulation model.

def build(sim, p): ... sim.metrics.set("answer", p.x * 2) m = Model(build, name="demo", duration=1.0, parameters={"x": 21}) m.simulate(seed=1).metrics["answer"] 42.0

preset_scenarios

preset_scenarios() -> list[Any]

The presets as scenarios, with a baseline (model defaults) first.

check_parameters

check_parameters(overrides: Mapping[str, Any] | None = None) -> list[str]

All problems with overrides (empty list when valid).

create

create(params: Mapping[str, Any] | None = None, *, seed: int | None = None, trace: bool = False, warmup: float | None = None, **sim_options: Any) -> Simulation

Build (but do not run) a simulation - handy for stepping and inspection.

simulate

simulate(params: Mapping[str, Any] | None = None, *, seed: int | None = None, duration: float | None = None, warmup: float | None = None, trace: bool = False, **sim_options: Any) -> SimulationResult

Run one replication and return its result.

If duration is shorter than the model's warm-up and no warmup is given, the warm-up is scaled to the same fraction of the shorter run.

evaluate

evaluate(params: Mapping[str, Any] | None = None, *, metric: str, replications: int = 10, seed: int = 0) -> float

Mean of metric over replications runs with common random numbers.

This is the bridge to optimisers: the same seed gives the same random streams for every parameter vector, which removes much of the noise when comparing candidates.

with_options

with_options(*, duration: float | None = None, warmup: float | None = None, name: str | None = None, sim_options: Mapping[str, Any] | None = None) -> Model

A copy with a different run length, warm-up, name or simulation options.

model

model(build: BuildFn) -> Model
model(build: None = None, /, *, name: str | None = ..., duration: float | None = ..., warmup: float = ..., parameters: Mapping[str, Any] | Sequence[Parameter] | None = ..., version: str = ..., description: str = ..., outputs: Sequence[str] | None = ..., sim_options: Mapping[str, Any] | None = ..., strict: bool = ...) -> Callable[[BuildFn], Model]
model(build: BuildFn | None = None, /, **kwargs: Any) -> Model | Callable[[BuildFn], Model]

Decorator turning a build function into a :class:Model.

@model(duration=100, parameters={"servers": 2}) ... def bank(sim, p): ... sim.resource("teller", p.servers) bank.simulate(seed=1).metrics["resource.teller.utilization"] 0.0

scenario

Scenarios: named parameter sets describing a "what if".

Scenario dataclass

Scenario(name: str, parameters: Mapping[str, Any] = dict(), description: str = '', tags: tuple[str, ...] = ())

A named set of parameter values.

Scenarios only hold overrides; the model supplies defaults for anything not mentioned. Derive variants instead of copying dictionaries:

base = Scenario("baseline", {"arrival_rate": 1.0, "capacity": 3}) base.derive("high_demand", arrival_rate=1.5).parameters["arrival_rate"] 1.5 base.scale("double_capacity", capacity=2).parameters["capacity"] 6

derive

derive(name: str, description: str = '', **overrides: Any) -> Scenario

A new scenario with some parameters replaced.

scale

scale(name: str, description: str = '', **factors: float) -> Scenario

A new scenario with numeric parameters multiplied by factors.

Integer parameters stay integers (rounded), so capacities remain valid.

grid

grid(axes: Mapping[str, Sequence[Any]], base: Scenario | Mapping[str, Any] | None = None, *, prefix: str = '') -> list[Scenario]

Full factorial design: one scenario per combination of axis values.

" | ".join(s.name for s in grid({"capacity": [1, 2], "rate": [0.5]})) 'capacity=1,rate=0.5 | capacity=2,rate=0.5'

one_at_a_time

one_at_a_time(base: Scenario, changes: Mapping[str, Iterable[Any]]) -> list[Scenario]

Vary one parameter at a time around base (the base itself is not included).

flowchart

Declarative flowchart models: build a simulation from YAML, no Python needed.

A flowchart describes where entities come from (sources), the stations they visit (a resource and a service time, or a plain delay), how they move on (next: one station, or random routing by probability) and where they leave (exit or any name listed in sinks). Numbers anywhere can refer to parameters as $name, so a flowchart works with experiments, simulsi whatif, the dashboard and the optimiser like any Python model.

.. code-block:: yaml

flow:
  name: clinic
  duration: 480
  parameters:
    nurses: 2
    doctors: {default: 3, low: 1, description: doctors on shift}
    arrival_mean: 4.0
  resources:
    nurse: $nurses
    doctor: {capacity: $doctors}
  sources:
    - name: patient
      interarrival: {distribution: exponential, mean: $arrival_mean}
      next: triage
  stations:
    triage: {resource: nurse, service: {distribution: triangular, low: 2, mode: 4, high: 8},
             next: {consult: 0.8, exit: rest}}
    consult: {resource: doctor, service: {distribution: exponential, mean: 10},
              patience: 60, next: exit}
  presets:
    extra_doctor: {doctors: 4}

Two more station kinds: batch: 4 (or batch: {size: 4, timeout: 30}) holds entities until a group is ready and does the step once for the whole group (an oven, a shuttle); parallel: {lab: {...}, xray: {...}} splits an entity into branches that run at the same time and joins when the slowest finishes.

Load it with :func:load_flowchart (or pass the file to any CLI command that takes a model). Files are data only: they can create registered distributions and the building blocks below, never run code.

flowchart_model

flowchart_model(spec: Mapping[str, Any]) -> Model

Build a :class:~simulsi.Model from a flowchart mapping (the content under flow:).

is_flowchart

is_flowchart(path: str | Path) -> bool

True if path is a YAML file with a top-level flow: section.

load_flowchart

load_flowchart(source: str | Path | Mapping[str, Any]) -> Model

Load a flowchart model from a YAML file, YAML text or an already-parsed mapping.

models

Built-in models: builtin:<name> in the CLI, configs and the dashboard.

  • mmc - the M/M/c queue (checked against Erlang C)
  • airline - aircraft rotations, delay propagation, crews, gates, spares, weather
  • airport_turnaround - parallel ground-handling tasks, shared crews, banked schedules
  • disruption_recovery - after a hub storm: delay vs cancel vs spare aircraft
  • emergency_department - triage, acuity, beds, doctors, boarding and diversion
  • restaurant - table mix, flexible seating, reservations and the kitchen
  • ev_charging - fast/slow chargers under a shared grid limit
  • inventory - the (s, S) reorder policy and its costs
  • theme_park - ride queues, express passes and app-based routing
  • warehouse - discrete vs batch picking, packing and truck cut-offs
  • epidemic - stochastic SEIR with hospital capacity and a lockdown policy
  • supply_chain - four-stage beer game and the bullwhip effect
  • ride_hailing - matching, pickup-time feedback, cancellations, surge pricing
  • cloud_autoscaling - bursty traffic, cold starts, timeouts, autoscaling
  • traffic_signal - fixed vs actuated signal timing, compared with Webster

Every model has presets: named what-if scenarios (model.preset_scenarios()).

Other packages can add models through the simulsi.models entry-point group, for example in pyproject.toml::

[project.entry-points."simulsi.models"]
bakery = "mypackage.models:bakery"

They then work as builtin:bakery in the CLI, configs and the dashboard.

erlang_c

erlang_c(arrival_rate: float, service_rate: float, servers: int) -> dict[str, float]

Steady-state M/M/c results (Erlang C). Requires utilization < 1.

webster_delay

webster_delay(flow_vph: float, green: float, cycle: float, headway: float) -> float

Webster (1958) average delay per vehicle at a fixed-time signal (NaN if oversaturated).

load_plugin_models

load_plugin_models() -> dict[str, str]

Register models from the simulsi.models entry points; returns load errors by name.

queueing

Built-in reference models.

These are small, well-understood models with known analytic results. They serve as quick-start examples, CLI defaults (simulsi run builtin:mmc) and as correctness checks for the engine.

erlang_c

erlang_c(arrival_rate: float, service_rate: float, servers: int) -> dict[str, float]

Steady-state M/M/c results (Erlang C). Requires utilization < 1.

airline

Airline schedule and delay propagation.

A fleet of aircraft flies rotations through a hub. Each aircraft has a schedule of legs with planned block times and planned ground time (minimum turn plus a schedule buffer). Delays come from random disruptions (technical, ATC, late passengers), but most delay in real networks is reactionary: a late inbound aircraft, or a crew arriving late from another aircraft, delays the next departure, which delays the one after that. Gates at the hub are held from arrival until pushback, so padding the schedule also ties up gates. Spare aircraft can take over badly delayed legs, and legs delayed beyond a limit are cancelled. A weather window cuts the runway departure rate.

Try: schedule_buffer 0 vs 30, crew_swap_prob 0 vs 0.8, spares 0 vs 3, weather_minutes 0 vs 180, gates 4 vs 10.

Time unit: minutes from the first departure bank. Synthetic data only.

epidemic

Epidemic with hospital capacity and a lockdown policy (stochastic SEIR).

People are Susceptible, Exposed (infected, not yet infectious), Infectious or Removed. Transitions happen one at a time at exponential rates (Gillespie's exact algorithm), so small outbreaks can fizzle out by chance and the same parameters give different epidemics. A share of new cases need a hospital bed; when no bed frees up within bed_patience days the patient goes untreated, with a much higher fatality rate. A lockdown starts when bed occupancy reaches lockdown_trigger and lifts once it falls below lockdown_release (after at least lockdown_min_days).

Try: r0 1.2 vs 3, vaccination 0 vs 0.6, hospital_beds 15 vs 60, lockdown_trigger 0.5 vs 2 (never), lockdown_effect 0.3 vs 0.8.

Time unit: days. Synthetic, illustrative only - not for public-health use.

supply_chain

Four-stage supply chain and the bullwhip effect (the "beer game").

Customers buy from a retailer, which orders from a wholesaler, which orders from a distributor, which orders from a factory. Every day each stage ships what it can (unfilled orders are backlogged), and orders up to a target based on its own demand forecast (exponential smoothing). Shipments take lead_time days. Small swings in customer demand become large swings in orders upstream - the bullwhip effect - because each stage reacts to the orders of the stage below, not to real demand. Sharing point-of-sale data (information_sharing) lets every stage forecast from customer demand.

Try: lead_time 1 vs 5, smoothing 0.1 vs 0.8, information_sharing false vs true, demand_shock 0 vs 0.3, safety_factor 0 vs 2.

Time unit: days. Synthetic data only.

ride_hailing

Ride-hailing: matching, pickup times, cancellations and surge pricing.

Riders request trips at a rate that peaks in the morning and evening rush. The nearest idle driver is assigned; with few idle drivers the nearest one is far away, so pickups take longer, which keeps drivers busy for longer and leaves even fewer idle - a vicious circle (the "wild goose chase"). Riders cancel if no driver is found within their patience or the pickup estimate is too long. Optional surge pricing raises the price as the share of busy drivers climbs: some riders decline the higher price, and more drivers log on.

Try: drivers 40 vs 90, rush_multiplier 1 vs 3, surge false vs true, price_elasticity 0.3 vs 2, max_pickup_eta 8 vs 20.

Time unit: minutes over one day. Synthetic data only.

cloud_autoscaling

Cloud service autoscaling under bursty traffic.

Requests arrive at a base rate with random traffic bursts. Each server handles one request at a time; a request that waits longer than timeout fails. Every check_interval seconds an autoscaler compares load (busy + queued requests per server) with its thresholds: above scale_up_at it launches a server, which only starts serving after spin_up seconds (a cold start); below scale_down_at it removes one. Servers cost money while they are provisioned.

Try: spin_up 10 vs 120, min_servers 2 vs 8, scale_step 1 vs 4, burst_multiplier 1 vs 6, check_interval 5 vs 60.

Time unit: seconds. Synthetic data only.

traffic_signal

Signalised intersection: cycle length, green split and actuated control.

Vehicles arrive on a north-south and an east-west approach and queue at a signal. During green, queued vehicles leave one per saturation_headway seconds; every phase change loses lost_time / 2 seconds (amber and all-red). With fixed timing the cycle repeats with greens split by green_split; delay is U-shaped in the cycle length: short cycles waste time switching, long ones make vehicles wait through long reds. Webster's classic formula for fixed-time delay is reported alongside for comparison. With control = "actuated" the green stays while vehicles keep arriving (between min_green and max_green) and switches when the queue clears.

Try: cycle 30 vs 60 vs 150, green_split 0.3 vs 0.6, flow_ns up to 900, control fixed vs actuated.

Time unit: seconds. Flows in vehicles per hour. Synthetic data only.

webster_delay

webster_delay(flow_vph: float, green: float, cycle: float, headway: float) -> float

Webster (1958) average delay per vehicle at a fixed-time signal (NaN if oversaturated).