Fitting light response curves

Preferred version (photosynthesis >= 2.1.1)

This package currently only implements the Marshall & Biscoe (1980) non-rectangular hyperbola model of the photosynthetic light response.

Fit the light-response curve using nonlinear least-squares (nls)

library(broom)
library(dplyr)
library(photosynthesis)

# Read in your data
dat = system.file("extdata", "A_Ci_Q_data_1.csv", package = "photosynthesis") |>
  read.csv() |>
  # Set grouping variable
  mutate(group = round(CO2_s, digits = 0)) |>
  # For this example, round sequentially due to CO2_s set points
  mutate(group = as.factor(round(group, digits = -1)))

# Fit one light-response curve
fit = fit_photosynthesis(
  .data = filter(dat, group == 600),
  .photo_fun = "aq_response",
  .vars = list(.A = A, .Q = Qabs),
)

# The 'fit' object inherits class 'nls' and many methods can be used

## Model summary:
summary(fit)
## 
## Formula: .A ~ marshall_biscoe_1980(Q_abs = .data[[".Qabs"]], k_sat, phi_J, 
##     theta_J) - Rd
## 
## Parameters:
##          Estimate Std. Error t value Pr(>|t|)    
## k_sat   21.170337   0.154428  137.09 1.70e-08 ***
## phi_J    0.061543   0.001218   50.52 9.19e-07 ***
## theta_J  0.775752   0.014526   53.40 7.36e-07 ***
## Rd       0.666320   0.063490   10.49 0.000466 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 0.0539 on 4 degrees of freedom
## 
## Number of iterations to convergence: 5 
## Achieved convergence tolerance: 1.49e-08
## Estimated parameters:
coef(fit)
##       k_sat       phi_J     theta_J          Rd 
## 21.17033712  0.06154347  0.77575157  0.66631983
## 95% confidence intervals:
confint(fit)
##                2.5%       97.5%
## k_sat   20.75337772 21.60612987
## phi_J    0.05835624  0.06501655
## theta_J  0.73285635  0.81306156
## Rd       0.49523165  0.84311606
## Tidy summary table using 'broom::tidy()'
tidy(fit, conf.int = TRUE, conf.level = 0.95)
## # A tibble: 4 × 7
##   term    estimate std.error statistic      p.value conf.low conf.high
##   <chr>      <dbl>     <dbl>     <dbl>        <dbl>    <dbl>     <dbl>
## 1 k_sat    21.2      0.154       137.  0.0000000170  20.8      21.6   
## 2 phi_J     0.0615   0.00122      50.5 0.000000919    0.0584    0.0650
## 3 theta_J   0.776    0.0145       53.4 0.000000736    0.733     0.813 
## 4 Rd        0.666    0.0635       10.5 0.000466       0.495     0.843
## Calculate light compensation point
coef(fit) |>
  t() |>
  as.data.frame() |>
  mutate(LCP = ((Rd) * (Rd * theta_J - k_sat) / (phi_J * (Rd - k_sat)))) |>

## Calculate residual sum-of-squares
sum(resid(fit) ^ 2)
## [1] 33.59129

Plot model fit and raw data

The deprecated function fit_aq_response() generated a figure automatically, but it used geom_smooth() rather than plotting the model fit. We now prefer to use generic methods from the package ggplot2 and plot the fitted curve. This allows users the ability to more easily customize their figures.

library(ggplot2)

b = coef(fit)

df_predict = data.frame(Qabs = seq(0, 0.84 * 1500, length.out = 100)) |>
  mutate(
    A = marshall_biscoe_1980(
      Q_abs = Qabs,
      k_sat = b["k_sat"],
      b["phi_J"],
      b["theta_J"]
    ) - b["Rd"]
  )

ggplot(mapping = aes(Qabs, A)) +
  geom_line(data = df_predict) +
  geom_point(data = filter(dat, group == 600)) +
  labs(
    x = expression("Irradiance (" * mu * mol ~ m^{-2} ~ s^{-1} * ")"),
    y = expression(A[net] ~ "(" * mu * mol ~ m^{-2} ~ s^{-1} * ")")
  ) +
  theme_bw()

Example of fitted light-response curve

Fit multiple curves with photosynthesis and purrr

In the previous version, we used fit_many() to fit many light-response curves simultaneously. We now prefer to use generic methods from the package purrr that are already pretty good.

library(purrr)

fits = dat |>
  split(~ group) |>
  map(fit_photosynthesis, .photo_fun = "aq_response", .vars = list(.A = A, .Q = Qabs))

## Estimated parameters:
fits |>
  map(coef) |>
  map(t) |>
  map(as.data.frame) |>
  imap_dfr(~ mutate(.x, CO2_s = .y))
##        k_sat       phi_J   theta_J          Rd CO2_s
## 1 -0.1832198 0.004189778 0.2428476 -0.09209662    50
## 2  2.6370733 0.017295931 0.8858139  0.59492313   100
## 3  7.3481571 0.032255707 0.8376462  0.61479141   200
## 4 11.4687982 0.045046336 0.7898826  0.76746932   300
## 5 16.4505827 0.057537264 0.6997933  0.89736506   410
## 6 21.1703371 0.061543474 0.7757516  0.66631983   600
## 7 23.9305140 0.067744069 0.7514331  0.78493847   820
## 8 25.0686239 0.068975627 0.7632357  0.58529680  1200
## 9 25.6413999 0.071556328 0.7411443  0.45326420  1600

Fit Bayesian light-response curves with brms and Stan

Traditional model fitting use a nonlinear least-squares approach, but Bayesian methods have some advantages, especially with more complex data sets. We added an option to fit a single Bayesian light-response curve using the amazing brms package which fits models in Stan. We have not implemented more complex approaches (e.g. multilevel light-response models) because once you are doing that, it’s probably easier to code the model directly into brms functions. Hopefully this code can get you started though. We have not run the example below, but copy-and-paste into your R Console to try.

fit = fit_photosynthesis(
  .data = filter(dat, group == 600),
  .photo_fun = "aq_response",
  .vars = list(.A = A, .Q = Qabs),
  .method = "brms",
  brm_options = list(chains = 1)
)

# The 'fit' object inherits class 'brmsfit' and many methods can be used
summary(fit)

Fit the light-response curve with photoinhibition using nonlinear least-squares (nls)

library(broom)
library(dplyr)
library(tibble)
library(photosynthesis)

# Simulate data
set.seed(202411123)

## Parameters 
k_sat = 30
phi_J = 0.05
theta_J = 0.8
Rd = 3
b_inh = 0.003

## Vector of light values
Q = c(0, 25, 50, 75, 100, 125, 150, 200, 300, 400, 500, 600, 700, 800, 1000, 1200, 1400, 1600, 1800, 2000)

dat = tibble(
  Qabs = 0.84 * Q,
  A = photoinhibition(Qabs, k_sat, phi_J, theta_J, b_inh) - Rd + rnorm(length(Qabs), 0, 0.1)
)

# Fit one light-response curve
fit = fit_photosynthesis(
  .data = dat,
  .photo_fun = "aq_response",
  .model = "photoinhibition",
  .vars = list(.A = A, .Q = Qabs),
)

# The 'fit' object inherits class 'nls' and many methods can be used

## Model summary:
summary(fit)
## 
## Formula: .A ~ photoinhibition(Q_abs = .data[[".Qabs"]], k_sat, phi_J, 
##     theta_J, b_inh) - Rd
## 
## Parameters:
##          Estimate Std. Error t value Pr(>|t|)    
## k_sat   3.023e+01  6.477e-01  46.671  < 2e-16 ***
## phi_J   5.023e-02  6.119e-04  82.087  < 2e-16 ***
## theta_J 8.001e-01  1.681e-02  47.584  < 2e-16 ***
## Rd      3.094e+00  4.595e-02  67.332  < 2e-16 ***
## b_inh   3.108e-03  3.282e-04   9.469 1.02e-07 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 0.07342 on 15 degrees of freedom
## 
## Number of iterations to convergence: 5 
## Achieved convergence tolerance: 1.49e-08
## Estimated parameters:
coef(fit)
##        k_sat        phi_J      theta_J           Rd        b_inh 
## 30.229369916  0.050228967  0.800109337  3.093915835  0.003107636
## 95% confidence intervals:
confint(fit)
##                 2.5%        97.5%
## k_sat   28.911503109 31.701749154
## phi_J    0.048955458  0.051587628
## theta_J  0.760461401  0.833244529
## Rd       2.996235314  3.192817270
## b_inh    0.002436191  0.003847506
## Tidy summary table using 'broom::tidy()'
tidy(fit, conf.int = TRUE, conf.level = 0.95)
## # A tibble: 5 × 7
##   term    estimate std.error statistic  p.value conf.low conf.high
##   <chr>      <dbl>     <dbl>     <dbl>    <dbl>    <dbl>     <dbl>
## 1 k_sat   30.2      0.648        46.7  1.18e-17 28.9      31.7    
## 2 phi_J    0.0502   0.000612     82.1  2.55e-21  0.0490    0.0516 
## 3 theta_J  0.800    0.0168       47.6  8.81e-18  0.760     0.833  
## 4 Rd       3.09     0.0459       67.3  4.94e-20  3.00      3.19   
## 5 b_inh    0.00311  0.000328      9.47 1.02e- 7  0.00244   0.00385

Plot model fit and raw data

library(ggplot2)

b = coef(fit)

df_predict = data.frame(Qabs = seq(0, 0.84 * 2000, length.out = 100)) |>
  mutate(
    A = photoinhibition(
      Q_abs = Qabs,
      k_sat = b["k_sat"],
      b["phi_J"],
      b["theta_J"],
      b["b_inh"]
    ) - b["Rd"]
  )

ggplot(mapping = aes(Qabs, A)) +
  geom_line(data = df_predict) +
  geom_point(data = dat) +
  labs(
    x = expression("Irradiance (" * mu * mol ~ m^{-2} ~ s^{-1} * ")"),
    y = expression(A[net] ~ "(" * mu * mol ~ m^{-2} ~ s^{-1} * ")")
  ) +
  theme_bw()

Deprecated version (photosynthesis <= 2.1.1)

The fit_aq_response() function is the original version, but we are no longer updating it and may phase it out of future releases. Use fit_photosynthesisi(..., .photo_fun = 'aq_response') instead.

library(dplyr)
library(photosynthesis)
# Read in your data
dat = system.file("extdata", "A_Ci_Q_data_1.csv", package = "photosynthesis") |>
  read.csv() |>
  # Set grouping variable
  mutate(group = round(CO2_s, digits = 0)) |>
  # For this example, round sequentially due to CO2_s setpoints
  mutate(group = as.factor(round(group, digits = -1))) |>
  rename(A_net = A, PPFD = Qin)

# To fit one AQ curve
fit = fit_aq_response(filter(dat, group == 600))
## Warning: `fit_aq_response()` was deprecated in photosynthesis 2.1.1.
## ℹ Please use the `.photo_fun` argument of `fit_photosynthesis()` instead.
## This warning is displayed once every 8 hours.
## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
## generated.
## Warning: `aq_response()` was deprecated in photosynthesis 2.1.1.
## ℹ Please use `marshall_biscoe_1980()` instead.
## ℹ The deprecated feature was likely used in the photosynthesis package.
##   Please report the issue at <https://github.com/cdmuir/photosynthesis/issues>.
## This warning is displayed once every 8 hours.
## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
## generated.
# Print model summary
summary(fit[[1]])
## 
## Formula: A_net ~ aq_response(k_sat, phi_J, Q_abs = data$Q_abs, theta_J) - 
##     Rd
## 
## Parameters:
##                 Estimate Std. Error t value Pr(>|t|)    
## k_sat          21.167200   0.158332  133.69 1.88e-08 ***
## phi_J.Q_abs     0.051907   0.001055   49.18 1.02e-06 ***
## theta_J         0.775484   0.014920   51.98 8.20e-07 ***
## Rd.(Intercept)  0.668495   0.065235   10.25 0.000511 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 0.05535 on 4 degrees of freedom
## 
## Number of iterations to convergence: 5 
## Achieved convergence tolerance: 1.49e-08
# Print fitted parameters
fit[[2]]
##         A_sat      phi_J   theta_J        Rd      LCP  resid_SSs
## k_sat 21.1672 0.05190746 0.7754836 0.6684953 12.97289 0.01225491
# Print graph
fit[[3]]

# Fit many curves
fits = fit_many(
  data = dat,
  funct = fit_aq_response,
  group = "group",
  progress = FALSE
)

# Look at model summary for a given fit
# First set of double parentheses selects an individual group value
# Second set selects an element of the sublist
summary(fits[[3]][[1]])
## 
## Formula: A_net ~ aq_response(k_sat, phi_J, Q_abs = data$Q_abs, theta_J) - 
##     Rd
## 
## Parameters:
##                Estimate Std. Error t value Pr(>|t|)    
## k_sat          7.347423   0.141931  51.768 8.33e-07 ***
## phi_J.Q_abs    0.027192   0.001511  17.994 5.61e-05 ***
## theta_J        0.837778   0.030608  27.371 1.06e-05 ***
## Rd.(Intercept) 0.615283   0.086994   7.073  0.00211 ** 
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 0.06799 on 4 degrees of freedom
## 
## Number of iterations to convergence: 4 
## Achieved convergence tolerance: 1.49e-08
# Print the parameters
fits[[2]][[2]]
##          A_sat      phi_J   theta_J        Rd      LCP  resid_SSs
## k_sat 2.637157 0.01458002 0.8858892 0.5951635 42.17813 0.02446394
# Print the graph
fits[[3]][[3]]

#Compile graphs into a list for plotting
fits_graphs = compile_data(fits, list_element = 3)

# Print graphs to jpeg
# print_graphs(data = fits_graphs, path = tempdir(), output_type = "jpeg")

#Compile parameters into data.frame for analysis
fits_pars = compile_data(fits, output_type = "dataframe", list_element = 2)

References

Marshall B, Biscoe P. 1980. A model for C3 leaves describing the dependence of net photosynthesis on irradiance. Journal of Experimental Botany 31:29-39.