|
1 | 1 | importnumpyasnp |
2 | 2 | importmatplotlib.pyplotasplt |
3 | | -fromscipyimportsignal |
4 | | -frommatplotlib.animationimportFuncAnimation |
5 | 3 |
|
6 | | -sample_rate=1e6 |
7 | | -d=0.5# half wavelength spacing |
8 | | -N=10000# number of samples to simulate |
9 | | -t=np.arange(N)/sample_rate# time vector |
| 4 | +# OBS plus lots of ezgif.com was used to make the final animated gif |
10 | 5 |
|
11 | | -# more complex scenario taken from DOA code |
12 | | -Nr=8# 8 elements |
13 | | -theta1=30/180*np.pi# convert to radians |
14 | | -theta2=60/180*np.pi |
15 | | -theta3=-60/180*np.pi |
16 | | -s1=np.exp(-2j*np.pi*d*np.arange(Nr)*np.sin(theta1)).reshape(-1,1)# 8x1 |
17 | | -s2=np.exp(-2j*np.pi*d*np.arange(Nr)*np.sin(theta2)).reshape(-1,1) |
18 | | -s3=np.exp(-2j*np.pi*d*np.arange(Nr)*np.sin(theta3)).reshape(-1,1) |
19 | | -# we'll use 3 different frequencies |
20 | | -ifFalse: |
21 | | -soi=np.exp(2j*np.pi*0.01e6*t).reshape(1,-1)# 1xN |
22 | | -else: |
23 | | -gold_code=np.array([-1,-1,1,-1,-1,1,1,1,1,1,1,1,1,-1,1,1,-1,1,-1,1,-1,1,-1,-1,1,1,1,1,1,-1,1],dtype=complex) |
24 | | -soi_samples_per_symbol=8 |
25 | | -soi=np.repeat(gold_code,soi_samples_per_symbol)# Gold code is 31 bits, so 31*8 = 248 samples |
26 | | -num_sequence_repeats=int(N/soi.shape[0])+1# number of times to repeat the sequence |
27 | | -soi=np.tile(soi,num_sequence_repeats)# repeat the sequence |
28 | | -soi=soi[:N]# trim to N samples |
29 | | -soi=soi.reshape(1,-1)# 1xN |
30 | | - |
31 | | -tone2=np.exp(2j*np.pi*0.02e6*t).reshape(1,-1) |
32 | | -tone3=np.exp(2j*np.pi*0.03e6*t).reshape(1,-1) |
| 6 | +fig,ax=plt.subplots(subplot_kw={'projection':'polar'}) |
| 7 | +theta_sois_deg=np.linspace(-90,90,200) |
| 8 | +fortheta_soi_degintheta_sois_deg: |
| 9 | +sample_rate=1e6 |
| 10 | +d=0.5# half wavelength spacing |
| 11 | +N=10000# number of samples to simulate |
| 12 | +t=np.arange(N)/sample_rate# time vector |
33 | 13 |
|
34 | | -print(np.var(soi)) |
35 | | -print(np.var(tone2)) |
36 | | -print(np.var(tone3)) |
| 14 | +Nr=8# elements |
| 15 | +theta_soi=theta_soi_deg/180*np.pi# convert to radians |
| 16 | +theta2=60/180*np.pi |
| 17 | +theta3=-50/180*np.pi |
| 18 | +s1=np.exp(-2j*np.pi*d*np.arange(Nr)*np.sin(theta_soi)).reshape(-1,1)# 8x1 |
| 19 | +s2=np.exp(-2j*np.pi*d*np.arange(Nr)*np.sin(theta2)).reshape(-1,1) |
| 20 | +s3=np.exp(-2j*np.pi*d*np.arange(Nr)*np.sin(theta3)).reshape(-1,1) |
37 | 21 |
|
38 | | -r=s1 @soi+s2 @tone2+0.1*s3 @tone3 |
39 | | -n=np.random.randn(Nr,N)+1j*np.random.randn(Nr,N) |
40 | | -r=r+0.05*n# 8xN |
41 | 22 |
|
| 23 | +ifFalse: |
| 24 | +# SOI is a tone |
| 25 | +soi=np.exp(2j*np.pi*0.01e6*t).reshape(1,-1)# 1xN |
| 26 | +else: |
| 27 | +# SOI is a gold code, repeated |
| 28 | + |
| 29 | +# Length 31 |
| 30 | +#gold_code = np.array([-1, -1, 1, -1, -1, 1, 1, 1, 1, 1, 1, 1, 1, -1, 1, 1, -1, 1, -1, 1, -1, 1, -1, -1, 1, 1, 1, 1, 1, -1, 1], dtype=complex) |
| 31 | + |
| 32 | +# Length 127 |
| 33 | +gold_code=np.array([-1,1,1,-1,1,1,1,1,-1,-1,-1,1,1,-1,-1,-1,-1,-1,1,1,1,-1,-1,1,1,1,-1,1,1,1,1,1,1,-1,-1,-1,1,1,1,-1,-1,1,1,-1,-1,1,-1,1,-1,-1,1,-1,-1,-1,-1,-1,-1,1,1,-1,1,-1,1,-1,1,1,-1,-1,-1,-1,1,1,1,-1,1,-1,1,1,1,1,1,-1,-1,-1,-1,1,1,1,-1,1,-1,-1,-1,1,1,1,1,-1,1,1,1,-1,1,-1,-1,-1,-1,1,-1,1,1,-1,-1,-1,-1,1,-1,1,1,-1,-1,-1,-1,-1,-1,1,1]) |
| 34 | + |
| 35 | +soi_samples_per_symbol=8 |
| 36 | +soi=np.repeat(gold_code,soi_samples_per_symbol)# Gold code is 31 bits, so 31*8 = 248 samples |
| 37 | +num_sequence_repeats=int(N/soi.shape[0])+1# number of times to repeat the sequence |
| 38 | +soi=np.tile(soi,num_sequence_repeats)# repeat the sequence to fill simulated time |
| 39 | +soi=soi[:N]# trim to N samples |
| 40 | +soi=soi.reshape(1,-1)# 1xN |
42 | 41 |
|
43 | | -# theta is the direction of interest, in radians, and r is our received signal |
44 | | -defw_mvdr(theta,r): |
45 | | -s=np.exp(-2j*np.pi*d*np.arange(r.shape[0])*np.sin(theta))# steering vector in the desired direction theta |
46 | | -s=s.reshape(-1,1)# make into a column vector (size 3x1) |
47 | | -R=np.cov(r)# Calc covariance matrix. gives a Nr x Nr covariance matrix of the samples |
48 | | -Rinv=np.linalg.pinv(R)# 3x3. pseudo-inverse tends to work better than a true inverse |
49 | | -w= (Rinv @s)/(s.conj().T @Rinv @s)# MVDR/Capon equation! numerator is 3x3 * 3x1, denominator is 1x3 * 3x3 * 3x1, resulting in a 3x1 weights vector |
50 | | -returnw |
| 42 | +# Interference, eg tone jammers, from different directions from the SOI |
| 43 | +tone2=np.exp(2j*np.pi*0.02e6*t).reshape(1,-1) |
| 44 | +tone3=np.exp(2j*np.pi*0.03e6*t).reshape(1,-1) |
51 | 45 |
|
52 | | -defpower_mvdr(theta,r): |
53 | | -s=np.exp(-2j*np.pi*d*np.arange(r.shape[0])*np.sin(theta))# steering vector in the desired direction theta_i |
54 | | -s=s.reshape(-1,1)# make into a column vector (size 3x1) |
55 | | -#R = (r @ r.conj().T)/r.shape[1] # Calc covariance matrix. gives a Nr x Nr covariance matrix of the samples |
56 | | -R=np.cov(r) |
57 | | -Rinv=np.linalg.pinv(R)# 3x3. pseudo-inverse tends to work better than a true inverse |
58 | | -return1/(s.conj().T @Rinv @s).squeeze() |
| 46 | +# Simulate received signal |
| 47 | +r=s1 @soi+s2 @tone2+s3 @tone3 |
| 48 | +n=np.random.randn(Nr,N)+1j*np.random.randn(Nr,N) |
| 49 | +r=r+0.5*n# 8xN |
59 | 50 |
|
60 | | -theta_scan=np.linspace(-1*np.pi,np.pi,1000)# 1000 different thetas between -180 and +180 degrees |
61 | | -results= [] |
62 | | -fortheta_iintheta_scan: |
63 | | -#w = w_mvdr(theta_i, r) # 3x1 |
64 | | -#r_weighted = w.conj().T @ r # apply weights |
65 | | -#power_dB = 10*np.log10(np.var(r_weighted)) # power in signal, in dB so its easier to see small and large lobes at the same time |
66 | | -#results.append(power_dB) |
67 | | -results.append(10*np.log10(power_mvdr(theta_i,r)))# compare to using equation for MVDR power, should match, SHOW MATH OF WHY THIS HAPPENS! |
68 | | -results-=np.max(results)# normalize |
| 51 | +# LMS, not knowing the direction of SOI but knowing the soi signal itself |
| 52 | +# TODO add in a random time delay to start of soi |
| 53 | +mu=1e-4# LMS step size |
| 54 | +gamma=0.995# knowledge decay rate |
| 55 | +#w_lms = np.random.randn(Nr, 1) + 1j*np.random.randn(Nr, 1) # random weights |
| 56 | +w_lms=np.zeros((Nr,1),dtype=np.complex128)# zeros |
69 | 57 |
|
70 | | -ifFalse: |
71 | | -fig,ax=plt.subplots(subplot_kw={'projection':'polar'}) |
72 | | -ax.plot(theta_scan,results)# MAKE SURE TO USE RADIAN FOR POLAR |
73 | | -ax.set_theta_zero_location('N')# make 0 degrees point up |
74 | | -ax.set_theta_direction(-1)# increase clockwise |
75 | | -ax.set_rlabel_position(30)# Move grid labels away from other labels |
76 | | -ax.set_thetamin(-90) |
77 | | -ax.set_thetamax(90) |
78 | | -plt.show() |
79 | | -#fig.savefig('../_images/doa_complex_scenario.svg', bbox_inches='tight') |
| 58 | +# Loop through received samples |
| 59 | +error_log= [] |
| 60 | +foriinrange(N): |
| 61 | +r_sample=r[:,i].reshape(-1,1)# 8x1 |
| 62 | +soi_sample=soi[0,i]# scalar |
| 63 | +y=w_lms.conj().T @r_sample# apply the weights (output is a scalar) |
| 64 | +y=y.squeeze()# make it a scalar |
| 65 | +error=soi_sample-y |
| 66 | +error_log.append(np.abs(error)**2) |
| 67 | +w_lms=w_lms*gamma+mu*np.conj(error)*r_sample# weights are still 8x1 |
| 68 | +w_lms/=np.linalg.norm(w_lms)# normalize |
80 | 69 |
|
81 | | -w_soi=w_mvdr(theta1,r) |
82 | | -w_soi=w_soi.reshape(-1)# make into a row vector |
83 | | -print(w_soi) |
| 70 | +''' |
| 71 | + plt.figure("Error Log") |
| 72 | + plt.plot(error_log) |
| 73 | + plt.xlabel('Iteration') |
| 74 | + plt.ylabel('Mean Square Error') |
| 75 | + plt.grid() |
| 76 | + ''' |
84 | 77 |
|
85 | | -N_fft=1024 |
86 | | -w_soi=np.conj(w_soi) |
87 | | -w_padded=np.concatenate((w_soi,np.zeros(N_fft-Nr)))# zero pad to N_fft elements to get more resolution in the FFT |
88 | | -w_fft_dB=10*np.log10(np.abs(np.fft.fftshift(np.fft.fft(w_padded)))**2)# magnitude of fft in dB |
89 | | -w_fft_dB-=np.max(w_fft_dB)# normalize to 0 dB at peak |
90 | | -theta_bins=np.arcsin(np.linspace(-1,1,N_fft))# in radians |
91 | | -fig,ax=plt.subplots(subplot_kw={'projection':'polar'}) |
92 | | -ax.plot(theta_bins,w_fft_dB)# MAKE SURE TO USE RADIAN FOR POLAR |
93 | | -ax.set_theta_zero_location('N')# make 0 degrees point up |
94 | | -ax.set_theta_direction(-1)# increase clockwise |
95 | | -ax.set_rlabel_position(55)# Move grid labels away from other labels |
96 | | -ax.set_thetamin(-90)# only show top half |
97 | | -ax.set_thetamax(90) |
98 | | -ax.set_ylim([-30,1])# because there's no noise, only go down 30 dB |
99 | | -plt.show() |
| 78 | +# Visualize the weights |
| 79 | +N_fft=1024 |
| 80 | +w_lms=w_lms.reshape(-1)# make into a row vector |
| 81 | +w_lms=w_lms.conj() |
| 82 | +w_padded=np.concatenate((w_lms,np.zeros(N_fft-Nr)))# zero pad to N_fft elements to get more resolution in the FFT |
| 83 | +w_fft_dB=10*np.log10(np.abs(np.fft.fftshift(np.fft.fft(w_padded)))**2)# magnitude of fft in dB |
| 84 | +#w_fft_dB -= np.max(w_fft_dB) # normalize to 0 dB at peak |
| 85 | +theta_bins=np.arcsin(np.linspace(-1,1,N_fft))# in radians |
| 86 | + |
| 87 | +ax.plot(theta_bins,w_fft_dB) |
| 88 | +ax.plot([theta_soi,theta_soi], [-30,10],'g--') |
| 89 | +ax.plot([theta2,theta2], [-30,10],'r--') |
| 90 | +ax.plot([theta3,theta3], [-30,10],'r--') |
| 91 | +ax.set_theta_zero_location('N')# type: ignore # make 0 degrees point up |
| 92 | +ax.set_theta_direction(-1)# type: ignore # increase clockwise |
| 93 | +ax.set_rlabel_position(55)# type: ignore # Move grid labels away from other labels |
| 94 | +ax.set_thetamin(-90)# type: ignore # only show top half |
| 95 | +ax.set_thetamax(90)# type: ignore |
| 96 | +ax.set_ylim((-30,10))# because there's no noise, only go down 30 dB |
| 97 | +plt.draw() |
| 98 | +plt.pause(0.001)# pause to update the plot |
| 99 | +plt.cla() |
100 | 100 |
|