Strategies & traits

guide

A strategy contains the biological settings for one type of plant. This guide shows two ways to create one: change a parameter directly, or provide measured trait values and let plant update the linked parameters. For the equations behind each strategy, see Models.

library(plant)

Inspect or directly change a strategy

Start with the default FF16 strategy. Printing its names shows that the object contains biological parameters, numerical controls, recruitment settings, and other model information:

s <- FF16_Strategy()
names(s)
[1] "pars"                   "control"                "collect_all_auxiliary" 
[4] "birth_rate_x"           "birth_rate_y"           "is_variable_birth_rate"

The physiological parameters live in the nested pars element. They are ordinary decimal numbers and can be inspected with familiar R tools:

unlist(s$pars)
              lma               rho              hmat             omega 
     1.978791e-01      6.080000e+02      1.659587e+01      3.800000e-05 
              eta             theta              a_l1              a_l2 
     1.200000e+01      2.141786e-04      5.440000e+00      3.060000e-01 
             a_r1              a_b1               r_s               r_b 
     7.000000e-02      1.700000e-01      6.598684e+00      1.319737e+01 
              r_r               r_l               a_y             a_bio 
     2.170000e+02      1.984545e+02      7.000000e-01      2.450000e-02 
              k_l               k_b               k_s               k_r 
     4.565855e-01      2.000000e-01      2.000000e-01      1.000000e+00 
             a_p1              a_p2              a_f3              a_f1 
     1.511778e+02      2.047162e-01      1.140000e-04      1.000000e+00 
             a_f2               S_D              a_d0               d_I 
     5.000000e+01      2.500000e-01      1.000000e-01      1.000000e-02 
            a_dG1             a_dG2               k_I recruitment_decay 
     5.500000e+00      2.000000e+01      5.000000e-01      0.000000e+00 

For a controlled experiment, you can change one value directly. Here we double leaf mass per unit leaf area (lma):

s$pars$lma <- s$pars$lma * 2

An individual constructed from this strategy uses the revised value:

pl <- FF16_Individual(s)
pl$strategy$pars$lma
[1] 0.3957582

This direct edit changes only lma. That is useful when deliberately varying one model parameter, but biological traits are often linked. In FF16, LMA affects leaf mass and seedling allometry directly and is also associated with leaf turnover and respiration.

Create strategies from traits

plant represents these links with a hyperparameterisation: an R function that translates a small set of input traits into a consistent set of model parameters. FF16 uses FF16_hyperpar():

FF16_hyperpar
function (m, s, filter = TRUE) 
{
    with_default <- function(name, default_value = s$pars[[name]]) {
        rep_len(if (name %in% colnames(m)) 
            m[, name]
        else default_value, nrow(m))
    }
    lma <- with_default("lma")
    rho <- with_default("rho")
    omega <- with_default("omega")
    narea <- with_default("narea", narea)
    k_I <- s$pars$k_I
    k_l <- B_kl1 * (lma/lma_0)^(-B_kl2)
    d_I <- B_dI1 * (rho/rho_0)^(-B_dI2)
    k_s <- B_ks1 * (rho/rho_0)^(-B_ks2)
    r_s <- B_rs1/rho
    r_b <- B_rb1/rho
    a_f3 <- B_f1 * omega
    assimilation_rectangular_hyperbolae <- function(I, Amax, 
        theta, QY) {
        x <- QY * I + Amax
        (x - sqrt(x^2 - 4 * theta * QY * I * Amax))/(2 * theta)
    }
    approximate_annual_assimilation <- function(narea, latitude) {
        E <- seq(0, 1, by = 0.02)
        D <- seq(0, 365/2, length.out = 10000)
        I <- PAR_given_solar_angle(solar_angle(D, latitude = abs(latitude)))
        Amax <- B_lf1 * (narea/narea_0)^B_lf5
        theta <- B_lf2
        QY <- B_lf3
        AA <- NA * E
        for (i in seq_len(length(E))) {
            AA[i] <- 2 * trapezium(D, assimilation_rectangular_hyperbolae(k_I * 
                I * E[i], Amax, theta, QY))
        }
        if (all(diff(AA) < 1e-08)) {
            ret <- c(dplyr::last(AA), 0)
            names(ret) <- c("p1", "p2")
        }
        else {
            fit <- nls(AA ~ p1 * E/(p2 + E), data.frame(E = E, 
                AA = AA), start = list(p1 = 100, p2 = 0.2))
            ret <- coef(fit)
        }
        ret
    }
    a_p1 <- a_p2 <- 0 * narea
    if (length(narea) > 0) {
        i <- match(narea, unique(narea))
        y <- vapply(unique(narea), approximate_annual_assimilation, 
            numeric(2), latitude)
        a_p1 <- y["p1", i]
        a_p2 <- y["p2", i]
    }
    r_l <- B_lf4 * narea/lma
    extra <- cbind(k_l, d_I, k_s, r_s, r_b, a_f3, a_p1, a_p2, 
        r_l)
    overlap <- intersect(colnames(m), colnames(extra))
    if (length(overlap) > 0L) {
        stop("Attempt to overwrite generated parameters: ", paste(overlap, 
            collapse = ", "))
    }
    if (any(is.infinite(extra))) {
        stop("Attempt to use infinite value in derived parameters: ", 
            paste(colnames(extra)[is.infinite(extra)], collapse = ", "))
    }
    if (filter) {
        if (nrow(extra) == 0L) {
            extra <- NULL
        }
        else {
            pos <- diff(apply(extra, 2, range)) == 0
            if (any(pos)) {
                eps <- sqrt(.Machine$double.eps)
                x1 <- extra[1, pos]
                x2 <- unlist(s$pars[names(x1)])
                drop <- abs(x1 - x2) < eps & abs(1 - x1/x2) < 
                  eps
                if (any(drop)) {
                  keep <- setdiff(colnames(extra), names(drop)[drop])
                  extra <- extra[, keep, drop = FALSE]
                }
            }
        }
    }
    if (!is.null(extra)) {
        m <- cbind(m, extra)
    }
    m
}
<bytecode: 0x139ff9c08>
<environment: 0x139ff14e8>

