Fixing Weird Patterns When Plotting NEXRAD Level 3 Data#

The motivation here comes from a thread on Twitter, indicating an issue when plotting NEXRAD Level 3 radial velocity (NOU).

Here is the radar image, plotted by David (@dryglick):

drawing

Notice the odd pattern directly north/south and east/west of the radar…

There appears to be a crosshair of grey/darker pixels in our image.

This was identified as a Moiré Pattern, by Ryan May (@dopplershift), lead developer of MetPy, an open source python library for working with meteorological data.

The Data#

We are using NEXRAD Level 3 Data, which is ground-based radar data in the United States, defined as:

“NEXRAD Level 3 products are used to remotely detect atmospheric features, such as precipitation, precipitation-type, storms, turbulence and wind, for operational forecasting and data research analysis.”

This dataset is provided by the National Oceanic and Atmospheric Administration (NOAA).

Data Access#

We can access this dataset using the NCEI NEXRAD Level 3 Search Portal. We could access data from Amazon Web Services (AWS) if we were interested in data after 2020, but since we are looking at at data from 2018, we need to use the NCEI archive.

David mentioned he was looking at velocity data (N0U) from Elgin Air Force Base in the Panhandle of Florida, with the station identifier of KEVX, at 1458 UTC on October 10, 2018, which was hours before Hurricane Michael made landfall along the Gulf Coast.

We can enter the:

  • Radar data field (N0U)

  • Location (KEVX)

  • Date/time (10/10/2018 at 1458 UTC)

into our query. Here is what it should lookl like on the webpage:

radar-data-query

Once we add this to our cart, and wait for the data to be sent to our email, we download via HTTP (through a website) or FTP.

We can now unzip the compressed file, then take a look at our data file, reading in the N0U (velocity) field.

The specific file we are interested in is:

KMOB_SDUS54_N0UEVX_201810101458

Recreate the Issue Using PyART#

Imports#

import cartopy.crs as ccrs
import cartopy.feature as cfeature
import matplotlib.colors as mcolors
import pyart
import numpy as np
import matplotlib.pyplot as plt
import warnings

warnings.filterwarnings("ignore")
## You are using the Python ARM Radar Toolkit (Py-ART), an open source
## library for working with weather radar data. Py-ART is partly
## supported by the U.S. Department of Energy as part of the Atmospheric
## Radiation Measurement (ARM) Climate Research Facility, an Office of
## Science user facility.
##
## If you use this software to prepare a publication, please cite:
##
##     JJ Helmus and SM Collis, JORS 2016, doi: 10.5334/jors.119

Read the data using PyART#

When reading into PyART, we can use the pyart.io.read_level3 module, or, we can use the pyart.io.read module which automatically detects the filetype being read in.

radar = pyart.io.read("KMOB_SDUS54_N0UEVX_201810101458")

Plot our Data Using RadarMapDisplay#

Setting our Matplotlib style#

Before we dig into plotting, let’s set our background to dark since that is what the original image showed and it will be easier to identify the pattern in the data.

plt.style.use("dark_background")

Plot our data without setting alpha#

Since we want geographical boundaries on our map, let’s use RadarMapDisplay with our Radar object to create the map.

When we create our plot, everything looks okay - we don’t see the odd Moiré Pattern…

fig = plt.figure(figsize=(10, 7))
display = pyart.graph.RadarMapDisplay(radar)
display.plot_ppi_map(
    "velocity",
    sweep=0,
    vmin=-100,
    vmax=100,
    projection=ccrs.PlateCarree(),
    colorbar_label="radial velocity (m/s)",
    cmap="pyart_balance",
)
plt.xlim(-88, -83)
plt.ylim(28, 33)
plt.show()
../../_images/f8b9932e744dc5d4f8c83362f66afdccf851fc5a1a34852862e1e538ed5880ba.png

Add in the alpha argument#

When we add in the alpha=0.8 argument, which adjusts the transparency of our field, we start to see the pattern.

