Canopy photosynthesis
Alejandro Morales
Centre for Crop Systems Analysis - Wageningen University
TL;DR
Similar in functionality to Forest tutorial with photosynthesis
- Run ray tracer multiple times per day using Gaussian integration
- Compute photosynthesis at each time point
- Aggregate photosynthesis to the tree and daily scales
In this tutorial we will add photosynthesis calculations to the forest model (for simplicity we will still grow the trees descriptively, but this could be extended to a full growth model including respiration, carbon allocation, etc.).
We start with the code from the forest with the following additions:
- Load the Ecophys.jl package
- Add materials to internodes, leaves and soil tile
- Keep track of absorbed PAR within each leaf
- Compute daily photosynthesis for each leaf using Gaussian-Legendre integration over the day
- Integrate to the tree level
using VirtualPlantLab, Distributions, Plots, Ecophys, SkyDomes, FastGaussQuadrature
import Base.Threads: @threads
import Random
Random.seed!(123456789)
import GLMakie
# Data types
module TreeTypes
import VirtualPlantLab as VPL
import Ecophys
# Meristem
struct Meristem <: VPL.Node end
# Bud
struct Bud <: VPL.Node end
# Node
struct Node <: VPL.Node end
# BudNode
struct BudNode <: VPL.Node end
# Internode (needs to be mutable to allow for changes over time)
Base.@kwdef mutable struct Internode <: VPL.Node
length::Float64 = 0.10 # Internodes start at 10 cm
mat::VPL.Lambertian{1} = VPL.Lambertian(τ = 0.00, ρ = 0.05)
end
# Leaf
Base.@kwdef mutable struct Leaf <: VPL.Node
length::Float64 = 0.20 # Leaves are 20 cm long
width::Float64 = 0.1 # Leaves are 10 cm wide
PARdif::Float64 = 0.0
PARdir::Float64 = 0.0
mat::VPL.Lambertian{1} = VPL.Lambertian(τ = 0.05, ρ = 0.1)
Ag::Float64 = 0.0
end
# Graph-level variables
Base.@kwdef mutable struct treeparams
growth::Float64 = 0.1
budbreak::Float64 = 0.25
phyllotaxis::Float64 = 140.0
leaf_angle::Float64 = 30.0
branch_angle::Float64 = 45.0
photos::Ecophys.C3{Float64} = Ecophys.C3()
Ag::Float64 = 0.0
end
end
import .TreeTypes
# Create geometry + color for the internodes
function VirtualPlantLab.feed!(turtle::Turtle, i::TreeTypes.Internode, vars)
# Rotate turtle around the head to implement elliptical phyllotaxis
rh!(turtle, vars.phyllotaxis)
HollowCylinder!(turtle, length = i.length, height = i.length/15, width = i.length/15,
move = true, colors = RGB(0.5,0.4,0.0), materials = i.mat)
return nothing
end
# Create geometry + color for the leaves
function VirtualPlantLab.feed!(turtle::Turtle, l::TreeTypes.Leaf, vars)
# Rotate turtle around the arm for insertion angle
ra!(turtle, -vars.leaf_angle)
# Generate the leaf
Ellipse!(turtle, length = l.length, width = l.width, move = false,
colors = RGB(0.2,0.6,0.2), materials = l.mat)
# Rotate turtle back to original direction
ra!(turtle, vars.leaf_angle)
return nothing
end
# Insertion angle for the bud nodes
function VirtualPlantLab.feed!(turtle::Turtle, b::TreeTypes.BudNode, vars)
# Rotate turtle around the arm for insertion angle
ra!(turtle, -vars.branch_angle)
end
# Rules
meristem_rule = Rule(TreeTypes.Meristem, rhs = mer -> TreeTypes.Node() +
(TreeTypes.Bud(), TreeTypes.Leaf()) +
TreeTypes.Internode() + TreeTypes.Meristem())
function prob_break(bud)
# We move to parent node in the branch where the bud was created
node = parent(bud)
# We count the number of internodes between node and the first Meristem
# moving down the graph
check, steps = has_descendant(node, condition = n -> data(n) isa TreeTypes.Meristem)
steps = Int(ceil(steps/2)) # Because it will count both the nodes and the internodes
# Compute probability of bud break and determine whether it happens
if check
prob = min(1.0, steps*graph_data(bud).budbreak)
return rand() < prob
# If there is no meristem, an error happened since the model does not allow for this
else
error("No meristem found in branch")
end
end
branch_rule = Rule(TreeTypes.Bud,
lhs = prob_break,
rhs = bud -> TreeTypes.BudNode() + TreeTypes.Internode() + TreeTypes.Meristem())
function create_tree(origin, growth, budbreak, orientation)
axiom = T(origin) + RH(orientation) + TreeTypes.Internode() + TreeTypes.Meristem()
tree = Graph(axiom = axiom, rules = (meristem_rule, branch_rule),
data = TreeTypes.treeparams(growth = growth, budbreak = budbreak))
return tree
end
getInternode = Query(TreeTypes.Internode)
function elongate!(tree, query)
for x in apply(tree, query)
x.length = x.length*(1.0 + data(tree).growth)
end
end
function growth!(tree, query)
elongate!(tree, query)
rewrite!(tree)
end
function simulate(tree, query, nsteps)
new_tree = deepcopy(tree)
for i in 1:nsteps
growth!(new_tree, query)
end
return new_tree
end
origins = [Vec(i,j,0) for i = 1:2.0:20.0, j = 1:2.0:20.0]
orientations = [rand()*360.0 for i = 1:2.0:20.0, j = 1:2.0:20.0]
growths = rand(LogNormal(-2, 0.3), 10, 10)
budbreaks = rand(Beta(2.0, 10), 10, 10)
forest = vec(create_tree.(origins, growths, budbreaks, orientations));
We run the simulation for a few steps to create a forest and add the soil:
newforest = [simulate(tree, getInternode, 15) for tree in forest];
scene = Mesh(newforest);
soil = Rectangle(length = 21.0, width = 21.0)
rotatey!(soil, pi/2)
VirtualPlantLab.translate!(soil, Vec(0.0, 10.5, 0.0))
VirtualPlantLab.add!(scene, soil, colors = RGB(1,1,0),
materials = Lambertian(τ = 0.0, ρ = 0.21))
#render(scene, backend = "web", resolution = (800, 600))
Unlike in the previous example, we can no longer run a single raytracer to compute daily photosynthesis, because of its non-linear response to irradiance. Instead, we need to compute photosynthesis at different time points during the day and integrate the results (e.g., using a Gaussian quadrature rule). However, this does not require more computation than the previous example if the calculations are done carefully to avoid redundancies.
Firstly, we can create the bounding volume hierarchy and grid cloner around the scene once for the whole day using the accelerate()
function (normally this is called by VPL internally):
settings = RTSettings(pkill = 0.8, maxiter = 3, nx = 5, ny = 5, dx = 20.0,
dy = 20.0, parallel = true)
acc_scene = accelerate(scene, settings = settings, acceleration = BVH,
rule = SAH{6}(1,20));
Then we compute the relative fraction of diffuse PAR that reaches each leaf (once for the whole day):
get_leaves(tree) = apply(tree, Query(TreeTypes.Leaf))
function calculate_diffuse!(;acc_scene, forest, lat = 52.0*π/180.0, DOY = 182)
# Create the dome of diffuse light
dome = sky(acc_scene,
Idir = 0.0, # No direct solar radiation
Idif = 1.0, # In order to get relative values
nrays_dif = 1_000_000, # Total number of rays for diffuse solar radiation
sky_model = StandardSky, # Angular distribution of solar radiation
dome_method = equal_solid_angles, # Discretization of the sky dome
ntheta = 9, # Number of discretization steps in the zenith angle
nphi = 12) # Number of discretization steps in the azimuth angle
# Ray trace the scene
settings = RTSettings(pkill = 0.9, maxiter = 4, nx = 5, ny = 5, dx = 20.0,
dy = 20.0, parallel = true)
# Because the acceleration was pre-computed, use direct RayTracer constructor
rtobj = RayTracer(acc_scene, dome, settings = settings);
trace!(rtobj)
# Transfer power to PARdif
@threads for tree in forest
for leaf in get_leaves(tree)
leaf.PARdif = power(leaf.mat)[1]/(π*leaf.length*leaf.width/4)
end
end
return nothing
end
Once the relative diffuse irradiance has been computed, we can loop over the day and compute direct PAR by using a single ray tracer and update photosynthesis from that. Notice that here we convert solar radiation to PAR in umol/m2/s as opposed to W/m2 (using :flux
rather than :power
in the waveband_conversion
function):
function calculate_photosynthesis!(;acc_scene, forest, lat = 52.0*π/180.0, DOY = 182,
f = 0.5, w = 0.5, DL = 12*3600)
# Compute the solar irradiance assuming clear sky conditions
Ig, Idir, Idif = clear_sky(lat = lat, DOY = DOY, f = f)
# Conversion factors to PAR for direct and diffuse irradiance
PARdir = Idir*waveband_conversion(Itype = :direct, waveband = :PAR, mode = :flux)
PARdif = Idif*waveband_conversion(Itype = :diffuse, waveband = :PAR, mode = :flux)
# Create the light source for the ray tracer
dome = sky(acc_scene, Idir = PARdir, nrays_dir = 100_000, Idif = 0.0)
# Ray trace the scene
settings = RTSettings(pkill = 0.9, maxiter = 4, nx = 5, ny = 5, dx = 20.0,
dy = 20.0, parallel = true)
rtobj = RayTracer(acc_scene, dome, settings = settings)
trace!(rtobj)
# Transfer power to PARdif
@threads for tree in forest
ph = data(tree).photos
for leaf in get_leaves(tree)
leaf.PARdir = power(leaf.mat)[1]/(π*leaf.length*leaf.width/4)
leaf.PARdif = leaf.PARdif*PARdif
PAR = leaf.PARdir + leaf.PARdif
leaf.Ag += (photosynthesis(ph, PAR = PAR).A + ph.Rd25)*w*DL
end
end
return nothing
end
# Reset photosynthesis
function reset_photosynthesis!(forest)
@threads for tree in forest
for leaf in get_leaves(tree)
leaf.Ag = 0.0
end
end
return nothing
end
This function may now be run for different time points during the day based on a Gaussian quadrature rule:
function daily_photosynthesis(forest; DOY = 182, lat = 52.0*π/180.0)
# Compute fraction of diffuse irradiance per leaf
calculate_diffuse!(acc_scene = acc_scene, forest = forest, DOY = DOY, lat = lat);
# Gaussian quadrature over the
NG = 5
f, w = gausslegendre(NG)
w ./= 2.0
f .= (f .+ 1.0)/2.0
# Reset photosynthesis
reset_photosynthesis!(forest)
# Loop over the day
dec = declination(DOY)
DL = day_length(lat, dec)*3600
for i in 1:NG
println("step $i out of $NG")
calculate_photosynthesis!(acc_scene = acc_scene, forest = forest,
f = f[i], w = w[i], DL = DL, DOY = DOY, lat = lat)
end
end
And we scale to the tree level with a simple query:
function canopy_photosynthesis!(forest)
# Integrate photosynthesis over the day at the leaf level
daily_photosynthesis(forest)
# Aggregate to the the tree level
Ag = Float64[]
for tree in forest
data(tree).Ag = sum(leaf.Ag*π*leaf.length*leaf.width/4 for leaf in get_leaves(tree))
push!(Ag, data(tree).Ag)
end
return Ag/1e6 # mol/tree
end
# Run the canopy photosynthesis model
Ag = canopy_photosynthesis!(newforest);
# Visualize distribution of tree photosynthesis
histogram(Ag)