The BER sweeps can add noise from a recording of your own instead of the Gaussian noise they generate. This is the one mechanism that lets you measure a receiver against an impairment that is not white and not Gaussian — a neighbouring transmitter, a switching supply, a jammer, something you captured off the air — and see whether the frame sync and channel estimator survive it.
The noise is summed onto the received samples in the time domain, so it passes through synchronisation, channel estimation and equalisation exactly as real interference would. Nothing about it is special-cased.
A SigMF pair: a .sigmf-data file of raw samples and a .sigmf-meta file of JSON beside it, sharing a base name. Samples are interleaved 32-bit little-endian floats, I then Q — complex64 in NumPy, cf32_le in SigMF, the same layout GNU Radio calls "complex float32". Nothing else is accepted.
my_noise.sigmf-data 8 bytes per sample: float32 I, float32 Q
my_noise.sigmf-meta JSON: datatype, sample rate, version
| Requirement | Why |
|---|---|
complex64, little-endian |
What the flowgraph reads. A float32 real-only file is read as alternating I and Q and produces nonsense. |
| File size a multiple of 8 bytes | One complex sample is 8 bytes. A file that is not is truncated or misaligned. |
| Not empty | Checked before any hardware is touched. |
Point at the .sigmf-data half |
The .sigmf-meta is the JSON. Handing over the metadata file is the most common mistake, and it is caught by name. |
| Long enough | It is read repeatedly, so short files repeat quickly. A million samples is a sensible floor. |
Amplitude does not matter. The sweep measures the recording's RMS and scales it to hit the requested SNR, so a file at any level works. What does matter is the shape — the statistics you are trying to impose.
make_noise.py ships with the collection and needs only NumPy. It writes both halves of the pair and is deterministic from (kind, count, seed), so regenerating gives byte-identical samples:
python3 make_noise.py ./my_noise --kind awgn --count 1048576 --seed 1234
wrote ./my_noise.sigmf-data (1048576 samples, awgn, rms 1.0000)
Three kinds are built in: awgn, coloured (spectrally tilted) and impulsive.
Everything that matters is a dozen lines. Gaussian noise first:
import numpy as np
count, rate = 1 << 20, 1e6
rng = np.random.RandomState(1234) # seeded: regenerate, do not store
# Complex Gaussian. The sqrt(2) makes the total power 1 rather than 2,
# because I and Q each contribute.
n = (rng.randn(count) + 1j * rng.randn(count)) / np.sqrt(2.0)
n = (n / np.sqrt(np.mean(np.abs(n) ** 2))).astype(np.complex64)
n.tofile("my_noise.sigmf-data")
Impulsive noise is the same, plus rare large excursions:
# Gaussian background, then one sample in a thousand multiplied by 12.
# That is the shape that defeats anything assuming Gaussian statistics -
# which is most receivers.
n[rng.rand(count) < 0.001] *= 12.0
And the metadata beside it:
import json
json.dump({
"global": {
"core:datatype": "cf32_le",
"core:sample_rate": rate,
"core:version": "1.0.0",
},
"captures": [{"core:sample_start": 0}],
"annotations": [],
}, open("my_noise.sigmf-meta", "w"), indent=2)
core:datatype and core:sample_rate are the fields that matter. The generator also records a SHA-512 of the data and the (kind, seed, count) that produced it, so a recording found in an artifact bundle a year later can be regenerated exactly rather than trusted.
awgn 1048576 samples rms 1.0000 peak/rms 3.7 kurtosis 2.0
impulsive 1048576 samples rms 1.0000 peak/rms 36.5 kurtosis 38.7
Same power. Ten times the peak, nineteen times the kurtosis. That is the whole point: at equal RMS — equal "SNR" by the usual definition — impulsive noise costs about 2.7 dB more than Gaussian on this receiver, because a handful of enormous samples wrecks whole OFDM symbols while the average says nothing is wrong.
If you are generating your own impairment, check its kurtosis, not only its power. Two recordings with the same RMS can be entirely different channels.
Two parameters, and exactly one of them:
# a file on the CONSOLE - the role ships it to the receiver for you
cosmos-run fr3/ofdm-ber.description.yml \
-e '{"cosmos_parameters": {"noise_file": "/home/you/my_noise.sigmf-data"}}'
# a file already ON the receiving node
cosmos-run fr3/ofdm-ber.description.yml \
-e '{"cosmos_parameters": {"noise_file_on_node": "/root/my_noise.sigmf-data"}}'
Setting both is refused, because the two mean different machines and guessing which you meant would be wrong half the time.
Not every experiment exposes these. A description has to declare a parameter before you can override it, and only the noise-injection experiments do:
| Experiment | noise_file |
noise_kind |
|---|---|---|
fr3/ofdm-ber.description.yml |
yes | yes |
sdr/ofdm-ber-grid.description.yml |
yes | yes |
sdr/ofdm-ber-direct.description.yml |
yes | yes |
sdr/ofdm-ber-sb5.description.yml |
yes | — |
The attenuation and transmit-power sweeps (ofdm-ber-sb4, *-txpower) do not: they move the operating point by changing the path or the transmitter, and add no noise at all. Asking one of them for a noise file is refused by name:
experiment description 'ofdm-ber-sb4' is not valid (1 problem):
parameters: references undeclared parameter 'noise_file'
Declared parameters: amplitude, atten_db, freq_hz, rx_gain,
stop_below_ber, sub, target_errors, tx_gain.
The file is validated on the console, before any hardware is touched — existence, the .sigmf-meta-instead-of-.sigmf-data mistake, emptiness, and a size that is not a multiple of 8. A bad path fails in a second rather than after the nodes have been imaged.
Collected artifacts name it noise_supplied_<yourfile>, so an artifact bundle shows plainly that the curve was measured against a stranger channel rather than the built-in one. A run says so as it happens:
"injecting /run/cosmos/fr3-ofdm/noise_supplied_page_awgn.sigmf-data (supplied from the console)"
"received RMS 0.035823, floor 0.00085188 -> signal RMS 0.035813 (32.5 dB above floor)"
14.00 13.94 0.007146 1571400 15714 558 4.6237e-05 15427
That is a file built with exactly the dozen lines above, run on the grid pair — shipped from the console, injected, and measured.
The recording is read repeatedly for as long as a point dwells. A short file repeats often, and a repeating "random" impairment is periodic — which a channel estimator can partly learn, flattering the result. A million samples at 1 MS/s is one second, which is long enough that nothing sees a pattern.
Part of OFDM BER Across Five Subcarrier Modulations.
Last verified: 2026-08-04.