#
# 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"""
from abc import ABC
from typing import Callable, List, Tuple
import inspect
import drjit as dr
import mitsuba as mi
import numpy as np
import matplotlib.pyplot as plt
from matplotlib import cm
from .utils import theta_phi_from_unit_vec, isclose, to_world_jones_rotator,\
jones_matrix_rotator_flip_forward
from .registry import Registry
###################################################
# Polarizations
###################################################
# A polarization is defined by either one or two
# slant angles that are applied to the antenna
# pattern.
polarization_registry = Registry()
[docs]
def register_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
"""
if not isinstance(slant_angles, List):
raise ValueError("`slant_angles` must be a list")
elif len(slant_angles) not in [1,2]:
raise ValueError("`slant_angles` must be a list of length one or two")
for slant_angle in slant_angles:
if not isinstance(slant_angle, float):
raise ValueError("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 pattern
polarization_model_registry = Registry()
[docs]
def register_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
"""
if not callable(model):
raise ValueError("`model` must be a callable")
if len(inspect.signature(model).parameters) != 4:
raise ValueError("`model` must take four arguments")
polarization_model_registry.register(model, name)
[docs]
@polarization_model_registry.register(name="tr38901_1")
def polarization_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-long
r"""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_phi
cos_psi = cos_slant*sin_theta + sin_slant*sin_phi*cos_theta
norm = 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_norm
cos_psi = cos_psi * inv_norm
c_theta = cos_psi * c_theta_tilde
c_phi = sin_psi * c_theta_tilde
return c_theta, c_phi
#pylint: disable=unused-argument
[docs]
@polarization_model_registry.register(name="tr38901_2")
def polarization_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_tilde
c_phi = sin_slant_angle * c_theta_tilde
return c_theta, c_phi
####################################################
# Vertically polarized antenna pattern functions
####################################################
# These functions are used by the above defined polarization models
# pylint: disable=unused-argument
[docs]
def v_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 samples
n = dr.shape(theta)[0]
# Zenith pattern
c_theta = dr.ones(mi.Float, n)
return mi.Complex2f(c_theta, 0)
[docs]
def v_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))
return mi.Complex2f(c_theta, 0)
[docs]
def v_hw_dipole_pattern(
theta: mi.Float,
phi: mi.Float
) -> mi.Complex2f:
r"""
Vertically polarized half-wavelength dipole antenna pattern function
from (Eq. 4-84) [Balanis97]_
:param theta: Elevation angle [rad]
:param phi: Elevation angle [rad]
"""
k = dr.sqrt(1.643)
sin_theta = dr.sin(theta)
inv_sin_theta = dr.select(isclose(sin_theta, mi.Float(0.0), atol=1e-5),
1.0,
dr.rcp(sin_theta))
c_theta = k*dr.cos(dr.pi/2.*dr.cos(theta)) * inv_sin_theta
c_theta = dr.abs(c_theta)
return mi.Complex2f(c_theta, 0)
[docs]
def v_tr38901_pattern(
theta: mi.Float,
phi: mi.Float
) -> mi.Complex2f:
r"""
Vertically polarized antenna pattern function from
3GPP TR 38.901 (Table 7.3-1) [TR38901]_
:param theta: Elevation angle [rad]
:param phi: Elevation angle [rad]
"""
# Wrap phi to [-PI,PI]
phi = phi+dr.pi
phi -= dr.floor(phi/(2.*dr.pi))*2.*dr.pi
phi -= dr.pi
# Zenith pattern
theta_3db = phi_3db = 65./180.*dr.pi
a_max = sla_v = 30.
g_e_max = 8.
a_v = -dr.min([12.*((theta-dr.pi/2.)/theta_3db)**2, sla_v])
a_h = -dr.min([12.*(phi/phi_3db)**2, a_max])
a_db = -dr.min([-(a_v + a_h), a_max]) + g_e_max
a = dr.power(10., a_db/10.)
c_theta = dr.sqrt(a)
return mi.Complex2f(c_theta, 0)
####################################################
# Antenna patterns
####################################################
[docs]
class AntennaPattern(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):
return self.patterns[index]
@property
def patterns(self):
# pylint: disable=line-too-long
r"""
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`]]]
"""
return self._patterns
@patterns.setter
def patterns(self, v):
if not isinstance(v, List):
raise ValueError("`patterns` must be a list")
if len(v)>2:
raise ValueError("`patterns` must be a list of length 1 or 2")
self._patterns = v
[docs]
def compute_gain(
self,
polarization_direction: int=0,
num_samples: int=1000,
verbose: bool=True
) -> Tuple[mi.Float, mi.Float, mi.Float]:
# pylint: disable=line-too-long
r"""
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 meshgrid
theta = 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 gain
c_theta, c_phi = pattern(theta_grid, phi_grid)
g = dr.abs(c_theta)**2 + dr.abs(c_phi)**2
# Find maximum directional gain
g_max = dr.max(g)
# Compute radiation efficiency
dtheta = theta[1]-theta[0]
dphi = phi[1]-phi[0]
eta_rad = dr.sum(g*dr.sin(theta_grid)*dtheta*dphi)/(4*dr.pi)
# Compute directivity
d = g_max / eta_rad
if verbose:
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}")
return d, g_max, eta_rad
[docs]
def show(
self,
polarization_direction: int=0,
) -> Tuple[plt.Figure, plt.Figure, plt.Figure]:
# pylint: disable=line-too-long
r"""
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 cut
theta = dr.linspace(mi.Float, 0, dr.pi, 1000)
phi = dr.zeros(mi.Float, 1000)
c_theta, c_phi = [c.numpy() for c in pattern(theta, phi)]
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.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 cut
theta = dr.pi/2*dr.ones(mi.Float, 1000)
phi = dr.linspace(mi.Float, -dr.pi, dr.pi, 1000)
c_theta, c_phi = [c.numpy() for c in pattern(theta, phi)]
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_max - g_db_min)<0.1:
g_db_min = -30
else:
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 visualization
n = 100 # sample steps
theta = 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)**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
antenna_pattern_registry = Registry()
[docs]
def register_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]
class PolarizedAntennaPattern(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_pattern
apply_pol = polarization_model_registry.get(polarization_model)
patterns = []
# Apply polarization model to the pattern for all slant angles
slant_angles = polarization_registry.get(polarization)
for slant_angle in slant_angles:
def make_closure(slant_angle):
def f(theta, phi):
return apply_pol(v_pattern(theta, phi),
theta, phi, slant_angle)
return f
patterns.append(make_closure(slant_angle))
self.patterns = patterns
# Register all available antenna patterns
def create_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
"""
def f(*, polarization, polarization_model="tr38901_2"):
return PolarizedAntennaPattern(
v_pattern=globals()["v_" + name + "_pattern"],
polarization=polarization,
polarization_model=polarization_model)
return f
for s in ["iso", "dipole", "hw_dipole", "tr38901"]:
register_antenna_pattern(s, create_factory(s))
####################################################
# Utilities
####################################################
[docs]
def antenna_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 frame
k_local = to_local@k_world
# Evaluate the antenna pattern
# Zenith and azimuth angle in the local frame
theta_local, phi_local = theta_phi_from_unit_vec(k_local)
# Evaluate the antenna pattern in the local spherical frame
c_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 basis
rot_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.
if direction == "in":
flip_rotator = jones_matrix_rotator_flip_forward(k_world)
rotator = flip_rotator@rot_to_world_implicit
else:
rotator = rot_to_world_implicit
# Apply the rotation
f_real = rotator@f_real
f_imag = rotator@f_imag
# Returns the antenna pattern vector as a real vector of dimension 4
f = mi.Vector4f(f_real.x, f_real.y, f_imag.x, f_imag.y)
return f
[docs]
def complex2real_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)
return c_real, c_imag