Evolving populations

Routines to advance a population state for a number of time steps, or until a pre-specified condition is met.

Available dynamics

All routines take a Population as their first and only positional argument, and further parameters like mutation rate as keyword arguments.

Spatial

An Eden-like model

GrowthDynamics.LatticeDynamics.eden_with_density!Function
eden_with_density!(state::Population)

Cells proliferate to neighboring lattice sites with a rate proportional to the number of free neighboring sites. A time step is either a birth, death, or mutation-only event.

A custom label function must be provided if genotypes are not integers.

See the 'extended help' for signatures of the various callbacks.

Keyword arguments

  • T::Int: number of steps to advance.
  • d=0.0: death rate. Zero halts the dynamics after carrying capacity is reached.
  • fitness=(population, old_genotype, new_genotype)->1.0: function that assigns a fitness value (default 1.0) to a new genotype.
  • mu=0.0: mutation rate; either a function or number.
  • label=(population, old_genotype) -> lastgenotype(population)+1: function that assigns the new genotype upon mutation.
  • makesnps=true: generate SNPs during the simulation?
  • mu_type=[:poisson, :fixed]: number of mutations is fixed, or Poisson distributed.
  • genome_length=10^9: length of the genome.
  • baserate=1.0: progressing real time is measured in 1/baserate.
  • prune_period=0: prune the phylogeny periodically after no. of steps, set to 0 to disable.
  • prune_on_exit=true: prune before leaving the simulation.
  • onstep: callback that is executed at the very beginning of every time step.
  • onprebirth: callback that runs if a birth event is about to occur. Return value determines whether cell actually proliferates.
  • onpostbirth: callback that runs at the very end of a proliferation event.
  • ondeath: callback that runs at the very end of a death event.

Extended help

Mutations

The rate of mutations is steered by the keyword argument mu, which is either a number or a function.

A number is automatically converted to an appropriate function. As a function it must have signature mu(population, old_genotype, I_old, I_new)->(rate, p_mutate)::Tuple{Float64, Float64} where

  • old_genotype is the genotype of the parent cell
  • I_old, I_new are the linear lattice indices of the parent and daughter cell.

This way the mutation rate can depend on position as well as on mutations present in the parental genome.

The output must be a tuple, where the first entry rate determines the number of mutations if mutations happen; the probability of which is given by the second entry p_mutate. The main reason for keeping these quantities separate is that one will often not generate SNPs during the simulation, because that process is rather costly, but generate them later on the basis of the final phylogeny.

If mu is a number, it is implicitely wrapped in a function that returns (mu, 1-exp(-mu)), i.e. the mutation probability is the probability of at least one event under a Poisson distribution with rate mu.

Setting makesnps to true/false determines whether SNPs are generated during the simulation. If it is set to false, the first value returned by mu is inconsequential.

Finally, label(population, old_genotype)->new_genotype assigns the designation to a newly entering genotype. It defaults to numbering genotypes consecutively. See also rename!.

Fitness

When a new genotype enters due to a mutation event, it is assigned a fitness value given by a user-provides function (population, old_genotype, new_genotype)->Float64 passed as keyword argument fitness. For example, to inherit the fitness, one would provide fitness=(p,og,ng)->p.meta[g=og; :fitness].

Callbacks

Callbacks are triggered at certain stages of the simulation loop:

  • onstep(population)::Bool is executed at the beginning of each time step. If true is returned, the simulation ends.
  • onprebirth(population, Iold)::Bool is executed when a birth event is triggered, but before it is executed. If false is returned, the cell will not proliferate, but might still mutate.
  • onpostbirth(population, Iold, Inew) is executed after proliferation and mutation are completed. Useful for collecting observables.
  • ondeath(population, Idead) is executed when a death event has finished.
source

Non-spatial

GrowthDynamics.LatticeDynamics.moran!Function
moran!(state::NoLattice{Int}; <keyword arguments>)

(Generalized) Moran dynamics on an unstructured population. Birth and death events are independent until the carrying capacity (keyword argument K) is reached. After that individuals begin replacing each other like in the classic Moran model.

Arguments

  • T::Int: the number of steps to advance.
  • d=0.0: death rate.
  • K=0: Carrying capacity. Set to 0 for unlimited.
  • fitness=(population, old_genotype, new_genotype)->1.0: function that assigns a fitness value (default 1.0) to a new genotype.
  • mu=0.0: mutation rate; either a function or number.
  • label=(population, old_genotype) -> lastgenotype(population)+1: function that assigns the new genotype upon mutation.
  • makesnps=true: generate SNPs during the simulation?
  • mu_type=[:poisson, :fixed]: number of mutations is fixed, or Poisson distributed.
  • genome_length=10^9: length of the genome.
  • baserate=1.0: progressing real time is measured in 1/baserate.
  • prune_period=0: prune the phylogeny periodically after no. of steps, set to 0 to disable.
  • prune_on_exit=true: prune before leaving the simulation.
  • onstep: callback that is executed at the very beginning of every time step.
  • onprebirth: callback that runs if a birth event is about to occur. Return value determines whether cell actually proliferates.
  • onpostbirth: callback that runs at the very end of a proliferation event.
  • ondeath: callback that runs at the very end of a death event.
source
GrowthDynamics.LatticeDynamics.exponential!Function
exponential!(state::NoLattice{Int}; <keyword arguments>)

Run exponential growth on an unstructered population until carrying capacity is reached. No death.

Each generation consists of the following actions for every genotype:

  1. The number of decendents is drawn from a binomial distribution with parameters n, the population number of that genotype, and p given by 1-exp(-f/<f>) with fitness value f. That number as capped so as not to exceed the defined carrying capacity.
  2. Of those decendents the number of mutants is drawn from a binomial distribution according to the mutation rate mu.

Arguments

  • T::Int: number of steps to advance.
  • fitness=(population, old_genotype, new_genotype)->1.0: function that assigns a fitness value (default 1.0) to a new genotype.
  • mu=0.0: mutation rate; either a function or number.
  • label=(population, old_genotype) -> lastgenotype(population)+1: function that assigns the new genotype upon mutation.
  • makesnps=true: generate SNPs during the simulation?
  • mu_type=[:poisson, :fixed]: number of mutations is fixed, or Poisson distributed.
  • genome_length=10^9: length of the genome.
  • baserate=1.0: progressing real time is measured in 1/baserate.
  • prune_period=0: prune the phylogeny periodically after no. of steps, set to 0 to disable.
  • prune_on_exit=true: prune before leaving the simulation.
  • onstep: callback that is executed at the very beginning of every time step.
  • ondeath: callback that runs at the very end of a death event.
source

Adding your own

There is no particular interface or signature an evolution routine must adhere to.

However, to avoid putting the lattice, metadata and phylogeny in an inconsistent state, a few tips should be followed

  • Use the getter and setter methods for MetaData instead of manipulating the fields directly.
  • Using metadata[g=genotype; Val(:field)] is more performant than metadata[g=genotype; :field] if :field is constant, because the former avoids dynamic dispatch.
  • Use population[index] (= genotype) to get/set the genotype of a cell at a position. Do not manipulate population.lattice.data directly.
  • Prefer add_genotype! over push!.
  • Use remove_genotype!.
  • For performance reasons SNPs are either Vector{Int} or nothing. Check for the latter with hassnps before adding new ones.
  • Not a must, but don't forget to advance the step/real time counters population.t and population.treal after each simulation step.