This is the standalone Julia library of the dynamically-structured matrix population model sPop2. This version implements both age-dependent and accumulative processes.
Just type this in Julia:
using Pkg
"Population") Pkg.add(
Alternatively, one could clone or download and extract the development version from this GitHub repository, and use the package as follows.
using Pkg
"Absolute path to the Population.jl directory")
Pkg.add(
using Population
The following creates a pseudo-structured population with 10 individuals and iterates it one step with 0 mortality and an Erlang-distributed development time of 20 ± 5 steps.
= sPop2(PopDataSto(), AccErlang())
pop
, 10)
AddPop(pop= (devmn=20.0, devsd=5.0)
pr , completed, poptabledone = StepPop(pop, pr) size
See section Usage examples for further examples.
Let’s begin with a canonical example. Arguably, the straightforward way to model insect development is to use an ordinary differential equations system with exponentially distributed transition times.
Let’s assume that experimental observations of larva development yielded a round figure of 20 days as average development time. This is usually translated to the differential equations reals as an instantaneous rate of α = 1/20.
using DifferentialEquations
function develop!(du,u,p,t)
1] = -(1.0/20.0)*u[1]
du[2] = (1.0/20.0)*u[1]
du[end
= [100.0; 0.0;]
u0 = (0.0, 100.0)
tspan = ODEProblem(develop!,u0,tspan)
prob = solve(prob) sol
Expectedly, this implies that larvae begin developing by the time they emerge from eggs, and keep producing pupae at the same constant rate until a negligible number of larvae is left.
An alternative representation with a structured population can be constructed. For this, we need to know not the rate of pupa production, but the average duration of the larva stage and its variation. Let’s assume that stage duration follows a gamma distribution (Erlang to be precise), which implies that all individuals race to develop out of the larva stage, while some are faster and even more of them are slower.
= sPop2(PopDataDet(), AccErlang())
pop
, 10.0)
AddPop(pop= [0 10.0 0.0]
out for n in 1:50
= (devmn=20.0, devsd=5.0)
pr1 = StepPop(pop, pr1)
ret = vcat(out, [n ret[1] ret[2]])
out end
With a simple modification, we can simulate how the dynamics vary when each individual is given a daily chance of completing its development based on the Erlang-distributed development time assumption.
= sPop2(PopDataSto(), AccErlang())
pop
, 10)
AddPop(pop= [0 10 0.0]
outst for n in 1:50
= (devmn=20.0, devsd=5.0)
pr1 = StepPop(pop, pr1)
ret = vcat(outst, [n ret[1] ret[2]])
outst end
The sPop2 framework allows for age-dependent and accumulative development times.
Process | Parameters | Definition |
---|---|---|
AccErlang | devmn, devsd | Erlang-distributed accumulative process |
AccPascal | devmn, devsd | Pascal-distributed accumulative process |
AccFixed | devmn | Fixed-duration accumulative process |
AgeFixed | devmn | Pascal-distributed age-dependent process |
AgeConst | prob | Constant-rate age-dependent process |
AgeGamma | devmn, devsd | Gamma-distributed age-dependent process |
AgeNbinom | devmn, devsd | Negative binomial-distributed age-dependent process |
AgeCustom | User-defined hazard function, Stepper | Age-dependent or accumulative stepping with a user-defined hazard function |
Age-dependent | Accumulative |
---|---|
Multiple processes can be added to an sPop2 to represent more complex dynamics. For instance, we can represent survival with a daily constant rate and development with an Erlang-distributed accumulative process. The processes are executed in the order they are added to the sPop2.
= sPop2(PopDataDet(), AgeConst(), AccErlang())
a , 100.0)
AddPop(a= [0 GetPop(a) 0.0 0.0]
ret for i in 0:100
= (prob=1.0/60.0,)
pr1 = (devmn=30.0, devsd=5.0)
pr2 = StepPop(a, pr1, pr2)
out = vcat(ret, [i+1 out[1] out[2][1] out[2][2]])
ret end
The above instruction represent a life stage with 0.017 daily mortality and 30 ± 5 days of development time.
In this section, we will attempt to reproduce Figure 1 in the original age-dependent sPop model. We will represent an adult female mosquito with a lifetime of 20 ± 2 days. The female enters a cyclic process of obtaining bloodmeal and egg development, which takes about 2 days. However, at the end of each ovipositioning, her expected lifetime decreases by 2 days. I assure you that the actual physiology is far more complicated than this!
In order to represent the lifetime of this female mosquito, we will employ 3 processes:
# Declare an sPop2 population with deterministic dynamics,
# and define three processes in this order: Mortality, Gonotropic cycle, Ovipositioning
= sPop2(PopDataDet(), AgeCustom(custom, AgeStepper), AgeGamma(), AgeDummy()) a
The 3rd process is a dummy (AgeDummy), which does not affect the population or even does not posess a time counter. The 2nd process is the regular age-dependent gamma-distributed development process (AgeGamma).
The mortality process, on the other hand, is defined with a custom function and uses the AgeStepper. We included this stepper in process declaration, because we require that the status indicator be an age counter, i.e. the number of steps the sPop2 is iterated is kept in the status indicator (see qkey below). We declare the custom function as the following.
function custom(heval::Function, d::Number, q::Number, k::Number, theta::Number, qkey::Tuple)
= 480.0 - (qkey[3] > 4 ? 240.0 : 48.0 * qkey[3])
devmn = 0.1 * devmn
devsd = (devmn=devmn, devsd=devsd)
pr , theta, stay = sPop2.age_gamma_pars(pr)
kreturn sPop2.age_hazard_calc(sPop2.age_gamma_haz, 0, qkey[1], k, theta, qkey)
end
With this function, we override an internal mechanism used by all other processes to calculate the probability of exit (need this be due to mortality, development, or something else) from the sPop2 population. This generic functional form takes the following parameters.
Of all these, we use the status indicators to calculate the mean and standard deviation of mortality based on the third (dummy) process.
= 480.0 - (qkey[3] > 4 ? 240.0 : 48.0 * qkey[3]) # qkey[3]: Ovipositioning
devmn = 0.1 * devmn
devsd = (devmn=devmn, devsd=devsd) pr
Then, we calculate the parameters of the corresponding gamma distribution using the internal function
, theta, stay = sPop2.age_gamma_pars(pr) k
and, with these, we calculate the cumulative probability of daily mortality
, 0, qkey[1], k, theta, qkey) # qkey[1]: Mortality sPop2.age_hazard_calc(sPop2.age_gamma_haz
Overall, the script to model the dynamics is given below.
function custom(heval::Function, d::Number, q::Number, k::Number, theta::Number, qkey::Tuple)
= 480.0 - (qkey[3] > 4 ? 240.0 : 48.0 * qkey[3])
devmn = 0.1 * devmn
devsd = (devmn=devmn, devsd=devsd)
pr , theta, stay = sPop2.age_gamma_pars(pr)
kreturn sPop2.age_hazard_calc(sPop2.age_gamma_haz, 0, qkey[1], k, theta, qkey)
end
# Stochastic dynamics, Mortality, Gonotropic cycle, Ovipositioning
= sPop2(PopDataDet(), AgeCustom(custom, AgeStepper), AgeGamma(), AgeDummy())
a ,1000.0)
AddPop(a= [0 0.0]
ret for i in 0:480
= NamedTuple{}()
pr1 = (devmn=50.0, devsd=10.0)
pr2 = StepPop(a, pr1, pr2, pr1)
out for (q,n) in out[3][2]
# Please note that this step is essential!
, n, q.key[1], 0, q.key[3]+one(q.key[3]))
AddPop(aend
= vcat(ret, [i+1 out[2][2]])
ret end
return ret
At each step, StepPop returns the following tuple
The script adds all individuals completing a gonotrophic cycle (out[3][2]) back to the population one by one. While doing so, their status indicators are updated manually.
Contributed by Sean L. Wu @slwu89.
Nicholson’s blowflies are a classic example of a time-delayed stage-structured population model, and was presented as a case study in the sPop paper. We reproduce that example here using Population.jl.
The model comprises five distinct life-stages and exhibits stable quasi-cyclic oscillations. Although the model was originally constructed using delay differential equations, Population.jl adheres well to the observed dynamics despite the discrete time step. We demonstrate both deterministic and stochastic versions of the basic dynamical model.
using Population
using Plots
using Distributions
using Statistics
First we define parameters of the model.
# Define daily propensity of death
= [0.07, 0.004, 0.003, 0.0025, 0.27]
death
# Define the expected duration of development (in days)
= [0.6, 5.0, 5.9, 4.1, Inf]
develop
# Define the rest of the parameters
= 600.0
A0 = 8.5
q = 24
tau # tau is a scaling factor to extend from daily to hourly iterations for more accurracy
Next we define a function to run the simulation for 300 days, in either stochastic or deterministic mode. The 5 life stages are stored in the vector vec
. Each life stage has an AgeConst
process for simulating mortality, which is independent of age in the blowflies model. It also has an AgeFixed
process to simulate maturation, which has a deterministic (fixed) distribution in the blowflies model. Therefore variation in the stochastic version arises from the mortality process and egg laying, which is assumed to be drawn from a Poisson distribution centered on the deterministic mean.
On each day we calculate the terms required for the mortality and development processes (pr1
and pr2
). Then we use StepPop
to iterate the population for one day, and we store the output containing the total population size, dictionary of individuals completing development, and dictionary of all developing individuals for that stage in the vector out
. Then we pass individuals who have completed development to the next stage life stage. Finally we compute the number of eggs produced by adults and add them to the egg stage.
# this function is a wrapper routine to simulate a deterministic or a stochastic time series
function sim_blowfly(stoch)
= zeros(300*tau+1, 5)
output # initiate 5 stages of the population and store them in a vector
= [stoch ? sPop2(PopDataSto(), AgeConst(), AgeFixed()) : sPop2(PopDataDet(), AgeConst(), AgeFixed()) for n in 1:5]
vec # add individuals to the initial stage (the egg stage)
1], stoch ? 500 : 500.0)
AddPop(vec[# record the initial conditions
1, :] = [GetPop(v) for v in vec]
output[# vector to store the output each day
= Vector{Any}(nothing, length(vec))
out # iterate for 300 days
for n in 1:300*tau
# for each stage, repeat the following
for m in 1:length(vec)
# iterate the stage for one time unit
= (prob = death[m]/tau,)
pr1 = (devmn = develop[m]*tau,)
pr2 = StepPop(vec[m], pr1, pr2)
out[m] # and pass on to the next stage (if not the adult stage)
if m > one(m)
for (q,s) in out[m-1][3][2]
, s, 0.0, 0.0)
AddPop(vec[m]end
end
end
# calculate the number of eggs to produce
= q * GetPop(vec[5]) * exp(-GetPop(vec[5])/A0)/tau
eggs if stoch
= rand(Poisson(eggs))
eggs end
1], eggs)
AddPop(vec[# record the current state
+1, :] = [GetPop(v) for v in vec]
output[nend
# return the results
return output
end
We run the deterministic model, and sample 100 trajectories from the stochastic model. We plot results from the last 200 days of simulation.
= sim_blowfly(false)
out
= 100:(1/tau):300
xtick , out[100*tau+1:end,5], label = "Adults", color = :blue, ylim = [0,6000])
plot(xtick!(xtick, out[100*tau+1:end,1], label = "Eggs", color = :red) plot
= [sim_blowfly(true) for n in 1:100]
outs = cat(outs..., dims=3)
outs
= [quantile(outs[i,5,:], [0.025, 0.5, 0.975]) for i in axes(outs,1)]
outs_adult_qt = transpose(hcat(outs_adult_qt...))
outs_adult_qt
= [quantile(outs[i,1,:], [0.025, 0.5, 0.975]) for i in axes(outs,1)]
outs_eggs_qt = transpose(hcat(outs_eggs_qt...))
outs_eggs_qt
= (0:(size(outs_adult_qt)[1]-1))/24.0
xtick
, outs_adult_qt[:,3], color="#0571b0", fillrange=outs_adult_qt[:,1], lw=1, alpha=0.5, label=:none)
plot(xtick!(xtick, outs_adult_qt[:,2], color="#0571b0", lw=2, label=:none)
plot!(xtick, outs_eggs_qt[:,3], color="#d6604d", fillrange=outs_eggs_qt[:,1], lw=1, alpha=0.5, label=:none)
plot!(xtick, outs_eggs_qt[:,2], color="#d6604d", lw=2, label=:none)
plot!(100,300) xlims