Phase 2.2: performance optimizations, debug tools, app close fix
This commit is contained in:
@@ -1,6 +1,44 @@
|
||||
import math
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import threading
|
||||
import numpy as np
|
||||
import asyncio
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from typing import List, Optional, Tuple
|
||||
|
||||
|
||||
_coverage_log_file = None
|
||||
|
||||
def _clog(msg: str):
|
||||
"""Coverage debug log — always flushed, with timestamp and thread name.
|
||||
Writes to stdout, stderr, AND a file so output is always available."""
|
||||
global _coverage_log_file
|
||||
ts = time.strftime('%H:%M:%S')
|
||||
thr = threading.current_thread().name
|
||||
line = f"[COVERAGE {ts}] [{thr}] {msg}"
|
||||
print(line, flush=True)
|
||||
# Backup: also write to stderr in case stdout is broken
|
||||
try:
|
||||
sys.stderr.write(line + '\n')
|
||||
sys.stderr.flush()
|
||||
except Exception:
|
||||
pass
|
||||
# Backup: also write to a file
|
||||
try:
|
||||
if _coverage_log_file is None:
|
||||
log_dir = os.environ.get('RFCP_DATA_PATH', './data')
|
||||
os.makedirs(log_dir, exist_ok=True)
|
||||
log_path = os.path.join(log_dir, 'coverage-debug.log')
|
||||
_coverage_log_file = open(log_path, 'a')
|
||||
_coverage_log_file.write(f"\n{'='*60}\n")
|
||||
_coverage_log_file.write(f"[COVERAGE {ts}] Log started\n")
|
||||
_coverage_log_file.flush()
|
||||
_coverage_log_file.write(line + '\n')
|
||||
_coverage_log_file.flush()
|
||||
except Exception:
|
||||
pass
|
||||
from pydantic import BaseModel
|
||||
from app.services.terrain_service import terrain_service, TerrainService
|
||||
from app.services.los_service import los_service
|
||||
@@ -142,6 +180,100 @@ class CoverageService:
|
||||
self.buildings = buildings_service
|
||||
self.los = los_service
|
||||
|
||||
async def _fetch_osm_grid_aligned(
|
||||
self,
|
||||
min_lat: float, min_lon: float,
|
||||
max_lat: float, max_lon: float,
|
||||
settings: CoverageSettings
|
||||
) -> dict:
|
||||
"""
|
||||
Fetch OSM data using 1-degree grid-aligned cells.
|
||||
|
||||
This ensures cache keys match the region download grid,
|
||||
so pre-cached data is actually used.
|
||||
"""
|
||||
t0 = time.time()
|
||||
|
||||
lat_start = int(math.floor(min_lat))
|
||||
lat_end = int(math.floor(max_lat))
|
||||
lon_start = int(math.floor(min_lon))
|
||||
lon_end = int(math.floor(max_lon))
|
||||
|
||||
cells = []
|
||||
for lat_int in range(lat_start, lat_end + 1):
|
||||
for lon_int in range(lon_start, lon_end + 1):
|
||||
cells.append((float(lat_int), float(lon_int),
|
||||
float(lat_int + 1), float(lon_int + 1)))
|
||||
|
||||
buildings: List[Building] = []
|
||||
streets: List[Street] = []
|
||||
water_bodies: List[WaterBody] = []
|
||||
vegetation_areas: List[VegetationArea] = []
|
||||
|
||||
cache_stats = {"buildings": "skip", "streets": "skip",
|
||||
"water": "skip", "vegetation": "skip"}
|
||||
|
||||
for cell_min_lat, cell_min_lon, cell_max_lat, cell_max_lon in cells:
|
||||
cell_label = f"[{cell_min_lat:.0f},{cell_min_lon:.0f}]"
|
||||
|
||||
if settings.use_buildings:
|
||||
t1 = time.time()
|
||||
chunk = await self.buildings.fetch_buildings(
|
||||
cell_min_lat, cell_min_lon, cell_max_lat, cell_max_lon
|
||||
)
|
||||
dt = time.time() - t1
|
||||
src = "CACHE" if dt < 0.5 else "API"
|
||||
buildings.extend(chunk)
|
||||
cache_stats["buildings"] = src
|
||||
_clog(f"Buildings {cell_label}: {len(chunk)} items ({src}, {dt:.1f}s)")
|
||||
|
||||
if settings.use_street_canyon:
|
||||
t1 = time.time()
|
||||
chunk = await street_canyon_service.fetch_streets(
|
||||
cell_min_lat, cell_min_lon, cell_max_lat, cell_max_lon
|
||||
)
|
||||
dt = time.time() - t1
|
||||
src = "CACHE" if dt < 0.5 else "API"
|
||||
streets.extend(chunk)
|
||||
cache_stats["streets"] = src
|
||||
_clog(f"Streets {cell_label}: {len(chunk)} items ({src}, {dt:.1f}s)")
|
||||
|
||||
if settings.use_water_reflection:
|
||||
t1 = time.time()
|
||||
chunk = await water_service.fetch_water_bodies(
|
||||
cell_min_lat, cell_min_lon, cell_max_lat, cell_max_lon
|
||||
)
|
||||
dt = time.time() - t1
|
||||
src = "CACHE" if dt < 0.5 else "API"
|
||||
water_bodies.extend(chunk)
|
||||
cache_stats["water"] = src
|
||||
_clog(f"Water {cell_label}: {len(chunk)} items ({src}, {dt:.1f}s)")
|
||||
|
||||
if settings.use_vegetation:
|
||||
t1 = time.time()
|
||||
chunk = await vegetation_service.fetch_vegetation(
|
||||
cell_min_lat, cell_min_lon, cell_max_lat, cell_max_lon
|
||||
)
|
||||
dt = time.time() - t1
|
||||
src = "CACHE" if dt < 0.5 else "API"
|
||||
vegetation_areas.extend(chunk)
|
||||
cache_stats["vegetation"] = src
|
||||
_clog(f"Vegetation {cell_label}: {len(chunk)} items ({src}, {dt:.1f}s)")
|
||||
|
||||
total_fetch = time.time() - t0
|
||||
_clog(f"OSM fetch total: {total_fetch:.1f}s "
|
||||
f"({len(cells)} cells, "
|
||||
f"{len(buildings)} bldgs, {len(streets)} streets, "
|
||||
f"{len(water_bodies)} water, {len(vegetation_areas)} veg)")
|
||||
_clog(f"Cache status: {cache_stats}")
|
||||
|
||||
return {
|
||||
"buildings": buildings,
|
||||
"streets": streets,
|
||||
"water_bodies": water_bodies,
|
||||
"vegetation_areas": vegetation_areas,
|
||||
}
|
||||
|
||||
async def calculate_coverage(
|
||||
self,
|
||||
site: SiteParams,
|
||||
@@ -152,6 +284,8 @@ class CoverageService:
|
||||
|
||||
Returns list of CoveragePoint with RSRP values
|
||||
"""
|
||||
calc_start = time.time()
|
||||
|
||||
# Apply preset if specified
|
||||
settings = apply_preset(settings)
|
||||
|
||||
@@ -163,6 +297,7 @@ class CoverageService:
|
||||
settings.radius,
|
||||
settings.resolution
|
||||
)
|
||||
_clog(f"Grid: {len(grid)} points, radius={settings.radius}m, res={settings.resolution}m")
|
||||
|
||||
# Calculate bbox for data fetching
|
||||
lat_delta = settings.radius / 111000
|
||||
@@ -173,48 +308,80 @@ class CoverageService:
|
||||
min_lon = site.lon - lon_delta
|
||||
max_lon = site.lon + lon_delta
|
||||
|
||||
# Fetch buildings (if enabled) and build spatial index
|
||||
buildings: List[Building] = []
|
||||
_clog(f"Bbox: [{min_lat:.4f}, {min_lon:.4f}, {max_lat:.4f}, {max_lon:.4f}]")
|
||||
|
||||
# ━━━ PHASE 1: Fetch OSM data ━━━
|
||||
_clog("━━━ PHASE 1: Fetching OSM data ━━━")
|
||||
t_osm = time.time()
|
||||
osm_data = await self._fetch_osm_grid_aligned(
|
||||
min_lat, min_lon, max_lat, max_lon, settings
|
||||
)
|
||||
osm_time = time.time() - t_osm
|
||||
|
||||
buildings = osm_data["buildings"]
|
||||
streets = osm_data["streets"]
|
||||
water_bodies = osm_data["water_bodies"]
|
||||
vegetation_areas = osm_data["vegetation_areas"]
|
||||
_clog(f"━━━ PHASE 1 done: {osm_time:.1f}s ━━━")
|
||||
|
||||
# Build spatial index for buildings
|
||||
spatial_idx: Optional[SpatialIndex] = None
|
||||
if settings.use_buildings:
|
||||
buildings = await self.buildings.fetch_buildings(
|
||||
min_lat, min_lon, max_lat, max_lon
|
||||
)
|
||||
if buildings:
|
||||
cache_key = f"{min_lat:.3f},{min_lon:.3f},{max_lat:.3f},{max_lon:.3f}"
|
||||
spatial_idx = get_spatial_index(cache_key, buildings)
|
||||
if buildings:
|
||||
cache_key = f"{min_lat:.3f},{min_lon:.3f},{max_lat:.3f},{max_lon:.3f}"
|
||||
spatial_idx = get_spatial_index(cache_key, buildings)
|
||||
|
||||
# Fetch streets (if street canyon enabled)
|
||||
streets: List[Street] = []
|
||||
if settings.use_street_canyon:
|
||||
streets = await street_canyon_service.fetch_streets(
|
||||
min_lat, min_lon, max_lat, max_lon
|
||||
)
|
||||
# ━━━ PHASE 2: Pre-load terrain ━━━
|
||||
_clog("━━━ PHASE 2: Pre-loading terrain ━━━")
|
||||
t_terrain = time.time()
|
||||
tile_names = await self.terrain.ensure_tiles_for_bbox(
|
||||
min_lat, min_lon, max_lat, max_lon
|
||||
)
|
||||
for tn in tile_names:
|
||||
self.terrain._load_tile(tn)
|
||||
|
||||
# Fetch water bodies (if water reflection enabled)
|
||||
water_bodies: List[WaterBody] = []
|
||||
if settings.use_water_reflection:
|
||||
water_bodies = await water_service.fetch_water_bodies(
|
||||
min_lat, min_lon, max_lat, max_lon
|
||||
)
|
||||
site_elevation = self.terrain.get_elevation_sync(site.lat, site.lon)
|
||||
|
||||
# Fetch vegetation (if enabled)
|
||||
vegetation_areas: List[VegetationArea] = []
|
||||
if settings.use_vegetation:
|
||||
vegetation_areas = await vegetation_service.fetch_vegetation(
|
||||
min_lat, min_lon, max_lat, max_lon
|
||||
)
|
||||
|
||||
# Calculate coverage for each point
|
||||
point_elevations = {}
|
||||
for lat, lon in grid:
|
||||
point = await self._calculate_point(
|
||||
site, lat, lon,
|
||||
settings, buildings, streets,
|
||||
spatial_idx, water_bodies, vegetation_areas
|
||||
)
|
||||
point_elevations[(lat, lon)] = self.terrain.get_elevation_sync(lat, lon)
|
||||
terrain_time = time.time() - t_terrain
|
||||
_clog(f"Tiles: {len(tile_names)}, site elev: {site_elevation:.0f}m, "
|
||||
f"pre-computed {len(grid)} elevations")
|
||||
_clog(f"━━━ PHASE 2 done: {terrain_time:.1f}s ━━━")
|
||||
|
||||
if point.rsrp >= settings.min_signal:
|
||||
points.append(point)
|
||||
# ━━━ PHASE 3: Point calculation (sync, in thread pool) ━━━
|
||||
_clog(f"━━━ PHASE 3: Calculating {len(grid)} points (threaded) ━━━")
|
||||
dominant_path_service._log_count = 0 # Reset diagnostic counter
|
||||
t_points = time.time()
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
points, timing = await loop.run_in_executor(
|
||||
None,
|
||||
self._run_point_loop,
|
||||
grid, site, settings, buildings, streets,
|
||||
spatial_idx, water_bodies, vegetation_areas,
|
||||
site_elevation, point_elevations
|
||||
)
|
||||
|
||||
points_time = time.time() - t_points
|
||||
total_time = time.time() - calc_start
|
||||
|
||||
_clog(f"━━━ PHASE 3 done: {points_time:.1f}s ━━━")
|
||||
_clog("=== RESULTS ===")
|
||||
_clog(f" Grid points: {len(grid)}")
|
||||
_clog(f" Result points: {len(points)}")
|
||||
_clog(f" OSM fetch: {osm_time:.1f}s")
|
||||
_clog(f" Terrain pre-load:{terrain_time:.1f}s")
|
||||
_clog(f" Point calc: {points_time:.1f}s "
|
||||
f"({points_time/max(1,len(grid))*1000:.1f}ms/point)")
|
||||
_clog(f" TOTAL: {total_time:.1f}s")
|
||||
_clog(f" Tiles in memory: {len(self.terrain._tile_cache)}")
|
||||
if any(v > 0.001 for v in timing.values()):
|
||||
_clog("=== PER-STEP BREAKDOWN ===")
|
||||
for step, dt in timing.items():
|
||||
if dt > 0.001:
|
||||
_clog(f" {step:20s} {dt:.3f}s "
|
||||
f"({dt/max(1,len(grid))*1000:.2f}ms/point)")
|
||||
|
||||
return points
|
||||
|
||||
@@ -280,7 +447,36 @@ class CoverageService:
|
||||
|
||||
return points
|
||||
|
||||
async def _calculate_point(
|
||||
def _run_point_loop(
|
||||
self, grid, site, settings, buildings, streets,
|
||||
spatial_idx, water_bodies, vegetation_areas,
|
||||
site_elevation, point_elevations
|
||||
):
|
||||
"""Sync point loop - runs in ThreadPoolExecutor, bypasses event loop."""
|
||||
points = []
|
||||
timing = {"los": 0.0, "buildings": 0.0, "antenna": 0.0,
|
||||
"dominant_path": 0.0, "street_canyon": 0.0,
|
||||
"reflection": 0.0, "vegetation": 0.0}
|
||||
total = len(grid)
|
||||
log_interval = max(1, total // 20)
|
||||
|
||||
for i, (lat, lon) in enumerate(grid):
|
||||
if i % log_interval == 0:
|
||||
_clog(f"Progress: {i}/{total} ({i*100//total}%)")
|
||||
|
||||
point = self._calculate_point_sync(
|
||||
site, lat, lon, settings, buildings, streets,
|
||||
spatial_idx, water_bodies, vegetation_areas,
|
||||
site_elevation, point_elevations.get((lat, lon), 0.0),
|
||||
timing
|
||||
)
|
||||
if point.rsrp >= settings.min_signal:
|
||||
points.append(point)
|
||||
|
||||
_clog(f"Progress: {total}/{total} (100%)")
|
||||
return points, timing
|
||||
|
||||
def _calculate_point_sync(
|
||||
self,
|
||||
site: SiteParams,
|
||||
lat: float, lon: float,
|
||||
@@ -289,58 +485,62 @@ class CoverageService:
|
||||
streets: List[Street],
|
||||
spatial_idx: Optional[SpatialIndex],
|
||||
water_bodies: List[WaterBody],
|
||||
vegetation_areas: List[VegetationArea]
|
||||
vegetation_areas: List[VegetationArea],
|
||||
site_elevation: float,
|
||||
point_elevation: float,
|
||||
timing: dict
|
||||
) -> CoveragePoint:
|
||||
"""Calculate RSRP at a single point with all propagation models"""
|
||||
"""Fully synchronous point calculation. All terrain tiles must be pre-loaded."""
|
||||
|
||||
# Distance
|
||||
distance = TerrainService.haversine_distance(site.lat, site.lon, lat, lon)
|
||||
|
||||
if distance < 1:
|
||||
distance = 1 # Avoid division by zero
|
||||
distance = 1
|
||||
|
||||
# Base path loss (Okumura-Hata for urban)
|
||||
path_loss = self._okumura_hata(
|
||||
distance, site.frequency, site.height, 1.5
|
||||
)
|
||||
# Base path loss
|
||||
path_loss = self._okumura_hata(distance, site.frequency, site.height, 1.5)
|
||||
|
||||
# Antenna pattern loss (if directional)
|
||||
# Antenna pattern
|
||||
antenna_loss = 0.0
|
||||
if site.azimuth is not None and site.beamwidth:
|
||||
t0 = time.time()
|
||||
antenna_loss = self._antenna_pattern_loss(
|
||||
site.lat, site.lon, lat, lon,
|
||||
site.azimuth, site.beamwidth
|
||||
site.lat, site.lon, lat, lon, site.azimuth, site.beamwidth
|
||||
)
|
||||
timing["antenna"] += time.time() - t0
|
||||
|
||||
# Terrain loss (LoS check)
|
||||
# Terrain LOS (sync)
|
||||
terrain_loss = 0.0
|
||||
has_los = True
|
||||
|
||||
if settings.use_terrain:
|
||||
los_result = await self.los.check_line_of_sight(
|
||||
site.lat, site.lon, site.height,
|
||||
lat, lon, 1.5
|
||||
t0 = time.time()
|
||||
los_result = self.los.check_line_of_sight_sync(
|
||||
site.lat, site.lon, site.height, lat, lon, 1.5
|
||||
)
|
||||
has_los = los_result["has_los"]
|
||||
|
||||
if not has_los:
|
||||
clearance = los_result["clearance"]
|
||||
terrain_loss = self._diffraction_loss(clearance, site.frequency)
|
||||
terrain_loss = self._diffraction_loss(
|
||||
los_result["clearance"], site.frequency
|
||||
)
|
||||
timing["los"] += time.time() - t0
|
||||
|
||||
# Building loss — use spatial index for fast lookup
|
||||
# Building loss (spatial index)
|
||||
building_loss = 0.0
|
||||
t0 = time.time()
|
||||
nearby_buildings = (
|
||||
spatial_idx.query_line(site.lat, site.lon, lat, lon)
|
||||
if spatial_idx else buildings
|
||||
)
|
||||
|
||||
if settings.use_buildings and nearby_buildings:
|
||||
site_total_h = site.height + site_elevation
|
||||
point_total_h = 1.5 + point_elevation
|
||||
|
||||
if settings.use_materials:
|
||||
for building in nearby_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
|
||||
site.lat, site.lon, site_total_h,
|
||||
lat, lon, point_total_h, building
|
||||
)
|
||||
if intersection is not None:
|
||||
material = materials_service.detect_material(building.tags)
|
||||
@@ -352,21 +552,23 @@ class CoverageService:
|
||||
else:
|
||||
for building in nearby_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
|
||||
site.lat, site.lon, site_total_h,
|
||||
lat, lon, point_total_h, building
|
||||
)
|
||||
if intersection is not None:
|
||||
building_loss += 20.0
|
||||
has_los = False
|
||||
break
|
||||
timing["buildings"] += time.time() - t0
|
||||
|
||||
# Dominant path analysis
|
||||
if settings.use_dominant_path and nearby_buildings:
|
||||
paths = await dominant_path_service.find_dominant_paths(
|
||||
# Dominant path (sync) — uses spatial index for O(1) building lookups
|
||||
if settings.use_dominant_path and (spatial_idx or nearby_buildings):
|
||||
t0 = time.time()
|
||||
paths = dominant_path_service.find_dominant_paths_sync(
|
||||
site.lat, site.lon, site.height,
|
||||
lat, lon, 1.5,
|
||||
site.frequency, nearby_buildings
|
||||
site.frequency, nearby_buildings,
|
||||
spatial_idx=spatial_idx
|
||||
)
|
||||
if paths:
|
||||
best_path = paths[0]
|
||||
@@ -375,10 +577,12 @@ class CoverageService:
|
||||
terrain_loss = 0
|
||||
building_loss = 0
|
||||
has_los = best_path.path_type == "direct" and not best_path.materials_crossed
|
||||
timing["dominant_path"] += time.time() - t0
|
||||
|
||||
# Street canyon model
|
||||
# Street canyon (sync)
|
||||
if settings.use_street_canyon and streets:
|
||||
canyon_loss, street_path = await street_canyon_service.calculate_street_canyon_loss(
|
||||
t0 = time.time()
|
||||
canyon_loss, _street_path = street_canyon_service.calculate_street_canyon_loss_sync(
|
||||
site.lat, site.lon, site.height,
|
||||
lat, lon, 1.5,
|
||||
site.frequency, streets
|
||||
@@ -387,80 +591,76 @@ class CoverageService:
|
||||
path_loss = canyon_loss
|
||||
terrain_loss = 0
|
||||
building_loss = 0
|
||||
timing["street_canyon"] += time.time() - t0
|
||||
|
||||
# Vegetation loss
|
||||
# Vegetation (already sync)
|
||||
veg_loss = 0.0
|
||||
if settings.use_vegetation and vegetation_areas:
|
||||
t0 = time.time()
|
||||
veg_loss = vegetation_service.calculate_vegetation_loss(
|
||||
site.lat, site.lon, lat, lon,
|
||||
vegetation_areas, settings.season
|
||||
site.lat, site.lon, lat, lon, vegetation_areas, settings.season
|
||||
)
|
||||
timing["vegetation"] += time.time() - t0
|
||||
|
||||
# Reflections (building + ground/water)
|
||||
# Reflections (sync)
|
||||
reflection_gain = 0.0
|
||||
if settings.use_reflections and nearby_buildings:
|
||||
t0 = time.time()
|
||||
is_over_water = False
|
||||
if settings.use_water_reflection and water_bodies:
|
||||
is_over_water = water_service.point_over_water(lat, lon, water_bodies) is not None
|
||||
|
||||
reflection_paths = await reflection_service.find_reflection_paths(
|
||||
refl_paths = reflection_service.find_reflection_paths_sync(
|
||||
site.lat, site.lon, site.height,
|
||||
lat, lon, 1.5,
|
||||
site.frequency, nearby_buildings,
|
||||
include_ground=True
|
||||
)
|
||||
|
||||
# If over water, replace ground reflection with stronger water reflection
|
||||
if is_over_water and reflection_paths:
|
||||
if is_over_water and refl_paths:
|
||||
water_path = reflection_service._calculate_ground_reflection(
|
||||
site.lat, site.lon, site.height,
|
||||
lat, lon, 1.5,
|
||||
site.frequency, is_water=True
|
||||
)
|
||||
if water_path:
|
||||
reflection_paths = [
|
||||
p for p in reflection_paths if "ground" not in p.materials
|
||||
]
|
||||
reflection_paths.append(water_path)
|
||||
reflection_paths.sort(key=lambda p: p.total_loss)
|
||||
refl_paths = [p for p in refl_paths if "ground" not in p.materials]
|
||||
refl_paths.append(water_path)
|
||||
refl_paths.sort(key=lambda p: p.total_loss)
|
||||
|
||||
if reflection_paths:
|
||||
direct_rsrp = site.power + site.gain - path_loss - antenna_loss - terrain_loss - building_loss - veg_loss
|
||||
if refl_paths:
|
||||
direct_rsrp = (site.power + site.gain - path_loss - antenna_loss
|
||||
- terrain_loss - building_loss - veg_loss)
|
||||
combined_rsrp = reflection_service.combine_paths(
|
||||
direct_rsrp, reflection_paths, site.power + site.gain
|
||||
direct_rsrp, refl_paths, site.power + site.gain
|
||||
)
|
||||
reflection_gain = max(0, combined_rsrp - direct_rsrp)
|
||||
timing["reflection"] += time.time() - t0
|
||||
elif settings.use_water_reflection and water_bodies and not settings.use_reflections:
|
||||
# Water reflection without full reflection model
|
||||
is_over_water = water_service.point_over_water(lat, lon, water_bodies) is not None
|
||||
if is_over_water:
|
||||
reflection_gain = 3.0 # ~3dB boost over water
|
||||
reflection_gain = 3.0
|
||||
|
||||
# Rain attenuation
|
||||
# Rain
|
||||
rain_loss = 0.0
|
||||
if settings.rain_rate > 0:
|
||||
rain_loss = weather_service.calculate_rain_attenuation(
|
||||
site.frequency,
|
||||
distance / 1000, # km
|
||||
settings.rain_rate
|
||||
site.frequency, distance / 1000, settings.rain_rate
|
||||
)
|
||||
|
||||
# Indoor penetration loss
|
||||
# Indoor
|
||||
indoor_loss = 0.0
|
||||
if settings.indoor_loss_type != "none":
|
||||
indoor_loss = indoor_service.calculate_indoor_loss(
|
||||
site.frequency,
|
||||
settings.indoor_loss_type
|
||||
site.frequency, settings.indoor_loss_type
|
||||
)
|
||||
|
||||
# Atmospheric absorption
|
||||
# Atmospheric
|
||||
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
|
||||
site.frequency, distance / 1000,
|
||||
settings.temperature_c, settings.humidity_percent
|
||||
)
|
||||
|
||||
# Final RSRP
|
||||
@@ -470,18 +670,11 @@ class CoverageService:
|
||||
+ reflection_gain)
|
||||
|
||||
return CoveragePoint(
|
||||
lat=lat,
|
||||
lon=lon,
|
||||
rsrp=rsrp,
|
||||
distance=distance,
|
||||
has_los=has_los,
|
||||
terrain_loss=terrain_loss,
|
||||
building_loss=building_loss,
|
||||
reflection_gain=reflection_gain,
|
||||
vegetation_loss=veg_loss,
|
||||
rain_loss=rain_loss,
|
||||
indoor_loss=indoor_loss,
|
||||
atmospheric_loss=atmo_loss,
|
||||
lat=lat, lon=lon, rsrp=rsrp, distance=distance,
|
||||
has_los=has_los, terrain_loss=terrain_loss,
|
||||
building_loss=building_loss, reflection_gain=reflection_gain,
|
||||
vegetation_loss=veg_loss, rain_loss=rain_loss,
|
||||
indoor_loss=indoor_loss, atmospheric_loss=atmo_loss,
|
||||
)
|
||||
|
||||
def _okumura_hata(
|
||||
|
||||
Reference in New Issue
Block a user