Note

You can download this demonstration as a Jupyter Notebook here

Randomness and reproducibility

Random numbers and stochastic processes are essential to most agent-based models. Pseudo-random number generators can be used to create numbers in a sequence that appears random but is actually a deterministic sequence based on an initial seed value. In other words, the generator will produce the same pseudo-random sequence over multiple runs if it is given the same seed at the beginning. Note that is possible that the generators will draw the same number repeatedly, as illustrated in this comic strip from Scott Adams:

Alt text

[1]:
import agentpy as ap
import numpy as np
import random

Random number generators

Agentpy models contain two internal pseudo-random number generators with different features:

  • Model.random is an instance of random.Random (more info here)

  • Model.nprandom is an instance of numpy.random.Generator (more info here)

To illustrate, let us define a model that uses both generators to draw a random integer:

[2]:
class RandomModel(ap.Model):

    def setup(self):
        self.x = self.random.randint(0, 99)
        self.y = self.nprandom.integers(99)
        self.report(['x', 'y'])
        self.stop()

If we run this model multiple times, we will likely get a different series of numbers in each iteration:

[3]:
exp = ap.Experiment(RandomModel, iterations=5)
results = exp.run()
Scheduled runs: 5
Completed: 5, estimated time remaining: 0:00:00
Experiment finished
Run time: 0:00:00.027836
[4]:
results.reporters
[4]:
seed x y
iteration
0 163546198553218547629179155646693947592 75 1
1 248413101981860191382115517400004092470 57 61
2 71182126006424514048330534400698800795 96 37
3 319505356893330694850769146666666339584 89 95
4 64281825103124977892605409325092957646 37 84

Defining custom seeds

If we want the results to be reproducible, we can define a parameter seed that will be used automatically at the beginning of a simulation to initialize both generators.

[5]:
parameters = {'seed': 42}
exp = ap.Experiment(RandomModel, parameters, iterations=5)
results = exp.run()
Scheduled runs: 5
Completed: 5, estimated time remaining: 0:00:00
Experiment finished
Run time: 0:00:00.039785

By default, the experiment will use this seed to generate different random seeds for each iteration:

[6]:
results.reporters
[6]:
seed x y
iteration
0 252336560693540533935881068298825202077 26 68
1 47482295457342411543800303662309855831 70 9
2 252036172554514852379917073716435574953 58 66
3 200934189435493509245876840523779924304 48 77
4 31882839497307630496007576300860674457 94 65

Repeating this experiment will yield the same results:

[7]:
exp2 = ap.Experiment(RandomModel, parameters, iterations=5)
results2 = exp2.run()
Scheduled runs: 5
Completed: 5, estimated time remaining: 0:00:00
Experiment finished
Run time: 0:00:00.047647
[8]:
results2.reporters
[8]:
seed x y
iteration
0 252336560693540533935881068298825202077 26 68
1 47482295457342411543800303662309855831 70 9
2 252036172554514852379917073716435574953 58 66
3 200934189435493509245876840523779924304 48 77
4 31882839497307630496007576300860674457 94 65

Alternatively, we can set the argument randomize=False so that the experiment will use the same seed for each iteration:

[9]:
exp3 = ap.Experiment(RandomModel, parameters, iterations=5, randomize=False)
results3 = exp3.run()
Scheduled runs: 5
Completed: 5, estimated time remaining: 0:00:00
Experiment finished
Run time: 0:00:00.021621

Now, each iteration yields the same results:

[10]:
results3.reporters
[10]:
seed x y
iteration
0 42 35 39
1 42 35 39
2 42 35 39
3 42 35 39
4 42 35 39

Sampling seeds

For a sample with multiple parameter combinations, we can treat the seed like any other parameter. The following example will use the same seed for each parameter combination:

[11]:
parameters = {'p': ap.Values(0, 1), 'seed': 0}
sample1 = ap.Sample(parameters, randomize=False)
list(sample1)
[11]:
[{'p': 0, 'seed': 0}, {'p': 1, 'seed': 0}]

If we run an experiment with this sample, the same iteration of each parameter combination will have the same seed (remember that the experiment will generate different seeds for each iteration by default):

[12]:
exp = ap.Experiment(RandomModel, sample1, iterations=2)
results = exp.run()
Scheduled runs: 4
Completed: 4, estimated time remaining: 0:00:00
Experiment finished
Run time: 0:00:00.052923
[13]:
results.reporters
[13]:
seed x y
sample_id iteration
0 0 302934307671667531413257853548643485645 68 31
1 328530677494498397859470651507255972949 55 30
1 0 302934307671667531413257853548643485645 68 31
1 328530677494498397859470651507255972949 55 30

Alternatively, we can use Sample with randomize=True (default) to generate random seeds for each parameter combination in the sample.

[14]:
sample3 = ap.Sample(parameters, randomize=True)
list(sample3)
[14]:
[{'p': 0, 'seed': 302934307671667531413257853548643485645},
 {'p': 1, 'seed': 328530677494498397859470651507255972949}]

This will always generate the same set of random seeds:

[15]:
sample3 = ap.Sample(parameters)
list(sample3)
[15]:
[{'p': 0, 'seed': 302934307671667531413257853548643485645},
 {'p': 1, 'seed': 328530677494498397859470651507255972949}]

An experiment will now have different results for every parameter combination and iteration:

[16]:
exp = ap.Experiment(RandomModel, sample3, iterations=2)
results = exp.run()
Scheduled runs: 4
Completed: 4, estimated time remaining: 0:00:00
Experiment finished
Run time: 0:00:00.050806
[17]:
results.reporters
[17]:
seed x y
sample_id iteration
0 0 189926022767640608296581374469671322148 53 18
1 179917731653904247792112551705722901296 3 60
1 0 255437819654147499963378822313666594855 83 62
1 68871684356256783618296489618877951982 80 68

Repeating this experiment will yield the same results:

[18]:
exp = ap.Experiment(RandomModel, sample3, iterations=2)
results = exp.run()
Scheduled runs: 4
Completed: 4, estimated time remaining: 0:00:00
Experiment finished
Run time: 0:00:00.037482
[19]:
results.reporters
[19]:
seed x y
sample_id iteration
0 0 189926022767640608296581374469671322148 53 18
1 179917731653904247792112551705722901296 3 60
1 0 255437819654147499963378822313666594855 83 62
1 68871684356256783618296489618877951982 80 68

Stochastic methods of AgentList

Let us now look at some stochastic operations that are often used in agent-based models. To start, we create a list of five agents:

[20]:
model = ap.Model()
agents = ap.AgentList(model, 5)
[21]:
agents
[21]:
AgentList (5 objects)

If we look at the agent’s ids, we see that they have been created in order:

[22]:
agents.id
[22]:
[1, 2, 3, 4, 5]

To shuffle this list, we can use AgentList.shuffle:

[23]:
agents.shuffle().id
[23]:
[3, 2, 1, 4, 5]

To create a random subset, we can use AgentList.random:

[24]:
agents.random(3).id
[24]:
[2, 1, 4]

And if we want it to be possible to select the same agent more than once:

[25]:
agents.random(6, replace=True).id
[25]:
[5, 3, 2, 5, 2, 3]

Agent-specific generators

For more advanced applications, we can create separate generators for each object. We can ensure that the seeds of each object follow a controlled pseudo-random sequence by using the models’ main generator to generate the seeds.

[26]:
class RandomAgent(ap.Agent):

    def setup(self):
        seed = self.model.random.getrandbits(128) # Seed from model
        self.random = random.Random(seed)  # Create agent generator
        self.x = self.random.random()  # Create a random number

class MultiRandomModel(ap.Model):

    def setup(self):
        self.agents = ap.AgentList(self, 2, RandomAgent)
        self.agents.record('x')
        self.stop()
[27]:
parameters = {'seed': 42}
exp = ap.Experiment(
    MultiRandomModel, parameters, iterations=2,
    record=True, randomize=False)
results = exp.run()
Scheduled runs: 2
Completed: 2, estimated time remaining: 0:00:00
Experiment finished
Run time: 0:00:00.033219
[28]:
results.variables.RandomAgent
[28]:
x
iteration obj_id t
0 1 0 0.414688
2 0 0.591608
1 1 0 0.414688
2 0 0.591608

Alternatively, we can also have each agent start from the same seed:

[29]:
class RandomAgent2(ap.Agent):

    def setup(self):
        self.random = random.Random(self.p.agent_seed)  # Create agent generator
        self.x = self.random.random()  # Create a random number

class MultiRandomModel2(ap.Model):

    def setup(self):
        self.agents = ap.AgentList(self, 2, RandomAgent2)
        self.agents.record('x')
        self.stop()
[30]:
parameters = {'agent_seed': 42}
exp = ap.Experiment(
    MultiRandomModel2, parameters, iterations=2,
    record=True, randomize=False)
results = exp.run()
Scheduled runs: 2
Completed: 2, estimated time remaining: 0:00:00
Experiment finished
Run time: 0:00:00.033855
[31]:
results.variables.RandomAgent2
[31]:
x
iteration obj_id t
0 1 0 0.639427
2 0 0.639427
1 1 0 0.639427
2 0 0.639427