rma_scenario.py (10367B)
1 # 2 # SPDX-FileCopyrightText: Copyright (c) 2021-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. 3 # SPDX-License-Identifier: Apache-2.0 4 # 5 """3GPP TR39.801 rural macrocell (RMa) channel scenario""" 6 7 import tensorflow as tf 8 9 from sionna import SPEED_OF_LIGHT, PI 10 from sionna.utils import log10 11 from . import SystemLevelScenario 12 13 class RMaScenario(SystemLevelScenario): 14 r""" 15 3GPP TR 38.901 rural macrocell (RMa) channel model scenario. 16 17 Parameters 18 ----------- 19 20 carrier_frequency : float 21 Carrier frequency [Hz] 22 23 rx_array : PanelArray 24 Panel array used by the receivers. All receivers share the same 25 antenna array configuration. 26 27 tx_array : PanelArray 28 Panel array used by the transmitters. All transmitters share the 29 same antenna array configuration. 30 31 direction : str 32 Link direction. Either "uplink" or "downlink". 33 34 enable_pathloss : bool 35 If `True`, apply pathloss. Otherwise doesn't. Defaults to `True`. 36 37 enable_shadow_fading : bool 38 If `True`, apply shadow fading. Otherwise doesn't. 39 Defaults to `True`. 40 41 average_street_width : float 42 Average street width [m]. Defaults to 5m. 43 44 average_street_width : float 45 Average building height [m]. Defaults to 20m. 46 47 always_generate_lsp : bool 48 If `True`, new large scale parameters (LSPs) are generated for every 49 new generation of channel impulse responses. Otherwise, always reuse 50 the same LSPs, except if the topology is changed. Defaults to 51 `False`. 52 53 dtype : Complex tf.DType 54 Defines the datatype for internal calculations and the output 55 dtype. Defaults to `tf.complex64`. 56 """ 57 58 def __init__(self, carrier_frequency, ut_array, bs_array, 59 direction, enable_pathloss=True, enable_shadow_fading=True, 60 average_street_width=20.0, average_building_height=5.0, 61 dtype=tf.complex64): 62 63 # Only the low-loss O2I model if available for RMa. 64 super().__init__(carrier_frequency, 'low', ut_array, bs_array, 65 direction, enable_pathloss, enable_shadow_fading, dtype) 66 67 # Average street width [m] 68 self._average_street_width = tf.constant(average_street_width, 69 self._dtype.real_dtype) 70 71 # Average building height [m] 72 self._average_building_height = tf.constant(average_building_height, 73 self._dtype.real_dtype) 74 75 ######################################### 76 # Public methods and properties 77 ######################################### 78 79 def clip_carrier_frequency_lsp(self, fc): 80 r"""Clip the carrier frequency ``fc`` in GHz for LSP calculation 81 82 Input 83 ----- 84 fc : float 85 Carrier frequency [GHz] 86 87 Output 88 ------- 89 : float 90 Clipped carrier frequency, that should be used for LSp computation 91 """ 92 return fc 93 94 @property 95 def min_2d_in(self): 96 r"""Minimum indoor 2D distance for indoor UTs [m]""" 97 return tf.constant(0.0, self._dtype.real_dtype) 98 99 @property 100 def max_2d_in(self): 101 r"""Maximum indoor 2D distance for indoor UTs [m]""" 102 return tf.constant(10.0, self._dtype.real_dtype) 103 104 @property 105 def average_street_width(self): 106 r"""Average street width [m]""" 107 return self._average_street_width 108 109 @property 110 def average_building_height(self): 111 r"""Average building height [m]""" 112 return self._average_building_height 113 114 @property 115 def los_probability(self): 116 r"""Probability of each UT to be LoS. Used to randomly generate LoS 117 status of outdoor UTs. 118 119 Computed following section 7.4.2 of TR 38.901. 120 121 [batch size, num_ut]""" 122 distance_2d_out = self._distance_2d_out 123 los_probability = tf.exp(-(distance_2d_out-10.0)/1000.0) 124 los_probability = tf.where(tf.math.less(distance_2d_out, 10.0), 125 tf.constant(1.0, self._dtype.real_dtype), los_probability) 126 return los_probability 127 128 @property 129 def rays_per_cluster(self): 130 r"""Number of rays per cluster""" 131 return tf.constant(20, tf.int32) 132 133 @property 134 def los_parameter_filepath(self): 135 r""" Path of the configuration file for LoS scenario""" 136 return 'RMa_LoS.json' 137 138 @property 139 def nlos_parameter_filepath(self): 140 r""" Path of the configuration file for NLoS scenario""" 141 return'RMa_NLoS.json' 142 143 @property 144 def o2i_parameter_filepath(self): 145 r""" Path of the configuration file for indoor scenario""" 146 return 'RMa_O2I.json' 147 148 ######################### 149 # Utility methods 150 ######################### 151 152 def _compute_lsp_log_mean_std(self): 153 r"""Computes the mean and standard deviations of LSPs in log-domain""" 154 155 batch_size = self.batch_size 156 num_bs = self.num_bs 157 num_ut = self.num_ut 158 distance_2d = self.distance_2d 159 h_bs = self.h_bs 160 h_bs = tf.expand_dims(h_bs, axis=2) # For broadcasting 161 h_ut = self.h_ut 162 h_ut = tf.expand_dims(h_ut, axis=1) # For broadcasting 163 164 ## Mean 165 # DS 166 log_mean_ds = self.get_param("muDS") 167 # ASD 168 log_mean_asd = self.get_param("muASD") 169 # ASA 170 log_mean_asa = self.get_param("muASA") 171 # SF. Has zero-mean. 172 log_mean_sf = tf.zeros([batch_size, num_bs, num_ut], 173 self._dtype.real_dtype) 174 # K. Given in dB in the 3GPP tables, hence the division by 10 175 log_mean_k = self.get_param("muK")/10.0 176 # ZSA 177 log_mean_zsa = self.get_param("muZSA") 178 # ZSD mean is of the form max(-1, A*d2D/1000 - 0.01*(hUT-1.5) + B) 179 log_mean_zsd = (self.get_param("muZSDa")*(distance_2d/1000.) 180 - 0.01*(h_ut-1.5) + self.get_param("muZSDb")) 181 log_mean_zsd = tf.math.maximum(tf.constant(-1.0, 182 self._dtype.real_dtype), log_mean_zsd) 183 184 lsp_log_mean = tf.stack([log_mean_ds, 185 log_mean_asd, 186 log_mean_asa, 187 log_mean_sf, 188 log_mean_k, 189 log_mean_zsa, 190 log_mean_zsd], axis=3) 191 192 ## STD 193 # DS 194 log_std_ds = self.get_param("sigmaDS") 195 # ASD 196 log_std_asd = self.get_param("sigmaASD") 197 # ASA 198 log_std_asa = self.get_param("sigmaASA") 199 # SF. Given in dB in the 3GPP tables, hence the division by 10 200 # O2I and NLoS cases just require the use of a predefined value 201 log_std_sf_o2i_nlos = self.get_param("sigmaSF")/10.0 202 # For LoS, two possible scenarion depending on the 2D location of the 203 # user 204 distance_breakpoint = (2.*PI*h_bs*h_ut*self.carrier_frequency/ 205 SPEED_OF_LIGHT) 206 log_std_sf_los=tf.where(tf.math.less(distance_2d, distance_breakpoint), 207 self.get_param("sigmaSF1")/10.0, self.get_param("sigmaSF2")/10.0) 208 # Use the correct SF STD according to the user scenario: NLoS/O2I, or 209 # LoS 210 log_std_sf = tf.where(self.los, log_std_sf_los, log_std_sf_o2i_nlos) 211 # K. Given in dB in the 3GPP tables, hence the division by 10. 212 log_std_k = self.get_param("sigmaK")/10.0 213 # ZSA 214 log_std_zsa = self.get_param("sigmaZSA") 215 # ZSD 216 log_std_zsd = self.get_param("sigmaZSD") 217 218 lsp_log_std = tf.stack([log_std_ds, 219 log_std_asd, 220 log_std_asa, 221 log_std_sf, 222 log_std_k, 223 log_std_zsa, 224 log_std_zsd], axis=3) 225 226 self._lsp_log_mean = lsp_log_mean 227 self._lsp_log_std = lsp_log_std 228 229 # ZOD offset 230 zod_offset = (tf.atan((35.-3.5)/distance_2d) 231 - tf.atan((35.-1.5)/distance_2d)) 232 zod_offset = tf.where(self.los, tf.constant(0.0,self._dtype.real_dtype), 233 zod_offset) 234 self._zod_offset = zod_offset 235 236 def _compute_pathloss_basic(self): 237 r"""Computes the basic component of the pathloss [dB]""" 238 239 distance_2d = self.distance_2d 240 distance_3d = self.distance_3d 241 fc = self.carrier_frequency/1e9 # Carrier frequency (GHz) 242 h_bs = self.h_bs 243 h_bs = tf.expand_dims(h_bs, axis=2) # For broadcasting 244 h_ut = self.h_ut 245 h_ut = tf.expand_dims(h_ut, axis=1) # For broadcasting 246 average_building_height = self.average_building_height 247 248 # Beak point distance 249 # For this computation, the carrifer frequency needs to be in Hz 250 distance_breakpoint = (2.*PI*h_bs*h_ut*self.carrier_frequency 251 /SPEED_OF_LIGHT) 252 253 ## Basic path loss for LoS 254 255 pl_1 = (20.0*log10(40.0*PI*distance_3d*fc/3.) 256 + tf.math.minimum(0.03*tf.math.pow(average_building_height,1.72), 257 10.0)*log10(distance_3d) 258 - tf.math.minimum(0.044*tf.math.pow(average_building_height,1.72), 259 14.77) 260 + 0.002*log10(average_building_height)*distance_3d) 261 pl_2 = (20.0*log10(40.0*PI*distance_breakpoint*fc/3.) 262 + tf.math.minimum(0.03*tf.math.pow(average_building_height,1.72), 263 10.0)*log10(distance_breakpoint) 264 - tf.math.minimum(0.044*tf.math.pow(average_building_height,1.72), 265 14.77) 266 + 0.002*log10(average_building_height)*distance_breakpoint 267 + 40.0*log10(distance_3d/distance_breakpoint)) 268 pl_los = tf.where(tf.math.less(distance_2d, distance_breakpoint), 269 pl_1, pl_2) 270 271 ## Basic pathloss for NLoS and O2I 272 273 pl_3 = (161.04 - 7.1*log10(self.average_street_width) 274 + 7.5*log10(average_building_height) 275 - (24.37 - 3.7*tf.square(average_building_height/h_bs)) 276 *log10(h_bs) 277 + (43.42 - 3.1*log10(h_bs))*(log10(distance_3d)-3.0) 278 + 20.0*log10(fc) - (3.2*tf.square(log10(11.75*h_ut)) 279 - 4.97)) 280 pl_nlos = tf.math.maximum(pl_los, pl_3) 281 282 ## Set the basic pathloss according to UT state 283 284 # LoS 285 pl_b = tf.where(self.los, pl_los, pl_nlos) 286 287 self._pl_b = pl_b