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.
The foundation of balloon communication is line-of-sight (LOS) propagation, modified by Earth's curvature and atmospheric refraction.
The geometric horizon distance considering Earth's curvature:
Maximum communication range between balloon and ground station:
The Friis transmission equation gives basic path loss:
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 gases absorb RF energy, particularly oxygen and water vapor:
Total path attenuation requires integration through varying atmospheric density:
The refractive index varies with altitude:
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)
Atmospheric turbulence causes signal amplitude and phase fluctuations:
Typical $C_n^2$ values:
Two-ray model for ground reflection:
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}}$$For beyond-horizon communication:
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)
Complete link budget calculation includes all propagation effects:
class LinkBudgetCalculator:
"""Complete link budget analysis"""
def __init__(self):
self.k_boltzmann = 1.38e-23 # J/K
self.noise_temp = 290 # K
def calculate_link_budget(self, link_params, propagation_losses):
"""Calculate complete link budget"""
# Transmitter parameters
P_tx_dbm = 10 * np.log10(link_params['tx_power_w'] * 1000)
G_tx_dbi = link_params['tx_antenna_gain']
# Receiver parameters
G_rx_dbi = link_params['rx_antenna_gain']
NF_db = link_params['noise_figure']
bandwidth_hz = link_params['bandwidth']
# Losses
L_fs = propagation_losses['free_space']
L_atm = propagation_losses['atmospheric']
L_rain = propagation_losses.get('rain', 0)
L_pointing = propagation_losses.get('pointing', 0)
# Cable and connector losses
L_tx_cable = link_params.get('tx_cable_loss', 0)
L_rx_cable = link_params.get('rx_cable_loss', 0)
# Multipath gain (can be positive or negative)
G_multipath = propagation_losses.get('multipath_gain', 0)
# Received power
P_rx_dbm = (P_tx_dbm + G_tx_dbi + G_rx_dbi - L_fs - L_atm -
L_rain - L_pointing - L_tx_cable - L_rx_cable + G_multipath)
# Noise floor
N_0_dbm = -174 # dBm/Hz at 290K
N_total_dbm = N_0_dbm + 10*np.log10(bandwidth_hz) + NF_db
# Signal-to-noise ratio
SNR_db = P_rx_dbm - N_total_dbm
# Margin calculation
required_snr = link_params.get('required_snr', 10)
link_margin = SNR_db - required_snr
return {
'tx_eirp': P_tx_dbm + G_tx_dbi - L_tx_cable,
'path_loss': L_fs + L_atm + L_rain,
'rx_power': P_rx_dbm,
'noise_floor': N_total_dbm,
'snr': SNR_db,
'link_margin': link_margin,
'max_data_rate': self.shannon_capacity(SNR_db, bandwidth_hz)
}
def shannon_capacity(self, snr_db, bandwidth_hz):
"""Calculate theoretical maximum data rate"""
snr_linear = 10**(snr_db / 10)
capacity_bps = bandwidth_hz * np.log2(1 + snr_linear)
return capacity_bps
def calculate_fade_margin(self, availability_percent):
"""Calculate required fade margin for given availability"""
# Simplified model - actual calculation depends on climate
if availability_percent >= 99.99:
fade_margin = 15 # dB
elif availability_percent >= 99.9:
fade_margin = 10
elif availability_percent >= 99:
fade_margin = 6
else:
fade_margin = 3
return fade_margin
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
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
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
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
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.
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)
3D radiation patterns with polarization mismatch:
The implementation supports multiple antenna configurations:
MIMO capacity calculation:
$$C = \log_2 \det\left(I_{N_r} + \frac{\rho}{N_t} HH^*\right) \text{ bps/Hz}$$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)
Using ITU-R P.838 coefficients:
Tropospheric scintillation standard deviation:
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)
)
The RF analysis system features a comprehensive web interface:
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
}
}
}
The implementation includes several optimizations:
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
}
]
}
}
}
}
Planned additions to the RF system: