"""Towerpy: an open-source toolbox for processing polarimetric radar data."""
import numpy as np
import xarray as xr
import matplotlib as mpl
import matplotlib.pyplot as plt
import matplotlib.colors as mpc
from matplotlib.collections import LineCollection
from mpl_toolkits.axes_grid1.axes_divider import make_axes_locatable
from matplotlib.offsetbox import AnchoredText
import matplotlib.patheffects as pe
import matplotlib.ticker as mticker
import cartopy.crs as ccrs
import cartopy.feature as cfeature
import cartopy.io.img_tiles as cimgt
from ..utils import radutilities as rut
from ..base import TowerpyError
from ..utils import unit_conversion as tpuc
# from cartopy.mpl.ticker import LongitudeFormatter, LatitudeFormatter
[docs]
def pltparams(var2plot, rad_varskeys, vars_bounds, ucmap=None, unorm=None,
cb_ext=None):
"""
Create parameters for plots.
Parameters
----------
var2plot : str
Key of the radar variable to plot. The default is None.
This option will plot ZH or the 'first' element in the
rad_vars dict.
rad_varskeys : list
List of radar variables names.
vars_bounds : dict containing key and 3-element tuple or list
The default are:
{'ZH [dBZ]': [-10, 60, 15], 'ZDR [dB]': [-2, 6, 17],
'PhiDP [deg]': [0, 180, 10], 'KDP [deg/km]': [-2, 6, 17],
'rhoHV [-]': [0.3, .9, 1], 'AH [dB/km]': [0, 0.5, 11],
'V [m/s]': [-5, 5, 11], 'gradV [dV/dh]': [-1, 0, 11],
'LDR [dB]': [-30, 10, 17], }, 'Rainfall [mm/h]': [0, 64, 11],
'Rainfall [mm]': [0, 200, 14], 'SQI [0-1]': [0, 1, 11]
'beam_height [km]': [0, 7, 36]}
ucmap : colormap, optional
User-defined colormap, either a mpl.colors.ListedColormap,
or string from matplotlib.colormaps.
unorm : dict containing mpl.colors normalisation objects, optional
User-defined normalisation methods to map colormaps onto
radar data. The default is None.
cb_ext : dict containing key and str, optional
The str modifies the end(s) for out-of-range values for a
given key (radar variable). The str has to be one of 'neither',
'both', 'min' or 'max'.
"""
lpv = {'ZH [dBZ]': [-10, 60, 15], 'ZDR [dB]': [-2, 6, 17],
'PhiDP [deg]': [0, 180, 10], 'KDP [deg/km]': [-2, 6, 17],
'rhoHV [-]': [0.3, .9, 1], 'AH [dB/km]': [0, 0.5, 11],
'V [m/s]': [-5, 5, 11], 'gradV [dV/dh]': [-1.8, 0.6, 13],
'Rainfall [mm/h]': [0, 64, 11], 'Rainfall [mm]': [0, 200, 14],
'beam_height [km]': [0, 7, 36], 'SQI [0-1]': [0, 1, 11]}
if var2plot is not None:
if var2plot != 'ZDR [dB]':
if var2plot == 'LDR [dB]' or 'LDR [dB]' in rad_varskeys:
lpv['LDR [dB]'] = [-30, 10, 17]
if var2plot == 'PIA [dB]' or 'PIA [dB]' in rad_varskeys:
lpv['PIA [dB]'] = [0, 20, 17]
if vars_bounds is not None:
lpv.update(vars_bounds)
if unorm is not None:
lpv2 = {key: [value.vmin, value.vmax, value.N]
for key, value in unorm.items()}
lpv.update(lpv2)
#
bnd = {key[key.find('['):]: np.linspace(value[0], value[1], value[2])
if 'rhoHV' not in key
else np.hstack((np.linspace(value[0], value[1], 4)[:-1],
np.linspace(value[1], value[2], 11)))
for key, value in lpv.items()}
if vars_bounds is None or 'Rainfall [mm/h]' not in vars_bounds.keys():
bnd['[mm/h]'] = np.array((0.1, 1, 2, 4, 8, 12, 16, 20, 24, 30, 36, 48,
56, 64))
if vars_bounds is None or 'Rainfall [mm]' not in vars_bounds.keys():
bnd['[mm]'] = np.array((0.1, 1, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50,
75, 100, 125, 150, 200))
#
cmaph = {'[dBZ]': mpl.colormaps['tpylsc_rad_ref'],
'[-]': mpl.colormaps['tpylsc_rad_pvars'],
'[dB]': mpl.colormaps['tpylsc_rad_2slope'],
'[deg/km]': mpl.colormaps['tpylsc_rad_2slope'],
'[dB/km]': mpl.colormaps['tpylsc_rad_pvars'],
'[m/s]': mpl.colormaps['tpylsc_div_dbu_rd'],
'[mm/h]': mpl.colormaps['tpylsc_rad_rainrt'],
'[mm]': mpl.colormaps['tpylsc_rad_rainrt'],
'[km]': mpl.colormaps['gist_earth'],
'[dV/dh]': mpl.colormaps['tpylsc_rad_2slope_r'],
}
cmaph['[mm/h]'].set_under('whitesmoke')
cmaph['[mm]'].set_under('whitesmoke')
if var2plot and '[dB]' in var2plot and var2plot != 'ZDR [dB]':
cmaph['[dB]'] = mpl.colormaps['tpylsc_rad_pvars']
if var2plot == 'LDR [dB]':
cmaph['[dB]'] = mpl.colormaps['tpylsc_rad_2slope_r']
if var2plot == 'PIA [dB]':
cmaph['[dB]'] = mpl.colormaps['tpylsc_useq_fiery']
# elif len(rad_varskeys) == 1 and 'LDR [dB]' in rad_varskeys:
# cmaph['[dB]'] = mpl.colormaps['tpylsc_rad_2slope_r']
if ucmap is not None:
if var2plot:
if isinstance(ucmap, str):
cmaph[var2plot[var2plot.find('['):]] = mpl.colormaps[ucmap]
else:
cmaph[var2plot[var2plot.find('['):]] = ucmap
else:
if 'ZH [dBZ]' in rad_varskeys:
v2pdmmy = 'ZH [dBZ]'
else:
v2pdmmy = list(rad_varskeys)[0]
if isinstance(ucmap, str):
cmaph[v2pdmmy[v2pdmmy.find('['):]] = mpl.colormaps[ucmap]
else:
cmaph[v2pdmmy[v2pdmmy.find('['):]] = ucmap
#
cmapext = {'[dBZ]': 'both', '[-]': 'both', '[dB]': 'both',
'[deg/km]': 'both', '[m/s]': 'both', '[mm/h]': 'max',
'[mm]': 'max', '[km]': 'max', '[dB/km]': 'max',
'[0-1]': 'both', '[dV/dh]': 'both'}
if var2plot == 'rhoHV [-]':
cmapext['[-]'] = 'min'
if cb_ext:
cb_ext2 = {key[key.find('['):]: value for key, value in cb_ext.items()}
cmapext.update(cb_ext2)
#
dnorm = {key: [value2 for key2, value2 in unorm.items()
if key2[key2.find('['):] == key][0]
if unorm and key in [key2[key2.find('['):]
for key2, value2 in unorm.items()]
else
mpc.BoundaryNorm(value, cmaph.get(
key[key.find('['):], mpl.colormaps['tpylsc_rad_pvars']).N,
extend=cmapext.get(key[key.find('['):], 'both'))
for key, value in bnd.items()}
#
cbtks_fmt = 0
tcks = None
if var2plot is None or var2plot == 'ZH [dBZ]':
if 'ZH [dBZ]' in rad_varskeys:
var2plot = 'ZH [dBZ]'
normp = dnorm['[dBZ]']
if dnorm['[dBZ]']:
tcks = dnorm['[dBZ]'].boundaries
else:
tcks = bnd['[dBZ]']
else:
var2plot = list(rad_varskeys)[0]
normp = dnorm.get(var2plot[var2plot.find('['):])
if dnorm.get(var2plot[var2plot.find('['):]):
tcks = dnorm.get(var2plot[var2plot.find('['):]).boundaries
else:
tcks = bnd.get(var2plot[var2plot.find('['):])
else:
normp = dnorm.get(var2plot[var2plot.find('['):])
# tcks = bnd.get(var2plot[var2plot.find('['):])
if dnorm.get(var2plot[var2plot.find('['):]):
tcks = dnorm.get(var2plot[var2plot.find('['):]).boundaries
else:
tcks = bnd.get(var2plot[var2plot.find('['):])
if var2plot == 'rhoHV [-]':
cbtks_fmt = 2
if '[dB]' in var2plot:
cbtks_fmt = 1
if '[mm/h]' in var2plot:
cbtks_fmt = 1
# tickLabels = map(str, tcks)
if '[mm]' in var2plot:
cbtks_fmt = 1
if '[km]' in var2plot:
cbtks_fmt = 2
if '[dB/km]' in var2plot:
cbtks_fmt = 2
if '[deg/km]' in var2plot:
cbtks_fmt = 1
if '[dV/dh]' in var2plot:
cbtks_fmt = 2
if tcks is not None and len(tcks) > 20:
tcks = None
return lpv, bnd, cmaph, cmapext, dnorm, var2plot, normp, cbtks_fmt, tcks
[docs]
def plot_ppi(rad_georef, rad_params, rad_vars, var2plot=None, mlyr=None,
vars_bounds=None, ucmap=None, unorm=None, plot_contourl=None,
contour_kw=None, coord_sys='polar', cpy_feats=None, data_proj=None,
proj_suffix='osgb', xlims=None, ylims=None, ring=None,
range_rings=None, rd_maxrange=False, pixel_midp=False,
points2plot=None, ptsvar2plot=None, cbticks=None, cb_ext=None,
fig_title=None, fig_size=None, font_sizes='regular'):
"""
Display a radar PPI scan.
Parameters
----------
rad_georef : dict
Georeferenced data containing descriptors of the azimuth, gates
and beam height, amongst others.
rad_params : dict
Radar technical details.
rad_vars : dict
Dict containing radar variables to plot.
var2plot : str, optional
Key of the radar variable to plot. The default is None. This option
will plot ZH or look for the 'first' element in the rad_vars dict.
mlyr : MeltingLayer Class, optional
Plot the melting layer height. ml_top (float, int, list or np.array)
and ml_bottom (float, int, list or np.array) must be explicitly
defined. The default is None.
vars_bounds : dict containing key and 3-element tuple or list, optional
Boundaries [min, max, nvals] between which radar variables are
to be mapped.
ucmap : colormap, optional
User-defined colormap, either a matplotlib.colors.ListedColormap,
or string from matplotlib.colormaps.
unorm : dict containing matplotlib.colors normalisation objects, optional
User-defined normalisation methods to map colormaps onto radar data.
The default is None.
plot_contourl: str, optional
Key of the variable (within rad_vars) used to plot contour lines.
Levels and normalisation are retrieved from vars_bounds, but
these and other parameters can be overridden using the contour_kw
parameter.
contour_kw:
Additional keyword arguments passed to matplotlib.pyplot.contour.
coord_sys : 'rect' or 'polar', optional
Coordinates system (polar or rectangular). The default is 'polar'.
cpy_feats : dict, optional
Cartopy attributes to add to the map. The default are:
{'status': False, 'add_land': False, 'add_ocean': False,
'add_coastline': False, 'add_borders': False, 'add_countries': True,
'add_provinces': True, 'borders_ls': ':', 'add_lakes': False,
'lakes_transparency': 0.5, 'add_rivers': False, 'tiles': False,
'tiles_source': None, 'tiles_style': None}
data_proj : Cartopy Coordinate Reference System object, optional
Cartopy projection used to plot the data in a map e.g.,
ccrs.OSGB(approx=False).
proj_suffix : str, optional
Suffix of the georeferenced grids used to display the data.
The X/Y grids must exist in the rad_georef dictionary, e.g.
'grid_osgbx--grid_osgby', 'grid_utmx--grid_utmy',
'grid_wgs84x--grid_wgs84y', etc. The default is 'osgb'.
xlims : 2-element tuple or list, optional
Set the x-axis view limits [min, max]. The default is None.
ylims : 2-element tuple or list, optional
Set the y-axis view limits [min, max]. The default is None.
ring : int or float, optional
Plot a circle in the given distance, in km.
range_rings : int, float, list or tuple, optional
If int or float, plot circles at a fixed range, in km.
If list or tuple, plot circles at the given ranges, in km.
rd_maxrange : Bool, optional
If True, plot the radar's maximum range coverage. Note that this arg
won't work if a polar coordinates system is used. The default is False.
pixel_midp : Bool, optional
If True, mark the mid-point of all radar pixels. Note that this arg
won't work if a polar coordinates system is used. The default is False.
points2plot : dict, optional
Plot a given set of points. Dict must contain the x-coord and y-coord
in the same format as coord_sys or proj_suffix. A third element inside
the dict can be used as the z-coord.
ptsvar2plot : str, optional
Key of the variable to plot. The default is None. This option
will looks for the 'first' element in the points2plot dict.
cbticks : dict, optional
Modifies the default ticks' location (dict values) and labels
(dict keys) in the colour bar.
cb_ext : dict containing key and str, optional
The str modifies the end(s) for out-of-range values for a
given key (radar variable). The str has to be one of 'neither',
'both', 'min' or 'max'.
fig_title : str, optional
String to show in the plot title.
fig_size : 2-element tuple or list, optional
Modify the default plot size.
font_sizes : str, optional
Modifies the size of the fonts in the plot. The string has to
be one of 'regular' or 'large'.
"""
fsizes = {'fsz_cb': 10, 'fsz_cbt': 12, 'fsz_pt': 14, 'fsz_axlb': 12,
'fsz_axtk': 10}
if font_sizes == 'large':
fsizes = {k1: v1 + 4 for k1, v1 in fsizes.items()}
# szpnts = 25
szpnts = None
#
lpv, bnd, cmaph, cmapext, dnorm, v2p, normp, cbtks_fmt, tcks = pltparams(
var2plot, rad_vars.keys(), vars_bounds, ucmap=ucmap, unorm=unorm,
cb_ext=cb_ext)
if var2plot is None:
var2plot = v2p
cmapp = cmaph.get(var2plot[var2plot.find('['):],
mpl.colormaps['tpylsc_rad_pvars'])
# =============================================================================
# txtboxs = 'round, rounding_size=0.5, pad=0.5'
# txtboxc = (0, -.09)
# fc, ec = 'w', 'k'
# =============================================================================
cpy_features = {'status': False,
# 'coastresolution': '10m',
'add_land': False,
'add_ocean': False,
'add_coastline': False,
'add_borders': False,
'add_countries': True,
'add_provinces': True,
'borders_ls': ':',
'add_lakes': False,
'lakes_transparency': 0.5,
'add_rivers': False,
'tiles': False,
'tiles_source': None,
'tiles_style': None,
'tiles_res': 8, 'alpha_tiles': 0.5, 'alpha_rad': 1
}
if cpy_feats:
cpy_features.update(cpy_feats)
if cpy_features['status']:
coord_sys = 'rect'
states_provinces = cfeature.NaturalEarthFeature(
category='cultural',
name='admin_1_states_provinces_lines',
scale='10m',
facecolor='none')
countries = cfeature.NaturalEarthFeature(
category='cultural',
name='admin_0_countries',
scale='10m',
facecolor='none')
# =============================================================================
if fig_title is None:
if isinstance(rad_params['elev_ang [deg]'], str):
dtdes1 = f"{rad_params['elev_ang [deg]']} -- "
else:
dtdes1 = f"{rad_params['elev_ang [deg]']:{2}.{2}f} deg. -- "
if rad_params['datetime']:
dtdes2 = f"{rad_params['datetime']:%Y-%m-%d %H:%M:%S}"
else:
dtdes2 = ''
ptitle = dtdes1 + dtdes2
else:
ptitle = fig_title
plotunits = [i[i.find('['):]
for i in rad_vars.keys() if var2plot == i][0]
# =============================================================================
if coord_sys == 'polar':
if fig_size is None:
fig_size = (6, 6.15)
fig, ax1 = plt.subplots(figsize=fig_size,
subplot_kw=dict(projection='polar'))
mappable = ax1.pcolormesh(
*np.meshgrid(rad_georef['azim [rad]'],
rad_georef['range [m]'] / 1000, indexing='ij'),
np.flipud(rad_vars[var2plot]), shading='auto', cmap=cmapp,
norm=normp)
ax1.set_title(f'{ptitle} \n' + f'PPI {var2plot}',
fontsize=fsizes['fsz_pt'])
ax1.grid(color='gray', linestyle=':')
ax1.set_theta_zero_location('N')
ax1.tick_params(axis='both', labelsize=fsizes['fsz_axlb'])
ax1.set_yticklabels([])
ax1.set_thetagrids(np.arange(0, 360, 90))
ax1.axes.set_aspect('equal')
if (var2plot == 'rhoHV [-]' or '[mm]' in var2plot
or '[mm/h]' in var2plot):
cb1 = plt.colorbar(mappable, ax=ax1, aspect=8, shrink=0.65,
pad=.1, norm=normp, ticks=tcks,
format=f'%.{cbtks_fmt}f')
cb1.ax.tick_params(direction='in', axis='both',
labelsize=fsizes['fsz_cb'])
else:
cb1 = plt.colorbar(mappable, ax=ax1, aspect=8, shrink=0.65,
pad=.1, norm=normp)
cb1.ax.tick_params(direction='in', axis='both',
labelsize=fsizes['fsz_cb'])
cb1.ax.set_title(f'{plotunits}', fontsize=fsizes['fsz_cbt'])
if cbticks is not None:
cb1.set_ticks(ticks=list(cbticks.values()),
labels=list(cbticks.keys()))
# ax1.annotate('| Created using Towerpy |', xy=txtboxc,
# fontsize=8, xycoords='axes fraction',
# va='center', ha='center',
# bbox=dict(boxstyle=txtboxs, fc=fc, ec=ec))
plt.tight_layout()
# plt.show()
elif coord_sys == 'rect' and cpy_features['status'] is False:
# =====================================================================
# ptitle = dtdes1 + dtdes2
# =====================================================================
if fig_size is None:
fig_size = (6, 6.75)
fig, ax1 = plt.subplots(figsize=fig_size)
mappable = ax1.pcolormesh(rad_georef['grid_rectx'],
rad_georef['grid_recty'],
rad_vars[var2plot], shading='auto',
cmap=cmapp, norm=normp)
if rd_maxrange:
ax1.plot(rad_georef['grid_rectx'][:, -1],
rad_georef['grid_recty'][:, -1], 'gray')
if pixel_midp:
binx = rad_georef['grid_rectx'].ravel()
biny = rad_georef['grid_recty'].ravel()
ax1.scatter(binx, biny, c='grey', marker='+', alpha=0.2)
# =============================================================================
if points2plot is not None:
if len(points2plot) == 2:
ax1.scatter(
points2plot['grid_rectx'], points2plot['grid_recty'],
color='k', marker='o', s=szpnts)
elif len(points2plot) >= 3:
ax1.scatter(
points2plot['grid_rectx'], points2plot['grid_recty'],
marker='o', norm=normp, edgecolors='k', cmap=cmapp,
c=[points2plot[ptsvar2plot]], s=szpnts)
# =============================================================================
if mlyr is not None:
if isinstance(mlyr.ml_top, (int, float)):
mlt_idx = [rut.find_nearest(nbh, mlyr.ml_top)
for nbh in rad_georef['beam_height [km]']]
elif isinstance(mlyr.ml_top, (np.ndarray, list, tuple)):
mlt_idx = [rut.find_nearest(nbh, mlyr.ml_top[cnt])
for cnt, nbh in
enumerate(rad_georef['beam_height [km]'])]
if isinstance(mlyr.ml_bottom, (int, float)):
mlb_idx = [rut.find_nearest(nbh, mlyr.ml_bottom)
for nbh in rad_georef['beam_height [km]']]
elif isinstance(mlyr.ml_bottom, (np.ndarray, list, tuple)):
mlb_idx = [rut.find_nearest(nbh, mlyr.ml_bottom[cnt])
for cnt, nbh in
enumerate(rad_georef['beam_height [km]'])]
mlt_idxx = np.array([rad_georef['grid_rectx'][cnt, ix]
for cnt, ix in enumerate(mlt_idx)])
mlt_idxy = np.array([rad_georef['grid_recty'][cnt, ix]
for cnt, ix in enumerate(mlt_idx)])
mlb_idxx = np.array([rad_georef['grid_rectx'][cnt, ix]
for cnt, ix in enumerate(mlb_idx)])
mlb_idxy = np.array([rad_georef['grid_recty'][cnt, ix]
for cnt, ix in enumerate(mlb_idx)])
ax1.plot(mlt_idxx, mlt_idxy, c='k', ls='-', alpha=3/4,
path_effects=[pe.Stroke(linewidth=5, foreground='w'),
pe.Normal()], label=r'$MLyr_{(T)}$')
ax1.plot(mlb_idxx, mlb_idxy, c='grey', ls='-', alpha=3/4,
path_effects=[pe.Stroke(linewidth=5, foreground='w'),
pe.Normal()], label=r'$MLyr_{(B)}$')
first_legend = ax1.legend(loc='upper left')
# Add the legend manually to the Axes.
ax1.add_artist(first_legend)
# =============================================================================
if range_rings is not None:
if isinstance(range_rings, range):
range_rings = list(range_rings)
if isinstance(range_rings, (int, float)):
nrings = np.arange(range_rings*1000,
rad_georef['range [m]'][-1],
range_rings*1000)
elif isinstance(range_rings, (np.ndarray, list, tuple)):
nrings = np.array(range_rings) * 1000
idx_rs = [rut.find_nearest(rad_georef['range [m]'], r)
for r in nrings]
dmmy_rsx = np.array([rad_georef['grid_rectx'][:, i]
for i in idx_rs])
dmmy_rsy = np.array([rad_georef['grid_recty'][:, i]
for i in idx_rs])
dmmy_rsz = np.array([np.ones(i.shape) for i in dmmy_rsx])
ax1.scatter(dmmy_rsx, dmmy_rsy, dmmy_rsz, c='grey', ls='--',
alpha=3/4)
ax1.axhline(0, c='grey', ls='--', alpha=3/4)
ax1.axvline(0, c='grey', ls='--', alpha=3/4)
ax1.grid(True)
# =============================================================================
if ring is not None:
idx_rr = rut.find_nearest(rad_georef['range [m]'],
ring*1000)
dmmy_rx = rad_georef['grid_rectx'][:, idx_rr]
dmmy_ry = rad_georef['grid_recty'][:, idx_rr]
dmmy_rz = np.ones(dmmy_rx.shape)
ax1.scatter(dmmy_rx, dmmy_ry, dmmy_rz, c='k', ls='--', alpha=3/4)
# =============================================================================
if plot_contourl:
ckw = {'alpha': 0.5, 'zorder': 2, 'colors': None,
'levels': bnd.get(plot_contourl[plot_contourl.find('['):]),
'norm': dnorm.get(plot_contourl[plot_contourl.find('['):]),
'cmap': cmaph.get(plot_contourl[plot_contourl.find('['):]),
'legend': False,
}
if contour_kw is not None:
ckw.update(contour_kw)
contourlp = ax1.contour(
rad_georef['grid_rectx'], rad_georef['grid_recty'],
rad_vars[plot_contourl],
**ckw)
ax1.clabel(contourlp, inline=True, fontsize=fsizes['fsz_cbt'])
if ckw['legend']:
cspl, labels = contourlp.legend_elements()
labels = [lb.replace('x = ', '') for lb in labels]
ax1.legend(cspl, labels, title=plot_contourl,
loc='upper right').set_zorder(5)
# =============================================================================
ax1_divider = make_axes_locatable(ax1)
cax1 = ax1_divider.append_axes('top', size="7%", pad="2%")
if (var2plot == 'rhoHV [-]' or '[mm]' in var2plot
or '[mm/h]' in var2plot):
cb1 = fig.colorbar(mappable, cax=cax1, orientation='horizontal',
ticks=tcks, format=f'%.{cbtks_fmt}f')
cb1.ax.tick_params(direction='in', labelsize=fsizes['fsz_cb'],
rotation=(45 if font_sizes == 'large' else 0))
else:
cb1 = fig.colorbar(mappable, cax=cax1, orientation='horizontal')
cb1.ax.tick_params(direction='in', labelsize=fsizes['fsz_cb'])
fig.suptitle(f'{ptitle} \n' + f'PPI {var2plot}',
fontsize=fsizes['fsz_pt'])
cax1.xaxis.set_ticks_position('top')
if xlims is not None:
ax1.set_xlim(xlims)
if ylims is not None:
ax1.set_ylim(ylims)
ax1.set_xlabel('Distance from the radar [km]',
fontsize=fsizes['fsz_axlb'], labelpad=10)
ax1.set_ylabel('Distance from the radar [km]',
fontsize=fsizes['fsz_axlb'], labelpad=10)
ax1.tick_params(direction='in', axis='both',
labelsize=fsizes['fsz_axtk'])
if cbticks is not None:
cb1.set_ticks(ticks=list(cbticks.values()),
labels=list(cbticks.keys()), ha='right')
ax1.axes.set_aspect('equal')
# ax1.annotate('| Created using Towerpy |', xy=txtboxc,
# fontsize=8, xycoords='axes fraction',
# va='center', ha='center',
# bbox=dict(boxstyle=txtboxs, fc=fc, ec=ec))
# ax1.grid(True)
plt.tight_layout()
# plt.show()
elif cpy_features['status']:
# ptitle = dtdes1 + dtdes2
proj = ccrs.PlateCarree()
if fig_size is None:
fig_size = (12, 6)
if data_proj:
proj2 = data_proj
else:
raise TowerpyError('User must specify the projected coordinate'
' system of the radar data e.g.'
' ccrs.OSGB(approx=False) or ccrs.UTM(zone=32)')
fig = plt.figure(figsize=fig_size, constrained_layout=True)
plt.subplots_adjust(left=0.05, right=0.99, top=0.981, bottom=0.019,
wspace=0, hspace=1)
ax1 = fig.add_subplot(projection=proj)
if xlims and ylims:
extx = xlims
exty = ylims
ax1.set_extent(extx+exty, crs=proj)
if cpy_features['tiles']:
if (cpy_features['tiles_source'] is None
or cpy_features['tiles_source'] == 'OSM'):
imtiles = cimgt.OSM()
ax1.add_image(imtiles, cpy_features['tiles_res'],
interpolation='spline36',
alpha=cpy_features['alpha_tiles'])
elif cpy_features['tiles_source'] == 'GoogleTiles':
if cpy_features['tiles_style'] is None:
imtiles = cimgt.GoogleTiles(style='street')
ax1.add_image(imtiles, cpy_features['tiles_res'],
interpolation='spline36',
alpha=cpy_features['alpha_tiles'])
else:
imtiles = cimgt.GoogleTiles(
style=cpy_features['tiles_style'])
ax1.add_image(imtiles, cpy_features['tiles_res'],
# interpolation='spline36',
alpha=cpy_features['alpha_tiles'])
elif cpy_features['tiles_source'] == 'QuadtreeTiles':
if cpy_features['tiles_style'] is None:
imtiles = cimgt.QuadtreeTiles()
ax1.add_image(imtiles, cpy_features['tiles_res'],
interpolation='spline36',
alpha=cpy_features['alpha_tiles'])
elif cpy_features['tiles_source'] == 'Stamen':
if cpy_features['tiles_style'] is None:
imtiles = cimgt.Stamen(style='toner')
ax1.add_image(imtiles, cpy_features['tiles_res'],
interpolation='spline36',
alpha=cpy_features['alpha_tiles'])
else:
imtiles = cimgt.Stamen(style=cpy_features['tiles_style'])
ax1.add_image(imtiles, cpy_features['tiles_res'],
interpolation='spline36',
alpha=cpy_features['alpha_tiles'])
if cpy_features['add_land']:
ax1.add_feature(cfeature.LAND)
if cpy_features['add_ocean']:
ax1.add_feature(cfeature.OCEAN)
if cpy_features['add_coastline']:
ax1.add_feature(cfeature.COASTLINE)
if cpy_features['add_borders']:
ax1.add_feature(cfeature.BORDERS,
linestyle=cpy_features['borders_ls'])
if cpy_features['add_lakes']:
ax1.add_feature(cfeature.LAKES,
alpha=cpy_features['lakes_transparency'])
if cpy_features['add_rivers']:
ax1.add_feature(cfeature.RIVERS)
if cpy_features['add_countries']:
ax1.add_feature(states_provinces, edgecolor='black', ls=":")
if cpy_features['add_provinces']:
ax1.add_feature(countries, edgecolor='black', )
data_source = 'Natural Earth'
data_license = 'public domain'
# Add a text annotation for the license information to the
# the bottom right corner.
# text = AnchoredText(r'$\copyright$ {}; license: {}'
# ''.format(SOURCE, LICENSE),
# loc=4, prop={'size': 12}, frameon=True)
# ax1.add_artist(text)
print('\N{COPYRIGHT SIGN}' + f'{data_source}; license: {data_license}')
if cpy_features['tiles_source'] == 'Stamen':
print('\N{COPYRIGHT SIGN}' + 'Map tiles by Stamen Design, '
+ 'under CC BY 3.0. Data by OpenStreetMap, under ODbL.')
gl = ax1.gridlines(draw_labels=True, dms=False,
x_inline=False, y_inline=False)
gl.xlabel_style = {'size': fsizes['fsz_axlb']}
gl.ylabel_style = {'size': fsizes['fsz_axlb']}
ax1.set_title(f'{ptitle} \n' + f'PPI {var2plot}',
fontsize=fsizes['fsz_pt'])
# lon_formatter = LongitudeFormatter(number_format='.4f',
# degree_symbol='',
# dateline_direction_label=True)
# lat_formatter = LatitudeFormatter(number_format='.0f',
# degree_symbol=''
# )
mappable = ax1.pcolormesh(rad_georef[f'grid_{proj_suffix}x'],
rad_georef[f'grid_{proj_suffix}y'],
rad_vars[var2plot], transform=proj2,
shading='auto', cmap=cmapp, norm=normp,
alpha=cpy_features['alpha_rad'])
# ax1.xaxis.set_major_formatter(lon_formatter)
# ax1.yaxis.set_major_formatter(lat_formatter)
if pixel_midp:
binx = rad_georef[f'grid_{proj_suffix}x'].ravel()
biny = rad_georef[f'grid_{proj_suffix}y'].ravel()
ax1.scatter(binx, biny, c='grey', marker='+', transform=proj2,
alpha=0.2)
if rd_maxrange:
ax1.plot(rad_georef[f'grid_{proj_suffix}x'][:, -1],
rad_georef[f'grid_{proj_suffix}y'][:, -1],
'gray', transform=proj2)
# =============================================================================
if points2plot is not None:
if len(points2plot) == 2:
ax1.scatter(points2plot[f'grid_{proj_suffix}x'],
points2plot[f'grid_{proj_suffix}y'], color='k',
marker='o', s=szpnts)
elif len(points2plot) >= 3:
ax1.scatter(points2plot[f'grid_{proj_suffix}x'],
points2plot[f'grid_{proj_suffix}y'],
marker='o', norm=normp, edgecolors='k', s=szpnts,
c=points2plot[ptsvar2plot], cmap=cmapp)
# =============================================================================
def make_colorbar(ax1, mappable, **kwargs):
ax1_divider = make_axes_locatable(ax1)
orientation = kwargs.pop('orientation', 'vertical')
if orientation == 'vertical':
loc = 'right'
elif orientation == 'horizontal':
loc = 'top'
# =============================================================================
ticks = tcks
if var2plot in lpv.keys():
if ticks is not None and len(tcks) > 20:
ticks = tcks[::5]
else:
None
# =============================================================================
cax = ax1_divider.append_axes(loc, '7%', pad='15%',
axes_class=plt.Axes)
if cbticks is not None:
ax1.get_figure().colorbar(
mappable, cax=cax, orientation=orientation,
ticks=list(cbticks.values()),
format=mticker.FixedFormatter(list(cbticks.keys())))
else:
ax1.get_figure().colorbar(mappable, cax=cax,
orientation=orientation,
ticks=ticks,
format=f'%.{cbtks_fmt}f')
cax.tick_params(direction='in', labelsize=fsizes['fsz_cb'])
cax.xaxis.set_ticks_position('top')
cax.set_title(plotunits, fontsize=fsizes['fsz_cbt'])
make_colorbar(ax1, mappable, orientation='vertical')
[docs]
def plot_setppi(rad_georef, rad_params, rad_vars, mlyr=None, vars_bounds=None,
ucmap=None, unorm=None, cb_ext=None, xlims=None, ylims=None,
ncols=None, nrows=None, fig_title=None, fig_size=None):
"""
Plot a set of PPIs of polarimetric variables.
Parameters
----------
rad_georef : dict
Georeferenced data containing descriptors of the azimuth, gates
and beam height, amongst others.
rad_params : dict
Radar technical details.
rad_vars : dict
Radar variables to be plotted.
mlyr : MeltingLayer Class, optional
Plot the melting layer height. ml_top (float, int, list or np.array)
and ml_bottom (float, int, list or np.array) must be explicitly
defined. The default is None.
vars_bounds : dict containing key and 3-element tuple or list, optional
Boundaries [min, max, nvals] between which radar variables are
to be mapped.
ucmap : colormap, optional
User-defined colormap, either a matplotlib.colors.ListedColormap,
or string from matplotlib.colormaps.
unorm : dict containing matplotlib.colors normalisation objects, optional
User-defined normalisation methods to map colormaps onto radar data.
The default is None.
cb_ext : dict containing key and str, optional
The str modifies the end(s) for out-of-range values for a
given key (radar variable). The str has to be one of 'neither',
'both', 'min' or 'max'.
xlims : 2-element tuple or list, optional
Set the x-axis view limits [min, max]. The default is None.
ylims : 2-element tuple or list, optional
Set the y-axis view limits [min, max]. The default is None.
ncols : int, optional
Number of columns used to build the grid. The default is None.
nrows : int, optional
Number of rows used to build the grid. The default is None.
fig_title : str, optional
Modify the default plot title.
fig_size : 2-element tuple or list, optional
Modify the default plot size.
"""
if mlyr is not None:
if isinstance(mlyr.ml_top, (int, float)):
mlt_idx = [rut.find_nearest(nbh, mlyr.ml_top)
for nbh in rad_georef['beam_height [km]']]
elif isinstance(mlyr.ml_top, (np.ndarray, list, tuple)):
mlt_idx = [rut.find_nearest(nbh, mlyr.ml_top[cnt])
for cnt, nbh in
enumerate(rad_georef['beam_height [km]'])]
if isinstance(mlyr.ml_bottom, (int, float)):
mlb_idx = [rut.find_nearest(nbh, mlyr.ml_bottom)
for nbh in rad_georef['beam_height [km]']]
elif isinstance(mlyr.ml_bottom, (np.ndarray, list, tuple)):
mlb_idx = [rut.find_nearest(nbh, mlyr.ml_bottom[cnt])
for cnt, nbh in
enumerate(rad_georef['beam_height [km]'])]
mlt_idxx = np.array([rad_georef['grid_rectx'][cnt, ix]
for cnt, ix in enumerate(mlt_idx)])
mlt_idxy = np.array([rad_georef['grid_recty'][cnt, ix]
for cnt, ix in enumerate(mlt_idx)])
mlb_idxx = np.array([rad_georef['grid_rectx'][cnt, ix]
for cnt, ix in enumerate(mlb_idx)])
mlb_idxy = np.array([rad_georef['grid_recty'][cnt, ix]
for cnt, ix in enumerate(mlb_idx)])
if isinstance(rad_params['elev_ang [deg]'], str):
dtdes1 = rad_params['elev_ang [deg]']
else:
dtdes1 = f"{rad_params['elev_ang [deg]']:{2}.{2}f} deg"
if rad_params['datetime']:
dtdes2 = f"{rad_params['datetime']:%Y-%m-%d %H:%M:%S}"
else:
dtdes2 = ''
if fig_title is None:
ptitle = (f"{rad_params['site_name'].title()} "
+ f"[{dtdes1}] -- {dtdes2}")
else:
ptitle = fig_title
# txtboxs = 'round, rounding_size=0.5, pad=0.5'
# fc, ec = 'w', 'k'
if nrows is None and ncols is None:
if len(rad_vars) <= 3:
nrw = 1
ncl = int(len(rad_vars))
elif len(rad_vars) > 3 and len(rad_vars) < 10 and len(rad_vars) % 2:
ncl = int(np.ceil(len(rad_vars)/2))
nrw = int(np.ceil(len(rad_vars)/ncl))
elif len(rad_vars) >= 10 and len(rad_vars) % 2:
ncl = int(np.ceil(len(rad_vars)/4))
nrw = int(np.ceil(len(rad_vars)/ncl))
else:
ncl = int(np.ceil(len(rad_vars)/2))
nrw = int(np.ceil(len(rad_vars)/ncl))
elif nrows is not None and ncols is None:
if len(rad_vars) <= 3:
nrw = nrows
ncl = int(len(rad_vars))
else:
nrw = nrows
ncl = int(np.ceil(len(rad_vars)/nrw))
elif ncols is not None and nrows is None:
if len(rad_vars) <= 3:
nrw = 1
ncl = ncols
else:
ncl = ncols
nrw = int(np.ceil(len(rad_vars)/ncl))
else:
nrw = nrows
ncl = ncols
if nrw * ncl < len(rad_vars):
print('Warning: Due to the selected grid, some variables may not '
+ 'be displayed. Please adjust your settings to view all '
+ 'available variables.')
if fig_size is None and nrw != 1:
fig_size = (16, 9)
if fig_size is None and nrw == 1:
fig_size = (16, 4.5)
f, ax = plt.subplots(nrw, ncl, sharex=True, sharey=True, figsize=fig_size)
f.suptitle(f'{ptitle}', fontsize=16)
lpv, bnd, cmaph, cmapext, dnorm, v2p, normp, cbtks_fmt, tcks = pltparams(
None, rad_vars.keys(), vars_bounds, ucmap=ucmap, unorm=unorm,
cb_ext=cb_ext)
lpv_vars = [rkey[:rkey.find('[')-1] for rkey in lpv.keys()]
for a, (rkey, var2plot) in zip(ax.flatten(), rad_vars.items()):
rkey_units = rkey[rkey.find('['):]
rkey_var = rkey[:rkey.find('[')-1]
if rkey in lpv or rkey_var in lpv_vars or [rk for rk in lpv_vars
if rkey_var.startswith(rk)]:
lpv, bnd, cmaph, cmapext, dnorm, v2p, normp, cbtks_fmt, tcks = pltparams(
rkey, rad_vars.keys(), vars_bounds, ucmap=ucmap, unorm=unorm,
cb_ext=cb_ext)
else:
lpv, bnd, cmaph, cmapext, dnorm, v2p, normp, cbtks_fmt, tcks = pltparams(
rkey, rad_vars.keys(), vars_bounds, cb_ext=cb_ext)
b1 = np.linspace(np.nanmin(var2plot), np.nanmax(var2plot), 11)
normp = mpc.BoundaryNorm(
b1, mpl.colormaps['tpylsc_rad_pvars'].N,
extend=cmapext.get(rkey[rkey.find('['):], 'both'))
cmapp = cmaph.get(rkey[rkey.find('['):],
mpl.colormaps['tpylsc_rad_pvars'])
# if rkey.lower().startswith('z') and '[dBZ]' in rkey:
# normp = dnorm.get('[dBZ]')
# cmapp = mpl.colormaps['tpylsc_rad_ref']
# if rkey.lower().startswith('zdr') and '[dB]' in rkey:
# normp = dnorm.get('[dB]')
# cmapp = mpl.colormaps['tpylsc_rad_2slope']
# if '[0-1]' in rkey:
# normp = dnorm.get('[0-1]')
# if rkey == 'rhoHV [-]':
# norm = [mpc.BoundaryNorm(
# value, cmaph.get(key[key.find('['):],
# mpl.colormaps['tpylsc_rad_pvars']).N,
# extend='min')
# for key, value in bnd.items() if key == '[-]'][0]
# if rkey == 'PIA [dB]':
# cmapp = mpl.colormaps['tpylsc_useq_fiery']
f1 = a.pcolormesh(rad_georef['grid_rectx'], rad_georef['grid_recty'],
var2plot, shading='auto', cmap=cmapp, norm=normp)
if mlyr is not None:
a.plot(mlt_idxx, mlt_idxy, c='k', ls='-', alpha=3/4,
path_effects=[pe.Stroke(linewidth=5, foreground='w'),
pe.Normal()], label=r'$MLyr_{(T)}$')
a.plot(mlb_idxx, mlb_idxy, c='grey', ls='-', alpha=3/4,
path_effects=[pe.Stroke(linewidth=5, foreground='w'),
pe.Normal()], label=r'$MLyr_{(B)}$')
a.legend(loc='upper left')
if xlims is not None:
a.set_xlim(xlims)
if ylims is not None:
a.set_ylim(ylims)
# a.set_title(f'{dtdes}' "\n" f'{key}')
a.set_title(f'{rkey}', fontsize=12)
if nrw == 1:
a.set_xlabel('Distance from the radar [km]', fontsize=12)
elif ncl == 1:
a.set_ylabel('Distance from the radar [km]', fontsize=12)
else:
a.set_xlabel(None, size=12)
a.set_ylabel(None, size=12)
a.grid(True)
a.axes.set_aspect('equal')
a.tick_params(axis='both', which='major', labelsize=10)
if rkey.startswith('rhoHV'):
f.colorbar(f1, ax=a, ticks=tcks, format=f'%.{cbtks_fmt}f')
else:
f.colorbar(f1, ax=a)
if nrw*ncl > len(rad_vars):
for empax in range(nrw*ncl-len(rad_vars)):
f.delaxes(ax.flatten()[-empax-1])
if ax.ndim > 1:
plt.setp(ax[-1, :], xlabel='Distance from the radar [km]')
plt.setp(ax[:, 0], ylabel='Distance from the radar [km]')
if nrw == 1:
ax[0].set_ylabel('Distance from the radar [km]', fontsize=12)
elif ncl == 1:
ax[-1].set_xlabel('Distance from the radar [km]', fontsize=12)
# txtboxc = (1.025, -.10)
# txtboxc = (-3., -.10)
# a.annotate('| Created using Towerpy |', xy=txtboxc, fontsize=8,
# xycoords='axes fraction', va='center', ha='center',
# bbox=dict(boxstyle=txtboxs, fc=fc, ec=ec))
# figManager = plt.get_current_fig_manager()
# figManager.window.showMaximized()
plt.tight_layout()
# plt.show()
[docs]
def plot_mgrid(rscans_georef, rscans_params, rscans_vars, var2plot=None,
vars_bounds=None, ucmap=None, unorm=None, cb_ext=None,
cpy_feats=None, proj_suffix='osgb', data_proj=None,
xlims=None, ylims=None, ncols=None, nrows=None, fig_size=None):
"""
Graph multiple PPI scans into a grid.
Parameters
----------
rscans_georef : list
List of georeferenced data containing descriptors of the azimuth, gates
and beam height, amongst others, corresponding to each PPI scan.
rscans_params : list
List of radar technical details corresponding to each PPI scan.
rscans_vars : list
List of Dicts containing radar variables to plot corresponding to each
PPI scan.
var2plot : str, optional
Key of the radar variable to plot. The default is None. This option
will plot ZH or the 'first' element in the rad_vars dict.
vars_bounds : dict containing key and 3-element tuple or list, optional
Boundaries [min, max, nvals] between which radar variables are
to be mapped. The default are:
{'ZH [dBZ]': [-10, 60, 15],
'ZDR [dB]': [-2, 6, 17],
'PhiDP [deg]': [0, 180, 10], 'KDP [deg/km]': [-2, 6, 17],
'rhoHV [-]': [0.3, .9, 1],
'V [m/s]': [-5, 5, 11], 'gradV [dV/dh]': [-1, 0, 11],
'LDR [dB]': [-35, 0, 11],
'Rainfall [mm/h]': [0.1, 64, 11]}
ucmap : colormap, optional
User-defined colormap, either a matplotlib.colors.ListedColormap,
or string from matplotlib.colormaps.
unorm : dict containing matplotlib.colors normalisation objects, optional
User-defined normalisation methods to map colormaps onto radar data.
The default is None.
cb_ext : dict containing key and str, optional
The str modifies the end(s) for out-of-range values for a
given key (radar variable). The str has to be one of 'neither',
'both', 'min' or 'max'.
coord_sys : 'rect' or 'polar', optional
Coordinates system (polar or rectangular). The default is 'polar'.
cpy_feats : dict, optional
Cartopy attributes to add to the map. The default are:
{
'status': False,
'add_land': False,
'add_ocean': False,
'add_coastline': False,
'add_borders': False,
'add_countries': True,
'add_provinces': True,
'borders_ls': ':',
'add_lakes': False,
'lakes_transparency': 0.5,
'add_rivers': False,
'tiles': False,
'tiles_source': None,
'tiles_style': None,
}
proj_suffix : str
Suffix of the georeferenced grids used to display the data.
The X/Y grids must exist in the rad_georef dictionary, e.g.
'grid_osgbx--grid_osgby', 'grid_utmx--grid_utmy',
'grid_wgs84x--grid_wgs84y', etc. The default is 'osgb'.
data_proj : Cartopy Coordinate Reference System object, optional
Cartopy projection used to plot the data in a map e.g.,
ccrs.OSGB(approx=False).
xlims : 2-element tuple or list, optional
Set the x-axis view limits [min, max]. The default is None.
ylims : 2-element tuple or list, optional
Set the y-axis view limits [min, max]. The default is None.
ncols : int, optional
Set the number of columns used to build the grid. The default is None.
nrows : int, optional
Set the number of rows used to build the grid. The default is None.
fig_size : 2-element tuple or list, optional
Modify the default plot size.
"""
from mpl_toolkits.axes_grid1 import ImageGrid
from cartopy.mpl.geoaxes import GeoAxes
dskeys = [k for i in rscans_vars for k in i.keys()]
lpv, bnd, cmaph, cmapext, dnorm, v2p, normp, cbtks_fmt, tcks = pltparams(
var2plot,
('ZH [dBZ]' if all('ZH [dBZ]' in i.keys() for i in rscans_vars)
else list(set([x for x in dskeys
if dskeys.count(x) >= len(rscans_vars)
and '[' in x]))),
vars_bounds, ucmap=ucmap, unorm=unorm, cb_ext=cb_ext)
if var2plot is None:
var2plot = v2p
# txtboxs = 'round, rounding_size=0.5, pad=0.5'
# txtboxc = (0, -.09)
# fc, ec = 'w', 'k'
cmapp = cmaph.get(var2plot[var2plot.find('['):],
mpl.colormaps['tpylsc_rad_pvars'])
cpy_features = {'status': False,
# 'coastresolution': '10m',
'add_land': False,
'add_ocean': False,
'add_coastline': False,
'add_borders': False,
'add_countries': True,
'add_provinces': True,
'borders_ls': ':',
'add_lakes': False,
'lakes_transparency': 0.5,
'add_rivers': False,
'tiles': False,
'tiles_source': None,
'tiles_style': None,
'tiles_res': 8, 'alpha_tiles': 0.5, 'alpha_rad': 1
}
if cpy_feats:
cpy_features.update(cpy_feats)
if cpy_features['status']:
states_provinces = cfeature.NaturalEarthFeature(
category='cultural',
name='admin_1_states_provinces_lines',
scale='10m',
facecolor='none')
countries = cfeature.NaturalEarthFeature(
category='cultural',
name='admin_0_countries',
scale='10m',
facecolor='none')
# TODO add fig_title
pttl = [f"{p['elev_ang [deg]']} -- "
+ f"{p['datetime']:%Y-%m-%d %H:%M:%S}"
if isinstance(p['elev_ang [deg]'], str)
else
f"{p['elev_ang [deg]']:{2}.{2}f} deg. -- "
+ f"{p['datetime']:%Y-%m-%d %H:%M:%S}"
for p in rscans_params]
if nrows is None and ncols is None:
if len(rscans_vars) <= 3:
nrw = 1
ncl = int(len(rscans_vars))
elif len(rscans_vars) > 3 and len(rscans_vars) < 10 and len(rscans_vars) % 2:
ncl = int(np.ceil(len(rscans_vars)/2))
nrw = int(np.ceil(len(rscans_vars)/ncl))
elif len(rscans_vars) >= 10 and len(rscans_vars) % 2:
ncl = int(np.ceil(len(rscans_vars)/4))
nrw = int(np.ceil(len(rscans_vars)/ncl))
else:
ncl = int(np.ceil(len(rscans_vars)/2))
nrw = int(np.ceil(len(rscans_vars)/ncl))
elif nrows is not None and ncols is None:
if len(rscans_vars) <= 3:
nrw = nrows
ncl = int(len(rscans_vars))
else:
nrw = nrows
ncl = int(np.ceil(len(rscans_vars)/nrw))
elif ncols is not None and nrows is None:
if len(rscans_vars) <= 3:
nrw = 1
ncl = ncols
else:
ncl = ncols
nrw = int(np.ceil(len(rscans_vars)/ncl))
else:
nrw = nrows
ncl = ncols
if nrw * ncl < len(rscans_vars):
print('Warning: Due to the selected grid, some variables may not '
+ 'be displayed. Please adjust your settings to view all '
+ 'available variables.')
if cpy_features['status'] is False:
if fig_size is None:
fig_size = (15, 5)
fig = plt.figure(figsize=fig_size)
grgeor = [[i['grid_rectx'], i['grid_recty']] for i in rscans_georef]
grid2 = ImageGrid(fig, 111, nrows_ncols=(nrw, ncl), label_mode="L",
cbar_location="right", cbar_mode="single",
cbar_size="10%", cbar_pad=0.25, axes_pad=(0.5, 0.75),
share_all=True)
for ax, z, g, pr, pt in zip(grid2, [i[var2plot] for i in rscans_vars],
grgeor, rscans_params, pttl):
f1 = ax.pcolormesh(g[0], g[1], z, shading='auto', cmap=cmapp,
norm=normp)
ax.set_title(f"{pt} \n {pr['site_name']} - PPI {var2plot}",
fontsize=12)
ax.set_xlabel('Distance from the radar [km]', fontsize=12)
ax.set_ylabel('Distance from the radar [km]', fontsize=12)
ax.grid(True)
ax.axes.set_aspect('equal')
ax.tick_params(axis='both', which='major', labelsize=12)
ax.set_xlim(xlims)
ax.set_ylim(ylims)
if (var2plot == 'rhoHV [-]' or '[mm]' in var2plot
or '[mm/h]' in var2plot):
ax.cax.colorbar(f1, ticks=tcks, format=f'%.{cbtks_fmt}f')
else:
ax.cax.colorbar(f1)
ax.cax.tick_params(direction='in', which='both', labelsize=12)
ax.cax.set_title(var2plot[var2plot .find('['):], fontsize=12)
# for ax, im_title in zip(grid2, ["(a)", "(b)", "(c)"]):
# t = add_inner_title(ax, im_title, loc='upper left')
# t.patch.set_ec("none")
# t.patch.set_alpha(0.5)
if nrw*ncl > len(rscans_vars):
for empax in range(nrw*ncl-len(rscans_vars)):
grid2[-empax-1].remove()
plt.tight_layout()
plt.show()
elif cpy_features['status']:
if fig_size is None:
fig_size = (16, 6)
fig = plt.figure(figsize=fig_size)
projection = ccrs.PlateCarree()
axes_class = (GeoAxes, dict(map_projection=projection))
grgeor = [[i[f'grid_{proj_suffix}x'], i[f'grid_{proj_suffix}y']]
for i in rscans_georef]
grid2 = ImageGrid(fig, 111, nrows_ncols=(nrw, ncl), axes_pad=(.6, .9),
label_mode="L", cbar_mode="single", cbar_size="10%",
cbar_pad=0.75, cbar_location="right", share_all=True,
axes_class=axes_class)
if data_proj:
proj2 = data_proj
else:
raise TowerpyError('User must specify the projected coordinate'
' system of the radar data e.g.'
' ccrs.OSGB(approx=False) or ccrs.UTM(zone=32)')
for ax1, z, g, pr, pt in zip(grid2, [i[var2plot] for i in rscans_vars],
grgeor, rscans_params, pttl):
ax1.set_title(f"{pt} \n {pr['site_name']} - PPI {var2plot}",
fontsize=12)
if xlims and ylims:
extx = xlims
exty = ylims
ax1.set_extent(extx+exty, crs=projection)
if cpy_features['tiles']:
if (cpy_features['tiles_source'] is None
or cpy_features['tiles_source'] == 'OSM'):
imtiles = cimgt.OSM()
ax1.add_image(imtiles, cpy_features['tiles_res'],
interpolation='spline36',
alpha=cpy_features['alpha_tiles'])
elif cpy_features['tiles_source'] == 'GoogleTiles':
if cpy_features['tiles_style'] is None:
imtiles = cimgt.GoogleTiles(style='street')
ax1.add_image(imtiles, cpy_features['tiles_res'],
interpolation='spline36',
alpha=cpy_features['alpha_tiles'])
else:
imtiles = cimgt.GoogleTiles(
style=cpy_features['tiles_style'])
ax1.add_image(imtiles, cpy_features['tiles_res'],
# interpolation='spline36',
alpha=cpy_features['alpha_tiles'])
elif cpy_features['tiles_source'] == 'Stamen':
if cpy_features['tiles_style'] is None:
imtiles = cimgt.Stamen(style='toner')
ax1.add_image(imtiles, cpy_features['tiles_res'],
interpolation='spline36',
alpha=cpy_features['alpha_tiles'])
else:
imtiles = cimgt.Stamen(
style=cpy_features['tiles_style'])
ax1.add_image(imtiles, cpy_features['tiles_res'],
interpolation='spline36',
alpha=cpy_features['alpha_tiles'])
if cpy_features['add_land']:
ax1.add_feature(cfeature.LAND)
if cpy_features['add_ocean']:
ax1.add_feature(cfeature.OCEAN)
if cpy_features['add_coastline']:
ax1.add_feature(cfeature.COASTLINE)
if cpy_features['add_borders']:
ax1.add_feature(cfeature.BORDERS,
linestyle=cpy_features['borders_ls'])
if cpy_features['add_lakes']:
ax1.add_feature(cfeature.LAKES,
alpha=cpy_features['lakes_transparency'])
if cpy_features['add_rivers']:
ax1.add_feature(cfeature.RIVERS)
if cpy_features['add_countries']:
ax1.add_feature(states_provinces, edgecolor='black', ls=":")
if cpy_features['add_provinces']:
ax1.add_feature(countries, edgecolor='black')
data_source = 'Natural Earth'
data_license = 'public domain'
# Add a text annotation for the license information to the
# the bottom right corner.
# text = AnchoredText(r'$\copyright$ {}; license: {}'
# ''.format(SOURCE, LICENSE),
# loc=4, prop={'size': 12}, frameon=True)
# ax1.add_artist(text)
print('\N{COPYRIGHT SIGN}'
+ f'{data_source}; license: {data_license}')
if cpy_features['tiles_source'] == 'Stamen':
print('\N{COPYRIGHT SIGN}' + 'Map tiles by Stamen Design, '
+ 'under CC BY 3.0. Data by OpenStreetMap, under ODbL.')
gl = ax1.gridlines(draw_labels=True, dms=False,
x_inline=False, y_inline=False)
gl.xlabel_style = {'size': 11}
gl.ylabel_style = {'size': 11}
gl.top_labels = False
gl.right_labels = False
# ax1.set_title(f'{ptitle} \n' + f'PPI {var2plot}', fontsize=14)
# lon_formatter = LongitudeFormatter(number_format='.4f',
# degree_symbol='',
# dateline_direction_label=True)
# lat_formatter = LatitudeFormatter(number_format='.0f',
# degree_symbol=''
# )
# ax1.xaxis.set_major_formatter(lon_formatter)
# ax1.yaxis.set_major_formatter(lat_formatter)
# plotunits = [i[i.find('['):]
# for i in rad_vars.keys() if var2plot == i][0]
mappable = ax1.pcolormesh(g[0], g[1], z, transform=proj2,
shading='auto', cmap=cmapp, norm=normp,
alpha=cpy_features['alpha_rad'])
if (var2plot == 'rhoHV [-]' or '[mm]' in var2plot
or '[mm/h]' in var2plot):
# ticks = bnd.get(var2plot[var2plot.find('['):])
if len(tcks) > 20:
tcks = tcks[::5]
grid2.cbar_axes[0].colorbar(mappable, ticks=tcks,
format=f'%.{cbtks_fmt}f')
else:
grid2.cbar_axes[0].colorbar(mappable)
ax1.cax.set_title(var2plot[var2plot .find('['):], fontsize=12)
# ax1.axes.set_aspect('equal')
if nrw*ncl > len(rscans_vars):
for empax in range(nrw*ncl-len(rscans_vars)):
grid2[-empax-1].remove()
plt.tight_layout()
plt.show()
[docs]
def plot_cone_coverage(rad_georef, rad_params, rad_vars, var2plot=None,
vars_bounds=None, xlims=None, ylims=None, zlims=[0, 8],
limh=8, ucmap=None, unorm=None, cbticks=None,
cb_ext=None, fig_size=None):
"""
Display a 3-D representation of the radar cone coverage.
Parameters
----------
rad_georef : dict
Georeferenced data containing descriptors of the azimuth, gates
and beam height, amongst others.
rad_params : dict
Radar technical details.
rad_vars : dict
Dict containing radar variables to plot.
var2plot : str, optional
Key of the radar variable to plot. The default is None. This option
will plot ZH or the 'first' element in the rad_vars dict.
vars_bounds : dict containing key and 3-element tuple or list, optional
Boundaries [min, max, nvals] between which radar variables are
to be mapped.
xlims : 2-element tuple or list, optional
Set the x-axis view limits [min, max]. The default is None.
ylims : 2-element tuple or list, optional
Set the y-axis view limits [min, max]. The default is None.
zlims : 2-element tuple or list, optional
Set the z-axis view limits [min, max]. The default is None.
limh : int or float, optional
Set a height limit to the plot. The default is None.
ucmap : colormap, optional
User-defined colormap, either a matplotlib.colors.ListedColormap,
or string from matplotlib.colormaps.
unorm : dict containing matplotlib.colors normalisation objects, optional
User-defined normalisation methods to map colormaps onto radar data.
The default is None.
cbticks : dict, optional
Modifies the default ticks' location (dict values) and labels
(dict keys) in the colour bar.
cb_ext : dict containing key and str, optional
The str modifies the end(s) for out-of-range values for a
given key (radar variable). The str has to be one of 'neither',
'both', 'min' or 'max'.
fig_size : 2-element tuple or list, optional
Modify the default plot size.
"""
from matplotlib.colors import LightSource
lpv, bnd, cmaph, cmapext, dnorm, v2p, normp, cbtks_fmt, tcks = pltparams(
var2plot, rad_vars.keys(), vars_bounds, ucmap=ucmap, unorm=unorm,
cb_ext=cb_ext)
#
if var2plot is None:
var2plot = v2p
#
cmapp = cmaph.get(var2plot[var2plot.find('['):],
mpl.colormaps['tpylsc_rad_pvars'])
# dtdes0 = f"[{rad_params['site_name']}]"
if isinstance(rad_params['elev_ang [deg]'], str):
dtdes1 = f"{rad_params['elev_ang [deg]']} -- "
else:
dtdes1 = f"{rad_params['elev_ang [deg]']:{2}.{2}} deg. -- "
if rad_params['datetime']:
dtdes2 = f"{rad_params['datetime']:%Y-%m-%d %H:%M:%S}"
else:
dtdes2 = ''
ptitle = dtdes1 + dtdes2
# txtboxs = 'round, rounding_size=0.5, pad=0.5'
# txtboxc = (0, -.09)
# fc, ec = 'w', 'k'
limidx = [rut.find_nearest(row, limh)
for row in rad_georef['beam_height [km]']]
m = np.ma.masked_invalid(rad_vars[var2plot]).mask
for n, rows in enumerate(m):
rows[limidx[n]:] = 1
R = rad_vars[var2plot]
X, Y = rad_georef['grid_rectx'], rad_georef['grid_recty']
Z = np.resize(rad_georef['beam_height [km]'], R.shape)
Z = np.resize(rad_georef['beam_height [km]'], R.shape)
X = np.ma.array(X, mask=m)
Y = np.ma.array(Y, mask=m)
Z = np.ma.array(Z, mask=m)
R = np.ma.array(R, mask=m)
ls = LightSource(0, 0)
rgb = ls.shade(R, cmap=cmapp, norm=normp, vert_exag=0.1, blend_mode='soft')
if fig_size is None:
fig_size = (12, 8)
fig, ax = plt.subplots(subplot_kw={"projection": "3d"}, figsize=fig_size)
# Plot the surface.
ax.plot_surface(X, Y, Z, cmap=cmapp, norm=normp, facecolors=rgb,
rstride=1, cstride=8,
# rcount=360, ccount=600,
# rcount=360, ccount=150,
linewidth=0, antialiased=True, shade=False,)
if cbticks is not None:
mappable2 = ax.contourf(X, Y, R, zdir='z', offset=0, levels=tcks,
cmap=cmapp, norm=normp, antialiased=True)
else:
mappable2 = ax.contourf(X, Y, R, zdir='z', offset=0,
levels=normp.boundaries,
cmap=cmapp, norm=normp, extend=normp.extend,
antialiased=True)
# Customize the axis.
ax.set_xlim(xlims)
ax.set_ylim(ylims)
ax.set_zlim(zlims)
ax.view_init(elev=10)
ax.tick_params(axis='both', labelsize=14)
# ax.zaxis.set_major_locator(LinearLocator(10))
# ax.zaxis.set_major_formatter('{x:.02f}')
# Add a color bar which maps values to colors.
if cbticks is not None:
cb1 = fig.colorbar(mappable2, shrink=0.4, aspect=5, norm=normp,
cmap=cmapp, ticks=list(cbticks.values()),
format=mticker.FixedFormatter(list(cbticks.keys())))
else:
if (var2plot == 'rhoHV [-]' or '[mm]' in var2plot
or '[mm/h]' in var2plot):
cb1 = fig.colorbar(
mappable2, shrink=0.4, aspect=5, norm=normp, cmap=cmapp,
ticks=tcks, format=f'%.{cbtks_fmt}f')
else:
cb1 = fig.colorbar(
mappable2, shrink=0.4, aspect=5, norm=normp, cmap=cmapp)
cb1.ax.tick_params(direction='in', axis='both', labelsize=14)
cb1.ax.set_title(var2plot[var2plot .find('['):], fontsize=14)
ax.set_title(f'{ptitle} \n' + f'PPI {var2plot}', fontsize=16)
ax.set_xlabel('Distance from the radar [km]', fontsize=14, labelpad=15)
ax.set_ylabel('Distance from the radar [km]', fontsize=14, labelpad=15)
ax.set_zlabel('Height [km]', fontsize=14, labelpad=15)
plt.tight_layout()
plt.show()
[docs]
def plot_snr(rad_georef, rad_params, snr_data, min_snr, coord_sys='polar',
ucmap_snr=None, fig_size=None):
"""
Display the results of the SNR classification.
Parameters
----------
rad_georef : dict
Georeferenced data containing descriptors of the azimuth, gates
and beam height, amongst others.
rad_params : dict
Radar technical details.
snr_data : dict
Results of the SNR_Classif method.
proj : 'rect' or 'polar', optional
Coordinates system (polar or rectangular). The default is 'polar'.
"""
if isinstance(rad_params['elev_ang [deg]'], str):
dtdes1 = f"{rad_params['elev_ang [deg]']} -- "
else:
dtdes1 = f"{rad_params['elev_ang [deg]']:{2}.{2}} deg. -- "
if rad_params['datetime']:
dtdes2 = f"{rad_params['datetime']:%Y-%m-%d %H:%M:%S}"
else:
dtdes2 = ''
ptitle = dtdes1 + dtdes2
if fig_size is None:
fig_size = (10.5, 6.5)
if coord_sys == 'polar':
fig, (ax2, ax3) = plt.subplots(1, 2, figsize=fig_size,
subplot_kw=dict(projection='polar'))
f2 = ax2.pcolormesh(
*np.meshgrid(rad_georef['azim [rad]'],
rad_georef['range [m]'] / 1000, indexing='ij'),
np.flipud(snr_data['snr [dB]']), shading='auto',
cmap='tpylsc_rad_ref')
# ax2.axes.set_aspect('equal')
ax2.grid(color='gray', linestyle=':')
ax2.set_theta_zero_location('N')
ax2.set_thetagrids(np.arange(0, 360, 90))
# ax2.set_yticklabels([])
cb2 = plt.colorbar(f2, ax=ax2, extend='both', orientation='horizontal',
# shrink=0.5,
)
cb2.ax.tick_params(direction='in', axis='both', labelsize=14)
cb2.ax.set_title('SNR [dB]', fontsize=14, y=-2.5)
ax3.set_title(f'Signal (SNR > minSNR [{min_snr:.2f}])', fontsize=14,
y=-0.15)
ax3.pcolormesh(
*np.meshgrid(rad_georef['azim [rad]'],
rad_georef['range [m]'] / 1000, indexing='ij'),
np.flipud(snr_data['snrclass']), shading='auto',
cmap=mpl.colormaps['tpylc_div_yw_gy_bu'])
# ax3.axes.set_aspect('equal')
ax3.grid(color='w', linestyle=':')
ax3.set_theta_zero_location('N')
ax3.set_thetagrids(np.arange(0, 360, 90))
# ax3.set_yticklabels([])
mpl.colormaps['tpylc_div_yw_gy_bu'].set_bad(color='#505050')
plt.show()
elif coord_sys == 'rect':
fig, (ax2, ax3) = plt.subplots(1, 2, figsize=fig_size,
sharex=True, sharey=True)
fig.suptitle(f'{ptitle}', fontsize=16)
# Plots the SNR
f2 = ax2.pcolormesh(rad_georef['grid_rectx'], rad_georef['grid_recty'],
snr_data['snr [dB]'], shading='auto',
cmap='tpylsc_rad_ref')
ax2_divider = make_axes_locatable(ax2)
cax2 = ax2_divider.append_axes("top", size="7%", pad="2%")
cb2 = fig.colorbar(f2, cax=cax2, extend='max',
orientation='horizontal')
cb2.ax.tick_params(direction='in', labelsize=10)
# cb2.ax.set_xticklabels(cb2.ax.get_xticklabels(), rotation=90)
cb2.ax.set_title('SNR [dB]', fontsize=14)
# cb2.ax.set_ylabel('[dB]', fontsize=12, labelpad=0)
cax2.xaxis.set_ticks_position("top")
ax2.tick_params(axis='both', which='major', labelsize=10)
ax2.set_ylabel('Distance from the radar [km]', fontsize=12,
labelpad=10)
ax2.set_xlabel('Distance from the radar [km]', fontsize=12,
labelpad=10)
ax2.axes.set_aspect('equal')
# Plots the Signal detection
f3 = ax3.pcolormesh(rad_georef['grid_rectx'], rad_georef['grid_recty'],
snr_data['snrclass'],
cmap=mpl.colormaps['tpylc_div_yw_gy_bu'],
vmin=0, vmax=6,
)
ax3_divider = make_axes_locatable(ax3)
cax3 = ax3_divider.append_axes("top", size="7%", pad="2%")
cb3 = fig.colorbar(f3, cax=cax3, orientation='horizontal')
cb3.ax.tick_params(direction='in', labelsize=10)
cb3.ax.set_title(f'Signal detection - SNR >= minSNR [{min_snr:.2f}]',
fontsize=14)
cax3.xaxis.set_ticks_position("top")
cb3.set_ticks(ticks=[1., 3., 6.],
labels=['Signal', 'Noise', ''])
ax3.set_xlabel('Distance from the radar [km]', fontsize=12,
labelpad=10)
ax3.tick_params(axis='both', which='major', labelsize=10)
ax3.axes.set_aspect('equal')
plt.tight_layout()
plt.show()
[docs]
def plot_nmeclassif(rad_georef, rad_params, nme_classif, echoesID,
clutter_map=None, xlims=None, ylims=None, fig_size=None):
"""
Plot a set of PPIs of polarimetric variables.
Parameters
----------
rad_georef : dict
Georeferenced data containing descriptors of the azimuth, gates
and beam height, amongst others.
rad_params : dict
Radar technical details.
nme_classif : dict
Results of the NME_ID method.
clutter_map : array, optional
Clutter map used for the NME_ID method. The default is None.
xlims : 2-element tuple or list, optional
Set the x-axis view limits [min, max]. The default is None.
ylims : 2-element tuple or list, optional
Set the y-axis view limits [min, max]. The default is None.
"""
# txtboxs = 'round, rounding_size=0.5, pad=0.5'
# fc, ec = 'w', 'k'
# =========================================================================
# Plot the Clutter classification
# =========================================================================
if fig_size is None:
fig_size = (6, 6.15)
clcdummy = nme_classif[nme_classif == echoesID['clutter']]
if not clcdummy.size:
nme_classif[0, 0] = echoesID['clutter']
plot_ppi(rad_georef, rad_params, {'classif [EC]': nme_classif},
cbticks=echoesID, ucmap='tpylc_div_yw_gy_bu')
plt.tight_layout()
# # txtboxc = (0, -.09)
# # ax.annotate('| Created using Towerpy |', xy=txtboxc, fontsize=8,
# # xycoords='axes fraction', va='center', ha='center',
# # bbox=dict(boxstyle=txtboxs, fc=fc, ec=ec))
# =========================================================================
# Plot the Clutter Map
# =========================================================================
if clutter_map is not None:
norm = mpc.BoundaryNorm(boundaries=np.linspace(0, 100, 11),
ncolors=256)
plot_ppi(rad_georef, rad_params,
{'Clutter probability [%]': clutter_map*100},
unorm={'Clutter probability [%]': norm},
ucmap='tpylsc_useq_bupkyw')
# txtboxc = (0, -.09)
# ax.annotate('| Created using Towerpy |', xy=txtboxc, fontsize=8,
# xycoords='axes fraction', va='center', ha='center',
# bbox=dict(boxstyle=txtboxs, fc=fc, ec=ec))
plt.tight_layout()
plt.show()
[docs]
def plot_zhattcorr(rad_georef, rad_params, rad_vars_att, rad_vars_attcorr,
vars_bounds=None, mlyr=None, xlims=None, ylims=None,
fig_size1=None, fig_size2=None):
"""
Plot the results of the ZH attenuation correction method.
Parameters
----------
rad_georef : dict
Georeferenced data containing descriptors of the azimuth, gates
and beam height, amongst others.
rad_params : dict
Radar technical details.
rad_vars_att : dict
Radar variables not corrected for attenuation.
rad_vars_attcorr : dict
Results of the AttenuationCorection method.
vars_bounds : dict containing key and 3-element tuple or list, optional
Boundaries [min, max, nvals] between which radar variables are
to be mapped. The default are:
{'ZH [dBZ]': [-10, 60, 15],
'ZDR [dB]': [-2, 6, 17],
'PhiDP [deg]': [0, 180, 10], 'KDP [deg/km]': [-2, 6, 17],
'rhoHV [-]': [0.3, .9, 1],
'V [m/s]': [-5, 5, 11], 'gradV [dV/dh]': [-1, 0, 11],
'LDR [dB]': [-35, 0, 11],
'Rainfall [mm/h]': [0.1, 64, 11]}
mlyr : MeltingLayer Class, optional
Plot the melting layer height. ml_top (float, int, list or np.array)
and ml_bottom (float, int, list or np.array) must be explicitly
defined. The default is None.
xlims : 2-element tuple or list, optional
Set the x-axis view limits [min, max]. The default is None.
ylims : 2-element tuple or list, optional
Set the y-axis view limits [min, max]. The default is None.
"""
lpv = {'ZH [dBZ]': [-10, 60, 15], 'PhiDP [deg]': [0, 180, 10],
'KDP [deg/km]': [-2, 6, 17], 'AH [dB/km]': [0, .1, 11],
'alpha [-]': [0, 0.2, 11]}
if vars_bounds is not None:
lpv.update(vars_bounds)
# =============================================================================
bnd = {key[key.find('['):]: np.linspace(value[0], value[1], value[2])
if 'rhoHV' not in key
else np.hstack((np.linspace(value[0], value[1], 4)[:-1],
np.linspace(value[1], value[2], 11)))
for key, value in lpv.items()}
# =============================================================================
dnorm = {key: mpc.BoundaryNorm(
value, mpl.colormaps['tpylsc_rad_pvars'].N, extend='both')
for key, value in bnd.items()}
if '[dBZ]' in bnd.keys():
dnorm['[dBZ]'] = mpc.BoundaryNorm(
bnd['[dBZ]'], mpl.colormaps['tpylsc_rad_ref'].N, extend='both')
if '[dB]' in bnd.keys():
dnorm['[dB]'] = mpc.BoundaryNorm(
bnd['[dB]'], mpl.colormaps['tpylsc_rad_2slope'].N, extend='both')
if '[dB/km]' in bnd.keys():
dnorm['[dB/km]'] = mpc.BoundaryNorm(
bnd['[dB/km]'], mpl.colormaps['tpylsc_rad_pvars'].N,
extend='max')
if '[-]' in bnd.keys():
dnorm['[-]'] = mpc.BoundaryNorm(
bnd['[-]'], mpl.colormaps['tpylsc_useq_fiery'].N,
extend='neither')
# =============================================================================
if mlyr is not None:
if isinstance(mlyr.ml_top, (int, float)):
mlt_idx = [rut.find_nearest(nbh, mlyr.ml_top)
for nbh in rad_georef['beam_height [km]']]
elif isinstance(mlyr.ml_top, (np.ndarray, list, tuple)):
mlt_idx = [rut.find_nearest(nbh, mlyr.ml_top[cnt])
for cnt, nbh in
enumerate(rad_georef['beam_height [km]'])]
if isinstance(mlyr.ml_bottom, (int, float)):
mlb_idx = [rut.find_nearest(nbh, mlyr.ml_bottom)
for nbh in rad_georef['beam_height [km]']]
elif isinstance(mlyr.ml_bottom, (np.ndarray, list, tuple)):
mlb_idx = [rut.find_nearest(nbh, mlyr.ml_bottom[cnt])
for cnt, nbh in
enumerate(rad_georef['beam_height [km]'])]
mlt_idxx = np.array([rad_georef['grid_rectx'][cnt, ix]
for cnt, ix in enumerate(mlt_idx)])
mlt_idxy = np.array([rad_georef['grid_recty'][cnt, ix]
for cnt, ix in enumerate(mlt_idx)])
mlb_idxx = np.array([rad_georef['grid_rectx'][cnt, ix]
for cnt, ix in enumerate(mlb_idx)])
mlb_idxy = np.array([rad_georef['grid_recty'][cnt, ix]
for cnt, ix in enumerate(mlb_idx)])
if isinstance(rad_params['elev_ang [deg]'], str):
dtdes1 = f"{rad_params['elev_ang [deg]']} -- "
else:
dtdes1 = f"{rad_params['elev_ang [deg]']:{2}.{2}} deg. -- "
if rad_params['datetime']:
dtdes2 = f"{rad_params['datetime']:%Y-%m-%d %H:%M:%S}"
else:
dtdes2 = ''
ptitle = dtdes1 + dtdes2
# =========================================================================
# Creates plots for ZH attenuation correction results.
# =========================================================================
mosaic = 'ABC'
if fig_size1 is None:
fig_size1 = (16, 5)
if fig_size2 is None:
fig_size2 = (6, 5)
fig_mos1 = plt.figure(figsize=fig_size1, constrained_layout=True)
ax_idx = fig_mos1.subplot_mosaic(mosaic, sharex=True, sharey=True)
for key, value in rad_vars_att.items():
if '[dBZ]' in key:
cmap = mpl.colormaps['tpylsc_rad_ref']
# norm = dnorm.get('n'+key)
norm = dnorm.get(key[key.find('['):])
fzhna = ax_idx['A'].pcolormesh(rad_georef['grid_rectx'],
rad_georef['grid_recty'], value,
shading='auto', cmap=cmap,
norm=norm)
ax_idx['A'].set_title(f"{ptitle}" "\n" f'Uncorrected {key}')
if mlyr is not None:
ax_idx['A'].plot(mlt_idxx, mlt_idxy, c='k', ls='-', alpha=3/4,
path_effects=[pe.Stroke(linewidth=5, foreground='w'),
pe.Normal()], label=r'$MLyr_{(T)}$')
ax_idx['A'].plot(mlb_idxx, mlb_idxy, c='grey', ls='-', alpha=3/4,
path_effects=[pe.Stroke(linewidth=5, foreground='w'),
pe.Normal()], label=r'$MLyr_{(B)}$')
ax_idx['A'].legend(loc='upper left')
if xlims is not None:
ax_idx['A'].set_xlim(xlims)
if ylims is not None:
ax_idx['A'].set_ylim(ylims)
plt.colorbar(fzhna, ax=ax_idx['A']).ax.tick_params(labelsize=10)
ax_idx['A'].grid(True)
ax_idx['A'].axes.set_aspect('equal')
ax_idx['A'].tick_params(axis='both', labelsize=10)
for key, value in rad_vars_attcorr.items():
if '[dBZ]' in key:
cmap = mpl.colormaps['tpylsc_rad_ref']
norm = dnorm.get(key[key.find('['):])
fzhna = ax_idx['B'].pcolormesh(rad_georef['grid_rectx'],
rad_georef['grid_recty'], value,
shading='auto', cmap=cmap,
norm=norm)
ax_idx['B'].set_title(f"{ptitle}" "\n" f'Corrected {key}')
if mlyr is not None:
ax_idx['B'].plot(mlt_idxx, mlt_idxy, c='k', ls='-', alpha=3/4,
path_effects=[pe.Stroke(linewidth=5, foreground='w'),
pe.Normal()], label=r'$MLyr_{(T)}$')
ax_idx['B'].plot(mlb_idxx, mlb_idxy, c='grey', ls='-', alpha=3/4,
path_effects=[pe.Stroke(linewidth=5, foreground='w'),
pe.Normal()], label=r'$MLyr_{(B)}$')
ax_idx['B'].legend(loc='upper left')
if xlims is not None:
ax_idx['B'].set_xlim(xlims)
if ylims is not None:
ax_idx['B'].set_ylim(ylims)
plt.colorbar(fzhna, ax=ax_idx['B']).ax.tick_params(labelsize=10)
ax_idx['B'].grid(True)
ax_idx['B'].axes.set_aspect('equal')
ax_idx['B'].tick_params(axis='both', labelsize=10)
for key, value in rad_vars_attcorr.items():
if 'AH' in key:
cmap = mpl.colormaps['tpylsc_rad_pvars']
norm = dnorm.get(key[key.find('['):])
fzhna = ax_idx['C'].pcolormesh(rad_georef['grid_rectx'],
rad_georef['grid_recty'], value,
shading='auto', cmap=cmap,
norm=norm
)
ax_idx['C'].set_title(f"{ptitle}" "\n" f'{key}')
if mlyr is not None:
ax_idx['C'].plot(mlt_idxx, mlt_idxy, c='k', ls='-', alpha=3/4,
path_effects=[pe.Stroke(linewidth=5, foreground='w'),
pe.Normal()], label=r'$MLyr_{(T)}$')
ax_idx['C'].plot(mlb_idxx, mlb_idxy, c='grey', ls='-', alpha=3/4,
path_effects=[pe.Stroke(linewidth=5, foreground='w'),
pe.Normal()], label=r'$MLyr_{(B)}$')
ax_idx['C'].legend(loc='upper left')
plt.colorbar(fzhna, ax=ax_idx['C']).ax.tick_params(labelsize=10)
ax_idx['C'].grid(True)
ax_idx['C'].axes.set_aspect('equal')
ax_idx['C'].tick_params(axis='both', labelsize=10)
# =========================================================================
# Creates plots for PHIDP attenuation correction results.
# =========================================================================
fig_mos2 = plt.figure(figsize=fig_size1, constrained_layout=True)
ax_idx2 = fig_mos2.subplot_mosaic(mosaic, sharex=True, sharey=True)
for key, value in rad_vars_att.items():
if '[deg]' in key:
cmap = mpl.colormaps['tpylsc_rad_pvars']
norm = dnorm.get(key[key.find('['):])
fzhna = ax_idx2['A'].pcolormesh(rad_georef['grid_rectx'],
rad_georef['grid_recty'], value,
shading='auto', cmap=cmap,
norm=norm)
ax_idx2['A'].set_title(f"{ptitle}" "\n" f'Uncorrected {key}')
if mlyr is not None:
ax_idx2['A'].plot(mlt_idxx, mlt_idxy, c='k', ls='-', alpha=3/4,
path_effects=[pe.Stroke(linewidth=5, foreground='w'),
pe.Normal()], label=r'$MLyr_{(T)}$')
ax_idx2['A'].plot(mlb_idxx, mlb_idxy, c='grey', ls='-', alpha=3/4,
path_effects=[pe.Stroke(linewidth=5, foreground='w'),
pe.Normal()], label=r'$MLyr_{(B)}$')
ax_idx2['A'].legend(loc='upper left')
plt.colorbar(fzhna, ax=ax_idx2['A']).ax.tick_params(labelsize=10)
ax_idx2['A'].grid(True)
ax_idx2['A'].axes.set_aspect('equal')
ax_idx2['A'].tick_params(axis='both', labelsize=10)
for key, value in rad_vars_attcorr.items():
if key == 'PhiDP [deg]':
cmap = mpl.colormaps['tpylsc_rad_pvars']
norm = dnorm.get(key[key.find('['):])
fzhna = ax_idx2['B'].pcolormesh(rad_georef['grid_rectx'],
rad_georef['grid_recty'], value,
shading='auto', cmap=cmap,
norm=norm)
ax_idx2['B'].set_title(f"{ptitle}" "\n" f'Corrected {key}')
if mlyr is not None:
ax_idx2['B'].plot(mlt_idxx, mlt_idxy, c='k', ls='-', alpha=3/4,
path_effects=[pe.Stroke(linewidth=5, foreground='w'),
pe.Normal()], label=r'$MLyr_{(T)}$')
ax_idx2['B'].plot(mlb_idxx, mlb_idxy, c='grey', ls='-', alpha=3/4,
path_effects=[pe.Stroke(linewidth=5, foreground='w'),
pe.Normal()], label=r'$MLyr_{(B)}$')
ax_idx2['B'].legend(loc='upper left')
plt.colorbar(fzhna, ax=ax_idx2['B']).ax.tick_params(labelsize=10)
ax_idx2['B'].grid(True)
ax_idx2['B'].axes.set_aspect('equal')
ax_idx2['B'].tick_params(axis='both', labelsize=10)
for key, value in rad_vars_attcorr.items():
if key == 'PhiDP* [deg]':
cmap = mpl.colormaps['tpylsc_rad_pvars']
# norm = dnorm.get('n'+key.replace('*', ''))
norm = dnorm.get(key[key.find('['):])
fzhna = ax_idx2['C'].pcolormesh(rad_georef['grid_rectx'],
rad_georef['grid_recty'], value,
shading='auto', cmap=cmap,
norm=norm)
ax_idx2['C'].set_title(f"{ptitle}" "\n" f'{key}')
if mlyr is not None:
ax_idx2['C'].plot(mlt_idxx, mlt_idxy, c='k', ls='-', alpha=3/4,
path_effects=[pe.Stroke(linewidth=5, foreground='w'),
pe.Normal()], label=r'$MLyr_{(T)}$')
ax_idx2['C'].plot(mlb_idxx, mlb_idxy, c='grey', ls='-', alpha=3/4,
path_effects=[pe.Stroke(linewidth=5, foreground='w'),
pe.Normal()], label=r'$MLyr_{(B)}$')
ax_idx2['C'].legend(loc='upper left')
plt.colorbar(fzhna, ax=ax_idx2['C']).ax.tick_params(labelsize=10)
ax_idx2['C'].grid(True)
ax_idx2['C'].axes.set_aspect('equal')
ax_idx2['C'].tick_params(axis='both', labelsize=10)
# =========================================================================
# Creates plots for attenuation correction vars.
# =========================================================================
fig_mos3, ax_idx3 = plt.subplots(figsize=fig_size2)
for key, value in rad_vars_attcorr.items():
if key == 'alpha [-]':
# cmap = 'tpylsc_rad_pvars'
cmap = 'tpylsc_useq_fiery'
norm = dnorm.get(key[key.find('['):])
fzhna = ax_idx3.pcolormesh(rad_georef['grid_rectx'],
rad_georef['grid_recty'], value,
shading='auto', cmap=cmap,
norm=norm
)
ax_idx3.set_title(f"{ptitle}" "\n" f'{key}')
if mlyr is not None:
ax_idx3.plot(mlt_idxx, mlt_idxy, c='k', ls='-', alpha=3/4,
path_effects=[pe.Stroke(linewidth=5, foreground='w'),
pe.Normal()], label=r'$MLyr_{(T)}$')
ax_idx3.plot(mlb_idxx, mlb_idxy, c='grey', ls='-', alpha=3/4,
path_effects=[pe.Stroke(linewidth=5, foreground='w'),
pe.Normal()], label=r'$MLyr_{(B)}$')
ax_idx3.legend(loc='upper left')
plt.colorbar(fzhna, ax=ax_idx3).ax.tick_params(labelsize=10)
ax_idx3.grid(True)
ax_idx3.axes.set_aspect('equal')
ax_idx3.tick_params(axis='both', labelsize=10)
plt.tight_layout()
fig_mos4, ax_idx4 = plt.subplots(figsize=fig_size2)
for key, value in rad_vars_attcorr.items():
if 'PIA' in key:
# cmap = 'tpylsc_rad_pvars'
cmap = 'tpylsc_useq_fiery'
fzhna = ax_idx4.pcolormesh(rad_georef['grid_rectx'],
rad_georef['grid_recty'], value,
shading='auto', cmap=cmap,
# norm=norm
)
ax_idx4.set_title(f"{ptitle}" "\n" f'{key}')
if mlyr is not None:
ax_idx4.plot(mlt_idxx, mlt_idxy, c='k', ls='-', alpha=3/4,
path_effects=[pe.Stroke(linewidth=5, foreground='w'),
pe.Normal()], label=r'$MLyr_{(T)}$')
ax_idx4.plot(mlb_idxx, mlb_idxy, c='grey', ls='-', alpha=3/4,
path_effects=[pe.Stroke(linewidth=5, foreground='w'),
pe.Normal()], label=r'$MLyr_{(B)}$')
ax_idx4.legend(loc='upper left')
plt.colorbar(fzhna, ax=ax_idx4).ax.tick_params(labelsize=10)
ax_idx4.grid(True)
ax_idx4.axes.set_aspect('equal')
ax_idx4.tick_params(axis='both', labelsize=10)
plt.tight_layout()
[docs]
def plot_zdrattcorr(rad_georef, rad_params, rad_vars_att, rad_vars_attcorr,
vars_bounds=None, mlyr=None, xlims=None, ylims=None,
fig_size1=None, fig_size2=None):
"""
Plot the results of the ZDR attenuation correction method.
Parameters
----------
rad_georef : dict
Georeferenced data containing descriptors of the azimuth, gates
and beam height, amongst others.
rad_params : dict
Radar technical details.
rad_vars_att : dict
Radar variables not corrected for attenuation.
rad_vars_attcorr : dict
Results of the AttenuationCorection method.
vars_bounds : dict containing key and 3-element tuple or list, optional
Boundaries [min, max, nvals] between which radar variables are
to be mapped. The default are:
{'ZH [dBZ]': [-10, 60, 15],
'ZDR [dB]': [-2, 6, 17],
'PhiDP [deg]': [0, 180, 10], 'KDP [deg/km]': [-2, 6, 17],
'rhoHV [-]': [0.3, .9, 1],
'V [m/s]': [-5, 5, 11], 'gradV [dV/dh]': [-1, 0, 11],
'LDR [dB]': [-35, 0, 11],
'Rainfall [mm/h]': [0.1, 64, 11]}
mlyr : MeltingLayer Class, optional
Plot the melting layer height. ml_top (float, int, list or np.array)
and ml_bottom (float, int, list or np.array) must be explicitly
defined. The default is None.
xlims : 2-element tuple or list, optional
Set the x-axis view limits [min, max]. The default is None.
ylims : 2-element tuple or list, optional
Set the y-axis view limits [min, max]. The default is None.
"""
lpv = {'ZDR [dB]': [-2, 6, 17], 'ADP [dB/km]': [0, 2.5, 20],
'beta [-]': [0, 0.1, 11]}
if vars_bounds is not None:
lpv.update(vars_bounds)
# =============================================================================
bnd = {key[key.find('['):]: np.linspace(value[0], value[1], value[2])
if 'rhoHV' not in key
else np.hstack((np.linspace(value[0], value[1], 4)[:-1],
np.linspace(value[1], value[2], 11)))
for key, value in lpv.items()}
# =============================================================================
dnorm = {key: mpc.BoundaryNorm(
value, mpl.colormaps['tpylsc_rad_pvars'].N, extend='both')
for key, value in bnd.items()}
if '[dBZ]' in bnd.keys():
dnorm['[dBZ]'] = mpc.BoundaryNorm(
bnd['[dBZ]'], mpl.colormaps['tpylsc_rad_ref'].N, extend='both')
if '[dB]' in bnd.keys():
dnorm['[dB]'] = mpc.BoundaryNorm(
bnd['[dB]'], mpl.colormaps['tpylsc_rad_2slope'].N, extend='both')
if '[dB/km]' in bnd.keys():
dnorm['[dB/km]'] = mpc.BoundaryNorm(
bnd['[dB/km]'], mpl.colormaps['tpylsc_rad_pvars'].N,
extend='max')
if '[-]' in bnd.keys():
dnorm['[-]'] = mpc.BoundaryNorm(
bnd['[-]'], mpl.colormaps['tpylsc_useq_fiery'].N,
extend='max')
# =============================================================================
if mlyr is not None:
if isinstance(mlyr.ml_top, (int, float)):
mlt_idx = [rut.find_nearest(nbh, mlyr.ml_top)
for nbh in rad_georef['beam_height [km]']]
elif isinstance(mlyr.ml_top, (np.ndarray, list, tuple)):
mlt_idx = [rut.find_nearest(nbh, mlyr.ml_top[cnt])
for cnt, nbh in
enumerate(rad_georef['beam_height [km]'])]
if isinstance(mlyr.ml_bottom, (int, float)):
mlb_idx = [rut.find_nearest(nbh, mlyr.ml_bottom)
for nbh in rad_georef['beam_height [km]']]
elif isinstance(mlyr.ml_bottom, (np.ndarray, list, tuple)):
mlb_idx = [rut.find_nearest(nbh, mlyr.ml_bottom[cnt])
for cnt, nbh in
enumerate(rad_georef['beam_height [km]'])]
mlt_idxx = np.array([rad_georef['grid_rectx'][cnt, ix]
for cnt, ix in enumerate(mlt_idx)])
mlt_idxy = np.array([rad_georef['grid_recty'][cnt, ix]
for cnt, ix in enumerate(mlt_idx)])
mlb_idxx = np.array([rad_georef['grid_rectx'][cnt, ix]
for cnt, ix in enumerate(mlb_idx)])
mlb_idxy = np.array([rad_georef['grid_recty'][cnt, ix]
for cnt, ix in enumerate(mlb_idx)])
if isinstance(rad_params['elev_ang [deg]'], str):
dtdes1 = f"{rad_params['elev_ang [deg]']} -- "
else:
dtdes1 = f"{rad_params['elev_ang [deg]']:{2}.{2}} deg. -- "
if rad_params['datetime']:
dtdes2 = f"{rad_params['datetime']:%Y-%m-%d %H:%M:%S}"
else:
dtdes2 = ''
ptitle = dtdes1 + dtdes2
# =========================================================================
# Creates plots for ZDR attenuation correction results.
# =========================================================================
mosaic = 'DEF'
if fig_size1 is None:
fig_size1 = (16, 5)
if fig_size2 is None:
fig_size2 = (6, 5)
fig_mos1 = plt.figure(figsize=fig_size1, constrained_layout=True)
ax_idx = fig_mos1.subplot_mosaic(mosaic, sharex=True, sharey=True)
for key, value in rad_vars_att.items():
if '[dB]' in key:
cmap = mpl.colormaps['tpylsc_rad_2slope']
norm = dnorm.get(key[key.find('['):])
fzhna = ax_idx['D'].pcolormesh(rad_georef['grid_rectx'],
rad_georef['grid_recty'], value,
shading='auto', cmap=cmap,
norm=norm)
ax_idx['D'].set_title(f"{ptitle}" "\n" f'Uncorrected {key}')
if mlyr is not None:
ax_idx['D'].plot(mlt_idxx, mlt_idxy, c='k', ls='-', alpha=3/4,
path_effects=[pe.Stroke(linewidth=5, foreground='w'),
pe.Normal()], label=r'$MLyr_{(T)}$')
ax_idx['D'].plot(mlb_idxx, mlb_idxy, c='grey', ls='-', alpha=3/4,
path_effects=[pe.Stroke(linewidth=5, foreground='w'),
pe.Normal()], label=r'$MLyr_{(B)}$')
ax_idx['D'].legend(loc='upper left')
plt.colorbar(fzhna, ax=ax_idx['D']).ax.tick_params(labelsize=10)
ax_idx['D'].grid(True)
ax_idx['D'].axes.set_aspect('equal')
ax_idx['D'].tick_params(axis='both', labelsize=10)
for key, value in rad_vars_attcorr.items():
if '[dB]' in key:
cmap = mpl.colormaps['tpylsc_rad_2slope']
norm = dnorm.get(key[key.find('['):])
fzhna = ax_idx['E'].pcolormesh(rad_georef['grid_rectx'],
rad_georef['grid_recty'], value,
shading='auto', cmap=cmap,
norm=norm)
ax_idx['E'].set_title(f"{ptitle}" "\n" f'Corrected {key}')
if mlyr is not None:
ax_idx['E'].plot(mlt_idxx, mlt_idxy, c='k', ls='-', alpha=3/4,
path_effects=[pe.Stroke(linewidth=5, foreground='w'),
pe.Normal()], label=r'$MLyr_{(T)}$')
ax_idx['E'].plot(mlb_idxx, mlb_idxy, c='grey', ls='-', alpha=3/4,
path_effects=[pe.Stroke(linewidth=5, foreground='w'),
pe.Normal()], label=r'$MLyr_{(B)}$')
ax_idx['E'].legend(loc='upper left')
plt.colorbar(fzhna, ax=ax_idx['E']).ax.tick_params(labelsize=10)
ax_idx['E'].grid(True)
ax_idx['E'].axes.set_aspect('equal')
ax_idx['E'].tick_params(axis='both', labelsize=10)
for key, value in rad_vars_attcorr.items():
if 'ADP' in key:
cmap = mpl.colormaps['tpylsc_rad_pvars']
norm = dnorm.get(key[key.find('['):])
fzhna = ax_idx['F'].pcolormesh(rad_georef['grid_rectx'],
rad_georef['grid_recty'], value,
shading='auto', cmap=cmap,
norm=norm)
ax_idx['F'].set_title(f"{ptitle}" "\n" f'{key}')
if mlyr is not None:
ax_idx['F'].plot(mlt_idxx, mlt_idxy, c='k', ls='-', alpha=3/4,
path_effects=[pe.Stroke(linewidth=5, foreground='w'),
pe.Normal()], label=r'$MLyr_{(T)}$')
ax_idx['F'].plot(mlb_idxx, mlb_idxy, c='grey', ls='-', alpha=3/4,
path_effects=[pe.Stroke(linewidth=5, foreground='w'),
pe.Normal()], label=r'$MLyr_{(B)}$')
ax_idx['F'].legend(loc='upper left')
plt.colorbar(fzhna, ax=ax_idx['F']).ax.tick_params(labelsize=10)
ax_idx['F'].grid(True)
ax_idx['F'].axes.set_aspect('equal')
ax_idx['F'].tick_params(axis='both', labelsize=10)
# =========================================================================
# Creates plots for attenuation correction vars.
# =========================================================================
fig_mos3, ax_idx3 = plt.subplots(figsize=fig_size2)
for key, value in rad_vars_attcorr.items():
if key == 'beta [-]':
cmap = 'tpylsc_useq_fiery'
norm = dnorm.get(key[key.find('['):])
fzhna = ax_idx3.pcolormesh(rad_georef['grid_rectx'],
rad_georef['grid_recty'], value,
shading='auto', cmap=cmap, norm=norm)
ax_idx3.set_title(f"{ptitle}" "\n" f'{key}')
if mlyr is not None:
ax_idx3.plot(mlt_idxx, mlt_idxy, c='k', ls='-', alpha=3/4,
path_effects=[pe.Stroke(linewidth=5, foreground='w'),
pe.Normal()], label=r'$MLyr_{(T)}$')
ax_idx3.plot(mlb_idxx, mlb_idxy, c='grey', ls='-', alpha=3/4,
path_effects=[pe.Stroke(linewidth=5, foreground='w'),
pe.Normal()], label=r'$MLyr_{(B)}$')
ax_idx3.legend(loc='upper left')
plt.colorbar(fzhna, ax=ax_idx3).ax.tick_params(labelsize=10)
ax_idx3.grid(True)
ax_idx3.axes.set_aspect('equal')
ax_idx3.tick_params(axis='both', labelsize=10)
plt.tight_layout()
[docs]
def plot_radprofiles(rad_profs, beam_height, mlyr=None, stats=None, ylims=None,
vars_bounds=None, colours=False, unorm=None, ucmap=None,
cb_ext=None, fig_size=None):
"""
Display a set of profiles of polarimetric variables.
Parameters
----------
rad_profs : dict
Profiles generated by the PolarimetricProfiles class.
beam_height : array
The beam height.
mlyr : MeltingLayer Class, optional
Plots the melting layer within the polarimetric profiles.
The default is None.
stats : dict, optional
Statistics of the profiles generation computed by the
PolarimetricProfiles class. The default is None.
ylims : 2-element tuple or list, optional
Set the y-axis view limits [min, max]. The default is None.
vars_bounds : dict containing key and 3-element tuple or list, optional
Boundaries [min, max] between which radar variables are
to be plotted.
colours : Bool, optional
Creates coloured profiles using norm to map colormaps.
unorm : dict containing matplotlib.colors normalisation objects, optional
User-defined normalisation methods to map colormaps onto radar data.
The default is None.
ucmap : colormap, optional
User-defined colormap, either a matplotlib.colors.ListedColormap,
or string from matplotlib.colormaps.
cb_ext : dict containing key and str, optional
The str modifies the end(s) for out-of-range values for a
given key (radar variable). The str has to be one of 'neither',
'both', 'min' or 'max'.
fig_size : 2-element tuple or list, optional
Modify the default plot size.
"""
fontsizelabels = 20
fontsizetitle = 25
fontsizetick = 18
prftype = getattr(rad_profs, 'profs_type').lower()
# ttxt_elev = f"{rad_profs.elev_angle:{2}.{2}} Deg."
# ttxt_dt = f"{rad_profs.scandatetime:%Y-%m-%d %H:%M:%S}"
# ttxt = dtdes1+ttxt_dt
if isinstance(rad_profs.elev_angle, str):
dtdes1 = f"{rad_profs.elev_angle} -- "
else:
dtdes1 = f"{rad_profs.elev_angle:{2}.{2}} deg. -- "
dtdes2 = f"{rad_profs.scandatetime:%Y-%m-%d %H:%M:%S}"
ptitle = dtdes1 + dtdes2
if fig_size is None:
fig_size = (14, 10)
def make_colorbar(ax1, mappable, **kwargs):
ax1_divider = make_axes_locatable(ax1)
orientation = kwargs.pop('orientation', 'vertical')
if orientation == 'vertical':
loc = 'right'
elif orientation == 'horizontal':
loc = 'top'
cax = ax1_divider.append_axes(loc, '7%', pad='2.5%',
axes_class=plt.Axes)
ax1.get_figure().colorbar(mappable, cax=cax,
orientation=orientation,
ticks=ticks,
format=f'%.{cbtks_fmt}f')
cax.tick_params(direction='in', labelsize=10, rotation=90)
cax.xaxis.set_ticks_position('top')
if rad_profs.profs_type.lower() == 'vps':
rprofs = rad_profs.vps
fig, ax = plt.subplots(1, len(rad_profs.vps), figsize=fig_size,
sharey=True)
fig.suptitle(f'Vertical profiles of polarimetric variables'
'\n' f'{ptitle}',
fontsize=fontsizetitle)
elif rad_profs.profs_type.lower() == 'qvps':
rprofs = rad_profs.qvps
fig, ax = plt.subplots(1, len(rad_profs.qvps), figsize=fig_size,
sharey=True)
fig.suptitle('Quasi-Vertical profiles of polarimetric variables \n'
f'{ptitle}',
fontsize=fontsizetitle)
for n, (a, (key, value)) in enumerate(zip(ax.flatten(), rprofs.items())):
lpv, bnd, cmaph, cmapext, dnorm, v2p, normp, cbtks_fmt, ticks = pltparams(
key, getattr(rad_profs, prftype).keys(), vars_bounds, ucmap=ucmap,
unorm=unorm, cb_ext=cb_ext)
if key == 'rhoHV [-]':
ticks = ticks
else:
ticks = None
if colours is False:
a.plot(value, beam_height, 'k')
elif colours:
if unorm is not None:
dnorm.update(unorm)
cmapp = cmaph.get(key[key.find('['):],
mpl.colormaps['tpylsc_rad_pvars'])
points = np.array([value, beam_height]).T.reshape(-1, 1, 2)
segments = np.concatenate([points[:-1], points[1:]], axis=1)
lc = LineCollection(segments, cmap=cmapp,
norm=dnorm.get(key[key.find('['):]))
# Set the values used for colormapping
lc.set_array(value)
lc.set_linewidth(2)
# line = a.add_collection(lc)
a.add_collection(lc)
make_colorbar(a, lc, orientation='horizontal')
if np.isfinite(np.nanmin(value)) and np.isfinite(np.nanmax(value)):
a.set_xlim([np.nanmin(value), np.nanmax(value)])
# if stats:
# a.fill_betweenx(beam_height,
# value + stats.get(key, value*np.nan),
# value - stats.get(key, value*np.nan),
# alpha=0.4, color='gray', label='std')
if stats == 'std_dev' or stats == 'sem':
if rad_profs.profs_type.lower() == 'vps':
a.fill_betweenx(beam_height,
value + rad_profs.vps_stats[stats][key],
value - rad_profs.vps_stats[stats][key],
alpha=0.4, label=f'{stats}')
if rad_profs.profs_type.lower() == 'qvps':
a.fill_betweenx(beam_height,
value + rad_profs.qvps_stats[stats][key],
value - rad_profs.qvps_stats[stats][key],
alpha=0.4, label=f'{stats}')
# a.fill_betweenx(beam_height,
# value + stats.get(key, value*np.nan),
# value - stats.get(key, value*np.nan),
# alpha=0.4, color='gray', label='std')
if n == 0:
a.set_ylabel('Height [km]', fontsize=fontsizelabels, labelpad=15)
a.tick_params(axis='both', labelsize=fontsizetick)
a.grid(True)
if vars_bounds:
if key in lpv:
if key == 'rhoHV [-]':
a.set_xlim(lpv.get(key)[0], lpv.get(key)[2])
else:
a.set_xlim(lpv.get(key)[:2])
if mlyr:
a.axhline(mlyr.ml_top, c='tab:blue', ls='dashed', lw=5,
alpha=.5, label='$ML_{top}$')
a.axhline(mlyr.ml_bottom, c='tab:purple', ls='dashed', lw=5,
alpha=.5, label='$ML_{bottom}$')
a.legend(loc='upper right', fontsize=fontsizetick)
if key == 'ZH [dBZ]':
a.set_xlabel('$Z_{H}$ [dBZ]', fontsize=fontsizelabels)
elif key == 'ZDR [dB]':
a.set_xlabel('$Z_{DR}$ [dB]', fontsize=fontsizelabels)
elif key == 'rhoHV [-]':
a.set_xlabel(r'$ \rho_{HV}$ [-]', fontsize=fontsizelabels)
elif key == 'PhiDP [deg]':
a.set_xlabel(r'$ \Phi_{DP}$ [deg]', fontsize=fontsizelabels)
elif key == 'V [m/s]':
a.set_xlabel('V [m/s]', fontsize=fontsizelabels)
elif key == 'gradV [dV/dh]' and rad_profs.profs_type.lower() == 'vps':
a.set_xlabel('grad V [dV/dh]', fontsize=fontsizelabels)
elif key == 'KDP [deg/km]':
a.set_xlabel('$K_{DP}$'+r'$\left [\frac{deg}{km}\right ]$',
fontsize=fontsizelabels)
else:
a.set_xlabel(key, fontsize=fontsizelabels)
if ylims:
a.set_ylim(ylims)
else:
a.set_ylim(0, 10)
plt.show()
plt.tight_layout()
[docs]
def plot_rdqvps(rscans_georef, rscans_params, tp_rdqvp, spec_range=None,
mlyr=None, ylims=None, vars_bounds=None, ucmap=None,
cb_ext=None, all_desc=False, fig_size=None):
"""
Display a set of RD-QVPS of polarimetric variables.
Parameters
----------
rscans_georef : List
List of georeferenced data containing descriptors of the azimuth, gates
and beam height, amongst others, corresponding to each QVP.
rscans_params : List
List of radar technical details corresponding to each QVP.
tp_rdqvp : PolarimetricProfiles Class
Outputs of the RD-QVPs function.
spec_range : int, optional
Range from the radar within which the RD-QVPS were built.
mlyr : MeltingLayer Class, optional
Plots the melting layer within the polarimetric profiles.
The default is None.
ylims : 2-element tuple or list, optional
Set the y-axis view limits [min, max]. The default is None.
vars_bounds : dict containing key and 2-element tuple or list, optional
Boundaries [min, max] between which radar variables are
to be plotted.
ucmap : colormap, optional
User-defined colormap, either a matplotlib.colors.ListedColormap,
or string from matplotlib.colormaps.
cb_ext : dict containing key and str, optional
The str modifies the end(s) for out-of-range values for a
given key (radar variable). The str has to be one of 'neither',
'both', 'min' or 'max'.
all_desc : bool, optional
If True, plots the initial QVPs used to compute the RD-QPVs.
The default is True.
fig_size : 2-element tuple or list, optional
Modify the default plot size.
"""
if fig_size is None:
fig_size = (14, 10)
fontsizelabels = 20
fontsizetitle = 25
fontsizetick = 18
lpv, bnd, cmaph, cmapext, dnorm, v2p, normp, cbtks_fmt, tcks = pltparams(
None, tp_rdqvp.rd_qvps.keys(), vars_bounds, ucmap=ucmap, cb_ext=cb_ext)
if vars_bounds:
lpv.update(vars_bounds)
cmaph = mpl.colormaps['Spectral'](
np.linspace(0, 1, len(rscans_params)))
if ucmap is not None:
if isinstance(ucmap, str):
cmaph = mpl.colormaps[ucmap](np.linspace(0, 1, len(rscans_params)))
else:
cmaph = ucmap(np.linspace(0, 1, len(rscans_params)))
# ttxt = f"{rscans_params[0]['datetime']:%Y-%m-%d %H:%M:%S}"
dt1 = min([i['datetime'] for i in rscans_params])
dt2 = max([i['datetime'] for i in rscans_params])
ttxt = (f"{dt1:%Y-%m-%d %H:%M:%S} - {dt2:%H:%M:%S}")
mosaic = [chr(ord('@')+c+1) for c in range(len(tp_rdqvp.rd_qvps)+1)]
mosaic = f'{"".join(mosaic)}'
fig = plt.figure(layout="constrained", figsize=fig_size)
fig.suptitle('RD-QVPs of polarimetric variables \n' f'{ttxt}',
fontsize=fontsizetitle)
axd = fig.subplot_mosaic(mosaic, sharey=True, height_ratios=[5])
if all_desc:
for c, i in enumerate(tp_rdqvp.qvps_itp):
for n, (a, (key, value)) in enumerate(zip(axd, i.items())):
axd[a].plot(value, tp_rdqvp.georef['profiles_height [km]'],
color=cmaph[c], ls='--',
label=(f"{rscans_params[c]['elev_ang [deg]']:.1f}"
+ r"$^{\circ}$"))
# axd[a].legend(loc='upper right')
if not all_desc:
i = tp_rdqvp.rd_qvps
for n, (a, (key, value)) in enumerate(zip(axd, i.items())):
axd[a].plot(tp_rdqvp.rd_qvps[key],
tp_rdqvp.georef['profiles_height [km]'], 'k', lw=3,
label='RD-QVP')
axd[a].legend(loc='upper right')
if vars_bounds:
if key in lpv:
axd[a].set_xlim(lpv.get(key))
else:
axd[a].set_xlim([np.nanmin(value), np.nanmax(value)])
if mlyr:
axd[a].axhline(mlyr.ml_top, c='tab:blue', ls='dashed', lw=5,
alpha=.5, label='$ML_{top}$')
axd[a].axhline(mlyr.ml_bottom, c='tab:purple', ls='dashed', lw=5,
alpha=.5, label='$ML_{bottom}$')
if ylims:
axd[a].set_ylim(ylims)
axd[a].set_xlabel(f'{key}', fontsize=fontsizelabels)
if n == 0:
axd[a].set_ylabel('Height [km]', fontsize=fontsizelabels,
labelpad=10)
axd[a].tick_params(axis='both', labelsize=fontsizetick)
axd[a].grid(True)
scan_st = axd[mosaic[-1]]
for c, i in enumerate(rscans_georef):
scan_st.plot(i['range [m]']/1000, i['beam_height [km]'][0],
color=cmaph[c], ls='--',
label=(f"{rscans_params[c]['elev_ang [deg]']:.1f}"
+ r"$^{\circ}$"))
# scan_st.plot(i['range [m]']/-1000, i['beam_height [km]'][0],
# color=cmaph[c], ls='--')
if spec_range:
scan_st.axvline(spec_range, c='k', lw=3, label=f'RD={spec_range}')
scan_st.set_xlabel('Range [km]', fontsize=fontsizelabels)
scan_st.tick_params(axis='both', labelsize=fontsizetick)
scan_st.grid(True)
scan_st.legend(loc='upper right')
[docs]
def plot_offsetcorrection(rad_georef, rad_params, rad_var, var_m=None,
var_offset=0, fig_size=None, var_name='PhiDP [deg]',
mode='mean', cmap='tpylsc_div_lbu_w_rd'):
"""
Plot the offset detection method from ZDR/PhiDP_Calibration Class.
Parameters
----------
rad_georef : dict
Georeferenced data containing descriptors of the azimuth, gates
and beam height, amongst others.
rad_params : dict
Radar technical details.
rad_var : dict
PPI scan of the radar variable used to detect the offset.
var_name : str
Key of the radar variable used to detect the offset.
cmap : colormap, optional
User-defined colormap. The default is 'tpylsc_div_lbu_w_rd'.
"""
if var_m is None:
if mode == 'mean':
var_m = np.array([np.nanmean(i) for i in rad_var])
elif mode == 'median':
var_m = np.array([np.nanmedian(i) for i in rad_var])
else:
var_m = var_m
if var_name == 'PhiDP [deg]':
label1 = r'$\Phi_{DP}$'
labelm = r'$\overline{\Phi_{DP}}$'
labelo = r'$\Phi_{DP}$ offset'
dof = 90
dval = dof // 3
elif var_name == 'ZDR [dB]':
label1 = '$Z_{DR}$'
labelm = r'$\overline{Z_{DR}}$'
labelo = r'$Z_{DR}$ offset'
dval = 0.1
dof = 1
if fig_size is None:
fig_size = (8, 8)
fig, ax = plt.subplots(figsize=fig_size,
subplot_kw={'projection': 'polar'})
ax.set_theta_direction(-1)
if isinstance(rad_params['elev_ang [deg]'], str):
dtdes1 = f"{rad_params['elev_ang [deg]']} -- "
else:
dtdes1 = f"{rad_params['elev_ang [deg]']:{2}.{2}} deg. -- "
if rad_params['datetime']:
dtdes2 = f"{rad_params['datetime']:%Y-%m-%d %H:%M:%S}"
else:
dtdes2 = ''
ptitle = dtdes1 + dtdes2
ax.set_title(ptitle, fontsize=16)
ax.grid(color='gray', linestyle=':')
ax.set_theta_zero_location('N', offset=0)
# =========================================================================
# Plot the radar variable values at each azimuth
# =========================================================================
ax.scatter((np.ones_like(rad_var.T) * [rad_georef['azim [rad]']]).T,
rad_var, s=5, c=rad_var, cmap=cmap, label=label1,
norm=mpc.SymLogNorm(
linthresh=.01, linscale=.01, base=2,
vmin=(var_m.mean()-dval if mode == 'mean'
else np.median(var_m)-dval if mode == 'median'
else var_m.min()),
vmax=(var_m.mean()+dval if mode == 'mean'
else np.median(var_m)+dval if mode == 'median'
else var_m.max())))
# =========================================================================
# Plot the radar variable mean/median value of each azimuth
# =========================================================================
ax.plot(rad_georef['azim [rad]'], var_m, c='grey', linewidth=2,
ls='', marker='s', markeredgecolor='k', alpha=0.4, label=labelm)
# =========================================================================
# Plot the radar variable offset
# =========================================================================
if var_offset != 0:
ax.plot(rad_georef['azim [rad]'],
np.full(rad_georef['azim [rad]'].shape, var_offset),
c='k', linewidth=2.5,
label=f'{labelo} \n [{var_offset:0.2f}]')
ax.set_thetagrids(np.arange(0, 360, 90))
ax.xaxis.grid(ls='-')
ax.tick_params(axis='both', labelsize=14)
ax.set_rlabel_position(-45)
# if var_name == 'PhiDP [deg]':
# ax.set_ylim([var_m.mean()-dof, var_m.mean()+dof])
# ax.set_yticks(np.arange(round(var_m.mean()/dval)*dval-dof,
# round(var_m.mean()/dval)*dval+dof+1,
# dval))
# else:
# ax.set_ylim([var_m.mean()-dof, var_m.mean()+dof])
# # ax.set_yticks(np.arange(round(var_m.mean()/dval)*dval-dof,
# # round(var_m.mean()/dval)*dval+dof+.1,
# # dval))
angle = np.deg2rad(67.5)
ax.legend(fontsize=15, loc="lower left",
bbox_to_anchor=(.58 + np.cos(angle)/2, .4 + np.sin(angle)/2))
ax.axes.set_aspect('equal')
plt.tight_layout()
[docs]
def plot_mfs(path_mfs, norm=True, vars_bounds=None, fig_size=None):
"""
Plot the membership functions used in clutter classification.
Parameters
----------
path_mfs : str
Location of the membership function files..
norm : bool, optional
Determines if the variables are normalised for a more comprehensive
visualisation of the MFS. The default is True.
vars_bounds : dict containing key and 3-element tuple or list, optional
Boundaries [min, max, LaTeX Varnames] between which radar variables are
to be mapped.
fig_size : list or tuple containing 2-element numbers, optional
Width, height in inches. The default is None.
"""
import os
mfspk = {
'ZHH': [[-10, 60], '$Z_H$ [dBZ]'],
'sZhh': [[0, 20], r'$\sigma(Z_{H})$ [dBZ]'],
'ZDR': [[-6, 6], '$Z_{DR}$ [dB]'],
'sZdr': [[0, 5], r'$\sigma(Z_{DR}$) [dB]'],
'Rhv': [[0, 1], r'$\rho_{HV}$ [-]'],
'sRhv': [[0, .4], r'$\sigma(\rho_{HV})$ [-]'],
'Pdp': [[0, 180], r'$\Phi_{DP}$ [deg]'],
'sPdp': [[0, 180], r'$\sigma(\Phi_{DP})$ [deg]'],
'Vel': [[-3, 3], 'V [m/s]'],
'sVel': [[0, 5], r'$\sigma(V)$ [m/s]'],
'LDR': [[-40, 10], 'LDR [dB]'],
}
if vars_bounds is not None:
mfspk.update(vars_bounds)
mfsp = {f[f.find('mf_')+3: f.find('_preci')]: np.loadtxt(f'{path_mfs}{f}')
for f in sorted(os.listdir(path_mfs))
if f.endswith('_precipi.dat')}
mfsp = {k: v for k, v in sorted(mfsp.items()) if k in mfspk}
mfsc = {f[f.find('mf_')+3: f.find('_clu')]: np.loadtxt(f'{path_mfs}{f}')
for f in sorted(os.listdir(path_mfs))
if f.endswith('_clutter.dat')}
mfsc = {k: v for k, v in sorted(mfsc.items()) if k in mfspk}
varsp = {k for k in mfsp.keys()}
varsc = {k for k in mfsc.keys()}
if len(varsp) % 2 == 0:
ncols = int(len(varsp) / 2)
nrows = len(varsp) // ncols
if fig_size is None:
fig_size = (18, 5)
else:
ncols = 3
if len(varsp) % 3 == 0:
nrows = (len(varsp) // ncols)
else:
nrows = (len(varsp) // ncols)+1
if fig_size is None:
fig_size = (18, 7.5)
if varsp != varsc:
raise TowerpyError('Oops!... The number of membership functions for'
+ 'clutter and precipitation do not correspond.'
+ 'Please check before continue.')
if norm is True:
mfs_prnorm = {k: np.array([val[:, 0], rut.normalisenan(val[:, 1])]).T
for k, val in mfsp.items()}
mfs_clnorm = {k: np.array([val[:, 0], rut.normalisenan(val[:, 1])]).T
for k, val in mfsc.items()}
f, ax = plt.subplots(nrows, ncols, sharey=True, figsize=fig_size)
for a, (key, value) in zip(ax.flatten(), mfs_prnorm.items()):
a.plot(value[:, 0], value[:, 1], c='tab:blue', label='PR')
a.plot(mfs_clnorm[key][:, 0], mfs_clnorm[key][:, 1], label='CL',
ls='dashed', c='tab:orange')
# a.set_xlim(left=0)
a.set_xlim(mfspk[key][0])
a.tick_params(axis='both', labelsize=16)
divider = make_axes_locatable(a)
cax = divider.append_axes("top", size="15%", pad=0)
cax.get_xaxis().set_visible(False)
cax.get_yaxis().set_visible(False)
cax.set_facecolor('slategrey')
at = AnchoredText(mfspk[key][1], loc='center',
prop=dict(size=18, color='white'), frameon=False)
cax.add_artist(at)
a.legend(fontsize=14)
f.tight_layout()
[docs]
def plot_zhah(rad_vars, r_ahzh, temp, coeff_a, coeff_b, coeffs_a, coeffs_b,
temps, zh_lower_lim, zh_upper_lim, var2calc='ZH [dBZ]'):
r"""
Display the AH-ZH relation.
Parameters
----------
rad_vars : dict
Dict containing radar variables to plot.
r_ahzh : obj
Results of the Attn_Refl_Relation class.
temp: float
Temperature, in :math:`^{\circ}C`, used to derive the coefficients
according to [1]_. The default is 10.
coeff_a, coeff_b: float
Computed coefficients of the :math:`A_H(Z_H)` relationship.
coeffs_a, coeffs_b: list or array
Default coefficients of the :math:`A_H(Z_H)` relationship..
temps : list or array
Default values for the temperature.
var2calc : str, optional
Radar variable to be computed. The string has to be one of
'AH [dB/km]' or 'ZH [dBZ]'. The default is 'ZH [dBZ]'.
"""
tcksize = 14
cmap = 'Spectral_r'
n1 = mpc.LogNorm(vmin=1, vmax=1000)
gridsize = 200
ahzhii = np.arange(zh_lower_lim, zh_upper_lim, 0.05)
ahzhlii = tpuc.xdb2x(ahzhii)
ahzhi = coeff_a * ahzhlii ** coeff_b
if var2calc == 'AH [dB/km]' and 'ZH [dBZ]' in rad_vars.keys():
zh_all = rad_vars['ZH [dBZ]'].ravel()
ah_all = rad_vars['AH [dB/km]'].ravel()
elif 'AH [dB/km]' in r_ahzh.keys():
zh_all = rad_vars['ZH [dBZ]'].ravel()
ah_all = r_ahzh['AH [dB/km]'].ravel()
if var2calc == 'ZH [dBZ]' and 'ZH [dBZ]' in rad_vars.keys():
zh_all = rad_vars['ZH [dBZ]'].ravel()
ah_all = rad_vars['AH [dB/km]'].ravel()
# Plot the AH-ZH values
fig, ax = plt.subplots()
ax.plot(ahzhii, ahzhi, c='k', ls='--',
label=f'$A_H={coeff_a:,.2e}Z_H^{{{coeff_b:,.2f}}}$')
ax_divider = make_axes_locatable(ax)
cax = ax_divider.append_axes("top", size="7%", pad="2%")
hxb = ax.hexbin(np.ma.masked_invalid(zh_all),
np.ma.masked_invalid(ah_all), gridsize=gridsize,
mincnt=1, cmap=cmap, norm=n1)
cb = fig.colorbar(hxb, cax=cax, extend='max',
orientation='horizontal')
ax.set_xlim([-10, 60])
ax.set_ylim([0, 2])
cb.ax.tick_params(direction='in', labelsize=tcksize,)
cb.ax.set_title('Counts', fontsize=tcksize)
cax.xaxis.set_ticks_position("top")
ax.set_xlabel('$Z_H$ [dBZ]')
ax.set_ylabel('$A_H$ [dB/km]')
ax.legend(loc='upper left')
ax.grid()
# Plot the linar interpolation of temp.
fig, axs = plt.subplots(1, 2, figsize=(12, 4), sharey=True)
fig.suptitle(rf'Linear Interpolation at T = {temp}$\degree$')
ax = axs[0]
ax.plot(coeffs_a, temps, '-ob')
ax.plot(coeff_a, temp, 'ro')
ax.set_xlabel('coeff a')
ax.set_ylabel(r'Temp ${\degree}$C')
ax = axs[1]
ax.plot(coeffs_b, temps, '-ob')
ax.plot(coeff_b, temp, 'ro')
ax.set_xlabel('coeff b')
fig.tight_layout()
[docs]
def plot_ppidiff(rad_georef, rad_params, rad_var1, rad_var2, var2plot1=None,
var2plot2=None, diff_lims=[-10, 10, 1], mlyr=None, xlims=None,
ylims=None, vars_bounds=None, unorm=None, ucmap=None,
ucmap_diff=None, cb_ext=None, fig_title=None, fig_size=None):
"""
Plot the difference between a radar variable from different dicts.
Parameters
----------
rad_georef : dict
Georeferenced data containing descriptors of the azimuth, gates
and beam height, amongst others.
rad_params : dict
Radar technical details.
rad_var1 : dict
Dict containing radar variables to plot.
rad_var2 : dict
Dict containing radar variables to plot.
vars2plot : str, optional
Keys of the radar variables to plot. Variables must have the same
units. The default is None. This option will plot ZH or look for the
'first' element in the rad_vars dict.
diff_lims : 3-element tuple or list, optional
Boundaries [min, max, step] used for mapping the difference plot.
The default is [-10, 10, 1].
mlyr : MeltingLayer Class, optional
Plot the melting layer height. ml_top (float, int, list or np.array)
and ml_bottom (float, int, list or np.array) must be explicitly
defined. The default is None.
xlims : 2-element tuple or list, optional
Set the x-axis view limits [min, max]. The default is None.
ylims : 2-element tuple or list, optional
Set the y-axis view limits [min, max]. The default is None.
vars_bounds : dict containing key and 3-element tuple or list, optional
Boundaries [min, max, nvals] between which radar variables are
to be mapped.
unorm : dict containing matplotlib.colors normalisation objects, optional
User-defined normalisation methods to map colormaps onto radar data.
The default is None.
ucmap : colormap, optional
User-defined colormap, either a matplotlib.colors.ListedColormap,
or string from matplotlib.colormaps.
ucmap_diff : str of colormap, optional
User-defined colormap used in the difference plot.
cb_ext : dict containing key and str, optional
The str modifies the end(s) for out-of-range values for a
given key (radar variable). The str has to be one of 'neither',
'both', 'min' or 'max'.
fig_title : str, optional
String to show in the plot title.
fig_size : 2-element tuple or list, optional
Modify the default plot size.
"""
lpv, bnd, cmaph, cmapext, dnorm, v2p, normp, cbtks_fmt, tcks = pltparams(
var2plot1, rad_var1, vars_bounds, ucmap=ucmap, unorm=unorm,
cb_ext=cb_ext)
if var2plot1 is None:
var2plot1 = v2p
lpv2, bnd2, cmaph2, cmapext2, dnorm2, v2p2, normp2, cbtks_fmt2, tcks2 = pltparams(
var2plot2, rad_var2, vars_bounds, ucmap=ucmap, unorm=unorm,
cb_ext=cb_ext)
if var2plot2 is None:
var2plot2 = v2p2
cmapp = cmaph.get(var2plot1[var2plot1.find('['):],
mpl.colormaps['tpylsc_rad_pvars'])
if fig_title is None:
if isinstance(rad_params['elev_ang [deg]'], str):
dtdes1 = f"{rad_params['elev_ang [deg]']} -- "
else:
dtdes1 = f"{rad_params['elev_ang [deg]']:{2}.{2}} deg. -- "
if rad_params['datetime']:
dtdes2 = f"{rad_params['datetime']:%Y-%m-%d %H:%M:%S}"
else:
dtdes2 = ''
ptitle = dtdes1 + dtdes2
else:
ptitle = fig_title
if mlyr is not None:
if isinstance(mlyr.ml_top, (int, float)):
mlt_idx = [rut.find_nearest(nbh, mlyr.ml_top)
for nbh in rad_georef['beam_height [km]']]
elif isinstance(mlyr.ml_top, (np.ndarray, list, tuple)):
mlt_idx = [rut.find_nearest(nbh, mlyr.ml_top[cnt])
for cnt, nbh in
enumerate(rad_georef['beam_height [km]'])]
if isinstance(mlyr.ml_bottom, (int, float)):
mlb_idx = [rut.find_nearest(nbh, mlyr.ml_bottom)
for nbh in rad_georef['beam_height [km]']]
elif isinstance(mlyr.ml_bottom, (np.ndarray, list, tuple)):
mlb_idx = [rut.find_nearest(nbh, mlyr.ml_bottom[cnt])
for cnt, nbh in
enumerate(rad_georef['beam_height [km]'])]
mlt_idxx = np.array([rad_georef['grid_rectx'][cnt, ix]
for cnt, ix in enumerate(mlt_idx)])
mlt_idxy = np.array([rad_georef['grid_recty'][cnt, ix]
for cnt, ix in enumerate(mlt_idx)])
mlb_idxx = np.array([rad_georef['grid_rectx'][cnt, ix]
for cnt, ix in enumerate(mlb_idx)])
mlb_idxy = np.array([rad_georef['grid_recty'][cnt, ix]
for cnt, ix in enumerate(mlb_idx)])
# =========================================================================
# Creates plots to visualise difference
# =========================================================================
mosaic = 'ABC'
if fig_size is None:
fig_size = (16, 5)
fig_mos1 = plt.figure(figsize=fig_size, constrained_layout=True)
ax_idx = fig_mos1.subplot_mosaic(mosaic, sharex=True, sharey=True)
for key, value in rad_var1.items():
if key == var2plot1:
fzhna = ax_idx['A'].pcolormesh(rad_georef['grid_rectx'],
rad_georef['grid_recty'], value,
shading='auto', cmap=cmapp,
norm=normp)
ax_idx['A'].set_title(f"{ptitle}" "\n" f'{key}')
if mlyr is not None:
ax_idx['A'].plot(mlt_idxx, mlt_idxy, c='k', ls='-', alpha=3/4,
path_effects=[pe.Stroke(linewidth=5, foreground='w'),
pe.Normal()], label=r'$MLyr_{(T)}$')
ax_idx['A'].plot(mlb_idxx, mlb_idxy, c='grey', ls='-', alpha=3/4,
path_effects=[pe.Stroke(linewidth=5, foreground='w'),
pe.Normal()], label=r'$MLyr_{(B)}$')
ax_idx['A'].legend(loc='upper left')
if xlims is not None:
ax_idx['A'].set_xlim(xlims)
if ylims is not None:
ax_idx['A'].set_ylim(ylims)
# plt.colorbar(fzhna, ax=ax_idx['A']).ax.tick_params(labelsize=10)
ax_idx['A'].grid(True)
ax_idx['A'].axes.set_aspect('equal')
ax_idx['A'].tick_params(axis='both', labelsize=10)
for key, value in rad_var2.items():
if key == var2plot2:
fzhna = ax_idx['B'].pcolormesh(rad_georef['grid_rectx'],
rad_georef['grid_recty'], value,
shading='auto', cmap=cmapp,
norm=normp)
ax_idx['B'].set_title(f"{ptitle}" "\n" f'{key}')
if mlyr is not None:
ax_idx['B'].plot(mlt_idxx, mlt_idxy, c='k', ls='-', alpha=3/4,
path_effects=[pe.Stroke(linewidth=5, foreground='w'),
pe.Normal()], label=r'$MLyr_{(T)}$')
ax_idx['B'].plot(mlb_idxx, mlb_idxy, c='grey', ls='-', alpha=3/4,
path_effects=[pe.Stroke(linewidth=5, foreground='w'),
pe.Normal()], label=r'$MLyr_{(B)}$')
ax_idx['B'].legend(loc='upper left')
if xlims is not None:
ax_idx['B'].set_xlim(xlims)
if ylims is not None:
ax_idx['B'].set_ylim(ylims)
# plt.colorbar(fzhna, ax=ax_idx['B']).ax.tick_params(labelsize=10)
if (var2plot1 == 'rhoHV [-]' or '[mm]' in var2plot1
or '[mm/h]' in var2plot1):
plt.colorbar(fzhna, ax=ax_idx['B'], ticks=tcks,
format=f'%.{cbtks_fmt}f')
else:
plt.colorbar(fzhna, ax=ax_idx['B'])
ax_idx['B'].grid(True)
ax_idx['B'].axes.set_aspect('equal')
ax_idx['B'].tick_params(axis='both', labelsize=10)
cmaph = 'tpylsc_div_dbu_rd'
if ucmap_diff is not None:
cmaph = ucmap_diff
divnorm = mpl.colors.BoundaryNorm(
rut.linspace_step(diff_lims[0], diff_lims[1], diff_lims[2]),
mpl.colormaps[cmaph].N, extend='both')
fzhna = ax_idx['C'].pcolormesh(rad_georef['grid_rectx'],
rad_georef['grid_recty'],
rad_var1[var2plot1]-rad_var2[var2plot2],
shading='auto', cmap=cmaph, norm=divnorm)
ax_idx['C'].set_title(f"{ptitle}" "\n"
+ f"diff {var2plot1[var2plot1.find('['):]}")
if mlyr is not None:
ax_idx['C'].plot(mlt_idxx, mlt_idxy, c='k', ls='-', alpha=3/4,
path_effects=[pe.Stroke(linewidth=5, foreground='w'),
pe.Normal()], label=r'$MLyr_{(T)}$')
ax_idx['C'].plot(mlb_idxx, mlb_idxy, c='grey', ls='-', alpha=3/4,
path_effects=[pe.Stroke(linewidth=5, foreground='w'),
pe.Normal()], label=r'$MLyr_{(B)}$')
ax_idx['C'].legend(loc='upper left')
plt.colorbar(fzhna, ax=ax_idx['C']).ax.tick_params(labelsize=10)
ax_idx['C'].grid(True)
ax_idx['C'].axes.set_aspect('equal')
ax_idx['C'].tick_params(axis='both', labelsize=10)
# =============================================================================
# %% xarray implementation
# =============================================================================
[docs]
def _plot_rhohvmethod_single(snr_edges, rho_edges, hist, snr_db, rhohv_na,
snr_centers, theo_line, histmax, opt_noise,
fig_size=(8, 6)):
fig, ax = plt.subplots(figsize=fig_size)
pcm = ax.pcolormesh(snr_edges, rho_edges, hist.T,
norm=mpc.LogNorm(vmin=10**0, vmax=10**1),
cmap="tpylsc_useq_calm_r")
ax.plot(snr_centers, theo_line, color="tab:purple", lw=3,
label=r"theoretical $\rho_{HV}$")
ax.plot(snr_centers, histmax, color="k", lw=2, ls=":",
label="histogram maxima")
ax.scatter(snr_db.values.flatten(), rhohv_na.values.flatten(),
s=2, alpha=0.2, color="grey", label=r"raw $\rho_{HV}$")
ax.axhline(1.0, ls="--", color="tab:red")
ax.set_title(f"Noise level rc = {opt_noise:.2f} dB")
ax.set_xlabel("SNR [dB]")
ax.set_ylabel(r"$\rho_{HV}$ [-]")
ax.set_xlim(5, 30)
ax.set_ylim(0.8, 1.1)
fig.colorbar(pcm, ax=ax, label="n points")
ax.legend()
plt.show()
[docs]
def _plot_rhohvmethod_grid(Z, rng_km, rhohv_na, mode="linear", exp_curvet=20.0,
eps=0.005, rhohv_theo=(0.90, 1.0), opt_noise=None,
bins_snr=(5, 30, 0.1), bins_rho=(0.8, 1.1, 0.005),
fig_size=(16, 9)):
"""
Plot calibration grid around the optimised noise level.
Shows histograms for opt_noise ±5 dB in 1 dB steps,
using corrected rhoHV (rhohv_corr).
"""
from ..calib.calib_rhohv import _build_theo_line
from ..eclass.snr import signal2noiseratio
from ..utils.radutilities import xr_hist2d
if opt_noise is None:
raise ValueError("opt_noise must be provided (final noise_level_dB).")
rc_values = np.arange(opt_noise - 4, opt_noise + 5, 1.0)
snr_edges = np.arange(*bins_snr)
rho_edges = np.arange(*bins_rho)
rhohv_centers = 0.5 * (rho_edges[:-1] + rho_edges[1:])
snr_centers = 0.5 * (snr_edges[:-1] + snr_edges[1:])
hists, histmax, theo_lines = [], [], []
for rc in rc_values:
snr_db = signal2noiseratio(Z, rng_km, rc, scale="db")
snr_lin = signal2noiseratio(Z, rng_km, rc, scale="lin")
rhohv_corr = (rhohv_na * (1 + 1 / snr_lin)).rename("rhohv_corr")
hist = xr_hist2d(snr_db, rhohv_corr, snr_edges, rho_edges,
dim=list(snr_db.dims))
hists.append((hist.values, snr_edges, rho_edges))
rhohv_bin_dim = [d for d in hist.dims if d.endswith("_bin")][1]
idx = hist.argmax(dim=rhohv_bin_dim)
maxima = xr.apply_ufunc(lambda i: rhohv_centers[i], idx,
vectorize=True, dask="parallelized",
output_dtypes=[float]).values
histmax.append(maxima)
theo_line = _build_theo_line(snr_centers, rhohv_theo, mode=mode,
exp_curvet=exp_curvet, eps=eps)
theo_lines.append(theo_line)
nc = min(3, len(hists))
nr = len(hists) // nc + (len(hists) % nc > 0)
fig, axes = plt.subplots(nrows=nr, ncols=nc,
sharex=True, sharey=True,
figsize=fig_size, constrained_layout=True)
axes = np.atleast_1d(axes).ravel()
for i, ax in enumerate(axes):
if i < len(hists):
H, snr_edges, rho_edges = hists[i]
maxima = histmax[i]
theo_line = theo_lines[i]
rc = rc_values[i]
if np.isclose(rc, opt_noise, atol=0.5):
title_color, weight = "tab:purple", "bold"
else:
title_color, weight = "tab:grey", "normal"
ax.set_title(f"{rc:.1f} dB", color=title_color, fontweight=weight)
ax.plot(snr_edges[1:], maxima, color="k", lw=2, ls=":",
label="histogram maxima")
ax.plot(snr_centers, theo_line, color="tab:purple", lw=2,
label="theoretical line")
ax.axhline(1.0, ls="--", color="tab:red")
pcm = ax.pcolormesh(snr_edges, rho_edges, H.T,
norm=mpc.LogNorm(vmin=10**0, vmax=10**1),
cmap="tpylsc_useq_calm_r", rasterized=True)
ax.tick_params(axis="both", which="major", labelsize=9)
clb = fig.colorbar(pcm, ax=axes, location="right", shrink=0.85)
clb.ax.set_title("n points")
fig.supxlabel("SNR [dB]")
fig.supylabel(r"$\rho_{HV}$ [-]")
plt.show()