Skip to content

Commit 0348fad

Browse files
seismanmichaelgrundyvonnefroehlich
authored
pygmt.grdclip: Parameters 'between' and 'replace' accept a 2-D sequence (#3883)
Co-authored-by: Michael Grund <[email protected]> Co-authored-by: Yvonne Fröhlich <[email protected]>
1 parent ecb933c commit 0348fad

File tree

2 files changed

+161
-35
lines changed

2 files changed

+161
-35
lines changed

pygmt/src/grdclip.py

Lines changed: 137 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -2,53 +2,138 @@
22
grdclip - Clip the range of grid values.
33
"""
44

5+
from collections.abc import Sequence
6+
57
import xarray as xr
68
from pygmt._typing import PathLike
79
from pygmt.clib import Session
10+
from pygmt.exceptions import GMTInvalidInput
811
from pygmt.helpers import (
912
build_arg_list,
1013
deprecate_parameter,
1114
fmt_docstring,
15+
is_nonstr_iter,
1216
kwargs_to_strings,
1317
use_alias,
1418
)
1519

1620
__doctest_skip__ = ["grdclip"]
1721

1822

23+
def _parse_sequence(name, value, separator="/", size=2, ndim=1):
24+
"""
25+
Parse a 1-D or 2-D sequence of values and join them by a separator.
26+
27+
Parameters
28+
----------
29+
name
30+
The parameter name.
31+
value
32+
The 1-D or 2-D sequence of values to parse.
33+
separator
34+
The separator to join the values.
35+
size
36+
The number of values in the sequence.
37+
ndim
38+
The expected maximum number of dimensions of the sequence.
39+
40+
Returns
41+
-------
42+
str
43+
The parsed sequence.
44+
45+
Examples
46+
--------
47+
>>> _parse_sequence("above_or_below", [1000, 0], size=2, ndim=1)
48+
'1000/0'
49+
>>> _parse_sequence("between", [1000, 1500, 10000], size=3, ndim=2)
50+
'1000/1500/10000'
51+
>>> _parse_sequence("between", [[1000, 1500, 10000]], size=3, ndim=2)
52+
['1000/1500/10000']
53+
>>> _parse_sequence(
54+
... "between", [[1000, 1500, 10000], [1500, 2000, 20000]], size=3, ndim=2
55+
... )
56+
['1000/1500/10000', '1500/2000/20000']
57+
>>> _parse_sequence("replace", [1000, 0], size=2, ndim=2)
58+
'1000/0'
59+
>>> _parse_sequence("replace", [[1000, 0]], size=2, ndim=2)
60+
['1000/0']
61+
>>> _parse_sequence("replace", [[1000, 0], [1500, 10000]], size=2, ndim=2)
62+
['1000/0', '1500/10000']
63+
>>> _parse_sequence("any", "1000/100")
64+
'1000/100'
65+
>>> _parse_sequence("any", None)
66+
>>> _parse_sequence("any", [])
67+
[]
68+
>>> _parse_sequence("above_or_below", [[100, 1000], [1500, 2000]], size=2, ndim=1)
69+
Traceback (most recent call last):
70+
...
71+
pygmt.exceptions.GMTInvalidInput: Parameter ... must be a 1-D sequence...
72+
>>> _parse_sequence("above_or_below", [100, 200, 300], size=2, ndim=1)
73+
Traceback (most recent call last):
74+
...
75+
pygmt.exceptions.GMTInvalidInput: Parameter ... must be a 1-D sequence ...
76+
>>> _parse_sequence("between", [[100, 200, 300], [500, 600]], size=3, ndim=2)
77+
Traceback (most recent call last):
78+
...
79+
pygmt.exceptions.GMTInvalidInput: Parameter ... must be a 2-D sequence with ...
80+
"""
81+
# Return the value as is if not a sequence (e.g., str or None) or empty.
82+
if not is_nonstr_iter(value) or len(value) == 0:
83+
return value
84+
85+
# 1-D sequence
86+
if not is_nonstr_iter(value[0]):
87+
if len(value) != size:
88+
msg = (
89+
f"Parameter '{name}' must be a 1-D sequence of {size} values, "
90+
f"but got {len(value)} values."
91+
)
92+
raise GMTInvalidInput(msg)
93+
return separator.join(str(i) for i in value)
94+
95+
# 2-D sequence
96+
if ndim == 1:
97+
msg = f"Parameter '{name}' must be a 1-D sequence, not a 2-D sequence."
98+
raise GMTInvalidInput(msg)
99+
100+
if any(len(i) != size for i in value):
101+
msg = (
102+
f"Parameter '{name}' must be a 2-D sequence with each sub-sequence "
103+
f"having {size} values."
104+
)
105+
raise GMTInvalidInput(msg)
106+
return [separator.join(str(j) for j in value[i]) for i in range(len(value))]
107+
108+
19109
# TODO(PyGMT>=0.19.0): Remove the deprecated "new" parameter.
20110
@fmt_docstring
21111
@deprecate_parameter("new", "replace", "v0.15.0", remove_version="v0.19.0")
22-
@use_alias(
23-
R="region",
24-
Sa="above",
25-
Sb="below",
26-
Si="between",
27-
Sr="replace",
28-
V="verbose",
29-
)
30-
@kwargs_to_strings(
31-
R="sequence",
32-
Sa="sequence",
33-
Sb="sequence",
34-
Si="sequence",
35-
Sr="sequence",
36-
)
112+
@use_alias(R="region", V="verbose")
113+
@kwargs_to_strings(R="sequence")
37114
def grdclip(
38115
grid: PathLike | xr.DataArray,
39116
outgrid: PathLike | None = None,
117+
above: Sequence[float] | None = None,
118+
below: Sequence[float] | None = None,
119+
between: Sequence[float] | Sequence[Sequence[float]] | None = None,
120+
replace: Sequence[float] | Sequence[Sequence[float]] | None = None,
40121
**kwargs,
41122
) -> xr.DataArray | None:
42-
r"""
123+
"""
43124
Clip the range of grid values.
44125
45-
Produce a clipped ``outgrid`` or :class:`xarray.DataArray` version of the
46-
input ``grid`` file.
126+
This function operates on the values of a grid. It can:
127+
128+
- Set values smaller than a threshold to a new value
129+
- Set values larger than a threshold to a new value
130+
- Set values within a range to a new value
131+
- Replace individual values with a new value
47132
48-
The parameters ``above`` and ``below`` allow for a given value to be set
49-
for values above or below a set amount, respectively. This allows for
50-
extreme values in a grid, such as points below a certain depth when
51-
plotting Earth relief, to all be set to the same value.
133+
Such operations are useful when you want all of a continent or an ocean to fall into
134+
one color or gray shade in image processing, when clipping the range of data
135+
values is required, or for reclassification of data values. The values can be any
136+
number or NaN (Not a Number).
52137
53138
Full option list at :gmt-docs:`grdclip.html`
54139
@@ -59,19 +144,23 @@ def grdclip(
59144
{grid}
60145
{outgrid}
61146
{region}
62-
above : str or list
63-
[*high*, *above*].
64-
Set all data[i] > *high* to *above*.
65-
below : str or list
66-
[*low*, *below*].
67-
Set all data[i] < *low* to *below*.
68-
between : str or list
69-
[*low*, *high*, *between*].
70-
Set all data[i] >= *low* and <= *high* to *between*.
71-
replace : str or list
72-
[*old*, *new*].
73-
Set all data[i] == *old* to *new*. This is mostly useful when
74-
your data are known to be integer values.
147+
above
148+
Pass a sequence of two values in the form of (*high*, *above*), to set all node
149+
values greater than *high* to *above*.
150+
below
151+
Pass a sequence of two values in the form of (*low*, *below*) to set all node
152+
values less than *low* to *below*.
153+
between
154+
Pass a sequence of three values in the form of (*low*, *high*, *between*) to set
155+
all node values between *low* and *high* to *between*. It can also accept a
156+
sequence of sequences (e.g., list of lists or 2-D numpy array) to set different
157+
values for different ranges.
158+
replace
159+
Pass a sequence of two values in the form of (*old*, *new*) to replace all node
160+
values equal to *old* with *new*. It can also accept a sequence of sequences
161+
(e.g., list of lists or 2-D numpy array) to replace different old values with
162+
different new values. This is mostly useful when your data are known to be
163+
integer values.
75164
{verbose}
76165
77166
Returns
@@ -101,6 +190,19 @@ def grdclip(
101190
>>> [new_grid.data.min(), new_grid.data.max()]
102191
[0.0, 10000.0]
103192
"""
193+
if all(v is None for v in (above, below, between, replace)):
194+
msg = (
195+
"Must specify at least one of the following parameters: ",
196+
"'above', 'below', 'between', or 'replace'.",
197+
)
198+
raise GMTInvalidInput(msg)
199+
200+
# Parse the -S option.
201+
kwargs["Sa"] = _parse_sequence("above", above, size=2, ndim=1)
202+
kwargs["Sb"] = _parse_sequence("below", below, size=2, ndim=1)
203+
kwargs["Si"] = _parse_sequence("between", between, size=3, ndim=2)
204+
kwargs["Sr"] = _parse_sequence("replace", replace, size=2, ndim=2)
205+
104206
with Session() as lib:
105207
with (
106208
lib.virtualfile_in(check_kind="raster", data=grid) as vingrd,

pygmt/tests/test_grdclip.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from pygmt import grdclip, load_dataarray
1212
from pygmt.datasets import load_earth_mask
1313
from pygmt.enums import GridRegistration, GridType
14+
from pygmt.exceptions import GMTInvalidInput
1415
from pygmt.helpers import GMTTempFile
1516
from pygmt.helpers.testing import load_static_earth_relief
1617

@@ -88,3 +89,26 @@ def test_grdclip_replace():
8889
with pytest.warns(FutureWarning):
8990
grid = grdclip(grid=grid, new=[1, 3]) # Replace 1 with 3
9091
npt.assert_array_equal(np.unique(grid), [2, 3])
92+
93+
94+
def test_grdclip_between_repeated():
95+
"""
96+
Test passing a 2-D sequence to the between parameter for grdclip.
97+
"""
98+
grid = load_static_earth_relief()
99+
# Replace values in the range 0-250 with 0, 250-500 with 1, 500-750 with 2, and
100+
# 750-1000 with 3
101+
result = grdclip(
102+
grid,
103+
between=[[0, 250, 0], [250, 500, 1], [500, 750, 2], [750, 1000, 3]],
104+
)
105+
# Result should have 4 unique values.
106+
npt.assert_array_equal(np.unique(result.data), [0, 1, 2, 3])
107+
108+
109+
def test_grdclip_missing_required_parameter(grid):
110+
"""
111+
Test that grdclip raises a ValueError if the required parameter is missing.
112+
"""
113+
with pytest.raises(GMTInvalidInput):
114+
grdclip(grid=grid)

0 commit comments

Comments
 (0)