통신

Binary Phase Shift Keying(BPSK) 변조

coding art 2022. 3. 12. 13:38
728x90

Digital Modulations using Python 56-60페이지 참조

 

통신에서 활동되는 많은 변조 방식 가운데 하나인 BPSK 변조는  time domain 상에서 지속 시간이 Tb인 디지털 비트 데이터 "0"과 "1" 로 이루어진 신호를 각각 위상이 180도 차이가 나는 코사인 파형으로 변조하는 기법이다.

                              s1(t) = Ac cos(2π fct), 0 ≤ t ≤ Tb for binary 1

                              s0(t) = Ac cos(2π fct +π), 0 ≤ t ≤ Tb for binary 0

여기서 fc 는 변조 주파수를 뜻한다.

이러한 BPSK 변조파는 constellation diagram 상에서 실수축상에서 대칭을 이루는 두개의 점으로 표현이 가능하다.

 

BPSK 변조의 송신 transmitter 부분은 다음과 같이 구성된다. 변조된 신호는 (광)전송라인을 통해 보내지고 Coherent Receiver 에서 수신하게 된다. 이과정에서 노이즈를 탈 수 있으며 대표적인 노이즈가 바로 AWGN(Additive White Gaussian Noise) 이다. 이 노이즈는 변조도니 파형에 곱셈으로 작용하는 것이 아닌 덧셈 개념의 노이즈다. 즉 변조파형에 Gaussian 형태로 랜덤하게 가산되어 삐죽삐죽한 형상을 보여주게 되며 이과정에서 전송한 정보가 "0"인지 "1"인지 에러가 발생하게 되면 이 비율을 Bit Error Rate  약어로 BER이라고 한다. 이 부분은 다소 수학적이므로 별도로 언급할 예정이다.(참조:

BPSK변조에서 AGWN(Eb/No)노이즈에 의한 BER 이론 계산

https://blog.daum.net/ejleep1/1248 )

 

이러한 BPSK 변조과정을 전송되어 수신기에서 복조(Demodulation) 과정이 일어난다. BPSK 변조과정을 거친 데이터는 Coherent Receiver 를 사용하여 수신 후 복조가 가능하다. Coherent 라 함은 전송되는 변조 데이터의 주파수와 위상이 동일해야 함이 요구된다. 즉 변조 주파수와 위상을 정확히 알면 편하게 복조할 수 있다는 것이다. 실제로 전송 후 수신과정에서 알고 있는 전송주파수와 위상값 처리는 Costas loop 또는 PLL(Phase Locked Koop) 가 담당하게 된다. 아래 그림에서 Costas loop 로 처리돈 부분이 rbb(t) 라 보면 될 것이다.

 

복조과정에서 rbb(t) 는 전송과정에서 노이즈를 타기때문에 원래의 파형 형상을 유지하지는 못할 것이다. 따라서 전송된 NRZ 코드 즉 "+1"인지 아니면 "-1"인지를 판단하는 기준은 즉 threshold 값으로서 "0"V 가 될 것이다. 왜 BPSK 변조 과정에서 "0"V 사용을 배제하는 주된 이유가 바로 그것이다. rbb(t) 값을 1비트 지속시간 동안 적분하고 그 결과치가 "0" 이상인지 아닌지를 확인하여 신호를 복조하여 내는 것이다. 이와같이 복조헤서 얻어지는 신호에 "^" 기호로 처리 후 이론적인 BER 계산에 사용하기로 하자. "^" 기호로 처리된 이 부분은 실제 통신에서 사용하는 양이므로 하드웨어 및 소프트웨어에서 원 신호와 차이가 최소화 될 수 있도록 처리되어야 하며 일명 "optimal"값  이라 한다. 물론 여러가지 방법이 있을 수 있다.

 

이러한  "0"과 "1" 로만 이루어진 신호를 코사인 변조주파수를 사용한 코사인 변조 파형 및 constellation  diagram 을 matplotlib 라이브러리를 사용하여 작도해보자. 아울러 변조파형에 AWGN(additive white Gaussian Noise) 를 추가해 보자. AWGN 은 -4dB에서 2 dB 씩 증분하여 7가지 경우를 계산하여 중첩 작도하였다. 노이즈를 탄 개별 파형을 보려면 for loop 부분을 수정해야 할 것이다.

 

한편 Bit Error 계산 결과 작도는 앞으로 별도로 해설을 올릴 예정이다.(참조: BPSK변조에서 AGWN(Eb/No)노이즈에 의한 BER 이론 계산https://blog.daum.net/ejleep1/1248  )

 

BPSK_MODIFIED.PY의 코드 시작 부분을 살펴 보기로 한다.

첫번째 비트 신호 생성에 관해서 살펴 보자. 

전송해야 할 심볼의 수를 100000개로 설정하자. 심볼의 종류는 위 그래프 상에서 constellation diagram의 점 또는 점 주위에 모인 군집으로 나타낼 수 있으며 BPSK에서는 2개로 볼 수 있다. 아울러 "1" 또는 "0" 각각이 하나의 심볼이며 이들은 전송전에 NRZ(non return zero)코딩에 의해 예를 들면 "+1V" 와 "-1" 로 처리하여 좌표 평면 상에서 y 축에 대칭인 점들로 constellation diagram 상에 위치하게 된다. 

N 값은 충분히 큰 수값을 취하게 되는데 carrier 의 주파수나 또는 sampling  주파수에 의해 요구되는 최소한의 Nykist criteria 보다 더 클 필요가 있다. 즉 sampling  주파수의 2배 이상으로 큰 값이 좋다. 으며 시간적으로 말하자면 배수* 초당sampling  주파수에 해당하는 심볼수를 가지게 된다.

carrier 주파수가 800Hz 이고 oversampling rate 이 16 이면  sampling 주파수가 12800 Hz 가 된다. 따라서  N=100000이면 거의 7.8초 가량이므로 충분하다고 볼 수 있다.

N=100000 # Number of symbols to transmit

ENN0dB는 Eb/N0 비율을 일반적으로 start= -4dB에서 stop =  -2, 0, 2, 4, 6, 8, 10dB 로 두고 step = 2dB 로 둔다. 
EbN0dB = np.arange(start=10,stop = 10.5,step = 2) # Eb/N0 range in dB for simulation
L 은 oversampling factor로 10 이상의 충분히 큰 수로 잡자.
L=16 # oversampling factor,L=Tb/Ts(Tb=bit period,Ts=sampling period)
# if a carrier is used, use L = Fs/Fc, where Fs >> 2xFc
Fc=800 # carrier frequency
Fs=L*Fc # sampling frequency

BER 값을 0.0으로 초기화 한다.
BER = np.zeros(len(EbN0dB)) # for BER values for each Eb/N0

radint(2, size=N)은 0과 같거나 큰 2보다는 작은 정수형 난수를 N개 만큼 생성하여 array ak 를 준비한다.

ak = np.random.randint(2, size=N) # uniform random symbols from 0's and 1's

channels.py  코드에 포함된 bpsk_mod(ak,L) 함수를 사용하여 NRZ baseband 코드인 s_bb 를 생성한다. 

그 갯수는 N개만큼 생성된 ak 의 L배 만큼이다. 이는  t의 np.arrange 명령에 의한 시간간격 생성에서 시작이 0 이고 마지막이 len(ak)*L이라는 사실에서 알 수 있다.

(s_bb,t)= bpsk_mod(ak,L) # BPSK modulation(waveform) - baseband

코사인 파형의 캐리어 파형을 생성하여 s_bb 로 변조한다.
s = s_bb*np.cos(2*np.pi*Fc*t/Fs) # with carrier

생성된 변조 파형이 위 그림의 2번째 파형이다.  이 파형의 주파수 특성을 관찰하기 위한 DFT 처리는 다음 url 을 방문하여 참고하기 바란다. (DFT 신호처리 및 AWGN효과 시각화: https://blog.daum.net/ejleep1/1252)

 

BPSK 시뮬레이션을 통해 마치 baseband 신호 ak 가 전송중에 white 노이즈를 탄 것처럼 조작 후 다시 복조하고 원 baseband 신호와 차이가 있는지 체크하여 평균을 구하여 시뮬레이션  BER 값으로 둔다.

한편 ak는 0.0과 1.0 으로 표현되는 리스트 데이터인 반면에 ak_hat는 Boolean 으로 표현된다. 이들이 일치하지 않으면 1만큼씩 카운트 합산된다. ( 이 숫자 "0.0"과 "1.0" 인 데이터와 "False"와 "True" 인 데이터를 비교해서 처리하는 이 파이선 명령 사용법은 실제 예제를 만들어 실행해 보아야 이해가 된다.)

Eb/N0dB 값에 따른 복조(demodulation) 과정을 살펴보자. 전송 준비된 신호 s에 white noise 를 부가할 수 있도록 channels.py 에 포함되어 있는 함수 awgn 을 사용하자. 이렇게 처리된 신호 데이터를 함수 bpsk_demod() 를 사용하여 복조(demodulation) 하도록 하자.

복조를 위한 코드를 살펴보면 전송하고자 하는 심볼 수인 N개에 해당하는 만큼 생성된 "_1"과 "+1" 이루어진 데이터를 처리함에 있어서 변조에서와 마찬가지로 N개의 데이터 r_bb 와 L 즉 oversampling rate 이 필요하다.

np.ones(L) 의 실행은 L개의 원소를 갖는 리스트 데이터 즉 [1., 1., 1., 1., ..., 1.]가 생성된다.

디폴트 조건의 convolve 명령은 컨볼루션 적분을 수행한다. 현재 np.ones(L)을 커늘로 하여 1차원적으로 리스트 데이터 x 를 +1씩 스트라이딩하며 결과를 생성한다. 하지만 첫 컨볼루션에서 (L-1)개 만큼은 padding  x 데이터가 없으므로 0 으로 처리하면 L번째와 x의 첫번째 데이터와의 곱만이 남는다. 이런 방식으로 1씩 스트라이딩 하면서 L 개의 컨볼루션 값을 구해서 합산하도록 하는데 이것이 바로 적분을 뜻하는 듯 하다. (현재 간격 사이를 1 로 보기때문에 L 값으로 나누어야 정확한 적분 값이 되지 않을까 한다.) 하지만 이어지는 계산에서 결과가 단지 0보다 큰지 작은지만을 체크하기 때문에 문제가 될 것은 없다.

x[L-1:-1:L]에서 첫법째 인덱스 L-1 은 앞서 거론했던 padding 없는 부분까지의 갯수를 감안허여 적어도 x의 첫번째 데이터가 사용되는 지점까지를 의미하는데 x 로서는 첫번째 0에 해당하는 위치이며 :-1 은 x의 L-1 번째 데이터를 지정한다. 마지막의 :L은 L개 단위로 건너 뛰면서 적분 값을 계산한다는 의미이다. 

마지막 (x>0).transpose( ) 명령에서 .transpose( ) 는 별 의미가 없어 보인다. 이 명령 없이 추력해도 결과는 마찬가지로 확인되었다.

첨부된 파이선 코드를 실행해보자. 아나콘다에서 실행을 위해서 아래의 bpsk_modified.py를 실행하자.

폴더 구조를 살펴보면 현재 chapter_2 내부에 이 코드가 포함되어 있다.

아울러 passband_modulations.py 도 같은 chapter_2 폴더와 함께 위치하고 있어야 bpsk_modified.py 가 불러서 사용하게 된다. 코드 bpsk_modified 는 원래 subplot 명령을 사용하여 아주 작은 크기로 출력되기때문에 큼직한 그래프를 출력하도록 변경하였다.

물론 이 코드에는 BPSK 이외도 많은 변조 방식 시뮬레이션을 지원하는 코드들이 들어 있으므로 앞으로 다른 변조방식에 대한 블로그 작성 시 지속적으로 사용할 예정이다. 따라서 셸(shell)에서 cd .. 명령을 사용하여 chapter_2로부터 DigicommPy 로 변경 후 exec(open("chapter_2/bpsk_modified.py").read()) 명령을 실행하자. 

아래 그림의 폴더 구조를 참조 확인하여 실행하도록 하자.

 

 

코드 실행하는 요령을 다음 보기 YouTube 를 클릭하여 살펴보세요. 구독 좋아요 부탁해요!

https://youtu.be/A5qLefazrI8

 

 

Under Construction ...

 

#BPSK_modified.py

#Execute in Python3: exec(open("chapter_2/bpsk_modified.py").read())
import numpy as np #for numerical computing
import matplotlib.pyplot as plt #for plotting functions
from passband_modulations import bpsk_mod, bpsk_demod
from channels import awgn
from scipy.special import erfc

N=100000 # Number of symbols to transmit
print(N)
EbN0dB = np.arange(start=-4,stop = 11,step = 2) # Eb/N0 range in dB for simulation
L=16 # oversampling factor,L=Tb/Ts(Tb=bit period,Ts=sampling period)
# if a carrier is used, use L = Fs/Fc, where Fs >> 2xFc
Fc=800 # carrier frequency
Fs=L*Fc # sampling frequency
BER = np.zeros(len(EbN0dB)) # for BER values for each Eb/N0
ak = np.random.randint(2, size=N) # uniform random symbols from 0's and 1's
(s_bb,t)= bpsk_mod(ak,L) # BPSK modulation(waveform) - baseband
s = s_bb*np.cos(2*np.pi*Fc*t/Fs) # with carrier
# Waveforms at the transmitter

fig1 = plt.figure(figsize=(10,5))
plt.plot(t,s_bb) # baseband wfm zoomed to first 10 bits
plt.xlabel('t(s)');plt.ylabel(r'$s_{bb}(t)$-baseband')
plt.xlim(0,10*L)
plt.show()
fig2 = plt.figure(figsize=(10,5))
plt.plot(t,s) # transmitted wfm zoomed to first 10 bits
plt.xlabel('t(s)');plt.ylabel('s(t)-with carrier')
plt.xlim(0,10*L)
plt.show()
#signal constellation at transmitter
fig3 = plt.figure(figsize=(5,5))
plt.plot(np.real(s_bb),np.imag(s_bb),'o')
plt.xlim(-1.5,1.5) 
plt.ylim(-1.5,1.5)
plt.show()
fig4 = plt.figure(figsize=(10,5))
for i,EbN0 in enumerate(EbN0dB):
    # Compute and add AWGN noise
    r = awgn(s,EbN0,L) # refer Chapter section 4.1
    
    r_bb = r*np.cos(2*np.pi*Fc*t/Fs) # recovered baseband signal
    ak_hat = bpsk_demod(r_bb,L) # baseband correlation demodulator
    BER[i] = np.sum(ak !=ak_hat)/N # Bit Error Rate Computation
    
    # Received signal waveform zoomed to first 10 bits
    plt.plot(t,r) # received signal (with noise)
    plt.xlabel('t(s)');plt.ylabel('r(t)')
    plt.xlim(0,10*L)
plt.show()
#------Theoretical Bit/Symbol Error Rates-------------
theoreticalBER = 0.5*erfc(np.sqrt(10**(EbN0dB/10))) # Theoretical bit error rate

#-------------Plots---------------------------
fig5, ax1 = plt.subplots(nrows=1,ncols = 1)
ax1.semilogy(EbN0dB,BER,'k*',label='Simulated') # simulated BER
ax1.semilogy(EbN0dB,theoreticalBER,'r-',label='Theoretical')
ax1.set_xlabel(r'$E_b/N_0$ (dB)')
ax1.set_ylabel(r'Probability of Bit Error - $P_b$')
ax1.set_title(['Probability of Bit Error for BPSK modulation'])
ax1.legend();plt.show()

 

 

 

 

#pasband_moulations.py

"""
Passband simulation models - modulation and demodulation techniques (Chapter 2)

@author: Mathuranathan Viswanathan
Created on Jul 17, 2019
"""
import numpy as np
import matplotlib.pyplot as plt

def bpsk_mod(ak,L):
    """
    Function to modulate an incoming binary stream using BPSK (baseband)
    Parameters:
        ak : input binary data stream (0's and 1's) to modulate
        L : oversampling factor (Tb/Ts)
    Returns:
        (s_bb,t) : tuple of following variables
                s_bb: BPSK modulated signal(baseband) - s_bb(t)
                 t :  generated time base for the modulated signal
    """
    from scipy.signal import upfirdn
    s_bb = upfirdn(h=[1]*L, x=2*ak-1, up = L) # NRZ encoder
    t=np.arange(start = 0,stop = len(ak)*L) #discrete time base
    return (s_bb,t)

def bpsk_demod(r_bb,L):
    """
    Function to demodulate a BPSK (baseband) signal
    Parameters:
        r_bb : received signal at the receiver front end (baseband)
        L : oversampling factor (Tsym/Ts)
    Returns:
        ak_hat : detected/estimated binary stream
    """
    x = np.real(r_bb) # I arm
    x = np.convolve(x,np.ones(L)) # integrate for Tb duration (L samples)
    x = x[L-1:-1:L] # I arm - sample at every L
    ak_hat = (x > 0).transpose() # threshold detector
    return ak_hat

def add_awgn_noise(s,SNRdB,L=1):
    """
    Function to add AWGN noise to input signal
    The function adds AWGN noise vector to signal 's' to generate a \
    resulting signal vector 'r' of specified SNR in dB. It also returns\
    the noise vector 'n' that is added to the signal 's' and the spectral\
    density N0 of noise added
    
    Parameters:
        s : input/transmitted signal vector
        SNRdB : desired signal to noise ratio (expressed in dB)
            for the received signal
        L : oversampling factor (applicable for waveform simulation)
            default L = 1.
    Returns:
        (r,n,N0) : tuple of following variables
                r : received signal vector (r = s+n)
                n : noise signal vector added
                N0 : spectral density of the generated noise vector n
    """
    gamma = 10**(SNRdB/10) #SNR to linear scale
    
    if s.ndim==1:# if s is single dimensional vector
        P=L*np.sum(abs(s)**2)/len(s) #Actual power in the vector
    else: # multi-dimensional signals like MFSK
        P=L*np.sum(np.sum(abs(s)**2))/len(s) # if s is a matrix [MxN]
        
    N0=P/gamma # Find the noise spectral density
    
    from numpy.random import standard_normal
    if np.isrealobj(s):# check if input is real/complex object type
        n = np.sqrt(N0/2)*standard_normal(s.shape) # computed noise
    else:
        n = np.sqrt(N0/2)*(standard_normal(s.shape)+1j*standard_normal(s.shape)) #computed noise
    r = s + n # received signal    
    return (r,n,N0)

def qpsk_mod(a, fc, OF, enable_plot = False):
    """
    Modulate an incoming binary stream using conventional QPSK
    Parameters:
        a : input binary data stream (0's and 1's) to modulate
        fc : carrier frequency in Hertz
        OF : oversampling factor - at least 4 is better
        enable_plot : True = plot transmitter waveforms (default False)
    Returns:
        result : Dictionary containing the following keyword entries:
          s(t) : QPSK modulated signal vector with carrier i.e, s(t)
          I(t) : baseband I channel waveform (no carrier)
          Q(t) : baseband Q channel waveform (no carrier)
          t : time base for the carrier modulated signal
    """
    L = 2*OF # samples in each symbol (QPSK has 2 bits in each symbol)
    I = a[0::2];Q = a[1::2] #even and odd bit streams
    # even/odd streams at 1/2Tb baud
    from scipy.signal import upfirdn #NRZ encoder
    I = upfirdn(h=[1]*L, x=2*I-1, up = L)
    Q = upfirdn(h=[1]*L, x=2*Q-1, up = L)
    
    fs = OF*fc # sampling frequency 
    t=np.arange(0,len(I)/fs,1/fs)  #time base
    I_t = I*np.cos(2*np.pi*fc*t);Q_t = -Q*np.sin(2*np.pi*fc*t)
    s_t = I_t + Q_t # QPSK modulated baseband signal
    
    if enable_plot:
        fig = plt.figure(constrained_layout=True)
        
        from matplotlib.gridspec import GridSpec
        gs = GridSpec(3, 2, figure=fig)
        ax1 = fig.add_subplot(gs[0, 0]);ax2 = fig.add_subplot(gs[0, 1])
        ax3 = fig.add_subplot(gs[1, 0]);ax4 = fig.add_subplot(gs[1, 1])
        ax5 = fig.add_subplot(gs[-1,:])
        
        # show first few symbols of I(t), Q(t)
        ax1.plot(t,I);ax1.set_title('I(t)')        
        ax2.plot(t,Q);ax2.set_title('Q(t)')
        ax3.plot(t,I_t,'r');ax3.set_title('$I(t) cos(2 \pi f_c t)$')
        ax4.plot(t,Q_t,'r');ax4.set_title('$Q(t) sin(2 \pi f_c t)$')
        ax1.set_xlim(0,20*L/fs);ax2.set_xlim(0,20*L/fs)
        ax3.set_xlim(0,20*L/fs);ax4.set_xlim(0,20*L/fs)
        ax5.plot(t,s_t);ax5.set_xlim(0,20*L/fs);fig.show()
        ax5.set_title('$s(t) = I(t) cos(2 \pi f_c t) - Q(t) sin(2 \pi f_c t)$')
    result = dict()
    result['s(t)'] =s_t;result['I(t)'] = I;result['Q(t)'] = Q;result['t'] = t           
    return result
        
def qpsk_demod(r,fc,OF,enable_plot=False):
    """
    Demodulate a conventional QPSK signal
    Parameters:
        r : received signal at the receiver front end
        fc : carrier frequency (Hz)
        OF : oversampling factor (at least 4 is better)
        enable_plot : True = plot receiver waveforms (default False)
    Returns:
        a_hat - detected binary stream
    """
    fs = OF*fc # sampling frequency
    L = 2*OF # number of samples in 2Tb duration
    t=np.arange(0,len(r)/fs,1/fs) # time base
    x=r*np.cos(2*np.pi*fc*t) # I arm
    y=-r*np.sin(2*np.pi*fc*t) # Q arm
    x = np.convolve(x,np.ones(L)) # integrate for L (Tsym=2*Tb) duration
    y = np.convolve(y,np.ones(L)) #integrate for L (Tsym=2*Tb) duration
    
    x = x[L-1::L] # I arm - sample at every symbol instant Tsym
    y = y[L-1::L] # Q arm - sample at every symbol instant Tsym
    a_hat = np.zeros(2*len(x))
    a_hat[0::2] = (x>0) # even bits
    a_hat[1::2] = (y>0) # odd bits
    
    if enable_plot:
        fig, axs = plt.subplots(1, 1)
        axs.plot(x[0:200],y[0:200],'o');fig.show()
                
    return a_hat

def oqpsk_mod(a,fc,OF,enable_plot=False):
    """
    Modulate an incoming binary stream using OQPSK
    Parameters:
        a : input binary data stream (0's and 1's) to modulate
        fc : carrier frequency in Hertz
        OF : oversampling factor - at least 4 is better
        enable_plot : True = plot transmitter waveforms (default False)
    Returns:
        result : Dictionary containing the following keyword entries:
          s(t) : QPSK modulated signal vector with carrier i.e, s(t)
          I(t) : baseband I channel waveform (no carrier)
          Q(t) : baseband Q channel waveform (no carrier)
          t : time base for the carrier modulated signal
    """
    L = 2*OF # samples in each symbol (QPSK has 2 bits in each symbol)
    I = a[0::2];Q = a[1::2] #even and odd bit streams
    # even/odd streams at 1/2Tb baud
    from scipy.signal import upfirdn #NRZ encoder
    I = upfirdn(h=[1]*L, x=2*I-1, up = L)
    Q = upfirdn(h=[1]*L, x=2*Q-1, up = L)

    I = np.hstack((I,np.zeros(L//2))) # padding at end
    Q = np.hstack((np.zeros(L//2),Q)) # padding at start
    
    fs = OF*fc # sampling frequency 
    t=np.arange(0,len(I)/fs,1/fs)  #time base
    I_t = I*np.cos(2*np.pi*fc*t);Q_t = -Q*np.sin(2*np.pi*fc*t)
    s = I_t + Q_t # QPSK modulated baseband signal
    
    if enable_plot:
        fig = plt.figure(constrained_layout=True)
        
        from matplotlib.gridspec import GridSpec
        gs = GridSpec(3, 2, figure=fig)
        ax1 = fig.add_subplot(gs[0, 0]);ax2 = fig.add_subplot(gs[0, 1])
        ax3 = fig.add_subplot(gs[1, 0]);ax4 = fig.add_subplot(gs[1, 1])
        ax5 = fig.add_subplot(gs[-1,:])
        
        # show first few symbols of I(t), Q(t)
        ax1.plot(t,I);ax1.set_title('I(t)')        
        ax2.plot(t,Q);ax2.set_title('Q(t)')
        ax3.plot(t,I_t,'r');ax3.set_title('$I(t) cos(2 \pi f_c t)$')
        ax4.plot(t,Q_t,'r');ax4.set_title('$Q(t) sin(2 \pi f_c t)$')
        ax1.set_xlim(0,20*L/fs);ax2.set_xlim(0,20*L/fs)
        ax3.set_xlim(0,20*L/fs);ax4.set_xlim(0,20*L/fs)
        ax5.plot(t,s);ax5.set_xlim(0,20*L/fs);fig.show()
        ax5.set_title('$s(t) = I(t) cos(2 \pi f_c t) - Q(t) sin(2 \pi f_c t)$')
        
        fig, axs = plt.subplots(1, 1)
        axs.plot(I,Q);fig.show()#constellation plot
    result = dict()
    result['s(t)'] =s;result['I(t)'] = I;result['Q(t)'] = Q;result['t'] = t           
    return result

def oqpsk_demod(r,N,fc,OF,enable_plot=False):
    """
    Demodulate a OQPSK signal
    Parameters:
        r : received signal at the receiver front end
        N : Number of OQPSK symbols transmitted
        fc : carrier frequency (Hz)
        OF : oversampling factor (at least 4 is better)
        enable_plot : True = plot receiver waveforms (default False)
    Returns:
        a_hat - detected binary stream
    """
    fs = OF*fc # sampling frequency
    L = 2*OF # number of samples in 2Tb duration
    t=np.arange(0,(N+1)*OF/fs,1/fs) # time base
    x=r*np.cos(2*np.pi*fc*t) # I arm
    y=-r*np.sin(2*np.pi*fc*t) # Q arm
    x = np.convolve(x,np.ones(L)) # integrate for L (Tsym=2*Tb) duration
    y = np.convolve(y,np.ones(L)) #integrate for L (Tsym=2*Tb) duration
    
    x = x[L-1:-1-L:L] # I arm - sample at every symbol instant Tsym
    y = y[L+L//2-1:-1-L//2:L] # Q arm - sample at every symbol starting at L+L/2-1th sample
    a_hat = np.zeros(N)
    a_hat[0::2] = (x>0) # even bits
    a_hat[1::2] = (y>0) # odd bits
    
    if enable_plot:
        fig, axs = plt.subplots(1, 1)
        axs.plot(x[0:200],y[0:200],'o');fig.show()                
    return a_hat

def piBy4_dqpsk_Diff_encoding(a,enable_plot=False):
    """
    Phase Mapper for pi/4-DQPSK modulation
    Parameters:
        a : input stream of binary bits
    Returns:
        (u,v): tuple, where
           u : differentially coded I-channel bits
           v : differentially coded Q-channel bits    
    """
    from numpy import pi, cos, sin
    if len(a)%2: raise ValueError('Length of binary stream must be even')
    I = a[0::2] # odd bit stream
    Q = a[1::2] # even bit stream
    # club 2-bits to form a symbol and use it as index for dTheta table
    m = 2*I+Q
    dTheta = np.array([-3*pi/4, 3*pi/4, -pi/4, pi/4]) #LUT for pi/4-DQPSK
    u = np.zeros(len(m)+1);v = np.zeros(len(m)+1)
    u[0]=1; v[0]=0 # initial conditions for uk and vk
    for k in range(0,len(m)):
        u[k+1] = u[k] * cos(dTheta[m[k]]) - v[k] * sin(dTheta[m[k]])
        v[k+1] = u[k] * sin(dTheta[m[k]]) + v[k] * cos(dTheta[m[k]])
    if enable_plot:#constellation plot
        fig, axs = plt.subplots(1, 1)
        axs.plot(u,v,'o');
        axs.set_title('Constellation');fig.show()
    return (u,v)

def piBy4_dqpsk_mod(a,fc,OF,enable_plot = False):
    """
    Modulate a binary stream using pi/4 DQPSK
    Parameters:
        a : input binary data stream (0's and 1's) to modulate
        fc : carrier frequency in Hertz
        OF : oversampling factor
    Returns:
        result : Dictionary containing the following keyword entries:
          s(t) : pi/4 QPSK modulated signal vector with carrier
          U(t) : differentially coded I-channel waveform (no carrier)
          V(t) : differentially coded Q-channel waveform (no carrier)
          t: time base
    """
    (u,v)=piBy4_dqpsk_Diff_encoding(a) # Differential Encoding for pi/4 QPSK
    #Waveform formation (similar to conventional QPSK)
    L = 2*OF # number of samples in each symbol (QPSK has 2 bits/symbol)
    
    U = np.tile(u, (L,1)).flatten('F')# odd bit stream at 1/2Tb baud
    V = np.tile(v, (L,1)).flatten('F')# even bit stream at 1/2Tb baud
    
    fs = OF*fc # sampling frequency
    
    t=np.arange(0, len(U)/fs,1/fs) #time base
    U_t = U*np.cos(2*np.pi*fc*t)
    V_t = -V*np.sin(2*np.pi*fc*t)
    s_t = U_t + V_t
    
    if enable_plot:
        fig = plt.figure(constrained_layout=True)
        
        from matplotlib.gridspec import GridSpec
        gs = GridSpec(3, 2, figure=fig)
        ax1 = fig.add_subplot(gs[0, 0]);ax2 = fig.add_subplot(gs[0, 1])
        ax3 = fig.add_subplot(gs[1, 0]);ax4 = fig.add_subplot(gs[1, 1])
        ax5 = fig.add_subplot(gs[-1,:])
        
        ax1.plot(t,U);ax2.plot(t,V)
        ax3.plot(t,U_t,'r');ax4.plot(t,V_t,'r')
        ax5.plot(t,s_t) #QPSK waveform zoomed to first few symbols
        
        ax1.set_ylabel('U(t)-baseband');ax2.set_ylabel('V(t)-baseband')
        ax3.set_ylabel('U(t)-with carrier');ax4.set_ylabel('V(t)-with carrier')
        ax5.set_ylabel('s(t)');ax5.set_xlim([0,10*L/fs])
        ax1.set_xlim([0,10*L/fs]);ax2.set_xlim([0,10*L/fs])
        ax3.set_xlim([0,10*L/fs]);ax4.set_xlim([0,10*L/fs])
        fig.show()
        
    result = dict()
    result['s(t)'] =s_t;result['U(t)'] = U;result['V(t)'] = V;result['t'] = t
    return result

def piBy4_dqpsk_Diff_decoding(w,z):
    """
    Phase Mapper for pi/4-DQPSK modulation
    Parameters:
        w - differentially coded I-channel bits at the receiver
        z - differentially coded Q-channel bits at the receiver
    Returns:
        a_hat - binary bit stream after differential decoding
    """
    if len(w)!=len(z): raise ValueError('Length mismatch between w and z')
    x = np.zeros(len(w)-1);y = np.zeros(len(w)-1);
    
    for k in range(0,len(w)-1):
        x[k] = w[k+1]*w[k] + z[k+1]*z[k]
        y[k] = z[k+1]*w[k] - w[k+1]*z[k]
    
    a_hat = np.zeros(2*len(x))
    a_hat[0::2] = (x > 0) # odd bits
    a_hat[1::2] = (y > 0) # even bits
    return a_hat

def piBy4_dqpsk_demod(r,fc,OF,enable_plot=False):
    """
    Differential coherent demodulation of pi/4-DQPSK
    Parameters:
        r : received signal at the receiver front end
        fc : carrier frequency in Hertz
        OF : oversampling factor (multiples of fc) - at least 4 is better
    Returns:
        a_cap :  detected binary stream
    """
    fs = OF*fc # sampling frequency
    L = 2*OF # samples in 2Tb duration
    t=np.arange(0, len(r)/fs,1/fs)
    w=r*np.cos(2*np.pi*fc*t) # I arm
    z=-r*np.sin(2*np.pi*fc*t) # Q arm
    w = np.convolve(w,np.ones(L)) # integrate for L (Tsym=2*Tb) duration
    z = np.convolve(z,np.ones(L)) # integrate for L (Tsym=2*Tb) duration
    w = w[L-1::L] # I arm - sample at every symbol instant Tsym
    z = z[L-1::L] # Q arm - sample at every symbol instant Tsym
    a_cap = piBy4_dqpsk_Diff_decoding(w,z)
    
    if enable_plot:#constellation plot
        fig, axs = plt.subplots(1, 1)
        axs.plot(w,z,'o')
        axs.set_title('Constellation');fig.show()
    return a_cap

def msk_mod(a, fc, OF, enable_plot = False):
    """
    Modulate an incoming binary stream using MSK
    Parameters:
        a : input binary data stream (0's and 1's) to modulate
        fc : carrier frequency in Hertz
        OF : oversampling factor (at least 4 is better)
    Returns:
        result : Dictionary containing the following keyword entries:
          s(t) : MSK modulated signal with carrier
          sI(t) : baseband I channel waveform(no carrier)
          sQ(t) : baseband Q channel waveform(no carrier)
          t: time base
    """ 
    ak = 2*a-1 # NRZ encoding 0-> -1, 1->+1
    ai = ak[0::2]; aq = ak[1::2] # split even and odd bit streams
    L = 2*OF # represents one symbol duration Tsym=2xTb
    
    #upsample by L the bits streams in I and Q arms
    from scipy.signal import upfirdn, lfilter
    ai = upfirdn(h=[1], x=ai, up = L)
    aq = upfirdn(h=[1], x=aq, up = L)
    
    aq = np.pad(aq, (L//2,0), 'constant') # delay aq by Tb (delay by L/2)
    ai = np.pad(ai, (0,L//2), 'constant') # padding at end to equal length of Q
    
    #construct Low-pass filter and filter the I/Q samples through it
    Fs = OF*fc;Ts = 1/Fs;Tb = OF*Ts
    t = np.arange(0,2*Tb+Ts,Ts)
    h = np.sin(np.pi*t/(2*Tb))# LPF filter
    sI_t = lfilter(b = h, a = [1], x = ai) # baseband I-channel
    sQ_t = lfilter(b = h, a = [1], x = aq) # baseband Q-channel
    
    t=np.arange(0, Ts*len(sI_t), Ts) # for RF carrier
    sIc_t = sI_t*np.cos(2*np.pi*fc*t) #with carrier
    sQc_t = sQ_t*np.sin(2*np.pi*fc*t) #with carrier
    s_t =  sIc_t - sQc_t# Bandpass MSK modulated signal
    
    if enable_plot:
        fig, (ax1,ax2,ax3) = plt.subplots(3, 1)
        
        ax1.plot(t,sI_t);ax1.plot(t,sIc_t,'r')
        ax2.plot(t,sQ_t);ax2.plot(t,sQc_t,'r')
        ax3.plot(t,s_t,'--')        
        ax1.set_ylabel('$s_I(t)$');ax2.set_ylabel('$s_Q(t)$')
        ax3.set_ylabel('s(t)')        
        ax1.set_xlim([-Tb,20*Tb]);ax2.set_xlim([-Tb,20*Tb])
        ax3.set_xlim([-Tb,20*Tb])
        fig.show()
        
    result = dict()
    result['s(t)'] =s_t;result['sI(t)'] = sI_t;result['sQ(t)'] = sQ_t;result['t'] = t
    return result

def msk_demod(r,N,fc,OF):
    """
    MSK demodulator
    Parameters:
        r : received signal at the receiver front end
        N : number of symbols transmitted
        fc : carrier frequency in Hertz
        OF : oversampling factor (at least 4 is better)
    Returns:
        a_hat : detected binary stream
    """
    L = 2*OF # samples in 2Tb duration
    Fs=OF*fc;Ts=1/Fs;Tb = OF*Ts; # sampling frequency, durations
    t=np.arange(-OF, len(r) - OF)/Fs # time base
    
    # cosine and sine functions for half-sinusoid shaping
    x=abs(np.cos(np.pi*t/(2*Tb)));y=abs(np.sin(np.pi*t/(2*Tb)))
    
    u=r*x*np.cos(2*np.pi*fc*t) # multiply I by half cosines and cos(2pifct)
    v=-r*y*np.sin(2*np.pi*fc*t) # multiply Q by half sines and sin(2pifct)
    
    iHat = np.convolve(u,np.ones(L)) # integrate for L (Tsym=2*Tb) duration
    qHat = np.convolve(v,np.ones(L)) # integrate for L (Tsym=2*Tb) duration
    
    iHat= iHat[L-1:-1-L:L]  # I- sample at the end of every symbol
    qHat= qHat[L+L//2-1:-1-L//2:L] # Q-sample from L+L/2th sample
    
    a_hat = np.zeros(N)
    a_hat[0::2] = iHat > 0 # thresholding - odd bits
    a_hat[1::2] = qHat > 0 # thresholding - even bits
    
    return a_hat

def gaussianLPF(BT, Tb, L, k):
    """
    Function to generate filter coefficients of Gaussian low pass filter (used in gmsk_mod)
    Parameters:
        BT : BT product - Bandwidth x bit period
        Tb : bit period
        L : oversampling factor (number of samples per bit)
        k : span length of the pulse (bit interval)        
    Returns:
        h_norm : normalized filter coefficients of Gaussian LPF
    """
    B = BT/Tb # bandwidth of the filter
    t = np.arange(start = -k*Tb, stop = k*Tb + Tb/L, step = Tb/L) # truncated time limits for the filter
    h = B*np.sqrt(2*np.pi/(np.log(2)))*np.exp(-2 * (t*np.pi*B)**2 /(np.log(2)))
    h_norm=h/np.sum(h)
    return h_norm

def gmsk_mod(a,fc,L,BT,enable_plot=False):
    """
    Function to modulate a binary stream using GMSK modulation
    Parameters:
        BT : BT product (bandwidth x bit period) for GMSK
        a : input binary data stream (0's and 1's) to modulate
        fc : RF carrier frequency in Hertz
        L : oversampling factor
        enable_plot: True = plot transmitter waveforms (default False)
    Returns:
        (s_t,s_complex) : tuple containing the following variables
            s_t : GMSK modulated signal with carrier s(t)
            s_complex : baseband GMSK signal (I+jQ)
    """
    from scipy.signal import upfirdn,lfilter
    
    fs = L*fc; Ts=1/fs;Tb = L*Ts; # derived waveform timing parameters
    c_t = upfirdn(h=[1]*L, x=2*a-1, up = L) #NRZ pulse train c(t)
    
    k=1 # truncation length for Gaussian LPF
    h_t = gaussianLPF(BT,Tb,L,k) # Gaussian LPF with BT=0.25
    b_t = np.convolve(h_t,c_t,'full') # convolve c(t) with Gaussian LPF to get b(t)
    bnorm_t = b_t/max(abs(b_t)) # normalize the output of Gaussian LPF to +/-1
    
    h = 0.5;
    phi_t = lfilter(b = [1], a = [1,-1], x = bnorm_t*Ts) * h*np.pi/Tb # integrate to get phase information
    
    I = np.cos(phi_t)
    Q = np.sin(phi_t) # cross-correlated baseband I/Q signals
    
    s_complex = I - 1j*Q # complex baseband representation
    t = Ts* np.arange(start = 0, stop = len(I)) # time base for RF carrier
    sI_t = I*np.cos(2*np.pi*fc*t); sQ_t = Q*np.sin(2*np.pi*fc*t)
    s_t = sI_t - sQ_t # s(t) - GMSK with RF carrier
    
    if enable_plot:
        fig, axs = plt.subplots(2, 4)
        axs[0,0].plot(np.arange(0,len(c_t))*Ts,c_t);axs[0,0].set_title('c(t)');axs[0,0].set_xlim(0,40*Tb)
        axs[0,1].plot(np.arange(-k*Tb,k*Tb+Ts,Ts),h_t);axs[0,1].set_title('$h(t): BT_b$='+str(BT))
        axs[0,2].plot(t,I,'--');axs[0,2].plot(t,sI_t,'r');axs[0,2].set_title('$I(t)cos(2 \pi f_c t)$');axs[0,2].set_xlim(0,10*Tb)
        axs[0,3].plot(t,Q,'--');axs[0,3].plot(t,sQ_t,'r');axs[0,3].set_title('$Q(t)sin(2 \pi f_c t)$');axs[0,3].set_xlim(0,10*Tb)
        axs[1,0].plot( np.arange(0,len(bnorm_t))*Ts,bnorm_t);axs[1,0].set_title('b(t)');axs[1,0].set_xlim(0,40*Tb)
        axs[1,1].plot(np.arange(0,len(phi_t))*Ts, phi_t);axs[1,1].set_title('$\phi(t)$')
        axs[1,2].plot(t,s_t);axs[1,2].set_title('s(t)');axs[1,2].set_xlim(0,20*Tb)
        axs[1,3].plot(I,Q);axs[1,3].set_title('constellation')
        fig.show()
        
    return (s_t,s_complex)
def gmsk_demod(r_complex,L):
    """
    Function to demodulate a baseband GMSK signal
    Parameters:
        r_complex : received signal at receiver front end (complex form - I+jQ)
        L : oversampling factor
    Returns:
        a_hat : detected binary stream
    """
    I=np.real(r_complex); Q = -np.imag(r_complex); # I,Q streams
    z1 = Q * np.hstack((np.zeros(L), I[0:len(I)-L]))
    z2 = I * np.hstack((np.zeros(L), Q[0:len(I)-L]))
    z = z1 - z2
    a_hat = (z[2*L-1:-L:L] > 0).astype(int) # sampling and hard decision
    #sampling indices depends on the truncation length (k) of Gaussian LPF defined in the modulator
    return a_hat

def bfsk_mod(a,fc,fd,L,fs,fsk_type='coherent',enable_plot = False):
    """
    Function to modulate an incoming binary stream using BFSK
    Parameters:
        a : input binary data stream (0's and 1's) to modulate
        fc : center frequency of the carrier in Hertz
        fd : frequency separation measured from Fc
        L : number of samples in 1-bit period
        fs : Sampling frequency for discrete-time simulation
        fsk_type : 'coherent' (default) or 'noncoherent' FSK generation
        enable_plot: True = plot transmitter waveforms (default False)
    Returns:
        (s_t,phase) : tuple containing following parameters
            s_t : BFSK modulated signal
            phase : initial phase generated by modulator, applicable only for coherent FSK. It can be used when using coherent detection at Rx    
    """
    from scipy.signal import upfirdn
    a_t = upfirdn(h=[1]*L, x=a, up = L) #data to waveform
    
    t = np.arange(start=0,stop=len(a_t))/fs #time base    
    if fsk_type.lower() == 'noncoherent':
        c1 = np.cos(2*np.pi*(fc+fd/2)*t+2*np.pi*np.random.random_sample()) # carrier 1 with random phase
        c2 = np.cos(2*np.pi*(fc-fd/2)*t+2*np.pi*np.random.random_sample()) # carrier 2 with random phase
    else: #coherent is default
        phase=2*np.pi*np.random.random_sample() # random phase from uniform distribution [0,2pi)
        c1 = np.cos(2*np.pi*(fc+fd/2)*t+phase) # carrier 1 with random phase
        c2 = np.cos(2*np.pi*(fc-fd/2)*t+phase) # carrier 2 with the same random phase
    s_t = a_t*c1 +(-a_t+1)*c2 # BFSK signal (MUX selection)
    
    if enable_plot:
        fig, (ax1,ax2) = plt.subplots(2, 1)
        ax1.plot(t,a_t);ax2.plot(t,s_t);fig.show()
        
    return (s_t,phase)

def bfsk_coherent_demod(r_t,phase,fc,fd,L,fs):
    """
    Coherent demodulation of BFSK modulated signal
    Parameters:
        r_t : BFSK modulated signal at the receiver r(t)
        phase : initial phase generated at the transmitter
        fc : center frequency of the carrier in Hertz
        fd : frequency separation measured from Fc
        L : number of samples in 1-bit period
        fs : Sampling frequency for discrete-time simulation    
    Returns:
        a_hat : data bits after demodulation        
    """
    t = np.arange(start=0,stop=len(r_t))/fs # time base
    x = r_t*(np.cos(2*np.pi*(fc+fd/2)*t+phase)-np.cos(2*np.pi*(fc-fd/2)*t+phase))
    y = np.convolve(x,np.ones(L)) # integrate/sum from 0 to L
    a_hat = (y[L-1::L]>0).astype(int) # sample at every sampling instant and detect
    return a_hat

def bfsk_noncoherent_demod(r_t,fc,fd,L,fs):
    """
    Non-coherent demodulation of BFSK modulated signal
    Parameters:
        r_t : BFSK modulated signal at the receiver r(t)
        fc : center frequency of the carrier in Hertz
        fd : frequency separation measured from Fc
        L : number of samples in 1-bit period
        fs : Sampling frequency for discrete-time simulation
    Returns:
        a_hat : data bits after demodulation        
    """
    t = np.arange(start=0,stop=len(r_t))/fs # time base
    f1 = (fc+fd/2); f2 = (fc-fd/2)
    #define four basis functions
    p1c = np.cos(2*np.pi*f1*t); p2c = np.cos(2*np.pi*f2*t)
    p1s = -1*np.sin(2*np.pi*f1*t); p2s = -1*np.sin(2*np.pi*f2*t)
    # multiply and integrate from 0 to L
    r1c = np.convolve(r_t*p1c,np.ones(L)); r2c = np.convolve(r_t*p2c,np.ones(L))
    r1s = np.convolve(r_t*p1s,np.ones(L)); r2s = np.convolve(r_t*p2s,np.ones(L))
    # sample at every sampling instant
    r1c = r1c[L-1::L]; r2c = r2c[L-1::L]
    r1s = r1s[L-1::L]; r2s = r2s[L-1::L]
    # square and add
    x = r1c**2 + r1s**2
    y = r2c**2 + r2s**2
    a_hat=((x-y)>0).astype(int) # compare and decide
    return a_hat