Source code for sionna.rt.antenna

#
# SPDX-FileCopyrightText: Copyright (c) 2021-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
"""
Implements classes and methods related to antennas.
"""

import numpy as np
import matplotlib.pyplot as plt
from matplotlib import cm
from sionna.constants import PI
import tensorflow as tf
from collections.abc import Sequence

[docs]class Antenna: r""" Class implementing an antenna Creates an antenna object with an either predefined or custom antenna pattern. Can be single or dual polarized. Parameters ---------- pattern : str, callable, or length-2 sequence of callables Antenna pattern. Either one of ["iso", "dipole", "hw_dipole", "tr38901"], or a callable, or a length-2 sequence of callables defining antenna patterns. In the latter case, the antenna is dual polarized and each callable defines the antenna pattern in one of the two orthogonal polarization directions. An antenna pattern is a callable that takes as inputs vectors of zenith and azimuth angles of the same length and returns for each pair the corresponding zenith and azimuth patterns. polarization : str or None Type of polarization. For single polarization, must be "V" (vertical) or "H" (horizontal). For dual polarization, must be "VH" or "cross". Only needed if ``pattern`` is a string. polarization_model: int, one of [1,2] Polarization model to be used. Options `1` and `2` refer to :func:`~sionna.rt.antenna.polarization_model_1` and :func:`~sionna.rt.antenna.polarization_model_2`, respectively. Defaults to `2`. dtype : tf.complex64 or tf.complex128 Datatype used for all computations. Defaults to `tf.complex64`. Example ------- >>> Antenna("tr38901", "VH") """ def __init__(self, pattern, polarization=None, polarization_model=2, dtype=tf.complex64 ): if dtype not in (tf.complex64, tf.complex128): raise ValueError("`dtype` must be tf.complex64 or tf.complex128`") self._dtype = dtype = dtype if polarization_model not in [1,2]: raise ValueError("`polarization_model` must be 1 or 2") self._polarization_model = polarization_model # Pattern is provided as string if isinstance(pattern, str): # Set correct pattern if pattern=="iso": pattern = iso_pattern elif pattern=="dipole": pattern = dipole_pattern elif pattern=="hw_dipole": pattern = hw_dipole_pattern elif pattern=="tr38901": pattern = tr38901_pattern else: raise ValueError("Unknown antenna pattern") # Set slant angles if polarization=="V": slant_angles = [0.0] elif polarization=="H": slant_angles = [PI/2] elif polarization=="VH": slant_angles = [0.0, PI/2] elif polarization=="cross": slant_angles = [-PI/4, PI/4] else: raise ValueError("Unknown polarization") # Create antenna patterns with slant angles self._patterns = [] for sa in slant_angles: f = self.pattern_with_slant_angle(pattern, sa) self._patterns.append(f) # Pattern is a callable elif callable(pattern): self._patterns = [pattern] # Pattern is sequence of callables elif isinstance(pattern, Sequence): if len(pattern) > 2: msg = "An antennta cannot have more than two patterns." raise ValueError(msg) for p in pattern: if not callable(p): msg = "Each element of antenna_pattern must be callable" raise ValueError(msg) self._patterns = pattern # Unsupported pattern else: raise ValueError("Unsupported pattern") @property def patterns(self): """ `list`, `callable` : Antenna patterns for one or two polarization directions """ return self._patterns def pattern_with_slant_angle(self, pattern, slant_angle): """Applies slant angle to antenna pattern""" return lambda theta, phi: pattern(theta, phi, slant_angle, self._polarization_model, self._dtype)
[docs]def compute_gain(pattern, dtype=tf.complex64): # pylint: disable=line-too-long r"""compute_gain(pattern) Computes the directivity, gain, and radiation efficiency of an antenna pattern Given a function :math:`f:(\theta,\varphi)\mapsto (C_\theta(\theta, \varphi), C_\varphi(\theta, \varphi))` describing an antenna pattern :eq:`C`, this function computes the gain :math:`G`, directivity :math:`D`, and radiation efficiency :math:`\eta_\text{rad}=G/D` (see :eq:`G` and text below). Input ----- pattern : callable A callable that takes as inputs vectors of zenith and azimuth angles of the same length and returns for each pair the corresponding zenith and azimuth patterns. Output ------ D : float Directivity :math:`D` G : float Gain :math:`G` eta_rad : float Radiation efficiency :math:`\eta_\text{rad}` Examples -------- >>> compute_gain(tr38901_pattern) (<tf.Tensor: shape=(), dtype=float32, numpy=9.606758>, <tf.Tensor: shape=(), dtype=float32, numpy=6.3095527>, <tf.Tensor: shape=(), dtype=float32, numpy=0.65678275>) """ if dtype not in (tf.complex64, tf.complex128): raise ValueError("`dtype` must be tf.complex64 or tf.complex128`") # Create angular meshgrid theta = tf.linspace(0.0, PI, 1810) theta = tf.cast(theta, dtype.real_dtype) phi = tf.linspace(-PI, PI, 3610) phi = tf.cast(phi, dtype.real_dtype) theta_grid, phi_grid = tf.meshgrid(theta, phi, indexing="ij") # Compute the gain c_theta, c_phi = pattern(theta_grid, phi_grid) g = tf.abs(c_theta)**2 + tf.abs(c_phi)**2 # Find maximum directional gain g_max = tf.reduce_max(g) # Compute radiation efficiency dtheta = theta[1]-theta[0] dphi = phi[1]-phi[0] eta_rad = tf.reduce_sum(g*tf.sin(theta_grid)*dtheta*dphi)/(4*PI) # Compute directivity d = g_max / eta_rad return d, g_max, eta_rad
[docs]def visualize(pattern): r"""visualize(pattern) Visualizes an antenna pattern This function visualizes an antenna pattern with the help of three figures showing the vertical and horizontal cuts as well as a three-dimensional visualization of the antenna gain. Input ----- pattern : callable A callable that takes as inputs vectors of zenith and azimuth angles of the same length and returns for each pair the corresponding zenith and azimuth patterns. Output ------ : :class:`matplotlib.pyplot.Figure` Vertical cut of the antenna gain : :class:`matplotlib.pyplot.Figure` Horizontal cut of the antenna gain : :class:`matplotlib.pyplot.Figure` 3D visualization of the antenna gain Examples -------- >>> fig_v, fig_h, fig_3d = visualize(hw_dipole_pattern) .. figure:: ../figures/pattern_vertical.png :align: center :scale: 80% .. figure:: ../figures/pattern_horizontal.png :align: center :scale: 80% .. figure:: ../figures/pattern_3d.png :align: center :scale: 80% """ # Vertical cut theta = np.linspace(0.0, PI, 1000) c_theta, c_phi = pattern(theta, np.zeros_like(theta)) g = np.abs(c_theta)**2 + np.abs(c_phi)**2 g = np.where(g==0, 1e-12, g) g_db = 10*np.log10(g) g_db_max = np.max(g_db) g_db_min = np.min(g_db) if g_db_min==g_db_max: g_db_min = -30 else: g_db_min = np.maximum(-60., g_db_min) fig_v = plt.figure() plt.polar(theta, g_db) fig_v.axes[0].set_rmin(g_db_min) fig_v.axes[0].set_rmax(g_db_max+3) fig_v.axes[0].set_theta_zero_location("N") fig_v.axes[0].set_theta_direction(-1) plt.title(r"Vertical cut of the radiation pattern $G(\theta,0)$ ") # Horizontal cut phi = np.linspace(-PI, PI, 1000) c_theta, c_phi = pattern(PI/2*tf.ones_like(phi) , tf.constant(phi, tf.float32)) c_theta = c_theta.numpy() c_phi = c_phi.numpy() g = np.abs(c_theta)**2 + np.abs(c_phi)**2 g = np.where(g==0, 1e-12, g) g_db = 10*np.log10(g) g_db_max = np.max(g_db) g_db_min = np.min(g_db) if g_db_min==g_db_max: g_db_min = -30 else: g_db_min = np.maximum(-60., g_db_min) fig_h = plt.figure() plt.polar(phi, g_db) fig_h.axes[0].set_rmin(g_db_min) fig_h.axes[0].set_rmax(g_db_max+3) fig_h.axes[0].set_theta_zero_location("E") plt.title(r"Horizontal cut of the radiation pattern $G(\pi/2,\varphi)$") # 3D visualization theta = np.linspace(0.0, PI, 50) phi = np.linspace(-PI, PI, 50) theta_grid, phi_grid = np.meshgrid(theta, phi, indexing='ij') c_theta, c_phi = pattern(theta_grid, phi_grid) g = np.abs(c_theta)**2 + np.abs(c_phi)**2 x = g * np.sin(theta_grid) * np.cos(phi_grid) y = g * np.sin(theta_grid) * np.sin(phi_grid) z = g * np.cos(theta_grid) g = np.maximum(g, 1e-5) g_db = 10*np.log10(g) def norm(x, x_max, x_min): """Maps input to [0,1] range""" x = 10**(x/10) x_max = 10**(x_max/10) x_min = 10**(x_min/10) if x_min==x_max: x = np.ones_like(x) else: x -= x_min x /= np.abs(x_max-x_min) return x g_db_min = np.min(g_db) g_db_max = np.max(g_db) fig_3d = plt.figure() ax = fig_3d.add_subplot(1,1,1, projection='3d') ax.plot_surface(x, y, z, rstride=1, cstride=1, linewidth=0, antialiased=False, alpha=0.7, facecolors=cm.turbo(norm(g_db, g_db_max, g_db_min))) sm = cm.ScalarMappable(cmap=plt.cm.turbo) sm.set_array([]) cbar = plt.colorbar(sm, ax=ax, orientation="vertical", location="right", shrink=0.7, pad=0.15) xticks = cbar.ax.get_yticks() xticklabels = cbar.ax.get_yticklabels() xticklabels = g_db_min + xticks*(g_db_max-g_db_min) xticklabels = [f"{z:.2f} dB" for z in xticklabels] cbar.ax.set_yticks(xticks) cbar.ax.set_yticklabels(xticklabels) ax.view_init(elev=30., azim=-45) plt.xlabel("x") plt.ylabel("y") ax.set_zlabel("z") plt.suptitle( r"3D visualization of the radiation pattern $G(\theta,\varphi)$") return fig_v, fig_h, fig_3d
[docs]def polarization_model_1(c_theta, theta, phi, slant_angle): # pylint: disable=line-too-long r"""Model-1 for polarized antennas from 3GPP TR 38.901 Transforms a vertically polarized antenna pattern :math:`\tilde{C}_\theta(\theta, \varphi)` into a linearly polarized pattern whose direction is specified by a slant angle :math:`\zeta`. For example, :math:`\zeta=0` and :math:`\zeta=\pi/2` correspond to vertical and horizontal polarization, respectively, and :math:`\zeta=\pm \pi/4` to a pair of cross polarized antenna elements. The transformed antenna pattern is given by (7.3-3) [TR38901]_: .. math:: \begin{align} \begin{bmatrix} C_\theta(\theta, \varphi) \\ C_\varphi(\theta, \varphi) \end{bmatrix} &= \begin{bmatrix} \cos(\psi) \\ \sin(\psi) \end{bmatrix} \tilde{C}_\theta(\theta, \varphi)\\ \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}} \\ \sin(\psi) &= \frac{\sin(\zeta)\cos(\varphi)}{\sqrt{1-\left(\cos(\zeta)\cos(\theta)-\sin(\zeta)\sin(\varphi)\sin(\theta)\right)^2}} \end{align} Input ----- c_tilde_theta: array_like, complex Zenith pattern theta: array_like, float Zenith angles wrapped within [0,pi] [rad] phi: array_like, float Azimuth angles wrapped within [-pi, pi) [rad] slant_angle: float Slant angle of the linear polarization [rad]. A slant angle of zero means vertical polarization. Output ------ c_theta: array_like, complex Zenith pattern c_phi: array_like, complex Azimuth pattern """ if slant_angle==0: return c_theta, tf.zeros_like(c_theta) if slant_angle==PI/2: return tf.zeros_like(c_theta), c_theta sin_slant = tf.cast(tf.sin(slant_angle), theta.dtype) cos_slant = tf.cast(tf.cos(slant_angle), theta.dtype) sin_theta = tf.sin(theta) cos_theta = tf.cos(theta) sin_phi = tf.sin(phi) cos_phi = tf.cos(phi) sin_psi = sin_slant*cos_phi cos_psi = cos_slant*sin_theta + sin_slant*sin_phi*cos_theta norm = tf.sqrt(1-(cos_slant*cos_theta - sin_slant*sin_phi*sin_theta)**2) sin_psi = tf.math.divide_no_nan(sin_psi, norm) cos_psi = tf.math.divide_no_nan(cos_psi, norm) c_theta = c_theta*tf.complex(cos_psi, tf.zeros_like(cos_psi)) c_phi = c_theta*tf.complex(sin_psi, tf.zeros_like(sin_psi)) return c_theta, c_phi
[docs]def polarization_model_2(c, slant_angle): # pylint: disable=line-too-long r"""Model-2 for polarized antennas from 3GPP TR 38.901 Transforms a vertically polarized antenna pattern :math:`\tilde{C}_\theta(\theta, \varphi)` into a linearly polarized pattern whose direction is specified by a slant angle :math:`\zeta`. For example, :math:`\zeta=0` and :math:`\zeta=\pi/2` correspond to vertical and horizontal polarization, respectively, and :math:`\zeta=\pm \pi/4` to a pair of cross polarized antenna elements. The transformed antenna pattern is given by (7.3-4/5) [TR38901]_: .. math:: \begin{align} \begin{bmatrix} C_\theta(\theta, \varphi) \\ C_\varphi(\theta, \varphi) \end{bmatrix} &= \begin{bmatrix} \cos(\zeta) \\ \sin(\zeta) \end{bmatrix} \tilde{C}_\theta(\theta, \varphi) \end{align} Input ----- c_tilde_theta: array_like, complex Zenith pattern slant_angle: float Slant angle of the linear polarization [rad]. A slant angle of zero means vertical polarization. Output ------ c_theta: array_like, complex Zenith pattern c_phi: array_like, complex Azimuth pattern """ cos_slant_angle = tf.cos(slant_angle) c_theta = c*tf.complex(cos_slant_angle, tf.zeros_like(cos_slant_angle)) sin_slant_angle = tf.sin(slant_angle) c_phi = c*tf.complex(sin_slant_angle, tf.zeros_like(sin_slant_angle)) return c_theta, c_phi
[docs]def iso_pattern(theta, phi, slant_angle=0.0, polarization_model=2, dtype=tf.complex64): r""" Isotropic antenna pattern with linear polarizarion Input ----- theta: array_like, float Zenith angles wrapped within [0,pi] [rad] phi: array_like, float Azimuth angles wrapped within [-pi, pi) [rad] slant_angle: float Slant angle of the linear polarization [rad]. A slant angle of zero means vertical polarization. polarization_model: int, one of [1,2] Polarization model to be used. Options `1` and `2` refer to :func:`~sionna.rt.antenna.polarization_model_1` and :func:`~sionna.rt.antenna.polarization_model_2`, respectively. Defaults to `2`. dtype : tf.complex64 or tf.complex128 Datatype. Defaults to `tf.complex64`. Output ------ c_theta: array_like, complex Zenith pattern c_phi: array_like, complex Azimuth pattern .. figure:: ../figures/iso_pattern.png :align: center """ rdtype = dtype.real_dtype theta = tf.cast(theta, rdtype) phi = tf.cast(phi, rdtype) slant_angle = tf.cast(slant_angle, rdtype) if not theta.shape==phi.shape: raise ValueError("theta and phi must have the same shape.") if polarization_model not in [1,2]: raise ValueError("polarization_model must be 1 or 2") c = tf.ones_like(theta, dtype=dtype) if polarization_model==1: return polarization_model_1(c, theta, phi, slant_angle) else: return polarization_model_2(c, slant_angle)
[docs]def dipole_pattern(theta, phi, slant_angle=0.0, polarization_model=2, dtype=tf.complex64): r""" Short dipole pattern with linear polarizarion (Eq. 4-26a) [Balanis97]_ Input ----- theta: array_like, float Zenith angles wrapped within [0,pi] [rad] phi: array_like, float Azimuth angles wrapped within [-pi, pi) [rad] slant_angle: float Slant angle of the linear polarization [rad]. A slant angle of zero means vertical polarization. polarization_model: int, one of [1,2] Polarization model to be used. Options `1` and `2` refer to :func:`~sionna.rt.antenna.polarization_model_1` and :func:`~sionna.rt.antenna.polarization_model_2`, respectively. Defaults to `2`. dtype : tf.complex64 or tf.complex128 Datatype. Defaults to `tf.complex64`. Output ------ c_theta: array_like, complex Zenith pattern c_phi: array_like, complex Azimuth pattern .. figure:: ../figures/dipole_pattern.png :align: center """ rdtype = dtype.real_dtype k = tf.cast(tf.sqrt(1.5), dtype) theta = tf.cast(theta, rdtype) phi = tf.cast(phi, rdtype) slant_angle = tf.cast(slant_angle, rdtype) if not theta.shape==phi.shape: raise ValueError("theta and phi must have the same shape.") if polarization_model not in [1,2]: raise ValueError("polarization_model must be 1 or 2") c = k*tf.complex(tf.sin(theta), tf.zeros_like(theta)) if polarization_model==1: return polarization_model_1(c, theta, phi, slant_angle) else: return polarization_model_2(c, slant_angle)
[docs]def hw_dipole_pattern(theta, phi, slant_angle=0.0, polarization_model=2, dtype=tf.complex64): # pylint: disable=line-too-long r""" Half-wavelength dipole pattern with linear polarizarion (Eq. 4-84) [Balanis97]_ Input ----- theta: array_like, float Zenith angles wrapped within [0,pi] [rad] phi: array_like, float Azimuth angles wrapped within [-pi, pi) [rad] slant_angle: float Slant angle of the linear polarization [rad]. A slant angle of zero means vertical polarization. polarization_model: int, one of [1,2] Polarization model to be used. Options `1` and `2` refer to :func:`~sionna.rt.antenna.polarization_model_1` and :func:`~sionna.rt.antenna.polarization_model_2`, respectively. Defaults to `2`. dtype : tf.complex64 or tf.complex128 Datatype. Defaults to `tf.complex64`. Output ------ c_theta: array_like, complex Zenith pattern c_phi: array_like, complex Azimuth pattern .. figure:: ../figures/hw_dipole_pattern.png :align: center """ rdtype = dtype.real_dtype k = tf.cast(np.sqrt(1.643), rdtype) theta = tf.cast(theta, rdtype) phi = tf.cast(phi, rdtype) slant_angle = tf.cast(slant_angle, rdtype) if not theta.shape== phi.shape: raise ValueError("theta and phi must have the same shape.") if polarization_model not in [1,2]: raise ValueError("polarization_model must be 1 or 2") c = k*tf.math.divide_no_nan(tf.cos(PI/2*tf.cos(theta)), tf.sin(theta)) c = tf.complex(c, tf.zeros_like(c)) if polarization_model==1: return polarization_model_1(c, theta, phi, slant_angle) else: return polarization_model_2(c, slant_angle)
[docs]def tr38901_pattern(theta, phi, slant_angle=0.0, polarization_model=2, dtype=tf.complex64): r""" Antenna pattern from 3GPP TR 38.901 (Table 7.3-1) [TR38901]_ Input ----- theta: array_like, float Zenith angles wrapped within [0,pi] [rad] phi: array_like, float Azimuth angles wrapped within [-pi, pi) [rad] slant_angle: float Slant angle of the linear polarization [rad]. A slant angle of zero means vertical polarization. polarization_model: int, one of [1,2] Polarization model to be used. Options `1` and `2` refer to :func:`~sionna.rt.antenna.polarization_model_1` and :func:`~sionna.rt.antenna.polarization_model_2`, respectively. Defaults to `2`. dtype : tf.complex64 or tf.complex128 Datatype. Defaults to `tf.complex64`. Output ------ c_theta: array_like, complex Zenith pattern c_phi: array_like, complex Azimuth pattern .. figure:: ../figures/tr38901_pattern.png :align: center """ rdtype = dtype.real_dtype theta = tf.cast(theta, rdtype) phi = tf.cast(phi, rdtype) slant_angle = tf.cast(slant_angle, rdtype) # Wrap phi to [-PI,PI] phi = tf.math.floormod(phi+PI, 2*PI)-PI if not theta.shape==phi.shape: raise ValueError("theta and phi must have the same shape.") if polarization_model not in [1,2]: raise ValueError("polarization_model must be 1 or 2") theta_3db = phi_3db = tf.cast(65/180*PI, rdtype) a_max = sla_v = 30 g_e_max = 8 a_v = -tf.minimum(12*((theta-PI/2)/theta_3db)**2, sla_v) a_h = -tf.minimum(12*(phi/phi_3db)**2, a_max) a_db = -tf.minimum(-(a_v + a_h), a_max) + g_e_max a = 10**(a_db/10) c = tf.complex(tf.sqrt(a), tf.zeros_like(a)) if polarization_model==1: return polarization_model_1(c, theta, phi, slant_angle) else: return polarization_model_2(c, slant_angle)