rfgen.device_fingerprint

The rfgen.device_fingerprint module ships the per-device fingerprint parameter store: the frozen Pydantic v2 model FingerprintParams carrying the canonical per-device priors and the read-after-construct DeviceRegistry cache mapping device_id to a deterministic per-device draw. A helper default_priors_from_torchsig() returns the TorchSig-sourced prior ranges for callers that need to align with the TorchSig snapshot.

Scientific validation

The fingerprint module has been scientifically validated against the documented impairment-chain references (Saleh-1981 TWTA fit, Rapp-1991 PA model, Leeson-1966 phase-noise model). See:

The report covers construct validity, mathematical correctness against cited equations, empirical comparison to published reference numbers, literature grounding, experimental methodology, operating envelope, and documented limitations.

Scope and threading contract

The impairment math itself is NOT implemented in this module; that lives in rfgen.tx_impairments (LinearCFO, RappPA, SalehPA, LeesonTXPhaseNoise, TorchSigTXIQImbalance, LinearDACQuantizer) and rfgen.rx_frontend (LinearLNANoise, LeesonRXPhaseNoise, TorchSigRXIQImbalance, LinearADCQuantizer, …).

The registry only exposes the parameter store. Channel transformations consume FingerprintParams through ChannelContext.emitter_meta.extras["fingerprint_params"] (a Pydantic-validated dict serialised by FingerprintParams.model_dump()); they do not import DeviceRegistry directly.

Library-First posture. The prior-distribution defaults documented in fingerprint-parameter-priors.md are sourced from TorchSig v2 (torchsig.transforms.IQImbalance for amplitude/phase imbalance and torchsig.transforms.ClockDrift for clock-rate priors). The model field names are a superset of FINGERPRINT_PARAM_KEYS documented on the Channels page, so the threading contract documented in Layer 2 channel-protocols is preserved.

Module surface

from rfgen.device_fingerprint import (
    FingerprintParams,
    DeviceRegistry,
    default_priors_from_torchsig,
)

registry = DeviceRegistry()  # uses documented default ranges
params = registry.draw("device-001", rng=torch.Generator().manual_seed(0))
# `params` is a FingerprintParams; same device_id returns the same instance.

Class index

Class / function

Kind

Notes

FingerprintParams

frozen Pydantic model

Carries the canonical per-device priors (CFO, SFO, IQ-imbalance, PA, phase noise)

DeviceRegistry

concrete

Content-addressed cache of per-device FingerprintParams

default_priors_from_torchsig

function

Returns per-field prior ranges sourced from torchsig.transforms defaults


class rfgen.device_fingerprint.FingerprintParams

Per-device CFO / SFO / IQ-imbalance / phase-noise / PA priors. Frozen Pydantic v2 model with extra="forbid".

class FingerprintParams(BaseModel):
    model_config = ConfigDict(frozen=True, extra="forbid")

    cfo_hz: float = 0.0
    sfo_ppm: float = 0.0
    iq_imbalance_db: float = 0.0
    iq_imbalance_rad: float = 0.0
    pa_p: float = 2.0
    pa_a: float = 1.0
    phase_noise_dbc_hz: float = -100.0
    pa_model: PAModel = PAModel.RAPP
    alpha_a: float = 2.1587
    beta_a: float = 1.1517
    alpha_phi: float = 4.0033
    beta_phi: float = 9.1040

The field names are a superset of FINGERPRINT_PARAM_KEYS (declared on rfgen.channels.protocols), so the runtime contract test asserts set(FingerprintParams.model_dump().keys()) >= set(FINGERPRINT_PARAM_KEYS). Adding new fields is non-breaking; renaming or removing a documented key is a breaking change caught by that test.

Fields

Name

Type

Default

Range

Notes

cfo_hz

float

0.0

[-1e6, 1e6]

Carrier-frequency offset in Hz. The default DeviceRegistry band of (-1000, +1000) Hz assumes a ~1 GHz carrier and ~1 ppm TCXO-class reference stability; rescale by f_c / 1 GHz for other carriers.

