Effect of Antenna Bearing Angles on PDSCH BER over CDL Channels

This notebook studies the impact of transmit and receive antenna bearing angles on the bit error rate (BER) of a 5G NR PDSCH link-level simulation.

For each CDL channel profile, the simulation sweeps either the TX or RX bearing angle while keeping the other side fixed. The BER results are compared across multiple random seeds to illustrate how antenna orientation and CDL channel conditions affect PDSCH performance.

[1]:
import numpy as np
import time
import matplotlib.pyplot as plt

from neoradium import CdlChannel, Carrier, PDSCH, AntennaPanel, AntennaElement, Grid, random
[2]:
# A multipurpose function for calculating BER with different configurations
def getBitrateInfo(numSlots, snrDbs, pdsc, cdlProfile, txAlphas, rxAlphas, seed, rxOmni=False, quiet=False):
    results = {}                # Dictionary for storing the results
    for snrDb in snrDbs:
        random.setSeed(seed)    # Make the results reproducible
        t0 = time.monotonic()
        results[snrDb]={}

        for txAlpha in txAlphas:
            results[snrDb][txAlpha] = {}
            for rxAlpha in rxAlphas:

                # Create a CDL channel model
                rxAntenna = AntennaElement(beamWidth=[60,360]) if rxOmni else AntennaPanel([1,2])
                channel = CdlChannel(bwp, cdlProfile, delaySpread=300, carrierFreq=4e9, dopplerShift=5,
                                     txAntenna = AntennaPanel([2,4]),       # 8 TX antenna elements
                                     txOrientation = [txAlpha,0,0],
                                     rxAntenna = rxAntenna,
                                     rxOrientation = [rxAlpha,0,0],
                                     seed = seed)
                bitErrors = 0
                totalBits = 0

                # Use the same channel for numSlots transmissions with different data bits
                for slotNo in range(numSlots):
                    pdsch.initGrid()                             # Create and initialize the internal PDSCH grid
                    numBits = pdsch.getBitCapacity()[0]          # Number of bits available in the PDSCH grid
                    txBits = random.bits(numBits)                # Generate random binary data
                    pdsch.setPdschData(txBits)                   # Map the bitstream to the resource elements

                    channelMatrix = channel.getChannelMatrix()              # Get the channel matrix
                    precoder = pdsch.getPrecodingMatrix(channelMatrix)      # Get the precoding matrix

                    txGrid = bwp.createGrid(channel.nrNt[1])                # Create the transmit resource grid
                    pdsch.precodeTo(txGrid, precoder)                       # Precode the PDSCH into txGrid

                    rxGrid = txGrid.applyChannel(channelMatrix)             # Apply the channel in the frequency domain
                    rxGrid = rxGrid.addNoise(snrDb=snrDb)                   # Add noise

                    effChanMat = channelMatrix @ precoder[None,...]         # Effective channel matrix

                    eqGrid, llrScales = pdsch.equalize(rxGrid, effChanMat)  # Equalization
                    rxBits = pdsch.getHardBits(eqGrid)[0]                   # Demodulation
                    bitErrors += np.abs(rxBits-txBits).sum()                # Calculate the number of bit errors
                    totalBits += numBits

                dt = time.monotonic()-t0                                    # Total runtime for this SNR
                results[snrDb][txAlpha][rxAlpha] = {"totalBits":totalBits,
                                                    "bitErrors":bitErrors,
                                                    "BER":      bitErrors*100/totalBits,
                                                    "Time":     dt}

    return results

Different TX Bearing Angles

[3]:
carrier = Carrier(numRbs=24, spacing=30)    # Carrier with 24 resource blocks and 30 kHz subcarrier spacing
bwp = carrier.curBwp                        # The only bandwidth part in the carrier

pdsch = PDSCH(bwp, numLayers=2, modulation="16QAM") # Create a PDSCH object
pdsch.setDMRS(configType=2, additionalPos=2)        # DMRS configuration

txAlphas=range(-180,181,10)
rxAlphas=[180]

