Skip to content

Q15 vs UQ15 Quantization

Q15 vs UQ15 spectrum comparison

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.

from doppler.cvt import F32ToI16, I16ToF32
import numpy as np

# Q15 bipolar roundtrip
enc, dec = F32ToI16(), I16ToF32()
x_q15 = dec.steps(enc.steps(x.real))   # real channel only

# UQ15 offset-binary roundtrip (numpy — no cvt UQ15 type yet)
v     = np.clip(np.round(x.real * 32768.0), -32768, 32767).astype(np.int16)
u     = (v.astype(np.int32) + 32768).astype(np.uint16)
x_uq15 = (u.astype(np.float32) - 32768.0) / 32768.0
python examples/python/q15_uq15_demo.py   # → q15_uq15_demo.png

See docs/design/QUANTIZATION.md for the full mathematical model and C99 cast-chain analysis.