Build your first dataset

This tutorial produces a small dataset for a familiar ML task: predict a digital modulation class from complex baseband IQ. It is deliberately small so you can inspect every layer of the data product before adapting the configuration for your own study.

Before running the template, install the optional TorchSig and Sionna integrations because the shipped configuration selects its communications emitter and the 3GPP TR 38.901 Urban Microcell (UMi) propagation model, an urban-microcell propagation scenario: uv pip install -e '.[torchsig,sionna]'. The core uv pip install -e . installation remains sufficient for native local Signal Dataset workflows that do not select TorchSig.

What you will make

The retained narrowband-baseline template generates 100 synthetic, single-emitter records. Each record contains:

  • complex baseband IQ: paired in-phase and quadrature samples that preserve amplitude and phase;

  • a structured label identifying one of BPSK, QPSK, 16-QAM, or 64-QAM;

  • scene and generator provenance explaining how the example was made;

  • a local Signal Dataset snapshot that supports inspection and ordinal training access.

This is a controlled synthetic baseline, not a balanced benchmark, a claim about over-the-air realism, or a promise of classifier performance.

1. Materialize and validate a configuration

Modulation classification means training a model to map an IQ observation to the modulation format used to produce it (for this tutorial, one of the four structured labels below).

rfgen init copies a maintained starting configuration into a directory you own. Read and edit this YAML when you start a new experiment; rfgen does not ask you to select from a catalog of hidden scenarios.

rfgen init narrowband-baseline ./narrowband-config
cd ./narrowband-config
rfgen validate --config-dir .

Validation checks that the configuration can be composed and satisfies rfgen’s generation contract. It does not write records.

2. Generate and inspect the dataset

rfgen generate --config-dir .
rfgen inspect ./rfgen-output --sample-size 1

generate exits 0 on success and normally prints nothing, so silence is not a sign it did nothing. It does print a warning when --num-samples is not a multiple of the configured shard size, which is a note about the last shard being partial, not a failure. inspect is how you confirm the result.

Generation publishes a Signal Dataset snapshot at ./narrowband-config/rfgen-output. Inspection is read-only and reports its record count, field inventory, and a bounded ordinal prefix of record IDs. The display classes BPSK, QPSK, 16-QAM, and 64-QAM use the canonical class_name strings bpsk, qpsk, 16qam, and 64qam, respectively, in record labels and metadata. Treat the output as a dataset with labels and provenance, not as a loose directory of waveform files.

3. Open record 0

SignalDatasetStore opens the published snapshot, and signal_dataset.open reads it directly. Ordinal access is lazy. Convert to tensors only at the training boundary, which your application owns.

import torch

import signal_dataset as sd

dataset = sd.open("./rfgen-output")
assert len(dataset) == 100

record = dataset[0]
# Field names are namespaced by the projection that produced them; `rfgen
# inspect` printed the full list.
iq = record["projections/receiver/receivers/rx0/iq"].data
tensors = {"iq": torch.from_numpy(iq.copy())}
print(record.id, tensors["iq"].shape)

The record ID and IQ shape are the success signal: you reopened the published item by ordinal. Tensor conversion, batching, and split policy belong to your training application — rfgen stops at the published snapshot.

RFGen scene and emitter details are nested under record.metadata["rfgen"], inside the projection that produced them. Read them through the two accessors rather than walking the envelope by hand:

from rfgen.core.metadata import decode_rfgen_metadata
from rfgen.record_reconstruction import scene_provenance_of

declared = scene_provenance_of(decode_rfgen_metadata(record.metadata["rfgen"]))
print(declared["scene"]["scene_id"], declared["emitters"][0]["class_name"])

decode_rfgen_metadata restores permitted non-finite RF metrics as Python floats; scene_provenance_of returns the scene, emitters, and boxes the record declares, and refuses a record whose envelope does not name exactly one scene.

What to do next

  • Annotate the snapshot you just produced with Annotate an existing dataset.

  • Read Labels for what the boxes and the segmentation raster in that record mean, and Label schema for the frames they are stated in — the box edges are offsets from the receiver’s tuned centre, not absolute RF, which is the single easiest thing to get wrong about this data.

  • Read Generation Jobs for the editable configuration, validation, execution, and provenance lifecycle you just used.

  • Read RF & ML Quick Overview for the relationship between a scene, IQ, labels, annotation overlays, storage, and training.

  • Use the narrowband Golden Path for the exact qualification boundary and safe customization handoff.

  • Use How-to Guides when you have a focused task.