rfgen.rx_frontend

The rfgen.rx_frontend module ships the Group.RX_CAPTURE and Group.RX_HARDWARE concretes: the eight per-transformation ABCs (mixer, IF filter, resampler, LNA noise, ADC quantizer, RX phase noise, RX IQ-imbalance, AGC) and their default concrete implementations, plus the opt-in TorchSigImpairments adapter that applies an explicitly configured, pinned TorchSig transform sequence.

TorchSig is the RF machine-learning (RFML) signal-generation and dataset toolkit rfgen interoperates with; see Reference / TorchSig Interop. TorchSigImpairments executes the configured public TorchSig 2.1.1 transforms directly. Its level value is retained only to verify the upstream Impairments(level, seed) constructor and does not select or execute a bundled chain. By contrast, TorchSigRXIQImbalance and the TX-side IQ-imbalance concrete documented on TX impairments are rfgen-native classes that keep TorchSig-aligned naming and prior provenance but own their internal math because the upstream helper semantics do not match the documented contract.

Scientific validation

The RX-side concretes have been scientifically validated against published references. See the per-component reports:

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.

Module surface

import rfgen.rx_frontend as rx

mixer = rx.LinearRXMixer()
iffilt = rx.ScipyFIRIFFilter(cutoff_norm=0.4, num_taps=65, gain=1.0)
resampler = rx.ScipyPolyResampler(up=1, down=1)
lna = rx.LinearLNANoise(noise_figure_db=6.0)
adc = rx.LinearADCQuantizer(enob_bits=12)
phase_noise = rx.LeesonRXPhaseNoise(psd_floor_dbc_hz=-100.0, fc_hz=1e4)
iq_imb = rx.TorchSigRXIQImbalance(amplitude_imbalance_db=0.0, phase_imbalance_rad=0.0)
agc = rx.LinearAGC(target=0.5, tau_attack=64.0, tau_decay=1024.0)

Constructor arguments are keyword-only. Every concrete subclasses its per-transformation ABC, pins the matching Transformation ClassVar, and reads required runtime context (sample rate, RX bandwidth, RNG, optional per-device fingerprint params) from ctx.

Class index

Group.RX_CAPTURE

ABC

Concrete

Backend

BaseRXMixer

LinearRXMixer

Pure-torch complex mixing to RX baseband

BaseIFFilter

ScipyFIRIFFilter

scipy.signal.firwin design + scipy.signal.lfilter apply

BaseResampler

ScipyPolyResampler

scipy.signal.resample_poly

BaseLNANoise

LinearLNANoise

Pure-torch thermal noise from k_B * T * BW * NF (no Friis cascade)

Group.RX_HARDWARE

ABC

Concrete

Backend

BaseADCQuantization

LinearADCQuantizer

Inline mid-tread uniform quantiser (IEEE Std 1241-2010)

BaseRXPhaseNoise

LeesonRXPhaseNoise

Shared rfgen._leeson.synthesize_leeson_phase_noise

BaseRXIQImbalance

TorchSigRXIQImbalance

Inline differential I/Q model (Razavi RF Microelectronics 2e, sec. 4.2.4)

BaseAGC

LinearAGC

Custom torch attack/decay loop

Cross-group adapter

Class

Slot

Notes

TorchSigImpairments

Registers under Transformation.ADC for book-keeping; spans multiple groups

Pins TorchSig 2.1.1; executes configured public transforms, while Impairments(level, seed) is constructor-only and level is nonsemantic


Group.RX_CAPTURE

class rfgen.rx_frontend.BaseRXMixer

class BaseRXMixer(BaseChannel):
    transformation: ClassVar[Transformation] = Transformation.RX_MIXER

    @abstractmethod
    def apply(self, signal: Signal, ctx: ChannelContext) -> Signal: ...

Per-transformation ABC for RX-side downconversion mixers.

class rfgen.rx_frontend.LinearRXMixer

class LinearRXMixer(BaseRXMixer):
    def __init__(self) -> None: ...

