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.540674
210.555865
320.534324
430.570363
540.596332
650.582346
760.5772
870.584075
980.590567
1090.58364
11100.578553
12110.62567
13120.606685
14130.592396
15140.577259
16150.551849
17160.540901
18170.547915
19180.561568
20190.561245
21200.57563

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.525768
210.684621
320.736674
430.794236
540.837069
650.845882
760.863521
870.889436
980.89822
1090.912791
11100.927714
12110.921683
13120.927152
14130.92976
15140.923554
16150.915291
17160.922749
18170.934277
19180.936009
20190.93479
21200.932559

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