#Code to replicate results in hmde paper
# install.packages("remotes")
#remotes::install_github("traitecoevo/hmde@a79baae6c2df22789cccc1fd96eb3a55da9628ec")
{
library(hmde)
library(ggplot2)
library(cowplot)
library(tidyverse)
library(rstan)
library(deSolve)
}
#-----------------------------------------------------------------------------#
# Figure 1
set.seed(2025)
{
#Analytic solution for vB model
solution <- function(t, pars = list(y_0, beta, S_max)){
return(
pars[[3]] + (pars[[1]] - pars[[3]])*exp(-t * pars[[2]])
)
}
#Set up 'observation' data
pars_1 <- list(
y_0 = 1,
beta = 0.5,
S_max = 7
)
pars_2 <- list(
y_0 = 1,
beta = 0.8,
S_max = 12
)
obs_time <- 0:9
obs_data <- tibble(
ind_id = c(
rep(1, times = length(obs_time)),
rep(2, times = length(obs_time))
),
y_true = c(
solution(obs_time, pars = pars_1),
solution(obs_time, pars = pars_2)
),
time = c(obs_time, obs_time)
) %>%
mutate(y_obs = y_true + rnorm((length(obs_time)*2), 0, 0.2),
obs_index = time + 1)
obs_plot <-
ggplot(data=obs_data, aes(group = ind_id)) +
geom_point(aes(x = time, y=y_obs,
colour = as.factor(ind_id),
shape = as.factor(ind_id)),
size = 2, stroke = 1
) +
geom_line(aes(x = time, y=y_obs, colour = as.factor(ind_id)),
linetype = "dashed", alpha = 0.5) +
scale_shape_manual(values = c(1,3)) +
labs(title = "Repeat observations over time", x="Time", y="Observed size", colour="Individual",
shape = "Individual", linetype = "Individual") +
theme_classic() +
theme(legend.position = "inside",
legend.position.inside = c(0.8, 0.2),
axis.ticks = element_blank(),
axis.text = element_blank())
#Fit model
data <- tibble(
ind_id = obs_data$ind_id,
time = obs_data$time,
y_obs = obs_data$y_obs,
obs_index = obs_data$obs_index
)
fit <-
hmde_model("vb_multi_ind") |>
hmde_assign_data(data) |>
hmde_run(chains = 4,
cores = 4,
iter = 2000)
#Diag plots
diag_plot <- traceplot(fit, pars=c("ind_max_size", "ind_growth_rate"), inc_warmup=FALSE)
diag_plot
#plots of sizes over time
ests <- hmde_extract_estimates(fit, data)
fit_plot <-
ggplot(data=ests$measurement_data, aes(group = ind_id)) +
geom_point(aes(x = time, y=y_obs,
colour = as.factor(ind_id),
shape = as.factor(ind_id)),
size = 2, stroke = 1
) +
geom_line(aes(x = time, y=y_obs, colour = as.factor(ind_id)),
linetype = "dashed", alpha = 0.5) +
geom_line(aes(x = time, y=y_hat),
colour = "black", alpha = 0.5,
linetype = "solid") +
geom_point(aes(x = time, y=y_hat), colour = "black",
shape = 2, size = 2, stroke = 1, alpha = 0.8) +
scale_shape_manual(values = c(1,3)) +
labs(x="Time", y="Observed size", colour="Individual",
shape = "Individual", linetype = "Individual",
title = "Observed and estimated sizes over time") +
theme_classic() +
theme(legend.position = "inside",
legend.position.inside = c(0.8, 0.2),
axis.ticks = element_blank(),
axis.text = element_blank())
fit_plot
fitted_function_plot <- hmde_plot_de_pieces(estimate_list = ests,
xlab = "Size",
ylab = "Growth rate",
title = "Fitted growth functions",
colour = "black",
alpha = 0.8) +
theme(axis.ticks = element_blank(),
axis.text = element_blank())
fitted_function_plot
#Theoretical size over time plots
theory_pars_list <- list(
const_pars = list(beta = 1.5),
vb_pars = list(beta = 0.3, S_max = 15),
canham_pars = list(g_max = 0.8,
y_max = 8,
k = 1)
)
y_0 = 1
times <- seq(from = 0, to = 50, by = 0.01)
#Constant
const_size_time_data <- tibble(
time = times[c(1, length(times))]
) %>%
mutate(y_true = y_0 + theory_pars_list$const_pars[[1]] * time)
const_size_time_plot <-
ggplot(const_size_time_data, aes(x = time, y = y_true)) +
geom_line(colour = "black", linewidth = 1) +
labs(x="Time", y="Size") +
theme_classic() +
theme(axis.ticks = element_blank(),
axis.text = element_blank())
#vB
vb_size_time_plot <-
ggplot() +
geom_function(fun=solution,
args=list(pars = c(y_0 = 1,
theory_pars_list$vb_pars)),
colour="black", linewidth=1, alpha = 1,
xlim=c(0,20)) +
labs(x="Time", y="Size") +
theme_classic() +
theme(axis.ticks = element_blank(),
axis.text = element_blank())
#Canham
#Build data
Canham_DE <- function(Time, State, Pars) { #Pars: g_max, y_max, k
with(as.list(c(State, Pars)), {
dY <- g_max * exp(-0.5 * (log(Y / y_max) / k)^2)
return(list(c(dY)))
})
}
pars_combo <- c(g_max = 0.8,
y_max = 8,
k = 1)
times <- seq(0, 50, by = 0.001)
yini <- c(Y = 1) #Initial condition
canham_true_y <- ode(yini, times, Canham_DE, pars_combo, method = "rk4")[,2]
canham_true_data <- tibble(
y = canham_true_y,
time = times
)
canham_size_time_plot <-
ggplot(canham_true_data, aes(x = time, y = y)) +
geom_line(colour = "black", linewidth = 1) +
labs(x="Time", y="Size") +
theme_classic() +
theme(axis.ticks = element_blank(),
axis.text = element_blank())
#Get size limits
y_0_vec <- c(
const_size_time_data$y_true[1],
1,
canham_true_data$y[1]
)
y_final_vec <- c(
max(const_size_time_data$y_true),
theory_pars_list$vb_pars$S_max,
max(canham_true_data$y)
)
#Function plots
function_plots <- list()
de_name <- c("constant_single_ind",
"vb_single_ind",
"canham_single_ind")
for(i in 1:3){
DE_function <- hmde_model_des(de_name[i])
args_list <- list(
pars = theory_pars_list[[i]]
)
function_plots[[i]] <-
ggplot() +
xlim((y_0_vec[i]-1), (y_final_vec[i]+1)) +
labs(x = "Size", y = "Growth rate") +
geom_function(fun=DE_function, args=args_list,
colour="black", linewidth=1, alpha = 1,
xlim=c(y_0_vec[i], y_final_vec[i])) +
theme_classic()+
theme(axis.ticks = element_blank(),
axis.text = element_blank())
}
function_selection <- plot_grid(
function_plots[[1]], const_size_time_plot,
function_plots[[2]], vb_size_time_plot,
function_plots[[3]], canham_size_time_plot,
nrow = 3,
byrow = TRUE,
align = "hv"
)
function_selection
#Produces the plots for Figure 1, excluding the screenshot of code
fig_1_pieces <- plot_grid(
obs_plot,
function_selection,
diag_plot,
fitted_function_plot,
fit_plot,
nrow = 3,
byrow = TRUE
)
print(fig_1_pieces)
}
## Warning: There were 45 divergent transitions after warmup. See
## https://mc-stan.org/misc/warnings.html#divergent-transitions-after-warmup
## to find out why this is a problem and how to eliminate them.
## Warning: Examine the pairs() plot to diagnose sampling problems

