Skip to main content
country_code
Ctrl+K
Aerial CUDA-Accelerated RAN - Home Aerial CUDA-Accelerated RAN - Home

Aerial CUDA-Accelerated RAN

  • pdf
  • github
  • www
Aerial CUDA-Accelerated RAN - Home Aerial CUDA-Accelerated RAN - Home

Aerial CUDA-Accelerated RAN

  • pdf
  • github
  • www

Table of Contents

  • Supported Systems
  • Release Notes
    • Software Manifest
    • Supported Features and Configurations
    • Multicell Capacity
    • Supported Test Vector Configurations
    • Limitations
    • Acknowledgements
  • Installation Guide
    • Installing Tools on Grace Hopper MGX System
    • Installing Tools on NVIDIA DGX Spark System
    • Installing Tools on Dell R750
    • Installing and Upgrading cuBB
    • Aerial System Scripts
    • Aerial CUDA-Accelerated RAN Versioning in YAML Files
    • Troubleshooting
  • Quickstart Guide
    • Quickstart Overview
    • Generating TV and Launch Pattern Files
    • Running Aerial cuPHY
    • Running cuBB End-to-End
    • Running cuBB Performance tests
    • E2E gNodeB on MIG
  • cuBB
    • Getting Started
    • Features and Architecture
      • cuPHY Features Overview
      • Aerial CUDA-Accelerated RAN Features for 5G gNB
    • cuBB Integration Guide
      • NVIPC
        • NVIPC Overview
        • NVIPC Integration
      • FAPI Support
        • Standard FAPI Support
        • Vendor-Specific Extensions
        • FAPI Message Formats and Specifications
      • Run-time Configuration and Status
      • 64 TR MU-MIMO Support with Static and Dynamic Beamforming
    • cuPHY Developer Guide
      • Overview
      • Components
      • Test MAC and RU Emulator Architecture Overview
      • 5G MATLAB Models for Testing and Validation
      • AI/ML Components for PUSCH Channel Estimation
      • References
    • OAM
      • Configuration
      • Operation
      • Logging
      • Metrics
    • cuMAC
      • Implementation Details
      • Examples
      • cuMAC API Reference
      • cuMAC-CP integration guide
        • cuMAC-CP API Procedures
        • cuMAC-CP API Messages
        • L2 integration notes
        • cuMAC-CP Tests
    • Glossary
  • Data Lake
  • pyAerial
    • Overview
    • Getting Started with pyAerial
    • Examples of Using pyAerial
      • Using pyAerial to run a PUSCH link simulation
      • Using pyAerial for LDPC encoding-decoding chain
      • Using pyAerial to run 5G sounding reference signal transmission and reception
      • Using pyAerial to run CSI-RS transmission and reception
      • Using pyAerial for data generation by simulation
      • LLRNet: Dataset generation
      • LLRNet: Model training and testing
      • Using pyAerial to evaluate a PUSCH neural receiver
      • Channel Estimation for Uplink Shared Channel (PUSCH) in pyAerial
      • Using pyAerial for channel estimation on Aerial Data Lake data
      • Using pyAerial for PUSCH decoding on Aerial Data Lake data
      • Using pyAerial for decoding PUSCH transmissions from multiple cells using Aerial Data Lake data
    • API Reference
      • Physical layer for 5G
        • Receiver algorithms
        • Configuration classes
        • PDSCH
        • PUSCH
        • LDPC 5G
        • CSI reference signals (CSI-RS)
        • Sounding reference signals (SRS)
        • Fading channel
        • Pipeline API definitions
      • Utilities
  • pyAerial
  • Examples of Using pyAerial
  • Using pyAerial to run a PUSCH link simulation
Is this page helpful?

Using pyAerial to run a PUSCH link simulation#

This example shows how to use the pyAerial cuPHY Python bindings to run a PUSCH link simulation. PUSCH transmitter is emulated by PDSCH transmission with properly chosen parameters, that way making it a 5G NR compliant PUSCH transmission. Building a PUSCH receiver using pyAerial is demonstrated in two ways, first by using a fully fused, complete, PUSCH receiver called from Python using just a single function call. The same is then achieved by building the complete PUSCH receiver using individual separate Python function calls to individual PUSCH receiver components. We use the pyAerial implementation of 3GPP TDL/CDL channel models for simulating the radio channel.

Imports#

[1]:
%matplotlib widget
from collections import defaultdict
import os
import time
os.environ["CUDA_VISIBLE_DEVICES"] = "0"

import numpy as np
import cupy as cp

from aerial.phy5g.pdsch import PdschTxPipelineFactory
from aerial.phy5g.pusch import PuschRxPipelineFactory
from aerial.phy5g.pusch import SeparablePuschRxPipelineFactory
from aerial.phy5g.config import AerialPuschRxConfig
from aerial.phy5g.config import AerialPdschTxConfig
from aerial.phy5g.config import PdschConfig
from aerial.phy5g.config import PdschUeConfig
from aerial.phy5g.config import PdschCwConfig
from aerial.phy5g.config import PuschConfig
from aerial.phy5g.config import PuschUeConfig
from aerial.phy5g.ldpc import get_mcs
from aerial.phy5g.ldpc import get_tb_size
from aerial.phy5g.ldpc import random_tb
from aerial.util.cuda import CudaStream
from aerial.pycuphy.types import PuschLdpcKernelLaunch
from aerial.phy5g.channel_models import FadingChannel
from aerial.phy5g.channel_models import TdlChannelConfig
from aerial.phy5g.channel_models import CdlChannelConfig
from simulation_monitor import SimulationMonitor

Parameters#

Set simulation parameters, numerology, PUSCH parameters and channel parameters here.

[2]:
# Simulation parameters.
esno_db_range = np.arange(-7.5, 2.5, 1.0)
num_slots = 10000
min_num_tb_errors = 250

# Numerology and frame structure. See TS 38.211.
num_ofdm_symbols = 14
fft_size = 4096
num_guard_subcarriers = (410, 410)
num_slots_per_frame = 20

# System/gNB configuration
num_tx_ant = 1             # UE antennas
num_rx_ant = 2             # gNB antennas
cell_id = 41               # Physical cell ID
enable_pusch_tdi = 1       # Enable time interpolation for equalizer coefficients
eq_coeff_algo = 1          # Equalizer algorithm

# PUSCH configuration
rnti = 1234                # UE RNTI
scid = 0                   # DMRS scrambling ID
data_scid = 0              # Data scrambling ID
layers = 1                 # Number of layers
mcs_index = 2              # MCS index as per TS 38.214 table. Note: Es/No range may need to be changed too to get meaningful results.
mcs_table = 0              # MCS table index
dmrs_ports = 1             # Used DMRS port.
start_prb = 0              # Start PRB index.
num_prbs = 273             # Number of allocated PRBs.
start_sym = 2              # Start symbol index.
num_symbols = 12           # Number of symbols.
dmrs_scrm_id = 41          # DMRS scrambling ID
dmrs_syms = [0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]  # Indicates which symbols are used for DMRS.
dmrs_max_len = 1
dmrs_add_ln_pos = 0
num_dmrs_cdm_grps_no_data = 2
precoding_matrix = None
mod_order, code_rate = get_mcs(mcs_index, mcs_table+1)  # Different indexing for MCS table.
tb_size = get_tb_size(  # TB size in bits
    mod_order=mod_order,
    code_rate=code_rate,
    dmrs_syms=dmrs_syms,
    num_prbs=num_prbs,
    start_sym=start_sym,
    num_symbols=num_symbols,
    num_layers=layers)

# Channel parameters
carrier_frequency = 3.5e9  # Carrier frequency in Hz.
channel_type = "tdl"       # Channel type: "tdl" or "cdl"
delay_profile = "A"        # Delay profile: 'A', 'B', 'C', 'D', 'E' as per 3GPP TR 38.901
delay_spread = 30.0        # RMS delay spread in nanoseconds.
speed = 0.8333             # UE speed [m/s]. Used to calculate Doppler shift.

Create the pipelines#

As mentioned, PUSCH transmission is emulated here by the PDSCH transmission chain. Note that the static cell parameters and static PUSCH parameters are given upon creating the PUSCH transmission/reception objects. Dynamically (per slot) changing parameters are however set when actually running the transmission/reception, see further below.

[3]:
cuda_stream = CudaStream()

# PDSCH configuration objects. PDSCH transmitter emulates PUSCH transmission here.
# Note that default values are used for some parameters not given here.
pdsch_cw_config = PdschCwConfig(
    mcs_table=mcs_table,
    mcs_index=mcs_index,
    code_rate=int(code_rate * 10),
    mod_order=mod_order
)
pdsch_ue_config = PdschUeConfig(
    cw_configs=[pdsch_cw_config],
    scid=scid,
    dmrs_scrm_id=dmrs_scrm_id,
    layers=layers,
    dmrs_ports=dmrs_ports,
    rnti=rnti,
    data_scid=data_scid,
    precoding_matrix=precoding_matrix
)
pdsch_config = PdschConfig(
    ue_configs=[pdsch_ue_config],
    num_dmrs_cdm_grps_no_data=num_dmrs_cdm_grps_no_data,

    start_prb=start_prb,
    num_prbs=num_prbs,
    dmrs_syms=dmrs_syms,
    start_sym=start_sym,
    num_symbols=num_symbols
)

pdsch_tx_config = AerialPdschTxConfig(
    cell_id=cell_id,
    num_tx_ant=num_tx_ant
)

# Transmitter pipeline.
tx_pipeline = PdschTxPipelineFactory().create(pdsch_tx_config, cuda_stream)

# PUSCH configuration objects. Note that default values are used for some parameters
# not given here.
pusch_ue_config = PuschUeConfig(
    scid=scid,
    layers=layers,
    dmrs_ports=dmrs_ports,
    rnti=rnti,
    data_scid=data_scid,
    mcs_table=mcs_table,
    mcs_index=mcs_index,
    code_rate=int(code_rate * 10),
    mod_order=mod_order,
    tb_size=tb_size // 8
)
# Note that this is a list. One UE group only in this case.
pusch_configs = [PuschConfig(
    ue_configs=[pusch_ue_config],
    num_dmrs_cdm_grps_no_data=num_dmrs_cdm_grps_no_data,
    dmrs_scrm_id=dmrs_scrm_id,
    start_prb=start_prb,
    num_prbs=num_prbs,
    dmrs_syms=dmrs_syms,
    dmrs_max_len=dmrs_max_len,
    dmrs_add_ln_pos=dmrs_add_ln_pos,
    start_sym=start_sym,
    num_symbols=num_symbols
)]

pusch_rx_config = AerialPuschRxConfig(
    cell_id=cell_id,
    num_rx_ant=num_rx_ant,
    enable_pusch_tdi=enable_pusch_tdi,
    eq_coeff_algo=eq_coeff_algo,
    ldpc_kernel_launch=PuschLdpcKernelLaunch.PUSCH_RX_LDPC_STREAM_SEQUENTIAL
)

cases = {
    "Fused": PuschRxPipelineFactory,
    "Separable": SeparablePuschRxPipelineFactory
}

pipelines = {}
for name, factory in cases.items():
    pipelines[name] = factory().create(pusch_rx_config, cuda_stream)

Channel model setup#

The pyAerial FadingChannel class provides GPU-accelerated TDL (Tapped Delay Line) and CDL (Clustered Delay Line) channel models based on 3GPP TR 38.901. The channel operates in frequency domain and includes built-in OFDM modulation/demodulation and AWGN noise addition.

The channel accepts both CuPy and NumPy arrays, enabling seamless integration with the rest of the pyAerial pipeline.

[4]:
# Calculate Doppler shift from speed and carrier frequency.
max_doppler_shift = speed * carrier_frequency / 3e8

# Create channel configuration based on channel type.
# Note: n_bs_ant = gNB antennas, n_ue_ant = UE antennas
# With enable_swap_tx_rx=True, TX=UE, RX=gNB (uplink)
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_rx_ant,  # gNB antennas (receiver in uplink)
        n_ue_ant=num_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_rx_ant // 2, 2),  # gNB antenna array
        ue_ant_size=(1, num_tx_ant, 1)        # UE antenna array
    )
else:
    raise ValueError(f"Invalid channel type: {channel_type}. Use 'tdl' or 'cdl'.")

# Create FadingChannel with OFDM parameters.
n_sc = fft_size - sum(num_guard_subcarriers)
channel = FadingChannel(
    channel_config=channel_config,
    n_sc=n_sc,
    numerology=1,  # 30 kHz subcarrier spacing
    n_fft=fft_size,
    n_symbol_slot=num_ofdm_symbols,
    cuda_stream=cuda_stream
)


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:
        Received signal after channel and noise, 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
    )

    # Reshape back to (n_sc, n_symbol, n_rx_ant)
    return rx_signal[0, 0].transpose((2, 1, 0))

