Skip to content

Commit 03fc33e

Browse files
String representations of SGRID layouts (#2553)
1 parent 1484818 commit 03fc33e

2 files changed

Lines changed: 402 additions & 0 deletions

File tree

src/parcels/_core/utils/sgrid.py

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import re
1616
from collections.abc import Hashable, Iterable
1717
from dataclasses import dataclass
18+
from textwrap import indent
1819
from typing import Any, Literal, Protocol, Self, cast, overload
1920

2021
import xarray as xr
@@ -26,6 +27,10 @@
2627
Dim = str
2728

2829

30+
def _indent_lines(lst: list[str], indent: int = 2):
31+
return [indent * " " + line for line in lst]
32+
33+
2934
class Padding(enum.Enum):
3035
NONE = "none"
3136
LOW = "low"
@@ -143,6 +148,9 @@ def __init__(
143148
def __repr__(self) -> str:
144149
return repr_from_dunder_dict(self)
145150

151+
def __str__(self) -> str:
152+
return _grid2d_to_ascii(self)
153+
146154
def __eq__(self, other: Any) -> bool:
147155
if not isinstance(other, Grid2DMetadata):
148156
return NotImplemented
@@ -256,6 +264,9 @@ def __init__(
256264
def __repr__(self) -> str:
257265
return repr_from_dunder_dict(self)
258266

267+
def __str__(self) -> str:
268+
return _grid3d_to_ascii(self)
269+
259270
def __eq__(self, other: Any) -> bool:
260271
if not isinstance(other, Grid3DMetadata):
261272
return NotImplemented
@@ -333,6 +344,9 @@ def load(cls, s: str) -> Self:
333344
padding = Padding(match.group(3).lower())
334345
return cls(dim1, dim2, padding)
335346

347+
def to_diagram(self) -> str:
348+
return "\n".join(_face_node_padding_to_text(self))
349+
336350

337351
def dump_mappings(parts: Iterable[DimDimPadding | Dim]) -> str:
338352
"""Takes in a list of edge-node-padding tuples and serializes them into a string
@@ -505,6 +519,181 @@ def get_unique_names(grid: Grid2DMetadata | Grid3DMetadata) -> set[str]:
505519
return dims
506520

507521

522+
def _face_node_padding_to_text(obj: DimDimPadding) -> list[str]:
523+
"""Return ASCII diagram lines showing a face-node padding relationship.
524+
525+
Produces a symbolic 5-node diagram like the image below, matching the
526+
four padding modes::
527+
528+
face:node (padding:none)
529+
●─────●─────●─────●─────●
530+
1 1 2 2 3 3 4 4 5
531+
532+
face:node (padding:low)
533+
─────●─────●─────●─────●─────●
534+
1 1 2 2 3 3 4 4 5 5
535+
536+
face:node (padding:high)
537+
●─────●─────●─────●─────●─────
538+
1 1 2 2 3 3 4 4 5 5
539+
540+
face:node (padding:both)
541+
─────●─────●─────●─────●─────●─────
542+
1 1 2 2 3 3 4 4 5 5 6
543+
"""
544+
FACE_WIDTH = 5 # dashes per face segment
545+
padding = obj.padding
546+
547+
bars = {
548+
Padding.NONE: "x-x-x-x-x",
549+
Padding.LOW: "-x-x-x-x-x",
550+
Padding.HIGH: "x-x-x-x-x-",
551+
Padding.BOTH: "-x-x-x-x-x-",
552+
}
553+
bar = bars[obj.padding]
554+
node_count = 0
555+
face_count = 0
556+
bar_rendered = ""
557+
label = ""
558+
for char in bar:
559+
if char == "x":
560+
bar_rendered += "●"
561+
label += str(node_count)
562+
node_count += 1
563+
elif char == "-":
564+
bar_rendered += "─" * FACE_WIDTH
565+
label += str(face_count).center(FACE_WIDTH)
566+
face_count += 1
567+
568+
return [
569+
f"{obj.dim1}:{obj.dim2} (padding:{padding.value})",
570+
f" {bar_rendered}",
571+
f" {label.rstrip()}",
572+
]
573+
574+
575+
_TEXT_GRID2D_WITHOUT_Z = """
576+
Staggered grid layout (symbolic 3x3 nodes):
577+
578+
↑ Y
579+
|
580+
n --u-- n --u-- n
581+
| | |
582+
v · v · v
583+
| | |
584+
n --u-- n --u-- n
585+
| | |
586+
v · v · v
587+
| | |
588+
n --u-- n --u-- n --→ X
589+
590+
n = node ({n1}, {n2})
591+
u = x-face ({u})
592+
v = y-face ({v})
593+
· = cell centre"""
594+
595+
_TEXT_GRID2D_WITH_Z = """
596+
Staggered grid layout (symbolic 3x3 nodes):
597+
598+
↑ Y ↑ Z
599+
| |
600+
n --u-- n --u-- n w
601+
| | | |
602+
v · v · v ·
603+
| | | |
604+
n --u-- n --u-- n w
605+
| | | |
606+
v · v · v ·
607+
| | | |
608+
n --u-- n --u-- n --→ X w
609+
610+
n = node ({n1}, {n2})
611+
u = x-face ({u})
612+
v = y-face ({v})
613+
w = z-node ({w})
614+
· = cell centre"""
615+
616+
_TEXT_GRID3D = """
617+
Staggered grid layout (XY cross-section; Z-faces not shown):
618+
619+
↑ Y
620+
|
621+
n --u-- n --u-- n
622+
| | |
623+
v · v · v
624+
| | |
625+
n --u-- n --u-- n
626+
| | |
627+
v · v · v
628+
| | |
629+
n --u-- n --u-- n --→ X
630+
631+
n = node ({n1}, {n2}, {n3})
632+
u = x-face ({u})
633+
v = y-face ({v})
634+
w = z-face ({w}) [not shown in cross-section]
635+
· = cell centre"""
636+
637+
638+
def _grid2d_to_ascii(grid: Grid2DMetadata) -> str:
639+
fd = grid.face_dimensions
640+
nd = grid.node_dimensions
641+
lines = [
642+
"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}",
645+
]
646+
if grid.vertical_dimensions:
647+
vd = grid.vertical_dimensions[0]
648+
lines.append(f" Z-axis: face={vd.dim1!r} node={vd.dim2!r} padding={vd.padding.value}")
649+
if grid.node_coordinates:
650+
lines.append(f" Coordinates: {grid.node_coordinates[0]}, {grid.node_coordinates[1]}")
651+
652+
format_kwargs = dict(n1=nd[0], n2=nd[1], u=fd[0].dim1, v=fd[1].dim1)
653+
654+
if grid.vertical_dimensions:
655+
format_kwargs["w"] = grid.vertical_dimensions[0].dim2
656+
lines += indent(_TEXT_GRID2D_WITH_Z, " ").format(**format_kwargs).split("\n")
657+
else:
658+
lines += indent(_TEXT_GRID2D_WITHOUT_Z, " ").format(**format_kwargs).split("\n")
659+
660+
lines += ["", " Axis padding:", ""]
661+
lines += _indent_lines(_face_node_padding_to_text(fd[0]))
662+
lines += [""]
663+
lines += _indent_lines(_face_node_padding_to_text(fd[1]))
664+
if grid.vertical_dimensions:
665+
lines += [""]
666+
lines += _indent_lines(_face_node_padding_to_text(grid.vertical_dimensions[0]))
667+
return "\n".join(lines)
668+
669+
670+
def _grid3d_to_ascii(grid: Grid3DMetadata) -> str:
671+
vd = grid.volume_dimensions
672+
nd = grid.node_dimensions
673+
lines = [
674+
"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}",
678+
]
679+
if grid.node_coordinates:
680+
lines.append(f" Coordinates: {', '.join(grid.node_coordinates)}")
681+
682+
lines += (
683+
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)
685+
.split("\n")
686+
)
687+
688+
lines += ["", " Axis padding:", ""]
689+
lines += _indent_lines(_face_node_padding_to_text(vd[0]))
690+
lines += [""]
691+
lines += _indent_lines(_face_node_padding_to_text(vd[1]))
692+
lines += [""]
693+
lines += _indent_lines(_face_node_padding_to_text(vd[2]))
694+
return "\n".join(lines)
695+
696+
508697
def _attach_sgrid_metadata(ds, grid: Grid2DMetadata | Grid3DMetadata):
509698
"""Copies the dataset and attaches the SGRID metadata in 'grid' variable. Modifies 'conventions' attribute."""
510699
ds = ds.copy()

0 commit comments

Comments
 (0)