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
100

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

@agent struct Haploid(NoSpaceAgent)
    trait::Float64
end

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))
modelstep_neutral! (generic function with 1 method)

And make a model without any spatial structure:

model = StandardABM(Haploid; model_step! = modelstep_neutral!)
StandardABM with 0 agents of type Haploid
 agents container: Dict
 space: nothing (no spatial structure)
 scheduler: fastest

Create n random individuals:

for i in 1:numagents
    add_agent!(model, rand(abmrng(model)))
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))
StandardABM with 100 agents of type Haploid
 agents container: Dict
 space: nothing (no spatial structure)
 scheduler: fastest

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, 20; adata = [(:trait, mean)])
data
21×2 DataFrame
Rowtimemean_trait
Int64Float64
100.456202
210.479757
320.440221
430.423166
540.414824
650.42311
760.421922
870.419701
980.419014
1090.431818
11100.448848
12110.454435
13120.447619
14130.464933
15140.470005
16150.462823
17160.482759
18170.488548
19180.499203
20190.521059
21200.519108

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.

modelstep_selection!(model::ABM) = sample!(model, nagents(model), :trait)
model = StandardABM(Haploid; model_step! = modelstep_selection!)
for i in 1:numagents
    add_agent!(model, rand(abmrng(model)))
end

data, _ = run!(model, 20; adata = [(:trait, mean)])
data
21×2 DataFrame
Rowtimemean_trait
Int64Float64
100.508718
210.695109
320.744566
430.766511
540.79085
650.797307
760.803455
870.834982
980.831595
1090.846259
11100.874394
12110.871265
13120.880645
14130.884751
15140.88582
16150.889223
17160.890288
18170.898402
19180.894093
20190.904528
21200.914728

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