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:
[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 ofrandom.Random
(more info here)Model.nprandom
is an instance ofnumpy.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 |