From e22f0c532d297ced2fcb9f7ab81451ebe7a1c4dc Mon Sep 17 00:00:00 2001 From: Vecko <36369090+VeckoTheGecko@users.noreply.github.com> Date: Tue, 24 Mar 2026 11:33:41 +0100 Subject: [PATCH 01/11] Initial functions --- src/parcels/_core/utils/sgrid.py | 276 +++++++++++++++++++++++++++++++ tests/utils/test_sgrid.py | 24 +++ 2 files changed, 300 insertions(+) diff --git a/src/parcels/_core/utils/sgrid.py b/src/parcels/_core/utils/sgrid.py index 2ee70bfce8..699844196c 100644 --- a/src/parcels/_core/utils/sgrid.py +++ b/src/parcels/_core/utils/sgrid.py @@ -505,6 +505,282 @@ def get_unique_names(grid: Grid2DMetadata | Grid3DMetadata) -> set[str]: return dims +def to_ascii(grid: Grid2DMetadata | Grid3DMetadata) -> str: + """Return an ASCII diagram of the staggered grid structure. + + Parameters + ---------- + grid : Grid2DMetadata | Grid3DMetadata + + Returns + ------- + str + """ + if isinstance(grid, Grid2DMetadata): + return _grid2d_to_ascii(grid) + return _grid3d_to_ascii(grid) + + +def grid_plot(grid: Grid2DMetadata | Grid3DMetadata): + """Return a matplotlib Figure showing the staggered grid point positions. + + For 2D grids, a single panel is produced. For 3D grids, three panels + show the XY, XZ, and YZ cross-sections. + + Requires ``matplotlib`` to be installed. + + Parameters + ---------- + grid : Grid2DMetadata | Grid3DMetadata + + Returns + ------- + matplotlib.figure.Figure + """ + if isinstance(grid, Grid2DMetadata): + return _grid2d_plot(grid) + return _grid3d_plot(grid) + + +def _grid2d_to_ascii(grid: Grid2DMetadata) -> str: + fd = grid.face_dimensions + nd = grid.node_dimensions + lines = [ + "Grid2DMetadata", + f" X-axis: face={fd[0].dim1!r} node={nd[0]!r} padding={fd[0].padding.value}", + f" Y-axis: face={fd[1].dim1!r} node={nd[1]!r} padding={fd[1].padding.value}", + ] + if grid.vertical_dimensions: + vd = grid.vertical_dimensions[0] + lines.append(f" Z-axis: face={vd.dim1!r} node={vd.dim2!r} padding={vd.padding.value}") + if grid.node_coordinates: + lines.append(f" Coordinates: {grid.node_coordinates[0]}, {grid.node_coordinates[1]}") + if grid.vertical_dimensions: + vd = grid.vertical_dimensions[0] + + def _z(base: str, sym: str) -> str: + return base.ljust(28) + sym + + lines += [ + "", + " Staggered grid layout (symbolic 3x3 nodes):", + "", + _z(" ↑ Y", "↑ Z"), + _z(" |", "|"), + _z(" n --u-- n --u-- n", "w"), + _z(" | | |", "|"), + _z(" v · v · v", "·"), + _z(" | | |", "|"), + _z(" n --u-- n --u-- n", "w"), + _z(" | | |", "|"), + _z(" v · v · v", "·"), + _z(" | | |", "|"), + " n --u-- n --u-- n --→ X w", + "", + f" n = node ({nd[0]}, {nd[1]})", + f" u = x-face ({fd[0].dim1})", + f" v = y-face ({fd[1].dim1})", + f" w = z-node ({vd.dim2})", + " · = cell centre", + ] + else: + lines += [ + "", + " Staggered grid layout (symbolic 3x3 nodes):", + "", + " ↑ Y", + " |", + " n --u-- n --u-- n", + " | | |", + " v · v · v", + " | | |", + " n --u-- n --u-- n", + " | | |", + " v · v · v", + " | | |", + " n --u-- n --u-- n --→ X", + "", + f" n = node ({nd[0]}, {nd[1]})", + f" u = x-face ({fd[0].dim1})", + f" v = y-face ({fd[1].dim1})", + " · = cell centre", + ] + return "\n".join(lines) + + +def _grid3d_to_ascii(grid: Grid3DMetadata) -> str: + vd = grid.volume_dimensions + nd = grid.node_dimensions + lines = [ + "Grid3DMetadata", + f" X-axis: face={vd[0].dim1!r} node={nd[0]!r} padding={vd[0].padding.value}", + f" Y-axis: face={vd[1].dim1!r} node={nd[1]!r} padding={vd[1].padding.value}", + f" Z-axis: face={vd[2].dim1!r} node={nd[2]!r} padding={vd[2].padding.value}", + ] + if grid.node_coordinates: + lines.append(f" Coordinates: {', '.join(grid.node_coordinates)}") + lines += [ + "", + " Staggered grid layout (XY cross-section; Z-faces not shown):", + "", + " ↑ Y", + " |", + " n --u-- n --u-- n", + " | | |", + " v · v · v", + " | | |", + " n --u-- n --u-- n", + " | | |", + " v · v · v", + " | | |", + " n --u-- n --u-- n --→ X", + "", + f" n = node ({nd[0]}, {nd[1]}, {nd[2]})", + f" u = x-face ({vd[0].dim1})", + f" v = y-face ({vd[1].dim1})", + f" w = z-face ({vd[2].dim1}) [not shown in cross-section]", + " · = cell centre", + ] + return "\n".join(lines) + + +def _face_positions(padding: Padding, n_nodes: int): + import numpy as np + + if padding in (Padding.LOW, Padding.HIGH): + return np.arange(n_nodes - 1) + 0.5 + elif padding == Padding.BOTH: + return np.arange(1, n_nodes - 1, dtype=float) + else: # Padding.NONE / outer + return np.arange(n_nodes, dtype=float) + + +def _grid2d_plot(grid: Grid2DMetadata): + import matplotlib.pyplot as plt + import numpy as np + + fd = grid.face_dimensions + nd = grid.node_dimensions + N = 4 # illustrative node count per axis + + node_x = np.arange(N, dtype=float) + node_y = np.arange(N, dtype=float) + xface_x = _face_positions(fd[0].padding, N) + yface_y = _face_positions(fd[1].padding, N) + + fig, ax = plt.subplots(figsize=(6, 6)) + ax.set_aspect("equal") + + for x in node_x: + ax.axvline(x, color="lightgray", lw=0.7, zorder=0) + for y in node_y: + ax.axhline(y, color="lightgray", lw=0.7, zorder=0) + + nx, ny = np.meshgrid(node_x, node_y) + ax.scatter(nx.ravel(), ny.ravel(), s=70, color="steelblue", zorder=3, label=f"node ({nd[0]}, {nd[1]})", marker="o") + + fxx, fxy = np.meshgrid(xface_x, node_y) + ax.scatter(fxx.ravel(), fxy.ravel(), s=70, color="firebrick", zorder=3, label=f"x-face ({fd[0].dim1})", marker=">") + + fyx, fyy = np.meshgrid(node_x, yface_y) + ax.scatter( + fyx.ravel(), fyy.ravel(), s=70, color="forestgreen", zorder=3, label=f"y-face ({fd[1].dim1})", marker="^" + ) + + ax.set_xlabel(nd[0]) + ax.set_ylabel(nd[1]) + ax.set_title( + f"Grid2DMetadata — staggered positions\nX padding: {fd[0].padding.value} | Y padding: {fd[1].padding.value}" + ) + ax.legend(loc="upper right", fontsize=8) + fig.tight_layout() + return fig + + +def _grid3d_plot(grid: Grid3DMetadata): + import matplotlib.pyplot as plt + import numpy as np + + vd = grid.volume_dimensions + nd = grid.node_dimensions + N = 4 + + node = [np.arange(N, dtype=float) for _ in range(3)] + face = [_face_positions(vd[i].padding, N) for i in range(3)] + + fig, axes = plt.subplots(1, 3, figsize=(15, 5)) + + panels = [ + ( + "XY", + axes[0], + node[0], + node[1], + face[0], + face[1], + nd[0], + nd[1], + vd[0].dim1, + vd[1].dim1, + vd[0].padding, + vd[1].padding, + ), + ( + "XZ", + axes[1], + node[0], + node[2], + face[0], + face[2], + nd[0], + nd[2], + vd[0].dim1, + vd[2].dim1, + vd[0].padding, + vd[2].padding, + ), + ( + "YZ", + axes[2], + node[1], + node[2], + face[1], + face[2], + nd[1], + nd[2], + vd[1].dim1, + vd[2].dim1, + vd[1].padding, + vd[2].padding, + ), + ] + + for label, ax, n_h, n_v, f_h, f_v, dim_h, dim_v, face_h, face_v, pad_h, pad_v in panels: + for x in n_h: + ax.axvline(x, color="lightgray", lw=0.7, zorder=0) + for y in n_v: + ax.axhline(y, color="lightgray", lw=0.7, zorder=0) + + nh, nv = np.meshgrid(n_h, n_v) + ax.scatter(nh.ravel(), nv.ravel(), s=50, color="steelblue", zorder=3, marker="o", label="node") + + fhh, fhv = np.meshgrid(f_h, n_v) + ax.scatter(fhh.ravel(), fhv.ravel(), s=50, color="firebrick", zorder=3, marker=">", label=f"{face_h}-face") + + fvh, fvv = np.meshgrid(n_h, f_v) + ax.scatter(fvh.ravel(), fvv.ravel(), s=50, color="forestgreen", zorder=3, marker="^", label=f"{face_v}-face") + + ax.set_xlabel(dim_h) + ax.set_ylabel(dim_v) + ax.set_title(f"{label} cross-section\npad: {pad_h.value} / {pad_v.value}") + ax.set_aspect("equal") + ax.legend(fontsize=7) + + fig.suptitle("Grid3DMetadata — staggered positions", fontsize=12) + fig.tight_layout() + return fig + + 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() diff --git a/tests/utils/test_sgrid.py b/tests/utils/test_sgrid.py index c3f3d666ab..e48b18944e 100644 --- a/tests/utils/test_sgrid.py +++ b/tests/utils/test_sgrid.py @@ -1,3 +1,4 @@ +import difflib import itertools import numpy as np @@ -348,3 +349,26 @@ def test_rename_dataset(ds): assert "XC_updated" in ds_new.dims assert "XC" not in ds_new.dims assert "XC_updated" == grid_new.face_dimensions[0].dim1 + + +@pytest.mark.parametrize( + ("metadata, expected"), + [ + (create_example_grid2dmetadata(with_vertical_dimensions=False, with_node_coordinates=False), ""), + (create_example_grid2dmetadata(with_vertical_dimensions=True, with_node_coordinates=True), ""), + (create_example_grid3dmetadata(with_node_coordinates=False), ""), + (create_example_grid3dmetadata(with_node_coordinates=True), ""), + ], +) +def test_grid_text_repr(metadata, expected): + actual = sgrid.to_ascii(metadata) + if actual != expected: + diff = "\n".join( + difflib.unified_diff( + expected.splitlines(keepends=True), + actual.splitlines(keepends=True), + fromfile="expected", + tofile="actual", + ) + ) + pytest.fail(f"grid_text_repr output differs:\n{diff}") From 177b3fabb698a202197cf9483485375cd041761e Mon Sep 17 00:00:00 2001 From: Vecko <36369090+VeckoTheGecko@users.noreply.github.com> Date: Tue, 24 Mar 2026 12:06:40 +0100 Subject: [PATCH 02/11] Refactor --- src/parcels/_core/utils/sgrid.py | 87 ++++++++++++++++++++++++++++++++ tests/utils/test_sgrid.py | 42 +++++++++++++++ 2 files changed, 129 insertions(+) diff --git a/src/parcels/_core/utils/sgrid.py b/src/parcels/_core/utils/sgrid.py index 699844196c..31616705c0 100644 --- a/src/parcels/_core/utils/sgrid.py +++ b/src/parcels/_core/utils/sgrid.py @@ -542,6 +542,80 @@ def grid_plot(grid: Grid2DMetadata | Grid3DMetadata): return _grid3d_plot(grid) +def _face_node_padding_ascii(obj: DimDimPadding) -> list[str]: + """Return ASCII diagram lines showing a face-node padding relationship. + + Produces a symbolic 5-node diagram like the image below, matching the + four padding modes:: + + face:node (padding:none) + ●─────●─────●─────●─────● + 1 1 2 2 3 3 4 4 5 + + face:node (padding:low) + ─────●─────●─────●─────●─────● + 1 1 2 2 3 3 4 4 5 5 + + face:node (padding:high) + ●─────●─────●─────●─────●───── + 1 1 2 2 3 3 4 4 5 5 + + face:node (padding:both) + ─────●─────●─────●─────●─────●───── + 1 1 2 2 3 3 4 4 5 5 6 + """ + FACE_WIDTH = 5 # dashes per face segment + padding = obj.padding + + layouts = { + Padding.NONE: "x-x-x-x-x", + Padding.LOW: "-x-x-x-x-x", + Padding.HIGH: "x-x-x-x-x-", + Padding.BOTH: "-x-x-x-x-x-", + } + + # Build an ordered sequence of ('n', index) / ('f', index) elements + seq_str = layouts[obj.padding] + seq: list[tuple[Literal["n", "f"], int]] = [] + node_count = 0 + face_count = 0 + for char in seq_str: + if char == "x": + node_count += 1 + seq.append(("n", node_count)) + elif char == "-": + face_count += 1 + seq.append(("f", face_count)) + + bar_parts: list[str] = [] + label_positions: dict[int, str] = {} + pos = 0 + for typ, idx in seq: + if typ == "n": + bar_parts.append("●") + label_positions[pos] = str(idx) + pos += 1 + else: + bar_parts.append("─" * FACE_WIDTH) + label_positions[pos + FACE_WIDTH // 2] = str(idx) + pos += FACE_WIDTH + + bar = "".join(bar_parts) + + max_pos = max(p + len(lbl) for p, lbl in label_positions.items()) + label_chars = [" "] * max_pos + for p, lbl in label_positions.items(): + for i, c in enumerate(lbl): + label_chars[p + i] = c + label = "".join(label_chars).rstrip() + + return [ + f" {obj.dim1}:{obj.dim2} (padding:{padding.value})", + f" {bar}", + f" {label}", + ] + + def _grid2d_to_ascii(grid: Grid2DMetadata) -> str: fd = grid.face_dimensions nd = grid.node_dimensions @@ -605,6 +679,13 @@ def _z(base: str, sym: str) -> str: f" v = y-face ({fd[1].dim1})", " · = cell centre", ] + lines += ["", " Axis padding:", ""] + lines += _face_node_padding_ascii(fd[0]) + lines += [""] + lines += _face_node_padding_ascii(fd[1]) + if grid.vertical_dimensions: + lines += [""] + lines += _face_node_padding_ascii(grid.vertical_dimensions[0]) return "\n".join(lines) @@ -641,6 +722,12 @@ def _grid3d_to_ascii(grid: Grid3DMetadata) -> str: f" w = z-face ({vd[2].dim1}) [not shown in cross-section]", " · = cell centre", ] + lines += ["", " Axis padding:", ""] + lines += _face_node_padding_ascii(vd[0]) + lines += [""] + lines += _face_node_padding_ascii(vd[1]) + lines += [""] + lines += _face_node_padding_ascii(vd[2]) return "\n".join(lines) diff --git a/tests/utils/test_sgrid.py b/tests/utils/test_sgrid.py index e48b18944e..a4d702c236 100644 --- a/tests/utils/test_sgrid.py +++ b/tests/utils/test_sgrid.py @@ -372,3 +372,45 @@ def test_grid_text_repr(metadata, expected): ) ) pytest.fail(f"grid_text_repr output differs:\n{diff}") + + +@pytest.mark.parametrize( + ("face_node_padding", "expected"), + [ + ( + sgrid.DimDimPadding("face", "node", sgrid.Padding.LOW), + [ + " face:node (padding:low)", + " ─────●─────●─────●─────●─────●", + " 1 1 2 2 3 3 4 4 5 5", + ], + ), + ( + sgrid.DimDimPadding("face", "node", sgrid.Padding.HIGH), + [ + " face:node (padding:high)", + " ●─────●─────●─────●─────●─────", + " 1 1 2 2 3 3 4 4 5 5", + ], + ), + ( + sgrid.DimDimPadding("face", "node", sgrid.Padding.BOTH), + [ + " face:node (padding:both)", + " ─────●─────●─────●─────●─────●─────", + " 1 1 2 2 3 3 4 4 5 5 6", + ], + ), + ( + sgrid.DimDimPadding("face", "node", sgrid.Padding.NONE), + [ + " face:node (padding:none)", + " ●─────●─────●─────●─────●", + " 1 1 2 2 3 3 4 4 5", + ], + ), + ], +) +def test_face_node_padding_ascii(face_node_padding: sgrid.DimDimPadding, expected: str): + actual = sgrid._face_node_padding_ascii(face_node_padding) + assert actual == expected From 020d4186970479b2c841cf56ddd3219a7c129844 Mon Sep 17 00:00:00 2001 From: Vecko <36369090+VeckoTheGecko@users.noreply.github.com> Date: Tue, 24 Mar 2026 12:17:01 +0100 Subject: [PATCH 03/11] Refactor to str methods --- src/parcels/_core/utils/sgrid.py | 194 +++---------------------------- tests/utils/test_sgrid.py | 6 +- 2 files changed, 16 insertions(+), 184 deletions(-) diff --git a/src/parcels/_core/utils/sgrid.py b/src/parcels/_core/utils/sgrid.py index 31616705c0..4e2f247db1 100644 --- a/src/parcels/_core/utils/sgrid.py +++ b/src/parcels/_core/utils/sgrid.py @@ -143,6 +143,9 @@ def __init__( def __repr__(self) -> str: return repr_from_dunder_dict(self) + def __str__(self) -> str: + return _grid2d_to_ascii(self) + def __eq__(self, other: Any) -> bool: if not isinstance(other, Grid2DMetadata): return NotImplemented @@ -256,6 +259,9 @@ def __init__( def __repr__(self) -> str: return repr_from_dunder_dict(self) + def __str__(self) -> str: + return _grid3d_to_ascii(self) + def __eq__(self, other: Any) -> bool: if not isinstance(other, Grid3DMetadata): return NotImplemented @@ -505,44 +511,7 @@ def get_unique_names(grid: Grid2DMetadata | Grid3DMetadata) -> set[str]: return dims -def to_ascii(grid: Grid2DMetadata | Grid3DMetadata) -> str: - """Return an ASCII diagram of the staggered grid structure. - - Parameters - ---------- - grid : Grid2DMetadata | Grid3DMetadata - - Returns - ------- - str - """ - if isinstance(grid, Grid2DMetadata): - return _grid2d_to_ascii(grid) - return _grid3d_to_ascii(grid) - - -def grid_plot(grid: Grid2DMetadata | Grid3DMetadata): - """Return a matplotlib Figure showing the staggered grid point positions. - - For 2D grids, a single panel is produced. For 3D grids, three panels - show the XY, XZ, and YZ cross-sections. - - Requires ``matplotlib`` to be installed. - - Parameters - ---------- - grid : Grid2DMetadata | Grid3DMetadata - - Returns - ------- - matplotlib.figure.Figure - """ - if isinstance(grid, Grid2DMetadata): - return _grid2d_plot(grid) - return _grid3d_plot(grid) - - -def _face_node_padding_ascii(obj: DimDimPadding) -> list[str]: +def _face_node_padding_to_text(obj: DimDimPadding) -> list[str]: """Return ASCII diagram lines showing a face-node padding relationship. Produces a symbolic 5-node diagram like the image below, matching the @@ -680,12 +649,12 @@ def _z(base: str, sym: str) -> str: " · = cell centre", ] lines += ["", " Axis padding:", ""] - lines += _face_node_padding_ascii(fd[0]) + lines += _face_node_padding_to_text(fd[0]) lines += [""] - lines += _face_node_padding_ascii(fd[1]) + lines += _face_node_padding_to_text(fd[1]) if grid.vertical_dimensions: lines += [""] - lines += _face_node_padding_ascii(grid.vertical_dimensions[0]) + lines += _face_node_padding_to_text(grid.vertical_dimensions[0]) return "\n".join(lines) @@ -723,151 +692,14 @@ def _grid3d_to_ascii(grid: Grid3DMetadata) -> str: " · = cell centre", ] lines += ["", " Axis padding:", ""] - lines += _face_node_padding_ascii(vd[0]) + lines += _face_node_padding_to_text(vd[0]) lines += [""] - lines += _face_node_padding_ascii(vd[1]) + lines += _face_node_padding_to_text(vd[1]) lines += [""] - lines += _face_node_padding_ascii(vd[2]) + lines += _face_node_padding_to_text(vd[2]) return "\n".join(lines) -def _face_positions(padding: Padding, n_nodes: int): - import numpy as np - - if padding in (Padding.LOW, Padding.HIGH): - return np.arange(n_nodes - 1) + 0.5 - elif padding == Padding.BOTH: - return np.arange(1, n_nodes - 1, dtype=float) - else: # Padding.NONE / outer - return np.arange(n_nodes, dtype=float) - - -def _grid2d_plot(grid: Grid2DMetadata): - import matplotlib.pyplot as plt - import numpy as np - - fd = grid.face_dimensions - nd = grid.node_dimensions - N = 4 # illustrative node count per axis - - node_x = np.arange(N, dtype=float) - node_y = np.arange(N, dtype=float) - xface_x = _face_positions(fd[0].padding, N) - yface_y = _face_positions(fd[1].padding, N) - - fig, ax = plt.subplots(figsize=(6, 6)) - ax.set_aspect("equal") - - for x in node_x: - ax.axvline(x, color="lightgray", lw=0.7, zorder=0) - for y in node_y: - ax.axhline(y, color="lightgray", lw=0.7, zorder=0) - - nx, ny = np.meshgrid(node_x, node_y) - ax.scatter(nx.ravel(), ny.ravel(), s=70, color="steelblue", zorder=3, label=f"node ({nd[0]}, {nd[1]})", marker="o") - - fxx, fxy = np.meshgrid(xface_x, node_y) - ax.scatter(fxx.ravel(), fxy.ravel(), s=70, color="firebrick", zorder=3, label=f"x-face ({fd[0].dim1})", marker=">") - - fyx, fyy = np.meshgrid(node_x, yface_y) - ax.scatter( - fyx.ravel(), fyy.ravel(), s=70, color="forestgreen", zorder=3, label=f"y-face ({fd[1].dim1})", marker="^" - ) - - ax.set_xlabel(nd[0]) - ax.set_ylabel(nd[1]) - ax.set_title( - f"Grid2DMetadata — staggered positions\nX padding: {fd[0].padding.value} | Y padding: {fd[1].padding.value}" - ) - ax.legend(loc="upper right", fontsize=8) - fig.tight_layout() - return fig - - -def _grid3d_plot(grid: Grid3DMetadata): - import matplotlib.pyplot as plt - import numpy as np - - vd = grid.volume_dimensions - nd = grid.node_dimensions - N = 4 - - node = [np.arange(N, dtype=float) for _ in range(3)] - face = [_face_positions(vd[i].padding, N) for i in range(3)] - - fig, axes = plt.subplots(1, 3, figsize=(15, 5)) - - panels = [ - ( - "XY", - axes[0], - node[0], - node[1], - face[0], - face[1], - nd[0], - nd[1], - vd[0].dim1, - vd[1].dim1, - vd[0].padding, - vd[1].padding, - ), - ( - "XZ", - axes[1], - node[0], - node[2], - face[0], - face[2], - nd[0], - nd[2], - vd[0].dim1, - vd[2].dim1, - vd[0].padding, - vd[2].padding, - ), - ( - "YZ", - axes[2], - node[1], - node[2], - face[1], - face[2], - nd[1], - nd[2], - vd[1].dim1, - vd[2].dim1, - vd[1].padding, - vd[2].padding, - ), - ] - - for label, ax, n_h, n_v, f_h, f_v, dim_h, dim_v, face_h, face_v, pad_h, pad_v in panels: - for x in n_h: - ax.axvline(x, color="lightgray", lw=0.7, zorder=0) - for y in n_v: - ax.axhline(y, color="lightgray", lw=0.7, zorder=0) - - nh, nv = np.meshgrid(n_h, n_v) - ax.scatter(nh.ravel(), nv.ravel(), s=50, color="steelblue", zorder=3, marker="o", label="node") - - fhh, fhv = np.meshgrid(f_h, n_v) - ax.scatter(fhh.ravel(), fhv.ravel(), s=50, color="firebrick", zorder=3, marker=">", label=f"{face_h}-face") - - fvh, fvv = np.meshgrid(n_h, f_v) - ax.scatter(fvh.ravel(), fvv.ravel(), s=50, color="forestgreen", zorder=3, marker="^", label=f"{face_v}-face") - - ax.set_xlabel(dim_h) - ax.set_ylabel(dim_v) - ax.set_title(f"{label} cross-section\npad: {pad_h.value} / {pad_v.value}") - ax.set_aspect("equal") - ax.legend(fontsize=7) - - fig.suptitle("Grid3DMetadata — staggered positions", fontsize=12) - fig.tight_layout() - return fig - - 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() diff --git a/tests/utils/test_sgrid.py b/tests/utils/test_sgrid.py index a4d702c236..d63ba97c0e 100644 --- a/tests/utils/test_sgrid.py +++ b/tests/utils/test_sgrid.py @@ -360,8 +360,8 @@ def test_rename_dataset(ds): (create_example_grid3dmetadata(with_node_coordinates=True), ""), ], ) -def test_grid_text_repr(metadata, expected): - actual = sgrid.to_ascii(metadata) +def test_grid_str(metadata, expected): + actual = str(metadata) if actual != expected: diff = "\n".join( difflib.unified_diff( @@ -412,5 +412,5 @@ def test_grid_text_repr(metadata, expected): ], ) def test_face_node_padding_ascii(face_node_padding: sgrid.DimDimPadding, expected: str): - actual = sgrid._face_node_padding_ascii(face_node_padding) + actual = sgrid._face_node_padding_to_text(face_node_padding) assert actual == expected From d17845eeaf5aa7299467b8d752e90cdbd301ded2 Mon Sep 17 00:00:00 2001 From: Vecko <36369090+VeckoTheGecko@users.noreply.github.com> Date: Tue, 24 Mar 2026 12:23:31 +0100 Subject: [PATCH 04/11] Update expected outputs --- tests/utils/test_sgrid.py | 176 ++++++++++++++++++++++++++++++++++---- 1 file changed, 161 insertions(+), 15 deletions(-) diff --git a/tests/utils/test_sgrid.py b/tests/utils/test_sgrid.py index d63ba97c0e..f7c725d80c 100644 --- a/tests/utils/test_sgrid.py +++ b/tests/utils/test_sgrid.py @@ -1,4 +1,3 @@ -import difflib import itertools import numpy as np @@ -354,24 +353,171 @@ def test_rename_dataset(ds): @pytest.mark.parametrize( ("metadata, expected"), [ - (create_example_grid2dmetadata(with_vertical_dimensions=False, with_node_coordinates=False), ""), - (create_example_grid2dmetadata(with_vertical_dimensions=True, with_node_coordinates=True), ""), - (create_example_grid3dmetadata(with_node_coordinates=False), ""), - (create_example_grid3dmetadata(with_node_coordinates=True), ""), + ( + create_example_grid2dmetadata(with_vertical_dimensions=False, with_node_coordinates=False), + """Grid2DMetadata + X-axis: face='face_dimension1' node='node_dimension1' padding=low + Y-axis: face='face_dimension2' node='node_dimension2' padding=low + + Staggered grid layout (symbolic 3x3 nodes): + + ↑ Y + | + n --u-- n --u-- n + | | | + v · v · v + | | | + n --u-- n --u-- n + | | | + v · v · v + | | | + n --u-- n --u-- n --→ X + + n = node (node_dimension1, node_dimension2) + u = x-face (face_dimension1) + v = y-face (face_dimension2) + · = cell centre + + Axis padding: + + face_dimension1:node_dimension1 (padding:low) + ─────●─────●─────●─────●─────● + 1 1 2 2 3 3 4 4 5 5 + + face_dimension2:node_dimension2 (padding:low) + ─────●─────●─────●─────●─────● + 1 1 2 2 3 3 4 4 5 5""", + ), + ( + create_example_grid2dmetadata(with_vertical_dimensions=True, with_node_coordinates=True), + """Grid2DMetadata + X-axis: face='face_dimension1' node='node_dimension1' padding=low + Y-axis: face='face_dimension2' node='node_dimension2' padding=low + Z-axis: face='vertical_dimensions_dim1' node='vertical_dimensions_dim2' padding=low + Coordinates: node_coordinates_var1, node_coordinates_var2 + + Staggered grid layout (symbolic 3x3 nodes): + + ↑ Y ↑ Z + | | + n --u-- n --u-- n w + | | | | + v · v · v · + | | | | + n --u-- n --u-- n w + | | | | + v · v · v · + | | | | + n --u-- n --u-- n --→ X w + + n = node (node_dimension1, node_dimension2) + u = x-face (face_dimension1) + v = y-face (face_dimension2) + w = z-node (vertical_dimensions_dim2) + · = cell centre + + Axis padding: + + face_dimension1:node_dimension1 (padding:low) + ─────●─────●─────●─────●─────● + 1 1 2 2 3 3 4 4 5 5 + + face_dimension2:node_dimension2 (padding:low) + ─────●─────●─────●─────●─────● + 1 1 2 2 3 3 4 4 5 5 + + vertical_dimensions_dim1:vertical_dimensions_dim2 (padding:low) + ─────●─────●─────●─────●─────● + 1 1 2 2 3 3 4 4 5 5""", + ), + ( + create_example_grid3dmetadata(with_node_coordinates=False), + """Grid3DMetadata + X-axis: face='face_dimension1' node='node_dimension1' padding=low + Y-axis: face='face_dimension2' node='node_dimension2' padding=low + Z-axis: face='face_dimension3' node='node_dimension3' padding=low + + Staggered grid layout (XY cross-section; Z-faces not shown): + + ↑ Y + | + n --u-- n --u-- n + | | | + v · v · v + | | | + n --u-- n --u-- n + | | | + v · v · v + | | | + n --u-- n --u-- n --→ X + + n = node (node_dimension1, node_dimension2, node_dimension3) + u = x-face (face_dimension1) + v = y-face (face_dimension2) + w = z-face (face_dimension3) [not shown in cross-section] + · = cell centre + + Axis padding: + + face_dimension1:node_dimension1 (padding:low) + ─────●─────●─────●─────●─────● + 1 1 2 2 3 3 4 4 5 5 + + face_dimension2:node_dimension2 (padding:low) + ─────●─────●─────●─────●─────● + 1 1 2 2 3 3 4 4 5 5 + + face_dimension3:node_dimension3 (padding:low) + ─────●─────●─────●─────●─────● + 1 1 2 2 3 3 4 4 5 5""", + ), + ( + create_example_grid3dmetadata(with_node_coordinates=True), + """Grid3DMetadata + X-axis: face='face_dimension1' node='node_dimension1' padding=low + Y-axis: face='face_dimension2' node='node_dimension2' padding=low + Z-axis: face='face_dimension3' node='node_dimension3' padding=low + Coordinates: node_coordinates_var1, node_coordinates_var2, node_coordinates_dim3 + + Staggered grid layout (XY cross-section; Z-faces not shown): + + ↑ Y + | + n --u-- n --u-- n + | | | + v · v · v + | | | + n --u-- n --u-- n + | | | + v · v · v + | | | + n --u-- n --u-- n --→ X + + n = node (node_dimension1, node_dimension2, node_dimension3) + u = x-face (face_dimension1) + v = y-face (face_dimension2) + w = z-face (face_dimension3) [not shown in cross-section] + · = cell centre + + Axis padding: + + face_dimension1:node_dimension1 (padding:low) + ─────●─────●─────●─────●─────● + 1 1 2 2 3 3 4 4 5 5 + + face_dimension2:node_dimension2 (padding:low) + ─────●─────●─────●─────●─────● + 1 1 2 2 3 3 4 4 5 5 + + face_dimension3:node_dimension3 (padding:low) + ─────●─────●─────●─────●─────● + 1 1 2 2 3 3 4 4 5 5""", + ), ], ) def test_grid_str(metadata, expected): actual = str(metadata) - if actual != expected: - diff = "\n".join( - difflib.unified_diff( - expected.splitlines(keepends=True), - actual.splitlines(keepends=True), - fromfile="expected", - tofile="actual", - ) - ) - pytest.fail(f"grid_text_repr output differs:\n{diff}") + assert actual == expected @pytest.mark.parametrize( From 0755b489eb75df8cc80e52a3836911a39bf11a07 Mon Sep 17 00:00:00 2001 From: Vecko <36369090+VeckoTheGecko@users.noreply.github.com> Date: Tue, 24 Mar 2026 13:02:50 +0100 Subject: [PATCH 05/11] refactor --- src/parcels/_core/utils/sgrid.py | 46 ++++++++++---------------------- tests/utils/test_sgrid.py | 4 +-- 2 files changed, 16 insertions(+), 34 deletions(-) diff --git a/src/parcels/_core/utils/sgrid.py b/src/parcels/_core/utils/sgrid.py index 4e2f247db1..2e423c80cb 100644 --- a/src/parcels/_core/utils/sgrid.py +++ b/src/parcels/_core/utils/sgrid.py @@ -339,6 +339,9 @@ def load(cls, s: str) -> Self: padding = Padding(match.group(3).lower()) return cls(dim1, dim2, padding) + def to_diagram(self) -> str: + return _face_node_padding_to_text(self) + def dump_mappings(parts: Iterable[DimDimPadding | Dim]) -> str: """Takes in a list of edge-node-padding tuples and serializes them into a string @@ -536,52 +539,31 @@ def _face_node_padding_to_text(obj: DimDimPadding) -> list[str]: FACE_WIDTH = 5 # dashes per face segment padding = obj.padding - layouts = { + bars = { Padding.NONE: "x-x-x-x-x", Padding.LOW: "-x-x-x-x-x", Padding.HIGH: "x-x-x-x-x-", Padding.BOTH: "-x-x-x-x-x-", } - - # Build an ordered sequence of ('n', index) / ('f', index) elements - seq_str = layouts[obj.padding] - seq: list[tuple[Literal["n", "f"], int]] = [] + bar = bars[obj.padding] node_count = 0 face_count = 0 - for char in seq_str: + bar_rendered = "" + label = "" + for char in bar: if char == "x": node_count += 1 - seq.append(("n", node_count)) + bar_rendered += "●" + label += str(node_count) elif char == "-": face_count += 1 - seq.append(("f", face_count)) - - bar_parts: list[str] = [] - label_positions: dict[int, str] = {} - pos = 0 - for typ, idx in seq: - if typ == "n": - bar_parts.append("●") - label_positions[pos] = str(idx) - pos += 1 - else: - bar_parts.append("─" * FACE_WIDTH) - label_positions[pos + FACE_WIDTH // 2] = str(idx) - pos += FACE_WIDTH - - bar = "".join(bar_parts) - - max_pos = max(p + len(lbl) for p, lbl in label_positions.items()) - label_chars = [" "] * max_pos - for p, lbl in label_positions.items(): - for i, c in enumerate(lbl): - label_chars[p + i] = c - label = "".join(label_chars).rstrip() + bar_rendered += "─" * FACE_WIDTH + label += str(face_count).center(FACE_WIDTH) return [ f" {obj.dim1}:{obj.dim2} (padding:{padding.value})", - f" {bar}", - f" {label}", + f" {bar_rendered}", + f" {label.rstrip()}", ] diff --git a/tests/utils/test_sgrid.py b/tests/utils/test_sgrid.py index f7c725d80c..dcf117f1cc 100644 --- a/tests/utils/test_sgrid.py +++ b/tests/utils/test_sgrid.py @@ -557,6 +557,6 @@ def test_grid_str(metadata, expected): ), ], ) -def test_face_node_padding_ascii(face_node_padding: sgrid.DimDimPadding, expected: str): - actual = sgrid._face_node_padding_to_text(face_node_padding) +def test_face_node_padding_to_diagram(face_node_padding: sgrid.DimDimPadding, expected: str): + actual = face_node_padding.to_diagram() assert actual == expected From e5be85fe577e5207cd28a908ab012593b77cb20a Mon Sep 17 00:00:00 2001 From: Vecko <36369090+VeckoTheGecko@users.noreply.github.com> Date: Tue, 24 Mar 2026 13:20:11 +0100 Subject: [PATCH 06/11] Refactor indentation --- src/parcels/_core/utils/sgrid.py | 24 ++++++++++++++---------- tests/utils/test_sgrid.py | 31 ++++++++++++++++--------------- 2 files changed, 30 insertions(+), 25 deletions(-) diff --git a/src/parcels/_core/utils/sgrid.py b/src/parcels/_core/utils/sgrid.py index 2e423c80cb..6a799ecbdc 100644 --- a/src/parcels/_core/utils/sgrid.py +++ b/src/parcels/_core/utils/sgrid.py @@ -26,6 +26,10 @@ Dim = str +def indent_lines(lst: list[str], indent: int = 2): + return [indent * " " + line for line in lst] + + class Padding(enum.Enum): NONE = "none" LOW = "low" @@ -340,7 +344,7 @@ def load(cls, s: str) -> Self: return cls(dim1, dim2, padding) def to_diagram(self) -> str: - return _face_node_padding_to_text(self) + return "\n".join(_face_node_padding_to_text(self)) def dump_mappings(parts: Iterable[DimDimPadding | Dim]) -> str: @@ -561,9 +565,9 @@ def _face_node_padding_to_text(obj: DimDimPadding) -> list[str]: label += str(face_count).center(FACE_WIDTH) return [ - f" {obj.dim1}:{obj.dim2} (padding:{padding.value})", - f" {bar_rendered}", - f" {label.rstrip()}", + f"{obj.dim1}:{obj.dim2} (padding:{padding.value})", + f" {bar_rendered}", + f" {label.rstrip()}", ] @@ -631,12 +635,12 @@ def _z(base: str, sym: str) -> str: " · = cell centre", ] lines += ["", " Axis padding:", ""] - lines += _face_node_padding_to_text(fd[0]) + lines += indent_lines(_face_node_padding_to_text(fd[0])) lines += [""] - lines += _face_node_padding_to_text(fd[1]) + lines += indent_lines(_face_node_padding_to_text(fd[1])) if grid.vertical_dimensions: lines += [""] - lines += _face_node_padding_to_text(grid.vertical_dimensions[0]) + lines += indent_lines(_face_node_padding_to_text(grid.vertical_dimensions[0])) return "\n".join(lines) @@ -674,11 +678,11 @@ def _grid3d_to_ascii(grid: Grid3DMetadata) -> str: " · = cell centre", ] lines += ["", " Axis padding:", ""] - lines += _face_node_padding_to_text(vd[0]) + lines += indent_lines(_face_node_padding_to_text(vd[0])) lines += [""] - lines += _face_node_padding_to_text(vd[1]) + lines += indent_lines(_face_node_padding_to_text(vd[1])) lines += [""] - lines += _face_node_padding_to_text(vd[2]) + lines += indent_lines(_face_node_padding_to_text(vd[2])) return "\n".join(lines) diff --git a/tests/utils/test_sgrid.py b/tests/utils/test_sgrid.py index dcf117f1cc..cb86c3160b 100644 --- a/tests/utils/test_sgrid.py +++ b/tests/utils/test_sgrid.py @@ -521,42 +521,43 @@ def test_grid_str(metadata, expected): @pytest.mark.parametrize( - ("face_node_padding", "expected"), + ("face_node_padding", "expected_lines"), [ ( sgrid.DimDimPadding("face", "node", sgrid.Padding.LOW), [ - " face:node (padding:low)", - " ─────●─────●─────●─────●─────●", - " 1 1 2 2 3 3 4 4 5 5", + "face:node (padding:low)", + " ─────●─────●─────●─────●─────●", + " 1 1 2 2 3 3 4 4 5 5", ], ), ( sgrid.DimDimPadding("face", "node", sgrid.Padding.HIGH), [ - " face:node (padding:high)", - " ●─────●─────●─────●─────●─────", - " 1 1 2 2 3 3 4 4 5 5", + "face:node (padding:high)", + " ●─────●─────●─────●─────●─────", + " 1 1 2 2 3 3 4 4 5 5", ], ), ( sgrid.DimDimPadding("face", "node", sgrid.Padding.BOTH), [ - " face:node (padding:both)", - " ─────●─────●─────●─────●─────●─────", - " 1 1 2 2 3 3 4 4 5 5 6", + "face:node (padding:both)", + " ─────●─────●─────●─────●─────●─────", + " 1 1 2 2 3 3 4 4 5 5 6", ], ), ( sgrid.DimDimPadding("face", "node", sgrid.Padding.NONE), [ - " face:node (padding:none)", - " ●─────●─────●─────●─────●", - " 1 1 2 2 3 3 4 4 5", + "face:node (padding:none)", + " ●─────●─────●─────●─────●", + " 1 1 2 2 3 3 4 4 5", ], ), ], ) -def test_face_node_padding_to_diagram(face_node_padding: sgrid.DimDimPadding, expected: str): +def test_face_node_padding_to_diagram(face_node_padding: sgrid.DimDimPadding, expected_lines: list[str]): actual = face_node_padding.to_diagram() - assert actual == expected + lines = actual.split("\n") + assert lines == expected_lines From 9e0cfe3f99336d281e0d92d8eb920bcf57648ff2 Mon Sep 17 00:00:00 2001 From: Vecko <36369090+VeckoTheGecko@users.noreply.github.com> Date: Tue, 24 Mar 2026 14:23:00 +0100 Subject: [PATCH 07/11] rename function --- src/parcels/_core/utils/sgrid.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/parcels/_core/utils/sgrid.py b/src/parcels/_core/utils/sgrid.py index 6a799ecbdc..272f4be8e5 100644 --- a/src/parcels/_core/utils/sgrid.py +++ b/src/parcels/_core/utils/sgrid.py @@ -26,7 +26,7 @@ Dim = str -def indent_lines(lst: list[str], indent: int = 2): +def _indent_lines(lst: list[str], indent: int = 2): return [indent * " " + line for line in lst] @@ -635,12 +635,12 @@ def _z(base: str, sym: str) -> str: " · = cell centre", ] lines += ["", " Axis padding:", ""] - lines += indent_lines(_face_node_padding_to_text(fd[0])) + lines += _indent_lines(_face_node_padding_to_text(fd[0])) lines += [""] - lines += indent_lines(_face_node_padding_to_text(fd[1])) + lines += _indent_lines(_face_node_padding_to_text(fd[1])) if grid.vertical_dimensions: lines += [""] - lines += indent_lines(_face_node_padding_to_text(grid.vertical_dimensions[0])) + lines += _indent_lines(_face_node_padding_to_text(grid.vertical_dimensions[0])) return "\n".join(lines) @@ -678,11 +678,11 @@ def _grid3d_to_ascii(grid: Grid3DMetadata) -> str: " · = cell centre", ] lines += ["", " Axis padding:", ""] - lines += indent_lines(_face_node_padding_to_text(vd[0])) + lines += _indent_lines(_face_node_padding_to_text(vd[0])) lines += [""] - lines += indent_lines(_face_node_padding_to_text(vd[1])) + lines += _indent_lines(_face_node_padding_to_text(vd[1])) lines += [""] - lines += indent_lines(_face_node_padding_to_text(vd[2])) + lines += _indent_lines(_face_node_padding_to_text(vd[2])) return "\n".join(lines) From a1deb9f77453a8632aff41551b8b5f54d311c46a Mon Sep 17 00:00:00 2001 From: Vecko <36369090+VeckoTheGecko@users.noreply.github.com> Date: Wed, 25 Mar 2026 12:27:42 +0100 Subject: [PATCH 08/11] Count from 0 --- src/parcels/_core/utils/sgrid.py | 4 ++-- tests/utils/test_sgrid.py | 30 +++++++++++++++--------------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/parcels/_core/utils/sgrid.py b/src/parcels/_core/utils/sgrid.py index 272f4be8e5..b5fdc00612 100644 --- a/src/parcels/_core/utils/sgrid.py +++ b/src/parcels/_core/utils/sgrid.py @@ -556,13 +556,13 @@ def _face_node_padding_to_text(obj: DimDimPadding) -> list[str]: label = "" for char in bar: if char == "x": - node_count += 1 bar_rendered += "●" label += str(node_count) + node_count += 1 elif char == "-": - face_count += 1 bar_rendered += "─" * FACE_WIDTH label += str(face_count).center(FACE_WIDTH) + face_count += 1 return [ f"{obj.dim1}:{obj.dim2} (padding:{padding.value})", diff --git a/tests/utils/test_sgrid.py b/tests/utils/test_sgrid.py index cb86c3160b..b5559b9adb 100644 --- a/tests/utils/test_sgrid.py +++ b/tests/utils/test_sgrid.py @@ -382,11 +382,11 @@ def test_rename_dataset(ds): face_dimension1:node_dimension1 (padding:low) ─────●─────●─────●─────●─────● - 1 1 2 2 3 3 4 4 5 5 + 0 0 1 1 2 2 3 3 4 4 face_dimension2:node_dimension2 (padding:low) ─────●─────●─────●─────●─────● - 1 1 2 2 3 3 4 4 5 5""", + 0 0 1 1 2 2 3 3 4 4""", ), ( create_example_grid2dmetadata(with_vertical_dimensions=True, with_node_coordinates=True), @@ -420,15 +420,15 @@ def test_rename_dataset(ds): face_dimension1:node_dimension1 (padding:low) ─────●─────●─────●─────●─────● - 1 1 2 2 3 3 4 4 5 5 + 0 0 1 1 2 2 3 3 4 4 face_dimension2:node_dimension2 (padding:low) ─────●─────●─────●─────●─────● - 1 1 2 2 3 3 4 4 5 5 + 0 0 1 1 2 2 3 3 4 4 vertical_dimensions_dim1:vertical_dimensions_dim2 (padding:low) ─────●─────●─────●─────●─────● - 1 1 2 2 3 3 4 4 5 5""", + 0 0 1 1 2 2 3 3 4 4""", ), ( create_example_grid3dmetadata(with_node_coordinates=False), @@ -461,15 +461,15 @@ def test_rename_dataset(ds): face_dimension1:node_dimension1 (padding:low) ─────●─────●─────●─────●─────● - 1 1 2 2 3 3 4 4 5 5 + 0 0 1 1 2 2 3 3 4 4 face_dimension2:node_dimension2 (padding:low) ─────●─────●─────●─────●─────● - 1 1 2 2 3 3 4 4 5 5 + 0 0 1 1 2 2 3 3 4 4 face_dimension3:node_dimension3 (padding:low) ─────●─────●─────●─────●─────● - 1 1 2 2 3 3 4 4 5 5""", + 0 0 1 1 2 2 3 3 4 4""", ), ( create_example_grid3dmetadata(with_node_coordinates=True), @@ -503,15 +503,15 @@ def test_rename_dataset(ds): face_dimension1:node_dimension1 (padding:low) ─────●─────●─────●─────●─────● - 1 1 2 2 3 3 4 4 5 5 + 0 0 1 1 2 2 3 3 4 4 face_dimension2:node_dimension2 (padding:low) ─────●─────●─────●─────●─────● - 1 1 2 2 3 3 4 4 5 5 + 0 0 1 1 2 2 3 3 4 4 face_dimension3:node_dimension3 (padding:low) ─────●─────●─────●─────●─────● - 1 1 2 2 3 3 4 4 5 5""", + 0 0 1 1 2 2 3 3 4 4""", ), ], ) @@ -528,7 +528,7 @@ def test_grid_str(metadata, expected): [ "face:node (padding:low)", " ─────●─────●─────●─────●─────●", - " 1 1 2 2 3 3 4 4 5 5", + " 0 0 1 1 2 2 3 3 4 4", ], ), ( @@ -536,7 +536,7 @@ def test_grid_str(metadata, expected): [ "face:node (padding:high)", " ●─────●─────●─────●─────●─────", - " 1 1 2 2 3 3 4 4 5 5", + " 0 0 1 1 2 2 3 3 4 4", ], ), ( @@ -544,7 +544,7 @@ def test_grid_str(metadata, expected): [ "face:node (padding:both)", " ─────●─────●─────●─────●─────●─────", - " 1 1 2 2 3 3 4 4 5 5 6", + " 0 0 1 1 2 2 3 3 4 4 5", ], ), ( @@ -552,7 +552,7 @@ def test_grid_str(metadata, expected): [ "face:node (padding:none)", " ●─────●─────●─────●─────●", - " 1 1 2 2 3 3 4 4 5", + " 0 0 1 1 2 2 3 3 4", ], ), ], From da22d68952720f2d40ae07e6dc7dbf0d5a2306c3 Mon Sep 17 00:00:00 2001 From: Vecko <36369090+VeckoTheGecko@users.noreply.github.com> Date: Wed, 25 Mar 2026 12:31:08 +0100 Subject: [PATCH 09/11] Fix indent --- src/parcels/_core/utils/sgrid.py | 28 +++++++++++------------ tests/utils/test_sgrid.py | 38 ++++++++++++++++---------------- 2 files changed, 33 insertions(+), 33 deletions(-) diff --git a/src/parcels/_core/utils/sgrid.py b/src/parcels/_core/utils/sgrid.py index b5fdc00612..7022b56c83 100644 --- a/src/parcels/_core/utils/sgrid.py +++ b/src/parcels/_core/utils/sgrid.py @@ -606,11 +606,11 @@ def _z(base: str, sym: str) -> str: _z(" | | |", "|"), " n --u-- n --u-- n --→ X w", "", - f" n = node ({nd[0]}, {nd[1]})", - f" u = x-face ({fd[0].dim1})", - f" v = y-face ({fd[1].dim1})", - f" w = z-node ({vd.dim2})", - " · = cell centre", + f" n = node ({nd[0]}, {nd[1]})", + f" u = x-face ({fd[0].dim1})", + f" v = y-face ({fd[1].dim1})", + f" w = z-node ({vd.dim2})", + " · = cell centre", ] else: lines += [ @@ -629,10 +629,10 @@ def _z(base: str, sym: str) -> str: " | | |", " n --u-- n --u-- n --→ X", "", - f" n = node ({nd[0]}, {nd[1]})", - f" u = x-face ({fd[0].dim1})", - f" v = y-face ({fd[1].dim1})", - " · = cell centre", + f" n = node ({nd[0]}, {nd[1]})", + f" u = x-face ({fd[0].dim1})", + f" v = y-face ({fd[1].dim1})", + " · = cell centre", ] lines += ["", " Axis padding:", ""] lines += _indent_lines(_face_node_padding_to_text(fd[0])) @@ -671,11 +671,11 @@ def _grid3d_to_ascii(grid: Grid3DMetadata) -> str: " | | |", " n --u-- n --u-- n --→ X", "", - f" n = node ({nd[0]}, {nd[1]}, {nd[2]})", - f" u = x-face ({vd[0].dim1})", - f" v = y-face ({vd[1].dim1})", - f" w = z-face ({vd[2].dim1}) [not shown in cross-section]", - " · = cell centre", + f" n = node ({nd[0]}, {nd[1]}, {nd[2]})", + f" u = x-face ({vd[0].dim1})", + f" v = y-face ({vd[1].dim1})", + f" w = z-face ({vd[2].dim1}) [not shown in cross-section]", + " · = cell centre", ] lines += ["", " Axis padding:", ""] lines += _indent_lines(_face_node_padding_to_text(vd[0])) diff --git a/tests/utils/test_sgrid.py b/tests/utils/test_sgrid.py index b5559b9adb..43588a987c 100644 --- a/tests/utils/test_sgrid.py +++ b/tests/utils/test_sgrid.py @@ -373,10 +373,10 @@ def test_rename_dataset(ds): | | | n --u-- n --u-- n --→ X - n = node (node_dimension1, node_dimension2) - u = x-face (face_dimension1) - v = y-face (face_dimension2) - · = cell centre + n = node (node_dimension1, node_dimension2) + u = x-face (face_dimension1) + v = y-face (face_dimension2) + · = cell centre Axis padding: @@ -410,11 +410,11 @@ def test_rename_dataset(ds): | | | | n --u-- n --u-- n --→ X w - n = node (node_dimension1, node_dimension2) - u = x-face (face_dimension1) - v = y-face (face_dimension2) - w = z-node (vertical_dimensions_dim2) - · = cell centre + n = node (node_dimension1, node_dimension2) + u = x-face (face_dimension1) + v = y-face (face_dimension2) + w = z-node (vertical_dimensions_dim2) + · = cell centre Axis padding: @@ -451,11 +451,11 @@ def test_rename_dataset(ds): | | | n --u-- n --u-- n --→ X - n = node (node_dimension1, node_dimension2, node_dimension3) - u = x-face (face_dimension1) - v = y-face (face_dimension2) - w = z-face (face_dimension3) [not shown in cross-section] - · = cell centre + n = node (node_dimension1, node_dimension2, node_dimension3) + u = x-face (face_dimension1) + v = y-face (face_dimension2) + w = z-face (face_dimension3) [not shown in cross-section] + · = cell centre Axis padding: @@ -493,11 +493,11 @@ def test_rename_dataset(ds): | | | n --u-- n --u-- n --→ X - n = node (node_dimension1, node_dimension2, node_dimension3) - u = x-face (face_dimension1) - v = y-face (face_dimension2) - w = z-face (face_dimension3) [not shown in cross-section] - · = cell centre + n = node (node_dimension1, node_dimension2, node_dimension3) + u = x-face (face_dimension1) + v = y-face (face_dimension2) + w = z-face (face_dimension3) [not shown in cross-section] + · = cell centre Axis padding: From 3df112f8d135f809432b3a29611cb50409445f8c Mon Sep 17 00:00:00 2001 From: Vecko <36369090+VeckoTheGecko@users.noreply.github.com> Date: Wed, 25 Mar 2026 13:18:04 +0100 Subject: [PATCH 10/11] Refactor --- src/parcels/_core/utils/sgrid.py | 148 ++++++++++++++++--------------- 1 file changed, 78 insertions(+), 70 deletions(-) diff --git a/src/parcels/_core/utils/sgrid.py b/src/parcels/_core/utils/sgrid.py index 7022b56c83..bd47b667d4 100644 --- a/src/parcels/_core/utils/sgrid.py +++ b/src/parcels/_core/utils/sgrid.py @@ -15,6 +15,7 @@ import re from collections.abc import Hashable, Iterable from dataclasses import dataclass +from textwrap import indent from typing import Any, Literal, Protocol, Self, cast, overload import xarray as xr @@ -571,6 +572,69 @@ def _face_node_padding_to_text(obj: DimDimPadding) -> list[str]: ] +TEXT_GRID2D_WITHOUT_Z = """ +Staggered grid layout (symbolic 3x3 nodes): + + ↑ Y + | + n --u-- n --u-- n + | | | + v · v · v + | | | + n --u-- n --u-- n + | | | + v · v · v + | | | + n --u-- n --u-- n --→ X + + n = node ({n1}, {n2}) + u = x-face ({u}) + v = y-face ({v}) + · = cell centre""" + +TEXT_GRID2D_WITH_Z = """ +Staggered grid layout (symbolic 3x3 nodes): + + ↑ Y ↑ Z + | | + n --u-- n --u-- n w + | | | | + v · v · v · + | | | | + n --u-- n --u-- n w + | | | | + v · v · v · + | | | | + n --u-- n --u-- n --→ X w + + n = node ({n1}, {n2}) + u = x-face ({u}) + v = y-face ({v}) + w = z-node ({w}) + · = cell centre""" + +TEXT_GRID3D = """ +Staggered grid layout (XY cross-section; Z-faces not shown): + + ↑ Y + | + n --u-- n --u-- n + | | | + v · v · v + | | | + n --u-- n --u-- n + | | | + v · v · v + | | | + n --u-- n --u-- n --→ X + + n = node ({n1}, {n2}, {n3}) + u = x-face ({u}) + v = y-face ({v}) + w = z-face ({w}) [not shown in cross-section] + · = cell centre""" + + def _grid2d_to_ascii(grid: Grid2DMetadata) -> str: fd = grid.face_dimensions nd = grid.node_dimensions @@ -584,56 +648,15 @@ def _grid2d_to_ascii(grid: Grid2DMetadata) -> str: lines.append(f" Z-axis: face={vd.dim1!r} node={vd.dim2!r} padding={vd.padding.value}") if grid.node_coordinates: lines.append(f" Coordinates: {grid.node_coordinates[0]}, {grid.node_coordinates[1]}") - if grid.vertical_dimensions: - vd = grid.vertical_dimensions[0] - def _z(base: str, sym: str) -> str: - return base.ljust(28) + sym - - lines += [ - "", - " Staggered grid layout (symbolic 3x3 nodes):", - "", - _z(" ↑ Y", "↑ Z"), - _z(" |", "|"), - _z(" n --u-- n --u-- n", "w"), - _z(" | | |", "|"), - _z(" v · v · v", "·"), - _z(" | | |", "|"), - _z(" n --u-- n --u-- n", "w"), - _z(" | | |", "|"), - _z(" v · v · v", "·"), - _z(" | | |", "|"), - " n --u-- n --u-- n --→ X w", - "", - f" n = node ({nd[0]}, {nd[1]})", - f" u = x-face ({fd[0].dim1})", - f" v = y-face ({fd[1].dim1})", - f" w = z-node ({vd.dim2})", - " · = cell centre", - ] + format_kwargs = dict(n1=nd[0], n2=nd[1], u=fd[0].dim1, v=fd[1].dim1) + + if grid.vertical_dimensions: + format_kwargs["w"] = grid.vertical_dimensions[0].dim2 + lines += indent(TEXT_GRID2D_WITH_Z, " ").format(**format_kwargs).split("\n") else: - lines += [ - "", - " Staggered grid layout (symbolic 3x3 nodes):", - "", - " ↑ Y", - " |", - " n --u-- n --u-- n", - " | | |", - " v · v · v", - " | | |", - " n --u-- n --u-- n", - " | | |", - " v · v · v", - " | | |", - " n --u-- n --u-- n --→ X", - "", - f" n = node ({nd[0]}, {nd[1]})", - f" u = x-face ({fd[0].dim1})", - f" v = y-face ({fd[1].dim1})", - " · = cell centre", - ] + lines += indent(TEXT_GRID2D_WITHOUT_Z, " ").format(**format_kwargs).split("\n") + lines += ["", " Axis padding:", ""] lines += _indent_lines(_face_node_padding_to_text(fd[0])) lines += [""] @@ -655,28 +678,13 @@ def _grid3d_to_ascii(grid: Grid3DMetadata) -> str: ] if grid.node_coordinates: lines.append(f" Coordinates: {', '.join(grid.node_coordinates)}") - lines += [ - "", - " Staggered grid layout (XY cross-section; Z-faces not shown):", - "", - " ↑ Y", - " |", - " n --u-- n --u-- n", - " | | |", - " v · v · v", - " | | |", - " n --u-- n --u-- n", - " | | |", - " v · v · v", - " | | |", - " n --u-- n --u-- n --→ X", - "", - f" n = node ({nd[0]}, {nd[1]}, {nd[2]})", - f" u = x-face ({vd[0].dim1})", - f" v = y-face ({vd[1].dim1})", - f" w = z-face ({vd[2].dim1}) [not shown in cross-section]", - " · = cell centre", - ] + + lines += ( + indent(TEXT_GRID3D, " ") + .format(n1=nd[0], n2=nd[1], n3=nd[2], u=vd[0].dim1, v=vd[1].dim1, w=vd[2].dim1) + .split("\n") + ) + lines += ["", " Axis padding:", ""] lines += _indent_lines(_face_node_padding_to_text(vd[0])) lines += [""] From 2020cf039ce82cdef70d326944e9c25c59d8dac9 Mon Sep 17 00:00:00 2001 From: Vecko <36369090+VeckoTheGecko@users.noreply.github.com> Date: Wed, 25 Mar 2026 13:23:20 +0100 Subject: [PATCH 11/11] rename vars --- src/parcels/_core/utils/sgrid.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/parcels/_core/utils/sgrid.py b/src/parcels/_core/utils/sgrid.py index bd47b667d4..2db7149642 100644 --- a/src/parcels/_core/utils/sgrid.py +++ b/src/parcels/_core/utils/sgrid.py @@ -572,7 +572,7 @@ def _face_node_padding_to_text(obj: DimDimPadding) -> list[str]: ] -TEXT_GRID2D_WITHOUT_Z = """ +_TEXT_GRID2D_WITHOUT_Z = """ Staggered grid layout (symbolic 3x3 nodes): ↑ Y @@ -592,7 +592,7 @@ def _face_node_padding_to_text(obj: DimDimPadding) -> list[str]: v = y-face ({v}) · = cell centre""" -TEXT_GRID2D_WITH_Z = """ +_TEXT_GRID2D_WITH_Z = """ Staggered grid layout (symbolic 3x3 nodes): ↑ Y ↑ Z @@ -613,7 +613,7 @@ def _face_node_padding_to_text(obj: DimDimPadding) -> list[str]: w = z-node ({w}) · = cell centre""" -TEXT_GRID3D = """ +_TEXT_GRID3D = """ Staggered grid layout (XY cross-section; Z-faces not shown): ↑ Y @@ -653,9 +653,9 @@ def _grid2d_to_ascii(grid: Grid2DMetadata) -> str: if grid.vertical_dimensions: format_kwargs["w"] = grid.vertical_dimensions[0].dim2 - lines += indent(TEXT_GRID2D_WITH_Z, " ").format(**format_kwargs).split("\n") + lines += indent(_TEXT_GRID2D_WITH_Z, " ").format(**format_kwargs).split("\n") else: - lines += indent(TEXT_GRID2D_WITHOUT_Z, " ").format(**format_kwargs).split("\n") + lines += indent(_TEXT_GRID2D_WITHOUT_Z, " ").format(**format_kwargs).split("\n") lines += ["", " Axis padding:", ""] lines += _indent_lines(_face_node_padding_to_text(fd[0])) @@ -680,7 +680,7 @@ def _grid3d_to_ascii(grid: Grid3DMetadata) -> str: lines.append(f" Coordinates: {', '.join(grid.node_coordinates)}") lines += ( - indent(TEXT_GRID3D, " ") + indent(_TEXT_GRID3D, " ") .format(n1=nd[0], n2=nd[1], n3=nd[2], u=vd[0].dim1, v=vd[1].dim1, w=vd[2].dim1) .split("\n") )