#-----------------------------------------------------------------------------#
# Constant function demonstration
set.seed(2025)
{
histogram_func <- function(data, var, main, xlab, ...){
ggplot2::ggplot(data = data, aes(x = {{var}})) +
geom_histogram(colour = "black", fill = "lightblue", ...) +
labs(title = main,
x= xlab) +
theme_classic()
}
trout_constant_fit <-
hmde_model("constant_multi_ind") |>
hmde_assign_data(data = Trout_Size_Data) |>
hmde_run(chains = 4, cores = 4, iter = 2000)
#Diag plots, requires large image size to see properly
diag_plot_growth_rate <- traceplot(trout_constant_fit,
pars="ind_beta",
inc_warmup=FALSE)
print(diag_plot_growth_rate)
trout_estimates <-
hmde_extract_estimates(trout_constant_fit,
input_measurement_data = Trout_Size_Data)
trout_estimates
#relationship between estimate and observed sizes
cor(trout_estimates$measurement_data$y_obs, trout_estimates$measurement_data$y_hat)^2
r_sq_est <- cor(trout_estimates$measurement_data$y_obs,
trout_estimates$measurement_data$y_hat)^2
r_sq <- paste0("R^2 = ",
signif(r_sq_est,
digits = 3))
obs_est_size_plot <- ggplot(data = trout_estimates$measurement_data,
aes(x = y_obs, y = y_hat)) +
geom_point(shape = 16, size = 1, colour = "green4") +
xlab("Y obs.") +
ylab("Y est.") +
geom_abline(slope = 1, linetype = "dashed") +
annotate("text", x = 45, y = 80,
label = r_sq) +
theme_classic()
#Plots of size over time for a sample of 5 individuals
size_over_time_plot <- hmde_plot_obs_est_inds(n_ind_to_plot = 5,
measurement_data =
trout_estimates$measurement_data) +
theme(legend.position = "inside",
legend.position.inside = c(0.8,0.3)) +
guides(colour=guide_legend(ncol=2))
ind_hist_beta <- histogram_func(trout_estimates$individual_data,
ind_beta_mean,
main = "Individual beta parameters",
xlab = "beta estimate")
de_pieces <- hmde_plot_de_pieces(trout_estimates)
#Figure 2
fig_2 <- plot_grid(
obs_est_size_plot,
size_over_time_plot,
de_pieces,
ind_hist_beta,
nrow = 2,
byrow = TRUE,
labels = c("(a)", "(b)",
"(c)", "(d)")
)
print(fig_2)
#Table 1: species-level hyper parameters
exp_mean <- trout_estimates$population_data[1,c(1,2,4,5)] %>%
mutate(mean = exp(mean),
CI_lower = exp(CI_lower),
CI_upper = exp(CI_upper),
par_name = "pop_beta_mu")
sp_ests <- rbind(trout_estimates$population_data[,c(1,2,4,5)],
exp_mean)
print(sp_ests)
}
## Warning: There were 1 divergent transitions after warmup. See
## https://mc-stan.org/misc/warnings.html#divergent-transitions-after-warmup
## to find out why this is a problem and how to eliminate them.
## Warning: Examine the pairs() plot to diagnose sampling problems
## Warning: Bulk Effective Samples Size (ESS) is too low, indicating posterior means and medians may be unreliable.
## Running the chains for more iterations may help. See
## https://mc-stan.org/misc/warnings.html#bulk-ess
## Warning: Tail Effective Samples Size (ESS) is too low, indicating posterior variances and tail quantiles may be unreliable.
## Running the chains for more iterations may help. See
## https://mc-stan.org/misc/warnings.html#tail-ess
## `stat_bin()` using `bins = 30`. Pick better value with `binwidth`.


