Simulation on a plant (MTG)

Multi-scale Tree Graph

The Multi-scale Tree Graph, or MTG for short is a data structure that helps represent a plant topology, and optionally its geometry.

The OPF is a file format that stores an MTG with geometry onto the disk. Let's read an example OPF using read_opf(), a function from the PlantGeom package:

using PlantGeom
mtg = read_opf(joinpath(dirname(dirname(pathof(PlantBiophysics))), "test", "inputs", "scene", "opf", "coffee.opf"))
/ 1: Individual
└─ / 2: Axis
   ├─ < 5: Metamer
   ├─ < 799: Metamer
   │  ├─ + 800: Axis
   │  │  ├─ < 806: Metamer
   │  │  │  └─ + 807: Axis
   │  │  │     ├─ / 808: Metamer
   │  │  │     ├─ < 810: Metamer
   │  │  │     ├─ < 814: Metamer
   │  │  │     │  ├─ + 816: Leaf
   │  │  │     │  └─ + 815: Leaf
   │  │  │     ├─ < 811: Metamer
   │  │  │     │  ├─ + 813: Leaf
   │  │  │     │  └─ + 812: Leaf
   │  │  │     ├─ < 809: Metamer
   │  │  │     └─ < 817: Metamer
   │  │  │        └─ + 818: Leaf
   │  │  ├─ < 939: Metamer
   │  │  │  ├─ + 940: Leaf
   │  │  │  └─ + 941: Leaf
   │  │  ├─ < 803: Metamer
   │  │  ├─ < 926: Metamer
   │  │  ├─ < 936: Metamer
…

The result is an MTG defining the plant at several scales using a tree graph. You can read the introduction to the MTG from MultiScaleTreeGraph.jl's documentation if you want to understand how it works.

Now let's import the weather data:

using PlantBiophysics, PlantSimEngine, PlantMeteo, Dates

weather = read_weather(
    joinpath(dirname(dirname(pathof(PlantMeteo))), "test", "data", "meteo.csv"),
    :temperature => :T,
    :relativeHumidity => (x -> x ./ 100) => :Rh,
    :wind => :Wind,
    :atmosphereCO2_ppm => :Cₐ,
    date_format = DateFormat("yyyy/mm/dd")
)
TimeStepTable{Atmosphere{(:date, :duration,...}(3 x 29):
date duration T Wind P Rh Precipitations Cₐ e eₛ VPD ρ λ γ ε Δ clearness Ri_SW_f Ri_PAR_f Ri_NIR_f Ri_TIR_f Ri_custom_f hour_start hour_end temperature relativeHumidity Re_SW_f wind atmosphereCO2_ppm
Dates.DateTime Dates.CompoundPeriod Float64 Float64 Float64 Float64 Float64 Float64 Float64 Float64 Float64 Float64 Float64 Float64 Float64 Float64 Float64 Float64 Float64 Float64 Float64 Float64 Dates.Time Dates.Time Float64 Float64 Float64 Float64 Float64
1 2016-06-12T12:00:00 30 minutes 25.0 1.0 101.325 0.6 0.0 380.0 1.90812 3.1802 1.27208 1.18389 2.44188e6 0.0675791 0.602345 0.190095 0.75 Inf Inf Inf Inf Inf 12:00:00 12:30:00 25.0 60.0 500.0 1.0 380.0
2 2016-06-12T12:30:00 30 minutes 26.0 1.5 101.325 0.62 0.0 380.0 2.09238 3.37481 1.28243 1.17993 2.43951e6 0.0676446 0.610038 0.200215 0.75 Inf Inf Inf Inf Inf 12:30:00 13:00:00 26.0 62.0 500.0 1.5 380.0
3 2016-06-12T13:00:00 30 minutes 25.3 1.5 101.325 0.58 0.0 380.0 1.87776 3.23752 1.35976 1.1827 2.44117e6 0.0675987 0.60088 0.193085 0.75 Inf Inf Inf Inf Inf 13:00:00 13:30:00 25.3 58.0 500.0 1.5 380.0
Metadata: `Dict{String, Any}("name" => "Aquiares", "latitude" => 15.0, "altitude" => 100.0, "use" => [:clearness], "file" => "/home/runner/.julia/packages/PlantMeteo/WTk5G/test/data/meteo.csv")`

And read the models associated to the MTG from a YAML file:

file = joinpath(dirname(dirname(pathof(PlantBiophysics))), "test", "inputs", "models", "plant_coffee.yml")
models = read_model(file)
Dict{Symbol, Tuple} with 2 entries:
  :Leaf    => (Monteith{Float64, Int64}(2, 1, 0.955, 10, 0.01), Translucent{Flo…
  :Metamer => (Translucent{Float64}(0.1, σ{Float64}(0.15, 0.9)),)

Let's check which variables we need to provide for our model configuration:

to_initialize(models, mtg)
Dict{Symbol, Vector{Symbol}} with 1 entry:
  :Leaf => [:d]

OK, only the :Leaf component must be initialized before computation for the coupled energy balance, with the characteristic dimension of the object d.

But we also know that the Translucent model reads some variables from the MTG nodes directly: the absorbed shortwave radiation flux Ra_SW_f, the visible sky fraction seen by the object sky_fraction, and the photosynthetically active absorbed radiation flux Ra_PAR_f. We are in luck, we used Archimed-ϕ to compute the radiation interception of each organ in the example coffee plant we are using. So the only thing we need to do is to transform the variables given by Archimed-ϕ in each node to compute the ones we need. We use transform! from MultiScaleTreeGraph.jl to traverse the MTG and compute the right variable for each node:

using MultiScaleTreeGraph

MultiScaleTreeGraph.transform!(
    mtg,
    :Ra_PAR_f => (x -> fill(x, length(weather))) => :Ra_PAR_f,
    :sky_fraction => (x -> fill(x, length(weather))) => :sky_fraction,
    [:Ra_PAR_f, :Ra_NIR_f] => ((x, y) -> x .+ y) => :Ra_SW_f,
    (x -> 0.3) => :d,
    ignore_nothing = true
)

The design of MultiScaleTreeGraph.transform! is very close to the one from DataFrames. It helps us compute new variables (or attributes) from others, modify their units or rename them. Here we compute a value for each time-step by repeating the values of Ra_PAR_f and sky_fraction 3 times, and compute Ra_SW_f from the sum of Ra_PAR_f (absorbed radiation flux in the PAR) and Ra_NIR_f (...in the NIR). We also put a single constant value for d: 0.3 m.

Now let's choose the outputs we want to save. Here we choose to only output the leaf temperature Tₗ:

vars=Dict{Symbol,Any}(:Leaf => (:Tₗ,))
Dict{Symbol, Any} with 1 entry:
  :Leaf => (:Tₗ,)

Now we can run a simulation using run! from PlantSimEngine:

outs = run!(mtg, ModelMapping(models), weather, tracked_outputs=vars);

We can now extract the outputs from the simulation and store them in the MTG:

for ts in outs[:Leaf]
    ts.node.Tₗ = ts.Tₗ
end
Note

The outputs are stored in the MTG nodes. The node is also accessible from the simulation output as the node field.

And finally, we can visualize the outputs in 3D using PlantGeom's plantviz function:

f, ax, p = plantviz(mtg, color = :Tₗ, index = 2)
colorbar(f[1, 2], p)
f
Example block output

Note that we use the index keyword argument to select the time-step we want to visualize.