RF Propagation

Table of Contents

Overview

Radio frequency (RF) propagation modeling is crucial for maintaining communication with high-altitude balloons throughout their flight. The simulator implements sophisticated models that account for the unique challenges of balloon-to-ground communication across varying altitudes, atmospheric conditions, and terrain features. This section details the physics and implementation of these RF propagation models.

RF Challenges: A balloon at 30 km altitude has a radio horizon exceeding 600 km, but actual communication range depends on frequency, power, atmospheric conditions, and terrain. Signal strength can vary by 40+ dB during a flight due to changing path loss, atmospheric absorption, and multipath effects.

Line-of-Sight Propagation

The foundation of balloon communication is line-of-sight (LOS) propagation, modified by Earth's curvature and atmospheric refraction.

Radio Horizon

The geometric horizon distance considering Earth's curvature:

$$d_{horizon} = \sqrt{2R_e h}$$ For atmospheric refraction (4/3 Earth model): $$d_{radio} = \sqrt{2k_r R_e h} \approx 1.15 \sqrt{2R_e h}$$ where $k_r = 4/3$ is the refractivity factor

Maximum communication range between balloon and ground station:

$$d_{max} = \sqrt{2k_r R_e h_1} + \sqrt{2k_r R_e h_2}$$ where $h_1$ is balloon altitude and $h_2$ is ground station elevation

Free Space Path Loss

The Friis transmission equation gives basic path loss:

$$L_{fs} = 20\log_{10}\left(\frac{4\pi d}{\lambda}\right) = 20\log_{10}(d) + 20\log_{10}(f) - 147.55$$ where $L_{fs}$ is in dB, $d$ in meters, $f$ in Hz

Implementation

class LineOfSightPropagation:
    """Calculate line-of-sight propagation parameters"""

    def __init__(self):
        self.earth_radius = 6371000  # meters
        self.k_factor = 4/3  # Standard atmospheric refraction

    def calculate_radio_horizon(self, altitude_m):
        """Calculate radio horizon distance"""
        # Effective Earth radius
        R_eff = self.k_factor * self.earth_radius

        # Radio horizon
        d_horizon = np.sqrt(2 * R_eff * altitude_m)

        return d_horizon

    def calculate_max_range(self, balloon_alt, station_alt):
        """Maximum communication range"""
        d_balloon = self.calculate_radio_horizon(balloon_alt)
        d_station = self.calculate_radio_horizon(station_alt)

        return d_balloon + d_station

    def check_los(self, balloon_pos, station_pos, terrain_model):
        """Check if line-of-sight exists"""
        # Vector from station to balloon
        dx = balloon_pos - station_pos
        distance = np.linalg.norm(dx)

        # Sample points along path
        n_samples = int(distance / 1000)  # Sample every km

        for i in range(1, n_samples):
            # Interpolated position
            alpha = i / n_samples
            pos = station_pos + alpha * dx

            # Ray height at this distance
            ray_height = self.calculate_ray_height(
                station_pos[2], balloon_pos[2], alpha, distance
            )

            # Terrain height
            terrain_height = terrain_model.get_elevation(pos[0], pos[1])

            # Check obstruction
            if ray_height < terrain_height + 10:  # 10m clearance
                return False, i * distance / n_samples

        return True, distance

    def calculate_ray_height(self, h1, h2, alpha, distance):
        """Calculate ray height considering Earth curvature"""
        # Height due to curvature
        h_curve = (alpha * (1 - alpha) * distance**2) / (2 * self.k_factor * self.earth_radius)

        # Linear interpolation plus curvature
        h_ray = (1 - alpha) * h1 + alpha * h2 + h_curve

        return h_ray

Atmospheric Effects

Atmospheric Absorption

Atmospheric gases absorb RF energy, particularly oxygen and water vapor:

$$L_{atm} = \gamma_O \cdot d_O + \gamma_W \cdot d_W$$ where: $$\gamma_O = \frac{7.19 \times 10^{-3} + 6.09/f^2 + 4.81/(f-57)^2}{f^2 + 0.227} \quad \text{(dB/km for O₂)}$$ $$\gamma_W = \frac{0.067 + 3.0/(f-22.3)^2 + 7.3/(f-183.3)^2 + 4.3/(f-323.8)^2}{f^2} \rho_w \quad \text{(dB/km for H₂O)}$$

Total path attenuation requires integration through varying atmospheric density:

$$L_{total} = \int_0^d \gamma(h(s)) \, ds$$

Refraction and Ducting

The refractive index varies with altitude:

$$n = 1 + N \times 10^{-6}$$ $$N = 77.6 \frac{P}{T} + 3.73 \times 10^5 \frac{e}{T^2}$$ where $P$ is pressure (mbar), $T$ is temperature (K), $e$ is water vapor pressure (mbar)
class AtmosphericEffects:
    """Model atmospheric effects on RF propagation"""

    def __init__(self, atmosphere_model):
        self.atmosphere = atmosphere_model

    def calculate_absorption(self, freq_ghz, path_points):
        """Calculate total atmospheric absorption along path"""
        total_loss = 0.0

        for i in range(len(path_points) - 1):
            # Segment properties
            p1, p2 = path_points[i], path_points[i+1]
            distance = np.linalg.norm(p2 - p1) / 1000  # km
            avg_alt = (p1[2] + p2[2]) / 2

            # Atmospheric conditions
            conditions = self.atmosphere.get_conditions(avg_alt)

            # Specific attenuation
            gamma_o2 = self.oxygen_absorption(freq_ghz, conditions)
            gamma_h2o = self.water_vapor_absorption(freq_ghz, conditions)

            # Segment loss
            segment_loss = (gamma_o2 + gamma_h2o) * distance
            total_loss += segment_loss

        return total_loss

    def oxygen_absorption(self, freq_ghz, conditions):
        """ITU-R P.676 oxygen absorption model"""
        f = freq_ghz
        P = conditions['pressure'] / 100  # hPa
        T = conditions['temperature']

        # Simplified model for 1-350 GHz
        rp = P / 1013.0
        rt = 288.0 / T

        # Oxygen resonance at 60 GHz
        gamma_o = 0.0
        for f_o in [50.474, 50.987, 51.503, 52.021, 52.542, 53.066, 53.595,
                    54.130, 54.671, 55.221, 55.783, 56.264, 56.363, 56.968,
                    57.612, 58.323, 58.446, 59.164, 59.590, 60.306, 60.434,
                    61.150, 61.800, 62.411, 62.486, 62.997, 63.568, 64.127,
                    64.678, 65.224, 65.764, 66.302, 66.836, 67.369, 67.900,
                    68.431, 68.960, 69.489]:
            S = 0.001  # Simplified line strength
            gamma = 0.001  # Simplified line width
            gamma_o += S * gamma / ((f - f_o)**2 + gamma**2)

        return gamma_o * rp * rt**2

    def calculate_refractivity(self, altitude):
        """Calculate radio refractivity N-units"""
        conditions = self.atmosphere.get_conditions(altitude)

        P = conditions['pressure'] / 100  # hPa
        T = conditions['temperature']
        e = conditions['humidity'] * self.saturation_pressure(T) / 100

        # ITU-R P.453 refractivity
        N = 77.6 * P / T + 3.73e5 * e / T**2

        return N

    def calculate_ray_bending(self, elevation_angle, altitude):
        """Calculate ray bending due to refraction"""
        # Refractivity gradient
        dN_dh = -40  # N-units/km typical

        # Initial ray curvature
        n0 = 1 + self.calculate_refractivity(0) * 1e-6
        k = -dN_dh * 1e-6 / 1000  # 1/m

        # Ray bending (small angle approximation)
        theta_0 = np.radians(elevation_angle)
        bending = -k * altitude / np.sin(theta_0)

        return np.degrees(bending)

Scintillation

Atmospheric turbulence causes signal amplitude and phase fluctuations:

$$\sigma_\chi^2 = 1.23 C_n^2 k^{7/6} L^{11/6}$$ where $C_n^2$ is the refractive index structure constant

Typical $C_n^2$ values:

  • Ground level: $10^{-13}$ to $10^{-15}$ m^{-2/3}
  • Tropopause: $10^{-16}$ to $10^{-18}$ m^{-2/3}
  • Stratosphere: $10^{-19}$ to $10^{-20}$ m^{-2/3}

Multipath and Scattering

Ground Reflection

Two-ray model for ground reflection:

$$E_{total} = E_{direct} + \Gamma E_{reflected}$$ $$P_r = P_t G_t G_r \left|\frac{\lambda}{4\pi}\right|^2 \left|\frac{1}{d_1} + \frac{\Gamma e^{-j\Delta\phi}}{d_2}\right|^2$$

Where the reflection coefficient $\Gamma$ depends on polarization and grazing angle:

Horizontal polarization:

$$\Gamma_h = \frac{\sin\psi - \sqrt{\epsilon_r - \cos^2\psi}}{\sin\psi + \sqrt{\epsilon_r - \cos^2\psi}}$$

Vertical polarization:

$$\Gamma_v = \frac{\epsilon_r\sin\psi - \sqrt{\epsilon_r - \cos^2\psi}}{\epsilon_r\sin\psi + \sqrt{\epsilon_r - \cos^2\psi}}$$

Tropospheric Scatter

For beyond-horizon communication:

$$L_{scatter} = 30\log_{10}(f) + 10\log_{10}(d) + L_s + L_z - G_{scatter}$$ where $L_s$ accounts for scattering volume and $L_z$ for altitude factors
class MultipathPropagation:
    """Model multipath propagation effects"""

    def __init__(self):
        self.terrain_dielectric = {
            'water': (80, 5.0),      # (epsilon_r, sigma)
            'wet_ground': (25, 0.02),
            'dry_ground': (15, 0.005),
            'concrete': (9, 0.001),
            'forest': (5, 0.0001)
        }

    def two_ray_model(self, tx_pos, rx_pos, freq_hz, terrain_type='dry_ground'):
        """Calculate received power using two-ray model"""
        # Direct path
        d_direct = np.linalg.norm(rx_pos - tx_pos)

        # Reflection point (simplified - assumes flat earth)
        h_tx = tx_pos[2]
        h_rx = rx_pos[2]
        d_horizontal = np.sqrt((tx_pos[0] - rx_pos[0])**2 +
                              (tx_pos[1] - rx_pos[1])**2)

        # Grazing angle
        psi = np.arctan((h_tx + h_rx) / d_horizontal)

        # Reflected path distances
        d_reflected = np.sqrt(d_horizontal**2 + (h_tx + h_rx)**2)

        # Reflection coefficient
        epsilon_r, sigma = self.terrain_dielectric[terrain_type]
        gamma = self.calculate_reflection_coefficient(
            psi, freq_hz, epsilon_r, sigma, polarization='vertical'
        )

        # Path difference
        delta_d = d_reflected - d_direct
        wavelength = 3e8 / freq_hz
        phase_diff = 2 * np.pi * delta_d / wavelength

        # Combined field
        E_ratio = np.abs(1/d_direct + gamma * np.exp(-1j*phase_diff) / d_reflected)

        # Power ratio (relative to free space)
        power_factor = 20 * np.log10(E_ratio * d_direct)

        return power_factor

    def calculate_reflection_coefficient(self, grazing_angle, freq_hz,
                                       epsilon_r, sigma, polarization='vertical'):
        """Calculate Fresnel reflection coefficient"""
        # Complex permittivity
        epsilon_0 = 8.854e-12
        omega = 2 * np.pi * freq_hz
        epsilon_c = epsilon_r - 1j * sigma / (omega * epsilon_0)

        # Fresnel coefficients
        sin_psi = np.sin(grazing_angle)
        cos_psi = np.cos(grazing_angle)

        if polarization == 'horizontal':
            numerator = sin_psi - np.sqrt(epsilon_c - cos_psi**2)
            denominator = sin_psi + np.sqrt(epsilon_c - cos_psi**2)
        else:  # vertical
            numerator = epsilon_c * sin_psi - np.sqrt(epsilon_c - cos_psi**2)
            denominator = epsilon_c * sin_psi + np.sqrt(epsilon_c - cos_psi**2)

        return numerator / denominator

    def knife_edge_diffraction(self, tx_pos, rx_pos, obstacle_pos, freq_hz):
        """Calculate knife-edge diffraction loss"""
        # Distances
        d1 = np.linalg.norm(obstacle_pos - tx_pos)
        d2 = np.linalg.norm(rx_pos - obstacle_pos)
        d = d1 + d2

        # Height of obstacle above direct path
        h_los = tx_pos[2] + (rx_pos[2] - tx_pos[2]) * d1 / (d1 + d2)
        h = obstacle_pos[2] - h_los

        # Fresnel-Kirchhoff diffraction parameter
        wavelength = 3e8 / freq_hz
        nu = h * np.sqrt(2 * (d1 + d2) / (wavelength * d1 * d2))

        # Diffraction loss (approximate)
        if nu < -0.7:
            L_diff = 0  # No obstruction
        elif nu < 1:
            L_diff = 20 * np.log10(0.5 + 0.62 * nu)
        else:
            L_diff = 20 * np.log10(0.225 / nu)

        return abs(L_diff)

