Overview
pace searches, downloads, indexes, subsets, and plots PACE OCI Level-2
ocean-colour granules from NASA Earthdata. PACE (Plankton, Aerosol, Cloud, ocean
Ecosystem) carries the OCI hyperspectral imager; the L2 products covered here are
apparent optical properties (AOP — Rrs, avw, nflh, aot_865, angstrom),
biogeochemistry (BGC — chlor_a, poc, pic, carbon_phyto, …), inherent
optical properties (IOP — a, bb, Kd, aph, adg_442, bbp_442, …), and
photosynthetically available radiation (PAR). The L4 mapped MOANA
picophytoplankton community-structure product is also reachable.
PACE science data begins 2024-03-05, and all products are served under the
OB_CLOUD provider (the former OB_DAAC provider returns no results).
Two pieces:
pace_downloader.py— a one-pass CMR query + downloader. It resolves the collection concept ID (cached on disk), pages through CMR granules for a date range and optional bounding box (harvesting each granule's exact size, checksum, and bounding box), writes a link manifest CSV, then downloads the missing.ncfiles through the shared sync engine (common/sync.py):--jobs Nparallel workers, staged atomic writes (no truncated granules on interrupt), post-transfer size/checksum verification, per-file retry with backoff,--dry-run, and per-day directory layout. After each run it updates the granule footprint index from the CMR bounding boxes for every locally-present file — no granule is opened.utils.py— file-index, granule-reader,BBox, and extraction helpers for working with the downloaded L2 files, plusget_pace(), a one-call search→download→subset→scatter-plot convenience function.
Products (CMR short names, provider OB_CLOUD, _NRT variants for near-real-time):
| Short name | Description |
|---|---|
PACE_OCI_L2_AOP[_NRT] |
Apparent optical properties — Rrs spectrum, avw, nflh, aot_865, angstrom |
PACE_OCI_L2_BGC[_NRT] |
Biogeochemical — chlor_a, poc, pic, carbon_phyto (+ _unc) |
PACE_OCI_L2_IOP[_NRT] |
Inherent optical properties — a, bb, Kd, aph, adg_442, bbp_442 (+ _unc) |
PACE_OCI_L2_PAR[_NRT] |
Photosynthetically available radiation — par_day_*, ipar_* (split by geometry; no plain par/ipar in v3.2) |
PACE_OCI_L4M_MOANA[_NRT] |
MOANA picophytoplankton community structure (L4 mapped) |
Installation & dependencies
Core deps (always installed with odsl): pandas, numpy, xarray,
netcdf4 / h5netcdf, requests, tqdm, earthaccess. The pace extra
adds tenacity (used by the @retry HTTP / download wrappers). Plotting in
get_pace() additionally needs matplotlib and, optionally, cartopy (the
plot extra) for map projections.
python -m pip install -e ".[pace]" # downloader + utils
python -m pip install -e ".[pace,plot]" # + cartopy map plotting
# or the full conda environment:
conda env create -f pace/pace_env.yml && conda activate pace_env
NASA Earthdata credentials are required. Store them in ~/.netrc:
machine urs.earthdata.nasa.gov login <username> password <password>
get_token() reads this file to mint an EDL bearer token for CMR, and
earthaccess.login() uses it for downloads.
Scripts & entry points
| Script / symbol | Console entry point | Purpose |
|---|---|---|
pace/pace_downloader.py |
odsl-pace-download (pace.pace_downloader:main) |
Query CMR + download granules (harvests the granule bounding box from CMR into the manifest) |
pace/granule_footprints.py |
odsl-pace-footprints (pace.granule_footprints:main) |
Build a per-granule bounding-box index (from the swath perimeter) for fast spatial pruning; query with paths_overlapping |
pace/utils.py |
— (library) | BBox, load_pace_l2, build_pace_index, extract_*, get_pace |
pace/collection_ids.json |
— | On-disk cache of CMR collection concept IDs |
Usage
Download granules
# BGC science over the Gulf of Mexico for one week, 8 parallel workers,
# size-verifying existing files against the CMR manifest (local stat only)
python pace/pace_downloader.py \
--product PACE_OCI_L2_BGC \
--start 2024-03-05 --end 2024-03-11 \
--bbox -98 18 -80 30 \
--jobs 8 --verify size
# Full-swath AOP backfill (largest L2 product — check disk first)
odsl-pace-download --product PACE_OCI_L2_AOP --start 2024-03-05 --jobs 8
# Plan without transferring (also refreshes footprints for local files)
python pace/pace_downloader.py --product PACE_OCI_L2_BGC --start 2024-03-05 --dry-run
Flags: --product SHORT_NAME [SHORT_NAME ...] (one or more, default
PACE_OCI_L2_BGC_NRT), --all-nrt (all NRT products, overrides --product),
--days-back N (rolling window ending today, overrides --start/--end),
--start / --end (YYYY-MM-DD, inclusive), --bbox WEST SOUTH EAST NORTH
(decimal degrees, default none = global), --data-root (default /spray/pace),
--database-dir (default <repo>/database), --provider (default OB_CLOUD).
Shared sync flags (from common.cli.common_sync_parser): --jobs N (default 4),
--verify {none,size,checksum} (size is a local stat-only check against
the CMR-manifest size — a full-archive integrity sweep takes minutes;
--verify-downloads is a legacy alias for --verify checksum), --dry-run,
--overwrite, --retries N, --timeout S, --limit N (caps files per
product), --log-file PATH (rotating; default
/spray/pace/logs/pace_downloader.log, "" disables), --no-lock,
-v/--verbose, -q/--quiet. Footprint maintenance: --no-footprint-update,
--footprints-out PATH (default
/spray/pace/footprints/pace_granule_footprints.parquet).
Scheduled / cron runs
The downloader is built to run unattended on a schedule:
- Rolling window —
--days-back Nfetches the last N days ending today (UTC), so a fixed crontab line always pulls fresh data. - Product sets —
--all-nrt(4 NRT L2 collections), or--all(everything: NRT + science L2 + MOANA L4 = 10 collections). NRT lands within hours; science lags days-to-weeks, so--allre-checks the same window and skips what is already present. - Idempotent — files already present are skipped (add
--verify sizeto re-fetch truncated/stale files by CMR size — a local stat, no network — or--verify checksumfor a full re-hash). - Footprint index stays current — every run upserts
/spray/pace/footprints/pace_granule_footprints.parquetfrom the CMR bounding boxes of locally-present granules (used bycolocateandodsl-super-subsetterfor spatial search). - File logging — timestamped, size-rotating log via
--log-file(default/spray/pace/logs/pace_downloader.log, 5 × 5 MB). Console goes to stderr; use--quietto silence it under cron. The tqdm bar auto-disables off a TTY. Missing/unwritable log dir degrades to console-only, never crashes. - Single instance — an exclusive
flockon/spray/pace/logs/pace_downloader.lockmakes an overlapping run exit cleanly (0) instead of double-downloading. - Exit code —
0on success (or lock contention),1if any download failed, so cronMAILTO/ monitoring can alert on real failures.
# crontab -e — daily at 02:00, last 3 days of *all* products (global).
# Defaults: data -> /spray/pace, log -> /spray/pace/logs/pace_downloader.log
0 2 * * * /opt/miniforge3/bin/python \
/home/jinbo.wang/code/odsl_code/pace/pace_downloader.py \
--all --days-back 3 --jobs 4 --quiet >> /spray/pace/logs/cron.out 2>&1
Volume note: global
--allis large — full-swath AOP granules are ~240 MB each and PACE images the globe every day. Add--bbox WEST SOUTH EAST NORTHto restrict to a region if disk is tight.
Read / subset in Python
from pathlib import Path
from pace.utils import BBox, load_pace_l2, extract_bgc, extract_rrs
bbox = BBox(west=-98, south=18, east=-80, north=30)
g = load_pace_l2(
Path("/spray/pace/PACE_OCI_L2_BGC/2024/03/05/PACE_OCI.20240305T180543.L2.OC_BGC.V3_2.nc"),
bbox=bbox,
)
bgc = extract_bgc(g) # xarray.Dataset: chlor_a, poc, pic, …
rrs, wavelengths = extract_rrs(g) # (n_pixels, n_bands), (n_bands,)
# Build a local file index and filter by time
from pace.utils import build_pace_index, filter_index_by_time
import pandas as pd
idx = build_pace_index(Path("/spray/pace"), short_names=["PACE_OCI_L2_BGC"])
idx = filter_index_by_time(idx, pd.Timestamp("2024-03-05"), pd.Timestamp("2024-03-11"))
# One-call search + download + subset + scatter plot (nearest granule in time)
from pace.utils import get_pace
get_pace("2024-03-05T18:00:00", bbox, "chlor_a.png", "chlor_a", search_window="1D")
get_pace("2024-03-05", bbox, "rrs443.png", "Rrs", wavelength=443)
Granule footprint index (fast spatial pruning)
PACE flies a drifting orbit, so every granule has a unique footprint. Build a
per-granule bounding-box index once (read cheaply from the swath perimeter), then
prune granules to a region without opening them — used automatically by the
colocate PACE provider.
odsl-pace-footprints --short-names PACE_OCI_L2_BGC # -> /spray/pace/footprints/…parquet
from pace.granule_footprints import load_footprint_index, paths_overlapping
fp = load_footprint_index()
hits = paths_overlapping(fp, (-80, 25, -60, 45)) # granule paths crossing the box
Data layout / outputs
Downloader default data root is /spray/pace (override with --data-root);
rotating logs and the single-instance lock live under /spray/pace/logs/.
Granules are written per product / date:
{data-root}/{short_name}/YYYY/MM/DD/PACE_OCI.YYYYMMDDTHHMMSS.L2.<PRODUCT>.<VERSION>[.NRT].nc
Per-run link manifest CSV (columns: short_name, granule_ur, date, orbit,
filename, url) goes to --database-dir (default <repo>/database/):
database/pace_{short_name}_{start}_{end}_links.csv
build_pace_index() walks that same tree and returns a DataFrame with columns
path, short_name, datetime, product, version (short name is inferred from the
.../{short_name}/YYYY/MM/DD/ parent path).
Conventions & gotchas
- Group layout. L2 files are HDF5/netCDF4 with
navigation_data(lon/lat),geophysical_data(variables),sensor_band_parameters(wavelengths), andscan_line_attributes(per-scanyear/day/msec, reconstructed into UTC scan times).load_pace_l2opens each group separately. - Scan-time decoding.
day/mseccarry timedelta-likeunits, so_scan_timesopensscan_line_attributeswithdecode_timedelta=False— otherwise xarray decodes them totimedelta64, the integer reconstruction fails, and every scan time comes backNaT(silently breaking any temporal matching). short_nameis four levels above the file (.../{short_name}/YYYY/MM/DD/ file.nc), i.e.path.parents[3]— not three. Getting this wrong makesbuild_pace_index(short_names=[…])match nothing.- Bounding boxes are
west south east northin degrees.BBoxnormalizes to[0,360)internally (west360/east360/span360) so it handles dateline-wrapping boxes;BBox.contains(lon, lat)returns the per-pixel mask used bysubset_bbox. Index-level bbox filtering (filter_index_by_bbox) is only a loose pass — precise filtering happens per pixel at load time. - Subsetting flattens the 2-D swath. After
subset_bbox, arrays are 1-D along apixeldim (banded vars keep abanddim); concatenate several withconcat_granules. - Collection IDs are cached.
collection_ids.jsonis populated on first miss via a CMR lookup and reused thereafter; delete the entry to force a refresh. - Retries on CMR/HEAD/download calls come from
tenacity(stop_after_attempt(3), exponential backoff) — thepaceextra.
Related
See the repo-level ../README.md for cross-package conventions
(Earthdata/~/.netrc, collection_ids.json caching, bounding-box normalization,
idempotent downloads, data-root constants).