[{"data":1,"prerenderedAt":4},["ShallowReactive",2],{"KF3klWUHN4":3},"# Certifying the Riemann hypothesis up to bounded height in Lean\n\nThis repository proves in Lean's kernel that all the nontrivial zeros of the \nRiemann zeta function up to a specified height $T$ lie on the critical line, \nas predicted by the Riemann hypothesis. \nIntervals are certified on the critical line, proving the location of \nnontrivial zeros with configurable precision. \nThe main theorem is proved fully in the kernel, depending \nonly on the standard axiom set `[propext, Classical.choice, Quot.sound]`.\nThe kernel is asked only to check: both the zero search and the numerical \nevaluation inside the proofs are done outside it (see [Architecture](#architecture)).\nOur current record is $T = 100,000,300$: the $248,008,818$ zeros with imaginary part between $0$ and\nthe Gram point $g \\approx 100,000,300.797$ just above $T$ all lie on the critical line.\n\n\n---\n\n## Table of contents\n\n- [Main theorem](#main-theorem)\n- [Frontier](#frontier)\n- [Installation](#installation)\n- [Build](#build)\n- [Comparator](#comparator)\n- [Architecture](#architecture)\n- [Trust model](#trust-model)\n- [Scaling](#scaling)\n- [Lean libraries](#lean-libraries)\n- [Authors, license and citation](#authors-license-and-citation)\n\n---\n\n## Main theorem \n\nThe main theorem is a term of type\n```lean\ndef RiemannHypothesisUpTo (T : ℝ) : Prop :=\n  ∀ s, riemannZeta s = 0 → (¬∃ n : ℕ, s = -2 * (n + 1)) → s ≠ 1 → |s.im| \u003C T → s.re = 1/2\n```\nwhich is the statement that all the nontrivial zeros of the Riemann zeta \nfunction with imaginary part in `(-T, T)` (equivalently `(0, T)`) lie \non the critical line. It is proved as `Zeta.riemannHypothesisUpTo_\u003CT>`, and as\n`Native.riemannHypothesisUpTo_\u003CT>` at the Native trust layer (see [Trust model](#trust-model)).\n\n--- \n\n## Frontier\n\nHeights reached at each trust layer; see [Architecture](#architecture) for the role that Kernel, Native, and Python play, and [Trust model](#trust-model) for what each one trusts.\n\n| Trust layer | Height $T$ | Zeros\u003Csup>[1](#note-zeros)\u003C/sup> | Hardware | Wall time\u003Csup>[2](#note-wall)\u003C/sup> | CPU core-hours\u003Csup>[3](#note-cpu)\u003C/sup> | CPU-ms / zero |\n|---|---:|---:|---|---:|---:|---:|\n| **Kernel** | [10,011](https://github.com/smmercuri/zeta/tree/v1-1e4) | 10,156 | laptop\u003Csup>[4](#note-laptop)\u003C/sup> | 1 min 29 s | 0.13 | 46 |\n| | [100,013](https://github.com/smmercuri/zeta/tree/v1-1e5) | 138,090 | laptop | 18 min 25 s | 2.5 | 65 |\n| | [1,000,449](https://github.com/smmercuri/zeta/tree/v1-1e6) | 1,748,002 | server\u003Csup>[5](#note-server)\u003C/sup> | 1 h 40 min | 79 | 164 |\n| | [10,001,072](https://github.com/smmercuri/zeta/tree/v1-1e7) | 21,138,563 | server | 133 h 58 min\u003Csup>[7](#note-previous)\u003C/sup> | 6,417 | 1,093 |\n| | [100,000,300](https://github.com/smmercuri/zeta/tree/v1-1e8) | 248,008,818 | HPC\u003Csup>[6](#note-hpc)\u003C/sup> | 49 h 55 min\u003Csup>[7](#note-previous)\u003C/sup> | 244,190 | 3,545 |\n| **Native** | [10,011](https://github.com/smmercuri/zeta/tree/v1-1e4) | 10,156 | laptop | 26 s | 0.012 | 4.3 |\n| | [100,013](https://github.com/smmercuri/zeta/tree/v1-1e5) | 138,090 | laptop | 1 min 9 s | 0.11 | 2.9 |\n| | [1,000,449](https://github.com/smmercuri/zeta/tree/v1-1e6) | 1,748,002 | laptop | 14 min | 1.9 | 3.9 |\n| | [10,001,072](https://github.com/smmercuri/zeta/tree/v1-1e7) | 21,138,563 | laptop | 5 h 20 min\u003Csup>[7](#note-previous)\u003C/sup> | 46.7 | 8.0 |\n| | [100,000,300](https://github.com/smmercuri/zeta/tree/v1-1e8) | 248,008,818 | server | 85 h 20 min\u003Csup>[7](#note-previous)\u003C/sup> | 4,018 | 58 |\n| **Python** | [10,011](https://github.com/smmercuri/zeta/tree/v1-1e4) | 10,156 | laptop | 39 s | 0.027 | 9.7 |\n| | [100,013](https://github.com/smmercuri/zeta/tree/v1-1e5) | 138,090 | laptop | 1 min 40 s | 0.19 | 4.9 |\n| | [1,000,449](https://github.com/smmercuri/zeta/tree/v1-1e6) | 1,748,002 | laptop | 17 min | 2.6 | 5.3 |\n| | [10,001,072](https://github.com/smmercuri/zeta/tree/v1-1e7) | 21,138,563 | laptop | 5 h 48 min\u003Csup>[7](#note-previous)\u003C/sup> | 53 | 9.0 |\n| | [100,000,300](https://github.com/smmercuri/zeta/tree/v1-1e8) | 248,008,818 | server | 40 h 28 min\u003Csup>[7](#note-previous)\u003C/sup> | 1,927 | 28 |\n\n1. \u003Ca id=\"note-zeros\">\u003C/a>**Zeros.** The number of zeros certified on the critical line, namely those with positive imaginary part below the first Gram point above `T`. Since `T` is that Gram point rounded down, this can slightly exceed the number in `(0, T)`.\n2. \u003Ca id=\"note-wall\">\u003C/a>**Wall time** represents the time to build the certificates, with Mathlib \n   and the rest of the libraries built beforehand, using all available threads or as many workers as \n   `build.sh` fits in memory. At $10^8$, where the build was split across many nodes over several \n   campaigns, it is the total time with at least one node building. For Native, it is an endpoint\n   build (`scripts/build.sh --native --endpoint`), which skips the prediction of the kernel run. For Python, it is the time for the zero search.\n   Each height links to the tagged commit its timings were measured at.\n3. \u003Ca id=\"note-cpu\">\u003C/a>**CPU core-hours** represents the total user and system CPU time for the build described in \n   the [wall time note](#note-wall). \n   CPU-ms/zero divides by the number of zeros.\n4. \u003Ca id=\"note-laptop\">\u003C/a>**Laptop.** A MacBook Pro with an Apple M4 (10 cores: 4 performance,\n   6 efficiency) and 16 GB of memory.\n5. \u003Ca id=\"note-server\">\u003C/a>**Server.** A dedicated server with 24 cores, 48 threads and 256 GB RAM.\n6. \u003Ca id=\"note-hpc\">\u003C/a>**HPC.** EPCC's [Cirrus](https://www.cirrus.ac.uk/) supercomputer, whose\n   nodes each have 288 cores, 576 threads and 745 GB RAM. This project had a budget of 128\n   concurrent nodes, of which up to 57 were used concurrently.\n7. \u003Ca id=\"note-previous\">\u003C/a>**Previous version.** All timings at $10^7$ and $10^8$ (kernel, native and\n   Python) were measured with an earlier version of the project. They are not directly comparable \n   with the kernel timings at lower heights, which were measured with the current version and are \n   substantially faster (at $10^6$, 1 h 40 min against 3 h 28 min previously).\n\nEach of the heights in the above table is available through branches, tags or GitHub \nreleases on the remote repository. \n\n| $T$ | Branch | Tag | Release |\n|---:|---|---|---|\n| 10,011 | `main` | `v1-1e4` | — |\n| 100,013 | `1e5` | `v1-1e5` | — |\n| 1,000,449 | `1e6` | `v1-1e6` | [`1e6`](https://github.com/smmercuri/zeta/releases/tag/v1-1e6) |\n| 1,895,609 | `1.8956e6` | `v1-1.8956e6` | [`1.8956e6`](https://github.com/smmercuri/zeta/releases/tag/v1-1.8956e6) |\n| 10,001,072 | — | `v1-1e7` | [`1e7`](https://github.com/smmercuri/zeta/releases/tag/v1-1e7) |\n| 100,000,300 | — | `v1-1e8` | [`1e8`](https://github.com/smmercuri/zeta/releases/tag/v1-1e8) |\n\nThe branches provide complete repositories which can be rebuilt as they are.\nThe larger heights $10^7$ and $10^8$ are released using a GitHub tag, which is the commit \nthese results correspond to, without the zero data or generated Lean certificates due to \nGitHub storage limitations.\nAt these tags, `Zeta/Main.lean` therefore still states the $10^4$ theorem and certificates.\nTo obtain the main theorem at the tag's height, download the zeros from Zenodo\n([1e7](https://zenodo.org/records/24090490) and [1e8](https://zenodo.org/records/24085183), which also hold the\nbuild logs and archives), unpack them into the repository, and run `./gen_certificates.sh` (in the repository root\nat these tags). Each Zenodo record's README gives the exact commands.\nTheir comparator targets are also on Zenodo, with instructions in each record's `COMPARATOR.md`. Use those\nrather than the targets committed at these two tags, which are for $10^4$, and avoid `gen_certificates.sh --comparator`,\nwhich is impractical at this scale.\n\n---\n\n## Installation\n\nInstall Lean through [elan](https://github.com/leanprover/elan); the toolchain is pinned in\n`lean-toolchain`. Next fetch the dependencies and Mathlib's prebuilt cache.\n```bash\ngit clone https://github.com/smmercuri/zeta.git && cd zeta\nlake exe cache get\n```\nThe first build also compiles the `PrimeNumberTheoremAnd` dependency. The Python proposer is\nonly needed to regenerate the certificate data: see [`python/README.md`](python/README.md#installation).\n\n---\n\n## Build\n\nThis assumes the zero stores and Lean certificate files are already generated, as they are on each\nbranch.\n\n```bash\nscripts/build.sh                     # the headline theorem Zeta.riemannHypothesisUpTo_\u003CT>\nscripts/build.sh --native            # its native counterpart Native.riemannHypothesisUpTo_\u003CT>\nscripts/build.sh --native --endpoint # faster than --native alone, skips kernel-run prechecks\nscripts/build.sh --proofs=archive    # losslessly compress each batch's proof term once built\nscripts/build.sh --proofs=discard    # drop each batch's proof term once built\nscripts/build.sh --proofs=auto       # keep proof terms if they fit easily, else archive, else discard\nscripts/build.sh --estimate          # build two sample batches, then project time, memory and disk\nscripts/build.sh --retries 5         # the kernel raises a failing interval's precision up to 5 times (default 3)\n```\n`build.sh` wraps `lake build`: it sizes the number of parallel workers to the free memory\n(override with `LEAN_NUM_THREADS`) and warns when the proof terms would fill the disk (see\n[Scaling](#scaling)). `Zeta.Main` checks the headline theorem's axioms with `#guard_msgs`, so the\nbuild fails if they change.\n\nTo try a new height, run the entire pipeline in the `zeta` conda environment:\n\n```bash\nscripts/pipeline.sh \u003CT> --build\n```\n\nThis finds the zeros, generates the Lean certificates and builds them, choosing how to store proof\nterms to fit the disk. It takes all the parameters that `build.sh` takes as well as the Python \nscripts `store.py` for generating zeros. Leave out `--build` to stop once the certificates are generated. The\nindividual steps and the `zeta` environment are described in [`python/README.md`](python/README.md#usage).\n\nTo depend on a tagged release, point your `require` at its tag, and Lake will download its prebuilt\narchive if the release has one. Your project must use the same Lean toolchain and Mathlib version\nas that release, otherwise Lake will rebuild every certificate from source. Files that import the\ncertificates must also start with `module`, because the archive has placeholders in place of the\nproof terms.\n\n---\n\n## Comparator\n\n[`leanprover/comparator`](https://github.com/leanprover/comparator) checks independently that the\nproof establishes exactly the statement in a separate challenge file, using only the standard\naxioms, by re-checking the whole dependency tree in a fresh kernel. `comparator/` sets this up\nsharded (one challenge per batch, plus a reduction), which is the main method, and globally (one\nchallenge for the main theorem). See [`comparator/README.md`](comparator/README.md).\n\nTo run it, build the tools at tag `v4.35.0` in `.comparator-tools/` (gitignored), using this\nproject's Lean version, from the repository root:\n```bash\ngit clone --branch v4.35.0 https://github.com/leanprover/comparator.git .comparator-tools\ncd .comparator-tools\necho 'leanprover/lean4:v4.35.0' > lean-toolchain\nlake build lean4export comparator\n```\nThe tools must match the Lean version of the checkout (`lean-toolchain`). The `v1-1e7` and `v1-1e8`\ntags use Lean `v4.33.1`, so for them use tag `v4.33.0` and `leanprover/lean4:v4.33.1`.\nThen verify the main theorem's statement in shards:\n```bash\ncomparator/sharded_run.sh [--size 10] [--jobs 1]\n```\nThis checks the reduction and every batch, and gives one verdict at the end. It does not check the\nformal theorem `Zeta.riemannHypothesisUpTo_\u003CT>` itself; see\n[`comparator/README.md`](comparator/README.md#running) for what it establishes \nand the warning below for why this is the default comparator setup.\n\nComparator can also be run on a single target with `comparator/run.sh \u003Cconfig.json>`, on chosen\nbatches with `comparator/shard.sh RiemannSiegel/Batch0001 RiemannSiegel/Batch0002`, or on a\nreproducible random sample with `comparator/sharded_run.sh --sample 300 --seed \u003Cpublic value>`.\n\n> **Warning.** The global check (`comparator/run.sh --force`) of `Zeta.riemannHypothesisUpTo_\u003CT>` is currently impractical beyond\n> `1e4`. This is because our proofs contain many large dyadic literals \n> which are multiples of $2^{64}$ and Lean hashes `Nat` literals modulo $2^{64}$ ([lean4#15686](https://github.com/leanprover/lean4/issues/15686)).\n\n---\n\n## Architecture \n\nThe core design principle is to keep as much of the computation outside the \nkernel as possible, since the kernel is slow. \nThere are two components which work in a proposer-certifier architecture.\nThe proposer is a Python library which searches for zeta zeros and Gram points \nand stores them as data.\nThe certifier consists of a metaprogram which evaluates numerical quantities in \ncompiled code and emits a proof certifying the numerical Python artifacts. \nThe kernel only needs to check the side conditions within these proofs, \nwhich are comparisons of natural-number literals; the kernel is able to \ndo this quickly with GMP arithmetic.\nFurther details on the Python code can be found in [`python/README.md`](python/README.md).\n\nThe following zero interval certificate for the Riemann-Siegel\napproximation is an example of what the metaprogram proves:\n```lean\nstructure ZIntervalRS.Certificate (I : ZIntervalRS) where\n  le_fst : 32 * Real.pi ≤ I.fst\n  is_sign_change : I.fstVal * I.sndVal \u003C 0\n  fst_error : ‖I.fstVal‖ > hardyZErrorBoundRS I.fst\n  snd_error : ‖I.sndVal‖ > hardyZErrorBoundRS I.snd\n```\nThe metaprogram must prove that `t` is large enough to apply Riemann-Siegel \n(`le_fst`), that the approximant exhibits a sign change at the endpoints \n(`is_sign_change`), and that the left/right endpoint approximate values \nare greater in absolute value than the error bound (`fst_error`/`snd_error`) \nso that this also represents a sign change of the true `Z`-function.\nThere are similar certificates for proving that an interval contains a\nGram point. All of these proofs are assembled, the zeros are counted \nand Turing's method is applied to prove that the Riemann hypothesis holds \nup to some height $T$. This is proved fully in the kernel, depending \nonly on the standard axiom set `[propext, Classical.choice, Quot.sound]`.\n\nBecause the kernel proof is computationally expensive, this architecture \nbecomes a proposer-checker-certifier model in high-compute scenarios \n(for example, beyond `T = 1e6`).\nThe proposer runs as usual; however prior to the kernel build we run a \nnative sibling of the metaprogram within `Native`. \nThis allows `native_decide`, thus making the metaprogram significantly faster \nat the cost of trusting the compiler and expanding the axiom set.\nThis run also flags cases where the current configuration will fail, for example \nif the number of decimal places in the store does not separate close zeros.\nIn this way, a multi-CPU-day kernel run can be preconfigured with higher confidence of success.\n\n\nIn cases where high compute is not available, proposer-checker represents \na performant but lower-trust architecture.\n\n```mermaid\nflowchart LR\n    subgraph P[\"Proposer\"]\n        S[\"zero search\u003Cbr/>Euler–Maclaurin, Riemann–Siegel\"]\n        G[\"Gram-point search\"]\n    end\n    D[(\"store\u003Cbr/>zero brackets, Gram points\")]\n    subgraph C[\"Checker\"]\n        N[\"native metaprogram\u003Cbr/>build_batch_rs +native\u003Cbr/>(native_decide)\"]\n    end\n    subgraph K[\"Certifier\"]\n        M[\"metaprogram (compiled)\u003Cbr/>build_batch, build_batch_rs\u003Cbr/>computes values, emits proof terms\"]\n        KC[\"kernel\u003Cbr/>checks the proof terms:\u003Cbr/>literal comparisons\"]\n        A[\"assemble, count zeros,\u003Cbr/>Turing's method\"]\n    end\n    S --> D\n    G --> D\n    D --> N\n    D --> M\n    N -. \"informs\u003Cbr/>the kernel run\" .-> M\n    M --> KC\n    KC --> A\n    A --> RH[\"RiemannHypothesisUpTo T\u003Cbr/>[propext, Classical.choice, Quot.sound]\"]\n    N --> RHN[\"RiemannHypothesisUpTo T\u003Cbr/>+ one native_decide axiom per batch\"]\n\n    classDef proposer fill:#dbeafe,stroke:#2563eb,stroke-width:1.5px,color:#1e293b\n    classDef store fill:#f1f5f9,stroke:#64748b,stroke-width:1.5px,color:#1e293b\n    classDef checker fill:#fef3c7,stroke:#d97706,stroke-width:1.5px,color:#1e293b\n    classDef certifier fill:#dcfce7,stroke:#16a34a,stroke-width:1.5px,color:#1e293b\n    classDef kernelResult fill:#15803d,stroke:#14532d,stroke-width:2px,color:#ffffff,font-weight:bold\n    classDef nativeResult fill:#f59e0b,stroke:#b45309,stroke-width:2px,color:#1e293b,font-weight:bold\n    class S,G proposer\n    class D store\n    class N checker\n    class M,KC,A certifier\n    class RH kernelResult\n    class RHN nativeResult\n    style P fill:#eff6ff,stroke:#93c5fd,color:#1e3a8a\n    style C fill:#fffbeb,stroke:#fcd34d,color:#78350f\n    style K fill:#f0fdf4,stroke:#86efac,color:#14532d\n    linkStyle default stroke:#64748b,stroke-width:1.5px\n    linkStyle 4 stroke:#d97706,stroke-width:2px\n```\n\n--- \n\n## Trust model\n\nEach layer of the [architecture](#architecture) corresponds to a different \ndegree of trust. \n\n| Layer | Output | Trusted components |\n|---|---|---|\n| **Python** (`python/`) | numerical evidence (zero intervals and Gram blocks) | none |\n| **Kernel** (`Zeta/`) | `Zeta.riemannHypothesisUpTo_\u003CT>` | the Lean kernel and the axioms `propext`, `Classical.choice`, `Quot.sound` |\n| **Native** (`Native/`) | `Native.riemannHypothesisUpTo_\u003CT>` | the above, plus the Lean compiler and runtime (one `native_decide` axiom per batch) |\n\n- The Python proposer is untrusted. It tracks analytic truncation bounds throughout, but not\n  floating-point roundoff, and serves only as a provider of numerical evidence.\n  The Lean metaprogram itself is similarly untrusted, as it only proposes proofs.\n- The native checker is proved sound in Lean. Additional trust is required that its compiled\n  code runs correctly. It is used only for the Riemann-Siegel batches, which make up the vast\n  majority of batches; the small number of Euler-Maclaurin and Gram point certificates are always\n  kernel-checked.\n- At large heights proof terms are too large to keep (see [Scaling](#scaling)). \n  The kernel checks every batch during the build, after which its proof term in the private \n  `olean` is either compressed losslessly or replaced by a placeholder with its SHA-256 recorded.\n  Where proofs are discarded, the proofs cannot be inspected afterwards without re-running\n  the build. Trust at these heights depends additionally on our published build logs, and \n  our provided comparator configurations allow users to sample and verify individual batches.\n  We have run a successful comparator check on height $10^4$, and sharded\n  comparator runs on heights $10^5$ and $10^6$.\n\n--- \n\n## Scaling\n\nScaling the project to work for large heights presents a number of \ncomputational challenges.\nA certificate for each zero must be proved.\nUp to the Gram point just above 100,000,300 there are 248,008,818 zeros and so writing an \nefficient kernel-checked numerical proof and organising how the collection of\nall these proofs should be structured are challenges.\nAt $10^8$ (tag [`v1-1e8`](https://github.com/smmercuri/zeta/tree/v1-1e8), commit `3dab6480a8`), \nthe `Zeta` library had 6,013,447 modules and 240,354,586 lines of Lean.\n\n### Kernel \n\nFollowing the design principle in [Architecture](#architecture), the kernel \nonly confirms results the metaprogram has already computed. For instance:\n- The metaprogram computes numerical values in advance, and the kernel needs only \n  to compare numbers and confirm the results, mostly via natural-number literals.\n- Sealing is used throughout. Sub-proofs are emitted as separate auxiliary lemmas, \n  checked once by the kernel, and can then be cited throughout numerical \n  calculations by name. This is used for:\n    - shared constants in a module ($\\log n$, $1/\\sqrt{n}$, $\\pi$);\n    - interval endpoint values of the approximation, which are used in both sign-change and error bound checks;\n    - running partial sums defining approximations, which are saved partway to keep proof depth independent of height.\n- Cheap expressions are used throughout, for example using higher-order correction terms only \n  when they are needed.\n- Lemma applications are reduced within Riemann-Siegel approximation calculations in the \n  metaprogram by advancing the sum two terms at a time with a single lemma `mainSumRS_step2`, \n  which would otherwise involve 18 lemma applications per term.\n\n### Zero precision \n\nBy default, the widths of the intervals containing zeros in the Python module are maximised, \nso that they form a tiling of the search range on the critical line.\nThis reduces the precision of each zero's location, but it enables lower working \nprecision in Lean's kernel and speeds up computation. \nHowever, the Python module can be configured to locate zeros more precisely, by \ndisabling interval maximisation and using bisection search. \nSee the [Python README](python/README.md) for details. \nWe provide a branch `1e4-prec` with a working example of higher zero precision.\nThe build time for this configuration up to height $10^4$ is longer at $0.33$ CPU hours \n(vs. $0.13$ CPU hours with widened intervals).\n\n### Batching \n\nZeros are collected into batches and the metaprogram proves certificates for \nall zeros in a batch.\nBatch size is chosen to keep worker RAM at or under around $3.35$ GB, which is the \navailable RAM per worker on the HPC node described above.\nUp to height about `2e6` batch sizes of $200$ are used, which were found empirically \nto optimise build time.\nIn general, the rule $\\min(200, \\lfloor 2.75 / (2.4 \\cdot 10^{-5} \\nu) \\rfloor)$, where \n$\\nu = \\lfloor \\sqrt{t / 2\\pi} \\rfloor$, is used to define batch sizes which result in smaller \nbatch sizes as the height increases.\nThe rule comes from an empirical measurement showing that the memory grows by around \n$2.4 \\cdot 10^{-5}GB$ per row per unit of $\\nu$, combined with a measurement of $0.6$ GB fixed \noverhead for each worker.\nThese figures can be configured through global constants in `python/src/config.py` to suit \ndifferent machine specs.\nSubdirectories with potentially millions of files are avoided by placing batches \ninto subdirectories of $10,000$ batch files.\n\nThe _public_ interface of each batch module is `BatchSpec`, which is just the \nexistential statement that there is a list of certified zeros in a given range. \nEverything else is private.\nBatch specs are assembled through a tree of groups: each level-`1` group \nassembles at most `125` batches, each level-`i + 1` group at most `125` \nlevel-`i` groups, and levels are added until at most `125` groups remain \nfor the final assembler.\nThis bounds every assembling module at `125` imports.\nIn practice, only a single level is used up to height $10^6$. \nBeyond this, $10^7$ uses two levels and $10^8$ uses three.\n\n\n```\n                                    Zeta.Main\n                             riemannHypothesisUpTo_T\n                                        ▲\n                                    assembler\n                       one BatchSpec for all zeros below T\n                                        ▲\n                ┌───────────────────────┼───────────────────────┐\n         level-L group 1         level-L group 2    …    level-L group m\n                ▲                                           (m ≤ 125)\n                ⋮\n                ▲\n         level-2 group 1\n      level-1 groups 1–125\n                ▲\n          ┌─────┴───────────────┬─────────────────────────┐\n   level-1 group 1       level-1 group 2     …    level-1 group 125\n    batches 1–125        batches 126–250        batches 15,501–15,625\n          ▲\n     ┌────┴─────────┬─────────────────┐\n  batch 1        batch 2     …    batch 125\nzeros 1–52    zeros 53–252   zeros 24,653–24,852\n```\n\n### Storage \n\nA single proof term for Riemann-Siegel certificates grows with the height $t$ \n(on the order of $\\sqrt{t}$). These are kept in the private `olean` files for \neach batch. At height $10^5$ these take up about 18 GB, and at $10^6$ about 500 GB. \nTo resolve this, we include compression and \ndiscarding options in the build script. Downstream modules read only the public\noleans, so a batch's private olean can be compressed on the fly once it is built. Compression reduces storage by around $3\\times$ \nand is lossless so enables easy restoration; discarding involves overwriting \nthe private oleans with a placeholder, which effectively reduces their storage \nto zero but they can only be restored by rebuilding.\n\n### Native prechecking and working precision\n\nThe native module can be used as a test prior to a more expensive kernel run \nat a given height. Running `scripts/build.sh --native` will perform this.\nThis is particularly useful for catching failing zero checks that occur due to \nworking precision selection.\nPrecision selection is adaptive in the metaprogram, meaning that if \nthere is a failure then working precision is raised ($100$-fold each time) until \nthe check passes or it has been raised $3$ times (by default).\nRunning the native module will alert you to whether the kernel will fail at the \ncap value supplied via `--retries` (default `3`); this is a prediction via a model of the kernel's enclosure widths.\nThis can then be set via CLI in the full build using the `--retries` flag, \ne.g. `scripts/build.sh --retries 5`.\nThe prediction costs about one extra evaluation of each endpoint; when the native theorem is \nthe deliverable, `scripts/build.sh --native --endpoint` skips it.\n\n### Building\n\nWhen running a full build on the kernel, the provided build script `scripts/build.sh` \nuses as many workers as fit in memory, up to the number of cores.\nIt uses more workers at lower heights, where batches need less memory.\nSince batches are sized to a memory of $3.35$ GB per worker, this will \nlimit machines which have less than this.\nThis can be overridden by setting `LEAN_NUM_THREADS` as usual.\nThe script also provides an `--estimate` flag which will test two batches \nand provide an estimate for the resultant build time; it will not perform\nthe full build with this flag.\nOn a laptop or a server, for heights `1e4` through `1e7`, the build script \nis used in a single command, running a build over all zero certificates in the \nrange `(0, T)`.\n\nOn the Cirrus supercomputer, in order to leverage concurrency across the \nbudgeted 128 nodes, the height range `(0, 1e8)` was split up into 175 \nindependent ranges, each of which was built on a separate node. \nThe number of nodes was increased gradually, to a peak of 57, until \nall ranges were built. \nAfter this, build artifacts were archived.\nA final assembly job unpacked all the range archives into a single node with \n$1.5$ TB RAM to run the final build.\n\n\n--- \n\n## Lean libraries\n\n- `Zeta` consists of the formalized theory required to apply Turing's method and \n  certify approximant values. \n  - Complex analysis (argument principle, Hadamard factorisation, Euler-Maclaurin).\n  - Analytic number theory (Riemann-von Mangoldt formula).\n  - Riemann-Siegel approximation of Hardy's `Z`-function.\n  - Certificates for zero and Gram point intervals.\n  - The main theorem.\n- `Enclosure` contains kernel-checked interval and ball arithmetic, with binary-splitting bounds on\n  elementary functions (`exp`, `log`, `sin`, `cos`, `arctan`, `π`, `√`) exposed as tactics.\n- `Bernoulli` develops Bernoulli and tangent numbers for the Euler–Maclaurin correction terms, and a\n  Kummer regular-prime sweep. \n- `Native` is the native checker for the Riemann–Siegel certificates. Its checking functions\n  (`Native.Eval`, built separately as `NativeEval` without Mathlib) are compiled to machine code,\n  proved sound in Lean, and run through `native_decide`.\n- [`PrimeNumberTheoremAnd`](https://github.com/AlexKontorovich/PrimeNumberTheoremAnd) (external,\n  a pinned fork) is used for the following:\n  - bounds on ζ;\n  - residue calculus on rectangles;\n  - Borel–Carathéodory and zero-counting results used by the analytic part of `Zeta`.\n\n--- \n\n## Authors, license and citation\n\nSalvatore Mercuri and Bhavik Mehta. Released under the Apache License 2.0 (see [`LICENSE`](LICENSE)).\n\nThis work used the [Cirrus](https://www.cirrus.ac.uk) NCR HPC Service at EPCC, funded by the University of\nEdinburgh, the Edinburgh and South East Scotland City Region Deal, and UKRI.\n\nTo cite this work:\n```bibtex\n@software{zeta,\n  author = {Mercuri, Salvatore and Mehta, Bhavik},\n  title  = {Certifying the {R}iemann hypothesis up to bounded height in Lean},\n  year   = {2026},\n  url    = {https://github.com/smmercuri/zeta}\n}\n```\n",1794686190086]