Skip to content

Run a simulation

The general parallel simulation template, with the worker as the first argument, and the two pieces of it that are useful on their own: dividing the replications and building the per-process generators.

simstreams.template

The general parallel simulation template.

The book's sim_template.py reads its worker with from sim_worker import worker. Here the worker is the first argument, and the two pieces of the template that are useful on their own, dividing the replications and building the per-process generators, are separate functions.

split_reps

split_reps(n_reps, n_cores)

Divide n_reps replications evenly across n_cores processes.

Every process gets the same base share, and the remainder is handed out one replication at a time starting with the first process. This is the load-balancing step of the general parallel simulation algorithm.

Returns a list of n_cores integers that sums to n_reps.

Source code in src/simstreams/template.py
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
def split_reps(n_reps, n_cores):
    """Divide ``n_reps`` replications evenly across ``n_cores`` processes.

    Every process gets the same base share, and the remainder is handed out
    one replication at a time starting with the first process. This is the
    load-balancing step of the general parallel simulation algorithm.

    Returns a list of ``n_cores`` integers that sums to ``n_reps``.
    """
    n_reps = int(n_reps)
    n_cores = int(n_cores)
    if n_reps < 1:
        raise ValueError("n_reps must be a positive integer")
    if n_cores < 1:
        raise ValueError("n_cores must be a positive integer")

    base_reps = n_reps // n_cores
    remainder = n_reps % n_cores
    reps = [base_reps] * n_cores

    # load balance
    for i in range(remainder):
        reps[i] += 1

    return reps

make_generators

make_generators(seed, n_cores)

Create one independent PCG64 generator per process.

SeedSequence(seed).spawn(n_cores) gives each process its own child seed, so the master seed fixes every process's stream and the streams do not overlap.

Returns a list of n_cores numpy.random.Generator objects.

Source code in src/simstreams/template.py
41
42
43
44
45
46
47
48
49
50
51
52
def make_generators(seed, n_cores):
    """Create one independent PCG64 generator per process.

    ``SeedSequence(seed).spawn(n_cores)`` gives each process its own child
    seed, so the master ``seed`` fixes every process's stream and the streams
    do not overlap.

    Returns a list of ``n_cores`` ``numpy.random.Generator`` objects.
    """
    ss = SeedSequence(seed)
    child_seeds = ss.spawn(int(n_cores))
    return [default_rng(s) for s in child_seeds]

make_set_up

make_set_up(seed, n, n_reps, n_cores, *args)

Build the argument tuple handed to each process.

Element i is (rg, n, reps[i], args): the process's generator, the per-replication sample size, its share of the replications, and the model parameters as a tuple. A worker is called as worker(*set_up[i]).

Source code in src/simstreams/template.py
55
56
57
58
59
60
61
62
63
64
def make_set_up(seed, n, n_reps, n_cores, *args):
    """Build the argument tuple handed to each process.

    Element ``i`` is ``(rg, n, reps[i], args)``: the process's generator, the
    per-replication sample size, its share of the replications, and the model
    parameters as a tuple. A worker is called as ``worker(*set_up[i])``.
    """
    rg_list = make_generators(seed, n_cores)
    reps = split_reps(n_reps, n_cores)
    return [(rg_list[i], n, reps[i], args) for i in range(len(reps))]

sim_template

sim_template(
    worker, seed, n, n_reps, n_cores, *args, pool=None
)

Run a simulation experiment in parallel.

The general parallel simulation algorithm from the book: choose a master seed, give each process an independent PCG64 stream, divide the replications evenly, run the worker on every process, and return the combined results.

Parameters:

Name Type Description Default
worker callable

worker(rg, n, n_reps, param_tuple). rg is the process's numpy.random.Generator, n the per-replication sample size, n_reps the process's share of the replications, and param_tuple the model parameters passed as *args. The worker must be importable by the child processes (defined at module level, not a lambda or a closure) and self-contained.

required
seed int

Master seed.

required
n int

Sample size for each replication.

required
n_reps int

Total number of replications.

required
n_cores int

Number of worker processes.

required
*args

Model parameters passed on to the worker.

()
pool Pool

An existing pool to reuse. When omitted, a pool of n_cores processes is created for the call and closed afterwards.

None

Returns:

Type Description
list

One element per process, in process order: whatever the worker returned for its replications.

Notes

Results are reproducible for a fixed seed and a fixed n_cores. Changing the number of processes changes how the replications are divided and therefore which stream produces which draw.

On macOS and Windows, child processes are spawned, so the calling script needs an if __name__ == "__main__": guard.

Source code in src/simstreams/template.py
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
def sim_template(worker, seed, n, n_reps, n_cores, *args, pool=None):
    """Run a simulation experiment in parallel.

    The general parallel simulation algorithm from the book: choose a master
    seed, give each process an independent PCG64 stream, divide the
    replications evenly, run the worker on every process, and return the
    combined results.

    Parameters
    ----------
    worker : callable
        ``worker(rg, n, n_reps, param_tuple)``. ``rg`` is the process's
        ``numpy.random.Generator``, ``n`` the per-replication sample
        size, ``n_reps`` the process's share of the replications, and
        ``param_tuple`` the model parameters passed as ``*args``. The worker
        must be importable by the child processes (defined at module level,
        not a lambda or a closure) and self-contained.
    seed : int
        Master seed.
    n : int
        Sample size for each replication.
    n_reps : int
        Total number of replications.
    n_cores : int
        Number of worker processes.
    *args
        Model parameters passed on to the worker.
    pool : multiprocessing.pool.Pool, optional
        An existing pool to reuse. When omitted, a pool of ``n_cores``
        processes is created for the call and closed afterwards.

    Returns
    -------
    list
        One element per process, in process order: whatever the worker
        returned for its replications.

    Notes
    -----
    Results are reproducible for a fixed ``seed`` **and** a fixed
    ``n_cores``. Changing the number of processes changes how the
    replications are divided and therefore which stream produces which draw.

    On macOS and Windows, child processes are spawned, so the calling script
    needs an ``if __name__ == "__main__":`` guard.
    """
    if not callable(worker):
        raise TypeError("worker must be a callable worker(rg, n, n_reps, param_tuple)")

    set_up = make_set_up(seed, n, n_reps, n_cores, *args)

    if pool is not None:
        return pool.starmap(worker, set_up)

    with Pool(processes=len(set_up)) as own_pool:
        result = own_pool.starmap(worker, set_up)

    return result

sim_template_serial

sim_template_serial(worker, seed, n, n_reps, *args)

Run a simulation experiment in the current process.

The serial counterpart of sim_template: the same seed layout as a one-process run, executed without a pool. Useful for debugging a worker before running it in parallel, and for the one-core baseline of a timing experiment. The result equals sim_template(worker, seed, n, n_reps, 1, *args)[0].

Source code in src/simstreams/template.py
127
128
129
130
131
132
133
134
135
136
137
138
139
140
def sim_template_serial(worker, seed, n, n_reps, *args):
    """Run a simulation experiment in the current process.

    The serial counterpart of ``sim_template``: the same seed layout as a
    one-process run, executed without a pool. Useful for debugging a worker
    before running it in parallel, and for the one-core baseline of a timing
    experiment. The result equals
    ``sim_template(worker, seed, n, n_reps, 1, *args)[0]``.
    """
    if not callable(worker):
        raise TypeError("worker must be a callable worker(rg, n, n_reps, param_tuple)")

    (set_up,) = make_set_up(seed, n, n_reps, 1, *args)
    return worker(*set_up)