- 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).
7.2 KiB
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 2–4 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:
-
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 intohrrr_native_profiles. This is how the existingPropagationGridWorkerpipeline works for the pressure-level file; we mirror it for native levels. -
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
- Rename worker:
HrrrNativeGridWorkerinstead ofHrrrNativeFetchWorker; unique keys{date, hour}; downloads the full file once per run hour and bulk-processes points. - Keep
hrrr_native_profilesschema as planned (per-point, per-valid_time), just populated in batch. - Task 1.5 backfill mix task now enqueues grid workers for the top-N (date, hour) hours by QSO density.
- Feature functions in later phases look up
hrrr_native_profilesthe same way the existing scorer looks uphrrr_profiles— no API change on the consuming side. - 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.