simstreams
¶
Run simulation experiments in parallel on a multicore personal computer.
simstreams generalizes the sim_template program developed in the book
Parallel Simulation Experiments on a Personal Computer by R. L. Eubank and
Chester Ismay (CRC Press). The book's template reads its worker with
from sim_worker import worker. In the package the worker is an argument, so
any number of workers can live side by side, and the pieces of the template
are available on their own:
sim_template()runs a worker acrossn_coresprocesses, each with its own independent PCG64 stream from a spawnedSeedSequence.sim_template_serial()runs the same experiment in the current process, for debugging a worker and for the one-core baseline of a timing study.make_generators(),split_reps(), andmake_set_up()are the building blocks: the per-process generators, the even division of the replications, and the argument tuple each process receives.pi_worker()andcauchy_worker()are the book's example workers (Chapter 4).sim_template_coupled()runs workers that communicate through shared mailboxes and a barrier, andsimstreams.epidemicis the Chapter 6 SIRD case study with migration built on it.benchmark()andbenchmark_cores()time a simulation across core counts and report the speed-up (Chapter 7).PCG8is the toy 8-bit generator of Appendix C.
The companion R package is simstreams. The book's code repository is parallel-simulation-book, and the web edition is at https://parallel-simulation-book.netlify.app.
Installation¶
pip install git+https://github.com/ismayc/simstreams-python
Plotting the epidemic needs matplotlib: add [plot], as in
pip install "simstreams[plot] @ git+https://github.com/ismayc/simstreams-python".
Usage¶
The call mirrors the book: the seed, the sample size for each replication, the number of replications, the number of processes, and then any model parameters. The one difference is that the worker comes first.
import numpy as np
from simstreams import sim_template, pi_worker, cauchy_worker
if __name__ == "__main__":
# Chapter 4: estimate pi with 1,000,000 replications of 100 darts on 4 cores
out = sim_template(pi_worker, 1234, 100, 1_000_000, 4)
print(np.concatenate(out).mean())
# 3.14141632
# Chapter 4: the Cauchy scale estimator, with theta = 5 as a model parameter
out = sim_template(cauchy_worker, 1234, 100, 20, 4, 5)
print(np.round(out, 3))
# [[5.621 5.955 5.26 4.612 5.325]
# ...
Those are the numbers printed in the book. The package's tests pin them, so a change that alters a published result fails the test suite.
The if __name__ == "__main__": guard matters on macOS and Windows, where
child processes are spawned by importing the calling script.
Writing your own worker¶
A worker is a function worker(rg, n, n_reps, param_tuple). It draws from
rg, the process's own generator, generates the data for its n_reps
replications, computes the quantity of interest, and returns it. Define it at
module level so the child processes can import it, and keep it
self-contained.
def mean_worker(rg, n, n_reps, param_tuple):
mu = param_tuple[0] # model parameters arrive as a tuple
x = rg.normal(loc=mu, size=(n, n_reps)) # replications in columns
return x.mean(axis=0) # one estimate per replication
if __name__ == "__main__":
out = sim_template(mean_worker, 2026, 50, 10_000, 4, 10.0)
print(np.concatenate(out).std())
The epidemic case study¶
from simstreams import epidemic_driver
if __name__ == "__main__":
# 4 towns, 30 days, 20 replications, 10 migrants a day, one infection
# probability per town
out = epidemic_driver(1234, 30, 20, 4, 1000, 20, 0.05, 5, 3, 10,
0.05, 0.15, 0.25, 0.30)
print([h.max() for h in out]) # peak daily infections in each town
Timing a speed-up experiment¶
from simstreams import benchmark_cores
def run_pi(n_cores):
return sim_template(pi_worker, 1234, 100, 1_000_000, n_cores)
if __name__ == "__main__":
for r in benchmark_cores(run_pi, range(1, 9), repeats=10):
print(r["n_cores"], round(r["median"], 3), round(r["speedup"], 2))
Reproducibility¶
A fixed seed reproduces a run exactly on the same number of cores. Changing
n_cores changes how the replications are divided across the streams, so the
full quartet seed, n, n_reps, and n_cores is what reproduces a result.
Development¶
uv sync --group dev
uv run pytest
The test suite covers every line, and pytest fails below 100% (the gate is
in pyproject.toml). The parallel templates run their workers in child
processes, where coverage is not recorded, so tests/test_inprocess.py calls
the same worker functions directly.
License¶
MIT. See LICENSE.