Circular Convolution
Learn about the difference between linear and circular convolution and its importance in the context of the DFT.
We'll cover the following...
When a complex sinusoid is given as an input to an LTI system, the output should be the same complex sinusoid scaled by the magnitude response of the system and phase-shifted by the phase response. This did not happen. Why?
DFT periodicity
Recall that sampling a signal in the time domain gives rise to spectral aliases in the frequency domain. Similarly, when we sample the spectrum in the frequency domain, time-domain aliases arise that actually make the signal periodic. This is why simple convolution, in which the output length is larger than both signals, cannot generate the correct answer.
Consider the process of convolution with a periodic signal as shown in the figure below. As soon as the one period ends, there is a short duration during which the convolving sequence overlaps with both the original signal and its copy in the time domain. These are the points a linear convolution cannot replicate.
For this purpose, circular convolution is devised in a simple manner. It is very much like the linear convolution, except that the signal flipping and time shifting occur in a circular fashion. Therefore, circular convolution is the same as the periodic convolution shown above.
Example
Let’s go back to the same example. Only now, we perform the circular convolution instead of linear convolution and check the results.
import numpy as npimport matplotlib.pyplot as plfigWidth = 20figHeight = 10# Generating the signalfs = 120 # sample rateTs = 1/fs # sample timef = 30T = 0.1A = 1phi = -20*np.pi/180t = np.arange(0, T, Ts)x = A*np.exp(1j*(2*np.pi*f*t + phi))h = np.array([1, -3, 1, 2, -1])n = x.shape[0]y = np.convolve(np.tile(x, 2), h)[n:2*n] # circular convolution through repeating twice and selecting# Plotting the signalfig, axs = pl.subplots(2, figsize=(figWidth, figHeight))nfft = nxf = np.fft.fft(y,nfft) # from 0 to 2pixf = np.fft.fftshift(xf) # from -pi to pik = np.arange(-nfft/2,nfft/2,1)axs[0].vlines(k, ymin=np.abs(xf), ymax=0, color='b', linewidth=2)axs[0].plot(k, np.abs(xf), 'o', color='b', markersize=16, zorder=10, clip_on=False)phase = np.angle(xf)*180/np.piphase[0:9]=0 # removing random phases from numerical imprecisionsphase[10:]=0axs[1].vlines(k, ymin=phase, ymax=0, color='b', linewidth=2)axs[1].plot(k, phase, 'o', color='b', markersize=16, zorder=10, clip_on=False)for ax in axs:ax.axhline(y=0, color='k', linewidth=1)ax.axvline(x=0, color='k', linewidth=1)ax.set_xlim(min(k),max(k)+1)ax.set_xticks(np.arange(min(k),max(k)+1,1))ax.set_xlabel('$k$', fontsize=18)ax.grid()axs[0].set_ylim(0, 71)axs[0].set_yticks(np.arange(0, 71, 10))axs[1].set_ylim(-60, 121)axs[1].set_yticks(np.arange(-60, 121, 30))axs[0].set_ylabel("$|Y[k]|$", fontsize=18)axs[1].set_ylabel("$\measuredangle Y[k]$", fontsize=18)axs[0].tick_params(labelsize=18)axs[1].tick_params(labelsize=18)fig.tight_layout()pl.savefig('output/circular-convolution-output.png', bbox_inches='tight')
It’s clear from the code above that the output has the magnitude and phase values as:
...