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

Let's define an agent. The genetic value of an agent is a number (trait field).

mutable struct Haploid <: AbstractAgent
    id::Int
    trait::Float64
end

And 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))
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))

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

21 rows × 2 columns

stepmean_trait
Int64Float64
100.502108
210.538608
320.531461
430.541992
540.548552
650.558523
760.57982
870.593878
980.605408
1090.571188
11100.517833
12110.516386
13120.485332
14130.510029
15140.48295
16150.478217
17160.440758
18170.448953
19180.427823
20190.421834
21200.448359

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

21 rows × 2 columns

stepmean_trait
Int64Float64
100.508108
210.615817
320.713224
430.76079
540.790314
650.824339
760.836574
870.851675
980.858929
1090.869889
11100.882297
12110.891229
13120.885766
14130.908889
15140.91444
16150.914898
17160.912845
18170.917033
19180.918522
20190.919485
21200.920151

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".