rfgen.labels

Reference surface for the labelers: metadata-only bbox labeling, grid segmentation labeling, and a joint labeler that produces both.

A labeler returns a LabelSet — what it derived, and nothing it was handed.

Every shipped labeler derives its labels from declared emitter metadata — carrier, bandwidth, on-time — and never from the composed samples. That is what makes a label usable as ground truth: it states what the generator put into the scene, not what a transform found afterwards.

Package layout

Module

Holds

rfgen.labels.base

BaseLabeler

rfgen.labels.params

the three labelers’ constructor-parameter models

rfgen.labels.receiver_frame

converting emitter-frame facts into the frame of the receiver that heard them

rfgen.labels.segmentation_grid

rasterizing receiver-frame boxes onto a time-frequency grid

rfgen.labels.results

LabelSet, LabeledScene

rfgen.labels.box_fields

encoding boxes as typed observation fields, with the frame they are in

rfgen.labels.registry

resolving a configured labeler name to its class, and building it

rfgen.labels.bbox

BBoxLabeler

rfgen.labels.segmentation

SegmentationLabeler

rfgen.labels.joint

JointLabeler

Module summary

from rfgen.labels import JointLabeler

labels = JointLabeler(seg_n_fft=1024, seg_hop=256).label(
    iq=scene_iq,
    emitters=component_metadata,
    scene=scene_metadata,
)

assert labels.bboxes
assert labels.segmentation is not None

TorchSig interop

Conversion lives in rfgen.integrations.torchsig.interop and is imported from there:

Export

Purpose

to_torchsig_signal(record)

Convert one single-receiver LabeledScene to a TorchSig 2.1 Signal.

from_torchsig_signal(ts_signal, scene_metadata)

Convert a TorchSig Signal, with caller-supplied rfgen scene context, to a LabeledScene.

See TorchSig interop for the shape contract and what does not survive the round trip.

Class index

Class

Kind

Notes

BaseLabeler

abc

Common pure-function contract for all labelers.

BBoxLabeler

concrete

Derives one receiver-frame bbox per emitter from metadata only.

SegmentationLabeler

concrete

Rasterizes occupancy onto a time-frequency grid.

JointLabeler

concrete

Returns a LabelSet carrying both boxes and a raster.

class rfgen.labels.BaseLabeler

Abstract base class for shipped and plugin labelers.

class BaseLabeler(ABC):
    name: ClassVar[str]

    @abstractmethod
    def label(
        self,
        iq: torch.Tensor,
        emitters: tuple[SignalMetadata, ...],
        scene: SceneMetadata,
    ) -> LabelSet: ...

    @classmethod
    @abstractmethod
    def schema(cls) -> type[BaseModel]: ...

    @classmethod
    def from_config(cls, config: LabelConfig) -> Self: ...

Notes

  • label(...) is the runtime contract. The shipped labelers are pure functions of (iq, emitters, scene).

  • schema() returns the Pydantic constructor schema used by config-driven construction.

  • from_config(...) resolves the configured primary labeler through rfgen.labelers.

Method: label

def label(
    self,
    iq: torch.Tensor,
    emitters: tuple[SignalMetadata, ...],
    scene: SceneMetadata,
) -> LabelSet: ...

Runtime labeler contract. An implementation returns a LabelSet carrying only what it derived.

iq is a shape and device contract, not a source of labels: every shipped labeler derives occupancy from declared metadata. A labeler that needs no tensor may accept None.

class rfgen.labels.BBoxLabeler

Metadata-only bbox derivation.

BBoxLabeler()

Method: label

def label(
    self,
    iq: torch.Tensor | None = None,
    emitters: tuple[SignalMetadata, ...] | None = None,
    scene: SceneMetadata | None = None,
) -> LabelSet

Behavior

  • Does not inspect iq.

  • Returns one BBox per emitter, in emitter order.

  • Resolves bbox frequency bounds in the active receiver frame from scene.extras.

  • Rejects unresolved joint multi-RX scenes when receiver center frequencies are heterogeneous, because one record’s box list can only describe one receiver frequency frame.

  • Raises LabelError on invalid bandwidth, invalid duration, negative start sample, missing receiver center frequency, or bboxes that escape the capture window.

class rfgen.labels.SegmentationLabeler

Time-frequency grid occupancy rasterization.

SegmentationLabeler(
    *,
    seg_n_fft: int = 1024,
    seg_hop: int = 256,
    mode: SegmentationMode = SegmentationMode.SINGLE_LABEL,
    tie_break: SegmentationTieBreak = SegmentationTieBreak.LOWER_EMITTER_INDEX,
)

Method: label

def label(
    self,
    iq: torch.Tensor,
    emitters: tuple[SignalMetadata, ...],
    scene: SceneMetadata,
) -> LabelSet

Behavior

  • Requires iq.

  • Uses BBoxLabeler to derive the metadata bbox list first, then rasterizes those bboxes.

  • Uses iq only to validate rank and capture length, preserve the receiver axis for an unresolved multi-receiver scene, and place the output on the input device. Occupancy is not inferred from IQ amplitude or STFT energy.

  • Resolves the active receiver sample rate from scene.extras["receivers"][scene.rx_index]["sample_rate_hz"], scene.extras["rx_sample_rate_hz"], or the emitter metadata fallback.

  • Single-label mode returns torch.int16 shape [F, T] with -1 for background and class_id for occupied cells.

  • Multi-label mode returns torch.uint8 shape [C, F, T], one binary plane per distinct class in sorted class_id order. Emitters with the same class share the same plane.

  • tie_break=LOWER_EMITTER_INDEX is the shipped single-label overlap rule.

class rfgen.labels.JointLabeler

Default shipped labeler.

JointLabeler(
    *,
    seg_n_fft: int = 1024,
    seg_hop: int = 256,
    segmentation_mode: SegmentationMode = SegmentationMode.SINGLE_LABEL,
    segmentation_tie_break: SegmentationTieBreak = SegmentationTieBreak.LOWER_EMITTER_INDEX,
)

Method: label

def label(
    self,
    iq: torch.Tensor,
    emitters: tuple[SignalMetadata, ...],
    scene: SceneMetadata,
) -> LabelSet

Behavior

  • Requires len(emitters) == scene.num_emitters.

  • Requires iq.

  • Reuses the shipped BBoxLabeler and SegmentationLabeler implementations so the two modalities stay aligned.

  • Rejects unresolved joint multi-RX scenes when scene.extras["receivers"] carries heterogeneous center_freq_hz values, because one record’s box list cannot represent several receiver frequency frames safely.

  • Returns a LabelSet whose bboxes stay parallel to the emitters it was given.

rfgen.labels.registry.build_labeler_suite

def build_labeler_suite(
    config: LabelConfig,
) -> tuple[BaseLabeler, tuple[BaseLabeler, ...]]

Builds the configured primary labeler plus any extra_labelers, applying the shared seg_n_fft, seg_hop, segmentation_mode, and segmentation_tie_break defaults where the target labeler supports them.

class rfgen.labels.LabelSet

@dataclass(frozen=True, slots=True)
class LabelSet:
    bboxes: tuple[BBox, ...] = ()
    segmentation: torch.Tensor | None = None

    def observation_fields(
        self,
        *,
        receiver_center_hz: float,
        sample_rate_hz: float,
        class_names: Mapping[int, str] | None = None,
    ) -> dict[str, BaseObservationField]: ...

What a labeler derived. It replaces the Record earlier versions returned, which carried back the IQ, scene, and emitters the labeler had just been given — a bbox-only labeler had to fabricate a tensor it never read to satisfy that return type.

observation_fields() encodes the clock-free labels as the two typed fields a projection publishes, labels/boxes/extent and labels/boxes/identity. Both frame arguments are required, with no defaults, so a labeler cannot publish frequency edges without saying what they are offsets from. See Label Schema.

The segmentation raster stays a tensor rather than becoming a field here: giving it coordinates needs the capture’s sample rate, time reference, origin, and per-receiver frames, none of which a labeler is given. The projection places it.

class rfgen.labels.LabeledScene

@dataclass(frozen=True, slots=True)
class LabeledScene:
    iq: torch.Tensor
    scene: SceneMetadata
    emitters: tuple[SignalMetadata, ...]
    labels: LabelSet = LabelSet()

One composed scene and the labels derived from it — the in-flight value that travels from composition to projection, and what generate_record returns. It is not a stored record: no schema_version, because that is a storage format’s concern, and no text, because annotation is published as a snapshot’s own annotation set.

Writing a labeler

Register the class under rfgen.labelers and return a LabelSet:

class MyLabeler(BaseLabeler):
    name: ClassVar[str] = "mine"

    @classmethod
    def schema(cls) -> type[BaseModel]:
        return MyParams

    def label(self, iq, emitters, scene) -> LabelSet:
        return LabelSet(bboxes=my_boxes(emitters, scene))

Returning a Record was the contract until the legacy record path was retired, and no longer type-checks.

See Also