From 4a65e0ba9570570986ccbc0b617b3e2ab801814b Mon Sep 17 00:00:00 2001 From: Vecko <36369090+VeckoTheGecko@users.noreply.github.com> Date: Tue, 13 Jan 2026 11:08:49 +0100 Subject: [PATCH 1/5] Update gitignore Docs reference generation wasn't ignored before --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 563827d8d..a0b957bd9 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ docs/_build/* docs/_downloads docs/jupyter_execute/* docs/.jupyter_cache/* +docs/reference output *.log From 2c99d1d75bcd6e21c3bdfab629717dd5baa2f048 Mon Sep 17 00:00:00 2001 From: Vecko <36369090+VeckoTheGecko@users.noreply.github.com> Date: Tue, 13 Jan 2026 11:31:05 +0100 Subject: [PATCH 2/5] Add node_coordinates metadata to example datasets --- src/parcels/_datasets/structured/generic.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/parcels/_datasets/structured/generic.py b/src/parcels/_datasets/structured/generic.py index 628e790ed..6cda5f172 100644 --- a/src/parcels/_datasets/structured/generic.py +++ b/src/parcels/_datasets/structured/generic.py @@ -258,6 +258,7 @@ def _unrolled_cone_curvilinear_grid(): DimDimPadding("XC", "XG", Padding.HIGH), DimDimPadding("YC", "YG", Padding.HIGH), ), + node_coordinates=("lon", "lat"), vertical_dimensions=(DimDimPadding("ZC", "ZG", Padding.HIGH),), ), ) @@ -278,6 +279,7 @@ def _unrolled_cone_curvilinear_grid(): DimDimPadding("XC", "XG", Padding.LOW), DimDimPadding("YC", "YG", Padding.LOW), ), + node_coordinates=("lon", "lat"), vertical_dimensions=(DimDimPadding("ZC", "ZG", Padding.LOW),), ), ) From 0ec1006f615f88bc546379e79b46a1eacf5adf87 Mon Sep 17 00:00:00 2001 From: Vecko <36369090+VeckoTheGecko@users.noreply.github.com> Date: Tue, 13 Jan 2026 11:43:35 +0100 Subject: [PATCH 3/5] Update sgrid renaming tooling to work with node_coordinates - Update Grid*DMetadata.rename_dims to .rename - Update sgrid.rename_dims to sgrid.rename - Add test --- src/parcels/_core/utils/sgrid.py | 52 +++++++------- src/parcels/_datasets/structured/generic.py | 2 +- tests/utils/test_sgrid.py | 75 +++++++++++++++++---- 3 files changed, 88 insertions(+), 41 deletions(-) diff --git a/src/parcels/_core/utils/sgrid.py b/src/parcels/_core/utils/sgrid.py index 4e671e822..d6851ee2a 100644 --- a/src/parcels/_core/utils/sgrid.py +++ b/src/parcels/_core/utils/sgrid.py @@ -151,8 +151,8 @@ def to_attrs(self) -> dict[str, str | int]: d["vertical_dimensions"] = dump_mappings(self.vertical_dimensions) return d - def rename_dims(self, dims_dict: dict[str, str]) -> Self: - return _metadata_rename_dims(self, dims_dict) + def rename(self, names_dict: dict[str, str]) -> Self: + return _metadata_rename(self, names_dict) class Grid3DMetadata(AttrsSerializable): @@ -248,8 +248,8 @@ def to_attrs(self) -> dict[str, str | int]: d["node_coordinates"] = dump_mappings(self.node_coordinates) return d - def rename_dims(self, dims_dict: dict[str, str]) -> Self: - return _metadata_rename_dims(self, dims_dict) + def rename(self, dims_dict: dict[str, str]) -> Self: + return _metadata_rename(self, dims_dict) @dataclass @@ -418,22 +418,22 @@ def parse_sgrid(ds: xr.Dataset): return (ds, {"coords": xgcm_coords}) -def rename_dims(ds: xr.Dataset, dims_dict: dict[str, str]) -> xr.Dataset: +def rename(ds: xr.Dataset, name_dict: dict[str, str]) -> xr.Dataset: grid_da = get_grid_topology(ds) if grid_da is None: raise ValueError( "No variable found in dataset with 'cf_role' attribute set to 'grid_topology'. This doesn't look to be an SGrid dataset - please make your dataset conforms to SGrid conventions." ) - ds = ds.rename_dims(dims_dict) + ds = ds.rename(name_dict) # Update the metadata grid = parse_grid_attrs(grid_da.attrs) - ds[grid_da.name].attrs = grid.rename_dims(dims_dict).to_attrs() + ds[grid_da.name].attrs = grid.rename(name_dict).to_attrs() return ds -def get_unique_dim_names(grid: Grid2DMetadata | Grid3DMetadata) -> set[str]: +def get_unique_names(grid: Grid2DMetadata | Grid3DMetadata) -> set[str]: dims = set() dims.update(set(grid.node_dimensions)) @@ -454,11 +454,11 @@ def get_unique_dim_names(grid: Grid2DMetadata | Grid3DMetadata) -> set[str]: @overload -def _metadata_rename_dims(grid: Grid2DMetadata, dims_dict: dict[str, str]) -> Grid2DMetadata: ... +def _metadata_rename(grid: Grid2DMetadata, names_dict: dict[str, str]) -> Grid2DMetadata: ... @overload -def _metadata_rename_dims(grid: Grid3DMetadata, dims_dict: dict[str, str]) -> Grid3DMetadata: ... +def _metadata_rename(grid: Grid3DMetadata, names_dict: dict[str, str]) -> Grid3DMetadata: ... def _attach_sgrid_metadata(ds, grid: Grid2DMetadata | Grid3DMetadata): @@ -473,24 +473,24 @@ def _attach_sgrid_metadata(ds, grid: Grid2DMetadata | Grid3DMetadata): return ds -def _metadata_rename_dims(grid, dims_dict): +def _metadata_rename(grid, names_dict): """ - Renames dimensions in SGrid metadata. + Renames dimensions and coordinates in SGrid metadata. - Similar in API to xr.Dataset.rename_dims. Renames dimensions according to dims_dict mapping + Similar in API to xr.Dataset.rename . Renames dimensions according to names_dict mapping of old dimension names to new dimension names. """ - dims_dict = dims_dict.copy() - assert len(dims_dict) == len(set(dims_dict.values())), "dims_dict contains duplicate target dimension names" + names_dict = names_dict.copy() + assert len(names_dict) == len(set(names_dict.values())), "names_dict contains duplicate target dimension names" - existing_dims = get_unique_dim_names(grid) - for dim in dims_dict.keys(): - if dim not in existing_dims: - raise ValueError(f"Dimension {dim!r} not found in SGrid metadata dimensions {existing_dims!r}") + existing_names = get_unique_names(grid) + for name in names_dict.keys(): + if name not in existing_names: + raise ValueError(f"Name {name!r} not found in names defined in SGrid metadata {existing_names!r}") - for dim in existing_dims: - if dim not in dims_dict: - dims_dict[dim] = dim # identity mapping for dimensions not being renamed + for name in existing_names: + if name not in names_dict: + names_dict[name] = name # identity mapping for names not being renamed kwargs = {} for key, value in grid.__dict__.items(): @@ -499,14 +499,14 @@ def _metadata_rename_dims(grid, dims_dict): for item in value: if isinstance(item, DimDimPadding): new_item = DimDimPadding( - dim1=dims_dict[item.dim1], - dim2=dims_dict[item.dim2], + dim1=names_dict[item.dim1], + dim2=names_dict[item.dim2], padding=item.padding, ) new_value.append(new_item) else: assert isinstance(item, str) - new_value.append(dims_dict[item]) + new_value.append(names_dict[item]) kwargs[key] = tuple(new_value) continue @@ -515,7 +515,7 @@ def _metadata_rename_dims(grid, dims_dict): continue if isinstance(value, str): - kwargs[key] = dims_dict[value] + kwargs[key] = names_dict[value] continue raise ValueError(f"Unexpected attribute {key!r} on {grid!r}") diff --git a/src/parcels/_datasets/structured/generic.py b/src/parcels/_datasets/structured/generic.py index 6cda5f172..e77e7ee63 100644 --- a/src/parcels/_datasets/structured/generic.py +++ b/src/parcels/_datasets/structured/generic.py @@ -8,7 +8,7 @@ _attach_sgrid_metadata, ) from parcels._core.utils.sgrid import ( - rename_dims as sgrid_rename_dims, + rename as sgrid_rename_dims, ) from parcels._datasets.utils import _attach_sgrid_metadata diff --git a/tests/utils/test_sgrid.py b/tests/utils/test_sgrid.py index e73c97865..e7de520ca 100644 --- a/tests/utils/test_sgrid.py +++ b/tests/utils/test_sgrid.py @@ -65,7 +65,7 @@ def dummy_sgrid_2d_ds(grid: sgrid.Grid2DMetadata) -> xr.Dataset: ds = dummy_comodo_3d_ds() # Can't rename dimensions that already exist in the dataset - assume(sgrid.get_unique_dim_names(grid) & set(ds.dims) == set()) + assume(sgrid.get_unique_names(grid) & set(ds.dims) == set()) renamings = {} if grid.vertical_dimensions is None: @@ -90,7 +90,7 @@ def dummy_sgrid_3d_ds(grid: sgrid.Grid3DMetadata) -> xr.Dataset: ds = dummy_comodo_3d_ds() # Can't rename dimensions that already exist in the dataset - assume(sgrid.get_unique_dim_names(grid) & set(ds.dims) == set()) + assume(sgrid.get_unique_names(grid) & set(ds.dims) == set()) renamings = {} for old, new in zip(["XG", "YG", "ZG"], grid.node_dimensions, strict=True): @@ -250,30 +250,77 @@ def test_parse_sgrid_3d(grid_metadata: sgrid.Grid3DMetadata): ] + [create_example_grid3dmetadata(with_node_coordinates=i) for i in [False, True]], ) -def test_rename_dims(grid): - dims = sgrid.get_unique_dim_names(grid) +def test_rename(grid): + dims = sgrid.get_unique_names(grid) dims_dict = {dim: f"new_{dim}" for dim in dims} dims_dict_inv = {v: k for k, v in dims_dict.items()} - grid_new = grid.rename_dims(dims_dict) - assert dims & set(sgrid.get_unique_dim_names(grid_new)) == set() + grid_new = grid.rename(dims_dict) + assert dims & set(sgrid.get_unique_names(grid_new)) == set() - assert grid == grid_new.rename_dims(dims_dict_inv) + assert grid == grid_new.rename(dims_dict_inv) -def test_rename_dims_errors(): +def test_rename_errors(): # Test various error modes of rename_dims grid = grid2dmetadata # Non-unique target dimension names - dims_dict = { + names_dict = { "node_dimension1": "new_node_dimension", "node_dimension2": "new_node_dimension", } - with pytest.raises(AssertionError, match="dims_dict contains duplicate target dimension names"): - grid.rename_dims(dims_dict) + with pytest.raises(AssertionError, match="names_dict contains duplicate target dimension names"): + grid.rename(names_dict) # Unexpected attribute in dims_dict - dims_dict = { + names_dict = { "unexpected_dimension": "new_unexpected_dimension", } - with pytest.raises(ValueError, match="Dimension 'unexpected_dimension' not found in SGrid metadata dimensions"): - grid.rename_dims(dims_dict) + with pytest.raises(ValueError, match="Name 'unexpected_dimension' not found in names defined in SGrid metadata"): + grid.rename(names_dict) + + +@pytest.mark.parametrize( + "ds", + [ + xr.Dataset( + { + "data_g": (["time", "ZG", "YG", "XG"], np.random.rand(10, 10, 10, 10)), + "data_c": (["time", "ZC", "YC", "XC"], np.random.rand(10, 10, 10, 10)), + "grid": ( + [], + np.array(0), + sgrid.Grid2DMetadata( + cf_role="grid_topology", + topology_dimension=2, + node_dimensions=("XG", "YG"), + face_dimensions=( + sgrid.DimDimPadding("XC", "XG", sgrid.Padding.HIGH), + sgrid.DimDimPadding("YC", "YG", sgrid.Padding.HIGH), + ), + vertical_dimensions=(sgrid.DimDimPadding("ZC", "ZG", sgrid.Padding.HIGH),), + node_coordinates=("lon", "lat"), + ).to_attrs(), + ), + }, + coords={ + "lon": (["XG"], 2 * np.pi / 10 * np.arange(0, 10)), + "lat": (["YG"], 2 * np.pi / (10) * np.arange(0, 10)), + "depth": (["ZG"], np.arange(10)), + "time": (["time"], xr.date_range("2000", "2001", 10), {"axis": "T"}), + }, + ), + ], +) +def test_rename_dataset(ds): + # Check renaming works for coordinates + ds_new = sgrid.rename(ds, {"lon": "lon_updated"}) + grid_new = sgrid.parse_grid_attrs(ds_new["grid"].attrs) + assert "lon_updated" in ds_new.coords + assert "lon_updated" == grid_new.node_coordinates[0] + + # Check renaming works for dim + ds_new = sgrid.rename(ds, {"XC": "XC_updated"}) + grid_new = sgrid.parse_grid_attrs(ds_new["grid"].attrs) + assert "XC_updated" in ds_new.dims + assert "XC" not in ds_new.dims + assert "XC_updated" == grid_new.face_dimensions[0].dim1 From a3a21796a8616093b8f37ff81882bf0af7d42c2e Mon Sep 17 00:00:00 2001 From: Vecko <36369090+VeckoTheGecko@users.noreply.github.com> Date: Tue, 13 Jan 2026 11:44:01 +0100 Subject: [PATCH 4/5] Move function to fix overload --- src/parcels/_core/utils/sgrid.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/parcels/_core/utils/sgrid.py b/src/parcels/_core/utils/sgrid.py index d6851ee2a..ff75c1a8f 100644 --- a/src/parcels/_core/utils/sgrid.py +++ b/src/parcels/_core/utils/sgrid.py @@ -453,14 +453,6 @@ def get_unique_names(grid: Grid2DMetadata | Grid3DMetadata) -> set[str]: return dims -@overload -def _metadata_rename(grid: Grid2DMetadata, names_dict: dict[str, str]) -> Grid2DMetadata: ... - - -@overload -def _metadata_rename(grid: Grid3DMetadata, names_dict: dict[str, str]) -> Grid3DMetadata: ... - - def _attach_sgrid_metadata(ds, grid: Grid2DMetadata | Grid3DMetadata): """Copies the dataset and attaches the SGRID metadata in 'grid' variable. Modifies 'conventions' attribute.""" ds = ds.copy() @@ -473,6 +465,14 @@ def _attach_sgrid_metadata(ds, grid: Grid2DMetadata | Grid3DMetadata): return ds +@overload +def _metadata_rename(grid: Grid2DMetadata, names_dict: dict[str, str]) -> Grid2DMetadata: ... + + +@overload +def _metadata_rename(grid: Grid3DMetadata, names_dict: dict[str, str]) -> Grid3DMetadata: ... + + def _metadata_rename(grid, names_dict): """ Renames dimensions and coordinates in SGrid metadata. From 0e858c15b9b35a5af5bfa2ea4f00ec0a098c2323 Mon Sep 17 00:00:00 2001 From: Vecko <36369090+VeckoTheGecko@users.noreply.github.com> Date: Tue, 13 Jan 2026 11:51:41 +0100 Subject: [PATCH 5/5] Update sgrid_rename_dims to sgrid_rename --- src/parcels/_datasets/structured/generic.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/parcels/_datasets/structured/generic.py b/src/parcels/_datasets/structured/generic.py index e77e7ee63..db8ddc21e 100644 --- a/src/parcels/_datasets/structured/generic.py +++ b/src/parcels/_datasets/structured/generic.py @@ -8,7 +8,7 @@ _attach_sgrid_metadata, ) from parcels._core.utils.sgrid import ( - rename as sgrid_rename_dims, + rename as sgrid_rename, ) from parcels._datasets.utils import _attach_sgrid_metadata @@ -263,7 +263,7 @@ def _unrolled_cone_curvilinear_grid(): ), ) .pipe( - sgrid_rename_dims, + sgrid_rename, _COMODO_TO_2D_SGRID, ) ), @@ -284,7 +284,7 @@ def _unrolled_cone_curvilinear_grid(): ), ) .pipe( - sgrid_rename_dims, + sgrid_rename, _COMODO_TO_2D_SGRID, ) ),