Frontmatter

If you are publishing this notebook on the web, you can set the parameters below to provide HTML metadata. This is useful for search engines and social media.

PlantBiophysics.jl benchmark

The main objective of this notebook is to compare the computational times of PlantBiophysics.jl against the plantecophys R package and the LeafGasExchange.jl Julia package from the Cropbox.jl framework. The comparison follows three steps:

  • create an N-large basis of random conditions.

  • benchmark the computational time of the three packages via similar functions (i.e. photosynthesis-stomatal conductance-energy balance coupled model for C3 leaves): energy_balance, photosynEB and simulate with ModelC3MD.

  • compare the results with plots and statistics.

This notebook does not perform the benchmark by itself for the obvious reason that it takes forever to run, and because there is an overhead cause by Pluto (benchmark and reactive are not a good mix). Instead, it shows the outputs of a script from this repository that implements the code shown here. If you want to perform the benchmark by yourself, you can run this script from the command line. The versions used for the above dependencies are available in the Project.toml of the repository.

Importing the dependencies:

Note

Make sure to have R installed on your computer first.

Loading the Julia packages:

begin
    using CairoMakie
	using BenchmarkTools
	using PlantBiophysics
    using Cropbox
    using LeafGasExchange
    using RCall
end
md"""

# PlantBiophysics.jl benchmark

The main objective of this notebook is to compare the computational times of `PlantBiophysics.jl` against the [plantecophys](https://github.com/RemkoDuursma/plantecophys) R package and the [LeafGasExchange.jl](https://github.com/cropbox/LeafGasExchange.jl) Julia package from the [Cropbox.jl](https://github.com/cropbox/Cropbox.jl) framework. The comparison follows three steps:
- create an N-large basis of random conditions.
- benchmark the computational time of the three packages via similar functions (_i.e._ photosynthesis-stomatal conductance-energy balance coupled model for C3 leaves): `energy_balance`, `photosynEB` and `simulate` with `ModelC3MD`.
- compare the results with plots and statistics.

This notebook does not perform the benchmark by itself for the obvious reason that it takes forever to run, and because there is an overhead cause by Pluto (benchmark and reactive are not a good mix). Instead, it shows the outputs of [a script from this repository](https://github.com/VEZY/PlantBiophysics-paper/blob/main/tutorials/Fig5_PlantBiophysics_performance_noPluto.jl) that implements the code shown here. If you want to perform the benchmark by yourself, you can run this script from the command line. The versions used for the above dependencies are available in the [Project.toml](https://github.com/VEZY/PlantBiophysics-paper/blob/main/tutorials/Project.toml) of the repository.


## Importing the dependencies:

###### Note
Make sure to have R installed on your computer first.

Loading the Julia packages:

```julia
begin
using CairoMakie
using BenchmarkTools
using PlantBiophysics
using Cropbox
using LeafGasExchange
using RCall
end
```
"""
51.5 ms
begin
using Statistics
using DataFrames
using CSV
using Random
using PlantBiophysics
using Downloads
end
6.8 s

Parameters

Benchmark parameters

You'll find below the main parameters of the benchmark. In a few words, each package runs a simulation for N different time-steps microbenchmark_steps times repeated microbenchmark_evals times. We make N different simulations because the simulation duration can vary depending on the inputs due to iterative computations in the code, i.e. different initial conditions can make the algorithms converge more or less rapidly.

md"""

## Parameters

### Benchmark parameters

You'll find below the main parameters of the benchmark. In a few words, each package runs a simulation for `N` different time-steps `microbenchmark_steps` times repeated `microbenchmark_evals` times. We make `N` different simulations because the simulation duration can vary depending on the inputs due to iterative computations in the code, *i.e.* different initial conditions can make the algorithms converge more or less rapidly.
"""
18.0 ms
100
begin
Random.seed!(1) # Set random seed
microbenchmark_steps = 100 # Number of times the microbenchmark is run
microbenchmark_evals = 1 # N. times each sample is run to be sure of the output
N = 100 # Number of timesteps simulated for each microbenchmark step
end
81.9 ms