You will rarely call this function directly. Looking at its output is useful, however, because it makes the parameter links explicit. Supplying LMA updates leaf turnover (k_l) and leaf respiration per mass (r_l). The output also includes photosynthesis per leaf area (a_p1), which is derived from leaf nitrogen per area (narea) rather than LMA itself.

FF16_hyperpar(trait_matrix(0.1, "lma"), s)
   lma      k_l     a_p1   r_l
p1 0.1 1.466783 151.1778 392.7

The three returned quantities are:

  • k_l: Turnover rate for leaves
  • a_p1: Leaf photosynthesis per area
  • r_l: Leaf respiration per mass

Because these linking rules live in R, advanced users can define a different trait trade-off without changing the underlying physiological model in C++.

trait_matrix() puts one or more trait values into the row-and-column format expected by the strategy-building functions:

trait_matrix(0.1, "lma")
     lma
[1,] 0.1
trait_matrix(c(0.1, 500), "rho")
       rho
[1,]   0.1
[2,] 500.0

The preferred workflow uses generate_strategy() or add_strategies(). Both start from a Parameters object, which collects the settings needed for a patch run and stores the default strategy from which new strategies are built:

p <- FF16_Parameters()

Its top-level names show the main groups of settings:

names(p)
 [1] "patch_area"                  "n_patches"                  
 [3] "patch_type"                  "max_patch_lifetime"         
 [5] "strategies"                  "strategy_default"           
 [7] "node_schedule_times_default" "node_schedule_times"        
 [9] "ode_times"                   "initial_state"              
[11] "n_initial_cohorts"           "initial_node_times"         
[13] "initial_patch_density"       "initial_pr_patch_survival"  
[15] "initial_time"               

param_hyperpar() retrieves the linking function associated with the model:

identical(param_hyperpar(p), FF16_hyperpar)
[1] TRUE

The strategy_default member is the template copied when new strategies are generated. Physiological parameters, including the light extinction coefficient k_I, live under its pars element:

class(p$strategy_default)
[1] "FF16_Strategy"

Generating a strategy copies that template, sets LMA, and updates the linked parameters:

s <- generate_strategy(p, trait_matrix(0.1, "lma"), birth_rate = 1)[[1]]

The same function can generate several strategies at once. Each row of the trait matrix becomes one strategy, so the birth-rate vector also has one value per row:

lma <- trait_matrix(seq(0.1, 0.5, length.out=5), "lma")
FF16_hyperpar(trait_matrix(lma, "lma"), s)
     lma        k_l     r_l
[1,] 0.1 1.46678341 392.700
[2,] 0.2 0.44833712 196.350
[3,] 0.3 0.22412414 130.900
[4,] 0.4 0.13703875  98.175
[5,] 0.5 0.09356799  78.540
ss <- generate_strategy(p, lma, birth_rate = rep(1, 5))
length(ss)
[1] 5

Standard R functions can compare the resulting parameter values:

sapply(ss, function(x) x$pars$lma)
[1] 0.1 0.2 0.3 0.4 0.5
sapply(ss, function(x) x$pars$k_l)
[1] 1.46678341 0.44833712 0.22412414 0.13703875 0.09356799
sapply(ss, function(x) x$pars$a_p1)
[1] 151.1778 151.1778 151.1778 151.1778 151.1778
sapply(ss, function(x) x$pars$r_l)
[1] 392.700 196.350 130.900  98.175  78.540

Choose numerical accuracy

Strategies describe plants; Control settings describe how the numerical solver runs. The controls include time-step, error-tolerance, and node-schedule settings, and may be numeric or logical:

Control()
$function_integration_rule
[1] 21

$shading_model
[1] ""

$ppa_layer_optical_depth
[1] 0.5

$ppa_layer_smoothing
[1] 0.3

$offspring_production_tol
[1] 1e-08

$offspring_production_iterations
[1] 1000

$node_gradient_eps
[1] 1e-06

$node_gradient_direction
[1] -1

$node_gradient_richardson
[1] FALSE

$node_gradient_richardson_depth
[1] 4

$ode_step_size_initial
[1] 1e-06

$ode_step_size_min
[1] 1e-06

$ode_step_size_max
[1] 5

$ode_tol_rel
[1] 1e-04

$ode_tol_abs
[1] 1e-04

$ode_a_y
[1] 1

$ode_a_dydt
[1] 0

$fixed_time_step
[1] 0

$schedule_nsteps
[1] 20

$schedule_eps
[1] 0.02

$schedule_verbose
[1] FALSE

$save_RK45_cache
[1] FALSE

$GSS_tol_abs
[1] 0.001

$vulnerability_curve_ncontrol
[1] 100

$ci_abs_tol
[1] 0.001

$ci_niter
[1] 1000

attr(,"class")
[1] "Control"

The Control() defaults are appropriate for routine exploration; control() is a lowercase alias for the same constructor. Use control_accurate() when a final result needs tighter numerical tolerances, accepting the additional run time. The ODE stepping and node-spacing pages explain these trade-offs. The following comparison shows which defaults change:

ctrl_accurate <- control_accurate()
ctrl_accurate[unlist(control()) != unlist(ctrl_accurate)]
$ode_step_size_max
[1] 0.1

$ode_tol_rel
[1] 1e-06

$ode_tol_abs
[1] 1e-06

$schedule_eps
[1] 0.001