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 rows × 2 columns

stepmean_trait
Int64Float64
100.502929
210.473112
320.515665
430.533874
540.519728
650.549797
760.560395
870.594715
980.631248
1090.680033
11100.674431
12110.687995
13120.652608
14130.675221
15140.650108
16150.67653
17160.657881
18170.632291
19180.607392
20190.633512
21200.642224

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.469451
210.660135
320.789815
430.857325
540.872941
650.887346
760.899798
870.902471
980.925959
1090.92721
11100.93218
12110.927893
13120.937662
14130.945969
15140.9416
16150.946617
17160.94994
18170.951554
19180.945537
20190.941952
21200.94057

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