Using pyAerial for LDPC encoding-decoding chain#

This example shows how to use the pyAerial Python bindings to run 5G NR LDPC encoding, rate matching and decoding. Information bits, i.e. a transport block, get segmented into code blocks, LDPC encoded and rate matched onto the available time-frequency resources (resource elements), all following TS 38.212 precisely. The bits are then transmitted over an AWGN channel using QAM modulation. At the receiver side, log likelihood ratios are extracted from the received symbols using soft demapping, (de)rate matching is performed and LDPC decoder is run to get the transmitted information bits. Finally, the code blocks are concatenated back into a received transport block.

pyAerial utilizes the cuPHY library underneath for all components. In this example, CRCs are just random blocks of bits so we can compare the transmitted and received bits directly to compute block error rates.

Imports#

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

import numpy as np
import cupy as cp

from aerial.phy5g.ldpc import LdpcEncoder
from aerial.phy5g.ldpc import LdpcDecoder
from aerial.phy5g.ldpc import LdpcRateMatch
from aerial.phy5g.ldpc import LdpcDeRateMatch
from aerial.phy5g.ldpc import CrcEncoder
from aerial.phy5g.ldpc import CrcChecker
from aerial.phy5g.ldpc import get_mcs
from aerial.phy5g.ldpc import random_tb
from aerial.phy5g.algorithms import ModulationMapper
from aerial.util.cuda import CudaStream
from aerial.util.visualization import plot_constellation
from simulation_monitor import SimulationMonitor

Parameters#

Set simulation parameters, some numerology parameters, enable/disable scrambling etc.

[2]:
# Simulation parameters.
esno_db_range = np.arange(8.1, 8.8, 0.1)
num_slots = 10000
min_num_tb_errors = 250

# Numerology and frame structure. See TS 38.211.
num_prb = 100              # Number of allocated PRBs. This is used to compute the transport block
                           # as well as the rate matching length.
start_sym = 0              # PxSCH start symbol
num_symbols = 14           # Number of symbols in a slot.
num_layers = 1
dmrs_sym = [0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0]

# Rate matching procedure includes scrambling if this flag is set.
enable_scrambling = True

# The scrambling initialization value is computed as per TS 38.211
# using the RNTI and data scrambling ID.
rnti = 20000               # UE RNTI
data_scid = 41             # Data scrambling ID
cinit = (rnti << 15) + data_scid

rv = 0                     # Redundancy version
mcs = 9                    # MCS index as per TS 38.214 table. Note: Es/No range may need to be changed too to get meaningful results.

mod_order, code_rate = get_mcs(mcs)
code_rate /= 1024.

Create the LDPC coding chain objects#

The LDPC coding chain objects are created here. This includes the following:

  • CrcEncoder which takes the information bits, i.e. the transport block, attaches a transport block CRC into it, segments the TB into code blocks and adds code block CRCs.

  • LdpcEncoder which takes the code blocks from CrcEncoder as its input, and outputs LDPC encoded code blocks.

  • LdpcRateMatch which takes encoded code blocks as its input and outputs a rate matched (and optionally scrambled) stream of bits.

  • LdpcDerateMatch which takes the received stream of log-likelihood ratios (LLRs) as its input and outputs derate matched code blocks of LLRs which can be fed to the LDPC decoding. This block performs also descrambling if scrambling is enabled in the pipeline.

  • LdpcDecoder which takes the output of LDPC derate matching and decodes the LLRs into code blocks that can then be further concatenated into a received transport block.

  • CrcChecker which takes the output of the LDPC decoding block, checks and removes code block CRCs, concatenates code blocks into a full transport block, and finally checks and removed the transport block CRC.

  • ModulationMapper which handles QAM modulation (mapping bits to symbols) and soft demapping (computing LLRs from received symbols) following 3GPP TS 38.211 Gray-coded constellations.

All components are based on TS 38.211/38.212 and thus can be used for transmitting/receiving 5G NR compliant bit streams.

[3]:
# Create also the CUDA stream that running the objects requires.
cuda_stream = CudaStream()

# Create the Aerial Python LDPC objects.
crc_encoder = CrcEncoder(cuda_stream=cuda_stream)
ldpc_encoder = LdpcEncoder(cuda_stream=cuda_stream)
ldpc_decoder = LdpcDecoder(cuda_stream=cuda_stream)
ldpc_rate_match = LdpcRateMatch(enable_scrambling=enable_scrambling, cuda_stream=cuda_stream)
ldpc_derate_match = LdpcDeRateMatch(enable_scrambling=enable_scrambling, cuda_stream=cuda_stream)
crc_checker = CrcChecker(cuda_stream=cuda_stream)

# Create the modulation mapper (handles both mapping and demapping).
mapper = ModulationMapper(mod_order=mod_order)


