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 \(\alpha\in[0;1]\) while targeting a received power of \(P_0\) [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 Sionna
try:
    import sionna.sys
except ImportError as e:
    import os
    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

import torch
[2]:
# Additional external libraries
import matplotlib.pyplot as plt
import numpy as np

# Sionna components
from sionna.phy.utils import 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()
../../../../build/doctrees/nbsphinx/sys_tutorials_notebooks_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 = torch.permute(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 = torch.full([batch_size,
                        num_bs,
                        num_ofdm_sym,
                        num_subcarriers,
                        num_ut_per_sector,
                        num_streams_per_ut],
                       True)

num_allocated_subcarriers = torch.full([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.