From b14c87126cd6ed37fb9588727211579d0cbe7b34 Mon Sep 17 00:00:00 2001 From: Graham McIntire Date: Thu, 30 Apr 2026 16:44:53 -0500 Subject: [PATCH] fix(prop-grid): keep HRDPS lat=50 boundary row to close /weather seam MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit HRDPS at 0.5° step previously dropped its lat=50 row inside HRRR's lon range, so its first painted cell sat at lat=50.5 (halfStep=0.25 → 50.25°N). HRRR's top row at lat=50 paints to 50.0625°N. The ~0.19° (~21 km) blank strip between them was visible on /weather wherever the viewport crossed the US-Canada border. Make in_conus_bbox exclusive on the north edge (lat < LAT_MAX) so the boundary row stays in HRDPS. The merge_prefer_hrrr layer dedupes by exact (lat, lon) on the merged read path, and the canvas overlay paints HRRR above HRDPS, so the new overlap is invisible everywhere HRRR has data. Won't take effect on prod until the next HRDPS chain run produces fresh chunk files. --- rust/prop_grid_rs/src/grid.rs | 49 ++++++++++++++++++++++++++++++----- 1 file changed, 42 insertions(+), 7 deletions(-) diff --git a/rust/prop_grid_rs/src/grid.rs b/rust/prop_grid_rs/src/grid.rs index eb002d1b..de3c9709 100644 --- a/rust/prop_grid_rs/src/grid.rs +++ b/rust/prop_grid_rs/src/grid.rs @@ -1,10 +1,13 @@ //! Grid definitions at 0.125° resolution. 1:1 port of -//! `lib/microwaveprop/propagation/grid.ex`. Two regions, disjoint by -//! construction: +//! `lib/microwaveprop/propagation/grid.ex`. Two regions, disjoint on +//! HRRR's *interior*: //! //! * `conus_points()` — HRRR coverage (lat 25-50, lon -125 to -66). -//! * `hrdps_only_points()` — Canadian extent covered by HRDPS but not -//! HRRR (lat 49-60 minus the HRRR overlap, lon -141 to -52). +//! * `hrdps_only_points()` — HRDPS bbox (lat 49-60, lon -141 to -52) +//! minus HRRR's interior (lat < 50 inside HRRR's lon range). The +//! lat=50 boundary row is kept so HRDPS's halfStep painting closes +//! the seam between HRRR and HRDPS on /weather; the merge layer +//! dedupes by exact (lat, lon) for the merged read path. //! //! Coverage stops at 60°N for v1 (SRTM elevation cuts off there; Arctic //! CDEM coverage is deferred to a follow-up plan). @@ -110,7 +113,14 @@ pub fn hrdps_only_points() -> Vec<(f64, f64)> { #[inline] fn in_conus_bbox(lat: f64, lon: f64) -> bool { - (LAT_MIN..=LAT_MAX).contains(&lat) && (LON_MIN..=LON_MAX).contains(&lon) + // Exclusive on the north edge: HRDPS keeps the lat=LAT_MAX (50.0) + // boundary row so its halfStep painting (±0.25°) overlaps HRRR's top + // edge (±0.0625°) on /weather, closing a ~0.19° blank strip the + // user would otherwise see between HRRR and HRDPS coverage. The + // merge_prefer_hrrr layer dedupes by exact (lat, lon) on the merged + // read path, and the canvas overlay paints HRRR above HRDPS, so the + // overlap is only visible north of HRRR's coverage. + (LAT_MIN..LAT_MAX).contains(&lat) && (LON_MIN..=LON_MAX).contains(&lon) } /// Matches Elixir's `Float.round(x, 3)` — banker's rounding isn't used, @@ -166,16 +176,21 @@ mod tests { } #[test] - fn hrdps_only_points_are_disjoint_from_conus() { + fn hrdps_only_points_disjoint_from_conus_interior() { + // Disjointness now holds for CONUS *interior* (lat < LAT_MAX); the + // lat=LAT_MAX boundary row is intentionally shared so HRDPS fills + // the seam between HRRR and Canada on /weather. See + // `hrdps_keeps_lat_50_boundary_inside_conus_lons`. let conus: std::collections::HashSet<(u64, u64)> = conus_points() .into_iter() + .filter(|(la, _)| *la < LAT_MAX) .map(|(la, lo)| (la.to_bits(), lo.to_bits())) .collect(); for (la, lo) in hrdps_only_points() { assert!( !conus.contains(&(la.to_bits(), lo.to_bits())), - "hrdps point {la},{lo} overlaps conus" + "hrdps point {la},{lo} overlaps conus interior" ); } } @@ -194,4 +209,24 @@ mod tests { let any_above_50 = hrdps_only_points().iter().any(|(la, _)| *la > 50.0); assert!(any_above_50); } + + #[test] + fn hrdps_keeps_lat_50_boundary_inside_conus_lons() { + // HRRR tops out at 50.0°N (halfStep=0.0625 → paints to 50.0625°N). + // HRDPS at 0.5° step would otherwise jump to 50.5°N (halfStep=0.25 + // → paints down to 50.25°N), leaving a ~21 km blank strip on the + // map. Keep the lat=50 row in HRDPS so it paints from 49.75°N to + // 50.25°N and overlaps HRRR's top edge — the merge layer dedupes + // by exact (lat, lon) match for the merged read path, and the + // /weather canvas paints HRRR on top of HRDPS so the overlap is + // invisible everywhere HRRR exists. + let points = hrdps_only_points(); + let has_50_in_conus = points + .iter() + .any(|(la, lo)| (*la - 50.0).abs() < 1e-9 && (-125.0..=-66.0).contains(lo)); + assert!( + has_50_in_conus, + "expected hrdps_only_points to include the lat=50 row inside CONUS lon range" + ); + } }