Update analysis with solar time, refine algo.md time-of-day section
Solar time (longitude/15) replaces fixed CDT/CST offset for time-of-day scoring. Correlation analysis shows dramatic improvement at higher frequencies: 24 GHz rho jumps from 0.056 (UTC) to 0.188 (solar), and 75 GHz corrects from spurious -0.39 to physically correct +0.24.
This commit is contained in:
parent
c12f8cf5ed
commit
5c9b43d619
3 changed files with 51 additions and 11 deletions
27
algo.md
27
algo.md
|
|
@ -404,7 +404,7 @@ Commercial link data shows 1-5 dB daily variation even on perfectly clear, stabl
|
|||
|
||||
On short LOS paths (3-7 km), sub-refractive conditions (dN/dh > -40/km) produce the best signal — minimal multipath, clean beam coupling. On long beyond-LOS paths (50-500+ km), enhanced refraction/ducting is essential. The algorithm must handle both regimes.
|
||||
|
||||
### Finding 8: Time-of-Day Effect Scales with Frequency
|
||||
### Finding 8: Time-of-Day Effect Scales with Frequency (Solar Time)
|
||||
|
||||
Night/dawn (22Z-10Z) enhancement vs afternoon baseline, from QSO distance data:
|
||||
|
||||
|
|
@ -417,6 +417,8 @@ Night/dawn (22Z-10Z) enhancement vs afternoon baseline, from QSO distance data:
|
|||
|
||||
At 10 GHz the effect is modest. At 47+ GHz it is the **dominant variable**, more important than most weather parameters. The 75 GHz result is from only 20 night/dawn QSOs but the 4.6x multiplier is consistent with strong ducting being the *only* path at that frequency.
|
||||
|
||||
**Update (April 2026):** Switching from fixed CDT/CST timezone to longitude-based **solar time** (`longitude / 15`) dramatically improves the time-of-day correlation at higher frequencies. Spearman correlation with distance: UTC hour rho=0.056 vs solar hour rho=0.188 at 24 GHz (3.4x improvement). At 75 GHz the UTC correlation was confounded by geographic longitude — solar time corrects this from rho=-0.39 to rho=+0.24.
|
||||
|
||||
### Finding 9: Ducting Peaks June-July, Not August
|
||||
|
||||
Monthly ducting probability from 3,901 soundings:
|
||||
|
|
@ -772,16 +774,27 @@ def score_humidity(abs_humidity_gm3, band_config) do
|
|||
end
|
||||
```
|
||||
|
||||
### 2. Time of Day Score — Inversion Lifecycle
|
||||
### 2. Time of Day Score — Solar Time, Inversion Lifecycle
|
||||
|
||||
The strongest diurnal predictor in the data. Dawn shows the highest P90 distances; late evening shows elevated averages.
|
||||
Uses longitude-based **solar time** (`longitude / 15` offset) instead of a fixed timezone offset. This produces physically correct local time at every grid point across CONUS and dramatically improves correlation with QSO distance:
|
||||
|
||||
| Band | UTC Hour rho | Solar Hour rho | Improvement |
|
||||
|------|-------------|---------------|-------------|
|
||||
| 10 GHz | 0.007 | 0.016 | 2.4x (still weak — 10G ducts form at all times) |
|
||||
| 24 GHz | 0.056 | **0.188** | 3.4x (now #5 predictor) |
|
||||
| 47 GHz | -0.024 | **0.152** | Sign corrected (UTC was confounded by longitude) |
|
||||
| 75 GHz | -0.392 | **0.239** | Sign corrected (western US at lower UTC ≠ better propagation) |
|
||||
|
||||
The UTC hour correlation at 75 GHz was spuriously negative because western US stations (lower UTC hours) happened to have longer paths — a geographic confound, not physics. Solar time corrects this.
|
||||
|
||||
At 24 GHz, the solar hour bins show a clear physical pattern: 03-05 solar (pre-dawn) has worst distances (57 km median), evening/night (18-23 solar) has best (107-140 km median) — consistent with nocturnal inversion formation.
|
||||
|
||||
```elixir
|
||||
@sunrise_table [7.4, 7.3, 7.0, 6.7, 6.35, 6.25,
|
||||
6.35, 6.65, 6.9, 7.1, 7.35, 7.45]
|
||||
|
||||
def score_time_of_day(utc_hour, utc_minute, month) do
|
||||
offset = if month >= 3 and month <= 10, do: -5, else: -6 # CDT/CST
|
||||
def score_time_of_day(utc_hour, utc_minute, month, longitude) do
|
||||
offset = longitude / 15 # solar time offset from longitude
|
||||
local = rem(utc_hour + utc_minute / 60 + offset + 24, 24)
|
||||
sunrise = Enum.at(@sunrise_table, month - 1)
|
||||
d = local - sunrise # hours relative to sunrise
|
||||
|
|
@ -1134,7 +1147,7 @@ def compute_score(conditions, band_config, path_type \\ :beyond_los) do
|
|||
humidity: score_humidity(conditions.abs_humidity, band_config),
|
||||
wind: score_wind(conditions.wind_speed_kts),
|
||||
sky: score_sky(conditions.sky_condition),
|
||||
time_of_day: score_time_of_day(conditions.utc_hour, conditions.utc_minute, conditions.month) |> elem(0),
|
||||
time_of_day: score_time_of_day(conditions.utc_hour, conditions.utc_minute, conditions.month, conditions.longitude) |> elem(0),
|
||||
td_depression: score_td_depression(conditions.temp_f, conditions.dewpoint_f, band_config),
|
||||
season: score_season(conditions.month, band_config),
|
||||
pressure: score_pressure(conditions.slp, conditions.prev_slp),
|
||||
|
|
@ -1339,7 +1352,7 @@ def predict_scores(current_obs, obs_3hr_ago, forecast, band_config) do
|
|||
humidity: score_humidity(abs_hum, band_config),
|
||||
wind: score_wind(projected_wind),
|
||||
sky: score_sky(projected_sky),
|
||||
time_of_day: score_time_of_day(future_time.hour, future_time.minute, month) |> elem(0),
|
||||
time_of_day: score_time_of_day(future_time.hour, future_time.minute, month, longitude) |> elem(0),
|
||||
td_depression: score_td_depression(projected_temp, projected_dp, band_config),
|
||||
season: score_season(month, band_config),
|
||||
pressure: score_pressure(projected_slp, current_obs.slp),
|
||||
|
|
|
|||
|
|
@ -217,8 +217,8 @@ defmodule Microwaveprop.Propagation.Model do
|
|||
:min_refractivity_gradient,
|
||||
:hpbl_m,
|
||||
:pwat_mm,
|
||||
:utc_hour_sin,
|
||||
:utc_hour_cos,
|
||||
:solar_hour_sin,
|
||||
:solar_hour_cos,
|
||||
:month_sin,
|
||||
:month_cos,
|
||||
:log_freq_mhz
|
||||
|
|
|
|||
|
|
@ -89,7 +89,10 @@ defmodule Mix.Tasks.PropagationAnalyze do
|
|||
h2.ducting_detected AS h2_ducting,
|
||||
|
||||
-- Terrain
|
||||
tp.verdict AS terrain_verdict
|
||||
tp.verdict AS terrain_verdict,
|
||||
|
||||
-- Longitude for solar time
|
||||
((q.pos1->>'lng')::float + (q.pos2->>'lng')::float) / 2.0 AS avg_longitude
|
||||
|
||||
FROM qsos q
|
||||
|
||||
|
|
@ -132,6 +135,18 @@ defmodule Mix.Tasks.PropagationAnalyze do
|
|||
|> Map.put(:avg_hpbl, safe_avg(row[:h1_hpbl], row[:h2_hpbl]))
|
||||
|> Map.put(:avg_pwat, safe_avg(row[:h1_pwat], row[:h2_pwat]))
|
||||
|> Map.put(:either_ducting, row[:h1_ducting] == true or row[:h2_ducting] == true)
|
||||
|> derive_solar_hour()
|
||||
end
|
||||
|
||||
defp derive_solar_hour(row) do
|
||||
case {row[:utc_hour], row[:avg_longitude]} do
|
||||
{h, lng} when is_number(h) and is_number(lng) ->
|
||||
solar = :math.fmod(h + lng / 15 + 24, 24)
|
||||
Map.put(row, :solar_hour, solar)
|
||||
|
||||
_ ->
|
||||
Map.put(row, :solar_hour, nil)
|
||||
end
|
||||
end
|
||||
|
||||
defp safe_avg(nil, nil), do: nil
|
||||
|
|
@ -196,7 +211,8 @@ defmodule Mix.Tasks.PropagationAnalyze do
|
|||
{:avg_hpbl, "HPBL (m)"},
|
||||
{:avg_pwat, "PWAT (mm)"},
|
||||
{:month, "Month"},
|
||||
{:utc_hour, "UTC Hour"}
|
||||
{:utc_hour, "UTC Hour"},
|
||||
{:solar_hour, "Solar Hour"}
|
||||
]
|
||||
|
||||
for band <- @bands do
|
||||
|
|
@ -296,6 +312,17 @@ defmodule Mix.Tasks.PropagationAnalyze do
|
|||
{18, 21, "18-20 UTC"},
|
||||
{21, 24, "21-23 UTC"}
|
||||
]},
|
||||
{:solar_hour, "Solar Hour",
|
||||
[
|
||||
{0, 3, "00-02 Solar (night)"},
|
||||
{3, 6, "03-05 Solar (pre-dawn)"},
|
||||
{6, 9, "06-08 Solar (dawn)"},
|
||||
{9, 12, "09-11 Solar (morning)"},
|
||||
{12, 15, "12-14 Solar (midday)"},
|
||||
{15, 18, "15-17 Solar (afternoon)"},
|
||||
{18, 21, "18-20 Solar (evening)"},
|
||||
{21, 24, "21-23 Solar (night)"}
|
||||
]},
|
||||
{:month, "Month",
|
||||
[
|
||||
{1, 2, "Jan"},
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue