Power Control

This tutorial demonstrates how to allocate transmission power on a per-user basis in a 3GPP-compliant multicell scenario in the uplink and downlink direction.

  • Uplink: Implemented by the open_loop_uplink_power_control function. This follows the open-loop power allocation procedure in 3GPP TS 38.213 [1], where the transmit power partially compensate for the pathloss by a factor α[0;1] while targeting a received power of P0 [dBm];

  • Downlink: Handled by the downlink_fair_power_control function. This function maximizes a fairness function of the attainable throughput across the users on a per-sector basis.

power_control_notebook.png

Imports

We start by importing Sionna and the relevant external libraries:

[1]:
import os
os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3'
if os.getenv("CUDA_VISIBLE_DEVICES") is None:
    gpu_num = 0 # Use "" to use the CPU
    if gpu_num!="":
        print(f'\nUsing GPU {gpu_num}\n')
    else:
        print('\nUsing CPU\n')
    os.environ["CUDA_VISIBLE_DEVICES"] = f"{gpu_num}"

# Import Sionna
try:
    import sionna.sys
except ImportError as e:
    import sys
    if 'google.colab' in sys.modules:
       # Install Sionna in Google Colab
       print("Installing Sionna and restarting the runtime. Please run the cell again.")
       os.system("pip install sionna")
       os.kill(os.getpid(), 5)
    else:
       raise e

# Configure the notebook to use only a single GPU and allocate only as much memory as needed
# For more details, see https://www.tensorflow.org/guide/gpu
import tensorflow as tf
tf.get_logger().setLevel('ERROR')
gpus = tf.config.list_physical_devices('GPU')
if gpus:
    try:
        tf.config.experimental.set_memory_growth(gpus[0], True)
    except RuntimeError as e:
        print(e)
[2]:
# Additional external libraries
import matplotlib.pyplot as plt
import numpy as np

# Sionna components
from sionna.phy.utils import log2, dbm_to_watt, watt_to_dbm
from sionna.phy.constants import BOLTZMANN_CONSTANT
from sionna.phy.channel import GenerateOFDMChannel
from sionna.phy.ofdm import ResourceGrid, RZFPrecodedChannel, \
    EyePrecodedChannel, LMMSEPostEqualizationSINR
from sionna.phy.channel.tr38901 import UMi, PanelArray
from sionna.phy.mimo import StreamManagement
from sionna.sys import gen_hexgrid_topology, \
    downlink_fair_power_control, open_loop_uplink_power_control
from sionna.sys.utils import spread_across_subcarriers, get_pathloss

# Internal computational precision
sionna.phy.config.precision = 'single'  # 'single' or 'double'

# Set random seed for reproducibility
sionna.phy.config.seed = 45

Multicell scenario

We first create a multicell 3GPP-compliant scenario on which power control will be performed in both the uplink and downlink directions.

Simulation Parameters

We define the main simulation parameters, including the topology settings, OFDM resource grid, and transmission power for the base stations and user terminal.

[3]:
# Number of independent scenarios
batch_size = 1

# Number of rings of the spiral hexagonal grid
num_rings = 1

# Number of co-scheduled users per sector
num_ut_per_sector = 5

# OFDM resource grid
num_ofdm_sym = 10
num_subcarriers = 32
subcarrier_spacing = 15e3  # [Hz]
carrier_frequency = 3.5e9  # [Hz]

# Base station and user terminal transmit power
bs_max_power_dbm = 46  # [dBm]
ut_max_power_dbm = 26  # [dBm]

# Convert power to Watts
ut_max_power = dbm_to_watt(ut_max_power_dbm)  # [W]
bs_max_power = dbm_to_watt(bs_max_power_dbm)  # [W]

# Environment temperature
temperature = 294  # [K]
# Noise power per subcarrier
no = BOLTZMANN_CONSTANT * temperature * subcarrier_spacing

# Max distance between user terminal and serving base station
max_bs_ut_dist = 80  # [m]

Antenna patterns

We create the antenna patterns for base stations and user terminals.

[4]:
# Create antenna arrays
bs_array = PanelArray(num_rows_per_panel=3,
                      num_cols_per_panel=2,
                      polarization='dual',
                      polarization_type='VH',
                      antenna_pattern='38.901',
                      carrier_frequency=carrier_frequency)

ut_array = PanelArray(num_rows_per_panel=1,
                      num_cols_per_panel=1,
                      polarization='single',
                      polarization_type='V',
                      antenna_pattern='omni',
                      carrier_frequency=carrier_frequency)

num_ut_ant = ut_array.num_ant
num_bs_ant = bs_array.num_ant

Topology

Next, we position base stations on a hexagonal grid according to a 3GPP-compliant scenario and randomly distribute users uniformly in each sector.

