rfgen.channels¶
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 rfgen.channels module ships the Layer 2 channel-transformation contract: the BaseChannel ABC, the per-call ChannelContext and ChannelRxParams value types, and the Transformation and Group enums. Concrete per-transformation ABCs and their built-in implementations live in three sibling Layer 3 modules:
TorchSig, the external RFML benchmark library, appears only in the opt-in adapter path; see TorchSig Interop.
Module |
API page |
What it ships |
|---|---|---|
|
(per-transformation ABCs listed below) |
TX-side concretes (DAC, PA, TX phase noise, TX IQ-imbalance, CFO) |
|
AWGN and the Sionna-backed propagation skeletons |
|
|
RX-side concretes (mixer, IF filter, resampler, LNA noise, ADC, RX phase noise, RX IQ-imbalance, AGC) plus the opt-in TorchSig adapter |
The per-device fingerprint store lives in rfgen.device_fingerprint; see the Device Fingerprint reference for FingerprintParams and DeviceRegistry.
This page documents the Layer 2 surface only. The 13 per-transformation ABCs (one per slot, except channel propagation which reuses :class:~rfgen.channels.protocols.BaseChannel indirectly through its propagation base) are listed in the class index with cross-references to their owning module page.
Pipeline model¶
The pipeline is a sequence of BaseChannel subclasses with one transformation per slot. Each concrete pins its slot via the Transformation class variable; the scene composer reads that variable to dispatch each emitter through the canonical pipeline order.
The 14 transformations split across a pre-sum and a post-sum boundary at the head of RX capture. Pre-sum stages run per-emitter (TX-side) and per-(emitter, RX) pair (propagation); at the combine point, the composer selects the emitter components whose realized_carrier_hz falls within [rx.center_freq_hz +/- rx.bandwidth_hz / 2], sums them, and forwards the result through the post-sum RX-capture and RX-hardware stages.
The emitter plugin’s baseband modulation is outside this pipeline; it is the emitter contract, not a channel transformation. Each emitter carries realized_carrier_hz as absolute Hz in its metadata. Each receiver carries center_freq_hz, bandwidth_hz, sample_rate_hz, and noise_figure_db on its ChannelRxParams instance.
Class index¶
Class |
Kind |
Group / Role |
|---|---|---|
abc |
Layer 2 ABC; every concrete transformation inherits from this |
|
datatype |
Per-call context passed to every |
|
datatype |
Active receiver RF and hardware parameters |
|
dataclass |
Layer 4 validated, ordered chain of |
|
alias |
Alias of |
|
sentinel |
Pydantic model returned from |
|
TypedDict |
Schema for one entry of |
|
|
constant |
Canonical key tuple consumed from |
enum |
|
|
enum |
|
|
|
abc |
TX impairments: DAC quantization (lives in |
|
abc |
TX impairments: PA nonlinearity (lives in |
|
abc |
TX impairments: TX phase noise (lives in |
|
abc |
TX impairments: TX IQ imbalance (lives in |
|
abc |
TX impairments: CFO (lives in |
|
abc |
Channel propagation (lives in |
|
abc |
RX capture: RX mixer (lives in |
|
abc |
RX capture: IF filter (lives in |
|
abc |
RX capture: resampler (lives in |
|
abc |
RX capture: LNA noise (lives in |
|
abc |
RX hardware: ADC quantization (lives in |
|
abc |
RX hardware: RX phase noise (lives in |
|
abc |
RX hardware: RX IQ imbalance (lives in |
|
abc |
RX hardware: AGC (lives in |
For per-class signatures of TX, propagation, and RX concretes, see the TX Impairments, Propagation, and RX Frontend pages.
class rfgen.channels.Group¶
class Group(IntEnum):
TX = 1 # per-emitter TX impairments
CHANNEL = 2 # per-(emitter, RX) pair propagation
RX_CAPTURE = 3 # per-RX mixing, filtering, resampling, LNA noise
RX_HARDWARE = 4 # per-RX ADC, phase noise, IQ imbalance, AGC
Pipeline group tag. The leading digit of a Transformation integer is its group; the trailing digit is intra-group order. Group is distinct from Transformation: Group partitions the chain at the pre-sum / post-sum boundary; Transformation names individual operations within a group.
Transformation carries a .group property that returns the matching Group member.
class rfgen.channels.Transformation¶
class Transformation(IntEnum):
# Group.TX (10s)
DAC = 11
PA = 12
TX_PHASE_NOISE = 13
TX_IQ_IMB = 14
CFO = 15
# Group.CHANNEL (20s)
PROPAGATION = 21
# Group.RX_CAPTURE (30s)
RX_MIXER = 31
IF_FILTER = 32
RESAMPLER = 33
LNA_NOISE = 34
# Group.RX_HARDWARE (40s)
ADC = 41
RX_PHASE_NOISE = 42
RX_IQ_IMB = 43
AGC = 44
Integer tag pinned to every concrete channel implementation as a ClassVar[Transformation]. The integer value encodes ordering: value // 10 is the Group integer, value % 10 is the intra-group canonical order. The scene composer reads this tag to dispatch each emitter through the pipeline in monotonic order.
Notes¶
Multiple concrete implementations per transformation are allowed; only one transformation tag per class.
Do not define a new class with a Transformation value that does not appear in this enum. Renumbering existing members is a breaking change for the config validator.
class rfgen.channels.ChannelContext¶
@dataclass(frozen=True, slots=True)
class ChannelContext:
"""Per-call context handed to every BaseChannel.apply() invocation."""
emitter_meta: SignalMetadata
rx_params: ChannelRxParams
scene_id: str
sample_idx: int
rng: torch.Generator
Kind. Frozen dataclass / value type.
Carries the per-transformation call context into BaseChannel.apply(). The scene driver constructs one ChannelContext per (emitter, RX, transformation) call; it is not shared across calls.
Fields¶
Field |
Type |
Description |
|---|---|---|
|
Ground-truth metadata for the emitter being transformed. TX-side transformations read |
|
|
The active receiver’s RF and hardware parameters. Carries |
|
|
str |
Stable scene identifier, used by transformations to derive deterministic per-scene seeds when needed. |
|
int |
Zero-based index of this sample within the current shard. Combined with |
|
|
The per-call RNG. All randomness inside |
Fingerprint fallback contract¶
When emitter_meta.extras does NOT contain the "fingerprint_params" slot, transformations that consume per-device priors MUST read defaults from FingerprintParams() (the device-fingerprint Pydantic model instantiated with declared default-prior values, which corresponds to the population-level prior mean). Such transformations MUST emit a transformation_log entry with fallback=True and fallback_reason="missing_fingerprint_params". The byte-for-byte equivalence test in tests/unit/test_channel_protocols.py ties Layer 3 channel implementations to this fallback shape.
class rfgen.channels.ChannelRxParams¶
@dataclass(frozen=True, slots=True)
class ChannelRxParams:
"""Active receiver RF and hardware parameters."""
center_freq_hz: float
bandwidth_hz: float
sample_rate_hz: float
noise_figure_db: float
position_m: tuple[float, float, float] | None = None
antenna_id: str | None = None
tag: str | None = None
Kind. Frozen dataclass / value type.
The per-receiver RF and hardware parameters passed through ChannelContext.rx_params. Built by the scene driver from the active ReceiverConfig for each receiver in the scene.
Fields¶
Field |
Type |
Required |
Default |
Description |
|---|---|---|---|---|
|
float |
yes |
– |
Receiver LO frequency in Hz. Used by |
|
float |
yes |
– |
Receiver analog capture bandwidth in Hz. Used by |
|
float |
yes |
– |
Receiver ADC sample rate in Hz. Target output rate for |
|
float |
yes |
– |
Receiver noise figure in dB, used by |
|
|
no |
|
3D position in scene-frame meters. Consumed by Sionna RT and CDL backends for geometric calculations. |
|
str | None |
no |
|
Antenna-pattern identifier. Consumed by Sionna RT and PHY backends for AoA-dependent gain. |
|
str | None |
no |
|
Human-readable receiver label. The scene composer sets this to |
class rfgen.channels.BaseChannel¶
class BaseChannel(ABC):
transformation: ClassVar[Transformation]
@abstractmethod
def apply(self, signal: Signal, ctx: ChannelContext) -> Signal: ...
@abstractmethod
def schema(self) -> type[BaseModel]: ...
@classmethod
def schema_from_init(cls) -> type[BaseModel]: ...
Layer 2 ABC. Every concrete transformation inherits from a per-transformation ABC, which itself inherits from BaseChannel. BaseChannel is the type the scene composer dispatches against.
Class attributes¶
Attribute |
Type |
Description |
|---|---|---|
|
|
Identifies which of the 14 named transformations this implementation performs. The ABC enforces at class-definition time via |
Method: apply¶
@abstractmethod
def apply(self, signal: Signal, ctx: ChannelContext) -> Signal
Applies the transformation to signal and returns a new Signal with updated IQ and metadata. ctx carries the per-call context.
Parameters¶
Name |
Type |
Description |
|---|---|---|
|
Input IQ plus metadata; shape conventions follow the emitter contract |
|
|
Per-call context: |
Returns¶
A new Signal with the same IQ shape (unless the transformation changes sample rate) and metadata updated to reflect the applied operation.
Method: schema¶
@abstractmethod
def schema(self) -> type[BaseModel]
Returns the Pydantic model describing this transformation’s parameters. Subclasses with no parameters return EmptyParams; subclasses with keyword-only __init__ parameters MAY delegate to cls.schema_from_init().
Method: schema_from_init (classmethod, cached)¶
@classmethod
@functools.cache
def schema_from_init(cls) -> type[BaseModel]
Builds a Pydantic model from the subclass’s __init__ signature. Each keyword argument (excluding self and ctx) becomes a field with the parameter annotation forwarded as the field type and the parameter default forwarded as the field default; parameters without a default become required. Subclasses with no keyword arguments return EmptyParams.
The result is cached per-class via functools.cache, so repeated calls in hot loops are O(1) after the first invocation.
Extension contract¶
Every subclass of BaseChannel (whether via a per-transformation ABC or directly) MUST satisfy these invariants:
IQ shape.
apply()MUST return a Signal whoseiqhas shape(2, N)whereN == signal.iq.shape[-1], with the following exception:BaseResamplerimplementations MAY return a differentNequal toint(round(signal.iq.shape[-1] * ctx.rx_params.sample_rate_hz / signal.metadata.sample_rate_hz)). No other transformation may change the sample count.No in-place mutation.
apply()MUST NOT modifysignal.iqorsignal.metadatain place. Return a new Signal object; the input must be unchanged after the call.Metadata update.
apply()MUST update the metadata fields that its transformation affects, and SHOULD append a TransformationLogEntry describing the realised parameters.Randomness source. All randomness MUST be drawn from
ctx.rng. Subclasses MUST NOT use module-level random state,random.random(), ortorch.rand()without a generator.transformationClassVar. Must equal theTransformationenum member identifying this slot. The ABC enforces this at class definition; missing or incorrectly-typed values raiseTypeError.No side effects.
apply()MUST NOT perform I/O, network calls, or mutations of shared state. State that persists across calls (e.g., per-device filter kernels) must be stored as instance attributes, not module globals.
Extension points¶
Method |
Status |
Notes |
|---|---|---|
|
Must override |
Core transformation logic; the primary abstract method |
|
Must override |
Return a Pydantic model for config validation; return |
|
Must set as ClassVar |
Set to the matching |
Notes¶
Do not inherit directly from BaseChannel to add a new transformation implementation. Inherit from the per-transformation ABC for the relevant slot, such as
BasePANonlinearityfor PA nonlinearity.Concrete-to-concrete inheritance is forbidden per project conventions.
class rfgen.channels.EmptyParams¶
class EmptyParams(BaseModel):
model_config = ConfigDict(frozen=True, extra="forbid")
Sentinel Pydantic model returned from BaseChannel.schema() by transformations that take no parameters. The model has no fields, so EmptyParams.model_fields == {} and EmptyParams().model_dump() == {}.
class rfgen.channels.TransformationLogEntry¶
class TransformationLogEntry(TypedDict):
name: str
group: int
transformation: int
params: dict[str, object]
applied_at_iso: str
fallback: bool
fallback_reason: str
TypedDict schema for one entry of SignalMetadata.transformation_log. Layer 3 channel implementations append one TransformationLogEntry per applied transformation; audit and replay tooling consumes the log key-by-key.
Keys¶
Key |
Type |
Description |
|---|---|---|
|
str |
Human-readable transformation name (e.g. |
|
int |
Group integer (1…4). |
|
int |
Transformation integer. |
|
dict[str, object] |
Pydantic |
|
str |
ISO-8601 UTC timestamp when the transformation ran. |
|
bool |
|
|
str |
Free-form reason string when |
ChannelChainConfig¶
ChannelChainConfig = ChannelConfig
Kind. Module-level alias (not a new class).
ChannelChainConfig is an alias of ChannelConfig, the Layer 2 Pydantic config model that already carries the chain field and its cross-entry invariants. The alias exists so the Layer 4 API surface (ChannelPipeline.from_config(ChannelChainConfig)) has a distinct name without duplicating the config model.
Construct via ChannelConfig directly:
from rfgen.config.channel import ChannelConfig
from rfgen.channel_pipeline import ChannelChainConfig # same object
assert ChannelChainConfig is ChannelConfig # True
See ChannelConfig on the Config page for the full field list.
class rfgen.channel_pipeline.ChannelPipeline¶
@dataclass(frozen=True, slots=True)
class ChannelPipeline:
chain: tuple[BaseChannel, ...] = ()
def __init__(self, transformations: Sequence[BaseChannel] = ()) -> None: ...
@classmethod
def from_config(cls, config: ChannelChainConfig) -> ChannelPipeline: ...
Kind. Frozen dataclass. Lives in rfgen.channel_pipeline, exported from rfgen.channel_pipeline.__all__.
A validated, ordered chain of BaseChannel transformations. The pipeline carries no DSP code: it holds the BaseChannel instances in canonical pipeline order and exposes the four group-aligned partition properties consumed by the scene composer. Two mutation paths are blocked by the frozen-dataclass contract:
pipeline.chain[i] = otherraisesTypeError(tuple is immutable).pipeline.chain = (...)raisesdataclasses.FrozenInstanceError(frozen dataclass).
Attributes¶
Attribute |
Type |
Description |
|---|---|---|
|
|
Validated transformation chain, in canonical pipeline order. Read-only; set once at construction. |
Constructor¶
ChannelPipeline(transformations: Sequence[BaseChannel] = ())
Validates and freezes the transformation chain.
Parameters¶
Name |
Type |
Default |
Description |
|---|---|---|---|
|
|
|
Ordered sequence of BaseChannel instances. An empty sequence is allowed; the scene composer documents which groups it requires. |
Raises¶
ChannelError when any of the following invariants is violated:
Invariant |
Trigger |
|---|---|
All entries are |
Any entry fails |
Group order is monotonic non-decreasing |
An entry’s |
Intra-group ordinals are monotonic non-decreasing |
Within the same Group, an entry’s |
At most one |
More than one propagation transformation appears in the chain |
The error message names the offending pair so the configuration author can correct the chain without reading the validator source.
Partition properties¶
The four properties split chain by Group using the value // 10 rule. Their concatenation reconstructs list(chain):
tx_transforms + ([propagation] if propagation else []) + list(rx_capture) + list(rx_hardware) == list(chain)
Property |
Type |
Description |
|---|---|---|
|
|
Entries whose |
|
|
The single |
|
|
Entries whose |
|
|
Entries whose |
Dunder methods¶
Method |
Signature |
Behavior |
|---|---|---|
|
|
Returns |
|
|
Yields each |
|
|
Compares pipelines by |
|
|
Returns |
Method: from_config (classmethod)¶
@classmethod
def from_config(cls, config: ChannelChainConfig) -> ChannelPipeline
Builds a pipeline by resolving each chain entry through the plugin registry.
Each ChannelChainEntry in config.chain is resolved through an EntryPointRegistry over the rfgen.channels entry-point group. The discovered class is instantiated with the entry’s params mapping as keyword arguments. The resulting instances are then passed through the standard ChannelPipeline validator, so config-time and code-time construction enforce identical invariants.
Parameters¶
Name |
Type |
Description |
|---|---|---|
|
A validated |
Returns¶
A new ChannelPipeline whose chain contains one BaseChannel instance per config.chain entry.
Raises¶
Exception |
Trigger |
|---|---|
|
Chain-level validation fails after plugin instantiation. |
|
An entry’s |
Validation invariants summary¶
Invariant |
Error type |
Error details |
|---|---|---|
All entries are |
|
Names the offending entry index and its actual type. |
Group order is monotonic non-decreasing |
|
Names the offending pair and their group labels. |
Intra-group ordinals are monotonic non-decreasing |
|
Names the offending pair, transformation names, and values. |
At most one |
|
Lists all |
Minimal example¶
from rfgen.channel_pipeline import ChannelPipeline
from rfgen.tx_impairments import LinearDACQuantizer, LinearCFO
from rfgen.propagation import AWGNChannel
from rfgen.rx_frontend import LinearRXMixer, ScipyFIRIFFilter, LinearADCQuantizer
pipeline = ChannelPipeline(
transformations=[
LinearDACQuantizer(),
LinearCFO(f_offset_hz=100.0),
AWGNChannel(snr_db=20.0),
LinearRXMixer(),
ScipyFIRIFFilter(),
LinearADCQuantizer(),
]
)
print(len(pipeline)) # 6
print(pipeline.tx_transforms) # (LinearDACQuantizer, LinearCFO)
print(pipeline.propagation) # AWGNChannel instance
print(repr(pipeline))
# ChannelPipeline(6 transformations: LinearDACQuantizer, LinearCFO, AWGNChannel, LinearRXMixer, ScipyFIRIFFilter, LinearADCQuantizer)
From config:
from rfgen.channel_pipeline import ChannelPipeline, ChannelChainConfig
from rfgen.config.channel import ChannelConfig
cfg = ChannelConfig.model_validate({
"chain": [
{"transformation": "DAC", "params": {}},
{"transformation": "PROPAGATION", "params": {"snr_db": 20.0}},
{"transformation": "ADC", "params": {}},
]
})
pipeline = ChannelPipeline.from_config(cfg)
Per-transformation ABCs¶
The 13 per-transformation ABCs live alongside their concretes in the three Layer 3 modules listed at the top of this page. Each ABC sets its transformation ClassVar to the matching Transformation member and leaves apply and schema abstract for concrete subclasses to override.
TX impairments (rfgen.tx_impairments)¶
ABC |
|
Concretes shipped |
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Channel propagation (rfgen.propagation)¶
ABC |
|
Concretes shipped |
|---|---|---|
|
|
|
See Propagation for per-class signatures.
RX capture and RX hardware (rfgen.rx_frontend)¶
ABC |
|
Concretes shipped |
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
The opt-in TorchSigImpairments adapter applies TorchSig’s bundled impairment chain in one call and registers under Transformation.ADC for book-keeping. See RX Frontend for per-class signatures.
See Also¶
Concepts / Channels: mental model, boundaries, and data flow for the 4-group pipeline.
ChannelPipeline: Layer 4 validated chain container; constructor, partition properties, and
from_configfactory.Group: group membership enum.
Transformation: 14-member enum pinned on every concrete implementation.
Reference / API / Enums: Group and Transformation entries.
Device Fingerprint:
FingerprintParamsandDeviceRegistryconsumed by TX-side and RX-hardware transformations.Propagation: per-class signatures for
AWGNChanneland the Sionna-backed propagation concretes.RX Frontend: per-class signatures for the eight RX-side concretes plus the
TorchSigImpairmentsadapter.
Anchor aliases (cross-reference compatibility)¶
The anchors below preserve cross-reference targets from earlier design drafts so existing concept pages resolve without modification. They each resolve to the per-transformation ABC index above or the relevant Layer 3 page.
The old Stage enum is replaced by Group and Transformation. The old per-stage ABCs (BasePropagation, BaseDeviceFingerprint, BaseFrequencyShift, BaseRxFrontend, BaseAwgn, BaseTorchSigTransform) are replaced by the 13 per-transformation ABCs listed in the Class index.
Old concrete class names (AWGN, DeviceFingerprint, FrequencyShift, Impairments, TorchSigImpairments, AnalyticReceiverFrontend, RappPA, SalehPA) belong to the previous 5-stage design. Equivalent functionality ships through the per-transformation concretes documented in TX Impairments, Propagation, and RX Frontend.
Earlier drafts described a ChannelPipeline with a ChainKind flag spanning pre-sum and post-sum partitions. The shipped ChannelPipeline (see above) is a frozen dataclass in rfgen.channel_pipeline with no ChainKind. The ChannelChainConfig alias resolves to ChannelConfig; no ChainKind class exists.
Old Sionna class names with the ...Channel suffix (SionnaUMaChannel, SionnaUMiChannel, SionnaRMaChannel, SionnaTDLChannel, SionnaCDLChannel, SionnaRTChannel) belong to earlier drafts. The shipped names drop the suffix: SionnaRT, SionnaUMa, SionnaUMi, SionnaRMa, SionnaTDL, SionnaCDL. See Propagation for the per-class signatures.