Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds function to find and plot local extrema #3110

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/_templates/overrides/metpy.calc.rst
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,7 @@ Other
azimuth_range_to_lat_lon
find_bounding_indices
find_intersections
find_local_extrema
get_layer
get_layer_heights
get_perturbation
Expand Down
38 changes: 38 additions & 0 deletions src/metpy/calc/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -781,6 +781,44 @@ def take(indexer):
return take


@exporter.export
def find_local_extrema(var, nsize=15, extrema='max'):
r"""Find the local extreme (max/min) values of a 2D array.

Parameters
----------
var : `numpy.array`
The variable to locate the local extrema using the nearest method
from the maximum_filter or minimum_filter from the scipy.ndimage module.
nsize : int
dopplershift marked this conversation as resolved.
Show resolved Hide resolved
The minimum number of grid points between each local extrema.
Default value is 15.
extrema: str
The value 'max' for local maxima or 'min' for local minima.
Default value is 'max'.

Returns
-------
extrema_mask: `numpy.array`
The boolean array of the local extrema.

See Also
--------
:func:`~metpy.plots.plot_local_extrema`

"""
from scipy.ndimage import maximum_filter, minimum_filter

if extrema == 'max':
extreme_val = maximum_filter(var, nsize, mode='nearest')
elif extrema == 'min':
extreme_val = minimum_filter(var, nsize, mode='nearest')
else:
raise ValueError(f'Invalid value for "extrema": {extrema}. '
'Valid options are "max" or "min".')
return var == extreme_val


@exporter.export
@preprocess_and_wrap()
def lat_lon_grid_deltas(longitude, latitude, x_dim=-1, y_dim=-2, geod=None):
Expand Down
4 changes: 2 additions & 2 deletions src/metpy/plots/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from . import _mpl # noqa: F401
from . import cartopy_utils, plot_areas
from ._util import (add_metpy_logo, add_timestamp, add_unidata_logo, # noqa: F401
convert_gempak_color)
convert_gempak_color, plot_local_extrema)
from .ctables import * # noqa: F403
from .declarative import * # noqa: F403
from .patheffects import * # noqa: F403
Expand All @@ -23,7 +23,7 @@
__all__.extend(station_plot.__all__) # pylint: disable=undefined-variable
__all__.extend(wx_symbols.__all__) # pylint: disable=undefined-variable
__all__.extend(['add_metpy_logo', 'add_timestamp', 'add_unidata_logo',
'convert_gempak_color'])
'convert_gempak_color', 'plot_local_extrema'])

set_module(globals())

Expand Down
76 changes: 76 additions & 0 deletions src/metpy/plots/_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -283,3 +283,79 @@ def normalize(x):
except TypeError:
res = cols[normalize(c)]
return res


def plot_local_extrema(ax, extrema_mask, vals, x, y, symbol, plot_val=True, **kwargs):
"""Plot the local extreme (max/min) values of a 2D array.

The behavior of the plotting will have the symbol horizontal/vertical alignment
be center/bottom and any value plotted will be center/top. The default text size of plotted
values is 0.65 of the symbol size.

Parameters
----------
ax: `matplotlib.axes`
The axes which to plot the local extrema
extrema_mask : `numpy.array`
A boolean array that contains the variable local extrema
vals : `numpy.array`
The variable associated with the extrema_mask
x : `numpy.array`
The x-dimension variable associated with the extrema_vals
y : `numpy.array`
The y-dimension variable associated with the extrema_vals
symbol : str
The text or other string to plot at the local extrema location
plot_val: bool
Whether to plot the local extreme value (default is True)
textsize: int (optional)
Size of plotted extreme values, Default is 0.65 * size

Returns
-------
Plots local extrema on the plot axes

Other Parameters
----------------
kwargs : `matplotlib.pyplot.text` properties.
Other valid `matplotlib.pyplot.text` kwargs can be specified
except verticalalalignment if plotting both a symbol and the value.

Default kwargs:
size : 20
color : 'black'
fontweight : 'bold'
horizontalalignment : 'center'
verticalalignment : 'center'
transform : None

See Also
--------
:func:`~metpy.calc.find_local_extrema`

"""
defaultkwargs = {'size': 20, 'color': 'black', 'fontweight': 'bold',
'horizontalalignment': 'center', 'verticalalignment': 'center'}
kwargs = {**defaultkwargs, **kwargs}
if plot_val:
kwargs.pop('verticalalignment')
size = kwargs.pop('size')
textsize = kwargs.pop('textsize', size * 0.65)

extreme_vals = vals[extrema_mask]
if x.ndim == 1:
xx, yy = np.meshgrid(x, y)
else:
xx = x
yy = y
extreme_x = xx[extrema_mask]
extreme_y = yy[extrema_mask]
for extrema, ex_x, ex_y in zip(extreme_vals, extreme_x, extreme_y):
if plot_val:
ax.text(ex_x, ex_y, symbol, clip_on=True, clip_box=ax.bbox, size=size,
verticalalignment='bottom', **kwargs)
ax.text(ex_x, ex_y, f'{extrema:.0f}', clip_on=True, clip_box=ax.bbox,
size=textsize, verticalalignment='top', **kwargs)
else:
ax.text(ex_x, ex_y, symbol, clip_on=True, clip_box=ax.bbox, size=size,
**kwargs)
51 changes: 47 additions & 4 deletions tests/calc/test_calc_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,11 @@
import xarray as xr