For more details on the generation of the topology, see the Hexagonal Grid Topology notebook.

[5]:
# Generate the spiral hexagonal grid topology
topology = gen_hexgrid_topology(batch_size=batch_size,
                                num_rings=num_rings,
                                num_ut_per_sector=num_ut_per_sector,
                                max_bs_ut_dist=max_bs_ut_dist,
                                scenario='umi')

ut_loc, bs_loc, *_ = topology

# N. users and base stations
num_bs = bs_loc.shape[1]
num_ut = ut_loc.shape[1]

# In the uplink, the user is the transmitter and the base station is the receiver
num_rx, num_tx = num_bs, num_ut

We set and compute the number of streams per user and base station, respectively.

[6]:
# Set number of streams per user
num_streams_per_ut = num_ut_ant

# Number of streams per base station
num_streams_per_bs = num_streams_per_ut * num_ut_per_sector

assert num_streams_per_ut <= num_ut_ant, \
    "The # of streams per user must not exceed the # of its antennas"

Each receiver is associated with its serving base station.

[7]:
# For simplicity, each user is associated with its nearest base station

# Uplink
# RX-TX association matrix
rx_tx_association_ul = np.zeros([num_rx, num_tx])
idx_fair = np.array([[i1, i2] for i1 in range(num_rx) for i2 in
                np.arange(i1*num_ut_per_sector, (i1+1)*num_ut_per_sector)])
rx_tx_association_ul[idx_fair[:, 0], idx_fair[:, 1]] = 1

# Instantiate a Stream Management object
stream_management_ul = StreamManagement(rx_tx_association_ul, num_streams_per_ut)

# Downlink
# Receivers and transmitters are swapped wrt uplink
rx_tx_association_dl = rx_tx_association_ul.T
stream_management_dl = StreamManagement(rx_tx_association_dl, num_streams_per_bs)

We create the channel model that will be used to generate the channel impulse responses:

[8]:
# Create channel model
channel_model = UMi(carrier_frequency=carrier_frequency,
                    o2i_model='low',  # 'low' or 'high'
                    ut_array=ut_array,
                    bs_array=bs_array,
                    direction='uplink',
                    enable_pathloss=True,
                    enable_shadow_fading=True)

channel_model.set_topology(*topology)
channel_model.show_topology()
../../_images/sys_tutorials_Power_Control_16_0.png

Channel

Next, the channel frequency response is computed over the OFDM resource grid.

[9]:
# Set up the OFDM resource grid
resource_grid = ResourceGrid(num_ofdm_symbols=num_ofdm_sym,
                             fft_size=num_subcarriers,
                             subcarrier_spacing=subcarrier_spacing,
                             num_tx=num_ut_per_sector,
                             num_streams_per_tx=num_streams_per_ut)

# Instantiate the OFDM channel generator
ofdm_channel = GenerateOFDMChannel(channel_model, resource_grid)

# Generate the OFDM channel matrix in the uplink
# [batch_size, num_rx=num_bs, num_rx_ant, num_tx=num_ut, num_tx_ant, num_ofdm_symbols, num_subcarriers]
h_freq_ul = ofdm_channel(batch_size)

# [batch_size, num_rx=num_ut, num_rx_ant, num_tx=num_bs, num_tx_ant, num_ofdm_symbols, num_subcarriers]
h_freq_dl = tf.transpose(h_freq_ul, [0, 3, 4, 1, 2, 5, 6])
We conclude this section by scheduling users for transmission across the resource grid.
Note that only one slot is simulated. For more realistic simulations, a scheduler should be used. This is explained in more details in the Proportional Fairness Scheduler notebook.
[10]:
# For simplicity, all users are allocated simultaneously on all resources
is_scheduled = tf.fill([batch_size,
                        num_bs,
                        num_ofdm_sym,
                        num_subcarriers,
                        num_ut_per_sector,
                        num_streams_per_ut],
                       True)

num_allocated_subcarriers = tf.fill([batch_size,
                                     num_bs,
                                     num_ofdm_sym,
                                     num_ut_per_sector],
                                     num_subcarriers)

Conclusions

Power allocation in both uplink and downlink requires a global perspective on the network to ensure fairness across all served users.

We provide two built-in functions for this purpose:

Note that power control is closely tied to scheduling: once users are assigned to resource elements and streams, the transmission power is determined accordingly.
For more details on scheduling, refer to the Proportional Fairness Scheduler notebook. The System-Level Simulations notebook further illustrates how scheduling and power control interact.

References

[1] 3GPP TS 38.213. “NR; Physical layer procedures for control”
[2] J. Mo, J. Walrand. Fair end-to-end window-based congestion control. IEEE/ACM Transactions on networking 8.5 (2000): 556-567.