Scientific validation: Device fingerprint sampler¶
Validated with documented limitations.
1. The component¶
DeviceRegistry is a content-addressed (lookup by key, always returning the same value for the same key) parameter store that draws and caches per-device hardware impairment parameters for use by downstream signal-processing stages. It does not apply any signal transformation; it only produces and stores the parameter vector that those downstream stages consume.
class FingerprintParams(BaseModel):
model_config = ConfigDict(frozen=True, extra="forbid")
cfo_hz: float = Field(default=0.0, ge=-1e6, le=1e6)
sfo_ppm: float = Field(default=0.0, ge=-100.0, le=100.0)
iq_imbalance_db: float = Field(default=0.0, ge=-3.0, le=3.0)
iq_imbalance_rad: float = Field(default=0.0, ge=-0.5, le=0.5)
pa_p: float = Field(default=2.0, ge=1.0, le=10.0)
pa_a: float = Field(default=1.0, gt=0.0, le=10.0)
phase_noise_dbc_hz: float = Field(default=-100.0, ge=-160.0, le=-50.0)
pa_model: PAModel = PAModel.RAPP
alpha_a: float = Field(default=2.1587, gt=0.0)
beta_a: float = Field(default=1.1517, gt=0.0)
alpha_phi: float = Field(default=4.0033, gt=0.0)
beta_phi: float = Field(default=9.1040, gt=0.0)
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: ...
def draw(self, device_id: str, rng: torch.Generator) -> FingerprintParams: ...
def dump_cache(self) -> dict[str, dict[str, object]]: ...
def load_cache(self, snapshot: dict[str, dict[str, object]]) -> None: ...
Parameter |
Type |
Units |
Default |
Purpose |
|---|---|---|---|---|
|
|
Hz |
|
Population draw band for carrier-frequency offset (CFO, the frequency error of the device’s local oscillator relative to nominal; modeled at ~1 GHz carrier with ~1 ppm TCXO-class stability). |
|
|
ppm |
|
Draw band for sample-frequency offset (SFO, clock-rate deviation in parts per million). |
|
|
dB |
|
Draw band for in-phase / quadrature (IQ) amplitude imbalance. IQ is the two-channel complex baseband representation; amplitude imbalance is a gain mismatch between the I and Q paths. |
|
|
rad |
|
Draw band for IQ phase imbalance (~±2°). |
|
|
dimensionless |
|
Draw band for Rapp power-amplifier (PA) smoothness exponent p. |
|
|
linear |
|
Draw band for Rapp PA saturation amplitude A, normalised to input scale. |
|
|
dBc/Hz |
|
Draw band for one-sided phase-noise (oscillator phase instability) power-spectral-density floor, measured at a 10 kHz offset from the carrier. dBc/Hz is decibels relative to the carrier power per hertz of bandwidth. |
|
|
dimensionless |
±5% of Saleh Table II |
Draw bands for the Saleh travelling-wave-tube-amplifier (TWTA) AM/AM and AM/PM coefficients. |
|
|
n/a |
|
Registry-wide PA model family selector: |
import torch
from rfgen.calibration.fingerprint import DeviceRegistry, FingerprintParams
rng = torch.Generator()
rng.manual_seed(42)
registry = DeviceRegistry()
# First call draws and caches; subsequent calls for the same id are instant.
params = registry.draw("transmitter-001", rng)
assert isinstance(params, FingerprintParams)
assert -1000.0 <= params.cfo_hz <= 1000.0
# Same device_id always returns the same object, regardless of rng state.
params_again = registry.draw("transmitter-001", rng)
assert params is params_again # identity, not equality
# Snapshot the cache for cross-process transfer.
snapshot = registry.dump_cache()
fresh = DeviceRegistry()
fresh.load_cache(snapshot)
assert fresh.draw("transmitter-001", rng).cfo_hz == params.cfo_hz
DeviceRegistry sits at the head of the per-device parameter pipeline. Downstream channel transformations (power-amplifier nonlinearity, oscillator phase noise, IQ imbalance) read FingerprintParams from ChannelContext.emitter_meta.extras["fingerprint_params"]; they never import DeviceRegistry directly. The threading contract is enforced by a compile-time assertion that FingerprintParams.model_fields is a superset of FINGERPRINT_PARAM_KEYS declared in the channel-protocols layer.
The component’s exclusions are explicit: it models only a population prior over hardware constants. It does not apply impairment math (that lives in tx_impairments and rx_frontend), it does not model joint correlations across physically coupled fields, and it does not support per-device PA-model variation. These exclusions are documented in §4.
2. What we validated¶
This validation establishes 8 load-bearing claims. Each is restated and supported by evidence in §3.
Per-field marginal uniformity (§3.1): each of the 11 continuous fields is drawn from a uniform distribution over its declared range.
Pairwise field independence (§3.2): the 11 continuous fields are statistically independent across devices.
Cross-process determinism (§3.3): the same device identifier and parent seed produce byte-identical parameters across independent processes.
Cache identity and immutability (§3.4): repeated draws for the same device identifier return the same object; the record cannot be mutated after creation.
Snapshot round-trip fidelity (§3.5): dump-and-load preserves every field value including the categorical PA-model selector.
TorchSig canary and Saleh constant accuracy (§3.6): IQ imbalance and clock-drift defaults track TorchSig v2; Saleh (1981) Table II coefficients are reproduced exactly.
Default prior ranges consistent with published hardware data (§3.7): each default draw band is anchored to a published reference or library default.
Safe operating envelope (§3.8): boundary inputs (empty ID, inverted range, non-finite endpoint, out-of-bound range, degenerate range) behave as documented.
Limits and scope-bounded items appear in §4; full citations are in §5.
3. Evidence per claim¶
3.1 Per-field marginal uniformity¶
Each of the 11 continuous fields in FingerprintParams is drawn from a uniform distribution over its declared (low, high) range. The empirical check draws N = 10,000 distinct device identifiers under torch.manual_seed(42), then applies a Kolmogorov-Smirnov (KS) test (a non-parametric test that measures the maximum gap between the empirical cumulative distribution and the theoretical one) against scipy.stats.uniform(loc=low, scale=high-low) for each field.
Because 11 tests run simultaneously, a Bonferroni correction is applied: the family-wise false-positive rate is held at α = 0.01 by requiring each per-field p-value to exceed 0.01/11 = 9.1 × 10⁻⁴. The minimum observed p-value across all 11 fields is 5.0 × 10⁻³ (for pa_a), which is 5.5× above the corrected threshold, leaving a comfortable margin.
Figure 1 shows the empirical distribution for each field overlaid with the ideal uniform density. All 11 panels show good agreement between the blue empirical histogram and the orange uniform reference line.

