prop/test/microwaveprop/propagation/region_test.exs
Graham McIntire 6aa91e7656
fix: April 2026 codebase review — address 13 bugs across propagation chain
Each fix is covered by a regression test that fails on `main` and
passes on this commit.

Round 1 (initial review):

* propagation: thread `latitude` into the conditions map so
  `score_season/4` actually picks up regional multipliers
* hrrr_client / fetcher.rs: `nearest_hrrr_hour` rounds DOWN, never at
  a future cycle that NOAA hasn't published yet
* radio: spherical-vector great-circle midpoint replaces the
  arithmetic mean — anti-meridian paths no longer fold to Greenwich
* weather: `reconcile_weather_statuses` scales the longitude band by
  `1 / cos(lat)` so the bbox stays ~150 km wide at every latitude
* radio/maidenhead: clamp 90°/180° below the field-bucket overflow so
  `from_latlon` never emits invalid characters like 'S'
* prop_grid_rs/pipeline: merge HRRR + NEXRAD-derived rain rates and
  read `best_duct_freq_ghz` into `best_duct_band_ghz` so the Native
  Duct Boost actually fires
* propagation/region (Elixir + Rust): inclusive upper bounds so points
  exactly at lat_max get the regional multiplier
* weather/sounding_params (Elixir + Rust): drop the 10 m gradient
  floor so HRRR's thin near-surface layers stop hiding sharp ducts
* weather/sounding_params: when the profile ends inside a duct,
  finalize it with the highest sample as the top instead of throwing
  it away (Rust port already correct)

Round 2 (post-fix sweep):

* radio + commercial: single canonical haversine in Radio (atan2
  form); Commercial delegates instead of carrying a second copy that
  could disagree at threshold distances
* prop_grid_rs/profiles_file: `snap_coords` matches Elixir's
  step-aware snap (`round(coord/0.125) * 0.125`, then 3-dp round) so
  Rust-keyed and Elixir-keyed profile maps land on the same cell
* weather/grib2/wgrib2: `parse_lon_val_segment` uses `Float.parse`
  uniformly — wgrib2 dropping the trailing `.0` from a longitude no
  longer crashes the whole chain step
2026-04-25 10:52:51 -05:00

65 lines
2 KiB
Elixir

defmodule Microwaveprop.Propagation.RegionTest do
use ExUnit.Case, async: true
alias Microwaveprop.Propagation.Region
describe "for_point/2" do
test "Dallas TX is in southern_plains" do
assert Region.for_point(32.8, -96.8) == :southern_plains
end
test "Houston TX is in gulf_coast" do
assert Region.for_point(29.8, -95.4) == :gulf_coast
end
test "Des Moines IA is in corn_belt" do
assert Region.for_point(41.6, -93.6) == :corn_belt
end
test "Tucson AZ is in desert_southwest" do
assert Region.for_point(32.2, -110.9) == :desert_southwest
end
test "Seattle WA is in pacific_northwest" do
assert Region.for_point(47.6, -122.3) == :pacific_northwest
end
test "New York NY is in northeast" do
assert Region.for_point(40.7, -74.0) == :northeast
end
test "Atlanta GA is in southeast" do
assert Region.for_point(33.7, -84.4) == :southeast
end
test "Denver CO falls into a region" do
region = Region.for_point(39.7, -104.9)
assert is_atom(region)
end
test "returns :other for points outside CONUS" do
assert Region.for_point(19.0, -155.0) == :other
end
test "includes points sitting exactly on the upper latitude boundary" do
# corn_belt is defined as 38.0..48.0 lat; pacific_northwest as
# 42.0..50.0. The exclusive upper bound dropped points exactly at
# 48.0 / 50.0 into `:other` and stripped their seasonal multiplier.
assert Region.for_point(48.0, -93.0) != :other
assert Region.for_point(50.0, -122.0) != :other
end
end
describe "seasonal_adjustment/2" do
test "returns a multiplier for known regions and months" do
adj = Region.seasonal_adjustment(:gulf_coast, 8)
assert is_float(adj)
# Gulf coast August should be drier → higher seasonal score than default
assert adj >= 0.8 and adj <= 1.3
end
test "returns 1.0 for unknown regions" do
assert Region.seasonal_adjustment(:other, 6) == 1.0
end
end
end