pyart.retrieve.echo_class — pyart-mch 0.4.1 documentation

# Source code for pyart.retrieve.echo_class

    """
    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

    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')}

    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

    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