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 rdtype, respectively:

from sionna.phy import Object
obj = Object(precision="single")
print(obj.cdtype)
print(obj.rdtype)
<dtype: 'complex64'>
<dtype: 'float32'>

If the precision argument is not provided, Objects use the global config.precision parameter, 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.rdtype)
<dtype: 'complex128'>
<dtype: 'float64'>

Understanding Sionna Blocks

Sionna Blocks inherit from Objects 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 arbitray 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 tensorflow as tf
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 = tf.constant([3], dtype=tf.float32)
y = tf.complex(2., 3.)
my_block(x, y)
<dtype: 'float64'>
<dtype: 'complex128'>

As the internal precision of all Blocks was set via the global precision flag to double preceision, 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:

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 to a function so that algorithms can be unrolled during the creation of the computation graph.

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

import numpy as np
import tensorflow as tf
from sionna.phy import Block

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=tf.zeros([10, 12]))
(2,)
<dtype: 'float64'>
(10, 12)

Note that the argument x was provided as NumPy array which is converted to a TensorFlow tensor within the Block. This is in contrast to the example above, where this did not happen for an integer input. For a detailed understanding of type conversions within Blocks, we refer to the source code of the class method Block._convert_to_tensor.