Signal, Capture, Record: the containers that hold the samples

Reading RFGen source or reference pages, you meet several objects that all appear to “hold the samples”: Signal, ComplexCapture, LabeledScene, and signal_dataset.Record. They are not alternatives and you do not choose between them. Each owns a different stretch of the pipeline, and only the last one is something you consume.

This page says which container holds your data where, why there is more than one, and what changed in the receiver unification work so that the reference pages and the code agree.

If you only consume datasets, you need one sentence from this page: the stored object is a signal_dataset.Record, its contract is frozen, and nothing described below changed it. The rest matters when you write an extension or read a stack trace.

The containers at a glance

Container

Where it lives

Who touches it

Do you see it?

Signal

Transmit side: emitter output, transmit hardware impairments, one-way propagation

Emitters, TX stages, propagation backends

Only if you write an emitter or a transmit-side stage

ComplexCapture

Receive side: everything from the receiver frontend to the projection, in both domains

The shared receiver chain, both composers

Only if you write a receiver stage or a scene renderer

LabeledScene

In flight between composition and projection: the composed scene plus its labels

Labelers, projections

Only if you write a labeler or a projection

signal_dataset.Record

Storage: the unit written to disk and read by training code

The store, your dataloader

Yes. This is the product

Two vocabulary notes, because the rest of the page leans on them:

  • IQ (“in-phase and quadrature”) is the pair of numbers that represents one complex sample of a radio signal. RFGen carries it either as two interleaved real channels shaped [..., 2, N] or as one complex array, depending on the container.

  • Baseband means the signal has already been shifted down so that the carrier frequency you tuned to sits at zero. A “complex baseband capture” is what a software-defined radio hands you.

The flow, end to end

        flowchart TB
  EM[Emitters<br/>one Signal per emitted event]
  TX[Transmit hardware impairments<br/>Signal in, Signal out]
  PROP[One-way propagation<br/>Signal in, Signal out]
  SUM[Scene composition<br/>place, delay, scale, sum per receiver]
  SEAM[The receiver seam<br/>each receiver's summed baseband<br/>is wrapped as a ComplexCapture]
  RADAR[Radar response backend<br/>emits component ComplexCaptures directly]
  CAP[Shared receiver chain, capture plane<br/>local-oscillator error, mixer, IF filter,<br/>resampler, clock offset, low-noise-amplifier noise]
  BG[Joint receiver background<br/>injected once across all receivers]
  HW[Shared receiver chain, hardware plane<br/>ADC, phase noise, IQ imbalance, gain control]
  COMP[Composition and labelling<br/>one LabeledScene]
  PROJ[Projections<br/>typed observation fields]
  REC[(One signal_dataset.Record)]
  EM --> TX --> PROP --> SUM --> SEAM --> CAP --> BG --> HW --> COMP --> PROJ --> REC
  RADAR --> CAP
    

Read left to right down the page, the containers hand off three times: at the receiver seam, at composition, and at the store.

Signal carries the transmit side. Every emitter’s generate returns a Signal: IQ samples plus the metadata that travels with them (sample rate, bandwidth, class name, realized carrier, start sample, duration, signal-to-noise ratio). Transmit hardware stages and propagation backends take a Signal and return a Signal. Scene composition places each emitted event on the scene clock, applies the per-receiver channel, and sums everything that arrives at each receiver.

The handoff happens at the receiver seam. The moment a receiver’s summed baseband is ready, the composer wraps it as a ComplexCapture and the Signal is done. Everything downstream of that point, in both domains, is capture work.

ComplexCapture carries the receive side. It is the in-flight container inside the pipeline: complex samples, named axes with one of them declared as the time axis, the sample rate, receiver and hardware identity, the alignment block (time origin, carrier, local-oscillator frequency, phase reference), the measurement plane, and an append-only transformation_log recording which receiver stages ran. The shared receiver chain runs in two segments with the joint receiver background injected between them, because the background is the receiver’s noise floor and has to be present before the gain control and the analog-to-digital converter, which are the two stages whose whole job is to react to the total level.

LabeledScene is what leaves composition. It carries the IQ payload, scene metadata, per-emitter metadata, and a LabelSet. A projection turns it into the typed observation fields the store writes, namespaced under that projection’s id; the record on disk is a signal_dataset.Record holding those fields.

Why the radar branch has no Signal at all

Active radar never produces one. A radar response backend returns component captures directly, already at the radar measurement plane (a dechirped beat signal, not a communications baseband), and they enter the same shared receiver chain that the communications captures enter. See Cross-Domain Architecture for why the two planes coexist in one record and are never added together.

This is the point of the convergence. Before it, the communications path ran the receiver stages through adapters that dressed a capture up as a Signal and back again, and the radar path ran the same stage objects natively on captures. Those adapters are gone. One container, one chain, both domains.

What Signal retiring at the receiver seam does and does not mean

Signal is not deleted and is not deprecated. It is scoped: it is now the transmit-side and propagation container, and nothing on the receive side accepts or returns one.

Concretely, Signal is still what you work with when you:

  • write an emitter, because BaseEmitter.generate returns a Signal;

  • write a transmit hardware stage or a propagation backend, because BaseChannel.apply takes a Signal and returns a Signal;

  • read component signals out of scene composition, because the per-emitter components used for labels and signal-to-noise bookkeeping are Signal objects.

What no longer exists is a Signal anywhere past the receiver seam. If you are writing a receiver stage, you write against the stage core and the frontend chain: your stage transforms interleaved IQ and knows nothing about any container, and the chain owns the ComplexCapture around it.

Why the stored record did not move

The stored signal_dataset.Record is the convergence point, and it is a frozen contract: both domains project into it, and the whole receiver unification was carried out under the constraint that the stored bytes do not change. That constraint was measured rather than asserted. Nine pinned configurations covering both domains produce byte-identical stored payloads before and after the convergence, and the two things that did move on the communications path (the realized receiver metadata, which is now projected from the capture, and the capture alignment, which exists on that path for the first time) are metadata fields, not samples.

So the practical answer for a consumer is that this page describes an internal reorganization. The record layout in Records, Receivers, and Assets and the schema in Core Types are what they were.

See also