Access Along-track, Sea Surface Elevation from NASA’s SWOT Mission
Requirements
- Earthdata login (EDL) credentials.
- Concept Collection ID or DOI for the relevant data product.
- Python >= 3.11.
- Mamba-forge (or conda-forge) installed on the machine.
- Familiarity with Jupyter notebooks and Jupyter Lab.
Optional
- Store all EDL credentials in a
.netrcfile. - Basic knowledge of conda environment installation.
Objectives
To download 3 months of Sea Surface Height (SSH) and Sea Surface Temperature data in a region near Iceland over the Iceland-Faroe_Front. The spatial and temporal range is defined by the following parameters:
- Time range: 01/09/2023 – 12/31/2023.
- Spatial range: 20.7 < longitude < -4.3, and 60 < latitude < 65.67
To accomplish this goal above, the tutorial will demonstrate how to:
- Authenticate (via
earthaccess). - Search for all available NASA OPeNDAP URLs for a specific NASA collection. The search will further filter by Longitude, Latitude, and time range.
- Subset with OPeNDAP, by variable name and spatial / temporal range.
Install required python dependencies
$ mamba create -n opendap_env -c conda-forge python=3.12 ipython jupyterlab earthaccess netCDF4
$ mamba activate opendap_env
$ pip install git+https://github.com/pydap/pydap.git
$ jupyter lab
Once in the jupyter notebook environment, import in the first cell all necessary methods that will be used to stream remote data into a local file:
import xarray as xr
import datetime as dt
import earthaccess
import numpy as np
# import pydap-specific tools
from pydap.client import get_cmr_urls, open_url
from pydap.client import to_netcdf as dap_to_netcdf
Finding OPeNDAP URLs with PyDAP
The needed parameter to search for all Sea Surface Height Swath (Level 2) data from the SWOT mission that is available through OPeNDAP is
Concept Collection ID = C2799438313-POCLOUD
Data from the above collection is a Level 2 data product, meaning SWATH data, and all remote files span different longitudes and latitudes. In this case, the necessary first step is to filter the search for all relevant data URLs by a bounding box. Later on, a further subset by coordinate values will be done by OPeNDAP.
To learn how to find the concept collection id for a specific data product, click the button below:
Below are the required parameters to search for all OPeNDAP URLs using PyDAP's get_cmr_urls:
swot_ccid = "C2799438313-POCLOUD"
time_range = [dt.datetime(2023, 9, 1), dt.datetime(2023, 12, 31)]
# select a region in the Iceland-Faroe Ridge
bbox = [-20.760731, 60.080727, -4.297294,65.675099] #[west, south, east, north]
cmr_urls = get_cmr_urls(ccid=swot_ccid, bounding_box = bbox, time_range=time_range, limit=1000) # you can incread the limit of results
cmr_urls[:6]
['https://opendap.earthdata.nasa.gov/collections/C2799438313-POCLOUD/granules/SWOT_GPR_2PfP402_005_20230116_124113_20230116_133219',
'https://opendap.earthdata.nasa.gov/collections/C2799438313-POCLOUD/granules/SWOT_GPS_2PfP402_005_20230116_124113_20230116_133219',
'https://opendap.earthdata.nasa.gov/collections/C2799438313-POCLOUD/granules/SWOT_GPN_2PfP402_005_20230116_124113_20230116_133219',
'https://opendap.earthdata.nasa.gov/collections/C2799438313-POCLOUD/granules/SWOT_GPN_2PfP402_016_20230116_220315_20230116_225421',
'https://opendap.earthdata.nasa.gov/collections/C2799438313-POCLOUD/granules/SWOT_GPS_2PfP402_016_20230116_220315_20230116_225421',
'https://opendap.earthdata.nasa.gov/collections/C2799438313-POCLOUD/granules/SWOT_GPR_2PfP402_016_20230116_220315_20230116_225421']
NOTE: The CMR search yields OPeNDAP URLs that below to two categories:
- Standard Product
- Expert Products.
It is important to identidy whether a CMR search for a Collection data yields uniform results or not. In this case it does not, and we neeed to further filter these. We are interested int he "Standard Product".
# Identify the STANDARD Data Product (There is also the Expert data Product
swot_standard_urls = [url for url in cmr_urls if url.split('/')[-1][:8]=='SWOT_GPN']
EDL Authentication with earthaccess and OPeNDAP
There are various ways to authenticate with NASA, and here we will use earthaccess to retrieve a session object containing all required credentials to access data.
When using earthaccess to "login", you need to define a strategy and you have two options:
- If you already have a
.netrcfile with your EDL credentials stored in your machine, setstrategy="netrc" - If you DO NOT have a
.netrcfile with your EDL credentials, or you are not sure, do insteadstrategy="interactive"
from earthaccess.exceptions import LoginStrategyUnavailable
try:
auth = earthaccess.login(strategy="netrc", persist=True)
except LoginStrategyUnavailable:
# you will be prompted to add your EDL credentials
auth = earthaccess.login(strategy="interactive", persist=True)
# pass Token Authorization to a new Session.
my_session = session=auth.get_session()
The object my_session contains your EDL credentials, and it will be used to retrieve data from OPeNDAP. Moreover, by adding a persist=True as an argument to earthaccess.login, a .netrc is created to stored your EDL credentials in the machine for later reuse.
Use OPeNDAP to subset data by coordinate values and variable names
One may think that the CMR search for OPeNDAP URLs already returned the desired subset of data when we provided a bounding box in the CMR query. That is unfortunately not the case. The CMR did filter using the bounding box, and returns ALL OPeNDAP URLs containing data within the bounding box. This is, it is usually the case that an OPeNDAP returned from the CMR query contains only 1% of data within the bounding box, as opposed to all data within the bounding box.
How can I download only the data within the bounding box? This can be done with OPeNDAP in a two stage process.
- Download ONLY coordinate data from each granule, to identify the slice that is needed to download only the relevant data from out desired data of interest.
- Use the identified slice from each OPeNDAP URL, to stream data into a local file for analysis.
Stage 1
For SWATH data, Coordinate arrays such as Latitude and Longitude are NOT the dimensions of the dataset. Typically time or Track are the dimensions of SWATH data. Moreover, Latitude is usually NOT monotonic, but Longitude is! Below we download time and Longitude from ALL identified OPeNDAP data URLs and idenfity the slice for the time dimension that yields data within out bounding box.
NOTE: We are choosing NOT to download Latitude to speed up the workflow. Latitude and Longitude arrays can be very large for the newer data products! Identifying the slice for the time dimension that produces Longitude data within our bounding box will likely produce Latitude data within the Bounding box. Any further subset can take place once data has been downloaded.
Below is the code used that
- Subsets by Variable Name: Only
timeandLongitudewill be downloaded. - Identifies the time slice: Iterates over all downloaded granules to identify the logical slice that, when applied to time, it produces Longitude data within the bounding box.
- Cleans: Removes all downloaded data to avoid name collision in the Stage 2.
# Create a PyDAP Dataset Object to Identify Variable Names
pyds = open_url(swot_standard_urls[0], protocol="dap4", session=my_session)
pyds.tree()
.SWOT_GPN_2PfP418_005_20230201_101117_20230201_110222.nc
βββdata_01
β βββc
β β βββswh_cor_ocean_net_instr_qual
β β βββsea_state_bias_mle3
β β βββsig0_ocean_numval
β β βββagc
β β βββswh_cor_ocean_net_instr
β β βββrange_ocean
β β βββswh_ocean_rms
β β βββagc_rms
β β βββsig0_ocean
β β βββrange_ocean_compression_qual
β β βββrange_cor_ocean_net_instr_qual
β β βββsea_state_bias_adaptive
β β βββsig0_ocean_rms
β β βββsea_state_bias
β β βββrange_ocean_rms
β β βββalt_state_band_status_flag
β β βββsig0_ocean_compression_qual
β β βββswh_ocean_numval
β β βββswh_ocean
β β βββsig0_cor_ocean_net_instr_qual
β β βββswh_ocean_compression_qual
β β βββsig0_cor_atm
β β βββrange_cor_ocean_net_instr
β β βββalt_state_c_band_flag
β β βββrange_ocean_numval
β β βββagc_numval
β β βββsig0_cor_ocean_net_instr
β βββku
β β βββiono_cor_alt_filtered_mle3
β β βββiono_cor_alt_filtered_adaptive_qual
β β βββswh_cor_ocean_net_instr_qual
β β βββrange_ocean_mle3_rms
β β βββiono_cor_alt_adaptive
β β βββswh_cor_adaptive_net_instr
β β βββsea_state_bias_mle3
β β βββoff_nadir_angle_wf_ocean_rms
β β βββwvf_main_class
β β βββrange_cor_ocean_mle3_net_instr
β β βββiono_cor_alt_filtered_mle3_qual
β β βββswh_ocean_mle3_compression_qual
β β βββsig0_ocean_numval
β β βββagc
β β βββswh_cor_ocean_net_instr
β β βββiono_cor_alt_filtered
β β βββrange_ocean
β β βββssha_mle3
β β βββsig0_adaptive_rms
β β βββswh_ocean_rms
β β βββagc_rms
β β βββsig0_adaptive
β β βββsig0_ocean
β β βββrange_ocean_compression_qual
β β βββsig0_ocean_mle3_numval
β β βββrange_cor_ocean_net_instr_qual
β β βββiono_cor_alt_filtered_qual
β β βββsea_state_bias_adaptive
β β βββrange_adaptive_compression_qual
β β βββswh_ocean_mle3_numval
β β βββsig0_ocean_mle3
β β βββiono_cor_alt_filtered_adaptive
β β βββoff_nadir_angle_wf_ocean_numval
β β βββsig0_ocean_rms
β β βββrange_cor_adaptive_net_instr
β β βββsea_state_bias
β β βββrange_ocean_mle3_compression_qual
β β βββrange_ocean_rms
β β βββrange_adaptive
β β βββalt_state_band_status_flag
β β βββsig0_ocean_compression_qual
β β βββswh_ocean_numval
β β βββoff_nadir_angle_wf_ocean
β β βββswh_adaptive_numval
β β βββswh_adaptive_compression_qual
β β βββswh_ocean
β β βββiono_cor_gim
β β βββsig0_cor_ocean_net_instr_qual
β β βββiono_cor_alt_mle3
β β βββsea_state_bias_3d_mp2
β β βββsea_state_bias_adaptive_3d_mp2
β β βββsig0_cor_adaptive_net_instr
β β βββsig0_adaptive_numval
β β βββsig0_cor_ocean_mle3_net_instr
β β βββswh_cor_ocean_mle3_net_instr
β β βββswh_ocean_compression_qual
β β βββsig0_cor_atm
β β βββoff_nadir_angle_wf_ocean_compression_qual
β β βββrange_ocean_mle3
β β βββrange_cor_ocean_net_instr
β β βββrange_ocean_mle3_numval
β β βββssha
β β βββrange_adaptive_numval
β β βββswh_adaptive_rms
β β βββsig0_adaptive_compression_qual
β β βββrange_adaptive_rms
β β βββswh_adaptive
β β βββiono_cor_alt
β β βββswh_ocean_mle3_rms
β β βββswh_ocean_mle3
β β βββrange_ocean_numval
β β βββsig0_ocean_mle3_compression_qual
β β βββagc_numval
β β βββsig0_ocean_mle3_rms
β β βββsig0_cor_ocean_net_instr
β βββtime
β βββlongitude
β βββlatitude
β βββwind_speed_mod_v
β βββmean_dynamic_topography_interp_qual
β βββocean_tide_fes_interp_qual
β βββmean_sea_surface_dtu
β βββwave_model_map_availability_flag
β βββrad_water_vapor
β βββsst
β βββrad_side_1_land_frac_340
β βββwind_speed_mod_u
β βββdistance_to_coast
β βββmeteo_measurement_altitude_interp_qual
β βββmodel_dry_tropo_cor_measurement_altitude
β βββrad_tmb_187
β βββmeteo_zero_altitude_interp_qual
β βββpole_tide
β βββrad_tmb_238
β βββsurface_slope_cor
β βββrad_side_2_distance_to_land
β βββrad_side_2_land_frac_238
β βββocean_tide_non_eq
β βββrad_side_2_rain_flag
β βββrad_tb_238
β βββdepth_or_elevation
β βββrad_tb_340
β βββrad_tb_187_qual
β βββrad_tb_238_qual
β βββsea_ice_concentration_interp_qual
β βββrad_side_1_distance_to_land
β βββorb_state_diode_flag
β βββrad_side_1_land_frac_187
β βββsig0_cor_atm_source
β βββrad_side_2_sea_ice_flag
β βββrad_side_2_land_frac_340
β βββalt_state_band_seq_flag
β βββrain_rate
β βββrad_wet_tropo_cor
β βββmodel_dry_tropo_cor_zero_altitude
β βββsolid_earth_tide
β βββload_tide_got
β βββdac
β βββrad_side_1_surface_type_flag
β βββsurface_classification_flag
β βββtime_tai
β βββaltitude
β βββinternal_tide_hret_interp_qual
β βββrad_side_1_rain_flag
β βββrad_side_1_sea_ice_flag
β βββmean_wave_direction
β βββocean_tide_eq
β βββice_flag
β βββrad_tmb_340
β βββrad_wet_tropo_cor_interp_qual
β βββrad_tb_340_qual
β βββrad_tb_187
β βββinv_bar_cor
β βββgeoid
β βββindex_first_20hz_measurement
β βββocean_tide_fes
β βββmean_dynamic_topography
β βββrad_side_2_surface_type_flag
β βββinternal_tide_hret
β βββrad_side_2_land_frac_187
β βββocean_tide_got
β βββaltitude_rate
β βββrad_wind_speed
β βββwave_model_interp_qual
β βββorb_state_rest_flag
β βββangle_of_approach_to_coast
β βββmeteo_map_availability_flag
β βββwind_speed_alt
β βββwind_speed_alt_adaptive
β βββload_tide_fes
β βββwind_speed_alt_mle3
β βββrain_flag
β βββmean_sea_surface_dtu_interp_qual
β βββmean_sea_surface_cnescls
β βββsea_ice_concentration
β βββmean_wave_period_t02
β βββrad_cloud_liquid_water
β βββmodel_wet_tropo_cor_zero_altitude
β βββocean_tide_got_interp_qual
β βββmean_sea_surface_cnescls_interp_qual
β βββrad_side_1_land_frac_238
β βββnumtotal_20hz_measurement
β βββmodel_wet_tropo_cor_measurement_altitude
βββdata_20
βββc
β βββice2_qual
β βββrange_ice2
β βββsigmal_ice2
β βββrange_ocean
β βββsig0_ocean
β βββrange_ocean_compression_qual
β βββpeakiness
β βββsig0_ocean_compression_qual
β βββswh_ocean
β βββrange_ocog
β βββswh_ocean_compression_qual
β βββnum_iterations_ocean
β βββmqe_ocean
β βββslope2_ice2
β βββmqe_ice2
β βββsig0_ocog
β βββsig0_ice2
β βββslope1_ice2
β βββsig0_leading_edge_ice2
βββku
β βββseaice_qual
β βββrange_seaice
β βββnum_iterations_adaptive
β βββice2_qual
β βββmqe_ocean_mle3
β βββwvf_main_class
β βββrange_ice2
β βββswh_ocean_mle3_compression_qual
β βββsigmal_ice2
β βββnum_iterations_ocean_mle3
β βββrange_ocean
β βββsig0_adaptive
β βββsig0_ocean
β βββrange_ocean_compression_qual
β βββmqe_adaptive
β βββrange_tfmra
β βββrange_adaptive_compression_qual
β βββsig0_seaice
β βββsig0_ocean_mle3
β βββsig0_tfmra
β βββpeakiness
β βββrange_ocean_mle3_compression_qual
β βββtfmra_qual
β βββrange_adaptive
β βββsig0_ocean_compression_qual
β βββoff_nadir_angle_wf_ocean
β βββswh_adaptive_compression_qual
β βββswh_ocean
β βββrange_ocog
β βββocog_qual
β βββswh_ocean_compression_qual
β βββoff_nadir_angle_wf_ocean_compression_qual
β βββrange_ocean_mle3
β βββnum_iterations_ocean
β βββmqe_ocean
β βββsig0_adaptive_compression_qual
β βββswh_adaptive
β βββslope2_ice2
β βββmqe_ice2
β βββswh_ocean_mle3
β βββsig0_ocean_mle3_compression_qual
β βββsig0_ocog
β βββsig0_ice2
β βββslope1_ice2
β βββsig0_leading_edge_ice2
βββtime
βββlongitude
βββlatitude
βββmodel_dry_tropo_cor_measurement_altitude
βββsurface_slope_cor
βββdistance_to_coast
βββmeteo_measurement_altitude_interp_qual
βββindex_1hz_measurement
βββalt_state_acq_mode_flag
βββsurface_classification_flag
βββtime_tai
βββaltitude
βββangle_of_approach_to_coast
βββalt_state_track_trans_flag
βββmodel_wet_tropo_cor_measurement_altitude
# Declare Variable Names by their Fully Qualifying Name (FQN) similar to a filepath
Variables = [
"/data_01/time",
"/data_01/longitude",
"/data_20/time",
"/data_20/longitude",
]
output_path = "data/" # <-------- Adapt for your case
Download coordinate data
dap_to_netcdf(swot_standard_urls,
session=my_session,
keep_variables=Variables,
output_path=output_path)
Having downloaded the data, we now identify the index values of Longitude that, for each downloaded granule, yield data within our bounding box.
## get min lat/lon from
minLon, maxLon = bbox[0], bbox[2]
slices = []
# Longitude is spans 0, 360 values. The bounding box has -180,180 format
# So an extra step to turn downloaded lon values into -180,180 format is applied below
for url in swot_standard_urls:
filename = output_path+f"{url.split('/')[-1]}.nc4"
dt1 = xr.open_datatree(filename).load()
# find index /data_01/longitude
longitude = dt1['data_01/longitude']
longitude = longitude.where(longitude <=180, longitude -360)
mask = (longitude >= minLon) & (longitude <= maxLon)
idx = np.nonzero(mask.values)[0]
# find index /data_20/longitude
longitude = dt1['data_20/longitude']
longitude = longitude.where(longitude <=180, longitude -360)
mask = (longitude >= minLon) & (longitude <= maxLon)
idx2 = np.nonzero(mask.values)[0]
# create slice for each granule
slices.append({"/data_01/time":(idx[0],idx[-1]),
"/data_20/time": (idx2[0],idx2[-1])})
# inspect the slices to the first 4 remote granules on List
print(slices[:4])
[{'/data_01/time': (196, 365), '/data_20/time': (3793, 7199)},
{'/data_01/time': (1875, 2436), '/data_20/time': (37341, 48583)},
{'/data_01/time': (241, 465), '/data_20/time': (4581, 9082)},
{'/data_01/time': (2031, 2471), '/data_20/time': (40501, 49316)}]
Finally we clean the downloaded data to avoid filename collisions with the data download in the next step (NOTE replace output_path with your own!)
$ cd path_to_data_replace_here
$ rm SWOT_GPN*.nc4
Stage 2
Now we stream all the data of interest, applying the subset to each remote granule that we just calculated
# Define all Variables of interest using their Fully Qualifying Name
Variables = [
"/data_01/time", "/data_01/longitude", "/data_01/latitude",
"/data_01/sst", "/data_01/mean_dynamic_topography_interp_qual",
"/data_01/surface_classification_flag", "/data_01/ice_flag",
"/data_01/mean_dynamic_topography", "/data_01/rain_flag",
"/data_20/time", "/data_20/longitude", "/data_20/latitude",
"/data_20/distance_to_coast",
]
# Stream data to local file directory
dap_to_netcdf(swot_standard_urls,
session=my_session,
keep_variables=Variables,
dim_slices = slices,
output_path=output_path)
See the code in action below!

References
SWOT. (2024). SWOT Level 2 Nadir Altimeter Geophysical Data Record with Waveforms Version 2.0 [Data set]. NASA Physical Oceanography Distributed Active Archive Center. https://doi.org/10.5067/SWOT-NALT-GDR-2.0
Cite this Tutorial
Jimenez-Urias, M. A. (2026). Access Sea Surface Height (SWATH) Data from SWOT. Zenodo. https://doi.org/10.5281/zenodo.19598977
@misc{jimenez_urias_2026_19598977,
author = {Jimenez-Urias, Miguel Angel},
title = {Access Sea Surface Height (SWATH) Data from SWOT},
month = apr,
year = 2026,
publisher = {Zenodo},
doi = {10.5281/zenodo.19598977},
url = {https://doi.org/10.5281/zenodo.19598977},
}
