Skip to content

Commit 2b871de

Browse files
authored
Merge pull request #178 from VirtualPlantLab/modelmapping-validations
`ModelMapping` validations at construction
2 parents 75a96e2 + f564bb3 commit 2b871de

18 files changed

Lines changed: 653 additions & 123 deletions

docs/src/API/API_public.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,14 +60,13 @@ Scope selection detail:
6060
```julia
6161
req_hold = OutputRequest("Leaf", :A; name=:A_hourly, process=:assim, policy=HoldLast())
6262
req_day = OutputRequest("Leaf", :A; name=:A_daily_sum, process=:assim, policy=Integrate(), clock=ClockSpec(24.0, 1.0))
63-
run!(sim, meteo; multirate=true, tracked_outputs=[req_hold, req_day], executor=SequentialEx())
63+
run!(sim, meteo; tracked_outputs=[req_hold, req_day], executor=SequentialEx())
6464
out = collect_outputs(sim; sink=DataFrame)
6565

6666
# or directly:
6767
out_status, out = run!(
6868
sim,
6969
meteo;
70-
multirate=true,
7170
tracked_outputs=[req_hold, req_day],
7271
return_requested_outputs=true,
7372
)
@@ -109,6 +108,8 @@ MeteoBindings(
109108
### Parameterized window reducers
110109

111110
`Integrate()` defaults to `SumReducer()`; `Aggregate()` defaults to `MeanReducer()`.
111+
With the same reducer, they are runtime-equivalent.
112+
Use `Integrate` for accumulation semantics and `Aggregate` for summary-statistics semantics.
112113

113114
```julia
114115
ModelSpec(DailyModel()) |>

docs/src/model_execution.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ Inspection helpers:
5050
Policy parameterization:
5151
- `Integrate()` defaults to `SumReducer()`; you can pass another reducer, e.g. `Integrate(MeanReducer())` or `Integrate(vals -> maximum(vals) - minimum(vals))`.
5252
- `Aggregate()` defaults to `MeanReducer()`; you can pass reducers such as `Aggregate(MaxReducer())`.
53+
- Difference between `Integrate` and `Aggregate`: with the same reducer they are runtime-equivalent.
54+
In practice, only defaults and naming intent differ (`Integrate` for accumulation, `Aggregate` for summary statistics).
5355
- `Interpolate()` defaults to `mode=:linear, extrapolation=:linear`; use `Interpolate(; mode=:hold, extrapolation=:hold)` for hold behavior.
5456
- The same reducer objects are reused by meteo sampling (`MeteoBindings`) and by windowed policies (`Integrate`, `Aggregate`).
5557

@@ -131,7 +133,7 @@ mapping = ModelMapping(
131133
)
132134
```
133135

134-
When `multirate=true` is passed to `run!`, the runtime resolves inputs from producer temporal streams according to these policies.
136+
When the `ModelMapping` declares multirate configuration, the runtime resolves inputs from producer temporal streams according to these policies.
135137
Meteo rows are also sampled at each model clock. By default, meteo variables are aggregated from
136138
the finest weather step (for example `T` and `Rh` as weighted means, `Tmin/Tmax`, and radiation
137139
quantity aliases such as `Ri_SW_q` in MJ m-2). You can override these rules with `MeteoBindings(...)`
@@ -154,7 +156,7 @@ req = OutputRequest("Leaf", :carbon_assimilation;
154156
clock=ClockSpec(24.0, 1.0)
155157
)
156158

157-
run!(sim, meteo; multirate=true, tracked_outputs=[req], executor=SequentialEx())
159+
run!(sim, meteo; tracked_outputs=[req], executor=SequentialEx())
158160
exported = collect_outputs(sim; sink=DataFrame)
159161
```
160162

@@ -165,7 +167,6 @@ You can also return them directly from `run!`:
165167
out_status, exported = run!(
166168
sim,
167169
meteo;
168-
multirate=true,
169170
tracked_outputs=[req],
170171
return_requested_outputs=true,
171172
)

