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.

Note

The nomenclature for variable names in linear modulators is as follows: \(s[k]\) are decimal symbols, \(\hat{s}[k]\) are decimal symbol decisions, \(a[k]\) are complex symbols, \(\tilde{a}[k]\) are received complex symbols, \(\hat{a}[k]\) are complex symbol decisions, \(x[n]\) are pulse-shaped complex samples, and \(\tilde{x}[n]\) are received pulse-shaped complex samples. \(k\) indicates a symbol index and \(n\) indicates a sample index.

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(); \
   ...: 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(); \
   ...: 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(); \
   ...: sdr.plot.time_domain(tx_samples[0:50*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)); \
   ...: sdr.plot.eye(tx_samples[5*msk.sps : -5*msk.sps], msk.sps); \
   ...: plt.suptitle("Noiseless transmitted signal");
   ...: 
../../_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(); \
   ....: sdr.plot.phase_tree(tx_samples[msk.sps:], msk.sps);
   ....: 
../../_images/sdr_MSK_5.png

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

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

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

Manually apply a matched filter. Examine the eye diagram of the matched filtered received signal.

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

In [14]: plt.figure(figsize=(8, 6)); \
   ....: sdr.plot.eye(mf_samples[10*msk.sps : -10*msk.sps], msk.sps); \
   ....: plt.suptitle("Noisy received and matched filtered signal");
   ....: 
../../_images/sdr_MSK_7.png

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

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

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

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

See the Phase-shift keying example.

Constructors

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

Creates a new MSK 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 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.