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. """ # 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. """ @spec find_scatter_cells( [{float(), float(), float()}], float(), float(), float() ) :: [ %{ lat: float(), lon: float(), dbz: float(), distance_km: float(), bearing: float(), scatter_db: float() } ] 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. """ @spec classify([%{scatter_db: float()}]) :: :excellent | :good | :marginal | :none 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: Microwaveprop.Geo.haversine_km(lat1, lon1, lat2, lon2) 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