Third-generation implementation of the dynamically-structured matrix population model, with multiple processes acting on member-classes, implementing both age-dependent and accumulative processes.
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.
char arbiters[2] = {ACC_ERLANG, STOP};
= spop2_init(arbiters, STOCHASTIC);
population pop
= numZERO;
number key = {.i = 10};
number num
(pop, &key, num);
spop2_add
;
number size;
number completed= spop2_init(arbiters, STOCHASTIC);
population poptabledone
double pr[2] = {20.0, 5.0};
(pop, pr, &size, &completed, &poptabledone); spop2_step
Note: Make sure you call
spop2_random_init
before declaring aSTOCHASTIC
population (and then, callspop2_random_destroy
at the end). This can be omitted forDETERMINISTIC
models.
Note:
number
is declared inpopulation.h
as aunion
ofunsigned int
anddouble
.
Let’s begin with a simple case of insect development. For this, we need to know 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.
char arbiters[2] = {ACC_ERLANG, STOP};
= spop2_init(arbiters, DETERMINISTIC);
population pop
[1] = {numZERO};
number key= {.d=10.0};
number num (pop, key, num);
spop2_add
("%d,%g,%g\n",0,spop2_size(pop).d,0.0);
printf
double par[2] = {20.0, 10.0};
, cm;
number szint i;
for (i=1; i<50; i++) {
(pop, par, &sz, &cm, 0);
spop2_step("%d,%g,%g\n",i,sz.d,cm.d);
printf}
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.
char arbiters[2] = {ACC_ERLANG, STOP};
= spop2_init(arbiters, STOCHASTIC);
population pop
[1] = {numZERO};
number key= {.i=10};
number num (pop, key, num);
spop2_add
("%d,%u,%u\n",0,spop2_size(pop).i,0);
printf
double par[2] = {20.0, 10.0};
, cm;
number szint i;
for (i=1; i<50; i++) {
(pop, par, &sz, &cm, 0);
spop2_step("%d,%u,%u\n",i,sz.i,cm.i);
printf}
The sPop2 framework allows for age-dependent and accumulative development times.
Process | Parameters | Definition |
---|---|---|
ACC_ERLANG | devmn, devsd | Erlang-distributed accumulative process |
ACC_PASCAL | devmn, devsd | Pascal-distributed accumulative process |
ACC_FIXED | devmn | Fixed-duration accumulative process |
AGE_FIXED | devmn | Fixed-duration age-dependent process |
NOAGE_CONST | prob | Constant-rate age-independent process |
AGE_CONST | prob | Constant-rate age-dependent process |
AGE_GAMMA | devmn, devsd | Gamma-distributed age-dependent process |
AGE_NBINOM | devmn, devsd | Negative binomial-distributed age-dependent process |
AGE_CUSTOM | User-defined hazard function and 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
.
char arbiters[3] = {AGE_CONST, ACC_ERLANG, STOP};
= spop2_init(arbiters, DETERMINISTIC);
population pop
[2] = {numZERO,numZERO};
number key= {.d=100.0};
number num (pop, key, num);
spop2_add
("%d,%g,%g,%g\n",0,spop2_size(pop).d,0.0,0.0);
printf
, completed[2];
number sizedouble par[4] = {1.0/60.0, 30.0, 5.0};
int i;
for (i=0; i<100; i++) {
(pop, par, &size, completed, 0);
spop2_step("%d,%g,%g,%g\n",i+1,size.d,completed[0].d,completed[1].d);
printf}
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
char arbiters[4] = {AGE_CUSTOM, AGE_GAMMA, AGE_CUSTOM, STOP};
= spop2_init(arbiters, DETERMINISTIC);
population pop
->arbiters[0]->fun_calc = custom;
pop->arbiters[2]->fun_step = 0; pop
The 3rd
process is a dummy (AGE_CUSTOM
), which does not affect the
population or even does not posess a time counter
(fun_step = 0
). The 2nd process is
the regular age-dependent gamma-distributed development process
(AGE_GAMMA
).
The mortality process, on the other hand, is defined with a
custom
function and uses an age counter. 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.
double custom(hazard hfun, unsigned int d, number q, number k, double theta, const number *qkey) {
double devmn = 480.0 - (qkey[2].i > 4 ? 240.0 : 48.0 * qkey[2].i);
double devsd = 0.1 * devmn;
= age_gamma_pars(devmn, devsd);
hazpar hz double a = age_hazard_calc(age_gamma_haz, 0, qkey[0], hz.k, hz.theta, qkey);
return a;
}
With this function, we override an internal mechanism used by all
other processes to calculate the probability of exit from the
sPop2
population. This way, we can link one process to
another (mortality, development, or something else) if necessary. This
generic functional form takes the following parameters.
hfun
is a function to calculate the cumulative
probability density of an exit event. It may use some of the other
parameters to do so.d
refers to the number of days (more correctly time
steps) elapsed in sPop2
.q
refers to the fraction of development (used in
accumulative processes).k
and theta
are the parameters of the
stage duration (development time) distribution.qkey
is an array of status indicators (age for
age-dependent and development fraction for accumulative processes).Of all these, we use the status indicators to calculate the mean and standard deviation of mortality based on the third (dummy) process.
double devmn = 480.0 - (qkey[2].i > 4 ? 240.0 : 48.0 * qkey[2].i);
double devsd = 0.1 * devmn;
Then, we calculate the parameters of the corresponding gamma distribution using the internal function
= age_gamma_pars(devmn, devsd); hazpar hz
and, with these, we calculate the cumulative probability of daily mortality
double a = age_hazard_calc(age_gamma_haz, 0, qkey[0], hz.k, hz.theta, qkey); // qkey[0]: Mortality
Overall, the script to model the dynamics is given below.
double custom(hazard hfun, unsigned int d, number q, number k, double theta, const number *qkey) {
double devmn = 480.0 - (qkey[2].i > 4 ? 240.0 : 48.0 * qkey[2].i);
double devsd = 0.1 * devmn;
= age_gamma_pars(devmn, devsd);
hazpar hz double a = age_hazard_calc(age_gamma_haz, 0, qkey[0], hz.k, hz.theta, qkey);
return a;
}
void fun_transfer(number *key, number num, void *pop) {
[3] = {
number q{.i=key[0].i},
,
numZERO{.i=key[2].i+1}
};
(*(population *)pop, q, num);
spop2_add}
void sim(char stoch) {
int i, j;
char arbiters[4] = {AGE_CUSTOM, AGE_GAMMA, AGE_CUSTOM, STOP};
= spop2_init(arbiters, stoch);
population pop
[3];
population popdonefor (i=0; i<3; i++)
[i] = spop2_init(arbiters, stoch);
popdone
->arbiters[0]->fun_calc = custom;
pop->arbiters[2]->fun_step = 0;
pop
[3] = {numZERO,numZERO,numZERO};
number key;
number numif (stoch == STOCHASTIC)
.i = 1000;
numelse
.d = 1000.0;
num(pop, key, num);
spop2_add
if (stoch == STOCHASTIC)
("%d,%d\n",0,0);
printfelse
("%d,%g\n",0,0.0);
printf
, completed[3];
number sizedouble par[2] = {50.0, 10.0};
for (i=0; i<480; i++) {
(pop, par, &size, completed, popdone);
spop2_stepif (stoch == STOCHASTIC)
("%d,%d\n",i+1,completed[1].i);
printfelse
("%d,%g\n",i+1,completed[1].d);
printf
(popdone[1], fun_transfer, (void *)(&pop));
spop2_foreach
for (j=0; j<3; j++)
(&popdone[j]);
spop2_empty}
}
At each step, spop2_step
returns the following
population
s with members completing each
process.The script adds all individuals completing a gonotrophic cycle
(popdone[1]
) back to pop
one by one. While
doing so, their status indicators are updated manually.
num
individuals are addedkey[0]
: the
first process already has a stepper) (Editted in v0.1.0)numZERO
is the 0
value of number
)key[2] + 1
)This is new in v0.1.0
This recently added feature helps us simplify the above example a bit. By default, we deal with uniform properties across a population. Namely, the mean and standard deviation of development applies to all sub-groups, but each sub-group positions itself at a different level of completion.
In the example above, we used the hazard function
(fun_calc
) to access sub-group keys (qkey
) and
define specific lifetimes for each. Here we will use the
fun_q_par
function, which calculates mean and standard
deviation for each group.
We redefine the custom
function as follows.
void custom(const number *qkey, const number num, double *par) {
[0] = 480.0 - (qkey[2].i > 4 ? 240.0 : 48.0 * qkey[2].i);
par[1] = 0.1 * par[0];
par}
It takes the following as parameters.
qkey
is the array of status indicatorsnum
is the number of individuals in the grouppar
is the array of parameter(s) specific for the
process chosen (mean and standard deviation could be the outputs of the
function)Here, we choose AGE_GAMMA
, which takes 2 parameters by default (mean and st.dev.).
We reset these with pop->numpars[0] = 0
and link
custom
with pop->arbiters[0]->fun_q_par
to switch from population-wide parameters to group-specific ones.
The overall script is given below, which produces an output identical to that of the previous section.
void custom(const number *qkey, const number num, double *par) {
[0] = 480.0 - (qkey[2].i > 4 ? 240.0 : 48.0 * qkey[2].i);
par[1] = 0.1 * par[0];
par}
void fun_transfer(number *key, number num, void *pop) {
[3] = {
number q{.i=key[0].i},
,
numZERO{.i=key[2].i+1}
};
(*(population *)pop, q, num);
spop2_add}
void sim(char stoch) {
if (stoch)
();
spop2_random_init
int i, j;
char arbiters[4] = {AGE_GAMMA, AGE_GAMMA, AGE_CUSTOM, STOP};
= spop2_init(arbiters, stoch);
population pop
[3];
population popdonefor (i=0; i<3; i++)
[i] = spop2_init(arbiters, stoch);
popdone
->arbiters[0]->fun_q_par = custom;
pop->numpars[0] = 0;
pop//
->arbiters[2]->fun_step = 0;
pop
[3] = {numZERO, numZERO, numZERO};
number key;
number numif (stoch == STOCHASTIC)
.i = 1000;
numelse
.d = 1000.0;
num(pop, key, num);
spop2_add
if (stoch == STOCHASTIC)
("%d,%d\n",0,0);
printfelse
("%d,%g\n",0,0.0);
printf
, completed[3];
number sizedouble par[2] = {50.0, 10.0};
for (i=0; i<480; i++) {
(pop, par, &size, completed, popdone);
spop2_stepif (stoch == STOCHASTIC)
("%d,%d\n",i+1,completed[1].i);
printfelse
("%d,%g\n",i+1,completed[1].d);
printf
(popdone[1], fun_transfer, (void *)(&pop));
spop2_foreach
for (j=0; j<3; j++)
(&popdone[j]);
spop2_empty}
}
This is new in v0.1.4
The spop2_foreach
function above transfers the entire
population, class by class, into the target population. To extract only
a fraction of each class, maybe a fraction based on the sub-group key,
we can use the newly introduced spop2_harvest
function.
Here, we create N populations with age-structured development time and initiate the first one with 100 individuals. At each iteration, we transfer a fraction of the individuals completing their development into the second population, and move the rest to the third population, and so on. We collect all individuals in the last two populations.
We can use the following macro definitions to set N, stochasticity, and the age of the transferred population.
#define N 5
#define STOCH FALSE
#define NEWAGE FALSE
By default, we transfer the individuals as they are, with their final
ages kept intact. Due to their age, however, they are quickly removed
from the sub=sequent population as well. By setting the macro
NEWAGE
as TRUE
, we reset the age of the
transferred and provide them with the opportunity to go through another
development process from the start.
void fun_harvest(number *key, number num, number *newkey, double *frac) {
[0].i = NEWAGE ? 0 : key[0].i;
newkey*frac = 0.5;
}
void fun_rest(number *key, number num, number *newkey, double *frac) {
[0].i = key[0].i;
newkey*frac = 1.0;
}
The functions fun_harvest
and fun_rest
handle transfers to the subsequent populations, while setting the
fraction of individuals to be transferred (frac
) and their
sub-group keys (newkey
).
char arbiters[2] = {AGE_GAMMA, STOP};
*pop = (population *)malloc(N * sizeof(population));
population *popdone = (population *)malloc(N * sizeof(population));
population for (j=0; j<N; j++) {
[j] = spop2_init(arbiters, stoch);
pop[j] = spop2_init(arbiters, stoch);
popdone}
= numZERO;
number key = {.d = 100.0};
number num (pop[0], &key, num);
spop2_add
[N], completed[N];
number sizedouble par[2] = {48.0, 6.0};
for (i=0; i<240; i++) {
for (j=0; j<(N-2); j++)
(pop[j], par, &size[j], &completed[j], &popdone[j]);
spop2_step
for (j=0; j<(N-2); j++) {
(popdone[j], pop[j+1], fun_harvest);
spop2_harvest(popdone[j], pop[j+2], fun_rest);
spop2_harvest}
for (j=0; j<N; j++)
(&popdone[j]);
spop2_empty}
for (j=0; j<N; j++) {
(&pop[j]);
spop2_free(&popdone[j]);
spop2_free}
with special thanks to 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 the Population
library.
The model comprises five distinct life-stages and exhibits stable
quasi-cyclic oscillations. Although the model was originally constructed
using delay differential equations, Population
adheres well
to the observed dynamics despite the discrete time step. We demonstrate
both deterministic and stochastic versions of the basic dynamical
model.
First we define parameters of the model.
// Define daily propensity of death
double death[5] = {0.07, 0.004, 0.003, 0.0025, 0.27};
// Define the expected duration of development (in days)
double develop[5] = {0.6, 5.0, 5.9, 4.1, 1e13};
// Define the rest of the parameters
double A0 = 600.0;
double q = 8.5;
double tau = 24.0;
// 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 pop
. Each life stage has a
NOAGE_CONST
process for simulating mortality, which is
independent of age in the blowflies model. It also has an
AGE_FIXED
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 (par[0]
and par[1]
).
Then, we use spop2_step
to iterate the population for one
day. Following a step, the function stores the total population size in
size[i]
and the number of individuals completing
development in completed[i]
for population i
.
Then, we pass individuals who have completed development to the next
life stage. Finally, we compute the number of eggs produced by adults
and add them to the egg stage.
extern gsl_rng *RANDOM;
void sim(char stoch, char id) {
int i, j, k;
double death[5] = {0.07, 0.004, 0.003, 0.0025, 0.27};
double develop[5] = {0.6, 5.0, 5.9, 4.1, 1e13};
double A0 = 600.0;
double q = 8.5;
double tau = 24.0;
char arbiters[3] = {NOAGE_CONST, AGE_FIXED, STOP};
[5];
population popfor (i=0; i<5; i++)
[i] = spop2_init(arbiters, stoch);
pop
[2] = {numZERO,numZERO};
number key;
number numif (stoch == STOCHASTIC)
.i = 500;
numelse
.d = 500.0;
num(pop[0], key, num);
spop2_add
if (stoch == STOCHASTIC) {
("%d,%d",id,0); for (i=0; i<5; i++) printf(",%d",spop2_size(pop[i]).i); printf("\n");
printf} else {
("%d,%d",id,0); for (i=0; i<5; i++) printf(",%g",spop2_size(pop[i]).d); printf("\n");
printf}
[5], completed[5][2];
number sizedouble par[2];
;
number eggs
for (j=0; j<300*tau; j++) {
for (i=0; i<5; i++) {
[0] = death[i] / tau;
par[1] = develop[i] * tau;
par(pop[i], par, &size[i], completed[i], 0);
spop2_step
if (i) {
(pop[i], key, completed[i-1][1]);
spop2_add}
}
if (stoch == STOCHASTIC) {
double tmp = q * (double)size[4].i * exp(-(double)size[4].i / A0) / tau;
.i = (unsigned int)gsl_ran_poisson(RANDOM, tmp);
eggs} else {
.d = q * size[4].d * exp(-size[4].d / A0) / tau;
eggs}
(pop[0], key, eggs);
spop2_add
if (stoch == STOCHASTIC) {
("%d,%d",id,j); for (k=0; k<5; k++) printf(",%d",spop2_size(pop[k]).i); printf("\n");
printf} else {
("%d,%d",id,j); for (k=0; k<5; k++) printf(",%g",spop2_size(pop[k]).d); printf("\n");
printf}
}
}
We run the deterministic model, and sample 100 trajectories from the stochastic model. We plot results from the last 200 days of simulation.