## # A tibble: 3 × 4
## par_name mean CI_lower CI_upper
## <chr> <dbl> <dbl> <dbl>
## 1 pop_log_beta_mu 0.890 0.595 1.13
## 2 pop_log_beta_sigma 0.474 0.183 0.808
## 3 pop_beta_mu 2.44 1.81 3.09
#-----------------------------------------------------------------------------#
# von Bertalanffy function demonstration
set.seed(2025)
{
lizard_vb_fit <- hmde_model("vb_multi_ind") |>
hmde_assign_data(data = Lizard_Size_Data) |>
hmde_run(chains = 4, cores = 4, iter = 2000)
diag_max_size <- traceplot(lizard_vb_fit,
pars="ind_max_size",
inc_warmup=FALSE)
diag_growth_rate <- traceplot(lizard_vb_fit,
pars="ind_growth_rate",
inc_warmup=FALSE)
print(diag_max_size)
print(diag_growth_rate)
lizard_estimates <- hmde_extract_estimates(fit = lizard_vb_fit,
input_measurement_data = Lizard_Size_Data)
#Quantitative R^2
r_sq_est <- cor(lizard_estimates$measurement_data$y_obs,
lizard_estimates$measurement_data$y_hat)^2
r_sq <- paste0("R^2 = ",
signif(r_sq_est,
digits = 3))
obs_scatter <- ggplot(data = lizard_estimates$measurement_data,
aes(x = y_obs, y = y_hat)) +
geom_point(shape = 16, size = 1, colour = "green4") +
xlab("Y obs.") +
ylab("Y est.") +
geom_abline(slope = 1, linetype = "dashed") +
annotate("text", x = 25, y = 18,
label = r_sq) +
theme_classic()
#Plots of size over time for a sample of 5 individuals
obs_est_ind <- hmde_plot_obs_est_inds(n_ind_to_plot = 5,
measurement_data =
lizard_estimates$measurement_data) +
theme(legend.position = "inside",
legend.position.inside = c(0.8, 0.2))
#1-dimensional parameter distributions
s_max_hist <- ggplot(lizard_estimates$individual_data,
aes(ind_max_size_mean)) +
geom_histogram(bins = 10,
colour = "black",
fill = "lightblue") +
labs(x="S_max estimate") +
theme_classic()
beta_hist <- ggplot(lizard_estimates$individual_data,
aes(ind_growth_rate_mean)) +
geom_histogram(bins = 10,
colour = "black",
fill = "lightblue") +
labs(x="beta estimate") +
theme_classic()
#2-dimensional parameter distribution
par_scatter <- ggplot(data = lizard_estimates$individual_data,
aes(x = ind_max_size_mean, y = ind_growth_rate_mean)) +
geom_point(shape = 16, size = 1, colour = "green4") +
xlab("Individual max sizes (mm)") +
ylab("Individual growth rate parameters") +
theme_classic()
#Correlation of parameters
cor(lizard_estimates$individual_data$ind_max_size_mean,
lizard_estimates$individual_data$ind_growth_rate_mean,
method = "spearman")
#Plot function pieces over estimated sizes.
de_pieces <- hmde_plot_de_pieces(lizard_estimates)
#Figure 3
fig_3 <- plot_grid(
obs_scatter,
obs_est_ind,
de_pieces,
par_scatter,
s_max_hist,
beta_hist,
nrow = 3,
byrow = TRUE,
labels = c("(a)", "(b)",
"(c)", "(d)",
"(e)", "(f)")
)
print(fig_3)
exp_mean <- lizard_estimates$population_data[c(1,3),c(1,2,4,5)] %>%
mutate(mean = exp(mean),
CI_lower = exp(CI_lower),
CI_upper = exp(CI_upper))
exp_mean$par_name <- c("pop_max_size_mean", "pop_growth_rate_mean")
sp_pars <- rbind(lizard_estimates$population_data[,c(1,2,4,5)],
exp_mean)
print(sp_pars)
}
## Warning: There were 526 divergent transitions after warmup. See
## https://mc-stan.org/misc/warnings.html#divergent-transitions-after-warmup
## to find out why this is a problem and how to eliminate them.
## Warning: There were 2 chains where the estimated Bayesian Fraction of Missing Information was low. See
## https://mc-stan.org/misc/warnings.html#bfmi-low
## Warning: Examine the pairs() plot to diagnose sampling problems
## Warning: The largest R-hat is NA, indicating chains have not mixed.
## Running the chains for more iterations may help. See
## https://mc-stan.org/misc/warnings.html#r-hat
## Warning: Bulk Effective Samples Size (ESS) is too low, indicating posterior means and medians may be unreliable.
## Running the chains for more iterations may help. See
## https://mc-stan.org/misc/warnings.html#bulk-ess
## Warning: Tail Effective Samples Size (ESS) is too low, indicating posterior variances and tail quantiles may be unreliable.
## Running the chains for more iterations may help. See
## https://mc-stan.org/misc/warnings.html#tail-ess



