Butterworth filter cutoff attenuation is not exactly 0.707(-3dB)

301 views Asked by At

The documentation of scipy Butterworth filter says its cutoff attenuation is 0.707(-3dB). To validate this, I created a square wave signal, performed FFT and implemented a bandpass filter with a band range from 21Hz to 49Hz, finally compared the magnitude of cutoff frequencies(21Hz and 49Hz) between before filter implementation and after. However, the ratio is around 0.689, not even close to 0.707. Can anyone explain why?

import numpy as np
import matplotlib.pyplot as plt
from scipy.signal import butter, sosfilt

def butter_bandpass(lowcut, highcut, fs, order=5):
    nyq = 0.5 * fs
    normalized_low = lowcut / nyq
    normalized_high = highcut / nyq
    sos = butter(order, [normalized_low, normalized_high], btype='bandpass', output='sos')
    return sos

def filter_implementation(sos, data):
    y = sosfilt(sos, data)
    return y

#------------------------------------------------------------------------
# Create a signal

from scipy import signal

# How many time points are needed i,e., Sampling Frequency
sampling_frequency = 1000;

# At what intervals time points are sampled
sampling_interval = 1 / sampling_frequency;

# Begin time period of the signals
begin_time = 0;

# End time period of the signals
end_time = 10; 

# Define signal frequency
signal_frequency = 1

# Time points
time = np.arange(begin_time, end_time, sampling_interval);
data = signal.square(2 * np.pi * signal_frequency * time)

plt.figure(figsize = (8, 4))
plt.plot(time, data)
plt.xlabel("Time(s)")
plt.ylabel("Amplitude")
plt.title("Square Wave")
plt.grid()
plt.show()

#------------------------------------------------------------------------
# Perform fft

from scipy.fft import fft, fftfreq

def perform_fft(y, dt):
    yf_temp = fft(y)
    xf = fftfreq(len(y), dt)[:len(y)//2]
    yf = 2.0/len(y) * np.abs(yf_temp[0:len(y)//2])
    return xf, yf

xf, yf = perform_fft(data, 1/sampling_frequency)

plt.figure(figsize = (8, 4))
plt.plot(xf, yf)
plt.xlabel("Frequency(Hz)")
plt.ylabel("Amplitude")
plt.title("Frequency Domain Signal")
plt.grid()
plt.show()

#------------------------------------------------------------------------
# Apply bandpass filter

#Low Frequency
low_cut = 21

#high Frequency
high_cut = 49

print(xf[210])
print(yf[210])
ref_low = yf[210]

print(xf[490])
print(yf[490])
ref_high = yf[490]

# Apply bandpass filter
sos = butter_bandpass(low_cut, high_cut, sampling_frequency, order=5)
filtered_data = filter_implementation(sos, data)

# Plot time domain filtered_data
plt.figure(figsize = (8, 4))
plt.plot(time, filtered_data)
plt.xlabel("Time(s)")
plt.ylabel("Amplitude")
plt.title("Square Wave")
plt.grid()
plt.show()

# Plot frequency domain filtered_data
xf, yf = perform_fft(filtered_data, 1/sampling_frequency)

plt.figure(figsize = (8, 4))
plt.plot(xf, yf)
plt.xlabel("Frequency(Hz)")
plt.ylabel("Amplitude")
plt.title("Frequency Domain Signal")
plt.grid()
plt.show()

#------------------------------------------------------------------------
# Calculate attenuation at cutoff frequencies

print(yf[210]/ref_low)
print(yf[490]/ref_high)
1

There are 1 answers

0
John On

I just realized filter needs time to settle down. This settling process altered the beginning of time domain data and created the small difference. I took the second half of time domain filtered data and got a ratio of 0.70659.