PlantBiophysics.jlPlantBiophysics.jl

Simulation Over Several Objects​#

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.

Build a small plant​#

Object describes the entities in the simulation. Here a scene contains one plant with a sunlit and a shaded leaf:

julia
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)]
2-element Vector{@NamedTuple{id::ObjectId{Symbol}, aPPFD::Float64}}: (id = ObjectId{Symbol}(:shade_leaf), aPPFD = 600.0) (id = ObjectId{Symbol}(:sun_leaf), aPPFD = 1500.0)

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.

Run every leaf over the shared weather​#

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:

julia
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])
6×6 DataFrame
Rowtimestepobject_idAGₛTₗλE
Int64SymbolFloat64?Float64?Float64?Float64?
11shade_leaf26.65431.3283817.6854156.394
21sun_leaf32.30711.671317.6796147.815
32shade_leaf27.44281.1766520.1947220.776
42sun_leaf34.10211.5294420.0705209.997
53shade_leaf27.79091.0598221.7784283.867
63sun_leaf35.12121.4071621.5407272.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:

julia
Dict(
    object.id => (Tₗ=object.status.Tₗ, A=object.status.A, Gₛ=object.status.Gₛ)
    for object in model_objects(scene; scale=:Leaf)
)
Dict{ObjectId{Symbol}, @NamedTuple{Tₗ::Float64, A::Float64, Gₛ::Float64}} with 2 entries: ObjectId{Symbol}(:shade_leaf) => (Tₗ = 21.7784, A = 27.7909, Gₛ = 1.05982) ObjectId{Symbol}(:sun_leaf) => (Tₗ = 21.5407, A = 35.1212, Gₛ = 1.40716)

When a model needs values from other objects​#

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.