fig = plt.figure(figsize=(10, 7))
display = pyart.graph.RadarMapDisplay(radar)
display.plot_ppi_map(
    "velocity",
    alpha=0.8,
    sweep=0,
    vmin=-100,
    vmax=100,
    projection=ccrs.PlateCarree(),
    colorbar_label="radial velocity (m/s)",
    cmap="pyart_balance",
)

plt.xlim(-88, -83)
plt.ylim(28, 33)
plt.show()
../../_images/41b10f5841282cd40064bea17f02c33e9660f45e8c4d070ec9cf4a81d078bd7a.png

Dig into the Issue#

Investigate plot_ppi_map#

Let’s take a look at the specific plotting call being made with plot_ppi_map using ??:

??display.plot_ppi_map
Signature:
display.plot_ppi_map(
    field,
    sweep=0,
    mask_tuple=None,
    vmin=None,
    vmax=None,
    cmap=None,
    norm=None,
    mask_outside=False,
    title=None,
    title_flag=True,
    colorbar_flag=True,
    colorbar_label=None,
    ax=None,
    fig=None,
    lat_lines=None,
    lon_lines=None,
    projection=None,
    min_lon=None,
    max_lon=None,
    min_lat=None,
    max_lat=None,
    width=None,
    height=None,
    lon_0=None,
    lat_0=None,
    resolution='110m',
    shapefile=None,
    shapefile_kwargs=None,
    edges=True,
    gatefilter=None,
    filter_transitions=True,
    embelish=True,
    raster=False,
    ticks=None,
    ticklabs=None,
    alpha=None,
)
Source:   
    def plot_ppi_map(
            self, field, sweep=0, mask_tuple=None,
            vmin=None, vmax=None, cmap=None, norm=None, mask_outside=False,
            title=None, title_flag=True,
            colorbar_flag=True, colorbar_label=None, ax=None, fig=None,
            lat_lines=None, lon_lines=None, projection=None,
            min_lon=None, max_lon=None, min_lat=None, max_lat=None,
            width=None, height=None, lon_0=None, lat_0=None,
            resolution='110m', shapefile=None, shapefile_kwargs=None,
            edges=True, gatefilter=None,
            filter_transitions=True, embelish=True, raster=False,
            ticks=None, ticklabs=None, alpha=None):
        """
        Plot a PPI volume sweep onto a geographic map.

        Parameters
        ----------
        field : str
            Field to plot.
        sweep : int, optional
            Sweep number to plot.

        Other Parameters
        ----------------
        mask_tuple : (str, float)
            Tuple containing the field name and value below which to mask
            field prior to plotting, for example to mask all data where
            NCP < 0.5 set mask_tuple to ['NCP', 0.5]. None performs no masking.
        vmin : float
            Luminance minimum value, None for default value.
            Parameter is ignored is norm is not None.
        vmax : float
            Luminance maximum value, None for default value.
            Parameter is ignored is norm is not None.
        norm : Normalize or None, optional
            matplotlib Normalize instance used to scale luminance data. If not
            None the vmax and vmin parameters are ignored. If None, vmin and
            vmax are used for luminance scaling.
        cmap : str or None
            Matplotlib colormap name. None will use the default colormap for
            the field being plotted as specified by the Py-ART configuration.
        mask_outside : bool
            True to mask data outside of vmin, vmax. False performs no
            masking.
        title : str
            Title to label plot with, None to use default title generated from
            the field and tilt parameters. Parameter is ignored if title_flag
            is False.
        title_flag : bool
            True to add a title to the plot, False does not add a title.
        colorbar_flag : bool
            True to add a colorbar with label to the axis. False leaves off
            the colorbar.
        ticks : array
            Colorbar custom tick label locations.
        ticklabs : array
            Colorbar custom tick labels.
        colorbar_label : str
            Colorbar label, None will use a default label generated from the
            field information.
        ax : Cartopy GeoAxes instance
            If None, create GeoAxes instance using other keyword info.
            If provided, ax must have a Cartopy crs projection and projection
            kwarg below is ignored.
        fig : Figure
            Figure to add the colorbar to. None will use the current figure.
        lat_lines, lon_lines : array or None
            Locations at which to draw latitude and longitude lines.
            None will use default values which are resonable for maps of
            North America.
        projection : cartopy.crs class
            Map projection supported by cartopy. Used for all subsequent calls
            to the GeoAxes object generated. Defaults to LambertConformal
            centered on radar.
        min_lat, max_lat, min_lon, max_lon : float
            Latitude and longitude ranges for the map projection region in
            degrees.
        width, height : float
            Width and height of map domain in meters.
            Only this set of parameters or the previous set of parameters
            (min_lat, max_lat, min_lon, max_lon) should be specified.
            If neither set is specified then the map domain will be determined
            from the extend of the radar gate locations.
        shapefile : str
            Filename for a shapefile to add to map.
        shapefile_kwargs : dict
            Key word arguments used to format shapefile. Projection defaults
            to lat lon (cartopy.crs.PlateCarree())
        resolution : '10m', '50m', '110m'.
            Resolution of NaturalEarthFeatures to use. See Cartopy
            documentation for details.
        gatefilter : GateFilter
            GateFilter instance. None will result in no gatefilter mask being
            applied to data.
        filter_transitions : bool
            True to remove rays where the antenna was in transition between
            sweeps from the plot. False will include these rays in the plot.
            No rays are filtered when the antenna_transition attribute of the
            underlying radar is not present.
        edges : bool
            True will interpolate and extrapolate the gate edges from the
            range, azimuth and elevations in the radar, treating these
            as specifying the center of each gate. False treats these
            coordinates themselved as the gate edges, resulting in a plot
            in which the last gate in each ray and the entire last ray are not
            not plotted.
        embelish: bool
            True by default. Set to False to supress drawing of coastlines
            etc.. Use for speedup when specifying shapefiles.
            Note that lat lon labels only work with certain projections.
        raster : bool
            False by default. Set to true to render the display as a raster
            rather than a vector in call to pcolormesh. Saves time in plotting
            high resolution data over large areas. Be sure to set the dpi
            of the plot for your application if you save it as a vector format
            (i.e., pdf, eps, svg).
        alpha : float or None
            Set the alpha tranparency of the radar plot. Useful for
            overplotting radar over other datasets.

        """
        # parse parameters
        vmin, vmax = parse_vmin_vmax(self._radar, field, vmin, vmax)
        cmap = parse_cmap(cmap, field)
        if lat_lines is None:
            lat_lines = np.arange(30, 46, 1)
        if lon_lines is None:
            lon_lines = np.arange(-110, -75, 1)
        lat_0 = self.loc[0]
        lon_0 = self.loc[1]

        # get data for the plot
        data = self._get_data(
            field, sweep, mask_tuple, filter_transitions, gatefilter)
        x, y = self._get_x_y(sweep, edges, filter_transitions)

        # mask the data where outside the limits
        if mask_outside:
            data = np.ma.masked_outside(data, vmin, vmax)

        # Define a figure if None is provided.
        if fig is None:
            fig = plt.gcf()

        # initialize instance of GeoAxes if not provided
        if ax is not None:
            if hasattr(ax, 'projection'):
                projection = ax.projection
            else:
                if projection is None:
                    # set map projection to LambertConformal if none is
                    # specified.
                    projection = cartopy.crs.LambertConformal(
                        central_longitude=lon_0, central_latitude=lat_0)
                    warnings.warn(
                        "No projection was defined for the axes."
                        + " Overridding defined axes and using default "
                        + "axes with projection Lambert Conformal.",
                        UserWarning)
                ax = plt.axes(projection=projection)

        # Define GeoAxes if None is provided.
        else:
            if projection is None:
                # set map projection to LambertConformal if none is
                # specified.
                projection = cartopy.crs.LambertConformal(
                    central_longitude=lon_0, central_latitude=lat_0)
                warnings.warn(
                    "No projection was defined for the axes."
                    + " Overridding defined axes and using default "
                    + "axes with projection Lambert Conformal.",
                    UserWarning)
            ax = plt.axes(projection=projection)

        if min_lon:
            ax.set_extent([min_lon, max_lon, min_lat, max_lat],
                          crs=cartopy.crs.PlateCarree())
        elif width:
            ax.set_extent([-width/2., width/2., -height/2., height/2.],
                          crs=self.grid_projection)

        # plot the data
        if norm is not None:  # if norm is set do not override with vmin/vmax
            vmin = vmax = None
        pm = ax.pcolormesh(x * 1000., y * 1000., data, alpha=alpha,
                           vmin=vmin, vmax=vmax, cmap=cmap,
                           norm=norm, transform=self.grid_projection)

        # plot as raster in vector graphics files
        if raster:
            pm.set_rasterized(True)

        # add embelishments
        if embelish is True:
            # Create a feature for States/Admin 1 regions at 1:resolution
            # from Natural Earth
            states_provinces = cartopy.feature.NaturalEarthFeature(
                category='cultural',
                name='admin_1_states_provinces_lines',
                scale=resolution,
                facecolor='none')
            ax.coastlines(resolution=resolution)
            ax.add_feature(states_provinces, edgecolor='gray')

            # labeling gridlines poses some difficulties depending on the
            # projection, so we need some projection-spectific methods
            if ax.projection in [cartopy.crs.PlateCarree(),
                                 cartopy.crs.Mercator()]:
                gl = ax.gridlines(xlocs=lon_lines, ylocs=lat_lines,
                                  draw_labels=True)
                gl.xlabels_top = False
                gl.ylabels_right = False

            elif isinstance(ax.projection, cartopy.crs.LambertConformal):
                ax.figure.canvas.draw()
                ax.gridlines(xlocs=lon_lines, ylocs=lat_lines)

                # Label the end-points of the gridlines using the custom
                # tick makers:
                ax.xaxis.set_major_formatter(
                    cartopy.mpl.gridliner.LONGITUDE_FORMATTER)
                ax.yaxis.set_major_formatter(
                    cartopy.mpl.gridliner.LATITUDE_FORMATTER)
                if _LAMBERT_GRIDLINES:
                    lambert_xticks(ax, lon_lines)
                    lambert_yticks(ax, lat_lines)
            else:
                ax.gridlines(xlocs=lon_lines, ylocs=lat_lines)

        # plot the data and optionally the shape file
        # we need to convert the radar gate locations (x and y) which are in
        # km to meters we also need to give the original projection of the
        # data which is stored in self.grid_projection

        if shapefile is not None:
            from cartopy.io.shapereader import Reader
            if shapefile_kwargs is None:
                shapefile_kwargs = {}
            if 'crs' not in shapefile_kwargs:
                shapefile_kwargs['crs'] = cartopy.crs.PlateCarree()
            ax.add_geometries(Reader(shapefile).geometries(),
                              **shapefile_kwargs)

        if title_flag:
            self._set_title(field, sweep, title, ax)

        # add plot and field to lists
        self.plots.append(pm)
        self.plot_vars.append(field)

        if colorbar_flag:
            self.plot_colorbar(
                mappable=pm, label=colorbar_label, field=field, fig=fig,
                ax=ax, ticks=ticks, ticklabs=ticklabs)
        # keep track of this GeoAxes object for later
        self.ax = ax
        return
File:      ~/git_repos/pyart/pyart/graph/radarmapdisplay.py
Type:      method

We are using matplotlib.pyplot.pcolormesh (plt.pcolormesh) here to plot our radar data - using the included parameters:

  • alpha (data transparency)

  • vmin (minimum value to plot)

  • vmax (maximum value to plot)

  • cmap (colormap to use)

  • norm (colormap normalization)

  • transform (which projection to use)

For pcolormesh parameters not included in this list, pyart is using the default parameters…

Investigate pcolormesh parameters#

In a similar fashion to the plot_ppi_map function from pyart, we can take a look at the parameters for plt.pcolormesh:

?plt.pcolormesh
Signature:
plt.pcolormesh(
    *args,
    alpha=None,
    norm=None,
    cmap=None,
    vmin=None,
    vmax=None,
    shading=None,
    antialiased=False,
    data=None,
    **kwargs,
)
Docstring:
Create a pseudocolor plot with a non-regular rectangular grid.

Call signature::

    pcolormesh([X, Y,] C, **kwargs)

*X* and *Y* can be used to specify the corners of the quadrilaterals.

.. hint::

   `~.Axes.pcolormesh` is similar to `~.Axes.pcolor`. It is much faster
   and preferred in most cases. For a detailed discussion on the
   differences see :ref:`Differences between pcolor() and pcolormesh()
   <differences-pcolor-pcolormesh>`.

Parameters
----------
C : 2D array-like
    The color-mapped values.

X, Y : array-like, optional
    The coordinates of the corners of quadrilaterals of a pcolormesh::

        (X[i+1, j], Y[i+1, j])       (X[i+1, j+1], Y[i+1, j+1])
                              +-----+
                              |     |
                              +-----+
            (X[i, j], Y[i, j])       (X[i, j+1], Y[i, j+1])

    Note that the column index corresponds to the x-coordinate, and
    the row index corresponds to y. For details, see the
    :ref:`Notes <axes-pcolormesh-grid-orientation>` section below.

    If ``shading='flat'`` the dimensions of *X* and *Y* should be one
    greater than those of *C*, and the quadrilateral is colored due
    to the value at ``C[i, j]``.  If *X*, *Y* and *C* have equal
    dimensions, a warning will be raised and the last row and column
    of *C* will be ignored.

    If ``shading='nearest'`` or ``'gouraud'``, the dimensions of *X*
    and *Y* should be the same as those of *C* (if not, a ValueError
    will be raised).  For ``'nearest'`` the color ``C[i, j]`` is
    centered on ``(X[i, j], Y[i, j])``.  For ``'gouraud'``, a smooth
    interpolation is caried out between the quadrilateral corners.

    If *X* and/or *Y* are 1-D arrays or column vectors they will be
    expanded as needed into the appropriate 2D arrays, making a
    rectangular grid.

cmap : str or `~matplotlib.colors.Colormap`, default: :rc:`image.cmap`
    A Colormap instance or registered colormap name. The colormap
    maps the *C* values to colors.

norm : `~matplotlib.colors.Normalize`, optional
    The Normalize instance scales the data values to the canonical
    colormap range [0, 1] for mapping to colors. By default, the data
    range is mapped to the colorbar range using linear scaling.

vmin, vmax : float, default: None
    The colorbar range. If *None*, suitable min/max values are
    automatically chosen by the `.Normalize` instance (defaults to
    the respective min/max values of *C* in case of the default linear
    scaling).
    It is an error to use *vmin*/*vmax* when *norm* is given.

edgecolors : {'none', None, 'face', color, color sequence}, optional
    The color of the edges. Defaults to 'none'. Possible values:

    - 'none' or '': No edge.
    - *None*: :rc:`patch.edgecolor` will be used. Note that currently
      :rc:`patch.force_edgecolor` has to be True for this to work.
    - 'face': Use the adjacent face color.
    - A color or sequence of colors will set the edge color.

    The singular form *edgecolor* works as an alias.

alpha : float, default: None
    The alpha blending value, between 0 (transparent) and 1 (opaque).

shading : {'flat', 'nearest', 'gouraud', 'auto'}, optional
    The fill style for the quadrilateral; defaults to
    'flat' or :rc:`pcolor.shading`. Possible values:

    - 'flat': A solid color is used for each quad. The color of the
      quad (i, j), (i+1, j), (i, j+1), (i+1, j+1) is given by
      ``C[i, j]``. The dimensions of *X* and *Y* should be
      one greater than those of *C*; if they are the same as *C*,
      then a deprecation warning is raised, and the last row
      and column of *C* are dropped.
    - 'nearest': Each grid point will have a color centered on it,
      extending halfway between the adjacent grid centers.  The
      dimensions of *X* and *Y* must be the same as *C*.
    - 'gouraud': Each quad will be Gouraud shaded: The color of the
      corners (i', j') are given by ``C[i', j']``. The color values of
      the area in between is interpolated from the corner values.
      The dimensions of *X* and *Y* must be the same as *C*. When
      Gouraud shading is used, *edgecolors* is ignored.
    - 'auto': Choose 'flat' if dimensions of *X* and *Y* are one
      larger than *C*.  Choose 'nearest' if dimensions are the same.

    See :doc:`/gallery/images_contours_and_fields/pcolormesh_grids`
    for more description.

snap : bool, default: False
    Whether to snap the mesh to pixel boundaries.

rasterized : bool, optional
    Rasterize the pcolormesh when drawing vector graphics.  This can
    speed up rendering and produce smaller files for large data sets.
    See also :doc:`/gallery/misc/rasterization_demo`.

Returns
-------
`matplotlib.collections.QuadMesh`

Other Parameters
----------------
data : indexable object, optional
    If given, all parameters also accept a string ``s``, which is
    interpreted as ``data[s]`` (unless this raises an exception).

**kwargs
    Additionally, the following arguments are allowed. They are passed
    along to the `~matplotlib.collections.QuadMesh` constructor:

Properties:
    agg_filter: a filter function, which takes a (m, n, 3) float array and a dpi value, and returns a (m, n, 3) array
    alpha: array-like or scalar or None
    animated: bool
    antialiased or aa or antialiaseds: bool or list of bools
    array: (M, N) array-like or M*N array-like
    capstyle: `.CapStyle` or {'butt', 'projecting', 'round'}
    clim: (vmin: float, vmax: float)
    clip_box: `.Bbox`
    clip_on: bool
    clip_path: Patch or (Path, Transform) or None
    cmap: `.Colormap` or str or None
    color: color or list of rgba tuples
    edgecolor or ec or edgecolors: color or list of colors or 'face'
    facecolor or facecolors or fc: color or list of colors
    figure: `.Figure`
    gid: str
    hatch: {'/', '\\', '|', '-', '+', 'x', 'o', 'O', '.', '*'}
    in_layout: bool
    joinstyle: `.JoinStyle` or {'miter', 'round', 'bevel'}
    label: object
    linestyle or dashes or linestyles or ls: str or tuple or list thereof
    linewidth or linewidths or lw: float or list of floats
    norm: `.Normalize` or None
    offset_transform: `.Transform`
    offsets: (N, 2) or (2,) array-like
    path_effects: `.AbstractPathEffect`
    picker: None or bool or float or callable
    pickradius: float
    rasterized: bool
    sketch_params: (scale: float, length: float, randomness: float)
    snap: bool or None
    transform: `.Transform`
    url: str
    urls: list of str or None
    visible: bool
    zorder: float

See Also
--------
pcolor : An alternative implementation with slightly different
    features. For a detailed discussion on the differences see
    :ref:`Differences between pcolor() and pcolormesh()
    <differences-pcolor-pcolormesh>`.
imshow : If *X* and *Y* are each equidistant, `~.Axes.imshow` can be a
    faster alternative.

Notes
-----
**Masked arrays**

*C* may be a masked array. If ``C[i, j]`` is masked, the corresponding
quadrilateral will be transparent. Masking of *X* and *Y* is not
supported. Use `~.Axes.pcolor` if you need this functionality.

.. _axes-pcolormesh-grid-orientation:

**Grid orientation**

The grid orientation follows the standard matrix convention: An array
*C* with shape (nrows, ncolumns) is plotted with the column number as
*X* and the row number as *Y*.

.. _differences-pcolor-pcolormesh:

**Differences between pcolor() and pcolormesh()**

Both methods are used to create a pseudocolor plot of a 2D array
using quadrilaterals.

The main difference lies in the created object and internal data
handling:
While `~.Axes.pcolor` returns a `.PolyCollection`, `~.Axes.pcolormesh`
returns a `.QuadMesh`. The latter is more specialized for the given
purpose and thus is faster. It should almost always be preferred.

There is also a slight difference in the handling of masked arrays.
Both `~.Axes.pcolor` and `~.Axes.pcolormesh` support masked arrays
for *C*. However, only `~.Axes.pcolor` supports masked arrays for *X*
and *Y*. The reason lies in the internal handling of the masked values.
`~.Axes.pcolor` leaves out the respective polygons from the
PolyCollection. `~.Axes.pcolormesh` sets the facecolor of the masked
elements to transparent. You can see the difference when using
edgecolors. While all edges are drawn irrespective of masking in a
QuadMesh, the edge between two adjacent masked quadrilaterals in
`~.Axes.pcolor` is not drawn as the corresponding polygons do not
exist in the PolyCollection.

Another difference is the support of Gouraud shading in
`~.Axes.pcolormesh`, which is not available with `~.Axes.pcolor`.
File:      ~/opt/anaconda3/envs/pyart-docs/lib/python3.9/site-packages/matplotlib/pyplot.py
Type:      function

Digging into edgecolors#

Notice how one of the arguments included here is edgecolors, which by default, is none. One of the options here is to set these to be “face”, which is the adjacent data color.

Let’s test this out with our data to see if changing this parameter makes a difference…

Try Modifying the edgecolors argument with pcolormesh#

Let’s test this out with our data!

First, let’s read in the numpy array of data using ['data'].

velocity = radar.fields["velocity"]["data"]
velocity
masked_array(
  data=[[--, --, --, ..., --, --, --],
        [--, --, --, ..., --, --, --],
        [--, --, --, ..., --, --, --],
        ...,
        [--, --, --, ..., --, --, --],
        [--, --, --, ..., --, --, --],
        [--, --, --, ..., --, --, --]],
  mask=[[ True,  True,  True, ...,  True,  True,  True],
        [ True,  True,  True, ...,  True,  True,  True],
        [ True,  True,  True, ...,  True,  True,  True],
        ...,
        [ True,  True,  True, ...,  True,  True,  True],
        [ True,  True,  True, ...,  True,  True,  True],
        [ True,  True,  True, ...,  True,  True,  True]],
  fill_value=1e+20,
  dtype=float32)

Next, we need to access our coordinates in latitude/longitude form using get_gate_lat_lon_alt(sweep)

Since we only have a single sweep, we can set that to 0.

lats, lons, alt = radar.get_gate_lat_lon_alt(0)
lats, lons
(array([[30.565   , 30.562763, 30.560526, ..., 27.888458, 27.886227,
         27.883991],
        [30.565   , 30.562765, 30.560532, ..., 27.892834, 27.890606,
         27.888374],
        [30.565   , 30.562769, 30.56054 , ..., 27.898003, 27.89578 ,
         27.893553],
        ...,
        [30.565   , 30.562756, 30.560509, ..., 27.880096, 27.877857,
         27.875618],
        [30.565   , 30.562756, 30.560513, ..., 27.881853, 27.879616,
         27.877377],
        [30.565   , 30.56276 , 30.560518, ..., 27.884874, 27.88264 ,
         27.880404]], dtype=float32),
 array([[-85.92199 , -85.92222 , -85.92245 , ..., -86.186806, -86.18703 ,
         -86.18723 ],
        [-85.92199 , -85.922264, -85.92254 , ..., -86.2396  , -86.23986 ,
         -86.24012 ],
        [-85.92199 , -85.92231 , -85.92263 , ..., -86.29231 , -86.29262 ,
         -86.29292 ],
        ...,
        [-85.92199 , -85.92208 , -85.92218 , ..., -86.02802 , -86.02811 ,
         -86.0282  ],
        [-85.92199 , -85.92212 , -85.92226 , ..., -86.07571 , -86.07583 ,
         -86.07595 ],
        [-85.92199 , -85.92218 , -85.922356, ..., -86.133934, -86.1341  ,
         -86.13428 ]], dtype=float32))

Plot our data with the default edgecolors argument#

Notice how our pattern shows up again - we get the Moiré Pattern…

plt.pcolormesh(
    lons,
    lats,
    velocity,
    alpha=0.8,
);
../../_images/1ecb2f9924fdda0f2c31e111e4d6dbea665f809b7941d40a6b5ac6b2957dc7b8.png

When we try passing in edgecolors='face' instead, it goes away!

plt.pcolormesh(lons, lats, velocity, alpha=0.8, edgecolors="face");
../../_images/97cc4a1b7e97ec90df79f0bebfaf67489223f0987b5a0e5d8cc0cfc874a599c7.png

Plotting up our Solution#

We found the issue! 😁

Let’s plot up the original figure, with features, colorbars, and all!

# Setup our figure/geoaxis
fig = plt.figure(figsize=(10, 7))
ax = plt.subplot(111, projection=ccrs.PlateCarree())

# Add state borders and the coastline
ax.add_feature(cfeature.STATES, edgecolor="white", linewidth=2)
ax.add_feature(cfeature.COASTLINE, edgecolor="white")

# Plot our velocity field using pcolormesh with edgecolors argument
mesh = ax.pcolormesh(
    lons, lats, velocity, vmin=-100, vmax=100, edgecolors="face", cmap="pyart_balance"
)

# Configure our colorbar
cbar = plt.colorbar(mesh)
cbar.ax.tick_params(labelsize=14)
cbar.set_label("Radial Velocity (m/s)", fontsize=16)

# Add gridlines
gl = ax.gridlines(
    crs=ccrs.PlateCarree(),
    draw_labels=True,
    linewidth=2,
    color="gray",
    alpha=0.5,
    linestyle="--",
)

# Make sure labels are only plotted on the left and bottom
gl.xlabels_top = False
gl.ylabels_right = False

# Increase the fontsize of our gridline labels
gl.xlabel_style = {"fontsize": 14}
gl.ylabel_style = {"fontsize": 14}

# Add a title, using time information
plt.title(
    f"KEVX (Elgin Air Force Base) \n 0.5 Degree Radial Velocity \n {radar.time['units'][14:]}",
    fontsize=16,
)

plt.savefig("kevz_velocity_2018_10_10_1458.png", dpi=300)
../../_images/26394df3238281f870d88f55f6fc4c27a943c8fbf4f682ac894220715aa6071b.png

Fixing the Issue in PyART#

Currently, within PyART, there is not a method of specificying the argument in plot_ppi_map, which is why we needed to add that custom section at the end using plt.pcolormesh directly.

We are submitting a pull request (PR) to change the default behavior here (edgecolors='face').

More importantly, this highlights a current limitation of our plotting API where we cannot specify matplotlib.pcolormesh arguments through plot_ppi_map. We will make sure this PR adds the ability to specify plotting arguements to pass to plt.pcolormesh

Conclusion#

Within this post, we explored an odd pattern that shows up when setting alpha values when plotting radar data using plot_ppi_map(pcolormesh). We needed to change the edgecolors argument in plt.pcolormesh to edgecolors='faces'.

This also highlighted a need in the plot_ppi_map method in PyART to allow users to pass plotting arguments to pcolormesh.

We are appreciative of David for raising this question on Twitter, helping us to work together to find a solution and improve our plotting functionality in PyART!