Records, Receivers, and Assets

A scene is the logical result of composing a fixed-duration receiver capture from a planned physical situation. A record is the persisted dataset unit consumed by training or evaluation, derived from that scene after labels are attached. One sample publishes exactly one record, whatever the scene contains: neither the number of emitter slots the plan selects nor the number of receivers observing it changes that count.

Minimal Example

First complete the Narrowband classifier Golden Path, then return to the directory you ran rfgen init from, so that ./narrowband-config/rfgen-output exists. The Golden Path’s last steps run inside narrowband-config, and storage.path resolves against the working directory rather than the configuration directory. That prerequisite has one receiver, so the record below carries one receiver subtree. To add receivers to the same record, use Configure multi-receiver output.

import signal_dataset as sd

from rfgen.record_reconstruction import record_from_signal_dataset

dataset = sd.open("./narrowband-config/rfgen-output")
record = record_from_signal_dataset(dataset[0])

# One sample published one record; its scene_id names the scene behind it.
assert dataset[0].scene_id
assert record.iq.shape[0] == 2

What It Covers

This page defines:

  • scene vs record,

  • multi-emitter planning versus accepted events,

  • TX count and receiver count against record count (both independent),

  • how a multi-receiver scene fits inside one record,

  • content-addressed scene assets,

  • reconstruction of a Sionna RT environment from a record.

Scene vs Record

A scene is the in-memory composition result for a fixed receiver capture: receiver IQ, scene metadata, and the accepted emitted events. Its plan describes emitter slots in time and frequency, optional physical geometry, materials, antenna patterns, and one or more receivers.

A record is one persisted training or evaluation example derived from a scene. It contains IQ, labels, metadata, and references to any large assets needed to interpret or reproduce it. The scene is the in-memory value and the record is the persisted one; they stand one to one.

The distinction still matters, because a scene carries structure a record has to express: several receivers, and the geometry and material assets a ray-traced scene depends on.

Multi-Emitter Planning and Accepted Events

This section assumes the semantic channel pipeline (TX impairments, channel propagation, RX capture, RX hardware) and the receiver sum point. See Channels if those terms are new. TX impairments are per-emitter; propagation follows scene.channel_application; RX stages are post-sum and per-receiver.

Scene composition uses a multi-emitter planning model rather than a separate single-TX path. The configured density selects zero or more planned emitter slots; a count of one is simply the lower end of a nonzero draw. A plan is not an event list: each selected slot still has to produce an accepted placement.

Accepted placements produce component signals. The scene composer calls an emitter’s generate() method for a selected slot, then creates one component signal for each accepted time placement. A slot can yield no component when its allowed-empty strategy returns no starts, or several components when a strategy returns several valid starts. TX impairments preserve those components; propagation follows the selected application mode. The scene-level Signal therefore carries one component_signals[] entry per accepted placed event, so labels and annotations can read event ground truth directly. Scene Capture Boundaries defines which starts fit the capture; Coordinate Systems defines the resulting coordinates.

Consequently, a scene can plan multiple emitter slots and still have zero accepted events when every selected slot uses an allowed-empty strategy that returns no valid starts. It remains a valid background scene: its composed IQ and derived records have an empty component/event list and empty event labels. This is distinct from planning zero slots; both cases are valid and neither changes the record count, which is always one per sample.

One channel chain, scoped by group. The composer constructs a single ChannelPipeline. TX impairments run per-emitter; propagation follows scene.channel_application; RX capture and RX hardware run post-sum, per-receiver.

Compatibility or ablation modes that propagate an already-summed composite IQ must be explicit because they erase per-emitter propagation paths. Full mode semantics and the parameter surface are in Reference / Scene Composition Algorithm.

Source: Concepts / Channels defines the four channel groups and their cited subpages; Scenes § Composer Flow defines the pre-sum and post-sum composer flow; Reference / Scene Composition Algorithm records the implementation contract for emitter sampling, per-emitter routing, receiver summation, and multi-RX output.

One sample is one record. Neither the number of planned slots nor the number of receivers changes the record count. A scene plan that selects five slots produces exactly one record whether it accepts five events or none, and a scene with four receivers still produces one.

Multi-RX: receivers inside one record

A multi-receiver scene does not fan out into several records. Each receiver arrives as its own named subtree of the single record:

projections/receiver/receivers/rx0/iq
projections/receiver/receivers/rx1/iq

There is no configuration knob to change this. A consumer that wants one training example per receiver selects the subtree it needs at read time, which costs nothing and reads exactly the bytes it asked for; a consumer that needs the receivers jointly — AoA, beamforming, MIMO decoding, TDOA, distributed sensing — reads several subtrees from the one record and gets samples that are guaranteed to share a scene, a clock, and a time origin.

That guarantee is why the split is not offered. Two records fanned out from one scene look independent, and nothing in the format says they must be read together; a per-receiver layout therefore lets a consumer silently train on half a scene. Keeping the receivers in one record makes the joint case correct by construction and leaves the single-receiver case one field name away.

Within one record:

  • IQ is per receiver, under that receiver’s subtree.

  • The segmentation raster is per receiver too, and carries a leading receiver axis when the scene has more than one: (receiver, frequency, time) for a single-label raster, (receiver, class, frequency, time) for multi-label. A single-receiver scene has no such axis.

  • Declared boxes are stated once for the record, in the receiver-baseband frame. That is unambiguous rather than approximate: a scene whose receivers do not share a centre frequency is refused at label time with a LabelError naming heterogeneous receiver centre frequencies, so a record that exists has exactly one receiver frame for its boxes to be in.

  • scene_id is shared; rx_index distinguishes a per-receiver view once a consumer resolves one.

Multi-band Devices: Heterogeneous Receivers

Real customer devices often carry several receivers in one chassis, each tuned to a different band: a 5G NR receiver, a Wi-Fi receiver, a UHF radar tuner, and so on. The receivers share a position but capture different parts of the spectrum and run on different sample rates.

The framework expresses this with the heterogeneous-receivers path of MultiRXConfig. Set receivers to a list of ReceiverConfig entries; each carries its own position, optional center_freq_hz, bandwidth_hz, and sample_rate_hz. Per-emitter signals are merged at each receiver after band-filtering to that receiver’s capture window. Each receiver still gets its own subtree in the one record, so a multi-band chassis reads back tuner by tuner.

Construct the config in Python:

from rfgen.config import MultiRXConfig, ReceiverConfig

multi_rx = MultiRXConfig(
    receivers=[
        ReceiverConfig(
            rx_id="5g_nr",
            position_m=(0.0, 0.0, 1.5),
            center_freq_hz=3.5e9,
            bandwidth_hz=100e6,
            sample_rate_hz=122.88e6,
            antenna_id="rugged_omni_v2",
        ),
        ReceiverConfig(
            rx_id="wifi",
            position_m=(0.0, 0.0, 1.5),
            center_freq_hz=2.4e9,
            bandwidth_hz=80e6,
            sample_rate_hz=80e6,
            antenna_id="patch_2_4ghz",
        ),
    ],
)
assert [receiver.rx_id for receiver in multi_rx.receivers] == ["5g_nr", "wifi"]

The equivalent config that constructs the same objects:

# Two co-located receivers on one device: a 5G NR tuner and a Wi-Fi tuner.
scene:
  multi_rx:
    receivers:
      - rx_id: "5g_nr"
        position_m: [0.0, 0.0, 1.5]
        center_freq_hz: 3.5e9
        bandwidth_hz: 100e6
        sample_rate_hz: 122.88e6
        antenna_id: "rugged_omni_v2"
      - rx_id: "wifi"
        position_m: [0.0, 0.0, 1.5]
        center_freq_hz: 2.4e9
        bandwidth_hz: 80e6
        sample_rate_hz: 80e6
        antenna_id: "patch_2_4ghz"

Each scene produces one record holding two receiver subtrees, one per tuner. The stable receiver label begins at ReceiverConfig.rx_id; the scene composer copies that value into ChannelRxParams.tag for the active receiver on every channel call. In stored multi-RX scene metadata, the full receiver catalog is mirrored at record.scene.extras["receivers"][i]["rx_id"]; index that list by receiver ordinal to recover which configured receiver produced a given subtree. The 5G subtree carries only emitters whose realized carrier overlaps [3.45, 3.55] GHz; the Wi-Fi subtree carries only emitters in [2.36, 2.44] GHz. An emitter outside both bands contributes to neither receiver’s IQ, and scene metadata does not currently persist a drop-reason audit field.

This shape is distinct from the array path (MultiRXConfig.geometry = "ula" | "ura" | "arbitrary"), which assumes homogeneous receivers sharing the scene-level RF parameters. Use the array path for TDOA arrays and MIMO basestations; use the heterogeneous path for multi-band devices and distributed sensing networks where each node has its own front-end.

Source: Reference / Scene Composition Algorithm § Multi-RX defines the homogeneous receiver-array path and heterogeneous receiver-list path. Scene Geometry and Reference / Scene Geometry Assets ground the Sionna RT array, antenna, and receiver-position inputs used when the multi-RX scene is geometric.

Content-Addressed Assets

When Sionna RT is in use, a record must be enough to reconstruct the environment that produced it:

  • Mitsuba/Sionna scene geometry,

  • radio-material database,

  • antenna patterns,

  • TX/RX poses,

  • channel realization seed.

Embedding geometry in every record does not scale. Sionna RT, OpenStreetMap-derived, and CAD-derived scenes may include geometry, mesh, material, and antenna assets that are shared by many records. The framework stores large assets once by content hash.

Source: Reference / Scene Geometry Assets documents the Sionna RT and Mitsuba scene assets, radio materials, antenna arrays, and receiver positions that must be reconstructable. Sionna RT’s official documentation and Mitsuba’s scene-format documentation are the external sources for those asset types.

Do not read the dataset directory yourself. Signal Dataset owns the on-disk layout, and Storage layout states the rule plainly: an application must not construct internal filenames or list a dataset directory or GCS prefix. Open a snapshot and index it.

What a record carries instead is a small content-addressed reference per asset, in scene metadata:

from rfgen.record_reconstruction import record_from_signal_dataset
import signal_dataset as sd

dataset = sd.open("./narrowband-config/rfgen-output")
record = record_from_signal_dataset(dataset[0])

asset_refs = record.scene.geometry_asset_refs
assert isinstance(asset_refs, tuple)
assert record.scene.rx_index == 0
assert record.scene.rx_pose is not None

A stored record does not expose dataset.resolve(...). The current contract is the metadata itself: record.scene.geometry_asset_refs carries the resolved typed asset references, while scene_geometry_hash, material_db_hash, and antenna_pattern_hash provide content-addressed audit keys. Consumers read the typed refs directly and use StorageConfig.assets_path to locate the corresponding blobs on disk. The exact channel realization is reproducible from those asset refs and hashes, record.scene.channel_realization_seed, the receiver pose, and the config.

Source: Reference / Determinism defines (global_seed, shard_id, sample_idx), shard content addressing, per-layer seed splitting, and backend-version recording. Reference / Scene Geometry Assets defines the loadable Sionna RT / Mitsuba asset bundle that scene_geometry_hash identifies.

Why Content Addressing Fits Ray-Traced Scenes

All receivers in one scene share its geometry, material database, and antenna pattern hashes, and share one record rather than repeating them. Across a corpus of N records drawn from the same environment, disk cost is:

1 x geometry assets + N x small record metadata

not:

N x geometry assets

Content addressing is what keeps a large ray-traced environment affordable at dataset scale.

Inline Asset Export

inline_assets=True is not supported. Asset blobs stay content-addressed under the asset store, and callers should use StorageConfig.assets_path plus the typed refs in record.scene.geometry_asset_refs.

Determinism

A record is reproducible from (global_seed, shard_id, sample_idx) plus the asset store. The asset hash binds a record to the environment used to produce it.

Re-running with the same config, seeds, backend versions, and asset store should produce the same deterministic generation output. Full seed-flow rules live in Reference / Determinism.

Source: Reference / Determinism is the normative seed-flow contract. It defines the shard and sample seed hierarchy, layer-specific random-number-generator splits, and the acceptable backend nondeterminism caveat for Sionna and GPU reductions.

See Also