Run the actual simulation#

Here we loop across the Es/No range, and simulate a number of slots for each Es/No value. A single transport block is simulated within a slot. The simulation starts over from the next Es/No value when a minimum number of transport block errors is reached.

[5]:
monitor = SimulationMonitor(cases, esno_db_range)
exec_times = dict.fromkeys(cases, 0)

# Loop the Es/No range.
for esno_db in esno_db_range:
    monitor.step(esno_db)
    num_tb_errors = defaultdict(int)

    # Run multiple slots and compute BLER.
    # We reset the channel for every slot to simulate different channel realizations.
    for slot_idx in range(num_slots):
        channel.reset()

        slot_number = slot_idx % num_slots_per_frame

        # Get random transport block.
        tb_input_np = random_tb(
            mod_order=mod_order,
            code_rate=code_rate,
            dmrs_syms=dmrs_syms,
            num_prbs=num_prbs,
            start_sym=start_sym,
            num_symbols=num_symbols,
            num_layers=layers)

        tb_input = cp.array(tb_input_np, dtype=cp.uint8, order='F')

        # Transmit PUSCH. This is where we set the dynamically changing parameters.
        # Input parameters are given as lists as the interface supports multiple UEs.
        tx_tensor = tx_pipeline(
            slot=slot_number,
            tb_inputs=[tb_input],
            config=[pdsch_config]
        )

        # Apply fading channel with AWGN noise.
        # FadingChannel handles both CuPy and NumPy arrays transparently.
        rx_tensor = apply_channel(tx_tensor, esno_db, slot_idx)

        # Run the PUSCH receiver pipelines.
        # Note that this is where we set the dynamically changing parameters.
        for name, pipeline in pipelines.items():
            start_time = time.time()
            tb_crcs, tbs = pipeline(
                slot=slot_number,
                rx_slot=rx_tensor,
                config=pusch_configs
            )
            exec_times[name] += time.time() - start_time
            num_tb_errors[name] += int(np.array_equal(tbs[0], tb_input_np) == False)

        monitor.update(num_tbs=slot_idx + 1, num_tb_errors=num_tb_errors)
        if (np.array(list(num_tb_errors.values())) >= min_num_tb_errors).all():
            break  # Next Es/No value.

    monitor.finish_step(num_tbs=slot_idx + 1, num_tb_errors=num_tb_errors)
monitor.finish()
                            Fused              Separable
                     -------------------- --------------------
  Es/No (dB)     TBs    TB Errors    BLER    TB Errors    BLER    ms/TB
==================== ==================== ==================== ========
       -7.50     264          250  0.9470          250  0.9470      5.5
       -6.50     291          250  0.8591          250  0.8591      4.6
       -5.50     350          250  0.7143          250  0.7143      4.6
       -4.50     516          250  0.4845          250  0.4845      4.6
       -3.50     981          251  0.2559          250  0.2548      4.5
       -2.50    2179          250  0.1147          251  0.1152      4.6
       -1.50    5409          254  0.0470          250  0.0462      4.6
       -0.50   10000           94  0.0094           95  0.0095      4.5
        0.50   10000           21  0.0021           21  0.0021      4.5
        1.50   10000            4  0.0004            4  0.0004      4.6
[6]:
exec_times = {k : v / (num_slots * len(esno_db_range)) for k, v in exec_times.items()}
print("Average execution times:")
for name, exec_time in exec_times.items():
    print(f"{name}: {exec_time * 1000: .2f} ms.")
Average execution times:
Fused:  0.31 ms.
Separable:  0.92 ms.

previous

Examples of Using pyAerial

next

Using pyAerial for LDPC encoding-decoding chain

On this page
  • Imports
  • Parameters
  • Create the pipelines
  • Channel model setup
  • Run the actual simulation
NVIDIA NVIDIA
Privacy Policy | Your Privacy Choices | Terms of Service | Accessibility | Corporate Policies | Product Security | Contact

Copyright © 2026, NVIDIA Corporation.

Last updated on Apr 13, 2026.