def _find_rain_from_radar_echo(z: np.ndarray, time: np.ndarray, time_buffer: int = 5) -> np.ndarray: """Find profiles affected by rain. Rain is present in such profiles where the radar echo in the third range gate is > 0 dB. To make sure we do not include any rainy profiles, we also flag a few profiles before and after detections as raining. Args: z: Radar echo. time: Time vector. time_buffer: Time in minutes. Returns: 1D Boolean array denoting profiles with rain. """ is_rain = ma.array(z[:, 3] > 0, dtype=bool).filled(False) is_rain = skimage.morphology.remove_small_objects( is_rain, 2, connectivity=1) # Filter hot pixels n_profiles = len(time) n_steps = utils.n_elements(time, time_buffer, "time") for ind in np.where(is_rain)[0]: ind1 = max(0, ind - n_steps) ind2 = min(ind + n_steps, n_profiles) is_rain[ind1:ind2 + 1] = True return is_rain
def correct_liquid_top(obs: ClassData, liquid: dict, is_freezing: np.ndarray, limit: float = 200) -> np.ndarray: """Corrects lidar detected liquid cloud top using radar data. Args: obs: The :class:`ClassData` instance. liquid: Dictionary about liquid clouds including `tops` and `presence`. is_freezing: 2-D boolean array of sub-zero temperature, derived from the model temperature and melting layer based on radar data. limit: The maximum correction distance (m) above liquid cloud top. Returns: Corrected liquid cloud array. References: Hogan R. and O'Connor E., 2004, https://bit.ly/2Yjz9DZ. """ is_liquid_corrected = np.copy(liquid["presence"]) top_above = utils.n_elements(obs.height, limit) for prof, top in zip(*np.where(liquid["tops"])): ind = _find_ind_above_top(is_freezing[prof, top:], top_above) rad = obs.z[prof, top:top + ind + 1] if not (rad.mask.all() or ~rad.mask.any()): first_masked = ma.where(rad.mask)[0][0] is_liquid_corrected[prof, top:top + first_masked] = True return is_liquid_corrected
def _find_rain(z: np.ndarray, time: np.ndarray, time_buffer: Optional[int] = 5) -> np.ndarray: """Find profiles affected by rain. Rain is present in such profiles where the radar echo in the third range gate is > 0 dB. To make sure we do not include any rainy profiles, we also flag a few profiles before and after detections as raining. Args: z: Radar echo. time: Time vector. time_buffer: Time in minutes. Returns: 1D Boolean array denoting profiles with rain. """ is_rain = ma.array(z[:, 3] > 0, dtype=bool).filled(False) n_profiles = len(time) n_steps = utils.n_elements(time, time_buffer, 'time') for ind in np.where(is_rain)[0]: ind1 = max(0, ind - n_steps) ind2 = min(ind + n_steps, n_profiles) is_rain[ind1:ind2 + 1] = True return is_rain
def correct_liquid_top(obs, liquid, is_freezing, limit=200): """Corrects lidar detected liquid cloud top using radar data. Args: obs (ClassData): The :class:`ClassData` instance. liquid (dict): Dictionary about liquid clouds including `tops` and `presence`. is_freezing (ndarray): 2-D boolean array of sub-zero temperature, derived from the model temperature and melting layer based on radar data. limit (float): The maximum correction distance (m) above liquid cloud top. Returns: ndarray: Corrected liquid cloud array. """ is_liquid_corrected = np.copy(liquid['presence']) top_above = utils.n_elements(obs.height, limit) for prof, top in zip(*np.where(liquid['tops'])): ind = _find_ind_above_top(is_freezing[prof, top:], top_above) rad = obs.z[prof, top:top + ind + 1] if not (rad.mask.all() or ~rad.mask.any()): first_masked = ma.where(rad.mask)[0][0] is_liquid_corrected[prof, top:top + first_masked] = True return is_liquid_corrected
def find_freezing_region(obs: ClassData, melting_layer: np.ndarray) -> np.ndarray: """Finds freezing region using the model temperature and melting layer. Every profile that contains melting layer, subzero region starts from the mean melting layer height. If there are (long) time windows where no melting layer is present, model temperature is used in the middle of the time window. Finally, the subzero altitudes are linearly interpolated for all profiles. Args: obs: The :class:`ClassData` instance. melting_layer: 2-D boolean array denoting melting layer. Returns: 2-D boolean array denoting the sub-zero region. Notes: It is not clear how model temperature and melting layer should be ideally combined to determine the sub-zero region. This current method differs slightly from the original Matlab code and should be validated more carefully later. """ is_freezing = np.zeros(obs.tw.shape, dtype=bool) t0_alt = _find_t0_alt(obs.tw, obs.height) mean_melting_alt = _find_mean_melting_alt(obs, melting_layer) if _is_all_freezing(mean_melting_alt, t0_alt, obs.height): logging.info( "All temperatures below freezing and no detected melting layer") return np.ones(obs.tw.shape, dtype=bool) freezing_alt = ma.copy(mean_melting_alt) for ind in (0, -1): freezing_alt[ind] = mean_melting_alt[ind] or t0_alt[ind] win = utils.n_elements(obs.time, 240, "time") # 4h window mid_win = int(win / 2) for n in range(len(obs.time) - win): if mean_melting_alt[n:n + win].mask.all(): freezing_alt[n + mid_win] = t0_alt[n + mid_win] ind = ~freezing_alt.mask f = interp1d(obs.time[ind], freezing_alt[ind]) freezing_alt_interpolated = f(obs.time) - 1 for ii, alt in enumerate(freezing_alt_interpolated): is_freezing[ii, obs.height > alt] = True return is_freezing
def find_freezing_region(obs, melting_layer): """Finds freezing region using the model temperature and melting layer. Every profile that contains melting layer, subzero region starts from the mean melting layer height. If there are (long) time windows where no melting layer is present, model temperature is used in the middle of the time window. Finally, the subzero altitudes are linearly interpolated for all profiles. Args: obs (ClassData): The :class:`ClassData` instance. melting_layer (ndarray): 2-D boolean array denoting melting layer. Returns: ndarray: 2-D boolean array denoting the sub-zero region. Notes: It is not clear how model temperature and melting layer should be ideally combined to determine the sub-zero region. """ is_freezing = np.zeros(obs.tw.shape, dtype=bool) t0_alt = _find_t0_alt(obs.tw, obs.height) mean_melting_alt = _find_mean_melting_alt(obs, melting_layer) freezing_alt = ma.copy(mean_melting_alt) for ind in (0, -1): freezing_alt[ind] = mean_melting_alt[ind] or t0_alt[ind] win = utils.n_elements(obs.time, 240, 'time') # 4h window mid_win = int(win / 2) for n in range(len(obs.time) - win): if mean_melting_alt[n:n + win].mask.all(): freezing_alt[n + mid_win] = t0_alt[n + mid_win] ind = ~freezing_alt.mask f = interp1d(obs.time[ind], freezing_alt[ind]) for ii, alt in enumerate(f(obs.time)): is_freezing[ii, obs.height > alt] = True return is_freezing
def test_n_elements_2(x, a, result): assert utils.n_elements(x, a, 'time') == result
def test_n_elements(x, a, result): assert utils.n_elements(x, a) == result
def find_liquid( obs: ClassData, peak_amp: float = 1e-6, max_width: float = 300, min_points: int = 3, min_top_der: float = 1e-7, min_lwp: float = 0, min_alt: float = 100, ) -> dict: """Estimate liquid layers from SNR-screened attenuated backscatter. Args: obs: The :class:`ClassData` instance. peak_amp: Minimum value of peak. Default is 1e-6. max_width: Maximum width of peak. Default is 300 (m). min_points: Minimum number of valid points in peak. Default is 3. min_top_der: Minimum derivative above peak, defined as (beta_peak-beta_top) / (alt_top-alt_peak). Default is 1e-7. min_lwp: Minimum value from linearly interpolated lwp measured by the mwr. Default is 0. min_alt: Minimum altitude of the peak from the ground. Default is 100 (m). Returns: Dict containing `presence`, `bases` and `tops`. References: The method is based on Tuononen, M. et.al, 2019, https://acp.copernicus.org/articles/19/1985/2019/. """ def _is_proper_peak(): conditions = ( npoints >= min_points, peak_width < max_width, top_der > min_top_der, is_positive_lwp, peak_alt > min_alt, ) return all(conditions) def _save_peak_position(): is_liquid[n, base:top + 1] = True liquid_top[n, top] = True liquid_base[n, base] = True lwp_int = interpolate_lwp(obs) beta = ma.copy(obs.beta) height = obs.height is_liquid, liquid_top, liquid_base = utils.init(3, beta.shape, dtype=bool, masked=False) base_below_peak = utils.n_elements(height, 200) top_above_peak = utils.n_elements(height, 150) difference = np.diff(beta, axis=1) assert isinstance(difference, ma.MaskedArray) beta_diff = difference.filled(0) beta = beta.filled(0) peak_indices = _find_strong_peaks(beta, peak_amp) for n, peak in zip(*peak_indices): lprof = beta[n, :] dprof = beta_diff[n, :] try: base = ind_base(dprof, peak, base_below_peak, 4) top = ind_top(dprof, peak, height.shape[0], top_above_peak, 4) except IndexError: continue npoints = np.count_nonzero(lprof[base:top + 1]) peak_width = height[top] - height[base] peak_alt = height[peak] - height[0] top_der = (lprof[peak] - lprof[top]) / (height[top] - height[peak]) is_positive_lwp = lwp_int[n] > min_lwp if _is_proper_peak(): _save_peak_position() return {"presence": is_liquid, "bases": liquid_base, "tops": liquid_top}
def find_liquid(obs: ClassData, peak_amp: Optional[float] = 1e-6, max_width: Optional[float] = 300, min_points: Optional[int] = 3, min_top_der: Optional[float] = 1e-7, min_lwp: Optional[float] = 0) -> dict: """ Estimate liquid layers from SNR-screened attenuated backscatter. Args: obs: The :class:`ClassData` instance. peak_amp: Minimum value of peak. Default is 2e-5. max_width: Maximum width of peak. Default is 300 (m). min_points: Minimum number of valid points in peak. Default is 3. min_top_der: Minimum derivative above peak, defined as (beta_peak-beta_top) / (alt_top-alt_peak), which is always positive. Default is 2e-7. min_lwp: Minimum value from linearly interpolated lwp measured by the mwr. Default is 0. Returns: Dict containing `presence`, `bases` and `tops`. References: The method is based on Tuononen, M. et.al, 2019, https://acp.copernicus.org/articles/19/1985/2019/. """ def _is_proper_peak(): conditions = (npoints >= min_points, peak_width < max_width, top_der > min_top_der, is_positive_lwp) return all(conditions) def _save_peak_position(): is_liquid[n, base:top + 1] = True liquid_top[n, top] = True liquid_base[n, base] = True lwp_int = interpolate_lwp(obs) beta = ma.copy(obs.beta) # TODO: append zero-row into data instead of setting first values to zero. # This fix is because the peak can be the very first value # (thus there is no proper base in data) beta[:, 0] = 0 height = obs.height is_liquid, liquid_top, liquid_base = utils.init(3, beta.shape, dtype=bool, masked=False) base_below_peak = utils.n_elements(height, 200) top_above_peak = utils.n_elements(height, 150) beta_diff = np.diff(beta, axis=1).filled(0) beta = beta.filled(0) peak_indices = _find_strong_peaks(beta, peak_amp) for n, peak in zip(*peak_indices): lprof = beta[n, :] dprof = beta_diff[n, :] try: base = ind_base(dprof, peak, base_below_peak, 4) top = ind_top(dprof, peak, height.shape[0], top_above_peak, 4) except IndexError: continue npoints = np.count_nonzero(lprof[base:top+1]) peak_width = height[top] - height[base] top_der = (lprof[peak] - lprof[top]) / (height[top] - height[peak]) is_positive_lwp = lwp_int[n] > min_lwp if _is_proper_peak(): _save_peak_position() return {'presence': is_liquid, 'bases': liquid_base, 'tops': liquid_top}