defmodule Microwaveprop.Propagation.Region do @moduledoc """ Classifies CONUS grid points into climatological regions for band-specific seasonal scoring adjustments. The meteorologist's April 2026 review noted that the existing uniform seasonal scoring is wrong: Gulf coast August is drier and better for propagation than June/July, while Iowa August is peak corn evapotranspiration with brutally high dewpoints. Same month, opposite effect. This module provides: - `for_point/2` — classifies a lat/lon into one of ~8 regions - `seasonal_adjustment/2` — returns a multiplier (0.7-1.3) that the scorer applies on top of the band's `seasonal_base` score. Region boundaries are deliberately simple bounding boxes rather than precise climate zone shapefiles. The scoring impact of getting a boundary pixel wrong is a few points out of 100; the impact of having no regional adjustment at all is the entire seasonal factor being wrong for half the country. """ @regions [ {:gulf_coast, {25.0, 32.0}, {-100.0, -80.0}}, {:southeast, {30.0, 37.0}, {-90.0, -75.0}}, {:southern_plains, {32.0, 38.0}, {-105.0, -93.0}}, {:corn_belt, {38.0, 48.0}, {-100.0, -82.0}}, {:northeast, {38.0, 48.0}, {-82.0, -67.0}}, {:desert_southwest, {30.0, 38.0}, {-120.0, -105.0}}, {:pacific_northwest, {42.0, 50.0}, {-125.0, -115.0}}, {:mountain_west, {38.0, 48.0}, {-115.0, -100.0}} ] @doc """ Classify a lat/lon point into a climatological region. Returns an atom like `:gulf_coast`, `:corn_belt`, etc. Returns `:other` for points outside any defined region (including non-CONUS). """ @spec for_point(float, float) :: atom def for_point(lat, lon) do Enum.find_value(@regions, :other, fn {name, {lat_min, lat_max}, {lon_min, lon_max}} -> if lat >= lat_min and lat <= lat_max and lon >= lon_min and lon <= lon_max do name end end) end # Regional seasonal adjustments: multipliers on the band's seasonal_base # score by month. 1.0 = no change; > 1.0 = better than the base suggests; # < 1.0 = worse. Only months/regions where the uniform base is known to # be wrong get non-1.0 values. # # These are hand-tuned starting points based on the meteorologist's # qualitative guidance. Phase 9 recalibration will refine them from # backtest data. @seasonal_adjustments %{ gulf_coast: %{ # Gulf August is drier than June/July → better propagation 6 => 0.95, 7 => 0.95, 8 => 1.15, 9 => 1.10 }, corn_belt: %{ # Iowa July/August: corn evapotranspiration → high dewpoints → worse 6 => 1.05, 7 => 0.85, 8 => 0.80, 9 => 0.95 }, southeast: %{ # Similar to Gulf but less extreme 7 => 0.97, 8 => 1.08 }, southern_plains: %{ # TX panhandle drier than Gulf but wetter than desert 8 => 1.05 }, desert_southwest: %{ # Monsoon July/Aug brings moisture → briefly good ducting 7 => 1.10, 8 => 1.10, 9 => 1.05 }, pacific_northwest: %{ # Marine layer summer inversions 6 => 1.15, 7 => 1.20, 8 => 1.20, 9 => 1.10 } } @doc """ Seasonal score multiplier for a given region and month (1-12). Returns a float in [0.7, 1.3]. The scorer multiplies the band's `seasonal_base[month]` by this value. Unknown regions or months without an adjustment return 1.0. """ @spec seasonal_adjustment(atom, integer) :: float def seasonal_adjustment(region, month) do @seasonal_adjustments |> Map.get(region, %{}) |> Map.get(month, 1.0) end end