Skip to content

feat(weather): tier-2 VIIRS products — AOD, CloudHeight, LST, SnowCover#104

Merged
helloiamvu merged 3 commits into
mainfrom
phase28/viirs-tier2
Jul 6, 2026
Merged

feat(weather): tier-2 VIIRS products — AOD, CloudHeight, LST, SnowCover#104
helloiamvu merged 3 commits into
mainfrom
phase28/viirs-tier2

Conversation

@helloiamvu

Copy link
Copy Markdown
Member

Why

"Backfill fully" tier-2: the VIIRS buckets carry far more than CloudMask. This registers the four highest-value families for temperature markets — including polar-orbit land-surface temperature, the only satellite LST available for EU/Asia stations (GOES can't see them).

What (3 commits, all live-verified on real n20 granules)

  • VIIRS-JRR-AOD/AOD550 (2022+) — smoke/haze suppresses Tmax
  • VIIRS-JRR-CloudHeight/CldTopHght+Temp+Pres (2023+, ~67 MB granules under the 1 GiB cap)
  • JPSSRR_LST/LST — scaled int16→Kelvin, validated end-to-end (285–323 K over the tropical Pacific, physically exact); archive ~Dec 2024+ (young, forward value)
  • JPSSRR_SnowCover/SnowFraction+SnowMaskI-band (1536,6400), double every other VIIRS grid; snow albedo/radiative-cooling features
  • Floors restructured: yesterday's per-satellite floors were CloudMask dates masquerading as platform floors — they'd have clipped AOD's 2022+ archive. Platform floors = earliest-any-product; _PRODUCT_AVAILABLE_SINCE carries specifics; effective = max (mirrors fix(weather): constellation product parity + probe-measured ingest speed #103's GOES pattern). No-extra fallback synced + AST drift guard extended.

Review loop (codex ×4)

  • P2: JPSSRR_* underscores collide with the progress-key field separator → resume would ProgressCorrupt its own file → keys sanitize _- in key-space (opaque identities, loss-free)
  • P2: VIIRS extractor never enforced raw-unit valid_range → invalid LST raw 30000 scaled to a plausible 350 K as clean_apply_scale_offset now mirrors _goes_extract._apply_valid_range exactly, incl. the physical-units exemption
  • Final pass: clean

Deferred with rationale: Himawari Winds (channel-partitioned multi-file layout → own transport PR), ISatSS (display imagery), DSRM (roaming sector).

After merge

Image rebuild (main-<merge>) → VIIRS top-up dispatches for KNYC + RJTT + EGLC (polar = all three gain LST/AOD/cloud-props/snow).

TS Parity

python_only: true — satellite ingest internals; no public API surface.

🤖 Generated with Claude Code

helloiamvu and others added 3 commits July 6, 2026 01:52
…, SnowCover

Live-verified against real n20 granules (2026-07-06; per-satellite year
availability probed across all three NODD buckets):

- VIIRS-JRR-AOD / AOD550 — aerosol optical depth, (768,3200) M-band,
  fill -999.999, archive snpp/n20 2022+ n21 2023+. Smoke/haze days
  measurably suppress Tmax.
- VIIRS-JRR-CloudHeight / CldTopHght+CldTopTemp+CldTopPres — float32
  (768,3200), fill -999, ~67 MB granules (well under the 1 GiB cap),
  archive 2023+. Deep-convection vs stratus discrimination.
- JPSSRR_LST / LST — land surface temperature swath, SCALED int16
  (raw*0.005+200 -> Kelvin), validated end-to-end on a real granule
  (285-323 K over the tropical Pacific — physically exact). Polar-orbit
  LST covers the EU/Asia stations GOES cannot see. Archive ~Dec 2024+
  (young; forward value). Filename prefix is LST_v2r2 (not JRR-*) —
  cosmetic, the transport lists day prefixes.
- JPSSRR_SnowCover / SnowFraction+SnowMask — I-BAND grid (1536,6400),
  DOUBLE the M-band resolution of every other VIIRS product (per-entry
  registry shapes handle it). Archive ~Dec 2024+. Snow albedo suppresses
  Tmax; snowpack enables radiative-cooling cold NLOW nights.

Floors restructured: the 2026-07-05 per-satellite floors were CloudMask
product dates masquerading as platform floors — correct while CloudMask
was the only product, but they would have silently clipped AOD's 2022+
archive on snpp/n20. Platform floors are now earliest-any-product;
_PRODUCT_AVAILABLE_SINCE carries per-(satellite, product) specifics
(CloudMask keeps its exact verified dates); the clamp uses
_available_since_for() = max(platform, product), mirroring _goes_s3
(PR #103).

Also syncs the no-extra VIIRS fallback product set + extends the AST
drift guard to it (same class as the Codex P2 on the DSRC registration).

Transport untouched: the flat {product}/{YYYY}/{MM}/{DD}/ listing is
product-agnostic and all new granules fit the existing 1 GiB cap.

SKIPPED with rationale: ISatSS (display imagery), DSRM (roaming
mesoscale sector), SDR/RDR instrument products, L1b radiances. Himawari
AHI-L2-FLDK-Winds deferred to its own PR: channel-partitioned multi-file
scans under an extra path level ({YYYY}/{MM}/{DD}/{HHMM}/) need
transport + a vector-extraction mode, not a registry entry.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Codex P2 on the tier-2 VIIRS registrations: JPSSRR_LST / JPSSRR_SnowCover
contain "_" — the progress key's FIELD SEPARATOR — so a resume run would
write viirs-n20_JPSSRR_LST_... and then raise ProgressCorrupt loading its
OWN file (the validator's product segment is deliberately
underscore-free). _progress_key() now maps "_" -> "-" in key-space only:
keys are opaque full-string identities (never parsed back), build and
skip-check share the function, and no registered products differ only by
underscore-vs-hyphen, so the transform is loss-free. Cache paths and
scope digests keep the raw product names.

Tests: sanitized key validates + round-trips _validate_progress; key
identity stable for skip-checks and distinct across sibling products.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Codex P2 (round 3, tier-2 branch): the VIIRS extractor applied only
_FillValue + scale/offset — never the CF-1.8 raw-unit valid_range the
GOES path enforces. With JPSSRR_LST (the first SCALED VIIRS product), an
invalid raw like 30000 (> valid_range max 28600) scaled into a
plausible-looking 350 K inside the broad registry bounds and entered
rows as CLEAN data.

_apply_scale_offset gains valid_range_filter (default False), masking
raw values outside the attr range to NaN BEFORE scaling — mirroring
_goes_extract._apply_valid_range semantics exactly, including the
physical-units exemption (AOD550's valid_range is already physical, so
its entry keeps the filter off, like GOES Cloud_Probabilities). The
extract loop wires pv.valid_range_filter through; LST's entry uses the
dataclass default True.

Tests: out-of-range raw masks to NaN while in-range scales (float32
scale-precision tolerance), disabled filter passes physical-range
products untouched, LST registry entry pins the filter on.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@helloiamvu helloiamvu requested a review from Tarabcak July 6, 2026 00:28
@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown

📝 Docs-required check: REMINDER

API-surface change without docs and no opt-out — surfacing reminder.

API-surface files changed:

packages/weather/src/mostlyright/weather/_fetchers/_viirs_extract.py
packages/weather/src/mostlyright/weather/_fetchers/_viirs_s3.py
packages/weather/src/mostlyright/weather/satellite/_backfill.py
packages/weather/src/mostlyright/weather/satellite/_sources.py

Docs files changed:

(none)

Docs surfaces to consider

  • CHANGELOG.md — every behavior change goes here. Auto-synced to the docs site on every release.
  • docs/ — hand-authored prose. Lifted into the landing repo as MDX on every release.
  • Per-package READMEs (packages/*/README.md, packages-ts/*/README.md) — front-door copy on PyPI / npm.
  • Docstrings (Python """ ... """ / TypeScript JSDoc /** ... */) — propagate to the auto-generated API reference via Sphinx / TypeDoc.

How to silence this reminder

  • Add a docs change to this PR (any *.md / *.mdx under docs/, or CHANGELOG.md, or any README.md).
  • Apply the docs-not-required label.
  • Add a line to the PR body: docs-not-required: <one-sentence reason>.

This check is advisory — it never blocks the merge.

@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown

Parity ticket gate: PASSED

parity-ticket-check: Python-side trigger surface touched; opt-out satisfied (parity ticket, python_only flag, or label).

See CROSS-SDK-SYNC.md §2 for the workflow.

@helloiamvu helloiamvu merged commit a79584a into main Jul 6, 2026
19 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant