Overview
swot is the largest ODSL subpackage. It handles the Surface Water and Ocean
Topography (SWOT) Level-2 Low-Rate Sea Surface Height products end to end:
searching NASA Earthdata, downloading per-cycle granules, subsetting swaths into
tidy per-pass "cubes", regridding swaths onto lat/lon or Cartesian grids,
finding and printing satellite passes over a region, plotting swath maps, and
performing geometric eddy/internal-wave separation on SSHA fields.
New here? Step-by-step user tutorials live in
docs/tutorials/, starting with Getting started — environment, data layout on/spray, downloading, pass finding, subset cubes, regridding, and the PIXC pixel cloud.
Data products handled:
- L2 LR SSH — Expert (
SWOT_L2_LR_SSH_Expert): the primary 2 km smoothed KaRIn SSHA product (defaultssha_karin_2), with quality flags, crossover correction (height_cor_xover), and internal-tide correction (internal_tide_hret). - L2 LR SSH — Unsmoothed: native-resolution swath, stored in left/right NetCDF groups.
- L2 HR PIXC (
SWOT_L2_HR_PIXC_D): high-rate water-mask pixel cloud (point-cloud product,/pixel_cloudgroup) — handled by theL2_HR_PIXC*scripts.
Default data root is the lab's /spray/swot/versionD storage
(DEFAULT_SWOT_ROOT / DATA_ROOT), laid out as cycle_###/ subfolders.
Per-cycle CSV manifests and link lists live in ../database/. Regridded output
defaults to /spray/swot/regridded. All roots are module constants that are
overridable via CLI flags.
The package is importable two ways — import swot and import odsl_code.swot.
Reusable logic lives in swot/utils.py; the scripts stay thin.
Installation & dependencies
python -m pip install -e ".[all]" # everything
# or minimal + only what swot needs:
python -m pip install -e ".[plot,swot-passes]"
- Core deps (always installed):
numpy,pandas,xarray,netCDF4,h5netcdf,scipy,matplotlib,pyproj,pyresample,earthaccess,tqdm,requests. These cover download, subset, regrid, and the geometric separation scripts. plotextra (cartopy): map plotting inplot_swath_map.py,plot_subset_cube.py(--map),subset_expert_cube.py(--plot),regrid_swot_l2.py(--plot), and the demoplot_*.py.swot-passesextra (geopandas,shapely): required byfind_swot_passes_science.py, which intersects a bbox against the SWOT orbit ground-track shapefiles (download separately from AVISO).krigingextra (pykrige): only forregrid_swot_l2.py --method kriging.- Earthdata credentials come from
~/.netrc(machine urs.earthdata.nasa.gov). CMR collection concept IDs are cached incollection_ids.jsonand refreshed on miss. combine_passes.pyneeds the optionalmpiextra (mpi4py); run it undermpirun(not part of.[all]).python -m pip install -e ".[mpi]"thenmpirun -n 8 python swot/combine_passes.py ….
Scripts & entry points
| Script | Console entry point | Purpose |
|---|---|---|
| Unified CLI | ||
swath_cli.py |
odsl-swot-swath |
One command dispatching to the swath scripts: subset (--product expert\|unsmoothed), regrid, plot, pixc-download, pixc-plot. Forwards remaining args to the underlying script (so odsl-swot-swath <cmd> --help shows its options). The standalone commands below keep working. |
| Download | ||
swot_downloader.py |
odsl-swot-download |
Query CMR for SWOT LR SSH links, save per-cycle CSVs, download missing files through common.sync (--jobs, --verify, --dry-run, --overwrite). Default (no args): most recent cycle, Expert + Unsmoothed together. |
get_swot_expert_granule_link.py |
odsl-swot-granule-link |
Print the download link(s) for a single Expert granule given cycle + pass. |
L2_HR_PIXC.py |
odsl-swot-swath pixc-download |
Discover & download L2 HR PIXC granules. Default --out-dir is /spray/swot/pixc. |
| Passes | ||
build_pass_footprints.py |
odsl-swot-footprints |
Build the segmented pass-footprint index (~120 km along-track segments, bbox + predicted times) from one complete cycle's true swath coordinates. |
pass_footprints.py |
— | Query that index: which passes/cycles/segments cross a bbox (+ time window), opening no data files. |
find_swot_passes_science.py |
— | Find science-phase fly-by times through a bbox using AVISO orbit shapefiles (--shapefile-dir, default /spray/swot/orbits; -sw_corner / -ne_corner aliases kept). |
p_find_swot_passes_expert.py |
— | Scan downloaded Expert files and record which passes fall in a box (writes results.csv). |
p_print_swot_passes.py |
— | Print pass numbers flagged in-box from results.csv. |
| Subset | ||
subset_expert_cube.py |
odsl-swot-subset |
Stack one pass across a cycle range into an (n_cycle, …) Expert SSHA cube; optional map plot. |
subset_unsmoothed_cube.py |
odsl-swot-swath subset --product unsmoothed |
Same for the Unsmoothed product (left/right/both swath sides). |
combine_passes.py |
— | MPI: combine all passes of each cycle into one per-cycle NetCDF (phase-aware pass count). |
pass_mean.py |
odsl-swot-pass-mean |
Per-pass temporal statistics (mean/std/median/count) of Expert SSHA across science cycles (default 1–52); one NetCDF per pass, idempotent, --jobs parallel. |
| Regrid | ||
regrid_swot_l2.py |
odsl-swot-regrid |
Regrid Expert/Unsmoothed swaths onto a lat/lon or Cartesian grid via common.swath_regrid; batch + plot. |
build_uniform_grid.py |
odsl-swot-build-1km-grid |
Build the uniform 1 km merged-swath coordinate files used by the Unsmoothed OI regrid. |
regrid_unsmoothed_to_1km.py |
odsl-swot-regrid-1km |
Regrid Unsmoothed SSHA onto that 1 km grid with local OI. |
unsmoothed_fft_oi.py |
— | QC + FFT2 4 km denoise + 250 m OI tiles (/spray/swot/statistics/oi_250m_fft4km/). |
extract_unsmoothed_tiles.py |
— | Extract 40×40 Unsmoothed tiles to HDF5 for the noise catalog. |
oi_regrid_tile.py |
— | Single-tile OI on swath-aligned coordinates. |
plot_regridded_segment.py |
— | Plot a latitude band from a 1 km regridded pass. |
| Plot | ||
plot_subset_cube.py |
odsl-swot-plot-cube |
Multi-panel plot of a subset cube across cycles (imshow or Cartopy --map). |
plot_swath_map.py |
odsl-swot-swath-map |
Scatter map of raw swath pixels for given passes or a time/bbox window. |
L2_HR_PIXC_plot.py |
odsl-swot-swath pixc-plot |
Summary maps of PIXC point-cloud variables from one granule. |
demo_plot_gulf_america_expert_ssha.py |
— | Demo: Expert SSHA over the Gulf of America for a date range. |
examples/plot_amazon_soliton.py, examples/plot_arctic_view.py, examples/plot_GoA_example.py |
— | Throwaway demos with hardcoded regions/paths (see gotchas). |
| Geometric separation | ||
geometric_separation_eddy_waves_ssh.py |
— | TV + directional-sparsity separation of SSHA into eddy vs internal-wave parts. |
geometric_separation_eddy_waves_ssha_curvelets.py |
— | Same, using a curvelet (UDCT) sparse wave transform. |
| Shared | ||
utils.py |
— | File discovery, subsetting, corrections, plotting, point loading (imported by the above). |
Usage
All bounding boxes are west south east north in degrees. Examples below use a
Gulf of America box -98 18 -80 31.
Unified swath CLI (odsl-swot-swath)
One entry point that dispatches to the swath scripts. Everything after the
subcommand is forwarded verbatim, so the standalone commands (odsl-swot-subset,
odsl-swot-regrid, odsl-swot-swath-map, …) and this dispatcher accept the same
options:
odsl-swot-swath --help # list subcommands
odsl-swot-swath subset --help # forwards --help to the subset script
# Subset one pass across cycles (Expert is the default product)
odsl-swot-swath subset --cycle-start 20 --cycle-end 30 --pass-number 26 \
--lat-range 18 31
# ...or the Unsmoothed product (left/right/both sides)
odsl-swot-swath subset --product unsmoothed --cycle-start 20 --cycle-end 30 \
--pass-number 26 --lat-range 18 31 --side both
# Regrid a granule onto a 0.1° lat/lon grid
odsl-swot-swath regrid --input /spray/swot/versionD/cycle_020/SWOT_..._Expert_020_026_*.nc \
--bbox -98 18 -80 31 --resolution-deg 0.1 --output tmp/regrid.nc
# Scatter-map raw swath pixels over a time/bbox window
odsl-swot-swath plot --bbox -98 18 -80 31 --start 2024-06-01 --end 2024-06-15
# L2 HR PIXC (pixel cloud)
odsl-swot-swath pixc-download --start 2024-06-01 --end 2024-06-02 --bbox -98 18 -80 31
odsl-swot-swath pixc-plot /path/to/SWOT_L2_HR_PIXC_*.nc
Download (odsl-swot-download)
# Default: most recent cycle, Expert + Unsmoothed together (auto-detected from CMR)
odsl-swot-download
# Most recent cycle, Expert only
odsl-swot-download --product-type Expert
# Download Expert product for an explicit cycle range 20–30 into the default /spray root
odsl-swot-download --cycle-start 20 --cycle-end 30 --product-type Expert
# Custom root/manifest dir, with an extra HEAD validation before each download
odsl-swot-download --cycle-start 1 --cycle-end 5 --product-type Expert Unsmoothed \
--data-root /scratch/swot --database-dir /scratch/swot/database \
--provider POCLOUD --validate-urls --verify-downloads
With no cycle range, the downloader auto-detects the single most recent cycle
(by observation time, i.e. the ongoing science phase — not the higher-numbered
cal/val cycles) and fetches its missing granules. --product-type accepts
multiple products; pass --no-latest to fall back to the explicit cycle range.
Get a single granule link:
odsl-swot-granule-link 20 13 # cycle 20, pass 13 (preferred link)
odsl-swot-granule-link 20 13 --all # every matching link
Subset into a per-pass cube (odsl-swot-subset)
odsl-swot-subset --cycle-start 20 --cycle-end 40 --pass 13 \
--lat-range 18 31 \
--base-folder /spray/swot/versionD \
--product-type Expert \
--variables ssha_karin_2 \
--output /scratch/subsets/pass_013_cycle_020-040.nc \
--plot --plot-variable ssha_karin_2 --overwrite
Unsmoothed variant (choose swath side):
odsl-swot-subset --cycle-start 20 --cycle-end 40 --pass 13 \
--lat-range 18 31 --side left # subset_unsmoothed_cube.py
Regrid a swath (odsl-swot-regrid)
# Gaussian-interpolate one Expert file onto a 0.02° lat/lon grid over the Gulf
odsl-swot-regrid --input /spray/swot/versionD/cycle_020/SWOT_L2_LR_SSH_Expert_020_013_*.nc \
--variable ssha_karin_2 \
--bbox -98 18 -80 31 --resolution-deg 0.02 \
--method gaussian --radius-of-influence 20000 \
--output /scratch/grids/c020_p013_ssha.nc \
--plot --cmap RdBu_r
# Batch: glob many files onto a regional Cartesian grid, output to a dir
odsl-swot-regrid --input "/spray/swot/versionD/cycle_0*/SWOT_L2_LR_SSH_Expert_*_013_*.nc" \
--grid-type cartesian --center-lon -89 --center-lat 25 \
--width-m 1500000 --height-m 1200000 --dx-m 2000 \
--output-dir /scratch/grids --plot-dir /scratch/grids/png
Unsmoothed 1 km OI (odsl-swot-regrid-1km)
Swath-aligned local OI onto the prebuilt 1 km grids. See tutorial 8.
odsl-swot-regrid-1km --cycle 3 --pass 1
odsl-swot-regrid-1km --cycle 3 --pass 1 --search-radius 1500 --half-decay-scale 800 --workers 8
python swot/unsmoothed_fft_oi.py --cycle 6 --pass 350 # 250 m FFT tiles
python swot/unsmoothed_fft_oi.py --workers 8 # mass job, resume-safe
QC on Unsmoothed pixels: additive height_cor_xover, ssha_karin_2_qual==0,
open-ocean surface flag, |ssha|≤3 m. Output is swath-aligned (no rotation to
north). Default two parameter sets land in ssha/uncert with a n_param
axis.
Plot
# Multi-panel cube across cycles, on Cartopy maps
odsl-swot-plot-cube /scratch/subsets/pass_013_cycle_020-040.nc \
--variable ssha_karin_2 --map --bbox -98 18 -80 31 \
--max-panels 12 --cmap RdBu_r --output /scratch/png/pass013_panels.png
# Raw swath scatter for explicit cycle:pass pairs
odsl-swot-swath-map --cycle-pass 20:013 20:026 \
--bbox -98 18 -80 31 --variable ssha_karin_2 \
--max-quality 2 --output /scratch/png/swath.png
# ...or discover passes from a time window + bbox
odsl-swot-swath-map --start 2024-06-01 --end 2024-06-03 \
--bbox -98 18 -80 31 --output /scratch/png/swath.png
Pass footprint index (fast spatial pruning)
SWOT flies a fixed repeat orbit, so a pass number always traces the same
footprint. build_pass_footprints.py reads the true swath lat/lon/time
from one complete 584-pass cycle and stores ~120 km along-track segments
(60 lines each) with tight bounding boxes and along-track time offsets. Because
a whole pass is a long, narrow, diagonal ribbon, its full bbox is nearly
useless, but a 120 km segment's bbox is near-square and hugs the swath — so a
plain bbox test prunes ~90% of passes for a regional query without opening any
granule. The precise repeat period (20.864637 d) turns segment time offsets
into predicted UTC times for every cycle.
# Build once (default: reference cycle 002 -> GeoParquet under versionD/footprints/)
odsl-swot-footprints # ~96k segments, 584 passes
# Query in Python (opens no data files):
python -c "
from swot.pass_footprints import candidate_cycle_passes
print(sorted(candidate_cycle_passes((-80,25,-60,45), '2024-05-01', '2024-05-03')))"
The colocate SWOT provider uses this index automatically to skip passes whose
swath misses the region (no-op if the index hasn't been built).
Geometric eddy/wave separation
python swot/geometric_separation_eddy_waves_ssh.py \
--input /scratch/grids/c020_p013_ssha.nc \
--alpha 0.02 --beta 0.05 --outer 20 --plot
python swot/geometric_separation_eddy_waves_ssh.py --demo # synthetic self-test
Conventions & gotchas
- Bounding boxes are
west south east north. Both[-180, 180)and[0, 360)longitude conventions are accepted and normalized internally (pyresample wants[-180, 180)); reuse the longitude-wrap helpers inutils.pyrather than re-rolling. - Idempotent downloads/subsets. The downloader skips files already present;
subset scripts refuse to clobber an existing
--outputunless--overwriteis passed. Downloads land under thecycle_###/layout beneath--data-root. - SWOT restarted cycle numbering between its two orbits, so cycle number is not monotonic in time and maps to different geometry / pass counts:
- Science phase (21-day repeat): cycles numbered from 1 (≈50 by 2026),
584 passes/cycle — the ongoing data (
SCIENCE_PASSES_PER_CYCLE = 584). - Cal/val phase (1-day repeat, early 2023): cycles numbered high
(~474–577), 28 passes/cycle (
CALVAL_PASSES_PER_CYCLE = 28).combine_passes.pypicks the count per cycle via a threshold:cycle >= 474is treated as cal/val (CALVAL_CYCLE_MINinswot.constants). Override with--calval-cycle-minor force a count with--expected-passes. Getting this wrong used to silently drop passes (out-of-range placement raisedIndexError, which was caught and swallowed). Placement now fails loudly. Run under MPI, e.g.mpirun -n 8 python swot/combine_passes.py --cycle-start 1 --cycle-end 30 …(the older--snake_casespellings, e.g.--cycle_start, still work as aliases). - Corrections. SSHA regridding/subsetting add
height_cor_xover(crossover) andinternal_tide_hret(internal tide) by default; disable with--no-xover-correction/--no-internal-tide-correction(regrid) or--no-internal-tide-correction(subset). Default quality gate keeps pixels with quality flag<= 2(DEFAULT_MAX_QUALITY); crossover<= 1. - Geometric separation scripts share helpers from
common.field_ops(apply_fft_multiplier,finite_mask,grad_forward,k_from_lambda_km,replace_nans) — keep changes to those functions incommon/, not copied into both scripts. - Throwaway demos live under
examples/:examples/plot_amazon_soliton.py,examples/plot_arctic_view.py, andexamples/plot_GoA_example.pyhave hardcoded regions, absolute local paths (e.g.sys.path.append('/home/jinbo.wang/projects/oceanic/')), and expect a legacyoceanic.swot_sshmodule. Treat them as scratch examples, not entry points. find_swot_passes_science.pykeeps single-dash aliases (-sw_corner,-ne_corner,-output_filename) and accepts--bbox/--shapefile-dir. Orbit shapefiles default to/spray/swot/orbits(download_swot_orbit.sh).
Related
../common— shared swath→grid regridding (swath_regrid, built on pyresample) and field ops (common.field_ops) used by the regrid and geometric-separation scripts.../README.md— repo-wide conventions: CLI/utils.pystructure, data roots on/spray, Earthdata/.netrcauth, idempotency, and the SWOT cycle-numbering note summarized above.