# 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:** ```python 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:** ```python 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:** ```python 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:** ```python # 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:** ```python 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:** ```typescript // Weather section