Skip to content

Commit 538d006

Browse files
Chuan1937seismanCopilotyvonnefroehlich
authored
Wrap grdmask for creating mask grid from polygons or point coverage (#4463)
Co-authored-by: Dongdong Tian <seisman.info@gmail.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Yvonne Fröhlich <94163266+yvonnefroehlich@users.noreply.github.com>
1 parent babdfb5 commit 538d006

File tree

5 files changed

+402
-0
lines changed

5 files changed

+402
-0
lines changed

doc/api/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ Operations on tabular data
126126
blockmedian
127127
blockmode
128128
filter1d
129+
grdmask
129130
nearneighbor
130131
project
131132
select

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+
grdmask,
4849
grdpaste,
4950
grdproject,
5051
grdsample,

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.grdmask import grdmask
2829
from pygmt.src.grdpaste import grdpaste
2930
from pygmt.src.grdproject import grdproject
3031
from pygmt.src.grdsample import grdsample

pygmt/src/grdmask.py

Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
"""
2+
grdmask - Create mask grid from polygons or point coverage.
3+
"""
4+
5+
from collections.abc import Sequence
6+
from typing import Literal
7+
8+
import xarray as xr
9+
from pygmt._typing import PathLike
10+
from pygmt.alias import Alias, AliasSystem
11+
from pygmt.clib import Session
12+
from pygmt.exceptions import GMTParameterError, GMTValueError
13+
from pygmt.helpers import build_arg_list, fmt_docstring
14+
15+
__doctest_skip__ = ["grdmask"]
16+
17+
18+
def _alias_option_N( # noqa: N802
19+
outside: float | None = None,
20+
edge: float | Literal["z", "id"] | None = None,
21+
inside: float | Literal["z", "id"] | None = None,
22+
id_start: float | None = None,
23+
) -> Alias:
24+
"""
25+
Return an Alias object for the -N option.
26+
27+
Builds the -N parameter string for grdmask based on the inside, edge, and outside
28+
values. Handles special modes "z" (use z-value from polygon data) and "id" (use
29+
running polygon ID).
30+
31+
Examples
32+
--------
33+
>>> def parse(**kwargs):
34+
... return AliasSystem(N=_alias_option_N(**kwargs)).get("N")
35+
>>> parse()
36+
>>> parse(outside=1, edge=2, inside=3)
37+
'1/2/3'
38+
>>> parse(outside=3)
39+
'3/0/1'
40+
>>> parse(inside="z")
41+
'z'
42+
>>> parse(outside=1, inside="z")
43+
'z/1'
44+
>>> parse(edge="z", inside="z")
45+
'Z'
46+
>>> parse(inside="id")
47+
'p'
48+
>>> parse(edge="id", inside="id")
49+
'P'
50+
>>> parse(inside="id", id_start=5)
51+
'p5'
52+
>>> parse(edge="id", inside="id", id_start=10)
53+
'P10'
54+
>>> parse(edge="id", inside="id", id_start=5, outside=3)
55+
'P5/3'
56+
>>> parse(edge="id", id_start=5, outside=3)
57+
Traceback (most recent call last):
58+
...
59+
pygmt.exceptions.GMTValueError: ...
60+
>>> parse(edge="z")
61+
Traceback (most recent call last):
62+
...
63+
pygmt.exceptions.GMTValueError: ...
64+
>>> parse(inside="z", edge="id")
65+
Traceback (most recent call last):
66+
...
67+
pygmt.exceptions.GMTValueError: ...
68+
>>> parse(inside="z", id_start=5)
69+
Traceback (most recent call last):
70+
...
71+
pygmt.exceptions.GMTValueError: ...
72+
"""
73+
_inside_modes = {"z": "z", "id": "p"}
74+
75+
if id_start is not None and inside != "id":
76+
raise GMTValueError(
77+
inside,
78+
description="value for parameter 'inside'",
79+
reason="Parameter 'id_start' requires inside='id'.",
80+
)
81+
82+
# In the special modes, 'edge' must be None or the same as 'inside'
83+
if (edge in _inside_modes or inside in _inside_modes) and edge not in {
84+
None,
85+
inside,
86+
}:
87+
raise GMTValueError(
88+
edge,
89+
description="edge",
90+
reason=f"inside={inside!r} and edge={edge!r} must be the same.",
91+
)
92+
93+
# outside/edge/inside are all omitted: keep GMT default 0/0/1
94+
if all(v is None for v in (outside, inside, edge)):
95+
return Alias(None, name="mask_values")
96+
# Build -N argument
97+
if inside in _inside_modes: # Mode: -Nz, -NZ, -Np, or -NP
98+
mode = "z" if inside == "z" else "p"
99+
if edge == inside:
100+
mode = mode.upper()
101+
# Append id_start if specified (only valid for "id" mode)
102+
if id_start:
103+
mode = f"{mode}{id_start}"
104+
mask_values = mode if outside is None else [mode, outside]
105+
else: # Build the full mask with defaults for any missing values.
106+
mask_values = [
107+
0 if outside is None else outside,
108+
0 if edge is None else edge,
109+
1 if inside is None else inside,
110+
]
111+
return Alias(mask_values, name="mask_values", sep="/", size=(2, 3))
112+
113+
114+
@fmt_docstring
115+
def grdmask(
116+
data,
117+
outgrid: PathLike | None = None,
118+
spacing: Sequence[float | str] | None = None,
119+
region: Sequence[float | str] | str | None = None,
120+
outside: float | None = None,
121+
edge: float | Literal["z", "id"] | None = None,
122+
inside: float | Literal["z", "id"] | None = None,
123+
id_start: float | None = None,
124+
verbose: Literal["quiet", "error", "warning", "timing", "info", "compat", "debug"]
125+
| bool = False,
126+
**kwargs,
127+
) -> xr.DataArray | None:
128+
"""
129+
Create mask grid from polygons or point coverage.
130+
131+
Reads one or more files containing polygon or data point coordinates, and creates a
132+
grid where nodes that fall inside, on the edge, or outside the polygons (or within
133+
the search radius from data points) are assigned values based on the ``outside``,
134+
``edge``, and ``inside`` parameters.
135+
136+
The mask grid can be used to mask out specific regions in other grids using
137+
:func:`pygmt.grdmath` or similar tools. For masking based on coastline features,
138+
consider using :func:`pygmt.grdlandmask` instead.
139+
140+
Full GMT docs at :gmt-docs:`grdmask.html`.
141+
142+
**Aliases**
143+
144+
.. hlist::
145+
:columns: 3
146+
147+
- G = outgrid
148+
- I = spacing
149+
- N = outside, edge, inside, id_start
150+
- R = region
151+
- V = verbose
152+
153+
Parameters
154+
----------
155+
data
156+
Pass in either a file name to an ASCII data table, a 2-D $table_classes
157+
containing the polygon(s) or data points. Input can be:
158+
159+
- **Polygon mode**: One or more files containing closed polygon coordinates
160+
- **Point coverage mode**: Data points (used with ``search_radius`` parameter)
161+
$outgrid
162+
$spacing
163+
outside
164+
edge
165+
inside
166+
Set the value assigned to nodes outside, on the edge, or inside the polygons.
167+
Can be any number, or one of ``None``, ``"NaN"``, and ``np.nan`` for NaN.
168+
169+
``inside`` and ``edge`` can also be set to one of the following values:
170+
171+
- ``"z"``: Use the z-value from polygon data (segment header ``-Zzval``,
172+
``-Lheader``, or via ``-aZ=name``).
173+
- ``"id"``: Use a running polygon ID number.
174+
175+
To treat edges as inside, use the same value as ``inside``.
176+
id_start
177+
The starting number for polygon IDs when ``inside="id"`` [Default is ``0``].
178+
Only valid when ``inside="id"``.
179+
$region
180+
$verbose
181+
182+
Returns
183+
-------
184+
ret
185+
Return type depends on whether the ``outgrid`` parameter is set:
186+
187+
- :class:`xarray.DataArray` if ``outgrid`` is not set
188+
- ``None`` if ``outgrid`` is set (grid output will be stored in the file set by
189+
``outgrid``)
190+
191+
Example
192+
-------
193+
>>> import pygmt
194+
>>> import numpy as np
195+
>>> # Create a simple polygon as a triangle
196+
>>> polygon = np.array([[125, 30], [130, 30], [130, 35], [125, 30]])
197+
>>> # Create a mask grid with 1 arc-degree spacing
198+
>>> mask = pygmt.grdmask(data=polygon, spacing=1, region=[125, 130, 30, 35])
199+
>>> mask.values
200+
array([[0., 0., 0., 0., 0., 0.],
201+
[0., 0., 1., 1., 1., 0.],
202+
[0., 0., 0., 1., 1., 0.],
203+
[0., 0., 0., 0., 1., 0.],
204+
[0., 0., 0., 0., 0., 0.],
205+
[0., 0., 0., 0., 0., 0.]], dtype=float32)
206+
"""
207+
if kwargs.get("I", spacing) is None or kwargs.get("R", region) is None:
208+
raise GMTParameterError(required=["region", "spacing"])
209+
210+
aliasdict = AliasSystem(
211+
I=Alias(spacing, name="spacing", sep="/", size=2),
212+
N=_alias_option_N(outside=outside, edge=edge, inside=inside, id_start=id_start),
213+
).add_common(
214+
R=region,
215+
V=verbose,
216+
)
217+
aliasdict.merge(kwargs)
218+
219+
with Session() as lib:
220+
with (
221+
lib.virtualfile_in(check_kind="vector", data=data) as vintbl,
222+
lib.virtualfile_out(kind="grid", fname=outgrid) as voutgrd,
223+
):
224+
aliasdict["G"] = voutgrd
225+
lib.call_module(
226+
module="grdmask",
227+
args=build_arg_list(aliasdict, infile=vintbl),
228+
)
229+
return lib.virtualfile_to_raster(vfname=voutgrd, outgrid=outgrid)

0 commit comments

Comments
 (0)