Hilbert Transform and Analytic Signal: Instantaneous Amplitude, Phase, and Frequency in Python

Explains the theory of the Hilbert transform (frequency-domain interpretation, analytic signal) and provides Python implementations of instantaneous amplitude, phase, and frequency using scipy.signal.hilbert, including AM demodulation and envelope detection.

Introduction

When analyzing audio, vibration, or biomedical signals, it is often necessary to know the instantaneous amplitude or instantaneous frequency at each point in time. However, the conventional Fourier transform only reveals the overall frequency content of a signal, losing the information about when each frequency component is present.

The Hilbert Transform is a powerful tool that solves this problem. By using the Hilbert transform to construct the Analytic Signal, we can directly compute the instantaneous amplitude, instantaneous phase, and instantaneous frequency of any real signal.

This article covers the mathematical definition of the Hilbert transform through to practical Python implementations using scipy.signal.hilbert.

Definition of the Hilbert Transform

Continuous-Time Hilbert Transform

The Hilbert transform \(\hat{x}(t)\) of a real signal \(x(t)\) is defined as:

\[\hat{x}(t) = \mathcal{H}\{x(t)\} = \frac{1}{\pi} \text{P.V.} \int_{-\infty}^{\infty} \frac{x(\tau)}{t - \tau} d\tau \tag{1}\]

where P.V. denotes the Cauchy principal value integral, which symmetrically avoids the singularity at \(t = \tau\).

Equation \((1)\) can be viewed as the convolution of \(x(t)\) with \(h(t) = \frac{1}{\pi t}\):

\[\hat{x}(t) = x(t) * \frac{1}{\pi t} \tag{2}\]

Frequency-Domain Interpretation

By the convolution theorem, the Hilbert transform takes a particularly elegant form in the frequency domain:

\[\hat{X}(f) = H(f) \cdot X(f) \tag{3}\]

where the filter \(H(f)\) is:

\[H(f) = \begin{cases} -j & (f > 0) \\ 0 & (f = 0) \\ +j & (f < 0) \end{cases} \tag{4}\]

In other words, the Hilbert transform is an all-pass filter that shifts positive frequency components by \(-90°\) and negative frequency components by \(+90°\). The amplitude spectrum is unchanged; only the phase is rotated.

This property leads to the well-known result: applying the Hilbert transform to a cosine gives a sine:

\[\mathcal{H}\{\cos(2\pi f_0 t)\} = \sin(2\pi f_0 t) \tag{5}\]

Analytic Signal

Definition

From a real signal \(x(t)\), we construct the analytic signal \(z(t)\) as:

\[z(t) = x(t) + j\hat{x}(t) \tag{6}\]

The analytic signal is complex-valued: its real part is the original signal \(x(t)\) and its imaginary part is the Hilbert transform \(\hat{x}(t)\).

In the frequency domain:

\[Z(f) = \begin{cases} 2X(f) & (f > 0) \\ X(0) & (f = 0) \\ 0 & (f < 0) \end{cases} \tag{7}\]

That is, the analytic signal suppresses the negative-frequency components and doubles the positive-frequency components. This one-sided spectrum representation is what makes instantaneous feature extraction possible.

Instantaneous Amplitude, Phase, and Frequency

Writing the analytic signal in polar form:

\[z(t) = A(t) \cdot e^{j\phi(t)} \tag{8}\]

we can directly extract the following instantaneous characteristics.

Instantaneous amplitude (envelope):

\[A(t) = |z(t)| = \sqrt{x(t)^2 + \hat{x}(t)^2} \tag{9}\]

Instantaneous phase:

\[\phi(t) = \angle z(t) = \arctan\!\left(\frac{\hat{x}(t)}{x(t)}\right) \tag{10}\]

Instantaneous frequency:

\[f_i(t) = \frac{1}{2\pi} \frac{d\phi(t)}{dt} \tag{11}\]

The instantaneous frequency is the time derivative of the phase and tracks how the signal’s frequency evolves over time.

Python Implementation

Using scipy.signal.hilbert

SciPy provides scipy.signal.hilbert, which computes the analytic signal efficiently:

from scipy.signal import hilbert
import numpy as np

# Input signal
x = np.array([...])  # real-valued signal

# Compute the analytic signal (uses discrete Hilbert transform internally)
z = hilbert(x)

# Instantaneous amplitude (envelope)
amplitude = np.abs(z)

# Instantaneous phase (radians)
phase = np.angle(z)

# Instantaneous frequency (Hz)
fs = 1000  # sampling frequency
inst_freq = np.diff(np.unwrap(phase)) / (2 * np.pi) * fs

scipy.signal.hilbert is implemented using the FFT, with complexity \(O(N \log N)\). Internally it applies the frequency-domain filter in equation \((4)\) and returns the analytic signal \(z(t)\).

Note: The return value of scipy.signal.hilbert is the full analytic signal \(z(t)\) (real part = original signal, imaginary part = Hilbert transform). To obtain only \(\hat{x}(t)\), use np.imag(hilbert(x)).

Basic Verification

We verify the \(\mathcal{H}\{\cos\} = \sin\) identity numerically:

import numpy as np
import matplotlib.pyplot as plt
from scipy.signal import hilbert

# --- Signal generation ---
fs = 1000          # sampling frequency [Hz]
f0 = 10            # signal frequency [Hz]
t = np.arange(0, 1, 1/fs)

cos_signal = np.cos(2 * np.pi * f0 * t)
sin_signal = np.sin(2 * np.pi * f0 * t)

# --- Hilbert transform ---
cos_hilbert = np.imag(hilbert(cos_signal))  # expected: sin
sin_hilbert = np.imag(hilbert(sin_signal))  # expected: -cos

# --- Plot ---
fig, axes = plt.subplots(2, 1, figsize=(10, 6))

axes[0].plot(t[:200], cos_signal[:200], label='cos(t)')
axes[0].plot(t[:200], cos_hilbert[:200], '--', label='H{cos(t)} ≈ sin(t)')
axes[0].set_title('Hilbert Transform of cos(t)')
axes[0].set_xlabel('Time [s]')
axes[0].legend()
axes[0].grid(True, alpha=0.3)

axes[1].plot(t[:200], sin_signal[:200], label='sin(t)')
axes[1].plot(t[:200], sin_hilbert[:200], '--', label='H{sin(t)} ≈ -cos(t)')
axes[1].set_title('Hilbert Transform of sin(t)')
axes[1].set_xlabel('Time [s]')
axes[1].legend()
axes[1].grid(True, alpha=0.3)

plt.tight_layout()
plt.show()

# Verify errors
print(f"Max error H{{cos}} vs sin: {np.max(np.abs(cos_hilbert - sin_signal)):.2e}")
print(f"Max error H{{sin}} vs -cos: {np.max(np.abs(sin_hilbert + cos_signal)):.2e}")
# Example output:
# Max error H{cos} vs sin: 1.33e-15
# Max error H{sin} vs -cos: 1.33e-15

The errors are at the level of floating-point precision (\(10^{-15}\)), confirming that \(\mathcal{H}\{\cos\} = \sin\) and \(\mathcal{H}\{\sin\} = -\cos\) hold exactly.

Application 1: Envelope Detection

Extracting the Envelope of an AM Signal

In amplitude modulation (AM), a carrier wave is modulated by an information signal. Hilbert-based envelope detection allows us to recover the original message signal.

import numpy as np
import matplotlib.pyplot as plt
from scipy.signal import hilbert

# --- Generate AM signal ---
fs = 10000          # sampling frequency [Hz]
t = np.arange(0, 1, 1/fs)

# Message signal (5 Hz)
f_msg = 5
message = 0.5 * np.sin(2 * np.pi * f_msg * t)

# Carrier (500 Hz)
f_carrier = 500
carrier = np.cos(2 * np.pi * f_carrier * t)

# AM modulation: x(t) = (1 + m(t)) * carrier
am_signal = (1 + message) * carrier

# --- Envelope detection using Hilbert transform ---
analytic = hilbert(am_signal)
envelope = np.abs(analytic)  # instantaneous amplitude = envelope

# --- Plot ---
fig, axes = plt.subplots(3, 1, figsize=(10, 9))

axes[0].plot(t, am_signal, alpha=0.5, label='AM Signal')
axes[0].plot(t, envelope, 'r-', linewidth=2, label='Envelope (Hilbert)')
axes[0].plot(t, -envelope, 'r-', linewidth=2)
axes[0].set_xlim(0, 0.4)
axes[0].set_xlabel('Time [s]')
axes[0].set_ylabel('Amplitude')
axes[0].set_title('AM Signal and Envelope')
axes[0].legend()
axes[0].grid(True, alpha=0.3)

