Channels¶
Note
Layers 3 and 4 shipped. Layer 3 (emitters, device-fingerprint,
tx-impairments, propagation, rx-frontend) landed on branch
rfgen-impl-2026-06-25-105955 (PR #94). Layer 4 (channel-pipeline)
landed on branch rfgen-impl-2026-06-28-193914 (PR #100), shipping
ChannelPipeline as the
validated aggregator that holds the full transformation chain. The
class names, Pydantic schemas, and Transformation enum members
referenced below match the shipped surface; Pass-1 stubs (GNU Radio
OOT emitters, cellular emitters, Sionna propagation backends)
construct cleanly and raise an EmitterError or ChannelError tagged
with stage="pass1_stub" until backend wiring lands. See
Reference / rfgen.emitters and
Reference / rfgen.channels for the
shipped class roster.
Scientific validation
The channel pipeline transformations and their immutable parameter record have been scientifically validated against published references. See the per-component reports:
Device fingerprint: validated with documented limitations.
TX power-amplifier nonlinearity (Rapp, Saleh): validated with documented limitations.
TX phase noise (Leeson): validated with documented limitations.
TX IQ imbalance, DAC quantization, CFO: validated.
AWGN channel propagation: validated.
RX mixer, IF filter, resampler: validated with documented limitations.
RX LNA noise, ADC quantization, AGC: validated with documented limitations.
RX phase noise and IQ imbalance: validated with documented limitations.
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.
The channel pipeline is the “what happens over the air and inside the radios” part of a scene. Each emitter starts with clean complex baseband IQ, meaning in-phase/quadrature samples centered at 0 Hz while the real carrier frequency stays in metadata. The channel pipeline turns each clean emitted waveform into what each receiver records after transmit (TX) hardware effects, propagation through the environment, receive (RX) band selection, and RX hardware effects. The pipeline is organized by semantic channel groups containing fourteen explicit transformations.
Overview¶
Channel pipeline with three transmitters and two receivers. TX impairments and channel propagation run pre-sum, per emitter; the sum at the head of RX capture collapses the per-(TX, RX) paths into one stream per receiver; RX capture and RX hardware run post-sum, per receiver.¶
The four groups are:
TX impairments (per emitter): transmitter hardware effects before the signal enters the channel: digital-to-analog converter (DAC) quantization, power-amplifier (PA) nonlinearity, TX phase noise, TX IQ imbalance, and carrier frequency offset (CFO).
Channel propagation (per emitter/RX pair): one BaseChannel slot backed by Sionna, NVIDIA’s physical-layer simulation library. Sionna models the signal weakening with distance, arriving through reflected paths, fading behind obstacles, and shifting with motion.
RX capture (per receiver, post-sum): receive-band selection, mixing into the receiver frequency frame, intermediate-frequency (IF) filtering, resampling to the receiver sample rate, summing in-band components, and low-noise-amplifier (LNA) noise injection. This group turns surviving propagated components into one receiver-centered stream.
RX hardware (per receiver): analog-to-digital converter (ADC) quantization, RX phase noise, RX IQ imbalance, and automatic gain control (AGC).
Source: the operation inventory is a summary of the four channel-group subpages. Those pages cite IEEE Std 1241-2010 and Analog Devices MT-001 for converter quantization, Rapp and Saleh for PA nonlinearity, Leeson for phase noise, Razavi, MathWorks, and Analog Devices for IQ imbalance, GNU Radio for carrier-frequency offset and receiver frequency translation, SciPy and Crochiere/Rabiner for resampling, Nyquist, Friis, and IEEE Std 145-2013 for receiver noise, MathWorks and liquid-dsp for AGC, and Sionna plus 3GPP TR 38.901 for channel propagation.
Each emitter carries its absolute carrier as realized_carrier_hz in metadata. Each receiver resolves its effective center_freq_hz, bandwidth_hz, and sample_rate_hz from ReceiverConfig when those fields are set, otherwise from the scene-level defaults on scene_cfg. Stored SceneMetadata does not duplicate those RF fields.
Pipeline overview¶
R_emitter is the emitter component’s native sample rate. R_rx is the receiver sample rate after RX capture resampling. Acronym-heavy rows point to group subpages where each operation is introduced with its API slot and default backend.
Group |
Scope |
Sample rate |
Transformations |
Reference |
|---|---|---|---|---|
per emitter |
|
DAC quantization, PA nonlinearity, TX phase noise, TX IQ imbalance, CFO |
||
per (emitter, RX) pair |
|
Channel propagation |
||
per RX |
|
RX mixer, IF filter, resampler, LNA noise |
||
per RX |
|
ADC quantization, RX phase noise, RX IQ imbalance, AGC |
Per-transformation tables live on each group sub-page. Each row carries the operation name, its Transformation enum member, the ABC, and the default backend.
Place In The System¶
The channel pipeline sits between scene composition and storage. The scene composer assigns emitter positions, realized_carrier_hz, and TX/RX antenna metadata. The channel pipeline receives those assignments and produces per-receiver digitized records. The storage layer writes those records to disk.
Emitters are upstream: they produce clean baseband IQ and place it in metadata before TX impairments run. Labels and annotations are downstream: they read metadata written by the pipeline, including realized_carrier_hz, channel profile parameters, and per-RX SNR estimates.
Boundaries¶
The channel pipeline owns:
All fourteen transformations listed above.
The band-overlap predicate that selects which emitter components reach each receiver.
The sum of in-band propagated components at the head of RX capture.
Per-receiver routing of the post-sum signal through RX capture and RX hardware.
The channel pipeline does not own:
Baseband modulation and pulse shaping: emitter responsibility.
Carrier assignment and time placement: scene-composer responsibility.
Label generation and bounding boxes: labeler responsibility.
Record persistence: storage responsibility.
Sample-rate selection at the emitter: emitters generate at their own native rate; resampling to the RX rate happens in the RX-capture resampler.
Data Flow¶
Before the diagram, scene composition has written realized_carrier_hz onto each emitter component. Receiver configs provide the tuned center frequency, analog capture bandwidth, and sample rate used for the band-overlap test and RX resampling.
flowchart TD
EMT["Emitter\n(clean baseband, realized_carrier_hz)"]
G1["TX impairments\nDAC · PA · TX-PN · TX-IQ · CFO\nper emitter"]
G2["Channel propagation\nBaseChannel (Sionna)\nper (emitter, RX) pair"]
SEL["Band-overlap select\nemitter.realized_carrier_hz in\nrx.center_freq_hz ± rx.bandwidth_hz/2"]
G3["RX capture\nRX mixer · IF filter · resampler\n[sum in-band components]\nLNA noise\nper RX"]
G4["RX hardware\nADC · RX-PN · RX-IQ · AGC\nper RX"]
REC["Record\n(per-RX digitized IQ + metadata)"]
EMT --> G1 --> G2 --> SEL --> G3 --> G4 --> REC
TX impairments and channel propagation are pre-sum: they operate on individual emitter components. RX capture begins with the sum point. RX capture and RX hardware are per-receiver post-sum chains.
The TorchSig adapters below refer to TorchSig, the external RFML benchmark library; see TorchSig Interop.
Minimal Example¶
This is a full-contract skeleton, not the smallest quickstart path. Build a single ChannelPipeline containing all 14 transformations and pass it to the scene composer. The composer dispatches TX impairments and channel propagation per emitter before the receiver sum, then RX capture and RX hardware per receiver after the sum, according to each transformation’s Group membership.
import torch
from rfgen.channel_pipeline import ChannelPipeline
from omegaconf import OmegaConf
from rfgen.config import from_hydra, validate_config
from rfgen.propagation import SionnaUMi
from rfgen.rx_frontend import (
LeesonRXPhaseNoise,
LinearADCQuantizer,
LinearAGC,
LinearLNANoise,
LinearRXMixer,
ScipyFIRIFFilter,
ScipyPolyResampler,
TorchSigRXIQImbalance,
)
from rfgen.scene import DefaultSceneComposer
from rfgen.tx_impairments import (
LeesonTXPhaseNoise,
LinearCFO,
LinearDACQuantizer,
RappPA,
TorchSigTXIQImbalance,
)
channel = ChannelPipeline([
# TX impairments (per emitter, pre-sum)
LinearDACQuantizer(enob_bits=14),
RappPA(), # Rapp model default
LeesonTXPhaseNoise(), # Leeson model default
TorchSigTXIQImbalance(),
LinearCFO(f_offset_hz=250.0),
# Channel propagation (per (TX, RX) pair, pre-sum)
SionnaUMi(),
# RX capture (per receiver, post-sum)
LinearRXMixer(),
ScipyFIRIFFilter(cutoff_norm=0.4, num_taps=65, gain=1.0),
ScipyPolyResampler(up=1, down=1),
LinearLNANoise(noise_figure_db=5.0),
# RX hardware (per receiver, post-sum)
LinearADCQuantizer(enob_bits=10),
LeesonRXPhaseNoise(),
TorchSigRXIQImbalance(),
LinearAGC(),
])
composer = DefaultSceneComposer()
scene_signal = composer.build(
scene_cfg=from_hydra(OmegaConf.load("configs/config.yaml")).scene,
emitter_pool=emitter_pool, # supplied by the emitter setup code
channel=channel,
rng=torch.Generator().manual_seed(0),
)
Load Hydra-managed configs through from_hydra() or validate plain dict payloads through validate_config(), then pass the resolved cfg.scene into the composer. See Reference / Scene Composition Algorithm § Frequency placement strategies for how dense-band scenarios place emitters.
Each receiver’s effective center_freq_hz, bandwidth_hz, and sample_rate_hz come from the ReceiverConfig entry when that field is set, otherwise from scene_cfg.center_hz, scene_cfg.bandwidth_hz, and scene_cfg.sample_rate_hz. The shipped ReceiverConfig schema does not carry an rx_chain override; the post-sum path comes from the Group.RX_CAPTURE and Group.RX_HARDWARE entries in channel. See Reference / Scene Composition Algorithm for the full dispatch contract.
Design Notes¶
Sample rate hierarchy¶
The emitter generates at its own native sample rate R_emitter. TX impairments and channel propagation operate at R_emitter. The resampler moves each component to R_rx before the sum in RX capture. RX capture and RX hardware operate at R_rx. Each receiver may have a different R_rx; SceneConfig.sample_rate_hz is only the construction-time default and fallback, not a stored scene-metadata field.
Why one channel propagation slot¶
rfgen delegates Group.CHANNEL physics to Sionna; see Background / Sionna stack for how Sionna PHY and RT map onto the framework. Sionna’s channel objects bundle path loss, multipath, shadowing, and Doppler into a single model. Splitting those effects across multiple pipeline slots would require rfgen to orchestrate them in sequence, duplicating Sionna’s internal state. One slot per (emitter, RX) pair is the correct level of abstraction for Sionna’s API. The current composer applies that slot per pair; vectorized multi-pair Sionna calls are a future optimization, not today’s execution contract.
Source: Channel propagation cites Sionna’s TR 38.901 UMa, UMi, RMa, TDL, CDL, and RT implementations, plus the underlying ETSI TR 138 901 / 3GPP TR 38.901, for the one-slot propagation contract.
ChannelPipeline aggregator¶
ChannelPipeline is the
Layer 4 container that collects the full transformation chain and
enforces its structural invariants before any DSP runs. The pipeline
carries no signal-processing code; it holds an ordered
tuple[BaseChannel, ...] and exposes the four group-partitioned views
the scene composer reads at dispatch time.
Why a separate aggregator. Putting the chain in a dedicated validated object means errors in chain ordering surface at construction time, not partway through a scene generation run. Plugin authors who assemble a chain from config get the same validation as authors who write Python directly.
Group-partition rule. The four partition properties
(tx_transforms, propagation, rx_capture, rx_hardware) split the
chain by Group using each entry’s
transformation.value // 10. Group values are assigned in multiples of
10 so that the integer floor division maps cleanly onto the four
group names without ambiguity between groups.
Immutability contract. ChannelPipeline is a frozen dataclass and
stores the chain as a tuple. Two mutation attempts are pinned:
pipeline.chain[i] = otherraisesTypeErrorbecause tuples are immutable.pipeline.chain = (...)raisesdataclasses.FrozenInstanceErrorbecause the dataclass is frozen.
Both paths are exercised by the contract tests.
Validation invariants. The following are checked at construction:
Every entry is a BaseChannel instance.
Group order is monotonic non-decreasing on
transformation.groupalong the chain (TX before CHANNEL before RX_CAPTURE before RX_HARDWARE).Within a group,
transformationordinals are monotonic non-decreasing (the canonical per-group ordering).At most one Group.CHANNEL propagation entry is present.
A violation raises ChannelError with a message that names the offending pair, so the configuration author can correct the chain without reading the validator source.
An empty chain and a chain missing any particular group both pass validation; the scene composer documents which groups it requires.
Config-driven construction. ChannelPipeline.from_config(config: ChannelChainConfig) resolves each chain entry through
EntryPointRegistry under the
rfgen.channels entry-point group. The discovered class is
instantiated with the entry’s params mapping forwarded as keyword
arguments, then the full validated chain is built via the standard
constructor. Config-time and code-time construction therefore enforce
the same invariants.
ChannelChainConfig is an alias
for ChannelConfig (the Layer 2 Pydantic model). No separate config
class was introduced for Layer 4.
See Reference / rfgen.channels for the full
parameter surface, field schemas, method signatures, and error
contracts.
Explicit transformations everywhere else¶
TX impairments, RX capture, and RX hardware use explicit plugin slots instead of a single opaque impairment object. This makes each effect replaceable independently, keeps the metadata contract auditable at each step, and supports ablation studies that disable one transformation without touching others.
See Also¶
TX impairments: DAC, PA, phase noise, IQ imbalance, CFO.
Channel propagation: Sionna-backed propagation slot and per-pair propagation semantics.
RX capture: mixing, filtering, resampling, summing, and LNA noise.
RX hardware: ADC, RX phase noise, RX IQ imbalance, AGC.
Coordinate Systems § Frequency Frames: how
realized_carrier_hz(metadata) becomes a sample-domain shift at the RX mixer.Reference /
rfgen.channels: full parameter surface, schemas, and method signatures.Background / Sionna stack: how Sionna PHY and RT map onto the framework.
Background / Design Decisions § Channel pipeline for the rationale behind the semantic-group, fourteen-transformation redesign and why the prior five-stage chain was retired.