KerberosSDR code notes (5) signal processing (two methods of sampling time delay calculation, phase difference calculation, MUSIC algorithm)

https://github.com/rtlsdrblog/kerberossdr/blob/master/_signalProcessing/hydra_signal_processor.py

This part is the most important part, it contains several important algorithms, sampling time synchronization, phase synchronization, and calling the spatial spectrum estimation algorithm.

# KerberosSDR Signal Processor
#
# Copyright (C) 2018-2019  Carl Laufer, Tamás Pető
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <https://www.gnu.org/licenses/>.
#
#
# -*
# - coding: utf-8 -*-

import sys
import os
import time
# Math support
import numpy as np

# Signal processing support
from scipy import fft,ifft
from scipy import signal
from scipy.signal import correlate #互相关函数

# Plot support
#import matplotlib.pyplot as plt

# GUI support
from PyQt4 import QtGui, QtCore

# Import the pyArgus module
#root_path = os.getcwd()
#pyargus_path = os.path.join(os.path.join(root_path, "pyArgus"), "pyArgus")
#sys.path.insert(0, pyargus_path)
#import directionEstimation_v1p15 as de

from pyargus import directionEstimation as de #测向算法


# Import APRiL module
#april_path = os.path.join(os.path.join(root_path, "_APRIL"), "APRIL")
#sys.path.insert(0, april_path)
#import channelPreparation as cp
#import clutterCancellation as cc
#import detector as det

from pyapril import channelPreparation as cp #这些都是被动雷达的
from pyapril import clutterCancellation as cc
from pyapril import detector as det
from pyapril.hitProcessor import CA_CFAR

class SignalProcessor(QtCore.QThread):
    #初始化了几个信号
    signal_spectrum_ready = QtCore.pyqtSignal() #频谱计算完成的信号
    signal_sync_ready = QtCore.pyqtSignal() #采样延迟计算完成的信号
    signal_DOA_ready = QtCore.pyqtSignal() #测向计算完成的信号
    signal_overdrive = QtCore.pyqtSignal(int) #信号是否饱和的信号,但是没用到
    signal_period    = QtCore.pyqtSignal(float) #计算周期
    signal_PR_ready = QtCore.pyqtSignal()
    def __init__(self, parent=None, module_receiver=None):
        """
            Description:
            ------------
            Parameters:
            -----------
            Return values:
            --------------
        """
        super(SignalProcessor, self).__init__(parent)

        self.module_receiver = module_receiver #传入接收机对象给新处理模块
        self.en_spectrum = True  #是否显示频谱
        self.en_sync = True  #这里对应的是界面上的显示同步曲线(enable sync display)
        self.en_sample_offset_sync = False#这里对应的是界面上的执行同步按钮(sample sync)
        self.en_record = False #是否保存数据
        self.en_calib_iq = False #是否做相位差校准
        self.en_calib_DOA_90 = False
        self.en_DOA_estimation = False #是否要做测向
        self.en_PR_processing = False
        self.en_PR_autodet = False
        
        # DOA processing options
        self.en_DOA_Bartlett = False
        self.en_DOA_Capon = False
        self.en_DOA_MEM = False
        self.en_DOA_MUSIC = False #这是选择使用哪个测向算法,一般用MUSIC
        self.en_DOA_FB_avg = False #这是是否打开FB average
        self.DOA_inter_elem_space = 0.5 #这是lamda
        self.DOA_ant_alignment = "ULA" #这是阵型
        
        # Passive Radar processing parameters #这都是被动雷达用的参数
        self.ref_ch_id = 0
        self.surv_ch_id = 1
        self.en_td_filtering = False
        self.td_filter_dimension = 1        
        self.max_Doppler = 500  # [Hz]
        self.windowing_mode = 0
        self.max_range = 128  # [range cell]
        self.cfar_win_params = [10,10,4,4] # [Est. win length, Est. win width, Guard win length, Guard win width]
        self.cfar_threshold = 13
        self.RD_matrix = np.ones((10,10))
        self.hit_matrix = np.ones((10,10))
        self.RD_matrix_last = np.ones((10,10))
        self.RD_matrix_last_2 = np.ones((10,10))
        self.RD_matrix_last_3 = np.ones((10,10))
        
        self.center_freq = 0  # TODO: Initialize this [Hz] #中心频率
        self.fs = 1.024 * 10**6  # Decimated sampling frequncy - Update from GUI
        #这是经过降采样的采样率
        #self.sample_size = 2**15
        self.channel_number = 4 #接收机通道数量
        
        # Processing parameters        
        self.test = None
        self.spectrum_sample_size = 2**14 #2**14 #用于频谱运算的采样点数量
        self.DOA_sample_size = 2**15 # Connect to GUI value?? #用于测向运算的采样点数量
        self.xcorr_sample_size = 2**18 #2**18 #用于互相关运算的采样点数量
        self.spectrum = np.ones((self.channel_number+1,self.spectrum_sample_size), dtype=np.float32)   
        #这里存储的是频谱的数据,每个通道一行,所以行数是channel_number,
        #但是第0行要存储横轴的频率值信息,所以还要+1
        #列的数量就是频谱显示的采样点数量
        self.xcorr = np.ones((self.channel_number-1,self.xcorr_sample_size*2), dtype=np.complex64) 
        #这里存储的是互相关数据,都是另3个通道与第1个通道互相关运算的结果
        #这个结果只有3个,所以只有channel_number-1行,互相关是卷积,长度会补得比函数本身长
        #所以是原函数的采样点数量*2 xcorr_sample_size*2
        self.phasor_win = 2**10 # Phasor plot window#这个本来用来绘制相位差的但是实际没用
        self.phasors = np.ones((self.channel_number-1, self.phasor_win), dtype=np.complex64)
        self.run_processing = False #这是用来控制主循环的标志位
        
        # Result vectors
        self.delay_log= np.array([[0],[0],[0]]) 
        #记录采样时间延迟的数组,另3个通道相对第1个通道的,每个结果放在一行里,所以有3行
        #每一列是不同时间点求的延迟结果
        self.phase_log= np.array([[0],[0],[0]])
        #记录相位差的数组,类似采样时间延迟,也是另3个通道相对第一个通道的,有3行
        #每一列是不同时间点求的相位差的结果
        self.DOA_Bartlett_res = np.ones(181)
        self.DOA_Capon_res = np.ones(181)
        self.DOA_MEM_res = np.ones(181)
        self.DOA_MUSIC_res = np.ones(181)
        #这里存储的是MUSIC算法计算出的功率谱密度函数
        self.DOA_theta = np.arange(0,181,1)
        #这里存储的是DOA测出的角度
        
        # Auto resync params #自动定期同步用的参数,实际没有实现
        self.lastTime = 0
        self.runningSync = 0
        self.timed_sync = False
        self.noise_checked = False
        self.resync_time = -1

    def run(self):
        # xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
        #    
        # xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
        self.run_processing = True
        
        while self.run_processing:
            start_time = time.time() #记录进入循环的时间点

            # Download samples
            #if(self.en_sync or self.en_spectrum): 
            time.sleep(0.25) # You can play with this value, but it may affect stability   
            
            self.module_receiver.download_iq_samples()
            #告诉receiver程序,可以下载iq数据了,它会通知c程序开门
            #这样4个接收机通道的数据都会存进iq_samples这个二维数组里
            self.DOA_sample_size = self.module_receiver.iq_samples[0,:].size
            #用iq_samples中得到的实际长度(第1个接收机通道的数据长度)来决定DOA计算的长度
            self.xcorr_sample_size = self.module_receiver.iq_samples[0,:].size
            #互相关计算的长度也是iq_samples的第1个通道的实际长度
            self.xcorr = np.ones((self.channel_number-1,self.xcorr_sample_size*2), dtype=np.complex64) 
            #这里是存储互相关结果的地方,前面说了,互相关结果是后3个通道与第1个通道做的
            #所以能得到3组结果,就是3行,所以行数是channel_number-1
            #但是互相关结果的长度由于是卷积运算,所以比原来的函数长,要xcorr_sample_size*2
            #原函数里的采样点都是复数,互相关结果也是复数,所以dtype是np.complex64
            
            # Check overdrive #检测信号饱和,实际没有用
            if self.module_receiver.overdrive_detect_flag:
                self.signal_overdrive.emit(1)
            else:
                self.signal_overdrive.emit(0)
            
            # Display spectrum #显示频谱
            if self.en_spectrum:  #先判断是否勾选了显示频谱
                self.spectrum[0, :] = np.fft.fftshift(np.fft.fftfreq(self.spectrum_sample_size, 1/self.fs))/10**6
                #self.spectrum的第0行存储的应该是频谱的频率范围
                #np.fft.fftfreq可以生成这个范围,它的参数是fft点数(输入和输出的数据长度)
                #和采样周期(也就是采样率的倒数)
                #np.fft.fftshift的作用是将0频点移到中间
                #最后还要除以10**6,只保存MHz前的数字
                m = self.channel_number
                #self.spectrum[1:m+1,:] = 10*np.log10(np.fft.fftshift(np.abs(np.fft.fft(self.module_receiver.iq_samples[0:m, 0:self.spectrum_sample_size]))))

                for m in range(self.channel_number):
                    self.spectrum[m+1,:] = 10*np.log10(np.fft.fftshift(np.abs(np.fft.fft(self.module_receiver.iq_samples[m, 0:self.spectrum_sample_size]))))
                    #用一个循环来生成4个接收机通道的fft结果,并存入self.spectrum的后几行
                    #首先读出iq_samples里第m行的数据,代表第m个接收机的采样点
                    #纵轴从0开始表示第一个数据开始算
                    #直到达到要用来算频谱的数据长度spectrum_sample_size
                    #有了数据后就用np.fft.fft来计算fft结果
                    #再求np.abs这样只保留实数值,就是我们熟悉的频谱图了
                    #然后再用np.fft.fftshift把零频点移到频谱中间
                    #再用10*np.log10把单位转为dB
                    
                self.signal_spectrum_ready.emit() #发出信号表示频谱算好了
            
            # Synchronization #同步
            if self.en_sync or self.timed_sync: 
                #print("Sync graph enabled") #这里勾选了同步显示
                self.sample_delay() #一旦勾选同步显示,就会计算几个通道的延迟大小
                self.signal_sync_ready.emit() #上面函数返回表示计算完成了,发出信号
            
            # Sample offset compensation request #延迟补偿
            if self.en_sample_offset_sync: #这里是点击了同步按钮
                self.module_receiver.set_sample_offsets(self.delay_log[:,-1])
                #把之前计算到的各通道的延迟大小的最新值应用到接收机python程序里
                #[:,-1]代表所有行的最后一列数据
                self.en_sample_offset_sync = False #纠正完了采样延迟就可以把这个按钮取消了
            
            # IQ calibration request #相位差校准
            if self.en_calib_iq: #点击相位差校准按钮
                # IQ correction
                for m in range(self.channel_number):
                    self.module_receiver.iq_corrections[m] *= np.size(self.module_receiver.iq_samples[0, :])/(np.dot(self.module_receiver.iq_samples[m, :],self.module_receiver.iq_samples[0, :].conj()))
                #这里在计算每个接收机通道需要的校准值,这个校准值的分母是np.dot两个复数点乘
                #2个复数点乘得到的复数的相位是这2个复数的相位之和
                #第一个复数是m通道的采样点
                #第二个复数是第1个接收机通道的共轭(相当于相位加了负号)
                #这样点乘得到的复数的相位就是m通道相对第1个接收机通道的相位差
                #把含有相位差信息的复数放到分母上,这样其它通道乘以它就会把自己相位减去相位差
                #分子是第一个通道的采样点大小,应该是用来归一化用的?
                #iq_correction原本的值是1,所以第一次运算*=就跟=直接赋值是一样的
                #后期再做这一步就是把最新的相位差继续乘上去
                #相当于继续纠正相位差(但一般一次就够了,不需要后续再做相位差校准)
                c = np.sqrt(np.sum(np.abs(self.module_receiver.iq_corrections)**2))
                #这里先对前面算出来的iq_correction求模,再求平方,又把数组全部加起来后开根号
                #得到的c应该也是归一化用的?
                self.module_receiver.iq_corrections = np.divide(self.module_receiver.iq_corrections, c)
                #用np.divide把之前求的iq_corrections除以c就得到了要用的相位差校准值
                #print("Corrections: ",self.module_receiver.iq_corrections)
                self.en_calib_iq = False #得到相位差校准值后就可以把相位差校准按钮取消了
            
            if self.en_calib_DOA_90: #这个暂时还在实验阶段
                #TODO: Experimental only for UCA, implement this properly!
                # This calibration is currently done for 0 deg not 90 
                x = self.DOA_inter_elem_space * np.cos(2*np.pi/4 * np.arange(4))
                y = self.DOA_inter_elem_space * np.sin(-2*np.pi/4 * np.arange(4)) # For this specific array only
                ref_vector = de.gen_scanning_vectors(4, x, y, np.zeros(1))[:, 0]                
                #ref_vector = np.exp(1j*2*np.pi*0.5*np.cos(np.radians(0-np.arange(self.channel_number)*(360)/self.channel_number))) # UCA                
                N= np.size(self.module_receiver.iq_samples[0, :])
                for m in range(self.channel_number):
                    self.module_receiver.iq_corrections[m] *= ref_vector[m]*N/(np.dot(self.module_receiver.iq_samples[m, :],self.module_receiver.iq_samples[0, :].conj()))                
                #print("Corrections: ",self.module_receiver.iq_corrections)
                self.en_calib_DOA_90 = False
                
            # Direction of Arrival estimation #DOA计算
            if self.en_DOA_estimation: #如果勾选了DOA计算
                # Get FFT for squelch
                self.spectrum[1,:] = 10*np.log10(np.fft.fftshift(np.abs(np.fft.fft(self.module_receiver.iq_samples[0, 0:self.spectrum_sample_size]))))
                #获取第1个接收机通道的fft,用来滤除小信号

                self.estimate_DOA() #计算DOA
                self.signal_DOA_ready.emit() #计算完DOA要发出信号
            
            # Passive Radar processing #被动雷达处理
            if self.en_PR_processing:
#                self.module_receiver.channel_number = 2
                self.PR_processing()
                self.signal_PR_ready.emit()
#            else:
#                self.module_receiver.channel_number = 4
            

            # Record IQ samples #保存iq采样点
            if self.en_record: #如果打开了会把iq_samples里的采样点存下来
                np.save('hydra_samples.npy', self.module_receiver.iq_samples)


# Code to maintain sync #定期同步,但是没有使用
            '''if self.timed_sync and not self.en_sync:
                if not self.noise_checked:
                    self.module_receiver.switch_noise_source(0)
                self.timed_sync = False
                self.en_sample_offset_sync=True
                self.runningSync = 0
            resync_on = True
            if(self.resync_time < 10):
                resync_on = False
            if(((start_time - self.lastTime) > self.resync_time) and not self.en_sync and resync_on):
                self.lastTime = start_time
                self.module_receiver.switch_noise_source(1)
                time.sleep(0.1)
                self.runningSync = 1
                self.timed_sync = True'''

            stop_time = time.time() #循环结束前计时
            self.signal_period.emit(stop_time - start_time) 
            #这里会发出信号,并且包含测向运行时所有的获取数据+运算需要的时间多长


    def sample_delay(self): #计算采样时间延迟
        #print("Entered sample delay func")
        N = self.xcorr_sample_size #互相关大小
        iq_samples = self.module_receiver.iq_samples[:, 0:N] 
        #根据要计算的互相关长度,从iq_samples里取出4个接收机通道的数据
        #互相关结果是xcorr_sample_size的两倍,xcorr_sample_size只是互相关的输入原函数长度
        #xcorr_sample_size=2**18=256*1024,iq_samples里的数据正好够用
       
        delays = np.array([[0],[0],[0]]) #延迟
        phases = np.array([[0],[0],[0]]) #相位差
        #上面的2个数组看着都是二维,实际都是一维数组,是3行1列的数组
        #delays只存储3个值,是目前的另3个通道相对第1通道的延迟
        #都是当前值,不包含历史信息,phases也是如此

        # Channel matching
        np_zeros = np.zeros(N, dtype=np.complex64)#用来填补互相关原函数用的
        #互相关运算是2个原函数求卷积,卷积结果的长度是原函数长度的2倍,原函数不够长
        #需要用0把它们填满,使其长度跟结果一样长,所以np_zeros长度也是N
        x_padd = np.concatenate([iq_samples[0, :], np_zeros])
        #这个concatenate就是用来把原函数和np_zeros接起来的
        #根据卷积运算的要求,两个原函数一个从左往右,另一个从右往左,各个点相乘再求和
        #所以x_padd是把第1个通道的值取出来再把np_zeros填在右侧,并且接起来
        x_fft = np.fft.fft(x_padd)
        #为了计算方便,在原始数据数据处理完后选择在频域计算
        #频域只要求一个原函数乘以另一个原函数的共轭就得到互相关的fft,再用ifft变回时域就行
        #所以这里用x_padd做了fft得到了x_fft
        for m in np.arange(1, self.channel_number):
            y_padd = np.concatenate([np_zeros, iq_samples[m, :]])
            y_fft = np.fft.fft(y_padd)
            #类似的,这里对第m通道的采样数据求了y_padd,然后再求了y_fft
            self.xcorr[m-1] = np.fft.ifft(x_fft.conj() * y_fft)
            #这样就得到了第m通道相对于第1个接收机通道(m=0)的互相关函数
            delay = np.argmax(np.abs(self.xcorr[m-1])) - N
            #互相关函数的模的最大值对应横坐标就是延迟大小,为什么要减去N,暂时不知道?
            #这个可能是类似fftshift类似,延迟可能是正可能是负(提前)
            #但是目前的互相关函数的中间点应该不在0点而是在N点,所以要减去N
            #相当于把函数往左搬回0点,得到的delay才是真正的延迟
            #phase = np.rad2deg(np.angle(self.xcorr[m-1, delay + N]))
            phase = np.rad2deg(np.angle(self.xcorr[m-1, N]))
            #互相关函数第N个位置,也就是中间点或者理论上对应0位置的点的复数角就是相位差?
            #这里还不是很清楚
            
            #offset = 50000                     
            #self.phasors[m-1, :] = (iq_samples[0, offset: self.phasor_win+offset] * iq_samples[m, offset+delay: self.phasor_win+offset+delay].conj())
            #self.phasors[m-1, :] = (iq_samples[0, 0: self.phasor_win] * iq_samples[m, 0: self.phasor_win].conj())
            #本来记录用的相位差也是把一个采样点的值乘以另一个采样点的共轭求出来的
            #与相位差校准时原理一样,但是现在不用了,直接用互相关结果的复数来得到?
            """
            self.IQSamples[1, :] = np.roll(self.IQSamples[1, :], delay * -1)
            if delay > 0:
                self.IQSamples[1, -delay::] = np.zeros(delay, dtype=np.complex64)
            if delay < 0:
                self.IQSamples[1, 0: np.abs(delay)] = np.zeros(np.abs(delay), dtype=np.complex64)
            """
            #msg = "[ INFO ] delay: " + str(delay)
            #print(msg)
            delays[m-1,0] = delay 
            phases[m-1,0] = phase
            #计算完成后就要把计算结果存入3行1列的数组了,列号都是0,行号是当前计算通道-1
            #比如第2个接收机通道(m=1)相对于第1个通道的延迟存入delays[0,0]是第一行第一列
            #第3个接收机通道(m=2)存入delays[1,0]第二行第一列
            #第4个接收机通道(m=3)存入delays[2,0]第三行第一列
            #相位差phases也是类似

        self.delay_log = np.concatenate((self.delay_log, delays),axis=1)
        self.phase_log = np.concatenate((self.phase_log, phases),axis=1)
        #得到当前时间点的数据后,就要把它们存入历史记录了
        #直接用np.concatenate把2个3行1列的数组接到delay_log和phase_log的最后就行了
    
    def delete_sync_history(self): #删除同步历史信息
        self.delay_log= np.array([[0],[0],[0]]) #清空采样时间延迟数组
        self.phase_log= np.array([[0],[0],[0]]) #清空相位差数组
        #上面2个都是二维数组,有3行,每行好多列,分别代表另3个通道相对第1个通道的
        #采样时间延迟和相位差数据,有好多列是因为把不同时间算出的数据都存下来了
        #越靠后的数据越新
    

    def estimate_DOA(self): #计算DOA,主要都在调用pyargus的de包
        #print("[ INFO ] Python DSP: Estimating DOA")

        iq_samples = self.module_receiver.iq_samples[:, 0:self.DOA_sample_size]
        #首先要截取iq_samples,4个通道的数据都要,所以每行都要了,行用了冒号表示
        #但是DOA算法只需要计算DOA_sample_size长度的数据,所以列是0:self.DOA_sample_size
        # Calculating spatial correlation matrix #计算协方差矩阵
        R = de.corr_matrix_estimate(iq_samples.T, imp="fast")

        if self.en_DOA_FB_avg: #如果是线阵并且打开了FB average要更新协方差矩阵
            R=de.forward_backward_avg(R)

        M = np.size(iq_samples, 0) #iq_samples的行数,就是接收机通道数,4

        if self.DOA_ant_alignment == "UCA": #均匀圆阵
            self.DOA_theta =  np.linspace(0,360,361)
            #scanning_vectors = de.gen_uca_scanning_vectors(M, self.DOA_inter_elem_space, self.DOA_theta)
            x = self.DOA_inter_elem_space * np.cos(2*np.pi/M * np.arange(M))
            y = self.DOA_inter_elem_space * np.sin(-2*np.pi/M * np.arange(M)) # For this specific array only
            scanning_vectors = de.gen_scanning_vectors(M, x, y, self.DOA_theta)
            #生成扫描向量

             # DOA estimation
            if self.en_DOA_Bartlett:
                self.DOA_Bartlett_res = de.DOA_Bartlett(R, scanning_vectors)
            if self.en_DOA_Capon:
                self.DOA_Capon_res = de.DOA_Capon(R, scanning_vectors)
            if self.en_DOA_MEM:
                self.DOA_MEM_res = de.DOA_MEM(R, scanning_vectors,  column_select = 0)
            if self.en_DOA_MUSIC:
                self.DOA_MUSIC_res = de.DOA_MUSIC(R, scanning_vectors, signal_dimension = 1)
                #使用MUSIC算法,计算协方差矩阵和扫描向量,得到DOA结果

        elif self.DOA_ant_alignment == "ULA": #均匀线阵
            self.DOA_theta =  np.linspace(-90,90,181)
            x = np.zeros(M)
            y = np.arange(M) * self.DOA_inter_elem_space            
            scanning_vectors = de.gen_scanning_vectors(M, x, y, self.DOA_theta)

            # DOA estimation
            if self.en_DOA_Bartlett:
                self.DOA_Bartlett_res = de.DOA_Bartlett(R, scanning_vectors)
            if self.en_DOA_Capon:
                self.DOA_Capon_res = de.DOA_Capon(R, scanning_vectors)
            if self.en_DOA_MEM:
                self.DOA_MEM_res = de.DOA_MEM(R, scanning_vectors,  column_select = 0)
            if self.en_DOA_MUSIC:
                self.DOA_MUSIC_res = de.DOA_MUSIC(R, scanning_vectors, signal_dimension = 1)

        #print(self.DOA_MUSIC_res)
        

    def PR_processing(self): #被动雷达的算法
        #print("[ INFO ] Python DSP: Start Passive Radar processing")

        ref_ch = self.module_receiver.iq_samples[self.ref_ch_id, :]
        surv_ch = self.module_receiver.iq_samples[self.surv_ch_id, :]

        if self.en_td_filtering:
            surv_ch, w = cc.Wiener_SMI_MRE(ref_ch, surv_ch, self.td_filter_dimension)
#            surv_ch, w = cc.fast_wiener(self.td_filter_dimension, ref_ch, surv_ch)
            #surv_ch, w = cc.Wiener_SMI(ref_ch, surv_ch, self.td_filter_dimension, imp="fast")
            #print("[ DONE ] Timde domain filtering finished")


        if(self.windowing_mode == 0):
           pass
           #surv_ch = det.windowing(surv_ch, "Rectangular")
        else:
           surv_ch = det.windowing(surv_ch, "Hamming")
           
        self.RD_matrix = det.cc_detector_ons(ref_ch, surv_ch, self.fs, self.max_Doppler, self.max_range, verbose=0, Qt_obj=None)
        
        if self.en_PR_autodet:
            self.hit_matrix = CA_CFAR(self.RD_matrix,self.cfar_win_params, self.cfar_threshold)            
        #print("[ DONE ] Range-Doppler processing finished")
    def stop(self):
        self.run_processing = False


def busy_wait(dt):
    current_time = time.time()
    while (time.time() < current_time+dt):
        pass

Next, take a look at the specific implementation of several functions involved in the most commonly used UCA uniform circular array:

de_corr_matrix_estimate , de.gen_scanning_vectors , de.DOA_MUSIC

They all come from the pyargus library

https://github.com/petotamas/pyArgus