## # A tibble: 6 × 4
## par_name mean CI_lower CI_upper
## <chr> <dbl> <dbl> <dbl>
## 1 pop_log_max_size_mean 3.20 3.18 3.22
## 2 pop_log_max_size_sd 0.0323 0.0141 0.0504
## 3 pop_log_growth_rate_mean -3.98 -4.23 -3.76
## 4 pop_log_growth_rate_sd 0.0783 0.0112 0.224
## 5 pop_max_size_mean 24.5 24.0 25.1
## 6 pop_growth_rate_mean 0.0187 0.0146 0.0232
#-----------------------------------------------------------------------------#
# Canham function demonstration
# Due to the runtime we skip fitting the model and use provided model fit in Tree_Size_Ests object
if(FALSE){ #Re-run model to reproduce Tree_Size_Ests results
set.seed(2025)
tree_fit <-
hmde_model("canham_multi_ind") |>
hmde_assign_data(data = Tree_Size_Data) |>
hmde_run(chains = 4, cores = 4, iter = 2000)
Tree_Size_Ests <-
hmde_extract_estimates(
fit = tree_fit,
input_measurement_data = Tree_Size_Data)
}
set.seed(2025)
{
#Quantitative R^2
r_sq_est <- cor(Tree_Size_Ests$measurement_data$y_obs,
Tree_Size_Ests$measurement_data$y_hat)^2
r_sq <- paste0("R^2 = ",
signif(r_sq_est,
digits = 3))
obs_est_scatter <- ggplot(data = Tree_Size_Ests$measurement_data,
aes(x = y_obs, y = y_hat)) +
geom_point(shape = 16, size = 1, colour = "green4") +
xlab("Y obs.") +
ylab("Y est.") +
geom_abline(slope = 1, linetype = "dashed") +
annotate("text", x = 7, y = 22,
label = r_sq) +
theme_classic()
#Plots of size over time for a sample of 5 individuals
obs_est_size_plot <- hmde_plot_obs_est_inds(n_ind_to_plot = 5,
measurement_data = Tree_Size_Ests$measurement_data) +
theme(legend.position = "top") +
guides(colour=guide_legend(nrow=2,byrow=TRUE))
#1-dimensional parameter distributions
gmax_hist <- ggplot(Tree_Size_Ests$individual_data,
aes(ind_max_growth_mean)) +
geom_histogram(bins = 10,
colour = "black",
fill = "lightblue") +
labs(x="g_max estimate") +
theme_classic()
Smax_hist <- ggplot(Tree_Size_Ests$individual_data,
aes(ind_size_at_max_growth_mean)) +
geom_histogram(bins = 10,
colour = "black",
fill = "lightblue") +
labs(x="S_max estimate") +
theme_classic()
k_hist <- ggplot(Tree_Size_Ests$individual_data,
aes(ind_k_mean)) +
geom_histogram(bins = 10,
colour = "black",
fill = "lightblue") +
labs(x="k estimate") +
theme_classic()
#2-dimensional parameter distributions
pairplot1 <- ggplot(data = Tree_Size_Ests$individual_data,
aes(x = ind_max_growth_mean, y = ind_size_at_max_growth_mean)) +
geom_point(shape = 16, size = 1, colour = "green4") +
xlab("Ind. max growth (cm/yr)") +
ylab("Ind. size at max growth (cm)") +
theme_classic()
pairplot2 <- ggplot(data = Tree_Size_Ests$individual_data,
aes(x = ind_k_mean, y = ind_max_growth_mean)) +
geom_point(shape = 16, size = 1, colour = "green4") +
ylab("Ind. max growth (cm/yr)") +
xlab("Ind. spread par.") +
theme_classic()
pairplot3 <- ggplot(data = Tree_Size_Ests$individual_data,
aes(x = ind_size_at_max_growth_mean, y = ind_k_mean)) +
geom_point(shape = 16, size = 1, colour = "green4") +
ylab("Ind. spread par.") +
xlab("Ind. size at max growth (cm)") +
theme_classic()
est_de_plot <- hmde_plot_de_pieces(Tree_Size_Ests)
#Figure 4
piece_1 <- plot_grid(
obs_est_scatter,
obs_est_size_plot,
est_de_plot,
nrow = 1,
byrow = TRUE,
labels = c("(a)", "(b)",
"(c)")
)
piece_2 <-
plot_grid(
gmax_hist,
Smax_hist,
k_hist,
pairplot1,
pairplot3,
pairplot2,
nrow = 2,
byrow = TRUE,
labels = c("(d)",
"(e)", "(f)",
"(g)", "(h)",
"(i)"),
align = "hv"
)
fig_4 <- plot_grid(piece_1, piece_2, nrow = 2,
rel_heights = c(1/3,2/3))
print(fig_4)
exp_mean <- Tree_Size_Ests$population_data[c(1,3,5),c(1,2,4,5)] %>%
mutate(mean = exp(mean),
CI_lower = exp(CI_lower),
CI_upper = exp(CI_upper))
exp_mean$par_name <- c("pop_max_growth_mean",
"pop_size_at_max_growth_mean",
"pop_k_mean")
canham_sp_data <- rbind(Tree_Size_Ests$population_data[,c(1,2,4,5)],
exp_mean)
print(canham_sp_data)
}