Frequency-Specific Models

VHF/UHF (144/440 MHz Amateur Bands)

Characteristics:

  • Minimal atmospheric absorption
  • Significant ground reflection effects
  • Tropospheric ducting possible
  • Doppler shift: ±3 kHz at 440 MHz for 100 m/s velocity

L-Band (1.2-1.7 GHz GPS/Iridium)

Characteristics:

  • Moderate rain attenuation
  • Ionospheric effects negligible
  • Multipath significant near ground
  • Atmospheric loss: ~0.1 dB total

S-Band (2.4 GHz ISM)

Characteristics:

  • Water vapor absorption: 0.01 dB/km
  • Rain fade significant above 10 mm/hr
  • Oxygen absorption: negligible
  • Free space loss: 126 dB at 100 km
class FrequencySpecificModels:
    """Frequency-dependent propagation models"""

    def __init__(self):
        self.models = {
            'vhf': VHFPropagation(),
            'uhf': UHFPropagation(),
            'lband': LBandPropagation(),
            'sband': SBandPropagation()
        }

    def get_model(self, frequency_mhz):
        """Select appropriate model based on frequency"""
        if frequency_mhz < 300:
            return self.models['vhf']
        elif frequency_mhz < 1000:
            return self.models['uhf']
        elif frequency_mhz < 2000:
            return self.models['lband']
        elif frequency_mhz < 3000:
            return self.models['sband']
        else:
            raise ValueError(f"No model for {frequency_mhz} MHz")

    def calculate_doppler(self, freq_mhz, velocity_vector, look_vector):
        """Calculate Doppler shift"""
        # Radial velocity component
        v_radial = np.dot(velocity_vector, look_vector)

        # Doppler shift
        c = 3e8  # m/s
        f_shift = freq_mhz * 1e6 * v_radial / c

        return f_shift

    def ionospheric_delay(self, freq_mhz, elevation_angle, tec=20):
        """Calculate ionospheric delay

        Args:
            freq_mhz: Frequency in MHz
            elevation_angle: Elevation angle in degrees
            tec: Total Electron Content in TECU (10^16 electrons/m²)
        """
        # Only significant below ~2 GHz
        if freq_mhz > 2000:
            return 0

        # Mapping function for oblique paths
        Re = 6371  # km
        hI = 350   # km ionosphere height

        chi = np.radians(90 - elevation_angle)
        F = 1 / np.sqrt(1 - (Re/(Re+hI) * np.sin(chi))**2)

        # Delay in meters
        delay_m = 40.3 * tec * F / (freq_mhz**2)

        return delay_m

Implementation

Complete Propagation Model

