Custom Antenna Patterns#
As explained in greater detail in “Far Field of a Transmitting Antenna”, an antenna pattern maps a zenith and azimuth angle to two complex numbers, the zenith and azimuth patterns, respectively. Mathematically, it is defined as a function \(f:(\theta,\varphi)\mapsto (C_\theta(\theta, \varphi), C_\varphi(\theta, \varphi))\).
If you want to add a new AntennaPattern to Sionna RT, you must register
a factory method for it together with a name, using the function
register_antenna_pattern().
Once this is done, the new antenna pattern can be used everywhere by providing
its name. An example is shown below:
import mitsuba as mi
import drjit as dr
from sionna.rt import AntennaPattern, PlanarArray, register_antenna_pattern
def v_sin_pow_pattern(theta: mi.Float, phi: mi.Float, n: mi.Float) -> mi.Complex2f:
"""Vertically polarized antenna pattern function"""
return mi.Complex2f(dr.power(dr.sin(theta), n), 0)
class MyPattern(AntennaPattern):
def __init__(self, n):
def my_pattern(theta, phi, n):
"""Adds a zero azimuth component to define a valid antenna pattern"""
c_theta = v_sin_pow_pattern(theta, phi, n=n)
c_phi = dr.zeros(mi.Complex2f, dr.width(c_theta))
return c_theta, c_phi
# Create compulsory pattern property
# Add a second pattern here to make it dual-polarized
self.patterns = [lambda theta, phi: my_pattern(theta, phi, n)]
def my_pattern_factory(n=3):
"""Factory method that returns an instance of the antenna pattern"""
return MyPattern(n=n)
# Register the factory method
register_antenna_pattern("my_pattern", my_pattern_factory)
# Use the custom antenna pattern with the rest of Sionna RT
array = PlanarArray(num_rows=1, num_cols=1, pattern="my_pattern", n=8)
array.antenna_pattern.compute_gain();
Directivity [dB]: 5.24
Gain [dB]: 0.0
Efficiency [%]: 30.0
Rather than specifying an antenna pattern from scratch, you can also register a
factory method for a new PolarizedAntennaPattern which uses
a vertically polarized antenna pattern function:
import mitsuba as mi
import drjit as dr
from sionna.rt import PolarizedAntennaPattern, PlanarArray, \
register_antenna_pattern, register_polarization
def v_sin_pow_pattern(theta: mi.Float, phi: mi.Float, n: mi.Float) -> mi.Complex2f:
"""Vertically polarized antenna pattern function"""
return mi.Complex2f(dr.power(dr.sin(theta), n), 0)
def my_pattern_factory(*, n, polarization, polarization_model):
"""Factory method returning an instance of a PolarizedAntennaPattern
with the newly created pattern function
"""
return PolarizedAntennaPattern(
v_pattern=lambda theta, phi: v_sin_pow_pattern(theta, phi, n),
polarization=polarization,
polarization_model=polarization_model
)
register_antenna_pattern("my_pattern", my_pattern_factory)
# Register a custom polarization
# Since we provide two slant angles, the resulting
# antenna pattern will be dual-polarized
register_polarization("my_polarization", [-dr.pi/6, dr.pi*2/6])
# Use the custom antenna pattern with the rest of Sionna RT
array = PlanarArray(num_rows=1, num_cols=1,
pattern="my_pattern",
n=12,
polarization="my_polarization",
polarization_model="tr38901_1")
In the example above, we have also used register_polarization()
to create a new polarization which can be used together with any registered
antenna pattern factory method that uses a polarization as keyword argument.
If needed, also new polarization models can be registered via
register_polarization_model().
Gradient-based Optimization#
Thanks to Dr.Jit’s automatic differentiation capabilities, it is possible to define antenna patterns with parameters that can be optimized via gradient descent. In the following example, we will create a new antenna pattern that consists of a single spherical Gaussian with trainable mean direction and sharpness.
import mitsuba as mi
import drjit as dr
from sionna.rt import r_hat, PolarizedAntennaPattern, register_antenna_pattern
class Trainable_V_Pattern:
"""Trainable vertically polarized antenna pattern function
Defined via a spherical Gaussian with trainable mean direction
and sharpness.
"""
def __init__(self, opt: mi.ad.Optimizer):
# Add trainable target directions to optimizer
opt["theta_t"] = mi.Float(dr.pi/2)
opt["phi_t"] = mi.Float(0)
# Add trainable sharpness to optimizer
opt["lambda"] = mi.Float(1)
self.opt = opt
def __call__(self, theta, phi):
mu = r_hat(self.opt["theta_t"], self.opt["phi_t"])
v = r_hat(theta, phi)
gain = 2*self.opt["lambda"]*dr.rcp(1-dr.exp(-2*self.opt["lambda"])) \
*dr.exp(self.opt["lambda"]*(dr.dot(mu, v) - 1))
c_theta_real = dr.sqrt(gain)
return mi.Complex2f(c_theta_real, 0)
# The factory method requires a new keyword argument `opt` which must be
# a Mitsuba optimizer
def trainable_pattern_factory(*, opt, polarization, polarization_model="tr38901_2"):
return PolarizedAntennaPattern(
v_pattern=Trainable_V_Pattern(opt),
polarization=polarization,
polarization_model=polarization_model
)
register_antenna_pattern("trainable", trainable_pattern_factory)
Let us now load and empty scene in which we place a transmitter and a receiver. The transmitter has an antenna array using our newly defined trainable antenna pattern.
from sionna.rt import load_scene, PlanarArray, Transmitter, Receiver
# Load empty scene
scene = load_scene()
# Create a Mitsuba Optimizer
opt = mi.ad.Adam(lr=1e-2)
# Define transmit array with trainable antenna pattern
scene.tx_array = PlanarArray(num_rows=1, num_cols=1,
pattern="trainable",
opt=opt,
polarization="V")
scene.rx_array = PlanarArray(num_rows=1, num_cols=1,
pattern="iso",
polarization="V")
# Add transmitter and receiver to the scene
scene.add(Transmitter(name="tx", position=[0,0,0]))
scene.add(Receiver(name="rx", position=[10,10,10]))
Next, we will compute propagation paths and compute gradients of the total receiver power with respect to the trainable parameters of the antenna patterns.
solver = PathSolver()
# Switch the computation of field loop to "evaluated" mode to
# enable gradient backpropagation through the loop
solver.field_calculator.loop_mode = "evaluated"
# Compute propagation paths
paths = solver(scene, max_depth=0)
# Compute total received power
a_r, a_i = paths.a
power = dr.sum(a_r**2 + a_i**2)
# Compute gradients
dr.backward(power)
print(opt.variables["theta_t"].grad)
print(opt.variables["phi_t"].grad)
print(opt.variables["lambda"].grad)
[-1.35529e-07]
[1.35529e-07]
[6.20459e-08]