Note
Go to the end to download the full example code.
OFDM synchronisation and MIMO-OFDM#
Timing and frequency. The preamble of Schmidl & Cox (IEEE Trans. Commun. 45(12), 1997) has two identical halves: their correlation peaks — on a plateau as long as the cyclic prefix — at the start of the symbol, and its phase gives the carrier frequency offset, here in units of the subcarrier spacing. The error variance of the offset falls as \(1/\mathrm{SNR}\).
MIMO-OFDM. With two transmit and two receive antennas over the EPA profile, every subcarrier carries a flat 2 × 2 MIMO channel; the detectors of the MIMO module run subcarrier by subcarrier (Stüber et al., Proc. IEEE 92(2), 2004). ML (here the sphere decoder) collects the receive diversity that the linear detectors lose.
import matplotlib.pyplot as plt
import numpy as np
from _gallery_utils import mimo_ofdm_ser
import comnum
from comnum.plot import AMBER, BACKGROUND, GREY
ofdm = comnum.Ofdm(128, 100, n_cp=16)
c = comnum.Constellation("qam", 4).normalized()
pre = comnum.ofdm_preamble(ofdm, seed=3)
rng = comnum.Rng(1)
data = ofdm.modulate(c.symbols(rng.integers(4, 2 * 100)))
r = np.concatenate([np.zeros(300), pre, data])
r = r + rng.complex_noise(r.size, 0.1)
metric = comnum.schmidl_cox(r, ofdm.fft_len // 2)
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(9.5, 3.8))
ax1.plot(metric, linewidth=1.4)
ax1.axvspan(300, 300 + 4 * 16, color=AMBER, alpha=0.25, label="cyclic prefix")
ax1.set(xlabel="sample", ylabel="M(d)", title="Schmidl & Cox timing metric, 10 dB", xlim=(0, 1100))
ax1.legend()
def cfo_rmse(snr_db, trials=200):
s2 = comnum.db2lin(-snr_db)
err = []
for t in range(trials):
g = comnum.Rng.stream(2, "cfo", int(snr_db + 100), t)
eps = g.uniform(1)[0] - 0.5
d = ofdm.modulate(c.symbols(g.integers(4, 100)))
x = np.concatenate([np.zeros(150), pre, d])
x = (x + g.complex_noise(x.size, s2)) * np.exp(
2j * np.pi * eps * np.arange(x.size) / ofdm.fft_len
)
err.append(comnum.ofdm_synchronize(x, ofdm)["cfo"] - eps)
return np.sqrt(np.mean(np.square(err)))
snrs = np.arange(-5.0, 26.0, 5.0)
ax2.semilogy(
snrs, [cfo_rmse(s) for s in snrs], marker="o", markeredgecolor=BACKGROUND, label="simulated"
)
ax2.semilogy(
snrs,
cfo_rmse(10.0) * 10 ** (-(snrs - 10) / 20),
color=GREY,
linestyle=":",
linewidth=1.2,
label=r"$\propto 1/\sqrt{\mathrm{SNR}}$",
)
ax2.set(
xlabel="SNR (dB)", ylabel="RMS error (subcarrier spacings)", title="frequency offset estimate"
)
ax2.legend()
fig.tight_layout()

2 × 2 MIMO-OFDM over EPA (1.92 MHz, 96 of 128 subcarriers), perfect CSI.
mofdm = comnum.Ofdm(128, 96, oversampling=1, n_cp=16)
ch = comnum.TdlChannel("epa", 1.92e6, n_rx=2, n_tx=2)
def ser(snr_db, method, frames=150):
"""Four OFDM symbols per frame, one channel per frame, per-subcarrier detection
(shared with the test that checks these curves)."""
return mimo_ofdm_ser(mofdm, ch, c, snr_db, method, frames, seed=3, condition=int(snr_db))
fig, ax = plt.subplots()
snr = np.arange(0.0, 31.0, 5.0)
for (name, m), marker in zip(
{"ZF": "zf", "MMSE": "mmse", "MMSE-SIC": "mmse_sic", "ML (sphere)": "sphere"}.items(),
["o", "s", "D", "^"],
strict=True,
):
v = np.array([ser(s, m) for s in snr])
keep = v > 0
ax.semilogy(snr[keep], v[keep], marker=marker, markeredgecolor=BACKGROUND, label=name)
ax.set(
xlabel=r"$E_s/N_0$ per receive antenna (dB)",
ylabel="symbol error rate",
title="2 × 2 MIMO-OFDM, QPSK, EPA",
)
ax.legend()
fig.tight_layout()

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