This is a library related to the antenna array. There are three main files, antennaArrayPattern.py, which is used to draw the pattern of the array antenna, beamform.py, which is used to achieve beamforming, directionEstimation.py, to calculate the signal arrival angle Yes, in addition to some procedures and supporting documents for testing.

These modules are not dependent on each other, so we can directly look at directionEstimation.py

https://github.com/petotamas/pyArgus/blob/master/pyArgus/directionEstimation.py

And its documentation:

https://github.com/petotamas/pyArgus/blob/master/docs/nb_direction_of_arrival_estimaton.ipynb

ipynb can be viewed directly with github, but the network speed may not be open, you can try this website nbviewer.jupyter.org

The principle of direction finding can refer to this article:

https://blog.csdn.net/qq_23947237/article/details/82318222

It explains why the direction can be measured after the steps it proposes.

The summary of this step is:

1. First find the array matrix (scanning vector)

The array matrix is ​​related to the formation. The blog above uses a linear array, but our actual python code uses a more general code. You only need to fill in the x, y information to generate the area array array matrix.

2. Find the covariance matrix (correlation matrix)

The covariance matrix can be estimated using a large number of sampling points received on the receiver channel

3. Find the noise subspace

Perform eigenvalue decomposition on the covariance matrix to get this noise subspace

4. Calculate the spatial spectrum function

Using the array matrix and noise subspace to calculate the spatial spectrum function, you can get the waveform of the music, and finally you can get the incident angle by searching for the peak (do it in other parts of the program).

The following is the code that I will call when using UCA and MUSIC algorithm extracted from hydra_signal_processing.py. It basically follows the above steps, except that the order of the first and second steps is changed. This has no effect.

iq_samples = self.module_receiver.iq_samples[:, 0:self.DOA_sample_size]
#首先要截取iq_samples,4个通道的数据都要,所以每行都要了,行用了冒号表示
#但是DOA算法只需要计算DOA_sample_size长度的数据,所以列是0:self.DOA_sample_size
        
R = de.corr_matrix_estimate(iq_samples.T, imp="fast")
#使用大量的采样点来估计协方差矩阵

M = np.size(iq_samples, 0) 
#iq_samples的行数,就是接收机通道数,4
self.DOA_theta =  np.linspace(0,360,361)
#生成0~360,一共361个数字的等间隔数列,self.DOA_theta=[0 1 2 3 ... 360]
x = self.DOA_inter_elem_space * np.cos(2*np.pi/M * np.arange(M))
y = self.DOA_inter_elem_space * np.sin(-2*np.pi/M * np.arange(M)) 
#DOA_inter_elem_space里是lamda 天线间距/波长 对于圆阵还要除以sqrt(2),晚点确认?
#np.arange(4)会生成一个数组[0 1 2 3]
#如果暂时忽略掉self.DOA_inter_elem_space
#只考虑np.cos(2*np.pi/M * np.arange(M))和np.sin(-2*np.pi/M * np.arange(M))
#它们会生成单位圆与横轴右侧交点开始逆时针旋转,均匀分布的4个点,x和y分别是这些点的横坐标和纵坐标
#[x,y] = [1,0] [0,-1] [-1,0] [0,1] 实际x和y还要乘以lamda
scanning_vectors = de.gen_scanning_vectors(M, x, y, self.DOA_theta)
#根据阵列的空间布局(包括阵形和lamda)来计算阵列向量

self.DOA_MUSIC_res = de.DOA_MUSIC(R, scanning_vectors, signal_dimension = 1)
#根据协方差矩阵和阵列向量来做MUSIC计算
#signal_dimension是待测信号数量,这里规定了只有1个待测信号

It can be seen that although the steps of hydra_signal_processing.py are similar to the theory when doing the music algorithm, it only calls a few functions in pyargus. The specific operations are in pyargus. I have extracted several corresponding functions. 

def corr_matrix_estimate(X, imp="mem_eff"):
    """
        Estimates the spatial correlation matrix with sample averaging    
    
    Implementation notes:
    --------------------
        Two different implementation exist for this function call. One of them use a for loop to iterate through the
        signal samples while the other use a direct matrix product from numpy. The latter consumes more memory
        (as all the received coherent multichannel samples must be available at the same time)
        but much faster for large arrays. The implementation can be selected using the "imp" function parameter.
        Set imp="mem_eff" to use the memory efficient implementation with a for loop or set to "fast" in order to use
        the faster direct matrix product implementation.
    
        
    Parameters:
    -----------
        :param X : Received multichannel signal matrix from the antenna array.         
        :param imp: Selects the implementation method. Valid values are "mem_eff" and "fast". The default value is "mem_eff".
        :type X: N x M complex numpy array N is the number of samples, M is the number of antenna elements.
        :type imp: string
            
    Return values:
    -------------
    
        :return R : Estimated spatial correlation matrix
        :rtype R: M x M complex numpy array
        
        :return -1 : When unidentified implementation method was specified
    """      
    #由于调用这个函数的时候,iq_sample做了转置(iq_sample.T)
    #因此X变量里现在的列数是4列,对应4个接收机通道,行数是每一个接收机通道的采样点数量
    N = np.size(X, 0) #每个通道采样点数量
    M = np.size(X, 1) #接收机通道数,4个
    R = np.zeros((M, M), dtype=complex)    
    
    # --input check--
    if N < M:
        print("WARNING: Number of antenna elements is greather than the number of time samples")  #如果采样点比接收机通道少,那肯定不行
        print("WARNING: You may flipped the input matrix")
    
    # --calculation--
    if imp == "mem_eff":            
        for n in range(N):
            R += np.outer(X[n, :], np.conjugate(X[n, :]))
    elif imp == "fast":
            X = X.T #现在X矩阵又做了一次转置,那么X里面现在就是原始的iq_samples了
            #根据协方差矩阵的估计公式(参考那篇博客的文章)
            #接收机采样点矩阵乘以自己的共轭转置,再累加,再除以采样点数量就行
            #矩阵右上角的H就代表共轭转置
            #下面这一行做的就是采样点乘以自己的共轭转置并累加了
            R = np.dot(X, X.conj().T)
    else:
        print("ERROR: Unidentified implementation method")
        print("ERROR: No output is generated")
        return -1
    #下面这一行做的是除以采样点数量,做完了就得到协方差矩阵的估计值了
    R = np.divide(R, N)
    return R

def gen_scanning_vectors(M, x, y, thetas):
    """
    Description:
    ------------
        This function prepares scanning vectorors for general antenna array configurations        
        #这里生成的是广义的阵列天线的阵列矩阵,更接近面阵或者任意平面阵

    Parameters:
    -----------
        :param M : Number of antenna elements on the circle
        :param x : x coordinates of the antenna elements on a plane
        :param y : y coordinates of the antenna elements on a plane
        :param thetas : A vector containing the incident angles e.g.: [0deg, 1deg, 2deg, ..., 180 deg]
        
        :type M: int
        :type x: 1D numpy array
        :type y: 1D numpy array
        :type R: float
        :type thetas: 1D numpy array
            
    Return values:
    -------------
    
        :return scanning_vectors : Estimated signal dimension
        :rtype scanning_vectors: 2D numpy array with size: M x P, where P is the number of incident angles
        
    """
    scanning_vectors = np.zeros((M, np.size(thetas)), dtype=complex)
    #np.zeros会生成4行,361列的数组
    for i in range(np.size(thetas)):        
        scanning_vectors[:,i] = np.exp(1j*2*np.pi* (x*np.cos(np.deg2rad(thetas[i])) + y*np.sin(np.deg2rad(thetas[i]))))    
        #我暂时还没找到面阵的阵列矩阵的公式,但是那篇博客里的线阵公式看着也差不多
        #也是e^(-j*(N)*2*pi*d*sin(theta)/lamda),复数表示差不多,但是公式里lamda在分母
        #我这里x,y里包含了lamda,但是它们在分子上
    
    return scanning_vectors

