Skip to content

Commit 81d2572

Browse files
committed
Add MultiScale test with status vectors, and fix issues that popped up... Make the function that checks whether vectors are still in mapping statuses return organ + boolean instead of asserting
1 parent 5fbae42 commit 81d2572

3 files changed

Lines changed: 124 additions & 35 deletions

File tree

src/mtg/initialisation.jl

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -309,7 +309,10 @@ function init_simulation(mtg, mapping; nsteps=1, outputs=nothing, type_promotion
309309

310310
# Ensure the user called the model generation function to handle vectors passed into a status
311311
# before we keep going
312-
check_statuses_contain_no_remaining_vectors(mapping)
312+
(organ_with_vector, no_vectors_found) = (check_statuses_contain_no_remaining_vectors(mapping))
313+
if !no_vectors_found
314+
@assert false "Error : Mapping status at $organ_with_vector 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."
315+
end
313316

314317
# Get the status of each node by node type, pre-initialised considering multi-scale variables:
315318
statuses, status_templates, reverse_multiscale_mapping, vars_need_init =

src/mtg/mapping/model_generation_from_status_vectors.jl

Lines changed: 53 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -49,30 +49,54 @@ PlantSimEngine.TimeStepDependencyTrait(::Type{<:HelperNextTimestepModel}) = Plan
4949
# Note : User specifies at which level they want the basic timestep model to be inserted at, as well as the meteo length
5050
function replace_mapping_status_vectors_with_generated_models(mapping_with_vectors_in_status, timestep_model_organ_level, nsteps)
5151

52+
(organ, check) = check_statuses_contain_no_remaining_vectors(mapping_with_vectors_in_status)
53+
if check
54+
@warn "No vectors, or types deriving from AbstractVector found in statuses, returning mapping as is."
55+
return mapping_with_vectors_in_status
56+
end
57+
58+
# we are now certain a model will be generated, and that the timestep models need to be inserted
5259
mapping = Dict(organ => models for (organ, models) in mapping_with_vectors_in_status)
5360
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)
61+
for status in models
62+
if isa(status, Status)
63+
# Generate models and remove corresponding vectors from status
64+
new_status, generated_models = generate_model_from_status_vector_variable(mapping, timestep_model_organ_level, status, organ, nsteps)
5765

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
66+
# Avoid inserting empty named tuples into the mapping
67+
models_and_new_status = [model for model in models if !isa(model, Status)]
68+
if length(new_status) != 0
69+
models_and_new_status = [models_and_new_status..., new_status]
7570
end
71+
72+
# The timestep models might be inserted elsewhere in the mapping, handle various cases
73+
if length(generated_models) > 0
74+
mapping[organ] = (
75+
generated_models...,
76+
models_and_new_status...,)
77+
end
78+
end
79+
end
80+
81+
# insert timestep models wherever they're required
82+
if organ == timestep_model_organ_level
83+
# mapping at a given level can be a tuple or a single model
84+
if isa(mapping[organ], AbstractModel) || isa(mapping[organ], MultiScaleModel)
85+
mapping[organ] = (
86+
HelperNextTimestepModel(),
87+
MultiScaleModel(
88+
model=HelperCurrentTimestepModel(),
89+
mapping=[PreviousTimeStep(:next_timestep),],
90+
),
91+
mapping[organ], )
92+
else
93+
mapping[organ] = (
94+
HelperNextTimestepModel(),
95+
MultiScaleModel(
96+
model=HelperCurrentTimestepModel(),
97+
mapping=[PreviousTimeStep(:next_timestep),],
98+
),
99+
mapping[organ]..., )
76100
end
77101
end
78102
end
@@ -98,7 +122,7 @@ function generate_model_from_status_vector_variable(mapping, timestep_scale, sta
98122
# See the test code in test-mapping.jl : cumsum(meteo_day.TT) returns such a data structure
99123

100124
global generated_models_534f1c161f91bb346feba1a84a55e8251f5ad446 = ()
101-
global new_status_534f1c161f91bb346feba1a84a55e8251f5ad446 = NamedTuple()
125+
global new_status_534f1c161f91bb346feba1a84a55e8251f5ad446 = Status(NamedTuple())
102126

103127
for symbol in keys(status)
104128
global value_534f1c161f91bb346feba1a84a55e8251f5ad446 = getproperty(status, symbol)
@@ -137,14 +161,13 @@ function generate_model_from_status_vector_variable(mapping, timestep_scale, sta
137161
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"
138162
eval(Meta.parse(run_decl))
139163

140-
# add name to vector of models
164+
model_add_decl = "generated_models_534f1c161f91bb346feba1a84a55e8251f5ad446 = (generated_models_534f1c161f91bb346feba1a84a55e8251f5ad446..., $model_name($var_vector=$value_534f1c161f91bb346feba1a84a55e8251f5ad446),)"
165+
166+
# if :current_timestep is not in the same scale
141167
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
168+
model_add_decl = "generated_models_534f1c161f91bb346feba1a84a55e8251f5ad446 = (generated_models_534f1c161f91bb346feba1a84a55e8251f5ad446..., MultiScaleModel(model=$model_name($value_534f1c161f91bb346feba1a84a55e8251f5ad446), mapping=[:current_timestep=>\"$timestep_scale\"],),)"
145169
end
146170

147-
model_add_decl = "generated_models_534f1c161f91bb346feba1a84a55e8251f5ad446 = (generated_models_534f1c161f91bb346feba1a84a55e8251f5ad446..., $model_name($var_vector=$value_534f1c161f91bb346feba1a84a55e8251f5ad446),)"
148171
eval(Meta.parse(model_add_decl))
149172
else
150173
new_status_decl = "new_status_534f1c161f91bb346feba1a84a55e8251f5ad446 = Status(; NamedTuple(new_status_534f1c161f91bb346feba1a84a55e8251f5ad446)..., $symbol=$value_534f1c161f91bb346feba1a84a55e8251f5ad446)"
@@ -231,10 +254,12 @@ function check_statuses_contain_no_remaining_vectors(mapping)
231254
if isa(status, Status)
232255
for symbol in keys(status)
233256
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."
257+
if isa(value, AbstractVector)
258+
return (organ, false)
259+
end
235260
end
236261
end
237262
end
238263
end
239-
return true
264+
return ("", true)
240265
end

test/test-mapping.jl

Lines changed: 67 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,9 @@ end
7777
),
7878
)
7979

