Coordinate Systems

Warning

Pre-implementation. This page describes proposed contracts. Class signatures, parameter types, config field names, and behavior are subject to change before code lands. Once implementation exists, content here will be regenerated from docstrings or sourced from running tests.

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.

  • Per-emitter time placement.

  • Label coordinate conventions in the receiver frame.

  • TorchSig boundary adapter notes.

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)

SignalMetadata

realized_carrier_hz

Absolute carrier frequency in Hz where this emitter actually transmits (e.g., 2.412e9 for Wi-Fi channel 1). Set by the emitter from its protocol and configuration.

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 channel pipeline at the BaseRXMixer step in Group.RX_CAPTURE (see Frame transitions below).

Frame transitions

The frame transition happens at one place: BaseRXMixer at the head of Group.RX_CAPTURE. 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)

Before the mixer, sample values remain emitter-baseband IQ; realized_carrier_hz records the absolute RF carrier as metadata only. After the mixer, samples are in the receiver’s baseband, positioned at offset delta_f_hz from 0 Hz so that the BaseIFFilter step that follows can apply the receiver’s anti-aliasing response to 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): the emitter sets metadata.realized_carrier_hz from its configuration (modulation, channel number, scenario). Samples are returned at emitter baseband; the carrier is metadata only.

  • Channel (BaseChannel, Group.CHANNEL): reads metadata.realized_carrier_hz because Sionna channel models use carrier frequency when computing motion-related frequency shift (Doppler) and, for Sionna ray tracing (RT), wavelength-dependent propagation effects. The current composer applies propagation per (emitter, RX) pair; any future batching must preserve that observable contract.

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

  • Labels: the per-RX 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 record.

Sample Rate Frames

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

Rate

Where

Why

Emitter native rate (R_emitter)

Set by the emitter plugin’s generate(). Carried as SignalMetadata.sample_rate_hz.

Each waveform has a natural rate (Wi-Fi 11g at 20 megasamples per second, Bluetooth Low Energy (BLE) at 4 megasamples per second, 5G New Radio (NR) 100 MHz at 122.88 megasamples per second).

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 (between TX impairments and the RX mixer)

Sionna’s channel call accepts any rate; the framework runs the channel at R_emitter.

One rate per channel call simplifies Sionna RT and CDL batching.

The BaseResampler step in Group.RX_CAPTURE is the one place where the rate transitions from R_emitter to R_rx.

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 in the per-RX record.

Quantity

Frame

Owner

Emitter-local time

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

Emitter (SignalMetadata.duration_samples at R_emitter).

Scene time

Seconds within the scene, shared across emitters and receivers.

Scene composer (SceneMetadata.duration_s).

Per-RX sample index

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

Per-RX 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-RX record 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 recorded in the emitter’s native rate. The label stage converts to per-RX sample indexes per record.

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.

These emitters set realized_carrier_hz to the center of the sweep and generate IQ at their native rate. The downstream pipeline does not re-mix them; the sweep is intrinsic to the emitter’s IQ. The receiver hears whatever portion of the 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 on the per-RX record, in the receiver’s frame.

  • 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.

  • Per-emitter metadata recorded on the label retains realized_carrier_hz (absolute) and sample_rate_hz (emitter native), so the absolute RF position is recoverable.

A detector trained on the per-RX 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.core.types import SignalMetadata
from rfgen.config import ReceiverConfig

# 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
)

# A receiver tuned to 2.45 GHz with 80 MHz capture bandwidth
rx = ReceiverConfig(
    rx_id="wifi",
    position_m=(0.0, 0.0, 1.5),
    center_freq_hz=2.45e9,
    bandwidth_hz=80e6,
    sample_rate_hz=80e6,
)

# In-band check
delta_f_hz = wifi_meta.realized_carrier_hz - rx.center_freq_hz   # -13e6
in_band = (
    abs(delta_f_hz) - wifi_meta.bandwidth_hz / 2
    < rx.bandwidth_hz / 2
)                                                                  # True

# RX mixer shifts the emitter into rx baseband
# After the mixer, the emitter's spectrum is centered at -13 MHz inside rx baseband.

See Also