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" + ); + } }