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.515
210.511351
320.548122
430.560224
540.599837
650.624906
760.603651
870.616553
980.612578
1090.589663
11100.545579
12110.531726
13120.541994
14130.532004
15140.529418
16150.527376
17160.551529
18170.598082
19180.590321
20190.578985
21200.560946

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.495789
210.651807
320.704933
430.809162
540.862305
650.892445
760.893429
870.901741
980.912755
1090.915897
11100.919325
12110.929424
13120.92719
14130.924943
15140.924376
16150.923924
17160.921374
18170.92446
19180.923667
20190.93073
21200.940364

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