Skip to content

fix(weather): unblock full-disk goes16 DSRF + fast single-station year-shard backfill#99

Merged
helloiamvu merged 4 commits into
mainfrom
phase28/full-single-station-backfill
Jul 5, 2026
Merged

fix(weather): unblock full-disk goes16 DSRF + fast single-station year-shard backfill#99
helloiamvu merged 4 commits into
mainfrom
phase28/full-single-station-backfill

Conversation

@helloiamvu

@helloiamvu helloiamvu commented Jul 5, 2026

Copy link
Copy Markdown
Member

Why

Two blockers stood between us and a full single-station GOES backfill to measure cost + speed:

  1. DSRF backfill wall. GOES-16 ABI-L2-DSRF switched from the coarse 326×326 lat/lon grid to the full-disk 5424×5424 ABI fixed grid around 2024 day-108 (~Apr 17). The shape gate hard-coded goes16 DSRF to the coarse shape by satellite, so every post-cutover file was rejected shape (5424, 5424) does not match expected [(326, 326)] — rejected before pixel read. Observed live: a KNYC 2024 pilot uploaded DSRF Jan–Mar then failed every file from day-108 on, stalling the whole DSRF ≥ mid-2024 backfill.
  2. No fast single-station path. pilot mode runs a full station as one monolithic Batch task that blows the 6h maxRunDuration (a KNYC 2017–2026 pilot timed out with only 2 years done); full mode is the cost-gated 65-station × N-year ~28 TB fleet. Neither gives a bounded, parallel single-station run.

What

fix(weather) — accept full-disk goes16 DSRF. _validate_dataset_shape now keys the accepted DSRF shape for goes16 on the file's own projection variable (goes_lat_lon_projection → 326×326; else fixed grid → 5424×5424), mirroring _extract_from_dataset's existing per-file branch (which already decodes full-disk files correctly — only the gate was wrong). goes19 stays full-disk-only, and a mislabeled/swapped grid still fails loudly.

feat(weather) — fast full-single-station backfill. Explicit --stations mode now honors the same station×year shard_roster decomposition roster mode uses: when an array job supplies a shard count > 1 (--shard-count or Batch BATCH_TASK_COUNT), each task narrows to exactly one (station, year). Shard count == 1 is a no-op (classic pilot / local run stays byte-identical). New station workflow mode: task_count = stations × years, n2-standard-8 SPOT, parallelism ≤ 48, --executor process (DSRF full-disk), R2 + durable progress + CLOUD_LOGGING. KNYC 2017–2026 → 10 parallel year-shards, each a bounded < 6h unit.

fix(ci) — guard the station-mode year window (reversed/empty/non-numeric → loud ::error:: before the task_count math).

How to run the measurement (operator)

Actions → Run weather backfill fleet → mode=station, pilot_station=KNYC, products=ABI-L2-ACMC,ABI-L2-DSRF, year_start=2017. Cost/speed come from the 10 job durations + R2 bytes. Not cost-gated (bounded single station).

Tests

  • RED→GREEN DSRF: full-disk goes16 file passes the gate; wrong-shape-on-fixed-grid and full-disk-shape-on-latlon still rejected; goes19 swapped-grid rejection preserved. test_satellite_s3 46/46.
  • Explicit-mode sharding: single + multi-station decomposition, shard-count-1 no-op, BATCH_TASK env-driven. test_cli_roster + test_roster 38/38.
  • Workflow: pilot (1 task) / full (650) modes verified unchanged; station mode emits valid Batch JSON (10 tasks) and rejects bad year windows.

Review

  • Two-reviewer loop: an adversarial 4-dimension review (DSRF correctness, CLI sharding, workflow/infra safety, settlement-parity firewall) with per-finding verification — 0 P1/P2, 1 P3 (year-window guard) caught + fixed in 57dc215. Codex high independent pass alongside.
  • Parity firewall untouched — no change to research(), merge/*, live/_sources.py, or CWOP.

TS Parity

python_only: true — internal satellite backfill infra: a _fetchers shape-guard fix + the ingest/ops backfill CLI + a GH workflow. No mostlyright.* public API surface change, and the TS SDK is client-side (query/catalog) with no satellite ingest/backfill, so there is no TS equivalent to keep in parity. (_goes_s3.py matched the _fetchers/** parity-trigger glob, but the change is an internal grid-shape gate, not public surface.)

🤖 Generated with Claude Code

helloiamvu and others added 3 commits July 5, 2026 09:07
goes16 ABI-L2-DSRF switched from the coarse 326x326 lat/lon grid to the
full-disk 5424x5424 ABI fixed grid around 2024 day-108 (~Apr 17). The
_validate_dataset_shape gate hard-coded goes16 DSRF to the coarse shape
by SATELLITE, so every post-cutover full-disk file was rejected "before
pixel read" — stalling the entire DSRF ≥ mid-2024 backfill (observed
live: a KNYC 2024 pilot uploaded DSRF Jan–Mar then failed every file
from day-108 on).

The extractor (_extract_from_dataset) already branches per-file on the
projection variable (goes_imager_projection vs goes_lat_lon_projection)
and is satellite-agnostic — it would decode the full-disk file correctly.
Only the shape gate was wrong. Fix: for goes16 DSRF, key the accepted
shape on the file's OWN projection variable (coarse lat/lon → 326x326;
fixed grid → 5424x5424), mirroring the extractor. goes19 stays strict
(full-disk only) so a swapped/mislabeled grid still fails loudly.

RED→GREEN: adds a regression that a full-disk goes16 DSRF file passes the
gate, plus wrong-shape-on-fixed-grid and full-disk-shape-on-latlon
rejection tests. All 46 test_satellite_s3 tests green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A full single-station backfill (one station × all years) had no fast
path: `pilot` mode runs it as ONE monolithic Cloud Batch task that blows
the 6h maxRunDuration (observed: a KNYC 2017–2026 pilot timed out with
only 2 years done), and `full` mode is the 65-station × N-year ~28 TB
fleet. Neither gives a bounded, parallel single-station run for a
cost/speed measurement.

- CLI: extend explicit `--stations` mode to honor the same station×year
  shard decomposition roster mode already uses. When an array job supplies
  a shard count > 1 (`--shard-count` or Cloud Batch `BATCH_TASK_COUNT`),
  each task narrows to exactly one (station, year) via `shard_roster`.
  Shard count == 1 is a no-op, so the classic single-task pilot / local
  run stays byte-identical. Reuses the existing per-shard progress key so
  year-shards never clobber each other's resume markers.
- Workflow: add a `station` mode to run-weather-backfill.yml —
  task_count = stations × years, n2-standard-8 SPOT, parallelism capped
  at 48, `--executor process` (DSRF full-disk), `--max-workers 8`, R2 +
  durable progress + CLOUD_LOGGING. One station × 10 years submits as 10
  parallel year-shards, each a bounded < 6h unit. Not the 28 TB fleet, so
  NOT cost-gated. pilot/full modes unchanged (verified: task_count 1 / 650).

Tests: explicit-mode station×year decomposition (single + multi station),
shard-count-1 no-op, and BATCH_TASK env-driven sharding.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adversarial review caught (and reproduced in bash under set -euo
pipefail) that a reversed year window (year_end < year_start), empty, or
non-numeric year input in the new `station` backfill mode makes
NUM_YEARS <= 0 -> negative TASK_COUNT/PARALLELISM that jq --argjson would
bake into an invalid Batch job submitted to the API. Add an explicit
numeric + ordering guard that fails loud with an operator-facing
::error:: before the arithmetic. pilot/full modes unaffected.

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

github-actions Bot commented Jul 5, 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/_goes_s3.py
packages/weather/src/mostlyright/weather/satellite/__main__.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 5, 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.

Codex closing-pass P2: the previous guard only rejected non-digit
input, so a short-year typo like year_start=17 (blank year_end) passed
and made NUM_YEARS = current_year - 17 + 1 ≈ 2010 — submitting ~2,010
n2-standard-8 Spot tasks for one station instead of failing early.
Match exactly [0-9][0-9][0-9][0-9] before the task_count arithmetic;
verified: 17 / 20267 / 99 / abcd / reversed all rejected, 2017..2026
still yields task_count=10.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@helloiamvu helloiamvu merged commit 930636d into main Jul 5, 2026
11 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