prop/test/microwaveprop/weather/hrrr_native_profile_test.exs
Graham McIntire 30c1018400
Extend skew-T plot to cover the full troposphere
Historical contacts showed a skew-T log-P diagram that stopped at 700 mb
because HrrrClient and Era5Client only fetched pressure-level data down
to the top of the boundary layer. The chart canvas already ran up to
100 mb, so the trace clipped mid-atmosphere.

Two complementary fixes:

1. Extend @pressure_levels in HrrrClient and Era5Client with
   650/600/550/500/450/400/350/300/250/200/150/100 mb so new fetches
   cover the full troposphere + lower stratosphere.

2. Prefer the native hybrid-sigma profile for the contact-detail
   skew-T when one has been backfilled for the contact's hour. The
   native profile already stores all 50 hybrid levels up to ~19 km,
   so historical contacts covered by the native backfill get a full
   trace without re-hitting S3. A new HrrrNativeProfile.to_skew_t_profile/1
   converts the parallel arrays into the %{"pres","tmpc","dwpc","hght"}
   list shape the renderer expects, deriving dewpoint from SPFH via the
   Magnus inverse. Weather.find_nearest_native_profile/3 mirrors
   find_nearest_hrrr/3 for the lookup.
2026-04-14 17:25:58 -05:00

111 lines
3.6 KiB
Elixir
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

defmodule Microwaveprop.Weather.HrrrNativeProfileTest do
use Microwaveprop.DataCase, async: true
alias Microwaveprop.Weather.HrrrNativeProfile
@valid_attrs %{
valid_time: ~U[2026-03-28 18:00:00Z],
run_time: ~U[2026-03-28 18:00:00Z],
lat: 32.9,
lon: -97.0,
level_count: 3,
heights_m: [10.0, 100.0, 500.0],
temp_k: [295.0, 292.0, 285.0],
spfh: [0.010, 0.008, 0.004],
pressure_pa: [101_000.0, 99_800.0, 95_000.0],
u_wind_ms: [2.0, 3.0, 4.0],
v_wind_ms: [1.0, 1.5, 2.0],
tke_m2s2: [0.5, 0.3, 0.1],
surface_temp_k: 295.0,
surface_spfh: 0.010,
surface_pressure_pa: 101_325.0
}
describe "changeset/2" do
test "accepts a full native profile" do
changeset = HrrrNativeProfile.changeset(%HrrrNativeProfile{}, @valid_attrs)
assert changeset.valid?
assert {:ok, profile} = Repo.insert(changeset)
assert profile.level_count == 3
assert profile.heights_m == [10.0, 100.0, 500.0]
end
test "requires valid_time, lat, lon" do
changeset = HrrrNativeProfile.changeset(%HrrrNativeProfile{}, %{})
refute changeset.valid?
assert "can't be blank" in errors_on(changeset).valid_time
assert "can't be blank" in errors_on(changeset).lat
assert "can't be blank" in errors_on(changeset).lon
end
test "rejects mismatched level array lengths" do
attrs = Map.put(@valid_attrs, :temp_k, [295.0, 292.0])
changeset = HrrrNativeProfile.changeset(%HrrrNativeProfile{}, attrs)
refute changeset.valid?
assert errors_on(changeset)[:temp_k]
end
test "enforces (lat, lon, valid_time) uniqueness" do
{:ok, _} = %HrrrNativeProfile{} |> HrrrNativeProfile.changeset(@valid_attrs) |> Repo.insert()
{:error, changeset} =
%HrrrNativeProfile{} |> HrrrNativeProfile.changeset(@valid_attrs) |> Repo.insert()
refute changeset.valid?
assert "has already been taken" in errors_on(changeset).lat
end
end
describe "to_skew_t_profile/1" do
test "converts native parallel arrays into skew-T map list sorted surface-first" do
profile = struct(HrrrNativeProfile, @valid_attrs)
levels = HrrrNativeProfile.to_skew_t_profile(profile)
assert length(levels) == 3
# Ordered surface-first (descending pressure).
pressures = Enum.map(levels, & &1["pres"])
assert pressures == Enum.sort(pressures, :desc)
[surface | _] = levels
# pressure Pa → hPa
assert_in_delta surface["pres"], 1010.0, 0.01
# temperature K → °C
assert_in_delta surface["tmpc"], 295.0 - 273.15, 0.01
# geopotential height passes through
assert surface["hght"] == 10.0
# Dewpoint derived from SPFH + pressure + temperature via Magnus.
# e = q*p/(0.622 + 0.378*q)/100 = 0.010*101000/0.62578/100 ≈ 16.140 hPa
# td = 243.5*ln(e/6.112)/(17.67 ln(e/6.112)) ≈ 14.16 °C
assert_in_delta surface["dwpc"], 14.16, 0.1
end
test "returns empty list when arrays are nil or empty" do
blank = %HrrrNativeProfile{
heights_m: nil,
temp_k: nil,
spfh: nil,
pressure_pa: nil
}
assert HrrrNativeProfile.to_skew_t_profile(blank) == []
end
test "drops levels with missing pressure or temperature" do
attrs =
@valid_attrs
|> Map.put(:temp_k, [295.0, nil, 285.0])
|> Map.put(:pressure_pa, [101_000.0, 99_800.0, nil])
profile = struct(HrrrNativeProfile, attrs)
levels = HrrrNativeProfile.to_skew_t_profile(profile)
assert length(levels) == 1
assert hd(levels)["hght"] == 10.0
end
end
end