Skip to content

Validate the grid, the history dimension and the latitudes and longitudes - #204

Open
priyDe (priyald) wants to merge 5 commits into
microsoft:mainfrom
priyald:validate-grid-history-latlon
Open

priyDe (priyald) wants to merge 5 commits into
microsoft:mainfrom
priyald:validate-grid-history-latlon

Conversation

@priyald

@priyald priyDe (priyald) commented Sep 9, 2026 •

Copy link
Copy Markdown

Follow-up to #196, rebased onto main now that it has landed. The checks from both PRs sit in one
Batch.__post_init__.

#196 validates that metadata.time and metadata.atmos_levels match the data. This adds the
remaining shape invariants that Batch documents but does not enforce.

What it checks

  1. Every surface-level, static and atmospheric variable describes the same grid.
  2. The surface-level and atmospheric variables agree on the history dimension.
  3. The latitudes and longitudes match that grid, in both the vector and the matrix form.

Why these three

The latitudes and longitudes were already checked in Perceiver3DEncoder.forward, but with

assert lat.shape[0] == H and lon.shape[-1] == W

which 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 at
construction, 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 tell
the 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.forward inserted it. The decoder now
inserts it directly, and the block in Aurora.forward is gone. The _post_decoder_hook
implementations already ran after that block, so they see the same shapes as before.

Tests

Added to tests/test_batch.py:

  • latitudes or longitudes that do not match the grid raise, in vector and matrix form;
  • matrix latitudes and longitudes are accepted;
  • variables that disagree on the grid raise;
  • variables that disagree on the history dimension raise.

Full suite passes apart from test_aurora_small, which fails for me at 0.04224444 on unmodified
main as well (likely the machine-dependent numerics in #169). ruff check and
ruff format --check are clean.

@priyald
priyDe (priyald) requested a review from a team September 9, 2026 21:52

@wesselb Wessel (wesselb) left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the PR, priyDe (@priyald). :)

I've left some Claude-assisted comments and suggestions.

Comment thread aurora/batch.py Resolved
Comment thread aurora/batch.py Resolved
Comment thread aurora/batch.py Resolved
Comment thread aurora/batch.py Resolved
Comment thread aurora/batch.py Resolved
Comment thread aurora/batch.py Resolved
Comment thread tests/test_batch.py Resolved
Comment thread tests/test_batch.py Resolved
Comment thread tests/test_batch.py Resolved
…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.
@priyald
priyDe (priyald) force-pushed the validate-grid-history-latlon branch from b0d3be4 to 16202d9 Compare October 28, 2026 10:13
@priyald

Copy link
Copy Markdown
Author

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.

Comment thread aurora/model/decoder.py

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)},

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
{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)},

@wesselb Wessel (wesselb) left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Left two more comments. Happy to merge after those. Thanks, priyDe (@priyald)! This looks really great. :)

Comment thread tests/test_batch.py
Comment on lines +84 to +119
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)})

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Document the fixed grid and rename.

Suggested change
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.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants