Levels & SNR¶
Amplitude & full-scale¶
The amplitude invariant is unit average power: every waveform is normalised
so its mean power is 1.0. That — not a constant envelope — is what the rest
of the system is built on. It is the reference the SNR math uses (signal power
≡ 1, so the noise σ falls straight out of the target SNR), and the level you
control is the SNR, not a signal gain. The I/Q full-scale is ±1.0 per axis
(→ the largest integer code).
Today's built-in types all happen to be constant-envelope, so for them the peak equals the average and they sit exactly at ±1.0 — but that is a property of the current set, not a design assumption:
--type |
Sample values | Envelope | Avg. power |
|---|---|---|---|
tone |
exp(j·2πft) |
constant, mag 1 | 1.0 |
bpsk / pn |
±1 (real axis) |
constant, mag 1 | 1.0 |
qpsk |
(±1/√2, ±1/√2) |
constant, mag 1 | 1.0 |
chirp |
exp(j·φ(t)), φ′ ramps freq→f_end |
constant, mag 1 | 1.0 |
noise |
complex Gaussian, σ = 1/√2 per axis |
Gaussian, PAPR > 0 | 1.0 |
symbols |
whatever you supply (e.g. 16-QAM) | user-defined | user-defined |
Don't rely on |z| = 1. A pulse-shaped (RRC), QAM, or OFDM waveform has a
peak-to-average power ratio (PAPR) above 0 dB: at unit average power its
peaks run well past ±1.0. noise, and any signal-plus-noise sum, already do.
Scaling to the wire, and headroom¶
cf32 / cf64 carry samples verbatim and never clip — peaks past ±1.0 are
preserved. The integer types map ±1.0 → ±max-code by saturating each axis
to ±1.0, then truncating toward zero (a plain cast, not round-to-nearest):
--sample-type |
Map | Full-scale code |
|---|---|---|
ci32 |
clip(v, ±1)·(2³¹−1) |
±2 147 483 647 |
ci16 |
clip(v, ±1)·32767 |
±32 767 |
ci8 |
clip(v, ±1)·127 |
±127 |
So clipping is governed by PAPR, not by something being "signal" vs "noise":
- A constant-envelope, clean signal (a tone/PSK/PN at
--snr 100) fills the integer range exactly, with no clipping. - Any PAPR > 0 dB content clips at the rails — added noise (at
--snr 0, noise power = signal power, ~⅓ of integer I/Q components already saturate) and any pulse-shaped / QAM / OFDM mode. Such a signal needs headroom:--headroom <dB>(andWriter(headroom=…)in Python) scales the whole output down to−HdBFS so the peaks fit. It is a single common gain, so it is SNR-invariant — it moves only the absolute level, not any power ratio — and0dB (the default) is a bit-exact no-op. An integer capture that clips reports the exact backoff to use (remedy: --headroom N). You can also just carry envelope-varying signals as a float type (cf32/cf64), which never clips.
Reader (see Output & containers) inverts the same map, so a float
round-trip is exact and an integer round-trip is exact only where it neither
clipped nor truncated.
>>> import numpy as np
>>> from doppler.wfm import Synth
>>> # the invariant is unit *average* power (here a clean, constant-envelope QPSK)
>>> x = Synth(type="qpsk", sps=1, snr=100.0).steps(4096)
>>> bool(np.allclose(np.mean(np.abs(x) ** 2), 1.0))
True
>>> # add noise (or pulse-shaping / QAM) and peaks exceed full-scale:
>>> y = Synth(type="qpsk", sps=1, snr=0.0).steps(100000)
>>> float(np.mean(np.abs(y.real) > 1.0)) > 0.1 # many samples clip in ci*
True
SNR & noise¶
--snr is applied as AWGN; --snr-mode chooses the reference:
| Mode | --snr means |
Use for |
|---|---|---|
fs |
SNR over the full sample rate (in-band power / noise power) | tones, wideband |
esno |
Es/No — energy per symbol over noise PSD | modulated (*psk) |
ebno |
Eb/No — energy per bit over noise PSD | link-budget work |
auto |
fs for tone/noise/pn, esno for bpsk/qpsk |
the sensible default |
--snr 100 (the default) is clean — snr ≥ 100 dB generates no AWGN at
all, so a clean waveform pays no noise cost. Lower --snr to add noise; the
signal stays at unit average power, so the per-axis noise σ is
σ = sqrt(1 / (2·10^(snr_fs/10))), where Es/No and Eb/No are first converted to
an over-fs SNR using 10·log10(sps) (and, for Eb/No, the bits/symbol: 1 for
BPSK/PN, 2 for QPSK). (--type noise always generates AWGN.) Likewise
--freq 0 skips the LO — the carrier is a constant 1 — so a clean baseband
waveform is pure signal generation.