## SPDX-FileCopyrightText: Copyright (c) 2021-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.# SPDX-License-Identifier: Apache-2.0#"""Classes and functions related to antenna patternsT"""fromabcimportABCfromtypingimportCallable,List,Tupleimportinspectimportdrjitasdrimportmitsubaasmiimportnumpyasnpimportmatplotlib.pyplotaspltfrommatplotlibimportcmfrom.utilsimporttheta_phi_from_unit_vec,isclose,to_world_jones_rotator,\
jones_matrix_rotator_flip_forwardfrom.registryimportRegistry#################################################### Polarizations#################################################### A polarization is defined by either one or two# slant angles that are applied to the antenna# pattern.polarization_registry=Registry()
[docs]defregister_polarization(name:str,slant_angles:List[float]):"""Registers a new polarization A polarization is defined as a list of one or two slant angles that will be applied to a vertically polarized antenna pattern function to create the desired polarization directions. :param name: Name of the polarization :param slant_angles: List of one or two slant angles """ifnotisinstance(slant_angles,List):raiseValueError("`slant_angles` must be a list")eliflen(slant_angles)notin[1,2]:raiseValueError("`slant_angles` must be a list of length one or two")forslant_angleinslant_angles:ifnotisinstance(slant_angle,float):raiseValueError("Each slant angle must be a float")polarization_registry.register(slant_angles,name)
register_polarization("V",[0.0])register_polarization("H",[dr.pi/2])register_polarization("VH",[0.0,dr.pi/2])register_polarization("cross",[-dr.pi/4,dr.pi/4])##################################################### Polarization models##################################################### A polarization model use a slant angle to transform# a vertically polarized antenna pattern into an arbitrarily# rotated linearly polarized antenna patternpolarization_model_registry=Registry()
[docs]defregister_polarization_model(name:str,model:Callable[[mi.Complex2f,mi.Float,mi.Float,mi.Float],Tuple[mi.Complex2f,mi.Complex2f]]):"""Registers a new polarization model A polarization model uses a slant angle to transform a vertically polarized antenna pattern into an arbitrarily rotated linearly polarized antenna pattern :param name: Name of the polarization model :param model: Polarization model """ifnotcallable(model):raiseValueError("`model` must be a callable")iflen(inspect.signature(model).parameters)!=4:raiseValueError("`model` must take four arguments")polarization_model_registry.register(model,name)
[docs]@polarization_model_registry.register(name="tr38901_1")defpolarization_model_tr38901_1(c_theta_tilde:mi.Complex2f,theta:mi.Float,phi:mi.Float,slant_angle:mi.Float)->Tuple[mi.Complex2f,mi.Complex2f]:# pylint: disable=line-too-longr"""Model-1 for polarized antennas from 3GPP TR 38.901 [TR38901]_ 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} :param c_theta_tilde: Vertically polarized zenith pattern :math:`\tilde{C}_\theta(\theta, \varphi)` :param theta: Zenith angles [rad] :param phi: Azimuth angles [rad] :param slant_angle: Slant angle of the linear polarization [rad]. A slant angle of zero means vertical polarization. :returns: Zenith (:math:`C_\theta`) and azimuth (:math:`C_\phi`) pattern """sin_slant,cos_slant=dr.sincos(slant_angle)sin_theta,cos_theta=dr.sincos(theta)sin_phi,cos_phi=dr.sincos(phi)sin_psi=sin_slant*cos_phicos_psi=cos_slant*sin_theta+sin_slant*sin_phi*cos_thetanorm=dr.sqrt(1.-dr.square(cos_slant*cos_theta-sin_slant*sin_phi*sin_theta))inv_norm=dr.select(isclose(norm,mi.Float(0.0),atol=1e-5),1.0,dr.rcp(norm))sin_psi=sin_psi*inv_normcos_psi=cos_psi*inv_normc_theta=cos_psi*c_theta_tildec_phi=sin_psi*c_theta_tildereturnc_theta,c_phi
#pylint: disable=unused-argument
[docs]@polarization_model_registry.register(name="tr38901_2")defpolarization_model_tr38901_2(c_theta_tilde:mi.Complex2f,theta:mi.Float,phi:mi.Float,slant_angle:mi.Float)->Tuple[mi.Complex2f,mi.Complex2f]:r"""Model-2 for polarized antennas from 3GPP TR 38.901 [TR38901]_ 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} :param c_theta_tilde: Vertically polarized zenith pattern :math:`\tilde{C}_\theta(\theta, \varphi)` :param theta: Zenith angles [rad] :param phi: Azimuth angles [-pi, pi) [rad] :param slant_angle: Slant angle of the linear polarization [rad]. A slant angle of zero means vertical polarization. :returns: Zenith (:math:`C_\theta`) and azimuth (:math:`C_\phi`) pattern """sin_slant_angle,cos_slant_angle=dr.sincos(slant_angle)c_theta=cos_slant_angle*c_theta_tildec_phi=sin_slant_angle*c_theta_tildereturnc_theta,c_phi
##################################################### Vertically polarized antenna pattern functions##################################################### These functions are used by the above defined polarization models# pylint: disable=unused-argument
[docs]defv_iso_pattern(theta:mi.Float,phi:mi.Float)->mi.Complex2f:r""" Vertically polarized isotropic antenna pattern function :param theta: Elevation angle [rad] :param phi: Elevation angle [rad] """# Number of samplesn=dr.shape(theta)[0]# Zenith patternc_theta=dr.ones(mi.Float,n)returnmi.Complex2f(c_theta,0)
[docs]defv_dipole_pattern(theta:mi.Float,phi:mi.Float)->mi.Complex2f:r""" Vertically polarized short dipole antenna pattern function from (Eq. 4-26a) [Balanis97]_ :param theta: Elevation angle [rad] :param phi: Elevation angle [rad] """k=dr.sqrt(1.5)c_theta=dr.abs(k*dr.sin(theta))returnmi.Complex2f(c_theta,0)
[docs]classAntennaPattern(ABC):"""Abstract class for antenna patterns Any instance of this class must implement the :attr:`~sionna.rt.AntennaPattern.patterns` property which returns a list of one or two antenna patterns for single- or dual-polarized antennas, respectively. """def__getitem__(self,index):returnself.patterns[index]@propertydefpatterns(self):# pylint: disable=line-too-longr""" List of antenna patterns for one or two polarization directions. The pattern of a specific polarization direction can be also accessed by indexing the antenna pattern instance. :type: :py:class:`List` [:py:class:`Callable` [[:py:class:`mitsuba.Float`, :py:class:`mitsuba.Float`], :py:class:`Tuple` [:py:class:`mitsuba.Complex2f`, :py:class:`mitsuba.Complex2f`]]] """returnself._patterns@patterns.setterdefpatterns(self,v):ifnotisinstance(v,List):raiseValueError("`patterns` must be a list")iflen(v)>2:raiseValueError("`patterns` must be a list of length 1 or 2")self._patterns=v
[docs]defcompute_gain(self,polarization_direction:int=0,num_samples:int=1000,verbose:bool=True)->Tuple[mi.Float,mi.Float,mi.Float]:# pylint: disable=line-too-longr""" Computes directivity, gain, and radiation efficiency of the antenna pattern of one of the polarization directions 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 directivity :math:`D`, gain :math:`G`, and radiation efficiency :math:`\eta_\text{rad}=G/D` (see :eq:`G`). :param polarization_direction: Polarization direction (0 | 1) :param num_samples: Number of discretization steps for numerical integration :param verbose: If `True`, the results are pretty printed. :return: Directivity :math:`D`, gain :math:`G`, and radiation efficiency :math:`\eta_\text{rad}=G/D` Example ------- .. code-block:: Python from sionna.rt import PlanarArray array = PlanarArray(num_rows=1, num_cols=1, pattern="tr38901", polarization="V") d, g, eta = array.antenna_pattern.compute_gain(); :: Directivity [dB]: 9.825768560205825 Gain [dB]: 7.99998570013805 Efficiency [%]: 65.67826867103577 """pattern=self[polarization_direction]# Create angular meshgridtheta=dr.linspace(mi.Float,0,dr.pi,num_samples,False)phi=dr.linspace(mi.Float,-dr.pi,dr.pi,2*num_samples,False)theta_grid,phi_grid=dr.meshgrid(theta,phi,indexing="ij")# Compute the gainc_theta,c_phi=pattern(theta_grid,phi_grid)g=dr.abs(c_theta)**2+dr.abs(c_phi)**2# Find maximum directional gaing_max=dr.max(g)# Compute radiation efficiencydtheta=theta[1]-theta[0]dphi=phi[1]-phi[0]eta_rad=dr.sum(g*dr.sin(theta_grid)*dtheta*dphi)/(4*dr.pi)# Compute directivityd=g_max/eta_radifverbose:dr.print(f"Directivity [dB]: {10*np.log10(d[0]):.3}")dr.print(f"Gain [dB]: {10*np.log10(g_max[0]):.3}")dr.print(f"Efficiency [%]: {eta_rad[0]*100:.3}")returnd,g_max,eta_rad
[docs]defshow(self,polarization_direction:int=0,)->Tuple[plt.Figure,plt.Figure,plt.Figure]:# pylint: disable=line-too-longr""" Visualizes the antenna gain of the antenna pattern of one of the polarization directions This function visualizes the directional antenna gain with the help of three figures showing the vertical and horizontal cuts as well as a three-dimensional visualization. :param polarization_direction: Polarization direction (0 | 1) :returns: Vertical cut, horizontal cut, and 3D visualization of the antenna gain Example -------- .. code-block:: from sionna.rt import PlanarArray array = PlanarArray(num_rows=1, num_cols=1, pattern="dipole", polarization="V") array.antenna_pattern.show() .. 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% """pattern=self[polarization_direction]# Vertical cuttheta=dr.linspace(mi.Float,0,dr.pi,1000)phi=dr.zeros(mi.Float,1000)c_theta,c_phi=[c.numpy()forcinpattern(theta,phi)]g=np.abs(c_theta)**2+np.abs(c_phi)**2g=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)ifg_db_min==g_db_max:g_db_min=-30else:g_db_min=np.maximum(-60.,g_db_min)fig_v=plt.figure()plt.polar(theta.numpy(),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 cuttheta=dr.pi/2*dr.ones(mi.Float,1000)phi=dr.linspace(mi.Float,-dr.pi,dr.pi,1000)c_theta,c_phi=[c.numpy()forcinpattern(theta,phi)]g=np.abs(c_theta)**2+np.abs(c_phi)**2g=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_max-g_db_min)<0.1:g_db_min=-30else:g_db_min=np.maximum(-60.,g_db_min)fig_h=plt.figure()plt.polar(phi.numpy(),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 visualizationn=100# sample stepstheta=dr.linspace(mi.Float,0,dr.pi,n,False)phi=dr.linspace(mi.Float,-dr.pi,dr.pi,n,False)theta_grid,phi_grid=dr.meshgrid(theta,phi,indexing='ij')c_theta,c_phi=pattern(theta_grid,phi_grid)theta_grid=np.reshape(theta_grid,[n,n])phi_grid=np.reshape(phi_grid,[n,n])c_theta=np.reshape(c_theta,[n,n])c_phi=np.reshape(c_phi,[n,n])g=np.abs(c_theta)**2+np.abs(c_phi)**2x=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)defnorm(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)ifx_min==x_max:x=np.ones_like(x)else:x-=x_minx/=np.abs(x_max-x_min)returnxg_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"forzinxticklabels]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)$")returnfig_v,fig_h,fig_3d
antenna_pattern_registry=Registry()
[docs]defregister_antenna_pattern(name:str,pattern_factory:Callable[...,AntennaPattern]):"""Registers a new factory method for an antenna pattern :param name: Name of the factory method :param pattern_factory: A factory method returning an instance of :class:`~sionna.rt.AntennaPattern` """antenna_pattern_registry.register(pattern_factory,name)
[docs]classPolarizedAntennaPattern(AntennaPattern):""" Transforms a :ref:`vertically polarized antenna pattern function <v_pattern>` into an arbitray single- or dual-polarized antenna pattern based on a polarization and polarization model :param v_pattern: Vertically polarized antenna pattern function :param polarization: Name of registered polarization :list-registry:`sionna.rt.antenna_pattern.polarization_registry` :param polarization_model: Name of registered polarization model :list-registry:`sionna.rt.antenna_pattern.polarization_model_registry` """def__init__(self,*,v_pattern:Callable[[mi.Float,mi.Float],mi.Complex2f],polarization:str,polarization_model:str="tr38901_2"):super().__init__()self.v_pattern=v_patternapply_pol=polarization_model_registry.get(polarization_model)patterns=[]# Apply polarization model to the pattern for all slant anglesslant_angles=polarization_registry.get(polarization)forslant_angleinslant_angles:defmake_closure(slant_angle):deff(theta,phi):returnapply_pol(v_pattern(theta,phi),theta,phi,slant_angle)returnfpatterns.append(make_closure(slant_angle))self.patterns=patterns
# Register all available antenna patternsdefcreate_factory(name:str)->Callable[[str,str],PolarizedAntennaPattern]:r"""Create a factory method for the instantiation of polarized antenna patterns Note that there must be a vertical antenna pattern function with name "v_{s}_pattern" which is used. :param name: Name under which to register the factory method :returns: Callable creating an instance of PolarizedAntennaPattern """deff(*,polarization,polarization_model="tr38901_2"):returnPolarizedAntennaPattern(v_pattern=globals()["v_"+name+"_pattern"],polarization=polarization,polarization_model=polarization_model)returnfforsin["iso","dipole","hw_dipole","tr38901"]:register_antenna_pattern(s,create_factory(s))##################################################### Utilities####################################################
[docs]defantenna_pattern_to_world_implicit(pattern:Callable[[mi.Float,mi.Float],Tuple[mi.Complex2f,mi.Complex2f]],to_world:mi.Matrix3f,k_world:mi.Vector3f,direction:str)->mi.Vector4f:r""" Evaluates an antenna pattern for a given direction and returns it in the world implicit basis For a given direction in the world frame, this function first obtains the local zenith and azimuth angles :math:`\theta` and :math:`\phi` of the antenna. Then, the antenna pattern is evaluated to obtain the complex-valued zenith and azimuth patterns :math:`C_\theta` and :math:`C_\phi`, respectively. Both are then transformed into the real-valued vectors .. math:: \mathbf{f}_\text{real} = \begin{bmatrix} \Re\{C_\theta(\theta,\phi)\} \\ \Re\{C_\phi(\theta,\phi)\} \end{bmatrix} \mathbf{f}_\text{imag} = \begin{bmatrix} \Im\{C_\theta(\theta,\phi)\} \\ \Im\{C_\phi(\theta,\phi)\} \end{bmatrix}. The final output is obtained by applying a to-world rotation matrix :math:`\mathbf{W}` to both vectors before they are stacked: .. math:: \mathbf{v}_\text{out} = \begin{bmatrix} \mathbf{W} \mathbf{f}_\text{real}\\ \mathbf{W} \mathbf{f}_\text{imag} \end{bmatrix}. The parameter `direction` indicates the direction of propagation of the transverse wave with respect to the antenna, i.e., away from the antenna (`direction = "out"`) or towards the antenna (`direction = "in"`). If the wave propagates towards the antenna, then the evaluated antenna pattern is rotated to be represented in the world frame. :param pattern: Antenna pattern :param to_world: To-world rotation matrix :param k_world: Direction in which to evaluate the antenna pattern in the world frame :param direction: Direction of propagation with respect to the antenna ("in" | "out") :return: Antenna pattern in the world implicit basis as a real-valued vector """to_local=to_world.T# Direction of propagation in the local framek_local=to_local@k_world# Evaluate the antenna pattern# Zenith and azimuth angle in the local frametheta_local,phi_local=theta_phi_from_unit_vec(k_local)# Evaluate the antenna pattern in the local spherical framec_theta,c_phi=pattern(theta_local,phi_local)f_real,f_imag=complex2real_antenna_pattern(c_theta,c_phi)# As the antenna pattern is evaluated in the spherical coordinate# system, and because the implicit basis is the spherical coordinate system,# there is no need to rotate the antenna pattern to the implicit basis.# Rotation matrix to the world implicit basis from the local implicit basisrot_to_world_implicit=to_world_jones_rotator(to_world,k_local)# If the wave propagates towards the antenna, then `k_world` points in the# opposite direction, and the antenna pattern is rotated to match the# frame in which the wave is represented.ifdirection=="in":flip_rotator=jones_matrix_rotator_flip_forward(k_world)rotator=flip_rotator@rot_to_world_implicitelse:rotator=rot_to_world_implicit# Apply the rotationf_real=rotator@f_realf_imag=rotator@f_imag# Returns the antenna pattern vector as a real vector of dimension 4f=mi.Vector4f(f_real.x,f_real.y,f_imag.x,f_imag.y)returnf
[docs]defcomplex2real_antenna_pattern(c_theta:mi.Complex2f,c_phi:mi.Complex2f,)->Tuple[mi.Vector2f,mi.Vector2f]:""" Converts a complex-valued antenna pattern to a real-valued representation :param c_theta: Zenith antenna pattern :param c_phi: Azimuth antenna pattern :returns: Tuple of the real and imaginary parts of the zenith and azimuth antenna patterns """c_real=mi.Vector2f(c_theta.real,c_phi.real)c_imag=mi.Vector2f(c_theta.imag,c_phi.imag)returnc_real,c_imag