fix(rust): snap HRRR point lookups to 0.125° grid

hrrr-point-rs was marking fetch tasks 'done' with 0 profiles inserted
for ~13,900 historical contacts. The wgrib2 subprocess resamples HRRR
to the 0.125° CONUS prop grid, but the lookup keyed on the requested
point (rounded to 0.01° by Weather.round_to_hrrr_grid/2) — those keys
never match a 0.125° cell exactly, so every point fell through the
`cell.is_empty()` skip. Snap to the nearest 0.125° cell for lookup,
store at the original (lat, lon) so Weather.has_hrrr_profile?/3's
0.07° search window finds it on the next backfill tick.

TDD: added snap_key unit tests.
This commit is contained in:
Graham McIntire 2026-04-22 16:50:36 -05:00
parent d6d50f7729
commit 7062344434
No known key found for this signature in database
GPG key ID: F4ABF488E6029E59

View file

@ -18,7 +18,7 @@ use uuid::Uuid;
use crate::decoder::{self, CellValues};
use crate::fetcher::{self, HrrrClient, Product};
use crate::grid::wgrib2_grid_spec;
use crate::grid::{wgrib2_grid_spec, STEP};
// wgrib2 filter patterns are derived from static SURFACE_MESSAGES /
// GRID_PRESSURE_LEVELS tables and never change at runtime. Building
@ -104,11 +104,16 @@ pub async fn process_batch(
}
// For each requested point, pull the merged cell and build an
// hrrr_profiles row. Skip the point silently if extract_grid
// produced nothing for its snapped key (e.g. off-CONUS).
// hrrr_profiles row. Elixir snaps points to 0.01° (see
// `Weather.round_to_hrrr_grid/2`), but wgrib2 resamples to the 0.125°
// CONUS prop grid — so we must re-snap before keying into `sfc_grid`
// or the lookup misses and the task completes with zero inserts.
// Store the profile at the original requested (lat, lon) so
// `Weather.has_hrrr_profile?/3` (0.07° window) finds it on the
// next backfill tick and flips the contact to `:complete`.
let mut inserted = 0u32;
for &(lat, lon) in points {
let key = ((lat * 1000.0).round() as i32, (lon * 1000.0).round() as i32);
let key = snap_key(lat, lon);
let mut cell: CellValues = sfc_grid.get(&key).cloned().unwrap_or_default();
if let Some(prs) = prs_grid.get(&key) {
cell.extend(prs.iter().map(|(k, v)| (k.clone(), *v)));
@ -210,7 +215,43 @@ async fn upsert_profile(
Ok(())
}
/// Snap a requested (lat, lon) to the nearest 0.125° grid cell and
/// return its integer-millidegree key. The wgrib2 subprocess resamples
/// HRRR to that grid (see `wgrib2_grid_spec()`), so a request on the
/// HRRR native ~3 km grid (e.g. 44.94, -91.63 after
/// `Weather.round_to_hrrr_grid/2`, which rounds to 0.01°) never matches
/// a 0.125° cell key exactly and the point is silently dropped —
/// producing the "task done with 0 profiles inserted" pattern that
/// wedged 13k+ contacts at `hrrr_status = :queued`.
fn snap_key(lat: f64, lon: f64) -> (i32, i32) {
let snap_lat = (lat / STEP).round() * STEP;
let snap_lon = (lon / STEP).round() * STEP;
(
(snap_lat * 1000.0).round() as i32,
(snap_lon * 1000.0).round() as i32,
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn snap_key_on_grid_is_identity() {
assert_eq!(snap_key(45.0, -91.625), (45_000, -91_625));
assert_eq!(snap_key(25.0, -125.0), (25_000, -125_000));
}
#[test]
fn snap_key_rounds_to_nearest_0_125_cell() {
// 44.94 → 45.0 (0.06 away vs 44.875 which is 0.065 away)
assert_eq!(snap_key(44.94, -91.63), (45_000, -91_625));
// 44.83 → 44.875 (0.045 vs 44.75 which is 0.08)
assert_eq!(snap_key(44.83, -91.5), (44_875, -91_500));
// 44.73 → 44.75 (0.02 vs 44.625 which is 0.105)
assert_eq!(snap_key(44.73, -91.38), (44_750, -91_375));
}
}
// Integration tests for process_batch + upsert_profile live in
// tests/hrrr_points_live.rs (network + DB gated). No unit tests here
// — the two public functions are thin glue over fetcher/decoder which
// carry their own test coverage.
// tests/hrrr_points_live.rs (network + DB gated).