Skip to content

Python M-PSK Constellation API

The doppler.mpsk module is the M-ary PSK constellation layer over the C mpsk core: Gray-coded map / demap for BPSK, QPSK, and 8PSK. It is the decision primitive (and its transmit inverse) that the track.Carrier.Mpsk carrier loop and the MPSK receiver compose; the inline mpsk_slice() / mpsk_constellation() helpers in mpsk_core.h are the C composition API those loops inline per symbol.

A symbol carries log2(M) bits packed LSB-first into one uint8 (0..M−1); that byte is the Gray-coded label, so a slip to an adjacent constellation point flips exactly one bit. Constellations are unit amplitude: BPSK {+1, −1}, QPSK (±1 ± j)/√2 (axis-separable, at π/4), 8PSK exp(j·k·π/4). m defaults to QPSK and is keyword-capable.

See the M-PSK gallery page for the constellations and the BER-vs-Eb/N0 validation against theory.

Memoryless map / demap

mpsk_map and mpsk_demap are element-wise (one label byte ↔ one cf32 point), absolute-phase. mpsk_demap is a hard decision (nearest point by phase; amplitude-invariant).

mpsk_map

mpsk_map(sym: NDArray[uint8], m: int = 4) -> NDArray[np.complex64]

Map Gray-coded M-PSK labels to unit-amplitude constellation points.

Element-wise inverse of mpsk_demap(): each input byte is one symbol's log2(M) Gray-coded bits (0..M-1), each output is its cf32 point. Memoryless (absolute phase). out must hold sym_len points.

Parameters:

Name Type Description Default
sym NDArray[uint8]

Gray label bytes (0..M-1), one per symbol.

required
m int

M in {2,4,8}.

4

Returns:

Type Description
NDArray[complex64]

Output.

Examples:

>>> import numpy as np
>>> from doppler.mpsk import mpsk_map, mpsk_demap
>>> sym = np.array([0, 1, 2, 3], dtype=np.uint8)   # QPSK labels
>>> pts = mpsk_map(sym, 4)
>>> np.round(np.abs(pts), 5)
array([1., 1., 1., 1.], dtype=float32)
>>> np.array_equal(mpsk_demap(pts, 4), sym)
True

mpsk_demap

mpsk_demap(x: NDArray[complex64], m: int = 4) -> NDArray[np.uint8]

Hard-decide M-PSK symbols to their Gray-coded label bytes.

Element-wise inverse of mpsk_map(): each cf32 symbol is sliced to the nearest constellation point and its Gray label (0..M-1) is written out. A slip to an adjacent point flips exactly one bit (Gray). out must hold x_len bytes.

Parameters:

Name Type Description Default
x NDArray[complex64]

Received symbols (any amplitude; phase only).

required
m int

M in {2,4,8}.

4

Returns:

Type Description
NDArray[uint8]

Output.

Examples:

>>> import numpy as np
>>> from doppler.mpsk import mpsk_demap
>>> x = np.array([1+0j, 1j, -1+0j, -1j], dtype=np.complex64)   # 8PSK points
>>> mpsk_demap(x, 8).tolist()   # Gray labels of indices 0, 2, 4, 6
[0, 3, 6, 5]

Differential map / demap

The differential variants carry phase state across the array — information rides on phase differences, so an unknown constant carrier rotation cancels at the receiver. This resolves the M-fold phase ambiguity a decision-directed carrier loop leaves, at ~2× the symbol-error rate. Every symbol after the implicit zero-phase reference (the first) is rotation-invariant.

mpsk_diff_map

mpsk_diff_map(sym: NDArray[uint8], m: int = 4) -> NDArray[np.complex64]

Differential M-PSK map: the label selects a phase INCREMENT.

Information rides on phase differences: the running constellation index accumulates gray_decode(label) each symbol (starting from an implicit zero-phase reference), so an unknown constant carrier phase cancels at the receiver (mpsk_diff_demap) — resolving the M-fold ambiguity, at ~2x the symbol-error rate of coherent map(). Sequential over the array.

Parameters:

Name Type Description Default
sym NDArray[uint8]

Gray label bytes (0..M-1), one per symbol.

required
m int

M in {2,4,8}.

4

Returns:

Type Description
NDArray[complex64]

Output.

Examples:

>>> import numpy as np
>>> from doppler.mpsk import mpsk_diff_map, mpsk_diff_demap
>>> sym = np.array([1, 0, 3, 2, 1], dtype=np.uint8)
>>> pts = mpsk_diff_map(sym, 4)
>>> np.array_equal(mpsk_diff_demap(pts, 4), sym)   # exact round-trip
True
>>> rot = (pts * np.exp(1j * np.pi / 2)).astype(np.complex64)  # 90 deg slip
>>> np.array_equal(mpsk_diff_demap(rot, 4)[1:], sym[1:])   # rotation-invariant
True

mpsk_diff_demap

mpsk_diff_demap(x: NDArray[complex64], m: int = 4) -> NDArray[np.uint8]

Differential M-PSK demap: decide from the phase DIFFERENCE.

Inverse of mpsk_diff_map(): the Gray label of each symbol is decided from the phase difference between consecutive sliced indices (the first references an implicit zero-phase start). Invariant to an unknown constant carrier phase.

Parameters:

Name Type Description Default
x NDArray[complex64]

Received symbols (any amplitude; phase only).

required
m int

M in {2,4,8}.

4

Returns:

Type Description
NDArray[uint8]

Output.

Examples:

>>> import numpy as np
>>> from doppler.mpsk import mpsk_diff_demap, mpsk_diff_map
>>> sym = np.array([2, 2, 1, 0], dtype=np.uint8)
>>> np.array_equal(mpsk_diff_demap(mpsk_diff_map(sym, 8), 8), sym)
True

Helpers

mpsk_bits_per_symbol

mpsk_bits_per_symbol(m: int = 4) -> int

Bits per M-PSK symbol = log2(M).

Parameters:

Name Type Description Default
m int

M in {2,4,8}.

4

Returns:

Type Description
int

1, 2, or 3 (0 for an unsupported M).

Examples:

>>> from doppler.mpsk import mpsk_bits_per_symbol
>>> [mpsk_bits_per_symbol(m) for m in (2, 4, 8)]
[1, 2, 3]