Data Analysis for Engineers/Module 3

Module 3 · Section 10 of 11

Lesson 3.9 - Signal Processing Basics

Target: ~9 min read - 20 min hands-on

Overview

The Fast Fourier Transform (FFT) decomposes a signal into its constituent frequencies - essential whenever you need to know which frequency dominates a noisy sensor stream. The same technique reads a machine's running speed from an accelerometer, a fault signature from motor current, a harmonic from a power-quality log, or a resonance from an acoustic record. We'll analyze a synthetic vibration signal and apply a simple low-pass filter.

Why This Matters (Engineering Context)

Frequency-domain monitoring is spreading across PH manufacturing, power, and process plants - a shift in a dominant frequency is often the earliest warning of a developing fault, well before an amplitude threshold trips.

Code-Along

import numpy as np

np.random.seed(5)

# Build a synthetic vibration signal: 30 Hz fundamental + 60 Hz harmonic + noise
fs = 1000            # sampling frequency, Hz (samples per second)
duration = 2.0
# time axis: fs*duration samples, endpoint=False so it tiles cleanly for the FFT
t = np.linspace(0, duration, int(fs * duration), endpoint=False)

signal = (
    1.0 * np.sin(2 * np.pi * 30 * t)       # amplitude 1.0 at 30 Hz
    + 0.35 * np.sin(2 * np.pi * 60 * t)     # amplitude 0.35 at 60 Hz
    + 0.4 * np.random.normal(0, 1, len(t)) # broadband noise
)

# rfft = FFT for real input; rfftfreq gives the frequency (Hz) of each FFT bin
fft_vals = np.fft.rfft(signal)
fft_freqs = np.fft.rfftfreq(len(signal), d=1 / fs)
magnitude = np.abs(fft_vals) / len(signal)   # |complex| -> amplitude, normalised

# argmax = index of the largest magnitude -> the dominant frequency bin
dominant_freq = fft_freqs[np.argmax(magnitude)]
print(f"Dominant frequency: {dominant_freq:.1f} Hz")
print(f"Implied running speed: {dominant_freq * 60:.0f} RPM")   # Hz * 60 = rev/min

# argsort returns indices low->high; [-3:] = the 3 biggest, [::-1] reverses to high->low
top3 = np.argsort(magnitude)[-3:][::-1]
print("\nTop 3 frequency components:")
for idx in top3:
    print(f"  {fft_freqs[idx]:6.1f} Hz  magnitude={magnitude[idx]:.3f}")

# Simple low-pass filter: zero the FFT bins above 45 Hz, then irfft back to time domain
fft_filtered = fft_vals.copy()
fft_filtered[fft_freqs > 45] = 0
signal_filtered = np.fft.irfft(fft_filtered, n=len(signal))
print(f"\nOriginal signal std dev: {np.std(signal):.3f}")
print(f"Filtered signal std dev: {np.std(signal_filtered):.3f} (noise reduced)")

Run it: the dominant frequency lands at (or very close to) 30 Hz, with the second-strongest peak near 60 Hz. After low-pass filtering above 45 Hz, the signal's standard deviation drops, because most of the random-noise energy sits at higher frequencies and gets removed while the 30 Hz component is preserved.

Practice Exercises

  1. Change the low-pass cutoff to 25 Hz - what happens to the 30 Hz component itself?
  2. Add a third component at 90 Hz with amplitude 0.15 and confirm it appears as a (smaller) peak in the FFT.
  3. Compute the running speed in RPM for a machine whose dominant vibration frequency is measured at 24.5 Hz.
# Try the practice exercises here

Knowledge Check

  1. What does the FFT convert a signal from and into?
  2. Why might a secondary frequency peak matter diagnostically?
  3. What's the risk of setting a low-pass cutoff too close to the signal's own dominant frequency?
Answer key
  1. From the time domain into the frequency domain
  2. Specific harmonic patterns map to specific faults, so they say what is wrong, not just that something is
  3. You filter out part of the genuine signal, not just the noise, distorting the measurement

32 / 63 sections · Course home · Join the coaching cohort