This tutorial runs the same coupled PlantBiophysics model on two leaves. The leaves share one weather sequence, while each keeps its own state and absorbed radiation.
Object describes the entities in the simulation. Here a scene contains one plant with a sunlit and a shaded leaf:
using PlantBiophysics, PlantSimEngine, PlantMeteo, Dates, DataFrames
weather = Weather([
Atmosphere(T=20.0, Wind=1.0, P=101.3, Rh=0.65, duration=Hour(1)),
Atmosphere(T=23.0, Wind=1.5, P=101.3, Rh=0.60, duration=Hour(1)),
Atmosphere(T=25.0, Wind=2.0, P=101.3, Rh=0.55, duration=Hour(1)),
])
applications = (
ModelSpec(Monteith(); name=:energy_balance, on=Many(scale=:Leaf)),
ModelSpec(Fvcb(); name=:photosynthesis, on=Many(scale=:Leaf)),
ModelSpec(Medlyn(0.03, 12.0); name=:stomatal_conductance, on=Many(scale=:Leaf)),
)
scene = CompositeModel(
Object(:scene; scale=:Scene, kind=:scene),
Object(:plant; scale=:Plant, kind=:plant, parent=:scene),
Object(
:sun_leaf;
scale=:Leaf,
kind=:plant,
parent=:plant,
status=Status(
Ra_SW_f=20.0,
sky_fraction=1.0,
aPPFD=1500.0,
d=0.03,
),
),
Object(
:shade_leaf;
scale=:Leaf,
kind=:plant,
parent=:plant,
status=Status(
Ra_SW_f=8.0,
sky_fraction=0.5,
aPPFD=600.0,
d=0.02,
),
);
applications=applications,
environment=weather,
)
[(id=object.id, aPPFD=object.status.aPPFD) for
object in model_objects(scene; scale=:Leaf)]Many(scale=:Leaf) applies each model once to every leaf. Model parameters are shared, while the two Status objects remain independent. Radiation is held constant per leaf here; see Simulation over several time steps for externally prescribed drivers that vary over time.
outputs=:all saves the calculated values for both leaves at each timestep. As in the previous tutorial, we collect and reshape them into a table:
simulation = run!(scene; steps=length(weather), outputs=:all)
rows = collect_outputs(simulation; sink=DataFrame)
leaf_rows = subset(
rows,
:application_id => ByRow(==(:energy_balance)),
:variable => ByRow(in((:Tₗ, :A, :Gₛ, :λE))),
)
leaf_results = unstack(
select(leaf_rows, :timestep, :object_id, :variable, :value),
[:timestep, :object_id],
:variable,
:value,
)
sort!(leaf_results, [:timestep, :object_id])| Row | timestep | object_id | A | Gₛ | Tₗ | λE |
|---|---|---|---|---|---|---|
| Int64 | Symbol | Float64? | Float64? | Float64? | Float64? | |
| 1 | 1 | shade_leaf | 26.6543 | 1.32838 | 17.6854 | 156.394 |
| 2 | 1 | sun_leaf | 32.3071 | 1.6713 | 17.6796 | 147.815 |
| 3 | 2 | shade_leaf | 27.4428 | 1.17665 | 20.1947 | 220.776 |
| 4 | 2 | sun_leaf | 34.1021 | 1.52944 | 20.0705 | 209.997 |
| 5 | 3 | shade_leaf | 27.7909 | 1.05982 | 21.7784 | 283.867 |
| 6 | 3 | sun_leaf | 35.1212 | 1.40716 | 21.5407 | 272.392 |
There is one row per leaf and timestep. Use object_id to distinguish :sun_leaf from :shade_leaf. Tₗ is in °C, A in µmol CO₂ m⁻² s⁻¹, Gₛ in mol CO₂ m⁻² s⁻¹, and λE in W m⁻², all fluxes per unit leaf area. The latest state also remains available on each object:
Dict(
object.id => (Tₗ=object.status.Tₗ, A=object.status.A, Gₛ=object.status.Gₛ)
for object in model_objects(scene; scale=:Leaf)
)The two leaves above only share their weather. If you later add a model that combines leaf results at plant scale, it needs to know which leaves belong to that plant. PlantSimEngine provides selectors for this:
Self() means only the object where the consuming application runs, this is the default.
Subtree() means that object and its descendants. A plant-scale model uses Many(scale=:Leaf, within=Subtree(), var=:A) to read only its own leaves.
SceneScope() searches the complete scene and is appropriate for a scene-scale aggregate.
The leaf assimilation rates above are per unit leaf area. To calculate a whole-plant assimilation rate, multiply each rate by its leaf area before adding them. Simply adding the rates would not give a whole-plant total. See PlantSimEngine's coupling guide when you need to implement this type of model.
Continue with Whole-plant simulation from an MTG to adapt an existing plant topology to the same applications.