Using pyAerial to run 5G sounding reference signal transmission and reception#
This example shows how to use the pyAerial cuPHY Python bindings to run sounding reference signal (SRS) transmission and reception using the pyAerial SRS transmitter and receiver pipelines.
The pyAerial GPU-accelerated channel models (TDL/CDL from 3GPP TR 38.901) are used to simulate the radio channel.
Imports#
[1]:
%matplotlib widget
import os
os.environ["CUDA_VISIBLE_DEVICES"] = "0"
import numpy as np
import matplotlib.pyplot as plt
from aerial.phy5g.srs import SrsTx
from aerial.phy5g.srs import SrsRx
from aerial.phy5g.srs import SrsTxConfig
from aerial.phy5g.srs import SrsRxConfig
from aerial.phy5g.srs import SrsConfig
from aerial.phy5g.srs import SrsRxUeConfig
from aerial.phy5g.srs import SrsRxCellConfig
from aerial.phy5g.channel_models import FadingChannel
from aerial.phy5g.channel_models import TdlChannelConfig
from aerial.phy5g.channel_models import CdlChannelConfig
from aerial.util.cuda import CudaStream
Simulation parameters#
[2]:
esno_db = 40
# Numerology and frame structure. See TS 38.211.
num_symb_per_slot = 14
fft_size = 4096
num_guard_subcarriers = (410, 410)
num_slots_per_frame = 20
srs_symbols = [13]
# Channel parameters
num_ue_tx_ant = 1 # Number of UE TX antennas
num_gnb_rx_ant = 4 # Number of gNB RX antennas
carrier_frequency = 3.5e9 # Carrier frequency in Hz.
channel_type = "cdl" # Channel type: "tdl" or "cdl"
delay_profile = "C" # Delay profile: 'A', 'B', 'C' (NLOS). Note: 'D', 'E' (LOS) not yet supported.
delay_spread = 100.0 # RMS delay spread in nanoseconds.
speed = 0.8333 # UE speed [m/s]. Used to calculate Doppler shift.
Build the pipelines#
We build the SRS transmitter and receiver pipeline objects here.
[3]:
# Generate a CUDA stream for running the whole thing.
cuda_stream = CudaStream()
srs_tx = SrsTx(
num_max_srs_ues=1, # Maximum number of UEs for which this object will handle SRS Tx. Here just one.
num_slot_per_frame=num_slots_per_frame,
num_symb_per_slot=num_symb_per_slot,
cuda_stream=cuda_stream
)
srs_rx = SrsRx(
num_rx_ant=[num_gnb_rx_ant], # A list, one element per cell.
chest_algo_idx=0, # MMSE.
enable_delay_offset_correction=1,
chest_params=None, # Use defaults.
num_max_srs_ues=1, # Maximum number of UEs for which this object will handle SRS Rx.
cuda_stream=cuda_stream
)
Sounding reference signal and SRS Tx and Rx pipeline slot configurations#
Define the SRS signal configuration for the slot, as well as the slot configurations for the SRS Tx and Rx pipelines built above. These are the dynamic configurations that depend on slot and frame index, and other parameters.
The SRS signal parameters follow essentially the 3GPP specifications, 3GPP TS 38.211 in particular.
[4]:
# Slot and frame indices.
frame = 0
slot = 0
# SRS signal configuration. Just one UE.
srs_config = SrsConfig(
num_ant_ports=1,
num_syms=len(srs_symbols),
num_repetitions=1,
comb_size=2,
start_sym=srs_symbols[0],
sequence_id=0,
config_idx=63,
bandwidth_idx=0,
comb_offset=0,
cyclic_shift=0,
frequency_position=0,
frequency_shift=0,
frequency_hopping=0,
resource_type=0,
periodicity=1,
offset=0,
group_or_sequence_hopping=0
)
# UE SRS transmitter pipeline slot configuration. One UE, one SRS signal configuration.
srs_tx_config = SrsTxConfig(
slot=slot,
frame=frame,
srs_configs=[srs_config]
)
# gNB SRS receiver pipeline slot configuration
# - UEs from which SRS are received
srs_rx_ue_config = SrsRxUeConfig(
cell_idx=0, # Cell association.
srs_config=srs_config, # SRS signal configuration.
srs_ant_port_to_ue_ant_map=np.zeros(1, dtype=np.uint8), # Mapping to UE antennas.
prg_size=2, # PRB group size.
start_prg=0, # Start PRB group.
num_prgs=136 # 273 // prg_size.
)
# - Cells handled by this pipeline.
srs_rx_cell_config = SrsRxCellConfig(
slot=slot,
frame=frame,
srs_start_sym=srs_symbols[0],
num_srs_sym=len(srs_symbols)
)
# - The actual slot configuration.
srs_rx_config = SrsRxConfig(
srs_cell_configs=[srs_rx_cell_config],
srs_ue_configs=[srs_rx_ue_config]
)
Channel modeling#
The radio channel is simulated using the pyAerial GPU-accelerated FadingChannel class from aerial.phy5g.channel_models. It supports both TDL (Tapped Delay Line) and CDL (Clustered Delay Line) channel models as defined in 3GPP TR 38.901. The channel operates in the frequency domain: it applies the fading channel to the transmitted signal and adds AWGN noise at the specified SNR. We configure the channel for only the SRS symbol(s), since those are the only symbols being simulated here.
[5]:
# Calculate Doppler shift from speed and carrier frequency.
max_doppler_shift = speed * carrier_frequency / 3e8
# Create channel configuration.
# Note: This is uplink (UE TX, gNB RX), so use enable_swap_tx_rx=True.
# n_bs_ant = gNB antennas (receiver), n_ue_ant = UE antennas (transmitter)
n_sc = fft_size - sum(num_guard_subcarriers)
if channel_type == "tdl":
channel_config = TdlChannelConfig(
delay_profile=delay_profile,
delay_spread=delay_spread,
max_doppler_shift=max_doppler_shift,
n_bs_ant=num_gnb_rx_ant, # gNB antennas (receiver in uplink)
n_ue_ant=num_ue_tx_ant # UE antennas (transmitter in uplink)
)
elif channel_type == "cdl":
channel_config = CdlChannelConfig(
delay_profile=delay_profile,
delay_spread=delay_spread,
max_doppler_shift=max_doppler_shift,
bs_ant_size=(1, num_gnb_rx_ant // 2, 2), # gNB antenna array (dual-pol)
ue_ant_size=(1, num_ue_tx_ant, 1) # UE antenna array (single-pol)
)
else:
raise ValueError(f"Invalid channel type: {channel_type}. Use 'tdl' or 'cdl'.")
# Create FadingChannel with OFDM parameters.
# Note: Configure for just the SRS symbols (not full slot) to match what we simulate.
channel = FadingChannel(
channel_config=channel_config,
n_sc=n_sc,
numerology=1, # 30 kHz subcarrier spacing
n_fft=fft_size,
n_symbol_slot=len(srs_symbols) # Only simulate SRS symbol(s)
)
def apply_channel(tx_signal, snr_db, slot_idx):
"""Apply fading channel with AWGN noise.
Args:
tx_signal: Transmitted signal, shape (n_sc, n_symbol, n_tx_ant).
snr_db: Signal-to-noise ratio in dB.
slot_idx: Slot index for time-varying channel.
Returns:
Tuple of (rx_signal, channel_freq_response):
- rx_signal: Received signal after channel and noise, shape (n_sc, n_symbol, n_rx_ant).
- channel_freq_response: CFR, shape (n_sc, n_symbol, n_rx_ant).
"""
# Reshape for FadingChannel: (n_cell, n_ue, n_tx_ant, n_symbol, n_sc)
tx_reshaped = tx_signal.transpose((2, 1, 0))[None, None, ...]
# Run channel (uplink: swap tx/rx to apply channel in uplink direction)
rx_signal = channel(
freq_in=tx_reshaped,
tti_idx=slot_idx,
snr_db=snr_db,
enable_swap_tx_rx=True
)
# Get channel frequency response for comparison
# Shape: (n_cell, n_ue, n_rx_ant, n_tx_ant, n_symbol, n_sc)
cfr = channel.get_channel_frequency_response()
# Reshape back to expected shapes
rx_out = rx_signal[0, 0].transpose((2, 1, 0)) # (n_sc, n_symbol, n_rx_ant)
# For uplink CFR with swap, shape is (n_rx_ant, n_tx_ant, n_symbol, n_sc) after remove batch dims
# We want (n_sc, n_symbol, n_rx_ant) for single TX antenna
cfr_out = cfr[0, 0, :, 0, :, :].transpose((2, 0, 1)) # (n_sc, n_symbol, n_rx_ant)
return rx_out, cfr_out
Run the SRS transmission and reception#
We run the SRS transmitter, using the transmitter configuration as an argument. Then the generated SRS signal gets transmitted through the radio channel. The received tensor is fed into the SRS receiver pipeline.
[6]:
# Take the Tx buffer of cell #0.
# The output remains in GPU memory in this case.
tx_tensor = srs_tx(srs_tx_config)[0]
tx_tensor = tx_tensor[:, srs_symbols, :]
# Apply fading channel with AWGN noise.
# Note: tx_tensor already contains only SRS symbols, and channel is configured for len(srs_symbols).
rx_tensor, cfr = apply_channel(tx_tensor, esno_db, slot_idx=0)
# Convert CFR to NumPy for plotting
if hasattr(cfr, 'get'):
cfr = cfr.get()
else:
cfr = np.array(cfr)
# Run the receiver pipeline.
srs_report = srs_rx([rx_tensor], srs_rx_config)
Plot results#
Plot the sounding results for each PRB group, along with the actual channel realization.
[7]:
subc_idx = 6 + np.arange(0, 272 * 12, 2 * 12)
for rx_ant in range(num_gnb_rx_ant):
fig, axs = plt.subplots(2, figsize=(10, 10))
fig.suptitle(f"SRS channel estimates for Rx antenna {rx_ant}")
axs[0].plot(np.real(srs_report[0].ch_est[:, rx_ant, 0]), 'bo', label='MMSE')
axs[0].plot(np.real(cfr[subc_idx, 0, rx_ant]), 'k:', label='Channel')
axs[1].plot(np.imag(srs_report[0].ch_est[:, rx_ant, 0]), 'bo', label='MMSE')
axs[1].plot(np.imag(cfr[subc_idx, 0, rx_ant]), 'k:', label='Channel')
axs[0].set_title("Real part")
axs[1].set_title("Imaginary part")
for ax in axs:
ax.grid(True)
ax.set_xlim(0, 136)
ax.set_xlabel('PRB group index')
ax.legend()
axs[0].grid(True)
axs[1].grid(True)
plt.show()