ARM Logo

Solutions: Snowfall Retrievals from SAIL X-Band Radar

%env PYTHONWARNINGS=ignore
env: PYTHONWARNINGS=ignore
import warnings
warnings.filterwarnings("ignore", category=DeprecationWarning)
warnings.filterwarnings("ignore", category=UserWarning)

import glob
import os
import datetime

import numpy as np
import matplotlib.pyplot as plt
import pyart
import act
import xarray as xr
from matplotlib.dates import DateFormatter
from pathlib import Path

from metpy.plots import USCOUNTIES
from dask.distributed import Client, LocalCluster

import cartopy.crs as ccrs
import cartopy.feature as cfeature

import xwrf
## You are using the Python ARM Radar Toolkit (Py-ART), an open source
## library for working with weather radar data. Py-ART is partly
## supported by the U.S. Department of Energy as part of the Atmospheric
## Radiation Measurement (ARM) Climate Research Facility, an Office of
## Science user facility.
##
## If you use this software to prepare a publication, please cite:
##
##     JJ Helmus and SM Collis, JORS 2016, doi: 10.5334/jors.119
ERROR 1: PROJ: proj_create_from_database: Open of /opt/conda/share/proj failed

Setup Helper Functions

We setup helper functions to calculate the snowfall retrieval, using following notation:

\(Z = A*S ^ {B}\)

Where:

  • Z = Reflectivity in dBZ

  • A = Coefficient applied to Z-S Relationship (not in the exponent)

  • S = Liquid snowfall rate

  • B = Coefficient applied to Z-S Relationship (in the exponent)

We also need to apply a snow water equivalent ratio (swe) to convert from liquid to snow (ex. 8 inches of snow –> 1 inch of rain would be 8.0).

This equation now becomes:

\(Z = swe*A*S ^ {B}\)

Solving for S, we get:

\(S = swe * (\frac{z}{a})^{1/B}\)

Where z is reflectivity in units of dB (\(z =10^{Z/10}\))

List the Available Files

We will use files on the Oak Ridge Laboratory Computing Facility (ORLCF), within the shared SAIL directory /gpfs/wolf/atm124/proj-shared/sail.

file_list = sorted(glob.glob("/data/project/ARM_Summer_School_2024_Data/sail/radar/*"))
file_list[0:10]
['/data/project/ARM_Summer_School_2024_Data/sail/radar/gucxprecipradarcmacS2.c1.20220310.000206.nc',
 '/data/project/ARM_Summer_School_2024_Data/sail/radar/gucxprecipradarcmacS2.c1.20220310.000726.nc',
 '/data/project/ARM_Summer_School_2024_Data/sail/radar/gucxprecipradarcmacS2.c1.20220310.001246.nc',
 '/data/project/ARM_Summer_School_2024_Data/sail/radar/gucxprecipradarcmacS2.c1.20220310.001806.nc',
 '/data/project/ARM_Summer_School_2024_Data/sail/radar/gucxprecipradarcmacS2.c1.20220310.002326.nc',
 '/data/project/ARM_Summer_School_2024_Data/sail/radar/gucxprecipradarcmacS2.c1.20220310.002846.nc',
 '/data/project/ARM_Summer_School_2024_Data/sail/radar/gucxprecipradarcmacS2.c1.20220310.003406.nc',
 '/data/project/ARM_Summer_School_2024_Data/sail/radar/gucxprecipradarcmacS2.c1.20220310.003926.nc',
 '/data/project/ARM_Summer_School_2024_Data/sail/radar/gucxprecipradarcmacS2.c1.20220310.004446.nc',
 '/data/project/ARM_Summer_School_2024_Data/sail/radar/gucxprecipradarcmacS2.c1.20220310.005006.nc']

Question 1 - Easy

Using material presented this week, apply one of the above relationships to SAIL X-Band radar file and display

Read Data + Apply Snowfall Retrieval

def snow_rate(radar, swe_ratio, A, B, key="snow_z"):
    """
    Snow rate applied to a pyart.Radar object
    
    Takes a given Snow Water Equivilent ratio (swe_ratio), A and B value
    for the Z-S relationship and creates a radar field similar to DBZ
    showing the radar estimated snowfall rate in mm/hr. Then the given
    SWE_ratio, A and B are stored in the radar metadata for later 
    reference.

    """
    # Setting up for Z-S relationship:
    snow_z = radar.fields['corrected_reflectivity']['data'].copy()
    # Convert it from dB to linear units
    z_lin = 10.0**(radar.fields['corrected_reflectivity']['data']/10.)
    # Apply the Z-S relation.
    snow_z = swe_ratio * (z_lin/A)**(1./B)
    # Add the field back to the radar. Use reflectivity as a template
    radar.add_field_like('corrected_reflectivity', key,  snow_z,
                         replace_existing=True)
    # Update units and metadata
    radar.fields[key]['units'] = 'mm/h'
    radar.fields[key]['standard_name'] = 'snowfall_rate'
    radar.fields[key]['long_name'] = 'user_defined_snowfall_rate_from_z'
    radar.fields[key]['valid_min'] = 0
    radar.fields[key]['valid_max'] = 500
    radar.fields[key]['swe_ratio'] = swe_ratio
    radar.fields[key]['A'] = A
    radar.fields[key]['B'] = B
    return radar
radar = pyart.io.read(file_list[0])
radar = snow_rate(radar, 8.5, 67, 1.28, key="snow_z_new")
/opt/conda/lib/python3.11/site-packages/numpy/ma/core.py:6980: RuntimeWarning: overflow encountered in power
  result = np.where(m, fa, umath.power(fa, fb)).view(basetype)
# Check to see if the snowfall retrieval was applied
list(radar.fields.keys())
['DBZ',
 'VEL',
 'WIDTH',
 'ZDR',
 'PHIDP',
 'RHOHV',
 'NCP',
 'DBZhv',
 'cbb_flag',
 'sounding_temperature',
 'height',
 'signal_to_noise_ratio',
 'velocity_texture',
 'gate_id',
 'simulated_velocity',
 'corrected_velocity',
 'unfolded_differential_phase',
 'corrected_differential_phase',
 'filtered_corrected_differential_phase',
 'corrected_specific_diff_phase',
 'filtered_corrected_specific_diff_phase',
 'corrected_differential_reflectivity',
 'corrected_reflectivity',
 'height_over_iso0',
 'specific_attenuation',
 'path_integrated_attenuation',
 'specific_differential_attenuation',
 'path_integrated_differential_attenuation',
 'rain_rate_A',
 'snow_rate_ws2012',
 'snow_rate_ws88diw',
 'snow_rate_m2009_1',
 'snow_rate_m2009_2',
 'snow_z_new']

Display Snowfall Retrieval

# Create the Py-ART display object
display = pyart.graph.RadarMapDisplay(radar)
# Define the matplotlib figure
fig = plt.figure(figsize=(18,10))

# Extract the latitude and longitude of the radar and use it for the center of the map
lat_center = round(radar.latitude['data'][0], 2)
lon_center = round(radar.longitude['data'][0], 2)

# Set the projection - in this case, we use a general PlateCarree projection
projection = ccrs.PlateCarree()

# Determine the ticks
lat_ticks = np.arange(lat_center-0.5, lat_center+1, .1)
lon_ticks = np.arange(lon_center-0.5, lon_center+1, .1)

# Define a subplot for the reflectivity field
ax1 = plt.subplot(211, projection=projection)
display.plot_ppi_map("corrected_reflectivity", 
                     2, 
                     resolution='10m', 
                     ax=ax1, 
                     lat_lines=lat_ticks, 
                     lon_lines=lon_ticks,
                     vmin=-10,
                     vmax=35)

# Define a subplot for the snowfall retrieval
ax2 = plt.subplot(212, projection=projection)
display.plot_ppi_map("snow_z_new", 
                     2, 
                     resolution='10m', 
                     ax=ax2, 
                     lat_lines=lat_ticks, 
                     lon_lines=lon_ticks,
                     vmin=0,
                     vmax=30,
                     cmap='Blues')
../../_images/bf5ecf9e03f7577bb50a0d0838be88e1b51a408cbbb118b1c893dd7b526cc2fd.png

Question 2 - Intermediate

Using the above methodology, calculate the snowfall accumulation for an hour (or day)

Gridded Observations Using Nearest Neighbor Interpolation

def compute_number_of_points(extent, resolution):
    """
    Create a helper function to determine number of points
    """
    return int((extent[1] - extent[0])/resolution)

# Grid extents in meters
z_grid_limits = (500.,5_000.)
y_grid_limits = (-20_000.,20_000.)
x_grid_limits = (-20_000.,20_000.)

# Grid resolution in meters
grid_resolution = 250

# Determine number of points with our desired input
x_grid_points = compute_number_of_points(x_grid_limits, grid_resolution)
y_grid_points = compute_number_of_points(y_grid_limits, grid_resolution)
z_grid_points = compute_number_of_points(z_grid_limits, grid_resolution)

print(z_grid_points,
      y_grid_points,
      x_grid_points)
18 160 160
# Call Py-ART gridding module, with our grid specifications
grid = pyart.map.grid_from_radars(radar,
                                  grid_shape=(z_grid_points,
                                              y_grid_points,
                                              x_grid_points),
                                  grid_limits=(z_grid_limits,
                                               y_grid_limits,
                                               x_grid_limits),
                                  method='nearest'
                                 )
ds = grid.to_xarray()
ds
<xarray.Dataset> Size: 122MB
Dimensions:                                   (time: 1, z: 18, y: 160, x: 160)
Coordinates:
  * time                                      (time) object 8B 2022-03-10 00:...
  * z                                         (z) float64 144B 500.0 ... 5e+03
    lat                                       (y, x) float64 205kB 38.72 ... ...
    lon                                       (y, x) float64 205kB -107.2 ......
  * y                                         (y) float64 1kB -2e+04 ... 2e+04
  * x                                         (x) float64 1kB -2e+04 ... 2e+04
Data variables: (12/35)
    NCP                                       (time, z, y, x) float64 4MB 0.1...
    snow_rate_m2009_1                         (time, z, y, x) float64 4MB nan...
    specific_differential_attenuation         (time, z, y, x) float64 4MB nan...
    filtered_corrected_specific_diff_phase    (time, z, y, x) float64 4MB 0.0...
    path_integrated_differential_attenuation  (time, z, y, x) float64 4MB nan...
    filtered_corrected_differential_phase     (time, z, y, x) float64 4MB 0.0...
    ...                                        ...
    corrected_differential_reflectivity       (time, z, y, x) float64 4MB nan...
    simulated_velocity                        (time, z, y, x) float64 4MB -2....
    rain_rate_A                               (time, z, y, x) float64 4MB nan...
    sounding_temperature                      (time, z, y, x) float32 2MB -13...
    signal_to_noise_ratio                     (time, z, y, x) float64 4MB -1....
    ROI                                       (time, z, y, x) float32 2MB 493...
ds.snow_z_new.isel(z=0).plot(x='lon',
                             y='lat',
                             vmin=0, 
                             vmax=50,
                             cmap='pyart_HomeyerRainbow')
<matplotlib.collections.QuadMesh at 0x7f0e9ea34510>
../../_images/95e98dfeeddfc7f5c900a0ea64febeb93e6ca165fae696bfd1e0d8439386ccd5.png

Determine the Lowest Height in Each Column

We plotted the lowest level (500 m) in the plot above. It would be more helpful to have data from the lowest data point (lowest z) in each column (across time, latitude, and longitude)

We start first by creating a new field in our dataset, height_expanded, which is a four-dimensional (time, z, x, y) vertical coordinate, with nan values where we have missing snow rate values.

ds["height_expanded"] = (ds.z * (ds.snow_z_new/ds.snow_z_new)).fillna(10_000)

Next, we find the index of the lowest value in this column, using the .argmin method, looking over the column (z)

min_index = ds.height_expanded.argmin(dim='z',
                                      skipna=True)

Here is a plot of the lowest value height in the column for our domain:

**Notice how some values are the top of the column - 5000 m, whereas some of the values are close lowest vertical level, 500 m

ds.height_expanded.isel(z=min_index).plot(vmin=500,
                                          vmax=5000);
../../_images/2780e56e0fd1a92d908e51c7f737133ee0266cdf4ff4193baea266db27d56d9c.png

Apply this to our snow fields

We first check for snow fields in our dataset, by using the following list comprehension line:

snow_fields = [var for var in list(ds.variables) if "snow" in var]
snow_fields
['snow_rate_m2009_1',
 'snow_rate_m2009_2',
 'snow_rate_ws88diw',
 'snow_z_new',
 'snow_rate_ws2012']
subset_ds = ds[snow_fields].isel(z=min_index)

Visualize our closest-to-ground snow value

Now that we have the lowest vertical level in each column, let’s plot our revised maps, which only have dimensions:

  • time

  • latitude

  • longitude

for snow_field in snow_fields:
    subset_ds[snow_field].plot(x='lon',
                               y='lat',
                               cmap='pyart_HomeyerRainbow')
    plt.show()
    plt.close()
../../_images/8f5808bd2fa7df649bb45f81ed6a40b58223f863f140b723177e673439419596.png ../../_images/03a5411a28dcfae913cce500562bcd23406b969d0b594c64942fdc93f2575cb4.png ../../_images/9c462c298ec8efbd056786f552ead7d142093aa089828ed5416351373d652c76.png ../../_images/ad2e5d53bd8f4b460e6b8a78240016b27b1a5b81d6113a9af85f9c533d1028c6.png ../../_images/4db4a90303657b3b93b4e79d3cd42701128f25f4fb51f69da035ad3e38bbe200.png

Calculate Daily Accumulation of Snowfall

def grid_radar(file,
               x_grid_limits=(-20_000.,20_000.),
               y_grid_limits=(-20_000.,20_000.),
               z_grid_limits = (500.,5_000.),
               grid_resolution = 250,
               
               ):
    """
    Grid the radar using some provided parameters
    """
    try:
        radar = pyart.io.read(file)
    except:
        radar = None

    if radar:
        x_grid_points = compute_number_of_points(x_grid_limits, grid_resolution)
        y_grid_points = compute_number_of_points(y_grid_limits, grid_resolution)
        z_grid_points = compute_number_of_points(z_grid_limits, grid_resolution)
    
        grid = pyart.map.grid_from_radars(radar,
                                          grid_shape=(z_grid_points,
                                                      y_grid_points,
                                                      x_grid_points),
                                          grid_limits=(z_grid_limits,
                                                       y_grid_limits,
                                                       x_grid_limits),
                                          method='nearest'
                                         )
        grid_out = grid.to_xarray()
        del radar
    else:
        grid_out = None
        
    return grid_out

def subset_lowest_vertical_level(ds, additional_fields=["corrected_reflectivity"]):
    """
    Filter the dataset based on the lowest vertical level
    """
    snow_fields = [var for var in list(ds.variables) if "snow" in var] + additional_fields
    
    # Create a new 4-d height field
    ds["height_expanded"] = (ds.z * (ds[snow_fields[0]]/ds[snow_fields[0]])).fillna(5_000)
    
    # Find the minimum height index
    min_index = ds.height_expanded.argmin(dim='z',
                                          skipna=True)
    
    # Subset our snow fields based on this new index
    subset_ds = ds[snow_fields].isel(z=min_index)
    
    return subset_ds
def process_squire(file):
    """ Script to process SQUIRE """
    ds = grid_radar(file)
    if ds:
        out_ds = subset_lowest_vertical_level(ds)
    
        # Create an output path
        ##out_path = f"./{Path(file).stem}.gridded.nc"
        ##out_ds.to_netcdf(out_path)

        # free up memory
        del ds
    else:
        out_ds = None
    
    return out_ds 

Spin up Dask Cluster and Process SQUIRE

%%time
## Start up a Dask Cluster
cluster = LocalCluster()
with Client(cluster) as client:
    future = client.map(process_squire, file_list)
    my_data = client.gather(future)
ERROR 1: PROJ: proj_create_from_database: Open of /opt/conda/share/proj failed
ERROR 1: PROJ: proj_create_from_database: Open of /opt/conda/share/proj failed
ERROR 1: PROJ: proj_create_from_database: Open of /opt/conda/share/proj failed
ERROR 1: PROJ: proj_create_from_database: Open of /opt/conda/share/proj failed
2024-05-22 18:33:11,321 - distributed.nanny - WARNING - Restarting worker
2024-05-22 18:33:11,377 - distributed.nanny - WARNING - Restarting worker
2024-05-22 18:33:11,383 - distributed.nanny - WARNING - Restarting worker
ERROR 1: PROJ: proj_create_from_database: Open of /opt/conda/share/proj failed
ERROR 1: PROJ: proj_create_from_database: Open of /opt/conda/share/proj failed
ERROR 1: PROJ: proj_create_from_database: Open of /opt/conda/share/proj failed
2024-05-22 18:33:13,939 - distributed.nanny - WARNING - Restarting worker
ERROR 1: PROJ: proj_create_from_database: Open of /opt/conda/share/proj failed
HDF5-DIAG: Error detected in HDF5 (1.14.3) thread 1:
  #000: H5D.c line 611 in H5Dget_space(): unable to synchronously get dataspace
    major: Dataset
    minor: Can't get value
  #001: H5D.c line 571 in H5D__get_space_api_common(): invalid dataset identifier
    major: Invalid arguments to routine
    minor: Inappropriate type
HDF5-DIAG: Error detected in HDF5 (1.14.3) thread 1:
  #000: H5D.c line 611 in H5Dget_space(): unable to synchronously get dataspace
    major: Dataset
    minor: Can't get value
  #001: H5D.c line 571 in H5D__get_space_api_common(): invalid dataset identifier
    major: Invalid arguments to routine
    minor: Inappropriate type
CPU times: user 9.7 s, sys: 4.61 s, total: 14.3 s
Wall time: 9min 37s
# Concatenate all extracted columns across time dimension to form daily timeseries
ds = xr.concat([data for data in my_data if data], dim='time')

Calculate Daily Accumulation

new_ds = ds.resample(time='h').sum()
new_ds
<xarray.Dataset> Size: 25MB
Dimensions:                 (time: 24, y: 160, x: 160)
Coordinates:
    lat                     (y, x) float64 205kB 38.72 38.72 ... 39.08 39.08
    lon                     (y, x) float64 205kB -107.2 -107.2 ... -106.7 -106.7
  * y                       (y) float64 1kB -2e+04 -1.975e+04 ... 2e+04
  * x                       (x) float64 1kB -2e+04 -1.975e+04 ... 2e+04
  * time                    (time) object 192B 2022-03-10 00:00:00 ... 2022-0...
Data variables:
    snow_rate_ws2012        (time, y, x) float64 5MB 0.0 0.0 0.0 ... 0.0 0.0 0.0
    snow_rate_m2009_1       (time, y, x) float64 5MB 0.0 0.0 0.0 ... 0.0 0.0 0.0
    snow_rate_m2009_2       (time, y, x) float64 5MB 0.0 0.0 0.0 ... 0.0 0.0 0.0
    snow_rate_ws88diw       (time, y, x) float64 5MB 0.0 0.0 0.0 ... 0.0 0.0 0.0
    corrected_reflectivity  (time, y, x) float64 5MB 0.0 0.0 0.0 ... 0.0 0.0 0.0
new_ds['snow_rate_ws2012'].isel(time=1).plot(x='lon', y='lat', cmap='pyart_HomeyerRainbow', vmin=0, vmax=15)
<matplotlib.collections.QuadMesh at 0x7f0e9e02e650>
../../_images/f8afec3c07d36490ae927ce849254cfa8a39717256b988bb7546ba19f833e748.png
new_ds['snow_rate_ws2012'].resample(time='D').sum().plot(x='lon', y='lat', cmap='pyart_HomeyerRainbow')
<matplotlib.collections.QuadMesh at 0x7f0e9de79f10>
../../_images/755adf3a936c7ea971417cd7b30db2c1039f7dfa653bde6b6968e564d39a00a9.png

Question 3 - Hard

Compare this hourly (or daily) accumulation to WRF output for the same day

Compare against WRF Run

files = sorted(glob.glob("/data/project/ARM_Summer_School_2024_Data/sail/wrf/*"))
first_ds = xr.open_dataset(files[0]).xwrf.postprocess().squeeze()
last_ds = xr.open_dataset(files[-1]).xwrf.postprocess().squeeze()
difference = last_ds["SNOW"] - first_ds["SNOW"]
difference.plot(x='XLONG',
                y='XLAT',
                cbar_kwargs={'label': "Change in Snow Water Equivalent ($kgm^{-2}$)"},
                vmin=0,
                vmax=20,
                cmap="pyart_balance")
plt.title("24 Hour Difference in \n Snow Water Liquid Equivalent \n 10 March to 11 March 2023")
Text(0.5, 1.0, '24 Hour Difference in \n Snow Water Liquid Equivalent \n 10 March to 11 March 2023')
../../_images/3c6dc467bed458f0ff8a16e11b32a75b66a25ac93fff8a76dd8b49f2a3bf9779.png
fig, axarr = plt.subplots(2, 1, figsize=[8, 10])

new_ds['snow_rate_ws2012'].resample(time='D').sum().plot(x='lon', 
                                                         y='lat', 
                                                         cmap='pyart_HomeyerRainbow',
                                                         ax=axarr[0])
difference.plot(x='XLONG',
                y='XLAT',
                cbar_kwargs={'label': "Change in Snow Water Equivalent ($kgm^{-2}$)"},
                vmin=0,
                vmax=20,
                cmap="pyart_balance",
                ax=axarr[1])
#axarr[0].set_xlim([-107.7, -106.5])
axarr[1].set_xlim([-107.2, -106.7])
axarr[1].set_ylim([38.7, 39.1])
(38.7, 39.1)
../../_images/bed3fb44957c0f1cf6da04e7f6c3f794aebde95204e25060467360bbf532ca4f.png