def add_awgn(signal: cp.ndarray, noise_var: float) -> cp.ndarray:
    """Add AWGN noise to signal in-place.

    Args:
        signal: Complex signal array (CuPy). Modified in-place.
        noise_var: Noise variance (linear scale).

    Returns:
        The input signal with noise added (same array, modified in-place).
    """
    # Generate noise matching signal dtype and add in-place to avoid extra allocation
    std = cp.sqrt(cp.float32(noise_var / 2))
    noise_real = cp.random.randn(*signal.shape, dtype=cp.float32)
    noise_imag = cp.random.randn(*signal.shape, dtype=cp.float32)
    signal += std * (noise_real + 1j * noise_imag)
    return signal

[4]:
# Plot the constellation diagram with bit labels.
fig, ax = plot_constellation(mapper.constellation, mod_order, figsize=(6, 6))
fig.tight_layout()

Main simulation loop#

[5]:
case = "LDPC decoding perf."
monitor = SimulationMonitor([case], esno_db_range)

# 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.
    for slot_idx in range(num_slots):

        # Generate a random transport block (in bits) and move to GPU.
        transport_block = cp.asarray(random_tb(
            mod_order=mod_order,
            code_rate=code_rate * 1024,
            dmrs_syms=dmrs_sym,
            num_prbs=num_prb,
            start_sym=start_sym,
            num_symbols=num_symbols,
            num_layers=num_layers,
            return_bits=False
        ))
        tb_size = transport_block.shape[0] * 8

        # Run transport block CRC encoding, code block segmentation and code block CRC encoding.
        code_blocks = crc_encoder.encode(
            tb_inputs=[transport_block],
            tb_sizes=[tb_size],
            code_rates=[code_rate]
        )

        # Run the LDPC encoding. The LDPC encoder takes a K x C array as its input, where K is the number of bits per code
        # block and C is the number of code blocks. Its output is N x C where N is the number of coded bits per code block.
        # If there is more than one code block, a code block CRC (random in this case as we do not need an actual CRC) is
        # attached to
        coded_bits = ldpc_encoder.encode(
            code_blocks=code_blocks,
            tb_sizes=[tb_size],
            code_rates=[code_rate],
            redundancy_versions=[rv]
        )

        # Run rate matching. This needs rate matching length as its input, meaning the number of bits that can be
        # transmitted within the allocated resource elements. The input data is fed as 32-bit floats.
        num_data_sym = (np.array(dmrs_sym[start_sym:start_sym + num_symbols]) == 0).sum()
        rate_match_len = num_data_sym * num_prb * 12 * num_layers * mod_order
        rate_matched_bits = ldpc_rate_match.rate_match(
            coded_blocks=coded_bits,
            tb_sizes=[tb_size],
            code_rates=[code_rate],
            rate_match_lens=[rate_match_len],
            mod_orders=[mod_order],
            num_layers=[num_layers],
            redundancy_versions=[rv],
            cinits=[cinit]
        )[0]

        # Map the bits to symbols and transmit through an AWGN channel.
        noise_var = 10 ** (-esno_db / 10)
        tx_symbols = mapper.map(rate_matched_bits)
        rx_symbols = add_awgn(tx_symbols, noise_var)
        # Demapper returns shape (mod_order, num_symbols), reshape to (num_bits, 1)
        llr = mapper.demap(rx_symbols, 1.0 / noise_var)
        llr = llr.T.flatten()[:, None]

        # Run receiver side (de)rate matching. The input is the received array of bits directly, and the output
        # is a NumPy array of size N x C of log likelihood ratios, represented as 32-bit floats. Descrambling
        # is also performed here in case scrambling is enabled.
        derate_matched_bits = ldpc_derate_match.derate_match(
            input_llrs=[llr],
            tb_sizes=[tb_size],
            code_rates=[code_rate],
            rate_match_lengths=[rate_match_len],
            mod_orders=[mod_order],
            num_layers=[num_layers],
            redundancy_versions=[rv],
            ndis=[1],
            cinits=[cinit]
        )

        # Run LDPC decoding. The decoder takes the derate matching output as its input and returns
        decoded_bits = ldpc_decoder.decode(
            input_llrs=derate_matched_bits,
            tb_sizes=[tb_size],
            code_rates=[code_rate],
            redundancy_versions=[rv],
            rate_match_lengths=[rate_match_len]
        )

        # Combine code blocks into a transport block. CRC ignored as it was just random bits in this example.
        decoded_tb, _ = crc_checker.check_crc(
            input_bits=decoded_bits,
            tb_sizes=[tb_size],
            code_rates=[code_rate]
        )

        tb_error = not np.array_equal(decoded_tb[0], transport_block)
        num_tb_errors[case] += tb_error
        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()
                     LDPC decoding perf.
                     --------------------
  Es/No (dB)     TBs    TB Errors    BLER    ms/TB
==================== ==================== ========
        8.10     250          250  1.0000     34.5
        8.20     252          250  0.9921      3.5
        8.30     320          250  0.7812      3.5
        8.40     762          250  0.3281      3.5
        8.50    3803          250  0.0657      3.5
        8.60   10000           71  0.0071      3.5
        8.70   10000            1  0.0001      3.5
        8.80   10000            0  0.0000      3.4