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.

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
n = 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:

m = ABM(Haploid)
AgentBasedModel with 0 agents of type Haploid
 no space
 scheduler: fastest

Create n random individuals:

for i in 1:n
    add_agent!(m, rand())
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!(m, nagents(m))

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!(m) = sample!(m, nagents(m))

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!(m, dummystep, modelstep_neutral!, 20; adata = [(:trait, mean)])
data

21 rows × 2 columns

stepmean_trait
Int64Float64
100.42748
210.466301
320.491889
430.519205
540.538416
650.519753
760.541751
870.527809
980.538966
1090.532131
11100.537809
12110.538059
13120.539744
14130.604315
15140.636157
16150.648527
17160.631555
18170.667634
19180.664524
20190.637829
21200.639602

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.

m = ABM(Haploid)
for i in 1:100
    add_agent!(m, rand())
end

modelstep_selection!(m::ABM) = sample!(m, nagents(m), :trait)

data, _ = run!(m, dummystep, modelstep_selection!, 20; adata = [(:trait, mean)])
data

21 rows × 2 columns

stepmean_trait
Int64Float64
100.511277
210.694014
320.785328
430.828866
540.868187
650.869305
760.880079
870.901108
980.897571
1090.911525
11100.907961
12110.925238
13120.934203
14130.944933
15140.948984
16150.952078
17160.961232
18170.96482
19180.972801
20190.977566
21200.978035

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