RX Capture

Scientific validation

The RX capture transformations named below have been scientifically validated against published references. See the per-component reports:

SampleRateOffsetStage falls outside the scope of all three reports and has no validation report of its own yet.

Each report covers construct validity, mathematical correctness against cited equations, empirical comparison to published reference numbers, literature grounding, experimental methodology, operating envelope, and documented limitations.

Receiver capture, abbreviated RX capture, is the receiver’s capture plane (ReceiverStagePlane.CAPTURE), the first of the two planes the shared receiver frontend chain runs after the two pipeline groups, TX and CHANNEL. The composer first sums propagated components into one receiver-local IQ buffer. The configured RX-capture stages then run on that post-sum buffer: receiver LO error, intended tuning, filtering, resampling, and LNA thermal noise. Together with RX hardware they form rfgen’s core-only nine-stage post-sum receiver chain; TorchSig is not an RX channel backend.

In this page, R_rx is the receiver’s configured sample rate. Baseband IQ means complex-valued in-phase/quadrature samples whose zero-frequency bin is a reference point rather than the physical carrier itself. This page calls a component receiver-visible when its occupied frequency interval intersects the receiver’s capture interval before the composer forms the receiver buffer. That visibility predicate is deliberately different from strict placement’s PlacementProvenance.in_band, which requires the complete guarded interval to be contained by the capture.

Strict occupied-band placement

Carrier selection and capture admission are separate decisions. A frequency strategy proposes a receiver-baseband carrier; use rfgen.placement.BandwidthSafePlacement when a job must retain only signals whose measured occupied interval fits the capture. The wrapper reads the canonical signal.metadata.extras["occupied_interval"] field (and accepts the retained occupied_interval_hz compatibility key). See measured waveform evidence for how waveform and SigMF-capture sources produce that metadata. With B as the capture bandwidth and g as an optional edge guard, it requires:

[carrier_hz + low_hz - g, carrier_hz + high_hz + g] is within [-B/2, B/2]

Strict mode is the default. It rejects a missing interval with occupied_interval_unavailable and an interval outside the capture with occupied_band_out_of_capture; both errors carry machine-readable context. This deliberately does not replace an unavailable measurement with the emitter’s requested bandwidth. allow_clipping=True is an explicit recovery mode: it records clipped: true and preserves the original measured interval in artifacts/plans/<scene_id>/placement.json. This is an opted-in carrier-clamping policy record, not physical waveform clipping or receiver filtering. A waveform wider than the capture remains marked PlacementProvenance.in_band: false, because no carrier shift can make it fit; it is not a successful strict admission.

Overview

Name

Transformation

ABC

Default backend

RX LO frequency error

RX_LO_FREQUENCY_ERROR

BaseLOFrequencyErrorStage

LOFrequencyErrorStage: independent constant receiver-oscillator error in Hz; distinct from intended tuning

Mix to RX frame

RX_MIXER

BaseMixerStage

Shift by the intended carrier-to-receiver-centre difference

IF / bandpass filter

IF_FILTER

BaseIFFilterStage

Limit the summed receiver buffer with a finite impulse response (FIR) filter

Resample to R_rx

RESAMPLER

BaseResamplerStage

Polyphase rational resampling of the summed receiver buffer

LNA noise injection

LNA_NOISE

BaseThermalNoiseStage

Additive complex Gaussian noise with power kTB * 10^(NF_dB/10); symbols are defined in LNA Noise Injection

The Transformation enum has 15 slots: five TX (11–15), propagation (21), five capture-plane (30–34), and four hardware-plane (41–44). The nine receiver members are the ones for which Transformation.is_receiver is True; they are collected in RECEIVER_TRANSFORMATIONS, and their canonical order is RECEIVER_TRANSFORMATION_ORDER: RX_LO_FREQUENCY_ERRORRX_MIXERIF_FILTERRESAMPLERLNA_NOISEADCRX_PHASE_NOISERX_IQ_IMBAGC. Asking a receiver member for its Transformation.group raises ValidationError, because the receiver is no longer a pipeline group. See primary RX frontend validation for the executable order and evidence.

Note

Sum step. The composer sums propagated emitter components into one receiver buffer before RX capture stages. RX capture and RX hardware then operate once per receiver. The sum is an operational step rather than a Transformation; it has no plugin slot.

The Sum Point

The sum occurs before RX capture. Before this point, each component travels independently through TX impairments and channel propagation. After it, there is a single composite IQ at each receiver.

The sum is composer-driven: it is not a plugin slot and cannot be replaced by a user implementation. It forms the receiver buffer before all RX-capture transformations; filtering and resampling are therefore receiver operations, not per-emitter operations.

Band-Overlap Predicate

Only receiver-visible emitters whose signal band overlaps the receiver’s capture band contribute to the sum. The default visibility predicate is an interval intersection:

emitter_low_hz  = emitter.realized_carrier_hz - emitter_signal_bw / 2
emitter_high_hz = emitter.realized_carrier_hz + emitter_signal_bw / 2
rx_low_hz       = rx.center_freq_hz - rx.bandwidth_hz / 2
rx_high_hz      = rx.center_freq_hz + rx.bandwidth_hz / 2

overlaps = emitter_low_hz < rx_high_hz and rx_low_hz < emitter_high_hz

When emitter signal bandwidth is not tracked separately, the backend may use this center-frequency approximation:

|emitter.realized_carrier_hz - rx.center_freq_hz| < rx.bandwidth_hz / 2

Emitters that fail this predicate are excluded before summation. They contribute nothing to that receiver’s IQ or labels; no drop-reason field is currently persisted in scene metadata.

Sample Rate and Carrier Frame

  • After sum: composite IQ in the receiver path.

  • After RX LO error: an independent constant rotation set by frequency_error_hz in Hz.

  • After RX mixer: IQ shifted by the intended carrier-to-receiver-centre difference.

  • After IF filter: bandlimited for the configured receiver capture.

  • After resampler: resampled to R_rx; DC maps to rx.center_freq_hz.

  • After LNA noise: composite IQ with LNA noise added, at R_rx.

RX hardware receives the output of LNA noise injection and operates entirely at R_rx.

Mix to RX Frame

The RX mixer shifts the summed receiver buffer into the receiver’s frequency frame by applying the complex rotation:

\[ y[n] = x[n] \cdot e^{-j 2\pi f_\mathrm{LO} \, n / R_\mathrm{in}} \]

where \(f_\mathrm{LO} = \texttt{signal.metadata.realized_carrier_hz} - \texttt{ctx.rx_params.center_freq_hz}\) and \(R_\mathrm{in}\) is the buffer’s current sample rate. The negative sign is the downconversion convention.

Here n is an integer sample index and n / R_in is elapsed time in seconds.

A buffer already centered at rx.center_freq_hz passes through unchanged.

Source: GNU Radio’s Frequency Xlating FIR Filter documents the receiver-side frequency-translation pattern. The explicit complex-rotation equation is the MixerStage contract.

IF/Bandpass Filter

The intermediate-frequency (IF) filter applies a low-pass filter after mixing to the summed receiver buffer. The default implementation uses a linear-phase FIR filter; its transition band limits energy that would fold during resampling.

Source: GNU Radio’s Frequency Xlating FIR Filter grounds the paired frequency-translation plus FIR channel-selection model. SciPy’s signal.resample_poly documents the upsample, FIR low-pass, downsample pattern used by the default anti-aliasing path.

Resample to R_rx

The resampler converts the filtered receiver buffer from its current rate to R_rx using a polyphase rational resampler. It runs after summation and does not reconcile distinct per-emitter sample rates.

Source: SciPy’s signal.resample_poly is the implementation reference for polyphase FIR resampling. Crochiere and Rabiner’s Multirate Digital Signal Processing is the textbook grounding for rational-rate conversion.

Sample-rate offset (clock drift)

Resampling to R_rx reconciles two rates that are both known. Sample-rate offset (SRO) is a separate physical effect: the transmitter and receiver clocks are independent oscillators, so they tick at slightly different rates and the receiver ends up sampling on a time grid that is fractionally stretched or compressed. SRO is the timing-axis counterpart of carrier-frequency offset, which leaves sample timing alone and instead puts a slow phase ramp on the received I/Q. Both effects are unavoidable in a real link between two separate radios, so a dataset that models them keeps its signals inside the range a real receiver produces.

The offset is quoted in parts per million relative to the nominal clock rate. Oscillator datasheets use the same ppm unit. Consumer-grade crystal oscillators drift within roughly ±20 ppm, and temperature-compensated parts are tighter; see Fingerprint parameter priors for the sourced ranges, which cite IEEE 802.11-2020 §17.3.9.5 and the TorchSig ClockDrift defaults.

SampleRateOffsetStage applies one fixed offset as a length-preserving fractional resample onto a warped grid, where ratio = 1 + ppm * 1e-6, and scales sample_rate_hz by that ratio. It is a second BaseResamplerStage concrete alongside PolyphaseResamplerStage and reuses Transformation.RESAMPLER. A receiver may hold both concretes; the SampleRateOffsetStage reference gives the ordering that follows from the physics, along with the rest of the contract.

The channel defaults to sample_rate_offset_ppm = 0.0, an identity no-op, and applies one deterministic offset per instance. Varying that offset across a dataset is caller-owned: generator code draws a value per signal and constructs the channel with it.

Source: SciPy’s ndimage.map_coordinates is the interpolation reference for the fractional grid warp.

Opt-in receiver background

Most generation jobs begin with emitted components, so the default scene configuration leaves receiver background disabled. That default is useful for controlled ablations: a zero-emitter scene has exactly zero IQ. Enable scene.receiver_background when a job instead needs a deterministic thermal receiver floor even without transmitters.

The shipped thermal_receiver policy applies the existing linear_lna_noise transformation once per receiver, after the configured RX-capture stages and before RX hardware. It uses the equivalent single-stage kTBF model: noise figure, reference temperature, and effective bandwidth determine a complex Gaussian realization. The current model is not a receiver calibration, an antenna-temperature model, a captured ambient recording, or a multi-stage Friis cascade. ambient_explicit is a provenance category for an enabled policy; it does not silently synthesize an additional environmental source.

Enabled scenes write artifacts/plans/<scene_id>/background.json, recording the resolved policy and measured background-only mean |IQ|^2. The artifact is a reproducibility record, not an estimate of total received scene power. For fields, defaults, and validation errors see the receiver-background configuration reference.

Generate and inspect a background-enabled capture

Use this path when a job needs a reproducible receiver floor in an empty capture: configure the policy, build through the composer, then inspect the immutable record. The policy is appended after configured RX capture and before RX hardware. It adds an independent contribution and does not replace a configured ThermalNoiseStage, so configure both only when that double-noise scenario is intentional.

import json
import torch
from pathlib import Path
from rfgen.core.pipeline import ChannelPipeline
from rfgen.config import DensityConfig, ReceiverBackgroundConfig, SceneConfig
import rfgen.scene as rfgen_scene

scene = SceneConfig(
    sample_rate_hz=2e6, bandwidth_hz=1e6, duration_s=0.001,
    density=DensityConfig(min_emitters=0, max_emitters=0),
    receiver_background=ReceiverBackgroundConfig(
        enabled=True, background_type="thermal_receiver", noise_figure_db=3.0,
    ),
)
capture = rfgen_scene.DefaultSceneComposer().build(
    scene_cfg=scene, emitter_pool={}, channel=ChannelPipeline(()),
    rng=torch.Generator().manual_seed(1337), provenance_root="output",
)
artifact = next((Path("output") / "artifacts" / "plans").glob("*/background.json"))
record = json.loads(artifact.read_text())
assert capture.iq.abs().mean() > 0 and record["background_power_w"] > 0

The record stores the shared policy and mean background-only |IQ|^2 across receivers. It is neither a per-receiver calibration trace nor total scene power. See Receiver-background configuration for fields and errors, and Scene API for the policy reader and writer.

LNA Noise Injection

The LNA noise transformation adds thermal noise representing the receiver low-noise amplifier’s contribution. The Johnson-Nyquist formula gives the thermal-noise floor for a resistor over a bandwidth:

\[ P_\text{noise} = k T_0 B \cdot 10^{NF_\text{dB}/10} \]

where \(k\) is Boltzmann’s constant, \(T_0 = 290\,\text{K}\) (IEEE reference temperature), \(B = \texttt{ctx.rx_params.bandwidth_hz}\), and \(NF_\text{dB}\) is the receiver noise figure. This model uses configured capture bandwidth, not sample rate. The transformation log records the effective bandwidth, effective noise power, per-real-component sigma, and whether the small-power floor was applied.

LNA noise runs after the sum so that one noise realization is injected per receiver, not per component. This correctly models the physical situation: the LNA is at the receiver input and sees the composite RF signal.

Source: Nyquist’s thermal-noise derivation (DOI 10.1103/PhysRev.32.110) grounds the Johnson-Nyquist kTB term, and Friis’ receiver-noise paper (DOI 10.1109/JRPROC.1944.232049) grounds noise figure. IEEE Std 145-2013 gives the \(T_0 = 290\) K reference temperature. The rfgen equation is collected with the Friis cascade model in Reference / RF frontend models.

Per-Receiver Parameters

RX capture runs once per receiver. Each receiver carries its own center_freq_hz, bandwidth_hz, sample_rate_hz, and noise_figure_db when those fields are set. When ReceiverConfig.center_freq_hz, bandwidth_hz, or sample_rate_hz is omitted, the composer falls back to scene_cfg.center_hz, scene_cfg.bandwidth_hz, or scene_cfg.sample_rate_hz before building ChannelRxParams. The scene-level capture-plane and hardware-plane receiver stages then run once on that receiver’s summed buffer.

Minimal Example

This example builds a pipeline whose chain is empty and whose nine receiver stages cover both planes for one receiver, plus the matching receiver config. The 5 dB LNA noise figure and 30.72 Msps sample rate are representative example values; exact values come from the receiver model or scenario config.

from rfgen.core.pipeline import ChannelPipeline
from rfgen.config import ReceiverConfig
from rfgen.receiver.stages.analog import (
    IQImbalanceStage,
    RXPhaseNoiseStage,
    ThermalNoiseStage,
)
from rfgen.receiver.stages.conversion import ADCQuantizerStage, AGCStage
from rfgen.receiver.stages.filtering import IFFilterStage, PolyphaseResamplerStage
from rfgen.receiver.stages.frequency import LOFrequencyErrorStage, MixerStage

rx_chain = ChannelPipeline(
    receiver_stages=[
        # Capture plane
        LOFrequencyErrorStage(frequency_error_hz=125.0),
        MixerStage(),
        IFFilterStage(cutoff_norm=0.4, num_taps=65, gain=1.0),
        PolyphaseResamplerStage(up=1, down=1),
        # The composer has already summed the receiver buffer before these stages.
        ThermalNoiseStage(noise_figure_db=5.0),
        # Hardware plane
        ADCQuantizerStage(enob_bits=10),
        RXPhaseNoiseStage(),
        IQImbalanceStage(),
        AGCStage(),
    ],
)

rx_cfg = ReceiverConfig(
    rx_id="rx0",
    center_freq_hz=3.5e9,
    bandwidth_hz=20e6,
    sample_rate_hz=30.72e6,
)

The pipeline above is supplied at the scene level; ReceiverConfig carries only the per-receiver RF and geometry parameters. See Channels overview for the full pipeline including the pre-sum chain.

See Also

References

The RX mixer, IF filter, and resampler use frequency-translation, FIR channel-selection, and polyphase-resampling identities; LNA noise is the Johnson-Nyquist thermal-noise floor scaled by receiver noise figure. The math is collected in Reference / RF frontend models and Reference / Noise floor table; the citations below duplicate the inline sources.

  1. GNU Radio. Frequency Xlating FIR Filter. https://wiki.gnuradio.org/index.php/Frequency_Xlating_FIR_Filter (frequency translation plus FIR channel selection)

  2. SciPy. scipy.signal.resample_poly. https://docs.scipy.org/doc/scipy/reference/generated/scipy.signal.resample_poly.html (polyphase FIR upsample, filter, downsample behavior)

  3. Crochiere, R. E. and Rabiner, L. R. Multirate Digital Signal Processing, Prentice Hall, 1983. (polyphase rational resampler)

  4. Vaidyanathan, P. P. Multirate Systems and Filter Banks, Prentice Hall, 1993. (polyphase FIR and filter-bank design)

  5. SciPy. scipy.ndimage.map_coordinates. https://docs.scipy.org/doc/scipy/reference/generated/scipy.ndimage.map_coordinates.html (spline interpolation onto the fractionally warped sample grid used for sample-rate offset)

  6. Friis, H. T. Noise Figures of Radio Receivers, Proc. IRE, 1944. https://doi.org/10.1109/JRPROC.1944.232049 (LNA noise figure, cascade noise factor)

  7. Nyquist, H. Thermal Agitation of Electric Charge in Conductors, Phys. Rev., 1928. https://doi.org/10.1103/PhysRev.32.110 (kT0B Johnson-Nyquist floor)

  8. IEEE Std 145-2013 IEEE Standard for Definitions of Terms for Antennas. https://doi.org/10.1109/IEEESTD.2014.6758443 (reference temperature \(T_0 = 290\) K)