Weather Sampling

Use weather sampling when your model needs more than simple one-row-per-day aggregation. Sampling lets you define rolling or calendar windows, control reducers per variable, and cache repeated queries for long simulation loops.

When To Use Sampling Instead Of to_daily

Use to_daily when you want standard daily weather summaries and one row per civil day.

Use sampling when:

  • the model timestep is not just daily
  • you need a trailing window such as "the last 24 hourly steps"
  • you need calendar windows such as current day, previous week, or current month
  • different variables require different reducers
  • repeated simulation queries should be cached

Sampling Workflow

  1. define the source weather table
  2. prepare a sampler object
  3. choose a window specification
  4. sample one step or materialize full sampled tables

1. Create a Small, Predictable Weather Series

For this example we'll use simple values so aggregation effects are easy to verify. We make 48 hourly rows (two days) with monotonic T:

using PlantMeteo
using Dates
using Statistics

base = DateTime(2025, 1, 1)
meteo = Weather([
    Atmosphere(
        date = base + Hour(i),
        duration = Hour(1),
        T = 10.0 + i,
        Wind = 1.0,
        Rh = 0.50 + 0.005 * i,
        P = 100.0,
        Ri_SW_f = 100.0 + 10.0 * i
    )
    for i in 0:47
])

meteo[1:6]
TimeStepTable{Atmosphere{(:date, :duration,...}(6 x 22):
date duration T Wind P Rh Precipitations Cₐ e eₛ VPD ρ λ γ ε Δ clearness Ri_SW_f Ri_PAR_f Ri_NIR_f Ri_TIR_f Ri_custom_f
DateTime Hour Float64 Float64 Float64 Float64 Float64 Float64 Float64 Float64 Float64 Float64 Float64 Float64 Float64 Float64 Float64 Float64 Float64 Float64 Float64 Float64
1 2025-01-01T00:00:00 1 hour 10.0 1.0 100.0 0.5 0.0 400.0 0.61635 1.2327 0.61635 1.23031 2.47735e6 0.0657403 0.516341 0.0827838 Inf 100.0 Inf Inf Inf Inf
2 2025-01-01T01:00:00 1 hour 11.0 1.0 100.0 0.505 0.0 400.0 0.665446 1.31772 0.652269 1.22598 2.47498e6 0.0658031 0.521763 0.08779 Inf 110.0 Inf Inf Inf Inf
3 2025-01-01T02:00:00 1 hour 12.0 1.0 100.0 0.51 0.0 400.0 0.718005 1.40785 0.689848 1.22168 2.47262e6 0.0658661 0.527195 0.0930528 Inf 120.0 Inf Inf Inf Inf
4 2025-01-01T03:00:00 1 hour 13.0 1.0 100.0 0.515 0.0 400.0 0.774236 1.50337 0.729135 1.21741 2.47026e6 0.0659291 0.532638 0.0985828 Inf 130.0 Inf Inf Inf Inf
5 2025-01-01T04:00:00 1 hour 14.0 1.0 100.0 0.52 0.0 400.0 0.834363 1.60454 0.770181 1.21317 2.46789e6 0.0659923 0.538091 0.104391 Inf 140.0 Inf Inf Inf Inf
6 2025-01-01T05:00:00 1 hour 15.0 1.0 100.0 0.525 0.0 400.0 0.898619 1.71165 0.813036 1.20896 2.46552e6 0.0660556 0.543555 0.110488 Inf 150.0 Inf Inf Inf Inf

2. Prepare Sampler State

We can normalize transforms and enable query-level memoization with prepare_weather_sampler. This is optional but useful when the same weather is sampled many times during a simulation.

prepared = prepare_weather_sampler(meteo)  # lazy cache enabled by default
typeof(prepared)
PreparedWeather{TimeStepTable{Atmosphere{(:date, :duration, :T, :Wind, :P, :Rh, :Precipitations, :Cₐ, :e, :eₛ, :VPD, :ρ, :λ, :γ, :ε, :Δ, :clearness, :Ri_SW_f, :Ri_PAR_f, :Ri_NIR_f, :Ri_TIR_f, :Ri_custom_f), Tuple{DateTime, Hour, Vararg{Float64, 20}}}}, Vector{MeteoTransform}, Dict{Tuple{Int64, UInt64, UInt64}, Any}, Dict{UInt64, Any}}

3. Rolling Window Sampling

We can then aggregate over a trailing window in source-step units using sample_weather, which returns one aggregated Atmosphere.

window2 = RollingWindow(2.0)
row3 = sample_weather(prepared, 3; window = window2)
row3_cached = sample_weather(prepared, 3; window = window2)
Atmosphere(date = DateTime("2025-01-01T02:00:00"), duration = Millisecond(7200000), T = 11.5, Wind = 1.0, P = 100.0, Rh = 0.5075, Precipitations = 0.0, Cₐ = 400.0, e = 0.6917254565512273, eₛ = 1.3627838142874376, VPD = 0.6710583577362105, ρ = 1.2238257574344642, λ = 2.4738025e6, γ = 0.0658345900883861, ε = 0.5244788008257141, Δ = 0.09042138305283774, clearness = Inf, Ri_SW_f = 115.0, Ri_PAR_f = Inf, Ri_NIR_f = Inf, Ri_TIR_f = Inf, Ri_custom_f = Inf, Rhmax = 0.51, Rhmin = 0.505, Ri_NIR_q = Inf, Ri_PAR_q = Inf, Ri_SW_q = 0.828, Ri_TIR_q = Inf, Ri_custom_q = Inf, Tmax = 12.0, Tmin = 11.0)
row3.T
11.5
row3.Tmin
11.0
row3.Tmax
12.0
row3_cached === row3
true

sample_weather provides default transforms for common atmospheric variables, but you can override them when your model semantics differ.

4. Override Variable-wise Aggregation Rules

We can match aggregation logic to model semantics by overriding variable-wise aggregation rules.

custom = (
    T = MeanWeighted(),
    Tmax = (source = :T, reducer = MaxReducer()),
    Tsum = (source = :T, reducer = SumReducer())
)

row_custom = sample_weather(prepared, 3; window = window2, transforms = custom)
Atmosphere(date = DateTime("2025-01-01T02:00:00"), duration = Millisecond(7200000), T = 11.5, Wind = 1.0, P = 100.0, Rh = 0.51, Precipitations = 0.0, Cₐ = 400.0, e = 0.6946850911337292, eₛ = 1.3621276296739788, VPD = 0.6674425385402496, ρ = 1.223821981388938, λ = 2.4738025e6, γ = 0.06583457504566559, ε = 0.5248457833468954, Δ = 0.09038865037775068, clearness = Inf, Ri_SW_f = Inf, Ri_PAR_f = Inf, Ri_NIR_f = Inf, Ri_TIR_f = Inf, Ri_custom_f = Inf, Tmax = 12.0, Tsum = 23.0)
row_custom.T
11.5
row_custom.Tmax
12.0
row_custom.Tsum
23.0

5. Calendar Window Sampling

Sometimes model logic is tied to civil periods (day/week/month) rather than fixed trailing windows. In that case, use CalendarWindow to aggregate all source timesteps that fall within the same period.

window_day = CalendarWindow(:day)
day_sample = sample_weather(prepared, 5; window = window_day)
Atmosphere(date = DateTime("2025-01-01T04:00:00"), duration = Millisecond(86400000), T = 21.5, Wind = 1.0, P = 100.0, Rh = 0.5575, Precipitations = 0.0, Cₐ = 400.0, e = 1.5882901485029282, eₛ = 2.7788527315526586, VPD = 1.1905625830497304, ρ = 1.1829403837798358, λ = 2.4501525e6, γ = 0.06647300839817989, ε = 0.5794989608153686, Δ = 0.16728180969800288, clearness = Inf, Ri_SW_f = 215.0, Ri_PAR_f = Inf, Ri_NIR_f = Inf, Ri_TIR_f = Inf, Ri_custom_f = Inf, Rhmax = 0.615, Rhmin = 0.5, Ri_NIR_q = Inf, Ri_PAR_q = Inf, Ri_SW_q = 18.576, Ri_TIR_q = Inf, Ri_custom_q = Inf, Tmax = 33.0, Tmin = 10.0)
day_sample.T
21.5
day_sample.Tmin
10.0
day_sample.Tmax
33.0
day_sample.duration
86400000 milliseconds

We sample the fifth hour of the series, which falls on the first day. The sampled T is the average of the 24 hourly steps that fall within that day, which matches our expectation for a daily aggregation:

mean(meteo[i].T for i in 1:24)
21.5

The result would be the same for any hour from 1 to 24, since they all fall in the same day:

day_sample2 = sample_weather(prepared, 20; window = window_day)
day_sample2.T == day_sample.T
true

The aggregated results are cached, so repeated calls with the same window and query step will return the same values, and it means that performance will be ensured for long simulation loops with repeated calls:

day_sample_cached = sample_weather(prepared, 6; window = window_day)
day_sample_cached.T === day_sample.T
true

The returned Atmosphere is different though, since the date (and sometimes duration) are different for each query step, even if the aggregated values are the same:

day_sample.date
2025-01-01T04:00:00
day_sample_cached.date
2025-01-01T05:00:00
day_sample2.date
2025-01-01T19:00:00

Note that CalendarWindow expects a date::DateTime column in the weather. By default, it checks for completeness of the period, but you can use completeness = :allow_partial to authorize incomplete periods. This is also faster since it doesn't have to perform checks.

6. Precompute for Simulation Loops

When running long simulations, you can precompute all sampled weather tables upfront rather than sampling on-demand at each step. This trades a one-time preprocessing cost for faster lookups during the simulation loop. The materialize_weather function returns one sampled table per requested sampling window, allowing efficient access throughout your simulation.

windows = [window2, window_day]
tables = materialize_weather(prepared; windows = windows)
Dict{AbstractSamplingWindow, TimeStepTable{Atmosphere}} with 2 entries:
  CalendarWindow(:day, :cu… => TimeStepTable{Atmosphere}((:date, :duration, :T,…
  RollingWindow(2.0)        => TimeStepTable{Atmosphere}((:date, :duration, :T,…
length(tables)
2
length(tables[window2])
48
tables[window2][3].T ≈ row3.T
true

This is typically the best approach when you perform many simulations with the same weather, for example model calibration, sensitivity analysis, or repeated scenario runs.