Skip to content

Commit 022182d

Browse files
committed
Add a way to tell that meteo window is using past window (rolling window) or calendar (possibly using values future to the current step)
1 parent aee3491 commit 022182d

8 files changed

Lines changed: 231 additions & 12 deletions

File tree

docs/src/API/API_public.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ For mapping-level multi-rate configuration, combine:
2121
- `TimeStepModel(...)`
2222
- `InputBindings(...)`
2323
- `MeteoBindings(...)`
24+
- `MeteoWindow(...)`
2425
- `OutputRouting(...)`
2526
- `ScopeModel(...)`
2627
- `OutputRequest(...)` in `tracked_outputs` for resampled exports
@@ -76,13 +77,22 @@ InputBindings(; x=(process=:producer, var=:x))
7677
```julia
7778
ModelSpec(DailyModel()) |>
7879
TimeStepModel(ClockSpec(24.0, 1.0)) |>
80+
MeteoWindow(CalendarWindow(:day; anchor=:current_period, week_start=1, completeness=:strict)) |>
7981
MeteoBindings(
8082
T=MeanWeighted(), # default source is :T
8183
Ri_SW_f=RadiationEnergy(), # integrate W m-2 to MJ m-2 over the model window
8284
custom_peak=(source=:custom_var, reducer=MaxReducer()),
8385
)
8486
```
8587

88+
`MeteoWindow(...)` options:
89+
- `RollingWindow()` (default): trailing rolling window driven by `dt`.
90+
- `CalendarWindow(period; anchor, week_start, completeness)` with:
91+
: `period` in `:day`, `:week`, `:month`
92+
: `anchor` in `:current_period`, `:previous_complete_period`
93+
: `week_start` in `1:7` (1 = Monday)
94+
: `completeness` in `:allow_partial`, `:strict`
95+
8696
### Parameterized window reducers
8797

8898
`Integrate()` defaults to `SumReducer()`; `Aggregate()` defaults to `MeanReducer()`.

docs/src/model_execution.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ For multiscale simulations, model usage is configured in the mapping through `Mo
1616
- `TimeStepModel(...)`: sets model execution clock.
1717
- `InputBindings(...)`: sets producer, source variable, optional source scale, and policy for each consumer input.
1818
- `MeteoBindings(...)`: sets weather aggregation rules at the model clock for meteo variables.
19+
- `MeteoWindow(...)`: sets weather row selection strategy (`RollingWindow()` or `CalendarWindow(...)`).
1920
- `OutputRouting(...)`: sets whether an output is canonical (`:canonical`) or stream-only (`:stream_only`).
2021
- `ScopeModel(...)`: partitions producer streams by scope (`:global`, `:plant`, `:scene`, `:self`) for multi-entity simulations.
2122

@@ -43,11 +44,25 @@ Typical pipeline form:
4344
```julia
4445
ModelSpec(MyModel()) |>
4546
TimeStepModel(ClockSpec(24.0, 1.0)) |>
47+
MeteoWindow(CalendarWindow(:day; anchor=:current_period, week_start=1, completeness=:strict)) |>
4648
MeteoBindings(; T=MeanWeighted()) |>
4749
InputBindings(; x=(process=:producer, var=:y, policy=HoldLast())) |>
4850
OutputRouting(; z=:stream_only)
4951
```
5052

53+
### Calendar-aligned meteo windows
54+
55+
`MeteoWindow(...)` controls how rows are selected before reducers are applied:
56+
- `RollingWindow()` (default): trailing window based on `dt` (for example "last 24 steps").
57+
- `CalendarWindow(period; anchor, week_start, completeness)`:
58+
: `period` in `:day`, `:week`, `:month`
59+
: `anchor` in `:current_period`, `:previous_complete_period`
60+
: `week_start` in `1:7` (1 = Monday)
61+
: `completeness` in `:allow_partial`, `:strict`
62+
63+
`CalendarWindow(:day; anchor=:current_period, ...)` guarantees that a model running inside a day sees
64+
aggregates over that civil day (including later timesteps from that day when available).
65+
5166
### Hold-last coupling (default policy)
5267

5368
```julia

src/PlantSimEngine.jl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -123,15 +123,15 @@ export AbstractTimeReducer, MeanWeighted, MeanReducer, SumReducer, MinReducer, M
123123
export OutputCache, HoldLastCache, InterpolateCache, IntegrateCache, AggregateCache
124124
export TemporalState
125125
export OutputRequest, collect_outputs
126-
export ModelList, MultiScaleModel, ModelSpec, TimeStepModel, InputBindings, MeteoBindings, OutputRouting, ScopeModel
126+
export ModelList, MultiScaleModel, ModelSpec, TimeStepModel, InputBindings, MeteoBindings, MeteoWindow, OutputRouting, ScopeModel
127127
export RMSE, NRMSE, EF, dr
128128
export Status, TimeStepTable, status
129129
export init_status!
130130
export add_organ!
131131
export @process, process
132132
export to_initialize, is_initialized, init_variables, dep
133133
export inputs, outputs, variables, convert_outputs
134-
export timespec, output_policy, input_bindings, meteo_bindings, output_routing, model_scope
134+
export timespec, output_policy, input_bindings, meteo_bindings, meteo_window, output_routing, model_scope
135135
export run!
136136
export fit
137137

src/mtg/ModelSpec.jl

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,19 @@
11
"""
2-
ModelSpec(model; multiscale=nothing, timestep=nothing, input_bindings=NamedTuple(), meteo_bindings=NamedTuple(), output_routing=NamedTuple(), scope=:global)
2+
ModelSpec(model; multiscale=nothing, timestep=nothing, input_bindings=NamedTuple(), meteo_bindings=NamedTuple(), meteo_window=nothing, 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,MB,OR,SC}
10+
struct ModelSpec{M,MS,TS,IB,MB,MW,OR,SC}
1111
model::M
1212
multiscale::MS
1313
timestep::TS
1414
input_bindings::IB
1515
meteo_bindings::MB
16+
meteo_window::MW
1617
output_routing::OR
1718
scope::SC
1819
end
@@ -29,6 +30,7 @@ function ModelSpec(
2930
timestep=nothing,
3031
input_bindings=NamedTuple(),
3132
meteo_bindings=NamedTuple(),
33+
meteo_window=nothing,
3234
output_routing=NamedTuple(),
3335
scope=:global
3436
)
@@ -43,14 +45,16 @@ function ModelSpec(
4345
normalized_multiscale = _normalize_multiscale_mapping(base_model, base_multiscale)
4446
normalized_input_bindings = _normalize_input_bindings(input_bindings)
4547
normalized_meteo_bindings = _normalize_meteo_bindings(meteo_bindings)
48+
normalized_meteo_window = _normalize_meteo_window(meteo_window)
4649
normalized_output_routing = _normalize_output_routing(output_routing)
4750
normalized_scope = _normalize_scope_selector(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)}(
51+
return ModelSpec{typeof(base_model),typeof(normalized_multiscale),typeof(timestep),typeof(normalized_input_bindings),typeof(normalized_meteo_bindings),typeof(normalized_meteo_window),typeof(normalized_output_routing),typeof(normalized_scope)}(
4952
base_model,
5053
normalized_multiscale,
5154
timestep,
5255
normalized_input_bindings,
5356
normalized_meteo_bindings,
57+
normalized_meteo_window,
5458
normalized_output_routing,
5559
normalized_scope
5660
)
@@ -63,10 +67,11 @@ function ModelSpec(
6367
timestep=spec.timestep,
6468
input_bindings=spec.input_bindings,
6569
meteo_bindings=spec.meteo_bindings,
70+
meteo_window=spec.meteo_window,
6671
output_routing=spec.output_routing,
6772
scope=spec.scope
6873
)
69-
ModelSpec(model; multiscale=multiscale, timestep=timestep, input_bindings=input_bindings, meteo_bindings=meteo_bindings, output_routing=output_routing, scope=scope)
74+
ModelSpec(model; multiscale=multiscale, timestep=timestep, input_bindings=input_bindings, meteo_bindings=meteo_bindings, meteo_window=meteo_window, output_routing=output_routing, scope=scope)
7075
end
7176

7277
as_model_spec(spec::ModelSpec) = spec
@@ -113,6 +118,16 @@ function with_meteo_bindings(model_or_spec, bindings)
113118
return ModelSpec(spec; meteo_bindings=_normalize_meteo_bindings(bindings))
114119
end
115120

121+
"""
122+
with_meteo_window(model_or_spec, window)
123+
124+
Return a `ModelSpec` with explicit weather-window selection strategy.
125+
"""
126+
function with_meteo_window(model_or_spec, window)
127+
spec = as_model_spec(model_or_spec)
128+
return ModelSpec(spec; meteo_window=_normalize_meteo_window(window))
129+
end
130+
116131
"""
117132
with_output_routing(model_or_spec, routing)
118133
@@ -184,6 +199,25 @@ end
184199

185200
_normalize_meteo_bindings(bindings) = bindings
186201

202+
function _normalize_meteo_window(window)
203+
if isnothing(window)
204+
return nothing
205+
elseif window isa DataType
206+
window <: PlantMeteo.AbstractSamplingWindow || error(
207+
"Unsupported MeteoWindow type `$(window)`. ",
208+
"Use a PlantMeteo sampling-window type/instance."
209+
)
210+
return window()
211+
elseif window isa PlantMeteo.AbstractSamplingWindow
212+
return window
213+
end
214+
215+
error(
216+
"Unsupported MeteoWindow value `$(window)` of type `$(typeof(window))`. ",
217+
"Use a PlantMeteo sampling-window type/instance."
218+
)
219+
end
220+
187221
function _normalize_output_routing(routing::NamedTuple)
188222
normalized = Pair{Symbol,Symbol}[]
189223
for (k, v) in pairs(routing)
@@ -243,6 +277,14 @@ Each value can be:
243277
MeteoBindings(bindings) = x -> with_meteo_bindings(x, bindings)
244278
MeteoBindings(; kwargs...) = MeteoBindings((; kwargs...))
245279

280+
"""
281+
MeteoWindow(window)
282+
283+
Pipe-style transform that sets the weather window-selection strategy on a model/spec.
284+
Use `PlantMeteo.RollingWindow()` (default) or `PlantMeteo.CalendarWindow(...)`.
285+
"""
286+
MeteoWindow(window) = x -> with_meteo_window(x, window)
287+
246288
"""
247289
OutputRouting(routing)
248290
OutputRouting(; kwargs...)

src/mtg/model_spec_validation.jl

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
const _INPUT_BINDING_FIELDS = (:process, :var, :scale, :policy)
22
const _MODEL_SCOPE_SELECTORS = (:global, :plant, :scene, :self)
33
const _METEO_BINDING_FIELDS = (:source, :reducer)
4+
const _CALENDAR_PERIODS = (:day, :week, :month)
5+
const _CALENDAR_ANCHORS = (:current_period, :previous_complete_period)
6+
const _CALENDAR_COMPLETENESS = (:allow_partial, :strict)
47

58
function _validate_window_reducer(scale::String, process::Symbol, input_var::Symbol, policy_name::Symbol, reducer)
69
if reducer isa DataType
@@ -375,6 +378,36 @@ function _validate_meteo_bindings_for_spec(scale::String, process::Symbol, spec:
375378
return nothing
376379
end
377380

381+
function _validate_meteo_window_for_spec(scale::String, process::Symbol, spec::ModelSpec)
382+
window = meteo_window(spec)
383+
isnothing(window) && return nothing
384+
385+
window isa PlantMeteo.AbstractSamplingWindow || error(
386+
"MeteoWindow for process `$(process)` at scale `$(scale)` must be a PlantMeteo sampling-window instance, got `$(typeof(window))`."
387+
)
388+
389+
if window isa PlantMeteo.CalendarWindow
390+
window.period in _CALENDAR_PERIODS || error(
391+
"Invalid CalendarWindow period `$(window.period)` for process `$(process)` at scale `$(scale)`. ",
392+
"Allowed values are $(_CALENDAR_PERIODS)."
393+
)
394+
window.anchor in _CALENDAR_ANCHORS || error(
395+
"Invalid CalendarWindow anchor `$(window.anchor)` for process `$(process)` at scale `$(scale)`. ",
396+
"Allowed values are $(_CALENDAR_ANCHORS)."
397+
)
398+
1 <= window.week_start <= 7 || error(
399+
"Invalid CalendarWindow week_start `$(window.week_start)` for process `$(process)` at scale `$(scale)`. ",
400+
"Allowed values are integers in 1:7."
401+
)
402+
window.completeness in _CALENDAR_COMPLETENESS || error(
403+
"Invalid CalendarWindow completeness `$(window.completeness)` for process `$(process)` at scale `$(scale)`. ",
404+
"Allowed values are $(_CALENDAR_COMPLETENESS)."
405+
)
406+
end
407+
408+
return nothing
409+
end
410+
378411
"""
379412
validate_model_specs_configuration(model_specs)
380413
@@ -393,6 +426,7 @@ function validate_model_specs_configuration(model_specs)
393426
_validate_scope_spec(scale, process, spec)
394427
_validate_input_bindings_for_spec(scale, process, spec, model_specs, known_processes)
395428
_validate_meteo_bindings_for_spec(scale, process, spec)
429+
_validate_meteo_window_for_spec(scale, process, spec)
396430
_validate_output_routing_for_spec(scale, process, spec)
397431
end
398432
end