axes[1].plot(t, 1 + message, 'k--', label='True Envelope')
axes[1].plot(t, envelope, 'r-', alpha=0.8, label='Detected Envelope')
axes[1].set_xlim(0, 0.4)
axes[1].set_xlabel('Time [s]')
axes[1].set_ylabel('Amplitude')
axes[1].set_title('True Envelope vs Detected Envelope')
axes[1].legend()
axes[1].grid(True, alpha=0.3)

demodulated = envelope - 1  # remove DC component
axes[2].plot(t, message, 'k--', label='Original Message')
axes[2].plot(t, demodulated, 'r-', alpha=0.8, label='Demodulated (Hilbert)')
axes[2].set_xlim(0, 0.4)
axes[2].set_xlabel('Time [s]')
axes[2].set_ylabel('Amplitude')
axes[2].set_title('Demodulation Result')
axes[2].legend()
axes[2].grid(True, alpha=0.3)

plt.tight_layout()
plt.show()

rmse = np.sqrt(np.mean((message - demodulated)**2))
print(f"Demodulation RMSE: {rmse:.6f}")

The detected envelope closely follows the shape of the original 5 Hz message signal, demonstrating accurate AM demodulation.

Application 2: Instantaneous Frequency Estimation

Chirp Signal Analysis

We apply the Hilbert transform to a chirp signal whose frequency varies linearly over time.

import numpy as np
import matplotlib.pyplot as plt
from scipy.signal import hilbert, chirp

# --- Generate chirp signal (50 Hz → 200 Hz over 1 second) ---
fs = 4000
t = np.arange(0, 1, 1/fs)
f_start, f_end = 50, 200
signal = chirp(t, f0=f_start, f1=f_end, t1=1, method='linear')

# --- Instantaneous frequency via Hilbert transform ---
analytic = hilbert(signal)
phase = np.unwrap(np.angle(analytic))
inst_freq = np.diff(phase) / (2 * np.pi) * fs

# Theoretical frequency (linear ramp)
f_theory = f_start + (f_end - f_start) * t[:-1]

# --- Plot ---
fig, axes = plt.subplots(3, 1, figsize=(10, 9))

axes[0].plot(t, signal, alpha=0.7)
axes[0].set_xlabel('Time [s]')
axes[0].set_ylabel('Amplitude')
axes[0].set_title(f'Chirp Signal ({f_start} Hz → {f_end} Hz)')
axes[0].grid(True, alpha=0.3)

axes[1].plot(t, np.abs(analytic), 'r-')
axes[1].set_xlabel('Time [s]')
axes[1].set_ylabel('Amplitude')
axes[1].set_title('Instantaneous Amplitude (Envelope)')
axes[1].set_ylim(0, 1.5)
axes[1].grid(True, alpha=0.3)

axes[2].plot(t[:-1], inst_freq, label='Instantaneous Frequency (Hilbert)')
axes[2].plot(t[:-1], f_theory, 'k--', label='Theoretical Frequency')
axes[2].set_xlabel('Time [s]')
axes[2].set_ylabel('Frequency [Hz]')
axes[2].set_title('Instantaneous Frequency vs Theoretical')
axes[2].legend()
axes[2].grid(True, alpha=0.3)

plt.tight_layout()
plt.show()

mae = np.mean(np.abs(inst_freq - f_theory))
print(f"Mean absolute error in instantaneous frequency: {mae:.4f} Hz")

The estimated instantaneous frequency closely matches the theoretical linear ramp, demonstrating the accuracy of Hilbert-based frequency tracking.

Application 3: Envelope Spectrum Analysis for Fault Diagnosis

A practical application of the Hilbert transform is envelope spectrum analysis for rotating machinery fault diagnosis. Bearing defects generate repetitive impulse-like vibrations, and the envelope spectrum of the filtered vibration signal reveals characteristic frequency peaks related to the fault.

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

# --- Simulate bearing fault vibration signal ---
np.random.seed(42)
fs = 20000
t = np.arange(0, 1, 1/fs)

# High-frequency resonance (bearing natural frequency: 5 kHz)
f_resonance = 5000
resonance_carrier = np.cos(2 * np.pi * f_resonance * t)

# Fault frequency: 100 Hz repetitive impulses
f_fault = 100
impulse_train = np.zeros_like(t)
impulse_indices = (np.arange(0, 1, 1/f_fault) * fs).astype(int)
impulse_indices = impulse_indices[impulse_indices < len(t)]
impulse_train[impulse_indices] = 1.0

