antenna.py (27234B)
1 # 2 # SPDX-FileCopyrightText: Copyright (c) 2021-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. 3 # SPDX-License-Identifier: Apache-2.0 4 # 5 """3GPP TR 38.901 antenna modeling""" 6 7 import tensorflow as tf 8 from tensorflow import sin, cos, sqrt 9 10 import numpy as np 11 12 import matplotlib.pyplot as plt 13 from matplotlib.markers import MarkerStyle 14 15 from sionna import SPEED_OF_LIGHT, PI 16 from sionna.utils import log10 17 18 19 class AntennaElement: 20 """Antenna element following the [TR38901]_ specification 21 22 Parameters 23 ---------- 24 25 pattern : str 26 Radiation pattern. Should be "omni" or "38.901". 27 28 slant_angle : float 29 Polarization slant angle [radian] 30 31 dtype : tf.DType 32 Complex datatype to use for internal processing and output. 33 Defaults to `tf.complex64`. 34 """ 35 36 def __init__(self, 37 pattern, 38 slant_angle=0.0, 39 dtype=tf.complex64, 40 ): 41 42 assert pattern in ["omni", "38.901"], \ 43 "The radiation_pattern must be one of [\"omni\", \"38.901\"]." 44 assert dtype.is_complex, "'dtype' must be complex type" 45 46 self._pattern = pattern 47 self._slant_angle = tf.constant(slant_angle, dtype=dtype.real_dtype) 48 49 # Selected the radiation field correspding to the requested pattern 50 if pattern == "omni": 51 self._radiation_pattern = self._radiation_pattern_omni 52 else: 53 self._radiation_pattern = self._radiation_pattern_38901 54 55 self._dtype = dtype 56 57 def field(self, theta, phi): 58 """ 59 Field pattern in the vertical and horizontal polarization (7.3-4/5) 60 61 Inputs 62 ------- 63 theta: 64 Zenith angle wrapped within (0,pi) [radian] 65 66 phi: 67 Azimuth angle wrapped within (-pi, pi) [radian] 68 """ 69 a = sqrt(self._radiation_pattern(theta, phi)) 70 f_theta = a * cos(self._slant_angle) 71 f_phi = a * sin(self._slant_angle) 72 return (f_theta, f_phi) 73 74 def show(self): 75 """ 76 Shows the field pattern of an antenna element 77 """ 78 theta = tf.linspace(0.0, PI, 361) 79 phi = tf.linspace(-PI, PI, 361) 80 a_v = 10*log10(self._radiation_pattern(theta, tf.zeros_like(theta) )) 81 a_h = 10*log10(self._radiation_pattern(PI/2*tf.ones_like(phi) , phi)) 82 83 fig = plt.figure() 84 plt.polar(theta, a_v) 85 fig.axes[0].set_theta_zero_location("N") 86 fig.axes[0].set_theta_direction(-1) 87 plt.title(r"Vertical cut of the radiation pattern ($\phi = 0 $) ") 88 plt.legend([f"{self._pattern}"]) 89 90 fig = plt.figure() 91 plt.polar(phi, a_h) 92 fig.axes[0].set_theta_zero_location("E") 93 plt.title(r"Horizontal cut of the radiation pattern ($\theta = \pi/2$)") 94 plt.legend([f"{self._pattern}"]) 95 96 theta = tf.linspace(0.0, PI, 50) 97 phi = tf.linspace(-PI, PI, 50) 98 phi_grid, theta_grid = tf.meshgrid(phi, theta) 99 a = self._radiation_pattern(theta_grid, phi_grid) 100 x = a * sin(theta_grid) * cos(phi_grid) 101 y = a * sin(theta_grid) * sin(phi_grid) 102 z = a * cos(theta_grid) 103 fig = plt.figure() 104 ax = fig.add_subplot(1,1,1, projection='3d') 105 ax.plot_surface(x, y, z, rstride=1, cstride=1, 106 linewidth=0, antialiased=False, alpha=0.5) 107 ax.view_init(elev=30., azim=-45) 108 plt.xlabel("x") 109 plt.ylabel("y") 110 ax.set_zlabel("z") 111 plt.title(f"Radiation power pattern ({self._pattern})") 112 113 114 ############################### 115 # Utility functions 116 ############################### 117 118 # pylint: disable=unused-argument 119 def _radiation_pattern_omni(self, theta, phi): 120 """ 121 Radiation pattern of an omnidirectional 3D radiation pattern 122 123 Inputs 124 ------- 125 theta: 126 Zenith angle 127 128 phi: 129 Azimuth angle 130 """ 131 return tf.ones_like(theta) 132 133 def _radiation_pattern_38901(self, theta, phi): 134 """ 135 Radiation pattern from TR38901 (Table 7.3-1) 136 137 Inputs 138 ------- 139 theta: 140 Zenith angle wrapped within (0,pi) [radian] 141 142 phi: 143 Azimuth angle wrapped within (-pi, pi) [radian] 144 """ 145 theta_3db = phi_3db = 65/180*PI 146 a_max = sla_v = 30 147 g_e_max = 8 148 a_v = -tf.minimum(12*((theta-PI/2)/theta_3db)**2, sla_v) 149 a_h = -tf.minimum(12*(phi/phi_3db)**2, a_max) 150 a_db = -tf.minimum(-(a_v + a_h), a_max) + g_e_max 151 return 10**(a_db/10) 152 153 def _compute_gain(self): 154 """ 155 Compute antenna gain and directivity through numerical integration 156 """ 157 # Create angular meshgrid 158 theta = tf.linspace(0.0, PI, 181) 159 phi = tf.linspace(-PI, PI, 361) 160 phi_grid, theta_grid = tf.meshgrid(phi, theta) 161 162 # Compute field strength over the grid 163 f_theta, f_phi = self.field(theta_grid, phi_grid) 164 u = f_theta**2 + f_phi**2 165 gain_db = 10*log10(tf.reduce_max(u)) 166 167 # Numerical integration of the field components 168 dtheta = theta[1]-theta[0] 169 dphi = phi[1]-phi[0] 170 po = tf.reduce_sum(u*sin(theta_grid)*dtheta*dphi) 171 172 # Compute directivity 173 u_bar = po/(4*PI) # Equivalent isotropic radiator 174 d = u/u_bar # Directivity grid 175 directivity_db = 10*log10(tf.reduce_max(d)) 176 return (gain_db, directivity_db) 177 178 179 class AntennaPanel: 180 """Antenna panel following the [TR38901]_ specification 181 182 Parameters 183 ----------- 184 185 num_rows : int 186 Number of rows forming the panel 187 188 num_cols : int 189 Number of columns forming the panel 190 191 polarization : str 192 Polarization. Should be "single" or "dual" 193 194 vertical_spacing : float 195 Vertical antenna element spacing [multiples of wavelength] 196 197 horizontal_spacing : float 198 Horizontal antenna element spacing [multiples of wavelength] 199 200 dtype : tf.DType 201 Complex datatype to use for internal processing and output. 202 Defaults to `tf.complex64`. 203 """ 204 205 def __init__(self, 206 num_rows, 207 num_cols, 208 polarization, 209 vertical_spacing, 210 horizontal_spacing, 211 dtype=tf.complex64): 212 213 assert dtype.is_complex, "'dtype' must be complex type" 214 assert polarization in ('single', 'dual'), \ 215 "polarization must be either 'single' or 'dual'" 216 217 self._num_rows = tf.constant(num_rows, tf.int32) 218 self._num_cols = tf.constant(num_cols, tf.int32) 219 self._polarization = polarization 220 self._horizontal_spacing = tf.constant(horizontal_spacing, 221 dtype.real_dtype) 222 self._vertical_spacing = tf.constant(vertical_spacing, dtype.real_dtype) 223 self._dtype = dtype.real_dtype 224 225 # Place the antenna elements of the first polarization direction 226 # on the y-z-plane 227 p = 1 if polarization == 'single' else 2 228 ant_pos = np.zeros([num_rows*num_cols*p, 3]) 229 for i in range(num_rows): 230 for j in range(num_cols): 231 ant_pos[i +j*num_rows] = [ 0, 232 j*horizontal_spacing, 233 -i*vertical_spacing] 234 235 # Center the panel around the origin 236 offset = [ 0, 237 -(num_cols-1)*horizontal_spacing/2, 238 (num_rows-1)*vertical_spacing/2] 239 ant_pos += offset 240 241 # Create the antenna elements of the second polarization direction 242 if polarization == 'dual': 243 ant_pos[num_rows*num_cols:] = ant_pos[:num_rows*num_cols] 244 self._ant_pos = tf.constant(ant_pos, self._dtype.real_dtype) 245 246 @property 247 def ant_pos(self): 248 """Antenna positions in the local coordinate system""" 249 return self._ant_pos 250 251 @property 252 def num_rows(self): 253 """Number of rows""" 254 return self._num_rows 255 256 def num_cols(self): 257 """Number of columns""" 258 return self._num_cols 259 260 @property 261 def porlarization(self): 262 """Polarization ("single" or "dual")""" 263 return self._polarization 264 265 @property 266 def vertical_spacing(self): 267 """Vertical spacing between elements [multiple of wavelength]""" 268 return self._vertical_spacing 269 270 @property 271 def horizontal_spacing(self): 272 """Vertical spacing between elements [multiple of wavelength]""" 273 return self._horizontal_spacing 274 275 def show(self): 276 """Shows the panel geometry""" 277 fig = plt.figure() 278 pos = self._ant_pos[:self._num_rows*self._num_cols] 279 plt.plot(pos[:,1], pos[:,2], marker = "|", markeredgecolor='red', 280 markersize="20", linestyle="None", markeredgewidth="2") 281 for i, p in enumerate(pos): 282 fig.axes[0].annotate(i+1, (p[1], p[2])) 283 if self._polarization == 'dual': 284 pos = self._ant_pos[self._num_rows*self._num_cols:] 285 plt.plot(pos[:,1], pos[:,2], marker = "_", markeredgecolor='black', 286 markersize="20", linestyle="None", markeredgewidth="1") 287 plt.xlabel(r"y ($\lambda_0$)") 288 plt.ylabel(r"z ($\lambda_0$)") 289 plt.title("Antenna Panel") 290 plt.legend(["Polarization 1", "Polarization 2"], loc="upper right") 291 292 293 class PanelArray: 294 # pylint: disable=line-too-long 295 r"""PanelArray(num_rows_per_panel, num_cols_per_panel, polarization, polarization_type, antenna_pattern, carrier_frequency, num_rows=1, num_cols=1, panel_vertical_spacing=None, panel_horizontal_spacing=None, element_vertical_spacing=None, element_horizontal_spacing=None, dtype=tf.complex64) 296 297 Antenna panel array following the [TR38901]_ specification. 298 299 This class is used to create models of the panel arrays used by the 300 transmitters and receivers and that need to be specified when using the 301 :ref:`CDL <cdl>`, :ref:`UMi <umi>`, :ref:`UMa <uma>`, and :ref:`RMa <rma>` 302 models. 303 304 Example 305 -------- 306 307 >>> array = PanelArray(num_rows_per_panel = 4, 308 ... num_cols_per_panel = 4, 309 ... polarization = 'dual', 310 ... polarization_type = 'VH', 311 ... antenna_pattern = '38.901', 312 ... carrier_frequency = 3.5e9, 313 ... num_cols = 2, 314 ... panel_horizontal_spacing = 3.) 315 >>> array.show() 316 317 .. image:: ../figures/panel_array.png 318 319 Parameters 320 ---------- 321 322 num_rows_per_panel : int 323 Number of rows of elements per panel 324 325 num_cols_per_panel : int 326 Number of columns of elements per panel 327 328 polarization : str 329 Polarization, either "single" or "dual" 330 331 polarization_type : str 332 Type of polarization. For single polarization, must be "V" or "H". 333 For dual polarization, must be "VH" or "cross". 334 335 antenna_pattern : str 336 Element radiation pattern, either "omni" or "38.901" 337 338 carrier_frequency : float 339 Carrier frequency [Hz] 340 341 num_rows : int 342 Number of rows of panels. Defaults to 1. 343 344 num_cols : int 345 Number of columns of panels. Defaults to 1. 346 347 panel_vertical_spacing : `None` or float 348 Vertical spacing of panels [multiples of wavelength]. 349 Must be greater than the panel width. 350 If set to `None` (default value), it is set to the panel width + 0.5. 351 352 panel_horizontal_spacing : `None` or float 353 Horizontal spacing of panels [in multiples of wavelength]. 354 Must be greater than the panel height. 355 If set to `None` (default value), it is set to the panel height + 0.5. 356 357 element_vertical_spacing : `None` or float 358 Element vertical spacing [multiple of wavelength]. 359 Defaults to 0.5 if set to `None`. 360 361 element_horizontal_spacing : `None` or float 362 Element horizontal spacing [multiple of wavelength]. 363 Defaults to 0.5 if set to `None`. 364 365 dtype : Complex tf.DType 366 Defines the datatype for internal calculations and the output 367 dtype. Defaults to `tf.complex64`. 368 """ 369 370 def __init__(self, num_rows_per_panel, 371 num_cols_per_panel, 372 polarization, 373 polarization_type, 374 antenna_pattern, 375 carrier_frequency, 376 num_rows=1, 377 num_cols=1, 378 panel_vertical_spacing=None, 379 panel_horizontal_spacing=None, 380 element_vertical_spacing=None, 381 element_horizontal_spacing=None, 382 dtype=tf.complex64): 383 384 assert dtype.is_complex, "'dtype' must be complex type" 385 386 assert polarization in ('single', 'dual'), \ 387 "polarization must be either 'single' or 'dual'" 388 389 # Setting default values for antenna and panel spacings if not 390 # specified by the user 391 # Default spacing for antenna elements is half a wavelength 392 if element_vertical_spacing is None: 393 element_vertical_spacing = 0.5 394 if element_horizontal_spacing is None: 395 element_horizontal_spacing = 0.5 396 # Default values of panel spacing is the pannel size + 0.5 397 if panel_vertical_spacing is None: 398 panel_vertical_spacing = (num_rows_per_panel-1)\ 399 *element_vertical_spacing+0.5 400 if panel_horizontal_spacing is None: 401 panel_horizontal_spacing = (num_cols_per_panel-1)\ 402 *element_horizontal_spacing+0.5 403 404 # Check that panel spacing is larger than panel dimensions 405 assert panel_horizontal_spacing > (num_cols_per_panel-1)\ 406 *element_horizontal_spacing,\ 407 "Pannel horizontal spacing must be larger than the panel width" 408 assert panel_vertical_spacing > (num_rows_per_panel-1)\ 409 *element_vertical_spacing,\ 410 "Pannel vertical spacing must be larger than panel height" 411 412 self._num_rows = tf.constant(num_rows, tf.int32) 413 self._num_cols = tf.constant(num_cols, tf.int32) 414 self._num_rows_per_panel = tf.constant(num_rows_per_panel, tf.int32) 415 self._num_cols_per_panel = tf.constant(num_cols_per_panel, tf.int32) 416 self._polarization = polarization 417 self._polarization_type = polarization_type 418 self._panel_vertical_spacing = tf.constant(panel_vertical_spacing, 419 dtype.real_dtype) 420 self._panel_horizontal_spacing = tf.constant(panel_horizontal_spacing, 421 dtype.real_dtype) 422 self._element_vertical_spacing = tf.constant(element_vertical_spacing, 423 dtype.real_dtype) 424 self._element_horizontal_spacing=tf.constant(element_horizontal_spacing, 425 dtype.real_dtype) 426 self._dtype = dtype 427 428 self._num_panels = tf.constant(num_cols*num_rows, tf.int32) 429 430 p = 1 if polarization == 'single' else 2 431 self._num_panel_ant = tf.constant( num_cols_per_panel* 432 num_rows_per_panel*p, 433 tf.int32) 434 # Total number of antenna elements 435 self._num_ant = self._num_panels * self._num_panel_ant 436 437 # Wavelength (m) 438 self._lambda_0 = tf.constant(SPEED_OF_LIGHT / carrier_frequency, 439 dtype.real_dtype) 440 441 # Create one antenna element for each polarization direction 442 # polarization must be one of {"V", "H", "VH", "cross"} 443 if polarization == 'single': 444 assert polarization_type in ["V", "H"],\ 445 "For single polarization, polarization_type must be 'V' or 'H'" 446 slant_angle = 0 if polarization_type == "V" else PI/2 447 self._ant_pol1 = AntennaElement(antenna_pattern, slant_angle, 448 self._dtype) 449 else: 450 assert polarization_type in ["VH", "cross"],\ 451 "For dual polarization, polarization_type must be 'VH' or 'cross'" 452 slant_angle = 0 if polarization_type == "VH" else -PI/4 453 self._ant_pol1 = AntennaElement(antenna_pattern, slant_angle, 454 self._dtype) 455 self._ant_pol2 = AntennaElement(antenna_pattern, slant_angle+PI/2, 456 self._dtype) 457 458 # Compose array from panels 459 ant_pos = np.zeros([self._num_ant, 3]) 460 panel = AntennaPanel(num_rows_per_panel, num_cols_per_panel, 461 polarization, element_vertical_spacing, element_horizontal_spacing, 462 dtype) 463 pos = panel.ant_pos 464 count = 0 465 num_panel_ant = self._num_panel_ant 466 for j in range(num_cols): 467 for i in range(num_rows): 468 offset = [ 0, 469 j*panel_horizontal_spacing, 470 -i*panel_vertical_spacing] 471 new_pos = pos + offset 472 ant_pos[count*num_panel_ant:(count+1)*num_panel_ant] = new_pos 473 count += 1 474 475 # Center the entire panel array around the orgin of the y-z plane 476 offset = [ 0, 477 -(num_cols-1)*panel_horizontal_spacing/2, 478 (num_rows-1)*panel_vertical_spacing/2] 479 ant_pos += offset 480 481 # Scale antenna element positions by the wavelength 482 ant_pos *= self._lambda_0 483 self._ant_pos = tf.constant(ant_pos, dtype.real_dtype) 484 485 # Compute indices of antennas for polarization directions 486 ind = np.arange(0, self._num_ant) 487 ind = np.reshape(ind, [self._num_panels*p, -1]) 488 self._ant_ind_pol1 = tf.constant(np.reshape(ind[::p], [-1]), tf.int32) 489 if polarization == 'single': 490 self._ant_ind_pol2 = tf.constant(np.array([]), tf.int32) 491 else: 492 self._ant_ind_pol2 = tf.constant(np.reshape( 493 ind[1:self._num_panels*p:2], [-1]), tf.int32) 494 495 # Get positions of antenna elements for each polarization direction 496 self._ant_pos_pol1 = tf.gather(self._ant_pos, self._ant_ind_pol1, 497 axis=0) 498 self._ant_pos_pol2 = tf.gather(self._ant_pos, self._ant_ind_pol2, 499 axis=0) 500 501 @property 502 def num_rows(self): 503 """Number of rows of panels""" 504 return self._num_rows 505 506 @property 507 def num_cols(self): 508 """Number of columns of panels""" 509 return self._num_cols 510 511 @property 512 def num_rows_per_panel(self): 513 """Number of rows of elements per panel""" 514 return self._num_rows_per_panel 515 516 @property 517 def num_cols_per_panel(self): 518 """Number of columns of elements per panel""" 519 return self._num_cols_per_panel 520 521 @property 522 def polarization(self): 523 """Polarization ("single" or "dual")""" 524 return self._polarization 525 526 @property 527 def polarization_type(self): 528 """Polarization type. "V" or "H" for single polarization. 529 "VH" or "cross" for dual polarization.""" 530 return self._polarization_type 531 532 @property 533 def panel_vertical_spacing(self): 534 """Vertical spacing between the panels [multiple of wavelength]""" 535 return self._panel_vertical_spacing 536 537 @property 538 def panel_horizontal_spacing(self): 539 """Horizontal spacing between the panels [multiple of wavelength]""" 540 return self._panel_horizontal_spacing 541 542 @property 543 def element_vertical_spacing(self): 544 """Vertical spacing between the antenna elements within a panel 545 [multiple of wavelength]""" 546 return self._element_vertical_spacing 547 548 @property 549 def element_horizontal_spacing(self): 550 """Horizontal spacing between the antenna elements within a panel 551 [multiple of wavelength]""" 552 return self._element_horizontal_spacing 553 554 @property 555 def num_panels(self): 556 """Number of panels""" 557 return self._num_panels 558 559 @property 560 def num_panels_ant(self): 561 """Number of antenna elements per panel""" 562 return self._num_panel_ant 563 564 @property 565 def num_ant(self): 566 """Total number of antenna elements""" 567 return self._num_ant 568 569 @property 570 def ant_pol1(self): 571 """Field of an antenna element with the first polarization direction""" 572 return self._ant_pol1 573 574 @property 575 def ant_pol2(self): 576 """Field of an antenna element with the second polarization direction. 577 Only defined with dual polarization.""" 578 assert self._polarization == 'dual',\ 579 "This property is not defined with single polarization" 580 return self._ant_pol2 581 582 @property 583 def ant_pos(self): 584 """Positions of the antennas""" 585 return self._ant_pos 586 587 @property 588 def ant_ind_pol1(self): 589 """Indices of antenna elements with the first polarization direction""" 590 return self._ant_ind_pol1 591 592 @property 593 def ant_ind_pol2(self): 594 """Indices of antenna elements with the second polarization direction. 595 Only defined with dual polarization.""" 596 assert self._polarization == 'dual',\ 597 "This property is not defined with single polarization" 598 return self._ant_ind_pol2 599 600 @property 601 def ant_pos_pol1(self): 602 """Positions of the antenna elements with the first polarization 603 direction""" 604 return self._ant_pos_pol1 605 606 @property 607 def ant_pos_pol2(self): 608 """Positions of antenna elements with the second polarization direction. 609 Only defined with dual polarization.""" 610 assert self._polarization == 'dual',\ 611 "This property is not defined with single polarization" 612 return self._ant_pos_pol2 613 614 def show(self): 615 """Show the panel array geometry""" 616 if self._polarization == 'single': 617 if self._polarization_type == 'H': 618 marker_p1 = MarkerStyle("_").get_marker() 619 else: 620 marker_p1 = MarkerStyle("|") 621 else: # 'dual' 622 if self._polarization_type == 'cross': 623 marker_p1 = (2, 0, -45) 624 marker_p2 = (2, 0, 45) 625 else: 626 marker_p1 = MarkerStyle("_").get_marker() 627 marker_p2 = MarkerStyle("|").get_marker() 628 629 fig = plt.figure() 630 pos_pol1 = self._ant_pos_pol1 631 plt.plot(pos_pol1[:,1], pos_pol1[:,2], 632 marker=marker_p1, markeredgecolor='red', 633 markersize="20", linestyle="None", markeredgewidth="2") 634 for i, p in enumerate(pos_pol1): 635 fig.axes[0].annotate(self._ant_ind_pol1[i].numpy()+1, (p[1], p[2])) 636 if self._polarization == 'dual': 637 pos_pol2 = self._ant_pos_pol2 638 plt.plot(pos_pol2[:,1], pos_pol2[:,2], 639 marker=marker_p2, markeredgecolor='black', # pylint: disable=possibly-used-before-assignment 640 markersize="20", linestyle="None", markeredgewidth="1") 641 plt.xlabel("y (m)") 642 plt.ylabel("z (m)") 643 plt.title("Panel Array") 644 plt.legend(["Polarization 1", "Polarization 2"], loc="upper right") 645 646 def show_element_radiation_pattern(self): 647 """Show the radiation field of antenna elements forming the panel""" 648 self._ant_pol1.show() 649 650 class Antenna(PanelArray): 651 # pylint: disable=line-too-long 652 r"""Antenna(polarization, polarization_type, antenna_pattern, carrier_frequency, dtype=tf.complex64) 653 654 Single antenna following the [TR38901]_ specification. 655 656 This class is a special case of :class:`~sionna.channel.tr38901.PanelArray`, 657 and can be used in lieu of it. 658 659 Parameters 660 ---------- 661 polarization : str 662 Polarization, either "single" or "dual" 663 664 polarization_type : str 665 Type of polarization. For single polarization, must be "V" or "H". 666 For dual polarization, must be "VH" or "cross". 667 668 antenna_pattern : str 669 Element radiation pattern, either "omni" or "38.901" 670 671 carrier_frequency : float 672 Carrier frequency [Hz] 673 674 dtype : Complex tf.DType 675 Defines the datatype for internal calculations and the output 676 dtype. Defaults to `tf.complex64`. 677 """ 678 679 def __init__(self, polarization, 680 polarization_type, 681 antenna_pattern, 682 carrier_frequency, 683 dtype=tf.complex64): 684 685 super().__init__(num_rows_per_panel=1, 686 num_cols_per_panel=1, 687 polarization=polarization, 688 polarization_type=polarization_type, 689 antenna_pattern=antenna_pattern, 690 carrier_frequency=carrier_frequency, 691 dtype=dtype) 692 693 class AntennaArray(PanelArray): 694 # pylint: disable=line-too-long 695 r"""AntennaArray(num_rows, num_cols, polarization, polarization_type, antenna_pattern, carrier_frequency, vertical_spacing, horizontal_spacing, dtype=tf.complex64) 696 697 Antenna array following the [TR38901]_ specification. 698 699 This class is a special case of :class:`~sionna.channel.tr38901.PanelArray`, 700 and can used in lieu of it. 701 702 Parameters 703 ---------- 704 num_rows : int 705 Number of rows of elements 706 707 num_cols : int 708 Number of columns of elements 709 710 polarization : str 711 Polarization, either "single" or "dual" 712 713 polarization_type : str 714 Type of polarization. For single polarization, must be "V" or "H". 715 For dual polarization, must be "VH" or "cross". 716 717 antenna_pattern : str 718 Element radiation pattern, either "omni" or "38.901" 719 720 carrier_frequency : float 721 Carrier frequency [Hz] 722 723 vertical_spacing : `None` or float 724 Element vertical spacing [multiple of wavelength]. 725 Defaults to 0.5 if set to `None`. 726 727 horizontal_spacing : `None` or float 728 Element horizontal spacing [multiple of wavelength]. 729 Defaults to 0.5 if set to `None`. 730 731 dtype : Complex tf.DType 732 Defines the datatype for internal calculations and the output 733 dtype. Defaults to `tf.complex64`. 734 """ 735 736 def __init__(self, num_rows, 737 num_cols, 738 polarization, 739 polarization_type, 740 antenna_pattern, 741 carrier_frequency, 742 vertical_spacing=None, 743 horizontal_spacing=None, 744 dtype=tf.complex64): 745 746 super().__init__(num_rows_per_panel=num_rows, 747 num_cols_per_panel=num_cols, 748 polarization=polarization, 749 polarization_type=polarization_type, 750 antenna_pattern=antenna_pattern, 751 carrier_frequency=carrier_frequency, 752 element_vertical_spacing=vertical_spacing, 753 element_horizontal_spacing=horizontal_spacing, 754 dtype=dtype)