diff --git a/.claude/settings.local.json b/.claude/settings.local.json
index 61f41ab..3f81947 100644
--- a/.claude/settings.local.json
+++ b/.claude/settings.local.json
@@ -10,7 +10,16 @@
"Bash(python:*)",
"Bash(pip --version:*)",
"Bash(pip install:*)",
- "Bash(npx vite build:*)"
+ "Bash(npx vite build:*)",
+ "Bash(git:*)",
+ "Bash(cat:*)",
+ "Bash(ls:*)",
+ "Bash(cd:*)",
+ "Bash(mkdir:*)",
+ "Bash(cp:*)",
+ "Bash(mv:*)",
+ "Read(*)",
+ "Write(*)"
]
}
-}
+}
\ No newline at end of file
diff --git a/RFCP-Iteration-1.6.1-Extra-Factors.md b/RFCP-Iteration-1.6.1-Extra-Factors.md
new file mode 100644
index 0000000..2cc8f58
--- /dev/null
+++ b/RFCP-Iteration-1.6.1-Extra-Factors.md
@@ -0,0 +1,549 @@
+# 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
+
+
Weather Conditions
+
+
+
+
+// Indoor section
+
+
Indoor Coverage
+
+
+
+```
+
+---
+
+## ๐ 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
+
+```bash
+# 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** ๐
diff --git a/backend/app/api/routes/coverage.py b/backend/app/api/routes/coverage.py
index 582a1cb..5b7f236 100644
--- a/backend/app/api/routes/coverage.py
+++ b/backend/app/api/routes/coverage.py
@@ -96,6 +96,9 @@ async def calculate_coverage(request: CoverageRequest) -> CoverageResponse:
"points_with_terrain_loss": sum(1 for p in points if p.terrain_loss > 0),
"points_with_reflection_gain": sum(1 for p in points if p.reflection_gain > 0),
"points_with_vegetation_loss": sum(1 for p in points if p.vegetation_loss > 0),
+ "points_with_rain_loss": sum(1 for p in points if p.rain_loss > 0),
+ "points_with_indoor_loss": sum(1 for p in points if p.indoor_loss > 0),
+ "points_with_atmospheric_loss": sum(1 for p in points if p.atmospheric_loss > 0),
}
return CoverageResponse(
@@ -183,5 +186,11 @@ def _get_active_models(settings: CoverageSettings) -> List[str]:
models.append("water_reflection")
if settings.use_vegetation:
models.append("vegetation")
+ if settings.rain_rate > 0:
+ models.append("rain_attenuation")
+ if settings.indoor_loss_type != "none":
+ models.append("indoor_penetration")
+ if settings.use_atmospheric:
+ models.append("atmospheric")
return models
diff --git a/backend/app/services/atmospheric_service.py b/backend/app/services/atmospheric_service.py
new file mode 100644
index 0000000..8a24013
--- /dev/null
+++ b/backend/app/services/atmospheric_service.py
@@ -0,0 +1,98 @@
+import math
+
+
+class AtmosphericService:
+ """ITU-R P.676 atmospheric absorption model"""
+
+ # Simplified model for frequencies < 50 GHz
+ # Standard atmosphere: T=15C, 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/m3) - 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()
diff --git a/backend/app/services/coverage_service.py b/backend/app/services/coverage_service.py
index e239ea0..b438ca2 100644
--- a/backend/app/services/coverage_service.py
+++ b/backend/app/services/coverage_service.py
@@ -12,6 +12,9 @@ from app.services.reflection_service import reflection_service
from app.services.spatial_index import get_spatial_index, SpatialIndex
from app.services.water_service import water_service, WaterBody
from app.services.vegetation_service import vegetation_service, VegetationArea
+from app.services.weather_service import weather_service
+from app.services.indoor_service import indoor_service
+from app.services.atmospheric_service import atmospheric_service
class CoveragePoint(BaseModel):
@@ -24,6 +27,9 @@ class CoveragePoint(BaseModel):
building_loss: float # dB
reflection_gain: float = 0.0 # dB
vegetation_loss: float = 0.0 # dB
+ rain_loss: float = 0.0 # dB
+ indoor_loss: float = 0.0 # dB
+ atmospheric_loss: float = 0.0 # dB
class CoverageSettings(BaseModel):
@@ -44,6 +50,17 @@ class CoverageSettings(BaseModel):
# Vegetation season
season: str = "summer"
+ # Weather
+ rain_rate: float = 0.0 # mm/h (0=none, 5=light, 25=heavy)
+
+ # Indoor
+ indoor_loss_type: str = "none" # none, light, medium, heavy, vehicle
+
+ # Atmospheric
+ use_atmospheric: bool = False
+ temperature_c: float = 15.0
+ humidity_percent: float = 50.0
+
# Preset
preset: Optional[str] = None # fast, standard, detailed, full
@@ -419,9 +436,38 @@ class CoverageService:
if is_over_water:
reflection_gain = 3.0 # ~3dB boost over water
+ # 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 loss
+ 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 - veg_loss + reflection_gain)
+ - terrain_loss - building_loss - veg_loss
+ - rain_loss - indoor_loss - atmo_loss
+ + reflection_gain)
return CoveragePoint(
lat=lat,
@@ -432,7 +478,10 @@ class CoverageService:
terrain_loss=terrain_loss,
building_loss=building_loss,
reflection_gain=reflection_gain,
- vegetation_loss=veg_loss
+ vegetation_loss=veg_loss,
+ rain_loss=rain_loss,
+ indoor_loss=indoor_loss,
+ atmospheric_loss=atmo_loss,
)
def _okumura_hata(
diff --git a/backend/app/services/indoor_service.py b/backend/app/services/indoor_service.py
new file mode 100644
index 0000000..be2a0af
--- /dev/null
+++ b/backend/app/services/indoor_service.py
@@ -0,0 +1,82 @@
+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()
diff --git a/backend/app/services/weather_service.py b/backend/app/services/weather_service.py
new file mode 100644
index 0000000..fd66919
--- /dev/null
+++ b/backend/app/services/weather_service.py
@@ -0,0 +1,102 @@
+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
+ ) -> 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)
+
+ 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()
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index b604c7a..b847623 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -803,6 +803,118 @@ export default function App() {
)}
+
+ {/* Atmospheric absorption toggle */}
+
+ {settings.use_atmospheric && (
+
+
+
+
+
+
+
+
+
+
+ )}
+
+ {/* Weather / Rain section */}
+
+
Environment
+
+
+
+
+
+
+ {/* Indoor penetration section */}
+
+
+
+
)}
diff --git a/frontend/src/services/api.ts b/frontend/src/services/api.ts
index b9f840d..dc93047 100644
--- a/frontend/src/services/api.ts
+++ b/frontend/src/services/api.ts
@@ -31,6 +31,11 @@ export interface ApiCoverageSettings {
use_water_reflection?: boolean;
use_vegetation?: boolean;
season?: 'summer' | 'winter' | 'spring' | 'autumn';
+ rain_rate?: number;
+ indoor_loss_type?: string;
+ use_atmospheric?: boolean;
+ temperature_c?: number;
+ humidity_percent?: number;
}
export interface CoverageRequest {
@@ -50,6 +55,9 @@ export interface ApiCoveragePoint {
building_loss: number;
reflection_gain: number;
vegetation_loss: number;
+ rain_loss: number;
+ indoor_loss: number;
+ atmospheric_loss: number;
}
export interface ApiCoverageStats {
@@ -61,6 +69,9 @@ export interface ApiCoverageStats {
points_with_terrain_loss: number;
points_with_reflection_gain: number;
points_with_vegetation_loss: number;
+ points_with_rain_loss: number;
+ points_with_indoor_loss: number;
+ points_with_atmospheric_loss: number;
}
export interface CoverageResponse {
diff --git a/frontend/src/store/coverage.ts b/frontend/src/store/coverage.ts
index 0e54f4e..4f79f34 100644
--- a/frontend/src/store/coverage.ts
+++ b/frontend/src/store/coverage.ts
@@ -44,6 +44,14 @@ export const useCoverageStore = create((set, get) => ({
use_water_reflection: false,
use_vegetation: false,
season: 'summer',
+ // Weather
+ rain_rate: 0,
+ // Indoor
+ indoor_loss_type: 'none',
+ // Atmospheric
+ use_atmospheric: false,
+ temperature_c: 15,
+ humidity_percent: 50,
},
heatmapVisible: true,
error: null,
@@ -107,6 +115,11 @@ export const useCoverageStore = create((set, get) => ({
use_water_reflection: settings.use_water_reflection,
use_vegetation: settings.use_vegetation,
season: settings.season,
+ rain_rate: settings.rain_rate,
+ indoor_loss_type: settings.indoor_loss_type,
+ use_atmospheric: settings.use_atmospheric,
+ temperature_c: settings.temperature_c,
+ humidity_percent: settings.humidity_percent,
},
});
@@ -122,6 +135,9 @@ export const useCoverageStore = create((set, get) => ({
building_loss: p.building_loss,
reflection_gain: p.reflection_gain,
vegetation_loss: p.vegetation_loss,
+ rain_loss: p.rain_loss,
+ indoor_loss: p.indoor_loss,
+ atmospheric_loss: p.atmospheric_loss,
})),
calculationTime: response.computation_time,
totalPoints: response.count,
diff --git a/frontend/src/types/coverage.ts b/frontend/src/types/coverage.ts
index 6ee887d..ff5782d 100644
--- a/frontend/src/types/coverage.ts
+++ b/frontend/src/types/coverage.ts
@@ -10,6 +10,9 @@ export interface CoveragePoint {
building_loss?: number; // dB building penetration loss
reflection_gain?: number; // dB reflection signal gain
vegetation_loss?: number; // dB vegetation attenuation
+ rain_loss?: number; // dB rain attenuation
+ indoor_loss?: number; // dB indoor penetration loss
+ atmospheric_loss?: number; // dB atmospheric absorption
}
export interface CoverageResult {
@@ -31,6 +34,9 @@ export interface CoverageApiStats {
points_with_terrain_loss: number;
points_with_reflection_gain: number;
points_with_vegetation_loss: number;
+ points_with_rain_loss: number;
+ points_with_indoor_loss: number;
+ points_with_atmospheric_loss: number;
}
export interface CoverageSettings {
@@ -50,6 +56,14 @@ export interface CoverageSettings {
use_water_reflection?: boolean;
use_vegetation?: boolean;
season?: 'summer' | 'winter' | 'spring' | 'autumn';
+ // Weather
+ rain_rate?: number; // mm/h (0=none, 5=light, 25=heavy)
+ // Indoor
+ indoor_loss_type?: string; // none, light, medium, heavy, vehicle
+ // Atmospheric
+ use_atmospheric?: boolean;
+ temperature_c?: number;
+ humidity_percent?: number;
}
export interface GridPoint {