|
| 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