Repository navigation
Conversation
| provider: str = Field(..., description="The provider of the NWP data") | ||
|
|
||
| max_staleness_minutes: int | None = Field( | ||
| staleness_limit_minutes: int | None = Field( |
There was a problem hiding this comment.
I've changed the name of this to reflect the fact that it is now exclusive whereas previously it was inclusive. I.e. if set to 720(=12 hours), previously this would have allowed an NWP with init-time from 12 hours ago, now it does not. I chose to change this because it matches better to the new half-open period schema
| t0_embedding: T0Embedding | None = None | ||
|
|
||
| @model_validator(mode="after") | ||
| def validate_t0_resolution_alignment(self) -> "PVNetDataConfig": |
There was a problem hiding this comment.
The t0 times are no-longer restricted in this way
| requested_periods, columns=["start_dt", "end_dt"], dtype="datetime64[ns]" | ||
| ) | ||
| # Filling includes endpoints; keep adjacent half-open periods from sharing a timestamp. | ||
| requested_periods_df["end_dt"] -= np.timedelta64(1, "ns") |
There was a problem hiding this comment.
The half-open intervals mean we don't need to do this hack anymore
| # Shift the boundaries to give the range of valid t0 values in each period | ||
| t0_period_starts = period_starts[mask] - interval_start | ||
| t0_period_ends = period_ends[mask] - interval_end | ||
| t0_period_ends = period_ends[mask] - interval_end + time_resolution |
There was a problem hiding this comment.
Adding time_resolution here accounts for the time slice flooring and the half open intervals
|
|
||
| # This is the max staleness we can use considering the max step of the input data | ||
| max_possible_staleness = last_forecast_step - interval_end | ||
| max_possible_staleness = last_forecast_step - interval_end + time_resolution |
There was a problem hiding this comment.
This changed due to the time slice flooring and half open intervals
| a: |-----| |----| |----------| |-----------| | ||
| b: |--------| |----| |---| | ||
| intersection: |--| |-| |--| |---| | ||
| a: [-----) [----) [----------) [-----------) |
|
|
||
|
|
||
| @pytest.mark.parametrize("source, resolution", [("generation", 15), ("satellite", 3)]) | ||
| def test_sampling_grid_rejects_resolution_non_multiple(config_filename, source, resolution): |
There was a problem hiding this comment.
This is no longer a restriction
There was a problem hiding this comment.
I cleaned up this file a bit to use some helpers and to move us away from the pandas datetimes and towards the numpy datetimes we expect to use within this library
| ) | ||
|
|
||
|
|
||
| def test_find_contiguous_t0_periods(): |
There was a problem hiding this comment.
This function hasn't actually changed except for the end-times in the expected results which reflect the half-open periods. They've all increased by 5 mins (the frequency)
| expected_results = [exp_res1, exp_res2, exp_res3, exp_res4, exp_res5] | ||
|
|
||
| # Create 3-hourly init times with a few time stamps missing | ||
| init_times = ( |
There was a problem hiding this comment.
I decided to make this more explicit
| # b: |--| | ||
| # a: [----) | ||
| # b: [-) | ||
| a = construct_time_periods_df(start_dt=["2025-01-01 00:00"], end_dt=["2025-01-01 12:00"]) |
There was a problem hiding this comment.
None of these times actually change since the input and expected periods are now both half-open. The input and output times are identical but how we interpret them HAS changed
| exp_res = construct_time_periods_df([], []) | ||
| assert_expected_result_with_reverse(a, b, expected_result=exp_res) | ||
|
|
||
| # a: | |
There was a problem hiding this comment.
Since we've moved to half-open periods we cannot have periods which start and end at the same datetime, so a lot of these cases disappear
| assert len(filled) == 0 | ||
|
|
||
|
|
||
| def test_find_contiguous_t0_periods_matches_slicing(): |
There was a problem hiding this comment.
These next two test functions are the real crux of this PR. We check that the t0 times we calculate should be available match the t0 times that we can actually slice from the data
|
|
||
| def test_select_time_slice_rejects_off_grid_request(): | ||
| """Test that off-grid requests are rejected instead of rounded""" | ||
| def test_select_time_slice_floors_off_grid_request(): |
There was a problem hiding this comment.
We now support off-grid t0s
| ("12:00", 120, -120, "09:00"), | ||
| ("12:00", 180, -120, "09:00"), | ||
| ("12:00", 300, -120, "06:00"), | ||
| ("12:30", 30, 0, "12:00"), # At 12:30, the 12:00 forecast will be available. |
There was a problem hiding this comment.
This function is basically the same except for this additional case. Although we floor the requested time window, we don't affect when an init-time becomes available.
Say for example we accidentally floored rather than flooring the time window. If we had floored t0 first then we'd have calculated t0_available as 11:30 and we'd select the 09:00 init time
|
|
||
|
|
||
| # Helpers | ||
| def to_datetime64(datetimes: str | list[str], unit="ns"): |
There was a problem hiding this comment.
This is just a new helper. I put it into the test_time_periods function I was already updating. I didn't want to touch lots of unnecessary files to use it everywhere
| ) | ||
|
|
||
| @field_validator("time_resolution_minutes") | ||
| def validate_resolution(cls, resolution: int) -> int: |
There was a problem hiding this comment.
I've moved this check onto the TimeWindowMixin instead of having it separately on satellite and NWP. Now everything that uses TimeWindowMixin has this requirement - namely the solar features and datetime features need this now
Pull Request
Context
Time slicing
In the current version of the library we have some slight weird behaviour in how we do time slicing.
The second point limits the samples we can create. For example, if we had an satellite data that was 5-minutely, but generation data that was 30-minutely (as in the UK) we currently cannot use a t0 time of HH:15. So we cannot make predictions more frequently than the time resolution of the generation data. This is something we have wanted to be able to do, to perhaps run a forecast every 5 minutes to make use of newly available satellite images.
We can allow this by snapping the non-NWP time slices onto the source grid. However unlike with NWP I think the correct behaviour has to be to floor the time slice rather than ceil it. Otherwise we could put future (after t0) observations into the input part of the sample. For example, we set the input generation slice to be -30 -> 0 minutes. If we ran with t0 of HH:15 and ceiled the requested slice this would put the HH:30 into the input generation slice. More broadly flooring is consistent with how we define t0. We expect to be able to use observations up to and including t0. So flooring the time slice window would be the correct option.
Since we need to floor the other data sources, I think it makes sense to floor the NWP window slice so that we don't have competing standards. This will break backwards compatibility to trained model but data-sampler v2 does not aim to maintain that anyway. I think we should do this for cleanliness. Plus when we diff the accumulated NWP channels we floor the timestamp the value is assigned to. So for example, when we diff shortwave, our current scheme says this is amount of energy which will bit the ground in the coming hour. Therefore I do think its, more natural to floor the NWP time slice to be in keeping with this too. For a t0 at 12:15 predicting for the next 2 hours (12:30 - 14:15) and a 2-NWP window from 0-120 minutes, then we'd get the irradiance accumulated between [12-13, 13-14, 14-15] when using floor. When using ceil, as we do now we get the accumulated irradiance [13-14, 14-15, 15-16].
Calculating periods
The current version of the library calculates the available t0 times based on closed intervals. For example, the
find_contiguous_t0_periods()andfind_contiguous_t0_periods_nwp()function here. I think these closed periods don't serve us well when we start flooring the time windows. For example, image 30-minutely generation data which stops at 12:00 and for which we configure a 60 minute slice. With our current close intervals we would calculate that the max t0 we could fulfil would be 11:00. However when flooring the time slice we can have t0 times < 11:30, for example 11:25 would be fine. So I think it is better to express the time periods as half-open slices[start, end)where the start period is inclusive and the end is exclusive. This perfectly matches to the scheme of flooring all time windows for all data sources.The PR
This PR implements the changes described above.
[start, end)Checklist: