@mytec: iter3.2.1 start

This commit is contained in:
2026-02-01 23:51:21 +02:00
parent defa3ad440
commit b5b2fd90d2
4 changed files with 1057 additions and 23 deletions

View File

@@ -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:

View File

@@ -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"""