wfmgen — One Engine, Every Waveform¶
What you're seeing¶
A single declarative engine — doppler.wfm.Synth, the same C core the
wfmgen command-line tool run — produces every waveform type,
shown here through the view that makes each one's structure obvious.
Top-left — tone. A complex baseband tone at fn = 0.10 (relative to fs)
at 30 dB SNR over the sample rate. Because the signal is complex, the spectrum
has a single line at +0.10 with no mirror image; the red marker labels the peak.
Top-right — PN. A maximum-length sequence (LFSR register length 7 → period
127) at one chip per sample. Its periodic autocorrelation is the MLS
"thumbtack": 1.0 at zero lag and a flat −1/127 floor at every other lag — the
property that makes an MLS a good spreading and ranging code. --pn_poly 0
selects the primitive polynomial for the chosen length automatically.
Bottom-left — QPSK at 20 dB Es/No and bottom-right — BPSK at 8 dB Es/No,
both after a boxcar matched filter over each symbol (the receiver view that
realises the Es/No — integrating a symbol's sps samples buys
10·log10(sps) dB over a single mid-symbol sample). QPSK's four Gray-coded
points and BPSK's two antipodal points sit exactly where the modulation places
them; the cloud size is the symbol-energy SNR made visible.
The engine and its CLI¶
Every type, SNR mode, and the MLS auto-polynomial live once in C
(native/src/wfm_synth/wfm_synth_core.c, generated from
objects/wfm_synth.toml), and one command-line tool — wfmgen — exposes
it:
wfmgen |
|
|---|---|
| Scope | one waveform from flags, or a multi-segment scene |
| Spec | flags or --from-file spec.json |
| Containers | raw, csv, BLUE-1000, SigMF |
| Output | file / stdout / zmq:// |
| Provenance | --record run.json (replays byte-identically) |
The same engine is the doppler.wfm Python API; a one-segment wfmgen run is
the simple single-waveform case of the composer.
Smart defaults¶
The goal is that a bare command Just Works and you only dial in what you need:
wfmgen --type tone # clean baseband tone, fs 1 MHz, 1024 cf32
wfmgen --type qpsk --snr 12 # QPSK at 12 dB Es/No (the auto mode for *psk)
wfmgen --type pn --pn_length 9 # length-9 MLS, primitive poly chosen for you
--snr_mode autoresolves per type: over-fs for tone/noise/pn, Es/No for bpsk/qpsk. Override with--snr_mode fs|ebno|esno.--snr 100(the default) is effectively clean — raise the noise by lowering it.--pn_poly 0picks the maximum-length polynomial for--pn_length.
Containers, sample types, byte order¶
The sample type (--sample_type cf32|cf64|ci32|ci16|ci8, full-scale ±1.0 for
the integer types) is orthogonal to the container and byte order:
# 16-bit I/Q, big-endian, into a self-describing BLUE type-1000 file
wfmgen --type qpsk --count 100000 --sample_type ci16 --endian be \
--file_type blue -o capture.blue
# SigMF pair: capture.sigmf-data (raw) + capture.sigmf-meta (one annotation
# per segment, with frequency edges and the waveform label)
wfmgen --from-file scenario.json --sample_type ci16 --file_type sigmf -o capture
# stream to a ZMQ PUB endpoint a doppler subscriber can read
wfmgen --type tone --continuous --output zmq://tcp://*:5555
BLUE carries fs (as xdelta = 1/fs), the complex sample format, and byte
order in its 512-byte header, so one file is fully self-describing.
Multi-segment specs and reproducible runs¶
--record FILE writes the fully-resolved spec as JSON — every value after
defaulting (the chosen MLS polynomial, the resolved SNR mode, …). Feed that file
straight back with --from-file and you get a byte-identical stream:
wfmgen --type bpsk --count 50000 --sps 4 --record run.json -o a.iq
wfmgen --from-file run.json -o b.iq # a.iq and b.iq are identical
A multi-segment spec sequences waveforms with off-time gaps, and can repeat or run forever:
{
"version": "wfmgen-1", "repeat": false, "continuous": false,
"segments": [
{ "type": "tone", "fs": 1e6, "freq": 1e5, "snr": 100.0,
"num_samples": 10000, "off_samples": 5000 },
{ "type": "qpsk", "fs": 1e6, "snr": 9.0, "snr_mode": "esno",
"sps": 8, "num_samples": 40000, "off_samples": 0 }
]
}
PN codes — length, polynomial, realization¶
The PN/LFSR register runs from 2 to 64 bits; --pn_poly 0 (the default)
auto-selects a verified primitive polynomial for the length, so every auto code
is a true maximum-length sequence. --lfsr picks the realization — galois
(default, internal XOR) or fibonacci (external XOR) — same polynomial and
period 2ⁿ−1, different chip ordering.
wfmgen --type pn --pn_length 23 --sps 1 # length-23 MLS (auto poly)
wfmgen --type pn --pn_length 40 --sps 1 # 64-bit register, auto MLS
wfmgen --type pn --pn_length 9 --sps 1 --lfsr fibonacci
import numpy as np
from doppler.wfm import PN
# Galois and Fibonacci realizations of the same length-9 polynomial:
# identical period (511) and balance, different ordering.
galois = np.asarray(PN(0x108, 1, 9, lfsr="galois").generate(511))
fibonacci = np.asarray(PN(0x108, 1, 9, lfsr="fibonacci").generate(511))
assert galois.sum() == fibonacci.sum() == 256 # 2**8 ones, balanced
assert not np.array_equal(galois, fibonacci) # distinct sequence
A multi-segment spec can carry "lfsr": "fibonacci" and a 64-bit "pn_poly"
per segment.
Detached BLUE headers¶
--detached (BLUE only) splits the container into a header + data pair —
<out>.hdr (the 512-byte HCB, with detached=1 / data_start=0) and
<out>.det (the raw samples). Attached output keeps whatever extension you give
-o (.blue / .prm / …).
wfmgen --type qpsk --sps 8 --count 50000 \
--sample_type ci16 --file_type blue --detached -o capture
# → capture.hdr (512-byte HCB) + capture.det (raw interleaved I/Q)
Reproduce¶
python examples/python/wfmgen_demo.py # the four-waveform figure (writes .png)
python examples/python/pn_codes.py # PN MLS / Galois vs Fibonacci / 64-bit
From Python — the composer API¶
The same composer is available in Python (doppler.wfm), producing
byte-identical output to the CLI:
from doppler.wfm import Composer, Segment, Writer
spec = [Segment("pn", num_samples=127), Segment("qpsk", num_samples=4096,
off_samples=512)]
with Writer("frame.cf32", sample_type="cf32") as w:
w.write(Composer(spec).compose())
See the Python composer API
for Writer containers (raw / CSV / BLUE / SigMF), the ZmqSink, JSON
round-tripping, and the rrc_taps / dsss_spread / mls_poly helpers.