def DOA_MUSIC(R, scanning_vectors, signal_dimension, angle_resolution = 1):
    """                                 
                    MUSIC - Multiple Signal Classification method
        
        
        Description:
         ------------    
           The function implements the MUSIC method for direction estimation
           
           Calculation method : 
                                                    1
                        ADORT(theta) = ---------------------------
                                             H        H 
                                      S(theta) * En En  * S(theta)
           #这个公式和那篇博客文章里的空间谱函数一样,现在S(theta)也就是阵列矩阵已经直接传入了
           #我们只需要算出En就行,它是噪声子空间
         Parameters:
        -----------                
            :param R: spatial correlation matrix            
            :param scanning_vectors : Generated using the array alignment and the incident angles                                                 
            :param signal_dimension:  Number of signal sources    
                       
            :type R: 2D numpy array with size of M x M, where M is the number of antennas in the antenna system                            
            :tpye scanning vectors: 2D numpy array with size: M x P, where P is the number of incident angles                                   
            :type signal_dimension: int
            
       Return values:
       --------------
       
            :return  ADORT : Angular dependent orthogonality. Expresses the orthongonality of the current steering vector to the 
                    noise subspace
            :rtype : numpy array
         
            :return -1, -1: Input spatial correlation matrix is not quadratic
            :return -2, -2: dimension of R not equal with dimension of the antenna array 
            :return -3, -3: Spatial correlation matrix is singular
    """
    # --- Parameters ---  
    
    # --> Input check
    if np.size(R, 0) != np.size(R, 1): #协方差矩阵应该是一个方阵
        print("ERROR: Correlation matrix is not quadratic")
        return -1, -1
    
    if np.size(R, 0) != np.size(scanning_vectors, 0): 
        #阵列矩阵的列数要和协方差矩阵相同,都是通道数4
        print("ERROR: Correlation matrix dimension does not match with the antenna array dimension")
        return -2, -2                    
    
    ADORT = np.zeros(np.size(scanning_vectors, 1),dtype=complex)
    #这是计算出的谱函数,横坐标就是所有角度0~360,就是阵列矩阵的列数
    M = np.size(R, 0) #用协方差矩阵的行数得到通道数
    
    # --- Calculation ---
    # Determine eigenvectors and eigenvalues
    sigmai, vi = lin.eig(R) #求协方差矩阵R的特征值sigmai和特征向量vi
    #sigmai 是一行数组,每一元素是一个特征值
    #vi 也是一行,但是是数组套数组,每一个元素都是一个特征向量
    #这个特征向量本身也是一行数组
    #比如
    #矩阵A:
    # [[ 3 -1]
    # [-1  3]]
    #特征值a:
    [4. 2.]
    #特征向量b:
    [[ 0.70710678  0.70710678] [-0.70710678  0.70710678]]
    
    #array里存储的必须是同一类型的数据,list中的元素可以类型不同
    
    # Sorting  
    #信号子空间是由大的特征值对应的特征向量张成的子空间
    #噪声子空间是由小的特征值对应的特征向量张成的子空间
    #要找出噪声子空间,就要找小的特征值,要排序
  
    #接下来做的就是按照特征值的绝对值大小对特征向量排序
    eig_array = []
    for i in range(M):
        eig_array.append([np.abs(sigmai[i]),vi[:,i]])
    #先把特征值的绝对值和特征向量放到一行里,然后再把它们合并成一个元素放到一个list里
    #一共M(4)个接收机,协方差矩阵是4x4的,所以特征值和特征向量有4组
    #那么这个list有4个元素,每个元素的第一个子元素是特征值的绝对值eig_array[0]
    #第二个子元素是对应的特征向量
    #[[特征值1,[特征向量1[0],特征向量1[1],特征向量1[2],特征向量1[3]]],
    # [特征值2,[特征向量2[0],特征向量2[1],特征向量2[2],特征向量2[3]]],
    # [特征值3,[特征向量3[0],特征向量3[1],特征向量3[2],特征向量3[3]]],
    # [特征值4,[特征向量4[0],特征向量4[1],特征向量4[2],特征向量4[3]]]]
    eig_array = sorted(eig_array, key=lambda eig_array: eig_array[0], reverse=False)
    #sorted对所有可迭代对象进行排序
    #key是用来比较的元素,这个参数来自于可迭代的对象中,就是eig_array[0]
    #reverse=False,代表是从小到大排序的
    
    
    # Generate noise subspace matrix
    noise_dimension = M - signal_dimension    
    #M是特征值总数,减去信号子空间的大小(1个信号发射机,信号子空间就对应1个特征值?)
    #就得到了噪声子空间的大小
    E = np.zeros((M,noise_dimension),dtype=complex)
    #E是4行3列的array,现在里面都是0
    #根据这个尺寸,应该是把3个噪声子空间对应的特征向量竖着存放,
    #每一列对应一个噪声子空间的特征向量
    for i in range(noise_dimension):     
        E[:,i] = eig_array[i][1] 

    #i的范围是0,1,2,一开头是0
    #eig_array[i][1],就是eig_array[0][1],就是eig_array的第一个元素的第二个子元素,
    #经过排序,第一个元素就是特征值最小的那一对特征值+特征向量
    #第二个子元素就是这个特征向量了,所以E[:,0]里存的是特征向量
    #只不过E里是把特征向量竖过来存储的,所以是所有行(4行)和第一列是这个特征值向量
    #等所有3个噪声特征值对应的特征向量存完以后E就是噪声子空间了
    
        
    E = np.matrix(E)    
    
    theta_index=0
    for i in range(np.size(scanning_vectors, 1)):  
        #scanning_vectors里有4行361列,所以i的范围就是0~360           
        S_theta_ = scanning_vectors[:, i] 
        #每行都要(对应4个接收机通道),列对应每个角度,这是阵列矩阵
        S_theta_  = np.matrix(S_theta_).getT() 
        #从4行1列转为1行4列,暂时不知道为什么要转置?可能是为了下面的矩阵乘法?        
        ADORT[theta_index]=  1/np.abs(S_theta_.getH()*(E*E.getH())*S_theta_)
        #这个公式就是空间谱密度的公式
        theta_index += 1 #这个theta_index就跟i一样
         
    return ADORT

 

 

Guess you like

Origin blog.csdn.net/shukebeta008/article/details/104090069