defmodule Microwaveprop.Propagation.Eme do @moduledoc """ Earth-Moon-Earth (moonbounce) link-budget helpers. EME path loss follows the bistatic radar equation with the Moon acting as a limb-darkened sphere of albedo ρ ≈ 0.065. That factors into a single RCS σ_moon ≈ 0.065 · π · R_moon² ≈ 6.16 × 10¹¹ m². Reducing the full radar equation to `f_MHz` and `d_km` gives the closed form used by this module: L_eme(dB) = -14.44 + 40·log₁₀(d_km) + 20·log₁₀(f_MHz) Spot-check values (round-trip, one leg Earth→Moon, one leg Moon→Earth): - 144 MHz at 384_400 km → ~252 dB - 432 MHz at 384_400 km → ~261 dB - 1_296 MHz at 384_400 km → ~271 dB - 10_368 MHz at 384_400 km → ~289 dB Pure functions only — no DB, no observer state. """ @moon_radius_km 1738.1 @default_albedo 0.065 @doc """ Band-dependent lunar radar albedo ρ (dimensionless). Measured / cited values from the EME literature (Evans 1969, Hagfors, and later K1JT / W5LUA handbook values): | Band | ρ | |------------------|------| | < 300 MHz | 0.065| | 300–999 MHz | 0.081| | 1–1.999 GHz | 0.065| | 2–4.999 GHz | 0.070| | 5–9.999 GHz | 0.080| | 10–23.999 GHz | 0.090| | 24–46.999 GHz | 0.060| | 47–75.999 GHz | 0.030| | ≥ 76 GHz | 0.020| The tiers reflect the surface-roughness regime the radar beam probes: meter-wavelength bands see quasi-specular returns off smooth mare, mid-centimetre bands see a rough surface, and millimetre bands start losing reflectivity to thermal emission. """ @spec moon_albedo(number()) :: float() def moon_albedo(freq_mhz) when is_number(freq_mhz) and freq_mhz < 300, do: 0.065 def moon_albedo(freq_mhz) when freq_mhz < 1000, do: 0.081 def moon_albedo(freq_mhz) when freq_mhz < 2000, do: 0.065 def moon_albedo(freq_mhz) when freq_mhz < 5000, do: 0.070 def moon_albedo(freq_mhz) when freq_mhz < 10_000, do: 0.080 def moon_albedo(freq_mhz) when freq_mhz < 24_000, do: 0.090 def moon_albedo(freq_mhz) when freq_mhz < 47_000, do: 0.060 def moon_albedo(freq_mhz) when freq_mhz < 76_000, do: 0.030 def moon_albedo(freq_mhz) when freq_mhz >= 76_000, do: 0.020 @doc """ Returns the Moon's radar cross-section σ in m² for a given albedo. Defaults to 0.065 (the mean value commonly quoted at 1.3 GHz) so callers that don't care about band-dependence get the classic baseline. """ @spec moon_rcs_m2(number()) :: float() def moon_rcs_m2(albedo \\ @default_albedo) when is_number(albedo) and albedo > 0 do radius_m = @moon_radius_km * 1000.0 albedo * :math.pi() * radius_m * radius_m end @doc """ Free-space round-trip path loss (Earth → Moon → Earth) in dB, without any Moon-reflection term. This is just 2 × one-way FSPL: L_fs,2way(dB) = 64.90 + 40·log₁₀(f_MHz) + 40·log₁₀(d_km) Pair with `moon_reflection_gain_db/2` to decompose the full EME loss into its spreading vs scattering terms. """ @spec fspl_round_trip_db(number(), number()) :: float() def fspl_round_trip_db(freq_mhz, distance_km) when is_number(freq_mhz) and freq_mhz > 0 and is_number(distance_km) and distance_km > 0 do 64.90 + 40.0 * :math.log10(freq_mhz) + 40.0 * :math.log10(distance_km) end @doc """ The Moon's equivalent "antenna gain" in dB for a monostatic radar: `G_moon = 10·log₁₀(4π σ / λ²)` where σ = ρ · π · R². This is the term that *reduces* the net round-trip path loss relative to plain double-FSPL — i.e. the moon's reflection effectiveness. `moon_reflection_gain_db/2` lets callers override the albedo (for example via `moon_albedo/1`); `moon_reflection_gain_db/1` defaults to ρ = 0.065. """ @spec moon_reflection_gain_db(number(), number()) :: float() def moon_reflection_gain_db(freq_mhz, albedo \\ @default_albedo) when is_number(freq_mhz) and freq_mhz > 0 and is_number(albedo) and albedo > 0 do lambda_m = 300.0 / freq_mhz sigma = moon_rcs_m2(albedo) 10.0 * :math.log10(4.0 * :math.pi() * sigma / (lambda_m * lambda_m)) end @doc """ Round-trip EME path loss in dB for a frequency in MHz and an Earth→Moon slant range in km. Covers the transmit leg, Moon scattering, and receive leg in a single number (the conventional "moonbounce path loss" that link-budget spreadsheets use). Accepts an optional `albedo` override — pass `moon_albedo(freq_mhz)` for a band-aware calculation. Defaults to ρ = 0.065, the classic 1296 MHz value, which keeps legacy callers stable. Equivalent to `fspl_round_trip_db - moon_reflection_gain_db`. """ @spec path_loss_db(number(), number(), number()) :: float() def path_loss_db(freq_mhz, distance_km, albedo \\ @default_albedo) when is_number(freq_mhz) and freq_mhz > 0 and is_number(distance_km) and distance_km > 0 and is_number(albedo) and albedo > 0 do fspl_round_trip_db(freq_mhz, distance_km) - moon_reflection_gain_db(freq_mhz, albedo) end # Speed of light (m/s). @c_m_s 299_792_458.0 @doc """ Self-echo Doppler shift in Hz for an EME bounce, given the operating frequency and the Moon's radial velocity relative to the observer. For an own-echo round-trip the Doppler is twice the one-way shift: `Δf = −2 · f · v_radial / c`. A positive radial velocity (Moon receding) produces a negative Hz shift — the returned bounce lands *below* the transmit frequency. """ @spec doppler_shift_hz(number(), number()) :: float() def doppler_shift_hz(freq_mhz, radial_velocity_km_s) when is_number(freq_mhz) and freq_mhz > 0 and is_number(radial_velocity_km_s) do freq_hz = freq_mhz * 1_000_000.0 v_m_s = radial_velocity_km_s * 1000.0 -2.0 * freq_hz * v_m_s / @c_m_s end @doc """ Received signal power in dBm for a symmetric own-echo EME link (operator listens to their own bounce) given: - `:tx_power_dbm` — transmitter output power (dBm) - `:tx_gain_dbi` — transmit antenna gain (dBi) - `:rx_gain_dbi` — receive antenna gain (dBi) — same as tx for self-echo, kept distinct so the function also works for cross- station contacts - `:freq_mhz` — operating frequency (MHz) - `:distance_km` — one-way slant range to the Moon (km) """ @spec received_power_dbm(keyword()) :: float() def received_power_dbm(opts) do tx = Keyword.fetch!(opts, :tx_power_dbm) gt = Keyword.fetch!(opts, :tx_gain_dbi) gr = Keyword.fetch!(opts, :rx_gain_dbi) f = Keyword.fetch!(opts, :freq_mhz) d = Keyword.fetch!(opts, :distance_km) tx + gt + gr - path_loss_db(f, d) end @doc """ Thermal noise floor (dBm) in a given bandwidth and system noise temperature. Callers use this against `received_power_dbm/1` to get SNR. Default Tsys = 290 K (room-temperature receive system, a reasonable placeholder for VHF; a real cooled LNA at 10 GHz is closer to 100 K). """ @spec noise_floor_dbm(number(), number()) :: float() def noise_floor_dbm(bandwidth_hz, t_sys_k \\ 290.0) when is_number(bandwidth_hz) and bandwidth_hz > 0 and is_number(t_sys_k) and t_sys_k > 0 do # -174 dBm/Hz + 10 log(BW) + 10 log(T / 290) -174.0 + 10.0 * :math.log10(bandwidth_hz) + 10.0 * :math.log10(t_sys_k / 290.0) end @doc """ Signal-to-noise ratio (dB) for a symmetric own-echo EME link. Convenience on top of `received_power_dbm/1` and `noise_floor_dbm/2` — returns the dB margin over thermal noise in the specified detection bandwidth. """ @spec snr_db(keyword()) :: float() def snr_db(opts) do bw_hz = Keyword.fetch!(opts, :bandwidth_hz) t_sys_k = Keyword.get(opts, :t_sys_k, 290.0) received_power_dbm(opts) - noise_floor_dbm(bw_hz, t_sys_k) end end