API - internal functions

Un-exported

Private functions, types or constants from PlantSimEngine. These are not exported, so you need to use PlantSimEngine. to access them (e.g. PlantSimEngine.DataFormat). Most of them are developer code, but some may be useful for tinkerers, or to have greater control over some simulation parameters (future versions of this documentation might break those categories into separate pages for clarity).

Index

API documentation

DataFrames.DataFrameMethod
DataFrame(components <: AbstractArray{<:ModelList})
DataFrame(components <: AbstractDict{N,<:ModelList})

Fetch the data from a ModelList (or an Array/Dict of) status into a DataFrame.

Examples

using PlantSimEngine
using DataFrames

# Creating a ModelList
models = ModelList(
    process1=Process1Model(1.0),
    process2=Process2Model(),
    process3=Process3Model(),
    status=(var1=15.0, var2=0.3)
)

# Converting to a DataFrame
df = DataFrame(models)

# Converting to a Dict of ModelLists
models = Dict(
    "Leaf" => ModelList(
        process1=Process1Model(1.0),
        process2=Process2Model(),
        process3=Process3Model()
    ),
    "InterNode" => ModelList(
        process1=Process1Model(1.0),
        process2=Process2Model(),
        process3=Process3Model()
    )
)

# Converting to a DataFrame
df = DataFrame(models)
source
DataFrames.DataFrameMethod
DataFrame(components::ModelList{T,S}) where {T,S<:Status}

Implementation of DataFrame for a ModelList model with one time step.

source
PlantSimEngine.AbstractBasic_Current_TimestepModelType

basic_current_timestep process abstract model.

All models implemented to simulate the basic_current_timestep process must be a subtype of this type, e.g. struct MyBasic_Current_TimestepModel <: AbstractBasic_Current_TimestepModel end.

You can list all models implementing this process using subtypes:

Examples

subtypes(AbstractBasic_Current_TimestepModel)
source
PlantSimEngine.AbstractBasic_Next_TimestepModelType

basic_next_timestep process abstract model.

All models implemented to simulate the basic_next_timestep process must be a subtype of this type, e.g. struct MyBasic_Next_TimestepModel <: AbstractBasic_Next_TimestepModel end.

You can list all models implementing this process using subtypes:

Examples

subtypes(AbstractBasic_Next_TimestepModel)
source
PlantSimEngine.DataFormatMethod
DataFormat(T::Type)

Returns the data format of the type T. The data format is used to determine how to iterate over the data. The following data formats are supported:

  • TableAlike: The data is a table-like object, e.g. a DataFrame or a TimeStepTable. The data is iterated over by rows using the Tables.jl interface.
  • SingletonAlike: The data is a singleton-like object, e.g. a NamedTuple or a TimeStepRow. The data is iterated over by columns.
  • TreeAlike: The data is a tree-like object, e.g. a Node.

The default implementation returns TableAlike for AbstractDataFrame, TimeStepTable, AbstractVector and Dict, TreeAlike for GraphSimulation, SingletonAlike for Status, ModelList, NamedTuple and TimeStepRow.

The default implementation for Any throws an error. Users that want to use another input should define this trait for the new data format, e.g.:

PlantSimEngine.DataFormat(::Type{<:MyType}) = TableAlike()

Examples

julia> using PlantSimEngine, PlantMeteo, DataFrames

julia> PlantSimEngine.DataFormat(DataFrame)
PlantSimEngine.TableAlike()

julia> PlantSimEngine.DataFormat(TimeStepTable([Status(a = 1, b = 2, c = 3)]))
PlantSimEngine.TableAlike()

julia> PlantSimEngine.DataFormat([1, 2, 3])
PlantSimEngine.TableAlike()

julia> PlantSimEngine.DataFormat(Dict(:a => 1, :b => 2))
PlantSimEngine.TableAlike()

julia> PlantSimEngine.DataFormat(Status(a = 1, b = 2, c = 3))
PlantSimEngine.SingletonAlike()
source
PlantSimEngine.DependencyGraphType
DependencyGraph{T}(roots::T, not_found::Dict{Symbol,DataType})

A graph of dependencies between models.

Arguments

  • roots::T: the root nodes of the graph.
  • not_found::Dict{Symbol,DataType}: the models that were not found in the graph.
source
PlantSimEngine.DependencyTraitType
DependencyTrait(T::Type)

Returns information about the eventual dependence of a model T to other time-steps or objects for its computation. The dependence trait is used to determine if a model is parallelizable or not.

The following dependence traits are supported:

  • TimeStepDependencyTrait: Trait that defines whether a model can be parallelizable over time-steps for its computation.
  • ObjectDependencyTrait: Trait that defines whether a model can be parallelizable over objects for its computation.
source
PlantSimEngine.GraphSimulationType
GraphSimulation(graph, mapping)
GraphSimulation(graph, statuses, dependency_graph, models, outputs)

A type that holds all information for a simulation over a graph.

Arguments

  • graph: an graph, such as an MTG
  • mapping: a dictionary of model mapping
  • statuses: a structure that defines the status of each node in the graph
  • status_templates: a dictionary of status templates
  • reverse_multiscale_mapping: a dictionary of mapping for other scales
  • var_need_init: a dictionary indicating if a variable needs to be initialized
  • dependency_graph: the dependency graph of the models applied to the graph
  • models: a dictionary of models
  • outputs: a dictionary of outputs
source
PlantSimEngine.MappedVarType
MappedVar(source_organ, variable, source_variable, source_default)

A variable mapped to another scale.

Arguments

  • source_organ: the organ(s) that are targeted by the mapping
  • variable: the name of the variable that is mapped
  • source_variable: the name of the variable from the source organ (the one that computes the variable)
  • source_default: the default value of the variable

Examples

julia> using PlantSimEngine
julia> PlantSimEngine.MappedVar(PlantSimEngine.SingleNodeMapping("Leaf"), :carbon_assimilation, :carbon_assimilation, 1.0)
PlantSimEngine.MappedVar{PlantSimEngine.SingleNodeMapping, Symbol, Symbol, Float64}(PlantSimEngine.SingleNodeMapping("Leaf"), :carbon_assimilation, :carbon_assimilation, 1.0)
source
PlantSimEngine.MultiNodeMappingType
MultiNodeMapping(scale)

