# Q15 vs UQ15 Quantization

![Q15 vs UQ15 spectrum comparison](../assets/q15_uq15_demo.png)

## 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.

```python
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
```

```bash
python examples/python/q15_uq15_demo.py   # → q15_uq15_demo.png
```

See [`docs/design/QUANTIZATION.md`](../design/QUANTIZATION.md) for the
full mathematical model and C99 cast-chain analysis.
