Introduction to Beamforming: Part 1

How to estimate the Direction of Arrival with an Array

Isaac Berrios
9 min readJul 8, 2024

This post presents an intuitive approach to Beamforming and then demonstrates how to implement the Delay-and-Sum Beamformer from scratch in Python. The code is located on GitHub.

This is the first post in a series on Beamformers:

Photo by Alex Gruber on Unsplash

The contents of this post are below:

Background

Beamforming is a signal processing technique that aims to estimate Direction of signals impinging on a sensor array.

The incoming direction of a signal is referred to as Direction or Angle of Arrival

This is accomplished by combining elements such that some add constructively and others add destructively. Now we will introduce the Uniform Linear Array, if you are unfamiliar with arrays, please see this post.

Array Model

Consider the Uniform Linear Array (ULA) below with M elements:

Figure 1. Uniform Linear Array with signal wavefront at r₁. Source.

A signal wavefront, denoted by the diagonal dotted line, is impinging on element r₁ and is yet to impinge on the remaining elements. If we know the element spacing, we can determine the distance that the wavefront must travel to each subsequent element. The remaining elements will receive the signal at an increasingly delayed time relative to their physical position in the array. In this case, the elements further to the right from r₁ will have a larger time delay, with the largest time delay occurring at element rₘ. If we change the direction of the signal, then the time delay experienced by each element would also change accordingly.

Understanding how the incoming signal direction impacts the time delay at each element is the key to developing Beamforming methods. As stated above, we can find ways to make the received signal at each element add up constructively in order to achieve a desired result. We explore this intuition in the next section.

Intuitive Beamforming

Below we have three different scenarios for a 3-element microphone array, each scenario has a signal impinging from a different direction (-45°, 0°, 45°).

Figure 2. Example of signals impinging from different directions on a 3-element microphone array. Source.

The signals from each element are summed together, notice how the output signals for the ±45° cases have very little correlation and the signal for the 0° case has a high correlation (constructive interference). We could say that the array is tuned to the 0° signal, in other words the array response is the greatest for 0° signals.

Now let’s consider the possibilities for the ±45° signals. We can apply a combination of time delay for each element such that the correlation is maximized. We could then estimate the Angle of Arrival based on this combination of time delays. This is the basis for the Delay-and-Sum Beamformer, which is pictured below.

When we process the signal with the time delays, we are actually forming a beam in particular direction, hence the term beamforming.

Delay-and-Sum Beamformer

On the top of figure 3, we have a scenario where a desired signal is received and the appropriate time delays (t₁, t₂) are applied such that the signals constructively interfere and the array produces a high response. On the bottom, we have an undesired signal and no time delays are applied, there is no constructive interference and the array does not have a high response to this signal.

Figure 3. Delay-and-Sum Beamformer, top: target signal, bottom: noise signal. Source.

The Delay-and-Sum Beamformer applies a time delay to the incoming signal from each element and sums the output together. If we get the time delays correct, we will have a single high output signal. We can then use the time delays that produced this signal to determine the angle of it’s arrival.

How do we get the time delays? We can steer the array across multiple angles and choose the angle that produces the largest response. We process the incoming signal with the steering vector to produce an array response, the angle that produces the largest response is the most likely Angle of Arrival. Let’s look at a Python example to demonstrate this.

See this post if you’re unfamiliar with the Steering Vector or Array Response

Implementing the Delay-and-Sum Beamformer

Most of this example is modified from this tutorial. First, let’s generate a signal for our array to receive, the code is located on GitHub.

sample_rate = 1e6
N = 10000 # number of samples to simulate

# Create a tone to act as the transmitter signal
t = np.arange(N)/sample_rate # time vector
f_tone = 0.02e6 # signal frequency

# create signal with AWG Noise
awgn = np.random.normal(0, 0.1, N) + 1j*np.random.normal(0, 0.1, N)
s_tx = np.exp(2j * np.pi * f_tone * t) + awgn

Note: tone is a term that refers to the frequency of a signal, it doesn’t need to be an audio tone.

Figure 4. Input signal to the array. Source: Author.

Now let’s construct the array steering vector, here we assume that we know the direction of arrival. We will use a 3-element Uniform Linear Array with half-wavelength spacing.

d = 0.5 # half wavelength spacing
Nr = 3

theta_degrees = 25 # direction of arrival
theta = theta_degrees / 180 * np.pi # convert to radians

# compute steering vector of the signal
steering_vector = lambda theta, Nr, d =d : np.exp(-2j * np.pi * d * np.arange(Nr) * np.sin(theta))
v_signal = steering_vector(theta, Nr)[:, None]

To simulate the signal impinging on the array, we will matrix multiply the signal with the steering vector and then add noise. In reality, each element will always be noisy and this is what we are simulating when we add additional noise here which is denoted by σₑ.

awgn = np.random.normal(0, 0.1, (N, Nr)) + 1j*np.random.normal(0, 0.1, (N, Nr))
X = (tx[:, None] @ v_signal.T) + awgn
X.shape # (10000, 3)

The received signal is an (N x 3) matrix X, a plot of these signals at each element is shown below.

Figure 5. Received signals at each element. Source: Author.

Now we have the received signals at each array element, notice how they are slightly delayed (out of phase) with each other. We could also go back and change the Angle of Arrival to 0°, then we would see that all the incoming signals would be aligned (in phase). Now let’s implement the Delay-and-Sum Beamformer without knowledge of the arrival angle.

Angles

First let’s establish the angles we would like to consider, this will just be a vector from -90° to +90° with some predefined spacing (angular resolution); we can decrease this spacing in order to increase our angular resolution.

# Angles with 0.1 degree resolution
thetas = np.arange(-90, 90 + 0.1, 0.1)

Process the Received Signal with the Steering Vector

We will process the received signal at each of these angles in order to compute the array response. This processing is performed by matrix multiplying the Hermitian (Conjugate Transpose) of the steering vector at angle v(θ) with the received signal matrix X, which produces an N dimensional vector y.

Always keep actual dimensions in mind, in the implementation the steering vector doesn’t need the transpose, only the conjugate. Also, we need to be sure to normalize by the number of elements to get the correct signal amplitude.

w = steering_vector(theta, Nr) # Steering Vector
y = (X @ w.conj()) / Nr # process signal at current angle

This matrix multiplication is where the time delays and signal summing occur, the steering vector actually applies a time delay to the signal at each element. This is because their is a direct relation between time delay and Angle of Arrival. Let’s look at the steering vector (at element m) for a ULA with half-wavelength spacing which was derived here.

The key is msin(θ), which corresponds to the time delay based on physical location m of the element. This time delay is proportional to angle θ.

Compute Array Response

The array response is determined by the variance of the output vector y, if we have a small variance, then we know that the received signals have a large degree of constructive interference and the output signal will be the largest. If we have a large variance, then we know that our Steering Vector (time delays) are way off.

response = 10*np.log10(np.var(y)) # compute response power in dB

Now let’s put this all together, we will iterate through each angle and process the signal with the steering vector to obtain an output signal y. We then compute the power of the array response by taking the log of the variance of the output signal. Then we find the angle that produced the least variance, this is our Angle of Arrival (AoA) estimate.

# collection angles to process
thetas = np.arange(-90, 90 + 0.1, 0.1)

outputs = []
responses = []
for theta in thetas:
theta *= np.pi/180
w = steering_vector(theta, Nr) # Steering Vector

y = (X @ w.conj()) / Nr # process signal at current angle
response = 10*np.log10(np.var(y)) # compute response power in dB

outputs.append(y)
responses.append(response)

responses -= np.max(responses) # normalize (optional)

# obtain angle that gave us the max value
angle_idx = np.argmax(responses)

aoa = thetas[angle_idx]
s_hat = outputs[angle_idx]
Figure 6. Output Array Response to the incoming signal. Source: Author.

The image above shows the output array response for each angle that we processed. The highest peak is around 25 which is the true angle of arrival, even with the added noise the Delay-and-Sum Beamformer is able to accurately estimate the AoA. Now let’s take a look at the actual signal, below is a plot of the original noisy signal and the processed signal. (Processed using the AoA that corresponds to the largest Array Response).

Figure 7. Estimate of received signal. Source: Author.

Now let’s see how the processed signal looks at the other angles, the blue is the originally transmitted signal, and the orange is the processed signal at various angles.

Figure 8. Received signal as processed at each angle. Source: Author.

We can see that the signal level is much lower for angles that are further way and higher for angles close to the true Direction of Arrival; in fact the signal level is proportional to the array response in figure 6.

Conclusion

We have learned about the basics of Beamforming in Array Processing. The Delay-and-Sum Beamformer is also known as the Conventional Beamformer. It’s performance has proven well for simulated data, but in reality we will need something a bit more robust. In the remaining portions of this series we will explore practical Beamformers that have proven to work well with real data.

References

--

--

Isaac Berrios

Electrical Engineer interested in Sensor Fusion, Computer Vision, and Autonomy