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

cfo_hz_range

tuple[float, float]

Hz

(-1000, 1000)

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).

sfo_ppm_range

tuple[float, float]

ppm

(-20, 20)

Draw band for sample-frequency offset (SFO, clock-rate deviation in parts per million).

iq_imbalance_db_range

tuple[float, float]

dB

(-1, 1)

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.

iq_imbalance_rad_range

tuple[float, float]

rad

(-0.035, 0.035)

Draw band for IQ phase imbalance (~±2°).

pa_p_range

tuple[float, float]

dimensionless

(1.5, 3.0)

Draw band for Rapp power-amplifier (PA) smoothness exponent p.

pa_a_range

tuple[float, float]

linear

(0.8, 1.2)

Draw band for Rapp PA saturation amplitude A, normalised to input scale.

phase_noise_dbc_hz_range

tuple[float, float]

dBc/Hz

(-110, -90)

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.

alpha_a_range, beta_a_range, alpha_phi_range, beta_phi_range

tuple[float, float]

dimensionless

±5% of Saleh Table II

Draw bands for the Saleh travelling-wave-tube-amplifier (TWTA) AM/AM and AM/PM coefficients.

default_pa_model

PAModel

n/a

PAModel.RAPP

Registry-wide PA model family selector: RAPP or SALEH.

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.

  1. Per-field marginal uniformity (§3.1): each of the 11 continuous fields is drawn from a uniform distribution over its declared range.

  2. Pairwise field independence (§3.2): the 11 continuous fields are statistically independent across devices.

  3. Cross-process determinism (§3.3): the same device identifier and parent seed produce byte-identical parameters across independent processes.

  4. Cache identity and immutability (§3.4): repeated draws for the same device identifier return the same object; the record cannot be mutated after creation.

  5. Snapshot round-trip fidelity (§3.5): dump-and-load preserves every field value including the categorical PA-model selector.

  6. 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.

  7. Default prior ranges consistent with published hardware data (§3.7): each default draw band is anchored to a published reference or library default.

  8. 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.

Figure 1: Per-field empirical distributions vs. declared uniform prior. Each panel shows the histogram of values drawn for one field across 10,000 devices, overlaid with the ideal uniform density. Blue bars = empirical; orange line = uniform prior. Supports §3.1 (per-field marginal uniformity claim).

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_uniformity

  • Sample 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.

Figure 2: Pairwise Pearson correlation matrix for 11 continuous fields across 10,000 devices. All off-diagonal entries have ρ < 0.05. Colour scale spans ±0.05; diagonal (self-correlation = 1.0) is unlabelled. Supports §3.2 (pairwise field independence claim).

  • Test: tests/validation/device_fingerprint/test_pairwise_independence.py::test_pairwise_field_independence

  • Sample size: N = 10,000, seed = 0xC0FFEE

  • Statistical test: Pearson correlation matrix, threshold |ρ| < 0.05

  • Measured result: maximum |ρ| = 2.25 × 10⁻² (between cfo_hz and pa_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_subprocesses

  • Method: Two independent subprocess.run children with default randomised PYTHONHASHSEED; compare model_dump_json() byte-for-byte

  • Measured 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_frozen

  • Measured result: is-identity confirmed; ValidationError raised 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_entries

  • Sample size: N = 1,000 devices

  • Measured result: field-by-field equality for all 1,000 entries including PAModel enum; 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_fields

  • Reference: 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

cfo_hz

(−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.

sfo_ppm

(−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 ClockDrift.drift_ppm default; vendor data sheets. Plausible.

iq_imbalance_db

(−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 IQImbalance.amplitude_imbalance default; vendor data sheets. Plausible.

iq_imbalance_rad

(−0.035, +0.035) rad

~±2°; radio IC: 0.5–2°; uncalibrated SDR: up to 5°. Reference: TorchSig IQImbalance.phase_imbalance default. Plausible.

pa_p

(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.

pa_a

(0.8, 1.2) linear

Normalised saturation amplitude; unit is input-relative. Reference: Rapp (1991). Plausible.

phase_noise_dbc_hz

(−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

test_empty_device_id_accepted_and_deterministic

1 MiB device identifier string

Accepted; full key stored in cache (linear memory cost documented)

test_long_device_id_accepted

Unicode device identifier (mixed emoji and combining characters)

Accepted; UTF-8 encoding is unambiguous; SHA-256 derivation is stable across processes

test_unicode_device_id_accepted

Distinct device IDs produce distinct parameter draws

Verified: empty ID and "abc" differ in every non-categorical field

test_distinct_device_ids_produce_distinct_params

Inverted range (low > high) at constructor

Raises ValueError naming the offending field before any draw

test_inverted_range_raises_at_constructor

Non-finite range endpoint (NaN, inf) at constructor

Raises ValueError naming the offending field

test_non_finite_range_raises_at_constructor

Range outside FingerprintParams field bounds

Raises pydantic.ValidationError at first draw

test_out_of_bound_range_raises_validation_error

Degenerate range (low == high)

Accepted; every draw returns the boundary value

test_degenerate_range_returns_constant

priors= constructor argument

Accepted and stored; not consulted by draw (dead-code contract pinned by test)

test_priors_constructor_argument_is_dead_code

  • 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 pa_p and pa_a; cited in source and canary test.

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 phase_noise_dbc_hz carries; the 10 kHz reference offset is pinned by the downstream Leeson synthesiser.

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

torchsig

2.1.1

https://torchsig.readthedocs.io

Source of IQ-imbalance and clock-drift default ranges; introspected at runtime by default_priors_from_torchsig(); pinned by canary test in §3.6.

pydantic

2.13.4

https://docs.pydantic.dev

Provides FingerprintParams as a frozen, validated model; raises ValidationError on mutation and out-of-bounds draws, as confirmed in §3.4 and §3.8.

numpy

2.4.6

https://numpy.org/doc/stable

Provides numpy.random.default_rng (PCG64) for uniform draws; the successive-draw independence property of PCG64 underpins §3.2.

scipy

1.18.0

https://docs.scipy.org/doc/scipy

Provides scipy.stats.kstest and scipy.stats.uniform used in the KS uniformity test in §3.1.