Complex-Valued and Phase Plots
Visualizing Complex-Valued Data
In wireless communications and signal processing, nearly everything is complex-valued: channel coefficients, baseband signals, frequency responses, and constellation points. Plotting complex data requires special techniques — magnitude/phase dual plots, HSV phase coloring, constellation diagrams, and polar plots.
Definition: Magnitude/Phase (Bode) Plot
Magnitude/Phase (Bode) Plot
A complex-valued function is visualized as two vertically-stacked panels:
fig, (ax1, ax2) = plt.subplots(2, 1, sharex=True)
ax1.plot(f, 20*np.log10(np.abs(H)))
ax1.set_ylabel('|H(f)| (dB)')
ax2.plot(f, np.degrees(np.angle(H)))
ax2.set_ylabel('Phase (deg)')
ax2.set_xlabel('Frequency (Hz)')
Definition: Constellation Diagram
Constellation Diagram
A constellation diagram plots complex-valued symbols in the I-Q plane:
ax.scatter(symbols.real, symbols.imag, s=3, alpha=0.5)
ax.set(xlabel='In-Phase (I)', ylabel='Quadrature (Q)')
ax.set_aspect('equal')
ax.grid(True, alpha=0.3)
Ideal constellation points appear as tight clusters; noise and interference cause spreading.
Use ax.set_aspect('equal') — distorted aspect ratios make QPSK
look like a rectangle instead of a square.
Theorem: Phase Unwrapping
The np.angle() function returns phase in . When the
true phase exceeds this range, discontinuous jumps of
appear. np.unwrap(phase) corrects these:
Always unwrap phase before plotting to show the true continuous phase response.
A linear-phase FIR filter has . Without unwrapping, this appears as a sawtooth; after unwrapping, it is a clean straight line.
Example: Bode Plot of a Digital Filter
Plot the magnitude and unwrapped phase response of a 4th-order Butterworth lowpass filter.
Implementation
from scipy.signal import butter, freqz
import numpy as np, matplotlib.pyplot as plt
b, a = butter(4, 0.3)
w, H = freqz(b, a, worN=1024)
f_norm = w / np.pi
fig, (ax1, ax2) = plt.subplots(2, 1, sharex=True, figsize=(7, 5))
ax1.plot(f_norm, 20*np.log10(np.abs(H) + 1e-12), color='#2563EB')
ax1.set_ylabel('Magnitude (dB)')
ax1.set_ylim(-80, 5)
ax1.grid(True, alpha=0.3)
ax2.plot(f_norm, np.degrees(np.unwrap(np.angle(H))), color='#DC2626')
ax2.set_ylabel('Phase (degrees)')
ax2.set_xlabel('Normalized Frequency ( rad/sample)')
ax2.grid(True, alpha=0.3)
fig.suptitle('Butterworth LP Filter (order=4, )')
fig.tight_layout()
Example: 16-QAM Constellation with Noise
Plot an ideal and noisy 16-QAM constellation diagram side by side.
Implementation
rng = np.random.default_rng(42)
# Ideal 16-QAM points
iq = np.array([-3, -1, 1, 3])
I, Q = np.meshgrid(iq, iq)
ideal = (I + 1j*Q).ravel()
# Transmit with noise
N_sym = 5000
idx = rng.integers(0, 16, N_sym)
tx = ideal[idx]
snr_db = 15
noise = rng.standard_normal(N_sym) + 1j*rng.standard_normal(N_sym)
noise *= np.sqrt(np.mean(np.abs(ideal)**2) / (2 * 10**(snr_db/10)))
rx = tx + noise
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(10, 4))
ax1.scatter(ideal.real, ideal.imag, s=80, c='#2563EB', zorder=3)
ax1.set_title('Ideal 16-QAM')
ax2.scatter(rx.real, rx.imag, s=1, alpha=0.3, c='#2563EB')
ax2.scatter(ideal.real, ideal.imag, s=40, c='#DC2626', zorder=3,
marker='x', linewidths=2)
ax2.set_title(f'Received (SNR = {snr_db} dB)')
for ax in (ax1, ax2):
ax.set(xlabel='I', ylabel='Q', aspect='equal')
ax.grid(True, alpha=0.3)
fig.tight_layout()
Example: HSV Phase Coloring of a Complex Field
Visualize a 2D complex field where hue encodes phase and brightness encodes magnitude.
Implementation
from matplotlib.colors import hsv_to_rgb
x = np.linspace(-3, 3, 400)
X, Y = np.meshgrid(x, x)
Z = (X + 1j*Y)**2 - 1 # complex polynomial
mag = np.abs(Z)
phase = np.angle(Z)
H = (phase + np.pi) / (2 * np.pi) # [0, 1]
S = np.ones_like(H)
V = 1 - 1 / (1 + mag) # sigmoid brightness
rgb = hsv_to_rgb(np.stack([H, S, V], axis=-1))
fig, ax = plt.subplots(figsize=(6, 6))
ax.imshow(rgb, extent=[-3, 3, -3, 3], origin='lower')
ax.set(xlabel='Re(z)', ylabel='Im(z)',
title=r' (HSV phase coloring)')
Constellation Diagram Explorer
View constellation diagrams for different modulation schemes at various SNR levels. See how noise spreads the ideal constellation points.
Parameters
Phase Rotation Animation
Watch how a frequency offset causes the constellation to rotate over time, and how a phase-locked loop (PLL) corrects it.
Parameters
Why This Matters: Constellation Diagrams and EVM
The Error Vector Magnitude (EVM) is measured directly from the constellation diagram as the RMS distance between received symbols and ideal constellation points. 3GPP specifies EVM limits for each modulation order: 17.5% for QPSK, 12.5% for 16-QAM, 8% for 64-QAM, and 3.5% for 256-QAM in 5G NR. Plotting the constellation is the first diagnostic step when a transmitter fails EVM tests.
Key Takeaway
For complex-valued data, always use dual mag/phase plots or
constellation diagrams. Never plot only np.real(z) — you lose
half the information. Always unwrap phase before plotting, and
always use equal aspect ratio for constellation diagrams.
Quick Check
What does np.unwrap() do to a phase array?
Converts radians to degrees
Removes discontinuities larger than pi by adding multiples of 2*pi
Clips the phase to [-pi, pi]
Computes the inverse FFT of the phase
unwrap detects jumps > pi between consecutive samples and adds/subtracts 2*pi to make the phase continuous.
Constellation Diagram
A scatter plot of complex-valued symbols in the I-Q plane, showing the positions of transmitted or received modulation points.
Related: EVM (Error Vector Magnitude)
EVM (Error Vector Magnitude)
The RMS distance between received symbols and their ideal constellation positions, expressed as a percentage of the reference signal amplitude.
Bode Plot
A dual plot showing the magnitude (in dB) and phase of a complex-valued frequency response as functions of frequency.