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

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

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.