prop/docs/research/hrrr_native_levels.md
Graham McIntire 900685aa06 Phase 1 tasks 1.1-1.5: HRRR native hybrid-sigma ingestion
- Spike docs at docs/research/hrrr_native_levels.md confirming files
  are on AWS S3 for 5+ years, 50 hybrid levels, and include TKE and
  SPFH needed for Phase 2 turbulence features. Architectural finding:
  per-point on-demand fetching is impractical (~530 MB/file), so
  the ingestion worker batches per (date, hour) instead.
- hrrr_native_profiles schema: arrays per level plus cached surface
  scalars and placeholder columns for Phase 2/4 derived fields.
  Strictly additive — the existing hrrr_profiles table is untouched.
- HrrrNativeClient: pure URL/message-list helpers, build_native_profile/1
  that turns a parsed wgrib2 map into the schema shape (TDD'd).
- Exposed HrrrClient.download_grib_ranges/2 so the native client
  reuses the existing parallel byte-range download + disk cache.
- HrrrNativeGridWorker: Oban worker keyed on {year, month, day, hour},
  unique at :infinity, pulls distinct (lat, lon) points from contacts
  in the ±30 min window, downloads the native grib2, extracts per
  point, bulk-upserts.
- mix hrrr_native_backfill --limit N enqueues the top-N hours by
  contact count.

Phase 1 gate still pending Task 1.6 (sanity-check backtest after
live data lands).
2026-04-09 16:23:51 -05:00

7.2 KiB
Raw Blame History

HRRR Hybrid-Sigma ("Native") Levels — Spike Findings

Date: 2026-04-09 Purpose: Task 1.1 of docs/plans/2026-04-09-propagation-modeling-improvements.md Question: Can we practically ingest HRRR native hybrid-sigma profiles to replace the 25 hPa pressure-level product for boundary-layer propagation features?

TL;DR

Yes — with one architecture change. The files are available on AWS S3 going back at least 5 years and expose all the variables needed for Richardson number, θₑ, shear, and TKE computations on 50 hybrid-sigma levels. But the per-file download is too large to run on-demand per QSO point. Phase 1 should ingest native profiles in batch mode per HRRR run hour, not per QSO.

File location

Same S3 bucket as the existing surface/pressure products:

https://noaa-hrrr-bdp-pds.s3.amazonaws.com/hrrr.<YYYYMMDD>/conus/hrrr.t<HH>z.wrfnatf00.grib2
https://noaa-hrrr-bdp-pds.s3.amazonaws.com/hrrr.<YYYYMMDD>/conus/hrrr.t<HH>z.wrfnatf00.grib2.idx

wrfnatf00 = native levels, 0-hour forecast. Forecast hours wrfnatf01 through wrfnatf18 also exist.

Historical availability

Spot-checked on 2026-04-09 by HEAD-ing the .idx URL:

Date HTTP status
2026-03-09 (30d) 200
2026-02-09 (60d) 200
2026-01-09 (~90d) 200
2025-12-09 (~120d) 200
2024-04-09 (~1y) 200
2023-04-09 (~2y) 200
2022-04-09 (~3y) 200
2021-04-09 (~5y) 200

No rotation problem. We can backtest against years of historical native data if we want to, which unblocks Phase 6 (climatology) as well.

What the file contains

The .idx file for one HRRR run has 1133 GRIB2 messages. 1000 of them are on hybrid levels 1-50, plus 133 surface/2m/10m/composite entries.

Each of the 50 hybrid levels carries these 20 variables:

PRES      pressure (Pa)
HGT       geopotential height (m)
TMP       temperature (K)
SPFH      specific humidity (kg/kg)   ← derive Td, θ, θₑ from SPFH + TMP + PRES
UGRD      u wind (m/s)
VGRD      v wind (m/s)
VVEL      vertical velocity (Pa/s)
TKE       turbulent kinetic energy (m²/s²)   ← **exactly the turbulence signal Phase 2 needs**
CLMR      cloud water mixing ratio
CIMIXR    cloud ice mixing ratio
RWMR      rain water mixing ratio
SNMR      snow mixing ratio
GRLE      graupel mixing ratio
NCONCD    number concentration of cloud droplets
NCCICE    number concentration of cloud ice
SPNCR     number concentration of rain
PMTF      ???
PMTC      ???
FRACCC    convective cloud fraction
MASSDEN   particulate mass density

The seven we care about for Phase 24 are TMP, SPFH, HGT, UGRD, VGRD, TKE, PRES × 50 levels = 350 messages out of the 1133.

Note: there is no DPT on hybrid levels. The pressure-level product has dewpoint directly; the native product only has specific humidity. We will compute Td from (T, SPFH, P) using the Bolton (1980) or Magnus-Tetens formulation. This is standard and cheap.

Vertical spacing

Hybrid levels are defined by the HRRR vertical coordinate function; level 1 is closest to the surface. The actual geometric height of each level varies by grid point (because hybrid coordinates conform to terrain). Typical spacing for a plains grid point:

  • Level 1: ~8 m AGL
  • Level 2: ~25 m AGL
  • Level 3: ~50 m AGL
  • Level 5: ~130 m AGL
  • Level 10: ~460 m AGL
  • Level 20: ~1.7 km AGL
  • Level 30: ~3.8 km AGL
  • Level 50: ~19 km AGL

The first ~5 hybrid levels alone span the range where most surface-based ducts live. That's the whole point: 25 hPa pressure levels (~250 m spacing at the surface) completely miss anything thinner than ~200 m, and the native product gives us 30-50 m resolution in the same band.

(Actual per-level heights must be read from the HGT message for each grid point, not from a table. We store heights_m as {:array, :float} on hrrr_native_profiles for exactly this reason.)

Download size — the architectural gotcha

Downloaded one full wrfnatf00.grib2.idx and walked the offsets for the seven essential variables on all 50 levels:

  • 350 GRIB2 messages
  • Total byte span of the essential messages: ~530 MB
  • Full native file: ~566 MB (so essentials are almost the whole file)

That's per HRRR run hour, for the full CONUS grid.

This matters because the plan's draft of Task 1.4 has a per-QSO HrrrNativeFetchWorker. 530 MB × 58k QSOs = 29 TB of re-downloads, which is absurd. Byte-range requests don't save us — GRIB2 messages can only be decoded whole, so we'd have to pull ~350 × 1.5 MB per point, and AWS doesn't support server-side subsetting on this bucket.

What to do instead

Two workable architectures:

  1. Batch per run hour. A single worker downloads one full wrfnatf00.grib2 (~566 MB), extracts native profiles at all points of interest for that hour in one pass (the grid of contacts + whatever points the live scorer wants), and bulk-upserts them into hrrr_native_profiles. This is how the existing PropagationGridWorker pipeline works for the pressure-level file; we mirror it for native levels.

  2. Per-day local cache. Download one file per (date, hour), stash it on local disk for the duration of a backfill job, extract all points of interest for that hour, delete the file. This is probably only worth the bandwidth for dense backfills and can be folded into option 1.

Plan correction: the HrrrNativeFetchWorker in Task 1.4 should be renamed HrrrNativeGridWorker and take {date, hour} as its unique key, not {lat, lon, valid_time}. The backfill mix task in Task 1.5 iterates over historical (date, hour) pairs where we have contacts and enqueues one grid worker per hour.

Bandwidth budget

If we backfill 1 year of hourly native grids for every hour where we have at least one QSO, and we have ~58k QSOs spread over 5+ years, conservatively ~15k distinct (date, hour) hours. 15k × 566 MB ≈ 8.5 TB. That's a lot. We don't need it all — Phase 2 only needs enough data to backtest. Realistic first backfill:

  • Top 500 distinct hours by QSO count → ~280 GB (one-time, finishes overnight)
  • Extract only the lat/lon points where we have QSOs in that hour plus the propagation grid points
  • Store derived profiles only (< 5 KB per profile), not the raw GRIB2

Storage for derived profiles: 500 hours × ~300 points/hour × 5 KB ≈ 750 MB in the DB. Fine.

Proposed Phase 1 plan adjustments

  1. Rename worker: HrrrNativeGridWorker instead of HrrrNativeFetchWorker; unique keys {date, hour}; downloads the full file once per run hour and bulk-processes points.
  2. Keep hrrr_native_profiles schema as planned (per-point, per-valid_time), just populated in batch.
  3. Task 1.5 backfill mix task now enqueues grid workers for the top-N (date, hour) hours by QSO density.
  4. Feature functions in later phases look up hrrr_native_profiles the same way the existing scorer looks up hrrr_profiles — no API change on the consuming side.
  5. Computing Td from SPFH is a small Phase 2 helper; budget ~half a day for it.

Gate decision

Gate pass. Files are available historically, all required variables are present, vertical resolution is 5-30× finer than the pressure-level product near the surface, and the ingestion architecture is a known pattern (matches the existing grid worker). Proceed to Task 1.2 (schema) with the batch-mode correction.