alpha = 500
decay = np.exp(-alpha * np.mod(t, 1/f_fault))
fault_component = np.convolve(impulse_train, decay[:len(t)//f_fault], mode='same')
fault_signal = fault_component * 0.3 * resonance_carrier

observed = fault_signal + 0.5 * np.random.randn(len(t))

# --- Step 1: Bandpass filter around resonance frequency ---
sos = butter(4, [4000, 6000], btype='bandpass', fs=fs, output='sos')
filtered = sosfiltfilt(sos, observed)

# --- Step 2: Envelope via Hilbert transform ---
analytic = hilbert(filtered)
envelope = np.abs(analytic)

# --- Step 3: FFT of the envelope ---
N = len(envelope)
envelope_spectrum = np.abs(np.fft.rfft(envelope - np.mean(envelope))) / N
freqs = np.fft.rfftfreq(N, 1/fs)

# --- Plot ---
fig, axes = plt.subplots(3, 1, figsize=(10, 9))

axes[0].plot(t[:3000], observed[:3000], alpha=0.7)
axes[0].set_xlabel('Time [s]')
axes[0].set_ylabel('Amplitude')
axes[0].set_title('Observed Vibration Signal')
axes[0].grid(True, alpha=0.3)

axes[1].plot(t[:3000], filtered[:3000], alpha=0.5, label='Bandpass Filtered')
axes[1].plot(t[:3000], envelope[:3000], 'r-', linewidth=1.5, label='Envelope')
axes[1].set_xlabel('Time [s]')
axes[1].set_ylabel('Amplitude')
axes[1].set_title('Bandpass Filtered Signal and Envelope')
axes[1].legend()
axes[1].grid(True, alpha=0.3)

mask = freqs <= 500
axes[2].plot(freqs[mask], envelope_spectrum[mask])
axes[2].axvline(f_fault, color='r', linestyle='--', alpha=0.7,
               label=f'Fault Freq = {f_fault} Hz')
for harmonic in range(2, 6):
    axes[2].axvline(f_fault * harmonic, color='r', linestyle=':', alpha=0.4)
axes[2].set_xlabel('Frequency [Hz]')
axes[2].set_ylabel('Amplitude')
axes[2].set_title('Envelope Spectrum (Fault Diagnosis)')
axes[2].legend()
axes[2].grid(True, alpha=0.3)

plt.tight_layout()
plt.show()

Peaks appear in the envelope spectrum at the fault frequency (100 Hz) and its harmonics (200 Hz, 300 Hz, …). This technique is widely used in industrial bearing and gear fault detection.

Practical Considerations

Edge Effects

Because scipy.signal.hilbert is FFT-based, it assumes the input signal is periodic. When the signal is not periodic over the analysis window, discontinuities at the boundaries cause accuracy degradation near the edges — the so-called edge effect.

Mitigation strategies:

  • Pad the signal with zeros at both ends so that edge artifacts fall outside the region of interest.
  • Acquire extra data before and after the analysis window and discard the buffered portions after processing.

Narrowband Condition

A physically meaningful interpretation of instantaneous frequency requires the signal to be narrowband (a single dominant frequency component at any given time). For wideband or multi-component signals, the instantaneous frequency can become negative or otherwise meaningless.

Signal TypeRecommended Pre-processing
Wideband signalBandpass filter to isolate the band of interest
Multi-component signalEMD (Empirical Mode Decomposition) to separate IMFs
Non-stationary signalConsider STFT or wavelet transform

Summary

  • The Hilbert transform is a \(-90°\) phase-shift operator; in the frequency domain it is expressed as \(H(f) = -j\,\text{sgn}(f)\)
  • The analytic signal, formed by combining the original signal with its Hilbert transform, has a one-sided spectrum and enables extraction of instantaneous signal characteristics
  • The modulus, argument, and time derivative of the argument of the analytic signal yield the instantaneous amplitude, instantaneous phase, and instantaneous frequency, respectively
  • scipy.signal.hilbert provides an \(O(N \log N)\) FFT-based implementation suitable for envelope detection, AM demodulation, and fault diagnosis
  • Edge effects and the narrowband condition must be considered; appropriate bandpass filtering or padding is recommended

References

  • Gabor, D. (1946). “Theory of communication.” Journal of the Institution of Electrical Engineers, 93(3), 429-457.
  • Huang, N. E., et al. (1998). “The empirical mode decomposition and the Hilbert spectrum for nonlinear and non-stationary time series analysis.” Proceedings of the Royal Society of London A, 454, 903-995.
  • Oppenheim, A. V., & Schafer, R. W. (2009). Discrete-Time Signal Processing (3rd ed.). Prentice Hall.
  • SciPy signal.hilbert documentation