Bit error rate of M-QAM over AWGN#

The waterfall curves of Gray-mapped 4-, 16- and 64-QAM over the additive white Gaussian noise channel, against the closed form

\[P_b \approx \frac{4}{\log_2 M}\left(1 - \frac{1}{\sqrt{M}}\right) Q\left(\sqrt{\frac{3 \log_2 M}{M - 1}\frac{E_b}{N_0}}\right)\]

(Proakis & Salehi, Digital Communications, Sec. 4.3). The campaign is a declarative specification run on every core by the Rust runner: each point stops after 200 bit errors, and the sweep of a constellation stops once its BER falls below \(10^{-6}\). The error bars are the 95 % Wilson intervals.

import matplotlib.pyplot as plt
import numpy as np

import comnum
from comnum.plot import BACKGROUND, MUTED

results = comnum.run_spec(
    """
name: qam_awgn
seed: 2024
grid:
  m: [4, 16, 64]
  ebn0_db: {from: 0, to: 20, step: 1}
link:
  - {type: source, constellation: {kind: qam, m: $m}, n_symbols: 8192}
  - {type: awgn_ebn0, ebn0_db: $ebn0_db}
metrics: {report: [ber]}
stop: {min_errors: {ber: 200}, min_trials: 8, max_trials: 4000}
callbacks:
  skip_below: {metric: ber, threshold: 1e-6, along: ebn0_db}
"""
)

Simulated points with their intervals, and the closed form.

fig, ax = plt.subplots()
eb = np.linspace(0, 20, 201)
for m in [4, 16, 64]:
    # the conditions skipped below 1e-6 have no metric: series leaves them out
    x, ber, lo, hi = results.series("ebn0_db", "ber", interval=True, m=m)
    keep = ber > 0
    (line,) = ax.semilogy(eb, comnum.theory_ber(m, eb, "qam"), linewidth=1.4)
    ax.errorbar(
        x[keep],
        ber[keep],
        yerr=[ber[keep] - lo[keep], hi[keep] - ber[keep]],
        fmt="o",
        color=line.get_color(),
        markeredgecolor=BACKGROUND,
        capsize=0,
        label=f"{m}-QAM",
    )
ax.plot([], [], color=MUTED, linewidth=1.4, label="theory")
ax.set(
    ylim=(1e-6, 0.5),
    xlim=(0, 20),
    xlabel="$E_b/N_0$ (dB)",
    ylabel="bit error rate",
    title="Gray-mapped QAM over AWGN",
)
ax.legend()
fig.tight_layout()
Gray-mapped QAM over AWGN

The Eb/N0 needed for a BER of 1e-5 grows by 3.8 dB from QPSK to 16-QAM, and by 4.4 dB more to 64-QAM: the price of two more bits per symbol.

for m in [4, 16, 64]:
    lo, hi = 0.0, 30.0
    for _ in range(60):
        mid = (lo + hi) / 2
        lo, hi = (mid, hi) if comnum.theory_ber(m, mid, "qam") > 1e-5 else (lo, mid)
    print(f"{m:>2}-QAM: Eb/N0 = {lo:.2f} dB at a BER of 1e-5")
 4-QAM: Eb/N0 = 9.59 dB at a BER of 1e-5
16-QAM: Eb/N0 = 13.43 dB at a BER of 1e-5
64-QAM: Eb/N0 = 17.79 dB at a BER of 1e-5

Total running time of the script: (0 minutes 3.877 seconds)

Gallery generated by Sphinx-Gallery