diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 6c6bf7f..8636759 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -7,6 +7,9 @@ on: jobs: tests: runs-on: ubuntu-latest + env: + OPENBLAS_NUM_THREADS: "1" + OMP_NUM_THREADS: "1" strategy: matrix: python-version: ["3.10", "3.12"] @@ -22,3 +25,5 @@ jobs: - run: python -m pip install -r requirements.txt - run: python scripts/run_smoke_tests.py - run: python scripts/final_scientific_validation.py + - run: python scripts/check_repository.py + - run: python scripts/reproduce_publication_figures.py diff --git a/.gitignore b/.gitignore index 8c47abb..aa93132 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,9 @@ final_audit.txt *.zip *.tar.gz *.tgz + +# Regenerated checks and figures +generated/ +*.egg-info/ +build/ +dist/ diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..82e77e9 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,10 @@ +# Repository working rules + +- Keep research prose and audit reports in Markdown. Use plain-text or Unicode mathematics; do not introduce TeX sources, BibTeX, PDF deliverables, or a TeX build dependency. +- Preserve the existing license and author attribution. +- Keep proved finite-ensemble identities, asymptotic consequences, heuristic approximations, finite numerical observations, and historical estimates explicitly distinguished. +- Never infer a von Neumann entropy maximum from the purity minimum or its dense-side crossover. Never label local peak checks as a global search. +- Treat `data/` CSV files and `outputs/` CSV/PNG files as released evidence. Default computations write under `generated/`. Evidence changes require an explicit reason and an updated provenance record; never silently refresh the checksum manifest. +- Keep public preprint metadata tied to a verified public version, rather than an unpublished working title. +- Before delivery run the unit tests, `scripts/final_scientific_validation.py`, `scripts/check_repository.py`, and a CSV figure redraw if plotting changed. For computational changes use a small smoke run; do not launch high-n searches without a concrete research need. +- A validator PASS is scoped to its listed checks. It is not a novelty or submission-readiness verdict. diff --git a/CHANGELOG.md b/CHANGELOG.md index 2bb9109..79ae4c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,14 +1,24 @@ # Changelog +## Markdown cleanup — 2026-09-22 + +- Replaced manuscript-readiness reports with a scoped sanity check, research proofs, reproducibility instructions, and primary references in Markdown. +- Corrected the public preprint title and year; aligned the existing rc2 software version labels without creating a release tag. +- Fixed support validation, coefficient-matrix basis ordering, and invalid Rényi inputs. +- Reworked scientific validation to read released data and compare production routines, with explicit failures that survive Python optimization. +- Made all seven CSV figure redraws and reduced simulations write under `generated/`; changed default graphics to PNG. +- Removed PDF duplicates and obsolete text reports. Preserved all released CSV/PNG evidence and the license byte-for-byte, with checksums. + + ## 1.0.0-rc2 — 2026-08-21 - Added the general power-law support window - $1/2<\gamma<3/4$ implied by the exact average-purity formula. -- Distinguished the purity-optimal interior balance $\gamma=2/3$ from the - dense-side boundary $\gamma=3/4$. + 1/2 < γ < 3/4 implied by the exact average-purity formula. +- Distinguished the purity-optimal interior balance γ = 2/3 from the + dense-side boundary γ = 3/4. - Clarified that the finite-size peak-support regression is a separate numerical observation and not an asymptotic consequence of the purity bound. -- Standardized ordinary Shannon-entropy notation from $H_2$ to $H$. +- Standardized ordinary Shannon-entropy notation from H₂ to H. - Kept all seven released figures, Table I values, and numerical datasets unchanged. diff --git a/CITATION.cff b/CITATION.cff index 1df213b..40ab912 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -1,8 +1,7 @@ cff-version: 1.2.0 -message: "Please cite the accompanying paper and this software release." +message: "Please cite the public preprint and the repository commit used." title: "Support-size entanglement trajectories of random subset states: code and data" -version: "1.0.0-rc2" -date-released: 2026-08-21 +version: "1.0.0rc2" repository-code: "https://github.com/GoGoKo699/Subset-states" url: "https://arxiv.org/abs/2501.06292" license: MIT @@ -18,8 +17,8 @@ authors: affiliation: "Centre for Quantum Technologies, National University of Singapore" preferred-citation: type: article - title: "Support-size entanglement trajectories of random subset states" - year: 2026 + title: "Arithmetic sequences as quantum states" + year: 2025 authors: - family-names: Lin given-names: Ruge diff --git a/PROVENANCE.md b/PROVENANCE.md index cb53090..19613da 100644 --- a/PROVENANCE.md +++ b/PROVENANCE.md @@ -1,154 +1,45 @@ -# Numerical provenance and robustness notes +# Evidence and provenance -This document records the evidence hierarchy behind the publication release. -It is intentionally more technical than the repository landing page. +This document describes the evidence present in the repository, rather than treating earlier review verdicts as evidence. The cleanup started from commit [4cb99f1](https://github.com/GoGoKo699/Subset-states/tree/4cb99f109194124f500d75dd065ff43581a2682d), dated 21 August 2026. -## Table I peak estimates +## Evidence hierarchy -`data/table_i_peaks.csv` contains the retained production estimates -$(\widehat{M}_n,\widehat{S}_n)$ for even $n=10,\ldots,30$. +| Material | What can be checked | Remaining limitation | +|---|---|---| +| Exact mean-state, purity, diagonal-entropy and residue formulas | Counting proofs in [research notes](docs/RESEARCH.md), independent enumeration, production-code comparisons | Enumeration alone is not a proof for arbitrary size | +| `data/table_i_peaks.csv` | Transcription of Table I in the [2025 preprint](https://arxiv.org/html/2501.06292v1), arithmetic regressions, agreement with released derived tables | No complete original global-search records, raw peak samples, search uncertainty or full execution provenance | +| `outputs/peak_verification/` | Local grids and quadratic fits for n = 10, 12, 14, 16, 18, 20 | These are neighborhood checks around given centers, not global rediscovery; files named `samples` hold aggregate statistics at each M | +| `outputs/fig1/` | Released individual entropy samples and summaries | Support labels and a full original execution manifest are absent | +| `outputs/fig3/` | One n = 24 reduced-state spectrum | One sample does not establish typicality or a random-matrix limit | +| `outputs/fig4/`, `outputs/fig6/` | Summary curves and plotting inputs | Complete per-state records are not released here | +| `data/residue_matched_*` | 12,000 paired entropy observations, 12 group summaries, seeds, and residue populations | Raw support labels are absent; a limited seed replay checks implementation consistency, not every stored observation | +| `outputs/fig7/` | Partition entropy samples for two selected n = 20 states | Does not establish a uniform statement about all supports or all cuts | -The complete intermediate global-search records for -$n=22,24,26,28,30$ were not retained. Repeating those searches with the -released dense exact-entropy implementation is computationally expensive. The -recorded point estimates are therefore preserved as historical production data, -not reconstructed raw data. +CSV files and retained PNGs are unchanged by the cleanup. Their SHA-256 checksums are recorded in [the evidence manifest](validation/evidence_sha256.json), alongside the original license. This freezes the inherited evidence; it does not certify the original data-generation process. -The repository supplies `scripts/peak_scaling_verification.py`, which samples a -local window around a recorded $\widehat{M}_n$ and fits a quadratic in -$\log_2 M$. This verifies neighbourhood consistency; it is not a global -rediscovery procedure. +## Retained peaks and finite fits -## Robustness of the finite-size regressions +All eleven Table I rows, for even n = 10 through 30, appear in the public 2025 preprint. Their publication establishes the source of the numbers, not independent reproducibility of the global searches. In particular, the complete intermediate searches at n = 22, 24, 26, 28, 30 were not retained. -Using all eleven retained rows, $n=10,\ldots,30$, +The unweighted regressions recomputed from the CSV are: -$$ -\widehat{S}_n=0.509300\,n-0.990091, -$$ +| Range | Entropy fit | Log-support fit | +|---|---|---| +| n = 10–30 | Ŝ = 0.509300n − 0.990091 | log₂ M̂ = 0.703541n − 0.357734 | +| n = 10–20 | Ŝ = 0.514457n − 1.065190 | log₂ M̂ = 0.693450n − 0.210116 | -$$ -\log_2\widehat{M}_n=0.703541\,n-0.357734. -$$ +The slopes differ by roughly 1.01% and 1.43%, respectively, relative to the full-range slopes. This is a descriptive sensitivity check. It supplies neither missing uncertainty estimates nor a proof that the historical estimates are unbiased. Regression standard errors describe scatter of the rounded table entries, not uncertainty in the original search. -Restricting the fit to the computationally accessible range -$n=10,\ldots,20$ gives +The effective ratio log₂ M̂/n grows from about 0.674 to 0.694 across the table. These finite observations establish no limiting exponent. In particular, the purity crossover at exponent 3/4 does not predict convergence of the entropy maximizer to that exponent. An entropy slope above 1/2 also cannot persist asymptotically, since S ≤ n/2. -$$ -\widehat{S}_n=0.514457\,n-1.065190, -$$ +## Residue-matched comparisons -$$ -\log_2\widehat{M}_n=0.693450\,n-0.210116. -$$ +The structured supports contain labels x with 1 ≤ Ω(x) ≤ k, where Ω counts prime factors with multiplicity and k = 1, 2, 3. For n = 14, each support is compared with four ensembles matching populations modulo 2ᵗ, for t = 0, 1, 2, 3. Each group contains 1,000 samples, and the same sampled support is evaluated before and after the QFT. -The slope changes are approximately 1.01% for peak entropy and 1.43% for peak -support size. The unreconstructed larger-size rows extend the observed trend but -do not create or materially change it. Neither regression is claimed as an -asymptotic theorem. +A remaining deficit means that uniform placement conditional on those counts does not reproduce the structured entropy. It does not identify the cause or a unique fingerprint of primality. Zero sampled null entropies below a structured value is a finite rank observation, not a zero tail probability. The exact computational-basis residue ceiling is not automatically a Fourier-basis ceiling. -## Analytic scale hierarchy and peak interpretation +## Historical material -For a power-law support $M=cN^\gamma$ with fixed $c>0$, the exact -average-purity formula has the leading structure +The old manuscript-readiness review, bibliography audit, duplicated text validation reports, and PDF copies of PNG figures were removed from the active tree. They remain available at the starting commit above. No manuscript source or complete revised bibliography was present in that tree, so this cleanup does not claim a lossless conversion of an unseen manuscript. -$$ -\overline{P}_{N,M} -= -2N^{-1/2} -+c^{-1}N^{-\gamma} -+c^2N^{2\gamma-2} -+\text{lower-order terms}. -$$ - -This gives the following hierarchy. - -- Every fixed $1/2<\gamma<3/4$ yields - - $$ - \overline{S}_{N,M}\geq \frac{n}{2}-1-o(1). - $$ - -- The exponent $\gamma=2/3$ uniquely balances the sparse and rectangle - corrections. Optimizing the remaining coefficient gives - $M=2^{-1/3}N^{2/3}+O(1)$. - -- The exponent $\gamma=3/4$ is the dense-side boundary at which the rectangle - term becomes comparable with the $2N^{-1/2}$ balanced-cut contribution. At - $M=cN^{3/4}$, - - $$ - \overline{P}_{N,M} - = - (2+c^2)N^{-1/2}+o(N^{-1/2}). - $$ - -These are statements about the exact average purity and the entropy lower bound -derived from it. They do not locate the maximizer of the mean von Neumann -entropy. - -For the retained numerical peaks, the pointwise effective exponent - -$$ -\gamma_n^{\mathrm{eff}} -= -\frac{\log_2\widehat M_n}{n} -$$ - -increases from approximately $0.674$ at $n=10$ to $0.694$ at $n=30$. -The full-range fitted slope is $0.703541$. This motivates comparison with the -$3/4$ dense-side boundary but does not establish convergence to $3/4$ or any -other asymptotic peak law. No released figure or numerical dataset was altered -for this interpretation. - -## Exact analytical validation - -The release exposes code for: - -- the exact fixed-cardinality ensemble-mean reduced state; -- the exact ensemble-average purity; -- the hypergeometric diagonal-entropy curve; -- the residue-class entropy ceiling - $S(A)\leq n/2-t+H(\mathbf{p}^{(t)})$. - -`tests/test_exact_purity_and_residue.py` and -`scripts/final_scientific_validation.py` independently check these formulas by -exhaustively enumerating all 65,535 nonempty supports at $n=4$. - -## Residue-matched reference ensembles - -For each almost-prime support $U_{N,k}$ with $k=1,2,3$, the released -Figure 5 analysis uses four nested null ensembles: - -| matched low bits $t$ | retained information | -|---:|---| -| 0 | support cardinality only | -| 1 | even/odd populations | -| 2 | populations modulo 4 | -| 3 | populations modulo 8 | - -For every $(k,t)$, 1,000 supports are independently sampled while preserving -the complete residue-count vector. The same support is evaluated in the -computational and Fourier bases, giving paired observations. Seeds are recorded -in `data/residue_matched_summary.csv`; all 12,000 paired samples are in -`data/residue_matched_samples.csv`. - -The residual deficit after matching a constraint means “not explained by the -matched information under uniform placement.” It is not claimed to be a unique -fingerprint of primality or to isolate all higher-order arithmetic correlations. - -## Figure files and generated data - -The manuscript-ready vector PDFs in `outputs/fig1/` through `outputs/fig7/` are -the files used for the final manuscript candidate. Existing historical output -CSVs in the GitHub repository may be retained when applying this release as an -overlay. Figure 5’s final raw and summary data are included directly in this -release because the constrained-null calculation is new and central to the -revised manuscript. - -## Historical best-of-random-candidates experiment - -The former “greedy” Figure 8 experiment is not part of the manuscript. It -screened independent random candidates and selected the one with the largest -mean balanced-cut entropy; it was not a greedy construction. The code is kept -under `exploratory/best_of_random_candidates/` with corrected terminology. +The historical best-of-random-candidates experiment remains under [exploratory](exploratory/best_of_random_candidates/README.md). It selects among independent random candidates; it is not a greedy construction. diff --git a/README.md b/README.md index 58bdba4..057511f 100644 --- a/README.md +++ b/README.md @@ -1,196 +1,61 @@ -# Support-size entanglement trajectories of random subset states - -Code, data, released figures, and validation for the manuscript -**“Support-size entanglement trajectories of random subset states.”** -The manuscript updates the existing preprint record [arXiv:2501.06292](https://arxiv.org/abs/2501.06292). - -Equal-positive-amplitude subset states exhibit a rise–peak–fall entanglement -trajectory as their computational-basis support grows. This repository contains -the fixed-cardinality ensemble calculations, exact-moment checks, numerical -figure scripts, and constrained reference ensembles used in the paper. - -## Key results represented in this repository - -- Exact ensemble-mean reduced state for uniformly random supports of fixed size. -- Exact average purity across a balanced bipartition, yielding a rigorous - power-law support window, an optimal interior balance, and a dense-side - boundary. -- Numerical rise–peak–fall trajectories and retained peak estimates through - $n=30$. -- Exact hypergeometric diagonal-entropy and residue-class entropy bounds. -- Cardinality-, parity-, mod-4-, and mod-8-matched reference ensembles for - almost-prime supports, evaluated before and after a quantum Fourier transform. - -## Analytic scale hierarchy - -Let $N=2^n$ and write a power-law support size as $M=cN^\gamma$, with -$c>0$. The exact fixed-cardinality average-purity formula has the leading -structure - -```math -\overline{P}_{N,M} -= -2N^{-1/2} -+c^{-1}N^{-\gamma} -+c^2N^{2\gamma-2} -+\text{lower-order terms}. -``` +# Subset states: support size and entanglement -**Near-maximal window.** Every fixed exponent -$\frac{1}{2}<\gamma<\frac{3}{4}$ gives +How does entanglement change when an equal-positive-amplitude quantum state occupies more computational-basis labels? A single label and the full basis both give product states. Random supports in between can give high bipartite entanglement. -```math -\overline{S}_{N,M} -\geq -\frac{n}{2}-1-o(1). -``` +This repository contains exact finite-ensemble formulas, numerical experiments, and released evidence for that question. **The exact mean-state and purity results survive the September 2026 sanity check. The asymptotic location of the von Neumann entropy maximum and the novelty of the revised research contribution remain unresolved.** -**Purity-optimal interior scale.** The exponent -$\gamma=\frac{2}{3}$ uniquely balances the sparse -$N^{-\gamma}$ correction and the dense rectangle -$N^{2\gamma-2}$ correction. Optimizing the remaining prefactor gives +The related public preprint is [*Arithmetic sequences as quantum states* (2025), arXiv:2501.06292v1](https://arxiv.org/abs/2501.06292v1), by Ruge Lin, Germán Sierra, and José I. Latorre. “Support-size entanglement trajectories of random subset states” was the working title of a later revision; it is not the title of the current arXiv record checked on 22 September 2026. -```math -M -= -2^{-1/3}N^{\frac{2}{3}}+O(1). -``` +## Read the project -**Dense-side boundary.** The exponent $\gamma=\frac{3}{4}$ is the point at -which the rectangle contribution enters at the same $N^{-1/2}$ order as the -balanced-cut background. +| Document | Purpose | +|---|---| +| [Research notes](docs/RESEARCH.md) | Definitions, exact formulas and proofs, approximations, and figures | +| [Sanity check](docs/SANITY_CHECK.md) | Findings, corrections, and limits of the validation | +| [Provenance](PROVENANCE.md) | What the released data can and cannot substantiate | +| [Reproducibility](docs/REPRODUCIBILITY.md) | Commands, environments, and computational costs | +| [References](docs/REFERENCES.md) | Verified primary sources and the open novelty question | -The numerical maximum of the mean von Neumann entropy is a separate quantity. -Over $n=10,\ldots,30$, the retained estimates give +All research prose is Markdown with plain-text or Unicode mathematics. There is no TeX source, bibliography build, or PDF workflow. Python code, CSV evidence, PNG figures, and machine-readable metadata retain their useful formats. -```math -\log_2 \widehat{M}_n -= -0.703541\,n-0.357734. -``` +## What is established? -The pointwise effective exponent is +For an even number of qubits n, write N = 2ⁿ and d = 2^(n/2). Choose M distinct basis labels uniformly, assign each amplitude 1/√M, and split the qubits into two fixed halves. -```math -\gamma_n^{\mathrm{eff}} -= -\frac{\log_2 \widehat{M}_n}{n}, -\qquad -\gamma_{10}^{\mathrm{eff}}\approx 0.674, -\qquad -\gamma_{30}^{\mathrm{eff}}\approx 0.694. -``` +| Result | Status | +|---|---| +| Exact ensemble-mean reduced state and average purity | Derived by counting; independently checked on all nonempty supports at n = 4 | +| Mean entropy at least n/2 − 1 − o(1), for M = cN^γ and 1/2 < γ < 3/4 | Consequence of the exact purity formula, for fixed c > 0 | +| Purity-minimizing support M = 2^(−1/3)N^(2/3) + O(1) | Analytic result; does not locate the von Neumann entropy maximum | +| Rise–peak–fall curves and retained peak estimates through n = 30 | Finite numerical evidence; original global-search records are incomplete | +| Hypergeometric diagonal-entropy bound and low-bit residue ceiling | Exact bounds with stated ensemble/cut assumptions | +| Almost-prime deficits after matching cardinality and residues | Finite comparisons at n = 14; no unique arithmetic fingerprint established | -It increases over the retained range. This motivates comparison with the -$\frac{3}{4}$ dense-side boundary but does not establish an asymptotic peak law. +![Released support-size trajectory](outputs/fig1/fig1_concentration.png) -## Repository map +This figure summarizes sampled finite systems. It is not a concentration theorem or an asymptotic scaling result. -```text -subset_states/ reusable scientific routines -scripts/ figure, validation, and local-verification entry points -data/ Table I and released matched-null datasets -outputs/ manuscript-ready figures and generated outputs -tests/ lightweight and exhaustive small-system regression tests -validation/ final independent validation reports -exploratory/ historical experiments not used in the manuscript -docs/ audit and release documentation -``` - -## Quick start +## Run the checks ```bash python3 -m venv .venv source .venv/bin/activate -python3 -m pip install -r requirements.txt -python3 scripts/run_smoke_tests.py -``` - -Run the independent publication validation: - -```bash -python3 scripts/final_scientific_validation.py -``` - -Regenerate the fast deterministic figures and verify that all seven released -PDFs are present: - -```bash -python3 scripts/reproduce_publication_figures.py -``` - -A reduced computational smoke run is available through: - -```bash -python3 scripts/reproduce_publication_figures.py --smoke -``` - -The full publication computations can be launched with `--full`, but several -runs are intentionally expensive. - -## Publication figures - -| Figure | Scientific content | Main script | Released output | -|---|---|---|---| -| 1 | Support-size trajectory and cut comparison | `scripts/fig1_concentration.py` | `outputs/fig1/fig1_concentration.pdf` | -| 2 | Peak entropy and support-size scaling | `scripts/fig2_peak_scaling.py` | `outputs/fig2/fig2_peak_scaling.pdf` | -| 3 | Reduced-state spectrum near the peak | `scripts/fig3_spectral.py` | `outputs/fig3/fig3_spectral_bulk.pdf` | -| 4 | Sparse and dense approximations | `scripts/fig4_approximation.py` | `outputs/fig4/fig4_approximation.pdf` | -| 5 | Cardinality and residue controls | `scripts/fig5_qft_residue_controls.py` | `outputs/fig5/fig5_qft_residue_controls.pdf` | -| 6 | Rényi trajectories | `scripts/fig6_renyi.py` | `outputs/fig6/fig6_renyi.pdf` | -| 7 | Entropy over balanced cuts | `scripts/fig7_partitions.py` | `outputs/fig7/fig7_partitions.pdf` | - -Figure 5 can be redrawn exactly from the released CSV files. The matched-null -raw dataset contains 12,000 paired computational/Fourier samples. - -## Table I and local verification - -`data/table_i_peaks.csv` stores the retained peak estimates used in the paper. -The command - -```bash -python3 scripts/peak_scaling_verification.py --n-values 10 12 14 +python -m pip install -r requirements.txt +python scripts/run_smoke_tests.py +python scripts/final_scientific_validation.py +python scripts/check_repository.py ``` -performs a local neighbourhood check around selected tabulated values. It is not -a reconstruction of the original global search. Full technical provenance and -the robustness comparison are documented in [PROVENANCE.md](PROVENANCE.md). - -## Figure 5 constrained ensembles - -The released files are: - -```text -data/fig5_random_qft_summary.csv -data/fig5_almost_prime_unions.csv -data/residue_matched_summary.csv -data/residue_matched_samples.csv -data/residue_entropy_bounds.csv -``` - -Regenerate the null samples with: +Redraw all seven figures from released CSV files, without repeating simulations: ```bash -python3 scripts/run_residue_controls.py -python3 scripts/fig5_qft_residue_controls.py +python scripts/reproduce_publication_figures.py ``` -## Reproducibility levels - -1. **Fast:** unit tests, exact small-system checks, Figure 2, and Figure 5 redraw. -2. **Standard:** regenerate selected numerical figures with reduced or custom - workloads. -3. **Computational:** use publication defaults for the full sampling runs and - local high-dimensional peak checks. - -All stochastic scripts expose seeds. The tested software environment is recorded -in `requirements-tested.txt`. - -## Citation +The historical command name is retained for compatibility. New outputs go under `generated/`; the released evidence stays in `data/` and `outputs/`. See [reproducibility](docs/REPRODUCIBILITY.md) for reduced simulations and expensive runs. -Use the citation metadata in `CITATION.cff`. The preferred paper citation is the -updated arXiv record and, once available, the journal version. +## Scope and citation -## License +This is a research repository, not a submission-readiness certificate. The original global searches, especially n = 22–30, have not been reconstructed. Small changes in fitted slopes when dropping those rows do not resolve that provenance gap. Broader novelty claims require comparison with existing sparse-state and fixed-size subset-state results. -The repository is released under the MIT License. See `LICENSE`. +Use [CITATION.cff](CITATION.cff) for the public preprint, and cite a specific repository commit when using this revised implementation or data. The existing [MIT license](LICENSE) and author attribution are preserved. diff --git a/data/README.md b/data/README.md index 26a9a9d..c76bae8 100644 --- a/data/README.md +++ b/data/README.md @@ -1,11 +1,15 @@ # Released data -- `table_i_peaks.csv`: retained peak estimates used in Table I and Figure 2. -- `peak_verification_schedule.csv`: default local-check workloads. -- `fig5_random_qft_summary.csv`: random-support baseline for Figure 5(a). -- `fig5_almost_prime_unions.csv`: deterministic almost-prime points. -- `residue_matched_summary.csv`: constrained-null summary statistics. -- `residue_matched_samples.csv`: all 12,000 paired constrained-null samples. -- `residue_entropy_bounds.csv`: residue populations and exact entropy ceilings. +These inherited CSV files are preserved as evidence. Regeneration defaults write under `generated/`; they do not replace these files. -See `../PROVENANCE.md` for interpretation and limitations. +| File | Contents | +|---|---| +| [table_i_peaks.csv](table_i_peaks.csv) | Historical peak estimates from Table I of the 2025 preprint; original global searches are not fully reconstructable | +| [peak_verification_schedule.csv](peak_verification_schedule.csv) | Default local-check workloads; high-n entries are computationally expensive | +| [fig5_random_qft_summary.csv](fig5_random_qft_summary.csv) | Cardinality-matched computational/Fourier baseline summaries | +| [fig5_almost_prime_unions.csv](fig5_almost_prime_unions.csv) | Deterministic almost-prime support entropies | +| [residue_matched_summary.csv](residue_matched_summary.csv) | Twelve matched-ensemble summaries and seeds | +| [residue_matched_samples.csv](residue_matched_samples.csv) | 12,000 paired entropy observations; no raw support labels | +| [residue_entropy_bounds.csv](residue_entropy_bounds.csv) | Residue populations and exact computational-basis entropy ceilings | + +See [provenance](../PROVENANCE.md) for limitations and [research notes](../docs/RESEARCH.md) for definitions. diff --git a/docs/CITATION_AUDIT.txt b/docs/CITATION_AUDIT.txt deleted file mode 100644 index 889441a..0000000 --- a/docs/CITATION_AUDIT.txt +++ /dev/null @@ -1,18 +0,0 @@ -FINAL CITATION AUDIT -==================== - -Citation occurrences in main.tex: 58 -Unique citation keys used: 33 -Entries in Citations.bib: 33 -Missing bibliography keys: none -Unused bibliography entries: none -Duplicate bibliography keys: none -Biber warnings or errors: none - -Positioning checks completed: -- Prior fixed-size random-subset moment work is cited where the exact moment section begins. -- The compressed-support result is distinguished from pseudorandomness and does not claim to rederive indistinguishability results. -- Sparse-support, constrained-ensemble, prime-state, QFT, random-matrix, Page, and Renyi references are attached to their specific scientific roles. -- No citation cluster is used to imply prior authorship of the manuscript's new exact purity or residue-ceiling results. - -STATUS: PASS diff --git a/docs/FINAL_MANUSCRIPT_REVIEW.md b/docs/FINAL_MANUSCRIPT_REVIEW.md deleted file mode 100644 index 0289764..0000000 --- a/docs/FINAL_MANUSCRIPT_REVIEW.md +++ /dev/null @@ -1,154 +0,0 @@ -# Historical target-reader and submission-level review - -> **Historical record.** This review is dated 17 August 2026 and predates the -> three-scale manuscript revision, the public repository release, and the -> `1.0.0-rc2` metadata. It is retained as a development record. For current -> scientific provenance, see [PROVENANCE.md](../PROVENANCE.md) and the files -> under [`validation/`](../validation/). - -**Manuscript:** *Support-size entanglement trajectories of random subset states* -**Target:** *Journal of Physics A: Mathematical and Theoretical* -**Review date:** 17 August 2026 - -## Verdict - -The manuscript is scientifically coherent, internally consistent, readable as a quantum-information paper, and ready for author-level confirmation and repository release. I found no remaining mathematical, numerical, citation, or LaTeX defect that blocks submission. - -The main result hierarchy is now clear: - -1. exact ensemble mean reduced state; -2. exact average-purity theorem; -3. rigorous near-maximal average-entanglement guarantee at support size \(2^{(2/3)n+O(1)}\); -4. finite-size rise--peak--fall von Neumann trajectory through \(n=30\); -5. sparse hypergeometric and dense separated-mode explanations; -6. cardinality- and residue-matched application to almost-prime supports. - -The paper no longer depends on the historical arithmetic framing for its identity. The almost-prime analysis functions as a controlled application of the general subset-state framework. - -## Substantive refinements made in the final pass - -### Prior-work positioning - -The exact-moment section now acknowledges recent multi-copy moment analyses of fixed-size random subset states. The manuscript states precisely that its contribution is the direct balanced-bipartition formula for the ensemble-mean reduced state and average purity, rather than implying that fixed-size subset-state moments were previously unstudied. - -The finite-size scaling section now relates the rigorous \(N^{2/3}\) support scale to known pseudorandom-subset-state regimes while explicitly distinguishing entanglement from indistinguishability. - -### Claim calibration - -The abstract now says that the estimated peak support grows exponentially while occupying a decreasing fraction of the basis **over the studied range**. It does not convert the finite-size fit into an asymptotic law. - -Single-instance results are consistently described as sampled or selected states. Figure 3 is one sampled spectrum; Figure 7 concerns two selected states. No concentration theorem is inferred from those examples. - -### Dense-side interpretation - -The dense approximation is written in the transparent form - -\[ -T_{N,M}=h_2(\lambda_{\mathrm{mf}}) - +(1-\lambda_{\mathrm{mf}}) - \left(\log_2 d-\frac{1}{2\ln2}\right), -\] - -which separates the binary mixing entropy of the coherent mode from the rescaled Page-like bulk entropy. This is algebraically identical to the previous expression. - -### Residue-matched application - -Fourier-space matched means and deficits are now explicitly defined: - -\[ -\mu_t^F(B)=\mathbb E_{A\sim\mathcal E_t(B)}S_F(A), -\qquad -\Delta_t^F(B)=\mu_t^F(B)-S_F(B). -\] - -The manuscript uses **deficit reduction** rather than causal language such as “fraction explained.” It also distinguishes two different statements: - -- the exact parity ceiling gives a guaranteed gap from the algebraic balanced maximum; -- the parity-matched null ensemble measures the residual gap from a cardinality-matched random mean. - -### Readability - -The long Section 3 heading was manually broken between “support” and “entanglement” to avoid an unattractive hyphenated section title. No substantive compression or expansion was needed elsewhere. - -## Independent scientific validation - -The final validation script exhaustively enumerated all 65,535 nonempty supports at \(n=4\). - -| Check | Maximum discrepancy or violation | -|---|---:| -| Ensemble-mean reduced matrix | \(2.07\times10^{-14}\) | -| Exact average-purity formula | \(8.88\times10^{-16}\) | -| Residue ceiling, \(t=1\) | 0 | -| Residue ceiling, \(t=2\) | 0 | -| Independent entropy implementations | \(5.55\times10^{-16}\) bits | - -The Table I regressions were reconstructed exactly: - -\[ -\widehat S_n=0.5093n-0.9900909\ldots, -\] - -\[ -\log_2\widehat M_n -=0.703540951\ldots n-0.357734368\ldots. -\] - -The Page gap and support fraction decrease monotonically across all eleven retained rows. The old and revised dense-ansatz formulas agree to \(2.66\times10^{-15}\) bits over the complete \(n=14\) support range. - -The residue-control summary reproduces the displayed residual deficits, deficit reductions, Prime-state parity ceiling, and all six finite-sample rank statements. Full numerical output is in `FINAL_SCIENTIFIC_VALIDATION.txt`. - -## Bibliography audit - -- 33 bibliography entries; -- 33 entries cited; -- no missing keys; -- no unused keys; -- no duplicate keys; -- no Biber warning or error. - -The citations are generous but role-specific. Prior work is cited for terminology, random-state benchmarks, constrained ensembles, subset-state moments, sparse supports, prime-state structure, and QFT entanglement without obscuring which propositions and theorems are new here. - -## LaTeX and PDF validation - -The clean source compiles with - -```text -pdflatex main.tex -biber main -pdflatex main.tex -pdflatex main.tex -``` - -Final status: - -- 24 A4 pages; -- approximately 5,581 body words by `texcount`; -- 189 abstract prose words, excluding its two displayed formulas; -- no unresolved citation or reference; -- no LaTeX/package warning; -- no overfull or underfull box; -- all fonts embedded; -- PDF openable and unencrypted; -- not a scanned PDF; -- all pages rendered and visually inspected; -- no clipping, overlap, black box, or broken glyph. - -## Journal fit - -The paper is best submitted as a regular **Paper** in the journal's quantum-mechanics and quantum-information section, with additional relevance to physical combinatorics, asymptotics, number theory, and random matrices. The manuscript is shorter in words than a typical long JPhysA Paper, but its 24-page review layout contains seven figures, formal results, and four proof/diagnostic appendices. Padding would reduce readability and is not recommended. - -## Author and release confirmations still required - -These are administrative or authorial decisions, not scientific defects: - -1. Confirm the author order and each current affiliation. -2. Confirm the corresponding author, current email addresses, and submission-system ORCID information. -3. Confirm the acknowledgement wording and all funder/grant identifiers. -4. Confirm the conflict-of-interest declaration. -5. Make the GitHub repository public before submission, create a release tag, and preferably archive the release with a persistent DOI. -6. Once the repository is public, change the data-availability sentence from future tense to a stable “openly available” statement and update the software citation with the release identifier. -7. Upload this manuscript as the revised version of arXiv:2501.06292 and ensure the arXiv metadata match the new title and abstract. - -## Final assessment - -After the seven confirmations above, the manuscript is suitable for submission-level packaging. No further scientific expansion is recommended before peer review. diff --git a/docs/REFERENCES.md b/docs/REFERENCES.md new file mode 100644 index 0000000..4b78c32 --- /dev/null +++ b/docs/REFERENCES.md @@ -0,0 +1,23 @@ +# References and research positioning + +Primary records checked on 22 September 2026. This is a targeted literature check, not a completed novelty assessment or a reconstruction of the absent revised manuscript bibliography. + +| Source | Role in this project | +|---|---| +| Ruge Lin, Germán Sierra, José I. Latorre, [Arithmetic sequences as quantum states](https://arxiv.org/abs/2501.06292v1), arXiv:2501.06292v1 (2025) | Public predecessor. Its [Table I](https://arxiv.org/html/2501.06292v1) contains the retained n = 10–30 peak values. The currently listed title is not the later working title used in old repository metadata. | +| Fernando Granha Jeronimo, Nir Magrafta, Pei Wu, [Pseudorandom and Pseudoentangled States from Subset States](https://arxiv.org/abs/2312.15285), arXiv:2312.15285 (2023; revised 2024) | Prior analysis of the same equal-positive-amplitude, fixed-size random subset ensemble. The multiple-copy calculation includes falling-factorial inclusion probabilities; see Equation 3.2 in the [full text](https://arxiv.org/html/2312.15285v2). | +| Tudor Giurgica-Tiron, Adam Bouland, [Pseudorandomness from Subset States](https://arxiv.org/abs/2312.09206), arXiv:2312.09206 (2023) | Independent prior subset-state moment and indistinguishability analysis. Equal-amplitude subset states and their phase-free pseudorandomness are not introduced by this repository. | +| Giuseppe De Tomasi, Ivan M. Khaymovich, [Multifractality meets entanglement: relation for non-ergodic extended states](https://arxiv.org/abs/2001.03173), [Physical Review Letters 124, 200602 (2020)](https://doi.org/10.1103/PhysRevLett.124.200602) | Sparse random states can reach Page-scale entanglement despite occupying a vanishing fraction of the basis. Their Gaussian nonzero amplitudes distinguish that ensemble from the equal-positive-amplitude model here. | +| Don N. Page, [Average Entropy of a Subsystem](https://arxiv.org/abs/gr-qc/9305007), [Physical Review Letters 71, 1291 (1993)](https://doi.org/10.1103/PhysRevLett.71.1291) | Haar-random reference mean and its asymptotic behavior; a benchmark for a different ensemble. | + +## What the comparison establishes + +The broad fact that sparse support can coexist with high entanglement is established prior work. Fixed-cardinality subset moments are also established objects of study. The counting arguments in the research notes should therefore be read as direct derivations and checks for this repository, without a claim that moment counting itself is new. + +The equal-positive-amplitude assumption matters: full support is a product state, and the coherent dense-side contribution cannot be transferred unchanged from random-phase or Gaussian-amplitude ensembles. + +It remains necessary to compare the exact balanced-cut purity formula, its finite-size minimization, and the residue ceilings directly against prior equations before asserting a new publishable theorem. This cleanup makes no claim of quantum speedup, computational pseudorandomness, a unique primality fingerprint, or a settled asymptotic entropy-peak exponent. + +## Citation metadata + +`CITATION.cff` names the existing public preprint with its actual title and year. Its software title describes this repository separately. Cite the commit used for revised code or data. A working title in a README is not evidence of a new arXiv version or a journal publication. diff --git a/docs/REPRODUCIBILITY.md b/docs/REPRODUCIBILITY.md new file mode 100644 index 0000000..84b55b1 --- /dev/null +++ b/docs/REPRODUCIBILITY.md @@ -0,0 +1,72 @@ +# Reproduce the checks and figures + +Run commands from the repository root. Python code and CSV data remain executable and machine-readable; all narrative documentation and reports use Markdown. + +## Environment + +```bash +python3 -m venv .venv +source .venv/bin/activate +python -m pip install -r requirements.txt +``` + +The cleanup was checked with Python 3.12.14, NumPy 2.3.5, SciPy 1.17.0, and Matplotlib 3.10.8. For those numerical package versions use `python -m pip install -r requirements-tested.txt`. That file pins the direct dependencies, not every transitive dependency or BLAS implementation. CI checks the supported Python 3.10 and 3.12 configurations against the dependency ranges in `requirements.txt`. + +Matplotlib uses its own math-text renderer for plot labels, with external TeX disabled. No document compiler is required. + +## Scientific validation + +```bash +python scripts/run_smoke_tests.py +python scripts/final_scientific_validation.py +python scripts/validate_residue_results.py +python scripts/check_repository.py +``` + +The test suite covers exact identities, API input rejection, basis ordering, numerical entropy behavior, validator failure cases, and figure-output isolation. The comprehensive validator independently enumerates all nonempty supports for n = 2 and n = 4, compares against production routines, reads the actual peak table, and checks released residue summaries against the 12,000 observations. The residue command is a focused view of those checks. Default validators print Markdown and do not modify released evidence. + +To save a new report: + +```bash +python scripts/final_scientific_validation.py --output generated/scientific-validation.md +``` + +Choose a new Markdown path; report writing rejects existing files and protected evidence destinations. Acceptance checks remain active under `python -O`. A passing report is limited to the checks it lists: it does not reconstruct the high-n global searches or establish novelty. + +The repository check verifies frozen evidence hashes, local Markdown link paths, and the absence of TeX/PDF artifacts. It does not fetch external links or inspect missing historical records. + +## Three figure workflows + +| Command | Work performed | Output directory | +|---|---|---| +| `python scripts/reproduce_publication_figures.py` | Redraw all seven figures from released CSVs; no simulations | `generated/figures/` | +| `python scripts/reproduce_publication_figures.py --smoke` | Small fresh numerical runs, including Figure 5 baselines and matched controls; Figure 2 refits the retained table | `generated/smoke/` | +| `python scripts/reproduce_publication_figures.py --full` | Original-size figure computations; expensive; Figure 2 still uses the historical table | `generated/full/` | + +The driver rejects destinations that overlap the released `data/` or `outputs/` trees. Missing input CSVs cause failure even if old images are present. Individual scripts also default to generated directories. Explicit custom destinations on individual scripts remain the caller's responsibility. + +All outputs are PNG by default. Pixel identity with historical images is not guaranteed across library/font versions. Fresh simulations may differ in floating-point details even with the same seed. CSV redraw demonstrates that a figure can be reconstructed from its supplied inputs; it does not independently regenerate those inputs. + +## Local peak check + +A modest example is: + +```bash +python scripts/peak_scaling_verification.py --n-values 10 --samples 20 --points 7 +``` + +This samples a neighborhood around a supplied Table I value, then fits a quadratic in log₂ M. It does not search the entire support-size range. The original global searches for the historical peaks are not part of `--full` either. + +Inspect a workload without executing it: + +```bash +python scripts/peak_scaling_verification.py --all-table --dry-run +``` + +The default schedule reaches dimensions that can be expensive with dense exact entropy. A d-by-d real coefficient matrix needs 8N bytes before copies, reduced states, or SVD workspace; a complex state vector needs 16N bytes. At n = 30 these alone are 8 GiB and 16 GiB. Dense SVD time grows roughly as N^(3/2), multiplied by support sizes and sample counts. The September cleanup did not run those high-n searches or the full figure simulations. + +## Evidence protection + +[The manifest](../validation/evidence_sha256.json) records all 50 released CSV/PNG files and the original license. `generated/` is ignored by Git. The manifest's hashes were computed from the pre-cleanup commit and checked against the working tree; they must not be silently regenerated to hide a mismatch. + +See [provenance](../PROVENANCE.md) for original-data limitations and [the sanity check](SANITY_CHECK.md) for corrections made during this cleanup. diff --git a/docs/RESEARCH.md b/docs/RESEARCH.md new file mode 100644 index 0000000..9c4f207 --- /dev/null +++ b/docs/RESEARCH.md @@ -0,0 +1,178 @@ +# Research notes: equal-positive-amplitude subset states + +These notes state the mathematical model, derive the exact results represented in the code, and separate them from approximations and numerical evidence. They are the maintained research text for this repository. Entropies and logarithms use bits unless `ln` is written. + +## 1. Model and endpoints + +Let n be a positive even integer, N = 2ⁿ, and d = 2^(n/2). Choose a support A uniformly among all M-element subsets of the N computational-basis labels, with 1 ≤ M ≤ N. Define + +```text +|A⟩ = (1/√M) ∑ over x in A of |x⟩. +``` + +All nonzero amplitudes are real and positive. Random phases or Gaussian nonzero amplitudes give different ensembles. A fixed balanced cut identifies each label with a pair (a,b) in a d-by-d grid. Write X for its binary incidence matrix and C = X/√M. The right reduced state is ρ = XᵀX/M. For a general complex coefficient matrix with entries C[a,b], the corresponding formula is ρ = CᵀC*, where * denotes entrywise conjugation. Its eigenvalues are the squared singular values of C. + +The von Neumann entropy is S = −∑ λ log₂ λ, with zero terms omitted. Rényi-2 entropy is −log₂ Tr(ρ²); min-entropy is −log₂ λmax. Always 0 ≤ S ≤ log₂ d = n/2. + +A singleton support is a computational-basis product state. Full support is |+⟩ raised to the n-fold tensor product and is also unentangled. A permutation-matrix support of size d is maximally entangled. Thus support size alone does not specify an individual state's entropy. The curves here describe ensembles indexed by M, not a physical time evolution or a nested growth process. + +Uniform support sampling is invariant under bit permutations, so every fixed balanced cut has the same ensemble distribution. This does not imply that all cuts of an individual support have the same entropy. + +## 2. Exact mean reduced state + +For k distinct grid cells, the probability that all are occupied is + +```text +pₖ = (M)ₖ / (N)ₖ, +(z)ₖ = z(z−1)…(z−k+1), +pₖ = 0 when k > M. +``` + +Each diagonal entry of ρ sums d occupied-cell indicators divided by M. Each off-diagonal entry sums d products of two distinct indicators divided by M. Therefore + +```text +E[ρ] = (1/d − β) I + β J, +β = (M−1) / [d(N−1)], +``` + +where J is the all-ones matrix. Its uniform-vector eigenvalue is 1/d + (d−1)β. Each of its other d−1 eigenvalues equals 1/d − β. + +This is the spectrum of the mean matrix. It is not the mean of the ordered sample eigenvalues. For example, M = 1 gives E[ρ] = I/d even though every sample has zero entropy. Concavity gives E[S(ρ)] ≤ S(E[ρ]), not equality. + +Implementation: `fixed_cardinality_mean_spectrum` in [core.py](../subset_states/core.py). + +## 3. Exact average purity + +Expand Tr(ρ²) as the sum of X[a,b] X[a,c] X[a′,c] X[a′,b] over all four indices, divided by M². There are three cases: + +| Index pattern | Number of terms | Distinct occupied cells | +|---|---:|---:| +| a = a′ and b = c | N | 1 | +| Exactly one equality | 2N(d−1) | 2 | +| Neither equality | N(d−1)² | 4 | + +Substituting the inclusion probabilities gives + +```text +P̄ = E[Tr(ρ²)] + = 1/M + + 2(d−1)(M−1) / [M(N−1)] + + (d−1)²(M−1)(M−2)(M−3) / [M(N−1)(N−2)(N−3)]. +``` + +The formula covers n ≥ 2 even and every valid M, including both product-state endpoints. The last contribution counts rectangles in the grid; no independence approximation is used. + +For each sample S ≥ −log₂ Tr(ρ²). Since −log₂ is convex, Jensen's inequality yields the ensemble bound + +```text +E[S] ≥ E[−log₂ Tr(ρ²)] ≥ −log₂ P̄. +``` + +This is a lower bound on mean entropy. A concentration claim or an individual-state guarantee requires an additional argument. + +Implementation: `fixed_cardinality_average_purity` and `average_entropy_lower_bound_from_purity`. + +## 4. Three distinct support scales + +For fixed c > 0 and a fixed exponent 0 < γ < 1, take integer M asymptotic to cN^γ. Expanding the exact purity gives + +```text +P̄ = 2N^(−1/2) + c^(−1)N^(−γ) + c²N^(2γ−2) + + terms smaller than their respective leading contributions. +``` + +For every 1/2 < γ < 3/4, the last two displayed contributions are smaller than N^(−1/2), so + +```text +P̄ = [2 + o(1)] N^(−1/2), +E[S] ≥ n/2 − 1 − o(1). +``` + +The deficit stays bounded by approximately one bit. This is not a statement that the absolute deficit vanishes, nor that the ensemble attains the Page mean. + +The smallest correction to this purity background occurs when the sparse and rectangle terms balance, giving γ = 2/3 and c = 2^(−1/3). The O(1) correction to the resulting minimizer can be checked directly. Set + +```text +a = 2/(d+1), +b = (d−1)/[(d+1)(N−2)(N−3)]. + +P̄(M) = bM² − 6bM + a + 11b + (1−a−6b)/M. +``` + +For N ≥ 16, the last numerator is positive. The second derivative is positive, and the derivative vanishes exactly when + +```text +M²(M−3) = N(N−5)/2. +``` + +The continuous minimizer is 2^(−1/3)N^(2/3) + 1 + O(N^(−1/3)); comparing the two neighboring integers gives an integer minimizer of the form 2^(−1/3)N^(2/3) + O(1). At N = 4, direct evaluation gives the minimum at M = 3. + +At γ = 3/4 instead, + +```text +P̄ = (2+c²)N^(−1/2) + o(N^(−1/2)). +``` + +This is the crossover where the rectangle term becomes comparable with the balanced-cut background. It is not a theorem about the von Neumann entropy maximum. The average-purity minimizer, the maximizer of average Rényi-2 entropy, and the maximizer of average von Neumann entropy are different optimization problems. + +## 5. Exact diagonal and residue bounds + +The occupancy W of any column is hypergeometric with population size N, d marked cells, and M draws. The diagonal probability of that column is W/M. Hence the mean diagonal entropy is exactly + +```text +E[H(diag ρ)] = −d E[(W/M) log₂(W/M)]. +``` + +Deleting off-diagonal coherences cannot reduce entropy, so E[S] ≤ E[H(diag ρ)]. The function named `hypergeometric_occupancy_approximation` computes this exact diagonal expectation; treating it as an approximation to von Neumann entropy is appropriate only when coherences are sufficiently small. + +For the natural cut, the right subsystem consists of the low n/2 bits. Let pᵣ be the fraction of occupied labels congruent to r modulo 2ᵗ, with 0 ≤ t ≤ n/2. Dephase the t low bits. The resulting blocks have total weights pᵣ and dimension at most d/2ᵗ. Their entropy is at most H(p) + log₂(d/2ᵗ), while dephasing cannot decrease entropy. Therefore every such support satisfies + +```text +S ≤ n/2 − t + H(p). +``` + +This is a cut-specific computational-basis statement. Applying a global QFT changes the state; the input residue populations do not automatically bound its output entanglement in the same way. + +## 6. Approximations and reference ensembles + +The dense-bulk ansatz uses the uniform-mode eigenvalue λ of E[ρ] as an estimate for a separated sample eigenvalue, then assigns the remaining weight a Page-like bulk: + +```text +T = h₂(λ) + (1−λ)[log₂ d − 1/(2 ln 2)], +h₂(λ) = −λ log₂ λ − (1−λ) log₂(1−λ). +``` + +The mean-state formula is exact. The identification of its eigenvalue with a typical separated eigenvalue, the bulk shape, and this entropy expression are approximations. Algebraically equivalent formulas and one spectral plot do not establish their accuracy uniformly in M or n. + +The Page mean for a complex Haar-random pure state across a d-by-d cut is + +```text +S_Page = [H_(d²) − H_d − (d−1)/(2d)] / ln 2, +``` + +where H_j is the j-th harmonic number. It is a reference average for a different ensemble, not an entropy ceiling. The ceiling is log₂ d. Related work and ensemble distinctions are in [references](REFERENCES.md). + +## 7. Numerical evidence + +The retained peak table fits log₂ M̂ = 0.703541n − 0.357734 over eleven even sizes n = 10–30. This is a descriptive regression of historical point estimates. The original global-search records are incomplete, especially for n = 22–30. Local checks near the supplied centers cannot recover global optimality or supply the original search uncertainty. See [provenance](../PROVENANCE.md). + +| Figure | What is shown | Scope | +|---|---|---| +| [1](../outputs/fig1/fig1_concentration.png) | Support-size curves and cut sampling | Finite ensemble samples | +| [2](../outputs/fig2/fig2_peak_scaling.png) | Historical peak table and regressions | No asymptotic exponent established | +| [3](../outputs/fig3/fig3_spectral_bulk.png) | Spectrum and a bulk comparison | One sampled n = 24 state | +| [4](../outputs/fig4/fig4_approximation.png) | Sparse and dense approximations | Numerical comparison, not a uniform error theorem | +| [5](../outputs/fig5/fig5_qft_residue_controls.png) | Almost-prime supports and matched ensembles | Paired computational/Fourier comparisons at n = 14 | +| [6](../outputs/fig6/fig6_renyi.png) | Several Rényi entropy curves | Different entropy orders need not peak together | +| [7](../outputs/fig7/fig7_partitions.png) | Entropy over sampled balanced cuts | Two selected n = 20 states | + +The QFT uses the positive-exponent convention, implemented by NumPy's orthonormal inverse FFT. Changing its sign conjugates the output for these real inputs and leaves bipartite entropies unchanged. This convention check says nothing about implementing a physical QFT at large n. + +## 8. Open scientific questions + +1. Determine the asymptotic maximizer of the mean von Neumann entropy, with error control; the purity calculation alone does not do this. +2. Establish a quantitative dense-bulk approximation for this fixed-cardinality, positive-amplitude ensemble, with a stated regime and finite-size errors. +3. Compare the exact formulas and their consequences directly with existing fixed-size subset-state moment calculations before making a novelty claim. +4. Recover or replace the missing global-search evidence if the high-n numerical peak law is retained as a central contribution. + +The present cleanup strengthens the repository's reliability and boundaries. It does not settle these research questions. diff --git a/docs/SANITY_CHECK.md b/docs/SANITY_CHECK.md new file mode 100644 index 0000000..c03c0c1 --- /dev/null +++ b/docs/SANITY_CHECK.md @@ -0,0 +1,40 @@ +# Sanity check and cleanup — 22 September 2026 + +**Assessment: the core exact results survive this audit; the repository does not establish a complete publication-ready contribution.** The remaining high-n provenance and novelty questions are substantive. They are now visible in the landing page and research notes. + +The baseline was commit [4cb99f109194124f500d75dd065ff43581a2682d](https://github.com/GoGoKo699/Subset-states/tree/4cb99f109194124f500d75dd065ff43581a2682d). Independent mathematical, numerical-code, validator, and primary-literature reviews were used. Prior automated PASS reports were treated as historical claims to inspect. + +## Findings and actions + +| Finding | Action | +|---|---| +| Exact mean reduced state, average purity, entropy lower bound, hypergeometric diagonal entropy, and residue ceiling check out | Added derivations and explicit assumptions in [research notes](RESEARCH.md), including the exact cubic equation for the purity minimizer | +| Old validation hardcoded Table I and mainly checked copies of formulas | Validator now reads actual CSVs, compares derived tables, and checks production routines against independent calculations | +| Old acceptance assertions disappear under optimized Python | Replaced with explicit failures; mutation tests exercise both `-O` and `-OO` | +| QFT routine accepted duplicate and negative support labels; for example, duplicate labels could yield a state with squared norm 1/2 | Validate labels before indexing or integer conversion; added rejection and normalization regressions | +| State-vector and support constructors used inconsistent subsystem basis ordering | Aligned ordering and documented the complex partial-trace convention; valid historical entropy spectra are invariant under the corrected row/column permutations | +| Invalid Rényi orders/spectra and extreme orders could silently misbehave | Added input checks and stable entropy evaluation, with targeted regressions | +| Default figure “reproduction” redrew only two figures and checked that old PDFs existed; smoke mode overwrote some released outputs | Redraw all seven from CSV; put all default fresh outputs under `generated/`; test destination isolation and missing-input failures | +| Repository cited a 2026 renamed arXiv paper that is not the current public record | Corrected the citation to *Arithmetic sequences as quantum states* (2025); documented the later title as a working title | +| Old reports claimed submission readiness and audited absent manuscript/build files | Removed those reports from the active tree; linked the baseline commit for history; replaced them with scoped Markdown documentation | +| No TeX sources were present, but documentation contained TeX notation and the repository retained 14 PDF figures | Converted prose mathematics to plain text/Unicode, removed PDF duplicates, and made plot defaults PNG without an external TeX dependency | + +## What was verified + +The initial ten tests passed before editing. All 39 tests in the updated integrated suite passed. Exact/data checks and figure workflows are recorded in [the scientific report](../validation/SCIENTIFIC_SANITY_CHECKS.md) and [reproducibility instructions](REPRODUCIBILITY.md). + +The mathematical audit rederived the identities and separately checked integer/rational enumeration at n = 4. The maintained validator exhaustively checks 65,550 supports in total across n = 2 and n = 4, including comparisons with the production coefficient matrices, entropies, mean matrices, purity formula, diagonal expectation, residue ceilings, and entropy lower bound. QFT amplitudes receive an explicit small-system DFT check. + +The data check reconstructs all 12,000 released residue observations into their summaries and checks residue bounds. It replays three seeded supports per group, 36 in total. That is a limited seed replay, not a regeneration of every raw observation. + +All seven CSV-based figure redraws and all seven reduced-run outputs completed successfully. The complete original-size simulations and missing global peak searches were not run. SHA-256 comparison against the baseline confirms all 50 retained CSV/PNG evidence files and the original license are unchanged. + +## What remains unresolved + +- Historical peak values are publicly traceable to the 2025 preprint, but the complete global-search records and uncertainty estimates are absent. Later local checks cannot supply them. +- The exact purity minimizer does not locate the mean von Neumann entropy maximum. No asymptotic entropy-peak exponent is proved. +- The dense-bulk approximation lacks a uniform error theorem. Checking its implementation or algebra does not validate its accuracy throughout the parameter range. +- A single spectrum and two selected states do not prove concentration or universality. +- Existing sparse-state and fixed-size subset-state literature overlaps the broad framing. The targeted [literature check](REFERENCES.md) does not establish novelty of the revised contribution. + +The cleanup makes the project inspectable and reproducible within the stated limits. It does not replace peer review or recover missing experiments. diff --git a/outputs/README.md b/outputs/README.md index 095ea57..3e7f9e5 100644 --- a/outputs/README.md +++ b/outputs/README.md @@ -1,5 +1,7 @@ -# Released outputs +# Released figures and numerical records -The subdirectories `fig1` through `fig7` contain the manuscript-ready vector PDF -and a PNG preview for each final figure. Scripts write regenerated outputs to the -same locations unless an alternative `--outdir` is supplied. +Directories `fig1` through `fig7` contain inherited PNG figures and their available CSV inputs. PDF duplicates were removed in the Markdown cleanup; the original versions remain in Git history. Retained PNG and CSV bytes are unchanged. + +`peak_verification/` contains local checks for n = 10 through 20. Its `samples.csv` files contain statistics at each sampled support size, not individual states or their labels. These are not the missing original global-search records. + +Use `python scripts/reproduce_publication_figures.py` from the repository root to redraw all seven figures into `generated/figures/`. Rendering can vary with fonts and library versions, so pixel identity with the inherited PNGs is not promised. See [reproducibility](../docs/REPRODUCIBILITY.md). diff --git a/outputs/fig1/fig1_concentration.pdf b/outputs/fig1/fig1_concentration.pdf deleted file mode 100644 index 213d889..0000000 Binary files a/outputs/fig1/fig1_concentration.pdf and /dev/null differ diff --git a/outputs/fig2/fig2_peak_scaling.pdf b/outputs/fig2/fig2_peak_scaling.pdf deleted file mode 100644 index 614cfe3..0000000 Binary files a/outputs/fig2/fig2_peak_scaling.pdf and /dev/null differ diff --git a/outputs/fig3/fig3_spectral_bulk.pdf b/outputs/fig3/fig3_spectral_bulk.pdf deleted file mode 100644 index 60ea87d..0000000 Binary files a/outputs/fig3/fig3_spectral_bulk.pdf and /dev/null differ diff --git a/outputs/fig4/fig4_approximation.pdf b/outputs/fig4/fig4_approximation.pdf deleted file mode 100644 index 0539f27..0000000 Binary files a/outputs/fig4/fig4_approximation.pdf and /dev/null differ diff --git a/outputs/fig5/fig5_qft_residue_controls.pdf b/outputs/fig5/fig5_qft_residue_controls.pdf deleted file mode 100644 index 619b091..0000000 Binary files a/outputs/fig5/fig5_qft_residue_controls.pdf and /dev/null differ diff --git a/outputs/fig6/fig6_renyi.pdf b/outputs/fig6/fig6_renyi.pdf deleted file mode 100644 index d5b26a0..0000000 Binary files a/outputs/fig6/fig6_renyi.pdf and /dev/null differ diff --git a/outputs/fig7/fig7_partitions.pdf b/outputs/fig7/fig7_partitions.pdf deleted file mode 100644 index 547e07e..0000000 Binary files a/outputs/fig7/fig7_partitions.pdf and /dev/null differ diff --git a/outputs/peak_verification/peak_verification_n10.pdf b/outputs/peak_verification/peak_verification_n10.pdf deleted file mode 100644 index cb7cfdf..0000000 Binary files a/outputs/peak_verification/peak_verification_n10.pdf and /dev/null differ diff --git a/outputs/peak_verification/peak_verification_n12.pdf b/outputs/peak_verification/peak_verification_n12.pdf deleted file mode 100644 index 6d5834c..0000000 Binary files a/outputs/peak_verification/peak_verification_n12.pdf and /dev/null differ diff --git a/outputs/peak_verification/peak_verification_n14.pdf b/outputs/peak_verification/peak_verification_n14.pdf deleted file mode 100644 index 2f0b19f..0000000 Binary files a/outputs/peak_verification/peak_verification_n14.pdf and /dev/null differ diff --git a/outputs/peak_verification/peak_verification_n16.pdf b/outputs/peak_verification/peak_verification_n16.pdf deleted file mode 100644 index 0c127bf..0000000 Binary files a/outputs/peak_verification/peak_verification_n16.pdf and /dev/null differ diff --git a/outputs/peak_verification/peak_verification_n18.pdf b/outputs/peak_verification/peak_verification_n18.pdf deleted file mode 100644 index 65508e3..0000000 Binary files a/outputs/peak_verification/peak_verification_n18.pdf and /dev/null differ diff --git a/outputs/peak_verification/peak_verification_n20.pdf b/outputs/peak_verification/peak_verification_n20.pdf deleted file mode 100644 index f5f7533..0000000 Binary files a/outputs/peak_verification/peak_verification_n20.pdf and /dev/null differ diff --git a/outputs/peak_verification/peak_verification_summary.pdf b/outputs/peak_verification/peak_verification_summary.pdf deleted file mode 100644 index 62d5569..0000000 Binary files a/outputs/peak_verification/peak_verification_summary.pdf and /dev/null differ diff --git a/pyproject.toml b/pyproject.toml index e254dd7..aa00bbc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "subset-state-entanglement" -version = "1.0.0rc1" +version = "1.0.0rc2" description = "Numerical and analytical tools for support-size entanglement trajectories of random subset states" requires-python = ">=3.10" authors = [ diff --git a/requirements-tested.txt b/requirements-tested.txt index 6fed565..a295ddd 100644 --- a/requirements-tested.txt +++ b/requirements-tested.txt @@ -1,5 +1,5 @@ -# Environment used for the publication-release validation on 2026-08-18. -Python==3.13.5 +# Dependencies tested during the Markdown cleanup on 2026-09-22. +# Interpreter: Python 3.12.14 (do not install Python as a pip package). numpy==2.3.5 scipy==1.17.0 matplotlib==3.10.8 diff --git a/scripts/check_repository.py b/scripts/check_repository.py new file mode 100644 index 0000000..04d04d9 --- /dev/null +++ b/scripts/check_repository.py @@ -0,0 +1,52 @@ +#!/usr/bin/env python3 +"""Check the Markdown-only documentation policy and frozen evidence checksums.""" +from __future__ import annotations + +import hashlib +import json +from pathlib import Path +import re +import sys +from urllib.parse import unquote, urlsplit + +ROOT = Path(__file__).resolve().parents[1] +SKIP = {'.git', '.venv', 'venv', 'env', 'generated', '__pycache__', 'build', 'dist'} +FORBIDDEN = {'.tex', '.bib', '.sty', '.cls', '.aux', '.bbl', '.blg', '.pdf'} + + +def main() -> None: + errors = [] + manifest = json.loads((ROOT / 'validation/evidence_sha256.json').read_text()) + for name, expected in manifest['files'].items(): + path = ROOT / name + if not path.is_file() or hashlib.sha256(path.read_bytes()).hexdigest() != expected: + errors.append(f'released evidence changed or missing: {name}') + files = [p for p in ROOT.rglob('*') if p.is_file() + and not any(part in SKIP or part.endswith('.egg-info') for part in p.relative_to(ROOT).parts)] + for path in files: + rel = path.relative_to(ROOT) + if path.suffix.lower() in FORBIDDEN: + errors.append(f'unsupported document/build artifact: {rel}') + if path.suffix == '.txt' and path.name not in {'requirements.txt', 'requirements-tested.txt'}: + errors.append(f'narrative text should be Markdown: {rel}') + if path.suffix != '.md': + continue + content = path.read_text() + if re.search(r'\\(?:begin|end|frac|mathrm|mathbb|gamma|lambda)\b|\\[\[\]()]|\$|```math\b', content): + errors.append(f'use plain-text or Unicode mathematics in Markdown: {rel}') + # This repository uses inline links without spaces or optional titles. + for target in re.findall(r'!?\[[^\]]*\]\(([^)]+)\)', content): + url = urlsplit(target) + if url.scheme or not url.path: + continue + if not (path.parent / unquote(url.path)).exists(): + errors.append(f'broken local link in {rel}: {target}') + if errors: + print('\n'.join(errors), file=sys.stderr) + raise SystemExit(1) + docs = sum(p.suffix == '.md' for p in files) + print(f'PASS: {len(manifest["files"])} evidence checksums; {docs} Markdown documents; local link paths; no TeX/PDF artifacts.') + + +if __name__ == '__main__': + main() diff --git a/scripts/fig1_concentration.py b/scripts/fig1_concentration.py index a0ab52f..da225ca 100644 --- a/scripts/fig1_concentration.py +++ b/scripts/fig1_concentration.py @@ -67,7 +67,7 @@ def main() -> None: parser.add_argument("--zoom-samples", type=int, default=500) parser.add_argument("--zoom-stop", type=int, default=1600) parser.add_argument("--seed", type=int, default=20250604) - parser.add_argument("--outdir", type=Path, default=ROOT / "outputs" / "fig1") + parser.add_argument("--outdir", type=Path, default=ROOT / "generated" / "full" / "fig1") args = parser.parse_args() args.outdir.mkdir(parents=True, exist_ok=True) N = 1 << args.n @@ -84,7 +84,7 @@ def main() -> None: write_rows(args.outdir / "fig1_zoom_summary.csv", zoom_summary, fields) write_rows(args.outdir / "fig1_raw.csv", raw, list(raw[0].keys())) write_rows(args.outdir / "fig1_zoom_raw.csv", zoom_raw, list(zoom_raw[0].keys())) - plot_fig1(args.outdir, args.outdir / "fig1_concentration.pdf", n=args.n) + plot_fig1(args.outdir, args.outdir / "fig1_concentration.png", n=args.n) if __name__ == "__main__": diff --git a/scripts/fig2_peak_scaling.py b/scripts/fig2_peak_scaling.py index bacd4c7..cd358e6 100644 --- a/scripts/fig2_peak_scaling.py +++ b/scripts/fig2_peak_scaling.py @@ -36,7 +36,7 @@ def fit_line(x: np.ndarray, y: np.ndarray) -> dict[str, float]: def main() -> None: parser = argparse.ArgumentParser() parser.add_argument("--table", type=Path, default=ROOT / "data" / "table_i_peaks.csv") - parser.add_argument("--outdir", type=Path, default=ROOT / "outputs" / "fig2") + parser.add_argument("--outdir", type=Path, default=ROOT / "generated" / "full" / "fig2") args = parser.parse_args() args.outdir.mkdir(parents=True, exist_ok=True) @@ -63,7 +63,7 @@ def main() -> None: {"quantity": "S_n", **fit_line(n, S_n)}, ] write_rows(args.outdir / "fig2_linear_fit_summary.csv", fits, fits[0].keys()) - plot_fig2(args.outdir, args.outdir / "fig2_peak_scaling.pdf", write_fit_summary=False) + plot_fig2(args.outdir, args.outdir / "fig2_peak_scaling.png", write_fit_summary=False) if __name__ == "__main__": diff --git a/scripts/fig3_spectral.py b/scripts/fig3_spectral.py index 467aa6d..fb19fe8 100644 --- a/scripts/fig3_spectral.py +++ b/scripts/fig3_spectral.py @@ -28,7 +28,7 @@ def main() -> None: parser.add_argument("--m", type=int, default=None) parser.add_argument("--seed", type=int, default=20250604) parser.add_argument("--bins", type=int, default=500) - parser.add_argument("--outdir", type=Path, default=ROOT / "outputs" / "fig3") + parser.add_argument("--outdir", type=Path, default=ROOT / "generated" / "full" / "fig3") args = parser.parse_args() args.outdir.mkdir(parents=True, exist_ok=True) @@ -79,7 +79,7 @@ def main() -> None: "bulk_max", ], ) - plot_fig3(args.outdir, args.outdir / "fig3_spectral_bulk.pdf", bins=args.bins) + plot_fig3(args.outdir, args.outdir / "fig3_spectral_bulk.png", bins=args.bins) if __name__ == "__main__": diff --git a/scripts/fig4_approximation.py b/scripts/fig4_approximation.py index 64a0825..e30053e 100644 --- a/scripts/fig4_approximation.py +++ b/scripts/fig4_approximation.py @@ -53,7 +53,7 @@ def main() -> None: parser.add_argument("--zoom-points", type=int, default=100) parser.add_argument("--zoom-samples", type=int, default=500) parser.add_argument("--seed", type=int, default=20250604) - parser.add_argument("--outdir", type=Path, default=ROOT / "outputs" / "fig4") + parser.add_argument("--outdir", type=Path, default=ROOT / "generated" / "full" / "fig4") args = parser.parse_args() args.outdir.mkdir(parents=True, exist_ok=True) N = 1 << args.n @@ -67,7 +67,7 @@ def main() -> None: ) write_rows(args.outdir / "fig4_summary.csv", rows, rows[0].keys()) write_rows(args.outdir / "fig4_zoom_summary.csv", zoom_rows, zoom_rows[0].keys()) - plot_fig4(args.outdir, args.outdir / "fig4_approximation.pdf", n=args.n) + plot_fig4(args.outdir, args.outdir / "fig4_approximation.png", n=args.n) if __name__ == "__main__": diff --git a/scripts/fig5_qft_residue_controls.py b/scripts/fig5_qft_residue_controls.py index 8bd84df..de57dd9 100644 --- a/scripts/fig5_qft_residue_controls.py +++ b/scripts/fig5_qft_residue_controls.py @@ -32,7 +32,7 @@ def values(rows: list[dict[str, str]], key: str) -> np.ndarray: def main() -> None: parser = argparse.ArgumentParser() parser.add_argument("--data-dir", type=Path, default=ROOT / "data") - parser.add_argument("--outdir", type=Path, default=ROOT / "outputs" / "fig5") + parser.add_argument("--outdir", type=Path, default=ROOT / "generated" / "figures" / "fig5") args = parser.parse_args() args.outdir.mkdir(parents=True, exist_ok=True) @@ -40,6 +40,14 @@ def main() -> None: unions = read_rows(args.data_dir / "fig5_almost_prime_unions.csv") controls = read_rows(args.data_dir / "residue_matched_summary.csv") + largest_support = int(max(values(baseline, "m"))) + n = largest_support.bit_length() - 1 + if 1 << n != largest_support: + raise ValueError("baseline must include the full-support endpoint to infer n") + control_n = {int(row["n"]) for row in controls} + if control_n != {n}: + raise ValueError(f"Figure 5 baseline n={n} disagrees with controls n={control_n}") + apply_journal_style() fig, (ax, ax2) = plt.subplots( 1, 2, figsize=(7.25, 3.35), gridspec_kw={"width_ratios": [1.35, 1]} @@ -60,8 +68,8 @@ def main() -> None: ax.plot(union_m, values(unions, "qft_entropy"), "x", color=COOL_PALETTE[5], markersize=4.8, markeredgewidth=1.0, label=r"QFT $U_{N,k}$") ax.set_xlabel(r"support size $M$") ax.set_ylabel("entropy (bits)") - ax.set_xlim(0, 2**14) - ax.set_ylim(0, 7) + ax.set_xlim(0, 1 << n) + ax.set_ylim(0, n / 2) ax.legend(frameon=False, loc="lower center", bbox_to_anchor=(0.5, -0.42), ncol=2, columnspacing=0.9, handlelength=1.8) ax.text(0.02, 0.96, "(a)", transform=ax.transAxes, ha="left", va="top", fontweight="bold") @@ -83,7 +91,7 @@ def main() -> None: ax2.set_ylabel(r"residual deficit $\Delta_t$ (bits)") ax2.set_xlabel("constraint retained in null ensemble", labelpad=2) ax2.set_xlim(-0.5, 3.35) - ax2.set_ylim(0, 1.62) + ax2.margins(y=0.10) style_legend = [ Line2D([0], [0], color="0.2", marker="o", linestyle="-", markersize=4, label="computational basis"), Line2D([0], [0], color="0.2", marker="o", markerfacecolor="white", linestyle="--", markersize=4, label="after QFT"), @@ -91,7 +99,7 @@ def main() -> None: ax2.legend(handles=style_legend, frameon=False, loc="upper right", handlelength=2.0) ax2.text(0.02, 0.96, "(b)", transform=ax2.transAxes, ha="left", va="top", fontweight="bold") fig.subplots_adjust(left=0.085, right=0.99, top=0.97, bottom=0.26, wspace=0.32) - save_figure(fig, args.outdir / "fig5_qft_residue_controls.pdf") + save_figure(fig, args.outdir / "fig5_qft_residue_controls.png") if __name__ == "__main__": diff --git a/scripts/fig6_renyi.py b/scripts/fig6_renyi.py index 1637079..1a13863 100644 --- a/scripts/fig6_renyi.py +++ b/scripts/fig6_renyi.py @@ -48,7 +48,7 @@ def main() -> None: parser.add_argument("--zoom-points", type=int, default=100) parser.add_argument("--zoom-samples", type=int, default=1000) parser.add_argument("--seed", type=int, default=20250604) - parser.add_argument("--outdir", type=Path, default=ROOT / "outputs" / "fig6") + parser.add_argument("--outdir", type=Path, default=ROOT / "generated" / "full" / "fig6") args = parser.parse_args() args.outdir.mkdir(parents=True, exist_ok=True) N = 1 << args.n @@ -62,7 +62,7 @@ def main() -> None: ) write_rows(args.outdir / "fig6_renyi_summary.csv", rows, rows[0].keys()) write_rows(args.outdir / "fig6_renyi_zoom_summary.csv", zoom, zoom[0].keys()) - plot_fig6(args.outdir, args.outdir / "fig6_renyi.pdf", n=args.n) + plot_fig6(args.outdir, args.outdir / "fig6_renyi.png", n=args.n) if __name__ == "__main__": diff --git a/scripts/fig7_partitions.py b/scripts/fig7_partitions.py index 5e9f414..8b00fc8 100644 --- a/scripts/fig7_partitions.py +++ b/scripts/fig7_partitions.py @@ -27,7 +27,7 @@ def main() -> None: parser.add_argument("--samples", type=int, default=1000) parser.add_argument("--seed", type=int, default=20250604) parser.add_argument("--bins", type=int, default=100) - parser.add_argument("--outdir", type=Path, default=ROOT / "outputs" / "fig7") + parser.add_argument("--outdir", type=Path, default=ROOT / "generated" / "full" / "fig7") args = parser.parse_args() args.outdir.mkdir(parents=True, exist_ok=True) m = args.m if args.m is not None else PEAK_M[args.n] @@ -75,7 +75,7 @@ def main() -> None: }, ] write_rows(args.outdir / "fig7_partition_summary.csv", summary_rows, summary_rows[0].keys()) - plot_fig7(args.outdir, args.outdir / "fig7_partitions.pdf", n=args.n, bins=args.bins) + plot_fig7(args.outdir, args.outdir / "fig7_partitions.png", n=args.n, bins=args.bins) if __name__ == "__main__": diff --git a/scripts/final_scientific_validation.py b/scripts/final_scientific_validation.py index 5d2eab9..c7eb6e3 100644 --- a/scripts/final_scientific_validation.py +++ b/scripts/final_scientific_validation.py @@ -1,251 +1,216 @@ #!/usr/bin/env python3 -"""Independent checks for the final subset-state manuscript candidate.""" +"""Validate exact small-system mathematics and consistency of retained CSV evidence. + +Prints Markdown without changing the repository. This check is not a peak-search +rerun, an asymptotic theorem, a novelty assessment, or a publication certificate. +""" from __future__ import annotations -import csv +import argparse import math -from collections import defaultdict from pathlib import Path +import sys import numpy as np -from scipy.special import digamma ROOT = Path(__file__).resolve().parents[1] - - -def entropy_bits(rho: np.ndarray, tol: float = 1e-14) -> float: - vals = np.linalg.eigvalsh(rho).real - vals = vals[vals > tol] - vals = vals / vals.sum() - return float(-np.sum(vals * np.log2(vals))) - - -def h_shannon(probs: np.ndarray) -> float: - p = probs[probs > 0] - return float(-np.sum(p * np.log2(p))) if p.size else 0.0 - - -def exact_mean_rho(n: int, M: int) -> np.ndarray: - d = 1 << (n // 2) - N = 1 << n - beta = (M - 1) / (d * (N - 1)) - return (1 / d - beta) * np.eye(d) + beta * np.ones((d, d)) - - -def exact_avg_purity(n: int, M: int) -> float: - d = 1 << (n // 2) - N = 1 << n - return ( - 1 / M - + 2 * (d - 1) * (M - 1) / (M * (N - 1)) - + (d - 1) ** 2 * (M - 1) * (M - 2) * (M - 3) - / (M * (N - 1) * (N - 2) * (N - 3)) - ) - - -def exhaustive_n4_checks() -> dict[str, float]: - n = 4 - N = 1 << n - d = 1 << (n // 2) - m = n // 2 - rho_sum = np.zeros((N + 1, d, d), dtype=float) - purity_sum = np.zeros(N + 1, dtype=float) - counts = np.zeros(N + 1, dtype=np.int64) - max_residue_violation = {1: -math.inf, 2: -math.inf} - max_entropy_routine_diff = 0.0 - - labels = np.arange(N, dtype=np.uint32) - batch_size = 4096 - for lo in range(1, 1 << N, batch_size): - hi = min(1 << N, lo + batch_size) - masks = np.arange(lo, hi, dtype=np.uint32) - bits = ((masks[:, None] >> labels[None, :]) & 1).astype(float) - Ms = bits.sum(axis=1).astype(np.int64) - X = bits.reshape(-1, d, d) - rho = np.einsum('bai,baj->bij', X, X) / Ms[:, None, None] - evals = np.linalg.eigvalsh(rho).real - positive = np.where(evals > 1e-14, evals, 1.0) - entropy = -np.sum(np.where(evals > 1e-14, evals * np.log2(positive), 0.0), axis=1) - if lo == 1: - entropy_direct = np.asarray([entropy_bits(r) for r in rho[:128]]) - max_entropy_routine_diff = max( - max_entropy_routine_diff, - float(np.max(np.abs(entropy[:128] - entropy_direct))), - ) - purity = np.sum(evals * evals, axis=1) - - np.add.at(rho_sum, Ms, rho) - purity_sum += np.bincount(Ms, weights=purity, minlength=N + 1) - counts += np.bincount(Ms, minlength=N + 1) - - for t in (1, 2): - modulus = 1 << t - residue_counts = np.stack( - [bits[:, (labels % modulus) == r].sum(axis=1) for r in range(modulus)], - axis=1, - ) - probs = residue_counts / Ms[:, None] - p_safe = np.where(probs > 0, probs, 1.0) - H = -np.sum(np.where(probs > 0, probs * np.log2(p_safe), 0.0), axis=1) - bound = m - t + H - max_residue_violation[t] = max( - max_residue_violation[t], - float(np.max(entropy - bound)), - ) - - max_mean_rho_error = 0.0 - max_purity_error = 0.0 - for M in range(1, N + 1): - empirical_rho = rho_sum[M] / counts[M] - empirical_purity = purity_sum[M] / counts[M] - max_mean_rho_error = max( - max_mean_rho_error, - float(np.max(np.abs(empirical_rho - exact_mean_rho(n, M)))), - ) - max_purity_error = max( - max_purity_error, - abs(empirical_purity - exact_avg_purity(n, M)), - ) - - return { - "supports_enumerated": float((1 << N) - 1), - "max_mean_rho_error": max_mean_rho_error, - "max_average_purity_error": max_purity_error, - "max_residue_bound_violation_t1": max_residue_violation[1], - "max_residue_bound_violation_t2": max_residue_violation[2], - "max_entropy_routine_difference": max_entropy_routine_diff, - } - -def page_entropy(n: int) -> float: - d = 1 << (n // 2) - return float((digamma(d * d + 1) - digamma(d + 1) - (d - 1) / (2 * d)) / math.log(2)) - - -def table_fit_checks() -> dict[str, float]: - rows = np.asarray( - [ - (10, 107, 4.072), - (12, 276, 5.108), - (14, 716, 6.143), - (16, 1873, 7.176), - (18, 4934, 8.196), - (20, 13091, 9.215), - (22, 34771, 10.231), - (24, 93018, 11.242), - (26, 250660, 12.251), - (28, 672556, 13.258), - (30, 1836685, 14.263), - ], - dtype=float, - ) - n, M, S = rows.T - slope_S, intercept_S = np.polyfit(n, S, 1) - slope_M, intercept_M = np.polyfit(n, np.log2(M), 1) - page = np.asarray([page_entropy(int(x)) for x in n]) - gaps = page - S - fractions = M / np.power(2.0, n) - assert np.all(np.diff(gaps) < 0), "Page gaps are not strictly decreasing" - assert np.all(np.diff(fractions) < 0), "support fractions are not strictly decreasing" - return { - "S_slope": float(slope_S), - "S_intercept": float(intercept_S), - "log2M_slope": float(slope_M), - "log2M_intercept": float(intercept_M), - "page_gap_n10": float(gaps[0]), - "page_gap_n30": float(gaps[-1]), - "support_fraction_n10": float(fractions[0]), - "support_fraction_n30": float(fractions[-1]), - } - - -def dense_ansatz_equivalence() -> float: - n = 14 - N = 1 << n +sys.path.insert(0, str(ROOT)) + +from subset_states import core, tables +from scripts.fig2_peak_scaling import fit_line +from scripts.validate_residue_results import ( + close, finite, markdown_report, read_rows, require, residue_checks, write_report, +) + + +def exhaustive_checks(n_values: tuple[int, ...] = (2, 4)) -> dict[str, float | int]: + """Independently enumerate supports and compare the production implementations.""" + errors = {name: 0.0 for name in ( + "coefficient_matrix", "entropy", "residue_ceiling", "mean_spectrum", "average_purity", + "diagonal_entropy", "entropy_lower_bound", "qft_amplitude", "qft_entropy", + )} + supports_enumerated = 0 + max_bound_violation = 0.0 + for n in n_values: + require(n in (2, 4), "exhaustive checks are deliberately limited to n=2 and n=4") + N, d = 1 << n, 1 << (n // 2) + rho_sum = np.zeros((N + 1, d, d)) + purity_sum = np.zeros(N + 1) + diagonal_sum = np.zeros(N + 1) + entropy_sum = np.zeros(N + 1) + counts = np.zeros(N + 1, dtype=int) + labels = np.arange(N, dtype=np.uint32) + # Batch eigensolvers provide an independent route from the production SVD. + for lo in range(1, 1 << N, 4096): + masks = np.arange(lo, min(1 << N, lo + 4096), dtype=np.uint32) + bits = ((masks[:, None] >> labels[None, :]) & 1).astype(float) + sizes = bits.sum(axis=1).astype(int) + matrices = bits.reshape(-1, d, d) / np.sqrt(sizes[:, None, None]) + rho = np.einsum("bai,baj->bij", matrices, matrices) + eigenvalues = np.linalg.eigvalsh(rho) + safe = np.where(eigenvalues > 1e-14, eigenvalues, 1.0) + entropies = -np.sum(np.where(eigenvalues > 1e-14, eigenvalues * np.log2(safe), 0), axis=1) + diagonal = np.diagonal(rho, axis1=1, axis2=2) + safe_diag = np.where(diagonal > 0, diagonal, 1.0) + diagonal_entropies = -np.sum(diagonal * np.log2(safe_diag), axis=1) + np.add.at(rho_sum, sizes, rho) + purity_sum += np.bincount(sizes, weights=np.sum(rho * rho, axis=(1, 2)), minlength=N + 1) + diagonal_sum += np.bincount(sizes, weights=diagonal_entropies, minlength=N + 1) + entropy_sum += np.bincount(sizes, weights=entropies, minlength=N + 1) + counts += np.bincount(sizes, minlength=N + 1) + for index, size in enumerate(sizes): + support = np.flatnonzero(bits[index]) + production_matrix = core.matrix_from_support(n, support) + matrix_error = float(np.max(np.abs(production_matrix - matrices[index]))) + require(math.isfinite(matrix_error), "non-finite production coefficient matrix") + errors["coefficient_matrix"] = max(errors["coefficient_matrix"], matrix_error) + errors["entropy"] = max(errors["entropy"], close( + core.entropy_from_support(n, support), float(entropies[index]), "production entropy")) + for t in range(n // 2 + 1): + population = np.bincount(support % (1 << t), minlength=1 << t) + probs = population[population > 0] / size + bound = n / 2 - t - float(np.sum(probs * np.log2(probs))) + errors["residue_ceiling"] = max(errors["residue_ceiling"], close( + core.residue_class_entropy_ceiling(n, support, t), bound, "production residue ceiling")) + max_bound_violation = max(max_bound_violation, float(entropies[index]) - bound) + if n == 2: + # Explicit DFT checks sign and normalization, independently of FFT. + phases = np.exp(2j * np.pi * np.outer(np.arange(N), support) / N) + direct_state = phases.sum(axis=1) / math.sqrt(N * size) + actual_state = core.qft_state_from_support(n, support) + amplitude_error = float(np.max(np.abs(actual_state - direct_state))) + require(math.isfinite(amplitude_error), "non-finite production QFT") + errors["qft_amplitude"] = max(errors["qft_amplitude"], amplitude_error) + c = direct_state.reshape(d, d) + eig = np.linalg.eigvalsh(c.conj().T @ c) + positive = eig[eig > 1e-14] + direct_entropy = float(-np.sum(positive * np.log2(positive))) + errors["qft_entropy"] = max(errors["qft_entropy"], close( + core.entropy_from_state_vector(n, actual_state), direct_entropy, "production QFT entropy")) + supports_enumerated += len(masks) + for size in range(1, N + 1): + require(counts[size] == math.comb(N, size), f"n={n}, M={size}: enumeration count") + mean_rho = rho_sum[size] / counts[size] + uniform, orthogonal = core.fixed_cardinality_mean_spectrum(n, size) + predicted_rho = orthogonal * np.eye(d) + (uniform - orthogonal) * np.ones((d, d)) / d + error = float(np.max(np.abs(predicted_rho - mean_rho))) + require(math.isfinite(error), "non-finite production mean spectrum") + errors["mean_spectrum"] = max(errors["mean_spectrum"], error) + close(core.mean_matrix_uniform_eigenvalue(n, size), uniform, "production uniform eigenvalue") + mean_purity = float(purity_sum[size] / counts[size]) + errors["average_purity"] = max(errors["average_purity"], close( + core.fixed_cardinality_average_purity(n, size), mean_purity, "production average purity")) + errors["diagonal_entropy"] = max(errors["diagonal_entropy"], close( + core.hypergeometric_occupancy_approximation(n, size), float(diagonal_sum[size] / counts[size]), + "production diagonal entropy")) + lower = core.average_entropy_lower_bound_from_purity(n, size) + errors["entropy_lower_bound"] = max(errors["entropy_lower_bound"], close( + lower, -math.log2(mean_purity), "production purity entropy bound")) + max_bound_violation = max(max_bound_violation, lower - float(entropy_sum[size] / counts[size])) + for name, error in errors.items(): + require(error < 5e-12, f"{name} maximum error {error} exceeds tolerance") + require(max_bound_violation < 5e-12, f"entropy bound violated by {max_bound_violation}") + return {"supports_enumerated": supports_enumerated, + **{f"max_{name}_error": value for name, value in errors.items()}, + "max_entropy_bound_violation": max_bound_violation} + + +def independent_page_entropy(n: int) -> float: + """Page expression using harmonic sums/expansion, independently of digamma.""" d = 1 << (n // 2) - max_diff = 0.0 - for M in range(1, N): - lam = 1 / d + (d - 1) * (M - 1) / (d * (N - 1)) - old = ( - (1 - lam) * (math.log(d) - math.log(1 - lam) - 0.5) - - lam * math.log(lam) - ) / math.log(2) - h2 = -lam * math.log2(lam) - (1 - lam) * math.log2(1 - lam) - new = h2 + (1 - lam) * (math.log2(d) - 1 / (2 * math.log(2))) - max_diff = max(max_diff, abs(old - new)) - return max_diff - - -def residue_checks() -> dict[str, float]: - path = ROOT / "data" / "residue_matched_summary.csv" - with path.open(newline="") as f: - rows = list(csv.DictReader(f)) - - by_key = {(int(r["k"]), int(r["matched_low_bits"])): r for r in rows} - strongest = [(1, 1), (2, 2), (3, 3)] - expected = { - (1, 1): (0.124881, 0.114442, 0.885607, 0.894136), - (2, 2): (0.230201, 0.212232, 0.841422, 0.851975), - (3, 3): (0.111500, 0.103991, 0.910577, 0.916102), - } - max_claim_error = 0.0 - for key in strongest: - r = by_key[key] - vals = ( - float(r["position_residual_deficit"]), - float(r["fourier_residual_deficit"]), - float(r["cardinality_deficit_reduction_position"]), - float(r["cardinality_deficit_reduction_fourier"]), - ) - max_claim_error = max(max_claim_error, max(abs(a - b) for a, b in zip(vals, expected[key]))) - assert int(r["position_null_count_at_or_below_structured"]) == 0 - assert int(r["fourier_null_count_at_or_below_structured"]) == 0 - - prime = by_key[(1, 1)] - forced_gap = float(prime["forced_gap_from_balanced_max_bits"]) - ceiling = float(prime["entropy_ceiling_bits"]) - return { - "max_residue_claim_rounding_error": max_claim_error, - "prime_parity_ceiling_bits": ceiling, - "prime_forced_gap_bits": forced_gap, - "all_six_strongest_controls_below_all_1000_nulls": 1.0, - } - - -def main() -> None: - results = { - "exhaustive_n4": exhaustive_n4_checks(), - "table_fits": table_fit_checks(), - "dense_ansatz": {"max_old_new_difference": dense_ansatz_equivalence()}, - "residue_controls": residue_checks(), - } - - lines = ["FINAL SCIENTIFIC VALIDATION", "=" * 29, ""] - for section, values in results.items(): - lines.append(section) - lines.append("-" * len(section)) - for key, value in values.items(): - lines.append(f"{key}: {value:.16g}") - lines.append("") - - # Hard acceptance thresholds. - ex = results["exhaustive_n4"] - assert ex["max_mean_rho_error"] < 5e-13 - assert ex["max_average_purity_error"] < 5e-13 - assert ex["max_residue_bound_violation_t1"] < 5e-13 - assert ex["max_residue_bound_violation_t2"] < 5e-13 - assert results["dense_ansatz"]["max_old_new_difference"] < 5e-13 - tf = results["table_fits"] - assert abs(tf["S_slope"] - 0.5093) < 1e-12 - assert abs(tf["S_intercept"] + 0.9900909090909096) < 1e-12 - assert abs(tf["log2M_slope"] - 0.7035409513877945) < 1e-12 - assert abs(tf["log2M_intercept"] + 0.35773436814817394) < 1e-12 - - lines.append("STATUS: PASS") - out = ROOT / "validation" / "FINAL_SCIENTIFIC_VALIDATION.txt" - out.parent.mkdir(parents=True, exist_ok=True) - out.write_text("\n".join(lines) + "\n") - print(out.read_text()) + def harmonic(number: int) -> float: + if number < 32: + return math.fsum(1 / k for k in range(1, number + 1)) + x = 1.0 / number + return (math.log(number) + 0.5772156649015328606 + x / 2 - x**2 / 12 + + x**4 / 120 - x**6 / 252 + x**8 / 240 - x**10 / 132) + return (harmonic(d * d) - harmonic(d) - (d - 1) / (2 * d)) / math.log(2) + + +def table_fit_checks(root: Path = ROOT) -> dict[str, float | int]: + rows = read_rows(root / "data/table_i_peaks.csv", {"n", "M_n", "S_n"}) + require([int(row["n"]) for row in rows] == list(range(10, 31, 2)), "Table I n grid is incomplete or duplicated") + table = np.asarray([(int(row["n"]), int(row["M_n"]), finite(row, "S_n")) for row in rows], dtype=float) + require(np.array_equal(table, np.asarray([[r["n"], r["M_n"], r["S_n"]] + for r in tables.read_table_i(root / "data/table_i_peaks.csv")])), "production Table I parser disagrees") + require(np.array_equal(table, tables.table_i_array()), "Table I CSV and production defaults disagree") + n, sizes, entropies = table.T + require(np.all((sizes >= 1) & (sizes <= 2**n)), "Table I support sizes out of range") + require(np.all((entropies >= 0) & (entropies <= n / 2)), "Table I entropy out of range") + page = np.asarray([independent_page_entropy(int(value)) for value in n]) + for ni, value in zip(n, page): + close(core.exact_page_entropy_bits(int(ni)), float(value), "production Page entropy") + derived = read_rows(root / "outputs/fig2/fig2_table_with_page.csv", { + "n", "M_n", "log2_M_n", "S_n", "Page_exact_bits", "Page_minus_S_n"}) + require(len(derived) == len(rows), "derived Table I row count mismatch") + max_error = 0.0 + for row, ni, size, entropy, page_value in zip(derived, n, sizes, entropies, page): + for field, value in {"n": ni, "M_n": size, "log2_M_n": math.log2(size), "S_n": entropy, + "Page_exact_bits": page_value, "Page_minus_S_n": page_value - entropy}.items(): + max_error = max(max_error, close(finite(row, field), float(value), f"derived Table I {field}")) + fit_fields = {"slope", "intercept", "r_value", "r_squared", "p_value", "slope_stderr", "intercept_stderr", "residual_std"} + fits = read_rows(root / "outputs/fig2/fig2_linear_fit_summary.csv", {"quantity"} | fit_fields) + by_quantity = {row["quantity"]: row for row in fits} + require(len(fits) == 2 and set(by_quantity) == {"log2_M_n", "S_n"}, "fit quantities missing or duplicated") + results: dict[str, float | int] = {"table_rows_checked": len(rows)} + for quantity, values in (("log2_M_n", np.log2(sizes)), ("S_n", entropies)): + slope, intercept = np.polyfit(n, values, 1) + production = fit_line(n, values) + close(production["slope"], float(slope), f"independent {quantity} slope") + close(production["intercept"], float(intercept), f"independent {quantity} intercept") + for field in fit_fields: + # Relative tolerance is needed for tiny p-values; do not accept zero in their place. + expected = production[field] + max_error = max(max_error, close(finite(by_quantity[quantity], field), expected, + f"stored {quantity} fit {field}", atol=max(abs(expected) * 1e-8, 1e-30))) + results[f"{quantity}_slope"] = float(slope) + results[f"{quantity}_intercept"] = float(intercept) + gaps = page - entropies + require(np.all(np.diff(gaps) < 0), "recorded Page gaps are not decreasing") + require(np.all(np.diff(sizes / 2**n) < 0), "recorded support fractions are not decreasing") + results["max_derived_table_or_fit_error"] = max_error + return results + + +def dense_ansatz_checks() -> dict[str, float | int]: + max_error, tested = 0.0, 0 + for n in (2, 4, 8, 14): + N, d = 1 << n, 1 << (n // 2) + for size in range(1, N + 1): + lam = (N - size + d * (size - 1)) / (d * (N - 1)) + if size == N: + independent = 0.0 + else: + h2 = -lam * math.log2(lam) - (1 - lam) * math.log2(1 - lam) + independent = h2 + (1 - lam) * (math.log2(d) - 1 / (2 * math.log(2))) + max_error = max(max_error, close(core.dense_bulk_approximation(n, size), independent, + "production dense-bulk ansatz", atol=5e-13)) + tested += 1 + return {"parameter_pairs_checked": tested, "max_formula_error": max_error} + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--output", type=Path, help="save the report to a new .md file") + args = parser.parse_args() + try: + # Check files first so malformed or stale evidence fails promptly. + results = {"Retained Table I consistency": table_fit_checks(), + "Exhaustive small systems": exhaustive_checks(), + "Dense-bulk formula implementation": dense_ansatz_checks(), + "Residue-control evidence": residue_checks()} + report = markdown_report("Scientific sanity checks", results, + "Exhaustive checks cover every nonempty support at n=2 and n=4 against production routines. " + "Table I checks read the actual retained CSV and its derived outputs; they do not rerun the " + "peak searches or validate an asymptotic law. The residue checks reconstruct all 12,000-row " + "statistics and replay three seeded supports per group (36 total). Formula agreement does " + "not establish the dense-bulk ansatz's approximation accuracy. Novelty remains a separate question.") + write_report(report, args.output) + except (ValueError, OSError, KeyError, TypeError) as exc: + print(f"# Scientific sanity checks\n\n**Status: FAIL**\n\n{exc}", file=sys.stderr) + return 1 + return 0 if __name__ == "__main__": - main() + raise SystemExit(main()) diff --git a/scripts/peak_scaling_verification.py b/scripts/peak_scaling_verification.py index 254e98b..6996c96 100644 --- a/scripts/peak_scaling_verification.py +++ b/scripts/peak_scaling_verification.py @@ -42,7 +42,7 @@ def read_schedule(path: Path) -> dict[int, dict[str, float | int]]: } -def plot_one(n: int, rows: list[dict], fit: dict, out_pdf: Path) -> None: +def plot_one(n: int, rows: list[dict], fit: dict, out_path: Path) -> None: apply_journal_style() fig, ax = plt.subplots(figsize=(4.7, 3.6)) m = np.asarray([row["m"] for row in rows], dtype=float) @@ -59,7 +59,7 @@ def plot_one(n: int, rows: list[dict], fit: dict, out_pdf: Path) -> None: ax.set_ylabel(r"$S_{N,M}$") ax.set_title(fr"local peak check, $n={n}$") ax.legend(frameon=False) - save_figure(fig, out_pdf) + save_figure(fig, out_path) def main() -> None: @@ -74,7 +74,7 @@ def main() -> None: parser.add_argument("--span", type=float, default=None) parser.add_argument("--seed", type=int, default=20250605) parser.add_argument("--dry-run", action="store_true") - parser.add_argument("--outdir", type=Path, default=ROOT / "outputs" / "peak_verification") + parser.add_argument("--outdir", type=Path, default=ROOT / "generated" / "full" / "peak_verification") args = parser.parse_args() table = read_table_i(args.table) @@ -131,7 +131,7 @@ def main() -> None: fit["S_peak_minus_table_S_n"] = float(fit["S_peak"] - job["table_S_n"]) write_rows(args.outdir / f"peak_verification_n{job['n']}_samples.csv", rows, rows[0].keys()) write_rows(args.outdir / f"peak_verification_n{job['n']}_fit.csv", [fit], fit.keys()) - plot_one(job["n"], rows, fit, args.outdir / f"peak_verification_n{job['n']}.pdf") + plot_one(job["n"], rows, fit, args.outdir / f"peak_verification_n{job['n']}.png") summary.append(fit) write_rows(args.outdir / "peak_verification_summary.csv", summary, summary[0].keys()) diff --git a/scripts/plot_fig1_from_csv.py b/scripts/plot_fig1_from_csv.py index c461f68..3d28001 100644 --- a/scripts/plot_fig1_from_csv.py +++ b/scripts/plot_fig1_from_csv.py @@ -17,7 +17,7 @@ def main() -> None: parser.add_argument("--data-dir", type=Path, default=ROOT / "outputs" / "fig1") parser.add_argument("--out", type=Path, default=None) args = parser.parse_args() - out = args.out if args.out is not None else args.data_dir / "fig1_concentration.pdf" + out = args.out if args.out is not None else ROOT / "generated" / "figures" / "fig1" / "fig1_concentration.png" plot_fig1(args.data_dir, out) diff --git a/scripts/plot_fig2_from_csv.py b/scripts/plot_fig2_from_csv.py index d8c2d61..57084ba 100644 --- a/scripts/plot_fig2_from_csv.py +++ b/scripts/plot_fig2_from_csv.py @@ -17,7 +17,7 @@ def main() -> None: parser.add_argument("--data-dir", type=Path, default=ROOT / "outputs" / "fig2") parser.add_argument("--out", type=Path, default=None) args = parser.parse_args() - out = args.out if args.out is not None else args.data_dir / "fig2_peak_scaling.pdf" + out = args.out if args.out is not None else ROOT / "generated" / "figures" / "fig2" / "fig2_peak_scaling.png" plot_fig2(args.data_dir, out) diff --git a/scripts/plot_fig3_from_csv.py b/scripts/plot_fig3_from_csv.py index bcbec5d..31bd527 100644 --- a/scripts/plot_fig3_from_csv.py +++ b/scripts/plot_fig3_from_csv.py @@ -18,7 +18,7 @@ def main() -> None: parser.add_argument("--out", type=Path, default=None) parser.add_argument("--bins", type=int, default=500) args = parser.parse_args() - out = args.out if args.out is not None else args.data_dir / "fig3_spectral_bulk.pdf" + out = args.out if args.out is not None else ROOT / "generated" / "figures" / "fig3" / "fig3_spectral_bulk.png" plot_fig3(args.data_dir, out, bins=args.bins) diff --git a/scripts/plot_fig4_from_csv.py b/scripts/plot_fig4_from_csv.py index 7954de5..03c649b 100644 --- a/scripts/plot_fig4_from_csv.py +++ b/scripts/plot_fig4_from_csv.py @@ -17,7 +17,7 @@ def main() -> None: parser.add_argument("--data-dir", type=Path, default=ROOT / "outputs" / "fig4") parser.add_argument("--out", type=Path, default=None) args = parser.parse_args() - out = args.out if args.out is not None else args.data_dir / "fig4_approximation.pdf" + out = args.out if args.out is not None else ROOT / "generated" / "figures" / "fig4" / "fig4_approximation.png" plot_fig4(args.data_dir, out) diff --git a/scripts/plot_fig6_from_csv.py b/scripts/plot_fig6_from_csv.py index 937bf50..96003f9 100644 --- a/scripts/plot_fig6_from_csv.py +++ b/scripts/plot_fig6_from_csv.py @@ -17,7 +17,7 @@ def main() -> None: parser.add_argument("--data-dir", type=Path, default=ROOT / "outputs" / "fig6") parser.add_argument("--out", type=Path, default=None) args = parser.parse_args() - out = args.out if args.out is not None else args.data_dir / "fig6_renyi.pdf" + out = args.out if args.out is not None else ROOT / "generated" / "figures" / "fig6" / "fig6_renyi.png" plot_fig6(args.data_dir, out) diff --git a/scripts/plot_fig7_from_csv.py b/scripts/plot_fig7_from_csv.py index 2124011..c4c32ca 100644 --- a/scripts/plot_fig7_from_csv.py +++ b/scripts/plot_fig7_from_csv.py @@ -18,7 +18,7 @@ def main() -> None: parser.add_argument("--out", type=Path, default=None) parser.add_argument("--bins", type=int, default=100) args = parser.parse_args() - out = args.out if args.out is not None else args.data_dir / "fig7_partitions.pdf" + out = args.out if args.out is not None else ROOT / "generated" / "figures" / "fig7" / "fig7_partitions.png" plot_fig7(args.data_dir, out, bins=args.bins) diff --git a/scripts/plot_peak_verification_from_csv.py b/scripts/plot_peak_verification_from_csv.py index 36cc614..7e304d4 100644 --- a/scripts/plot_peak_verification_from_csv.py +++ b/scripts/plot_peak_verification_from_csv.py @@ -16,7 +16,7 @@ def main() -> None: args = parser.parse_args() samples = args.data_dir / f"peak_verification_n{args.n}_samples.csv" fit = args.data_dir / f"peak_verification_n{args.n}_fit.csv" - out = args.out or args.data_dir / f"peak_verification_n{args.n}_from_csv.pdf" + out = args.out or ROOT / "generated" / "figures" / "peak_verification" / f"peak_verification_n{args.n}_from_csv.png" plot_peak_search(samples, fit, out) if __name__ == "__main__": diff --git a/scripts/regenerate_fig5_baseline.py b/scripts/regenerate_fig5_baseline.py index 3d7986c..9ac71de 100644 --- a/scripts/regenerate_fig5_baseline.py +++ b/scripts/regenerate_fig5_baseline.py @@ -1,29 +1,56 @@ #!/usr/bin/env python3 -"""Regenerate the n=14 random position/Fourier baseline and arithmetic points.""" +"""Regenerate the random position/Fourier baseline and arithmetic points. + +The n=14 default is expensive. New CSVs go to generated/full/fig5. +""" from __future__ import annotations -import argparse, csv, sys + +import argparse from pathlib import Path -import numpy as np -ROOT=Path(__file__).resolve().parents[1] -sys.path.insert(0,str(ROOT)) -from subset_states.core import entropy_from_state_vector, entropy_from_support, qft_state_from_support, summary_stats -from subset_states.experiments import almost_prime_union_supports, m_grid, random_subset_qft_samples - -def write(path, rows): - path.parent.mkdir(parents=True,exist_ok=True) - with path.open('w',newline='') as f: - w=csv.DictWriter(f,fieldnames=list(rows[0].keys()));w.writeheader();w.writerows(rows) - -def main(): - p=argparse.ArgumentParser();p.add_argument('--n',type=int,default=14);p.add_argument('--points',type=int,default=100);p.add_argument('--samples',type=int,default=100);p.add_argument('--seed',type=int,default=20250604);p.add_argument('--outdir',type=Path,default=ROOT/'data');a=p.parse_args() - rows=[] - for off,m in enumerate(m_grid(1,1< None: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--n", type=int, default=14) + parser.add_argument("--points", type=int, default=100) + parser.add_argument("--samples", type=int, default=100) + parser.add_argument("--seed", type=int, default=20250604) + parser.add_argument("--outdir", type=Path, default=ROOT / "generated" / "full" / "fig5") + args = parser.parse_args() + rows = [] + for offset, m in enumerate(m_grid(1, 1 << args.n, args.points)): + position, fourier = random_subset_qft_samples(args.n, int(m), args.samples, seed=args.seed + offset) + ps, fs = summary_stats(position), summary_stats(fourier) + rows.append({ + "m": int(m), "sample_count": args.samples, + "position_mean": ps.mean, "position_std": ps.std, "position_sem": ps.sem, + "qft_mean": fs.mean, "qft_std": fs.std, "qft_sem": fs.sem, + "delta_qft_minus_position": fs.mean - ps.mean, + }) + if offset % 10 == 0: + print(f"baseline {offset + 1}/{args.points}") + unions = [] + for k, support in almost_prime_union_supports(args.n): + unions.append({ + "k": k, "m": int(support.size), + "position_entropy": entropy_from_support(args.n, support), + "qft_entropy": entropy_from_state_vector(args.n, qft_state_from_support(args.n, support)), + }) + write_rows(args.outdir / "fig5_random_qft_summary.csv", rows, rows[0].keys()) + write_rows(args.outdir / "fig5_almost_prime_unions.csv", unions, unions[0].keys()) + + +if __name__ == "__main__": + main() diff --git a/scripts/reproduce_publication_figures.py b/scripts/reproduce_publication_figures.py index f85a8fc..bdab436 100644 --- a/scripts/reproduce_publication_figures.py +++ b/scripts/reproduce_publication_figures.py @@ -1,11 +1,12 @@ #!/usr/bin/env python3 -"""Convenience driver for publication figures. +"""Redraw all seven figures from released CSV, or opt into new computations. -Default mode regenerates the fast deterministic figures (2 and 5) and verifies -that all released vector figures are present. ``--full`` launches every -sampling script with its publication defaults and can be computationally costly. +Default: generated/figures. --smoke: small new runs in generated/smoke. +--full: original sampling defaults in generated/full (potentially expensive). +No mode writes into the released data/ or outputs/ trees. """ from __future__ import annotations + import argparse from pathlib import Path import subprocess @@ -14,52 +15,113 @@ ROOT = Path(__file__).resolve().parents[1] FIGURE_FILES = { - 1: "fig1/fig1_concentration.pdf", - 2: "fig2/fig2_peak_scaling.pdf", - 3: "fig3/fig3_spectral_bulk.pdf", - 4: "fig4/fig4_approximation.pdf", - 5: "fig5/fig5_qft_residue_controls.pdf", - 6: "fig6/fig6_renyi.pdf", - 7: "fig7/fig7_partitions.pdf", + 1: "fig1/fig1_concentration.png", + 2: "fig2/fig2_peak_scaling.png", + 3: "fig3/fig3_spectral_bulk.png", + 4: "fig4/fig4_approximation.png", + 5: "fig5/fig5_qft_residue_controls.png", + 6: "fig6/fig6_renyi.png", + 7: "fig7/fig7_partitions.png", +} +CSV_INPUTS = { + 1: ("fig1_summary.csv", "fig1_zoom_summary.csv"), + 2: ("fig2_table_with_page.csv",), + 3: ("fig3_spectrum.csv", "fig3_spectrum_summary.csv"), + 4: ("fig4_summary.csv", "fig4_zoom_summary.csv"), + 5: ("fig5_random_qft_summary.csv", "fig5_almost_prime_unions.csv", "residue_matched_summary.csv"), + 6: ("fig6_renyi_summary.csv", "fig6_renyi_zoom_summary.csv"), + 7: ("fig7_partition_samples.csv", "fig7_partition_summary.csv"), } -def run(script: str, *args: str) -> None: - command = [sys.executable, str(ROOT / "scripts" / script), *args] - print("+", " ".join(command)) - subprocess.run(command, check=True, cwd=ROOT) +def validate_csv_inputs(released_root: Path, data_root: Path) -> None: + """Fail before any drawing if even one required released table is absent.""" + missing = [] + for number, names in CSV_INPUTS.items(): + folder = data_root if number == 5 else released_root / f"fig{number}" + missing.extend(str(folder / name) for name in names if not (folder / name).is_file()) + if missing: + raise FileNotFoundError("missing released CSV inputs:\n" + "\n".join(missing)) + + +def validate_output_root(outdir: Path, *protected: Path) -> None: + """Reject destinations that could overwrite released inputs or figures.""" + target = outdir.resolve() + for folder in protected: + source = folder.resolve() + if target == source or source in target.parents or target in source.parents: + raise ValueError(f"output directory must be separate from released evidence: {folder}") + + +def build_commands( + mode: str, + outdir: Path, + *, + released_root: Path = ROOT / "outputs", + data_root: Path = ROOT / "data", +) -> list[list[str]]: + """Build explicit input/output commands so no child relies on output defaults.""" + commands: list[list[str]] = [] + + def add(script: str, *args: str | Path) -> None: + commands.append([sys.executable, str(ROOT / "scripts" / script), *map(str, args)]) + + if mode == "redraw": + for number, filename in FIGURE_FILES.items(): + if number == 5: + add("fig5_qft_residue_controls.py", "--data-dir", data_root, "--outdir", outdir / "fig5") + else: + add(f"plot_fig{number}_from_csv.py", "--data-dir", released_root / f"fig{number}", "--out", outdir / filename) + return commands + if mode not in {"smoke", "full"}: + raise ValueError(f"unknown figure mode: {mode}") + + small_grid = ["--n", "6", "--points", "5", "--samples", "2", "--zoom-points", "4", "--zoom-samples", "2", "--zoom-stop", "32"] if mode == "smoke" else [] + add("fig1_concentration.py", *small_grid, "--outdir", outdir / "fig1") + add("fig2_peak_scaling.py", "--table", data_root / "table_i_peaks.csv", "--outdir", outdir / "fig2") + spectrum = ["--n", "8", "--m", "40", "--bins", "20"] if mode == "smoke" else [] + add("fig3_spectral.py", *spectrum, "--outdir", outdir / "fig3") + add("fig4_approximation.py", *small_grid, "--outdir", outdir / "fig4") + baseline = ["--n", "6", "--points", "5", "--samples", "2"] if mode == "smoke" else [] + nulls = ["--n", "6", "--samples", "2"] if mode == "smoke" else [] + add("regenerate_fig5_baseline.py", *baseline, "--outdir", outdir / "fig5") + add("run_residue_controls.py", *nulls, "--outdir", outdir / "fig5") + add("fig5_qft_residue_controls.py", "--data-dir", outdir / "fig5", "--outdir", outdir / "fig5") + add("fig6_renyi.py", *small_grid, "--outdir", outdir / "fig6") + partitions = ["--n", "6", "--m", "20", "--samples", "3"] if mode == "smoke" else [] + add("fig7_partitions.py", *partitions, "--outdir", outdir / "fig7") + return commands def main() -> None: - parser = argparse.ArgumentParser() - parser.add_argument("--full", action="store_true", help="run every publication computation; expensive") - parser.add_argument("--smoke", action="store_true", help="run reduced-size computation checks") + parser = argparse.ArgumentParser(description=__doc__) + modes = parser.add_mutually_exclusive_group() + modes.add_argument("--full", action="store_true", help="run every figure computation with original defaults; expensive") + modes.add_argument("--smoke", action="store_true", help="run reduced-size computations, including Figure 5 nulls") + parser.add_argument("--outdir", type=Path, help="generated destination; must be separate from released evidence") + parser.add_argument("--released-dir", type=Path, default=ROOT / "outputs", help="released figure CSV tree") + parser.add_argument("--data-dir", type=Path, default=ROOT / "data", help="released Figure 5 CSV and peak-table inputs") args = parser.parse_args() + args.released_dir = args.released_dir.resolve() + args.data_dir = args.data_dir.resolve() + mode = "full" if args.full else "smoke" if args.smoke else "redraw" + outdir = (args.outdir or ROOT / "generated" / ("figures" if mode == "redraw" else mode)).resolve() + try: + validate_output_root(outdir, ROOT / "outputs", ROOT / "data", args.released_dir, args.data_dir) + if mode == "redraw": + validate_csv_inputs(args.released_dir, args.data_dir) + elif not (args.data_dir / "table_i_peaks.csv").is_file(): + raise FileNotFoundError(f"missing peak-table input: {args.data_dir / 'table_i_peaks.csv'}") + except (ValueError, FileNotFoundError) as exc: + parser.error(str(exc)) - if args.full: - for script in [ - "fig1_concentration.py", "fig2_peak_scaling.py", "fig3_spectral.py", - "fig4_approximation.py", "regenerate_fig5_baseline.py", - "run_residue_controls.py", "fig5_qft_residue_controls.py", - "fig6_renyi.py", "fig7_partitions.py", - ]: - run(script) - elif args.smoke: - run("fig1_concentration.py", "--n", "6", "--points", "5", "--samples", "2", "--zoom-points", "4", "--zoom-samples", "2", "--zoom-stop", "32", "--outdir", str(ROOT / "outputs" / "smoke" / "fig1")) - run("fig2_peak_scaling.py") - run("fig3_spectral.py", "--n", "8", "--m", "40", "--bins", "20", "--outdir", str(ROOT / "outputs" / "smoke" / "fig3")) - run("fig4_approximation.py", "--n", "6", "--points", "5", "--samples", "2", "--zoom-stop", "32", "--zoom-points", "4", "--zoom-samples", "2", "--outdir", str(ROOT / "outputs" / "smoke" / "fig4")) - run("fig5_qft_residue_controls.py") - run("fig6_renyi.py", "--n", "6", "--points", "5", "--samples", "2", "--zoom-stop", "32", "--zoom-points", "4", "--zoom-samples", "2", "--outdir", str(ROOT / "outputs" / "smoke" / "fig6")) - run("fig7_partitions.py", "--n", "6", "--m", "20", "--samples", "3", "--outdir", str(ROOT / "outputs" / "smoke" / "fig7")) - else: - run("fig2_peak_scaling.py") - run("fig5_qft_residue_controls.py") - - missing = [str(ROOT / "outputs" / rel) for rel in FIGURE_FILES.values() if not (ROOT / "outputs" / rel).exists()] + for command in build_commands(mode, outdir, released_root=args.released_dir, data_root=args.data_dir): + print("+", " ".join(command), flush=True) + subprocess.run(command, check=True, cwd=ROOT) + missing = [str(outdir / rel) for rel in FIGURE_FILES.values() if not (outdir / rel).is_file()] if missing: - raise SystemExit("missing released figure files:\n" + "\n".join(missing)) - print("All seven released figure PDFs are present.") + raise SystemExit("missing generated figure files:\n" + "\n".join(missing)) + print(f"Generated all seven PNG figures in {outdir} ({mode}).") if __name__ == "__main__": diff --git a/scripts/run_residue_controls.py b/scripts/run_residue_controls.py index 1a1bede..c000119 100644 --- a/scripts/run_residue_controls.py +++ b/scripts/run_residue_controls.py @@ -96,7 +96,7 @@ def main() -> None: parser.add_argument("--matched-bits", type=int, nargs="+", default=[0, 1, 2, 3]) parser.add_argument("--samples", type=int, default=1000) parser.add_argument("--seed", type=int, default=2026081701) - parser.add_argument("--outdir", type=Path, default=ROOT / "data") + parser.add_argument("--outdir", type=Path, default=ROOT / "generated" / "full" / "fig5") args = parser.parse_args() if args.n <= 0 or args.n % 2: diff --git a/scripts/validate_residue_results.py b/scripts/validate_residue_results.py index 81ffaae..4a6ea0f 100644 --- a/scripts/validate_residue_results.py +++ b/scripts/validate_residue_results.py @@ -1,76 +1,275 @@ #!/usr/bin/env python3 +"""Check retained residue-control evidence without rewriting it. + +This validates the recorded n=14, k=1..3, t=0..3, 1000-sample experiment. +All raw-row statistics are checked; only a stated prefix is replayed from seeds. +""" from __future__ import annotations -import csv, json, math, sys -from itertools import combinations + +import argparse +import csv +import json +import math +from collections import defaultdict from pathlib import Path +import sys + import numpy as np -ROOT=Path(__file__).resolve().parents[1];sys.path.insert(0,str(ROOT)) -from subset_states.core import entropy_from_support, matrix_from_support + +ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(ROOT)) + +from subset_states import core from subset_states.experiments import almost_prime_union_supports +from scripts.run_residue_controls import sample_matched_support + + +class ValidationError(ValueError): + """An explicit acceptance failure, including when Python uses -O.""" + + +def require(condition: bool, message: str) -> None: + if not condition: + raise ValidationError(message) + + +def close(actual: float, expected: float, label: str, *, atol: float = 2e-11) -> float: + require(math.isfinite(actual) and math.isfinite(expected), f"{label}: non-finite value") + error = abs(actual - expected) + require(error <= atol, f"{label}: {actual:.16g} != {expected:.16g} (error {error:.3g})") + return error + + +def read_rows(path: Path, required: set[str]) -> list[dict[str, str]]: + with path.open(newline="", encoding="utf-8") as handle: + reader = csv.DictReader(handle) + names = reader.fieldnames or [] + require(len(names) == len(set(names)), f"{path}: duplicate columns") + require(required <= set(names), f"{path}: missing columns {sorted(required - set(names))}") + rows = list(reader) + require(bool(rows), f"{path}: no data rows") + for index, row in enumerate(rows, 2): + require(None not in row and all(value is not None for value in row.values()), + f"{path}:{index}: malformed CSV row") + for field in required: + require(row[field] != "", f"{path}:{index}: empty {field}") + return rows + + +def finite(row: dict[str, str], field: str) -> float: + value = float(row[field]) + require(math.isfinite(value), f"{field}: non-finite value") + return value + + +def shannon(counts: np.ndarray) -> float: + p = counts[counts > 0] / counts.sum() + return float(-np.sum(p * np.log2(p))) + + +def independent_entropy(n: int, support: np.ndarray, *, fourier: bool = False) -> float: + vector = np.zeros(1 << n, dtype=complex) + vector[support] = 1 / math.sqrt(len(support)) + if fourier: + # Opposite-sign QFT conjugates these real input amplitudes and preserves entropy. + vector = np.fft.fft(vector, norm="ortho") + matrix = vector.reshape(1 << (n // 2), -1) + values = np.linalg.eigvalsh(matrix.conj().T @ matrix) + require(float(values.min()) >= -2e-12, "independent density matrix is not positive") + close(float(values.sum()), 1.0, "independent density matrix trace") + values = values[values > 1e-14] + return float(-np.sum(values * np.log2(values))) + + +def independent_almost_prime_supports(n: int) -> dict[int, np.ndarray]: + # Trial division is independent of the production sieve. + counts = np.zeros(1 << n, dtype=int) + for number in range(2, 1 << n): + remainder, factor = number, 2 + while factor * factor <= remainder: + while remainder % factor == 0: + counts[number] += 1 + remainder //= factor + factor += 1 + if remainder > 1: + counts[number] += 1 + return {k: np.flatnonzero((counts >= 1) & (counts <= k)) for k in (1, 2, 3)} + + +def residue_checks(root: Path = ROOT, *, replay_samples: int = 3) -> dict[str, float | int]: + require(0 <= replay_samples <= 1000, "replay_samples must lie in [0, 1000]") + metadata = {"n", "k", "M", "matched_low_bits", "modulus", "seed"} + stat_names = {"mean", "std", "sem", "q025", "median", "q975", "min", "max"} + summary_fields = metadata | { + "samples", "residue_counts", "residue_entropy_bits", "entropy_ceiling_bits", + "forced_gap_from_balanced_max_bits", "paired_null_fourier_minus_position_mean", + "paired_null_fourier_minus_position_std", + } + for basis in ("position", "fourier"): + summary_fields |= {f"null_{basis}_{stat}" for stat in stat_names} + summary_fields |= {f"structured_{basis}_entropy", f"{basis}_residual_deficit", + f"{basis}_standardized_deficit", f"{basis}_null_count_at_or_below_structured", + f"cardinality_deficit_reduction_{basis}"} + summary = read_rows(root / "data/residue_matched_summary.csv", summary_fields) + raw = read_rows(root / "data/residue_matched_samples.csv", metadata | { + "sample", "position_entropy", "fourier_entropy", "fourier_minus_position"}) + bounds = read_rows(root / "data/residue_entropy_bounds.csv", metadata - {"seed"} | { + "residue_counts", "residue_probabilities", "residue_entropy_bits", "entropy_ceiling_bits", + "forced_gap_from_balanced_max_bits", "structured_position_entropy", "ceiling_minus_structured_entropy"}) + key = lambda row: (int(row["n"]), int(row["k"]), int(row["matched_low_bits"])) + expected = {(14, k, t) for k in (1, 2, 3) for t in range(4)} + by_key = {key(row): row for row in summary} + bound_by_key = {key(row): row for row in bounds} + require(len(summary) == len(by_key) == 12 and set(by_key) == expected, + "summary must contain each of the 12 recorded groups exactly once") + require(len(bounds) == len(bound_by_key) == 12 and set(bound_by_key) == expected, + "bounds must contain each of the 12 recorded groups exactly once") + grouped: dict[tuple[int, int, int], list[dict[str, str]]] = defaultdict(list) + for row in raw: + grouped[key(row)].append(row) + require(set(grouped) == expected, "raw group keys disagree with the recorded experiment") + + supports = independent_almost_prime_supports(14) + production_supports = dict(almost_prime_union_supports(14)) + structured = {} + for k, support in supports.items(): + require(np.array_equal(support, production_supports[k]), f"k={k}: production support sieve") + structured[k] = (independent_entropy(14, support), independent_entropy(14, support, fourier=True)) + close(core.entropy_from_support(14, support), structured[k][0], f"k={k}: production entropy") + close(core.entropy_from_state_vector(14, core.qft_state_from_support(14, support)), + structured[k][1], f"k={k}: production Fourier entropy") + + max_error, max_replay_error, replayed = 0.0, 0.0, 0 + for group in sorted(expected): + n, k, t = group + row, bound = by_key[group], bound_by_key[group] + label = f"n={n}, k={k}, t={t}" + support, q = supports[k], 1 << t + target = np.bincount(support % q, minlength=q) + count, seed = int(row["samples"]), int(row["seed"]) + require(count == 1000, f"{label}: expected 1000 recorded samples") + require(seed == 2026081701 + 1000 * k + 37 * t, f"{label}: recorded seed schedule") + require(int(row["M"]) == len(support) and int(row["modulus"]) == q, f"{label}: support metadata") + require(json.loads(row["residue_counts"]) == target.tolist(), f"{label}: residue populations") + group_rows = grouped[group] + require(len(group_rows) == count, f"{label}: sample count mismatch") + require(sorted(int(r["sample"]) for r in group_rows) == list(range(count)), + f"{label}: duplicate or missing sample IDs") + group_rows.sort(key=lambda r: int(r["sample"])) + for sample_row in group_rows: + require(all(int(sample_row[field]) == int(row[field]) for field in metadata), + f"{label}: raw metadata mismatch") + position = np.array([finite(r, "position_entropy") for r in group_rows]) + fourier = np.array([finite(r, "fourier_entropy") for r in group_rows]) + differences = np.array([finite(r, "fourier_minus_position") for r in group_rows]) + require(np.max(np.abs(differences - (fourier - position))) < 2e-11, + f"{label}: paired raw differences") + H = shannon(target) + ceiling = n / 2 - t + H + close(core.residue_class_entropy_ceiling(n, support, t), ceiling, f"{label}: production ceiling") + for field, value in { + "residue_entropy_bits": H, "entropy_ceiling_bits": ceiling, + "forced_gap_from_balanced_max_bits": n / 2 - ceiling, + "structured_position_entropy": structured[k][0], + }.items(): + max_error = max(max_error, close(finite(row, field), value, f"{label}: {field}")) + max_error = max(max_error, close(finite(bound, field), value, f"{label}: bounds {field}")) + require(all(int(bound[field]) == int(row[field]) for field in metadata - {"seed"}), + f"{label}: bounds metadata") + require(json.loads(bound["residue_counts"]) == target.tolist(), f"{label}: bounds populations") + probabilities = np.asarray(json.loads(bound["residue_probabilities"]), dtype=float) + require(probabilities.shape == target.shape and np.all(np.isfinite(probabilities)), + f"{label}: bounds probabilities shape or finiteness") + require(np.max(np.abs(probabilities - target / len(support))) < 2e-12, + f"{label}: bounds probabilities") + close(finite(bound, "ceiling_minus_structured_entropy"), ceiling - structured[k][0], + f"{label}: ceiling slack") + require(structured[k][0] <= ceiling + 2e-11 and float(position.max()) <= ceiling + 2e-11, + f"{label}: position residue ceiling violated") + for basis, values, entropy in zip(("position", "fourier"), (position, fourier), structured[k]): + require(np.all((values >= -2e-11) & (values <= n / 2 + 2e-11)), f"{label}: entropy range") + close(finite(row, f"structured_{basis}_entropy"), entropy, f"{label}: structured {basis}") + sd = float(values.std(ddof=1)) + require(sd > 0, f"{label}: zero null variance") + statistics = dict(mean=float(values.mean()), std=sd, sem=sd / math.sqrt(count), + q025=float(np.quantile(values, .025)), median=float(np.median(values)), + q975=float(np.quantile(values, .975)), min=float(values.min()), max=float(values.max())) + for stat, value in statistics.items(): + field = f"null_{basis}_{stat}" + max_error = max(max_error, close(finite(row, field), value, f"{label}: {field}")) + residual = statistics["mean"] - entropy + close(finite(row, f"{basis}_residual_deficit"), residual, f"{label}: {basis} residual") + close(finite(row, f"{basis}_standardized_deficit"), residual / sd, + f"{label}: {basis} standardized deficit", atol=2e-8) + below = int(np.count_nonzero(values <= entropy)) + require(int(row[f"{basis}_null_count_at_or_below_structured"]) == below, + f"{label}: {basis} tail count mismatch") + require(below == 0, f"{label}: recorded all-samples-above-structured claim fails") + baseline = finite(by_key[(n, k, 0)], f"{basis}_residual_deficit") + require(baseline != 0, f"{label}: zero cardinality baseline") + close(finite(row, f"cardinality_deficit_reduction_{basis}"), 1 - residual / baseline, + f"{label}: {basis} deficit reduction") + close(finite(row, "paired_null_fourier_minus_position_mean"), float(differences.mean()), f"{label}: paired mean") + close(finite(row, "paired_null_fourier_minus_position_std"), float(differences.std(ddof=1)), f"{label}: paired std") + + rng = np.random.default_rng(seed) + for sample in range(replay_samples): + sampled = sample_matched_support(n, target, q, rng) + require(len(sampled) == len(support) and len(np.unique(sampled)) == len(sampled), + f"{label}: sampler cardinality or uniqueness") + require(np.all((sampled >= 0) & (sampled < (1 << n))), f"{label}: sampler label range") + require(np.array_equal(np.bincount(sampled % q, minlength=q), target), f"{label}: sampler populations") + for basis, actual, recorded in ( + ("position", core.entropy_from_support(n, sampled), position[sample]), + ("fourier", core.entropy_from_state_vector(n, core.qft_state_from_support(n, sampled)), fourier[sample]), + ): + max_replay_error = max(max_replay_error, close(actual, recorded, f"{label}, sample={sample}: replay {basis}")) + replayed += 1 + return {"groups_checked": len(summary), "raw_rows_checked": len(raw), + "max_summary_error": max_error, "seed_replayed_supports": replayed, + "max_seed_replay_entropy_error": max_replay_error} + + +def markdown_report(title: str, sections: dict[str, dict[str, float | int]], scope: str) -> str: + lines = [f"# {title}", "", "**Status: PASS for the checks listed below.**", "", scope, ""] + for name, values in sections.items(): + lines.extend([f"## {name}", "", "| Check | Result |", "| --- | ---: |"]) + lines.extend(f"| {key.replace('_', ' ')} | {value:.12g} |" for key, value in values.items()) + lines.append("") + return "\n".join(lines) + + +def write_report(text: str, output: Path | None) -> None: + if output is not None: + destination = output.resolve() + require(destination.suffix.lower() == ".md", "--output must name a Markdown (.md) file") + require(not destination.exists(), "refusing to overwrite an existing file") + for name in ("data", "outputs", "subset_states", "scripts", "tests"): + require(not destination.is_relative_to(ROOT / name), "refusing to write inside source or evidence directories") + destination.parent.mkdir(parents=True, exist_ok=True) + with destination.open("x", encoding="utf-8") as handle: + handle.write(text) + print(text) + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--output", type=Path, help="save the report to a new .md file") + parser.add_argument("--replay-samples", type=int, default=3, help="seed-replay prefix per group (1..1000; default 3)") + args = parser.parse_args() + try: + require(1 <= args.replay_samples <= 1000, "--replay-samples must lie in [1, 1000]") + results = residue_checks(replay_samples=args.replay_samples) + report = markdown_report("Residue-control validation", {"Recorded experiment": results}, + "Checks all 12,000 raw rows against summaries and bounds, recomputes the three structured states, " + "and replays the stated number of matched supports. This is not a full rerun unless all 1,000 " + "samples per group are requested; it does not establish an arithmetic mechanism or novelty.") + write_report(report, args.output) + except (ValueError, OSError, KeyError, TypeError) as exc: + print(f"# Residue-control validation\n\n**Status: FAIL**\n\n{exc}", file=sys.stderr) + return 1 + return 0 + -def H(counts): - c=np.asarray(counts,dtype=float);p=c[c>0]/c.sum();return float(-np.sum(p*np.log2(p))) if p.size else 0.0 - -def direct_entropy(n,support): - omega=matrix_from_support(n,support);rho=omega.conj().T@omega - w=np.linalg.eigvalsh(rho).real;w=w[w>1e-14];w=w/w.sum();return float(-np.sum(w*np.log2(w))) - -def main(): - out=[];n=4;N=1<>i&1),dtype=np.int64) - S=entropy_from_support(n,s);max_direct=max(max_direct,abs(S-direct_entropy(n,s))) - for t in (1,2): - q=1<1e-10 or max_direct>1e-10 or single_class_violation>1e-10 or max_mean_err>1e-12 or max_std_err>1e-12 or not all_counts or not all_below: raise SystemExit(1) -if __name__=='__main__':main() +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/subset_states/core.py b/subset_states/core.py index 1624240..49ed444 100644 --- a/subset_states/core.py +++ b/subset_states/core.py @@ -3,6 +3,7 @@ from dataclasses import dataclass from itertools import combinations from math import log2, sqrt +from numbers import Integral, Real from typing import Iterable, Sequence import numpy as np @@ -22,18 +23,48 @@ class SummaryStats: maximum: float +def _validate_positive_n(n: int) -> None: + if isinstance(n, (bool, np.bool_)) or not isinstance(n, Integral) or n <= 0: + raise ValueError(f"n must be a positive integer; received n={n}.") + + def _validate_even_n(n: int) -> None: - if n <= 0 or n % 2 != 0: + _validate_positive_n(n) + if n % 2 != 0: raise ValueError(f"n must be a positive even integer; received n={n}.") def _validate_support_size(n: int, m: int, *, allow_zero: bool = False) -> None: N = 1 << n lower = 0 if allow_zero else 1 - if not (lower <= m <= N): + if isinstance(m, (bool, np.bool_)) or not isinstance(m, Integral) or not (lower <= m <= N): raise ValueError(f"m must satisfy {lower} <= m <= 2**n={N}; received m={m}.") +def _validated_support(n: int, support: ArrayLike) -> NDArray[np.int64]: + """Validate before conversion so fractional or unsigned labels cannot wrap.""" + + _validate_positive_n(n) + labels = np.asarray(support) + if labels.ndim != 1 or labels.size == 0: + raise ValueError("support must be a nonempty one-dimensional array of integer basis labels.") + if labels.dtype.kind not in "iu": + raise ValueError("support labels must be integers.") + N = 1 << n + if np.any(labels < 0) or np.any(labels >= N): + raise ValueError(f"support labels must lie in [0, {N - 1}].") + if np.unique(labels).size != labels.size: + raise ValueError("support contains duplicate basis labels.") + if np.any(labels > np.iinfo(np.int64).max): + raise ValueError("support labels exceed the supported int64 range.") + return labels.astype(np.int64, copy=False) + + +def _validate_atol(atol: float) -> None: + if not isinstance(atol, Real) or not np.isfinite(atol) or atol < 0: + raise ValueError("atol must be a finite nonnegative real number.") + + def natural_right_bits(n: int) -> tuple[int, ...]: """Return the natural right subsystem: the low-order n/2 bits.""" @@ -42,6 +73,8 @@ def natural_right_bits(n: int) -> tuple[int, ...]: def _left_bits(n: int, right_bits: Sequence[int]) -> tuple[int, ...]: + if any(isinstance(bit, (bool, np.bool_)) or not isinstance(bit, Integral) for bit in right_bits): + raise ValueError("right_bits must contain integer bit positions.") right = set(right_bits) if len(right) != len(right_bits): raise ValueError("right_bits contains repeated bit positions.") @@ -53,8 +86,7 @@ def _left_bits(n: int, right_bits: Sequence[int]) -> tuple[int, ...]: def random_subset(n: int, m: int, rng: np.random.Generator) -> NDArray[np.int64]: """Sample a uniformly random subset of {0, ..., 2**n - 1} of size m.""" - if n <= 0: - raise ValueError("n must be positive.") + _validate_positive_n(n) _validate_support_size(n, m) return np.sort(rng.choice(1 << n, size=m, replace=False).astype(np.int64)) @@ -79,22 +111,17 @@ def matrix_from_support( ) -> NDArray: """Return the bipartite coefficient matrix for an equal-amplitude support. - With ``normalize=True``, each occupied entry equals 1/sqrt(M), so the reduced - density matrix of the right subsystem is ``C.conj().T @ C``. Set + The first position in each subsystem's bit list is its least significant + bit. With ``normalize=True``, each occupied entry equals 1/sqrt(M). For + coefficients C[a, b], the right reduced state is ``C.T @ C.conj()`` + (here C is real). Set ``normalize=False`` to obtain the binary support-incidence matrix. """ _validate_even_n(n) - support_arr = np.asarray(support, dtype=np.int64) - if support_arr.ndim != 1: - raise ValueError("support must be a one-dimensional array of basis labels.") - if support_arr.size == 0: - raise ValueError("support must contain at least one basis label.") - N = 1 << n - if np.any(support_arr < 0) or np.any(support_arr >= N): - raise ValueError(f"support labels must lie in [0, {N - 1}].") - if np.unique(support_arr).size != support_arr.size: - raise ValueError("support contains duplicate basis labels.") + support_arr = _validated_support(n, support) + if normalize and np.dtype(dtype).kind not in "fc": + raise ValueError("normalized coefficients require a floating-point or complex dtype.") right = tuple(right_bits) if right_bits is not None else natural_right_bits(n) if len(right) != n // 2: @@ -114,28 +141,42 @@ def matrix_from_state_vector( state: ArrayLike, right_bits: Sequence[int] | None = None, ) -> NDArray: - """Return the bipartite coefficient matrix for an arbitrary state vector.""" + """Return C[a, b] in the same basis order as :func:`matrix_from_support`. + + The first position in each subsystem's bit list is its least significant + bit. The right reduced state is ``C.T @ C.conj()``; ``C.conj().T @ C`` + is its transpose and has the same spectrum, but generally different entries. + The vector need not be normalized. + """ _validate_even_n(n) vector = np.asarray(state) N = 1 << n if vector.shape != (N,): raise ValueError(f"state must have shape ({N},); received {vector.shape}.") + if vector.dtype.kind not in "biufc" or not np.all(np.isfinite(vector)): + raise ValueError("state must contain finite numeric amplitudes.") right = tuple(right_bits) if right_bits is not None else natural_right_bits(n) if len(right) != n // 2: raise ValueError(f"balanced bipartition requires {n // 2} right bits.") left = _left_bits(n, right) - left_axes = [n - 1 - bit for bit in left] - right_axes = [n - 1 - bit for bit in right] + # Reshape traverses axes from most to least significant within each index. + left_axes = [n - 1 - bit for bit in reversed(left)] + right_axes = [n - 1 - bit for bit in reversed(right)] tensor = vector.reshape((2,) * n) return np.transpose(tensor, left_axes + right_axes).reshape(1 << len(left), 1 << len(right)) def spectrum_from_matrix(matrix: ArrayLike, *, atol: float = 1e-14) -> NDArray[np.float64]: - """Return the non-zero eigenvalues of C†C using a stable SVD.""" + """Return normalized non-zero reduced-state eigenvalues using a stable SVD.""" + _validate_atol(atol) mat = np.asarray(matrix) + if mat.ndim != 2 or mat.size == 0: + raise ValueError("matrix must be a nonempty two-dimensional array.") + if mat.dtype.kind not in "biufc" or not np.all(np.isfinite(mat)): + raise ValueError("matrix must contain finite numeric coefficients.") singular_values = np.linalg.svd(mat, compute_uv=False) eigvals = np.real_if_close(singular_values * singular_values).astype(np.float64) eigvals[eigvals < atol] = 0.0 @@ -152,18 +193,41 @@ def renyi_entropy_from_spectrum( *, atol: float = 1e-14, ) -> float: - """Return the Rényi entropy in bits. order=1 gives von Neumann entropy.""" + """Return the Rényi entropy in bits; order=1 gives von Neumann entropy. - p = np.asarray(eigvals, dtype=np.float64) + A finite, nonnegative spectrum is normalized after entries at or below + ``atol`` are removed. Negative roundoff no smaller than ``-atol`` is allowed. + The order must be positive; positive infinity gives the min-entropy. + """ + + _validate_atol(atol) + if not isinstance(order, Real) or np.isnan(order) or order <= 0: + raise ValueError("Rényi order must be a positive real number or positive infinity.") + p = np.asarray(eigvals) + if p.ndim != 1 or p.size == 0 or p.dtype.kind not in "biuf": + raise ValueError("spectrum must be a nonempty one-dimensional array of real eigenvalues.") + p = p.astype(np.float64) + if not np.all(np.isfinite(p)) or np.any(p < -atol): + raise ValueError("spectrum must be finite and nonnegative within atol.") p = p[p > atol] + if p.size == 0: + raise ValueError("density-matrix spectrum has zero trace at the requested atol.") + # Scaling first avoids overflow when normalizing a finite input spectrum. + p = p / p.max() p = p / p.sum() - if np.isclose(order, 1.0): + if order == 1.0: return float(-np.sum(xlogy(p, p)) / np.log(2.0)) if np.isinf(order): return float(-np.log2(np.max(p))) - if order <= 0: - raise ValueError("Rényi order must be positive.") - return float(np.log2(np.sum(p**order)) / (1.0 - order)) + log_p = np.log(p) + if abs(order - 1.0) < 0.1: + # log(sum(p**order)) near one, without subtracting nearly equal numbers. + delta = np.sum(p * np.expm1((order - 1.0) * log_p)) + return float(np.log1p(delta) / ((1.0 - order) * np.log(2.0))) + log_max = np.max(log_p) + with np.errstate(over="ignore"): + tail = logsumexp(order * (log_p - log_max)) + return float((order / (1.0 - order) * log_max + tail / (1.0 - order)) / np.log(2.0)) def entropy_from_support( @@ -192,13 +256,13 @@ def qft_state_from_support(n: int, support: ArrayLike) -> NDArray[np.complex128] """Return the positive-exponent QFT of an equal-amplitude subset state. NumPy's ``ifft(..., norm='ortho')`` implements the convention - exp(+2πijk/N)/sqrt(N), matching the manuscript definition. For the real + exp(+2πijk/N)/sqrt(N), matching the research notes. For the real input states used here, switching the sign only complex-conjugates the output and therefore leaves all reported entropies unchanged. """ + support_arr = _validated_support(n, support) N = 1 << n - support_arr = np.asarray(support, dtype=np.int64) vector = np.zeros(N, dtype=np.complex128) vector[support_arr] = 1.0 / sqrt(support_arr.size) return np.fft.ifft(vector, norm="ortho") @@ -297,9 +361,9 @@ def hypergeometric_occupancy_approximation(n: int, m: int) -> float: """ _validate_even_n(n) - if m <= 0: + _validate_support_size(n, m, allow_zero=True) + if m == 0: return 0.0 - _validate_support_size(n, m) d = 1 << (n // 2) N = 1 << n w_min = max(0, m - (N - d)) @@ -326,7 +390,7 @@ def fixed_cardinality_average_purity(n: int, m: int) -> float: """Exact ensemble-average purity for the balanced fixed-cardinality ensemble. For N=2**n=d**2 and a support chosen uniformly among all M-element subsets, - this returns E[Tr(rho_R**2)] as derived in the manuscript. + this returns E[Tr(rho_R**2)] as derived in the research notes. """ _validate_even_n(n) @@ -356,17 +420,12 @@ class r modulo 2**t, then S <= n/2 - t + H(p). The case t=1 is the """ _validate_even_n(n) + if isinstance(matched_low_bits, (bool, np.bool_)) or not isinstance(matched_low_bits, Integral): + raise ValueError("matched_low_bits must be an integer.") t = int(matched_low_bits) if not (0 <= t <= n // 2): raise ValueError(f"matched_low_bits must lie in [0, {n // 2}].") - support_arr = np.asarray(support, dtype=np.int64) - if support_arr.ndim != 1 or support_arr.size == 0: - raise ValueError("support must be a nonempty one-dimensional array.") - N = 1 << n - if np.any(support_arr < 0) or np.any(support_arr >= N): - raise ValueError(f"support labels must lie in [0, {N - 1}].") - if np.unique(support_arr).size != support_arr.size: - raise ValueError("support contains duplicate basis labels.") + support_arr = _validated_support(n, support) modulus = 1 << t counts = np.bincount(support_arr % modulus, minlength=modulus).astype(float) probabilities = counts[counts > 0] / support_arr.size diff --git a/subset_states/csv_plotting.py b/subset_states/csv_plotting.py index c790b09..60f1f93 100644 --- a/subset_states/csv_plotting.py +++ b/subset_states/csv_plotting.py @@ -70,7 +70,7 @@ def _fit_line(x: np.ndarray, y: np.ndarray) -> dict: } -def plot_fig1(data_dir: str | Path, out_pdf: str | Path, *, n: int | None = None) -> None: +def plot_fig1(data_dir: str | Path, out_path: str | Path, *, n: int | None = None) -> None: """Plot the two averaging procedures with explicit entropy units.""" data_dir = Path(data_dir) @@ -121,10 +121,10 @@ def draw(axis, rows, markersize: float = 2.5): inset.set_xlabel(r"$M$", labelpad=1) inset.set_ylabel("bits", labelpad=1) inset.legend().remove() - save_figure(fig, out_pdf) + save_figure(fig, out_path) -def plot_fig2(data_dir: str | Path, out_pdf: str | Path, *, write_fit_summary: bool = True) -> None: +def plot_fig2(data_dir: str | Path, out_path: str | Path, *, write_fit_summary: bool = False) -> None: """Plot Table-I scaling, retaining all n=10,...,30 points.""" data_dir = Path(data_dir) @@ -143,7 +143,7 @@ def plot_fig2(data_dir: str | Path, out_pdf: str | Path, *, write_fit_summary: b fit_S = _fit_line(n, S_n) if write_fit_summary: fit_rows = [{"quantity": "log2_M_n", **fit_logM}, {"quantity": "S_n", **fit_S}] - write_rows(data_dir / "fig2_linear_fit_summary_from_csv.csv", fit_rows, fit_rows[0].keys()) + write_rows(Path(out_path).parent / "fig2_linear_fit_summary_from_csv.csv", fit_rows, fit_rows[0].keys()) apply_journal_style() fig, ax = plt.subplots(figsize=(4.7, 3.6)) @@ -172,10 +172,10 @@ def plot_fig2(data_dir: str | Path, out_pdf: str | Path, *, write_fit_summary: b ax.set_xticks([10, 15, 20, 25, 30]) ax.set_ylim(0, max(log2_M.max(), page.max()) + 1) ax.legend(frameon=False, loc="upper left") - save_figure(fig, out_pdf) + save_figure(fig, out_path) -def plot_fig3(data_dir: str | Path, out_pdf: str | Path, *, bins: int = 500) -> None: +def plot_fig3(data_dir: str | Path, out_path: str | Path, *, bins: int = 500) -> None: """Plot the spectral bulk and compare three isolated-mode estimates.""" data_dir = Path(data_dir) @@ -221,7 +221,7 @@ def plot_fig3(data_dir: str | Path, out_pdf: str | Path, *, bins: int = 500) -> inset.set_xlabel("isolated eigenvalue", labelpad=1) inset.tick_params(axis="y", labelsize=6.8) inset.tick_params(axis="x", labelsize=6.8) - save_figure(fig, out_pdf) + save_figure(fig, out_path) def _draw_fig4(axis, rows: Sequence[dict], n: int, label_data: bool) -> None: @@ -243,7 +243,7 @@ def _draw_fig4(axis, rows: Sequence[dict], n: int, label_data: bool) -> None: axis.plot(m, dense, "--", label=r"dense bulk $T_{N,M}$" if label_data else None) -def plot_fig4(data_dir: str | Path, out_pdf: str | Path, *, n: int | None = None) -> None: +def plot_fig4(data_dir: str | Path, out_path: str | Path, *, n: int | None = None) -> None: """Plot numerical data with corrected fixed-cardinality approximations.""" data_dir = Path(data_dir) @@ -267,10 +267,10 @@ def plot_fig4(data_dir: str | Path, out_pdf: str | Path, *, n: int | None = None inset.set_ylim(max(0, n / 2 - 3), n / 2) inset.set_xlabel(r"$M$", labelpad=1) inset.set_ylabel("bits", labelpad=1) - save_figure(fig, out_pdf) + save_figure(fig, out_path) -def plot_fig5(data_dir: str | Path, out_pdf: str | Path, *, n: int | None = None) -> None: +def plot_fig5(data_dir: str | Path, out_path: str | Path, *, n: int | None = None) -> None: """Compare deterministic arithmetic supports with the random null distribution. The shaded regions are one ensemble standard deviation. They represent the @@ -325,7 +325,7 @@ def plot_fig5(data_dir: str | Path, out_pdf: str | Path, *, n: int | None = None ax.set_xlim(0, 1 << n) ax.set_ylim(0, n / 2) ax.legend(frameon=False, loc="lower center", bbox_to_anchor=(0.5, -0.46), ncol=2) - save_figure(fig, out_pdf) + save_figure(fig, out_path) def _order_key(value) -> float: @@ -365,7 +365,7 @@ def _draw_fig6(axis, rows: Sequence[dict]) -> list: return handles -def plot_fig6(data_dir: str | Path, out_pdf: str | Path, *, n: int | None = None) -> None: +def plot_fig6(data_dir: str | Path, out_path: str | Path, *, n: int | None = None) -> None: data_dir = Path(data_dir) rows = read_csv_rows(data_dir / "fig6_renyi_summary.csv") zoom = read_csv_rows(data_dir / "fig6_renyi_zoom_summary.csv") @@ -392,7 +392,7 @@ def plot_fig6(data_dir: str | Path, out_pdf: str | Path, *, n: int | None = None labels = [r"$S^{(1)}$", r"$S^{(2)}$", r"$S^{(\infty)}$"] fig.legend(handles=handles, labels=labels, frameon=False, ncol=3, loc="lower center") fig.subplots_adjust(hspace=0.45, bottom=0.12) - save_figure(fig, out_pdf) + save_figure(fig, out_path) def _families(rows: Sequence[dict]) -> list[str]: @@ -404,7 +404,7 @@ def _families(rows: Sequence[dict]) -> list[str]: return seen -def plot_fig7(data_dir: str | Path, out_pdf: str | Path, *, n: int | None = None, bins: int = 100) -> None: +def plot_fig7(data_dir: str | Path, out_path: str | Path, *, n: int | None = None, bins: int = 100) -> None: """Plot both bipartition distributions on a common entropy scale.""" data_dir = Path(data_dir) @@ -462,7 +462,7 @@ def plot_fig7(data_dir: str | Path, out_pdf: str | Path, *, n: int | None = None ax.legend(frameon=False, loc="upper right") axes[-1].set_xlabel("entropy (bits)") fig.subplots_adjust(hspace=0.30) - save_figure(fig, out_pdf) + save_figure(fig, out_path) def _n_from_fig8_path(path: Path) -> int: @@ -474,7 +474,7 @@ def _n_from_fig8_path(path: Path) -> int: def plot_fig8( data_dir: str | Path, - out_pdf: str | Path, + out_path: str | Path, *, n_values: Sequence[int] | None = None, bins: int = 120, @@ -526,10 +526,10 @@ def plot_fig8( ax.set_title(rf"$n={n}$, $M={M}$") ax.legend(frameon=False) fig.subplots_adjust(hspace=0.55) - save_figure(fig, out_pdf) + save_figure(fig, out_path) -def plot_peak_search(samples_csv: str | Path, fit_csv: str | Path, out_pdf: str | Path) -> None: +def plot_peak_search(samples_csv: str | Path, fit_csv: str | Path, out_path: str | Path) -> None: samples = read_csv_rows(samples_csv) fit_rows = read_csv_rows(fit_csv) if not fit_rows: @@ -554,4 +554,4 @@ def plot_peak_search(samples_csv: str | Path, fit_csv: str | Path, out_pdf: str ax.set_xlabel(r"$\log_2 M$") ax.set_ylabel("entropy (bits)") ax.legend(frameon=False) - save_figure(fig, out_pdf) + save_figure(fig, out_path) diff --git a/subset_states/plotting.py b/subset_states/plotting.py index 55df510..1c0290a 100644 --- a/subset_states/plotting.py +++ b/subset_states/plotting.py @@ -41,8 +41,8 @@ def apply_journal_style() -> None: "ytick.labelsize": 8, "lines.linewidth": 1.15, "axes.prop_cycle": cycler(color=COOL_PALETTE), - "pdf.fonttype": 42, - "ps.fonttype": 42, + "text.usetex": False, + "savefig.dpi": 300, "savefig.bbox": "tight", } ) @@ -52,6 +52,4 @@ def save_figure(fig, path: str | Path) -> None: path = Path(path) path.parent.mkdir(parents=True, exist_ok=True) fig.savefig(path) - if path.suffix.lower() == ".pdf": - fig.savefig(path.with_suffix(".png"), dpi=300) plt.close(fig) diff --git a/tests/test_figure_workflow.py b/tests/test_figure_workflow.py new file mode 100644 index 0000000..8bc87d7 --- /dev/null +++ b/tests/test_figure_workflow.py @@ -0,0 +1,95 @@ +"""Regression checks for evidence-safe figure reproduction.""" +from __future__ import annotations + +import importlib.util +from pathlib import Path +import shutil +import subprocess +import sys +import tempfile +import unittest + +ROOT = Path(__file__).resolve().parents[1] +SCRIPT = ROOT / "scripts" / "reproduce_publication_figures.py" +SPEC = importlib.util.spec_from_file_location("figure_workflow", SCRIPT) +workflow = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(workflow) + + +def option(command: list[str], name: str) -> Path: + return Path(command[command.index(name) + 1]) + + +class FigureWorkflowTests(unittest.TestCase): + def test_every_mode_separates_outputs_and_fig5_consumes_its_new_data(self): + with tempfile.TemporaryDirectory() as directory: + destination = Path(directory) / "new" + for mode in ("redraw", "smoke", "full"): + with self.subTest(mode=mode): + commands = workflow.build_commands(mode, destination) + for command in commands: + flag = "--out" if "--out" in command else "--outdir" + self.assertTrue(option(command, flag).is_relative_to(destination)) + if mode == "redraw": + self.assertEqual(len(commands), 7) + self.assertEqual(len({Path(command[1]).stem for command in commands}), 7) + self.assertTrue(all("from_csv" in command[1] or "fig5_qft_residue_controls" in command[1] for command in commands)) + else: + by_script = {Path(command[1]).name: command for command in commands} + plot = by_script["fig5_qft_residue_controls.py"] + for producer in ("regenerate_fig5_baseline.py", "run_residue_controls.py"): + self.assertEqual(option(by_script[producer], "--outdir"), option(plot, "--data-dir")) + self.assertLess(commands.index(by_script[producer]), commands.index(plot)) + + def test_existing_pngs_do_not_hide_a_missing_csv(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + released, data, out = root / "released", root / "data", root / "new" + for number, names in workflow.CSV_INPUTS.items(): + folder = data if number == 5 else released / f"fig{number}" + folder.mkdir(parents=True, exist_ok=True) + for name in names: + (folder / name).write_text("placeholder\n") + missing = released / "fig4" / "fig4_zoom_summary.csv" + missing.unlink() + for filename in workflow.FIGURE_FILES.values(): + path = out / filename + path.parent.mkdir(parents=True, exist_ok=True) + path.write_bytes(b"old figure") + result = subprocess.run([sys.executable, str(SCRIPT), "--released-dir", str(released), "--data-dir", str(data), "--outdir", str(out)], capture_output=True, text=True) + self.assertNotEqual(result.returncode, 0) + self.assertIn(str(missing), result.stderr) + self.assertNotIn("+ ", result.stdout) + self.assertTrue(all((out / name).read_bytes() == b"old figure" for name in workflow.FIGURE_FILES.values())) + + def test_evidence_destination_and_symlink_alias_are_rejected(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + evidence = root / "released" + evidence.mkdir() + alias = root / "alias" + alias.symlink_to(evidence, target_is_directory=True) + for destination in (root, evidence, evidence / "fig1", alias / "fig1"): + with self.subTest(destination=destination), self.assertRaises(ValueError): + workflow.validate_output_root(destination, evidence) + workflow.validate_output_root(root / "generated", evidence) + + def test_figure_two_redraw_keeps_input_directory_read_only(self): + from subset_states.csv_plotting import plot_fig2 + + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + source = root / "released" + source.mkdir() + name = "fig2_table_with_page.csv" + shutil.copy2(ROOT / "outputs" / "fig2" / name, source / name) + before = (source / name).read_bytes() + destination = root / "generated" / "figure.png" + plot_fig2(source, destination) + self.assertEqual(list(source.iterdir()), [source / name]) + self.assertEqual((source / name).read_bytes(), before) + self.assertTrue(destination.read_bytes().startswith(b"\x89PNG\r\n\x1a\n")) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_input_validation.py b/tests/test_input_validation.py new file mode 100644 index 0000000..e6160a1 --- /dev/null +++ b/tests/test_input_validation.py @@ -0,0 +1,139 @@ +from __future__ import annotations + +from decimal import Decimal, localcontext +from itertools import permutations +import unittest + +import numpy as np + +from subset_states.core import ( + fixed_cardinality_mean_spectrum, + hypergeometric_occupancy_approximation, + matrix_from_state_vector, + matrix_from_support, + qft_state_from_support, + renyi_entropy_from_spectrum, + residue_class_entropy_ceiling, + spectrum_from_matrix, +) + + +class SupportValidationTests(unittest.TestCase): + def test_support_apis_reject_malformed_labels_before_casting(self) -> None: + invalid = ( + [], [[1, 2]], 1, [1, 1], [-1], [16], [1.5], [1.0], + [True], [1j], [np.nan], [np.inf], ["1"], + np.asarray([2**64 - 1], dtype=np.uint64), + ) + functions = ( + lambda support: matrix_from_support(4, support), + lambda support: qft_state_from_support(4, support), + lambda support: residue_class_entropy_ceiling(4, support, 1), + ) + for support in invalid: + for function in functions: + with self.subTest(support=support, function=function): + with self.assertRaises(ValueError): + function(support) + + def test_qft_accepts_odd_qubit_count_and_preserves_norm(self) -> None: + support = np.asarray([0, 3, 7], dtype=np.uint64) + state = qft_state_from_support(3, support) + np.testing.assert_allclose(np.vdot(state, state), 1.0, atol=1e-14) + + def test_integer_parameters_are_not_silently_truncated(self) -> None: + for n in (0, -2, 4.0, True): + with self.subTest(n=n), self.assertRaises(ValueError): + qft_state_from_support(n, [0]) + for m in (1.5, True, -1): + for function in (fixed_cardinality_mean_spectrum, hypergeometric_occupancy_approximation): + with self.subTest(m=m, function=function), self.assertRaises(ValueError): + function(4, m) + for t in (1.5, True, -1, 3): + with self.subTest(t=t), self.assertRaises(ValueError): + residue_class_entropy_ceiling(4, [0, 1], t) + + def test_normalized_integer_dtype_cannot_silently_erase_amplitudes(self) -> None: + with self.assertRaises(ValueError): + matrix_from_support(4, [0, 1], dtype=int) + binary = matrix_from_support(4, [0, 1], normalize=False, dtype=int) + self.assertEqual(binary.sum(), 2) + + +class CoefficientMatrixTests(unittest.TestCase): + def test_vector_and_support_use_identical_basis_order(self) -> None: + support = [1, 4, 9, 15] + state = np.zeros(16) + state[support] = 1 / np.sqrt(len(support)) + for right in permutations(range(4), 2): + with self.subTest(right=right): + np.testing.assert_array_equal( + matrix_from_state_vector(4, state, right), + matrix_from_support(4, support, right), + ) + + def test_complex_coefficients_and_partial_trace_in_computational_basis(self) -> None: + state = np.arange(16) + 1j * np.arange(16)[::-1] ** 2 + state = state / np.linalg.norm(state) + expected_matrix = state.reshape(4, 4) + matrix = matrix_from_state_vector(4, state) + np.testing.assert_array_equal(matrix, expected_matrix) + expected_rho = np.zeros((4, 4), dtype=complex) + for a in range(4): + for b in range(4): + for c in range(4): + expected_rho[b, c] += state[4 * a + b] * state[4 * a + c].conjugate() + np.testing.assert_allclose(matrix.T @ matrix.conj(), expected_rho, atol=1e-15) + self.assertGreater(np.max(abs(matrix.conj().T @ matrix - expected_rho)), 0.01) + expected_spectrum = np.linalg.eigvalsh(expected_rho) + expected_spectrum = expected_spectrum[expected_spectrum > 1e-14] + np.testing.assert_allclose(spectrum_from_matrix(matrix), expected_spectrum, atol=1e-14) + + def test_partitions_and_state_values_are_validated(self) -> None: + for right in ((0, 0), (0, 4), (0, 1.5), (0, True)): + for function, data in ((matrix_from_state_vector, np.ones(16)), (matrix_from_support, [0])): + with self.subTest(right=right, function=function), self.assertRaises(ValueError): + function(4, data, right) + for state in (np.ones((4, 4)), np.full(16, np.nan), np.full(16, np.inf)): + with self.subTest(state=state), self.assertRaises(ValueError): + matrix_from_state_vector(4, state) + + +class SpectrumValidationTests(unittest.TestCase): + def test_invalid_spectra_and_orders_are_rejected(self) -> None: + for spectrum in ([], [0, 0], [-0.1, 1.1], [np.nan, 1], [np.inf, 1], [[0.5, 0.5]], [1j]): + with self.subTest(spectrum=spectrum), self.assertRaises(ValueError): + renyi_entropy_from_spectrum(spectrum) + for order in (0, -1, -np.inf, np.nan, 1j): + with self.subTest(order=order), self.assertRaises(ValueError): + renyi_entropy_from_spectrum([0.25, 0.75], order) + + def test_tolerated_roundoff_and_positive_infinite_order(self) -> None: + self.assertAlmostEqual(renyi_entropy_from_spectrum([-1e-16, 0.5, 0.5]), 1) + self.assertAlmostEqual(renyi_entropy_from_spectrum([0.25, 0.75], np.inf), -np.log2(0.75)) + + def test_finite_orders_do_not_underflow_or_collapse_to_order_one(self) -> None: + self.assertAlmostEqual(renyi_entropy_from_spectrum([0.5, 0.5], 2000), 1) + p = np.asarray([0.2, 0.8]) + order = 1.0 + 1e-6 + with localcontext() as context: + context.prec = 60 + alpha = Decimal.from_float(order) + expected = (Decimal("0.2") ** alpha + Decimal("0.8") ** alpha).ln() / ((1 - alpha) * Decimal(2).ln()) + computed = renyi_entropy_from_spectrum(p, order) + self.assertAlmostEqual(computed, float(expected), places=11) + self.assertGreater(abs(computed - renyi_entropy_from_spectrum(p, 1)), 1e-8) + self.assertAlmostEqual(renyi_entropy_from_spectrum(p, 1 + 1e-12), renyi_entropy_from_spectrum(p), places=11) + + def test_invalid_matrices_and_cutoffs_are_rejected(self) -> None: + for matrix in ([], [1, 2], np.zeros((2, 2)), [[np.nan]], [[np.inf]]): + with self.subTest(matrix=matrix), self.assertRaises(ValueError): + spectrum_from_matrix(matrix) + for atol in (-1, np.nan, np.inf): + for function, argument in ((spectrum_from_matrix, [[1]]), (renyi_entropy_from_spectrum, [1])): + with self.subTest(atol=atol, function=function), self.assertRaises(ValueError): + function(argument, atol=atol) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_validation.py b/tests/test_validation.py new file mode 100644 index 0000000..cc2eb07 --- /dev/null +++ b/tests/test_validation.py @@ -0,0 +1,172 @@ +"""Adversarial checks for scientific validators and evidence preservation.""" +from __future__ import annotations + +import contextlib +import csv +import io +import os +from pathlib import Path +import shutil +import subprocess +import sys +import tempfile +import unittest +from unittest import mock + +import numpy as np + +from scripts import final_scientific_validation as scientific +from scripts import validate_residue_results as residue + +ROOT = Path(__file__).resolve().parents[1] + + +class ValidationTests(unittest.TestCase): + def setUp(self) -> None: + self.temporary = tempfile.TemporaryDirectory() + self.addCleanup(self.temporary.cleanup) + self.root = Path(self.temporary.name) + + def copy_file(self, relative: str) -> Path: + path = self.root / relative + path.parent.mkdir(parents=True, exist_ok=True) + shutil.copyfile(ROOT / relative, path) + return path + + def table_fixture(self) -> Path: + path = self.copy_file("data/table_i_peaks.csv") + self.copy_file("outputs/fig2/fig2_table_with_page.csv") + self.copy_file("outputs/fig2/fig2_linear_fit_summary.csv") + return path + + def residue_fixture(self) -> None: + for name in ("residue_matched_summary.csv", "residue_matched_samples.csv", "residue_entropy_bounds.csv"): + self.copy_file("data/" + name) + + @staticmethod + def mutate(path: Path, mutation) -> None: + with path.open(newline="") as handle: + reader = csv.DictReader(handle) + fields, rows = reader.fieldnames, list(reader) + mutation(rows) + with path.open("w", newline="") as handle: + writer = csv.DictWriter(handle, fieldnames=fields) + writer.writeheader() + writer.writerows(rows) + + def test_retained_table_and_derived_csv_agree(self) -> None: + self.table_fixture() + result = scientific.table_fit_checks(self.root) + self.assertEqual(result["table_rows_checked"], 11) + + def test_changed_table_value_is_not_hidden_by_constants(self) -> None: + path = self.table_fixture() + self.mutate(path, lambda rows: rows[0].update(S_n="4.073")) + with self.assertRaisesRegex(residue.ValidationError, "CSV and production defaults"): + scientific.table_fit_checks(self.root) + + def test_changed_derived_fit_is_rejected(self) -> None: + self.table_fixture() + path = self.root / "outputs/fig2/fig2_linear_fit_summary.csv" + self.mutate(path, lambda rows: rows[0].update(slope="0.75")) + with self.assertRaisesRegex(residue.ValidationError, "stored log2_M_n fit slope"): + scientific.table_fit_checks(self.root) + + def test_zero_cannot_replace_tiny_fit_p_value(self) -> None: + self.table_fixture() + path = self.root / "outputs/fig2/fig2_linear_fit_summary.csv" + self.mutate(path, lambda rows: rows[0].update(p_value="0")) + with self.assertRaisesRegex(residue.ValidationError, "p_value"): + scientific.table_fit_checks(self.root) + + def test_exhaustive_check_calls_production_formula(self) -> None: + result = scientific.exhaustive_checks((2,)) + self.assertEqual(result["supports_enumerated"], 15) + with mock.patch.object(scientific.core, "fixed_cardinality_average_purity", return_value=0.5): + with self.assertRaisesRegex(residue.ValidationError, "production average purity"): + scientific.exhaustive_checks((2,)) + + def test_nonfinite_production_output_is_rejected(self) -> None: + with mock.patch.object(scientific.core, "entropy_from_support", return_value=float("nan")): + with self.assertRaisesRegex(residue.ValidationError, "non-finite"): + scientific.exhaustive_checks((2,)) + + def test_all_recorded_residue_statistics_and_seed_prefix(self) -> None: + self.residue_fixture() + result = residue.residue_checks(self.root, replay_samples=1) + self.assertEqual(result["raw_rows_checked"], 12000) + self.assertEqual(result["seed_replayed_supports"], 12) + + def test_nonfinite_raw_entropy_is_rejected(self) -> None: + self.residue_fixture() + path = self.root / "data/residue_matched_samples.csv" + self.mutate(path, lambda rows: rows[0].update(position_entropy="nan")) + with self.assertRaisesRegex(residue.ValidationError, "non-finite"): + residue.residue_checks(self.root, replay_samples=0) + + def test_duplicate_sample_cannot_silently_replace_a_missing_sample(self) -> None: + self.residue_fixture() + path = self.root / "data/residue_matched_samples.csv" + self.mutate(path, lambda rows: rows[1].update(sample=rows[0]["sample"])) + with self.assertRaisesRegex(residue.ValidationError, "duplicate or missing sample"): + residue.residue_checks(self.root, replay_samples=0) + + def test_missing_group_cannot_shrink_validation_scope(self) -> None: + self.residue_fixture() + path = self.root / "data/residue_matched_summary.csv" + self.mutate(path, lambda rows: rows.pop()) + with self.assertRaisesRegex(residue.ValidationError, "12 recorded groups"): + residue.residue_checks(self.root, replay_samples=0) + + def test_summary_quantile_is_reconstructed_from_raw_samples(self) -> None: + self.residue_fixture() + path = self.root / "data/residue_matched_summary.csv" + self.mutate(path, lambda rows: rows[0].update(null_position_q025="5.0")) + with self.assertRaisesRegex(residue.ValidationError, "null_position_q025"): + residue.residue_checks(self.root, replay_samples=0) + + def test_replay_uses_production_sampler(self) -> None: + self.residue_fixture() + with mock.patch.object(residue, "sample_matched_support", return_value=np.array([0])): + with self.assertRaisesRegex(residue.ValidationError, "sampler cardinality"): + residue.residue_checks(self.root, replay_samples=1) + + def test_optimized_cli_still_rejects_corrupt_data(self) -> None: + path = self.table_fixture() + self.mutate(path, lambda rows: rows[0].update(S_n="nan")) + code = ( + "from pathlib import Path; import sys; " + "from scripts import final_scientific_validation as m; " + "original=m.table_fit_checks; root=Path(sys.argv[1]); " + "m.table_fit_checks=lambda: original(root); sys.argv=['validator']; " + "raise SystemExit(m.main())" + ) + for flag in ("-O", "-OO"): + with self.subTest(flag=flag): + process = subprocess.run([sys.executable, flag, "-c", code, str(self.root)], + cwd=ROOT, env={**os.environ, "OPENBLAS_NUM_THREADS": "1", "OMP_NUM_THREADS": "1"}, + capture_output=True, text=True, check=False) + self.assertEqual(process.returncode, 1, process.stderr) + self.assertIn("Status: FAIL", process.stderr) + self.assertIn("non-finite", process.stderr) + + def test_report_defaults_to_stdout_and_never_overwrites(self) -> None: + report = "# Test report\n\nChecked.\n" + with contextlib.redirect_stdout(io.StringIO()) as output: + residue.write_report(report, None) + self.assertEqual(output.getvalue().strip(), report.strip()) + path = self.root / "report.md" + with contextlib.redirect_stdout(io.StringIO()): + residue.write_report(report, path) + self.assertEqual(path.read_text(), report) + with self.assertRaisesRegex(residue.ValidationError, "overwrite"): + residue.write_report("changed", path) + self.assertEqual(path.read_text(), report) + with self.assertRaisesRegex(residue.ValidationError, "Markdown"): + residue.write_report(report, self.root / "report.txt") + with self.assertRaisesRegex(residue.ValidationError, "evidence directories"): + residue.write_report(report, ROOT / "data/new-validation-report.md") + + +if __name__ == "__main__": + unittest.main() diff --git a/validation/FINAL_SCIENTIFIC_VALIDATION.txt b/validation/FINAL_SCIENTIFIC_VALIDATION.txt deleted file mode 100644 index a94227e..0000000 --- a/validation/FINAL_SCIENTIFIC_VALIDATION.txt +++ /dev/null @@ -1,35 +0,0 @@ -FINAL SCIENTIFIC VALIDATION -============================= - -exhaustive_n4 -------------- -supports_enumerated: 65535 -max_mean_rho_error: 2.067790383364354e-14 -max_average_purity_error: 8.881784197001252e-16 -max_residue_bound_violation_t1: 0 -max_residue_bound_violation_t2: -0 -max_entropy_routine_difference: 5.551115123125783e-16 - -table_fits ----------- -S_slope: 0.5093 -S_intercept: -0.9900909090909144 -log2M_slope: 0.7035409513877944 -log2M_intercept: -0.3577343681481748 -page_gap_n10: 0.207474201201169 -page_gap_n30: 0.01565248033929123 -support_fraction_n10: 0.1044921875 -support_fraction_n30: 0.00171054620295763 - -dense_ansatz ------------- -max_old_new_difference: 2.664535259100376e-15 - -residue_controls ----------------- -max_residue_claim_rounding_error: 3.951718681866057e-07 -prime_parity_ceiling_bits: 6.006491631063628 -prime_forced_gap_bits: 0.9935083689363715 -all_six_strongest_controls_below_all_1000_nulls: 1 - -STATUS: PASS diff --git a/validation/REFERENCE_FINAL_SCIENTIFIC_VALIDATION.txt b/validation/REFERENCE_FINAL_SCIENTIFIC_VALIDATION.txt deleted file mode 100644 index a94227e..0000000 --- a/validation/REFERENCE_FINAL_SCIENTIFIC_VALIDATION.txt +++ /dev/null @@ -1,35 +0,0 @@ -FINAL SCIENTIFIC VALIDATION -============================= - -exhaustive_n4 -------------- -supports_enumerated: 65535 -max_mean_rho_error: 2.067790383364354e-14 -max_average_purity_error: 8.881784197001252e-16 -max_residue_bound_violation_t1: 0 -max_residue_bound_violation_t2: -0 -max_entropy_routine_difference: 5.551115123125783e-16 - -table_fits ----------- -S_slope: 0.5093 -S_intercept: -0.9900909090909144 -log2M_slope: 0.7035409513877944 -log2M_intercept: -0.3577343681481748 -page_gap_n10: 0.207474201201169 -page_gap_n30: 0.01565248033929123 -support_fraction_n10: 0.1044921875 -support_fraction_n30: 0.00171054620295763 - -dense_ansatz ------------- -max_old_new_difference: 2.664535259100376e-15 - -residue_controls ----------------- -max_residue_claim_rounding_error: 3.951718681866057e-07 -prime_parity_ceiling_bits: 6.006491631063628 -prime_forced_gap_bits: 0.9935083689363715 -all_six_strongest_controls_below_all_1000_nulls: 1 - -STATUS: PASS diff --git a/validation/RELEASE_VALIDATION.txt b/validation/RELEASE_VALIDATION.txt deleted file mode 100644 index 13a1241..0000000 --- a/validation/RELEASE_VALIDATION.txt +++ /dev/null @@ -1,17 +0,0 @@ -PUBLICATION RELEASE VALIDATION -============================== - -Date: 2026-08-18 -Python: 3.13.5 - -Checks completed: -- source compilation: PASS -- unit and regression tests: PASS -- independent final scientific validation: PASS -- Figure 2 regeneration: PASS -- Figure 5 redraw from released data: PASS -- all seven final PDF figures present: PASS - -This package is an overlay release candidate. Existing historical output CSVs in -the GitHub repository should be preserved by uploading over the current branch -rather than deleting the repository contents first. diff --git a/validation/SCIENTIFIC_SANITY_CHECKS.md b/validation/SCIENTIFIC_SANITY_CHECKS.md new file mode 100644 index 0000000..126e86e --- /dev/null +++ b/validation/SCIENTIFIC_SANITY_CHECKS.md @@ -0,0 +1,49 @@ +# Scientific sanity checks + +**Status: PASS for the checks listed below.** + +Exhaustive checks cover every nonempty support at n=2 and n=4 against production routines. Table I checks read the actual retained CSV and its derived outputs; they do not rerun the peak searches or validate an asymptotic law. The residue checks reconstruct all 12,000-row statistics and replay three seeded supports per group (36 total). Formula agreement does not establish the dense-bulk ansatz's approximation accuracy. Novelty remains a separate question. + +## Retained Table I consistency + +| Check | Result | +| --- | ---: | +| table rows checked | 11 | +| log2 M n slope | 0.703540951388 | +| log2 M n intercept | -0.357734368148 | +| S n slope | 0.5093 | +| S n intercept | -0.990090909091 | +| max derived table or fit error | 3.5527136788e-15 | + +## Exhaustive small systems + +| Check | Result | +| --- | ---: | +| supports enumerated | 65550 | +| max coefficient matrix error | 0 | +| max entropy error | 1.7763568394e-15 | +| max residue ceiling error | 0 | +| max mean spectrum error | 2.06779038336e-14 | +| max average purity error | 1.11022302463e-15 | +| max diagonal entropy error | 2.68673971959e-14 | +| max entropy lower bound error | 2.44249065418e-15 | +| max qft amplitude error | 2.77555756156e-16 | +| max qft entropy error | 3.33066907388e-16 | +| max entropy bound violation | 3.20342650381e-16 | + +## Dense-bulk formula implementation + +| Check | Result | +| --- | ---: | +| parameter pairs checked | 16660 | +| max formula error | 2.6645352591e-15 | + +## Residue-control evidence + +| Check | Result | +| --- | ---: | +| groups checked | 12 | +| raw rows checked | 12000 | +| max summary error | 3.5527136788e-15 | +| seed replayed supports | 36 | +| max seed replay entropy error | 2.6645352591e-15 | diff --git a/validation/evidence_sha256.json b/validation/evidence_sha256.json new file mode 100644 index 0000000..be5c05c --- /dev/null +++ b/validation/evidence_sha256.json @@ -0,0 +1,57 @@ +{ + "algorithm": "sha256", + "baseline_commit": "4cb99f109194124f500d75dd065ff43581a2682d", + "files": { + "LICENSE": "a69dda1ec276c8e819c340aacbdf4eb8d66ffeffcbd023f2cf6bc0b5bfc71d64", + "data/fig5_almost_prime_unions.csv": "177e0d34dc25db06e37bb6bd0597be186f3a2c6fa06c49852bd3ed1249a73418", + "data/fig5_random_qft_summary.csv": "dc9fad6dd669ff309622966b63648fc909b8b2eec86cb81469ad8332c14b84ca", + "data/peak_verification_schedule.csv": "d527a7661297d573083879e73bd313e2bf5470ea21ec8515cf5611bde19831f4", + "data/residue_entropy_bounds.csv": "58732b3b16d2596d61f2c87ed4956badba36e5ac5b25ebcbc93c248cd7023e7b", + "data/residue_matched_samples.csv": "f6a12c9bed9bb2a03523ed9a8dc5f275122f86664b047ce27179d24d50cf5f31", + "data/residue_matched_summary.csv": "43e35efeb054e216928d319243c1107efe5707ca1e3e9c8780f4380593ebdee3", + "data/table_i_peaks.csv": "2412f162f2b6901202423f85ee777be95682b2cc43e311bb52a651df193aecae", + "outputs/fig1/fig1_concentration.png": "28b0af1283a69ec4aa2bac97ba5925376255f98fab463ce8599974be9967de59", + "outputs/fig1/fig1_raw.csv": "25c35027366bd376527fb12b365ad7f92211d606862d47b503337b6d8da63455", + "outputs/fig1/fig1_summary.csv": "e898e2f2db35a1dcdcde87b485b842cb0717d8f80b776e90b9ade9d19e4b1d44", + "outputs/fig1/fig1_zoom_raw.csv": "12d1b735b183a828df9dc21f80d3014e0509f2435d46a24a270cf6a792cd69aa", + "outputs/fig1/fig1_zoom_summary.csv": "d91c629336ba0011f77dbbd2fe621065a49bb4de78c29dd5e81b51ffa01fca1a", + "outputs/fig2/fig2_linear_fit_summary.csv": "9884ed618dbb393fa43aab7e8cf28fd0b24c4fb8fc18c1b1e34b2911fab4707f", + "outputs/fig2/fig2_peak_scaling.png": "ca22d149b1eb819cf1c18eb983167eaab572ee168820b5ee0b38e263ca622691", + "outputs/fig2/fig2_table_with_page.csv": "c2dcc97bd9ad5976e8a16c9fd103eaa99ba3b481abd1ea73a886bf89e4857f59", + "outputs/fig3/fig3_spectral_bulk.png": "58b84b12b53d3b7f96b183f3713b11d137d1cc5892f91be61be43958eacd542e", + "outputs/fig3/fig3_spectrum.csv": "cf8ace7de5818d52fea0d3c206eaf0c17963d939274b56c9e695a975cf3e8db8", + "outputs/fig3/fig3_spectrum_summary.csv": "bf3ed56ac1bdfeb6c6e40ed31478fa5f7d85f431b2061c6383f46f58336be7f2", + "outputs/fig4/fig4_approximation.png": "8fd16588599ec014fc53010945c1b56794bd022f6edbd6b829a5fbf8d4b51037", + "outputs/fig4/fig4_summary.csv": "bc400df3c29622bee9ee13da4c48898746c303d75c4e2e87fffe40fab7f65935", + "outputs/fig4/fig4_zoom_summary.csv": "de98ae3f86dd5a37d397031e6f19451b12924e73d6a9b5d5bbff832eecb1fd11", + "outputs/fig5/fig5_almost_prime_unions.csv": "f3abe84233aa7d8453503e0be79fb6198de457da97f556dc163ed78847faa937", + "outputs/fig5/fig5_qft_residue_controls.png": "e9ec19a64d04a914ff771f90e61a35a942adcb761cf12723548c9060029c6f7a", + "outputs/fig5/fig5_random_qft_summary.csv": "c08ac235b3e9472aceaef3c636d50d4ad1c4db7f246797e043e9e3b1a4d86a70", + "outputs/fig6/fig6_renyi.png": "33ee3b47ad429647999c7ee3bbf2ca5f36d30c94cf5fd697c29b5ffb0a213a4b", + "outputs/fig6/fig6_renyi_summary.csv": "7f8e64c047c5427c659dfff73fa219927d7478468dd925aac72cf90a3f9f60e0", + "outputs/fig6/fig6_renyi_zoom_summary.csv": "540ac3a7b46293ad7103d3c79d694c4a83ec206cd2ff3e551d270f7e16ecf90f", + "outputs/fig7/fig7_partition_samples.csv": "6ce8e29a5cb4d0139d2e9e1200688fd9be186a92de9e4e9da08faaf76e73f9a7", + "outputs/fig7/fig7_partition_summary.csv": "5afc21d65aeaa0ae2eb8410d445e4960bede0f184ae3d90a52ee874a7e7dd425", + "outputs/fig7/fig7_partitions.png": "3e01bb55f826dd5b059354d63962689f8a48c42456a9cccac8cd66ff13830b6e", + "outputs/peak_verification/peak_verification_n10.png": "35d856494c607b153dee58c0570a0a9360784727f6319d8dbd001bc4f91a9a81", + "outputs/peak_verification/peak_verification_n10_fit.csv": "5b76e14c4a7e0731ac4ae22f79ef2a7fed96f0837fed45e479ec9661cba1c292", + "outputs/peak_verification/peak_verification_n10_samples.csv": "e9526315250aae7327ac4624929add981ddffc57ef809c4a7effaafe876498a4", + "outputs/peak_verification/peak_verification_n12.png": "4903c423ee0055169cc46daff9e5a76b00a65a664491ae476b1a19236ff5c295", + "outputs/peak_verification/peak_verification_n12_fit.csv": "48fb372e58be43cc916885343c7ea2924ad23a5d10dbd827dd00271becd7def0", + "outputs/peak_verification/peak_verification_n12_samples.csv": "cc4584c5cf0f8d925098f04d9c8324d364332dd1f1b1a9c5f53167f477b54a1f", + "outputs/peak_verification/peak_verification_n14.png": "ea2b6feb1e100dca65e1003ef6d8c55cf86186d00837f3483509da596d3f7b89", + "outputs/peak_verification/peak_verification_n14_fit.csv": "a8f1d30d5b62bed9cedb777950e59fc7d87d350ad96e9eca55864fbf460a58ec", + "outputs/peak_verification/peak_verification_n14_samples.csv": "112cd6bbf8253052cc2ca7b748007fa7fd5a7a84cc88e6ff038f325f84226cc1", + "outputs/peak_verification/peak_verification_n16.png": "a4941f80388b274ffc30d3f1198f1706b20491f40873c4498776b6d9a9c48933", + "outputs/peak_verification/peak_verification_n16_fit.csv": "cb9686603168d755e72b940c5fc153b6e56929bba0053edf03e112cc39974d77", + "outputs/peak_verification/peak_verification_n16_samples.csv": "4e9bf5383a0cb7e5ee778304460c07f9af59640d1b6a46b0491de1aec20ae78c", + "outputs/peak_verification/peak_verification_n18.png": "47d2e59c22d11d3e250909ff1d2d7b162e03073b30ed1c4eb56e3daa1e91326c", + "outputs/peak_verification/peak_verification_n18_fit.csv": "7f576cb540caf8cd7953d0db4c02b6e68715d35c60589922011223d4708c7c9a", + "outputs/peak_verification/peak_verification_n18_samples.csv": "b149f773f7ad582e886bc44566b2d406385068a1363899d52183406e3724d9a1", + "outputs/peak_verification/peak_verification_n20.png": "1192700001850167069c8ef752dcc1a26b45a3b28cc331b2d9376283fca12779", + "outputs/peak_verification/peak_verification_n20_fit.csv": "6ff25aa3fb955e39ae8f1a5be04f1f3b516839ddfbb65e68b8177dd1a8902fe0", + "outputs/peak_verification/peak_verification_n20_samples.csv": "81bfc8a7ff9496b566ac5eaa5f444aced13c11d2c5e751b99c21d275566bac80", + "outputs/peak_verification/peak_verification_summary.csv": "176b037441c910c6da0bc2edfd37a2f8302afc308cc82bfd6e56baf1368175b2", + "outputs/peak_verification/peak_verification_summary.png": "91093b361683327f39dbd9bfa0f981dc1e52d51b5684d78f2756ca5062f3e16e" + } +}