class sdr.MSK(sdr.OQPSK)

Implements minimum-shift keying (MSK) modulation and demodulation.

Notes

MSK is a linear phase modulation scheme similar to OQPSK. One key distinction is that the pulse shape is a half sine wave. This results in a constant envelope signal, which results in a lower peak-to-average power ratio (PAPR).

MSK can also be consider as continuous-phase frequency-shift keying (CPFSK) with the frequency separation equaling half the bit period.

Examples

Create a MSK modem.

In [1]: msk = sdr.MSK(); msk
Out[1]: sdr.MSK(phase_offset=45, symbol_labels='gray')

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

In [5]: complex_symbols = msk.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_MSK_2.png

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

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

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

MSK, like OQPSK, has I and Q channels that are offset by half a symbol period.

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

The phase trajectory of MSK is linear and continuous. Although, it should be noted that the phase is not differentiable at the symbol boundaries. This leads to lower spectral efficiency than, for instance, GMSK.

In [10]: plt.figure(figsize=(8, 4)); \
   ....: sdr.plot.phase_tree(tx_samples[msk.sps:], msk.sps);
   ....: 
../../_images/sdr_MSK_5.png

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

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

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

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

In [13]: rx_symbols, rx_complex_symbols = msk.demodulate(rx_samples)

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

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

See the Phase-shift keying example.

Constructors

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

Creates a new MSK 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]^*\).