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.