fix(weather): snap HRDPS reads to nearest valid_time within 6h
The /weather dual-source timeline picks times from HRRR's hourly cadence, but HRDPS publishes 4×/day with multi-hour latency, so the requested time almost never has an exact HRDPS dir on disk. The controller was returning empty rows for HRDPS, leaving the Canadian half of the map blank. Snap to the closest HRDPS valid_time within a 6h window. /weather-ca keeps exact-time semantics in practice because its timeline is built from HRDPS-only listings, so the nearest is always the same time.
This commit is contained in:
parent
b6fb7d2a13
commit
040ebf9d3d
3 changed files with 96 additions and 1 deletions
|
|
@ -933,10 +933,20 @@ defmodule Microwaveprop.Weather do
|
|||
because the cache mixes HRRR + HRDPS rows by design — caching this
|
||||
separately would double the per-pod memory budget for marginal benefit
|
||||
(Canadian viewport reads are infrequent compared to CONUS).
|
||||
|
||||
The /weather (dual-source) timeline picks valid_times from HRRR's
|
||||
hourly cadence, but HRDPS publishes 4×/day with multi-hour latency,
|
||||
so the requested time will frequently miss any on-disk HRDPS dir.
|
||||
Snapping to the nearest available HRDPS time within a 6 h window
|
||||
keeps the Canadian overlay rendering slightly stale instead of
|
||||
disappearing entirely. /weather-ca picks times from HRDPS-only listings
|
||||
so the exact time always matches and the snap is a no-op.
|
||||
"""
|
||||
@hrdps_nearest_window_seconds 6 * 3600
|
||||
|
||||
@spec weather_grid_hrdps_at(DateTime.t(), %{optional(String.t()) => float()} | nil) :: [map()]
|
||||
def weather_grid_hrdps_at(%DateTime{} = valid_time, bounds) do
|
||||
ScalarFile.read_bounds_hrdps(valid_time, bounds)
|
||||
ScalarFile.read_bounds_hrdps_nearest(valid_time, bounds, @hrdps_nearest_window_seconds)
|
||||
end
|
||||
|
||||
@doc """
|
||||
|
|
|
|||
|
|
@ -191,6 +191,39 @@ defmodule Microwaveprop.Weather.ScalarFile do
|
|||
read_chunks(list_chunk_files_hrdps(valid_time), bounds)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Read HRDPS rows for the closest available valid_time within
|
||||
`window_seconds` of `target`. Returns rows from the snapped time, or
|
||||
`[]` if no HRDPS dir falls inside the window.
|
||||
|
||||
HRDPS publishes 4×/day with ~3-4 h latency, so the /weather timeline
|
||||
(driven by HRRR's hourly cadence) will frequently request a time that
|
||||
has no HRDPS dir on disk. Snapping to the nearest available time lets
|
||||
the Canadian overlay render slightly stale rather than disappear
|
||||
entirely whenever the user's selected forecast hour misses HRDPS.
|
||||
"""
|
||||
@spec read_bounds_hrdps_nearest(DateTime.t(), bounds() | nil, non_neg_integer()) :: [row()]
|
||||
def read_bounds_hrdps_nearest(%DateTime{} = target, bounds, window_seconds)
|
||||
when is_integer(window_seconds) and window_seconds >= 0 do
|
||||
case nearest_hrdps_valid_time(target, window_seconds) do
|
||||
nil -> []
|
||||
vt -> read_chunks(list_chunk_files_hrdps(vt), bounds)
|
||||
end
|
||||
end
|
||||
|
||||
defp nearest_hrdps_valid_time(target, window_seconds) do
|
||||
target_unix = DateTime.to_unix(target)
|
||||
|
||||
list_valid_times_hrdps()
|
||||
|> Enum.map(fn vt -> {vt, abs(DateTime.to_unix(vt) - target_unix)} end)
|
||||
|> Enum.filter(fn {_vt, delta} -> delta <= window_seconds end)
|
||||
|> Enum.min_by(fn {_vt, delta} -> delta end, fn -> nil end)
|
||||
|> case do
|
||||
nil -> nil
|
||||
{vt, _delta} -> vt
|
||||
end
|
||||
end
|
||||
|
||||
defp read_chunks([], _bounds), do: []
|
||||
|
||||
defp read_chunks(files, bounds) do
|
||||
|
|
|
|||
|
|
@ -246,6 +246,58 @@ defmodule Microwaveprop.Weather.ScalarFileTest do
|
|||
end
|
||||
end
|
||||
|
||||
# The /weather (dual-source) timeline picks valid_times from HRRR's
|
||||
# hourly cadence. HRDPS publishes 4×/day with multi-hour latency, so
|
||||
# the requested time will frequently miss any HRDPS dir. Snapping to
|
||||
# the closest HRDPS dir within a window lets the Canadian overlay
|
||||
# render slightly stale rather than disappear.
|
||||
describe "read_bounds_hrdps_nearest/3" do
|
||||
test "returns rows from the exact time when its HRDPS dir exists" do
|
||||
vt = ~U[2026-04-29 12:00:00Z]
|
||||
hand_write_chunk(ScalarFile.dir_for_hrdps(vt), 53.0, -60.0, 8.0)
|
||||
|
||||
assert [%{lat: 53.0, lon: -60.0, temperature: 8.0}] =
|
||||
ScalarFile.read_bounds_hrdps_nearest(vt, nil, 6 * 3600)
|
||||
end
|
||||
|
||||
test "snaps to the closest HRDPS dir within the window when exact time is absent" do
|
||||
requested = ~U[2026-04-29 14:00:00Z]
|
||||
closer = ~U[2026-04-29 13:00:00Z]
|
||||
farther = ~U[2026-04-29 10:00:00Z]
|
||||
|
||||
hand_write_chunk(ScalarFile.dir_for_hrdps(closer), 53.0, -60.0, 7.0)
|
||||
hand_write_chunk(ScalarFile.dir_for_hrdps(farther), 53.0, -60.0, 99.0)
|
||||
|
||||
assert [%{temperature: 7.0}] =
|
||||
ScalarFile.read_bounds_hrdps_nearest(requested, nil, 6 * 3600)
|
||||
end
|
||||
|
||||
test "snaps forward as well as backward (HRDPS forecast can lead requested time)" do
|
||||
requested = ~U[2026-04-29 12:00:00Z]
|
||||
future = ~U[2026-04-29 13:00:00Z]
|
||||
|
||||
hand_write_chunk(ScalarFile.dir_for_hrdps(future), 53.0, -60.0, 11.0)
|
||||
|
||||
assert [%{temperature: 11.0}] =
|
||||
ScalarFile.read_bounds_hrdps_nearest(requested, nil, 6 * 3600)
|
||||
end
|
||||
|
||||
test "returns [] when no HRDPS dir exists inside the window" do
|
||||
requested = ~U[2026-04-29 14:00:00Z]
|
||||
stale = ~U[2026-04-29 02:00:00Z]
|
||||
|
||||
hand_write_chunk(ScalarFile.dir_for_hrdps(stale), 53.0, -60.0, 8.0)
|
||||
|
||||
# 12h gap, 6h window → not eligible.
|
||||
assert ScalarFile.read_bounds_hrdps_nearest(requested, nil, 6 * 3600) == []
|
||||
end
|
||||
|
||||
test "returns [] when no HRDPS dirs exist at all" do
|
||||
requested = ~U[2026-04-29 14:00:00Z]
|
||||
assert ScalarFile.read_bounds_hrdps_nearest(requested, nil, 6 * 3600) == []
|
||||
end
|
||||
end
|
||||
|
||||
defp hand_write_chunk(dir, lat, lon, temp) do
|
||||
File.mkdir_p!(dir)
|
||||
lat_band = (lat / 5) |> Float.floor() |> trunc()
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue