Module DigiCommPy.modem

Module: DigiCommPy.modem.py

@author: Mathuranathan Viswanathan Created on Aug 6, 2019

Expand source code
"""
Module: DigiCommPy.modem.py

@author: Mathuranathan Viswanathan
Created on Aug 6, 2019
"""
import numpy as np
import abc
import matplotlib.pyplot as plt

class Modem:
    __metadata__ = abc.ABCMeta
    # Base class: Modem
    # Attribute definitions:
    #    self.M : number of points in the MPSK constellation
    #    self.name: name of the modem : PSK, QAM, PAM, FSK
    #    self.constellation : reference constellation
    #    self.coherence : only for 'coherent' or 'noncoherent' FSK
    def __init__(self,M,constellation,name,coherence=None): #constructor
        if (M<2) or ((M & (M -1))!=0): #if M not a power of 2
            raise ValueError('M should be a power of 2')
        if name.lower()=='fsk':
            if (coherence.lower()=='coherent') or (coherence.lower()=='noncoherent'):
                self.coherence = coherence
            else:
                raise ValueError('Coherence must be \'coherent\' or \'noncoherent\'')
        else:
            self.coherence = None
        self.M = M # number of points in the constellation
        self.name = name # name of the modem : PSK, QAM, PAM, FSK
        self.constellation = constellation # reference constellation
    
    def plotConstellation(self):
        """
        Plot the reference constellation points for the selected modem
        """
        from math import log2
        if self.name.lower()=='fsk':
            return 0 # FSK is multi-dimensional difficult to visualize 
        
        fig, axs = plt.subplots(1, 1)
        axs.plot(np.real(self.constellation),np.imag(self.constellation),'o')        
        for i in range(0,self.M):
            axs.annotate("{0:0{1}b}".format(i,int(log2(self.M))), (np.real(self.constellation[i]),np.imag(self.constellation[i])))
        
        axs.set_title('Constellation');
        axs.set_xlabel('I');axs.set_ylabel('Q');fig.show()
    
    def modulate(self,inputSymbols):
        """
            Modulate a vector of input symbols (numpy array format) using the chosen
             modem. The input symbols take integer values in the range 0 to M-1.
        """
        if isinstance(inputSymbols,list):
            inputSymbols = np.array(inputSymbols)
        
        if  not (0 <= inputSymbols.all() <= self.M-1):
            raise ValueError('Values for inputSymbols are beyond the range 0 to M-1')
        
        modulatedVec = self.constellation[inputSymbols]
        return modulatedVec #return modulated vector
    
    def demodulate(self,receivedSyms):
        """
            Demodulate a vector of received symbols using the chosen modem.            
        """        
        if isinstance(receivedSyms,list):
            receivedSyms = np.array(receivedSyms)
            
        detectedSyms= self.iqDetector(receivedSyms)
        return detectedSyms
    
    def iqDetector(self,receivedSyms):
        """
        Optimum Detector for 2-dim. signals (ex: MQAM,MPSK,MPAM) in IQ Plane
        Note: MPAM/BPSK are one dimensional modulations. The same function can be 
        applied for these modulations since quadrature is zero (Q=0)
        
        The function computes the pair-wise Euclidean distance of each point in the
        received vector against every point in the reference constellation. It then
        returns the symbols from the reference constellation that provide the
        minimum Euclidean distance.
        
        Parameters:
            receivedSyms : received symbol vector of complex form
        Returns:
            detectedSyms:decoded symbols that provide the minimum Euclidean distance        
        """
        from scipy.spatial.distance import cdist
        # received vector and reference in cartesian form
        XA = np.column_stack((np.real(receivedSyms),np.imag(receivedSyms))) 
        XB=np.column_stack((np.real(self.constellation),np.imag(self.constellation)))
        
        d = cdist(XA,XB,metric='euclidean') #compute pair-wise Euclidean distances
        detectedSyms = np.argmin(d,axis=1)#indices corresponding minimum Euclid. dist.
        return detectedSyms

class PAMModem(Modem):
    # Derived class: PAMModem
    def __init__(self, M):
        m = np.arange(0,M) #all information symbols m={0,1,...,M-1}
        constellation = 2*m+1-M + 1j*0  # reference constellation        
        Modem.__init__(self, M, constellation, name='PAM') #set the modem attributes
            
class PSKModem(Modem):
    # Derived class: PSKModem
    def __init__(self, M):        
        #Generate reference constellation
        m = np.arange(0,M) #all information symbols m={0,1,...,M-1}
        I = 1/np.sqrt(2)*np.cos(m/M*2*np.pi)
        Q = 1/np.sqrt(2)*np.sin(m/M*2*np.pi)
        constellation = I + 1j*Q #reference constellation        
        Modem.__init__(self, M, constellation, name='PSK') #set the modem attributes
        
class QAMModem(Modem):
    # Derived class: QAMModem
    def __init__(self,M):
        if (M==1) or (np.mod(np.log2(M),2)!=0): # M not a even power of 2
            raise ValueError('Only square MQAM supported. M must be even power of 2')
        
        n = np.arange(0,M) # Sequential address from 0 to M-1 (1xM dimension)
        a = np.asarray([x^(x>>1) for x in n]) #convert linear addresses to Gray code
        D = np.sqrt(M).astype(int) #Dimension of K-Map - N x N matrix
        a = np.reshape(a,(D,D)) # NxN gray coded matrix
        oddRows=np.arange(start = 1, stop = D ,step=2) # identify alternate rows
        a[oddRows,:] = np.fliplr(a[oddRows,:]) #Flip rows - KMap representation
        nGray=np.reshape(a,(M)) # reshape to 1xM - Gray code walk on KMap
        
        #Construction of ideal M-QAM constellation from sqrt(M)-PAM
        (x,y)=np.divmod(nGray,D) #element-wise quotient and remainder
        Ax=2*x+1-D # PAM Amplitudes 2d+1-D - real axis
        Ay=2*y+1-D # PAM Amplitudes 2d+1-D - imag axis
        constellation = Ax+1j*Ay
        Modem.__init__(self, M, constellation, name='QAM') #set the modem attributes
        
class FSKModem(Modem):
    # Derivied class: FSKModem
    def __init__(self,M,coherence='coherent'):
        if coherence.lower()=='coherent':
            phi= np.zeros(M) # phase=0 for coherent detection
        elif coherence.lower()=='noncoherent':
            phi = 2*np.pi*np.random.rand(M) # M random phases in the (0,2pi)
        else:
            raise ValueError('Coherence must be \'coherent\' or \'noncoherent\'')
        constellation = np.diag(np.exp(1j*phi))
        Modem.__init__(self, M, constellation, name='FSK',coherence=coherence.lower()) #set the base modem attributes
        
    def demodulate(self, receivedSyms,coherence='coherent'):
        #overridden method in Modem class
        if coherence.lower()=='coherent':
            return self.iqDetector(receivedSyms)
        elif coherence.lower()=='noncoherent':
            return np.argmax(np.abs(receivedSyms),axis=1)
        else:
            raise ValueError('Coherence must be \'coherent\' or \'noncoherent\'')

Classes

class FSKModem (M, coherence='coherent')
Expand source code
class FSKModem(Modem):
    # Derivied class: FSKModem
    def __init__(self,M,coherence='coherent'):
        if coherence.lower()=='coherent':
            phi= np.zeros(M) # phase=0 for coherent detection
        elif coherence.lower()=='noncoherent':
            phi = 2*np.pi*np.random.rand(M) # M random phases in the (0,2pi)
        else:
            raise ValueError('Coherence must be \'coherent\' or \'noncoherent\'')
        constellation = np.diag(np.exp(1j*phi))
        Modem.__init__(self, M, constellation, name='FSK',coherence=coherence.lower()) #set the base modem attributes
        
    def demodulate(self, receivedSyms,coherence='coherent'):
        #overridden method in Modem class
        if coherence.lower()=='coherent':
            return self.iqDetector(receivedSyms)
        elif coherence.lower()=='noncoherent':
            return np.argmax(np.abs(receivedSyms),axis=1)
        else:
            raise ValueError('Coherence must be \'coherent\' or \'noncoherent\'')

Ancestors

Inherited members

class Modem (M, constellation, name, coherence=None)
Expand source code
class Modem:
    __metadata__ = abc.ABCMeta
    # Base class: Modem
    # Attribute definitions:
    #    self.M : number of points in the MPSK constellation
    #    self.name: name of the modem : PSK, QAM, PAM, FSK
    #    self.constellation : reference constellation
    #    self.coherence : only for 'coherent' or 'noncoherent' FSK
    def __init__(self,M,constellation,name,coherence=None): #constructor
        if (M<2) or ((M & (M -1))!=0): #if M not a power of 2
            raise ValueError('M should be a power of 2')
        if name.lower()=='fsk':
            if (coherence.lower()=='coherent') or (coherence.lower()=='noncoherent'):
                self.coherence = coherence
            else:
                raise ValueError('Coherence must be \'coherent\' or \'noncoherent\'')
        else:
            self.coherence = None
        self.M = M # number of points in the constellation
        self.name = name # name of the modem : PSK, QAM, PAM, FSK
        self.constellation = constellation # reference constellation
    
    def plotConstellation(self):
        """
        Plot the reference constellation points for the selected modem
        """
        from math import log2
        if self.name.lower()=='fsk':
            return 0 # FSK is multi-dimensional difficult to visualize 
        
        fig, axs = plt.subplots(1, 1)
        axs.plot(np.real(self.constellation),np.imag(self.constellation),'o')        
        for i in range(0,self.M):
            axs.annotate("{0:0{1}b}".format(i,int(log2(self.M))), (np.real(self.constellation[i]),np.imag(self.constellation[i])))
        
        axs.set_title('Constellation');
        axs.set_xlabel('I');axs.set_ylabel('Q');fig.show()
    
    def modulate(self,inputSymbols):
        """
            Modulate a vector of input symbols (numpy array format) using the chosen
             modem. The input symbols take integer values in the range 0 to M-1.
        """
        if isinstance(inputSymbols,list):
            inputSymbols = np.array(inputSymbols)
        
        if  not (0 <= inputSymbols.all() <= self.M-1):
            raise ValueError('Values for inputSymbols are beyond the range 0 to M-1')
        
        modulatedVec = self.constellation[inputSymbols]
        return modulatedVec #return modulated vector
    
    def demodulate(self,receivedSyms):
        """
            Demodulate a vector of received symbols using the chosen modem.            
        """        
        if isinstance(receivedSyms,list):
            receivedSyms = np.array(receivedSyms)
            
        detectedSyms= self.iqDetector(receivedSyms)
        return detectedSyms
    
    def iqDetector(self,receivedSyms):
        """
        Optimum Detector for 2-dim. signals (ex: MQAM,MPSK,MPAM) in IQ Plane
        Note: MPAM/BPSK are one dimensional modulations. The same function can be 
        applied for these modulations since quadrature is zero (Q=0)
        
        The function computes the pair-wise Euclidean distance of each point in the
        received vector against every point in the reference constellation. It then
        returns the symbols from the reference constellation that provide the
        minimum Euclidean distance.
        
        Parameters:
            receivedSyms : received symbol vector of complex form
        Returns:
            detectedSyms:decoded symbols that provide the minimum Euclidean distance        
        """
        from scipy.spatial.distance import cdist
        # received vector and reference in cartesian form
        XA = np.column_stack((np.real(receivedSyms),np.imag(receivedSyms))) 
        XB=np.column_stack((np.real(self.constellation),np.imag(self.constellation)))
        
        d = cdist(XA,XB,metric='euclidean') #compute pair-wise Euclidean distances
        detectedSyms = np.argmin(d,axis=1)#indices corresponding minimum Euclid. dist.
        return detectedSyms

Subclasses

Methods

def demodulate(self, receivedSyms)

Demodulate a vector of received symbols using the chosen modem.

Expand source code
def demodulate(self,receivedSyms):
    """
        Demodulate a vector of received symbols using the chosen modem.            
    """        
    if isinstance(receivedSyms,list):
        receivedSyms = np.array(receivedSyms)
        
    detectedSyms= self.iqDetector(receivedSyms)
    return detectedSyms
def iqDetector(self, receivedSyms)

Optimum Detector for 2-dim. signals (ex: MQAM,MPSK,MPAM) in IQ Plane Note: MPAM/BPSK are one dimensional modulations. The same function can be applied for these modulations since quadrature is zero (Q=0)

The function computes the pair-wise Euclidean distance of each point in the received vector against every point in the reference constellation. It then returns the symbols from the reference constellation that provide the minimum Euclidean distance.

Parameters

receivedSyms : received symbol vector of complex form
 

Returns

detectedSyms:decoded symbols that provide the minimum Euclidean distance
 
Expand source code
def iqDetector(self,receivedSyms):
    """
    Optimum Detector for 2-dim. signals (ex: MQAM,MPSK,MPAM) in IQ Plane
    Note: MPAM/BPSK are one dimensional modulations. The same function can be 
    applied for these modulations since quadrature is zero (Q=0)
    
    The function computes the pair-wise Euclidean distance of each point in the
    received vector against every point in the reference constellation. It then
    returns the symbols from the reference constellation that provide the
    minimum Euclidean distance.
    
    Parameters:
        receivedSyms : received symbol vector of complex form
    Returns:
        detectedSyms:decoded symbols that provide the minimum Euclidean distance        
    """
    from scipy.spatial.distance import cdist
    # received vector and reference in cartesian form
    XA = np.column_stack((np.real(receivedSyms),np.imag(receivedSyms))) 
    XB=np.column_stack((np.real(self.constellation),np.imag(self.constellation)))
    
    d = cdist(XA,XB,metric='euclidean') #compute pair-wise Euclidean distances
    detectedSyms = np.argmin(d,axis=1)#indices corresponding minimum Euclid. dist.
    return detectedSyms
def modulate(self, inputSymbols)

Modulate a vector of input symbols (numpy array format) using the chosen modem. The input symbols take integer values in the range 0 to M-1.

Expand source code
def modulate(self,inputSymbols):
    """
        Modulate a vector of input symbols (numpy array format) using the chosen
         modem. The input symbols take integer values in the range 0 to M-1.
    """
    if isinstance(inputSymbols,list):
        inputSymbols = np.array(inputSymbols)
    
    if  not (0 <= inputSymbols.all() <= self.M-1):
        raise ValueError('Values for inputSymbols are beyond the range 0 to M-1')
    
    modulatedVec = self.constellation[inputSymbols]
    return modulatedVec #return modulated vector
def plotConstellation(self)

Plot the reference constellation points for the selected modem

Expand source code
def plotConstellation(self):
    """
    Plot the reference constellation points for the selected modem
    """
    from math import log2
    if self.name.lower()=='fsk':
        return 0 # FSK is multi-dimensional difficult to visualize 
    
    fig, axs = plt.subplots(1, 1)
    axs.plot(np.real(self.constellation),np.imag(self.constellation),'o')        
    for i in range(0,self.M):
        axs.annotate("{0:0{1}b}".format(i,int(log2(self.M))), (np.real(self.constellation[i]),np.imag(self.constellation[i])))
    
    axs.set_title('Constellation');
    axs.set_xlabel('I');axs.set_ylabel('Q');fig.show()
class PAMModem (M)
Expand source code
class PAMModem(Modem):
    # Derived class: PAMModem
    def __init__(self, M):
        m = np.arange(0,M) #all information symbols m={0,1,...,M-1}
        constellation = 2*m+1-M + 1j*0  # reference constellation        
        Modem.__init__(self, M, constellation, name='PAM') #set the modem attributes

Ancestors

Inherited members

class PSKModem (M)
Expand source code
class PSKModem(Modem):
    # Derived class: PSKModem
    def __init__(self, M):        
        #Generate reference constellation
        m = np.arange(0,M) #all information symbols m={0,1,...,M-1}
        I = 1/np.sqrt(2)*np.cos(m/M*2*np.pi)
        Q = 1/np.sqrt(2)*np.sin(m/M*2*np.pi)
        constellation = I + 1j*Q #reference constellation        
        Modem.__init__(self, M, constellation, name='PSK') #set the modem attributes

Ancestors

Inherited members

class QAMModem (M)
Expand source code
class QAMModem(Modem):
    # Derived class: QAMModem
    def __init__(self,M):
        if (M==1) or (np.mod(np.log2(M),2)!=0): # M not a even power of 2
            raise ValueError('Only square MQAM supported. M must be even power of 2')
        
        n = np.arange(0,M) # Sequential address from 0 to M-1 (1xM dimension)
        a = np.asarray([x^(x>>1) for x in n]) #convert linear addresses to Gray code
        D = np.sqrt(M).astype(int) #Dimension of K-Map - N x N matrix
        a = np.reshape(a,(D,D)) # NxN gray coded matrix
        oddRows=np.arange(start = 1, stop = D ,step=2) # identify alternate rows
        a[oddRows,:] = np.fliplr(a[oddRows,:]) #Flip rows - KMap representation
        nGray=np.reshape(a,(M)) # reshape to 1xM - Gray code walk on KMap
        
        #Construction of ideal M-QAM constellation from sqrt(M)-PAM
        (x,y)=np.divmod(nGray,D) #element-wise quotient and remainder
        Ax=2*x+1-D # PAM Amplitudes 2d+1-D - real axis
        Ay=2*y+1-D # PAM Amplitudes 2d+1-D - imag axis
        constellation = Ax+1j*Ay
        Modem.__init__(self, M, constellation, name='QAM') #set the modem attributes

Ancestors

Inherited members