Random input simulation dataset

We create possible ranges for input parameters. These ranges where chosen so all of the three packages don't return errors during computation (plantecophys has issues with low temperatures).

  • Ta: air temperature ($°C$)

  • Wind: wind speed ($m.s^{-1}$)

  • P: ambient pressure ($kPa$)

  • Rh: relative humidity (between 0 and 1)

  • Ca: air CO₂ concentration ($ppm$)

  • Jmax: potential rate of electron transport ($\mu mol_{CO2}.m^{-2}.s^{-1}$)

  • Vmax: maximum rate of Rubisco activity ($\mu mol_{CO2}.m^{-2}.s^{-1}$)

  • Rd: mitochondrial respiration in the light at reference temperature ($\mu mol_{CO2}.m^{-2}.s^{-1}$)

  • TPU: triose phosphate utilization-limited photosynthesis rate ($\mu mol_{CO2}.m^{-2}.s^{-1}$)

  • Rs: short-wave net radiation ($W.m^{-1}$)

  • skyF: Sun-visible fraction of the leaf (between 0 and 1)

  • d: characteristic length ($m$)

  • g0: residual stomatal conductance ($mol_{CO2}.m^{-2}.s^{-1}$)

  • g1: slope of the stomatal conductance relationship.

md"""

### Random input simulation dataset

We create possible ranges for input parameters. These ranges where chosen so all of the three packages don't return errors during computation (`plantecophys` has issues with low temperatures).

- Ta: air temperature ($°C$)
- Wind: wind speed ($m.s^{-1}$)
- P: ambient pressure ($kPa$)
- Rh: relative humidity (between 0 and 1)
- Ca: air CO₂ concentration ($ppm$)
- Jmax: potential rate of electron transport ($\mu mol_{CO2}.m^{-2}.s^{-1}$)
- Vmax: maximum rate of Rubisco activity ($\mu mol_{CO2}.m^{-2}.s^{-1}$)
- Rd: mitochondrial respiration in the light at reference temperature ($\mu mol_{CO2}.m^{-2}.s^{-1}$)
- TPU: triose phosphate utilization-limited photosynthesis rate ($\mu mol_{CO2}.m^{-2}.s^{-1}$)
- Rs: short-wave net radiation ($W.m^{-1}$)
- skyF: Sun-visible fraction of the leaf (between 0 and 1)
- d: characteristic length ($m$)
- g0: residual stomatal conductance ($mol_{CO2}.m^{-2}.s^{-1}$)
- g1: slope of the stomatal conductance relationship.
"""
8.0 ms
begin
# Create the ranges of input parameters
length_range = 10000
Rs = range(10, 500, length=length_range)
Ta = range(18, 40, length=length_range)
Wind = range(0.5, 20, length=length_range)
P = range(90, 101, length=length_range)
Rh = range(0.1, 0.98, length=length_range)
Ca = range(360, 900, length=length_range)
skyF = range(0.0, 1.0, length=length_range)
d = range(0.001, 0.5, length=length_range)
Jmax = range(200.0, 300.0, length=length_range)
Vmax = range(150.0, 250.0, length=length_range)
Rd = range(0.3, 2.0, length=length_range)
TPU = range(5.0, 20.0, length=length_range)
g0 = range(0.001, 2.0, length=length_range)
g1 = range(0.5, 15.0, length=length_range)
vars = hcat([Ta, Wind, P, Rh, Ca, Jmax, Vmax, Rd, Rs, skyF, d, TPU, g0, g1])
nothing
end
313 ms

We then sample N conditions from the given ranges:

md"We then sample `N` conditions from the given ranges:"
258 μs
TWindPRhCaJMaxRefVcMaxRefRdRefmore
Float64Float64Float64Float64Float64Float64Float64Float64
1
30.6403
11.4074
99.3432
0.889087
853.879
248.195
186.594
1.94406
2
23.758
14.7676
99.1716
0.673377
813.483
248.515
166.332
0.570497
3
24.5633
9.57621
100.194
0.616524
775.194
202.18
245.23
1.69057
4
29.4059
18.1961
90.5578
0.749593
643.852
266.087
244.119
1.30497
5
23.2629
14.5083
94.9857
0.251111
809.163
233.643
154.66
1.4267
6
37.3179
5.75383
90.1672
0.89648
785.671
217.242
200.045
1.65657
7
26.319
8.90144
93.1628
0.970583
409.199
232.903
157.321
0.630003
8
20.418
11.7916
100.938
0.1844
844.914
210.951
249.69
0.613171
9
29.9428
10.251
98.2222
0.18264
404.068
236.814
241.459
0.940794
10
24.6073
3.59106
92.7624
0.676106
444.086
242.684
229.828
0.529863
more
100
30.1452
14.2118
90.3795
0.792981
367.993
277.558
222.267
1.63412
begin
set = [rand.(vars) for i = 1:N]
set = reshape(vcat(set...), (length(set[1]), length(set)))'
name = [
"T",
"Wind",
"P",
"Rh",
"Ca",
"JMaxRef",
"VcMaxRef",
"RdRef",
"Rs",
"sky_fraction",
"d",
"TPURef",
"g0",
"g1",
]
set = DataFrame(set, name)
@. set[!, :vpd] = e_sat(set.T) - vapor_pressure(set.T, set.Rh)
@. set[!, :PPFD] = set.Rs * 0.48 * 4.57
set
end
1.0 s

Benchmarking

md"
## Benchmarking
"
198 μs
plantecophys

Preparing R to make the benchmark:

R"""
if(!require("plantecophys")){
    install.packages("plantecophys", repos = "https://cloud.r-project.org")
}
if(!require("microbenchmark")){
    install.packages("microbenchmark", repos = "https://cloud.r-project.org")
}
"""

# Make variables available to the R session
@rput set N microbenchmark_steps

Making the benchmark:

R"""
# Define the function call in a function that takes a list as input to limit DataFrame overhead
function_EB <- function(input) {
    PhotosynEB(
        Tair = input$Tair, VPD = input$VPD, Wind = input$Wind,
        Wleaf = input$Wleaf,Ca = input$Ca,  StomatalRatio = 1,
        LeafAbs = input$LeafAbs, gsmodel = "BBOpti", g0 = input$g0, g1 = input$g1,
        alpha = 0.24, theta = 0.7, Jmax = input$Jmax,
        Vcmax = input$Vcmax, TPU = input$TPU, Rd = input$Rd,
        RH = input$RH, PPFD=input$PPFD, Patm = input$Patm
    )
}

time_PE = c()
for(i in seq_len(N)){
    # Put the inputs into a vector to limit dataframe overhead:
    input = list(
        Tair = set$T[i], VPD = set$vpd[i], Wind = set$Wind[i], Wleaf = set$d[i],
        Ca = set$Ca[i], LeafAbs = set$sky_fraction[i], g0 = set$g0[i], g1 = set$g1[i],
        Jmax = set$JMaxRef[i], Vcmax = set$VcMaxRef[i], TPU = set$TPURef[i],
        Rd = set$RdRef[i], RH = set$Rh[i]*100, PPFD=set$PPFD[i],Patm = set$P[i]
    )

    m = microbenchmark(function_EB(input), times = microbenchmark_steps)

    time_PE = append(time_PE,m$time * 10e-9) # transform in seconds
}
"""

@rget time_PE
md"""
##### plantecophys

Preparing R to make the benchmark:

```julia
R\"\"\"
if(!require("plantecophys")){
install.packages("plantecophys", repos = "https://cloud.r-project.org")
}
if(!require("microbenchmark")){
install.packages("microbenchmark", repos = "https://cloud.r-project.org")
}
\"\"\"

# Make variables available to the R session
@rput set N microbenchmark_steps

```

Making the benchmark:

```julia
R\"\"\"
# Define the function call in a function that takes a list as input to limit DataFrame overhead
function_EB <- function(input) {
PhotosynEB(
Tair = input$Tair, VPD = input$VPD, Wind = input$Wind,
Wleaf = input$Wleaf,Ca = input$Ca, StomatalRatio = 1,
LeafAbs = input$LeafAbs, gsmodel = "BBOpti", g0 = input$g0, g1 = input$g1,
alpha = 0.24, theta = 0.7, Jmax = input$Jmax,
Vcmax = input$Vcmax, TPU = input$TPU, Rd = input$Rd,
RH = input$RH, PPFD=input$PPFD, Patm = input$Patm
)
}

8.2 ms
LeafGasExchange.jl

Note that we benchmark LeafGasExchange.jl with the nounit flag to compute a fair comparison with PlantBiophysics.jl in case computing units takes time (it shouldn't much).

time_LG = []
n_lg = fill(0, N)
for i = 1:N
    config =
        :Weather => (
            PFD=set.PPFD[i],
            CO2=set.Ca[i],
            RH=set.Rh[i] * 100,
            T_air=set.T[i],
            wind=set.Wind[i],
            P_air=set.P[i],
            g0=set.g0[i],
            g1=set.g1[i],
            Vcmax=set.VcMaxRef[i],
            Jmax=set.JMaxRef[i],
            Rd=set.RdRef[i],
            TPU=set.TPURef[i],
        )
    b_LG =
        @benchmark simulate($ModelC3MD; config=$config) evals = microbenchmark_evals samples =
            microbenchmark_steps
    append!(time_LG, b_LG.times .* 1e-9) # transform in seconds
    n_lg[i] = 1
end
md"""

##### LeafGasExchange.jl

Note that we benchmark `LeafGasExchange.jl` with the `nounit` flag to compute a fair comparison with `PlantBiophysics.jl` in case computing units takes time (it shouldn't much).

```julia
time_LG = []
n_lg = fill(0, N)
for i = 1:N
config =
:Weather => (
PFD=set.PPFD[i],
CO2=set.Ca[i],
RH=set.Rh[i] * 100,
T_air=set.T[i],
wind=set.Wind[i],
P_air=set.P[i],
g0=set.g0[i],
g1=set.g1[i],
Vcmax=set.VcMaxRef[i],
Jmax=set.JMaxRef[i],
Rd=set.RdRef[i],
TPU=set.TPURef[i],
)
b_LG =
@benchmark simulate($ModelC3MD; config=$config) evals = microbenchmark_evals samples =
microbenchmark_steps
append!(time_LG, b_LG.times .* 1e-9) # transform in seconds
n_lg[i] = 1
end
```

"""
348 μs
PlantBiophysics.jl

Benchmarking PlantBiophysics.jl:

constants = Constants()
time_PB = []
for i = 1:N
    leaf = ModelList(
        energy_balance=Monteith(),
        photosynthesis=Fvcb(
            VcMaxRef=set.VcMaxRef[i],
            JMaxRef=set.JMaxRef[i],
            RdRef=set.RdRef[i],
            TPURef=set.TPURef[i],
        ),
        stomatal_conductance=Medlyn(set.g0[i], set.g1[i]),
        status=(
            Rₛ=set.Rs[i],
            sky_fraction=set.sky_fraction[i],
            PPFD=set.PPFD[i],
            d=set.d[i],
        ),
    )
    deps = PlantSimEngine.dep(leaf)
    meteo = Atmosphere(T=set.T[i], Wind=set.Wind[i], P=set.P[i], Rh=set.Rh[i], Cₐ=set.Ca[i])
    st = PlantMeteo.row_struct(leaf.status[1])
    b_PB = @benchmark run!($leaf, $deps, $st, $meteo, $constants, nothing) evals =
        microbenchmark_evals samples = microbenchmark_steps
    append!(time_PB, b_PB.times .* 1e-9) # transform in seconds
end
md"""
##### PlantBiophysics.jl

Benchmarking `PlantBiophysics.jl`:

```julia
constants = Constants()
time_PB = []
for i = 1:N
leaf = ModelList(
energy_balance=Monteith(),
photosynthesis=Fvcb(
VcMaxRef=set.VcMaxRef[i],
JMaxRef=set.JMaxRef[i],
RdRef=set.RdRef[i],
TPURef=set.TPURef[i],
),
stomatal_conductance=Medlyn(set.g0[i], set.g1[i]),
status=(
Rₛ=set.Rs[i],
sky_fraction=set.sky_fraction[i],
PPFD=set.PPFD[i],
d=set.d[i],
),
)
deps = PlantSimEngine.dep(leaf)
meteo = Atmosphere(T=set.T[i], Wind=set.Wind[i], P=set.P[i], Rh=set.Rh[i], Cₐ=set.Ca[i])
st = PlantMeteo.row_struct(leaf.status[1])
b_PB = @benchmark run!($leaf, $deps, $st, $meteo, $constants, nothing) evals =
microbenchmark_evals samples = microbenchmark_steps
append!(time_PB, b_PB.times .* 1e-9) # transform in seconds
end
```
"""
293 μs

Comparison

md"
## Comparison
"
186 μs
Statistics

We compute here basic statistics, i.e. mean, median, min, max, standard deviation.

statsPB = basic_stat(time_PB)
statsPE = basic_stat(time_PE)
statsLG = basic_stat(time_LG)

factorPE = mean(time_PE) / mean(time_PB)
factorLG = mean(time_LG) / mean(time_PB)

# Write overall timings:
df = DataFrame(
	[getfield(j, i) for i in fieldnames(StatResults), j in [statsPB, statsPE, statsLG]],
	["PlantBiophysics", "plantecophys", "LeafGasExchange"]
)
insertcols!(df, 1, :Stat => [fieldnames(StatResults)...])
CSV.write("benchmark.csv", df)

# Write timing for each sample:
CSV.write("benchmark_full.csv",
	DataFrame(
		"package" => vcat(
			[
				repeat([i.first], length(i.second)) for i in [
					"PlantBiophysics" => time_PB,
					"plantecophys" => time_PE,
					"LeafGasExchange" => time_LG
				]
			]...
		),
		"sample_time" => vcat(time_PB, time_PE, time_LG)
	)
)
md"""
##### Statistics

We compute here basic statistics, _i.e._ mean, median, min, max, standard deviation.

```julia
statsPB = basic_stat(time_PB)
statsPE = basic_stat(time_PE)
statsLG = basic_stat(time_LG)

factorPE = mean(time_PE) / mean(time_PB)
factorLG = mean(time_LG) / mean(time_PB)

# Write overall timings:
df = DataFrame(
[getfield(j, i) for i in fieldnames(StatResults), j in [statsPB, statsPE, statsLG]],
["PlantBiophysics", "plantecophys", "LeafGasExchange"]
)
insertcols!(df, 1, :Stat => [fieldnames(StatResults)...])
CSV.write("benchmark.csv", df)

# Write timing for each sample:
CSV.write("benchmark_full.csv",
DataFrame(
"package" => vcat(
[
repeat([i.first], length(i.second)) for i in [
"PlantBiophysics" => time_PB,
"plantecophys" => time_PE,
"LeafGasExchange" => time_LG
]
]...
),
"sample_time" => vcat(time_PB, time_PE, time_LG)
)
)
317 μs
df_res
StatPlantBiophysicsplantecophysLeafGasExchange
String7Float64Float64Float64
1
"mean"
1.21661e-6
0.0470207
0.0226825
2
"median"
1.083e-6
0.04569
0.0214673
3
"stddev"
1.20394e-6
0.00808695
0.0047238
4
"min"
5.41e-7
0.0346401
0.0192761
5
"max"
2.5e-5
0.172202
0.12373
df_res = CSV.read(Downloads.download("https://raw.githubusercontent.com/VEZY/PlantBiophysics-paper/main/notebooks/performance/benchmark.csv"), DataFrame)
16.2 s
Histogram plotting
md"
##### Histogram plotting
"
180 μs

We here display the computational time histogram of each package on the same scale in order to compare them: PlantBiophysics.jl (a), plantecophys (b) and LeafGasExchange.jl (c). The y-axis represents the density (i.e. reaching 0.3 means that 30% of the computed times are in this bar). Orange zone represents the interval [mean - standard deviation; mean + standard deviation]. Red dashed line represents the mean. Note that the x-axis is logarithmic.

    fig = plot_benchmark_Makie(statsPB, statsPE, statsLG, time_PB, time_PE, time_LG)
    save("benchmark_each_time_steps.png", fig, px_per_unit=3)

Note

PlantBiophysics.jl is 38649 times faster than plantecophys, and 18644 times faster than LeafGasExchange.jl.

Warning

This is the plot from the latest commit on https://github.com/VEZY/PlantBiophysics-paper/. If you want to make your own benchmarking, run the script that was used to perform it, but careful, it takes a long time to perform!

Markdown.parse("""
We here display the computational time histogram of each package on the same scale in order to compare them: `PlantBiophysics.jl` (a), `plantecophys` (b) and `LeafGasExchange.jl` (c). The y-axis represents the density (_i.e._ reaching 0.3 means that 30% of the computed times are in this bar). Orange zone represents the interval [mean - standard deviation; mean + standard deviation]. Red dashed line represents the mean. Note that the x-axis is logarithmic.

```julia
fig = plot_benchmark_Makie(statsPB, statsPE, statsLG, time_PB, time_PE, time_LG)
save("benchmark_each_time_steps.png", fig, px_per_unit=3)
```

![](https://raw.githubusercontent.com/VEZY/PlantBiophysics-paper/main/notebooks/performance/benchmark_each_time_steps.png)

!!! note
PlantBiophysics.jl is $(Int(round(df_res[1,:plantecophys] / df_res[1,:PlantBiophysics]))) times faster than plantecophys, and $(Int(round(df_res[1,:LeafGasExchange] / df_res[1,:PlantBiophysics]))) times faster than LeafGasExchange.jl.

!!! warning
This is the plot from the latest commit on <https://github.com/VEZY/PlantBiophysics-paper/>. If you want to make your own benchmarking, run the script that was used to perform it, but careful, it takes a long time to perform!
""")
130 ms

References

md"""
# References
"""
183 μs
StatResults
StatResults(
	mean::AbstractFloat
	median::AbstractFloat
	stddev::AbstractFloat
	min::AbstractFloat
	max::AbstractFloat
)

Structure to hold basic statistics of model performance.

"""
StatResults(
mean::AbstractFloat
median::AbstractFloat
stddev::AbstractFloat
min::AbstractFloat
max::AbstractFloat
)

Structure to hold basic statistics of model performance.
"""
struct StatResults
mean::AbstractFloat
median::AbstractFloat
stddev::AbstractFloat
min::AbstractFloat
max::AbstractFloat
end
2.1 ms

Base.show

Base.show(io::IO, m::StatResults)
Base.show(io::IO, ::MIME"text/plain", m::StatResults)

Add a show method for our StatResults type.

begin
function Base.show(io::IO, ::MIME"text/plain", m::StatResults)
print(
io,
"Benchmark:",
"\nMean time -> ",
m.mean,
" ± ",
m.stddev,
"\nMedian time -> ",
m.median,
"\nMinimum time -> ",
m.min,
"\nMaximum time -> ",
m.max,
)
end
Base.show(io::IO, m::StatResults) = print(io, m.mean, "(±", m.stddev, ')')


md"""
**Base.show**

Base.show(io::IO, m::StatResults)
Base.show(io::IO, ::MIME"text/plain", m::StatResults)

Add a show method for our `StatResults` type.
"""
end
6.0 ms
basic_stat
basic_stat(df)

Compute basic statistics from the benchmarking

"""
basic_stat(df)

Compute basic statistics from the benchmarking
"""
function basic_stat(df)
m = mean(df)
med = median(df)
std = Statistics.std(df)
min = findmin(df)[1]
max = findmax(df)[1]
return StatResults(m, med, std, min, max)
end
1.1 ms
Loading...