Skip to content

Commit 57f9c6d

Browse files
authored
pygmt.grdgradient: Add parameters 'normalize'/'norm_ambient'/'norm_amp'/'norm_sigma'/'norm_offset' for normalization (#4365)
1 parent 9380f5a commit 57f9c6d

File tree

3 files changed

+165
-70
lines changed

3 files changed

+165
-70
lines changed

examples/gallery/images/grdgradient_shading.py

Lines changed: 39 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,23 @@
11
"""
2-
Calculating grid gradient with custom ``azimuth`` and ``normalize`` parameters
3-
==============================================================================
2+
Calculating grid gradient with custom azimuth and normalization parameters
3+
==========================================================================
44
5-
The :func:`pygmt.grdgradient` function calculates the gradient of a grid file.
6-
As input, :func:`pygmt.grdgradient` gets an :class:`xarray.DataArray` object
7-
or a path string to a grid file. It then calculates the respective gradient
8-
and returns an :class:`xarray.DataArray` object. The example below sets two
9-
main parameters:
5+
The :func:`pygmt.grdgradient` function calculates the gradient of a grid file. As input,
6+
:func:`pygmt.grdgradient` gets an :class:`xarray.DataArray` object or a path string to a
7+
grid file. It then calculates the respective gradient and returns an
8+
:class:`xarray.DataArray` object.
109
11-
- ``azimuth`` to set the illumination light source direction (0° is North,
12-
90° is East, 180° is South, 270° is West).
13-
- ``normalize`` to enhance the three-dimensional sense of the topography.
10+
The ``normalize`` parameter calculates the azimuthal gradient of each point along a
11+
certain azimuth angle, then adjusts the brightness value of the color according to the
12+
positive/negative of the azimuthal gradient and the amplitude of each point. The example
13+
below shows how to customize the gradient by setting azimuth and normalization
14+
parameters.
1415
15-
The ``normalize`` parameter calculates the azimuthal gradient of each point
16-
along a certain azimuth angle, then adjusts the brightness value of the color
17-
according to the positive/negative of the azimuthal gradient and the amplitude
18-
of each point.
16+
- ``azimuth`` sets the illumination light source direction (0° is North, 90° is East,
17+
180° is South, 270° is West).
18+
- ``normalize`` sets the normalization method (e.g., Cauchy or Laplace distribution)
19+
- ``norm_amp`` sets the amplitude of the normalization
20+
- more parameters are available to further enhance the 3-D sense of the topography
1921
"""
2022

2123
# %%
@@ -28,7 +30,7 @@
2830
fig = pygmt.Figure()
2931

3032
# Define a colormap to be used for topography
31-
pygmt.makecpt(cmap="gmt/terra", series=[-7000, 7000])
33+
pygmt.makecpt(cmap="gmt/terra", series=(-7000, 7000))
3234

3335
# Define figure configuration
3436
pygmt.config(FONT_TITLE="10p,5", MAP_TITLE_OFFSET="1p", MAP_FRAME_TYPE="plain")
@@ -41,24 +43,28 @@
4143
sharex="b",
4244
sharey="l",
4345
):
44-
# E.g. "0/90" illuminates light source from the North (top) and East
45-
# (right), and so on
46-
for azi in ["0/90", "0/300", "180/225"]:
47-
# "e" and "t" are cumulative Laplace distribution and cumulative
48-
# Cauchy distribution, respectively
49-
# "amp" (e.g. 1 or 10) controls the brightness value of the color
50-
for nor in ["t1", "e1", "t10", "e10"]:
51-
# Making an intensity DataArray using azimuth and normalize
52-
# parameters
53-
shade = pygmt.grdgradient(grid=grid, azimuth=azi, normalize=nor)
54-
fig.grdimage(
55-
grid=grid,
56-
shading=shade,
57-
projection="M?",
58-
frame=["a4f2", f"+tazimuth={azi}, normalize={nor}"],
59-
cmap=True,
60-
panel=True,
61-
)
46+
# Setting azimuth angles, e.g. (0, 90) illuminates light source from the North (top)
47+
# and East (right).
48+
for azi in [(0, 90), (0, 300), (180, 225)]:
49+
# "cauchy"/"laplace" sets cumulative Cauchy/Laplace distribution, respectively.
50+
for normalize in ("cauchy", "laplace"):
51+
# amp (e.g., 1 or 10) controls the brightness value of the color.
52+
for amp in (1, 10):
53+
# Making an intensity DataArray using azimuth and normalize parameters
54+
shade = pygmt.grdgradient(
55+
grid=grid, azimuth=azi, normalize=normalize, norm_amp=amp
56+
)
57+
fig.grdimage(
58+
grid=grid,
59+
shading=shade,
60+
projection="M?",
61+
frame=[
62+
"a4f2",
63+
f"+tazimuth={azi}, normalize={normalize}, amp={amp}",
64+
],
65+
cmap=True,
66+
panel=True,
67+
)
6268

6369
fig.colorbar(
6470
position=Position("BC", cstype="outside"),

pygmt/src/grdgradient.py

Lines changed: 109 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -9,26 +9,80 @@
99
from pygmt._typing import PathLike
1010
from pygmt.alias import Alias, AliasSystem
1111
from pygmt.clib import Session
12-
from pygmt.exceptions import GMTParameterError
12+
from pygmt.exceptions import GMTInvalidInput, GMTParameterError
1313
from pygmt.helpers import build_arg_list, fmt_docstring, use_alias
1414

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 list 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", "cauchy"] | 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,
@@ -49,6 +103,12 @@ def grdgradient(
49103
- R = region
50104
- V = verbose
51105
106+
.. hlist::
107+
:columns: 1
108+
109+
- N = normalize, norm_amp, **+a**: norm_ambient, **+s**: norm_sigma,
110+
**+o**: norm_offset
111+
52112
Parameters
53113
----------
54114
$grid
@@ -101,31 +161,37 @@ def grdgradient(
101161
algorithm; in this case *azim* and *elev* are hardwired to 315
102162
and 45 degrees. This means that even if you provide other values
103163
they will be ignored.).
104-
normalize : str or bool
105-
[**e**\|\ **t**][*amp*][**+a**\ *ambient*][**+s**\ *sigma*]\
106-
[**+o**\ *offset*].
107-
The actual gradients :math:`g` are offset and scaled to produce
108-
normalized gradients :math:`g_n` with a maximum output magnitude of
109-
*amp*. If *amp* is not given, default *amp* = 1. If *offset* is not
110-
given, it is set to the average of :math:`g`. The following forms are
111-
supported:
112-
113-
- **True**: Normalize using math:`g_n = \mbox{amp}\
114-
(\frac{g - \mbox{offset}}{max(|g - \mbox{offset}|)})`
115-
- **e**: Normalize using a cumulative Laplace distribution yielding:
116-
:math:`g_n = \mbox{amp}(1 - \
117-
\exp{(\sqrt{2}\frac{g - \mbox{offset}}{\sigma}))}`, where
118-
:math:`\sigma` is estimated using the L1 norm of
119-
:math:`(g - \mbox{offset})` if it is not given.
120-
- **t**: Normalize using a cumulative Cauchy distribution yielding:
121-
:math:`g_n = \
122-
\frac{2(\mbox{amp})}{\pi}(\tan^{-1}(\frac{g - \
123-
\mbox{offset}}{\sigma}))` where :math:`\sigma` is estimated
124-
using the L2 norm of :math:`(g - \mbox{offset})` if it is not
125-
given.
126-
127-
As a final option, you may add **+a**\ *ambient* to add *ambient* to
128-
all nodes after gradient calculations are completed.
164+
normalize
165+
Normalize the output gradients. Valid values are:
166+
167+
- ``False``: No normalization is done [Default].
168+
- ``True``: Normalize using max absolute value.
169+
- ``"laplace"``: Normalize using cumulative Laplace distribution.
170+
- ``"cauchy"``: Normalize using cumulative Cauchy distribution.
171+
172+
The normalization process is controlled via the additional parameters
173+
``norm_amp``, ``norm_ambient``, ``norm_sigma``, and ``norm_offset``.
174+
175+
Let :math:`g` denote the actual gradients, :math:`g_n` the normalized gradients,
176+
:math:`a` the maximum output magnitude (``norm_amp``), :math:`o` the offset
177+
value (``norm_offset``), and :math:`\sigma` the sigma value (``norm_sigma``).
178+
The normalization is computed as follows:
179+
180+
- ``True``: :math:`g_n = a (\frac{g - o}{\max(|g - o|)})`
181+
- ``"laplace"``: :math:`g_n = a(1 - \exp(\sqrt{2}\frac{g - o}{\sigma}))`
182+
- ``"cauchy"``: :math:`g_n = \frac{2a}{\pi}\arctan(\frac{g - o}{\sigma})`
183+
norm_amp
184+
Set the maximum output magnitude [Default is 1].
185+
norm_ambient
186+
The ambient value to add to all nodes after gradient calculations are completed
187+
[Default is 0].
188+
norm_offset
189+
The offset value used in the normalization. If not given, it is set to the
190+
average of :math:`g`.
191+
norm_sigma
192+
The *sigma* value used in the Laplace or Cauchy normalization. If not given,
193+
it is estimated from the L1 norm of :math:`g-o` for Laplace or the L2 norm of
194+
:math:`g-o` for Cauchy.
129195
tiles : str
130196
**c**\|\ **r**\|\ **R**.
131197
Control how normalization via ``normalize`` is carried out. When
@@ -137,10 +203,10 @@ def grdgradient(
137203
grid output is not needed for this run then do not specify
138204
``outgrid``. For subsequent runs, just use **r** to read these
139205
values. Using **R** will read then delete the statistics file.
140-
$region
141206
slope_file : str
142207
Name of output grid file with scalar magnitudes of gradient vectors.
143208
Requires ``direction`` but makes ``outgrid`` optional.
209+
$region
144210
$verbose
145211
$coltypes
146212
$interpolation
@@ -179,6 +245,13 @@ def grdgradient(
179245
aliasdict = AliasSystem(
180246
A=Alias(azimuth, name="azimuth", sep="/", size=2),
181247
E=Alias(radiance, name="radiance", sep="/", size=2),
248+
N=_alias_option_N(
249+
normalize=normalize,
250+
norm_amp=norm_amp,
251+
norm_ambient=norm_ambient,
252+
norm_sigma=norm_sigma,
253+
norm_offset=norm_offset,
254+
),
182255
).add_common(
183256
R=region,
184257
V=verbose,

pygmt/tests/test_grdgradient.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import xarray as xr
99
from pygmt import grdgradient
1010
from pygmt.enums import GridRegistration, GridType
11-
from pygmt.exceptions import GMTParameterError
11+
from pygmt.exceptions import GMTInvalidInput, GMTParameterError
1212
from pygmt.helpers import GMTTempFile
1313
from pygmt.helpers.testing import load_static_earth_relief
1414

@@ -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, azimuth=10, normalize="t", norm_amp=2)
82+
with pytest.raises(GMTInvalidInput):
83+
grdgradient(grid=grid, azimuth=10, normalize="t1", norm_amp=2)
84+
with pytest.raises(GMTInvalidInput):
85+
grdgradient(grid=grid, azimuth=10, normalize="t1", norm_sigma=2)
86+
with pytest.raises(GMTInvalidInput):
87+
grdgradient(grid=grid, azimuth=10, normalize="e", norm_offset=5)
88+
with pytest.raises(GMTInvalidInput):
89+
grdgradient(grid=grid, azimuth=10, 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)