Sionna Block and Object#

All of Sionna PHY’s components inherit from the Sionna Object class.

A Sionna Object is instantiated with an optional precision argument from which it derives complex- and real-valued data types which can be accessed via the properties cdtype and dtype, respectively:

from sionna.phy import Object
obj = Object(precision="single")
print(obj.cdtype)
print(obj.dtype)
torch.complex64
torch.float32

If the precision argument is not provided, Object instances use the global precision parameter of the config singleton, as shown next:

from sionna.phy import config
from sionna.phy import Object
config.precision = "double"  # Set global precision
obj = Object()
print(obj.cdtype)
print(obj.dtype)
torch.complex128
torch.float64

Understanding Sionna Blocks#

Sionna Blocks inherit from Object and are used to implement most of Sionna’s components. To get an understanding of their features, let us implement a simple custom Block. Every Block must implement the method call() which can take arbitrary arguments and keyword arguments. It is important to understand that all tensor arguments are cast to the Block’s internal precision. The following code snippet demonstrates this behavior:

import torch
from sionna.phy import config
from sionna.phy import Block
config.precision = "double"

class MyBlock(Block):
    def call(self, x, y=None):
        print(x.dtype)
        if y is not None:
            print(y.dtype)

my_block = MyBlock()
x = torch.tensor([3.], dtype=torch.float32)
y = torch.complex(torch.tensor(2.), torch.tensor(3.))
my_block(x, y)
torch.float64
torch.complex128

As the internal precision of all Blocks was set via the global precision flag to double precision, the inputs x and y were cast to the corresponding dtypes prior to executing the Block’s call() method. Note that only floating data types are cast, as can be seen from the following example:

from sionna.phy import Block

class MyBlock(Block):
    def call(self, x):
        print(type(x))

my_block = MyBlock()
my_block(3)
<class 'int'>

The reason for this behavior is that we sometimes need to pass non-tensor arguments (e.g. shapes or indices) so that control flow can be traced correctly when using torch.compile.

In many cases, a Block requires some initialization that depends on the shapes of its inputs. The first time a Block is called, it executes the build() method which receives the shapes of all arguments and keyword arguments. The next example demonstrates this feature:

import numpy as np
import torch
from sionna.phy import config
from sionna.phy import Block
config.precision = "double"

class MyBlock(Block):
    def build(self, *args, **kwargs):
        self.x_shape = args[0]
        self.y_shape = kwargs["y"]

    def call(self, x, y=None):
        print(self.x_shape)
        print(x.dtype)
        print(self.y_shape)

my_block = MyBlock()
my_block(np.array([3., 3.]), y=torch.zeros([10, 12]))
(2,)
torch.float64
(10, 12)

Note that the argument x was provided as a NumPy array which is converted to a PyTorch tensor within the Block. This is in contrast to the example above, where an integer input was left unchanged. For a detailed understanding of type conversions within Blocks, see the method _convert() in the Object class.