|
| 1 | +"""Monostatic radar range equation and SNR computation. |
| 2 | +
|
| 3 | +Formulas: |
| 4 | + R_max = [P_t * G^2 * lambda^2 * sigma / ((4*pi)^3 * S_min * L)]^(1/4) |
| 5 | + SNR = P_t * G^2 * lambda^2 * sigma / ((4*pi)^3 * k_B * T_s * B_n * R^4 * L) |
| 6 | + S_min = k_B * T_s * B_n * SNR_min / N_pulses |
| 7 | +""" |
| 8 | + |
| 9 | +import math |
| 10 | + |
| 11 | +from physbound.engines.constants import BOLTZMANN, SPEED_OF_LIGHT |
| 12 | +from physbound.engines.units import db_to_linear |
| 13 | +from physbound.errors import PhysicalViolationError |
| 14 | +from physbound.validators import ( |
| 15 | + validate_positive_bandwidth, |
| 16 | + validate_positive_frequency, |
| 17 | + validate_positive_power, |
| 18 | + validate_positive_rcs, |
| 19 | + validate_temperature, |
| 20 | +) |
| 21 | + |
| 22 | + |
| 23 | +def compute_radar_range( |
| 24 | + peak_power_w: float, |
| 25 | + antenna_gain_dbi: float, |
| 26 | + frequency_hz: float, |
| 27 | + rcs_m2: float, |
| 28 | + system_noise_temp_k: float = 290.0, |
| 29 | + noise_bandwidth_hz: float = 1e6, |
| 30 | + min_snr_db: float = 13.0, |
| 31 | + claimed_range_m: float | None = None, |
| 32 | + num_pulses: int = 1, |
| 33 | + losses_db: float = 0.0, |
| 34 | +) -> dict: |
| 35 | + """Compute maximum monostatic radar detection range. |
| 36 | +
|
| 37 | + Args: |
| 38 | + peak_power_w: Peak transmit power in watts. |
| 39 | + antenna_gain_dbi: Antenna gain in dBi (monostatic: same for TX/RX). |
| 40 | + frequency_hz: Operating frequency in Hz. |
| 41 | + rcs_m2: Radar cross section in m^2. |
| 42 | + system_noise_temp_k: System noise temperature in Kelvin. |
| 43 | + noise_bandwidth_hz: Receiver noise bandwidth in Hz. |
| 44 | + min_snr_db: Minimum required SNR in dB for detection. |
| 45 | + claimed_range_m: Optional claimed detection range to validate. |
| 46 | + num_pulses: Number of coherently integrated pulses. |
| 47 | + losses_db: Total system losses in dB. |
| 48 | +
|
| 49 | + Returns: |
| 50 | + Dict with max_range_m, max_range_km, wavelength_m, |
| 51 | + min_detectable_power_w/dbm, human_readable, latex, warnings. |
| 52 | +
|
| 53 | + Raises: |
| 54 | + PhysicalViolationError: If inputs violate physics or claimed range |
| 55 | + exceeds R_max. |
| 56 | + """ |
| 57 | + # Validate inputs |
| 58 | + validate_positive_power(peak_power_w) |
| 59 | + validate_positive_frequency(frequency_hz) |
| 60 | + validate_positive_rcs(rcs_m2) |
| 61 | + validate_temperature(system_noise_temp_k) |
| 62 | + validate_positive_bandwidth(noise_bandwidth_hz) |
| 63 | + |
| 64 | + if num_pulses < 1: |
| 65 | + raise PhysicalViolationError( |
| 66 | + message=f"Number of pulses must be >= 1, got {num_pulses}", |
| 67 | + law_violated="Radar Signal Processing", |
| 68 | + latex_explanation=r"$N_{\text{pulses}} \geq 1$ required", |
| 69 | + claimed_value=float(num_pulses), |
| 70 | + ) |
| 71 | + if losses_db < 0: |
| 72 | + raise PhysicalViolationError( |
| 73 | + message=f"System losses must be >= 0 dB, got {losses_db} dB", |
| 74 | + law_violated="Conservation of Energy", |
| 75 | + latex_explanation=r"$L \geq 0\,\text{dB}$; negative loss implies free energy gain", |
| 76 | + claimed_value=losses_db, |
| 77 | + unit="dB", |
| 78 | + ) |
| 79 | + |
| 80 | + # Derived quantities |
| 81 | + c = SPEED_OF_LIGHT.magnitude |
| 82 | + k_b = BOLTZMANN.magnitude |
| 83 | + wavelength_m = c / frequency_hz |
| 84 | + gain_linear = db_to_linear(antenna_gain_dbi) |
| 85 | + snr_min_linear = db_to_linear(min_snr_db) |
| 86 | + losses_linear = db_to_linear(losses_db) |
| 87 | + integration_gain = num_pulses |
| 88 | + |
| 89 | + # Minimum detectable signal power |
| 90 | + s_min_w = (k_b * system_noise_temp_k * noise_bandwidth_hz * snr_min_linear) / integration_gain |
| 91 | + s_min_dbm = 10.0 * math.log10(s_min_w / 1e-3) |
| 92 | + |
| 93 | + # Radar range equation: R_max |
| 94 | + numerator = peak_power_w * gain_linear**2 * wavelength_m**2 * rcs_m2 |
| 95 | + denominator = (4.0 * math.pi) ** 3 * s_min_w * losses_linear |
| 96 | + r_max_m = (numerator / denominator) ** 0.25 |
| 97 | + |
| 98 | + # Warnings |
| 99 | + warnings: list[str] = [] |
| 100 | + if frequency_hz > 3e11: |
| 101 | + warnings.append( |
| 102 | + "Frequency > 300 GHz: atmospheric absorption may significantly " |
| 103 | + "reduce effective range beyond the free-space model." |
| 104 | + ) |
| 105 | + if num_pulses > 1: |
| 106 | + warnings.append( |
| 107 | + f"Coherent integration of {num_pulses} pulses assumed " |
| 108 | + f"(gain = N). Non-coherent integration yields gain = sqrt(N)." |
| 109 | + ) |
| 110 | + if rcs_m2 > 100: |
| 111 | + warnings.append( |
| 112 | + "RCS > 100 m^2 is typical only for very large targets (ships, large aircraft)." |
| 113 | + ) |
| 114 | + if rcs_m2 < 1e-4: |
| 115 | + warnings.append("RCS < 0.0001 m^2 is at the limit of detectability for most radar systems.") |
| 116 | + |
| 117 | + # Validate claimed range |
| 118 | + if claimed_range_m is not None and claimed_range_m > r_max_m: |
| 119 | + excess_pct = ((claimed_range_m - r_max_m) / r_max_m) * 100.0 |
| 120 | + raise PhysicalViolationError( |
| 121 | + message=( |
| 122 | + f"Claimed detection range {claimed_range_m:.1f} m " |
| 123 | + f"({claimed_range_m / 1000:.1f} km) exceeds radar range " |
| 124 | + f"equation limit of {r_max_m:.1f} m " |
| 125 | + f"({r_max_m / 1000:.1f} km) by {excess_pct:.1f}%" |
| 126 | + ), |
| 127 | + law_violated="Radar Range Equation", |
| 128 | + latex_explanation=( |
| 129 | + rf"$R_{{\max}} = \left[\frac{{P_t G^2 \lambda^2 \sigma}}" |
| 130 | + rf"{{(4\pi)^3 S_{{\min}} L}}\right]^{{1/4}} = " |
| 131 | + rf"{r_max_m:.1f}\,\text{{m}}$. " |
| 132 | + rf"Claimed ${claimed_range_m:.1f}\,\text{{m}}$ exceeds " |
| 133 | + rf"this limit by ${excess_pct:.1f}\%$." |
| 134 | + ), |
| 135 | + computed_limit=r_max_m, |
| 136 | + claimed_value=claimed_range_m, |
| 137 | + unit="m", |
| 138 | + ) |
| 139 | + |
| 140 | + # Human-readable output |
| 141 | + power_dbm = 10.0 * math.log10(peak_power_w / 1e-3) |
| 142 | + human_readable = ( |
| 143 | + f"Radar Range Equation (Monostatic):\n" |
| 144 | + f" Peak Power: {peak_power_w:.1f} W ({power_dbm:.1f} dBm)\n" |
| 145 | + f" Antenna Gain: {antenna_gain_dbi:.1f} dBi\n" |
| 146 | + f" Frequency: {frequency_hz / 1e9:.3f} GHz " |
| 147 | + f"(lambda = {wavelength_m:.4f} m)\n" |
| 148 | + f" RCS: {rcs_m2:.4f} m^2\n" |
| 149 | + f" System Temp: {system_noise_temp_k:.1f} K\n" |
| 150 | + f" Noise BW: {noise_bandwidth_hz / 1e6:.3f} MHz\n" |
| 151 | + f" Min SNR: {min_snr_db:.1f} dB\n" |
| 152 | + f" Losses: {losses_db:.1f} dB\n" |
| 153 | + f" Pulses: {num_pulses}\n" |
| 154 | + f" S_min: {s_min_dbm:.2f} dBm ({s_min_w:.3e} W)\n" |
| 155 | + f" Max Range: {r_max_m:.1f} m ({r_max_m / 1000:.2f} km)" |
| 156 | + ) |
| 157 | + |
| 158 | + # LaTeX output |
| 159 | + latex = ( |
| 160 | + rf"$R_{{\max}} = \left[\frac{{P_t G^2 \lambda^2 \sigma}}" |
| 161 | + rf"{{(4\pi)^3 S_{{\min}} L}}\right]^{{1/4}} = " |
| 162 | + rf"\left[\frac{{{peak_power_w:.1f} \times {gain_linear:.2f}^2 \times " |
| 163 | + rf"{wavelength_m:.4f}^2 \times {rcs_m2:.4f}}}" |
| 164 | + rf"{{(4\pi)^3 \times {s_min_w:.3e} \times {losses_linear:.2f}}}" |
| 165 | + rf"\right]^{{1/4}} = {r_max_m:.1f}\,\text{{m}}$" |
| 166 | + ) |
| 167 | + |
| 168 | + return { |
| 169 | + "max_range_m": r_max_m, |
| 170 | + "max_range_km": r_max_m / 1000.0, |
| 171 | + "wavelength_m": wavelength_m, |
| 172 | + "min_detectable_power_w": s_min_w, |
| 173 | + "min_detectable_power_dbm": s_min_dbm, |
| 174 | + "antenna_gain_linear": gain_linear, |
| 175 | + "snr_min_linear": snr_min_linear, |
| 176 | + "integration_gain": integration_gain, |
| 177 | + "losses_linear": losses_linear, |
| 178 | + "warnings": warnings, |
| 179 | + "human_readable": human_readable, |
| 180 | + "latex": latex, |
| 181 | + } |
0 commit comments