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
 space: nothing (no spatial structure)
 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×2 DataFrame
Rowstepmean_trait
Int64Float64
100.478048
210.472249
320.449431
430.484856
540.452488
650.48156
760.476576
870.471093
980.48046
1090.452903
11100.459151
12110.414842
13120.393756
14130.407445
15140.401127
16150.408465
17160.455384
18170.473536
19180.534936
20190.524724
21200.539426

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×2 DataFrame
Rowstepmean_trait
Int64Float64
100.472414
210.59853
320.715738
430.767257
540.817088
650.842317
760.8601
870.8473
980.880932
1090.893361
11100.916486
12110.926954
13120.933944
14130.933488
15140.923982
16150.92481
17160.930412
18170.926794
19180.934649
20190.941354
21200.948037

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