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):
╭─────┬─────────────────────┬──────────────────────┬─────────┬─────────┬────────
│ Row │ date │ duration │ T │ Wind │ ⋯
│ │ Dates.DateTime │ Dates.CompoundPeriod │ Float64 │ Float64 │ Float ⋯
├─────┼─────────────────────┼──────────────────────┼─────────┼─────────┼────────
│ 1 │ 2016-06-12T12:00:00 │ 30 minutes │ 25.0 │ 1.0 │ 101.3 ⋯
│ 2 │ 2016-06-12T12:30:00 │ 30 minutes │ 26.0 │ 1.5 │ 101.3 ⋯
│ 3 │ 2016-06-12T13:00:00 │ 30 minutes │ 25.3 │ 1.5 │ 101.3 ⋯
╰─────┴─────────────────────┴──────────────────────┴─────────┴─────────┴────────
25 columns omitted
Metadata: `Dict{String, Any}("name" => "Aquiares", "latitude" => 15.0, "altitude" => 100.0, "use" => [:clearness], "file" => "/home/runner/.julia/packages/PlantMeteo/1u2oP/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{String, Tuple} with 2 entries:
"Metamer" => (Translucent{Float64}(0.1, σ{Float64}(0.15, 0.9)),)
"Leaf" => (Monteith{Float64, Int64}(2, 1, 0.955, 10, 0.01), Translucent{Fl…
Let's check which variables we need to provide for our model configuration:
to_initialize(models, mtg)
Dict{String, 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
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{String,Any}("Leaf" => (:Tₗ,))
Dict{String, Any} with 1 entry:
"Leaf" => (:Tₗ,)
Now we can run a simulation using run!
from PlantSimEngine
:
outs = run!(mtg, models, weather, outputs=vars);
We can now extract the outputs from the simulation and store them in the MTG:
outputs_leaves = outputs(outs)["Leaf"]
for ts in eachindex(outputs_leaves[:node])
for node in outputs_leaves[:node][ts]
node[:Tₗ] = outputs_leaves[:Tₗ][ts]
end
end
And finally, we can visualize the outputs in 3D using PlantGeom's viz
function:
f, ax, p = viz(mtg, color = :Tₗ, index = 2)
colorbar(f[1, 2], p)
f
Note that we use the index
keyword argument to select the time-step we want to visualize.