docs/src/multirate/multirate_tutorial.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,6 @@ req_plant_weekly = OutputRequest("Plant", :plant_assim_w;
173173
out_status, exported = run!(
174174
sim,
175175
meteo_hourly;
176-
multirate=true,
177176
executor=SequentialEx(),
178177
tracked_outputs=[req_leaf_hourly, req_plant_daily, req_plant_daily_T, req_plant_weekly],
179178
return_requested_outputs=true,

docs/src/planned_features.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
### Varying timesteps
66

77
Model-level varying timesteps are now available experimentally for MTG simulations
8-
through multi-rate execution (`multirate=true`) and mapping-level `ModelSpec` transforms
8+
through mapping-declared multi-rate execution and mapping-level `ModelSpec` transforms
99
such as `TimeStepModel`, `InputBindings`, `OutputRouting`, and `ScopeModel`.
1010

1111
Current remaining gaps for this area are:

src/mtg/GraphSimulation.jl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ struct GraphSimulation{T,S,U,O,V,TS,MS}
3131
outputs::Dict{String,O}
3232
outputs_index::Dict{String, Int}
3333
temporal_state::TS
34+
is_multirate::Bool
3435
end
3536

3637
function GraphSimulation(graph, mapping; nsteps=1, outputs=nothing, type_promotion=nothing, check=true, verbose=false)
@@ -47,6 +48,7 @@ get_models(g::GraphSimulation) = g.models
4748
get_model_specs(g::GraphSimulation) = g.model_specs
4849
outputs(g::GraphSimulation) = g.outputs
4950
temporal_state(g::GraphSimulation) = g.temporal_state
51+
is_multirate(g::GraphSimulation) = g.is_multirate
5052

5153
"""
5254
convert_outputs(sim_outputs::Dict{String,O} where O, sink; refvectors=false, no_value=nothing)

src/mtg/initialisation.jl

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -315,13 +315,25 @@ function init_simulation(mtg, mapping; nsteps=1, outputs=nothing, type_promotion
315315
end
316316

317317
models = Dict(first(m) => parse_models(get_models(last(m))) for m in mapping)
318-
model_specs = Dict(first(m) => parse_model_specs(last(m)) for m in mapping)
319-
scale_reachability = _scale_reachability_from_mtg(mtg)
320-
infer_model_specs_configuration!(model_specs; scale_reachability=scale_reachability)
321-
validate_model_specs_configuration(model_specs)
318+
model_specs = if mapping isa ModelMapping && !isempty(mapping.info.model_specs)
319+
deepcopy(mapping.info.model_specs)
320+
else
321+
Dict(first(m) => parse_model_specs(last(m)) for m in mapping)
322+
end
322323

323324
soft_dep_graphs_roots, hard_dep_dict = hard_dependencies(mapping; verbose=false)
324325

326+
scale_reachability = _scale_reachability_from_mtg(mtg)
327+
_infer_timestep_hints!(model_specs)
328+
ignored_same_rate_hard_children = _same_rate_hard_dependency_children(model_specs, soft_dep_graphs_roots)
329+
active_processes_by_scale = _active_processes_for_inference(model_specs, ignored_same_rate_hard_children)
330+
infer_model_specs_configuration!(
331+
model_specs;
332+
scale_reachability=scale_reachability,
333+
active_processes_by_scale=active_processes_by_scale
334+
)
335+
validate_model_specs_configuration(model_specs)
336+
325337
# Get the status of each node by node type, pre-initialised considering multi-scale variables:
326338
statuses, status_templates, reverse_multiscale_mapping, vars_need_init =
327339
init_statuses(mtg, mapping, soft_dep_graphs_roots; type_promotion=type_promotion, verbose=verbose, check=check)
@@ -356,5 +368,19 @@ function init_simulation(mtg, mapping; nsteps=1, outputs=nothing, type_promotion
356368

357369
outputs_index = Dict{String, Int}(s => 1 for s in keys(outputs))
358370
temporal_state = TemporalState()
359-
return (; mtg, statuses, status_templates, reverse_multiscale_mapping, vars_need_init, dependency_graph=dep_graph, models, model_specs, outputs, outputs_index, temporal_state)
371+
mapping_is_multirate = mapping isa ModelMapping ? is_multirate(mapping) : false
372+
return (;
373+
mtg,
374+
statuses,
375+
status_templates,
376+
reverse_multiscale_mapping,
377+
vars_need_init,
378+
dependency_graph=dep_graph,
379+
models,
380+
model_specs,
381+
outputs,
382+
outputs_index,
383+
temporal_state,
384+
is_multirate=mapping_is_multirate
385+
)
360386
end

0 commit comments

Comments
 (0)