Source code for sionna.phy.channel.tr38901.antenna

#
# SPDX-FileCopyrightText: Copyright (c) 2021-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
"""3GPP TR 38.901 antenna modeling"""

from typing import Optional, Tuple

import numpy as np
import matplotlib.pyplot as plt
from matplotlib.markers import MarkerStyle
import torch

from sionna.phy import SPEED_OF_LIGHT, PI
from sionna.phy.object import Object


class AntennaElement(Object):
    """Antenna element following the :cite:p:`TR38901` specification

    :param pattern: Radiation pattern. One of ``"omni"`` or ``"38.901"``.
    :param slant_angle: Polarization slant angle [radian]
    :param precision: Precision used for internal calculations and outputs.
        If set to `None`, :attr:`~sionna.phy.config.Config.precision` is used.
    :param device: Device for computation (e.g., ``"cpu"``, ``"cuda:0"``).
        If `None`, :attr:`~sionna.phy.config.Config.device` is used.

    .. rubric:: Examples

    .. code-block:: python

        from sionna.phy.channel.tr38901 import AntennaElement
        import torch

        # Create an antenna element with 38.901 radiation pattern
        ant = AntennaElement(pattern="38.901", slant_angle=0.0)

        # Compute field at zenith angle pi/2 and azimuth angle 0
        theta = torch.tensor([1.5708])
        phi = torch.tensor([0.0])
        f_theta, f_phi = ant.field(theta, phi)
    """

    def __init__(
        self,
        pattern: str,
        slant_angle: float = 0.0,
        precision: Optional[str] = None,
        device: Optional[str] = None,
    ) -> None:
        super().__init__(precision=precision, device=device)
        assert pattern in ["omni", "38.901"], \
            "The radiation_pattern must be one of [\"omni\", \"38.901\"]."

        self._pattern = pattern
        # Register as buffer for CUDAGraph compatibility
        self.register_buffer("_slant_angle", torch.tensor(slant_angle, dtype=self.dtype, device=self.device))

        # Select the radiation field corresponding to the requested pattern
        if pattern == "omni":
            self._radiation_pattern = self._radiation_pattern_omni
        else:
            self._radiation_pattern = self._radiation_pattern_38901

    @property
    def pattern(self) -> str:
        """Radiation pattern type ('omni' or '38.901')"""
        return self._pattern

    @property
    def slant_angle(self) -> torch.Tensor:
        """Polarization slant angle [radian]"""
        return self._slant_angle

    def field(self, theta: torch.Tensor, phi: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]:
        """Field pattern in the vertical and horizontal polarization (7.3-4/5)

        :param theta: Zenith angle wrapped within (0, pi) [radian]
        :param phi: Azimuth angle wrapped within (-pi, pi) [radian]
        """
        theta = theta.to(dtype=self.dtype, device=self.device)
        phi = phi.to(dtype=self.dtype, device=self.device)
        a = torch.sqrt(self._radiation_pattern(theta, phi))
        f_theta = a * torch.cos(self._slant_angle)
        f_phi = a * torch.sin(self._slant_angle)
        return (f_theta, f_phi)

    def show(self) -> None:
        """Shows the field pattern of an antenna element"""
        theta = torch.linspace(0.0, PI, 361, dtype=self.dtype, device=self.device)
        phi = torch.linspace(-PI, PI, 361, dtype=self.dtype, device=self.device)
        a_v = 10 * torch.log10(self._radiation_pattern(theta, torch.zeros_like(theta)))
        a_h = 10 * torch.log10(self._radiation_pattern(PI / 2 * torch.ones_like(phi), phi))

        # Convert to numpy for plotting
        theta_np = theta.cpu().numpy()
        phi_np = phi.cpu().numpy()
        a_v_np = a_v.cpu().numpy()
        a_h_np = a_h.cpu().numpy()

        fig = plt.figure()
        plt.polar(theta_np, a_v_np)
        fig.axes[0].set_theta_zero_location("N")
        fig.axes[0].set_theta_direction(-1)
        plt.title(r"Vertical cut of the radiation pattern ($\phi = 0$)")
        plt.legend([f"{self._pattern}"])

        fig = plt.figure()
        plt.polar(phi_np, a_h_np)
        fig.axes[0].set_theta_zero_location("E")
        plt.title(r"Horizontal cut of the radiation pattern ($\theta = \pi/2$)")
        plt.legend([f"{self._pattern}"])

        theta = torch.linspace(0.0, PI, 50, dtype=self.dtype, device=self.device)
        phi = torch.linspace(-PI, PI, 50, dtype=self.dtype, device=self.device)
        phi_grid, theta_grid = torch.meshgrid(phi, theta, indexing='xy')
        a = self._radiation_pattern(theta_grid, phi_grid)
        x = a * torch.sin(theta_grid) * torch.cos(phi_grid)
        y = a * torch.sin(theta_grid) * torch.sin(phi_grid)
        z = a * torch.cos(theta_grid)

        # Convert to numpy for 3D plotting
        x_np = x.cpu().numpy()
        y_np = y.cpu().numpy()
        z_np = z.cpu().numpy()

        fig = plt.figure()
        ax = fig.add_subplot(1, 1, 1, projection='3d')
        ax.plot_surface(x_np, y_np, z_np, rstride=1, cstride=1,
                        linewidth=0, antialiased=False, alpha=0.5)
        ax.view_init(elev=30., azim=-45)
        plt.xlabel("x")
        plt.ylabel("y")
        ax.set_zlabel("z")
        plt.title(f"Radiation power pattern ({self._pattern})")

    def _radiation_pattern_omni(self, theta: torch.Tensor, phi: torch.Tensor) -> torch.Tensor:
        """Radiation pattern of an omnidirectional 3D radiation pattern

        :param theta: Zenith angle
        :param phi: Azimuth angle
        """
        return torch.ones_like(theta)

    def _radiation_pattern_38901(self, theta: torch.Tensor, phi: torch.Tensor) -> torch.Tensor:
        """Radiation pattern from TR38901 (Table 7.3-1)

        :param theta: Zenith angle wrapped within (0, pi) [radian]
        :param phi: Azimuth angle wrapped within (-pi, pi) [radian]
        """
        theta_3db = phi_3db = 65 / 180 * PI
        a_max = sla_v = 30.0
        g_e_max = 8.0
        a_v = -torch.minimum(12 * ((theta - PI / 2) / theta_3db) ** 2,
                              torch.tensor(sla_v, dtype=self.dtype, device=self.device))
        a_h = -torch.minimum(12 * (phi / phi_3db) ** 2,
                              torch.tensor(a_max, dtype=self.dtype, device=self.device))
        a_db = -torch.minimum(-(a_v + a_h),
                               torch.tensor(a_max, dtype=self.dtype, device=self.device)) + g_e_max
        return 10 ** (a_db / 10)

    def _compute_gain(self) -> Tuple[torch.Tensor, torch.Tensor]:
        """Compute antenna gain and directivity through numerical integration"""
        # Create angular meshgrid
        theta = torch.linspace(0.0, PI, 181, dtype=self.dtype, device=self.device)
        phi = torch.linspace(-PI, PI, 361, dtype=self.dtype, device=self.device)
        phi_grid, theta_grid = torch.meshgrid(phi, theta, indexing='xy')

        # Compute field strength over the grid
        f_theta, f_phi = self.field(theta_grid, phi_grid)
        u = f_theta ** 2 + f_phi ** 2
        gain_db = 10 * torch.log10(torch.max(u))

        # Numerical integration of the field components
        dtheta = theta[1] - theta[0]
        dphi = phi[1] - phi[0]
        po = torch.sum(u * torch.sin(theta_grid) * dtheta * dphi)

        # Compute directivity
        u_bar = po / (4 * PI)  # Equivalent isotropic radiator
        d = u / u_bar  # Directivity grid
        directivity_db = 10 * torch.log10(torch.max(d))
        return (gain_db, directivity_db)


class AntennaPanel(Object):
    """Antenna panel following the :cite:p:`TR38901` specification

    :param num_rows: Number of rows forming the panel
    :param num_cols: Number of columns forming the panel
    :param polarization: Polarization. One of ``"single"`` or ``"dual"``.
    :param vertical_spacing: Vertical antenna element spacing
        [multiples of wavelength]
    :param horizontal_spacing: Horizontal antenna element spacing
        [multiples of wavelength]
    :param precision: Precision used for internal calculations and outputs.
        If set to `None`, :attr:`~sionna.phy.config.Config.precision` is used.
    :param device: Device for computation (e.g., ``"cpu"``, ``"cuda:0"``).
        If `None`, :attr:`~sionna.phy.config.Config.device` is used.
    """

    def __init__(
        self,
        num_rows: int,
        num_cols: int,
        polarization: str,
        vertical_spacing: float,
        horizontal_spacing: float,
        precision: Optional[str] = None,
        device: Optional[str] = None,
    ) -> None:
        super().__init__(precision=precision, device=device)
        assert polarization in ('single', 'dual'), \
            "polarization must be either 'single' or 'dual'"

        self._num_rows = num_rows
        self._num_cols = num_cols
        self._polarization = polarization
        # Register as buffers for CUDAGraph compatibility
        self.register_buffer("_horizontal_spacing", torch.tensor(horizontal_spacing, dtype=self.dtype, device=self.device))
        self.register_buffer("_vertical_spacing", torch.tensor(vertical_spacing, dtype=self.dtype, device=self.device))

        # Place the antenna elements of the first polarization direction
        # on the y-z-plane
        p = 1 if polarization == 'single' else 2
        ant_pos = np.zeros([num_rows * num_cols * p, 3])
        for i in range(num_rows):
            for j in range(num_cols):
                ant_pos[i + j * num_rows] = [0,
                                              j * horizontal_spacing,
                                              -i * vertical_spacing]

        # Center the panel around the origin
        offset = [0,
                  -(num_cols - 1) * horizontal_spacing / 2,
                  (num_rows - 1) * vertical_spacing / 2]
        ant_pos += offset

        # Create the antenna elements of the second polarization direction
        if polarization == 'dual':
            ant_pos[num_rows * num_cols:] = ant_pos[:num_rows * num_cols]
        # Register as buffer for CUDAGraph compatibility
        self.register_buffer("_ant_pos", torch.tensor(ant_pos, dtype=self.dtype, device=self.device))

    @property
    def ant_pos(self) -> torch.Tensor:
        """Antenna positions in the local coordinate system"""
        return self._ant_pos

    @property
    def num_rows(self) -> int:
        """Number of rows"""
        return self._num_rows

    @property
    def num_cols(self) -> int:
        """Number of columns"""
        return self._num_cols

    @property
    def polarization(self) -> str:
        """Polarization ('single' or 'dual')"""
        return self._polarization

    @property
    def vertical_spacing(self) -> torch.Tensor:
        """Vertical spacing between elements [multiple of wavelength]"""
        return self._vertical_spacing

    @property
    def horizontal_spacing(self) -> torch.Tensor:
        """Horizontal spacing between elements [multiple of wavelength]"""
        return self._horizontal_spacing

    def show(self) -> None:
        """Shows the panel geometry"""
        fig = plt.figure()
        pos = self._ant_pos[:self._num_rows * self._num_cols].cpu().numpy()
        plt.plot(pos[:, 1], pos[:, 2], marker="|", markeredgecolor='red',
                 markersize="20", linestyle="None", markeredgewidth="2")
        for i, p in enumerate(pos):
            fig.axes[0].annotate(i + 1, (p[1], p[2]))
        if self._polarization == 'dual':
            pos = self._ant_pos[self._num_rows * self._num_cols:].cpu().numpy()
            plt.plot(pos[:, 1], pos[:, 2], marker="_", markeredgecolor='black',
                     markersize="20", linestyle="None", markeredgewidth="1")
        plt.xlabel(r"y ($\lambda_0$)")
        plt.ylabel(r"z ($\lambda_0$)")
        plt.title("Antenna Panel")
        plt.legend(["Polarization 1", "Polarization 2"], loc="upper right")


[docs] class PanelArray(Object): # pylint: disable=line-too-long r""" Antenna panel array following the :cite:p:`TR38901` specification This class is used to create models of the panel arrays used by the transmitters and receivers and that need to be specified when using the :class:`~sionna.phy.channel.tr38901.CDL`, :class:`~sionna.phy.channel.tr38901.UMi`, :class:`~sionna.phy.channel.tr38901.UMa`, and :class:`~sionna.phy.channel.tr38901.RMa` models. :param num_rows_per_panel: Number of rows of elements per panel :param num_cols_per_panel: Number of columns of elements per panel :param polarization: Polarization. One of ``"single"`` or ``"dual"``. :param polarization_type: Type of polarization. For single polarization, must be ``"V"`` or ``"H"``. For dual polarization, must be ``"VH"`` or ``"cross"``. :param antenna_pattern: Element radiation pattern. One of ``"omni"`` or ``"38.901"``. :param carrier_frequency: Carrier frequency [Hz] :param num_rows: Number of rows of panels. Defaults to 1. :param num_cols: Number of columns of panels. Defaults to 1. :param panel_vertical_spacing: Vertical spacing of panels [multiples of wavelength]. Must be greater than the panel width. If set to `None`, it is set to the panel width + 0.5. :param panel_horizontal_spacing: Horizontal spacing of panels [in multiples of wavelength]. Must be greater than the panel height. If set to `None`, it is set to the panel height + 0.5. :param element_vertical_spacing: Element vertical spacing [multiple of wavelength]. Defaults to 0.5 if set to `None`. :param element_horizontal_spacing: Element horizontal spacing [multiple of wavelength]. Defaults to 0.5 if set to `None`. :param precision: Precision used for internal calculations and outputs. If set to `None`, :attr:`~sionna.phy.config.Config.precision` is used. :param device: Device for computation (e.g., ``"cpu"``, ``"cuda:0"``). If `None`, :attr:`~sionna.phy.config.Config.device` is used. .. rubric:: Examples .. code-block:: python from sionna.phy.channel.tr38901 import PanelArray array = PanelArray(num_rows_per_panel=4, num_cols_per_panel=4, polarization='dual', polarization_type='VH', antenna_pattern='38.901', carrier_frequency=3.5e9, num_cols=2, panel_horizontal_spacing=3.) array.show() """ def __init__( self, num_rows_per_panel: int, num_cols_per_panel: int, polarization: str, polarization_type: str, antenna_pattern: str, carrier_frequency: float, num_rows: int = 1, num_cols: int = 1, panel_vertical_spacing: Optional[float] = None, panel_horizontal_spacing: Optional[float] = None, element_vertical_spacing: Optional[float] = None, element_horizontal_spacing: Optional[float] = None, precision: Optional[str] = None, device: Optional[str] = None, ) -> None: super().__init__(precision=precision, device=device) assert polarization in ('single', 'dual'), \ "polarization must be either 'single' or 'dual'" # Setting default values for antenna and panel spacings if not # specified by the user # Default spacing for antenna elements is half a wavelength if element_vertical_spacing is None: element_vertical_spacing = 0.5 if element_horizontal_spacing is None: element_horizontal_spacing = 0.5 # Default values of panel spacing is the panel size + 0.5 if panel_vertical_spacing is None: panel_vertical_spacing = (num_rows_per_panel - 1) \ * element_vertical_spacing + 0.5 if panel_horizontal_spacing is None: panel_horizontal_spacing = (num_cols_per_panel - 1) \ * element_horizontal_spacing + 0.5 # Check that panel spacing is larger than panel dimensions assert panel_horizontal_spacing > (num_cols_per_panel - 1) \ * element_horizontal_spacing, \ "Panel horizontal spacing must be larger than the panel width" assert panel_vertical_spacing > (num_rows_per_panel - 1) \ * element_vertical_spacing, \ "Panel vertical spacing must be larger than panel height" self._num_rows = num_rows self._num_cols = num_cols self._num_rows_per_panel = num_rows_per_panel self._num_cols_per_panel = num_cols_per_panel self._polarization = polarization self._polarization_type = polarization_type # Register as buffers for CUDAGraph compatibility self.register_buffer("_panel_vertical_spacing", torch.tensor(panel_vertical_spacing, dtype=self.dtype, device=self.device)) self.register_buffer("_panel_horizontal_spacing", torch.tensor(panel_horizontal_spacing, dtype=self.dtype, device=self.device)) self.register_buffer("_element_vertical_spacing", torch.tensor(element_vertical_spacing, dtype=self.dtype, device=self.device)) self.register_buffer("_element_horizontal_spacing", torch.tensor(element_horizontal_spacing, dtype=self.dtype, device=self.device)) self._num_panels = num_cols * num_rows p = 1 if polarization == 'single' else 2 self._num_panel_ant = num_cols_per_panel * num_rows_per_panel * p # Total number of antenna elements self._num_ant = self._num_panels * self._num_panel_ant # Wavelength (m) # Register as buffer for CUDAGraph compatibility self.register_buffer("_lambda_0", torch.tensor(SPEED_OF_LIGHT / carrier_frequency, dtype=self.dtype, device=self.device)) # Create one antenna element for each polarization direction # polarization must be one of {"V", "H", "VH", "cross"} if polarization == 'single': assert polarization_type in ["V", "H"], \ "For single polarization, polarization_type must be 'V' or 'H'" slant_angle = 0 if polarization_type == "V" else PI / 2 self._ant_pol1 = AntennaElement(antenna_pattern, slant_angle, precision=self.precision, device=self.device) self._ant_pol2 = None else: assert polarization_type in ["VH", "cross"], \ "For dual polarization, polarization_type must be 'VH' or 'cross'" slant_angle = 0 if polarization_type == "VH" else -PI / 4 self._ant_pol1 = AntennaElement(antenna_pattern, slant_angle, precision=self.precision, device=self.device) self._ant_pol2 = AntennaElement(antenna_pattern, slant_angle + PI / 2, precision=self.precision, device=self.device) # Compose array from panels ant_pos = np.zeros([self._num_ant, 3]) panel = AntennaPanel(num_rows_per_panel, num_cols_per_panel, polarization, element_vertical_spacing, element_horizontal_spacing, precision=self.precision, device=self.device) pos = panel.ant_pos.cpu().numpy() count = 0 num_panel_ant = self._num_panel_ant for j in range(num_cols): for i in range(num_rows): offset = [0, j * panel_horizontal_spacing, -i * panel_vertical_spacing] new_pos = pos + offset ant_pos[count * num_panel_ant:(count + 1) * num_panel_ant] = new_pos count += 1 # Center the entire panel array around the origin of the y-z plane offset = [0, -(num_cols - 1) * panel_horizontal_spacing / 2, (num_rows - 1) * panel_vertical_spacing / 2] ant_pos += offset # Scale antenna element positions by the wavelength ant_pos *= self._lambda_0.cpu().numpy() # Register as buffer for CUDAGraph compatibility self.register_buffer("_ant_pos", torch.tensor(ant_pos, dtype=self.dtype, device=self.device)) # Compute indices of antennas for polarization directions ind = np.arange(0, self._num_ant) ind = np.reshape(ind, [self._num_panels * p, -1]) # Register as buffers for CUDAGraph compatibility self.register_buffer("_ant_ind_pol1", torch.tensor(np.reshape(ind[::p], [-1]), dtype=torch.int64, device=self.device)) if polarization == 'single': self.register_buffer("_ant_ind_pol2", torch.tensor(np.array([]), dtype=torch.int64, device=self.device)) else: self.register_buffer("_ant_ind_pol2", torch.tensor(np.reshape( ind[1:self._num_panels * p:2], [-1]), dtype=torch.int64, device=self.device)) # Get positions of antenna elements for each polarization direction self.register_buffer("_ant_pos_pol1", self._ant_pos[self._ant_ind_pol1]) self.register_buffer("_ant_pos_pol2", self._ant_pos[self._ant_ind_pol2] if polarization == 'dual' else torch.tensor([], dtype=self.dtype, device=self.device)) @property def num_rows(self) -> int: """Number of rows of panels""" return self._num_rows @property def num_cols(self) -> int: """Number of columns of panels""" return self._num_cols @property def num_rows_per_panel(self) -> int: """Number of rows of elements per panel""" return self._num_rows_per_panel @property def num_cols_per_panel(self) -> int: """Number of columns of elements per panel""" return self._num_cols_per_panel @property def polarization(self) -> str: """Polarization ('single' or 'dual')""" return self._polarization @property def polarization_type(self) -> str: """Polarization type. ``"V"`` or ``"H"`` for single polarization. ``"VH"`` or ``"cross"`` for dual polarization.""" return self._polarization_type @property def panel_vertical_spacing(self) -> torch.Tensor: """Vertical spacing between the panels [multiple of wavelength]""" return self._panel_vertical_spacing @property def panel_horizontal_spacing(self) -> torch.Tensor: """Horizontal spacing between the panels [multiple of wavelength]""" return self._panel_horizontal_spacing @property def element_vertical_spacing(self) -> torch.Tensor: """Vertical spacing between the antenna elements within a panel [multiple of wavelength]""" return self._element_vertical_spacing @property def element_horizontal_spacing(self) -> torch.Tensor: """Horizontal spacing between the antenna elements within a panel [multiple of wavelength]""" return self._element_horizontal_spacing @property def num_panels(self) -> int: """Number of panels""" return self._num_panels @property def num_panels_ant(self) -> int: """Number of antenna elements per panel""" return self._num_panel_ant @property def num_ant(self) -> int: """Total number of antenna elements""" return self._num_ant @property def ant_pol1(self) -> AntennaElement: """Field of an antenna element with the first polarization direction""" return self._ant_pol1 @property def ant_pol2(self) -> AntennaElement: """Field of an antenna element with the second polarization direction. Only defined with dual polarization.""" assert self._polarization == 'dual', \ "This property is not defined with single polarization" return self._ant_pol2 @property def ant_pos(self) -> torch.Tensor: """Positions of the antennas""" return self._ant_pos @property def ant_ind_pol1(self) -> torch.Tensor: """Indices of antenna elements with the first polarization direction""" return self._ant_ind_pol1 @property def ant_ind_pol2(self) -> torch.Tensor: """Indices of antenna elements with the second polarization direction. Only defined with dual polarization.""" assert self._polarization == 'dual', \ "This property is not defined with single polarization" return self._ant_ind_pol2 @property def ant_pos_pol1(self) -> torch.Tensor: """Positions of the antenna elements with the first polarization direction""" return self._ant_pos_pol1 @property def ant_pos_pol2(self) -> torch.Tensor: """Positions of antenna elements with the second polarization direction. Only defined with dual polarization.""" assert self._polarization == 'dual', \ "This property is not defined with single polarization" return self._ant_pos_pol2
[docs] def show(self) -> None: """Show the panel array geometry""" if self._polarization == 'single': if self._polarization_type == 'H': marker_p1 = MarkerStyle("_").get_marker() else: marker_p1 = MarkerStyle("|") else: # 'dual' if self._polarization_type == 'cross': marker_p1 = (2, 0, -45) marker_p2 = (2, 0, 45) else: marker_p1 = MarkerStyle("_").get_marker() marker_p2 = MarkerStyle("|").get_marker() fig = plt.figure() pos_pol1 = self._ant_pos_pol1.cpu().numpy() plt.plot(pos_pol1[:, 1], pos_pol1[:, 2], marker=marker_p1, markeredgecolor='red', markersize="20", linestyle="None", markeredgewidth="2") ant_ind_pol1 = self._ant_ind_pol1.cpu().numpy() for i, p in enumerate(pos_pol1): fig.axes[0].annotate(ant_ind_pol1[i] + 1, (p[1], p[2])) if self._polarization == 'dual': pos_pol2 = self._ant_pos_pol2.cpu().numpy() plt.plot(pos_pol2[:, 1], pos_pol2[:, 2], marker=marker_p2, # pylint: disable=possibly-used-before-assignment markeredgecolor='black', markersize="20", linestyle="None", markeredgewidth="1") plt.xlabel("y (m)") plt.ylabel("z (m)") plt.title("Panel Array") plt.legend(["Polarization 1", "Polarization 2"], loc="upper right")
[docs] def show_element_radiation_pattern(self) -> None: """Show the radiation field of antenna elements forming the panel""" self._ant_pol1.show()
[docs] class Antenna(PanelArray): # pylint: disable=line-too-long r""" Single antenna following the :cite:p:`TR38901` specification This class is a special case of :class:`~sionna.phy.channel.tr38901.PanelArray`, and can be used in lieu of it. :param polarization: Polarization. One of ``"single"`` or ``"dual"``. :param polarization_type: Type of polarization. For single polarization, must be ``"V"`` or ``"H"``. For dual polarization, must be ``"VH"`` or ``"cross"``. :param antenna_pattern: Element radiation pattern. One of ``"omni"`` or ``"38.901"``. :param carrier_frequency: Carrier frequency [Hz] :param precision: Precision used for internal calculations and outputs. If set to `None`, :attr:`~sionna.phy.config.Config.precision` is used. :param device: Device for computation (e.g., ``"cpu"``, ``"cuda:0"``). If `None`, :attr:`~sionna.phy.config.Config.device` is used. .. rubric:: Examples .. code-block:: python from sionna.phy.channel.tr38901 import Antenna ant = Antenna(polarization='single', polarization_type='V', antenna_pattern='omni', carrier_frequency=3.5e9) print(ant.num_ant) # 1 """ def __init__( self, polarization: str, polarization_type: str, antenna_pattern: str, carrier_frequency: float, precision: Optional[str] = None, device: Optional[str] = None, ) -> None: super().__init__( num_rows_per_panel=1, num_cols_per_panel=1, polarization=polarization, polarization_type=polarization_type, antenna_pattern=antenna_pattern, carrier_frequency=carrier_frequency, precision=precision, device=device, )
[docs] class AntennaArray(PanelArray): # pylint: disable=line-too-long r""" Antenna array following the :cite:p:`TR38901` specification This class is a special case of :class:`~sionna.phy.channel.tr38901.PanelArray`, and can be used in lieu of it. :param num_rows: Number of rows of elements :param num_cols: Number of columns of elements :param polarization: Polarization. One of ``"single"`` or ``"dual"``. :param polarization_type: Type of polarization. For single polarization, must be ``"V"`` or ``"H"``. For dual polarization, must be ``"VH"`` or ``"cross"``. :param antenna_pattern: Element radiation pattern. One of ``"omni"`` or ``"38.901"``. :param carrier_frequency: Carrier frequency [Hz] :param vertical_spacing: Element vertical spacing [multiple of wavelength]. Defaults to 0.5 if set to `None`. :param horizontal_spacing: Element horizontal spacing [multiple of wavelength]. Defaults to 0.5 if set to `None`. :param precision: Precision used for internal calculations and outputs. If set to `None`, :attr:`~sionna.phy.config.Config.precision` is used. :param device: Device for computation (e.g., ``"cpu"``, ``"cuda:0"``). If `None`, :attr:`~sionna.phy.config.Config.device` is used. .. rubric:: Examples .. code-block:: python from sionna.phy.channel.tr38901 import AntennaArray array = AntennaArray(num_rows=4, num_cols=4, polarization='dual', polarization_type='cross', antenna_pattern='38.901', carrier_frequency=3.5e9) print(array.num_ant) # 32 """ def __init__( self, num_rows: int, num_cols: int, polarization: str, polarization_type: str, antenna_pattern: str, carrier_frequency: float, vertical_spacing: Optional[float] = None, horizontal_spacing: Optional[float] = None, precision: Optional[str] = None, device: Optional[str] = None, ) -> None: super().__init__( num_rows_per_panel=num_rows, num_cols_per_panel=num_cols, polarization=polarization, polarization_type=polarization_type, antenna_pattern=antenna_pattern, carrier_frequency=carrier_frequency, element_vertical_spacing=vertical_spacing, element_horizontal_spacing=horizontal_spacing, precision=precision, device=device, )