Source code for towerpy.attc.attc_zhzdr

"""Towerpy: an open-source toolbox for processing polarimetric radar data."""

from pathlib import Path
import ctypes as ctp
import copy
import platform
import numpy as np
import numpy.ctypeslib as npct
from scipy import optimize
from ..base import TowerpyError
from ..utils.radutilities import find_nearest
from ..utils.radutilities import rolling_window
from ..utils.radutilities import maf_radial
from ..utils.radutilities import interp_nan
from ..utils.radutilities import fillnan1d
from ..datavis import rad_display
from ..ml.mlyr import MeltingLayer


[docs] class AttenuationCorrection: r""" A class to calculate the attenuation of the radar signal power. Attributes ---------- elev_angle : float Elevation angle at which the scan was taken, in deg. file_name : str Name of the file containing radar data. scandatetime : datetime Date and time of scan. site_name : str Name of the radar site. res_zdrcorrection : dict, opt Descriptor of the :math:`Z_{DR}` attenuation correction process. vars : dict Output of the :math:`Z_{H}` and/or :math:`Z_{DR}` attenuation correction process. """ def __init__(self, radobj=None):
[docs] self.elev_angle = getattr(radobj, 'elev_angle', None) if radobj else None
[docs] self.file_name = getattr(radobj, 'file_name', None) if radobj else None
[docs] self.scandatetime = getattr(radobj, 'scandatetime', None) if radobj else None
[docs] self.site_name = getattr(radobj, 'site_name', None) if radobj else None
[docs] def attc_phidp_prepro(self, rad_georef, rad_params, attvars, mov_avrgf_len=(1, 3), t_spdp=10, minthr_pdp0=-5, rhohv_min=0.90, phidp0_correction=False, mlyr=None): r""" Prepare :math:`\Phi_{DP}` for attenuation correction. Parameters ---------- rad_georef : dict Georeferenced data containing descriptors of the azimuth, gates and beam height, amongst others. rad_params : dict Radar technical details. attvars : dict Polarimetric variables used for the attenuation correction. mov_avrgf_len : 2-element tuple or list, optional Window size used to smooth :math:`\Phi_{DP}` by applying a moving average window. The default is (1, 3). It is recommended to average :math:`\Phi_{DP}` along the range, i.e. keep the window size in a (1, n) size. t_spdp: int or float, optional Discard bins with standard deviations of :math:`\Phi_{DP}` greater than the selected value. The default is 10 deg. minthr_pdp0: int or float, optional Tolerance for the true value of :math:`\Phi_{DP}(r0)`. Values below this threshold are removed. The default is -5 deg. rhohv_min: float, optional Threshold in :math:`\rho_{HV}` used to discard bins related to nonmeteorological signals. The default is 0.90 phidp0_correction : Bool, optional If True, adjust :math:`\Phi_{DP}(r0)` for each individual ray. mlyr : MeltingLayer Class, optional Filter and interpolate PhiDP within the melting layer. The 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. Notes ----- 1. This function smooths the total :math:`\Phi_{DP}` using the given window size, remove spurious values within the signal phase, etc. 2. :math:`\Phi_{DP}` must have been previously unfolded and offset (:math:`\Phi_{DP}(r0)`) corrected. """ ngates = rad_params['ngates'] attvars = copy.copy(attvars) if (mov_avrgf_len[1] % 2) == 0: print('Choose an odd number to apply the ' + 'moving average filter') phidp_O = {k: np.ones_like(attvars[k]) * attvars[k] for k in list(attvars) if k.startswith('Phi')} # Removes low-noisy values of PhiDP below the given PhiDP(0) phidp_O['PhiDP [deg]'][phidp_O['PhiDP [deg]'] < minthr_pdp0] = minthr_pdp0 # Filter isolated values phidp_pad = np.pad(phidp_O['PhiDP [deg]'], ((0, 0), (mov_avrgf_len[1]//2, mov_avrgf_len[1]//2)), mode='constant', constant_values=(np.nan)) phidp_dspk = np.array( [[np.nan if ~np.isnan(vbin) and (np.isnan(phidp_pad[nr][nbin-1]) and np.isnan(phidp_pad[nr][nbin+1])) else 1 for nbin, vbin in enumerate(phidp_pad[nr]) if nbin != 0 and nbin < phidp_pad.shape[1] - mov_avrgf_len[1] + 2] for nr in range(phidp_pad.shape[0])], dtype=np.float64) # Filter using rhoHV phidp_dspk[attvars['rhoHV [-]'] < rhohv_min] = np.nan # Computes sPhiDP for each ray phidp_dspk_rhv = phidp_O['PhiDP [deg]'] * phidp_dspk phidp_s = np.nanstd(rolling_window( phidp_dspk_rhv, mov_avrgf_len), axis=-1, ddof=1) phidp_pad = np.pad(phidp_s, ((0, 0), (mov_avrgf_len[1]//2, mov_avrgf_len[1]//2)), mode='constant', constant_values=(np.nan)) # Filter values with std values greater than std threshold phidp_sfnv = np.array( [[np.nan if vbin >= t_spdp and (phidp_pad[nr][nbin-1] >= t_spdp or phidp_pad[nr][nbin+1] >= t_spdp) else 1 for nbin, vbin in enumerate(phidp_pad[nr]) if nbin != 0 and nbin < phidp_pad.shape[1] - mov_avrgf_len[1] + 2] for nr in range(phidp_pad.shape[0])], dtype=np.float64) # Filter isolated values phidp_sfnv2 = np.array( [[np.nan if ~np.isnan(vbin) and (np.isnan(phidp_pad[nr][nbin-1]) or np.isnan(phidp_pad[nr][nbin+1])) else 1 for nbin, vbin in enumerate(phidp_pad[nr]) if nbin != 0 and nbin < phidp_pad.shape[1] - mov_avrgf_len[1] + 2] for nr in range(phidp_pad.shape[0])], dtype=np.float64) phidp_sfnv = phidp_sfnv*phidp_sfnv2 phidp_f = phidp_dspk_rhv * phidp_sfnv # Filter isolated values phidp_pad = np.pad(phidp_f, ((0, 0), (mov_avrgf_len[1]//2, mov_avrgf_len[1]//2)), mode='constant', constant_values=(np.nan)) phidp_f2 = np.array( [[np.nan if ~np.isnan(vbin) and (np.isnan(phidp_pad[nr][nbin-1]) or np.isnan(phidp_pad[nr][nbin+1])) else 1 for nbin, vbin in enumerate(phidp_pad[nr]) if nbin != 0 and nbin < phidp_pad.shape[1] - mov_avrgf_len[1] + 2] for nr in range(phidp_pad.shape[0])], dtype=np.float64) phidp_f = phidp_f * phidp_f2 # Computes initial PhiDP(0) if phidp0_correction: phidp0 = np.array([[ nr[np.isfinite(nr)][0] if ~np.isnan(nr).all() else 0 for nr in phidp_f]], dtype=np.float64).transpose() else: phidp0 = np.array([[1e-5 if ~np.isnan(nr).all() else 0 for nr in phidp_f]], dtype=np.float64).transpose() phidp0[phidp0 == 0] = np.nanmedian(phidp0[phidp0 != 0]) phidp0t = np.tile(phidp0, (1, mov_avrgf_len[1] + 1)) phidp_f[:, : mov_avrgf_len[1] + 1] = phidp0t # Add phidp0 phidp_f = np.array([nr - phidp0[cr] for cr, nr in enumerate(phidp_f)], dtype=np.float64) # Filter and interpolate ML if mlyr is not None: if not getattr(mlyr, 'mlyr_limits', None): mlyr.ml_ppidelimitation(rad_georef, rad_params) phidp_fml = np.where( mlyr.mlyr_limits['pcp_region [HC]'] == 2, np.nan, phidp_f) itprng = np.array(range(ngates), dtype=np.float64) phidp_fmli = np.array( [interp_nan(itprng, nr, nan_type='nan') if ~np.isnan(nr).all() else nr for nr in phidp_fml], dtype=np.float64) phidp_f = np.where(mlyr.mlyr_limits['pcp_region [HC]'] == 2, phidp_fmli, phidp_f) # Computes a MAV phidp_m = maf_radial( {'PhiDPi [deg]': phidp_f}, maf_len=mov_avrgf_len[1], maf_ignorenan=True, maf_extendvalid=False)['PhiDPi [deg]'] # Filter isolated values phidp_pad = np.pad(phidp_m, ((0, 0), (mov_avrgf_len[1]//2, mov_avrgf_len[1]//2)), mode='constant', constant_values=(np.nan)) phidp_f2 = np.array( [[np.nan if ~np.isnan(vbin) and (np.isnan(phidp_pad[nr][nbin-1]) or np.isnan(phidp_pad[nr][nbin+1])) else 1 for nbin, vbin in enumerate(phidp_pad[nr]) if nbin != 0 and nbin < phidp_pad.shape[1] - mov_avrgf_len[1] + 2] for nr in range(phidp_pad.shape[0])], dtype=np.float64) phidp_m = phidp_m * phidp_f2 # Interpolation and final filtering itprng = np.array(range(ngates), dtype=np.float64) phidp_i = {'PhiDPi [deg]': np.array( [interp_nan(itprng, nr, nan_type='nan') if ~np.isnan(nr).all() else nr for nr in phidp_m], dtype=np.float64)} phidp_i['PhiDPi [deg]'][:, : mov_avrgf_len[ 1] + 1] = phidp_f[:, : mov_avrgf_len[1] + 1] phidp_i = {'PhiDPi [deg]': np.array( [fillnan1d(i) for i in phidp_i['PhiDPi [deg]']], dtype=np.float64)} # Apply a moving average filter to the whole PPI phidp_maf = maf_radial( phidp_i, maf_len=mov_avrgf_len[1], maf_ignorenan=False, maf_extendvalid=True) # Filter values using ZH phidp_maf['PhiDPi [deg]'][np.isnan( attvars['ZH [dBZ]'])] = np.nan attvars['PhiDP [deg]'] = phidp_maf['PhiDPi [deg]'] self.vars = attvars
[docs] def zh_correction(self, rad_georef, attvars, cclass, mlyr=None, attc_method='ABRI', pdp_pxavr_rng=7, pdp_pxavr_azm=1, pdp_dmin=20, coeff_alpha=[0.020, 0.1, 0.073], coeff_a=[1e-5, 9e-5, 3e-5], coeff_b=[0.65, 0.85, 0.78], phidp0=0, niter=500, plot_method=False): r""" Calculate the attenuation of :math:`Z_{H}`. Parameters ---------- rad_georef : dict Georeferenced data containing descriptors of the azimuth, gates and beam height, amongst others. attvars : dict Polarimetric variables used for the attenuation correction. cclass : array Clutter, noise and meteorological echoes classification: 'pcpn' = 0, 'noise' = 3, 'clutter' = 5 mlyr : MeltingLayer Class, optional Melting layer class containing the top of the melting layer, (i.e., the melting level) and its thickness, in km. Only gates below the melting layer bottom (i.e. the rain region below the melting layer) are included in the computation; ml_top and ml_thickness can be either a single value (float, int), or an array (or list) of values corresponding to each azimuth angle of the scan. If None, the function is applied assuming ml_top=5km and ml_thickness=0.5 km. attc_method : str Attenuation correction algorithm to be used. The default is 'ABRI': [ABRI] = Bringi (optimised). [AFV] = Final value (optimised). [AHB] = Hitschfeld and Bordan (optimised). [ZPHI] = Testud (constant parameters). [BRI] = Bringi (constant parameters). [FV] = Final value (constant parameters). [HB] = Hitschfeld and Bordan (constant parameters). pdp_pxavr_rng : int Pixels to average in :math:`\Phi_{DP}` along range: Odd number equivalent to about 4km, i.e. 4km/range_resolution. The default is 7. pdp_pxavr_azm : int Pixels to average in :math:`\Phi_{DP}` along azimuth. Must be an odd number. The default is 1. pdp_dmin : float Minimum total :math:`\Delta\Phi_{DP}` expected in a ray to perform attenuation correction (at least 10-20 degrees). The default is 20. coeff_alpha : 3-element tuple or list, optional [Min, max, fixed value] of coeff :math:`\alpha`. These bounds are used to find the optimum value of :math:`\alpha` from :math:`A_H = \alpha K_{DP}`. Default values are [0.020, 0.1, 0.073], derived for C-band. coeff_a : 3-element tuple or list, optional [Min, max, fixed value] of coeff :math:`a`. These bounds are used to find the optimum value of :math:`a` from :math:`A_H = a Z_{H}^b`. Default values are [1e-5, 9e-5, 3e-5], derived for C-band. coeff_b : 3-element tuple or list, optional [Min, max, fixed value] of coeff :math:`b`. These bounds are used to find the optimum value of :math:`b` from :math:`A_H = a Z_{H}^b`. Default values are [0.65, 0.85, 0.78], derived for C-band. niter : int Number of iterations to find the optimised values of the coeffs :math:`a, b, \alpha`. The default is 500. phidp0 : int , float or None Adjusts the value (in deg) of :math:`\Phi_{DP}(r0)` for the whole scan. If None, the function computes the value of the offset by averaging the value of :math:`\Phi_{DP}` in the first ten consecutive bins classified as rain, according to the cclass. The default is 0. plot_method : Bool, optional Plot the ZH attenuation correction method. The default is False. Returns ------- vars : dict ZH [dBZ]: Corrected horizontal reflectivity AH [dB/km]: Specific horizontal attenuation PhiDP [deg]: Processed and adjusted differential phase PhiDP* [deg]: Computed differential phase: Its availability depends on the selected method. KDP [deg/km]: Specific differential phase, calculated using the equation :math:`K_{DP}=A_H/\alpha` PIA [dB]: Path-Integrated Attenuation, calculated using the equation :math:`PIA=\Phi_{DP}*\alpha` alpha: parameter :math:`\alpha` that represents the ratio :math:`A_H/K_{DP}` Notes ----- 1. The attenuation is computed up to a user-defined melting level height. 2. This function uses the shared object 'lnxlibattenuationcorrection' or the dynamic link library 'w64libattenuationcorrection' depending on the operating system (OS). 3. Based on the method described in [1]_ References ---------- .. [1] M. A. Rico-Ramirez, "Adaptive Attenuation Correction Techniques for C-Band Polarimetric Weather Radars," in IEEE Transactions on Geoscience and Remote Sensing, vol. 50, no. 12, pp. 5061-5071, Dec. 2012. https://doi.org/10.1109/TGRS.2012.2195228 """ array1d = npct.ndpointer(dtype=np.double, ndim=1, flags='CONTIGUOUS') array2d = npct.ndpointer(dtype=np.double, ndim=2, flags='CONTIGUOUS') if platform.system() == 'Linux': libac = npct.load_library("lnxlibattenuationcorrection.so", Path(__file__).parent.absolute()) elif platform.system() == 'Windows': libac = ctp.cdll.LoadLibrary(f'{Path(__file__).parent.absolute()}' '/w64libattenuationcorrection.dll') else: libac = None raise TowerpyError(f'The {platform.system} OS is not currently' 'compatible with this version of Towerpy') libac.attenuationcorrection.restype = None libac.attenuationcorrection.argtypes = [ctp.c_int, ctp.c_int, array2d, array2d, array2d, array2d, array2d, array1d, array1d, array1d, array1d, array2d, array2d, array2d, array2d, array2d] if mlyr is None: mlyr = MeltingLayer(self) mlyr.ml_top = 5 mlyr.ml_thickness = 0.5 mlyr.ml_bottom = mlyr.ml_top - mlyr.ml_thickness if isinstance(mlyr.ml_top, (int, float)): mlgrid = np.zeros_like( attvars['ZH [dBZ]']) + (mlyr.ml_top) * 1000 elif isinstance(mlyr.ml_top, (np.ndarray, list, tuple)): mlgrid = (np.ones_like( attvars['ZH [dBZ]'].T) * mlyr.ml_top * 1000).T param_atc = np.zeros(16) nrays = len(rad_georef['azim [rad]']) ngates = len(rad_georef['range [m]']) if attc_method == 'ABRI': param_atc[0] = 0 elif attc_method == 'AFV': param_atc[0] = 1 elif attc_method == 'AHB': param_atc[0] = 2 elif attc_method == 'ZPHI': param_atc[0] = 3 elif attc_method == 'BRI': param_atc[0] = 4 elif attc_method == 'FV': param_atc[0] = 5 elif attc_method == 'HB': param_atc[0] = 6 else: raise TowerpyError('Please select a valid attenuation correction' ' method') param_atc[1] = pdp_pxavr_rng param_atc[2] = pdp_pxavr_azm param_atc[3] = pdp_dmin param_atc[4] = coeff_a[2] # a_opt param_atc[5] = coeff_b[2] # b_opt param_atc[6] = coeff_alpha[2] # alpha_opt param_atc[7] = coeff_a[0] # mina param_atc[8] = coeff_a[1] # maxa param_atc[9] = coeff_b[0] # minb param_atc[10] = coeff_b[1] # maxb param_atc[11] = coeff_alpha[0] # minalpha param_atc[12] = coeff_alpha[1] # maxalpha param_atc[13] = niter # number of iterations param_atc[14] = mlyr.ml_thickness * 1000 # BB thickness in meters if isinstance(phidp0, (int, float)): param_atc[15] = phidp0 else: param_atc[15] = -999 # PhiDP offset zhh_Ac = np.full(attvars['ZH [dBZ]'].shape, np.nan) Ah = np.full(attvars['ZH [dBZ]'].shape, np.nan) phidp_m = np.full(attvars['ZH [dBZ]'].shape, np.nan) phidp_c = np.full(attvars['ZH [dBZ]'].shape, np.nan) alpha = np.full(attvars['ZH [dBZ]'].shape, np.nan) libac.attenuationcorrection( nrays, ngates, attvars['ZH [dBZ]'], attvars['PhiDP [deg]'], attvars['rhoHV [-]'], mlgrid, cclass, rad_georef['range [m]'], rad_georef['azim [rad]'], rad_georef['elev [rad]'], param_atc, zhh_Ac, Ah, phidp_m, phidp_c, alpha) attcorr = {'ZH [dBZ]': zhh_Ac, 'AH [dB/km]': Ah, 'alpha [-]': alpha, 'PhiDP [deg]': phidp_m, 'PhiDP* [deg]': phidp_c} attcorr['PIA [dB]'] = attcorr['PhiDP [deg]'] * attcorr['alpha [-]'] attcorr['PIA [dB]'][attcorr['PIA [dB]'] < 0] = 0 attcorr['AH [dB/km]'][attcorr['AH [dB/km]'] < 0] = 0 attcorr['KDP [deg/km]'] = np.nan_to_num( attcorr['AH [dB/km]'] / attcorr['alpha [-]']) piacopy = np.zeros_like(attcorr['PIA [dB]']) + attcorr['PIA [dB]'] for i in range(nrays): idmx = np.nancumsum(piacopy[i]).argmax() if idmx != 0: attcorr['PIA [dB]'][i][idmx+1:] = attcorr['PIA [dB]'][i][idmx] # ===================================================================== # Filter non met values # ===================================================================== for key, values in attcorr.items(): if np.array(values).ndim == 2: values[cclass != 0] = np.nan self.vars = attcorr self.phidp_offset = param_atc[15] if plot_method: rad_params = {} if self.elev_angle: rad_params['elev_ang [deg]'] = self.elev_angle else: rad_params['elev_ang [deg]'] = 'surveillance scan' if self.scandatetime: rad_params['datetime'] = self.scandatetime else: rad_params['datetime'] = None rad_display.plot_zhattcorr( rad_georef, rad_params, attvars, attcorr, mlyr=mlyr, vars_bounds={'alpha [-]': [0, round((coeff_alpha[1]+(coeff_alpha[1]*.1))*100, -1)/100, 11]})
[docs] def zdr_correction(self, rad_georef, attvars, attcorr_vars, cclass, mlyr=None, attc_method='BRI', coeff_beta=[0.008, 0.1, 0.04], beta_alpha_ratio=0.265, rhv_thld=0.985, mov_avrgf_len=9, minbins=10, p2avrf=5, zh_zdr_model='linear', rparams=None, descr=False, plot_method=False): r""" Calculate the attenuation of :math:`Z_{DR}`. Parameters ---------- rad_georef : dict Georeferenced data containing descriptors of the azimuth, gates and beam height. attvars : dict Polarimetric variables to be corrected for attenuation. attcorr_vars : dict Attenuation-corrected polarimetric variables used for calculations. cclass : array Clutter, noise and meteorological echoes classification: 'pcpn' = 0, 'noise' = 3, 'clutter' = 5 mlyr : MeltingLayer Class, optional Melting layer class containing the top of the melting layer, (i.e., the melting level) and its thickness, in km. Only gates below the melting layer bottom (i.e. the rain region below the melting layer) are included in the computation; ml_top and ml_thickness can be either a single value (float, int), or an array (or list) of values corresponding to each azimuth angle of the scan. If None, the function is applied assuming ml_top=5km and ml_thickness=0.5 km. attc_method : str Attenuation correction algorithm to be used. The default is 'BRI': [ABRI] = Bringi (optimised beta parameter). [BRI] = Bringi (constant beta/alpha ratio). coeff_beta : 3-element tuple or list, optional [Min, max, fixed value] of coeff :math:`\beta`. These bounds are used to find the optimum value of :math:`\beta`. Default values are [0.002, 0.04, 0.02], derived for C-band. beta_alpha_ratio : float, opt Quotient between :math:`\alpha` and :math:`\beta` parameters from :math:`A_{DP} = ( \beta / \alpha )A_{H}`. The default is 0.265 for C-band. rhv_thld : float Minimum value of :math:`\rho_{HV}` expected in the rain medium. The default is 0.98. mov_avrgf_len : int Odd number used to apply a moving average filter to each beam and smooth the signal. The default is 5. minbins : int Minimum number of bins related to the length of each rain cell along the beam. The default is 10. p2avrf : int Number of bins to average on the far side of the rain cell. The default is 3. zh_zdr_model : str Method used to compute the ZH-ZDR relationship. The "linear" model uses the relation provided in [2]_ and [4]_, whereas the "exp" model uses the relation proposed by [3]_. rparams: dict Additional parameters describing the ZH-ZDR relationship: For the linear model: ZH_lower_lim: 20 dBZ, ZH_upper_lim: 45 dBZ, coeff_a: 0.048, coeff_b: 0.774, zdr_max: 1.4 .. math:: \overline{Z}_{DR} = \Biggl\{ 0 \rightarrow \overline{Z_H}(r_m)<=Z_H(lowerlim) \\ a*Z_H-b \rightarrow Z_H(lowerlim)<Z_H(r_m)<=Z_H(upperlim) \\ Z_{DR}(max) \rightarrow Z_H(r_m)>Z_H(upperlim) \Biggl\} For the exp model: coeff_a: 0.00012, coeff_b: 2.5515 .. math:: \overline{Z}_{DR} = \Biggl\{ a*Z_H^{b} \Biggl\} descr : bool Controls if the statistics of the calculations are returned. The default is False. plot_method : Bool, optional Plot the ZDR attenuation correction method. The default is False. Returns ------- vars : dict ZDR [dB]: Attenuation-corrected differential reflectivity. ADP [dB/km]': Specific differential attenuation. beta: parameter :math:`\beta` optimised for each beam. Notes ----- 1. The attenuation is computed up to a user-defined melting level height. 2. The ZDR attenuation correction method assumes that ZH has first been corrected for attenuation, e.g., using the methods described in [1]_ References ---------- .. [1] M. A. Rico-Ramirez, "Adaptive Attenuation Correction Techniques for C-Band Polarimetric Weather Radars," in IEEE Transactions on Geoscience and Remote Sensing, vol. 50, no. 12, pp. 5061-5071, Dec. 2012. https://doi.org/10.1109/TGRS.2012.2195228 .. [2] V. N. Bringi, T. D. Keenan and V. Chandrasekar, "Correcting C-band radar reflectivity and differential reflectivity data for rain attenuation: a self-consistent method with constraints," in IEEE Transactions on Geoscience and Remote Sensing, vol. 39, no. 9, pp. 1906-1915, Sept. 2001, https://doi.org/10.1109/36.951081 .. [3] Gou, Y., Chen, H. and Zheng, J., 2019. "An improved self-consistent approach to attenuation correction for C-band polarimetric radar measurements and its impact on quantitative precipitation estimation", in Atmospheric Research, 226, pp.32-48. https://doi.org/10.1016/j.atmosres.2019.03.006 .. [4] Park, S., Bringi, V. N., Chandrasekar, V., Maki, M., & Iwanami, K. (2005). Correction of Radar Reflectivity and Differential Reflectivity for Rain Attenuation at X Band. Part I: Theoretical and Empirical Basis, Journal of Atmospheric and Oceanic Technology, 22(11), 1621-1632. https://doi.org/10.1175/JTECH1803.1 """ # ===================================================================== # Parameter setup # ===================================================================== params = {'ZH-ZDR model': zh_zdr_model, 'ZH_lower_lim': 20, 'ZH_upper_lim': 45, 'model': 'coeff_a*ZH-coeff_b', 'zdr_max': 1.4, 'coeff_a': 0.048, 'coeff_b': 0.774} if zh_zdr_model == 'exp': params['ZH-ZDR model'] = zh_zdr_model params['model'] = 'coeff_a*ZH^coeff_b' params['coeff_a'] = 0.00012 params['coeff_b'] = 2.5515 if rparams is not None: params.update(rparams) if mlyr is None: mlyr = MeltingLayer(self) mlyr.ml_top = 5 mlyr.ml_thickness = 0.5 mlyr.ml_bottom = mlyr.ml_top - mlyr.ml_thickness else: mlyr.ml_bottom = mlyr.ml_top - mlyr.ml_thickness if isinstance(mlyr.ml_bottom, (int, float)): mlb_idx = [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 = [find_nearest(nbh, mlyr.ml_bottom[cnt]) for cnt, nbh in enumerate(rad_georef['beam_height [km]'])] nrays = len(rad_georef['azim [rad]']) mlb_mask = np.full_like(attcorr_vars['ZH [dBZ]'], 0.0, dtype=float) for n, rows in enumerate(mlb_mask): rows[mlb_idx[n]+1:] = np.nan mlb_mask[cclass != 0] = np.nan mlb_mask[:, 0] = np.nan attcorrmask = {keys: np.ma.masked_array(values, mlb_mask) for keys, values in attcorr_vars.items()} attcorrmask['rhoHV [-]'] = np.ma.masked_array(attvars['rhoHV [-]'], mlb_mask) attcorrmask['ZDR [dB]'] = np.ma.masked_array(attvars['ZDR [dB]'], mlb_mask) idxzdrattcorr = [i for i in range(nrays) if any(attcorr_vars['alpha [-]'][i, :] > 0)] zdrattcorr, adpif, betaof, zdrstat = [], [], [], [] alphacopy = np.array(attcorr_vars['alpha [-]'], copy=True) if attc_method == 'ABRI': bracket = coeff_beta[:2] elif attc_method == 'BRI': bracket = [] # ===================================================================== # Ray loop for attc # ===================================================================== for i in range(nrays): if i in idxzdrattcorr: idmx = np.nancumsum(alphacopy[i]).argmax() if idmx != 0: alphacopy[i][idmx+1:] = alphacopy[i][idmx] zdr_ibeam = (np.ones_like(attcorrmask['ZDR [dB]'][i, :]) * attcorrmask['ZDR [dB]'][i, :]) zh_ibeam = (np.ones_like(attcorrmask['ZH [dBZ]'][i, :]) * attcorrmask['ZH [dBZ]'][i, :]) zdr_fltr_rhv = np.ma.masked_where( attcorrmask['rhoHV [-]'][i, :] < rhv_thld, zdr_ibeam) zh_fltr_rhv = np.ma.masked_where( attcorrmask['rhoHV [-]'][i, :] < rhv_thld, zh_ibeam) zdr_mvavfl = (np.ma.convolve(zdr_fltr_rhv, np.ones(mov_avrgf_len) / mov_avrgf_len, mode='same')) zh_mvavfl = (np.ma.convolve(zh_fltr_rhv, np.ones(mov_avrgf_len) / mov_avrgf_len, mode='same')) rcells_idx = np.array( sorted( np.vstack((np.argwhere( np.diff(np.isnan(zdr_mvavfl).mask)), np.argwhere(np.diff(np.isnan(zdr_mvavfl).mask))+1, np.array([[0], [len(zdr_mvavfl)-1]]))))) rcells_raw = [rcells_idx[i:i + 2] for i in range(0, len(rcells_idx), 2)][1::2] if rcells_raw: rcells_dm1 = np.concatenate( [np.array(range(int(i[0].item()), int(i[-1].item())+1)) for i in rcells_raw]) rcells_dm2 = np.split( rcells_dm1, np.where(np.diff(rcells_dm1) > 1)[0] + 1) rcells_crc = [np.array([i[0], i[-1]]).reshape((2, 1)) for i in rcells_dm2] rcells_crc_len = np.concatenate( [np.ediff1d(i)+1 if np.ediff1d(i)+1 >= minbins else [np.nan] for i in rcells_crc]) raincells = [j for i, j in enumerate(rcells_crc) if not np.isnan(rcells_crc_len[i])] if raincells: raincells_len = np.concatenate([np.ediff1d(i)+1 for i in raincells]) rcell = raincells[np.nanargmax(raincells_len)] idxrs = int(rcell[0].item()) idxrf = int(rcell[1].item()) zhcrf = np.nanmean(zh_mvavfl[idxrf - p2avrf+1:idxrf+1]) zdrmrf = np.nanmean( zdr_mvavfl[idxrf - p2avrf+1:idxrf+1]) if params['ZH-ZDR model'] == 'linear': if zhcrf <= params['ZH_lower_lim']: zdrerf = 0 elif (params['ZH_lower_lim'] < zhcrf <= params['ZH_upper_lim']): zdrerf = params['coeff_a'] * zhcrf - params['coeff_b'] elif zhcrf > params['ZH_upper_lim']: zdrerf = params['zdr_max'] else: zdrerf = np.nan elif params['ZH-ZDR model'] == 'exp': zdrerf = params['coeff_a']*zhcrf**params['coeff_b'] else: raise TowerpyError('Please check the method ' 'selected for estimating the ' 'theoretical values of ZDR') if zdrerf > zdrmrf: try: betai = ( abs(zdrmrf - zdrerf) / (attcorrmask['PhiDP [deg]'][i, :][idxrf] - attcorrmask['PhiDP [deg]'][i, :][idxrs])) zdrirfpia = ( zdrmrf + (betai / attcorrmask['alpha [-]'][i, :][idxrf]) * np.nanmean(attcorrmask ['PIA [dB]'][i, :] [idxrf-p2avrf+1:idxrf+1])) if abs(zdrirfpia - zdrerf) > 0: sl1 = optimize.root_scalar( lambda betaif: ( (zdrmrf) + (betaif) * ((1 / attcorrmask['alpha [-]'][i, :][idxrf]) * np.nanmean( attcorrmask['PIA [dB]'][i, :] [idxrf-p2avrf+1:idxrf+1]))) - zdrerf, bracket=bracket, x0=(np.nanmin(alphacopy[i]) * beta_alpha_ratio), method='brentq') betao = sl1.root else: betao = betai if betao <= coeff_beta[0]: betao = coeff_beta[0] if betao >= coeff_beta[1]: betao = coeff_beta[1] if np.isnan(betao): betao = coeff_beta[2] betaopt = (np.zeros_like( attcorr_vars['alpha [-]'][i, :]) + betao) zdrcr = ((attvars['ZDR [dB]'][i, :]) + ((betao/alphacopy[i, :]) * attcorr_vars['PIA [dB]'][i, :])) adpi = ((betao / attcorr_vars['alpha [-]'][i, :]) * attcorr_vars['AH [dB/km]'][i, :]) statzdr = f'{i}: beta coeff optimised 1 iter' except ValueError: idxrs = int(raincells[0][0].item()) idxrf = int(raincells[-1][-1].item()) zhcrf = np.nanmean(zh_mvavfl[idxrf-p2avrf+1: idxrf+1]) zdrmrf = np.nanmean(zdr_mvavfl[idxrf-p2avrf+1: idxrf+1]) if params['ZH-ZDR model'] == 'linear': if zhcrf <= params['ZH_lower_lim']: zdrerf = 0 elif (params['ZH_lower_lim'] < zhcrf <= params['ZH_upper_lim']): zdrerf = (params['coeff_a'] * zhcrf - params['coeff_b']) elif zhcrf > params['ZH_upper_lim']: zdrerf = params['zdr_max'] else: zdrerf = np.nan elif params['ZH-ZDR model'] == 'exp': zdrerf = params['coeff_a']*zhcrf**params['coeff_b'] else: raise TowerpyError('Please check the method ' 'selected for estimating the ' 'theoretical values of ZDR') try: betai = ( abs(zdrmrf - zdrerf) / (attcorrmask['PhiDP [deg]'][i, :][idxrf] - attcorrmask['PhiDP [deg]'][i, :][idxrs])) zdrirfpia = ( zdrmrf + (betai / attcorrmask['alpha [-]'][i, :][idxrf]) * np.nanmean( attcorrmask['PIA [dB]'][i, :] [idxrf-p2avrf+1:idxrf+1])) if abs(zdrirfpia - zdrerf) > 0: sl1 = optimize.root_scalar( lambda betaif: ((zdrmrf) + (betaif) * ((1 / attcorrmask['alpha [-]'][i, :][idxrf]) * np.nanmean(attcorrmask['PIA [dB]'][i, :][idxrf - p2avrf+1:idxrf+1]))) - zdrerf, bracket=bracket, x0=(np.nanmin(alphacopy[i]) * beta_alpha_ratio), method='brentq') betao = sl1.root else: betao = betai if betao <= coeff_beta[0]: betao = coeff_beta[0] if betao >= coeff_beta[1]: betao = coeff_beta[1] if np.isnan(betao): betao = coeff_beta[2] zdrcr = ( (attvars['ZDR [dB]'][i, :]) + ((betao/alphacopy[i, :]) * attcorr_vars['PIA [dB]'][i, :])) adpi = ((betao / attcorr_vars['alpha [-]'][i, :]) * attcorr_vars['AH [dB/km]'][i, :]) betaopt = (np.zeros_like( attcorr_vars['alpha [-]'][i, :]) + betao) statzdr = f'{i}: beta coeff optimised 2 iter' except ValueError: zdrcr = ( (attvars['ZDR [dB]'][i, :]) + (beta_alpha_ratio * attcorr_vars['PIA [dB]'][i, :])) adpi = (beta_alpha_ratio * attcorr_vars['AH [dB/km]'][i, :]) betaopt = (attcorr_vars['alpha [-]'][i, :] * beta_alpha_ratio) statzdr = f'{i}: beta/alpha: fixed value' else: zdrcr = ((attvars['ZDR [dB]'][i, :]) + (beta_alpha_ratio * attcorr_vars['PIA [dB]'][i, :])) adpi = (beta_alpha_ratio * attcorr_vars['AH [dB/km]'][i, :]) betaopt = (attcorr_vars['alpha [-]'][i, :] * beta_alpha_ratio) statzdr = f'{i}: beta/alpha: fixed value' else: zdrcr = ( (attvars['ZDR [dB]'][i, :]) + (beta_alpha_ratio * attcorr_vars['PIA [dB]'][i, :])) adpi = (beta_alpha_ratio * attcorr_vars['AH [dB/km]'][i, :]) betaopt = (attcorr_vars['alpha [-]'][i, :] * beta_alpha_ratio) statzdr = f'{i}: beta/alpha: fixed value' else: # No segments found zdrcr = ( (attvars['ZDR [dB]'][i, :]) + (beta_alpha_ratio * attcorr_vars['PIA [dB]'][i, :])) adpi = beta_alpha_ratio * attcorr_vars['AH [dB/km]'][i, :] betaopt = (attcorr_vars['alpha [-]'][i, :] * beta_alpha_ratio) statzdr = f'{i}: beta/alpha: fixed value' else: # Ray with no positive alpha zdrcr = ( (attvars['ZDR [dB]'][i, :]) + (beta_alpha_ratio * attcorr_vars['PIA [dB]'][i, :])) adpi = beta_alpha_ratio * attcorr_vars['AH [dB/km]'][i, :] betaopt = attcorr_vars['alpha [-]'][i, :] * beta_alpha_ratio statzdr = f'{i}: beta/alpha: fixed value' zdrattcorr.append(zdrcr) adpif.append(adpi) betaof.append(betaopt) zdrstat.append(statzdr) attcorr1 = {'ZDR [dB]': np.array(zdrattcorr), 'ADP [dB/km]': np.array(adpif), 'beta [-]': np.array(betaof)} for n, rows in enumerate(attcorr1['ADP [dB/km]']): rows[mlb_idx[n]+1:] = 0 for n, rows in enumerate(attcorr1['beta [-]']): rows[mlb_idx[n]+1:] = 0 # ===================================================================== # Filter non met values # ===================================================================== for key, values in attcorr1.items(): values[cclass != 0] = np.nan zdr_calc = {} if descr is not False: zdr_calc['descriptor'] = [j for i, j in enumerate(zdrstat) if i in idxzdrattcorr] self.res_zdrcorrection = zdr_calc self.vars |= attcorr1 if plot_method: rad_params = {} if self.elev_angle: rad_params['elev_ang [deg]'] = self.elev_angle else: rad_params['elev_ang [deg]'] = 'surveillance scan' if self.scandatetime: rad_params['datetime'] = self.scandatetime else: rad_params['datetime'] = None rad_display.plot_zdrattcorr(rad_georef, rad_params, attvars, attcorr1, mlyr=mlyr)