Skip to content

Commit 38c29a8

Browse files
authored
pygmt.grdfilter: Add parameters filter/width/highpass to set the filter [Part 1] (#4401)
1 parent 867b4bb commit 38c29a8

2 files changed

Lines changed: 128 additions & 21 deletions

File tree

pygmt/src/grdfilter.py

Lines changed: 101 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,68 @@
1515
__doctest_skip__ = ["grdfilter"]
1616

1717

18+
def _alias_option_F( # noqa: N802
19+
filter=None, # noqa: A002
20+
width=None,
21+
highpass=False,
22+
):
23+
"""
24+
Helper function to create the alias list for the -F option.
25+
26+
Examples
27+
--------
28+
>>> def parse(**kwargs):
29+
... return AliasSystem(F=_alias_option_F(**kwargs)).get("F")
30+
>>> parse(filter="boxcar", width=2.0)
31+
'b2.0'
32+
>>> parse(filter="cosarch", width=(5, 10))
33+
'c5/10'
34+
>>> parse(filter="gaussian", width=100, highpass=True)
35+
'g100+h'
36+
"""
37+
_filter_mapping = {
38+
"boxcar": "b",
39+
"cosarch": "c",
40+
"gaussian": "g",
41+
"minall": "l",
42+
"minpos": "L",
43+
"maxall": "u",
44+
"maxneg": "U",
45+
}
46+
# Check if the 'filter' parameter is using the old GMT command string syntax.
47+
_old_filter_syntax = isinstance(filter, str) and filter not in _filter_mapping
48+
49+
if _old_filter_syntax:
50+
kwdict = {"width": width, "highpass": highpass}
51+
if any(v is not None and v is not False for v in kwdict.values()):
52+
raise GMTParameterError(
53+
conflicts_with=("filter", kwdict.keys()),
54+
reason="'filter' is specified using the unrecommended GMT command string syntax.",
55+
)
56+
return Alias(filter, name="filter") # Deprecated raw GMT command string.
57+
58+
if filter is None or width is None:
59+
raise GMTParameterError(required=["filter", "width"])
60+
61+
return [
62+
Alias(filter, name="filter", mapping=_filter_mapping),
63+
Alias(width, name="width", sep="/", size=2),
64+
Alias(highpass, name="highpass", prefix="+h"),
65+
]
66+
67+
1868
@fmt_docstring
19-
@use_alias(F="filter", f="coltypes")
20-
def grdfilter(
69+
@use_alias(f="coltypes")
70+
def grdfilter( # noqa: PLR0913
2171
grid: PathLike | xr.DataArray,
2272
outgrid: PathLike | None = None,
73+
filter: Literal[ # noqa: A002
74+
"boxcar", "cosarch", "gaussian", "minall", "minpos", "maxall", "maxneg"
75+
]
76+
| str
77+
| None = None,
78+
width: float | Sequence[float] | None = None,
79+
highpass: bool = False,
2380
distance: Literal[
2481
"pixel",
2582
"cartesian",
@@ -40,7 +97,7 @@ def grdfilter(
4097
cores: int | bool = False,
4198
**kwargs,
4299
) -> xr.DataArray | None:
43-
r"""
100+
"""
44101
Filter a grid in the space (or time) domain.
45102
46103
Filter a grid file in the space (or time) domain using one of the selected
@@ -57,6 +114,7 @@ def grdfilter(
57114
58115
$aliases
59116
- D = distance
117+
- F = filter, width, **+h**: highpass
60118
- G = outgrid
61119
- I = spacing
62120
- N = nans
@@ -70,19 +128,35 @@ def grdfilter(
70128
----------
71129
$grid
72130
$outgrid
73-
filter : str
74-
**b**\|\ **c**\|\ **g**\|\ **o**\|\ **m**\|\ **p**\|\ **h**\ *width*\
75-
[/*width2*\][*modifiers*].
76-
Name of the filter type you wish to apply, followed by the *width*:
77-
78-
- **b**: Box Car
79-
- **c**: Cosine Arch
80-
- **g**: Gaussian
81-
- **o**: Operator
82-
- **m**: Median
83-
- **p**: Maximum Likelihood probability
84-
- **h**: Histogram
131+
filter
132+
The filter type. Choose among convolution and non-convolution filters.
133+
134+
Convolution filters include:
85135
136+
- ``"boxcar"``: All weights are equal.
137+
- ``"cosarch"``: Weights follow a cosine arch curve.
138+
- ``"gaussian"``: Weights are given by the Gaussian function, where filter width
139+
is 6 times the conventional Gaussian sigma.
140+
141+
Non-convolution filters include:
142+
143+
- ``"minall"``: Return minimum of all values.
144+
- ``"minpos"``: Return minimum of all positive values only.
145+
- ``"maxall"``: Return maximum of all values.
146+
- ``"maxneg"``: Return maximum of all negative values only.
147+
148+
**Note**: There are still a few other filter types available in GMT (e.g.,
149+
histogram and mode filters), but they are not implemented in PyGMT yet. As a
150+
workaround, pass the raw GMT command string to this parameter to use these other
151+
filter types. Refer to :gmt-docs:`grdfilter.html#f` for the full syntax of this
152+
parameter.
153+
width
154+
The full diameter width of the filter. It can be a single value for an isotropic
155+
filter, or a pair of values for a rectangular filter (width in x- and
156+
y-directions, requiring ``distance`` be either ``"pixel"`` or ``"cartesian"``).
157+
highpass
158+
By default, the filter is a low-pass filter. If True, then the filter is a
159+
high-pass filter. [Default is ``False``].
86160
distance
87161
Determine how grid (*x, y*) relates to filter *width* and how distances are
88162
calculated. Valid values are list below. The first four options are fastest
@@ -159,11 +233,12 @@ def grdfilter(
159233
--------
160234
>>> from pathlib import Path
161235
>>> import pygmt
162-
>>> # Apply a median filter of 600 km (full width) to the @earth_relief_30m_g grid
236+
>>> # Apply a gaussian filter of 600 km (full width) to the @earth_relief_30m_g grid
163237
>>> # and return a filtered grid (saved as netCDF file).
164238
>>> pygmt.grdfilter(
165239
... grid="@earth_relief_30m_g",
166-
... filter="m600",
240+
... filter="gaussian",
241+
... width=600,
167242
... distance="geo_spherical",
168243
... region=[150, 250, 10, 40],
169244
... spacing=0.5,
@@ -173,10 +248,13 @@ def grdfilter(
173248
>>> # Apply a Gaussian smoothing filter of 600 km to the input DataArray and return
174249
>>> # a filtered DataArray with the smoothed grid.
175250
>>> grid = pygmt.datasets.load_earth_relief()
176-
>>> smoothed = pygmt.grdfilter(grid=grid, filter="g600", distance="geo_spherical")
251+
>>> smoothed = pygmt.grdfilter(
252+
... grid=grid, filter="gaussian", width=600, distance="geo_spherical"
253+
... )
177254
"""
178255
if kwargs.get("D", distance) is None:
179256
raise GMTParameterError(required="distance")
257+
# filter is also required but will be checked in _alias_option_F.
180258

181259
aliasdict = AliasSystem(
182260
D=Alias(
@@ -192,6 +270,11 @@ def grdfilter(
192270
"geo_mercator": 5,
193271
},
194272
),
273+
F=_alias_option_F(
274+
filter=filter,
275+
width=width,
276+
highpass=highpass,
277+
),
195278
I=Alias(spacing, name="spacing", sep="/", size=2),
196279
N=Alias(
197280
nans, name="nans", mapping={"ignore": "i", "replace": "r", "preserve": "p"}

pygmt/tests/test_grdfilter.py

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,8 @@ def test_grdfilter_dataarray_in_dataarray_out(grid, expected_grid):
4848
"""
4949
result = grdfilter(
5050
grid=grid,
51-
filter="g600",
51+
filter="gaussian",
52+
width=600,
5253
distance="geo_spherical",
5354
region=[-53, -49, -20, -17],
5455
cores=2,
@@ -69,7 +70,8 @@ def test_grdfilter_dataarray_in_file_out(grid, expected_grid):
6970
result = grdfilter(
7071
grid,
7172
outgrid=tmpfile.name,
72-
filter="g600",
73+
filter="gaussian",
74+
width=600,
7375
distance="geo_spherical",
7476
region=[-53, -49, -20, -17],
7577
)
@@ -85,13 +87,35 @@ def test_grdfilter_fails():
8587
"""
8688
with pytest.raises(GMTTypeError):
8789
grdfilter(
88-
np.arange(10).reshape((5, 2)), filter="g600", distance="geo_spherical"
90+
np.arange(10).reshape((5, 2)),
91+
filter="gaussian",
92+
width=600,
93+
distance="geo_spherical",
8994
)
9095

9196

9297
def test_grdfilter_required(grid):
9398
"""
9499
Test that grdfilter raises an exception when required parameters are missing.
95100
"""
101+
with pytest.raises(GMTParameterError, match="distance"):
102+
grdfilter(grid=grid)
103+
with pytest.raises(GMTParameterError, match="distance"):
104+
grdfilter(grid=grid, filter="gaussian")
105+
with pytest.raises(GMTParameterError, match="width"):
106+
grdfilter(grid=grid, filter="gaussian", distance="geo_spherical")
107+
with pytest.raises(GMTParameterError, match="filter"):
108+
grdfilter(grid=grid, width=600, distance="geo_spherical")
96109
with pytest.raises(GMTParameterError, match="distance"):
97110
grdfilter(grid=grid, filter="g600")
111+
112+
113+
def test_grdfilter_mixed_syntax(grid):
114+
"""
115+
Test grdfilter's filter parameter with mixed syntax.
116+
"""
117+
kwargs = {"grid": grid, "filter": "g600", "distance": 4}
118+
with pytest.raises(GMTParameterError):
119+
grdfilter(width=600, **kwargs)
120+
with pytest.raises(GMTParameterError):
121+
grdfilter(highpass=True, **kwargs)

0 commit comments

Comments
 (0)