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.42726
210.497929
320.513011
430.571239
540.583501
650.624367
760.679946
870.724317
980.725342
1090.689457
11100.705233
12110.704372
13120.697773
14130.725385
15140.72586
16150.731266
17160.765105
18170.744987
19180.744663
20190.74374
21200.753556

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.505639
210.655232
320.737398
430.771379
540.785315
650.818285
760.83646
870.860588
980.86836
1090.889281
11100.907066
12110.90858
13120.908554
14130.920063
15140.930113
16150.927999
17160.936381
18170.943464
19180.945925
20190.942305
21200.940535

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