Skip to content

Commit 1ac7e7c

Browse files
Chuan1937seisman
andauthored
Wrap grdpaste for joining two grids along their common edge (#4399)
Co-authored-by: Dongdong Tian <seisman.info@gmail.com>
1 parent cb607ed commit 1ac7e7c

5 files changed

Lines changed: 221 additions & 0 deletions

File tree

doc/api/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,7 @@ Operations on raster data
155155
grdhisteq.equalize_grid
156156
grdhisteq.compute_bins
157157
grdlandmask
158+
grdpaste
158159
grdproject
159160
grdsample
160161
grdtrack

pygmt/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
grdhisteq,
4646
grdinfo,
4747
grdlandmask,
48+
grdpaste,
4849
grdproject,
4950
grdsample,
5051
grdtrack,

pygmt/src/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
from pygmt.src.grdimage import grdimage
2626
from pygmt.src.grdinfo import grdinfo
2727
from pygmt.src.grdlandmask import grdlandmask
28+
from pygmt.src.grdpaste import grdpaste
2829
from pygmt.src.grdproject import grdproject
2930
from pygmt.src.grdsample import grdsample
3031
from pygmt.src.grdtrack import grdtrack

pygmt/src/grdpaste.py

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
"""
2+
grdpaste - Join two grids along their common edge.
3+
"""
4+
5+
from typing import Literal
6+
7+
import xarray as xr
8+
from pygmt._typing import PathLike
9+
from pygmt.alias import AliasSystem
10+
from pygmt.clib import Session
11+
from pygmt.exceptions import GMTTypeError
12+
from pygmt.helpers import build_arg_list, data_kind, fmt_docstring, use_alias
13+
14+
15+
@fmt_docstring
16+
@use_alias(f="coltypes")
17+
def grdpaste(
18+
grid1: PathLike | xr.DataArray,
19+
grid2: PathLike | xr.DataArray,
20+
outgrid: PathLike | None = None,
21+
verbose: Literal["quiet", "error", "warning", "timing", "info", "compat", "debug"]
22+
| bool = False,
23+
**kwargs,
24+
) -> xr.DataArray | None:
25+
"""
26+
Join two grids along their common edge.
27+
28+
Combine ``grid1`` and ``grid2`` into a single grid by pasting them together along
29+
their common edge. The two input grids must have the same grid spacings and
30+
registration, and must have one edge in common. If in doubt, check with
31+
:func:`pygmt.grdinfo` and use :func:`pygmt.grdcut` and/or :func:`pygmt.grdsample` if
32+
necessary to prepare the edge joint. Note: For geographical grids, you may have to
33+
use ``coltypes`` to handle periodic longitudes unless the input grids are properly
34+
recognized as such via their meta-data. For stitching multiple grids, see
35+
``grdblend`` (not implemented in PyGMT yet) instead.
36+
37+
Full GMT docs at :gmt-docs:`grdpaste.html`.
38+
39+
$aliases
40+
- G = outgrid
41+
- V = verbose
42+
43+
Parameters
44+
----------
45+
grid1
46+
grid2
47+
The two grids to be pasted. Accepts either file names or
48+
:class:`xarray.DataArray` objects.
49+
50+
**Notes:**
51+
52+
#. Both grids must be of the same type—mixing a file name with an
53+
``xarray.DataArray`` is not allowed.
54+
#. Passing two ``xarray.DataArray`` objects requires GMT>6.6.0 due to
55+
an upstream GMT bug.
56+
$outgrid
57+
$verbose
58+
$coltypes
59+
60+
Returns
61+
-------
62+
ret
63+
Return type depends on whether the ``outgrid`` parameter is set:
64+
65+
- :class:`xarray.DataArray` if ``outgrid`` is not set
66+
- ``None`` if ``outgrid`` is set (grid output will be stored in the file set by
67+
``outgrid``)
68+
"""
69+
# Check if grid1 and grid2 are of the same kind
70+
if data_kind(grid1) != data_kind(grid2):
71+
raise GMTTypeError(
72+
(type(grid1), type(grid2)),
73+
reason="Both input grids must be of the same type (file or xarray.DataArray).",
74+
)
75+
76+
aliasdict = AliasSystem().add_common(V=verbose)
77+
aliasdict.merge(kwargs)
78+
79+
with Session() as lib:
80+
with (
81+
lib.virtualfile_in(check_kind="raster", data=grid1) as vingrd1,
82+
lib.virtualfile_in(check_kind="raster", data=grid2) as vingrd2,
83+
lib.virtualfile_out(kind="grid", fname=outgrid) as voutgrd,
84+
):
85+
aliasdict["G"] = voutgrd
86+
lib.call_module(
87+
module="grdpaste",
88+
args=build_arg_list(aliasdict, infile=[vingrd1, vingrd2]),
89+
)
90+
return lib.virtualfile_to_raster(vfname=voutgrd, outgrid=outgrid)

pygmt/tests/test_grdpaste.py

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
"""
2+
Test pygmt.grdpaste.
3+
"""
4+
5+
import pytest
6+
import xarray as xr
7+
from packaging.version import Version
8+
from pygmt import grdcut, grdpaste
9+
from pygmt.clib import __gmt_version__
10+
from pygmt.exceptions import GMTTypeError
11+
from pygmt.helpers import GMTTempFile
12+
from pygmt.helpers.testing import load_static_earth_relief
13+
14+
15+
@pytest.fixture(scope="module", name="grid")
16+
def fixture_grid():
17+
"""
18+
Load the grid data from the sample earth_relief file.
19+
"""
20+
return load_static_earth_relief()
21+
22+
23+
@pytest.fixture(scope="module", name="grid_top")
24+
def fixture_grid_top(grid):
25+
"""
26+
Load the top part of the grid data from the sample earth_relief file.
27+
"""
28+
return grdcut(grid, region=[-53, -49, -19, -16])
29+
30+
31+
@pytest.fixture(scope="module", name="grid_bottom")
32+
def fixture_grid_bottom(grid):
33+
"""
34+
Load the bottom part of the grid data from the sample earth_relief file.
35+
"""
36+
return grdcut(grid, region=[-53, -49, -22, -19])
37+
38+
39+
def test_grdpaste_file_in_file_out(grid):
40+
"""
41+
Test grdpaste with file input and file output.
42+
"""
43+
with (
44+
GMTTempFile(suffix=".nc") as tmp1,
45+
GMTTempFile(suffix=".nc") as tmp2,
46+
GMTTempFile(suffix=".nc") as tmpout,
47+
):
48+
grdcut(grid, region=[-53, -49, -19, -16], outgrid=tmp1.name)
49+
grdcut(grid, region=[-53, -49, -22, -19], outgrid=tmp2.name)
50+
result = grdpaste(grid1=tmp1.name, grid2=tmp2.name, outgrid=tmpout.name)
51+
assert result is None # grdpaste returns None if output to a file
52+
temp_grid = xr.load_dataarray(tmpout.name, engine="gmt", raster_kind="grid")
53+
assert isinstance(temp_grid, xr.DataArray)
54+
assert temp_grid.shape == (6, 4)
55+
# Check that the result has the expected min and max values
56+
assert temp_grid.min().values == 345.5
57+
assert temp_grid.max().values == 886.0
58+
59+
60+
def test_grdpaste_file_in_xarray_out(grid):
61+
"""
62+
Test grdpaste with file input and xarray output.
63+
"""
64+
with (
65+
GMTTempFile(suffix=".nc") as tmp1,
66+
GMTTempFile(suffix=".nc") as tmp2,
67+
):
68+
grdcut(grid, region=[-53, -49, -19, -16], outgrid=tmp1.name)
69+
grdcut(grid, region=[-53, -49, -22, -19], outgrid=tmp2.name)
70+
result = grdpaste(grid1=tmp1.name, grid2=tmp2.name)
71+
assert isinstance(result, xr.DataArray)
72+
assert result.shape == (6, 4)
73+
# Check that the result has the expected min and max values
74+
assert result.min().values == 345.5
75+
assert result.max().values == 886.0
76+
77+
78+
# TODO(GMT>6.6.0): Remove the xfail marker.
79+
@pytest.mark.xfail(
80+
condition=Version(__gmt_version__) <= Version("6.6.0"),
81+
reason="Upstream bug fixed in https://github.com/GenericMappingTools/gmt/pull/8901",
82+
)
83+
def test_grdpaste(grid_top, grid_bottom):
84+
"""
85+
Test grdpaste by pasting two grids together along their common edge.
86+
"""
87+
# Paste the two grids back together
88+
result = grdpaste(grid1=grid_top, grid2=grid_bottom)
89+
# Check that the result is a DataArray
90+
assert isinstance(result, xr.DataArray)
91+
# Check that the result has the expected shape
92+
# grid_top has 3x4, grid_bottom has 3x4, so result should have 6x4
93+
assert result.shape == (6, 4)
94+
# Check that the result has the expected min and max values
95+
assert result.min().values == 345.5
96+
assert result.max().values == 886.0
97+
98+
99+
# TODO(GMT>6.6.0): Remove the xfail marker.
100+
@pytest.mark.xfail(
101+
condition=Version(__gmt_version__) <= Version("6.6.0"),
102+
reason="Upstream bug fixed in https://github.com/GenericMappingTools/gmt/pull/8901",
103+
)
104+
def test_grdpaste_outgrid(grid_top, grid_bottom):
105+
"""
106+
Test grdpaste with outgrid parameter.
107+
"""
108+
# Paste the two grids back together and save to file
109+
with GMTTempFile(suffix=".nc") as tmpfile:
110+
result = grdpaste(grid1=grid_top, grid2=grid_bottom, outgrid=tmpfile.name)
111+
assert result is None # grdpaste returns None if output to a file
112+
temp_grid = xr.load_dataarray(tmpfile.name, engine="gmt", raster_kind="grid")
113+
assert isinstance(temp_grid, xr.DataArray)
114+
assert temp_grid.shape == (6, 4)
115+
# Check that the result has the expected min and max values
116+
assert temp_grid.min().values == 345.5
117+
assert temp_grid.max().values == 886.0
118+
119+
120+
def test_grdpaste_mixed_inputs_file_xarray(grid, grid_bottom):
121+
"""
122+
Test that mixing file and xarray inputs raises GMTTypeError.
123+
"""
124+
with GMTTempFile(suffix=".nc") as tmp1:
125+
grdcut(grid, region=[-53, -49, -19, -16], outgrid=tmp1.name)
126+
# This should raise GMTTypeError because we're mixing file and xarray inputs
127+
with pytest.raises(GMTTypeError):
128+
grdpaste(grid1=tmp1.name, grid2=grid_bottom)

0 commit comments

Comments
 (0)