Public API
Index
PlantMeteo.TimeStepTable
PlantSimEngine.AbstractModel
PlantSimEngine.ModelList
PlantSimEngine.MultiScaleModel
PlantSimEngine.PreviousTimeStep
PlantSimEngine.Status
PlantSimEngine.EF
PlantSimEngine.NRMSE
PlantSimEngine.RMSE
PlantSimEngine.add_organ!
PlantSimEngine.convert_outputs
PlantSimEngine.dep
PlantSimEngine.dr
PlantSimEngine.fit
PlantSimEngine.init_status!
PlantSimEngine.init_variables
PlantSimEngine.inputs
PlantSimEngine.inputs
PlantSimEngine.is_initialized
PlantSimEngine.outputs
PlantSimEngine.outputs
PlantSimEngine.run!
PlantSimEngine.status
PlantSimEngine.to_initialize
PlantSimEngine.variables
PlantSimEngine.variables
PlantSimEngine.variables
PlantSimEngine.@process
API documentation
PlantMeteo.TimeStepTable
— MethodTimeStepTable{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),
]
)
PlantSimEngine.AbstractModel
— TypeAbstract model type. All models are subtypes of this one.
PlantSimEngine.ModelList
— TypeModelList(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.
Arguments
models
: a list of models. Usually given as aNamedTuple
, 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 ModelList
makes 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)
TimeStepTable{Status{(:var5, :var4, :var6, ...}(1 x 6):
╭─────┬─────────┬─────────┬─────────┬─────────┬─────────┬─────────╮
│ Row │ var5 │ var4 │ var6 │ var1 │ var3 │ var2 │
│ │ Float64 │ Float64 │ Float64 │ Float64 │ Float64 │ Float64 │
├─────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┤
│ 1 │ 36.0139 │ 22.0 │ 58.0139 │ 15.0 │ 5.5 │ 0.3 │
╰─────┴─────────┴─────────┴─────────┴─────────┴─────────┴─────────╯
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 = 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
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
PlantSimEngine.MultiScaleModel
— TypeMultiScaleModel(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-scalemapped_variables<:Vector{Pair{Symbol,Union{AbstractString,Vector{AbstractString}}}}
: a vector of pairs of symbols and strings or vectors of strings
The mapped_variables argument can be of the form:
[:variable_name => "Plant"]
: We take one value from the Plant node[:variable_name => ["Leaf"]]
: We take a vector of values from the Leaf nodes[:variable_name => ["Leaf", "Internode"]]
: We take a vector of values from the Leaf and Internode nodes[:variable_name => "Plant" => :variable_name_in_plant_scale]
: We take one value from another variable name in the Plant node[: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[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[:variable_name => :variable_name_from_another_model]
: We take the value from another model at the same scale but rename it[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:
- The variable
variable_name
of the model will be taken from thePlant
node, assuming only one node has thePlant
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.
- The variable
variable_name
of the model will be taken from theLeaf
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.
- The variable
variable_name
of the model will be taken from theLeaf
andInternode
nodes. The values will be taken from all the nodes of typeLeaf
and Internode
.
- The variable
variable_name
of the model will be taken from the variable calledvariable_name_in_plant_scale
in thePlant
node. This is useful
when the variable name in the model is different from the variable name in the scale it is taken from.
The variable
variable_name
of the model will be taken from the variable calledvariable_name_1
in theLeaf
node andvariable_name_2
in theInternode
node.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.
The variable
variable_name
of the model will be taken from another model at the same scale, but with another variable name.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{String}}}:
:carbon_allocation => ["Leaf", "Internode"]
The mappedvariables argument is a vector of pairs of symbols and strings or vectors of strings. In this case, we have only one pair to define the mapping between the `carbonallocationvariable and the
Leafand
Internode` 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)
MultiScaleModel{ToyCAllocationModel, Vector{Pair{Union{Symbol, PreviousTimeStep}, Union{Pair{String, Symbol}, Vector{Pair{String, Symbol}}}}}}(ToyCAllocationModel(), Pair{Union{Symbol, PreviousTimeStep}, Union{Pair{String, Symbol}, Vector{Pair{String, Symbol}}}}[:carbon_allocation => ["Leaf" => :carbon_allocation, "Internode" => :carbon_allocation]])
We can access the mapped variables and the model:
julia> PlantSimEngine.mapped_variables_(multiscale_model)
1-element Vector{Pair{Union{Symbol, PreviousTimeStep}, Union{Pair{String, Symbol}, Vector{Pair{String, Symbol}}}}}:
:carbon_allocation => ["Leaf" => :carbon_allocation, "Internode" => :carbon_allocation]
julia> PlantSimEngine.model_(multiscale_model)
ToyCAllocationModel()
PlantSimEngine.PreviousTimeStep
— TypePreviousTimeStep(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).
PlantSimEngine.Status
— TypeStatus(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(Rₛ=13.747, sky_fraction=1.0, d=0.03, aPPFD=1500.0);
All these indexing methods are valid:
julia> st[:Rₛ]
13.747
julia> st.Rₛ
13.747
julia> st[1]
13.747
Setting a Status variable is very easy:
julia> st[:Rₛ] = 20.0
20.0
julia> st.Rₛ = 21.0
21.0
julia> st[1] = 22.0
22.0
PlantSimEngine.EF
— MethodEF(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)
PlantSimEngine.NRMSE
— MethodNRMSE(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)
PlantSimEngine.RMSE
— MethodRMSE(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)
PlantSimEngine.add_organ!
— Methodadd_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. theGraphSimulation
object from theextra
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 nodeid
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 toinit_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.
PlantSimEngine.convert_outputs
— Methodconvert_outputs(sim_outputs::Dict{String,O} where O, sink; refvectors=false, no_value=nothing)
convert_outputs(sim_outputs::TimeStepTable{T} where T, sink)
Convert the outputs returned by a simulation made on a plant graph into another format.
Details
The first method operates on the outputs of a multiscale simulation, the second one on those of a typical single-scale simulation. The sink function determines the format used, for exemple a DataFrame
.
Arguments
sim_outputs : the outputs of a prior simulation, typically returned by
run!`.sink
: a sink compatible with the Tables.jl interface (e.g. aDataFrame
)refvectors
: iffalse
(default), the function will remove the RefVector values, otherwise it will keep themno_value
: the value to replacenothing
values. Default isnothing
. Usually used to replacenothing
values
by missing
in DataFrames.
Examples
using PlantSimEngine, MultiScaleTreeGraph, DataFrames, PlantSimEngine.Examples
Import example models (can be found in the examples
folder of the package, or in the Examples
sub-modules):
julia> using PlantSimEngine.Examples;
mapping = Dict( "Plant" => ( MultiScaleModel( model=ToyCAllocationModel(), mapped_variables=[ :carbon_assimilation => ["Leaf"], :carbon_demand => ["Leaf", "Internode"], :carbon_allocation => ["Leaf", "Internode"] ], ),
MultiScaleModel( model=ToyPlantRmModel(), mapped_variables=[:Rm_organs => ["Leaf" => :Rm, "Internode" => :Rm],] ), ),"Internode" => ( ToyCDemandModel(optimal_biomass=10.0, development_duration=200.0), ToyMaintenanceRespirationModel(1.5, 0.06, 25.0, 0.6, 0.004), Status(TT=10.0) ), "Leaf" => ( MultiScaleModel( model=ToyAssimModel(), mapped_variables=[:soil_water_content => "Soil",], ), ToyCDemandModel(optimal_biomass=10.0, development_duration=200.0), ToyMaintenanceRespirationModel(2.1, 0.06, 25.0, 1.0, 0.025), Status(aPPFD=1300.0, TT=10.0), ), "Soil" => ( ToySoilWaterModel(), ), )
mtg = import_mtg_example();
out = run!(mtg, mapping, meteo, tracked_outputs = Dict(
"Leaf" => (:carbon_assimilation, :carbon_demand, :soil_water_content, :carbon_allocation),
"Internode" => (:carbon_allocation,),
"Plant" => (:carbon_allocation,),
"Soil" => (:soil_water_content,),
));
convert_outputs(out, DataFrames)
PlantSimEngine.dep
— Functiondep(m::ModelList, nsteps=1; verbose::Bool=true)
dep(mapping::Dict{String,T}; verbose=true)
Get the model dependency graph given a ModelList 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):
- The process has no inputs. It is completely independent, and is placed as one of the roots of the dependency graph.
- The process needs inputs from models at its own scale. We put it as a child of this other process.
- The process needs inputs from another scale. We put it as a child of this process at another scale.
- The process needs inputs from its own scale and another scale. We put it as a child of both.
- 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 => Dict(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 retrieve the models that are coupled together, in the right order of execution.
Examples
using PlantSimEngine
# Including example processes and models:
using PlantSimEngine.Examples;
models = ModelList(
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...)
PlantSimEngine.dr
— Methoddr(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)
PlantSimEngine.fit
— Functionfit()
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.PPFD ./ 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 PPFD
, 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 PPFD using the Beer-Lambert law with a value of k=0.6
, and then use the simulated PPFD to fit the k
parameter again, which gives the same value as the one used on the simulation.
PlantSimEngine.init_status!
— Methodinit_status!(object::Dict{String,ModelList};vars...)
init_status!(component::ModelList;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" => ModelList(
process1=Process1Model(1.0),
process2=Process2Model(),
process3=Process3Model()
),
"InterNode" => ModelList(
process1=Process1Model(1.0),
)
)
init_status!(models, var1=1.0 , var2=2.0)
status(models["Leaf"])
PlantSimEngine.init_variables
— Methodinit_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())
PlantSimEngine.inputs
— Methodinputs(model::AbstractModel)
inputs(...)
Get the inputs of one or several models.
Returns an empty tuple by default for AbstractModel
s (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)
PlantSimEngine.inputs
— Methodinputs(mapping::Dict{String,T})
Get the inputs of the models in a mapping, for each process and organ type.
PlantSimEngine.is_initialized
— Methodis_initialized(m::T) where T <: ModelList
is_initialized(m::T, models...) where T <: ModelList
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 = ModelList(
process1=Process1Model(1.0),
process2=Process2Model(),
process3=Process3Model()
)
is_initialized(models)
PlantSimEngine.outputs
— Methodoutputs(model::AbstractModel)
outputs(...)
Get the outputs of one or several models.
Returns an empty tuple by default for AbstractModel
s (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,)
PlantSimEngine.outputs
— Methodoutputs(mapping::Dict{String,T})
Get the outputs of the models in a mapping, for each process and organ type.
PlantSimEngine.run!
— Functionrun!(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
object
: aModelList
, an array or dict ofModelList
, or a plant graph (MTG).meteo
: aPlantMeteo.TimeStepTable
of
PlantMeteo.Atmosphere
or a single PlantMeteo.Atmosphere
.
constants
: aPlantMeteo.Constants
object, or aNamedTuple
of constant keys and values.extra
: extra parameters, not available for simulation of plant graphs (the simulation object is passed using this).check
: iftrue
, check the validity of the model list before running the simulation (takes a little bit of time), and return more information while running.executor
: theFloops
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 mapping between the MTG and the model list.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.
Returns
Modifies the status of the object in-place. Users may retrieve the results from the object using the status
function (see examples).
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 list:
julia> models = ModelList(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!(models, meteo);
Get the results:
julia> (outputs_sim[:var4],outputs_sim[:var6])
([12.0], [41.95])
PlantSimEngine.status
— Methodstatus(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 = ModelList(
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
PlantSimEngine.to_initialize
— Methodto_initialize(; verbose=true, vars...)
to_initialize(m::T) where T <: ModelList
to_initialize(m::DependencyGraph)
to_initialize(mapping::Dict{String,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
: iftrue
, print information messages.vars...
: the models and processes to consider.m::T
: aModelList
.m::DependencyGraph
: aDependencyGraph
.mapping::Dict{String,T}
: a mapping that associates models to organs.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 = ModelList(process1=Process1Model(1.0), process2=Process2Model())
to_initialize(models)
m = ModelList(
(
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 = Dict(
"Leaf" => ModelList(
process1=Process1Model(1.0),
process2=Process2Model(),
process3=Process3Model()
),
"Internode" => ModelList(
process1=Process1Model(1.0),
)
)
to_initialize(mapping)
PlantSimEngine.variables
— Methodvariables(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)
PlantSimEngine.variables
— Methodvariables(mapping::Dict{String,T})
Get the variables (inputs and outputs) of the models in a mapping, for each process and organ type.
PlantSimEngine.variables
— Methodvariables(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
PlantSimEngine.@process
— Macro@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"