Repository navigation
Validate the grid, the history dimension and the latitudes and longitudes - #204
priyDe (priyald) wants to merge 5 commits into
Conversation
Wessel (wesselb)
left a comment
There was a problem hiding this comment.
Thanks for the PR, priyDe (@priyald). :)
I've left some Claude-assisted comments and suggestions.
…udes `Batch` documents the shapes of its variables but does not enforce them. Add a `__post_init__` that checks the three invariants not covered by microsoft#196: * every surface-level, static and atmospheric variable describes the same grid; * the surface-level and atmospheric variables agree on the history dimension; * the latitudes and longitudes match that grid, in both the vector and the matrix form. The latitudes and longitudes are checked in the encoder today, but with a bare `assert` that carries no message and is removed under `python -O`. The other two surface as shape errors from inside torch, which do not say which variable is wrong. Raising at construction is consistent with the existing latitude and longitude validation in `Metadata.__post_init__`. The spatial shape is read from the last two dimensions because the static variables are expanded to `(b, t, h, w)` inside the model and the decoder builds a batch before the history dimension is inserted. The history dimension is checked only on variables that still have one, for the same reason. An empty `surf_vars` or `atmos_vars` is left alone rather than treated as an error, since running without atmospheric variables is suggested in microsoft#176.
b0d3be4 to
16202d9
Compare
|
Sorry for the slow reply on this! Rebased onto main and applied all your suggestions, including the decoder change and removing the assert in the encoder. Thanks for the detailed review. |
|
|
||
| return Batch( | ||
| {v: surf_preds[:, i] for i, v in enumerate(surf_vars)}, | ||
| {v: surf_preds[:, i, None] for i, v in enumerate(surf_vars)}, |
There was a problem hiding this comment.
| {v: surf_preds[:, i, None] for i, v in enumerate(surf_vars)}, | |
| # Insert a history dimension for the surf. and atmos variables. | |
| {v: surf_preds[:, i, None] for i, v in enumerate(surf_vars)}, |
Wessel (wesselb)
left a comment
There was a problem hiding this comment.
Left two more comments. Happy to merge after those. Thanks, priyDe (@priyald)! This looks really great. :)
| def _batch(n_lat: int = 17, n_lon: int = 32, matrix: bool = False) -> Batch: | ||
| lat = torch.linspace(90, -90, n_lat) | ||
| lon = torch.linspace(0, 360, n_lon + 1)[:-1] | ||
| if matrix: | ||
| lat, lon = lat[:, None].expand(n_lat, n_lon), lon[None, :].expand(n_lat, n_lon) | ||
| return Batch( | ||
| surf_vars={"2t": torch.randn(1, 2, 17, 32)}, | ||
| static_vars={"lsm": torch.randn(17, 32)}, | ||
| atmos_vars={"z": torch.randn(1, 2, 4, 17, 32)}, | ||
| metadata=Metadata( | ||
| lat=lat, | ||
| lon=lon, | ||
| time=(datetime(2020, 6, 1, 12, 0),), | ||
| atmos_levels=(100, 250, 500, 850), | ||
| ), | ||
| ) | ||
|
|
||
|
|
||
| @pytest.mark.parametrize("n_lat, n_lon, matrix", [(18, 32, False), (17, 33, False), (18, 32, True)]) | ||
| def test_mismatching_lat_lon(n_lat: int, n_lon: int, matrix: bool) -> None: | ||
| with pytest.raises(ValueError, match="but the variables have spatial shape"): | ||
| _batch(n_lat, n_lon, matrix) | ||
|
|
||
|
|
||
| def test_matrix_lat_lon_is_accepted() -> None: | ||
| _batch(matrix=True) | ||
|
|
||
|
|
||
| def test_inconsistent_spatial_shape_between_variables() -> None: | ||
| with pytest.raises(AssertionError, match="same spatial shape"): | ||
| dataclasses.replace(_batch(), static_vars={"lsm": torch.randn(9, 32)}) | ||
|
|
||
|
|
||
| def test_inconsistent_history_between_variables() -> None: | ||
| with pytest.raises(AssertionError, match="same history size"): | ||
| dataclasses.replace(_batch(), atmos_vars={"z": torch.randn(1, 3, 4, 17, 32)}) |
There was a problem hiding this comment.
Document the fixed grid and rename.
| def _batch(n_lat: int = 17, n_lon: int = 32, matrix: bool = False) -> Batch: | |
| lat = torch.linspace(90, -90, n_lat) | |
| lon = torch.linspace(0, 360, n_lon + 1)[:-1] | |
| if matrix: | |
| lat, lon = lat[:, None].expand(n_lat, n_lon), lon[None, :].expand(n_lat, n_lon) | |
| return Batch( | |
| surf_vars={"2t": torch.randn(1, 2, 17, 32)}, | |
| static_vars={"lsm": torch.randn(17, 32)}, | |
| atmos_vars={"z": torch.randn(1, 2, 4, 17, 32)}, | |
| metadata=Metadata( | |
| lat=lat, | |
| lon=lon, | |
| time=(datetime(2020, 6, 1, 12, 0),), | |
| atmos_levels=(100, 250, 500, 850), | |
| ), | |
| ) | |
| @pytest.mark.parametrize("n_lat, n_lon, matrix", [(18, 32, False), (17, 33, False), (18, 32, True)]) | |
| def test_mismatching_lat_lon(n_lat: int, n_lon: int, matrix: bool) -> None: | |
| with pytest.raises(ValueError, match="but the variables have spatial shape"): | |
| _batch(n_lat, n_lon, matrix) | |
| def test_matrix_lat_lon_is_accepted() -> None: | |
| _batch(matrix=True) | |
| def test_inconsistent_spatial_shape_between_variables() -> None: | |
| with pytest.raises(AssertionError, match="same spatial shape"): | |
| dataclasses.replace(_batch(), static_vars={"lsm": torch.randn(9, 32)}) | |
| def test_inconsistent_history_between_variables() -> None: | |
| with pytest.raises(AssertionError, match="same history size"): | |
| dataclasses.replace(_batch(), atmos_vars={"z": torch.randn(1, 3, 4, 17, 32)}) | |
| def _make_batch(n_lat: int = 17, n_lon: int = 32, matrix: bool = False) -> Batch: | |
| """Construct a batch whose variables have spatial shape `(17, 32)`. | |
| The latitudes and longitudes describe an `n_lat` by `n_lon` grid, so values other than the | |
| defaults give latitudes and longitudes that do not match the variables. If `matrix` is `True`, | |
| they are matrices rather than vectors. | |
| """ | |
| lat = torch.linspace(90, -90, n_lat) | |
| lon = torch.linspace(0, 360, n_lon + 1)[:-1] | |
| if matrix: | |
| lat, lon = lat[:, None].expand(n_lat, n_lon), lon[None, :].expand(n_lat, n_lon) | |
| return Batch( | |
| surf_vars={"2t": torch.randn(1, 2, 17, 32)}, | |
| static_vars={"lsm": torch.randn(17, 32)}, | |
| atmos_vars={"z": torch.randn(1, 2, 4, 17, 32)}, | |
| metadata=Metadata( | |
| lat=lat, | |
| lon=lon, | |
| time=(datetime(2020, 6, 1, 12, 0),), | |
| atmos_levels=(100, 250, 500, 850), | |
| ), | |
| ) | |
| @pytest.mark.parametrize("n_lat, n_lon, matrix", [(18, 32, False), (17, 33, False), (18, 32, True)]) | |
| def test_mismatching_lat_lon(n_lat: int, n_lon: int, matrix: bool) -> None: | |
| with pytest.raises(ValueError, match="but the variables have spatial shape"): | |
| _make_batch(n_lat, n_lon, matrix) | |
| def test_matrix_lat_lon_is_accepted() -> None: | |
| _make_batch(matrix=True) | |
| def test_inconsistent_spatial_shape_between_variables() -> None: | |
| with pytest.raises(AssertionError, match="same spatial shape"): | |
| dataclasses.replace(_make_batch(), static_vars={"lsm": torch.randn(9, 32)}) | |
| def test_inconsistent_history_between_variables() -> None: | |
| with pytest.raises(AssertionError, match="same history size"): | |
| dataclasses.replace(_make_batch(), atmos_vars={"z": torch.randn(1, 3, 4, 17, 32)}) |
A caller needs to know that only the latitudes and longitudes follow n_lat and n_lon while the variables stay on a 17 by 32 grid. _make_batch matches the helper of the same name in tests/v1p5/_helpers.py.
Comment generated in collaboration with Claude. I take full responsibility for the content of this comment.
Follow-up to #196, rebased onto
mainnow that it has landed. The checks from both PRs sit in oneBatch.__post_init__.#196 validates that
metadata.timeandmetadata.atmos_levelsmatch the data. This adds theremaining shape invariants that
Batchdocuments but does not enforce.What it checks
Why these three
The latitudes and longitudes were already checked in
Perceiver3DEncoder.forward, but withwhich carries no message, fires deep inside the forward pass rather than where the batch was
built, and is removed entirely under
python -O. Since the batch now validates the grid atconstruction, this PR removes that assert. The other two currently surface as shape errors from
inside torch, for example
Sizes of tensors must match except in dimension 2, which does not tellthe caller which variable is wrong.
Decoder change
The checks assume the documented shapes. The one batch that did not follow them was the decoder
output, which lacked the history dimension until
Aurora.forwardinserted it. The decoder nowinserts it directly, and the block in
Aurora.forwardis gone. The_post_decoder_hookimplementations already ran after that block, so they see the same shapes as before.
Tests
Added to
tests/test_batch.py:Full suite passes apart from
test_aurora_small, which fails for me at0.04224444on unmodifiedmainas well (likely the machine-dependent numerics in #169).ruff checkandruff format --checkare clean.