Cahn’s M-ary phase-shift keying (1959)#

Charles Cahn asked what happens when a carrier’s phase takes not two but \(M\) values, so that each symbol carries \(\log_2 M\) bits. The points sit on a circle (psk_constellation()) and crowd together as \(M\) grows: the distance between neighbours is \(2\sin(\pi/M)\). Every doubling of \(M\) beyond four costs about 6 dB of \(E_s/N_0\). Cahn concluded that multiphase signalling trades bandwidth for signal-to-noise ratio more efficiently than multilevel amplitude signalling. The exact symbol-error rate of minimum-distance detection (minimum_distance_detect()) is given by Craig’s integral (ser_mpsk_awgn()).

import matplotlib.pyplot as plt
import numpy as np

from engineeringkit.communications import from_db, minimum_distance_detect, psk_constellation, ser_mpsk_awgn
from engineeringkit.communications.visualizers import plot_constellation

Symbol errors by simulation and by Craig’s integral#

rng = np.random.default_rng(1959)
ebn0_db = np.arange(0, 21, 2.0)
fig, axes = plt.subplots(1, 2, figsize=(11, 4.2))
for M in (2, 4, 8, 16):
    points = psk_constellation(M)
    esn0 = from_db(ebn0_db) * np.log2(M)
    theory = ser_mpsk_awgn(esn0, M)
    axes[1].semilogy(ebn0_db, theory, label=f"{M}-PSK")
    n = 40_000
    labels = rng.integers(0, M, n)
    measured = []
    for g in esn0:
        noise = (rng.standard_normal(n) + 1j * rng.standard_normal(n)) * np.sqrt(1 / (2 * g))
        measured.append(np.mean(minimum_distance_detect(points[labels] + noise, points) != labels))
    measured = np.array(measured)
    axes[1].semilogy(ebn0_db[measured > 0], measured[measured > 0], "k.", ms=4)
    reliable = theory > 1e-2
    assert np.allclose(measured[reliable], theory[reliable], rtol=0.1)
    print(f"{M:2d}-PSK: SER at 10 dB Eb/N0 = {ser_mpsk_awgn(from_db(10.0) * np.log2(M), M):.2e}")
axes[1].set(xlabel="$E_b/N_0$ [dB]", ylabel="symbol-error rate", ylim=(1e-5, 1), title="M-PSK in AWGN (dots: simulation)")
axes[1].grid(True, which="both", alpha=0.3)
axes[1].legend()

points = psk_constellation(8)
noisy = points[rng.integers(0, 8, 2000)] + (rng.standard_normal(2000) + 1j * rng.standard_normal(2000)) * 0.12
plot_constellation(noisy, ax=axes[0])
for label, point in enumerate(points):
    axes[0].annotate(f"{label:03b}", (1.25 * point.real, 1.25 * point.imag), ha="center", va="center")
axes[0].set(title="8-PSK with Gray labels", xlim=(-1.5, 1.5), ylim=(-1.5, 1.5))
fig.tight_layout()
8-PSK with Gray labels, M-PSK in AWGN (dots: simulation)
 2-PSK: SER at 10 dB Eb/N0 = 3.87e-06
 4-PSK: SER at 10 dB Eb/N0 = 7.74e-06
 8-PSK: SER at 10 dB Eb/N0 = 3.03e-03
16-PSK: SER at 10 dB Eb/N0 = 8.10e-02

Neighbouring phases differ in one bit, so a symbol error usually costs a single bit, and the bit-error rate is close to \(P_s/\log_2 M\).

distance = np.abs(points[:, None] - points[None, :])
nearest = np.isclose(distance, 2 * np.sin(np.pi / 8))
assert all(bin(i ^ j).count("1") == 1 for i, j in zip(*np.nonzero(nearest)))

plt.show()

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

Gallery generated by Sphinx-Gallery