prop/lib/microwaveprop/propagation/sporadic_e.ex
Graham McIntire 36adf875b7
Add 50/144/222 MHz bands, rename 440→432
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.
2026-04-16 12:36:10 -05:00

84 lines
3.1 KiB
Elixir
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 0100 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 0100 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