Public API

Index

API documentation

PlantMeteo.TimeStepTableMethod
TimeStepTable{Status}(df::DataFrame)

Method to build a TimeStepTable (from PlantMeteo.jl) from a DataFrame, but with each row being a Status.

Note

ModelList uses TimeStepTable{Status} by default (see examples below).

Examples

using PlantSimEngine, DataFrames

# A TimeStepTable from a DataFrame:
df = DataFrame(
    Tₗ=[25.0, 26.0],
    aPPFD=[1000.0, 1200.0],
    Cₛ=[400.0, 400.0],
    Dₗ=[1.0, 1.2],
)
TimeStepTable{Status}(df)

# A leaf with several values for at least one of its variable will automatically use 
# TimeStepTable{Status} with the time steps:
models = ModelList(
    process1=Process1Model(1.0),
    process2=Process2Model(),
    process3=Process3Model(),
    status=(var1=15.0, var2=0.3)
)

# The status of the leaf is a TimeStepTable:
status(models)

# Of course we can also create a TimeStepTable with Status manually:
TimeStepTable(
    [
        Status(Tₗ=25.0, aPPFD=1000.0, Cₛ=400.0, Dₗ=1.0),
        Status(Tₗ=26.0, aPPFD=1200.0, Cₛ=400.0, Dₗ=1.2),
    ]
)
source
PlantSimEngine.AggregateType
Aggregate()
Aggregate(reducer)

Windowed aggregation policy for consumers running at coarser clocks. Values in the consumer window are reduced with reducer.

Intended meaning: summarize window values as a statistic (for example mean/max). Default reducer is MeanReducer().

Important: Aggregate(r) and Integrate(r) are runtime-equivalent when they use the same reducer r; they only differ by default reducer and naming intent.

Built-in reducers can be shared with meteo sampling from PlantMeteo: SumReducer(), MeanReducer(), MaxReducer(), MinReducer(), FirstReducer(), LastReducer(). You can also provide a callable taking either:

  • values
  • values, durations_seconds
source
PlantSimEngine.ClockSpecType
ClockSpec(dt, phase)

Clock definition for a model/process.

Details

dt is the execution interval and phase is the offset of the execution grid. In the current runtime, simulation steps are indexed as t = 1, 2, 3, ... (1-based). A model runs when t is aligned with its clock.

Examples

With dt=24:

  • ClockSpec(24.0, 1.0) runs at t = 1, 25, 49, ...
  • ClockSpec(24.0, 0.0) runs at t = 24, 48, 72, ...
source
PlantSimEngine.IntegrateType
Integrate()
Integrate(reducer)

Windowed policy for consumers running at coarser clocks. Values in the consumer window are reduced with reducer.

Intended meaning: integrate/accumulate quantities over a window (for example hourly flux to daily total). Default reducer is SumReducer().

Important: Integrate(r) and Aggregate(r) are runtime-equivalent when they use the same reducer r; they only differ by default reducer and naming intent.

Built-in reducers can be shared with meteo sampling from PlantMeteo: SumReducer(), MeanReducer(), MaxReducer(), MinReducer(), FirstReducer(), LastReducer(). You can also provide a callable taking either:

  • values
  • values, durations_seconds
source
PlantSimEngine.InterpolateType
Interpolate()
Interpolate(mode)
Interpolate(mode, extrapolation)
Interpolate(; mode=:linear, extrapolation=:linear)

Interpolation policy for fast consumers reading slower producer streams.

Supported modes:

  • :linear: linear interpolation between bracket points for real values
  • :hold: left-hold (previous sample)

Supported extrapolation modes when no future sample exists:

  • :linear: linear extrapolation from last two samples when possible
  • :hold: keep the latest sample
source
PlantSimEngine.ModelListType
ModelList(models::M, status::S)
ModelList(;
    status=nothing,
    type_promotion=nothing,
    variables_check=true,
    kwargs...
)

List the models for a simulation (models), and does all boilerplate for variable initialization, type promotion, time steps handling.

Note

The status field depends on the input models. You can get the variables needed by a model using variables on the instantiation of a model. You can also use inputs and outputs instead.

Arguments

  • models: a list of models. Usually given as a NamedTuple, but can be any other structure that

implements getproperty.

  • status: a structure containing the initializations for the variables of the models. Usually a NamedTuple

when given as a kwarg, or any structure that implements the Tables interface from Tables.jl (e.g. DataFrame, see details).

  • type_promotion: optional type conversion for the variables with default values.

nothing by default, i.e. no conversion. Note that conversion is not applied to the variables input by the user as kwargs (need to do it manually). Should be provided as a Dict with current type as keys and new type as values.

  • variables_check=true: check that all needed variables are initialized by the user.
  • kwargs: the models, named after the process they simulate.

Details

If you need to input a custom Type for the status and make your users able to only partially initialize the status field in the input, you'll have to implement a method for add_model_vars!, a function that adds the models variables to the type in case it is not fully initialized. The default method is compatible with any type that implements the Tables.jl interface (e.g. DataFrame), and NamedTuples.

Note that ModelListmakes a copy of the input status if it does not list all needed variables.

Examples

We'll use the dummy models from the dummy.jl in the examples folder of the package. It implements three dummy processes: Process1Model, Process2Model and Process3Model, with one model implementation each: Process1Model, Process2Model and Process3Model.

julia> using PlantSimEngine;

Including example processes and models:

julia> using PlantSimEngine.Examples;
julia> models = ModelList(process1=Process1Model(1.0), process2=Process2Model(), process3=Process3Model());
[ Info: Some variables must be initialized before simulation: (process1 = (:var1, :var2), process2 = (:var1,)) (see `to_initialize()`)
julia> typeof(models)
ModelList{@NamedTuple{process1::Process1Model, process2::Process2Model, process3::Process3Model}, Status{(:var5, :var4, :var6, :var1, :var3, :var2), NTuple{6, Base.RefValue{Float64}}}}

No variables were given as keyword arguments, that means that the status of the ModelList is not set yet, and all variables are initialized to their default values given in the inputs and outputs (usually typemin(Type), i.e. -Inf for floating point numbers). This component cannot be simulated yet.

To know which variables we need to initialize for a simulation, we use to_initialize:

julia> to_initialize(models)
(process1 = (:var1, :var2), process2 = (:var1,))

We can now provide values for these variables in the status field, and simulate the ModelList, e.g. for process3 (coupled with process1 and process2):

julia> models = ModelList(process1=Process1Model(1.0), process2=Process2Model(), process3=Process3Model(), status=(var1=15.0, var2=0.3));
julia> meteo = Atmosphere(T = 22.0, Wind = 0.8333, P = 101.325, Rh = 0.4490995);
julia> outputs_sim = run!(models,meteo);
julia> outputs_sim[:var6]
1-element Vector{Float64}:
 58.0138985

If we want to use special types for the variables, we can use the type_promotion argument:

julia> models = ModelList(process1=Process1Model(1.0), process2=Process2Model(), process3=Process3Model(), status=(var1=15.0, var2=0.3), type_promotion = ModelMapping(Float64 => Float32));

We used type_promotion to force the status into Float32:

julia> [typeof(models[i][1]) for i in keys(status(models))]
6-element Vector{DataType}:
 Float32
 Float32
 Float32
 Float64
 Float64
 Float32

But we see that only the default variables (the ones that are not given in the status arguments) were converted to Float32, the two other variables that we gave were not converted. This is because we want to give the ability to users to give any type for the variables they provide in the status. If we want all variables to be converted to Float32, we can pass them as Float32:

julia> models = ModelList(process1=Process1Model(1.0), process2=Process2Model(), process3=Process3Model(), status=(var1=15.0f0, var2=0.3f0), type_promotion = Dict(Float64 => Float32));

We used type_promotion to force the status into Float32:

julia> [typeof(models[i][1]) for i in keys(status(models))]
6-element Vector{DataType}:
 Float32
 Float32
 Float32
 Float32
 Float32
 Float32
source
PlantSimEngine.ModelMappingType
ModelMapping(mapping; check=true)

Validated mapping between MTG scales and model definitions.

Each scale entry may be provided as:

  • a single model,
  • a tuple of models with an optional Status,

At construction time, the mapping is normalized and checked to fail early on common configuration errors:

  • each scale must define at least one model,
  • at most one Status is allowed per scale,
  • mapped scales must exist in the mapping,
  • mapped source variables must exist on the source scale (as a model output or status variable),
  • duplicate process declarations at a given scale are rejected.

Notes

The type behaves like a read-only dictionary keyed by scale name (Symbol). Use Dict(mapping) to recover a plain dictionary.

source
PlantSimEngine.ModelMappingMethod
ModelMapping(scale_mapping_pairs...; check=true)
ModelMapping(models...; scale=:Default, status=nothing, check=true, processes...)

Convenience constructors for ModelMapping:

  • pass scale => models pairs directly (dict-like syntax),
  • or pass models/processes directly for a single scale (old ModelList syntax).
source
PlantSimEngine.ModelSpecType
ModelSpec(model; multiscale=nothing, timestep=nothing, input_bindings=NamedTuple(), meteo_bindings=NamedTuple(), meteo_window=nothing, output_routing=NamedTuple(), scope=:global)

User-side model configuration wrapper for mapping/model list composition.

ModelSpec keeps model implementation and scenario-specific usage metadata in one place. This allows modelers to publish reusable models while users decide how models are coupled in their simulation setup.

source
PlantSimEngine.MultiScaleModelType
MultiScaleModel(model, mapped_variables)

A structure to make a model multi-scale. It defines a mapping between the variables of a model and the nodes symbols from which the values are taken from.

Arguments

  • model<:AbstractModel: the model to make multi-scale
  • mapped_variables<:Vector{Pair{Symbol,Union{Symbol,AbstractString,Vector{<:Union{Symbol,AbstractString}}}}}: a vector of pairs of model variables and source scale declarations.

The mapped_variables argument can be of the form:

  1. [:variable_name => (:Plant => :variable_name)]: We take one value from the Plant node
  2. [:variable_name => [:Leaf]]: We take a vector of values from the Leaf nodes
  3. [:variable_name => [:Leaf, :Internode]]: We take a vector of values from the Leaf and Internode nodes
  4. [:variable_name => (:Plant => :variable_name_in_plant_scale)]: We take one value from another variable name in the Plant node
  5. [:variable_name => [:Leaf => :variable_name_1, :Internode => :variable_name_2]]: We take a vector of values from the Leaf and Internode nodes with different names
  6. [PreviousTimeStep(:variable_name) => ...]: We flag the variable to be initialized with the value from the previous time step, and we do not use it to build the dep graph
  7. [:variable_name => (Symbol() => :variable_name_from_another_model)]: We take the value from another model at the same scale but rename it
  8. [PreviousTimeStep(:variable_name),]: We just flag the variable as a PreviousTimeStep to not use it to build the dep graph

Details about the different forms:

  1. The variable variable_name of the model will be taken from the Plant node, assuming only one node has the Plant symbol.

In this case the value available from the status will be a scalar, and so the user must guaranty that only one node of type Plant is available in the MTG.

  1. The variable variable_name of the model will be taken from the Leaf nodes. Notice it is given as a vector, indicating that the values will be taken

from all the nodes of type Leaf. The model should be able to handle a vector of values. Note that even if there is only one node of type Leaf, the value will be taken as a vector of one element.

  1. The variable variable_name of the model will be taken from the Leaf and Internode nodes. The values will be taken from all the nodes of type Leaf

and Internode.

  1. The variable variable_name of the model will be taken from the variable called variable_name_in_plant_scale in the Plant node. This is useful

when the variable name in the model is different from the variable name in the scale it is taken from.

  1. The variable variable_name of the model will be taken from the variable called variable_name_1 in the Leaf node and variable_name_2 in the Internode node.

  2. The variable variable_name of the model uses the value computed on the previous time-step. This implies that the variable is not used to build the dependency graph

because the dependency graph only applies on the current time-step. This is used to avoid circular dependencies when a variable depends on itself. The value can be initialized in the Status if needed.

  1. The variable variable_name of the model will be taken from another model at the same scale, but with another variable name.

  2. The variable variable_name of the model is just flagged as a PreviousTimeStep variable, so it is not used to build the dependency graph.

Note that the mapping does not make any copy of the values, it only references them. This means that if the values are updated in the status of one node, they will be updated in the other nodes.

Examples

julia> using PlantSimEngine;

Including example processes and models:

julia> using PlantSimEngine.Examples;

Let's take a model:

julia> model = ToyCAllocationModel()
ToyCAllocationModel()

We can make it multi-scale by defining a mapping between the variables of the model and the nodes symbols from which the values are taken from:

For example, if the carbon_allocation comes from the Leaf and Internode nodes, we can define the mapping as follows:

julia> mapped_variables = [:carbon_allocation => [:Leaf, :Internode]]
1-element Vector{Pair{Symbol, Vector{Symbol}}}:
 :carbon_allocation => [:Leaf, :Internode]

The mappedvariables argument is a vector of pairs of symbols and symbol scales. In this case, we have only one pair to define the mapping between the `carbonallocationvariable and theLeafandInternode` nodes.

We can now make the model multi-scale by passing the model and the mapped variables to the MultiScaleModel constructor :

julia> multiscale_model = PlantSimEngine.MultiScaleModel(model, mapped_variables);

We can access the mapped variables and the model:

julia> PlantSimEngine.mapped_variables_(multiscale_model) == [:carbon_allocation => [:Leaf => :carbon_allocation, :Internode => :carbon_allocation]]
true
julia> PlantSimEngine.model_(multiscale_model)
ToyCAllocationModel()
source
PlantSimEngine.OutputRequestType
OutputRequest(scale, var; name=var, process=nothing, policy=HoldLast(), clock=nothing)

Describe one online-exported multi-rate output series for MTG multi-rate runs.

Use this type in run!(...; tracked_outputs=...) to export resampled temporal streams while simulation is running.

Arguments

  • scale::Symbol: producer scale (for example :Leaf or :Plant).
  • var::Symbol: source variable name published on scale.

Keyword arguments

  • name::Symbol=var: name of the exported series in collect_outputs(sim) or returned output dictionaries. Names must be unique across requests.
  • process=nothing: producer process name (Symbol/String) or nothing. When nothing, runtime tries to use the unique canonical publisher for (scale, var) and errors on ambiguity.
  • policy::SchedulePolicy=HoldLast(): resampling policy applied at export time. Common values are HoldLast(), Integrate(...), Aggregate(...), Interpolate(...). Integrate and Aggregate are runtime-equivalent with the same reducer; they differ by default reducer (SumReducer vs MeanReducer) and intent.
  • clock=nothing: export clock. When nothing, export is evaluated at each simulation step (ClockSpec(1.0, 0.0)). Accepted explicit values are the same as model timestep specs (Real, ClockSpec, or fixed Dates.Period).

Example

req_daily = OutputRequest(
    :Leaf,
    :A;
    name=:A_daily,
    process=:toyassim,
    policy=Integrate(),
    clock=ClockSpec(24.0, 0.0),
)
source
PlantSimEngine.PreviousTimeStepType
PreviousTimeStep(variable)

A structure to manually flag a variable in a model to use the value computed on the previous time-step. This implies that the variable is not used to build the dependency graph because the dependency graph only applies on the current time-step. This is used to avoid circular dependencies when a variable depends on itself. The value can be initialized in the Status if needed.

The process is added when building the MultiScaleModel, to avoid conflicts between processes with the same variable name. For exemple one process can define a variable :carbon_biomass as a PreviousTimeStep, but the othe process would use the variable as a dependency for the current time-step (and it would be fine because theyr don't share the same issue of cyclic dependency).

source
PlantSimEngine.StatusType
Status(vars)

Status type used to store the values of the variables during simulation. It is mainly used as the structure to store the variables in the TimeStepRow of a TimeStepTable (see PlantMeteo.jl docs) of a ModelList.

Most of the code is taken from MasonProtter/MutableNamedTuples.jl, so Status is a MutableNamedTuples with a few modifications, so in essence, it is a stuct that stores a NamedTuple of the references to the values of the variables, which makes it mutable.

Examples

A leaf with one value for all variables will make a status with one time step:

julia> using PlantSimEngine
julia> st = PlantSimEngine.Status(Ra_SW_f=13.747, sky_fraction=1.0, d=0.03, aPPFD=1500.0);

All these indexing methods are valid:

julia> st[:Ra_SW_f]
13.747
julia> st.Ra_SW_f
13.747
julia> st[1]
13.747

Setting a Status variable is very easy:

julia> st[:Ra_SW_f] = 20.0
20.0
julia> st.Ra_SW_f = 21.0
21.0
julia> st[1] = 22.0
22.0
source
PlantSimEngine.TemporalStateType
TemporalState(caches, last_run, streams, producer_horizons, export_plans, export_rows)
TemporalState()

Temporal storage for multi-rate simulations. caches stores producer hold-last outputs. last_run stores last execution time per model key. streams stores bounded producer (time, value) samples used for windowed and interpolated policies. producer_horizons stores required retention horizon per producer (scale, process, var). export_plans stores resolved online export requests prepared before the run. export_rows stores online-exported rows keyed by request name.

source
PlantSimEngine.EFMethod
EF(obs,sim)

Returns the Efficiency Factor between observations obs and simulations sim using NSE (Nash-Sutcliffe efficiency) model. More information can be found at https://en.wikipedia.org/wiki/Nash%E2%80%93Sutcliffemodelefficiency_coefficient.

The closer to 1 the better.

Examples

using PlantSimEngine

obs = [1.0, 2.0, 3.0]
sim = [1.1, 2.1, 3.1]

EF(obs, sim)
source
PlantSimEngine.InputBindingsMethod
InputBindings(bindings)
InputBindings(; kwargs...)

Pipe-style transform that sets explicit producer bindings for model inputs.

This is used in multi-rate mappings to tell runtime where each input should be read from (process, optional source variable, optional source scale, and policy).

Arguments

  • bindings::NamedTuple: maps each consumer input variable (Symbol) to a binding descriptor.
  • kwargs...: keyword shorthand equivalent to a NamedTuple.

Each binding descriptor can be:

  • Symbol: producer process (policy=HoldLast() and source variable inferred).
  • Pair{Symbol,Symbol}: producer_process => source_var (policy=HoldLast()).
  • NamedTuple: explicit fields:
    • process (Symbol/String, optional if uniquely inferable),
    • var (Symbol, optional, defaults to same-name input when inferable),
    • scale (String/Symbol, optional, useful for cross-scale disambiguation),
    • policy (SchedulePolicy instance/type, optional, default HoldLast()).

When omitted fields cannot be inferred uniquely, runtime errors and asks for an explicit InputBindings(...).

Example

ModelSpec(ConsumerModel()) |>
TimeStepModel(ClockSpec(24.0, 0.0)) |>
InputBindings(; A=(process=:assim, var=:carbon_assimilation, scale="Leaf", policy=Integrate()))
source
PlantSimEngine.MeteoBindingsMethod
MeteoBindings(bindings)
MeteoBindings(; kwargs...)

Pipe-style transform that sets weather-variable aggregation rules per model.

Each key is the target weather variable name as seen by the model (for example :T, :Rh, :Ri_SW_q).

Arguments

  • bindings::NamedTuple: per-target meteo binding rules.
  • kwargs...: keyword shorthand equivalent to a NamedTuple.

Each rule value can be:

  • a PlantMeteo.AbstractTimeReducer instance/type (for example MeanWeighted(), MaxReducer, RadiationEnergy()),
  • a callable reducer (Function) receiving sampled values,
  • a NamedTuple with:
    • source (Symbol/String, optional, defaults to target key),
    • reducer (reducer type/instance/callable, optional, defaults to MeanWeighted()).

Example

ModelSpec(DailyModel()) |>
TimeStepModel(ClockSpec(24.0, 0.0)) |>
MeteoBindings(
    ;
    T=MeanWeighted(),
    Rh=MeanWeighted(),
    Ri_SW_q=(source=:Ri_SW_f, reducer=RadiationEnergy()),
)
source
PlantSimEngine.MeteoWindowMethod
MeteoWindow(window)

Pipe-style transform that sets the weather row-selection window for one model.

This controls which meteo rows are sampled before MeteoBindings reducers are applied.

Arguments

  • window: a PlantMeteo.AbstractSamplingWindow instance/type. Typical values are:
    • PlantMeteo.RollingWindow() (default trailing window),
    • PlantMeteo.CalendarWindow(...) (calendar-aligned day/week/month windows).

Example

ModelSpec(DailyModel()) |>
TimeStepModel(ClockSpec(24.0, 0.0)) |>
MeteoWindow(CalendarWindow(:day; anchor=:current_period, week_start=1, completeness=:strict))
source
PlantSimEngine.NRMSEMethod
NRMSE(obs,sim)

Returns the Normalized Root Mean Squared Error between observations obs and simulations sim. Normalization is performed using division by observations range (max-min).

Examples

using PlantSimEngine

obs = [1.0, 2.0, 3.0]
sim = [1.1, 2.1, 3.1]

NRMSE(obs, sim)
source
PlantSimEngine.OutputRoutingMethod
OutputRouting(routing)
OutputRouting(; kwargs...)

Pipe-style transform that sets output publication mode for a model.

This is mainly used to disambiguate publishers in multi-rate runs when several models write variables with the same name.

Arguments

  • routing::NamedTuple: maps output variable symbols to routing mode.
  • kwargs...: keyword shorthand equivalent to a NamedTuple.

Allowed routing values:

  • :canonical (default): output is considered canonical at that scale and can be auto-selected as source/export publisher.
  • :stream_only: output is kept only in temporal streams and excluded from canonical publisher resolution.

Example

ModelSpec(AltSourceModel()) |>
OutputRouting(; C=:stream_only)
source
PlantSimEngine.RMSEMethod
RMSE(obs,sim)

Returns the Root Mean Squared Error between observations obs and simulations sim.

The closer to 0 the better.

Examples

using PlantSimEngine

obs = [1.0, 2.0, 3.0]
sim = [1.1, 2.1, 3.1]

RMSE(obs, sim)
source
PlantSimEngine.ScopeModelMethod
ScopeModel(scope)

Pipe-style transform that sets stream scope selection for a model.

Scope controls how temporal streams are partitioned/resolved across entities in multi-rate simulations.

Arguments

  • scope: one of:
    • selector symbols/strings: :global, :plant, :scene, :self,
    • a concrete ScopeId,
    • a callable returning a scope selector/id at runtime.

Example

ModelSpec(LeafSourceModel()) |>
ScopeModel(:plant)
source
PlantSimEngine.add_organ!Method
add_organ!(node::MultiScaleTreeGraph.Node, sim_object, link, symbol, scale; index=0, id=MultiScaleTreeGraph.new_id(MultiScaleTreeGraph.get_root(node)), attributes=Dict{Symbol,Any}(), check=true)

Add an organ to the graph, automatically taking care of initialising the status of the organ (multiscale-)variables.

This function should be called from a model that implements organ emergence, for example in function of thermal time.

Arguments

  • node: the node to which the organ is added (the parent organ of the new organ)
  • sim_object: the simulation object, e.g. the GraphSimulation object from the extra argument of a model.
  • link: the link type between the new node and the organ:
    • "<": the new node is following the parent organ
    • "+": the new node is branching the parent organ
    • "/": the new node is decomposing the parent organ, i.e. we change scale
  • symbol: the symbol of the organ, e.g. "Leaf"
  • scale: the scale of the organ, e.g. 2.
  • index: the index of the organ, e.g. 1. The index may be used to easily identify branching order, or growth unit index on the axis. It is different from the node id that is unique.
  • id: the unique id of the new node. If not provided, a new id is generated.
  • attributes: the attributes of the new node. If not provided, an empty dictionary is used.
  • check: a boolean indicating if variables initialisation should be checked. Passed to init_node_status!.

Returns

  • status: the status of the new node

Examples

See the ToyInternodeEmergence example model from the Examples module (also found in the examples folder), or the test-mtg-dynamic.jl test file for an example usage.

source
PlantSimEngine.depFunction
dep(mapping::ModelMapping; verbose=true)
dep(mapping::AbstractDict{Symbol,T}; verbose=true)
dep!(m::ModelMapping, nsteps=1)

Get the model dependency graph given a ModelMapping or a multiscale model mapping. If one graph is returned, then all models are coupled. If several graphs are returned, then only the models inside each graph are coupled, and the models in different graphs are not coupled. nsteps is the number of steps the dependency graph will be used over. It is used to determine the length of the simulation_id argument for each soft dependencies in the graph. It is set to 1 in the case of a multiscale mapping.

Details

The dependency graph is computed by searching the inputs of each process in the outputs of its own scale, or the other scales. There are five cases for every model (one model simulates one process):

  1. The process has no inputs. It is completely independent, and is placed as one of the roots of the dependency graph.
  2. The process needs inputs from models at its own scale. We put it as a child of this other process.
  3. The process needs inputs from another scale. We put it as a child of this process at another scale.
  4. The process needs inputs from its own scale and another scale. We put it as a child of both.
  5. The process is a hard dependency of another process (only possible at the same scale). In this case, the process is set as a hard-dependency of the

other process, and its simulation is handled directly from this process.

For the 4th case, the process have two parent processes. This is OK because the process will only be computed once during simulation as we check if both parents were run before running the process.

Note that in the 5th case, we still need to check if a variable is needed from another scale. In this case, the parent node is used as a child of the process at the other scale. Note there can be several levels of hard dependency graph, so this is done recursively.

How do we do all that? We identify the hard dependencies first. Then we link the inputs/outputs of the hard dependencies roots to other scales if needed. Then we transform all these nodes into soft dependencies, that we put into a Dict of Scale => ModelMapping(process => SoftDependencyNode). Then we traverse all these and we set nodes that need outputs from other nodes as inputs as children/parents. If a node has no dependency, it is set as a root node and pushed into a new Dict (independantprocessroot). This Dict is the returned dependency graph. And it presents root nodes as independent starting points for the sub-graphs, which are the models that are coupled together. We can then traverse each of these graphs independently to r

Notes

The difference between dep(m::ModelMapping) and dep!(m::ModelMapping, nsteps) is that the first one returns the dependency graph found in the model list, while the second one returns the dependency graph with the specified number of steps, modifying the simulation IDs of each node in the graph (simulation_id=fill(0, nsteps)).

Examples

using PlantSimEngine

# Including example processes and models:
using PlantSimEngine.Examples;

models = ModelMapping(
    process1=Process1Model(1.0),
    process2=Process2Model(),
    process3=Process3Model(),
    status=(var1=15.0, var2=0.3)
)

dep(models)

# or directly with the processes:
models = (
    process1=Process1Model(1.0),
    process2=Process2Model(),
    process3=Process3Model(),
    process4=Process4Model(),
    process5=Process5Model(),
    process6=Process6Model(),
    process7=Process7Model(),
)

dep(;models...)
source
PlantSimEngine.drMethod
dr(obs,sim)

Returns the Willmott’s refined index of agreement dᵣ. Willmot et al. 2011. A refined index of model performance. https://rmets.onlinelibrary.wiley.com/doi/10.1002/joc.2419

The closer to 1 the better.

Examples

using PlantSimEngine

obs = [1.0, 2.0, 3.0]
sim = [1.1, 2.1, 3.1]

dr(obs, sim)
source
PlantSimEngine.effective_rate_summaryMethod
effective_rate_summary(mapping::ModelMapping{MultiScale}, meteo)

Summarize the effective model execution rates implied by mapping and meteo. Returns an EffectiveRateSummary struct with details on the effective rates and their sources.

To get the value of the base step used for the simulation, use summary.base_step_seconds. To get the effective rates and their sources, use summary.rates, which is a dictionary mapping each effective rate (in seconds) to a vector of named tuples with keys :scale, :process, :source, and :model describing the models running at that rate and their source of timing.

source
PlantSimEngine.explain_model_specsMethod
explain_model_specs(target; io=stdout, infer=true, validate=true)

Print a compact per-model summary of resolved runtime configuration and return it as a vector of named tuples.

Summary fields:

  • scale
  • process
  • model
  • timestep
  • input_bindings
  • meteo_bindings
  • meteo_window
source
PlantSimEngine.fitFunction
fit()

Optimize the parameters of a model using measurements and (potentially) initialisation values.

Modellers should implement a method to fit for their model, with the following design pattern:

The call to the function should take the model type as the first argument (T::Type{<:AbstractModel}), the data as the second argument (as a Table.jl compatible type, such as DataFrame), and the parameters initializations as keyword arguments (with default values when necessary).

For example the method for fitting the Beer model from the example script (see src/examples/Beer.jl) looks like this:

function PlantSimEngine.fit(::Type{Beer}, df; J_to_umol=PlantMeteo.Constants().J_to_umol)
    k = Statistics.mean(log.(df.Ri_PAR_f ./ (df.aPPFD ./ J_to_umol)) ./ df.LAI)
    return (k=k,)
end

The function should return the optimized parameters as a NamedTuple of the form (parameter_name=parameter_value,).

Here is an example usage with the Beer model, where we fit the k parameter from "measurements" of aPPFD, LAI and Ri_PAR_f.

# Including example processes and models:
using PlantSimEngine.Examples;

m = ModelList(Beer(0.6), status=(LAI=2.0,))
meteo = Atmosphere(T=20.0, Wind=1.0, P=101.3, Rh=0.65, Ri_PAR_f=300.0)
run!(m, meteo)
df = DataFrame(aPPFD=m[:aPPFD][1], LAI=m.status.LAI[1], Ri_PAR_f=meteo.Ri_PAR_f[1])
fit(Beer, df)

Note that this is a dummy example to show that the fitting method works, as we simulate the aPPFD using the Beer-Lambert law with a value of k=0.6, and then use the simulated aPPFD to fit the k parameter again, which gives the same value as the one used on the simulation.

source
PlantSimEngine.init_status!Method
init_status!(object::Dict{Symbol,ModelMapping};vars...)
init_status!(component::ModelMapping;vars...)

Initialise model variables for components with user input.

Examples

using PlantSimEngine

# Load the dummy models given as example in the package:
using PlantSimEngine.Examples

models = Dict(
    :Leaf => ModelMapping(
        process1=Process1Model(1.0),
        process2=Process2Model(),
        process3=Process3Model()
    ),
    :InterNode => ModelMapping(
        process1=Process1Model(1.0),
    )
)

init_status!(models, var1=1.0 , var2=2.0)
status(models[:Leaf])
source
PlantSimEngine.init_variablesMethod
init_variables(models...)

Initialized model variables with their default values. The variables are taken from the inputs and outputs of the models.

Examples

using PlantSimEngine

# Load the dummy models given as example in the package:
using PlantSimEngine.Examples

init_variables(Process1Model(2.0))
init_variables(process1=Process1Model(2.0), process2=Process2Model())
source
PlantSimEngine.input_bindingsMethod
input_bindings(spec::ModelSpec)

Optional explicit input-to-producer bindings used by multi-rate resolution.

ModelSpec is the user-facing API for mapping-level coupling configuration. Bindings are read from ModelSpec during multiscale multi-rate simulation.

Expected return value is a NamedTuple keyed by consumer input variable names. Each value must itself be a NamedTuple with:

  • process::Symbol: producer process name (as generated by @process)
  • var::Symbol: producer output variable name
  • policy: scheduling policy instance (defaults to HoldLast() when omitted)

This lets a consumer input read from a producer variable even when names differ. For example, consumer input :C can be mapped from producer output :S.

Example

InputBindings(; C=(process=:myproducer, var=:S))
source
PlantSimEngine.inputsMethod
inputs(model::AbstractModel)
inputs(...)

Get the inputs of one or several models.

Returns an empty tuple by default for AbstractModels (no inputs) or Missing models.

Examples

using PlantSimEngine;

# Load the dummy models given as example in the package:
using PlantSimEngine.Examples;

inputs(Process1Model(1.0))

# output
(:var1, :var2)
source
PlantSimEngine.inputsMethod
inputs(mapping::ModelMapping)
inputs(mapping::AbstractDict{Symbol,T})

Get the inputs of the models in a mapping, for each process and organ type.

source
PlantSimEngine.is_initializedMethod
is_initialized(m::T) where T <: ModelMapping
is_initialized(m::T, models...) where T <: ModelMapping

Check if the variables that must be initialized are, and return true if so, and false and an information message if not.

Note

There is no way to know before-hand which process will be simulated by the user, so if you have a component with a model for each process, the variables to initialize are always the smallest subset of all, meaning it is considered the user will simulate the variables needed for other models.

Examples

using PlantSimEngine

# Load the dummy models given as example in the package:
using PlantSimEngine.Examples

models = ModelMapping(
    process1=Process1Model(1.0),
    process2=Process2Model(),
    process3=Process3Model()
)

is_initialized(models)
source
PlantSimEngine.meteo_bindingsMethod
meteo_bindings(spec::ModelSpec)

Optional explicit weather aggregation bindings used by multi-rate MTG runtime. Each key is the target meteo variable exposed to the model at execution time. Each value can be:

  • PlantMeteo reducer instance/type (e.g. MeanWeighted(), MaxReducer)
  • Function: custom reducer callable
  • NamedTuple: optional fields source and reducer
source
PlantSimEngine.meteo_hintMethod
meteo_hint(model::AbstractModel)
meteo_hint(::Type{<:AbstractModel})

Optional model trait used to infer weather sampling when ModelSpec does not provide MeteoBindings(...) and/or MeteoWindow(...).

Expected return value is a NamedTuple with optional fields:

  • bindings: compatible with MeteoBindings(...)
  • window: compatible with MeteoWindow(...)
source
PlantSimEngine.meteo_windowMethod
meteo_window(spec::ModelSpec)

Optional weather window-selection strategy used by multi-rate MTG runtime. Defaults to nothing (runtime falls back to PlantMeteo.RollingWindow() behavior).

source
PlantSimEngine.model_scopeMethod
model_scope(spec::ModelSpec)

Scope selector used by multi-rate runtime to partition producer streams. Default is :global.

source
PlantSimEngine.output_policyMethod
output_policy(model::AbstractModel)
output_policy(::Type{<:AbstractModel})

Per-output scheduling policy for a model. Default is empty, meaning all outputs fallback to hold-last behaviour.

When multi-rate input bindings are inferred automatically, this trait also provides the default cross-clock policy (HoldLast, Integrate, Aggregate, or Interpolate) for each producer output.

source
PlantSimEngine.output_routingMethod
output_routing(spec::ModelSpec)

Per-output routing mode for multi-rate runs. Allowed values are:

  • :canonical (default): output participates in canonical status publication.
  • :stream_only: output is only tracked in temporal streams.
source
PlantSimEngine.outputsMethod
outputs(model::AbstractModel)
outputs(...)

Get the outputs of one or several models.

Returns an empty tuple by default for AbstractModels (no outputs) or Missing models.

Examples

using PlantSimEngine;

# Load the dummy models given as example in the package:
using PlantSimEngine.Examples;

outputs(Process1Model(1.0))

# output
(:var3,)
source
PlantSimEngine.outputsMethod
outputs(mapping::ModelMapping)
outputs(mapping::AbstractDict{Symbol,T})

Get the outputs of the models in a mapping, for each process and organ type.

source
PlantSimEngine.resolved_model_specsMethod
resolved_model_specs(mapping; infer=true, validate=true)
resolved_model_specs(sim::GraphSimulation)

Return process-indexed ModelSpec dictionaries as used by runtime: Dict{Symbol, Dict{Symbol, ModelSpec}}.

For a mapping, this parses model declarations and optionally applies inference (timestep_hint, meteo_hint) and validation. For a GraphSimulation, this returns the already resolved model specs used by the simulation.

source
PlantSimEngine.run!Function
run!(object, meteo, constants, extra=nothing; check=true, executor=Floops.ThreadedEx())
run!(object, mapping, meteo, constants, extra; nsteps, outputs, check, executor)

Run the simulation for each model in the model list in the correct order, i.e. respecting the dependency graph.

If several time-steps are given, the models are run sequentially for each time-step.

Arguments

PlantMeteo.Atmosphere or a single PlantMeteo.Atmosphere. When meteo is provided, duration must be present and strictly positive.

  • constants: a PlantMeteo.Constants object, or a NamedTuple of constant keys and values.
  • extra: extra parameters, not available for simulation of plant graphs (the simulation object is passed using this).
  • check: if true, check the validity of the model list before running the simulation (takes a little bit of time), and return more information while running.
  • executor: the Floops executor used to run the simulation either in sequential (executor=SequentialEx()), in a

multi-threaded way (executor=ThreadedEx(), the default), or in a distributed way (executor=DistributedEx()).

  • mapping: a ModelMapping between MTG scales and models.
  • nsteps: the number of time-steps to run, only needed if no meteo is given (else it is infered from it).
  • outputs: the outputs to get in dynamic for each node type of the MTG.
  • return_requested_outputs: when true in MTG multi-rate runs, return requested resampled outputs directly as second return value.
  • requested_outputs_sink: sink used to materialize requested outputs when return_requested_outputs=true.

Returns

Returns status outputs (and optionally requested exports). For MTG multi-rate runs with return_requested_outputs=true, returns (status_outputs, requested_outputs).

Details

Model execution

The models are run according to the dependency graph. If a model has a soft dependency on another model (i.e. its inputs are computed by another model), the other model is run first. If a model has several soft dependencies, the parents (the soft dependencies) are always computed first.

Parallel execution

Users can ask for parallel execution by providing a compatible executor to the executor argument. The package will also automatically check if the execution can be parallelized. If it is not the case and the user asked for a parallel computation, it return a warning and run the simulation sequentially. We use the Floops package to run the simulation in parallel. That means that you can provide any compatible executor to the executor argument. You can take a look at FoldsThreads.jl for extra thread-based executors, FoldsDagger.jl for Transducers.jl-compatible parallel fold implemented using the Dagger.jl framework, and soon FoldsCUDA.jl for GPU computations (see this issue) and FoldsKernelAbstractions.jl. You can also take a look at ParallelMagics.jl to check if automatic parallelization is possible.

Example

Import the packages:

julia> using PlantSimEngine, PlantMeteo;

Load the dummy models given as example in the Examples sub-module:

julia> using PlantSimEngine.Examples;

Create a model mapping:

julia> mapping = ModelMapping(Process1Model(1.0), Process2Model(), Process3Model(); status = (var1=1.0, var2=2.0));

Create meteo data:

julia> meteo = Atmosphere(T=20.0, Wind=1.0, P=101.3, Rh=0.65, Ri_PAR_f=300.0);

Run the simulation:

julia> outputs_sim = run!(mapping, meteo);

Get the results:

julia> (outputs_sim[:var4],outputs_sim[:var6])
([12.0], [41.95])
source
PlantSimEngine.statusMethod
status(m)
status(m::AbstractArray{<:ModelList})
status(m::AbstractDict{T,<:ModelList})

Get a ModelList status, i.e. the state of the input (and output) variables.

See also is_initialized and to_initialize

Examples

using PlantSimEngine

# Including example models and processes:
using PlantSimEngine.Examples;

# Create a ModelList
models = ModelMapping(
    process1=Process1Model(1.0),
    process2=Process2Model(),
    process3=Process3Model(),
    status = (var1=[15.0, 16.0], var2=0.3)
);

status(models)

# Or just one variable:
status(models,:var1)


# Or the status at the ith time-step:
status(models, 2)

# Or even more simply:
models[:var1]
# output
2-element Vector{Float64}:
 15.0
 16.0
source
PlantSimEngine.timespecMethod
timespec(model::AbstractModel)
timespec(::Type{<:AbstractModel})

Clock definition for a model. Default is single-rate behaviour (dt=1.0, phase=0.0).

source
PlantSimEngine.timestep_hintMethod
timestep_hint(model::AbstractModel)
timestep_hint(::Type{<:AbstractModel})

Optional model trait used to declare runtime compatibility constraints when ModelSpec.timestep is not provided.

Supported return values:

  • nothing (default): no hint
  • Dates.FixedPeriod: fixed required timestep
  • (min_period, max_period): required timestep range (Dates.FixedPeriod pair)
  • NamedTuple: with required (one of the forms above) and optional preferred (:finest, :coarsest, or a Dates.FixedPeriod within the required range). preferred is informational only when runtime derives timestep from meteo.
source
PlantSimEngine.to_initializeMethod
to_initialize(; verbose=true, vars...)
to_initialize(m::T)  where T <: ModelMapping
to_initialize(m::DependencyGraph)
to_initialize(mapping::ModelMapping, graph=nothing)
to_initialize(mapping::AbstractDict{Symbol,T}, graph=nothing)

Return the variables that must be initialized providing a set of models and processes. The function takes into account model coupling and only returns the variables that are needed considering that some variables that are outputs of some models are used as inputs of others.

Arguments

  • verbose: if true, print information messages.
  • vars...: the models and processes to consider.
  • m::T: a ModelMapping.
  • m::DependencyGraph: a DependencyGraph.
  • mapping::ModelMapping (or dictionary-like mapping): associates models to organs/scales.
  • graph: a graph representing a plant or a scene, e.g. a multiscale tree graph. The graph is used to check if variables that are not initialized can be found in the graph nodes attributes.

Examples

using PlantSimEngine

# Load the dummy models given as example in the package:
using PlantSimEngine.Examples

to_initialize(process1=Process1Model(1.0), process2=Process2Model())

# Or using a component directly:
models = ModelMapping(process1=Process1Model(1.0), process2=Process2Model())
to_initialize(models)

m = ModelMapping(
    (
        process1=Process1Model(1.0),
        process2=Process2Model()
    ),
    Status(var1 = 5.0, var2 = -Inf, var3 = -Inf, var4 = -Inf, var5 = -Inf)
)

to_initialize(m)

Or with a mapping:

using PlantSimEngine

# Load the dummy models given as example in the package:
using PlantSimEngine.Examples

mapping = ModelMapping(
    "Leaf" => ModelMapping(
        process1=Process1Model(1.0),
        process2=Process2Model(),
        process3=Process3Model()
    ),
    "Internode" => ModelMapping(
        process1=Process1Model(1.0),
    )
)

to_initialize(mapping)
source
PlantSimEngine.variablesMethod
variables(pkg::Module)

Returns a dataframe of all variables, their description and units in a package that has PlantSimEngine as a dependency (if implemented by the authors).

Note to developers

Developers of a package that depends on PlantSimEngine should put a csv file in "data/variables.csv", then this file will be returned by the function.

Examples

Here is an example with the PlantBiophysics package:

#] add PlantBiophysics
using PlantBiophysics
variables(PlantBiophysics)
source
PlantSimEngine.variablesMethod
variables(mapping::ModelMapping)
variables(mapping::AbstractDict{Symbol,T})

Get the variables (inputs and outputs) of the models in a mapping, for each process and organ type.

source
PlantSimEngine.variablesMethod
variables(model)
variables(model, models...)

Returns a tuple with the name of the variables needed by a model, or a union of those variables for several models.

Note

Each model can (and should) have a method for this function.


using PlantSimEngine;

# Load the dummy models given as example in the package:
using PlantSimEngine.Examples;

variables(Process1Model(1.0))

variables(Process1Model(1.0), Process2Model())

# output

(var1 = -Inf, var2 = -Inf, var3 = -Inf, var4 = -Inf, var5 = -Inf)

See also

inputs, outputs and variables_typed

source
PlantSimEngine.@processMacro
@process(process::String, doc::String=""; verbose::Bool=true)

This macro generate the abstract type and some boilerplate code for the simulation of a process, along with its documentation. It also prints out a short tutorial for implementing a model if verbose=true.

The abstract process type is then used as a supertype of all models implementations for the process, and is named "Abstract<ProcessName>Model", e.g. AbstractGrowthModel for a process called growth.

The first argument to @process is the new process name, the second is any additional documentation that should be added to the Abstract<ProcessName>Model type, and the third determines whether the short tutorial should be printed or not.

Newcomers are encouraged to use this macro because it explains in detail what to do next with the process. But more experienced users may want to directly define their process without printing the tutorial. To do so, you can just define a new abstract type and define it as a subtype of AbstractModel:

abstract type MyNewProcess <: AbstractModel end

Examples

@process "dummy_process" "This is a dummy process that shall not be used"
source

Multi-rate policy examples

For mapping-level multi-rate configuration, combine:

  • ModelSpec(...)
  • TimeStepModel(...)
  • InputBindings(...)
  • MeteoBindings(...)
  • MeteoWindow(...)
  • OutputRouting(...)
  • ScopeModel(...)
  • timespec(::Type{<:AbstractModel}) (optional trait)
  • output_policy(::Type{<:AbstractModel}) (optional trait)
  • timestep_hint(::Type{<:AbstractModel}) (optional trait)
  • meteo_hint(::Type{<:AbstractModel}) (optional trait)
  • resolved_model_specs(mapping) (utility)
  • explain_model_specs(mapping_or_sim) (utility)
  • OutputRequest(...) in tracked_outputs for resampled exports

TimeStepModel(...) accepts:

  • Real step counts
  • ClockSpec
  • fixed Dates periods (Dates.Second, Dates.Minute, Dates.Hour, Dates.Day, ...)

Period conversion detail:

  • Period-based timesteps are converted using the meteo base step duration.
  • Example: TimeStepModel(Dates.Day(1)) with hourly meteo (Dates.Hour(1)) maps to ClockSpec(24.0, 1.0), so execution times are t = 1, 25, 49, ....

Trait-based inference detail:

  • If TimeStepModel(...) is omitted, runtime resolves timestep from:

: timespec(model) when non-default, otherwise meteo duration.

  • timestep_hint(::Type{<:Model}) is then interpreted as:

: required = hard compatibility constraint, preferred = informational only.

  • If InputBindings(...) is omitted, same-name sources are inferred automatically from

: unique producers (same scale first, then cross-scale). Ambiguous cases require explicit bindings.

  • For inferred bindings, policy defaults to producer output_policy when defined, otherwise HoldLast().
  • Explicit InputBindings(..., policy=...) always overrides trait defaults.
  • output_policy is hint-only: it is applied only when an output is actually consumed/exported.
  • If MeteoBindings(...) / MeteoWindow(...) are omitted, meteo_hint(::Type{<:Model})

: may provide (; bindings=..., window=...).

  • Explicit mapping-level configuration always overrides hints.

Compatibility checks:

  • Meteo duration is mandatory when meteo is provided.
  • For models with meteo-derived timestep, runtime enforces timestep_hint.required.
  • timestep_hint.preferred never sets runtime timestep by itself.

Scope selection detail:

  • ScopeModel(:global) is the default and shares streams across the whole simulation.
  • ScopeModel(:plant) isolates streams within each plant subtree.
  • ScopeModel(:scene) isolates by scene ancestor.
  • ScopeModel(:self) isolates by node id.

Exporting variables at requested rates

req_hold = OutputRequest("Leaf", :A; name=:A_hourly, process=:assim, policy=HoldLast())
req_day = OutputRequest("Leaf", :A; name=:A_daily_sum, process=:assim, policy=Integrate(), clock=ClockSpec(24.0, 1.0))
run!(sim, meteo; tracked_outputs=[req_hold, req_day], executor=SequentialEx())
out = collect_outputs(sim; sink=DataFrame)

# or directly:
out_status, out = run!(
    sim,
    meteo;
    tracked_outputs=[req_hold, req_day],
    return_requested_outputs=true,
)
  • process is optional when the source is canonical and unique.
  • policy defines how source streams are resampled at export time.
  • clock defines the export schedule; omit it to export every simulation step.

Default hold-last

ModelSpec(ConsumerModel()) |>
TimeStepModel(ClockSpec(2.0, 1.0)) |>
InputBindings(; x=(process=:producer, var=:x))

Meteo aggregation bindings

ModelSpec(DailyModel()) |>
TimeStepModel(ClockSpec(24.0, 1.0)) |>
MeteoWindow(CalendarWindow(:day; anchor=:current_period, week_start=1, completeness=:strict)) |>
MeteoBindings(
    T=MeanWeighted(),                     # default source is :T
    Ri_SW_f=RadiationEnergy(),            # integrate W m-2 to MJ m-2 over the model window
    custom_peak=(source=:custom_var, reducer=MaxReducer()),
)

MeteoWindow(...) options:

  • RollingWindow() (default): trailing rolling window driven by dt.
  • CalendarWindow(period; anchor, week_start, completeness) with:

: period in :day, :week, :month : anchor in :current_period, :previous_complete_period : week_start in 1:7 (1 = Monday) : completeness in :allow_partial, :strict

Parameterized window reducers

Integrate() defaults to SumReducer(); Aggregate() defaults to MeanReducer(). With the same reducer, they are runtime-equivalent. Use Integrate for accumulation semantics and Aggregate for summary-statistics semantics.

ModelSpec(DailyModel()) |>
TimeStepModel(ClockSpec(24.0, 1.0)) |>
InputBindings(; a=(process=:hourly_assim, var=:A, scale="Leaf", policy=Integrate(SumReducer())))

ModelSpec(DailyModel()) |>
TimeStepModel(ClockSpec(24.0, 1.0)) |>
InputBindings(; a=(process=:hourly_assim, var=:A, scale="Leaf", policy=Aggregate(MaxReducer())))

ModelSpec(DailyModel()) |>
TimeStepModel(ClockSpec(24.0, 1.0)) |>
InputBindings(; a=(process=:hourly_assim, var=:A, scale="Leaf", policy=Integrate(vals -> maximum(vals) - minimum(vals))))

ModelSpec(DailyModel()) |>
TimeStepModel(ClockSpec(24.0, 1.0)) |>
InputBindings(; a=(process=:hourly_assim, var=:A, scale="Leaf", policy=Integrate((vals, durations) -> sum(vals .* durations))))

ModelSpec(DailyModel()) |>
TimeStepModel(ClockSpec(24.0, 1.0)) |>
InputBindings(; a=(process=:hourly_assim, var=:A, scale="Leaf", policy=Integrate(PlantMeteo.DurationSumReducer())))

Built-in reducer types are: SumReducer(), MeanReducer(), MaxReducer(), MinReducer(), FirstReducer(), LastReducer(). The same reducer objects are also used by MeteoBindings(...). Custom reducers/callables can accept either (values) or (values, durations_seconds).

Parameterized interpolation mode

Interpolate() defaults to mode=:linear, extrapolation=:linear.

ModelSpec(FastModel()) |>
TimeStepModel(1.0) |>
InputBindings(; x=(process=:slow_source, var=:x, policy=Interpolate()))

ModelSpec(FastModel()) |>
TimeStepModel(1.0) |>
InputBindings(; x=(process=:slow_source, var=:x, policy=Interpolate(; mode=:hold, extrapolation=:hold)))