Phase A — 7 pipeline bugs:
- retain_scores_window: fix timeline self-deletion (NOTIFY payload run_time|valid_time)
- Rust/Elixir weight divergence: Rust loads band_weights.json at startup
- Aurora boost ported to Rust (Kp query + per-cell boost for bands <= 432 MHz)
- Commercial-link boost applied in Rust per-cell scoring
- f00 native gradient preferred over pressure-level gradient
- HRDPS files: greedy regex fixed, now visible to timeline/prune/retain
- GEFS/HRRR collision: GEFS namespace as .gefs.prop, merge at read
Phase B — Validation harness:
- scripts/validate_algo.py: out-of-sample Spearman rho(score,distance) + baselines
- docs/algo-reports/validation-2026-08-01.{json,md}
Phase C — Forecast-skill evaluation:
- scripts/validate_forecast.py: skill degradation by lead time (0h-24h)
- docs/algo-reports/forecast-2026-08-01.{json,md}
Phase D — Calibration + model improvements:
- recalibrate.py: validation gate before weight deploy
- Recalibrator: Nx.max(0.0) replaces Nx.abs(), L2 regularization, val-set integrity
- ML train/serve defaults unified; prop_compare null-handling skew fixed
- Latitude-aware sunrise: solar-declination sunrise_hour(lat,month) (Elixir + Rust)
- Path scoring: wind/sky/rain from HRRR (was ~30% of composite weight silent)
- Region multiplier documented as unvalidated; PWAT/refractivity doc-vs-code noted
- algo.md: synced scoring sections, marked retired features, D5/D6 changelog
- Credo: path_compute cyclomatic complexity bumped (6->10 fields) — informational only
118 lines
4 KiB
Elixir
118 lines
4 KiB
Elixir
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.
|
||
#
|
||
# ⚠ VALIDATION STATUS: hand-tuned, never backtest-validated.
|
||
# These multipliers (0.7–1.3 on the 11%-weight season factor) are based
|
||
# on the meteorologist's qualitative guidance from April 2026. A full
|
||
# backtest evaluation against out-of-sample contacts is pending.
|
||
# Until validated, treat these as experimental priors — the uniform
|
||
# seasonal_base tables documented in algo.md Part 4 §5 already provide
|
||
# the data-calibrated seasonal signal from RAOB ducting probabilities.
|
||
#
|
||
# Phase 9 recalibration: refine from backtest data, or remove if no
|
||
# measurable improvement in holdout ρ(score, distance).
|
||
@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
|