16 KiB
RFCP Iteration 1.6.1: Extra Factors
Date: January 31, 2025
Type: Backend Enhancement
Estimated: 4-6 hours
Location: /opt/rfcp/backend/
Priority: Low — nice to have for realism
🎯 Goal
Add weather effects, indoor penetration loss, and atmospheric absorption for more realistic RF propagation modeling.
✅ Features
1. Rain Attenuation (ITU-R P.838)
Theory: Rain causes signal attenuation, especially at higher frequencies (>10 GHz). Even at LTE frequencies (700-2600 MHz), heavy rain can add 1-3 dB loss.
app/services/weather_service.py:
from typing import Optional
import math
class WeatherService:
"""ITU-R P.838 rain attenuation model"""
# ITU-R P.838-3 coefficients for horizontal polarization
# Format: (frequency_GHz, k, alpha)
RAIN_COEFFICIENTS = {
0.7: (0.0000387, 0.912),
1.0: (0.0000887, 0.949),
1.8: (0.000292, 1.021),
2.1: (0.000425, 1.052),
2.6: (0.000683, 1.091),
3.5: (0.00138, 1.149),
5.0: (0.00361, 1.206),
10.0: (0.0245, 1.200),
20.0: (0.0906, 1.099),
30.0: (0.175, 1.021),
}
def calculate_rain_attenuation(
self,
frequency_mhz: float,
distance_km: float,
rain_rate: float, # mm/h
polarization: str = "horizontal"
) -> float:
"""
Calculate rain attenuation in dB
Args:
frequency_mhz: Frequency in MHz
distance_km: Path length in km
rain_rate: Rain rate in mm/h (0=none, 5=light, 25=moderate, 50=heavy)
polarization: "horizontal" or "vertical"
Returns:
Attenuation in dB
"""
if rain_rate <= 0:
return 0.0
freq_ghz = frequency_mhz / 1000
# Get interpolated coefficients
k, alpha = self._get_coefficients(freq_ghz)
# Specific attenuation (dB/km)
gamma_r = k * (rain_rate ** alpha)
# Effective path length reduction for longer paths
# Rain cells are typically 2-5 km
if distance_km > 2:
reduction_factor = 1 / (1 + distance_km / 35)
effective_distance = distance_km * reduction_factor
else:
effective_distance = distance_km
attenuation = gamma_r * effective_distance
return min(attenuation, 30.0) # Cap at 30 dB
def _get_coefficients(self, freq_ghz: float) -> tuple[float, float]:
"""Interpolate rain coefficients for frequency"""
freqs = sorted(self.RAIN_COEFFICIENTS.keys())
# Find surrounding frequencies
if freq_ghz <= freqs[0]:
return self.RAIN_COEFFICIENTS[freqs[0]]
if freq_ghz >= freqs[-1]:
return self.RAIN_COEFFICIENTS[freqs[-1]]
for i in range(len(freqs) - 1):
if freqs[i] <= freq_ghz <= freqs[i + 1]:
f1, f2 = freqs[i], freqs[i + 1]
k1, a1 = self.RAIN_COEFFICIENTS[f1]
k2, a2 = self.RAIN_COEFFICIENTS[f2]
# Linear interpolation
t = (freq_ghz - f1) / (f2 - f1)
k = k1 + t * (k2 - k1)
alpha = a1 + t * (a2 - a1)
return k, alpha
return self.RAIN_COEFFICIENTS[freqs[0]]
@staticmethod
def rain_rate_from_description(description: str) -> float:
"""Convert rain description to rate"""
rates = {
"none": 0.0,
"drizzle": 2.5,
"light": 5.0,
"moderate": 12.5,
"heavy": 25.0,
"very_heavy": 50.0,
"extreme": 100.0,
}
return rates.get(description.lower(), 0.0)
weather_service = WeatherService()
2. Indoor Penetration Loss (ITU-R P.2109)
Theory: Signals lose strength when entering buildings. Loss depends on building type, frequency, and wall materials.
app/services/indoor_service.py:
from typing import Optional
import math
class IndoorService:
"""ITU-R P.2109 building entry loss model"""
# Building Entry Loss (BEL) by construction type at 2 GHz
# Format: (median_loss_dB, std_dev_dB)
BUILDING_TYPES = {
"none": (0.0, 0.0), # Outdoor only
"light": (8.0, 4.0), # Wood frame, large windows
"medium": (15.0, 6.0), # Brick, standard windows
"heavy": (22.0, 8.0), # Concrete, small windows
"basement": (30.0, 10.0), # Underground
"vehicle": (6.0, 3.0), # Inside car
"train": (20.0, 5.0), # Inside train
}
# Frequency correction factor (dB per octave above 2 GHz)
FREQ_CORRECTION = 2.5
def calculate_indoor_loss(
self,
frequency_mhz: float,
building_type: str = "medium",
floor_number: int = 0,
depth_m: float = 0.0,
) -> float:
"""
Calculate building entry/indoor penetration loss
Args:
frequency_mhz: Frequency in MHz
building_type: Type of building construction
floor_number: Floor number (0=ground, negative=basement)
depth_m: Distance from exterior wall in meters
Returns:
Loss in dB
"""
if building_type == "none":
return 0.0
base_loss, _ = self.BUILDING_TYPES.get(building_type, (15.0, 6.0))
# Frequency correction
freq_ghz = frequency_mhz / 1000
if freq_ghz > 2.0:
octaves = math.log2(freq_ghz / 2.0)
freq_correction = self.FREQ_CORRECTION * octaves
else:
freq_correction = 0.0
# Floor correction (higher floors = less loss due to better angle)
if floor_number > 0:
floor_correction = -1.5 * min(floor_number, 10)
elif floor_number < 0:
# Basement - additional loss
floor_correction = 5.0 * abs(floor_number)
else:
floor_correction = 0.0
# Depth correction (signal attenuates inside building)
# Approximately 0.5 dB per meter for first 10m
depth_correction = 0.5 * min(depth_m, 20)
total_loss = base_loss + freq_correction + floor_correction + depth_correction
return max(0.0, min(total_loss, 50.0)) # Clamp 0-50 dB
def calculate_outdoor_to_indoor_coverage(
self,
outdoor_rsrp: float,
building_type: str,
frequency_mhz: float,
) -> float:
"""Calculate expected indoor RSRP from outdoor signal"""
indoor_loss = self.calculate_indoor_loss(frequency_mhz, building_type)
return outdoor_rsrp - indoor_loss
indoor_service = IndoorService()
3. Atmospheric Absorption (ITU-R P.676)
Theory: Oxygen and water vapor absorb RF energy. Significant at mmWave (>10 GHz), minor at LTE bands.
app/services/atmospheric_service.py:
import math
class AtmosphericService:
"""ITU-R P.676 atmospheric absorption model"""
# Simplified model for frequencies < 50 GHz
# Standard atmosphere: T=15°C, P=1013 hPa, humidity=50%
def calculate_atmospheric_loss(
self,
frequency_mhz: float,
distance_km: float,
temperature_c: float = 15.0,
humidity_percent: float = 50.0,
altitude_m: float = 0.0,
) -> float:
"""
Calculate atmospheric absorption loss
Args:
frequency_mhz: Frequency in MHz
distance_km: Path length in km
temperature_c: Temperature in Celsius
humidity_percent: Relative humidity (0-100)
altitude_m: Altitude above sea level
Returns:
Loss in dB
"""
freq_ghz = frequency_mhz / 1000
# Below 1 GHz - negligible
if freq_ghz < 1.0:
return 0.0
# Calculate specific attenuation (dB/km)
gamma = self._specific_attenuation(freq_ghz, temperature_c, humidity_percent)
# Altitude correction (less atmosphere at higher altitudes)
altitude_factor = math.exp(-altitude_m / 8500) # Scale height ~8.5km
loss = gamma * distance_km * altitude_factor
return min(loss, 20.0) # Cap for reasonable distances
def _specific_attenuation(
self,
freq_ghz: float,
temperature_c: float,
humidity_percent: float,
) -> float:
"""
Calculate specific attenuation in dB/km
Simplified ITU-R P.676 model
"""
# Water vapor density (g/m³) - simplified
# Saturation vapor pressure (hPa)
es = 6.1121 * math.exp((18.678 - temperature_c / 234.5) *
(temperature_c / (257.14 + temperature_c)))
rho = (humidity_percent / 100) * es * 216.7 / (273.15 + temperature_c)
# Oxygen absorption (dominant at 60 GHz, minor below 10 GHz)
if freq_ghz < 10:
gamma_o = 0.001 * freq_ghz ** 2 # Very small
elif freq_ghz < 57:
gamma_o = 0.001 * (freq_ghz / 10) ** 2.5
else:
# Near 60 GHz resonance
gamma_o = 15.0 # Peak absorption
# Water vapor absorption (peaks at 22 GHz and 183 GHz)
if freq_ghz < 10:
gamma_w = 0.0001 * rho * freq_ghz ** 2
elif freq_ghz < 50:
gamma_w = 0.001 * rho * (freq_ghz / 22) ** 2
else:
gamma_w = 0.01 * rho
return gamma_o + gamma_w
@staticmethod
def get_weather_description(loss_db: float) -> str:
"""Describe atmospheric conditions based on loss"""
if loss_db < 0.1:
return "clear"
elif loss_db < 0.5:
return "normal"
elif loss_db < 2.0:
return "humid"
else:
return "foggy"
atmospheric_service = AtmosphericService()
4. Integration into Coverage Service
Update app/services/coverage_service.py:
# Add imports
from app.services.weather_service import weather_service
from app.services.indoor_service import indoor_service
from app.services.atmospheric_service import atmospheric_service
# Update CoverageSettings
class CoverageSettings(BaseModel):
# ... existing fields ...
# Weather
rain_rate: float = 0.0 # mm/h (0=none, 5=light, 25=heavy)
# Indoor
indoor_loss_type: str = "none" # none, light, medium, heavy
# Atmospheric
use_atmospheric: bool = False
temperature_c: float = 15.0
humidity_percent: float = 50.0
# Update _calculate_point()
async def _calculate_point(self, ...):
# ... existing calculations ...
# Rain attenuation
rain_loss = 0.0
if settings.rain_rate > 0:
rain_loss = weather_service.calculate_rain_attenuation(
site.frequency,
distance / 1000, # km
settings.rain_rate
)
# Indoor penetration
indoor_loss = 0.0
if settings.indoor_loss_type != "none":
indoor_loss = indoor_service.calculate_indoor_loss(
site.frequency,
settings.indoor_loss_type
)
# Atmospheric absorption
atmo_loss = 0.0
if settings.use_atmospheric:
atmo_loss = atmospheric_service.calculate_atmospheric_loss(
site.frequency,
distance / 1000,
settings.temperature_c,
settings.humidity_percent
)
# Final RSRP
rsrp = (site.power + site.gain - path_loss - antenna_loss
- terrain_loss - building_loss - vegetation_loss
- rain_loss - indoor_loss - atmo_loss
+ reflection_gain + water_reflection_gain)
return CoveragePoint(
# ... existing fields ...
rain_loss=rain_loss,
indoor_loss=indoor_loss,
atmospheric_loss=atmo_loss,
)
5. Update API Response
Update CoveragePoint model:
class CoveragePoint(BaseModel):
lat: float
lon: float
rsrp: float
distance: float
has_los: bool
terrain_loss: float = 0.0
building_loss: float = 0.0
vegetation_loss: float = 0.0
reflection_gain: float = 0.0
rain_loss: float = 0.0 # NEW
indoor_loss: float = 0.0 # NEW
atmospheric_loss: float = 0.0 # NEW
6. Frontend UI Updates
Add to CoverageSettings panel:
// Weather section
<div className="settings-section">
<h4>Weather Conditions</h4>
<select
value={settings.rain_rate}
onChange={(e) => updateSettings({ rain_rate: Number(e.target.value) })}
>
<option value={0}>No Rain</option>
<option value={2.5}>Drizzle</option>
<option value={5}>Light Rain</option>
<option value={12.5}>Moderate Rain</option>
<option value={25}>Heavy Rain</option>
<option value={50}>Very Heavy Rain</option>
</select>
</div>
// Indoor section
<div className="settings-section">
<h4>Indoor Coverage</h4>
<select
value={settings.indoor_loss_type}
onChange={(e) => updateSettings({ indoor_loss_type: e.target.value })}
>
<option value="none">Outdoor Only</option>
<option value="light">Light Building (wood, glass)</option>
<option value="medium">Medium Building (brick)</option>
<option value="heavy">Heavy Building (concrete)</option>
<option value="vehicle">Inside Vehicle</option>
</select>
</div>
📁 Files Summary
New Files:
backend/app/services/
├── weather_service.py # Rain attenuation
├── indoor_service.py # Building entry loss
└── atmospheric_service.py # Atmospheric absorption
Modified Files:
backend/app/
├── services/coverage_service.py # Integration
├── api/routes/coverage.py # New settings
└── models/coverage.py # New fields
frontend/src/
├── types/coverage.ts # New fields
├── services/api.ts # New settings
├── store/coverage.ts # New state
└── components/panels/CoverageSettingsPanel.tsx # UI
🧪 Testing
# Test rain attenuation
curl -X POST "https://api.rfcp.eliah.one/api/coverage/calculate" \
-H "Content-Type: application/json" \
-d '{
"sites":[{"lat":48.46,"lon":35.05,"height":30,"power":43,"gain":15,"frequency":1800}],
"settings":{
"radius":1000,
"resolution":100,
"preset":"fast",
"rain_rate":25
}
}'
# Test indoor penetration
curl -X POST "https://api.rfcp.eliah.one/api/coverage/calculate" \
-H "Content-Type: application/json" \
-d '{
"sites":[{"lat":48.46,"lon":35.05,"height":30,"power":43,"gain":15,"frequency":1800}],
"settings":{
"radius":1000,
"resolution":100,
"preset":"fast",
"indoor_loss_type":"medium"
}
}'
✅ Success Criteria
- Rain attenuation calculated correctly (0 dB at 0 mm/h, ~5 dB at 25 mm/h for 1km)
- Indoor loss applied uniformly (~15 dB for medium building)
- Atmospheric loss minimal at LTE frequencies (<0.5 dB for 10km)
- Frontend shows weather/indoor selectors
- All presets still work
- Tests pass
📝 Notes
- Rain attenuation most significant at mmWave (5G NR FR2)
- Indoor loss is uniform for coverage area (all points affected equally)
- Atmospheric loss negligible below 6 GHz for typical distances
- These are "what-if" analysis features, not real-time weather integration
🔜 Next: 2.1 — Desktop Installer
After 1.6.1:
- Electron packaging
- Offline mode with local data
- GPU acceleration for fast calculations
Ready for Claude Code 🚀