antenna.py (22303B)
1 # 2 # SPDX-FileCopyrightText: Copyright (c) 2021-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. 3 # SPDX-License-Identifier: Apache-2.0 4 # 5 """ 6 Implements classes and methods related to antennas. 7 """ 8 9 import numpy as np 10 import matplotlib.pyplot as plt 11 from matplotlib import cm 12 from sionna.constants import PI 13 import tensorflow as tf 14 from collections.abc import Sequence 15 16 class Antenna: 17 r""" 18 Class implementing an antenna 19 20 Creates an antenna object with an either predefined or custom antenna 21 pattern. Can be single or dual polarized. 22 23 Parameters 24 ---------- 25 pattern : str, callable, or length-2 sequence of callables 26 Antenna pattern. Either one of 27 ["iso", "dipole", "hw_dipole", "tr38901"], 28 or a callable, or a length-2 sequence of callables defining 29 antenna patterns. In the latter case, the antenna is dual 30 polarized and each callable defines the antenna pattern 31 in one of the two orthogonal polarization directions. 32 An antenna pattern is a callable that takes as inputs vectors of 33 zenith and azimuth angles of the same length and returns for each 34 pair the corresponding zenith and azimuth patterns. 35 36 polarization : str or None 37 Type of polarization. For single polarization, must be "V" (vertical) 38 or "H" (horizontal). For dual polarization, must be "VH" or "cross". 39 Only needed if ``pattern`` is a string. 40 41 polarization_model: int, one of [1,2] 42 Polarization model to be used. Options `1` and `2` 43 refer to :func:`~sionna.rt.antenna.polarization_model_1` 44 and :func:`~sionna.rt.antenna.polarization_model_2`, 45 respectively. 46 Defaults to `2`. 47 48 dtype : tf.complex64 or tf.complex128 49 Datatype used for all computations. 50 Defaults to `tf.complex64`. 51 52 Example 53 ------- 54 >>> Antenna("tr38901", "VH") 55 """ 56 def __init__(self, 57 pattern, 58 polarization=None, 59 polarization_model=2, 60 dtype=tf.complex64 61 ): 62 63 if dtype not in (tf.complex64, tf.complex128): 64 raise ValueError("`dtype` must be tf.complex64 or tf.complex128`") 65 self._dtype = dtype = dtype 66 67 if polarization_model not in [1,2]: 68 raise ValueError("`polarization_model` must be 1 or 2") 69 self._polarization_model = polarization_model 70 71 # Pattern is provided as string 72 if isinstance(pattern, str): 73 74 # Set correct pattern 75 if pattern=="iso": 76 pattern = iso_pattern 77 elif pattern=="dipole": 78 pattern = dipole_pattern 79 elif pattern=="hw_dipole": 80 pattern = hw_dipole_pattern 81 elif pattern=="tr38901": 82 pattern = tr38901_pattern 83 else: 84 raise ValueError("Unknown antenna pattern") 85 86 # Set slant angles 87 if polarization=="V": 88 slant_angles = [0.0] 89 elif polarization=="H": 90 slant_angles = [PI/2] 91 elif polarization=="VH": 92 slant_angles = [0.0, PI/2] 93 elif polarization=="cross": 94 slant_angles = [-PI/4, PI/4] 95 else: 96 raise ValueError("Unknown polarization") 97 98 # Create antenna patterns with slant angles 99 self._patterns = [] 100 for sa in slant_angles: 101 f = self.pattern_with_slant_angle(pattern, sa) 102 self._patterns.append(f) 103 104 # Pattern is a callable 105 elif callable(pattern): 106 self._patterns = [pattern] 107 108 # Pattern is sequence of callables 109 elif isinstance(pattern, Sequence): 110 if len(pattern) > 2: 111 msg = "An antennta cannot have more than two patterns." 112 raise ValueError(msg) 113 for p in pattern: 114 if not callable(p): 115 msg = "Each element of antenna_pattern must be callable" 116 raise ValueError(msg) 117 self._patterns = pattern 118 119 # Unsupported pattern 120 else: 121 raise ValueError("Unsupported pattern") 122 123 @property 124 def patterns(self): 125 """ 126 `list`, `callable` : Antenna patterns for one or two 127 polarization directions 128 """ 129 return self._patterns 130 131 def pattern_with_slant_angle(self, pattern, slant_angle): 132 """Applies slant angle to antenna pattern""" 133 return lambda theta, phi: pattern(theta, phi, slant_angle, 134 self._polarization_model, self._dtype) 135 136 def compute_gain(pattern, dtype=tf.complex64): 137 # pylint: disable=line-too-long 138 r"""compute_gain(pattern) 139 Computes the directivity, gain, and radiation efficiency of an antenna pattern 140 141 Given a function :math:`f:(\theta,\varphi)\mapsto (C_\theta(\theta, \varphi), C_\varphi(\theta, \varphi))` 142 describing an antenna pattern :eq:`C`, this function computes the gain :math:`G`, 143 directivity :math:`D`, and radiation efficiency :math:`\eta_\text{rad}=G/D` 144 (see :eq:`G` and text below). 145 146 Input 147 ----- 148 pattern : callable 149 A callable that takes as inputs vectors of zenith and azimuth angles of the same 150 length and returns for each pair the corresponding zenith and azimuth patterns. 151 152 Output 153 ------ 154 D : float 155 Directivity :math:`D` 156 157 G : float 158 Gain :math:`G` 159 160 eta_rad : float 161 Radiation efficiency :math:`\eta_\text{rad}` 162 163 Examples 164 -------- 165 >>> compute_gain(tr38901_pattern) 166 (<tf.Tensor: shape=(), dtype=float32, numpy=9.606758>, 167 <tf.Tensor: shape=(), dtype=float32, numpy=6.3095527>, 168 <tf.Tensor: shape=(), dtype=float32, numpy=0.65678275>) 169 """ 170 171 if dtype not in (tf.complex64, tf.complex128): 172 raise ValueError("`dtype` must be tf.complex64 or tf.complex128`") 173 174 # Create angular meshgrid 175 theta = tf.linspace(0.0, PI, 1810) 176 theta = tf.cast(theta, dtype.real_dtype) 177 phi = tf.linspace(-PI, PI, 3610) 178 phi = tf.cast(phi, dtype.real_dtype) 179 180 theta_grid, phi_grid = tf.meshgrid(theta, phi, indexing="ij") 181 182 # Compute the gain 183 c_theta, c_phi = pattern(theta_grid, phi_grid) 184 g = tf.abs(c_theta)**2 + tf.abs(c_phi)**2 185 186 # Find maximum directional gain 187 g_max = tf.reduce_max(g) 188 189 # Compute radiation efficiency 190 dtheta = theta[1]-theta[0] 191 dphi = phi[1]-phi[0] 192 eta_rad = tf.reduce_sum(g*tf.sin(theta_grid)*dtheta*dphi)/(4*PI) 193 194 # Compute directivity 195 d = g_max / eta_rad 196 return d, g_max, eta_rad 197 198 def visualize(pattern): 199 r"""visualize(pattern) 200 Visualizes an antenna pattern 201 202 This function visualizes an antenna pattern with the help of three 203 figures showing the vertical and horizontal cuts as well as a 204 three-dimensional visualization of the antenna gain. 205 206 Input 207 ----- 208 pattern : callable 209 A callable that takes as inputs vectors of zenith and azimuth angles 210 of the same length and returns for each pair the corresponding zenith 211 and azimuth patterns. 212 213 Output 214 ------ 215 : :class:`matplotlib.pyplot.Figure` 216 Vertical cut of the antenna gain 217 218 : :class:`matplotlib.pyplot.Figure` 219 Horizontal cut of the antenna gain 220 221 : :class:`matplotlib.pyplot.Figure` 222 3D visualization of the antenna gain 223 224 Examples 225 -------- 226 >>> fig_v, fig_h, fig_3d = visualize(hw_dipole_pattern) 227 228 .. figure:: ../figures/pattern_vertical.png 229 :align: center 230 :scale: 80% 231 .. figure:: ../figures/pattern_horizontal.png 232 :align: center 233 :scale: 80% 234 .. figure:: ../figures/pattern_3d.png 235 :align: center 236 :scale: 80% 237 """ 238 # Vertical cut 239 theta = np.linspace(0.0, PI, 1000) 240 c_theta, c_phi = pattern(theta, np.zeros_like(theta)) 241 g = np.abs(c_theta)**2 + np.abs(c_phi)**2 242 g = np.where(g==0, 1e-12, g) 243 g_db = 10*np.log10(g) 244 g_db_max = np.max(g_db) 245 g_db_min = np.min(g_db) 246 if g_db_min==g_db_max: 247 g_db_min = -30 248 else: 249 g_db_min = np.maximum(-60., g_db_min) 250 fig_v = plt.figure() 251 plt.polar(theta, g_db) 252 fig_v.axes[0].set_rmin(g_db_min) 253 fig_v.axes[0].set_rmax(g_db_max+3) 254 fig_v.axes[0].set_theta_zero_location("N") 255 fig_v.axes[0].set_theta_direction(-1) 256 plt.title(r"Vertical cut of the radiation pattern $G(\theta,0)$ ") 257 258 # Horizontal cut 259 phi = np.linspace(-PI, PI, 1000) 260 c_theta, c_phi = pattern(PI/2*tf.ones_like(phi) , 261 tf.constant(phi, tf.float32)) 262 c_theta = c_theta.numpy() 263 c_phi = c_phi.numpy() 264 g = np.abs(c_theta)**2 + np.abs(c_phi)**2 265 g = np.where(g==0, 1e-12, g) 266 g_db = 10*np.log10(g) 267 g_db_max = np.max(g_db) 268 g_db_min = np.min(g_db) 269 if g_db_min==g_db_max: 270 g_db_min = -30 271 else: 272 g_db_min = np.maximum(-60., g_db_min) 273 274 fig_h = plt.figure() 275 plt.polar(phi, g_db) 276 fig_h.axes[0].set_rmin(g_db_min) 277 fig_h.axes[0].set_rmax(g_db_max+3) 278 fig_h.axes[0].set_theta_zero_location("E") 279 plt.title(r"Horizontal cut of the radiation pattern $G(\pi/2,\varphi)$") 280 281 # 3D visualization 282 theta = np.linspace(0.0, PI, 50) 283 phi = np.linspace(-PI, PI, 50) 284 theta_grid, phi_grid = np.meshgrid(theta, phi, indexing='ij') 285 c_theta, c_phi = pattern(theta_grid, phi_grid) 286 g = np.abs(c_theta)**2 + np.abs(c_phi)**2 287 x = g * np.sin(theta_grid) * np.cos(phi_grid) 288 y = g * np.sin(theta_grid) * np.sin(phi_grid) 289 z = g * np.cos(theta_grid) 290 291 g = np.maximum(g, 1e-5) 292 g_db = 10*np.log10(g) 293 294 def norm(x, x_max, x_min): 295 """Maps input to [0,1] range""" 296 x = 10**(x/10) 297 x_max = 10**(x_max/10) 298 x_min = 10**(x_min/10) 299 if x_min==x_max: 300 x = np.ones_like(x) 301 else: 302 x -= x_min 303 x /= np.abs(x_max-x_min) 304 return x 305 306 g_db_min = np.min(g_db) 307 g_db_max = np.max(g_db) 308 309 fig_3d = plt.figure() 310 ax = fig_3d.add_subplot(1,1,1, projection='3d') 311 ax.plot_surface(x, y, z, rstride=1, cstride=1, linewidth=0, 312 antialiased=False, alpha=0.7, 313 facecolors=cm.turbo(norm(g_db, g_db_max, g_db_min))) 314 315 sm = cm.ScalarMappable(cmap=plt.cm.turbo) 316 sm.set_array([]) 317 cbar = plt.colorbar(sm, ax=ax, orientation="vertical", location="right", 318 shrink=0.7, pad=0.15) 319 xticks = cbar.ax.get_yticks() 320 xticklabels = cbar.ax.get_yticklabels() 321 xticklabels = g_db_min + xticks*(g_db_max-g_db_min) 322 xticklabels = [f"{z:.2f} dB" for z in xticklabels] 323 cbar.ax.set_yticks(xticks) 324 cbar.ax.set_yticklabels(xticklabels) 325 326 ax.view_init(elev=30., azim=-45) 327 plt.xlabel("x") 328 plt.ylabel("y") 329 ax.set_zlabel("z") 330 plt.suptitle( 331 r"3D visualization of the radiation pattern $G(\theta,\varphi)$") 332 333 return fig_v, fig_h, fig_3d 334 335 def polarization_model_1(c_theta, theta, phi, slant_angle): 336 # pylint: disable=line-too-long 337 r"""Model-1 for polarized antennas from 3GPP TR 38.901 338 339 Transforms a vertically polarized antenna pattern :math:`\tilde{C}_\theta(\theta, \varphi)` 340 into a linearly polarized pattern whose direction 341 is specified by a slant angle :math:`\zeta`. For example, 342 :math:`\zeta=0` and :math:`\zeta=\pi/2` correspond 343 to vertical and horizontal polarization, respectively, 344 and :math:`\zeta=\pm \pi/4` to a pair of cross polarized 345 antenna elements. 346 347 The transformed antenna pattern is given by (7.3-3) [TR38901]_: 348 349 350 .. math:: 351 \begin{align} 352 \begin{bmatrix} 353 C_\theta(\theta, \varphi) \\ 354 C_\varphi(\theta, \varphi) 355 \end{bmatrix} &= \begin{bmatrix} 356 \cos(\psi) \\ 357 \sin(\psi) 358 \end{bmatrix} \tilde{C}_\theta(\theta, \varphi)\\ 359 \cos(\psi) &= \frac{\cos(\zeta)\sin(\theta)+\sin(\zeta)\sin(\varphi)\cos(\theta)}{\sqrt{1-\left(\cos(\zeta)\cos(\theta)-\sin(\zeta)\sin(\varphi)\sin(\theta)\right)^2}} \\ 360 \sin(\psi) &= \frac{\sin(\zeta)\cos(\varphi)}{\sqrt{1-\left(\cos(\zeta)\cos(\theta)-\sin(\zeta)\sin(\varphi)\sin(\theta)\right)^2}} 361 \end{align} 362 363 364 Input 365 ----- 366 c_tilde_theta: array_like, complex 367 Zenith pattern 368 369 theta: array_like, float 370 Zenith angles wrapped within [0,pi] [rad] 371 372 phi: array_like, float 373 Azimuth angles wrapped within [-pi, pi) [rad] 374 375 slant_angle: float 376 Slant angle of the linear polarization [rad]. 377 A slant angle of zero means vertical polarization. 378 379 Output 380 ------ 381 c_theta: array_like, complex 382 Zenith pattern 383 384 c_phi: array_like, complex 385 Azimuth pattern 386 """ 387 if slant_angle==0: 388 return c_theta, tf.zeros_like(c_theta) 389 if slant_angle==PI/2: 390 return tf.zeros_like(c_theta), c_theta 391 sin_slant = tf.cast(tf.sin(slant_angle), theta.dtype) 392 cos_slant = tf.cast(tf.cos(slant_angle), theta.dtype) 393 sin_theta = tf.sin(theta) 394 cos_theta = tf.cos(theta) 395 sin_phi = tf.sin(phi) 396 cos_phi = tf.cos(phi) 397 sin_psi = sin_slant*cos_phi 398 cos_psi = cos_slant*sin_theta + sin_slant*sin_phi*cos_theta 399 norm = tf.sqrt(1-(cos_slant*cos_theta - sin_slant*sin_phi*sin_theta)**2) 400 sin_psi = tf.math.divide_no_nan(sin_psi, norm) 401 cos_psi = tf.math.divide_no_nan(cos_psi, norm) 402 c_theta = c_theta*tf.complex(cos_psi, tf.zeros_like(cos_psi)) 403 c_phi = c_theta*tf.complex(sin_psi, tf.zeros_like(sin_psi)) 404 return c_theta, c_phi 405 406 def polarization_model_2(c, slant_angle): 407 # pylint: disable=line-too-long 408 r"""Model-2 for polarized antennas from 3GPP TR 38.901 409 410 Transforms a vertically polarized antenna pattern :math:`\tilde{C}_\theta(\theta, \varphi)` 411 into a linearly polarized pattern whose direction 412 is specified by a slant angle :math:`\zeta`. For example, 413 :math:`\zeta=0` and :math:`\zeta=\pi/2` correspond 414 to vertical and horizontal polarization, respectively, 415 and :math:`\zeta=\pm \pi/4` to a pair of cross polarized 416 antenna elements. 417 418 The transformed antenna pattern is given by (7.3-4/5) [TR38901]_: 419 420 .. math:: 421 \begin{align} 422 \begin{bmatrix} 423 C_\theta(\theta, \varphi) \\ 424 C_\varphi(\theta, \varphi) 425 \end{bmatrix} &= \begin{bmatrix} 426 \cos(\zeta) \\ 427 \sin(\zeta) 428 \end{bmatrix} \tilde{C}_\theta(\theta, \varphi) 429 \end{align} 430 431 Input 432 ----- 433 c_tilde_theta: array_like, complex 434 Zenith pattern 435 436 slant_angle: float 437 Slant angle of the linear polarization [rad]. 438 A slant angle of zero means vertical polarization. 439 440 Output 441 ------ 442 c_theta: array_like, complex 443 Zenith pattern 444 445 c_phi: array_like, complex 446 Azimuth pattern 447 """ 448 cos_slant_angle = tf.cos(slant_angle) 449 c_theta = c*tf.complex(cos_slant_angle, tf.zeros_like(cos_slant_angle)) 450 sin_slant_angle = tf.sin(slant_angle) 451 c_phi = c*tf.complex(sin_slant_angle, tf.zeros_like(sin_slant_angle)) 452 return c_theta, c_phi 453 454 def iso_pattern(theta, phi, slant_angle=0.0, 455 polarization_model=2, dtype=tf.complex64): 456 r""" 457 Isotropic antenna pattern with linear polarizarion 458 459 Input 460 ----- 461 theta: array_like, float 462 Zenith angles wrapped within [0,pi] [rad] 463 464 phi: array_like, float 465 Azimuth angles wrapped within [-pi, pi) [rad] 466 467 slant_angle: float 468 Slant angle of the linear polarization [rad]. 469 A slant angle of zero means vertical polarization. 470 471 polarization_model: int, one of [1,2] 472 Polarization model to be used. Options `1` and `2` 473 refer to :func:`~sionna.rt.antenna.polarization_model_1` 474 and :func:`~sionna.rt.antenna.polarization_model_2`, 475 respectively. 476 Defaults to `2`. 477 478 dtype : tf.complex64 or tf.complex128 479 Datatype. 480 Defaults to `tf.complex64`. 481 482 Output 483 ------ 484 c_theta: array_like, complex 485 Zenith pattern 486 487 c_phi: array_like, complex 488 Azimuth pattern 489 490 491 .. figure:: ../figures/iso_pattern.png 492 :align: center 493 """ 494 rdtype = dtype.real_dtype 495 theta = tf.cast(theta, rdtype) 496 phi = tf.cast(phi, rdtype) 497 slant_angle = tf.cast(slant_angle, rdtype) 498 if not theta.shape==phi.shape: 499 raise ValueError("theta and phi must have the same shape.") 500 if polarization_model not in [1,2]: 501 raise ValueError("polarization_model must be 1 or 2") 502 c = tf.ones_like(theta, dtype=dtype) 503 if polarization_model==1: 504 return polarization_model_1(c, theta, phi, slant_angle) 505 else: 506 return polarization_model_2(c, slant_angle) 507 508 def dipole_pattern(theta, phi, slant_angle=0.0, 509 polarization_model=2, dtype=tf.complex64): 510 r""" 511 Short dipole pattern with linear polarizarion (Eq. 4-26a) [Balanis97]_ 512 513 Input 514 ----- 515 theta: array_like, float 516 Zenith angles wrapped within [0,pi] [rad] 517 518 phi: array_like, float 519 Azimuth angles wrapped within [-pi, pi) [rad] 520 521 slant_angle: float 522 Slant angle of the linear polarization [rad]. 523 A slant angle of zero means vertical polarization. 524 525 polarization_model: int, one of [1,2] 526 Polarization model to be used. Options `1` and `2` 527 refer to :func:`~sionna.rt.antenna.polarization_model_1` 528 and :func:`~sionna.rt.antenna.polarization_model_2`, 529 respectively. 530 Defaults to `2`. 531 532 dtype : tf.complex64 or tf.complex128 533 Datatype. 534 Defaults to `tf.complex64`. 535 536 Output 537 ------ 538 c_theta: array_like, complex 539 Zenith pattern 540 541 c_phi: array_like, complex 542 Azimuth pattern 543 544 545 .. figure:: ../figures/dipole_pattern.png 546 :align: center 547 """ 548 rdtype = dtype.real_dtype 549 k = tf.cast(tf.sqrt(1.5), dtype) 550 theta = tf.cast(theta, rdtype) 551 phi = tf.cast(phi, rdtype) 552 slant_angle = tf.cast(slant_angle, rdtype) 553 if not theta.shape==phi.shape: 554 raise ValueError("theta and phi must have the same shape.") 555 if polarization_model not in [1,2]: 556 raise ValueError("polarization_model must be 1 or 2") 557 c = k*tf.complex(tf.sin(theta), tf.zeros_like(theta)) 558 if polarization_model==1: 559 return polarization_model_1(c, theta, phi, slant_angle) 560 else: 561 return polarization_model_2(c, slant_angle) 562 563 def hw_dipole_pattern(theta, phi, slant_angle=0.0, 564 polarization_model=2, dtype=tf.complex64): 565 # pylint: disable=line-too-long 566 r""" 567 Half-wavelength dipole pattern with linear polarizarion (Eq. 4-84) [Balanis97]_ 568 569 Input 570 ----- 571 theta: array_like, float 572 Zenith angles wrapped within [0,pi] [rad] 573 574 phi: array_like, float 575 Azimuth angles wrapped within [-pi, pi) [rad] 576 577 slant_angle: float 578 Slant angle of the linear polarization [rad]. 579 A slant angle of zero means vertical polarization. 580 581 polarization_model: int, one of [1,2] 582 Polarization model to be used. Options `1` and `2` 583 refer to :func:`~sionna.rt.antenna.polarization_model_1` 584 and :func:`~sionna.rt.antenna.polarization_model_2`, 585 respectively. 586 Defaults to `2`. 587 588 dtype : tf.complex64 or tf.complex128 589 Datatype. 590 Defaults to `tf.complex64`. 591 592 Output 593 ------ 594 c_theta: array_like, complex 595 Zenith pattern 596 597 c_phi: array_like, complex 598 Azimuth pattern 599 600 601 .. figure:: ../figures/hw_dipole_pattern.png 602 :align: center 603 """ 604 rdtype = dtype.real_dtype 605 k = tf.cast(np.sqrt(1.643), rdtype) 606 theta = tf.cast(theta, rdtype) 607 phi = tf.cast(phi, rdtype) 608 slant_angle = tf.cast(slant_angle, rdtype) 609 if not theta.shape== phi.shape: 610 raise ValueError("theta and phi must have the same shape.") 611 if polarization_model not in [1,2]: 612 raise ValueError("polarization_model must be 1 or 2") 613 c = k*tf.math.divide_no_nan(tf.cos(PI/2*tf.cos(theta)), tf.sin(theta)) 614 c = tf.complex(c, tf.zeros_like(c)) 615 if polarization_model==1: 616 return polarization_model_1(c, theta, phi, slant_angle) 617 else: 618 return polarization_model_2(c, slant_angle) 619 620 def tr38901_pattern(theta, phi, slant_angle=0.0, 621 polarization_model=2, dtype=tf.complex64): 622 r""" 623 Antenna pattern from 3GPP TR 38.901 (Table 7.3-1) [TR38901]_ 624 625 Input 626 ----- 627 theta: array_like, float 628 Zenith angles wrapped within [0,pi] [rad] 629 630 phi: array_like, float 631 Azimuth angles wrapped within [-pi, pi) [rad] 632 633 slant_angle: float 634 Slant angle of the linear polarization [rad]. 635 A slant angle of zero means vertical polarization. 636 637 polarization_model: int, one of [1,2] 638 Polarization model to be used. Options `1` and `2` 639 refer to :func:`~sionna.rt.antenna.polarization_model_1` 640 and :func:`~sionna.rt.antenna.polarization_model_2`, 641 respectively. 642 Defaults to `2`. 643 644 dtype : tf.complex64 or tf.complex128 645 Datatype. 646 Defaults to `tf.complex64`. 647 648 Output 649 ------ 650 c_theta: array_like, complex 651 Zenith pattern 652 653 c_phi: array_like, complex 654 Azimuth pattern 655 656 657 .. figure:: ../figures/tr38901_pattern.png 658 :align: center 659 """ 660 rdtype = dtype.real_dtype 661 theta = tf.cast(theta, rdtype) 662 phi = tf.cast(phi, rdtype) 663 slant_angle = tf.cast(slant_angle, rdtype) 664 665 # Wrap phi to [-PI,PI] 666 phi = tf.math.floormod(phi+PI, 2*PI)-PI 667 668 if not theta.shape==phi.shape: 669 raise ValueError("theta and phi must have the same shape.") 670 if polarization_model not in [1,2]: 671 raise ValueError("polarization_model must be 1 or 2") 672 theta_3db = phi_3db = tf.cast(65/180*PI, rdtype) 673 a_max = sla_v = 30 674 g_e_max = 8 675 a_v = -tf.minimum(12*((theta-PI/2)/theta_3db)**2, sla_v) 676 a_h = -tf.minimum(12*(phi/phi_3db)**2, a_max) 677 a_db = -tf.minimum(-(a_v + a_h), a_max) + g_e_max 678 a = 10**(a_db/10) 679 c = tf.complex(tf.sqrt(a), tf.zeros_like(a)) 680 if polarization_model==1: 681 return polarization_model_1(c, theta, phi, slant_angle) 682 else: 683 return polarization_model_2(c, slant_angle)