A secondary coverage check confirms that draws reach within ~1% of both band edges with no clipping: the empirical minimum and maximum for each field span nearly the full declared range.
Test:
tests/validation/device_fingerprint/test_marginal_uniformity.py::test_per_field_marginal_uniformitySample size: N = 10,000 device identifiers, seed = 42
Statistical test: KS test against
scipy.stats.uniform, Bonferroni-corrected per-field threshold = 9.1 × 10⁻⁴Measured result: all 11 fields pass; minimum p-value = 5.0 × 10⁻³ (
pa_a)
3.2 Pairwise field independence¶
Each device’s 11 continuous fields are drawn sequentially from a single NumPy PCG64 generator (Parallel Congruential Generator, 64-bit: a high-quality pseudorandom generator whose successive draws are statistically independent). Because the draws are sequential within one generator stream, independence is a property of the PCG64 generator itself. The empirical check validates this property at the relevant draw length.
The same N = 10,000 draw matrix is used to compute the 11 × 11 Pearson correlation matrix. The pass criterion is that every off-diagonal entry satisfies |ρ| < 0.05. Under the null hypothesis of independence, the standard error of a single Pearson coefficient at N = 10,000 is approximately 1/√N = 0.01, so the 0.05 threshold is roughly five standard errors from zero; a conservative bound.
Figure 2 shows the pairwise correlation heatmap. All off-diagonal cells are near zero; the colour scale spans ±0.05 to make any deviation visible.

Test:
tests/validation/device_fingerprint/test_pairwise_independence.py::test_pairwise_field_independenceSample size: N = 10,000, seed = 0xC0FFEE
Statistical test: Pearson correlation matrix, threshold |ρ| < 0.05
Measured result: maximum |ρ| = 2.25 × 10⁻² (between
cfo_hzandpa_a)
3.3 Cross-process determinism¶
Cross-process determinism is the load-bearing dataset-replay property: a dataset producer running in one Python process must generate the same fingerprint as a consumer reading it in another process, even when Python’s hash randomization (PYTHONHASHSEED) differs between them. The implementation uses hashlib.sha256 to derive the per-device sub-seed; SHA-256 (Secure Hash Algorithm, 256-bit) output is deterministic by definition, independent of Python’s salted hash().
The test launches two independent child subprocesses via subprocess.run (inheriting the default randomised PYTHONHASHSEED) and compares their JSON dumps against the parent process’s result. Two children rule out parent-state leakage as a confound.
Test:
tests/validation/device_fingerprint/test_cross_process_determinism.py::test_cross_process_determinism;test_cross_process_determinism_two_subprocessesMethod: Two independent
subprocess.runchildren with default randomisedPYTHONHASHSEED; comparemodel_dump_json()byte-for-byteMeasured result: byte-identical JSON across parent and both children for
(seed=42, device_id="dev-A")
3.4 Cache identity and immutability¶
Once a device identifier has been drawn, DeviceRegistry.draw returns the exact same Python object on all subsequent calls, regardless of the rng argument. This is tested by calling draw("dev-A", ...) with two distinct torch.Generator instances seeded differently and asserting Python is-identity (same object in memory, not merely equal values).
FingerprintParams is frozen (ConfigDict(frozen=True, extra="forbid") in Pydantic v2): any attempt to assign a new value to a field raises pydantic.ValidationError immediately.
Tests:
tests/validation/device_fingerprint/test_cache_identity.py::test_cache_identity_returns_same_object;test_fingerprint_params_is_frozenMeasured result:
is-identity confirmed;ValidationErrorraised on mutation attempt
3.5 Snapshot round-trip fidelity¶
dump_cache() returns a JSON-serialisable snapshot; load_cache(snapshot) restores it into a fresh registry with field-by-field equality, including the categorical pa_model enum. Three contracts are pinned: (a) the snapshot is a deep copy, so mutating it does not affect the live cache; (b) load_cache replaces (does not merge) prior entries; © all 1,000 devices round-trip with exact float equality.
Test:
tests/validation/device_fingerprint/test_cache_roundtrip.py::test_dump_load_cache_roundtrip_is_bit_identical;test_dump_cache_returns_deep_copy;test_load_cache_replaces_existing_entriesSample size: N = 1,000 devices
Measured result: field-by-field equality for all 1,000 entries including
PAModelenum; deep-copy and replace semantics confirmed
3.6 Frozen prior snapshot and Saleh constant accuracy¶
default_fingerprint_priors() returns rfgen’s checked-in ranges and does not
import TorchSig at runtime. Saleh coefficients remain verified against the
documented reference table.
Libraries¶
PyPI distribution |
Installed version |
Documentation |
Role in this validation |
|---|---|---|---|
|
2.13.4 |
Provides |
|
|
2.4.6 |
Provides |
|
|
1.18.0 |
Provides |