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:
Device fingerprint: validated with documented limitations.
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 |
|---|---|---|
frozen Pydantic model |
Carries the canonical per-device priors (CFO, SFO, IQ-imbalance, PA, phase noise) |
|
concrete |
Content-addressed cache of per-device |
|
|
function |
Returns per-field prior ranges sourced from |
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 |
|---|---|---|---|---|
|
float |
|
|
Carrier-frequency offset in Hz. The default |
|
float |
|
|
Sample-rate (clock-drift) offset in ppm. |
|
float |
|
|
Amplitude imbalance in dB; range matches |
|
float |
|
|
Phase imbalance in radians; range matches |
|
float |
|
|
Rapp PA smoothness exponent; typical solid-state PA fits give |
|
float |
|
|
Rapp PA saturation amplitude (linear, normalised to input scale). |
|
float |
|
|
One-sided PSD floor in dBc/Hz at 10 kHz offset, consumed by the Leeson synthesizer. Reference: Leeson (1966). |
|
|
enum |
Closed-set PA-model selector. Routes to |
|
|
float |
|
|
Saleh AM/AM |
|
float |
|
|
Saleh AM/AM |
|
float |
|
|
Saleh AM/PM |
|
float |
|
|
Saleh AM/PM |
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 |
|---|---|---|---|---|
|
FingerprintParams | None |
no |
|
Optional overriding population-mean priors. |
|
tuple[float, float] |
no |
|
Inclusive low/high band for the CFO prior draw. |
|
tuple[float, float] |
no |
|
Inclusive low/high band for the SFO prior draw. |
|
tuple[float, float] |
no |
|
Amplitude-imbalance prior range. |
|
tuple[float, float] |
no |
|
Phase-imbalance prior range. |
|
tuple[float, float] |
no |
|
Rapp |
|
tuple[float, float] |
no |
|
Rapp |
|
tuple[float, float] |
no |
|
Leeson PSD level prior range. |
|
tuple[float, float] |
no |
|
Saleh |
|
tuple[float, float] |
no |
|
Saleh |
|
tuple[float, float] |
no |
|
Saleh |
|
tuple[float, float] |
no |
|
Saleh |
|
no |
|
Categorical PA-model selector applied uniformly to every drawn device. |
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-> TorchSigIQImbalance.amplitude_imbalanceiq_imbalance_rad-> TorchSigIQImbalance.phase_imbalancesfo_ppm-> TorchSigClockDrift.drift_ppm
Raises¶
BackendUnavailableError when torchsig is not importable.
See Also¶
Channels: Layer 2
BaseChannel,ChannelContext, theFINGERPRINT_PARAM_KEYStuple, and the fingerprint-fallback contract.RX Frontend: the RX-hardware concretes (
LeesonRXPhaseNoise,TorchSigRXIQImbalance) that consumeFingerprintParamsthroughctx.emitter_meta.extras.Fingerprint Math: the impairment-chain mathematical specification.
Fingerprint Parameter Priors: the documented prior-distribution rationale per field.