src/processes/models_inputs_outputs.jl

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,14 @@ Each value can be:
110110
"""
111111
meteo_bindings(spec::ModelSpec) = spec.meteo_bindings
112112

113+
"""
114+
meteo_window(spec::ModelSpec)
115+
116+
Optional weather window-selection strategy used by multi-rate MTG runtime.
117+
Defaults to `nothing` (runtime falls back to `PlantMeteo.RollingWindow()` behavior).
118+
"""
119+
meteo_window(spec::ModelSpec) = spec.meteo_window
120+
113121
"""
114122
inputs(mapping::Dict{String,T})
115123

src/time/runtime/meteo_sampling.jl

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,32 @@ function _prepare_meteo_sampler(meteo)
1010
return PlantMeteo.prepare_weather_sampler(meteo)
1111
end
1212

13-
_meteo_sampling_spec(clock::ClockSpec) = PlantMeteo.MeteoSamplingSpec(float(clock.dt), float(clock.phase))
13+
function _runtime_meteo_window(window)
14+
if isnothing(window)
15+
return nothing
16+
elseif window isa PlantMeteo.AbstractSamplingWindow
17+
return window
18+
elseif window isa DataType
19+
window <: PlantMeteo.AbstractSamplingWindow || error(
20+
"Unsupported MeteoWindow type `$(window)`. ",
21+
"Use a PlantMeteo sampling-window type/instance."
22+
)
23+
return window()
24+
end
25+
26+
error(
27+
"Unsupported MeteoWindow value `$(window)` of type `$(typeof(window))`. ",
28+
"Use a PlantMeteo sampling-window type/instance."
29+
)
30+
end
31+
32+
function _meteo_sampling_spec(clock::ClockSpec, model_spec)
33+
window = _runtime_meteo_window(meteo_window(model_spec))
34+
if isnothing(window)
35+
return PlantMeteo.MeteoSamplingSpec(float(clock.dt), float(clock.phase))
36+
end
37+
return PlantMeteo.MeteoSamplingSpec(float(clock.dt), float(clock.phase); window=window)
38+
end
1439

1540
function _normalize_meteo_reducer(reducer)
1641
if reducer isa DataType
@@ -67,22 +92,25 @@ function _sample_meteo_for_model(
6792
model_spec
6893
)
6994
transforms = _meteo_transforms_for_model(model_spec)
95+
window = _runtime_meteo_window(meteo_window(model_spec))
7096

7197
isnothing(meteo_sampler) && begin
72-
if !isnothing(transforms)
98+
if !isnothing(transforms) || !isnothing(window)
7399
@warn string(
74-
"MeteoBindings were provided but weather sampler API is unavailable or meteo is not TimeStepTable{Atmosphere}. ",
100+
"MeteoBindings or MeteoWindow were provided but weather sampler API is unavailable or meteo is not TimeStepTable{Atmosphere}. ",
75101
"Falling back to raw meteo rows."
76102
) maxlog = 1
77103
end
78104
return meteo
79105
end
80106

81107
# Fast-path: default 1:1 weather step with no custom transforms.
82-
if float(model_clock.dt) <= 1.0 && isnothing(transforms)
108+
if float(model_clock.dt) <= 1.0 &&
109+
isnothing(transforms) &&
110+
(isnothing(window) || window isa PlantMeteo.RollingWindow)
83111
return meteo
84112
end
85113

86-
spec = _meteo_sampling_spec(model_clock)
114+
spec = _meteo_sampling_spec(model_clock, model_spec)
87115
return PlantMeteo.sample_weather(meteo_sampler, i; spec=spec, transforms=transforms)
88116
end

0 commit comments

Comments
 (0)