wgrib2 -lola ... bin writes Fortran unformatted records (4-byte length header + data + 4-byte length trailer per message). parse_lola_binary was treating the binary as tightly packed, causing every message after the first to read from the wrong offset — values came out as garbage across all grid points. Fix: account for the 8-byte record overhead per message when computing the data offset for each message's grid values. This bug affects both the existing propagation grid extraction (which may have been producing subtly wrong scores) and the new native-level extraction (which was producing obviously wrong values). The fix is a one-line stride change. Also adds Backtest.Features.native_surface_refractivity for the Phase 1 sanity check, plus a tighter wgrib2 match pattern that selects only hybrid-level messages from the native file.
96 lines
3.1 KiB
Elixir
96 lines
3.1 KiB
Elixir
defmodule Microwaveprop.Backtest.FeaturesTest do
|
|
use Microwaveprop.DataCase, async: true
|
|
|
|
alias Microwaveprop.Backtest.Features
|
|
alias Microwaveprop.Weather
|
|
alias Microwaveprop.Weather.HrrrNativeProfile
|
|
|
|
@point_lat 32.9
|
|
@point_lon -97.0
|
|
@valid_time ~U[2026-03-28 18:00:00Z]
|
|
|
|
defp create_profile(attrs) do
|
|
base = %{
|
|
valid_time: @valid_time,
|
|
lat: @point_lat,
|
|
lon: @point_lon
|
|
}
|
|
|
|
{:ok, _} = Weather.upsert_hrrr_profile(Map.merge(base, attrs))
|
|
end
|
|
|
|
describe "naive_gradient/3" do
|
|
test "returns the nearest profile's min_refractivity_gradient" do
|
|
create_profile(%{min_refractivity_gradient: -250.0})
|
|
assert Features.naive_gradient(@point_lat, @point_lon, @valid_time) == -250.0
|
|
end
|
|
|
|
test "returns nil when no profile is within match window" do
|
|
assert Features.naive_gradient(@point_lat, @point_lon, @valid_time) == nil
|
|
end
|
|
end
|
|
|
|
describe "td_depression/3" do
|
|
test "returns surface_temp_c minus surface_dewpoint_c" do
|
|
create_profile(%{surface_temp_c: 20.0, surface_dewpoint_c: 14.0})
|
|
assert Features.td_depression(@point_lat, @point_lon, @valid_time) == 6.0
|
|
end
|
|
|
|
test "returns nil when profile has no surface data" do
|
|
create_profile(%{surface_temp_c: nil, surface_dewpoint_c: nil})
|
|
assert Features.td_depression(@point_lat, @point_lon, @valid_time) == nil
|
|
end
|
|
end
|
|
|
|
describe "time_of_day/3" do
|
|
test "returns UTC hour + minute fraction and never calls the database" do
|
|
assert Features.time_of_day(0.0, 0.0, ~U[2026-01-01 06:30:00Z]) == 6.5
|
|
assert Features.time_of_day(0.0, 0.0, ~U[2026-01-01 00:00:00Z]) == 0.0
|
|
assert Features.time_of_day(0.0, 0.0, ~U[2026-01-01 23:45:00Z]) == 23.75
|
|
end
|
|
end
|
|
|
|
describe "pressure/3" do
|
|
test "returns surface_pressure_mb from the nearest profile" do
|
|
create_profile(%{surface_pressure_mb: 1013.2})
|
|
assert Features.pressure(@point_lat, @point_lon, @valid_time) == 1013.2
|
|
end
|
|
|
|
test "returns nil when no profile exists" do
|
|
assert Features.pressure(@point_lat, @point_lon, @valid_time) == nil
|
|
end
|
|
end
|
|
|
|
describe "native_surface_refractivity/3" do
|
|
test "computes N-units from native profile surface scalars" do
|
|
{:ok, _} =
|
|
%HrrrNativeProfile{}
|
|
|> HrrrNativeProfile.changeset(%{
|
|
valid_time: @valid_time,
|
|
lat: @point_lat,
|
|
lon: @point_lon,
|
|
level_count: 1,
|
|
heights_m: [10.0],
|
|
temp_k: [295.0],
|
|
spfh: [0.010],
|
|
pressure_pa: [101_325.0],
|
|
u_wind_ms: [2.0],
|
|
v_wind_ms: [1.0],
|
|
tke_m2s2: [0.5],
|
|
surface_temp_k: 295.0,
|
|
surface_spfh: 0.010,
|
|
surface_pressure_pa: 101_325.0
|
|
})
|
|
|> Repo.insert()
|
|
|
|
result = Features.native_surface_refractivity(@point_lat, @point_lon, @valid_time)
|
|
assert is_float(result)
|
|
# Typical surface N is 250-400 in temperate latitudes
|
|
assert result > 200.0 and result < 500.0
|
|
end
|
|
|
|
test "returns nil when no native profile exists" do
|
|
assert Features.native_surface_refractivity(@point_lat, @point_lon, @valid_time) == nil
|
|
end
|
|
end
|
|
end
|