@mytec: 1.4iter ready for testing
This commit is contained in:
@@ -5,6 +5,10 @@ from pydantic import BaseModel
|
||||
from app.services.terrain_service import terrain_service, TerrainService
|
||||
from app.services.los_service import los_service
|
||||
from app.services.buildings_service import buildings_service, Building
|
||||
from app.services.materials_service import materials_service
|
||||
from app.services.dominant_path_service import dominant_path_service
|
||||
from app.services.street_canyon_service import street_canyon_service, Street
|
||||
from app.services.reflection_service import reflection_service
|
||||
|
||||
|
||||
class CoveragePoint(BaseModel):
|
||||
@@ -15,14 +19,69 @@ class CoveragePoint(BaseModel):
|
||||
has_los: bool
|
||||
terrain_loss: float # dB
|
||||
building_loss: float # dB
|
||||
reflection_gain: float = 0.0 # dB (NEW)
|
||||
|
||||
|
||||
class CoverageSettings(BaseModel):
|
||||
radius: float = 10000 # meters
|
||||
resolution: float = 200 # meters
|
||||
min_signal: float = -120 # dBm threshold
|
||||
|
||||
# Layer toggles
|
||||
use_terrain: bool = True
|
||||
use_buildings: bool = True
|
||||
use_materials: bool = True
|
||||
use_dominant_path: bool = False
|
||||
use_street_canyon: bool = False
|
||||
use_reflections: bool = False
|
||||
|
||||
# Preset
|
||||
preset: Optional[str] = None # fast, standard, detailed, full
|
||||
|
||||
|
||||
# Propagation model presets
|
||||
PRESETS = {
|
||||
"fast": {
|
||||
"use_terrain": True,
|
||||
"use_buildings": False,
|
||||
"use_materials": False,
|
||||
"use_dominant_path": False,
|
||||
"use_street_canyon": False,
|
||||
"use_reflections": False,
|
||||
},
|
||||
"standard": {
|
||||
"use_terrain": True,
|
||||
"use_buildings": True,
|
||||
"use_materials": True,
|
||||
"use_dominant_path": False,
|
||||
"use_street_canyon": False,
|
||||
"use_reflections": False,
|
||||
},
|
||||
"detailed": {
|
||||
"use_terrain": True,
|
||||
"use_buildings": True,
|
||||
"use_materials": True,
|
||||
"use_dominant_path": True,
|
||||
"use_street_canyon": False,
|
||||
"use_reflections": False,
|
||||
},
|
||||
"full": {
|
||||
"use_terrain": True,
|
||||
"use_buildings": True,
|
||||
"use_materials": True,
|
||||
"use_dominant_path": True,
|
||||
"use_street_canyon": True,
|
||||
"use_reflections": True,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def apply_preset(settings: CoverageSettings) -> CoverageSettings:
|
||||
"""Apply preset configuration to settings"""
|
||||
if settings.preset and settings.preset in PRESETS:
|
||||
for key, value in PRESETS[settings.preset].items():
|
||||
setattr(settings, key, value)
|
||||
return settings
|
||||
|
||||
|
||||
class SiteParams(BaseModel):
|
||||
@@ -38,7 +97,8 @@ class SiteParams(BaseModel):
|
||||
|
||||
class CoverageService:
|
||||
"""
|
||||
RF Coverage calculation with terrain and buildings
|
||||
RF Coverage calculation with terrain, buildings, materials,
|
||||
dominant path, street canyon, and reflections
|
||||
"""
|
||||
|
||||
EARTH_RADIUS = 6371000
|
||||
@@ -58,6 +118,9 @@ class CoverageService:
|
||||
|
||||
Returns list of CoveragePoint with RSRP values
|
||||
"""
|
||||
# Apply preset if specified
|
||||
settings = apply_preset(settings)
|
||||
|
||||
points = []
|
||||
|
||||
# Generate grid
|
||||
@@ -67,23 +130,31 @@ class CoverageService:
|
||||
settings.resolution
|
||||
)
|
||||
|
||||
# Fetch buildings for coverage area (if enabled)
|
||||
buildings = []
|
||||
if settings.use_buildings:
|
||||
# Calculate bbox with margin
|
||||
lat_delta = settings.radius / 111000 # ~111km per degree
|
||||
lon_delta = settings.radius / (111000 * np.cos(np.radians(site.lat)))
|
||||
# Calculate bbox for data fetching
|
||||
lat_delta = settings.radius / 111000
|
||||
lon_delta = settings.radius / (111000 * np.cos(np.radians(site.lat)))
|
||||
|
||||
# Fetch buildings for coverage area (if enabled)
|
||||
buildings: List[Building] = []
|
||||
if settings.use_buildings:
|
||||
buildings = await self.buildings.fetch_buildings(
|
||||
site.lat - lat_delta, site.lon - lon_delta,
|
||||
site.lat + lat_delta, site.lon + lon_delta
|
||||
)
|
||||
|
||||
# Fetch streets (if street canyon enabled)
|
||||
streets: List[Street] = []
|
||||
if settings.use_street_canyon:
|
||||
streets = await street_canyon_service.fetch_streets(
|
||||
site.lat - lat_delta, site.lon - lon_delta,
|
||||
site.lat + lat_delta, site.lon + lon_delta
|
||||
)
|
||||
|
||||
# Calculate coverage for each point
|
||||
for lat, lon in grid:
|
||||
point = await self._calculate_point(
|
||||
site, lat, lon,
|
||||
settings, buildings
|
||||
settings, buildings, streets
|
||||
)
|
||||
|
||||
if point.rsrp >= settings.min_signal:
|
||||
@@ -103,6 +174,9 @@ class CoverageService:
|
||||
if not sites:
|
||||
return []
|
||||
|
||||
# Apply preset once
|
||||
settings = apply_preset(settings)
|
||||
|
||||
# Get all individual coverages
|
||||
all_coverages = await asyncio.gather(*[
|
||||
self.calculate_coverage(site, settings)
|
||||
@@ -155,9 +229,10 @@ class CoverageService:
|
||||
site: SiteParams,
|
||||
lat: float, lon: float,
|
||||
settings: CoverageSettings,
|
||||
buildings: List[Building]
|
||||
buildings: List[Building],
|
||||
streets: List[Street]
|
||||
) -> CoveragePoint:
|
||||
"""Calculate RSRP at a single point"""
|
||||
"""Calculate RSRP at a single point with all propagation models"""
|
||||
|
||||
# Distance
|
||||
distance = TerrainService.haversine_distance(site.lat, site.lon, lat, lon)
|
||||
@@ -194,25 +269,85 @@ class CoverageService:
|
||||
clearance = los_result["clearance"]
|
||||
terrain_loss = self._diffraction_loss(clearance, site.frequency)
|
||||
|
||||
# Building loss
|
||||
# Building loss (with optional material awareness)
|
||||
building_loss = 0.0
|
||||
|
||||
if settings.use_buildings and buildings:
|
||||
for building in buildings:
|
||||
intersection = self.buildings.line_intersects_building(
|
||||
site.lat, site.lon, site.height + await self.terrain.get_elevation(site.lat, site.lon),
|
||||
lat, lon, 1.5 + await self.terrain.get_elevation(lat, lon),
|
||||
building
|
||||
)
|
||||
if intersection is not None:
|
||||
# Building penetration loss (~20dB for concrete)
|
||||
building_loss += 20.0
|
||||
has_los = False
|
||||
break # One building is enough
|
||||
if settings.use_materials:
|
||||
# Material-aware building loss
|
||||
for building in buildings:
|
||||
intersection = self.buildings.line_intersects_building(
|
||||
site.lat, site.lon, site.height + await self.terrain.get_elevation(site.lat, site.lon),
|
||||
lat, lon, 1.5 + await self.terrain.get_elevation(lat, lon),
|
||||
building
|
||||
)
|
||||
if intersection is not None:
|
||||
material = materials_service.detect_material(building.tags)
|
||||
building_loss += materials_service.get_penetration_loss(
|
||||
material, site.frequency
|
||||
)
|
||||
has_los = False
|
||||
break # One building is enough
|
||||
else:
|
||||
# Simple building loss (legacy behavior)
|
||||
for building in buildings:
|
||||
intersection = self.buildings.line_intersects_building(
|
||||
site.lat, site.lon, site.height + await self.terrain.get_elevation(site.lat, site.lon),
|
||||
lat, lon, 1.5 + await self.terrain.get_elevation(lat, lon),
|
||||
building
|
||||
)
|
||||
if intersection is not None:
|
||||
building_loss += 20.0 # Default concrete
|
||||
has_los = False
|
||||
break
|
||||
|
||||
# Calculate RSRP
|
||||
# RSRP = Tx Power + Tx Gain - Path Loss - Antenna Loss - Terrain Loss - Building Loss
|
||||
rsrp = site.power + site.gain - path_loss - antenna_loss - terrain_loss - building_loss
|
||||
# Dominant path analysis (find best route)
|
||||
if settings.use_dominant_path and buildings:
|
||||
paths = await dominant_path_service.find_dominant_paths(
|
||||
site.lat, site.lon, site.height,
|
||||
lat, lon, 1.5,
|
||||
site.frequency, buildings
|
||||
)
|
||||
if paths:
|
||||
best_path = paths[0]
|
||||
# Use best path's loss if it's better
|
||||
if best_path.is_valid and best_path.path_loss < (path_loss + terrain_loss + building_loss):
|
||||
path_loss = best_path.path_loss
|
||||
terrain_loss = 0
|
||||
building_loss = 0
|
||||
has_los = best_path.path_type == "direct" and not best_path.materials_crossed
|
||||
|
||||
# Street canyon model
|
||||
if settings.use_street_canyon and streets:
|
||||
canyon_loss, street_path = await street_canyon_service.calculate_street_canyon_loss(
|
||||
site.lat, site.lon, site.height,
|
||||
lat, lon, 1.5,
|
||||
site.frequency, streets
|
||||
)
|
||||
# Use canyon loss if better than current total
|
||||
if canyon_loss < (path_loss + terrain_loss + building_loss):
|
||||
path_loss = canyon_loss
|
||||
terrain_loss = 0
|
||||
building_loss = 0
|
||||
|
||||
# Reflections
|
||||
reflection_gain = 0.0
|
||||
if settings.use_reflections and buildings:
|
||||
reflection_paths = await reflection_service.find_reflection_paths(
|
||||
site.lat, site.lon, site.height,
|
||||
lat, lon, 1.5,
|
||||
site.frequency, buildings
|
||||
)
|
||||
if reflection_paths:
|
||||
# Combine direct and reflected signals
|
||||
direct_rsrp = site.power + site.gain - path_loss - antenna_loss - terrain_loss - building_loss
|
||||
combined_rsrp = reflection_service.combine_paths(
|
||||
direct_rsrp, reflection_paths, site.power + site.gain
|
||||
)
|
||||
reflection_gain = max(0, combined_rsrp - direct_rsrp)
|
||||
|
||||
# Final RSRP
|
||||
rsrp = site.power + site.gain - path_loss - antenna_loss - terrain_loss - building_loss + reflection_gain
|
||||
|
||||
return CoveragePoint(
|
||||
lat=lat,
|
||||
@@ -221,7 +356,8 @@ class CoverageService:
|
||||
distance=distance,
|
||||
has_los=has_los,
|
||||
terrain_loss=terrain_loss,
|
||||
building_loss=building_loss
|
||||
building_loss=building_loss,
|
||||
reflection_gain=reflection_gain
|
||||
)
|
||||
|
||||
def _okumura_hata(
|
||||
@@ -311,9 +447,6 @@ class CoverageService:
|
||||
return 0.0 # No obstruction
|
||||
|
||||
# Fresnel parameter approximation
|
||||
# v ~ clearance * sqrt(2 / (lambda * d))
|
||||
# Simplified: use clearance directly
|
||||
|
||||
v = abs(clearance) / 10 # Normalize
|
||||
|
||||
# Knife-edge loss approximation
|
||||
|
||||
Reference in New Issue
Block a user