from metpy.calc import (angle_to_direction, find_bounding_indices, find_intersections,
first_derivative, geospatial_gradient, get_layer, get_layer_heights,
gradient, laplacian, lat_lon_grid_deltas, nearest_intersection_idx,
parse_angle, pressure_to_height_std, reduce_point_density,
resample_nn_1d, second_derivative, vector_derivative)
find_local_extrema, first_derivative, geospatial_gradient, get_layer,
get_layer_heights, gradient, laplacian, lat_lon_grid_deltas,
nearest_intersection_idx, parse_angle, pressure_to_height_std,
reduce_point_density, resample_nn_1d, second_derivative,
vector_derivative)
from metpy.calc.tools import (_delete_masked_points, _get_bound_pressure_height,
_greater_or_close, _less_or_close, _next_non_masked_element,
_remove_nans, azimuth_range_to_lat_lon, BASE_DEGREE_MULTIPLIER,
Expand Down Expand Up @@ -475,6 +476,48 @@ def test_get_layer_heights_agl_bottom_no_interp():
assert_array_almost_equal(data_true, data, 6)


@pytest.fixture
def local_extrema_data():
"""Test data for local extrema finding."""
data = xr.DataArray(
np.array([[101628.24, 101483.67, 101366.06, 101287.55, 101233.45],
[101637.19, 101515.555, 101387.164, 101280.32, 101210.15],
[101581.78, 101465.234, 101342., 101233.22, 101180.25],
[101404.31, 101318.4, 101233.18, 101166.445, 101159.93],
[101280.586, 101238.445, 101195.234, 101183.34, 101212.8]]),
name='mslp',
dims=('lat', 'lon'),
coords={'lat': xr.DataArray(np.array([45., 43., 41., 39., 37.]),
dims=('lat',), attrs={'units': 'degrees_north'}),
'lon': xr.DataArray(np.array([265., 267., 269., 271., 273.]),
dims=('lon',), attrs={'units': 'degrees_east'})},
attrs={'units': 'Pa'}
)
return data


def test_find_local_extrema(local_extrema_data):
"""Test find_local_extrema function for maximum."""
local_max = find_local_extrema(local_extrema_data.values, 3, 'max')
local_min = find_local_extrema(local_extrema_data.values, 3, 'min')

max_truth = np.array([[False, False, False, False, False],
[True, False, False, False, False],
[False, False, False, False, False],
[False, False, False, False, False],
[False, False, False, False, True]])
min_truth = np.array([[False, False, False, False, False],
[False, False, False, False, False],
[False, False, False, False, False],
[False, False, False, False, True],
[False, False, False, False, False]])
assert_array_almost_equal(local_max, max_truth)
assert_array_almost_equal(local_min, min_truth)

with pytest.raises(ValueError):
find_local_extrema(local_extrema_data, 3, 'large')


def test_lat_lon_grid_deltas_1d():
"""Test for lat_lon_grid_deltas for variable grid."""
lat = np.arange(40, 50, 2.5)
Expand Down
Binary file added tests/plots/baseline/test_plot_extrema.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
29 changes: 28 additions & 1 deletion tests/plots/test_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@
import pytest
import xarray as xr

from metpy.plots import add_metpy_logo, add_timestamp, add_unidata_logo, convert_gempak_color
from metpy.calc import find_local_extrema
from metpy.plots import (add_metpy_logo, add_timestamp, add_unidata_logo, convert_gempak_color,
plot_local_extrema)
from metpy.testing import get_test_data

MPL_VERSION = matplotlib.__version__[:3]
Expand Down Expand Up @@ -154,3 +156,28 @@ def test_gempak_color_scalar():
mplc = convert_gempak_color(6)
truth = 'cyan'
assert mplc == truth


@pytest.mark.mpl_image_compare(remove_text=True)
def test_plot_extrema():
"""Test plotting of local max/min values."""
data = xr.open_dataset(get_test_data('GFS_test.nc', as_file_obj=False))

Check warning

Code scanning / CodeQL

File is not always closed

File is opened but is not closed.
Fixed Show fixed Hide fixed

mslp = data.Pressure_reduced_to_MSL_msl.squeeze().metpy.convert_units('hPa')
relmax2d = find_local_extrema(mslp.values, 10, 'max')
relmin2d = find_local_extrema(mslp.values, 15, 'min')

fig = plt.figure(figsize=(8., 8.))
ax = fig.add_subplot(1, 1, 1)

# Plot MSLP
clevmslp = np.arange(800., 1120., 4)
ax.contour(mslp.lon, mslp.lat, mslp,
clevmslp, colors='k', linewidths=1.25, linestyles='solid')

plot_local_extrema(ax, relmax2d, mslp.values, mslp.lon, mslp.lat,
'H', plot_val=False, color='tab:red')
plot_local_extrema(ax, relmin2d, mslp.values, mslp.lon, mslp.lat,
'L', color='tab:blue')

return fig
Loading