FrontalAnalysis module (Weather.FrontalAnalysis): - detect_fronts/3 computes the Thermal Front Parameter (TFP) from 2D grids of surface temperature and pressure using Nx vectorized ops. TFP = -nabla|nabla(theta)| . nabla(theta)/|nabla(theta)|. Most negative values mark cold fronts. - central_gradient/1 for 2D finite differences with edge handling - nearest_front/3 finds closest front point with distance and bearing - path_front_angle/2 computes angle between a QSO path and the front (0 = parallel = good, 90 = crosses = dead) Backtest feature stubs for distance_to_front and parallel_to_front (return nil until the pipeline caches per-cell frontal features from the hourly HRRR grid run). The FrontalAnalysis module itself is tested and ready for integration. NEXRAD spike docs also included in this commit.
72 lines
2.2 KiB
Elixir
72 lines
2.2 KiB
Elixir
defmodule Microwaveprop.Weather.FrontalAnalysisTest do
|
|
use ExUnit.Case, async: true
|
|
|
|
alias Microwaveprop.Weather.FrontalAnalysis
|
|
|
|
@grid_spec %{lat_start: 30.0, lat_step: 0.5, lon_start: -100.0, lon_step: 0.5}
|
|
|
|
describe "detect_fronts/3" do
|
|
test "finds no fronts in a uniform temperature field" do
|
|
# 10x10 grid, all same temperature and pressure
|
|
temp = Nx.broadcast(295.0, {10, 10})
|
|
pres = Nx.broadcast(101_325.0, {10, 10})
|
|
|
|
result = FrontalAnalysis.detect_fronts(temp, pres, @grid_spec)
|
|
|
|
assert result.front_points == []
|
|
end
|
|
|
|
test "detects a front at a sharp east-west temperature boundary" do
|
|
# North half cold (280K), south half warm (300K) → sharp gradient mid-grid
|
|
cold = Nx.broadcast(280.0, {5, 10})
|
|
warm = Nx.broadcast(300.0, {5, 10})
|
|
temp = Nx.concatenate([cold, warm], axis: 0)
|
|
pres = Nx.broadcast(101_325.0, {10, 10})
|
|
|
|
result = FrontalAnalysis.detect_fronts(temp, pres, @grid_spec)
|
|
|
|
# Should find front points near the boundary (rows 4-5)
|
|
assert length(result.front_points) > 0
|
|
|
|
# All front points should be near the middle rows
|
|
Enum.each(result.front_points, fn {lat, _lon, _bearing} ->
|
|
assert lat >= 31.5 and lat <= 34.0
|
|
end)
|
|
end
|
|
end
|
|
|
|
describe "nearest_front/3" do
|
|
test "returns the closest front point with distance and bearing" do
|
|
front_points = [
|
|
{33.0, -97.0, 90.0},
|
|
{35.0, -97.0, 45.0}
|
|
]
|
|
|
|
result = FrontalAnalysis.nearest_front(front_points, 33.1, -97.1)
|
|
assert result.distance_km < 20.0
|
|
assert result.front_bearing_deg == 90.0
|
|
end
|
|
|
|
test "returns nil for empty front_points" do
|
|
assert FrontalAnalysis.nearest_front([], 33.0, -97.0) == nil
|
|
end
|
|
end
|
|
|
|
describe "path_front_angle/2" do
|
|
test "parallel paths return 0" do
|
|
assert FrontalAnalysis.path_front_angle(90.0, 90.0) == 0.0
|
|
end
|
|
|
|
test "perpendicular paths return 90" do
|
|
assert FrontalAnalysis.path_front_angle(0.0, 90.0) == 90.0
|
|
end
|
|
|
|
test "anti-parallel paths return 0" do
|
|
assert FrontalAnalysis.path_front_angle(180.0, 0.0) == 0.0
|
|
end
|
|
|
|
test "45-degree offset returns 45" do
|
|
assert_in_delta FrontalAnalysis.path_front_angle(45.0, 90.0), 45.0, 0.01
|
|
end
|
|
end
|
|
end
|