Example #1
0
 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)
Example #2
0
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
Example #3
0
 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)