Skip to content

Commit aee3491

Browse files
committed
Working on meteo sampling (e.g. aggregation at daily timestep from hourly meteo) + make a uniform interface with policies
1 parent 46d4b24 commit aee3491

11 files changed

Lines changed: 452 additions & 114 deletions

docs/src/API/API_public.md

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ For mapping-level multi-rate configuration, combine:
2020
- `ModelSpec(...)`
2121
- `TimeStepModel(...)`
2222
- `InputBindings(...)`
23+
- `MeteoBindings(...)`
2324
- `OutputRouting(...)`
2425
- `ScopeModel(...)`
2526
- `OutputRequest(...)` in `tracked_outputs` for resampled exports
@@ -70,26 +71,39 @@ TimeStepModel(ClockSpec(2.0, 1.0)) |>
7071
InputBindings(; x=(process=:producer, var=:x))
7172
```
7273

74+
### Meteo aggregation bindings
75+
76+
```julia
77+
ModelSpec(DailyModel()) |>
78+
TimeStepModel(ClockSpec(24.0, 1.0)) |>
79+
MeteoBindings(
80+
T=MeanWeighted(), # default source is :T
81+
Ri_SW_f=RadiationEnergy(), # integrate W m-2 to MJ m-2 over the model window
82+
custom_peak=(source=:custom_var, reducer=MaxReducer()),
83+
)
84+
```
85+
7386
### Parameterized window reducers
7487

75-
`Integrate()` defaults to `:sum`; `Aggregate()` defaults to `:mean`.
88+
`Integrate()` defaults to `SumReducer()`; `Aggregate()` defaults to `MeanReducer()`.
7689

7790
```julia
7891
ModelSpec(DailyModel()) |>
7992
TimeStepModel(ClockSpec(24.0, 1.0)) |>
80-
InputBindings(; a=(process=:hourly_assim, var=:A, scale="Leaf", policy=Integrate(:sum)))
93+
InputBindings(; a=(process=:hourly_assim, var=:A, scale="Leaf", policy=Integrate(SumReducer())))
8194

8295
ModelSpec(DailyModel()) |>
8396
TimeStepModel(ClockSpec(24.0, 1.0)) |>
84-
InputBindings(; a=(process=:hourly_assim, var=:A, scale="Leaf", policy=Aggregate(:max)))
97+
InputBindings(; a=(process=:hourly_assim, var=:A, scale="Leaf", policy=Aggregate(MaxReducer())))
8598

8699
ModelSpec(DailyModel()) |>
87100
TimeStepModel(ClockSpec(24.0, 1.0)) |>
88101
InputBindings(; a=(process=:hourly_assim, var=:A, scale="Leaf", policy=Integrate(vals -> maximum(vals) - minimum(vals))))
89102
```
90103

91-
Supported reducer symbols are:
92-
`:sum`, `:mean`, `:max`, `:min`, `:first`, `:last`.
104+
Built-in reducer types are:
105+
`SumReducer()`, `MeanReducer()`, `MaxReducer()`, `MinReducer()`, `FirstReducer()`, `LastReducer()`.
106+
The same reducer objects are also used by `MeteoBindings(...)`.
93107

94108
### Parameterized interpolation mode
95109

docs/src/model_execution.md

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,15 @@ For multiscale simulations, model usage is configured in the mapping through `Mo
1515

1616
- `TimeStepModel(...)`: sets model execution clock.
1717
- `InputBindings(...)`: sets producer, source variable, optional source scale, and policy for each consumer input.
18+
- `MeteoBindings(...)`: sets weather aggregation rules at the model clock for meteo variables.
1819
- `OutputRouting(...)`: sets whether an output is canonical (`:canonical`) or stream-only (`:stream_only`).
1920
- `ScopeModel(...)`: partitions producer streams by scope (`:global`, `:plant`, `:scene`, `:self`) for multi-entity simulations.
2021

2122
Policy parameterization:
22-
- `Integrate()` defaults to `:sum`; you can pass another reducer, e.g. `Integrate(:mean)` or `Integrate(vals -> maximum(vals) - minimum(vals))`.
23-
- `Aggregate()` defaults to `:mean`; you can pass reducers such as `Aggregate(:max)`.
23+
- `Integrate()` defaults to `SumReducer()`; you can pass another reducer, e.g. `Integrate(MeanReducer())` or `Integrate(vals -> maximum(vals) - minimum(vals))`.
24+
- `Aggregate()` defaults to `MeanReducer()`; you can pass reducers such as `Aggregate(MaxReducer())`.
2425
- `Interpolate()` defaults to `mode=:linear, extrapolation=:linear`; use `Interpolate(; mode=:hold, extrapolation=:hold)` for hold behavior.
26+
- The same reducer objects are reused by meteo sampling (`MeteoBindings`) and by windowed policies (`Integrate`, `Aggregate`).
2527

