Repository navigation
Clean up config and add extra validation; refactor dropout params - #450
Conversation
| from numpy.typing import NDArray | ||
|
|
||
| # A single path, a local glob, or an explicit sequence of Zarr store paths | ||
| ZarrSource: TypeAlias = str | list[str] | tuple[str, ...] |
There was a problem hiding this comment.
Moved this from xr_tensorstore since we also use it in the config. It feels like it fits better in this types module
| ) | ||
|
|
||
| @field_validator("dropout_timedeltas_minutes") | ||
| def dropout_timedeltas_minutes_negative(cls, v: list[int]) -> list[int]: |
There was a problem hiding this comment.
The _MinuteProbabilities class does its own validation
| description="The value used to fill in dropped out data or any missing values. This is " | ||
| "applied after normalisation, so it is in normalised units." | ||
| fill_value: float | None = Field( | ||
| default=None, |
There was a problem hiding this comment.
We default to no infilling. I want to make infilling very explicit so its never glossed over. Also this isn't dropout specific, it fills any NaN values at all hence name change
There was a problem hiding this comment.
Agree on making infilling explicit. Could we add a check that a finished sample has no NaNs left, so a missing fill value fails loudly rather than showing up later as a NaN loss? Also worth a line in the migration notes maybe.
There was a problem hiding this comment.
Scanning through each array for NaNs can have a significant performance hit. I mean to refactor how we fill NaNs since right now the nan_to_num function we're using has a weird side effect of replacing infinites as well. So I'd rather leave it to be considered then.
I'm also not maintaining migration notes for the dev branch, and a lot more than this has changed. If we want them I'll have to build them up
|
|
||
| class Base(BaseModel): | ||
| """Pydantic Base model where no extras can be added.""" | ||
| def _validate_grid_resolution_minutes(resolution: int) -> int: |
There was a problem hiding this comment.
This is an extra validator on the spacing of our temporal resolutions. We generally aren't set up to deal with time resolutions of say 11 minutes anywhere. We cn deal with the simple cases which are factors of an hour or multiple of an hour.
| dropout_minute_probabilities: dict[int, float] | None, | ||
| interval_end_minutes: int, | ||
| ) -> None: | ||
| """Reject choices that would leave every observation in the window untouched.""" |
There was a problem hiding this comment.
This is extra validation. We should know if we've configured a dropout which does nothing
| ] | ||
|
|
||
| _Embedding = tuple[ | ||
| Annotated[str, Field(pattern=r"^[0-9]+[hy]$")], |
There was a problem hiding this comment.
This replaces a bunch of validation code on the t0 embeddings
| default=0.0, | ||
| description="Either a float(Chance of dropout being applied to each sample) or a list of " | ||
| "floats (probability that dropout of the corresponding timedelta is applied)", | ||
| dropout_minute_probabilities: _MinuteProbabilities | None = Field( |
There was a problem hiding this comment.
I've refactored the dropout configuration. Instead of being a linked-list of dropout values and probabilities it is now a dict which is configured like dict[dropout_minutes: int, probability: float]. I think this is nicer and a more secure way of configuring this than linked lists which require extra validation (like checking lengths) that this data structure does not
| channels: list[str] = Field( | ||
| ..., | ||
| description="the satellite channels that are used", | ||
| min_length=1, |
There was a problem hiding this comment.
Must include at least 1 channel
| description="Channels to include, in output order", | ||
| ) | ||
|
|
||
| delay_minute_probabilities: _MinuteProbabilities | None = Field( |
There was a problem hiding this comment.
For NWP we rename dropout to delay since that better reflects the data outputs. Dropout replaces the values with NaNs which are then infilled. Delay just uses an older init-time
| @field_validator("provider") | ||
| def validate_provider(cls, v: str) -> str: | ||
| """Validator for 'provider'.""" | ||
| provider = v.lower() |
There was a problem hiding this comment.
I don't think we should be case insensitive. This is easy for us to just get right
| ) | ||
|
|
||
| @field_validator("embeddings") | ||
| def validate_embeddings(cls, embeddings: _embedding_type) -> _embedding_type: |
There was a problem hiding this comment.
This validation plus more is now down in the _Embedding class and in in validate_embeddings() below
There was a problem hiding this comment.
These are just changes for the new dropout data structure
| if config.generation is not None: | ||
| for key, window_config in ( | ||
| ("generation_input", config.generation.input), | ||
| ("generation_target", config.generation.target), |
There was a problem hiding this comment.
There should only ever be NaNs in the input generation, and those only come from dropout
|
|
||
|
|
||
| def fill_nans(da: TArray, fill_value: float) -> TArray: | ||
| def fill_nans(da: TArray, fill_value: float) -> None: |
There was a problem hiding this comment.
This function changes the inputs in-place. I want to make that more explicit
| from ocf_data_sampler.common.types import TArray | ||
|
|
||
|
|
||
| def choose_delay(delay_minute_probabilities: Mapping[int, float]) -> np.timedelta64 | None: |
There was a problem hiding this comment.
I've refactored the dropout codepaths. Now we choose a delay and apply it in different steps. This helps split out the responsibilities a bit more and we can use choose_delay() both for dropout and for choosing the NWP delay
| f"Dropout can only be applied to DataArrays with numpy data. Got: {type(da.data)}" | ||
| ) | ||
|
|
||
| if isinstance(dropout_frac, float | int): |
There was a problem hiding this comment.
This is all validated at the config level. I think that's a better boundary
There was a problem hiding this comment.
Agreed. Should max_delay <= max_staleness move into NWP config too, then?
There was a problem hiding this comment.
I'm not sure, it would move that error message earlier, BUT if max_staleness is set to None then we calculate a max_staleness from the max forecast step minus the interval end minutes. We need to open the zarr to do that and we shouldn't be touching data when validating the config.
Therefore, we can't get rid of the check inside select_time_slice_nwp. We could add a additional validator in the config for the case where max_staleness is not None, but its not going to catch many errors much earlier. I think leaving it as None is the dominant usage pattern
| return da.isel(init_time_utc=selected_init_time_index, step=selected_step_indices) | ||
|
|
||
|
|
||
| def _get_nwp_dropout_available_time( |
There was a problem hiding this comment.
After the refactor this isn't needed. We get to reuse choose_delay() instead and have all the validation done at the config level
| assert (expected_init_time == da_slice["init_time_utc"].values) | ||
|
|
||
|
|
||
| def test_select_time_slice_nwp_with_weighted_dropout_list(da_nwp_like): |
There was a problem hiding this comment.
This validation is done at the config level now. The function expects clean inputs
| """NWP source configurations, accessed by name: config.nwp["ukv"].""" | ||
|
|
||
| root: dict[str, NWP] | ||
| root: dict[str, NWP] = Field(..., min_length=1) |
There was a problem hiding this comment.
Make sure NWP can't be configured as an empty dictionary. We set it to None if we don't want any NWP
| t0, | ||
| dropout_timedeltas=minutes(config.satellite.dropout_timedeltas_minutes), | ||
| dropout_frac=config.satellite.dropout_fraction, | ||
| dropout_delay=choose_delay(sat_conf.dropout_minute_probabilities), |
There was a problem hiding this comment.
The dropout time is chosen and passed in. I think this is better separation of concerns
| return self | ||
|
|
||
|
|
||
| class MultiNWP(RootModel): |
There was a problem hiding this comment.
Turns out this class is completely unnecessary with the current version of the library. It just mimics the behaviour of a dictionary. Pydantic does actually allow you to use a dictionary as part of the config, so I've switched to that
There was a problem hiding this comment.
Correct me if I'm wrong, but #451 will need a rebase onto this right?
There was a problem hiding this comment.
Yeh in general I'm maintaining that all these PRs will need to rebase on the PRs that came before them. This is part_22, the PR you mention is part_23 (as per the branch names)
Pull Request
Description
This PR
Checklist: