@mytec: before 3.0 REFACTOR

This commit is contained in:
2026-02-01 14:26:17 +02:00
parent acc90fe538
commit 1dde56705a
13 changed files with 1759 additions and 61 deletions

View File

@@ -1,10 +1,15 @@
import time
import numpy as np
from typing import List, Tuple, Optional, TYPE_CHECKING
from typing import List, Tuple, Optional, Dict, Any, TYPE_CHECKING
from dataclasses import dataclass
from app.services.terrain_service import terrain_service
from app.services.buildings_service import buildings_service, Building
from app.services.materials_service import materials_service, BuildingMaterial
from app.services.geometry_vectorized import (
points_to_local_coords,
line_intersects_polygons_batch,
find_best_reflection_path_vectorized,
)
if TYPE_CHECKING:
from app.services.spatial_index import SpatialIndex
@@ -21,9 +26,9 @@ class RayPath:
is_valid: bool # Does this path exist?
MAX_BUILDINGS_FOR_LINE = 50
MAX_BUILDINGS_FOR_REFLECTION = 30
MAX_DISTANCE_FROM_PATH = 300 # meters
MAX_BUILDINGS_FOR_LINE = 30
MAX_BUILDINGS_FOR_REFLECTION = 20
MAX_DISTANCE_FROM_PATH = 200 # meters
def _filter_buildings_by_distance(buildings, tx_point, rx_point, max_count=100, max_distance=500):
@@ -60,6 +65,204 @@ def _filter_buildings_by_distance(buildings, tx_point, rx_point, max_count=100,
return filtered[:max_count]
# ── Vectorized dominant path (NumPy) ──
_vec_log_count = 0
def _buildings_to_arrays(buildings: List[Building], ref_lat: float, ref_lon: float):
"""Convert Building objects to numpy arrays for vectorized geometry.
Returns:
walls_start: (W, 2) wall start points in local XY meters
walls_end: (W, 2) wall end points in local XY meters
wall_to_building: (W,) mapping wall index -> building index
poly_x: flattened polygon X coords
poly_y: flattened polygon Y coords
poly_lengths: (num_polygons,) vertices per polygon
"""
all_walls_start = []
all_walls_end = []
wall_to_building = []
all_poly_x = []
all_poly_y = []
poly_lengths = []
for i, b in enumerate(buildings):
geom = b.geometry # [[lon, lat], ...]
if not geom or len(geom) < 3:
poly_lengths.append(0)
continue
poly_lats = np.array([p[1] for p in geom])
poly_lons = np.array([p[0] for p in geom])
px, py = points_to_local_coords(ref_lat, ref_lon, poly_lats, poly_lons)
all_poly_x.extend(px)
all_poly_y.extend(py)
poly_lengths.append(len(geom))
# Extract wall segments
for j in range(len(geom) - 1):
all_walls_start.append([px[j], py[j]])
all_walls_end.append([px[j + 1], py[j + 1]])
wall_to_building.append(i)
return (
np.array(all_walls_start) if all_walls_start else np.zeros((0, 2)),
np.array(all_walls_end) if all_walls_end else np.zeros((0, 2)),
np.array(wall_to_building, dtype=int) if wall_to_building else np.zeros(0, dtype=int),
np.array(all_poly_x) if all_poly_x else np.zeros(0),
np.array(all_poly_y) if all_poly_y else np.zeros(0),
np.array(poly_lengths, dtype=int),
)
def find_dominant_paths_vectorized(
tx_lat: float, tx_lon: float, tx_height: float,
rx_lat: float, rx_lon: float, rx_height: float,
frequency_mhz: float,
buildings: List[Building],
spatial_idx: 'Optional[SpatialIndex]' = None,
) -> Dict[str, Any]:
"""Vectorized dominant path finding using NumPy batch operations.
Replaces the loop-based find_dominant_paths_sync() with:
1. Batch building-to-array conversion
2. Vectorized LOS polygon intersection check
3. Vectorized reflection point calculation
4. Simplified diffraction estimate
Returns dict with:
has_los, path_type, total_loss, path_length, reflection_point
"""
global _vec_log_count
# Get nearby buildings via spatial index (same filtering as sync version)
if spatial_idx:
line_buildings = spatial_idx.query_line(tx_lat, tx_lon, rx_lat, rx_lon)
else:
line_buildings = buildings
line_buildings = _filter_buildings_by_distance(
line_buildings,
(tx_lat, tx_lon), (rx_lat, rx_lon),
max_count=MAX_BUILDINGS_FOR_LINE,
max_distance=MAX_DISTANCE_FROM_PATH,
)
# Reference point for local coordinate system
ref_lat = (tx_lat + rx_lat) / 2
ref_lon = (tx_lon + rx_lon) / 2
# Convert TX/RX to local meters
tx_xy = points_to_local_coords(ref_lat, ref_lon, np.array([tx_lat]), np.array([tx_lon]))
rx_xy = points_to_local_coords(ref_lat, ref_lon, np.array([rx_lat]), np.array([rx_lon]))
tx = np.array([tx_xy[0][0], tx_xy[1][0]])
rx = np.array([rx_xy[0][0], rx_xy[1][0]])
direct_dist = np.linalg.norm(rx - tx)
# Convert buildings to arrays
walls_start, walls_end, wall_to_bldg, poly_x, poly_y, poly_lengths = (
_buildings_to_arrays(line_buildings, ref_lat, ref_lon)
)
# Diagnostic log for first few points
_vec_log_count += 1
if _vec_log_count <= 3:
print(
f"[DOMINANT_PATH_VEC] Point #{_vec_log_count}: "
f"buildings={len(line_buildings)}, walls={len(walls_start)}, "
f"dist={direct_dist:.0f}m",
flush=True,
)
# No buildings → direct LOS
if len(poly_lengths) == 0 or np.all(poly_lengths < 3):
return {
'has_los': True,
'path_type': 'direct',
'total_loss': 0.0,
'path_length': direct_dist,
'reflection_point': None,
}
# Step 1: Vectorized direct LOS check
intersects, _ = line_intersects_polygons_batch(tx, rx, poly_x, poly_y, poly_lengths)
if not np.any(intersects):
return {
'has_los': True,
'path_type': 'direct',
'total_loss': 0.0,
'path_length': direct_dist,
'reflection_point': None,
}
# Step 2: Vectorized reflection path finding
# Use all line buildings for reflection walls
if spatial_idx:
mid_lat = (tx_lat + rx_lat) / 2
mid_lon = (tx_lon + rx_lon) / 2
refl_buildings = spatial_idx.query_point(mid_lat, mid_lon, buffer_cells=3)
refl_buildings = _filter_buildings_by_distance(
refl_buildings,
(tx_lat, tx_lon), (rx_lat, rx_lon),
max_count=MAX_BUILDINGS_FOR_REFLECTION,
max_distance=MAX_DISTANCE_FROM_PATH,
)
# Merge line + reflection buildings (deduplicate by id)
seen_ids = {b.id for b in line_buildings}
merged = list(line_buildings)
for b in refl_buildings:
if b.id not in seen_ids:
merged.append(b)
seen_ids.add(b.id)
r_walls_start, r_walls_end, r_wall_to_bldg, r_poly_x, r_poly_y, r_poly_lengths = (
_buildings_to_arrays(merged, ref_lat, ref_lon)
)
else:
r_walls_start, r_walls_end, r_wall_to_bldg = walls_start, walls_end, wall_to_bldg
r_poly_x, r_poly_y, r_poly_lengths = poly_x, poly_y, poly_lengths
refl_point, refl_length, refl_loss = find_best_reflection_path_vectorized(
tx, rx,
r_walls_start, r_walls_end, r_wall_to_bldg,
r_poly_x, r_poly_y, r_poly_lengths,
max_candidates=30,
max_walls=100,
max_los_checks=10,
)
if refl_point is not None:
# Convert reflection point back to lat/lon
cos_lat = np.cos(np.radians(ref_lat))
refl_lat = ref_lat + refl_point[1] / 110540.0
refl_lon = ref_lon + refl_point[0] / (111320.0 * cos_lat)
return {
'has_los': False,
'path_type': 'reflection',
'total_loss': refl_loss,
'path_length': refl_length,
'reflection_point': (refl_lat, refl_lon),
}
# Step 3: Diffraction fallback
num_blocking = int(np.sum(intersects))
diffraction_loss = 10.0 + 5.0 * min(num_blocking, 5)
return {
'has_los': False,
'path_type': 'diffraction',
'total_loss': diffraction_loss,
'path_length': direct_dist,
'reflection_point': None,
}
class DominantPathService:
"""
Find dominant propagation paths (2-3 strongest)