class sdr.OQPSK(sdr.PSK)

Implements offset quadrature phase-shift keying (OQPSK) modulation and demodulation.

Notes

Offset QPSK is a linear phase modulation scheme similar to conventional QPSK. One key distinction is that the I and Q channels transition independently, one half symbol apart. 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. 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

\[I[k] + jQ[k] = \exp \left[ j\left(\frac{2\pi}{M}s[k] + \phi\right) \right]\]

\[\begin{split} \begin{align} a[k + 0] &= I[k] + jQ[k - 1] \\ a[k + 1/2] &= I[k] + jQ[k] \\ a[k + 1] &= I[k + 1] + jQ[k] \\ a[k + 3/2] &= I[k + 1] + jQ[k + 1] \\ \end{align} \end{split}\]

Examples

Create a OQPSK modem.

In [1]: oqpsk = sdr.OQPSK(pulse_shape="srrc"); oqpsk
Out[1]: sdr.OQPSK(phase_offset=45, symbol_labels='gray')

In [2]: plt.figure(figsize=(8, 4)); \
   ...: sdr.plot.symbol_map(oqpsk);
   ...: 
../../_images/sdr_OQPSK_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, oqpsk.bps); symbols[0:4]
Out[4]: array([1, 2, 3, 3], dtype=uint8)

In [5]: complex_symbols = oqpsk.map_symbols(symbols); complex_symbols[0:4]
Out[5]: 
array([-0.70710678+0.j        , -0.70710678+0.70710678j,
        0.70710678+0.70710678j,  0.70710678-0.70710678j])

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

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

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

In [8]: plt.figure(figsize=(8, 4)); \
   ...: sdr.plot.time_domain(tx_samples[0:50*oqpsk.sps], sample_rate=oqpsk.sps);
   ...: 

In [9]: plt.figure(figsize=(8, 6)); \
   ...: plt.subplot(2, 1, 1); \
   ...: sdr.plot.eye(tx_samples[5*oqpsk.sps : -5*oqpsk.sps].real, oqpsk.sps); \
   ...: plt.title("In-phase channel, $I$"); \
   ...: plt.subplot(2, 1, 2); \
   ...: sdr.plot.eye(tx_samples[5*oqpsk.sps : -5*oqpsk.sps].imag, oqpsk.sps); \
   ...: plt.title("Quadrature channel, $Q$"); \
   ...: plt.tight_layout();
   ...: 
../../_images/sdr_OQPSK_3.png ../../_images/sdr_OQPSK_4.png

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

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

In [11]: plt.figure(figsize=(8, 4)); \
   ....: sdr.plot.time_domain(rx_samples[0:50*oqpsk.sps], sample_rate=oqpsk.sps);
   ....: 
../../_images/sdr_OQPSK_5.png

Matched filter and demodulate. Note, the first symbol has \(Q = 0\) and the last symbol has \(I = 0\).

In [12]: rx_symbols, rx_complex_symbols = oqpsk.demodulate(rx_samples)

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

In [14]: plt.figure(figsize=(8, 4)); \
   ....: sdr.plot.constellation(rx_complex_symbols);
   ....: 
../../_images/sdr_OQPSK_6.png

See the Phase-shift keying example.

Constructors

OQPSK(phase_offset: float = 45, ...)

Creates a new OQPSK object.

String representation

__repr__() str

Returns a code-styled string representation of the object.

__str__() str

Returns a human-readable string representation of the object.

Methods

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

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

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

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

map_symbols(s: ArrayLike) NDArray[complex_]

Converts the decimal symbols \(s[k]\) to complex symbols \(a[k]\).

decide_symbols(a_hat: ArrayLike) NDArray[int_]

Converts the received complex symbols \(\hat{a}[k]\) into decimal symbol decisions \(\hat{s}[k]\) using maximum-likelihood estimation (MLE).

modulate(s: ArrayLike) NDArray[complex_]

Modulates the decimal symbols \(s[k]\) into pulse-shaped complex samples \(x[n]\).

demodulate(x_hat) tuple[NDArray[int_], NDArray[complex_]]

Demodulates the pulse-shaped complex samples \(\hat{x}[n]\) into decimal symbol decisions \(\hat{s}[k]\) using matched filtering and maximum-likelihood estimation.

Properties

property phase_offset : float

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

property symbol_map : NDArray[np.complex_]

The symbol map \(\{0, \dots, M-1\} \mapsto \mathbb{C}\). This maps decimal symbols from \(0\) to \(M-1\) to complex symbols.

property order : int

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

property bps : int

The number of 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.float_]

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

property tx_filter : Interpolator

The transmit interpolating pulse shaping filter. The filter coefficients are the pulse shape \(h[n]\).

property rx_filter : Decimator

The receive decimating matched filter. The filter coefficients are matched to the pulse shape \(h[-n]^*\).