Phase-shift keying

import matplotlib.pyplot as plt
import numpy as np

import sdr

%config InlineBackend.print_figure_kwargs = {"facecolor" : "w"}
# %matplotlib widget

In the sdr library, phase-shift keying modulation is available in the sdr.PSK class.

def analyze_psk(psk, esn0):
    # Generate random decimal symbols
    s = np.random.randint(0, psk.order, 100_000)

    # Modulate decimal symbols to complex symbols
    x = psk.modulate(s)

    # Add AWGN to complex symbols to achieve desired Es/N0
    snr = sdr.esn0_to_snr(esn0, sps=1)
    x_hat = sdr.awgn(x, snr)

    plt.figure(figsize=(10, 5))
    plt.subplot(1, 2, 1)
    sdr.plot.symbol_map(psk.symbol_map, limits=(-2, 2))
    plt.subplot(1, 2, 2)
    sdr.plot.constellation(x_hat, bins=75, heatmap=True, limits=(-2, 2))
    plt.title(f"Constellation at {esn0} dB $E_s/N_0$")
    plt.suptitle(f"{psk.order}-PSK constellation")
    plt.tight_layout()
    plt.show()

    sps = 10
    h_srrc = sdr.root_raised_cosine(0.1, 6, sps)
    tx_mf = sdr.FIRInterpolator(h_srrc, sps)
    y = tx_mf.filter(x)

    plt.figure(figsize=(10, 5))
    sdr.plot.time_domain(y[0:1000])
    plt.show()

Constellations

BPSK

bpsk = sdr.PSK(2)
analyze_psk(bpsk, 6)
../../_images/8ba848959f71943d93a28ccd35dc56727d35ea54eadea537a59bbfd73f7a44fe.png ../../_images/02bb491f10a0251b6071b77f1af3e6287d29ad684d9e1a82c2fa13fcf71b6f82.png

QPSK

qpsk = sdr.PSK(4, phase_offset=45)
analyze_psk(qpsk, 9)
../../_images/f6de3f58b668c46d9c1efc67ad249f773e965670a88c53095c596cab3036e5bc.png ../../_images/b7d88e236f223889d07c49076c06566595886314a91782e617d6344811654d2f.png

8-PSK

psk8 = sdr.PSK(8)
analyze_psk(psk8, 12)
../../_images/24204997ca6e0acdd9ed659a1bf2292e5bb6dbf5d65ff4187e2fed8371d09b5e.png ../../_images/452c632c7aab04bcca68837f670dff49bc32f095fb2a776c1238a306f556020f.png

16-PSK

psk16 = sdr.PSK(16)
analyze_psk(psk16, 18)
../../_images/b56665db329897efc7bf3071011507a424775a9274a08eb5e7d1350d71251c0d.png ../../_images/d24eaea73f0c5fb83effe187507d883a6141765735ef74a2b6520137cd8b49e4.png

Error rate curves

def error_rates(psk, ebn0):
    esn0 = sdr.ebn0_to_esn0(ebn0, psk.bps)
    snr = sdr.esn0_to_snr(esn0)

    ber = sdr.ErrorRate()
    ser = sdr.ErrorRate()

    for i in range(snr.size):
        s = np.random.randint(0, psk.order, int(1e6))
        x = psk.modulate(s)
        x_hat = sdr.awgn(x, snr[i])
        s_hat = psk.demodulate(x_hat)

        ber.add(ebn0[i], sdr.unpack(s, psk.bps), sdr.unpack(s_hat, psk.bps))
        ser.add(esn0[i], s, s_hat)

    return ber, ser
ebn0 = np.linspace(-2, 10, 20)

bpsk_ber, bpsk_ser = error_rates(bpsk, ebn0)
qpsk_ber, qpsk_ser = error_rates(qpsk, ebn0)
psk8_ber, psk8_ser = error_rates(psk8, ebn0)
psk16_ber, psk16_ser = error_rates(psk16, ebn0)

Bit error rate curves

plt.figure(figsize=(10, 5))
ebn0 = np.linspace(-2, 10, 200)
sdr.plot.ber(ebn0, bpsk.ber(ebn0), label="BPSK theoretical")
sdr.plot.ber(ebn0, qpsk.ber(ebn0), label="QPSK theoretical")
sdr.plot.ber(ebn0, psk8.ber(ebn0), label="8-PSK theoretical")
sdr.plot.ber(ebn0, psk16.ber(ebn0), label="16-PSK theoretical")
plt.gca().set_prop_cycle(None)
sdr.plot.ber(*bpsk_ber.error_rates(), linestyle="none", marker=".", label="BPSK simulated")
sdr.plot.ber(*qpsk_ber.error_rates(), linestyle="none", marker=".", label="QPSK simulated")
sdr.plot.ber(*psk8_ber.error_rates(), linestyle="none", marker=".", label="8-PSK simulated")
sdr.plot.ber(*psk16_ber.error_rates(), linestyle="none", marker=".", label="16-PSK simulated")
plt.ylim(1e-6, 1e0)
plt.title("Bit error rate curves for PSK modulation in AWGN")
plt.tight_layout()
plt.show()
../../_images/b627127a36bd9e883bdf11c7dded2e78b59f5364419741474b106c334337536f.png

Symbol error rate curves

plt.figure(figsize=(10, 5))
esn0 = np.linspace(-4, 16, 200)
sdr.plot.ser(esn0, bpsk.ser(esn0), label="BPSK theoretical")
sdr.plot.ser(esn0, qpsk.ser(esn0), label="QPSK theoretical")
sdr.plot.ser(esn0, psk8.ser(esn0), label="8-PSK theoretical")
sdr.plot.ser(esn0, psk16.ser(esn0), label="16-PSK theoretical")
plt.gca().set_prop_cycle(None)
sdr.plot.ser(*bpsk_ser.error_rates(), linestyle="none", marker=".", label="BPSK simulated")
sdr.plot.ser(*qpsk_ser.error_rates(), linestyle="none", marker=".", label="QPSK simulated")
sdr.plot.ser(*psk8_ser.error_rates(), linestyle="none", marker=".", label="8-PSK simulated")
sdr.plot.ser(*psk16_ser.error_rates(), linestyle="none", marker=".", label="16-PSK simulated")
plt.ylim(1e-6, 1e0)
plt.title("Symbol error rate curves for PSK modulation in AWGN")
plt.tight_layout()
plt.show()
../../_images/6fcf63e6afeef7e227f834cd291a65480959c84c3b60d40308df5a119f88dd27.png

Symbol mapping

The mapping of decimal symbols to complex symbols is important. Ideally, adjacent symbol errors only result in 1 bit error. This is generally accomplished using a Gray code (the default in :class:sdr.PSK).

psk8_bin = sdr.PSK(8, symbol_labels="bin")
psk8_gray = sdr.PSK(8, symbol_labels="gray")
plt.figure(figsize=(10, 5))
plt.subplot(1, 2, 1)
sdr.plot.symbol_map(psk8_bin.symbol_map, limits=(-2, 2))
plt.title(f"Symbol map for binary coded 8-PSK")
plt.subplot(1, 2, 2)
sdr.plot.symbol_map(psk8_gray.symbol_map, limits=(-2, 2))
plt.title(f"Symbol map for gray coded 8-PSK")
plt.tight_layout()
plt.show()
../../_images/772721577c2900d4abbe3ca510758d8997ac16c059e0c178a41f661bc2a64968.png

Since adjacent symbol errors can lead to multiple bit errors, binary-coded 8-PSK has worse bit error performance.

plt.figure(figsize=(10, 5))
ebn0 = np.linspace(-2, 10, 200)
sdr.plot.ber(ebn0, sdr.PSK(8, symbol_labels="bin").ber(ebn0), label="8-PSK w/ binary code")
sdr.plot.ber(ebn0, sdr.PSK(8, symbol_labels="gray").ber(ebn0), label="8-PSK w/ gray code")
plt.title("Bit error rate curves for 8-PSK modulation in AWGN")
plt.tight_layout()
plt.show()
../../_images/ad609c142343c13cc06d28baf02d21688bb47faf8321a32a71f47d4fdd3f7123.png

Last update: Aug 06, 2023