- Spike docs at docs/research/hrrr_native_levels.md confirming files
are on AWS S3 for 5+ years, 50 hybrid levels, and include TKE and
SPFH needed for Phase 2 turbulence features. Architectural finding:
per-point on-demand fetching is impractical (~530 MB/file), so
the ingestion worker batches per (date, hour) instead.
- hrrr_native_profiles schema: arrays per level plus cached surface
scalars and placeholder columns for Phase 2/4 derived fields.
Strictly additive — the existing hrrr_profiles table is untouched.
- HrrrNativeClient: pure URL/message-list helpers, build_native_profile/1
that turns a parsed wgrib2 map into the schema shape (TDD'd).
- Exposed HrrrClient.download_grib_ranges/2 so the native client
reuses the existing parallel byte-range download + disk cache.
- HrrrNativeGridWorker: Oban worker keyed on {year, month, day, hour},
unique at :infinity, pulls distinct (lat, lon) points from contacts
in the ±30 min window, downloads the native grib2, extracts per
point, bulk-upserts.
- mix hrrr_native_backfill --limit N enqueues the top-N hours by
contact count.
Phase 1 gate still pending Task 1.6 (sanity-check backtest after
live data lands).
59 lines
1.9 KiB
Elixir
59 lines
1.9 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] != nil
|
|
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
|
|
end
|