0
$\begingroup$

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)
$\endgroup$
1
  • $\begingroup$ Most likely your filter is fine but your analysis is wrong. Your choice of test signal and the way you do the spectral analysis is questionable. To can use docs.scipy.org/doc/scipy/reference/generated/… directly to calculate the transfer function of the filter or use an unit impulse as test signal $\endgroup$
    – Hilmar
    Commented Apr 27, 2022 at 9:11

2 Answers 2

1
$\begingroup$

What you see there are margin issues. By not applying a window function to your signal before the FFT, you effectively convolute your spectrum with an $\text{si}$ function, which leads to artifacts such as the ones you observed. Try adding

y *= np.hanning(len(y))

as the first line in your perform_fft function, that should get you closer to the truth.

$\endgroup$
2
  • $\begingroup$ I just realized why and posted an answer $\endgroup$
    – John
    Commented Apr 27, 2022 at 10:07
  • 1
    $\begingroup$ None the less, windowing is a good idea when dealing with FFTs. $\endgroup$
    – Max
    Commented Apr 27, 2022 at 10:16
1
$\begingroup$

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.

$\endgroup$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.