Python Signal Processing Lab: Applications
1. Introduction & Objective
This lab demonstrates how to calculate Fourier Series coefficients numerically using Python
instead of manual integration. We'll use SciPy's integration capabilities to evaluate the
integrals and visualize the spectrum.
2. Numerical Integration with SciPy
import numpy as np
from [Link] import quad
import [Link] as plt
# Define a simple function to integrate
def fcos(t):
return [Link]([Link]*t)
# Perform numerical integration
result, error = quad(fcos, 0, 5)
print(f"Integration result: {result:.6f}, Error estimate: {error:.2e}")
# More complex example with absolute value
def exp_abs(t):
return [Link](-[Link](t))
result, error = quad(exp_abs, -2, 2)
print(f"Integration of e^(-|t|) from -2 to 2: {result:.6f}")
3. Fourier Series Coefficient Calculation
def fourier_coefficient(x, k, T0, num_points=1000):
"""
Calculate the k-th Fourier series coefficient for periodic function x(t)
"""
omega0 = 2*[Link]/T0
integrand = lambda t: x(t) * [Link](-1j*k*omega0*t)
ak, _ = quad(integrand, -T0/2, T0/2)
return ak/T0
# Define a square wave function
def square_wave(t, T0=2*[Link]):
t_mod = t % T0
return 1 if t_mod < T0/2 else -1
# Calculate coefficients for k=-10 to 10
N = 10
T0 = 2*[Link]
coefficients = [fourier_coefficient(square_wave, k, T0) for k in range(-N, N+1)]
k_values = [Link](-N, N+1)
# Plot the magnitude spectrum
[Link](figsize=(10, 5))
[Link](k_values, [Link](coefficients))
[Link]('Fourier Series Coefficients for Square Wave')
[Link]('k')
[Link]('|a_k|')
[Link](True)
[Link]()
3.1. Interactive Square Wave Approximation
import numpy as np
import [Link] as plt
import [Link] as animation
from [Link] import HTML
# Fourier Series Square Wave Approximation
def square_wave_fourier(t, N):
T0 = 2 * [Link]
result = np.zeros_like(t)
for k in range(1, N+1, 2): # Only odd harmonics
result += (4 / ([Link] * k)) * [Link](k * (2 * [Link] / T0) * t)
return result
# Setting up the figure for animation
fig, ax = [Link](figsize=(8, 5))
ax.set_xlim(-[Link], [Link])
ax.set_ylim(-1.5, 1.5)
ax.set_xlabel("t")
ax.set_ylabel("Signal")
ax.set_title("Fourier Series Approximation of Square Wave")
t_values = [Link](-[Link], [Link], 500)
line, = [Link]([], [], lw=2, label="Fourier Approximation")
[Link]()
# Update function for animation
def update(frame):
N = frame * 2 + 1 # Increase number of terms incrementally
y_values = square_wave_fourier(t_values, N)
line.set_data(t_values, y_values)
return line,
# Create animation
ani = [Link](fig, update, frames=100, interval=300, repeat=True)
# Display animation
HTML(ani.to_jshtml())
4. Interactive Fourier Series Visualization
import numpy as np
import [Link] as plt
import ipywidgets as widgets
from [Link] import display
T0 = 2 * [Link]
def square_wave(t, T0=2*[Link]):
return [Link]((t % T0) < T0/2, 1, -1) # Vectorized for efficiency
def fourier_coefficient(k):
""" Analytical Fourier coefficients for a square wave """
if k == 0:
return 0 # No DC component
elif k % 2 == 1: # Only odd harmonics exist
return 4 / ([Link] * k)
else:
return 0 # Even harmonics are zero
def plot_fourier_approximation(N_terms=5):
t = [Link](-2*[Link], 2*[Link], 1000)
approximation = np.zeros_like(t)
for k in range(1, 2*N_terms, 2): # Only odd harmonics
ak = fourier_coefficient(k)
approximation += ak * [Link](k * (2 * [Link] / T0) * t)
[Link](figsize=(10, 5))
[Link](t, approximation, label='Fourier approximation')
[Link](t, square_wave(t), '--', label='Original signal')
[Link](f'Fourier Series Approximation with {2*N_terms-1} terms')
[Link]()
[Link](True)
[Link]()
interactive_plot = [Link](plot_fourier_approximation,
N_terms=[Link](min=1, max=20, value=5))
display(interactive_plot)
Design with Fourier Series – Distortion
1. Filtering Periodic Signals
import numpy as np
import [Link] as plt
# Define the frequency response of the filter
def H(jw):
return (1j * jw) / ((4 - jw**2) + 1j * jw / 3)
# Define Fourier series parameters
T0 = 2 * [Link]
omega0 = 2 * [Link] / T0
k_values = [Link](-10, 11) # Range of Fourier coefficients
# Generate Fourier coefficients (example function)
coefficients = [4 / ([Link] * k) if k % 2 == 1 else 0 for k in k_values] # Square wave Fourier
series
# Calculate output coefficients by filtering input coefficients
output_coeffs = [coeff * H(1j * k * omega0) for k, coeff in zip(k_values, coefficients)]
# Plot input and output spectra
[Link](figsize=(12, 5))
# Input Spectrum
[Link](1, 2, 1)
[Link](k_values * omega0, [Link](coefficients), markerfmt="ro", basefmt=" ")
[Link]("Input Spectrum")
[Link]("Frequency (rad/s)")
[Link]("|a_k|")
[Link](True)
# Output Spectrum After Filtering
[Link](1, 2, 2)
[Link](k_values * omega0, [Link](output_coeffs), markerfmt="bo", basefmt=" ")
[Link]("Output Spectrum After Filtering")
[Link]("Frequency (rad/s)")
[Link]("|b_k|")
[Link](True)
plt.tight_layout()
[Link]()
2. Time Domain Reconstruction
import numpy as np
import [Link] as plt
def reconstruct_signal(coeffs, t, T0, N):
omega0 = 2*[Link]/T0
signal = np.zeros_like(t, dtype=complex) # Ensure it's initialized properly
for k, ak in zip(range(-N, N+1), coeffs): # Pass N explicitly
signal += ak * [Link](1j*k*omega0*t)
return [Link](signal)
# Define parameters
T0 = 2 * [Link]
N = len(coefficients) // 2 # Ensure correct range for coefficients
t = [Link](-T0, 2*T0, 1000)
# Reconstruct input and output signals
input_signal = reconstruct_signal(coefficients, t, T0, N)
output_signal = reconstruct_signal(output_coeffs, t, T0, N)
# Plot reconstructed signals
[Link](figsize=(10, 5))
[Link](t, input_signal, label='Input Square Wave', color='blue')
[Link](t, output_signal, label='Filtered Output', color='red')
[Link]('Time Domain Signals Before and After Filtering')
[Link]()
[Link](True)
[Link]()
2. Fast Forier Transform (FFT) Visuilization
import numpy as np
import [Link] as plt
import ipywidgets as widgets
from [Link] import display
# Ensure correct backend for plotting
%matplotlib inline
# Sampling parameters
Fs = 1000 # Sampling frequency (Hz)
T = 1.0 # Duration (seconds)
t = [Link](0, T, int(Fs*T), endpoint=False) # Time array
def generate_signal(freq1=50, freq2=150, freq3=300):
""" Generate a signal composed of multiple sine waves """
return [Link](2*[Link]*freq1*t) + [Link](2*[Link]*freq2*t) + [Link](2*[Link]*freq3*t)
def plot_fft(freq1=50, freq2=150, freq3=300):
""" Compute and plot FFT of the generated signal """
signal = generate_signal(freq1, freq2, freq3)
N = len(signal) # Number of samples
# Compute FFT
fft_values = [Link](signal)
freqs = [Link](N, 1/Fs)
# Only take positive frequencies
mask = freqs >= 0 # Fixed issue: previously filtering only greater than zero
# Plot time-domain signal
[Link](figsize=(12, 5))
[Link](1, 2, 1)
[Link](t, signal, label="Time-domain Signal", color="royalblue")
[Link]("Time (s)")
[Link]("Amplitude")
[Link]("Original Signal")
[Link]()
[Link](True)
# Plot FFT magnitude spectrum with enhanced visuals
[Link](1, 2, 2)
[Link](freqs[mask], [Link](fft_values[mask]), markerfmt="ro")
[Link]("Frequency (Hz)")
[Link]("Magnitude")
[Link]("Frequency-domain (FFT) Spectrum")
[Link](True)
plt.tight_layout()
[Link]()
# Use interactive sliders with improved functionality for Colab
interactive_fft = [Link](plot_fft,
freq1=[Link](min=10, max=500, value=50, step=10),
freq2=[Link](min=10, max=500, value=150, step=10),
freq3=[Link](min=10, max=500, value=300, step=10))
display(interactive_fft)
Convolution Visualization in Python
1. Discrete-Time Convolution
import numpy as np
import [Link] as plt
def discrete_convolution(x, h):
N = len(x)
M = len(h)
y = [Link](N + M - 1)
for n in range(len(y)):
for k in range(max(0, n-M+1), min(n+1, N)):
y[n] += x[k] * h[n-k]
return y
# Create a pulse signal
x = [Link]([[Link](5), [Link](10), [Link](5)])
# Create a moving average filter
h = [Link](5)/5
y = discrete_convolution(x, h)
[Link](figsize=(12, 4))
[Link](1, 3, 1)
[Link](x, markerfmt="ro") # Removed use_line_collection=True
[Link]("Input Signal x[n]")
[Link](1, 3, 2)
[Link](h, markerfmt="bo") # Removed use_line_collection=True
[Link]("Impulse Response h[n]")
[Link](1, 3, 3)
[Link](y, markerfmt="go") # Removed use_line_collection=True
[Link]("Output y[n] = x[n] * h[n]")
plt.tight_layout()
[Link]()
2. Continuous-Time Convolution
from [Link] import convolve
# Define time axis
t = [Link](0, 10, 1000)
# Create exponential signals
x = [Link](-0.25*t) * (t <= 6)
h = [Link](-t) * ((t >= -1) & (t <= 5)) # Shifted to include negative time
# Perform convolution
y = convolve(x, h, mode='same') * (t[1]-t[0]) # Multiply by dt for proper scaling
[Link](figsize=(12, 4))
[Link](1, 3, 1)
[Link](t, x)
[Link]('Input Signal x(t)')
[Link](1, 3, 2)
[Link](t, h)
[Link]('Impulse Response h(t)')
[Link](1, 3, 3)
[Link](t, y)
[Link]('Output y(t) = x(t)*h(t)')
plt.tight_layout()
[Link]()
3. Interactive Convolution Visualization
from [Link] import FuncAnimation
from [Link] import HTML
def animate_convolution(x, h, t):
fig, (ax1, ax2, ax3) = [Link](3, 1, figsize=(10, 8))
# Plot original signals
[Link](t, x, label='x(τ)')
ax1.set_title('Input Signal x(τ)')
[Link]()
[Link](t, h, label='h(τ)')
ax2.set_title('Impulse Response h(τ)')
[Link]()
# Initialize output plot
line, = [Link]([], [], 'r-')
ax3.set_xlim(t[0], t[-1])
ax3.set_ylim(0, 1.1*[Link](convolve(x, h, mode='same')))
ax3.set_title('Convolution Result y(t) = x(t)*h(t)')
# Calculate full convolution result
full_conv = convolve(x, h, mode='same') * (t[1]-t[0])
def update(frame):
# Flip and shift h
shifted_h = [Link](h[::-1], frame)
# Clear and redraw
[Link]()
[Link](t, x, label='x(τ)')
[Link](t, shifted_h, 'g--', label=f'h({frame}-τ)')
ax1.set_title(f'Time t = {t[frame]:.2f}')
[Link]()
[Link]()
[Link](t, shifted_h, 'g--', label=f'h({frame}-τ)')
ax2.set_title('Flipped and Shifted Impulse Response')
[Link]()
# Update convolution result up to current frame
line.set_data(t[:frame], full_conv[:frame])
return line,
ani = FuncAnimation(fig, update, frames=len(t), interval=50, blit=True)
[Link]()
return HTML(ani.to_jshtml())
# Create signals
t = [Link](0, 10, 100)
x = [Link](-0.5*t) * (t <= 5)
h = [Link](-2*t) * (t <= 3)
animate_convolution(x, h, t)
Other Applications
1. Multipath Channel Analysis
def multipath_signal(f0=1000, delays=[0, 0.001, 0.002], amps=[1, 0.7, 0.3], dur=0.01,
fs=44100):
t = [Link](0, dur, 1/fs)
signal = np.zeros_like(t)
for delay, amp in zip(delays, amps):
signal += amp * [Link](2*[Link]*f0*(t - delay))
return t, signal
t, sig = multipath_signal()
[Link](figsize=(10, 4))
[Link](t, sig)
[Link]('Multipath Signal Composition')
[Link]('Time (s)')
[Link]('Amplitude')
[Link](True)
[Link]()
2. Spectrogram Analysis
from [Link] import spectrogram
# Create a signal with time-varying frequency
fs = 10000
t = [Link](0, 1, 1/fs)
f_inst = 100 + 800*t # Frequency sweeps from 100Hz to 900Hz
x = [Link](2*[Link]*f_inst*t)
# Compute spectrogram
f, t_spec, Sxx = spectrogram(x, fs, nperseg=1024)
[Link](figsize=(10, 5))
[Link](t_spec, f, 10*np.log10(Sxx), shading='gouraud')
[Link](label='Power Spectral Density (dB)')
[Link]('Frequency [Hz]')
[Link]('Time [sec]')
[Link]('Spectrogram of Frequency-Swept Signal')
[Link]()