Skip to content

Commit 6232bee

Browse files
committed
Add tutorial with canopy photosynthesis using Gaussian integration
1 parent 1647258 commit 6232bee

2 files changed

Lines changed: 313 additions & 1 deletion

File tree

docs/make.jl

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,8 @@ makedocs(;
3939
["Tree" => "tutorials/from_tree_forest/tree.md",
4040
"Forest" => "tutorials/from_tree_forest/forest.md",
4141
"Growth forest" => "tutorials/from_tree_forest/growthforest.md",
42-
"Ray-traced forest" => "tutorials/from_tree_forest/raytracedforest.md"],
42+
"Ray-traced forest" => "tutorials/from_tree_forest/raytracedforest.md",
43+
"Canopy photosynthesis" => "tutorials/from_tree_forest/photosynthesis.md"],
4344
"More on rules and queries" =>
4445
["Context sensitive rules" => "tutorials/more_rules_queries/context.md",
4546
"Relational queries" => "tutorials/more_rules_queries/relationalqueries.md"]
Lines changed: 311 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,311 @@
1+
# Canopy photosynthesis
2+
3+
Alejandro Morales
4+
5+
Centre for Crop Systems Analysis - Wageningen University
6+
7+
> ## TL;DR
8+
> Similar in functionality to [Forest](https://virtualplantlab.com/dev/tutorials/from_tree_forest/forest/) tutorial with photosynthesis
9+
> - Run ray tracer multiple times per day using Gaussian integration
10+
> - Compute photosynthesis at each time point
11+
> - Aggregate photosynthesis to the tree and daily scales
12+
13+
In this tutorial we will add photosynthesis calculations to the forest model (for
14+
simplicity we will still grow the trees descriptively, but this could be extended
15+
to a full growth model including respiration, carbon allocation, etc.).
16+
17+
We start with the code from the [forest](./forest.md) with the following additions:
18+
19+
* Load the Ecophys.jl package
20+
* Add materials to internodes, leaves and soil tile
21+
* Keep track of absorbed PAR within each leaf
22+
* Compute daily photosynthesis for each leaf using Gaussian-Legendre integration over the day
23+
* Integrate to the tree level
24+
25+
```julia
26+
using VirtualPlantLab, Distributions, Plots, Ecophys, SkyDomes, FastGaussQuadrature
27+
import Base.Threads: @threads
28+
import Random
29+
Random.seed!(123456789)
30+
import GLMakie
31+
# Data types
32+
module TreeTypes
33+
import VirtualPlantLab as VPL
34+
import Ecophys
35+
# Meristem
36+
struct Meristem <: VPL.Node end
37+
# Bud
38+
struct Bud <: VPL.Node end
39+
# Node
40+
struct Node <: VPL.Node end
41+
# BudNode
42+
struct BudNode <: VPL.Node end
43+
# Internode (needs to be mutable to allow for changes over time)
44+
Base.@kwdef mutable struct Internode <: VPL.Node
45+
length::Float64 = 0.10 # Internodes start at 10 cm
46+
mat::VPL.Lambertian{1} = VPL.Lambertian= 0.00, ρ = 0.05)
47+
end
48+
# Leaf
49+
Base.@kwdef mutable struct Leaf <: VPL.Node
50+
length::Float64 = 0.20 # Leaves are 20 cm long
51+
width::Float64 = 0.1 # Leaves are 10 cm wide
52+
PARdif::Float64 = 0.0
53+
PARdir::Float64 = 0.0
54+
mat::VPL.Lambertian{1} = VPL.Lambertian= 0.05, ρ = 0.1)
55+
Ag::Float64 = 0.0
56+
end
57+
# Graph-level variables
58+
Base.@kwdef mutable struct treeparams
59+
growth::Float64 = 0.1
60+
budbreak::Float64 = 0.25
61+
phyllotaxis::Float64 = 140.0
62+
leaf_angle::Float64 = 30.0
63+
branch_angle::Float64 = 45.0
64+
photos::Ecophys.C3{Float64} = Ecophys.C3()
65+
Ag::Float64 = 0.0
66+
end
67+
end
68+
69+
import .TreeTypes
70+
71+
# Create geometry + color for the internodes
72+
function VirtualPlantLab.feed!(turtle::Turtle, i::TreeTypes.Internode, vars)
73+
# Rotate turtle around the head to implement elliptical phyllotaxis
74+
rh!(turtle, vars.phyllotaxis)
75+
HollowCylinder!(turtle, length = i.length, height = i.length/15, width = i.length/15,
76+
move = true, colors = RGB(0.5,0.4,0.0), materials = i.mat)
77+
return nothing
78+
end
79+
80+
# Create geometry + color for the leaves
81+
function VirtualPlantLab.feed!(turtle::Turtle, l::TreeTypes.Leaf, vars)
82+
# Rotate turtle around the arm for insertion angle
83+
ra!(turtle, -vars.leaf_angle)
84+
# Generate the leaf
85+
Ellipse!(turtle, length = l.length, width = l.width, move = false,
86+
colors = RGB(0.2,0.6,0.2), materials = l.mat)
87+
# Rotate turtle back to original direction
88+
ra!(turtle, vars.leaf_angle)
89+
return nothing
90+
end
91+
92+
# Insertion angle for the bud nodes
93+
function VirtualPlantLab.feed!(turtle::Turtle, b::TreeTypes.BudNode, vars)
94+
# Rotate turtle around the arm for insertion angle
95+
ra!(turtle, -vars.branch_angle)
96+
end
97+
98+
99+
# Rules
100+
meristem_rule = Rule(TreeTypes.Meristem, rhs = mer -> TreeTypes.Node() +
101+
(TreeTypes.Bud(), TreeTypes.Leaf()) +
102+
TreeTypes.Internode() + TreeTypes.Meristem())
103+
104+
function prob_break(bud)
105+
# We move to parent node in the branch where the bud was created
106+
node = parent(bud)
107+
# We count the number of internodes between node and the first Meristem
108+
# moving down the graph
109+
check, steps = has_descendant(node, condition = n -> data(n) isa TreeTypes.Meristem)
110+
steps = Int(ceil(steps/2)) # Because it will count both the nodes and the internodes
111+
# Compute probability of bud break and determine whether it happens
112+
if check
113+
prob = min(1.0, steps*graph_data(bud).budbreak)
114+
return rand() < prob
115+
# If there is no meristem, an error happened since the model does not allow for this
116+
else
117+
error("No meristem found in branch")
118+
end
119+
end
120+
branch_rule = Rule(TreeTypes.Bud,
121+
lhs = prob_break,
122+
rhs = bud -> TreeTypes.BudNode() + TreeTypes.Internode() + TreeTypes.Meristem())
123+
124+
function create_tree(origin, growth, budbreak, orientation)
125+
axiom = T(origin) + RH(orientation) + TreeTypes.Internode() + TreeTypes.Meristem()
126+
tree = Graph(axiom = axiom, rules = (meristem_rule, branch_rule),
127+
data = TreeTypes.treeparams(growth = growth, budbreak = budbreak))
128+
return tree
129+
end
130+
131+
getInternode = Query(TreeTypes.Internode)
132+
133+
function elongate!(tree, query)
134+
for x in apply(tree, query)
135+
x.length = x.length*(1.0 + data(tree).growth)
136+
end
137+
end
138+
139+
function growth!(tree, query)
140+
elongate!(tree, query)
141+
rewrite!(tree)
142+
end
143+
144+
function simulate(tree, query, nsteps)
145+
new_tree = deepcopy(tree)
146+
for i in 1:nsteps
147+
growth!(new_tree, query)
148+
end
149+
return new_tree
150+
end
151+
origins = [Vec(i,j,0) for i = 1:2.0:20.0, j = 1:2.0:20.0]
152+
orientations = [rand()*360.0 for i = 1:2.0:20.0, j = 1:2.0:20.0]
153+
growths = rand(LogNormal(-2, 0.3), 10, 10)
154+
budbreaks = rand(Beta(2.0, 10), 10, 10)
155+
forest = vec(create_tree.(origins, growths, budbreaks, orientations));
156+
```
157+
158+
We run the simulation for a few steps to create a forest and add the soil:
159+
160+
```julia
161+
newforest = [simulate(tree, getInternode, 15) for tree in forest];
162+
scene = Mesh(newforest);
163+
soil = Rectangle(length = 21.0, width = 21.0)
164+
rotatey!(soil, pi/2)
165+
VirtualPlantLab.translate!(soil, Vec(0.0, 10.5, 0.0))
166+
VirtualPlantLab.add!(scene, soil, colors = RGB(1,1,0),
167+
materials = Lambertian= 0.0, ρ = 0.21))
168+
#render(scene, backend = "web", resolution = (800, 600))
169+
```
170+
171+
Unlike in the previous example, we can no longer run a single raytracer to
172+
compute daily photosynthesis, because of its non-linear response to irradiance.
173+
Instead, we need to compute photosynthesis at different time points during the
174+
day and integrate the results (e.g., using a Gaussian quadrature rule). However,
175+
this does not require more computation than the previous example if the calculations
176+
are done carefully to avoid redundancies.
177+
178+
Firstly, we can create the bounding volume hierarchy and grid cloner around the
179+
scene once for the whole day using the `accelerate()` function (normally this is called
180+
by VPL internally):
181+
182+
```julia
183+
settings = RTSettings(pkill = 0.8, maxiter = 3, nx = 5, ny = 5, dx = 20.0,
184+
dy = 20.0, parallel = true)
185+
acc_scene = accelerate(scene, settings = settings, acceleration = BVH,
186+
rule = SAH{6}(1,20));
187+
```
188+
189+
Then we compute the relative fraction of diffuse PAR that reaches each leaf (once
190+
for the whole day):
191+
192+
```julia
193+
get_leaves(tree) = apply(tree, Query(TreeTypes.Leaf))
194+
195+
function calculate_diffuse!(;acc_scene, forest, lat = 52.0*π/180.0, DOY = 182)
196+
# Create the dome of diffuse light
197+
dome = sky(acc_scene,
198+
Idir = 0.0, # No direct solar radiation
199+
Idif = 1.0, # In order to get relative values
200+
nrays_dif = 1_000_000, # Total number of rays for diffuse solar radiation
201+
sky_model = StandardSky, # Angular distribution of solar radiation
202+
dome_method = equal_solid_angles, # Discretization of the sky dome
203+
ntheta = 9, # Number of discretization steps in the zenith angle
204+
nphi = 12) # Number of discretization steps in the azimuth angle
205+
# Ray trace the scene
206+
settings = RTSettings(pkill = 0.9, maxiter = 4, nx = 5, ny = 5, dx = 20.0,
207+
dy = 20.0, parallel = true)
208+
# Because the acceleration was pre-computed, use direct RayTracer constructor
209+
rtobj = RayTracer(acc_scene, dome, settings = settings);
210+
trace!(rtobj)
211+
# Transfer power to PARdif
212+
@threads for tree in forest
213+
for leaf in get_leaves(tree)
214+
leaf.PARdif = power(leaf.mat)[1]/*leaf.length*leaf.width/4)
215+
end
216+
end
217+
return nothing
218+
end
219+
```
220+
221+
Once the relative diffuse irradiance has been computed, we can loop over the
222+
day and compute direct PAR by using a single ray tracer and update photosynthesis
223+
from that. Notice that here we convert solar radiation to PAR in umol/m2/s as
224+
opposed to W/m2 (using `:flux` rather than `:power` in the `waveband_conversion`
225+
function):
226+
227+
```julia
228+
function calculate_photosynthesis!(;acc_scene, forest, lat = 52.0*π/180.0, DOY = 182,
229+
f = 0.5, w = 0.5, DL = 12*3600)
230+
# Compute the solar irradiance assuming clear sky conditions
231+
Ig, Idir, Idif = clear_sky(lat = lat, DOY = DOY, f = f)
232+
# Conversion factors to PAR for direct and diffuse irradiance
233+
PARdir = Idir*waveband_conversion(Itype = :direct, waveband = :PAR, mode = :flux)
234+
PARdif = Idif*waveband_conversion(Itype = :diffuse, waveband = :PAR, mode = :flux)
235+
# Create the light source for the ray tracer
236+
dome = sky(acc_scene, Idir = PARdir, nrays_dir = 100_000, Idif = 0.0)
237+
# Ray trace the scene
238+
settings = RTSettings(pkill = 0.9, maxiter = 4, nx = 5, ny = 5, dx = 20.0,
239+
dy = 20.0, parallel = true)
240+
rtobj = RayTracer(acc_scene, dome, settings = settings)
241+
trace!(rtobj)
242+
# Transfer power to PARdif
243+
@threads for tree in forest
244+
ph = data(tree).photos
245+
for leaf in get_leaves(tree)
246+
leaf.PARdir = power(leaf.mat)[1]/*leaf.length*leaf.width/4)
247+
leaf.PARdif = leaf.PARdif*PARdif
248+
PAR = leaf.PARdir + leaf.PARdif
249+
leaf.Ag += (photosynthesis(ph, PAR = PAR).A + ph.Rd25)*w*DL
250+
end
251+
end
252+
return nothing
253+
end
254+
255+
# Reset photosynthesis
256+
function reset_photosynthesis!(forest)
257+
@threads for tree in forest
258+
for leaf in get_leaves(tree)
259+
leaf.Ag = 0.0
260+
end
261+
end
262+
return nothing
263+
end
264+
```
265+
266+
This function may now be run for different time points during the day based on
267+
a Gaussian quadrature rule:
268+
269+
```julia
270+
function daily_photosynthesis(forest; DOY = 182, lat = 52.0*π/180.0)
271+
# Compute fraction of diffuse irradiance per leaf
272+
calculate_diffuse!(acc_scene = acc_scene, forest = forest, DOY = DOY, lat = lat);
273+
# Gaussian quadrature over the
274+
NG = 5
275+
f, w = gausslegendre(NG)
276+
w ./= 2.0
277+
f .= (f .+ 1.0)/2.0
278+
# Reset photosynthesis
279+
reset_photosynthesis!(forest)
280+
# Loop over the day
281+
dec = declination(DOY)
282+
DL = day_length(lat, dec)*3600
283+
for i in 1:NG
284+
println("step $i out of $NG")
285+
calculate_photosynthesis!(acc_scene = acc_scene, forest = forest,
286+
f = f[i], w = w[i], DL = DL, DOY = DOY, lat = lat)
287+
end
288+
end
289+
```
290+
291+
And we scale to the tree level with a simple query:
292+
293+
```julia
294+
function canopy_photosynthesis!(forest)
295+
# Integrate photosynthesis over the day at the leaf level
296+
daily_photosynthesis(forest)
297+
# Aggregate to the the tree level
298+
Ag = Float64[]
299+
for tree in forest
300+
data(tree).Ag = sum(leaf.Ag*π*leaf.length*leaf.width/4 for leaf in get_leaves(tree))
301+
push!(Ag, data(tree).Ag)
302+
end
303+
return Ag/1e6 # mol/tree
304+
end
305+
306+
# Run the canopy photosynthesis model
307+
Ag = canopy_photosynthesis!(newforest);
308+
309+
# Visualize distribution of tree photosynthesis
310+
histogram(Ag)
311+
```

0 commit comments

Comments
 (0)