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!
— Functioneden_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 (default1.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 in1/baserate
.prune_period=0
: prune the phylogeny periodically after no. of steps, set to0
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 cellI_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. Iftrue
is returned, the simulation ends.onprebirth(population, Iold)::Bool
is executed when a birth event is triggered, but before it is executed. Iffalse
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.
Non-spatial
GrowthDynamics.LatticeDynamics.moran!
— Functionmoran!(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 to0
for unlimited.fitness=(population, old_genotype, new_genotype)->1.0
: function that assigns a fitness value (default1.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 in1/baserate
.prune_period=0
: prune the phylogeny periodically after no. of steps, set to0
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.
GrowthDynamics.LatticeDynamics.exponential!
— Functionexponential!(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:
- The number of decendents is drawn from a binomial distribution with parameters
n
, the population number of that genotype, andp
given by1-exp(-f/<f>)
with fitness valuef
. That number as capped so as not to exceed the defined carrying capacity. - 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 (default1.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 in1/baserate
.prune_period=0
: prune the phylogeny periodically after no. of steps, set to0
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.
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 thanmetadata[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 manipulatepopulation.lattice.data
directly. - Prefer
add_genotype!
overpush!
. - Use
remove_genotype!
. - For performance reasons SNPs are either
Vector{Int}
ornothing
. Check for the latter withhassnps
before adding new ones. - Not a must, but don't forget to advance the step/real time counters
population.t
andpopulation.treal
after each simulation step.