lsp.py (19125B)
1 # 2 # SPDX-FileCopyrightText: Copyright (c) 2021-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. 3 # SPDX-License-Identifier: Apache-2.0 4 # 5 """ 6 Class for sampling large scale parameters (LSPs) and pathloss following the 7 3GPP TR38.901 specifications and according to a channel simulation scenario. 8 """ 9 10 11 import tensorflow as tf 12 from sionna.utils import log10 13 from sionna.utils import matrix_sqrt 14 from sionna import config 15 16 class LSP: 17 r""" 18 Class for conveniently storing LSPs 19 20 Parameters 21 ----------- 22 23 ds : [batch size, num tx, num rx], tf.float 24 RMS delay spread [s] 25 26 asd : [batch size, num tx, num rx], tf.float 27 azimuth angle spread of departure [deg] 28 29 asa : [batch size, num tx, num rx], tf.float 30 azimuth angle spread of arrival [deg] 31 32 sf : [batch size, num tx, num rx], tf.float 33 shadow fading 34 35 k_factor : [batch size, num tx, num rx], tf.float 36 Rician K-factor. Only used for LoS. 37 38 zsa : [batch size, num tx, num rx], tf.float 39 Zenith angle spread of arrival [deg] 40 41 zsd: [batch size, num tx, num rx], tf.float 42 Zenith angle spread of departure [deg] 43 """ 44 45 def __init__(self, ds, asd, asa, sf, k_factor, zsa, zsd): 46 self.ds = ds 47 self.asd = asd 48 self.asa = asa 49 self.sf = sf 50 self.k_factor = k_factor 51 self.zsa = zsa 52 self.zsd = zsd 53 54 class LSPGenerator: 55 """ 56 Sample large scale parameters (LSP) and pathloss given a channel scenario, 57 e.g., UMa, UMi, RMa. 58 59 This class implements steps 1 to 4 of the TR 38.901 specifications 60 (section 7.5), as well as path-loss generation (Section 7.4.1) with O2I 61 low- and high- loss models (Section 7.4.3). 62 63 Note that a global scenario is set for the entire batches when instantiating 64 this class (UMa, UMi, or RMa). However, each UT-BS link can have its 65 specific state (LoS, NLoS, or indoor). 66 67 The batch size is set by the ``scenario`` given as argument when 68 constructing the class. 69 70 Parameters 71 ---------- 72 scenario : :class:`~sionna.channel.tr38901.SystemLevelScenario`` 73 Scenario used to generate LSPs 74 75 Input 76 ----- 77 None 78 79 Output 80 ------ 81 An `LSP` instance storing realization of LSPs. 82 """ 83 84 def __init__(self, scenario): 85 self._scenario = scenario 86 87 def sample_pathloss(self): 88 """ 89 Generate pathlosses [dB] for each BS-UT link. 90 91 Input 92 ------ 93 None 94 95 Output 96 ------- 97 A tensor with shape [batch size, number of BSs, number of UTs] of 98 pathloss [dB] for each BS-UT link 99 """ 100 101 # Pre-computed basic pathloss 102 pl_b = self._scenario.basic_pathloss 103 104 ## O2I penetration 105 if self._scenario.o2i_model == 'low': 106 pl_o2i = self._o2i_low_loss() 107 else: # 'high' 108 pl_o2i = self._o2i_high_loss() 109 110 ## Total path loss, including O2I penetration 111 pl = pl_b + pl_o2i 112 113 return pl 114 115 def __call__(self): 116 117 # LSPs are assumed to follow a log-normal distribution. 118 # They are generated in the log-domain (where they follow a normal 119 # distribution), where they are correlated as indicated in TR38901 120 # specification (Section 7.5, step 4) 121 122 s = config.tf_rng.normal(shape=[self._scenario.batch_size, 123 self._scenario.num_bs, 124 self._scenario.num_ut, 7], 125 dtype=self._scenario.dtype.real_dtype) 126 127 ## Applyting cross-LSP correlation 128 s = tf.expand_dims(s, axis=4) 129 s = self._cross_lsp_correlation_matrix_sqrt@s 130 s = tf.squeeze(s, axis=4) 131 132 ## Applying spatial correlation 133 s = tf.expand_dims(tf.transpose(s, [0, 1, 3, 2]), axis=3) 134 s = tf.matmul(s, self._spatial_lsp_correlation_matrix_sqrt, 135 transpose_b=True) 136 s = tf.transpose(tf.squeeze(s, axis=3), [0, 1, 3, 2]) 137 138 ## Scaling and transposing LSPs to the right mean and variance 139 lsp_log_mean = self._scenario.lsp_log_mean 140 lsp_log_std = self._scenario.lsp_log_std 141 lsp_log = lsp_log_std*s + lsp_log_mean 142 143 ## Mapping to linear domain 144 lsp = tf.math.pow(tf.constant(10., self._scenario.dtype.real_dtype), 145 lsp_log) 146 147 # Limit the RMS azimuth arrival (ASA) and azimuth departure (ASD) 148 # spread values to 104 degrees 149 # Limit the RMS zenith arrival (ZSA) and zenith departure (ZSD) 150 # spread values to 52 degrees 151 lsp = LSP( ds = lsp[:,:,:,0], 152 asd = tf.math.minimum(lsp[:,:,:,1], 104.0), 153 asa = tf.math.minimum(lsp[:,:,:,2], 104.0), 154 sf = lsp[:,:,:,3], 155 k_factor = lsp[:,:,:,4], 156 zsa = tf.math.minimum(lsp[:,:,:,5], 52.0), 157 zsd = tf.math.minimum(lsp[:,:,:,6], 52.0) 158 ) 159 160 return lsp 161 162 def topology_updated_callback(self): 163 """ 164 Updates internal quantities. Must be called at every update of the 165 scenario that changes the state of UTs or their locations. 166 167 Input 168 ------ 169 None 170 171 Output 172 ------ 173 None 174 """ 175 176 # Pre-computing these quantities avoid unnecessary calculations at every 177 # generation of new LSPs 178 179 # Compute cross-LSP correlation matrix 180 self._compute_cross_lsp_correlation_matrix() 181 182 # Compute LSP spatial correlation matrix 183 self._compute_lsp_spatial_correlation_sqrt() 184 185 ######################################## 186 # Internal utility methods 187 ######################################## 188 189 def _compute_cross_lsp_correlation_matrix(self): 190 """ 191 Compute and store as attribute the square-root of the cross-LSPs 192 correlation matrices for each BS-UT link, and then the corresponding 193 matrix square root for filtering. 194 195 The resulting tensor is of shape 196 [batch size, number of BSs, number of UTs, 7, 7) 197 7 being the number of LSPs to correlate. 198 199 Input 200 ------ 201 None 202 203 Output 204 ------- 205 None 206 """ 207 208 # The following 7 LSPs are correlated: 209 # DS, ASA, ASD, SF, K, ZSA, ZSD 210 # We create the correlation matrix initialized to the identity matrix 211 cross_lsp_corr_mat = tf.eye(7, 7,batch_shape=[self._scenario.batch_size, 212 self._scenario.num_bs, self._scenario.num_ut], 213 dtype=self._scenario.dtype.real_dtype) 214 215 # Tensors of bool indicating the state of UT-BS links 216 # Indoor 217 indoor_bool = tf.tile(tf.expand_dims(self._scenario.indoor, axis=1), 218 [1, self._scenario.num_bs, 1]) 219 # LoS 220 los_bool = self._scenario.los 221 # NLoS (outdoor) 222 nlos_bool = tf.logical_and(tf.logical_not(self._scenario.los), 223 tf.logical_not(indoor_bool)) 224 # Expand to allow broadcasting with the BS dimension 225 indoor_bool = tf.expand_dims(tf.expand_dims(indoor_bool, axis=3),axis=4) 226 los_bool = tf.expand_dims(tf.expand_dims(los_bool, axis=3),axis=4) 227 nlos_bool = tf.expand_dims(tf.expand_dims(nlos_bool, axis=3),axis=4) 228 229 # Internal function that adds to the correlation matrix ``mat`` 230 # ``cross_lsp_corr_mat`` the parameter ``parameter_name`` at location 231 # (m,n) 232 def _add_param(mat, parameter_name, m, n): 233 # Mask to put the parameters in the right spot of the 7x7 234 # correlation matrix 235 mask = tf.scatter_nd([[m,n],[n,m]], 236 tf.constant([1.0, 1.0], self._scenario.dtype.real_dtype), [7,7]) 237 mask = tf.reshape(mask, [1,1,1,7,7]) 238 # Get the parameter value according to the link scenario 239 update = self._scenario.get_param(parameter_name) 240 update = tf.expand_dims(tf.expand_dims(update, axis=3), axis=4) 241 # Add update 242 mat = mat + update*mask 243 return mat 244 245 # Fill off-diagonal elements of the correlation matrices 246 # ASD vs DS 247 cross_lsp_corr_mat = _add_param(cross_lsp_corr_mat, 'corrASDvsDS', 0, 1) 248 # ASA vs DS 249 cross_lsp_corr_mat = _add_param(cross_lsp_corr_mat, 'corrASAvsDS', 0, 2) 250 # ASA vs SF 251 cross_lsp_corr_mat = _add_param(cross_lsp_corr_mat, 'corrASAvsSF', 3, 2) 252 # ASD vs SF 253 cross_lsp_corr_mat = _add_param(cross_lsp_corr_mat, 'corrASDvsSF', 3, 1) 254 # DS vs SF 255 cross_lsp_corr_mat = _add_param(cross_lsp_corr_mat, 'corrDSvsSF', 3, 0) 256 # ASD vs ASA 257 cross_lsp_corr_mat = _add_param(cross_lsp_corr_mat, 'corrASDvsASA', 1,2) 258 # ASD vs K 259 cross_lsp_corr_mat = _add_param(cross_lsp_corr_mat, 'corrASDvsK', 1, 4) 260 # ASA vs K 261 cross_lsp_corr_mat = _add_param(cross_lsp_corr_mat, 'corrASAvsK', 2, 4) 262 # DS vs K 263 cross_lsp_corr_mat = _add_param(cross_lsp_corr_mat, 'corrDSvsK', 0, 4) 264 # SF vs K 265 cross_lsp_corr_mat = _add_param(cross_lsp_corr_mat, 'corrSFvsK', 3, 4) 266 # ZSD vs SF 267 cross_lsp_corr_mat = _add_param(cross_lsp_corr_mat, 'corrZSDvsSF', 3, 6) 268 # ZSA vs SF 269 cross_lsp_corr_mat = _add_param(cross_lsp_corr_mat, 'corrZSAvsSF', 3, 5) 270 # ZSD vs K 271 cross_lsp_corr_mat = _add_param(cross_lsp_corr_mat, 'corrZSDvsK', 6, 4) 272 # ZSA vs K 273 cross_lsp_corr_mat = _add_param(cross_lsp_corr_mat, 'corrZSAvsK', 5, 4) 274 # ZSD vs DS 275 cross_lsp_corr_mat = _add_param(cross_lsp_corr_mat, 'corrZSDvsDS', 6, 0) 276 # ZSA vs DS 277 cross_lsp_corr_mat = _add_param(cross_lsp_corr_mat, 'corrZSAvsDS', 5, 0) 278 # ZSD vs ASD 279 cross_lsp_corr_mat = _add_param(cross_lsp_corr_mat, 'corrZSDvsASD', 6,1) 280 # ZSA vs ASD 281 cross_lsp_corr_mat = _add_param(cross_lsp_corr_mat, 'corrZSAvsASD', 5,1) 282 # ZSD vs ASA 283 cross_lsp_corr_mat = _add_param(cross_lsp_corr_mat, 'corrZSDvsASA', 6,2) 284 # ZSA vs ASA 285 cross_lsp_corr_mat = _add_param(cross_lsp_corr_mat, 'corrZSAvsASA', 5,2) 286 # ZSD vs ZSA 287 cross_lsp_corr_mat = _add_param(cross_lsp_corr_mat, 'corrZSDvsZSA', 5,6) 288 289 # Compute and store the square root of the cross-LSP correlation 290 # matrix 291 self._cross_lsp_correlation_matrix_sqrt = matrix_sqrt( 292 cross_lsp_corr_mat) 293 294 def _compute_lsp_spatial_correlation_sqrt(self): 295 """ 296 Compute the square root of the spatial correlation matrices of LSPs. 297 298 The LSPs are correlated accross users according to the distance between 299 the users. Each LSP is spatially correlated according to a different 300 spatial correlation matrix. 301 302 The links involving different BSs are not correlated. 303 UTs in different state (LoS, NLoS, O2I) are not assumed to be 304 correlated. 305 306 The correlation of the LSPs X of two UTs in the same state related to 307 the links of these UTs to a same BS is 308 309 .. math:: 310 C(X_1,X_2) = exp(-d/D_X) 311 312 where :math:`d` is the distance between the UTs in the X-Y plane (2D 313 distance) and D_X the correlation distance of LSP X. 314 315 The resulting tensor if of shape 316 [batch size, number of BSs, 7, number of UTs, number of UTs) 317 7 being the number of LSPs. 318 319 Input 320 ------ 321 None 322 323 Output 324 ------- 325 None 326 """ 327 328 # Tensors of bool indicating which pair of UTs to correlate. 329 # Pairs of UTs that are correlated are those that share the same state 330 # (indoor, LoS, or NLoS). 331 # Indoor 332 indoor = tf.tile(tf.expand_dims(self._scenario.indoor, axis=1), 333 [1, self._scenario.num_bs, 1]) 334 # LoS 335 los_ut = self._scenario.los 336 los_pair_bool = tf.logical_and(tf.expand_dims(los_ut, axis=3), 337 tf.expand_dims(los_ut, axis=2)) 338 # NLoS 339 nlos_ut = tf.logical_and(tf.logical_not(self._scenario.los), 340 tf.logical_not(indoor)) 341 nlos_pair_bool = tf.logical_and(tf.expand_dims(nlos_ut, axis=3), 342 tf.expand_dims(nlos_ut, axis=2)) 343 # O2I 344 o2i_pair_bool = tf.logical_and(tf.expand_dims(indoor, axis=3), 345 tf.expand_dims(indoor, axis=2)) 346 347 # Stacking the correlation matrix 348 # One correlation matrix per LSP 349 filtering_matrices = [] 350 distance_scaling_matrices = [] 351 for parameter_name in ('corrDistDS', 'corrDistASD', 'corrDistASA', 352 'corrDistSF', 'corrDistK', 'corrDistZSA', 'corrDistZSD'): 353 # Matrix used for filtering and scaling the 2D distances 354 # For each pair of UTs, the entry is set to 0 if the UTs are in 355 # different states, -1/(correlation distance) otherwise. 356 # The correlation distance is different for each LSP. 357 filtering_matrix = tf.eye(self._scenario.num_ut, 358 self._scenario.num_ut, batch_shape=[self._scenario.batch_size, 359 self._scenario.num_bs], dtype=self._scenario.dtype.real_dtype) 360 distance_scaling_matrix = self._scenario.get_param(parameter_name) 361 distance_scaling_matrix = tf.tile(tf.expand_dims( 362 distance_scaling_matrix, axis=3), 363 [1, 1, 1, self._scenario.num_ut]) 364 distance_scaling_matrix = -1./distance_scaling_matrix 365 # LoS 366 filtering_matrix = tf.where(los_pair_bool, 367 tf.constant(1.0, self._scenario.dtype.real_dtype), 368 filtering_matrix) 369 # NLoS 370 filtering_matrix = tf.where(nlos_pair_bool, 371 tf.constant(1.0, self._scenario.dtype.real_dtype), 372 filtering_matrix) 373 # indoor 374 filtering_matrix = tf.where(o2i_pair_bool, 375 tf.constant(1.0, self._scenario.dtype.real_dtype), 376 filtering_matrix) 377 # Stacking 378 filtering_matrices.append(filtering_matrix) 379 distance_scaling_matrices.append(distance_scaling_matrix) 380 filtering_matrices = tf.stack(filtering_matrices, axis=2) 381 distance_scaling_matrices = tf.stack(distance_scaling_matrices, axis=2) 382 383 ut_dist_2d = self._scenario.matrix_ut_distance_2d 384 # Adding a dimension for broadcasting with BS 385 ut_dist_2d = tf.expand_dims(tf.expand_dims(ut_dist_2d, axis=1), axis=2) 386 387 # Correlation matrix 388 spatial_lsp_correlation = (tf.math.exp( 389 ut_dist_2d*distance_scaling_matrices)*filtering_matrices) 390 391 # Compute and store the square root of the spatial correlation matrix 392 self._spatial_lsp_correlation_matrix_sqrt = matrix_sqrt( 393 spatial_lsp_correlation) 394 395 def _o2i_low_loss(self): 396 """ 397 Compute for each BS-UT link the pathloss due to the O2I penetration loss 398 in dB with the low-loss model. 399 See section 7.4.3.1 of 38.901 specification. 400 401 UTs located outdoor (LoS and NLoS) get O2I pathloss of 0dB. 402 403 Input 404 ----- 405 None 406 407 Output 408 ------- 409 Tensor with shape 410 [batch size, number of BSs, number of UTs] 411 containing the O2I penetration low-loss in dB for each BS-UT link 412 """ 413 414 fc = self._scenario.carrier_frequency/1e9 # Carrier frequency (GHz) 415 batch_size = self._scenario.batch_size 416 num_ut = self._scenario.num_ut 417 num_bs = self._scenario.num_bs 418 419 # Material penetration losses 420 # fc must be in GHz 421 l_glass = 2. + 0.2*fc 422 l_concrete = 5. + 4.*fc 423 424 # Path loss through external wall 425 pl_tw = 5.0 - 10.*log10(0.3*tf.math.pow(tf.constant(10., 426 self._scenario.dtype.real_dtype), -l_glass/10.0) + 0.7*tf.math.pow( 427 tf.constant(10., self._scenario.dtype.real_dtype), 428 -l_concrete/10.0)) 429 430 # Filtering-out the O2I pathloss for UTs located outdoor 431 indoor_mask = tf.where(self._scenario.indoor, tf.constant(1.0, 432 self._scenario.dtype.real_dtype), tf.zeros([batch_size, num_ut], 433 self._scenario.dtype.real_dtype)) 434 indoor_mask = tf.expand_dims(indoor_mask, axis=1) 435 pl_tw = pl_tw*indoor_mask 436 437 # Pathloss due to indoor propagation 438 # The indoor 2D distance for outdoor UTs is 0 439 pl_in = 0.5*self._scenario.distance_2d_in 440 441 # Random path loss component 442 # Gaussian distributed with standard deviation 4.4 in dB 443 pl_rnd = config.tf_rng.normal(shape=[batch_size, num_bs, num_ut], 444 mean=0.0, 445 stddev=4.4, 446 dtype=self._scenario.dtype.real_dtype) 447 pl_rnd = pl_rnd*indoor_mask 448 449 return pl_tw + pl_in + pl_rnd 450 451 def _o2i_high_loss(self): 452 """ 453 Compute for each BS-UT link the pathloss due to the O2I penetration loss 454 in dB with the high-loss model. 455 See section 7.4.3.1 of 38.901 specification. 456 457 UTs located outdoor (LoS and NLoS) get O2I pathloss of 0dB. 458 459 Input 460 ----- 461 None 462 463 Output 464 ------- 465 Tensor with shape 466 [batch size, number of BSs, number of UTs] 467 containing the O2I penetration low-loss in dB for each BS-UT link 468 """ 469 470 fc = self._scenario.carrier_frequency/1e9 # Carrier frequency (GHz) 471 batch_size = self._scenario.batch_size 472 num_ut = self._scenario.num_ut 473 num_bs = self._scenario.num_bs 474 475 # Material penetration losses 476 # fc must be in GHz 477 l_iirglass = 23. + 0.3*fc 478 l_concrete = 5. + 4.*fc 479 480 # Path loss through external wall 481 pl_tw = 5.0 - 10.*log10(0.7*tf.math.pow(tf.constant(10., 482 self._scenario.dtype.real_dtype), -l_iirglass/10.0) 483 + 0.3*tf.math.pow(tf.constant(10., 484 self._scenario.dtype.real_dtype), -l_concrete/10.0)) 485 486 # Filtering-out the O2I pathloss for outdoor UTs 487 indoor_mask = tf.where(self._scenario.indoor, 1.0, 488 tf.zeros([batch_size, num_ut], self._scenario.dtype.real_dtype)) 489 indoor_mask = tf.expand_dims(indoor_mask, axis=1) 490 pl_tw = pl_tw*indoor_mask 491 492 # Pathloss due to indoor propagation 493 # The indoor 2D distance for outdoor UTs is 0 494 pl_in = 0.5*self._scenario.distance_2d_in 495 496 # Random path loss component 497 # Gaussian distributed with standard deviation 6.5 in dB for the 498 # high loss model 499 pl_rnd = config.tf_rng.normal(shape=[batch_size, num_bs, num_ut], 500 mean=0.0, 501 stddev=6.5, 502 dtype=self._scenario.dtype.real_dtype) 503 pl_rnd = pl_rnd*indoor_mask 504 505 return pl_tw + pl_in + pl_rnd