Monorepo for the independent stages of the altimetry processing pipeline used to generate NASA SSH products.
Pipeline consists of:
Daily files -> Crossover -> OER -> Crossover -> Bad pass flagging -> Finalization
Simple grids -> Indicators -> Imagery for website
GSFC Data S6 Data
| |
+------------+
|
v
[ Generate Daily Files ]
|
v
[ Generate Simple Grids ]
|
v
+--------------+--------------+
| |
v v
[ Generate ENSO Maps & Imagery] [ Generate Indicators ]
The directories contained within pipeline/ contain everything for a given stage in the pipeline (note: stage as opposed to step, as the crossover stage is used for two steps). Each stage's directory can be thought of as an independent repository for that stage.
Each stage has been designed to be run on AWS Lambda, and as such there are limitations to how much can be executed locally.
The pipeline is orchestrated by AWS Step Functions using JSONata query language. State machine definitions live in state_machines/ as ASL (Amazon States Language) JSON files. The rendered/ subdirectory contains the same definitions with AWS account/region placeholders resolved.
A top-level orchestrator (pipeline.asl.json) coordinates three sub-pipelines, each of which delegates per-date work to Distributed Map states that fan out to Lambda functions:
pipeline.asl.json
├── along_track_pipeline.asl.json
│ ├── pipeline_init (Lambda — determines which dates need processing)
│ ├── daily_file.asl.json (Distributed Map → daily_files Lambda)
│ ├── xover.asl.json (Distributed Map → xover Lambda, df_version=p1)
│ ├── oer.asl.json (Distributed Map → oer Lambda)
│ ├── xover.asl.json (Distributed Map → xover Lambda, df_version=p2)
│ ├── bad_pass.asl.json (Distributed Map → bad_pass Lambda)
│ └── finalizer.asl.json (Distributed Map → finalizer Lambda)
├── unifier.asl.json (conditional — runs if source has unify=true)
│ ├── Distributed Map → unifier Lambda
│ └── rewrite_manifest (Lambda — rewrites jobs manifest with NASA-SSH source)
└── simple_grid_pipeline.asl.json
├── set_sg_jobs (Lambda — filters manifest to Monday dates)
├── simple_grid.asl.json (Distributed Map → simple_grids Lambda)
├── enso.asl.json (Distributed Map → enso Lambda)
└── indicators (Lambda)
- Jobs manifest:
pipeline_initwrites ajobs.jsonto S3 listing dates that need processing. All downstream Distributed Maps read from this manifest viaItemReader. - Distributed Map: Most processing stages use Distributed Map (mode
DISTRIBUTED, max concurrency 100) to process each date independently. Results are written to S3 viaResultWriter. - Input threading: Orchestrator states use
Output: "{% $states.input %}"to pass the original input (jobs_key,bucket,source) through to the next state, since child state machine outputs aren't needed upstream. - Two crossover passes: The xover state machine is invoked twice — once with
df_version=p1(before OER) and once withdf_version=p2(after OER). - Conditional unification: Sources with
unify=true(GSFC, S6) get their finalized daily files copied to a unifiedNASA-SSHprefix by the unifier. Therewrite_manifestLambda then produces a new jobs manifest under the NASA-SSH source for downstream simple grid processing.
For a detailed reference of every S3 object written by each stage (success and failure), key patterns, and operator troubleshooting tips, see S3_DATA_FLOW.md.
Each source has one profile file, utilities/sources/{source}.yaml, holding a
shared common: block plus one section per pipeline stage. A stage's config
loader (load_source_config) merges common with that stage's section and
constructs the stage's config dataclass, so each stage sees only the fields it
declares.
Source-identity metadata used by multiple stages:
product_type—referenceorhigh_latitude; determines the daily-file filename family (alt_ref_at_*vsalt_hilat_at_*) and which downstream stages applydiscovery_type— how pipeline_init finds upstream granules (cmr,thredds, …)unify— whether the source participates in NASA-SSH unificationstart_date— first date with available dataend_date(optional) — last date with available data; omit for ongoing collectionscollections— the upstream collection descriptors discovery reads
Each stage that a source participates in gets its own section (pipeline_init:,
daily_files:, finalizer:, xover:, …) carrying only that stage's settings.
The xover: section selects the crossover type (see CONTEXT.md):
crossover_type: self— areferenceproduct-type source (S6, S6B, GSFC) crossed against its own passes over a forward window. Fields:cycle_length,window_size,window_padding,max_pass_number.crossover_type: reference— ahigh_latitudesource (S3B) crossed against the finalized NASA-SSH P3 reference mission over a window centered on the processing day. Addsreference_sourceandreference_version(see ADR-0006).
Example (utilities/sources/S3B.yaml, abbreviated):
common:
product_type: high_latitude
discovery_type: thredds
start_date: "2018-04-25"
xover:
crossover_type: reference
reference_source: NASA-SSH
reference_version: p3
cycle_length: 9.9156
window_size: 12 # interpreted as a ±centered window for reference crossovers
window_padding: 2
max_pass_number: 9999When adding a new source, create utilities/sources/{source}.yaml with a
common block and a section for each stage it participates in.
Dependencies are declared in the root pyproject.toml (the single source of
truth) and locked in uv.lock. Each stage is a named optional extra; the dev
extra installs the union of them all plus the test/lint tooling. Use
uv to create the dev environment:
uv sync --extra dev
This installs the shared utilities package (required to execute containers and
unit tests) along with every stage's dependencies.
Tests run one process per stage via scripts/test.sh — each stage is rooted at
its own directory so its code resolves as a top-level import, mirroring the
Lambda container. See the script header for the rationale.
./scripts/test.sh # all stages
./scripts/test.sh oer xover # only the named stages
./scripts/test.sh oer -- -k foo -x # pass args after `--` through to pytest
uv run ruff check . # config lives in [tool.ruff] in pyproject.toml
Images must be built from the root directory context. Each stage Dockerfile
exports its extra's locked dependencies with uv and installs the shared
utilities package, so no per-stage requirements.txt is needed.
ex:
docker buildx build --platform linux/amd64 --load -t daily_files:latest -f path/to/nasa-ssh-pipeline/pipeline/daily_files/Dockerfile {path/to/nasa-ssh-pipeline/}