Root cause of 14 stuck hrrr-queued contacts: hrrr_point_rs's CONUS-only grid silently writes zero profiles for out-of-bounds points (UK, Winnipeg, Alberta, >50° lat), but the fetch task still completes successfully. ContactWeatherEnqueueWorker's hrrr_placeholder_jobs always emitted a queued placeholder for any contact with a pos1, which meant mark_hrrr_status! re-flagged :queued every backfill tick forever. Introduce Propagation.Grid.contains?/1 as the canonical in-CONUS check (inclusive on all four edges, nil-safe). Use it in two places of ContactWeatherEnqueueWorker: - hrrr_placeholder_jobs/1 now returns [] for OCONUS contacts, same as pre-2014 / already-complete ones. - mark_hrrr_status!/3 empty-list clause now branches on Grid.contains? first (→ :unavailable) before falling through to the existing NARR-coverage / :complete logic. Next backfill tick will flip the 14 stuck OCONUS contacts in prod to :unavailable automatically. The remaining 13 "CONUS" stuck ones are right at the 50° edge (lat=50.1875 rounds outside the box) — same reconciliation path. Coverage additions: - Grid.contains?/1 — corner-inclusive, OCONUS positive/negative fixtures based on the actual stuck-contact pos1 values, nil/missing- key/nil-value handling. - enqueue_for_contact regression tests: pos1 in the UK and pos1 just north of the lat cap both land at :unavailable, not :queued. Suite: 2,416 tests + 159 properties (was 2,409 + 159); credo strict clean.
73 lines
2.4 KiB
Elixir
73 lines
2.4 KiB
Elixir
defmodule Microwaveprop.Propagation.Grid do
|
|
@moduledoc "CONUS grid definition for propagation scoring. 0.125 degree resolution (~14 km)."
|
|
|
|
@lat_min 25.0
|
|
@lat_max 50.0
|
|
@lon_min -125.0
|
|
@lon_max -66.0
|
|
@step 0.125
|
|
|
|
@doc "Returns all grid points as `{lat, lon}` tuples covering CONUS at 0.125 degree spacing."
|
|
@spec conus_points() :: [{float(), float()}]
|
|
def conus_points do
|
|
for lat <- float_range(@lat_min, @lat_max, @step),
|
|
lon <- float_range(@lon_min, @lon_max, @step) do
|
|
{Float.round(lat, 3), Float.round(lon, 3)}
|
|
end
|
|
end
|
|
|
|
@doc "Returns the grid step size in degrees."
|
|
@spec step() :: float()
|
|
def step, do: @step
|
|
|
|
@doc "Returns the CONUS bounding box as a map."
|
|
@spec bounds() :: %{lat_min: float(), lat_max: float(), lon_min: float(), lon_max: float()}
|
|
def bounds, do: %{lat_min: @lat_min, lat_max: @lat_max, lon_min: @lon_min, lon_max: @lon_max}
|
|
|
|
@doc """
|
|
True when the given position lies inside the CONUS bounding box
|
|
(inclusive on all four edges). Callers use this to gate HRRR
|
|
enrichment — contacts whose path origin is outside the grid would
|
|
be enqueued forever because `hrrr_point_rs` silently writes no
|
|
profiles for out-of-grid points.
|
|
|
|
Accepts the canonical `%{"lat" => _, "lon" => _}` pos map. Returns
|
|
`false` for nil, missing keys, or nil numeric values.
|
|
"""
|
|
@spec contains?(map() | nil) :: boolean()
|
|
def contains?(nil), do: false
|
|
|
|
def contains?(%{"lat" => lat, "lon" => lon}) when is_number(lat) and is_number(lon) do
|
|
lat >= @lat_min and lat <= @lat_max and lon >= @lon_min and lon <= @lon_max
|
|
end
|
|
|
|
def contains?(_), do: false
|
|
|
|
@doc "Returns the grid specification for wgrib2 -lola extraction."
|
|
@spec wgrib2_grid_spec() :: %{
|
|
lon_start: float(),
|
|
lon_count: non_neg_integer(),
|
|
lon_step: float(),
|
|
lat_start: float(),
|
|
lat_count: non_neg_integer(),
|
|
lat_step: float()
|
|
}
|
|
def wgrib2_grid_spec do
|
|
lon_count = round((@lon_max - @lon_min) / @step) + 1
|
|
lat_count = round((@lat_max - @lat_min) / @step) + 1
|
|
|
|
%{
|
|
lon_start: @lon_min,
|
|
lon_count: lon_count,
|
|
lon_step: @step,
|
|
lat_start: @lat_min,
|
|
lat_count: lat_count,
|
|
lat_step: @step
|
|
}
|
|
end
|
|
|
|
defp float_range(start, stop, step) do
|
|
count = round((stop - start) / step) + 1
|
|
Enum.map(0..(count - 1), fn i -> start + i * step end)
|
|
end
|
|
end
|