10. MIMO-OFDM in Spatial Multiplexing Mode in 5G Networks
In this project we will demonstrate the transmission and reception of the data from base-station (BS) using low cost SDRs. The whole simulation consists of two parts:
Transmitter side:
Generate the SSB block
Generate the transport block ==> Process it with PDSCH chain ==> Create the slot resource grids
Generate the resource grid and load SSB + PDSCH both into it.
Pass the resource grid through the OFDM modulator: Generate the time domain I/Q samples.
Pass it to the SDR and radiate the signal to the medium.
Receiver side:
Sample the medium to receive the samples and store thm in the buffer.
Downlink synchronization
This is performed using synchronization signal block (SSB).
Helps with
time/frame synchronization.
Coarse CFO correction.
Cell ID detection.
Decoding MIB information.
Data transmission and decoding
10. Import Python Libraries
[1]:
# %matplotlib widget
import os
os.environ["CUDA_VISIBLE_DEVICES"] = "-1"
os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3'
import matplotlib.pyplot as plt
import matplotlib.patches as patches
import matplotlib.animation as animation
import numpy as np
import adi
# from IPython.display import display, HTML
# display(HTML("<style>.container { width:98% !important; }</style>"))
10. 5G Toolkit Libraries
[2]:
import sys
sys.path.append("../../../../")
from toolkit5G.SequenceGeneration import PSS, SSS, DMRS
from toolkit5G.PhysicalChannels import PBCH
from toolkit5G.ResourceMapping import SSB_Grid, ResourceMapperSSB
from toolkit5G.OFDM import OFDMModulator
from toolkit5G.MIMOProcessing import AnalogBeamforming
from toolkit5G.Configurations import TimeFrequency5GParameters, GenerateValidSSBParameters
from toolkit5G.SequenceGeneration import PSS, SSS, DMRS
from toolkit5G.PhysicalChannels import PBCH, PBCHDecoder
from toolkit5G.ResourceMapping import SSB_Grid, ResourceMapperSSB
from toolkit5G.OFDM import OFDMModulator, OFDMDemodulator
from toolkit5G.MIMOProcessing import AnalogBeamforming, ReceiveCombining
from toolkit5G.ReceiverAlgorithms import PSSDetection, SSSDetection, ChannelEstimationAndEqualization, DMRSParameterDetection
from toolkit5G.Configurations import TimeFrequency5GParameters, GenerateValidSSBParameters
from toolkit5G.PhysicalChannels.PDSCH import ComputeTransportBlockSize
from toolkit5G.PhysicalChannels import PDSCHLowerPhy, PDSCHUpperPhy, PDSCHDecoderLowerPhy, PDSCHDecoderUpperPhy
from toolkit5G.Configurations import PDSCHLowerPhyConfiguration, PDSCHUpperPhyConfiguration
from toolkit5G.SymbolMapping import Mapper, Demapper
from toolkit5G.Scrambler import DeScrambler, Scrambler
from toolkit5G.SymbolMapping import Demapper
from toolkit5G.PhysicalChannels.PDSCH import LayerDemapper
from toolkit5G.PhysicalChannels import PBCHDecoder
from toolkit5G.OFDM import OFDMDemodulator
from toolkit5G.ReceiverAlgorithms import PSSDetection, SSSDetection, ChannelEstimationAndEqualizationPBCH, DMRSParameterDetection, CarrierFrequencyOffsetEstimation, ChannelEstimationAndEqualizationPDSCH
from toolkit5G.PhysicalChannels import PDSCHLowerPhy, PDSCHUpperPhy, PDSCHDecoderLowerPhy, PDSCHDecoderUpperPhy
10. Emulation Parameters
[3]:
# Carrier Frequency
carrierFrequency = 1*10**9
numBatches = 9 # Number of batches considered for simulation
scs = 30*10**3 # Subcarrier Spacing for simulation
numBSs = 1 # Number of BSs considered for simulation
bandwidth = 30*10**6
# Number of UEs considered for simulation
numUEs = numBatches # For now we are assuming that the numbatches are captured via numUEs
numRB = 85 # Number of Resource mapping considered for simulation | # 1 RB = 12 subcarrier
slotNumber = int(np.random.randint(0,2**(scs/15000)*10)) # Index of the slot considered for simulation
Nfft = 1024 # FFTSize
Nt = 1
enablePrecoding = False
enableCombining = False
print("************ Simulation Parameters *************")
print()
print(" numBatches: "+str(numBatches))
print(" numRB: "+str(numRB))
print(" fft Size: "+str(Nfft))
print(" numBSs: "+str(numBSs))
print(" numUEs: "+str(numUEs))
print(" scs: "+str(scs))
print(" slotNumber: "+str(slotNumber))
print("Num of Antennas: "+str(Nt))
print()
print("********************************************")
************ Simulation Parameters *************
numBatches: 9
numRB: 85
fft Size: 1024
numBSs: 1
numUEs: 9
scs: 30000
slotNumber: 3
Num of Antennas: 1
********************************************
10. PDSCH Transmitter Implementation
10. Generate the PDSCH Resource Grid
[5]:
pdschUpperPhy = PDSCHUpperPhy(symbolsPerSlot = numSymbols, numRB = numRB, mcsIndex = mcsIndex,
numlayers = numlayers, scalingField = scalingField, additionalOverhead = additionalOverhead,
dmrsREs = dmrsREs, numTBs=numTBs, pdschTable = mcsTable, verbose = False)
codeword = pdschUpperPhy(tblock = [None, None], rvid = [0, 0], enableLBRM = [False, False], numBatch = numBatches, numBSs = numBSs)
rnti = np.random.randint(65536, size=numBSs*numBatches)
nID = np.random.randint(1024, size=numBSs*numBatches)
bits2 = codeword[1] if numTBs == 2 else None
pdschLowerPhyChain = PDSCHLowerPhy(pdschMappingType, configurationType, dmrsTypeAPosition,
maxLength, dmrsAdditionalPosition, l0, ld, l1)
resourceGrid = pdschLowerPhyChain(codeword[0], numRB, rank, slotNumber, scramblingID,
nSCID, rnti, nID, modOrder, startSymbol, bits2 = bits2)
## Load the resource Grid into the transmision Grid
txGrid = np.zeros(resourceGrid.shape[0:-1]+(Nfft,), dtype= np.complex64)
bwpOffset = np.int32(0.5*(Nfft-numRB*12))
txGrid[...,bwpOffset:bwpOffset+numRB*12] = np.sqrt(1/rank)*resourceGrid
txGrid = txGrid/(np.linalg.norm(np.abs(txGrid), axis=-1)[...,np.newaxis] + 0.0001)
pdschLowerPhyChain.displayDMRSGrid(), pdschLowerPhyChain.displayResourceGrid()
[5]:
((<Figure size 640x480 with 2 Axes>,
array([<Axes: title={'center': 'Port number: 0'}, xlabel='OFDM Symbol-Index', ylabel='Subcarrier-Index'>,
<Axes: title={'center': 'Port number: 1'}, xlabel='OFDM Symbol-Index', ylabel='Subcarrier-Index'>],
dtype=object)),
None)
[6]:
txGrid.shape
[6]:
(9, 1, 2, 14, 1024)
[7]:
# Plot Resource Grid
#################################################################
fig, ax = plt.subplots()
plt.imshow(np.abs(txGrid[0,0,0]), cmap = 'hot', interpolation='nearest', aspect = "auto")
ax = plt.gca();
ax.grid(color='c', linestyle='-', linewidth=1)
ax.set_xlabel("Subcarrier-Index (k)")
ax.set_ylabel("OFDM Symbol Index (n)")
ax.set_title("Heat map of Transmit Grid")
# Gridlines based on minor ticks
plt.show()
10. SSB Transmitter Implementation
10. Generate the SSB Resource Grid
[8]:
## This class fetches valid set of 5G parameters for the system configurations
nSymbolFrame = 14
numOFDMSymbols = 14
tfParams = TimeFrequency5GParameters(bandwidth, scs)
tfParams(nSymbolFrame, typeCP = "normal")
nRB = tfParams.numRBs # SSB Grid size (Number of RBs considered for SSB transition)
Neff = nRB*12
lengthCP = (tfParams.lengthCP).astype(np.int32) # CP length
#___________________________________________________________________
#### Generate MIB Information
lamda = 3e8/carrierFrequency
nSCSOffset = 1
ssbParameters = GenerateValidSSBParameters(carrierFrequency, nSCSOffset, "caseA")
systemFrameNumber = ssbParameters.systemFrameNumber
subCarrierSpacingCommon = scs
ssbSubCarrierOffset = ssbParameters.ssbSubCarrierOffset
DMRSTypeAPosition = ssbParameters.DMRSTypeAPosition
controlResourceSet0 = ssbParameters.controlResourceSet0
searchSpace0 = ssbParameters.searchSpace0
isPairedBand = ssbParameters.isPairedBand
nSCSOffset = ssbParameters.nSCSOffset
choiceBit = ssbParameters.choiceBit
ssbType = ssbParameters.ssbType
nssbCandidatesInHrf = ssbParameters.nssbCandidatesInHrf
ssbIndex = ssbParameters.ssbIndex
hrfBit = ssbParameters.hrfBit
cellBarred = ssbParameters.cellBarred
intraFrequencyReselection = ssbParameters.intraFrequencyReselection
withSharedSpectrumChannelAccess = ssbParameters.withSharedSpectrumChannelAccess
Nsc_ssb = 240
Nsymb_ssb = 4
#___________________________________________________________________________
N_ID2 = np.random.randint(3)
# Generate PSS sequence
pssObject = PSS(N_ID2);
pssSequence = pssObject()
N_ID1 = np.random.randint(336)
N_ID = 3*N_ID1 + N_ID2
# Generate SSS sequence
sssObject = SSS(N_ID1, N_ID2);
sssSequence = sssObject()
# Generate DMRS sequence
dmrsLen = 144;
dmrsObject = DMRS("PBCH", N_ID, ssbIndex, nssbCandidatesInHrf, hrfBit)
# dmrsSeq = dmrs.getSequence("tensorflow")
dmrsSequence = dmrsObject(dmrsLen)
# Generate PBCH symbols
pbchObject = PBCH(carrierFrequency, choiceBit, subCarrierSpacingCommon, DMRSTypeAPosition,
controlResourceSet0, searchSpace0, cellBarred, intraFrequencyReselection,
systemFrameNumber, ssbSubCarrierOffset, hrfBit, ssbIndex, N_ID,
nssbCandidatesInHrf)
pbchSymbols = pbchObject()
## Generate SSB Object
ssbObject = SSB_Grid(N_ID, True)
ssb = ssbObject(pssSequence, sssSequence, dmrsSequence, pbchSymbols) # generating SSB using PSS,SSS, PBCH payload and DMRS.
## Loading SSB to Resource Grid
ssbPositionInBurst = np.zeros(nssbCandidatesInHrf, dtype=int)
ssbPositionInBurst[0] = 1
ssbRGobject = ResourceMapperSSB(ssbType, carrierFrequency, isPairedBand, withSharedSpectrumChannelAccess)
offsetInRBs = int((nRB-20)/2)
ssbGrid = ssbRGobject(ssb[0], ssbPositionInBurst, offsetInSubcarriers = ssbSubCarrierOffset,
offsetInRBs = offsetInRBs, numRBs = nRB)[0:14] # SSB Grid of size 14 X numbers of RBs x 12.
fig, ax = ssbObject.displayGrid(option=1)
firstSymbolIndex = int(2)
numofGuardCarriers = (int((Nfft - Neff)/2), int((Nfft - Neff)/2))
offsetToPointA = 0
firstSCIndex = int(numofGuardCarriers[0] + offsetToPointA)
ssbResGrid = np.zeros((1, numOFDMSymbols, Nfft), dtype= np.complex64)
ssbResGrid[..., firstSCIndex:firstSCIndex+ssbGrid.shape[-1]] = ssbGrid
#__________________________________________________
10. Precoding and Beamforming Architecture
[9]:
## Concatenate the SSB and PDSCH Grid
X = np.concatenate([ssbResGrid.repeat(2,0),
txGrid[:,0].transpose(1,0,2,3).reshape(rank,-1,Nfft)], axis=1)
## OFDM Modulation at Transmitter
#####################################
modulator = OFDMModulator(lengthCP[1])
x_time = modulator(X)
#______________________________________________________
# Plot Resource Grid
#################################################################
fig, ax = plt.subplots()
plt.imshow(np.abs(X[1]).T, cmap = 'hot', interpolation='nearest', aspect = "auto")
ax = plt.gca();
ax.grid(color='c', linestyle='-', linewidth=1)
ax.set_xlabel("Subcarrier-Index (k)")
ax.set_ylabel("OFDM Symbol Index (n)")
ax.set_title("Heat map of Transmit Grid")
# Gridlines based on minor ticks
plt.show()
10. SDR-Setup Configurations
[10]:
## SDR Parameters
sample_rate = Nfft*scs
# Pulse Shaping
numSamplesPerSymbol = 1
# number of samples returned per call to rx()
buffer_size = int(Nfft*1.2*numSamplesPerSymbol*112)
# Basic SDR Setup
sdr = adi.ad9361("ip:192.168.2.1")
sdr.tx_enabled_channels = [0,1] # Enable 2 transmit chains
sdr.rx_enabled_channels = [0,1]
sdr.sample_rate = int(sample_rate)
# Config Tx
sdr.tx_rf_bandwidth = int(sample_rate) # filter cutoff, just set it to the same as sample rate
sdr.tx_lo = int(carrierFrequency)
sdr.tx_hardwaregain_chan0 = 0 # Increase to increase tx power, valid range is -90 to 0 dB
# # Config Rx
# sdr.gain_control_mode_chan0 = 'manual'
# sdr.rx_hardwaregain_chan0 = 40.0 # dB
# # The receive gain on the Pluto has a range from 0 to 74.5 dB.
sdr.gain_control_mode_chan0 = 'slow_attack'
# # AGC modes:
# # 1. "manual"
# # 2. "slow_attack"
# # 3. "fast_attack"
sdr.rx_lo = int(carrierFrequency)
sdr.rx_rf_bandwidth = int(sample_rate) # filter width, just set it to the same as sample rate for now
sdr.rx_buffer_size = int(4*buffer_size)
10. Transmission: SDR RF Transmitter
10. Precoding and Beamforming the OFDM signal
[11]:
sdr.tx_destroy_buffer()
# Start the transmitter
sdr.tx_cyclic_buffer = True # Enable cyclic buffers
scalingFactor = 1.9*2**16
numRepetition = 1
# sdr.tx([scalingFactor*(x_time[0] + x_time[1]).repeat(numRepetition), # Transmitting via-RF channel-1
# scalingFactor*(x_time[0] - 1j*x_time[1]).repeat(numRepetition)]) # Transmitting via-RF channel-2
sdr.tx([scalingFactor*(x_time[0]).repeat(numRepetition), # Transmitting via-RF channel-1
scalingFactor*(x_time[1]).repeat(numRepetition)]) # Transmitting via-RF channel-2
[12]:
np.abs(scalingFactor*x_time.repeat(numRepetition)).max(), np.abs(scalingFactor*x_time.repeat(numRepetition)).min()
[12]:
(13793.594910929532, 0.0)
10. Receiver Implementation: SSB
10. Reception: SDR RF Receiver
[13]:
# Clear buffer just to be safe
for i in range (0, 10):
raw_data = sdr.rx()
# Receive samples
rx_samples = sdr.rx()
# # # Stop transmitting
# sdr.tx_destroy_buffer()
10. Time Synchronization: Based on PSS Correlation
[14]:
## PSS Detection: Based on time domain PSS Correlation
# pssPeakIndices, pssCorrelation, rN_ID2 = pssDetection(r, Nfft, lengthCP = lengthCP[1],
# N_ID2 = None, freqOffset = ssboffset,
# height = 0.75, prominence = 0.65, width=10)
## PSS Detection: Based on time domain PSS Correlation
# pssDetection = PSSDetection("correlation", "threshold")
pssDetection = PSSDetection("largestPeak")
ssboffset = int((Nfft-Neff)/2+ssbRGobject.startingSubcarrierIndices)
pssPeakIndices, pssCorrelation, rN_ID2, freqOffset = pssDetection(rx_samples[1], Nfft, lengthCP = lengthCP[1],
nID2 = None, freqOffset = ssboffset)
if(pssPeakIndices > rx_samples[1].size - 140*(Nfft + lengthCP[1])):
pssPeakIndices = pssPeakIndices - 140*(Nfft + lengthCP[1])
## PSS Detection Plot
#################################################################
fig, ax = plt.subplots(figsize = (10.5, 4))
# single line
ax.plot(pssCorrelation)
ax.vlines(x = pssPeakIndices, ymin = 0*pssCorrelation[pssPeakIndices],
ymax = pssCorrelation[pssPeakIndices], colors = 'purple')
ax.set_ylim([0,np.max(pssCorrelation)*1.1])
ax.set_xlabel("Time Samples Index")
ax.set_ylabel("Amplitude of Time Domain Correlation")
ax.set_title("Amplitude (of Time Domain Correlation) vs Time-samples")
plt.show()
#________________________________________________________________
**(rasterOffset, PSS-ID) (402, 0)
**(rasterOffset, PSS-ID) (402, 1)
**(rasterOffset, PSS-ID) (402, 2)
10. PBCH Receiver
OFDM Demodulation
SSS Dtection
DMRS Parameters Detection
Channel Estimation and PBCH Equalization
PBCH Decoding and MIB Extraction
[15]:
## OFDM Demodulator Object
ofdmDemodulator = OFDMDemodulator(Nfft, lengthCP[1])
pssStartIndex = pssPeakIndices
# pssStartIndex = pssPeakIndices[0][0]
rxGrid = ofdmDemodulator((rx_samples[0].reshape(1,-1))[...,pssStartIndex:(pssStartIndex+4*(Nfft+lengthCP[1]))])
ssbSCSoffset = int((Nfft-Neff)/2+ssbRGobject.startingSubcarrierIndices)
ssbEstimate = rxGrid[:,:,ssbSCSoffset:(ssbSCSoffset+240)]
nssbCandidatesInHrf = 4
dmrsLen = 144
## N_ID_1 Estimation: SSS based
sssDetection = SSSDetection(method="channelAssisted", nID2=rN_ID2)
rN_ID1 = sssDetection(ssbEstimate[0])
rN_ID = 3*rN_ID1 + rN_ID2
## Generate SSB object to get DMRS and PBCH Indices
rxSSBobject = SSB_Grid(rN_ID)
rxDMRSIndices = rxSSBobject.dmrsIndices
## Generate DMRS sequence
dmrsDetection = DMRSParameterDetection(int(rN_ID), nssbCandidatesInHrf)
rssbIndex, rHrfBit = dmrsDetection(ssbEstimate[0])
rxDMRSobject = DMRS("PBCH", int(rN_ID), int(rssbIndex), nssbCandidatesInHrf, rHrfBit)
rxDMRSseq = rxDMRSobject(dmrsLen)
# ## Estimating the channel at DMRS (t-f) location, interpolting for data (t-f) location and equalizing the symbols
# ## Object for Channel Estimation
# chanEst = ChannelEstimationAndEqualization(estimatorType = "ZF", interpolatorType = "NN")
# rxPBCHIndices = rxSSBobject.pbchIndices
# pbchEstimate = chanEst(ssbEstimate, rxDMRSseq, rxDMRSIndices, rxPBCHIndices, 10)
chanEst = ChannelEstimationAndEqualizationPBCH(estimatorType = "ZF", interpolatorType = "Linear", isUEmobile=True)
pbchEstimate = chanEst(ssbEstimate, rxDMRSseq, rN_ID)
## PBCH Chain for Decoding information
polarDecoder = "SCL"
symbolDemapper = "maxlog"
# extractMIBinfo = False
extractMIBinfo = True
# carrierFreq, cellID, nssbCandidatesInHrf, ssbIndex, polarDecType, symbolDemapperType
pbchDecoder = PBCHDecoder(carrierFrequency, int(rN_ID), nssbCandidatesInHrf, rssbIndex, polarDecoder, symbolDemapper)
rxMIB, check = pbchDecoder(pbchEstimate, 100, extractMIBinfo)
<frozen toolkit5G.ChannelCoder.PolarCoder.polarDecoder>:494: UserWarning: Required ressource allocation is large for the selected blocklength. Consider option `cpu_only=True`.
10. SSB Grid: Transmitter and Receiver
[16]:
# Plot SSB
fig, ax = plt.subplots(1,2, figsize = (9,3))
ax[0].imshow(np.abs(ssbEstimate[0]), cmap = 'hot', interpolation='nearest', aspect = "auto")
ax[0].grid(color='c', linestyle='-', linewidth=1)
ax[0].set_xlabel("Subcarrier-Index (k)")
ax[0].set_ylabel("Normalized Magnitude")
ax[1].imshow(np.abs(ssb[0]), cmap = 'hot', interpolation='nearest', aspect = "auto")
ax[1].grid(color='c', linestyle='-', linewidth=1)
ax[1].set_xlabel("Subcarrier-Index (k)")
ax[1].set_ylabel("Normalized Magnitude")
plt.title("Heat-map of Received and Transmitted SSB Grid")
plt.show()
10. Spectrum: Transmitted Grid and Received Grid
[17]:
# Plot SSB
fig, ax = plt.subplots(figsize = (9,5))
ax.plot(np.abs(rxGrid[0][0])/np.abs(rxGrid[0][0]).max())
ax.plot(np.abs(X[0,2])/np.abs(X[0,2]).max())
ax.grid(color='c', linestyle='-', linewidth=1)
ax.set_xlabel("Subcarrier-Index (k)")
ax.set_ylabel("Normalized Magnitude")
ax.set_title("Magnitude Spreactrum of Transmitted and Received $1^{st}$ SSB OFDM Symbol")
plt.show()
10. PBCH Decoding and Constellation
[18]:
fig, ax = plt.subplots()
ax.scatter(np.real(pbchEstimate), np.imag(pbchEstimate), s = 12, label = "Equalized Symbols")
ax.scatter(np.real(pbchSymbols), np.imag(pbchSymbols), s = 12, label = "Transmitted Symbols")
ax.grid()
ax.axhline(y=0, ls=":", c="k", lw = 2); ax.axvline(x=0, ls=":", c="k", lw = 2)
ax.set_xlim([-1,1]); ax.set_ylim([-1,1])
ax.set_xlabel("Real {x}"); ax.set_ylabel("Imag {x}")
ax.set_title("Constellation Diagram: QPSK")
ax.legend(loc = "best")
plt.show()
[19]:
pbchDecoder.mibRx.displayParameters(0)
Carrier Frequency: 1000000000
ChoiceBit: 0
nSsbCandidatesInHrf: 4
subCarrierSpacingCommon:30000
DMRSTypeAPosition: typeA
controlResourceSet0: 14
searchSpace0: 0
cellBarred: barred
intraFreqReselection: allowed
systemFrameNumber: 803
ssbSubCarrierOffset: 10
HRFBit: 1
iSSBindex: 0
[20]:
pbchObject.mib.displayParameters(0)
Carrier Frequency: 1000000000
ChoiceBit: 0
nSsbCandidatesInHrf: 4
subCarrierSpacingCommon:30000
DMRSTypeAPosition: typeA
controlResourceSet0: 14
searchSpace0: 0
cellBarred: barred
intraFreqReselection: allowed
systemFrameNumber: 803
ssbSubCarrierOffset: 10
HRFBit: 1
iSSBindex: 0
10. Performance Verification
[21]:
if (rN_ID == N_ID):
print("[Success]: Cell-IDs correctly detected!")
else:
if (rN_ID1 != N_ID1 and rN_ID2 != N_ID2):
print("[Failed]: Receiver couldn't detect the Cell-ID1 and cell-ID2 correctly!")
elif(rN_ID1 != N_ID1):
print("[Failed]: Receiver couldn't detect the Cell-ID1 correctly!")
else:
print("[Failed]: Receiver couldn't detect the cell-ID2 correctly!")
if (rssbIndex == ssbIndex[0]):
print("[Success]: DMRS parameters correctly detected!")
else:
print("[Failed]: Receiver couldn't detect the ssbIndex correctly!")
## Computing BER: Coded and Uncoded
numUEs = 1
nBatch = 1
uncodedBER = np.zeros((numUEs, nBatch))
codedBER = np.zeros((numUEs, nBatch))
bitEst = pbchDecoder.llr.copy()
bitEst[pbchDecoder.llr > 0] = 1
bitEst[pbchDecoder.llr < 0] = 0
uncodedBER = np.mean(np.abs(bitEst - pbchObject.scr2bits[0]))
codedBER = np.mean(np.abs(pbchDecoder.pbchDeInterleavedBits - pbchObject.mibSequence[0]))
print(" (uncoded-BER, codedBER): "+str((uncodedBER, codedBER)))
[Success]: Cell-IDs correctly detected!
[Success]: DMRS parameters correctly detected!
(uncoded-BER, codedBER): (0.0, 0.0)
10. PDSCH Receiver Implementation
10. Extract PDSCH Resource Grid
[22]:
## OFDM Demodulator Object
ofdmDemodulator = OFDMDemodulator(Nfft, lengthCP[1])
pdschStartIndex = pssStartIndex+12*(Nfft+lengthCP[1])
# pssStartIndex = pssPeakIndices[0][0]
rxGrid = ofdmDemodulator(np.array(rx_samples)[...,pdschStartIndex:(pdschStartIndex+14*numBatches*(Nfft+lengthCP[1]))])
pdschGrid = rxGrid[:,:,bwpOffset:bwpOffset+numRB*12].reshape(rank,numBatches,14,-1).transpose(1,0,2,3)
# Plot SSB
fig, ax = plt.subplots()
plt.imshow(np.abs(pdschGrid[0,0]), cmap = 'hot', interpolation='nearest', aspect = "auto")
ax = plt.gca();
ax.grid(color='c', linestyle='-', linewidth=1)
ax.set_xlabel("Subcarrier-Index (k)")
ax.set_ylabel("Normalized Magnitude")
ax.set_title("Heat-map of Received and SSB Grid")
plt.show()
10. PDSCH Receiver
[23]:
# Channel Estimation and Equalization
chEst = ChannelEstimationAndEqualizationPDSCH(slotNumber, scramblingID, nSCID, rnti, nID,
pdschMappingType, configurationType, dmrsTypeAPosition,
maxLength, dmrsAdditionalPosition, l0, ld, l1,
startSymbol, numTargetBits, modOrder)
interpolator = "linear" # Can be "nearest" | "linear" | "cubicspline"
polyOrder = 3 # Required only for Spline, Will be ignored for other interpolators
rsymbols = chEst(pdschGrid, interpolator)
#### Layer Demapping
layDemap = LayerDemapper(numTBs, rank)
rlmSym = layDemap(rsymbols) # received symbols
######## Demapping.
demapMethod = "app" # Demapping method
constType = "qam" # Symbol mapping type
modOrder = modOrder # Mordulation order or number of bits per symbol
snr = 10
hard_out = False
demapper = Demapper(demapMethod, constType, modOrder, hard_out = hard_out)
## Computed Received bits
rxLLRs = demapper([np.complex64(rlmSym), 1/snr])
## Descrambling
if hard_out:
scrObject = Scrambler("PDSCH", rnti, 0, nID)
rxBits = scrObject(rxLLRs)
else:
scrObject = DeScrambler("PDSCH", rnti, 0, nID)
rscrBits = scrObject(rxLLRs)
rxBits = np.where(rscrBits>0, 1, 0)
# PDSCH Upper Physical layer Decoder
pdschUpPhyDec = PDSCHDecoderUpperPhy(numTBs = numTBs, mcsIndex = mcsIndex, symbolsPerSlot= numSymbols,
numRB = numRB, numLayers = numlayers, scalingField = scalingField,
additionalOverhead = additionalOverhead, dmrsREs = dmrsREs,
enableLBRM = [False, False], pdschTable = mcsTable, rvid = [0, 0], verbose=False)
rbits = pdschUpPhyDec([rscrBits])
10. Constellation Diagram
[24]:
fig, ax = plt.subplots()
ax.set_aspect(True)
ax.scatter(np.real(rsymbols),
np.imag(rsymbols), s=24, label="Received Constellation")
# equalizedSamples = rsymbols.flatten()
# indices = np.random.choice(equalizedSamples.size, size = 10000, replace = False)
# ax.scatter(np.real(equalizedSamples[indices]),
# np.imag(equalizedSamples[indices]), s=24, label="Received Constellation")
ax.axhline(y=0, ls=":", c="k"); ax.axvline(x=0, ls=":", c="k")
# ax.set_xlim([-1.5,1.5]); ax.set_ylim([-1.5,1.5])
ax.set_xlim([-2,2]); ax.set_ylim([-2,2])
# ax.set_xlabel("Real {x}"); ax.set_ylabel("Imag {x}")
# if modOrder ==2:
# ax.set_title("Constellation Diagram: QPSK")
# else:
# ax.set_title("Constellation Diagram: "+str(pow(2,modOrder))+str(" QAM"))
ax.grid();
ax.legend(loc = "best")
plt.show()
10. Key Performance Indicators
Data-rate Indicators
Thoughput (Bits per seconds)
Spectral Efficency (Bits per second per Hertz)
Reliability
Block Error Rate (BLER)
Bit Error Rate (BER)
[25]:
# Reliability Metrics
codedBER = np.mean(np.abs(rbits[0][:,np.newaxis]-pdschUpperPhy.tblock1))
uncodedBER = np.mean(np.abs(codeword[0][:,0,:] - rxBits))
# bler = 1-np.mean(pdschUpPhyDec.crcCheckTBs)
bler = 1-np.mean(pdschUpPhyDec.crcCheckforCBs)
# Data rate Metrics
slotDuration = 10**(-3)*15000/(scs)
maxThroughput = (14*numRB*12)*modOrder*codeRate/slotDuration
throughput = (1-bler)*numBatches*pdschUpperPhy.tbLen1/(slotDuration*(numBatches+1))
spectalEfficiency = throughput/bandwidth
print()
print(" Throughput: "+str(throughput/10**6)+" Mbps")
print("Spectral Efficiency: "+str(spectalEfficiency)+" bits per second per Hz")
print()
print("*********************** Reliability ***********************")
print(" Block Error Rate: "+str(bler))
print(" Bit Error Rate: "+str(codedBER))
print()
Throughput: 105.0912 Mbps
Spectral Efficiency: 3.50304 bits per second per Hz
*********************** Reliability ***********************
Block Error Rate: 0.0
Bit Error Rate: 0.0
10. Quasi-realtime simulation: MIMO in Spatial Multiplexing Mode
[26]:
# function that draws each frame of the animation
numSamples = Nfft + lengthCP[1]
#
pssDetection = PSSDetection("largestPeak")
ssboffset = int((Nfft-Neff)/2+ssbRGobject.startingSubcarrierIndices)
# Channel Estimation and Equalization
chEst = ChannelEstimationAndEqualizationPDSCH(slotNumber, scramblingID, nSCID, rnti, nID,
pdschMappingType, configurationType, dmrsTypeAPosition,
maxLength, dmrsAdditionalPosition, l0, ld, l1,
startSymbol, numTargetBits, modOrder)
interpolator = "linear" # Can be "nearest" | "linear" | "cubicspline"
polyOrder = 3 # Required only for Spline, Will be ignored for other interpolators
demapMethod = "app" # Demapping method
constType = "qam" # Symbol mapping type
modOrder = modOrder # Mordulation order or number of bits per symbol
snr = 10
hard_out = False
def animate(i):
# Receive samples
# Clear buffer just to be safe
for i in range (0, 10):
raw_data = sdr.rx()
# Receive samples
rx_samples = sdr.rx()
pssDetection = PSSDetection("largestPeak")
pssPeakIndices, pssCorrelation, rN_ID2, freqOffset = pssDetection(rx_samples[1], Nfft, lengthCP = lengthCP[1],
nID2 = None, freqOffset = ssboffset)
if(pssPeakIndices > rx_samples[1].size - 140*(Nfft + lengthCP[1])):
pssPeakIndices = pssPeakIndices - 140*(Nfft + lengthCP[1])
ofdmDemodulator = OFDMDemodulator(Nfft, lengthCP[1])
pdschStartIndex = pssPeakIndices+12*(Nfft+lengthCP[1])
# pssStartIndex = pssPeakIndices[0][0]
rxGrid = ofdmDemodulator(np.array(rx_samples)[...,pdschStartIndex:(pdschStartIndex+14*numBatches*(Nfft+lengthCP[1]))])
pdschGrid = rxGrid[:,:,bwpOffset:bwpOffset+numRB*12].reshape(rank,numBatches,14,-1).transpose(1,0,2,3)
# Channel Estimation and Equalization
rsymbols = chEst(pdschGrid, interpolator)
#### Layer Demapping
layDemap = LayerDemapper(numTBs, rank)
rlmSym = layDemap(rsymbols) # received symbols
######## Demapping.
demapper = Demapper(demapMethod, constType, modOrder, hard_out = hard_out)
## Computed Received bits
rxLLRs = demapper([np.complex64(rlmSym), 1/snr])
## Descrambling
if hard_out:
scrObject = Scrambler("PDSCH", rnti, 0, nID)
rxBits = scrObject(rxLLRs)
else:
scrObject = DeScrambler("PDSCH", rnti, 0, nID)
rscrBits = scrObject(rxLLRs)
rxBits = np.where(rscrBits>0, 1, 0)
# PDSCH Upper Physical layer Decoder
pdschUpPhyDec = PDSCHDecoderUpperPhy(numTBs = numTBs, mcsIndex = mcsIndex, symbolsPerSlot= numSymbols,
numRB = numRB, numLayers = numlayers, scalingField = scalingField,
additionalOverhead = additionalOverhead, dmrsREs = dmrsREs,
enableLBRM = [False, False], pdschTable = mcsTable, rvid = [0, 0], verbose=False)
rbits = pdschUpPhyDec([rscrBits])
# Reliability Metrics
codedBER = np.mean(np.abs(rbits[0][:,np.newaxis]-pdschUpperPhy.tblock1))
uncodedBER = np.mean(np.abs(codeword[0][:,0,:] - rxBits))
# bler = 1-np.mean(pdschUpPhyDec.crcCheckTBs)
bler = 1-np.mean(pdschUpPhyDec.crcCheckforCBs)
# Data rate Metrics
throughput = (1-bler)*numBatches*pdschUpperPhy.tbLen1/(slotDuration*(numBatches+1))
spectalEfficiency = throughput/bandwidth
ax[0].clear()
ax[0].set_xlim([-2.5, 2.5])
ax[0].set_ylim([-2.5, 2.5])
ax[0].scatter(np.real(rsymbols.flatten()), np.imag(rsymbols.flatten()), s=24, label="Received Constellation")
# indices = np.random.choice(rsymbols.size, size = 10000, replace = False)
# equalizedSamples = rsymbols.flatten()[indices]
# ax[0].scatter(np.real(equalizedSamples), np.imag(equalizedSamples), s=24, label="Received Constellation")
ax[0].axhline(y=0, ls=":", c="k");
ax[0].axvline(x=0, ls=":", c="k")
ax[0].set_xlabel("Real {x}")
ax[0].set_ylabel("Imag {x}")
if modOrder ==2:
ax[0].set_title("Constellation Diagram: QPSK")
else:
ax[0].set_title("Constellation Diagram: "+str(pow(2,modOrder))+str(" QAM"))
ax[0].grid()
ax[0].legend(loc = "best")
data[0] = throughput/maxThroughput
data[1] = 1 - data[0]
print(data)
ax[1].clear()
ax[1].pie(data, wedgeprops=dict(width=0.33, edgecolor='r'), startangle=90, colors = colors[0:2])
ax[1].text(-0.5, 0.05, r'Throughput', fontsize=12)
ax[1].text(-0.5, -0.2, r'$'+str(np.round(throughput/10**6, 2))+'$ Mbps', fontsize=11)
ax[1].set_title("Throughput Performance")
data[0] = bler
data[1] = 1-bler
ax[2].clear()
ax[2].pie(data, wedgeprops=dict(width=0.33, edgecolor='k'), startangle=90, colors = colors[2:4])
ax[2].text(-0.25, 0.05, r'BLER', fontsize=12)
ax[2].text(-0.25, -0.2, r'$'+str(np.round(bler,2))+'$', fontsize=12)
ax[2].set_title("BLER Performance")
# Plot SSB
fig, ax = plt.subplots(1,3, figsize=(10, 3.3), subplot_kw=dict(aspect="equal"))
ax[0].set_aspect(True)
ax[1].set_aspect(True)
ax[2].set_aspect(True)
scale = 100
slotDuration = 10**(-3)*15000/(scs)
maxThroughput = (14*numRB*12)*modOrder*codeRate*rank/slotDuration
data = [0.75,0.25]
cmap = plt.get_cmap("tab20c")
colors = cmap(np.arange(4)*3)
#####################
# run the animation
#####################
# frames= 20 means 20 times the animation function is called.
# interval=500 means 500 milliseconds between each frame.
# repeat=False means that after all the frames are drawn, the animation will not repeat.
# Note: plt.show() line is always called after the FuncAnimation line.
anim = animation.FuncAnimation(fig, animate, frames=100, interval=1, repeat=False, blit=True)
# saving to mp4 using ffmpeg writer
plt.show()
anim.save("MIMO_Constellation.gif", fps = 10)
# writervideo = animation.FFMpegWriter(fps=60)
# anim.save('MIMO_Constellation.mp4', writer=writervideo)
[ ]: