Model execution
Simulation order
PlantSimEngine.jl uses the ModelMapping to automatically compute a dependency graph between the models and run the simulation in the correct order. When running a simulation with run!, the models are then executed following this simple set of rules:
- Independent models are run first. A model is independent if it can be run independently from other models, only using initializations (or nothing).
- Then, models that have a dependency on other models are run. The first ones are the ones that depend on an independent model. Then the ones that are children of the second ones, and then their children ... until no children are found anymore. There are two types of children models (i.e. dependencies): hard and soft dependencies:
- Hard dependencies are always run before soft dependencies. A hard dependency is a model that is directly called by another model. It is declared as such by its parent that lists its hard-dependencies as
dep. See this example that showsProcess2Modeldefining a hard dependency on any model that simulatesprocess1. - Soft dependencies are then run sequentially. A model has a soft dependency on another model if one or more of its inputs is computed by another model. If a soft dependency has several parent nodes (e.g. two different models compute two inputs of the model), it is run only if all its parent nodes have been run already. In practice, when we visit a node that has one of its parent that did not run already, we stop the visit of this branch. The node will eventually be visited from the branch of the last parent that was run.
- Hard dependencies are always run before soft dependencies. A hard dependency is a model that is directly called by another model. It is declared as such by its parent that lists its hard-dependencies as
Multi-rate model configuration (experimental)
For multiscale simulations, model usage is configured in the mapping through ModelSpec transforms:
TimeStepModel(...): sets model execution clock.InputBindings(...): sets producer, source variable, optional source scale, and policy for each consumer input.MeteoBindings(...): sets weather aggregation rules at the model clock for meteo variables.MeteoWindow(...): sets weather row selection strategy (RollingWindow()orCalendarWindow(...)).OutputRouting(...): sets whether an output is canonical (:canonical) or stream-only (:stream_only).ScopeModel(...): partitions producer streams by scope (:global,:plant,:scene,:self) for multi-entity simulations.
For a compact overview of all model traits and precedence rules, see Model traits.
If users do not provide MeteoBindings(...) or MeteoWindow(...), the runtime can infer defaults from model traits:
timespec(::Type{<:MyModel})output_policy(::Type{<:MyModel})timestep_hint(::Type{<:MyModel})meteo_hint(::Type{<:MyModel})
For timestep specifically, runtime is meteo-first (see decision flow below): timestep_hint is used for compatibility validation (and user guidance), not to auto-assign model clocks.
If users do not provide InputBindings(...), runtime infers same-name bindings:
- first from a unique producer at the same scale;
- otherwise from a unique producer at another scale;
- if no producer exists, input stays unresolved (so initialization/forced values can be used);
- if multiple producers are possible, runtime errors and asks for explicit
InputBindings(...).
For inferred bindings, default policy is resolved as:
- producer
output_policyfor the source output when defined; - otherwise
HoldLast().
output_policy is a default hint, applied only when an output stream is actually read by another model input (or output export). Unused outputs do not trigger integration/reduction work.
Explicit mapping policies still have priority (InputBindings(..., policy=...)) and can complement trait defaults by defining additional bindings with different policies.
For timestep hints:
timestep_hint.requiredis a hard compatibility constraint when runtime uses meteo-derived timestep.timestep_hint.preferredis informational only (it does not set runtime timestep by itself).- Explicit
TimeStepModel(...)always takes precedence.
For meteo hints:
- return
(; bindings=..., window=...)wherebindingsmatchesMeteoBindings(...)andwindowmatchesMeteoWindow(...). - Explicit
MeteoBindings(...)/MeteoWindow(...)always take precedence.
Inspection helpers:
resolved_model_specs(mapping)returns resolved specs after inference/validation.explain_model_specs(mapping_or_sim)prints a compact summary (timestep,input_bindings,meteo_bindings,meteo_window) for each model process.
Policy parameterization:
Integrate()defaults toSumReducer(); you can pass another reducer, e.g.Integrate(MeanReducer())orIntegrate(vals -> maximum(vals) - minimum(vals)).Aggregate()defaults toMeanReducer(); you can pass reducers such asAggregate(MaxReducer()).- Difference between
IntegrateandAggregate: with the same reducer they are runtime-equivalent. In practice, only defaults and naming intent differ (Integratefor accumulation,Aggregatefor summary statistics). Interpolate()defaults tomode=:linear, extrapolation=:linear; useInterpolate(; mode=:hold, extrapolation=:hold)for hold behavior.- The same reducer objects are reused by meteo sampling (
MeteoBindings) and by windowed policies (Integrate,Aggregate). - Custom reducers/callables can accept either
(values)or(values, durations_seconds). - For flux-to-amount conversions, use
Integrate(PlantMeteo.DurationSumReducer())(equivalent tosum(values .* durations_seconds)), instead of hardcoding a fixed factor.
TimeStepModel(...) accepts either step counts (Real), ClockSpec, or fixed Dates periods (for example Dates.Hour(1), Dates.Day(1)). Fixed periods are converted internally using the meteo base timestep duration.
Timestep decision flow
When meteo is provided, duration is mandatory for each row (or the simulation errors).
Runtime picks each model effective clock with this order:
- If
ModelSpechasTimeStepModel(...), use it. - Else if
timespec(model)is non-default, use it. - Else use meteo base timestep (
duration) for that model.
Then runtime applies constraints:
- If the model clock is meteo-derived (rule 3),
timestep_hint.requiredis validated:- fixed required period: meteo timestep must match exactly;
- required range: meteo timestep must be inside the range.
timestep_hint.preferrednever overrides the clock when timestep is unset.- Meteo aggregation/integration is applied only when effective model timestep is coarser than meteo timestep.
Practical consequences:
- Unset
TimeStepModel+ required includes meteo + preferred is coarser: model still runs at meteo timestep. - Explicit coarser
TimeStepModel(Dates.Hour(2))with hourly meteo: model runs every 2 hours and receives aggregated meteo over that window. - Unset
TimeStepModel+ required excludes meteo: runtime errors with an actionable compatibility message.
Developer note on period conversion:
- Runtime time is indexed on a 1-based timeline (
t = 1, 2, 3, ...). TimeStepModel(Dates.Day(1))is converted to a clock step count using:dt = day_seconds / meteo_step_seconds.- For hourly meteo (
duration = Dates.Hour(1)), this givesdt = 24and the default phase is1, so the model runs att = 1, 25, 49, .... - This is equivalent to
ClockSpec(24.0, 1.0). - If you need runs at
t = 24, 48, 72, ..., set an explicit phase withClockSpec(24.0, 0.0).
Typical pipeline form:
ModelSpec(MyModel()) |>
TimeStepModel(ClockSpec(24.0, 1.0)) |>
MeteoWindow(CalendarWindow(:day; anchor=:current_period, week_start=1, completeness=:strict)) |>
MeteoBindings(; T=MeanWeighted()) |>
InputBindings(; x=(process=:producer, var=:y, policy=HoldLast())) |>
OutputRouting(; z=:stream_only)Calendar-aligned meteo windows
MeteoWindow(...) controls how rows are selected before reducers are applied:
RollingWindow()(default): trailing window based ondt(for example "last 24 steps").CalendarWindow(period; anchor, week_start, completeness):
: period in :day, :week, :month : anchor in :current_period, :previous_complete_period : week_start in 1:7 (1 = Monday) : completeness in :allow_partial, :strict
CalendarWindow(:day; anchor=:current_period, ...) guarantees that a model running inside a day sees aggregates over that civil day (including later timesteps from that day when available).
Hold-last coupling (default policy)
mapping = ModelMapping(
"Leaf" => (
ModelSpec(LeafSourceModel()) |> TimeStepModel(1.0),
ModelSpec(LeafConsumerModel()) |>
TimeStepModel(ClockSpec(2.0, 1.0)) |>
InputBindings(; C=(process=:leafsource, var=:S)),
),
)Daily integration from hourly stream
mapping = ModelMapping(
"Leaf" => (
ModelSpec(HourlyAssimModel()) |> TimeStepModel(1.0),
),
"Plant" => (
ModelSpec(DailyCarbonOfferModel()) |>
TimeStepModel(ClockSpec(24.0, 1.0)) |>
InputBindings(; A=(process=:hourlyassim, var=:A, scale="Leaf", policy=Integrate())),
),
)Interpolate slow producer to fast consumer
mapping = ModelMapping(
"Leaf" => (
ModelSpec(SlowSourceModel()) |> TimeStepModel(ClockSpec(2.0, 1.0)),
ModelSpec(FastConsumerModel()) |>
TimeStepModel(1.0) |>
InputBindings(; X=(process=:slowsource, var=:X, policy=Interpolate())),
),
)When the ModelMapping declares multirate configuration, the runtime resolves inputs from producer temporal streams according to these policies. Meteo rows are also sampled at each model clock. By default, meteo variables are aggregated from the finest weather step (for example T and Rh as weighted means, Tmin/Tmax, and radiation quantity aliases such as Ri_SW_q in MJ m-2). You can override these rules with MeteoBindings(...) on each ModelSpec.
Current limitations
- Multi-rate MTG runs currently execute sequentially. Passing
executor=ThreadedEx()orexecutor=DistributedEx()falls back to sequential execution with a warning. - Sub-step execution is currently unsupported: model timesteps shorter than the meteo base step (for example
TimeStepModel(Dates.Minute(30))with hourly meteo) raise an error.
Multi-rate output export (experimental)
You can export selected variables at a requested rate from temporal streams:
req = OutputRequest("Leaf", :carbon_assimilation;
name=:A_daily,
process=:toyassim,
policy=Integrate(),
clock=ClockSpec(24.0, 1.0)
)
run!(sim, meteo; tracked_outputs=[req], executor=SequentialEx())
exported = collect_outputs(sim; sink=DataFrame)tracked_outputs accepts OutputRequest values for these resampled exports. You can also return them directly from run!:
out_status, exported = run!(
sim,
meteo;
tracked_outputs=[req],
return_requested_outputs=true,
)