Weather Sampling

This guide explains how to align raw weather time steps with model requirements.

Why Sampling?

A model often needs weather at a different temporal scale than the source data. Examples:

  • source meteo is hourly, model step is 3 hours
  • source meteo is sub-hourly, model step is daily
  • a simulation needs both rolling and calendar-based aggregates

The sampling API makes these choices explicit and testable.

Sampling Workflow

  1. define the source weather table
  2. prepare a sampler object (with optional caching)
  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
TimeStepTable{Atmosphere{(:date, :duration,...}(48 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
7 2025-01-01T06:00:00 1 hour 16.0 1.0 100.0 0.53 0.0 400.0 0.967248 1.825 0.857749 1.20478 2.46316e6 0.066119 0.549028 0.116887 Inf 160.0 Inf Inf Inf Inf
8 2025-01-01T07:00:00 1 hour 17.0 1.0 100.0 0.535 0.0 400.0 1.04051 1.94488 0.904368 1.20062 2.4608e6 0.0661826 0.554511 0.123598 Inf 170.0 Inf Inf Inf Inf
9 2025-01-01T08:00:00 1 hour 18.0 1.0 100.0 0.54 0.0 400.0 1.11867 2.07162 0.952943 1.1965 2.45843e6 0.0662462 0.560003 0.130635 Inf 180.0 Inf Inf Inf Inf
10 2025-01-01T09:00:00 1 hour 19.0 1.0 100.0 0.545 0.0 400.0 1.20202 2.20554 1.00352 1.1924 2.45606e6 0.06631 0.565504 0.138009 Inf 190.0 Inf Inf Inf Inf
11 2025-01-01T10:00:00 1 hour 20.0 1.0 100.0 0.55 0.0 400.0 1.29085 2.347 1.05615 1.18834 2.4537e6 0.0663739 0.571015 0.145734 Inf 200.0 Inf Inf Inf Inf
12 2025-01-01T11:00:00 1 hour 21.0 1.0 100.0 0.555 0.0 400.0 1.38547 2.49634 1.11087 1.1843 2.45134e6 0.066438 0.576534 0.153823 Inf 210.0 Inf Inf Inf Inf
13 2025-01-01T12:00:00 1 hour 22.0 1.0 100.0 0.56 0.0 400.0 1.48621 2.65394 1.16773 1.18028 2.44897e6 0.0665021 0.582061 0.162289 Inf 220.0 Inf Inf Inf Inf
14 2025-01-01T13:00:00 1 hour 23.0 1.0 100.0 0.565 0.0 400.0 1.5934 2.82018 1.22678 1.1763 2.4466e6 0.0665664 0.587597 0.171147 Inf 230.0 Inf Inf Inf Inf
15 2025-01-01T14:00:00 1 hour 24.0 1.0 100.0 0.57 0.0 400.0 1.70741 2.99546 1.28805 1.17234 2.44424e6 0.0666308 0.593142 0.180411 Inf 240.0 Inf Inf Inf Inf
16 2025-01-01T15:00:00 1 hour 25.0 1.0 100.0 0.575 0.0 400.0 1.82861 3.1802 1.35158 1.16841 2.44188e6 0.0666954 0.598694 0.190095 Inf 250.0 Inf Inf Inf Inf
17 2025-01-01T16:00:00 1 hour 26.0 1.0 100.0 0.58 0.0 400.0 1.95739 3.37481 1.41742 1.1645 2.43951e6 0.06676 0.604253 0.200215 Inf 260.0 Inf Inf Inf Inf
18 2025-01-01T17:00:00 1 hour 27.0 1.0 100.0 0.585 0.0 400.0 2.09415 3.57974 1.48559 1.16062 2.43714e6 0.0668248 0.609821 0.210787 Inf 270.0 Inf Inf Inf Inf
19 2025-01-01T18:00:00 1 hour 28.0 1.0 100.0 0.59 0.0 400.0 2.23932 3.79546 1.55614 1.15677 2.43478e6 0.0668897 0.615395 0.221826 Inf 280.0 Inf Inf Inf Inf
20 2025-01-01T19:00:00 1 hour 29.0 1.0 100.0 0.595 0.0 400.0 2.39334 4.02243 1.62908 1.15294 2.43242e6 0.0669547 0.620977 0.233348 Inf 290.0 Inf Inf Inf Inf
21 2025-01-01T20:00:00 1 hour 30.0 1.0 100.0 0.6 0.0 400.0 2.55669 4.26114 1.70446 1.14914 2.43005e6 0.0670199 0.626565 0.24537 Inf 300.0 Inf Inf Inf Inf
22 2025-01-01T21:00:00 1 hour 31.0 1.0 100.0 0.605 0.0 400.0 2.72983 4.51211 1.78228 1.14536 2.42768e6 0.0670852 0.632161 0.257909 Inf 310.0 Inf Inf Inf Inf
23 2025-01-01T22:00:00 1 hour 32.0 1.0 100.0 0.61 0.0 400.0 2.91328 4.77586 1.86259 1.14161 2.42532e6 0.0671506 0.637763 0.270983 Inf 320.0 Inf Inf Inf Inf
24 2025-01-01T23:00:00 1 hour 33.0 1.0 100.0 0.615 0.0 400.0 3.10755 5.05293 1.94538 1.13788 2.42296e6 0.0672162 0.643371 0.28461 Inf 330.0 Inf Inf Inf Inf
25 2025-01-02T00:00:00 1 hour 34.0 1.0 100.0 0.62 0.0 400.0 3.31321 5.34388 2.03067 1.13417 2.42059e6 0.0672818 0.648985 0.298807 Inf 340.0 Inf Inf Inf Inf
26 2025-01-02T01:00:00 1 hour 35.0 1.0 100.0 0.625 0.0 400.0 3.53081 5.64929 2.11848 1.13049 2.41822e6 0.0673476 0.654605 0.313593 Inf 350.0 Inf Inf Inf Inf
27 2025-01-02T02:00:00 1 hour 36.0 1.0 100.0 0.63 0.0 400.0 3.76095 5.96976 2.20881 1.12683 2.41586e6 0.0674136 0.660231 0.328987 Inf 360.0 Inf Inf Inf Inf
28 2025-01-02T03:00:00 1 hour 37.0 1.0 100.0 0.635 0.0 400.0 4.00425 6.30591 2.30166 1.1232 2.4135e6 0.0674796 0.665863 0.34501 Inf 370.0 Inf Inf Inf Inf
29 2025-01-02T04:00:00 1 hour 38.0 1.0 100.0 0.64 0.0 400.0 4.26135 6.65836 2.39701 1.11959 2.41113e6 0.0675458 0.6715 0.36168 Inf 380.0 Inf Inf Inf Inf
30 2025-01-02T05:00:00 1 hour 39.0 1.0 100.0 0.645 0.0 400.0 4.53292 7.02779 2.49487 1.116 2.40876e6 0.0676121 0.677142 0.379018 Inf 390.0 Inf Inf Inf Inf
31 2025-01-02T06:00:00 1 hour 40.0 1.0 100.0 0.65 0.0 400.0 4.81966 7.41486 2.5952 1.11244 2.4064e6 0.0676786 0.68279 0.397045 Inf 400.0 Inf Inf Inf Inf
32 2025-01-02T07:00:00 1 hour 41.0 1.0 100.0 0.655 0.0 400.0 5.12228 7.82028 2.698 1.1089 2.40404e6 0.0677452 0.688442 0.415781 Inf 410.0 Inf Inf Inf Inf
33 2025-01-02T08:00:00 1 hour 42.0 1.0 100.0 0.66 0.0 400.0 5.44154 8.24476 2.80322 1.10538 2.40167e6 0.0678119 0.694099 0.435249 Inf 420.0 Inf Inf Inf Inf
34 2025-01-02T09:00:00 1 hour 43.0 1.0 100.0 0.665 0.0 400.0 5.77822 8.68905 2.91083 1.10188 2.3993e6 0.0678787 0.69976 0.455471 Inf 430.0 Inf Inf Inf Inf
35 2025-01-02T10:00:00 1 hour 44.0 1.0 100.0 0.67 0.0 400.0 6.13311 9.1539 3.02079 1.09841 2.39694e6 0.0679457 0.705426 0.476468 Inf 440.0 Inf Inf Inf Inf
36 2025-01-02T11:00:00 1 hour 45.0 1.0 100.0 0.675 0.0 400.0 6.50707 9.64011 3.13304 1.09496 2.39458e6 0.0680128 0.711096 0.498264 Inf 450.0 Inf Inf Inf Inf
37 2025-01-02T12:00:00 1 hour 46.0 1.0 100.0 0.68 0.0 400.0 6.90097 10.1485 3.24751 1.09153 2.39221e6 0.06808 0.71677 0.520882 Inf 460.0 Inf Inf Inf Inf
38 2025-01-02T13:00:00 1 hour 47.0 1.0 100.0 0.685 0.0 400.0 7.3157 10.6799 3.36415 1.08812 2.38984e6 0.0681474 0.722448 0.544346 Inf 470.0 Inf Inf Inf Inf
39 2025-01-02T14:00:00 1 hour 48.0 1.0 100.0 0.69 0.0 400.0 7.7522 11.2351 3.48287 1.08473 2.38748e6 0.0682149 0.72813 0.56868 Inf 480.0 Inf Inf Inf Inf
40 2025-01-02T15:00:00 1 hour 49.0 1.0 100.0 0.695 0.0 400.0 8.21145 11.815 3.60359 1.08136 2.38512e6 0.0682826 0.733815 0.593908 Inf 490.0 Inf Inf Inf Inf
41 2025-01-02T16:00:00 1 hour 50.0 1.0 100.0 0.7 0.0 400.0 8.69444 12.4206 3.72619 1.07802 2.38275e6 0.0683503 0.739504 0.620055 Inf 500.0 Inf Inf Inf Inf
42 2025-01-02T17:00:00 1 hour 51.0 1.0 100.0 0.705 0.0 400.0 9.20222 13.0528 3.85058 1.07469 2.38038e6 0.0684182 0.745196 0.647148 Inf 510.0 Inf Inf Inf Inf
43 2025-01-02T18:00:00 1 hour 52.0 1.0 100.0 0.71 0.0 400.0 9.73587 13.7125 3.97662 1.07139 2.37802e6 0.0684863 0.750891 0.675211 Inf 520.0 Inf Inf Inf Inf
44 2025-01-02T19:00:00 1 hour 53.0 1.0 100.0 0.715 0.0 400.0 10.2965 14.4007 4.1042 1.0681 2.37566e6 0.0685545 0.756588 0.704272 Inf 530.0 Inf Inf Inf Inf
45 2025-01-02T20:00:00 1 hour 54.0 1.0 100.0 0.72 0.0 400.0 10.8853 15.1184 4.23316 1.06484 2.37329e6 0.0686228 0.762289 0.734356 Inf 540.0 Inf Inf Inf Inf
46 2025-01-02T21:00:00 1 hour 55.0 1.0 100.0 0.725 0.0 400.0 11.5034 15.8667 4.36334 1.06159 2.37092e6 0.0686912 0.767992 0.765492 Inf 550.0 Inf Inf Inf Inf
47 2025-01-02T22:00:00 1 hour 56.0 1.0 100.0 0.73 0.0 400.0 12.152 16.6466 4.49458 1.05837 2.36856e6 0.0687598 0.773698 0.797707 Inf 560.0 Inf Inf Inf Inf
48 2025-01-02T23:00:00 1 hour 57.0 1.0 100.0 0.735 0.0 400.0 12.8325 17.4592 4.62669 1.05516 2.3662e6 0.0688285 0.779406 0.831029 Inf 570.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 can speed up repeated calls with the same windows. This is done so that repeated identical calls can return cached objects (lazy=true).

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)

(row3.T, row3.Tmin, row3.Tmax, row3_cached === row3)
(11.5, 11.0, 12.0, true)

sample_weather provides default transforms for common variables (i.e. the ones in Atmosphere), but you can override them with custom rules (see next section).

4. Override Variable-wise Aggregation Rules

We can match aggregation logic to model semantics by overriding variable-wise aggregation rules. Custom transforms are normalized and applied for this call.

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

row_custom = sample_weather(prepared, 3; window = window2, transforms = custom)
(row_custom.T, row_custom.Tmax, row_custom.Tsum)
(11.5, 12.0, 23.0)

5. Calendar Window Sampling

Sometimes model logic is tied to civil periods (day/week/month) rather than fixed trailing windows. In this case, we can use CalendarWindow to aggregate all source steps that fall within the same period. This is useful for e.g. daily models that need to aggregate all hourly steps that fall within the same day, regardless of how many there are or where they fall in the source data. For example if you need the average temperature for the day, you can use a CalendarWindow anchored to the current period:

window_day = CalendarWindow(:day)
day_sample = sample_weather(prepared, 5; window = window_day)
(day_sample.T, day_sample.Tmin, day_sample.Tmax, day_sample.duration)
(21.5, 10.0, 33.0, Millisecond(86400000))

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, day_sample_cached.date, day_sample2.date
(DateTime("2025-01-01T04:00:00"), DateTime("2025-01-01T05:00:00"), DateTime("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)

(length(tables), length(tables[window2]), tables[window2][3].T ≈ row3.T)
(2, 48, true)

This is typically the best approach when you perform many simulations with the same weather, e.g. for model calibration, sensitivity analysis, or system optimization.