Using SWOT satellite altimetry

This example will guide you through each step required to estimate energy cascade rates from a single SWOT KaRIn swath.

General procedure

  1. Load and visualize the SWOT sea surface height anomaly (SSHA) data

  2. Use SSHA to estimate geostrophic velocities

  3. Subset the data to a small region

  4. Calculate various structure functions from the geostrophic velocities in the subset region

  5. Estimate cascade rates from the structure functions

  6. Plot cascade rates

  7. Plot 2nd order structure functions

[1]:
import matplotlib_inline.backend_inline
import seaborn as sns

sns.set_style(style="white")
sns.set_context("talk")

matplotlib_inline.backend_inline.set_matplotlib_formats("png", dpi=200)

Load the SWOT NetCDF file and visualize the data

We will use xarray to load the .nc file. We are using the Expert version of SWOT Level-3 LR SSH v1.0, available through AVISO. This version includes estimates of geostrophic velocity along with improved data post-processing. It is possible to estimate geostrophic velocities from SSH data only, which is available in the Basic version of Level-3 and Level-2.

Note: The SWOT data is not provided in the FluidSF repository, so you must instead access the data through PO.DAAC, AVISO, or other methods. This example uses L3 data, accessible through AVISO.

[2]:
import xarray as xr

# Provide the path to the SWOT data you have downloaded
ds = xr.open_dataset('example_data/SWOT_L3_LR_SSH_Expert_SAMPLE.nc')
[3]:
ds
[3]:
<xarray.Dataset>
Dimensions:               (num_lines: 9860, num_pixels: 69, num_nadir: 1631)
Coordinates:
    latitude              (num_lines, num_pixels) float64 ...
    longitude             (num_lines, num_pixels) float64 ...
Dimensions without coordinates: num_lines, num_pixels, num_nadir
Data variables: (12/18)
    time                  (num_lines) datetime64[ns] ...
    mdt                   (num_lines, num_pixels) float64 ...
    ssha                  (num_lines, num_pixels) float64 ...
    ssha_noiseless        (num_lines, num_pixels) float64 ...
    ssha_unedited         (num_lines, num_pixels) float64 ...
    quality_flag          (num_lines, num_pixels) uint8 ...
    ...                    ...
    ugosa                 (num_lines, num_pixels) float64 ...
    vgosa                 (num_lines, num_pixels) float64 ...
    sigma0                (num_lines, num_pixels) float64 ...
    cross_track_distance  (num_pixels) float64 ...
    i_num_line            (num_nadir) int16 ...
    i_num_pixel           (num_nadir) int8 ...
Attributes: (12/42)
    Conventions:                     CF-1.7
    Metadata_Conventions:            Unidata Dataset Discovery v1.0
    cdm_data_type:                   Swath
    comment:                         Sea Surface Height measured by Altimetry
    geospatial_lat_units:            degrees_north
    geospatial_lon_units:            degrees_east
    ...                              ...
    geospatial_lat_min:              -78.272196
    geospatial_lat_max:              78.272247
    geospatial_lon_min:              0.000835
    geospatial_lon_max:              359.99901
    data_used:                       SWOT KaRIn L2_LR_SSH PGC0 (NASA/CNES). D...
    doi:                             https://doi.org/10.24400/527896/A01-2023...

Plot the SSH anomaly data with and without noise removal

[4]:
import cartopy.crs as ccrs
import matplotlib.pyplot as plt

localbox = [-90, -30, 20, 60]
localbox_subset = [-49, -41, 44, 50]

fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(12, 12),
                               subplot_kw={"projection": ccrs.PlateCarree()})
ax1.set_extent(localbox)
ax2.set_extent(localbox)
plot_kwargs = {
    "x": "longitude",
    "y": "latitude",
    "cmap": "Spectral_r",
    "vmin": -0.2,
    "vmax": 0.2,
    "cbar_kwargs": {"shrink": 0.9},}

# Plot SSHA with and without noise removal
ds.ssha.plot.pcolormesh(ax=ax1, **plot_kwargs)
ds.ssha_noiseless.plot.pcolormesh(ax=ax2, **plot_kwargs)
ax1.coastlines()
gl1 = ax1.gridlines(draw_labels=True)
gl1.top_labels = False
gl1.right_labels = False
gl1.bottom_labels = False
ax2.coastlines()
gl2 = ax2.gridlines(draw_labels=True)
gl2.top_labels = False
gl2.right_labels = False

plt.tight_layout()
_images/swot_ex_7_0.png

Plot the zonal geostrophic velocity

[5]:
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(12, 12),
                               subplot_kw={"projection": ccrs.PlateCarree()})
ax1.set_extent(localbox)
ax2.set_extent(localbox_subset)
plot_kwargs = {
    "x": "longitude",
    "y": "latitude",
    "cmap": "RdBu_r",
    "vmin": -1,
    "vmax": 1,
    "cbar_kwargs": {"shrink": 0.9},}

# Plot geostrophic zonal velocity in large and small domains
ds.ugos.plot.pcolormesh(ax=ax1, **plot_kwargs)
ds.ugos.plot.pcolormesh(ax=ax2, **plot_kwargs)
ax1.coastlines()
gl1 = ax1.gridlines(draw_labels=True)
gl1.top_labels = False
gl1.right_labels = False
ax2.coastlines()
gl2 = ax2.gridlines(draw_labels=True)
gl2.top_labels = False
gl2.right_labels = False

plt.tight_layout()
_images/swot_ex_9_0.png

Calculate the velocity-based structure functions

We’ll calculate multiple types of structure functions here, including traditional 2nd (SF_LL) and 3rd (SF_LLL) order and advective structure functions (SF_advection_velocity). We calculate the structure functions in two directions, across the swath (_x) and along the swath (_y). FluidSF also calculates the separation distances in both directions (x-diffs and y-diffs).

[6]:
import warnings

import fluidsf

warnings.filterwarnings("ignore") # Ignore warnings for the purpose of this tutorial

# Subset the data to a smaller region
ds_cut = ds.where(
            (ds.latitude > localbox_subset[2])
            & (ds.latitude < localbox_subset[3])
            & (ds.longitude < 360+localbox_subset[1])
            & (ds.longitude > 360+localbox_subset[0]),
            drop=True,
        )

# Define the grid spacing in meters
dx = 2000
dy = 2000

# Define x and y positions based on number of pixels and lines and dx/dy
x = dx * ds_cut.num_pixels.values
y = dy * ds_cut.num_lines.values

# Compute the structure functions
sf = fluidsf.generate_structure_functions_2d(u = ds_cut.ugos.data, v = ds_cut.vgos.data,
                                          x = x, y = y, sf_type=["ASF_V", "LLL", "LL"],
                                          boundary=None, grid_type="uniform")

Check which keys have data in the sf dictionary. Other keys are available but have been initialized to None.

[7]:
for key in sf.keys():
    if sf[key] is not None:
        print(key)
SF_advection_velocity_x
SF_advection_velocity_y
SF_LL_x
SF_LL_y
SF_LLL_x
SF_LLL_y
x-diffs
y-diffs

Estimate cascade rates with the structure functions

[8]:
sf['SF_LLL_x_epsilon'] = sf["SF_LLL_x"]/(3*sf["x-diffs"]/2)
sf['SF_LLL_y_epsilon'] = sf["SF_LLL_y"]/(3*sf["y-diffs"]/2)

sf['SF_advection_velocity_x_epsilon'] = sf["SF_advection_velocity_x"]/2
sf['SF_advection_velocity_y_epsilon'] = sf["SF_advection_velocity_y"]/2

Plot the cascade rates

We computed two types of structure functions (advective and traditional 3rd order) in the across-track (x) and along-track (y) directions, so we will plot 4 different estimates of the cascade rate as a function of separation distance (in km).

[9]:
fig, ax = plt.subplots(figsize=(9,6))

# Plot the cascade rates derived from traditional third order structure functions
plt.semilogx(sf["x-diffs"]/1e3, sf['SF_LLL_x_epsilon'], label="LLL across-track",
             color='tab:blue', linestyle=':')
plt.semilogx(sf["y-diffs"]/1e3, sf['SF_LLL_y_epsilon'], label="LLL along-track",
             color='tab:blue', linestyle='-')

# Plot the cascade rates derived from advective structure functions
plt.semilogx(sf["x-diffs"]/1e3, sf['SF_advection_velocity_x_epsilon'],
             label="ASF_V across-track", color='tab:red', linestyle=':')
plt.semilogx(sf["y-diffs"]/1e3, sf['SF_advection_velocity_y_epsilon'],
             label="ASF_V along-track", color='tab:red', linestyle='-')

plt.hlines(0, 1e0, 1e3, lw=1, color="k",zorder=0)
plt.xlim(1e0, 1e3)
plt.ylim(-5e-7, 5e-7)
plt.xlabel("Separation distance [km]")
plt.ylabel(r"Inverse energy cascade rate [m$^2$s$^{-3}$]")
plt.legend()

plt.tight_layout()
_images/swot_ex_17_0.png

Plot the 2nd order traditional structure functions

These structure functions are related to the energy spectrum and are therefore also useful for analysis.

[10]:
fig, ax = plt.subplots(figsize=(9,6))


plt.loglog(sf["x-diffs"][1:]/1e3, sf['SF_LL_x'][1:], label="Across-track",
           color='tab:blue', linestyle=':')
plt.loglog(sf["y-diffs"]/1e3, sf['SF_LL_y'], label="Along-track",
           color='tab:blue', linestyle='-')

plt.xlim(1e0, 1e3)
plt.xlabel("Separation distance [km]")
plt.ylabel("LL structure function")
plt.legend()

plt.tight_layout()
_images/swot_ex_19_0.png

Binned structure functions

You can also provide an argument to fluidsf.generate_structure_functions_2d() that bins the data. This may be useful when you have more data in one direction.

[11]:
sf_binned = fluidsf.generate_structure_functions_2d(u = ds_cut.ugos.data,
                                                 v = ds_cut.vgos.data,
                                                 x = x, y = y,
                                                 sf_type=["LL"],
                                                 boundary=None, grid_type="uniform",
                                                 nbins=20)
[12]:
fig, ax = plt.subplots(figsize=(9,6))


plt.loglog(sf_binned["x-diffs"][1:]/1e3, sf_binned['SF_LL_x'][1:], label="Across-track",
           color='tab:blue', linestyle=':')
plt.loglog(sf_binned["y-diffs"]/1e3, sf_binned['SF_LL_y'], label="Along-track",
           color='tab:blue', linestyle='-')

plt.xlim(1e0, 1e3)
plt.xlabel("Separation distance [km]")
plt.ylabel("LL structure function")
plt.legend()

plt.tight_layout()
_images/swot_ex_22_0.png