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.device_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 TorchSig canary and Saleh constant accuracy¶
TorchSig defaults canary. The default_priors_from_torchsig() helper reads the default arguments of torchsig.transforms.IQImbalance.__init__ and ClockDrift.__init__ at runtime via inspect.signature. A contract test compares the returned tuples byte-for-byte against the live TorchSig source. If TorchSig changes its defaults in a future release, this test fails loudly, alerting maintainers before the change silently propagates into drawn populations.
Saleh (1981) Table II. The four Saleh coefficients (alpha_a = 2.1587, beta_a = 1.1517, alpha_phi = 4.0033, beta_phi = 9.1040) are the canonical TWTA (travelling-wave-tube amplifier) fit from Saleh (1981), Table II. The test asserts exact float equality against those four values.
Tests:
tests/validation/device_fingerprint/test_torchsig_canary.py::test_default_priors_match_torchsig_source_byte_equal;test_saleh_defaults_match_saleh_1981_table_ii;test_fingerprint_params_declares_twelve_fieldsReference: Saleh (1981), Table II (see §5)
Measured result: byte-equal match for all three TorchSig fields; byte-equal match for all four Saleh coefficients
3.7 Default prior ranges consistent with published hardware data¶
The default draw bands are compared against published hardware characterization data and library-sourced references to confirm they represent a plausible commercial-grade RF transceiver population.
Field |
Default range |
Reference and status |
|---|---|---|
|
(−1000, +1000) Hz |
TCXO (temperature-compensated crystal oscillator) at 1 GHz with 1 ppm stability yields ±1000 Hz; documented assumption in source comment. Reference: ITU-R TF.686-3. Plausible. |
|
(−20, +20) ppm |
Commercial-grade crystal: 10–50 ppm; TCXO: 0.5–2.5 ppm. The default spans the high end of TCXO and below consumer crystal worst case. Reference: TorchSig |
|
(−1.0, +1.0) dB |
Commercial direct-conversion radio IC: 0.1–0.5 dB; consumer SDR (software-defined radio, e.g. ADALM-PLUTO): up to ±1 dB. Reference: TorchSig |
|
(−0.035, +0.035) rad |
~±2°; radio IC: 0.5–2°; uncalibrated SDR: up to 5°. Reference: TorchSig |
|
(1.5, 3.0) |
Solid-state PA fits in Rapp (1991): p ∈ [2, 3]; Class A (soft saturation): p ~ 1–2. Reference: Rapp (1991), §IV. Plausible solid-state range; excludes hard-clipping Class AB/C. |
|
(0.8, 1.2) linear |
Normalised saturation amplitude; unit is input-relative. Reference: Rapp (1991). Plausible. |
|
(−110, −90) dBc/Hz |
Consumer local oscillator at 10 kHz offset: −85 to −95 dBc/Hz; commercial TCXO at 10 kHz offset: −110 to −120 dBc/Hz. Reference: Leeson (1966); commercial oscillator data sheets. Plausible commercial-grade band. |
Saleh coefficients |
±5% of Table II fit |
Per-device population spread around the canonical Saleh (1981) TWTA fit. Reference: Saleh (1981), Table II. Represents TWTA population only; not representative of modern solid-state amplifiers (see §4). |
Tests:
tests/validation/device_fingerprint/test_torchsig_canary.py(canary);tests/validation/device_fingerprint/test_marginal_uniformity.py(coverage measurement)
3.8 Safe operating envelope¶
The following boundary regimes were probed and their documented behaviours verified:
Input regime |
Expected behaviour |
Test |
|---|---|---|
Empty string device ID |
Accepted; SHA-256 of empty bytes is a fixed constant; registry caches the empty key deterministically |
|
1 MiB device identifier string |
Accepted; full key stored in cache (linear memory cost documented) |
|
Unicode device identifier (mixed emoji and combining characters) |
Accepted; UTF-8 encoding is unambiguous; SHA-256 derivation is stable across processes |
|
Distinct device IDs produce distinct parameter draws |
Verified: empty ID and |
|
Inverted range (low > high) at constructor |
Raises |
|
Non-finite range endpoint (NaN, inf) at constructor |
Raises |
|
Range outside |
Raises |
|
Degenerate range (low == high) |
Accepted; every draw returns the boundary value |
|
|
Accepted and stored; not consulted by |
|
Tests:
tests/validation/device_fingerprint/test_robustness_boundaries.py(12 tests)
4. Limits and what’s not validated¶
Joint priors across physically coupled fields are not modelled. Carrier-frequency offset (CFO) and phase-noise floor share a physical origin in the same reference oscillator and are correlated in real hardware populations; the sampler draws them independently. Modeling joint structure would require a multivariate distribution primitive (e.g. a multivariate Gaussian or copula on log-transformed inputs); that primitive does not yet exist in the framework and would be a cross-cutting API change.
Per-device PA-model family variation is not exposed. The pa_model selector is registry-wide: every device drawn from a given registry uses the Rapp or Saleh family uniformly, fixed at construction. A mixed-family population requires a composition layer above the registry. Adding per-device categorical draws changes the registry’s constructor signature and the per-device random stream; this is an architectural change outside the current validation scope.
Receiver-side impairments beyond phase noise are absent. Noise figure, antenna mismatch loss, and analog-to-digital converter (ADC) quantisation are first-order receiver impairments not present in FingerprintParams. Adding them requires extending FINGERPRINT_PARAM_KEYS in the channel-protocols layer and threading the new fields through the receiver front end.
The Saleh defaults represent a single 1981 TWTA fit. When default_pa_model = PAModel.SALEH, every drawn device falls within ±5% of one published TWTA coefficient set; this does not represent a population of modern solid-state amplifiers. A multi-anchor Saleh prior would require additional published coefficient sets.
The priors= constructor argument is accepted but not used. The draw method does not consult self._priors when generating per-device values; draws stay within the registry’s range parameters regardless of the priors= value. The dead-code contract is pinned by test_priors_constructor_argument_is_dead_code; any future change to this behaviour is a breaking change.
The CFO default range assumes a documented carrier band. The (−1000, +1000) Hz default is calibrated to a ~1 GHz carrier with ~1 ppm TCXO-class stability. There is no runtime check that the caller is operating at that carrier; a caller at 2.4 GHz with the same default range underestimates the implied ppm spread by a factor of 2.4.
5. References¶
Published works¶
Reference |
Role in this validation |
|---|---|
C. Rapp, “Effects of HPA-Nonlinearity on a 4-DPSK/OFDM-Signal for a Digital Sound Broadcasting System,” Proc. 2nd European Conf. Satellite Communications, Liege, Belgium, 1991, pp. 179–184. |
Defines the Rapp solid-state PA model parameterised by |
A. A. M. Saleh, “Frequency-Independent and Frequency-Dependent Nonlinear Models of TWT Amplifiers,” IEEE Trans. Communications, vol. COM-29, no. 11, pp. 1715–1720, Nov. 1981. DOI: 10.1109/TCOM.1981.1094911 |
Table II gives the canonical TWTA fit whose four coefficients are used as defaults; reproduced byte-exactly in §3.6. |
D. B. Leeson, “A simple model of feedback oscillator noise spectrum,” Proc. IEEE, vol. 54, no. 2, pp. 329–330, Feb. 1966. DOI: 10.1109/PROC.1966.4682 |
Defines the L(f) phase-noise model whose dBc/Hz units |
ITU-R Recommendation TF.686-3, “Glossary and Definitions of Time and Frequency Terms,” 2013. |
Reference for oscillator-stability terminology (ppm, frequency offset, TCXO, OCXO). |
Libraries¶
PyPI distribution |
Installed version |
Documentation |
Role in this validation |
|---|---|---|---|
|
2.1.1 |
Source of IQ-imbalance and clock-drift default ranges; introspected at runtime by |
|
|
2.13.4 |
Provides |
|
|
2.4.6 |
Provides |
|
|
1.18.0 |
Provides |