Type for the multiple node mapping, e.g. [:carbon_assimilation => ["Leaf"],]. Note that "Leaf" is given as a vector, which means :carbon_assimilation will be a vector of values taken from each "Leaf" in the plant graph.

source
PlantSimEngine.ObjectDependencyTraitType
ObjectDependencyTrait(::Type{T})

Defines the trait about the eventual dependence of a model T to other objects for its computation. This dependency trait is used to determine if a model is parallelizable over objects or not.

The following dependency traits are supported:

  • IsObjectDependent: The model depends on other objects for its computation, it cannot be run in parallel.
  • IsObjectIndependent: The model does not depend on other objects for its computation, it can be run in parallel.

All models are object dependent by default (i.e. IsObjectDependent). This is probably not right for the majority of models, but:

  1. It is the safest default, as it will not lead to incorrect results if the user forgets to override this trait

which is not the case for the opposite (i.e. IsObjectIndependent)

  1. It is easy to override this trait for models that are object independent

See also

  • timestep_parallelizable: Returns true if the model is parallelizable over time-steps, and false otherwise.
  • object_parallelizable: Returns true if the model is parallelizable over objects, and false otherwise.
  • parallelizable: Returns true if the model is parallelizable, and false otherwise.
  • TimeStepDependencyTrait: Defines the trait about the eventual dependence of a model to other time-steps for its computation.

Examples

Define a dummy process:

using PlantSimEngine

# Define a test process:
@process "TestProcess"

Define a model that is object independent:

struct MyModel <: AbstractTestprocessModel end

# Override the object dependency trait:
PlantSimEngine.ObjectDependencyTrait(::Type{MyModel}) = IsObjectIndependent()

Check if the model is parallelizable over objects:

object_parallelizable(MyModel()) # false

Define a model that is object dependent:

struct MyModel2 <: AbstractTestprocessModel end

# Override the object dependency trait:
PlantSimEngine.ObjectDependencyTrait(::Type{MyModel2}) = IsObjectDependent()

Check if the model is parallelizable over objects:

object_parallelizable(MyModel()) # true
source
PlantSimEngine.RefVariableType
RefVariable(reference_variable)

A structure to manually flag a variable in a model to use the value of another variable at the same scale. This is used for variable renaming, when a variable is computed by a model but is used by another model with a different name.

Note: we don't really rename the variable in the status (we need it for the other models), but we create a new one that is a reference to the first one.

source
PlantSimEngine.RefVectorType
RefVector(field::Symbol, sts...)
RefVector(field::Symbol, sts::Vector{<:Status})
RefVector(v::Vector{Base.RefValue{T}})

A vector of references to a field of a vector of structs. This is used to efficiently pass the values between scales.

Arguments

  • field: the field of the struct to reference
  • sts...: the structs to reference
  • sts::Vector{<:Status}: a vector of structs to reference

Examples

julia> using PlantSimEngine

Let's take two Status structs:

julia> status1 = Status(a = 1.0, b = 2.0, c = 3.0);
julia> status2 = Status(a = 2.0, b = 3.0, c = 4.0);

We can make a RefVector of the field a of the structs st1 and st2:

julia> rv = PlantSimEngine.RefVector(:a, status1, status2)
2-element PlantSimEngine.RefVector{Float64}:
 1.0
 2.0

Which is equivalent to:

julia> rv = PlantSimEngine.RefVector(:a, [status1, status2])
2-element PlantSimEngine.RefVector{Float64}:
 1.0
 2.0

We can access the values of the RefVector:

julia> rv[1]
1.0

Updating the value in the RefVector will update the value in the original struct:

julia> rv[1] = 10.0
10.0
julia> status1.a
10.0

We can also make a RefVector from a vector of references:

julia> vec = [Ref(1.0), Ref(2.0), Ref(3.0)]
3-element Vector{Base.RefValue{Float64}}:
 Base.RefValue{Float64}(1.0)
 Base.RefValue{Float64}(2.0)
 Base.RefValue{Float64}(3.0)
julia> rv = PlantSimEngine.RefVector(vec)
3-element PlantSimEngine.RefVector{Float64}:
 1.0
 2.0
 3.0
julia> rv[1]
1.0
source
PlantSimEngine.SelfNodeMappingType
SelfNodeMapping()

Type for the self node mapping, i.e. a node that maps onto itself. This is used to flag variables that will be referenced as a scalar value by other models. It can happen in two conditions: - the variable is computed by another scale, so we need this variable to exist as an input to this scale (it is not computed at this scale otherwise) - the variable is used as input to another scale but as a single value (scalar), so we need to reference it as a scalar.

source
PlantSimEngine.SingleNodeMappingType
SingleNodeMapping(scale)

Type for the single node mapping, e.g. [:soil_water_content => "Soil",]. Note that "Soil" is given as a scalar, which means that :soil_water_content will be a scalar value taken from the unique "Soil" node in the plant graph.

source
PlantSimEngine.TimeStepDependencyTraitMethod
TimeStepDependencyTrait(::Type{T})

Defines the trait about the eventual dependence of a model T to other time-steps for its computation. This dependency trait is used to determine if a model is parallelizable over time-steps or not.

The following dependency traits are supported:

  • IsTimeStepDependent: The model depends on other time-steps for its computation, it cannot be run in parallel.
  • IsTimeStepIndependent: The model does not depend on other time-steps for its computation, it can be run in parallel.

All models are time-step dependent by default (i.e. IsTimeStepDependent). This is probably not right for the majority of models, but:

  1. It is the safest default, as it will not lead to incorrect results if the user forgets to override this trait

which is not the case for the opposite (i.e. IsTimeStepIndependent)

  1. It is easy to override this trait for models that are time-step independent

See also

  • timestep_parallelizable: Returns true if the model is parallelizable over time-steps, and false otherwise.
  • object_parallelizable: Returns true if the model is parallelizable over objects, and false otherwise.
  • parallelizable: Returns true if the model is parallelizable, and false otherwise.
  • ObjectDependencyTrait: Defines the trait about the eventual dependence of a model to other objects for its computation.

Examples

Define a dummy process:

using PlantSimEngine

# Define a test process:
@process "TestProcess"

Define a model that is time-step independent:

struct MyModel <: AbstractTestprocessModel end

# Override the time-step dependency trait:
PlantSimEngine.TimeStepDependencyTrait(::Type{MyModel}) = IsTimeStepIndependent()

Check if the model is parallelizable over time-steps:

timestep_parallelizable(MyModel()) # false

Define a model that is time-step dependent:

struct MyModel2 <: AbstractTestprocessModel end

# Override the time-step dependency trait:
PlantSimEngine.TimeStepDependencyTrait(::Type{MyModel2}) = IsTimeStepDependent()

Check if the model is parallelizable over time-steps:

timestep_parallelizable(MyModel()) # true
source
Base.copyMethod
Base.copy(l::ModelList)
Base.copy(l::ModelList, status)

Copy a ModelList, eventually with new values for the status.

Examples

using PlantSimEngine

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

# Create a model list:
models = ModelList(
    process1=Process1Model(1.0),
    process2=Process2Model(),
    process3=Process3Model(),
    status=(var1=15.0, var2=0.3)
)

# Copy the model list:
ml2 = copy(models)

# Copy the model list with new status:
ml3 = copy(models, TimeStepTable([Status(var1=20.0, var2=0.5))])
source
Base.getindexMethod
getindex(component<:ModelList, key::Symbol)
getindex(component<:ModelList, key)

Indexing a component models structure: - with an integer, will return the status at the ith time-step - with anything else (Symbol, String) will return the required variable from the status

Examples

using PlantSimEngine

lm = ModelList(
    process1=Process1Model(1.0),
    process2=Process2Model(),
    process3=Process3Model(),
    status = (var1=[15.0, 16.0], var2=0.3)
);

lm[:var1] # Returns the value of the Tₗ variable
lm[2]  # Returns the status at the second time-step
lm[2][:var1] # Returns the value of Tₗ at the second time-step
lm[:var1][2] # Equivalent of the above

# output
16.0
source
PlantSimEngine.add_model_varsMethod
add_model_vars(x, models, type_promotion)

Check which variables in x are not initialized considering a set of models and the variables needed for their simulation. If some variables are uninitialized, initialize them to their default values.

This function needs to be implemented for each type of x. The default method works for any Tables.jl-compatible x and for NamedTuples.

Careful, the function makes a copy of the input x if it does not list all needed variables.

source
PlantSimEngine.check_dimensionsMethod
check_dimensions(component,weather)
check_dimensions(status,weather)

Checks if a component status (or a status directly) and the weather have the same length, or if they can be recycled (length 1 for one of them).

Examples

using PlantSimEngine, PlantMeteo

# Including an example script that implements dummy processes and models:
using PlantSimEngine.Examples

# Creating a dummy weather:
w = Atmosphere(T = 20.0, Rh = 0.5, Wind = 1.0)

# Creating a dummy component:
models = ModelList(
    process1=Process1Model(1.0),
    process2=Process2Model(),
    process3=Process3Model(),
    status=(var1=[15.0, 16.0], var2=0.3)
)

# Checking that the number of time-steps are compatible (here, they are, it returns nothing):
PlantSimEngine.check_dimensions(models, w) 

# Creating a dummy weather with 3 time-steps:
w = Weather([
    Atmosphere(T = 20.0, Rh = 0.5, Wind = 1.0),
    Atmosphere(T = 25.0, Rh = 0.5, Wind = 1.0),
    Atmosphere(T = 30.0, Rh = 0.5, Wind = 1.0)
])

# Checking that the number of time-steps are compatible (here, they are not, it throws an error):
PlantSimEngine.check_dimensions(models, w)

# output
ERROR: DimensionMismatch: Component status has a vector variable : var1 implying multiple timesteps but weather data only provides a single timestep.
source
PlantSimEngine.convert_reference_values!Method
convert_reference_values!(mapped_vars::Dict{String,Dict{Symbol,Any}})

Convert the variables that are MappedVar{SelfNodeMapping} or MappedVar{SingleNodeMapping} to RefValues that reference a common value for the variable; and convert MappedVar{MultiNodeMapping} to RefVectors that reference the values for the variable in the source organs.

source
PlantSimEngine.convert_varsFunction
convert_vars(ref_vars, type_promotion::Dict{DataType,DataType})
convert_vars(ref_vars, type_promotion::Nothing)
convert_vars!(ref_vars::Dict{Symbol}, type_promotion::Dict{DataType,DataType})
convert_vars!(ref_vars::Dict{Symbol}, type_promotion::Nothing)

Convert the status variables to the type specified in the type promotion dictionary. Note: the mutating version only works with a dictionary of variables.

Examples

If we want all the variables that are Reals to be Float32, we can use:

using PlantSimEngine

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

ref_vars = init_variables(
    process1=Process1Model(1.0),
    process2=Process2Model(),
    process3=Process3Model(),
)
type_promotion = Dict(Real => Float32)

PlantSimEngine.convert_vars(type_promotion, ref_vars.process3)
source
PlantSimEngine.convert_vars!Function
convert_vars(ref_vars, type_promotion::Dict{DataType,DataType})
convert_vars(ref_vars, type_promotion::Nothing)
convert_vars!(ref_vars::Dict{Symbol}, type_promotion::Dict{DataType,DataType})
convert_vars!(ref_vars::Dict{Symbol}, type_promotion::Nothing)

Convert the status variables to the type specified in the type promotion dictionary. Note: the mutating version only works with a dictionary of variables.

Examples

If we want all the variables that are Reals to be Float32, we can use:

using PlantSimEngine

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

ref_vars = init_variables(
    process1=Process1Model(1.0),
    process2=Process2Model(),
    process3=Process3Model(),
)
type_promotion = Dict(Real => Float32)

PlantSimEngine.convert_vars(type_promotion, ref_vars.process3)
source
PlantSimEngine.convert_vars!Method
convert_vars!(mapped_vars::Dict{String,Dict{String,Any}}, type_promotion)

Converts the types of the variables in a mapping (mapped_vars) using the type_promotion dictionary.

The mapping should be a dictionary with organ name as keys and a dictionary of variables as values, with variable names as symbols and variable value as value.

source
PlantSimEngine.default_variables_from_mappingFunction
default_variables_from_mapping(mapped_vars, verbose=true)

Get the default values for the mapped variables by recursively searching from the mapping to find the original mapped value.

Arguments

  • mapped_vars::Dict{String,Dict{Symbol,Any}}: the variables mapped to each organ.
  • verbose::Bool: whether to print the stacktrace of the search for the default value in the mapping.
source
PlantSimEngine.draw_panelMethod
draw_panel(node, graph, prefix, dep_graph_guides, parent; title="Soft-coupled model")

Draw the panels for all dependencies

source
PlantSimEngine.drop_processMethod
drop_process(proc_vars, process)

Return a new NamedTuple with the process process removed from the NamedTuple proc_vars.

Arguments

  • proc_vars::NamedTuple: the NamedTuple from which we want to remove the process process.
  • process::Symbol: the process we want to remove from the NamedTuple proc_vars.

Returns

A new NamedTuple with the process process removed from the NamedTuple proc_vars.

Example

julia> drop_process((a = 1, b = 2, c = 3), :b)
(a = 1, c = 3)

julia> drop_process((a = 1, b = 2, c = 3), (:a, :c))
(b = 2,)
source
PlantSimEngine.flatten_varsMethod
flatten_vars(vars)

Return a set of the variables in the vars dictionary.

Arguments

  • vars::Dict{Symbol, Tuple{Symbol, Vararg{Symbol}}}: a dict of process => namedtuple of variables => value.

Returns

A set of the variables in the vars dictionary.

Example

julia> flatten_vars(Dict(:process1 => (:var1, :var2), :process2 => (:var3, :var4)))
Set{Symbol} with 4 elements:
  :var4
  :var3
  :var2
  :var1
julia> flatten_vars([:process1 => (var1 = -Inf, var2 = -Inf), :process2 => (var3 = -Inf, var4 = -Inf)])
(var2 = -Inf, var4 = -Inf, var3 = -Inf, var1 = -Inf)
source
PlantSimEngine.get_mapped_variablesMethod
get_mapped_variables(m)

Get the mapping of a dictionary of model mapping.

Arguments

  • m::Dict{String,Any}: a dictionary of model mapping

Returns a vector of pairs of symbols and strings or vectors of strings

Examples

See get_models for examples.

source
PlantSimEngine.get_model_nodesMethod
get_model_nodes(dep_graph::DependencyGraph, model)

Get the nodes in the dependency graph implementing a type of model.

Arguments

  • dep_graph::DependencyGraph: the dependency graph.
  • model: the model type to look for.

Returns

  • An array of nodes implementing the model type.

Examples

PlantSimEngine.get_model_nodes(dependency_graph, Beer)
source
PlantSimEngine.get_modelsMethod
get_models(m)

Get the models of a dictionary of model mapping.

Arguments

  • m::Dict{String,Any}: a dictionary of model mapping

Returns a vector of models

Examples

julia> using PlantSimEngine;

Import example models (can be found in the examples folder of the package, or in the Examples sub-modules):

julia> using PlantSimEngine.Examples;

If we just give a MultiScaleModel, we get its model as a one-element vector:

julia> models = MultiScaleModel( model=ToyCAllocationModel(), mapped_variables=[ :carbon_assimilation => ["Leaf"], :carbon_demand => ["Leaf", "Internode"], :carbon_allocation => ["Leaf", "Internode"] ], );
julia> PlantSimEngine.get_models(models)
1-element Vector{ToyCAllocationModel}:
 ToyCAllocationModel()

If we give a tuple of models, we get each model in a vector:

julia> models2 = (  MultiScaleModel( model=ToyAssimModel(), mapped_variables=[:soil_water_content => "Soil",], ), ToyCDemandModel(optimal_biomass=10.0, development_duration=200.0), Status(aPPFD=1300.0, TT=10.0), );

Notice that we provide "Soil", not ["Soil"] in the mapping because a single value is expected for the mapping here.

julia> PlantSimEngine.get_models(models2)
2-element Vector{AbstractModel}:
 ToyAssimModel{Float64}(0.2)
 ToyCDemandModel{Float64}(10.0, 200.0)
source
PlantSimEngine.get_multiscale_default_valueFunction
get_multiscale_default_value(mapped_vars, val, mapping_stacktrace=[])

Get the default value of a variable from a mapping.

Arguments

  • mapped_vars::Dict{String,Dict{Symbol,Any}}: the variables mapped to each organ.
  • val::Any: the variable to get the default value of.
  • mapping_stacktrace::Vector{Any}: the stacktrace of the search for the value in ascendind the mapping.
source
PlantSimEngine.hard_dependenciesMethod
hard_dependencies(models; verbose::Bool=true)
hard_dependencies(mapping::Dict{String,T}; verbose::Bool=true)

Compute the hard dependencies between models.

source
PlantSimEngine.homogeneous_ts_kwargsMethod
kwargs_to_timestep(kwargs::NamedTuple{N,T}) where {N,T}

Takes a NamedTuple with optionnaly vector of values for each variable, and makes a vector of NamedTuple, with each being a time step. It is used to be able to e.g. give constant values for all time-steps for one variable.

Examples

PlantSimEngine.homogeneous_ts_kwargs((Tₗ=[25.0, 26.0], aPPFD=1000.0))
source
PlantSimEngine.init_node_status!Function
init_node_status!(
    node, 
    statuses, 
    mapped_vars, 
    reverse_multiscale_mapping,
    vars_need_init=Dict{String,Any}(),
    type_promotion=nothing;
    check=true,
    attribute_name=:plantsimengine_status)
)

Initialise the status of a plant graph node, taking into account the multiscale mapping, and add it to the statuses dictionary.

Arguments

  • node: the node to initialise
  • statuses: the dictionary of statuses by node type
  • mapped_vars: the template of status for each node type
  • reverse_multiscale_mapping: the variables that are mapped to other scales
  • var_need_init: the variables that are not initialised or computed by other models
  • nodes_with_models: the nodes that have a model defined for their symbol
  • type_promotion: the type promotion to use for the variables
  • check: whether to check the mapping for errors (see details)
  • attribute_name: the name of the attribute to store the status in the node, by default: :plantsimengine_status

Details

Most arguments can be computed from the graph and the mapping:

  • statuses is given by the first initialisation: statuses = Dict(i => Status[] for i in nodes_with_models)
  • mapped_vars is computed using mapped_variables(), see code in init_statuses
  • vars_need_init is computed using `varsneedinit = Dict(org => filter(x -> isa(last(x), UninitializedVar), vars) |> keys for (org, vars) in mapped_vars) |>

filter(x -> length(last(x)) > 0)`

The check argument is a boolean indicating if variables initialisation should be checked. In the case that some variables need initialisation (partially initialized mapping), we check if the value can be found in the node attributes (using the variable name). If true, the function returns an error if the attribute is missing, otherwise it uses the default value from the model.

source
PlantSimEngine.init_simulationMethod
init_simulation(mtg, mapping; nsteps=1, outputs=nothing, type_promotion=nothing, check=true, verbose=true)

Initialise the simulation. Returns:

  • the mtg
  • a status for each node by organ type, considering multi-scale variables
  • the dependency graph of the models
  • the models parsed as a Dict of organ type => NamedTuple of process => model mapping
  • the pre-allocated outputs

Arguments

  • mtg: the MTG
  • mapping::Dict{String,Any}: a dictionary of model mapping
  • nsteps: the number of steps of the simulation
  • outputs: the dynamic outputs needed for the simulation
  • type_promotion: the type promotion to use for the variables
  • check: whether to check the mapping for errors. Passed to init_node_status!.
  • verbose: print information about errors in the mapping

Details

The function first computes a template of status for each organ type that has a model in the mapping. This template is used to initialise the status of each node of the MTG, taking into account the user-defined initialisation, and the (multiscale) mapping. The mapping is used to make references to the variables that are defined at another scale, so that the values are automatically updated when the variable is changed at the other scale. Two types of multiscale variables are available: RefVector and MappedVar. The first one is used when the variable is mapped to a vector of nodes, and the second one when it is mapped to a single node. This is given by the user through the mapping, using a string for a single node (e.g. => "Leaf"), and a vector of strings for a vector of nodes (e.g. => ["Leaf"] for one type of node or => ["Leaf", "Internode"] for several).

The function also computes the dependency graph of the models, i.e. the order in which the models should be called, considering the dependencies between them. The dependency graph is used to call the models in the right order when the simulation is run.

Note that if a variable is not computed by models or initialised from the mapping, it is searched in the MTG attributes. The value is not a reference to the one in the attribute of the MTG, but a copy of it. This is because we can't reference a value in a Dict. If you need a reference, you can use a Ref for your variable in the MTG directly, and it will be automatically passed as is.

source
PlantSimEngine.init_statusesFunction
init_statuses(mtg, mapping, dependency_graph=dep(mapping); type_promotion=nothing, verbose=true, check=true)

Get the status of each node in the MTG by node type, pre-initialised considering multi-scale variables.

Arguments

  • mtg: the plant graph
  • mapping: a dictionary of model mapping
  • dependency_graph::DependencyGraph: the first-order dependency graph where each model in the mapping is assigned a node.

However, models that are identified as hard-dependencies are not given individual nodes. Instead, they are nested as child nodes under other models.

  • type_promotion: the type promotion to use for the variables
  • verbose: print information when compiling the mapping
  • check: whether to check the mapping for errors. Passed to init_node_status!.

Return

A NamedTuple of status by node type, a dictionary of status templates by node type, a dictionary of variables mapped to other scales, a dictionary of variables that need to be initialised or computed by other models, and a vector of nodes that have a model defined for their symbol:

(;statuses, status_templates, reverse_multiscale_mapping, vars_need_init, nodes_with_models)

source
PlantSimEngine.init_variables_manualMethod
init_variables_manual(models...;vars...)

Return an initialisation of the model variables with given values.

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()
)

PlantSimEngine.init_variables_manual(status(models), (var1=20.0,))
source
PlantSimEngine.is_graph_cyclicMethod
is_graph_cyclic(dependency_graph::DependencyGraph; full_stack=false, verbose=true)

Check if the dependency graph is cyclic.

Arguments

  • dependency_graph::DependencyGraph: the dependency graph to check.
  • full_stack::Bool=false: if true, return the full stack of nodes that makes the cycle, otherwise return only the cycle.
  • warn::Bool=true: if true, print a stylised warning message when a cycle is detected.

Return a boolean indicating if the graph is cyclic, and the stack of nodes as a vector.

source
PlantSimEngine.mapped_variablesFunction
mapped_variables(mapping, dependency_graph=first(hard_dependencies(mapping; verbose=false)); verbose=false)

Get the variables for each organ type from a dependency graph, with MappedVars for the multiscale mapping.

Arguments

  • mapping::Dict{String,T}: the mapping between models and scales.
  • dependency_graph::DependencyGraph: the first-order dependency graph where each model in the mapping is assigned a node.

However, models that are identified as hard-dependencies are not given individual nodes. Instead, they are nested as child nodes under other models.

  • verbose::Bool: whether to print the stacktrace of the search for the default value in the mapping.
source
PlantSimEngine.mapped_variables_no_outputs_from_other_scaleFunction
mapped_variables_no_outputs_from_other_scale(mapping, dependency_graph=first(hard_dependencies(mapping; verbose=false)))

Get the variables for each organ type from a dependency graph, without the variables that are outputs from another scale.

Arguments

  • mapping::Dict{String,T}: the mapping between models and scales.
  • dependency_graph::DependencyGraph: the first-order dependency graph where each model in the mapping is assigned a node.

However, models that are identified as hard-dependencies are not given individual nodes. Instead, they are nested as child nodes under other models.

Details

This function returns a dictionary with the (multiscale-) inputs and outputs variables for each organ type.

Note that this function does not include the variables that are outputs from another scale and not computed by this scale, see mapped_variables_with_outputs_as_inputs for that.

source
PlantSimEngine.model_Method
model_(m::AbstractModel)

Get the model of an AbstractModel (it is the model itself if it is not a MultiScaleModel).

source
PlantSimEngine.object_parallelizableMethod
object_parallelizable(x::T)
object_parallelizable(x::DependencyGraph)

Returns true if the model x is parallelizable, i.e. if the model can be computed in parallel for different objects, or false otherwise.

The default implementation returns false for all models. If you develop a model that is parallelizable over objects, you should add a method to ObjectDependencyTrait for your model.

Note that this method can also be applied on a DependencyGraph directly, in which case it returns true if all models in the graph are parallelizable, and false otherwise.

See also

  • timestep_parallelizable: Returns true if the model is parallelizable over time-steps, and false otherwise.
  • parallelizable: Returns true if the model is parallelizable, and false otherwise.
  • ObjectDependencyTrait: Defines the trait about the eventual dependence of a model to other objects for its computation.

Examples

Define a dummy process:

using PlantSimEngine

# Define a test process:
@process "TestProcess"

Define a model that is object independent:

struct MyModel <: AbstractTestprocessModel end

# Override the object dependency trait:
PlantSimEngine.ObjectDependencyTrait(::Type{MyModel}) = IsObjectIndependent()

Check if the model is parallelizable over objects:

object_parallelizable(MyModel()) # true
source
PlantSimEngine.parallelizableMethod
parallelizable(::T)
object_parallelizable(x::DependencyGraph)

Returns true if the model T or the whole dependency graph is parallelizable, i.e. if the model can be computed in parallel for different time-steps or objects. The default implementation returns false for all models.

See also

Examples

Define a dummy process:

using PlantSimEngine

# Define a test process:
@process "TestProcess"

Define a model that is parallelizable:

struct MyModel <: AbstractTestprocessModel end

# Override the time-step dependency trait:
PlantSimEngine.TimeStepDependencyTrait(::Type{MyModel}) = IsTimeStepIndependent()

# Override the object dependency trait:
PlantSimEngine.ObjectDependencyTrait(::Type{MyModel}) = IsObjectIndependent()

Check if the model is parallelizable:

parallelizable(MyModel()) # true

Or if we want to be more explicit:

timestep_parallelizable(MyModel())
object_parallelizable(MyModel())
source
PlantSimEngine.pre_allocate_outputsMethod
pre_allocate_outputs(statuses, outs, nsteps; check=true)

Pre-allocate the outputs of needed variable for each node type in vectors of vectors. The first level vectors have length nsteps, and the second level vectors have length n_nodes of this type.

Note that we pre-allocate the vectors for the time-steps, but not for each organ, because we don't know how many nodes will be in each organ in the future (organs can appear or disapear).

Arguments

  • statuses: a dictionary of status by node type
  • outs: a dictionary of outputs by node type
  • nsteps: the number of time-steps
  • check: whether to check the mapping for errors. Default (true) returns an error if some variables do not exist.

If false and some variables are missing, return an info, remove the unknown variables and continue.

Returns

  • A dictionary of pre-allocated output of vector of time-step and vector of node of that type.

Examples

julia> using PlantSimEngine, MultiScaleTreeGraph, 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;

Define the models mapping:

julia> 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, carbon_biomass=1.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, carbon_biomass=1.0), ), "Soil" => ( ToySoilWaterModel(), ), );

Importing an example MTG provided by the package:

julia> mtg = import_mtg_example();
julia> statuses, status_templates, reverse_multiscale_mapping, vars_need_init = PlantSimEngine.init_statuses(mtg, mapping);
julia> outs = Dict("Leaf" => (:carbon_assimilation, :carbon_demand), "Soil" => (:soil_water_content,));

Pre-allocate the outputs as a dictionary:

julia> preallocated_vars = PlantSimEngine.pre_allocate_outputs(statuses, status_templates, reverse_multiscale_mapping, vars_need_init, outs, 2);

The dictionary has a key for each organ from which we want outputs:

julia> collect(keys(preallocated_vars))
2-element Vector{String}:
 "Soil"
 "Leaf"

Each organ has a dictionary of variables for which we want outputs from, with the pre-allocated empty vectors (one per time-step that will be filled with one value per node):

julia> collect(keys(preallocated_vars["Leaf"]))
3-element Vector{Symbol}:
 :carbon_assimilation
 :node
 :carbon_demand
source
PlantSimEngine.ref_varMethod
ref_var(v)

Create a reference to a variable. If the variable is already a Base.RefValue, it is returned as is, else it is returned as a Ref to the copy of the value, or a Ref to the RefVector (in case v is a RefVector).

Examples

julia> using PlantSimEngine;
julia> PlantSimEngine.ref_var(1.0)
Base.RefValue{Float64}(1.0)
julia> PlantSimEngine.ref_var([1.0])
Base.RefValue{Vector{Float64}}([1.0])
julia> PlantSimEngine.ref_var(Base.RefValue(1.0))
Base.RefValue{Float64}(1.0)
julia> PlantSimEngine.ref_var(Base.RefValue([1.0]))
Base.RefValue{Vector{Float64}}([1.0])
julia> PlantSimEngine.ref_var(PlantSimEngine.RefVector([Ref(1.0), Ref(2.0), Ref(3.0)]))
Base.RefValue{PlantSimEngine.RefVector{Float64}}(RefVector{Float64}[1.0, 2.0, 3.0])
source
PlantSimEngine.reverse_mappingMethod
reverse_mapping(mapping::Dict{String,Tuple{Any,Vararg{Any}}}; all=true)
reverse_mapping(mapped_vars::Dict{String,Dict{Symbol,Any}})

