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:
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.
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 |
|---|---|---|
Pure-torch complex mixing to RX baseband |
||
|
||
|
||
Pure-torch thermal noise from |
Group.RX_HARDWARE¶
ABC |
Concrete |
Backend |
|---|---|---|
Inline mid-tread uniform quantiser (IEEE Std 1241-2010) |
||
Shared |
||
Inline differential I/Q model (Razavi RF Microelectronics 2e, sec. 4.2.4) |
||
Custom torch attack/decay loop |
Cross-group adapter¶
Class |
Slot |
Notes |
|---|---|---|
Registers under |
Pins TorchSig 2.1.1; executes configured public transforms, while |
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 |
|---|---|---|---|---|
|
float |
no |
|
Cutoff normalised to Nyquist ( |
|
int |
no |
|
FIR length; must be odd and |
|
float |
no |
|
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 |
|---|---|---|---|---|
|
int |
no |
|
Upsampling factor (positive integer). |
|
int |
no |
|
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 |
|---|---|---|---|---|
|
float |
no |
|
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 |
|---|---|---|---|---|
|
int |
no |
|
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 |
|---|---|---|---|---|
|
float |
no |
|
One-sided PSD floor in dBc/Hz at the 10 kHz reference offset. |
|
float |
no |
|
Corner frequency of the Leeson PSD shape. |
|
float |
no |
|
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 |
|---|---|---|---|---|
|
float |
no |
|
Amplitude imbalance in dB. |
|
float |
no |
|
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:
Computes the instantaneous post-gain magnitude
y = gain * |x|.Updates the gain by a single-pole IIR: attack when
y > target(gain decreases), decay otherwise (gain increases).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 |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
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
Transformationenum, the per-callChannelContext.TX Impairments (in rfgen.tx_impairments): the symmetric TX-side concretes (
LinearCFO,LeesonTXPhaseNoise,TorchSigTXIQImbalance,RappPA,SalehPA,LinearDACQuantizer).Propagation: the single
Group.CHANNELslot upstream of the RX-side concretes.Device Fingerprint:
FingerprintParamsconsumed byLeesonRXPhaseNoiseandTorchSigRXIQImbalance.RF Frontend Models: the algorithmic background for the receiver-side concretes.