End-to-End PDSCH Simulation with a Trajectory-Based Channel Model

This notebook demonstrates how to use NeoRadium with a DeepMIMO ray-tracing scenario to simulate an end-to-end 5G NR PDSCH link while the UE moves along a trajectory. The example starts by opening a DeepMIMO scenario, selecting a base station and user grid, and creating a random trajectory through the DeepMIMO user grid. The trajectory-based channel model is then used in a slot-by-slot PDSCH simulation to measure the bit error rate (BER) at different SNR values.

The main goal is to compare two receiver-side channel estimation assumptions:

  • Perfect channel knowledge: the receiver uses the exact effective channel.

  • LS + interpolation: the receiver estimates the channel from the received grid using least-squares estimation and interpolation.

For simplicity, both cases use the same perfect-channel precoder at the transmitter. This keeps the example focused on the receiver-side channel estimation difference and avoids adding extra complications related to transmitter-side CSI acquisition or precoder estimation.

A few implementation notes:

  • The dataFolder variable should point to the folder where the DeepMIMO scenario files are stored on your system. Update it if your local DeepMIMO data is in a different location.

  • The trajLen parameter used when generating the random trajectory refers to the number of DeepMIMO grid points used for the path. This is different from the number of trajectory points used by the channel model, since the trajectory points are obtained by interpolation between DeepMIMO grid points.

  • The simulation uses only the first 5,000 trajectory points. This keeps the runtime manageable while still allowing the UE to move through a meaningful portion of the generated trajectory.

The final result is a BER-vs-SNR comparison for the two channel estimation methods.

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

from neoradium import DeepMimoData, TrjChannel, BandwidthPart, PDSCH, AntennaPanel, random

[2]:
# Replace this with the folder on your system where the DeepMIMO scenarios are stored
dataFolder = "/data/RayTracing/DeepMIMO/Scenarios/V4/"
DeepMimoData.setScenariosPath(dataFolder)

# Create a DeepMimoData object
deepMimoData = DeepMimoData("asu_campus_3p5")
deepMimoData.print()

DeepMimoData Properties:
  Scenario:                   asu_campus_3p5
  Version:                    4.0.0a3
  UE Grid:                    rx_grid
  Grid Size:                  411 x 321
  Base Station:               BS (at [166. 104.  22.])
  Total Grid Points:          131,931
  UE Spacing:                 [1. 1.]
  UE bounds (xyMin, xyMax)    [-225.55 -160.17], [184.45 159.83]
  UE Height:                  1.50
  Carrier Frequency:          3.5 GHz
  Num. paths (Min, Avg, Max): 0, 6.21, 10
  Num. total blockage:        46,774
  LOS percentage:             19.71%

[3]:
random.setSeed(123)                                 # Make results reproducible
bwp = BandwidthPart(numRbs=24, spacing=15)          # Create the BandwidthPart object

# Create a random trajectory
trajectory = deepMimoData.getRandomTrajectory(xyBounds=np.array([[-210, 40], [-120, 100]]),   # Trajectory bounds
                                              segLen=2,     # Number of DeepMIMO grid points on the shortest segment
                                              bwp=bwp,      # The bandwidth part
                                              trajLen=100,  # Number of DeepMIMO grid points on the trajectory
                                              speedMps=14,  # Speed in m/s
                                              trajDir="+X") # Trajectory direction

trajectory.print()                                  # Print the trajectory information
ax = deepMimoData.drawMap("LOS-NLOS", trajectory)   # Draw the map with the trajectory
deepMimoData.drawBsPanel(ax, 180)                   # TX antenna at a 180-degree bearing angle

Trajectory Properties:
  start (x,y,z):          (-209.55, 69.83, 1.50)
  No. of points:          7,648
  curIdx:                 0 (0.00%)
  curSpeed:               [ 9.9 -9.9  0. ]
  Total distance:         107.30 meters
  Total time:             7.647 seconds
  Average Speed:          14.031 m/s
  Carrier Frequency:      3.5 GHz
  Paths (Min, Avg, Max):  6, 8.99, 10
  Totally blocked:        0
  LOS percentage:         29.11%

../../../../_images/source_Playground_Notebooks_RayTracing_TrajEndToEnd_3_1.png
[4]:
numSlots = 5000                 # Number of OFDM slots; uses the first 5,000 trajectory points
snrDbs = [5,10,15,20,25,30]     # SNR values, in dB
freqDomain = True               # Set to False to apply the channel in the time domain

# Create a PDSCH object
pdsch = PDSCH(bwp, numLayers=2, modulation="16QAM")     # PDSCH with 2 layers
pdsch.setDMRS(configType=2, additionalPos=2)            # DMRS configuration
results = {}                                            # Store simulation results

# Create a trajectory-based channel model
channel = TrjChannel(bwp, trajectory,
                     txAntenna = AntennaPanel([2,4]),   # 8 TX antennas
                     txOrientation = [180,0,0],         # BS antenna facing the left side of the map
                     rxAntenna = AntennaPanel([1,2]),   # 2 RX antennas
                     seed = 123)