2628
`TimeStepModel(...)` accepts either step counts (`Real`), `ClockSpec`, or fixed `Dates` periods
2729
(for example `Dates.Hour(1)`, `Dates.Day(1)`). Fixed periods are converted internally using
@@ -41,6 +43,7 @@ Typical pipeline form:
4143
```julia
4244
ModelSpec(MyModel()) |>
4345
TimeStepModel(ClockSpec(24.0, 1.0)) |>
46+
MeteoBindings(; T=MeanWeighted()) |>
4447
InputBindings(; x=(process=:producer, var=:y, policy=HoldLast())) |>
4548
OutputRouting(; z=:stream_only)
4649
```
@@ -87,6 +90,10 @@ mapping = Dict(
8790
```
8891

8992
When `multirate=true` is passed to `run!`, the runtime resolves inputs from producer temporal streams according to these policies.
93+
Meteo rows are also sampled at each model clock. By default, meteo variables are aggregated from
94+
the finest weather step (for example `T` and `Rh` as weighted means, `Tmin/Tmax`, and radiation
95+
quantity aliases such as `Ri_SW_q` in MJ m-2). You can override these rules with `MeteoBindings(...)`
96+
on each `ModelSpec`.
9097

9198
### Current limitations
9299

src/PlantSimEngine.jl

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ include("time/runtime/bindings.jl")
101101
include("time/runtime/input_resolution.jl")
102102
include("time/runtime/publishers.jl")
103103
include("time/runtime/output_export.jl")
104+
include("time/runtime/meteo_sampling.jl")
104105

105106
# Simulation:
106107
include("run.jl")
@@ -118,18 +119,19 @@ export PreviousTimeStep
118119
export AbstractModel
119120
export ScopeId, ClockSpec, ModelKey, OutputKey
120121
export SchedulePolicy, HoldLast, Interpolate, Integrate, Aggregate
122+
export AbstractTimeReducer, MeanWeighted, MeanReducer, SumReducer, MinReducer, MaxReducer, FirstReducer, LastReducer, RadiationEnergy
121123
export OutputCache, HoldLastCache, InterpolateCache, IntegrateCache, AggregateCache
122124
export TemporalState
123125
export OutputRequest, collect_outputs
124-
export ModelList, MultiScaleModel, ModelSpec, TimeStepModel, InputBindings, OutputRouting, ScopeModel
126+
export ModelList, MultiScaleModel, ModelSpec, TimeStepModel, InputBindings, MeteoBindings, OutputRouting, ScopeModel
125127
export RMSE, NRMSE, EF, dr
126128
export Status, TimeStepTable, status
127129
export init_status!
128130
export add_organ!
129131
export @process, process
130132
export to_initialize, is_initialized, init_variables, dep
131133
export inputs, outputs, variables, convert_outputs
132-
export timespec, output_policy, input_bindings, output_routing, model_scope
134+
export timespec, output_policy, input_bindings, meteo_bindings, output_routing, model_scope
133135
export run!
134136
export fit
135137

src/mtg/ModelSpec.jl

Lines changed: 63 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,18 @@
11
"""
2-
ModelSpec(model; multiscale=nothing, timestep=nothing, input_bindings=NamedTuple(), output_routing=NamedTuple(), scope=:global)
2+
ModelSpec(model; multiscale=nothing, timestep=nothing, input_bindings=NamedTuple(), meteo_bindings=NamedTuple(), output_routing=NamedTuple(), scope=:global)
33
44
User-side model configuration wrapper for mapping/model list composition.
55
66
`ModelSpec` keeps model implementation and scenario-specific usage metadata in one place.
77
This allows modelers to publish reusable models while users decide how models are coupled in
88
their simulation setup.
99
"""
10-
struct ModelSpec{M,MS,TS,IB,OR,SC}
10+
struct ModelSpec{M,MS,TS,IB,MB,OR,SC}
1111
model::M
1212
multiscale::MS
1313
timestep::TS
1414
input_bindings::IB
15+
meteo_bindings::MB
1516
output_routing::OR
1617
scope::SC
1718
end
@@ -27,6 +28,7 @@ function ModelSpec(
2728
multiscale=nothing,
2829
timestep=nothing,
2930
input_bindings=NamedTuple(),
31+
meteo_bindings=NamedTuple(),
3032
output_routing=NamedTuple(),
3133
scope=:global
3234
)
@@ -40,13 +42,15 @@ function ModelSpec(
4042

4143
normalized_multiscale = _normalize_multiscale_mapping(base_model, base_multiscale)
4244
normalized_input_bindings = _normalize_input_bindings(input_bindings)
45+
normalized_meteo_bindings = _normalize_meteo_bindings(meteo_bindings)
4346
normalized_output_routing = _normalize_output_routing(output_routing)
4447
normalized_scope = _normalize_scope_selector(scope)
45-
return ModelSpec{typeof(base_model),typeof(normalized_multiscale),typeof(timestep),typeof(normalized_input_bindings),typeof(normalized_output_routing),typeof(normalized_scope)}(
48+
return ModelSpec{typeof(base_model),typeof(normalized_multiscale),typeof(timestep),typeof(normalized_input_bindings),typeof(normalized_meteo_bindings),typeof(normalized_output_routing),typeof(normalized_scope)}(
4649
base_model,
4750
normalized_multiscale,
4851
timestep,
4952
normalized_input_bindings,
53+
normalized_meteo_bindings,
5054
normalized_output_routing,
5155
normalized_scope
5256
)
@@ -58,10 +62,11 @@ function ModelSpec(
5862
multiscale=spec.multiscale,
5963
timestep=spec.timestep,
6064
input_bindings=spec.input_bindings,
65+
meteo_bindings=spec.meteo_bindings,
6166
output_routing=spec.output_routing,
6267
scope=spec.scope
6368
)
64-
ModelSpec(model; multiscale=multiscale, timestep=timestep, input_bindings=input_bindings, output_routing=output_routing, scope=scope)
69+
ModelSpec(model; multiscale=multiscale, timestep=timestep, input_bindings=input_bindings, meteo_bindings=meteo_bindings, output_routing=output_routing, scope=scope)
6570
end
6671

6772
as_model_spec(spec::ModelSpec) = spec
@@ -98,6 +103,16 @@ function with_input_bindings(model_or_spec, bindings)
98103
return ModelSpec(spec; input_bindings=_normalize_input_bindings(bindings))
99104
end
100105

106+
"""
107+
with_meteo_bindings(model_or_spec, bindings)
108+
109+
Return a `ModelSpec` with explicit meteo aggregation bindings.
110+
"""
111+
function with_meteo_bindings(model_or_spec, bindings)
112+
spec = as_model_spec(model_or_spec)
113+
return ModelSpec(spec; meteo_bindings=_normalize_meteo_bindings(bindings))
114+
end
115+
101116
"""
102117
with_output_routing(model_or_spec, routing)
103118
@@ -139,6 +154,36 @@ end
139154

140155
_normalize_input_bindings(bindings) = bindings
141156

157+
function _normalize_meteo_binding(binding)
158+
if binding isa DataType
159+
binding <: PlantMeteo.AbstractTimeReducer || error(
160+
"Unsupported MeteoBindings reducer type `$(binding)`. ",
161+
"Use a PlantMeteo reducer type/instance, callable, or NamedTuple(source=..., reducer=...)."
162+
)
163+
return binding
164+
elseif binding isa PlantMeteo.AbstractTimeReducer
165+
return binding
166+
elseif binding isa Function
167+
return binding
168+
elseif binding isa NamedTuple
169+
return binding
170+
end
171+
error(
172+
"Unsupported MeteoBindings value `$(binding)` of type `$(typeof(binding))`. ",
173+
"Use a PlantMeteo reducer type/instance, callable, or NamedTuple(source=..., reducer=...)."
174+
)
175+
end
176+
177+
function _normalize_meteo_bindings(bindings::NamedTuple)
178+
normalized = Pair{Symbol,Any}[]
179+
for (k, v) in pairs(bindings)
180+
push!(normalized, k => _normalize_meteo_binding(v))
181+
end
182+
return (; normalized...)
183+
end
184+
185+
_normalize_meteo_bindings(bindings) = bindings
186+
142187
function _normalize_output_routing(routing::NamedTuple)
143188
normalized = Pair{Symbol,Symbol}[]
144189
for (k, v) in pairs(routing)
@@ -184,6 +229,20 @@ Pipe-style transform that sets explicit input bindings on a model/spec.
184229
InputBindings(bindings) = x -> with_input_bindings(x, bindings)
185230
InputBindings(; kwargs...) = InputBindings((; kwargs...))
186231

232+
"""
233+
MeteoBindings(bindings)
234+
MeteoBindings(; kwargs...)
235+
236+
Pipe-style transform that sets explicit weather sampling bindings on a model/spec.
237+
Each key is the target meteo variable seen by the model.
238+
Each value can be:
239+
- a PlantMeteo reducer instance/type (e.g. `MeanWeighted()`, `MaxReducer`)
240+
- `Function`: custom reducer callable
241+
- `NamedTuple`: optional fields `source` (Symbol/String) and `reducer`
242+
"""
243+
MeteoBindings(bindings) = x -> with_meteo_bindings(x, bindings)
244+
MeteoBindings(; kwargs...) = MeteoBindings((; kwargs...))
245+
187246
"""
188247
OutputRouting(routing)
189248
OutputRouting(; kwargs...)

src/mtg/model_spec_validation.jl

Lines changed: 110 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,46 @@
11
const _INPUT_BINDING_FIELDS = (:process, :var, :scale, :policy)
22
const _MODEL_SCOPE_SELECTORS = (:global, :plant, :scene, :self)
3+
const _METEO_BINDING_FIELDS = (:source, :reducer)
34

45
function _validate_window_reducer(scale::String, process::Symbol, input_var::Symbol, policy_name::Symbol, reducer)
5-
if reducer isa Symbol
6-
reducer in _WINDOW_REDUCER_SYMBOLS || error(
7-
"Invalid reducer `$(reducer)` for policy `$(policy_name)` on input `$(input_var)` ",
8-
"in process `$(process)` at scale `$(scale)`. Supported symbols are $(_WINDOW_REDUCER_SYMBOLS)."
6+
if reducer isa DataType
7+
reducer <: PlantMeteo.AbstractTimeReducer || error(
8+
"Invalid reducer type `$(reducer)` for policy `$(policy_name)` on input `$(input_var)` ",
9+
"in process `$(process)` at scale `$(scale)`. ",
10+
"Expected a PlantMeteo reducer type/instance or a callable."
11+
)
12+
rr = try
13+
reducer()
14+
catch
15+
error(
16+
"Reducer type `$(reducer)` for policy `$(policy_name)` on input `$(input_var)` ",
17+
"in process `$(process)` at scale `$(scale)` cannot be instantiated without arguments."
18+
)
19+
end
20+
applicable(rr, [1.0, 2.0]) || error(
21+
"Reducer type `$(reducer)` for policy `$(policy_name)` on input `$(input_var)` in process `$(process)` at scale `$(scale)` ",
22+
"must be callable on a vector of numeric values."
23+
)
24+
return nothing
25+
elseif reducer isa PlantMeteo.AbstractTimeReducer
26+
applicable(reducer, [1.0, 2.0]) || error(
27+
"Reducer `$(typeof(reducer))` for policy `$(policy_name)` on input `$(input_var)` in process `$(process)` at scale `$(scale)` ",
28+
"must be callable on a vector of numeric values."
29+
)
30+
return nothing
31+
elseif reducer isa Function
32+
applicable(reducer, [1.0, 2.0]) || error(
33+
"Reducer for policy `$(policy_name)` on input `$(input_var)` in process `$(process)` at scale `$(scale)` ",
34+
"must be callable on a vector of numeric values."
935
)
1036
return nothing
1137
end
1238

13-
applicable(reducer, [1.0, 2.0]) || error(
14-
"Reducer for policy `$(policy_name)` on input `$(input_var)` in process `$(process)` at scale `$(scale)` ",
15-
"must be a supported Symbol or a callable accepting a vector of numeric values."
39+
error(
40+
"Invalid reducer value `$(reducer)` (type `$(typeof(reducer))`) for policy `$(policy_name)` ",
41+
"on input `$(input_var)` in process `$(process)` at scale `$(scale)`. ",
42+
"Expected a PlantMeteo reducer type/instance or a callable."
1643
)
17-
18-
return nothing
1944
end
2045

2146
function _validate_policy_instance(scale::String, process::Symbol, input_var::Symbol, policy::SchedulePolicy)
@@ -275,6 +300,81 @@ function _validate_output_routing_for_spec(scale::String, process::Symbol, spec:
275300
return nothing
276301
end
277302

303+
function _validate_meteo_binding(scale::String, process::Symbol, target_var::Symbol, binding)
304+
if binding isa Function || binding isa PlantMeteo.AbstractTimeReducer
305+
return nothing
306+
elseif binding isa DataType
307+
binding <: PlantMeteo.AbstractTimeReducer || error(
308+
"Invalid MeteoBindings reducer type for variable `$(target_var)` in process `$(process)` at scale `$(scale)`: ",
309+
"expected a subtype of `PlantMeteo.AbstractTimeReducer`."
310+
)
311+
try
312+
binding()
313+
catch
314+
error(
315+
"Invalid MeteoBindings reducer type for variable `$(target_var)` in process `$(process)` at scale `$(scale)`: ",
316+
"type `$(binding)` cannot be instantiated without arguments."
317+
)
318+
end
319+
return nothing
320+
elseif binding isa NamedTuple
321+
extra = setdiff(collect(keys(binding)), collect(_METEO_BINDING_FIELDS))
322+
isempty(extra) || error(
323+
"Invalid MeteoBindings for variable `$(target_var)` in process `$(process)` at scale `$(scale)`: ",
324+
"unsupported fields $(extra)."
325+
)
326+
327+
if haskey(binding, :source)
328+
binding.source isa Symbol || binding.source isa AbstractString || error(
329+
"Invalid MeteoBindings source for variable `$(target_var)` in process `$(process)` at scale `$(scale)`: ",
330+
"`source` must be a Symbol or String."
331+
)
332+
end
333+
if haskey(binding, :reducer)
334+
reducer = binding.reducer
335+
if reducer isa DataType
336+
reducer <: PlantMeteo.AbstractTimeReducer || error(
337+
"Invalid MeteoBindings reducer for variable `$(target_var)` in process `$(process)` at scale `$(scale)`: ",
338+
"`reducer` type must subtype `PlantMeteo.AbstractTimeReducer`."
339+
)
340+
try
341+
reducer()
342+
catch
343+
error(
344+
"Invalid MeteoBindings reducer type for variable `$(target_var)` in process `$(process)` at scale `$(scale)`: ",
345+
"type `$(reducer)` cannot be instantiated without arguments."
346+
)
347+
end
348+
else
349+
(reducer isa PlantMeteo.AbstractTimeReducer || reducer isa Function) || error(
350+
"Invalid MeteoBindings reducer for variable `$(target_var)` in process `$(process)` at scale `$(scale)`: ",
351+
"`reducer` must be a reducer instance/type or a callable."
352+
)
353+
end
354+
end
355+
return nothing
356+
end
357+
358+
error(
359+
"Invalid MeteoBindings value for variable `$(target_var)` in process `$(process)` at scale `$(scale)`: ",
360+
"unsupported type `$(typeof(binding))`."
361+
)
362+
end
363+
function _validate_meteo_bindings_for_spec(scale::String, process::Symbol, spec::ModelSpec)
364+
bindings = meteo_bindings(spec)
365+
bindings isa NamedTuple || error(
366+
"MeteoBindings for process `$(process)` at scale `$(scale)` must be a NamedTuple, got `$(typeof(bindings))`."
367+
)
368+
369+
for (target_var, binding) in pairs(bindings)
370+
target_var isa Symbol || error(
371+
"MeteoBindings key for process `$(process)` at scale `$(scale)` must be a Symbol, got `$(typeof(target_var))`."
372+
)
373+
_validate_meteo_binding(scale, process, target_var, binding)
374+
end
375+
return nothing
376+
end
377+
278378
"""
279379
validate_model_specs_configuration(model_specs)
280380
@@ -292,6 +392,7 @@ function validate_model_specs_configuration(model_specs)
292392
_validate_timestep_spec(scale, process, spec)
293393
_validate_scope_spec(scale, process, spec)
294394
_validate_input_bindings_for_spec(scale, process, spec, model_specs, known_processes)
395+
_validate_meteo_bindings_for_spec(scale, process, spec)
295396
_validate_output_routing_for_spec(scale, process, spec)
296397
end
297398
end

0 commit comments

Comments
 (0)