antenna_array.py (9203B)
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 antenna arrays 7 """ 8 import tensorflow as tf 9 import numpy as np 10 import matplotlib.pyplot as plt 11 from matplotlib.markers import MarkerStyle 12 from .antenna import Antenna 13 from . import scene 14 from .utils import rotate 15 16 class AntennaArray(): 17 # pylint: disable=line-too-long 18 r""" 19 Class implementing an antenna array 20 21 An antenna array is composed of identical antennas that are placed 22 at different positions. The ``positions`` parameter can be assigned 23 to a TensorFlow variable or tensor. 24 25 .. code-block:: Python 26 27 array = AntennaArray(antenna=Antenna("tr38901", "V"), 28 positions=tf.Variable([[0,0,0], [0, 1, 1]])) 29 30 Parameters 31 ---------- 32 antenna : :class:`~sionna.rt.Antenna` 33 Antenna instance 34 35 positions : [array_size, 3], array_like 36 Array of relative positions :math:`(x,y,z)` [m] of each 37 antenna (dual-polarized antennas are counted as a single antenna 38 and share the same position). 39 The absolute position of the antennas is obtained by 40 adding the position of the :class:`~sionna.rt.Transmitter` 41 or :class:`~sionna.rt.Receiver` using it. 42 43 dtype : tf.complex64 or tf.complex128 44 Data type used for all computations. 45 Defaults to `tf.complex64`. 46 """ 47 def __init__(self, antenna, positions, dtype=tf.complex64): 48 super().__init__() 49 50 if dtype not in (tf.complex64, tf.complex128): 51 raise ValueError("`dtype` must be tf.complex64 or tf.complex128`") 52 self._rdtype = dtype.real_dtype 53 self.antenna = antenna 54 self.positions = positions 55 56 @property 57 def antenna(self): 58 """ 59 :class:`~sionna.rt.Antenna` : Get/set the antenna 60 """ 61 return self._antenna 62 63 @antenna.setter 64 def antenna(self, antenna): 65 if not isinstance(antenna, Antenna): 66 raise TypeError("``antenna`` must be an instance of Antenna.") 67 self._antenna = antenna 68 69 @property 70 def positions(self): 71 """ 72 [array_size, 3], `tf.float` : Get/set array of relative positions 73 :math:`(x,y,z)` [m] of each antenna (dual-polarized antennas are 74 counted as a single antenna and share the same position). 75 """ 76 return self._positions 77 78 @positions.setter 79 def positions(self, positions): 80 if isinstance(positions, tf.Variable): 81 if positions.dtype != self._rdtype: 82 raise TypeError(f"`positions` must have dtype={self._rdtype}") 83 else: 84 self._positions = positions 85 else: 86 self._positions = tf.cast(positions, self._rdtype) 87 88 @property 89 def num_ant(self): 90 """ 91 int (read-only) : Number of linearly polarized antennas in the array. 92 Dual-polarized antennas are counted as two linearly polarized 93 antennas. 94 """ 95 return self._positions.shape[0]*len(self._antenna.patterns) 96 97 @property 98 def array_size(self): 99 """ 100 int (read-only) : Number of antennas in the array. 101 Dual-polarized antennas are counted as a single antenna. 102 """ 103 return self._positions.shape[0] 104 105 def rotated_positions(self, orientation): 106 r""" 107 Get the antenna positions rotated according to ``orientation`` 108 109 Input 110 ------ 111 orientation : [3], tf.float 112 Orientation :math:`(\alpha, \beta, \gamma)` [rad] specified 113 through three angles corresponding to a 3D rotation 114 as defined in :eq:`rotation`. 115 116 Output 117 ------- 118 : [array_size, 3] 119 Rotated positions 120 """ 121 # [array_size, 3] 122 rot_p = rotate(self.positions, orientation) 123 return rot_p 124 125 class PlanarArray(AntennaArray): 126 # pylint: disable=line-too-long 127 r""" 128 Class implementing a planar antenna array 129 130 The antennas are regularly spaced, located in the y-z plane, and 131 numbered column-first from the top-left to bottom-right corner. 132 133 Parameters 134 ---------- 135 num_rows : int 136 Number of rows 137 138 num_cols : int 139 Number of columns 140 141 vertical_spacing : float 142 Vertical antenna spacing [multiples of wavelength]. 143 144 horizontal_spacing : float 145 Horizontal antenna spacing [multiples of wavelength]. 146 147 pattern : str, callable, or length-2 sequence of callables 148 Antenna pattern. Either one of 149 ["iso", "dipole", "hw_dipole", "tr38901"], 150 or a callable, or a length-2 sequence of callables defining 151 antenna patterns. In the latter case, the antennas are dual 152 polarized and each callable defines the antenna pattern 153 in one of the two orthogonal polarization directions. 154 An antenna pattern is a callable that takes as inputs vectors of 155 zenith and azimuth angles of the same length and returns for each 156 pair the corresponding zenith and azimuth patterns. See :eq:`C` for 157 more detail. 158 159 polarization : str or None 160 Type of polarization. For single polarization, must be "V" (vertical) 161 or "H" (horizontal). For dual polarization, must be "VH" or "cross". 162 Only needed if ``pattern`` is a string. 163 164 polarization_model: int, one of [1,2] 165 Polarization model to be used. Options `1` and `2` 166 refer to :func:`~sionna.rt.antenna.polarization_model_1` 167 and :func:`~sionna.rt.antenna.polarization_model_2`, 168 respectively. 169 Defaults to `2`. 170 171 dtype : tf.complex64 or tf.complex128 172 Datatype used for all computations. 173 Defaults to `tf.complex64`. 174 175 Example 176 ------- 177 .. code-block:: Python 178 179 array = PlanarArray(8,4, 0.5, 0.5, "tr38901", "VH") 180 array.show() 181 182 .. figure:: ../figures/antenna_array.png 183 :align: center 184 :scale: 100% 185 """ 186 def __init__(self, 187 num_rows, 188 num_cols, 189 vertical_spacing, 190 horizontal_spacing, 191 pattern, 192 polarization=None, 193 polarization_model=2, 194 dtype=tf.complex64): 195 196 if dtype not in (tf.complex64, tf.complex128): 197 raise ValueError("`dtype` must be tf.complex64 or tf.complex128`") 198 199 # Create list of antennas 200 array_size = num_rows*num_cols 201 antenna = Antenna(pattern, polarization, polarization_model, dtype) 202 203 # Compute antenna positions 204 d_v = vertical_spacing 205 d_h = horizontal_spacing 206 positions = np.zeros([array_size, 3]) 207 208 for i in range(num_rows): 209 for j in range(num_cols): 210 positions[i + j*num_rows] = [0, 211 j*d_h, 212 -i*d_v] 213 214 # Center the panel around the origin 215 offset = [0, 216 -(num_cols-1)*d_h/2, 217 (num_rows-1)*d_v/2] 218 positions += offset 219 super().__init__(antenna, positions, dtype) 220 self._positions_set = False 221 222 @property 223 def positions(self): 224 """ 225 [array_size, 3], `tf.float` : Get/set array of relative positions 226 :math:`(x,y,z)` [m] of each antenna (dual-polarized antennas are 227 counted as a single antenna and share the same position). 228 """ 229 if not self._positions_set: 230 # Scale positions by wavelength 231 if hasattr(scene.Scene(), "wavelength"): 232 wavelength = scene.Scene().wavelength 233 else: 234 wavelength = tf.cast(1, self._rdtype) 235 return self._positions*wavelength 236 else: 237 # Return provided positions 238 return self._positions 239 240 @positions.setter 241 def positions(self, positions): 242 if isinstance(positions, tf.Variable): 243 if positions.dtype != self._rdtype: 244 raise TypeError(f"`positions` must have dtype={self._rdtype}") 245 else: 246 self._positions = positions 247 else: 248 self._positions = tf.cast(positions, self._rdtype) 249 self._positions_set = True 250 251 def show(self): 252 r"""show() 253 254 Visualizes the antenna array 255 256 Antennas are depicted by markers that are annotated with the antenna 257 number. The marker is not related to the polarization of an antenna. 258 259 Output 260 ------ 261 : :class:`matplotlib.pyplot.Figure` 262 Figure depicting the antenna array 263 """ 264 fig = plt.figure() 265 plt.plot(self.positions[:,1], self.positions[:,2], 266 marker=MarkerStyle("+").get_marker(), markeredgecolor='red', 267 markerfacecolor='red', markersize="10", linestyle="None", 268 markeredgewidth="1") 269 for i, p in enumerate(self.positions): 270 fig.axes[0].annotate(i+1, (p[1], p[2])) 271 plt.xlabel("y (m)") 272 plt.ylabel("z (m)") 273 plt.title("Planar Array Layout") 274 return fig