Realistic Multiuser MIMO OFDM Simulations

In this notebook, you will learn how to setup realistic simulations of multiuser MIMO uplink transmissions. Multiple user terminals (UTs) are randomly distributed in a cell sector and communicate with a multi-antenna base station.


The block-diagramm of the system model looks as follows:

System Model

It includes the following components:


  • QAM modulation

  • OFDM resource grid with configurable pilot pattern

  • Multiple single-antenna transmitters and a multi-antenna receiver

  • 3GPP 38.901 UMi, UMa, and RMa channel models and antenna patterns

  • LS Channel estimation with nearest-neighbor interpolation as well as perfect CSI

  • LMMSE MIMO equalization

You will learn how to setup the topologies required to simulate such scenarios and investigate

  • the performance over different models, and

  • the impact of imperfect CSI.

We will first walk through the configuration of all components of the system model, before simulating some simple uplink transmissions in the frequency domain. We will then simulate CDFs of the channel condition number and look into frequency-selectivity of the different channel models to understand the reasons for the observed performance differences.

It is recommended that you familiarize yourself with the API documentation of the Channel module and, in particular, the 3GPP 38,901 models that require a substantial amount of configuration. The last set of simulations in this notebook take some time, especially when you have no GPU available. For this reason, we provide the simulation results directly in the cells generating the figures. Simply uncomment the corresponding lines to show this results.

GPU Configuration and Imports

import os
gpu_num = 0 # Use "" to use the CPU
os.environ["CUDA_VISIBLE_DEVICES"] = f"{gpu_num}"
os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3'
# Import Sionna
    import sionna
except ImportError as e:
    # Install Sionna if package is not already installed
    import os
    os.system("pip install sionna")
    import sionna

# Configure the notebook to use only a single GPU and allocate only as much memory as needed
# For more details, see
import tensorflow as tf
gpus = tf.config.list_physical_devices('GPU')
if gpus:
        tf.config.experimental.set_memory_growth(gpus[0], True)
    except RuntimeError as e:
# Avoid warnings from TensorFlow
%matplotlib inline
import matplotlib.pyplot as plt
import numpy as np
import time
import pickle

from sionna.mimo import StreamManagement

from sionna.ofdm import ResourceGrid, ResourceGridMapper, LSChannelEstimator, LMMSEEqualizer
from sionna.ofdm import OFDMModulator, OFDMDemodulator, ZFPrecoder, RemoveNulledSubcarriers

from import Antenna, AntennaArray, CDL, UMi, UMa, RMa
from import gen_single_sector_topology as gen_topology
from import subcarrier_frequencies, cir_to_ofdm_channel, cir_to_time_channel
from import ApplyOFDMChannel, ApplyTimeChannel, OFDMChannel

from sionna.fec.ldpc.encoding import LDPC5GEncoder
from sionna.fec.ldpc.decoding import LDPC5GDecoder

from sionna.mapping import Mapper, Demapper

from sionna.utils import BinarySource, ebnodb2no, sim_ber, QAMSource
from sionna.utils.metrics import compute_ber

System Setup

We will now configure all components of the system model step-by-step.

scenario = "umi"
carrier_frequency = 3.5e9
direction = "uplink"
num_ut = 4
batch_size = 32
# Define the UT antenna array
ut_array = Antenna(polarization="single",

# Define the BS antenna array
bs_array = AntennaArray(num_rows=1,

# Create channel model
channel_model = UMi(carrier_frequency=carrier_frequency,

# Generate the topology
topology = gen_topology(batch_size, num_ut, scenario)

# Set the topology

# Visualize the topology
# The number of transmitted streams is equal to the number of UT antennas
num_streams_per_tx = 1

# Create an RX-TX association matrix
# rx_tx_association[i,j]=1 means that receiver i gets at least one stream
# from transmitter j. Depending on the transmission direction (uplink or downlink),
# the role of UT and BS can change. However, as we have only a single
# transmitter and receiver, this does not matter:
rx_tx_association = np.zeros([1, num_ut])
rx_tx_association[0, :] = 1

# Instantiate a StreamManagement object
# This determines which data streams are determined for which receiver.
# In this simple setup, this is fairly simple. However, it can get complicated
# for simulations with many transmitters and receivers.
sm = StreamManagement(rx_tx_association, num_streams_per_tx)
rg = ResourceGrid(num_ofdm_symbols=14,
num_bits_per_symbol = 2 # QPSK modulation
coderate = 0.5 # The code rate
n = int(rg.num_data_symbols*num_bits_per_symbol) # Number of coded bits
k = int(n*coderate) # Number of information bits

# The binary source will create batches of information bits
binary_source = BinarySource()
qam_source = QAMSource(num_bits_per_symbol)

# The encoder maps information bits to coded bits
encoder = LDPC5GEncoder(k, n)

# The mapper maps blocks of information bits to constellation symbols
mapper = Mapper("qam", num_bits_per_symbol)

# The resource grid mapper maps symbols onto an OFDM resource grid
rg_mapper = ResourceGridMapper(rg)

# This function removes nulled subcarriers from any tensor having the shape of a resource grid
remove_nulled_scs = RemoveNulledSubcarriers(rg)

# The LS channel estimator will provide channel estimates and error variances
ls_est = LSChannelEstimator(rg, interpolation_type="nn")

# The LMMSE equalizer will provide soft symbols together with noise variance estimates
lmmse_equ = LMMSEEqualizer(rg, sm)

# The demapper produces LLR for all coded bits
demapper = Demapper("app", "qam", num_bits_per_symbol)

# The decoder provides hard-decisions on the information bits
decoder = LDPC5GDecoder(encoder, hard_out=True)

# OFDM CHannel
ofdm_channel = OFDMChannel(channel_model, rg, add_awgn=True, normalize_channel=False, return_channel=True)
channel_freq = ApplyOFDMChannel(add_awgn=True)
frequencies = subcarrier_frequencies(rg.fft_size, rg.subcarrier_spacing)