Core Types¶
Module: rfgen.core_types
The data types every layer of the framework speaks. Defined as frozen dataclasses (frozen=True, slots=True) so they cross process boundaries (Spark, Ray) without surprise mutation.
The module ships ten frozen dataclasses (Signal, SignalMetadata, SceneMetadata, Record, StoredRecord, BBox, ChunkedSignal, GeometryPose, GeometryAssetRef, GeometryProvenance) plus four TypeAlias declarations (IQ, Spectrogram, SampleRate, CenterHz). All ten dataclasses use frozen=True, slots=True.
Type aliases¶
IQ, Spectrogram, SampleRate, CenterHz¶
from typing import TypeAlias
import torch
IQ: TypeAlias = torch.Tensor
"""Complex baseband. Single-RX shape `(2, N)` float32 (channel-first real/imag);
multi-RX shape `(num_rx, 2, N)` float32. Channel 0 is in-phase; channel 1 is
quadrature. Stored as float32 rather than complex64 for autograd
compatibility. See [Reference / IQ Layout Policy](../data/iq-layout-policy.md)
for the full in-memory vs storage contract."""
Spectrogram: TypeAlias = torch.Tensor
"""Shape `(F, T)` float32, log-magnitude unless otherwise specified."""
SampleRate: TypeAlias = float
"""Sample rate in Hz."""
CenterHz: TypeAlias = float
"""Carrier or center frequency in Hz, absolute, for HIL alignment."""
These are TypeAlias declarations (not NewType). For the canonical IQ shape rules, storage conversion boundary, and interop shapes (Zarr, WebDataset, HDF5, SigMF), see Reference / IQ Layout Policy.
class BBox¶
@dataclass(frozen=True, slots=True)
class BBox:
start_sample: int # inclusive start in the parent record's sample frame
duration_samples: int # number of samples this bbox spans
low_freq_hz: float # Hz, baseband-relative
high_freq_hz: float
class_id: int # integer class identifier
emitter_index: int # index into the parent Record's emitters tuple
Time-frequency rectangle for one signal. Used in detection and segmentation labels. The low_freq_hz and high_freq_hz edges are baseband-relative; the parent record’s per-RX rate sets the absolute axis.
BBox round-trips losslessly to and from a normalized YOLO-style 5-tuple (t_norm, f_norm, dt_norm, df_norm, class_id) for inputs in valid ranges via BBox.to_yolo and BBox.from_yolo. For the on-disk normalization (YOLO / COCO conventions) see Label Schema.
class GeometryPose¶
@dataclass(frozen=True, slots=True)
class GeometryPose:
position_m: tuple[float, float, float]
orientation_rad: tuple[float, float, float]
velocity_mps: tuple[float, float, float] = (0.0, 0.0, 0.0)
frame: str = "scene"
Typed 3D pose in the Sionna/Mitsuba scene frame. orientation_rad is
Euler angles; frame must be "scene" for v1 (__post_init__ raises
ValueError otherwise). velocity_mps feeds SionnaRT’s Doppler wiring:
a node constructed from a pose with nonzero velocity produces genuinely
time-varying channel taps when RTSolverConfig.cir_num_time_steps > 1.
The Pydantic config-layer mirror is
GeometryPoseConfig.
class GeometryAssetRef¶
@dataclass(frozen=True, slots=True)
class GeometryAssetRef:
kind: GeometryAssetKind
uri: str
content_hash: str
version: str | None = None
entrypoint: str | None = None
media_type: str | None = None
content_addressed: bool = True
metadata: dict[str, object] = field(default_factory=dict)
Content-addressed reference to a scene-geometry-related asset.
kind is a GeometryAssetKind
closed enum (Sionna built-in scene, Mitsuba XML bundle, material database,
antenna pattern, and related kinds). uri must start with file://,
gs://, s3://, https://, or sionna://builtin/. content_hash must
be sha256: followed by 64 hex characters. For
GeometryAssetKind.SIONNA_BUILTIN_SCENE, the uri suffix after
sionna://builtin/ names the Sionna built-in scene (e.g. munich),
resolved via sionna.rt.scene.<name>. For Mitsuba XML bundles,
entrypoint is the worker-local filesystem path SionnaRT passes
directly to sionna.rt.load_scene. content_addressed is True when
content_hash is a true digest of the asset’s bytes (the normal case for a
directly-constructed typed ref) and False when it is only a URI-identity
stand-in that cannot detect an in-place edit at a fixed URI (the legacy-URI
fallback for remote or unreadable assets). A False ref bypasses
SionnaRT’s scene cache and is loaded fresh on every solve.
class GeometryProvenance¶
@dataclass(frozen=True, slots=True)
class GeometryProvenance:
asset_refs: tuple[GeometryAssetRef, ...]
tx_pose: GeometryPose
rx_pose: GeometryPose
tx_array_id: str | None
rx_array_id: str | None
material_db_hash: str | None
antenna_pattern_hash: str | None
solver_backend: str
sionna_version: str
mitsuba_version: str
drjit_version: str
Geometry provenance recorded on RT-generated component metadata.
SionnaRT.apply populates this on every real solve: asset_refs is the
resolved scene/material/antenna asset refs; tx_pose/rx_pose are the
typed poses actually used; solver_backend is "sionna-rt"; and
sionna_version/mitsuba_version/drjit_version are the installed
library versions (__post_init__ requires all three non-empty when
solver_backend == "sionna-rt"). Lives on
SignalMetadata.geometry for single-RX RT scenes and is threaded through
SceneMetadata.extras for joint multi-RX RT scenes.
class SignalMetadata¶
@dataclass(frozen=True, slots=True)
class SignalMetadata:
family: str
class_name: str
class_taxonomy: tuple[str, ...]
generator_name: str
device_id: str | None
sample_rate_hz: float
bandwidth_hz: float
realized_carrier_hz: float
start_sample: int
duration_samples: int
snr_db: float
extras: dict[str, object] = field(default_factory=dict)
schema_version: int = 1
Per-signal ground truth. Carried by every Signal: one for each emitter, one for each component of a scene. Populated by the producing layer; immutable after.
Fields¶
Field |
Type |
Set by |
Purpose |
|---|---|---|---|
|
str |
emitter |
Top-level emitter family, e.g. |
|
str |
emitter |
Specific class within the family, e.g. |
|
tuple[str, …] |
emitter |
Hierarchical path, e.g. |
|
str |
emitter |
Concrete class name of the producing emitter |
|
str | None |
scene composer |
Stable virtual-device identifier for fingerprinting |
|
float |
emitter |
Sample rate the IQ was synthesized at |
|
float |
emitter |
Occupied bandwidth |
|
float |
emitter |
Absolute carrier frequency in Hz. Used by BaseChannel for Doppler scaling and by BaseRXMixer to compute the per-RX downconversion offset |
|
int |
scene composer |
Inclusive start in the emitter’s IQ buffer |
|
int |
emitter |
Number of samples this signal occupies |
|
float |
channel |
|
|
dict[str, object] |
any |
Backend-specific extras. The on-disk schema preserves these |
|
int |
constructor |
Storage schema version. Default |
Per-impairment values live in extras¶
Per-device fingerprint values (CFO, IQ imbalance, PA, phase noise) are not inline fields. They are threaded through SignalMetadata.extras["fingerprint_params"] as a dict whose keys are documented in rfgen.channels.protocols.FINGERPRINT_PARAM_KEYS:
("cfo_hz", "iq_imbalance_db", "iq_imbalance_rad",
"pa_p", "pa_a", "phase_noise_db_per_rt_hz")
The Layer 3 device-fingerprint module produces these values from FingerprintParams (Pydantic v2). tx-impairments and rx-frontend transformations consume them only via these documented keys, so a device-fingerprint field rename is caught by a contract test rather than silently changing the shape scene-composer threads through.
Notes¶
Field names mirror TorchSig where applicable. Hz-suffixed and sample-suffixed variants are used here for unambiguous units; both forms exist in the on-disk schema. See TorchSig Interop.
Carrier is absolute.
realized_carrier_hzis the absolute RF carrier (e.g.2.412e9for Wi-Fi channel 1). There is no scene-level frequency anchor; per-emitter and per-RX frames meet at the BaseRXMixer step in Group.RX_CAPTURE.
class SceneMetadata¶
@dataclass(frozen=True, slots=True)
class SceneMetadata:
scene_id: str
duration_s: float
num_emitters: int
num_rx: int
rx_index: int | None
rx_pose: GeometryPose | None
rx_antenna_id: str | None
scene_geometry_hash: str | None
material_db_hash: str | None
antenna_pattern_hash: str | None
geometry_asset_refs: tuple[GeometryAssetRef, ...]
channel_realization_seed: int | None
realized_emitter_count: int
realized_snr_db_stats: dict[str, float]
realized_class_histogram: dict[str, int]
realized_cochannel_overlap_rate: float
realized_spectral_occupancy: float
extras: dict[str, object] = field(default_factory=dict)
schema_version: int = 1
Scene-level ground truth, with per-RX context. One per Record. SceneMetadata deliberately does not expose center_freq_hz or bandwidth_hz at the scene level (the no-shared-scene-RF-anchor decision); RF context lives on per-signal SignalMetadata, on the typed rx_pose, and on the resolved geometry-asset refs preserved for RT and Mitsuba-backed scenes.
Fields¶
Field |
Type |
Purpose |
|---|---|---|
|
str |
Stable identifier shared across records produced from one scene |
|
float |
Scene duration in seconds |
|
int |
Realized emitter count |
|
int |
Number of receivers in the parent scene |
|
int | None |
Index of this RX within the parent scene; |
|
|
Typed RX pose in scene-frame coordinates; populated for single-RX records whenever the resolved receiver carries a pose, |
|
str | None |
Antenna pattern identifier within the antenna-pattern blob |
|
str | None |
SHA-256 of the active scene-geometry asset, typically a Mitsuba XML bundle or a compatibility hash for a typed built-in scene ref; |
|
str | None |
SHA-256 of the material database blob |
|
str | None |
SHA-256 of the antenna pattern blob |
|
|
Resolved typed asset refs active for this scene. Preserved even for URI-only configs via compatibility refs synthesized by the composer. |
|
int | None |
Seed reproducing this exact channel realization, given the geometry and material hashes |
|
int |
Audit field: realized emitter count |
|
dict[str, float] |
Audit field: SNR statistics; required keys |
|
dict[str, int] |
Audit field: |
|
float |
Audit field: realized overlap rate in |
|
float |
Audit field: realized spectral occupancy in |
|
dict[str, object] |
Backend-specific extras |
|
int |
Storage schema version. Default |
The hash fields are content-addressed references into the dataset’s sibling assets/ directory; see Concepts / Records, Receivers, and Assets for the storage layout and reconstruction contract.
extras["receivers"] receiver-catalog contract¶
When num_rx > 1, SceneMetadata.extras may carry a receiver catalog under
"receivers". The shipped scene composer writes one entry per configured
receiver, in the same order as the resolved receiver list:
extras["receivers"] = [
{
"rx_id": str,
"position_m": tuple[float, float, float],
"orientation": tuple[float, float, float, float],
"antenna_id": str | None,
},
...
]
Contract notes:
The list index is the stable receiver index. Entry
extras["receivers"][i]describes receiveri.For a per-RX record, recover the originating configured receiver with
extras["receivers"][scene.rx_index].For a joint multi-RX record,
scene.rx_index is None; the catalog is still present but no single entry is the “active” receiver.Single-RX records do not need this catalog; the current shipped composer omits it when
num_rx == 1.
The realized_* audit fields are read by name by validation-and-audit; any rename or type change is a schema_version bump.
class Signal¶
@dataclass(frozen=True, slots=True)
class Signal:
iq: torch.Tensor
metadata: SignalMetadata | SceneMetadata
component_signals: tuple["Signal", ...] = ()
The universal currency of the channel pipeline. An IQ tensor plus its metadata, optionally with component_signals for nested multi-emitter scenes.
Invariants¶
iqhas dtypetorch.float32, shape(2, N)for one receiver or(num_rx, 2, N)for joint multi-RX records.A per-signal
Signalcarries SignalMetadata; a scene-levelSignalcarries SceneMetadata.Mutating any field after construction raises
dataclasses.FrozenInstanceError.component_signalsis atuple[Signal, ...](recursive); the type annotation matches the actual runtime type.
class Record¶
@dataclass(frozen=True, slots=True)
class Record:
iq: torch.Tensor
scene: SceneMetadata
emitters: tuple[SignalMetadata, ...]
bboxes: tuple[BBox, ...]
seg_mask: torch.Tensor | None = None
text: dict[str, object] | None = None
schema_version: int = 1
One sample produced by the pipeline. Record is what the storage layer persists.
Fields¶
Field |
Type |
Purpose |
|---|---|---|
|
torch.Tensor |
Scene IQ; shape |
|
Scene-level ground truth |
|
|
tuple[SignalMetadata, …] |
Per-emitter metadata, parallel to |
|
tuple[BBox, …] |
Time-frequency bounding boxes, one per emitter component |
|
torch.Tensor | None |
|
|
dict | None |
Inference-grounded annotations (caption / qa / reasoning / scene_report); |
|
int |
Storage schema version. Default |
Record does not carry a pre-computed spectrogram field. When a labeler needs a spectrogram, it computes one from iq at label time. The Record shape in rfgen.core_types is (iq, scene, emitters, bboxes, seg_mask, text, schema_version).
Notes¶
Two-phase generation. A freshly produced
Recordfrom Phase 1 hastext=None. The annotator (Phase 2) reads emitter and scene metadata, generates text, and produces a StoredRecord-shaped output viaStoredRecord.from_record.Record IDs are content-hashed by default. See Storage Layout.
class StoredRecord¶
@dataclass(frozen=True, slots=True)
class StoredRecord:
scene: SceneMetadata
emitters: tuple[SignalMetadata, ...]
bboxes: tuple[BBox, ...]
seg_mask: torch.Tensor | None = None
text: dict[str, object] | None = None
schema_version: int = 1
@classmethod
def from_record(cls, record: Record) -> "StoredRecord": ...
A Record minus IQ. StoredRecord.from_record(record) is the core-type constructor that drops IQ. The annotator pipeline then applies whitelist_filter(record, sample_id=...) to build this same StoredRecord shape with prompt-safe metadata only, including the validated sample id in scene.extras["sample_id"].
StoredRecord lives in rfgen.core_types (rather than in annotators) so Layer 8 (annotators) and later validation or audit consumers can import it from below without a backward-layer typedef edge.
class ChunkedSignal¶
@dataclass(frozen=True, slots=True)
class ChunkedSignal:
num_rx: int
duration_samples: int
chunk_samples: int
def __iter__(self) -> Iterator[tuple[int, torch.Tensor]]: ...
def materialize(self) -> torch.Tensor: ...
Streaming wrapper for scenes too large to hold in RAM. Produced by the scene composer when the materialized scene IQ would exceed the configured chunk threshold. Iterating yields (chunk_offset, chunk_iq) pairs; each chunk_iq is a torch.float32 tensor of shape (num_rx, 2, chunk_samples) (the Signal.iq layout).
Memory bound¶
The RAM footprint per yielded chunk is chunk_iq.element_size() * chunk_iq.numel() == 4 * num_rx * 2 * chunk_samples bytes. The contract test asserts the tracemalloc.get_traced_memory() peak during one full iteration is bounded above by 2 * (4 * num_rx * 2 * chunk_samples) bytes (one chunk in flight plus the iterator’s local references), regardless of duration_samples.
ChunkedSignal is a frozen dataclass with slots=True; subclassing to override __iter__ is awkward and discouraged. The intended extension pattern is composition (decorator or adapter): a concrete producer wraps a ChunkedSignal instance and delegates field access while replacing the chunk-yielding loop. The default __iter__ yields torch.zeros chunks as a placeholder for the materialization-from-storage iterator that ships with Layer 6 storage.
__iter__()¶
Yields (chunk_offset, chunk_iq) pairs of length chunk_samples (the last chunk may be shorter). Channels consume the iterator and apply impairments per chunk.
materialize()¶
Concatenates all chunks into a single torch.Tensor of shape (num_rx, 2, duration_samples). Only safe when the full scene fits in memory; intended for tests and small-scene inspection rather than the streaming path.
Schema versioning¶
SignalMetadata, SceneMetadata, and Record each carry an integer attribute schema_version: int = 1 (literal default 1 at module ship time). StoredRecord carries the same attribute.
Adding a new optional field to any of these dataclasses keeps
schema_versionat its current value.Removing a field, changing a field’s type, or renaming a field requires bumping
schema_versionby+1and is a breaking change for storage round-trip tests.
See Also¶
API Reference: the ABCs that produce and consume these types.
IQ Layout Policy: in-memory and on-disk IQ shapes.
Label Schema: on-disk representation of
BBoxandseg_mask.TorchSig Interop: round-trip with TorchSig’s
SignalMetadataObject.