GRACE/GRACE-FO Error Visualization Program

This (notebook) demonstrates visualizing spherical harmonic errors from the Gravity Recovery and Climate Experiment (GRACE) and the GRACE Follow-On (GRACE-FO) Level-2 products.

Load necessary modules for running the notebook

import numpy as np
import matplotlib
matplotlib.rcParams['mathtext.default'] = 'regular'
import matplotlib.pyplot as plt
import cartopy.crs as ccrs
import ipywidgets
import gravity_toolkit as gravtk

Set the GRACE/GRACE-FO Data Directory

Directory should contain:

  • Technical notes with SLR coefficients

  • Subdirectory with geocenter coefficients

  • Subdirectories for each processing center

# set the directory with GRACE/GRACE-FO data
# update local data with PO.DAAC https servers
widgets = gravtk.tools.widgets()
ipywidgets.VBox([
    widgets.directory,
    widgets.update,
    widgets.endpoint
])

Update Data in Directory

# if updating the local data
if widgets.update.value:
    # run podaac sync program to get latest data
    !podaac_cumulus.py --directory=$widgets.base_directory --endpoint=$widgets.endpoint
    # run GRACE date program to verify months
    !run_grace_date.py --directory=$widgets.base_directory --verbose
    # get geocenter data from Sutterley and Velicogna (2019)
    gravtk.utilities.from_figshare(widgets.base_directory)

Set GRACE/GRACE-FO Parameters

These parameters describe the specific GRACE/GRACE-FO product and the months of data to read

  • GRACE/GRACE-FO Processing Center

    • CSR: University of Texas Center for Space Research

    • GFZ: German Research Centre for Geosciences (GeoForschungsZentrum)

    • JPL: Jet Propulsion Laboratory

    • CNES: French Centre National D’Etudes Spatiales

  • GRACE/GRACE-FO Data Release

  • GRACE/GRACE-FO Data Product

    • GAA: non-tidal atmospheric correction

    • GAB: non-tidal oceanic correction

    • GAC: combined non-tidal atmospheric and oceanic correction

    • GAD: GRACE/GRACE-FO ocean bottom pressure product

    • GSM: corrected monthly GRACE/GRACE-FO static field product

  • GRACE/GRACE-FO Date Range

# update widgets
widgets.select_product()
# display widgets for setting GRACE/GRACE-FO parameters
ipywidgets.VBox([
    widgets.center,
    widgets.release,
    widgets.product,
    widgets.months
])

Set Parameters for Reading GRACE/GRACE-FO Data

These parameters describe processing steps and corrections to be applied when reading the GRACE/GRACE-FO data

  • Maximum Degree and Order

  • Geocenter product (Degree 1)

  • Oblateness product (C20)

  • Figure axis product (C21 and S21)

  • Azimuthal dependence product (C22 and S22)

  • Low Degree Zonal products (C30, C40 and C50)

  • Pole Tide Correction from Wahr et al. (2015)

  • Atmospheric Correction as described in Fagiolini et al. (2015)

# update widgets
widgets.select_options()
# display widgets for setting GRACE/GRACE-FO read parameters
ipywidgets.VBox([
    widgets.lmax,
    widgets.mmax,
    widgets.geocenter,
    widgets.C20,
    widgets.CS21,
    widgets.CS22,
    widgets.C30,
    widgets.C40,
    widgets.C50,
    widgets.pole_tide,
    widgets.atm,
])

Read GRACE/GRACE-FO data

This step extracts the parameters chosen above and then reads the GRACE/GRACE-FO data applying the specified procedures

# extract values from widgets
PROC = widgets.center.value
DREL = widgets.release.value
DSET = widgets.product.value
months = [int(m) for m in widgets.months.value]
LMAX = widgets.lmax.value
MMAX = widgets.mmax.value
DEG1 = widgets.geocenter.value
SLR_C20 = widgets.C20.value
SLR_21 = widgets.CS21.value
SLR_22 = widgets.CS22.value
SLR_C30 = widgets.C30.value
SLR_C40 = widgets.C40.value
SLR_C50 = widgets.C50.value
POLE_TIDE = widgets.pole_tide.value
ATM = widgets.atm.value

# read GRACE/GRACE-FO data for parameters
start_mon = np.min(months)
end_mon = np.max(months)
missing = sorted(set(np.arange(start_mon,end_mon+1)) - set(months))
Ylms = gravtk.grace_input_months(widgets.base_directory, PROC, DREL, DSET,
    LMAX, start_mon, end_mon, missing, SLR_C20, DEG1, MMAX=MMAX,
    SLR_21=SLR_21, SLR_22=SLR_22, SLR_C30=SLR_C30, SLR_C40=SLR_C40,
    SLR_C50=SLR_C50, POLE_TIDE=POLE_TIDE, ATM=ATM)
# create harmonics object and remove mean
GRACE_Ylms = gravtk.harmonics().from_dict(Ylms)
GRACE_Ylms.mean(apply=True)
# number of time steps
nt = len(months)

Set Parameters to Convert to Spatial Error Maps

These parameters specify corrections and filtering steps for converting to the spatial domain at a specified grid spacing

  • Gaussian Smoothing Radius in kilometers

  • Filter (destripe) harmonics (Swenson and Wahr, 2006)

  • Spatial degree spacing

  • Spatial degree interval

    1. (-180:180,90:-90)

    2. (degree spacing)/2

# update widgets
widgets.select_corrections()
# display widgets for setting GRACE/GRACE-FO corrections parameters
ipywidgets.VBox([
    widgets.gaussian,
    widgets.destripe,
    widgets.spacing,
    widgets.interval])

Estimate GRACE/GRACE-FO errors and convert to spatial domain

  • Set output grid domain

  • Calculate Fully-Normalized Legendre Polynomials

  • Calculate coefficients for converting to the output units

  • Convert errors from the spherical harmonic domain into the spatial domain

# Output spatial data
grid = gravtk.spatial()
# Output degree spacing
dlon = widgets.spacing.value
dlat = widgets.spacing.value
# Output Degree Interval
INTERVAL = widgets.interval.index + 1
if (INTERVAL == 1):
    # (-180:180,90:-90)
    nlon = np.int64((360.0/dlon)+1.0)
    nlat = np.int64((180.0/dlat)+1.0)
    grid.lon = -180 + dlon*np.arange(0,nlon)
    grid.lat = 90.0 - dlat*np.arange(0,nlat)
elif (INTERVAL == 2):
    # (Degree spacing)/2
    grid.lon = np.arange(-180+dlon/2.0,180+dlon/2.0,dlon)
    grid.lat = np.arange(90.0-dlat/2.0,-90.0-dlat/2.0,-dlat)
    nlon = len(grid.lon)
    nlat = len(grid.lat)

# Computing plms for converting to spatial domain
theta = (90.0 - grid.lat)*np.pi/180.0
PLM, dPLM = gravtk.plm_holmes(LMAX, np.cos(theta))
# square of legendre polynomials truncated to order MMAX
mm = np.arange(0,MMAX+1)
PLM2 = PLM[:,mm,:]**2
# Calculating cos(m*phi)^2 and sin(m*phi)^2
phi = grid.lon[np.newaxis,:]*np.pi/180.0
ccos = np.cos(np.dot(mm[:,np.newaxis],phi))**2
ssin = np.sin(np.dot(mm[:,np.newaxis],phi))**2
    
# read load love numbers file
# PREM outputs from Han and Wahr (1995)
# https://doi.org/10.1111/j.1365-246X.1995.tb01819.x
love_numbers_file = gravtk.utilities.get_data_path(['data','love_numbers'])
header = 2
columns = ['l','hl','kl','ll']
# LMAX of load love numbers from Han and Wahr (1995) is 696.
# from Wahr (2007) linearly interpolating kl works
# however, as we are linearly extrapolating out, do not make
# LMAX too much larger than 696
# read arrays of kl, hl, and ll Love Numbers
hl,kl,ll = gravtk.read_love_numbers(love_numbers_file, LMAX=LMAX,
    HEADER=header, COLUMNS=columns, REFERENCE='CF', FORMAT='tuple')

# gaussian smoothing radius in km (Jekeli, 1981)
RAD = widgets.gaussian.value
if (RAD != 0):
    wt = 2.0*np.pi*gravtk.gauss_weights(RAD,LMAX)
else:
    # else = 1
    wt = np.ones((LMAX+1))

# destriping the GRACE/GRACE-FO harmonics
if widgets.destripe.value:
    Ylms = GRACE_Ylms.destripe()
else:
    Ylms = GRACE_Ylms.copy()

# dfactor is the degree dependent coefficients
# for converting to spherical harmonic output units
factors = gravtk.units(lmax=LMAX).harmonic(hl,kl,ll).mmwe
# mmwe, millimeters water equivalent
dfactor = factors.get('mmwe')
# units strings for output plots
unit_label = 'mm'
unit_name = 'Equivalent Water Thickness'

# Delta coefficients of GRACE time series (Error components)
delta_Ylms = gravtk.harmonics(lmax=LMAX,mmax=MMAX)
delta_Ylms.clm = np.zeros((LMAX+1, MMAX+1))
delta_Ylms.slm = np.zeros((LMAX+1, MMAX+1))
# Smoothing Half-Width (CNES is a 10-day solution)
# All other solutions are monthly solutions (HFWTH for annual = 6)
if ((PROC == 'CNES') and (DREL in ('RL01','RL02'))):
    HFWTH = 19
else:
    HFWTH = 6
# Equal to the noise of the smoothed time-series
# for each spherical harmonic order
for m in range(0,MMAX+1):# MMAX+1 to include MMAX
    # for each spherical harmonic degree
    for l in range(m,LMAX+1):# LMAX+1 to include LMAX
        # Delta coefficients of GRACE time series
        for cs,csharm in enumerate(['clm','slm']):
            # calculate GRACE Error (Noise of smoothed time-series)
            # With Annual and Semi-Annual Terms
            val1 = getattr(Ylms, csharm)
            smth = gravtk.time_series.smooth(Ylms.time, val1[l,m,:],
                HFWTH=HFWTH)
            # number of smoothed points
            nsmth = len(smth['data'])
            tsmth = np.mean(smth['time'])
            # GRACE delta Ylms
            # variance of data-(smoothed+annual+semi)
            val2 = getattr(delta_Ylms, csharm)
            val2[l,m] = np.sqrt(np.sum(smth['noise']**2)/nsmth)
                
# convolve delta harmonics with degree dependent factors
delta_Ylms = delta_Ylms.convolve(dfactor*wt)
# smooth harmonics and convert to output units
YLM2 = delta_Ylms.power(2.0).scale(1.0/nsmth)
# Calculate fourier coefficients
d_cos = np.zeros((MMAX+1,nlat))# [m,th]
d_sin = np.zeros((MMAX+1,nlat))# [m,th]
# Calculating delta spatial values
for k in range(0,nlat):
    # summation over all spherical harmonic degrees
    d_cos[:,k] = np.sum(PLM2[:,:,k]*YLM2.clm, axis=0)
    d_sin[:,k] = np.sum(PLM2[:,:,k]*YLM2.slm, axis=0)

# Multiplying by c/s(phi#m) to get spatial maps (lon,lat)
grid.data = np.sqrt(np.dot(ccos.T,d_cos) + np.dot(ssin.T,d_sin)).T
grid.mask = np.zeros_like(grid.data, dtype=bool)

Create plot of GRACE/GRACE-FO degree spectra

fig, ax1 = plt.subplots(num=1, nrows=1, ncols=1)
ax1.plot(delta_Ylms.l[1:], delta_Ylms.amplitude[1:], color='red', linewidth=2)
ax1.set_xlabel('Degree [l]', fontsize=13)
ax1.set_ylabel(f'{unit_name} [{unit_label}]', fontsize=13)
ax1.set_yscale('log')
ax1.set_xlim(0, LMAX)
ax1.grid(True, which='both', linestyle='-', color='grey')
ax1.set_title('GRACE/GRACE-FO Error Degree Amplitude')
plt.show()

Create GRACE/GRACE-FO error map

# slider for the plot min and max for normalization
vmax = np.ceil(np.max(grid.data)).astype(np.int64)
cmap = gravtk.tools.colormap(vmin=0, vmax=vmax)
# display widgets for setting GRACE/GRACE-FO plot parameters
ipywidgets.VBox([cmap.range,cmap.step,cmap.name,cmap.reverse])
fig, ax2 = plt.subplots(num=2, nrows=1, ncols=1, figsize=(10.375,6.625),
    subplot_kw=dict(projection=ccrs.PlateCarree()))

# levels and normalization for plot range
im = ax2.imshow(grid.data, interpolation='nearest',
    norm=cmap.norm, cmap=cmap.value, transform=ccrs.PlateCarree(),
    extent=grid.extent, origin='upper')
ax2.coastlines('50m')

# Add horizontal colorbar and adjust size
# extend = add extension triangles to upper and lower bounds
# options: neither, both, min, max
# pad = distance from main plot axis
# shrink = percent size of colorbar
# aspect = lengthXwidth aspect of colorbar
cbar = plt.colorbar(im, ax=ax2, extend='both', extendfrac=0.0375,
    orientation='horizontal', pad=0.025, shrink=0.85,
    aspect=22, drawedges=False)
# rasterized colorbar to remove lines
cbar.solids.set_rasterized(True)
# Add label to the colorbar
cbar.ax.set_xlabel(f'{unit_name} [{unit_label}]',
    labelpad=10, fontsize=24)
# Set the tick levels for the colorbar
cbar.set_ticks(cmap.levels)
cbar.set_ticklabels(cmap.label)
# ticks lines all the way across
cbar.ax.tick_params(which='both', width=1, length=26, labelsize=24,
    direction='in')
    
# stronger linewidth on frame
ax2.spines['geo'].set_linewidth(2.0)
ax2.spines['geo'].set_capstyle('projecting')
# adjust subplot within figure
fig.patch.set_facecolor('white')
fig.subplots_adjust(left=0.02,right=0.98,bottom=0.05,top=0.98)
plt.show()