defmodule Microwaveprop.Weather.HrrrProfileLookupTest do use Microwaveprop.DataCase, async: true alias Microwaveprop.Weather.HrrrProfile alias Microwaveprop.Weather.HrrrProfileLookup # 13-level standard HRRR profile so test fixtures pass the default # min_profile_levels filter. Tests that want to exercise the # partial-profile rejection path pass an explicit shorter profile. @full_profile [ %{"pres" => 1000.0, "hght" => 100.0, "tmpc" => 25.0, "dwpc" => 18.0}, %{"pres" => 925.0, "hght" => 800.0, "tmpc" => 19.0, "dwpc" => 14.0}, %{"pres" => 850.0, "hght" => 1500.0, "tmpc" => 13.0, "dwpc" => 8.0}, %{"pres" => 700.0, "hght" => 3100.0, "tmpc" => 2.0, "dwpc" => -8.0}, %{"pres" => 500.0, "hght" => 5800.0, "tmpc" => -15.0, "dwpc" => -28.0}, %{"pres" => 400.0, "hght" => 7200.0, "tmpc" => -28.0, "dwpc" => -42.0}, %{"pres" => 300.0, "hght" => 9300.0, "tmpc" => -42.0, "dwpc" => -56.0}, %{"pres" => 250.0, "hght" => 10_400.0, "tmpc" => -50.0, "dwpc" => -64.0}, %{"pres" => 200.0, "hght" => 11_700.0, "tmpc" => -55.0, "dwpc" => -70.0}, %{"pres" => 150.0, "hght" => 13_500.0, "tmpc" => -60.0, "dwpc" => -75.0}, %{"pres" => 100.0, "hght" => 16_000.0, "tmpc" => -65.0, "dwpc" => -80.0}, %{"pres" => 70.0, "hght" => 18_500.0, "tmpc" => -65.0, "dwpc" => -82.0}, %{"pres" => 50.0, "hght" => 20_500.0, "tmpc" => -62.0, "dwpc" => -84.0} ] describe "list_valid_times_near/3" do test "returns valid_times of every profile within the spatial tolerance" do insert_profile!(%{lat: 32.625, lon: -97.125, valid_time: ~U[2026-04-25 14:00:00Z]}) insert_profile!(%{lat: 32.625, lon: -97.125, valid_time: ~U[2026-04-25 15:00:00Z]}) # Far cell — must be excluded. insert_profile!(%{lat: 40.000, lon: -80.000, valid_time: ~U[2026-04-25 14:00:00Z]}) assert [t1, t2] = HrrrProfileLookup.list_valid_times_near(32.65, -97.13, []) assert DateTime.to_iso8601(t1) == "2026-04-25T14:00:00Z" assert DateTime.to_iso8601(t2) == "2026-04-25T15:00:00Z" end test "limit option caps the number of times returned (most recent first kept)" do for h <- 0..4 do valid_time = DateTime.add(~U[2026-04-25 12:00:00Z], h * 3600, :second) insert_profile!(%{lat: 32.625, lon: -97.125, valid_time: valid_time}) end times = HrrrProfileLookup.list_valid_times_near(32.65, -97.13, limit: 3) assert length(times) == 3 # Capped to the 3 most recent — but still sorted ascending in the # returned list so SkewtLive's selector renders left-to-right. assert Enum.map(times, &DateTime.to_iso8601/1) == [ "2026-04-25T14:00:00Z", "2026-04-25T15:00:00Z", "2026-04-25T16:00:00Z" ] end test "tolerance option widens the spatial search box" do insert_profile!(%{lat: 32.625, lon: -97.125, valid_time: ~U[2026-04-25 14:00:00Z]}) # 0.20° away, outside the default 0.07° box but inside 0.25°. insert_profile!(%{lat: 32.825, lon: -97.125, valid_time: ~U[2026-04-25 15:00:00Z]}) tight = HrrrProfileLookup.list_valid_times_near(32.625, -97.125, tolerance_deg: 0.07) wide = HrrrProfileLookup.list_valid_times_near(32.625, -97.125, tolerance_deg: 0.25) assert length(tight) == 1 assert length(wide) == 2 end end describe "read_point_near/3" do test "returns the profile of the nearest cell at the requested valid_time" do insert_profile!(%{ lat: 32.625, lon: -97.125, valid_time: ~U[2026-04-25 14:00:00Z], surface_temp_c: 20.5, surface_dewpoint_c: 15.0, surface_pressure_mb: 1013.0 }) assert %{} = cell = HrrrProfileLookup.read_point_near(~U[2026-04-25 14:00:00Z], 32.65, -97.13) # Default fixture is the 13-level @full_profile (HRRR's standard # canonical pressure-level set). assert length(cell[:profile]) == 13 [%{"pres" => pres} | _] = cell[:profile] assert pres == 1000.0 or pres == 1000 assert cell[:surface_temp_c] == 20.5 assert cell[:surface_pressure_mb] == 1013.0 end test "returns nil if no profile is within tolerance at that valid_time" do assert HrrrProfileLookup.read_point_near(~U[2026-04-25 14:00:00Z], 32.65, -97.13) == nil end test "picks the spatially closest cell when multiple match" do # Both cells use the 13-level default profile so they pass the # `min_profile_levels` filter; surface T/Td differ so we can tell # which row read_point_near returned. [first | rest] = @full_profile close_profile = [Map.put(first, "tmpc", 25.0) | rest] far_profile = [Map.put(first, "tmpc", 30.0) | rest] insert_profile!(%{ lat: 32.65, lon: -97.13, valid_time: ~U[2026-04-25 14:00:00Z], profile: close_profile }) insert_profile!(%{ lat: 32.69, lon: -97.18, valid_time: ~U[2026-04-25 14:00:00Z], profile: far_profile }) cell = HrrrProfileLookup.read_point_near(~U[2026-04-25 14:00:00Z], 32.65, -97.13) [%{"tmpc" => surface_t} | _] = cell[:profile] assert surface_t == 25.0 end end describe "min_profile_levels filtering" do test "list_valid_times_near rejects rows whose profile is shorter than the threshold" do # Two rows: one full (13 levels), one partial (3 levels). insert_profile!(%{lat: 32.625, lon: -97.125, valid_time: ~U[2026-04-25 14:00:00Z]}) insert_profile!(%{ lat: 32.625, lon: -97.125, valid_time: ~U[2026-04-25 15:00:00Z], profile: [ %{"pres" => 1000.0, "hght" => 100.0, "tmpc" => 25.0, "dwpc" => 18.0}, %{"pres" => 850.0, "hght" => 1500.0, "tmpc" => 13.0, "dwpc" => 8.0}, %{"pres" => 500.0, "hght" => 5800.0, "tmpc" => -15.0, "dwpc" => -28.0} ] }) # Default threshold (13) → only the 14 :00 row. times = HrrrProfileLookup.list_valid_times_near(32.65, -97.13) assert Enum.map(times, &DateTime.to_iso8601/1) == ["2026-04-25T14:00:00Z"] # Threshold of 0 → both rows. both = HrrrProfileLookup.list_valid_times_near(32.65, -97.13, min_profile_levels: 0) assert length(both) == 2 end test "read_point_near returns nil for partial-profile valid_times under the default threshold" do insert_profile!(%{ lat: 32.625, lon: -97.125, valid_time: ~U[2026-04-25 15:00:00Z], profile: [%{"pres" => 1000.0, "hght" => 100.0, "tmpc" => 25.0, "dwpc" => 18.0}] }) assert HrrrProfileLookup.read_point_near(~U[2026-04-25 15:00:00Z], 32.65, -97.13) == nil assert %{} = HrrrProfileLookup.read_point_near(~U[2026-04-25 15:00:00Z], 32.65, -97.13, min_profile_levels: 0) end end defp insert_profile!(attrs) do defaults = %{ profile: @full_profile, run_time: nil, hpbl_m: 800.0, pwat_mm: 25.0, surface_temp_c: 25.0, surface_dewpoint_c: 18.0, surface_pressure_mb: 1013.0, surface_refractivity: 320.0, min_refractivity_gradient: -90.0, ducting_detected: false, duct_characteristics: [], is_grid_point: false } %HrrrProfile{} |> HrrrProfile.changeset(Map.merge(defaults, attrs)) |> Microwaveprop.Repo.insert!() end end