"""
pyart.retrieve.echo_class
=========================
Functions for echo classification.
.. autosummary::
:toctree: generated/
steiner_conv_strat
hydroclass_semisupervised
get_freq_band
_standardize
_assign_to_class
_assign_to_class_scan
_compute_coeff_transform
_get_mass_centers
_mass_centers_table
_data_limits_table
"""
from warnings import warn
import numpy as np
from ..config import get_fillvalue, get_field_name, get_metadata
from ._echo_class import steiner_class_buff
[docs]def steiner_conv_strat(grid, dx=None, dy=None, intense=42.0,
work_level=3000.0, peak_relation='default',
area_relation='medium', bkg_rad=11000.0,
use_intense=True, fill_value=None,
refl_field=None):
"""
Partition reflectivity into convective-stratiform using the Steiner et
al. (1995) algorithm.
Parameters
----------
grid : Grid
Grid containing reflectivity field to partition.
dx, dy : float, optional
The x- and y-dimension resolutions in meters, respectively. If None
the resolution is determined from the first two axes values.
intense : float, optional
The intensity value in dBZ. Grid points with a reflectivity
value greater or equal to the intensity are automatically
flagged as convective. See reference for more information.
work_level : float, optional
The working level (separation altitude) in meters. This is the height
at which the partitioning will be done, and should minimize bright band
contamination. See reference for more information.
peak_relation : 'default' or 'sgp', optional
The peakedness relation. See reference for more information.
area_relation : 'small', 'medium', 'large', or 'sgp', optional
The convective area relation. See reference for more information.
bkg_rad : float, optional
The background radius in meters. See reference for more information.
use_intense : bool, optional
True to use the intensity criteria.
fill_value : float, optional
Missing value used to signify bad data points. A value of None
will use the default fill value as defined in the Py-ART
configuration file.
refl_field : str, optional
Field in grid to use as the reflectivity during partitioning. None
will use the default reflectivity field name from the Py-ART
configuration file.
Returns
-------
eclass : dict
Steiner convective-stratiform classification dictionary.
References
----------
Steiner, M. R., R. A. Houze Jr., and S. E. Yuter, 1995: Climatological
Characterization of Three-Dimensional Storm Structure from Operational
Radar and Rain Gauge Data. J. Appl. Meteor., 34, 1978-2007.
"""
# Get fill value
if fill_value is None:
fill_value = get_fillvalue()
# Parse field parameters
if refl_field is None:
refl_field = get_field_name('reflectivity')
# parse dx and dy
if dx is None:
dx = grid.x['data'][1] - grid.x['data'][0]
if dy is None:
dy = grid.y['data'][1] - grid.y['data'][0]
# Get coordinates
x = grid.x['data']
y = grid.y['data']
z = grid.z['data']
# Get reflectivity data
ze = np.ma.copy(grid.fields[refl_field]['data'])
ze = ze.filled(np.NaN)
eclass = steiner_class_buff(ze, x, y, z, dx=dx, dy=dy, bkg_rad=bkg_rad,
work_level=work_level, intense=intense,
peak_relation=peak_relation,
area_relation=area_relation,
use_intense=use_intense,)
return {'data': eclass.astype(np.int32),
'standard_name': 'echo_classification',
'long_name': 'Steiner echo classification',
'valid_min': 0,
'valid_max': 2,
'comment_1': ('Convective-stratiform echo '
'classification based on '
'Steiner et al. (1995)'),
'comment_2': ('0 = Undefined, 1 = Stratiform, '
'2 = Convective')}
[docs]def hydroclass_semisupervised(radar, mass_centers=None,
weights=np.array([1., 1., 1., 0.75, 0.5]),
value=50., refl_field=None, zdr_field=None,
rhv_field=None, kdp_field=None, temp_field=None,
iso0_field=None, hydro_field=None,
entropy_field=None, temp_ref='temperature',
compute_entropy=False, output_distances=False,
vectorize=False):
"""
Classifies precipitation echoes following the approach by Besic et al
(2016).
Parameters
----------
radar : radar
Radar object.
mass_centers : ndarray 2D, optional
The centroids for each variable and hydrometeor class in (nclasses,
nvariables).
weights : ndarray 1D, optional
The weight given to each variable.
value : float
The value controlling the rate of decay in the distance transformation
refl_field, zdr_field, rhv_field, kdp_field, temp_field, iso0_field : str
Inputs. Field names within the radar object which represent the
horizonal reflectivity, the differential reflectivity, the copolar
correlation coefficient, the specific differential phase, the
temperature and the height respect to the iso0 fields. A value of None
for any of these parameters will use the default field name as defined
in the Py-ART configuration file.
hydro_field : str
Output. Field name which represents the hydrometeor class field.
A value of None will use the default field name as defined in the
Py-ART configuration file.
temp_ref : str
the field use as reference for temperature. Can be either temperature
or height_over_iso0
compute_entropy : bool
If true, the entropy is computed
output_distances : bool
If true, the normalized distances to the centroids for each
hydrometeor are provided as output
vectorize : bool
If true, a vectorized version of the class assignation is going to be
used
Returns
-------
fields_dict : dict
Dictionary containing the retrieved fields
References
----------
Besic, N., Figueras i Ventura, J., Grazioli, J., Gabella, M., Germann, U.,
and Berne, A.: Hydrometeor classification through statistical clustering
of polarimetric radar measurements: a semi-supervised approach,
Atmos. Meas. Tech., 9, 4425-4445, doi:10.5194/amt-9-4425-2016, 2016
"""
lapse_rate = -6.5
# select the centroids as a function of frequency band
if mass_centers is None:
# assign coefficients according to radar frequency
if 'frequency' in radar.instrument_parameters:
mass_centers = _get_mass_centers(
radar.instrument_parameters['frequency']['data'][0])
else:
mass_centers = _mass_centers_table()['C']
warn('Radar frequency unknown. ' +
'Default coefficients for C band will be applied')
# parse the field parameters
if refl_field is None:
refl_field = get_field_name('reflectivity')
if zdr_field is None:
zdr_field = get_field_name('differential_reflectivity')
if rhv_field is None:
rhv_field = get_field_name('cross_correlation_ratio')
if kdp_field is None:
kdp_field = get_field_name('specific_differential_phase')
if hydro_field is None:
hydro_field = get_field_name('radar_echo_classification')
if compute_entropy:
if entropy_field is None:
entropy_field = get_field_name('hydroclass_entropy')
if temp_ref == 'temperature':
if temp_field is None:
temp_field = get_field_name('temperature')
else:
if iso0_field is None:
iso0_field = get_field_name('height_over_iso0')
# extract fields and parameters from radar
radar.check_field_exists(refl_field)
radar.check_field_exists(zdr_field)
radar.check_field_exists(rhv_field)
radar.check_field_exists(kdp_field)
if temp_ref == 'temperature':
radar.check_field_exists(temp_field)
else:
radar.check_field_exists(iso0_field)
refl = radar.fields[refl_field]['data']
zdr = radar.fields[zdr_field]['data']
rhohv = radar.fields[rhv_field]['data']
kdp = radar.fields[kdp_field]['data']
if temp_ref == 'temperature':
# convert temp in relative height respect to iso0
temp = radar.fields[temp_field]['data']
relh = temp*(1000./lapse_rate)
else:
relh = radar.fields[iso0_field]['data']
# standardize data
refl_std = _standardize(refl, 'Zh')
zdr_std = _standardize(zdr, 'ZDR')
kdp_std = _standardize(kdp, 'KDP')
rhohv_std = _standardize(rhohv, 'RhoHV')
relh_std = _standardize(relh, 'relH')
# standardize centroids
mc_std = np.zeros(np.shape(mass_centers), dtype=refl.dtype)
mc_std[:, 0] = _standardize(mass_centers[:, 0], 'Zh')
mc_std[:, 1] = _standardize(mass_centers[:, 1], 'ZDR')
mc_std[:, 2] = _standardize(mass_centers[:, 2], 'KDP')
mc_std[:, 3] = _standardize(mass_centers[:, 3], 'RhoHV')
mc_std[:, 4] = _standardize(mass_centers[:, 4], 'relH')
# if entropy has to be computed get transformation parameters
t_vals = None
if compute_entropy:
t_vals = _compute_coeff_transform(
mc_std, weights=weights, value=value)
# assign to class
if vectorize:
hydroclass_data, entropy_data, prop_data = _assign_to_class_scan(
refl_std, zdr_std, kdp_std, rhohv_std, relh_std, mc_std,
weights=weights, t_vals=t_vals)
else:
hydroclass_data, entropy_data, prop_data = _assign_to_class(
refl_std, zdr_std, kdp_std, rhohv_std, relh_std, mc_std,
weights=weights, t_vals=t_vals)
# prepare output fields
fields_dict = dict()
hydro = get_metadata(hydro_field)
hydro['data'] = hydroclass_data
hydro.update({'_FillValue': 0})
fields_dict.update({'hydro': hydro})
if compute_entropy:
entropy = get_metadata(entropy_field)
entropy['data'] = entropy_data
fields_dict.update({'entropy': entropy})
if output_distances:
prop_AG = get_metadata('proportion_AG')
prop_AG['data'] = prop_data[:, :, 0]
fields_dict.update({'prop_AG': prop_AG})
prop_CR = get_metadata('proportion_CR')
prop_CR['data'] = prop_data[:, :, 1]
fields_dict.update({'prop_CR': prop_CR})
prop_LR = get_metadata('proportion_LR')
prop_LR['data'] = prop_data[:, :, 2]
fields_dict.update({'prop_LR': prop_LR})
prop_RP = get_metadata('proportion_RP')
prop_RP['data'] = prop_data[:, :, 3]
fields_dict.update({'prop_RP': prop_RP})
prop_RN = get_metadata('proportion_RN')
prop_RN['data'] = prop_data[:, :, 4]
fields_dict.update({'prop_RN': prop_RN})
prop_VI = get_metadata('proportion_VI')
prop_VI['data'] = prop_data[:, :, 5]
fields_dict.update({'prop_VI': prop_VI})
prop_WS = get_metadata('proportion_WS')
prop_WS['data'] = prop_data[:, :, 6]
fields_dict.update({'prop_WS': prop_WS})
prop_MH = get_metadata('proportion_MH')
prop_MH['data'] = prop_data[:, :, 7]
fields_dict.update({'prop_MH': prop_MH})
prop_IH = get_metadata('proportion_IH')
prop_IH['data'] = prop_data[:, :, 8]
fields_dict.update({'prop_IH': prop_IH})
return fields_dict
def _standardize(data, field_name, mx=None, mn=None):
"""
Streches the radar data to -1 to 1 interval.
Parameters
----------
data : array
Radar field.
field_name : str
Type of field (relH, Zh, ZDR, KDP or RhoHV).
mx, mn : floats or None, optional
Data limits for array values.
Returns
-------
field_std : dict
Standardized radar data.
"""
if field_name == 'relH':
field_std = 2./(1.+np.ma.exp(-0.005*data))-1.
return field_std
if (mx is None) or (mn is None):
dlimits_dict = _data_limits_table()
if field_name not in dlimits_dict:
raise ValueError(
'Field ' + field_name + ' unknown. '
+ 'Valid field names for standardizing are: '
+ 'relH, Zh, ZDR, KDP and RhoHV')
mx, mn = dlimits_dict[field_name]
if field_name == 'KDP':
data[data < -0.5] = -0.5
data = 10.*np.ma.log10(data+0.6)
elif field_name == 'RhoHV':
data = 10.*np.ma.log10(1.-data)
mask = np.ma.getmaskarray(data)
field_std = 2.*(data-mn)/(mx-mn)-1.
field_std[data < mn] = -1.
field_std[data > mx] = 1.
field_std[mask] = np.ma.masked
return field_std
def _assign_to_class(zh, zdr, kdp, rhohv, relh, mass_centers,
weights=np.array([1., 1., 1., 0.75, 0.5]),
t_vals=None):
"""
Assigns an hydrometeor class to a radar range bin computing
the distance between the radar variables an a centroid.
Parameters
----------
zh, zdr, kdp, rhohv, relh : radar field
variables used for assigment normalized to [-1, 1] values
mass_centers : matrix
centroids normalized to [-1, 1] values (nclasses, nvariables)
weights : array
optional. The weight given to each variable (nvariables)
t_vals : array
transformation values for the distance to centroids (nclasses)
Returns
-------
hydroclass : int array
the index corresponding to the assigned class
entropy : float array
the entropy
t_dist : float matrix
if entropy is computed, the transformed distances of each class
(proxy for proportions of each hydrometeor) (nrays, nbins, nclasses)
"""
# prepare data
nrays = zh.shape[0]
nbins = zdr.shape[1]
nclasses = mass_centers.shape[0]
nvariables = mass_centers.shape[1]
hydroclass = np.ma.empty((nrays, nbins), dtype=np.uint8)
entropy = None
t_dist = None
if t_vals is not None:
entropy = np.ma.empty((nrays, nbins), dtype=zh.dtype)
t_dist = np.ma.masked_all((nrays, nbins, nclasses), dtype=zh.dtype)
for ray in range(nrays):
data = np.ma.array([zh[ray, :], zdr[ray, :], kdp[ray, :],
rhohv[ray, :], relh[ray, :]], dtype=zh.dtype)
weights_mat = np.broadcast_to(
weights.reshape(nvariables, 1), (nvariables, nbins))
dist = np.ma.zeros((nclasses, nbins), dtype=zh.dtype)
# compute distance: masked entries will not contribute to the distance
mask = np.ma.getmaskarray(zh[ray, :])
for i in range(nclasses):
centroids_class = mass_centers[i, :]
centroids_class = np.broadcast_to(
centroids_class.reshape(nvariables, 1), (nvariables, nbins))
dist_ray = np.ma.sqrt(np.ma.sum(
((centroids_class-data)**2.)*weights_mat, axis=0))
dist_ray[mask] = np.ma.masked
dist[i, :] = dist_ray
# Get hydrometeor class
class_vec = dist.argsort(axis=0, fill_value=10e40)
hydroclass_ray = (class_vec[0, :]+2).astype(np.uint8)
hydroclass_ray[mask] = 1
hydroclass[ray, :] = hydroclass_ray
if t_vals is None:
continue
# Transform the distance using the coefficient of the dominant class
t_vals_ray = np.ma.masked_where(mask, t_vals[class_vec[0, :]])
t_vals_ray = np.broadcast_to(
t_vals_ray.reshape(1, nbins), (nclasses, nbins))
t_dist_ray = np.ma.exp(-t_vals_ray*dist)
# set transformed distances to a value between 0 and 1
dist_total = np.ma.sum(t_dist_ray, axis=0)
dist_total = np.broadcast_to(
dist_total.reshape(1, nbins), (nclasses, nbins))
t_dist_ray /= dist_total
# Compute entropy
entropy_ray = -np.ma.sum(
t_dist_ray*np.ma.log(t_dist_ray)/np.ma.log(nclasses), axis=0)
entropy_ray[mask] = np.ma.masked
entropy[ray, :] = entropy_ray
t_dist[ray, :, :] = np.ma.transpose(t_dist_ray)
if t_vals is not None:
t_dist *= 100.
return hydroclass, entropy, t_dist
def _assign_to_class_scan(zh, zdr, kdp, rhohv, relh, mass_centers,
weights=np.array([1., 1., 1., 0.75, 0.5]),
t_vals=None):
"""
assigns an hydrometeor class to a radar range bin computing
the distance between the radar variables an a centroid.
Computes the entire radar volume at once
Parameters
----------
zh, zdr, kdp, rhohv, relh : radar field
variables used for assigment normalized to [-1, 1] values
mass_centers : matrix
centroids normalized to [-1, 1] values
weights : array
optional. The weight given to each variable
t_vals : matrix
transformation values for the distance to centroids
(nclasses, nvariables)
Returns
-------
hydroclass : int array
the index corresponding to the assigned class
entropy : float array
the entropy
t_dist : float matrix
if entropy is computed, the transformed distances of each class
(proxy for proportions of each hydrometeor) (nrays, nbins, nclasses)
"""
# prepare data
nrays = zh.shape[0]
nbins = zdr.shape[1]
nclasses = mass_centers.shape[0]
nvariables = mass_centers.shape[1]
data = np.ma.array([zh, zdr, kdp, rhohv, relh], dtype=zh.dtype)
weights_mat = np.broadcast_to(
weights.reshape(nvariables, 1, 1),
(nvariables, nrays, nbins))
# compute distance: masked entries will not contribute to the distance
mask = np.ma.getmaskarray(zh)
dist = np.ma.zeros((nrays, nbins, nclasses), dtype=zh.dtype)
t_dist = None
entropy = None
for i in range(nclasses):
centroids_class = mass_centers[i, :]
centroids_class = np.broadcast_to(
centroids_class.reshape(nvariables, 1, 1),
(nvariables, nrays, nbins))
dist_aux = np.ma.sqrt(np.ma.sum(
((centroids_class-data)**2.)*weights_mat, axis=0))
dist_aux[mask] = np.ma.masked
dist[:, :, i] = dist_aux
del data
del weights_mat
# Get hydrometeor class
class_vec = dist.argsort(axis=-1, fill_value=10e40)
hydroclass = np.ma.asarray(class_vec[:, :, 0]+2, dtype=np.uint8)
hydroclass[mask] = 1
if t_vals is not None:
# Transform the distance using the coefficient of the dominant class
t_vals_aux = np.ma.masked_where(mask, t_vals[class_vec[:, :, 0]])
t_vals_aux = np.broadcast_to(
t_vals_aux.reshape(nrays, nbins, 1), (nrays, nbins, nclasses))
t_dist = np.ma.exp(-t_vals_aux*dist)
del t_vals_aux
# set distance to a value between 0 and 1
dist_total = np.ma.sum(t_dist, axis=-1)
dist_total = np.broadcast_to(
dist_total.reshape(nrays, nbins, 1), (nrays, nbins, nclasses))
t_dist /= dist_total
del dist_total
# compute entroy
entropy = -np.ma.sum(
t_dist*np.ma.log(t_dist)/np.ma.log(nclasses), axis=-1)
entropy[mask] = np.ma.masked
t_dist *= 100.
return hydroclass, entropy, t_dist
def _compute_coeff_transform(mass_centers,
weights=np.array([1., 1., 1., 0.75, 0.5]),
value=50.):
"""
get the transformation coefficients
Parameters
----------
mass_centers : ndarray 2D
The centroids for each class and variable (nclasses, nvariables)
weights : array
optional. The weight given to each variable (nvariables)
value : float
parameter controlling the rate of decay of the distance transformation
Returns
-------
t_vals : ndarray 1D
The coefficients used to transform the distances to each centroid for
each class (nclasses)
"""
nclasses, nvariables = np.shape(mass_centers)
t_vals = np.empty((nclasses, nclasses), dtype=mass_centers.dtype)
for i in range(nclasses):
weights_mat = np.broadcast_to(
weights.reshape(1, nvariables), (nclasses, nvariables))
centroids_class = mass_centers[i, :]
centroids_class = np.broadcast_to(
centroids_class.reshape(1, nvariables), (nclasses, nvariables))
t_vals[i, :] = np.sqrt(
np.sum(weights_mat*np.power(
np.abs(centroids_class-mass_centers), 2.), axis=1))
# pick the second lowest value (the first is 0)
t_vals = np.sort(t_vals, axis=-1)[:, 1]
t_vals = np.log(value)/t_vals
return t_vals
def _get_mass_centers(freq):
"""
Get mass centers for a particular frequency.
Parameters
----------
freq : float
Radar frequency [Hz].
Returns
-------
mass_centers : ndarray 2D
The centroids for each variable and hydrometeor class in (nclasses,
nvariables).
"""
mass_centers_dict = _mass_centers_table()
freq_band = get_freq_band(freq)
if (freq_band is not None) and (freq_band in mass_centers_dict):
return mass_centers_dict[freq_band]
if freq < 4e9:
freq_band_aux = 'C'
elif freq > 12e9:
freq_band_aux = 'X'
mass_centers = mass_centers_dict[freq_band_aux]
warn('Radar frequency out of range. ' +
'Centroids only valid for C or X band. ' +
freq_band_aux + ' band centroids will be applied')
return mass_centers
def _mass_centers_table():
"""
Defines the mass centers look up table for each frequency band.
Returns
-------
mass_centers_dict : dict
A dictionary with the mass centers for each frequency band.
"""
nclasses = 9
nvariables = 5
mass_centers = np.zeros((nclasses, nvariables))
mass_centers_dict = dict()
# C-band centroids derived for MeteoSwiss Albis radar
# Zh ZDR kdp RhoHV delta_Z
mass_centers[0, :] = [13.5829, 0.4063, 0.0497, 0.9868, 1330.3] # DS
mass_centers[1, :] = [02.8453, 0.2457, 0.0000, 0.9798, 0653.8] # CR
mass_centers[2, :] = [07.6597, 0.2180, 0.0019, 0.9799, -1426.5] # LR
mass_centers[3, :] = [31.6815, 0.3926, 0.0828, 0.9978, 0535.3] # GR
mass_centers[4, :] = [39.4703, 1.0734, 0.4919, 0.9876, -1036.3] # RN
mass_centers[5, :] = [04.8267, -0.5690, 0.0000, 0.9691, 0869.8] # VI
mass_centers[6, :] = [30.8613, 0.9819, 0.1998, 0.9845, -0066.1] # WS
mass_centers[7, :] = [52.3969, 2.1094, 2.4675, 0.9730, -1550.2] # MH
mass_centers[8, :] = [50.6186, -0.0649, 0.0946, 0.9904, 1179.9] # IH/HDG
mass_centers_dict.update({'C': mass_centers})
# X-band centroids derived for MeteoSwiss DX50 radar
# Zh ZDR kdp RhoHV delta_Z
mass_centers[0, :] = [19.0770, 0.4139, 0.0099, 0.9841, 1061.7] # DS
mass_centers[1, :] = [03.9877, 0.5040, 0.0000, 0.9642, 0856.6] # CR
mass_centers[2, :] = [20.7982, 0.3177, 0.0004, 0.9858, -1375.1] # LR
mass_centers[3, :] = [34.7124, -0.3748, 0.0988, 0.9828, 1224.2] # GR
mass_centers[4, :] = [33.0134, 0.6614, 0.0819, 0.9802, -1169.8] # RN
mass_centers[5, :] = [08.2610, -0.4681, 0.0000, 0.9722, 1100.7] # VI
mass_centers[6, :] = [35.1801, 1.2830, 0.1322, 0.9162, -0159.8] # WS
mass_centers[7, :] = [52.4539, 2.3714, 1.1120, 0.9382, -1618.5] # MH
mass_centers[8, :] = [44.2216, -0.3419, 0.0687, 0.9683, 1272.7] # IH/HDG
mass_centers_dict.update({'X': mass_centers})
return mass_centers_dict
def _data_limits_table():
"""
Defines the data limits used in the standardization.
Returns
-------
dlimits_dict : dict
A dictionary with the limits for each variable.
"""
dlimits_dict = dict()
dlimits_dict.update({'Zh': (60., -10.)})
dlimits_dict.update({'ZDR': (5., -1.5)})
dlimits_dict.update({'KDP': (7., -10.)})
dlimits_dict.update({'RhoHV': (-5.23, -50.)})
return dlimits_dict
[docs]def get_freq_band(freq):
"""
Returns the frequency band name (S, C, X, ...).
Parameters
----------
freq : float
Radar frequency [Hz].
Returns
-------
freq_band : str
Frequency band name.
"""
if 2e9 <= freq < 4e9:
return 'S'
if 4e9 <= freq < 8e9:
return 'C'
if 8e9 <= freq <= 12e9:
return 'X'
warn('Unknown frequency band')
return None