diff --git a/backend/app/services/coverage_service.py b/backend/app/services/coverage_service.py index 3f4dc26..b93ea75 100644 --- a/backend/app/services/coverage_service.py +++ b/backend/app/services/coverage_service.py @@ -47,6 +47,7 @@ from app.services.materials_service import materials_service from app.services.dominant_path_service import ( dominant_path_service, find_dominant_paths_vectorized, get_lod_level, LODLevel, SIMPLIFIED_MAX_BUILDINGS, + _filter_buildings_by_distance, ) from app.services.street_canyon_service import street_canyon_service, Street from app.services.reflection_service import reflection_service @@ -853,6 +854,16 @@ class CoverageService: if spatial_idx else buildings ) + # Cap building count for the intersection loop — query_line can + # return hundreds of buildings; sort by proximity so the first + # intersecting building is found faster (loop breaks early). + if len(nearby_buildings) > 50: + nearby_buildings = _filter_buildings_by_distance( + nearby_buildings, + (site.lat, site.lon), (lat, lon), + max_count=50, max_distance=500, + ) + if settings.use_buildings and nearby_buildings: site_total_h = site.height + site_elevation point_total_h = 1.5 + point_elevation @@ -897,22 +908,22 @@ class CoverageService: 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 + # nearby_buildings already filtered via spatial index — + # don't pass spatial_idx to avoid redundant query_line() + # and query_point() inside the vectorized function. 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 diff --git a/backend/app/services/dominant_path_service.py b/backend/app/services/dominant_path_service.py index 7f57da0..f4fd1de 100644 --- a/backend/app/services/dominant_path_service.py +++ b/backend/app/services/dominant_path_service.py @@ -167,10 +167,14 @@ def find_dominant_paths_vectorized( 3. Vectorized reflection point calculation 4. Simplified diffraction estimate + Callers should pass pre-filtered buildings and spatial_idx=None + to avoid redundant spatial queries (coverage_service already filters). + Returns dict with: has_los, path_type, total_loss, path_length, reflection_point """ global _vec_log_count + t_total = time.perf_counter() # Fast path: no buildings at all → direct LOS, skip all numpy work has_spatial_data = spatial_idx is not None and spatial_idx._grid @@ -183,11 +187,13 @@ def find_dominant_paths_vectorized( 'reflection_point': None, } - # Get nearby buildings via spatial index (same filtering as sync version) + # Get nearby buildings via spatial index or use pre-filtered list + t0 = time.perf_counter() if spatial_idx: line_buildings = spatial_idx.query_line(tx_lat, tx_lon, rx_lat, rx_lon) else: line_buildings = buildings + t_query = time.perf_counter() - t0 # No nearby buildings along this line → direct LOS if not line_buildings: @@ -199,12 +205,14 @@ def find_dominant_paths_vectorized( 'reflection_point': None, } + t0 = time.perf_counter() 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, ) + t_filter = time.perf_counter() - t0 # Reference point for local coordinate system ref_lat = (tx_lat + rx_lat) / 2 @@ -219,19 +227,11 @@ def find_dominant_paths_vectorized( direct_dist = np.linalg.norm(rx - tx) # Convert buildings to arrays + t0 = time.perf_counter() 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, - ) + t_arrays = time.perf_counter() - t0 # No buildings → direct LOS if len(poly_lengths) == 0 or np.all(poly_lengths < 3): @@ -244,9 +244,22 @@ def find_dominant_paths_vectorized( } # Step 1: Vectorized direct LOS check + t0 = time.perf_counter() intersects, _ = line_intersects_polygons_batch(tx, rx, poly_x, poly_y, poly_lengths) + t_los = time.perf_counter() - t0 if not np.any(intersects): + t_total_ms = (time.perf_counter() - t_total) * 1000 + _vec_log_count += 1 + if _vec_log_count <= 10: + print( + f"[DP_TIMING] #{_vec_log_count} LOS_CLEAR " + f"bldgs={len(line_buildings)} walls={len(walls_start)} " + f"query={t_query*1000:.1f}ms filter={t_filter*1000:.1f}ms " + f"arrays={t_arrays*1000:.1f}ms los={t_los*1000:.1f}ms " + f"total={t_total_ms:.1f}ms", + flush=True, + ) return { 'has_los': True, 'path_type': 'direct', @@ -256,7 +269,8 @@ def find_dominant_paths_vectorized( } # Step 2: Vectorized reflection path finding - # Use all line buildings for reflection walls + # Reuse line buildings for reflection (no separate spatial query) + t0 = time.perf_counter() if spatial_idx: mid_lat = (tx_lat + rx_lat) / 2 mid_lon = (tx_lon + rx_lon) / 2 @@ -280,15 +294,34 @@ def find_dominant_paths_vectorized( 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 + t_refl_setup = time.perf_counter() - t0 + t0 = time.perf_counter() 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, + max_los_checks=5, ) + t_refl_calc = time.perf_counter() - t0 + + t_total_ms = (time.perf_counter() - t_total) * 1000 + + # Diagnostic log for first few points + _vec_log_count += 1 + if _vec_log_count <= 10: + print( + f"[DP_TIMING] #{_vec_log_count} LOS_BLOCKED " + f"bldgs={len(line_buildings)} walls={len(walls_start)} " + f"dist={direct_dist:.0f}m " + f"query={t_query*1000:.1f}ms filter={t_filter*1000:.1f}ms " + f"arrays={t_arrays*1000:.1f}ms los={t_los*1000:.1f}ms " + f"refl_setup={t_refl_setup*1000:.1f}ms refl_calc={t_refl_calc*1000:.1f}ms " + f"total={t_total_ms:.1f}ms", + flush=True, + ) if refl_point is not None: # Convert reflection point back to lat/lon