Expands submittable-contact bands to include 6m, 2m, 1.25m, and 70cm (as 432 rather than the old 440 placeholder). Each new band gets an explicit allocation window in BandResolver.nearest_band so ADIF FREQ fields near the amateur allocations resolve correctly while 60-900 MHz frequencies outside those windows are still rejected. Microwave (>= 900 MHz) snapping is unchanged — nearest-band match across the full @allowed_bands list. Also adds BandConfig entries for 50 and 222 (tropo-only config, same pattern as 144/432). Sporadic-E / F2 / meteor scatter modeling is not yet in scope — ionosphere data is only used to compute an Es readout on /path for 50/144/222/432.
84 lines
3.1 KiB
Elixir
84 lines
3.1 KiB
Elixir
defmodule Microwaveprop.Propagation.SporadicE do
|
||
@moduledoc """
|
||
Sporadic-E propagation physics for VHF scoring.
|
||
|
||
Given foEs (the E-layer critical frequency measured by an ionosonde,
|
||
in MHz) and a ground-range distance in km, computes the single-hop
|
||
Es MUF via the thin-layer obliquity factor from ITU-R P.534-6 /
|
||
Davies 1990 *Ionospheric Radio*, Eq. 6.26:
|
||
|
||
MUF_Es = foEs · sec(i)
|
||
|
||
where `sec(i) = √(1 + (D / (2h))²)` with the reflection height
|
||
`h = 110 km` (standard thin-E approximation).
|
||
|
||
The `es_score/3` function maps a `(foEs, target band, distance)`
|
||
triple into a 0–100 propagation likelihood for **single-hop Es
|
||
only**. Multi-hop Es (which extends the range but needs consecutive
|
||
intense Es patches) and Es-scatter are not modelled here — those
|
||
are separate factors.
|
||
|
||
Calibration notes:
|
||
- 50 MHz Es at 2000 km needs foEs ≳ 5.5 MHz (routine summer Es).
|
||
- 144 MHz Es at 2000 km needs foEs ≳ 15.8 MHz (rare, observed during
|
||
major Jun/Jul peaks).
|
||
- 222/432 MHz Es does not occur at physically observed foEs values.
|
||
- Single-hop Es has a minimum useful range of ~500 km (steeper rays
|
||
escape the layer rather than reflect) and a maximum of ~2500 km
|
||
(the geometric horizon limit for a 110 km reflection height).
|
||
"""
|
||
|
||
# Standard E-layer reflection height for the thin-layer approximation.
|
||
@e_layer_height_km 110.0
|
||
|
||
@min_hop_km 500
|
||
@max_hop_km 2500
|
||
|
||
@doc """
|
||
Single-hop Es MUF in MHz. Returns 0.0 for nil/non-positive inputs.
|
||
"""
|
||
@spec single_hop_muf(number() | nil, number() | nil) :: float()
|
||
def single_hop_muf(nil, _), do: 0.0
|
||
def single_hop_muf(_, nil), do: 0.0
|
||
def single_hop_muf(fo_es_mhz, _) when fo_es_mhz <= 0, do: 0.0
|
||
def single_hop_muf(_, distance_km) when distance_km <= 0, do: 0.0
|
||
|
||
def single_hop_muf(fo_es_mhz, distance_km) do
|
||
sec_i = :math.sqrt(1 + :math.pow(distance_km / (2 * @e_layer_height_km), 2))
|
||
fo_es_mhz * sec_i
|
||
end
|
||
|
||
@doc """
|
||
Returns a 0–100 score for single-hop Es propagation of `band_mhz`
|
||
over a ground-range `distance_km`, given the measured `fo_es_mhz`.
|
||
|
||
- Short paths (< 500 km) and long paths (> 2500 km) are outside the
|
||
single-hop window → 0.
|
||
- `nil` or 0 foEs → 0.
|
||
- Otherwise the score ramps with `MUF_Es / band_mhz`:
|
||
≥ 1.20 → 100 (comfortably above the critical threshold)
|
||
≥ 1.00 → 80 (just above, marginal Es)
|
||
≥ 0.90 → 50 (close, occasional fringe)
|
||
≥ 0.70 → 20 (weak fringe, unreliable)
|
||
below → 0
|
||
"""
|
||
@spec es_score(number() | nil, number(), number() | nil) :: integer()
|
||
def es_score(nil, _band, _distance), do: 0
|
||
def es_score(_fo_es, _band, nil), do: 0
|
||
def es_score(fo_es_mhz, _band, _distance) when fo_es_mhz <= 0, do: 0
|
||
|
||
def es_score(_fo_es, _band, distance_km) when distance_km < @min_hop_km or distance_km > @max_hop_km, do: 0
|
||
|
||
def es_score(fo_es_mhz, band_mhz, distance_km) do
|
||
muf = single_hop_muf(fo_es_mhz, distance_km)
|
||
ratio = muf / band_mhz
|
||
|
||
cond do
|
||
ratio >= 1.20 -> 100
|
||
ratio >= 1.00 -> 80
|
||
ratio >= 0.90 -> 50
|
||
ratio >= 0.70 -> 20
|
||
true -> 0
|
||
end
|
||
end
|
||
end
|