Skip to content

Commit ac01ac2

Browse files
committed
Integrate relevant changes into PSE. Check pre initialisation vectors are already handled. Extend a couple of tests to convert from modellist to mapping. TODO : multiscale testing
1 parent 1c07e00 commit ac01ac2

8 files changed

Lines changed: 338 additions & 270 deletions

Project.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ FLoops = "cc61a311-1640-44b5-9fba-1b764f453329"
1212
Markdown = "d6f4376e-aef5-505a-96c1-9c027394607a"
1313
MultiScaleTreeGraph = "dd4a991b-8a45-4075-bede-262ee62d5583"
1414
PlantMeteo = "4630fe09-e0fb-4da5-a846-781cb73437b6"
15+
SHA = "ea8e919c-243c-51af-8825-aaa63cd721ce"
1516
Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2"
1617
Tables = "bd369af6-aec1-5ad0-b16a-f7cc5008161c"
1718
Term = "22787eb5-b846-44ae-b979-8e399b8463ab"
@@ -25,6 +26,7 @@ FLoops = "0.2"
2526
Markdown = "1.9"
2627
MultiScaleTreeGraph = "0.13, 0.14"
2728
PlantMeteo = "0.6"
29+
SHA = "0.7.0"
2830
Statistics = "1.9"
2931
Tables = "1"
3032
Term = "1, 2"

src/PlantSimEngine.jl

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ import MultiScaleTreeGraph: symbol, node_id
2222
# To compute mean:
2323
import Statistics
2424

25+
# For avoiding name conflicts when generating models from status vectors
26+
import SHA: sha1
27+
2528
using PlantMeteo
2629

2730
# UninitializedVar + PreviousTimeStep:
@@ -89,6 +92,9 @@ include("run.jl")
8992
# Fitting
9093
include("evaluation/fit.jl")
9194

95+
# Utilities for mapping initialisation
96+
include("mtg/mapping/model_generation_from_status_vectors.jl")
97+
9298
# Examples
9399
include("examples_import.jl")
94100

src/mtg/initialisation.jl

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,10 @@ automatically passed as is.
307307
"""
308308
function init_simulation(mtg, mapping; nsteps=1, outputs=nothing, type_promotion=nothing, check=true, verbose=false)
309309

310+
# Ensure the user called the model generation function to handle vectors passed into a status
311+
# before we keep going
312+
check_statuses_contain_no_remaining_vectors(mapping)
313+
310314
# Get the status of each node by node type, pre-initialised considering multi-scale variables:
311315
statuses, status_templates, reverse_multiscale_mapping, vars_need_init =
312316
init_statuses(mtg, mapping, first(hard_dependencies(mapping; verbose=false)); type_promotion=type_promotion, verbose=verbose, check=check)
Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
1+
# Utilities to handle vectors passed into a mapping's statuses
2+
# This is a convenience when prototyping, not recommended for production or proper fitting
3+
# (Write the actual finalised model explicitely instead)
4+
5+
6+
# The way we generate models from status vectors is to eval() code at runtime.
7+
# A simple custom timestep model provides the correct index to the generated models
8+
# This approach feels a little brittle but works. A (possible ?) improvement would be to directly fiddle with the AST, but it's a little more involved
9+
# Another approach might be to generate a string to be included with include_string, that might avoid awkward global variables and world age problems
10+
11+
# There will still be brittleness given that it's not trivial to handle user/modeler errors :
12+
# For instance, providing a vector that is called in a scale mapping is likely to cause things to go badly
13+
14+
# May need some more complex timestep models in the future
15+
# TODO : unhandled case : what if the timestep models are already in the provided modellist ?
16+
17+
# These models might be worth exposing in the future ?
18+
PlantSimEngine.@process "basic_current_timestep" verbose = false
19+
20+
struct HelperCurrentTimestepModel <: AbstractBasic_Current_TimestepModel
21+
end
22+
23+
PlantSimEngine.inputs_(::HelperCurrentTimestepModel) = (next_timestep=1,)
24+
PlantSimEngine.outputs_(m::HelperCurrentTimestepModel) = (current_timestep=1,)
25+
26+
function PlantSimEngine.run!(m::HelperCurrentTimestepModel, models, status, meteo, constants=nothing, extra=nothing)
27+
status.current_timestep = status.next_timestep
28+
end
29+
30+
PlantSimEngine.ObjectDependencyTrait(::Type{<:HelperCurrentTimestepModel}) = PlantSimEngine.IsObjectDependent()
31+
PlantSimEngine.TimeStepDependencyTrait(::Type{<:HelperCurrentTimestepModel}) = PlantSimEngine.IsTimeStepDependent()
32+
33+
PlantSimEngine.@process "basic_next_timestep" verbose = false
34+
struct HelperNextTimestepModel <: AbstractBasic_Next_TimestepModel
35+
end
36+
37+
PlantSimEngine.inputs_(::HelperNextTimestepModel) = (current_timestep=1,)
38+
PlantSimEngine.outputs_(m::HelperNextTimestepModel) = (next_timestep=1,)
39+
40+
function PlantSimEngine.run!(m::HelperNextTimestepModel, models, status, meteo, constants=nothing, extra=nothing)
41+
status.next_timestep = status.current_timestep + 1
42+
end
43+
44+
PlantSimEngine.ObjectDependencyTrait(::Type{<:HelperNextTimestepModel}) = PlantSimEngine.IsObjectDependent()
45+
PlantSimEngine.TimeStepDependencyTrait(::Type{<:HelperNextTimestepModel}) = PlantSimEngine.IsTimeStepDependent()
46+
47+
48+
# TODO should the new_status be copied ?
49+
# Note : User specifies at which level they want the basic timestep model to be inserted at, as well as the meteo length
50+
function replace_mapping_status_vectors_with_generated_models(mapping_with_vectors_in_status, timestep_model_organ_level, nsteps)
51+
52+
mapping = Dict(organ => models for (organ, models) in mapping_with_vectors_in_status)
53+
for (organ,models) in mapping
54+
for status in models
55+
if isa(status, Status)
56+
new_status, generated_models = generate_model_from_status_vector_variable(mapping, timestep_scale, status, organ, nsteps)
57+
58+
if length(generated_models) > 0
59+
if organ == timestep_model_organ_level
60+
mapping[organ] = (
61+
models...,
62+
generated_models...,
63+
HelperNextTimestepModel(),
64+
MultiScaleModel(
65+
model=HelperCurrentTimestepModel(),
66+
mapping=[PreviousTimeStep(:next_timestep),],
67+
),
68+
new_status,)
69+
else
70+
mapping[organ] = (
71+
models...,
72+
generated_models...,
73+
new_status,)
74+
end
75+
end
76+
end
77+
end
78+
end
79+
80+
return mapping
81+
end
82+
83+
# Note : eval works in global scope, and state synchronisation doesn't occur until one returns to top-level
84+
# This is to enable optimisations. See 'world-age problem'. The doc for eval currently isn't detailed enough.
85+
# Essentially, generating a struct with a process_ method and then immediately creating a simulation graph
86+
# that calls process_ will fail as it won't yet be defined since state hasn't synchronised.
87+
# Returning a new mapping to top-level and *then* creating the graph will work.
88+
# The fact that eval works in global scope is also why we make use of some global variables here
89+
function generate_model_from_status_vector_variable(mapping, timestep_scale, status, organ, nsteps)
90+
91+
# Note : 534f1c161f91bb346feba1a84a55e8251f5ad446 is a prefix to reduce likelihood of global variable name conflicts
92+
# it is the hash generated by bytes2hex(sha1("PlantSimEngine_prototype"))
93+
# If this function is hard to read, copy it into a temporary file and remove the hash suffix
94+
95+
# Ah, another point that remains to be seen is that those CSV.SentinelArrays.ChainedVector obtained from the meteo file isn't an AbstractVector
96+
# meaning currently we won't generate models from them unless the conversion is made before that
97+
# So another minor potential improvement would be to return a warning to the user and do the conversion when generating the model
98+
# See the test code in test-mapping.jl : cumsum(meteo_day.TT) returns such a data structure
99+
100+
global generated_models_534f1c161f91bb346feba1a84a55e8251f5ad446 = ()
101+
global new_status_534f1c161f91bb346feba1a84a55e8251f5ad446 = NamedTuple()
102+
103+
for symbol in keys(status)
104+
global value_534f1c161f91bb346feba1a84a55e8251f5ad446 = getproperty(status, symbol)
105+
if isa(value_534f1c161f91bb346feba1a84a55e8251f5ad446, AbstractVector)
106+
@assert length(value_534f1c161f91bb346feba1a84a55e8251f5ad446) > 0 "Error during generation of models from vector values provided at the $organ-level status : provided $symbol vector is empty"
107+
# TODO : Might need to fiddle with timesteps here in the future in case of varying timestep models
108+
@assert nsteps == length(value_534f1c161f91bb346feba1a84a55e8251f5ad446) "Error during generation of models from vector values provided at the $organ-level status : provided $symbol vector length doesn't match the expected # of timesteps"
109+
var_type = eltype(value_534f1c161f91bb346feba1a84a55e8251f5ad446)
110+
base_name = string(symbol) * bytes2hex(sha1(join(value_534f1c161f91bb346feba1a84a55e8251f5ad446)))
111+
process_name = lowercase(base_name)
112+
113+
var_titlecase::String = titlecase(base_name)
114+
model_name = "My$(var_titlecase)Model"
115+
process_abstract_name = "Abstract$(var_titlecase)Model"
116+
var_vector = "$(symbol)_vector"
117+
118+
abstract_process_decl = "abstract type $process_abstract_name <: PlantSimEngine.AbstractModel end"
119+
eval(Meta.parse(abstract_process_decl))
120+
121+
process_name_decl = "PlantSimEngine.process_(::Type{$process_abstract_name}) = :$process_name"
122+
eval(Meta.parse(process_name_decl))
123+
124+
struct_decl::String = "struct $model_name <: $process_abstract_name \n$var_vector::Vector{$var_type} \nend\n\n"
125+
eval(Meta.parse(struct_decl))
126+
127+
inputs_decl::String = "function PlantSimEngine.inputs_(::$model_name)\n(current_timestep=1,)\nend\n\n"
128+
eval(Meta.parse(inputs_decl))
129+
130+
default_value = value_534f1c161f91bb346feba1a84a55e8251f5ad446[1]
131+
outputs_decl::String = "function PlantSimEngine.outputs_(::$model_name)\n($symbol=$default_value,)\nend\n\n"
132+
eval(Meta.parse(outputs_decl))
133+
134+
constructor_decl = "$model_name(; $var_vector = Vector{$var_type}()) = $model_name($var_vector)\n\n"
135+
eval(Meta.parse(constructor_decl))
136+
137+
run_decl = "function PlantSimEngine.run!(m::$model_name, models, status, meteo, constants=nothing, extra_args=nothing)\nstatus.$symbol = m.$var_vector[status.current_timestep]\nend\n\n"
138+
eval(Meta.parse(run_decl))
139+
140+
# add name to vector of models
141+
if timestep_scale != organ
142+
mapping_decl = "mapping[\"($organ)\"] = MultiScaleModel($process_name($var_vector), mapping=\"($timestep_scale)\" => (:current_timestep,))"
143+
eval(Meta.parse(mapping_decl))
144+
else
145+
end
146+
147+
model_add_decl = "generated_models_534f1c161f91bb346feba1a84a55e8251f5ad446 = (generated_models_534f1c161f91bb346feba1a84a55e8251f5ad446..., $model_name($var_vector=$value_534f1c161f91bb346feba1a84a55e8251f5ad446),)"
148+
eval(Meta.parse(model_add_decl))
149+
else
150+
new_status_decl = "new_status_534f1c161f91bb346feba1a84a55e8251f5ad446 = Status(; NamedTuple(new_status_534f1c161f91bb346feba1a84a55e8251f5ad446)..., $symbol=$value_534f1c161f91bb346feba1a84a55e8251f5ad446)"
151+
eval(Meta.parse(new_status_decl))
152+
end
153+
end
154+
155+
@assert length(status) == length(new_status_534f1c161f91bb346feba1a84a55e8251f5ad446) + length(generated_models_534f1c161f91bb346feba1a84a55e8251f5ad446) "Error during generation of models from vector values provided at the $organ-level status"
156+
return new_status_534f1c161f91bb346feba1a84a55e8251f5ad446, generated_models_534f1c161f91bb346feba1a84a55e8251f5ad446
157+
end
158+
159+
160+
# This is a helper function only for testing purposes, but it makes sense to include it here since it calls
161+
# generate_model_from_status_vector_variable, which has those awkward global variables
162+
function modellist_to_mapping(modellist_original::ModelList, modellist_status; nsteps=nothing, outputs=nothing)
163+
164+
modellist = Base.copy(modellist_original, modellist_original.status)
165+
166+
default_scale = "Default"
167+
mtg = MultiScaleTreeGraph.Node(MultiScaleTreeGraph.NodeMTG("/", default_scale, 0, 0),)
168+
169+
models = modellist.models
170+
171+
mapping_incomplete = Dict(
172+
default_scale => (
173+
models...,
174+
MultiScaleModel(
175+
model=HelperCurrentTimestepModel(),
176+
mapping=[PreviousTimeStep(:next_timestep),],
177+
),
178+
Status((modellist_status..., current_timestep=1,next_timestep=1,))
179+
),
180+
)
181+
182+
timestep_scale = "Default"
183+
organ = "Default"
184+
185+
# todo improve on this
186+
st = (last(mapping_incomplete["Default"]))
187+
new_status, generated_models = generate_model_from_status_vector_variable(mapping_incomplete, timestep_scale, st, organ, nsteps)
188+
189+
mapping = Dict(default_scale => (
190+
models..., generated_models...,
191+
HelperNextTimestepModel(),
192+
MultiScaleModel(
193+
model=HelperCurrentTimestepModel(),
194+
mapping=[PreviousTimeStep(:next_timestep),],
195+
),
196+
new_status,
197+
),
198+
)
199+
200+
if isnothing(outputs)
201+
f = []
202+
for i in 1:length(modellist.models)
203+
aa = init_variables(modellist.models[i])
204+
bb = keys(aa)
205+
for j in 1:length(bb)
206+
push!(f, bb[j])
207+
end
208+
#f = (f..., bb...)
209+
end
210+
211+
f = unique!(f)
212+
all_vars = (f...,)
213+
#all_vars = merge((keys(init_variables(object.models[i])) for i in 1:length(object.models))...)
214+
else
215+
all_vars = outputs
216+
# TODO sanity check
217+
end
218+
219+
return mtg, mapping, Dict(default_scale => all_vars)
220+
end
221+
222+
function check_statuses_contain_no_remaining_vectors(mapping)
223+
for (organ,models) in mapping
224+
225+
# Special case (scales that map to a single-model don't need to be declared as a tuple for user-convenience)
226+
if isa(models, AbstractModel) || isa(models, MultiScaleModel)
227+
continue
228+
end
229+
230+
for status in models
231+
if isa(status, Status)
232+
for symbol in keys(status)
233+
value = getproperty(status, symbol)
234+
@assert !isa(value, AbstractVector) "Error : Mapping status at $organ level contains a vector. If this was intentional, call the function generate_models_from_status_vectors on your mapping before calling run!. And bear in mind this is not meant for production. If this wasn't intentional, then it's likely an issue on the mapping definition, or an unusual model."
235+
end
236+
end
237+
end
238+
end
239+
return true
240+
end

test/helper-functions.jl

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# Simple helper functions that can be used in various tests here and there
2+
3+
4+
function compare_outputs_modellist_mapping(models, graphsim)
5+
graphsim_df = outputs(graphsim, DataFrame)
6+
7+
graphsim_df_outputs_only = select(graphsim_df, Not([:timestep, :organ, :node]))
8+
models_df = DataFrame(status(models))
9+
10+
models_df_sorted = models_df[:, sortperm(names(models_df))]
11+
graphsim_df_outputs_only_sorted = graphsim_df_outputs_only[:, sortperm(names(graphsim_df_outputs_only))]
12+
return graphsim_df_outputs_only_sorted == models_df_sorted
13+
end
14+
15+
# doesn't check for mtg equality
16+
function compare_outputs_graphsim(graphsim, graphsim2)
17+
graphsim_df = outputs(graphsim, DataFrame)
18+
graphsim_df_sorted = graphsim_df[:, sortperm(names(graphsim_df))]
19+
20+
graphsim2_df = outputs(graphsim2, DataFrame)
21+
graphsim2_df_sorted = graphsim2_df[:, sortperm(names(graphsim2_df))]
22+
return graphsim_df_sorted == graphsim2_df_sorted
23+
end
24+
25+
# Breaking this function into two to ensure eval() state synchronisation happens (see comments around the modellist_to_mapping definition)
26+
# Naming could be better
27+
function check_multiscale_simulation_is_equivalent_begin(models::ModelList, status, meteo)
28+
29+
mtg, mapping, out = PlantSimEngine.modellist_to_mapping(models, status; nsteps=length(meteo), outputs=nothing)
30+
return mtg, mapping, out
31+
end
32+
33+
function check_multiscale_simulation_is_equivalent_end(models::ModelList, mtg, mapping, out, meteo)
34+
graph_sim = PlantSimEngine.GraphSimulation(mtg, mapping, nsteps=length(meteo), check=true, outputs=out)
35+
36+
sim = run!(graph_sim,
37+
meteo,
38+
PlantMeteo.Constants(),
39+
nothing;
40+
check=true,
41+
executor=SequentialEx()
42+
);
43+
44+
return compare_outputs_modellist_mapping(models, graph_sim)
45+
end

test/runtests.jl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ using MultiScaleTreeGraph
77
using PlantMeteo, Statistics
88
using Documenter # for doctests
99

10+
include("helper-functions.jl")
11+
1012
@testset "Testing PlantSimEngine" begin
1113
Aqua.test_all(PlantSimEngine, ambiguities=false)
1214
Aqua.test_ambiguities([PlantSimEngine])

0 commit comments

Comments
 (0)