-
Notifications
You must be signed in to change notification settings - Fork 270
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
ENH: Addition of New Radar Echo Classification Function (#1495)
* ADD: _echo_class_wt.py: RadarEchoClass module with all methods/code from PyREClass. * ADD:wrapper function for wt_reclass * REFORMAT: Function names comply with PyART & PEP8 standards. * REFORMAT: Function names comply with PyART & PEP8 standards. * Revert "REFORMAT: Function names comply with PyART & PEP8 standards." This reverts commit 6ce7cc8. * REFORMAT: Function names comply with PyART & PEP8 standards. * ADD: masked array, detailed func arguments * MOD:func user arguments used,class num changed. * DOC:function and user arguments. * MOD: added metadata to field dictinery. * order of classes changed * cappi level added;better description of calsses * REFORMAT: PEP8 style * FORMAT:PEP8 * MOD:sanity check for conv core threshold * MOD:More sanity check for thresholds * MOD:Added override to sanity check+documentation * MOD:minor * invalid grid raises exception * first unit test added * ADD:func make_gaussian_storm_grid * ADD: test for output validity * ADD:utest for results of classification * ENH: test documentation and apply black formatting * ENH:small test func merged;name change * ADD: Stat-based tests for Gaussian storm grid function * ADD: example * FORMAT:Black * FIX: issues for #1495 the code review - aded TypeError handling in `_echo_class_wt.py` - Left tab docstring in `echo_class.py` - Added blank line in `_echo_class_wt.py` - Did not check `reflectivity_to_rainrate` function for duplication - Spellcheck Done - Confirmed grid object check in `echo_class.py` - Added `#noqa` comment in `__init__.py` * FIX:unused imports andvariables removed * Removed two line func sum_conv_wavelets(); * minor * Removed reflectivity_to_rainrate() * Test PAssed reflectivity_to_rainrate(): * minor:autosummary * ADD:conv_wavelet_sum() * Func Renamed: get_reclass -> wavelet_reclas * Refined documentation and Descriptive user arguments * FMT: Black * ENH:Docstring * DOC:Better description & ordering of classification categories * REFACTOR:scale_break now computed in calling function Moved scale break calculation to calling function, outputting scale_break as a parameter for the user. Adjusted related functions and variables. * ENH:scale_break is adaptive to resolution * ENH:atol added, tests default function arguments * FROMAT:black * MINOR:corrected tab in docstring * used new scaling and added description * MINOR: alignment in docstring * FORAMT:linting error corrected * 'LINTING ERR:import-block sorted;extra'()' removed' Follwing errors are fixed: pyart/retrieve/_echo_class_wt.py:192:26: UP034 [*] Avoid extraneous parentheses pyart/retrieve/echo_class.py:6:1: I001 [*] Import block is un-sorted or un-formatted Found 2 errors. [*] 2 fixable with the `--fix` option. Error: Process completed with exit code 1. * FIX: Fix linting error * FIX: Fix more linting errors * DOCS:minor docstrig chages from Zach * DOCS: testing precommit --------- Co-authored-by: mgrover1 <[email protected]> Co-authored-by: Max Grover <[email protected]>
- Loading branch information
1 parent
0c5017a
commit 2051365
Showing
7 changed files
with
1,315 additions
and
0 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,313 @@ | ||
""" | ||
Classification of Precipitation Echoes in Radar Data. | ||
Created on Thu Oct 12 23:12:19 2017 | ||
@author: Bhupendra Raut | ||
@modifed: 11/19/2023 | ||
@references: 10.1109/TGRS.2020.2965649 | ||
.. autosummary:: | ||
wavelet_reclass | ||
label_classes | ||
calc_scale_break | ||
atwt2d | ||
""" | ||
|
||
|
||
import numpy as np | ||
|
||
|
||
def wavelet_reclass( | ||
grid, | ||
refl_field, | ||
level, | ||
zr_a, | ||
zr_b, | ||
core_wt_threshold, | ||
conv_wt_threshold, | ||
scale_break, | ||
min_reflectivity, | ||
conv_min_refl, | ||
conv_core_threshold, | ||
): | ||
""" | ||
Compute ATWT described as Raut et al (2008) and classify radar echoes using scheme of Raut et al (2020). | ||
First, convert dBZ to rain rates using standard Z-R relationship or user given coefficients. This is to | ||
transform the normally distributed dBZ to gamma-like distribution, enhancing the structure of the field. | ||
Parameters | ||
---------- | ||
dbz_data : ndarray | ||
2D array containing radar data. Last dimension should be levels. | ||
res_km : float | ||
Resolution of the radar data in km | ||
scale_break : int | ||
Calculated scale break between convective and stratiform scales. Dyadically spaced in grid pixels. | ||
Returns | ||
------- | ||
wt_class : ndarray | ||
Precipitation type classification: 0. N/A 1. stratiform/non-convective, | ||
2. convective cores and 3. moderate+transitional (mix) convective | ||
regions. | ||
""" | ||
|
||
# Extract grid data, save mask and get the resolution in km | ||
try: | ||
dbz_data = grid.fields[refl_field]["data"][level, :, :] | ||
except: | ||
dbz_data = grid.fields[refl_field]["data"][:, :] | ||
|
||
# save the radar original mask for missing data. | ||
radar_mask = np.ma.getmask(dbz_data) | ||
|
||
wt_sum = conv_wavelet_sum(dbz_data, zr_a, zr_b, scale_break) | ||
|
||
wt_class = label_classes( | ||
wt_sum, | ||
dbz_data, | ||
core_wt_threshold, | ||
conv_wt_threshold, | ||
min_reflectivity, | ||
conv_min_refl, | ||
conv_core_threshold, | ||
) | ||
|
||
wt_class_ma = np.ma.masked_where(radar_mask, wt_class) # add mask back | ||
wt_class_ma = wt_class_ma.squeeze() | ||
|
||
return wt_class_ma | ||
|
||
|
||
def conv_wavelet_sum(dbz_data, zr_a, zr_b, scale_break): | ||
""" | ||
Computes the sum of wavelet transform components for convective scales from dBZ data. | ||
Parameters | ||
------------ | ||
dbz_data : ndarray | ||
2D array containing radar dBZ data. | ||
zr_a, zr_b : float | ||
Coefficients for the Z-R relationship. | ||
res_km : float | ||
Resolution of the radar data in km. | ||
scale_break : int | ||
Calculated scale break (in pixels) between convective and stratiform scales | ||
Returns | ||
--------- | ||
wt_sum : ndarray | ||
Sum of convective scale wavelet transform components. | ||
""" | ||
try: | ||
dbz_data = dbz_data.filled(0) | ||
except Exception: | ||
pass | ||
|
||
dbz_data[np.isnan(dbz_data)] = 0 | ||
rr_data = ((10.0 ** (dbz_data / 10.0)) / zr_a) ** (1.0 / zr_b) | ||
|
||
wt, _ = atwt2d(rr_data, max_scale=scale_break) | ||
wt_sum = np.sum(wt, axis=(0)) | ||
|
||
return wt_sum | ||
|
||
|
||
def label_classes( | ||
wt_sum, | ||
dbz_data, | ||
core_wt_threshold, | ||
conv_wt_threshold, | ||
min_reflectivity, | ||
conv_min_refl, | ||
conv_core_threshold, | ||
): | ||
""" | ||
Labels classes using given thresholds: | ||
- 0: No precipitation or unclassified | ||
- 1: Stratiform/non-convective regions | ||
- 2: Transitional and mixed convective regions | ||
- 3: Convective cores | ||
Following hard coded values are optimized and validated using C-band radars | ||
over Darwin, Australia (2.5 km grid spacing) and tested for Solapur, India (1km grid spacing) [Raut et al. 2020]. | ||
core_wt_threshold = 5 # WT value more than this is strong convection | ||
conv_wt_threshold = 2 # WT value for moderate convection | ||
min_reflectivity = 10 # pixels below this value are not classified. | ||
conv_min_refl = 30 # pixel below this value are not convective. This works for most cases. | ||
Parameters | ||
----------- | ||
wt_sum : ndarray | ||
Integrated wavelet transform | ||
vol_data : ndarray | ||
Array, vector or matrix of data | ||
Returns | ||
--------- | ||
wt_class : ndarray | ||
Precipitation type classification. | ||
""" | ||
|
||
# I first used negative numbers to annotate the categories. Then multiply it by -1. | ||
wt_class = np.where( | ||
(wt_sum >= conv_wt_threshold) & (dbz_data >= conv_core_threshold), -3, 0 | ||
) | ||
wt_class = np.where( | ||
(wt_sum >= core_wt_threshold) & (dbz_data >= conv_min_refl), -3, 0 | ||
) | ||
wt_class = np.where( | ||
(wt_sum < core_wt_threshold) | ||
& (wt_sum >= conv_wt_threshold) | ||
& (dbz_data >= conv_min_refl), | ||
-2, | ||
wt_class, | ||
) | ||
wt_class = np.where((wt_class == 0) & (dbz_data >= min_reflectivity), -1, wt_class) | ||
|
||
wt_class = -1 * wt_class | ||
wt_class = np.where((wt_class == 0), np.nan, wt_class) | ||
|
||
return wt_class.astype(np.int32) | ||
|
||
|
||
def calc_scale_break(res_meters, conv_scale_km): | ||
""" | ||
Compute scale break for convection and stratiform regions. WT will be | ||
computed upto this scale and features will be designated as convection. | ||
Parameters | ||
----------- | ||
res_meters : float | ||
resolution of the image. | ||
conv_scale_km : float | ||
expected size of spatial variations due to convection. | ||
Returns | ||
-------- | ||
dyadic scale break : int | ||
integer scale break in dyadic scale. | ||
""" | ||
res_km = res_meters / 1000 | ||
scale_break = np.log(conv_scale_km / res_km) / np.log(2) + 1 | ||
|
||
return int(round(scale_break)) | ||
|
||
|
||
def atwt2d(data2d, max_scale=-1): | ||
""" | ||
Computes a trous wavelet transform (ATWT). Computes ATWT of the 2D array | ||
up to max_scale. If max_scale is outside the boundaries, number of scales | ||
will be reduced. | ||
Data is mirrored at the boundaries. 'Negative WT are removed. Not tested | ||
for non-square data. | ||
@authors: Bhupendra A. Raut and Dileep M. Puranik | ||
@references: Press et al. (1992) Numerical Recipes in C. | ||
Parameters | ||
----------- | ||
data2d : ndarray | ||
2D image as array or matrix. | ||
max_scale : | ||
Computes wavelets up to max_scale. Leave blank for maximum possible | ||
scales. | ||
Returns | ||
--------- | ||
tuple of ndarray | ||
ATWT of input image and the final smoothed image or background image. | ||
""" | ||
|
||
if not isinstance(data2d, np.ndarray): | ||
raise TypeError("The input data2d must be a numpy array.") | ||
|
||
data2d = data2d.squeeze() | ||
|
||
dims = data2d.shape | ||
min_dims = np.min(dims) | ||
max_possible_scales = int(np.floor(np.log(min_dims) / np.log(2))) | ||
|
||
if max_scale < 0 or max_possible_scales <= max_scale: | ||
max_scale = max_possible_scales - 1 | ||
|
||
ny = dims[0] | ||
nx = dims[1] | ||
|
||
# For saving wt components | ||
wt = np.zeros((max_scale, ny, nx)) | ||
|
||
temp1 = np.zeros(dims) | ||
temp2 = np.zeros(dims) | ||
|
||
sf = (0.0625, 0.25, 0.375) # scaling function | ||
|
||
# start wavelet loop | ||
for scale in range(1, max_scale + 1): | ||
# print(scale) | ||
x1 = 2 ** (scale - 1) | ||
x2 = 2 * x1 | ||
|
||
# Row-wise smoothing | ||
for i in range(0, nx): | ||
# find the indices for prev and next points on the line | ||
prev2 = abs(i - x2) | ||
prev1 = abs(i - x1) | ||
next1 = i + x1 | ||
next2 = i + x2 | ||
|
||
# If these indices are outside the image, "mirror" them | ||
# Sometime this causes issues at higher scales. | ||
if next1 > nx - 1: | ||
next1 = 2 * (nx - 1) - next1 | ||
|
||
if next2 > nx - 1: | ||
next2 = 2 * (nx - 1) - next2 | ||
|
||
if prev1 < 0 or prev2 < 0: | ||
prev1 = next1 | ||
prev2 = next2 | ||
|
||
for j in range(0, ny): | ||
left2 = data2d[j, prev2] | ||
left1 = data2d[j, prev1] | ||
right1 = data2d[j, next1] | ||
right2 = data2d[j, next2] | ||
temp1[j, i] = ( | ||
sf[0] * (left2 + right2) | ||
+ sf[1] * (left1 + right1) | ||
+ sf[2] * data2d[j, i] | ||
) | ||
|
||
# Column-wise smoothing | ||
for i in range(0, ny): | ||
prev2 = abs(i - x2) | ||
prev1 = abs(i - x1) | ||
next1 = i + x1 | ||
next2 = i + x2 | ||
|
||
# If these indices are outside the image use next values | ||
if next1 > ny - 1: | ||
next1 = 2 * (ny - 1) - next1 | ||
if next2 > ny - 1: | ||
next2 = 2 * (ny - 1) - next2 | ||
if prev1 < 0 or prev2 < 0: | ||
prev1 = next1 | ||
prev2 = next2 | ||
|
||
for j in range(0, nx): | ||
top2 = temp1[prev2, j] | ||
top1 = temp1[prev1, j] | ||
bottom1 = temp1[next1, j] | ||
bottom2 = temp1[next2, j] | ||
temp2[i, j] = ( | ||
sf[0] * (top2 + bottom2) | ||
+ sf[1] * (top1 + bottom1) | ||
+ sf[2] * temp1[i, j] | ||
) | ||
|
||
wt[scale - 1, :, :] = data2d - temp2 | ||
data2d[:] = temp2 | ||
|
||
return wt, data2d |
Oops, something went wrong.