Source code for urbanmarl.models.channel_advanced

"""UrbanMARL Advanced 3D mmWave Channel Model.

Implements 3GPP TR 38.901 compliant wireless propagation modeling:

1. 3D Directional Antenna Radiation Patterns:
   - Elevation beam attenuation: A_V(theta) = -min(12 * ((theta - theta_tilt) / theta_3dB)^2, SLA_V)
   - Azimuth beam attenuation: A_H(phi) = -min(12 * (phi / phi_3dB)^2, A_max)
   - Combined 3D antenna gain: G(theta, phi) = G_max - min(-(A_V + A_H), A_max)
2. Elevation-Dependent Rician / Rayleigh Small-Scale Fading:
   - Elevation-dependent Rician K-factor for LoS paths.
   - Rayleigh fading for NLoS paths.
"""

from typing import Optional, Union

import torch


[docs] class AdvancedChannelModel: """3GPP compliant 3D directional wireless channel propagation model. Attributes: frequency_ghz (float): Carrier frequency in GHz. bandwidth (float): Transmission bandwidth in Hz. g_max_dbi (float): Maximum boresight antenna gain in dBi. theta_3db (float): Vertical half-power 3dB beamwidth in degrees. phi_3db (float): Horizontal half-power 3dB beamwidth in degrees. noise_figure_db (float): Receiver noise figure in dB. """
[docs] def __init__( self, frequency_ghz: float = 29.0, bandwidth_hz: float = 10e6, g_max_dbi: float = 15.0, theta_3db_deg: float = 65.0, phi_3db_deg: float = 65.0, noise_figure_db: float = 7.0, device: Union[torch.device, str] = "cpu", ) -> None: """Initializes the advanced channel model. Args: frequency_ghz (float): Carrier frequency. Defaults to 29.0 GHz. bandwidth_hz (float): Bandwidth. Defaults to 10 MHz. g_max_dbi (float): Antenna gain in dBi. Defaults to 15.0. theta_3db_deg (float): Vertical 3dB beamwidth. Defaults to 65 deg. phi_3db_deg (float): Horizontal 3dB beamwidth. Defaults to 65 deg. noise_figure_db (float): Receiver noise figure. Defaults to 7.0 dB. device (Union[torch.device, str]): PyTorch compute device. """ self.frequency_ghz = frequency_ghz self.bandwidth = bandwidth_hz self.g_max_dbi = g_max_dbi self.theta_3db = theta_3db_deg self.phi_3db = phi_3db_deg self.noise_figure_db = noise_figure_db self.device = torch.device(device) k_b = 1.380649e-23 t_k = 290.0 thermal_noise = k_b * t_k * self.bandwidth self.noise_power = thermal_noise * (10 ** (self.noise_figure_db / 10.0)) c = 299792458.0 self.wavelength = c / (self.frequency_ghz * 1e9)
[docs] def compute_3d_antenna_gain( self, tx_pos: torch.Tensor, rx_pos: torch.Tensor ) -> torch.Tensor: """Computes 3D directional antenna radiation pattern gain G(theta, phi) in linear scale. Args: tx_pos (torch.Tensor): UAV transmitter coordinates of shape (..., N, 3). rx_pos (torch.Tensor): UE receiver coordinates of shape (..., M, 3). Returns: torch.Tensor: Linear antenna gain array of shape (..., N, M). """ # Relative vectors: shape (..., N, M, 3) diff = rx_pos.unsqueeze(-3) - tx_pos.unsqueeze(-2) d_xy = torch.clamp(torch.norm(diff[..., :2], dim=-1), min=0.1) # Elevation angle theta off nadir (downward pointing): theta = atan2(d_xy, -dz) dz = diff[..., 2] # typically negative from UAV to ground theta_rad = torch.atan2(d_xy, -dz) theta_deg = theta_rad * (180.0 / torch.pi) # Horizontal azimuth angle phi phi_rad = torch.atan2(diff[..., 1], diff[..., 0]) phi_deg = phi_rad * (180.0 / torch.pi) # 3GPP TR 38.901 antenna pattern a_v = -torch.clamp(12.0 * ((theta_deg / self.theta_3db) ** 2), max=30.0) a_h = -torch.clamp(12.0 * ((phi_deg / self.phi_3db) ** 2), max=30.0) # Combined attenuation: -min(-(A_V + A_H), A_max) combined_attenuation = torch.clamp(a_v + a_h, min=-30.0) gain_dbi = self.g_max_dbi + combined_attenuation gain_linear = 10.0 ** (gain_dbi / 10.0) return gain_linear
[docs] def compute_data_rates( self, tx_pos: torch.Tensor, rx_pos: torch.Tensor, tx_power: Union[float, torch.Tensor] = 2.0, los_mask: Optional[torch.Tensor] = None, include_fading: bool = False, ) -> torch.Tensor: """Computes Shannon data rates with 3D directional antenna patterns and fading. Args: tx_pos (torch.Tensor): UAV 3D coordinates (..., N, 3). rx_pos (torch.Tensor): UE 3D coordinates (..., M, 3). tx_power (Union[float, torch.Tensor]): Transmit power in Watts. los_mask (Optional[torch.Tensor]): Boolean LoS tensor of shape (..., N, M). include_fading (bool): Whether to sample stochastic small-scale fading. Returns: torch.Tensor: Achievable transmission data rate in bits per second (..., N, M). """ diff = rx_pos.unsqueeze(-3) - tx_pos.unsqueeze(-2) distances = torch.clamp(torch.norm(diff, dim=-1), min=1.0) # Free space path loss: (4 * pi * d / lambda)^2 fspl = (4.0 * torch.pi * distances / self.wavelength) ** 2 if los_mask is None: los_mask = torch.ones_like(distances, dtype=torch.bool) pl_exponent = torch.where(los_mask, 2.0, 3.5) pathloss = fspl * (distances ** (pl_exponent - 2.0)) # 3D Directional antenna gain antenna_gain = self.compute_3d_antenna_gain(tx_pos, rx_pos) # Small scale fading power gain if include_fading: # Elevation angle in [0, pi/2] dz = torch.abs(diff[..., 2]) d_xy = torch.norm(diff[..., :2], dim=-1) elev_rad = torch.atan2(dz, torch.clamp(d_xy, min=0.1)) k_factor = 1.0 + 15.0 * (elev_rad / (torch.pi / 2.0)) # Rician fading power s = torch.sqrt(k_factor / (k_factor + 1.0)) sigma = torch.sqrt(0.5 / (k_factor + 1.0)) h_real = s + sigma * torch.randn_like(distances) h_imag = sigma * torch.randn_like(distances) rician_gain = h_real**2 + h_imag**2 # Rayleigh fading power rayleigh_gain = -torch.log( torch.clamp(torch.rand_like(distances), min=1e-6) ) fading_gain = torch.where(los_mask, rician_gain, rayleigh_gain) else: fading_gain = 1.0 if isinstance(tx_power, torch.Tensor): p_tx = ( tx_power.unsqueeze(-1) if tx_power.ndim < distances.ndim else tx_power ) else: p_tx = tx_power rx_power = (p_tx * antenna_gain * fading_gain) / pathloss sinr = rx_power / self.noise_power data_rates = self.bandwidth * torch.log2(1.0 + sinr) return data_rates