class sdr.PiMPSK(sdr.PSK)

Implements \(\pi/M\) phase-shift keying (\(\pi/M\) PSK) modulation and demodulation.

Notes

\(\pi/M\) M-PSK is a linear phase modulation scheme similar to conventional M-PSK. One key distinction is that in \(\pi/M\) M-PSK, the odd symbols are rotated by \(\pi/M\) radians relative to the even symbols. This prevents symbol transitions through the origin, which results in a lower peak-to-average power ratio (PAPR).

The modulation order \(M = 2^k\) is a power of 2 and indicates the number of phases used \(2M\). The input bit stream is taken \(k\) bits at a time to create decimal symbols \(s[k] \in \{0, \dots, M-1\}\). These decimal symbols \(s[k]\) are then mapped to complex symbols \(a[k] \in \mathbb{C}\) by the equation

\[\begin{split}a[k] = \begin{cases} \displaystyle \exp \left[ j\left(\frac{2\pi}{M}s[k] + \phi\right) \right] & k\ \text{even} \\ \displaystyle \exp \left[ j\left(\frac{2\pi}{M}s[k] + \phi + \pi/M\right) \right] & k\ \text{odd} \\ \end{cases} \end{split}\]

Variable nomenclature
  • \(k\): Symbol index

  • \(n\): Sample index

  • \(s[k]\): Decimal symbols

  • \(a[k]\) Complex symbols

  • \(x[n]\): Pulse-shaped complex samples

  • \(\tilde{x}[n]\): Received (noisy) pulse-shaped complex samples

  • \(\tilde{a}[k]\): Received (noisy) complex symbols

  • \(\hat{a}[k]\): Complex symbol decisions

  • \(\hat{s}[k]\): Decimal symbol decisions

Examples

Create a \(\pi/4\) QPSK modem.

In [1]: pi4_qpsk = sdr.PiMPSK(4, pulse_shape="srrc"); pi4_qpsk
Out[1]: sdr.PiMPSK(4, phase_offset=0.0, symbol_labels='gray')

In [2]: plt.figure(); \
   ...: sdr.plot.symbol_map(pi4_qpsk);
   ...: 
../../_images/sdr_PiMPSK_1.png

Generate a random bit stream, convert to 2-bit symbols, and map to complex symbols.

In [3]: bits = np.random.randint(0, 2, 1000); bits[0:8]
Out[3]: array([0, 1, 1, 0, 1, 1, 1, 1])

In [4]: symbols = sdr.pack(bits, pi4_qpsk.bps); symbols[0:4]
Out[4]: array([1, 2, 3, 3], dtype=uint8)

In [5]: complex_symbols = pi4_qpsk.map_symbols(symbols); complex_symbols[0:4]
Out[5]: 
array([ 6.12323400e-17+1.00000000e+00j,  7.07106781e-01-7.07106781e-01j,
       -1.00000000e+00+1.22464680e-16j, -7.07106781e-01-7.07106781e-01j])

In [6]: plt.figure(); \
   ...: sdr.plot.constellation(complex_symbols, linestyle="-");
   ...: 
../../_images/sdr_PiMPSK_2.png

Modulate and pulse shape the symbols to a complex baseband signal.

In [7]: tx_samples = pi4_qpsk.modulate(symbols)

In [8]: plt.figure(); \
   ...: sdr.plot.time_domain(tx_samples[0:50*pi4_qpsk.sps]);
   ...: 
../../_images/sdr_PiMPSK_3.png

Examine the eye diagram of the pulse-shaped transmitted signal. The SRRC pulse shape is not a Nyquist filter, so ISI is present.

In [9]: plt.figure(figsize=(8, 6)); \
   ...: sdr.plot.eye(tx_samples[5*pi4_qpsk.sps : -5*pi4_qpsk.sps], pi4_qpsk.sps, persistence=True); \
   ...: plt.suptitle("Noiseless transmitted signal with ISI");
   ...: 
../../_images/sdr_PiMPSK_4.png

Add AWGN noise such that \(E_b/N_0 = 30\) dB.

In [10]: ebn0 = 30; \
   ....: snr = sdr.ebn0_to_snr(ebn0, bps=pi4_qpsk.bps, sps=pi4_qpsk.sps); \
   ....: rx_samples = sdr.awgn(tx_samples, snr=snr)
   ....: 

In [11]: plt.figure(); \
   ....: sdr.plot.time_domain(rx_samples[0:50*pi4_qpsk.sps]);
   ....: 
../../_images/sdr_PiMPSK_5.png

Manually apply a matched filter. Examine the eye diagram of the matched filtered received signal. The two cascaded SRRC filters create a Nyquist RC filter. Therefore, the ISI is removed.

In [12]: mf = sdr.FIR(pi4_qpsk.pulse_shape); \
   ....: mf_samples = mf(rx_samples)
   ....: 

In [13]: plt.figure(figsize=(8, 6)); \
   ....: sdr.plot.eye(mf_samples[10*pi4_qpsk.sps : -10*pi4_qpsk.sps], pi4_qpsk.sps, persistence=True); \
   ....: plt.suptitle("Noisy received and matched filtered signal without ISI");
   ....: 
../../_images/sdr_PiMPSK_6.png

Matched filter and demodulate.

In [14]: rx_symbols, rx_complex_symbols, _ = pi4_qpsk.demodulate(rx_samples)

# The symbol decisions are error-free
In [15]: np.array_equal(symbols, rx_symbols)
Out[15]: True

In [16]: plt.figure(); \
   ....: sdr.plot.constellation(rx_complex_symbols);
   ....: 
../../_images/sdr_PiMPSK_7.png

See the Phase-shift keying example.

Constructors

PiMPSK(order: int, phase_offset: float = 0.0, ...)

Creates a new \(\pi/M\) PSK object.

Methods

ber(ebn0: ArrayLike, diff_encoded: bool = False) NDArray[float64]

Computes the bit error rate (BER) at the provided \(E_b/N_0\) values.

ser(esn0: ArrayLike, diff_encoded: bool = False) NDArray[float64]

Computes the symbol error rate (SER) at the provided \(E_s/N_0\) values.

map_symbols(s: ArrayLike) NDArray[complex128]

Converts the decimal symbols into complex symbols.

decide_symbols(a_tilde) tuple[NDArray[int_], NDArray[complex128]]

Converts the received complex symbols into MLE symbol decisions.

modulate(s: ArrayLike) NDArray[complex128]

Modulates the decimal symbols into pulse-shaped complex samples.

demodulate(...) tuple[NDArray[int_], NDArray[complex128], NDArray[complex128]]

Demodulates the pulse-shaped complex samples.

Properties

property phase_offset : float

The phase offset \(\phi\) in degrees.

property symbol_map : NDArray[np.complex128]

The symbol map \(\{0, \dots, M-1\} \mapsto \mathbb{C}\).

property order : int

The modulation order \(M = 2^k\).

property bps : int

The number of coded bits per symbol \(k = \log_2 M\).

property sps : int

The number of samples per symbol \(f_s / f_{sym}\).

property pulse_shape : NDArray[np.float64]

The pulse shape \(h[n]\) of the modulated signal.

property tx_filter : Interpolator

The transmit interpolating pulse shaping filter.

property rx_filter : Decimator

The receive decimating matched filter.