Files
rfcp/RFCP-Iteration-1.6.1-Extra-Factors.md

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 🚀