Built-in model gallery¶
SimulSI ships fifteen ready-to-run models. Each is a normal Model with
documented parameters, key outputs and presets: named what-if
scenarios that show how the system reacts when you change something. Use
them to learn the library, to teach, or as starting points for your own
models.
simulsi models # list them
simulsi models airline # parameters, presets, outputs
simulsi whatif builtin:airline --presets # run every preset, compare
simulsi whatif builtin:traffic_signal --vary cycle=30,60,90,120 # sweep a parameter
simulsi whatif builtin:epidemic --vary r0=1.5,2.5 --vary vaccination=0,0.4 # full grid
In the dashboard (simulsi ui), pick a model on Run experiment and click
Load what-if presets, or use Explore for grid sweeps, sensitivity and
Monte Carlo. From Python:
from simulsi import Experiment
from simulsi.models import supply_chain
result = Experiment(supply_chain, supply_chain.preset_scenarios(), replications=3, seed=1).run()
print(result.format_summary(["bullwhip.factory", "fill_rate"]))
The tables below come from simulsi whatif ... --presets (seed 0, common
random numbers; ± is the 95% CI half-width). All data is synthetic, so
read the tables for direction and size of effects, not as forecasts.
| Model | What it shows | Time unit |
|---|---|---|
emergency_department |
triage, acuity priority, boarding, fast track, diversion | minutes |
restaurant |
table mix, flexible seating, reservations, kitchen | minutes |
ev_charging |
fast/slow chargers under a shared grid limit | minutes |
inventory |
the (s, S) reorder policy | days |
theme_park |
express passes, smart routing, ride capacity | minutes |
warehouse |
discrete vs batch picking, packing, truck cut-offs | minutes |
mmc |
the M/M/c queue, checked against Erlang C | any |
airline |
delay propagation through aircraft rotations, crews and gates | minutes |
airport_turnaround |
parallel ground-handling tasks, shared crews, the critical path | minutes |
disruption_recovery |
delay vs cancel vs spare aircraft after a hub storm | minutes |
epidemic |
stochastic SEIR with hospital overflow and lockdown policy | days |
supply_chain |
the bullwhip effect in a four-stage supply chain | days |
ride_hailing |
pickup-time feedback, cancellations and surge pricing | minutes |
cloud_autoscaling |
bursty traffic, cold starts and autoscaling policies | seconds |
traffic_signal |
signal timing, Webster's formula, actuated control | seconds |
mmc¶
Poisson arrivals, exponential service, servers identical servers. The
simulated mean wait matches the Erlang C formula (simulsi.models.erlang_c);
the test suite checks this. Presets: busy (utilization 0.97: waits
explode), two_servers and pooled_three (the same utilization with more,
pooled servers: much shorter waits).
airline¶
A fleet of aircraft flies rotations through a hub. Late inbound aircraft and crews that connect from other aircraft pass delay on to the next departure (reactionary delay). Gates are held from arrival to pushback, spare aircraft rescue badly delayed legs, and a weather window cuts the runway rate.
simulsi whatif builtin:airline --presets -r 8:
| scenario | on-time performance | mean arrival delay (min) | reactionary share | mean gate wait (min) |
|---|---|---|---|---|
| baseline (15 min buffer) | 0.65 ± 0.06 | 15.9 ± 3.3 | 0.63 | 0.7 |
| tight_schedule (0 buffer) | 0.32 ± 0.02 | 36.4 ± 3.5 | 0.85 | 0.4 |
| padded_schedule (30 min) | 0.79 ± 0.04 | 11.1 ± 3.4 | 0.35 | 2.4 |
| crew_chaos | 0.63 ± 0.06 | 17.3 ± 3.5 | 0.68 | 0.6 |
| no_spares | 0.63 ± 0.06 | 20.8 ± 5.1 | 0.69 | 0.6 |
| thunderstorm | 0.61 ± 0.05 | 18.4 ± 3.6 | 0.66 | 0.6 |
| storm_with_padding | 0.79 ± 0.03 | 9.7 ± 1.5 | 0.30 | 2.5 |
| gate_crunch (4 gates, padded) | 0.50 ± 0.09 | 24.2 ± 7.3 | 0.77 | 17.1 |
Without a buffer most delay is reactionary: one disruption ripples through the rest of the day. Padding fixes that, but padded aircraft sit at gates longer, so with too few gates the gain turns into gate queues.
airport turnaround¶
Every arriving aircraft needs deboarding, cleaning, catering, fuelling,
baggage unloading and loading, boarding and a pushback tug. Some tasks run
in parallel, some wait for others, and all aircraft on the ground share the
same crews and vehicles. critical.<task> counts how often each task was
the last one holding up boarding or pushback.
simulsi whatif builtin:airport_turnaround --presets -r 6:
| scenario | on-time | mean departure delay (min) | fuel critical | baggage critical |
|---|---|---|---|---|
| baseline (6 banks) | 0.93 ± 0.02 | 7.3 | 4 | 7.8 |
| depeaked | 0.94 ± 0.03 | 7.1 | 4 | 0.3 |
| fuel_with_passengers | 0.93 ± 0.02 | 7.2 | 0 | 8.3 |
| one_fuel_truck_short | 0.93 ± 0.03 | 7.2 | 10 | 6.7 |
| extra_baggage_teams | 0.93 ± 0.03 | 7.2 | 5.3 | 0.7 |
| tight_turns (45 min) | 0.85 ± 0.04 | 10.0 | 5 | 7.2 |
| big_banks (3 banks) | 0.31 ± 0.02 | 31.8 | 58 | 74 |
With enough crews the critical path is boarding itself. Squeeze the same 90 flights into three big waves and fuel trucks and baggage teams become the critical path for most turnarounds; on-time performance collapses.
disruption recovery¶
A storm closes the hub to departures; afterwards the runway works through
the backlog. Operations control either delays everything, cancels round
trips that would leave more than cancel_threshold late, or puts spare
aircraft on them.
simulsi whatif builtin:disruption_recovery --presets -r 6:
| scenario | on-time | passenger delay (h) | cancelled legs | spare trips | recovery (min) | cost |
|---|---|---|---|---|---|---|
| baseline: 150 min storm, delay all | 0.57 | 6,861 | 0 | 0 | 387 | 274k |
| cancel_policy | 0.73 | 13,210 | 12 | 0 | 215 | 798k |
| spares_policy | 0.63 | 5,945 | 0 | 2 | 321 | 238k |
| long_storm_delay (240 min) | 0.25 | 27,050 | 0 | 0 | 547 | 1,082k |
| long_storm_cancel | 0.75 | 29,040 | 32 | 0 | 0 | 1,882k |
| long_storm_spares (4 spares) | 0.50 | 18,130 | 0 | 8 | 543 | 725k |
| no_storm | 1.00 | 320 | 0 | 0 | 0 | 13k |
Cancelling makes the schedule look good (best on-time rate, fastest
recovery) but strands passengers, so total passenger delay and cost are
worst. Spare aircraft are the cheapest recovery here. Which policy is
"best" depends on the objective - a natural case for
Pareto analysis:
simulsi pareto builtin:disruption_recovery -o cost:min -o otp:max --vary policy=delay,cancel,spares --vary spares=1,2,4.
epidemic¶
Individuals move Susceptible → Exposed → Infectious → Removed one event at
a time (Gillespie's algorithm), so outbreaks differ run to run and small
ones can die out. Some cases need a bed; patients who cannot get one within
bed_patience days go untreated with a higher fatality rate. A lockdown
starts at lockdown_trigger bed occupancy.
simulsi whatif builtin:epidemic --presets -r 5:
| scenario | attack rate | peak infectious | deaths | untreated | lockdown days |
|---|---|---|---|---|---|
| baseline (R0 2.5) | 0.76 ± 0.06 | 425 | 9.2 | 0 | 37 |
| mild_strain (R0 1.3) | 0.39 ± 0.12 | 103 | 3.8 | 0 | 0 |
| aggressive_strain (R0 4) | 0.86 ± 0.02 | 573 | 13.2 | 10.8 | 51 |
| vaccinated_60pct | 0.02 ± 0.03 | 12 | 0.4 | 0 | 0 |
| no_lockdown | 0.90 ± 0.00 | 719 | 24.8 | 43.8 | 0 |
| early_lockdown | 0.69 ± 0.03 | 193 | 8.8 | 0 | 103 |
| surge_beds (80 beds) | 0.90 ± 0.00 | 719 | 12.6 | 0 | 0 |
Vaccinating 60% pushes the effective R0 below 1, so outbreaks die out. Without a lockdown the hospital overflows and deaths roughly double; doubling beds removes the overflow instead, at the cost of the full epidemic.
supply chain¶
The "beer game": customers → retailer → wholesaler → distributor →
factory. Each stage forecasts from the orders it receives and orders up to
a target, so demand noise is amplified upstream (the bullwhip effect).
bullwhip.<stage> is the variance of that stage's orders divided by the
variance of customer demand.
simulsi whatif builtin:supply_chain --presets -r 5:
| scenario | bullwhip at retailer | bullwhip at factory | fill rate | cost per day |
|---|---|---|---|---|
| baseline | 4.1 ± 0.3 | 34 ± 2 | 1.00 | 1,155 |
| long_lead_times (5 days) | 9.0 ± 0.6 | 38 ± 2 | 0.995 | 3,030 |
| jumpy_forecasts (alpha 0.8) | 16.7 ± 2.5 | 38 ± 2 | 0.999 | 6,719 |
| shared_pos_data | 4.1 ± 0.3 | 17.0 ± 1.6 | 1.00 | 620 |
| demand_shock (+30%) | 3.4 ± 0.2 | 18.7 ± 0.9 | 1.00 | 1,406 |
| lean_no_safety | 3.7 ± 0.1 | 29 ± 3 | 0.999 | 715 |
| tight_factory | 3.7 ± 0.2 | 0.3 ± 0.2 | 0.50 | 10,750 |
Sharing point-of-sale data halves the bullwhip at the factory and halves costs. A capacity-limited factory cannot amplify anything (its orders are capped) but leaves half of customer demand unmet.
ride hailing¶
Riders arrive with morning and evening peaks. Pickup time grows like
1 / sqrt(idle drivers), so a busy fleet spends more time driving to
pickups, which keeps it busy: the "wild goose chase". Riders give up when
no driver is found quickly or the pickup estimate is too long. Surge
pricing raises the price as the busy share of drivers climbs: some riders
decline, more drivers log on.
simulsi whatif builtin:ride_hailing --presets -r 4:
| scenario | service level | mean pickup (min) | surge (mean) | driver earnings / h |
|---|---|---|---|---|
| baseline (60 drivers) | 0.66 ± 0.03 | 8.7 | 1.00 | 15.9 |
| small_fleet (40) | 0.45 ± 0.01 | 9.5 | 1.00 | 16.3 |
| big_fleet (90) | 0.86 ± 0.02 | 5.6 | 1.00 | 14.0 |
| surge_pricing | 0.69 ± 0.02 | 6.3 | 1.27 | 20.3 |
| concert_night | 0.52 ± 0.02 | 8.7 | 1.00 | 15.9 |
| concert_night_with_surge | 0.58 ± 0.01 | 6.7 | 1.34 | 23.0 |
| impatient_riders | 0.65 ± 0.02 | 6.7 | 1.00 | 15.9 |
Surge pricing serves more riders even though some decline the price, because the extra drivers and the shed demand break the long-pickup spiral.
cloud autoscaling¶
Requests arrive at a base rate with random traffic bursts; each server
handles one at a time and requests time out after timeout seconds. Every
check_interval the autoscaler adds scale_step servers (ready only after
spin_up seconds) or removes one.
simulsi whatif builtin:cloud_autoscaling --presets -r 4:
| scenario | SLO attainment | error rate | p95 latency (s) | cost |
|---|---|---|---|---|
| baseline | 0.86 ± 0.09 | 0.012 | 1.39 | 0.81 |
| slow_cold_start (120 s) | 0.85 ± 0.14 | 0.008 | 1.34 | 0.94 |
| instant_start (5 s) | 0.87 ± 0.06 | 0.007 | 1.33 | 0.80 |
| overprovisioned (10 min) | 0.87 ± 0.08 | 0.005 | 1.40 | 1.01 |
| lazy_autoscaler | 0.72 ± 0.21 | 0.051 | 1.79 | 0.66 |
| flash_crowd (6x bursts) | 0.67 ± 0.28 | 0.107 | 1.84 | 0.94 |
| flash_crowd_ready | 0.87 ± 0.08 | 0.021 | 1.31 | 1.10 |
Bursts make the results noisy (look at the intervals): one long burst can dominate an hour. Against a 6x flash crowd, adding one server per check is far too slow; checking every 5 s and adding 4 servers at a time keeps the error rate near the baseline for about a third more cost.
traffic signal¶
Two approaches share a signal. In fixed-time mode the simulated delay can
be checked against Webster's (1958) formula, reported as
webster.delay_<approach>; with control = "actuated" the green extends
while vehicles keep arriving and ends when the queue clears.
simulsi whatif builtin:traffic_signal --vary cycle=30,45,60,90,120,180 -r 4:
| cycle (s) | simulated mean delay (s) | Webster, north-south (s) |
|---|---|---|
| 30 | 14.8 ± 2.2 | 16.7 |
| 45 | 15.2 ± 0.8 | 14.3 |
| 60 | 16.9 ± 0.8 | 15.5 |
| 90 | 22.0 ± 1.5 | 19.2 |
| 120 | 26.3 ± 1.5 | 23.5 |
| 180 | 36.6 ± 3.4 | 32.2 |
Presets (one run, seed 1): actuated control cuts mean delay from 16.9 s to
12.9 s; at rush hour (flow_ns 850 veh/h) fixed timing is close to
saturation and delay jumps to 40.7 s, while actuated control holds it at
15.5 s. With wrong_split (35% green to the busier approach) that approach
becomes oversaturated: Webster's formula has no answer (NaN) and the queue
grows for the whole hour.
More models¶
Results from simulsi whatif builtin:<model> --presets -r 4 (means; the
CIs are in the command output).
emergency_department - patients are triaged, prioritised by acuity and need a bed and a doctor; admitted patients board in their ED bed until a ward bed frees up. Baseline: 39 min door-to-doctor, 58 patients leaving unseen over two days. An extra doctor cuts door-to-doctor to 24 min and walk-outs to 21. A 40% surge pushes door-to-doctor to 102 min and walk-outs to 195 as boarding (71 min) blocks beds; a fast-track clinician for minor cases roughly halves the walk-outs in a surge.
restaurant - parties need a table of the right size and the kitchen cooks every order. Flexible seating (small parties at larger free tables) does not help here: waits rise from 7.8 to 8.7 min and walk-aways from 18.5 to 20.5, because small parties end up blocking big tables; the kitchen, not the tables, limits revenue in the rush (an extra cook: cook wait 11 -> 3 min, covers 163 -> 170). Heavy reservations cut walk-aways (19 -> 12) but lengthen waits for walk-ins.
ev_charging - with a 600 kW site limit, ten fast chargers on a busy day deliver only 76 kW per car (15% of drivers give up); with a 1,500 kW grid connection the same chargers deliver 147 kW and almost nobody leaves, and revenue rises from 4,900 to 6,050. More chargers without more grid mostly buys queue space, not throughput.
inventory - the classic (s, S) policy: lowering the reorder point from 80
to 40 drops the fill rate from 93% to 84% and raises cost; tiny order
quantities (S = 150) mean twice as many orders and higher cost; a slow
supplier (8-day lead time) cuts the fill rate to 70%. Use optimize(...) or
pareto_search(...) over s and S to find the best policy.
theme_park - express passes keep their holders' waits near zero, but at 40% express the regular visitors wait 85 min instead of 59. App-based smart routing (shortest posted wait) and a bigger coaster both raise rides per visitor; a crowd day cuts rides per visitor from 4.4 to 3.4.
warehouse - batch picking shares walking time: on a peak day (130 orders per hour) discrete picking saturates the pickers and only 27% of orders make their truck, while batches of four get 75% out on time with pickers 70% busy. Batches that are too big (eight) make early orders wait for the batch and hurt on-time shipping on normal days.
How the models are validated¶
Besides directional tests (for example "padding the schedule raises on-time performance"), the test suite checks the models against theory and limiting cases:
mmcagainst the Erlang C formula;epidemicagainst the Kermack-McKendrick final-size equationz = 1 - exp(-R0 z)(also with vaccination), and dying out when R0 < 1;traffic_signalagainst Webster's delay formula;cloud_autoscalingwith no queueing against the service-time distribution's tail;airline,airport_turnaround,disruption_recovery,supply_chainandride_hailingin their no-disruption / unlimited-capacity limits (no cancellations, no backorders, everyone served, no waiting for crews).
Writing your own¶
Each model is a single file in
src/simulsi/models/:
a build function plus a Model(...) with parameters, outputs and
presets={name: overrides}. Copy one as a template; the
core concepts explain the building blocks.