anomaly-detection-material-parameters-calibration

Sionna param calibration (research proj)
git clone https://git.ea.contact/anomaly-detection-material-parameters-calibration
Log | Files | Refs | README

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)