Skip to content

Commit a35ff60

Browse files
Merge branch 'main' into pr-template
2 parents 5ade2ff + 9dd7e47 commit a35ff60

File tree

6 files changed

+138
-141
lines changed

6 files changed

+138
-141
lines changed

src/parcels/_core/utils/sgrid.py

Lines changed: 74 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313

1414
import enum
1515
import re
16-
from collections.abc import Hashable, Iterable
16+
from collections.abc import Callable, Hashable, Iterable
1717
from dataclasses import dataclass
1818
from textwrap import indent
1919
from typing import Any, Literal, Protocol, Self, cast, overload
@@ -22,7 +22,7 @@
2222

2323
from parcels._python import repr_from_dunder_dict
2424

25-
RE_DIM_DIM_PADDING = r"(\w+):(\w+)\s*\(padding:\s*(\w+)\)"
25+
RE_FACE_NODE_PADDING = r"(\w+):(\w+)\s*\(padding:\s*(\w+)\)"
2626

2727
Dim = str
2828

@@ -56,22 +56,22 @@ def from_attrs(cls, d: dict[str, Hashable]) -> Self: ...
5656

5757
# Note that - for some optional attributes in the SGRID spec - these IDs are not available
5858
# hence this isn't full coverage
59-
_ID_FETCHERS_GRID2DMETADATA = {
59+
_ID_FETCHERS_GRID2DMETADATA: dict[str, Callable[[Grid2DMetadata], Dim | Padding]] = {
6060
"node_dimension1": lambda meta: meta.node_dimensions[0],
6161
"node_dimension2": lambda meta: meta.node_dimensions[1],
62-
"face_dimension1": lambda meta: meta.face_dimensions[0].dim1,
63-
"face_dimension2": lambda meta: meta.face_dimensions[1].dim1,
62+
"face_dimension1": lambda meta: meta.face_dimensions[0].face,
63+
"face_dimension2": lambda meta: meta.face_dimensions[1].face,
6464
"type1": lambda meta: meta.face_dimensions[0].padding,
6565
"type2": lambda meta: meta.face_dimensions[1].padding,
6666
}
6767

68-
_ID_FETCHERS_GRID3DMETADATA = {
68+
_ID_FETCHERS_GRID3DMETADATA: dict[str, Callable[[Grid3DMetadata], Dim | Padding]] = {
6969
"node_dimension1": lambda meta: meta.node_dimensions[0],
7070
"node_dimension2": lambda meta: meta.node_dimensions[1],
7171
"node_dimension3": lambda meta: meta.node_dimensions[2],
72-
"face_dimension1": lambda meta: meta.volume_dimensions[0].dim1,
73-
"face_dimension2": lambda meta: meta.volume_dimensions[1].dim1,
74-
"face_dimension3": lambda meta: meta.volume_dimensions[2].dim1,
72+
"face_dimension1": lambda meta: meta.volume_dimensions[0].face,
73+
"face_dimension2": lambda meta: meta.volume_dimensions[1].face,
74+
"face_dimension3": lambda meta: meta.volume_dimensions[2].face,
7575
"type1": lambda meta: meta.volume_dimensions[0].padding,
7676
"type2": lambda meta: meta.volume_dimensions[1].padding,
7777
"type3": lambda meta: meta.volume_dimensions[2].padding,
@@ -84,9 +84,9 @@ def __init__(
8484
cf_role: Literal["grid_topology"],
8585
topology_dimension: Literal[2],
8686
node_dimensions: tuple[Dim, Dim],
87-
face_dimensions: tuple[DimDimPadding, DimDimPadding],
87+
face_dimensions: tuple[FaceNodePadding, FaceNodePadding],
8888
node_coordinates: None | tuple[Dim, Dim] = None,
89-
vertical_dimensions: None | tuple[DimDimPadding] = None,
89+
vertical_dimensions: None | tuple[FaceNodePadding] = None,
9090
):
9191
if cf_role != "grid_topology":
9292
raise ValueError(f"cf_role must be 'grid_topology', got {cf_role!r}")
@@ -104,9 +104,9 @@ def __init__(
104104
if not (
105105
isinstance(face_dimensions, tuple)
106106
and len(face_dimensions) == 2
107-
and all(isinstance(fd, DimDimPadding) for fd in face_dimensions)
107+
and all(isinstance(fd, FaceNodePadding) for fd in face_dimensions)
108108
):
109-
raise ValueError("face_dimensions must be a tuple of 2 DimDimPadding for a 2D grid")
109+
raise ValueError("face_dimensions must be a tuple of 2 FaceNodePadding for a 2D grid")
110110

111111
if node_coordinates is not None:
112112
if not (
@@ -120,9 +120,9 @@ def __init__(
120120
if not (
121121
isinstance(vertical_dimensions, tuple)
122122
and len(vertical_dimensions) == 1
123-
and isinstance(vertical_dimensions[0], DimDimPadding)
123+
and isinstance(vertical_dimensions[0], FaceNodePadding)
124124
):
125-
raise ValueError("vertical_dimensions must be a tuple of 1 DimDimPadding for a 2D grid")
125+
raise ValueError("vertical_dimensions must be a tuple of 1 FaceNodePadding for a 2D grid")
126126

127127
# Required attributes
128128
self.cf_role = cf_role
@@ -137,8 +137,8 @@ def __init__(
137137
#! Some optional attributes aren't really important to Parcels, can be added later if needed
138138
# Optional attributes
139139
# # With defaults (set in init)
140-
# edge1_dimensions: tuple[Dim, DimDimPadding]
141-
# edge2_dimensions: tuple[DimDimPadding, Dim]
140+
# edge1_dimensions: tuple[Dim, FaceNodePadding]
141+
# edge2_dimensions: tuple[FaceNodePadding, Dim]
142142

143143
# # Without defaults
144144
# edge1_coordinates: None | Any = None
@@ -163,7 +163,7 @@ def from_attrs(cls, attrs): # type: ignore[override]
163163
cf_role=attrs["cf_role"],
164164
topology_dimension=attrs["topology_dimension"],
165165
node_dimensions=cast(tuple[Dim, Dim], load_mappings(attrs["node_dimensions"])),
166-
face_dimensions=cast(tuple[DimDimPadding, DimDimPadding], load_mappings(attrs["face_dimensions"])),
166+
face_dimensions=cast(tuple[FaceNodePadding, FaceNodePadding], load_mappings(attrs["face_dimensions"])),
167167
node_coordinates=maybe_load_mappings(attrs.get("node_coordinates")),
168168
vertical_dimensions=maybe_load_mappings(attrs.get("vertical_dimensions")),
169169
)
@@ -186,7 +186,7 @@ def to_attrs(self) -> dict[str, str | int]:
186186
def rename(self, names_dict: dict[str, str]) -> Self:
187187
return cast(Self, _metadata_rename(self, names_dict))
188188

189-
def get_value_by_id(self, id: str) -> str:
189+
def get_value_by_id(self, id: str) -> Dim | Padding:
190190
"""In the SGRID specification for 2D grids, different parts of the spec are identified by different "ID"s.
191191
192192
Easily extract the value for a given ID.
@@ -206,7 +206,7 @@ def __init__(
206206
cf_role: Literal["grid_topology"],
207207
topology_dimension: Literal[3],
208208
node_dimensions: tuple[Dim, Dim, Dim],
209-
volume_dimensions: tuple[DimDimPadding, DimDimPadding, DimDimPadding],
209+
volume_dimensions: tuple[FaceNodePadding, FaceNodePadding, FaceNodePadding],
210210
node_coordinates: None | tuple[Dim, Dim, Dim] = None,
211211
):
212212
if cf_role != "grid_topology":
@@ -225,9 +225,9 @@ def __init__(
225225
if not (
226226
isinstance(volume_dimensions, tuple)
227227
and len(volume_dimensions) == 3
228-
and all(isinstance(fd, DimDimPadding) for fd in volume_dimensions)
228+
and all(isinstance(fd, FaceNodePadding) for fd in volume_dimensions)
229229
):
230-
raise ValueError("face_dimensions must be a tuple of 2 DimDimPadding for a 2D grid")
230+
raise ValueError("face_dimensions must be a tuple of 2 FaceNodePadding for a 2D grid")
231231

232232
if node_coordinates is not None:
233233
if not (
@@ -249,12 +249,12 @@ def __init__(
249249
# ! Some optional attributes aren't really important to Parcels, can be added later if needed
250250
# Optional attributes
251251
# # With defaults (set in init)
252-
# edge1_dimensions: tuple[DimDimPadding, Dim, Dim]
253-
# edge2_dimensions: tuple[Dim, DimDimPadding, Dim]
254-
# edge3_dimensions: tuple[Dim, Dim, DimDimPadding]
255-
# face1_dimensions: tuple[Dim, DimDimPadding, DimDimPadding]
256-
# face2_dimensions: tuple[DimDimPadding, Dim, DimDimPadding]
257-
# face3_dimensions: tuple[DimDimPadding, DimDimPadding, Dim]
252+
# edge1_dimensions: tuple[FaceNodePadding, Dim, Dim]
253+
# edge2_dimensions: tuple[Dim, FaceNodePadding, Dim]
254+
# edge3_dimensions: tuple[Dim, Dim, FaceNodePadding]
255+
# face1_dimensions: tuple[Dim, FaceNodePadding, FaceNodePadding]
256+
# face2_dimensions: tuple[FaceNodePadding, Dim, FaceNodePadding]
257+
# face3_dimensions: tuple[FaceNodePadding, FaceNodePadding, Dim]
258258

259259
# # Without defaults
260260
# edge *i_coordinates*
@@ -280,7 +280,7 @@ def from_attrs(cls, attrs): # type: ignore[override]
280280
topology_dimension=attrs["topology_dimension"],
281281
node_dimensions=cast(tuple[Dim, Dim, Dim], load_mappings(attrs["node_dimensions"])),
282282
volume_dimensions=cast(
283-
tuple[DimDimPadding, DimDimPadding, DimDimPadding], load_mappings(attrs["volume_dimensions"])
283+
tuple[FaceNodePadding, FaceNodePadding, FaceNodePadding], load_mappings(attrs["volume_dimensions"])
284284
),
285285
node_coordinates=maybe_load_mappings(attrs.get("node_coordinates")),
286286
)
@@ -301,7 +301,7 @@ def to_attrs(self) -> dict[str, str | int]:
301301
def rename(self, dims_dict: dict[str, str]) -> Self:
302302
return cast(Self, _metadata_rename(self, dims_dict))
303303

304-
def get_value_by_id(self, id: str) -> str:
304+
def get_value_by_id(self, id: str) -> Dim | Padding:
305305
"""In the SGRID specification for 3D grids, different parts of the spec are identified by different "ID"s.
306306
307307
Easily extract the value for a given ID.
@@ -316,39 +316,39 @@ def get_value_by_id(self, id: str) -> str:
316316

317317

318318
@dataclass
319-
class DimDimPadding:
320-
"""A data class representing a dimension-dimension-padding triplet for SGrid metadata.
319+
class FaceNodePadding:
320+
"""A data class representing a face-node-padding triplet for SGrid metadata.
321321
322-
This triplet can represent different relations depending on context within the standard
323-
For example - for "face_dimensions" this can show the relation between an edge (dim1) and a node
324-
(dim2).
322+
In the context of a 2D grid, "face" corresponds with an edge.
323+
324+
Use the .to_diagram() method to visualize the representation.
325325
"""
326326

327-
dim1: str
328-
dim2: str
327+
face: Dim
328+
node: Dim
329329
padding: Padding
330330

331331
def __repr__(self) -> str:
332-
return f"DimDimPadding(dim1={self.dim1!r}, dim2={self.dim2!r}, padding={self.padding!r})"
332+
return f"FaceNodePadding(face={self.face!r}, node={self.node!r}, padding={self.padding!r})"
333333

334334
def __str__(self) -> str:
335-
return f"{self.dim1}:{self.dim2} (padding:{self.padding.value})"
335+
return f"{self.face}:{self.node} (padding:{self.padding.value})"
336336

337337
@classmethod
338338
def load(cls, s: str) -> Self:
339-
match = re.match(RE_DIM_DIM_PADDING, s)
339+
match = re.match(RE_FACE_NODE_PADDING, s)
340340
if not match:
341-
raise ValueError(f"String {s!r} does not match expected format for DimDimPadding")
342-
dim1 = match.group(1)
343-
dim2 = match.group(2)
341+
raise ValueError(f"String {s!r} does not match expected format for FaceNodePadding")
342+
face = match.group(1)
343+
node = match.group(2)
344344
padding = Padding(match.group(3).lower())
345-
return cls(dim1, dim2, padding)
345+
return cls(face, node, padding)
346346

347347
def to_diagram(self) -> str:
348348
return "\n".join(_face_node_padding_to_text(self))
349349

350350

351-
def dump_mappings(parts: Iterable[DimDimPadding | Dim]) -> str:
351+
def dump_mappings(parts: Iterable[FaceNodePadding | Dim]) -> str:
352352
"""Takes in a list of edge-node-padding tuples and serializes them into a string
353353
according to the SGrid convention.
354354
"""
@@ -361,7 +361,7 @@ def dump_mappings(parts: Iterable[DimDimPadding | Dim]) -> str:
361361
@overload
362362
def maybe_dump_mappings(parts: None) -> None: ...
363363
@overload
364-
def maybe_dump_mappings(parts: Iterable[DimDimPadding | Dim]) -> str: ...
364+
def maybe_dump_mappings(parts: Iterable[FaceNodePadding | Dim]) -> str: ...
365365

366366

367367
def maybe_dump_mappings(parts):
@@ -370,7 +370,7 @@ def maybe_dump_mappings(parts):
370370
return dump_mappings(parts)
371371

372372

373-
def load_mappings(s: str) -> tuple[DimDimPadding | Dim, ...]:
373+
def load_mappings(s: str) -> tuple[FaceNodePadding | Dim, ...]:
374374
"""Takes in a string indicating the mappings of dims and dim-dim-padding
375375
and returns a tuple with this data destructured.
376376
@@ -383,25 +383,25 @@ def load_mappings(s: str) -> tuple[DimDimPadding | Dim, ...]:
383383
ret = []
384384
while s:
385385
# find next part
386-
match = re.match(RE_DIM_DIM_PADDING, s)
386+
match = re.match(RE_FACE_NODE_PADDING, s)
387387
if match and match.start() == 0:
388388
# match found at start, take that as next part
389389
part = match.group(0)
390390
s_new = s[match.end() :].lstrip()
391391
else:
392-
# no DimDimPadding match at start, assume just a Dim until next space
392+
# no FaceNodePadding match at start, assume just a Dim until next space
393393
part, *s_new = s.split(" ", 1)
394394
s_new = "".join(s_new)
395395

396396
assert s != s_new, f"SGrid parsing did not advance, stuck at {s!r}"
397397

398-
parsed: DimDimPadding | Dim
398+
parsed: FaceNodePadding | Dim
399399
try:
400-
parsed = DimDimPadding.load(part)
400+
parsed = FaceNodePadding.load(part)
401401
except ValueError as e:
402402
e.add_note(f"Failed to parse part {part!r} from {s!r} as a dimension dimension padding string")
403403
try:
404-
# Not a DimDimPadding, assume it's just a Dim
404+
# Not a FaceNodePadding, assume it's just a Dim
405405
assert ":" not in part, f"Part {part!r} from {s!r} not a valid dim (contains ':')"
406406
parsed = part
407407
except AssertionError as e2:
@@ -416,7 +416,7 @@ def load_mappings(s: str) -> tuple[DimDimPadding | Dim, ...]:
416416
@overload
417417
def maybe_load_mappings(s: None) -> None: ...
418418
@overload
419-
def maybe_load_mappings(s: Hashable) -> tuple[DimDimPadding | Dim, ...]: ...
419+
def maybe_load_mappings(s: Hashable) -> tuple[FaceNodePadding | Dim, ...]: ...
420420

421421

422422
def maybe_load_mappings(s):
@@ -471,11 +471,11 @@ def parse_sgrid(ds: xr.Dataset):
471471
dimensions = grid.volume_dimensions
472472

473473
xgcm_coords = {}
474-
for dim_dim_padding, axis in zip(dimensions, "XYZ", strict=False):
475-
xgcm_position = SGRID_PADDING_TO_XGCM_POSITION[dim_dim_padding.padding]
474+
for face_node_padding, axis in zip(dimensions, "XYZ", strict=False):
475+
xgcm_position = SGRID_PADDING_TO_XGCM_POSITION[face_node_padding.padding]
476476

477477
coords = {}
478-
for pos, dim in [("center", dim_dim_padding.dim1), (xgcm_position, dim_dim_padding.dim2)]:
478+
for pos, dim in [("center", face_node_padding.face), (xgcm_position, face_node_padding.node)]:
479479
# only include dimensions in dataset (ignore dimensions in metadata that may not exist - e.g., due to `.isel`)
480480
if dim in ds.dims:
481481
coords[pos] = dim
@@ -510,16 +510,16 @@ def get_unique_names(grid: Grid2DMetadata | Grid3DMetadata) -> set[str]:
510510
f"Expected sgrid metadata attribute to be represented as a tuple, got {value!r}. This is an internal error to Parcels - please post an issue if you encounter this."
511511
)
512512
for item in value:
513-
if isinstance(item, DimDimPadding):
514-
dims.add(item.dim1)
515-
dims.add(item.dim2)
513+
if isinstance(item, FaceNodePadding):
514+
dims.add(item.face)
515+
dims.add(item.node)
516516
else:
517517
assert isinstance(item, str)
518518
dims.add(item)
519519
return dims
520520

521521

522-
def _face_node_padding_to_text(obj: DimDimPadding) -> list[str]:
522+
def _face_node_padding_to_text(obj: FaceNodePadding) -> list[str]:
523523
"""Return ASCII diagram lines showing a face-node padding relationship.
524524
525525
Produces a symbolic 5-node diagram like the image below, matching the
@@ -566,7 +566,7 @@ def _face_node_padding_to_text(obj: DimDimPadding) -> list[str]:
566566
face_count += 1
567567

568568
return [
569-
f"{obj.dim1}:{obj.dim2} (padding:{padding.value})",
569+
f"{obj.face}:{obj.node} (padding:{padding.value})",
570570
f" {bar_rendered}",
571571
f" {label.rstrip()}",
572572
]
@@ -640,19 +640,19 @@ def _grid2d_to_ascii(grid: Grid2DMetadata) -> str:
640640
nd = grid.node_dimensions
641641
lines = [
642642
"Grid2DMetadata",
643-
f" X-axis: face={fd[0].dim1!r} node={nd[0]!r} padding={fd[0].padding.value}",
644-
f" Y-axis: face={fd[1].dim1!r} node={nd[1]!r} padding={fd[1].padding.value}",
643+
f" X-axis: face={fd[0].face!r} node={nd[0]!r} padding={fd[0].padding.value}",
644+
f" Y-axis: face={fd[1].face!r} node={nd[1]!r} padding={fd[1].padding.value}",
645645
]
646646
if grid.vertical_dimensions:
647647
vd = grid.vertical_dimensions[0]
648-
lines.append(f" Z-axis: face={vd.dim1!r} node={vd.dim2!r} padding={vd.padding.value}")
648+
lines.append(f" Z-axis: face={vd.face!r} node={vd.node!r} padding={vd.padding.value}")
649649
if grid.node_coordinates:
650650
lines.append(f" Coordinates: {grid.node_coordinates[0]}, {grid.node_coordinates[1]}")
651651

652-
format_kwargs = dict(n1=nd[0], n2=nd[1], u=fd[0].dim1, v=fd[1].dim1)
652+
format_kwargs = dict(n1=nd[0], n2=nd[1], u=fd[0].face, v=fd[1].face)
653653

654654
if grid.vertical_dimensions:
655-
format_kwargs["w"] = grid.vertical_dimensions[0].dim2
655+
format_kwargs["w"] = grid.vertical_dimensions[0].node
656656
lines += indent(_TEXT_GRID2D_WITH_Z, " ").format(**format_kwargs).split("\n")
657657
else:
658658
lines += indent(_TEXT_GRID2D_WITHOUT_Z, " ").format(**format_kwargs).split("\n")
@@ -672,16 +672,16 @@ def _grid3d_to_ascii(grid: Grid3DMetadata) -> str:
672672
nd = grid.node_dimensions
673673
lines = [
674674
"Grid3DMetadata",
675-
f" X-axis: face={vd[0].dim1!r} node={nd[0]!r} padding={vd[0].padding.value}",
676-
f" Y-axis: face={vd[1].dim1!r} node={nd[1]!r} padding={vd[1].padding.value}",
677-
f" Z-axis: face={vd[2].dim1!r} node={nd[2]!r} padding={vd[2].padding.value}",
675+
f" X-axis: face={vd[0].face!r} node={nd[0]!r} padding={vd[0].padding.value}",
676+
f" Y-axis: face={vd[1].face!r} node={nd[1]!r} padding={vd[1].padding.value}",
677+
f" Z-axis: face={vd[2].face!r} node={nd[2]!r} padding={vd[2].padding.value}",
678678
]
679679
if grid.node_coordinates:
680680
lines.append(f" Coordinates: {', '.join(grid.node_coordinates)}")
681681

682682
lines += (
683683
indent(_TEXT_GRID3D, " ")
684-
.format(n1=nd[0], n2=nd[1], n3=nd[2], u=vd[0].dim1, v=vd[1].dim1, w=vd[2].dim1)
684+
.format(n1=nd[0], n2=nd[1], n3=nd[2], u=vd[0].face, v=vd[1].face, w=vd[2].face)
685685
.split("\n")
686686
)
687687

@@ -738,10 +738,10 @@ def _metadata_rename(grid, names_dict):
738738
if isinstance(value, tuple):
739739
new_value = []
740740
for item in value:
741-
if isinstance(item, DimDimPadding):
742-
new_item = DimDimPadding(
743-
dim1=names_dict[item.dim1],
744-
dim2=names_dict[item.dim2],
741+
if isinstance(item, FaceNodePadding):
742+
new_item = FaceNodePadding(
743+
face=names_dict[item.face],
744+
node=names_dict[item.node],
745745
padding=item.padding,
746746
)
747747
new_value.append(new_item)

0 commit comments

Comments
 (0)