Get the reverse mapping of a dictionary of model mapping, i.e. the variables that are mapped to other scales, or in other words, what variables are given to other scales from a given scale. This is used for e.g. knowing which scales are needed to add values to others.

Arguments

  • mapping::Dict{String,Any}: A dictionary of model mapping.
  • all::Bool: Whether to get all the variables that are mapped to other scales, including the ones that are mapped as single values.

Returns

A dictionary of organs (keys) with a dictionary of organs => vector of pair of variables. You can read the output as: "for each organ (source organ), to which other organ (target organ) it is giving values for its own variables. Then for each of these source organs, which variable it is giving to the target organ (first symbol in the pair), and to which variable it is mapping the value into the target organ (second symbol in the pair)".

Examples

julia> using PlantSimEngine

Import example models (can be found in the examples folder of the package, or in the Examples sub-modules):

julia> using PlantSimEngine.Examples;
julia> mapping = Dict( "Plant" => MultiScaleModel( model=ToyCAllocationModel(), mapped_variables=[ :carbon_assimilation => ["Leaf"], :carbon_demand => ["Leaf", "Internode"], :carbon_allocation => ["Leaf", "Internode"] ], ), "Internode" => ToyCDemandModel(optimal_biomass=10.0, development_duration=200.0), "Leaf" => ( MultiScaleModel( model=ToyAssimModel(), mapped_variables=[:soil_water_content => "Soil",], ), ToyCDemandModel(optimal_biomass=10.0, development_duration=200.0), Status(aPPFD=1300.0, TT=10.0), ), "Soil" => ( ToySoilWaterModel(), ), );

Notice we provide "Soil", not ["Soil"] in the mapping of the ToyAssimModel for the Leaf. This is because we expect a single value for the soil_water_content to be mapped here (there is only one soil). This allows to get the value as a singleton instead of a vector of values.

julia> PlantSimEngine.reverse_mapping(mapping)
Dict{String, Dict{String, Dict{Symbol, Any}}} with 3 entries:
  "Soil"      => Dict("Leaf"=>Dict(:soil_water_content=>:soil_water_content))
  "Internode" => Dict("Plant"=>Dict(:carbon_allocation=>:carbon_allocation, :ca…
  "Leaf"      => Dict("Plant"=>Dict(:carbon_allocation=>:carbon_allocation, :ca…
source
PlantSimEngine.save_results!Method
save_results!(object::GraphSimulation, i)

Save the results of the simulation for time-step i into the object. For a GraphSimulation object, this will save the results from the status(object) in the outputs(object).

source
PlantSimEngine.search_inputs_in_multiscale_outputMethod
search_inputs_in_multiscale_output(process, organ, inputs, soft_dep_graphs)

Arguments

  • process::Symbol: the process for which we want to find the soft dependencies at other scales.
  • organ::String: the organ for which we want to find the soft dependencies.
  • inputs::Dict{Symbol, Vector{Pair{Symbol}, Tuple{Symbol, Vararg{Symbol}}}}: a dict of process => [:subprocess => (:var1, :var2)].
  • soft_dep_graphs::Dict{String, ...}: a dict of organ => (softdepgraph, inputs, outputs).
  • rev_mapping::Dict{Symbol, Symbol}: a dict of mapped variable => source variable (this is the reverse mapping).
  • 'harddependenciesfromotherscale' : a vector of HardDependencyNode to provide access to the hard dependencies without traversing the whole graph

Details

The inputs (and similarly, outputs) give the inputs of each process, classified by the process it comes from. It can come from itself (its own inputs), or from another process that is a hard-dependency.

Returns

A dictionary with the soft dependencies variables found in outputs of other scales for each process, e.g.:

Dict{String, Dict{Symbol, Vector{Symbol}}} with 2 entries:
    "Internode" => Dict(:carbon_demand=>[:carbon_demand])
    "Leaf"      => Dict(:carbon_assimilation=>[:carbon_assimilation], :carbon_demand=>[:carbon_demand])

This means that the variable :carbon_demand is computed by the process :carbon_demand at the scale "Internode", and the variable :carbon_assimilation is computed by the process :carbon_assimilation at the scale "Leaf". Those variables are used as inputs for the process that we just passed.

source
PlantSimEngine.search_inputs_in_outputMethod
search_inputs_in_output(process, inputs, outputs)

Return a dictionary with the soft dependencies of the processes in the dependency graph d. A soft dependency is a dependency that is not explicitely defined in the model, but that can be inferred from the inputs and outputs of the processes.

Arguments

  • process::Symbol: the process for which we want to find the soft dependencies.
  • inputs::Dict{Symbol, Vector{Pair{Symbol}, Tuple{Symbol, Vararg{Symbol}}}}: a dict of process => symbols of inputs per process.
  • outputs::Dict{Symbol, Tuple{Symbol, Vararg{Symbol}}}: a dict of process => symbols of outputs per process.

Details

The inputs (and similarly, outputs) give the inputs of each process, classified by the process it comes from. It can come from itself (its own inputs), or from another process that is a hard-dependency.

Returns

A dictionary with the soft dependencies for the processes.

Example

in_ = Dict(
    :process3 => [:process3=>(:var4, :var5), :process2=>(:var1, :var3), :process1=>(:var1, :var2)],
    :process4 => [:process4=>(:var0,)],
    :process6 => [:process6=>(:var7, :var9)],
    :process5 => [:process5=>(:var5, :var6)],
)

out_ = Dict(
    :process3 => Pair{Symbol}[:process3=>(:var4, :var6), :process2=>(:var4, :var5), :process1=>(:var3,)],
    :process4 => [:process4=>(:var1, :var2)],
    :process6 => [:process6=>(:var8,)],
    :process5 => [:process5=>(:var7,)],
)

search_inputs_in_output(:process3, in_, out_)
(process4 = (:var1, :var2),)
source
PlantSimEngine.soft_dependenciesFunction
soft_dependencies(d::DependencyGraph)

Return a DependencyGraph with the soft dependencies of the processes in the dependency graph d. A soft dependency is a dependency that is not explicitely defined in the model, but that can be inferred from the inputs and outputs of the processes.

Arguments

  • d::DependencyGraph: the hard-dependency graph.

Example

using PlantSimEngine

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

# Create a model list:
models = ModelList(
    process1=Process1Model(1.0),
    process2=Process2Model(),
    process3=Process3Model(),
    process4=Process4Model(),
    process5=Process5Model(),
    process6=Process6Model(),
)

# Create the hard-dependency graph:
hard_dep = hard_dependencies(models.models, verbose=true)

# Get the soft dependencies graph:
soft_dep = soft_dependencies(hard_dep)
source
PlantSimEngine.status_from_templateMethod
status_from_template(d::Dict{Symbol,Any})

Create a status from a template dictionary of variables and values. If the values are already RefValues or RefVectors, they are used as is, else they are converted to Refs.

Arguments

  • d::Dict{Symbol,Any}: A dictionary of variables and values.

Returns

Examples

julia> using PlantSimEngine
julia> a, b = PlantSimEngine.status_from_template(Dict(:a => 1.0, :b => 2.0));
julia> a
1.0
julia> b
2.0
source
PlantSimEngine.timestep_parallelizableMethod
timestep_parallelizable(x::T)
timestep_parallelizable(x::DependencyGraph)

Returns true if the model x is parallelizable, i.e. if the model can be computed in parallel over time-steps, or false otherwise.

The default implementation returns false for all models. If you develop a model that is parallelizable over time-steps, you should add a method to ObjectDependencyTrait for your model.

Note that this method can also be applied on a DependencyGraph directly, in which case it returns true if all models in the graph are parallelizable, and false otherwise.

See also

  • object_parallelizable: Returns true if the model is parallelizable over time-steps, and false otherwise.
  • parallelizable: Returns true if the model is parallelizable, and false otherwise.
  • TimeStepDependencyTrait: Defines the trait about the eventual dependence of a model to other time-steps for its computation.

Examples

Define a dummy process:

using PlantSimEngine

# Define a test process:
@process "TestProcess"

Define a model that is time-step independent:

struct MyModel <: AbstractTestprocessModel end

# Override the time-step dependency trait:
PlantSimEngine.TimeStepDependencyTrait(::Type{MyModel}) = IsTimeStepIndependent()

Check if the model is parallelizable over objects:

timestep_parallelizable(MyModel()) # true
source
PlantSimEngine.transform_single_node_mapped_variables_as_self_node_output!Method
transform_single_node_mapped_variables_as_self_node_output!(mapped_vars)

Find variables that are inputs to other scales as a SingleNodeMapping and declare them as MappedVar from themselves in the source scale. This helps us declare it as a reference when we create the template status objects.

These node are found in the mapping as [:variable_name => "Plant"] (notice that "Plant" is a scalar value).

source
PlantSimEngine.traverse_dependency_graph!Method
traverse_dependency_graph(node::HardDependencyNode, f::Function, var::Vector)

Apply function f to node, and then its children (hard-dependency nodes).

Mutate the vector var by pushing a pair of the node process name and the result of the function f.

source
PlantSimEngine.traverse_dependency_graph!Method
traverse_dependency_graph(node::SoftDependencyNode, f::Function, var::Vector; visit_hard_dep=true)

Apply function f to node, visit its hard dependency nodes (if visit_hard_dep=true), and then its soft dependency children.

Mutate the vector var by pushing a pair of the node process name and the result of the function f.

source
PlantSimEngine.traverse_dependency_graphMethod
traverse_dependency_graph(graph::DependencyGraph, f::Function; visit_hard_dep=true)

Traverse the dependency graph and apply the function f to each node. The first-level soft-dependencies are traversed first, then their hard-dependencies (if visit_hard_dep=true), and then the children of the soft-dependencies.

Return a vector of pairs of the node and the result of the function f.

Example

using PlantSimEngine

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

function f(node)
    node.value
end

vars = (
    process1=Process1Model(1.0),
    process2=Process2Model(),
    process3=Process3Model(),
    process4=Process4Model(),
    process5=Process5Model(),
    process6=Process6Model(),
    process7=Process7Model(),
)

graph = dep(vars)
traverse_dependency_graph(graph, f)
source
PlantSimEngine.variables_multiscaleFunction
variables_multiscale(node, organ, mapping, st=NamedTuple())

Get the variables of a HardDependencyNode, taking into account the multiscale mapping, i.e. defining variables as MappedVar if they are mapped to another scale. The default values are taken from the model if not given by the user (st), and are marked as UninitializedVar if they are inputs of the node.

Return a NamedTuple with the variables and their default values.

Arguments

  • node::HardDependencyNode: the node to get the variables from.
  • organ::String: the organ type, e.g. "Leaf".
  • vars_mapping::Dict{String,T}: the mapping of the models (see details below).
  • st::NamedTuple: an optional named tuple with default values for the variables.

Details

The vars_mapping is a dictionary with the organ type as key and a dictionary as value. It is computed from the user mapping like so:

source
PlantSimEngine.variables_outputs_from_other_scaleMethod
variables_outputs_from_other_scale(mapped_vars)

For each organ in the mapped_vars, find the variables that are outputs from another scale and not computed at this scale otherwise. This function is used with mapped_variables

source
PlantSimEngine.variables_typedMethod
variables_typed(model)
variables_typed(model, models...)

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

Examples

using PlantSimEngine;

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

PlantSimEngine.variables_typed(Process1Model(1.0))
(var1 = Float64, var2 = Float64, var3 = Float64)

PlantSimEngine.variables_typed(Process1Model(1.0), Process2Model())

# output
(var4 = Float64, var5 = Float64, var1 = Float64, var2 = Float64, var3 = Float64)

See also

inputs, outputs and variables

source