PlantSimEngine.jlPlantSimEngine.jl

Run The Coupling On Several Objects​#

Run the same teaching models on two independent canopies with different initial development stages. Each canopy is one object; Many(scale=:Canopy) selects both. This lets you compare the two canopies without writing a loop inside each model. We start the canopies at different cumulative thermal times so their leaf area indices differ visibly over this short, five-day run.

julia
using PlantSimEngine, PlantMeteo, Dates, DataFrames
using PlantSimEngine.Examples

weather = read_weather(
    joinpath(pkgdir(PlantSimEngine), "examples/meteo_day.csv"),
    :Ri_SW_f => (x -> x .* 1e6 ./ 86_400) => :Ri_SW_f,
    :Ri_PAR_f => (x -> x .* 1e6 ./ 86_400) => :Ri_PAR_f,
    :Ri_NIR_f => (x -> x .* 1e6 ./ 86_400) => :Ri_NIR_f;
    duration=Day,
)

canopies = (
    Object(
        :canopy_a;
        scale=:Canopy,
        kind=:canopy,
        status=Status(TT_cu=600.0),
    ),
    Object(
        :canopy_b;
        scale=:Canopy,
        kind=:canopy,
        status=Status(TT_cu=900.0),
    ),
)
canopy_targets = Many(scale=:Canopy)

model = CompositeModel(
    canopies...;
    applications=(
        ModelSpec(
            ToyDegreeDaysCumulModel();
            name=:degree_days,
            on=canopy_targets,
        ),
        ModelSpec(ToyLAIModel(); name=:lai, on=canopy_targets),
        ModelSpec(Beer(0.6); name=:light, on=canopy_targets),
    ),
    environment=weather,
)
CompositeModel Objects: 2 (Canopy: 2) Shared environment: TimeStepTable (365 rows) Model applications: 3 degree_days: ToyDegreeDaysCumulModel lai: ToyLAIModel light: Beer

The names :canopy_a and :canopy_b identify the canopies throughout the simulation. Both use the same three models, but each keeps its own values. PlantSimEngine runs the models for each canopy, so the model code does not need a loop over canopies.

Run five daily steps and read the final values for each canopy:

julia
simulation = run!(model; steps=5, outputs=:all)
states = final_state(simulation, Many(scale=:Canopy))
Dict(
    id => (TT_cu=state.TT_cu, LAI=state.LAI, aPPFD=state.aPPFD)
    for (id, state) in states
)
Dict{Symbol, @NamedTuple{TT_cu::Float64, LAI::Float64, aPPFD::Float64}} with 2 entries: :canopy_b => (TT_cu = 900.0, LAI = 5.70251, aPPFD = 183.637) :canopy_a => (TT_cu = 600.0, LAI = 1.11722, aPPFD = 92.7283)

LAI is in m² of leaves per m² of ground; aPPFD is in μmol of absorbed PAR per m² of ground per second. The CSV radiation totals were converted to mean fluxes just as in the one-object example.

The saved results include the model application name, canopy name, and variable name. This keeps the two LAI time series separate:

julia
rows = collect_outputs(simulation)
lai_rows = rows[rows.variable .== :LAI, [
    :timestep,
    :application_id,
    :object_id,
    :value,
]]
first(lai_rows, 6)
6×4 DataFrame
Rowtimestepapplication_idobject_idvalue
Int64SymbolSymbolFloat64
11laicanopy_a1.11722
21laicanopy_b5.70251
32laicanopy_a1.11722
42laicanopy_b5.70251
53laicanopy_a1.11722
63laicanopy_b5.70251

You can also check which objects each model will run on. The table below should list both canopies for each application:

julia
select(
    DataFrame(Diagnostics.explain_applications(model)),
    :application_id,
    :target_ids,
)
3×2 DataFrame
Rowapplication_idtarget_ids
SymbolArray…
1degree_days[:canopy_a, :canopy_b]
2lai[:canopy_a, :canopy_b]
3light[:canopy_a, :canopy_b]

The two canopies run independently here. In one multiscale plant, you will connect a plant to its leaves and pass values between them. To compare the canopy time series visually, follow Collecting And Plotting Outputs.