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 (default ssha_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_cloud group) — handled by the L2_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.
  • plot extra (cartopy): map plotting in plot_swath_map.py, plot_subset_cube.py (--map), subset_expert_cube.py (--plot), regrid_swot_l2.py (--plot), and the demo plot_*.py.
  • swot-passes extra (geopandas, shapely): required by find_swot_passes_science.py, which intersects a bbox against the SWOT orbit ground-track shapefiles (download separately from AVISO).
  • kriging extra (pykrige): only for regrid_swot_l2.py --method kriging.
  • Earthdata credentials come from ~/.netrc (machine urs.earthdata.nasa.gov). CMR collection concept IDs are cached in collection_ids.json and refreshed on miss.
  • combine_passes.py needs the optional mpi extra (mpi4py); run it under mpirun (not part of .[all]). python -m pip install -e ".[mpi]" then mpirun -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 in utils.py rather than re-rolling.
  • Idempotent downloads/subsets. The downloader skips files already present; subset scripts refuse to clobber an existing --output unless --overwrite is passed. Downloads land under the cycle_###/ 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.py picks the count per cycle via a threshold: cycle >= 474 is treated as cal/val (CALVAL_CYCLE_MIN in swot.constants). Override with --calval-cycle-min or force a count with --expected-passes. Getting this wrong used to silently drop passes (out-of-range placement raised IndexError, 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_case spellings, e.g. --cycle_start, still work as aliases).
  • Corrections. SSHA regridding/subsetting add height_cor_xover (crossover) and internal_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 in common/, not copied into both scripts.
  • Throwaway demos live under examples/: examples/plot_amazon_soliton.py, examples/plot_arctic_view.py, and examples/plot_GoA_example.py have hardcoded regions, absolute local paths (e.g. sys.path.append('/home/jinbo.wang/projects/oceanic/')), and expect a legacy oceanic.swot_ssh module. Treat them as scratch examples, not entry points.
  • find_swot_passes_science.py keeps 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).
  • ../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.py structure, data roots on /spray, Earthdata/.netrc auth, idempotency, and the SWOT cycle-numbering note summarized above.