Generating a Dataset for Channel Estimation Training

The first step in this deep-learning example is to prepare a supervised dataset for effective channel estimation. This notebook demonstrates how to use the CDL channel’s getChanGen method to create a channel generator, run an end-to-end communication pipeline, and save training examples for a neural-network-based channel estimator.

Because DMRS reference signals are used for channel estimation, the model learns the effective channel, including the effect of precoding. For each receive antenna, the received DMRS observations are a superposition of the transmitted DMRS signals from all transmission layers after propagation through the effective channel. In this tutorial, we estimate the effective channel for each receive antenna independently. This keeps the example simple and matches the later model input/output format.

Building data samples

At the receiver, the values and locations of the transmitted DMRS symbols are known for all Nl layers. For each receive antenna, we create a complex tensor with shape (Nl+1) x L x K:

  • The first Nl matrices contain the transmitted DMRS symbols for each layer. Each L x K matrix has DMRS values at the configured DMRS resource elements and zeros elsewhere.

  • The last L x K matrix contains the received resource-grid values at the DMRS resource elements for one receive antenna, with zeros elsewhere.

The DMRS configuration used in this tutorial is selected so that PDSCH data is not transmitted on other layers at the DMRS resource elements. In particular, the CDM groups without data are configured to avoid mixing PDSCH data with the DMRS observations used by the model (default DMRS configuration).

Finally, because the neural network operates on real-valued tensors, each complex tensor is split into real and imaginary parts. This produces a real-valued sample tensor with shape 2(Nl+1) x L x K. Since there are Nr receive antennas, each communication slot produces Nr training samples.

Building data labels

The desired output is the effective channel matrix for one receive antenna. The ground-truth effective channel is available as a complex 4-D tensor with shape L x K x Nr x Nl. We decompose it into Nr tensors of shape Nl x L x K, one for each receive antenna, and then split each tensor into real and imaginary parts. The resulting label tensor has shape 2Nl x L x K.

This example intentionally keeps the tensor layout fixed to the PDSCH, DMRS, bandwidth, and layer configuration used throughout the tutorial. A more general package example could store additional metadata and construct the model dimensions dynamically, but the fixed layout makes the workflow easier to follow.

The following block diagram shows how the data samples and labels are derived from the DMRS symbols, the received resource grid, and the ground-truth effective channel matrix.

Data Generation Pipeline

Let’s start by importing the required modules from NeoRadium.

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

from neoradium import BandwidthPart, PDSCH, CdlChannel, AntennaPanel, Grid, random

The getSamples function below receives a PDSCH object, a received resource grid, and the ground-truth effective channel. It returns Nr pairs of samples and labels: one pair for each receive antenna.

[2]:
def getSamples(pdsch, rxGrid, actualChannel):
    dmrsIdx = pdsch.grid.getReIndexes("DMRS")   # This contains the locations of DMRS values
    rr, ll, kk = rxGrid.shape                   # Number of RX antennas, symbols, and subcarriers

    ls = np.unique(dmrsIdx[1])                                                  # Unique DMRS symbols
    ks = np.unique(dmrsIdx[2])                                                  # Unique DMRS subcarriers
    samples = np.zeros( (rr, pdsch.numLayers+1, ll, kk), dtype=np.complex128)   # Nr x Nl+1 x L x K
    labels = np.transpose( actualChannel, [2,3,0,1])                            # Nr x Nl x L x K
    for r in range(rr):
        samples[r][dmrsIdx] = pdsch.grid[dmrsIdx]                               # Known DMRS values
        samples[r][np.ix_([-1], ls, ks)] = rxGrid[ np.ix_([r], ls, ks) ]        # Received grid values at DMRS
                                                                                # symbol/subcarrier locations

    samples = np.concatenate([samples.real, samples.imag],axis=1)               # Nr x 2(Nl+1) x L x K
    labels = np.concatenate([labels.real, labels.imag],axis=1)                  # Nr x 2Nl x L x K
    return samples, labels

The makeDataset function below receives the number of communication slots (numSlots, where each slot contains 14 OFDM symbols), the list or range of SNR values in dB (snrDbs), the seed used to initialize NeoRadium’s random generator (seed), and the output dataset file name (fileName).

The function implements the communication pipeline shown above. For each OFDM slot, it randomly generates transport-block bits, transmits the corresponding PDSCH resource grid through a randomly generated CDL channel, adds noise, and then calls getSamples to create the supervised learning examples. The samples and labels are aggregated and saved to the specified file.

For this tutorial, the channel and link parameters are deliberately fixed except for the randomized channel profile, delay spread, UE direction, UE speed, and SNR. This provides enough variation to train a useful model while keeping the example compact.

[3]:
def makeDataset(numSlots, snrDbs, seed, fileName=None):
    # Create a bandwidth-part object with 24 PRBs and 15 kHz subcarrier spacing
    bwp = BandwidthPart(numRbs=24, spacing=15)

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

    samples, labels = [], []

    t0 = time.monotonic()                                   # Start time for time estimation

    # Create a channel generator
    chanGen = CdlChannel.getChanGen(numSlots,                   # Number of random channels
                                    bwp,                        # Bandwidth part
                                    profiles="ABCDE",           # Randomly pick a CDL profile
                                    delaySpread=(10,500),       # Uniformly sample between 10 and 500 ns
                                    ueSpeed=(0,20),             # Uniformly sample the UE speed between 0 and 20 m/s
                                    ueDir=[45, 135, 225, 315],  # Randomly pick a UE direction in degrees
                                    carrierFreq=4e9,            # Carrier frequency
                                    txAntenna=AntennaPanel([2,2], polarization="x"),  # 8 TX antennas
                                    rxAntenna=AntennaPanel([1,1], polarization="x"),  # 2 RX antennas
                                    seed=seed)


    totalSamples = numSlots*2                               # numSlots * Nr
    print(f"Creating dataset for SNR={snrDbs} dB with {numSlots:,} slots ({totalSamples:,} samples)")
    random.setSeed(seed)
    for slotNo, channelMatrix in enumerate(chanGen):        # For each random channel from the generator
        snrDb = random.integers(snrDbs[0],snrDbs[1]+1) if type(snrDbs)==tuple else random.choice(snrDbs)
        pdsch.initGrid()                                    # Create and initialize PDSCH's internal grid
        numBits = pdsch.getBitCapacity()[0]                 # Number of bits available in the resource grid
        txBits = random.bits(numBits)                       # Create random binary data
        pdsch.setPdschData(txBits)                          # Map/modulate the data to the resource grid

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

        txGrid = bwp.createGrid(channelMatrix.shape[3])     # Create the transmitted grid
        pdsch.precodeTo(txGrid, precoder)                   # Perform the precoding

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

        gtChanMat = CdlChannel.getEffChannel(channelMatrix, precoder)  # Ground-truth channel

        # Get the dataset samples and labels for the current slot
        # newSamples is Nr × 2(Nl+1) × L × K; newLabels is Nr × 2Nl × L × K
        newSamples, newLabels = getSamples(pdsch, rxGrid, gtChanMat)

        samples += [newSamples]
        labels += [newLabels]

        dt = time.monotonic()-t0                            # Get the elapsed time since the beginning
        percentDone = (slotNo+1)*100/numSlots               # Calculate the percentage of the task completed

        # Print progress messages
        print(f"  {int(percentDone)}% complete in {int(np.round(dt))} s; "
              f"estimated time remaining: {int(np.round(100*dt/percentDone-dt))} s    ",
              end="\r")

    samples = np.concatenate(samples, axis=0)   # numSamples x 2(Nl+1) x L x K
    labels = np.concatenate(labels, axis=0)     # numSamples x 2Nl x L x K

    if fileName is not None:
        np.save(fileName, np.concatenate([samples,labels],axis=1).astype(np.float32))   # Save the dataset to the specified file
        print(f"  Done. ({dt:.02f} sec., {samples.shape[0]} samples) Saved to \"{fileName}\".                        ")
    else:
        print(f"  Done. ({dt:.02f} sec., {samples.shape[0]} samples)                                                   ")

    return samples, labels

We can now create the datasets for the deep-learning project. The following cell creates separate training, validation, and test dataset files. We use 7000, 1000, and 2000 communication slots for the training, validation, and test datasets, respectively.

For each communication slot, an SNR value is selected randomly from the range -20 dB to 20 dB. Different random seeds are used for the three datasets so that validation and test samples are not reused during training.

[4]:
trainSample, trainlabels = makeDataset(numSlots=7000, snrDbs=(-20,20), seed=1349, fileName="ChestTrain.npy")
validSample, validlabels = makeDataset(numSlots=1000, snrDbs=(-20,20), seed=9431, fileName="ChestValid.npy")
testSample, testlabels   = makeDataset(numSlots=2000, snrDbs=(-20,20), seed=1970, fileName="ChestTest.npy")
Creating dataset for SNR=(-20, 20) dB with 7,000 slots (14,000 samples)
  Done. (784.52 Sec., 14000 samples) Saved to "ChestTrain.npy".
Creating dataset for SNR=(-20, 20) dB with 1,000 slots (2,000 samples)
  Done. (109.86 Sec., 2000 samples) Saved to "ChestValid.npy".
Creating dataset for SNR=(-20, 20) dB with 2,000 slots (4,000 samples)
  Done. (218.68 Sec., 4000 samples) Saved to "ChestTest.npy".
[ ]: