Skip to content

Commit 2465f60

Browse files
authored
Merge pull request #83 from VirtualPlantLab/hard-dependency-just-from-dep
Multiscale hard dependencies
2 parents 55f587a + bf567d6 commit 2465f60

12 files changed

Lines changed: 408 additions & 84 deletions

docs/src/model_coupling/model_coupling_modeler.md

Lines changed: 112 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,4 +59,115 @@ PlantSimEngine.dep(::Process2Model) = (process1=Process1Model,)
5959

6060
## Soft coupling
6161

62-
A model that takes outputs of another model as inputs is called a soft-coupled model. There is nothing to do on the modeler side to declare a soft-dependency. The detection is done automatically by PlantSimEngine using the inputs and outputs of the models.
62+
A model that takes outputs of another model as inputs is called a soft-coupled model. There is nothing to do on the modeler side to declare a soft-dependency. The detection is done automatically by PlantSimEngine using the inputs and outputs of the models.
63+
64+
## Handling dependencies in a multiscale context
65+
66+
If a model requires some input variable that is computed at another scale, providing the appropriate mapping will resolve name conflicts and enable proper use of that variable and there will be no extra steps for the user or the modeler.
67+
68+
In the case of a hard dependency that operates at a different scale from its parent, the same principle applies and there are also no extra steps on the user-side.
69+
70+
On the other hand, modelers need to bear in mind a couple of subtleties when developing models that possess hard dependencies that operate at a different organ level from their parent :
71+
72+
The parent model directly handles the call to its hard dependency model(s), meaning they are not explicitely managed by the dependency graph.
73+
Therefore only the owning model of that dependency is visible in the graph, and its hard dependency nodes are internal.
74+
75+
When the caller (or any downstream model that requires some variables from the hard dependency) operates at the same scale, variables are easily accessible, and no mapping is required.
76+
77+
If an inner model operates at a different scale/organ level, a modeler must declare hard dependencies with their respective organ level, similarly to the way the user provides a mapping.
78+
79+
Conceptually :
80+
81+
```julia
82+
PlantSimEngine.dep(m::ParentModel) = (
83+
name_provided_in_the_mapping=AbstractHardDependencyModel => ["Organ_Name_1",],
84+
)
85+
```
86+
87+
Here's a concrete example in [XPalm](https://github.com/PalmStudio/XPalm.jl), an oil palm model developed on top of PlantSimEngine.
88+
Organs are produced at the phytomer scale, but need to run an age model and a biomass model at the reproductive organs' scales.
89+
90+
```julia
91+
PlantSimEngine.dep(m::ReproductiveOrganEmission) = (
92+
initiation_age=AbstractInitiation_AgeModel => [m.male_symbol, m.female_symbol],
93+
final_potential_biomass=AbstractFinal_Potential_BiomassModel => [m.male_symbol, m.female_symbol],
94+
)
95+
```
96+
97+
The user-mapping includes the required models at specific organ levels. Here's the relevant portion of the mapping for the male reproductive organ :
98+
99+
```julia
100+
mapping = Dict(
101+
...
102+
"Male" =>
103+
MultiScaleModel(
104+
model=XPalm.InitiationAgeFromPlantAge(),
105+
mapping=[:plant_age => "Plant",],
106+
),
107+
...
108+
XPalm.MaleFinalPotentialBiomass(
109+
p.parameters[:male][:male_max_biomass],
110+
p.parameters[:male][:age_mature_male],
111+
p.parameters[:male][:fraction_biomass_first_male],
112+
),
113+
...
114+
)
115+
```
116+
117+
The model's constructor provides convenient default names for the scale corresponding to the reproductive organs. A user may override that if their naming schemes or MTG attributes differ.
118+
119+
```julia
120+
function ReproductiveOrganEmission(mtg::MultiScaleTreeGraph.Node; phytomer_symbol="Phytomer", male_symbol="Male", female_symbol="Female")
121+
...
122+
end
123+
```
124+
125+
But how does a model M calling a hard dependency H provide H's variables when calling H's `run!` function ? The status the user provides M operates at M's organ level, so if used to call H's run! function any required variable for H will be missing.
126+
127+
PlantSimEngine provides what are called Status Templates in the simulation graph. Each organ level has its own Status template listing the available variables at that scale.
128+
So when a model M calls a hard dependency H's `run!` function, any required variables can be accessed through the status template of H's organ level.
129+
130+
Using the same example in XPalm :
131+
132+
```julia
133+
# Note that the function's 'status' parameter does NOT contain the variables required by the hard dependencies as the calling model's organ level is "Phytomer", not "Male" or "Female"
134+
135+
function PlantSimEngine.run!(m::ReproductiveOrganEmission, models, status, meteo, constants, sim_object)
136+
...
137+
status.graph_node_count += 1
138+
139+
# Create the new organ as a child of the phytomer:
140+
st_repro_organ = add_organ!(
141+
status.node[1], # The phytomer's internode is its first child
142+
sim_object, # The simulation object, so we can add the new status
143+
"+", status.sex, 4;
144+
index=status.phytomer_count,
145+
id=status.graph_node_count,
146+
attributes=Dict{Symbol,Any}()
147+
)
148+
149+
# Compute the initiation age of the organ:
150+
PlantSimEngine.run!(sim_object.models[status.sex].initiation_age, sim_object.models[status.sex], st_repro_organ, meteo, constants, sim_object)
151+
PlantSimEngine.run!(sim_object.models[status.sex].final_potential_biomass, sim_object.models[status.sex], st_repro_organ, meteo, constants, sim_object)
152+
end
153+
```
154+
155+
In the above example the organ and its status template are created on the fly.
156+
When that isn't the case, the status template can be accessed through the simulation graph :
157+
158+
```julia
159+
function PlantSimEngine.run!(m::ReproductiveOrganEmission, models, status, meteo, constants, sim_object)
160+
161+
...
162+
163+
if status.sex == "Male"
164+
165+
status_male = sim_object.statuses["Male"][1]
166+
run!(sim_object.models["Male"].initiation_age, models, status_male, meteo, constants, sim_object)
167+
run!(sim_object.models["Male"].final_potential_biomass, models, status_male, meteo, constants, sim_object)
168+
else
169+
# Female
170+
...
171+
end
172+
end
173+
```

src/dependencies/dependencies.jl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,12 +88,12 @@ function dep(mapping::Dict{String,T}; verbose::Bool=true) where {T}
8888
# First step, get the hard-dependency graph and create SoftDependencyNodes for each hard-dependency root. In other word, we want
8989
# only the nodes that are not hard-dependency of other nodes. These nodes are taken as roots for the soft-dependency graph because they
9090
# are independant.
91-
soft_dep_graphs_roots = hard_dependencies(mapping; verbose=verbose)
91+
soft_dep_graphs_roots, hard_dep_dict = hard_dependencies(mapping; verbose=verbose)
9292
# Second step, compute the soft-dependency graph between SoftDependencyNodes computed in the first step. To do so, we search the
9393
# inputs of each process into the outputs of the other processes, at the same scale, but also between scales. Then we keep only the
9494
# nodes that have no soft-dependencies, and we set them as root nodes of the soft-dependency graph. The other nodes are set as children
9595
# of the nodes that they depend on.
96-
dep_graph = soft_dependencies_multiscale(soft_dep_graphs_roots, mapping)
96+
dep_graph = soft_dependencies_multiscale(soft_dep_graphs_roots, mapping, hard_dep_dict)
9797
# During the building of the soft-dependency graph, we identified the inputs and outputs of each dependency node,
9898
# and also defined **inputs** as MappedVar if they are multiscale, i.e. if they take their values from another scale.
9999
# What we are missing is that we need to also define **outputs** as multiscale if they are needed by another scale.

src/dependencies/dependency_graph.jl

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ mutable struct HardDependencyNode{T} <: AbstractDependencyNode
88
scale::String
99
inputs
1010
outputs
11-
parent::Union{Nothing,HardDependencyNode}
11+
parent::Union{Nothing,<:AbstractDependencyNode}
1212
children::Vector{HardDependencyNode}
1313
end
1414

@@ -39,9 +39,9 @@ A graph of dependencies between models.
3939
- `roots::T`: the root nodes of the graph.
4040
- `not_found::Dict{Symbol,DataType}`: the models that were not found in the graph.
4141
"""
42-
struct DependencyGraph{T}
42+
struct DependencyGraph{T,N}
4343
roots::T
44-
not_found::Dict{Symbol,DataType}
44+
not_found::Dict{Symbol,N}
4545
end
4646

4747
# Add methods to check if a node is parallelizable:

src/dependencies/get_model_in_dependency_graph.jl

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,16 @@ function get_model_nodes(dep_graph::DependencyGraph, model)
2828
end
2929

3030
return model_node
31+
end
32+
33+
function get_model_nodes(dep_graph::DependencyGraph, process::Symbol)
34+
process_node = Union{SoftDependencyNode,HardDependencyNode}[]
35+
36+
traverse_dependency_graph!(dep_graph) do node
37+
if node.process == process
38+
push!(process_node, node)
39+
end
40+
end
41+
42+
return process_node
3143
end

0 commit comments

Comments
 (0)