@mytec: iter3.2.1 start
This commit is contained in:
@@ -44,7 +44,10 @@ 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, find_dominant_paths_vectorized
|
||||
from app.services.dominant_path_service import (
|
||||
dominant_path_service, find_dominant_paths_vectorized,
|
||||
get_lod_level, LODLevel, SIMPLIFIED_MAX_BUILDINGS,
|
||||
)
|
||||
from app.services.street_canyon_service import street_canyon_service, Street
|
||||
from app.services.reflection_service import reflection_service
|
||||
from app.services.spatial_index import get_spatial_index, SpatialIndex
|
||||
@@ -619,12 +622,25 @@ class CoverageService:
|
||||
_clog(f" Tiles in memory: {len(self.terrain._tile_cache)}")
|
||||
if any(isinstance(v, (int, float)) and v > 0.001 for v in timing.values()):
|
||||
_clog("=== PER-STEP BREAKDOWN ===")
|
||||
lod_keys = {"lod_none", "lod_simplified", "lod_full"}
|
||||
for step, dt in timing.items():
|
||||
if step in lod_keys:
|
||||
continue # Print LOD stats separately
|
||||
if isinstance(dt, (int, float)) and dt > 0.001:
|
||||
_clog(f" {step:20s} {dt:.3f}s "
|
||||
f"({dt/max(1,len(grid))*1000:.2f}ms/point)")
|
||||
elif not isinstance(dt, (int, float)):
|
||||
_clog(f" {step:20s} {dt}")
|
||||
# LOD stats
|
||||
lod_none = timing.get("lod_none", 0)
|
||||
lod_simp = timing.get("lod_simplified", 0)
|
||||
lod_full = timing.get("lod_full", 0)
|
||||
lod_total = lod_none + lod_simp + lod_full
|
||||
if lod_total > 0:
|
||||
_clog(f"=== LOD BREAKDOWN ({lod_total} points with dominant_path) ===")
|
||||
_clog(f" LOD_NONE (>3km) {lod_none:5d} points ({lod_none*100//lod_total}%) — skipped")
|
||||
_clog(f" LOD_SIMPLIFIED {lod_simp:5d} points ({lod_simp*100//lod_total}%) — {SIMPLIFIED_MAX_BUILDINGS} buildings max")
|
||||
_clog(f" LOD_FULL (<1.5km) {lod_full:5d} points ({lod_full*100//lod_total}%) — full calculation")
|
||||
|
||||
return points
|
||||
|
||||
@@ -834,31 +850,51 @@ class CoverageService:
|
||||
break
|
||||
timing["buildings"] += time.time() - t0
|
||||
|
||||
# Dominant path (vectorized NumPy) — replaces loop-based sync version
|
||||
# Dominant path (vectorized NumPy) with LOD optimization
|
||||
# Only enter when there are actual buildings (spatial_idx with data OR non-empty list)
|
||||
has_building_data = nearby_buildings or (spatial_idx is not None and spatial_idx._grid)
|
||||
if settings.use_dominant_path and has_building_data:
|
||||
t0 = time.time()
|
||||
try:
|
||||
dominant = find_dominant_paths_vectorized(
|
||||
site.lat, site.lon, site.height,
|
||||
lat, lon, 1.5,
|
||||
site.frequency, nearby_buildings,
|
||||
spatial_idx=spatial_idx,
|
||||
)
|
||||
if dominant['path_type'] == 'direct':
|
||||
has_los = True
|
||||
building_loss = 0.0
|
||||
elif dominant['path_type'] == 'reflection':
|
||||
building_loss = max(0.0, building_loss - (10.0 - dominant['total_loss']))
|
||||
has_los = False
|
||||
elif dominant['path_type'] == 'diffraction':
|
||||
if dominant['total_loss'] > building_loss:
|
||||
building_loss = dominant['total_loss']
|
||||
has_los = False
|
||||
except Exception:
|
||||
pass # Skip dominant path on error — use base model
|
||||
timing["dominant_path"] += time.time() - t0
|
||||
lod = get_lod_level(distance)
|
||||
|
||||
# LOD_NONE: skip dominant path entirely for distant points (>3km)
|
||||
if lod == LODLevel.NONE:
|
||||
timing.setdefault("lod_none", 0)
|
||||
timing["lod_none"] += 1
|
||||
else:
|
||||
t0 = time.time()
|
||||
try:
|
||||
# LOD_SIMPLIFIED: limit buildings for mid-range points (1.5-3km)
|
||||
dp_buildings = nearby_buildings
|
||||
dp_spatial = spatial_idx
|
||||
if lod == LODLevel.SIMPLIFIED:
|
||||
timing.setdefault("lod_simplified", 0)
|
||||
timing["lod_simplified"] += 1
|
||||
if len(nearby_buildings) > SIMPLIFIED_MAX_BUILDINGS:
|
||||
dp_buildings = nearby_buildings[:SIMPLIFIED_MAX_BUILDINGS]
|
||||
dp_spatial = None # Skip spatial queries, use filtered list only
|
||||
else:
|
||||
timing.setdefault("lod_full", 0)
|
||||
timing["lod_full"] += 1
|
||||
|
||||
dominant = find_dominant_paths_vectorized(
|
||||
site.lat, site.lon, site.height,
|
||||
lat, lon, 1.5,
|
||||
site.frequency, dp_buildings,
|
||||
spatial_idx=dp_spatial,
|
||||
)
|
||||
if dominant['path_type'] == 'direct':
|
||||
has_los = True
|
||||
building_loss = 0.0
|
||||
elif dominant['path_type'] == 'reflection':
|
||||
building_loss = max(0.0, building_loss - (10.0 - dominant['total_loss']))
|
||||
has_los = False
|
||||
elif dominant['path_type'] == 'diffraction':
|
||||
if dominant['total_loss'] > building_loss:
|
||||
building_loss = dominant['total_loss']
|
||||
has_los = False
|
||||
except Exception:
|
||||
pass # Skip dominant path on error — use base model
|
||||
timing["dominant_path"] += time.time() - t0
|
||||
|
||||
# Street canyon (sync)
|
||||
if settings.use_street_canyon and streets:
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import time
|
||||
import numpy as np
|
||||
from enum import Enum
|
||||
from typing import List, Tuple, Optional, Dict, Any, TYPE_CHECKING
|
||||
from dataclasses import dataclass
|
||||
from app.services.terrain_service import terrain_service
|
||||
@@ -15,6 +16,38 @@ if TYPE_CHECKING:
|
||||
from app.services.spatial_index import SpatialIndex
|
||||
|
||||
|
||||
# ── Level of Detail (LOD) for dominant path calculations ──
|
||||
|
||||
class LODLevel(Enum):
|
||||
"""Distance-based level of detail for dominant path analysis.
|
||||
|
||||
At long distances, building-level multipath contributes minimally
|
||||
to path loss — macro propagation models suffice.
|
||||
"""
|
||||
NONE = "none" # Skip dominant path entirely
|
||||
SIMPLIFIED = "simplified" # Check only nearest few buildings
|
||||
FULL = "full" # Full calculation (current behavior)
|
||||
|
||||
|
||||
# LOD distance thresholds (meters)
|
||||
LOD_THRESHOLD_NONE = 3000 # >3km: skip dominant path
|
||||
LOD_THRESHOLD_SIMPLIFIED = 1500 # 1.5-3km: simplified mode
|
||||
|
||||
# Simplified mode limits
|
||||
SIMPLIFIED_MAX_BUILDINGS = 5
|
||||
SIMPLIFIED_MAX_WALLS = 50
|
||||
|
||||
|
||||
def get_lod_level(distance_m: float) -> LODLevel:
|
||||
"""Determine LOD level based on TX-RX distance."""
|
||||
if distance_m > LOD_THRESHOLD_NONE:
|
||||
return LODLevel.NONE
|
||||
elif distance_m > LOD_THRESHOLD_SIMPLIFIED:
|
||||
return LODLevel.SIMPLIFIED
|
||||
else:
|
||||
return LODLevel.FULL
|
||||
|
||||
|
||||
@dataclass
|
||||
class RayPath:
|
||||
"""Single ray path from TX to RX"""
|
||||
|
||||
Reference in New Issue
Block a user