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]