class RFPropagationModel:
    """Comprehensive RF propagation model for balloon tracking"""

    def __init__(self, terrain_model, atmosphere_model):
        self.terrain = terrain_model
        self.atmosphere = atmosphere_model
        self.los = LineOfSightPropagation()
        self.atmospheric = AtmosphericEffects(atmosphere_model)
        self.multipath = MultipathPropagation()
        self.link_budget = LinkBudgetCalculator()

    def calculate_propagation(self, balloon_state, ground_station, link_params):
        """Calculate all propagation parameters"""

        # Positions
        balloon_pos = balloon_state.position_ecef
        station_pos = ground_station.position_ecef

        # Basic geometry
        distance = np.linalg.norm(balloon_pos - station_pos)
        elevation = self.calculate_elevation_angle(balloon_pos, station_pos)
        azimuth = self.calculate_azimuth(balloon_pos, station_pos)

        # Check line of sight
        los_clear, los_distance = self.los.check_los(
            balloon_pos, station_pos, self.terrain
        )

        if not los_clear:
            # Beyond horizon or obstructed
            return self.calculate_nlos_propagation(
                balloon_state, ground_station, link_params
            )

        # Free space path loss
        freq_hz = link_params['frequency'] * 1e6
        L_fs = 20*np.log10(distance) + 20*np.log10(freq_hz) - 147.55

        # Atmospheric absorption
        path_points = self.generate_path_points(balloon_pos, station_pos, 50)
        L_atm = self.atmospheric.calculate_absorption(
            freq_hz/1e9, path_points
        )

        # Multipath effects (significant at low elevation)
        if elevation < 10:  # degrees
            multipath_gain = self.multipath.two_ray_model(
                balloon_pos, station_pos, freq_hz
            )
        else:
            multipath_gain = 0

        # Rain attenuation (if applicable)
        L_rain = self.calculate_rain_attenuation(
            freq_hz, distance, elevation,
            ground_station.weather.get('rain_rate', 0)
        )

        # Antenna pointing loss
        L_pointing = self.calculate_pointing_loss(
            balloon_state.velocity, distance, link_params
        )

        # Doppler shift
        doppler = self.calculate_doppler_shift(
            balloon_state, ground_station, freq_hz
        )

        # Compile losses
        propagation_losses = {
            'free_space': L_fs,
            'atmospheric': L_atm,
            'rain': L_rain,
            'pointing': L_pointing,
            'multipath_gain': multipath_gain
        }

        # Calculate link budget
        link_analysis = self.link_budget.calculate_link_budget(
            link_params, propagation_losses
        )

        return {
            'distance': distance,
            'elevation': elevation,
            'azimuth': azimuth,
            'los_clear': los_clear,
            'propagation_losses': propagation_losses,
            'link_budget': link_analysis,
            'doppler_shift': doppler,
            'data_rate': link_analysis['max_data_rate']
        }

    def calculate_coverage_area(self, balloon_state, link_params, margin_db=3):
        """Calculate ground coverage area for given link margin"""

        # Radio horizon
        max_range = self.los.calculate_radio_horizon(balloon_state.altitude)

        # Sample points in coverage area
        coverage_points = []

        for bearing in np.arange(0, 360, 5):
            for distance in np.arange(0, max_range, 10000):
                # Project point
                lat, lon = self.project_point(
                    balloon_state.latitude,
                    balloon_state.longitude,
                    bearing,
                    distance
                )

                # Create virtual ground station
                station = GroundStation(lat, lon, 0)

                # Calculate link
                result = self.calculate_propagation(
                    balloon_state, station, link_params
                )

                if result['link_budget']['link_margin'] >= margin_db:
                    coverage_points.append({
                        'lat': lat,
                        'lon': lon,
                        'distance': distance,
                        'margin': result['link_budget']['link_margin']
                    })

        return coverage_points

Real-Time Tracking Support

class RealtimeRFTracker:
    """Real-time RF tracking and prediction"""

    def __init__(self, propagation_model):
        self.propagation = propagation_model
        self.history = []
        self.predictions = {}

    def update(self, balloon_state, ground_stations, timestamp):
        """Update RF propagation for all ground stations"""

        results = {}

        for station_id, station in ground_stations.items():
            # Calculate current propagation
            link_params = station.get_link_parameters()
            prop_result = self.propagation.calculate_propagation(
                balloon_state, station, link_params
            )

            # Store results
            results[station_id] = {
                'timestamp': timestamp,
                'snr': prop_result['link_budget']['snr'],
                'elevation': prop_result['elevation'],
                'distance': prop_result['distance'],
                'doppler': prop_result['doppler_shift']
            }

            # Predict signal loss
            if prop_result['elevation'] < 5:
                los_time = self.predict_los_time(balloon_state, station)
                results[station_id]['los_warning'] = los_time

        self.history.append(results)
        return results

    def predict_handover(self, balloon_trajectory, ground_stations):
        """Predict optimal handover times between stations"""

        handover_schedule = []
        current_best = None

        for state in balloon_trajectory:
            station_snr = {}

            # Calculate SNR for each station
            for station_id, station in ground_stations.items():
                result = self.propagation.calculate_propagation(
                    state, station, station.get_link_parameters()
                )
                station_snr[station_id] = result['link_budget']['snr']

            # Find best station
            best_station = max(station_snr, key=station_snr.get)

            # Detect handover
            if best_station != current_best and current_best is not None:
                handover_schedule.append({
                    'time': state.timestamp,
                    'from_station': current_best,
                    'to_station': best_station,
                    'from_snr': station_snr[current_best],
                    'to_snr': station_snr[best_station]
                })

            current_best = best_station

        return handover_schedule

Real-Time Optimization

Adaptive Link Parameters

The system can dynamically adjust transmission parameters based on link conditions:

class AdaptiveLinkController:
    """Adaptive control of RF link parameters"""

    def __init__(self):
        self.min_snr_margin = 3  # dB
        self.power_steps = [0.1, 0.5, 1.0, 2.0, 5.0, 10.0]  # Watts
        self.rate_steps = [300, 1200, 9600, 19200]  # bps

    def optimize_link(self, current_snr, link_params):
        """Optimize power and data rate for conditions"""

        margin = current_snr - link_params['required_snr']

        if margin < self.min_snr_margin:
            # Need more margin
            return self.increase_margin(margin, link_params)
        elif margin > self.min_snr_margin + 10:
            # Can reduce power or increase rate
            return self.reduce_margin(margin, link_params)
        else:
            # Link is optimal
            return link_params

    def increase_margin(self, current_margin, link_params):
        """Increase link margin by adjusting parameters"""

        recommendations = []

        # Option 1: Increase power
        current_power = link_params['tx_power_w']
        power_index = self.power_steps.index(current_power)
        if power_index < len(self.power_steps) - 1:
            new_power = self.power_steps[power_index + 1]
            power_gain = 10 * np.log10(new_power / current_power)
            recommendations.append({
                'action': 'increase_power',
                'new_value': new_power,
                'margin_improvement': power_gain
            })

        # Option 2: Reduce data rate
        current_rate = link_params['data_rate']
        rate_index = self.rate_steps.index(current_rate)
        if rate_index > 0:
            new_rate = self.rate_steps[rate_index - 1]
            rate_gain = 10 * np.log10(current_rate / new_rate)
            recommendations.append({
                'action': 'reduce_rate',
                'new_value': new_rate,
                'margin_improvement': rate_gain
            })

        # Option 3: Improve coding
        if link_params.get('fec_rate', 1.0) > 0.5:
            recommendations.append({
                'action': 'increase_fec',
                'new_value': 'rate_1/2',
                'margin_improvement': 3  # dB typical
            })

        return recommendations

    def predict_link_quality(self, trajectory_prediction, ground_station):
        """Predict future link quality along trajectory"""

        quality_timeline = []

        for future_state in trajectory_prediction:
            # Propagation at future time
            prop = self.propagation.calculate_propagation(
                future_state, ground_station, self.current_link_params
            )

            quality = {
                'time': future_state.timestamp,
                'snr': prop['link_budget']['snr'],
                'margin': prop['link_budget']['link_margin'],
                'elevation': prop['elevation']
            }

            # Classify link quality
            if quality['margin'] < 0:
                quality['status'] = 'no_link'
            elif quality['margin'] < 3:
                quality['status'] = 'marginal'
            elif quality['margin'] < 10:
                quality['status'] = 'good'
            else:
                quality['status'] = 'excellent'

            quality_timeline.append(quality)

        return quality_timeline
Implementation Considerations:
  • Update propagation calculations at 1 Hz minimum during critical phases
  • Cache terrain data along predicted path to reduce computation
  • Pre-calculate atmospheric profiles for common trajectories
  • Implement hysteresis in handover decisions to prevent oscillation
  • Account for antenna pattern variations with pointing angle

Comprehensive RF System Implementation (2025 Update)

The balloon simulation platform now includes a state-of-the-art RF analysis system that integrates all modern propagation models and technologies. This implementation represents a significant advancement in balloon communication modeling.

Integrated RF System Architecture

