fix(weather): read .sgrid in read_bounds_hrdps_nearest/3

weather_grid_hrdps_at/2 is the only production caller and backs every
/weather/cells?source=hrdps request, but it read the legacy chunked
.mp.gz dirs exclusively. The pipeline writes <iso>.hrdps.sgrid, so the
snap found a valid_time via list_valid_times_hrdps/0 (which does see
.sgrid) and then read zero chunks for it.

Delegate to read_bounds_hrdps/2, which already does the .sgrid-first,
chunk-fallback read.

Every existing test for this function hand-wrote legacy chunk dirs,
which is how the .sgrid migration slipped past. Added coverage against
a real .sgrid artifact, including the mid-migration case where both
formats exist for one valid_time.
This commit is contained in:
Graham McIntire 2026-08-05 17:33:51 -05:00
parent 3d214f60f0
commit ad600361e0
No known key found for this signature in database
GPG key ID: F4ABF488E6029E59
2 changed files with 67 additions and 1 deletions

View file

@ -210,7 +210,7 @@ defmodule Microwaveprop.Weather.ScalarFile do
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)
vt -> read_bounds_hrdps(vt, bounds)
end
end

View file

@ -4,6 +4,7 @@ defmodule Microwaveprop.Weather.ScalarFileTest do
use ExUnit.Case, async: false
alias Microwaveprop.Weather.ScalarFile
alias Microwaveprop.Weather.Sgrid
setup do
dir =
@ -296,6 +297,71 @@ defmodule Microwaveprop.Weather.ScalarFileTest do
requested = ~U[2026-04-29 14:00:00Z]
assert ScalarFile.read_bounds_hrdps_nearest(requested, nil, 6 * 3600) == []
end
# The Rust pipeline writes `<iso>.hrdps.sgrid`, not the legacy chunked
# `<iso>.hrdps/` dirs. `weather_grid_hrdps_at/2` is the only production
# caller of this function and it backs every `/weather/cells?source=hrdps`
# request, so a chunk-only read here means the Canadian overlay renders
# nothing on `/weather` and `/weather-ca` is blank end to end.
test "reads the dense .sgrid artifact at the exact time" do
vt = ~U[2026-04-29 12:00:00Z]
hand_write_sgrid_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 .sgrid 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_sgrid_hrdps(closer, 53.0, -60.0, 7.0)
hand_write_sgrid_hrdps(farther, 53.0, -60.0, 99.0)
assert [%{temperature: 7.0}] =
ScalarFile.read_bounds_hrdps_nearest(requested, nil, 6 * 3600)
end
test "respects bounds when reading a .sgrid" do
vt = ~U[2026-04-29 12:00:00Z]
hand_write_sgrid_hrdps(vt, 53.0, -60.0, 8.0)
bounds = %{"south" => 60.0, "north" => 65.0, "west" => -70.0, "east" => -50.0}
assert ScalarFile.read_bounds_hrdps_nearest(vt, bounds, 6 * 3600) == []
end
# A valid_time can have both artifacts mid-migration (the chunk dir
# hasn't drained yet). The dense file is authoritative — reading both
# would double-paint the cell.
test "prefers the .sgrid over a co-located legacy chunk dir" do
vt = ~U[2026-04-29 12:00:00Z]
hand_write_chunk(ScalarFile.dir_for_hrdps(vt), 53.0, -60.0, 99.0)
hand_write_sgrid_hrdps(vt, 53.0, -60.0, 8.0)
assert [%{temperature: 8.0}] = ScalarFile.read_bounds_hrdps_nearest(vt, nil, 6 * 3600)
end
end
# Emit a single-cell `<iso>.hrdps.sgrid` byte-for-byte as
# `prop_grid_rs::sgrid` writes it, so this test fails if either side
# of the cross-language wire format drifts.
defp hand_write_sgrid_hrdps(valid_time, lat, lon, temp) do
path = Sgrid.path_for_hrdps(valid_time)
File.mkdir_p!(Path.dirname(path))
fields = ["temperature", "dewpoint_depression"]
field_table = Enum.map_join(fields, fn name -> name <> :binary.copy(<<0>>, 32 - byte_size(name)) end)
# flags bit0 = 1 → HRDPS. 1×1 grid anchored on the cell itself.
header =
<<"SGRD", 1::8, 1::8, length(fields)::little-16, DateTime.to_unix(valid_time)::little-signed-64,
lat::little-float-64, lon::little-float-64, 0.5::little-float-64, 0.5::little-float-64, 1::little-16,
1::little-16>>
body = <<temp::little-float-32, 5.0::little-float-32>>
File.write!(path, header <> field_table <> body)
end
defp hand_write_chunk(dir, lat, lon, temp) do