Skip to content

Commit da65ff7

Browse files
committed
Fix little issues + attempt at addressing main blocker : disconnect timestep-mapped variables from their source, to avoid overwriting source (inputs to non-default-timestep models are changed by the accumulation function so can't be a simple Ref to source)
1 parent 2d343f6 commit da65ff7

9 files changed

Lines changed: 249 additions & 150 deletions

File tree

src/dependencies/dependency_graph.jl

Lines changed: 0 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -89,82 +89,4 @@ function Base.show(io::IO, ::MIME"text/plain", t::DependencyGraph)
8989
else
9090
draw_dependency_graph(io, t)
9191
end
92-
end
93-
94-
"""
95-
variables_multiscale(node, organ, mapping, st=NamedTuple())
96-
97-
Get the variables of a HardDependencyNode, taking into account the multiscale mapping, *i.e.*
98-
defining variables as `MappedVar` if they are mapped to another scale. The default values are
99-
taken from the model if not given by the user (`st`), and are marked as `UninitializedVar` if
100-
they are inputs of the node.
101-
102-
Return a NamedTuple with the variables and their default values.
103-
104-
# Arguments
105-
106-
- `node::HardDependencyNode`: the node to get the variables from.
107-
- `organ::String`: the organ type, *e.g.* "Leaf".
108-
- `vars_mapping::Dict{String,T}`: the mapping of the models (see details below).
109-
- `st::NamedTuple`: an optional named tuple with default values for the variables.
110-
111-
# Details
112-
113-
The `vars_mapping` is a dictionary with the organ type as key and a dictionary as value. It is
114-
computed from the user mapping like so:
115-
"""
116-
function variables_multiscale(node, organ, vars_mapping, st=NamedTuple())
117-
node_vars = variables(node) # e.g. (inputs = (:var1=-Inf, :var2=-Inf), outputs = (:var3=-Inf,))
118-
ins = node_vars.inputs
119-
ins_variables = keys(ins)
120-
outs_variables = keys(node_vars.outputs)
121-
defaults = merge(node_vars...)
122-
map((inputs=ins_variables, outputs=outs_variables)) do vars # Map over vars from :inputs and vars from :outputs
123-
vars_ = Vector{Pair{Symbol,Any}}()
124-
for var in vars # e.g. var = :carbon_biomass
125-
if var in keys(st)
126-
#If the user has given a status, we use it as default value.
127-
default = st[var]
128-
elseif var in ins_variables
129-
# Otherwise, we use the default value given by the model:
130-
# If the variable is an input, we mark it as uninitialized:
131-
default = UninitializedVar(var, defaults[var])
132-
else
133-
# If the variable is an output, we use the default value given by the model:
134-
default = defaults[var]
135-
end
136-
137-
if haskey(vars_mapping[organ], var)
138-
organ_mapped, organ_mapped_var = _node_mapping(vars_mapping[organ][var])
139-
push!(vars_, var => MappedVar(organ_mapped, var, organ_mapped_var, default))
140-
#* We still check if the variable also exists wrapped in PreviousTimeStep, because one model could use the current
141-
#* values, and another one the previous values.
142-
if haskey(vars_mapping[organ], PreviousTimeStep(var, node.process))
143-
organ_mapped, organ_mapped_var = _node_mapping(vars_mapping[organ][PreviousTimeStep(var, node.process)])
144-
push!(vars_, var => MappedVar(organ_mapped, PreviousTimeStep(var, node.process), organ_mapped_var, default))
145-
end
146-
elseif haskey(vars_mapping[organ], PreviousTimeStep(var, node.process))
147-
# If not found in the current time step, we check if the variable is mapped to the previous time step:
148-
organ_mapped, organ_mapped_var = _node_mapping(vars_mapping[organ][PreviousTimeStep(var, node.process)])
149-
push!(vars_, var => MappedVar(organ_mapped, PreviousTimeStep(var, node.process), organ_mapped_var, default))
150-
else
151-
# Else we take the default value:
152-
push!(vars_, var => default)
153-
end
154-
end
155-
return (; vars_...,)
156-
end
157-
end
158-
159-
function _node_mapping(var_mapping::Pair{String,Symbol})
160-
# One organ is mapped to the variable:
161-
return SingleNodeMapping(first(var_mapping)), last(var_mapping)
162-
end
163-
164-
function _node_mapping(var_mapping)
165-
# Several organs are mapped to the variable:
166-
organ_mapped = MultiNodeMapping([first(i) for i in var_mapping])
167-
organ_mapped_var = [last(i) for i in var_mapping]
168-
169-
return organ_mapped, organ_mapped_var
17092
end

src/dependencies/hard_dependencies.jl

Lines changed: 87 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,92 @@ function initialise_all_as_hard_dependency_node(models, scale)
110110
return dep_graph
111111
end
112112

113+
# Samuel : this requires the orchestrator, which requires the dependency graph
114+
# Leaving it in dependency_graph.jl causes forward declaration issues, moving it here as a quick protoyping hack, it might not be the ideal spot
115+
"""
116+
variables_multiscale(node, organ, mapping, st=NamedTuple())
117+
118+
Get the variables of a HardDependencyNode, taking into account the multiscale mapping, *i.e.*
119+
defining variables as `MappedVar` if they are mapped to another scale. The default values are
120+
taken from the model if not given by the user (`st`), and are marked as `UninitializedVar` if
121+
they are inputs of the node.
122+
123+
Return a NamedTuple with the variables and their default values.
124+
125+
# Arguments
126+
127+
- `node::HardDependencyNode`: the node to get the variables from.
128+
- `organ::String`: the organ type, *e.g.* "Leaf".
129+
- `vars_mapping::Dict{String,T}`: the mapping of the models (see details below).
130+
- `st::NamedTuple`: an optional named tuple with default values for the variables.
131+
132+
# Details
133+
134+
The `vars_mapping` is a dictionary with the organ type as key and a dictionary as value. It is
135+
computed from the user mapping like so:
136+
"""
137+
function variables_multiscale(node, organ, vars_mapping, st=NamedTuple(), orchestrator::Orchestrator2=Orchestrator2())
138+
node_vars = variables(node) # e.g. (inputs = (:var1=-Inf, :var2=-Inf), outputs = (:var3=-Inf,))
139+
ins = node_vars.inputs
140+
ins_variables = keys(ins)
141+
outs_variables = keys(node_vars.outputs)
142+
defaults = merge(node_vars...)
143+
map((inputs=ins_variables, outputs=outs_variables)) do vars # Map over vars from :inputs and vars from :outputs
144+
vars_ = Vector{Pair{Symbol,Any}}()
145+
for var in vars # e.g. var = :carbon_biomass
146+
if var in keys(st)
147+
#If the user has given a status, we use it as default value.
148+
default = st[var]
149+
elseif var in ins_variables
150+
# Otherwise, we use the default value given by the model:
151+
# If the variable is an input, we mark it as uninitialized:
152+
default = UninitializedVar(var, defaults[var])
153+
else
154+
# If the variable is an output, we use the default value given by the model:
155+
default = defaults[var]
156+
end
157+
158+
# TODO no idea how this meshes with refvector situations or previoustimestep
159+
if is_timestep_mapped(organ => var, orchestrator, search_inputs_only=true)
160+
161+
push!(vars_, var => default)
162+
else
163+
164+
if haskey(vars_mapping[organ], var)
165+
organ_mapped, organ_mapped_var = _node_mapping(vars_mapping[organ][var])
166+
push!(vars_, var => MappedVar(organ_mapped, var, organ_mapped_var, default))
167+
#* We still check if the variable also exists wrapped in PreviousTimeStep, because one model could use the current
168+
#* values, and another one the previous values.
169+
if haskey(vars_mapping[organ], PreviousTimeStep(var, node.process))
170+
organ_mapped, organ_mapped_var = _node_mapping(vars_mapping[organ][PreviousTimeStep(var, node.process)])
171+
push!(vars_, var => MappedVar(organ_mapped, PreviousTimeStep(var, node.process), organ_mapped_var, default))
172+
end
173+
elseif haskey(vars_mapping[organ], PreviousTimeStep(var, node.process))
174+
# If not found in the current time step, we check if the variable is mapped to the previous time step:
175+
organ_mapped, organ_mapped_var = _node_mapping(vars_mapping[organ][PreviousTimeStep(var, node.process)])
176+
push!(vars_, var => MappedVar(organ_mapped, PreviousTimeStep(var, node.process), organ_mapped_var, default))
177+
else
178+
# Else we take the default value:
179+
push!(vars_, var => default)
180+
end
181+
end
182+
end
183+
return (; vars_...,)
184+
end
185+
end
186+
187+
function _node_mapping(var_mapping::Pair{String,Symbol})
188+
# One organ is mapped to the variable:
189+
return SingleNodeMapping(first(var_mapping)), last(var_mapping)
190+
end
191+
192+
function _node_mapping(var_mapping)
193+
# Several organs are mapped to the variable:
194+
organ_mapped = MultiNodeMapping([first(i) for i in var_mapping])
195+
organ_mapped_var = [last(i) for i in var_mapping]
196+
197+
return organ_mapped, organ_mapped_var
198+
end
113199

114200
# When we use a mapping (multiscale), we return the set of soft-dependencies (we put the hard-dependencies as their children):
115201
function hard_dependencies(mapping::Dict{String,T}; verbose::Bool=true, orchestrator::Orchestrator2=Orchestrator2()) where {T}
@@ -148,7 +234,7 @@ function hard_dependencies(mapping::Dict{String,T}; verbose::Bool=true, orchestr
148234
status_scale = Dict{Symbol,Vector{Pair{Symbol,NamedTuple}}}()
149235
for (procname, node) in hard_deps[organ].roots # procname = :leaf_surface ; node = hard_deps.roots[procname]
150236
var = Pair{Symbol,NamedTuple}[]
151-
traverse_dependency_graph!(node, x -> variables_multiscale(x, organ, full_vars_mapping, st_scale_user), var)
237+
traverse_dependency_graph!(node, x -> variables_multiscale(x, organ, full_vars_mapping, st_scale_user, orchestrator), var)
152238
push!(status_scale, procname => var)
153239
end
154240

src/dependencies/soft_dependencies.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -357,7 +357,7 @@ function add_timestep_data_to_node(soft_dependency_node, orchestrator::Orchestra
357357
# now we can create the mapping
358358
for mtsm in orchestrator.non_default_timestep_mapping
359359
if mtsm.scale == soft_dependency_node.scale && (mtsm.model) == typeof(model_(soft_dependency_node.value))
360-
for (var_to, var_from) in mtsm.var_to_var
360+
for (var_from, var_to) in mtsm.var_to_var
361361
if !isnothing(soft_dependency_node.parent)
362362
parent = nothing
363363
variable_mapping = nothing

src/mtg/initialisation.jl

Lines changed: 33 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -35,26 +35,31 @@ function init_statuses(mtg, mapping, dependency_graph=first(hard_dependencies(ma
3535
# Note 3: we do it before `convert_reference_values!` because we need the variables to be MappedVar{MultiNodeMapping} to get the reverse mapping.
3636

3737
# Convert the MappedVar{SelfNodeMapping} or MappedVar{SingleNodeMapping} to RefValues, and MappedVar{MultiNodeMapping} to RefVectors:
38-
convert_reference_values!(mapped_vars)
38+
convert_reference_values!(mapped_vars, orchestrator)
3939

4040
# Get the variables that are not initialised or computed by other models in the output:
4141
vars_need_init = Dict(org => filter(x -> isa(last(x), UninitializedVar), vars) |> keys |> collect for (org, vars) in mapped_vars) |>
4242
filter(x -> length(last(x)) > 0)
4343

4444
# Filter out variables that are timestep-mapped by the user,
45-
# as those variables are initialized by another model, but are currently flagged as needing initialization
45+
# Since we disconnect outputs from the source variable (as values are changed by the accumulation function,
46+
# meaning they differ and we can't just Ref point to the source variable)
47+
# they will be currently flagged as needing initialization
4648
# At this stage, data present in the orchestrator is expected to be valid, so we can take it into account
4749
# A model with a different timestep can still have unitialized vars found in a node, the meteo, or to be initialized by the user
4850
# in which case it'll be absent from the timestep mapping, but this needs testing
49-
#filter_timestep_mapped_variables!(vars_need_init, orchestrator)
51+
# TODO, *however*, this isn't the cleanest in its current state,
52+
# there may be some user initialisation issues that are hidden by this approach
53+
# Needs to be checked
54+
filter_timestep_mapped_variables!(vars_need_init, orchestrator)
5055

5156

5257
# Note: these variables may be present in the MTG attributes, we check that below when traversing the MTG.
5358

5459
# We traverse the MTG to initialise the statuses linked to the nodes:
5560
statuses = Dict(i => Status[] for i in collect(keys(mapped_vars)))
5661
MultiScaleTreeGraph.traverse!(mtg) do node # e.g.: node = MultiScaleTreeGraph.get_node(mtg, 5)
57-
init_node_status!(node, statuses, mapped_vars, reverse_multiscale_mapping, vars_need_init, type_promotion, check=check)
62+
init_node_status!(node, statuses, mapped_vars, reverse_multiscale_mapping, vars_need_init, type_promotion, check=check, orchestrator=orchestrator)
5863
end
5964

6065
return (; statuses, mapped_vars, reverse_multiscale_mapping, vars_need_init)
@@ -153,7 +158,7 @@ function init_node_status!(node, statuses, mapped_vars, reverse_multiscale_mappi
153158
end
154159

155160
# Make the node status from the template:
156-
st = status_from_template(st_template)
161+
st = status_from_template(st_template, symbol(node), orchestrator)
157162

158163
push!(statuses[symbol(node)], st)
159164

@@ -209,17 +214,23 @@ julia> b
209214
2.0
210215
```
211216
"""
212-
function status_from_template(d::Dict{Symbol,T} where {T})
217+
function status_from_template(d::Dict{Symbol,T} where {T}, scale::String, orchestrator::Orchestrator2)
213218
# Sort vars to put the values that are of type PerStatusRef at the end (we need the pass on the other ones first):
214219
sorted_vars = Dict{Symbol,Any}(sort([pairs(d)...], by=v -> last(v) isa RefVariable ? 1 : 0))
215220
# Note: PerStatusRef are used to reference variables in the same status for renaming.
216221

217222
# We create the status with the right references for variables, and for PerStatusRef (we reference the reference variable):
218223
for (k, v) in sorted_vars
219-
if isa(v, RefVariable)
220-
sorted_vars[k] = sorted_vars[v.reference_variable]
221-
else
224+
if is_timestep_mapped((scale => k), orchestrator, search_inputs_only=true)
225+
# avoid referring to the original variable
222226
sorted_vars[k] = ref_var(v)
227+
else
228+
229+
if isa(v, RefVariable)
230+
sorted_vars[k] = sorted_vars[v.reference_variable]
231+
else
232+
sorted_vars[k] = ref_var(v)
233+
end
223234
end
224235
end
225236

@@ -423,17 +434,22 @@ function filter_timestep_mapped_variables!(vars_need_init, orchestrator)
423434
end=#
424435

425436
function filter_timestep_mapped_variables!(vars_need_init, orchestrator)
426-
for (org, vars) in vars_need_init
427-
for tmst in orchestrator.non_default_timestep_mapping
437+
for tmst in orchestrator.non_default_timestep_mapping
438+
for (org, vars) in vars_need_init
428439
if tmst.scale == org
429-
430-
for (var_to, var_from) in tmst.var_to_var
431-
filter!(o -> o == var_to.name || o == var_from.name, vars)
432-
end
433-
if isempty(vars)
434-
delete!(vars_need_init, org)
440+
for (var_from, var_to) in tmst.var_to_var
441+
filter!(o -> o == var_to.name, vars)
442+
end
443+
end
444+
for (var_from, var_to) in tmst.var_to_var
445+
if var_from.scale == org
446+
filter!(o -> o == var_from.name, vars)
435447
end
436448
end
449+
450+
if isempty(vars)
451+
delete!(vars_need_init, org)
452+
end
437453
end
438454
end
439455
end

src/mtg/mapping/compute_mapping.jl

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -279,14 +279,26 @@ function default_variables_from_mapping(mapped_vars, verbose=true)
279279
end
280280

281281

282+
function is_timestep_mapped(key, orchestrator::Orchestrator2; search_inputs_only::Bool=false)
283+
for mtsm in orchestrator.non_default_timestep_mapping
284+
for (var_from, var_to) in mtsm.var_to_var
285+
if ((first(key) == mtsm.scale) && last(key) == var_to.name) ||
286+
(!search_inputs_only && (first(key) == var_from.scale && last(key) == var_from.name))
287+
return true
288+
end
289+
end
290+
end
291+
return false
292+
end
293+
282294
"""
283295
convert_reference_values!(mapped_vars::Dict{String,Dict{Symbol,Any}})
284296
285297
Convert the variables that are `MappedVar{SelfNodeMapping}` or `MappedVar{SingleNodeMapping}` to RefValues that reference a
286298
common value for the variable; and convert `MappedVar{MultiNodeMapping}` to RefVectors that reference the values for the
287299
variable in the source organs.
288300
"""
289-
function convert_reference_values!(mapped_vars::Dict{String,Dict{Symbol,Any}})
301+
function convert_reference_values!(mapped_vars::Dict{String,Dict{Symbol,Any}}, orchestrator::Orchestrator2)
290302
# For the variables that will be RefValues, i.e. referencing a value that exists for different scales, we need to first
291303
# create a common reference to the value that we use wherever we need this value. These values are created in the dict_mapped_vars
292304
# Dict, and then referenced from there every time we point to it.
@@ -303,7 +315,11 @@ function convert_reference_values!(mapped_vars::Dict{String,Dict{Symbol,Any}})
303315

304316
# First time we encounter this variable as a MappedVar, we create its value into the dict_mapped_vars Dict:
305317
if !haskey(dict_mapped_vars, key)
306-
push!(dict_mapped_vars, key => Ref(mapped_default(vars[k])))
318+
# if is_timestep_mapped(key, orchestrator)
319+
# push!(dict_mapped_vars, key => (mapped_default(vars[k])))
320+
# else
321+
push!(dict_mapped_vars, key => Ref(mapped_default(vars[k])))
322+
# end
307323
end
308324

309325
# Then we use the value for the particular variable to replace the MappedVar to a RefValue in the mapping:

src/mtg/save_results.jl

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -109,8 +109,8 @@ julia> collect(keys(preallocated_vars["Leaf"]))
109109
:carbon_demand
110110
```
111111
"""
112-
113-
function pre_allocate_outputs(statuses, statuses_template, reverse_multiscale_mapping, vars_need_init, outs, nsteps; type_promotion=nothing, check=true)
112+
# TODO orchestrator prob shouldn't be a kwarg with a default
113+
function pre_allocate_outputs(statuses, statuses_template, reverse_multiscale_mapping, vars_need_init, outs, nsteps; type_promotion=nothing, check=true, orchestrator=Orchestrator2())
114114
outs_ = Dict{String,Vector{Symbol}}()
115115

116116
# default behaviour : track everything
@@ -211,18 +211,18 @@ function pre_allocate_outputs(statuses, statuses_template, reverse_multiscale_ma
211211

212212
# I don't know if this function barrier is necessary
213213
preallocated_outputs = Dict{String,Vector}()
214-
complete_preallocation_from_types!(preallocated_outputs, nsteps, outs_, node_type, statuses_template)
214+
complete_preallocation_from_types!(preallocated_outputs, nsteps, outs_, node_type, statuses_template, orchestrator)
215215
return preallocated_outputs
216216
end
217217

218-
function complete_preallocation_from_types!(preallocated_outputs, nsteps, outs_, node_type, statuses_template)
218+
function complete_preallocation_from_types!(preallocated_outputs, nsteps, outs_, node_type, statuses_template, orchestrator)
219219
types = Vector{DataType}()
220220
for organ in keys(outs_)
221221

222222
outs_no_node = filter(x -> x != :node, outs_[organ])
223223

224224
#types = [typeof(status_from_template(statuses_template[organ])[var]) for var in outs[organ]]
225-
values = [status_from_template(statuses_template[organ])[var] for var in outs_no_node]
225+
values = [status_from_template(statuses_template[organ], organ, orchestrator)[var] for var in outs_no_node]
226226

227227
#push!(types, node_type)
228228

0 commit comments

Comments
 (0)