Coming from SimPy¶
Naga runs on SimPy: every block is a SimPy process and every resource a SimPy resource. What changes is where the model lives. In SimPy the model is your Python code. Here it is a JSON file, and the engine supplies the processes, the random streams, the replications and the statistics.
The same model twice¶
A single-server queue: arrivals at 1 a minute, service at 1.25 a minute, 10 000 minutes of which the first 1 000 are thrown away, five replications.
In SimPy you write the process, the monitoring and the replication loop:
import random
import statistics
import simpy
def customer(env, server, times, warmup):
arrived = env.now
with server.request() as request:
yield request
yield env.timeout(random.expovariate(1.25))
if arrived >= warmup:
times.append(env.now - arrived)
def source(env, server, times, warmup):
while True:
yield env.timeout(random.expovariate(1.0))
env.process(customer(env, server, times, warmup))
means = []
for seed in range(5):
random.seed(seed)
env = simpy.Environment()
server = simpy.Resource(env, capacity=1)
times = []
env.process(source(env, server, times, 1000))
env.run(until=10000)
means.append(statistics.mean(times))
print("SimPy: mean time in system", round(statistics.mean(means), 2))
Here you write what the system is:
{
"blocks": [
{"name": "arrive", "type": "CREATE",
"params": {"distribution": "Exponential", "dist_params": {"rate": 1.0}}},
{"name": "serve", "type": "PROCESS",
"params": {"action": "Seize Delay Release", "resource_name": "Server",
"distribution": "Exponential", "dist_params": {"rate": 1.25}}},
{"name": "leave", "type": "DISPOSE", "params": {}}
],
"links": [{"source": "arrive", "target": "serve"}, {"source": "serve", "target": "leave"}],
"resources": [{"name": "Server", "capacity": 1}],
"simulation": {"time_limit": 10000, "warmup": 1000, "replications": 5}
}
from nagasim.api import run_model
kpis = run_model("mm1.json", seed=0).kpis().set_index("metric")
print("Naga: mean time in system", round(kpis.loc["system/Avg Throughput Time", "mean"], 2),
"+/-", round(kpis.loc["system/Avg Throughput Time", "ci95"], 2))
print(kpis.loc[["system/Avg Queue Length", "resource/Server/Usage % (snapshot)"], ["mean", "ci95"]])
Both estimate the same quantity, 4 minutes in theory, from different random numbers. The second one also gave the queue length, the utilisation and their confidence intervals without a line of monitoring code, and the model can be validated, diffed, sent to an optimiser or edited by an agent, because it is data.
What maps to what¶
| In SimPy | Here |
|---|---|
| a generator function per kind of process | blocks joined by links; an entity flows through them |
env.process(...) in an arrival loop |
a CREATE block |
yield env.timeout(...) with your own random draw |
DELAY or PROCESS with a named distribution; every block has its own random stream |
simpy.Resource, with resource.request() |
an entry in resources, and PROCESS, or SEIZE ... RELEASE |
PriorityResource, PreemptiveResource |
priority on the seizing block; preempt on PROCESS |
simpy.Store, FilterStore |
HOLD, BATCH and MATCH, which keep entities until a signal, a condition or a partner |
simpy.Container |
a level with flows: a continuous quantity whose crossings are computed exactly |
env.event(), event.succeed() |
SIGNAL and a HOLD in "Signal" mode; a HOLD in "Condition" mode waits on an expression |
process.interrupt() |
failures on a resource, preemption, a renege time on PROCESS |
| lists you append to, monkey-patched resources | result frames for every block, resource, entity type and queue |
| a loop over seeds | simulation.replications; replication i uses seed + i |
| the same seeds for two variants, by hand | naga scenarios: common random numbers by block name, paired comparison |
env.run(until=...) |
simulation.time_limit, or no limit when arrivals are finite |
What has no equivalent here¶
- Free-form processes. A block does what its type does. Logic that is not a block goes in an
expression (a restricted grammar, never
eval), or in a LOGIC block or a plugin block, which are Python you write and install yourself. simpy.rt. There is no real-time environment.- Your own event loop. You do not get the
Environmentto schedule events on;run_modelowns the run. The agent tools can step a run (debug_run), not drive it.
When raw SimPy is the better choice¶
- The model is mostly control logic: a protocol, a scheduling algorithm, a state machine with many special cases. Python says that better than blocks do.
- You need real time, or to embed the simulation in another program's loop.
- You want one small pure-Python dependency. Naga needs NumPy, pandas, SciPy and openpyxl.
- The model is a few dozen lines and you do not need the statistics around it.
Naga is the better choice when the system is flow through stations and resources, when you want replications and intervals without writing them, when someone else (or an agent) must read and change the model, or when the model also has stocks and flows or agents.
Speed¶
Naga runs on SimPy and does more per event, so on a model both can express it is slower than a bare SimPy loop. This is by how much, on four small queueing models that SimPy, salabim, Ciw and Naga can all express exactly: a single queue, a three-server queue, a line of three stations and a three-station job shop with rework.
Measured 2026-10-13 with Python 3.12.4 on Intel64 Family 6 Model 186 Stepping 2, GenuineIntel, 20 logical CPUs, Windows 11. SimPy 4.1.1, salabim 26.0.8, Ciw 3.2.7, Naga 0.6.0. Each model: 10 replications of 10,000 time units, the first 1,000 discarded, about 100,000 customers in all. CPU seconds, the minimum of 3 runs.
| Model | Library | Mean time in system (95% interval) | Theory | CPU seconds | Customers per second | Times SimPy's |
|---|---|---|---|---|---|---|
mm1: M/M/1 queue |
SimPy | 3.996 +/- 0.174 | 4.000 | 0.58 | 172,973 | 1.0 |
mm1: M/M/1 queue |
salabim | 3.996 +/- 0.174 | 4.000 | 15.53 | 6,439 | 26.9 |
mm1: M/M/1 queue |
Ciw | 4.315 +/- 0.342 | 4.000 | 1.03 | 96,970 | 1.8 |
mm1: M/M/1 queue |
Naga | 3.981 +/- 0.221 | 4.000 | 3.22 | 31,068 | 5.6 |
mm1: M/M/1 queue |
Naga, logs and sampler off | 3.981 +/- 0.221 | 4.000 | 2.47 | 40,506 | 4.3 |
mm3: M/M/3 queue |
SimPy | 6.101 +/- 0.357 | 6.011 | 0.62 | 160,000 | 1.0 |
mm3: M/M/3 queue |
salabim | 6.101 +/- 0.357 | 6.011 | 16.08 | 6,220 | 25.7 |
mm3: M/M/3 queue |
Ciw | 6.049 +/- 0.380 | 6.011 | 1.08 | 92,754 | 1.7 |
mm3: M/M/3 queue |
Naga | 5.943 +/- 0.327 | 6.011 | 3.25 | 30,769 | 5.2 |
mm3: M/M/3 queue |
Naga, logs and sampler off | 5.943 +/- 0.327 | 6.011 | 2.47 | 40,506 | 4.0 |
tandem: Line of 3 stations |
SimPy | 7.418 +/- 0.457 | 7.000 | 1.42 | 70,330 | 1.0 |
tandem: Line of 3 stations |
salabim | 6.930 +/- 0.386 | 7.000 | 20.33 | 4,919 | 14.3 |
tandem: Line of 3 stations |
Ciw | 6.988 +/- 0.399 | 7.000 | 2.97 | 33,684 | 2.1 |
tandem: Line of 3 stations |
Naga | 7.031 +/- 0.233 | 7.000 | 7.34 | 13,617 | 5.2 |
tandem: Line of 3 stations |
Naga, logs and sampler off | 7.031 +/- 0.233 | 7.000 | 5.58 | 17,927 | 3.9 |
jackson: Job shop, 3 stations with rework |
SimPy | 8.457 +/- 0.353 | 8.376 | 1.36 | 73,563 | 1.0 |
jackson: Job shop, 3 stations with rework |
salabim | 8.495 +/- 0.257 | 8.376 | 20.62 | 4,848 | 15.2 |
jackson: Job shop, 3 stations with rework |
Ciw | 8.474 +/- 0.342 | 8.376 | 2.86 | 34,973 | 2.1 |
jackson: Job shop, 3 stations with rework |
Naga | 8.348 +/- 0.208 | 8.376 | 7.33 | 13,646 | 5.4 |
jackson: Job shop, 3 stations with rework |
Naga, logs and sampler off | 8.348 +/- 0.208 | 8.376 | 5.64 | 17,729 | 4.1 |
How to read it:
- The same model, the same run. Poisson arrivals, exponential service, first come first served, the same run length, warm-up and number of replications in every library, each written the way that library is normally used, one short file per library.
- Correct before fast. A library is timed only if the theory's mean time in system lies inside the confidence interval of its own estimate. The random numbers differ between libraries, so the estimates do too.
- What is timed is the CPU time of the whole run in one process: building the model, the replications, and reading the one statistic. "Customers per second" divides the expected number of arrivals, the same for every library, by that time.
- The libraries do not do the same work in that time. The SimPy code keeps one number per
customer and nothing else. salabim keeps monitors on every resource and component. Ciw keeps
a record of every visit to a station. Naga collects the statistics of every block, resource
and queue, logs every entity and resource event, samples a time series, and builds its result
tables for each replication. The row "logs and sampler off" is Naga with
"record_logs": falseand"timeseries_points": 0undersimulation; the summary statistics have no switch. - The limits. One machine, one Python version, small models with nothing but queues. A model with expressions, schedules or failures does more per event and was not measured; nor was memory. Take the ratios, not the seconds. In the salabim version measured, most of the time goes to creating a component per customer, not to its event loop.
The scripts are in the repository's benchmarks/ folder. To measure on your machine:
$ pip install -r benchmarks/requirements.txt
$ python benchmarks/compare.py