Registered users can suggest edits to any contact's core fields (callsigns, grids, band, mode, timestamp). Edits enter an admin approval queue with field-by-field diff view. On approve, changes are applied and enrichment re-enqueued if grids/band changed. Users receive email notification on approve or reject. Also updates dependabot.yml for mix ecosystem.
145 lines
4.8 KiB
Elixir
145 lines
4.8 KiB
Elixir
defmodule Microwaveprop.Propagation.RainScatter do
|
||
@moduledoc """
|
||
Estimates rain scatter potential from NEXRAD composite reflectivity.
|
||
|
||
Rain scatter enables microwave contacts at 100-300+ km by scattering
|
||
signals off precipitation cells. Signal strength depends on reflectivity
|
||
(rain intensity), frequency, and geometry (distance from each station
|
||
to the rain cell).
|
||
|
||
Uses a simplified bistatic radar equation:
|
||
scatter_db ≈ 10*log10(Z) + 10*log10(V) - 20*log10(R1) - 20*log10(R2)
|
||
+ frequency_gain - path_losses
|
||
|
||
where Z is reflectivity factor (from dBZ), V is effective scattering
|
||
volume, and R1/R2 are distances from each endpoint to the cell.
|
||
"""
|
||
|
||
@earth_radius_km 6371.0
|
||
|
||
# Minimum reflectivity to consider for scatter (dBZ)
|
||
@min_dbz 25.0
|
||
|
||
# Maximum scatter range from either endpoint (km)
|
||
@max_range_km 300.0
|
||
|
||
@doc """
|
||
Find rain cells with scatter potential for a given point and band.
|
||
|
||
Takes a list of `{lat, lon, dbz}` rain cells (from NEXRAD extraction),
|
||
the observer's position, and frequency in GHz.
|
||
|
||
Returns a list of scatter cell maps sorted by potential (strongest first),
|
||
each with: lat, lon, dbz, distance_km, scatter_db (relative signal estimate),
|
||
and bearing from the observer.
|
||
"""
|
||
def find_scatter_cells(rain_cells, obs_lat, obs_lon, freq_ghz) do
|
||
rain_cells
|
||
|> Enum.filter(fn {_lat, _lon, dbz} -> dbz >= @min_dbz end)
|
||
|> Enum.map(fn {lat, lon, dbz} ->
|
||
dist_km = haversine_km(obs_lat, obs_lon, lat, lon)
|
||
bearing = bearing_deg(obs_lat, obs_lon, lat, lon)
|
||
|
||
# Scatter signal estimate (relative dB)
|
||
# Higher reflectivity = more scattering targets
|
||
# Closer cells = stronger signal (inverse square from both endpoints)
|
||
# Higher frequency = stronger Rayleigh scattering (up to ~10 GHz, then Mie)
|
||
scatter_db = estimate_scatter_db(dbz, dist_km, freq_ghz)
|
||
|
||
%{
|
||
lat: Float.round(lat, 3),
|
||
lon: Float.round(lon, 3),
|
||
dbz: Float.round(dbz, 1),
|
||
distance_km: Float.round(dist_km, 1),
|
||
bearing: Float.round(bearing, 0),
|
||
scatter_db: Float.round(scatter_db, 1)
|
||
}
|
||
end)
|
||
|> Enum.filter(&(&1.distance_km <= @max_range_km and &1.distance_km >= 10))
|
||
|> Enum.sort_by(&(-&1.scatter_db))
|
||
|> Enum.take(20)
|
||
end
|
||
|
||
@doc """
|
||
Classify overall scatter potential for a point.
|
||
|
||
Returns :excellent, :good, :marginal, or :none based on
|
||
the best available scatter cell.
|
||
"""
|
||
def classify(scatter_cells) do
|
||
case scatter_cells do
|
||
[] ->
|
||
:none
|
||
|
||
[best | _] ->
|
||
cond do
|
||
best.scatter_db >= -10 -> :excellent
|
||
best.scatter_db >= -20 -> :good
|
||
best.scatter_db >= -30 -> :marginal
|
||
true -> :none
|
||
end
|
||
end
|
||
end
|
||
|
||
# Simplified scatter signal estimate in relative dB.
|
||
#
|
||
# Based on bistatic radar equation for volume scattering:
|
||
# P_rx ∝ Z * σ_scatter * V / (R^4)
|
||
#
|
||
# For a single cell at distance R from the observer (assuming the
|
||
# other station is also near R for a rough estimate):
|
||
# scatter_db ≈ dBZ + freq_factor - 40*log10(R_km) + volume_term
|
||
defp estimate_scatter_db(dbz, dist_km, freq_ghz) do
|
||
# Reflectivity contribution (dBZ is already in log scale)
|
||
z_term = dbz
|
||
|
||
# Frequency factor: Rayleigh scattering ∝ f^4 below ~10 GHz,
|
||
# transitions to Mie at higher frequencies (weaker dependence).
|
||
# Normalize to 10 GHz as reference.
|
||
freq_factor =
|
||
cond do
|
||
freq_ghz <= 10 -> 40 * :math.log10(max(freq_ghz, 0.5) / 10.0)
|
||
freq_ghz <= 50 -> 20 * :math.log10(freq_ghz / 10.0)
|
||
true -> 20 * :math.log10(50.0 / 10.0)
|
||
end
|
||
|
||
# Path loss: R^4 for bistatic (R^2 each way)
|
||
# Normalize to 100 km reference distance
|
||
range_loss = 40 * :math.log10(max(dist_km, 1) / 100.0)
|
||
|
||
# Effective scattering volume (~1 km^3 rain cell)
|
||
volume_term = 10.0
|
||
|
||
# Baseline offset to center the scale around useful values
|
||
baseline = -50.0
|
||
|
||
baseline + z_term + freq_factor - range_loss + volume_term
|
||
end
|
||
|
||
defp haversine_km(lat1, lon1, lat2, lon2) do
|
||
dlat = :math.pi() * (lat2 - lat1) / 180.0
|
||
dlon = :math.pi() * (lon2 - lon1) / 180.0
|
||
rlat1 = :math.pi() * lat1 / 180.0
|
||
rlat2 = :math.pi() * lat2 / 180.0
|
||
|
||
a =
|
||
:math.sin(dlat / 2) * :math.sin(dlat / 2) +
|
||
:math.cos(rlat1) * :math.cos(rlat2) *
|
||
:math.sin(dlon / 2) * :math.sin(dlon / 2)
|
||
|
||
c = 2.0 * :math.atan2(:math.sqrt(a), :math.sqrt(1.0 - a))
|
||
@earth_radius_km * c
|
||
end
|
||
|
||
defp bearing_deg(lat1, lon1, lat2, lon2) do
|
||
rlat1 = :math.pi() * lat1 / 180.0
|
||
rlat2 = :math.pi() * lat2 / 180.0
|
||
dlon = :math.pi() * (lon2 - lon1) / 180.0
|
||
|
||
x = :math.sin(dlon) * :math.cos(rlat2)
|
||
y = :math.cos(rlat1) * :math.sin(rlat2) - :math.sin(rlat1) * :math.cos(rlat2) * :math.cos(dlon)
|
||
|
||
bearing = :math.atan2(x, y) * 180.0 / :math.pi()
|
||
Float.round(:math.fmod(bearing + 360.0, 360.0), 1)
|
||
end
|
||
end
|