Coordinate Systems

A multi-emitter scene puts emitters at different RF carriers and receivers tuned to their own local oscillators (LOs) and sample rates; bounding-box labels live in time-frequency rectangles tied to a specific receiver. Without consistent rules for which time and frequency frame each value lives in, none of these pieces compose: an emitter generated at 2.4 GHz, a receiver tuned to 2.45 GHz, and a label drawn in receiver-relative coordinates would be ambiguous. This page names the frames the framework uses, says which object owns each, and points at where one frame becomes another in the pipeline.

What This Page Covers

  • The two named frequency frames and which entity owns each.

  • Sample rate frames per emitter and per receiver.

  • The scene-rate time coordinates used for placed events and receiver records.

  • Label coordinate conventions in the receiver frame.

  • TorchSig boundary adapter notes.

Orientation: From Samples to Records

Read the time axis in this order before following the frequency frames below:

  • A sample is one complex IQ value at a particular rate.

  • An event is one generated emitter transmission, represented by a contiguous run of scene-rate samples after it is accepted for placement.

  • A scene is the logical result of composing a fixed-duration receiver capture: its receiver IQ, scene metadata, and accepted event components.

  • A record is a persisted training or evaluation item derived from that scene after labels are attached. One scene publishes one record; a multi-receiver scene carries each receiver as its own named subtree inside it.

The high-level timing invariant is strict containment: an accepted event’s scene-rate interval lies wholly within the fixed scene capture, 0 <= start <= scene_samples - event_samples. The detailed policy, including full-scene defaults and channel-induced tails, belongs to Scene Capture Boundaries. The sections below define which sample rate and coordinate frame those quantities use; they do not add another placement policy.

Frequency Frames

Two frequency frames coexist. rfgen stores one absolute carrier frequency per emitter and one tuned center frequency per receiver. Stored SceneMetadata does not carry a separate scene-wide RF anchor between them, though SceneConfig.center_hz still exists as a scene-construction default and fallback.

The two frames

Frame

Owner

Field

Purpose

Emitter (absolute RF)

Composer-written SignalMetadata on the placed component

realized_carrier_hz

Absolute carrier frequency in Hz where this emitter is placed (e.g., 2.412e9 for Wi-Fi channel 1). In scene composition, the composer writes SceneConfig.center_hz + f_offset_hz after placement.

Receiver (RX baseband)

ReceiverConfig

center_freq_hz

Receiver’s local-oscillator (LO) frequency. Maps to 0 Hz in the receiver’s IQ buffer.

SceneMetadata does not carry center_freq_hz or bandwidth_hz. Emitters and receivers express RF location independently, and frequency-frame transitions happen explicitly inside the receiver frontend at the BaseMixerStage step on the receiver’s capture plane (ReceiverStagePlane.CAPTURE) (see Frame transitions below).

Frame transitions

The frame transition happens at one place: BaseMixerStage at the head of the receiver’s capture plane. In-band means the receiver bandwidth overlaps the emitter bandwidth. The mixer multiplies the emitter IQ by a complex sinusoid so the emitter appears at the correct offset inside the receiver’s baseband, the complex IQ frame where 0 Hz is the receiver’s tuned center frequency:

delta_f_hz = emitter.realized_carrier_hz - rx.center_freq_hz
component_in_rx_frame(t) = component(t) * exp(j * 2 * pi * delta_f_hz * t)

At emitter generation, samples are requested-rate emitter-baseband IQ. The current scene composer invokes emitters with sample_rate=scene_cfg.sample_rate_hz and f_offset_hz=0.0, so protocol or capture metadata is pre-placement context rather than the final scene carrier. After TX impairments, the composer resamples to the scene/channel rate, draws a scene-relative frequency offset, shifts IQ by it, and stamps realized_carrier_hz = SceneConfig.center_hz + f_offset_hz on the emitted waveform. It prepares any per-emitter or geometry-backed propagation for that waveform before drawing its time start. The eventual start remains a scene-time coordinate: it controls where the already-prepared received result is mixed, not the emitter or receiver frequency frame. The RX mixer then creates receiver-baseband IQ at offset delta_f_hz from 0 Hz so the following BaseIFFilterStage can apply its response in the correct frequency range.

Source: GNU Radio’s Frequency Xlating FIR Filter documents the receiver-side frequency-translation plus FIR channel-selection pattern used by the RX mixer and IF filter. The detailed rfgen RX-capture contract is in Channels / RX Capture.

When each field is read

  • Emitter generation (BaseEmitter.generate): is requested at the scene/channel rate and may carry protocol/capture frequency metadata. A plugin may return another rate, which the composer normalizes. In the current scene composer it is invoked with f_offset_hz=0.0; that metadata is not the final placed-scene carrier.

  • Scene frequency placement: after TX transformations and resampling, the composer draws a relative f_offset_hz, shifts IQ by it, and writes metadata.realized_carrier_hz = scene_cfg.center_hz + f_offset_hz. This is the absolute carrier used by later stages.

  • Channel (BaseChannel, Group.CHANNEL): reads metadata.realized_carrier_hz for carrier-dependent propagation effects such as wavelength scaling and Doppler. Per-emitter and geometry-backed propagation is prepared from this emitted waveform before the time planner draws a start; a statistical backend in scene mode instead runs after receiver summation.

  • RX mixer (BaseMixerStage): computes the offset (emitter.realized_carrier_hz - rx.center_freq_hz) and applies it.

  • Labels: the label stage computes bounding boxes in the receiver’s frame. Bbox frequency limits are receiver-relative offsets from rx.center_freq_hz.

  • Out-of-band check: if |emitter.realized_carrier_hz - rx.center_freq_hz| > rx.bandwidth_hz/2 + emitter.bandwidth_hz/2, this RX does not hear this emitter; the component is dropped from the RX’s chain and omitted from that receiver’s subtree.

Sample Rate Frames

Three rate frames exist; the framework uses each at one stage.

Rate

Where

Why

Emitter-returned rate (R_emitter)

A plugin may return this in SignalMetadata.sample_rate_hz even though the composer requested R_channel. The composer immediately normalizes it.

Supports a plugin whose waveform implementation has a distinct internal rate without changing placed-scene coordinates.

RX rate (R_rx)

Effective receiver rate: ReceiverConfig.sample_rate_hz when set, otherwise SceneConfig.sample_rate_hz. Not duplicated onto SceneMetadata; consumers recover it from the receiver that produced the record or from the config used to generate the dataset.

Each receiver’s ADC samples at its own rate.

Channel rate (R_channel)

SceneConfig.sample_rate_hz; it is requested from emitters and becomes the component rate after TX impairments. A plugin-returned R_emitter is resampled to this rate before frequency placement and propagation.

One shared buffer rate makes frequency placement, propagation, and scene composition consistent. Sample-rate-dependent fading, discrete delay positions, and label timing must refer to the rate of the buffer they describe.

The composer performs a plugin-returned R_emitterR_channel transition, when needed, before per-emitter propagation preparation and time-start drawing. The BaseResamplerStage step on the receiver’s capture plane performs the later R_channelR_rx transition for each receiver.

Stored scene metadata does not carry a scene-wide sample-rate field. SceneConfig.sample_rate_hz still exists at construction time: it sizes the scene/master buffer and supplies the fallback receiver rate when a receiver omits sample_rate_hz.

Source: Sionna’s TR 38.901 channel model APIs expose sampling frequency as a channel-model input, while SciPy’s signal.resample_poly documents the polyphase FIR resampling pattern used by the default RX resampler. The propagation handoff is described in Channels / Channel Propagation.

Time Frames

Time placement is in seconds at scene level; sample-indexed inside each receiver’s subtree of the record. This page owns the meaning and rate frame of those coordinates. The separate Scene Capture Boundaries page owns the rule that decides whether a generated event is allowed to occupy a particular interval of that coordinate system. For the per-field contract a published record carries — and the arithmetic that puts a radar pulse and a communications sample on one clock — see Time coordinates.

Quantity

Frame

Owner

Emitter-local time

Inside one emitter’s IQ buffer, starting at 0.

Emitter before composition; a returned R_emitter is normalized to R_channel.

Scene time

Seconds within the scene, shared across emitters and receivers.

Scene composer (SceneMetadata.duration_s).

Component placement index

Absolute sample index in the placed scene/channel buffer at R_channel.

Scene composer (SignalMetadata.start_sample on a placed component).

Per-RX sample index

Sample index within this RX’s IQ buffer at R_rx.

The receiver’s own subtree of the record. The scene composer sizes the initial pre-RX scene/master buffer with int(round(scene.duration_s * scene_cfg.sample_rate_hz)). When the post-sum path preserves duration, it sizes the final per-receiver buffer with int(round(scene.duration_s * effective_rx_sample_rate_hz)). Python’s round() is the shipped policy, so exact .5 ties go to the nearest even integer.

SignalMetadata.start_sample is the placed component’s absolute scene/channel- rate coordinate, not an emitter-native sample index. The label stage maps the placed component’s time extent into the receiver’s R_rx sample indexes as needed.

Component-to-record time mapping

Component metadata is provenance for the placed event: its start_sample, duration_samples, and sample_rate_hz remain in the scene/channel frame R_channel. A receiver record can have a different active rate R_rx after RX capture resampling, so a bbox cannot reuse those integer indexes directly.

For the component’s half-open channel-rate interval [start, stop), where stop = start + duration, the labeler writes the enclosing receiver-record interval:

scale          = R_rx / R_channel
record_start   = floor(start * scale)
record_stop    = ceil(stop * scale)
bbox interval  = [record_start, record_stop)

The outward rounding preserves every time point represented by the component when it is expressed on the receiver grid. The labeler rejects a converted interval that is empty or outside the fixed receiver capture; it does not crop it. Bounding boxes use that receiver-rate interval, and segmentation rasterizes the same bbox geometry. Thus label time coordinates always describe the IQ a model receives, while the per-event metadata remains audit provenance at the scene/channel rate. Equal rates preserve the same integer interval.

This conversion is intentionally separate from event placement. Strict containment is decided first at R_channel from the pre-propagation emitted footprint by Scene Capture Boundaries; conversion to R_rx does not relax that rule or create a new event boundary policy. A prepared propagation result can have a later channel tail, but only that tail may be clipped by the fixed receiver window. Labels explains how the mapped bbox then becomes segmentation cells, and Core Types identifies which stored fields carry provenance versus receiver-record labels.

Swept-Frequency Emitters

Some waveforms intentionally move their energy across frequency over time. A chirp is a sweep with frequency changing continuously; a hop schedule jumps among discrete frequencies.

  • LoRa chirp spread spectrum (CSS),

  • frequency-modulated continuous-wave (FMCW) radar,

  • other chirp-like or hop-like waveforms whose slope or hop schedule is part of the modulation.

The sweep is intrinsic to the emitter’s IQ: its slope or hop schedule is not re-synthesized by the scene composer. A direct emitter call may report a provisional center-of-sweep carrier. In a composed scene, the composer applies one whole-waveform scene-relative placement shift after TX impairments and resampling, then stamps the final realized_carrier_hz = SceneConfig.center_hz + f_offset_hz. The receiver then hears whatever portion of that placed sweep falls within its capture window.

Source: TorchSig documents LFM and chirp spread spectrum signal builders in its signal-generation API, and SciPy’s signal.chirp documents frequency-swept cosine generation. LoRa and radar backend-source choices are catalogued in Emitter Library Landscape.

Label Implications

Labels live in the receiver’s frame, alongside the receiver subtree they describe.

  • Bbox frequency limits are receiver-relative: low_freq_in_rx = (emitter.realized_carrier_hz - rx.center_freq_hz) - emitter.bandwidth_hz/2.

  • Bbox time limits are sample-indexed at rx.sample_rate_hz, using the half-open conversion in Component-to-record time mapping.

  • Per-event metadata recorded on the label retains realized_carrier_hz (absolute), sample_rate_hz, start_sample, and duration_samples at the placed/channel rate, so the original placement and RF position are recoverable.

A detector trained on one receiver’s IQ sees the same coordinate system the labeler wrote: receiver baseband, receiver rate.

TorchSig Boundary Adapter

TorchSig, the RF machine-learning (RFML) signal-generation and dataset toolkit rfgen interoperates with, enters this page only at the adapter boundary. See Reference / TorchSig Interop for the field-level mapping. Inside rfgen, carriers stay absolute on SignalMetadata.realized_carrier_hz and bboxes stay in the active receiver’s baseband frame.

Use this boundary rule:

  • Importing from TorchSig: if the TorchSig artifact already stores an absolute carrier, copy it into realized_carrier_hz. If it stores a carrier relative to a documented dataset anchor F_anchor_hz, resolve it immediately as realized_carrier_hz = F_anchor_hz + center_freq_relative_hz.

  • Exporting to TorchSig: keep rfgen’s internal metadata unchanged, then convert only at the adapter boundary. If the target TorchSig format expects relative frequency coordinates, write center_freq_relative_hz = realized_carrier_hz - F_anchor_hz. If it expects absolute frequency, pass realized_carrier_hz through unchanged.

The conditional anchor logic belongs only in the adapter. Core rfgen types and the channel pipeline never carry a dataset anchor. The exact field-level interop mapping lives in TorchSig Interop.

Source: TorchSig’s SignalMetadata documents center_freq, bandwidth, start_in_samples, duration_in_samples, and derived frequency-edge fields. rfgen’s exact import/export mapping is the local contract in Reference / TorchSig Interop.

Minimal Example

from rfgen.config import ReceiverConfig
from rfgen.core.types import SignalMetadata

# Wi-Fi 11g emitter on channel 6 (2.437 GHz)
wifi_meta = SignalMetadata(
    realized_carrier_hz=2.437e9,
    bandwidth_hz=20e6,
    sample_rate_hz=20e6,
    start_sample=0,
    duration_samples=8_000_000,
    # ... other fields
)

# Receiver tuned 5 MHz below the emitter's carrier, so the emitter lands
# off-center inside the receiver's baseband frame instead of at 0 Hz.
rx_cfg = ReceiverConfig(
    rx_id="rx0",
    center_freq_hz=2.432e9,
    bandwidth_hz=20e6,
    sample_rate_hz=20e6,
)

See Also