Skip to content

SAGA v2: full test suite overhaul - #8

Open
jameshoweee wants to merge 11 commits into
masterfrom
jh/phase6-calibration
Open

SAGA v2: full test suite overhaul#8
jameshoweee wants to merge 11 commits into
masterfrom
jh/phase6-calibration

Conversation

@jameshoweee

@jameshoweee jameshoweee commented Jun 22, 2026

Copy link
Copy Markdown
Owner

Full overhaul of the SAGA test suite for Falcon / FN-DSA discrete Gaussian samplers, plus the fixes from TP's review. Individual phases are in #1-#7 for granular review; this PR is the whole thing against master.

Summary

  • 9 correctness bugs fixed in the original suite + sampler
  • proper Python packaging, pytest suite, CI, and table linting
  • analytic Renyi-divergence certification (bridges statistical testing and proof-level bounds)
  • extended univariate, sequence, and multivariate test batteries (~20 new tests)
  • flaw-injection power matrix + calibration
  • all review findings addressed (3 blocking bugs, 7 nits, family-wise verdict, licence, tag, testdata)

Phase-by-phase

Phase 0 - Bugfixes (#1): sampler.py sigmin (1.3 -> 1.2778) and BerExp scaling; sampler.c RCDT/CDT table; saga.py chisquare rounding, Doornik-Hansen rank-deficient path, Mardia two-sided p_B; removed dead code. Added seeded test-vector infrastructure (good/bad/mediocre tiers with expected detections) and a baseline scorer.

Phase 1 - Engineering (#2): pyproject.toml, pytest suite, GitHub Actions CI (3.10/3.12), table lint against mpmath at 400-bit precision, programmatic API (to_dict/to_json/effect_sizes).

Phase 2 - Analytic certification (#3): certification.py computes Renyi divergence R_a(spec || ideal) for the base and full sampler chains in mpmath. Reaches the 2^-75 resolution statistical testing cannot.

Phase 3 - Univariate battery (#4): tail exceedance (exact binomial at 3-6 sigma), sign/half-Gaussian factorisation, discrete Anderson-Darling and higher criticism (both MC-calibrated), and the MC calibration helper.

Phase 4 - Sequence tests (#5): Ljung-Box, Wald-Wolfowitz runs, block homogeneity - closes the "perfect marginals, serial dependence" (Markov-coupling) blind spot.

Phase 5 - Multivariate battery (#6): squared-norm chi2, Fisher+BH per-coordinate meta-analysis, max off-diagonal correlation, FFT-domain battery, cross-key homogeneity (energy distance), two-sample tests, Henze-Zirkler (MC-calibrated).

Phase 6 - Calibration (#7): flaw-injection library, power matrix (detection rate per test x flaw), p-value uniformity meta-test, default-battery selection.

Review response (TP, 2026-07-07)

Blocking bugs:

  • sampler.c mirror: the ascending CDT was paired with the RCDT "v < row" count, sampling 18 - z0 (mass at |z| ~ 18). Fixed the count to "v >= row" so it matches the Python sampler0(). Added tests/test_sampler_c.py + a CI step that compiles, runs, and chi2-tests the output against the folded PDT - the table lint validated the table but never the comparison logic.
  • berexp edge case: rejects when the quantized acceptance probability floors to 0 (was accepting ~255/256 via the p = -1 / 0xff path), matching the certification model.
  • R_a - 1 convention: now exp(ra) - 1 (was exp(ra*(a-1)) - 1, a factor a-1 too large). Side effect: the earlier "sigma=1.8 marginally fails" result was mostly this factor; it passes once corrected.

Nits: MC-calibrated max off-diagonal correlation; summed FFT variance vs chi2(2(n-1)); (1+#)/(B+1) MC p-value estimator; reproducible power-matrix seeds (no PYTHONHASHSEED-salted hash); eigenvector projection on the DH singular path; cdist-based energy distance.

Follow-on work in this PR

  • Family-wise verdict: the extended-battery verdict was an AND of k tests each at alpha (family false-alarm ~0.7% at alpha=1e-3, k=7). Replaced with one Fisher-combined global p plus a Bonferroni gate, so the family rate stays at alpha without masking a lone strong flaw. All individual results + component p-values still reported.
  • Licence: pyproject declared MIT, but the repo ships under the PQShield non-commercial research licence (LICENCE). Points at the file now.
  • Paper baseline tag: pqcrypto2020-as-published re-pointed to the true 2020 commit (was one commit late, including IMPROVEMENTS.md) and made annotated.
  • testdata/: removed the ~530MB reference-sample dir from the tree (suite uses generated vectors); preserved in the paper tag, restore documented in test_falcon().
  • Tooling: report_card.py (ANSI terminal report card with failure-fingerprint diagnosis), visualize_mockups.py (paper figures), and the 7-param x 65-flaw x 8-test x 100-rep calibration run (extensive_calibration.json).

Testing

pytest green on 3.10/3.12 (130 passed, 1 skipped). New compile-and-chi2 CI step guards the C sampler against comparison-logic regressions. Bad test vectors detected, good vectors clean.

Bug fixes:
- 0.1: chisquare() rounding correction applied to wrong variable;
  now corrects the exp list actually passed to scipy and accounts
  for outliers in n_effective (crashed on scipy >= 1.7)
- 0.2: sampler.c used PDT table with RCDT comparison logic, giving
  P(0) ~ 0.64 instead of 0.36; replaced with actual RCDT values.
  Also fixed doubled loop increment (i=i+1 inside for)
- 0.3: doornik_hansen() had a raise before rank-deficient recovery
  path, making recovery dead code
- 0.4: removed sampler_rep.py import and test_rejind() (descoped
  per unified plan; code preserved in pqcrypto2020-as-published tag)
- 0.5: test_basesampler() now accepts optional data argument,
  fixing arity mismatch with csv_testing()
- 0.6: mardia() p_B now two-sided (kurtosis can deviate either way)
- 0.7: BerExp scaling factor corrected from sigma/sigma0 to
  sigmin/sigma (matches HPRR20 algorithm and Falcon reference)
- 0.8: sigmin lowered from 1.3 to 1.2778 to accept spec-legal
  values (Falcon spec sigma_min = 1.277833697 for n=512)

Test infrastructure:
- generate_test_vectors.py: 83 seeded test vectors across 3 tiers
  (good/bad/mediocre) covering 10 flaw types
- run_baseline.py: scores current SAGA against vectors

Baseline results after fixes:
- Errors: 52/78 -> 0/78
- Bad detection: 10/42 (23.8%) -> 30/42 (71.4%)
- Good false alarms: 0/6 (0%)
- Remaining blind spots: Markov correlation (needs Ljung-Box,
  Phase 4) and tail truncation (needs AD/tail exceedance, Phase 3)
- pyproject.toml replacing requirements.txt (Python >=3.10,
  scipy >=1.11, numpy >=1.24, pandas >=2.0, pytest + mpmath as dev)
- Pytest test suite: 82 parametrized tests from generated vectors
  (good/bad/mediocre), plus table lint via mpmath recomputation
- GitHub Actions CI: matrix on Python 3.10/3.12, generates vectors
  on the fly, no large testdata download needed
- Table lint (test_tables.py): recomputes half-Gaussian PDT/CDT
  at 400-bit precision with mpmath, verifies Python and C tables
  match — deterministic detector for table errors like the Phase 0
  PDT/RCDT confusion
- Structured output: to_dict()/to_json() on UnivariateSamples and
  MultivariateSamples for machine-readable results
- Configurable params: tau, chi2_bucket, pmin are now constructor
  args with defaults, no longer module-level globals
- Fixed doornik_hansen rank-deficient path (column projection +
  index reset so dot products align)
- Cleaned up unused imports from test_rejind removal
- .gitignore for __pycache__, test_vectors, .eps, .pytest_cache
Certification layer (certification.py):
- Computes R_a(P_spec || P_ideal) exactly from the RCDT table and
  BerExp quantization, without sampling
- Base sampler: R_128-1 ~ 9.1e-23 (informational; CDT quantization)
- Full sampler: R_128-1 ~ 1.3e-29 at typical (mu,sigma), 6 orders
  of magnitude below the 2^{-75} target — BerExp rejection tightens
  the distribution dramatically
- Tested at hostile fractional mu (0.0, 0.25, 0.5) and across sigma
  range; all pass with large margin
- Finding: sigma=1.8 (near sigma0=1.8205 boundary) marginally fails
  at R_128-1 = 3.94e-23 vs 2.65e-23 target — genuine BerExp
  quantization effect at extreme sigma values

Effect-size diagnostics (saga.py):
- effect_sizes() method on UnivariateSamples: TV distance with
  null-expected bias floor, R_2 divergence (= log(1 + chi2/n)),
  resolution floor 1/sqrt(N)
- Framed as diagnostics, not security certification

Seeded determinism (test_sampler_determinism.py):
- Same seed produces identical output stream
- Different seeds produce different streams

README scope paragraph:
- Statistical layer detects bugs at delta ~ 1/sqrt(N)
- Certification layer certifies proof-level RD bounds
- Neither detects trace-level side-channel leakage

Test suite: 104 passed, 12 skipped (known blind spots), 0 failed
New tests (code/univariate_tests.py):
- Tail exceedance: exact binomial tests at 3-6 sigma thresholds;
  detects truncation that chi-square's bucket aggregation misses
- Sign/half-Gaussian factorization: (a) chi-square on |z| vs folded
  PDT, (b) per-|z| sign balance binomial test, (c) joint (sign,|z|)
  chi-square. Tests the z=(2b-1)*z0+b recombination step.
- Moment CIs: 95% confidence intervals on mean, stdev, skewness,
  kurtosis with asymptotic standard errors
- Discrete Anderson-Darling: Choulakian-Lockhart-Stephens tail-
  weighted EDF statistic, MC-calibrated (null depends on mu,sigma)
- Higher criticism: max standardized CDF deviation, near-optimal
  against sparse alternatives (single wrong table entry), MC-calibrated
- MC calibration helper: mc_calibrate() draws B replicates from the
  exact PDT, enabling correct p-values for any test statistic

Integration:
- UnivariateSamples.run_extended_battery() runs all new tests
- is_valid_extended = is_valid AND all extended tests pass
- to_dict() includes extended results when available
- run_baseline.py uses extended battery for scoring

Test vector update:
- Tail truncation changed from 6-sigma (undetectable at n=10000,
  mass ~e^{-18}) to 3-sigma (detectable, ~30 expected samples)

Baseline results:
- Bad detection: 71.4% -> 85.7% (tail truncation 0/6 -> 6/6)
- Good false alarms: 0/6 (0%) -- unchanged
- Remaining blind spot: Markov correlation (6/6 MISSED, Phase 4)

Test suite: 116 passed, 6 skipped (Markov), 0 failed
New tests added to univariate_tests.py:
- Ljung-Box (4.1): Q(h) = n(n+2) * sum r_k^2/(n-k) ~ chi2(h),
  tests serial autocorrelation up to lag 20. Catches Markov-
  coupled samplers with perfect marginals.
- Wald-Wolfowitz runs test (4.2): tests randomness of above/below-
  median sequence. ~20 lines, marginal power beyond Ljung-Box but
  catches non-linear dependence.
- Block homogeneity (4.3): chi-square homogeneity across 10 stream
  blocks. Catches mid-run drift or state corruption invisible to
  pooled distributional tests.

All three integrated into run_extended_battery().

Baseline results:
- Bad detection: 85.7% -> 100.0% (Markov 0/6 -> 6/6)
- Good false alarms: 0/6 (0%) — unchanged
- ZERO remaining blind spots across all 7 flaw types

Test suite: 122 passed, 0 skipped, 0 failed
New tests (code/multivariate_tests.py):
- Squared-norm test (5.1): ||x||^2/sigma^2 ~ chi2(dim) via KS.
  Catches radial flaws from coordinate correlation.
- Fisher+BH meta-layer (5.2): Fisher combination of per-coordinate
  p-values for global verdict + BH for localization. Fixes the
  informal "X out of dim pass" count.
- Max off-diagonal correlation (5.3): max|rho_ij| with Jiang's
  Gumbel limit. Catches single-pair correlation that diagcov's
  diagonal sums dilute.
- FFT-domain battery (5.4): per-frequency variance, Re/Im
  independence, higher criticism across frequencies. ffSampling
  flaws are localized in the FFT basis.
- Cross-key homogeneity (5.5): two-sample energy distance with
  permutation test. Tests the GPV key-independence property.
- Two-sample tests (5.6): per-coordinate KS + CvM with BH
  correction for cross-implementation comparison.
- Henze-Zirkler (5.7): MC-calibrated (not analytic log-normal,
  which is uncalibrated for p >= 128). Shares pairwise distance
  computation with energy test.

Integration:
- MultivariateSamples.run_multivariate_battery() runs all new tests
- to_dict() includes extended results

New test vectors:
- mv_bad_norm_inflated: all coords share common factor (norm test)
- mv_bad_cross_key: mixed sigmas simulating key-dependent output
- mv_med_fft_weak: frequency at 70% variance (subtle FFT flaw)

Test suite: 125 passed, 0 skipped, 0 failed
Calibration layer (code/calibration.py):
- Flaw-injection library: 16 parametrized broken samplers covering
  sigma shift (1-10%), mu shift, sign coupling (52-60%), tail
  truncation (3-4 sigma), table error (2-3x), contamination (1-5%),
  Markov coupling (rho 0.1-0.3)
- Power matrix: detection rate for each (test, flaw) pair over
  configurable replicates. Run with: python calibration.py --reps 20
- p-value uniformity meta-test: KS test of chi-square p-values
  against U[0,1] under H0. Detects miscalibration of the suite
  itself. Passes (KS p=0.16, mean_p=0.45).
- Default battery selection: tests that win on at least one flaw
  type. Result: chi2, tail_exceedance, sign_halfgauss, discrete_ad,
  higher_crit, ljung_box, runs_test (7/8 tests earn a spot;
  block_homog wins nowhere at n=5000 but may at higher n).

Key power matrix findings (n=5000, reps=5 smoke):
- tail_exceedance: sole detector of 3-sigma truncation
- ljung_box + runs_test: sole detectors of Markov coupling
- higher_crit: best at mu shift (catches at mu+0.1)
- discrete_ad: tied-best on sign bias and table errors
- chi2: strong on all diffuse alternatives

Test suite: 127 passed, 0 failed

@tprest tprest left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the full +3153/−150 diff at head jh/phase6-calibration. All claims below were verified by execution, not just reading. (Review prepared with Claude's assistance — every finding was reproduced before being reported.)

Verdict: request changes. ~90% of this is excellent and faithful to the unified plan — the test-vector infrastructure (good/bad/mediocre tiers with expected detections) is actually better than what the plan specified, and the chi², ccs, sigmin, Mardia fixes, the MC helper, AD/HC, and the MC-calibrated HZ are all correct. Three real bugs need fixing before merge, one of them demonstrated empirically below.


Bug 1 (blocking): sampler.c still samples the wrong distribution — now mirrored

The new table is the ascending CDT, but the comparison loop still implements the RCDT convention (z = #{k : v < table[k]}). With an ascending table this computes 18 − z0: the mirror image. I compiled and ran this branch's sampler.c; histogram of 1M samples:

z=-18: 179785   z=-17: 154375   z=-16: 98435   ...   z=-9: 2
z= 11: 11       ...   z= 18: 154870   z= 19: 179125

Mass concentrated at |z| ≈ 18–19 instead of 0–1. Repro:

cc -O2 sampler.c -o s && ./s > /dev/null && sort -n samples.txt | uniq -c

Fix, either one:

  • keep the comparison, use Falcon's descending RCDT (row k = P(z0 > k)·2^72 — the third commented block in sampler.py, reversed); or
  • keep the ascending CDT and flip the comparison to count v >= row.

Note the irony: test_tables.py::TestCRCDT passes — the C table really does equal the Python CDT. The lint validates the table, not the comparison logic. That's a spec gap in our unified plan, not this implementation. Concrete ask: add a CI step that compiles sampler.c, runs it, and chi²-tests the output against the folded PDT (~5 lines of workflow). Without it this bug class stays invisible to merge.

Bug 2 (serious): berexp p = −1 unfixed, and certification.py silently models the fixed version

sampler.py's berexp still has the FIXME case: when exp(−x)·sf·2^64 < 1, p = int(...) − 1 = −1, and (p >> i) & 0xff = 255 → accepts with probability ~255/256 instead of ~0. Meanwhile certification.py::compute_full_sampler_distribution models accept_p = 0 for p_berexp < 0 — i.e. the corrected behaviour, not the implemented one. So the actual Python sampler puts ~2⁻⁷² mass at |z| ≈ 18–19 where the ideal has ≲2⁻¹³⁰ (true R₁₂₈ blows up), while the certification reports "passes". The model diverges from the code exactly on the known edge case.

Fix: in berexp, if p < 0: return False (exact: acceptance is floor(y)/2^64 = 0 there). Then the certification model and the code agree.

Bug 3 (moderate): the certified quantity is not R_a − 1

R_a_minus_1 = exp(ra·(a−1)) − 1 computes Σ pᵃ/qᵃ⁻¹ − 1 ≈ (a−1)·(R_a − 1) — a factor a−1 (127 / 255) too large vs the exponential-form convention of [Pre17] that the 2⁻⁷⁵ requirement is stated in. Conservative direction (you fail earlier than required, ~7–8 bits of margin burned), and the tests still pass, but the reported number doesn't match the stated requirement. Fix: R_a_minus_1 = exp(ra) − 1 (≈ ra at this scale).

Statistical nits (none blocking)

Where Issue
max_offdiag_correlation Gumbel centering uses log(dim) but the max is over dim·(dim−1)/2 pairs — miscalibrated, borderline false alarms at d = 2048. Simplest robust fix: MC-calibrate like you did for HZ.
fft_domain_battery Averaged Re/Im variance is tested against χ²(n−1); the summed statistic vs χ²(2(n−1)) is the calibrated version. Current form is conservative (~√2 power loss).
mc_pvalue #{null ≥ obs}/B with B = 200 and α = 0.001: granularity 1/200 means the test only rejects at p = 0, actual size ≈ 0.5%. Use (1 + #{null ≥ obs})/(B + 1) and B ≥ 10/α.
compute_power_matrix hash(flaw_name) in the seed is randomized per process (PYTHONHASHSEED) → power matrix not reproducible. Use a stable hash or enumerate.
doornik_hansen rank-deficient path Selects data columns by eigenvalue index; should project onto the nonzero eigenvectors (data @ V[:, nonzero]). Reachable: mv_bad_fft_zeroed makes the covariance exactly singular.
_energy_distance Pure-Python pair loops × 201 permutations → minutes per call. scipy.spatial.distance.cdist makes it instant.
sampler.c comment Calls the table "RCDT"; it's the CDT (whichever fix you pick for Bug 1, make the comment match).

Plan/process items

  • Extended-battery verdict is an AND of 7 tests at α = 0.001 each (family α ≈ 0.7%); the plan's verdict semantics was report-first + one global Fisher p. Worth aligning before the interface freezes.
  • testdata/ (~1 GB) still in the tree (plan item 1.6), no pqcrypto2020-as-published tag visible, and everything targets this fork — needs routing to PQShield/SAGA upstream eventually.
  • pyproject.toml declares license = "MIT" — double-check against the existing LICENCE file.
  • PR description: "Bug rate 67% → 0%", "False alarm rate: 0%" — suggest dropping these (the first is undefined, the second is just fixed seeds), and "9 bugs fixed" needs revising given Bug 1.

Bottom line

Fix bugs 1–3 + add the compile-and-test CI step, and this is mergeable; the nits can be follow-ups. Genuinely impressive turnaround for ten days — the vector infrastructure and the MC calibration layer are keepers, and with the C-output test added, the suite will catch its own blind spot from now on.

Bug 1 (blocking): sampler.c paired an ascending CDT with the RCDT
"v < row" count, sampling 18 - z0 (mass piled at |z| ~ 18). Flip the
count to "v >= row" so it matches the Python reference sampler0().
Add tests/test_sampler_c.py plus a CI step that compiles, runs, and
chi2-tests the output against the folded PDT -- the table lint checked
the table but never the comparison logic.

Bug 2 (serious): berexp accepted with prob ~255/256 when the quantized
acceptance probability floored to 0 (p = -1, whose byte is 0xff).
Guard with `if p < 0: return False`, matching certification.py's model.

Bug 3 (moderate): reported R_a - 1 as exp(ra*(a-1)) - 1, i.e.
sum p^a/q^(a-1) - 1 ~ (a-1)(R_a - 1) -- a factor a-1 too large vs the
[Pre17] convention the 2^-75 requirement is stated in. Use exp(ra) - 1.

Nits:
- max_offdiag_correlation: MC-calibrate (the log(dim) Gumbel centering
  miscalibrated the max over dim(dim-1)/2 dependent pairs).
- fft_domain_battery: test the summed variance statistic against
  chi2(2(n-1)) instead of the averaged form against chi2(n-1).
- mc_pvalue: (1 + #{null >= obs}) / (B + 1) so finite B stays valid.
- calibration: reproducible per-flaw seeds (drop PYTHONHASHSEED-salted
  hash()); enumerate index instead.
- doornik_hansen: project onto the nonzero eigenvectors on the singular
  path instead of dropping arbitrary data columns by eigenvalue index.
- _energy_distance: cdist-based, no subsampling.

Also: pyproject declared license = MIT, but the repo ships under the
PQShield non-commercial research licence (see LICENCE). Point at the
file instead of asserting a wrong SPDX id.
@jameshoweee

Copy link
Copy Markdown
Owner Author

thanks for the really thorough review, the C-output test gap was a great catch. all three blockers are fixed on the branch now:

  • bug 1: flipped the count to v >= row so it matches the python sampler0(). mirror's gone, histogram peaks at 0/1 like it should. added tests/test_sampler_c.py that compiles the C, runs it and chi2s against the folded pdt, plus a CI step, so this cant sneak back in
  • bug 2: berexp returns False when p < 0 now, agrees with the certification model
  • bug 3: switched to exp(ra) - 1. interesting side effect, the "sigma=1.8 marginally fails" thing i mentioned earlier was mostly this factor, it passes comfortably once corrected

did the nits too: MC-calibrated the max off-diag corr, summed fft variance stat vs chi2(2(n-1)), (1+k)/(B+1) mc pvalues, reproducible power-matrix seeds, eigenvector projection on the DH singular path, cdist energy distance. and you were right about the licence, pointed pyproject at LICENCE instead of claiming MIT.

left the testdata cleanup and the upstream routing to PQShield/SAGA for a seperate pass. pushed to the same branch.

The battery verdict was an AND of 7 (uni) / 5 (multi) per-test passes,
each thresholded at alpha -- inflating the family false-alarm rate to
~1-(1-alpha)^k (about 0.7% at alpha=1e-3, k=7). Replace it with one
family verdict:

  all_pass = (Fisher-combined global p > alpha) AND no test rejects at
             the Bonferroni threshold alpha/k

Fisher controls the family-wise rate against diffuse alternatives; the
Bonferroni gate keeps a single strong localized flaw from being diluted.
Individual results and per-test p-values are still reported
(report-first); global_pvalue / fisher_stat / bonferroni_rejects are
exposed for downstream use.
The SAGA v2 suite runs on generated, seeded vectors, not the original
HPRR20 reference sample files. Drop the ~530MB testdata/ directory from
the working tree and gitignore it. The data is preserved verbatim in the
pqcrypto2020-as-published tag; test_falcon() documents how to restore it
(git checkout pqcrypto2020-as-published -- code/testdata).

Note: removes it going forward only; the blobs remain in history. A full
history purge is a separate, destructive step left for later.
- report_card.py: ANSI terminal "report card" that runs the full battery
  on a sample set and prints a per-section pass/fail summary with
  component p-values, the global verdict, and per-test "check:" hints
  plus a failure-fingerprint match (nearest calibration flaw family).
  Supports --flaw for demoing injected defects.
- visualize_mockups.py: matplotlib figures (power-matrix heatmap,
  detection frontiers, p-value calibration, FFT spectrum, Manhattan,
  ACF, Renyi profile, QQ norms) for the paper. Illustrative layouts;
  swap in real run data before publication.
- extensive_calibration.json: results of the 7-param x 65-flaw x 8-test
  x 100-rep power-matrix sweep (n=10000). Generated figures are
  gitignored (regenerate from the scripts).

@tprest tprest left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed the four new commits (a69f2ac3..8210fe9d), same method as before — every fix verified by execution. (Review prepared with Claude's assistance.)

All three bugs are fixed, and the fixes are confirmed empirically:

  1. sampler.c — compiled from this branch and ran 1M samples: mode at z ∈ {0,1} at 0.1797 (expected 0.17975), and a formal chi² against the folded PDT gives p = 0.31. Correct. The new comment documenting the CDT-vs-RCDT trap is a nice touch.
  2. berexp — the p < 0 guard matches the exact quantized acceptance (floor(y)/2^64 = 0), and the code/certification agreement is now explicit in both files.
  3. R_a − 1 — now exp(ra) − 1, matching the [Pre17] convention the 2⁻⁷⁵ requirement is stated in.

Everything else from the review is addressed: the compile-and-chi²-test CI step (test_sampler_c.py — well built, this closes the lint blind spot for good), MC-calibrated max-offdiag with the (1+#)/(B+1) correction, χ²(2(n−1)) in the FFT battery, Davison–Hinkley mc_pvalue, enumerated seeds, DH eigenvector projection, cdist energy distance, Fisher + Bonferroni verdicts in both batteries, testdata/ removal with the pqcrypto2020-as-published tag in place — and good catch on the licence not being MIT.

Two non-blocking follow-ups (fine as later PRs):

  • In the extended-battery Fisher pool, the tail_exceedance component is the raw min of 4 dependent p-values — slightly anti-conservative as a U[0,1] component. A Bonferroni-adjusted min (min(1, 4·min_p)) would make it clean.
  • The sign-balance p-values are no longer in the component pool (only magnitude_chi2 is), so a pure sign-bias flaw with a correct |z| marginal wouldn't trip the extended verdict on its own — the main χ² still catches it, but worth adding a Bonferroni-adjusted sign component for completeness.

Note: I only spot-checked the unrequested additions (report_card.py, run_extensive_calibration.py, visualize_mockups.py, and the committed extensive_calibration.json — the latter is results data in-repo, slightly against the regenerate-don't-commit spirit, but it's small).

Approving. Impressive turnaround — twice now.

@tprest

tprest commented Jul 8, 2026

Copy link
Copy Markdown

Routed upstream: this branch is now PQShield/SAGA#3 (pushed as saga-v2, your commits/authorship intact). The pqcrypto2020-as-published tag now exists upstream too, and the two stale Dependabot PRs there are closed as superseded. Feel free to merge this one on your fork or close it in favour of the upstream PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants