Constellation Design and Mapping
Why Digital Modulation Matters
Every wireless system encodes bits into complex-valued symbols before transmission. The choice of constellation determines the data rate, error performance, and peak-to-average power ratio. In this section you will implement constellation mappers for BPSK, QPSK, 16-QAM, and 64-QAM, learning how Gray coding minimizes bit errors when neighboring symbols are confused by noise.
Definition: Digital Constellation
Digital Constellation
An -ary constellation is a set of complex-valued symbols where each symbol represents bits. The average symbol energy is:
and the average bit energy is .
import numpy as np
# QPSK constellation (M=4, k=2 bits/symbol)
M = 4
qpsk = np.exp(1j * np.pi * np.array([1, 3, 7, 5]) / 4)
Es = np.mean(np.abs(qpsk)**2)
Eb = Es / np.log2(M)
Normalizing so that simplifies SNR calculations. Multiply by to set any desired energy.
Definition: Gray Code Mapping
Gray Code Mapping
A Gray code assigns bit labels to constellation points such that adjacent symbols differ in exactly one bit. When noise causes a symbol error to a neighbor, only one bit is wrong, so:
at moderate-to-high SNR, where is the symbol error rate.
def gray_code(n_bits):
"""Generate Gray code sequence for n_bits."""
if n_bits == 0:
return [0]
lower = gray_code(n_bits - 1)
return lower + [x + (1 << (n_bits - 1)) for x in reversed(lower)]
Without Gray coding, a single symbol error can cause up to bit errors. Gray coding is universally used in practice.
Definition: Minimum Euclidean Distance
Minimum Euclidean Distance
The minimum Euclidean distance of a constellation is:
This quantity dominates the high-SNR error probability. For -QAM with average energy :
from itertools import combinations
def compute_dmin(constellation):
"""Compute minimum Euclidean distance."""
dists = [abs(si - sj) for si, sj in combinations(constellation, 2)]
return min(dists)
Definition: Rectangular QAM Constellation
Rectangular QAM Constellation
An -QAM constellation ( for some integer ) is the Cartesian product of two -PAM alphabets:
After normalization to unit average energy:
def qam_constellation(M):
"""Generate normalized M-QAM constellation."""
sqrt_M = int(np.sqrt(M))
assert sqrt_M**2 == M, "M must be a perfect square"
pam = np.arange(sqrt_M) - (sqrt_M - 1) / 2
I, Q = np.meshgrid(pam, pam)
constellation = (I + 1j * Q).flatten()
# Normalize to unit average energy
constellation /= np.sqrt(np.mean(np.abs(constellation)**2))
return constellation
Definition: Phase Shift Keying (PSK)
Phase Shift Keying (PSK)
An -PSK constellation places symbols uniformly on the unit circle:
All symbols have the same energy , making PSK attractive for nonlinear channels (constant envelope). The minimum distance is .
def psk_constellation(M):
"""Generate M-PSK constellation."""
return np.exp(1j * 2 * np.pi * np.arange(M) / M)
BPSK () and QPSK () are the most common PSK schemes. QPSK has the same BER as BPSK but double the spectral efficiency.
Definition: Bit-to-Symbol Mapper with Gray Coding
Bit-to-Symbol Mapper with Gray Coding
A mapper takes a binary bit stream and groups consecutive bits into an integer index, which selects a constellation point. With Gray coding, the mapping is:
def bits_to_symbols(bits, constellation, gray_map):
"""Map bits to constellation symbols using Gray mapping."""
k = int(np.log2(len(constellation)))
n_symbols = len(bits) // k
bit_groups = bits[:n_symbols * k].reshape(n_symbols, k)
indices = np.array([int(''.join(map(str, b)), 2) for b in bit_groups])
symbols = constellation[gray_map[indices]]
return symbols
Theorem: Optimality of Antipodal Signaling
Among all binary constellations with average bit energy , BPSK (, ) maximizes the minimum Euclidean distance and achieves:
This is the best achievable BER for uncoded binary signaling over AWGN.
Placing two points as far apart as possible on the real line maximizes the distance for a given energy budget. The factor of 2 in the exponent arises because the distance is , not .
Decision rule
The ML detector compares to 0. An error occurs when (or ).
Error probability
since .
Theorem: Nearest-Neighbor Union Bound
For an -ary constellation in AWGN, the symbol error rate is upper-bounded by:
At high SNR, only the nearest-neighbor pairs dominate:
where is the average number of nearest neighbors.
Each pairwise error acts like a BPSK decision between two symbols. The union bound sums these pairwise errors, and at high SNR only the closest pairs matter.
Theorem: QPSK BER Equivalence to BPSK
With Gray coding, QPSK achieves the same BER as BPSK:
despite transmitting 2 bits per symbol (double the spectral efficiency).
QPSK decomposes into two orthogonal BPSK channels on the I and Q axes. Each axis carries one bit at energy , and the noise on the two axes is independent.
Decomposition
Write where and . The I and Q components decouple into independent BPSK decisions.
BER per component
Each component has energy and noise variance , giving per component.
Example: Building QAM Constellations in NumPy
Implement a function that generates BPSK, QPSK, 16-QAM, and 64-QAM constellations with Gray coding and unit average energy.
QAM generator
import numpy as np
def qam_constellation(M):
sqrt_M = int(np.sqrt(M))
pam = 2 * np.arange(sqrt_M) - sqrt_M + 1
I, Q = np.meshgrid(pam, pam)
const = (I + 1j * Q).flatten()
return const / np.sqrt(np.mean(np.abs(const)**2))
for M in [4, 16, 64]:
c = qam_constellation(M)
print(f"{M}-QAM: Es={np.mean(np.abs(c)**2):.4f}, "
f"dmin={min(abs(ci-cj) for i,ci in enumerate(c) for j,cj in enumerate(c) if i<j):.4f}")
Verify
All constellations have . The decreases as increases (points get closer), which is why higher-order QAM requires higher SNR.
Example: Implementing Gray Code Bit Mapping
Implement Gray-coded bit mapping for 16-QAM and verify that adjacent constellation points differ by exactly one bit.
Gray code generation
def gray_encode(n):
return n ^ (n >> 1)
def gray_map_qam(M):
sqrt_M = int(np.sqrt(M))
k = int(np.log2(M))
gray_I = [gray_encode(i) for i in range(sqrt_M)]
gray_Q = [gray_encode(q) for q in range(sqrt_M)]
mapping = {}
for qi, gq in enumerate(gray_Q):
for ii, gi in enumerate(gray_I):
symbol_idx = qi * sqrt_M + ii
bit_label = (gq << (k//2)) | gi
mapping[bit_label] = symbol_idx
return mapping
Verify adjacency
mapping = gray_map_qam(16)
# Check: neighboring symbols differ by 1 bit
for label, idx in mapping.items():
bits = format(label, '04b')
print(f"Label {bits} -> symbol {idx}")
Example: Comparing BER Across Constellation Sizes
Simulate the BER of BPSK, QPSK, 16-QAM, and 64-QAM over AWGN and compare with theoretical curves.
Simulation setup
from scipy.special import erfc
def Q(x):
return 0.5 * erfc(x / np.sqrt(2))
def ber_mqam_theory(M, EbN0_dB):
EbN0 = 10**(EbN0_dB / 10)
k = np.log2(M)
return (4 / k) * (1 - 1/np.sqrt(M)) * Q(np.sqrt(3*k*EbN0/(M-1)))
Monte Carlo BER
def simulate_ber(M, EbN0_dB, n_bits=1_000_000):
k = int(np.log2(M))
constellation = qam_constellation(M)
bits = np.random.randint(0, 2, n_bits)
symbols = constellation[bits.reshape(-1, k).dot(1 << np.arange(k)[::-1]) % M]
EbN0 = 10**(EbN0_dB / 10)
sigma = np.sqrt(1 / (2 * k * EbN0))
noise = sigma * (np.random.randn(len(symbols)) + 1j*np.random.randn(len(symbols)))
r = symbols + noise
# ML detection: nearest constellation point
det_idx = np.argmin(np.abs(r[:, None] - constellation[None, :]), axis=1)
det_bits = np.array([list(np.binary_repr(i, k)) for i in det_idx], dtype=int).flatten()
return np.mean(bits[:len(det_bits)] != det_bits)
Interactive Constellation Viewer
Explore BPSK, QPSK, 16-QAM, and 64-QAM constellations. Adjust SNR to see noisy received symbols scatter around the ideal points.
Parameters
BER Comparison Across Modulation Schemes
Compare theoretical and simulated BER curves for different modulation orders.
Parameters
Gray Code vs Natural Binary: BER Impact
Compare BER with Gray coding versus natural binary labeling.
Parameters
Standard Constellation Gallery
Noise Effect on Constellation Points
Watch how increasing noise variance spreads received symbols away from ideal constellation points, causing detection errors.
Parameters
Quick Check
How many bits does each symbol carry in 64-QAM?
4
6
8
64
Correct: bits per symbol.
Quick Check
What is the primary benefit of Gray coding in a digital constellation?
Reduces symbol error rate
Ensures adjacent symbols differ by one bit
Increases minimum Euclidean distance
Reduces peak-to-average power ratio
Correct: this minimizes the number of bit errors per symbol error.
Common Mistake: Forgetting Constellation Normalization
Mistake:
Creating a QAM constellation without normalizing to unit energy, then using formulas that assume .
Correction:
Always normalize: const /= np.sqrt(np.mean(np.abs(const)**2)).
Alternatively, scale the noise variance to match the actual .
Common Mistake: Wrong Noise Variance for Complex AWGN
Mistake:
Adding complex noise with total variance instead of per real/imaginary component.
Correction:
For complex AWGN: n = sqrt(N0/2) * (randn(...) + 1j*randn(...)).
The total noise power is , split equally between
I and Q.
Key Takeaway
Higher-order constellations (16-QAM, 64-QAM) increase spectral efficiency ( bits/symbol) but require higher SNR to maintain the same BER. Gray coding is essential to minimize the BER-to-SER gap.
Key Takeaway
QPSK achieves the same BER as BPSK while doubling spectral efficiency. This is because QPSK decomposes into two independent BPSK channels on the I and Q axes.
Why This Matters: Modulation in 5G NR
5G NR supports QPSK, 16-QAM, 64-QAM, and 256-QAM. The base station selects the modulation order based on channel quality feedback (CQI): strong channels use 256-QAM for maximum throughput, while weak channels fall back to QPSK for robustness. The adaptive modulation and coding (AMC) scheme is the single most important factor in wireless throughput.
See full treatment in Chapter 22
Historical Note: The Evolution of QAM
1960s-presentQuadrature amplitude modulation was first used in telephone modems in the 1960s. The V.29 modem (1976) used 16-QAM at 9600 bps. Today, Wi-Fi 6 uses 1024-QAM, and cable modems use 4096-QAM, pushing the limits of analog hardware precision.
Historical Note: Frank Gray and the Reflected Binary Code
1953Frank Gray of Bell Labs patented the reflected binary code in 1953 (US Patent 2,632,058). He developed it to prevent errors in analog-to-digital conversion, but it became fundamental to digital communications. The recursive construction (reflect and prefix) is both elegant and practical.
Constellation
A set of complex-valued symbols used in digital modulation. Each symbol represents bits for an -ary constellation.
Related: Quadrature Amplitude Modulation (QAM), Phase Shift Keying (PSK)
Quadrature Amplitude Modulation (QAM)
A modulation scheme where symbols occupy a rectangular grid in the complex plane, varying both amplitude and phase.
Related: Constellation
Phase Shift Keying (PSK)
A modulation scheme where all symbols have equal energy (lie on a circle) and information is encoded only in the phase.
Related: Constellation
Gray Code
A binary labeling scheme where adjacent symbols differ by exactly one bit, minimizing the bit error rate for nearest-neighbor symbol errors.
Minimum Euclidean Distance
The smallest distance between any two constellation points; dominates the error probability at high SNR.
Spectral Efficiency
The number of bits transmitted per second per Hertz; equals for -ary modulation with Nyquist pulse shaping.
Modulation Scheme Comparison
| Property | BPSK | QPSK | 16-QAM | 64-QAM |
|---|---|---|---|---|
| Bits/symbol | 1 | 2 | 4 | 6 |
| 2.00 | 1.41 | 0.63 | 0.31 | |
| BER at 10 dB | ||||
| Constant envelope? | Yes | Yes | No | No |
| 5G NR support | No | Yes | Yes | Yes |