Step-by-step multi-rate tutorial (hourly + daily + weekly)
This page builds a more complete MTG simulation that mixes three model rates:
- hourly at
Leaf, - daily at
Plant, - weekly at
Plant.
It runs for one week and exports clean series at each rate.
If you want the conceptual overview first, start with Introduction to multi-rate execution. This page assumes you already understand the two basic ideas introduced there:
- without
TimeStepModel(...), a model follows the meteo cadence; - once a model is forced to run more coarsely than its inputs, PlantSimEngine must reduce both model outputs and meteorological inputs to match that slower cadence.
The goal of this second page is to put those ideas into a more contextualized MTG example, where we mix hourly, daily, and weekly models in the same simulation and export clean time series at each rate.
1. Setup and example data
This tutorial is more contextualized than the previous one. To keep the mechanics readable, we work with a minimal MTG containing only one plant and one leaf. That way the exported tables stay small enough to inspect directly.
We also reuse package example assets instead of inventing new input files. In particular, we use a weather file available from the package examples:
examples/meteo_day.csvfor weather.
We start by importing the packages we need and by creating a very small MTG with only four nodes: a Scene, a Plant, one Internode, and one Leaf.
using PlantSimEngine
using PlantMeteo
using MultiScaleTreeGraph
using DataFrames
using CSV
using Dates
# Minimal plant: Scene -> Plant -> Internode -> Leaf
mtg = Node(NodeMTG("/", :Scene, 1, 0))
plant = Node(mtg, NodeMTG("+", :Plant, 1, 1))
internode = Node(plant, NodeMTG("/", :Internode, 1, 2))
Node(internode, NodeMTG("+", :Leaf, 1, 2))+ 4: Leaf
Next, we point to the bundled weather file and confirm that it exists:
meteo_path = joinpath(pkgdir(PlantSimEngine), "examples", "meteo_day.csv")
@assert isfile(meteo_path)The weather file bundled with the package is daily. Since this tutorial is about mixing several rates, we first convert one week of daily weather into an hourly weather table. The values are simply repeated within each day, which is perfectly fine here because the purpose is to illustrate scheduling and data flow rather than to create a realistic forcing dataset.
The first step is to read the file and keep only one week of rows:
daily_df = CSV.read(meteo_path, DataFrame, header=18)
week_df = first(daily_df, 7)| Row | year | dayofyear | date | duration | Tmin | Tmax | T | Precipitations | Wind | P | Rh | Ca | Ri_SW_f | Ri_PAR_f | Ri_NIR_f | TT |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Int64 | Int64 | Date | Int64 | Float64 | Float64 | Float64 | Float64 | Float64 | Float64 | Float64 | Float64 | Float64 | Float64 | Float64 | Float64 | |
| 1 | 2021 | 1 | 2021-01-01 | 1 | 3.9 | 9.2 | 5.95 | 0.5 | 4.29042 | 100.15 | 0.754583 | 400.0 | 6.5304 | 3.13459 | 3.39581 | 0.0 |
| 2 | 2021 | 2 | 2021-01-02 | 1 | 3.5 | 6.0 | 4.42083 | 1.0 | 6.74083 | 100.177 | 0.622083 | 400.0 | 3.5748 | 1.7159 | 1.8589 | 0.0 |
| 3 | 2021 | 3 | 2021-01-03 | 1 | 3.3 | 6.4 | 4.32917 | 0.0 | 6.15708 | 100.513 | 0.5975 | 400.0 | 5.1804 | 2.48659 | 2.69381 | 0.0 |
| 4 | 2021 | 4 | 2021-01-04 | 1 | 1.0 | 5.8 | 3.22083 | 1.1 | 1.79917 | 100.478 | 0.822917 | 400.0 | 6.4296 | 3.08621 | 3.34339 | 0.0 |
| 5 | 2021 | 5 | 2021-01-05 | 1 | 0.0 | 7.0 | 2.825 | 0.6 | 3.535 | 100.602 | 0.76125 | 400.0 | 7.4772 | 3.58906 | 3.88814 | 0.0 |
| 6 | 2021 | 6 | 2021-01-06 | 1 | -0.8 | 6.4 | 2.09583 | 0.0 | 3.36875 | 100.996 | 0.7 | 400.0 | 7.776 | 3.73248 | 4.04352 | 0.0 |
| 7 | 2021 | 7 | 2021-01-07 | 1 | -1.7 | 5.7 | 1.69167 | 0.0 | 2.99625 | 101.067 | 0.694583 | 400.0 | 7.1496 | 3.43181 | 3.71779 | 0.0 |
We then expand each day into 24 hourly Atmosphere rows:
hourly_rows = Atmosphere[]
for row in eachrow(week_df)
for h in 0:23
push!(hourly_rows,
Atmosphere(
date=DateTime(row.date) + Hour(h),
duration=Hour(1),
T=row.T,
Wind=row.Wind,
P=row.P,
Rh=row.Rh,
Ri_PAR_f=row.Ri_PAR_f,
Ri_SW_f=row.Ri_SW_f,
)
)
end
endFinally, we wrap those rows into a Weather object, which is what run! expects:
meteo_hourly = Weather(hourly_rows)
meteo_hourly[1:3] # show the first 3 rows of the hourly weather table| TimeStepTable{Atmosphere}(3 x 22): | ||||||||||||||||||||||
| 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 | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Dates.DateTime | Dates.Hour | Float64 | Float64 | Float64 | Float64 | Float64 | Float64 | Float64 | Float64 | Float64 | Float64 | Float64 | Float64 | Float64 | Float64 | Float64 | Float64 | Float64 | Float64 | Float64 | Float64 | |
| 1 | 2021-01-01T00:00:00 | 1 hour | 5.95 | 4.29042 | 100.15 | 0.754583 | 0.0 | 400.0 | 0.706089 | 0.935734 | 0.229645 | 1.25004 | 2.48693e6 | 0.0655856 | 0.52755 | 0.0649258 | Inf | 6.5304 | 3.13459 | Inf | Inf | Inf |
| 2 | 2021-01-01T01:00:00 | 1 hour | 5.95 | 4.29042 | 100.15 | 0.754583 | 0.0 | 400.0 | 0.706089 | 0.935734 | 0.229645 | 1.25004 | 2.48693e6 | 0.0655856 | 0.52755 | 0.0649258 | Inf | 6.5304 | 3.13459 | Inf | Inf | Inf |
| 3 | 2021-01-01T02:00:00 | 1 hour | 5.95 | 4.29042 | 100.15 | 0.754583 | 0.0 | 400.0 | 0.706089 | 0.935734 | 0.229645 | 1.25004 | 2.48693e6 | 0.0655856 | 0.52755 | 0.0649258 | Inf | 6.5304 | 3.13459 | Inf | Inf | Inf |
2. Defining simple models
Next we define three deliberately simple models:
- an hourly
Leafmodel that turns incoming radiation into an hourly assimilation value; - a daily
Plantmodel that sums hourly leaf assimilation over a day and also consumes daily meteorological aggregates; - a weekly
Plantmodel that sums daily plant assimilation into one weekly value.
These models are intentionally minimal. Their role is to make the rate changes and aggregation policies obvious.
We begin with the hourly leaf model. It reads hourly meteorological radiation and produces an hourly assimilation value:
PlantSimEngine.@process "tutorialleafhourly" verbose=false
struct TutorialLeafHourlyModel <: AbstractTutorialleafhourlyModel end
PlantSimEngine.inputs_(::TutorialLeafHourlyModel) = NamedTuple()
PlantSimEngine.outputs_(::TutorialLeafHourlyModel) = (leaf_assim_h=0.0,)
function PlantSimEngine.run!(::TutorialLeafHourlyModel, models, status, meteo, constants=nothing, extra=nothing)
status.leaf_assim_h = 0.004 * meteo.Ri_PAR_f
end
PlantSimEngine.output_policy(::Type{<:TutorialLeafHourlyModel}) = (; leaf_assim_h=Integrate())The output_policy(...) declaration matters for multi-rate use: it says that when a slower model consumes leaf_assim_h, the natural default is to integrate it over the coarser time window.
Now we define the daily plant model. It receives leaf assimilation values, aggregates them over a day, and also reads daily reduced meteo variables:
PlantSimEngine.@process "tutorialplantdaily" verbose=false
struct TutorialPlantDailyModel <: AbstractTutorialplantdailyModel end
PlantSimEngine.inputs_(::TutorialPlantDailyModel) = (leaf_assim_h=[0.0],)
PlantSimEngine.outputs_(::TutorialPlantDailyModel) = (plant_assim_d=0.0, rad_sw_day=0.0, T=0.0)
function PlantSimEngine.run!(::TutorialPlantDailyModel, models, status, meteo, constants=nothing, extra=nothing)
status.plant_assim_d = sum(status.leaf_assim_h)
status.rad_sw_day = meteo.Ri_SW_q
status.T = meteo.T
end
PlantSimEngine.output_policy(::Type{<:TutorialPlantDailyModel}) = (; plant_assim_d=Integrate())Again, output_policy(...) is used so that a coarser consumer can infer the appropriate default behavior for plant_assim_d.
Finally, we define the weekly plant model. It simply sums the daily plant assimilation values over one week:
PlantSimEngine.@process "tutorialplantweekly" verbose=false
struct TutorialPlantWeeklyModel <: AbstractTutorialplantweeklyModel end
PlantSimEngine.inputs_(::TutorialPlantWeeklyModel) = (plant_assim_d=[0.0],)
PlantSimEngine.outputs_(::TutorialPlantWeeklyModel) = (plant_assim_w=0.0,)
function PlantSimEngine.run!(::TutorialPlantWeeklyModel, models, status, meteo, constants=nothing, extra=nothing)
status.plant_assim_w = sum(status.plant_assim_d)
endAt this point nothing is multi-rate yet. We have simply defined three processes whose intended cadences are hourly, daily, and weekly. The multi-rate behavior is declared in the mapping.
3. Configure multi-rate mapping
This is the heart of the tutorial. The mapping below does three things at once:
- it assigns each model to a scale;
- it declares the timestep at which each model should run;
- it defines how values move between rates and between scales.
Two pieces are especially important here:
TimeStepModel(...)states the model cadence;- PlantMeteo reduces meteorological inputs automatically when a model runs more coarsely than the weather data.
For model-to-model bindings, this tutorial relies on automatic source inference plus output_policy(...) on the source models. That keeps the main example compact while still exercising multi-rate input aggregation.
We start by defining the three clocks used in the simulation. These are the cadences that will later be assigned to the three models:
hourly = 1.0
daily = ClockSpec(24.0, 0.0)
weekly = ClockSpec(168.0, 0.0)ClockSpec{Float64}(168.0, 0.0)The leaf model is straightforward: it runs hourly and is scoped to the current plant. There is no multiscale mapping or meteo reduction to declare here, because the leaf model is the fastest model in this example and directly consumes the hourly weather rows:
leaf_spec = TutorialLeafHourlyModel() |> ModelSpec |> TimeStepModel(hourly)ModelSpec{Main.TutorialLeafHourlyModel, Nothing, Float64, @NamedTuple{}, @NamedTuple{}, Nothing, @NamedTuple{}, Symbol}(Main.TutorialLeafHourlyModel(), nothing, 1.0, NamedTuple(), NamedTuple(), nothing, NamedTuple(), :global)So at this point we have simply said: "run the leaf model every hour"
The daily plant model is where multi-rate coupling becomes visible. It:
- receives
leaf_assim_hfrom the:Leafscale throughMultiScaleModel(...); - runs daily;
- receives daily meteorological aggregates from the hourly weather automatically.
The important idea is that this model does not read the raw hourly values directly. Instead, it sees a daily view of those data:
leaf_assim_his integrated over the daily window because of the source model'soutput_policy(...);Tis turned into a daily mean by the default PlantMeteo sampling rules;Ri_SW_qis computed by integratingRi_SW_fover the day.
plant_daily_spec =
TutorialPlantDailyModel() |>
ModelSpec |>
MultiScaleModel([:leaf_assim_h => :Leaf]) |>
TimeStepModel(daily)ModelSpec{Main.TutorialPlantDailyModel, Vector{Pair{Union{Symbol, PreviousTimeStep}, Union{Pair{Symbol, Symbol}, Vector{Pair{Symbol, Symbol}}}}}, ClockSpec{Float64}, @NamedTuple{}, @NamedTuple{}, Nothing, @NamedTuple{}, Symbol}(Main.TutorialPlantDailyModel(), Pair{Union{Symbol, PreviousTimeStep}, Union{Pair{Symbol, Symbol}, Vector{Pair{Symbol, Symbol}}}}[:leaf_assim_h => (:Leaf => :leaf_assim_h)], ClockSpec{Float64}(24.0, 0.0), NamedTuple(), NamedTuple(), nothing, NamedTuple(), :global)This block is the first place where the "multi-rate" behavior is really visible: one model consumes fine-grained biological outputs and fine-grained meteorology, but only after both have been reduced to the model's own daily cadence.
The weekly plant model is simpler again: it only needs to run weekly and receive the daily plant output automatically. Since plant_assim_d has a unique producer and already declares its own output_policy(...), we do not need to add any explicit binding here:
plant_weekly_spec =
TutorialPlantWeeklyModel() |>
ModelSpec |>
TimeStepModel(weekly)ModelSpec{Main.TutorialPlantWeeklyModel, Nothing, ClockSpec{Float64}, @NamedTuple{}, @NamedTuple{}, Nothing, @NamedTuple{}, Symbol}(Main.TutorialPlantWeeklyModel(), nothing, ClockSpec{Float64}(168.0, 0.0), NamedTuple(), NamedTuple(), nothing, NamedTuple(), :global)So this weekly model effectively says: "take the daily plant assimilation stream, reduce it again to my weekly cadence, and run once per week."
We can now assemble the full mapping:
mapping = ModelMapping(
:Leaf => (leaf_spec,),
:Plant => (plant_daily_spec, plant_weekly_spec),
)ModelMapping
validated: true (valid)
multirate: true
scales (2): Leaf, Plant
- Leaf: 1 model(s), Processes=tutorialleafhourly
- Plant: 2 model(s), Processes=tutorialplantdaily, tutorialplantweekly
Timing groups:
- explicit 1.0 (ModelSpec): 1 model(s)
- explicit ClockSpec{Float64}(168.0, 0.0) (ModelSpec): 1 model(s)
- explicit ClockSpec{Float64}(24.0, 0.0) (ModelSpec): 1 model(s)
Get resolved timings with: `effective_rate_summary(modelmapping, meteo)`
Variables to initialize: none
Recommendations:
- Multirate is enabled from mapping metadata; `run!(mtg, mapping, ...)` auto-detects it.
Reading this mapping from top to bottom:
- the
Leafmodel runs hourly and producesleaf_assim_h; - the daily
Plantmodel receives leaf values from theLeafscale throughMultiScaleModel([:leaf_assim_h => :Leaf]), then integrates them over a day; - that same daily model also receives daily meteorological summaries through the default PlantMeteo sampling rules;
- the weekly
Plantmodel integrates the daily plant output into one weekly value.
In this tutorial, explicit InputBindings(...) are omitted because each input has a unique, inferable producer and the default reduction policy is declared on the source model with output_policy(...).
In more complex mappings, you should use explicit InputBindings(process=..., scale=..., var=..., policy=...) when:
- several models can produce the same input variable;
- the same process exists at several reachable scales;
- the source variable has a different name than the consumer input;
- you want to override the producer's default policy for a specific mapping.
MeteoBindings(...) is also omitted on purpose in the main example. PlantSimEngine delegates weather sampling to PlantMeteo, which already defines default transformations for common Atmosphere variables such as T, Rh, and radiation aliases like Ri_SW_q.
Add explicit MeteoBindings(...) when:
- you want a non-default reducer;
- the model expects a target variable with a different source name;
- the variable is not covered by PlantMeteo default transforms;
- you want the mapping to state the weather aggregation rule explicitly.
# The same daily model, with weather aggregation rules written explicitly.
plant_daily_spec_explicit_meteo = ModelSpec(TutorialPlantDailyModel()) |>
MultiScaleModel([:leaf_assim_h => :Leaf]) |>
TimeStepModel(daily) |>
MeteoBindings(
;
T=MeanWeighted(),
Ri_SW_q=(source=:Ri_SW_f, reducer=RadiationEnergy()),
)ModelSpec{Main.TutorialPlantDailyModel, Vector{Pair{Union{Symbol, PreviousTimeStep}, Union{Pair{Symbol, Symbol}, Vector{Pair{Symbol, Symbol}}}}}, ClockSpec{Float64}, @NamedTuple{}, @NamedTuple{T::MeanWeighted, Ri_SW_q::@NamedTuple{source::Symbol, reducer::RadiationEnergy}}, Nothing, @NamedTuple{}, Symbol}(Main.TutorialPlantDailyModel(), Pair{Union{Symbol, PreviousTimeStep}, Union{Pair{Symbol, Symbol}, Vector{Pair{Symbol, Symbol}}}}[:leaf_assim_h => (:Leaf => :leaf_assim_h)], ClockSpec{Float64}(24.0, 0.0), NamedTuple(), (T = MeanWeighted(), Ri_SW_q = (source = :Ri_SW_f, reducer = RadiationEnergy())), nothing, NamedTuple(), :global)4. Run and export hourly/daily/weekly series
Now we run the simulation and request three exported series. This is a good place to distinguish two related outputs returned by run!:
- the regular simulation outputs (
out_statusbelow), which still contain the model outputs tracked during the run; - the explicitly requested exported series (
exportedbelow), which are the clean hourly/daily/weekly tables we asked PlantSimEngine to materialize.
We use OutputRequest(...) to say which variable we want and on which clock. Here again we keep the example minimal: process= is omitted because each requested output has a unique canonical publisher.
We first declare the export requests. One request keeps the hourly leaf series, another exports the daily plant series, and the last one exports the weekly plant series.
The point of these requests is to obtain three clean tables that each live at a single rate, instead of having to reconstruct those time series manually from the full simulation outputs:
req_leaf_hourly = OutputRequest(:Leaf, :leaf_assim_h;
name=:leaf_assim_hourly,
)
req_plant_daily = OutputRequest(:Plant, :plant_assim_d;
name=:plant_assim_daily,
clock=daily,
)
req_plant_weekly = OutputRequest(:Plant, :plant_assim_w;
name=:plant_assim_weekly,
clock=weekly,
)OutputRequest{Nothing, HoldLast, ClockSpec{Float64}}(:Plant, :plant_assim_w, :plant_assim_weekly, nothing, HoldLast(), ClockSpec{Float64}(168.0, 0.0))Then we run the simulation and ask PlantSimEngine to return both the regular simulation outputs and the explicitly requested exported series:
out_statuscontains the regular tracked outputs of the simulation;exportedcontains the resampled, per-request tables defined above.
out_status, exported = run!(
mtg,
mapping,
meteo_hourly;
executor=SequentialEx(),
tracked_outputs=[req_leaf_hourly, req_plant_daily, req_plant_weekly],
return_requested_outputs=true,
)(Dict{Symbol, Vector}(:Leaf => Status{(:timestep, :node, :leaf_assim_h), Tuple{Base.RefValue{Int64}, Base.RefValue{MultiScaleTreeGraph.Node{MultiScaleTreeGraph.NodeMTG, MultiScaleTreeGraph.ColumnarAttrs}}, Base.RefValue{Float64}}}[Status(timestep = 1, node = + 4: Leaf
, leaf_assim_h = 0.012538368000000001), Status(timestep = 2, node = + 4: Leaf
, leaf_assim_h = 0.012538368000000001), Status(timestep = 3, node = + 4: Leaf
, leaf_assim_h = 0.012538368000000001), Status(timestep = 4, node = + 4: Leaf
, leaf_assim_h = 0.012538368000000001), Status(timestep = 5, node = + 4: Leaf
, leaf_assim_h = 0.012538368000000001), Status(timestep = 6, node = + 4: Leaf
, leaf_assim_h = 0.012538368000000001), Status(timestep = 7, node = + 4: Leaf
, leaf_assim_h = 0.012538368000000001), Status(timestep = 8, node = + 4: Leaf
, leaf_assim_h = 0.012538368000000001), Status(timestep = 9, node = + 4: Leaf
, leaf_assim_h = 0.012538368000000001), Status(timestep = 10, node = + 4: Leaf
, leaf_assim_h = 0.012538368000000001) … Status(timestep = 159, node = + 4: Leaf
, leaf_assim_h = 0.013727232), Status(timestep = 160, node = + 4: Leaf
, leaf_assim_h = 0.013727232), Status(timestep = 161, node = + 4: Leaf
, leaf_assim_h = 0.013727232), Status(timestep = 162, node = + 4: Leaf
, leaf_assim_h = 0.013727232), Status(timestep = 163, node = + 4: Leaf
, leaf_assim_h = 0.013727232), Status(timestep = 164, node = + 4: Leaf
, leaf_assim_h = 0.013727232), Status(timestep = 165, node = + 4: Leaf
, leaf_assim_h = 0.013727232), Status(timestep = 166, node = + 4: Leaf
, leaf_assim_h = 0.013727232), Status(timestep = 167, node = + 4: Leaf
, leaf_assim_h = 0.013727232), Status(timestep = 168, node = + 4: Leaf
, leaf_assim_h = 0.32945356799999986)], :Plant => Status{(:timestep, :node, :T, :rad_sw_day, :plant_assim_d, :plant_assim_w, :leaf_assim_h), Tuple{Base.RefValue{Int64}, Base.RefValue{MultiScaleTreeGraph.Node{MultiScaleTreeGraph.NodeMTG, MultiScaleTreeGraph.ColumnarAttrs}}, Vararg{Base.RefValue{Float64}, 5}}}[Status(timestep = 1, node = + 2: Plant
└─ / 3: Internode
└─ + 4: Leaf
, T = 0.0, rad_sw_day = 0.0, plant_assim_d = 0.0, plant_assim_w = 0.0, leaf_assim_h = 0.012538368000000001), Status(timestep = 2, node = + 2: Plant
└─ / 3: Internode
└─ + 4: Leaf
, T = 0.0, rad_sw_day = 0.0, plant_assim_d = 0.0, plant_assim_w = 0.0, leaf_assim_h = 0.012538368000000001), Status(timestep = 3, node = + 2: Plant
└─ / 3: Internode
└─ + 4: Leaf
, T = 0.0, rad_sw_day = 0.0, plant_assim_d = 0.0, plant_assim_w = 0.0, leaf_assim_h = 0.012538368000000001), Status(timestep = 4, node = + 2: Plant
└─ / 3: Internode
└─ + 4: Leaf
, T = 0.0, rad_sw_day = 0.0, plant_assim_d = 0.0, plant_assim_w = 0.0, leaf_assim_h = 0.012538368000000001), Status(timestep = 5, node = + 2: Plant
└─ / 3: Internode
└─ + 4: Leaf
, T = 0.0, rad_sw_day = 0.0, plant_assim_d = 0.0, plant_assim_w = 0.0, leaf_assim_h = 0.012538368000000001), Status(timestep = 6, node = + 2: Plant
└─ / 3: Internode
└─ + 4: Leaf
, T = 0.0, rad_sw_day = 0.0, plant_assim_d = 0.0, plant_assim_w = 0.0, leaf_assim_h = 0.012538368000000001), Status(timestep = 7, node = + 2: Plant
└─ / 3: Internode
└─ + 4: Leaf
, T = 0.0, rad_sw_day = 0.0, plant_assim_d = 0.0, plant_assim_w = 0.0, leaf_assim_h = 0.012538368000000001), Status(timestep = 8, node = + 2: Plant
└─ / 3: Internode
└─ + 4: Leaf
, T = 0.0, rad_sw_day = 0.0, plant_assim_d = 0.0, plant_assim_w = 0.0, leaf_assim_h = 0.012538368000000001), Status(timestep = 9, node = + 2: Plant
└─ / 3: Internode
└─ + 4: Leaf
, T = 0.0, rad_sw_day = 0.0, plant_assim_d = 0.0, plant_assim_w = 0.0, leaf_assim_h = 0.012538368000000001), Status(timestep = 10, node = + 2: Plant
└─ / 3: Internode
└─ + 4: Leaf
, T = 0.0, rad_sw_day = 0.0, plant_assim_d = 0.0, plant_assim_w = 0.0, leaf_assim_h = 0.012538368000000001) … Status(timestep = 159, node = + 2: Plant
└─ / 3: Internode
└─ + 4: Leaf
, T = 2.095833333333333, rad_sw_day = 0.6718463999999996, plant_assim_d = 0.3583180799999999, plant_assim_w = 0.0, leaf_assim_h = 0.013727232), Status(timestep = 160, node = + 2: Plant
└─ / 3: Internode
└─ + 4: Leaf
, T = 2.095833333333333, rad_sw_day = 0.6718463999999996, plant_assim_d = 0.3583180799999999, plant_assim_w = 0.0, leaf_assim_h = 0.013727232), Status(timestep = 161, node = + 2: Plant
└─ / 3: Internode
└─ + 4: Leaf
, T = 2.095833333333333, rad_sw_day = 0.6718463999999996, plant_assim_d = 0.3583180799999999, plant_assim_w = 0.0, leaf_assim_h = 0.013727232), Status(timestep = 162, node = + 2: Plant
└─ / 3: Internode
└─ + 4: Leaf
, T = 2.095833333333333, rad_sw_day = 0.6718463999999996, plant_assim_d = 0.3583180799999999, plant_assim_w = 0.0, leaf_assim_h = 0.013727232), Status(timestep = 163, node = + 2: Plant
└─ / 3: Internode
└─ + 4: Leaf
, T = 2.095833333333333, rad_sw_day = 0.6718463999999996, plant_assim_d = 0.3583180799999999, plant_assim_w = 0.0, leaf_assim_h = 0.013727232), Status(timestep = 164, node = + 2: Plant
└─ / 3: Internode
└─ + 4: Leaf
, T = 2.095833333333333, rad_sw_day = 0.6718463999999996, plant_assim_d = 0.3583180799999999, plant_assim_w = 0.0, leaf_assim_h = 0.013727232), Status(timestep = 165, node = + 2: Plant
└─ / 3: Internode
└─ + 4: Leaf
, T = 2.095833333333333, rad_sw_day = 0.6718463999999996, plant_assim_d = 0.3583180799999999, plant_assim_w = 0.0, leaf_assim_h = 0.013727232), Status(timestep = 166, node = + 2: Plant
└─ / 3: Internode
└─ + 4: Leaf
, T = 2.095833333333333, rad_sw_day = 0.6718463999999996, plant_assim_d = 0.3583180799999999, plant_assim_w = 0.0, leaf_assim_h = 0.013727232), Status(timestep = 167, node = + 2: Plant
└─ / 3: Internode
└─ + 4: Leaf
, T = 2.095833333333333, rad_sw_day = 0.6718463999999996, plant_assim_d = 0.3583180799999999, plant_assim_w = 0.0, leaf_assim_h = 0.013727232), Status(timestep = 168, node = + 2: Plant
└─ / 3: Internode
└─ + 4: Leaf
, T = 1.6916666666666664, rad_sw_day = 0.6177254400000002, plant_assim_d = 2.03295744, plant_assim_w = 2.03295744, leaf_assim_h = 0.32945356799999986)]), Dict{Symbol, Any}(:leaf_assim_hourly => 168×6 DataFrame
Row │ timestep scale process var node value
│ Int64 Symbol Symbol Symbol Int64 Any
─────┼────────────────────────────────────────────────────────────────────
1 │ 1 Leaf tutorialleafhourly leaf_assim_h 4 missing
2 │ 2 Leaf tutorialleafhourly leaf_assim_h 4 missing
3 │ 3 Leaf tutorialleafhourly leaf_assim_h 4 missing
4 │ 4 Leaf tutorialleafhourly leaf_assim_h 4 missing
5 │ 5 Leaf tutorialleafhourly leaf_assim_h 4 missing
6 │ 6 Leaf tutorialleafhourly leaf_assim_h 4 missing
7 │ 7 Leaf tutorialleafhourly leaf_assim_h 4 missing
8 │ 8 Leaf tutorialleafhourly leaf_assim_h 4 missing
⋮ │ ⋮ ⋮ ⋮ ⋮ ⋮ ⋮
162 │ 162 Leaf tutorialleafhourly leaf_assim_h 4 missing
163 │ 163 Leaf tutorialleafhourly leaf_assim_h 4 missing
164 │ 164 Leaf tutorialleafhourly leaf_assim_h 4 missing
165 │ 165 Leaf tutorialleafhourly leaf_assim_h 4 missing
166 │ 166 Leaf tutorialleafhourly leaf_assim_h 4 missing
167 │ 167 Leaf tutorialleafhourly leaf_assim_h 4 missing
168 │ 168 Leaf tutorialleafhourly leaf_assim_h 4 missing
153 rows omitted, :plant_assim_daily => 7×6 DataFrame
Row │ timestep scale process var node value
│ Int64 Symbol Symbol Symbol Int64 Any
─────┼─────────────────────────────────────────────────────────────────────
1 │ 24 Plant tutorialplantdaily plant_assim_d 2 missing
2 │ 48 Plant tutorialplantdaily plant_assim_d 2 missing
3 │ 72 Plant tutorialplantdaily plant_assim_d 2 missing
4 │ 96 Plant tutorialplantdaily plant_assim_d 2 missing
5 │ 120 Plant tutorialplantdaily plant_assim_d 2 missing
6 │ 144 Plant tutorialplantdaily plant_assim_d 2 missing
7 │ 168 Plant tutorialplantdaily plant_assim_d 2 missing , :plant_assim_weekly => 1×6 DataFrame
Row │ timestep scale process var node value
│ Int64 Symbol Symbol Symbol Int64 Any
─────┼──────────────────────────────────────────────────────────────────────
1 │ 168 Plant tutorialplantweekly plant_assim_w 2 2.03296))Finally, we extract the exported tables we want to inspect. At this point we are no longer dealing with abstract stream definitions: we now have actual DataFrame objects containing hourly, daily, and weekly series.
leaf_hourly_df = exported[:leaf_assim_hourly]
plant_daily_df = exported[:plant_assim_daily]
plant_weekly_df = exported[:plant_assim_weekly]| Row | timestep | scale | process | var | node | value |
|---|---|---|---|---|---|---|
| Int64 | Symbol | Symbol | Symbol | Int64 | Any | |
| 1 | 168 | Plant | tutorialplantweekly | plant_assim_w | 2 | 2.03296 |
The exported tables already have the cadence we asked for, so they are much easier to inspect than a single mixed output table.
We can start with a few basic checks on the number of rows. These checks are a simple way to confirm that the export clocks did what we expected:
@show nrow(leaf_hourly_df) # 168 (1 leaf x 168 hours)
@show nrow(plant_daily_df) # 7 (1 plant x 7 days)
@show nrow(plant_weekly_df) # 1 (1 plant x 1 week)1The hourly table has one row per hour, the daily table one row per day, and the weekly table one row for the whole run.
To compare the hourly and daily outputs directly, we group the hourly series by day and sum it manually. This lets us check that the daily plant model really did receive the integrated hourly leaf assimilation:
leaf_hourly_df.day = repeat(1:7, inner=24)
leaf_hourly_sum = combine(groupby(leaf_hourly_df, :day), :value => sum => :leaf_assim_h_sum)| Row | day | leaf_assim_h_sum |
|---|---|---|
| Int64 | Missing | |
| 1 | 1 | missing |
| 2 | 2 | missing |
| 3 | 3 | missing |
| 4 | 4 | missing |
| 5 | 5 | missing |
| 6 | 6 | missing |
| 7 | 7 | missing |
Those row counts match the intended design of the example: one hourly series for seven days, one daily series for seven days, and one weekly aggregate for the whole run.
We can also manually recompute the daily sums from the hourly exported series and compare them with the daily model output:
plant_daily_df| Row | timestep | scale | process | var | node | value |
|---|---|---|---|---|---|---|
| Int64 | Symbol | Symbol | Symbol | Int64 | Any | |
| 1 | 24 | Plant | tutorialplantdaily | plant_assim_d | 2 | missing |
| 2 | 48 | Plant | tutorialplantdaily | plant_assim_d | 2 | missing |
| 3 | 72 | Plant | tutorialplantdaily | plant_assim_d | 2 | missing |
| 4 | 96 | Plant | tutorialplantdaily | plant_assim_d | 2 | missing |
| 5 | 120 | Plant | tutorialplantdaily | plant_assim_d | 2 | missing |
| 6 | 144 | Plant | tutorialplantdaily | plant_assim_d | 2 | missing |
| 7 | 168 | Plant | tutorialplantdaily | plant_assim_d | 2 | missing |
This confirms that the daily assimilation values correspond to the sum of the hourly leaf assimilation collected over each day.
The regular outputs returned by run! are still available as well, and can be converted to DataFrames in the usual way. This is useful when you want both:
- clean resampled exports for analysis;
- the usual simulation outputs for debugging or broader inspection.
outs = convert_outputs(out_status, DataFrame)
outs[:Plant][1:3,:]| Row | node | timestep | T | rad_sw_day | plant_assim_d | plant_assim_w | leaf_assim_h |
|---|---|---|---|---|---|---|---|
| Int64 | Int64 | Float64 | Float64 | Float64 | Float64 | Float64 | |
| 1 | 2 | 1 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0125384 |
| 2 | 2 | 2 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0125384 |
| 3 | 2 | 3 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0125384 |
5. Where to go next
This page keeps the main walkthrough focused on a complete but still compact example. Once that example is clear, the next step is usually to learn the explicit configuration tools that become useful in larger mappings:
InputBindings(...)when inference is ambiguous or too implicit;MeteoBindings(...)when PlantMeteo defaults are not enough;MeteoWindow(...)for calendar-aligned aggregation;OutputRequest(...)when you want explicit export-time clocks and policies;ScopeModel(...),explain_model_specs(...), andresolved_model_specs(...)for larger and harder-to-debug MTGs.
Those topics are grouped in Advanced multi-rate configuration.