R for Photobiology

Difficult Computations Made Easy

Pedro J. Aphalo

2026-04-12

1 Introduction

1.1 Why and how did I develop the packages?

  • Because I am lazy, but mostly in the long term
    • … I like efficiency + reliability
  • Approach: write code well once and use many times
  • Document: for future me and for others
  • Test cases: avoid errors now and in the future
  • Write clear code: make enhancements and corrections easy

1.2 Did it work out as I expected?

  • Expected: In the long run I did save time and effort
  • Expected: Became easier to work with large data sets
  • Expected: Other researchers are using the packages
  • Unexpected: As it made calculations a lot easier
  • … I did “what if” calculations more frequently
  • … leading to new insights and original research
  • Unexpected: Citations!

1.3 Why did I use R?

  • I had been using R for > 10 years
    • … as it is the best language for statistics
  • The logic of R fits well with how my brain works
    • … it feels natural to me (not necessarily to you)
  • R had the best plotting approach available
    • … and I was familiar with it
  • The package system of R is very well structured

1.4 Side note on R

1.5 Design aims for my 3rd attempt

I made bad design decisions in my first two attempts at writing an R package for photobiology. My third attempt and current packages have as aims:

  • Avoid accidental loss of information
  • Automate all that can be automated
  • Allow customizations
  • Store metadata together with the data
  • (Fast computations on large data)

1.6 Design aims in detail

  • Automate all that can be automated
  • Allow customizations
  • Store metadata together with the data
  • Avoid accidental loss of information

1.7 Design aims in detail

  • Automate all that can be automated
    • Known units and bases of expression
    • Provide good defaults, but allow overriding them
  • Allow customizations
  • Store metadata together with the data
  • Avoid accidental loss of information

1.8 Design aims in detail

  • Automate all what can be automated
  • Allow customizations
    • Open source code
    • Export “building blocks” to allow extensions
  • Store metadata together with the data
  • Avoid accidental loss of information

1.9 Design aims in detail

  • Automate all that can be automated
  • Allow customizations
  • Store metadata together with the data
    • What was measured
    • When was it measured
    • Where was it measured
    • How was it measured
    • Store a trace of operations applied
  • Avoid accidental loss of information

1.10 Design aims in detail

  • Automate all that can be automated
  • Allow customizations
  • Store metadata together with the data
  • Avoid accidental loss of information
    • Use detector pixel wavelengths
    • Keep history of operations on the data
    • (Support undoing of scaling and normalizations)

1.11 Some recent enhancements

  • Accurate day and night lengths and position of the sun back in history and into the future
  • Support for long time series of spectra
  • Fast acquisition of spectra with Ocean Optics spectrometers
  • Faster computations: possible to compute summaries for many 1000s of spectra

1.12 Packages

+/-
library(photobiology)
library(photobiologyWavebands)
library(photobiologyPlants)
library(photobiologyLEDs)
library(ggspectra)
set_theme(theme_bw(14))

2 Basic concepts

2.1 Light spectra \(f(\lambda)\)

Figure 1: Handling of spectral data. Multiple entry and exit points.

2.2 Spectra: energy vs. photons (= quanta)

  • Energy irradiance \(E_\lambda\) in \(W\,m^{-2}\,nm^{-1}\)
  • Photon irradiance \(Q_\lambda\) in \(mol\,s^{-1}\,m^{-2}\,nm^{-1}\)
  • … conversion based on energy of one mole of photons \[q^\prime = h\cdot N_\mathrm{A} \cdot \frac{c}{\lambda}\] where \(\lambda\) is wavelength, and \(c\), speed of light, \(h\), Planck’s constant and \(N_\mathrm{A}\) Avogadro’s number are constants.
  • functions e2q() and q2e() in R package ‘photobiology’

3 Plots

3.1 Data and functions used for plots

  • Spectral data from package ‘photobiology’
    • sun.spct spectral irradiance
    • sun_daily.spct spectral daily exposure
  • Methods from package ‘ggspectra’ (‘ggplot2’)
    • autoplot()

3.2 Sunlight: energy

+/-
autoplot(sun.spct, unit.out = "energy")

3.3 Sunlight: photons

+/-
autoplot(sun.spct, unit.out = "photon")

3.4 Spectrum plot: playground

3.5 Sunlight daily: photons

+/-
autoplot(sun_daily.spct, unit.out = "photon")

4 Irradiance

4.1 Data and functions used for plots

  • Spectral data from package ‘photobiology’
    • sun.spct spectral irradiance
    • sun_daily.spct spectral daily exposure
  • Methods from package ‘photobioloy’
    • e_irrad()
    • q_irrad()
    • waveband()

4.2 Irradiance: energy vs. photons (= quanta)

  • Energy irradiance \(E\) in \(W\,m^{-2}\)
  • Photon irradiance \(Q\) in \(mol\,s^{-1}\,m^{-2}\)
  • … no direct conversion possible!
  • \(\to\) compute \(E\) and \(Q\) from spectrum

4.3 (Energy) Irradiance \(E\)

\[E = \int_{\lambda_1}^{\lambda_2} E_\lambda\ d\,\lambda\] where the wavelength range \(\lambda_1\) to \(\lambda_1\) is the waveband to integrate.

4.4 (Energy) Irradiance \(E\)

+/-
# VIS in W m-2
e_irrad(sun.spct, waveband(c(400, 500))) # a number with metadata
E_range.400.500 
       69.69043 
attr(,"time.unit")
[1] "second"
attr(,"radiation.unit")
[1] "total energy irradiance"
+/-
# VIS in W m-2
as.numeric(e_irrad(sun.spct, waveband(c(400, 500)))) # a plain number
[1] 69.69043

4.5 Photon Irradiance (=PFD) \(Q\)

\[Q = \int_{\lambda_1}^{\lambda_2} Q_\lambda\ d\,\lambda\] where the wavelength range \(\lambda_1\) to \(\lambda_1\) is the waveband to integrate.

4.6 Photon Irradiance (=PFD) \(Q\)

+/-
# PAR in mol m-2 s-1
q_irrad(sun.spct, waveband(c(400, 500)))
Q_range.400.500 
   0.0002633524 
attr(,"time.unit")
[1] "second"
attr(,"radiation.unit")
[1] "total photon irradiance"
+/-
# PAR in umol m-2 s-1
q_irrad(sun.spct, waveband(c(400, 500)), scale.factor = 1e6)
Q_range.400.500 
       263.3524 
attr(,"time.unit")
[1] "second"
attr(,"radiation.unit")
[1] "total photon irradiance"
+/-
# PAR in mol m-2 d-1
q_irrad(sun_daily.spct,waveband(c(400, 500)))
Q_range.400.500 
       10.46503 
attr(,"time.unit")
[1] "day"
attr(,"radiation.unit")
[1] "total photon irradiance"

4.7 Irradiance and PFD: Wavebands

4.8 Wavelength ranges by name

Multiple definitions in use!

+/-
q_irrad(sun.spct, 
        list(Red("Warrington"), Red("Smith20"),
             Red("Sellaro"), Red("Apogee")), 
        scale.factor = 1e6, return.tb = TRUE)
# A tibble: 1 × 4
  Q_Red.Warrington Q_Red.Smith20 Q_Red.Sellaro Q_Red.Apogee
             <dbl>         <dbl>         <dbl>        <dbl>
1             160.          63.6          192.         62.4
+/-
wl_range(Red("Warrington"))
[1] 625 675
+/-
wl_range(Red("Smith20"))
[1] 650 670
+/-
wl_range(Red("Sellaro"))
[1] 620 680
+/-
wl_range(Red("Apogee"))
[1] 645 665

4.9 Wavelength ranges by name

4.10 Multiple wavelength ranges

4.11 Spectral weighting functions

Energy irradiance in 400 to 700 nm is not PAR, even if meteorologists call it PAR. I use PhR instead.

+/-
# PhR in W m-2
e_irrad(sun.spct, PhR())
   E_PhR 
196.6343 
attr(,"time.unit")
[1] "second"
attr(,"radiation.unit")
[1] "total energy irradiance"
+/-
# PAR in umol m-2 s-1
q_irrad(sun.spct, PAR(), scale.factor = 1e6)
   Q_PAR 
894.1483 
attr(,"time.unit")
[1] "second"
attr(,"radiation.unit")
[1] "total photon irradiance"

4.12 Spectral weighting functions

4.13 Multiple spectra

5 Ratios

5.1 Ratios: energy vs. photons (= quanta)

  • Energy ratio \(E:E\) \(\neq\) photon ratio \(Q:Q\)
  • Unitless
  • shape of the spectrum matters
  • … no direct conversion possible!
    • compute from spectrum
    • functions e_ratio() and q_ratio()
    • functions eq_ratio() and qe_ratio()

5.2 Photon Ratios

+/-
# R:FR according to Harry Smith's 20 nm definition
R_FR(sun.spct)
R:FR[q:q] 
 1.242474 
attr(,"radiation.unit")
[1] "q:q ratio"
+/-
# R:FR according to Harry Smith's 20 nm definition
q_ratio(sun.spct, Red("Smith20"), Far_red("Smith20"))
R:FR[q:q] 
 1.242474 
