Skip to content

Commit c3d9ee3

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

2 files changed

Lines changed: 119 additions & 34 deletions

File tree

pygmt/src/grdgradient.py

Lines changed: 103 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -15,20 +15,74 @@
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+
Helper function to create the alias for the -N option.
27+
28+
Examples
29+
--------
30+
>>> def parse(**kwargs):
31+
... return AliasSystem(N=_alias_option_N(**kwargs)).get("N")
32+
>>> parse(normalize=True)
33+
''
34+
>>> parse(normalize="laplace")
35+
'e'
36+
>>> parse(normalize="cauchy")
37+
't'
38+
>>> parse(
39+
... normalize="laplace",
40+
... norm_amp=2,
41+
... norm_offset=10,
42+
... norm_sigma=0.5,
43+
... norm_ambient=0.1,
44+
... )
45+
'e2+a0.1+s0.5+o10'
46+
>>> # Check for backward compatibility with old syntax
47+
>>> parse(normalize="e2+a0.2+s0.5+o10")
48+
'e2+a0.2+s0.5+o10'
49+
"""
50+
_normalize_mapping = {"laplace": "e", "cauchy": "t"}
51+
# Check for old syntax for normalize
52+
if isinstance(normalize, str) and normalize not in _normalize_mapping:
53+
if any(
54+
v is not None and v is not False
55+
for v in [norm_amp, norm_ambient, norm_sigma, norm_offset]
56+
):
57+
msg = (
58+
"Parameter 'normalize' using old syntax with raw GMT CLI string "
59+
"conflicts with parameters 'norm_amp', 'norm_ambient', 'norm_sigma', "
60+
"or 'norm_offset'."
61+
)
62+
raise GMTInvalidInput(msg)
63+
_normalize_mapping = None
64+
65+
return [
66+
Alias(normalize, name="normalize", mapping=_normalize_mapping),
67+
Alias(norm_amp, name="norm_amp"),
68+
Alias(norm_ambient, name="norm_ambient", prefix="+a"),
69+
Alias(norm_sigma, name="norm_sigma", prefix="+s"),
70+
Alias(norm_offset, name="norm_offset", prefix="+o"),
71+
]
72+
73+
1874
@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(
75+
@use_alias(D="direction", Q="tiles", S="slope_file", f="coltypes", n="interpolation")
76+
def grdgradient( # noqa: PLR0913
2877
grid: PathLike | xr.DataArray,
2978
outgrid: PathLike | None = None,
3079
azimuth: float | Sequence[float] | None = None,
3180
radiance: Sequence[float] | str | None = None,
81+
normalize: Literal["laplace", "cachy"] | bool = False,
82+
norm_amp: float | None = None,
83+
norm_ambient: float | None = None,
84+
norm_sigma: float | None = None,
85+
norm_offset: float | None = None,
3286
region: Sequence[float | str] | str | None = None,
3387
verbose: Literal["quiet", "error", "warning", "timing", "info", "compat", "debug"]
3488
| bool = False,
@@ -44,6 +98,8 @@ def grdgradient(
4498
4599
$aliases
46100
- A = azimuth
101+
- N = normalize, norm_amp, **+a**: norm_ambient, **+s**: norm_sigma,
102+
**+o**: norm_offset
47103
- E = radiance
48104
- R = region
49105
- V = verbose
@@ -100,31 +156,37 @@ def grdgradient(
100156
algorithm; in this case *azim* and *elev* are hardwired to 315
101157
and 45 degrees. This means that even if you provide other values
102158
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.
159+
normalize
160+
Normalize the output gradients. Valid values are:
161+
162+
- ``False``: No normalization is done [Default].
163+
- ``True``: Normalize using max absolute value.
164+
- ``"laplace"``: Normalize using cumulative Laplace distribution.
165+
- ``"cauchy"``: Normalize using cumulative Cauchy distribution.
166+
167+
The normalization process is controlled via the additional parameters
168+
``norm_amp``, ``norm_ambient``, ``norm_sigma``, and ``norm_offset``.
169+
170+
Let :math:`g` denote the actual gradients, :math:`g_n` the normalized gradients,
171+
:math:`a` the maximum output magnitude (``norm_amp``), :math:`o` the offset
172+
value (``norm_offset``), and :math:`\sigma` the sigma value (``norm_sigma``).
173+
The normalization is computed as follows:
174+
175+
- ``True``: :math:`g_n = a (\frac{g - o}{\max(|g - o|)})`
176+
- ``"laplace"``: :math:`g_n = a(1 - \exp(\sqrt{2}\frac{g - o}{\sigma}))`
177+
- ``"cauchy"``: :math:`g_n = \frac{2a}{\pi}\arctan(\frac{g - o}{\sigma})`
178+
norm_amp
179+
Set the maximum output magnitude [Default is 1].
180+
norm_ambient
181+
The ambient value to add to all nodes after gradient calculations are completed
182+
[Default is 0].
183+
norm_offset
184+
The offset value used in the normalization. If not given, it is set to the
185+
average of :math:`g`.
186+
norm_sigma
187+
The *sigma* value used in the Laplace or Cauchy normalization. If not given,
188+
it is estimated from the L1 norm of :math:`g-o` for Laplace or the L2 norm of
189+
:math:`g-o` for Cauchy.
128190
tiles : str
129191
**c**\|\ **r**\|\ **R**.
130192
Control how normalization via ``normalize`` is carried out. When
@@ -181,6 +243,13 @@ def grdgradient(
181243
aliasdict = AliasSystem(
182244
A=Alias(azimuth, name="azimuth", sep="/", size=2),
183245
E=Alias(radiance, name="radiance", sep="/", size=2),
246+
N=_alias_option_N(
247+
normalize,
248+
norm_amp,
249+
norm_ambient,
250+
norm_sigma,
251+
norm_offset,
252+
),
184253
).add_common(
185254
R=region,
186255
V=verbose,

pygmt/tests/test_grdgradient.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,22 @@ def test_grdgradient_no_outgrid(grid, expected_grid):
7373
xr.testing.assert_allclose(a=result, b=expected_grid)
7474

7575

76+
def test_grdgradient_normalize_mixed_syntax(grid):
77+
"""
78+
Test that mixed syntax for normalize in grdgradient raises GMTInvalidInput.
79+
"""
80+
with pytest.raises(GMTInvalidInput):
81+
grdgradient(grid=grid, normalize="t", norm_amp=2)
82+
with pytest.raises(GMTInvalidInput):
83+
grdgradient(grid=grid, normalize="t1", norm_amp=2)
84+
with pytest.raises(GMTInvalidInput):
85+
grdgradient(grid=grid, normalize="t1", norm_sigma=2)
86+
with pytest.raises(GMTInvalidInput):
87+
grdgradient(grid=grid, normalize="e", norm_offset=5)
88+
with pytest.raises(GMTInvalidInput):
89+
grdgradient(grid=grid, normalize="e", norm_ambient=0.1)
90+
91+
7692
def test_grdgradient_fails(grid):
7793
"""
7894
Check that grdgradient fails correctly.

0 commit comments

Comments
 (0)