Discrete-event simulation engine for the BEAM.
Lightweight processes as entities, ETS-based shared topology, barrier synchronization, streaming Welford statistics. Zero runtime dependencies.
# M/M/1 queue: Poisson arrivals, exponential service
{:ok, result} = Sim.run(
entities: [
{:arrivals, Sim.Source, %{id: :arrivals, target: :server,
interarrival: {:exponential, 1.0}, seed: 42}},
{:server, Sim.Resource, %{id: :server, capacity: 1,
service: {:exponential, 0.5}, seed: 99}}
],
initial_events: [{0.0, :arrivals, :generate}],
stop_time: 10_000.0
)
result.stats[:server]
# %{arrivals: 10042, departures: 10041, mean_wait: 0.49, ...}Sim.Clock ──── advances virtual time event-by-event
│
Sim.Calendar ─ priority queue (:gb_trees, FIFO tie-breaking)
│
Sim.EntityManager ─ registry + dispatch
│
├── Sim.Entity ─── @behaviour: init/1, handle_event/3, statistics/1
├── Sim.Resource ── capacity-limited server with FIFO queue
├── Sim.Source ──── arrival generator (exponential, constant)
└── Sim.PHOLD ──── standard DES benchmark entity
Sim.Topology ──── ETS shared state (networks, occupancy, routing)
Sim.Statistics ── Welford streaming mean/variance + batch means CI
Sim.Experiment ── replications, CRN, paired comparison
- Process = entity (InterSCSimulator pattern). Inline mode for speed, process mode for scale.
- ETS for topology — shared reads are free, writes are rare. No actor-per-link bottleneck.
- Barrier synchronization — all entities complete current event before clock advances. Simple, correct.
- Functional PRNG —
:randstate threaded through entities. Same seed = same trajectory. CRN for free. - Zero runtime dependencies — pure Elixir + OTP for core engines. Rust toolchain for NIF engine. Optional integration with Les Quatre Probabileurs.
GPSS/Arena-inspired syntax that compiles to Entity modules:
defmodule Barbershop do
use Sim.DSL
model :barbershop do
resource :barber, capacity: 1
process :customer do
arrive every: exponential(18.0)
seize :barber
hold exponential(16.0)
release :barber
depart
end
end
end
Barbershop.run(stop_time: 10_000.0, seed: 42)No magic runtime. mix compile generates standard Sim.Entity modules.
Works in engine, ETS, tick-diasca, parallel, and Rust modes.
| Verb | Arena Equivalent | Description |
|---|---|---|
arrive every: |
CREATE | Stationary interarrival |
arrive schedule: |
CREATE (schedule) | Non-stationary (time-varying rate) |
seize :resource |
SEIZE | Request capacity, block if busy |
hold distribution |
DELAY | Consume time |
release :resource |
RELEASE | Free capacity |
route distribution |
ROUTE | Travel delay (no resource) |
decide prob, :label |
DECIDE | Binary probabilistic branch |
decide [{p, :l}, ...] |
DECIDE | Multi-way weighted routing |
batch N |
BATCH | Accumulate N parts |
split N |
SEPARATE | One part becomes N |
combine N |
COMBINE / MATCH | N parts merge into one |
assign :key, value |
ASSIGN | Set attribute on entity instance |
label :name |
STATION | Jump target for decide |
depart |
DISPOSE | Exit, collect statistics |
resource :r, capacity: N |
RESOURCE | Fixed capacity |
resource :r, schedule: [...] |
SCHEDULE | Time-varying capacity by shift |
defmodule Electronics do
use Sim.DSL
model :electronics do
resource :solder, capacity: 8
resource :inspector, schedule: [{0..479, 3}, {480..959, 1}]
resource :rework, capacity: 1
process :pcb do
arrive schedule: [{0..479, {:exponential, 3.0}},
{480..959, {:exponential, 6.0}}]
split 4 # PCB → 4 panels
seize :solder
hold exponential(3.0)
release :solder
decide 0.05, :rework_panel # 5% fail inspection
combine 4 # 4 panels → 1 board
batch 10 # 10 boards → 1 tray
depart
label :rework_panel
seize :rework
hold exponential(6.0)
release :rework
depart
end
end
endCausal ordering via two-level timestamps (Sim-Diasca pattern). Entity at
(T, D) produces events stamped (T, D+1). Tick advances only when no
more diascas are pending (quiescence).
# Entities return tagged events:
{:same_tick, target, payload} # → (T, D+1)
{:tick, future_tick, target, payload} # → (future_tick, 0)
{:delay, delta, target, payload} # → (T + delta, 0)Sim.run(
mode: :diasca,
entities: [...],
initial_events: [{0, :source, :generate}],
stop_tick: 10_000
)88-core Xeon E5-2699 v4, OTP 27, Elixir 1.18.3.
| LPs | Events | Engine E/s | GenServer E/s | Speedup |
|---|---|---|---|---|
| 100 | 162K | 533K | 88K | 6.1x |
| 1,000 | 1.6M | 159K | 57K | 2.8x |
| 10,000 | 16.2M | 140K | 38K | 3.6x |
| Stop tick | Events | Events/sec |
|---|---|---|
| 100K | 35K | 5.3M |
| 1M | 344K | 5.5M |
| 10M | 3.4M | 9.2M |
| Stop tick | Events | Events/sec | Parts |
|---|---|---|---|
| 100K | 750K | 8.3M | 17,862 |
| 1M | 7.4M | 9.0M | 176,580 |
| 10M | 74M | 8.9M | 1,762,873 |
645K events/sec — 11 DSL verbs composing in one model.
# Run benchmarks yourself:
mix run benchmark/phold_bench.exs # PHOLD sweep
mix run benchmark/engine_vs_genserver.exs # Engine vs GenServer A/B
mix run benchmark/ets_ab.exs # ETS vs Map A/B
mix run benchmark/full_bench.exs # Comprehensive (15 min)
mix run benchmark/full_bench.exs -- quick # Quick smoke (60s)
mix run benchmark/dsl_complexity_bench.exs # DSL verb complexity impactImplement the Sim.Entity behaviour:
defmodule MyMachine do
@behaviour Sim.Entity
@impl true
def init(config) do
{:ok, %{id: config.id, processed: 0}}
end
@impl true
def handle_event({:job, job_id}, clock, state) do
# Process the job, schedule completion
finish_time = clock + 5.0
events = [{finish_time, :sink, {:done, job_id}}]
{:ok, %{state | processed: state.processed + 1}, events}
end
@impl true
def statistics(state), do: %{processed: state.processed}
end# 30 independent replications, parallel across cores
results = Sim.Experiment.replicate(fn seed ->
{:ok, r} = Sim.run(my_model(seed))
r.stats[:server]
end, 30, parallel: true)
# Compare two configurations with common random numbers
comparison = Sim.Experiment.compare(
config_a: fn seed -> run_model(config_a, seed) end,
config_b: fn seed -> run_model(config_b, seed) end,
seeds: 1..30,
metric: :mean_wait
)
# => %{mean_diff: -0.12, ci: {-0.18, -0.06}, significant: true}Every DES engine does Monte Carlo: run 1,000 times, sample inputs from a fitted distribution, compute output confidence intervals. The input parameters are treated as known constants. They are not.
sim_ex is the only DES engine where simulation inputs have proper Bayesian posteriors, where the model self-calibrates from streaming data, and where input structure is discovered nonparametrically.
| What others do | What sim_ex + ecosystem does |
|---|---|
| Service time = Exponential(16) | Service time = Exponential(16.2 +/- 1.3) — full posterior from 200 observations (eXMC) |
| Parameters frozen at model build | Parameters track drift in real time from sensor data (smc_ex O-SMC²) |
| Analyst picks which inputs matter | BART discovers which 5 of 50 inputs drive output (StochTree-Ex) |
| Output CI from replications only | Output CI includes epistemic uncertainty over input parameters |
| Simultaneous events: FIFO | Simultaneous events: causal ordering via tick-diasca |
Arena and AnyLogic have 30 years of domain libraries and 3D animation. We don't replace them. We add the statistical brain they don't have.
sim_ex is part of an Elixir probabilistic computing ecosystem:
| Library | What | Role in Simulation |
|---|---|---|
| sim_ex | DES engine | The simulator itself |
| eXMC | NUTS/HMC, ADVI, Pathfinder | Input modeling: posterior over distribution params |
| smc_ex | O-SMC², particle filters | Self-calibrating digital twins |
| StochTree-Ex | BART | Metamodeling: which inputs drive output |
# Optional deps — sim_ex works standalone
def deps do
[
{:sim_ex, "~> 0.1"},
{:exmc, "~> 0.2", optional: true}, # Bayesian input modeling
{:smc_ex, "~> 0.1", optional: true}, # Online calibration
{:stochtree_ex, "~> 0.1", optional: true} # Sensitivity analysis
]
endThe Factory That Learns: A Practitioner's Guide to Bayesian Inference for Manufacturing is being written around sim_ex and Les Quatre Probabileurs. First draft complete. Not yet published — early access planned for late 2026. Eight chapters, each an executable Livebook notebook:
- Five Lines That Describe a Factory — DSL, M/M/1 verification, a 200-machine packaging line
- The Number You Don't Know — 1,000 replications in 68 seconds, the analysis Averill Law called "rarely done"
- The Chart That Thinks — Bayesian SPC: 73% the process shifted, not "alarm"
- When Will This Machine Fail? — hierarchical Weibull, 20 component types, censored data
- The Bearing Knows When It Will Die — degradation curves on FEMTO run-to-failure data
- A Hundred Engines, One Model — fleet RUL on NASA C-MAPSS, partial pooling across 100 turbofans
- The Simulation That Learns — all four libraries connected, self-calibrating digital twin
- What to Do Monday Morning — ten steps from "I read this book" to "my factory is learning"
The book is for manufacturing engineers, not Elixir developers.
seize :drill / hold exponential(8.0) / release :drill reads
like the routing sheet already taped to the machine. The posterior
is the part that's new. The factory floor is the part that's familiar.
def deps do
[
{:sim_ex, "~> 0.1.0"}
]
endApache-2.0
