All posts
Technical

PUFAnalytics, a complete guide to evaluating PUFs in the browser and in Python

A tour of PUFAnalytics, the open-source toolkit for physically unclonable functions. It covers every view of the web app, how each metric is computed, how the browser and Python give bit-identical results, and a full reference of the Python API.

, 25 min read

A paper about a new physically unclonable function (PUF) has to report the same handful of numbers: uniformity, uniqueness, reliability, bit-aliasing, an identification error rate, some randomness tests, and how much error correction a key would need. Almost every group computes them with its own scripts, and the scripts quietly disagree. Some report uniqueness as twice the standard value. Some quote a false-acceptance rate of zero because none of their few hundred comparisons failed. Some run NIST tests on a few hundred bits, where the p-values mean nothing.

PUFAnalytics is my attempt to make that part of the work boring and correct. It is a Python package and a web app that give the same answers to the last digit. You can load measurements or simulate a population, get every standard metric with confidence intervals, and leave with a LaTeX table, figures and a script that reproduces it all. It accompanies our IEEE APCC 2025 paper.

Try it: open the PUFAnalytics web app and press Explore a demo population. Nothing is uploaded; it runs in your browser and works offline. The code is on GitHub under the MIT license.

The PUFAnalytics overview for a simulated 64-stage Arbiter PUF: quality index 94, NIST 7 of 8 tests passed, mean BER 0.46%, and a radar chart of seven normalised scores

New to PUFs? How a PUF works explains the hardware, the four headline metrics and how PUFs are attacked. This post is about the tool: what each part does, how to use it, and how the numbers are computed.

Contents

  1. Two engines, one answer
  2. The data model
  3. The evaluation pipeline
  4. A tour of the web app, one view at a time
  5. Reproducibility: same seed, same bits
  6. Using the Python package and the command line
  7. Full Python API reference
  8. How it is tested, limits and how to cite

Two engines, one answer

PUFAnalytics exists twice. The Python package (pip install, NumPy only) suits scripts, notebooks, batch jobs and the command line. The web app is plain HTML, CSS and JavaScript modules with no framework and no build step; it suits exploring a data set, making figures and showing results to someone. Its analysis engine in web/js/core/ is a line-by-line port of the Python modules, and it runs in a Web Worker so the page stays responsive during a 1,000-sample bootstrap.

Scroll sideways to see the whole diagram.

Two engines, one resultA data file or a simulator seed goes into either the Python package or the browser engine. Both draw random numbers from the same portable generator and run the same algorithms, so both produce the same analysis result. A parity test in continuous integration checks more than 2,400 values from Python against the browser engine.SAME STREAMSAME STREAManalyze()analyze()COMPARED IN CIINPUTYour PUF datacsv · txt · json · npyor a simulator + seedSCRIPTS, NOTEBOOKS, CLIPython packagepufanalytics · NumPyPortable RNGxoshiro128** · SplitMix32Browser engineweb/js/core · Web WorkerWEB APP, OFFLINE, NO UPLOADSame resultidentical JSON, bit for bitParity test2,400+ checks · golden.json
Two engines, one answer. The browser engine is a line-by-line port of the Python package, and both take their random numbers from the same generator in the same order. A simulated population with seed 2024 is therefore the same 49,152 bits in both, and every metric computed from it matches.

Two decisions keep the engines in step:

  • One random number generator for both. Simulators and the bootstrap draw from xoshiro128**, seeded through SplitMix32, implemented identically in Python and JavaScript. Neither engine uses Math.random() or NumPy’s generator.
  • A parity test in CI. tests/make_golden.py writes reference values from Python into web/tests/golden.json, and node web/tests/parity.test.mjs requires the browser engine to reproduce all of them. That is over 2,400 checks across metrics, statistics, NIST tests and simulators.

Your data stays with you. The web app is a static site on GitHub Pages. Files are parsed and analysed on your machine and saved only in your browser’s IndexedDB, and a service worker caches the app so it keeps working offline.

The data model

Everything in PUFAnalytics works on one structure, PUFDataset: a block of devices × measurements × bits, plus an operating-condition label (such as 25C or 85C) for each read-out. Measurement 0 of each device is its enrolment reference. Every metric is one particular comparison inside this block:

Scroll sideways to see the whole diagram.

The data model: devices, read-outs and bitsThree devices, each read four times, two times at 25 degrees and two at 85 degrees. The first read-out of each device is its enrolment reference. Comparing the references of different devices gives uniqueness. Comparing a device's later read-outs with its own reference gives reliability and the bit error rate. The share of ones in a reference gives uniformity, and one bit position across all references gives bit-aliasing.REFERENCE vs REFERENCE → UNIQUENESSDevice ADevice BDevice Cm0 enrolm1 25Cm2 85Cm3 85Cre-reads vs own reference→ RELIABILITY, BERshare of 1s in m0→ UNIFORMITYbit 9 across all devices→ BIT-ALIASINGbit = 1bit = 0flipped vs enrolment (noise)enrolment referenceone bit positionPUFDataset holds one devices × measurements × bits block plus a condition label per read-out
Every metric is one kind of comparison inside this block. Measurement 0 is the enrolment reference (or, with reference="majority", the bitwise majority of all reads). Note how the 85 °C reads flip more bits: that is what the per-condition BER measures.
ComparisonMetricIdeal
Share of 1s in a device’s referenceUniformity50%
One bit position across all devices’ referencesBit-aliasing50% at every position
Reference vs reference, over all device pairsUniqueness (inter-device distance)50%
A device’s re-reads vs its own referenceReliability, bit error rate (BER)100%, 0
The same, split by condition labelBER per operating conditionas low as possible

Data formats

The importer accepts CSV, TXT, JSON and NumPy .npy. The richest layout is one row per read-out, with the response in binary or hex:

device,measurement,condition,response
chipA,0,25C,0xA3F1C29B7E4D0516
chipA,1,85C,0xA3F1C2997E4D0716
chipB,0,25C,0x5C0E3D64819BF2E9

Simpler layouts also work:

  • one response per line (one device each);
  • device,response, where repeated device rows are re-measurements;
  • device,measurement,response;
  • whitespace-separated bits;
  • NumPy arrays shaped (devices, bits) or (devices, measurements, bits).

Separators can be commas, semicolons, tabs or spaces, and # starts a comment. In auto mode a file is read as binary only if every response is made of 0s and 1s; otherwise it is hex, at 4 bits per digit. Method notes: data formats.

The evaluation pipeline

A full evaluation answers six questions in order and then writes them up. The web app’s sidebar follows the same order, so working down it top to bottom covers everything.

Scroll sideways to see the whole diagram.

The evaluation pipelineSeven stages from left to right: data, quality metrics, identification, randomness, modelling attacks, key generation and the report. The figure studio, simulation studio and compare view work across the five analysis stages.STEP 1Dataimport or simulatedevices × reads× bitsSTEP 2Qualitybalanced, uniqueand stable?STEP 3Identificationcan devices betold apart?STEP 4Randomnessany pattern anattacker can use?STEP 5Attackcan a modellearn the PUF?STEP 6Key generationhow many bitsfor a safe key?STEP 7Reporttables, figures,methods, scriptFigure studio · Simulation studio · Compare work across these five stages
One question per step. The sidebar of the web app follows this order, so working down it top to bottom is a complete evaluation. Each step is also a module of the Python package.

A tour of the web app

Open the app. With no data loaded, the overview offers two ways in:

  • Explore a demo population simulates 32 Arbiter PUF devices (64 stages, 128 challenges each). Each device is read 4 times at each of three conditions (25 °C, −40 °C and 85 °C), with noise scaled 1×, 1.8× and 2.6×.
  • Import your data takes you to the import panel.

The rest of this section goes through the views in sidebar order. All screenshots show the demo population, so you can follow along. Each view has a Method notes link to the part of the app’s built-in documentation that defines what it computes.

Some controls are common to every view:

  • The dataset picker in the top bar switches between loaded data sets.
  • The moon button toggles dark mode.
  • Every chart has CSV, SVG and PNG buttons, and a table icon that shows the numbers behind it.

1. Data & simulation

This is where data comes in, either simulated or imported.

The Data & simulation view: a simulator panel with model tabs (Arbiter/XOR, Feed-forward, Interpose, Ring oscillator, SRAM, DRAM, Ideal), population size, seed, model parameters and operating conditions, next to the import panel with a drop zone and a paste box

To simulate a population:

  1. Pick a model tab.
  2. Set the Devices, Response bits, Repeats / condition and Seed fields. The dice button draws a random seed.
  3. Set the model’s own parameters, in the row below.
  4. Add operating conditions with + Condition, or pick one from Presets. Each condition has a label and a noise scale. The first read of the first condition is the enrolment.
  5. Press Generate & analyse.

Seven models are built in:

ModelWhat it simulatesParameters (defaults)
Arbiter / XORAdditive delay model: the response is the sign of a weighted sum of challenge-parity features; XOR of several chainsstages 64, XOR chains 1, noise σ 0.05
Feed-forwardArbiter PUF where intermediate arbiters set later challenge bits (non-linear)stages 64, loops 1, noise 0.05
Interpose(x, y)-iPUF: an x-XOR arbiter’s output is inserted mid-challenge of a y-XOR arbiterstages 64, x 1, y 1, noise 0.05
Ring oscillatorPairs of oscillators compared by frequency, with process variation and a systematic gradient shared by all chipsprocess σ 1, noise 0.1, gradient 0, pairing neighbour/disjoint
SRAMPower-up state of each cell from a fixed mismatch plus thermal noisenoise 0.1, cell bias 0
DRAMRetention: a cell reads 1 if it decays during the refresh pause; heat speeds decaynoise 0.1, threshold 0, temperature shift 0.3
IdealUniform random bits with independent flips: what “perfect” looks like at your sample sizeflip probability 0.02

The Ideal model is more useful than it looks. With 32 devices, even a perfect PUF will not show exactly 50% uniqueness, and simulating an ideal population of the same size shows you how much scatter to expect. Method notes: simulators.

To import:

  1. Drop a file on the drop zone, or paste text into the box.
  2. Give it a name and choose an encoding: Auto-detect, Binary or Hexadecimal.
  3. Press Import & analyse.

Load example text fills the box with a valid sample, and Accepted layouts lists every format.

Below the two panels, every loaded data set has Rename, CSV, JSON and delete buttons. Under those are three pre-processing tools and a response explorer:

Pre-processing tools (bit window, dark-bit masking with a maximum flip rate, device subset) above the response explorer, a bitmap of every device's enrolled response

The pre-processing tools never change the original. Each one creates a new, derived data set:

  • Bit window keeps the bit positions from From bit up to, but not including, To bit. Use it, for example, to study the first 64 bits of a 128-bit response.
  • Dark-bit masking drops bit positions whose flip rate across the population is above a threshold (default 0.05). This is the standard way to trade response length for reliability before key generation.
  • Device subset keeps the first N devices. It is useful for checking how stable a metric is as the population grows.

The response explorer draws every device’s bits as a bitmap. It has three modes: Enrolment, Majority vote (the bitwise majority of all reads) and Stability map (the flip rate of each cell). Stripes or columns in the enrolment view mean bias, and bright cells in the stability map show where the noise is.

In Python: pa.simulate(), pa.PUFDataset.from_file(), PUFDataset.subset() and quality.bit_flip_rate().

2. Quality metrics

The Quality metrics view: analysis settings (enrolment reference, bootstrap B, CI level, NIST alpha, bootstrap seed) above a summary table of uniformity, bit-aliasing, uniqueness, reliability, steadiness, mean BER and stable-bit ratio with 95% confidence intervals and assessment badges

Analysis settings apply to every view, and the report and reproduction script record them:

  • Enrolment reference. First read (measurement 0) or Majority vote (the bitwise majority of all reads, a “golden” response).
  • Bootstrap B. The number of bootstrap resamples for the confidence intervals. The default is 1,000; 0 turns them off.
  • CI level. 90%, 95% or 99%.
  • NIST α. The significance level of the randomness tests. The default is 0.01, as NIST recommends.
  • Bootstrap seed. Makes the intervals reproducible.

Press Recompute after changing any of them.

The summary table gives each metric with its ideal value, interval and an assessment. Below it are bar charts per device and per bit position, plus the error rate of every bit position. For k devices with n-bit references Ri and T re-reads Ri,t:

Ui=HW(Ri)n,Q=2k(k−1)∑i<jHD(Ri,Rj)n,Reli=1−1T∑t=1THD(Ri,Ri,t)nU_i = \frac{\mathrm{HW}(R_i)}{n}, \qquad Q = \frac{2}{k(k-1)} \sum_{i<j} \frac{\mathrm{HD}(R_i, R_j)}{n}, \qquad \mathrm{Rel}_i = 1 - \frac{1}{T}\sum_{t=1}^{T} \frac{\mathrm{HD}(R_i, R_{i,t})}{n}

Here HW is the Hamming weight (number of 1s) and HD the Hamming distance (number of differing bits). Steadiness measures how consistently each cell of a device reads the same value. With pi,l the share of 1s that bit l of device i gives over its reads:

Si=1+1n∑llog⁡2max⁡(pi,l, 1−pi,l)S_i = 1 + \frac{1}{n}\sum_{l} \log_2 \max(p_{i,l},\, 1 - p_{i,l})

The confidence intervals resample devices, not bits. The interval should describe what happens if you had built a different batch of chips, and the chip is the unit of variation. For uniqueness, a device paired with its own duplicate in a resample is skipped, so it cannot create artificial zero distances. The demo gives uniqueness 50.13% with a 95% interval of 49.23 to 50.85, and reliability 99.54% (99.44 to 99.64).

One historical note: PUFAnalytics versions before 2.0 returned uniqueness as twice the mean inter-device distance (ideal 1.0 instead of 0.5). Version 2.0 fixed this, and the changelog says so, because a value copied from an old version into a new paper would be wrong by a factor of two.

Method notes: quality metrics. In Python, the pufanalytics.quality module has one function per metric.

3. Identification

Can a reader tell device A from device B, given one noisy read? This view treats the PUF like a biometric.

The Identification view: decidability 11.61, empirical EER 0, binomial threshold 16 bits, FAR 2.49e-19, FRR 8.05e-20; intra- and inter-device distance histograms with dashed binomial fits; and the acceptance threshold explorer

The histogram puts the two distributions side by side. The intra-device distances (a device against its own re-reads) sit near zero. The inter-device distances (device against device) sit near 50%. The further apart they are, the easier identification is. Three numbers summarise the gap:

  • Decidability d′ is the distance between the two means, in units of their pooled spread: d′=∣μinter−μintra∣ / (σinter2+σintra2)/2d' = |\mu_\text{inter} - \mu_\text{intra}| \,/\, \sqrt{(\sigma^2_\text{inter} + \sigma^2_\text{intra})/2}. Above about 6, errors are practically absent. The demo gives 11.61.
  • Empirical EER is the equal error rate read directly off the measured distances.
  • Binomial FAR and FRR model the distances as Binomial(n, p), with p fitted to each distribution.

A reader accepts a response when its distance to the enrolled one is at most t bits. Then the false-acceptance rate is FAR(t) = P(inter-distance ≤ t), and the false-rejection rate is FRR(t) = P(intra-distance > t). PUFAnalytics picks the optimal threshold t* that minimises the larger of the two.

The binomial model matters because measured data cannot show small error rates. The demo has 32 devices, so it has only 496 inter-device pairs, and the smallest non-zero FAR it could ever measure is about 1 in 500. The model extrapolates: at t* = 16 bits it gives FAR = 2.49 × 10−19.

Those tails are summed in log space, so they stay accurate below 10−300 rather than rounding to zero. A χ² goodness-of-fit p-value appears next to each fit, so you can see whether the binomial assumption holds for your data. The chart below shows the model’s main lesson for design: the error falls exponentially with response length.

Identification error vs response lengthWith inter-device distance at the ideal 50 percent, the identification error falls exponentially with response length. At a 1 percent bit error rate, 48 bits already reach one in a million; at 15 percent it takes between 128 and 192 bits.11e-101e-201e-30064128192256Response length n (bits)Identification error, min over t of max(FAR, FRR), log scaleone in a million1% BER5% BER10% BER15% BER
Show the numbers
Length1% BER5% BER10% BER15% BER
16 bits2.1e-31.1e-23.8e-27.9e-2
32 bits1.6e-58.7e-43.5e-31.6e-2
48 bits3.1e-73.1e-56.6e-43.4e-3
64 bits1.8e-92.7e-61.1e-48.0e-4
96 bits3.6e-139.7e-91.8e-66.6e-5
128 bits7.8e-173.6e-115.4e-85.8e-6
192 bits6.8e-255.6e-163.0e-112.8e-8
256 bits3.6e-329.1e-211.7e-141.8e-10
Every extra bit buys a constant factor of safety. Error under the binomial model with inter-device distance at the ideal 50%, from stats.identification_vs_length(). Empirical rates can never show anything this small: 32 devices give only 496 inter-device pairs, so the smallest error you can observe directly is about 1 in 500.

The acceptance threshold explorer has a slider for t. It shows the empirical and model FAR and FRR at the chosen threshold, and draws all four curves on a log axis. Further down are a DET curve (FRR against FAR, the standard biometric plot) and the inter-device distance matrix. A dark cell off the diagonal there means two devices that are suspiciously alike.

Method notes: identification. In Python: stats.decidability(), stats.far_frr_curve(), stats.binomial_far_frr(), stats.binomial_fit() and stats.identification_vs_length().

4. Randomness & entropy

The Randomness & entropy view: the NIST SP 800-22 table with p-values, a p-value bar, parameters and a pass/fail badge for each of eight tests, above a chart of Shannon and min-entropy per bit position

The enrolled responses of all devices are joined into one bit stream and run through eight tests from NIST SP 800-22:

TestWhat it looks forMinimum length
Frequency (monobit)too many 1s or 0s overall100 bits
Block frequencyimbalance within blocks of M bits100
Runstoo many or too few runs of identical bits100
Longest run of onesunusually long runs inside blocks128
Cumulative sumsdrift of the running ±1 sum (forward and backward)100
Approximate entropyover-frequent m-bit patternsdepends on m
Serialuneven frequencies of all overlapping m-bit patterns (two p-values)depends on m
Spectral (DFT)periodic structure1,000

