Wright-Fisher model of evolution
This is one of the simplest models of population genetics that demonstrates the use of sample!
. We implement a simple case of the model where we study haploids (cells with a single set of chromosomes) while for simplicity, focus only on one locus (a specific gene). In this example we will be dealing with a population of constant size.
It is also available from the Models
module as Models.wright_fisher
.
A neutral model
- Imagine a population of
n
haploid individuals. - At each generation,
n
offsprings replace the parents. - Each offspring chooses a parent at random and inherits its genetic material.
using Agents
numagents = 100
100
Let's define an agent. The genetic value of an agent is a number (trait
field).
@agent struct Haploid(NoSpaceAgent)
trait::Float64
end
The model can be run for many generations and we can collect the average trait value of the population. To do this we will use a model-step function (see step!
) that utilizes sample!
:
modelstep_neutral!(model::ABM) = sample!(model, nagents(model))
modelstep_neutral! (generic function with 1 method)
And make a model without any spatial structure:
model = StandardABM(Haploid; model_step! = modelstep_neutral!)
StandardABM with 0 agents of type Haploid
agents container: Dict
space: nothing (no spatial structure)
scheduler: fastest
Create n
random individuals:
for i in 1:numagents
add_agent!(model, rand(abmrng(model)))
end
To create a new generation, we can use the sample!
function. It chooses random individuals with replacement from the current individuals and updates the model. For example:
sample!(model, nagents(model))
StandardABM with 100 agents of type Haploid
agents container: Dict
space: nothing (no spatial structure)
scheduler: fastest
We can now run the model and collect data. We use dummystep
for the agent-step function (as the agents perform no actions).
using Statistics: mean
data, _ = run!(model, 20; adata = [(:trait, mean)])
data
Row | time | mean_trait |
---|---|---|
Int64 | Float64 | |
1 | 0 | 0.456202 |
2 | 1 | 0.479757 |
3 | 2 | 0.440221 |
4 | 3 | 0.423166 |
5 | 4 | 0.414824 |
6 | 5 | 0.42311 |
7 | 6 | 0.421922 |
8 | 7 | 0.419701 |
9 | 8 | 0.419014 |
10 | 9 | 0.431818 |
11 | 10 | 0.448848 |
12 | 11 | 0.454435 |
13 | 12 | 0.447619 |
14 | 13 | 0.464933 |
15 | 14 | 0.470005 |
16 | 15 | 0.462823 |
17 | 16 | 0.482759 |
18 | 17 | 0.488548 |
19 | 18 | 0.499203 |
20 | 19 | 0.521059 |
21 | 20 | 0.519108 |
As expected, the average value of the "trait" remains around 0.5.
A model with selection
We can sample individuals according to their trait values, supposing that their fitness is correlated with their trait values.
modelstep_selection!(model::ABM) = sample!(model, nagents(model), :trait)
model = StandardABM(Haploid; model_step! = modelstep_selection!)
for i in 1:numagents
add_agent!(model, rand(abmrng(model)))
end
data, _ = run!(model, 20; adata = [(:trait, mean)])
data
Row | time | mean_trait |
---|---|---|
Int64 | Float64 | |
1 | 0 | 0.508718 |
2 | 1 | 0.695109 |
3 | 2 | 0.744566 |
4 | 3 | 0.766511 |
5 | 4 | 0.79085 |
6 | 5 | 0.797307 |
7 | 6 | 0.803455 |
8 | 7 | 0.834982 |
9 | 8 | 0.831595 |
10 | 9 | 0.846259 |
11 | 10 | 0.874394 |
12 | 11 | 0.871265 |
13 | 12 | 0.880645 |
14 | 13 | 0.884751 |
15 | 14 | 0.88582 |
16 | 15 | 0.889223 |
17 | 16 | 0.890288 |
18 | 17 | 0.898402 |
19 | 18 | 0.894093 |
20 | 19 | 0.904528 |
21 | 20 | 0.914728 |
Here we see that as time progresses, the trait becomes closer and closer to 1, which is expected - since agents with higher traits have higher probability of being sampled for the next "generation".