P25 Phase 1 demodulator calibration & measurement
This is the operator’s guide to the P25 Phase 1 demodulator measurement harness — the instrument that answers, with numbers, whether a weak decode is the demodulator leaving SNR on the table or the signal simply being marginal. Before this harness the only signal was “the audio sounds bad” and a binary locked / not-locked; now every stage reports EVM, an SNR estimate, a pre-FEC error rate, and an FSW sync-margin, and the demod can be benchmarked against both the theoretical limit and a second real decoder.
The harness has three legs. Each localizes the gap from a different direction.
1. Versus theory — the synthetic sweep
internal/radio/p25/phase1/receiver/sweep_test.go drives the real C4FM and
CQPSK receivers across a seeded SNR ladder and measures the recovered
symbol-error rate and SNR estimate at each step, comparing to the closed-form
references in internal/radio/p25/phase1/metrics (coherent QPSK / 4-PAM).
TestSweepImplementationLossBudgetis a hard CI gate: SER must fall monotonically with SNR, the Es/N0 needed to reach 1% SER must stay within a committed loss budget of theory, and the SNR estimator must track (CQPSK) or rise with (C4FM) the injected SNR. The budgets are regression ceilings calibrated from the measured baseline — re-run the test and read itsloss=log lines to re-derive them.
Run it ad hoc, with a human-readable curve, via the CLI:
gophertrunk siglab sweep # both paths, default 2–30 dB
gophertrunk siglab sweep -snr-min 6 -snr-max 20 -snr-step 1 -csv sweep.csv
Measured baseline at the time of writing: CQPSK sits ~3.85 dB off coherent QPSK at 1% SER (the ~2.3 dB π/4-DQPSK differential penalty plus ~1.5 dB implementation); C4FM sits ~24 dB off coherent 4-PAM — the FM-discriminator path is sharply noise-fragile and its soft-axis SNR estimate saturates near 20 dB. That C4FM gap is the headroom any future demod work would target.
2. Versus the field — real captures
Drop a control-channel capture into samples/p25/ (see samples/p25/README.md
for the .cfile + .metadata.json schema). The integration-tagged, skip-gated
TestReplayP25RealCaptureMetrics runs it through the production receiver and
reports the pre-FEC EVM, estimated SNR, FSW sync-margin distribution, and
NID/TSBK yields — asserting whichever max_evm_pct / min_snr_db /
min_sync_margin bounds the metadata declares.
go test -tags integration -run TestReplayP25RealCaptureMetrics -v ./cmd/gophertrunk/
The same EVM/SNR appear on every gophertrunk analyze / replay run (and in
the siglab web Compare tab) via the demod block of the result’s signal
quality, so you can read them off a live capture without writing a test.
3. Versus another decoder — OP25 / DSD-FME cross-check
TestP25ReferenceCrossCheck (integration-tagged, doubly skip-gated) runs an
external reference decoder on the same capture and diffs the decoded NAC sets
and frame yields, modelled on internal/voice/calibrate’s pass-bar. Combined
with leg 1 it localizes a gap: if GopherTrunk trails both theory and the
reference, the gap is GopherTrunk-specific; if GopherTrunk ≈ the reference and
both trail theory, the signal is inherently marginal.
Configure via environment and run:
export P25_REFERENCE_CMD='dsd-fme -i {} -fp -o /dev/null' # {} = capture path
export P25_REFERENCE_DEMOD=c4fm # or cqpsk
export P25_REFERENCE_MIN_AGREE=1.0 # NAC-set overlap bar
go test -tags integration -run TestP25ReferenceCrossCheck -v ./cmd/gophertrunk/
The comparison math (internal/radio/p25/phase1/refcompare) is pure and
unit-tested; only the binary invocation and output scraping live in the
skip-gated test, so the tool/version-specific brittleness never reaches CI.
What this harness is not
It does not re-architect the demodulator. It is the prerequisite that tells you whether — and precisely where — that is warranted. The large C4FM-vs-theory gap in leg 1 is the evidence such work would start from; the real-capture and reference legs say whether closing it would actually help a given site.