Plant photoreceptors

Data and calculations

Photobiology examples

Pedro J. Aphalo






Explanations and example R code for computations related to the light receptor pigments present in plants and the quantification and manipulation of the amount and quality of light in ways relevant to research on plants and to the cultivation of plants under artificial or modified natural illumination. The R codes makes use of packages in the R for Photobiology suite.


photobiology pkg, photobiologyPlants pkg


As of 2023-07-24 you may get some spurious warnings caused by a pending package update that I will release in a few weeks time. For rendering this page I have silenced warnings in some code chunks.

1 Introduction

The R for Photobiology suite of packages contains data and tools useful in teaching and research. It includes spectral data for the different families of plant photoreceptors, and in the case of phytochromes functions for the calculation of reaction rates as well as the photoequilibrium.

This page is based on a part of the User Guide of package ‘photobiologyPlants’ but contains additional examples that make use of packages ‘photobiologyLEDs’ and ‘ggrepel’.

For other photoreceptors currently only spectral data is included in the suite. The examples below demonstrate some possible uses. For the all examples to run without warnings the most recent releases of the packages are recommended, and ‘photobiologyPlants’ (>= 0.4.3) is needed. Original data sources are given in the packages documentation and not repeated here.

2 Phytochrome

Phytochromes are photochromic pigments that interconvert between two stable forms, Pr and Pfr through the action of red and far-red photons.

2.1 Photoequilibrium

In the examples below we use the solar spectral data included in package ‘photobiology’ as a data frame in object sun.spct.

We can calculate the phytochrome photoequilibrium from spectral irradiance data contained in a source_spct object as follows.

[1] 0.68341

We can also calculate the red:far-red photon ratio, in this second example, for the same spectrum as above

[1] "q:q ratio"

which is equivalent to calculating it using package ‘photobiology’ directly, with wavebands 10 nm wide centred on 660 nm and 735 nm.

q_ratio(sun.spct, Red("Smith10"), Far_red("Smith10"))
[1] "q:q ratio"

We can, and should whenever spectral data are available, calculate the photoequilibrium as above, directly from these data. It is possible to obtain and approximation in case of the solar spectrum and other broad spectra, using the red:far-red photon ratio. The calculation, however, is only strictly valid, for di-chromatic illumination with 660 nm red plus 735 nm far-red light.

[1] 0.7051691

Here we calculated the R:FR ratio from spectral data, but in practice one would use this function only when spectral data is not available as when a R plus FR sensor is used. We can see that in such a case the photoequilibrium calculated is only a rough approximation. For sunlight, in the example above when using spectral data we obtained a value of 0.683 in contrast to 0.705 when using the R:FR photon ratio. For other light sources differences can be much larger. Furthermore, here we used the true R:FR ratio calculated from the spectrum, while broad-band red:far-red sensors give only an approximation, which is good for sunlight, but which will be inaccurate for artificial light, unless a special calibration is done for each type of lamp.


To test how big is the error introduced by estimating Pfr:Ptot from the R:FR photon ratio we calculate Pfr:Ptot using both approaches for sunlight and seven different LEDs sold for plant cultivation. We start by plotting the spectra.

some_leds.mspct <- leds.mspct[plant_grow_leds]
some_leds.mspct[["sun"]] <- sun.spct
some_leds.mspct <- normalize(some_leds.mspct)
autoplot(some_leds.mspct, idfactor = "LED", span = 101, facets = 2)

We then compute the four ratios for each spectrum and collect them into a table. Kusuma and Bugbee (2021) proposed the use of FR/(R + FR) as an easier to understand quantity varying between 0 and 1. It can be computed as 1 / (1 + R:FR), so we also include this so called “FR fraction” in the table.

summaries.tb <- R_FR(some_leds.mspct)
summaries.tb[["FR:(R+FR)[q:q]"]] <- 1 / (1 + summaries.tb[["R:FR[q:q]"]])
summaries.tb[["Pfr:Ptot (spectrum)"]] <-
  msdply(some_leds.mspct, Pfr_Ptot)[[2]]
