#
# SPDX-FileCopyrightText: Copyright (c) 2021-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
"""
Implements classes and methods related to antenna arrays
"""
import tensorflow as tf
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.markers import MarkerStyle
from .antenna import Antenna
from . import scene
from .utils import rotate
[docs]class AntennaArray():
# pylint: disable=line-too-long
r"""
Class implementing an antenna array
An antenna array is composed of identical antennas that are placed
at different positions. The ``positions`` parameter can be assigned
to a TensorFlow variable or tensor.
.. code-block:: Python
array = AntennaArray(antenna=Antenna("tr38901", "V"),
positions=tf.Variable([[0,0,0], [0, 1, 1]]))
Parameters
----------
antenna : :class:`~sionna.rt.Antenna`
Antenna instance
positions : [array_size, 3], array_like
Array of relative positions :math:`(x,y,z)` [m] of each
antenna (dual-polarized antennas are counted as a single antenna
and share the same position).
The absolute position of the antennas is obtained by
adding the position of the :class:`~sionna.rt.Transmitter`
or :class:`~sionna.rt.Receiver` using it.
dtype : tf.complex64 or tf.complex128
Data type used for all computations.
Defaults to `tf.complex64`.
"""
def __init__(self, antenna, positions, dtype=tf.complex64):
super().__init__()
if dtype not in (tf.complex64, tf.complex128):
raise ValueError("`dtype` must be tf.complex64 or tf.complex128`")
self._rdtype = dtype.real_dtype
self.antenna = antenna
self.positions = positions
@property
def antenna(self):
"""
:class:`~sionna.rt.Antenna` : Get/set the antenna
"""
return self._antenna
@antenna.setter
def antenna(self, antenna):
if not isinstance(antenna, Antenna):
raise TypeError("``antenna`` must be an instance of Antenna.")
self._antenna = antenna
@property
def positions(self):
"""
[array_size, 3], `tf.float` : Get/set array of relative positions
:math:`(x,y,z)` [m] of each antenna (dual-polarized antennas are
counted as a single antenna and share the same position).
"""
return self._positions
@positions.setter
def positions(self, positions):
if isinstance(positions, tf.Variable):
if positions.dtype != self._rdtype:
raise TypeError(f"`positions` must have dtype={self._rdtype}")
else:
self._positions = positions
else:
self._positions = tf.cast(positions, self._rdtype)
@property
def num_ant(self):
"""
int (read-only) : Number of linearly polarized antennas in the array.
Dual-polarized antennas are counted as two linearly polarized
antennas.
"""
return self._positions.shape[0]*len(self._antenna.patterns)
@property
def array_size(self):
"""
int (read-only) : Number of antennas in the array.
Dual-polarized antennas are counted as a single antenna.
"""
return self._positions.shape[0]
[docs] def rotated_positions(self, orientation):
r"""
Get the antenna positions rotated according to ``orientation``
Input
------
orientation : [3], tf.float
Orientation :math:`(\alpha, \beta, \gamma)` [rad] specified
through three angles corresponding to a 3D rotation
as defined in :eq:`rotation`.
Output
-------
: [array_size, 3]
Rotated positions
"""
# [array_size, 3]
rot_p = rotate(self.positions, orientation)
return rot_p
[docs]class PlanarArray(AntennaArray):
# pylint: disable=line-too-long
r"""
Class implementing a planar antenna array
The antennas are regularly spaced, located in the y-z plane, and
numbered column-first from the top-left to bottom-right corner.
Parameters
----------
num_rows : int
Number of rows
num_cols : int
Number of columns
vertical_spacing : float
Vertical antenna spacing [multiples of wavelength].
horizontal_spacing : float
Horizontal antenna spacing [multiples of wavelength].
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 antennas are 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. See :eq:`C` for
more detail.
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
-------
.. code-block:: Python
array = PlanarArray(8,4, 0.5, 0.5, "tr38901", "VH")
array.show()
.. figure:: ../figures/antenna_array.png
:align: center
:scale: 100%
"""
def __init__(self,
num_rows,
num_cols,
vertical_spacing,
horizontal_spacing,
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`")
# Create list of antennas
array_size = num_rows*num_cols
antenna = Antenna(pattern, polarization, polarization_model, dtype)
# Compute antenna positions
d_v = vertical_spacing
d_h = horizontal_spacing
positions = np.zeros([array_size, 3])
for i in range(num_rows):
for j in range(num_cols):
positions[i + j*num_rows] = [0,
j*d_h,
-i*d_v]
# Center the panel around the origin
offset = [0,
-(num_cols-1)*d_h/2,
(num_rows-1)*d_v/2]
positions += offset
super().__init__(antenna, positions, dtype)
self._positions_set = False
@property
def positions(self):
"""
[array_size, 3], `tf.float` : Get/set array of relative positions
:math:`(x,y,z)` [m] of each antenna (dual-polarized antennas are
counted as a single antenna and share the same position).
"""
if not self._positions_set:
"""Scale positions by wavelength"""
if hasattr(scene.Scene(), "wavelength"):
wavelength = scene.Scene().wavelength
else:
wavelength = tf.cast(1, self._rdtype)
return self._positions*wavelength
else:
"""Return provided positions"""
return self._positions
@positions.setter
def positions(self, positions):
if isinstance(positions, tf.Variable):
if positions.dtype != self._rdtype:
raise TypeError(f"`positions` must have dtype={self._rdtype}")
else:
self._positions = positions
else:
self._positions = tf.cast(positions, self._rdtype)
self._positions_set = True
[docs] def show(self):
r"""show()
Visualizes the antenna array
Antennas are depicted by markers that are annotated with the antenna
number. The marker is not related to the polarization of an antenna.
Output
------
: :class:`matplotlib.pyplot.Figure`
Figure depicting the antenna array
"""
fig = plt.figure()
plt.plot(self.positions[:,1], self.positions[:,2],
marker=MarkerStyle("+").get_marker(), markeredgecolor='red',
markerfacecolor='red', markersize="10", linestyle="None",
markeredgewidth="1")
for i, p in enumerate(self.positions):
fig.axes[0].annotate(i+1, (p[1], p[2]))
plt.xlabel("y (m)")
plt.ylabel("z (m)")
plt.title("Planar Array Layout")
return fig