Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commitfa7f65d

Browse files
authored
Section on LMS including animation (#79)
* starting to work* try using all samples* it now makes a strong null but main beam is sometimes -3 to -5 dB down* wrote section on LMS
1 parent9eadc10 commitfa7f65d

File tree

7 files changed

+553
-90
lines changed

7 files changed

+553
-90
lines changed

‎_images/doa_lms_animation.gif‎

2.03 MB
Loading

‎content/doa.rst‎

Lines changed: 74 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1140,11 +1140,81 @@ Another experiment worth trying with MUSIC is to see how close two signals can a
11401140
:scale:100%
11411141
:align:center
11421142

1143-
*******************
1144-
ESPRIT
1145-
*******************
1143+
***
1144+
LMS
1145+
***
11461146

1147-
Coming soon!
1147+
The Least Mean Squares (LMS) beamformer is a low-complexity beamformer introduced by Bernard Widrow. It is different from every beamformer we have shown so far in two ways: 1) it requires knowing the SOI, or at least part of it (e.g., a synchronization sequence, pilots, etc) and 2) it is iterative, meaning the weights are honed in on over some number of iterations. It works by minimizing the mean square error between the desired signal (the SOI) and the output of the beamformer (i.e., the weights applied to the received samples). The traditional implementation of LMS is to treat each received sample as the next step in the iterative process, by applying the current weights to the single sample and calculating the error. That error is then used to fine-tune the weights, and the process repeats. The LMS beamformer can be used in both analog and digital beamforming. The LMS algorithm is given by the following equation:
1148+
1149+
..math::
1150+
1151+
w_{n+1} = w_n +\mu\underbrace{\left(y_n - w_{n}^H x_n\right)^*}_{error} x_n
1152+
1153+
where :math:`w_n` is the weight vector at iteration/sample :math:`n`, :math:`\mu` is the step size, :math:`x_n` is the received sample at :math:`n`, :math:`y_n` is the expected value at that iteration (i.e., the known SOI), and :math:`*` is a complex conjugate. Don't let the :math:`w_{n}^H x_n` make the equation seem complicated, that term is simply the act of applying the current weights to the input signal, which is the standard beamforming equation. The step size :math:`\mu` controls how quickly the weights converge to their optimal values. A small value of :math:`\mu` will result in slow convergence (e.g. you may not reach the "best" weights before the known signal is gone), while a large value of :math:`\mu` may cause instability in the algorithm. The LMS algorithm is a powerful tool for adaptive beamforming, but it does have some limitations. It requires a known SOI, which may not always be available in practice, and you have to perform time and frequency synchronization as part of the LMS process so that your blueprint of the SOI is aligned with the received samples.
1154+
1155+
In the example Python code below, we simulate an 8-element array with a SOI that is comprised of a repeating Gold code transmitted as BPSK. Gold codes are used in 5G and GPS, and have excellent cross-correlation properties, making them great for synchronization signals. In the simulation we also include two tone interferers, at 60 and -50 degrees. Note that this simulation does not include any time or frequency shift, if it did then we would have to synchronize to the SOI as part of the LMS process (i.e., joint beamforming-synchronization). In the following animation we sweep the SOI angle of arrival and plot the beam pattern LMS created for us after 10k samples. Note how LMS keeps the gain towards the SOI at exactly 0 dB (unless there is an interferer on top), while placing nulls at the interferers.
1156+
1157+
..image::../_images/doa_lms_animation.gif
1158+
:scale:100%
1159+
:align:center
1160+
1161+
..code-block::python
1162+
1163+
# Scenario
1164+
sample_rate=1e6
1165+
d=0.5# half wavelength spacing
1166+
N=10000# number of samples to simulate
1167+
Nr=8# elements
1168+
theta_soi=20/180* np.pi# convert to radians
1169+
theta2=60/180* np.pi
1170+
theta3=-50/180* np.pi
1171+
t= np.arange(N)/sample_rate# time vector
1172+
s1= np.exp(-2j* np.pi* d* np.arange(Nr)* np.sin(theta_soi)).reshape(-1,1)# 8x1
1173+
s2= np.exp(-2j* np.pi* d* np.arange(Nr)* np.sin(theta2)).reshape(-1,1)
1174+
s3= np.exp(-2j* np.pi* d* np.arange(Nr)* np.sin(theta3)).reshape(-1,1)
1175+
1176+
# SOI is a gold code, repeated, length 127
1177+
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])
1178+
soi_samples_per_symbol=8
1179+
soi= np.repeat(gold_code, soi_samples_per_symbol)
1180+
num_sequence_repeats=int(N/ soi.shape[0])+1# number of times to repeat the sequence for N samples
1181+
soi= np.tile(soi, num_sequence_repeats)[:N]# repeat the sequence to fill simulated time, then trim
1182+
soi= soi.reshape(1,-1)# 1xN
1183+
1184+
# Interference, eg tone jammers, from different directions
1185+
tone2= np.exp(2j*np.pi*0.02e6*t).reshape(1,-1)
1186+
tone3= np.exp(2j*np.pi*0.03e6*t).reshape(1,-1)
1187+
1188+
# Simulate received signal
1189+
r= s1@ soi+ s2@ tone2+ s3@ tone3
1190+
n= np.random.randn(Nr, N)+1j*np.random.randn(Nr, N)
1191+
r= r+0.5*n# 8xN
1192+
1193+
# LMS, not knowing the direction of SOI but knowing the SOI signal itself
1194+
mu=1e-4# LMS step size
1195+
gamma=0.995# knowledge decay rate
1196+
w_lms= np.zeros((Nr,1),dtype=np.complex128)# start with all zeros
1197+
1198+
# Loop through received samples
1199+
error_log= []
1200+
for iinrange(N):
1201+
r_sample= r[:, i].reshape(-1,1)# 8x1
1202+
soi_sample= soi[0, i]# scalar
1203+
y= w_lms.conj().T@ r_sample# apply the weights
1204+
y= y.squeeze()# make it a scalar
1205+
error= soi_sample- y
1206+
error_log.append(np.abs(error)**2)
1207+
w_lms= w_lms* gamma+ mu* np.conj(error)* r_sample# weights are still 8x1
1208+
w_lms/= np.linalg.norm(w_lms)# normalize weights
1209+
1210+
plt.plot(error_log)
1211+
plt.xlabel('Iteration')
1212+
plt.ylabel('Mean Square Error')
1213+
plt.show()
1214+
1215+
# Plot the beam pattern as shown previously
1216+
1217+
Try changing:code:`theta_soi`, the amount of noise (i.e., the:code:`0.5*n`), and the step size:code:`mu` to see how the LMS algorithm performs.
11481218

11491219
*******************
11501220
Circular Arrays
Lines changed: 86 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -1,100 +1,100 @@
11
importnumpyasnp
22
importmatplotlib.pyplotasplt
3-
fromscipyimportsignal
4-
frommatplotlib.animationimportFuncAnimation
53

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
105

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
3313

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)
3721

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
4122

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
4241

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)
5145

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
5950

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
6957

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
8069

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+
'''
8477

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()
100100

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp