Part 10 of Signal Lab, a 10-part series on GopherTrunk’s offline signal-analysis workbench. We close where the workbench earns its keep — a repeatable demod benchmark that answers “did my change help?” with a curve.
TL;DR:
gophertrunk siglab sweepsynthesizes P25 Phase 1 across an SNR ladder on both demod paths (c4fmandcqpsk/lsm), decodes each rung through the production pipeline, and prints measured demod quality — lock, EVM, estimated SNR — against a theoretical symbol-error-rate reference. Flags set the ladder (-snr-min,-snr-max,-snr-step), the AWGN-seed, and an optional-csvexport. It’s the regression instrument that turns a demod tweak from a hunch into evidence.
Key takeaways
- The sweep is a curve, not a single test. It measures across an SNR ladder, so you see where a demod holds and where it falls apart.
- Both paths, every run.
c4fmandcqpsk/lsmare swept together against their own theoretical references. - Measured vs theory is the read. Tracking theory down to low SNR is good; an early divergence is a demod deficiency.
- Reproducible by seed. A fixed
-seedmakes the ladder a valid regression fixture. -csvexports the whole sweep for diffing across code changes.
Cheat sheet
| Command / flag | What it does |
|---|---|
gophertrunk siglab sweep |
Sweep both paths, 2–30 dB |
-snr-min <dB> |
Minimum injected SNR (default 2) |
-snr-max <dB> |
Maximum injected SNR (default 30) |
-snr-step <dB> |
SNR step (default 2) |
-seed <n> |
AWGN seed (default 0x5175) |
-csv <path> |
Also write the sweep as CSV |
In this post
- What the sweep measures — quality across an SNR ladder.
- Reading the curve — measured against theory.
- Both demod paths — c4fm and cqpsk/lsm side by side.
- The CSV — columns and regression workflow.
- Series wrap — where the Lab Bench trilogy goes from here.
What the sweep measures
Every earlier part measured one capture. The demod benchmark measures the demodulator itself — how well it performs as conditions get worse. It synthesizes P25 Phase 1 at a ladder of injected SNRs, decodes each rung through the exact production pipeline, and records the measured quality at each level. The result is a curve, and a curve says something a single pass can’t: not just “does it lock?” but “down to what SNR does it lock, and how gracefully does it degrade on the way down?”
gophertrunk siglab sweep # both paths, 2–30 dB
gophertrunk siglab sweep -snr-min 6 -snr-max 20 -snr-step 1 -csv sweep.csv
The default ladder runs 2 dB to 30 dB in 2 dB steps. Because it’s built on the
synthesis engine from Part 6, the whole thing is deterministic for a given
-seed — the same noise realization every run, which is precisely what a
regression fixture requires. The sweep is the human-readable, on-demand companion
to a hard-gated regression test that lives in CI: same instrument, run whenever
you want an answer.
Reading the curve: measured vs theory
The sweep’s output is a table per path, and the important column is the comparison to a theoretical symbol-error-rate reference. Theory says, for a given Es/N0, what the best-achievable symbol-error behavior is; the sweep plots your actual demod against that ideal. Here’s a representative run:
c4fm (vs theoretical reference):
SNR dB Es/N0 lock EVM % SNR~ dB
2.0 -1.8 no 34.0 3.1
6.0 2.2 yes 22.0 6.8
10.0 6.2 yes 12.0 10.4
14.0 10.2 yes 6.0 14.1
18.0 14.2 yes 3.0 18.0
The read is the gap between measured and theory. A demod that hugs the theoretical curve down to low SNR and only diverges near the noise floor is performing well; a demod that peels away early — losing lock or ballooning EVM several dB above where theory says it should still be fine — has a deficiency worth chasing. The divergence point is the single most useful number the sweep produces: it’s the honest floor of your demodulator.
Both demod paths
The sweep runs both P25 Phase 1 demod paths in one go, each against its own theoretical reference, because they’re different algorithms with different error curves:
// cmd/gophertrunk/siglab_sweep.go
modes := []struct{ ... }{
{"c4fm", "c4fm", "", metrics.SER4PAM, 2},
{"cqpsk", "lsm", "cqpsk", metrics.SERCoherentQPSK, 2},
}
The c4fm path (the four-rail FM family) is checked against a 4-PAM
symbol-error reference; the cqpsk path (synthesized as lsm, the
linear-simulcast-friendly modulation) is checked against a coherent-QPSK
reference. Sweeping them together is what lets you answer a real design question —
“is my change an improvement on both paths, or did I help one and hurt the
other?” A tweak that lowers the c4fm divergence point but raises cqpsk’s isn’t a
clean win, and only a two-path sweep exposes that trade.
The CSV and the regression workflow
Pass -csv and the sweep also writes a machine-readable table with a fixed
column set:
mode,snr_db,es_n0_db,locked,evm_pct,snr_est_db
c4fm,2.0,-1.8,false,34.00,3.10
c4fm,6.0,2.2,true,22.00,6.80
cqpsk,6.0,2.2,true,24.50,6.40
Those six columns — mode, snr_db, es_n0_db, locked, evm_pct,
snr_est_db — are the whole regression story. The workflow Reese runs on every
demod change: sweep with a fixed seed before the change and save the CSV, make
the change, sweep again with the same seed, and diff the two CSVs. Same
synthesized input, same noise draw, so any difference in locked, evm_pct, or
snr_est_db is attributable to the change alone. Load both into the Compare
tab (Part 3) to overlay them, and export the merged result as JSON / JSONL / YAML
/ CSV for the record. That’s how a demod change earns its way into the tree — a
curve that moved the right way on both paths, reproducibly.
SNR, Es/N0, and the lock threshold
Two of the sweep’s columns deserve a word, because conflating them is a common
mistake. snr_db is the SNR the sweep injected — the knob you turned.
es_n0_db is the energy-per-symbol to noise-density ratio the theoretical
reference is expressed in, derived from the injected SNR and the samples-per-symbol
(P25 Phase 1 runs 48 kHz / 4800 = 10 samples per symbol). They move together but
they aren’t the same number, and the theoretical symbol-error curve lives in
Es/N0 space, which is why the sweep computes and prints both — so measured and
theory are compared on the same footing rather than across a hidden conversion.
The number most operators actually care about, though, is the lock threshold —
the lowest SNR rung where locked flips from no to yes. That threshold is a
compact, honest figure of merit for the demodulator: a lower threshold means the
demod holds on to weaker signals, which is exactly the improvement most demod work
is chasing. Watch it alongside the EVM curve, because the two tell a fuller story
together. A change that lowers the lock threshold by 2 dB but worsens EVM at the
top of the ladder has traded steady-state cleanliness for weak-signal
sensitivity — sometimes a good trade, sometimes not, but never one you’d notice
from a single capture. The sweep makes the trade visible, which is the entire
reason it exists: real demod decisions are about the shape of the whole curve, not
one point on it.
One caution Reese repeats: the sweep is synthesized AWGN, the cleanest possible adversary. A demod that looks great on the ladder can still struggle on a real capture full of multipath, phase noise, and interference the sweep never injected. Treat a good sweep as necessary but not sufficient — it proves the demod is sound against the textbook channel, and then you confirm against real recordings and the stacked-impairment fixtures from Part 6. The ladder is the controlled experiment; the field is the exam.
Series wrap: the Lab Bench trilogy from here
That closes Signal Lab. Across ten parts you went from a first no-radio replay to reading the dashboard, driving the browser console, seeing modulation quality in constellations and eyes, measuring spectrum and occupancy, synthesizing references, taking VSA measurements, naming the unknown, dissecting P25 to the signaling block, and benchmarking the demodulator itself. The unifying idea never changed: it’s the same production pipeline the daemon runs, fed from a file — so everything you measured here is what you’d get on the air.
But the story isn’t over, because Mercury isn’t decoded. Signal Lab named it a candidate — ~4800 sym/s, 4-level-FSK-like — and handed the wideband capture on. The next instrument in the trilogy is RF Scope, which treats the airwaves like a protocol analyzer: it segments IQ into bursts, builds a protocol hierarchy, graphs channel activity, and triages payload entropy — flagging Mercury as an unknown, intermittent emitter and emitting its frames for the last stage. Those frames land in Crypto Lab, where the trilogy resolves: Mercury turns out not to be strong encryption at all, and “obfuscation is not encryption” gets the last word. If Signal Lab taught you to measure a signal, RF Scope teaches you to map it and Crypto Lab teaches you to break it.
FAQ
What does the sweep actually vary?
Injected SNR, across a ladder from -snr-min to -snr-max in -snr-step
increments (default 2–30 dB by 2). At each rung it synthesizes P25 Phase 1,
decodes it, and records lock, EVM, and estimated SNR against a theoretical
symbol-error reference.
Why sweep both c4fm and cqpsk? They’re separate demod algorithms with separate error curves. Sweeping both against their own references shows whether a change helps universally or trades one path against the other.
How do I use it as a regression test?
Fix the -seed, sweep and save the CSV before your change, sweep again after with
the same seed, and diff. Identical input and noise draw mean any difference is
your change. The default seed is 0x5175.
Where do I go after Signal Lab? To RF Scope to map signals into a protocol hierarchy, and to Crypto Lab to analyze their payloads — the other two legs of the Lab Bench trilogy, where the Mercury thread finally resolves.
Series navigation
Part 10 of 10 · ←Part 9 · Back to the Signal Lab series hub