RX Capture¶
Scientific validation
The RX capture transformations named below have been scientifically validated against published references. See the per-component reports:
RX mixer, IF filter, resampler: validated with documented limitations.
RX LNA noise, ADC quantization, AGC: validated with documented limitations.
Receiver background: validated with documented limitations.
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 |
ABC |
Default backend |
|
|---|---|---|---|
RX LO frequency error |
|
LOFrequencyErrorStage: independent constant receiver-oscillator error in Hz; distinct from intended tuning |
|
Mix to RX frame |
|
Shift by the intended carrier-to-receiver-centre difference |
|
IF / bandpass filter |
|
Limit the summed receiver buffer with a finite impulse response (FIR) filter |
|
Resample to |
|
Polyphase rational resampling of the summed receiver buffer |
|
LNA noise injection |
|
Additive complex Gaussian noise with power |
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_ERROR → RX_MIXER → IF_FILTER → RESAMPLER →
LNA_NOISE → ADC → RX_PHASE_NOISE → RX_IQ_IMB → AGC.
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_hzin 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 torx.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:
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:
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¶
Channels overview: semantic channel groups and the sum-point location.
Channel propagation: propagation that produces the per-component inputs to RX capture.
RX hardware: ADC and RX hardware effects that follow RX capture.
RX frontend API: full RX ABC and constructor contracts, including BaseLOFrequencyErrorStage.
Primary RX frontend validation: physical order, determinism, provenance, and operating boundaries.
Reference / RF frontend models: full receiver-chain derivations (Friis cascade, noise figure, kT0B identity).
Reference / Noise floor table: kT0B values per bandwidth and noise figure used to validate LNA noise.
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.
GNU Radio. Frequency Xlating FIR Filter. https://wiki.gnuradio.org/index.php/Frequency_Xlating_FIR_Filter (frequency translation plus FIR channel selection)
SciPy.
scipy.signal.resample_poly. https://docs.scipy.org/doc/scipy/reference/generated/scipy.signal.resample_poly.html (polyphase FIR upsample, filter, downsample behavior)Crochiere, R. E. and Rabiner, L. R. Multirate Digital Signal Processing, Prentice Hall, 1983. (polyphase rational resampler)
Vaidyanathan, P. P. Multirate Systems and Filter Banks, Prentice Hall, 1993. (polyphase FIR and filter-bank design)
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)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)
Nyquist, H. Thermal Agitation of Electric Charge in Conductors, Phys. Rev., 1928. https://doi.org/10.1103/PhysRev.32.110 (kT0B Johnson-Nyquist floor)
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)