The implementations reproduce the worked examples in the NIST standard to six decimal places. The spectral test uses an exact DFT for any length, through Bluestein’s algorithm, rather than padding to a power of two.

A test that does not have enough data says not applicable. It does not return a p-value, because a p-value from too little data looks like a result but is not one.

The demo fails one test: Serial, with p = 0.0037. That is worth reading closely rather than hiding. An Arbiter PUF’s response is a linear function of its challenge features, so its responses to related challenges are not fully independent. One failure in eight at α = 0.01 is also not proof of anything by itself. Run the test on more data before drawing a conclusion.

The tiles above the table give five entropy estimates, plus a correlation figure:

  • Shannon entropy and min-entropy per bit position, across the population;
  • the SP 800-90B Most Common Value estimate (0.930 bits per bit for the demo);
  • the SP 800-90B Markov estimate (0.970), which catches dependence between neighbouring bits;
  • normalised LZ76 complexity, about 1 for random data;
  • the mean absolute correlation between bit positions.

The charts below show entropy per bit position, the bit-position correlation matrix, and the autocorrelation of the responses with a 95% white-noise band.

Method notes: randomness and entropy. In Python: nist.run_all() and the eight test functions, plus the estimators module.

5. Figure studio

The figure studio draws publication-ready figures from the active data set. Download all figures (.zip) bundles them into one archive: an SVG and a CSV for every figure, a PNG at 3× resolution if you tick include PNG, and a captions file.

The Figure studio: a Download all figures button, then a violin plot of per-device uniformity and box plots of uniformity, reliability and steadiness

GroupFigures
Distributionsper-device uniformity (violin), box plots of per-device metrics, ECDFs of intra/inter distances, Q-Q plot against the binomial model, per-device BER by condition
Identification & reliabilityROC curve, identification error vs response length, temporal majority voting (TMV), bit-stability profile
Bit-level structurepolar bit-aliasing, entropy accumulation, bias vs instability per bit, uniformity vs reliability per device
3Dstability landscape (flip rate per device × bit), device quality space, inter-device distance bars, identification design space, key-failure surface

The temporal majority voting figure answers a practical question: if the device reads each bit m times and takes the majority, how much does the error drop? For a cell that flips with probability p, the leftover error is P[Binomial(m, p) > m/2], averaged over all cells. For the demo it falls from 0.38% with one read to 0.10% with fifteen.

Two 3D figures: inter-device distance bars for the first 20 devices, and the identification design space, a surface of log identification error over response length and intra-device BER

The 3D figures are drawn as SVG with no library behind them. Drag to rotate, scroll to zoom, or use Reset, Top, Front and Spin. The export is whatever view you are looking at. Method notes: figure studio. In Python: quality.tmv_ber() and stats.identification_vs_length().

6. Simulation studio

How does reliability change as noise grows? How many devices do you need before uniqueness settles? The simulation studio runs Monte-Carlo sweeps to find out.

Simulation studio results for an SRAM PUF swept over noise sigma: uniformity, bit-aliasing spread, uniqueness and reliability against noise, each with a shaded 95% band

To run a sweep:

  1. Pick a model and set the population: Devices, Response bits and Repeats.
  2. Set Seeds per point and First seed.
  3. Choose Sweep parameter 1, which becomes the x-axis. Any model parameter can be swept, and so can the population size, the response length or the number of repeats.
  4. Enter its values in one of three forms:
    • a list, such as 0.01,0.05,0.1;
    • a range, start:stop:steps, such as 0.02:0.5:8;
    • a log range, log:start:stop:steps.
  5. Optionally, choose a second parameter. That turns every metric into a 3D response surface.

Press Run sweep. The cost line shows how many simulations it will take.

Each grid point is simulated with every seed, and the result is the mean with a 95% band, mean ± 1.96·sd/√R for R seeds. The sweep uses common random numbers: replicate r gets seed s₀ + r at every grid point. Neighbouring points then differ only in the parameter, not in the luck of the draw, so the curves come out smooth with few seeds.

In the screenshot, SRAM reliability falls almost linearly from 100% to 80% as noise rises to 0.5, while uniqueness and uniformity do not move. Noise costs you reliability, not uniqueness.

CSV and Python script buttons export the sweep, and pa.sweep() runs the identical computation in Python. Method notes: simulation studio.

7. Attack lab

A PUF that a machine-learning model can predict is not a PUF. The attack lab runs the classic modelling attack: ridge-regularised logistic regression, fitted with Newton’s method, on the challenge-parity features φ(c) of the additive delay model.

The Attack lab: model tabs, stages, XOR values, noise, training and test CRP counts and seed, above a chart of held-out prediction accuracy against training set size for 1-, 2-, 3- and 4-XOR Arbiter PUFs

To run it:

  1. Pick Arbiter / XOR, Feed-forward or Interpose (1,y).
  2. Set the number of stages.
  3. List the variants to compare: XOR widths, loop counts or y values, such as 1,2,3,4.
  4. Set the noise, the training set sizes and the test set size.
  5. Press Run attack.

Each variant is a fresh simulated PUF, and the chart shows its accuracy on held-out challenges as the training set grows. With the defaults:

PUF100 CRPs1,000 CRPs10,000 CRPs
64-stage Arbiter (1-XOR)85.0%97.2%99.6%
2-XOR48.8%51.1%55.8%
3-XOR51.8%53.0%54.3%
4-XOR51.9%50.4%48.4%

A plain Arbiter PUF is broken with a few thousand challenge-response pairs (CRPs). XOR versions stay near 50%, which is guessing, because XORing chains makes the problem non-linear in φ and this learner can only draw a straight boundary.

This shows resistance to this attack only. Neural networks, CMA-ES and reliability-based attacks do break XOR Arbiter PUFs, and the app’s method notes say so.

Two more tools sit below the chart:

  • 3D learnability landscape plots accuracy over the number of stages and the training set size.
  • Attack your own CRPs takes pasted challenge,response lines, or the active simulated Arbiter data set, and runs the same attack on your own data.

