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.
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, weatherairport_turnaround- parallel ground-handling tasks, shared crews, banked schedulesdisruption_recovery- after a hub storm: delay vs cancel vs spare aircraftemergency_department- triage, acuity, beds, doctors, boarding and diversionrestaurant- table mix, flexible seating, reservations and the kitchenev_charging- fast/slow chargers under a shared grid limitinventory- the (s, S) reorder policy and its coststheme_park- ride queues, express passes and app-based routingwarehouse- discrete vs batch picking, packing and truck cut-offsepidemic- stochastic SEIR with hospital capacity and a lockdown policysupply_chain- four-stage beer game and the bullwhip effectride_hailing- matching, pickup-time feedback, cancellations, surge pricingcloud_autoscaling- bursty traffic, cold starts, timeouts, autoscalingtraffic_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).