Skip to content

Python SNR API

The doppler.snr module provides stateless SNR / Es-N0 estimators over a block of complex baseband samples, shared across any receiver that needs one rather than each object growing its own ad hoc formula. Two independent algorithms, each with a sliding-window _series sibling for visualizing drift vs time/index instead of reading one block-average scalar:

  • snr_data_aided_db — known-symbol estimator. Strip the known transmitted sign, then Es/N0 = (mean signal amplitude)^2 / (mean residual power). Needs ground truth (or trusted decisions), but is simple, unbiased, scale-invariant, and polarity-invariant (a global sign flip changes nothing, since the amplitude is squared).
  • snr_m2m4_db — non-data-aided (blind), moment-based estimator (Pauluzzi & Beaulieu, "A comparison of SNR estimation techniques for the AWGN channel", IEEE Trans. Commun. 48(10), 2000) for a constant-modulus signal (BPSK/QPSK/M-PSK) in circular complex AWGN. No known symbols needed at all.

Source: src/doppler/snr/__init__.py

See the 5-Burst DSSS Link gallery page for both estimators used against a real despread burst, including the _series sliding-window plot.

>>> import numpy as np
>>> from doppler.snr import snr_data_aided_db, snr_m2m4_db
>>> rng = np.random.default_rng(0)
>>> bits = (rng.random(5000) > 0.5).astype(np.uint8)
>>> sign = np.where(bits, -1.0, 1.0).astype(np.complex64)
>>> noise = (0.3 * (rng.standard_normal(5000)
...          + 1j * rng.standard_normal(5000))).astype(np.complex64)
>>> soft = (sign + noise).astype(np.complex64)
>>> round(float(snr_data_aided_db(soft, bits)), 1)  # known symbols
7.5
>>> round(float(snr_m2m4_db(soft)), 1)  # blind -- agrees, no `bits` needed
7.4

snr_data_aided_db — known-symbol Es/N0

snr_data_aided_db

snr_data_aided_db(
    soft: NDArray[complex64], sign_bits: NDArray[uint8]
) -> float

Data-aided Es/N0 (dB): strip the known sign, Es/N0 = a^2 / mean(|z-a|^2).

Strips the known transmitted sign (soft[i] * (sign_bits[i] ? -1 : 1)), then Es/N0 = (mean signal amplitude)^2 / (mean residual power). Scale-invariant (works regardless of the caller's symbol normalization) and polarity-invariant (a global sign flip in soft changes nothing, since the amplitude is squared) -- so it needs no resolution of an absolute-phase ambiguity a tracking loop may carry.

Parameters:

Name Type Description Default
soft NDArray[complex64]

Despread complex symbols.

required
sign_bits NDArray[uint8]

Known transmitted bits (0/1; 0 -> +1, 1 -> -1).

required

Returns:

Type Description
float

Es/N0 in dB over min(soft_len, sign_bits_len) paired samples, or NaN if that count is 0 or the residual power is exactly 0.

Examples:

>>> import numpy as np
>>> from doppler.snr import snr_data_aided_db
>>> rng = np.random.default_rng(0)
>>> bits = (rng.random(2000) > 0.5).astype(np.uint8)
>>> sign = np.where(bits, -1.0, 1.0).astype(np.complex64)
>>> noise = (0.1 * (rng.standard_normal(2000)
...          + 1j * rng.standard_normal(2000))).astype(np.complex64)
>>> soft = (sign + noise).astype(np.complex64)
>>> round(float(snr_data_aided_db(soft, bits)), 1)
17.1

snr_m2m4_db — blind (M2M4) Es/N0

snr_m2m4_db

snr_m2m4_db(x: NDArray[complex64]) -> float

Non-data-aided moment-based (M2M4) Es/N0 (dB) for a constant-modulus signal in AWGN.

M2M4 estimator (Pauluzzi & Beaulieu 2000) for a constant-modulus signal (BPSK/QPSK/M-PSK) in circular complex AWGN: no known symbols required.

Parameters:

Name Type Description Default
x NDArray[complex64]

Complex baseband samples (post-carrier-lock; residual phase does not bias the moment-based estimate).

required

Returns:

Type Description
float

Es/N0 in dB, 0-linear for pure noise, +inf for a noiseless constant-modulus signal, or NaN if x_len is 0 or the block has zero power.

Examples:

>>> import numpy as np
>>> from doppler.snr import snr_m2m4_db
>>> rng = np.random.default_rng(0)
>>> bits = (rng.random(2000) > 0.5).astype(np.uint8)
>>> sign = np.where(bits, -1.0, 1.0).astype(np.complex64)
>>> noise = (0.1 * (rng.standard_normal(2000)
...          + 1j * rng.standard_normal(2000))).astype(np.complex64)
>>> x = (sign + noise).astype(np.complex64)
>>> round(float(snr_m2m4_db(x)), 1)
17.1

snr_data_aided_db_series — sliding-window known-symbol Es/N0

snr_data_aided_db_series

snr_data_aided_db_series(
    soft: NDArray[complex64],
    sign_bits: NDArray[uint8],
    window: int,
) -> NDArray[np.float64]

Sliding-window data-aided Es/N0 (dB) vs index, for visualizing drift.

Same estimator as snr_data_aided_db(), applied to a [i - window/2, i + window/2] window centered (clamped at the edges) on each output index -- for visualizing SNR drift vs time/index rather than reading one block-average scalar.

Parameters:

Name Type Description Default
soft NDArray[complex64]

Despread complex symbols.

required
sign_bits NDArray[uint8]

Known transmitted bits (0/1).

required
window int

Window width in samples.

required

Returns:

Type Description
NDArray[float64]

Output.


snr_m2m4_db_series — sliding-window blind Es/N0

snr_m2m4_db_series

snr_m2m4_db_series(
    x: NDArray[complex64], window: int
) -> NDArray[np.float64]

Sliding-window blind (M2M4) Es/N0 (dB) vs index, for visualizing drift.

Same estimator as snr_m2m4_db(), applied to a [i - window/2, i + window/2] window centered (clamped at the edges) on each output index.

Parameters:

Name Type Description Default
x NDArray[complex64]

Complex baseband samples.

required
window int

Window width in samples.

required

Returns:

Type Description
NDArray[float64]

Output.

GalleryAsync DSSS Receiver: the SPEC waveform through coupled Doppler, A 5-Burst DSSS Link — wfmgen's Three Faces, the Full Receiver Chain, M-PSK Receiver — Pull-in, Lock, and BER