Q15 vs UQ15 Quantization¶
What you're seeing¶
All three panels show the spectrum of the same full-scale complex tone (N=65536 samples, Blackman-Harris windowed) at each stage of quantization.
Top — Input CF32. The reference: a single tone at 0.07 cycles/sample, noise floor limited by the float32 compute path (~−150 dBFS).
Middle — Q15 bipolar roundtrip. F32 → int16 → F32 via
doppler.cvt.F32ToI16 / I16ToF32. Zero maps to the integer zero;
negative values use two's-complement. Noise floor rises to ~−92 dBFS
(Q15 theoretical SNR = 6.02 × 15 + 1.76 dB). The tone is otherwise
identical to the input.
Bottom — UQ15 offset-binary roundtrip. F32 → v_Q15 + 32768 → uint16 → F32. The +32768 shift (DC in the integer domain) cancels
exactly on decode; the noise floor and tone level are indistinguishable
from the Q15 panel. This is the encoding convention used by the CIC
decimator's internal integer pipeline.
Why two conventions?¶
Bipolar (Q15): natural for audio, DSP literature, signed arithmetic. Zero is the integer zero; sign-extension preserves the value in wider containers.
Offset-binary (UQ15/UQ16): required when all downstream arithmetic
must be unsigned. The CIC integrators use uint64_t accumulators
whose overflow behaviour is defined by C99 (mod 2⁶⁴), unlike signed
integers which overflow with undefined behaviour. Keeping all inputs
non-negative avoids sign-extension casts that are implementation-defined
in C99 ((int16_t)(uint16_t)v for v ≥ 32768).
Both conventions use the same quantization step Δ = 2⁻¹⁵, so the noise floor is identical — the choice is purely architectural.
import numpy as np
from doppler.cvt import F32ToI16, F32ToUQ15, I16ToF32, UQ15ToF32
# full-scale complex tone at 0.07 cycles/sample (the demo's signal)
x = np.exp(2j * np.pi * 0.07 * np.arange(65536)).astype(np.complex64)
# Q15 bipolar roundtrip
enc, dec = F32ToI16(), I16ToF32()
x_q15 = dec.steps(enc.steps(x.real)) # real channel only
# UQ15 offset-binary roundtrip: uint16, 0.0 -> 32768
u = F32ToUQ15().steps(x.real)
x_uq15 = UQ15ToF32().steps(u)
# the +32768 offset cancels on decode: the same quantizer, bit for bit
assert np.array_equal(x_q15, x_uq15)
pip install "doppler-dsp[examples]" # matplotlib + scipy, once
python -m doppler.examples.q15_uq15_demo # → q15_uq15_demo.png
See docs/design/QUANTIZATION.md for the
full mathematical model and C99 cast-chain analysis.
