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.
111 lines
3.6 KiB
Elixir
111 lines
3.6 KiB
Elixir
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
|