80-
@test_throws "call the function generate_models_from_status_vectors" PlantSimEngine.check_statuses_contain_no_remaining_vectors(mapping_with_vector)
80+
mtg = import_mtg_example()
81+
@test !last(PlantSimEngine.check_statuses_contain_no_remaining_vectors(mapping_with_vector))
82+
@test_throws "call the function generate_models_from_status_vectors" PlantSimEngine.GraphSimulation(mtg, mapping_with_vector)
8183

8284
mapping_with_empty_status = Dict(
8385
"Scale" =>
@@ -87,7 +89,7 @@ end
8789
),
8890
)
8991

90-
@test PlantSimEngine.check_statuses_contain_no_remaining_vectors(mapping_with_empty_status)
92+
@test last(PlantSimEngine.check_statuses_contain_no_remaining_vectors(mapping_with_empty_status))
9193
end
9294

9395
# simple conversion to a mapping, with a manually written model
@@ -200,9 +202,7 @@ PlantSimEngine.ObjectDependencyTrait(::Type{<:ToyTestDegreeDaysCumulModel}) = Pl
200202
# TODO when outputs filtering is implemented, can test it with this function
201203
mtg, mapping, outputs_mapping = PlantSimEngine.modellist_to_mapping(models, st2; nsteps=nsteps, outputs=nothing)
202204

203-
# This test is now redundant as it is called during GraphSimulation initialisation
204-
#@test PlantSimEngine.check_statuses_contain_no_remaining_vectors(mapping)
205-
graphsim2 = PlantSimEngine.GraphSimulation(mtg, mapping, nsteps=nsteps, check=true, outputs=outputs_mapping)
205+
graphsim2 = PlantSimEngine.GraphSimulation(mtg, mapping, nsteps=nsteps, check=true, outputs=outputs_mapping)
206206

207207
sim2 = run!(graphsim2,
208208
meteo_day,
@@ -216,4 +216,65 @@ PlantSimEngine.ObjectDependencyTrait(::Type{<:ToyTestDegreeDaysCumulModel}) = Pl
216216

217217
end
218218

219-
#[getproperty(a,i) for i in fieldnames(typeof(a))]
219+
#[getproperty(a,i) for i in fieldnames(typeof(a))]
220+
221+
222+
@testset "Vector in status in a multiscale context" begin
223+
meteo_day = CSV.read(joinpath(pkgdir(PlantSimEngine), "examples/meteo_day.csv"), DataFrame, header=18)
224+
TT_v = Vector(meteo_day.TT)
225+
TT_cu_vec = Vector(cumsum(meteo_day.TT))
226+
nsteps = length(meteo_day.TT)
227+
228+
mapping_with_vector = Dict(
229+
230+
"Plant" => (
231+
MultiScaleModel(
232+
model=ToyCAllocationModel(),
233+
mapping=[
234+
# inputs
235+
:carbon_assimilation => ["Leaf"],
236+
:carbon_demand => ["Leaf", "Internode"],
237+
# outputs
238+
:carbon_allocation => ["Leaf", "Internode"]
239+
],
240+
),
241+
MultiScaleModel(
242+
model=ToyPlantRmModel(),
243+
mapping=[:Rm_organs => ["Leaf" => :Rm, "Internode" => :Rm],],
244+
),
245+
),
246+
"Internode" => (
247+
ToyCDemandModel(optimal_biomass=10.0, development_duration=200.0),
248+
ToyMaintenanceRespirationModel(1.5, 0.06, 25.0, 0.6, 0.004),
249+
Status(TT=TT_v, carbon_biomass=1.0)
250+
),
251+
"Leaf" => (
252+
MultiScaleModel(
253+
model=ToyAssimModel(),
254+
mapping=[:soil_water_content => "Soil",],
255+
# Notice we provide "Soil", not ["Soil"], so a single value is expected here
256+
),
257+
ToyCDemandModel(optimal_biomass=10.0, development_duration=200.0),
258+
ToyMaintenanceRespirationModel(2.1, 0.06, 25.0, 1.0, 0.025),
259+
Status(aPPFD=1300.0, carbon_biomass=2.0, TT=10.0), # TODO try calling the generated TT output through a variable mapping
260+
),
261+
"Soil" => (
262+
ToySoilWaterModel(),
263+
),
264+
)
265+
266+
out_multiscale = Dict("Plant" => (:Rm_organs,),)
267+
mtg = import_mtg_example();
268+
269+
mapping_without_vectors = PlantSimEngine.replace_mapping_status_vectors_with_generated_models(mapping_with_vector, "Soil", nsteps)
270+
271+
graph_sim_multiscale = @test_nowarn PlantSimEngine.GraphSimulation(mtg, mapping_without_vectors, nsteps=nsteps, check=true, outputs=out_multiscale)
272+
273+
sim_multiscale = run!(graph_sim_multiscale,
274+
meteo_day,
275+
PlantMeteo.Constants(),
276+
nothing;
277+
check=true,
278+
executor=SequentialEx()
279+
)
280+
end

0 commit comments

Comments
 (0)