Skip to content

Commit 5ae99df

Browse files
committed
duration is now mandatory in the meteo + Added runtime source resolution (explicit ModelSpec.timestep > non-default timespec > meteo base step)
1 parent 75a96e2 commit 5ae99df

8 files changed

Lines changed: 625 additions & 110 deletions

File tree

docs/src/API/API_public.md

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,14 +41,21 @@ Period conversion detail:
4141
so execution times are `t = 1, 25, 49, ...`.
4242

4343
Trait-based inference detail:
44-
- If `TimeStepModel(...)` is omitted, `timestep_hint(::Type{<:Model})` may provide:
45-
: fixed period (`Dates.Day(1)`) or required range (`(Dates.Minute(1), Dates.Hour(4))`).
44+
- If `TimeStepModel(...)` is omitted, runtime resolves timestep from:
45+
: `timespec(model)` when non-default, otherwise meteo `duration`.
46+
- `timestep_hint(::Type{<:Model})` is then interpreted as:
47+
: `required` = hard compatibility constraint, `preferred` = informational only.
4648
- If `InputBindings(...)` is omitted, same-name sources are inferred automatically from
4749
: unique producers (same scale first, then cross-scale). Ambiguous cases require explicit bindings.
4850
- If `MeteoBindings(...)` / `MeteoWindow(...)` are omitted, `meteo_hint(::Type{<:Model})`
4951
: may provide `(; bindings=..., window=...)`.
5052
- Explicit mapping-level configuration always overrides hints.
5153

54+
Compatibility checks:
55+
- Meteo `duration` is mandatory when meteo is provided.
56+
- For models with meteo-derived timestep, runtime enforces `timestep_hint.required`.
57+
- `timestep_hint.preferred` never sets runtime timestep by itself.
58+
5259
Scope selection detail:
5360
- `ScopeModel(:global)` is the default and shares streams across the whole simulation.
5461
- `ScopeModel(:plant)` isolates streams within each plant subtree.

docs/src/model_execution.md

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,21 +20,23 @@ For multiscale simulations, model usage is configured in the mapping through `Mo
2020
- `OutputRouting(...)`: sets whether an output is canonical (`:canonical`) or stream-only (`:stream_only`).
2121
- `ScopeModel(...)`: partitions producer streams by scope (`:global`, `:plant`, `:scene`, `:self`) for multi-entity simulations.
2222

23-
If users do not provide `TimeStepModel(...)`, `MeteoBindings(...)`, or `MeteoWindow(...)`,
23+
If users do not provide `MeteoBindings(...)` or `MeteoWindow(...)`,
2424
the runtime can infer defaults from model traits:
2525
- `timestep_hint(::Type{<:MyModel})`
2626
- `meteo_hint(::Type{<:MyModel})`
2727

28+
For timestep specifically, runtime is meteo-first (see decision flow below): `timestep_hint`
29+
is used for compatibility validation (and user guidance), not to auto-assign model clocks.
30+
2831
If users do not provide `InputBindings(...)`, runtime infers same-name bindings:
2932
- first from a unique producer at the same scale;
3033
- otherwise from a unique producer at another scale;
3134
- if no producer exists, input stays unresolved (so initialization/forced values can be used);
3235
- if multiple producers are possible, runtime errors and asks for explicit `InputBindings(...)`.
3336

3437
For timestep hints:
35-
- `Dates.FixedPeriod` sets a fixed inferred timestep, e.g. `Dates.Day(1)`.
36-
- `(min_period, max_period)` sets a required range. For models with only range hints,
37-
runtime computes a consensus (default: finest feasible period in the intersection).
38+
- `timestep_hint.required` is a hard compatibility constraint when runtime uses meteo-derived timestep.
39+
- `timestep_hint.preferred` is informational only (it does not set runtime timestep by itself).
3840
- Explicit `TimeStepModel(...)` always takes precedence.
3941

4042
For meteo hints:
@@ -57,6 +59,33 @@ Policy parameterization:
5759
(for example `Dates.Hour(1)`, `Dates.Day(1)`). Fixed periods are converted internally using
5860
the meteo base timestep duration.
5961

62+
### Timestep decision flow
63+
64+
When meteo is provided, `duration` is mandatory for each row (or the simulation errors).
65+
66+
Runtime picks each model effective clock with this order:
67+
68+
1. If `ModelSpec` has `TimeStepModel(...)`, use it.
69+
2. Else if `timespec(model)` is non-default, use it.
70+
3. Else use meteo base timestep (`duration`) for that model.
71+
72+
Then runtime applies constraints:
73+
74+
1. If the model clock is meteo-derived (rule 3), `timestep_hint.required` is validated:
75+
- fixed required period: meteo timestep must match exactly;
76+
- required range: meteo timestep must be inside the range.
77+
2. `timestep_hint.preferred` never overrides the clock when timestep is unset.
78+
3. Meteo aggregation/integration is applied only when effective model timestep is coarser than meteo timestep.
79+
80+
Practical consequences:
81+
82+
- Unset `TimeStepModel` + required includes meteo + preferred is coarser:
83+
model still runs at meteo timestep.
84+
- Explicit coarser `TimeStepModel(Dates.Hour(2))` with hourly meteo:
85+
model runs every 2 hours and receives aggregated meteo over that window.
86+
- Unset `TimeStepModel` + required excludes meteo:
87+
runtime errors with an actionable compatibility message.
88+
6089
Developer note on period conversion:
6190
- Runtime time is indexed on a 1-based timeline (`t = 1, 2, 3, ...`).
6291
- `TimeStepModel(Dates.Day(1))` is converted to a clock step count using:

docs/src/multirate/multirate_tutorial.md

Lines changed: 104 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,107 @@ This tutorial builds one MTG simulation that mixes three model rates:
77

88
It runs for one week and exports clean series at each rate.
99

10+
## Decision flow quick examples
11+
12+
### Unset model timestep uses meteo cadence (preferred is informational)
13+
14+
```@example multirate_timestep_flow
15+
using PlantSimEngine
16+
using PlantMeteo
17+
using MultiScaleTreeGraph
18+
using DataFrames
19+
using Dates
20+
21+
mtg = Node(NodeMTG("/", "Scene", 1, 0))
22+
plant = Node(mtg, NodeMTG("+", "Plant", 1, 1))
23+
internode = Node(plant, NodeMTG("/", "Internode", 1, 2))
24+
Node(internode, NodeMTG("+", "Leaf", 1, 2))
25+
26+
PlantSimEngine.@process "tutorialmeteodriven" verbose=false
27+
struct TutorialMeteoDrivenModel <: AbstractTutorialmeteodrivenModel
28+
n::Base.RefValue{Int}
29+
end
30+
PlantSimEngine.inputs_(::TutorialMeteoDrivenModel) = NamedTuple()
31+
PlantSimEngine.outputs_(::TutorialMeteoDrivenModel) = (count=-Inf,)
32+
function PlantSimEngine.run!(m::TutorialMeteoDrivenModel, models, status, meteo, constants=nothing, extra=nothing)
33+
m.n[] += 1
34+
status.count = float(m.n[])
35+
end
36+
PlantSimEngine.timestep_hint(::Type{<:TutorialMeteoDrivenModel}) = (; required=(Minute(30), Hour(2)), preferred=Hour(1))
37+
38+
mapping = ModelMapping("Leaf" => (ModelSpec(TutorialMeteoDrivenModel(Ref(0))),))
39+
meteo_30min = Weather([
40+
Atmosphere(date=DateTime(2025, 6, 12, 12, 0, 0), duration=Minute(30), T=20.0, Wind=1.0, Rh=0.6),
41+
Atmosphere(date=DateTime(2025, 6, 12, 12, 30, 0), duration=Minute(30), T=21.0, Wind=1.0, Rh=0.6),
42+
Atmosphere(date=DateTime(2025, 6, 12, 13, 0, 0), duration=Minute(30), T=22.0, Wind=1.0, Rh=0.6),
43+
])
44+
45+
out_meteo_driven = run!(
46+
mtg,
47+
mapping,
48+
meteo_30min;
49+
executor=SequentialEx(),
50+
tracked_outputs=Dict("Leaf" => (:count,)),
51+
)
52+
out_meteo_driven_df = PlantSimEngine.convert_outputs(out_meteo_driven, DataFrame)
53+
out_meteo_driven_df["Leaf"][end, :count]
54+
```
55+
56+
The last value is `3.0`, showing the model ran on all three 30-minute meteo rows, even though `preferred=Hour(1)`.
57+
58+
### Explicit coarse `TimeStepModel` triggers integration/aggregation
59+
60+
```@example multirate_timestep_flow
61+
PlantSimEngine.@process "tutorialhalfhoursource" verbose=false
62+
struct TutorialHalfHourSourceModel <: AbstractTutorialhalfhoursourceModel
63+
n::Base.RefValue{Int}
64+
end
65+
PlantSimEngine.inputs_(::TutorialHalfHourSourceModel) = NamedTuple()
66+
PlantSimEngine.outputs_(::TutorialHalfHourSourceModel) = (A=-Inf,)
67+
function PlantSimEngine.run!(m::TutorialHalfHourSourceModel, models, status, meteo, constants=nothing, extra=nothing)
68+
m.n[] += 1
69+
status.A = 1.0 # umol m-2 s-1
70+
end
71+
72+
PlantSimEngine.@process "tutorialhourlyintegrator" verbose=false
73+
struct TutorialHourlyIntegratorModel <: AbstractTutorialhourlyintegratorModel end
74+
PlantSimEngine.inputs_(::TutorialHourlyIntegratorModel) = (A=-Inf,)
75+
PlantSimEngine.outputs_(::TutorialHourlyIntegratorModel) = (A_hourly=-Inf, T_hourly=-Inf,)
76+
function PlantSimEngine.run!(::TutorialHourlyIntegratorModel, models, status, meteo, constants=nothing, extra=nothing)
77+
status.A_hourly = status.A
78+
status.T_hourly = meteo.T
79+
end
80+
81+
mapping_coarse = ModelMapping(
82+
"Leaf" => (
83+
ModelSpec(TutorialHalfHourSourceModel(Ref(0))),
84+
ModelSpec(TutorialHourlyIntegratorModel()) |>
85+
TimeStepModel(Hour(1)) |>
86+
InputBindings(; A=(process=:tutorialhalfhoursource, var=:A, policy=Integrate(vals -> sum(vals) * 1800.0))) |>
87+
MeteoBindings(; T=MeanWeighted()),
88+
),
89+
)
90+
91+
meteo_30min_4 = Weather([
92+
Atmosphere(date=DateTime(2025, 6, 12, 12, 0, 0), duration=Minute(30), T=20.0, Wind=1.0, Rh=0.6),
93+
Atmosphere(date=DateTime(2025, 6, 12, 12, 30, 0), duration=Minute(30), T=22.0, Wind=1.0, Rh=0.6),
94+
Atmosphere(date=DateTime(2025, 6, 12, 13, 0, 0), duration=Minute(30), T=24.0, Wind=1.0, Rh=0.6),
95+
Atmosphere(date=DateTime(2025, 6, 12, 13, 30, 0), duration=Minute(30), T=26.0, Wind=1.0, Rh=0.6),
96+
])
97+
98+
out_coarse = run!(
99+
mtg,
100+
mapping_coarse,
101+
meteo_30min_4;
102+
executor=SequentialEx(),
103+
tracked_outputs=Dict("Leaf" => (:A_hourly, :T_hourly)),
104+
)
105+
out_coarse_df = PlantSimEngine.convert_outputs(out_coarse, DataFrame)
106+
(out_coarse_df["Leaf"][end, :A_hourly], out_coarse_df["Leaf"][end, :T_hourly])
107+
```
108+
109+
The final tuple is `(3600.0, 23.0)`: hourly integrated assimilation (`1.0 * 1800 s * 2`) and hourly mean temperature over the coarse window.
110+
10111
## 1. Setup and example data
11112

12113
We reuse package example assets:
@@ -133,16 +234,9 @@ mapping = ModelMapping(
133234

134235
## 4. Run and export hourly/daily/weekly series
135236

136-
Use `GraphSimulation` directly so weather sampling (`MeteoBindings`) is active on the `TimeStepTable` meteo input.
237+
Run directly from `mtg + mapping` and request exported series.
137238

138239
```@example multirate_tutorial
139-
sim = PlantSimEngine.GraphSimulation(
140-
mtg,
141-
mapping,
142-
nsteps=PlantSimEngine.get_nsteps(meteo_hourly),
143-
check=true,
144-
)
145-
146240
req_leaf_hourly = OutputRequest("Leaf", :leaf_assim_h;
147241
name=:leaf_assim_hourly,
148242
process=leaf_proc,
@@ -171,7 +265,8 @@ req_plant_weekly = OutputRequest("Plant", :plant_assim_w;
171265
)
172266
173267
out_status, exported = run!(
174-
sim,
268+
mtg,
269+
mapping,
175270
meteo_hourly;
176271
multirate=true,
177272
executor=SequentialEx(),

docs/src/troubleshooting_and_testing/implicit_contracts.md

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,27 @@ Many models are considered to be steady-state over that timeframe, but not all :
1818
!!! Note
1919
Implicitely, this means any vector variables given as input to the simulation must be consistent with the number of weather timesteps. Providing one weather value but a larger vector variable is an exception : the weather data is replicated over each timestep. (This may be subject to change in the future when support for different timesteps in a single simulation is implemented)
2020

21+
## Why does my model skip half-hour rows?
22+
23+
If your meteo has 30-minute rows but a model appears to run hourly, check timestep resolution order:
24+
25+
1. If model has explicit `TimeStepModel(...)`, it is used.
26+
2. Else if model `timespec(model)` is non-default, it is used.
27+
3. Else model uses meteo `duration`.
28+
29+
Then compatibility rules apply:
30+
31+
1. `timestep_hint.required` is enforced for meteo-derived clocks.
32+
2. `timestep_hint.preferred` is informational only.
33+
3. Meteo aggregation/integration happens only for models with coarser effective clocks.
34+
35+
Common cause:
36+
- model has explicit hourly `TimeStepModel(...)`, so 30-minute rows are intentionally aggregated to hourly runs.
37+
38+
Quick diagnostics:
39+
- Run `explain_model_specs(mapping_or_sim)` to see, per process, whether runtime clock comes from explicit `ModelSpec`, model `timespec`, or meteo base step.
40+
- Ensure meteo `duration` is present and valid on every row (mandatory when meteo is provided).
41+
2142
## Weather data must be interpolated prior to simulation
2243

2344
If your weather data isn't adjusted to conform to a regular timestep, you will need to adjust it to fit that constraint. PlantSimEngine does no interpolation prior to simulation and expects regular weather timesteps.
@@ -72,4 +93,4 @@ This order of simulation depends on the way the models link together. If you rep
7293

7394
When iterating and slowly making a simulation more physiologically realistic and complex, it is therefore fully possible that the order in which two models are run is flipped by a user change.
7495

75-
This design choice implementation -a concession made for ease of use and flexibility when developing a simulation- means that until your set of models is fully stabilized and you know which variables are `PreviousTimestep` and what order models run in, as you expand and change the set you might see differences of execution of one timestep for some models. It isn't a conceptual problem as most models are steady-state, and simulation order is stable for a given set of models, but it does mean PlantSimEngine will be less conveient for some types of simulation.
96+
This design choice implementation -a concession made for ease of use and flexibility when developing a simulation- means that until your set of models is fully stabilized and you know which variables are `PreviousTimestep` and what order models run in, as you expand and change the set you might see differences of execution of one timestep for some models. It isn't a conceptual problem as most models are steady-state, and simulation order is stable for a given set of models, but it does mean PlantSimEngine will be less conveient for some types of simulation.

0 commit comments

Comments
 (0)