sfo_ppm

float

0.0

[-100.0, 100.0]

Sample-rate (clock-drift) offset in ppm.

iq_imbalance_db

float

0.0

[-3.0, 3.0]

Amplitude imbalance in dB; range matches torchsig.transforms.IQImbalance amplitude_imbalance prior.

iq_imbalance_rad

float

0.0

[-0.5, 0.5]

Phase imbalance in radians; range matches torchsig.transforms.IQImbalance phase_imbalance prior.

pa_p

float

2.0

[1.0, 10.0]

Rapp PA smoothness exponent; typical solid-state PA fits give p in [2, 3]. Reference: Rapp (1991).

pa_a

float

1.0

(0.0, 10.0]

Rapp PA saturation amplitude (linear, normalised to input scale).

phase_noise_dbc_hz

float

-100.0

[-160.0, -50.0]

One-sided PSD floor in dBc/Hz at 10 kHz offset, consumed by the Leeson synthesizer. Reference: Leeson (1966).

pa_model

PAModel

RAPP

enum

Closed-set PA-model selector. Routes to RappPA or SalehPA via select_pa_nonlinearity().

alpha_a

float

2.1587

> 0

Saleh AM/AM alpha_a; default is canonical Saleh-1981 TWTA fit (Hughes 261-H Helix at 6 GHz).

beta_a

float

1.1517

> 0

Saleh AM/AM beta_a.

alpha_phi

float

4.0033

> 0

Saleh AM/PM alpha_phi.

beta_phi

float

9.1040

> 0

Saleh AM/PM beta_phi.

Saleh defaults. Sources: Saleh (1981), Table II; republished in ITU-R Recommendation F.1336-5.

Rapp defaults. Source: Rapp, C. (1991), Effects of HPA-Nonlinearity on a 4-DPSK/OFDM-Signal for a Digital Sound Broadcasting System, Proc. 2nd European Conf. on Satellite Comm., Liege, Belgium, pp. 179-184.


class rfgen.device_fingerprint.DeviceRegistry

Content-addressed cache of per-device FingerprintParams. Construct with a population-prior FingerprintParams (the mean of the configured device population). draw() returns the same FingerprintParams instance for the same device_id regardless of the caller-supplied RNG; first-time draws derive a sub-seed from the parent generator and device_id so the draw is reproducible across processes.

The registry is read-only after construction: there is no public mutator. Internally the per-device cache is filled lazily; once populated, entries are immutable FingerprintParams instances.

Constructor

class DeviceRegistry:
    def __init__(
        self,
        *,
        priors: FingerprintParams | None = None,
        cfo_hz_range: tuple[float, float] = (-1000.0, 1000.0),
        sfo_ppm_range: tuple[float, float] = (-20.0, 20.0),
        iq_imbalance_db_range: tuple[float, float] = (-1.0, 1.0),
        iq_imbalance_rad_range: tuple[float, float] = (-0.035, 0.035),
        pa_p_range: tuple[float, float] = (1.5, 3.0),
        pa_a_range: tuple[float, float] = (0.8, 1.2),
        phase_noise_dbc_hz_range: tuple[float, float] = (-110.0, -90.0),
        alpha_a_range: tuple[float, float] = (2.05, 2.27),
        beta_a_range: tuple[float, float] = (1.09, 1.21),
        alpha_phi_range: tuple[float, float] = (3.80, 4.20),
        beta_phi_range: tuple[float, float] = (8.65, 9.56),
        default_pa_model: PAModel = PAModel.RAPP,
    ) -> None: ...

Constructor parameters

Name

Type

Required

Default

Description

priors

FingerprintParams | None

no

None

Optional overriding population-mean priors.

cfo_hz_range

tuple[float, float]

no

(-1000.0, 1000.0)

Inclusive low/high band for the CFO prior draw.

sfo_ppm_range

tuple[float, float]

no

(-20.0, 20.0)

Inclusive low/high band for the SFO prior draw.

iq_imbalance_db_range

tuple[float, float]

no

(-1.0, 1.0)

Amplitude-imbalance prior range.

iq_imbalance_rad_range

tuple[float, float]

no

(-0.035, 0.035)

Phase-imbalance prior range.

pa_p_range

tuple[float, float]

no

(1.5, 3.0)

Rapp p smoothness exponent range.

pa_a_range

tuple[float, float]

no

(0.8, 1.2)

Rapp A saturation amplitude range.

phase_noise_dbc_hz_range

tuple[float, float]

no

(-110.0, -90.0)

Leeson PSD level prior range.

alpha_a_range

tuple[float, float]

no

(2.05, 2.27)

Saleh alpha_a range (~ +/-5% around canonical fit).

beta_a_range

tuple[float, float]

no

(1.09, 1.21)

Saleh beta_a range.

alpha_phi_range

tuple[float, float]

no

(3.80, 4.20)

Saleh alpha_phi range.

beta_phi_range

tuple[float, float]

no

(8.65, 9.56)

Saleh beta_phi range.

default_pa_model

PAModel

no

RAPP

Categorical PA-model selector applied uniformly to every drawn device. pa_model is a closed-set enum (not a continuous draw), so the registry exposes it as a single registry-wide parameter rather than a per-device range.

The constructor validates every range: each (low, high) tuple must have finite endpoints and satisfy low <= high. Invalid ranges raise ValueError at construction time so misconfiguration surfaces immediately rather than at the first draw() call.

Methods

draw(device_id, rng) -> FingerprintParams

def draw(self, device_id: str, rng: torch.Generator) -> FingerprintParams: ...

Return per-device parameters; identical device_id returns the same params regardless of the supplied rng. First-time draws derive a sub-seed from rng.initial_seed() and device_id via rfgen.rng.derive_rng, then draw each continuous prior via numpy.random.default_rng(...).uniform(low, high). pa_model is set to the registry-wide default_pa_model (closed-set, not a continuous draw).

known_devices() -> tuple[str, ...]

Returns the sorted tuple of device ids already drawn.

dump_cache() -> dict[str, dict[str, object]]

Returns a JSON-serializable snapshot of the per-device cache. Each entry maps device_id -> FingerprintParams.model_dump(). The returned dict is a deep copy so callers cannot mutate the live cache. Pair with load_cache() for cross-process persistence. The inner value type is widened to object because FingerprintParams carries a PAModel enum alongside its float fields.

load_cache(snapshot) -> None

Restore the per-device cache from a dump_cache() snapshot. Each value is validated through FingerprintParams.model_validate so invalid snapshots fail loudly. Replaces the entire cache with the snapshot contents; entries not present in the snapshot are discarded.


rfgen.device_fingerprint.default_priors_from_torchsig

def default_priors_from_torchsig() -> dict[str, tuple[float, float]]: ...

Return the per-field default prior ranges sourced from TorchSig v2. The snapshot is built by reading the default arguments of torchsig.transforms.IQImbalance.__init__ and torchsig.transforms.ClockDrift.__init__ at call time. Drift in the upstream defaults will change the returned snapshot; a contract test compares the snapshot against a checked-in expected table.

Returns

Mapping of canonical priors to their inclusive (low, high) ranges:

  • iq_imbalance_db -> TorchSig IQImbalance.amplitude_imbalance

  • iq_imbalance_rad -> TorchSig IQImbalance.phase_imbalance

  • sfo_ppm -> TorchSig ClockDrift.drift_ppm

Raises

BackendUnavailableError when torchsig is not importable.


See Also

  • Channels: Layer 2 BaseChannel, ChannelContext, the FINGERPRINT_PARAM_KEYS tuple, and the fingerprint-fallback contract.

  • RX Frontend: the RX-hardware concretes (LeesonRXPhaseNoise, TorchSigRXIQImbalance) that consume FingerprintParams through ctx.emitter_meta.extras.

  • Fingerprint Math: the impairment-chain mathematical specification.

  • Fingerprint Parameter Priors: the documented prior-distribution rationale per field.