Skip to contents

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 from a file named sim_worker.R in the working directory. 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:

The companion Python package is simstreams-python. The book’s code repository is parallel-simulation-book, and the web edition is at https://parallel-simulation-book.netlify.app.

Installation

# install.packages("remotes")
remotes::install_github("ismayc/simstreams")

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.

library(simstreams)

# Chapter 3: estimate pi with 1,000,000 replications of 100 darts on 4 cores
out <- sim_template(pi_worker, 1234, 100, 1000000, 4)
mean(bind_results(out))
#> [1] 3.141554

# Chapter 3: the Cauchy scale estimator, with theta = 5 passed through `...`
out <- sim_template(iqr_worker, 1234, 100, 20, 4, 5)
lapply(out, round, 3)
#> [[1]]
#> [1] 5.930 4.992 4.470 4.336 4.306
#> ...

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.

Writing your own worker

A worker is a function worker(n, set_up, ...). It seeds its generator from set_up, generates the data for its set_up$n_reps replications, computes the quantity of interest, and returns it. Keep it self-contained: it runs in a fresh process that has never seen your workspace.

mean_worker <- function(n, set_up, ...) {
  set_worker_rng(set_up)              # the process's own PCG64 stream
  mu <- list(...)[[1]]                # model parameters arrive through ...
  x <- matrix(dqrng::dqrnorm(n * set_up$n_reps, mean = mu), nrow = n)
  colMeans(x)                         # one estimate per replication
}

out <- sim_template(mean_worker, 2026, 50, 10000, 4, 10)
summary(bind_results(out))

A worker that calls a package’s functions without the pkg:: prefix needs that package attached on every process: sim_template(..., packages = "matrixStats").

Timing a speed-up experiment

run_pi <- function(n_cores) sim_template(pi_worker, 1234, 100, 1000000, n_cores)
benchmark_cores(run_pi, cores = 1:8, n_runs = 10)

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

devtools::test()

The test suite covers every line of the package, and CI fails below 100%. To measure it locally, set NOT_CRAN so the tests marked skip_on_cran() run (they are the ones that start worker processes):

withr::with_envvar(c(NOT_CRAN = "true"), covr::package_coverage())

License

MIT. See LICENSE.md.