Method notes: modelling attacks. In Python: attacks.simulated_attack(), attacks.modeling_attack() and attacks.logistic_regression().

8. Key generation

To turn a noisy PUF response into a stable cryptographic key, you need a fuzzy extractor. It combines an error-correcting code with helper data that is stored in public and lets the device repair its noisy read. The planner sizes one.

The Key generation planner: bit error rate, min-entropy per bit, key length and target failure inputs filled from the dataset, tiles showing 11 of 22 configurations feasible with BCH(127,64,10) the most efficient at 381 PUF bits and 189 helper bits, and charts of key failure against PUF bits and against BER

To use it:

  1. Enter four numbers: the bit error rate p, the min-entropy per bit h, the key length (64 to 256 bits) and the target key failure (10−3 to 10−12).
  2. When a data set is loaded, the form suggests its own values. Use the worst operating condition’s BER for a conservative design.

The planner tries every combination of an inner repetition code (r = 1 to 15) and an outer block code (Hamming, Golay, Reed–Muller or BCH). It then lists the feasible ones, cheapest first. It uses the code-offset model:

pin=P[Bin(r,p)>⌊r/2⌋],Pb=P[Bin(n,pin)>t],Pfail=1−(1−Pb)Bp_\text{in} = P\big[\mathrm{Bin}(r, p) > \lfloor r/2 \rfloor\big], \qquad P_b = P\big[\mathrm{Bin}(n, p_\text{in}) > t\big], \qquad P_\text{fail} = 1 - (1 - P_b)^{B}

Each block of nr PUF bits leaks nr − k bits through the helper data, which leaves nrh − (nr − k) bits of entropy. The number of blocks B is chosen so that the leftover entropy covers the key.

For the demo, with a worst-case BER of 0.62%, h = 0.838 and a 128-bit key at 10−6, the winner is BCH(127,64,10) with no repetition. It needs 3 blocks, 381 PUF bits and 189 helper bits, and its failure rate is 1.9 × 10−9.

The 3D key-failure landscape for BCH(127,64,10): log key failure over bit error rate and repetition factor, above the table of configurations sorted by PUF bits

The cost vs failure scatter plots every configuration, and the BER sensitivity curve shows how fast the selected design fails if the error rate drifts. The 3D landscape shows the same thing over BER and repetition at once. Click any row of the table to inspect it.

The model assumes independent bit errors and ignores decoder-specific and side-channel leakage. Method notes: key generation. In Python: ecc.plan_key_generation(), ecc.repetition_error() and ecc.block_failure().

9. Compare

Load or simulate two or more data sets, and Compare puts them side by side. Tick the data sets to include.

The Compare view: checkboxes for two datasets, a radar chart of their quality fingerprints, a bar chart of quality index, and the start of a side-by-side metrics table

The radar overlays up to three data sets; more would no longer be readable in colour. The quality index bar chart and the side-by-side table show all selected data sets, and the table exports as CSV or LaTeX. The usual use is comparing designs (Arbiter against SRAM against RO) or batches (two wafers, two temperatures).

The quality index is the mean of seven normalised scores, which the radar also shows:

  • uniformity;
  • bit-aliasing;
  • uniqueness;
  • reliability;
  • min-entropy;
  • identifiability, from d′;
  • the share of NIST tests passed.

It is a navigation aid, not a standard. The app says so, and so do I: report the individual metrics in papers. Method notes: quality index.

10. Report & export

Everything you need to publish and reproduce the evaluation, in one place:

The Report & export view: buttons for Markdown report, LaTeX table, full JSON, Python reproduction script, dataset CSV, copy BibTeX and print; an auto-written methods paragraph; and previews of the Markdown report and Python script

  • Markdown report: every metric, readable.
  • LaTeX table: a booktabs table of the headline metrics, ready for \input{}.
  • Full JSON: every number, per device and per bit. The structure is identical to the one Python’s pa.analyze() returns.
  • Python reproduction script: a short .py file that loads the exported CSV with your exact settings and recomputes everything. A reviewer can check your numbers without opening a browser.
  • Dataset CSV: the data in the long format above.
  • Copy BibTeX and Print / PDF.
  • Methods paragraph: written from your actual data set and settings, with references, ready to edit and paste into the paper.

11. Methods & docs

The last view is the reference manual: every definition, estimator and assumption behind the numbers, with formulas typeset in KaTeX.

The Methods & docs view showing the quality metrics section with typeset formulas for uniformity, bit-aliasing, uniqueness, reliability and steadiness

Each section has its own link, so you can cite exactly the part you mean:

SectionLink
Getting started#/docs/start
Data formats#/docs/formats
Quality metrics and confidence intervals#/docs/metrics
Identification statistics#/docs/identification
Randomness tests and entropy#/docs/randomness
Simulators#/docs/simulators
Figure studio, TMV and sweeps#/docs/figures
Modelling attacks#/docs/attack
Key generation#/docs/ecc
Quality index#/docs/index
Python package and CLI#/docs/python
References and citation#/docs/refs

The PUFAnalytics overview in dark mode

Reproducibility: same seed, same bits

A simulated result is only reproducible if someone else can simulate the same population. Saying “we simulated 32 Arbiter PUFs with noise 0.06” is not enough, because a different random generator gives different chips, and different numbers. PUFAnalytics makes the seed the whole story:

Scroll sideways to see the whole diagram.

One seed, one stream, one populationThe seed 2024 is expanded by SplitMix32 into the four-word state of xoshiro128**. The generator's output stream is read in a fixed order for the arbiter simulator: first 8,192 challenge bits, then 2,080 normally distributed delay weights, then 49,152 noise values. Python and JavaScript read the stream in exactly this order, so they build the same population.Seed 2024any integerSplitMix32seed → 4 × 32-bit statexoshiro128**period 2¹²⁸ − 1THE STREAM, READ LEFT TO RIGHT (NOT TO SCALE)Challenge bits128 × 64 = 8,192 bitsone u32 each, top bitDelay weights32 × 65 = 2,080 normalstwo u32 each, Box–MullerMeasurement noise32 × 12 × 128 = 49,152 normalsone per device, read-out and bitPython rng.bits(n·k) → rng.normals(d·(k+1)) → rng.normals(d·m·n)Browser the same three calls, in the same order, with identical integer arithmetic
Reproducibility is a contract about order. Same generator, same seed and the same order of draws give the same bits, so a population you simulate in the browser for a paper can be rebuilt by any reader with pa.simulate(..., seed=2024). Shown here for the demo population: 32 devices, 12 read-outs, 128 challenges on a 64-stage Arbiter PUF.