summaries.tb[["Pfr:Ptot (ratio)"]] <-
names(summaries.tb)[1] <- "LED or daylight"
knitr::kable(summaries.tb[order(summaries.tb$`Pfr:Ptot (spectrum)`), ], 
             digits = 2)
LED or daylight R:FR[q:q] FR:(R+FR)[q:q] Pfr:Ptot (spectrum) Pfr:Ptot (ratio)
sun 1.27 0.44 0.68 0.71
Ledguhon_10WBVGIR14G24_Y6C_T4 1.73 0.37 0.73 0.74
Osram_GW_CSSRM3.HW 5.69 0.15 0.76 0.83
Ledguhon_10WBVG14G24_Y6C_T4 3.14 0.24 0.76 0.79
Luminus_CXM_14_HS_12_36_AC30 4.14 0.19 0.78 0.81
Nichia_NFSW757G_Rsp0a 4.72 0.17 0.79 0.82
LCFOCUS_LC_10FSCOB1917_4000 4.93 0.17 0.80 0.82
Nichia_NFSL757GT_Rsp0a 4.71 0.18 0.80 0.82

To make the estimation errors easier to appreciate we plot Pfr:Ptot computed from the R:FR ratios vs. Pfr:Ptot computed from the spectra.

ggplot(summaries.tb, aes(x = `Pfr:Ptot (spectrum)`, 
                         y = `Pfr:Ptot (ratio)`, 
                         label = paste("R:FR =", round(`R:FR[q:q]`, 2)),
                         colour = ifelse(`LED or daylight` == "sun",
                                         "sun", "LED"))) +
  geom_point() +
  geom_abline(linetype = "dashed") +
  geom_text_repel(min.segment.length = 0, size = 3, show.legend = FALSE) +
  labs(colour = "Light source") +

We can see that in all cases Ptot:Pfr is overestimated when computed from the R:FR photon ratio. On the other hand the computation of Ptot:Pfr is based on the incident spectrum instead of that inside the plant and based on properties of phytochromes measured in vitro. The Osram LED deviates the most, but it is not intended to be used by itself, as it emits almost no red light.

In the case of monochromatic light we can still use the same functions, as the defaults are such that we can use a single value as the ‘w.length’ argument, to obtain the Pfr:P ratio. For monochromatic light, irradiance is irrelevant for the photoequilibrium (steady-state).

[1] 0.869649
[1] 0.01749967
Pfr_Ptot(c(660, 735))
[1] 0.86964902 0.01749967
[1] 0.3859998

We can also plot Pfr:Ptot as a function of wavelength (nm) of monochromatic light. The default is to return a vector for short input vectors, and a response_spct object otherwise, but this can be changed through argument spct.out.

autoplot(Pfr_Ptot(300:770), norm = NULL, unit.out = "photon", = Plant_bands(),
         annotations = c("", "labels", "boxes", "peaks", "valleys"),
         span = 31) +
  labs(y = "Phytochrome photoequilibrium, Pfr:Ptot ratio")

It is, of course, also possible to use base R plotting functions, or as shown here functions from package ‘ggplot2’

ggplot(data = Pfr_Ptot(300:770), aes(w.length, s.q.response)) +
  geom_line() +
  labs(x = "Wavelength (nm)",
       y = "Phytochrome photoequilibrium, Pfr:Ptot ratio")

In the case of dichromatic illumination with red (660 nm) and far-red (730 nm) light, we can use a different function that takes the R:FR photon ratio as argument.

These computations are valid only for true mixes of light at these two wavelengths but not valid for broad spectra like sunlight and especially inaccurate for plant growth lamps with peaks in their output spectrum, such as most discharge lamps (sodium, mercury, multi-metal, fluorescent tubes) and many LED lamps.

[1] 0.6919699
[1] 0.04747996
[1] 0.69196990 0.04747996

It is also easy to plot Pfr:P ratio as a function of R:FR photon ratio. However we have to remember that such values are exact only for dichromatic light, and only a very rough approximation for wide-spectrum light sources. For wide-spectrum light sources, the photoequilibrium should, if possible, be calculated from spectral irradiance data. <- data.frame( = seq(0.01, 10.0, length.out = 100), 
                       Pfr.p = numeric(100))$Pfr.p <- Pfr_Ptot_R_FR($
ggplot(data =, aes(, Pfr.p)) +
  geom_line() +
    labs(x ="R:FR photon ratio",
         y = "Phytochrome photoequilibrium, Pfr:Ptot ratio")

As mentioned in the callout above, Kusuma and Bugbee (2021) proposed the use of FR/(R + FR) as an easier to understand quantity varying between 0 and 1. It can be computed as 1 / (1 + R:FR), so we plot Pfr;Ptot also as a function of this “FR fraction”.

ggplot(data =, aes(1 / (1 +, Pfr.p)) +
  geom_line() +
    labs(x ="FR photon fraction, FR:(R+FR)",
         y = "Phytochrome photoequilibrium, Pfr:Ptot ratio")

We see, indeed, that the relationship is in this case less asymptotic, so not only the range 0 to 1 is easier to interpret but can be expected to better reflect plant responses as shown by Kusuma and Bugbee (2021).


As red and far-red LEDs do not emit monochromatic ligth, we may want to check the photoequilibrium based on the spectrum.

RFR_leds.mspct <- leds.mspct[unique(grep("^Quantum", c(red_leds, ir_leds), value = TRUE))]
RFR_leds.mspct <- normalize(RFR_leds.mspct)
autoplot(RFR_leds.mspct, idfactor = "LED", span = 101, facets = 2)

summaries.tb <- R_FR(RFR_leds.mspct)
summaries.tb[["spct.idx"]] <- gsub("_", " ", summaries.tb[["spct.idx"]])
summaries.tb[["FR:(R+FR)[q:q]"]] <- 1 / (1 + summaries.tb[["R:FR[q:q]"]])
summaries.tb[["Pfr:Ptot (spectrum)"]] <-
  msdply(RFR_leds.mspct, Pfr_Ptot)[[2]]
summaries.tb[["Pfr:Ptot (nominal peak)"]] <- Pfr_Ptot(c(660, 680, 700, 735))
knitr::kable(summaries.tb, digits = 3)
spct.idx R:FR[q:q] FR:(R+FR)[q:q] Pfr:Ptot (spectrum) Pfr:Ptot (nominal peak)
QuantumDevices QDDH66002 2431.821 0.000 0.862 0.870
QuantumDevices QDDH68002 216.396 0.005 0.821 0.799
QuantumDevices QDDH70002 0.155 0.866 0.370 0.297
QuantumDevices QDDH73502 0.002 0.998 0.038 0.017

2.2 Reaction rates

To compute actual reactions rates we have to use actual spectral irradiances. Using normalized spectra gives wrong results as the reaction rates depend on the flow rate of photons of different wavelengths. This is different to the R:FR photon ratio and the Pfr:Ptot ratio at the photoequilibrium which depend on the relative abundance of photons of different wavelengths, as the assumption is that expossure has been enough for steady-state photoequilibrium.

with(clip_wl(sun.spct, c(300,770)), 
     Phy_reaction_rates(w.length, s.e.irrad))
[1] 1.25935

[1] 0.5833947

[1] 1.842745

2.3 Absorption cross section vs. wavelength

The phytochrome photoequilibrium cannot be calculated from the absorptance spectra of Pr and Pfr, because Pr and Pfr have different quantum yields for the respective phototransformations. We need to use action spectra, which in this context are usually called ‘absorption cross-sections’. They can be calculated as the product of absorptance and quantum yield. The values in these spectra, in the case of Phy are called `Sigma’.

Here we reproduce Figure 3 in Mancinelli (1994), which gives the ‘Relative photoconversion cross-sections’ of Pr (\(\sigma_R\)) and Pfr (\(\sigma_{FR}\)). The values are expressed relative to \(\sigma_R\) at its maximum at \(\lambda = 666\) nm. <- 
  data.frame(w.length=seq(300, 770, length.out=100))$sigma.r <- Phy_Sigma_R($w.length)$ <- Phy_Sigma_FR($w.length)$sigma <- Phy_Sigma($w.length)
ggplot(, aes(x = w.length)) +
  geom_line(aes(y = sigma.r/ max(sigma.r)), colour = "red") +
  geom_line(aes(y = max(sigma.r))) +
  labs(x = "Wavelength (nm)", 
       y = expression(sigma[R]~"and"~sigma[FR]))

rm( # clean up

3 Cryptochromes

The absorbance spectrum of crytpchromes has been shown to change upon exposure to light and its dynamics described. However, while for phytochromes rather good data are available both for absorption and quantum efficiency, for the other families of plant photoreceptors informations is not as readily available.

3.1 Spectral absorbance

[1] "CRY1_dark"  "CRY1_light" "CRY2_dark"  "CRY2_light" "CRY3_dark" 

Here we approximate Figure 1.B from Banerjee et al. (2007).

autoplot(interpolate_wl(CRYs.mspct$CRY2_dark, 300:500), 
         span = 31, annotations = c("+", "valleys"))

autoplot(CRYs.mspct[c("CRY2_dark", "CRY2_light")], 
         range = c(300,700), span = 51, annotations = c("+", "valleys")) +
  theme(legend.position = "bottom")
Warning in normalize_spct(spct = T2A(x, action = "replace.raw"), range = range,
: Object contains data for 2 spectra; skipping normalization

autoplot(CRYs.mspct[c("CRY1_dark", "CRY1_light")], 
         span = 51, annotations = c("+", "valleys")) +
  theme(legend.position = "bottom")
Warning in normalize_spct(spct = T2A(x, action = "replace.raw"), range = range,
: Object contains data for 2 spectra; skipping normalization

autoplot(CRYs.mspct["CRY3_dark"], range = c(300,700),
         span = 31, annotations = c("+", "valleys"))

ggplot(CRYs.mspct[c("CRY1_dark", "CRY2_dark", "CRY3_dark")]) +
  geom_line(aes(linetype = spct.idx)) +
  expand_limits(x = 300) +
  theme(legend.position = "bottom")

4 Phototropins

4.1 Spectral absorbance

[1] "PHOT1_fluo"  "PHOT2_fluo"  "PHOT1_dark"  "PHOT1_light"
autoplot(PHOTs.mspct[c("PHOT1_fluo", "PHOT2_fluo")],
         span = 31, annotations = c("+", "valleys")) +
  expand_limits(x = 300) +
  theme(legend.position = "bottom")
Warning in normalize_spct(spct = T2A(x, action = "replace.raw"), range = range,
: Object contains data for 2 spectra; skipping normalization

autoplot(PHOTs.mspct[c("PHOT1_dark", "PHOT1_light")], 
         span = 21, annotations = c("+", "valleys")) +
  theme(legend.position = "bottom")
Warning in normalize_spct(spct = T2A(x, action = "replace.raw"), range = range,
: Object contains data for 2 spectra; skipping normalization

5 UVR8

5.1 Spectral absorbance

[1] "UVR8.abs.Glasgow" "UVR8.abs.Orebro" 
autoplot(UVR8s.mspct, span = 51, annotations = c("+", "valleys"))
Warning in normalize_spct(spct = T2A(x, action = "replace.raw"), range = range,
: Object contains data for 2 spectra; skipping normalization

6 Zeitloupe proteins

6.1 Spectral absorbance

[1] "ZTL_dark"  "ZTL_light"
autoplot(ZTLs.mspct, span = 21, annotations = c("+", "valleys")) +
  expand_limits(x = 300) +
  theme(legend.position = "bottom")
Warning in normalize_spct(spct = T2A(x, action = "replace.raw"), range = range,
: Object contains data for 2 spectra; skipping normalization