Skip to content

Commit 6c08bae

Browse files
committed
pygmt.grdclip: Parameters 'between' and 'new' now can accept a 2-D sequence
1 parent a11fc15 commit 6c08bae

File tree

2 files changed

+123
-36
lines changed

2 files changed

+123
-36
lines changed

pygmt/src/grdclip.py

Lines changed: 106 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -2,40 +2,93 @@
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.clib import Session
7-
from pygmt.helpers import build_arg_list, fmt_docstring, kwargs_to_strings, use_alias
9+
from pygmt.exceptions import GMTInvalidInput
10+
from pygmt.helpers import (
11+
build_arg_list,
12+
fmt_docstring,
13+
is_nonstr_iter,
14+
kwargs_to_strings,
15+
use_alias,
16+
)
817

918
__doctest_skip__ = ["grdclip"]
1019

1120

21+
def _parse_sequence(name, value, separator="/", size=2, ndim=1):
22+
"""
23+
Parse a sequence of values from a string or a list.
24+
25+
>>> _parse_sequence("above", [1000, 0], size=2, ndim=1)
26+
'1000/0'
27+
>>> _parse_sequence("below", [1000, 0], size=2, ndim=1)
28+
'1000/0'
29+
>>> _parse_sequence("between", [1000, 1500, 10000], size=3, ndim=2)
30+
'1000/1500/10000'
31+
>>> _parse_sequence("between", [[1000, 1500, 10000]], size=3, ndim=2)
32+
['1000/1500/10000']
33+
>>> _parse_sequence(
34+
... "between", [[1000, 1500, 10000], [1500, 2000, 20000]], size=3, ndim=2
35+
... )
36+
['1000/1500/10000', '1500/2000/20000']
37+
>>> _parse_sequence("replace", [1000, 0], size=2, ndim=1)
38+
'1000/0'
39+
>>> _parse_sequence("replace", [[1000, 0], [1500, 10000]], size=2, ndim=2)
40+
['1000/0', '1500/10000']
41+
"""
42+
if not is_nonstr_iter(value): # Not a sequence. Likely str or None.
43+
return value
44+
45+
# A sequence of sequences.
46+
if len(value) == 0:
47+
return None
48+
if is_nonstr_iter(value[0]): # 2-D sequence
49+
if ndim == 1:
50+
msg = f"Parameter '{name}' must be a 1-D sequence, not a 2-D sequence."
51+
raise GMTInvalidInput(msg)
52+
53+
actual_sizes = {len(i) for i in value}
54+
if len(actual_sizes) != 1 or actual_sizes != {size}:
55+
msg = f"Parameter '{name}' must be a 1-D or 2D sequence with {size} values."
56+
raise GMTInvalidInput(msg)
57+
return [separator.join(str(j) for j in value[i]) for i in range(len(value))]
58+
59+
# A sequence.
60+
if len(value) != size:
61+
msg = f"Parameter '{name}' must be a 1-D sequence of {size} values, but got {len(value)}."
62+
raise GMTInvalidInput(msg)
63+
return separator.join(str(i) for i in value)
64+
65+
1266
@fmt_docstring
13-
@use_alias(
14-
R="region",
15-
Sa="above",
16-
Sb="below",
17-
Si="between",
18-
Sr="new",
19-
V="verbose",
20-
)
21-
@kwargs_to_strings(
22-
R="sequence",
23-
Sa="sequence",
24-
Sb="sequence",
25-
Si="sequence",
26-
Sr="sequence",
27-
)
28-
def grdclip(grid, outgrid: str | None = None, **kwargs) -> xr.DataArray | None:
67+
@use_alias(R="region", V="verbose")
68+
@kwargs_to_strings(R="sequence")
69+
def grdclip(
70+
grid,
71+
outgrid: str | None = None,
72+
above: Sequence[float] | None = None,
73+
below: Sequence[float] | None = None,
74+
between: Sequence[float] | Sequence[Sequence[float]] | None = None,
75+
new: Sequence[float] | Sequence[Sequence[float]] | None = None,
76+
**kwargs,
77+
) -> xr.DataArray | None:
2978
r"""
3079
Clip the range of grid values.
3180
32-
Produce a clipped ``outgrid`` or :class:`xarray.DataArray` version of the
33-
input ``grid`` file.
81+
This function operates on the values of a grid. It can:
82+
83+
- Set values smaller than a threshold to a new value
84+
- Set values larger than a threshold to a new value
85+
- Set values within a range to a new value
86+
- Replace individual values with a new value
3487
35-
The parameters ``above`` and ``below`` allow for a given value to be set
36-
for values above or below a set amount, respectively. This allows for
37-
extreme values in a grid, such as points below a certain depth when
38-
plotting Earth relief, to all be set to the same value.
88+
Such operations are useful when you want all of a continent or an ocean to fall into
89+
one color or gray shade in image processing, when clipping of the range of data
90+
values is required, or for reclassification of data values. The values can be any
91+
number or even NaN (Not a Number).
3992
4093
Full option list at :gmt-docs:`grdclip.html`
4194
@@ -46,19 +99,23 @@ def grdclip(grid, outgrid: str | None = None, **kwargs) -> xr.DataArray | None:
4699
{grid}
47100
{outgrid}
48101
{region}
49-
above : str or list
50-
[*high*, *above*].
51-
Set all data[i] > *high* to *above*.
52-
below : str or list
53-
[*low*, *below*].
54-
Set all data[i] < *low* to *below*.
55-
between : str or list
56-
[*low*, *high*, *between*].
57-
Set all data[i] >= *low* and <= *high* to *between*.
58-
new : str or list
59-
[*old*, *new*].
60-
Set all data[i] == *old* to *new*. This is mostly useful when
61-
your data are known to be integer values.
102+
above
103+
Pass a sequence of two values in the form of (*high*, *above*), to set all node
104+
values greater than *high* to *above*.
105+
below
106+
Pass a sequence of two values in the form of (*low*, *below*) to set all node
107+
values less than *low* to *below*.
108+
between
109+
Pass a sequence of three values in the form of (*low*, *high*, *between*) to set
110+
all node values between *low* and *high* to *between*. It can also accept a
111+
sequence of sequences (e.g., list of lists or 2-D numpy array) to set different
112+
values for different ranges.
113+
new
114+
Pass a sequence of two values in the form of (*old*, *new*) to replace all node
115+
values equal to *old* with *new*. It can also accept a sequence of sequences
116+
(e.g., list of lists or 2-D numpy array) to replace different old values with
117+
different new values. This is mostly useful when your data are known to be
118+
integer values.
62119
{verbose}
63120
64121
Returns
@@ -88,6 +145,19 @@ def grdclip(grid, outgrid: str | None = None, **kwargs) -> xr.DataArray | None:
88145
>>> [new_grid.data.min(), new_grid.data.max()]
89146
[0.0, 10000.0]
90147
"""
148+
if all(v is None for v in (above, below, between, new)):
149+
msg = (
150+
"Must specify at least one of the following parameters: ",
151+
"'above', 'below', 'between', or 'new'.",
152+
)
153+
raise GMTInvalidInput(msg)
154+
155+
# Parse the -S option.
156+
kwargs["Sa"] = _parse_sequence("above", above, size=2)
157+
kwargs["Sb"] = _parse_sequence("below", below, size=2)
158+
kwargs["Si"] = _parse_sequence("between", between, size=3, ndim=2)
159+
kwargs["Sr"] = _parse_sequence("new", new, size=2, ndim=2)
160+
91161
with Session() as lib:
92162
with (
93163
lib.virtualfile_in(check_kind="raster", data=grid) as vingrd,

pygmt/tests/test_grdclip.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
from pathlib import Path
66

7+
import numpy as np
8+
import numpy.testing as npt
79
import pytest
810
import xarray as xr
911
from pygmt import grdclip, load_dataarray
@@ -69,3 +71,18 @@ def test_grdclip_no_outgrid(grid, expected_grid):
6971
assert temp_grid.gmt.gtype == GridType.GEOGRAPHIC
7072
assert temp_grid.gmt.registration == GridRegistration.PIXEL
7173
xr.testing.assert_allclose(a=temp_grid, b=expected_grid)
74+
75+
76+
def test_grdclip_between():
77+
"""
78+
Test passing a 2D sequence to the between parameter for grdclip.
79+
"""
80+
grid = load_static_earth_relief()
81+
# Replace values in the range 0-250 with 0, 250-500 with 1, 500-750 with 2, and
82+
# 750-1000 with 3
83+
result = grdclip(
84+
grid,
85+
between=[[0, 250, 0], [250, 500, 1], [500, 750, 2], [750, 1000, 3]],
86+
)
87+
# result should have 4 unique values.
88+
npt.assert_array_equal(np.unique(result.data), [0, 1, 2, 3])

0 commit comments

Comments
 (0)