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 
nhaploid individuals. - At each generation, 
noffsprings replace the parents. - Each offspring chooses a parent at random and inherits its genetic material.
 
using Agents
numagents = 100Let's define an agent. The genetic value of an agent is a number (trait field).
mutable struct Haploid <: AbstractAgent
    id::Int
    trait::Float64
endAnd make a model without any spatial structure:
model = ABM(Haploid)AgentBasedModel with 0 agents of type Haploid no space scheduler: fastest
Create n random individuals:
for i in 1:numagents
    add_agent!(model, rand(model.rng))
endTo 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))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))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, dummystep, modelstep_neutral!, 20; adata = [(:trait, mean)])
data| step | mean_trait | |
|---|---|---|
| Int64 | Float64 | |
| 1 | 0 | 0.568395 | 
| 2 | 1 | 0.563868 | 
| 3 | 2 | 0.55858 | 
| 4 | 3 | 0.575658 | 
| 5 | 4 | 0.558653 | 
| 6 | 5 | 0.593829 | 
| 7 | 6 | 0.543887 | 
| 8 | 7 | 0.583726 | 
| 9 | 8 | 0.588029 | 
| 10 | 9 | 0.625381 | 
| 11 | 10 | 0.612036 | 
| 12 | 11 | 0.624554 | 
| 13 | 12 | 0.657911 | 
| 14 | 13 | 0.668756 | 
| 15 | 14 | 0.705398 | 
| 16 | 15 | 0.689301 | 
| 17 | 16 | 0.691256 | 
| 18 | 17 | 0.671149 | 
| 19 | 18 | 0.650738 | 
| 20 | 19 | 0.683608 | 
| 21 | 20 | 0.664821 | 
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.
model = ABM(Haploid)
for i in 1:numagents
    add_agent!(model, rand(model.rng))
end
modelstep_selection!(model::ABM) = sample!(model, nagents(model), :trait)
data, _ = run!(model, dummystep, modelstep_selection!, 20; adata = [(:trait, mean)])
data| step | mean_trait | |
|---|---|---|
| Int64 | Float64 | |
| 1 | 0 | 0.551704 | 
| 2 | 1 | 0.700771 | 
| 3 | 2 | 0.75958 | 
| 4 | 3 | 0.818158 | 
| 5 | 4 | 0.867937 | 
| 6 | 5 | 0.861506 | 
| 7 | 6 | 0.887199 | 
| 8 | 7 | 0.901307 | 
| 9 | 8 | 0.897411 | 
| 10 | 9 | 0.895407 | 
| 11 | 10 | 0.895847 | 
| 12 | 11 | 0.911254 | 
| 13 | 12 | 0.930029 | 
| 14 | 13 | 0.930694 | 
| 15 | 14 | 0.943999 | 
| 16 | 15 | 0.948164 | 
| 17 | 16 | 0.944993 | 
| 18 | 17 | 0.941845 | 
| 19 | 18 | 0.943305 | 
| 20 | 19 | 0.948013 | 
| 21 | 20 | 0.944437 | 
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".