snrDb=20
seeds = [12, 123, 1234, 12345]
allResults = {}
for cdl in ["A", "B", "C", "D", "E"]:
    for seed in seeds:
        print(f"Running with seed {seed} for CDL-{cdl} ...")
        allResults[seed] = getBitrateInfo(numSlots=50, snrDbs=[snrDb], pdsc=pdsch, cdlProfile=cdl,
                                          txAlphas=txAlphas, rxAlphas=rxAlphas, seed=seed, quiet=True)

        if len(rxAlphas)==1:   bers = [allResults[seed][snrDb][txAlpha][rxAlphas[0]]["BER"] for txAlpha in txAlphas]
        else:                  bers = [allResults[seed][snrDb][txAlphas[0]][rxAlpha]["BER"] for rxAlpha in rxAlphas]
        plt.plot(range(-180,181,10), bers, label=f"Seed={seed}")

    plt.title(f"BER for Different {'TX' if len(rxAlphas) == 1 else 'RX'} Bearing Angles over CDL-{cdl} at SNR = {snrDb} dB")
    plt.grid()
    plt.legend()
    plt.xlabel("Bearing Angle (degrees)")
    plt.ylabel("BER (%)")
    plt.show()
    print()

Running with seed 12 for CDL-A ...
Running with seed 123 for CDL-A ...
Running with seed 1234 for CDL-A ...
Running with seed 12345 for CDL-A ...
../../../../_images/source_Playground_Notebooks_Channels_CdlBearingAngles_4_1.png

Running with seed 12 for CDL-B ...
Running with seed 123 for CDL-B ...
Running with seed 1234 for CDL-B ...
Running with seed 12345 for CDL-B ...
../../../../_images/source_Playground_Notebooks_Channels_CdlBearingAngles_4_3.png

Running with seed 12 for CDL-C ...
Running with seed 123 for CDL-C ...
Running with seed 1234 for CDL-C ...
Running with seed 12345 for CDL-C ...
../../../../_images/source_Playground_Notebooks_Channels_CdlBearingAngles_4_5.png

Running with seed 12 for CDL-D ...
Running with seed 123 for CDL-D ...
Running with seed 1234 for CDL-D ...
Running with seed 12345 for CDL-D ...
../../../../_images/source_Playground_Notebooks_Channels_CdlBearingAngles_4_7.png

Running with seed 12 for CDL-E ...
Running with seed 123 for CDL-E ...
Running with seed 1234 for CDL-E ...
Running with seed 12345 for CDL-E ...
../../../../_images/source_Playground_Notebooks_Channels_CdlBearingAngles_4_9.png

Different RX Bearing Angles

[4]:
txAlphas=[0]
rxAlphas=range(-180,181,10)

snrDb=20
seeds = [56, 567, 5678, 56789]
allResults = {}
for cdl in ["A", "B", "C", "D", "E"]:
    for seed in seeds:
        print(f"Running with seed {seed} for CDL-{cdl} ...")
        allResults[seed] = getBitrateInfo(numSlots=50, snrDbs=[snrDb], pdsc=pdsch, cdlProfile=cdl,
                                          txAlphas=txAlphas, rxAlphas=rxAlphas, seed=seed, quiet=True)

        if len(rxAlphas)==1:   bers = [allResults[seed][snrDb][txAlpha][rxAlphas[0]]["BER"] for txAlpha in txAlphas]
        else:                  bers = [allResults[seed][snrDb][txAlphas[0]][rxAlpha]["BER"] for rxAlpha in rxAlphas]
        plt.plot(range(-180,181,10), bers, label=f"Seed={seed}")

    plt.title(f"BER for Different {'TX' if len(rxAlphas) == 1 else 'RX'} Bearing Angles over CDL-{cdl} at SNR = {snrDb} dB")
    plt.grid()
    plt.legend()
    plt.xlabel("Bearing Angle (degrees)")
    plt.ylabel("BER (%)")
    plt.show()
Running with seed 56 for CDL-A ...
Running with seed 567 for CDL-A ...
Running with seed 5678 for CDL-A ...
Running with seed 56789 for CDL-A ...
../../../../_images/source_Playground_Notebooks_Channels_CdlBearingAngles_6_1.png
Running with seed 56 for CDL-B ...
Running with seed 567 for CDL-B ...
Running with seed 5678 for CDL-B ...
Running with seed 56789 for CDL-B ...
../../../../_images/source_Playground_Notebooks_Channels_CdlBearingAngles_6_3.png
Running with seed 56 for CDL-C ...
Running with seed 567 for CDL-C ...
Running with seed 5678 for CDL-C ...
Running with seed 56789 for CDL-C ...
../../../../_images/source_Playground_Notebooks_Channels_CdlBearingAngles_6_5.png
Running with seed 56 for CDL-D ...
Running with seed 567 for CDL-D ...
Running with seed 5678 for CDL-D ...
Running with seed 56789 for CDL-D ...
../../../../_images/source_Playground_Notebooks_Channels_CdlBearingAngles_6_7.png
Running with seed 56 for CDL-E ...
Running with seed 567 for CDL-E ...
Running with seed 5678 for CDL-E ...
Running with seed 56789 for CDL-E ...
../../../../_images/source_Playground_Notebooks_Channels_CdlBearingAngles_6_9.png

TX Bearing Angles in MISO with an Omnidirectional RX Antenna

[5]:
# Create the carrier:
carrier = Carrier(numRbs=24, spacing=30)    # Carrier with 14 resource blocks and 30 kHz subcarrier spacing
bwp = carrier.curBwp                        # The only bandwidth part in the carrier

# Create a PDSCH object
pdsch = PDSCH(bwp, numLayers=2, modulation="16QAM")
pdsch.setDMRS(configType=2, additionalPos=2)     # Specify the DMRS configuration

txAlphas=range(-180,181,10)
rxAlphas=[0]

snrDb=20
seeds = [12, 123, 1234, 12345]
allResults = {}
for cdl in ["A", "B", "C", "D", "E"]:
    for seed in seeds:
        print(f"Running with seed {seed} for CDL-{cdl} ...")
        allResults[seed] = getBitrateInfo(numSlots=50, snrDbs=[snrDb], pdsc=pdsch, cdlProfile=cdl,
                                          txAlphas=txAlphas, rxAlphas=rxAlphas,
                                          seed=seed, rxOmni=True, quiet=True)

        if len(rxAlphas)==1:   bers = [allResults[seed][snrDb][txAlpha][rxAlphas[0]]["BER"] for txAlpha in txAlphas]
        else:                  bers = [allResults[seed][snrDb][txAlphas[0]][rxAlpha]["BER"] for rxAlpha in rxAlphas]
        plt.plot(range(-180,181,10), bers, label=f"Seed={seed}")

    plt.title(f"BER vs. TX Bearing Angle with Omnidirectional RX, CDL-{cdl}, SNR = {snrDb} dB")
    plt.grid()
    plt.legend()
    plt.xlabel("Bearing Angle (degrees)")
    plt.ylabel("BER (%)")
    plt.show()
Running with seed 12 for CDL-A ...
Running with seed 123 for CDL-A ...
Running with seed 1234 for CDL-A ...
Running with seed 12345 for CDL-A ...
../../../../_images/source_Playground_Notebooks_Channels_CdlBearingAngles_8_1.png
Running with seed 12 for CDL-B ...
Running with seed 123 for CDL-B ...
Running with seed 1234 for CDL-B ...
Running with seed 12345 for CDL-B ...
../../../../_images/source_Playground_Notebooks_Channels_CdlBearingAngles_8_3.png
Running with seed 12 for CDL-C ...
Running with seed 123 for CDL-C ...
Running with seed 1234 for CDL-C ...
Running with seed 12345 for CDL-C ...
../../../../_images/source_Playground_Notebooks_Channels_CdlBearingAngles_8_5.png
Running with seed 12 for CDL-D ...
Running with seed 123 for CDL-D ...
Running with seed 1234 for CDL-D ...
Running with seed 12345 for CDL-D ...
../../../../_images/source_Playground_Notebooks_Channels_CdlBearingAngles_8_7.png
Running with seed 12 for CDL-E ...
Running with seed 123 for CDL-E ...
Running with seed 1234 for CDL-E ...
Running with seed 12345 for CDL-E ...
../../../../_images/source_Playground_Notebooks_Channels_CdlBearingAngles_8_9.png
[ ]: