wgrib2 -lola ... bin writes Fortran unformatted records (4-byte
length header + data + 4-byte length trailer per message).
parse_lola_binary was treating the binary as tightly packed,
causing every message after the first to read from the wrong
offset — values came out as garbage across all grid points.
Fix: account for the 8-byte record overhead per message when
computing the data offset for each message's grid values.
This bug affects both the existing propagation grid extraction
(which may have been producing subtly wrong scores) and the new
native-level extraction (which was producing obviously wrong
values). The fix is a one-line stride change.
Also adds Backtest.Features.native_surface_refractivity for the
Phase 1 sanity check, plus a tighter wgrib2 match pattern that
selects only hybrid-level messages from the native file.
The Elixir GRIB2 decoder didn't map level type 105 (hybrid) or
variable IDs for SPFH and TKE, so native-level messages decoded as
"unknown:105:N" keys that build_native_profile couldn't find. Add
the three missing mappings to Section.identify_level/identify_var.
Also add HrrrNativeClient.extract_native_profiles/2 which uses
wgrib2's -lola on a tight bounding-box subgrid for speed (the pure
Elixir decoder takes ~70s per point on a 395 MB file; wgrib2 handles
40 points in seconds). The worker now routes through this path.
Per-point ERA5 fetches were tragically slow because every point-hour
triggered its own asynchronous CDS job (submit → poll → assemble →
download). For backfill this meant thousands of independent jobs
queued against Copernicus. The new path groups requests by calendar
month and a 2° × 2° lat/lon tile so one CDS cycle populates ~60k
profiles at once, and Oban uniqueness on (year, month, tile_lat,
tile_lon) collapses every duplicate enqueue.
- Era5BatchClient builds the monthly CDS requests, extracts every
(lat, lon, hour) from the GRIB2 blob with wgrib2, derives
refractivity params, and bulk-inserts in 2k-row chunks with
on_conflict: :nothing. fetch_month_into_db/1 short-circuits when
the month-tile already has any cached profile.
- Era5MonthBatchWorker runs the batch on the :era5 queue with a
generous backoff (10m → 1d) and the uniqueness key above.
- Era5FetchWorker is now a thin router: cache hit → :ok, cache miss
→ enqueue the month-batch for the point's tile-month and return.
No more per-point CDS calls.
- Wgrib2 grows extract_grid_messages/3 which preserves per-message
datetimes by parsing `d=YYYYMMDDHH[MMSS]` from the inventory, so a
single GRIB2 file carrying a whole month decodes correctly.
- The era5_backfill mix task enqueues month-tile batches directly.
Add a separate wgrib2-builder stage that compiles wgrib2 from source.
Docker layer caching means this only builds once — subsequent deploys
reuse the cached layer. The binary is copied into the final runtime
image. Also fixed wgrib2 path lookup to be runtime instead of
compile-time so it works in the Docker build pipeline.
Add Wgrib2 module that shells out to wgrib2 binary for fast GRIB2
grid extraction using -lola (nearest-neighbor to regular lat-lon grid).
Falls back to pure-Elixir decoder if wgrib2 is not installed.
Also: parallel GRIB2 range downloads, merge adjacent byte ranges,
skip corrupt messages instead of failing, pressure fetch is optional.
Both extract_points and extract_grid now skip messages with missing
data sections instead of halting. Pressure product fetch is now
optional for both grid and single-point modes — if pressure data
has corrupt messages, surface-only profiles are still stored. Fixes
historical HRRR backfill failing on old data with incomplete files.
Add fetch_grid/3 for batch HRRR data retrieval across multiple lat/lon
points in a single download pass. Expand surface messages to include
wind (UGRD/VGRD), cloud cover (TCDC), and precipitation (APCP). Add
extract_grid/2 to the GRIB2 extractor for multi-point extraction from
a single GRIB binary, and add GRIB2 variable identifiers for the new
surface fields.
Add extract_values/3 to SimplePacking and ComplexPacking for batch
index extraction from a single GRIB2 message. Add extract_grid/2 to
Extractor which takes a list of {lat, lon} points and returns all
variable values for each point, skipping points outside the grid.
This enables extracting weather data for many grid points from a
single HRRR download instead of re-parsing per point.
extract_n_values_array used Enum.take(count) on a reversed list
before reversing it, which included padding values from byte
alignment and dropped actual values. When group count * bits
wasn't a multiple of 8, the extra padding bits produced a
phantom value that shifted the entire array by one position.
This caused cascading errors in spatial differencing — values
started correct but diverged exponentially (DPT decoded as
38 billion K instead of 275 K).
Fix: reverse the list first, then take count, so padding values
at the end are discarded instead of actual values at the start.
Instead of returning "malformed section" on trailing bytes or truncated
sections, attempt to return parsed results. This handles older HRRR
files (2019) that have padding bytes after the last section.
consume_bits would crash with a MatchError when the remaining
bitstring had fewer bits than the group width. Added a guard
clause to stop consuming when insufficient bits remain, matching
how wgrib2 handles trailing padding in packed data sections.
Eliminates the external wgrib2 C tool dependency that blocked HRRR
processing. Implements Lambert Conformal projection, simple packing
(Template 5.0), complex packing with spatial differencing (Template 5.3),
and GRIB2 section parsing — enough to extract point values from HRRR
grid data using only Elixir.