@mytec: 1.4iter ready for testing
This commit is contained in:
265
backend/app/services/reflection_service.py
Normal file
265
backend/app/services/reflection_service.py
Normal file
@@ -0,0 +1,265 @@
|
||||
import numpy as np
|
||||
from typing import List, Tuple, Optional
|
||||
from dataclasses import dataclass
|
||||
from app.services.buildings_service import Building
|
||||
from app.services.materials_service import materials_service
|
||||
|
||||
|
||||
@dataclass
|
||||
class ReflectionPath:
|
||||
"""A reflection path with one or more bounces"""
|
||||
points: List[Tuple[float, float]] # [TX, reflection1, reflection2, ..., RX]
|
||||
total_distance: float
|
||||
total_loss: float
|
||||
reflection_count: int
|
||||
materials: List[str]
|
||||
|
||||
|
||||
class ReflectionService:
|
||||
"""
|
||||
Calculate reflection paths for RF propagation
|
||||
|
||||
- Single bounce (most common)
|
||||
- Double bounce (around corners)
|
||||
- Ground reflection
|
||||
"""
|
||||
|
||||
MAX_BOUNCES = 2
|
||||
GROUND_REFLECTION_COEFF = 0.3 # Depends on surface
|
||||
|
||||
async def find_reflection_paths(
|
||||
self,
|
||||
tx_lat: float, tx_lon: float, tx_height: float,
|
||||
rx_lat: float, rx_lon: float, rx_height: float,
|
||||
frequency_mhz: float,
|
||||
buildings: List[Building],
|
||||
include_ground: bool = True
|
||||
) -> List[ReflectionPath]:
|
||||
"""Find all viable reflection paths"""
|
||||
|
||||
paths = []
|
||||
|
||||
# Single-bounce building reflections
|
||||
for building in buildings:
|
||||
path = self._find_single_bounce(
|
||||
tx_lat, tx_lon, tx_height,
|
||||
rx_lat, rx_lon, rx_height,
|
||||
frequency_mhz, building
|
||||
)
|
||||
if path:
|
||||
paths.append(path)
|
||||
|
||||
# Ground reflection
|
||||
if include_ground:
|
||||
ground_path = self._calculate_ground_reflection(
|
||||
tx_lat, tx_lon, tx_height,
|
||||
rx_lat, rx_lon, rx_height,
|
||||
frequency_mhz
|
||||
)
|
||||
if ground_path:
|
||||
paths.append(ground_path)
|
||||
|
||||
# Sort by loss (best first)
|
||||
paths.sort(key=lambda p: p.total_loss)
|
||||
|
||||
return paths[:5] # Return top 5
|
||||
|
||||
def _find_single_bounce(
|
||||
self,
|
||||
tx_lat, tx_lon, tx_height,
|
||||
rx_lat, rx_lon, rx_height,
|
||||
frequency_mhz,
|
||||
building: Building
|
||||
) -> Optional[ReflectionPath]:
|
||||
"""Find single-bounce reflection off building"""
|
||||
|
||||
# Find reflection point on building walls
|
||||
geometry = building.geometry
|
||||
|
||||
for i in range(len(geometry) - 1):
|
||||
wall_start = geometry[i]
|
||||
wall_end = geometry[i + 1]
|
||||
|
||||
ref_point = self._specular_reflection_point(
|
||||
(tx_lon, tx_lat), (rx_lon, rx_lat),
|
||||
wall_start, wall_end
|
||||
)
|
||||
|
||||
if not ref_point:
|
||||
continue
|
||||
|
||||
ref_lat, ref_lon = ref_point[1], ref_point[0]
|
||||
|
||||
# Calculate distances
|
||||
from app.services.terrain_service import TerrainService
|
||||
d1 = TerrainService.haversine_distance(tx_lat, tx_lon, ref_lat, ref_lon)
|
||||
d2 = TerrainService.haversine_distance(ref_lat, ref_lon, rx_lat, rx_lon)
|
||||
total_dist = d1 + d2
|
||||
|
||||
# Direct distance check - reflection shouldn't be much longer
|
||||
direct_dist = TerrainService.haversine_distance(tx_lat, tx_lon, rx_lat, rx_lon)
|
||||
if total_dist > direct_dist * 1.5:
|
||||
continue
|
||||
|
||||
# Path loss
|
||||
path_loss = self._free_space_loss(total_dist, frequency_mhz)
|
||||
|
||||
# Reflection loss
|
||||
material = materials_service.detect_material(building.tags)
|
||||
reflection_loss = materials_service.get_reflection_loss(material)
|
||||
|
||||
total_loss = path_loss + reflection_loss
|
||||
|
||||
return ReflectionPath(
|
||||
points=[(tx_lat, tx_lon), (ref_lat, ref_lon), (rx_lat, rx_lon)],
|
||||
total_distance=total_dist,
|
||||
total_loss=total_loss,
|
||||
reflection_count=1,
|
||||
materials=[material.value]
|
||||
)
|
||||
|
||||
return None
|
||||
|
||||
def _calculate_ground_reflection(
|
||||
self,
|
||||
tx_lat, tx_lon, tx_height,
|
||||
rx_lat, rx_lon, rx_height,
|
||||
frequency_mhz
|
||||
) -> Optional[ReflectionPath]:
|
||||
"""Calculate ground reflection path"""
|
||||
|
||||
from app.services.terrain_service import TerrainService
|
||||
|
||||
# Reflection point (simplified - midpoint for flat ground)
|
||||
mid_lat = (tx_lat + rx_lat) / 2
|
||||
mid_lon = (tx_lon + rx_lon) / 2
|
||||
|
||||
# Path lengths
|
||||
d1 = TerrainService.haversine_distance(tx_lat, tx_lon, mid_lat, mid_lon)
|
||||
d2 = TerrainService.haversine_distance(mid_lat, mid_lon, rx_lat, rx_lon)
|
||||
|
||||
# Actual path length considering heights
|
||||
path1 = np.sqrt(d1**2 + tx_height**2)
|
||||
path2 = np.sqrt(d2**2 + rx_height**2)
|
||||
total_dist = path1 + path2
|
||||
|
||||
# Path loss
|
||||
path_loss = self._free_space_loss(total_dist, frequency_mhz)
|
||||
|
||||
# Ground reflection loss (~5-10 dB typically)
|
||||
ground_reflection_loss = -10 * np.log10(self.GROUND_REFLECTION_COEFF)
|
||||
|
||||
# Phase difference can cause constructive or destructive interference
|
||||
# Simplified: assume average case
|
||||
total_loss = path_loss + ground_reflection_loss
|
||||
|
||||
return ReflectionPath(
|
||||
points=[(tx_lat, tx_lon), (mid_lat, mid_lon), (rx_lat, rx_lon)],
|
||||
total_distance=total_dist,
|
||||
total_loss=total_loss,
|
||||
reflection_count=1,
|
||||
materials=["ground"]
|
||||
)
|
||||
|
||||
def _specular_reflection_point(
|
||||
self,
|
||||
tx: Tuple[float, float], # (lon, lat)
|
||||
rx: Tuple[float, float],
|
||||
wall_start: List[float], # [lon, lat]
|
||||
wall_end: List[float]
|
||||
) -> Optional[Tuple[float, float]]:
|
||||
"""Calculate specular reflection point on wall"""
|
||||
|
||||
# Wall vector
|
||||
wx = wall_end[0] - wall_start[0]
|
||||
wy = wall_end[1] - wall_start[1]
|
||||
wall_len = np.sqrt(wx**2 + wy**2)
|
||||
|
||||
if wall_len < 1e-10:
|
||||
return None
|
||||
|
||||
# Normalize
|
||||
wx /= wall_len
|
||||
wy /= wall_len
|
||||
|
||||
# Wall normal (perpendicular)
|
||||
nx = -wy
|
||||
ny = wx
|
||||
|
||||
# Vector from wall start to TX
|
||||
tx_rel_x = tx[0] - wall_start[0]
|
||||
tx_rel_y = tx[1] - wall_start[1]
|
||||
|
||||
# Distance from TX to wall line
|
||||
dist_to_wall = tx_rel_x * nx + tx_rel_y * ny
|
||||
|
||||
# Mirror TX across wall
|
||||
mirror_x = tx[0] - 2 * dist_to_wall * nx
|
||||
mirror_y = tx[1] - 2 * dist_to_wall * ny
|
||||
|
||||
# Line from mirror to RX
|
||||
dx = rx[0] - mirror_x
|
||||
dy = rx[1] - mirror_y
|
||||
|
||||
# Find intersection with wall
|
||||
# Parametric: wall_start + t * wall_dir
|
||||
# Parametric: mirror + s * (rx - mirror)
|
||||
|
||||
denom = dx * wy - dy * wx
|
||||
if abs(denom) < 1e-10:
|
||||
return None
|
||||
|
||||
t = ((wall_start[0] - mirror_x) * wy - (wall_start[1] - mirror_y) * wx) / denom
|
||||
s = ((wall_start[0] - mirror_x) * dy - (wall_start[1] - mirror_y) * dx) / (-denom) if denom != 0 else 0
|
||||
|
||||
# Check if on wall segment and between mirror and RX
|
||||
if 0 <= s <= 1 and 0 <= t <= 1:
|
||||
ref_x = mirror_x + t * dx
|
||||
ref_y = mirror_y + t * dy
|
||||
return (ref_x, ref_y)
|
||||
|
||||
return None
|
||||
|
||||
def _free_space_loss(self, distance: float, frequency_mhz: float) -> float:
|
||||
"""Free space path loss (dB)"""
|
||||
if distance <= 0:
|
||||
distance = 1
|
||||
|
||||
# FSPL = 20*log10(d) + 20*log10(f) + 20*log10(4*pi/c)
|
||||
# Simplified: FSPL = 32.45 + 20*log10(f_MHz) + 20*log10(d_km)
|
||||
d_km = distance / 1000
|
||||
return 32.45 + 20 * np.log10(frequency_mhz) + 20 * np.log10(d_km + 0.001)
|
||||
|
||||
def combine_paths(
|
||||
self,
|
||||
direct_power_dbm: float,
|
||||
reflection_paths: List[ReflectionPath],
|
||||
tx_power_dbm: float
|
||||
) -> float:
|
||||
"""
|
||||
Combine direct and reflected signals (power sum)
|
||||
|
||||
Returns total received power in dBm
|
||||
"""
|
||||
|
||||
# Convert to linear power
|
||||
powers = []
|
||||
|
||||
if direct_power_dbm > -150: # Valid direct signal
|
||||
powers.append(10 ** (direct_power_dbm / 10))
|
||||
|
||||
for path in reflection_paths:
|
||||
reflected_power_dbm = tx_power_dbm - path.total_loss
|
||||
if reflected_power_dbm > -150:
|
||||
powers.append(10 ** (reflected_power_dbm / 10))
|
||||
|
||||
if not powers:
|
||||
return -150.0 # No signal
|
||||
|
||||
# Sum powers (incoherent addition - conservative estimate)
|
||||
total_power = sum(powers)
|
||||
|
||||
return 10 * np.log10(total_power)
|
||||
|
||||
|
||||
reflection_service = ReflectionService()
|
||||
Reference in New Issue
Block a user