Recalibrate refractivity thresholds for HRRR gradient distribution

Previous thresholds (-500 to -60) were calibrated for radiosonde data.
HRRR profiles have coarser vertical resolution, with gradients clustering
between -40 and -130 N/km (median -70). Nearly all grid points were
falling through to the default score of 42, wasting the refractivity
factor. New thresholds (-200 to -40) spread across HRRR percentiles.
This commit is contained in:
Graham McIntire 2026-03-31 16:08:15 -05:00
parent 49cbe6789c
commit c9112b9280
No known key found for this signature in database
GPG key ID: F4ABF488E6029E59
4 changed files with 31 additions and 54 deletions

51
algo.md
View file

@ -685,48 +685,21 @@ end
### 9. Refractivity Score — When Sounding/HRRR Data Available
Best predictor when available. Binary duct detection is useless (54% baseline rate). Use continuous gradient magnitude and BL depth instead. Surface-based ducts (base=0) matter more than elevated ducts.
Best predictor when available. Binary duct detection is useless (54% baseline rate). Use continuous gradient magnitude and BL depth instead.
From sounding data: avg gradient -389 N/km for ducting events vs -123 for non-ducting. From HRRR: BL depth < 200m correlates with avg gradient of -93.7 (approaching super-refraction).
Thresholds calibrated for HRRR-derived gradients which are coarser than radiosonde soundings. HRRR gradient distribution: p1=-230, p5=-162, p10=-130, p25=-94, p50=-70, p75=-53, p95=-40 N/km. Previous thresholds (-500 to -60) placed nearly all HRRR profiles in the default bucket.
```elixir
def score_refractivity(sounding_or_hrrr, band_config) do
cond do
sounding_or_hrrr == nil -> 50 # No data — neutral
| Gradient (N/km) | Beneficial Score | Harmful Score | Condition |
|---|---|---|---|
| < -200 | 98 | 85 | Strong ducting (HRRR p1) |
| < -150 | 92 | 80 | Enhanced refraction (HRRR p5) |
| < -100 | 82 | 72 | Above-average refraction (HRRR p25) |
| < -75 | 68 | 62 | Near-median gradient (HRRR p50) |
| < -55 | 55 | 55 | Below-median (HRRR p75) |
| < -40 | 48 | 48 | Weak gradient (HRRR p95) |
| ≥ -40 | 42 | 42 | Standard/sub-refractive |
sounding_or_hrrr.min_dn_dh < -500 ->
# Strong ducting — gradient 3x non-ducting average
case band_config.humidity_effect do
:beneficial -> 98 # 10 GHz benefits most from ducting
:harmful -> 85 # Higher bands benefit but absorption limits range
end
sounding_or_hrrr.min_dn_dh < -300 ->
# Moderate ducting — near ducting average of -389
case band_config.humidity_effect do
:beneficial -> 92
:harmful -> 78
end
sounding_or_hrrr.min_dn_dh < -200 ->
80 # Enhanced refraction — above non-ducting average of -123
# BL depth signal: shallow BL at 12Z = strong surface duct potential
sounding_or_hrrr.bl_depth_m != nil and
sounding_or_hrrr.bl_depth_m < 300 ->
82 # Very shallow BL — strong inversion cap (12Z signature)
sounding_or_hrrr.min_dn_dh < -100 ->
65 # Mild enhancement
sounding_or_hrrr.min_dn_dh < -60 ->
55 # Slightly enhanced — within the broad "Enhanced" HRRR regime
true ->
42 # Standard/sub-refractive — HRRR shows only 5% of profiles here
end
end
```
Shallow BL fallback: when gradient is unavailable but BL depth < 300m, score 82 (strong inversion cap).
---

View file

@ -32,12 +32,15 @@ defmodule Microwaveprop.Propagation.BandConfig do
@humidity_beneficial_thresholds [{4, 55}, {7, 70}, {10, 82}, {14, 90}, {18, 95}, {22, 88}]
@humidity_beneficial_default 75
# Thresholds calibrated for HRRR-derived gradients (coarser than sounding data).
# HRRR percentiles: p1=-230, p5=-162, p10=-130, p25=-94, p50=-70, p75=-53, p95=-40
@refractivity_thresholds [
{-500, 98, 85},
{-300, 92, 78},
{-200, 80, 80},
{-100, 65, 65},
{-60, 55, 55}
{-200, 98, 85},
{-150, 92, 80},
{-100, 82, 72},
{-75, 68, 62},
{-55, 55, 55},
{-40, 48, 48}
]
@refractivity_default {42, 42}
@shallow_bl_threshold_m 300

View file

@ -252,11 +252,12 @@ defmodule Microwaveprop.Propagation.BandConfigTest do
thresholds = BandConfig.refractivity_thresholds()
assert thresholds == [
{-500, 98, 85},
{-300, 92, 78},
{-200, 80, 80},
{-100, 65, 65},
{-60, 55, 55}
{-200, 98, 85},
{-150, 92, 80},
{-100, 82, 72},
{-75, 68, 62},
{-55, 55, 55},
{-40, 48, 48}
]
end
end

View file

@ -227,14 +227,14 @@ defmodule Microwaveprop.Propagation.ScorerTest do
assert Scorer.score_refractivity(-600, 500, @band_10g) == 98
end
test "strong ducting gradient returns 85 for harmful" do
# gradient < -500 -> 85 for harmful
assert Scorer.score_refractivity(-600, 500, @band_24g) == 85
test "strong gradient returns 85 for harmful" do
# gradient < -200 -> 85 for harmful (HRRR-calibrated)
assert Scorer.score_refractivity(-250, 500, @band_24g) == 85
end
test "moderate gradient returns appropriate score" do
# gradient < -300 but >= -500 -> 92 for beneficial
assert Scorer.score_refractivity(-400, 500, @band_10g) == 92
# gradient < -150 but >= -200 -> 92 for beneficial (HRRR-calibrated)
assert Scorer.score_refractivity(-160, 500, @band_10g) == 92
end
test "nil gradient returns 50" do