|
15 | 15 | import re |
16 | 16 | from collections.abc import Hashable, Iterable |
17 | 17 | from dataclasses import dataclass |
| 18 | +from textwrap import indent |
18 | 19 | from typing import Any, Literal, Protocol, Self, cast, overload |
19 | 20 |
|
20 | 21 | import xarray as xr |
|
26 | 27 | Dim = str |
27 | 28 |
|
28 | 29 |
|
| 30 | +def _indent_lines(lst: list[str], indent: int = 2): |
| 31 | + return [indent * " " + line for line in lst] |
| 32 | + |
| 33 | + |
29 | 34 | class Padding(enum.Enum): |
30 | 35 | NONE = "none" |
31 | 36 | LOW = "low" |
@@ -143,6 +148,9 @@ def __init__( |
143 | 148 | def __repr__(self) -> str: |
144 | 149 | return repr_from_dunder_dict(self) |
145 | 150 |
|
| 151 | + def __str__(self) -> str: |
| 152 | + return _grid2d_to_ascii(self) |
| 153 | + |
146 | 154 | def __eq__(self, other: Any) -> bool: |
147 | 155 | if not isinstance(other, Grid2DMetadata): |
148 | 156 | return NotImplemented |
@@ -256,6 +264,9 @@ def __init__( |
256 | 264 | def __repr__(self) -> str: |
257 | 265 | return repr_from_dunder_dict(self) |
258 | 266 |
|
| 267 | + def __str__(self) -> str: |
| 268 | + return _grid3d_to_ascii(self) |
| 269 | + |
259 | 270 | def __eq__(self, other: Any) -> bool: |
260 | 271 | if not isinstance(other, Grid3DMetadata): |
261 | 272 | return NotImplemented |
@@ -333,6 +344,9 @@ def load(cls, s: str) -> Self: |
333 | 344 | padding = Padding(match.group(3).lower()) |
334 | 345 | return cls(dim1, dim2, padding) |
335 | 346 |
|
| 347 | + def to_diagram(self) -> str: |
| 348 | + return "\n".join(_face_node_padding_to_text(self)) |
| 349 | + |
336 | 350 |
|
337 | 351 | def dump_mappings(parts: Iterable[DimDimPadding | Dim]) -> str: |
338 | 352 | """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]: |
505 | 519 | return dims |
506 | 520 |
|
507 | 521 |
|
| 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 | + |
508 | 697 | def _attach_sgrid_metadata(ds, grid: Grid2DMetadata | Grid3DMetadata): |
509 | 698 | """Copies the dataset and attaches the SGRID metadata in 'grid' variable. Modifies 'conventions' attribute.""" |
510 | 699 | ds = ds.copy() |
|
0 commit comments