6  Pre-Calculate Growth

Purpose: Precalculate growth to look-up growth rates based on weight, temperature, and p-value (proportion of temperature-adjusted maximum consumption).

Source dependencies

6.1 Set-up

Set parameters for O. mykiss

Code
Constants <- fncReadConstants()
pred.en.dens <- 5900 #predator energy density
oxy <- 13560 #oxygen consumed
pff <- 0.1 #percent indigestible prey
prey.en.dens <- 3500 # see Railsback and Rose 1999, Beauchamp et al. 2007

Set up arrays. Sequences must match the lookup sequences defined in fncGrowthFish(), fncGrowthPossible(), and the IBM simulation loop.

Code
waterTemps <- cbind("pid" = seq(1, length(seq(0.1, 25, 0.1))), "WT" = seq(0.1, 25, 0.1))
pvals   <- seq(0.01, 1.0, 0.01)    # p-value (100 values)
weights <- seq(0.25, 6000, 0.25)   # fish mass (24,000 values)
wt.growth <- array(NA, dim = c(nrow(waterTemps), length(pvals), length(weights))) # dim = c(250 WTs, 100 pvals, 24000 weights)
respiration <- consumption <- wastes <- cmax <- wt.growth

6.2 Calculate growth

Loop through arrays to record growth for each case

Code
for(w in 1:dim(wt.growth)[3]){
  ww <- weights[w]
  for(r in 1:dim(wt.growth)[2]){
    pp <- pvals[r]
    Input <- fncGetBioEParms("cutthroat", pred.en.dens, prey.en.dens, oxy, pff,
                             waterTemps,
                             startweights = rep(ww, nrow(waterTemps)),
                             pvals = rep(pp, nrow(waterTemps)))

    Results <- BioE(Input, Constants) #run BioE code given input and constants
    wt.growth[,r,w]   <- c(Results$Gg_WinBioE)  # growth in g/g/d, standardized by fish weight
    consumption[,r,w] <- c(Results$Consumption)
    wastes[,r,w]      <- c(Results$Egestion + Results$Excretion + Results$S.resp)
    respiration[,r,w] <- c(Results$Respiration)
    cmax[,r,w]        <- c(Results$Consumption_max)

    ## In Joules instead of grams:
    # wt.growth[,r,w]   <- c(Results$Growth_j)
    # consumption[,r,w] <- c(Results$Consumption_j)
    # wastes[,r,w]      <- c(Results$Egestion_j + Results$Excretion_j + Results$Sj.resp)
    # respiration[,r,w] <- c(Results$Respiration_j)
  }
  if (w %% 500 == 0) print(w)
}

Save outputs. Only ‘wt.growth’ is needed for simulations. The others are merely for plotting below.

Code
save("wt.growth",    file = "data/wt.growth.array_pval_cuttCACB.RData")
save("respiration",  file = "data/respiration.array_pval_cuttCACB.RData")
save("wastes",       file = "data/wastes.array_pval_cuttCACB.RData")
save("consumption",  file = "data/consumption_pval_cuttCACB.array.RData")
save("cmax",         file = "data/cmax.array_pval_cuttCACB.RData")

Load back in to plot

Code
load("data/wt.growth.array_pval_sthdCACB.RData")
load("data/consumption_pval_sthdCACB.array.RData")
load("data/wastes.array_pval_sthdCACB.RData")
load("data/respiration.array_pval_sthdCACB.RData")
load("data/cmax.array_pval_sthdCACB.RData")

6.3 Plot output

6.3.1 Growth rate

Plot growth rate by temperature for fish of varying sizes, across the full range of p-values. Line color runs blue (low p) → red (high p).

Code
# Representative mass indices for the new array (step = 0.25 g): 1g→4, 10g→40, 100g→400, 250g→1000, 500g→2000, 1500g→6000
mass_idx <- c(4, 40, 400, 1000, 2000, 6000)
mass_lbl <- c(1, 10, 100, 250, 500, 1500)

map_dfr(seq_along(mass_idx), \(k) {
  map_dfr(seq(1, 100, by = 5), \(pi) {
    tibble(
      weight_g = factor(paste0(mass_lbl[k], " g"), levels = paste0(mass_lbl, " g")),
      pval     = pvals[pi],
      temp     = waterTemps[, "WT"],
      growth   = wt.growth[, pi, mass_idx[k]]
    )
  })
}) |>
  ggplot(aes(x = temp, y = growth, color = pval, group = pval)) +
  geom_line(linewidth = 0.4) +
  geom_hline(yintercept = 0, linetype = "dashed", color = "black") +
  facet_wrap(~ weight_g, ncol = 3) +
  scale_color_gradientn(colors = hcl.colors(100, "Temps"), name = "P-value") +
  coord_cartesian(ylim = c(-0.06, 0.16)) +
  labs(x = "Water temperature (°C)", y = "Growth rate (g/g/d)") +
  theme_bw() + theme(panel.grid = element_blank())

Plot growth potential by weight under different temperatures (panels) and p-values (line color: blue = low, red = high).

Code
tempseq <- seq(1, length(waterTemps[,"WT"]), by = 45)
w_show  <- which(weights <= 50)  # focus on 0–50 g range

map_dfr(seq_along(tempseq), \(ti) {
  temp_val <- waterTemps[tempseq[ti], "WT"]
  map_dfr(seq(1, 100, by = 5), \(pi) {
    tibble(
      temp_label = factor(paste0(temp_val, " °C"),
                          levels = paste0(waterTemps[tempseq, "WT"], " °C")),
      pval   = pvals[pi],
      weight = weights[w_show],
      growth = wt.growth[tempseq[ti], pi, w_show]
    )
  })
}) |>
  ggplot(aes(x = weight, y = growth, color = pval, group = pval)) +
  geom_line(linewidth = 0.4) +
  geom_hline(yintercept = 0, linetype = "dashed", color = "black") +
  facet_wrap(~ temp_label, ncol = 3) +
  scale_color_gradientn(colors = hcl.colors(100, "Temps"), name = "P-value") +
  coord_cartesian(ylim = c(-0.05, 0.1)) +
  labs(x = "Weight (g)", y = "Growth rate (g/g/d)") +
  theme_bw() + theme(panel.grid = element_blank())

6.3.2 Maintenance p-value

The maintenance p-value is the minimum proportion of maximum consumption a fish must achieve to maintain positive growth at a given temperature and body size. It increases with body size (as allometric Cmax declines) and deviates from its optimum at extreme temperatures (where respiration costs rise relative to consumption capacity). Lines in the optimal-temperature plots above are truncated at each p-value’s maintenance boundary; below we visualise the full surface.

Code
# For every [temperature, weight] cell, find the minimum p-value at which growth > 0.
# Note: as.vector() is column-major (ti varies fast); t() transposes so wi varies fast,
# matching the row order produced by expand_grid(ti, wi).
maint <- apply(wt.growth, c(1, 3), function(gvec) {
  pos <- which(gvec > 0)
  if (length(pos) > 0) pvals[min(pos)] else NA_real_
})

breaks_pv <- seq(0.1, 0.9, by = 0.1)

maint_df_full <- expand_grid(ti = seq_len(nrow(maint)), wi = seq_len(ncol(maint))) |>
  mutate(
    temp       = waterTemps[ti, "WT"],
    weight     = weights[wi],
    maint_pval = as.vector(t(maint))
  )

# Extract contour midpoints in a legible weight range for labelling
contour_data <- ggplot_build(
  ggplot(maint_df_full, aes(x = weight, y = temp, z = maint_pval)) +
    geom_contour(breaks = breaks_pv, na.rm = TRUE)
)$data[[1]]

label_pts <- contour_data |>
  filter(x > 500, x < 2500) |>
  group_by(level) |>
  slice(n() %/% 2) |>
  ungroup() |>
  mutate(label = as.character(round(as.numeric(level), 1)))

maint_df_full |>
  ggplot(aes(x = weight, y = temp, fill = maint_pval)) +
  geom_raster() +
  geom_contour(aes(z = maint_pval), breaks = breaks_pv,
               color = "white", linewidth = 0.4, na.rm = TRUE) +
  geom_label(data = label_pts, aes(x = x, y = y, label = label),
             size = 2.8, linewidth = 0, fill = alpha("white", 0.5),
             color = "grey20", inherit.aes = FALSE) +
  scale_fill_gradientn(
    colors   = hcl.colors(100, "Temps"),
    name     = "Maintenance\np-value",
    limits   = c(0, 1),
    na.value = "grey70"
  ) +
  scale_x_continuous(expand = c(0, 0)) +
  scale_y_continuous(expand = c(0, 0)) +
  labs(
    x       = "Fish weight (g)",
    y       = "Water temperature (°C)",
    title   = "Minimum p-value required for positive growth",
    caption = "Grey: growth impossible even at p = 1.0."
  ) +
  theme_bw()

Plot optimal growth temperature and maximum growth rate by fish weight across a range of p-values (blue = low, red = high). Each line is set to NA for weights at which that p-value cannot sustain positive growth at any temperature — i.e., where consumption falls below the maintenance ration. Consequently, line endpoints trace the maintenance p-value boundary: the minimum p-value required for zero net growth at a given body size. Lines are shorter at lower p-values because smaller fish are already below the maintenance threshold, and the boundary shifts to larger sizes as p-value increases.

Code
optgrowth_df <- map_dfr(seq(1, 100, by = 5), \(pi) {
  mat    <- wt.growth[, pi, ]                          # 250 temp × 24000 weight matrix
  best_t <- apply(mat, 2, which.max)                   # temperature index of max growth, per weight
  max_g  <- mat[cbind(best_t, seq_along(best_t))]
  tibble(
    pval      = pvals[pi],
    weight    = weights,
    # NA where max growth is negative: optimal temp is undefined when no positive growth is possible
    opttemp   = if_else(max_g > 0, waterTemps[best_t, "WT"], NA_real_),
    maxgrowth = if_else(max_g > 0, max_g,                    NA_real_)
  )
})

ggplot(optgrowth_df, aes(x = weight, y = opttemp, color = pval, group = pval)) +
  geom_line(linewidth = 0.4, na.rm = TRUE) +
  scale_color_gradientn(colors = hcl.colors(100, "Temps"), name = "P-value") +
  labs(x = "Fish weight (g)", y = "Optimal temperature for growth (°C)") +
  theme_bw() + theme(panel.grid = element_blank()) + 
  coord_cartesian(xlim = c(0, 2000))

Code
ggplot(optgrowth_df, aes(x = weight, y = maxgrowth, color = pval, group = pval)) +
  geom_line(linewidth = 0.4, na.rm = TRUE) +
  geom_hline(yintercept = 0, linetype = "dashed", color = "black") +
  scale_color_gradientn(colors = hcl.colors(100, "Temps"), name = "P-value") +
  labs(x = "Fish weight (g)", y = "Maximum growth rate (g/g/d)") +
  theme_bw() + theme(panel.grid = element_blank()) + 
  coord_cartesian(xlim = c(0, 2000))

6.3.3 Consumption

Consumption by temperature for representative fish sizes, across the full range of p-values (blue = low, red = high):

Code
map_dfr(seq_along(mass_idx), \(k) {
  map_dfr(seq(1, 100, by = 5), \(pi) {
    tibble(
      weight_g = factor(paste0(mass_lbl[k], " g"), levels = paste0(mass_lbl, " g")),
      pval     = pvals[pi],
      temp     = waterTemps[, "WT"],
      cons     = consumption[, pi, mass_idx[k]]
    )
  })
}) |>
  ggplot(aes(x = temp, y = cons, color = pval, group = pval)) +
  geom_hline(yintercept = 0, linetype = "dashed", color = "black") +
  geom_line(linewidth = 0.4) +
  facet_wrap(~ weight_g, ncol = 3) +
  scale_color_gradientn(colors = hcl.colors(100, "Temps"), name = "P-value") +
  coord_cartesian(ylim = c(0, 0.6)) +
  labs(x = "Water temperature (°C)", y = "Consumption (g/g/d)") +
  theme_bw() + theme(panel.grid = element_blank())

6.3.4 Energy budget

Create multi-panel energy budget plots for different sized fish: Consumption = metabolism + wastes + growth

Scope for growth is thus the difference between Consumption and (metabolism + wastes), i.e., the space between the Consumption and Wastes lines. Shown at p = 0.5 (index 50):

Code
budget_cols <- c("Consumption" = "#2166ac", "Wastes" = "#1a9641", "Respiration" = "#d73027")

map_dfr(seq_along(mass_idx), \(k) {
  mi   <- mass_idx[k]
  temp <- waterTemps[, "WT"]
  bind_rows(
    tibble(weight_g = paste0(mass_lbl[k], " g"), component = "Consumption", temp = temp, rate = consumption[, 50, mi]),
    tibble(weight_g = paste0(mass_lbl[k], " g"), component = "Wastes",      temp = temp, rate = wastes[,      50, mi]),
    tibble(weight_g = paste0(mass_lbl[k], " g"), component = "Respiration", temp = temp, rate = respiration[, 50, mi])
  )
}) |>
  mutate(
    weight_g  = factor(weight_g,  levels = paste0(mass_lbl, " g")),
    component = factor(component, levels = names(budget_cols))
  ) |>
  ggplot(aes(x = temp, y = rate, color = component)) +
  geom_hline(yintercept = 0, linetype = "dashed", color = "black") +
  geom_line(linewidth = 0.6) +
  facet_wrap(~ weight_g, ncol = 3) +
  scale_color_manual(values = budget_cols, name = NULL) +
  coord_cartesian(ylim = c(0, 0.3)) +
  labs(x = "Water temperature (°C)", y = "Rate (g/g/d)") +
  theme_bw() +
  theme(legend.position = "bottom", panel.grid = element_blank())

Create single panel ‘growth banana’ as in FB4 documentation: shaded region is the scope for growth (p = 0.5, 500g fish):

Code
mi <- 2000  # index for 500g fish

tibble(
  temp        = waterTemps[, "WT"],
  consumption = consumption[, 50, mi],
  wastes      = wastes[,      50, mi],
  respiration = respiration[, 50, mi]
) |>
  ggplot(aes(x = temp)) +
  geom_ribbon(aes(ymin = respiration, ymax = wastes), fill = "grey80") +
  geom_line(aes(y = consumption, color = "Consumption"), linewidth = 0.8) +
  geom_line(aes(y = wastes,      color = "Wastes"),      linewidth = 0.8) +
  geom_line(aes(y = respiration, color = "Respiration"), linewidth = 0.8) +
  geom_hline(yintercept = 0, linetype = "dashed", color = "grey60") +
  scale_color_manual(values = budget_cols, name = NULL) +
  coord_cartesian(ylim = c(0, 0.05)) +
  labs(x = "Water temperature (°C)", y = "Rate (g/g/d)",
       title = "500 g fish, p = 0.5",
       subtitle = "Shaded = scope for growth") +
  theme_bw() +
  theme(legend.position = "bottom", panel.grid = element_blank())