diff --git a/lib/microwaveprop/weather/scalar_file.ex b/lib/microwaveprop/weather/scalar_file.ex index 3b39b268..998c3093 100644 --- a/lib/microwaveprop/weather/scalar_file.ex +++ b/lib/microwaveprop/weather/scalar_file.ex @@ -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 diff --git a/test/microwaveprop/weather/scalar_file_test.exs b/test/microwaveprop/weather/scalar_file_test.exs index 901d7a4a..f58b84d8 100644 --- a/test/microwaveprop/weather/scalar_file_test.exs +++ b/test/microwaveprop/weather/scalar_file_test.exs @@ -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 `.hrdps.sgrid`, not the legacy chunked + # `.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 `.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 = <> + + File.write!(path, header <> field_table <> body) end defp hand_write_chunk(dir, lat, lon, temp) do