Multiplies IQ by exp(-j * 2 * pi * f_lo * t) where f_lo = signal.metadata.realized_carrier_hz - ctx.rx_params.center_freq_hz. Guards against non-finite f_lo (NaN, +/-Inf) at entry to avoid silent NaN propagation into the IQ output.

Post-call, signal.metadata.realized_carrier_hz is updated to ctx.rx_params.center_freq_hz. Appends a TransformationLogEntry with params={"f_lo_hz": f_lo}.


class rfgen.rx_frontend.BaseIFFilter

class BaseIFFilter(BaseChannel):
    transformation: ClassVar[Transformation] = Transformation.IF_FILTER

    @abstractmethod
    def apply(self, signal: Signal, ctx: ChannelContext) -> Signal: ...

Per-transformation ABC for the IF-stage low-pass filter.

class rfgen.rx_frontend.ScipyFIRIFFilter

class ScipyFIRIFFilter(BaseIFFilter):
    def __init__(
        self,
        *,
        cutoff_norm: float = 0.4,
        num_taps: int = 65,
        gain: float = 1.0,
    ) -> None: ...

Two-step FIR design + application driven by SciPy primitives. The constructor designs the FIR with scipy.signal.firwin(num_taps, cutoff_norm, window="hann") once and caches the taps; apply() scales the taps by the per-instance gain and runs scipy.signal.lfilter(taps, [1.0], iq, axis=-1) on the (2, N) array in a single C-extension call.

Constructor validates: 0 < cutoff_norm < 1 (normalised to Nyquist) and num_taps is odd and >= 3 (linear-phase FIR). scipy.signal.upfirdn would fuse the design and application steps and is the documented switch-target if the per-instance gain coefficient is dropped.

Constructor parameters

Name

Type

Required

Default

Description

cutoff_norm

float

no

0.4

Cutoff normalised to Nyquist (0 < cutoff_norm < 1).

num_taps

int

no

65

FIR length; must be odd and >= 3.

gain

float

no

1.0

Per-instance gain applied between FIR design and FIR application.


class rfgen.rx_frontend.BaseResampler

class BaseResampler(BaseChannel):
    transformation: ClassVar[Transformation] = Transformation.RESAMPLER

    @abstractmethod
    def apply(self, signal: Signal, ctx: ChannelContext) -> Signal: ...

Per-transformation ABC for sample-rate conversion.

class rfgen.rx_frontend.ScipyPolyResampler

class ScipyPolyResampler(BaseResampler):
    def __init__(self, *, up: int = 1, down: int = 1) -> None: ...

Polyphase resampler driven by scipy.signal.resample_poly(iq, up, down, axis=-1). Constructor requires up >= 1 and down >= 1.

Post-call, signal.metadata.sample_rate_hz is multiplied by up / down and signal.metadata.duration_samples is updated to match the new sample count.

Constructor parameters

Name

Type

Required

Default

Description

up

int

no

1

Upsampling factor (positive integer).

down

int

no

1

Downsampling factor (positive integer).


class rfgen.rx_frontend.BaseLNANoise

class BaseLNANoise(BaseChannel):
    transformation: ClassVar[Transformation] = Transformation.LNA_NOISE

    @abstractmethod
    def apply(self, signal: Signal, ctx: ChannelContext) -> Signal: ...

Per-transformation ABC for LNA / thermal-noise injection.

class rfgen.rx_frontend.LinearLNANoise

class LinearLNANoise(BaseLNANoise):
    def __init__(self, *, noise_figure_db: float = 6.0) -> None: ...

Pure-torch thermal noise driven by the RX noise figure. The variance per sample is k_B * T * BW * NF with T = 290 K and BW = ctx.rx_params.bandwidth_hz. A single fused torch.randn(2, n, ...) call draws I and Q in one RNG-state advance.

Scope - Friis cascade not implemented. The variance uses only this stage’s noise_figure_db; it does NOT compose noise figures across upstream stages per the Friis formula (IEEE Std 521-2002, Pozar sec. 10.3). A pipeline of LinearRXMixer -> ScipyFIRIFFilter -> LinearLNANoise therefore models the LNA as if it were the first noise contributor in the chain. Callers needing a true cascade must either (a) collapse the upstream NF into a single equivalent noise_figure_db and pass it here, or (b) use the Sionna backend with its multi-stage receive chain.

Sub-ULP bandwidth floor. For BW * NF small enough that k_B * T * BW * NF falls below 1e-24 (roughly BW * NF < 250 mHz), the noise power is floored at 1e-24 so float32 arithmetic stays numerically meaningful. The absolute scale lives in metadata for downstream calibration.

Constructor parameters

Name

Type

Required

Default

Description

noise_figure_db

float

no

6.0

RX noise figure in dB.


Group.RX_HARDWARE

class rfgen.rx_frontend.BaseADCQuantization

class BaseADCQuantization(BaseChannel):
    transformation: ClassVar[Transformation] = Transformation.ADC

    @abstractmethod
    def apply(self, signal: Signal, ctx: ChannelContext) -> Signal: ...

Per-transformation ABC for ADC quantisation.

class rfgen.rx_frontend.LinearADCQuantizer

class LinearADCQuantizer(BaseADCQuantization):
    def __init__(self, *, enob_bits: int = 12) -> None: ...

Uniform mid-tread ADC quantizer (IEEE Std 1241-2010). Constructor requires 1 <= enob_bits <= 24. Inline pure-torch implementation; the plan originally referenced torchsig.transforms.functional.quantize as the default but that helper applies a mid-rise quantiser with floor rounding (numerically distinct from the documented mid-tread contract). The Library-First override is recorded in .agent-state/rfgen-impl.log.md.

Step-size convention. The denominator uses levels = 2**enob_bits - 1, giving step size q = 2 * peak / (2**b - 1). The textbook full-scale-peak convention is q = peak / 2**(b - 1); this implementation is larger by a factor 2**b / (2**b - 1) (~0.02% at b = 12, ~6.7% at b = 4). The choice keeps the positive full-scale point representable; the SQNR impact for b >= 8 is below 0.01 dB.

Constructor parameters

Name

Type

Required

Default

Description

enob_bits

int

no

12

Effective number of bits (1…24).


class rfgen.rx_frontend.BaseRXPhaseNoise

class BaseRXPhaseNoise(BaseChannel):
    transformation: ClassVar[Transformation] = Transformation.RX_PHASE_NOISE

    @abstractmethod
    def apply(self, signal: Signal, ctx: ChannelContext) -> Signal: ...

Per-transformation ABC for RX-side phase noise.

class rfgen.rx_frontend.LeesonRXPhaseNoise

class LeesonRXPhaseNoise(BaseRXPhaseNoise):
    def __init__(
        self,
        *,
        psd_floor_dbc_hz: float = -100.0,
        fc_hz: float = 1e4,
        flicker_corner_hz: float = 1e3,
    ) -> None: ...

RX phase noise driven by the shared rfgen._leeson.synthesize_leeson_phase_noise synthesizer. The TX-side LeesonTXPhaseNoise and this RX-side class are bit-identical at matched seed and parameters because they share the same synthesizer. See rfgen._leeson for the Leeson (1966) and Rohde (1997) citations and the library-gap rationale.

The per-device fingerprint slot may override psd_floor_dbc_hz via params_dict["phase_noise_dbc_hz"]; corner frequencies remain class-level. Falls back per the fingerprint-fallback contract when emitter_meta.extras lacks the slot.

Constructor parameters

Name

Type

Required

Default

Description

psd_floor_dbc_hz

float

no

-100.0

One-sided PSD floor in dBc/Hz at the 10 kHz reference offset.

fc_hz

float

no

1e4

Corner frequency of the Leeson PSD shape.

flicker_corner_hz

float

no

1e3

Flicker-noise corner frequency.


class rfgen.rx_frontend.BaseRXIQImbalance

class BaseRXIQImbalance(BaseChannel):
    transformation: ClassVar[Transformation] = Transformation.RX_IQ_IMB

    @abstractmethod
    def apply(self, signal: Signal, ctx: ChannelContext) -> Signal: ...

Per-transformation ABC for RX IQ imbalance.

class rfgen.rx_frontend.TorchSigRXIQImbalance

class TorchSigRXIQImbalance(BaseRXIQImbalance):
    def __init__(
        self,
        *,
        amplitude_imbalance_db: float = 0.0,
        phase_imbalance_rad: float = 0.0,
    ) -> None: ...

RX IQ imbalance using the standard differential I/Q model from Razavi, RF Microelectronics 2nd ed., Prentice Hall 2011, sections 4.2.4 and 7.4:

I' = I * (1 + a/2)
Q' = I * sin(phi) + Q * cos(phi) * (1 - a/2)

where a = 10**(amp_db/20) - 1.

Despite the class name, the concrete intentionally inlines the closed form rather than calling torchsig.transforms.functional.iq_imbalance: that functional helper applies common-mode gain (10**(amp_db/10) to both channels), not the differential I-vs-Q form above, and its dc_offset_db=0.0 argument injects a DC tone rather than disabling DC offset. The TorchSig functional API therefore has the wrong semantics for the documented contract. The Library-First override is recorded in .agent-state/rfgen-impl.log.md.

The per-device fingerprint slot may override amplitude_imbalance_db via params_dict["iq_imbalance_db"] and phase_imbalance_rad via params_dict["iq_imbalance_rad"]. Falls back per the fingerprint-fallback contract when the slot is absent.

Constructor parameters

Name

Type

Required

Default

Description

amplitude_imbalance_db

float

no

0.0

Amplitude imbalance in dB.

phase_imbalance_rad

float

no

0.0

Phase imbalance in radians.


class rfgen.rx_frontend.BaseAGC

class BaseAGC(BaseChannel):
    transformation: ClassVar[Transformation] = Transformation.AGC

    @abstractmethod
    def apply(self, signal: Signal, ctx: ChannelContext) -> Signal: ...

Per-transformation ABC for automatic-gain control.

class rfgen.rx_frontend.LinearAGC

class LinearAGC(BaseAGC):
    def __init__(
        self,
        *,
        target: float = 0.5,
        tau_attack: float = 64.0,
        tau_decay: float = 1024.0,
        saturation_dbfs: float = -3.0,
        gain_init: float = 1.0,
    ) -> None: ...

Standard linear-AGC update with attack/decay time constants. For each sample, the loop:

  1. Computes the instantaneous post-gain magnitude y = gain * |x|.

  2. Updates the gain by a single-pole IIR: attack when y > target (gain decreases), decay otherwise (gain increases).

  3. Clamps the output magnitude at 10**(saturation_dbfs / 20).

For a constant-envelope input, the realised gain reaches target / |x| within 5% after 5 * tau_attack samples.

Library-First gap. Custom torch loop. No mature Python library covers the linear-AGC update documented above: SciPy has no AGC primitive and TorchSig’s DigitalAGC uses a different formulation. The user confirmed this gap in the pass-6 decision log.

Constructor validates: target > 0, tau_attack > 0, tau_decay > 0.

Cross-group adapter

class rfgen.rx_frontend.TorchSigImpairments

class TorchSigImpairments(BaseChannel):
    transformation: ClassVar[Transformation] = Transformation.ADC

    def __init__(self, config: TorchSigImpairmentsConfig) -> None: ...

Opt-in adapter that requires exactly TorchSig 2.1.1 and applies the configured public transform classes in the supplied order. The admitted names are AdditiveNoise, TimeVaryingNoise, CarrierPhaseNoise, IQImbalance, Quantize, and RandomDropSamples. It does not provide an rfgen DSP fallback.

The adapter spans multiple groups; for pipeline book-keeping it registers under Transformation.ADC. A pipeline requiring a separately slotted transformation must use the corresponding per-transformation concrete instead.

Configuration

class TorchSigTransformConfig(BaseModel):
    name: str
    params: Mapping[str, Any] = {}

class TorchSigImpairmentsConfig(BaseModel):
    transforms: tuple[TorchSigTransformConfig, ...]
    level: Literal[0, 1, 2] = 0
    seed: int  # 0 <= seed <= 2**64 - 1

config is required. transforms must be nonempty, contain no duplicate name, and use this canonical order: AdditiveNoise, TimeVaryingNoise, CarrierPhaseNoise, IQImbalance, Quantize, then RandomDropSamples. There is no implicit transform selection. An unadmitted transform name raises UnsupportedCapabilityError; an unknown parameter name or a parameter outside its admitted type/range raises ValueError during configuration validation.

params accepts only the public constructor parameters below. Tuples retain TorchSig continuous-range semantics and lists retain TorchSig discrete-choice semantics; callers must not interchange them.

Transform

Admitted params

AdditiveNoise

power_range two-number tuple or list; color one of white, pink, red; continuous bool; precise bool.

TimeVaryingNoise

noise_power_low and noise_power_high two-number tuple or list; inflections two-integer tuple/list or a nonempty list of nonnegative integers; random_regions bool or nonempty list of bool.

CarrierPhaseNoise

phase_noise_degrees two-number tuple or list.

IQImbalance

amplitude_imbalance, phase_imbalance, dc_offset_db, and dc_offset_rads, each a two-number tuple or list.

Quantize

num_bits two-positive-integer tuple or list; ref_level_adjustment_db two-number tuple or list; rounding_mode nonempty tuple or list containing only floor and/or ceiling.

RandomDropSamples

drop_rate number or two-number range in [0, 1]; size positive number or two-number range; fill one of ffill, bfill, mean, zero, or a nonempty list of those strings.

The adapter constructs TorchSig’s Impairments(level, seed) only as a pinned public-constructor compatibility probe. It never executes that bundled chain because it cannot select the explicit transform list. Therefore level has no DSP semantics and changing it must not change the adapter output for the same input, explicit transforms, and seed.

Execution and audit record

apply(signal, ctx) requires a CPU-resident, single-receiver IQ tensor shaped (2, N). TorchSig 2.1.1 executes through NumPy, so a non-CPU tensor fails closed with ChannelError before any device transfer; callers must explicitly move IQ to CPU or select a GPU-native channel transformation. The adapter converts the CPU tensor to TorchSig’s complex64 signal representation, constructs each configured public class with the configured parameters and seed, executes the classes in canonical configured order, and converts the finite same-shaped complex64 result back to (2, N). A non-finite or shape-changing upstream result raises ChannelError.

The appended TransformationLogEntry.params records torchsig_version, execution_backend="configured_public_torchsig_transforms", seed, an ordered transforms list with each class and validated parameters, plus pre_sha256 and post_sha256 hashes of the contiguous complex buffers. Its impairments_probe object records the upstream class, supplied level, executed: false, and the constructor-probe purpose; it is not evidence that the bundled chain executed.

If TorchSig cannot be imported, construction raises BackendUnavailableError. If the installed package version is not 2.1.1, construction raises UnsupportedCapabilityError. Omitting config is a Python TypeError. A non-(2, N) or non-CPU input raises ChannelError.


See Also

  • Channels: Layer 2 ABC, the Transformation enum, the per-call ChannelContext.

  • TX Impairments (in rfgen.tx_impairments): the symmetric TX-side concretes (LinearCFO, LeesonTXPhaseNoise, TorchSigTXIQImbalance, RappPA, SalehPA, LinearDACQuantizer).

  • Propagation: the single Group.CHANNEL slot upstream of the RX-side concretes.

  • Device Fingerprint: FingerprintParams consumed by LeesonRXPhaseNoise and TorchSigRXIQImbalance.

  • RF Frontend Models: the algorithmic background for the receiver-side concretes.