Key Components:
  • ITU-R P.452-16 terrestrial propagation model
  • ITU-R P.618-13 Earth-space propagation model
  • Advanced antenna pattern modeling with polarization
  • MIMO and diversity combining analysis
  • Forward Error Correction (FEC) performance modeling
  • Real terrain elevation data integration
  • Precipitation and scintillation effects
  • Building and vegetation loss models

Advanced Propagation Models

ITU-R P.452 Implementation

For terrestrial paths (balloon near ground or relay networks):

class ITURP452Model:
    """ITU-R P.452-16 propagation model for terrestrial paths"""

    def calculate_propagation_loss(self, freq_ghz, distance_km,
                                 tx_height_m, rx_height_m,
                                 time_percentage, climate_zone,
                                 terrain_profile=None):
        # Basic transmission loss
        L_b = self._basic_transmission_loss(freq_ghz, distance_km)

        # Diffraction loss over terrain
        L_d = self._diffraction_loss(terrain_profile, freq_ghz)

        # Tropospheric scatter loss
        L_bs = self._troposcatter_loss(freq_ghz, distance_km,
                                     tx_height_m, rx_height_m)

        # Ducting/layer reflection loss
        L_ba = self._anomalous_propagation_loss(freq_ghz, distance_km,
                                              time_percentage)

        # Combine using ITU-R P.452 method
        return self._combine_losses(L_b, L_d, L_bs, L_ba, time_percentage)

Antenna Pattern Modeling

3D radiation patterns with polarization mismatch:

$$G(\theta, \phi) = G_{max} \cdot F_\theta(\theta) \cdot F_\phi(\phi) \cdot PLF$$ where PLF is the polarization loss factor: $$PLF = |\hat{p}_{tx} \cdot \hat{p}_{rx}|^2$$

MIMO and Diversity Systems

The implementation supports multiple antenna configurations:

Diversity Combining Methods:

  • Maximum Ratio Combining (MRC): Optimal SNR improvement
  • Equal Gain Combining (EGC): Simple phase alignment
  • Selection Combining (SC): Choose best signal

MIMO capacity calculation:

$$C = \log_2 \det\left(I_{N_r} + \frac{\rho}{N_t} HH^*\right) \text{ bps/Hz}$$

Real Terrain Integration (2025 Enhanced)

The system now features automatic SRTM terrain data downloading and caching with multiple fallback sources:

class DEMManager:
    """Manages terrain elevation data from multiple sources with automatic fallback"""

    def __init__(self, cache_dir="./dem_cache"):
        self.sources = {
            'SRTM3': SRTM3Provider(),         # 90m resolution - automatic tile download
            'SRTM1': SRTM1Provider(),         # 30m resolution - where available
            'OpenTopoData': OpenTopoAPI(),    # Web API fallback with 1s timeout
            'Mapbox': MapboxProvider()        # Commercial fallback
        }

    def get_terrain_profile(self, lat1, lon1, lat2, lon2, num_points=100):
        # Generate 100+ points along great circle path
        # Automatic interpolation for missing elevations
        # Obstacle detection with prominence analysis
        terrain_points = []
        for point in self.generate_path_points(lat1, lon1, lat2, lon2):
            elevation = self.get_elevation(point.lat, point.lon)
            terrain_points.append(TerrainPoint(point, elevation))

        return self.analyze_terrain_obstacles(terrain_points)

Weather Effects Modeling

Rain Attenuation

Using ITU-R P.838 coefficients:

$$\gamma_R = k R^\alpha \text{ dB/km}$$ where $k$ and $\alpha$ depend on frequency and polarization

Scintillation

Tropospheric scintillation standard deviation:

$$\sigma_{scint} = \sigma_{ref} \cdot f^{0.85} \cdot \sin^{-1.2}(\theta) \cdot A(p)$$

Forward Error Correction Analysis

The system models various FEC schemes:

class FECAnalyzer:
    """Analyzes Forward Error Correction performance"""

    def analyze_fec_performance(self, scheme, channel_conditions,
                              modulation, data_rate):
        # Calculate uncoded BER
        ber_uncoded = self.calculate_uncoded_ber(
            channel_conditions.snr_db, modulation
        )

        # Apply coding gain
        coding_gain = self.get_coding_gain(scheme, channel_conditions)

        # Calculate coded BER
        ber_coded = self.calculate_coded_ber(
            scheme, ber_uncoded, channel_conditions
        )

        return FECPerformance(
            coding_gain_db=coding_gain,
            ber_coded=ber_coded,
            throughput_efficiency=scheme.rate,
            latency_ms=self.calculate_latency(scheme, data_rate)
        )

User Interface Integration

The RF analysis system features a comprehensive web interface:

UI Features:
  • Interactive map with click-to-set locations
  • Real-time link budget calculations
  • RF coverage heat map visualization
  • Multi-tab analysis results:
    • Link Budget with gain/loss breakdown
    • Loss Analysis with categorized contributions
    • Performance metrics (BER, data rate, availability)
    • Advanced features (MIMO, FEC, weather effects)
  • Export capabilities for further analysis

API Endpoints

The RF analysis system exposes RESTful API endpoints:

POST /api/analysis/comprehensive_enhanced
{
    "balloon_lat": 40.6072,
    "balloon_lon": -74.2772,
    "balloon_alt": 20000,
    "ground_lat": 40.7114,
    "ground_lon": -73.7117,
    "frequency": 915,
    "bandwidth": 10,
    "tx_power": 30,
    "balloon_antenna": "omnidirectional",
    "ground_antenna": "yagi",
    "enable_mimo": true,
    "rain_rate_mm_hr": 5
}

Response:
{
    "success": true,
    "enhanced_analysis": {
        "link_margin_db": 12.5,
        "total_path_loss_db": 145.3,
        "received_power_dbm": -82.8,
        "losses": {
            "free_space": 135.2,
            "atmospheric": 2.1,
            "rain": 1.5,
            "buildings": 0,
            "vegetation": 0,
            "multipath": 6.5
        },
        "performance": {
            "ber": 1e-8,
            "data_rate_mbps": 2.5,
            "availability_percent": 99.5
        },
        "mimo": {
            "capacity_bps_hz": 4.2,
            "diversity_order": 2
        }
    }
}

Performance Optimization

The implementation includes several optimizations:

Optimization Strategies:

  • Terrain data caching with spatial indexing
  • Parallel processing for multi-point analysis
  • Adaptive resolution based on distance
  • Pre-computed lookup tables for antenna patterns
  • Vectorized calculations using NumPy

API Response Format (2025 Update)

The enhanced RF analysis API now returns comprehensive terrain and propagation data suitable for TAKX plugins and advanced visualization:

{
  "enhanced_analysis": {
    "distance_km": 41.69,
    "total_path_loss_db": 121.5,
    "received_power_dbm": -99.7,
    "link_margin_db": 18.5,

    "terrain": {
      "terrain_obstruction": true,
      "los_clearance_m": -50,
      "profile": {
        "distances_km": [0, 0.42, ...41.69],      // 100 points
        "elevations_m": [2879, 2939, ...1630],    // Actual SRTM data
        "latitudes": [40.315, 40.312, ...40.015], // Full path coordinates
        "longitudes": [-105.470, ...],            // For map plotting
        "los_heights_m": [1641, 1841, ...19985],  // Radio beam path
        "obstacles": [                            // Detected peaks
          {
            "distance_km": 5.2,
            "elevation_m": 2939,
            "prominence_m": 89,
            "lat": 40.289,
            "lon": -105.445
          }
        ]
      }
    }
  }
}

Recent Enhancements (January 2025)

Completed Features:
  • Automatic SRTM tile downloading with caching
  • Real-time terrain profile generation (100+ points)
  • Coordinate system fixes for accurate terrain queries
  • Support for nested (TAKX) and flat (legacy) API structures
  • Complete antenna library (10+ types with polarization)
  • Enhanced UI with all RF configuration options
  • Terrain obstacle detection with prominence analysis
  • Line-of-sight and Fresnel zone clearance calculations

Future Enhancements

Planned additions to the RF system:

Roadmap Items:
  • Time-based propagation predictions
  • RF interference analysis and coordination
  • Phased array and adaptive beamforming
  • Machine learning-based propagation prediction
  • Integration with SDR hardware interfaces
  • Multi-balloon mesh network optimization