def test_trapezoid(self): data = np.arange(0.0, 30.1, 0.1) correct = np.arange(0.0, 1.0, 0.01) correct = np.concatenate((correct, np.ones(101), correct[::-1])) result = util.trapezoid(data, 0.0, 10.0, 20.0, 30.0) np.testing.assert_array_almost_equal(result, correct, decimal=9)
def classify_echo_fuzzy(dat, weights=None, trpz=None, thresh=0.5): """Fuzzy echo classification and clutter identification based on \ polarimetric moments. The implementation is based on :cite:`Vulpiani2012`. At the moment, it only distinguishes between meteorological and non-meteorological echos. .. versionchanged:: 1.4.0 The implementation was extended using depolarization ratio (dr) and clutter phase alignment (cpa). For Clutter Phase Alignment (CPA) see :cite:`Hubbert2009a` and :cite:`Hubbert2009b` For each decision variable and radar bin, the algorithm uses trapezoidal functions in order to define the membership to the non-meteorological echo class. Based on pre-defined weights, a linear combination of the different degrees of membership is computed. The echo is assumed to be non-meteorological in case the linear combination exceeds a threshold. At the moment, the following decision variables are considered: - Texture of differential reflectivity (zdr) (mandatory) - Texture of correlation coefficient (rho) (mandatory) - Texture of differential propagation phase (phidp) (mandatory) - Doppler velocity (dop) (mandatory) - Static clutter map (map) (mandatory) - Correlation coefficient (rho2) (additional) - Depolarization Ratio (dr), computed from correlation coefficient & differential reflectivity (additional) - clutter phase alignment (cpa) (additional) Parameters ---------- dat : dict dictionary of arrays. Contains the data of the decision variables. The shapes of the arrays should be (..., number of beams, number of gates) and the shapes need to be identical or be broadcastable. weights : dict dictionary of floats. Defines the weights of the decision variables. trpz : dict dictionary of lists of floats. Contains the arguments of the trapezoidal membership functions for each decision variable thresh : float Threshold below which membership in non-meteorological membership class is assumed. Returns ------- output : tuple a tuple of two boolean arrays of same shape as the input arrays The first array boolean array indicates non-meteorological echos based on the fuzzy classification. The second boolean array indicates where all the polarimetric moments had missing values which could be used as an additional information criterion. See Also -------- :func:`~wradlib.dp.texture` - texture :func:`~wradlib.dp.depolarization` - depolarization ratio """ # Check the inputs # mandatory data keys dkeys = ["zdr", "rho", "phi", "dop", "map"] # usable wkeys wkeys = ["zdr", "rho", "phi", "dop", "map", "rho2", "dr", "cpa"] # usable tkeys tkeys = ["zdr", "rho", "phi", "dop", "map", "rho2", "dr", "cpa"] # default weights weights_default = {"zdr": 0.4, "rho": 0.4, "phi": 0.1, "dop": 0.1, "map": 0.5, "rho2": 0.4, "dr": 0.4, "cpa": 0.4} if weights is None: weights = weights_default else: weights = dict(list(weights_default.items()) + list(weights.items())) # default trapezoidal membership functions trpz_default = {"zdr": [0.7, 1.0, 9999, 9999], "rho": [0.1, 0.15, 9999, 9999], "phi": [15, 20, 10000, 10000], "dop": [-0.2, -0.1, 0.1, 0.2], "map": [1, 1, 9999, 9999], "rho2": [-9999, -9999, 0.95, 0.98], "dr": [-20, -12, 9999, 9999], "cpa": [0.6, 0.9, 9999, 9999]} if trpz is None: trpz = trpz_default else: trpz = dict(list(trpz_default.items()) + list(trpz.items())) # check data conformity assert np.all(np.in1d(dkeys, list(dat.keys()))), \ "Argument dat of classify_echo_fuzzy must be a dictionary " \ "with mandatory keywords %r." % (dkeys,) assert np.all(np.in1d(wkeys, list(weights.keys()))), \ "Argument weights of classify_echo_fuzzy must be a dictionary " \ "with keywords %r." % (wkeys,) assert np.all(np.in1d(tkeys, list(trpz.keys()))), \ "Argument trpz of classify_echo_fuzzy must be a dictionary " \ "with keywords %r." % (tkeys,) # copy rho to rho2 dat['rho2'] = dat['rho'].copy() shape = None for key in dkeys: if not dat[key] is None: if shape is None: shape = dat[key].shape else: assert dat[key].shape[-2:] == shape[-2:], \ "Arrays of the decision variables have inconsistent " \ "shapes: %r vs. %r" % (dat[key].shape, shape) else: print("WARNING: Missing decision variable: %s" % key) # If all dual-pol moments are NaN, can we assume that and echo is # non-meteorological? # Successively identify those bins where all moments are NaN nmom = ['rho', 'zdr', 'phi', 'dr', 'cpa'] # 'dop' nan_mask = np.isnan(dat['rho']) for mom in nmom[1:]: try: nan_mask &= np.isnan(dat[mom]) except KeyError: pass # Replace missing data by NaN dummy = np.zeros(shape) * np.nan for key in dat.keys(): if dat[key] is None: dat[key] = dummy # membership in meteorological class for each variable qres = dict() for key in dat.keys(): if key not in tkeys: continue if key in ['zdr', 'rho', 'phi']: d = dp.texture(dat[key]) else: d = dat[key] qres[key] = 1. - util.trapezoid(d, trpz[key][0], trpz[key][1], trpz[key][2], trpz[key][3]) # create weight arrays which are zero where the data is NaN # This way, each pixel "adapts" to the local data availability wres = dict() for key in dat.keys(): if key not in wkeys: continue wres[key] = _weight_array(qres[key], weights[key]) # Membership in meteorological class after combining all variables qsum = [] wsum = [] for key in dat.keys(): if key not in wkeys: continue # weighted sum, also removing NaN from data qsum.append(np.nan_to_num(qres[key]) * wres[key]) wsum.append(wres[key]) q = np.array(qsum).sum(axis=0) / np.array(wsum).sum(axis=0) # flag low quality return np.where(q < thresh, True, False), nan_mask
def test_trapezoid(self): data = np.arange(0., 30.1, 0.1) correct = np.arange(0., 1., 0.01) correct = np.concatenate((correct, np.ones(101), correct[::-1])) result = util.trapezoid(data, 0., 10., 20., 30.) np.testing.assert_array_almost_equal(result, correct, decimal=9)