- Fix N+1 queries in radio.ex (preload :user) - Add encryption_salt to session options - Consolidate token deletion to single query - Wrap gunzip in try/rescue for corrupt files - Add Cache.match_delete/1 to encapsulate ETS access - Precompute sec_i_factor_ref as module attribute - Guard EXLA.Backend config to dev/test only - Split 3505-line contact_live_test.exs into 4 files - Replace Process.sleep with :sys.get_state synchronization - Replace Process.alive? with DOM output assertions - Clarify router.ex comment for non-live_session routes - Update findings.md to reflect fixes
94 lines
3.5 KiB
Elixir
94 lines
3.5 KiB
Elixir
defmodule Microwaveprop.Propagation.HfMuf do
|
||
@moduledoc """
|
||
HF / F2-layer MUF scoring from GIRO-measured MUFD(3000).
|
||
|
||
GIRO ionosondes publish `MUFD` — the maximum usable frequency for a
|
||
specific reference ground distance D (we request 3000 km). That is a
|
||
*measured* value incorporating the true F2-layer profile and the CCIR
|
||
M(3000)F2 climatology, not a thin-layer approximation.
|
||
|
||
This module treats the measured MUFD as the anchor and scales it to
|
||
the actual path distance using a thin-layer secant-of-incidence ratio
|
||
(h ≈ 300 km). That preserves the calibration from the measurement and
|
||
only uses the thin-layer formula for a *relative* distance correction,
|
||
which is much more accurate than recomputing MUF from scratch.
|
||
|
||
Limitations:
|
||
|
||
- Single-hop only; paths beyond ~4000 km need multi-hop logic we
|
||
don't implement here.
|
||
- Uses the nearest ionosonde's MUFD regardless of how far away it is
|
||
— for CONUS paths outside New England the F2 layer can look quite
|
||
different from Millstone Hill / Alpena. Good enough for a first
|
||
pass; a proper solution needs CCIR coefficient tables.
|
||
- Ignores D-layer absorption, so low-frequency cutoff (LUF) is not
|
||
modelled. The scoring curve is MUF-only.
|
||
- Ignores geomagnetic disturbance (Kp) effects. A storm-elevated Kp
|
||
can collapse daytime MUF without touching MUFD if the storm
|
||
post-dates the nearest observation.
|
||
"""
|
||
|
||
@f2_layer_height_km 300.0
|
||
@ref_distance_km 3000.0
|
||
@sec_i_factor_ref :math.sqrt(1 + :math.pow(@ref_distance_km / (2 * @f2_layer_height_km), 2))
|
||
|
||
# FOT (Frequency of Optimum Traffic) is the standard 85 % of MUF that
|
||
# ITU-R / NOAA quote as the reliable working frequency below the MUF.
|
||
@fot_ratio 0.85
|
||
|
||
@doc """
|
||
Scale a measured MUFD(3000) value to a different ground-range
|
||
distance using thin-layer secant obliquity. Returns a MUF in MHz.
|
||
|
||
At D = 3000 km this is a no-op (returns the input unchanged).
|
||
"""
|
||
@spec adjust_mufd(number() | nil, number() | nil) :: float()
|
||
def adjust_mufd(nil, _), do: 0.0
|
||
def adjust_mufd(_, nil), do: 0.0
|
||
def adjust_mufd(mufd, _) when mufd <= 0, do: 0.0
|
||
def adjust_mufd(_, distance_km) when distance_km <= 0, do: 0.0
|
||
|
||
def adjust_mufd(mufd_ref_mhz, distance_km) do
|
||
ratio = sec_i_factor(distance_km) / @sec_i_factor_ref
|
||
mufd_ref_mhz * ratio
|
||
end
|
||
|
||
@doc """
|
||
Frequency of Optimum Traffic — the standard 85 % of MUF used as the
|
||
reliable working frequency below the MUF ceiling.
|
||
"""
|
||
@spec fot(number() | nil) :: float()
|
||
def fot(nil), do: 0.0
|
||
def fot(muf) when muf <= 0, do: 0.0
|
||
def fot(muf_mhz), do: muf_mhz * @fot_ratio
|
||
|
||
@doc """
|
||
Score an HF band on a path with the given MUF, in the 0–100 scale
|
||
used by the rest of the propagation engine.
|
||
|
||
- band ≤ 0.75 × MUF → 100 (reliable, comfortably below FOT)
|
||
- band ≤ 0.85 × MUF (FOT) → 80 (optimal working band)
|
||
- band ≤ 1.00 × MUF → 50 (marginal, pushing the MUF ceiling)
|
||
- band ≤ 1.15 × MUF → 20 (fringe, intermittent)
|
||
- band > 1.15 × MUF → 0 (dead, well above MUF)
|
||
"""
|
||
@spec hf_score(number() | nil, number()) :: integer()
|
||
def hf_score(nil, _band), do: 0
|
||
def hf_score(muf, _band) when muf <= 0, do: 0
|
||
|
||
def hf_score(muf_mhz, band_mhz) do
|
||
ratio = band_mhz / muf_mhz
|
||
|
||
cond do
|
||
ratio <= 0.75 -> 100
|
||
ratio <= 0.85 -> 80
|
||
ratio <= 1.00 -> 50
|
||
ratio <= 1.15 -> 20
|
||
true -> 0
|
||
end
|
||
end
|
||
|
||
defp sec_i_factor(distance_km) do
|
||
:math.sqrt(1 + :math.pow(distance_km / (2 * @f2_layer_height_km), 2))
|
||
end
|
||
end
|