This file contains the code and data needed for demonstrating how a systems modelling approach can be applied in practice and can be found on GitHub. It is the code for reproducing the examples in Box 1 and Box 2 for the manuscript:
Noble et al. 2025. A systems-modelling approach to predict biological responses to extreme heat. Trends in Ecology and Evolution, under review.
It also demonstrates how to couple models from different disciplines to predict the impacts of extreme heat on plant-insect interactions.
Please get in touch with Daniel Noble (daniel.noble@anu.edu.au) and Mike Kearney (m.kearney@unimelb.edu.au) if you have any questions about the code or the models.
1Introduction
Here, we provide the code for reproducing the systems-modelling approaches described in Boxes 1 & 2.
2Load packages
Our code is built using the statistical software R. We use a number of packages to run our simulations, including NicheMapR[1,2], microclima[3,4], and plantecophys[5].
We recommend that readers interested in working through this the code download our GitHub repository, which contains all the data required to run the models. This code file can also located in the docs/supp.qmd file. You should then be able to run the code in this document to reproduce the figures and tables without having to setup your repository.
The code below loads the required packages that are needed.
Code
# You will need to install a few packages first, a couple from GitHub as they are not on CRAN. Note that you will need to remove '#' from the code below to install the packages.# install.packages("pacman")# devtools::install_github('mrke/NicheMapR')# devtools::install_github("ilyamaclean/microclima")# Load required libraries after the installationpacman::p_load(devtools, deSolve, ggplot2, dplyr, tidyr, purrr, patchwork, equatags, flextable, NicheMapR, terra, microclima, plantecophys, magick, here, ggrepel, lutz, R.matlab, magick, grid, DT, latex2exp, popbio, popdemo)
Throughout we’ll also need some helper functions. These can be added to your working environment by running the code below.
Code
#' @title convert_doy#' @description Convert day of year (DOY) to date, month-day, ymd, or julian date#' @param doy Numeric vector of day of year#' @param year Numeric vector of year#' @param format_type Character vector of format type. Default is "date"#' @return Date, month, month-day, ymd, or julian date#' @examples#' convert_doy(120, 2024, "date") # "2024-04-29"#' convert_doy(120, 2024, "month_day") # "April 29" #' convert_doy(120, 2024, "ymd") # "2024-04-29"convert_doy <-function(doy, year, format_type ="date") {# Convert DOY to Date date <-as.Date(doy -1, origin =paste0(year, "-01-01"))# Return different formats based on format_typeswitch(format_type,"date"= date, # Default: Date format"month_day"=format(date, "%B %d"), # "April 29""month"=format(date, "%B"), # "April""ymd"=format(date, "%Y-%m-%d"), # "2024-04-29""julian"=format(date, "%Y%j"), # "2024120"stop("Invalid format_type. Choose from 'date', 'month_day', 'ymd', or 'julian'.") )}#' @title convert_time#' @description Convert decimal time to hh:mm, hh:mm:ss, or 12-hour format#' @param time_minutes Numeric vector of decimal time in minutes#' @param format_type Character vector of format type. Default is "hh:mm"#' @return Time in hh:mm, hh:mm:ss, or 12-hour format#' @examples#' convert_time(13.5, "hh:mm") # "13:30"#' convert_time(13.5, "hh:mm:ss") # "13:30:00"#' convert_time(13.5, "12-hour") # "1:30 PM"convert_time_for_plot <-function(time_minutes, format_type ="decimal") {# Extract hours and minutes hours <- time_minutes %/%60 minutes <- time_minutes %%60# Format output based on format_typeswitch(format_type,"decimal"= hours + minutes /60, # e.g., 13.5 for 13:30"POSIXct"=as.POSIXct(sprintf("%02d:%02d", hours, minutes), format="%H:%M", tz="UTC"), # e.g., "13:30:00 UTC""hms"= hms::hms(hours *3600+ minutes *60), # e.g., 13:30:00stop("Invalid format_type. Choose from 'decimal', 'POSIXct', or 'hms'.") )}
Note
gFortran can be a problem for NicheMapR so you need to set the paths properly if you are getting errors. Follow details here. The following code will help you set the paths properly.
Code
# But do the following:# 1. Install gfortran using homebrew in the terminal/bash window brew install gcc # 2. Take note of the path to gfortran. You'll need this for later which gfortran# 3. In home directory of terminal create a Makevars file mkdir ~/.R touch ~/.R/Makevars open ~/.R/Makevars# 4. Add the following to the Makevars file. Note that you need to make sure your version of fortran is correct and replace 124.2.0_1 with your version. You can find version in terminal by typing `gfortran --version`. In addition you need to replace the path to gfortran with the path you found in step 2. FC =/opt/homebrew/Cellar/gcc/14.2.0_1/bin/gfortran F77 =/opt/homebrew/Cellar/gcc/14.2.0_1/bin/gfortran FLIBS =-L/opt/homebrew/Cellar/gcc/14.2.0_1/lib/gcc/11#5. Restart R and try to install the package again via devtools. It should now work.
3Box 1 Code: Building a systems modelling approach to capture multi-species exposure to extreme heat
3.1 Getting the microclimate data
We first need to use climate data to calculate the microclimate at our location of interest. Here, we use the micro_silo function to calculate the microclimate at a location near Renmark, South Australia (longitude = 140.799, latitude = -34.159) for the years 2018-2019.
Code
# First, set the location where NCEAP climate data is located. This is a global dataset. We need this to calculate the microclimate. Only need to do once.get.global.climate(folder=here("NCEP_dan", "NCEAP" ))# Location we want longlat <-c(140.79970255974192, -34.159176277782564) # Near Renmark, South Australia micro <-micro_silo( loc = longlat, # Locationdstart ="01/01/2018", # Start yeardfinish ="31/12/2019", # End yearminshade =40, # Minimum shade, %maxshade =80, # Maximum shade, %Usrhyt =0.2, # Height above ground, m microclima =1, # Use mircoclima PC =-1500, # Water potential for stomatal closureSP =10, # Stability parameter for stomatal closureemail ="daniel.noble@anu.edu.au") # Replace with your email# Save the microclimate object for later in case we need it again as it avoids needing to rerun. save(micro, file =here("output", "microclim", "microclimate.RDa"))
3.2 Plant biophysical model with stomatal conductance
Now that we have the microclimate data, we can run the biophysical model to calculate the realised environmental conditions for a plant leaf using the ectotherm function in NicheMapR. We first need to set up the parameters for the plant leaf.
Code
# Load the microclimate objectload(here("output", "microclim", "microclimate.RDa"))# First, lets extract the relevant information from the microclimate object PDIFs <- micro$diffuse_frac # use variable diffuse fraction P_a <-get_pressure(micro$elev) # air pressure, Pa P_a <-rep(P_a, nrow(micro$metout)) # create hourly vector dates <- micro$dates # dates# Set leaf functional traits live <-0# don't simulate behaviour or respiration leaf <-1# use the stomatal conductance formulation for evaporation Ww_g <-1# wet weight, g shape <-2# 0=plate, 1=cylinder, 2=ellipsoid shape_b <-0.0025# ratio of b axis:a axis for ellipsoid shape_c <-0.1176# ratio of c axis:a axis for ellipsoid g_vs_ab <-0.4# leaf vapour conductance, abaxial (bottom of leaf), mol/m2/s g_vs_ad <-0.0# leaf vapour conductance, adaxial (top of leaf), mol/m2/s g_vs_ab_max <-0.4# leaf vapour conductance, abaxial (bottom of leaf), mol/m2/s g_vs_ad_max <-0.0# leaf vapour conductance, adaxial (top of leaf), mol/m2/s epsilon_sub <-0.95# emissivity of the substrate (-) epsilon_sky <-1# emissivity of the sky (-), accounted for already in 'TSKY' output from microclimate model so make it 1 epsilon_L <-0.97# emissivity of the leaf (-) alpha_L <-0.5# solar absorptivity of the leaf (-) fatosk <-0.5# radiation configuration factor to sky (-) fatosb <-0.5# radiation configuration factor to substrate (-) conv_enhance <-1.4# convective enhancement factor (-) pct_cond <-0# percent of leaf conducting to the ground (%) postur <-3# leaf oriented horizontally, not tracking sun M_1 <-0# make metabolic rate zero# Set up vapour conductance vectors and simulate stomatal closure at night f_open <-rep(1, nrow(micro$metout)) f_open[micro$metout[,"ZEN"] ==90] <-0# close stomata when the sun is set g_vs_abs <- g_vs_ab * f_open g_vs_ads <- g_vs_ad * f_open# Run the heat-budget model for the plant leaf. Note that the microclimate object is used to get the relevant environmental temperatures to make the calculations of leaf temperature (by default, ectotherm expects an object in the environment called 'micro' and uses it to obtain the relevant environmental data, but this can be overridden). plant <-ectotherm( leaf = leaf, live = live,Ww_g = Ww_g, shape = shape, shape_b = shape_b, shape_c = shape_c, g_vs_ab = g_vs_abs, g_vs_ad = g_vs_ads, g_vs_ab_max = g_vs_ab_max, g_vs_ad_max = g_vs_ad_max, epsilon_sub = epsilon_sub, epsilon_sky = epsilon_sky,epsilon = epsilon_L, alpha_max = alpha_L, alpha_min = alpha_L,M_1 = M_1,fatosk = fatosk, fatosb = fatosb, conv_enhance = conv_enhance, pct_cond = pct_cond, postur = postur,preshr = P_a, PDIF = PDIFs)# Save model plant$dates <- dates # Add in dates for claritysave(plant, file =here("output", "microclim", "plant_micro.RDa"))
3.3 Plant biophysical model with stomatal conductance from plantecophys
Allow for stomatal conductance to be simulated using the plantecophys package. We can do this by calculating stomatal conductance using the Photosyn function in plantecophys. This function requires vapour pressure deficit (VPD) which we can calculate from the microclimate data using the WETAIR function in NicheMapR.
Code
# Need to extract a few things from the plant object we just created# Load the plant objectload(here("output", "microclim", "plant_micro.RDa"))# Include simulated stomatal conductance from plantecophys UMOLPERJ <-4.57# mu_mol photons / JWETAIR_out <-WETAIR(db = plant$metout[,"TALOC"], rh = plant$metout[,"RHLOC"], bp = P_a) VPD_Pa <- WETAIR_out$esat - WETAIR_out$e photo_out <-Photosyn(Tleaf = plant$environ[,"TC"], # Leaf temperature (C)g1 =5.2, # Parameter of Ball-Berry modelCa =400, # Atmospheric CO2 concentration (ppm)VPD = VPD_Pa/1000, # Vapour pressure deficit (kPa)vpdmin =0.5, # lower threshold on VPDPPFD = plant$metout[,"SOLR"] * UMOLPERJ *1, # mu mol m-2 s-1Patm = P_a/1000# Atmospheric pressure (kPa) )g_vs_abs_pho <- photo_out$GS
Now calculate leaf temperature using these stomatal conductance values from Photosyn. Note that we have re-written the g_vs_abs_pho to capture the stomatal conductance from Photosyn. We just replace this in the plant biophysical model.
Code
# Rerun the models. Note that the parameters as the same as above except for g_vs_ab which is now from plantecophys plant2 <-ectotherm( leaf = leaf, live = live,Ww_g = Ww_g, shape = shape, shape_b = shape_b, shape_c = shape_c, g_vs_ab = g_vs_abs_pho, g_vs_ad = g_vs_ads, g_vs_ab_max = g_vs_ab_max, g_vs_ad_max = g_vs_ad_max, epsilon_sub = epsilon_sub, epsilon_sky = epsilon_sky,epsilon = epsilon_L, alpha_max = alpha_L, alpha_min = alpha_L,M_1 = M_1,fatosk = fatosk, fatosb = fatosb, conv_enhance = conv_enhance, pct_cond = pct_cond, postur = postur,preshr = P_a, PDIF = PDIFs)# Save model plant2$dates <- dates # Add in dates for claritysave(plant2, file =here("output", "microclim", "plant2_micro.RDa"))
Now, lets have a look at the leaf temperature when stomatal closure behaviour is incorporated.
Code
# Load the plant objectload(here("output", "microclim", "plant_micro.RDa"))load(here("output", "microclim", "plant2_micro.RDa"))# Extract the microclimate temperatures. Because this was simulated for a plant the 'body tempertaure'is the leaf temperature and is stored as 'TC' in the environ object. Note now that this new biophysical model has stomatal behaviour incorporated in the new plant2 object. environ_plant <-as.data.frame(plant$environ) environ_plant$dates <- plant$dates # For clarity add in dates from microclimate that ecto model uses environ_plant_plantecophys2 <-as.data.frame(plant2$environ) environ_plant_plantecophys2$dates <- plant2$dates # For clarirty add in dates from microclimate that ecto model uses# Re-organise the data for plotting environ_plant2 <- environ_plant %>%filter(dates >"2019-01-15"& dates <"2019-01-18") %>%select(dates, TA, TC) %>%pivot_longer(cols =c(TA, TC), names_to ="Temps", values_to ="Temperature") %>%mutate(Temps =ifelse(Temps =="TA", "Air Temperature (2 m) (40% shade)", "Leaf Temperature (Stomata Open) (40% shade)")) environ_plant_plantecophys <- environ_plant_plantecophys2 %>%filter(dates >"2019-01-15"& dates <"2019-01-18") %>% dplyr::select(dates, TC) %>%pivot_longer(cols =c(TC), names_to ="Temps", values_to ="Temperature") %>%mutate(Temps =ifelse(Temps =="TC", "Leaf Temperature (Stomatal Closure) (40% shade)"))# Combine the two dataframes environ_plant_plantecophys and environ_plant2 new_environ_plant2 <-rbind(environ_plant_plantecophys, environ_plant2)# Produce element of the fgure in box 1. Note it was adjusted in Biorender.com for final version.ggplot(new_environ_plant2) +geom_line(aes(y = Temperature, x = dates, col = Temps), show.legend =TRUE, size =0.8) +labs(y ="Temperature (°C)", x ="Date") +theme_classic() +theme( axis.text.x =element_text(angle =45, vjust =1, hjust =1),legend.position ="none", legend.text =element_text(size =14), axis.text =element_text(size =14), axis.title =element_blank()) +scale_colour_viridis_d()# ggsave(here("figs", "fig_plant_temp2_box1.png"), width = 8, height = 5, dpi = 600)
Here, we can see that stomatal closure in the middle of the day causes the leaf temperatures to rise substantially compared to if they were always open and transpiring (i.e., evaporative cooling). This is important because it means that the grasshopper will be exposed to higher temperatures when plant stomata close midday.
3.4 Run biophysical-physiological model for two grasshoppers using the plant microclimate as the environment for grasshoppers
Define the biophysical model parameters for the grasshopper. Note we also include DEB model parameters here, which relate to Box 2, but it’s easier to run the model once with all the parameters defined and we can (and will) extract what we need for Box 1.
Code
# We first need to load the DEB model paraemeters. These are estimated using MatLab. We've already estimated these so we'll simply just load them into R. pars <-readMat(here("sims", "results_Warramaba_virgo.mat"))par.names <-unlist(labels(pars$par))for(i in1:length(par.names)){assign(par.names[i], unlist(pars$par[i]))} pars <-readMat(here("sims", "stat_Warramaba_virgo.mat"))par.names <-unlist(labels(pars$stat))for(i in1:length(par.names)){assign(par.names[i], unlist(pars$stat[i]))}# Additional parameters we need E_sm <-1800# J/cm^2, maximum stomach energy content fract <-1# scaling factor to change size# Inital state for DEB model# Egg Stage. Starting conditions E_init <- (E.0* fract ^4) / (3E-9* fract ^3) E_H_init <-0 V_init <-3E-9 stage <-0# Life history traits clutchsize <-15# Eggs per clutch stages <-7# Number of stages, 0 = egg, 1-5 = nymphs, 6 = pre egg-laying adult, 7 = post-laying adult S_instar <-rep(s.1, stages) # Stress at instar n: L_n^2/ L_n-1^2 (-)}\cr} L_b <-0.09# Structural length at birth (cm)}\cr} photostart <-0# no photoperiod effect on reproductionphotofinish <-0# no photoperiod effect on reproduction reset <-0# don't reset lifecycle if death occurs soilnode <-3# depth (soil node) at which eggs laid - 3 = 5 cm sG <-0.001# Gompertz stress coefficient# Thermoregulatory parameters Ww_g <-0.2# g, body mass, not used though because DEB model computes this shape <-1# cylinder shape_b <-10# ratio of long to short axis of cylinder T_pref <-40# preferred body temperature (animal will attempt to regulate as close to this value as possible) T_F_min <-13# degrees C, lower body temperature for foraging T_F_max <-42# degrees C, upper body temperature for foraging T_B_min <-13# degrees C, minimum basking temperature T_RB_min <-13# degrees C, temperature at which animal will move to a basking site CT_max <-50# degrees C, critical thermal maximum (animal will die if CT_kill = 1 and this threshold is exceeded) CT_min <-0# degrees C, critical thermal minimum (used by program to determine depth selected when inactive and burrowing)# Behavioural traits (made stage-specific later) diurn <-1# Diurnal activity allowed (1) or not (0)? nocturn <-1# Nocturnal activity allowed (1) or not (0)? crepus <-1# Crepuscular activity allowed (1) or not (0)? burrow <-0# Shelter in burrow allowed (1) or not (0)? shade_seek <-0# Shade seeking allowed (1) or not (0)? pct_cond <-0# Percentage of animal surface contacting the substrate (\%)
Run the biophysical-physiological model species A.
Code
# First, we need to load in the two microclimates scenarios. We'll do this in a loop so that we can run the DEB model for each of the two microclimates. So, here we'll just create a few vectors that cell specific objects we need. plant_micro <-c("plant_micro.RDa") plant_mirco_names <-c("stomata_closed")# We also need to load in the original microclimate object so that we can get the dates and other information from it. This is the original microclimate object that we used to simulate the plant microclimate. We'll also overide the `mirco` object so that the ectotherm model can use it.load(here("output", "microclim", "microclimate.RDa"))load(here("output", "microclim", "plant_micro.RDa"))# Use the plant microclimate data to reset the original microclimate object. Note that we need to adjust both shade and nonshade conditions# Unshaded microclimate micro$metout <- plant$metout micro$metout [,"TALOC"] <- plant$environ[,"TC"]# Update the IR profiles. micro$metout[,"TSKYC"] <- plant$environ[,"TSKY"] # Update the relative humidity. micro$metout[,"RHLOC"] <- plant$environ[,"RELHUM"]# Shaded microclimate microshadmet <- plant$shadmet micro$shadmet [,"TALOC"] <- plant$environ [,"TC"]# Soil microclimate micro$soil <- plant$soil micro$shadsoil <- plant$shadsoil micro$soilmoist <- plant$soilmoist micro$shadmoist <- plant$shadmoist# Get dates from microclimate object dates <- micro$dates # dates, hourlydates2 <- micro$dates2 # dates, daily# Set starting date by truncating microclimate model output to when grasshoppers are epected to oviposit eggs. This is the date when the life cylce of the insect is expected to start.oviposition_date <-'2018-03-31 23:00:00' subs <-which( dates >as.POSIXct(oviposition_date)) subs2 <-which(dates2 >as.POSIXct(oviposition_date))# Now run the biophysical model for the grasshopper ectoA <-ectotherm( Ww_g = Ww_g,shape = shape,shape_b = shape_b,T_pref = T_pref,T_F_min = T_F_min,T_F_max = T_F_max,T_B_min = T_B_min,T_RB_min = T_RB_min,CT_min = CT_min,CT_max = CT_max,diurn = diurn,nocturn = nocturn,crepus = crepus,shade_seek = shade_seek,burrow = burrow,pct_cond = pct_cond,nyears =ceiling(length(subs2)/365),metout = micro$metout[subs,],soil = micro$soil[subs,], soilpot = micro$soilpot[subs,],soilmoist = micro$soilmoist[subs,], humid = micro$humid[subs,],tcond = micro$tcond[subs,], shadmet = micro$shadmet[subs,],shadsoil = micro$shadsoil[subs,], shadpot = micro$shadpot[subs,],shadmoist = micro$shadmoist[subs,],shadhumid = micro$shadhumid[subs,],shadtcond = micro$shadtcond[subs,], rainfall = micro$RAINFALL[subs2],minshades =rep(unique(micro$minshade), length(subs)),maxshades =rep(unique(micro$maxshade), length(subs)),DEB =1, # turns DEB model onmetab_mode =1, # turns on hemimetabolous model, abpz = z * fract,del_M = del.M,p_Xm = p.Am *100/24/ kap.X,kap_X = kap.X,v = v /24,kap = kap,p_M = p.M /24,E_G = E.G,kap_R = kap.R,k_J = k.J /24,E_Hb = E.Hb * fract ^3,E_Hp = (E.Hj+1e-5) * fract ^3,E_Hj = E.Hj * fract ^3,h_a = h.a / (24^2),s_G = sG, # Gompertz stress coefficientE_0 = E.0* fract ^4,mu_E = mu.E,d_V = d.V,d_E = d.E,T_REF = T.ref,T_A = T.A,T_AL = T.AL,T_AH = T.AH,T_L = T.L,T_H = T.H,E_sm = E_sm,E_init = E_init,E_H_init = E_H_init,V_init = V_init,stage = stage,clutchsize = clutchsize,stages = stages,S_instar =rep(s.1, stages),L_b = L_b, # structural length at birth (cm),photostart = photostart,photofinish = photofinish,reset = reset,soilnode = soilnode)# Save the ectotherm model ectoA$dates <- dates[subs] # Add in dates for claritysave(ectoA, file =here("output", "microclim", paste0("grasshopperspA_", plant_mirco_names, "_Box1.RDa")))
Run the biophysical-physiological model again, but for species B which uses different parameters to capture the different life histories of the species. Note that species B occupies a slightly different microhabitat which is cooler than species A so we will assume they will be closet to leaf temperatures when stomata are open.
Code
# Using the same starting parameters as species A, so will not re-load the matrix again. # Additional parameters we need E_sm <-2000# J/cm^2, maximum stomach energy content fract <-1# scaling factor to change size# Inital state for DEB model. Note that DEB can be turned off and only heat and water budget models will be used so some parameters are not needed if DEB is off.# Egg Stage. Starting conditions E_init <- (E.0* fract ^4) / (3E-9* fract ^3) E_H_init <-0 V_init <-3E-9 stage <-0# Life history traits clutchsize <-8# Eggs per clutch stages <-7# Number of stages, 0 = egg, 1-5 = nymphs, 6 = pre egg-laying adult, 7 = post-laying adult S_instar <-rep(s.1, stages) # Stress at instar n: L_n^2/ L_n-1^2 (-)}\cr} L_b <- L.b # Structural length at birth (cm)}\cr} photostart <-0# no photoperiod effect on reproductionphotofinish <-0# no photoperiod effect on reproduction reset <-0# don't reset lifecycle if death occurs soilnode <-3# depth (soil node) at which eggs laid - 3 = 5 cm# Thermoregulatory parameters Ww_g <-0.2# g, body mass shape <-1# cylinder shape_b <-10# ratio of long to short axis of cylinder T_pref <-38# preferred body temperature species B T_F_min <-13# degrees C, lower body temperature for foraging T_F_max <-40# degrees C, upper body temperature for foraging T_B_min <-13# degrees C, minimum basking temperature T_RB_min <-13# degrees C, temperature at which animal will move to a basking site CT_max <-47# degrees C, critical thermal maximum species B CT_min <-0# degrees C, critical thermal minimum (used by program to determine depth selected when inactive and burrowing)# Behavioural traits diurn <-1# Diurnal activity allowed (1) or not (0)? nocturn <-1# Nocturnal activity allowed (1) or not (0)? crepus <-1# Crepuscular activity allowed (1) or not (0)? burrow <-0# Shelter in burrow allowed (1) or not (0)? shade_seek <-0# Shade seeking allowed (1) or not (0)? pct_cond <-0# Percentage of animal surface contacting the substrate (\%)
Code
plant_micro <-c("plant2_micro.RDa") plant_mirco_names <-c("stomata_open")# Use the plant microclimate data to reset the original microclimate object. Note that we need to adjust both shade and nonshade conditions# Unshaded microclimate micro$metout <- plant2$metout micro$metout [,"TALOC"] <- plant2$environ[,"TC"] micro$metout[,"TSKYC"] <- plant2$environ[,"TSKY"] micro$metout[,"RHLOC"] <- plant2$environ[,"RELHUM"]# Shaded microclimate microshadmet <- plant2$shadmet micro$shadmet [,"TALOC"] <- plant2$environ [,"TC"]# Soil microclimate micro$soil <- plant2$soil micro$shadsoil <- plant2$shadsoil micro$soilmoist <- plant2$soilmoist micro$shadmoist <- plant2$shadmoist# Get dates from microclimate object dates <- micro$dates # dates, hourlydates2 <- micro$dates2 # dates, daily# Set starting date by truncating microclimate model output to when grasshoppers are epected to oviposit eggs. This is the date when the life cylce of the insect is expected to start.oviposition_date <-'2018-03-31 23:00:00' subs <-which( dates >as.POSIXct(oviposition_date)) subs2 <-which(dates2 >as.POSIXct(oviposition_date))# Now run the DEB life-cyle model for the grasshopper ectoB <-ectotherm( Ww_g = Ww_g,shape = shape,shape_b = shape_b,T_pref = T_pref,T_F_min = T_F_min,T_F_max = T_F_max,T_B_min = T_B_min,T_RB_min = T_RB_min,CT_min = CT_min,CT_max = CT_max,diurn = diurn,nocturn = nocturn,crepus = crepus,shade_seek = shade_seek,burrow = burrow,pct_cond = pct_cond,nyears =ceiling(length(subs2)/365),metout = micro$metout[subs,],soil = micro$soil[subs,], soilpot = micro$soilpot[subs,],soilmoist = micro$soilmoist[subs,], humid = micro$humid[subs,],tcond = micro$tcond[subs,], shadmet = micro$shadmet[subs,],shadsoil = micro$shadsoil[subs,], shadpot = micro$shadpot[subs,],shadmoist = micro$shadmoist[subs,],shadhumid = micro$shadhumid[subs,],shadtcond = micro$shadtcond[subs,], rainfall = micro$RAINFALL[subs2],minshades =rep(unique(micro$minshade), length(subs)),maxshades =rep(unique(micro$maxshade), length(subs)),DEB =1, # turns DEB model onmetab_mode =1, # turns on hemimetabolous model, abpz = z * fract,del_M = del.M,p_Xm = p.Am *100/24/ kap.X,kap_X = kap.X,v = v /24,kap = kap,p_M = p.M /24,E_G = E.G,kap_R = kap.R,k_J = k.J /24,E_Hb = E.Hb * fract ^3,E_Hp = (E.Hj+1e-5) * fract ^3,E_Hj = E.Hj * fract ^3,h_a = h.a / (24^2),s_G = s.G,E_0 = E.0* fract ^4,mu_E = mu.E,d_V = d.V,d_E = d.E,T_REF = T.ref,T_A = T.A,T_AL = T.AL,T_AH = T.AH,T_L = T.L,T_H = T.H,E_sm = E_sm,E_init = E_init,E_H_init = E_H_init,V_init = V_init,stage = stage,clutchsize = clutchsize,stages = stages,S_instar =rep(s.1, stages),L_b = L.b,photostart = photostart,photofinish = photofinish,reset = reset,soilnode = soilnode)# Save the DEB model ectoB$dates <- dates[subs] # Add in dates for claritysave(ectoB, file =here("output", "microclim", paste0("grasshopperB_", plant_mirco_names, "_Box1.RDa")))
Now we can have a look at the body temperature of both species under both microclimate scenarios.
Code
## load the two grasshopper model outputsload(here("output", "microclim", "grasshopperspA_stomata_closed_Box1.RDa"))load(here("output", "microclim", "grasshopperB_stomata_open_Box1.RDa"))# Create dataframes for plotting for both species temp_spA_open <-data.frame(ectoA$environ) temp_spA_open$datetime <- ectoA$dates temp_spB_open <-data.frame(ectoB$environ)temp_spB_open$datetime <- ectoB$dates# Merge the two dataframes temp_combined <-rbind( temp_spA_open %>%mutate(Species ="Species A (Stomata Closed)"), temp_spB_open %>%mutate(Species ="Species B (Stomata Open)") )# Now subset to the desired dates temp_combined_sub <- temp_combined %>%filter(datetime >as.POSIXct('2019-01-15') & datetime <as.POSIXct('2019-01-18'))# Plot for the figure in Box 1. Note it was adjusted in Biorender.com for final version.ggplot(temp_combined_sub, aes(x = datetime, y = TA, color = Species)) +geom_line(size =0.8) +labs(x ="Date",y ="Body Temperature (°C)") +theme_classic() +theme( axis.text.x =element_text(angle =45, vjust =1, hjust =1),legend.position ="none", legend.text =element_text(size =14), axis.text =element_text(size =14), axis.title =element_blank()) +scale_colour_manual(values =c("Species A (Stomata Closed)"="#356920", "Species B (Stomata Open)"="#664229"))
Warning: Using `size` aesthetic for lines was deprecated in ggplot2 3.4.0.
ℹ Please use `linewidth` instead.
4Box 2 Code: Scaling up extreme heat effects from impacts on individuals of species to communities
Now that we have predictions about body temperatures we can incorporate thermal load accumulation and use the DEB models to predict life-cycles of the two grasshopper species. The output from DEB models can then be used to calculate vital rates such as growth, development, reproduction, and survival, which we can use to calculate population growth rates (\(r_{max}\)). We can then model population dynamics and interactions between the two species.
Note that in Box 1, we ran the biophysical model with DEB turned on, but we only extracted body temperature information. So, in essence, we already coupled heat and water-budget models with dynamic energy budget models for grasshoppers. Here, we will extract the DEB model and do a bit more with it.
4.1 Capturing impacts of extreme heat on physiological function across the life-cycle
First, we’ll need some functions to extract life-cycle details from the DEB model output and compute survival probabilities.
Code
#' @title alive_data: Subset DEB output to period when alive#' @description Extracts DEB model output corresponding only to periods when the individual#' is alive (post-hatch). This is done by subsetting the `debout` component#' of the ectotherm model output (`ecto`) to include only rows where both #' volume (`V`) and stage (`STAGE`) are greater than zero (indicating they are alive).#' @param ecto A list object (from NicheMapR's `ectotherm` function)#' containing components `debout` (developmental output) and `dates`.#' @return A data frame of post-hatch DEB output, including only periods when#' the organism is alive. Adds a `dates` column corresponding to those time points.#' @examples#' alive_data <- get_alive(ecto)#'get_alive <-function(ecto){ debout <-data.frame(ecto$debout)# Then, we can subset to just the post-hatch period debout_post_hatch <- debout[debout$V >0& debout$STAGE >0, ] debout_post_hatch$dates <- ecto$dates[which(debout$V >0& debout$STAGE >0)]return(debout_post_hatch)}#' @title get_environ: Extract environmental data from DEB model output#' @description Extracts and formats the environmental data (`environ`) from a DEB model#' object, appending the corresponding datetime information.#' @param ecto A list object (from NicheMapR's `ectotherm` function) containing components `environ` (heat budget calculations) and `dates`#' @return A data frame of environmental variables with an added `datetime` column.#' @examples#' env_data <- get_environ(ecto)#'get_environ <-function(ecto){ environ <-data.frame(ecto$environ) environ$datetime <- ecto$datesreturn(environ)}#' @title get_environ_alive: Limit environmental data to period when organism is alive#' @description Filters environmental data to match the period when the organism is alive#' (based on `dates` from the DEB output). Useful for synchronizing survival#' and environmental records.#' @param sub_alive A data frame of post-hatch DEB output (from [get_alive()]),#' containing a `dates` column for the alive period.#' @param sub_env A data frame of environmental data (from [get_environ()]),#' containing a `datetime` column.#' @return A data frame of environmental conditions corresponding only to the#' period when the organism is alive.#' @examples#' env_alive <- get_environ_alive(sub_alive = alive_data, sub_env = env_data)#'get_environ_alive <-function(sub_alive, sub_env){ sub_env_alive <-data.frame(sub_env[which(sub_env$datetime %in% sub_alive$dates), ])return(sub_env_alive)}#' @title update_p_surv: Update survival probabilities with additional mortality sources#' @description Adjusts survival probabilities in post-hatch DEB output to include additional#' sources of mortality such as hatch mortality, inactivity mortality, activity-based#' mortality, and thermal load stress mortality.#' @param debout_post_hatch A data frame of post-hatch DEB output (from [get_alive()]),#' containing a column `P_SURV` with baseline survival probabilities.#' @param m_h Numeric scalar; proportional mortality due to hatching (0–1).#' @param m_i Numeric scalar; proportional mortality due to inactivity (0–1).#' @param m_a Numeric scalar; proportional mortality due to activity (0–1).#' @param m_tls Numeric vector; proportional mortality due to thermal load stress (0–1)#' for each time step (must match `nrow(debout_post_hatch)`).#' @return A modified data frame with an additional column `P_SURV2` representing#' updated survival probabilities accounting for all mortality components.#' @examples#' debout_updated <- update_p_surv(debout_post_hatch, m_h = 0.1, m_i = 0.02, m_a = 0.03, m_tls = runif(nrow(debout_post_hatch), 0, 0.1))#'update_p_surv <-function(debout_post_hatch, m_h, m_i, m_a, m_tls){# Update survival at hatch debout_post_hatch$P_SURV2 <- debout_post_hatch$P_SURV * (1- m_h) # Update survival for additional mortality ratesfor(i in2:nrow(debout_post_hatch)){ debout_post_hatch$P_SURV2[i] <- debout_post_hatch$P_SURV2[i-1] * (1- m_a) * (1- m_tls[i]) * (1- m_i)# Given TLS mortality is incremented each time step we have one less row, so add row by adding on the last value of P_SURV2if(i ==nrow(debout_post_hatch)){ debout_post_hatch$P_SURV2[i] <- debout_post_hatch$P_SURV2[i-1] } } debout_post_hatch$P_SURV2 <-round(debout_post_hatch$P_SURV2, 2)return(debout_post_hatch)}#' @title get_lifecycle_vitals: Extract life-cycle vitals from DEB model output#' @description Derives key life-history parameters from DEB model output, including survival,#' fecundity, longevity, generation time, and the population growth rate (`rmax`).#' The function builds a stage-structured matrix and estimates the dominant#' eigenvalue (λ) of the matrix to calculate `rmax`.#' @param alive_debout A data frame of post-hatch DEB output (from [get_alive()]),#' with columns `STAGE`, `E_B`, and updated survival probabilities (`P_SURV2`).#' @param ecto A DEB model object containing at least a `yearout` component with#' a column `FECUNDITY` (representing reproductive output). This is summarised by stage.#' @return A list containing:#' \describe{#' \item{lambda}{Dominant eigenvalue of the life table (finite rate of increase).}#' \item{rmax}{Intrinsic rate of increase (log of λ).}#' \item{longevity}{Longevity in years (based on time steps in `alive_debout`).}#' \item{generation_time}{Age at first reproduction (years).}#' \item{stage_str}{Stage-structured matrix.}#' }#' @examples#' vitals <- get_lifecycle_vitals(alive_debout = alive_data, ecto = ecto)#' vitals$rmax#'get_lifecycle_vitals <-function(alive_debout, ecto, type = STAGE){# Fecundity data life_cycle_open <-data.frame(ecto$yearout)# Calculate longevity and generation time longevity <-nrow(alive_debout) # hours E_B_diff <- alive_debout$E_B[2:longevity] - alive_debout$E_B[1:(longevity -1)] age_first_repro <-which(E_B_diff <0)[1] generation_time <- age_first_repro /24/365# Create stage-specific survival and fecundity stage_str <- alive_debout %>%group_by({{type}}) %>%summarise(P_SURV =mean(P_SURV2),E_B =mean(E_B),fecundity =ifelse(E_B ==0, 0, life_cycle_open$FECUNDITY/life_cycle_open$CLUTCHES)) # Reproductive buffer energy, can see we only have a single reproductive event.# Build the Leslie matrix. First row fecundnity across stages, sub-diagonal survival probabilities build_life_table <-matrix(0, nrow =nrow(stage_str), ncol =nrow(stage_str)) build_life_table[1,] <- stage_str$fecundity# Now we need survival probabilities on the sub-diagonal. Because these are just hypothetical situations and only one life cyle realisation to be sure matrices are correct make sure that all survival is > 0 so that it allows for reproduction to occur at least once.for(i in2:nrow(stage_str)){ build_life_table[i, i -1] <- stage_str$P_SURV[i -1]if(build_life_table[i, i -1] ==0){ build_life_table[i, i -1] <-0.01 } } build_life_table[nrow(build_life_table), ncol(build_life_table)] <- stage_str$P_SURV[nrow(build_life_table)] if(build_life_table[nrow(build_life_table), ncol(build_life_table)] ==0){ build_life_table[nrow(build_life_table), ncol(build_life_table)] <-0.01 }#Estimate rmax and other demographic parameters from eigenvalue approach life_table_eigen <-Re(eigen(build_life_table)$values[1]) rmax_log <-log(life_table_eigen) lambda_dem <- popbio::lambda(build_life_table) # double check gen_time_dem <- popbio::generation.time(build_life_table) net_reproductive_rate_dem <- popbio::net.reproductive.rate(build_life_table)return(list(lambda =round(life_table_eigen, 2),rmax =round(rmax_log, 2),longevity = longevity /24/365, # daysnet_reproductive_rate_dem =round(net_reproductive_rate_dem, 2),gen_time_dem =round(gen_time_dem, 2),generation_time = generation_time,matrix = build_life_table))}#' @title damage_accum#' @description Calculates a cumulative percentage increase through time,#' where each interval’s increment depends on the temperature.#' @param time Numeric vector of time points (e.g., minutes).#' @param temp Numeric vector of temperatures (°C), same length as `time`.#' @param intercept Numeric scalar controlling the denominator (base-10 intercept).#' @param slope Numeric scalar controlling how temperature affects the rate.#' @param threshold Numeric scalar between 0 and 1 indicating the damage threshold.For example, if LT50 is used as the threshold, set `threshold = 0.5`. Used to convert heat injury to mortality probabilities#'#' @return A numeric vector of cumulative percentages of mortality (0–100).damage_accum <-function(time, temp, intercept, slope, threshold =0.5) { output =data.frame(damage_i =NA, cum_damage =NA, mortality_prob =NA)# Calculate the damage in a given incrementfor (i in1:(length(time) -1)) {# Calculate the accumulated damage increment for each time step acc = (100* (time[i +1] - time[i])) / (10^(slope *max(temp[i +1], temp[i]) + intercept)) output[i, "damage_i"] = acc }# Sum the increments cumulatively to get accumulated heat damage through time output$cum_damage[1] <- output$damage_i[1]for (j in2:nrow(output)) { output$cum_damage[j] <- output$cum_damage[j -1] + output$damage_i[j] }# Convert HI to survival probability at each time step given a threshold value used (50%, or 80% usually) output$mortality_prob = (output$cum_damage * threshold) /100# Now check that mortality cum damage and probabilities do not exceed 1 (100%) output$mortality_prob[output$mortality_prob >1] <-1 output$cum_damage[output$cum_damage >100] <-100 output$temp <- temp[1:(length(time) -1)] output$time_min <- time[1:(length(time) -1)]return(output)}# par(mfrow = c(3, 1))# plot(temp ~ time, data = damage, type = "l", xlim = c(109000, 120780))# plot(cum_damage_repair ~ time, data = damage, type = "l", xlim = c(109000, 120780))# plot(mortality_prob ~ time, data = damage, type = "l", xlim = c(109000, 120780))#' @title damage_accum#' @description Calculates cumulative heat damage through time, where each#' interval’s increment depends on temperature, and converts this to#' mortality probability via an exponential hazard model. The threshold#' is defined such that when cumulative damage reaches 100 * threshold,#' mortality is 50% (e.g., threshold = 0.5 for LT50).#'#' @param time Numeric vector of time points (e.g., minutes).#' @param temp Numeric vector of temperatures (°C), same length as `time`.#' @param intercept Numeric scalar controlling the denominator (base-10 intercept).#' @param slope Numeric scalar controlling how temperature affects the rate.#' @param threshold Numeric scalar between 0 and 1 indicating the damage#' threshold. For example, if LT50 is used as the threshold, set#' `threshold = 0.5`. When cum_damage reaches 100 * threshold, mortality = 0.5.#'#' @return A data.frame with columns:#' - damage_i: damage increment per interval (in arbitrary "% damage" units)#' - cum_damage: cumulative damage (same units as damage_i)#' - hazard: cumulative hazard H#' - mortality_prob: mortality probability (0–1)#' - temp: interval temperature (°C)#' - time_min: interval end time (minutes)damage_accum <-function(time, temp, intercept, slope, threshold =0.5) {if (length(time) !=length(temp)) {stop("'time' and 'temp' must have the same length.") }if (length(time) <2L) {stop("Need at least two time points to compute damage increments.") }# interval lengths dt <-diff(time)# use max temp over each interval temp_step <-pmax(temp[-1], temp[-length(temp)])# damage increment per interval (arbitrary "% damage" units) damage_i <- (100* dt) / (10^(slope * temp_step + intercept))# cumulative damage (same units as damage_i) cum_damage <-cumsum(damage_i)# map to hazard so that cum_damage = 100 * threshold -> mortality = 0.5 D50 <-100* threshold # "damage" at LT50 haz <-log(2) * (cum_damage / D50) # cumulative hazard surv <-exp(-haz) mort <-1- surv# numerical safety mort[mort <0] <-0 mort[mort >1] <-1 out <-data.frame(damage_i = damage_i,cum_damage = cum_damage,hazard = haz,mortality_prob = mort,temp = temp_step,time_min = time[-1] )return(out)}#' @title damage_repair_accum#' @description Calculates cumulative heat damage through time with#' temperature-dependent repair (Sharpe–Schoolfield form), then converts#' this to mortality probability via an exponential hazard model.#' The threshold is defined such that when cumulative damage reaches#' 100 * threshold, mortality is 50% (e.g., threshold = 0.5 for LT50).#'#' @param time Numeric vector of time points (e.g., minutes).#' @param temp Numeric vector of temperatures (°C), same length as `time`.#' @param intercept Numeric scalar controlling the denominator (base-10 intercept)#' for damage accumulation.#' @param slope Numeric scalar controlling how temperature affects the rate#' of damage accumulation.#' @param threshold Numeric scalar between 0 and 1 indicating the damage#' threshold (e.g., 0.5 for LT50). When cum_damage_repair reaches#' 100 * threshold, mortality = 0.5.#' @param TA Arrhenius activation parameter for repair (Kelvin).#' @param TAL Low-temperature inactivation parameter for repair (Kelvin).#' @param TAH High-temperature inactivation parameter for repair (Kelvin).#' @param TL Low-temperature inactivation midpoint (Kelvin).#' @param TH High-temperature inactivation midpoint (Kelvin).#' @param TREF Reference temperature for repair rate (Kelvin).#' @param kdot_ref Reference repair rate at `TREF` (per minute).#'#' @return A data.frame with columns:#' - damage_i: damage increment per interval (no repair)#' - repair_inc: repair amount per interval#' - net_damage_inc: net damage increment after repair#' - cum_damage_repair: cumulative net damage through time#' - hazard: cumulative hazard H#' - mortality_prob: mortality probability (0–1)#' - temp: interval temperature (°C)#' - time_min: interval end time (minutes)damage_repair_accum <-function(time, temp, intercept, slope,threshold =0.5, TA, TAL, TAH, TL, TH, TREF, kdot_ref) {if (!is.numeric(temp)) stop("'temp' must be numeric (°C).")if (length(time) !=length(temp)) {stop("'time' and 'temp' must have the same length.") }if (length(time) <2L) {stop("Need at least two time points to compute damage increments.") }# check scalar parametersfor (nm inc("TA", "TAL", "TAH", "TL", "TH", "TREF", "kdot_ref")) { val <-get(nm, inherits =FALSE)if (!is.numeric(val) ||length(val) !=1L) {stop(sprintf("'%s' must be a numeric scalar.", nm)) } }# Kelvin temps for repair TK <- temp +273.15# Step 1: baseline damage increments (without repair) damage <-damage_accum(time = time,temp = temp,intercept = intercept,slope = slope,threshold = threshold ) n <-nrow(damage)# prepare columns for repair; overwrite hazard/mortality later damage$repair_inc <-NA_real_ damage$net_damage_inc <-NA_real_ damage$cum_damage_repair <-NA_real_# interval lengths and Kelvin temps per interval dt <-diff(time) # minutes TK_step <-pmax(TK[-1], TK[-length(TK)]) # K, max over each interval# Step 2: compute repair and net damage per intervalfor (i inseq_len(n)) { T_K <- TK_step[i] dt_i <- dt[i]# Sharpe–Schoolfield repair rate (per minute) num <- kdot_ref *exp(TA * (1/ TREF -1/ T_K)) den <-1+exp(TAL * (1/ T_K -1/ TL)) +exp(TAH * (1/ TH -1/ T_K)) repair_rate <- num / den# amount repaired during this interval (same units as damage_i) repair_inc <- repair_rate * dt_i# net damage increment after repair, truncated at zero damage_inc <- damage$damage_i[i] net_inc <-max(damage_inc - repair_inc, 0) damage$repair_inc[i] <- repair_inc damage$net_damage_inc[i] <- net_inc# cumulative net damageif (i ==1L) { damage$cum_damage_repair[i] <- net_inc } else { damage$cum_damage_repair[i] <- damage$cum_damage_repair[i -1L] + net_inc } }# Step 3: map cumulative net damage to hazard and mortality D50 <-100* threshold haz <-log(2) * (damage$cum_damage_repair / D50) surv <-exp(-haz) mort <-1- surv mort[mort <0] <-0 mort[mort >1] <-1 damage$hazard <- haz damage$mortality_prob <- mort# ensure temp/time columns correspond to intervals damage$temp <-pmax(temp[-1], temp[-length(temp)]) damage$time_min <- time[-1]return(damage)}#' @title damage_repair_accum_state#' @description Calculates cumulative heat damage through time with#' temperature-dependent repair (Sharpe–Schoolfield form), tracking#' the damage state over time and computing instantaneous hazard and#' mortality probability at each time step.#' The threshold is defined such that when cumulative damage reaches#' 100 * threshold, mortality is 50% (e.g., threshold = 0.5 for LT50).#' @param time Numeric vector of time points (e.g., minutes).#' @param temp Numeric vector of temperatures (°C), same length as `time`.#' @param intercept Numeric scalar controlling the denominator (base-10 intercept)#' for damage accumulation.#' @param slope Numeric scalar controlling how temperature affects the rate#' of damage accumulation.#' @param threshold Numeric scalar between 0 and 1 indicating the damage#' threshold (e.g., 0.5 for LT50). When cum_damage_repair reaches#' 100 * threshold, mortality = 0.5.#' @param TA Arrhenius activation parameter for repair (Kelvin).#' @param TAL Low-temperature inactivation parameter for repair (Kelvin).#' @param TAH High-temperature inactivation parameter for repair (Kelvin).#' @param TL Low-temperature inactivation midpoint (Kelvin). #' @param TH High-temperature inactivation midpoint (Kelvin).#' @param TREF Reference temperature for repair rate (Kelvin).#' @param kdot_ref Reference repair rate at `TREF` (per minute).#' @return A data.frame with columns:#' - damage_i: damage increment per interval (no repair)#' - repair_inc: repair amount per interval #' - net_damage_inc: net damage increment after repair#' - cum_damage_repair: cumulative net damage through time#' - hazard: instantaneous hazard h from current damage state#' - mortality_prob: mortality probability (0–1)#' - temp: interval temperature (°C)#' - time_min: interval end time (minutes)#' damage_repair_accum_state <-function(time, temp, intercept, slope,threshold =0.5, TA, TAL, TAH, TL, TH, TREF, kdot_ref) {if (!is.numeric(temp)) stop("'temp' must be numeric (°C).")if (length(time) !=length(temp)) {stop("'time' and 'temp' must have the same length.") }if (length(time) <2L) {stop("Need at least two time points to compute damage increments.") }# Kelvin temps TK <- temp +273.15# interval lengths and step temperatures (max over interval) dt <-diff(time) # minutes temp_st <-pmax(temp[-1], temp[-length(temp)]) TK_st <-pmax(TK[-1], TK[-length(TK)]) n <-length(dt)# damage rate per interval (without repair), "% damage per minute"# from your original damage_accum:# damage_i = (100 * dt) / 10^(slope * T + intercept)# so: damage_rate <-100/ (10^(slope * temp_st + intercept))# Sharpe–Schoolfield repair rate per interval (per minute) repair_rate <-vapply(seq_len(n), function(i) { T_K <- TK_st[i] num <- kdot_ref *exp(TA * (1/ TREF -1/ T_K)) den <-1+exp(TAL * (1/ T_K -1/ TL)) +exp(TAH * (1/ TH -1/ T_K)) num / den }, numeric(1))# Raw increments for bookkeeping damage_i <- damage_rate * dt repair_inc <- repair_rate * dt# State variables net_damage_inc <-numeric(n) # can be negative cum_damage_state <-numeric(n) # damage STATE with repair hazard_inst <-numeric(n) # instantaneous hazard from state mortality_prob <-numeric(n) D50 <-100* threshold D_prev <-0for (i inseq_len(n)) {# net increment in damage state this interval net_inc <- (damage_rate[i] - repair_rate[i]) * dt[i] # can be < 0 D_new <-max(D_prev + net_inc, 0) # repair can undo damage# instantaneous hazard given CURRENT damage state h_i <-log(2) * (D_new / D50)# interpret this as "mortality if tested now" via your mapping mort_i <-1-exp(-h_i) net_damage_inc[i] <- net_inc cum_damage_state[i] <- D_new hazard_inst[i] <- h_i mortality_prob[i] <- mort_i D_prev <- D_new } out <-data.frame(damage_i = damage_i,repair_inc = repair_inc,net_damage_inc = net_damage_inc,cum_damage_repair = cum_damage_state, # current damage statehazard = hazard_inst, # from state, not cumulativemortality_prob = mortality_prob, # can go up or downtemp = temp_st,time_min = time[-1] )return(out)}
Code
# Load each grasshopper DEB model and extract life-cycle detailsload(here("output", "microclim", "grasshopperspA_stomata_closed_Box1.RDa")) #ectoAload(here("output", "microclim", "grasshopperB_stomata_open_Box1.RDa")) #ectoB# Define Common Parameters Applied to Both Species# Baseline mortality rates to add to DEB. TLS and differences in hatch mortality will be added to these baseline rates differently for each species m_i <-0# mortality per hour inactive m_a <-1e-4# mortality per hour active## ------------------## # Species A## ------------------## # First, find out dates and times when animal is alive. We need these to extract the body temperatures AND the relevant aspects of the lifecyle# Get the DEB output while alive debout_post_hatch_A <-get_alive(ectoA)# Get the environ data while alive, which we use for TLS as we need the body temperature every hour environ_A <-get_environ(ectoA) environ_A_alive <-get_environ_alive(debout_post_hatch_A, environ_A) environ_A_alive <- environ_A_alive %>%mutate(time_min =c(0:(n()-1))*60) # time in minutes# Thermal Load sensitivity parameters for species A. Note that CTmax and z are values derived from a regression between temperture (yaxis) and log time to 50% mortality (xaxis). See Arnold et al. 2025 for more details. We need to invert these values to get the intercept and slope for the damage accumulation function. ctmaxA <-50# CTmax1h, 50 zA <--1.8# z (slope), negative indicates decrease in survival with time which it must do as animals don't get better the longer they are at stressful temps - try -0.9alphaA <--ctmaxA/zA # Need these for the log time accumulation function betaA <-1/ zA # Need these for the log time accumulation function# Define temperatures and times tbA <- environ_A_alive$TC-3# body temps, Celsius timeA <- environ_A_alive$time_min # time in minutes# Using body temperatures through time, estimate the accumulation of physiological damage towards a threshold of 50% mortality under no repair. Note that threshold (50% = 0.5) is used to scale % HI to % mortalitydamageA <-damage_accum(time = timeA, temp = tbA, intercept = alphaA, slope = betaA)# Set Arrhenius model and repair rate parameters TA <-14065# Arrhenius temperature, KTL <-10.5+273.15# Arrhenius temperature lower threshold, KTH <-28.5+273.15# Arrhenius temperature upper threshold, KTAL <-50000# Arrhenius temperature lower, KTAH <-100000# Arrhenius temperature upper, KTREF <-28+273.15# reference temperature, Kkdot_ref <-0.005# repair rate % per minute at ref temp 28 deg CdamageA_repair <-damage_repair_accum_state(time = timeA, temp = tbA, intercept = alphaA, slope = betaA,TA = TA, TAL = TAL, TAH = TAH,TL = TL, TH = TH, TREF = TREF, kdot_ref = kdot_ref)#plot(damageA_repair$time_min, damageA_repair$mortality_prob, type = "l")# Now that we have the probability of mortality from thermal load stress, we can update the survival probabilities in the DEB model output. Note that the DEB model already has some baseline mortality rates from the Gompertz ageing model, so we are adding additional sources of mortality.m_h <-0.05# mortality at hatchdebout_post_hatch_A <-update_p_surv(debout_post_hatch_A, m_h = m_h, # mortality at hatchm_a = m_a, # mortality for active hourm_i = m_i, # mortality for inactive hourm_tls = damageA_repair$mortality_prob) # Heat load mortality# Now we can extract life-cycle details. Our life cycle model assumes all animals progress to the next stage that survive. Given we have multiple life stages we create a stage-structured matrix (or could be an age-structuted model – for example, based on 'day') to capture survival and fecundity at each stage, and quantify the probability of transitioning through each stage. From this, we can estimate population growth rate (rmax) and other life-cycle details. This could be changed to an age structure here by setting type = DAY and should be more comparable across species with different development times.lifecycle_A <-get_lifecycle_vitals(debout_post_hatch_A, ectoA, type = STAGE) ## ------------------## # Species B## ------------------## debout_post_hatch_B <-get_alive(ectoB) environ_B <-get_environ(ectoB) environ_B_alive <-get_environ_alive(debout_post_hatch_B, environ_B) %>%mutate(time_min =c(0:(n()-1))*60) # time in minutes# Thermal Load sensitivity parameters for species A. Note that CTmax and z are values derived from a regression between temperture (yaxis) and log time to 50% mortality (xaxis). See Arnold et al. 2025 for more details. We need to invert these values to get the intercept and slope for the damage accumulation function. ctmaxB <-47# CTmax1h zB <--1# z (slope), negative indicates decrease in survival with timealphaB <--ctmaxB/zB # Need these for the log time accumulation function betaB <-1/ zB # Need these for the log time accumulation function# Define temperatures and times tbB <- environ_B_alive$TC-3# body temps, Celsius timeB <- environ_B_alive$time_min # time in minutes# Using body temperatures through time, estimate the accumulation of physiological damage towards a threshold of 50% mortality under no repairdamageB <-damage_accum(time = timeB, temp = tbB, intercept = alphaB, slope = betaB)damageB_repair <-damage_repair_accum_state(time = timeB, temp = tbB, intercept = alphaB, slope = betaB,TA = TA, TAL = TAL, TAH = TAH,TL = TL, TH = TH, TREF = TREF, kdot_ref = kdot_ref)# plot(damageB_repair$time_min, damageB_repair$mortality_prob, type = "l")# Now that we have the probability of mortality from thermal load stress, we can update the survival probabilities in the DEB model output m_h <-0.05# mortality at hatchdebout_post_hatch_B <-update_p_surv(debout_post_hatch_B, m_h = m_h, # mortality at hatchm_a = m_a, # mortality for active hourm_i = m_i, # mortality for inactive hourm_tls = damageB_repair$mortality_prob) # Heat load mortality# Now we can extract life-cycle detailslifecycle_B <-get_lifecycle_vitals(debout_post_hatch_B, ectoB)### Create a TLS figure for box 2# Species Amultiplier <-50# to scale the two y axesp1_tlA <-ggplot() +annotate("rect",xmin =1800*60, xmax =2500*60,ymin =-Inf, ymax =Inf,fill ="red", alpha =0.2) +labs(x ="Time (hours)", y ="Body Temperature (°C)") +geom_line(aes(x = timeA, y = tbA), color ="#356920", size =0.5) +ylim(0, 50) +geom_line(aes(x = timeA, y =c(0, damageA_repair$mortality_prob) * multiplier), color ="red") +scale_y_continuous(name ="Probability of Mortality (Thermal Load Stress)", sec.axis =sec_axis(~ . / multiplier, name ="Body temperature (°C)")) +theme_classic() +theme( axis.text.x =element_blank(), legend.position ="none", legend.text =element_text(size =14), axis.text =element_text(size =14), axis.title =element_blank()) # Species Bp2_tlB <-ggplot() +annotate("rect",xmin =1800*60, xmax =2500*60,ymin =-Inf, ymax =Inf,fill ="red", alpha =0.2) +labs(x ="Time (hours)", y ="Body Temperature (°C)") +geom_line(aes(x = timeB, y = tbB), color ="#664229", size =0.5) +ylim(0, 50) +geom_line(aes(x = timeB, y =c(0, damageB_repair$mortality_prob) * multiplier), color ="red") +scale_y_continuous(name ="Probability of Mortality (Thermal Load Stress)", sec.axis =sec_axis(~ . / multiplier, name ="Body temperature (°C)")) +theme_classic() +theme( axis.text.x =element_blank(), legend.position ="none", legend.text =element_text(size =14), axis.text =element_text(size =14), axis.title =element_blank()) # Combine plotsggsave(p1_tlA | p2_tlB +plot_layout(ncol =1), width =11.876543, height =4.839506, dpi =600, filename =here("figs", "fig_grasshopper_TLS_box2.png"))# Note this is a trimed down version for the plot for the figure in main MS. It was edited in BioRender to add axis labels and panel labels.p1_tlA | p2_tlB +plot_layout(ncol =1)
4.2 Modelling population dynamics of the two grasshopper species including density dependence and interactions
We now have key vital rate information for each species that incorperates physiologically explicit processes governing growth, survival and reproduction. We can now use this information to model population dynamics of the two species including density dependence and interspecific interactions.
One way (of many) we could model the population dynamics of the two species is to use a discrete-time Ricker model that incorporates both intra and interspecific competition. The general form of the model is as follows:
where \(N_t^i\) is the population size of species \(i\) at time \(t\), \(\lambda_i\) is the intrinsic growth rate of species \(i\), \(K_i\) is the carrying capacity of species \(i\), and \(\alpha_{ij}\) is the competition coefficient that describes how much species \(j\) affects species \(i\). When \(\alpha_{ij}\) = 1, species \(j\) competes just as strongly as species \(i\) does with itself. When \(\alpha_{ij}\) < 1, then species \(j\) has a weaker effect on species \(i\) then conspecifics and \(\alpha_{ij}\) > 1, indicates that species \(j\) has a stronger effect on species \(i\) than conspeecifics. The sum runs over all species \(j\) that are not equal to \(i\), which means that the model can be extended to include any number of species within a community.
As with any model there are assumptions that need to be considered. In this case:
it again assumes discrete non-overlapping generations.
it assumes \(\lambda\), \(K\) and now \(\alpha_{ij}\) are constant over time, ignoring any effects of environmental stochasticity, changing interaction strengths between species or evolution. Again, these assumptions can be relaxed. For example, \(\alpha_{ij}\) can of course vary depending on the environment and could be viewed as a function in of itself. As such, we could, for example, couple our mircoclimate and biophysical model predictions to estimate how \(\alpha_{ij}\) varies as a function of temperature, water or even resource availability based on experimental data describing how \(\alpha_{ij}\) varies as across different environments.
it assumes that population growth is limited by the total effective density based on both intra and interspecific effects. It also assumes these act additively on the log scale.
it assumes a closed system with no emmigration or immigration.
For now, we will assume that the population is closed and that \(K\) is constant over time. This model allows for asymmetric interaction (i.e., \(\alpha_{ij} \neq \alpha_{ji}\)), and so, we’ll give each species its own competition coefficient. We will also assume that \(\lambda\) varies slightly over time and add this to our model to demonstrate how you can relax assumptions of \(\lambda\), \(K\) and \(\alpha_{ij}\) being constant over time.
We’ll need some functions to run the population dynamics model.
#' @title Ricker two species function#' @description A simple Ricker model function to simulate population dynamics for multiple species#' @param N0 Initial population size of species i#' @param r_i Intrinsic growth rate for species i#' @param sd_r_i Standard deviation of the growth rate for species i#' @param K_i Carrying capacity for species i#' @param alpha Competition coefficient between species #' @param N0_j Initial population size of species j#' @return Next years population sizericker_two <-function(N0_i, r_i, sd_r_i, K_i, N0_j, alpha){ N0_i *exp(rnorm(1, r_i, sd_r_i)*(1- ((N0_i + (alpha * N0_j))/K_i)))}## Create a function to do a stochastic simulation of the two species Ricker model#' @title Two species ricker model simulation function#' @description A simple two species Ricker model function to simulate population dynamics for two species#' @param N0_i Initial population size of species i#' @param r_i Intrinsic growth rate for species i#' @param sd_r_i Standard deviation of the growth rate for species i#' @param K_i Carrying capacity for species i#' @param N0_j Initial population size of species j#' @param r_j Intrinsic growth rate for species j#' @param sd_r_j Standard deviation of the growth rate for species j#' @param K_j Carrying capacity for species j#' @param alpha_ij Competition coefficient for species j on species i#' @param alpha_ji Competition coefficient for species i on species j#' @param time Time step#' @param nsims Number of replicates#' @return A matrix of population sizes at each time step for nsimssim_ricker_two_sp <-function(N0_i, r_i, sd_r_i, K_i, N0_j, r_j, sd_r_j, K_j, alpha_ij, alpha_ji, time, nsims){# Create a vector to store the population size at each time step N_array_i <-array(NA, dim =c(time, nsims)) N_array_j <-array(NA, dim =c(time, nsims))for(j in1:nsims){# Create arrays to store population sizes and initilise with initial population sizes N_array_i[1,j] <- N0_i N_array_j[1,j] <- N0_jfor(i in2:time){# Calculate the population size in the next time step for species i N_array_i[i,j] <-ricker_two(N_array_i[i-1, j], r_i, sd_r_i, K_i, N_array_j[i-1, j], alpha_ji)# Calculate the population size in the next time step for species j N_array_j[i,j] <-ricker_two(N_array_j[i-1, j], r_j, sd_r_j, K_j, N_array_i[i-1, j], alpha_ij) } }# Return the population size at each time step for species i and jreturn(list(N_i = N_array_i, N_j = N_array_j))}
Code
set.seed(123) # Set seed for reproducibility# Set up the parameters for the Ricker model N0_A <-1000# Initial population size, species A N0_B <-500# Initial population size, species B sd_r_A <-0.15# Standard deviation of the growth rate species A sd_r_B <-0.15# Standard deviation of the growth rate species B K_A <-10000# Carrying capacity species A K_B <-10000# Carrying capacity species Br_A <- lifecycle_A$rmax # Intrinsic growth rate species A and B open watering scenerio r_B <- lifecycle_B$rmax # Intrinsic growth rate species A and B closed watering scenerio alpha_AB <-0.8# Competition coefficient. Assumed not to vary with environment alpha_BA <-1.2# Competition coefficient. Assumed not to vary with environment time <-5# Time steps nsims <-1000# Number of simulations# Lets run the simulationsN_heat <-sim_ricker_two_sp( N0_i = N0_A, r_i = r_A, sd_r_i = sd_r_A, K_i = K_A, N0_j = N0_B, r_j = r_B, sd_r_j = sd_r_B, K_j = K_B, alpha_ij = alpha_AB, alpha_ji = alpha_BA, time, nsims)# Create a data frame to store the results for each species for each sceneriosims_pop <- plyr::ldply(lapply(N_heat, function(x){ pivot_longer(as.data.frame(x), cols =everything(), names_to ="sim", values_to ="N") %>%mutate(time =rep(1:time, each = nsims))}))
Code to plot the results and export part of the box figure below:
Code
# Create a summary of the simulations to make plotting easier sims_heat <- sims_pop %>%group_by(.id, time) %>%summarise(N_m =mean(N), sd =sd(N)) # Plot the resultsp1 =ggplot(sims_heat, aes(x = time, y = N_m, colour = .id, fill = .id)) +geom_ribbon(aes(ymin = N_m - sd, ymax = N_m + sd), alpha =0.3, colour =NA) +geom_line() +labs(x ="Generation (time)", y ="Population size (N)") +theme_classic() +scale_colour_manual(values =c("#356920", "#664229")) +scale_fill_manual(values =c("#356920", "#664229")) +theme(legend.position ="none", axis.text =element_text(size =14), axis.title =element_blank())# Save the plot# ggsave(here("figs", "fig-ricker_twosp_box2.png"), p1, width = 6, height = 5, dpi = 600)
More complex models that incorporate a multitude of species and their interactions can be built using the same principles [e.g., 6,7,8]. Information on interaction strength between species can be used to understand how species coexistence is expected to change under extreme heat scenarios.
5Code for “Hot Solutions for a changing world”: Using a systems modelling approach to help develop solutions to extreme heat events
A systems modelling approach can also be used to design possible solutions by understanding better how ‘thermal interventions’ affect heat experienced by organisms and how it influences growth, survival and reproduction. For example, we can manipulate rainfall at a particular time to simulate artificial watering which might be a possible intervention used during extreme heat. The added water can be used to make calculations of heat exchange, temperatures and transpiration under extreme heat events. We can also optimise the amount of water to balance costs and benefits of watering.
5.0.1Simulating the effect of watering on plant and insect body temperatures
5.0.1.1 Setting the scene
Let’s assume our plant species is of interest for conservation or agricultural reasons and we want to understand how watering during a heatwave might help it survive (Fig. 2). The challenge with watering is that often we are drought stressed in combination with the extreme heat [9]. As such, it’s ideal to reduce water use because this saves money and water. So, the questions become:
How much water do we need to protect the plant from extreme heat while also not overwatering?
In addition, how does this affect pest insects that might be residing on the plant?
Figure 2— Evaluating the impacts of ‘thermal interventions’ on temperatures experienced by plants and animals. Under extreme heat we might add water to the system to help keep plants cool, but such intervention may also have unexpected consequences on insects. Using a systems approach we can evaluate the impact of watering on plant and insect body temperatures. This can help us understand how to best manage the system to reduce the impacts of extreme heat on plants and insects.
Below, we load in our original microclimate object. We can now use this to simulate the effect of watering on the microclimate. First, lets just look at the rainfall that occurred over the period of interest (Fig. 3). The rainfall data is stored in the microclimate object and is provided as a daily value (rather than hourly as for temperature).
Code
# Lets reload our microclimate object so we can use it againload(here("output", "microclim", "microclimate.RDa"))# Get the dates from the microclimate object. We'll take it here because heat-budget objects are not saved with the dates dates <- micro$dates# Lets plot out the actual rainfall data for the location. This is the rainfall data that was used in the microclimate model. rainfall_actual <-data.frame(micro$RAINFALL) rainfall_actual$dates <-as.POSIXct(micro$dates2)# Plot the rainfall data each day through time p1 <-ggplot(rainfall_actual) +geom_line(aes(y = micro.RAINFALL, x = dates), col ="blue") +geom_hline(yintercept =10, colour ="red", lty =2) +labs(y ="Rainfall (mm)", x ="Date") +theme_classic() +theme( axis.text.x =element_text(angle =45, vjust =1, hjust =1)) +scale_colour_viridis_d()# Now a plot of ran over the heat wave even just to zoom in p2 <- rainfall_actual %>%filter(dates >"2019-01-15"& dates <"2019-01-31") %>%ggplot() +geom_line(aes(y = micro.RAINFALL, x = dates), col ="blue") +geom_hline(yintercept =10, colour ="red", lty =2) +labs(y ="Rainfall (mm)", x ="Date") +theme_classic() +theme( axis.text.x =element_text(angle =45, vjust =1, hjust =1)) +scale_colour_viridis_d()# Combine the two plotsp1 + p2 +plot_layout(ncol =1) +plot_annotation(tag_levels ="A") +theme(plot.tag =element_text(size =12, face ="bold"))
Figure 3— Rainfall events over time for A) over years and B) only during the heat wave event from 2019-01-15 to 2019-01-31.
It’s clear that we had very little water during the heatwave event. If this was an agriculturally important plant species, we might than as the question “how would the temperature of the plants change if we added water to the system?”. Remember, stomatal closure during the heat wave event meant that the leaf temperatures were much higher than the air temperature. Spraying water will cool the plants through evaportive cooling, but by how much can we expect it to do that? Well, we can use the microclimate model to simulate this. Lets set up our scenerios.
Code
# Lets assume we are planning on adding 10mm or 20 mm of water to the system to see how much it makes a difference on plant leaf temperaures. rainfall_2 <-2# mm water to add rainfall_5 <-5# mm water to add rainfall_10 <-10# mm water to add rainfall_20 <-20# mm water to add# Specify the dates we are going to add water. date_waters <-as.character(seq(as.POSIXct("2019-01-01"), as.POSIXct("2019-01-31"), by ="days"))# Lets create a new vector of rainfall and then add it to the microclimate object. This is done by creating a new vector of rainfall and then adding it to the microclimate object. RAINFALL <- micro$RAINFALL # extract rainfall from previous sim RAINFALL2 <- RAINFALL # copy rainfall into a new vector RAINFALL2[as.character(micro$dates2) %in% date_waters] <- rainfall_2 # add 2mm of water RAINFALL3 <- RAINFALL # copy rainfall into a new vector RAINFALL3[as.character(micro$dates2) %in% date_waters] <- rainfall_5 # add 5mm of water RAINFALL4 <- RAINFALL # copy rainfall into a new vector RAINFALL4[as.character(micro$dates2) %in% date_waters] <- rainfall_10 # add 5mm of water RAINFALL5 <- RAINFALL # copy rainfall into a new vector RAINFALL5[as.character(micro$dates2) %in% date_waters] <- rainfall_20 # add 5mm of water
Now we have created two new rainfall vectors simulating different amounts of water added to the system during the heatwave event. Lets have a look at the new rainfall data.
Code
# Add in rainfall manipulations to columnsrainfall_actual$rainfall_2 <- RAINFALL2rainfall_actual$rainfall_5 <- RAINFALL3rainfall_actual$rainfall_10 <- RAINFALL4rainfall_actual$rainfall_20 <- RAINFALL5# Re-arrange data for plottingrainfall_actual2 <- rainfall_actual %>%select(dates, micro.RAINFALL, rainfall_2, rainfall_5, rainfall_10, rainfall_20) %>%pivot_longer(cols =c(micro.RAINFALL, rainfall_2, rainfall_5, rainfall_10, rainfall_20), names_to ="Rainfall", values_to ="Rainfall_mm") %>%mutate(Rainfall =ifelse(Rainfall =="micro.RAINFALL", "Original Rainfall", ifelse(Rainfall =="rainfall_10", "Watering (10 mm)", ifelse(Rainfall =="rainfall_20", "Watering (20 mm)", ifelse(Rainfall =="rainfall_2", "Watering (2 mm)", ifelse(Rainfall =="rainfall_5", "Watering (5 mm)", NA))))),Rainfall =factor(Rainfall, levels =c("Watering (2 mm)", "Watering (5 mm)", "Watering (10 mm)", "Watering (20 mm)", "Original Rainfall")))# Plot the rainfall data each day through time water <-ggplot(rainfall_actual2) +geom_vline(xintercept =as.POSIXct("2019-01-01"), linetype ="dashed", color ="red") +geom_vline(xintercept =as.POSIXct("2019-01-31"), linetype ="dashed", color ="red") +geom_line(aes(y = Rainfall_mm, x = dates, col = Rainfall), show.legend =TRUE) +labs(y ="Rainfall (mm)", x ="Date") +theme_classic() +theme( legend.position ="inside",legend.position.inside =c(0.25,0.8),axis.title =element_text(size =20),axis.text.x =element_blank(),legend.title =element_text(size =20),legend.text =element_text(size =20)) +scale_colour_viridis_d() water
Figure 4— Rainfall events over time with two watering scenarios (10 mm and 20mm of water applied) during a heat wave event 2019-01-15 to 2019-01-31 (red dashed lines).
5.0.1.2 Calculating the different mircoclimates using the watering scenerios
We can now use the micro_silo function to run the microclimate model with the new rainfall data. We simply need to update the rainfall data in the rainhour argument. The rainhourly argument is set to 1 to indicate that we want to use hourly rainfall data.
Code
# Create hourly values, but water at midnight for one hour (00:00-01:00) (midnight is used to avoid changes too rapid for the integrator to solve). rainhour_2 <-unlist(lapply(RAINFALL2, function(x) c(rep(0,23), x))) rainhour_5 <-unlist(lapply(RAINFALL3, function(x) c(rep(0,23), x)))rainhour_10 <-unlist(lapply(RAINFALL4, function(x) c(rep(0,23), x)))rainhour_20 <-unlist(lapply(RAINFALL5, function(x) c(rep(0,23), x)))rain_list <-list(rainhour_2, rainhour_5, rainhour_10, rainhour_20)names(rain_list) <-c("rainhour_2", "rainhour_5", "rainhour_10", "rainhour_20")# Now lets run the microclimate model with the new rainfall data. We will do this for both 10mm and 20mm of water added to the system.#Set up the location and parameters for the microclimate model again. longlat =c(140.79970255974192, -34.159176277782564) dstart ="01/01/2018"# Start year dfinish ="31/12/2019"# End year minshade =40# Minimum shade, % maxshade =80# Maximum shade, % Usrhyt =0.2# Height above ground, m microclima =1# Use mircoclima PC =-1500# Water potential for stomatal closure SP =10for(i in1:length(rain_list)){# run the microclimate model for different watering regimes and save the output. rain <-micro_silo(loc = longlat, dstart = dstart,dfinish = dfinish, Usrhyt = Usrhyt, microclima = microclima,minshade = minshade,maxshade = maxshade, PC = PC, SP = SP, rainhourly =1, rainhour = rain_list[[i]],email ="daniel.noble@edu.au")# Save the microclimate objectsave(rain, file =here("output", "microclim", paste0(names(rain_list)[i], "_shade.RDa")))}
Now we have the new microclimate objects. The original, 2, 5, 10 and 20mm watering events. We have access to temperatures in the air and soil as well as high shade (80%) and low shade (40%). Let’s how feed this into our plant heat-budget model to predict the leaf temperature of the plant.
5.0.1.3 Calculating the plant leaf temperature using the watering scenerios
Code
# Now, run the plant heat-budget model using each of the three different microclimate objects. objects <-c("microclimate.RDa", "rainhour_2_shade.RDa", "rainhour_5_shade.RDa", "rainhour_10_shade.RDa", "rainhour_20_shade.RDa") names <-c("micro", rep("rain", 4)) # Name of the microclimate objects on load. Original mirciclimate object named "micro" and the rest "rain" because the object saved at each run of loop is "rain"# Set leaf functional traits live <-0# don't simulate behaviour or respiration leaf <-1# use the stomatal conductance formulation for evaporation Ww_g <-1# wet weight, g shape <-2# 0=plate, 1=cylinder, 2=ellipsoid shape_b <-0.0025# ratio of b axis:a axis for ellipsoid shape_c <-0.1176# ratio of c axis:a axis for ellipsoid g_vs_ab <-0.4# leaf vapour conductance, abaxial (bottom of leaf), mol/m2/s g_vs_ad <-0.0# leaf vapour conductance, adaxial (top of leaf), mol/m2/s g_vs_ab_max <-0.4# leaf vapour conductance, abaxial (bottom of leaf), mol/m2/s g_vs_ad_max <-0.0# leaf vapour conductance, adaxial (top of leaf), mol/m2/s epsilon_sub <-0.95# emissivity of the substrate (-) epsilon_sky <-1# emissivity of the sky (-), accounted for already in 'TSKY' output from microclimate model so make it 1 epsilon_L <-0.97# emissivity of the leaf (-) alpha_L <-0.5# solar absorptivity of the leaf (-) fatosk <-0.5# radiation configuration factor to sky (-) fatosb <-0.5# radiation configuration factor to substrate (-) conv_enhance <-1.4# convective enhancement factor (-) pct_cond <-0# percent of leaf conducting to the ground (%) postur <-3# leaf oriented horizontally, not tracking sun M_1 <-0# make metabolic rate zero# Load in the plant heat budgetload(here("output", "microclim", "plant_micro.RDa"))# Loop through the objects and run the plant heat-budget modelfor(i in1:length(objects)){# Load the microclimate objectload(here("output", "microclim", objects[i])) micro <-get(names[i]) # get the name of the object and set it as micro as ectotherm uses object names micro# First, lets extract the relevant information from the microclimate object to get plant stomatal conductance sorted. PDIFs <- micro$diffuse_frac # use variable diffuse fraction P_a <-get_pressure(micro$elev) # air pressure, Pa P_a <-rep(P_a, nrow(micro$metout)) # create hourly vector dates <- micro$dates # Set up vapour conductance vectors and simulate stomatal closure at night f_open <-rep(1, nrow(micro$metout)) f_open[micro$metout[,"ZEN"] ==90] <-0# close stomata when the sun is set g_vs_abs <- g_vs_ab * f_open g_vs_ads <- g_vs_ad * f_open# Include simulated stomatal conductance from plantecophys UMOLPERJ <-4.57# mu_mol photons / JWETAIR_out <-WETAIR(db = micro$metout[,"TALOC"], rh = micro$metout[,"RHLOC"], bp = P_a) VPD_Pa <- WETAIR_out$esat - WETAIR_out$e photo_out <-Photosyn(Tleaf = plant$environ[,"TC"], # Leaf temperature (C)g1 =5.2, # Parameter of Ball-Berry modelCa =400, # Atmospheric CO2 concentration (ppm)VPD = VPD_Pa/1000, # Vapour pressure deficit (kPa)vpdmin =0.5, # lower threshold on VPDPPFD = micro$metout[,"SOLR"] * UMOLPERJ *1, # mu mol m-2 s-1Patm = P_a/1000# Atmospheric pressure (kPa) )g_vs_abs_pho <- photo_out$GS# Run the heat-budget model for the plant leaf. Note that the microclimate object is used to get the relevant environmental temperatures to make the calculations of leaf temperature plant <-ectotherm( leaf = leaf, live = live,Ww_g = Ww_g, shape = shape, shape_b = shape_b, shape_c = shape_c, g_vs_ab = g_vs_abs_pho, g_vs_ad = g_vs_ads, g_vs_ab_max = g_vs_ab_max, g_vs_ad_max = g_vs_ad_max, epsilon_sub = epsilon_sub, epsilon_sky = epsilon_sky,epsilon = epsilon_L, alpha_max = alpha_L, alpha_min = alpha_L,M_1 = M_1,fatosk = fatosk, fatosb = fatosb, conv_enhance = conv_enhance, pct_cond = pct_cond, postur = postur,preshr = P_a, PDIF = PDIFs)# Save the plant objectsave(plant, file =here("output", "microclim", paste0("plant_", gsub(".RDa", "", objects[i]), "_micro.RDa")))}
5.0.1.4 Impacts of watering scenerios on leaf temperature
We’ve now used the heat budget model applying each of the three different microclimate objects to calculate the leaf temperature of the plant. We can now plot this out to see how the watering events affect leaf temperature.
Code
# Extract the microclimate temperatures. Because this was simulated for a plant the 'body temperature' is the leaf temperature and is stored as 'TC' in the environ objectobjects <-c("plant_microclimate_micro.RDa", "plant_rainhour_2_shade_micro.RDa", "plant_rainhour_5_shade_micro.RDa", "plant_rainhour_10_shade_micro.RDa", "plant_rainhour_20_shade_micro.RDa")listnames <-c("envron_plant", "environ_plant_2", "environ_plant_5", "environ_plant_10", "environ_plant_20")environ_plant <-list()for(i in1:length(objects)){# Load the plant objectload(here("output", "microclim", objects[i])) environ_plant[[i]] <-as.data.frame(plant$environ)}names(environ_plant) <- listnames# Now, lets bind these data.frames together and re-organise the data for plotting environ_plant2 <- plyr::ldply(environ_plant) # Extract and stores dates environ_plant2$dates <-rep(dates, 5) # Now create a column that names each scenario environ_plant2 <- environ_plant2 %>%mutate(.id =ifelse(.id =="envron_plant", "Original Rainfall", ifelse(.id =="environ_plant_2", "Watering (2 mm)", ifelse(.id =="environ_plant_5", "Watering (5 mm)", ifelse(.id =="environ_plant_10", "Watering (10 mm)", ifelse(.id =="environ_plant_20", "Watering (20 mm)", NA)))))) %>%mutate(.id =factor(.id, levels =c("Original Rainfall", "Watering (2 mm)", "Watering (5 mm)", "Watering (10 mm)", "Watering (20 mm)")))
Code
# Now lets plot the leaf temperature of the plant over time for each of the three different watering scenariosplant_temps_water <- environ_plant2 %>%filter(dates >"2019-01-15"& dates <"2019-01-20") %>%ggplot() +geom_line(aes(y = TC, x = dates, col = .id), show.legend =TRUE, size =1) +labs(y ="Leaf Temperature (°C)", x ="Date") +theme_classic() +theme(legend.position ="none") +scale_colour_viridis_d() +theme(legend.title =element_blank(),legend.text =element_blank(),axis.title =element_text(size =24),axis.text.x =element_blank(), axis.text.y =element_text(size =20)) # Export the figure for Fig 2 in paperggsave(plant_temps_water, filename =here("figs", "fig-plant_temp2.png"), width =8.580247, height =6.111111, dpi =600)
Table 1— Mean leaf temperature differences between the original rainfall and the watering scenarios (2, 5, 10 and 20 mm of water added) during the heat wave event (2019-01-15 to 2019-01-31) between the hours 11:00 and 16:00 hr.
Watering Scenario
Mean Leaf Temperature Difference (°C)
Original Rainfall
0.00
Watering (2 mm)
-2.80
Watering (5 mm)
-3.63
Watering (10 mm)
-3.82
Watering (20 mm)
-3.87
?@fig-plant_temp3 shows how much watering at the various levels affects the leaf temperature of the plant during the day (between 11:00-14:00 hr). We can see that watering even just 2mm can reduce the leaf temperature by on average 2.8 \(^\circ\text{C}\) (Tab. 1). Interestingly, we can see that after 5mm of water, the leaf temperature doesn’t change much, suggesting that we can get away with less water. A good thing in a very arid environment!
5.0.2Impacts of watering scenarios on grasshopper life cycle
5.0.2.1 Setting the scene
Now that we have the leaf temperature of the plant under the various watering scenarios, we can use this to understand how the different scenarios might affect grasshopper body temperature, life cycle and population dynamics like we did in Box 2. This is a nice demonstration of re-applying everything we’ve learnt to a new problem – a problem potentially involving the need to control and understand the probability of pest outbreaks.
We can do this by using the ectotherm model using the leaf temperature under the different watering scenarios as the microclimate for the grasshopper. Given the short period of the heat wave and watering regimes we don’t expect major differences but it’s better to check.
5.0.2.2 Use the different watering scenarios to look at how changes in temperature during the watering regime period affect the grasshopper life cycle
As we did before, we’ll update the microclimate experienced by the grasshopper to coincide with the leaf temperature and environmental conditions of the plant (assuming grasshoppers are within the bush in 40% shade). This will then calculate what temperatures the grasshopper experiences using a heat-budget model to predict the grasshopper body temperature. Then, we will use the heat-budget calculations to run the Dynamic Energy Budget model for the grasshopper to predict whether this impacts life-cycle. Again, these simulations are all done at once using the ectotherm function.
Code
# We first need to load the DEB model paraemeters. These are estimated using MatLab. We've already estimated these so we'll simply just load them into R. pars <-readMat(here("sims", "results_Warramaba_virgo.mat"))par.names <-unlist(labels(pars$par))for(i in1:length(par.names)){assign(par.names[i], unlist(pars$par[i]))} pars <-readMat(here("sims", "stat_Warramaba_virgo.mat"))par.names <-unlist(labels(pars$stat))for(i in1:length(par.names)){assign(par.names[i], unlist(pars$stat[i]))}# Additional parameters we need E_sm <-2000# J/cm^2, maximum stomach energy content fract <-1# scaling factor to change size# Inital state for DEB model# Egg Stage. Starting conditions E_init <- (E.0* fract ^4) / (3E-9* fract ^3) E_H_init <-0 V_init <-3E-9 stage <-0# Life history traits clutchsize <-8# Eggs per clutch stages <-7# Number of stages, 0 = egg, 1-5 = nymphs, 6 = pre egg-laying adult, 7 = post-laying adult S_instar <-rep(s.1, stages) # Stress at instar n: L_n^2/ L_n-1^2 (-)}\cr} L_b <- L.b # Structural length at birth (cm)}\cr} photostart <-0# no photoperiod effect on reproductionphotofinish <-0# no photoperiod effect on reproduction reset <-0# don't reset lifecycle if death occurs soilnode <-3# depth (soil node) at which eggs laid - 3 = 5 cm# Thermoregulatory parameters Ww_g <-0.2# g, body mass, not used though because DEB model computes this shape <-1# cylinder shape_b <-10# ratio of long to short axis of cylinder T_pref <-38# preferred body temperature (animal will attempt to regulate as close to this value as possible) T_F_min <-13# degrees C, lower body temperature for foraging T_F_max <-40# degrees C, upper body temperature for foraging T_B_min <-13# degrees C, minimum basking temperature T_RB_min <-13# degrees C, temperature at which animal will move to a basking site CT_max <-47# degrees C, critical thermal maximum (animal will die if CT_kill = 1 and this threshold is exceeded) CT_min <-0# degrees C, critical thermal minimum (used by program to determine depth selected when inactive and burrowing)# Behavioural traits (made stage-specific later) diurn <-1# Diurnal activity allowed (1) or not (0)? nocturn <-1# Nocturnal activity allowed (1) or not (0)? crepus <-1# Crepuscular activity allowed (1) or not (0)? burrow <-0# Shelter in burrow allowed (1) or not (0)? shade_seek <-1# Shade seeking allowed (1) or not (0)? pct_cond <-0# Percentage of animal surface contacting the substrate (\%)
Code
#' @title Ricker model function#' @description A simple Ricker model function to simulate population dynamics#' @param N0 Initial population size#' @param r Intrinsic growth rate#' @param sd_r Standard deviation of the growth rate#' @param K Carrying capacity#' @return Next years population sizericker <-function(N0, r, sd_r, K, time){ N0 *exp(rnorm(1, r, sd_r)*(1- (N0/K)))}## Create a function to do a stochastic simulation of the Ricker model#' @title Ricker model simulation function#' @description A simple Ricker model function to simulate population dynamics#' @param N0 Initial population size#' @param r Intrinsic growth rate#' @param sd_r Standard deviation of the growth rate#' @param K Carrying capacity#' @param time Time step#' @param nreps Number of replicates#' @return A matrix of population sizes at each time step for nrepssim_ricker <-function(N0, r, sd_r, K, time, nreps){ N_array <-array(NA, dim =c(time, nreps)) # Create a vector to store the population size at each time stepfor(j in1:nreps){ N_array[1,j] <- N0for(i in2:time){ N_array[i,j] <-ricker(N_array[i-1, j], r, sd_r, K) } }return(N_array)}
Code
# First, we need to load in the microclimates scenerios. We'll do this in a loop so that we can run the DEB model for each of the scenerios. So, here we'll just create a few vectors that cell specific objects we need. plant_micro <-c("plant_microclimate_micro.RDa", "plant_rainhour_2_shade_micro.RDa", "plant_rainhour_5_shade_micro.RDa", "plant_rainhour_10_shade_micro.RDa", "plant_rainhour_20_shade_micro.RDa") plant_mirco_names <-c("envron_plant", "environ_plant_2", "environ_plant_5", "environ_plant_10", "environ_plant_20")# We also need to load in the original microclimate object so that we can get the dates and other information from it. This is the original microclimate object that we used to simulate the plant microclimate. We'll also overide the `mirco` object so that the ectotherm model can use it.load(here("output", "microclim", "microclimate.RDa"))# Now we can loop through the two microclimates and run the DEB model for each of themfor(i in1:length(plant_micro)){# Load the microclimate objectload(here("output", "microclim", plant_micro[i]))# Use the plant microclimate data to reset the original microclimate object. Note that we need to adjust both shade and nonshade conditions # Unshaded microclimate micro$metout <- plant$metout micro$metout [,"TALOC"] <- plant$environ[,"TC"]# Shaded microclimate microshadmet <- plant$shadmet micro$shadmet [,"TALOC"] <- plant$environ [,"TC"]# Soil microclimate micro$soil <- plant$soil micro$shadsoil <- plant$shadsoil micro$soilmoist <- plant$soilmoist micro$shadmoist <- plant$shadmoist# Get dates from microclimate object dates <- micro$dates # dates, hourlydates2 <- micro$dates2 # dates, daily# Set starting date by truncating microclimate model output to when grasshoppers are epected to oviposit eggs. This is the date when the life cylce of the insect is expected to start.oviposition_date <-'2018-03-31 23:00:00' subs <-which( dates >as.POSIXct(oviposition_date)) subs2 <-which(dates2 >as.POSIXct(oviposition_date))# Now run the DEB life-cyle model for the grasshopper. See Case Study 2 for more details on the parameters used in the DEB model. ecto <-ectotherm( Ww_g = Ww_g,shape = shape,shape_b = shape_b,T_pref = T_pref,T_F_min = T_F_min,T_F_max = T_F_max,T_B_min = T_B_min,T_RB_min = T_RB_min,CT_min = CT_min,CT_max = CT_max,diurn = diurn,nocturn = nocturn,crepus = crepus,shade_seek = shade_seek,burrow = burrow,pct_cond = pct_cond,nyears =ceiling(length(subs2)/365),metout = micro$metout[subs,],soil = micro$soil[subs,], soilpot = micro$soilpot[subs,],soilmoist = micro$soilmoist[subs,], humid = micro$humid[subs,],tcond = micro$tcond[subs,], shadmet = micro$shadmet[subs,],shadsoil = micro$shadsoil[subs,], shadpot = micro$shadpot[subs,],shadmoist = micro$shadmoist[subs,],shadhumid = micro$shadhumid[subs,],shadtcond = micro$shadtcond[subs,], rainfall = micro$RAINFALL[subs2],minshades =rep(unique(micro$minshade), length(subs)),maxshades =rep(unique(micro$maxshade), length(subs)),DEB =1, # turns DEB model onmetab_mode =1, # turns on hemimetabolous model, abpz = z * fract,del_M = del.M,p_Xm = p.Am *100/24/ kap.X,kap_X = kap.X,v = v /24,kap = kap,p_M = p.M /24,E_G = E.G,kap_R = kap.R,k_J = k.J /24,E_Hb = E.Hb * fract ^3,E_Hp = (E.Hj+1e-5) * fract ^3,E_Hj = E.Hj * fract ^3,h_a = h.a / (24^2),s_G = s.G,E_0 = E.0* fract ^4,mu_E = mu.E,d_V = d.V,d_E = d.E,T_REF = T.ref,T_A = T.A,T_AL = T.AL,T_AH = T.AH,T_L = T.L,T_H = T.H,E_sm = E_sm,E_init = E_init,E_H_init = E_H_init,V_init = V_init,stage = stage,clutchsize = clutchsize,stages = stages,S_instar =rep(s.1, stages),L_b = L.b,photostart = photostart,photofinish = photofinish,reset = reset,soilnode = soilnode)# Save the DEB modelsave(ecto, file =here("output", "microclim", paste0("grasshopper_", plant_mirco_names[i], "_DEB.RDa")))}
Now that we have the DEB model for each of the watering scenerios, we can plot out the grasshopper life cycles for each scenerio (Fig. 5).
Code
# Scenerios plant_mirco_names <-c("envron_plant", "environ_plant_2", "environ_plant_5", "environ_plant_10", "environ_plant_20") debs <-paste0("grasshopper_", plant_mirco_names, "_DEB.RDa") scenerios <-c("Original Rainfall", "Watering (2 mm)", "Watering (5 mm)", "Watering (10 mm)", "Watering (20 mm)")# Now, lets load in the DEB model for each of the scenerios and plot them out. We'll save to a listplot_list <-list()for(i in1:length(debs)){# Load the DEB modelload(here("output", "microclim", debs[i])) datetime <- dates[subs] sub <-which(datetime >as.POSIXct('2018-03-31') & datetime <as.POSIXct('2019-04-30')) debout <-data.frame(ecto$debout) debout$datetime <- datetime scale_factor <-10 p <-ggplot() +ylim(-5, 400) +geom_line(data = debout, aes(x = datetime, y = WETMASS *1000), col ="black") +geom_line(data = debout %>%filter(STAGE ==0), aes(x = datetime, y = WETMASS *1000), col ="darkgrey", linewidth =2) +geom_line(data = debout %>%filter(STAGE ==1), aes(x = datetime, y = WETMASS *1000), col ="lightgreen", linewidth =2) +geom_line(data = debout %>%filter(STAGE ==2), aes(x = datetime, y = WETMASS *1000), col ="green", linewidth =2) +geom_line(data = debout %>%filter(STAGE ==3), aes(x = datetime, y = WETMASS *1000), col ="tan", linewidth =2) +geom_line(data = debout %>%filter(STAGE ==4), aes(x = datetime, y = WETMASS *1000), col ="brown", linewidth =2) +geom_line(data = debout %>%filter(STAGE ==5), aes(x = datetime, y = WETMASS *1000), col ="orange", linewidth =2) +geom_line(data = debout %>%filter(STAGE ==6), aes(x = datetime, y = WETMASS *1000), col ="pink", linewidth =2) +labs(title = scenerios[i], x ="Date") +scale_y_continuous(name ="Wet mass (mg)")+theme_classic()plot_list[[i]] <- p }(plot_list[[1]] + plot_list[[2]]) / (plot_list[[3]] + plot_list[[4]]) / (plot_list[[5]]) +plot_annotation(tag_levels ="A") +theme(plot.tag =element_text(size =12, face ="bold"))
Figure 5— Life-cycle of a grasshopper at a location near Renmark, South Australia. Life-cycle of the grasshopper is shown in under 5 different scenarios: A) Original rainfall and B) 2mm, C) 5mm, D) 10 mm and E) 20mm of watering to plants during a heat wave event. Note that the life-cycle is shown as the wet mass of the grasshopper (mg) over time (days) with different colours corresponding to different life stages. The life stages are: 0 = egg (‘grey’), 1 = first instar (‘forest green’), 2 = second instar (‘lime green’), 3 = third instar (‘tan’), 4 = fourth instar (‘brown’), 5 = fifth instar (‘orange’), 6 = adult (‘pink’).
Just like before we can get some key parameters from the DEB model to understand how the different watering scenerios affect the grasshopper life cycle.
Table 2— Predicted life-cycle parameters of a grasshopper at a location near Renmark, South Australia under different watering scenerios (2, 5, 10 and 20 mm of water added) during a heat wave event. The life-cycle parameters are: A) Time to first instar (days), B) Time to adult (days), C) Time to oviposition (days), D) Time to first egg hatch (days), E) Time to first instar hatch (days), F) Time to first adult hatch (days).
We can then look at the expected population growth rate of the grasshopper species under the different watering scenerios as we did in case study 2.
Code
set.seed(123) # Set seed for reproducibility# Set up the parameters for the Ricker model N0 <-1000# Initial population size r <- life_cycle[,"Intrinsic growth rate (r)"] # Intrinsic growth (DEB model) names <- scenerios # Scenerios sd_r <-0.15# Standard deviation of the growth rate K <-10000# Carrying capacity time <-5# Time steps nsims <-1000# Number of simulations# Lets run the simulationssims <-list()for(i in1:length(r)){ sims[[i]] <-sim_ricker(N0, r[i], sd_r, K, time, nsims)names(sims)[i] <- names[i]}# Re-organise the data for plottingdata_organised <-lapply(sims, function(x) {pivot_longer(as.data.frame(x), cols =everything(), names_to ="sim", values_to ="N") %>%mutate(time =rep(1:time, each = nsims))})# Plot the data for each sceneriodata_organised2 <- plyr::ldply(data_organised, .id ="scenario") %>%mutate(scenerio_sim =interaction(scenario, sim)) # Create a summary of the simulations to make plotting easier data_organised2_final <- data_organised2 %>%group_by(scenario, time) %>%summarise(N_m =mean(N), sd =sd(N)) # Now plot the simulationsplot_water_sims <-ggplot(data_organised2_final, aes(x = time, y = N_m, color = scenario)) +geom_ribbon(aes(ymin = N_m - sd, ymax = N_m + sd, fill = scenario), alpha =0.3, colour =NA) +geom_line() +labs(x ="Time (Generations)", y ="Population size (N)") +theme_classic() +scale_colour_viridis_d() +scale_fill_viridis_d() +theme(axis.title =element_text(size =20),axis.text =element_text(size =20),legend.position ="inside",legend.position.inside =c(0.25,0.8),legend.text =element_text(size =20),legend.title =element_blank())plot_water_sims
Figure 6— Expected population growth rate of a grasshopper at a location near Renmark, South Australia under different watering scenerios (2, 5, 10 and 20 mm of water added). Models assume that the watering scenerio is implemented each generation leading to a similar intrinsic population growth rate. Watering scenerios implemented at different times of the year are likely to have different effects on the population growth rate, but could be captured by simulating lice cyles for different years.
6 References
1.
Kearney, M. and Porter, W. (2009) Mechanistic niche modelling: Combining physiological and spatial data to predict species’ ranges. Ecol. Lett. 12, 334–350
2.
Kearney, M.R. et al. (2013) Balancing heat, water and nutrients under environmental change: A thermodynamic niche framework. Funct. Ecol. 27, 950–966
---title: "A systems-modelling approach to predict biological responses to extreme heat"date: "`r Sys.Date()`"bibliography: ../bib/refs.bibcsl: ../bib/trends-in-ecology-and-evolution.cslformat: html: toc: true toc-location: left toc-depth: 3 toc-title: "**Table of Contents**" output-file: "index.html" theme: cosmo embed-resources: true code-fold: show code-tools: true number-sections: true fontsize: "12" max-width: "10" code-overflow: wrapcrossref: fig-title: Figure # (default is "Figure") tbl-title: Table # (default is "Table") title-delim: — # (default is ":") fig-prefix: Fig. # (default is "Figure") tbl-prefix: Tab. # (default is "Table")editor_options: chunk_output_type: console---This file contains the code and data needed for demonstrating how a systems modelling approach can be applied in practice and can be found on [GitHub](https://github.com/daniel1noble/thermal_tol_interactions). It is the code for reproducing the examples in **Box 1** and **Box 2** for the manuscript:Noble *et al.* 2025. **A systems-modelling approach to predict biological responses to extreme heat**. *Trends in Ecology and Evolution*, under review.It also demonstrates how to couple models from different disciplines to predict the impacts of extreme heat on plant-insect interactions. Please get in touch with Daniel Noble (daniel.noble@anu.edu.au) and Mike Kearney (m.kearney@unimelb.edu.au) if you have any questions about the code or the models.# **Introduction**Here, we provide the code for reproducing the systems-modelling approaches described in Boxes 1 & 2. # **Load packages**Our code is built using the statistical software [R](https://www.r-project.org/). We use a number of packages to run our simulations, including [`NicheMapR`](https://mrke.github.io)[@Kearney2009b; @Kearney2013], [`microclima`](https://github.com/ilyamaclean/microclima)[@Maclean2019; @Kearney2020], and [`plantecophys`](https://github.com/RemkoDuursma/plantecophys)[@Duursma2015]. We recommend that readers interested in working through this the code download our [GitHub repository](https://github.com/daniel1noble/thermal_tol_interactions), which contains all the data required to run the models. This code file can also located in the `docs/supp.qmd` file. You should then be able to run the code in this document to reproduce the figures and tables without having to setup your repository. The code below loads the required packages that are needed.```{r, libraries}#| label: libraries#| echo: true#| warning: false#| message: false# You will need to install a few packages first, a couple from GitHub as they are not on CRAN. Note that you will need to remove '#' from the code below to install the packages.# install.packages("pacman")# devtools::install_github('mrke/NicheMapR')# devtools::install_github("ilyamaclean/microclima")# Load required libraries after the installationpacman::p_load(devtools, deSolve, ggplot2, dplyr, tidyr, purrr, patchwork, equatags, flextable, NicheMapR, terra, microclima, plantecophys, magick, here, ggrepel, lutz, R.matlab, magick, grid, DT, latex2exp, popbio, popdemo)```Throughout we'll also need some helper functions. These can be added to your working environment by running the code below.```{r, functions}#| label: functions#| echo: true#| warning: false#| message: false#| eval: false#' @title convert_doy#' @description Convert day of year (DOY) to date, month-day, ymd, or julian date#' @param doy Numeric vector of day of year#' @param year Numeric vector of year#' @param format_type Character vector of format type. Default is "date"#' @return Date, month, month-day, ymd, or julian date#' @examples#' convert_doy(120, 2024, "date") # "2024-04-29"#' convert_doy(120, 2024, "month_day") # "April 29" #' convert_doy(120, 2024, "ymd") # "2024-04-29"convert_doy <-function(doy, year, format_type ="date") {# Convert DOY to Date date <-as.Date(doy -1, origin =paste0(year, "-01-01"))# Return different formats based on format_typeswitch(format_type,"date"= date, # Default: Date format"month_day"=format(date, "%B %d"), # "April 29""month"=format(date, "%B"), # "April""ymd"=format(date, "%Y-%m-%d"), # "2024-04-29""julian"=format(date, "%Y%j"), # "2024120"stop("Invalid format_type. Choose from 'date', 'month_day', 'ymd', or 'julian'.") )}#' @title convert_time#' @description Convert decimal time to hh:mm, hh:mm:ss, or 12-hour format#' @param time_minutes Numeric vector of decimal time in minutes#' @param format_type Character vector of format type. Default is "hh:mm"#' @return Time in hh:mm, hh:mm:ss, or 12-hour format#' @examples#' convert_time(13.5, "hh:mm") # "13:30"#' convert_time(13.5, "hh:mm:ss") # "13:30:00"#' convert_time(13.5, "12-hour") # "1:30 PM"convert_time_for_plot <-function(time_minutes, format_type ="decimal") {# Extract hours and minutes hours <- time_minutes %/%60 minutes <- time_minutes %%60# Format output based on format_typeswitch(format_type,"decimal"= hours + minutes /60, # e.g., 13.5 for 13:30"POSIXct"=as.POSIXct(sprintf("%02d:%02d", hours, minutes), format="%H:%M", tz="UTC"), # e.g., "13:30:00 UTC""hms"= hms::hms(hours *3600+ minutes *60), # e.g., 13:30:00stop("Invalid format_type. Choose from 'decimal', 'POSIXct', or 'hms'.") )}```:::{.column-margin}:::{.callout-note}gFortran can be a problem for NicheMapR so you need to set the paths properly if you are getting errors. Follow details [here](https://stackoverflow.com/questions/69639782/installing-gfortran-on-macbook-with-apple-m1-chip-for-use-in-r). The following code will help you set the paths properly.```{r, nichmapr}#| label: nichmapr#| echo: true#| eval: false#| results: hide#| code-fold: true# But do the following:# 1. Install gfortran using homebrew in the terminal/bash window brew install gcc # 2. Take note of the path to gfortran. You'll need this for later which gfortran# 3. In home directory of terminal create a Makevars file mkdir ~/.R touch ~/.R/Makevars open ~/.R/Makevars# 4. Add the following to the Makevars file. Note that you need to make sure your version of fortran is correct and replace 124.2.0_1 with your version. You can find version in terminal by typing `gfortran --version`. In addition you need to replace the path to gfortran with the path you found in step 2. FC =/opt/homebrew/Cellar/gcc/14.2.0_1/bin/gfortran F77 =/opt/homebrew/Cellar/gcc/14.2.0_1/bin/gfortran FLIBS =-L/opt/homebrew/Cellar/gcc/14.2.0_1/lib/gcc/11#5. Restart R and try to install the package again via devtools. It should now work.```::::::# **Box 1 Code: Building a systems modelling approach to capture multi-species exposure to extreme heat**## Getting the microclimate dataWe first need to use climate data to calculate the microclimate at our location of interest. Here, we use the `micro_silo` function to calculate the microclimate at a location near Renmark, South Australia (longitude = 140.799, latitude = -34.159) for the years 2018-2019.```{r, microclimate_box1}#| label: microclimate_box1#| echo: true#| warning: false#| message: false#| error: false#| eval: false # First, set the location where NCEAP climate data is located. This is a global dataset. We need this to calculate the microclimate. Only need to do once.get.global.climate(folder=here("NCEP_dan", "NCEAP" ))# Location we want longlat <-c(140.79970255974192, -34.159176277782564) # Near Renmark, South Australia micro <-micro_silo( loc = longlat, # Locationdstart ="01/01/2018", # Start yeardfinish ="31/12/2019", # End yearminshade =40, # Minimum shade, %maxshade =80, # Maximum shade, %Usrhyt =0.2, # Height above ground, m microclima =1, # Use mircoclima PC =-1500, # Water potential for stomatal closureSP =10, # Stability parameter for stomatal closureemail ="daniel.noble@anu.edu.au") # Replace with your email# Save the microclimate object for later in case we need it again as it avoids needing to rerun. save(micro, file =here("output", "microclim", "microclimate.RDa"))```## Plant biophysical model with stomatal conductanceNow that we have the microclimate data, we can run the biophysical model to calculate the realised environmental conditions for a plant leaf using the `ectotherm` function in `NicheMapR`. We first need to set up the parameters for the plant leaf.```{r, microclimate_plant_box1}#| label: microclimate_plant_box1#| echo: true#| warning: false#| message: false# Load the microclimate objectload(here("output", "microclim", "microclimate.RDa"))# First, lets extract the relevant information from the microclimate object PDIFs <- micro$diffuse_frac # use variable diffuse fraction P_a <-get_pressure(micro$elev) # air pressure, Pa P_a <-rep(P_a, nrow(micro$metout)) # create hourly vector dates <- micro$dates # dates# Set leaf functional traits live <-0# don't simulate behaviour or respiration leaf <-1# use the stomatal conductance formulation for evaporation Ww_g <-1# wet weight, g shape <-2# 0=plate, 1=cylinder, 2=ellipsoid shape_b <-0.0025# ratio of b axis:a axis for ellipsoid shape_c <-0.1176# ratio of c axis:a axis for ellipsoid g_vs_ab <-0.4# leaf vapour conductance, abaxial (bottom of leaf), mol/m2/s g_vs_ad <-0.0# leaf vapour conductance, adaxial (top of leaf), mol/m2/s g_vs_ab_max <-0.4# leaf vapour conductance, abaxial (bottom of leaf), mol/m2/s g_vs_ad_max <-0.0# leaf vapour conductance, adaxial (top of leaf), mol/m2/s epsilon_sub <-0.95# emissivity of the substrate (-) epsilon_sky <-1# emissivity of the sky (-), accounted for already in 'TSKY' output from microclimate model so make it 1 epsilon_L <-0.97# emissivity of the leaf (-) alpha_L <-0.5# solar absorptivity of the leaf (-) fatosk <-0.5# radiation configuration factor to sky (-) fatosb <-0.5# radiation configuration factor to substrate (-) conv_enhance <-1.4# convective enhancement factor (-) pct_cond <-0# percent of leaf conducting to the ground (%) postur <-3# leaf oriented horizontally, not tracking sun M_1 <-0# make metabolic rate zero# Set up vapour conductance vectors and simulate stomatal closure at night f_open <-rep(1, nrow(micro$metout)) f_open[micro$metout[,"ZEN"] ==90] <-0# close stomata when the sun is set g_vs_abs <- g_vs_ab * f_open g_vs_ads <- g_vs_ad * f_open# Run the heat-budget model for the plant leaf. Note that the microclimate object is used to get the relevant environmental temperatures to make the calculations of leaf temperature (by default, ectotherm expects an object in the environment called 'micro' and uses it to obtain the relevant environmental data, but this can be overridden). plant <-ectotherm( leaf = leaf, live = live,Ww_g = Ww_g, shape = shape, shape_b = shape_b, shape_c = shape_c, g_vs_ab = g_vs_abs, g_vs_ad = g_vs_ads, g_vs_ab_max = g_vs_ab_max, g_vs_ad_max = g_vs_ad_max, epsilon_sub = epsilon_sub, epsilon_sky = epsilon_sky,epsilon = epsilon_L, alpha_max = alpha_L, alpha_min = alpha_L,M_1 = M_1,fatosk = fatosk, fatosb = fatosb, conv_enhance = conv_enhance, pct_cond = pct_cond, postur = postur,preshr = P_a, PDIF = PDIFs)# Save model plant$dates <- dates # Add in dates for claritysave(plant, file =here("output", "microclim", "plant_micro.RDa"))```## Plant biophysical model with stomatal conductance from plantecophysAllow for stomatal conductance to be simulated using the `plantecophys` package. We can do this by calculating stomatal conductance using the `Photosyn` function in `plantecophys`. This function requires vapour pressure deficit (VPD) which we can calculate from the microclimate data using the `WETAIR` function in `NicheMapR`.```{r, plant_stomatal_box1}#| label: plant_stomatal_box1#| echo: true#| warning: false#| message: false# Need to extract a few things from the plant object we just created# Load the plant objectload(here("output", "microclim", "plant_micro.RDa"))# Include simulated stomatal conductance from plantecophys UMOLPERJ <-4.57# mu_mol photons / JWETAIR_out <-WETAIR(db = plant$metout[,"TALOC"], rh = plant$metout[,"RHLOC"], bp = P_a) VPD_Pa <- WETAIR_out$esat - WETAIR_out$e photo_out <-Photosyn(Tleaf = plant$environ[,"TC"], # Leaf temperature (C)g1 =5.2, # Parameter of Ball-Berry modelCa =400, # Atmospheric CO2 concentration (ppm)VPD = VPD_Pa/1000, # Vapour pressure deficit (kPa)vpdmin =0.5, # lower threshold on VPDPPFD = plant$metout[,"SOLR"] * UMOLPERJ *1, # mu mol m-2 s-1Patm = P_a/1000# Atmospheric pressure (kPa) )g_vs_abs_pho <- photo_out$GS```Now calculate leaf temperature using these stomatal conductance values from `Photosyn`. Note that we have re-written the `g_vs_abs_pho` to capture the stomatal conductance from `Photosyn`. We just replace this in the plant biophysical model.```{r, plant_stomatal2_box1}#| label: plant_stomatal2_box1#| echo: true#| warning: false#| message: false# Rerun the models. Note that the parameters as the same as above except for g_vs_ab which is now from plantecophys plant2 <-ectotherm( leaf = leaf, live = live,Ww_g = Ww_g, shape = shape, shape_b = shape_b, shape_c = shape_c, g_vs_ab = g_vs_abs_pho, g_vs_ad = g_vs_ads, g_vs_ab_max = g_vs_ab_max, g_vs_ad_max = g_vs_ad_max, epsilon_sub = epsilon_sub, epsilon_sky = epsilon_sky,epsilon = epsilon_L, alpha_max = alpha_L, alpha_min = alpha_L,M_1 = M_1,fatosk = fatosk, fatosb = fatosb, conv_enhance = conv_enhance, pct_cond = pct_cond, postur = postur,preshr = P_a, PDIF = PDIFs)# Save model plant2$dates <- dates # Add in dates for claritysave(plant2, file =here("output", "microclim", "plant2_micro.RDa"))```Now, lets have a look at the leaf temperature when stomatal closure behaviour is incorporated.```{r, fig-plant_temp2_box1}#| label: fig-plant_temp2_box1#| eval: false#| echo: true#| code-fold: true# Load the plant objectload(here("output", "microclim", "plant_micro.RDa"))load(here("output", "microclim", "plant2_micro.RDa"))# Extract the microclimate temperatures. Because this was simulated for a plant the 'body tempertaure'is the leaf temperature and is stored as 'TC' in the environ object. Note now that this new biophysical model has stomatal behaviour incorporated in the new plant2 object. environ_plant <-as.data.frame(plant$environ) environ_plant$dates <- plant$dates # For clarity add in dates from microclimate that ecto model uses environ_plant_plantecophys2 <-as.data.frame(plant2$environ) environ_plant_plantecophys2$dates <- plant2$dates # For clarirty add in dates from microclimate that ecto model uses# Re-organise the data for plotting environ_plant2 <- environ_plant %>%filter(dates >"2019-01-15"& dates <"2019-01-18") %>%select(dates, TA, TC) %>%pivot_longer(cols =c(TA, TC), names_to ="Temps", values_to ="Temperature") %>%mutate(Temps =ifelse(Temps =="TA", "Air Temperature (2 m) (40% shade)", "Leaf Temperature (Stomata Open) (40% shade)")) environ_plant_plantecophys <- environ_plant_plantecophys2 %>%filter(dates >"2019-01-15"& dates <"2019-01-18") %>% dplyr::select(dates, TC) %>%pivot_longer(cols =c(TC), names_to ="Temps", values_to ="Temperature") %>%mutate(Temps =ifelse(Temps =="TC", "Leaf Temperature (Stomatal Closure) (40% shade)"))# Combine the two dataframes environ_plant_plantecophys and environ_plant2 new_environ_plant2 <-rbind(environ_plant_plantecophys, environ_plant2)# Produce element of the fgure in box 1. Note it was adjusted in Biorender.com for final version.ggplot(new_environ_plant2) +geom_line(aes(y = Temperature, x = dates, col = Temps), show.legend =TRUE, size =0.8) +labs(y ="Temperature (°C)", x ="Date") +theme_classic() +theme( axis.text.x =element_text(angle =45, vjust =1, hjust =1),legend.position ="none", legend.text =element_text(size =14), axis.text =element_text(size =14), axis.title =element_blank()) +scale_colour_viridis_d()# ggsave(here("figs", "fig_plant_temp2_box1.png"), width = 8, height = 5, dpi = 600)```Here, we can see that stomatal closure in the middle of the day causes the leaf temperatures to rise substantially compared to if they were always open and transpiring (i.e., evaporative cooling). This is important because it means that the grasshopper will be exposed to higher temperatures when plant stomata close midday. ## Run biophysical-physiological model for two grasshoppers using the plant microclimate as the environment for grasshoppersDefine the biophysical model parameters for the grasshopper. Note we also include DEB model parameters here, which relate to Box 2, but it's easier to run the model once with all the parameters defined and we can (and will) extract what we need for Box 1.```{r, speciesA_grasshopper_params}#| label: speciesA_grasshopper_params#| echo: true#| message: false#| warning: false# We first need to load the DEB model paraemeters. These are estimated using MatLab. We've already estimated these so we'll simply just load them into R. pars <-readMat(here("sims", "results_Warramaba_virgo.mat"))par.names <-unlist(labels(pars$par))for(i in1:length(par.names)){assign(par.names[i], unlist(pars$par[i]))} pars <-readMat(here("sims", "stat_Warramaba_virgo.mat"))par.names <-unlist(labels(pars$stat))for(i in1:length(par.names)){assign(par.names[i], unlist(pars$stat[i]))}# Additional parameters we need E_sm <-1800# J/cm^2, maximum stomach energy content fract <-1# scaling factor to change size# Inital state for DEB model# Egg Stage. Starting conditions E_init <- (E.0* fract ^4) / (3E-9* fract ^3) E_H_init <-0 V_init <-3E-9 stage <-0# Life history traits clutchsize <-15# Eggs per clutch stages <-7# Number of stages, 0 = egg, 1-5 = nymphs, 6 = pre egg-laying adult, 7 = post-laying adult S_instar <-rep(s.1, stages) # Stress at instar n: L_n^2/ L_n-1^2 (-)}\cr} L_b <-0.09# Structural length at birth (cm)}\cr} photostart <-0# no photoperiod effect on reproductionphotofinish <-0# no photoperiod effect on reproduction reset <-0# don't reset lifecycle if death occurs soilnode <-3# depth (soil node) at which eggs laid - 3 = 5 cm sG <-0.001# Gompertz stress coefficient# Thermoregulatory parameters Ww_g <-0.2# g, body mass, not used though because DEB model computes this shape <-1# cylinder shape_b <-10# ratio of long to short axis of cylinder T_pref <-40# preferred body temperature (animal will attempt to regulate as close to this value as possible) T_F_min <-13# degrees C, lower body temperature for foraging T_F_max <-42# degrees C, upper body temperature for foraging T_B_min <-13# degrees C, minimum basking temperature T_RB_min <-13# degrees C, temperature at which animal will move to a basking site CT_max <-50# degrees C, critical thermal maximum (animal will die if CT_kill = 1 and this threshold is exceeded) CT_min <-0# degrees C, critical thermal minimum (used by program to determine depth selected when inactive and burrowing)# Behavioural traits (made stage-specific later) diurn <-1# Diurnal activity allowed (1) or not (0)? nocturn <-1# Nocturnal activity allowed (1) or not (0)? crepus <-1# Crepuscular activity allowed (1) or not (0)? burrow <-0# Shelter in burrow allowed (1) or not (0)? shade_seek <-0# Shade seeking allowed (1) or not (0)? pct_cond <-0# Percentage of animal surface contacting the substrate (\%)```Run the biophysical-physiological model species A.```{r, grasshopperA_box1_2}#| label: grasshopperA_box1_2#| echo: true#| warning: false#| message: false# First, we need to load in the two microclimates scenarios. We'll do this in a loop so that we can run the DEB model for each of the two microclimates. So, here we'll just create a few vectors that cell specific objects we need. plant_micro <-c("plant_micro.RDa") plant_mirco_names <-c("stomata_closed")# We also need to load in the original microclimate object so that we can get the dates and other information from it. This is the original microclimate object that we used to simulate the plant microclimate. We'll also overide the `mirco` object so that the ectotherm model can use it.load(here("output", "microclim", "microclimate.RDa"))load(here("output", "microclim", "plant_micro.RDa"))# Use the plant microclimate data to reset the original microclimate object. Note that we need to adjust both shade and nonshade conditions# Unshaded microclimate micro$metout <- plant$metout micro$metout [,"TALOC"] <- plant$environ[,"TC"]# Update the IR profiles. micro$metout[,"TSKYC"] <- plant$environ[,"TSKY"] # Update the relative humidity. micro$metout[,"RHLOC"] <- plant$environ[,"RELHUM"]# Shaded microclimate microshadmet <- plant$shadmet micro$shadmet [,"TALOC"] <- plant$environ [,"TC"]# Soil microclimate micro$soil <- plant$soil micro$shadsoil <- plant$shadsoil micro$soilmoist <- plant$soilmoist micro$shadmoist <- plant$shadmoist# Get dates from microclimate object dates <- micro$dates # dates, hourlydates2 <- micro$dates2 # dates, daily# Set starting date by truncating microclimate model output to when grasshoppers are epected to oviposit eggs. This is the date when the life cylce of the insect is expected to start.oviposition_date <-'2018-03-31 23:00:00' subs <-which( dates >as.POSIXct(oviposition_date)) subs2 <-which(dates2 >as.POSIXct(oviposition_date))# Now run the biophysical model for the grasshopper ectoA <-ectotherm( Ww_g = Ww_g,shape = shape,shape_b = shape_b,T_pref = T_pref,T_F_min = T_F_min,T_F_max = T_F_max,T_B_min = T_B_min,T_RB_min = T_RB_min,CT_min = CT_min,CT_max = CT_max,diurn = diurn,nocturn = nocturn,crepus = crepus,shade_seek = shade_seek,burrow = burrow,pct_cond = pct_cond,nyears =ceiling(length(subs2)/365),metout = micro$metout[subs,],soil = micro$soil[subs,], soilpot = micro$soilpot[subs,],soilmoist = micro$soilmoist[subs,], humid = micro$humid[subs,],tcond = micro$tcond[subs,], shadmet = micro$shadmet[subs,],shadsoil = micro$shadsoil[subs,], shadpot = micro$shadpot[subs,],shadmoist = micro$shadmoist[subs,],shadhumid = micro$shadhumid[subs,],shadtcond = micro$shadtcond[subs,], rainfall = micro$RAINFALL[subs2],minshades =rep(unique(micro$minshade), length(subs)),maxshades =rep(unique(micro$maxshade), length(subs)),DEB =1, # turns DEB model onmetab_mode =1, # turns on hemimetabolous model, abpz = z * fract,del_M = del.M,p_Xm = p.Am *100/24/ kap.X,kap_X = kap.X,v = v /24,kap = kap,p_M = p.M /24,E_G = E.G,kap_R = kap.R,k_J = k.J /24,E_Hb = E.Hb * fract ^3,E_Hp = (E.Hj+1e-5) * fract ^3,E_Hj = E.Hj * fract ^3,h_a = h.a / (24^2),s_G = sG, # Gompertz stress coefficientE_0 = E.0* fract ^4,mu_E = mu.E,d_V = d.V,d_E = d.E,T_REF = T.ref,T_A = T.A,T_AL = T.AL,T_AH = T.AH,T_L = T.L,T_H = T.H,E_sm = E_sm,E_init = E_init,E_H_init = E_H_init,V_init = V_init,stage = stage,clutchsize = clutchsize,stages = stages,S_instar =rep(s.1, stages),L_b = L_b, # structural length at birth (cm),photostart = photostart,photofinish = photofinish,reset = reset,soilnode = soilnode)# Save the ectotherm model ectoA$dates <- dates[subs] # Add in dates for claritysave(ectoA, file =here("output", "microclim", paste0("grasshopperspA_", plant_mirco_names, "_Box1.RDa")))```Run the biophysical-physiological model again, but for species B which uses different parameters to capture the different life histories of the species. Note that species B occupies a slightly different microhabitat which is cooler than species A so we will assume they will be closet to leaf temperatures when stomata are open.```{r, speciesB_grasshopper_params}#| label: speciesB_grasshopper_params#| echo: true#| warning: false#| message: false# Using the same starting parameters as species A, so will not re-load the matrix again. # Additional parameters we need E_sm <-2000# J/cm^2, maximum stomach energy content fract <-1# scaling factor to change size# Inital state for DEB model. Note that DEB can be turned off and only heat and water budget models will be used so some parameters are not needed if DEB is off.# Egg Stage. Starting conditions E_init <- (E.0* fract ^4) / (3E-9* fract ^3) E_H_init <-0 V_init <-3E-9 stage <-0# Life history traits clutchsize <-8# Eggs per clutch stages <-7# Number of stages, 0 = egg, 1-5 = nymphs, 6 = pre egg-laying adult, 7 = post-laying adult S_instar <-rep(s.1, stages) # Stress at instar n: L_n^2/ L_n-1^2 (-)}\cr} L_b <- L.b # Structural length at birth (cm)}\cr} photostart <-0# no photoperiod effect on reproductionphotofinish <-0# no photoperiod effect on reproduction reset <-0# don't reset lifecycle if death occurs soilnode <-3# depth (soil node) at which eggs laid - 3 = 5 cm# Thermoregulatory parameters Ww_g <-0.2# g, body mass shape <-1# cylinder shape_b <-10# ratio of long to short axis of cylinder T_pref <-38# preferred body temperature species B T_F_min <-13# degrees C, lower body temperature for foraging T_F_max <-40# degrees C, upper body temperature for foraging T_B_min <-13# degrees C, minimum basking temperature T_RB_min <-13# degrees C, temperature at which animal will move to a basking site CT_max <-47# degrees C, critical thermal maximum species B CT_min <-0# degrees C, critical thermal minimum (used by program to determine depth selected when inactive and burrowing)# Behavioural traits diurn <-1# Diurnal activity allowed (1) or not (0)? nocturn <-1# Nocturnal activity allowed (1) or not (0)? crepus <-1# Crepuscular activity allowed (1) or not (0)? burrow <-0# Shelter in burrow allowed (1) or not (0)? shade_seek <-0# Shade seeking allowed (1) or not (0)? pct_cond <-0# Percentage of animal surface contacting the substrate (\%)``````{r, grasshopperB_box1_2}#| label: grasshopperB_box1_2#| echo: true#| warning: false#| message: false plant_micro <-c("plant2_micro.RDa") plant_mirco_names <-c("stomata_open")# Use the plant microclimate data to reset the original microclimate object. Note that we need to adjust both shade and nonshade conditions# Unshaded microclimate micro$metout <- plant2$metout micro$metout [,"TALOC"] <- plant2$environ[,"TC"] micro$metout[,"TSKYC"] <- plant2$environ[,"TSKY"] micro$metout[,"RHLOC"] <- plant2$environ[,"RELHUM"]# Shaded microclimate microshadmet <- plant2$shadmet micro$shadmet [,"TALOC"] <- plant2$environ [,"TC"]# Soil microclimate micro$soil <- plant2$soil micro$shadsoil <- plant2$shadsoil micro$soilmoist <- plant2$soilmoist micro$shadmoist <- plant2$shadmoist# Get dates from microclimate object dates <- micro$dates # dates, hourlydates2 <- micro$dates2 # dates, daily# Set starting date by truncating microclimate model output to when grasshoppers are epected to oviposit eggs. This is the date when the life cylce of the insect is expected to start.oviposition_date <-'2018-03-31 23:00:00' subs <-which( dates >as.POSIXct(oviposition_date)) subs2 <-which(dates2 >as.POSIXct(oviposition_date))# Now run the DEB life-cyle model for the grasshopper ectoB <-ectotherm( Ww_g = Ww_g,shape = shape,shape_b = shape_b,T_pref = T_pref,T_F_min = T_F_min,T_F_max = T_F_max,T_B_min = T_B_min,T_RB_min = T_RB_min,CT_min = CT_min,CT_max = CT_max,diurn = diurn,nocturn = nocturn,crepus = crepus,shade_seek = shade_seek,burrow = burrow,pct_cond = pct_cond,nyears =ceiling(length(subs2)/365),metout = micro$metout[subs,],soil = micro$soil[subs,], soilpot = micro$soilpot[subs,],soilmoist = micro$soilmoist[subs,], humid = micro$humid[subs,],tcond = micro$tcond[subs,], shadmet = micro$shadmet[subs,],shadsoil = micro$shadsoil[subs,], shadpot = micro$shadpot[subs,],shadmoist = micro$shadmoist[subs,],shadhumid = micro$shadhumid[subs,],shadtcond = micro$shadtcond[subs,], rainfall = micro$RAINFALL[subs2],minshades =rep(unique(micro$minshade), length(subs)),maxshades =rep(unique(micro$maxshade), length(subs)),DEB =1, # turns DEB model onmetab_mode =1, # turns on hemimetabolous model, abpz = z * fract,del_M = del.M,p_Xm = p.Am *100/24/ kap.X,kap_X = kap.X,v = v /24,kap = kap,p_M = p.M /24,E_G = E.G,kap_R = kap.R,k_J = k.J /24,E_Hb = E.Hb * fract ^3,E_Hp = (E.Hj+1e-5) * fract ^3,E_Hj = E.Hj * fract ^3,h_a = h.a / (24^2),s_G = s.G,E_0 = E.0* fract ^4,mu_E = mu.E,d_V = d.V,d_E = d.E,T_REF = T.ref,T_A = T.A,T_AL = T.AL,T_AH = T.AH,T_L = T.L,T_H = T.H,E_sm = E_sm,E_init = E_init,E_H_init = E_H_init,V_init = V_init,stage = stage,clutchsize = clutchsize,stages = stages,S_instar =rep(s.1, stages),L_b = L.b,photostart = photostart,photofinish = photofinish,reset = reset,soilnode = soilnode)# Save the DEB model ectoB$dates <- dates[subs] # Add in dates for claritysave(ectoB, file =here("output", "microclim", paste0("grasshopperB_", plant_mirco_names, "_Box1.RDa")))```Now we can have a look at the body temperature of both species under both microclimate scenarios.```{r}#| label: fig-grasshopper_temp_box1#| eval: true#| echo: true#| code-fold: true## load the two grasshopper model outputsload(here("output", "microclim", "grasshopperspA_stomata_closed_Box1.RDa"))load(here("output", "microclim", "grasshopperB_stomata_open_Box1.RDa"))# Create dataframes for plotting for both species temp_spA_open <-data.frame(ectoA$environ) temp_spA_open$datetime <- ectoA$dates temp_spB_open <-data.frame(ectoB$environ)temp_spB_open$datetime <- ectoB$dates# Merge the two dataframes temp_combined <-rbind( temp_spA_open %>%mutate(Species ="Species A (Stomata Closed)"), temp_spB_open %>%mutate(Species ="Species B (Stomata Open)") )# Now subset to the desired dates temp_combined_sub <- temp_combined %>%filter(datetime >as.POSIXct('2019-01-15') & datetime <as.POSIXct('2019-01-18'))# Plot for the figure in Box 1. Note it was adjusted in Biorender.com for final version.ggplot(temp_combined_sub, aes(x = datetime, y = TA, color = Species)) +geom_line(size =0.8) +labs(x ="Date",y ="Body Temperature (°C)") +theme_classic() +theme( axis.text.x =element_text(angle =45, vjust =1, hjust =1),legend.position ="none", legend.text =element_text(size =14), axis.text =element_text(size =14), axis.title =element_blank()) +scale_colour_manual(values =c("Species A (Stomata Closed)"="#356920", "Species B (Stomata Open)"="#664229"))# ggsave(here("figs", "fig_grasshopper_temp_box1.png"), width = 8, height = 5, dpi = 600)```# **Box 2 Code: Scaling up extreme heat effects from impacts on individuals of species to communities**Now that we have predictions about body temperatures we can incorporate thermal load accumulation and use the DEB models to predict life-cycles of the two grasshopper species. The output from DEB models can then be used to calculate vital rates such as growth, development, reproduction, and survival, which we can use to calculate population growth rates ($r_{max}$). We can then model population dynamics and interactions between the two species.Note that in Box 1, we ran the biophysical model with DEB turned on, but we only extracted body temperature information. So, in essence, we already coupled heat and water-budget models with dynamic energy budget models for grasshoppers. Here, we will extract the DEB model and do a bit more with it. ## Capturing impacts of extreme heat on physiological function across the life-cycleFirst, we'll need some functions to extract life-cycle details from the DEB model output and compute survival probabilities.```{r, grasshopper_lifecycle_functions}#| label: grasshopper_lifecycle_functions#| echo: true#| warning: false#| message: false#| code-fold: true#' @title alive_data: Subset DEB output to period when alive#' @description Extracts DEB model output corresponding only to periods when the individual#' is alive (post-hatch). This is done by subsetting the `debout` component#' of the ectotherm model output (`ecto`) to include only rows where both #' volume (`V`) and stage (`STAGE`) are greater than zero (indicating they are alive).#' @param ecto A list object (from NicheMapR's `ectotherm` function)#' containing components `debout` (developmental output) and `dates`.#' @return A data frame of post-hatch DEB output, including only periods when#' the organism is alive. Adds a `dates` column corresponding to those time points.#' @examples#' alive_data <- get_alive(ecto)#'get_alive <-function(ecto){ debout <-data.frame(ecto$debout)# Then, we can subset to just the post-hatch period debout_post_hatch <- debout[debout$V >0& debout$STAGE >0, ] debout_post_hatch$dates <- ecto$dates[which(debout$V >0& debout$STAGE >0)]return(debout_post_hatch)}#' @title get_environ: Extract environmental data from DEB model output#' @description Extracts and formats the environmental data (`environ`) from a DEB model#' object, appending the corresponding datetime information.#' @param ecto A list object (from NicheMapR's `ectotherm` function) containing components `environ` (heat budget calculations) and `dates`#' @return A data frame of environmental variables with an added `datetime` column.#' @examples#' env_data <- get_environ(ecto)#'get_environ <-function(ecto){ environ <-data.frame(ecto$environ) environ$datetime <- ecto$datesreturn(environ)}#' @title get_environ_alive: Limit environmental data to period when organism is alive#' @description Filters environmental data to match the period when the organism is alive#' (based on `dates` from the DEB output). Useful for synchronizing survival#' and environmental records.#' @param sub_alive A data frame of post-hatch DEB output (from [get_alive()]),#' containing a `dates` column for the alive period.#' @param sub_env A data frame of environmental data (from [get_environ()]),#' containing a `datetime` column.#' @return A data frame of environmental conditions corresponding only to the#' period when the organism is alive.#' @examples#' env_alive <- get_environ_alive(sub_alive = alive_data, sub_env = env_data)#'get_environ_alive <-function(sub_alive, sub_env){ sub_env_alive <-data.frame(sub_env[which(sub_env$datetime %in% sub_alive$dates), ])return(sub_env_alive)}#' @title update_p_surv: Update survival probabilities with additional mortality sources#' @description Adjusts survival probabilities in post-hatch DEB output to include additional#' sources of mortality such as hatch mortality, inactivity mortality, activity-based#' mortality, and thermal load stress mortality.#' @param debout_post_hatch A data frame of post-hatch DEB output (from [get_alive()]),#' containing a column `P_SURV` with baseline survival probabilities.#' @param m_h Numeric scalar; proportional mortality due to hatching (0–1).#' @param m_i Numeric scalar; proportional mortality due to inactivity (0–1).#' @param m_a Numeric scalar; proportional mortality due to activity (0–1).#' @param m_tls Numeric vector; proportional mortality due to thermal load stress (0–1)#' for each time step (must match `nrow(debout_post_hatch)`).#' @return A modified data frame with an additional column `P_SURV2` representing#' updated survival probabilities accounting for all mortality components.#' @examples#' debout_updated <- update_p_surv(debout_post_hatch, m_h = 0.1, m_i = 0.02, m_a = 0.03, m_tls = runif(nrow(debout_post_hatch), 0, 0.1))#'update_p_surv <-function(debout_post_hatch, m_h, m_i, m_a, m_tls){# Update survival at hatch debout_post_hatch$P_SURV2 <- debout_post_hatch$P_SURV * (1- m_h) # Update survival for additional mortality ratesfor(i in2:nrow(debout_post_hatch)){ debout_post_hatch$P_SURV2[i] <- debout_post_hatch$P_SURV2[i-1] * (1- m_a) * (1- m_tls[i]) * (1- m_i)# Given TLS mortality is incremented each time step we have one less row, so add row by adding on the last value of P_SURV2if(i ==nrow(debout_post_hatch)){ debout_post_hatch$P_SURV2[i] <- debout_post_hatch$P_SURV2[i-1] } } debout_post_hatch$P_SURV2 <-round(debout_post_hatch$P_SURV2, 2)return(debout_post_hatch)}#' @title get_lifecycle_vitals: Extract life-cycle vitals from DEB model output#' @description Derives key life-history parameters from DEB model output, including survival,#' fecundity, longevity, generation time, and the population growth rate (`rmax`).#' The function builds a stage-structured matrix and estimates the dominant#' eigenvalue (λ) of the matrix to calculate `rmax`.#' @param alive_debout A data frame of post-hatch DEB output (from [get_alive()]),#' with columns `STAGE`, `E_B`, and updated survival probabilities (`P_SURV2`).#' @param ecto A DEB model object containing at least a `yearout` component with#' a column `FECUNDITY` (representing reproductive output). This is summarised by stage.#' @return A list containing:#' \describe{#' \item{lambda}{Dominant eigenvalue of the life table (finite rate of increase).}#' \item{rmax}{Intrinsic rate of increase (log of λ).}#' \item{longevity}{Longevity in years (based on time steps in `alive_debout`).}#' \item{generation_time}{Age at first reproduction (years).}#' \item{stage_str}{Stage-structured matrix.}#' }#' @examples#' vitals <- get_lifecycle_vitals(alive_debout = alive_data, ecto = ecto)#' vitals$rmax#'get_lifecycle_vitals <-function(alive_debout, ecto, type = STAGE){# Fecundity data life_cycle_open <-data.frame(ecto$yearout)# Calculate longevity and generation time longevity <-nrow(alive_debout) # hours E_B_diff <- alive_debout$E_B[2:longevity] - alive_debout$E_B[1:(longevity -1)] age_first_repro <-which(E_B_diff <0)[1] generation_time <- age_first_repro /24/365# Create stage-specific survival and fecundity stage_str <- alive_debout %>%group_by({{type}}) %>%summarise(P_SURV =mean(P_SURV2),E_B =mean(E_B),fecundity =ifelse(E_B ==0, 0, life_cycle_open$FECUNDITY/life_cycle_open$CLUTCHES)) # Reproductive buffer energy, can see we only have a single reproductive event.# Build the Leslie matrix. First row fecundnity across stages, sub-diagonal survival probabilities build_life_table <-matrix(0, nrow =nrow(stage_str), ncol =nrow(stage_str)) build_life_table[1,] <- stage_str$fecundity# Now we need survival probabilities on the sub-diagonal. Because these are just hypothetical situations and only one life cyle realisation to be sure matrices are correct make sure that all survival is > 0 so that it allows for reproduction to occur at least once.for(i in2:nrow(stage_str)){ build_life_table[i, i -1] <- stage_str$P_SURV[i -1]if(build_life_table[i, i -1] ==0){ build_life_table[i, i -1] <-0.01 } } build_life_table[nrow(build_life_table), ncol(build_life_table)] <- stage_str$P_SURV[nrow(build_life_table)] if(build_life_table[nrow(build_life_table), ncol(build_life_table)] ==0){ build_life_table[nrow(build_life_table), ncol(build_life_table)] <-0.01 }#Estimate rmax and other demographic parameters from eigenvalue approach life_table_eigen <-Re(eigen(build_life_table)$values[1]) rmax_log <-log(life_table_eigen) lambda_dem <- popbio::lambda(build_life_table) # double check gen_time_dem <- popbio::generation.time(build_life_table) net_reproductive_rate_dem <- popbio::net.reproductive.rate(build_life_table)return(list(lambda =round(life_table_eigen, 2),rmax =round(rmax_log, 2),longevity = longevity /24/365, # daysnet_reproductive_rate_dem =round(net_reproductive_rate_dem, 2),gen_time_dem =round(gen_time_dem, 2),generation_time = generation_time,matrix = build_life_table))}#' @title damage_accum#' @description Calculates a cumulative percentage increase through time,#' where each interval’s increment depends on the temperature.#' @param time Numeric vector of time points (e.g., minutes).#' @param temp Numeric vector of temperatures (°C), same length as `time`.#' @param intercept Numeric scalar controlling the denominator (base-10 intercept).#' @param slope Numeric scalar controlling how temperature affects the rate.#' @param threshold Numeric scalar between 0 and 1 indicating the damage threshold.For example, if LT50 is used as the threshold, set `threshold = 0.5`. Used to convert heat injury to mortality probabilities#'#' @return A numeric vector of cumulative percentages of mortality (0–100).damage_accum <-function(time, temp, intercept, slope, threshold =0.5) { output =data.frame(damage_i =NA, cum_damage =NA, mortality_prob =NA)# Calculate the damage in a given incrementfor (i in1:(length(time) -1)) {# Calculate the accumulated damage increment for each time step acc = (100* (time[i +1] - time[i])) / (10^(slope *max(temp[i +1], temp[i]) + intercept)) output[i, "damage_i"] = acc }# Sum the increments cumulatively to get accumulated heat damage through time output$cum_damage[1] <- output$damage_i[1]for (j in2:nrow(output)) { output$cum_damage[j] <- output$cum_damage[j -1] + output$damage_i[j] }# Convert HI to survival probability at each time step given a threshold value used (50%, or 80% usually) output$mortality_prob = (output$cum_damage * threshold) /100# Now check that mortality cum damage and probabilities do not exceed 1 (100%) output$mortality_prob[output$mortality_prob >1] <-1 output$cum_damage[output$cum_damage >100] <-100 output$temp <- temp[1:(length(time) -1)] output$time_min <- time[1:(length(time) -1)]return(output)}# par(mfrow = c(3, 1))# plot(temp ~ time, data = damage, type = "l", xlim = c(109000, 120780))# plot(cum_damage_repair ~ time, data = damage, type = "l", xlim = c(109000, 120780))# plot(mortality_prob ~ time, data = damage, type = "l", xlim = c(109000, 120780))#' @title damage_accum#' @description Calculates cumulative heat damage through time, where each#' interval’s increment depends on temperature, and converts this to#' mortality probability via an exponential hazard model. The threshold#' is defined such that when cumulative damage reaches 100 * threshold,#' mortality is 50% (e.g., threshold = 0.5 for LT50).#'#' @param time Numeric vector of time points (e.g., minutes).#' @param temp Numeric vector of temperatures (°C), same length as `time`.#' @param intercept Numeric scalar controlling the denominator (base-10 intercept).#' @param slope Numeric scalar controlling how temperature affects the rate.#' @param threshold Numeric scalar between 0 and 1 indicating the damage#' threshold. For example, if LT50 is used as the threshold, set#' `threshold = 0.5`. When cum_damage reaches 100 * threshold, mortality = 0.5.#'#' @return A data.frame with columns:#' - damage_i: damage increment per interval (in arbitrary "% damage" units)#' - cum_damage: cumulative damage (same units as damage_i)#' - hazard: cumulative hazard H#' - mortality_prob: mortality probability (0–1)#' - temp: interval temperature (°C)#' - time_min: interval end time (minutes)damage_accum <-function(time, temp, intercept, slope, threshold =0.5) {if (length(time) !=length(temp)) {stop("'time' and 'temp' must have the same length.") }if (length(time) <2L) {stop("Need at least two time points to compute damage increments.") }# interval lengths dt <-diff(time)# use max temp over each interval temp_step <-pmax(temp[-1], temp[-length(temp)])# damage increment per interval (arbitrary "% damage" units) damage_i <- (100* dt) / (10^(slope * temp_step + intercept))# cumulative damage (same units as damage_i) cum_damage <-cumsum(damage_i)# map to hazard so that cum_damage = 100 * threshold -> mortality = 0.5 D50 <-100* threshold # "damage" at LT50 haz <-log(2) * (cum_damage / D50) # cumulative hazard surv <-exp(-haz) mort <-1- surv# numerical safety mort[mort <0] <-0 mort[mort >1] <-1 out <-data.frame(damage_i = damage_i,cum_damage = cum_damage,hazard = haz,mortality_prob = mort,temp = temp_step,time_min = time[-1] )return(out)}#' @title damage_repair_accum#' @description Calculates cumulative heat damage through time with#' temperature-dependent repair (Sharpe–Schoolfield form), then converts#' this to mortality probability via an exponential hazard model.#' The threshold is defined such that when cumulative damage reaches#' 100 * threshold, mortality is 50% (e.g., threshold = 0.5 for LT50).#'#' @param time Numeric vector of time points (e.g., minutes).#' @param temp Numeric vector of temperatures (°C), same length as `time`.#' @param intercept Numeric scalar controlling the denominator (base-10 intercept)#' for damage accumulation.#' @param slope Numeric scalar controlling how temperature affects the rate#' of damage accumulation.#' @param threshold Numeric scalar between 0 and 1 indicating the damage#' threshold (e.g., 0.5 for LT50). When cum_damage_repair reaches#' 100 * threshold, mortality = 0.5.#' @param TA Arrhenius activation parameter for repair (Kelvin).#' @param TAL Low-temperature inactivation parameter for repair (Kelvin).#' @param TAH High-temperature inactivation parameter for repair (Kelvin).#' @param TL Low-temperature inactivation midpoint (Kelvin).#' @param TH High-temperature inactivation midpoint (Kelvin).#' @param TREF Reference temperature for repair rate (Kelvin).#' @param kdot_ref Reference repair rate at `TREF` (per minute).#'#' @return A data.frame with columns:#' - damage_i: damage increment per interval (no repair)#' - repair_inc: repair amount per interval#' - net_damage_inc: net damage increment after repair#' - cum_damage_repair: cumulative net damage through time#' - hazard: cumulative hazard H#' - mortality_prob: mortality probability (0–1)#' - temp: interval temperature (°C)#' - time_min: interval end time (minutes)damage_repair_accum <-function(time, temp, intercept, slope,threshold =0.5, TA, TAL, TAH, TL, TH, TREF, kdot_ref) {if (!is.numeric(temp)) stop("'temp' must be numeric (°C).")if (length(time) !=length(temp)) {stop("'time' and 'temp' must have the same length.") }if (length(time) <2L) {stop("Need at least two time points to compute damage increments.") }# check scalar parametersfor (nm inc("TA", "TAL", "TAH", "TL", "TH", "TREF", "kdot_ref")) { val <-get(nm, inherits =FALSE)if (!is.numeric(val) ||length(val) !=1L) {stop(sprintf("'%s' must be a numeric scalar.", nm)) } }# Kelvin temps for repair TK <- temp +273.15# Step 1: baseline damage increments (without repair) damage <-damage_accum(time = time,temp = temp,intercept = intercept,slope = slope,threshold = threshold ) n <-nrow(damage)# prepare columns for repair; overwrite hazard/mortality later damage$repair_inc <-NA_real_ damage$net_damage_inc <-NA_real_ damage$cum_damage_repair <-NA_real_# interval lengths and Kelvin temps per interval dt <-diff(time) # minutes TK_step <-pmax(TK[-1], TK[-length(TK)]) # K, max over each interval# Step 2: compute repair and net damage per intervalfor (i inseq_len(n)) { T_K <- TK_step[i] dt_i <- dt[i]# Sharpe–Schoolfield repair rate (per minute) num <- kdot_ref *exp(TA * (1/ TREF -1/ T_K)) den <-1+exp(TAL * (1/ T_K -1/ TL)) +exp(TAH * (1/ TH -1/ T_K)) repair_rate <- num / den# amount repaired during this interval (same units as damage_i) repair_inc <- repair_rate * dt_i# net damage increment after repair, truncated at zero damage_inc <- damage$damage_i[i] net_inc <-max(damage_inc - repair_inc, 0) damage$repair_inc[i] <- repair_inc damage$net_damage_inc[i] <- net_inc# cumulative net damageif (i ==1L) { damage$cum_damage_repair[i] <- net_inc } else { damage$cum_damage_repair[i] <- damage$cum_damage_repair[i -1L] + net_inc } }# Step 3: map cumulative net damage to hazard and mortality D50 <-100* threshold haz <-log(2) * (damage$cum_damage_repair / D50) surv <-exp(-haz) mort <-1- surv mort[mort <0] <-0 mort[mort >1] <-1 damage$hazard <- haz damage$mortality_prob <- mort# ensure temp/time columns correspond to intervals damage$temp <-pmax(temp[-1], temp[-length(temp)]) damage$time_min <- time[-1]return(damage)}#' @title damage_repair_accum_state#' @description Calculates cumulative heat damage through time with#' temperature-dependent repair (Sharpe–Schoolfield form), tracking#' the damage state over time and computing instantaneous hazard and#' mortality probability at each time step.#' The threshold is defined such that when cumulative damage reaches#' 100 * threshold, mortality is 50% (e.g., threshold = 0.5 for LT50).#' @param time Numeric vector of time points (e.g., minutes).#' @param temp Numeric vector of temperatures (°C), same length as `time`.#' @param intercept Numeric scalar controlling the denominator (base-10 intercept)#' for damage accumulation.#' @param slope Numeric scalar controlling how temperature affects the rate#' of damage accumulation.#' @param threshold Numeric scalar between 0 and 1 indicating the damage#' threshold (e.g., 0.5 for LT50). When cum_damage_repair reaches#' 100 * threshold, mortality = 0.5.#' @param TA Arrhenius activation parameter for repair (Kelvin).#' @param TAL Low-temperature inactivation parameter for repair (Kelvin).#' @param TAH High-temperature inactivation parameter for repair (Kelvin).#' @param TL Low-temperature inactivation midpoint (Kelvin). #' @param TH High-temperature inactivation midpoint (Kelvin).#' @param TREF Reference temperature for repair rate (Kelvin).#' @param kdot_ref Reference repair rate at `TREF` (per minute).#' @return A data.frame with columns:#' - damage_i: damage increment per interval (no repair)#' - repair_inc: repair amount per interval #' - net_damage_inc: net damage increment after repair#' - cum_damage_repair: cumulative net damage through time#' - hazard: instantaneous hazard h from current damage state#' - mortality_prob: mortality probability (0–1)#' - temp: interval temperature (°C)#' - time_min: interval end time (minutes)#' damage_repair_accum_state <-function(time, temp, intercept, slope,threshold =0.5, TA, TAL, TAH, TL, TH, TREF, kdot_ref) {if (!is.numeric(temp)) stop("'temp' must be numeric (°C).")if (length(time) !=length(temp)) {stop("'time' and 'temp' must have the same length.") }if (length(time) <2L) {stop("Need at least two time points to compute damage increments.") }# Kelvin temps TK <- temp +273.15# interval lengths and step temperatures (max over interval) dt <-diff(time) # minutes temp_st <-pmax(temp[-1], temp[-length(temp)]) TK_st <-pmax(TK[-1], TK[-length(TK)]) n <-length(dt)# damage rate per interval (without repair), "% damage per minute"# from your original damage_accum:# damage_i = (100 * dt) / 10^(slope * T + intercept)# so: damage_rate <-100/ (10^(slope * temp_st + intercept))# Sharpe–Schoolfield repair rate per interval (per minute) repair_rate <-vapply(seq_len(n), function(i) { T_K <- TK_st[i] num <- kdot_ref *exp(TA * (1/ TREF -1/ T_K)) den <-1+exp(TAL * (1/ T_K -1/ TL)) +exp(TAH * (1/ TH -1/ T_K)) num / den }, numeric(1))# Raw increments for bookkeeping damage_i <- damage_rate * dt repair_inc <- repair_rate * dt# State variables net_damage_inc <-numeric(n) # can be negative cum_damage_state <-numeric(n) # damage STATE with repair hazard_inst <-numeric(n) # instantaneous hazard from state mortality_prob <-numeric(n) D50 <-100* threshold D_prev <-0for (i inseq_len(n)) {# net increment in damage state this interval net_inc <- (damage_rate[i] - repair_rate[i]) * dt[i] # can be < 0 D_new <-max(D_prev + net_inc, 0) # repair can undo damage# instantaneous hazard given CURRENT damage state h_i <-log(2) * (D_new / D50)# interpret this as "mortality if tested now" via your mapping mort_i <-1-exp(-h_i) net_damage_inc[i] <- net_inc cum_damage_state[i] <- D_new hazard_inst[i] <- h_i mortality_prob[i] <- mort_i D_prev <- D_new } out <-data.frame(damage_i = damage_i,repair_inc = repair_inc,net_damage_inc = net_damage_inc,cum_damage_repair = cum_damage_state, # current damage statehazard = hazard_inst, # from state, not cumulativemortality_prob = mortality_prob, # can go up or downtemp = temp_st,time_min = time[-1] )return(out)}``````{r, grasshopper_lifecycle_box2_setup}#| label: grasshopper_lifecycle_box2_setup#| echo: true#| warning: false#| message: false# Load each grasshopper DEB model and extract life-cycle detailsload(here("output", "microclim", "grasshopperspA_stomata_closed_Box1.RDa")) #ectoAload(here("output", "microclim", "grasshopperB_stomata_open_Box1.RDa")) #ectoB# Define Common Parameters Applied to Both Species# Baseline mortality rates to add to DEB. TLS and differences in hatch mortality will be added to these baseline rates differently for each species m_i <-0# mortality per hour inactive m_a <-1e-4# mortality per hour active## ------------------## # Species A## ------------------## # First, find out dates and times when animal is alive. We need these to extract the body temperatures AND the relevant aspects of the lifecyle# Get the DEB output while alive debout_post_hatch_A <-get_alive(ectoA)# Get the environ data while alive, which we use for TLS as we need the body temperature every hour environ_A <-get_environ(ectoA) environ_A_alive <-get_environ_alive(debout_post_hatch_A, environ_A) environ_A_alive <- environ_A_alive %>%mutate(time_min =c(0:(n()-1))*60) # time in minutes# Thermal Load sensitivity parameters for species A. Note that CTmax and z are values derived from a regression between temperture (yaxis) and log time to 50% mortality (xaxis). See Arnold et al. 2025 for more details. We need to invert these values to get the intercept and slope for the damage accumulation function. ctmaxA <-50# CTmax1h, 50 zA <--1.8# z (slope), negative indicates decrease in survival with time which it must do as animals don't get better the longer they are at stressful temps - try -0.9alphaA <--ctmaxA/zA # Need these for the log time accumulation function betaA <-1/ zA # Need these for the log time accumulation function# Define temperatures and times tbA <- environ_A_alive$TC-3# body temps, Celsius timeA <- environ_A_alive$time_min # time in minutes# Using body temperatures through time, estimate the accumulation of physiological damage towards a threshold of 50% mortality under no repair. Note that threshold (50% = 0.5) is used to scale % HI to % mortalitydamageA <-damage_accum(time = timeA, temp = tbA, intercept = alphaA, slope = betaA)# Set Arrhenius model and repair rate parameters TA <-14065# Arrhenius temperature, KTL <-10.5+273.15# Arrhenius temperature lower threshold, KTH <-28.5+273.15# Arrhenius temperature upper threshold, KTAL <-50000# Arrhenius temperature lower, KTAH <-100000# Arrhenius temperature upper, KTREF <-28+273.15# reference temperature, Kkdot_ref <-0.005# repair rate % per minute at ref temp 28 deg CdamageA_repair <-damage_repair_accum_state(time = timeA, temp = tbA, intercept = alphaA, slope = betaA,TA = TA, TAL = TAL, TAH = TAH,TL = TL, TH = TH, TREF = TREF, kdot_ref = kdot_ref)#plot(damageA_repair$time_min, damageA_repair$mortality_prob, type = "l")# Now that we have the probability of mortality from thermal load stress, we can update the survival probabilities in the DEB model output. Note that the DEB model already has some baseline mortality rates from the Gompertz ageing model, so we are adding additional sources of mortality.m_h <-0.05# mortality at hatchdebout_post_hatch_A <-update_p_surv(debout_post_hatch_A, m_h = m_h, # mortality at hatchm_a = m_a, # mortality for active hourm_i = m_i, # mortality for inactive hourm_tls = damageA_repair$mortality_prob) # Heat load mortality# Now we can extract life-cycle details. Our life cycle model assumes all animals progress to the next stage that survive. Given we have multiple life stages we create a stage-structured matrix (or could be an age-structuted model – for example, based on 'day') to capture survival and fecundity at each stage, and quantify the probability of transitioning through each stage. From this, we can estimate population growth rate (rmax) and other life-cycle details. This could be changed to an age structure here by setting type = DAY and should be more comparable across species with different development times.lifecycle_A <-get_lifecycle_vitals(debout_post_hatch_A, ectoA, type = STAGE) ## ------------------## # Species B## ------------------## debout_post_hatch_B <-get_alive(ectoB) environ_B <-get_environ(ectoB) environ_B_alive <-get_environ_alive(debout_post_hatch_B, environ_B) %>%mutate(time_min =c(0:(n()-1))*60) # time in minutes# Thermal Load sensitivity parameters for species A. Note that CTmax and z are values derived from a regression between temperture (yaxis) and log time to 50% mortality (xaxis). See Arnold et al. 2025 for more details. We need to invert these values to get the intercept and slope for the damage accumulation function. ctmaxB <-47# CTmax1h zB <--1# z (slope), negative indicates decrease in survival with timealphaB <--ctmaxB/zB # Need these for the log time accumulation function betaB <-1/ zB # Need these for the log time accumulation function# Define temperatures and times tbB <- environ_B_alive$TC-3# body temps, Celsius timeB <- environ_B_alive$time_min # time in minutes# Using body temperatures through time, estimate the accumulation of physiological damage towards a threshold of 50% mortality under no repairdamageB <-damage_accum(time = timeB, temp = tbB, intercept = alphaB, slope = betaB)damageB_repair <-damage_repair_accum_state(time = timeB, temp = tbB, intercept = alphaB, slope = betaB,TA = TA, TAL = TAL, TAH = TAH,TL = TL, TH = TH, TREF = TREF, kdot_ref = kdot_ref)# plot(damageB_repair$time_min, damageB_repair$mortality_prob, type = "l")# Now that we have the probability of mortality from thermal load stress, we can update the survival probabilities in the DEB model output m_h <-0.05# mortality at hatchdebout_post_hatch_B <-update_p_surv(debout_post_hatch_B, m_h = m_h, # mortality at hatchm_a = m_a, # mortality for active hourm_i = m_i, # mortality for inactive hourm_tls = damageB_repair$mortality_prob) # Heat load mortality# Now we can extract life-cycle detailslifecycle_B <-get_lifecycle_vitals(debout_post_hatch_B, ectoB)### Create a TLS figure for box 2# Species Amultiplier <-50# to scale the two y axesp1_tlA <-ggplot() +annotate("rect",xmin =1800*60, xmax =2500*60,ymin =-Inf, ymax =Inf,fill ="red", alpha =0.2) +labs(x ="Time (hours)", y ="Body Temperature (°C)") +geom_line(aes(x = timeA, y = tbA), color ="#356920", size =0.5) +ylim(0, 50) +geom_line(aes(x = timeA, y =c(0, damageA_repair$mortality_prob) * multiplier), color ="red") +scale_y_continuous(name ="Probability of Mortality (Thermal Load Stress)", sec.axis =sec_axis(~ . / multiplier, name ="Body temperature (°C)")) +theme_classic() +theme( axis.text.x =element_blank(), legend.position ="none", legend.text =element_text(size =14), axis.text =element_text(size =14), axis.title =element_blank()) # Species Bp2_tlB <-ggplot() +annotate("rect",xmin =1800*60, xmax =2500*60,ymin =-Inf, ymax =Inf,fill ="red", alpha =0.2) +labs(x ="Time (hours)", y ="Body Temperature (°C)") +geom_line(aes(x = timeB, y = tbB), color ="#664229", size =0.5) +ylim(0, 50) +geom_line(aes(x = timeB, y =c(0, damageB_repair$mortality_prob) * multiplier), color ="red") +scale_y_continuous(name ="Probability of Mortality (Thermal Load Stress)", sec.axis =sec_axis(~ . / multiplier, name ="Body temperature (°C)")) +theme_classic() +theme( axis.text.x =element_blank(), legend.position ="none", legend.text =element_text(size =14), axis.text =element_text(size =14), axis.title =element_blank()) # Combine plotsggsave(p1_tlA | p2_tlB +plot_layout(ncol =1), width =11.876543, height =4.839506, dpi =600, filename =here("figs", "fig_grasshopper_TLS_box2.png"))# Note this is a trimed down version for the plot for the figure in main MS. It was edited in BioRender to add axis labels and panel labels.p1_tlA | p2_tlB +plot_layout(ncol =1)```## Modelling population dynamics of the two grasshopper species including density dependence and interactionsWe now have key vital rate information for each species that incorperates physiologically explicit processes governing growth, survival and reproduction. We can now use this information to model population dynamics of the two species including density dependence and interspecific interactions.One way (of many) we could model the population dynamics of the two species is to use a discrete-time Ricker model that incorporates both intra and interspecific competition. The general form of the model is as follows:$$N_{t+1}^i = N_t^i \cdot \lambda_i^{\left(1 - \frac{N_t^i + \sum_{j \neq i} \alpha_{ij} N_t^j}{K_i}\right)}$$ {#eq-general}where $N_t^i$ is the population size of species $i$ at time $t$, $\lambda_i$ is the intrinsic growth rate of species $i$, $K_i$ is the carrying capacity of species $i$, and $\alpha_{ij}$ is the competition coefficient that describes how much species $j$ affects species $i$. When $\alpha_{ij}$ = 1, species $j$ competes just as strongly as species $i$ does with itself. When $\alpha_{ij}$ < 1, then species $j$ has a weaker effect on species $i$ then conspecifics and $\alpha_{ij}$ > 1, indicates that species $j$ has a stronger effect on species $i$ than conspeecifics. The sum runs over all species $j$ that are not equal to $i$, which means that the model can be extended to include any number of species within a community.As with any model there are assumptions that need to be considered. In this case:1. it again assumes discrete non-overlapping generations. 2. it assumes $\lambda$, $K$ and now $\alpha_{ij}$ are constant over time, ignoring any effects of environmental stochasticity, changing interaction strengths between species or evolution. Again, these assumptions can be relaxed. For example, $\alpha_{ij}$ can of course vary depending on the environment and could be viewed as a function in of itself. As such, we could, for example, couple our mircoclimate and biophysical model predictions to estimate how $\alpha_{ij}$ varies as a function of temperature, water or even resource availability based on experimental data describing how $\alpha_{ij}$ varies as across different environments.3. it assumes that population growth is limited by the total effective density based on both intra and interspecific effects. It also assumes these act additively on the log scale. 4. it assumes a closed system with no emmigration or immigration. For now, we will assume that the population is closed and that $K$ is constant over time. This model allows for asymmetric interaction (i.e., $\alpha_{ij} \neq \alpha_{ji}$), and so, we'll give each species its own competition coefficient. We will also assume that $\lambda$ varies slightly over time and add this to our model to demonstrate how you can relax assumptions of $\lambda$, $K$ and $\alpha_{ij}$ being constant over time.We'll need some functions to run the population dynamics model.```{r, ricker_two_species_functions_box2}#| label: ricker_two_species_functions_box2#| echo: true#| warning: false#| message: false#| code-fold: false#' @title Ricker two species function#' @description A simple Ricker model function to simulate population dynamics for multiple species#' @param N0 Initial population size of species i#' @param r_i Intrinsic growth rate for species i#' @param sd_r_i Standard deviation of the growth rate for species i#' @param K_i Carrying capacity for species i#' @param alpha Competition coefficient between species #' @param N0_j Initial population size of species j#' @return Next years population sizericker_two <-function(N0_i, r_i, sd_r_i, K_i, N0_j, alpha){ N0_i *exp(rnorm(1, r_i, sd_r_i)*(1- ((N0_i + (alpha * N0_j))/K_i)))}## Create a function to do a stochastic simulation of the two species Ricker model#' @title Two species ricker model simulation function#' @description A simple two species Ricker model function to simulate population dynamics for two species#' @param N0_i Initial population size of species i#' @param r_i Intrinsic growth rate for species i#' @param sd_r_i Standard deviation of the growth rate for species i#' @param K_i Carrying capacity for species i#' @param N0_j Initial population size of species j#' @param r_j Intrinsic growth rate for species j#' @param sd_r_j Standard deviation of the growth rate for species j#' @param K_j Carrying capacity for species j#' @param alpha_ij Competition coefficient for species j on species i#' @param alpha_ji Competition coefficient for species i on species j#' @param time Time step#' @param nsims Number of replicates#' @return A matrix of population sizes at each time step for nsimssim_ricker_two_sp <-function(N0_i, r_i, sd_r_i, K_i, N0_j, r_j, sd_r_j, K_j, alpha_ij, alpha_ji, time, nsims){# Create a vector to store the population size at each time step N_array_i <-array(NA, dim =c(time, nsims)) N_array_j <-array(NA, dim =c(time, nsims))for(j in1:nsims){# Create arrays to store population sizes and initilise with initial population sizes N_array_i[1,j] <- N0_i N_array_j[1,j] <- N0_jfor(i in2:time){# Calculate the population size in the next time step for species i N_array_i[i,j] <-ricker_two(N_array_i[i-1, j], r_i, sd_r_i, K_i, N_array_j[i-1, j], alpha_ji)# Calculate the population size in the next time step for species j N_array_j[i,j] <-ricker_two(N_array_j[i-1, j], r_j, sd_r_j, K_j, N_array_i[i-1, j], alpha_ij) } }# Return the population size at each time step for species i and jreturn(list(N_i = N_array_i, N_j = N_array_j))}``````{r, ricker_simulation_twosp_box2}#| label: ricker_simulation_twosp_box2#| echo: true#| message: false#| warning: falseset.seed(123) # Set seed for reproducibility# Set up the parameters for the Ricker model N0_A <-1000# Initial population size, species A N0_B <-500# Initial population size, species B sd_r_A <-0.15# Standard deviation of the growth rate species A sd_r_B <-0.15# Standard deviation of the growth rate species B K_A <-10000# Carrying capacity species A K_B <-10000# Carrying capacity species Br_A <- lifecycle_A$rmax # Intrinsic growth rate species A and B open watering scenerio r_B <- lifecycle_B$rmax # Intrinsic growth rate species A and B closed watering scenerio alpha_AB <-0.8# Competition coefficient. Assumed not to vary with environment alpha_BA <-1.2# Competition coefficient. Assumed not to vary with environment time <-5# Time steps nsims <-1000# Number of simulations# Lets run the simulationsN_heat <-sim_ricker_two_sp( N0_i = N0_A, r_i = r_A, sd_r_i = sd_r_A, K_i = K_A, N0_j = N0_B, r_j = r_B, sd_r_j = sd_r_B, K_j = K_B, alpha_ij = alpha_AB, alpha_ji = alpha_BA, time, nsims)# Create a data frame to store the results for each species for each sceneriosims_pop <- plyr::ldply(lapply(N_heat, function(x){ pivot_longer(as.data.frame(x), cols =everything(), names_to ="sim", values_to ="N") %>%mutate(time =rep(1:time, each = nsims))}))```Code to plot the results and export part of the box figure below:```{r, fig-ricker_twosp_box2}#| label: fig-ricker_twosp_box2#| echo: true#| code-fold: true#| message: false#| warning: false# Create a summary of the simulations to make plotting easier sims_heat <- sims_pop %>%group_by(.id, time) %>%summarise(N_m =mean(N), sd =sd(N)) # Plot the resultsp1 =ggplot(sims_heat, aes(x = time, y = N_m, colour = .id, fill = .id)) +geom_ribbon(aes(ymin = N_m - sd, ymax = N_m + sd), alpha =0.3, colour =NA) +geom_line() +labs(x ="Generation (time)", y ="Population size (N)") +theme_classic() +scale_colour_manual(values =c("#356920", "#664229")) +scale_fill_manual(values =c("#356920", "#664229")) +theme(legend.position ="none", axis.text =element_text(size =14), axis.title =element_blank())# Save the plot# ggsave(here("figs", "fig-ricker_twosp_box2.png"), p1, width = 6, height = 5, dpi = 600)```More complex models that incorporate a multitude of species and their interactions can be built using the same principles [e.g., @Bimler2018; @Bimler2023; @Bimler2024]. Information on interaction strength between species can be used to understand how species coexistence is expected to change under extreme heat scenarios.# **Code for "Hot Solutions for a changing world": Using a systems modelling approach to help develop solutions to extreme heat events**A systems modelling approach can also be used to design possible solutions by understanding better how 'thermal interventions' affect heat experienced by organisms and how it influences growth, survival and reproduction. For example, we can manipulate rainfall at a particular time to simulate artificial watering which might be a possible intervention used during extreme heat. The added water can be used to make calculations of heat exchange, temperatures and transpiration under extreme heat events. We can also optimise the amount of water to balance costs and benefits of watering.### **Simulating the effect of watering on plant and insect body temperatures** {#case5}#### Setting the sceneLet's assume our plant species is of interest for conservation or agricultural reasons and we want to understand how watering during a heatwave might help it survive (@fig-case5). The challenge with watering is that often we are drought stressed in combination with the extreme heat [@Dai2013]. As such, it's ideal to reduce water use because this saves money and water. So, the questions become:1. **How much water do we need to protect the plant from extreme heat while also not overwatering?**2. **In addition, how does this affect pest insects that might be residing on the plant?**```{r, fig-case5}#| label: fig-case5#| echo: false#| fig-cap: "Evaluating the impacts of 'thermal interventions' on temperatures experienced by plants and animals. Under extreme heat we might add water to the system to help keep plants cool, but such intervention may also have unexpected consequences on insects. Using a systems approach we can evaluate the impact of watering on plant and insect body temperatures. This can help us understand how to best manage the system to reduce the impacts of extreme heat on plants and insects."fig_case2 <-image_read(here("figs", "figS2.png"))fig_case2```Below, we load in our original microclimate object. We can now use this to simulate the effect of watering on the microclimate. First, lets just look at the rainfall that occurred over the period of interest (@fig-water). The rainfall data is stored in the `microclimate` object and is provided as a daily value (rather than hourly as for temperature). ```{r, fig-water}#| label: fig-water#| fig-cap: "Rainfall events over time for A) over years and B) only during the heat wave event from 2019-01-15 to 2019-01-31."#| code-fold: true#| echo: true# Lets reload our microclimate object so we can use it againload(here("output", "microclim", "microclimate.RDa"))# Get the dates from the microclimate object. We'll take it here because heat-budget objects are not saved with the dates dates <- micro$dates# Lets plot out the actual rainfall data for the location. This is the rainfall data that was used in the microclimate model. rainfall_actual <-data.frame(micro$RAINFALL) rainfall_actual$dates <-as.POSIXct(micro$dates2)# Plot the rainfall data each day through time p1 <-ggplot(rainfall_actual) +geom_line(aes(y = micro.RAINFALL, x = dates), col ="blue") +geom_hline(yintercept =10, colour ="red", lty =2) +labs(y ="Rainfall (mm)", x ="Date") +theme_classic() +theme( axis.text.x =element_text(angle =45, vjust =1, hjust =1)) +scale_colour_viridis_d()# Now a plot of ran over the heat wave even just to zoom in p2 <- rainfall_actual %>%filter(dates >"2019-01-15"& dates <"2019-01-31") %>%ggplot() +geom_line(aes(y = micro.RAINFALL, x = dates), col ="blue") +geom_hline(yintercept =10, colour ="red", lty =2) +labs(y ="Rainfall (mm)", x ="Date") +theme_classic() +theme( axis.text.x =element_text(angle =45, vjust =1, hjust =1)) +scale_colour_viridis_d()# Combine the two plotsp1 + p2 +plot_layout(ncol =1) +plot_annotation(tag_levels ="A") +theme(plot.tag =element_text(size =12, face ="bold"))```It's clear that we had very little water during the heatwave event. If this was an agriculturally important plant species, we might than as the question "how would the temperature of the plants change if we added water to the system?". Remember, stomatal closure during the heat wave event meant that the leaf temperatures were much higher than the air temperature. Spraying water will cool the plants through evaportive cooling, but by how much can we expect it to do that? Well, we can use the microclimate model to simulate this. Lets set up our scenerios. ```{r, watering}#| label: watering#| echo: true#| warning: false#| message: false# Lets assume we are planning on adding 10mm or 20 mm of water to the system to see how much it makes a difference on plant leaf temperaures. rainfall_2 <-2# mm water to add rainfall_5 <-5# mm water to add rainfall_10 <-10# mm water to add rainfall_20 <-20# mm water to add# Specify the dates we are going to add water. date_waters <-as.character(seq(as.POSIXct("2019-01-01"), as.POSIXct("2019-01-31"), by ="days"))# Lets create a new vector of rainfall and then add it to the microclimate object. This is done by creating a new vector of rainfall and then adding it to the microclimate object. RAINFALL <- micro$RAINFALL # extract rainfall from previous sim RAINFALL2 <- RAINFALL # copy rainfall into a new vector RAINFALL2[as.character(micro$dates2) %in% date_waters] <- rainfall_2 # add 2mm of water RAINFALL3 <- RAINFALL # copy rainfall into a new vector RAINFALL3[as.character(micro$dates2) %in% date_waters] <- rainfall_5 # add 5mm of water RAINFALL4 <- RAINFALL # copy rainfall into a new vector RAINFALL4[as.character(micro$dates2) %in% date_waters] <- rainfall_10 # add 5mm of water RAINFALL5 <- RAINFALL # copy rainfall into a new vector RAINFALL5[as.character(micro$dates2) %in% date_waters] <- rainfall_20 # add 5mm of water```Now we have created two new rainfall vectors simulating different amounts of water added to the system during the heatwave event. Lets have a look at the new rainfall data.```{r, fig-water2}#| label: fig-water2#| fig-cap: "Rainfall events over time with two watering scenarios (10 mm and 20mm of water applied) during a heat wave event 2019-01-15 to 2019-01-31 (red dashed lines)."#| code-fold: true#| echo: true # Add in rainfall manipulations to columnsrainfall_actual$rainfall_2 <- RAINFALL2rainfall_actual$rainfall_5 <- RAINFALL3rainfall_actual$rainfall_10 <- RAINFALL4rainfall_actual$rainfall_20 <- RAINFALL5# Re-arrange data for plottingrainfall_actual2 <- rainfall_actual %>%select(dates, micro.RAINFALL, rainfall_2, rainfall_5, rainfall_10, rainfall_20) %>%pivot_longer(cols =c(micro.RAINFALL, rainfall_2, rainfall_5, rainfall_10, rainfall_20), names_to ="Rainfall", values_to ="Rainfall_mm") %>%mutate(Rainfall =ifelse(Rainfall =="micro.RAINFALL", "Original Rainfall", ifelse(Rainfall =="rainfall_10", "Watering (10 mm)", ifelse(Rainfall =="rainfall_20", "Watering (20 mm)", ifelse(Rainfall =="rainfall_2", "Watering (2 mm)", ifelse(Rainfall =="rainfall_5", "Watering (5 mm)", NA))))),Rainfall =factor(Rainfall, levels =c("Watering (2 mm)", "Watering (5 mm)", "Watering (10 mm)", "Watering (20 mm)", "Original Rainfall")))# Plot the rainfall data each day through time water <-ggplot(rainfall_actual2) +geom_vline(xintercept =as.POSIXct("2019-01-01"), linetype ="dashed", color ="red") +geom_vline(xintercept =as.POSIXct("2019-01-31"), linetype ="dashed", color ="red") +geom_line(aes(y = Rainfall_mm, x = dates, col = Rainfall), show.legend =TRUE) +labs(y ="Rainfall (mm)", x ="Date") +theme_classic() +theme( legend.position ="inside",legend.position.inside =c(0.25,0.8),axis.title =element_text(size =20),axis.text.x =element_blank(),legend.title =element_text(size =20),legend.text =element_text(size =20)) +scale_colour_viridis_d() water``````{r}#| label: fig-water2.2#| echo: false#| warning: false#| message: false#| include: false#ggsave(water, filename = here("figs", "fig-water2.png"), width = 7.419753, height = 5.938272, dpi = 600)```#### Calculating the different mircoclimates using the watering scenerios We can now use the `micro_silo` function to run the microclimate model with the new rainfall data. We simply need to update the rainfall data in the `rainhour` argument. The `rainhourly` argument is set to 1 to indicate that we want to use hourly rainfall data.```{r, water_micoclimate}#| label: water_micoclimate#| echo: true#| eval: false#| warning: false#| message: false# Create hourly values, but water at midnight for one hour (00:00-01:00) (midnight is used to avoid changes too rapid for the integrator to solve). rainhour_2 <-unlist(lapply(RAINFALL2, function(x) c(rep(0,23), x))) rainhour_5 <-unlist(lapply(RAINFALL3, function(x) c(rep(0,23), x)))rainhour_10 <-unlist(lapply(RAINFALL4, function(x) c(rep(0,23), x)))rainhour_20 <-unlist(lapply(RAINFALL5, function(x) c(rep(0,23), x)))rain_list <-list(rainhour_2, rainhour_5, rainhour_10, rainhour_20)names(rain_list) <-c("rainhour_2", "rainhour_5", "rainhour_10", "rainhour_20")# Now lets run the microclimate model with the new rainfall data. We will do this for both 10mm and 20mm of water added to the system.#Set up the location and parameters for the microclimate model again. longlat =c(140.79970255974192, -34.159176277782564) dstart ="01/01/2018"# Start year dfinish ="31/12/2019"# End year minshade =40# Minimum shade, % maxshade =80# Maximum shade, % Usrhyt =0.2# Height above ground, m microclima =1# Use mircoclima PC =-1500# Water potential for stomatal closure SP =10for(i in1:length(rain_list)){# run the microclimate model for different watering regimes and save the output. rain <-micro_silo(loc = longlat, dstart = dstart,dfinish = dfinish, Usrhyt = Usrhyt, microclima = microclima,minshade = minshade,maxshade = maxshade, PC = PC, SP = SP, rainhourly =1, rainhour = rain_list[[i]],email ="daniel.noble@edu.au")# Save the microclimate objectsave(rain, file =here("output", "microclim", paste0(names(rain_list)[i], "_shade.RDa")))}```Now we have the new microclimate objects. The original, 2, 5, 10 and 20mm watering events. We have access to temperatures in the air and soil as well as high shade (80%) and low shade (40%). Let's how feed this into our plant heat-budget model to predict the leaf temperature of the plant.#### Calculating the plant leaf temperature using the watering scenerios```{r, water_plant}#| label: water_plant#| echo: true#| eval: false#| warning: false#| message: false# Now, run the plant heat-budget model using each of the three different microclimate objects. objects <-c("microclimate.RDa", "rainhour_2_shade.RDa", "rainhour_5_shade.RDa", "rainhour_10_shade.RDa", "rainhour_20_shade.RDa") names <-c("micro", rep("rain", 4)) # Name of the microclimate objects on load. Original mirciclimate object named "micro" and the rest "rain" because the object saved at each run of loop is "rain"# Set leaf functional traits live <-0# don't simulate behaviour or respiration leaf <-1# use the stomatal conductance formulation for evaporation Ww_g <-1# wet weight, g shape <-2# 0=plate, 1=cylinder, 2=ellipsoid shape_b <-0.0025# ratio of b axis:a axis for ellipsoid shape_c <-0.1176# ratio of c axis:a axis for ellipsoid g_vs_ab <-0.4# leaf vapour conductance, abaxial (bottom of leaf), mol/m2/s g_vs_ad <-0.0# leaf vapour conductance, adaxial (top of leaf), mol/m2/s g_vs_ab_max <-0.4# leaf vapour conductance, abaxial (bottom of leaf), mol/m2/s g_vs_ad_max <-0.0# leaf vapour conductance, adaxial (top of leaf), mol/m2/s epsilon_sub <-0.95# emissivity of the substrate (-) epsilon_sky <-1# emissivity of the sky (-), accounted for already in 'TSKY' output from microclimate model so make it 1 epsilon_L <-0.97# emissivity of the leaf (-) alpha_L <-0.5# solar absorptivity of the leaf (-) fatosk <-0.5# radiation configuration factor to sky (-) fatosb <-0.5# radiation configuration factor to substrate (-) conv_enhance <-1.4# convective enhancement factor (-) pct_cond <-0# percent of leaf conducting to the ground (%) postur <-3# leaf oriented horizontally, not tracking sun M_1 <-0# make metabolic rate zero# Load in the plant heat budgetload(here("output", "microclim", "plant_micro.RDa"))# Loop through the objects and run the plant heat-budget modelfor(i in1:length(objects)){# Load the microclimate objectload(here("output", "microclim", objects[i])) micro <-get(names[i]) # get the name of the object and set it as micro as ectotherm uses object names micro# First, lets extract the relevant information from the microclimate object to get plant stomatal conductance sorted. PDIFs <- micro$diffuse_frac # use variable diffuse fraction P_a <-get_pressure(micro$elev) # air pressure, Pa P_a <-rep(P_a, nrow(micro$metout)) # create hourly vector dates <- micro$dates # Set up vapour conductance vectors and simulate stomatal closure at night f_open <-rep(1, nrow(micro$metout)) f_open[micro$metout[,"ZEN"] ==90] <-0# close stomata when the sun is set g_vs_abs <- g_vs_ab * f_open g_vs_ads <- g_vs_ad * f_open# Include simulated stomatal conductance from plantecophys UMOLPERJ <-4.57# mu_mol photons / JWETAIR_out <-WETAIR(db = micro$metout[,"TALOC"], rh = micro$metout[,"RHLOC"], bp = P_a) VPD_Pa <- WETAIR_out$esat - WETAIR_out$e photo_out <-Photosyn(Tleaf = plant$environ[,"TC"], # Leaf temperature (C)g1 =5.2, # Parameter of Ball-Berry modelCa =400, # Atmospheric CO2 concentration (ppm)VPD = VPD_Pa/1000, # Vapour pressure deficit (kPa)vpdmin =0.5, # lower threshold on VPDPPFD = micro$metout[,"SOLR"] * UMOLPERJ *1, # mu mol m-2 s-1Patm = P_a/1000# Atmospheric pressure (kPa) )g_vs_abs_pho <- photo_out$GS# Run the heat-budget model for the plant leaf. Note that the microclimate object is used to get the relevant environmental temperatures to make the calculations of leaf temperature plant <-ectotherm( leaf = leaf, live = live,Ww_g = Ww_g, shape = shape, shape_b = shape_b, shape_c = shape_c, g_vs_ab = g_vs_abs_pho, g_vs_ad = g_vs_ads, g_vs_ab_max = g_vs_ab_max, g_vs_ad_max = g_vs_ad_max, epsilon_sub = epsilon_sub, epsilon_sky = epsilon_sky,epsilon = epsilon_L, alpha_max = alpha_L, alpha_min = alpha_L,M_1 = M_1,fatosk = fatosk, fatosb = fatosb, conv_enhance = conv_enhance, pct_cond = pct_cond, postur = postur,preshr = P_a, PDIF = PDIFs)# Save the plant objectsave(plant, file =here("output", "microclim", paste0("plant_", gsub(".RDa", "", objects[i]), "_micro.RDa")))}```#### Impacts of watering scenerios on leaf temperatureWe've now used the heat budget model applying each of the three different microclimate objects to calculate the leaf temperature of the plant. We can now plot this out to see how the watering events affect leaf temperature.```{r}#| label: fig-plant_temp3#| fig-cap: "Leaf temperature of a plant at a location near Renmark, South Australia. Leaf temperature is shown in relation to air temperature at 2 m above ground (and in minimum shade, 40%) between 2019-01-15 and 2019-01-31 for three different watering scenarios (original rainfall, 2, 5, 10 and 20 mm of water added)."#| code-fold: true# Extract the microclimate temperatures. Because this was simulated for a plant the 'body temperature' is the leaf temperature and is stored as 'TC' in the environ objectobjects <-c("plant_microclimate_micro.RDa", "plant_rainhour_2_shade_micro.RDa", "plant_rainhour_5_shade_micro.RDa", "plant_rainhour_10_shade_micro.RDa", "plant_rainhour_20_shade_micro.RDa")listnames <-c("envron_plant", "environ_plant_2", "environ_plant_5", "environ_plant_10", "environ_plant_20")environ_plant <-list()for(i in1:length(objects)){# Load the plant objectload(here("output", "microclim", objects[i])) environ_plant[[i]] <-as.data.frame(plant$environ)}names(environ_plant) <- listnames# Now, lets bind these data.frames together and re-organise the data for plotting environ_plant2 <- plyr::ldply(environ_plant) # Extract and stores dates environ_plant2$dates <-rep(dates, 5) # Now create a column that names each scenario environ_plant2 <- environ_plant2 %>%mutate(.id =ifelse(.id =="envron_plant", "Original Rainfall", ifelse(.id =="environ_plant_2", "Watering (2 mm)", ifelse(.id =="environ_plant_5", "Watering (5 mm)", ifelse(.id =="environ_plant_10", "Watering (10 mm)", ifelse(.id =="environ_plant_20", "Watering (20 mm)", NA)))))) %>%mutate(.id =factor(.id, levels =c("Original Rainfall", "Watering (2 mm)", "Watering (5 mm)", "Watering (10 mm)", "Watering (20 mm)")))``````{r, fig_plant_temp_water_main}#| label: fig-fig_plant_temp_water_main#| echo: true#| warning: false#| message: false#| eval: TRUE# Now lets plot the leaf temperature of the plant over time for each of the three different watering scenariosplant_temps_water <- environ_plant2 %>%filter(dates >"2019-01-15"& dates <"2019-01-20") %>%ggplot() +geom_line(aes(y = TC, x = dates, col = .id), show.legend =TRUE, size =1) +labs(y ="Leaf Temperature (°C)", x ="Date") +theme_classic() +theme(legend.position ="none") +scale_colour_viridis_d() +theme(legend.title =element_blank(),legend.text =element_blank(),axis.title =element_text(size =24),axis.text.x =element_blank(), axis.text.y =element_text(size =20)) # Export the figure for Fig 2 in paperggsave(plant_temps_water, filename =here("figs", "fig-plant_temp2.png"), width =8.580247, height =6.111111, dpi =600)``````{r}#| label: fig-plant_temp3.1#| echo: false#| warning: false#| message: false#| include: falsepw1<- plant_temps_water +theme(axis.text.x =element_blank(),legend.text =element_text(size =16),axis.title =element_text(size =24)) +annotate("text", x =as.POSIXct("2019-01-15"), y =52, label =TeX("$^\\circ$C Decrease"), size =6, hjust =0)+annotate("text", x =as.POSIXct("2019-01-19"), y =52, label =TeX("-2.80$^\\circ$C"), size =6, hjust =0) +annotate("text", x =as.POSIXct("2019-01-22"), y =52, label =TeX("-3.63$^\\circ$C"), size =6, hjust =0) +annotate("text", x =as.POSIXct("2019-01-25"), y =52, label =TeX("-3.82$^\\circ$C"), size =6, hjust =0) +annotate("text", x =as.POSIXct("2019-01-28"), y =52, label =TeX("-3.87$^\\circ$C"), size =6, hjust =0)ggsave(pw1, filename =here("figs", "fig-plant_temp3.png"), width =12.432099, height =6.876543, dpi =600)``````{r, tbl-plant_temp3}#| label: tbl-plant_temp3#| tbl-cap: "Mean leaf temperature differences between the original rainfall and the watering scenarios (2, 5, 10 and 20 mm of water added) during the heat wave event (2019-01-15 to 2019-01-31) between the hours 11:00 and 16:00 hr."#| echo: false# Have a look at the temperature reductions tbl_temps <- environ_plant2 %>%filter(dates >"2019-01-15"& dates <"2019-01-31"& TIME >11& TIME <16) %>%mutate(.id =ordered(.id, levels =c("Original Rainfall", "Watering (2 mm)", "Watering (5 mm)", "Watering (10 mm)", "Watering (20 mm)"))) %>%group_by(.id) %>%summarise(mean_temp =mean(TC, na.rm =TRUE)) %>%mutate(temp_diff =round(mean_temp - mean_temp[.id =="Original Rainfall"],2)) %>%select(.id, temp_diff) %>%data.frame() %>%rename(c("Watering Scenario"=".id", "Mean Leaf Temperature Difference (°C)"="temp_diff"))flextable(tbl_temps) %>% flextable::align(align ="center") %>% flextable::bold(part ="header") %>% flextable::align(align="center") %>% flextable::autofit()```@fig-plant_temp3 shows how much watering at the various levels affects the leaf temperature of the plant during the day (between 11:00-14:00 hr). We can see that watering even just 2mm can reduce the leaf temperature by on average `r abs(tbl_temps[2,2])` $^\circ\text{C}$ (@tbl-plant_temp3). Interestingly, we can see that after 5mm of water, the leaf temperature doesn't change much, suggesting that we can get away with less water. A good thing in a very arid environment!### **Impacts of watering scenarios on grasshopper life cycle** {#case6}#### Setting the sceneNow that we have the leaf temperature of the plant under the various watering scenarios, we can use this to understand how the different scenarios might affect grasshopper body temperature, life cycle and population dynamics like we did in Box 2. This is a nice demonstration of re-applying everything we've learnt to a new problem – a problem potentially involving the need to control and understand the probability of pest outbreaks.We can do this by using the ectotherm model using the leaf temperature under the different watering scenarios as the microclimate for the grasshopper. Given the short period of the heat wave and watering regimes we don't expect major differences but it's better to check.#### Use the different watering scenarios to look at how changes in temperature during the watering regime period affect the grasshopper life cycleAs we did before, we'll update the microclimate experienced by the grasshopper to coincide with the leaf temperature and environmental conditions of the plant (assuming grasshoppers are within the bush in 40% shade). This will then calculate what temperatures the grasshopper experiences using a heat-budget model to predict the grasshopper body temperature. Then, we will use the heat-budget calculations to run the Dynamic Energy Budget model for the grasshopper to predict whether this impacts life-cycle. Again, these simulations are all done at once using the `ectotherm` function.```{r, deb_grasshopper_params_sols}#| label: deb_grasshopper_params_sols#| echo: true#| warning: false#| message: false# We first need to load the DEB model paraemeters. These are estimated using MatLab. We've already estimated these so we'll simply just load them into R. pars <-readMat(here("sims", "results_Warramaba_virgo.mat"))par.names <-unlist(labels(pars$par))for(i in1:length(par.names)){assign(par.names[i], unlist(pars$par[i]))} pars <-readMat(here("sims", "stat_Warramaba_virgo.mat"))par.names <-unlist(labels(pars$stat))for(i in1:length(par.names)){assign(par.names[i], unlist(pars$stat[i]))}# Additional parameters we need E_sm <-2000# J/cm^2, maximum stomach energy content fract <-1# scaling factor to change size# Inital state for DEB model# Egg Stage. Starting conditions E_init <- (E.0* fract ^4) / (3E-9* fract ^3) E_H_init <-0 V_init <-3E-9 stage <-0# Life history traits clutchsize <-8# Eggs per clutch stages <-7# Number of stages, 0 = egg, 1-5 = nymphs, 6 = pre egg-laying adult, 7 = post-laying adult S_instar <-rep(s.1, stages) # Stress at instar n: L_n^2/ L_n-1^2 (-)}\cr} L_b <- L.b # Structural length at birth (cm)}\cr} photostart <-0# no photoperiod effect on reproductionphotofinish <-0# no photoperiod effect on reproduction reset <-0# don't reset lifecycle if death occurs soilnode <-3# depth (soil node) at which eggs laid - 3 = 5 cm# Thermoregulatory parameters Ww_g <-0.2# g, body mass, not used though because DEB model computes this shape <-1# cylinder shape_b <-10# ratio of long to short axis of cylinder T_pref <-38# preferred body temperature (animal will attempt to regulate as close to this value as possible) T_F_min <-13# degrees C, lower body temperature for foraging T_F_max <-40# degrees C, upper body temperature for foraging T_B_min <-13# degrees C, minimum basking temperature T_RB_min <-13# degrees C, temperature at which animal will move to a basking site CT_max <-47# degrees C, critical thermal maximum (animal will die if CT_kill = 1 and this threshold is exceeded) CT_min <-0# degrees C, critical thermal minimum (used by program to determine depth selected when inactive and burrowing)# Behavioural traits (made stage-specific later) diurn <-1# Diurnal activity allowed (1) or not (0)? nocturn <-1# Nocturnal activity allowed (1) or not (0)? crepus <-1# Crepuscular activity allowed (1) or not (0)? burrow <-0# Shelter in burrow allowed (1) or not (0)? shade_seek <-1# Shade seeking allowed (1) or not (0)? pct_cond <-0# Percentage of animal surface contacting the substrate (\%)``````{r, ricker_model_sols}#| label: ricker_model_sols#| echo: true#| message: false#| warning: false#' @title Ricker model function#' @description A simple Ricker model function to simulate population dynamics#' @param N0 Initial population size#' @param r Intrinsic growth rate#' @param sd_r Standard deviation of the growth rate#' @param K Carrying capacity#' @return Next years population sizericker <-function(N0, r, sd_r, K, time){ N0 *exp(rnorm(1, r, sd_r)*(1- (N0/K)))}## Create a function to do a stochastic simulation of the Ricker model#' @title Ricker model simulation function#' @description A simple Ricker model function to simulate population dynamics#' @param N0 Initial population size#' @param r Intrinsic growth rate#' @param sd_r Standard deviation of the growth rate#' @param K Carrying capacity#' @param time Time step#' @param nreps Number of replicates#' @return A matrix of population sizes at each time step for nrepssim_ricker <-function(N0, r, sd_r, K, time, nreps){ N_array <-array(NA, dim =c(time, nreps)) # Create a vector to store the population size at each time stepfor(j in1:nreps){ N_array[1,j] <- N0for(i in2:time){ N_array[i,j] <-ricker(N_array[i-1, j], r, sd_r, K) } }return(N_array)}``````{r, deb_grasshopper_water}#| label: deb_grasshopper_water#| echo: true#| warning: false#| message: false# First, we need to load in the microclimates scenerios. We'll do this in a loop so that we can run the DEB model for each of the scenerios. So, here we'll just create a few vectors that cell specific objects we need. plant_micro <-c("plant_microclimate_micro.RDa", "plant_rainhour_2_shade_micro.RDa", "plant_rainhour_5_shade_micro.RDa", "plant_rainhour_10_shade_micro.RDa", "plant_rainhour_20_shade_micro.RDa") plant_mirco_names <-c("envron_plant", "environ_plant_2", "environ_plant_5", "environ_plant_10", "environ_plant_20")# We also need to load in the original microclimate object so that we can get the dates and other information from it. This is the original microclimate object that we used to simulate the plant microclimate. We'll also overide the `mirco` object so that the ectotherm model can use it.load(here("output", "microclim", "microclimate.RDa"))# Now we can loop through the two microclimates and run the DEB model for each of themfor(i in1:length(plant_micro)){# Load the microclimate objectload(here("output", "microclim", plant_micro[i]))# Use the plant microclimate data to reset the original microclimate object. Note that we need to adjust both shade and nonshade conditions # Unshaded microclimate micro$metout <- plant$metout micro$metout [,"TALOC"] <- plant$environ[,"TC"]# Shaded microclimate microshadmet <- plant$shadmet micro$shadmet [,"TALOC"] <- plant$environ [,"TC"]# Soil microclimate micro$soil <- plant$soil micro$shadsoil <- plant$shadsoil micro$soilmoist <- plant$soilmoist micro$shadmoist <- plant$shadmoist# Get dates from microclimate object dates <- micro$dates # dates, hourlydates2 <- micro$dates2 # dates, daily# Set starting date by truncating microclimate model output to when grasshoppers are epected to oviposit eggs. This is the date when the life cylce of the insect is expected to start.oviposition_date <-'2018-03-31 23:00:00' subs <-which( dates >as.POSIXct(oviposition_date)) subs2 <-which(dates2 >as.POSIXct(oviposition_date))# Now run the DEB life-cyle model for the grasshopper. See Case Study 2 for more details on the parameters used in the DEB model. ecto <-ectotherm( Ww_g = Ww_g,shape = shape,shape_b = shape_b,T_pref = T_pref,T_F_min = T_F_min,T_F_max = T_F_max,T_B_min = T_B_min,T_RB_min = T_RB_min,CT_min = CT_min,CT_max = CT_max,diurn = diurn,nocturn = nocturn,crepus = crepus,shade_seek = shade_seek,burrow = burrow,pct_cond = pct_cond,nyears =ceiling(length(subs2)/365),metout = micro$metout[subs,],soil = micro$soil[subs,], soilpot = micro$soilpot[subs,],soilmoist = micro$soilmoist[subs,], humid = micro$humid[subs,],tcond = micro$tcond[subs,], shadmet = micro$shadmet[subs,],shadsoil = micro$shadsoil[subs,], shadpot = micro$shadpot[subs,],shadmoist = micro$shadmoist[subs,],shadhumid = micro$shadhumid[subs,],shadtcond = micro$shadtcond[subs,], rainfall = micro$RAINFALL[subs2],minshades =rep(unique(micro$minshade), length(subs)),maxshades =rep(unique(micro$maxshade), length(subs)),DEB =1, # turns DEB model onmetab_mode =1, # turns on hemimetabolous model, abpz = z * fract,del_M = del.M,p_Xm = p.Am *100/24/ kap.X,kap_X = kap.X,v = v /24,kap = kap,p_M = p.M /24,E_G = E.G,kap_R = kap.R,k_J = k.J /24,E_Hb = E.Hb * fract ^3,E_Hp = (E.Hj+1e-5) * fract ^3,E_Hj = E.Hj * fract ^3,h_a = h.a / (24^2),s_G = s.G,E_0 = E.0* fract ^4,mu_E = mu.E,d_V = d.V,d_E = d.E,T_REF = T.ref,T_A = T.A,T_AL = T.AL,T_AH = T.AH,T_L = T.L,T_H = T.H,E_sm = E_sm,E_init = E_init,E_H_init = E_H_init,V_init = V_init,stage = stage,clutchsize = clutchsize,stages = stages,S_instar =rep(s.1, stages),L_b = L.b,photostart = photostart,photofinish = photofinish,reset = reset,soilnode = soilnode)# Save the DEB modelsave(ecto, file =here("output", "microclim", paste0("grasshopper_", plant_mirco_names[i], "_DEB.RDa")))}```Now that we have the DEB model for each of the watering scenerios, we can plot out the grasshopper life cycles for each scenerio (@fig-grasshopper_lifecycle_water). ```{r, fig-grasshopper_lifecycle_water}#| label: fig-grasshopper_lifecycle_water#| fig-cap: "Life-cycle of a grasshopper at a location near Renmark, South Australia. Life-cycle of the grasshopper is shown in under 5 different scenarios: A) Original rainfall and B) 2mm, C) 5mm, D) 10 mm and E) 20mm of watering to plants during a heat wave event. Note that the life-cycle is shown as the wet mass of the grasshopper (mg) over time (days) with different colours corresponding to different life stages. The life stages are: 0 = egg ('grey'), 1 = first instar ('forest green'), 2 = second instar ('lime green'), 3 = third instar ('tan'), 4 = fourth instar ('brown'), 5 = fifth instar ('orange'), 6 = adult ('pink')."#| code-fold: true#| echo: true#| warning: false#| message: false# Scenerios plant_mirco_names <-c("envron_plant", "environ_plant_2", "environ_plant_5", "environ_plant_10", "environ_plant_20") debs <-paste0("grasshopper_", plant_mirco_names, "_DEB.RDa") scenerios <-c("Original Rainfall", "Watering (2 mm)", "Watering (5 mm)", "Watering (10 mm)", "Watering (20 mm)")# Now, lets load in the DEB model for each of the scenerios and plot them out. We'll save to a listplot_list <-list()for(i in1:length(debs)){# Load the DEB modelload(here("output", "microclim", debs[i])) datetime <- dates[subs] sub <-which(datetime >as.POSIXct('2018-03-31') & datetime <as.POSIXct('2019-04-30')) debout <-data.frame(ecto$debout) debout$datetime <- datetime scale_factor <-10 p <-ggplot() +ylim(-5, 400) +geom_line(data = debout, aes(x = datetime, y = WETMASS *1000), col ="black") +geom_line(data = debout %>%filter(STAGE ==0), aes(x = datetime, y = WETMASS *1000), col ="darkgrey", linewidth =2) +geom_line(data = debout %>%filter(STAGE ==1), aes(x = datetime, y = WETMASS *1000), col ="lightgreen", linewidth =2) +geom_line(data = debout %>%filter(STAGE ==2), aes(x = datetime, y = WETMASS *1000), col ="green", linewidth =2) +geom_line(data = debout %>%filter(STAGE ==3), aes(x = datetime, y = WETMASS *1000), col ="tan", linewidth =2) +geom_line(data = debout %>%filter(STAGE ==4), aes(x = datetime, y = WETMASS *1000), col ="brown", linewidth =2) +geom_line(data = debout %>%filter(STAGE ==5), aes(x = datetime, y = WETMASS *1000), col ="orange", linewidth =2) +geom_line(data = debout %>%filter(STAGE ==6), aes(x = datetime, y = WETMASS *1000), col ="pink", linewidth =2) +labs(title = scenerios[i], x ="Date") +scale_y_continuous(name ="Wet mass (mg)")+theme_classic()plot_list[[i]] <- p }(plot_list[[1]] + plot_list[[2]]) / (plot_list[[3]] + plot_list[[4]]) / (plot_list[[5]]) +plot_annotation(tag_levels ="A") +theme(plot.tag =element_text(size =12, face ="bold"))```Just like before we can get some key parameters from the DEB model to understand how the different watering scenerios affect the grasshopper life cycle.```{r, tbl-grasshopper_lifecycle_watering}#| label: tbl-grasshopper_lifecycle_watering#| tbl-cap: "Predicted life-cycle parameters of a grasshopper at a location near Renmark, South Australia under different watering scenerios (2, 5, 10 and 20 mm of water added) during a heat wave event. The life-cycle parameters are: A) Time to first instar (days), B) Time to adult (days), C) Time to oviposition (days), D) Time to first egg hatch (days), E) Time to first instar hatch (days), F) Time to first adult hatch (days)."#| echo: false#| code-fold: true#| warning: false#| message: false# Now lets extract the life-cycle parameters from the DEB model and put them into a table debs <-paste0("grasshopper_", plant_mirco_names, "_DEB.RDa") scenerios <-c("Original Rainfall", "Watering (2 mm)", "Watering (5 mm)", "Watering (10 mm)", "Watering (20 mm)")# Create a data.frame to store the life-cycle parameters life_cycle <-data.frame()# Loop through the DEB models and extract the life-cycle parametersfor(i in1:length(debs)){# Load the DEB modelload(here("output", "microclim", debs[i])) yearout <-data.frame(ecto$yearout) life_cycle <-rbind(life_cycle, yearout[1,])}# Lets add a column for the different scenerios and re-arrange the data so it's the first column life_cycle <- life_cycle %>%mutate(scenario = scenerios) %>%select(scenario, everything())# Remove irrelevant columns life_cycle <- life_cycle %>%select(-c("ANNUALACT", "Length"))colnames(life_cycle) <-c("Watering Scenario","Development time (days)", "Birth day (day of year)", "Mass at birth (g)", "Months to maturity", "Months to reproducton", "Length (mm) at first reproduction", "Total fecundity (# eggs)","Total clutches","Minimum reserve density (J/cm3)","Food eaten in last year (kg)","Total food eaten across life (kg)","Minimum body temperature (°C)","Maximum body temperature (°C)","Maximum level of desiccation","Maximum lifespan (days)","Generation time (years)","Net reproductive rate (R0)","Intrinsic growth rate (r)" )life_cycle %>%mutate(across(where(is.numeric), sprintf, fmt ='%.2f')) %>%datatable(.,extensions ="Buttons",rownames =FALSE)```We can then look at the expected population growth rate of the grasshopper species under the different watering scenerios as we did in [case study 2](#case2). ```{r}#| label: fig-grasshopper_growth_rate_water#| fig-cap: "Expected population growth rate of a grasshopper at a location near Renmark, South Australia under different watering scenerios (2, 5, 10 and 20 mm of water added). Models assume that the watering scenerio is implemented each generation leading to a similar intrinsic population growth rate. Watering scenerios implemented at different times of the year are likely to have different effects on the population growth rate, but could be captured by simulating lice cyles for different years."#| echo: true#| warning: false#| message: false#| code-fold: trueset.seed(123) # Set seed for reproducibility# Set up the parameters for the Ricker model N0 <-1000# Initial population size r <- life_cycle[,"Intrinsic growth rate (r)"] # Intrinsic growth (DEB model) names <- scenerios # Scenerios sd_r <-0.15# Standard deviation of the growth rate K <-10000# Carrying capacity time <-5# Time steps nsims <-1000# Number of simulations# Lets run the simulationssims <-list()for(i in1:length(r)){ sims[[i]] <-sim_ricker(N0, r[i], sd_r, K, time, nsims)names(sims)[i] <- names[i]}# Re-organise the data for plottingdata_organised <-lapply(sims, function(x) {pivot_longer(as.data.frame(x), cols =everything(), names_to ="sim", values_to ="N") %>%mutate(time =rep(1:time, each = nsims))})# Plot the data for each sceneriodata_organised2 <- plyr::ldply(data_organised, .id ="scenario") %>%mutate(scenerio_sim =interaction(scenario, sim)) # Create a summary of the simulations to make plotting easier data_organised2_final <- data_organised2 %>%group_by(scenario, time) %>%summarise(N_m =mean(N), sd =sd(N)) # Now plot the simulationsplot_water_sims <-ggplot(data_organised2_final, aes(x = time, y = N_m, color = scenario)) +geom_ribbon(aes(ymin = N_m - sd, ymax = N_m + sd, fill = scenario), alpha =0.3, colour =NA) +geom_line() +labs(x ="Time (Generations)", y ="Population size (N)") +theme_classic() +scale_colour_viridis_d() +scale_fill_viridis_d() +theme(axis.title =element_text(size =20),axis.text =element_text(size =20),legend.position ="inside",legend.position.inside =c(0.25,0.8),legend.text =element_text(size =20),legend.title =element_blank())plot_water_sims``````{r}#| label: fig-grasshopper_pop_growth_out#| echo: false#| warning: false#| message: false#| include: falseggsave(plot_water_sims, filename =here("figs", "fig-grasshopper_growth_rate_water.png"), width =7, height =5.8, dpi =600)```# **References**