uma_scenario.py (10183B)
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 urban macrocell (UMa) channel model""" 6 7 import tensorflow as tf 8 9 import sionna 10 from sionna import SPEED_OF_LIGHT 11 from sionna.utils import log10 12 from . import SystemLevelScenario 13 14 class UMaScenario(SystemLevelScenario): 15 r""" 16 3GPP TR 38.901 urban macrocell (UMa) channel model scenario. 17 18 Parameters 19 ----------- 20 carrier_frequency : float 21 Carrier frequency [Hz] 22 23 o2i_model : str 24 Outdoor to indoor (O2I) pathloss model, used for indoor UTs. 25 Either "low" or "high" (see section 7.4.3 from 38.901 specification) 26 27 ut_array : PanelArray 28 Panel array configuration used by UTs 29 30 bs_array : PanelArray 31 Panel array configuration used by BSs 32 33 direction : str 34 Link direction. Either "uplink" or "downlink" 35 36 enable_pathloss : bool 37 If set to `True`, apply pathloss. Otherwise, does not. Defaults to True. 38 39 enable_shadow_fading : bool 40 If set to `True`, apply shadow fading. Otherwise, does not. 41 Defaults to True. 42 43 dtype : tf.DType 44 Defines the datatype for internal calculations and the output 45 dtype. Defaults to `tf.complex64`. 46 """ 47 48 ######################################### 49 # Public methods and properties 50 ######################################### 51 52 def clip_carrier_frequency_lsp(self, fc): 53 r"""Clip the carrier frequency ``fc`` in GHz for LSP calculation 54 55 Input 56 ----- 57 fc : float 58 Carrier frequency [GHz] 59 60 Output 61 ------- 62 : float 63 Clipped carrier frequency, that should be used for LSp computation 64 """ 65 if fc < 6.: 66 fc = tf.cast(6., self._dtype.real_dtype) 67 return fc 68 69 @property 70 def min_2d_in(self): 71 r"""Minimum indoor 2D distance for indoor UTs [m]""" 72 return tf.constant(0.0, self._dtype.real_dtype) 73 74 @property 75 def max_2d_in(self): 76 r"""Maximum indoor 2D distance for indoor UTs [m]""" 77 return tf.constant(25.0, self._dtype.real_dtype) 78 79 @property 80 def los_probability(self): 81 r"""Probability of each UT to be LoS. Used to randomly generate LoS 82 status of outdoor UTs. 83 84 Computed following section 7.4.2 of TR 38.901. 85 86 [batch size, num_ut]""" 87 88 h_ut = self.h_ut 89 c = tf.math.pow((h_ut-13.)/10., 1.5) 90 c = tf.where(tf.math.less(h_ut, 13.0), 91 tf.constant(0., self._dtype.real_dtype), c) 92 c = tf.expand_dims(c, axis=1) 93 94 distance_2d_out = self._distance_2d_out 95 los_probability = ((18.0/distance_2d_out 96 + tf.math.exp(-distance_2d_out/63.0)*(1.-18./distance_2d_out)) 97 *(1.+c*5./4.*tf.math.pow(distance_2d_out/100., 3) 98 *tf.math.exp(-distance_2d_out/150.0))) 99 100 los_probability = tf.where(tf.math.less(distance_2d_out, 18.0), 101 tf.constant(1.0, self._dtype.real_dtype), los_probability) 102 return los_probability 103 104 @property 105 def rays_per_cluster(self): 106 r"""Number of rays per cluster""" 107 return tf.constant(20, tf.int32) 108 109 @property 110 def los_parameter_filepath(self): 111 r""" Path of the configuration file for LoS scenario""" 112 return "UMa_LoS.json" 113 114 @property 115 def nlos_parameter_filepath(self): 116 r""" Path of the configuration file for NLoS scenario""" 117 return"UMa_NLoS.json" 118 119 @property 120 def o2i_parameter_filepath(self): 121 r""" Path of the configuration file for indoor scenario""" 122 return "UMa_O2I.json" 123 124 ######################### 125 # Utility methods 126 ######################### 127 128 def _compute_lsp_log_mean_std(self): 129 r"""Computes the mean and standard deviations of LSPs in log-domain""" 130 131 batch_size = self.batch_size 132 num_bs = self.num_bs 133 num_ut = self.num_ut 134 distance_2d = self.distance_2d 135 h_bs = self.h_bs 136 h_bs = tf.expand_dims(h_bs, axis=2) # For broadcasting 137 h_ut = self.h_ut 138 h_ut = tf.expand_dims(h_ut, axis=1) # For broadcasting 139 140 ## Mean 141 # DS 142 log_mean_ds = self.get_param("muDS") 143 # ASD 144 log_mean_asd = self.get_param("muASD") 145 # ASA 146 log_mean_asa = self.get_param("muASA") 147 # SF. Has zero-mean. 148 log_mean_sf = tf.zeros([batch_size, num_bs, num_ut], 149 self._dtype.real_dtype) 150 # K. Given in dB in the 3GPP tables, hence the division by 10 151 log_mean_k = self.get_param("muK")/10.0 152 # ZSA 153 log_mean_zsa = self.get_param("muZSA") 154 # ZSD 155 log_mean_zsd_los = tf.math.maximum(tf.constant(-0.5, 156 self._dtype.real_dtype), 157 -2.1*(distance_2d/1000.0) - 0.01*tf.abs(h_ut-1.5)+0.75) 158 log_mean_zsd_nlos = tf.math.maximum(tf.constant(-0.5, 159 self._dtype.real_dtype), 160 -2.1*(distance_2d/1000.0) - 0.01*tf.abs(h_ut-1.5)+0.9) 161 log_mean_zsd = tf.where(self.los, log_mean_zsd_los, log_mean_zsd_nlos) 162 163 lsp_log_mean = tf.stack([log_mean_ds, 164 log_mean_asd, 165 log_mean_asa, 166 log_mean_sf, 167 log_mean_k, 168 log_mean_zsa, 169 log_mean_zsd], axis=3) 170 171 ## STD 172 # DS 173 log_std_ds = self.get_param("sigmaDS") 174 # ASD 175 log_std_asd = self.get_param("sigmaASD") 176 # ASA 177 log_std_asa = self.get_param("sigmaASA") 178 # SF. Given in dB in the 3GPP tables, hence the division by 10 179 # O2I and NLoS cases just require the use of a predefined value 180 log_std_sf = self.get_param("sigmaSF")/10.0 181 # K. Given in dB in the 3GPP tables, hence the division by 10. 182 log_std_k = self.get_param("sigmaK")/10.0 183 # ZSA 184 log_std_zsa = self.get_param("sigmaZSA") 185 # ZSD 186 log_std_zsd = self.get_param("sigmaZSD") 187 188 lsp_log_std = tf.stack([log_std_ds, 189 log_std_asd, 190 log_std_asa, 191 log_std_sf, 192 log_std_k, 193 log_std_zsa, 194 log_std_zsd], axis=3) 195 196 self._lsp_log_mean = lsp_log_mean 197 self._lsp_log_std = lsp_log_std 198 199 # ZOD offset 200 fc = self.carrier_frequency/1e9 201 if fc < 6.: 202 fc = tf.cast(6., self._dtype.real_dtype) 203 a = 0.208*log10(fc)-0.782 204 b = tf.constant(25., self._dtype.real_dtype) 205 c = -0.13*log10(fc)+2.03 206 e = 7.66*log10(fc)-5.96 207 zod_offset =(e-tf.math.pow(tf.constant(10., self._dtype.real_dtype), 208 a*log10(tf.maximum(b, distance_2d)) + c - 0.07*(h_ut-1.5))) 209 zod_offset = tf.where(self.los, 210 tf.constant(0., self._dtype.real_dtype),zod_offset) 211 self._zod_offset = zod_offset 212 213 def _compute_pathloss_basic(self): 214 r"""Computes the basic component of the pathloss [dB]""" 215 216 batch_size = self.batch_size 217 num_bs = self.num_bs 218 num_ut = self.num_ut 219 distance_2d = self.distance_2d 220 distance_3d = self.distance_3d 221 fc = self.carrier_frequency # Carrier frequency (Hz) 222 h_bs = self.h_bs 223 h_bs = tf.expand_dims(h_bs, axis=2) # For broadcasting 224 h_ut = self.h_ut 225 h_ut = tf.expand_dims(h_ut, axis=1) # For broadcasting 226 227 # Beak point distance 228 g = ((5./4.)*tf.math.pow(distance_2d/100., 3.) 229 *tf.math.exp(-distance_2d/150.0)) 230 g = tf.where(tf.math.less(distance_2d, 18.), 231 tf.constant(0.0, self._dtype.real_dtype), g) 232 c = g*tf.math.pow((h_ut-13.)/10., 1.5) 233 c = tf.where(tf.math.less(h_ut, 13.), 234 tf.constant(0., self._dtype.real_dtype), c) 235 p = 1./(1.+c) 236 r = sionna.config.tf_rng.uniform(shape=[batch_size, num_bs, num_ut], 237 minval=0.0, maxval=1.0, dtype=self._dtype.real_dtype) 238 r = tf.where(tf.math.less(r, p), 239 tf.constant(1.0, self._dtype.real_dtype), 240 tf.constant(0.0, self._dtype.real_dtype)) 241 242 max_value = h_ut- 1.5 243 # Random uniform integer generation is not supported when maxval and 244 # are not scalar. The following commented would therefore not work. 245 # Therefore, for now, we just sample from a continuous 246 # distribution. 247 #delta = tf.cast(tf.math.floor((max_value-min_value)/3.), tf.int32) 248 #s = tf_rng.uniform(shape=[batch_size, num_bs, num_ut], 249 # minval=0, maxval=delta+1, dtype=tf.int32) 250 #s = tf.cast(s, tf.float32)*3.+min_value 251 s = sionna.config.tf_rng.uniform(shape=[batch_size, num_bs, num_ut], 252 minval=12., maxval=max_value, dtype=self._dtype.real_dtype) 253 # Itc could happen that h_ut = 13m, and therefore max_value < 13m 254 s = tf.where(tf.math.less(s, 12.0), 255 tf.constant(12.0, self._dtype.real_dtype), s) 256 257 h_e = r + (1.-r)*s 258 h_bs_prime = h_bs - h_e 259 h_ut_prime = h_ut - h_e 260 distance_breakpoint = 4*h_bs_prime*h_ut_prime*fc/SPEED_OF_LIGHT 261 262 ## Basic path loss for LoS 263 264 pl_1 = 28.0 + 22.0*log10(distance_3d) + 20.0*log10(fc/1e9) 265 pl_2 = (28.0 + 40.0*log10(distance_3d) + 20.0*log10(fc/1e9) 266 - 9.0*log10(tf.square(distance_breakpoint)+tf.square(h_bs-h_ut))) 267 pl_los = tf.where(tf.math.less(distance_2d, distance_breakpoint), 268 pl_1, pl_2) 269 270 ## Basic pathloss for NLoS and O2I 271 272 pl_3 = (13.54 + 39.08*log10(distance_3d) + 20.0*log10(fc/1e9) 273 - 0.6*(h_ut-1.5)) 274 pl_nlos = tf.math.maximum(pl_los, pl_3) 275 276 ## Set the basic pathloss according to UT state 277 278 # Expand to allow broadcasting with the BS dimension 279 # LoS 280 pl_b = tf.where(self.los, pl_los, pl_nlos) 281 282 self._pl_b = pl_b