attr(,"radiation.unit")
[1] "q:q ratio"
+/-
# B:R according to Sellaro's wider definition
q_ratio(sun.spct, Blue("Sellaro"), Red("Sellaro"))
Blue:Red[q:q] 
    0.9840123 
attr(,"radiation.unit")
[1] "q:q ratio"
+/-
# R:FR according to Harry Smith's narrow definition
q_ratio(sun.spct, Red("Smith10"), Far_red("Smith10"))
R:FR[q:q] 
 1.266704 
attr(,"radiation.unit")
[1] "q:q ratio"
+/-
# R:FR according to Harry Smith's narrow definition
R_FR(sun.spct, "Smith10")
R:FR[q:q] 
 1.266704 
attr(,"radiation.unit")
[1] "q:q ratio"

6 Phytochrome photoequilibrium

Can be estimated from: - the wavelength of single colour light - from the spectrum of the light - for a mix of red and far red light

6.1 Single wavelength

+/-
Pfr_Ptot(660)
[1] 0.869649
+/-
round(
  Pfr_Ptot(c(435, 460, 530, 630, 660, 710, 735)),
  2)
[1] 0.39 0.44 0.68 0.86 0.87 0.10 0.02

6.2 Spectrum

+/-
round(Pfr_Ptot(sun.spct), 3)
[1] 0.683

7 Daylength and sun position

7.1 Daylength in Beijing

+/-
CAAS.Beijing <- data.frame(lat = 39.9596, 
                           lon = 116.3188)
day_length(geocode = CAAS.Beijing)
[1] 13.47349
+/-
day_length(date = c(as.POSIXct("2025-12-21"),
                    as.POSIXct("2026-06-21")),
           geocode = CAAS.Beijing)
[1]  9.329439 15.009919

7.2 Current position of the sun

8 Operations on spectra

  • *, +, etc.
  • simulate effect of a filter
  • mix light sources
  • estimate response

8.1 Sunlight below a yellow filter

+/-
filtered_sun.spct <- sun.spct * yellow_gel.spct
autoplot(filtered_sun.spct, unit.out = "photon")

8.2 Two LED spectra

Normalized spectra so that \(\mathrm{max} Q_\lambda = 1\).

+/-
autoplot(leds.mspct[c("Nichia_NF2W757GT_F1_sm505_Rfc00",
                      "LedEngin_LZ1_10R302_740nm")],
  unit.out = "photon", norm = "max") +
  theme(legend.position = "none")

8.3 Combine two LEDs

Spectrum with 200 \(\mu mol m^{-2} s^{-1}\) from white Nichia Optisolis LED and 20 \(\mu mol m^{-2} s^{-1}\) from an Ledengin-OSRAM fra-red LED.

+/-
white_led_200 <-
  fscale(leds.mspct$Nichia_NF2W757GT_F1_sm505_Rfc00, 
         f = q_irrad, 
         target = 200e-6, 
         w.band = PAR())

far_red_20 <-
  fscale(leds.mspct$LedEngin_LZ1_10R302_740nm,
         f = q_irrad, 
         target = 20e-6)

combined.spct <- white_led_200 + far_red_20

8.4 Plot computed spectrum

+/-
autoplot(combined.spct, unit.out = "photon")

8.5 PAR and ePAR

+/-
q_irrad(combined.spct, 
        list(PAR(), PAR("ePAR")),
        scale.factor = 1e6, return.tb = TRUE)
# A tibble: 1 × 2
  Q_PAR Q_ePAR
  <dbl>  <dbl>
1  202.   229.
+/-
q_irrad(white_led_200,
        list(PAR(), PAR("ePAR")),
        scale.factor = 1e6, return.tb = TRUE)
# A tibble: 1 × 2
  Q_PAR Q_ePAR
  <dbl>  <dbl>
1   200   210.

8.6 Red to far-red photon ratio (R:FR)

+/-
R_FR(combined.spct)
R:FR[q:q] 
 1.104595 
attr(,"radiation.unit")
[1] "q:q ratio"
+/-
R_FR(white_led_200)
R:FR[q:q] 
  4.62355 
attr(,"radiation.unit")
[1] "q:q ratio"

8.7 Phytochrome photoequilibrium

+/-
Pfr_Ptot(combined.spct)
[1] 0.7181451
+/-
Pfr_Ptot(white_led_200)
[1] 0.7776205

9 Thanks!

  • Thanks for listening today!
  • Thanks for looking after me so well while in Beijing!
  • Thanks for opening the doors of your group to me!

9.1 Resources

This presentation is a “web page” and the editable ‘.qmd’ markdown source file is at github.