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 } \n end\n\n "
125+ eval (Meta. parse (struct_decl))
126+
127+ inputs_decl:: String = " function PlantSimEngine.inputs_(::$model_name )\n (current_timestep=1,)\n end\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 ,)\n end\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)\n status.$symbol = m.$var_vector [status.current_timestep]\n end\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
0 commit comments