An R package for generating simulated population models (IPMs and MPMs)
🌿🍄🐖🌱🌵🌳🐃
This package is under active development - enter at you own risk
This is an R package I have been developing for my PhD research to simulate population models. It uses integral projection models.
How to install this package from GitHub
#install.packages("devtools")
library(devtools)
#install_github("simonrolph/simpopmods")
library(simpopmods)
#install.packages("rsvg")
library(DiagrammeR)##
## Attaching package: 'DiagrammeR'
## The following object is masked from 'package:devtools':
##
## add_path
library(ggplot2)Before you generate any simulated populations, you first need to define a template for your population model. This is done through a IPM descriptor object. You can generate a template for the IPM descriptor by using the init_IPM_desc() function.
An simpopmods IPM descriptor consists of
- Discrete states
- Continous states (which all share the same domain)
- Parameters
- Name of parameter
- maximum possible value
- Minimum possible value
- T/F as to whether it is sampled directly, or derived from a combination of other parameters
- Functions for generating parameters that are not sampled directly
- Demographic functions (equations of parameters)
- Kernels (consisting of demographic functions)
- Functions for the upper limit, lower limit and resolution of the kernel of the continuous state domain.
Here's one I made earlier:
################ IPM_desc
states <- c("mature")
states_z <- c(T)
# $par_info
par_info <- data.frame(t(data.frame(
## survival
surv.int = c(par = "surv.int",min = -10,max = 4,samp = T),
surv.z = c(par = "surv.z",min = 0,max = 10,samp = T),
## flowering
flow.int = c(par = "flow.int",min = -10,max = 10,samp = T),
flow.z = c(par = "flow.z",min = 0,max = 10,samp = T),
## growth
grow.int = c(par = "grow.int",min = 0,max = 1,samp = T),
grow.z = c(par = "grow.z",min = 0,max = 1,samp = F), # constrained from 0 to 1
grow.sd = c(par = "grow.sd",min = 0.01,max = 0.5,samp = T),
## recruit size
rcsz.int = c(par = "rcsz.int",min = 0,max = 0,samp = F), # doesn't change
rcsz.sd = c(par = "rcsz.sd",min = 0.1,max = 0.5,samp = T),
## seed size
seed.int = c(par = "seed.int",min = 0,max = 100,samp = T),
seed.z = c(par = "seed.z",min = 0,max = 10,samp = T),
## recruitment probability
p.r = c(par = "p.r",min = 0,max = 0.99,samp = T) # constrained from 0 to 1
)),stringsAsFactors=F)
par_info$min <- as.numeric(par_info$min)
par_info$max <- as.numeric(par_info$max)
par_info$samp <- as.logical(par_info$samp)
# $par_fns
par_fns <- list(
rcsz.int = function(params){0},
grow.z = function(params){1 - params["grow.int"]}
)
# $demo_fns
demo_fns <- list(
## G_z1z - Growth function, given you are size z now returns the pdf of size z1 next time
grow = function(z1, z, params){
mu <- params["grow.int"] + params["grow.z"] * z # mean size next year
sig <- params["grow.sd"] # sd about mean
p.den.grow <- dnorm(z1, mean = mu, sd = sig) # pdf that you are size z1 given you were size z
return(p.den.grow)
},
## s_z Survival function, logistic regression
surv = function(z, params){
linear.p <- params["surv.int"] + params["surv.z"] * z # linear predictor
p <- pmin(1/(1+exp(-linear.p)),rep(0.99,length(z))) # logistic transformation to probability with constant cap
p <- 1/(1+exp(-linear.p))
return(p)
},
## p_bz Probability of flowering function, logistic regression
flow = function(z, params){
linear.p <- params["flow.int"] + params["flow.z"] * z # linear predictor
p <- 1/(1+exp(-linear.p)) # logistic transformation to probability
return(p)
},
## b_z Seed production function
seed = function(z, params){
N <- exp(params["seed.int"] + params["seed.z"] * z) # seed production of a size z plant
return(N)
},
## c_0z1 Recruit size pdf
rcsz = function(z1, params){
mu <- params["rcsz.int"]
sig <- params["rcsz.sd"]
p.deRecr <- dnorm(z1, mean = mu, sd = sig) # pdf of a size z1 recruit
return(p.deRecr)
}
)
kernels <- c("P","Fec")
kernel_fns <- init_nested_list(kernels,states)
# kernel_fns$P$ TO $ FROM
# SURVIVAL/GROWTH
kernel_fns$P$mature$mature <- function (z1, z, params) {
return(IPM_desc$demo_fns$surv(z, params) * IPM_desc$demo_fns$grow(z1, z, params))
}
# FECUNDITY
kernel_fns$Fec$mature$mature <- function (z1, z, params) {
#flowering * number of seeds * recruit survival * recruit size * seedbank splitter
return( IPM_desc$demo_fns$flow(z, params) * IPM_desc$demo_fns$seed(z, params) * params["p.r"] * IPM_desc$demo_fns$rcsz(z1, params))
}
# lower size limit to prevent eviction
limit_lower <- function(params, IPM_desc){
max_sd <- params["rcsz.sd"]+params["grow.sd"]
# returns the lower size limit of the kernel
# a value of 0.05 means that 5% of new recruits are evicted
qnorm(0.01,mean = 0, sd = max_sd)
}
#upper size limit to prevent eviction
limit_upper <- function(params,IPM_desc){
upper_lims <- seq(from = 2, to = 10, by = 1)
#Calculate the proportion of individuals wrongly evicted at a size with a size limit
fac1 <- IPM_desc$demo_fns$surv(upper_lims, params) # survival probability ~ z
#integrate(function(x) IPM_desc$demo_fns$grow(x, z, params), U, Inf)$value
inter <- function(x) {
integrate(function(x) IPM_desc$demo_fns$grow(x, x, params), x, Inf)$value
}
fac2 <- sapply(upper_lims,inter)
props <- fac1 * fac2
props[props>0.01] <- 0
upper_lim <- upper_lims[which.max(props)]
return(upper_lim)
}
kernel_res <- function(params,IPM_desc,units_per_sd = 25){
min_sd <- min(params["rcsz.sd"],params["grow.sd"])
return(units_per_sd/min_sd)
}
# IPM_desc object
IPM_desc <- list(
states = states,
states_z = states_z,
kernels = kernels,
par_info = par_info,
par_fns = par_fns,
demo_fns = demo_fns,
kernel_fns = kernel_fns,
limit_lower = limit_lower,
limit_upper = limit_upper,
kernel_res = kernel_res
)Running plot_diagram(IPM_desc = IPM_desc) produce a diagram produced by DiagrammR. Use DiagrammeR::render_graph(IPM_diagram) to render the diagram.
IPM_diagram <- plot_diagram(IPM_desc = IPM_desc)
#render_graph(IPM_diagram)
#export
library(DiagrammeRsvg)
library(rsvg)
IPM_diagram %>%
export_graph(
file_name = "IPM_descs/IPM_desc_basic_graph.svg",
file_type = "SVG",
width = 700,
height = 600)Create a parameter set or use a sampler to sample parameter sets. I have been using an Adaptive Metropolis Algorithym with delayed rejection from the R package FME. This requires some informative priors.
Whenever I'm using parameter sets my convention is to refer to an incomplete parameter set (aka only the parameters that are sampled directly) as params_ic (as in parameters incomplete) and full parameter sets as params_c (as in, parameters complete).
# a starting incomplete parameter set
params_ic <- c(
surv.int = 0,
surv.z = 2,
flow.int = 0.3,
flow.z = 0.1,
grow.int = 0.2,
grow.sd = 0.25,
rcsz.sd = 0.3,
seed.int = 2.35,
seed.z = 2.37,
p.r = 0.4
)
# create a complete parameter set
params_c <- make_full_params(params_ic = params_ic, IPM_desc = IPM_desc)Once you have a parameter set you can create an IPM with:
IPM <- make_kernels(params_c,IPM_desc)This IPM can be discritised to an MPM by specifying a target stabe stage distribution for the MPM like so:
qtiles <- c(0,0.2,0.4,0.7,1)
MPM <- make_MPM (params_c, IPM_desc, qtiles, submesh_res = 200)lambda doesn't change in the discretisation process and you can use the calc_dom_eig() function to calculate the dominant eigenvalue from the mega kernel:
We also result with the same stabe stage distribuion as the qtiles that we specified above.
plot_MIPM(IPM)plot_MIPM(MPM)calc_dom_eig(IPM)$lambda## [1] 4.16543
calc_dom_eig(MPM)$lambda## [1] 4.1656
diff(qtiles)## [1] 0.2 0.2 0.3 0.3
as.numeric(calc_dom_eig(MPM)$w)## [1] 0.1954836 0.1983487 0.3008145 0.3053532
You can also plot the individual trajectories as IPM diagnosis with plot_trajectories(MIPM,pop_n = 100,t_steps = 50)
This currently only works for IPMs, not discretised MPMs
plot_trajectories(IPM,pop_n = 100,t_steps = 50)I have been using an Adaptive Metrpolis algorithm implemented with FME::modMCMC with success.
First, define a function that returns -2 x log(likelihood) from an incomplete parameter set. It it also useful to define a similar prior function that also returns -2 x log(likelihood) from an incomplete parameter set.
likelihood_function <- function(params_ic,IPM_desc){
# create the full parameter set
params_c <- make_full_params(params_ic = params_ic, IPM_desc = IPM_desc)
# make the IPM using simpopmods
IPM <- make_kernels(params_c,IPM_desc = IPM_desc)
# calculate lambda, at a fairly high tolerence for speed
eigens <- calc_dom_eig(IPM,tol = 0.005)
growth_rate <- eigens$lambda
# calculate log likelihood
likelihood <- sum(dnorm(growth_rate,mean = 1, sd = 0.1))
log_likelihood <- log(likelihood)
#return
return(-2*log_likelihood)
}
#check it correctly gives a likelihood value
likelihood_function(params_ic,IPM_desc = IPM_desc)## [1] 999.2367
#Here's an example prior function for the parameters on the survival function:
IPM_prior_fn <- function(params_ic){
params <- as.list(params_ic)
log_likelihood <- log(dnorm(params$surv.int,mean = 1, sd = 1)) #surv int
log_likelihood <- log_likelihood + log(dnorm(params$surv.z,mean = 2, sd = 2)) # surv.z
return(-2*log_likelihood)
}Run the sampler
#load in the FME library
library(FME)## Loading required package: deSolve
## Loading required package: rootSolve
## Loading required package: coda
#extract the upper and lower limits from IPM desc to use as arguments for the modMCMC function
lower <- IPM_desc$par_info$min[IPM_desc$par_info$samp]
upper <- IPM_desc$par_info$max[IPM_desc$par_info$samp]
# jump is the starting step size
jump <- (upper-lower)/100
start_time <- Sys.time()
# run the sampler, use ?modMCMC for more information, the documentation is quite good.
samples <- modMCMC(f = likelihood_function, # -2*log likelihood function
p = params_ic, # starting parameter set
IPM_desc = IPM_desc,
niter = 200, # number of iterations
updatecov = 500, # how many iterations after which to update the covariance matrix
burninlength = 0, # how many iterations to keep updating covariance matrix
lower = lower, # lower bounds
upper = upper, # upper bounds
jump = jump,
ntrydr = 3 # number of tries (delayed rejection)
)## number of accepted runs: 102 out of 200 (51%)
# how long did this take?
Sys.time() - start_time## Time difference of 2.007547 mins
#extract the parameter sets from the MCMC object and generate a full set of parameters to include those that cwere not sampled directly
param_sets_ic <- samples$pars
param_sets <- t(apply(param_sets_ic,FUN = make_full_params,1,IPM_desc=IPM_desc))
# use unique() to remove cases where the chain failed to jump to a new parameter set
param_sets <- as.data.frame(unique(param_sets))
# pariwise plot
pairs(param_sets,pch = ".")Calculate lambda from the parameter sets to see that lambda is ~ 1 (or at least converging towards in this short run)
# apply function to go throug heach parameter set, construct and IPM and calculated lambda.
param_sets$lambda <- apply(param_sets,
FUN = function(params_c,IPM_desc){
IPM <- simpopmods::make_kernels(params_c,IPM_desc)
return(calc_dom_eig(IPM)$lambda)
},
MARGIN = 1,
IPM_desc = IPM_desc
)
# a quick little plot
plot(param_sets$lambda)That's just a taster. You need a lots of burn in and thinning to remove autocorrelation to generate anything vaguely useful.
This R package has tests inplemented with R package testthat
These test to make sure that:
- Lambda does not change during discretisation from IPM to MPM
- Stable stage distribution of resulting MPM is the same as the target specified quantiles
The tests uses three different IPM descriptors.
Simon Rolph - srolph1@sheffield.ac.uk






