├── images └── SDecoder.png ├── src ├── frontendfunc.py ├── mimobasicfunc.py ├── plotfunc.py ├── mimoclasses.py ├── measurefunc.py ├── siggenfunc.py ├── combiningfunc.py ├── equalizers.py ├── moddemodfunc.py ├── channels.py ├── main_detection.py └── detectfunc.py ├── LICENSE ├── README.md └── examples └── waterfilling.py /images/SDecoder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/r4tn3sh/MIMO_detection/HEAD/images/SDecoder.png -------------------------------------------------------------------------------- /src/frontendfunc.py: -------------------------------------------------------------------------------- 1 | # TODO Add following front-end impairments 2 | # CFO, Phase noise, IQ imbalance, DC offset, sampling clock offset, 3 | # Additive noise. 4 | -------------------------------------------------------------------------------- /src/mimobasicfunc.py: -------------------------------------------------------------------------------- 1 | import os 2 | import math 3 | import numpy as np 4 | # -------- Basics --------- 5 | def isSquare (m): return all (len (row) == len (m) for row in m) 6 | 7 | -------------------------------------------------------------------------------- /src/plotfunc.py: -------------------------------------------------------------------------------- 1 | import matplotlib.pyplot as plt 2 | from numpy import array 3 | 4 | # ---------- PLOTS ------------ 5 | def plotConstell(y): 6 | """ 7 | Plots the constellation of given samples. 8 | """ 9 | yr = y.real#[a.real for a in y] 10 | yi = y.imag#[a.imag for a in y] 11 | nrow, ncol = y.shape 12 | #p = plt.figure() 13 | p, ax = plt.subplots() 14 | for idx in range(nrow): 15 | plt.scatter(array(yr[idx]), array(yi[idx]), s=3, label='path '+str(idx+1)) 16 | ax.legend() 17 | return p 18 | -------------------------------------------------------------------------------- /src/mimoclasses.py: -------------------------------------------------------------------------------- 1 | import math 2 | 3 | from dataclasses import dataclass 4 | from enum import Enum 5 | from enum import IntEnum 6 | 7 | class Equalizer(Enum): 8 | ZF = 1 9 | MMSE = 2 10 | 11 | class Channel(IntEnum): 12 | NONE = 1 13 | RAND_UNIT = 2 # Completely stochastic 14 | RAND_UNIT_GOOD = 3 15 | RAND_UNIT_BAD = 4 16 | FSPL = 5 # Free space path loss 17 | RAYLEIGH = 6 18 | RICIAN = 7 19 | 20 | @dataclass 21 | class ChParam: 22 | samprate: int 23 | delay: float 24 | doppler: float 25 | seed: int 26 | -------------------------------------------------------------------------------- /src/measurefunc.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from moddemodfunc import normFactor 3 | 4 | def getEVM (sampIn, sampOut, mod): 5 | if sampIn.shape == sampOut.shape: 6 | err = (sampIn-sampOut) 7 | evmlin = np.mean(np.square(np.absolute(err))) 8 | # Since the constellation already has normalized power, no need to 9 | # further normalize for EVM calculations. 10 | evm = 10*np.log10(evmlin) 11 | # There is maximum value of EVM that can be resolved by the signal 12 | # analyzers in real test environment. 13 | maxevm = -10*np.log10(normFactor(mod)) 14 | return min(evm, maxevm) 15 | else: 16 | print(sampIn.shape, sampOut.shape) 17 | raise ValueError('size of both parameters should be same') 18 | 19 | 20 | def getSER (sampIn, sampOut): 21 | if sampIn.shape == sampOut.shape: 22 | nofsamp_err = ((sampIn-sampOut)>0).sum(dtype='float') 23 | return nofsamp_err 24 | else: 25 | print(sampIn.shape, sampOut.shape) 26 | raise ValueError('size of both parameters should be same') 27 | -------------------------------------------------------------------------------- /src/siggenfunc.py: -------------------------------------------------------------------------------- 1 | from moddemodfunc import * 2 | 3 | def generateIQ(Nt, N, mod, tx_mode): 4 | # Nt = no. of antennas 5 | # N = number of samples 6 | # mod = modulation scheme, BPSK, QPSK, ..., 1024QAM 7 | # BPSK=1, QPSK=2, ... 1024QAM=10 8 | # TODO: improve the time-consuming loop 9 | c = np.asmatrix([[np.complex(0,0)]*N]*Nt) 10 | if tx_mode == 0: #sending same data in all antennas 11 | for j in range(N): 12 | # get random bits 13 | b = np.random.randint(2, size=mod) 14 | # encode bits into samples 15 | temp_c = qammod(b, mod) 16 | for i in range(Nt): 17 | c[i,j] = temp_c 18 | elif tx_mode == 1: #sending different data in each antenna 19 | for i in range(Nt): 20 | for j in range(N): 21 | # get random bits 22 | b = np.random.randint(2, size=mod) 23 | # encode bits into samples 24 | c[i,j] = qammod(b, mod) 25 | # Normalize the signal to unit power 26 | c = c/np.sqrt(normFactor(mod)) 27 | return c 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Ratnesh Kumbhkar 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MIMO Wireless Communication 2 | ## Description 3 | The goal of this project is to create a simulation of a basic baseband communication with MIMO using Python. The following sub-goals are expected to be achieved. 4 | * Bits to sample mapping based on the modulation scheme. Currently supporting BPSK, QPSK, 16QAM, 64QAM, 256QAM, and 1024QAM. 5 | * Modeling various channel matrices, H. 6 | * Implementation of AWGN channel. 7 | * Channel estimation algorithms. 8 | * Sample detection algorithms. 9 | * Useful plotting capability 10 | * Time and frequency domain representation 11 | * Constellation 12 | 13 | ## Currently implemented 14 | * Channels: 15 | * AWGN 16 | * Random fading channel based on condition number 17 | * Equalizer: Zero-Forcing, MMSE 18 | * Measurement of Symbol error rate and EVM 19 | * Basic Sphere decoding with Babai radius estimate 20 | * Diversity ratio combining 21 | * Equal gain combining 22 | * Selection combining 23 | * Maximal ratio combining 24 | 25 | ## TODO 26 | * Improve the efficiency of the sphereDecoder. Check if the Babai estimate needs to be calculated for all samples. 27 | 28 | 29 | ## Required Python libraries: 30 | * Numpy 31 | * Matplotlib 32 | * Argparse 33 | * Enum 34 | -------------------------------------------------------------------------------- /src/combiningfunc.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pdb 3 | from scipy import linalg 4 | from moddemodfunc import normFactor 5 | 6 | # All of the following combiner assume that the current MIMO system is used for 7 | # diversity gain. All Nr antennas receive the same data, and channel information 8 | # is either not available or not used by the trasnmitter. The transmitter is 9 | # using only one antenna for this particular stream. Hence it is a 1xNr system. 10 | 11 | def equalRatioCombine(_y,H): 12 | ''' 13 | equalRatioCombine() is used when the receiver uses no information about the 14 | channel. Goal is faster processing. 15 | Input: (Nr x N) 16 | Output: (1 x N) 17 | ''' 18 | return(np.sum(_y,0)/np.sum(H)) 19 | 20 | def maxRatioCombine(_y, H): 21 | ''' 22 | maxRatioCombine() is used when the receiver uses the information about the 23 | channel. Goal is to maximize SNR. 24 | Input: (Nr x N) 25 | Output: (1 x N) 26 | ''' 27 | return(((H.H)*_y)/(linalg.norm(H))**2) 28 | 29 | def selectRatioCombine(_y, H): 30 | ''' 31 | selectRatioCombine() is used when the receiver uses the channel with highest 32 | strength. Goal is faster processing. 33 | Input: (Nr x N) 34 | Output: (1 x N) 35 | ''' 36 | idx = np.argmax(H) 37 | return(_y[idx,]/H[idx]) 38 | -------------------------------------------------------------------------------- /src/equalizers.py: -------------------------------------------------------------------------------- 1 | import math 2 | import numpy as np 3 | 4 | from scipy import linalg 5 | from mimoclasses import Equalizer 6 | from mimobasicfunc import * 7 | 8 | # ---------- EQUALIZER ------------ 9 | def getZfEqualizer(H): 10 | """ 11 | Generates the zero forcing equalizer for a given channel matrix H. 12 | Estimate from this equalization is also called "Babai estimate". 13 | """ 14 | if isSquare(H): 15 | Eq = linalg.inv(H) 16 | else: 17 | Eq = linalg.pinv(H) 18 | return Eq 19 | 20 | def getMmseEqualizer(H, Cx, Cz): 21 | """ 22 | Generates the MMSE equalizer. 23 | (H_h Cz(-1) H + Cx(-1))(-1) H_h 24 | Cx = Covariance matrix of i/p signal 'x' across Nt antennas 25 | Cz = Covariance matrix of noise 'z' across Nr antennas 26 | """ 27 | Hh = H.conj().T 28 | tM = Hh*(linalg.inv(Cz)*H) + linalg.inv(Cx) 29 | return linalg.inv(tM)*Hh 30 | 31 | def decisionFeedbackEqualizer(H): 32 | return 0 33 | 34 | def getEqualizer(H, Cx, Cz, t): 35 | """ 36 | Main function to generate the channel equalizer. 37 | """ 38 | print(t) 39 | if t == Equalizer.ZF: 40 | Eq = getZfEqualizer(H) 41 | elif t == Equalizer.MMSE: 42 | Eq = getMmseEqualizer(H, Cx, Cz) 43 | else: 44 | raise ValueError('Choose a valid equalizer type.') 45 | return Eq 46 | -------------------------------------------------------------------------------- /src/moddemodfunc.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | 4 | def qammod(b, mod): 5 | """ 6 | This function takes specific set of bits and maps them into a desired QAM 7 | modulation. 8 | Real part : +-1 +-3 +-5 ....... 9 | Imag part : +-1 +-3 +-5 ....... 10 | """ 11 | if b.size != mod: 12 | print('number of bits do not match the modulation scheme') 13 | return -1 14 | elif mod not in [2, 4, 6, 8, 10]: 15 | print('Currently supporting only QPSK, 16QAM, 64QAM, 256QAM, 1024QAM') 16 | return -1 17 | else: 18 | dims = np.power(2,mod//2) # one side of the square 19 | #coord = qamcoord[0:dims] 20 | xdim = 0 21 | ydim = 0 22 | for i in range(0, mod//2): 23 | xdim = xdim+b[i]*np.power(2,i) 24 | ydim = ydim+b[i+mod//2]*np.power(2,i) 25 | return np.complex(dims-(2*xdim+1), dims-(2*ydim+1)) 26 | 27 | def normFactor(mod): 28 | if mod not in [2, 4, 6, 8, 10]: 29 | print('Currently supporting only QPSK, 16QAM, 64QAM, 256QAM, 1024QAM') 30 | return -1 31 | dims = np.power(2,mod//2) 32 | ene_sum = 0 33 | for i in range(0,dims//2): 34 | for j in range(0, dims//2): 35 | ene_sum = ene_sum+np.power(2*i+1,2)+np.power(2*j+1,2) 36 | ene_sum = ene_sum/(np.power(dims,2)//4) 37 | return ene_sum 38 | -------------------------------------------------------------------------------- /src/channels.py: -------------------------------------------------------------------------------- 1 | import os 2 | import math 3 | import numpy as np 4 | from scipy import linalg 5 | from mimoclasses import Channel 6 | 7 | # ---------- CHANNEL ------------ 8 | def awgnChannel(x,N0): 9 | """ 10 | Generates the AWGN channel 11 | Input parameters 12 | - Signal x (should be avg unit power) 13 | - Noise variance N0 14 | Other parameters 15 | - Thermal noise = -174dBm/Hz 16 | - Variance N0/2 per real symbol 17 | """ 18 | N0_r = np.random.normal(0, N0/2, x.shape) 19 | N0_i = np.random.normal(0, N0/2, x.shape) 20 | return (x+N0_r+1j*N0_i) 21 | 22 | 23 | def generateChMatrix(Nr,Nt,chtype): 24 | # Current threshold for good/bad condition number 25 | cond_num_thr = 5 26 | 27 | # Condition number based channel will not make sense for Nr=Nt=1 28 | # since the condition number is always 1 29 | if Nr==1 and Nt==1: 30 | if chtype > Channel.RAND_UNIT: 31 | chtype = Channel.RAND_UNIT 32 | 33 | # TODO: support different channel models in future. 34 | if chtype == Channel.NONE: 35 | if Nr==Nt: 36 | H_r = np.identity(Nr) 37 | H_i = np.zeros((Nr,Nt)) 38 | else: 39 | raise ValueError('For channel type-'+str(chtype)+', Nr=Nt is needed.') 40 | return -1 41 | elif chtype == Channel.RAND_UNIT: 42 | # Using complex gaussian random variable 43 | # Real and Img part: mean = 0, variance = 1 44 | H_r = np.random.normal(0, 1, size=(Nr,Nt)) 45 | H_i = np.random.normal(0, 1, size=(Nr,Nt)) 46 | elif chtype == Channel.RAND_UNIT_GOOD: 47 | cond_num = cond_num_thr + 1 48 | while cond_num > cond_num_thr: 49 | # Using complex gaussian random variable 50 | # Real and Img part: mean = 0, variance = 1 51 | H_r = np.random.normal(0, 1, size=(Nr,Nt)) 52 | H_i = np.random.normal(0, 1, size=(Nr,Nt)) 53 | cond_num = np.linalg.cond(np.asmatrix((H_r + 1j*H_i))) 54 | elif chtype == Channel.RAND_UNIT_BAD: 55 | cond_num = 0 56 | while cond_num < cond_num_thr: 57 | # Using complex gaussian random variable 58 | # Real and Img part: mean = 0, variance = 1 59 | H_r = np.random.normal(0, 1, size=(Nr,Nt)) 60 | H_i = np.random.normal(0, 1, size=(Nr,Nt)) 61 | cond_num = np.linalg.cond(np.asmatrix((H_r + 1j*H_i))) 62 | else: 63 | raise ValueError('Channel type-'+str(chtype)+' is not supported.') 64 | return -1 65 | 66 | cond_num = np.linalg.cond(np.asmatrix((H_r + 1j*H_i))) 67 | print('Condition number of the generated channel: '+str(cond_num)) 68 | return np.asmatrix((H_r + 1j*H_i)) 69 | -------------------------------------------------------------------------------- /src/main_detection.py: -------------------------------------------------------------------------------- 1 | import os 2 | import math 3 | #import pdb 4 | import numpy as np 5 | import matplotlib.pyplot as plt 6 | import argparse 7 | 8 | from scipy import linalg 9 | from channels import * 10 | from equalizers import * 11 | from mimoclasses import Equalizer 12 | from mimoclasses import Channel 13 | from mimobasicfunc import * 14 | from plotfunc import * 15 | from moddemodfunc import * 16 | from siggenfunc import * 17 | from detectfunc import * 18 | from measurefunc import * 19 | from combiningfunc import * 20 | 21 | 22 | # ---------- MAIN ------------ 23 | def main(): 24 | parser = argparse.ArgumentParser() 25 | parser.add_argument("N", help="Number of complex samples.", 26 | type=int) 27 | parser.add_argument("mod", help="Number of bits per sample.", 28 | type=int, choices=[2,4,6,8,10], metavar='Mod') 29 | parser.add_argument("--snr", help="Signal to noise ratio (dB).", 30 | type=float, nargs='?',default = 10, metavar='SNR') 31 | parser.add_argument("--Nr", help="Number of RX antennas.", 32 | type=int, nargs='?',default = 1, metavar='Nr') 33 | parser.add_argument("--Nt", help="Number of TX antennas.", 34 | type=int, nargs='?',default = 1, metavar='Nt') 35 | parser.add_argument("--txmode", help="Transmission moTX mode. Use multiple antennas for channel diversity or multiple streams.", 36 | type=int, nargs='?', choices=[0,1], default = 0, metavar='TX mode') 37 | args = parser.parse_args() 38 | N = args.N 39 | mod = args.mod 40 | snr = args.snr 41 | Nr = args.Nr 42 | Nt = args.Nt 43 | tx_mode = args.txmode 44 | N0 = 1/np.power(10,snr/10) 45 | 46 | #if Nt != Nr: 47 | # raise ValueError('Currently only Nr=Nt supported.') 48 | 49 | NoS = min(Nr, Nt) # maximum number of possible streams 50 | H = generateChMatrix(Nr,Nt,Channel.RAND_UNIT_GOOD) 51 | print('Condition number of the generated channel: '+str(np.linalg.cond(H))) 52 | 53 | # generate the baseband IQ signal 54 | X = generateIQ(Nt, N, mod, tx_mode) 55 | #plotConstell(x) 56 | 57 | # Starting with diversity gain 58 | # NOTE: Replicate same signal on all transmit antennas 59 | Xin = X#np.asmatrix([x]*Nt) 60 | Cx = np.var(X)*np.identity(Nt) #all antennas receiving same data 61 | pltx = plotConstell(Xin) 62 | plt.title('Transmit signal constellation') 63 | 64 | 65 | # Pass through the channel 66 | Xout = H*Xin 67 | 68 | # Adding white gaussian noise 69 | # The signal should have unit power. (?) 70 | Y = awgnChannel(Xout,N0) 71 | print('Size of received signal (Nr x N): '+str(Y.shape)) 72 | plty = plotConstell(Y) 73 | plt.title('Received signal constellation') 74 | 75 | # Covariance matrix of noise. Currently assuming uncorrelated across antennas. 76 | Cz = np.identity(Nr) 77 | 78 | Eq = getEqualizer(H, Cx, Cz, Equalizer.ZF) 79 | t_Yhat = Eq*Y 80 | print('Size of Equalized signal (Nt x N): '+str(t_Yhat.shape)) 81 | 82 | # NOTE: Following is done assuming all Nt antennas had the same data and RX 83 | # diversity is being exploited 84 | # Yhat = np.mean(t_Yhat,0) 85 | 86 | Yhat = t_Yhat 87 | print('Size of Equalized signal (Nt x N): '+str(Yhat.shape)) 88 | pltyhat = plotConstell(Yhat) 89 | plt.title('Equalized signal constellation') 90 | 91 | Xrec = mlDetectionIQ(Yhat, mod) 92 | #plotConstell(Xrec) 93 | 94 | nofsamp_err = getSER(X,Xrec) 95 | evm = getEVM(X,Yhat,mod) 96 | print('SER = '+str(nofsamp_err/N/Nt)) 97 | print('EVM = '+str(evm)+' dB') 98 | #plt.show() 99 | #sphereDecoder(H, X, Y, mod, 'basic', 4) 100 | if Nt==1: 101 | print('Using diversity gain.') 102 | Ycomb = equalRatioCombine(Y, H) 103 | Ycomb = maxRatioCombine(Y, H) 104 | Ycomb = selectRatioCombine(Y, H) 105 | 106 | if __name__ == "__main__": 107 | main() 108 | -------------------------------------------------------------------------------- /examples/waterfilling.py: -------------------------------------------------------------------------------- 1 | import sys 2 | sys.path.insert(1, '../src') 3 | 4 | import os 5 | import math 6 | #import pdb 7 | import numpy as np 8 | import matplotlib.pyplot as plt 9 | from scipy import linalg 10 | import argparse 11 | 12 | from channels import * 13 | from equalizers import * 14 | from mimoclasses import Equalizer 15 | from mimoclasses import Channel 16 | from mimobasicfunc import * 17 | from plotfunc import * 18 | from moddemodfunc import * 19 | from siggenfunc import * 20 | from detectfunc import * 21 | from measurefunc import * 22 | from combiningfunc import * 23 | 24 | 25 | def main(): 26 | parser = argparse.ArgumentParser() 27 | parser.add_argument("N", help="Number of complex samples.", 28 | type=int) 29 | parser.add_argument("mod", help="Number of bits per sample.", 30 | type=int, choices=[2,4,6,8,10], metavar='Mod') 31 | parser.add_argument("--snr", help="Signal to noise ratio (dB).", 32 | type=float, nargs='?',default = 10, metavar='SNR') 33 | parser.add_argument("--Nr", help="Number of RX antennas.", 34 | type=int, nargs='?',default = 3, metavar='Nr') 35 | parser.add_argument("--Nt", help="Number of TX antennas.", 36 | type=int, nargs='?',default = 3, metavar='Nt') 37 | parser.add_argument("--txmode", help="Transmission mode. Use multiple antennas for channel diversity or multiple streams.", 38 | type=int, nargs='?', choices=[0,1], default = 0, metavar='TX mode') 39 | args = parser.parse_args() 40 | N = args.N 41 | mod = args.mod 42 | snr = args.snr 43 | Nr = args.Nr 44 | Nt = args.Nt 45 | #tx_mode = args.txmode 46 | tx_mode = 0 47 | # Signal in each path is always assumed to have unit power 48 | Pnoise = 1/np.power(10,snr/10) 49 | 50 | #if Nt != Nr: 51 | # raise ValueError('Currently only Nr=Nt supported.') 52 | 53 | NoS = min(Nr, Nt) # maximum number of possible streams 54 | H = generateChMatrix(Nr,Nt,Channel.RAND_UNIT_BAD) 55 | print('Condition number of the generated channel: '+str(np.linalg.cond(H))) 56 | uHH, egHH, vhHH = np.linalg.svd(H.H*H) 57 | uH, egH, vhH = np.linalg.svd(H) 58 | print(egHH) 59 | P0 = 1*Nt 60 | 61 | for loop in range(Nt,0,-1): 62 | print('------------------') 63 | nu_inv = (P0+Pnoise*np.sum(np.reciprocal(egHH[0:loop])))/loop 64 | print('nu_inv: '+str(nu_inv)) 65 | pow_alloc = nu_inv - Pnoise*np.reciprocal(egHH[0:loop]); 66 | print(pow_alloc, np.sum(pow_alloc)) 67 | nmode = np.sum(nu_inv - Pnoise*np.reciprocal(egHH[0:loop])>0) 68 | if (nmode == loop): 69 | break 70 | print('noise power: '+str(Pnoise)) 71 | 72 | pow_alloc = np.concatenate([pow_alloc, np.zeros(Nt-np.size(pow_alloc))]) 73 | 74 | print(np.arange(Nt)) 75 | fig, ax = plt.subplots() 76 | ax.bar(range(Nt),pow_alloc) 77 | ax.set_title('Power allocation to '+str(Nt)+' modes') 78 | #labels = np.linspace(1, Nt,Nt) 79 | #print(labels) 80 | ax.set_xticklabels(labels) 81 | # generate the baseband IQ signal 82 | X = generateIQ(Nt, N, mod, tx_mode) 83 | #plotConstell(x) 84 | 85 | Xin = np.multiply((linalg.pinv(vhH)*X), np.sqrt(pow_alloc[:,np.newaxis]))#np.asmatrix([x]*Nt) 86 | Cx = np.var(X)*np.identity(Nt) #all antennas receiving same data 87 | pltx = plotConstell(Xin) 88 | plt.title('Transmit signal constellation') 89 | 90 | 91 | # Pass through the channel 92 | Xout = H*Xin 93 | 94 | # Adding white gaussian noise 95 | # The signal should have avg unit power. (?) 96 | Y = awgnChannel(Xout,Pnoise) 97 | Yrx = linalg.pinv(uH)*Y 98 | print('Size of received signal (Nr x N): '+str(Yrx.shape)) 99 | plty = plotConstell(Yrx) 100 | plt.title('Received signal constellation') 101 | 102 | Yhat = np.multiply(Yrx, np.reciprocal(egH[:,np.newaxis])) 103 | print('Size of Equalized signal (Nt x N): '+str(Yhat.shape)) 104 | pltyhat = plotConstell(Yhat) 105 | plt.title('Equalized signal constellation') 106 | 107 | plt.show() 108 | return 109 | 110 | evm = getEVM(X,Yhat,mod) 111 | print('EVM = '+str(evm)+' dB') 112 | 113 | plt.show() 114 | 115 | if __name__ == "__main__": 116 | main() 117 | -------------------------------------------------------------------------------- /src/detectfunc.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pdb 3 | from scipy import linalg 4 | from moddemodfunc import normFactor 5 | 6 | def mlDetectionIQ(_y, mod): 7 | """ 8 | Maximum Likerlihood detector 9 | _y = complex signal 10 | mod = modulation scheme, BPSK, QPSK, ..., 1024QAM 11 | BPSK=1, QPSK=2, ... 1024QAM=10 12 | """ 13 | dims = np.power(2,mod//2) # one side of the square 14 | constellind = [2*x-dims+1 for x in range(dims)]/np.sqrt(normFactor(mod)) 15 | constellarr = [np.complex(constellind[i], constellind[j]) for i in range(dims) for j in range(dims)] 16 | 17 | # Maximum Likerlihood detection 18 | #z = [constellarr[np.argmin(np.abs(constellarr - _y[i]))] for i in range(len(_y))] 19 | z = [constellarr[np.argmin(np.abs(constellarr - x))] for x in np.nditer(_y)] 20 | return np.asmatrix(z).reshape(_y.shape) 21 | 22 | def sphereDecoder(Hcplx, sampIn, ycplx, mod, method, d=np.inf): 23 | """ 24 | Sphere decoding. 25 | Default value of initial radius is infinity. Radius can also be provided 26 | as a parameter. Or, it can be calulated by zeroforcing. 27 | Requires QR factorization of the channel matrix 28 | 29 | [1]T. Kailath, H. Vikalo, and B. Hassibi, “MIMO receive algorithms,” in 30 | Space-Time Wireless Systems, 1st ed., H. Bölcskei, D. Gesbert, C. B. 31 | Papadias, and A.-J. van der Veen, Eds. Cambridge University Press, 2001, 32 | pp. 302–321. 33 | """ 34 | n, m = Hcplx.shape 35 | rY, cY = ycplx.shape 36 | if n < m : 37 | raise ValueError('The channel matrix dimensions are not supported') 38 | 39 | #Q, Rtemp = linalg.qr(H) 40 | #R = Rtemp[0:m, 0:m] 41 | if method == 'zf': 42 | if isSquare(H): 43 | Eq = linalg.inv(H) 44 | else: 45 | Eq = linalg.pinv(H) 46 | elif method == 'basic': 47 | # radius d should be provided as parameter 48 | # divide Q(n x n) into [Q1 Q2], where Q1(n x m) and Q2(n x n-m) 49 | #Q1 = Q[0:n, 0:m] 50 | #Q2 = Q[0:n, m:n] 51 | H1 = (np.concatenate((Hcplx.real, Hcplx.imag), axis=0)) 52 | H2 = (np.concatenate((-Hcplx.imag, Hcplx.real), axis=0)) 53 | H = np.concatenate((H1,H2), axis = 1) 54 | 55 | n, m = H.shape 56 | Q, Rtemp = linalg.qr(H) 57 | R = Rtemp[0:m, 0:m] 58 | Q1 = Q[0:n, 0:m] 59 | Q2 = Q[0:n, m:n] 60 | #---------- Estimation of radius 'd' (optional) ------------------------ 61 | ytemp = np.concatenate((ycplx[:,0].real, ycplx[:,0].imag), axis=0) 62 | samp = np.concatenate((sampIn[:,0].real, sampIn[:,0].imag), axis=0)*np.sqrt(normFactor(mod)) 63 | yhat = Q1.transpose()*ytemp 64 | xtemp = (linalg.inv(R)*yhat)*np.sqrt(normFactor(mod)) 65 | # Real part : +-1 +-3 +-5 ....... 66 | # Imag part : +-1 +-3 +-5 ....... 67 | # Rounding will be more involved 68 | xhat = np.rint(xtemp) 69 | # estimated inital value of radius (Babai estimate) 70 | dhat = R*xhat-yhat 71 | dest = 2*np.ceil(linalg.norm(dhat[0:m,0:1],2)) 72 | print('Babai radius estimate = '+str(dest)) 73 | #return 74 | #print(ynew) 75 | #----------------------------------------------------------------------- 76 | # TODO: Starting radius Babai estimate 77 | #dest = 100 78 | errcount = 0 79 | ctln_count = 0 80 | samp_n, samp_m = sampIn.shape 81 | output = np.zeros((samp_n, samp_m))+1j*np.zeros((samp_n, samp_m)) 82 | for idx in range(cY): 83 | diter = np.zeros((m,1)) 84 | _y = np.zeros((m,1)) 85 | yiter = np.zeros((m,1)) 86 | ubound = np.zeros((m,1)) 87 | _s = np.zeros((m,1)) 88 | final_s = np.zeros((m,1)) 89 | # multiply with np.sqrt(normFactor(mod)) to keep +-1 +-3 +-5 ....... 90 | ytemp = np.concatenate((ycplx[:,idx].real, ycplx[:,idx].imag), axis=0)*np.sqrt(normFactor(mod)) 91 | samp = np.concatenate((sampIn[:,idx].real, sampIn[:,idx].imag), axis=0)*np.sqrt(normFactor(mod)) 92 | endflag = 0 93 | slnfound = 0 94 | sdstate = 1 95 | loopcnt = 0 96 | ctln_d = np.inf 97 | minradius = dest*np.ones((m,1)) 98 | while True: 99 | if sdstate==1: 100 | # Initialization 101 | k = m-1 102 | diter[m-1] = np.sqrt(dest**2 - (linalg.norm(np.transpose(Q2)*ytemp))**2) 103 | _y = Q1.transpose()*ytemp 104 | yiter[m-1] = _y[m-1] 105 | sdstate = 2 106 | elif sdstate == 2: 107 | # calculate the bounds based on radius 108 | bound1 = np.divide((diter[k]+yiter[k]),R[k,k]) 109 | bound1 = (2*(np.floor((bound1+1)//2)+1)-1) 110 | bound2 = np.divide((-diter[k]+yiter[k]),R[k,k]) 111 | bound2 = (2*(np.floor((bound2+1)//2)+1)-1) 112 | _s[k] = max(min(bound1,bound2),-mod+1)-2 113 | ubound[k] = min(max(bound1,bound2),mod-1) 114 | sdstate = 3 115 | elif sdstate == 3: 116 | if _s[k]+2 > ubound[k]: 117 | sdstate = 4 118 | else: 119 | _s[k] = _s[k]+2 120 | sdstate = 5 121 | elif sdstate == 4: 122 | k = k+1 123 | if k==m: 124 | # The algorithm is complete 125 | endflag = 1 126 | break 127 | else: 128 | sdstate = 3 129 | elif sdstate == 5: 130 | loopcnt = loopcnt+1 131 | if k == 0: 132 | sdstate = 6 133 | else: 134 | k = k-1 135 | yiter[k] = _y[k] - np.dot(R[k+1:k+2,k+1:m],_s[k+1:m]) 136 | dtemp = diter[k+1]**2 - (yiter[k+1] - R[k+1,k+1]*_s[k+1])**2 137 | if dtemp<0: 138 | #dtemp = minradius[k]**2#diter[k+1]**2 139 | sdstate = 3 140 | else: 141 | diter[k] = np.sqrt(dtemp) 142 | sdstate = 2 143 | elif sdstate == 6: 144 | # Constellation point found within the radius bounds 145 | # TODO: Simpler method of distance calculation 146 | #t_ctln_d = linalg.norm(ytemp-H*_s) 147 | t_ctln_d = linalg.norm(_y-np.dot(R,_s)) #diagonal matrix should be faster 148 | if t_ctln_d<=ctln_d: 149 | # print(_s.transpose()) 150 | # print('distance='+str(t_ctln_d)) 151 | # print(diter.transpose()) 152 | # print('Alt distance='+str(diter[m-1]**2-diter[0]**2+(_y[0]-R[0][0]*_s[0])**2)) 153 | # print(_y.transpose()) 154 | # print(np.multiply(np.diag(R),_s.transpose())) 155 | # print(diter[m-1]**2+(linalg.norm(Q2.transpose()*ytemp))**2) 156 | # print('--------------------') 157 | #if t_ctln_d<1: 158 | # pdb.set_trace() 159 | # print(diter[m-1]**2+(linalg.norm(Q2.transpose()*ytemp))**2) 160 | final_s = np.copy(_s) 161 | ctln_d = t_ctln_d 162 | ctln_count = ctln_count+1 163 | sdstate = 3 164 | else: 165 | break 166 | #print(final_s.T) 167 | #print(samp.T) 168 | #pdb.set_trace() 169 | output[:,idx:idx+1] = (final_s[0:samp_n]+1j*final_s[samp_n:2*samp_n])/np.sqrt(normFactor(mod)) 170 | #print(ctln_count) 171 | #print(ctln_d) 172 | #print(diter) 173 | #print('Error count : '+str(errcount)) 174 | print('Number of samples in error : '+str(np.sum(output!=sampIn))) 175 | print('Total number of samples: '+str(np.size(sampIn))) 176 | return output 177 | --------------------------------------------------------------------------------