Two details make this hold across languages. xoshiro128** uses only 32-bit integer operations, which JavaScript does exactly with Math.imul and >>> 0. And normal variates use only the cosine branch of Box–Muller, so every normal uses exactly two uniforms and the stream never goes out of step.

The proof is in the numbers. These are the demo population’s results in the browser (from the screenshots above), next to the same population rebuilt in Python with pa.simulate("arbiter", n_devices=32, n_bits=128, n_repeats=4, seed=2024, ...):

ResultWeb appPython
Uniformity49.54% (CI 47.29–51.78)49.5361% (CI 47.2894–51.7822)
Uniqueness50.13% (CI 49.23–50.85)50.1276% (CI 49.2266–50.8494)
Reliability99.54% (CI 99.44–99.64)99.5361% (CI 99.4363–99.6404)
Decidability d′11.6111.6054
Binomial FAR at t* = 162.49 × 10−192.4935 × 10−19
NIST Serial p-values0.0623 / 0.00370.06228 / 0.00372
Best key designBCH(127,64,10), 381 bits, 1.89 × 10−9BCH(127,64,10), 381 bits, 1.89 × 10−9

The bootstrap intervals match too. They come from the same seeded generator, so the 1,000 resamples are the same 1,000 resamples.

Using the Python package

pip install "git+https://github.com/TakMashhido/PUFAnalytics.git"

It needs Python 3.8 or newer and NumPy, nothing else. SciPy is only used by the test suite, to cross-check the special functions.

Quick start

import pufanalytics as pa

# Real data: device,measurement,condition,response (binary or hex)
ds = pa.PUFDataset.from_file("measurements.csv")

# ...or the demo population from the web app, bit for bit
ds = pa.simulate("arbiter", n_devices=32, n_bits=128, n_repeats=4, seed=2024,
                 conditions=[("25C", 1.0), ("-40C", 1.8), ("85C", 2.6)],
                 stages=64, xor=1, noise=0.06)

result = pa.analyze(ds, reference="first", bootstrap=1000, seed=12345)
print(pa.to_markdown(result))           # readable report
pa.to_latex(result, "puf_table.tex")    # booktabs table for the paper
pa.to_json(result, "puf_full.json")     # every number, per device and per bit

pa.analyze() runs everything the web app’s views show and returns a plain dictionary. Its main keys are uniformity, uniqueness, reliability, ber, conditions, identification, entropy, nist, correlation and confidence_intervals.

Building blocks

Each analysis is also available on its own:

from pufanalytics import quality, stats, nist, estimators, attacks, ecc
import pufanalytics as pa

quality.uniqueness(ds)                  # % (ideal 50)
quality.reliability(ds)                 # % per device
quality.bit_flip_rate(ds)               # per-bit error rate, for dark-bit masking
quality.condition_ber(ds)               # {'25C': {'ber': 0.0024, ...}, '85C': {'ber': 0.0062, ...}}
quality.tmv_ber(ds)                     # BER after temporal majority voting
nist.run_all(ds.reference().ravel())    # the eight SP 800-22 tests
estimators.markov_min_entropy(ds.reference().ravel())
stats.identification_vs_length(0.05, 0.5, [64, 128, 256])
pa.sweep("sram", "noise", [0.05, 0.1, 0.2], param2="n_bits", values2=[64, 128], seeds=5)
attacks.simulated_attack(stages=64, xor=2, train_sizes=(100, 1000, 10000))
ecc.plan_key_generation(ber=0.0062, min_entropy=0.838, key_bits=128, target_failure=1e-6)

Command line

Installing the package also installs a pufanalytics command:

CommandWhat it does
pufanalytics simulate MODEL -o out.csvSimulate a population. Options: --devices, --bits, --repeats, --seed, --conditions 25C:1,85C:2.5 and repeatable --param key=value (such as --param xor=2). Writes .csv or .json.
pufanalytics analyze FILERun the full evaluation. Options: --markdown, --latex and --json to save reports, --reference first|majority, --bootstrap, --seed, --no-nist and --format auto|bin|hex.
pufanalytics nist FILEPrint the SP 800-22 results for the enrolled responses (--alpha, --reference).
pufanalytics attackLogistic-regression learning curve on a simulated Arbiter PUF (--stages, --xor, --noise, --train-sizes, --test-size, --seed).
pufanalytics ecc --ber PFuzzy-extractor plans (--min-entropy, --key-bits, --target, --top).
pufanalytics simulate sram -o sram.csv --devices 50 --bits 256 --conditions 25C:1,85C:2.5 --param bias=0.1
pufanalytics analyze sram.csv --markdown report.md --latex table.tex --json full.json
pufanalytics ecc --ber 0.0062 --min-entropy 0.838 --key-bits 128 --target 1e-6

The original v1 string-based functions (hamming_distance, uniqueness and the rest) still work unchanged, so old scripts keep running. One exception is uniqueness, which now follows the standard definition, as described above.

Full Python API reference

Every public function, class and method in the package is listed below, generated from the source code itself. Each name links to its definition on GitHub, pinned to the v2.1.0 commit, and each module links to the method notes that explain it. Open a module to see its functions.

104 public functions, classes and methods in 13 modules. Names link to the source line; Method notes opens the matching section of the web app's Methods & docs page.

pufanalytics.reportOne-call analysis and reports5
NameWhat it does
analyze(data, reference='first', bootstrap=1000, seed=12345, alpha=0.05, …)Run the complete PUFAnalytics evaluation.
to_json(result, path=None, indent=2)Serialise an analyze result (NaN/inf -> null).
headline_rows(r)Rows (metric, value, ideal, CI) shared by Markdown and LaTeX output.
to_markdown(result, path=None)Render an analyze result as a Markdown report.
to_latex(result, path=None, caption=None, label='tab:puf-metrics')Render the headline metrics as a LaTeX table (booktabs).
pufanalytics.datasetLoading and shaping data17
NameWhat it does
parse_response(token, fmt='auto')Convert a textual response into a uint8 bit vector.
parse_text(text, fmt='auto')Parse a textual data set.
PUFDataset(responses, device_ids=None, conditions=None, challenges=None, …)Container for PUF response measurements.
PUFDataset.n_devicesNumber of devices (property).
PUFDataset.n_bitsResponse length in bits (property).
PUFDataset.n_measurementsRead-outs per device, as a list (property).
PUFDataset.has_repeatsTrue when any device was measured more than once (property).
PUFDataset.condition_labelsOperating-condition labels in first-seen order (property).
PUFDataset.reference(method='first')Enrolled reference response of every device, shape (devices, bits).
PUFDataset.to_array()Return a (devices, measurements, bits) array (rectangular data only).
PUFDataset.subset(devices=None, bits=None)Return a new data set restricted to the given device / bit indices.
PUFDataset.from_bitstrings(responses, **kw)One response string per device.
PUFDataset.from_text(text, fmt='auto', name=None)Build a data set from CSV/TXT text (see parse_text).
PUFDataset.from_file(path, fmt='auto')Load a .csv/.txt (see parse_text), .npy or .json file.
PUFDataset.to_csv(path=None, fmt='bin')Write the long device,measurement,condition,response format.
PUFDataset.to_dict()Plain dict for JSON export; the web app imports it.
PUFDataset.from_dict(obj)Rebuild a data set from to_dict() output.
pufanalytics.qualityQuality metrics18
NameWhat it does
as_dataset(data)Coerce supported inputs into a PUFDataset.
uniformity(data, reference='first')Uniformity (fraction of 1s, %) of each device's reference response. Ideal 50 %.
bit_aliasing(data, reference='first')Bit-aliasing (%) per bit position across devices. Ideal 50 %.
inter_hd_matrix(data, reference='first')Symmetric devices x devices matrix of inter-device Hamming distances.
inter_hd(data, reference='first')All pairwise inter-device Hamming distances (integer), upper triangle.
uniqueness(data, reference='first')Uniqueness: mean pairwise inter-device fHD in %. Ideal 50 %.
intra_hd(data, reference='first')Intra-device Hamming distances between the reference and every re-measurement.
reliability(data, reference='first')Reliability (%) per device: 100 - mean intra fHD * 100. Ideal 100 %.
steadiness(data)Steadiness (%) per device (Maiti et al.): 100 (1 + mean_l log2 max(p_l, 1-p_l)).
bit_flip_rate(data, reference='first')Per-bit error rate: probability that bit l differs from the reference.
stable_bit_ratio(data)Fraction of (device, bit) cells that never changed over all measurements.
condition_ber(data)Mean bit error rate w.r.t. the enrolment read-out, per operating condition.
hamming_weights(data, reference='first')Hamming weight of every device's reference response.
bit_correlation_matrix(data, reference='first', max_bits=256)Pearson (phi) correlation between bit positions across devices.
autocorrelation(data, reference='first', max_lag=32)Mean normalised autocorrelation of the reference responses (±1 mapping).
bit_entropy(data, reference='first')Shannon entropy (bits) of every bit position across the device population.
bit_min_entropy(data, reference='first')Min-entropy (bits) of every bit position across the device population.
tmv_ber(data, votes=(1, 3, 5, 7, 9, 11, 13, 15))Bit error rate after temporal majority voting (TMV) over m read-outs.
pufanalytics.statsStatistics and identification16
NameWhat it does
igam(a, x)Regularised lower incomplete gamma function P(a, x).
igamc(a, x)Regularised upper incomplete gamma function Q(a, x) = 1 - P(a, x).
normal_cdf(x)Standard normal cumulative distribution function.
binom_logpmf(k, n, p)Natural log of the binomial probability mass function.
binom_pmf_vector(n, p)Return the binomial pmf for k = 0..n as a NumPy array.
binom_cdf(k, n, p)P(X <= k) for X ~ Binomial(n, p), accurate deep into the tails.
binom_sf(k, n, p)P(X > k) for X ~ Binomial(n, p), accurate deep into the tails.
decidability(intra, inter)Decidability index d' (Daugman) between intra- and inter-distance sets.
histogram_counts(hd_values, n_bits)Histogram of integer Hamming distances on the support 0..n_bits.
far_frr_curve(intra_hd, inter_hd, n_bits)Empirical FAR / FRR as a function of the HD acceptance threshold.
equal_error_rate(far, frr)Equal error rate and the threshold index where it is attained.
binomial_fit(hd_values, n_bits)Fit a binomial model Binomial(n_bits, p) to Hamming distances.
binomial_far_frr(n_bits, p_intra, p_inter)FAR / FRR under the independent-bit binomial model.
percentile_ci(samples, alpha=0.05)Two-sided percentile confidence interval of bootstrap replicates.
bootstrap_indices(n, n_resamples, seed=0)Generate bootstrap index sets with the portable RNG (reproducible).
identification_vs_length(p_intra, p_inter, lengths)Binomial-model identification error min_t max(FAR, FRR) for each response length.
pufanalytics.nistNIST SP 800-22 randomness tests9
NameWhat it does
monobit(seq, alpha=0.01, strict=True)2.1 Frequency (monobit) test. Requires n >= 100.
block_frequency(seq, alpha=0.01, block_size=None, strict=True)2.2 Frequency test within a block. Requires n >= 100.
runs(seq, alpha=0.01, strict=True)2.3 Runs test. Requires n >= 100.
longest_run(seq, alpha=0.01, strict=True)2.4 Test for the longest run of ones in a block. Requires n >= 128.
cumulative_sums(seq, alpha=0.01, strict=True)2.13 Cumulative sums (forward and backward). Requires n >= 100.
approximate_entropy(seq, alpha=0.01, m=None, strict=True)2.12 Approximate entropy test. Requires m < floor(log2 n) - 5.
serial(seq, alpha=0.01, m=None, strict=True)2.11 Serial test (two p-values). Requires m < floor(log2 n) - 2.
spectral(seq, alpha=0.01, strict=True)2.6 Discrete Fourier transform (spectral) test. Requires n >= 1000.
run_all(seq, alpha=0.01, strict=True)Run every implemented SP 800-22 test and return a list of results.
pufanalytics.estimatorsEntropy estimators5
NameWhat it does
shannon_entropy_rate(seq)Shannon entropy (bits per bit) of the empirical 0/1 distribution.
mcv_min_entropy(seq)SP 800-90B Most Common Value min-entropy estimate (bits per bit).
markov_min_entropy(seq, length=128)SP 800-90B Markov min-entropy estimate for binary data (bits per bit).
lz_complexity(seq)Normalised Lempel-Ziv (1976) complexity c(n) log2(n) / n.
population_entropy(data, reference='first')Summary of per-position entropy across a device population.
pufanalytics.simulatorsPUF simulators3
NameWhat it does
arbiter_features(challenges)Parity feature transform of the additive delay model.
ff_loops(stages, loops)(source, destination) stage pairs of evenly spaced, non-overlapping feed-forward loops.
simulate(model='arbiter', n_devices=20, n_bits=128, n_repeats=5, seed=1, …)Simulate a PUF population.
pufanalytics.sweepMonte-Carlo sweeps2
NameWhat it does
quick_metrics(ds)Fast headline metrics (no bootstrap, no NIST) used by sweeps.
sweep(model='arbiter', param='noise', values=(0.01, 0.05, 0.1), …)Sweep one (or two) simulator parameters.
pufanalytics.attacksModelling attacks3
NameWhat it does
logistic_regression(X, y, l2=0.01, max_iter=30, tol=1e-06)Fit ridge-regularised logistic regression with Newton's method.
modeling_attack(challenges, responses, train_sizes=(50, 100, 200, 500, 1000, …)Learning curve of a logistic-regression attack on supplied CRPs.
simulated_attack(stages=64, xor=1, noise=0.05, train_sizes=(50, 100, 200, 500, …)Learning curve of the attack against a simulated (k-XOR) Arbiter PUF.
pufanalytics.eccKey generation planning3
NameWhat it does
repetition_error(p, r)Residual bit error after majority decoding of an (r,1) repetition code.
block_failure(p, n, t)Probability that a block of n bits has more than t errors.
plan_key_generation(ber, min_entropy=1.0, key_bits=128, target_failure=1e-06, …)Enumerate ECC configurations and report the feasible ones.
pufanalytics.rngPortable random number generator9
NameWhat it does
PortableRNG(seed=0)xoshiro128** generator with a SplitMix32 seeder.
PortableRNG.next_u32()Return the next unsigned 32-bit integer.
PortableRNG.random()Uniform double in [0, 1).
PortableRNG.bit()A single fair random bit (the most significant bit of the output).
PortableRNG.normal()Standard normal variate (Box-Muller, cosine branch).
PortableRNG.randint(n)Uniform integer in [0, n).
PortableRNG.bits(count)Return a list of count fair bits.
PortableRNG.normals(count)Return a list of count standard normal variates.
PortableRNG.randoms(count)Return a list of count uniform doubles in [0, 1).
pufanalytics.cliCommand line1
NameWhat it does
main(argv=None)Entry point of the pufanalytics command (analyze, simulate, nist, attack, ecc).
pufanalytics.metricsOriginal string API (v1, kept for compatibility)13
NameWhat it does
hamming_distance(seq1, seq2)Compute the Hamming distance between two sequences of equal length.
bit_error_rate(expected, actual)Compute the bit error rate between two sequences of equal length.
intra_puf_variation(responses)Compute the intra-PUF variation.
inter_puf_variation(puf_responses)Compute the inter-PUF variation.
uniqueness(puf_responses)Compute the uniqueness metric for a list of PUF responses.
reliability(responses)Compute the reliability of a list of PUF responses.
avalanche_effect(responses)Compute the avalanche effect for a list of PUF responses.
uniformity(response)Compute the uniformity of a single PUF response.
bit_aliasing(responses)Compute the bit aliasing for a list of PUF responses.
correlation_coefficient(response)Compute the correlation coefficient between PUF responses.
entropy(puf_responses)Compute the entropy of a list of PUF responses.
mutual_information(puf_responses)Compute the mutual information between PUF responses.
min_entropy(puf_responses)Compute the min-entropy of a list of PUF responses.

How it is tested

  • NIST worked examples. Each SP 800-22 test reproduces the example sequences and p-values printed in the standard, to six decimal places.
  • SciPy cross-checks. The incomplete gamma functions, normal CDF and binomial tails are compared against SciPy to about 10−12 relative error, including far tails.
  • Cross-language parity. The over 2,400 values in golden.json, from Python, must be reproduced by the browser engine: simulated populations bit for bit, and statistics to floating-point tolerance.
  • Continuous integration runs the pytest suite and the parity test on every push. A separate workflow deploys the web app to GitHub Pages.
pytest -q                          # Python test suite
python tests/make_golden.py        # regenerate reference values from Python
node web/tests/parity.test.mjs     # the browser engine must match them

What it does not do

  • It assumes independent bit errors in the binomial identification model and in the key planner. Real PUFs often have correlated errors, for example cells that fail together when hot. Check the χ² fit p-value; when it is tiny, trust the empirical curves more than the extrapolated ones.
  • A resisted attack is not a security proof. The attack lab runs one classic learner. It is useful for showing why XOR helps, and useless for claiming that something is secure.
  • The quality index is a heuristic. It helps you compare designs at a glance. Do not put it in a paper as a metric.
  • The simulators are models, not silicon. They are good for intuition, for checking the pipeline, and for “what if” sweeps. They do not replace measuring real chips.

Citing PUFAnalytics

If PUFAnalytics helps your research, please cite the paper:

K. K. Mondal, A. Gupta and D. Das, “PUFAnalytics: Python Library for Comprehensive Evaluation and Analysis of PUFs,” 2025 30th Asia-Pacific Conference on Communications (APCC), IEEE, 2025, pp. 1–6, doi: 10.23919/apcc64555.2025.11279762.

The Copy BibTeX button in the app, and CITATION.cff in the repository, have the BibTeX entry.