## # A tibble: 9 × 4
## par_name mean CI_lower CI_upper
## <chr> <dbl> <dbl> <dbl>
## 1 pop_log_max_growth_mean -1.18 -1.35 -1.01
## 2 pop_log_max_growth_sd 0.438 0.324 0.565
## 3 pop_log_size_at_max_growth_mean 2.32 2.14 2.51
## 4 pop_log_size_at_max_growth_sd 0.385 0.236 0.553
## 5 pop_log_k_mean -0.564 -0.861 -0.219
## 6 pop_log_k_sd 0.584 0.352 0.867
## 7 pop_max_growth_mean 0.308 0.258 0.364
## 8 pop_size_at_max_growth_mean 10.2 8.52 12.3
## 9 pop_k_mean 0.569 0.423 0.804
sessionInfo()
## R version 4.4.3 (2025-02-28)
## Platform: x86_64-pc-linux-gnu
## Running under: Ubuntu 22.04.5 LTS
##
## Matrix products: default
## BLAS: /usr/lib/x86_64-linux-gnu/blas/libblas.so.3.10.0
## LAPACK: /usr/lib/x86_64-linux-gnu/lapack/liblapack.so.3.10.0
##
## locale:
## [1] LC_CTYPE=en_AU.UTF-8 LC_NUMERIC=C LC_TIME=en_AU.UTF-8
## [4] LC_COLLATE=en_AU.UTF-8 LC_MONETARY=en_AU.UTF-8 LC_MESSAGES=en_AU.UTF-8
## [7] LC_PAPER=en_AU.UTF-8 LC_NAME=C LC_ADDRESS=C
## [10] LC_TELEPHONE=C LC_MEASUREMENT=en_AU.UTF-8 LC_IDENTIFICATION=C
##
## time zone: Australia/Sydney
## tzcode source: system (glibc)
##
## attached base packages:
## [1] stats graphics grDevices utils datasets methods base
##
## other attached packages:
## [1] hmde_0.1.0 deSolve_1.40 rstan_2.32.7 StanHeaders_2.32.10
## [5] lubridate_1.9.4 forcats_1.0.0 stringr_1.5.1 dplyr_1.1.4
## [9] purrr_1.0.4 readr_2.1.5 tidyr_1.3.1 tibble_3.2.1
## [13] tidyverse_2.0.0 cowplot_1.1.3 ggplot2_3.5.2
##
## loaded via a namespace (and not attached):
## [1] utf8_1.2.5 generics_0.1.4 stringi_1.8.4 hms_1.1.3
## [5] magrittr_2.0.3 evaluate_1.0.1 grid_4.4.3 timechange_0.3.0
## [9] RColorBrewer_1.1-3 jsonlite_1.8.9 processx_3.8.6 pkgbuild_1.4.7
## [13] ps_1.9.1 gridExtra_2.3 QuickJSR_1.7.0 scales_1.4.0
## [17] codetools_0.2-19 cli_3.6.5 rlang_1.1.6 commonmark_1.9.2
## [21] remotes_2.5.0 withr_3.0.2 parallel_4.4.3 tools_4.4.3
## [25] inline_0.3.21 tzdb_0.4.0 rstantools_2.4.0 curl_6.0.1
## [29] mime_0.12 vctrs_0.6.5 R6_2.6.1 matrixStats_1.5.0
## [33] stats4_4.4.3 lifecycle_1.0.4 V8_6.0.0 pkgconfig_2.0.3
## [37] desc_1.4.3 callr_3.7.6 RcppParallel_5.1.10 pillar_1.10.2
## [41] gtable_0.3.6 loo_2.8.0 glue_1.8.0 Rcpp_1.0.14
## [45] xfun_0.49 tidyselect_1.2.1 rstudioapi_0.17.1 knitr_1.49
## [49] farver_2.1.2 labeling_0.4.3 compiler_4.4.3 markdown_1.13