for chanEstMethod in ["Perfect", "LS"]:                 # Two channel estimation methods
    results[chanEstMethod] = {}
    print(f"\nRunning the end-to-end simulation with \"{chanEstMethod}\" channel estimation "
          f"in the {'frequency' if freqDomain else 'time'} domain.")
    print("SNR(dB)    Total Bits    Bit Errors   BER(%)   Point   time(sec.)")
    print("-------   ------------   ----------   ------   -----   ----------")
    for snrDb in snrDbs:
        random.setSeed(123)                             # Make the results reproducible
        t0 = time.monotonic()
        channel.restart()

        bitErrors = 0
        totalBits = 0

        for slotNo in range(numSlots):
            pdsch.initGrid()                            # Create a resource grid populated with DMRS
            numBits = pdsch.getBitCapacity()[0]         # Actual number of bits available in the resource grid
            txBits = random.bits(numBits)               # Create random binary data

            # Populate the resource grid with data, including QAM modulation and resource mapping.
            pdsch.setPdschData(txBits)

            channelMatrix = channel.getChannelMatrix()              # Get the channel matrix
            precoder = pdsch.getPrecodingMatrix(channelMatrix)      # Get the precoder matrix from the PDSCH object

            txGrid = bwp.createGrid(channel.txAntenna.numEl)        # Create the transmitted resource grid
            pdsch.precodeTo(txGrid, precoder)                       # Perform the precoding

            if freqDomain:
                rxGrid = txGrid.applyChannel(channelMatrix)         # Apply the channel in the frequency domain
                rxGrid = rxGrid.addNoise(snrDb=snrDb)               # Add noise
            else:
                txWaveform = txGrid.ofdmModulate()                  # OFDM modulation
                maxDelay = channel.getMaxDelay()                    # Get the maximum channel delay
                txWaveform = txWaveform.pad(maxDelay)               # Pad with zeros
                rxWaveform = channel.applyToSignal(txWaveform)      # Apply channel in time domain
                noisyRxWaveform = rxWaveform.addNoise(snrDb=snrDb, bwp=bwp)     # Add noise
                offset = channel.getTimingOffset()                  # Get the timing offset for synchronization
                syncedWaveform = noisyRxWaveform.sync(offset)       # Synchronization
                rxGrid = syncedWaveform.ofdmDemodulate(bwp)         # OFDM demodulation

            errVar = None
            if chanEstMethod == "Perfect":                          # Perfect channel knowledge
                estChannelMatrix = TrjChannel.getEffChannel(channelMatrix, precoder)
            else:                                                   # LS + interpolation channel estimation
                estChannelMatrix, errVar = pdsch.estimateChannel(rxGrid)

            eqGrid, llrScales = pdsch.equalize(rxGrid, estChannelMatrix, errVar)    # Equalization
            rxBits = pdsch.getHardBits(eqGrid)[0]                   # Demodulation (hard decision)
            bitErrors += np.abs(rxBits-txBits).sum()                # Calculate the number of bit errors
            totalBits += numBits
            print(f"{snrDb:^7d}   {totalBits:<12,d}   {bitErrors:<10,d}   {bitErrors*100/totalBits:^6.3f}"
                  f"   {slotNo+1:^5,d}   {time.monotonic()-t0:^10.2f}", end='\r')
            channel.goNext()                                        # Prepare the channel model for the next slot

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

# Plot the results
for i,chanEstMethod in enumerate(['Perfect', 'LS']):
    bers = [results[chanEstMethod][snrDb]["BER"] for snrDb in snrDbs]
    plt.plot(snrDbs, bers, label=chanEstMethod)
plt.legend()
plt.title(f"Bit Error Rate for Different Channel-Estimation Methods\n"
          f"along the first {numSlots:,} trajectory points")
plt.grid()
plt.xlabel("SNR (dB)")
plt.xticks(snrDbs)
plt.ylabel("BER (%)")
plt.show()

Running the end-to-end simulation with "Perfect" channel estimation in the frequency domain.
SNR(dB)    Total Bits    Bit Errors   BER(%)   Point   time(sec.)
-------   ------------   ----------   ------   -----   ----------
   5      149,760,000    22,902,539   15.293   5,000     234.99
  10      149,760,000    15,291,978   10.211   5,000     234.88
  15      149,760,000    9,124,849    6.093    5,000     235.19
  20      149,760,000    5,234,897    3.496    5,000     235.25
  25      149,760,000    3,714,488    2.480    5,000     237.60
  30      149,760,000    3,029,572    2.023    5,000     234.70

Running the end-to-end simulation with "LS" channel estimation in the frequency domain.
SNR(dB)    Total Bits    Bit Errors   BER(%)   Point   time(sec.)
-------   ------------   ----------   ------   -----   ----------
   5      149,760,000    27,055,580   18.066   5,000     242.67
  10      149,760,000    18,667,701   12.465   5,000     240.65
  15      149,760,000    11,489,101   7.672    5,000     238.97
  20      149,760,000    6,697,518    4.472    5,000     237.22
  25      149,760,000    4,311,994    2.879    5,000     235.84
  30      149,760,000    3,376,237    2.254    5,000     234.05
../../../../_images/source_Playground_Notebooks_RayTracing_TrajEndToEnd_4_1.png
[ ]: