Skip to content

Commit 80afd26

Browse files
committed
pygmt.grdgradient: Add parameters 'normalize'/'norm_ambient'/'norm_amp'/'norm_sigma'/'norm_offset' for normalization
1 parent c245644 commit 80afd26

2 files changed

Lines changed: 124 additions & 34 deletions

File tree

pygmt/src/grdgradient.py

Lines changed: 81 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -15,20 +15,52 @@
1515
__doctest_skip__ = ["grdgradient"]
1616

1717

18+
def _alias_option_N( # noqa: N802
19+
normalize=False,
20+
norm_amp=None,
21+
norm_ambient=None,
22+
norm_sigma=None,
23+
norm_offset=None,
24+
):
25+
"""
26+
Create the alias for the N option in grdgradient.
27+
"""
28+
_normalize_mapping = {"laplace": "e", "cauchy": "t"}
29+
# Check for old syntax for normalize
30+
if isinstance(normalize, str) and normalize not in _normalize_mapping:
31+
if any(
32+
v is not None and v is not False
33+
for v in [norm_amp, norm_ambient, norm_sigma, norm_offset]
34+
):
35+
msg = (
36+
"Parameter 'normalize' using old syntax with raw GMT CLI string "
37+
"conflicts with parameters 'norm_amp', 'norm_ambient', 'norm_sigma', "
38+
"or 'norm_offset'."
39+
)
40+
raise GMTInvalidInput(msg)
41+
_normalize_mapping = None
42+
43+
return [
44+
Alias(normalize, name="normalize", mapping=_normalize_mapping),
45+
Alias(norm_amp, name="norm_amp"),
46+
Alias(norm_ambient, name="norm_ambient", prefix="+a"),
47+
Alias(norm_sigma, name="norm_sigma", prefix="+s"),
48+
Alias(norm_offset, name="norm_offset", prefix="+o"),
49+
]
50+
51+
1852
@fmt_docstring
19-
@use_alias(
20-
D="direction",
21-
N="normalize",
22-
Q="tiles",
23-
S="slope_file",
24-
f="coltypes",
25-
n="interpolation",
26-
)
27-
def grdgradient(
53+
@use_alias(D="direction", Q="tiles", S="slope_file", f="coltypes", n="interpolation")
54+
def grdgradient( # noqa: PLR0913
2855
grid: PathLike | xr.DataArray,
2956
outgrid: PathLike | None = None,
3057
azimuth: float | Sequence[float] | None = None,
3158
radiance: Sequence[float] | str | None = None,
59+
normalize: Literal["laplace", "cachy"] | bool = False,
60+
norm_amp: float | None = None,
61+
norm_ambient: float | None = None,
62+
norm_sigma: float | None = None,
63+
norm_offset: float | None = None,
3264
region: Sequence[float | str] | str | None = None,
3365
verbose: Literal["quiet", "error", "warning", "timing", "info", "compat", "debug"]
3466
| bool = False,
@@ -44,6 +76,8 @@ def grdgradient(
4476
4577
$aliases
4678
- A = azimuth
79+
- N = normalize, norm_amp, **+a**: norm_ambient, **+s**: norm_sigma,
80+
**+o**: norm_offset
4781
- E = radiance
4882
- R = region
4983
- V = verbose
@@ -100,31 +134,37 @@ def grdgradient(
100134
algorithm; in this case *azim* and *elev* are hardwired to 315
101135
and 45 degrees. This means that even if you provide other values
102136
they will be ignored.).
103-
normalize : str or bool
104-
[**e**\|\ **t**][*amp*][**+a**\ *ambient*][**+s**\ *sigma*]\
105-
[**+o**\ *offset*].
106-
The actual gradients :math:`g` are offset and scaled to produce
107-
normalized gradients :math:`g_n` with a maximum output magnitude of
108-
*amp*. If *amp* is not given, default *amp* = 1. If *offset* is not
109-
given, it is set to the average of :math:`g`. The following forms are
110-
supported:
111-
112-
- **True**: Normalize using math:`g_n = \mbox{amp}\
113-
(\frac{g - \mbox{offset}}{max(|g - \mbox{offset}|)})`
114-
- **e**: Normalize using a cumulative Laplace distribution yielding:
115-
:math:`g_n = \mbox{amp}(1 - \
116-
\exp{(\sqrt{2}\frac{g - \mbox{offset}}{\sigma}))}`, where
117-
:math:`\sigma` is estimated using the L1 norm of
118-
:math:`(g - \mbox{offset})` if it is not given.
119-
- **t**: Normalize using a cumulative Cauchy distribution yielding:
120-
:math:`g_n = \
121-
\frac{2(\mbox{amp})}{\pi}(\tan^{-1}(\frac{g - \
122-
\mbox{offset}}{\sigma}))` where :math:`\sigma` is estimated
123-
using the L2 norm of :math:`(g - \mbox{offset})` if it is not
124-
given.
125-
126-
As a final option, you may add **+a**\ *ambient* to add *ambient* to
127-
all nodes after gradient calculations are completed.
137+
normalize
138+
Normalize the output gradients. Valid values are:
139+
140+
- ``False``: No normalization is done [Default].
141+
- ``True``: Normalize using max absolute value.
142+
- ``"laplace"``: Normalize using cumulative Laplace distribution.
143+
- ``"cauchy"``: Normalize using cumulative Cauchy distribution.
144+
145+
The normalization process is controlled via the additional parameters
146+
``norm_amp``, ``norm_ambient``, ``norm_sigma``, and ``norm_offset``.
147+
148+
Let :math:`g` denote the actual gradients, :math:`g_n` the normalized gradients,
149+
:math:`a` the maximum output magnitude (``norm_amp``), :math:`o` the offset
150+
value (``norm_offset``), and :math:`\sigma` the sigma value (``norm_sigma``).
151+
The normalization is computed as follows:
152+
153+
- ``True``: :math:`g_n = a (\frac{g - o}{\max(|g - o|)})`
154+
- ``"laplace"``: :math:`g_n = a(1 - \exp(\sqrt{2}\frac{g - o}{\sigma}))`
155+
- ``"cauchy"``: :math:`g_n = \frac{2a}{\pi}\arctan(\frac{g - o}{\sigma})`
156+
norm_amp
157+
Set the maximum output magnitude [Default is 1].
158+
norm_ambient
159+
The ambient value to add to all nodes after gradient calculations are completed
160+
[Default is 0].
161+
norm_offset
162+
The offset value used in the normalization. If not given, it is set to the
163+
average of :math:`g`.
164+
norm_sigma
165+
The *sigma* value used in the Laplace or Cauchy normalization. If not given,
166+
it is estimated from the L1 norm of :math:`g-o` for Laplace or the L2 norm of
167+
:math:`g-o` for Cauchy.
128168
tiles : str
129169
**c**\|\ **r**\|\ **R**.
130170
Control how normalization via ``normalize`` is carried out. When
@@ -181,6 +221,13 @@ def grdgradient(
181221
aliasdict = AliasSystem(
182222
A=Alias(azimuth, name="azimuth", sep="/", size=2),
183223
E=Alias(radiance, name="radiance", sep="/", size=2),
224+
N=_alias_option_N(
225+
normalize,
226+
norm_amp,
227+
norm_ambient,
228+
norm_sigma,
229+
norm_offset,
230+
),
184231
).add_common(
185232
R=region,
186233
V=verbose,

pygmt/tests/test_grdgradient.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,12 @@
77
import pytest
88
import xarray as xr
99
from pygmt import grdgradient
10+
from pygmt.alias import AliasSystem
1011
from pygmt.enums import GridRegistration, GridType
1112
from pygmt.exceptions import GMTInvalidInput
1213
from pygmt.helpers import GMTTempFile
1314
from pygmt.helpers.testing import load_static_earth_relief
15+
from pygmt.src.grdgradient import _alias_option_N
1416

1517

1618
@pytest.fixture(scope="module", name="grid")
@@ -73,6 +75,47 @@ def test_grdgradient_no_outgrid(grid, expected_grid):
7375
xr.testing.assert_allclose(a=result, b=expected_grid)
7476

7577

78+
def test_grdgradient_normalize():
79+
"""
80+
Test parameters related to normalization in grdgradient.
81+
"""
82+
83+
def alias_wrapper(**kwargs):
84+
return AliasSystem(N=_alias_option_N(**kwargs)).get("N")
85+
86+
assert alias_wrapper(normalize=True) == ""
87+
assert alias_wrapper(normalize="laplace") == "e"
88+
assert alias_wrapper(normalize="cauchy") == "t"
89+
90+
argstr = alias_wrapper(
91+
normalize="laplace",
92+
norm_amp=2,
93+
norm_offset=10,
94+
norm_sigma=0.5,
95+
norm_ambient=0.1,
96+
)
97+
assert argstr == "e2+a0.1+s0.5+o10"
98+
99+
# Check for old syntax for normalize
100+
assert alias_wrapper(normalize="e2+a0.2+s0.5+o10") == "e2+a0.2+s0.5+o10"
101+
102+
103+
def test_grdgradient_normalize_mixed_syntax(grid):
104+
"""
105+
Test that mixed syntax for normalize in grdgradient raises GMTInvalidInput.
106+
"""
107+
with pytest.raises(GMTInvalidInput):
108+
grdgradient(grid=grid, normalize="t", norm_amp=2)
109+
with pytest.raises(GMTInvalidInput):
110+
grdgradient(grid=grid, normalize="t1", norm_amp=2)
111+
with pytest.raises(GMTInvalidInput):
112+
grdgradient(grid=grid, normalize="t1", norm_sigma=2)
113+
with pytest.raises(GMTInvalidInput):
114+
grdgradient(grid=grid, normalize="e", norm_offset=5)
115+
with pytest.raises(GMTInvalidInput):
116+
grdgradient(grid=grid, normalize="e", norm_ambient=0.1)
117+
118+
76119
def test_grdgradient_fails(grid):
77120
"""
78121
Check that grdgradient fails correctly.

0 commit comments

Comments
 (0)