@mytec: 3.8.0a done
This commit is contained in:
@@ -139,6 +139,279 @@ class GPUService:
|
||||
|
||||
return _to_cpu(L)
|
||||
|
||||
def batch_terrain_los(
|
||||
self,
|
||||
site_lat: float,
|
||||
site_lon: float,
|
||||
site_height: float,
|
||||
site_elevation: float,
|
||||
grid_lats: np.ndarray,
|
||||
grid_lons: np.ndarray,
|
||||
grid_elevations: np.ndarray,
|
||||
distances: np.ndarray,
|
||||
frequency_mhz: float,
|
||||
terrain_cache: dict,
|
||||
num_samples: int = 30,
|
||||
) -> tuple[np.ndarray, np.ndarray]:
|
||||
"""Batch compute terrain LOS and diffraction loss for all grid points.
|
||||
|
||||
This is the key GPU optimization — instead of sampling terrain profiles
|
||||
one point at a time, we sample ALL profiles in parallel using vectorized
|
||||
operations.
|
||||
|
||||
Args:
|
||||
site_lat, site_lon: Site coordinates
|
||||
site_height: Antenna height above ground (meters)
|
||||
site_elevation: Ground elevation at site (meters)
|
||||
grid_lats, grid_lons: All grid point coordinates
|
||||
grid_elevations: Ground elevation at each grid point
|
||||
distances: Pre-computed distances from site to each point (meters)
|
||||
frequency_mhz: Frequency for diffraction calculation
|
||||
terrain_cache: Dict[tile_name -> numpy array] from terrain_service
|
||||
num_samples: Number of samples per terrain profile
|
||||
|
||||
Returns:
|
||||
(has_los, terrain_loss) - both shape (N,)
|
||||
has_los: boolean array, True if clear line of sight
|
||||
terrain_loss: diffraction loss in dB (0 if has_los)
|
||||
"""
|
||||
_xp = gpu_manager.get_array_module()
|
||||
N = len(grid_lats)
|
||||
|
||||
if N == 0:
|
||||
return np.array([], dtype=bool), np.array([], dtype=np.float64)
|
||||
|
||||
# Convert inputs to GPU arrays
|
||||
g_lats = _xp.asarray(grid_lats, dtype=_xp.float64)
|
||||
g_lons = _xp.asarray(grid_lons, dtype=_xp.float64)
|
||||
g_elevs = _xp.asarray(grid_elevations, dtype=_xp.float64)
|
||||
g_dists = _xp.asarray(distances, dtype=_xp.float64)
|
||||
|
||||
# Heights
|
||||
tx_total = float(site_elevation + site_height)
|
||||
rx_height = 1.5 # Receiver height above ground
|
||||
|
||||
# Earth curvature constants
|
||||
EARTH_RADIUS = 6371000.0
|
||||
K_FACTOR = 4.0 / 3.0
|
||||
effective_radius = K_FACTOR * EARTH_RADIUS
|
||||
|
||||
# Sample terrain profiles for all points at once
|
||||
# Create sample positions: shape (N, num_samples)
|
||||
t = _xp.linspace(0, 1, num_samples, dtype=_xp.float64) # (S,)
|
||||
t = t.reshape(1, -1) # (1, S)
|
||||
|
||||
# Interpolate lat/lon for all sample points
|
||||
# sample_lats[i, j] = site_lat + t[j] * (grid_lats[i] - site_lat)
|
||||
dlat = g_lats.reshape(-1, 1) - site_lat # (N, 1)
|
||||
dlon = g_lons.reshape(-1, 1) - site_lon # (N, 1)
|
||||
sample_lats = site_lat + t * dlat # (N, S)
|
||||
sample_lons = site_lon + t * dlon # (N, S)
|
||||
|
||||
# Sample distances along path: shape (N, S)
|
||||
sample_dists = t * g_dists.reshape(-1, 1) # (N, S)
|
||||
|
||||
# Get terrain elevations for all samples
|
||||
# This is the tricky part - we need to look up from the tile cache
|
||||
# For GPU efficiency, we'll do this on CPU then transfer
|
||||
sample_lats_cpu = _to_cpu(sample_lats).flatten()
|
||||
sample_lons_cpu = _to_cpu(sample_lons).flatten()
|
||||
|
||||
# Batch elevation lookup from cache
|
||||
sample_elevs_cpu = self._batch_elevation_lookup(
|
||||
sample_lats_cpu, sample_lons_cpu, terrain_cache
|
||||
)
|
||||
sample_elevs = _xp.asarray(sample_elevs_cpu, dtype=_xp.float64).reshape(N, num_samples)
|
||||
|
||||
# Compute LOS line height at each sample point
|
||||
# Linear interpolation from tx to rx
|
||||
rx_total = g_elevs + rx_height # (N,)
|
||||
los_heights = tx_total + t * (rx_total.reshape(-1, 1) - tx_total) # (N, S)
|
||||
|
||||
# Earth curvature correction at each sample
|
||||
total_dist = g_dists.reshape(-1, 1) # (N, 1)
|
||||
d = sample_dists # (N, S)
|
||||
curvature = (d * (total_dist - d)) / (2 * effective_radius) # (N, S)
|
||||
los_heights_corrected = los_heights - curvature # (N, S)
|
||||
|
||||
# Clearance at each sample point
|
||||
clearances = los_heights_corrected - sample_elevs # (N, S)
|
||||
|
||||
# Minimum clearance per profile
|
||||
min_clearances = _xp.min(clearances, axis=1) # (N,)
|
||||
|
||||
# Has LOS if minimum clearance > 0
|
||||
has_los = min_clearances > 0 # (N,)
|
||||
|
||||
# Diffraction loss for points without LOS
|
||||
# Using simplified ITU-R P.526 formula
|
||||
terrain_loss = _xp.zeros(N, dtype=_xp.float64)
|
||||
|
||||
# Only compute diffraction where blocked
|
||||
blocked_mask = ~has_los
|
||||
blocked_clearances = min_clearances[blocked_mask]
|
||||
|
||||
if _xp.any(blocked_mask):
|
||||
# v = |clearance| / 10 (simplified Fresnel parameter)
|
||||
v = _xp.abs(blocked_clearances) / 10.0
|
||||
|
||||
# Diffraction loss formula from ITU-R P.526
|
||||
loss = _xp.where(
|
||||
v <= 0,
|
||||
_xp.zeros_like(v),
|
||||
_xp.where(
|
||||
v < 2.4,
|
||||
6.02 + 9.11 * v + 1.65 * v ** 2,
|
||||
12.95 + 20 * _xp.log10(v)
|
||||
)
|
||||
)
|
||||
# Cap at reasonable max
|
||||
loss = _xp.minimum(loss, 40.0)
|
||||
terrain_loss[blocked_mask] = loss
|
||||
|
||||
return _to_cpu(has_los).astype(bool), _to_cpu(terrain_loss)
|
||||
|
||||
def _batch_elevation_lookup(
|
||||
self,
|
||||
lats: np.ndarray,
|
||||
lons: np.ndarray,
|
||||
terrain_cache: dict,
|
||||
) -> np.ndarray:
|
||||
"""Look up elevations from cached terrain tiles.
|
||||
|
||||
Vectorized implementation: processes per-tile (1-4 tiles) instead of
|
||||
per-point (thousands of points). Inner operations are all NumPy vectorized.
|
||||
|
||||
Args:
|
||||
lats, lons: Flattened arrays of coordinates
|
||||
terrain_cache: Dict mapping tile_name -> numpy array
|
||||
|
||||
Returns:
|
||||
elevations: Same shape as input lats
|
||||
"""
|
||||
elevations = np.zeros(len(lats), dtype=np.float64)
|
||||
|
||||
# Vectorized tile identification
|
||||
lat_ints = np.floor(lats).astype(int)
|
||||
lon_ints = np.floor(lons).astype(int)
|
||||
|
||||
# Process per tile (usually 1-4 tiles, not per point)
|
||||
unique_tiles = set(zip(lat_ints, lon_ints))
|
||||
|
||||
for lat_int, lon_int in unique_tiles:
|
||||
lat_letter = 'N' if lat_int >= 0 else 'S'
|
||||
lon_letter = 'E' if lon_int >= 0 else 'W'
|
||||
tile_name = f"{lat_letter}{abs(lat_int):02d}{lon_letter}{abs(lon_int):03d}"
|
||||
|
||||
tile = terrain_cache.get(tile_name)
|
||||
if tile is None:
|
||||
continue
|
||||
|
||||
# Mask for points in this tile
|
||||
mask = (lat_ints == lat_int) & (lon_ints == lon_int)
|
||||
tile_lats = lats[mask]
|
||||
tile_lons = lons[mask]
|
||||
|
||||
size = tile.shape[0]
|
||||
# Vectorized row/col calculation
|
||||
rows = ((1 - (tile_lats - lat_int)) * (size - 1)).astype(int)
|
||||
cols = ((tile_lons - lon_int) * (size - 1)).astype(int)
|
||||
rows = np.clip(rows, 0, size - 1)
|
||||
cols = np.clip(cols, 0, size - 1)
|
||||
|
||||
# Vectorized lookup - single operation for ALL points in tile
|
||||
tile_elevs = tile[rows, cols].astype(np.float64)
|
||||
tile_elevs[tile_elevs == -32768] = 0.0
|
||||
elevations[mask] = tile_elevs
|
||||
|
||||
return elevations
|
||||
|
||||
def batch_antenna_pattern(
|
||||
self,
|
||||
site_lat: float,
|
||||
site_lon: float,
|
||||
grid_lats: np.ndarray,
|
||||
grid_lons: np.ndarray,
|
||||
azimuth: float,
|
||||
beamwidth: float,
|
||||
) -> np.ndarray:
|
||||
"""Batch compute antenna pattern loss for all grid points.
|
||||
|
||||
Returns antenna_loss in dB, shape (N,)
|
||||
"""
|
||||
_xp = gpu_manager.get_array_module()
|
||||
N = len(grid_lats)
|
||||
|
||||
if N == 0 or azimuth is None or not beamwidth:
|
||||
return np.zeros(N, dtype=np.float64)
|
||||
|
||||
# Convert to radians
|
||||
lat1 = _xp.radians(_xp.float64(site_lat))
|
||||
lon1 = _xp.radians(_xp.float64(site_lon))
|
||||
lat2 = _xp.radians(_xp.asarray(grid_lats, dtype=_xp.float64))
|
||||
lon2 = _xp.radians(_xp.asarray(grid_lons, dtype=_xp.float64))
|
||||
|
||||
# Calculate bearing from site to each point
|
||||
dlon = lon2 - lon1
|
||||
x = _xp.sin(dlon) * _xp.cos(lat2)
|
||||
y = _xp.cos(lat1) * _xp.sin(lat2) - _xp.sin(lat1) * _xp.cos(lat2) * _xp.cos(dlon)
|
||||
bearings = (_xp.degrees(_xp.arctan2(x, y)) + 360) % 360
|
||||
|
||||
# Angle difference from antenna azimuth
|
||||
angle_diff = _xp.abs(bearings - azimuth)
|
||||
angle_diff = _xp.where(angle_diff > 180, 360 - angle_diff, angle_diff)
|
||||
|
||||
# Antenna pattern loss (simplified sector pattern)
|
||||
half_bw = beamwidth / 2
|
||||
in_main = angle_diff <= half_bw
|
||||
loss_main = 3 * (angle_diff / half_bw) ** 2
|
||||
loss_side = 3 + 12 * ((angle_diff - half_bw) / half_bw) ** 2
|
||||
loss_side = _xp.minimum(loss_side, 25.0)
|
||||
|
||||
antenna_loss = _xp.where(in_main, loss_main, loss_side)
|
||||
return _to_cpu(antenna_loss)
|
||||
|
||||
def batch_final_rsrp(
|
||||
self,
|
||||
tx_power: float,
|
||||
tx_gain: float,
|
||||
path_loss: np.ndarray,
|
||||
terrain_loss: np.ndarray,
|
||||
antenna_loss: np.ndarray,
|
||||
building_loss: np.ndarray,
|
||||
vegetation_loss: np.ndarray,
|
||||
rain_loss: np.ndarray,
|
||||
indoor_loss: np.ndarray,
|
||||
atmospheric_loss: np.ndarray,
|
||||
reflection_gain: np.ndarray,
|
||||
fading_margin: float = 0.0,
|
||||
) -> np.ndarray:
|
||||
"""Vectorized final RSRP calculation.
|
||||
|
||||
RSRP = tx_power + tx_gain - path_loss - terrain_loss - antenna_loss
|
||||
- building_loss - vegetation_loss - rain_loss - indoor_loss
|
||||
- atmospheric_loss + reflection_gain - fading_margin
|
||||
|
||||
Returns RSRP in dBm, shape (N,)
|
||||
"""
|
||||
_xp = gpu_manager.get_array_module()
|
||||
|
||||
rsrp = (
|
||||
float(tx_power) + float(tx_gain)
|
||||
- _xp.asarray(path_loss, dtype=_xp.float64)
|
||||
- _xp.asarray(terrain_loss, dtype=_xp.float64)
|
||||
- _xp.asarray(antenna_loss, dtype=_xp.float64)
|
||||
- _xp.asarray(building_loss, dtype=_xp.float64)
|
||||
- _xp.asarray(vegetation_loss, dtype=_xp.float64)
|
||||
- _xp.asarray(rain_loss, dtype=_xp.float64)
|
||||
- _xp.asarray(indoor_loss, dtype=_xp.float64)
|
||||
- _xp.asarray(atmospheric_loss, dtype=_xp.float64)
|
||||
+ _xp.asarray(reflection_gain, dtype=_xp.float64)
|
||||
- float(fading_margin)
|
||||
)
|
||||
|
||||
return _to_cpu(rsrp)
|
||||
|
||||
|
||||
# Singleton
|
||||
gpu_service = GPUService()
|
||||
|
||||
Reference in New Issue
Block a user