diff --git a/.github/workflows/benchmarks.yml b/.github/workflows/benchmarks.yml new file mode 100644 index 00000000..a037b0f9 --- /dev/null +++ b/.github/workflows/benchmarks.yml @@ -0,0 +1,252 @@ +name: Benchmarks + +# Timings run ONLY on demand. `lean_action_ci.yml` gates on benchmark +# *correctness* (`--validate-only`) on every PR; timings are advisory and +# produced when someone asks for them. +# +# Not because this runner is jittery -- measured within-run dispersion here is +# tighter than on a quiet laptop (median MAD 0.2% vs 1.4%), though severe +# outliers are about twice as common. The reason is that a gate compares runs +# against each other, on a runner whose CPU model changes between runs, and that +# variance has not been measured. +# +# Three ways in: +# * Actions -> Benchmarks -> Run workflow, with a preset and optional groups +# * a `/bench` comment on a PR, from a repo member +# * automatically on a PR that touches `bench/**`, since a change to the +# harness itself should be measured +on: + workflow_dispatch: + inputs: + preset: + description: >- + Iteration budget. `small` is quickest; `large` gives the most samples + per benchmark and takes correspondingly longer. + type: choice + options: [small, medium, large] + default: medium + groups: + description: >- + Comma-separated group keys to run. Leave empty for the curated set in + bench/ci-groups.txt. `lake exe CompPolyBench --list` shows every key; + an unknown key fails the run. + type: string + default: '' + issue_comment: + types: [created] + pull_request: + paths: + - 'bench/**' + +# Keyed on the PR or ref rather than shared with CI, so asking for benchmarks +# never cancels a correctness run. A separate workflow file already gets a +# distinct `github.workflow`, so this only needs to disambiguate within itself. +concurrency: + group: ${{ github.workflow }}-${{ github.event.issue.number || github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + benchmark: + # The COMMENT author, not the PR author, is the trust boundary on the + # comment path -- the same rule `review.yml` applies. + if: >- + github.event_name != 'issue_comment' || + ( + github.event.issue.pull_request && + startsWith(github.event.comment.body, '/bench') && + ( + github.event.comment.author_association == 'OWNER' || + github.event.comment.author_association == 'MEMBER' || + github.event.comment.author_association == 'COLLABORATOR' + ) + ) + runs-on: ubuntu-latest + timeout-minutes: 90 + permissions: + contents: read + pull-requests: write + steps: + # `issue_comment` fires against the base ref, so the PR head has to be + # resolved and checked out explicitly or we would benchmark the wrong code. + - name: Resolve benchmark target + id: target + env: + GH_TOKEN: ${{ github.token }} + EVENT_NAME: ${{ github.event_name }} + ISSUE_NUMBER: ${{ github.event.issue.number }} + run: | + if [ "$EVENT_NAME" = "issue_comment" ]; then + ref="$(gh api "repos/$GITHUB_REPOSITORY/pulls/$ISSUE_NUMBER" --jq .head.sha)" + echo "pr=$ISSUE_NUMBER" >> "$GITHUB_OUTPUT" + else + ref="" + echo "pr=${{ github.event.pull_request.number }}" >> "$GITHUB_OUTPUT" + fi + echo "ref=$ref" >> "$GITHUB_OUTPUT" + - uses: actions/checkout@v4 + with: + ref: ${{ steps.target.outputs.ref }} + + # The comment body is read through env and never interpolated into the + # script, so a comment cannot inject shell. Anything after `/bench` is + # treated as a group list and validated against `--list` below. + - name: Resolve preset and group selection + id: selection + env: + EVENT_NAME: ${{ github.event_name }} + COMMENT_BODY: ${{ github.event.comment.body }} + DISPATCH_PRESET: ${{ inputs.preset }} + DISPATCH_GROUPS: ${{ inputs.groups }} + run: | + preset=medium + groups="" + case "$EVENT_NAME" in + workflow_dispatch) + preset="${DISPATCH_PRESET:-medium}" + groups="${DISPATCH_GROUPS:-}" + ;; + issue_comment) + groups="$(printf '%s' "$COMMENT_BODY" | head -n1 \ + | sed -E 's|^/bench[[:space:]]*||' | tr -d '[:space:]')" + ;; + esac + if [ -z "$groups" ]; then + groups="$(sed -e 's/#.*//' -e 's/[[:space:]]//g' bench/ci-groups.txt \ + | grep -v '^$' | paste -sd, -)" + echo "selection=the curated set in \`bench/ci-groups.txt\`" >> "$GITHUB_OUTPUT" + else + echo "selection=$groups" >> "$GITHUB_OUTPUT" + fi + echo "preset=$preset" >> "$GITHUB_OUTPUT" + echo "groups=$groups" >> "$GITHUB_OUTPUT" + + # Restore only. The repo's Actions cache is documented as already over + # quota, which is why `.lake` is split into two entries; saving from here + # would add a competing writer for no benefit. + - name: Restore dependency cache + uses: actions/cache/restore@v4 + with: + path: .lake/packages + key: lake-deps-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('lean-toolchain') }}-${{ hashFiles('lake-manifest.json') }} + - name: Restore build cache + uses: actions/cache/restore@v4 + with: + path: .lake/build + key: lake-build-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('lean-toolchain') }}-${{ hashFiles('lake-manifest.json') }}-${{ github.sha }} + restore-keys: | + lake-build-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('lean-toolchain') }}-${{ hashFiles('lake-manifest.json') }} + - name: Set up Lean environment + uses: leanprover/lean-action@v1 + with: + auto-config: false + build: false + test: false + lint: false + use-github-cache: false + use-mathlib-cache: true + + - name: Build benchmark executable + run: lake build CompPolyBench + - name: Validate group selection + env: + GROUPS: ${{ steps.selection.outputs.groups }} + run: | + # An unknown key already fails the benchmark run, but failing here + # costs seconds instead of the whole suite. Keys are matched as fixed + # whole strings, never as patterns: on the comment path a key is + # attacker-influenced text. + lake exe CompPolyBench --list \ + | sed -n 's/^ \([^ ][^ ]*\) - .*/\1/p' > "$RUNNER_TEMP/known-groups.txt" + status=0 + while IFS= read -r key; do + [ -z "$key" ] && continue + if ! grep -qxF "$key" "$RUNNER_TEMP/known-groups.txt"; then + echo "unknown benchmark group: $key" >&2 + status=1 + fi + done <<< "$(printf '%s' "$GROUPS" | tr ',' '\n')" + exit "$status" + - name: Run benchmarks + env: + PRESET: ${{ steps.selection.outputs.preset }} + GROUPS: ${{ steps.selection.outputs.groups }} + run: lake exe CompPolyBench "--$PRESET" --groups "$GROUPS" + + - name: Assemble benchmark report + if: always() + env: + PRESET: ${{ steps.selection.outputs.preset }} + SELECTION: ${{ steps.selection.outputs.selection }} + run: | + mkdir -p "$RUNNER_TEMP/bench-artifact" + results=(bench/out/results-*.jsonl) + reports=(bench/out/report-*.md) + manifests=(bench/out/manifest-*.json) + if [ -e "${results[0]}" ]; then + cp "${results[@]}" "$RUNNER_TEMP/bench-artifact/" + fi + # Provenance: which commit, toolchain and hardware produced these + # numbers, and whether the tree was dirty. Iteration counts come from + # a wall-clock budget now, so they no longer say anything about which + # machine a row ran on. + if [ -e "${manifests[0]}" ]; then + cp "${manifests[@]}" "$RUNNER_TEMP/bench-artifact/" + fi + { + echo '' + echo + echo "### Benchmarks (\`--$PRESET\`)" + echo + echo "Groups: $SELECTION" + echo + echo "Timings come from a shared 2-vCPU GitHub runner and are" + echo "**advisory**. Read the \`Spread\` column before any ratio:" + echo "\`n=1\` rows are a single unrepeated sample and carry no" + echo "dispersion at all. Correctness is gated separately, on every PR." + echo + if [ -e "${reports[0]}" ]; then + cp "${reports[@]}" "$RUNNER_TEMP/bench-artifact/" + for report in "${reports[@]}"; do + cat "$report" + done + else + echo 'No report was produced; the run failed before writing one.' + fi + } > "$RUNNER_TEMP/bench-comment.md" + cat "$RUNNER_TEMP/bench-comment.md" >> "$GITHUB_STEP_SUMMARY" + - name: Upload benchmark artifact + if: always() + uses: actions/upload-artifact@v4 + with: + name: benchmark-results + path: ${{ runner.temp }}/bench-artifact + if-no-files-found: warn + retention-days: 30 + + # Same marker-upsert shape as the build-timing comment in + # `lean_action_ci.yml`, so repeated runs replace rather than pile up. + - name: Upsert benchmark PR comment + if: always() && steps.target.outputs.pr != '' + uses: actions/github-script@v7 + with: + script: | + const fs = require('fs'); + const marker = ''; + const body = fs.readFileSync(process.env.RUNNER_TEMP + '/bench-comment.md', 'utf8'); + const issue_number = Number('${{ steps.target.outputs.pr }}'); + const { owner, repo } = context.repo; + let existing = null; + for await (const response of github.paginate.iterator( + github.rest.issues.listComments, { owner, repo, issue_number, per_page: 100 } + )) { + for (const comment of response.data) { + if (comment.user?.type === 'Bot' && comment.body?.includes(marker)) { + existing = comment; + } + } + } + if (existing) { + await github.rest.issues.updateComment({ owner, repo, comment_id: existing.id, body }); + } else { + await github.rest.issues.createComment({ owner, repo, issue_number, body }); + } diff --git a/.github/workflows/lean_action_ci.yml b/.github/workflows/lean_action_ci.yml index 5e3548eb..a3dcb23b 100644 --- a/.github/workflows/lean_action_ci.yml +++ b/.github/workflows/lean_action_ci.yml @@ -18,36 +18,6 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true -env: - # Benchmark groups CI runs, keeping the benchmark step's wall-clock bounded as the - # suite grows. This is deliberately a subset: `lake exe CompPolyBench --list` shows - # every registered group, and a new group is only covered here once added below. - # An unknown key fails the run, so a renamed group is caught rather than dropped. - BENCH_CI_GROUPS: >- - univariate-dense-koalabear,univariate-dense-babybear, - univariate-sparse-koalabear, - univariate-monic-remainder-small-koalabear, - univariate-dense-goldilocks,univariate-dense-bn254, - univariate-dense-bls12-381,univariate-dense-bls12-377, - univariate-batch-small-koalabear, - univariate-many-one-point-koalabear, - univariate-mul-koalabear,univariate-mul-babybear, - univariate-low-product-koalabear, - univariate-roots-finite-field-koalabear, - multivariate-dense-koalabear,multivariate-sparse-koalabear, - multilinear-coeff-koalabear,multilinear-hypercube-koalabear, - multilinear-many-mle-koalabear, - bivariate-full-koalabear,bivariate-divlinear-koalabear-y32, - guruswami-sudan-interp-small-koalabear, - guruswami-sudan-root-koalabear, - guruswami-sudan-core-small-koalabear, - guruswami-sudan-filtered-core-small-koalabear, - additive-ntt-btf3-l2-r2,additive-ntt-btf3-l4-r2,additive-ntt-btf4-l7-r2, - fields-extension-koalabear-ext4-mul,fields-extension-koalabear-ext4-inv, - fields-extension-babybear-ext4-mul,fields-extension-babybear-ext4-inv, - fields-mont64x8-bn254-inv,fields-mont64x8-bls12-381-inv, - fields-mont64x8-bls12-377-inv - jobs: build: runs-on: ubuntu-latest @@ -200,34 +170,56 @@ jobs: run: | bash scripts/build_timing_report.sh run test_path "$BUILD_TIMING_RESULTS" -- \ bash -eo pipefail -c 'lake test' + - name: Build native field smoke test + run: lake build --wfail CompPolyNativeSmoke + - name: Check native field startup and arithmetic + env: + LEAN_NUM_THREADS: "1" + run: | + ulimit -v 4194304 + ulimit -c 0 + timeout --kill-after=5s 30s .lake/build/bin/CompPolyNativeSmoke + - name: Axiom sweep + run: lake exe axiomsweep --check - name: Fail on build warnings continue-on-error: true run: lake build --wfail - name: Build evaluation benchmark executable run: lake build CompPolyBench - - name: Run evaluation benchmarks - # BENCH_CI_GROUPS is a folded YAML scalar, so strip the line-break whitespace - # the folding introduces before handing it to --groups. + - name: Validate benchmark implementations + # Correctness only: every implementation in a group runs over the same + # inputs and must agree on a digest, so a wrong-but-fast implementation + # fails here. No timings are collected -- this runner is a shared 2-vCPU + # VM whose wall-clock is not worth gating on. Timings come from the + # Benchmarks workflow, on demand. + # + # Deliberately no continue-on-error: a digest mismatch, or a harness + # canary that has collapsed onto the loop floor, is a correctness failure. run: | - lake exe CompPolyBench --medium \ - --groups "$(printf '%s' "$BENCH_CI_GROUPS" | tr -d '[:space:]')" - - name: Prepare evaluation benchmark artifact + groups="$(sed -e 's/#.*//' -e 's/[[:space:]]//g' bench/ci-groups.txt \ + | grep -v '^$' | paste -sd, -)" + lake exe CompPolyBench --medium --validate-only --groups "$groups" + - name: Prepare benchmark validation artifact if: always() run: | rm -rf "$EVALUATION_BENCH_ARTIFACT_DIR" mkdir -p "$EVALUATION_BENCH_ARTIFACT_DIR" - results=(bench/results-*.jsonl) - reports=(bench/report-*.md) + results=(bench/out/results-*.jsonl) + reports=(bench/out/report-*.md) + manifests=(bench/out/manifest-*.json) if [ -e "${results[0]}" ]; then cp "${results[@]}" "$EVALUATION_BENCH_ARTIFACT_DIR/" fi + if [ -e "${manifests[0]}" ]; then + cp "${manifests[@]}" "$EVALUATION_BENCH_ARTIFACT_DIR/" + fi if [ -e "${reports[0]}" ]; then cp "${reports[@]}" "$EVALUATION_BENCH_ARTIFACT_DIR/" for report in "${reports[@]}"; do cat "$report" >> "$GITHUB_STEP_SUMMARY" done fi - - name: Upload evaluation benchmark artifact + - name: Upload benchmark validation artifact if: always() uses: actions/upload-artifact@v4 with: diff --git a/.gitignore b/.gitignore index e98da8ae..5b1b1f7d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,2 @@ /.lake .DS_Store -bench/evaluation* \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md index e21c5676..2378e9b8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -16,6 +16,10 @@ Human contributors should usually start with [`README.md`](README.md), `./scripts/update-lib.sh` and then `./scripts/check-imports.sh`. 4. Use `./scripts/lint-style.sh` when touching Lean style-sensitive files. 5. If you touch repo docs or links, run `python3 ./scripts/check-docs-integrity.py`. +6. When filling or adding a `sorry` (or anything that must stay axiom-clean), run + `lake exe axiomsweep --check`; refresh `scripts/axiom_baseline.json` with + `lake exe axiomsweep --update-baseline` and commit the diff if the change is + intentional. Native-compiler trust is never baselineable. ## Where To Work diff --git a/BENCHMARKING.md b/BENCHMARKING.md new file mode 100644 index 00000000..cfaef5ce --- /dev/null +++ b/BENCHMARKING.md @@ -0,0 +1,1841 @@ +# Benchmarking in CompPoly: Audit and Redesign Plan + +Status: proposal for review. Nothing in `bench/` has been changed. +Author's note: §4.1 contains a measured result that should be read before +anything else, because it changes how every current benchmark number in this +repo should be interpreted. + +## Contents + +- [0. Executive summary](#0-executive-summary) +- [1. What exists today](#1-what-exists-today) +- [2. Strengths](#2-strengths) +- [3. Weaknesses](#3-weaknesses) +- [4. Evidence](#4-evidence) +- [5. What the research says](#5-what-the-research-says) +- [6. Proposed architecture](#6-proposed-architecture) +- [7. Implementation plan](#7-implementation-plan) +- [8. Multi-backend track: Lean C backend vs Peregrine](#8-multi-backend-track-lean-c-backend-vs-peregrine) +- [9. Decisions needed from you](#9-decisions-needed-from-you) +- [10. Sources](#10-sources) +- [11. Review of this audit against the code and the run data](#11-review-of-this-audit-against-the-code-and-the-run-data) +- [12. Change log](#12-change-log) + +--- + +## 0. Executive summary + +The existing suite is well-engineered in its *plumbing* — 66 registered groups, +deterministic inputs, cross-implementation checksum validation, CLI selection, +JSONL plus Markdown output, CI wiring — and the plumbing should largely survive. +The problem is the measurement itself. + +**The headline finding.** `runTimed` folds a bignum `Nat` checksum into the timed +loop on every iteration. I measured that checksum in isolation with a compiled +probe: it costs **~586 ns per call**. A `Goldilocks.Fast` multiplication costs +**~1.6 ns**. The suite therefore reports 619 ns for an operation that takes 1.6 ns, +and reports the fast field as **1.27× faster** than the `ZMod` model when the real +ratio is **~25×**. Details and full numbers in §4.1. + +The practical consequence: **the benchmark suite cannot currently observe the +optimisations it exists to guide.** Halving the cost of a field multiplication +moves the reported number by about 0.3%. + +Four further structural gaps, in priority order: + +1. **No dispersion or repetition.** Each row is one sample of one total; there is + no median, variance, confidence interval, or outlier signal, so a real + regression is indistinguishable from a scheduler hiccup (§3.2). +2. **No regression detection at all** for runtime benchmarks. Results go to a + 30-day CI artifact and a step summary; nothing is stored, compared, or alerted + on. A 30% NTT regression merges silently (§3.5). +3. **Coverage misses the stated targets.** There is no standalone multiplicative + NTT/iNTT group, no base-field microbenchmarks for KoalaBear/BabyBear/Mersenne31, + no `add`/`square`/`batch-inverse` anywhere, no Reed–Solomon or polynomial-matrix + groups, and the additive NTT is measured at 4–128 coefficients when the target + regime is 2^18–2^24 (§3.7, §3.8). +4. **~150 hand-tuned iteration-count magic numbers** that an adaptive harness + would compute for free, and which make `Total` incomparable between rows of + the same table (§3.3). + +**The recommendation is a targeted rebuild, not a rewrite.** Keep the group +registry, the generators, the checksum-as-correctness-oracle idea, and the CLI. +Replace the timing core, add a statistics layer, make the sink cheap, seed +per-group, add size sweeps, and hand result storage and regression detection to +[Radar](https://github.com/leanprover/radar) — the Lean FRO's own continuous +benchmarking service, whose bench-script contract CompPoly can satisfy with a +~20-line `bench/run` shell script (§6, §7). + +Two things I want to flag as *not* settled. First, the academic literature on +benchmark statistics is overwhelmingly about JIT-based managed runtimes; applying +it to AOT-compiled Lean→C is a defensible precaution, not a sourced result (§5.5). +Second, on your Peregrine suggestion: it is a good idea and I have designed a hook +for it, but the Lean→λ□ frontend currently maps `Nat` literals to **63-bit signed +integers** and emits every `@[extern]` function as an **axiom**, which means +CompPoly's `UInt64`/`Array`-based fast paths and its >2^63 moduli cannot pass +through it today. I recommend building the backend-agnostic hook now and gating +the Peregrine backend itself behind a one-day feasibility spike (§8). +--- + +## 1. What exists today + +### 1.1 The compiled suite (`bench/`, `lake exe CompPolyBench`) + +The primary harness is a Lean executable built from `bench/` (~5.8 kLOC across 25 +modules) and wired into `lakefile.lean` as `lean_exe CompPolyBench` plus +`lean_lib CompPolyBenchLib`. + +Architecture: + +- `bench/CompPolyBench/Common.lean` (935 lines) — the whole harness: presets, + the timing primitive `runTimed`, deterministic input generators, checksums, + hardware probing, JSONL emission, and a hand-rolled Markdown table renderer. +- `bench/CompPolyBench/Setup.lean` — CLI parsing (`--small/--medium/--large`, + `--group`, `--groups`, `--list`, `--json-only`, `--markdown-only`) and the + `allTasks` registry. +- One module per area under `bench/CompPolyBench/{Univariate,Multivariate, + Multilinear,Bivariate,Fields}/…`, each exporting a `List BenchTask`. + +The measurement primitive, `Common.lean:533`: + +```lean +def runTimed (…) (warmup measured : Nat) (run : Nat → α) (checksum : α → Nat) + (checksumIterations : Nat := measured) : IO BenchRecord := do + for i in [0:warmup] do + let _ := run i + -- untimed validation pass + for i in [0:checksumIterations] do + validationChecksum := mixChecksum validationChecksum (checksum (run i)) + let start ← IO.monoNanosNow + for i in [0:measured] do + timingChecksum := mixChecksum timingChecksum (checksum (run i)) + let stop ← IO.monoNanosNow + … -- totalNanos := stop - start; averageNanos := total / measured +``` + +Registered surface: 66 groups (`--list`), of which 41 run in CI via the +`BENCH_CI_GROUPS` allowlist in `.github/workflows/lean_action_ci.yml`. + +### 1.2 CI integration + +`lean_action_ci.yml` builds `CompPolyBench`, runs the allowlisted groups at +`--medium` on `ubuntu-latest`, copies `bench/results-*.jsonl` and +`bench/report-*.md` into an artifact (30-day retention), and `cat`s the Markdown +report into `$GITHUB_STEP_SUMMARY`. + +### 1.3 A second, separate benchmark path (`tests/`) + +Three files measure timings at *elaboration* time via `#eval`: + +- `Benchmark.lean` under `tests/CompPolyTests/Univariate/NTT/` (removed in 12.7; + the sweep it held is now the `univariate-mul-crossover-*` groups) — NTT-vs-naive + crossover sweep over 20 operand sizes, `IO.monoMsNow`. +- `KroneckerBenchmark.lean` under `tests/CompPolyTests/Bivariate/` (removed in + 12.7; `bivariate-full-*` covers the same multiply) +- `CommonBench.lean` under `tests/CompPolyTests/Fields/Binary/` (removed in 12.4; + its correctness guards now live in + `tests/CompPolyTests/Fields/Binary/Common.lean`) + +None was imported by `tests/CompPolyTests.lean`, so none ran under `lake test` +or in CI. Each documented its own manual invocation, in the shape `lake build +CompPolyTests.Bivariate.KroneckerBenchmark`. All three are now gone. + +### 1.4 Build-time measurement (`scripts/build_timing_report.sh`) + +430 lines of bash wrapping `lake build` / `lake test`, emitting JSONL and a +Markdown report, with a genuinely sophisticated baseline mechanism: a +`github-script` step walks previous successful workflow runs to find the +merge-base artifact and diffs against it. This is the most mature piece of +performance tooling in the repo — and it measures compile time, not run time. + +--- + +## 2. Strengths + +These are real and worth preserving through any refactor. + +1. **It is a compiled binary, not `#eval`.** The main suite measures + natively-compiled code. That is the correct fundamental choice and rules out + the interpreter overhead that invalidates the `tests/` path. +2. **Cross-implementation checksums.** `BenchGroup` bundles implementations that + must agree, computes a checksum over a shared iteration prefix, and exits + nonzero on mismatch. Benchmarking a wrong-but-fast implementation is a classic + failure mode, and the suite is structurally immune to it. This is better than + most crypto benchmark suites manage. +3. **Deterministic inputs.** A single fixed `seed` (`20260504`) threaded through + `StdGen` makes a run reproducible. +4. **Machine-readable output alongside human output.** JSONL rows carry + `representation`, `method`, `field`, `input_shape`, and iteration counts — + enough dimensional metadata to support a comparison tool that does not exist + yet. +5. **Group-level selection with fail-closed CI keys.** `--groups` plus an + explicit `BENCH_CI_GROUPS` allowlist where an unknown key fails the run, so a + renamed group is caught rather than silently dropped. +6. **Environment capture.** Reports record CPU model, topology, RAM, and + hypervisor vendor — an acknowledgement that runner identity matters. +7. **The `ZMod`-vs-fast pairing is the right comparison axis.** Every group that + pairs the canonical Mathlib-facing model against the native-word + implementation directly encodes the project's core claim: that the verified + fast path is worth having. + +--- + +## 3. Weaknesses + +### 3.1 The timing loop measures the harness, not the code (critical) + +`runTimed` folds a `Nat` checksum into the timed region on every iteration: + +```lean +mixChecksum acc value = (acc * 16777619 + value + 97) % 18446744073709551557 +``` + +The modulus exceeds `2^63`, so `acc` is always a GMP-backed bignum and every call +is a heap-allocating multi-precision multiply-and-mod. Measured cost: **~586 ns +per call**, against **~1.6 ns** for the operation it is supposed to be measuring. +Full numbers, method, and the corrected implementation ratios are in +[§4.1](#41-the-timing-loop-costs-more-than-the-operations-measured). + +Three consequences worth stating separately: + +- Every cheap-operation benchmark reports a near-constant (~590 ns) plus a small + perturbation. Optimising a field multiplication by 2× moves the reported number + by ~0.3%, i.e. below noise. +- The bias is **not uniform**, so it distorts comparisons *between* groups as well + as absolute numbers. It scales with how many checksum calls an iteration makes: + the additive-NTT groups checksum via a `List.finRange (2^n)` fold *inside* the + timed loop, where the sink can cost more than the transform being measured. +- The checksum is also the only thing preventing dead-code elimination of the + benchmark body, but nothing documents it as such — so "make the sink cheaper" + is a change that can silently delete the benchmark (see §3.10 and §4.2). + +### 3.2 No dispersion, no repetition, no confidence + +A record is one sample of one total: `averageNanos = totalNanos / measured` +(integer division). There is no median, no standard deviation, no confidence +interval, no outlier detection, and no repeated trial. A single GC pause or +scheduler preemption is indistinguishable from a real regression, and nothing in +the output lets a reader tell the difference. + +### 3.3 Iteration counts are ~150 hand-tuned magic numbers + +`preset.selectNat 45000 6500 1300`, `selectNat 490000 70000 14000`, +`selectNat 14 2 1` … scattered across every bench module, different per +implementation *within* a group. Consequences: + +- `Total (ms)` is not comparable across rows of the same table, because the rows + ran different iteration counts. Only `Avg` is, and `Avg` is the biased number + from §3.1. +- Every new benchmark requires a human to guess a count, and every hardware + change invalidates the guesses. +- The whole mechanism is what an adaptive harness (Criterion-style: run until a + target measurement time is reached) provides for free. + +### 3.4 Input generation is order-coupled + +One `StdGen` is threaded sequentially through the selected groups, so a group's +inputs depend on **which groups ran before it**. `bench/README.md` states this +outright: adding or reordering a group changes the inputs — and therefore the +checksums and timings — of every group after it. This means: + +- CI's 41-group subset does not measure the same inputs as a full local run. +- Checksums cannot be committed as regression fixtures. +- Bisecting a regression across commits that added a group is unsound. + +The fix is small (derive a per-group seed from the group key) and unlocks a lot. + +### 3.5 No regression detection for run-time benchmarks + +Results are uploaded as a 30-day artifact and pasted into the step summary. There +is no historical store, no baseline comparison, no trend, and no alert — so a 30% +NTT regression merges silently. The irony is that `build_timing_report.sh` +*already* implements merge-base baseline retrieval for build times; run-time +benchmarks get none of it. + +### 3.6 Measured on shared cloud runners + +`ubuntu-latest` is a 2-vCPU shared VM with a CPU model that varies between runs. +Even with a perfect harness, run-to-run wall-clock on such a runner typically +moves by tens of percent. The report collects `Hypervisor vendor` — the +infrastructure knows it is on a hypervisor, and then compares numbers across +hypervisors anyway. + +### 3.7 Coverage does not match the stated optimisation targets + +`ROADMAP.md` names fields, NTTs, and coding theory as the performance story. +Against the 66 registered groups: + +- **No standalone multiplicative NTT/iNTT group at all.** The forward and + inverse transforms — the single hottest primitive in the library's intended + use — are only measured indirectly, through `univariate-mul-*` and + `univariate-low-product-*`, where the transform's cost is entangled with + padding, Kronecker packing, and pointwise products. +- **No base-field microbenchmarks for KoalaBear, BabyBear, Mersenne31, or + Mont32.** Only Goldilocks has `mul`/`inv` groups. The 31-bit fields that the + library targets most heavily have no direct field-arithmetic measurement. +- **No `add`, `sub`, `square`, `batch inverse`, or `sum-of-products` anywhere** — + and `square`/`batch-inverse` are exactly where field implementations win. +- **No Reed-Solomon encode or Gao decode group**, despite `Univariate/ReedSolomon/` + being a named subsystem with a roadmap success criterion. +- **No `LinearAlgebra/PolynomialMatrix/` group**, so the approximant-basis and + Mulders–Storjohann work landed in #312/#313 is unmeasured. +- **Additive NTT is measured at ℓ = 2, 4, 7** — 4, 16, and 128 input + coefficients. Production STARK workloads are 2^18–2^24. At n = 4 the + measurement is dominated by setup and, per §3.1, by the checksum fold; it says + nothing about cache behaviour, which is what determines large-transform + performance. + +### 3.8 No size sweeps + +Almost every group pins one input shape. Where multiple sizes exist +(`bivariate-divlinear-*-y{8,16,32}`, `univariate-batch-{small,medium,large}`) +they are hand-enumerated as separate groups with separately hand-tuned iteration +counts. Nothing reports throughput normalised per element or per butterfly, so +asymptotic behaviour and crossover points are invisible. The only crossover +analysis in the repo is in the orphaned `tests/…/NTT/Benchmark.lean`. + +### 3.9 No external yardstick + +"As fast as possible" is unfalsifiable without a reference. Nothing compares +against plonky3, arkworks, gnark-crypto, or published cycles-per-operation +figures, and nothing is expressed in a comparable unit (cycles/op, ns/op at a +stated clock). + +### 3.10 Smaller defects + +- **`jsonString` does not escape** (`Common.lean:577`): `"\"" ++ s ++ "\""`. It + works today only because no label contains a quote or backslash. +- **Hardware probing is Linux-only** — `lscpu`, `/proc/meminfo`, `nproc`, + `df --output`. On macOS (the primary dev platform here) reports read + `- Runner: unavailable outside GitHub Actions` with no CPU information at all. +- **DCE/laziness is not addressed as a stated invariant.** It happens to be + defeated by the checksum, but nothing documents that as the reason, so removing + the checksum for performance would silently delete the benchmark bodies. I hit + exactly this while writing the probe above: a `let r := f ()` before reading the + clock left the entire 2M-iteration loop outside the timed region, reporting + 0.00002 ns/iter. +- **935-line `Common.lean` mixes five concerns** (timing, generation, checksums, + environment, reporting), and ~500 lines of it are a hand-rolled Markdown table + renderer and a hand-maintained `implementationNameLabels` / + `implementationMethodLabels` lookup table (~90 string pairs) that must be + edited whenever a benchmark is added. +- **Report clutter**: 16 timestamped `report-*.md` / `results-*.jsonl` files + accumulate in `bench/` (gitignored, including a stale `evaluation-*` generation + from May 2026). +- **No `docs/wiki/benchmarking.md`**, despite `AGENTS.md` requiring recurring + repo guidance to be promoted to the wiki. Benchmarking guidance lives only in + `bench/README.md`. +--- + +## 4. Evidence + +Both measurements below were made on this machine (darwin/arm64, Lean 4.33.1) by +adding a temporary `lean_exe` to `lakefile.lean`, building it natively with +`lake build`, running it, and then reverting the scaffolding. The working tree is +unchanged. + +### 4.1 The timing loop costs more than the operations measured + +Loop bodies, 2,000,000 iterations each, two runs agreeing to within ~2%: + +| Loop body | ns/iter | +|---|---:| +| `mixChecksum` alone, no field operation | 586 | +| `Goldilocks.Fast` mul + `mixChecksum` — what `runTimed` does | 606 | +| `Goldilocks.Fast` mul + `UInt64` xor sink | 3.60 | +| `UInt64` xor sink alone (array index + unbox baseline) | 1.96 | +| `Goldilocks` `ZMod` mul + `mixChecksum` | 678 | +| `Goldilocks` `ZMod` mul + `UInt64` xor sink | 78.9 | +| `ZMod` xor sink alone | 38.0 | + +Subtracting the sink-only baselines: + +| Operation | True cost | Suite reports | Inflation | +|---|---:|---:|---:| +| `Goldilocks.Fast` mul | **1.64 ns** | 619 ns | **~380×** | +| `Goldilocks` `ZMod` mul | **40.9 ns** | 788 ns | **~19×** | + +And therefore, for the comparison the group exists to make: + +| | `ZMod` : `Fast` speedup | +|---|---:| +| What `fields-goldilocks-mul` reports | **1.27×** | +| What is actually true | **~25×** | + +The verbatim report from `./.lake/build/bin/CompPolyBench --small --markdown-only +fields-goldilocks-mul`: + +``` +| Implementation | Iterations | Total (ms) | Avg (ns) | +| ------------------------ | ---------: | ---------: | -------: | +| Naive (Goldilocks.Field) | 6000 | 4.73 | 788 | +| Naive (fast Goldilocks) | 6000 | 3.72 | 619 | +``` + +Note also that `ZMod`'s own sink is expensive (38 ns) because Goldilocks exceeds +`2^63`, so `ZMod.val` yields a bignum `Nat`. Any redesigned sink must be measured +per representation, not assumed cheap. + +### 4.2 Dead-code elimination is a live hazard, not a theoretical one + +While writing the probe I wrote the obvious thing: + +```lean +let start ← IO.monoNanosNow +let r := f () -- f : Unit → Nat, a 2M-iteration loop +let stop ← IO.monoNanosNow +``` + +This reported **0.000021 ns/iter** — the entire loop was evaluated *outside* the +timed region. Forcing `r` before reading the clock (`if r % 2 == 7 then …`) gave +the real numbers in §4.1. + +This matters for the redesign because it means a Lean benchmark harness needs an +explicit, documented forcing discipline, and needs a self-check that would *fail* +if the discipline broke. A benchmark that silently measures nothing looks exactly +like a benchmark that got very fast. Recommendation in §6.2. +--- + +## 5. What the research says + +I ran a fan-out research pass (26 sources fetched, 129 claims extracted, each +surviving claim put to a 3-voter adversarial verification where 2 of 3 refutations +kill it; 16 confirmed, 9 killed). Findings below are labelled with what actually +verified. **Read §5.6 for what did not.** + +### 5.1 The Lean ecosystem already has continuous benchmarking: Radar + +This is the most actionable finding in the whole document, and it is the one I +would act on first after fixing the timing core. + +[`leanprover/radar`](https://github.com/leanprover/radar) ("Do you know how fast +you were going?", hosted at `radar.lean-lang.org`) is the Lean FRO's continuous +benchmarking service — a server plus **runners that live on dedicated machines**, +explicitly so that "interference by other processes" is not a problem. It is the +successor to [`leanprover/velcom`](https://github.com/leanprover/velcom), and +`radar-bench-lean4`, `radar-bench-mathlib4`, `radar-bench-cslib` and +`radar-bench-verso` are live consumers. + +Its integration contract is a good fit for CompPoly, and cheap: + +- A **bench script** is any executable. It receives the repo clone path and an + output path, plus `RADAR_REPO`, `RADAR_BENCH_REPO`, `RADAR_OUT`, `RADAR_CACHE` + (a cache directory preserved between runs — useful for `.lake`). +- Measurements are submitted either as **JSON Lines** in the output file, or by + printing lines containing `radar::measurement=` followed by JSON. Each record is + `{"metric": "", "value": , "unit": ""}`. +- Units with special support: `s`, `B`, `%`, `100%`. Metric names are conventionally + `Hierarchical/Path//Submetric`. +- The server queues new main-branch commits, stores history, serves a web UI, and + a **GitHub bot answers `!bench` / `!radar` in PR comments** with results for the + PR head. + +And [`radar-bench-generic`](https://github.com/leanprover/radar-bench-generic) is +a ready-made adaptor that looks for a benchmark suite at *well-known locations* — +`bench`, `bench/run`, `scripts/bench/run`, `tests/bench/run` — sets `IN_RADAR=1`, +and collects `measurements.jsonl` or `radar.jsonl`. + +**So CompPoly can join Lean-hosted continuous benchmarking by adding an executable +`bench/run` that shells out to `lake exe CompPolyBench` and emits +`measurements.jsonl`.** That single change buys dedicated-runner measurement, +historical storage, a web UI, and PR-comment benchmarking — every one of which +CompPoly currently lacks (§3.5, §3.6). It also removes the temptation to build a +regression-detection engine in-repo. + +One caveat from the spec that will bite if missed: **when a metric is measured +multiple times, radar sums the values.** Emitting the same metric name per +iteration would silently accumulate. One record per metric per run. + +*Confidence: high — read directly from the repositories' README and bench scripts +rather than from secondary documentation.* + +### 5.2 Lean's own micro/cross benchmarks delegate measurement, and Mathlib reports instructions + +Two ecosystem precedents, both verified: + +- **`lean4/tests/bench` does not hand-roll a timing harness.** It has two suites + built on the external [temci](https://github.com/parttimenerd/temci) tool — a + lightweight "Speedcenter" suite and a heavyweight "Cross" suite comparing Lean + against other functional compilers (built for the *Counting Immutable Beans* + paper). The precedent is: *outsource measurement statistics rather than + reimplementing them.* Caveats: both suites measure whole-program workloads, not + per-operation microbenchmarks; temci is at 0.8.5 with substantive commits around + 2022 and a maintainer who says the project "has sadly fallen off my radar"; and + its noise-reduction plugins are Linux-and-root-only, so unusable on darwin. + I would take temci's *checklist* (below) and not its code. +- **Mathlib reports most benchmark results in CPU instruction counts, not + wall-clock**, because "the number of instructions is more stable on the + benchmarking servers than the wall-clock time which is affected by process + scheduling" (*Growing Mathlib*, arXiv:2508.21593, by Mathlib maintainers). + Corroborated on Zulip by Sebastian Ullrich: "Instructions is the most robust + time-like measurement, which is why it's the only metric we use for individual + files." + +Two important qualifications on the instruction-count precedent, both of which +came out of the adversarial pass: + +1. Mathlib measures **elaboration cost of Lean source**, not runtime of compiled + executables. The *metric choice* transfers; the harness does not. +2. **Cross-machine comparability of instruction counts was explicitly refuted.** + The defensible justification is within-machine stability under scheduling + noise — not machine independence. Do not sell it as the latter. + +And instruction counts ignore cache and memory-hierarchy effects, which is exactly +what governs large-NTT performance. Hence the two-track split in §6.5: +**instructions gate CI; wall-clock and cycles back absolute claims.** + +### 5.3 Harness statistics: what verified and is worth copying + +- **Geometric warmup ramp** (Criterion.rs): run the routine once, twice, four + times… until accumulated time exceeds a warmup budget (default 3 s). This both + warms caches/branch predictors/CPU frequency *and* yields the per-iteration cost + estimate used to size later samples — which is precisely what would replace + CompPoly's ~150 hand-tuned iteration counts (§3.3). Note honestly that + Criterion's JIT-warmup rationale does not transfer to AOT Lean; the + cache/frequency and cost-estimation rationales do. +- **Tukey outlier classification that labels rather than trims**: fences at + 1.5×IQR (mild) and 3×IQR (severe) off the 25th/75th percentiles, with outliers + **kept in the analysis** and a warning printed as a data-quality signal. +- **Effect-size confidence intervals instead of p-values.** Kalibera & Jones: + with large samples "the decision will nearly always be it is likely that the + systems do not have the same performance, no matter how small or large the + difference actually is. The method then becomes of very little use — it just + adds an illusion of rigour." Their replacement is a CI on the **ratio of mean + execution times**, judged against a practical-importance threshold: with a 3% + threshold, declare a change only if the CI upper bound < 0.97 or lower bound + > 1.03, reported as "A is 4%±1.5% faster than B, with 95% confidence". This is + the right shape for a CompPoly regression gate. +- **Single-number reporting can invert rankings.** Georges et al. (OOPSLA 2007, + Most Influential Paper) found single-number methods misleading in up to 16% of + pairwise startup comparisons, and producing the *opposite* conclusion to the + rigorous verdict in >3%. Quote the 16% carefully: it is a maximum over + methods/configurations on 2007 JVMs, not a figure transferable to Lean→C. +- **Do not assume a steady state after a fixed warmup — test for it.** Georges + et al. detect steady state via the coefficient of variation of the last *k* + iterations dropping below ~0.01–0.02, then compute the CI **across process + invocations** (because iterations within one invocation are not independent). + Barrett et al. (*Virtual Machine Warmup Blows Hot and Cold*, OOPSLA 2017) then + showed the underlying "discard warmup, report peak" assumption is frequently + false — "at most 43.5% of ⟨VM, benchmark⟩ pairs consistently reach a steady + state of peak performance" — and proposed PELT changepoint detection over + per-iteration timings instead of hand-tuned warmup thresholds. +- **Instruction-count harnesses run each benchmark exactly once**, since + instruction counting needs no repetition to filter timing noise + (iai-callgrind, now [gungraun](https://github.com/gungraun/gungraun)). Their own + authors disclaim it as a wall-clock replacement: the cycle estimate "merely + correlates to wall-clock times". Valgrind on macOS is x86_64-only, so this track + is CI-Linux-only for this repo. + +### 5.4 The environment checklist worth stealing from temci + +temci's `usable` preset enumerates the OS-level controls that matter, and claims +to cover LLVM's benchmarking guidance: `cpu_governor` (performance), +`disable_swap`, `sync`, `nice` (default −15), `disable_aslr`, `disable_ht`, +`cpuset`, `disable_intel_turbo` (because "the CPUs cannot overclock partially"). +The `all` preset additionally `SIGSTOP`s non-vital processes and renices +competitors. All plugin actions are documented as reversible. + +Peregrine's own Lean benchmark suite independently arrives at a subset of the same +list: Linux booted with `isolcpus`, `taskset -c 3` to pin the benchmark to an +isolated core, and [hyperfine](https://github.com/sharkdp/hyperfine) as the +timing driver — while noting that further CPU tuning "were found to not further +reduce noise on the hardware tested". + +For CompPoly the actionable version is: **this is the runner's job, not the Lean +binary's.** Radar's dedicated runners are where these controls belong. + +### 5.5 What did NOT verify — read before relying on anything above + +The adversarial pass killed nine claims. Four are worth knowing as design traps: + +1. **Cross-machine comparability of instruction counts** — refuted (see §5.2). +2. **Criterion's regression-slope / bootstrap-hypothesis-test pipeline** — + refuted. A harness copying "Criterion methodology" should copy the warmup ramp + and Tukey labelling that *did* verify, not a regression/bootstrap pipeline. +3. **Mathlib's "5% threshold Zulip bot" and VelCom hosting** — not established by + this pass. It may well be true, but do not cite it without re-sourcing. +4. **"Keep running until the CI is within 1–2% of the mean, capped at 30 runs"** + as a Georges et al. prescription — refuted; they do not prescribe that. + +Two further honesty notes: + +- **Every statistical result cited in §5.3 was established on JIT-based managed + runtimes** with adaptive recompilation and managed-heap GC. Barrett et al. never + even warmup-classified their C baseline. Lean's AOT C output has different + non-stationarity sources — reference counting, its own allocator, page faults, + CPU frequency and cache state. Applying these protocols to CompPoly is a + defensible precaution presented as extrapolation, **not a sourced finding.** +- **The research pass returned nothing on two angles I asked for**, and the + session's web-search budget was exhausted before I could cover them myself: + - **zk/finite-field benchmark methodology and published cycle baselines** — + arkworks, plonky3, gnark-crypto, blst, zkalc, ZPrize. Zero surviving claims. + **I therefore quote no cycle-count baselines in this document.** §6.6 specifies + how to obtain a yardstick by measurement instead of by citation. + - **Lean-4-specific measurement hazards** — refcount traffic, `UInt64`/`USize` + boxing, `@[inline]`/`@[specialize]` effects, allocator/GC noise, black-box + patterns, `perf`/Instruments/valgrind over Lean-generated C. Zero surviving + claims. This is the single most decision-relevant gap, which is why §4 + measures the two hazards that matter most directly rather than citing anyone. +--- + +## 6. Proposed architecture + +Design goal, stated so it can be checked: **a 5% improvement to a +`KoalaBear.Fast` multiplication should be visible in the suite's output, and a 5% +regression should fail CI.** Neither is true today. Everything below is chosen to +make that sentence true and nothing more elaborate. + +### 6.1 Layering + +Split the 935-line `Common.lean` into five modules with one concern each. The +group registry, the `BenchTask`/`BenchGroup` shape, and the CLI stay essentially +as they are — they work. + +``` +bench/CompPolyBench/ + Harness/Timer.lean -- sink, forcing discipline, one timed sample + Harness/Sample.lean -- warmup ramp, adaptive sizing, sample collection + Harness/Stats.lean -- median/MAD/CI, Tukey labels, ratio CI vs baseline + Harness/Sink.lean -- `Sink α` class: cheap α → UInt64 digest + Harness/Emit.lean -- radar JSONL + human Markdown + Workloads/… -- generators, per-group seeding + Registry.lean -- groups (was Setup.lean) +``` + +### 6.2 Timing core: cheap sink + explicit forcing + a canary + +Three changes, all small, that together fix §3.1 and §3.10. + +**(a) Replace the bignum checksum in the timed loop with a `UInt64` sink.** +Correctness validation already happens in a *separate, untimed* pass — that pass +should keep the strong `Nat` digest. The timed loop only needs a +dead-code barrier: + +```lean +/-- Cheap, allocation-free digest of a benchmark result, used only as a +dead-code-elimination barrier inside the timed loop. Correctness is checked by +the untimed validation pass, which keeps the strong `Nat` digest. -/ +class Sink (α : Type _) where + toU64 : α → UInt64 + +@[noinline] def sinkStep (acc x : UInt64) : UInt64 := + (acc ^^^ x) * 0x9E3779B97F4A7C15 |>.rotateLeft 27 +``` + +Measured cost of this shape: **~1.9 ns/iter including the array index**, versus +586 ns for `mixChecksum` (§4.1). + +For aggregate results (arrays, `Fin n → α`) the sink must not walk the whole +structure inside the timed loop — that is the additive-NTT bug in §3.1. Sink a +**fixed-size sample** of the output (say elements `0`, `n/3`, `2n/3`, `n-1`) and +leave full-structure digesting to the validation pass. + +**(b) Make forcing explicit and documented.** §4.2 shows a bare `let` can hoist +the entire loop out of the timed region. The harness should have exactly one place +that reads the clock, and it should force inside: + +```lean +@[inline] def timeOne (iters : Nat) (body : Nat → UInt64 → UInt64) : + IO (Nat × UInt64) := do + let start ← IO.monoNanosNow + let mut acc : UInt64 := 0 + for i in [0:iters] do + acc := body i acc -- `body` folds the result into `acc` + let forced := acc -- consumed below, before the clock is read again + let stop ← IO.monoNanosNow + pure (stop - start, forced) +``` + +The `body : Nat → UInt64 → UInt64` signature is the important part: it makes it +*type-impossible* to write a benchmark whose result is unused. + +**(c) Add a canary and a loop-overhead floor.** Two synthetic groups that ship +with the harness: + +- `harness/empty` — an empty body. Its measured time is the loop-overhead floor. + Emit it as a metric every run, and **report every other benchmark's cost both + raw and floor-subtracted**. This is what turns 3.60 ns into the honest 1.64 ns. +- `harness/canary` — a body with a known, deliberately non-eliminable cost. If it + measures below a hard-coded threshold, **fail the run**: something has started + optimising benchmark bodies away. This is the self-check §4.2 argues for. + +### 6.3 Sampling and statistics + +Replace the ~150 magic iteration counts (§3.3) with a two-stage adaptive scheme, +per benchmark: + +1. **Warmup / calibration.** Geometric ramp (1, 2, 4, 8, … iterations) until + accumulated time exceeds a warmup budget (default ~200 ms locally, ~50 ms in + CI). Take the per-iteration cost estimate from the last ramp step. +2. **Sizing.** Choose `itersPerSample` so one sample takes a target duration + (~1 ms is a good default: long enough to dwarf the ~30 ns clock overhead, + short enough that many samples fit a budget). +3. **Collection.** Collect a fixed `sampleCount` (default 50; 20 in CI) samples. + Fixed, not significance-triggered: optional stopping on significance inflates + false positives, which is why I am *not* copying temci's early-stop rule (§5.3). + +Report per benchmark: `median`, `mean`, `stddev`, `min`, `p95`, `MAD`, +`itersPerSample`, `sampleCount`, and Tukey mild/severe outlier counts — outliers +**labelled, not dropped**. Use **median** as the headline number. + +Two independence points worth building in from the start: + +- Samples within one process are not independent (allocator state, page tables, + CPU frequency). Support `--processes k`: the driver invokes the binary *k* times + and aggregates across invocations, with the CI computed **across per-invocation + medians**. This is the Georges et al. structure adapted to AOT, and it is the + only way to get an honest interval. +- Do not implement changepoint steady-state detection yet. It is data-hungry, not + parameter-free, and its evidence base is JIT VMs (§5.5). Instead emit the raw + per-sample vector into the JSONL so the question *"does AOT Lean even need it?"* + can be answered offline from real data later. That is a cheap option to keep open. + +### 6.4 Per-group seeding + +Fix the order-coupling in §3.4 by deriving each group's generator from its key: + +```lean +def genFor (groupKey : String) : StdGen := + mkStdGen (mixSeed seed (hashString groupKey)) +``` + +Consequences, all of them wins: CI's subset measures the same inputs as a full +local run; adding a group perturbs nothing else; and **correctness digests become +committable fixtures**, so the validation pass turns into a real regression test +rather than an intra-run cross-check. That last point is what makes the multi-backend +work in §8 possible at all. + +### 6.5 Two metric tracks + +| Track | Metric | Where | Purpose | +|---|---|---|---| +| **Gate** | instructions/op | Radar Linux runner, `valgrind --tool=callgrind` over the benchmark binary | CI regression detection — stable under scheduling noise | +| **Claim** | ns/op, cycles/op | Radar dedicated runner, wall-clock | Absolute performance, size sweeps, external comparison | + +Both tracks emit into the same radar JSONL. Rationale and the explicit caveat that +instruction counts are *not* cross-machine comparable are in §5.2. Wall-clock stays +the source of truth for anything cache-sensitive, i.e. every large NTT. + +### 6.6 Coverage: close the gaps in §3.7 and §3.8 + +Restructure workloads as an explicit **operation × representation × size** matrix +rather than 66 hand-named groups, so a sweep is a parameter and not a copy-paste. + +Priority additions, in the order I would add them: + +1. **Base-field microbenchmarks** for every field with a `Fast` path — + KoalaBear, BabyBear, Mersenne31, Goldilocks, Mont32, Mont64x8, binary towers — + over `add`, `sub`, `mul`, `square`, `inv`, `batchInverse`, `sumOfProducts`. + This is the layer everything else is built on and it is almost entirely + unmeasured today. +2. **Standalone forward/inverse multiplicative NTT** over `log n = 8 … 22`, + reported as ns per butterfly (`t / (n/2 · log n)`) so the size sweep is + readable and cache cliffs show up as a curve rather than a number. +3. **Additive NTT at production sizes** — extend from ℓ = 2/4/7 to ℓ up to 20, + subject to a wall-clock budget per preset. +4. **Reed–Solomon encode and Gao decode**, and the + `LinearAlgebra/PolynomialMatrix` approximant/Mulders–Storjohann layer, neither + of which has any group. +5. **Crossover reporting.** Fold the orphaned `tests/…/NTT/Benchmark.lean` sweep + into the suite and emit the naive/NTT crossover degree as its own metric — it is + a genuinely useful number to track over time, and it is currently measured by a + file nothing runs. + +Then **delete the three orphaned `#eval` benchmarks in `tests/`** (§1.3). They +measure interpreted elaboration-time code, nothing runs them, and keeping a second +methodologically-broken benchmark path invites someone to trust it. + +**External yardstick (§3.9).** Since no published cycle baselines survived +verification (§5.5), do not cite numbers — *measure* them. Add +`bench/external/` holding a small Rust project pinning `plonky3` (and +optionally `arkworks`/`gnark-crypto`) with `cargo bench` over the *same* +operations at the *same* sizes, run on the *same* runner, emitted into the *same* +radar metrics under a `reference/` prefix. Then "CompPoly's KoalaBear mul is 3.2× +plonky3's" is a measured claim on identical hardware rather than a comparison +across two papers' machines. This is also the only honest way to state a +roadmap success criterion like "competitive performance with industry-standard +implementations". + +### 6.7 Reporting and storage + +- **Canonical output: radar JSONL** (§5.1), one record per metric per run, with + metric names like `fields/koalabear/mul//ns_per_op`. Keep the richer per-sample + vector in a sidecar file for offline analysis. +- **Keep the Markdown report** for humans and the CI step summary, but generate it + from the structured records. Retire the ~90-entry + `implementationNameLabels`/`implementationMethodLabels` lookup tables (§3.10) by + putting the display label in the group definition where the benchmark is + declared. +- **Fix `jsonString` to escape** (§3.10) — or emit via `Lean.Json`, which is + already imported. +- **Make hardware probing cross-platform** — add `sysctl -n machdep.cpu.brand_string`, + `hw.ncpu`, `hw.memsize` fallbacks so local darwin runs are not blank (§3.10). +- **Write to a single `bench/out/` directory** (gitignored) instead of accumulating + timestamped files in `bench/`, and delete the stale `evaluation-*` generation. +- **Add `docs/wiki/benchmarking.md`** and link it from the wiki hub, per the + `AGENTS.md` requirement that recurring repo guidance live in the wiki (§3.10). +--- + +## 7. Implementation plan + +Ordered so that each phase is independently valuable and the highest-value, +lowest-risk work lands first. Estimates are rough and assume familiarity with the +existing `bench/` code. + +### Phase 0 — Stop the bleeding (½ day, do this regardless) + +The cheapest change with the largest effect on the numbers' meaning. + +1. Swap `mixChecksum` for a `UInt64` sink **inside the timed loop only**; keep the + `Nat` digest in the untimed validation pass (§6.2a). +2. Fix the aggregate sinks that fold over `List.finRange (2^n)` inside the timed + loop to sample a fixed number of output positions instead. +3. Add `harness/empty` and report floor-subtracted cost (§6.2c). + +**Do not skip step 3.** Steps 1–2 will make every number in the suite drop by +roughly two orders of magnitude, and the first question anyone asks will be +"is the benchmark still doing anything?". The canary and floor answer it. + +Expected outcome: `fields-goldilocks-mul` reports ~1.6 ns and ~41 ns instead of +619 ns and 788 ns, and the group's speedup goes from 1.27× to ~25×. + +### Phase 1 — Statistics and adaptive sizing (2–3 days) + +1. The new Harness/Sample module: geometric warmup ramp, adaptive `itersPerSample`, fixed + sample count (§6.3). +2. The new Harness/Stats module: median, mean, stddev, MAD, min, p95, Tukey mild/severe + labels. +3. Delete the ~150 `selectNat` magic numbers; presets become + *(warmup budget, sample count, size cap)* triples rather than per-benchmark + iteration counts. +4. Emit the per-sample vector into the JSONL sidecar. +5. Add `--processes k` and aggregate across per-invocation medians. + +This is where the "5% change is visible" goal is actually met, and it deletes more +code than it adds. + +### Phase 2 — Radar integration and regression gating (1–2 days) + +1. Add an executable `bench/run` matching the `radar-bench-generic` contract: + builds `CompPolyBench`, runs it, writes `measurements.jsonl` (§5.1). Honour + `IN_RADAR`, use `RADAR_CACHE` for `.lake`. +2. Emit radar-format metric records with stable hierarchical names. One record per + metric per run — **radar sums repeats** (§5.1). +3. Ask the Lean FRO to register the repo with Radar and provision a runner. This + is the request that unlocks dedicated-hardware measurement, history, the web + UI, and `!bench` on PRs. +4. Implement the ratio-CI gate (§5.3): compare against the merge-base baseline, + declare a regression only when the 95% CI on the ratio clears a practical + threshold (start at 5%, tighten later). `scripts/build_timing_report.sh` + already contains the merge-base-artifact retrieval logic to crib from if Radar + provisioning takes time. + +Fallback if Radar registration is slow: the same JSONL plus the existing +merge-base artifact machinery gives a self-hosted version of the gate, on noisy +runners. Worth doing as a stopgap, not as the destination. + +### Phase 3 — Determinism and correctness fixtures (1 day) + +1. Per-group seeding from the group key (§6.4). +2. Commit the validation digests as fixtures; the validation pass becomes a + regression test that fails on a wrong answer, not just on intra-run + disagreement. +3. Update `bench/README.md` to drop the "checksums are not comparable across runs" + caveat, which per-group seeding removes. + +### Phase 4 — Coverage (3–5 days, incremental) + +Work the §6.6 priority list. Each item is independent, so this can land +group-by-group. I would do base-field microbenchmarks and the standalone NTT +sweep first — they are the two biggest holes relative to the stated goals. + +Also in this phase: delete the three orphaned `tests/` `#eval` benchmarks, and add +`docs/wiki/benchmarking.md`. + +### Phase 5 — Instruction-count track (2 days, Linux/CI only) + +1. A second bench script that runs the binary under + `valgrind --tool=callgrind` in one-shot mode and emits `instructions/op`. +2. Gate CI on instructions; keep wall-clock for absolute claims (§6.5). +3. Note the platform limit up front: Valgrind on macOS is x86_64-only, so this + track never runs on the darwin dev machine. + +### Phase 6 — External yardstick (2–3 days) + +`bench/external/` with pinned `plonky3` (± `arkworks`) benchmarks over matching +operations and sizes, on the same runner, into `reference/` metrics (§6.6). + +### Phase 7 — Multi-backend differential track + +See §8. Gated behind a feasibility spike; do not schedule until that spike +reports. + +### Cross-cutting: what to preserve + +Worth writing down so a refactor does not throw it away: the group registry and +`BenchTask` shape, the CLI surface, the `ZMod`-vs-`Fast` pairing as the primary +comparison axis, the checksum-as-correctness-oracle idea, the fail-closed +`BENCH_CI_GROUPS` key checking, and the JSONL-plus-Markdown dual output. All of +that is good and none of it is what is broken. +--- + +## 8. Multi-backend track: Lean C backend vs Peregrine + +You asked for a comparison of the stock Lean backend against Peregrine, valuing it +for **correctness as well as performance**. I think the correctness half is the +stronger argument of the two, and I have designed the hook for it — but the +Peregrine path itself has concrete blockers that should be tested before it is +scheduled. + +### 8.1 What Peregrine actually is + +[Peregrine](https://github.com/peregrine-project/peregrine-tool) is "a unified +middle-end for code generation from proof assistants". It takes **Agda, Lean, or +Rocq** frontends into the untyped λ□ (LambdaBox) intermediate language, and emits +**C, Rust, WebAssembly, OCaml, CakeML, or Elm**. The middle-end is verified in +Rocq; per its README, "some of the frontends and backends are". + +The Lean frontend is +[`peregrine-project/lean-to-lambdabox`](https://github.com/peregrine-project/lean-to-lambdabox), +which adds an `#erase` command performing type and proof erasure from Lean's +`Expr`: + +```lean +#erase val_at_false to "out.ast" +``` + +The `.ast` is then converted to Malfunction by the `peregrine` tool, compiled to +`.cmx`, and linked with `ocamlopt` — the same route as Rocq's verified extraction +pipeline. + +So this is not a faster Lean code generator competing on raw throughput. It is an +**independent, largely verified code-generation path**. That is exactly why it is +interesting here. + +### 8.2 Why the correctness payoff is the real prize + +`AGENTS.md` forbids `native_decide` so that no *proof* depends on +`Lean.ofReduceBool` — the compiler is outside the trusted base. But the benchmark +suite runs compiled code, and CompPoly's whole value proposition is "verified +*and* fast". A miscompilation in the fast path would produce a result that is +wrong but passes every kernel-checked proof, because the proofs are about the Lean +definitions and the benchmark measures the emitted C. + +Running the same source through two independent backends and requiring **identical +correctness digests** is direct evidence against that class of failure. It is +evidence, not proof — agreement could still hide a shared frontend bug, and the +Lean erasure frontend is not itself fully verified — but it is the cheapest +available check on the one link in the chain that the TCB policy deliberately +cannot cover. + +That reframes the priority: **build the backend-agnostic hook now** (it is nearly +free and useful on its own), and treat the Peregrine backend as a spike. + +### 8.3 The hook to build now (Phase 3, ~½ day on top of per-group seeding) + +Two things, both of which are worth having even if Peregrine never lands: + +1. **Make correctness digests backend-independent committed fixtures.** This falls + out of per-group seeding (§6.4): once a group's inputs no longer depend on run + order, its digest is a stable constant that can live in a JSON fixture file. Any + executable, however built, either reproduces the fixture or fails. +2. **Add `backend` as a dimension of the metric namespace**, e.g. + `fields/koalabear/mul//ns_per_op` tagged with `backend=lean-c` or + `backend=peregrine-ocaml`. Radar's flat metric names accommodate this by + convention; nothing in the harness needs to know how many backends exist. + +The same hook also serves comparisons you are more likely to want sooner: different +Lean versions, `-O2` vs `-O3` on the C output, or LLVM bitcode output (`lean --bc`) +versus the C path. + +### 8.4 The blockers, from reading the frontend source + +I read the frontend's LeanToLambdaBox/Erasure.lean. Three concrete gates, in descending +severity: + +1. **`Nat` literals are erased to a 63-bit signed λ□ primitive (`i63`)**, with an + outright `panic! "Nat literal not representable as a 63-bit signed integer."` + Consequences for CompPoly: + - **Goldilocks (2^64 − 2^32 + 1), BN254, BLS12-381, BLS12-377 and the + `Mont64x8` fields are immediately out** — their moduli exceed 2^63. + - **KoalaBear, BabyBear and Mersenne31 are in** — all are just under 2^31 and + fit comfortably. + - The current `mixChecksum` modulus (18446744073709551557) also exceeds 2^63, + which is a second reason to replace it (§6.2). +2. **`@[extern]` constants are emitted as axioms** (`Config.Extern.preferAxiom`), + left for the backend to supply. In Lean, `UInt64`/`USize` arithmetic, `Array` + primitives and `Nat` arithmetic are all `@[extern]`. So CompPoly's entire + `Fast` layer — which is *precisely* the code worth benchmarking — becomes a + wall of axioms unless the OCaml/Malfunction backend implements them. Their own + benchmark suite exercises `Nat`, `List`, `RBMap`, `binarytrees`, `qsort`, + `unionfind` (the *Counting Immutable Beans* workloads) — **no `UInt64` or + `Array`-heavy numeric kernels at all**, so this is untested territory rather + than known-broken. +3. **Toolchain gap.** `lean-to-lambdabox` pins `leanprover/lean4:v4.22.0`; + CompPoly is on `v4.33.1` and uses the new module system (`module`, + `public import`, module privacy). Erasure operating over `Expr` is probably + robust to that, but it is untested and CompPoly is a large Mathlib-dependent + library, not a self-contained benchmark file. + +### 8.5 Proposed spike (1 day, before scheduling anything) + +Answer the feasibility question cheaply, in this order — stop at the first failure: + +1. Can `lean-to-lambdabox` be built against CompPoly's toolchain at all, or does + it need a v4.22 shim project? +2. `#erase` the smallest genuinely-CompPoly definition that stays within i63 and + avoids `@[extern]`: a `KoalaBear` or `Mersenne31` **canonical `ZMod`-model** + arithmetic operation (values < 2^31, `Fin p` over `Nat`). Does it erase? +3. Does the resulting λ□ compile through `peregrine ocaml` → Malfunction → + `ocamlopt` and run? +4. Does it produce the same digest as the Lean-C build? +5. Only then: what happens to an `Array`-based kernel (a small NTT), and how many + axioms does the backend leave unimplemented? + +Deliverable: a one-page note with a go/no-go and, if no-go, the specific missing +primitive support — which is useful upstream feedback to the Peregrine project +regardless. + +### 8.6 What to expect on the performance side + +Two predictions, stated so they can be checked rather than assumed: + +- On the workloads that *do* pass, Peregrine's OCaml path will likely be **slower** + than Lean's C backend for numeric kernels, because Lean's `UInt64`/`Array` + primitives are hand-written C and OCaml's boxed-int and array semantics differ. + A slower verified path is still a useful data point — it quantifies the cost of + the extra assurance. +- Peregrine's own suite compares `via_lean` against several `malfunction-*` + configurations using [hyperfine](https://github.com/sharkdp/hyperfine) under + `isolcpus`/`taskset` CPU isolation. If the spike goes well, **reuse their + harness shape for the backend comparison** rather than pushing the Peregrine + path through CompPoly's in-process timer — whole-program timing is the right + granularity for a backend comparison, and it sidesteps the question of whether + `IO.monoNanosNow` even exists on that path. +--- + +## 9. Decisions needed from you + +1. **Phase 0 now, separately?** It is half a day, it is confined to + `runTimed` and the aggregate sinks, and it changes every published number in + the repo by up to two orders of magnitude. I would land it as its own PR with + the before/after table from §4.1 in the description, so the discontinuity in + the history is explained rather than mysterious. +2. **Radar, or self-hosted?** Radar gives dedicated runners, history, a web UI and + `!bench` on PRs for roughly a day of integration work, but it means asking the + Lean FRO to register the repo and provision a runner. Self-hosting on top of + the existing merge-base artifact machinery avoids that dependency but keeps + measuring on shared `ubuntu-latest` VMs, which caps the whole effort's value. + My recommendation is Radar, with the self-hosted gate as a stopgap. +3. **Regression-gate threshold.** I suggest starting at 5% on the ratio CI and + tightening once the instruction-count track (Phase 5) shows what the real noise + floor is. Worth deciding whether a gate failure blocks merge or only comments. +4. **Preset budget.** Adaptive sizing needs a wall-clock budget per preset instead + of iteration counts. What is the acceptable CI benchmark step duration? That + number determines how far the size sweeps in §6.6 can go. +5. **Scope of the coverage work.** §6.6 lists five priorities; items 1 and 2 + (base-field microbenchmarks, standalone NTT sweep) are the ones I would insist + on. The rest can wait. +6. **Peregrine spike — do you want it scheduled?** One day, and it may well end in + "not yet, needs `UInt64`/`Array` primitive support". The hook in §8.3 is worth + building either way. + +## 10. Sources + +Verified in this session by direct inspection: + +- [`leanprover/radar`](https://github.com/leanprover/radar) — README, bench-repo + and measurement-format specification +- [`leanprover/radar-bench-generic`](https://github.com/leanprover/radar-bench-generic), + [`radar-bench-lean4`](https://github.com/leanprover/radar-bench-lean4) — bench + script contract +- [`leanprover/velcom`](https://github.com/leanprover/velcom) — predecessor +- [`peregrine-project/peregrine-tool`](https://github.com/peregrine-project/peregrine-tool) — README +- [`peregrine-project/lean-to-lambdabox`](https://github.com/peregrine-project/lean-to-lambdabox) — + LeanToLambdaBox/Erasure.lean, benchmarks/README.md, benchmarks/TESTS, lean-toolchain + +Verified in the research pass (3-voter adversarial verification): + +- [`lean4/tests/bench/README.md`](https://github.com/leanprover/lean4/blob/master/tests/bench/README.md) — + temci-based suites +- [temci](https://github.com/parttimenerd/temci) and + [its docs](https://temci.readthedocs.io/en/latest/temci_exec.html) — runners, + `usable` preset, stored sample format +- *Growing Mathlib: maintenance of a large scale mathematical library*, + [arXiv:2508.21593](https://arxiv.org/html/2508.21593v1) — instruction counts as + the stable metric +- [Mathlib Speedcenter Zulip thread](https://leanprover-community.github.io/archive/stream/287929-mathlib4/topic/mathlib4.20speedcenter.html) +- Georges, Buytaert & Eeckhout, *Statistically Rigorous Java Performance + Evaluation*, OOPSLA 2007 — [PDF](https://dri.es/files/oopsla07-georges.pdf) +- Barrett et al., *Virtual Machine Warmup Blows Hot and Cold*, OOPSLA 2017 — + [arXiv:1602.00602](https://arxiv.org/abs/1602.00602), + [ACM](https://dl.acm.org/doi/10.1145/3133876) +- Kalibera & Jones, *Quantifying Performance Changes with Effect Size Confidence + Intervals* — [arXiv:2007.10899](https://arxiv.org/pdf/2007.10899) +- [Criterion.rs analysis documentation](https://bheisler.github.io/criterion.rs/book/analysis.html) + — warmup ramp, Tukey outlier classification +- [gungraun](https://github.com/gungraun/gungraun) (formerly iai-callgrind) and + [Callgrind manual](https://valgrind.org/docs/manual/cl-manual.html) — one-shot + instruction counting +- [LLVM benchmarking guidance](https://llvm.org/docs/Benchmarking.html) +- [hyperfine](https://github.com/sharkdp/hyperfine) +- Beseda et al., [arXiv:2506.04204](https://arxiv.org/abs/2506.04204) and + Traini et al., [arXiv:2209.15369](https://arxiv.org/abs/2209.15369) — + steady-state detection follow-ups + +**Not covered.** The research pass returned no surviving claims on zk/finite-field +benchmark methodology or published cycle baselines (arkworks, plonky3, +gnark-crypto, blst, zkalc, ZPrize), nor on Lean-4-specific measurement hazards, +and this session's web-search budget was exhausted before I could cover them +directly. §4 substitutes direct measurement for the second gap; §6.6 substitutes +measurement-on-identical-hardware for the first. No cycle-count figures are quoted +anywhere in this document. + +### Repo locations referenced + +| Thing | Path | +|---|---| +| Timing primitive | `bench/CompPolyBench/Common.lean:533` (`runTimed`) | +| Bignum checksum | `bench/CompPolyBench/Common.lean:471` (`mixChecksum`) | +| Unescaped JSON | `bench/CompPolyBench/Common.lean:577` (`jsonString`) | +| Linux-only hardware probe | `bench/CompPolyBench/Common.lean:364` | +| Label lookup tables | `bench/CompPolyBench/Common.lean:721`, `:744` | +| Registry / CLI | `bench/CompPolyBench/Setup.lean` | +| CI bench steps | `.github/workflows/lean_action_ci.yml:210` | +| CI group allowlist | `.github/workflows/lean_action_ci.yml:22` (`BENCH_CI_GROUPS`) | +| Build-time baseline logic | `scripts/build_timing_report.sh`, `lean_action_ci.yml:283` | +| Orphaned `#eval` benchmarks | none remain; `Benchmark.lean` and `KroneckerBenchmark.lean` were removed in 12.7, and a third, `CommonBench.lean`, in 12.4 | + +--- + +## 11. Review of this audit against the code and the run data + +Status: added after §1-§10, by a second pass that read `bench/` in full and +re-analysed the 253-row `--small` run still sitting in +`bench/results-260804-125549.jsonl`. §1-§10 above are left as written; this +section records where they hold, where they overstate, and what they missed. +Where the two disagree, this section is the one to act on. + +### 11.1 Confirmed + +The bias mechanism, restated precisely: `mixChecksum` is `Nat` arithmetic modulo +18446744073709551557, the largest prime below `2^64`, so the accumulator is always +a GMP bignum and every mix is a heap-allocating multi-precision multiply-and-mod +inside the timed region. Also confirmed: the DCE hazard, absent dispersion, +order-coupled inputs, unescaped `jsonString`, the Linux-only hardware probe, the +missing wiki page, and the magic-number problem — though the count is **227 +`selectNat` call sites**, not the ~150 of §0 and §3.3. + +### 11.2 The headline in §0 and §4.1 is scoped too widely + +The bias is a roughly **constant additive** offset of ~590 ns per checksum call. +It therefore only destroys rows whose per-iteration cost is of that order. Against +the real 253-row `--small` run: + +| Per-iteration cost | Rows | Bias contribution | +|---|---:|---| +| `> 59 us` | 228 | `< 1%` | +| `5.9 - 59 us` | 21 | 1 - 10% | +| `0.9 - 5.9 us` | 4 | 10 - 60% | +| `< 880 ns` (harness-dominated) | **0** | — | + +The cheapest row in that run is 1994 ns (`bivariate-full-eval-horner-xy-fast`). +`fields-goldilocks-mul` — the 603/800 ns exhibit of §4.1 — is not in the run at +all; it was measured in isolation, and it is one of only two groups anywhere near +the ns scale. + +So §0's "the benchmark suite cannot currently observe the optimisations it exists +to guide" is true of ns-scale field arithmetic and false of the suite as it stands. +The value of the sink fix is **enabling** the base-field microbenchmarks that §3.7 +correctly identifies as missing — not repairing 66 existing numbers. + +The corollary contradicts §9.1's worry about an unexplained discontinuity in the +history: the figures published in `docs/wiki/field-extensions.md:281-307` (25 us, +3.5 ms, and the 3.1x - 7.9x spec-vs-`csimp` ratios) and `ROADMAP.md:77-79` carry +one checksum call per iteration at the 25 us - 15 ms scale, so they are within +about 2% of correct and survive the fix. There is no doc churn to schedule. + +### 11.3 §3.1's additive-NTT claim does not hold in the data + +_Settled by measurement in 12.1; see findings 6 and 7 there for the corrected +account, which supersedes the closing paragraph of this section._ + +§3.1 states that in the additive-NTT groups "the sink can cost more than the +transform being measured". At `l = 4, R_rate = 2` the reference row measures +1.82 s/iter, against 64 bignum mixes at ~37 us — 0.002%. The fast row measures +9.8 ms, so ~0.4%. The concern is sound in principle and unobservable in practice. + +The real defect in those groups is sharper. In +`bench/CompPolyBench/Fields/Binary/AdditiveNTT/Impl.lean`: + +- `checksumBtf3Output` (`:28-30`) folds over `List.finRange (2 ^ n)`, materialising + a `2^n`-element list of `Fin` on **every** iteration before the bignum fold. +- `checksumConcreteBtfOutputArray` (`:37-42`) re-invokes + `AdditiveNTT.arrayToFinFunction` **per index inside the fold** rather than + hoisting it. + +The reference row and the fast row of the same group therefore pay *different* +in-loop overheads, which makes the reference-vs-fast **ratio** unclean. That +matters more than the absolute offset, and it is the specific thing to fix. + +### 11.4 What §1-§10 missed, in the order that matters + +1. **Warmup does not work at all — at any preset.** + `warmupIterations = preset.selectNat 100 10 0` (`Common.lean:56-57`), so warmup + is **zero for every group at `--small`**; all 253 rows report + `warmup_iterations: 0`. The batch, mod, mul and additive-NTT families use + `preset.selectNat 1 1 0`, so they get **one** warmup iteration at `--medium`, + which is the preset CI runs. And the warmup body is + + ```lean + for i in [0:warmup] do + let _ := run i + pure () + ``` + + a dead pure `let` — precisely the elimination pattern §4.2 documents — so it may + warm nothing even where the count is nonzero. §3.2 notes the absence of + repetition but never states that warmup is absent. + +2. **The dominant statistical defect is `n = 1`, not "one sample of one total".** + 67 of 253 rows measure exactly **one** iteration; 107 measure three or fewer; 27 + of those spend over a second in the timed region. The expensive groups pin a + large input shape and shrink the iteration count to 1, so the large-NTT and + batch-eval numbers — the suite's most strategically important — are single + unrepeated samples. Worst cases: `univariate-batch-large-naive-horner` at 22.25 s + over one iteration, and `univariate-batch-large-subproduct-ntt-mul-reversal-ntt-low-mod` + at 13.53 s over one iteration against a `-fast` sibling at 11.74 s over two. No + change to the sink touches any of this. + +3. **The untimed validation pass costs exactly as much as the measurement.** + `checksumIterations := measured` is the default (`Common.lean:535`), so the full + workload runs a second time for validation. Roughly half of the ~6.5 minutes a + full `--small` run spends measuring buys no measurement. Cutting the validation + pass to one iteration is a free 2x. + +4. **Declaration ergonomics, which the coming per-benchmark pass will pay for.** + The group key and title are written **three times** per group — the + `*GroupInfos` list, the `*Tasks` list, and the returned `BenchGroup` literal (see + `Fields/Goldilocks.lean:31-33`, `:53-54`, `:78-85`) — so a rename can silently + drift. `runTimed` takes 11 positional parameters, five of them consecutive + `String`s, across 226 call sites. §3.10 flags the label lookup tables but not this. + +5. **`BenchTask`'s generality is entirely unexercised**, which makes the §6.4 fix + far cheaper than §6.4 suggests. `BenchTask.runTask` has the shape + `BenchPreset → BenchSelection → StdGen → IO (Array BenchGroup × StdGen)`, but all + 66 registered tasks go through `BenchTask.fromGroupRunner`, which discards the + selection. Per-group seeding is therefore a change to **one function** + (`Common.lean:180-185`), not a 24-module refactor, and it lets `StdGen` disappear + from the task contract altogether. + +6. **Confirmed dead code.** Zero non-definition references anywhere: the ten + per-area `runX (preset) (selection) (gen)` wrappers (`runUnivariate`, + `runUnivariateBasic`, `runUnivariateManyEval`, `runUnivariateBatchEval`, + `runUnivariateNttFastMul`, `runUnivariateNttFastMulLow`, `runMultivariate`, + `runMultilinear`, `runBivariate`, `runAdditiveNtt`) and seven `*GroupInfos` + aggregate lists. The two live ones are reached only through fragile + `.getD i ⟨"…", ""⟩` indexing (`Bivariate/GuruswamiSudan.lean:328-341`, + `GuruswamiSudan/ReceivedWord.lean:439-446`). + +7. **§6.6's "delete the three orphaned `tests/` benchmarks" would drop real + coverage.** The since-removed `CommonBench.lean` also carried + four `#guard` correctness checks and a retained baseline `clMul` implementation + (the deleted `Finset.fold`-over-`Fin 256` version) used as a reference against + the current one. Nothing imports the file, so CI never runs those guards. They + must be migrated into a real test before the file goes. + +8. **The checksum already gates CI.** The `Run evaluation benchmarks` step has no + `continue-on-error` and no `if:`, so a checksum mismatch — which exits nonzero — + already fails the job. §2.2 and §3.5 treat the digest as an intra-run + cross-check only. Separately, the artifact step globs `bench/results-*.jsonl` + and `bench/report-*.md`, which is correct on a fresh CI checkout but locally + sweeps all 17 stale output files into the artifact. + +### 11.5 Consequences for the plan in §7 + +- **§7's Phase 0 / Phase 1 split is the wrong cut.** Phase 0 alone moves a handful + of ns-scale rows and leaves `n = 1` in place. The sink fix and the sampling layer + are both cheap and belong in sequence, not staged behind each other by priority. +- **§6.1's up-front split of `Common.lean` is premature.** It has 12 direct + importers, and Phases 0-3 rewrite most of what would be moved. Carving each + module out of `Common.lean` as part of the PR that rewrites it gets the same + Parnas separation without a big-bang refactor that is then rewritten. +- **The §6.3 adaptive scheme needs a defined behaviour for workloads where one + iteration already exceeds the sample target.** §6.3 assumes `itersPerSample` can + always be chosen; for 27 rows it cannot. The answer taken here is to collect as + many samples as the budget allows, minimum one, and mark the row unreplicated. + +### 11.6 Scope taken for the first push + +Foundations only: the measurement core, the sampling and statistics layer, input +determinism, and declaration ergonomics. Explicitly deferred — Radar integration +and registration (§5.1, §7 Phase 2), the regression gate and threshold (§9.3), +coverage work (§6.6, §7 Phase 4), the instruction-count track (§6.5, §7 Phase 5), +the external yardstick (§6.6, §7 Phase 6), and the Peregrine track (§8) including +its hook. + +**Radar is deferred by decision, not pending.** Recorded here so it is not +rediscovered later as an oversight: there is no `bench/run` entry point, no +registration ask, and no radar metric format, and none of the three is waiting +on anything. §5.1 and §7 Phase 2 keep the details should the decision change. +The regression gate stays blocked behind it — without Radar it is self-hosted +baseline comparison, for which the machinery exists to copy +(`lean_action_ci.yml` finds and downloads the merge-base run's artifact, and +`scripts/build_timing_report.sh render` renders the comparison for build +timing) but the threshold does not, and §12.5 finding 5 is explicit that no +single run can measure the run-to-run variance a gate would compare against. + +Three decisions settled that §9 left open: + +- **§9.4, preset budget: do not gate on CI benchmark wall-clock.** The benchmark + step moves off the blocking CI job, so measurement quality stops trading against + CI duration. +- **Input sizes are not retuned in this push.** Sample counts are reported honestly + and thin rows flagged; sizes are revisited in a later systematic per-benchmark + pass. +- **§9.1: no separate Phase 0 PR.** Per 11.2 there is no discontinuity to explain, + so the sink fix does not need its own PR to carry a before/after table. + +### 11.7 Method + +The band table in 11.2 and every count in 11.4 come from +`bench/results-260804-125549.jsonl` — a full 253-row `--small` run, the only +full-suite result file present — read with `python3`, plus direct reading of +`bench/` (23 files, 4635 lines), `.github/workflows/lean_action_ci.yml`, +`scripts/build_timing_report.sh`, and `docs/wiki/`. No benchmarks were rebuilt or +re-run for this section; the ~586 ns and ~1.6 ns figures of §4.1 are taken as given +from the original probe, and 11.2 only re-scopes their consequences. + +--- + +## 12. Change log + +Work lands as a **stack**: each branch is cut from the previous one, so each PR +reviews as a small diff and the stack merges bottom-up. Base of the stack is +`dhsorens/benchmarking`. + +| # | Branch | Scope | Status | +|---|---|---|---| +| 1 | `dhsorens/bench-measurement-core` | Cheap `UInt64` sink in the timed loop, forcing discipline, real warmup, floor + canary groups, `jsonString` escaping, symmetric additive-NTT digests | landed | +| 2 | `dhsorens/bench-sampling` | Multi-sample collection, median/MAD/Tukey stats, unreplicated flags, capped validation pass reused as warmup | landed | +| 3 | `dhsorens/bench-determinism` | Per-group seeding from the group key, registration made authoritative, dead-code removal | landed | +| 4 | `dhsorens/bench-reporting` | Cross-platform hardware probe, `bench/out/`, `docs/wiki/benchmarking.md`, `clMul` guard migration | landed | +| 5 | `dhsorens/bench-foundations` | `--validate-only`, correctness gate in main CI, on-demand `benchmarks.yml` | landed | +| 6 | `dhsorens/bench-sizing-and-coverage` | Wall-clock budgets replace 228 hand-tuned iteration counts, one declaration site per row, preset-independent digests, group identity and a run manifest in the output | in review | + +### 12.1 Measurement core (`dhsorens/bench-measurement-core`) + +**New modules.** `bench/CompPolyBench/Harness/Sink.lean` (sink primitives), +`bench/CompPolyBench/Harness/Timer.lean` (the single place that reads the clock), +`bench/CompPolyBench/Harness/SelfCheck.lean` (`harness-floor`, `harness-canary`). `Common.lean`'s +`runTimed` now delegates to `Timer`; the checksum block stays where it is until +PR 3 moves the registry. + +**Measured effect**, `--small`, darwin/arm64, Lean 4.33.1: + +| | before | after | +|---|---:|---:| +| `harness-floor` (loop + sink, per iteration) | n/a | **1.89 ns** | +| `goldilocks-mul-fast` | 619 ns | **3 ns** | +| `goldilocks-mul-zmod` | 788 ns | **365 ns** | +| reported `ZMod : Fast` ratio | 1.27x | **~108x** | +| `harness-canary` margin over floor | n/a | 236x | + +**Deviations from the plan, and why.** + +1. *`sink` is an optional argument, not a `body : Nat → UInt64 → UInt64` + signature.* The planned signature would have required editing all 226 + `runTimed` call sites, whose result types include partial applications over + `DenseMatrix F`, `Option (CBivariate F)` and `Option (Array F)`. A + default-valued `sink` removes the bignum `mixChecksum` from the timed loop at + every call site with no churn, and lets a cheap sink be declared where it pays. + The forcing guarantee comes from `Timer`'s accumulator plumbing plus the + canary rather than from the type. +2. *`sinkStep` is `@[inline]` and `runTimed` is `@[specialize]`.* With + `@[noinline]` and unspecialised closures the harness floor measured + **22.4 ns/iter** — which would itself have dominated the 1.6 ns field multiply + this work exists to expose. Inlining and specialising took the floor to + 1.89 ns. The elimination risk `@[noinline]` was guarding is now carried by the + canary, at a 236x margin. +3. *Per-row floor subtraction deferred to PR 2.* The floor turns out to be + **per representation**, not global: a `ZMod` element above `2 ^ 63` has no + cheap word digest while its fast counterpart does, so subtracting a single + global floor from a `ZMod` row would mislead. This needs the per-representation + floor the stats layer can provide. §4.1 anticipated this ("any redesigned sink + must be measured per representation, not assumed cheap"); it is a sharper + constraint than §6.2c allows for. +4. *Preset iteration counts untouched.* PR 1 makes warmup *effective* — it was a + dead `let` and may have warmed nothing at any preset (11.4.1). PR 2 makes it + *sized*; warmup is still 0 at `--small`. + +**Findings from doing the work.** + +5. *§4.1 underestimates the `ZMod` Goldilocks multiply by about 8x.* Measured + with a native sink over 200000 iterations with warmup, it is **325 ns/iter**, + not the ~41 ns of §4.1. `ZMod n` is `Fin n`, so the multiply is + `(a.val * b.val) % n` over `Nat` with both operands near `2 ^ 64` — a 128-bit + bignum multiply and mod, with allocation. The group's corrected ratio is + therefore ~108x, not the ~25x §4.1 predicts. +6. *§3.1's additive-NTT claim is refuted, and 11.3 was right to doubt it.* + Replacing the bignum digest and the per-iteration `List.finRange (2 ^ n)` + materialisation moves those rows by about **5%** (`additive-ntt-btf3-l4-r2` + 1736 ms → 1649 ms; `-fast` 9.15 ms → 8.81 ms). The digest was never close to + costing more than the transform. +7. *The additive-NTT rows are asymmetric for a reason neither §3.1 nor 11.3 + identified.* The reference row returns `Fin (2 ^ n) → α`, a **function**: an + output does not exist until an index is applied, so realising the whole result + is part of that row's work and not part of the `Array`-returning fast row's. + An intermediate version of this branch sampled four output positions in both + sinks and the reference row appeared to get 16x faster, because it was then + computing a sixteenth of what the fast row computed. Both sinks now fold over + every output position. The lesson generalises: **a sink may only skip work the + benchmark has already done.** Sampling is correct for a materialised array and + wrong for a lazily-indexed function. +8. *Cross-commit checksum comparison is impossible today, confirmed empirically.* + 199 of 235 shared rows differ from the August baseline purely because 13 groups + were added and 2 removed in between, shifting the shared `StdGen`. No point in + the suite's history can serve as a regression baseline until PR 3 lands. +9. *`sink_digest` earns its place in the schema.* Emitting the timed loop's + accumulator keeps it observably live (so an `@[inline]` timing loop cannot have + its accumulation eliminated) and doubles as a determinism signal. It is what + flagged finding 7: across two full runs it changed on exactly the five rows + whose sink had changed and nowhere else. + +**Also in this branch.** `jsonString` now escapes via `Lean.Json.renderString`; +`checksumConcreteBtfOutputArray` hoists `arrayToFinFunction` out of its fold +(digest-preserving — verified across two full runs, 250 named records, zero +mismatches); `harness-floor` and `harness-canary` added to `BENCH_CI_GROUPS`; +`bench/README.md` documents the validation/timed split and the sink contract. + +**Not required:** `./scripts/update-lib.sh` globs `CompPoly/*.lean` only, so new +modules under `bench/` need no regeneration — the lakefile's +`Glob.submodules \`CompPolyBench` covers them. + +### 12.2 Sampling and statistics (`dhsorens/bench-sampling`) + +**New modules.** `bench/CompPolyBench/Harness/Stats.lean` (summary statistics), +`bench/CompPolyBench/Harness/Sample.lean` (sample collection). `runTimed` now +splits its iteration budget into samples instead of timing one region. + +**Measured effect**, full 68-group `--small` run, 286 records: + +| | before | after | +|---|---:|---:| +| rows with 5 or more samples | 0 | **172** | +| rows with 2-4 samples, flagged | 0 | 47 | +| rows with a single unrepeated sample | 253 of 253 | 67, each marked `n=1` | +| dispersion reported | none | median MAD **1.4%**, p90 3.0%, max 5.1% | +| rows with severe Tukey outliers | not detectable | 27 | +| timed-region total | 152.8 s | 152.0 s | +| full run wall clock | — | 277 s | + +**The noise floor is now a measured quantity.** Across the 172 replicated rows +the median absolute deviation is 1.4% of the median, with p90 at 3.0% and a +maximum of 5.1%. §9.3 asks what the regression-gate threshold should be and +could not answer it without data; this is that data, on a *quiet local machine*. +A 5% gate sits at roughly the worst observed sample dispersion, so it is a +defensible starting point and anything tighter than about 3% would be +false-positive-prone even before a shared CI runner adds its own variance. + +**Deviation from the plan.** The 227 `selectNat` sites are **not** retired here. +Retiring them requires a wall-clock budget per benchmark, which is precisely the +per-benchmark judgement deferred to the systematic pass; doing it now would mean +touching all 226 `runTimed` call sites twice. Instead each existing count is +reinterpreted as a total-work budget and split into up to `targetSampleCount` +samples. This fixes replication wherever the budget can pay for it and leaves the +counts to retire naturally when each benchmark gets a considered budget. The +geometric calibration ramp of §6.3 is deferred with them: with a total budget +supplied there is nothing for it to calibrate. + +**Findings from doing the work.** + +1. *A zero interquartile range makes Tukey label everything.* Where samples agree + to the picosecond, both fences collapse onto the quartiles and every sample + that differs at all is marked a severe outlier — the opposite of the intended + signal. Labelling is now suppressed when the interquartile range is zero. +2. *An unspecialised function between `runTimed` and the timed loop costs 5x.* + Interposing `collectSamples` re-introduced the closure indirection that + `@[specialize] runTimed` had removed, and `goldilocks-mul-fast` went from 3 ns + to 16 ns. `@[specialize]` on `collectSamples` restored it. Anything that sits + between the specialisation boundary and the loop has to carry the attribute. +3. *11.4.3 was right about the validation pass but wrong about the fix.* On the + original harness the validation pass cost **138 s against a 192 s timed + region** — 72%, so it really did nearly double the suite. But capping it at + `validationIterationCap` saves only ~0.8 s, because the cost is concentrated + in rows whose validation already ran exactly *once* and whose single run takes + up to 22 s. The cap is still right for the 19 cheap rows it touches; the + saving on expensive rows comes instead from letting the validation pass count + towards warmup, since it has already executed the body. An expensive workload + validated once now runs twice per benchmark rather than three times. +4. *The capping change is exactly auditable.* 19 records had a validation pass + above the cap; exactly those 19 digests changed and the other 267 did not, + with no unexplained differences. Letting validation count as warmup changed no + digest at all. + +**Still unreplicated.** 67 rows remain at `n=1`. Every one is a workload whose +single iteration already exhausts its budget — `univariate-batch-large-*` at +degree 65536, the additive-NTT reference rows, `univariate-mod-by-monic-medium-*`. +No amount of harness work fixes these; they need smaller input shapes, which is a +per-benchmark judgement for the systematic pass. They are now visibly marked +rather than silently averaged. + +### 12.3 Determinism (`dhsorens/bench-determinism`) + +**The change.** `genFor` derives each group's `StdGen` from its key, applied +inside `BenchTask.fromGroupRunner`. Because all 66 registered tasks go through +that one function (11.4.5), this is a three-line change and no group runner's +signature moves. The shared generator is now passed through untouched. + +**Verified.** `--group fields-goldilocks-mul`, `--groups fields-goldilocks-mul,additive-ntt-btf3-l2-r2` +and the same pair reversed all produce identical digests for every row. Running +the exact `BENCH_CI_GROUPS` subset reproduces the full run's digest on **all 184 +comparable rows** — the property §3.4 says is unobtainable today. Full 68-group +run: exit 0, 286 records, 273 s. + +**Registration is now authoritative.** `fromGroupRunner` stamps `groupKey` and +`title` from the `BenchGroupInfo` that `--list` and the CI allowlist validate +against, so a runner's own literals can no longer drift from its registration. +The literals inside the 66 group runners are now inert; removing them means +rewriting every runner's return expression and is left to the systematic pass, +which will touch each one anyway. + +**Dead code removed.** 25 declarations with no references anywhere: the ten +per-area `runX` wrappers and fifteen `*GroupInfos` aggregate lists (seven dead +before this branch, eight more once the `runX` wrappers that consumed them +went). 175 lines deleted, 17 added. + +**Digest fixtures deferred, deliberately.** Per-group seeding is the prerequisite +and it now holds, but committing 250 expected digests immediately before a +systematic pass that will deliberately change many benchmarks' input shapes would +produce a fixture file in near-permanent conflict. The mechanism is worth adding +once the benchmark set settles. Note also that digests remain preset-dependent, +since the validation pass length derives from the measured iteration count. + +**Comparison keys need care.** Record `name` is not unique: `extension-mul` and +`extension-inv` are each emitted by the ext4, ext5 and ext6 groups. Any tool +diffing two result files must key on `(name, field, input_shape)` — keying on +`name` alone silently collapses those rows and reports false differences. Worth +knowing before the comparison tooling in §6.7 gets written. + +### 12.4 Reporting, platform, and docs (`dhsorens/bench-reporting`) + +**Darwin hardware probe.** `collectRunnerHardware` falls back to `sysctl` when +neither `lscpu` nor `nproc` exists, so a local run reports the machine that +produced a number instead of `unavailable outside GitHub Actions`. `df --output` +is GNU-only, so the darwin path parses the full `df -h` table where the size is +the second field. A local report now reads +`Apple M3 Max / 16 logical CPUs / 64 GiB`. + +**Single output directory.** Reports and results go to `bench/out/`, created on +demand and ignored wholesale, replacing two ignore rules across two files. This +also closes 11.4.8's live hazard: CI's artifact glob was `bench/results-*.jsonl`, +correct on a fresh checkout but locally sweeping every stale file into the +artifact. The 17 accumulated output files, including the May 2026 +`evaluation-*` generation, are gone. + +**`clMul` guards rescued before deletion.** The `CommonBench.lean` file under +`tests/CompPolyTests/Fields/Binary/` carried four `#guard` +correctness checks and the removed `Finset.fold`-over-`Fin 256` baseline that +pins the current `clMul` to the behaviour it replaced — none of which CI ran, +because nothing imported the file. They now live in +`tests/CompPolyTests/Fields/Binary/Common.lean`, which `CompPolyTests.lean` +imports, and the benchmark file is deleted. Verified by breaking one guard and +confirming the build fails, then restoring it. This is the coverage §6.6 would +have deleted silently. + +Rescuing the guards turned out to be worth more than it first appeared. While +this branch was open, #320 made the multiplication width-generic +(`carryLessMul {v w}`, with `clMul` as its 128-bit instance) and #321 added +`BF64`, whose `mul` is the 64-bit instance. The rescued baseline was written +against `clMul` alone, so on merging it pinned only one of the two widths now in +use — the new one was covered only indirectly, by #321's reference vectors, +which pin the *field* rather than the multiplication against its predecessor. +The baseline is now generic in the operand width and carries four more guards at +width 64, checked the same way. Had the file gone in §6.6, the generalization +would have landed with nothing pinning either width to the fold it replaced. + +`Benchmark.lean` under `tests/CompPolyTests/Univariate/NTT/` and +`KroneckerBenchmark.lean` under `tests/CompPolyTests/Bivariate/` were +deliberately left in place here: the former held the only NTT-vs-schoolbook +crossover logic in the repo and was the specification for a future crossover +metric. That metric exists as of 12.7 — the `univariate-mul-crossover-*` +groups — and both files were deleted with it. + +**`docs/wiki/benchmarking.md`** added and registered in both hand-maintained +lists in `docs/wiki/README.md`, since `check-docs-integrity.py` validates that +links resolve but not that a page is registered anywhere. It owns the two-pass +model, the sink rule, how to read a `Spread` column, the self-check, determinism, +how to add a group, and a known-gaps list. The duplicated line at +`docs/wiki/quickstart.md:111-112` is fixed and `generated-files.md` updated. + +**Deviations from the plan.** + +1. *The benchmark step was not moved off the blocking CI job.* The decision + recorded in 11.6 was that measurement quality should not trade against CI + duration, and it has not: nothing in PRs 1-3 caps sampling to fit a budget. + But *structurally* moving benchmarks to their own job means a second + Mathlib-dependent build, which trades a real and recurring CI cost for a + scheduling benefit. That is a cost decision rather than an engineering one and + is left open — see below. The `BENCH_CI_GROUPS` comment no longer claims + wall-clock is the limiting criterion, and the step keeps its fail-closed + behaviour: benchmark *timings* are informational, but a checksum mismatch or a + canary failure is a correctness signal that should fail the run. +2. *A separate reporting module was not split out.* `Common.lean` is down from 935 to ~1020 + lines gross, but four concerns have already moved into `Harness/` (`Sink`, + `Timer`, `Sample`, `Stats`) and the remaining reporting code is about to be + rewritten anyway when per-representation floors and the label-table retirement + land. Moving it now would mean moving it twice. + +**Open decision** — resolved in 12.5, though not the way it was framed. The +question assumed the choice was *where the benchmark job runs*. The better cut +turned out to be *what it runs*: the step was doing correctness and timing at +once, and only the timing half needed to leave. + +### 12.5 Splitting correctness from timing in CI (`--validate-only` and `benchmarks.yml`) + +The question was whether benchmarks could run only when the code they touch +changes. Path filtering is available — `lean_release_tag.yml` already uses a +`paths:` filter — but it is the wrong instrument twice over. A group's +performance depends on whatever it transitively calls, and +`CompPoly/Fields/Montgomery/**` underpins nearly every group, so a filter honest +enough to be safe would fire on almost every substantive PR. And it reduces the +*cost* of a signal that is not actionable rather than taking it off the blocking +path — see finding 5 below for what the runner's noise actually looks like, which +is not what I assumed. + +The step was doing two separable jobs — 41 groups cross-checking a canonical +`ZMod` model against its native-word implementation on random inputs plus the +harness canary, and a timing report. So the split is not "run benchmarks +sometimes" but **the correctness half always gates; the timing half never runs in +blocking CI**. + +**`--validate-only`.** Runs the untimed digest pass and the agreement check and +collects no samples. Deterministic and machine-independent, which is what a gate +should be. Threaded through an `initialize IO.Ref Bool` in +`bench/CompPolyBench/Harness/Timer.lean` rather than a parameter, because every +alternative means editing all 226 `runTimed` call sites. Reports through a +compact `renderValidationMarkdown` — group, rows, agreement, digest — rather +than a timing table of zeros. + +**Measured**, 41 curated groups at `--medium`, darwin/arm64: + +| | wall clock | +|---|---:| +| timed run (what CI did) | **124 s** | +| `--validate-only` (what CI does now) | **32 s** | +| `--validate-only`, all 68 groups | 138 s | + +So the gate got about 4x cheaper *and* kept the only part of it worth gating on. +The 41-group set is retained over all 68 because the extra 106 s buys coverage of +groups whose single iteration costs seconds; the numbers are recorded in +`bench/ci-groups.txt` so the trade can be revisited. + +**Findings from doing the work.** + +1. *The canary would have been silently disabled.* `runHarnessSelfCheck` compares + timed totals, and with no samples collected `0 < 3 * 0` is false — the check + would pass vacuously in exactly the mode CI runs, disabling the one guard + against benchmark bodies being optimised away. `runTimed` therefore takes + `forceTiming`, which the self-check sets; the canary costs ~50 ms and runs in + both modes. Verified by stubbing `canaryRounds` to 0 and confirming a + `--validate-only` run exits 1. +2. *`BENCH_CI_GROUPS` could not stay a workflow `env:` entry.* A second workflow + cannot see it, and duplicating 41 keys invites drift. The list moved to + `bench/ci-groups.txt`, one key per line with `#` comments, read by both + workflows — and it now sits next to the benchmarks it names rather than buried + in YAML. +3. *No required status checks exist on `main`.* The ruleset enforces only + `deletion`, `non_fast_forward` and `pull_request`. The usual footgun — a + workflow skipped by a `paths:` filter never reports its check and blocks the + PR — therefore does not apply here, so both a separate workflow and a + step-level skip were structurally safe. Worth re-checking if required checks + are ever added. +4. *The new workflow must not save caches.* `lean_action_ci.yml` documents the + Actions cache as already over quota (~10.3 GiB against 10 GB), which is why + `.lake` is split into two entries. `benchmarks.yml` restores both and saves + neither. + +**Kept in main CI deliberately.** `lake build CompPolyBench` still runs on every +PR, because the correctness gate needs the binary. Only the timed *run* moved +out. Dropping the compile too would mean dropping the differential check, which +is the opposite of the priority. + +5. *The shared runner is steadier than I assumed, and I had this backwards.* I + wrote here and in the docs that CI-runner dispersion would be worse than the + 1.4% median MAD §12.2 measured locally. The first real CI run of the timing + workflow says otherwise: **median MAD 0.2%, p90 0.5%, max 1.0%** across 172 + replicated rows, appreciably *tighter* than the local figures. A busy + development laptop with frequency scaling and heterogeneous cores is a noisier + place to measure than an idle VM slice. + + What is worse on CI is the tail: **56 of 172 rows carried severe Tukey + outliers, against 27 of 286 locally** — the signature of a quiet baseline + punctuated by preemption. + + The decision stands but the reason was wrong, and is corrected everywhere it + appeared. Neither figure is what a gate needs: a gate compares *runs against + each other*, on a runner whose CPU model varies between runs (§3.6), and one + run cannot measure that variance. Timings are advisory because cross-run + comparability is unvalidated, not because within-run noise is high. + Establishing the run-to-run figure is the next measurement worth making, and + is what any threshold should be set from. + +**Verified on a real runner**, not only locally. The first push auto-triggered the +timing workflow through the `bench/**` filter, so both paths ran end to end: + +| | duration | +|---|---:| +| main CI job, total | 3m29s | +| ... of which `Validate benchmark implementations` | **46s** | +| ... of which `Build evaluation benchmark executable` | 64s | +| timing workflow, total | 4m55s | +| ... of which `Run benchmarks` | 167s | + +On the runner the correctness gate costs 46s where the timed run it replaced +cost about 167s. The workflow restored both caches, validated its group +selection, and upserted a PR comment carrying the advisory caveat. + +**Not done.** No nightly schedule. Timings are produced when someone asks — +manual dispatch, a `/bench` comment from a repo member, or a PR touching +`bench/**`, which is the one place path filtering genuinely fits. + +### 12.6 Budget-driven sizing (`dhsorens/bench-sizing-and-coverage`) + +§11.4 item 4 and §6.3 said the same thing from two directions: the suite carried +one hand-tuned iteration count per benchmark per preset, 228 of them, and a count +is the wrong unit. It is not comparable between two rows of one table, it goes +stale as the code it measures gets faster, and choosing one for a new benchmark +is guesswork that has to be redone on every machine. §12.2 deferred retiring +them; this branch does it. + +A preset now selects a `BenchBudget` — warmup nanoseconds, sample length, sample +count, and a total ceiling per row — and each row is calibrated against it by a +geometric ramp that doubles as warmup. Two design points are load-bearing. +`sampleNanos` is 1 ms at every preset: a sample is a mean over `itersPerSample` +iterations, so raising that count averages dispersion away, and a preset-varying +sample length would make `--small` and `--large` report structurally different +spread for identical code. And `measureNanos` is a second, separate ceiling, +because `sampleNanos` × `sampleCount` is 50 ms even at `--large`, which would pin +the seconds-per-iteration rows at one sample forever. + +Getting there needed three preparatory steps, each verifiable on its own. A +`BenchSpec` record replaced five consecutive `String` arguments at 228 call +sites, in a commit that provably changed nothing. Digest lengths became the +**period of the body in its iteration index** rather than `min 256 measured`: +195 of the 282 rows shared across presets had carried three different digests, +and under budget sizing the same expression would have made a digest vary with +the *machine*, which turns committed fixtures from awkward into impossible. +`guruswami-sudan-packed-filter` needed a separate fix — its `candidateCount` was +`preset.selectNat 128 64 32`, an input shape wearing a budget's clothes, which no +digest rule could have repaired; pinned at 128. + +**Findings.** + +1. *The canary would have inverted silently.* + `bench/CompPolyBench/Harness/SelfCheck.lean` asserted + `canary.totalNanos > 3 x floor.totalNanos`. Totals separate the two rows only + while they run the same number of iterations, and a wall-clock budget + equalises them by construction. Left alone it throws on every run; "fixed" by + lowering `canaryFloorRatio` it passes vacuously forever and the harness loses + its only dead-code detection. It compares per-iteration medians now, landed + *before* the flip so the flip was not verified through a check that was + throwing. Observed ratios 160x-776x across presets and both modes. + +2. *One group had been reporting its cost divided by `itersPerSample` since it + was written.* The finite-field root group's body was a closed term — `p` was + bound to a nullary constant and so is the root context — so it was evaluated + once and every later iteration in a sample got the cached array back. At the + hand-tuned counts that was a factor of 1 to 20 and invisible; calibration + raised `itersPerSample` to ~700k and made it 10^8. Fixed by drawing the + workload's root seeds from the group's random stream, so the body depends on + a local the way every other group's does. `fast-nttfast`'s real cost is + 74 ms, not the 24 ms the suite had been reporting. Found by the per-iteration + median comparison the sizing flip's verification calls for, and the reason to + insist on that comparison rather than a digest diff alone. + +3. *Two report lines would have vanished without an error.* "Warmup iterations" + and "Samples" were rendered with `matchingNat?`, which stops matching once two + rows of a group are calibrated separately. They are table columns now. + +**Effect on the run data**, `--medium`, curated set: rows with fewer than five +samples fell from 22 to 9 and rows at `n=1` from 14 to 2, while per-iteration +medians moved by at most 8.6% (whole distribution 0.878-1.086, median 1.006). +Calibration repeats within 1.01x over three runs. The curated timed run went +from 120.1s to 110.7s and `--validate-only --medium` from 36.1s to 33.0s — the +latter a small saving, as §11.4 item 3 predicted, because the cost sits in rows +validated exactly once. + +**What it does not fix.** Rows still reading `n=1` have single iterations that +genuinely exhaust the budget; their problem is input shape and no harness change +reaches it. With calibration in place a parameterised group can pick the largest +shape that fits its budget, which is the mechanism for that pass when it happens. +One thing is lost deliberately: `measured_iterations` is no longer comparable +across runs, since it depends on how fast the machine was during calibration. +`group_key`, `group_title`, and a per-run `manifest-.json` — commit, +dirty flag, toolchain, preset, resolved budgets, seed, selection, hardware — +are what replaces it for attribution. + +### 12.7 Coverage: base fields, transforms, and chained bodies (`dhsorens/bench-coverage`) + +Closes the coverage gaps §6.6 listed, and the ones §3.8 called invisible. 70 +groups became 113 and 290 records became 413; the curated correctness gate went +from 44 groups to 59 and got *cheaper*, ~34s to ~29s of CPU, for the reason in +finding 3 below. + +**The body shape had to change first.** `harness-floor` was 1.80 ns and +`goldilocks-mul-fast` 3.16 ns, and the generated C showed why: the operand-pool +idiom every group used, `xs.getD (i % xs.size) unit`, is two boxed-`Nat` +modulos, two bounds checks and two boxed array reads around one multiply. The +field operation was a rounding error in its own measurement. +`bench/CompPolyBench/Harness/Chain.lean` performs the operation `workUnits` times per iteration instead — no array, +because `lean_box_uint64` allocates and a one-cycle dependent chain cannot be +fed from a pointer array; no `for` with `let mut`, because `Prod` does not +erase; unrolled blocks, because the `Nat` counter costs more than a Montgomery +multiply. Latency and throughput are reported separately, as Plonky3 separates +them, and `harness-chain-floor` and `harness-chain-linearity` police the shape. + +Three findings worth keeping. + +1. **A `GF(2)`-linear chain folds, and the linearity check does not catch it.** + The chain floor's first operation was `x ^^^ (x >>> 7)`, the map `I + S`. In + characteristic two `(I + S) ^ 64 = I + S ^ 64`, and `S ^ 64` shifts right by + 448, so a 64-deep block *is the identity*. LLVM found it; the row reported + 15 ps per operation, a sixteenth of a cycle, and `harness-chain-linearity` + passed anyway, because what collapsed was each block and not the loop over + blocks. The floor operation must mix two algebras; a wrapping add carries + between bits and does not commute with the shift. + +2. **Loop-invariance is a second way to lose a body, distinct from closed-term + caching.** §12.6 finding 2 recorded the closed-term case. The NTT group hit + the other one: it precomputed its spectrum with the very expression the + forward reference row then timed, the compiler recognised the two as one, + and the row reported 6 ns for a `2^12` transform at every size identically. + Indexing a small pool by the iteration counter closes both, and every chained + and transform body here does. + +3. **BabyBear was missing an instance KoalaBear had.** + `KoalaBear/Basic.lean` declares `instance : Field Field := ZMod.instField + fieldSize`; `BabyBear/Basic.lean` did not. Putting the two fields side by + side in one group made canonical BabyBear `mul` show as 32.0 ns against + 5.9 ns for KoalaBear and Mersenne31 — the same shape, the same size of + prime. Adding the instance closed the gap exactly. This is the case + `CLAUDE.md` describes under Performance Guidelines, and it is the first + thing the coverage work paid for. + +**Blocked, and recorded so it is not rediscovered.** The polynomial-basis +`GF(2^64)` and its cubic extension still have no group. `BF64.instFintype` +(`CompPoly/Fields/Binary/BF64/Impl.lean:391`) is a closed constant whose value +is a `Finset` of all `2 ^ 64` elements, and Lean evaluates closed constants at +module initialisation — so any executable importing that module hangs before +`main` runs, `--list` included. Elaboration never notices, because the +interpreter forces constants on demand, which is why the tests build. Marking +it `noncomputable` is not the repair: `Extension.Ext` carries `[Fintype F]` and +its operations stop compiling. The fix belongs in +`CompPoly/Fields/Extension/`. + +**Also deferred**, with reasons: `sub` groups; a larger additive NTT, since +each `(k, ℓ, R_rate)` needs its own proof-carrying wrapper and the reference +row cannot survive `ℓ ≥ 8`; `batchInverse` / `sumOfProducts` / `dot_array`, +which Plonky3 benchmarks and CompPoly does not have; and prime-field `square`, +which is `mul x x` on every carrier here — Plonky3 has no field-level `square` +benchmark for the same reason. diff --git a/CompPoly.lean b/CompPoly.lean index d069815a..c768366a 100644 --- a/CompPoly.lean +++ b/CompPoly.lean @@ -14,10 +14,20 @@ public import CompPoly.Bivariate.GuruswamiSudan.Executable public import CompPoly.Bivariate.GuruswamiSudan.Filter public import CompPoly.Bivariate.GuruswamiSudan.FilterCorrectness public import CompPoly.Bivariate.GuruswamiSudan.Implementations +public import CompPoly.Bivariate.GuruswamiSudan.Interpolation +public import CompPoly.Bivariate.GuruswamiSudan.Interpolation.ApproximantBasis +public import CompPoly.Bivariate.GuruswamiSudan.Interpolation.ApproximantBasis.Algorithm +public import CompPoly.Bivariate.GuruswamiSudan.Interpolation.ApproximantBasis.Basic +public import CompPoly.Bivariate.GuruswamiSudan.Interpolation.ApproximantBasis.Correctness +public import CompPoly.Bivariate.GuruswamiSudan.Interpolation.ApproximantBasis.ModularData +public import CompPoly.Bivariate.GuruswamiSudan.Interpolation.ApproximantBasis.Multiplicity public import CompPoly.Bivariate.GuruswamiSudan.Interpolation.Basic public import CompPoly.Bivariate.GuruswamiSudan.Interpolation.Correctness public import CompPoly.Bivariate.GuruswamiSudan.Interpolation.Dense.Algorithm public import CompPoly.Bivariate.GuruswamiSudan.Interpolation.Dense.Correctness +public import CompPoly.Bivariate.GuruswamiSudan.Interpolation.Hybrid +public import CompPoly.Bivariate.GuruswamiSudan.Interpolation.Hybrid.Algorithm +public import CompPoly.Bivariate.GuruswamiSudan.Interpolation.Hybrid.Correctness public import CompPoly.Bivariate.GuruswamiSudan.Interpolation.LeeOSullivan public import CompPoly.Bivariate.GuruswamiSudan.Interpolation.LeeOSullivan.Algorithm public import CompPoly.Bivariate.GuruswamiSudan.Interpolation.LeeOSullivan.Basic @@ -31,6 +41,8 @@ public import CompPoly.Bivariate.GuruswamiSudan.Interpolation.LeeOSullivan.Corre public import CompPoly.Bivariate.GuruswamiSudan.Interpolation.LeeOSullivan.Correctness.Rows public import CompPoly.Bivariate.GuruswamiSudan.Interpolation.LeeOSullivan.Correctness.Selection public import CompPoly.Bivariate.GuruswamiSudan.Interpolation.LeeOSullivan.Correctness.Soundness +public import CompPoly.Bivariate.GuruswamiSudan.Interpolation.WitnessDivisibility +public import CompPoly.Bivariate.GuruswamiSudan.Interpolation.WitnessDivisibilityCorrectness public import CompPoly.Bivariate.GuruswamiSudan.Polynomial public import CompPoly.Bivariate.GuruswamiSudan.PolynomialCorrectness public import CompPoly.Bivariate.GuruswamiSudan.Root.Alekhnovich.Algorithm @@ -59,11 +71,14 @@ public import CompPoly.Data.Fin.BigOperators public import CompPoly.Data.List.Lemmas public import CompPoly.Data.MvPolynomial.Notation public import CompPoly.Data.Nat.Bitwise +public import CompPoly.Data.Option.Lemmas public import CompPoly.Data.Polynomial.Frobenius public import CompPoly.Data.Polynomial.MonomialBasis public import CompPoly.Data.Polynomial.Rabin public import CompPoly.Data.Polynomial.RabinCertificate public import CompPoly.Data.RingTheory.AlgebraTower +public import CompPoly.Data.RingTheory.AlgebraTower.Basis +public import CompPoly.Data.RingTheory.AlgebraTower.Coordinates public import CompPoly.Data.RingTheory.CanonicalEuclideanDomain public import CompPoly.Data.Vector.Basic public import CompPoly.Fields.BLS12_377 @@ -84,6 +99,7 @@ public import CompPoly.Fields.Binary.AdditiveNTT.AdditiveNTT public import CompPoly.Fields.Binary.AdditiveNTT.Algorithm public import CompPoly.Fields.Binary.AdditiveNTT.Correctness public import CompPoly.Fields.Binary.AdditiveNTT.Domain +public import CompPoly.Fields.Binary.AdditiveNTT.Executable public import CompPoly.Fields.Binary.AdditiveNTT.Impl public import CompPoly.Fields.Binary.AdditiveNTT.Intermediate public import CompPoly.Fields.Binary.AdditiveNTT.NovelPolynomialBasis @@ -92,6 +108,12 @@ public import CompPoly.Fields.Binary.BF128Ghash.Impl public import CompPoly.Fields.Binary.BF128Ghash.Prelude public import CompPoly.Fields.Binary.BF128Ghash.XPowTwoPowGcdCertificate public import CompPoly.Fields.Binary.BF128Ghash.XPowTwoPowModCertificate +public import CompPoly.Fields.Binary.BF64 +public import CompPoly.Fields.Binary.BF64.BaseCertificate +public import CompPoly.Fields.Binary.BF64.Basic +public import CompPoly.Fields.Binary.BF64.Ext3 +public import CompPoly.Fields.Binary.BF64.Impl +public import CompPoly.Fields.Binary.BF64.Reduce public import CompPoly.Fields.Binary.Common public import CompPoly.Fields.Binary.Tower.Abstract.Algebra public import CompPoly.Fields.Binary.Tower.Abstract.Basis @@ -100,9 +122,14 @@ public import CompPoly.Fields.Binary.Tower.Abstract.Split public import CompPoly.Fields.Binary.Tower.Basic public import CompPoly.Fields.Binary.Tower.Concrete.Algebra public import CompPoly.Fields.Binary.Tower.Concrete.Basis +public import CompPoly.Fields.Binary.Tower.Concrete.BasisCoordinates +public import CompPoly.Fields.Binary.Tower.Concrete.Coordinates public import CompPoly.Fields.Binary.Tower.Concrete.Core public import CompPoly.Fields.Binary.Tower.Concrete.Field +public import CompPoly.Fields.Binary.Tower.Concrete.RelativeCoordinates public import CompPoly.Fields.Binary.Tower.Equiv +public import CompPoly.Fields.Binary.Tower.Fast +public import CompPoly.Fields.Binary.Tower.FastDefs public import CompPoly.Fields.Binary.Tower.Impl public import CompPoly.Fields.Binary.Tower.Prelude public import CompPoly.Fields.Binary.Tower.Support.DefiningPoly @@ -112,11 +139,17 @@ public import CompPoly.Fields.Binary.Tower.Support.LinearIndependentFin2 public import CompPoly.Fields.Binary.Tower.Support.Preliminaries public import CompPoly.Fields.Binary.Tower.TensorAlgebra public import CompPoly.Fields.Extension +public import CompPoly.Fields.Extension.Arithmetic public import CompPoly.Fields.Extension.Binomial public import CompPoly.Fields.Extension.Bridge +public import CompPoly.Fields.Extension.Cardinality public import CompPoly.Fields.Extension.Defs public import CompPoly.Fields.Extension.Field public import CompPoly.Fields.Goldilocks +public import CompPoly.Fields.Goldilocks.Basic +public import CompPoly.Fields.Goldilocks.Fast +public import CompPoly.Fields.Goldilocks.FastDefs +public import CompPoly.Fields.Goldilocks.FastReduction public import CompPoly.Fields.Hachi public import CompPoly.Fields.Hachi.Ext4 public import CompPoly.Fields.KoalaBear @@ -130,7 +163,9 @@ public import CompPoly.Fields.KoalaBear.Ext6.GaloisField public import CompPoly.Fields.KoalaBear.Ext6.SexticCertData public import CompPoly.Fields.KoalaBear.Ext6.SexticIrreducible public import CompPoly.Fields.KoalaBear.Fast -public import CompPoly.Fields.Mersenne +public import CompPoly.Fields.Mersenne31 +public import CompPoly.Fields.Mersenne31.Basic +public import CompPoly.Fields.Mersenne31.Fast public import CompPoly.Fields.Montgomery.Basic public import CompPoly.Fields.Montgomery.Native32 public import CompPoly.Fields.Montgomery.Native32Field @@ -140,6 +175,10 @@ public import CompPoly.Fields.Montgomery.Native64x8Field public import CompPoly.Fields.Montgomery.Native64x8Inv public import CompPoly.Fields.Montgomery.Native64x8InvDefs public import CompPoly.Fields.Montgomery.Native64x8Mul +public import CompPoly.Fields.Montgomery.ScalarFft +public import CompPoly.Fields.Pasta +public import CompPoly.Fields.Pasta.Basic +public import CompPoly.Fields.Pasta.Fast public import CompPoly.Fields.PrattCertificate public import CompPoly.Fields.Secp256k1 public import CompPoly.LinearAlgebra.Dense @@ -153,6 +192,22 @@ public import CompPoly.LinearAlgebra.Dense.RowOpsCorrectness public import CompPoly.LinearAlgebra.Dense.RrefSemantics public import CompPoly.LinearAlgebra.Dense.RrefShape public import CompPoly.LinearAlgebra.PolynomialMatrix +public import CompPoly.LinearAlgebra.PolynomialMatrix.Approximant +public import CompPoly.LinearAlgebra.PolynomialMatrix.Approximant.Basic +public import CompPoly.LinearAlgebra.PolynomialMatrix.Approximant.Correctness +public import CompPoly.LinearAlgebra.PolynomialMatrix.Approximant.ModularEquation +public import CompPoly.LinearAlgebra.PolynomialMatrix.Approximant.ModularEquation.Basic +public import CompPoly.LinearAlgebra.PolynomialMatrix.Approximant.ModularEquation.Completeness +public import CompPoly.LinearAlgebra.PolynomialMatrix.Approximant.PMBasis +public import CompPoly.LinearAlgebra.PolynomialMatrix.Approximant.PMBasis.Correctness +public import CompPoly.LinearAlgebra.PolynomialMatrix.Approximant.PMBasis.KernelLeaf +public import CompPoly.LinearAlgebra.PolynomialMatrix.Approximant.PMBasis.KernelLeafCompleteness +public import CompPoly.LinearAlgebra.PolynomialMatrix.Approximant.PMBasis.KernelLeafScalar +public import CompPoly.LinearAlgebra.PolynomialMatrix.Approximant.PMBasis.KernelLeafSoundness +public import CompPoly.LinearAlgebra.PolynomialMatrix.Approximant.PMBasis.KernelLeafSpan +public import CompPoly.LinearAlgebra.PolynomialMatrix.Approximant.PMBasis.Recursion +public import CompPoly.LinearAlgebra.PolynomialMatrix.Approximant.PMBasis.XAdicSoundness +public import CompPoly.LinearAlgebra.PolynomialMatrix.Approximant.PartialLinearization public import CompPoly.LinearAlgebra.PolynomialMatrix.Basic public import CompPoly.LinearAlgebra.PolynomialMatrix.Degree public import CompPoly.LinearAlgebra.PolynomialMatrix.MuldersStorjohann @@ -166,9 +221,14 @@ public import CompPoly.LinearAlgebra.PolynomialMatrix.MuldersStorjohannCorrectne public import CompPoly.LinearAlgebra.PolynomialMatrix.MuldersStorjohannCorrectness.Minimal public import CompPoly.LinearAlgebra.PolynomialMatrix.MuldersStorjohannCorrectness.Reduction public import CompPoly.LinearAlgebra.PolynomialMatrix.MuldersStorjohannCorrectness.RowOps +public import CompPoly.LinearAlgebra.PolynomialMatrix.MuldersStorjohannCorrectness.WeakPopovMinimal +public import CompPoly.LinearAlgebra.PolynomialMatrix.Operations +public import CompPoly.LinearAlgebra.PolynomialMatrix.RowSelection public import CompPoly.LinearAlgebra.PolynomialMatrix.RowSpan public import CompPoly.LinearAlgebra.PolynomialMatrix.Shifted public import CompPoly.LinearAlgebra.PolynomialMatrix.ShiftedReduction +public import CompPoly.LinearAlgebra.PolynomialMatrix.StrassenCorrectness +public import CompPoly.LinearAlgebra.TensorProduct.Basis public import CompPoly.Multilinear.Basic public import CompPoly.Multilinear.Equiv public import CompPoly.Multilinear.ManyEval @@ -186,11 +246,11 @@ public import CompPoly.Multivariate.MvPolyEquiv.Core public import CompPoly.Multivariate.MvPolyEquiv.Eval public import CompPoly.Multivariate.MvPolyEquiv.Instances public import CompPoly.Multivariate.Operations +public import CompPoly.Multivariate.PartialEval public import CompPoly.Multivariate.Rename public import CompPoly.Multivariate.Restrict public import CompPoly.Multivariate.Unlawful public import CompPoly.Multivariate.VarsDegrees -public import CompPoly.Multivariate.Wheels public import CompPoly.ToMathlib.Finsupp.Fin public import CompPoly.ToMathlib.MvPolynomial.Equiv public import CompPoly.ToMathlib.Order.WithBot @@ -263,7 +323,27 @@ public import CompPoly.Univariate.Roots.Context public import CompPoly.Univariate.Roots.Correctness public import CompPoly.Univariate.Roots.Enumeration public import CompPoly.Univariate.Roots.Extraction +public import CompPoly.Univariate.Roots.LasVegas +public import CompPoly.Univariate.Roots.LasVegas.Basic +public import CompPoly.Univariate.Roots.LasVegas.Correctness +public import CompPoly.Univariate.Roots.LasVegas.Correctness.Common +public import CompPoly.Univariate.Roots.LasVegas.Correctness.EvenTrace +public import CompPoly.Univariate.Roots.LasVegas.Correctness.Loop +public import CompPoly.Univariate.Roots.LasVegas.Correctness.Odd +public import CompPoly.Univariate.Roots.LasVegas.OddBucket +public import CompPoly.Univariate.Roots.LasVegas.Probability +public import CompPoly.Univariate.Roots.LasVegas.Probability.Basic +public import CompPoly.Univariate.Roots.LasVegas.Probability.EvenTrace +public import CompPoly.Univariate.Roots.LasVegas.Probability.OddBuckets +public import CompPoly.Univariate.Roots.LasVegas.Probability.OddTrial +public import CompPoly.Univariate.Roots.LasVegas.Probability.Recursive +public import CompPoly.Univariate.Roots.LasVegas.Probability.Repeated +public import CompPoly.Univariate.Roots.LasVegas.Probability.Uniform public import CompPoly.Univariate.Roots.RootProduct +public import CompPoly.Univariate.Roots.Shoup +public import CompPoly.Univariate.Roots.Shoup.Basic +public import CompPoly.Univariate.Roots.Shoup.Correctness +public import CompPoly.Univariate.Roots.Shoup.FrobeniusLinear public import CompPoly.Univariate.Roots.SmoothSubgroup public import CompPoly.Univariate.Roots.SmoothSubgroup.Basic public import CompPoly.Univariate.Roots.SmoothSubgroup.Correctness diff --git a/CompPoly/Bivariate/Deriv.lean b/CompPoly/Bivariate/Deriv.lean index 9ec9f3e7..44311c58 100644 --- a/CompPoly/Bivariate/Deriv.lean +++ b/CompPoly/Bivariate/Deriv.lean @@ -370,6 +370,29 @@ theorem hasMultiplicity_succ [CommSemiring R] [BEq R] [LawfulBEq R] [Nontrivial intro h i j hij exact h i j (by omega) +/-- `toPoly` is injective on canonical bivariate polynomials. -/ +theorem toPoly_injective [Semiring R] [BEq R] [LawfulBEq R] [Nontrivial R] + [DecidableEq R] {P Q : CBivariate R} (h : toPoly P = toPoly Q) : P = Q := by + rw [← toPoly_ofPoly P, ← toPoly_ofPoly Q, h] + +/-- The generic Taylor shift is additive. -/ +theorem shiftC_add [CommSemiring R] [BEq R] [LawfulBEq R] [Nontrivial R] [DecidableEq R] + (a b : R) (P Q : CBivariate R) : + shiftC a b (P + Q) = shiftC a b P + shiftC a b Q := by + apply toPoly_injective + rw [toPoly_add, shiftC_toPoly, shiftC_toPoly, shiftC_toPoly, toPoly_add] + unfold Polynomial.Bivariate.shift + rw [Polynomial.add_comp, Polynomial.map_add] + +/-- The generic Taylor shift is multiplicative. -/ +theorem shiftC_mul [CommSemiring R] [BEq R] [LawfulBEq R] [Nontrivial R] + [DecidableEq R] (a b : R) (P Q : CBivariate R) : + shiftC a b (P * Q) = shiftC a b P * shiftC a b Q := by + apply toPoly_injective + rw [toPoly_mul, shiftC_toPoly, shiftC_toPoly, shiftC_toPoly, toPoly_mul] + unfold Polynomial.Bivariate.shift + rw [Polynomial.mul_comp, Polynomial.map_mul] + /-- The decidable check agrees with the propositional multiplicity. -/ theorem hasMultiplicity_iff_check [CommSemiring R] [BEq R] [LawfulBEq R] [Nontrivial R] [DecidableEq R] (Q : CBivariate R) (r : ℕ) (a b : R) : diff --git a/CompPoly/Bivariate/GuruswamiSudan/Implementations.lean b/CompPoly/Bivariate/GuruswamiSudan/Implementations.lean index c75d526c..bcf32ec4 100644 --- a/CompPoly/Bivariate/GuruswamiSudan/Implementations.lean +++ b/CompPoly/Bivariate/GuruswamiSudan/Implementations.lean @@ -7,7 +7,9 @@ module public import CompPoly.Bivariate.GuruswamiSudan.Executable public import CompPoly.Bivariate.GuruswamiSudan.Interpolation.Dense.Correctness +public import CompPoly.Bivariate.GuruswamiSudan.Interpolation.Hybrid.Correctness public import CompPoly.Bivariate.GuruswamiSudan.Interpolation.LeeOSullivan.Correctness +public import CompPoly.Bivariate.GuruswamiSudan.Root.Alekhnovich.Correctness public import CompPoly.Bivariate.GuruswamiSudan.Root.FieldRoots.KoalaBear public import CompPoly.Bivariate.GuruswamiSudan.Root.RothRuckenstein.Correctness public import CompPoly.LinearAlgebra.PolynomialMatrix.MuldersStorjohannCorrectness.Fast @@ -17,8 +19,9 @@ public import CompPoly.Univariate.NTT.KoalaBear /-! # Guruswami-Sudan Concrete Implementations -Named concrete dense and Lee-O'Sullivan interpolation/Roth-Ruckenstein -implementations and correctness theorem specializations for the decoder surface. +Named concrete dense-interpolation/Roth-Ruckenstein implementations and +correctness theorem specializations for the implementations exercised by the +benchmark suite. -/ @[expose] public section @@ -39,6 +42,13 @@ def fastKoalaBearDenseInterpContext : GSInterpContext KoalaBear.Fast.Field := def koalaBearNttFastMulContext : CPolynomial.MulContext KoalaBear.Field := CPolynomial.MulContext.nttFast CPolynomial.NTT.KoalaBear.bestDomainForLength? +/-- NTTFast-backed low univariate multiplication over canonical KoalaBear. -/ +def koalaBearNttFastLowMulContext : + PolynomialMatrix.MulLowContext KoalaBear.Field := + PolynomialMatrix.MulLowContext.raw koalaBearNttFastMulContext + (CPolynomial.NTTFast.FastMulLow.withFallback + CPolynomial.NTT.KoalaBear.bestDomainForLength?) + /-- NTTFast-backed univariate monic remainders over canonical KoalaBear. -/ def koalaBearNttFastModContext : CPolynomial.ModContext KoalaBear.Field := CPolynomial.ModContext.reversalNttFast CPolynomial.NTT.KoalaBear.bestDomainForLength? @@ -52,6 +62,13 @@ def koalaBearNttFastBatchEvalContext : CPolynomial.BatchEvalContext KoalaBear.Fi def fastKoalaBearNttFastMulContext : CPolynomial.MulContext KoalaBear.Fast.Field := CPolynomial.MulContext.nttFast CPolynomial.NTT.KoalaBear.fastBestDomainForLength? +/-- NTTFast-backed low univariate multiplication over native-word fast KoalaBear. -/ +def fastKoalaBearNttFastLowMulContext : + PolynomialMatrix.MulLowContext KoalaBear.Fast.Field := + PolynomialMatrix.MulLowContext.raw fastKoalaBearNttFastMulContext + (CPolynomial.NTTFast.FastMulLow.withFallback + CPolynomial.NTT.KoalaBear.fastBestDomainForLength?) + /-- NTTFast-backed univariate monic remainders over native-word fast KoalaBear. -/ def fastKoalaBearNttFastModContext : CPolynomial.ModContext KoalaBear.Fast.Field := CPolynomial.ModContext.reversalNttFast CPolynomial.NTT.KoalaBear.fastBestDomainForLength? @@ -93,6 +110,117 @@ def fastKoalaBearLeeSubproductInterpContext : GSInterpContext KoalaBear.Fast.Fie fastKoalaBearNttFastBatchEvalContext (PolynomialMatrix.muldersStorjohannFastReducerContext KoalaBear.Fast.Field) +/-- PM-basis scalar-kernel cutoff for the approximant-basis interpolation backend. + +The recursive solver handles all larger orders with low-product residuals and +block matrix composition; dense scalar linear algebra is reserved for +small bounded leaves. -/ +def approximantPMBasisLeafCutoff : Nat := 8 + +/-- Polynomial-matrix basis-composition cutoff for the approximant backend. +The current GS interpolation shapes are narrow enough that direct bounded +composition is faster than recursing Strassen down to unit blocks. -/ +def approximantPMBasisComposeLeafCutoff : Nat := 8 + +/-- Recursive approximant-basis PM-basis context over canonical KoalaBear. -/ +def koalaBearApproximantPMBasisContext : + PolynomialMatrix.Approximant.PMBasisContext KoalaBear.Field := + PolynomialMatrix.Approximant.kernelLeafPMBasisContextWithLowAndCompose + koalaBearNttFastMulContext koalaBearNttFastLowMulContext + approximantPMBasisLeafCutoff approximantPMBasisComposeLeafCutoff + +/-- Diagonal modular-equation solution context over canonical KoalaBear. -/ +def koalaBearApproximantSolutionContext : + PolynomialMatrix.Approximant.ModularSolutionBasisContext KoalaBear.Field := + PolynomialMatrix.Approximant.modularSolutionBasisContextViaPMBasis + koalaBearNttFastMulContext koalaBearNttFastModContext + koalaBearApproximantPMBasisContext + +/-- Approximant-basis interpolation over canonical KoalaBear. -/ +def koalaBearApproximantBasisDirectInterpContext : GSInterpContext KoalaBear.Field := + ApproximantBasis.approximantBasisInterpContext + (CPolynomial.VanishingPolynomialContext.direct (F := KoalaBear.Field)) + (CPolynomial.BatchEvalContext.horner KoalaBear.Field) + koalaBearApproximantSolutionContext + +/-- Approximant-basis interpolation over canonical KoalaBear with +subproduct-tree vanishing setup. -/ +def koalaBearApproximantBasisSubproductInterpContext : GSInterpContext KoalaBear.Field := + ApproximantBasis.approximantBasisInterpContext + (CPolynomial.VanishingPolynomialContext.subproduct + koalaBearNttFastMulContext) + koalaBearNttFastBatchEvalContext + koalaBearApproximantSolutionContext + +/-- Default approximant-basis interpolation over canonical KoalaBear. -/ +def koalaBearApproximantBasisInterpContext : GSInterpContext KoalaBear.Field := + koalaBearApproximantBasisSubproductInterpContext + +/-- Recursive approximant-basis PM-basis context over native-word fast KoalaBear. -/ +def fastKoalaBearApproximantPMBasisContext : + PolynomialMatrix.Approximant.PMBasisContext KoalaBear.Fast.Field := + PolynomialMatrix.Approximant.kernelLeafPMBasisContextWithLowAndCompose + fastKoalaBearNttFastMulContext fastKoalaBearNttFastLowMulContext + approximantPMBasisLeafCutoff approximantPMBasisComposeLeafCutoff + +/-- Diagonal modular-equation solution context over native-word fast KoalaBear. -/ +def fastKoalaBearApproximantSolutionContext : + PolynomialMatrix.Approximant.ModularSolutionBasisContext KoalaBear.Fast.Field := + PolynomialMatrix.Approximant.modularSolutionBasisContextViaPMBasis + fastKoalaBearNttFastMulContext fastKoalaBearNttFastModContext + fastKoalaBearApproximantPMBasisContext + +/-- Approximant-basis interpolation over native-word fast KoalaBear. -/ +def fastKoalaBearApproximantBasisDirectInterpContext : + GSInterpContext KoalaBear.Fast.Field := + ApproximantBasis.approximantBasisInterpContext + (CPolynomial.VanishingPolynomialContext.direct (F := KoalaBear.Fast.Field)) + (CPolynomial.BatchEvalContext.horner KoalaBear.Fast.Field) + fastKoalaBearApproximantSolutionContext + +/-- Approximant-basis interpolation over native-word fast KoalaBear with +subproduct-tree vanishing setup. -/ +def fastKoalaBearApproximantBasisSubproductInterpContext : + GSInterpContext KoalaBear.Fast.Field := + ApproximantBasis.approximantBasisInterpContext + (CPolynomial.VanishingPolynomialContext.subproduct + fastKoalaBearNttFastMulContext) + fastKoalaBearNttFastBatchEvalContext + fastKoalaBearApproximantSolutionContext + +/-- Default approximant-basis interpolation over native-word fast KoalaBear. -/ +def fastKoalaBearApproximantBasisInterpContext : + GSInterpContext KoalaBear.Fast.Field := + fastKoalaBearApproximantBasisSubproductInterpContext + +/-- Mulders-Storjohann step budget for the hybrid interpolation backend: the +ski-rental rent/buy break-even, set near the cost ratio between one +approximant-fallback solve and one reduction step. Both scale with the input +mass, so the ratio is proportional to `ℓ^(ω−1)` (with `ℓ + 1` the module +width) and independent of `n` and `m` under softly-linear multiplication; +the constant is calibrated from the `n = 5040` long-code benchmark shape. -/ +def hybridReductionStepBudget (params : GSInterpParams) : Nat := + 500 * leeOSullivanWidth params * leeOSullivanWidth params + +/-- Hybrid interpolation (budgeted Lee-O'Sullivan reduction with approximant +fallback) over canonical KoalaBear. -/ +def koalaBearHybridInterpContext : GSInterpContext KoalaBear.Field := + Hybrid.hybridInterpContext + (CPolynomial.VanishingPolynomialContext.subproduct koalaBearNttFastMulContext) + koalaBearNttFastBatchEvalContext + koalaBearApproximantSolutionContext + hybridReductionStepBudget + +/-- Hybrid interpolation (budgeted Lee-O'Sullivan reduction with approximant +fallback) over native-word fast KoalaBear. -/ +def fastKoalaBearHybridInterpContext : GSInterpContext KoalaBear.Fast.Field := + Hybrid.hybridInterpContext + (CPolynomial.VanishingPolynomialContext.subproduct + fastKoalaBearNttFastMulContext) + fastKoalaBearNttFastBatchEvalContext + fastKoalaBearApproximantSolutionContext + hybridReductionStepBudget + /-- Roth-Ruckenstein root backend over canonical KoalaBear. -/ def koalaBearRothRootContext : GSRootContext KoalaBear.Field := rothRuckensteinRootContext KoalaBear.Field koalaBearFieldRootContext @@ -109,6 +237,22 @@ def fastKoalaBearRothRootContext : GSRootContext KoalaBear.Fast.Field := def fastKoalaBearRothNttFastRootContext : GSRootContext KoalaBear.Fast.Field := rothRuckensteinRootContext KoalaBear.Fast.Field fastKoalaBearNttFastFieldRootContext +/-- Alekhnovich root backend over canonical KoalaBear. -/ +def koalaBearAlekhnovichRootContext : GSRootContext KoalaBear.Field := + alekhnovichRootContext KoalaBear.Field koalaBearFieldRootContext + +/-- Alekhnovich root backend over canonical KoalaBear with NTTFast field roots. -/ +def koalaBearAlekhnovichNttFastRootContext : GSRootContext KoalaBear.Field := + alekhnovichRootContext KoalaBear.Field koalaBearNttFastFieldRootContext + +/-- Alekhnovich root backend over native-word fast KoalaBear. -/ +def fastKoalaBearAlekhnovichRootContext : GSRootContext KoalaBear.Fast.Field := + alekhnovichRootContext KoalaBear.Fast.Field fastKoalaBearFieldRootContext + +/-- Alekhnovich root backend over native-word fast KoalaBear with NTTFast field roots. -/ +def fastKoalaBearAlekhnovichNttFastRootContext : GSRootContext KoalaBear.Fast.Field := + alekhnovichRootContext KoalaBear.Fast.Field fastKoalaBearNttFastFieldRootContext + /-- Filtered dense/Roth context over canonical KoalaBear. -/ def koalaBearDenseRothContext : GSFilteredCoreContext KoalaBear.Field := filteredCoreContextOfInterpRootContexts koalaBearDenseInterpContext koalaBearRothRootContext diff --git a/CompPoly/Bivariate/GuruswamiSudan/Interpolation.lean b/CompPoly/Bivariate/GuruswamiSudan/Interpolation.lean new file mode 100644 index 00000000..3609359f --- /dev/null +++ b/CompPoly/Bivariate/GuruswamiSudan/Interpolation.lean @@ -0,0 +1,22 @@ +/- +Copyright (c) 2026 CompPoly Contributors. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Valerii Huhnin +-/ + +module + +public import CompPoly.Bivariate.GuruswamiSudan.Interpolation.Correctness +public import CompPoly.Bivariate.GuruswamiSudan.Interpolation.Dense.Correctness +public import CompPoly.Bivariate.GuruswamiSudan.Interpolation.LeeOSullivan.Correctness +public import CompPoly.Bivariate.GuruswamiSudan.Interpolation.ApproximantBasis.Correctness +public import CompPoly.Bivariate.GuruswamiSudan.Interpolation.Hybrid.Correctness + +/-! +# Guruswami-Sudan Interpolation + +Public interpolation surface for certified Guruswami-Sudan interpolation +backends. +-/ + +@[expose] public section diff --git a/CompPoly/Bivariate/GuruswamiSudan/Interpolation/ApproximantBasis.lean b/CompPoly/Bivariate/GuruswamiSudan/Interpolation/ApproximantBasis.lean new file mode 100644 index 00000000..fefd75ab --- /dev/null +++ b/CompPoly/Bivariate/GuruswamiSudan/Interpolation/ApproximantBasis.lean @@ -0,0 +1,17 @@ +/- +Copyright (c) 2026 CompPoly Contributors. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Valerii Huhnin +-/ + +module + +public import CompPoly.Bivariate.GuruswamiSudan.Interpolation.ApproximantBasis.Basic +public import CompPoly.Bivariate.GuruswamiSudan.Interpolation.ApproximantBasis.Algorithm +public import CompPoly.Bivariate.GuruswamiSudan.Interpolation.ApproximantBasis.Correctness + +/-! +# Approximant-Basis Guruswami-Sudan Interpolation +-/ + +@[expose] public section diff --git a/CompPoly/Bivariate/GuruswamiSudan/Interpolation/ApproximantBasis/Algorithm.lean b/CompPoly/Bivariate/GuruswamiSudan/Interpolation/ApproximantBasis/Algorithm.lean new file mode 100644 index 00000000..2238a7c8 --- /dev/null +++ b/CompPoly/Bivariate/GuruswamiSudan/Interpolation/ApproximantBasis/Algorithm.lean @@ -0,0 +1,85 @@ +/- +Copyright (c) 2026 CompPoly Contributors. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Valerii Huhnin +-/ + +module + +public import CompPoly.Bivariate.GuruswamiSudan.Interpolation.ApproximantBasis.Basic + +/-! +# Executable Approximant-Basis Interpolation + +This backend constructs the GS diagonal modular equations, calls an explicit +solution-basis context, selects a least shifted-degree solution row, and +normalizes the resulting bivariate polynomial. + +## References + +* [Chowdhury, M. F. I., Jeannerod, C.-P., Neiger, V., Schost, E., and + Villard, G., *Faster algorithms for multivariate interpolation with + multiplicities and simultaneous polynomial approximations*][CJNSV15] +-/ + +@[expose] public section + +namespace CompPoly + +namespace GuruswamiSudan + +namespace ApproximantBasis + +open PolynomialMatrix +open PolynomialMatrix.Approximant + +variable {F : Type*} [Field F] [BEq F] [LawfulBEq F] [DecidableEq F] + +/-- Normalize a row-derived approximant candidate using the shared interpolation +vector policy. -/ +def normalizeApproximantCandidate? (params : GSInterpParams) (Q : CBivariate F) : + Option (CBivariate F) := + normalizeInterpolationPolynomial? params + (interpolationCoefficientVector params Q) + +/-- Positive-`Y`-weight approximant-basis interpolation branch. -/ +def approximantBasisPositiveInterpolate + (V : CPolynomial.VanishingPolynomialContext F) + (E : CPolynomial.BatchEvalContext F) + (solver : ModularSolutionBasisContext F) + (points : Array (F × F)) (params : GSInterpParams) : + Option (CBivariate F) := + if distinctXCoordinatesBool points then + let G := V.vanishingPolynomial (points.map fun point ↦ point.1) + let R := CPolynomial.interpolateCoefficientFormWithVanishing E G points + let data := buildGSModularDataWithRG solver.mulContext solver.modContext R G params + let basis := solver.solutionBasis (modularEquation data) data.shift + (some params.weightedDegreeBound) + match leastShiftedDegreeChoice? basis data.shift with + | none => none + | some choice => + if choice.degree ≤ params.weightedDegreeBound then + let rawQ := CBivariate.ofCoeffRow choice.row + normalizeApproximantCandidate? params rawQ + else + none + else + none + +/-- Approximant-basis interpolation with the shared low-message branch. -/ +def approximantBasisInterpolate + (V : CPolynomial.VanishingPolynomialContext F) + (E : CPolynomial.BatchEvalContext F) + (solver : ModularSolutionBasisContext F) + (points : Array (F × F)) (params : GSInterpParams) : + Option (CBivariate F) := + if params.messageDegree ≤ 1 then + some (lowMessageDegreeInterpolation points params.multiplicity) + else + approximantBasisPositiveInterpolate V E solver points params + +end ApproximantBasis + +end GuruswamiSudan + +end CompPoly diff --git a/CompPoly/Bivariate/GuruswamiSudan/Interpolation/ApproximantBasis/Basic.lean b/CompPoly/Bivariate/GuruswamiSudan/Interpolation/ApproximantBasis/Basic.lean new file mode 100644 index 00000000..7616b872 --- /dev/null +++ b/CompPoly/Bivariate/GuruswamiSudan/Interpolation/ApproximantBasis/Basic.lean @@ -0,0 +1,142 @@ +/- +Copyright (c) 2026 CompPoly Contributors. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Valerii Huhnin +-/ + +module + +public import CompPoly.Bivariate.CoeffRows +public import CompPoly.Bivariate.GuruswamiSudan.Interpolation.Basic +public import CompPoly.LinearAlgebra.PolynomialMatrix.Approximant +public import CompPoly.Univariate.CoefficientInterpolation +public import CompPoly.Univariate.Vanishing + +/-! +# Approximant-Basis Guruswami-Sudan Modular Data + +Construction of the diagonal modular equations used by the approximant-basis +interpolation backend. + +## References + +* [Chowdhury, M. F. I., Jeannerod, C.-P., Neiger, V., Schost, E., and + Villard, G., *Faster algorithms for multivariate interpolation with + multiplicities and simultaneous polynomial approximations*][CJNSV15] +-/ + +@[expose] public section + +namespace CompPoly + +namespace GuruswamiSudan + +namespace ApproximantBasis + +open PolynomialMatrix +open PolynomialMatrix.Approximant + +variable {F : Type*} [Field F] [BEq F] [LawfulBEq F] [DecidableEq F] + +/-- GS column moduli `M_b = G^(s-b)`, for `b = 0, ..., s-1`, built by iterated +context multiplication so each power costs one fast product. -/ +def gsModuli (mulCtx : CPolynomial.MulContext F) (G : CPolynomial F) (s : Nat) : + Array (CPolynomial F) := Id.run do + if s == 0 then + return #[] + let mut ascending : Array (CPolynomial F) := #[G] + for _ in [1:s] do + ascending := ascending.push (mulCtx.mul (ascending.getD (ascending.size - 1) 0) G) + return ascending.reverse + +/-- Specification for one relation-matrix entry +`choose(j,b) * R^(j-b) mod M_b`, with zero below the triangular support. +This is the reference definition; the production path builds whole columns +incrementally with `gsRelationColumn`. -/ +def gsRelationEntry (modCtx : CPolynomial.ModContext F) + (R modulus : CPolynomial F) (j b : Nat) : CPolynomial F := + if b ≤ j then + PolynomialMatrix.modByMonicWith modCtx + (CPolynomial.C (Nat.choose j b : F) * R ^ (j - b)) modulus + else + 0 + +/-- One relation-matrix column, built by iterated multiply-and-reduce. Entry +`j` of column `b` is `choose(j,b) * R^(j-b) mod M_b`; the reduced power of `R` +is carried across entries so each step costs one context multiplication of +operands already reduced below `deg M_b` plus one context remainder. -/ +def gsRelationColumn (mulCtx : CPolynomial.MulContext F) + (modCtx : CPolynomial.ModContext F) + (R modulus : CPolynomial F) (width b : Nat) : + Array (CPolynomial F) := Id.run do + let mut column := Array.replicate width (0 : CPolynomial F) + let reducedR := PolynomialMatrix.modByMonicWith modCtx R modulus + let mut power : CPolynomial F := 1 + for j in [b:width] do + column := column.setIfInBounds j (CPolynomial.C (Nat.choose j b : F) * power) + power := PolynomialMatrix.modByMonicWith modCtx (mulCtx.mul power reducedR) modulus + return column + +/-- GS relation matrix for the congruences `p * Fmat = 0 mod (G^s, ..., G)`, +assembled from incrementally built columns over precomputed moduli. -/ +def gsRelationMatrixWithModuli (mulCtx : CPolynomial.MulContext F) + (modCtx : CPolynomial.ModContext F) + (R : CPolynomial F) (moduli : Array (CPolynomial F)) + (params : GSInterpParams) : PolynomialMatrix F := + let width := interpolationWidth params + let columns := (List.range params.multiplicity).map + (fun b ↦ gsRelationColumn mulCtx modCtx R (moduli.getD b 1) width b) |>.toArray + PolynomialMatrix.ofFn width params.multiplicity fun j b ↦ + (columns.getD b #[]).getD j 0 + +/-- GS relation matrix for the congruences +`p * Fmat = 0 mod (G^s, ..., G)`. -/ +def gsRelationMatrixWithRG (mulCtx : CPolynomial.MulContext F) + (modCtx : CPolynomial.ModContext F) + (R G : CPolynomial F) (params : GSInterpParams) : PolynomialMatrix F := + gsRelationMatrixWithModuli mulCtx modCtx R + (gsModuli mulCtx G params.multiplicity) params + +/-- Complete GS modular-equation data for the approximant backend. -/ +structure GSModularData (F : Type*) [Zero F] where + G : CPolynomial F + R : CPolynomial F + moduli : Array (CPolynomial F) + matrix : PolynomialMatrix F + shift : Array Nat + +/-- Build column moduli, the binomial relation matrix, and the GS shift array +from precomputed interpolation polynomials `R` and `G`. The moduli are +computed once and shared with the relation-matrix construction. -/ +def buildGSModularDataWithRG + (mulCtx : CPolynomial.MulContext F) + (modCtx : CPolynomial.ModContext F) + (R G : CPolynomial F) (params : GSInterpParams) : GSModularData F := + let moduli := gsModuli mulCtx G params.multiplicity + { G := G + R := R + moduli := moduli + matrix := gsRelationMatrixWithModuli mulCtx modCtx R moduli params + shift := interpolationShifts params } + +/-- Build `G`, `R`, column moduli, the binomial relation matrix, and the GS +shift array. -/ +def buildGSModularData + (V : CPolynomial.VanishingPolynomialContext F) + (E : CPolynomial.BatchEvalContext F) + (mulCtx : CPolynomial.MulContext F) + (modCtx : CPolynomial.ModContext F) + (points : Array (F × F)) (params : GSInterpParams) : GSModularData F := + let G := V.vanishingPolynomial (points.map fun point ↦ point.1) + let R := CPolynomial.interpolateCoefficientFormWithVanishing E G points + buildGSModularDataWithRG mulCtx modCtx R G params + +/-- Modular-equation view of GS modular data. -/ +def modularEquation (data : GSModularData F) : ModularEquation F := + { moduli := data.moduli, matrix := data.matrix } + +end ApproximantBasis + +end GuruswamiSudan + +end CompPoly diff --git a/CompPoly/Bivariate/GuruswamiSudan/Interpolation/ApproximantBasis/Correctness.lean b/CompPoly/Bivariate/GuruswamiSudan/Interpolation/ApproximantBasis/Correctness.lean new file mode 100644 index 00000000..aae50be4 --- /dev/null +++ b/CompPoly/Bivariate/GuruswamiSudan/Interpolation/ApproximantBasis/Correctness.lean @@ -0,0 +1,646 @@ +/- +Copyright (c) 2026 CompPoly Contributors. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Valerii Huhnin +-/ + +module + +-- `linearFactor`, `coeff` and friends are declared in bare `public section`s, so +-- their bodies are opaque downstream; see `docs/wiki/module-system.md`. +import all CompPoly.Univariate.Basic +import all CompPoly.Univariate.Raw.Core +public import CompPoly.Bivariate.GuruswamiSudan.Interpolation.ApproximantBasis.Algorithm +public import CompPoly.Bivariate.GuruswamiSudan.Interpolation.ApproximantBasis.ModularData +public import CompPoly.Bivariate.GuruswamiSudan.Interpolation.Correctness +public import CompPoly.Bivariate.GuruswamiSudan.Interpolation.LeeOSullivan.Correctness.Normalization +public import CompPoly.Bivariate.GuruswamiSudan.Interpolation.LeeOSullivan.Correctness.Rows +public import CompPoly.LinearAlgebra.PolynomialMatrix.RowSelection + +/-! +# Approximant-Basis Interpolation Correctness Surface + +Theorem statements for the modular-equation reduction and the public +`GSInterpContext` boundary. +-/ + +@[expose] public section + +namespace CompPoly + +namespace GuruswamiSudan + +namespace ApproximantBasis + +open PolynomialMatrix +open PolynomialMatrix.Approximant + +variable {F : Type*} [Field F] [BEq F] [LawfulBEq F] [DecidableEq F] + +/-! ## Width truncation helpers -/ + +omit [DecidableEq F] in +/-- Entries of the width truncation of a coefficient row. -/ +private theorem rowGet_toCoeffRow_ofCoeffRow (row : PolynomialRow F) + (width : Nat) {j : Nat} (hj : j < width) : + rowGet (CBivariate.toCoeffRow width (CBivariate.ofCoeffRow row)) j = + rowGet row j := by + rw [CBivariate.toCoeffRow, rowGet, Array.getD_eq_getD_getElem?, + List.getElem?_toArray, List.getElem?_map, List.getElem?_range hj, + Option.map_some, Option.getD_some, CBivariate.ofCoeffRow, + CPolynomial.coeff_ofArray] + rfl + +omit [DecidableEq F] in +/-- Width truncation preserves all coefficients below the width. -/ +private theorem coeff_toCoeffRow_ofCoeffRow (row : PolynomialRow F) + {width i j : Nat} (hj : j < width) : + CBivariate.coeff + (CBivariate.ofCoeffRow (CBivariate.toCoeffRow width + (CBivariate.ofCoeffRow row))) i j = + CBivariate.coeff (CBivariate.ofCoeffRow row) i j := by + have hsize : (CBivariate.toCoeffRow width (CBivariate.ofCoeffRow row)).size = + width := CBivariate.toCoeffRow_size _ _ + rw [CBivariate.coeff_ofCoeffRow_of_lt _ (by omega)] + rw [show (CBivariate.toCoeffRow width (CBivariate.ofCoeffRow row)).getD j 0 = + rowGet (CBivariate.toCoeffRow width (CBivariate.ofCoeffRow row)) j from rfl] + rw [rowGet_toCoeffRow_ofCoeffRow row width hj] + rcases Nat.lt_or_ge j row.size with hjrow | hjrow + · rw [CBivariate.coeff_ofCoeffRow_of_lt _ hjrow] + rfl + · rw [CBivariate.coeff_ofCoeffRow_of_size_le _ hjrow] + rw [show rowGet row j = row.getD j 0 from rfl] + rw [Array.getD_eq_getD_getElem?, Array.getElem?_eq_none hjrow] + exact CPolynomial.coeff_zero i + +omit [DecidableEq F] in +/-- Every interpolation monomial has `Y`-degree below the interpolation width +when the `Y`-weight is positive. -/ +private theorem interpolationMonomials_yDegree_lt (params : GSInterpParams) + (hw : 0 < yWeight params) {m : CBivariate.Monomial} + (hm : m ∈ (interpolationMonomials params).toList) : + m.yDegree < interpolationWidth params := by + have hbound := CBivariate.monomialsWeightedDegreeLE_sound + (xWeight := 1) (yWeight := yWeight params) + (bound := params.weightedDegreeBound) (by simpa [interpolationMonomials] using hm) + have hy : yWeight params * m.yDegree ≤ params.weightedDegreeBound := by omega + have hycap : m.yDegree ≤ interpolationYCap params := by + rw [interpolationYCap, Nat.le_div_iff_mul_le hw] + calc m.yDegree * yWeight params = yWeight params * m.yDegree := + Nat.mul_comm _ _ + _ ≤ params.weightedDegreeBound := by omega + rw [interpolationWidth] + omega + +omit [DecidableEq F] in +/-- The interpolation coefficient vector only sees the first +`interpolationWidth params` coefficient rows. -/ +private theorem interpolationCoefficientVector_toCoeffRow (params : GSInterpParams) + (hw : 0 < yWeight params) (row : PolynomialRow F) : + interpolationCoefficientVector params + (CBivariate.ofCoeffRow (CBivariate.toCoeffRow (interpolationWidth params) + (CBivariate.ofCoeffRow row))) = + interpolationCoefficientVector params (CBivariate.ofCoeffRow row) := by + rw [interpolationCoefficientVector, interpolationCoefficientVector, + interpolationCoefficientVectorOnBasis, interpolationCoefficientVectorOnBasis] + apply Array.ext + · simp + · intro i hi _hi' + have hi' : i < (interpolationMonomials params).size := by + simpa using hi + simp only [Array.getElem_map] + exact coeff_toCoeffRow_ofCoeffRow row + (interpolationMonomials_yDegree_lt params hw + (Array.getElem_mem_toList hi')) + +omit [DecidableEq F] in +/-- Width truncation does not increase the shifted row degree. -/ +private theorem rowShiftedDegree?_toCoeffRow_le {row : PolynomialRow F} + {shift : Array Nat} {width d d' : Nat} + (hd : rowShiftedDegree? row shift = some d) + (hd' : rowShiftedDegree? + (CBivariate.toCoeffRow width (CBivariate.ofCoeffRow row)) shift = some d') : + d' ≤ d := by + rcases exists_shiftedEntryDegree?_eq_of_rowShiftedDegree?_eq_some hd' with + ⟨j, hj, hentry⟩ + have hjw : j < width := by + simpa [CBivariate.toCoeffRow_size] using hj + have hgets := rowGet_toCoeffRow_ofCoeffRow row width hjw + simp only [shiftedEntryDegree?, hgets] at hentry + have hne : ¬ rowGet row j == 0 := by + by_contra h0 + rw [if_pos h0] at hentry + cases hentry + have hjrow : j < row.size := by + by_contra hge + apply hne + rw [rowGet, Array.getD_eq_getD_getElem?, Array.getElem?_eq_none (by omega)] + simp + refine shiftedEntryDegree?_le_of_rowShiftedDegree?_eq_some hd hjrow ?_ + simp only [shiftedEntryDegree?] + exact hentry + +omit [DecidableEq F] in +/-- A vector with all-zero entries does not normalize. -/ +private theorem normalizeVector?_eq_none_of_all_zero {v : Array F} + (h : ∀ i, i < v.size → v.getD i 0 = 0) : + normalizeVector? v = none := by + have hfirst : firstNonzeroIndex? v = none := by + rw [firstNonzeroIndex?, List.find?_eq_none] + intro i hi + have := h i (List.mem_range.mp hi) + simp [this] + rw [normalizeVector?, hfirst] + +/-- The coefficient row of a zero row never normalizes. -/ +private theorem normalizeInterpolationPolynomial?_eq_none_of_rowIsZero + (params : GSInterpParams) {row : PolynomialRow F} (h : RowIsZero row) : + normalizeInterpolationPolynomial? params + (interpolationCoefficientVector params (CBivariate.ofCoeffRow row)) = none := by + have hz : ∀ i, + i < (interpolationCoefficientVector params + (CBivariate.ofCoeffRow row)).size → + (interpolationCoefficientVector params + (CBivariate.ofCoeffRow row)).getD i 0 = 0 := by + intro i hi + rw [interpolationCoefficientVector, interpolationCoefficientVectorOnBasis] at hi ⊢ + have hi' : i < (interpolationMonomials params).size := by simpa using hi + rw [Array.getD_eq_getD_getElem?, Array.getElem?_eq_getElem hi, + Option.getD_some, Array.getElem_map] + rcases Nat.lt_or_ge ((interpolationMonomials params)[i]).yDegree row.size with + hj | hj + · rw [CBivariate.coeff_ofCoeffRow_of_lt _ hj] + have hrow0 : row.getD ((interpolationMonomials params)[i]).yDegree 0 = 0 := by + refine h _ ?_ + rw [Array.getD_eq_getD_getElem?, Array.getElem?_eq_getElem hj, + Option.getD_some] + exact Array.getElem_mem_toList hj + rw [hrow0] + exact CPolynomial.coeff_zero _ + · exact CBivariate.coeff_ofCoeffRow_of_size_le _ hj + rw [normalizeInterpolationPolynomial?, normalizeInterpolationPolynomialOnBasis?, + normalizeVector?_eq_none_of_all_zero hz] + +/-! ## The modular-equation layer -/ + +/-- The executable GS modular row predicate is equivalent to packed +multiplicity constraints for the bivariate coefficient-row view, for distinct +interpolation nodes and rows inside the interpolation width. -/ +theorem gsModularEquation_row_iff_multiplicity + (V : CPolynomial.VanishingPolynomialContext F) + (E : CPolynomial.BatchEvalContext F) + (modCtx : CPolynomial.ModContext F) + (mulCtx : CPolynomial.MulContext F) + (points : Array (F × F)) (params : GSInterpParams) + (row : PolynomialRow F) + (hdistinct : DistinctXCoordinates points) + (hwidth : row.size ≤ interpolationWidth params) : + let data := buildGSModularData V E mulCtx modCtx points params + rowSatisfiesModularBool mulCtx modCtx row data.matrix data.moduli = true ↔ + CBivariate.satisfiesMultiplicityConstraintsBool + (CBivariate.ofCoeffRow row) points params.multiplicity = true := by + intro data + set G := V.vanishingPolynomial (points.map fun point ↦ point.1) with hGdef + set R := CPolynomial.interpolateCoefficientFormWithVanishing E G points with hRdef + have hGcorrect : G = + CPolynomial.vanishingPolynomialArray (points.map fun point ↦ point.1) := + V.correct _ + have hG : Polynomial.Monic G.toPoly := by + rw [hGcorrect] + exact vanishingPolynomialArray_toPoly_monic _ + have hdata_matrix : data.matrix = gsRelationMatrixWithRG mulCtx modCtx R G params := + rfl + have hdata_moduli : data.moduli = gsModuli mulCtx G params.multiplicity := rfl + rw [hdata_matrix, hdata_moduli, + rowSatisfiesModularBool_gsRelationMatrix_iff mulCtx modCtx hG R params hwidth] + have hR : ∀ point, point ∈ points.toList → CPolynomial.eval point.1 R = point.2 := by + intro point hpoint + have heval := CPolynomial.interpolateCoefficientForm_eval_point V E + (by simpa [DistinctXCoordinates] using hdistinct) hpoint + simpa [hRdef, hGdef, CPolynomial.interpolateCoefficientForm] using heval + rw [CBivariate.satisfiesMultiplicityConstraintsBool_iff_hasMultiplicity, + ← CBivariate.satisfiesMultiplicityConstraints_iff_hasMultiplicity, + ← vanishing_pow_dvd_hasseDeriv_eval_iff_satisfiesMultiplicityConstraints + hdistinct hR, + hGcorrect] + +/-! ## Soundness -/ + +/-- Soundness for executable approximant-basis interpolation. -/ +theorem approximantBasisInterpolate_sound + (V : CPolynomial.VanishingPolynomialContext F) + (E : CPolynomial.BatchEvalContext F) + (solver : ModularSolutionBasisContext F) + {points : Array (F × F)} {params : GSInterpParams} {Q : CBivariate F} + (h : + approximantBasisInterpolate V E solver points params = some Q) : + ValidInterpolationWitness points params Q := by + unfold approximantBasisInterpolate at h + by_cases hLow : params.messageDegree ≤ 1 + · simp only [hLow, if_true, Option.some_inj] at h + rw [← h] + exact lowMessageDegreeInterpolation_sound (points := points) (params := params) hLow + · simp only [hLow, if_false] at h + rw [approximantBasisPositiveInterpolate] at h + by_cases hdistinctBool : distinctXCoordinatesBool points = true + · rw [if_pos hdistinctBool] at h + have hdistinct : DistinctXCoordinates points := + LeeOSullivan.distinctXCoordinatesBool_iff.mp hdistinctBool + set G := V.vanishingPolynomial (points.map fun point ↦ point.1) with hGdef + set R := CPolynomial.interpolateCoefficientFormWithVanishing E G points + with hRdef + set data := buildGSModularDataWithRG solver.mulContext solver.modContext + R G params with hdata + set basis := solver.solutionBasis (modularEquation data) data.shift + (some params.weightedDegreeBound) with hbasis + change (match leastShiftedDegreeChoice? basis data.shift with + | none => none + | some choice => + if choice.degree ≤ params.weightedDegreeBound then + normalizeApproximantCandidate? params (CBivariate.ofCoeffRow choice.row) + else none) = some Q at h + cases hchoice : leastShiftedDegreeChoice? basis data.shift with + | none => + rw [hchoice] at h + change (none : Option (CBivariate F)) = some Q at h + simp at h + | some choice => + rw [hchoice] at h + change (if choice.degree ≤ params.weightedDegreeBound then + normalizeApproximantCandidate? params (CBivariate.ofCoeffRow choice.row) + else none) = some Q at h + by_cases hdeg : choice.degree ≤ params.weightedDegreeBound + · rw [if_pos hdeg] at h + -- The chosen row is a basis member satisfying the modular predicate. + rcases leastShiftedDegreeChoice?_some_valid hchoice with + ⟨hindex, hrowEq, hrowDeg⟩ + have hmem : choice.row ∈ MatrixRows basis := by + rw [MatrixRows, hrowEq, Array.getD_eq_getD_getElem?, + Array.getElem?_eq_getElem hindex, Option.getD_some] + exact Array.getElem_mem_toList hindex + have hsat := solver.sound (modularEquation data) data.shift + (some params.weightedDegreeBound) choice.row hmem + -- Truncate the chosen row to the interpolation width. + set width := interpolationWidth params with hwidthdef + set row' := CBivariate.toCoeffRow width + (CBivariate.ofCoeffRow choice.row) with hrow' + have hrow'size : row'.size = width := CBivariate.toCoeffRow_size _ _ + have hmatrixsize : (modularEquation data).matrix.size = width := + gsRelationMatrixWithModuli_size _ _ _ _ _ + have hagree : ∀ k, k < (modularEquation data).matrix.size → + rowGet choice.row k = rowGet row' k := by + intro k hk + rw [hrow', rowGet_toCoeffRow_ofCoeffRow choice.row width + (by omega)] + have hsat' : rowSatisfiesModularBool solver.mulContext solver.modContext + row' (modularEquation data).matrix (modularEquation data).moduli = + true := by + rw [← rowSatisfiesModularBool_congr_of_agree solver.mulContext + solver.modContext (modularEquation data).matrix + (modularEquation data).moduli hagree] + exact hsat + -- Transfer to multiplicity constraints for the truncated polynomial. + have hiff := gsModularEquation_row_iff_multiplicity V E + solver.modContext solver.mulContext points params row' hdistinct + (by omega) + have hdataEq : buildGSModularData V E solver.mulContext + solver.modContext points params = data := rfl + rw [hdataEq] at hiff + have hmultBool := hiff.mp hsat' + have hmult : CBivariate.SatisfiesMultiplicityConstraints + (CBivariate.ofCoeffRow row') points params.multiplicity := by + rw [CBivariate.satisfiesMultiplicityConstraints_iff_hasMultiplicity] + exact (CBivariate.satisfiesMultiplicityConstraintsBool_iff_hasMultiplicity + _ _ _).mp hmultBool + -- The normalization only sees in-width coefficients. + have hw : 0 < yWeight params := by + rw [yWeight] + omega + have hvec := interpolationCoefficientVector_toCoeffRow params hw + choice.row + have hnorm' : LeeOSullivan.normalizeLeeCandidate? params + (CBivariate.ofCoeffRow row') = some Q := by + rw [LeeOSullivan.normalizeLeeCandidate?, + normalizeInterpolationPolynomial?, hrow', hvec] + rw [normalizeApproximantCandidate?, + normalizeInterpolationPolynomial?] at h + exact h + -- The truncated row is nonzero with bounded shifted degree. + cases hd' : rowShiftedDegree? row' data.shift with + | none => + exfalso + have hzero : RowIsZero row' := + rowShiftedDegree?_eq_none_iff.mp hd' + rw [LeeOSullivan.normalizeLeeCandidate?, + normalizeInterpolationPolynomial?_eq_none_of_rowIsZero params + hzero] at hnorm' + simp at hnorm' + | some d' => + have hd'le : d' ≤ choice.degree := + rowShiftedDegree?_toCoeffRow_le hrowDeg hd' + have hshiftEq : data.shift = + CBivariate.weightedDegreeShift (yWeight params) row'.size := by + rw [hrow'size, hwidthdef] + rfl + have hdegRaw : CBivariate.natWeightedDegree + (CBivariate.ofCoeffRow row') 1 (yWeight params) ≤ + params.weightedDegreeBound := by + refine CBivariate.natWeightedDegree_ofCoeffRow_le_of_rowShiftedDegree?_le + row' (yWeight params) params.weightedDegreeBound d' ?_ (by omega) + rw [← hshiftEq] + exact hd' + exact LeeOSullivan.normalizeLeeCandidate?_sound_of_raw + (points := points) (params := params) hLow hdegRaw hmult hnorm' + · rw [if_neg hdeg] at h + simp at h + · rw [if_neg hdistinctBool] at h + simp at h + +/-! ## Completeness -/ + +omit [LawfulBEq F] [DecidableEq F] in +/-- The shifted row degree only sees shift entries below the row size. -/ +private theorem rowShiftedDegree?_congr_shift {row : PolynomialRow F} + {shift shift' : Array Nat} + (h : ∀ j, j < row.size → shift.getD j 0 = shift'.getD j 0) : + rowShiftedDegree? row shift = rowShiftedDegree? row shift' := by + rw [rowShiftedDegree?, rowShiftedDegree?] + refine List.foldl_ext _ _ _ fun acc j hj ↦ ?_ + have hj' : j < row.size := List.mem_range.mp hj + simp only [shiftedEntryDegree?, h j hj'] + +omit [DecidableEq F] in +/-- Entry access for the weighted-degree shift array. -/ +private theorem weightedDegreeShift_getD {w width j : Nat} (hj : j < width) : + (CBivariate.weightedDegreeShift w width).getD j 0 = j * w := by + rw [CBivariate.weightedDegreeShift, Array.getD_eq_getD_getElem?, + List.getElem?_toArray, List.getElem?_map, List.getElem?_range hj, + Option.map_some, Option.getD_some] + +omit [DecidableEq F] in +/-- Zero rows have zero coefficient rows. -/ +private theorem ofCoeffRow_eq_zero_of_rowIsZero {row : PolynomialRow F} + (h : RowIsZero row) : CBivariate.ofCoeffRow row = 0 := by + rw [CBivariate.ofCoeffRow] + show (CPolynomial.ofArray row : CPolynomial (CPolynomial F)) = 0 + rw [CPolynomial.eq_zero_iff_coeff_zero] + intro n + rw [CPolynomial.coeff_ofArray] + rcases Nat.lt_or_ge n row.size with hn | hn + · refine h _ ?_ + rw [Array.getD_eq_getD_getElem?, Array.getElem?_eq_getElem hn, Option.getD_some] + exact Array.getElem_mem_toList hn + · rw [Array.getD_eq_getD_getElem?, Array.getElem?_eq_none hn] + rfl + +omit [DecidableEq F] in +/-- Bounded-weighted-degree polynomials have no `Y`-coefficients at or above +the interpolation width. -/ +private theorem coeff_eq_zero_of_interpolationWidth_le + {params : GSInterpParams} (hw : 0 < yWeight params) {Q : CBivariate F} + (hdeg : CBivariate.natWeightedDegree Q 1 (yWeight params) ≤ + params.weightedDegreeBound) + {j : Nat} (hj : interpolationWidth params ≤ j) : + CPolynomial.coeff Q j = 0 := by + have hcoeffs : ∀ i, CBivariate.coeff Q i j = 0 := by + intro i + by_contra hne0 + have hle := (CBivariate.natWeightedDegree_le_iff_coeff Q 1 (yWeight params) + params.weightedDegreeBound).mp hdeg i j hne0 + have hdm := Nat.div_add_mod params.weightedDegreeBound (yWeight params) + have hmod := Nat.mod_lt params.weightedDegreeBound hw + have hsum : (params.weightedDegreeBound / yWeight params + 1) * yWeight params + = yWeight params * (params.weightedDegreeBound / yWeight params) + + yWeight params := by + ring + have hwidthmul : (params.weightedDegreeBound / yWeight params + 1) * + yWeight params ≤ j * yWeight params := by + refine Nat.mul_le_mul_right _ ?_ + rw [interpolationWidth, interpolationYCap] at hj + omega + have hcomm : j * yWeight params = yWeight params * j := Nat.mul_comm _ _ + omega + show CPolynomial.coeff Q j = 0 + rw [CPolynomial.eq_zero_iff_coeff_zero] + intro i + exact hcoeffs i + +omit [DecidableEq F] in +/-- Width truncation is the identity on bounded-weighted-degree polynomials. -/ +private theorem ofCoeffRow_toCoeffRow_eq + {params : GSInterpParams} (hw : 0 < yWeight params) {Q : CBivariate F} + (hdeg : CBivariate.natWeightedDegree Q 1 (yWeight params) ≤ + params.weightedDegreeBound) : + CBivariate.ofCoeffRow (CBivariate.toCoeffRow (interpolationWidth params) Q) = + Q := by + rw [CBivariate.ofCoeffRow] + show (CPolynomial.ofArray (CBivariate.toCoeffRow (interpolationWidth params) Q) : + CPolynomial (CPolynomial F)) = Q + apply CPolynomial.eq_iff_coeff.2 + intro n + rw [CPolynomial.coeff_ofArray] + rcases Nat.lt_or_ge n (interpolationWidth params) with hn | hn + · rw [CBivariate.toCoeffRow, Array.getD_eq_getD_getElem?, List.getElem?_toArray, + List.getElem?_map, List.getElem?_range hn, Option.map_some, Option.getD_some] + · rw [CBivariate.toCoeffRow, Array.getD_eq_getD_getElem?, List.getElem?_toArray, + List.getElem?_map, List.getElem?_eq_none (by simpa using hn), + Option.map_none, Option.getD_none] + exact (coeff_eq_zero_of_interpolationWidth_le hw hdeg hn).symm + +/-- Completeness for executable approximant-basis interpolation on distinct +input `x`-coordinates. -/ +theorem approximantBasisInterpolate_complete + (V : CPolynomial.VanishingPolynomialContext F) + (E : CPolynomial.BatchEvalContext F) + (solver : ModularSolutionBasisContext F) + (points : Array (F × F)) (params : GSInterpParams) + (hdistinct : DistinctXCoordinates points) + (hexists : ∃ Q, ValidInterpolationWitness points params Q) : + ∃ Q, approximantBasisInterpolate V E solver points params = some Q := by + by_cases hLow : params.messageDegree ≤ 1 + · refine ⟨lowMessageDegreeInterpolation points params.multiplicity, ?_⟩ + unfold approximantBasisInterpolate + rw [if_pos hLow] + · rcases hexists with ⟨Q₀, hQ₀ne, hQ₀deg, hQ₀mult⟩ + have hw : 0 < yWeight params := by + rw [yWeight] + omega + have hdistinctBool : distinctXCoordinatesBool points = true := + LeeOSullivan.distinctXCoordinatesBool_iff.mpr hdistinct + set G := V.vanishingPolynomial (points.map fun point ↦ point.1) with hGdef + set R := CPolynomial.interpolateCoefficientFormWithVanishing E G points + with hRdef + set data := buildGSModularDataWithRG solver.mulContext solver.modContext + R G params with hdata + set basis := solver.solutionBasis (modularEquation data) data.shift + (some params.weightedDegreeBound) with hbasis + set width := interpolationWidth params with hwidthdef + -- The witness coefficient row. + set row₀ := CBivariate.toCoeffRow width Q₀ with hrow₀ + have hround : CBivariate.ofCoeffRow row₀ = Q₀ := + ofCoeffRow_toCoeffRow_eq hw hQ₀deg + have hrow₀size : row₀.size = width := CBivariate.toCoeffRow_size _ _ + have hnz₀ : rowIsZero row₀ = false := by + rw [Bool.eq_false_iff] + intro htrue + exact hQ₀ne (by rw [← hround, + ofCoeffRow_eq_zero_of_rowIsZero (rowIsZero_iff.mp htrue)]) + have hmatrixsize : (modularEquation data).matrix.size = width := + gsRelationMatrixWithModuli_size _ _ _ _ _ + have hsolwidth : (modularEquation data).solutionWidth = width := by + rw [ModularEquation.solutionWidth, hmatrixsize] + -- The witness row satisfies the modular predicate. + have hsat₀ : rowSatisfiesModularBool solver.mulContext solver.modContext row₀ + (modularEquation data).matrix (modularEquation data).moduli = true := by + have hiff := gsModularEquation_row_iff_multiplicity V E + solver.modContext solver.mulContext points params row₀ hdistinct + (by omega) + have hdataEq : buildGSModularData V E solver.mulContext + solver.modContext points params = data := rfl + rw [hdataEq] at hiff + refine hiff.mpr ?_ + rw [hround] + exact (CBivariate.satisfiesMultiplicityConstraintsBool_iff_hasMultiplicity + _ _ _).mpr hQ₀mult + -- The witness row has shifted degree at most the weighted-degree bound. + have hshift₀ : data.shift = CBivariate.weightedDegreeShift (yWeight params) + row₀.size := by + rw [hrow₀size] + rfl + obtain ⟨d₀, hd₀⟩ : ∃ d₀, rowShiftedDegree? row₀ data.shift = some d₀ := by + cases hcase : rowShiftedDegree? row₀ data.shift with + | none => + have hzero := rowShiftedDegree?_eq_none_iff.mp hcase + rw [Bool.eq_false_iff] at hnz₀ + exact absurd (rowIsZero_iff.mpr hzero) hnz₀ + | some d => exact ⟨d, rfl⟩ + have hd₀le : d₀ ≤ params.weightedDegreeBound := by + have hnat := CBivariate.rowShiftedDegree?_eq_natWeightedDegree_ofCoeffRow + row₀ (yWeight params) d₀ (by rw [← hshift₀]; exact hd₀) + rw [hround] at hnat + omega + -- The GS moduli are monic powers of the vanishing polynomial. + have hGmonic : Polynomial.Monic G.toPoly := by + rw [show G = CPolynomial.vanishingPolynomialArray + (points.map fun point ↦ point.1) from V.correct _] + exact vanishingPolynomialArray_toPoly_monic _ + have hmonic : ∀ b, b < (modularEquation data).moduli.size → + ((modularEquation data).moduli.getD b 0).monic := by + intro b hb + have hsize : (modularEquation data).moduli.size = params.multiplicity := + gsModuli_size _ _ _ + have hgetD : (modularEquation data).moduli.getD b 0 = + G ^ (params.multiplicity - b) := + gsModuli_getD solver.mulContext G (by omega) + rw [hgetD] + refine (CPolynomial.monic_toPoly_iff _).mpr ?_ + rw [CPolynomial.toPoly_pow] + exact hGmonic.pow _ + -- The relation matrix exposes every modular column to the row predicate. + have hcols : (modularEquation data).moduli.size ≤ + MatrixWidth (modularEquation data).matrix := by + have hmodsize : (modularEquation data).moduli.size = params.multiplicity := + gsModuli_size _ _ _ + have hmatwidth : MatrixWidth (modularEquation data).matrix = + params.multiplicity := + gsRelationMatrixWithModuli_matrixWidth _ _ _ _ _ + omega + -- The GS shift is aligned with the principal solution width. + have hshiftsize : data.shift.size = (modularEquation data).solutionWidth := by + have hdatashift : data.shift = + CBivariate.weightedDegreeShift (yWeight params) width := rfl + rw [hdatashift, hsolwidth, CBivariate.weightedDegreeShift] + simp + -- Apply the solver completeness/minimality contract with the GS + -- weighted-degree bound: the witness row is within the bound, so the + -- returned basis contains a row whose shifted degree meets it. + rcases solver.complete_minimal (modularEquation data) data.shift + (some params.weightedDegreeBound) row₀ d₀ hmonic hcols + hshiftsize hsat₀ hnz₀ (by omega) hd₀ + (fun bound hbound ↦ by + obtain rfl : params.weightedDegreeBound = bound := + Option.some.inj hbound + exact hd₀le) with + ⟨hbasisWidth, basisRow, bDeg, hbMem, hbDeg, hbLe⟩ + have hbDegLe : bDeg ≤ params.weightedDegreeBound := by + rwa [Option.getD_some] at hbLe + -- Select the least shifted-degree basis row. + rcases List.getElem_of_mem hbMem with ⟨bIdx, hbIdxList, hbGet⟩ + have hbIdx : bIdx < basis.size := by + simpa [MatrixRows] using hbIdxList + have hbGetD : basis.getD bIdx #[] = basisRow := by + rw [Array.getD_eq_getD_getElem?, Array.getElem?_eq_getElem hbIdx, + Option.getD_some] + simpa [MatrixRows, Array.getElem_toList] using hbGet + rcases leastShiftedDegreeChoice?_some_of_degree (M := basis) + (shift := data.shift) (i := bIdx) (d := bDeg) hbIdx + (by rw [hbGetD]; exact hbDeg) with + ⟨choice, hchoice, hchoiceLe⟩ + have hchoiceBound : choice.degree ≤ params.weightedDegreeBound := by + omega + rcases leastShiftedDegreeChoice?_some_valid hchoice with + ⟨hcIdx, hcRow, hcDeg⟩ + have hcMem : choice.row ∈ MatrixRows basis := by + rw [MatrixRows, hcRow, Array.getD_eq_getD_getElem?, + Array.getElem?_eq_getElem hcIdx, Option.getD_some] + exact Array.getElem_mem_toList hcIdx + have hcWidth : choice.row.size ≤ width := by + have := hbasisWidth choice.row hcMem + omega + -- The chosen row reconstructs and normalizes. + have hrawNe : CBivariate.ofCoeffRow choice.row ≠ 0 := + LeeOSullivan.ofCoeffRow_ne_zero_of_rowShiftedDegree?_some hcDeg + have hcShift : rowShiftedDegree? choice.row + (CBivariate.weightedDegreeShift (yWeight params) choice.row.size) = + some choice.degree := by + rw [← hcDeg] + refine (rowShiftedDegree?_congr_shift fun j hj ↦ ?_).symm + rw [weightedDegreeShift_getD hj] + have hjwidth : j < width := by omega + have hdatashift : data.shift = + CBivariate.weightedDegreeShift (yWeight params) width := rfl + rw [hdatashift, weightedDegreeShift_getD hjwidth] + have hdegRaw : CBivariate.natWeightedDegree (CBivariate.ofCoeffRow choice.row) + 1 (yWeight params) ≤ params.weightedDegreeBound := + CBivariate.natWeightedDegree_ofCoeffRow_le_of_rowShiftedDegree?_le + choice.row (yWeight params) params.weightedDegreeBound choice.degree + hcShift hchoiceBound + rcases LeeOSullivan.normalizeLeeCandidate?_some_of_raw hLow hrawNe hdegRaw with + ⟨Q, hnorm⟩ + -- Assemble the executable run. + refine ⟨Q, ?_⟩ + unfold approximantBasisInterpolate + rw [if_neg hLow, approximantBasisPositiveInterpolate, if_pos hdistinctBool] + change (match leastShiftedDegreeChoice? basis data.shift with + | none => none + | some choice => + if choice.degree ≤ params.weightedDegreeBound then + normalizeApproximantCandidate? params (CBivariate.ofCoeffRow choice.row) + else none) = some Q + rw [hchoice] + change (if choice.degree ≤ params.weightedDegreeBound then + normalizeApproximantCandidate? params (CBivariate.ofCoeffRow choice.row) + else none) = some Q + rw [if_pos hchoiceBound] + rw [normalizeApproximantCandidate?] + rw [LeeOSullivan.normalizeLeeCandidate?] at hnorm + exact hnorm + +/-- Public approximant-basis interpolation backend context. -/ +def approximantBasisInterpContext + (V : CPolynomial.VanishingPolynomialContext F) + (E : CPolynomial.BatchEvalContext F) + (solver : ModularSolutionBasisContext F) : GSInterpContext F where + interpolate := approximantBasisInterpolate V E solver + sound := by + intro points params Q h + exact approximantBasisInterpolate_sound V E solver h + complete := by + intro points params hdistinct hexists + exact approximantBasisInterpolate_complete V E solver points params hdistinct hexists + +end ApproximantBasis + +end GuruswamiSudan + +end CompPoly diff --git a/CompPoly/Bivariate/GuruswamiSudan/Interpolation/ApproximantBasis/ModularData.lean b/CompPoly/Bivariate/GuruswamiSudan/Interpolation/ApproximantBasis/ModularData.lean new file mode 100644 index 00000000..74e3ab9a --- /dev/null +++ b/CompPoly/Bivariate/GuruswamiSudan/Interpolation/ApproximantBasis/ModularData.lean @@ -0,0 +1,696 @@ +/- +Copyright (c) 2026 CompPoly Contributors. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Valerii Huhnin +-/ + +module + +-- `linearFactor`, `coeff` and friends are declared in bare `public section`s, so +-- their bodies are opaque downstream; see `docs/wiki/module-system.md`. +import all CompPoly.Univariate.Basic +import all CompPoly.Univariate.Raw.Core +public import CompPoly.Bivariate.GuruswamiSudan.Interpolation.ApproximantBasis.Basic +public import CompPoly.Bivariate.GuruswamiSudan.Interpolation.ApproximantBasis.Multiplicity +public import CompPoly.Univariate.DivisionCorrectness + +/-! +# Executable GS Modular-Data Bridges + +Semantic characterizations of the executable approximant-basis modular data: +the modulus array `gsModuli`, the incrementally built binomial relation matrix +`gsRelationColumn` / `gsRelationMatrixWithModuli`, and the executable row +predicate `rowSatisfiesModularBool`. The main result identifies the modular +row predicate over the GS data with divisibility of every sheared coefficient +`(hasseDeriv b Q.toPoly).eval R` by `G^(s-b)`. +-/ + +@[expose] public section + +namespace CompPoly + +namespace GuruswamiSudan + +namespace ApproximantBasis + +open Polynomial +open PolynomialMatrix + +variable {F : Type*} [Field F] [BEq F] [LawfulBEq F] [DecidableEq F] + +/-! ## Generic helpers -/ + +omit [Field F] [BEq F] [LawfulBEq F] [DecidableEq F] in +private theorem foldl_add_eq_sum {M : Type*} [AddCommMonoid M] (f : Nat → M) : + ∀ n : Nat, + (List.range n).foldl (fun acc k ↦ acc + f k) 0 = ∑ k ∈ Finset.range n, f k := by + intro n + induction n with + | zero => simp + | succ n ih => + rw [List.range_succ, List.foldl_append, ih, List.foldl_cons, List.foldl_nil, + Finset.sum_range_succ] + +omit [Field F] [BEq F] [LawfulBEq F] [DecidableEq F] in +/-- First component of an `MProd`-state fold whose second component evolves +independently of the first, as a `Prod`-state fold. -/ +private theorem foldl_mprod_fst {α β : Type u} {γ : Type*} + (f : α → β → γ → α) (g : β → γ → β) : + ∀ (l : List γ) (a : α) (b : β), + (l.foldl (fun s c ↦ (⟨f s.fst s.snd c, g s.snd c⟩ : MProd α β)) ⟨a, b⟩).fst = + (l.foldl (fun s c ↦ (f s.1 s.2 c, g s.2 c)) (a, b)).1 := by + intro l + induction l with + | nil => + intro a b + rfl + | cons c l ih => + intro a b + rw [List.foldl_cons, List.foldl_cons] + exact ih _ _ + +omit [DecidableEq F] in +private theorem toPoly_finset_sum (f : Nat → CPolynomial F) (n : Nat) : + (∑ k ∈ Finset.range n, f k).toPoly = ∑ k ∈ Finset.range n, (f k).toPoly := by + induction n with + | zero => + simp [CPolynomial.toPoly_zero] + | succ n ih => + rw [Finset.sum_range_succ, Finset.sum_range_succ, CPolynomial.toPoly_add, ih] + +omit [BEq F] [LawfulBEq F] [DecidableEq F] in +/-- A common modulus of differences identifies the two divisibility facts. -/ +private theorem dvd_iff_dvd_of_dvd_sub {M a b : Polynomial F} (h : M ∣ a - b) : + M ∣ a ↔ M ∣ b := by + constructor + · intro ha + have hb : M ∣ a - (a - b) := dvd_sub ha h + simpa using hb + · intro hb + have ha : M ∣ (a - b) + b := dvd_add h hb + simpa using ha + +omit [BEq F] [LawfulBEq F] [DecidableEq F] in +/-- Entry access for `PolynomialMatrix.ofFn`. -/ +theorem ofFn_rowGet (rows width : Nat) (entry : Nat → Nat → CPolynomial F) + {i j : Nat} (hi : i < rows) (hj : j < width) : + rowGet ((PolynomialMatrix.ofFn rows width entry).getD i #[]) j = entry i j := by + have hrow : (PolynomialMatrix.ofFn rows width entry).getD i #[] = + ((List.range width).map (entry i)).toArray := by + rw [PolynomialMatrix.ofFn, Array.getD_eq_getD_getElem?, List.getElem?_toArray, + List.getElem?_map, List.getElem?_range hi, Option.map_some, Option.getD_some] + rw [hrow, rowGet, Array.getD_eq_getD_getElem?, List.getElem?_toArray, + List.getElem?_map, List.getElem?_range hj, Option.map_some, Option.getD_some] + +omit [BEq F] [LawfulBEq F] [DecidableEq F] in +/-- Row count of `PolynomialMatrix.ofFn`. -/ +theorem ofFn_size (rows width : Nat) (entry : Nat → Nat → CPolynomial F) : + (PolynomialMatrix.ofFn rows width entry).size = rows := by + simp [PolynomialMatrix.ofFn] + +omit [BEq F] [LawfulBEq F] [DecidableEq F] in +/-- Width of `PolynomialMatrix.ofFn` with at least one row. -/ +theorem ofFn_matrixWidth (rows width : Nat) (entry : Nat → Nat → CPolynomial F) + (hrows : 0 < rows) : + MatrixWidth (PolynomialMatrix.ofFn rows width entry) = width := by + rw [PolynomialMatrix.ofFn, MatrixWidth] + rw [show ((List.range rows).map + (fun i ↦ ((List.range width).map (entry i)).toArray)).toArray[0]? = + some ((List.range width).map (entry 0)).toArray from by + rw [List.getElem?_toArray, List.getElem?_map, List.getElem?_range hrows] + simp] + simp + +/-! ## Modular reduction semantics -/ + +omit [DecidableEq F] in +/-- `modByMonicWith` computes the Mathlib monic remainder under `toPoly`. -/ +theorem modByMonicWith_toPoly (modCtx : CPolynomial.ModContext F) + {p M : CPolynomial F} (hM : Polynomial.Monic M.toPoly) : + (PolynomialMatrix.modByMonicWith modCtx p M).toPoly = p.toPoly %ₘ M.toPoly := by + have hMne : M ≠ 0 := by + intro hzero + have : M.toPoly = 0 := by + rw [hzero, CPolynomial.toPoly_zero] + exact hM.ne_zero this + rw [PolynomialMatrix.modByMonicWith, if_neg (by simpa using hMne), + modCtx.modByMonic_eq_modByMonic] + exact CPolynomial.modByMonic_toPoly_eq_modByMonic p M + ((CPolynomial.monic_toPoly_iff M).mpr hM) + +omit [DecidableEq F] in +/-- `modByMonicWith` is congruent to the identity modulo the modulus. -/ +theorem dvd_modByMonicWith_sub (modCtx : CPolynomial.ModContext F) + {p M : CPolynomial F} (hM : Polynomial.Monic M.toPoly) : + M.toPoly ∣ (PolynomialMatrix.modByMonicWith modCtx p M).toPoly - p.toPoly := by + rw [modByMonicWith_toPoly modCtx hM] + have hdecomp := Polynomial.modByMonic_add_div p.toPoly M.toPoly + refine ⟨-(p.toPoly /ₘ M.toPoly), ?_⟩ + linear_combination hdecomp + +omit [DecidableEq F] in +/-- The executable remainder vanishes exactly on multiples of the modulus. -/ +theorem modByMonicWith_eq_zero_iff_dvd (modCtx : CPolynomial.ModContext F) + {p M : CPolynomial F} (hM : Polynomial.Monic M.toPoly) : + PolynomialMatrix.modByMonicWith modCtx p M = 0 ↔ M.toPoly ∣ p.toPoly := by + rw [← Polynomial.modByMonic_eq_zero_iff_dvd hM] + rw [← modByMonicWith_toPoly modCtx hM] + constructor + · intro h + rw [h, CPolynomial.toPoly_zero] + · intro h + have := (CPolynomial.toPoly_eq_zero_iff _).mp h + exact this + +/-! ## The GS modulus array -/ + +omit [DecidableEq F] in +private theorem gsModuli_loop (mulCtx : CPolynomial.MulContext F) (G : CPolynomial F) : + ∀ (l : List Nat) (k : Nat), + l.foldl + (fun ascending _ ↦ + ascending.push (mulCtx.mul (ascending.getD (ascending.size - 1) 0) G)) + (((List.range (k + 1)).map fun i ↦ G ^ (i + 1)).toArray) = + ((List.range (k + 1 + l.length)).map fun i ↦ G ^ (i + 1)).toArray := by + intro l + induction l with + | nil => + intro k + simp + | cons a l ih => + intro k + rw [List.foldl_cons] + have hsize : (((List.range (k + 1)).map fun i ↦ G ^ (i + 1)).toArray).size = k + 1 := by + simp + have hget : (((List.range (k + 1)).map fun i ↦ G ^ (i + 1)).toArray).getD k 0 = + G ^ (k + 1) := by + rw [Array.getD_eq_getD_getElem?, List.getElem?_toArray, List.getElem?_map, + List.getElem?_range (by omega)] + simp + rw [hsize] + simp only [Nat.add_sub_cancel] + rw [hget, mulCtx.mul_eq_mul, ← pow_succ] + have hpush : (((List.range (k + 1)).map fun i ↦ G ^ (i + 1)).toArray).push + (G ^ (k + 1 + 1)) = + ((List.range (k + 2)).map fun i ↦ G ^ (i + 1)).toArray := by + rw [List.push_toArray] + congr 1 + rw [show k + 2 = (k + 1) + 1 from rfl, List.range_succ] + simp [List.range_succ] + rw [hpush, ih (k + 1)] + have harg : k + 1 + 1 + l.length = k + 1 + (a :: l).length := by + simp + omega + rw [harg] + +omit [DecidableEq F] in +/-- Closed form for the GS modulus array before reversal. -/ +theorem gsModuli_eq (mulCtx : CPolynomial.MulContext F) (G : CPolynomial F) (s : Nat) : + gsModuli mulCtx G s = (((List.range s).map fun i ↦ G ^ (i + 1)).toArray).reverse := by + rcases Nat.eq_zero_or_pos s with hs | hs + · subst hs + rfl + · rw [gsModuli] + simp only [beq_iff_eq, Std.Legacy.Range.forIn_eq_forIn_range', + Std.Legacy.Range.size, Nat.add_sub_cancel, Nat.div_one, + List.forIn_pure_yield_eq_foldl, bind_pure_comp, map_pure, + Id.run_pure, Nat.ne_of_gt hs, if_false] + congr 1 + have hstart : (#[G] : Array (CPolynomial F)) = + ((List.range 1).map fun i ↦ G ^ (i + 1)).toArray := by + simp + rw [hstart, gsModuli_loop mulCtx G (List.range' 1 (s - 1)) 0] + rw [List.length_range'] + congr 2 + congr 1 + omega + +omit [DecidableEq F] in +/-- The GS modulus array has one modulus per multiplicity level. -/ +theorem gsModuli_size (mulCtx : CPolynomial.MulContext F) (G : CPolynomial F) (s : Nat) : + (gsModuli mulCtx G s).size = s := by + rw [gsModuli_eq] + simp + +omit [DecidableEq F] in +/-- The `b`-th GS modulus is `G^(s-b)`. -/ +theorem gsModuli_getD (mulCtx : CPolynomial.MulContext F) (G : CPolynomial F) + {b s : Nat} (hb : b < s) : + (gsModuli mulCtx G s).getD b 0 = G ^ (s - b) := by + rw [gsModuli_eq] + have hsize : ((((List.range s).map fun i ↦ G ^ (i + 1)).toArray).reverse).size = s := by + simp + rw [Array.getD_eq_getD_getElem?, Array.getElem?_eq_getElem (by omega)] + simp only [Option.getD_some] + rw [Array.getElem_reverse] + simp only [List.getElem_toArray, List.getElem_map, List.getElem_range, + List.size_toArray, List.length_map, List.length_range] + congr 1 + omega + +/-! ## The GS relation column -/ + +private def relationColumnStep (mulCtx : CPolynomial.MulContext F) + (modCtx : CPolynomial.ModContext F) (reducedR modulus : CPolynomial F) (b : Nat) + (state : Array (CPolynomial F) × CPolynomial F) (j : Nat) : + Array (CPolynomial F) × CPolynomial F := + (state.1.setIfInBounds j (CPolynomial.C ((j.choose b : F)) * state.2), + PolynomialMatrix.modByMonicWith modCtx (mulCtx.mul state.2 reducedR) modulus) + +omit [DecidableEq F] in +private theorem gsRelationColumn_eq_foldl (mulCtx : CPolynomial.MulContext F) + (modCtx : CPolynomial.ModContext F) + (R modulus : CPolynomial F) (width b : Nat) : + gsRelationColumn mulCtx modCtx R modulus width b = + ((List.range' b (width - b)).foldl + (relationColumnStep mulCtx modCtx + (PolynomialMatrix.modByMonicWith modCtx R modulus) modulus b) + (Array.replicate width (0 : CPolynomial F), 1)).1 := by + rw [gsRelationColumn] + simp only [Std.Legacy.Range.forIn_eq_forIn_range', Std.Legacy.Range.size, + Nat.add_sub_cancel, Nat.div_one, List.forIn_pure_yield_eq_foldl, + bind_pure_comp, map_pure, Id.run_pure] + -- `forIn` now reduces to a `Prod` fold (not `MProd`); both sides match. + rfl + +omit [DecidableEq F] in +private theorem relationColumn_foldl_size_untouched (mulCtx : CPolynomial.MulContext F) + (modCtx : CPolynomial.ModContext F) (reducedR modulus : CPolynomial F) (b : Nat) : + ∀ (n j₀ : Nat) (col : Array (CPolynomial F)) (pow : CPolynomial F), + (((List.range' j₀ n).foldl + (relationColumnStep mulCtx modCtx reducedR modulus b) (col, pow)).1.size = + col.size) ∧ + (∀ j, j < j₀ → + ((List.range' j₀ n).foldl + (relationColumnStep mulCtx modCtx reducedR modulus b) (col, pow)).1.getD j 0 = + col.getD j 0) := by + intro n + induction n with + | zero => + intro j₀ col pow + exact ⟨rfl, fun j _hj ↦ rfl⟩ + | succ n ih => + intro j₀ col pow + rw [List.range'_succ, List.foldl_cons] + have hstep : relationColumnStep mulCtx modCtx reducedR modulus b (col, pow) j₀ = + (col.setIfInBounds j₀ (CPolynomial.C ((j₀.choose b : F)) * pow), + PolynomialMatrix.modByMonicWith modCtx (mulCtx.mul pow reducedR) modulus) := rfl + rw [hstep] + rcases ih (j₀ + 1) (col.setIfInBounds j₀ (CPolynomial.C ((j₀.choose b : F)) * pow)) + (PolynomialMatrix.modByMonicWith modCtx (mulCtx.mul pow reducedR) modulus) + with ⟨hsize, huntouched⟩ + refine ⟨by rw [hsize]; simp, ?_⟩ + intro j hj + rw [huntouched j (by omega)] + rw [Array.getD_eq_getD_getElem?, Array.getD_eq_getD_getElem?, + Array.getElem?_setIfInBounds_ne (by omega)] + +omit [DecidableEq F] in +private theorem relationColumn_foldl_spec (mulCtx : CPolynomial.MulContext F) + (modCtx : CPolynomial.ModContext F) + {R reducedR modulus : CPolynomial F} + (hM : Polynomial.Monic modulus.toPoly) + (hred : modulus.toPoly ∣ reducedR.toPoly - R.toPoly) (b : Nat) : + ∀ (n j₀ : Nat) (col : Array (CPolynomial F)) (pow : CPolynomial F), + b ≤ j₀ → + modulus.toPoly ∣ pow.toPoly - R.toPoly ^ (j₀ - b) → + (((List.range' j₀ n).foldl + (relationColumnStep mulCtx modCtx reducedR modulus b) (col, pow)).1.size = + col.size) ∧ + (∀ j, j < j₀ ∨ j₀ + n ≤ j → + ((List.range' j₀ n).foldl + (relationColumnStep mulCtx modCtx reducedR modulus b) (col, pow)).1.getD j 0 = + col.getD j 0) ∧ + (∀ j, j₀ ≤ j → j < j₀ + n → j < col.size → + modulus.toPoly ∣ + (((List.range' j₀ n).foldl + (relationColumnStep mulCtx modCtx reducedR modulus b) (col, pow)).1.getD j 0).toPoly - + Polynomial.C ((j.choose b : F)) * R.toPoly ^ (j - b)) := by + intro n + induction n with + | zero => + intro j₀ col pow _hb _hpow + refine ⟨rfl, fun j _hj ↦ rfl, fun j hj₁ hj₂ _hj₃ ↦ ?_⟩ + omega + | succ n ih => + intro j₀ col pow hb hpow + rw [List.range'_succ, List.foldl_cons] + set col' := col.setIfInBounds j₀ (CPolynomial.C ((j₀.choose b : F)) * pow) with hcol' + set pow' := PolynomialMatrix.modByMonicWith modCtx (mulCtx.mul pow reducedR) modulus + with hpow' + have hstep : relationColumnStep mulCtx modCtx reducedR modulus b (col, pow) j₀ = + (col', pow') := rfl + rw [hstep] + have hpow'spec : modulus.toPoly ∣ pow'.toPoly - R.toPoly ^ (j₀ + 1 - b) := by + have hmul : modulus.toPoly ∣ + (mulCtx.mul pow reducedR).toPoly - R.toPoly ^ (j₀ - b) * R.toPoly := by + rw [mulCtx.mul_eq_mul, CPolynomial.toPoly_mul] + have hexpand : pow.toPoly * reducedR.toPoly - + R.toPoly ^ (j₀ - b) * R.toPoly = + pow.toPoly * (reducedR.toPoly - R.toPoly) + + (pow.toPoly - R.toPoly ^ (j₀ - b)) * R.toPoly := by + ring + rw [hexpand] + exact dvd_add (Dvd.dvd.mul_left hred _) (Dvd.dvd.mul_right hpow _) + have hmod := dvd_modByMonicWith_sub modCtx + (p := mulCtx.mul pow reducedR) (M := modulus) hM + have hcomb := dvd_add hmod hmul + have hrw : (PolynomialMatrix.modByMonicWith modCtx (mulCtx.mul pow reducedR) + modulus).toPoly - (mulCtx.mul pow reducedR).toPoly + + ((mulCtx.mul pow reducedR).toPoly - R.toPoly ^ (j₀ - b) * R.toPoly) = + (PolynomialMatrix.modByMonicWith modCtx (mulCtx.mul pow reducedR) + modulus).toPoly - R.toPoly ^ (j₀ - b) * R.toPoly := by + ring + rw [hrw] at hcomb + rw [hpow'] + have hpowsucc : R.toPoly ^ (j₀ - b) * R.toPoly = R.toPoly ^ (j₀ + 1 - b) := by + rw [← pow_succ] + congr 1 + omega + rw [← hpowsucc] + exact hcomb + rcases ih (j₀ + 1) col' pow' (by omega) hpow'spec with ⟨hsize, huntouched, hspec⟩ + have hcol'size : col'.size = col.size := by + rw [hcol'] + simp + refine ⟨by rw [hsize, hcol'size], ?_, ?_⟩ + · intro j hj + have hj' : j < j₀ + 1 ∨ j₀ + 1 + n ≤ j := by omega + rw [huntouched j hj'] + rw [hcol'] + rcases hj with hj | hj + · rw [Array.getD_eq_getD_getElem?, Array.getD_eq_getD_getElem?] + rw [Array.getElem?_setIfInBounds_ne (by omega)] + · rw [Array.getD_eq_getD_getElem?, Array.getD_eq_getD_getElem?] + rw [Array.getElem?_setIfInBounds_ne (by omega)] + · intro j hj₁ hj₂ hj₃ + by_cases hj : j = j₀ + · subst hj + have huntouchedj : ((List.range' (j + 1) n).foldl + (relationColumnStep mulCtx modCtx reducedR modulus b) (col', pow')).1.getD j 0 = + col'.getD j 0 := huntouched j (by omega) + rw [huntouchedj, hcol'] + have hset : (col.setIfInBounds j + (CPolynomial.C ((j.choose b : F)) * pow)).getD j 0 = + CPolynomial.C ((j.choose b : F)) * pow := by + rw [Array.getD_eq_getD_getElem?, + Array.getElem?_setIfInBounds_self_of_lt (by omega)] + simp + rw [hset] + rw [CPolynomial.toPoly_mul, CPolynomial.C_toPoly] + have hfactor : Polynomial.C ((j.choose b : F)) * pow.toPoly - + Polynomial.C ((j.choose b : F)) * R.toPoly ^ (j - b) = + Polynomial.C ((j.choose b : F)) * (pow.toPoly - R.toPoly ^ (j - b)) := by + ring + rw [hfactor] + exact Dvd.dvd.mul_left hpow _ + · exact hspec j (by omega) (by omega) (by rw [hcol'size]; exact hj₃) + +omit [DecidableEq F] in +/-- Size of the GS relation column. -/ +theorem gsRelationColumn_size (mulCtx : CPolynomial.MulContext F) + (modCtx : CPolynomial.ModContext F) (R modulus : CPolynomial F) (width b : Nat) : + (gsRelationColumn mulCtx modCtx R modulus width b).size = width := by + rw [gsRelationColumn_eq_foldl] + rw [(relationColumn_foldl_size_untouched mulCtx modCtx + (PolynomialMatrix.modByMonicWith modCtx R modulus) modulus b (width - b) b + (Array.replicate width 0) 1).1] + simp + +omit [DecidableEq F] in +/-- Entries of the GS relation column below the diagonal vanish. -/ +theorem gsRelationColumn_getD_of_lt (mulCtx : CPolynomial.MulContext F) + (modCtx : CPolynomial.ModContext F) (R modulus : CPolynomial F) + {width b j : Nat} (hj : j < b) : + (gsRelationColumn mulCtx modCtx R modulus width b).getD j 0 = 0 := by + rw [gsRelationColumn_eq_foldl] + rw [(relationColumn_foldl_size_untouched mulCtx modCtx + (PolynomialMatrix.modByMonicWith modCtx R modulus) modulus b (width - b) b + (Array.replicate width 0) 1).2 j hj] + rw [Array.getD_eq_getD_getElem?, Array.getElem?_replicate] + split <;> simp + +omit [DecidableEq F] in +/-- Entries of the GS relation column are congruent to the binomial powers. -/ +theorem gsRelationColumn_getD_congr (mulCtx : CPolynomial.MulContext F) + (modCtx : CPolynomial.ModContext F) {R modulus : CPolynomial F} + (hM : Polynomial.Monic modulus.toPoly) + {width b j : Nat} (hbj : b ≤ j) (hj : j < width) : + modulus.toPoly ∣ + ((gsRelationColumn mulCtx modCtx R modulus width b).getD j 0).toPoly - + Polynomial.C ((j.choose b : F)) * R.toPoly ^ (j - b) := by + rw [gsRelationColumn_eq_foldl] + have hred := dvd_modByMonicWith_sub modCtx (p := R) (M := modulus) hM + rcases relationColumn_foldl_spec mulCtx modCtx (R := R) + (reducedR := PolynomialMatrix.modByMonicWith modCtx R modulus) + (modulus := modulus) hM hred b (width - b) b (Array.replicate width 0) 1 + le_rfl (by simp [CPolynomial.toPoly_one]) with ⟨_, _, hspec⟩ + exact hspec j hbj (by omega) (by simpa using hj) + +/-! ## The GS relation matrix and the modular row predicate -/ + +omit [DecidableEq F] in +/-- The `b`-th GS modulus is `G^(s-b)`, with default value `1`. -/ +theorem gsModuli_getD_one (mulCtx : CPolynomial.MulContext F) (G : CPolynomial F) + {b s : Nat} (hb : b < s) : + (gsModuli mulCtx G s).getD b 1 = G ^ (s - b) := by + have hsize : (gsModuli mulCtx G s).size = s := gsModuli_size mulCtx G s + have h0 := gsModuli_getD mulCtx G hb + rw [Array.getD_eq_getD_getElem?, Array.getElem?_eq_getElem (by omega), + Option.getD_some] at h0 ⊢ + exact h0 + +omit [DecidableEq F] in +/-- Entry access for the GS relation matrix. -/ +theorem gsRelationMatrixWithModuli_entry (mulCtx : CPolynomial.MulContext F) + (modCtx : CPolynomial.ModContext F) (R : CPolynomial F) + (moduli : Array (CPolynomial F)) (params : GSInterpParams) {k b : Nat} + (hk : k < interpolationWidth params) (hb : b < params.multiplicity) : + rowGet ((gsRelationMatrixWithModuli mulCtx modCtx R moduli params).getD k #[]) b = + (gsRelationColumn mulCtx modCtx R (moduli.getD b 1) + (interpolationWidth params) b).getD k 0 := by + rw [gsRelationMatrixWithModuli, ofFn_rowGet _ _ _ hk hb] + congr 1 + rw [Array.getD_eq_getD_getElem?, List.getElem?_toArray, List.getElem?_map, + List.getElem?_range hb, Option.map_some, Option.getD_some] + +omit [DecidableEq F] in +/-- The GS relation matrix has one row per interpolation coefficient. -/ +theorem gsRelationMatrixWithModuli_size (mulCtx : CPolynomial.MulContext F) + (modCtx : CPolynomial.ModContext F) (R : CPolynomial F) + (moduli : Array (CPolynomial F)) (params : GSInterpParams) : + (gsRelationMatrixWithModuli mulCtx modCtx R moduli params).size = + interpolationWidth params := + ofFn_size _ _ _ + +omit [DecidableEq F] in +/-- The GS relation matrix has one column per multiplicity level. -/ +theorem gsRelationMatrixWithModuli_matrixWidth (mulCtx : CPolynomial.MulContext F) + (modCtx : CPolynomial.ModContext F) (R : CPolynomial F) + (moduli : Array (CPolynomial F)) (params : GSInterpParams) : + MatrixWidth (gsRelationMatrixWithModuli mulCtx modCtx R moduli params) = + params.multiplicity := + ofFn_matrixWidth _ _ _ (Nat.succ_pos _) + +omit [DecidableEq F] in +/-- Outer-coefficient expansion of a coefficient row under `toPoly`. -/ +private theorem toPoly_ofCoeffRow_eq_sum (row : PolynomialRow F) : + (CBivariate.ofCoeffRow row).toPoly = + ∑ k ∈ Finset.range row.size, + Polynomial.monomial k ((row.getD k 0).toPoly) := by + ext n + rw [CBivariate.toPoly_coeff, Polynomial.finsetSum_coeff] + by_cases hn : n < row.size + · rw [Finset.sum_eq_single n + (fun k _hk hkn ↦ by rw [Polynomial.coeff_monomial, if_neg hkn]) + (fun hnotin ↦ absurd (Finset.mem_range.mpr hn) hnotin)] + rw [Polynomial.coeff_monomial, if_pos rfl, CBivariate.ofCoeffRow, + CPolynomial.coeff_ofArray] + · rw [Finset.sum_eq_zero + (fun k hk ↦ by + rw [Polynomial.coeff_monomial, + if_neg (by rcases Finset.mem_range.mp hk with h; omega)])] + rw [CBivariate.ofCoeffRow, CPolynomial.coeff_ofArray, + Array.getD_eq_getD_getElem?, Array.getElem?_eq_none (by omega)] + simp [CPolynomial.toPoly_zero] + +omit [DecidableEq F] in +/-- Sheared coefficients of a coefficient row: the outer Hasse derivative of +`ofCoeffRow row` evaluated at `R` is the binomial-weighted power sum. -/ +theorem hasseDeriv_toPoly_ofCoeffRow_eval (row : PolynomialRow F) + (R : Polynomial F) (b : Nat) : + (Polynomial.hasseDeriv b (CBivariate.ofCoeffRow row).toPoly).eval R = + ∑ k ∈ Finset.range row.size, + Polynomial.C ((k.choose b : F)) * (row.getD k 0).toPoly * R ^ (k - b) := by + rw [toPoly_ofCoeffRow_eq_sum, map_sum, Polynomial.eval_finsetSum] + refine Finset.sum_congr rfl fun k _hk ↦ ?_ + rw [Polynomial.hasseDeriv_monomial, Polynomial.eval_monomial, + Polynomial.C_eq_natCast] + +omit [DecidableEq F] in +/-- Column entries of a row-by-matrix product as binomial sums. -/ +private theorem rowGet_rowMulMatrixWith_eq_sum (mulCtx : CPolynomial.MulContext F) + (row : PolynomialRow F) (M : PolynomialMatrix F) {j : Nat} + (hj : j < MatrixWidth M) : + rowGet (rowMulMatrixWith mulCtx row M) j = + ∑ k ∈ Finset.range row.size, rowGet row k * rowGet (M.getD k #[]) j := by + rw [rowMulMatrixWith, rowGet] + rw [Array.getD_eq_getD_getElem?, List.getElem?_toArray, List.getElem?_map, + List.getElem?_range hj, Option.map_some, Option.getD_some] + rw [foldl_add_eq_sum (fun k ↦ mulCtx.mul (rowGet row k) (rowGet (M.getD k #[]) j))] + refine Finset.sum_congr rfl fun k _hk ↦ ?_ + rw [mulCtx.mul_eq_mul] + +omit [DecidableEq F] in +/-- One product-entry fold as a sum over the matrix height. -/ +private theorem rowMulMatrix_foldl_eq_sum_size (mulCtx : CPolynomial.MulContext F) + (row : PolynomialRow F) (M : PolynomialMatrix F) (j : Nat) : + (List.range row.size).foldl + (fun acc k ↦ acc + mulCtx.mul (rowGet row k) (rowGet (M.getD k #[]) j)) 0 = + ∑ k ∈ Finset.range M.size, rowGet row k * rowGet (M.getD k #[]) j := by + rw [foldl_add_eq_sum (fun k ↦ mulCtx.mul (rowGet row k) (rowGet (M.getD k #[]) j))] + have hterm : ∀ k, mulCtx.mul (rowGet row k) (rowGet (M.getD k #[]) j) = + rowGet row k * rowGet (M.getD k #[]) j := fun k ↦ mulCtx.mul_eq_mul _ _ + simp only [hterm] + rcases Nat.le_total row.size M.size with h | h + · refine Finset.sum_subset + (by intro x hx; simp only [Finset.mem_range] at hx ⊢; omega) fun k _hk hknot ↦ ?_ + have hk : row.size ≤ k := by simpa using hknot + have hzero : rowGet row k = 0 := by + rw [rowGet, Array.getD_eq_getD_getElem?, Array.getElem?_eq_none hk] + rfl + rw [hzero, zero_mul] + · symm + refine Finset.sum_subset + (by intro x hx; simp only [Finset.mem_range] at hx ⊢; omega) fun k _hk hknot ↦ ?_ + have hk : M.size ≤ k := by simpa using hknot + have hzero : M.getD k #[] = #[] := by + rw [Array.getD_eq_getD_getElem?, Array.getElem?_eq_none hk] + rfl + rw [hzero, show rowGet (#[] : PolynomialRow F) j = 0 from rfl, mul_zero] + +omit [DecidableEq F] in +/-- The row-by-matrix product only sees the first `M.size` row entries. -/ +theorem rowMulMatrixWith_congr_of_agree (mulCtx : CPolynomial.MulContext F) + {row row' : PolynomialRow F} (M : PolynomialMatrix F) + (hagree : ∀ k, k < M.size → rowGet row k = rowGet row' k) : + rowMulMatrixWith mulCtx row M = rowMulMatrixWith mulCtx row' M := by + rw [rowMulMatrixWith, rowMulMatrixWith] + congr 1 + refine List.map_congr_left fun j _hj ↦ ?_ + rw [rowMulMatrix_foldl_eq_sum_size, rowMulMatrix_foldl_eq_sum_size] + refine Finset.sum_congr rfl fun k hk ↦ ?_ + rw [hagree k (Finset.mem_range.mp hk)] + +omit [DecidableEq F] in +/-- The modular row predicate only sees the first `M.size` row entries. -/ +theorem rowSatisfiesModularBool_congr_of_agree (mulCtx : CPolynomial.MulContext F) + (modCtx : CPolynomial.ModContext F) {row row' : PolynomialRow F} + (M : PolynomialMatrix F) (moduli : Array (CPolynomial F)) + (hagree : ∀ k, k < M.size → rowGet row k = rowGet row' k) : + rowSatisfiesModularBool mulCtx modCtx row M moduli = + rowSatisfiesModularBool mulCtx modCtx row' M moduli := by + rw [rowSatisfiesModularBool, rowSatisfiesModularBool, rowMulMatrixModDiagonalWith, + rowMulMatrixModDiagonalWith, rowMulMatrixWith_congr_of_agree mulCtx M hagree] + +omit [DecidableEq F] in +/-- The executable modular row predicate, columnwise. -/ +private theorem rowSatisfiesModularBool_iff_forall (mulCtx : CPolynomial.MulContext F) + (modCtx : CPolynomial.ModContext F) (row : PolynomialRow F) + (M : PolynomialMatrix F) (moduli : Array (CPolynomial F)) : + rowSatisfiesModularBool mulCtx modCtx row M moduli = true ↔ + ∀ b, b < moduli.size → + PolynomialMatrix.modByMonicWith modCtx + (rowGet (rowMulMatrixWith mulCtx row M) b) (moduli.getD b 0) = 0 := by + rw [rowSatisfiesModularBool, rowMulMatrixModDiagonalWith, rowModDiagonalWith] + rw [Array.all_eq_true] + constructor + · intro h b hb + have hb' : b < (((List.range moduli.size).map fun j ↦ + PolynomialMatrix.modByMonicWith modCtx + (rowGet (rowMulMatrixWith mulCtx row M) j) (moduli.getD j 0)).toArray).size := by + simpa using hb + have := h b hb' + simpa using this + · intro h b hb + have hb' : b < moduli.size := by simpa using hb + simpa using h b hb' + +omit [DecidableEq F] in +/-- The executable GS modular row predicate over the relation matrix and +modulus array is exactly divisibility of every sheared coefficient +`(hasseDeriv b (ofCoeffRow row).toPoly).eval R` by `G^(s-b)`. -/ +theorem rowSatisfiesModularBool_gsRelationMatrix_iff + (mulCtx : CPolynomial.MulContext F) (modCtx : CPolynomial.ModContext F) + {G : CPolynomial F} (hG : Polynomial.Monic G.toPoly) + (R : CPolynomial F) (params : GSInterpParams) {row : PolynomialRow F} + (hwidth : row.size ≤ interpolationWidth params) : + rowSatisfiesModularBool mulCtx modCtx row + (gsRelationMatrixWithRG mulCtx modCtx R G params) + (gsModuli mulCtx G params.multiplicity) = true ↔ + ∀ b, b < params.multiplicity → + G.toPoly ^ (params.multiplicity - b) ∣ + (Polynomial.hasseDeriv b (CBivariate.ofCoeffRow row).toPoly).eval R.toPoly := by + set s := params.multiplicity with hs + set width := interpolationWidth params with hwidthdef + set M := gsRelationMatrixWithRG mulCtx modCtx R G params with hM + have hMsize : MatrixWidth M = s := + gsRelationMatrixWithModuli_matrixWidth mulCtx modCtx R _ params + have hmodsize : (gsModuli mulCtx G s).size = s := gsModuli_size mulCtx G s + rw [rowSatisfiesModularBool_iff_forall, hmodsize] + refine forall_congr' fun b ↦ ?_ + refine imp_congr_right fun hb ↦ ?_ + have hmonic : Polynomial.Monic ((G ^ (s - b)).toPoly) := by + rw [CPolynomial.toPoly_pow] + exact hG.pow _ + rw [gsModuli_getD mulCtx G hb] + rw [modByMonicWith_eq_zero_iff_dvd modCtx hmonic] + rw [CPolynomial.toPoly_pow] + rw [hasseDeriv_toPoly_ofCoeffRow_eval] + have hentry : ∀ k, k < row.size → + G.toPoly ^ (s - b) ∣ + (rowGet row k * rowGet (M.getD k #[]) b).toPoly - + Polynomial.C ((k.choose b : F)) * (rowGet row k).toPoly * + R.toPoly ^ (k - b) := by + intro k hk + have hkwidth : k < width := by omega + have hMk : rowGet (M.getD k #[]) b = + (gsRelationColumn mulCtx modCtx R (G ^ (s - b)) width b).getD k 0 := by + rw [hM, gsRelationMatrixWithRG, + gsRelationMatrixWithModuli_entry mulCtx modCtx R _ params hkwidth hb, + gsModuli_getD_one mulCtx G hb] + rw [hMk, CPolynomial.toPoly_mul] + rcases Nat.lt_or_ge k b with hkb | hkb + · rw [gsRelationColumn_getD_of_lt mulCtx modCtx R (G ^ (s - b)) hkb] + rw [Nat.choose_eq_zero_of_lt hkb] + simp [CPolynomial.toPoly_zero] + · have hcongr := gsRelationColumn_getD_congr mulCtx modCtx + (R := R) (modulus := G ^ (s - b)) hmonic hkb hkwidth + have hmul := Dvd.dvd.mul_left hcongr (rowGet row k).toPoly + have hexpand : (rowGet row k).toPoly * + (((gsRelationColumn mulCtx modCtx R (G ^ (s - b)) width b).getD k 0).toPoly - + Polynomial.C ((k.choose b : F)) * R.toPoly ^ (k - b)) = + (rowGet row k).toPoly * + ((gsRelationColumn mulCtx modCtx R (G ^ (s - b)) width b).getD k 0).toPoly - + Polynomial.C ((k.choose b : F)) * (rowGet row k).toPoly * + R.toPoly ^ (k - b) := by + ring + rw [hexpand] at hmul + rwa [CPolynomial.toPoly_pow] at hmul + have hsum : G.toPoly ^ (s - b) ∣ + (rowGet (rowMulMatrixWith mulCtx row M) b).toPoly - + ∑ k ∈ Finset.range row.size, + Polynomial.C ((k.choose b : F)) * (rowGet row k).toPoly * + R.toPoly ^ (k - b) := by + rw [rowGet_rowMulMatrixWith_eq_sum mulCtx row M (by omega)] + rw [toPoly_finset_sum] + rw [← Finset.sum_sub_distrib] + exact Finset.dvd_sum fun k hk ↦ hentry k (Finset.mem_range.mp hk) + rw [dvd_iff_dvd_of_dvd_sub hsum] + congr! 1 + +end ApproximantBasis + +end GuruswamiSudan + +end CompPoly diff --git a/CompPoly/Bivariate/GuruswamiSudan/Interpolation/ApproximantBasis/Multiplicity.lean b/CompPoly/Bivariate/GuruswamiSudan/Interpolation/ApproximantBasis/Multiplicity.lean new file mode 100644 index 00000000..de4bc577 --- /dev/null +++ b/CompPoly/Bivariate/GuruswamiSudan/Interpolation/ApproximantBasis/Multiplicity.lean @@ -0,0 +1,266 @@ +/- +Copyright (c) 2026 CompPoly Contributors. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Valerii Huhnin +-/ + +module + +-- `linearFactor`, `coeff` and friends are declared in bare `public section`s, so +-- their bodies are opaque downstream; see `docs/wiki/module-system.md`. +import all CompPoly.Univariate.Basic +import all CompPoly.Univariate.Raw.Core +public import CompPoly.Bivariate.GuruswamiSudan.Interpolation.Basic +public import CompPoly.Bivariate.GuruswamiSudan.PolynomialCorrectness +public import CompPoly.Univariate.Vanishing +public import Mathlib.Algebra.Polynomial.Taylor + +/-! +# Shear Equivalence for Guruswami-Sudan Multiplicity Constraints + +The approximant-basis backend reduces GS interpolation to diagonal modular +congruences on the `Z^b`-coefficients of the sheared polynomial `Q(X, R + Z)`. +This file proves the underlying semantic equivalence: for distinct +interpolation nodes, the packed multiplicity constraints on `Q` hold iff every +sheared coefficient `C_b = (hasseDeriv b Q.toPoly).eval R` is divisible by +`G^(s-b)`, where `G` is the vanishing polynomial of the nodes and `R` +interpolates the values. + +The proof works one node at a time. Writing `M_b = (hasseDeriv b Q.toPoly)` +for the outer `Y`-Hasse derivatives, multiplicity at `(x, y)` says that the +family `M_b.eval (C y)` is `(X - x)^(s-b)`-divisible, while the modular +congruence constrains the family `M_b.eval R`. Because `R` and `C y` agree at +`x`, an outer Taylor expansion transfers each divisibility family to the +other; distinctness of the nodes then glues the per-node factors into powers +of `G`. +-/ + +@[expose] public section + +namespace CompPoly + +namespace GuruswamiSudan + +namespace ApproximantBasis + +open Polynomial + +variable {F : Type*} [Field F] [BEq F] [LawfulBEq F] [DecidableEq F] + +omit [BEq F] [LawfulBEq F] [DecidableEq F] in +/-- `(X - x)^k` divides `A` iff the first `k` Hasse derivatives of `A` vanish +at `x`. -/ +theorem X_sub_C_pow_dvd_iff_hasseDeriv_eval_eq_zero + (A : Polynomial F) (x : F) (k : Nat) : + (X - C x) ^ k ∣ A ↔ + ∀ a, a < k → (Polynomial.hasseDeriv a A).eval x = 0 := by + rw [Polynomial.X_sub_C_pow_dvd_iff, Polynomial.X_pow_dvd_iff] + constructor + · intro h a ha + have hcoeff := h a ha + rwa [← Polynomial.taylor_apply, Polynomial.taylor_coeff] at hcoeff + · intro h d hd + rw [← Polynomial.taylor_apply, Polynomial.taylor_coeff] + exact h d hd + +omit [BEq F] [LawfulBEq F] [DecidableEq F] in +/-- Shear-transfer core: the `(X - x)`-adic divisibility family of the outer +Hasse-derivative evaluations of `P` moves between outer evaluation points `u` +and `v` that agree modulo `X - x`. -/ +theorem X_sub_C_pow_dvd_hasseDeriv_eval_of_dvd_sub + {P : Polynomial (Polynomial F)} {u v : Polynomial F} {x : F} {s : Nat} + (huv : (X - C x) ∣ (v - u)) + (h : ∀ b, b < s → + (X - C x) ^ (s - b) ∣ (Polynomial.hasseDeriv b P).eval u) + {b : Nat} (hb : b < s) : + (X - C x) ^ (s - b) ∣ (Polynomial.hasseDeriv b P).eval v := by + have heval : (Polynomial.hasseDeriv b P).eval v = + (Polynomial.taylor u (Polynomial.hasseDeriv b P)).eval (v - u) := by + rw [Polynomial.taylor_apply, Polynomial.eval_comp] + simp + rw [heval, Polynomial.eval_eq_sum, Polynomial.sum_def] + refine Finset.dvd_sum fun t _ht ↦ ?_ + rw [Polynomial.taylor_coeff] + by_cases hts : t + b < s + · have hsplit : (X - C x) ^ (s - b) = + (X - C x) ^ (s - (t + b)) * (X - C x) ^ t := by + rw [← pow_add] + congr 1 + omega + rw [hsplit] + refine mul_dvd_mul ?_ (pow_dvd_pow_of_dvd huv t) + have hcomp : Polynomial.hasseDeriv t (Polynomial.hasseDeriv b P) = + (t + b).choose t • Polynomial.hasseDeriv (t + b) P := by + have hmap := Polynomial.hasseDeriv_comp (R := Polynomial F) t b + calc Polynomial.hasseDeriv t (Polynomial.hasseDeriv b P) + = ((Polynomial.hasseDeriv t).comp (Polynomial.hasseDeriv b)) P := rfl + _ = ((t + b).choose t • Polynomial.hasseDeriv (t + b)) P := by rw [hmap] + _ = (t + b).choose t • Polynomial.hasseDeriv (t + b) P := rfl + rw [hcomp] + have heval_smul : + (((t + b).choose t • Polynomial.hasseDeriv (t + b) P).eval u) = + (t + b).choose t • ((Polynomial.hasseDeriv (t + b) P).eval u) := by + simp + rw [heval_smul, nsmul_eq_mul] + exact Dvd.dvd.mul_left (h (t + b) hts) _ + · have hpow : (X - C x) ^ (s - b) ∣ (v - u) ^ t := + dvd_trans (pow_dvd_pow _ (by omega)) (pow_dvd_pow_of_dvd huv t) + exact Dvd.dvd.mul_left hpow _ + +/-- At a single node `(x, y)` with `R(x) = y`, the `(X - x)`-adic divisibility +family of the sheared coefficients `(hasseDeriv b Q.toPoly).eval R` is +equivalent to GS multiplicity of `Q` at the node. -/ +theorem X_sub_C_pow_dvd_hasseDeriv_eval_iff_hasMultiplicity + {Q : CBivariate F} {R : Polynomial F} {x y : F} {s : Nat} + (hRx : R.eval x = y) : + (∀ b, b < s → + (X - C x) ^ (s - b) ∣ (Polynomial.hasseDeriv b Q.toPoly).eval R) ↔ + CBivariate.hasMultiplicity Q s x y := by + have hdvd_sub : (X - C x) ∣ (R - C y) := by + rw [Polynomial.dvd_iff_isRoot] + simp [Polynomial.IsRoot, hRx] + have hdvd_sub' : (X - C x) ∣ (C y - R) := by + have hneg := dvd_neg.mpr hdvd_sub + rwa [neg_sub] at hneg + have hfamilyCy : (∀ b, b < s → (X - C x) ^ (s - b) ∣ + (Polynomial.hasseDeriv b Q.toPoly).eval (C y)) ↔ + CBivariate.hasMultiplicity Q s x y := by + rw [CBivariate.hasMultiplicity_iff_hasMultiplicityAtLeast] + unfold CBivariate.HasMultiplicityAtLeast + constructor + · intro h a b hab + have hb : b < s := by omega + have hz := (X_sub_C_pow_dvd_iff_hasseDeriv_eval_eq_zero _ x (s - b)).1 + (h b hb) a (by omega) + rwa [CBivariate.eval_hasseDeriv_eval_hasseDeriv_toPoly] at hz + · intro h b hb + rw [X_sub_C_pow_dvd_iff_hasseDeriv_eval_eq_zero] + intro a ha + rw [CBivariate.eval_hasseDeriv_eval_hasseDeriv_toPoly] + exact h a b (by omega) + constructor + · intro h + refine hfamilyCy.1 fun b hb ↦ ?_ + exact X_sub_C_pow_dvd_hasseDeriv_eval_of_dvd_sub hdvd_sub' h hb + · intro h b hb + exact X_sub_C_pow_dvd_hasseDeriv_eval_of_dvd_sub hdvd_sub + (hfamilyCy.2 h) hb + +omit [DecidableEq F] in +/-- The linear factor of one node under `toPoly`. -/ +theorem linearFactor_toPoly_eq (x : F) : + (CPolynomial.linearFactor x).toPoly = (X - C x : Polynomial F) := by + rw [CPolynomial.linearFactor, CPolynomial.toPoly_add, CPolynomial.X_toPoly, + CPolynomial.C_toPoly] + simp [sub_eq_add_neg, add_comm] + +omit [DecidableEq F] in +private theorem vanishingPolynomialArray_toPoly_list + (xs : List F) (acc : CPolynomial F) : + (xs.foldl (fun acc x ↦ acc * CPolynomial.linearFactor x) acc).toPoly = + acc.toPoly * (xs.map fun x ↦ (X - C x : Polynomial F)).prod := by + induction xs generalizing acc with + | nil => + simp + | cons x xs ih => + rw [List.foldl_cons, ih, CPolynomial.toPoly_mul, linearFactor_toPoly_eq] + simp only [List.map_cons, List.prod_cons] + ring + +omit [DecidableEq F] in +/-- The array vanishing polynomial is the product of the node linear factors +under `toPoly`. -/ +theorem vanishingPolynomialArray_toPoly (xs : Array F) : + (CPolynomial.vanishingPolynomialArray xs).toPoly = + (((xs.toList : Multiset F).map fun x ↦ + (X - C x : Polynomial F))).prod := by + rw [CPolynomial.vanishingPolynomialArray] + have hlist := vanishingPolynomialArray_toPoly_list xs.toList (1 : CPolynomial F) + simpa [CPolynomial.toPoly_one, Multiset.map_coe, Multiset.prod_coe] using hlist + +omit [DecidableEq F] in +/-- The array vanishing polynomial is monic over the underlying `toPoly` +image. -/ +theorem vanishingPolynomialArray_toPoly_monic (xs : Array F) : + Polynomial.Monic (CPolynomial.vanishingPolynomialArray xs).toPoly := by + rw [vanishingPolynomialArray_toPoly] + exact Polynomial.monic_multiset_prod_of_monic _ _ + fun x _hx ↦ Polynomial.monic_X_sub_C x + +omit [BEq F] [LawfulBEq F] in +/-- Per-node `(X - x)^k` divisibility glues to divisibility by the `k`-th +power of the product of distinct linear factors. -/ +theorem prod_X_sub_C_pow_dvd_of_nodup + {A : Polynomial F} {xs : List F} {k : Nat} + (hA : A ≠ 0) (hnodup : xs.Nodup) + (hroot : ∀ x, x ∈ xs → (X - C x) ^ k ∣ A) : + (((xs : Multiset F).map fun x ↦ (X - C x : Polynomial F)).prod) ^ k ∣ A := by + let ms : Multiset F := xs + have hmsNodup : ms.Nodup := by + simpa [ms] using hnodup + have hle : k • ms ≤ A.roots := by + rw [Multiset.le_iff_count] + intro x + rw [Multiset.count_nsmul] + by_cases hx : x ∈ ms + · have hxCount : Multiset.count x ms = 1 := + Multiset.count_eq_one_of_mem hmsNodup hx + rw [hxCount, mul_one] + have hdiv : (X - C x) ^ k ∣ A := hroot x (by simpa [ms] using hx) + rw [Polynomial.count_roots] + exact (Polynomial.le_rootMultiplicity_iff hA).2 hdiv + · rw [Multiset.count_eq_zero_of_notMem hx, mul_zero] + exact Nat.zero_le _ + have hprod : + (((k • ms).map fun x ↦ (X - C x : Polynomial F)).prod) ∣ A := + (Multiset.prod_X_sub_C_dvd_iff_le_roots hA (k • ms)).2 hle + rw [Multiset.map_nsmul, Multiset.prod_nsmul] at hprod + simpa [ms] using hprod + +/-- Batch shear equivalence: over distinct nodes, divisibility of every +sheared coefficient `C_b` by `G^(s-b)` is equivalent to the packed GS +multiplicity constraints. -/ +theorem vanishing_pow_dvd_hasseDeriv_eval_iff_satisfiesMultiplicityConstraints + {points : Array (F × F)} {Q : CBivariate F} {R : CPolynomial F} {s : Nat} + (hdistinct : DistinctXCoordinates points) + (hR : ∀ point, point ∈ points.toList → CPolynomial.eval point.1 R = point.2) : + (∀ b, b < s → + (CPolynomial.vanishingPolynomialArray (points.map fun p ↦ p.1)).toPoly ^ (s - b) ∣ + (Polynomial.hasseDeriv b Q.toPoly).eval R.toPoly) ↔ + CBivariate.SatisfiesMultiplicityConstraints Q points s := by + have hnodup : (points.map fun p ↦ p.1).toList.Nodup := by + simpa [DistinctXCoordinates, Array.toList_map] using hdistinct + have hRp : ∀ point, point ∈ points.toList → + Polynomial.eval point.1 R.toPoly = point.2 := by + intro point hpoint + rw [← CPolynomial.eval_toPoly] + exact hR point hpoint + rw [CBivariate.satisfiesMultiplicityConstraints_iff_hasMultiplicity] + constructor + · intro h point hpoint + refine (X_sub_C_pow_dvd_hasseDeriv_eval_iff_hasMultiplicity + (hRp point hpoint)).1 ?_ + intro b hb + refine dvd_trans (pow_dvd_pow_of_dvd ?_ (s - b)) (h b hb) + rw [vanishingPolynomialArray_toPoly] + refine Multiset.dvd_prod ?_ + rw [Multiset.mem_map] + refine ⟨point.1, ?_, rfl⟩ + rw [Multiset.mem_coe, Array.toList_map] + exact List.mem_map.mpr ⟨point, hpoint, rfl⟩ + · intro h b hb + by_cases hzero : (Polynomial.hasseDeriv b Q.toPoly).eval R.toPoly = 0 + · simp [hzero] + · rw [vanishingPolynomialArray_toPoly] + refine prod_X_sub_C_pow_dvd_of_nodup hzero hnodup ?_ + intro x hx + rw [Array.toList_map] at hx + rcases List.mem_map.mp hx with ⟨point, hpoint, rfl⟩ + exact (X_sub_C_pow_dvd_hasseDeriv_eval_iff_hasMultiplicity + (hRp point hpoint)).2 (h point hpoint) b hb + +end ApproximantBasis + +end GuruswamiSudan + +end CompPoly diff --git a/CompPoly/Bivariate/GuruswamiSudan/Interpolation/Basic.lean b/CompPoly/Bivariate/GuruswamiSudan/Interpolation/Basic.lean index 922254a3..344845d8 100644 --- a/CompPoly/Bivariate/GuruswamiSudan/Interpolation/Basic.lean +++ b/CompPoly/Bivariate/GuruswamiSudan/Interpolation/Basic.lean @@ -5,6 +5,7 @@ Authors: Valerii Huhnin -/ module +public import CompPoly.Bivariate.CoeffRows public import CompPoly.Bivariate.FactorMonic public import CompPoly.Bivariate.GuruswamiSudan.Context @@ -27,6 +28,28 @@ open CBivariate def interpolationMonomials (params : GSInterpParams) : Array CBivariate.Monomial := CBivariate.monomialsWeightedDegreeLE 1 (yWeight params) params.weightedDegreeBound +/-- Finite `Y` cap used by positive-`Y`-weight interpolation backends. -/ +def interpolationYCap (params : GSInterpParams) : Nat := + params.weightedDegreeBound / yWeight params + +/-- Number of coefficient columns used by bounded-`Y` interpolation backends. -/ +def interpolationWidth (params : GSInterpParams) : Nat := + interpolationYCap params + 1 + +/-- Guruswami-Sudan shifted-degree shifts, `shift[j] = j * yWeight params`. -/ +def interpolationShifts (params : GSInterpParams) : Array Nat := + CBivariate.weightedDegreeShift (yWeight params) (interpolationWidth params) + +/-- Executable duplicate-`x` detector for packed point lists. -/ +def distinctXCoordinatesListBool {F : Type*} [BEq F] : List (F × F) → Bool + | [] => true + | point :: rest => + !(rest.any fun other ↦ other.1 == point.1) && distinctXCoordinatesListBool rest + +/-- Executable duplicate-`x` detector for packed points. -/ +def distinctXCoordinatesBool {F : Type*} [BEq F] (points : Array (F × F)) : Bool := + distinctXCoordinatesListBool points.toList + /-- Packed interpolation constraint `(x, y, a, b)`. -/ structure InterpolationConstraint (F : Type*) where x : F diff --git a/CompPoly/Bivariate/GuruswamiSudan/Interpolation/Hybrid.lean b/CompPoly/Bivariate/GuruswamiSudan/Interpolation/Hybrid.lean new file mode 100644 index 00000000..2cf3eed6 --- /dev/null +++ b/CompPoly/Bivariate/GuruswamiSudan/Interpolation/Hybrid.lean @@ -0,0 +1,16 @@ +/- +Copyright (c) 2026 CompPoly Contributors. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Valerii Huhnin +-/ + +module + +public import CompPoly.Bivariate.GuruswamiSudan.Interpolation.Hybrid.Algorithm +public import CompPoly.Bivariate.GuruswamiSudan.Interpolation.Hybrid.Correctness + +/-! +# Hybrid Guruswami-Sudan Interpolation +-/ + +@[expose] public section diff --git a/CompPoly/Bivariate/GuruswamiSudan/Interpolation/Hybrid/Algorithm.lean b/CompPoly/Bivariate/GuruswamiSudan/Interpolation/Hybrid/Algorithm.lean new file mode 100644 index 00000000..e48fa5ee --- /dev/null +++ b/CompPoly/Bivariate/GuruswamiSudan/Interpolation/Hybrid/Algorithm.lean @@ -0,0 +1,112 @@ +/- +Copyright (c) 2026 CompPoly Contributors. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Valerii Huhnin +-/ + +module + +public import CompPoly.Bivariate.GuruswamiSudan.Interpolation.ApproximantBasis.Algorithm +public import CompPoly.Bivariate.GuruswamiSudan.Interpolation.LeeOSullivan.Algorithm + +/-! +# Hybrid Guruswami-Sudan Interpolation + +Output-sensitive interpolation backend combining the adaptive Lee-O'Sullivan +route with the order-driven approximant-basis route through a ski-rental +budget policy. + +The Mulders-Storjohann reduction of the Lee-O'Sullivan basis terminates after +a number of steps that vanishes on codewords and scales with the distance of +the received word from the code, while the approximant-basis backend costs +the same on every input. The hybrid runs the reduction with a calibrated step +budget `B` as fuel; if the budget is exhausted before the basis is +conflict-free, it falls back to the approximant solver, so the total cost is +within a constant factor of the cheaper route on every input. + +The budget is the entire fuel policy: the reduction loop stops by itself as +soon as the basis is conflict-free, so unused fuel is free and any cap below +`B` could only trigger premature fallback. (In particular the excess of the +initial shifted-degree sum over the weak-Popov floor is *not* a usable cap: +the step count includes leading-position moves that leave the shifted-degree +sum unchanged, so reductions routinely need more steps than that excess.) + +The dispatch affects performance only: both branches are complete verified +interpolators, and `Interpolation/Hybrid/Correctness.lean` shows the hybrid +result always coincides with one of them. + +## References + +* [Karlin, A. R., Manasse, M. S., Rudolph, L., and Sleator, D. D., + *Competitive snoopy caching*][KMRS88] +-/ + +@[expose] public section + +namespace CompPoly + +namespace GuruswamiSudan + +namespace Hybrid + +open PolynomialMatrix +open PolynomialMatrix.Approximant + +variable {F : Type*} [Field F] [BEq F] [LawfulBEq F] [DecidableEq F] + +/-- Positive-`Y`-weight hybrid interpolation branch: reduce the +Lee-O'Sullivan basis with the budgeted fuel and keep the result when it +reached shifted weak Popov form (no leading conflict remains), otherwise fall +back to the approximant-basis solver. -/ +def hybridPositiveInterpolate + (V : CPolynomial.VanishingPolynomialContext F) + (E : CPolynomial.BatchEvalContext F) + (solver : ModularSolutionBasisContext F) + (budget : GSInterpParams → Nat) + (points : Array (F × F)) (params : GSInterpParams) : + Option (CBivariate F) := + if distinctXCoordinatesBool points then + let G := V.vanishingPolynomial (points.map fun point ↦ point.1) + let R := CPolynomial.interpolateCoefficientFormWithVanishing E G points + let basis := leeOSullivanBasisRowsWithRG R G params + let shift := leeOSullivanShifts params + let reduced := muldersStorjohannReduceWithFuelFast (budget params) basis shift + match cachedLeadingConflict? (rowLeadingPositions reduced shift) with + | some _ => + ApproximantBasis.approximantBasisPositiveInterpolate V E solver + points params + | none => + match LeeOSullivan.leastShiftedDegreeRow? reduced shift with + | none => none + | some row => + match rowShiftedDegree? row shift with + | none => none + | some degree => + if degree ≤ params.weightedDegreeBound then + let rawQ := CBivariate.ofCoeffRow row + match LeeOSullivan.normalizeLeeCandidate? params rawQ with + | none => none + | some Q => some Q + else + none + else + none + +/-- Hybrid interpolation with the shared low-message branch. -/ +def hybridInterpolate + (V : CPolynomial.VanishingPolynomialContext F) + (E : CPolynomial.BatchEvalContext F) + (solver : ModularSolutionBasisContext F) + (budget : GSInterpParams → Nat) + (points : Array (F × F)) (params : GSInterpParams) : + Option (CBivariate F) := + if params.messageDegree ≤ 1 then + some (lowMessageDegreeInterpolation points params.multiplicity) + else + hybridPositiveInterpolate V E solver budget points params + +end Hybrid + +end GuruswamiSudan + +end CompPoly diff --git a/CompPoly/Bivariate/GuruswamiSudan/Interpolation/Hybrid/Correctness.lean b/CompPoly/Bivariate/GuruswamiSudan/Interpolation/Hybrid/Correctness.lean new file mode 100644 index 00000000..3bf086f7 --- /dev/null +++ b/CompPoly/Bivariate/GuruswamiSudan/Interpolation/Hybrid/Correctness.lean @@ -0,0 +1,146 @@ +/- +Copyright (c) 2026 CompPoly Contributors. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Valerii Huhnin +-/ + +module + +public import CompPoly.Bivariate.GuruswamiSudan.Interpolation.ApproximantBasis.Correctness +public import CompPoly.Bivariate.GuruswamiSudan.Interpolation.Hybrid.Algorithm +public import CompPoly.Bivariate.GuruswamiSudan.Interpolation.LeeOSullivan.Correctness +public import CompPoly.LinearAlgebra.PolynomialMatrix.MuldersStorjohannCorrectness.Fast + +/-! +# Correctness of Hybrid Guruswami-Sudan Interpolation + +The hybrid's fuel/fallback dispatch affects performance only: its result +always coincides with the result of one of the two verified backends +(`hybridPositiveInterpolate_eq_lee_or_approximant`). Soundness and +completeness of `hybridInterpContext` follow by case analysis from the +corresponding backend theorems. + +The only reduction-level fact needed is determinism of the fueled +Mulders-Storjohann loop (`muldersStorjohannReduceWithFuel_eq_of_no_conflict`): +when the budgeted probe ends without a shifted leading conflict, it stopped at +the same matrix as the full reduction, so the kept Lee branch is literally the +Lee-O'Sullivan backend run with the fast reducer context. +-/ + +@[expose] public section + +namespace CompPoly + +namespace GuruswamiSudan + +namespace Hybrid + +open PolynomialMatrix +open PolynomialMatrix.Approximant + +variable {F : Type*} [Field F] [BEq F] [LawfulBEq F] [DecidableEq F] + +/-- When the budgeted probe ends conflict-free it equals the full fast +reduction. -/ +private theorem hybrid_probe_eq_reduceFast + {fuel : Nat} {basis : PolynomialMatrix F} {shift : Array Nat} + (hWF : WellFormed basis) + (hconf : cachedLeadingConflict? + (rowLeadingPositions + (muldersStorjohannReduceWithFuelFast fuel basis shift) shift) = none) : + muldersStorjohannReduceWithFuelFast fuel basis shift = + muldersStorjohannReduceFast basis shift := by + rw [muldersStorjohannReduceWithFuelFast_eq, muldersStorjohannReduceFast_eq, + muldersStorjohannReduce] + apply muldersStorjohannReduceWithFuel_eq_of_no_conflict + · rw [cachedLeadingConflict?_eq, muldersStorjohannReduceWithFuelFast_eq] at hconf + exact hconf + · have hno := muldersStorjohannReduce_no_conflict basis shift hWF + rw [muldersStorjohannReduce] at hno + exact hno + +/-- The hybrid branch result always coincides with one of the two verified +backends. -/ +theorem hybridPositiveInterpolate_eq_lee_or_approximant + (V : CPolynomial.VanishingPolynomialContext F) + (E : CPolynomial.BatchEvalContext F) + (solver : ModularSolutionBasisContext F) + (budget : GSInterpParams → Nat) + (points : Array (F × F)) (params : GSInterpParams) : + hybridPositiveInterpolate V E solver budget points params = + LeeOSullivan.leeOSullivanPositiveInterpolate V E + (muldersStorjohannFastReducerContext F) points params ∨ + hybridPositiveInterpolate V E solver budget points params = + ApproximantBasis.approximantBasisPositiveInterpolate V E solver + points params := by + by_cases hdist : distinctXCoordinatesBool points + · simp only [hybridPositiveInterpolate, hdist, if_true] + split + case h_1 _ hconf => + right + rfl + case h_2 _ hconf => + left + rw [hybrid_probe_eq_reduceFast + (LeeOSullivan.leeOSullivanBasisRowsWithRG_wellFormed _ _ params) hconf] + simp only [LeeOSullivan.leeOSullivanPositiveInterpolate, hdist, if_true, + muldersStorjohannFastReducerContext] + rfl + · left + rw [hybridPositiveInterpolate, if_neg hdist, + LeeOSullivan.leeOSullivanPositiveInterpolate, if_neg hdist] + +/-- The hybrid result always coincides with one of the two verified +backends. -/ +theorem hybridInterpolate_eq_lee_or_approximant + (V : CPolynomial.VanishingPolynomialContext F) + (E : CPolynomial.BatchEvalContext F) + (solver : ModularSolutionBasisContext F) + (budget : GSInterpParams → Nat) + (points : Array (F × F)) (params : GSInterpParams) : + hybridInterpolate V E solver budget points params = + LeeOSullivan.leeOSullivanInterpolate V E + (muldersStorjohannFastReducerContext F) points params ∨ + hybridInterpolate V E solver budget points params = + ApproximantBasis.approximantBasisInterpolate V E solver points params := by + by_cases hlow : params.messageDegree ≤ 1 + · left + simp only [hybridInterpolate, LeeOSullivan.leeOSullivanInterpolate, hlow, + if_true] + · simp only [hybridInterpolate, LeeOSullivan.leeOSullivanInterpolate, + ApproximantBasis.approximantBasisInterpolate, hlow, if_false] + exact hybridPositiveInterpolate_eq_lee_or_approximant V E solver budget + points params + +/-- Public hybrid interpolation backend context: adaptive Lee-O'Sullivan +reduction under a step budget with an approximant-basis fallback. -/ +def hybridInterpContext + (V : CPolynomial.VanishingPolynomialContext F) + (E : CPolynomial.BatchEvalContext F) + (solver : ModularSolutionBasisContext F) + (budget : GSInterpParams → Nat) : GSInterpContext F where + interpolate := hybridInterpolate V E solver budget + sound := by + intro points params Q h + rcases hybridInterpolate_eq_lee_or_approximant V E solver budget points + params with heq | heq + · exact LeeOSullivan.leeOSullivanInterpolate_sound V E + (muldersStorjohannFastReducerContext F) (heq ▸ h) + · exact ApproximantBasis.approximantBasisInterpolate_sound V E solver + (heq ▸ h) + complete := by + intro points params hdistinct hexists + rcases hybridInterpolate_eq_lee_or_approximant V E solver budget points + params with heq | heq + · rw [heq] + exact LeeOSullivan.leeOSullivanInterpolate_complete V E + (muldersStorjohannFastReducerContext F) hdistinct hexists + · rw [heq] + exact ApproximantBasis.approximantBasisInterpolate_complete V E solver + points params hdistinct hexists + +end Hybrid + +end GuruswamiSudan + +end CompPoly diff --git a/CompPoly/Bivariate/GuruswamiSudan/Interpolation/LeeOSullivan/Basic.lean b/CompPoly/Bivariate/GuruswamiSudan/Interpolation/LeeOSullivan/Basic.lean index cffa1f27..b04fe1e7 100644 --- a/CompPoly/Bivariate/GuruswamiSudan/Interpolation/LeeOSullivan/Basic.lean +++ b/CompPoly/Bivariate/GuruswamiSudan/Interpolation/LeeOSullivan/Basic.lean @@ -20,10 +20,6 @@ namespace CompPoly namespace GuruswamiSudan -/-- Finite `Y` cap used by positive-`Y`-weight interpolation backends. -/ -def interpolationYCap (params : GSInterpParams) : Nat := - params.weightedDegreeBound / yWeight params - /-- Number of coefficient columns in the Lee-O'Sullivan module basis. -/ def leeOSullivanWidth (params : GSInterpParams) : Nat := interpolationYCap params + 1 @@ -36,16 +32,6 @@ def leeOSullivanShifts (params : GSInterpParams) : Array Nat := def leeOSullivanT (params : GSInterpParams) (i : Nat) : Nat := min i params.multiplicity -/-- Executable duplicate-`x` detector for packed point lists. -/ -def distinctXCoordinatesListBool {F : Type*} [BEq F] : List (F × F) → Bool - | [] => true - | point :: rest => - !(rest.any fun other ↦ other.1 == point.1) && distinctXCoordinatesListBool rest - -/-- Executable duplicate-`x` detector for packed points. -/ -def distinctXCoordinatesBool {F : Type*} [BEq F] (points : Array (F × F)) : Bool := - distinctXCoordinatesListBool points.toList - /-- The Lee-O'Sullivan basis polynomial `P_i`. -/ def leeOSullivanBasisPolynomial {F : Type*} [Field F] [BEq F] [LawfulBEq F] [DecidableEq F] diff --git a/CompPoly/Bivariate/GuruswamiSudan/Interpolation/WitnessDivisibility.lean b/CompPoly/Bivariate/GuruswamiSudan/Interpolation/WitnessDivisibility.lean new file mode 100644 index 00000000..6485f7c1 --- /dev/null +++ b/CompPoly/Bivariate/GuruswamiSudan/Interpolation/WitnessDivisibility.lean @@ -0,0 +1,120 @@ +/- +Copyright (c) 2026 CompPoly Contributors. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Valerii Huhnin +-/ + +module + +-- `linearFactor`, `coeff` and friends are declared in bare `public section`s, so +-- their bodies are opaque downstream; see `docs/wiki/module-system.md`. +import all CompPoly.Univariate.Basic +import all CompPoly.Univariate.Raw.Core +public import CompPoly.Bivariate.GuruswamiSudan.Interpolation.Basic +public import CompPoly.Univariate.CoefficientInterpolation +public import CompPoly.Univariate.Context +public import CompPoly.Univariate.Vanishing + +/-! +# Divisibility-Based Interpolation Witness Checking + +A fast full recognizer for the Guruswami-Sudan multiplicity constraints. + +The pointwise check (`CBivariate.satisfiesMultiplicityConstraintsBool`) +evaluates every Hasse derivative of order below `m` at every interpolation +point, costing `O(n * m^2 * terms(Q))` field operations. For point sets with +distinct `x`-coordinates the same property has a module-theoretic +characterization: writing `Q = Σ_t c_t * (Y - R)^t` for the base-`(Y - R)` +expansion of `Q`, where `R` interpolates the points, `Q` has a zero of +multiplicity at least `m` at every point if and only if +`G^(m - t) ∣ c_t` for all `t < m`, with `G = Π_i (X - x_i)` the vanishing +polynomial of the `x`-coordinates. + +`satisfiesMultiplicityConstraintsViaDivisibilityBool` checks the right-hand +side directly: it peels `m` digits off `Q` by synthetic division by `Y - R` +and tests each digit for divisibility by the corresponding power of `G` with +a monic remainder. With NTT-backed multiplication and remainder contexts the +whole check is quasi-linear in the input size, with no per-point loop. + +`interpolationWitnessIsValidViaDivisibilityBool` combines the divisibility +check with the nonzero and weighted-degree witness conditions, mirroring +`interpolationWitnessIsValidBool`. +-/ + +@[expose] public section + +namespace CompPoly + +namespace CBivariate + +/-- `divByLinearY` with the inner univariate products supplied by an explicit +multiplication context, for callers that want NTT-backed Horner steps. -/ +def divByLinearYWith {R : Type*} [Semiring R] [BEq R] [LawfulBEq R] [Nontrivial R] + (M : CPolynomial.MulContext R) (Q : CBivariate R) (f : CPolynomial R) : + CBivariate R × CPolynomial R := + let n := natDegreeY Q + if n = 0 then (0, CPolynomial.coeff Q 0) + else + let a : ℕ → CPolynomial R := fun j => CPolynomial.coeff Q j + let b_init := a n + let (b_last, coeffs) := (List.range (n - 1)).foldl + (fun (b, acc) k => + let bj := a (n - 1 - k) + M.mul f b + (bj, acc.push bj)) + (b_init, #[b_init]) + let rem := a 0 + M.mul f b_last + let quotArr : CPolynomial.Raw (CPolynomial R) := coeffs.reverse + (⟨quotArr.trim, CPolynomial.Raw.Trim.isCanonical_trim quotArr⟩, rem) + +/-- The context-parametric synthetic division agrees with `divByLinearY`. -/ +theorem divByLinearYWith_eq_divByLinearY {R : Type*} + [Semiring R] [BEq R] [LawfulBEq R] [Nontrivial R] + (M : CPolynomial.MulContext R) (Q : CBivariate R) (f : CPolynomial R) : + divByLinearYWith M Q f = divByLinearY Q f := by + unfold divByLinearYWith divByLinearY + simp only [M.mul_eq_mul] + +end CBivariate + +namespace GuruswamiSudan + +/-- Divisibility form of the multiplicity constraints: the digit `c_t` of the +base-`(Y - R)` expansion of `Q` must be divisible by `G^(m - t)` for every +`t < m`. The recursion peels one digit per step by synthetic division, so the +step at recursion depth `t` checks `G^(m - t) ∣ c_t` on the running quotient. + +For `G` the vanishing polynomial of the (distinct) `x`-coordinates of a point +set and `R` its interpolant, this is equivalent to +`CBivariate.satisfiesMultiplicityConstraintsBool` on the same point set; see +`satisfiesMultiplicityConstraintsViaDivisibilityBool_eq`. -/ +def satisfiesMultiplicityConstraintsViaDivisibilityBool {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (Mul : CPolynomial.MulContext F) (Mod : CPolynomial.ModContext F) + (G R : CPolynomial F) : Nat → CBivariate F → Bool + | 0, _ => true + | m + 1, Q => + let step := CBivariate.divByLinearYWith Mul Q R + Mod.modByMonic step.2 (G ^ (m + 1)) == 0 && + satisfiesMultiplicityConstraintsViaDivisibilityBool Mul Mod G R m step.1 + +/-- Divisibility-based recognizer for the semantic interpolation witness +contract, agreeing with `interpolationWitnessIsValidBool` on point sets with +distinct `x`-coordinates but avoiding its per-point Hasse derivative loop. -/ +def interpolationWitnessIsValidViaDivisibilityBool {F : Type*} + [Field F] [BEq F] [LawfulBEq F] [DecidableEq F] + (V : CPolynomial.VanishingPolynomialContext F) + (E : CPolynomial.BatchEvalContext F) + (Mul : CPolynomial.MulContext F) (Mod : CPolynomial.ModContext F) + (points : Array (F × F)) (params : GSInterpParams) (Q : CBivariate F) : Bool := + let G := V.vanishingPolynomial (points.map fun point ↦ point.1) + let R := CPolynomial.interpolateCoefficientFormWithVanishing E G points + !(Q == 0) && + decide + (CBivariate.natWeightedDegree Q 1 (yWeight params) ≤ + params.weightedDegreeBound) && + satisfiesMultiplicityConstraintsViaDivisibilityBool Mul Mod G R + params.multiplicity Q + +end GuruswamiSudan + +end CompPoly diff --git a/CompPoly/Bivariate/GuruswamiSudan/Interpolation/WitnessDivisibilityCorrectness.lean b/CompPoly/Bivariate/GuruswamiSudan/Interpolation/WitnessDivisibilityCorrectness.lean new file mode 100644 index 00000000..6ca53e4b --- /dev/null +++ b/CompPoly/Bivariate/GuruswamiSudan/Interpolation/WitnessDivisibilityCorrectness.lean @@ -0,0 +1,514 @@ +/- +Copyright (c) 2026 CompPoly Contributors. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Valerii Huhnin +-/ + +module + +-- `linearFactor`, `coeff` and friends are declared in bare `public section`s, so +-- their bodies are opaque downstream; see `docs/wiki/module-system.md`. +import all CompPoly.Univariate.Basic +import all CompPoly.Univariate.Raw.Core +import all CompPoly.Univariate.ToPoly.Core +public import CompPoly.Bivariate.GuruswamiSudan.Interpolation.WitnessDivisibility +public import CompPoly.Bivariate.GuruswamiSudan.Interpolation.LeeOSullivan.Correctness.Combinations +public import CompPoly.Bivariate.GuruswamiSudan.Interpolation.LeeOSullivan.Correctness.Basis +public import CompPoly.Bivariate.GuruswamiSudan.Interpolation.LeeOSullivan.Correctness.Divisibility + +/-! +# Correctness of Divisibility-Based Witness Checking + +`satisfiesMultiplicityConstraintsViaDivisibilityBool` agrees with the +pointwise Hasse checker `CBivariate.satisfiesMultiplicityConstraintsBool` +on point sets with distinct `x`-coordinates, and consequently +`interpolationWitnessIsValidViaDivisibilityBool` agrees with +`interpolationWitnessIsValidBool`. + +The proof peels one base-`(Y - R)` digit per multiplicity level. + +* Soundness: from `Q = C c + (Y - R) * Q'` with `c = W * G^(m+1)`, the digit + term gains multiplicity `m + 1` from the `G`-power + (`hasMultiplicityAtLeast_ofYConstant_pow_mul_eval_zero`), the quotient term + gains one from `Y - R` (`hasMultiplicityAtLeast_linearYDivisor_mul`) on top + of the inductive hypothesis, and multiplicity is closed under addition. + +* Completeness: the quotient `Q'` keeps multiplicity `m` at every point + (`witness_hasMultiplicity_quot`, by a strong induction on the + `X`-coefficients of the Taylor-shifted division identity), so the digit + `C c = Q - (Y - R) * Q'` has multiplicity `m + 1` with `Y`-degree zero, and + `coeffY_dvd_vanishingPolynomial_pow_of_multiplicity` forces + `G^(m+1) ∣ c`. +-/ + +@[expose] public section + +namespace CompPoly + +namespace GuruswamiSudan + +open CBivariate + +variable {F : Type*} [Field F] [BEq F] [LawfulBEq F] [DecidableEq F] + +/-! ### Generic helpers -/ + +private theorem witness_cpoly_eq_of_toPoly_eq {R : Type*} + [Semiring R] [BEq R] [LawfulBEq R] {p q : CPolynomial R} + (h : p.toPoly = q.toPoly) : p = q := by + rw [CPolynomial.eq_iff_coeff] + intro i + rw [CPolynomial.coeff_toPoly, CPolynomial.coeff_toPoly, h] + +omit [DecidableEq F] in +private theorem witness_linearFactor_toPoly (x : F) : + (CPolynomial.linearFactor x).toPoly = + (Polynomial.X - Polynomial.C x : Polynomial F) := by + rw [CPolynomial.linearFactor, CPolynomial.toPoly_add, CPolynomial.C_toPoly, + CPolynomial.X_toPoly, Polynomial.C_neg] + ring + +omit [DecidableEq F] in +private theorem witness_foldl_linearFactor_monic (l : List F) (acc : CPolynomial F) + (hacc : acc.toPoly.Monic) : + (l.foldl (fun acc x ↦ acc * CPolynomial.linearFactor x) acc).toPoly.Monic := by + induction l generalizing acc with + | nil => simpa using hacc + | cons x xs ih => + simp only [List.foldl_cons] + apply ih + rw [CPolynomial.toPoly_mul, witness_linearFactor_toPoly] + exact hacc.mul (Polynomial.monic_X_sub_C x) + +omit [DecidableEq F] in +/-- The vanishing polynomial of a node array is monic. -/ +private theorem witness_vanishing_monic (xs : Array F) : + (CPolynomial.vanishingPolynomialArray xs).monic := by + rw [CPolynomial.monic_toPoly_iff, CPolynomial.vanishingPolynomialArray, + ← Array.foldl_toList] + exact witness_foldl_linearFactor_monic xs.toList 1 + (by rw [CPolynomial.toPoly_one]; exact Polynomial.monic_one) + +omit [DecidableEq F] in +private theorem witness_monic_pow {g : CPolynomial F} (hg : g.monic) (k : Nat) : + (g ^ k).monic := by + rw [CPolynomial.monic_toPoly_iff] at hg ⊢ + rw [CPolynomial.toPoly_pow] + exact hg.pow k + +omit [DecidableEq F] in +/-- Remainder by a monic divisor vanishes exactly on multiples. -/ +private theorem witness_modByMonic_eq_zero_iff {c g : CPolynomial F} + (hg : g.monic) : + CPolynomial.modByMonic c g = 0 ↔ ∃ W : CPolynomial F, c = W * g := by + have hgPoly : g.toPoly.Monic := (CPolynomial.monic_toPoly_iff g).mp hg + constructor + · intro h + have hmod : c.toPoly %ₘ g.toPoly = 0 := by + rw [← CPolynomial.modByMonic_toPoly_eq_modByMonic c g hg, h] + exact (CPolynomial.toPoly_eq_zero_iff _).mpr rfl + rcases (Polynomial.modByMonic_eq_zero_iff_dvd hgPoly).mp hmod with ⟨w, hw⟩ + refine ⟨⟨w.toImpl, CPolynomial.Raw.isCanonical_toImpl w⟩, ?_⟩ + apply witness_cpoly_eq_of_toPoly_eq + rw [CPolynomial.toPoly_mul] + have hWto : CPolynomial.toPoly + (⟨w.toImpl, CPolynomial.Raw.isCanonical_toImpl w⟩ : CPolynomial F) = w := + CPolynomial.Raw.toPoly_toImpl + rw [hWto, hw] + ring + · rintro ⟨W, rfl⟩ + have hmod : (W * g).toPoly %ₘ g.toPoly = 0 := + (Polynomial.modByMonic_eq_zero_iff_dvd hgPoly).mpr + ⟨W.toPoly, by rw [CPolynomial.toPoly_mul]; ring⟩ + apply witness_cpoly_eq_of_toPoly_eq + rw [CPolynomial.modByMonic_toPoly_eq_modByMonic _ _ hg, hmod] + exact ((CPolynomial.toPoly_eq_zero_iff _).mpr rfl).symm + +/-! ### The division identity -/ + +/-- Euclidean identity of `divByLinearY` at the `CBivariate` level. -/ +private theorem witness_divByLinearY_decomposition (Q : CBivariate F) + (R : CPolynomial F) : + Q = CBivariate.ofYConstant (CBivariate.divByLinearY Q R).2 + + CBivariate.linearYDivisor R * (CBivariate.divByLinearY Q R).1 := by + apply witness_cpoly_eq_of_toPoly_eq (R := CPolynomial F) + erw [CPolynomial.toPoly_add, CPolynomial.toPoly_mul] + exact (CBivariate.divByLinearY_euclid_toPoly Q R).symm + +/-! ### The shifted division identity -/ + +private theorem witness_taylor_neg (x : F) (u : CPolynomial F) : + CPolynomial.taylor x (-u) = -CPolynomial.taylor x u := by + apply witness_cpoly_eq_of_toPoly_eq + rw [CPolynomial.taylor_toPoly, CPolynomial.toPoly_neg, CPolynomial.toPoly_neg, + CPolynomial.taylor_toPoly, map_neg] + +private theorem witness_taylor_one (x : F) : + CPolynomial.taylor x (1 : CPolynomial F) = 1 := by + apply witness_cpoly_eq_of_toPoly_eq + simp [CPolynomial.taylor_toPoly, CPolynomial.toPoly_one] + +private theorem witness_taylor_sub_C (x y : F) (R : CPolynomial F) : + CPolynomial.taylor x (R - CPolynomial.C y) = + CPolynomial.taylor x R - CPolynomial.C y := by + apply witness_cpoly_eq_of_toPoly_eq + rw [CPolynomial.taylor_toPoly, CPolynomial.toPoly_sub, CPolynomial.toPoly_sub, + CPolynomial.taylor_toPoly, CPolynomial.C_toPoly, map_sub, Polynomial.taylor_C] + +/-- Outer (`Y`) coefficients of the linear divisor `Y - u`. -/ +private theorem witness_coeff_linearYDivisor (u : CPolynomial F) (j : Nat) : + CPolynomial.coeff (CBivariate.linearYDivisor u) j = + if j = 0 then -u else if j = 1 then (1 : CPolynomial F) else 0 := by + rw [CPolynomial.coeff_toPoly, CBivariate.linearYDivisor_toPoly, + Polynomial.coeff_sub, Polynomial.coeff_C] + rcases j with _ | _ | j <;> + simp [Polynomial.coeff_X] + +/-- The outer (`Y`) shift maps `Y - R` to `Y - (R - C y)`. -/ +private theorem witness_shiftY_linearYDivisor (y : F) (R : CPolynomial F) : + CBivariate.shiftY y (CBivariate.linearYDivisor R) = + CBivariate.linearYDivisor (R - CPolynomial.C y) := by + apply witness_cpoly_eq_of_toPoly_eq (R := CPolynomial F) + rw [CBivariate.shiftY, CPolynomial.taylor_toPoly, + CBivariate.linearYDivisor_toPoly, CBivariate.linearYDivisor_toPoly, + map_sub, Polynomial.taylor_X, Polynomial.taylor_C, map_sub] + ring + +/-- The inner (`X`) shift maps `Y - u` to `Y - taylor x u`. -/ +private theorem witness_shiftX_linearYDivisor (x : F) (u : CPolynomial F) : + CBivariate.shiftX x (CBivariate.linearYDivisor u) = + CBivariate.linearYDivisor (CPolynomial.taylor x u) := by + refine (CPolynomial.eq_iff_coeff (R := CPolynomial F)).mpr fun j ↦ ?_ + rw [CBivariate.outerCoeff_shiftX, witness_coeff_linearYDivisor, + witness_coeff_linearYDivisor] + rcases j with _ | _ | j + · simpa using witness_taylor_neg x u + · simpa using witness_taylor_one x + · simpa using CPolynomial.taylor_zero x + +/-- The full Taylor shift maps `Y - R` to `Y - (taylor x R - C y)`. -/ +private theorem witness_shiftC_linearYDivisor (x y : F) (R : CPolynomial F) : + CBivariate.shiftC x y (CBivariate.linearYDivisor R) = + CBivariate.linearYDivisor (CPolynomial.taylor x R - CPolynomial.C y) := by + show CBivariate.shiftX x (CBivariate.shiftY y (CBivariate.linearYDivisor R)) = _ + rw [witness_shiftY_linearYDivisor, witness_shiftX_linearYDivisor, + witness_taylor_sub_C] + +/-- Positive-`Y` outer coefficients of the shift of a `Y`-constant vanish. -/ +private theorem witness_outerCoeff_shiftC_C (x y : F) (c : CPolynomial F) + (j : Nat) : + CPolynomial.coeff (CBivariate.shiftC x y (CBivariate.ofYConstant c)) + (j + 1) = 0 := by + have hY : CBivariate.shiftY y (CBivariate.ofYConstant c) = + CBivariate.ofYConstant c := by + apply witness_cpoly_eq_of_toPoly_eq (R := CPolynomial F) + show CPolynomial.toPoly + (CPolynomial.taylor (CPolynomial.C y) (CPolynomial.C c)) = + CPolynomial.toPoly (CPolynomial.C c) + rw [CPolynomial.taylor_toPoly, CPolynomial.C_toPoly, Polynomial.taylor_C] + show CPolynomial.coeff + (CBivariate.shiftX x (CBivariate.shiftY y (CBivariate.ofYConstant c))) + (j + 1) = 0 + rw [hY] + show CPolynomial.coeff + (CBivariate.shiftX x (CPolynomial.C c)) (j + 1) = 0 + rw [CBivariate.outerCoeff_shiftX, CPolynomial.coeff_C] + simp [CPolynomial.taylor_zero] + +/-! ### The quotient keeps multiplicity -/ + +omit [DecidableEq F] in +/-- Univariate coefficients of a difference. -/ +private theorem witness_coeff_sub (p q : CPolynomial F) (i : Nat) : + CPolynomial.coeff (p - q) i = + CPolynomial.coeff p i - CPolynomial.coeff q i := by + rw [CPolynomial.coeff_toPoly, CPolynomial.coeff_toPoly, CPolynomial.coeff_toPoly, + CPolynomial.toPoly_sub, Polynomial.coeff_sub] + +/-- Outer (`Y`) coefficients of a product with the linear divisor `Y - u`. -/ +private theorem witness_outerCoeff_linearYDivisor_mul (u : CPolynomial F) + (P : CBivariate F) (j : Nat) : + CPolynomial.coeff (CBivariate.linearYDivisor u * P) (j + 1) = + CPolynomial.coeff P j - u * CPolynomial.coeff P (j + 1) := by + rw [CPolynomial.coeff_toPoly] + erw [CPolynomial.toPoly_mul] + rw [CBivariate.linearYDivisor_toPoly, mul_comm, + Polynomial.coeff_mul_X_sub_C, CPolynomial.coeff_toPoly, + CPolynomial.coeff_toPoly] + ring + +/-- Core coefficient induction: if every positive-`Y`-row low coefficient of +`(Y - u) * P` vanishes and `u` has no constant term, then every low +coefficient of `P` vanishes. -/ +private theorem witness_coeff_low_vanish + {u : CPolynomial F} (hu : CPolynomial.coeff u 0 = 0) + {P : CBivariate F} {m : Nat} + (h : ∀ i j, i + j + 1 < m + 1 → + CBivariate.coeff (CBivariate.linearYDivisor u * P) i (j + 1) = 0) : + ∀ i j, i + j < m → CBivariate.coeff P i j = 0 := by + intro i + induction i using Nat.strong_induction_on with + | _ i ih => + intro j hij + have hprod := h i j (by omega) + rw [CBivariate.coeff_eq_coeff_coeff, witness_outerCoeff_linearYDivisor_mul, + witness_coeff_sub] at hprod + have hmul : CPolynomial.coeff (u * CPolynomial.coeff P (j + 1)) i = 0 := by + rw [CPolynomial.coeff_mul] + apply Finset.sum_eq_zero + intro k hk + have hkle : k ≤ i := by + have := Finset.mem_range.mp hk + omega + rcases Nat.eq_zero_or_pos k with rfl | hkpos + · rw [hu, zero_mul] + · have hcoeff : CPolynomial.coeff (CPolynomial.coeff P (j + 1)) (i - k) = 0 := by + have hlow := ih (i - k) (by omega) (j + 1) (by omega) + rw [CBivariate.coeff_eq_coeff_coeff] at hlow + exact hlow + rw [hcoeff, mul_zero] + rw [hmul, sub_zero] at hprod + rw [CBivariate.coeff_eq_coeff_coeff] + exact hprod + +/-- Dividing by `Y - R` through a point of multiplicity `m + 1` leaves a +quotient of multiplicity `m` there. -/ +private theorem witness_hasMultiplicity_quot + {Q : CBivariate F} {R : CPolynomial F} {x y : F} {m : Nat} + (hxy : CPolynomial.eval x R = y) + (hQ : CBivariate.hasMultiplicity Q (m + 1) x y) : + CBivariate.hasMultiplicity (CBivariate.divByLinearY Q R).1 m x y := by + set c := (CBivariate.divByLinearY Q R).2 with hc + set Q' := (CBivariate.divByLinearY Q R).1 with hq' + have hu0 : CPolynomial.coeff (CPolynomial.taylor x R - CPolynomial.C y) 0 = 0 := by + rw [witness_coeff_sub, CPolynomial.taylor_coeff_zero, CPolynomial.coeff_C, + hxy] + simp + have hshift : CBivariate.shiftC x y Q = + CBivariate.shiftC x y (CBivariate.ofYConstant c) + + CBivariate.linearYDivisor (CPolynomial.taylor x R - CPolynomial.C y) * + CBivariate.shiftC x y Q' := by + conv_lhs => rw [witness_divByLinearY_decomposition Q R] + rw [CBivariate.shiftC_add, CBivariate.shiftC_mul, + witness_shiftC_linearYDivisor] + intro i j hij + refine witness_coeff_low_vanish hu0 (P := CBivariate.shiftC x y Q') ?_ i j hij + intro i' j' hij' + have h0 : CBivariate.coeff (CBivariate.shiftC x y Q) i' (j' + 1) = 0 := + hQ i' (j' + 1) (by omega) + rw [hshift, CBivariate.coeff_add] at h0 + have hCc : CBivariate.coeff + (CBivariate.shiftC x y (CBivariate.ofYConstant c)) i' (j' + 1) = 0 := by + rw [CBivariate.coeff_eq_coeff_coeff, witness_outerCoeff_shiftC_C] + exact CPolynomial.coeff_zero i' + rw [hCc, zero_add] at h0 + exact h0 + +/-! ### Multiplicity closure helpers -/ + +omit [BEq F] [LawfulBEq F] [DecidableEq F] in +private theorem witness_hasMultiplicityAtLeast_zero (P : CBivariate F) (x y : F) : + CBivariate.HasMultiplicityAtLeast P x y 0 := + fun _ _ hab ↦ absurd hab (by omega) + +private theorem witness_hasMultiplicityAtLeast_add + {P S : CBivariate F} {x y : F} {m : Nat} + (hP : CBivariate.HasMultiplicityAtLeast P x y m) + (hS : CBivariate.HasMultiplicityAtLeast S x y m) : + CBivariate.HasMultiplicityAtLeast (P + S) x y m := by + intro a b hab + rw [CBivariate.hasseDerivativeEval_add, hP a b hab, hS a b hab, add_zero] + +private theorem witness_hasMultiplicityAtLeast_sub + {P S : CBivariate F} {x y : F} {m : Nat} + (hP : CBivariate.HasMultiplicityAtLeast P x y m) + (hS : CBivariate.HasMultiplicityAtLeast S x y m) : + CBivariate.HasMultiplicityAtLeast (P - S) x y m := by + intro a b hab + rw [CBivariate.hasseDerivativeEval_sub, hP a b hab, hS a b hab, sub_zero] + +omit [DecidableEq F] in +private theorem witness_ofYConstant_pow (G : CPolynomial F) (k : Nat) : + CBivariate.ofYConstant (G ^ k) = (CBivariate.ofYConstant G) ^ k := by + induction k with + | zero => + rw [pow_zero, pow_zero] + apply witness_cpoly_eq_of_toPoly_eq (R := CPolynomial F) + show (CPolynomial.C (1 : CPolynomial F)).toPoly = + CPolynomial.toPoly (1 : CPolynomial (CPolynomial F)) + rw [CPolynomial.C_toPoly, CPolynomial.toPoly_one, Polynomial.C_1] + | succ k ih => + rw [pow_succ, pow_succ, ofYConstant_mul, ih] + +/-- A multiple of `G^k` viewed as a `Y`-constant has multiplicity `k` at every +point whose `x`-coordinate is a node of `G`. -/ +private theorem witness_hasMultiplicityAtLeast_C_of_dvd + {points : Array (F × F)} {point : F × F} (hpoint : point ∈ points.toList) + (W : CPolynomial F) (k : Nat) : + CBivariate.HasMultiplicityAtLeast + (CBivariate.ofYConstant (W * + (CPolynomial.vanishingPolynomialArray (points.map fun p ↦ p.1)) ^ k)) + point.1 point.2 k := by + set G := CPolynomial.vanishingPolynomialArray (points.map fun p ↦ p.1) with hG + have hEval : CPolynomial.eval point.1 G = 0 := by + apply CPolynomial.eval_vanishingPolynomialArray_eq_zero_of_mem + rw [Array.toList_map] + exact List.mem_map.mpr ⟨point, hpoint, rfl⟩ + have hsplit : CBivariate.ofYConstant (W * G ^ k) = + (CBivariate.ofYConstant G) ^ k * CBivariate.ofYConstant W := by + rw [ofYConstant_mul, witness_ofYConstant_pow] + ring + rw [hsplit] + have := LeeOSullivan.hasMultiplicityAtLeast_ofYConstant_pow_mul_eval_zero + (F := F) G k (P := CBivariate.ofYConstant W) (x := point.1) (y := point.2) + (m := 0) hEval (witness_hasMultiplicityAtLeast_zero _ _ _) + simpa using this + +/-! ### Main equivalence -/ + +/-- The digit-divisibility recursion recognizes exactly the multiplicity +constraints, for `G` the vanishing polynomial of the distinct `x`-coordinates +and `R` any interpolant of the points. -/ +private theorem witness_viaDivisibility_iff + (Mul : CPolynomial.MulContext F) (Mod : CPolynomial.ModContext F) + {points : Array (F × F)} (R : CPolynomial F) + (hdistinct : DistinctXCoordinates points) + (hR : ∀ point, point ∈ points.toList → CPolynomial.eval point.1 R = point.2) + (m : Nat) (Q : CBivariate F) : + satisfiesMultiplicityConstraintsViaDivisibilityBool Mul Mod + (CPolynomial.vanishingPolynomialArray (points.map fun p ↦ p.1)) R m Q + = true ↔ + CBivariate.SatisfiesMultiplicityConstraints Q points m := by + set G := CPolynomial.vanishingPolynomialArray (points.map fun p ↦ p.1) with hG + induction m generalizing Q with + | zero => + simp only [satisfiesMultiplicityConstraintsViaDivisibilityBool] + constructor + · intro _ point _ + exact witness_hasMultiplicityAtLeast_zero Q point.1 point.2 + · intro _ + trivial + | succ m ih => + have hGpow : (G ^ (m + 1)).monic := + witness_monic_pow (by simpa [hG] using + witness_vanishing_monic (points.map fun p ↦ p.1)) (m + 1) + set c := (CBivariate.divByLinearY Q R).2 with hc + set Q' := (CBivariate.divByLinearY Q R).1 with hq' + have hstep : satisfiesMultiplicityConstraintsViaDivisibilityBool Mul Mod + G R (m + 1) Q = + ((CPolynomial.modByMonic c (G ^ (m + 1)) == 0) && + satisfiesMultiplicityConstraintsViaDivisibilityBool Mul Mod + G R m Q') := by + simp only [satisfiesMultiplicityConstraintsViaDivisibilityBool, + CBivariate.divByLinearYWith_eq_divByLinearY, + Mod.modByMonic_eq_modByMonic, hc, hq'] + rw [hstep, Bool.and_eq_true, beq_iff_eq, + witness_modByMonic_eq_zero_iff hGpow] + constructor + · rintro ⟨⟨W, hW⟩, hrest⟩ + have hQ' : CBivariate.SatisfiesMultiplicityConstraints Q' points m := + (ih Q').mp hrest + intro point hpoint + have hCc : CBivariate.HasMultiplicityAtLeast + (CBivariate.ofYConstant c) point.1 point.2 (m + 1) := by + rw [hW, hG] + exact witness_hasMultiplicityAtLeast_C_of_dvd hpoint W (m + 1) + have hLin : CBivariate.HasMultiplicityAtLeast + (CBivariate.linearYDivisor R * Q') point.1 point.2 (m + 1) := + LeeOSullivan.hasMultiplicityAtLeast_linearYDivisor_mul R + (hR point hpoint) (hQ' point hpoint) + have hsum := witness_hasMultiplicityAtLeast_add hCc hLin + rwa [← witness_divByLinearY_decomposition Q R] at hsum + · intro hQ + have hQ' : CBivariate.SatisfiesMultiplicityConstraints Q' points m := by + intro point hpoint + have hquot := witness_hasMultiplicity_quot (hR point hpoint) + ((CBivariate.hasMultiplicity_iff_hasMultiplicityAtLeast Q (m + 1) + point.1 point.2).mpr (hQ point hpoint)) + exact (CBivariate.hasMultiplicity_iff_hasMultiplicityAtLeast Q' m + point.1 point.2).mp hquot + refine ⟨?_, (ih Q').mpr hQ'⟩ + have hCc : CBivariate.SatisfiesMultiplicityConstraints + (CBivariate.ofYConstant c) points (m + 1) := by + intro point hpoint + have hLin : CBivariate.HasMultiplicityAtLeast + (CBivariate.linearYDivisor R * Q') point.1 point.2 (m + 1) := + LeeOSullivan.hasMultiplicityAtLeast_linearYDivisor_mul R + (hR point hpoint) (hQ' point hpoint) + have hsub := witness_hasMultiplicityAtLeast_sub + (hQ point hpoint) hLin + have hdec : Q - CBivariate.linearYDivisor R * Q' = + CBivariate.ofYConstant c := by + conv_lhs => rw [witness_divByLinearY_decomposition Q R] + ring + rwa [hdec] at hsub + have hY : ∀ j, 0 < j → + (CBivariate.ofYConstant c).val.coeff j = 0 := by + intro j hj + have hcoeffj : CPolynomial.coeff (CBivariate.ofYConstant c) j = 0 := by + show CPolynomial.coeff (CPolynomial.C c) j = 0 + rw [CPolynomial.coeff_C] + simp [Nat.pos_iff_ne_zero.mp hj] + exact hcoeffj + have hdvd := LeeOSullivan.coeffY_dvd_vanishingPolynomial_pow_of_multiplicity + (CPolynomial.VanishingPolynomialContext.direct (F := F)) + (P := CBivariate.ofYConstant c) (m := m + 1) (n := 0) + hdistinct (by omega) hY hCc + rcases hdvd with ⟨W, hW⟩ + refine ⟨W, ?_⟩ + have hcoeff0 : (CBivariate.ofYConstant c).val.coeff 0 = c := by + have hcoeffc : CPolynomial.coeff (CBivariate.ofYConstant c) 0 = c := by + show CPolynomial.coeff (CPolynomial.C c) 0 = c + rw [CPolynomial.coeff_C] + simp + exact hcoeffc + rw [hcoeff0] at hW + -- `direct.vanishingPolynomial` is defeq to `vanishingPolynomialArray`. + simpa [hG, Nat.sub_zero, CPolynomial.VanishingPolynomialContext.direct] + using hW + +/-- The divisibility-based multiplicity checker agrees with the pointwise +Hasse checker on point sets with distinct `x`-coordinates. -/ +theorem satisfiesMultiplicityConstraintsViaDivisibilityBool_eq + (Mul : CPolynomial.MulContext F) (Mod : CPolynomial.ModContext F) + {points : Array (F × F)} (R : CPolynomial F) + (hdistinct : DistinctXCoordinates points) + (hR : ∀ point, point ∈ points.toList → CPolynomial.eval point.1 R = point.2) + (m : Nat) (Q : CBivariate F) : + satisfiesMultiplicityConstraintsViaDivisibilityBool Mul Mod + (CPolynomial.vanishingPolynomialArray (points.map fun p ↦ p.1)) R m Q = + CBivariate.satisfiesMultiplicityConstraintsBool Q points m := by + rw [Bool.eq_iff_iff, witness_viaDivisibility_iff Mul Mod R hdistinct hR m Q, + CBivariate.satisfiesMultiplicityConstraintsBool_iff_hasMultiplicity, + CBivariate.satisfiesMultiplicityConstraints_iff_hasMultiplicity] + +/-- The divisibility-based witness recognizer agrees with +`interpolationWitnessIsValidBool` on point sets with distinct +`x`-coordinates. -/ +theorem interpolationWitnessIsValidViaDivisibilityBool_eq + (V : CPolynomial.VanishingPolynomialContext F) + (E : CPolynomial.BatchEvalContext F) + (Mul : CPolynomial.MulContext F) (Mod : CPolynomial.ModContext F) + {points : Array (F × F)} (params : GSInterpParams) + (hdistinct : DistinctXCoordinates points) (Q : CBivariate F) : + interpolationWitnessIsValidViaDivisibilityBool V E Mul Mod points params Q = + interpolationWitnessIsValidBool points params Q := by + have hR : ∀ point, point ∈ points.toList → + CPolynomial.eval point.1 + (CPolynomial.interpolateCoefficientFormWithVanishing E + (CPolynomial.vanishingPolynomialArray + (points.map fun point ↦ point.1)) points) = + point.2 := by + intro point hpoint + have heval := + CPolynomial.interpolateCoefficientForm_eval_point V E hdistinct hpoint + rw [CPolynomial.interpolateCoefficientForm, V.correct] at heval + exact heval + simp only [interpolationWitnessIsValidViaDivisibilityBool, + interpolationWitnessIsValidBool, V.correct] + rw [satisfiesMultiplicityConstraintsViaDivisibilityBool_eq Mul Mod _ + hdistinct hR params.multiplicity Q (points := points)] + +end GuruswamiSudan + +end CompPoly diff --git a/CompPoly/Bivariate/GuruswamiSudan/PolynomialCorrectness.lean b/CompPoly/Bivariate/GuruswamiSudan/PolynomialCorrectness.lean index 612a8955..83b53d7d 100644 --- a/CompPoly/Bivariate/GuruswamiSudan/PolynomialCorrectness.lean +++ b/CompPoly/Bivariate/GuruswamiSudan/PolynomialCorrectness.lean @@ -842,7 +842,12 @@ private theorem eval_map_taylorAlgHom {F : Type*} [Field F] (Polynomial.taylorAlgHom x).toRingHom (P.eval₂ (RingHom.id (Polynomial F)) (Polynomial.C y)) rw [Polynomial.hom_eval₂] - simp [Polynomial.taylor_C] + -- The shift fixes constant inner coefficients, so the two `eval₂` constants agree. + -- Keep `taylorAlgHom` folded so `coe_taylorAlgHom` can fire. + have hC : (Polynomial.taylorAlgHom x).toRingHom (Polynomial.C y) = Polynomial.C y := by + simp [Polynomial.taylor_C] + rw [hC] + simp /-- Hasse derivatives in `X` commute with multiplication by an `X`-constant polynomial. -/ @@ -928,7 +933,7 @@ private theorem toPoly_hasseDerivative_eq_coeffwise_hasseDeriv_hasseDeriv {F : T /-- Evaluating the univariate `X`-Hasse derivative of the evaluated `Y`-Hasse derivative matches the executable bivariate Hasse derivative. -/ -private theorem eval_hasseDeriv_eval_hasseDeriv_toPoly {F : Type*} +theorem eval_hasseDeriv_eval_hasseDeriv_toPoly {F : Type*} [Field F] [BEq F] [LawfulBEq F] [DecidableEq F] (Q : CBivariate F) (x y : F) (a b : Nat) : Polynomial.eval x (Polynomial.hasseDeriv a diff --git a/CompPoly/Data/List/Lemmas.lean b/CompPoly/Data/List/Lemmas.lean index 7037a2b6..2ce10fa3 100644 --- a/CompPoly/Data/List/Lemmas.lean +++ b/CompPoly/Data/List/Lemmas.lean @@ -7,6 +7,7 @@ module public import Mathlib.Algebra.GroupWithZero.Nat public import Mathlib.Data.List.GetD +public import Mathlib.Data.List.Nodup public import Mathlib.Order.Lattice.Nat public import Mathlib.Tactic.Cases @@ -210,4 +211,12 @@ lemma zipWith_const {α β : Type _} {f : α → β → β} {l₁ : List α} {l (h₁ : l₁.length = l₂.length) (h₂ : ∀ a b, f a b = b) : l₁.zipWith f l₂ = l₂ := by induction' l₁ with hd tl ih generalizing l₂ <;> rcases l₂ <;> aesop +/-- An injective `f` separates the elements of a duplicate-free list pairwise. + +This is `List.Nodup.map` transported along `List.pairwise_map`, stated in the unmapped form that +`Std.ExtTreeMap.getElem_ofList_of_mem` and friends ask for. -/ +theorem Nodup.pairwise_ne_map {α : Type u} {β : Type v} {l : List α} {f : α → β} + (hf : Function.Injective f) (hl : l.Nodup) : l.Pairwise fun a b => f a ≠ f b := + List.pairwise_map.mp (Nodup.map hf hl) + end List diff --git a/CompPoly/Data/Option/Lemmas.lean b/CompPoly/Data/Option/Lemmas.lean new file mode 100644 index 00000000..031efb43 --- /dev/null +++ b/CompPoly/Data/Option/Lemmas.lean @@ -0,0 +1,24 @@ +/- +Copyright (c) 2025 CompPoly. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Frantisek Silvasi, Julian Sutherland, Andrei Burdușa +-/ +module + +public import Mathlib.Data.Option.Basic + +/-! +# Auxiliary lemmas for `Option` +-/ + +@[expose] public section + +namespace Option + +/-- `Option.filter` leaves its argument alone exactly when the predicate holds of the contained +value. The `Option` analogue of `List.filter_eq_self` and `Array.filter_eq_self`. -/ +theorem filter_eq_self {α : Type*} {o : Option α} {p : α → Bool} : + o.filter p = o ↔ ∀ a ∈ o, p a := by + cases o <;> simp + +end Option diff --git a/CompPoly/Data/Polynomial/RabinCertificate.lean b/CompPoly/Data/Polynomial/RabinCertificate.lean index cc060bc5..b02c7c0e 100644 --- a/CompPoly/Data/Polynomial/RabinCertificate.lean +++ b/CompPoly/Data/Polynomial/RabinCertificate.lean @@ -32,7 +32,9 @@ This file provides the reusable, degree-agnostic *certificate* infrastructure: `isCoprime_X_pow_sub_X_of_runChain` (coprimality, from a Bézout certificate on the reduced residue). * `irreducible_of_rabin_prime_degree` packages Rabin's test for *prime* degree `d`, where the - conditions collapse to a single trace and a single coprimality check. + conditions collapse to a single trace and a single coprimality check. The `_of_card` variants + of the packaged forms take the field size as a numeral `q` with `Fintype.card F = q`; that is + the shape concrete extensions use. Certificate data is produced by the untrusted generator `scripts/gen_rabin_certificate.py`; the kernel re-checks every step. Contrast `CompPoly/Fields/Binary/BF128Ghash/`, the bespoke @@ -389,4 +391,50 @@ theorem irreducible_of_rabin_degree_six {F : Type*} [Field F] [Fintype F] {f : F irreducible_of_rabin_two_prime_factors h_deg (by norm_num) primeFactors_six h_trace (by simpa using h_cop₃) (by simpa using h_cop₂) +/-! ### Explicit-cardinality forms + +The wrappers above state their conditions at `Fintype.card F`. Concrete extensions instead define +their field as `ZMod fieldSize` and generate certificates already stated in terms of the numeral +(`chainExp 1 steps = fieldSize ^ d`), so the `_of_card` forms below take the field size as a +caller-supplied `q` with `hcard : Fintype.card F = q`. Same shape as +`irreducible_X_pow_four_sub_C_of_card` in `CompPoly/Fields/Extension/Binomial.lean`. +-/ + +/-- +**Rabin's test for prime degree, with the cardinality abstracted into a numeral `q`.** + +Identical content to `irreducible_of_rabin_prime_degree`, with the field size supplied as `q` and +`hcard : Fintype.card F = q` rather than read off as `Fintype.card F`. Each Rabin condition is then +discharged by applying its certificate directly, avoiding the concrete caller-side `Eq.mpr` +transports introduced by `rw [hcard]`. Fresh replay checks a serialized and reconstructed expression +graph, so it need not follow the same normalization path as checking the elaborator's in-memory +term. In the observed cold replay, checking one such transport's certificate argument entered +`Polynomial.pow → npowRec → Nat.rec`, unfolding `X ^ (fieldSize ^ 6)` one exponent step at a time +until Lean's deep-recursion guard fired. Supply `hcard` as `ZMod.card _`. + +Nothing is weakened: instantiating at `q := Fintype.card F` with `rfl` recovers +`irreducible_of_rabin_prime_degree` verbatim, and `CompPolyTests.RabinCertificate` pins that +instantiation as a regression test. +-/ +theorem irreducible_of_rabin_prime_degree_of_card {F : Type*} [Field F] [Fintype F] + {f : F[X]} {d q : ℕ} (hcard : Fintype.card F = q) + (hd : d.Prime) (h_deg : f.natDegree = d) + (h_trace : f ∣ X ^ (q ^ d) - X) + (h_cop : IsCoprime f (X ^ q - X)) : + Irreducible f := by + subst hcard + exact irreducible_of_rabin_prime_degree hd h_deg h_trace h_cop + +/-- **Rabin's test at degree 6, with the cardinality abstracted into a numeral `q`.** See +`irreducible_of_rabin_prime_degree_of_card`; the same reasoning applies at composite degree 6. -/ +theorem irreducible_of_rabin_degree_six_of_card {F : Type*} [Field F] [Fintype F] + {f : F[X]} {q : ℕ} (hcard : Fintype.card F = q) + (h_deg : f.natDegree = 6) + (h_trace : f ∣ X ^ (q ^ 6) - X) + (h_cop₃ : IsCoprime f (X ^ (q ^ 3) - X)) + (h_cop₂ : IsCoprime f (X ^ (q ^ 2) - X)) : + Irreducible f := by + subst hcard + exact irreducible_of_rabin_degree_six h_deg h_trace h_cop₃ h_cop₂ + end CompPoly.RabinCert diff --git a/CompPoly/Data/RingTheory/AlgebraTower.lean b/CompPoly/Data/RingTheory/AlgebraTower.lean index 5bc81078..a9e2dd1d 100644 --- a/CompPoly/Data/RingTheory/AlgebraTower.lean +++ b/CompPoly/Data/RingTheory/AlgebraTower.lean @@ -5,45 +5,102 @@ Authors: Chung Thai Nguyen, Quang Dao -/ module +public import Mathlib.Data.Nat.Init public import Mathlib.LinearAlgebra.Matrix.Reindex /-! - # Tower of Algebras and Tower of Algebra Equivalences +# Towers of algebras and their equivalences - This file contains definitions, theorems, instances that are used in defining tower of algebras - and their equivalences. +An `AlgebraTower` is a preorder-indexed family of commutative semirings with ring +homomorphisms between comparable levels. Self-maps are identities, and the maps compose +along chains of indices. Each map induces an algebra structure on its target, and +composition gives compatible scalar actions across three levels. - ## Main definitions +`AlgebraTower.ofNatStep` constructs a natural-number-indexed tower by composing chosen +homomorphisms between adjacent levels. These homomorphisms need not be injective. - * `AlgebraTower` : a tower of algebras - * `AlgebraTowerEquiv` : an equivalence of towers of algebras +An `AlgebraTowerEquiv` consists of ring equivalences at each level that commute with +the tower maps. -/ @[expose] public section -/-- A tower of algebras is a sequence of algebras `AT i` indexed over a preorder `ι` with the - following data: - - `algebraMap : AT i →+* AT j` is a ring homomorphism from `AT i` to `AT j` for all `i ≤ j` - - `commutes'` is a proof that the ring homomorphism commutes with the multiplication - - `coherence'`: A tower of algebras is coherent if the algebra maps satisfy the - coherence condition: the direct map from i to k equals the composition of maps i → j → k. --/ +/-- A preorder-indexed family of commutative semirings with compatible ring homomorphisms. + +The map from a level to itself is the identity. For `i ≤ j ≤ k`, the map from `i` to `k` +is the composite of the maps from `i` to `j` and from `j` to `k`. -/ class AlgebraTower {ι : Type*} [Preorder ι] (AT : ι → Type*) [∀ i, CommSemiring (AT i)] where /-- Ring homomorphisms from `AT i` to `AT j` for all `i ≤ j`. -/ protected algebraMap : ∀ i j, (h : i ≤ j) → (AT i →+* AT j) - /-- Commutativity of multiplication with respect to the ring homomorphism. -/ + /-- The ring homomorphism from level `i` to itself is the identity. -/ + algebraMap_self' : ∀ i, algebraMap i i le_rfl = RingHom.id (AT i) + /-- Every image element commutes with every element of the target semiring. -/ commutes' : ∀ (i j : ι) (h : i ≤ j) (r : AT i) (x : AT j), (algebraMap i j h r) * x = x * (algebraMap i j h r) - coherence': ∀ (i j k : ι) (h1 : i ≤ j) (h2 : j ≤ k), + /-- The map from `i` to `k` is the composite of the maps from `i` to `j` and `j` to `k`. -/ + coherence' : ∀ (i j k : ι) (h1 : i ≤ j) (h2 : j ≤ k), algebraMap i k (h1.trans h2) = (algebraMap j k h2).comp (algebraMap i j h1) +namespace AlgebraTower + +section Nat + +variable {A : ℕ → Type*} [∀ k, CommSemiring (A k)] + +/-- Construct a tower by composing chosen ring homomorphisms between adjacent levels. + +The map from a level to itself is the identity. The map from `i` to `j + 1`, for `i ≤ j`, +is `step j` composed with the map from `i` to `j`. No injectivity assumption is required. -/ +@[instance_reducible] +def ofNatStep (step : ∀ k, A k →+* A (k + 1)) : AlgebraTower A where + algebraMap i _ h := + Nat.leRec (motive := fun j _ => A i →+* A j) (RingHom.id _) + (fun {_} _ f => (step _).comp f) h + algebraMap_self' _ := Nat.leRec_self _ _ + commutes' _ _ _ r x := mul_comm _ _ + coherence' _ _ _ hij hjk := by + induction hjk with + | refl => rw [Nat.leRec_self, RingHom.id_comp] + | @step k h ih => + rw [Nat.leRec_succ _ _ (hij.trans h), Nat.leRec_succ _ _ h, ih, RingHom.comp_assoc] + +/-- Extending a constructed tower map by one level composes it with the chosen next map. -/ +theorem ofNatStep_algebraMap_succ_right (step : ∀ k, A k →+* A (k + 1)) + {i j : ℕ} (h : i ≤ j) : + (ofNatStep step).algebraMap i (j + 1) (h.trans (Nat.le_succ j)) = + (step j).comp ((ofNatStep step).algebraMap i j h) := + Nat.leRec_succ _ _ h + +/-- The map between adjacent levels is the homomorphism supplied to the constructor. -/ +@[simp] +theorem ofNatStep_algebraMap_succ (step : ∀ k, A k →+* A (k + 1)) + (i : ℕ) (h : i ≤ i + 1) : + (ofNatStep step).algebraMap i (i + 1) h = step i := by + rw [ofNatStep_algebraMap_succ_right step (Nat.le_refl i), + (ofNatStep step).algebraMap_self', RingHom.comp_id] + +end Nat + +end AlgebraTower + variable {ι : Type*} [Preorder ι] {A : ι → Type*} [∀ i, CommSemiring (A i)] [AlgebraTower A] {B : ι → Type*} [∀ i, CommSemiring (B i)] [AlgebraTower B] {C : ι → Type*} [∀ i, CommSemiring (C i)] [AlgebraTower C] +/-- The tower map from level `i` to itself is the identity for every proof of `i ≤ i`. -/ +@[simp] +lemma AlgebraTower.algebraMap_self (i : ι) (h : i ≤ i) : + AlgebraTower.algebraMap (AT := A) i i h = RingHom.id (A i) := + AlgebraTower.algebraMap_self' i + +/-- The tower map from level `i` to itself fixes every element of that level. -/ +lemma AlgebraTower.algebraMap_self_apply (i : ι) (h : i ≤ i) (x : A i) : + AlgebraTower.algebraMap (AT := A) i i h x = x := by + rw [AlgebraTower.algebraMap_self, RingHom.id_apply] + @[simp] abbrev AlgebraTower.toAlgebra {i j : ι} (h : i ≤ j) : Algebra (A i) (A j) := (AlgebraTower.algebraMap (i:=i) (j:=j) (h:=h)).toAlgebra diff --git a/CompPoly/Data/RingTheory/AlgebraTower/Basis.lean b/CompPoly/Data/RingTheory/AlgebraTower/Basis.lean new file mode 100644 index 00000000..6618ce31 --- /dev/null +++ b/CompPoly/Data/RingTheory/AlgebraTower/Basis.lean @@ -0,0 +1,139 @@ +/- +Copyright (c) 2026 CompPoly Contributors. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: CompPoly Contributors +-/ +module + +public import CompPoly.Data.RingTheory.AlgebraTower.Coordinates +public import Mathlib.LinearAlgebra.Basis.Defs +public import Mathlib.RingTheory.AlgebraTower + +/-! +# Bases from adjacent tower coordinates + +`AlgebraTower.natBasis` is the basis determined by the executable equivalence +`AlgebraTower.natCoordinates`. Its `repr` is the coordinate map, and its vectors equal the +executable `AlgebraTower.natBasisVector` values. + +`AlgebraTower.natBasisOfLE` provides the same basis at arbitrary endpoints `i ≤ j`, +with vectors in `A j` and coefficients in `A i`. Its representation agrees with +`AlgebraTower.natCoordinatesOfLE`, and `AlgebraTower.natBasisVectorOfLE` computes its vectors. + +The successor basis agrees with `Module.Basis.smulTower'`, reindexed by `finProdFinEquiv`. +The old coordinate index varies fastest. Each vector is the embedded old basis vector +multiplied by the corresponding new successor basis vector. No normalization of that +successor vector is assumed. + +The construction uses `Module.Basis.ofEquivFun` and the scalar-tower basis equations from +Mathlib's `Mathlib.RingTheory.AlgebraTower` module. +-/ + +public section + +namespace AlgebraTower + +variable {A : ℕ → Type*} [∀ k, CommSemiring (A k)] [t : AlgebraTower A] + {d : ℕ → ℕ} + (step : ∀ k, letI := t.toAlgebra (Nat.le_succ k) + A (k + 1) ≃ₗ[A k] (Fin (d k) → A k)) + +/-- The basis whose coordinates are `natCoordinates`, over the action of the given tower map. -/ +noncomputable def natBasis (i n : ℕ) : + letI := t.toAlgebra (Nat.le_add_right i n) + Module.Basis (Fin (coordinateSize d i n)) (A i) (A (i + n)) := by + letI := t.toAlgebra (Nat.le_add_right i n) + exact Module.Basis.ofEquivFun (natCoordinates step i n) + +/-- The mathematical basis representation is the executable coordinate map. -/ +@[simp] +theorem natBasis_repr (i n : ℕ) (x : A (i + n)) (j : Fin (coordinateSize d i n)) : + letI := t.toAlgebra (Nat.le_add_right i n) + (natBasis step i n).repr x j = natCoordinates step i n x j := by + rfl + +/-- Packing a unit coordinate vector gives the corresponding mathematical basis vector. -/ +theorem natBasisVector_eq_natBasis (i n : ℕ) (j : Fin (coordinateSize d i n)) : + natBasisVector step i n j = natBasis step i n j := by + let := t.toAlgebra (Nat.le_add_right i n) + apply (natBasis step i n).repr.injective + ext idx + rw [natBasis_repr, congrFun (natCoordinates_natBasisVector step i n j) idx, + Module.Basis.repr_self] + simp only [Pi.single_apply, Finsupp.single_apply, eq_comm] + +/-- The next relative basis is the scalar-tower composition of the previous basis and the +next successor basis, with the old index varying fastest. -/ +theorem natBasis_succ (i n : ℕ) : + letI := t.toAlgebra (Nat.le_add_right i n) + letI := t.toAlgebra (Nat.le_succ (i + n)) + letI := t.toAlgebra (Nat.le_add_right i (n + 1)) + letI := toIsScalarTower t (Nat.le_add_right i n) (Nat.le_succ (i + n)) + natBasis step i (n + 1) = + ((natBasis step i n).smulTower' (Module.Basis.ofEquivFun (step (i + n)))).reindex + finProdFinEquiv := by + let := t.toAlgebra (Nat.le_add_right i n) + let := t.toAlgebra (Nat.le_succ (i + n)) + let := t.toAlgebra (Nat.le_add_right i (n + 1)) + let := toIsScalarTower t (Nat.le_add_right i n) (Nat.le_succ (i + n)) + apply Module.Basis.repr_injective + ext x j + exact natCoordinates_succ step i n x j + +/-- At flattened index `oldIndex + oldSize * newIndex`, the vector is the embedded old +vector multiplied by the new successor vector. -/ +theorem natBasisVector_succ (i n : ℕ) (b : Fin (d (i + n))) + (j : Fin (coordinateSize d i n)) : + natBasisVector step i (n + 1) (finProdFinEquiv (b, j)) = + t.algebraMap (i + n) (i + n + 1) (Nat.le_succ (i + n)) (natBasisVector step i n j) * + (step (i + n)).symm (Pi.single b 1) := by + let := t.toAlgebra (Nat.le_add_right i n) + let := t.toAlgebra (Nat.le_succ (i + n)) + let := t.toAlgebra (Nat.le_add_right i (n + 1)) + let := toIsScalarTower t (Nat.le_add_right i n) (Nat.le_succ (i + n)) + rw [natBasisVector_eq_natBasis, natBasis_succ] + simp only [Module.Basis.reindex_apply, Equiv.symm_apply_apply, Module.Basis.smulTower'_apply] + rw [natBasisVector_eq_natBasis, Module.Basis.coe_ofEquivFun] + rfl + +/-- The basis at arbitrary comparable endpoints, over the action of the given tower map. -/ +noncomputable def natBasisOfLE {i j : ℕ} (h : i ≤ j) : + letI := t.toAlgebra h + Module.Basis (Fin (coordinateSize d i (j - i))) (A i) (A j) := by + letI := t.toAlgebra h + exact Module.Basis.ofEquivFun (natCoordinatesOfLE step h) + +/-- The endpoint basis representation is the executable endpoint coordinate map. -/ +@[simp] +theorem natBasisOfLE_repr {i j : ℕ} (h : i ≤ j) (x : A j) + (idx : Fin (coordinateSize d i (j - i))) : + letI := t.toAlgebra h + (natBasisOfLE step h).repr x idx = natCoordinatesOfLE step h x idx := by + rfl + +/-- Packing a unit coordinate vector computes the corresponding endpoint basis vector. -/ +theorem natBasisVectorOfLE_eq_natBasisOfLE {i j : ℕ} (h : i ≤ j) + (idx : Fin (coordinateSize d i (j - i))) : + natBasisVectorOfLE step h idx = natBasisOfLE step h idx := by + let := t.toAlgebra h + apply (natBasisOfLE step h).repr.injective + ext pos + rw [natBasisOfLE_repr, congrFun (natCoordinatesOfLE_natBasisVectorOfLE step h idx) pos, + Module.Basis.repr_self] + simp only [Pi.single_apply, Finsupp.single_apply, eq_comm] + +/-- Endpoint basis vectors agree with the height-indexed basis after identifying the endpoints. -/ +theorem natBasisOfLE_apply {i j : ℕ} (h : i ≤ j) + (idx : Fin (coordinateSize d i (j - i))) : + natBasisOfLE step h idx = + cast (congrArg A (Nat.add_sub_of_le h)) (natBasis step i (j - i) idx) := by + rw [← natBasisVectorOfLE_eq_natBasisOfLE, natBasisVectorOfLE_eq_natBasisVector, + natBasisVector_eq_natBasis] + +/-- At equal endpoints the unique basis vector is one. -/ +@[simp] +theorem natBasisOfLE_self (i : ℕ) (idx : Fin (coordinateSize d i (i - i))) : + natBasisOfLE step (Nat.le_refl i) idx = 1 := by + rw [← natBasisVectorOfLE_eq_natBasisOfLE, natBasisVectorOfLE_self] + +end AlgebraTower diff --git a/CompPoly/Data/RingTheory/AlgebraTower/Coordinates.lean b/CompPoly/Data/RingTheory/AlgebraTower/Coordinates.lean new file mode 100644 index 00000000..3e0ac89e --- /dev/null +++ b/CompPoly/Data/RingTheory/AlgebraTower/Coordinates.lean @@ -0,0 +1,293 @@ +/- +Copyright (c) 2026 CompPoly Contributors. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: CompPoly Contributors +-/ +module + +public import CompPoly.Data.RingTheory.AlgebraTower +import Mathlib.LinearAlgebra.Pi +public import Mathlib.Logic.Equiv.Fin.Basic + +/-! +# Coordinates along a natural-number-indexed algebra tower + +Given linear coordinate equivalences between adjacent levels of an `AlgebraTower`, +`AlgebraTower.natCoordinates` gives coordinates at level `i + n` over level `i`. Each scalar +action is induced by the corresponding map of the given tower. The number of successor +coordinates may vary with the level, and may be zero when such an equivalence exists. + +The old coordinate index varies fastest: a pair `(newIndex, oldIndex)` has flattened index +`oldIndex + oldSize * newIndex`. Constant successor coordinate count two gives the usual +least-significant-bit-first order. + +`AlgebraTower.natPack` is the inverse coordinate map. `AlgebraTower.natBasisVector` packs +a unit coordinate vector. Both are executable, with no enumeration of the tower carriers. +The corresponding mathematical basis is constructed in +`CompPoly.Data.RingTheory.AlgebraTower.Basis`. + +For arbitrary endpoints `h : i ≤ j`, `AlgebraTower.natCoordinatesOfLE` and its packing +and vector companions accept elements of `A j` directly. `AlgebraTower.natCoordinatesConstOfLE` +also presents constant successor coordinate count `r` as `Fin (r ^ (j - i))`. + +The construction uses `LinearEquiv.restrictScalars`, `LinearEquiv.piCongrRight`, +`LinearEquiv.curry`, and `finProdFinEquiv`. Endpoint and coordinate-count identifications use +`LinearEquiv.cast`, `LinearEquiv.funCongrLeft`, and `finCongr`. +-/ + +public section + +namespace AlgebraTower + +private def composeCoordinates {R S T : Type*} + [CommSemiring R] [CommSemiring S] [CommSemiring T] + [Algebra R S] [Algebra S T] [Algebra R T] [IsScalarTower R S T] + {m n : ℕ} (e : S ≃ₗ[R] (Fin m → R)) (f : T ≃ₗ[S] (Fin n → S)) : + T ≃ₗ[R] (Fin (n * m) → R) := + (f.restrictScalars R).trans + ((LinearEquiv.piCongrRight fun _ : Fin n => e).trans + ((LinearEquiv.curry R R (Fin n) (Fin m)).symm.trans + (LinearEquiv.funCongrLeft R R finProdFinEquiv.symm))) + +/-- The number of coordinates from level `i` through `n` steps with chosen coordinate counts `d`. +The next step contributes a new outer block of coordinates. -/ +abbrev coordinateSize (d : ℕ → ℕ) (i n : ℕ) : ℕ := + Nat.rec 1 (fun k size => d (i + k) * size) n + +/-- An interval with no successor steps has one coordinate. -/ +@[simp] +theorem coordinateSize_zero (d : ℕ → ℕ) (i : ℕ) : coordinateSize d i 0 = 1 := rfl + +/-- The number of coordinates is multiplied by the next successor coordinate count. -/ +@[simp] +theorem coordinateSize_succ (d : ℕ → ℕ) (i n : ℕ) : + coordinateSize d i (n + 1) = d (i + n) * coordinateSize d i n := rfl + +/-- A constant successor coordinate count `r` gives `r ^ n` coordinates over `n` steps. -/ +theorem coordinateSize_const (r i n : ℕ) : + coordinateSize (fun _ => r) i n = r ^ n := by + induction n with + | zero => rfl + | succ n ih => rw [coordinateSize_succ, ih, pow_succ, Nat.mul_comm] + +variable {A : ℕ → Type*} [∀ k, CommSemiring (A k)] [t : AlgebraTower A] + {d : ℕ → ℕ} + (step : ∀ k, letI := t.toAlgebra (Nat.le_succ k) + A (k + 1) ≃ₗ[A k] (Fin (d k) → A k)) + +/-- Coordinates of `A (i + n)` over `A i`, obtained by composing adjacent equivalences. + +The scalar action is induced by the given tower map from `i` to `i + n`. At each step, +the old index varies fastest, so `(b, j)` is placed at `j + oldSize * b`. -/ +def natCoordinates (i : ℕ) : (n : ℕ) → + letI := t.toAlgebra (Nat.le_add_right i n) + A (i + n) ≃ₗ[A i] (Fin (coordinateSize d i n) → A i) + | 0 => by + -- Capture the coefficient action before installing the chosen self action. + letI : Module (A i) (Fin (coordinateSize d i 0) → A i) := inferInstance + letI : Module (A i) (A i) := (t.toAlgebra (Nat.le_add_right i 0)).toModule + let z : Fin (coordinateSize d i 0) := ⟨0, by change 0 < 1; decide⟩ + exact { + toFun := fun x _ => x + invFun := fun c => c z + map_add' := fun _ _ => rfl + map_smul' := fun a x => by + funext j + change t.algebraMap i i (Nat.le_add_right i 0) a * x = a * x + rw [algebraMap_self_apply] + left_inv := fun _ => rfl + right_inv := fun c => by + funext j + have hj : j = z := Fin.ext (by + have h : j.val < 1 := j.isLt + change j.val = 0 + omega) + subst j + rfl } + | n + 1 => by + letI := t.toAlgebra (Nat.le_add_right i n) + letI := t.toAlgebra (Nat.le_succ (i + n)) + letI := t.toAlgebra (Nat.le_add_right i (n + 1)) + letI := toIsScalarTower t (Nat.le_add_right i n) (Nat.le_succ (i + n)) + exact composeCoordinates (natCoordinates i n) (step (i + n)) + +/-- At height zero the single coordinate is the original element. -/ +@[simp] +theorem natCoordinates_zero (i : ℕ) (x : A i) (j : Fin (coordinateSize d i 0)) : + natCoordinates step i 0 x j = x := by + rfl + +/-- Read the new outer coordinate, then the old inner coordinate. +Division selects the outer block and remainder selects the position within that block. -/ +theorem natCoordinates_succ (i n : ℕ) (x : A (i + (n + 1))) + (j : Fin (coordinateSize d i (n + 1))) : + natCoordinates step i (n + 1) x j = + natCoordinates step i n (step (i + n) x j.divNat) j.modNat := by + rfl + +/-- Pack coordinates in the order used by `natCoordinates`, using the inverse successor maps. -/ +def natPack (i n : ℕ) (c : Fin (coordinateSize d i n) → A i) : A (i + n) := + (natCoordinates step i n).symm c + +/-- At height zero packing returns the single input coefficient. -/ +@[simp] +theorem natPack_zero (i : ℕ) (c : Fin (coordinateSize d i 0) → A i) : + natPack step i 0 c = c ⟨0, by simp only [coordinateSize_zero]; decide⟩ := by + rfl + +/-- Pack each old coordinate block, then apply the inverse of the next successor map. -/ +theorem natPack_succ (i n : ℕ) (c : Fin (coordinateSize d i (n + 1)) → A i) : + natPack step i (n + 1) c = + (step (i + n)).symm + (fun b => natPack step i n (fun j => c (finProdFinEquiv (b, j)))) := by + rfl + +/-- Reading back packed coordinates recovers the input coefficients. -/ +@[simp] +theorem natCoordinates_natPack (i n : ℕ) (c : Fin (coordinateSize d i n) → A i) : + natCoordinates step i n (natPack step i n c) = c := + (natCoordinates step i n).apply_symm_apply c + +/-- Packing the coordinates of an element recovers that element. -/ +@[simp] +theorem natPack_natCoordinates (i n : ℕ) (x : A (i + n)) : + natPack step i n (natCoordinates step i n x) = x := + (natCoordinates step i n).symm_apply_apply x + +/-- The executable vector whose coordinate at `j` is one and whose other coordinates are zero. -/ +def natBasisVector (i n : ℕ) (j : Fin (coordinateSize d i n)) : A (i + n) := + natPack step i n (Pi.single j 1) + +/-- A basis vector reads back as its unit coordinate vector. -/ +@[simp] +theorem natCoordinates_natBasisVector (i n : ℕ) (j : Fin (coordinateSize d i n)) : + natCoordinates step i n (natBasisVector step i n j) = Pi.single j 1 := + natCoordinates_natPack step i n (Pi.single j 1) + +private theorem natCoordinates_cast_apply (i : ℕ) {n m : ℕ} (h : n = m) + (x : A (i + n)) (idx : Fin (coordinateSize d i m)) : + natCoordinates step i m (cast (congrArg (fun k => A (i + k)) h) x) idx = + natCoordinates step i n x (Fin.cast (congrArg (coordinateSize d i) h.symm) idx) := by + cases h + rfl + +/-- Coordinates of `A j` over `A i` for comparable endpoints, with the given tower action. +The coordinate count and order are those of the `j - i` successor steps starting at `i`. -/ +def natCoordinatesOfLE {i j : ℕ} (h : i ≤ j) : + letI := t.toAlgebra h + A j ≃ₗ[A i] (Fin (coordinateSize d i (j - i)) → A i) := by + letI := t.toAlgebra h + letI : ∀ k : {k : ℕ // i ≤ k}, Module (A i) (A k.val) := + fun k => (t.toAlgebra k.property).toModule + let e : (⟨j, h⟩ : {k : ℕ // i ≤ k}) = + ⟨i + (j - i), Nat.le_add_right i (j - i)⟩ := + Subtype.ext (Nat.add_sub_of_le h).symm + exact (LinearEquiv.cast (R := A i) (M := fun k : {k : ℕ // i ≤ k} => A k.val) e).trans + (natCoordinates step i (j - i)) + +/-- Endpoint coordinates are height coordinates after identifying `j` with `i + (j - i)`. +The coordinate index is unchanged. -/ +theorem natCoordinatesOfLE_apply {i j : ℕ} (h : i ≤ j) (x : A j) + (idx : Fin (coordinateSize d i (j - i))) : + natCoordinatesOfLE step h x idx = + natCoordinates step i (j - i) (cast (congrArg A (Nat.add_sub_of_le h).symm) x) idx := by + rfl + +/-- With equal endpoints the single coordinate is the original element. -/ +@[simp] +theorem natCoordinatesOfLE_self (i : ℕ) (x : A i) + (idx : Fin (coordinateSize d i (i - i))) : + natCoordinatesOfLE step (Nat.le_refl i) x idx = x := by + rw [natCoordinatesOfLE_apply, + natCoordinates_cast_apply step i (Nat.sub_self i).symm, natCoordinates_zero] + +/-- Pack coordinates into `A j`, inverting the coordinate map over the given tower action. -/ +def natPackOfLE {i j : ℕ} (h : i ≤ j) (c : Fin (coordinateSize d i (j - i)) → A i) : A j := + (natCoordinatesOfLE step h).symm c + +/-- Endpoint packing is height packing followed by the identification `i + (j - i) = j`. -/ +theorem natPackOfLE_eq_natPack {i j : ℕ} (h : i ≤ j) + (c : Fin (coordinateSize d i (j - i)) → A i) : + natPackOfLE step h c = cast (congrArg A (Nat.add_sub_of_le h)) (natPack step i (j - i) c) := by + rfl + +/-- Packing at equal endpoints returns the single coefficient. -/ +@[simp] +theorem natPackOfLE_self (i : ℕ) (c : Fin (coordinateSize d i (i - i)) → A i) : + natPackOfLE step (Nat.le_refl i) c = + c ⟨0, by simp only [Nat.sub_self, coordinateSize_zero]; decide⟩ := by + exact (natCoordinatesOfLE_self step i (natPackOfLE step (Nat.le_refl i) c) _).symm.trans + (congrFun ((natCoordinatesOfLE step (Nat.le_refl i)).apply_symm_apply c) _) + +/-- Reading packed endpoint coordinates recovers the input coefficients. -/ +@[simp] +theorem natCoordinatesOfLE_natPackOfLE {i j : ℕ} (h : i ≤ j) + (c : Fin (coordinateSize d i (j - i)) → A i) : + natCoordinatesOfLE step h (natPackOfLE step h c) = c := + (natCoordinatesOfLE step h).apply_symm_apply c + +/-- Packing the endpoint coordinates of an element recovers that element. -/ +@[simp] +theorem natPackOfLE_natCoordinatesOfLE {i j : ℕ} (h : i ≤ j) (x : A j) : + natPackOfLE step h (natCoordinatesOfLE step h x) = x := + (natCoordinatesOfLE step h).symm_apply_apply x + +/-- The executable endpoint vector with coordinate one at `idx` and zero elsewhere. -/ +def natBasisVectorOfLE {i j : ℕ} (h : i ≤ j) (idx : Fin (coordinateSize d i (j - i))) : A j := + natPackOfLE step h (Pi.single idx 1) + +/-- Endpoint vectors agree with height vectors under the canonical endpoint identification. -/ +theorem natBasisVectorOfLE_eq_natBasisVector {i j : ℕ} (h : i ≤ j) + (idx : Fin (coordinateSize d i (j - i))) : + natBasisVectorOfLE step h idx = + cast (congrArg A (Nat.add_sub_of_le h)) (natBasisVector step i (j - i) idx) := + natPackOfLE_eq_natPack step h (Pi.single idx 1) + +/-- An endpoint vector reads back as its unit coordinate vector. -/ +@[simp] +theorem natCoordinatesOfLE_natBasisVectorOfLE {i j : ℕ} (h : i ≤ j) + (idx : Fin (coordinateSize d i (j - i))) : + natCoordinatesOfLE step h (natBasisVectorOfLE step h idx) = Pi.single idx 1 := + natCoordinatesOfLE_natPackOfLE step h (Pi.single idx 1) + +/-- At equal endpoints the unique executable basis vector is one. -/ +@[simp] +theorem natBasisVectorOfLE_self (i : ℕ) (idx : Fin (coordinateSize d i (i - i))) : + natBasisVectorOfLE step (Nat.le_refl i) idx = 1 := by + rw [natBasisVectorOfLE, natPackOfLE_self] + have hidx : idx = ⟨0, by simp only [Nat.sub_self, coordinateSize_zero]; decide⟩ := by + apply Fin.ext + have := idx.isLt + simp only [Nat.sub_self, coordinateSize_zero] at this + exact Nat.eq_zero_of_le_zero (Nat.le_of_lt_succ this) + simp only [hidx, Pi.single_eq_same] + +variable {r : ℕ} + (constantStep : ∀ k, letI := t.toAlgebra (Nat.le_succ k) + A (k + 1) ≃ₗ[A k] (Fin r → A k)) + +/-- Endpoint coordinates with constant successor coordinate count `r`, indexed by `Fin (r^(j-i))`. +Only the coordinate count is identified with a power; each index retains its numeric value. -/ +def natCoordinatesConstOfLE {i j : ℕ} (h : i ≤ j) : + letI := t.toAlgebra h + A j ≃ₗ[A i] (Fin (r ^ (j - i)) → A i) := by + letI := t.toAlgebra h + exact (natCoordinatesOfLE (d := fun _ => r) constantStep h).trans + (LinearEquiv.funCongrLeft (A i) (A i) + (finCongr (coordinateSize_const r i (j - i))).symm) + +/-- Constant-count coordinates read the same numeric index in the endpoint coordinate map. -/ +theorem natCoordinatesConstOfLE_apply {i j : ℕ} (h : i ≤ j) (x : A j) + (idx : Fin (r ^ (j - i))) : + natCoordinatesConstOfLE constantStep h x idx = + natCoordinatesOfLE constantStep h x + (Fin.cast (coordinateSize_const r i (j - i)).symm idx) := by + rfl + +/-- Constant-count coordinates at equal endpoints consist of the original element. -/ +@[simp] +theorem natCoordinatesConstOfLE_self (i : ℕ) (x : A i) (idx : Fin (r ^ (i - i))) : + natCoordinatesConstOfLE constantStep (Nat.le_refl i) x idx = x := by + rw [natCoordinatesConstOfLE_apply, natCoordinatesOfLE_self] + +end AlgebraTower diff --git a/CompPoly/Fields/BabyBear/Basic.lean b/CompPoly/Fields/BabyBear/Basic.lean index 75598e07..b8b67b6b 100644 --- a/CompPoly/Fields/BabyBear/Basic.lean +++ b/CompPoly/Fields/BabyBear/Basic.lean @@ -50,6 +50,8 @@ def twoAdicity : Nat := 27 instance : Fact (Nat.Prime fieldSize) := ⟨is_prime⟩ +instance : _root_.Field Field := ZMod.instField fieldSize + instance : NonBinaryField Field where char_neq_2 := by decide diff --git a/CompPoly/Fields/BabyBear/Ext4.lean b/CompPoly/Fields/BabyBear/Ext4.lean index 0bc5c977..872dcc9a 100644 --- a/CompPoly/Fields/BabyBear/Ext4.lean +++ b/CompPoly/Fields/BabyBear/Ext4.lean @@ -48,7 +48,9 @@ def ext4Params : BinomialParams Field where W := 11 two_le := by norm_num q := fieldSize - card_eq := ZMod.card _ + +instance : Fact (Nat.card Field = ext4Params.q) := + ⟨by rw [Nat.card_eq_fintype_card]; exact ZMod.card _⟩ @[simp] theorem ext4Params_d : ext4Params.d = 4 := rfl @[simp] theorem ext4Params_W : ext4Params.W = 11 := rfl diff --git a/CompPoly/Fields/Binary/AdditiveNTT/AdditiveNTT.lean b/CompPoly/Fields/Binary/AdditiveNTT/AdditiveNTT.lean index 63e3e4d0..f8a2428d 100644 --- a/CompPoly/Fields/Binary/AdditiveNTT/AdditiveNTT.lean +++ b/CompPoly/Fields/Binary/AdditiveNTT/AdditiveNTT.lean @@ -9,3 +9,4 @@ public import CompPoly.Fields.Binary.AdditiveNTT.Domain public import CompPoly.Fields.Binary.AdditiveNTT.Intermediate public import CompPoly.Fields.Binary.AdditiveNTT.Algorithm public import CompPoly.Fields.Binary.AdditiveNTT.Correctness +public import CompPoly.Fields.Binary.AdditiveNTT.Impl diff --git a/CompPoly/Fields/Binary/AdditiveNTT/Correctness.lean b/CompPoly/Fields/Binary/AdditiveNTT/Correctness.lean index 272236cb..7eda627e 100644 --- a/CompPoly/Fields/Binary/AdditiveNTT/Correctness.lean +++ b/CompPoly/Fields/Binary/AdditiveNTT/Correctness.lean @@ -6,7 +6,7 @@ Authors: Chung Thai Nguyen, Quang Dao module public import CompPoly.Fields.Binary.AdditiveNTT.Algorithm -public import CompPoly.Fields.Binary.AdditiveNTT.Impl +public import CompPoly.Fields.Binary.AdditiveNTT.Executable public import Mathlib.Algebra.CharP.CharAndCard public import Mathlib.Algebra.CharP.Two diff --git a/CompPoly/Fields/Binary/AdditiveNTT/Executable.lean b/CompPoly/Fields/Binary/AdditiveNTT/Executable.lean new file mode 100644 index 00000000..083066ac --- /dev/null +++ b/CompPoly/Fields/Binary/AdditiveNTT/Executable.lean @@ -0,0 +1,318 @@ +/- +Copyright (c) 2024-2025 ArkLib Contributors. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Chung Thai Nguyen, Quang Dao +-/ +module + +public import CompPoly.Fields.Binary.AdditiveNTT.Algorithm +import CompPoly.Data.Fin.BigOperators + +/-! +# Executable additive NTT algorithms + +Generic function-backed and array-backed additive NTT implementations over a finite field, +with an explicitly supplied binary-subfield algebra and independent basis vectors. The +function-backed transform mirrors the abstract stages; the array-backed transform caches +subspace-polynomial constants and twiddle tables. + +`CompPoly.Fields.Binary.AdditiveNTT.Correctness` relates these implementations to the +abstract algorithm and its evaluation specification. Concrete tower instances and the +compatibility entry point remain in `CompPoly.Fields.Binary.AdditiveNTT.Impl`. + +The coefficient tiling, twiddle factors, and descending butterfly stages follow the +Lin–Chung–Han additive NTT presentation in [DP24], Algorithm 2. The array-backed path +caches quantities used by these same stages. + +## References + +* [Diamond, B. E. and Posen, J., *Polylogarithmic proofs for multilinears over binary towers*][DP24] +-/ + +@[expose] public section + +namespace AdditiveNTT + +section HelperFunctions +/-- Read an array of length `n` as a function on `Fin n`. -/ +def Array.toFinVec {α : Type _} (n : ℕ) (arr : Array α) (h : arr.size = n) : Fin n → α := + fun i => arr[i] + +/-- Converts an array to a `Fin n` function, using `0` for missing entries. -/ +def arrayToFinFunction {α : Type _} [Zero α] (n : ℕ) (arr : Array α) : Fin n → α := + fun i => arr.getD i.val 0 + +/-- The product of a function over the list of finite indices equals its finite product. -/ +lemma List.prod_finRange_eq_finset_prod {M : Type*} [CommMonoid M] {n : ℕ} (f : Fin n → M) : + ((List.finRange n).map f).prod = ∏ i : Fin n, f i := rfl + +end HelperFunctions + +universe u + +variable {r : ℕ} [NeZero r] +variable {L : Type} [Field L] [Fintype L] [DecidableEq L] +variable {𝔽q : Type} [Field 𝔽q] [Fintype 𝔽q] [DecidableEq 𝔽q] +variable [hFq_card : Fact (Fintype.card 𝔽q = 2)] +variable [h_Fq_char_prime : Fact (Nat.Prime (ringChar 𝔽q))] +variable [Algebra 𝔽q L] +variable (β : Fin r → L) [hβ_lin_indep : Fact (LinearIndependent 𝔽q β)] +variable [h_β₀_eq_1 : Fact (β 0 = 1)] + +section Algorithm +variable {ℓ R_rate : ℕ} (h_ℓ_add_R_rate : ℓ + R_rate < r) + +/-- Map the numeric mask `k` to the sum of the first `i` basis vectors selected by its bits, +as an element of their linear span. -/ +def bitsToU (i : Fin r) (k : Fin (2 ^ i.val)) : + AdditiveNTT.U (L := L) (𝔽q := 𝔽q) (β := β) i := + let val := (Finset.univ : Finset (Fin i)).sum fun j => + if (Nat.getBit (n := k.val) (k := j.val) == 1) then + β ⟨j, by omega⟩ + else 0 + + ⟨val, by + apply Submodule.sum_mem + intro j _ + split + · apply Submodule.subset_span + refine Set.mem_image_of_mem β ?_ + rw [Set.mem_Ico] + exact ⟨Fin.zero_le _, j.isLt⟩ + · exact Submodule.zero_mem _ + ⟩ + +/-- Computes the elements of the subspace: `U_i = span({β_0, ..., β_{i-1}})`. -/ +def getUElements (i : Fin r) : List L := + (List.finRange (2^i.val)).map fun k => + (Finset.univ : Finset (Fin i)).sum fun j => + if Nat.getBit (n := k.val) (k := j.val) == 1 then + β ⟨j.val, by omega⟩ + else 0 + +/-- Evaluates the subspace vanishing polynomial `W_i(x) = ∏_{u ∈ U_i} (x - u).` -/ +def evalWAt (i : Fin r) (x : L) : L := + ((getUElements (β := β) (ℓ := ℓ) (R_rate := R_rate) i).map (fun u => x - u)).prod + +/-- Evaluates the normalized subspace vanishing polynomial `Ŵ_i(x) = W_i(x) / W_i(β_i)`. -/ +def evalNormalizedWAt (i : Fin r) (x : L) : L := + let W_x := evalWAt (r := r) (L := L) (ℓ := ℓ) (β := β) (R_rate := R_rate) (i := i) x + let beta_i := β i + let W_beta := evalWAt (β := β) (ℓ := ℓ) (R_rate := R_rate) (i := i) beta_i + W_x * W_beta⁻¹ + +/-- Compute the stage-`i` twiddle factor by summing normalized subspace-polynomial +evaluations selected by the bits of `u`. -/ +def computableTwiddleFactor (i : Fin ℓ) (u : Fin (2 ^ (ℓ + R_rate - i - 1))) : L := + ∑ (⟨k, hk⟩: Fin (ℓ + R_rate - i - 1)), + if Nat.getBit k u.val = 1 then + (evalNormalizedWAt (β := β) (ℓ := ℓ) (R_rate := R_rate) + (i := ⟨i, by omega⟩) (x := β ⟨i + 1 + k, by omega⟩)) + else 0 + +-- The `Fact` instance is stated explicitly (matching the variable declaration) so that the +-- basis `β` and field `𝔽q` remain named parameters for the `computableAdditiveNTT` call site. +set_option linter.overlappingInstances false in +/-- Perform stage `i` on the coefficient buffer `b`, pairing entries whose indices differ +in bit `i` and applying the corresponding twiddle factor. -/ +def computableNTTStage [Fact (LinearIndependent 𝔽q β)] + (i : Fin ℓ) (b : Fin (2 ^ (ℓ + R_rate)) → L) : Fin (2^(ℓ + R_rate)) → L := + have h_2_pow_i_lt_2_pow_ℓ_add_R_rate: 2^i.val < 2^(ℓ + R_rate) := by + calc + 2^i.val < 2 ^ (ℓ) := by + have hr := Nat.pow_lt_pow_right (a:=2) (m:=i.val) (n:=ℓ) (ha:=by omega) (by omega) + exact hr + _ ≤ 2 ^ (ℓ + R_rate) := by + exact Nat.pow_le_pow_right (n:=2) (i := ℓ) (j:=ℓ + R_rate) (by omega) (by omega) + fun (j : Fin (2^(ℓ + R_rate))) => + let u_b_v := j.val + have h_u_b_v : u_b_v = j.val := by rfl + let v: Fin (2^i.val) := ⟨Nat.getLowBits i.val u_b_v, by + have res := Nat.getLowBits_lt_two_pow (numLowBits:=i.val) (n:=u_b_v) + simp only [res] + ⟩ -- the i LSBs + let u_b := u_b_v / (2^i.val) -- the high (ℓ + R_rate - i) bits + have h_u_b : u_b = u_b_v / (2^i.val) := by rfl + have h_u_b_lt_2_pow : u_b < 2 ^ (ℓ + R_rate - i) := by + -- {m n k : Nat} (h : m < n * k) : m / n < k := + have res := Nat.div_lt_of_lt_mul (m:=u_b_v) (n:=2^i.val) (k:=2^(ℓ + R_rate - i)) (by + calc _ < 2 ^ (ℓ + R_rate) := by omega + _ = 2 ^ i.val * 2 ^ (ℓ + R_rate - i.val) := by + exact Eq.symm (pow_mul_pow_sub (a:=2) (m:=i.val) (n:=ℓ + R_rate) (by omega)) + ) + rw [h_u_b] + exact res + let u: ℕ := u_b / 2 -- the remaining high bits + let b_bit := u_b % 2 -- the LSB of the high bits, i.e. the `i`-th Nat.getBit + have h_u : u = u_b / 2 := by rfl + have h_u_lt_2_pow: u < 2 ^ (ℓ + R_rate - (i + 1)) := by + have h_u_eq: u = j.val / (2 ^ (i.val + 1)) := by + rw [h_u, h_u_b, h_u_b_v] + rw [Nat.div_div_eq_div_mul] + rfl + rw [h_u_eq] + -- ⊢ ↑j / 2 ^ (↑i + 1) < 2 ^ (ℓ + R_rate - (↑i + 1)) + exact div_two_pow_lt_two_pow (x:=j.val) (i := ℓ + R_rate - (i.val + 1)) (j:=i.val + 1) (by + rw [Nat.sub_add_cancel (by omega)] + omega + ) + let twiddleFactor: L := computableTwiddleFactor (r := r) (ℓ := ℓ) (β := β) (L := L) + (h_ℓ_add_R_rate := h_ℓ_add_R_rate) (i := ⟨i, by omega⟩) + (u := ⟨u, by simp only; exact h_u_lt_2_pow⟩) + let x0 := twiddleFactor -- since the last Nat.getBit of u||0 is 0 + let x1: L := x0 + 1 -- since the last Nat.getBit of u||1 is 1 and 1 * Ŵᵢ(βᵢ) = 1 + + have h_b_bit : b_bit = Nat.getBit i.val j.val := by + simp only [Nat.getBit, Nat.and_one_is_mod, b_bit, u_b, u_b_v] + rw [←Nat.shiftRight_eq_div_pow (m:=j.val) (n:=i.val)] + -- Each output reads from the unchanged input buffer. + if h_b_bit_zero: b_bit = 0 then -- This is the `b(u||0||v)` case + let odd_split_index := u_b_v + 2^i.val + have h_lt: odd_split_index < 2^(ℓ + R_rate) := by + have h_exp_eq: (↑i + (ℓ + R_rate - i)) = ℓ + R_rate := by omega + simp only [gt_iff_lt, odd_split_index, u_b_v] + -- ⊢ ↑j + 2 ^ ↑i < 2 ^ (ℓ + R_rate) + exact Nat.add_two_pow_of_getBit_eq_zero_lt_two_pow (n:=j.val) (m:=ℓ + R_rate) + (i := i.val) (h_n:=by omega) (h_i := by omega) (h_getBit_at_i_eq_zero:=by + rw [h_b_bit_zero] at h_b_bit + exact h_b_bit.symm + ) + b j + x0 * b ⟨odd_split_index, h_lt⟩ + else -- This is the `b(u||1||v)` case + let even_split_index := u_b_v ^^^ 2^i.val + have h_lt: even_split_index < 2^(ℓ + R_rate) := by + have h_exp_eq: (↑i + (ℓ + R_rate - i)) = ℓ + R_rate := by omega + simp only [even_split_index, u_b_v] + apply Nat.xor_lt_two_pow (by omega) (by omega) + -- b j is now the odd refinement P₁,₍₁ᵥ₎⁽ⁱ⁺¹⁾(X), + -- b (j - 2^i) stores the even refinement P₀,₍₀ᵥ₎⁽ⁱ⁺¹⁾(X) + b ⟨even_split_index, h_lt⟩ + x1 * b j + +/-- Transform `2 ^ ℓ` novel-basis coefficients into `2 ^ (ℓ + R_rate)` values by first +tiling the coefficients and then applying stages `ℓ - 1` down to `0`. The basis has length +`r`, and `ℓ + R_rate < r` bounds the evaluation domain. -/ +def computableAdditiveNTT (a : Fin (2 ^ ℓ) → L) : Fin (2^(ℓ + R_rate)) → L := + let b: Fin (2^(ℓ + R_rate)) → L := tileCoeffs a -- Note: can optimize on this + Fin.foldl (n:=ℓ) (f:= fun current_b i => + computableNTTStage (𝔽q := 𝔽q) (β := β) (ℓ := ℓ) (R_rate := R_rate) + (h_ℓ_add_R_rate := h_ℓ_add_R_rate) (i := ⟨ℓ - i - 1, by omega⟩) (b:=current_b) + ) (init:=b) + +/-- Array-backed coefficient tiling for the fast additive NTT path. -/ +def tileCoeffsArray (R_rate : ℕ) (a : Fin (2 ^ ℓ) → L) : Array L := + Array.ofFn (n := 2^(ℓ + R_rate)) fun v => + a ⟨v.val % (2^ℓ), Nat.mod_lt v.val (pow_pos (zero_lt_two) ℓ)⟩ + +/-- Starting from `acc`, iterate `acc ↦ acc * (acc + constants[k])` for indices +`k` from `j` to `constants.size - 1`. Return `acc` unchanged when `constants.size ≤ j`. + +For `j ≤ constants.size`, if the initial accumulator is `W_j(x)` and the remaining +constants are `W_k(β_k)`, the result is `W_{constants.size}(x)`. -/ +def evalWAtCachedConstantsLoop (constants : Array L) (j : Nat) (acc : L) : L := + if _h_j : j < constants.size then + let c := constants.getD j 0 + evalWAtCachedConstantsLoop constants (j + 1) (acc * (acc + c)) + else + acc +termination_by constants.size - j + +/-- Evaluate a subspace polynomial using cached constants `W_k(β_k)`. + +Starting from `W_0(x) = x`, each cached constant advances the recurrence +`W_{k+1}(x) = W_k(x) * (W_k(x) + W_k(β_k))`. -/ +def evalWAtCachedConstants (constants : Array L) (x : L) : L := + evalWAtCachedConstantsLoop constants 0 x + +/-- Extend the supplied `constants` array, starting at index `k` and stopping before `i`. +Each step evaluates the polynomial recurrence encoded by the current array at `β_k` +and appends the result. Return the array unchanged when `i ≤ k`. + +If the supplied array has length `k` and entry `j` is `W_j(β_j)` for every `j < k`, +each appended entry has the same interpretation at its index. -/ +def subspacePolynomialConstantsArrayLoop (i : Fin r) (k : Nat) (constants : Array L) : Array L := + if h_k : k < i.val then + let constant := evalWAtCachedConstants constants (β ⟨k, by omega⟩) + subspacePolynomialConstantsArrayLoop i (k + 1) (constants.push constant) + else + constants +termination_by i.val - k + +/-- Precompute the constants `W_k(β_k)` needed by the recursive subspace +polynomial evaluator up to stage `i`. -/ +def subspacePolynomialConstantsArray (i : Fin r) : Array L := + subspacePolynomialConstantsArrayLoop (β := β) (ℓ := ℓ) (R_rate := R_rate) i 0 #[] + +/-- Precompute normalized vanishing evaluations used by one stage's twiddle factors. -/ +def computableNormalizedWValuesArray (i : Fin ℓ) : Array L := + let stage : Fin r := ⟨i, by omega⟩ + let constants := subspacePolynomialConstantsArray (β := β) (ℓ := ℓ) (R_rate := R_rate) + (i := stage) + let denominatorInv := (evalWAtCachedConstants constants (β stage))⁻¹ + Array.ofFn (n := ℓ + R_rate - i - 1) fun k => + evalWAtCachedConstants constants (β ⟨i + 1 + k.val, by omega⟩) * denominatorInv + +/-- Precompute all twiddle factors for one additive NTT stage. + +The table entry for `u` is the subset sum of the cached normalized values +selected by the set bits of `u`. -/ +def computableTwiddleTableArray (i : Fin ℓ) : Array L := + let normalizedValues := computableNormalizedWValuesArray (β := β) (ℓ := ℓ) + (R_rate := R_rate) (h_ℓ_add_R_rate := h_ℓ_add_R_rate) (i := i) + let numBits := ℓ + R_rate - i - 1 + Array.ofFn (n := 2 ^ numBits) fun u => + ∑ k : Fin numBits, + if Nat.getBit k.val u.val = 1 then normalizedValues.getD k.val 0 else 0 + +/-- Array update for one additive NTT stage. + +The `twiddles` array is intended to store the values of `computableTwiddleFactor` +for this stage, indexed by `u`. Missing entries in either input array are read as zero. -/ +def computableNTTStageArray (i : Fin ℓ) (twiddles : Array L) (b : Array L) : Array L := + let stride := 2^i.val + Array.ofFn (n := 2^(ℓ + R_rate)) fun j => + let u_b_v := j.val + let u_b := u_b_v / stride + let u := u_b / 2 + let b_bit := u_b % 2 + let twiddleFactor : L := twiddles.getD u 0 + let x0 := twiddleFactor + let x1 : L := x0 + 1 + if _h_b_bit_zero : b_bit = 0 then + let oddIndex := u_b_v + stride + b.getD u_b_v 0 + x0 * b.getD oddIndex 0 + else + let evenIndex := u_b_v ^^^ stride + b.getD evenIndex 0 + x1 * b.getD u_b_v 0 + +/-- Fast additive NTT stage driver over an `Array L` state. + +The state is expected to contain the initialized output buffer. Each stage +updates that buffer using the array transition from +`computableNTTStageArray`. -/ +def computableAdditiveNTTFastStages : StateM (Array L) Unit := do + let _ ← Fin.foldlM (m := StateM (Array L)) (n := ℓ) (f := fun (_ : Unit) i => do + let stage : Fin ℓ := ⟨ℓ - i - 1, by omega⟩ + let twiddles := computableTwiddleTableArray (β := β) (ℓ := ℓ) + (R_rate := R_rate) (h_ℓ_add_R_rate := h_ℓ_add_R_rate) (i := stage) + modifyThe (Array L) fun current => + computableNTTStageArray (ℓ := ℓ) (R_rate := R_rate) + (i := stage) (twiddles := twiddles) current + pure ()) (init := ()) + pure () + +/-- Fast additive NTT array producer as a state action. -/ +def computableAdditiveNTTFastAction (a : Fin (2 ^ ℓ) → L) : + StateM (Array L) (Array L) := do + set (tileCoeffsArray (ℓ := ℓ) R_rate a) + computableAdditiveNTTFastStages (β := β) (ℓ := ℓ) (R_rate := R_rate) + (h_ℓ_add_R_rate := h_ℓ_add_R_rate) + getThe (Array L) + +/-- Fast additive NTT array producer. -/ +def computableAdditiveNTTFast (a : Fin (2 ^ ℓ) → L) : Array L := + ((computableAdditiveNTTFastAction (β := β) (ℓ := ℓ) + (R_rate := R_rate) (h_ℓ_add_R_rate := h_ℓ_add_R_rate) a).run #[]).1 + +end Algorithm + +end AdditiveNTT diff --git a/CompPoly/Fields/Binary/AdditiveNTT/Impl.lean b/CompPoly/Fields/Binary/AdditiveNTT/Impl.lean index ac521fd1..c8c3f990 100644 --- a/CompPoly/Fields/Binary/AdditiveNTT/Impl.lean +++ b/CompPoly/Fields/Binary/AdditiveNTT/Impl.lean @@ -5,14 +5,17 @@ Authors: Chung Thai Nguyen, Quang Dao -/ module -public import CompPoly.Fields.Binary.AdditiveNTT.Algorithm +public import CompPoly.Fields.Binary.AdditiveNTT.Executable public import CompPoly.Fields.Binary.Tower.Concrete.Basis public import Mathlib.Data.BitVec /-! -# Additive NTT Implementation +# Concrete additive NTT instances -Concrete implementation of the Additive NTT algorithm. +Compatibility entry point for the generic executable additive NTT algorithms, together with +concrete binary tower bases, instances and the existing example. Generic algorithm clients can +import `CompPoly.Fields.Binary.AdditiveNTT.Executable`; generic correctness clients can import +`CompPoly.Fields.Binary.AdditiveNTT.Correctness`. -/ @[expose] public section @@ -20,286 +23,7 @@ Concrete implementation of the Additive NTT algorithm. namespace AdditiveNTT open ConcreteBinaryTower -section HelperFunctions -/-- Converts an Array to a Fin function of a specific size `n`. -/ -def Array.toFinVec {α : Type _} (n : ℕ) (arr : Array α) (h : arr.size = n) : Fin n → α := - fun i => arr[i] - -/-- Converts an array to a `Fin n` function, using `0` for missing entries. -/ -def arrayToFinFunction {α : Type _} [Zero α] (n : ℕ) (arr : Array α) : Fin n → α := - fun i => arr.getD i.val 0 - -/- The product of a function mapped over the list `0..n-1`. -/ -lemma List.prod_finRange_eq_finset_prod {M : Type*} [CommMonoid M] {n : ℕ} (f : Fin n → M) : - ((List.finRange n).map f).prod = ∏ i : Fin n, f i := rfl - -end HelperFunctions - -universe u - variable {r : ℕ} [NeZero r] -variable {L : Type} [Field L] [Fintype L] [DecidableEq L] -variable {𝔽q : Type} [Field 𝔽q] [Fintype 𝔽q] [DecidableEq 𝔽q] -variable [hFq_card : Fact (Fintype.card 𝔽q = 2)] -variable [h_Fq_char_prime : Fact (Nat.Prime (ringChar 𝔽q))] -variable [Algebra 𝔽q L] -variable (β : Fin r → L) [hβ_lin_indep : Fact (LinearIndependent 𝔽q β)] -variable [h_β₀_eq_1 : Fact (β 0 = 1)] - -section Algorithm -variable {ℓ R_rate : ℕ} (h_ℓ_add_R_rate : ℓ + R_rate < r)-- ℓ ∈ {1, ..., r-1} - -/-- Define the mapping explicitly from the index k to the Submodule U. -/ -def bitsToU (i : Fin r) (k : Fin (2 ^ i.val)) : - AdditiveNTT.U (L := L) (𝔽q := 𝔽q) (β := β) i := - let val := (Finset.univ : Finset (Fin i)).sum fun j => - if (Nat.getBit (n := k.val) (k := j.val) == 1) then - β ⟨j, by omega⟩ - else 0 - - -- We essentially reuse your existing proof that this value is in the subspace - ⟨val, by - apply Submodule.sum_mem - intro j _ - split - · apply Submodule.subset_span - -- refine ⟨j, ?_, rfl⟩ - refine Set.mem_image_of_mem β ?_ - rw [Set.mem_Ico] - exact ⟨Fin.zero_le _, j.isLt⟩ - · exact Submodule.zero_mem _ - ⟩ - -/-- Computes the elements of the subspace: `U_i = span({β_0, ..., β_{i-1}})`. -/ -def getUElements (i : Fin r) : List L := - (List.finRange (2^i.val)).map fun k => - (Finset.univ : Finset (Fin i)).sum fun j => - if Nat.getBit (n := k.val) (k := j.val) == 1 then - β ⟨j.val, by omega⟩ - else 0 - -/-- Evaluates the subspace vanishing polynomial `W_i(x) = ∏_{u ∈ U_i} (x - u).` -/ -def evalWAt (i : Fin r) (x : L) : L := - ((getUElements (β := β) (ℓ := ℓ) (R_rate := R_rate) i).map (fun u => x - u)).prod - -/-- Evaluates the normalized subspace vanishing polynomial `Ŵ_i(x) = W_i(x) / W_i(β_i)`. -/ -def evalNormalizedWAt (i : Fin r) (x : L) : L := - let W_x := evalWAt (r := r) (L := L) (ℓ := ℓ) (β := β) (R_rate := R_rate) (i := i) x - let beta_i := β i - let W_beta := evalWAt (β := β) (ℓ := ℓ) (R_rate := R_rate) (i := i) beta_i - W_x * W_beta⁻¹ - -/-- Computes the twiddle factor used in the butterfly operation. -Corresponds to `AdditiveNTT.twiddleFactor`. --/ -def computableTwiddleFactor (i : Fin ℓ) (u : Fin (2 ^ (ℓ + R_rate - i - 1))) : L := - -- evalNormalizedWAt L i u - ∑ (⟨k, hk⟩: Fin (ℓ + R_rate - i - 1)), - if Nat.getBit k u.val = 1 then - -- this branch maps to the above Nat.getBit = 1 branch - -- (of evaluationPointω (i+1)) under (qMap i)(X) - (evalNormalizedWAt (β := β) (ℓ := ℓ) (R_rate := R_rate) - (i := ⟨i, by omega⟩) (x := β ⟨i + 1 + k, by omega⟩)) - else 0 - --- The `Fact` instance is stated explicitly (matching the variable declaration) so that the --- basis `β` and field `𝔽q` remain named parameters for the `computableAdditiveNTT` call site. -set_option linter.overlappingInstances false in -/-- Performs one stage of the Additive NTT. This corresponds to `NTTStage` in the abstract -definition: `b` is the array of coefficients. `i` is the stage index (0 to r-1). -/ -def computableNTTStage [Fact (LinearIndependent 𝔽q β)] - (i : Fin ℓ) (b : Fin (2 ^ (ℓ + R_rate)) → L) : Fin (2^(ℓ + R_rate)) → L := - have h_2_pow_i_lt_2_pow_ℓ_add_R_rate: 2^i.val < 2^(ℓ + R_rate) := by - calc - 2^i.val < 2 ^ (ℓ) := by - have hr := Nat.pow_lt_pow_right (a:=2) (m:=i.val) (n:=ℓ) (ha:=by omega) (by omega) - exact hr - _ ≤ 2 ^ (ℓ + R_rate) := by - exact Nat.pow_le_pow_right (n:=2) (i := ℓ) (j:=ℓ + R_rate) (by omega) (by omega) - fun (j : Fin (2^(ℓ + R_rate))) => - let u_b_v := j.val - have h_u_b_v : u_b_v = j.val := by rfl - let v: Fin (2^i.val) := ⟨Nat.getLowBits i.val u_b_v, by - have res := Nat.getLowBits_lt_two_pow (numLowBits:=i.val) (n:=u_b_v) - simp only [res] - ⟩ -- the i LSBs - let u_b := u_b_v / (2^i.val) -- the high (ℓ + R_rate - i) bits - have h_u_b : u_b = u_b_v / (2^i.val) := by rfl - have h_u_b_lt_2_pow : u_b < 2 ^ (ℓ + R_rate - i) := by - -- {m n k : Nat} (h : m < n * k) : m / n < k := - have res := Nat.div_lt_of_lt_mul (m:=u_b_v) (n:=2^i.val) (k:=2^(ℓ + R_rate - i)) (by - calc _ < 2 ^ (ℓ + R_rate) := by omega - _ = 2 ^ i.val * 2 ^ (ℓ + R_rate - i.val) := by - exact Eq.symm (pow_mul_pow_sub (a:=2) (m:=i.val) (n:=ℓ + R_rate) (by omega)) - ) - rw [h_u_b] - exact res - let u: ℕ := u_b / 2 -- the remaining high bits - let b_bit := u_b % 2 -- the LSB of the high bits, i.e. the `i`-th Nat.getBit - have h_u : u = u_b / 2 := by rfl - have h_u_lt_2_pow: u < 2 ^ (ℓ + R_rate - (i + 1)) := by - have h_u_eq: u = j.val / (2 ^ (i.val + 1)) := by - rw [h_u, h_u_b, h_u_b_v] - rw [Nat.div_div_eq_div_mul] - rfl - rw [h_u_eq] - -- ⊢ ↑j / 2 ^ (↑i + 1) < 2 ^ (ℓ + R_rate - (↑i + 1)) - exact div_two_pow_lt_two_pow (x:=j.val) (i := ℓ + R_rate - (i.val + 1)) (j:=i.val + 1) (by - rw [Nat.sub_add_cancel (by omega)] - omega - ) - let twiddleFactor: L := computableTwiddleFactor (r := r) (ℓ := ℓ) (β := β) (L := L) - (h_ℓ_add_R_rate := h_ℓ_add_R_rate) (i := ⟨i, by omega⟩) - (u := ⟨u, by simp only; exact h_u_lt_2_pow⟩) - let x0 := twiddleFactor -- since the last Nat.getBit of u||0 is 0 - let x1: L := x0 + 1 -- since the last Nat.getBit of u||1 is 1 and 1 * Ŵᵢ(βᵢ) = 1 - - have h_b_bit : b_bit = Nat.getBit i.val j.val := by - simp only [Nat.getBit, Nat.and_one_is_mod, b_bit, u_b, u_b_v] - rw [←Nat.shiftRight_eq_div_pow (m:=j.val) (n:=i.val)] - -- b remains unchanged through this whole function cuz we create new buffer - if h_b_bit_zero: b_bit = 0 then -- This is the `b(u||0||v)` case - let odd_split_index := u_b_v + 2^i.val - have h_lt: odd_split_index < 2^(ℓ + R_rate) := by - have h_exp_eq: (↑i + (ℓ + R_rate - i)) = ℓ + R_rate := by omega - simp only [gt_iff_lt, odd_split_index, u_b_v] - -- ⊢ ↑j + 2 ^ ↑i < 2 ^ (ℓ + R_rate) - exact Nat.add_two_pow_of_getBit_eq_zero_lt_two_pow (n:=j.val) (m:=ℓ + R_rate) - (i := i.val) (h_n:=by omega) (h_i := by omega) (h_getBit_at_i_eq_zero:=by - rw [h_b_bit_zero] at h_b_bit - exact h_b_bit.symm - ) - b j + x0 * b ⟨odd_split_index, h_lt⟩ - else -- This is the `b(u||1||v)` case - let even_split_index := u_b_v ^^^ 2^i.val - have h_lt: even_split_index < 2^(ℓ + R_rate) := by - have h_exp_eq: (↑i + (ℓ + R_rate - i)) = ℓ + R_rate := by omega - simp only [even_split_index, u_b_v] - apply Nat.xor_lt_two_pow (by omega) (by omega) - -- b j is now the odd refinement P₁,₍₁ᵥ₎⁽ⁱ⁺¹⁾(X), - -- b (j - 2^i) stores the even refinement P₀,₍₀ᵥ₎⁽ⁱ⁺¹⁾(X) - b ⟨even_split_index, h_lt⟩ + x1 * b j - -/-- The main computable Additive NTT function. `a` is the input array of coefficients. -`r` is the number of stages (dimension of the domain). The input array size must be at least 2^r. -/ -def computableAdditiveNTT (a : Fin (2 ^ ℓ) → L) : Fin (2^(ℓ + R_rate)) → L := - let b: Fin (2^(ℓ + R_rate)) → L := tileCoeffs a -- Note: can optimize on this - Fin.foldl (n:=ℓ) (f:= fun current_b i => - computableNTTStage (𝔽q := 𝔽q) (β := β) (ℓ := ℓ) (R_rate := R_rate) - (h_ℓ_add_R_rate := h_ℓ_add_R_rate) (i := ⟨ℓ - i - 1, by omega⟩) (b:=current_b) - ) (init:=b) - -/-- Array-backed coefficient tiling for the fast additive NTT path. -/ -def tileCoeffsArray (R_rate : ℕ) (a : Fin (2 ^ ℓ) → L) : Array L := - Array.ofFn (n := 2^(ℓ + R_rate)) fun v => - a ⟨v.val % (2^ℓ), Nat.mod_lt v.val (pow_pos (zero_lt_two) ℓ)⟩ - -/-- Evaluate a subspace polynomial using cached constants `W_k(β_k)`. - -Starting from `W_0(x) = x`, each cached constant advances the recurrence -`W_{k+1}(x) = W_k(x) * (W_k(x) + W_k(β_k))`. -/ -def evalWAtCachedConstantsLoop (constants : Array L) (j : Nat) (acc : L) : L := - if _h_j : j < constants.size then - let c := constants.getD j 0 - evalWAtCachedConstantsLoop constants (j + 1) (acc * (acc + c)) - else - acc -termination_by constants.size - j - -/-- Evaluate a subspace polynomial using cached constants `W_k(β_k)`. - -Starting from `W_0(x) = x`, each cached constant advances the recurrence -`W_{k+1}(x) = W_k(x) * (W_k(x) + W_k(β_k))`. -/ -def evalWAtCachedConstants (constants : Array L) (x : L) : L := - evalWAtCachedConstantsLoop constants 0 x - -/-- Precompute the constants `W_k(β_k)` needed by the recursive subspace -polynomial evaluator up to stage `i`. -/ -def subspacePolynomialConstantsArrayLoop (i : Fin r) (k : Nat) (constants : Array L) : Array L := - if h_k : k < i.val then - let constant := evalWAtCachedConstants constants (β ⟨k, by omega⟩) - subspacePolynomialConstantsArrayLoop i (k + 1) (constants.push constant) - else - constants -termination_by i.val - k - -/-- Precompute the constants `W_k(β_k)` needed by the recursive subspace -polynomial evaluator up to stage `i`. -/ -def subspacePolynomialConstantsArray (i : Fin r) : Array L := - subspacePolynomialConstantsArrayLoop (β := β) (ℓ := ℓ) (R_rate := R_rate) i 0 #[] - -/-- Precompute normalized vanishing evaluations used by one stage's twiddle factors. -/ -def computableNormalizedWValuesArray (i : Fin ℓ) : Array L := - let stage : Fin r := ⟨i, by omega⟩ - let constants := subspacePolynomialConstantsArray (β := β) (ℓ := ℓ) (R_rate := R_rate) - (i := stage) - let denominatorInv := (evalWAtCachedConstants constants (β stage))⁻¹ - Array.ofFn (n := ℓ + R_rate - i - 1) fun k => - evalWAtCachedConstants constants (β ⟨i + 1 + k.val, by omega⟩) * denominatorInv - -/-- Precompute all twiddle factors for one additive NTT stage. - -The table entry for `u` is the subset sum of the cached normalized values -selected by the set bits of `u`. -/ -def computableTwiddleTableArray (i : Fin ℓ) : Array L := - let normalizedValues := computableNormalizedWValuesArray (β := β) (ℓ := ℓ) - (R_rate := R_rate) (h_ℓ_add_R_rate := h_ℓ_add_R_rate) (i := i) - let numBits := ℓ + R_rate - i - 1 - Array.ofFn (n := 2 ^ numBits) fun u => - ∑ k : Fin numBits, - if Nat.getBit k.val u.val = 1 then normalizedValues.getD k.val 0 else 0 - -/-- Array update for one additive NTT stage. - -The `twiddles` array stores the values of `computableTwiddleFactor` for this -stage, indexed by `u`. -/ -def computableNTTStageArray (i : Fin ℓ) (twiddles : Array L) (b : Array L) : Array L := - let stride := 2^i.val - Array.ofFn (n := 2^(ℓ + R_rate)) fun j => - let u_b_v := j.val - let u_b := u_b_v / stride - let u := u_b / 2 - let b_bit := u_b % 2 - let twiddleFactor : L := twiddles.getD u 0 - let x0 := twiddleFactor - let x1 : L := x0 + 1 - if _h_b_bit_zero : b_bit = 0 then - let oddIndex := u_b_v + stride - b.getD u_b_v 0 + x0 * b.getD oddIndex 0 - else - let evenIndex := u_b_v ^^^ stride - b.getD evenIndex 0 + x1 * b.getD u_b_v 0 - -/-- Fast additive NTT stage driver over an `Array L` state. - -The state is expected to contain the initialized output buffer. Each stage -updates that buffer using the array transition from -`computableNTTStageArray`. -/ -def computableAdditiveNTTFastStages : StateM (Array L) Unit := do - let _ ← Fin.foldlM (m := StateM (Array L)) (n := ℓ) (f := fun (_ : Unit) i => do - let stage : Fin ℓ := ⟨ℓ - i - 1, by omega⟩ - let twiddles := computableTwiddleTableArray (β := β) (ℓ := ℓ) - (R_rate := R_rate) (h_ℓ_add_R_rate := h_ℓ_add_R_rate) (i := stage) - modifyThe (Array L) fun current => - computableNTTStageArray (ℓ := ℓ) (R_rate := R_rate) - (i := stage) (twiddles := twiddles) current - pure ()) (init := ()) - pure () - -/-- Fast additive NTT array producer as a state action. -/ -def computableAdditiveNTTFastAction (a : Fin (2 ^ ℓ) → L) : - StateM (Array L) (Array L) := do - set (tileCoeffsArray (ℓ := ℓ) R_rate a) - computableAdditiveNTTFastStages (β := β) (ℓ := ℓ) (R_rate := R_rate) - (h_ℓ_add_R_rate := h_ℓ_add_R_rate) - getThe (Array L) - -/-- Fast additive NTT array producer. -/ -def computableAdditiveNTTFast (a : Fin (2 ^ ℓ) → L) : Array L := - ((computableAdditiveNTTFastAction (β := β) (ℓ := ℓ) - (R_rate := R_rate) (h_ℓ_add_R_rate := h_ℓ_add_R_rate) a).run #[]).1 - -end Algorithm section ConcreteBTFieldInstances @@ -317,27 +41,31 @@ def computableBasisExplicit (k : ℕ) (i : Fin (2 ^ k)) : ConcreteBTField k := else 1 +omit [NeZero r] in +/-- The executable bit-indexed basis equals the multilinear tower basis over level zero. -/ +theorem computableBasisExplicit_eq_multilinearBasis (k : ℕ) : + computableBasisExplicit k = fun i => multilinearBasis 0 k (by omega) i := by + funext i + unfold computableBasisExplicit + rw [multilinearBasis_apply k 0 (by omega) i] + simp only [beq_iff_eq, Nat.sub_zero, 𝕏, map_pow] + congr 1 + funext x + have h_lt := Nat.getBit_lt_2 (n := i) (k := x) + by_cases h : Nat.getBit (k := x) (n := i) = 1 + · simp only [h, ↓reduceIte, pow_one] + rw! (castMode := .all) [Nat.zero_add] + rfl + · have hBit_eq_0 : Nat.getBit (k := x) (n := i) = 0 := by omega + simp only [hBit_eq_0, zero_ne_one, ↓reduceIte, pow_zero] + omit [NeZero r] in theorem hβ_lin_indep_concrete (k : ℕ) : letI := ConcreteBTFieldAlgebra (l:=0) (r:=k) (h_le:=by omega) LinearIndependent (R := ConcreteBTField 0) (v := computableBasisExplicit k) := by let := ConcreteBTFieldAlgebra (l:=0) (r:=k) (h_le:=by omega) - have h_eq : computableBasisExplicit k = fun i => multilinearBasis 0 k (by omega) i := by - funext i - unfold computableBasisExplicit - rw [multilinearBasis_apply k 0 (by omega) i] - simp only [beq_iff_eq, Nat.sub_zero, 𝕏, map_pow] - congr 1 - funext x - have h_lt := Nat.getBit_lt_2 (n := i) (k := x) - by_cases h: Nat.getBit (k := x) (n := i) = 1 - · simp only [h, ↓reduceIte, pow_one] - rw! (castMode := .all) [Nat.zero_add] - rfl - · have hBit_eq_0: Nat.getBit (k := x) (n := i) = 0 := by omega - simp only [hBit_eq_0, zero_ne_one, ↓reduceIte, pow_zero] - rw [h_eq] + rw [computableBasisExplicit_eq_multilinearBasis] exact (multilinearBasis 0 k (by omega)).linearIndependent abbrev BTF₃ := ConcreteBTField 3 -- 8 bits diff --git a/CompPoly/Fields/Binary/BF128Ghash/Prelude.lean b/CompPoly/Fields/Binary/BF128Ghash/Prelude.lean index c697ef6f..a910898b 100644 --- a/CompPoly/Fields/Binary/BF128Ghash/Prelude.lean +++ b/CompPoly/Fields/Binary/BF128Ghash/Prelude.lean @@ -40,6 +40,7 @@ export BinaryField (B128 B256 to256 to256_toNat clMul clSq toPoly clMul_unfold toPoly_one_eq_one toPoly_zero_eq_zero toPoly_ne_zero_iff_ne_zero toPoly_degree_lt_w toPoly_degree_of_lt_two_pow BitVec_lt_two_pow_of_toPoly_degree_lt toPoly_xor toPoly_fold_xor toPoly_128_extend_256 toPoly_shiftLeft_no_overflow + toPoly_one_shiftLeft toPoly_clMul gcd_eq_gcd_next_step gcd_one_zero) section GHASHPolynomial @@ -175,46 +176,6 @@ section VerificationFunctions def P_val : B256 := (1 <<< 128) ^^^ (1 <<< 7) ^^^ (1 <<< 2) ^^^ (1 <<< 1) ^^^ 1 --- Helper: toPoly (1 <<< n) is just X^n -lemma toPoly_one_shiftLeft {w : Nat} (n : Nat) (h : n < w) : - toPoly (1 <<< n : BitVec w) = X^n := by - rw [toPoly] - rw [Finset.sum_eq_single (⟨n, h⟩ : Fin w)] - -- 1. The Main Term (j = n): Prove it equals X^n - · simp only - simp only [BitVec.natCast_eq_ofNat, ite_eq_left_iff, Bool.not_eq_true] - intro h_getLsb_eq_false - have h_getLsb_eq_true : (BitVec.ofNat w (1 <<< n)).getLsb ⟨n, h⟩ = true := by - rw [BitVec.getLsb] - simp only [BitVec.toNat_ofNat, Nat.testBit_mod_two_pow, h, decide_true, Nat.testBit_shiftLeft, - ge_iff_le, le_refl, tsub_self, Nat.testBit_zero, Nat.mod_succ, Bool.and_self] - rw [h_getLsb_eq_false] at h_getLsb_eq_true - absurd h_getLsb_eq_true - exact Bool.false_ne_true - -- 2. The Other Terms (j ≠ n): Prove they are 0 - · intro b _ hb_ne_n_fin - split_ifs with h_lsb - · -- Contradiction: If bit is set, b must equal n - exfalso - have h_getLsb_eq_false : ((1 <<< n) : BitVec w).getLsb b = false := by - rw [BitVec.getLsb] - have h_lhs : ((1 <<< n) : BitVec w).toNat = 1 <<< n := by - simp only [Nat.shiftLeft_eq, one_mul, BitVec.natCast_eq_ofNat, BitVec.toNat_ofNat] - apply Nat.mod_eq_of_lt - apply Nat.pow_lt_pow_right (ha := by omega) (h := by omega) - rw [h_lhs] - rw [Nat.one_shiftLeft] - rw [Nat.testBit_two_pow]; - let h_ne := Fin.val_ne_of_ne hb_ne_n_fin - exact decide_eq_false (id (Ne.symm h_ne)) - rw [h_getLsb_eq_false] at h_lsb - absurd h_lsb - exact Bool.false_ne_true - · rfl -- If bit is not set, result is 0 - -- 3. Universe Check: Prove n is in Finset.univ - · intro h_absurd - simp at h_absurd -- Finset.univ contains everything - -- Main Proof lemma ghashPoly_eq_P_val : ghashPoly = toPoly P_val := by unfold ghashPoly P_val diff --git a/CompPoly/Fields/Binary/BF64.lean b/CompPoly/Fields/Binary/BF64.lean new file mode 100644 index 00000000..e387ab9d --- /dev/null +++ b/CompPoly/Fields/Binary/BF64.lean @@ -0,0 +1,21 @@ +/- +Copyright (c) 2026 CompPoly Contributors. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Nicolas Schleicher +-/ +module + +public import CompPoly.Fields.Binary.BF64.Basic +public import CompPoly.Fields.Binary.BF64.Reduce +public import CompPoly.Fields.Binary.BF64.Impl +public import CompPoly.Fields.Binary.BF64.Ext3 + +/-! +# `GF(2^64)` in a polynomial basis, and its cubic extension + +Facade module. It re-exports the specification-side quotient model from +`CompPoly.Fields.Binary.BF64.Basic`, the reduction from `...Reduce`, the computable +nominal word carrier from `...Impl`, and the degree-three extension from `...Ext3`. +-/ + +@[expose] public section diff --git a/CompPoly/Fields/Binary/BF64/BaseCertificate.lean b/CompPoly/Fields/Binary/BF64/BaseCertificate.lean new file mode 100644 index 00000000..c71394e3 --- /dev/null +++ b/CompPoly/Fields/Binary/BF64/BaseCertificate.lean @@ -0,0 +1,552 @@ +/- +Copyright (c) 2026 CompPoly Contributors. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Nicolas Schleicher +-/ +module + +public import CompPoly.Data.Polynomial.RabinCertificate + +/-! +# Rabin certificate data for `p = 2` + +The modulus `f` has little-endian coefficients `[1, 1, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, +0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, +0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]`. + +`d = 64` is composite, so Rabin's coprimality condition needs one certificate per +prime factor of `d` (2), at exponents p^32 respectively. Checking only the +linear-factor case would admit a product of equal-degree factors. + +GENERATED. Do not edit by hand; regenerate with: + +```sh +python3 scripts/gen_rabin_certificate.py --p 2 \ + --f '1,1,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,\ +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1' \ + --lean --namespace BF64.BaseCert \ + --authors 'Nicolas Schleicher' +``` + +Nothing here is trusted — the kernel re-checks every step through +`CompPoly.RabinCert.runChain`, so incorrect data fails to compile rather than +producing a false theorem. +-/ + +@[expose] public section + +namespace BF64.BaseCert + +open CompPoly.RabinCert + +/-- Square-and-multiply chain for `X^(p^64) mod f` (64 steps). -/ +def traceSteps : List Step := [ + ⟨false, [0], [0, 0, 1]⟩, + ⟨false, [0], [0, 0, 0, 0, 1]⟩, + ⟨false, [0], [0, 0, 0, 0, 0, 0, 0, 0, 1]⟩, + ⟨false, [0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]⟩, + ⟨false, + [0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 1]⟩, + ⟨false, [1], [1, 1, 0, 1, 1]⟩, + ⟨false, [0], [1, 0, 1, 0, 0, 0, 1, 0, 1]⟩, + ⟨false, [0], [1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1]⟩, + ⟨false, + [0], + [1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, + 0, 1]⟩, + ⟨false, + [1], + [0, 1, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]⟩, + ⟨false, + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 1], + [0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 1, 0, 1, 1]⟩, + ⟨false, [0, 0, 1, 0, 0, 0, 1, 0, 1], [0, 0, 1, 1, 1, 1, 0, 1, 1, 0, 1, 1, 0, 0, 0, 0, 1]⟩, + ⟨false, + [0], + [0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 1]⟩, + ⟨false, + [1], + [1, 1, 0, 1, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, + 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1]⟩, + ⟨false, + [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1], + [0, 1, 1, 1, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 0, 0, 0, 0, 1, 0, 1, 1, 0, 0, + 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]⟩, + ⟨false, + [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1], + [1, 1, 1, 1, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 1, 0, 1]⟩, + ⟨false, + [0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1], + [1, 0, 1, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 0, 1, 1, 1, 0, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, + 0, 1, 1, 0, 0, 0, 0, 1, 0, 1, 0, 1, 1, 0, 1, 0, 1, 0, 1, 0, 1, 1]⟩, + ⟨false, + [1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, + 0, 0, 0, 1, 0, 0, 0, 1, 0, 1], + [0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 1, 0, 0, 1, 1, 1, 1, 1, 0, 1, 0, 1, 1, 0, 1, 1, 1, 1, 0, 1, 1, + 1, 1, 1, 1, 1, 0, 1, 1, 1, 0, 0, 1, 1, 1, 0, 0, 0, 1]⟩, + ⟨false, + [1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, + 0, 1], + [1, 1, 0, 0, 1, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 1, 0, 0, 1, + 0, 0, 1, 1, 1, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, + 1]⟩, + ⟨false, + [1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, + 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1], + [0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 0, 1, 0, 1, 0, 0, 1, 0, 0, 1, 1, 1, 0, 1, 0, 1, 1, 0, 0, + 0, 0, 1, 1, 1, 1, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 1, 1, 0, 0, 0, 1, 1, 1, 1, 0, 1, 0, 1, 1, 1, + 0, 1]⟩, + ⟨false, + [0, 1, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, + 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, + 1], + [0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 0, 1, 1, 1, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 1, 1, 1, + 1, 1, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0, 1, 0, 1, 0, 1, 1, 1, 1, 0, 1, 1, 1, 0, 0, 0, 0, 1, 1, + 0, 1]⟩, + ⟨false, + [1, 1, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, + 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, + 1], + [1, 0, 1, 0, 1, 1, 0, 0, 1, 1, 1, 0, 1, 0, 0, 0, 1, 1, 1, 0, 1, 1, 1, 0, 0, 1, 1, 1, 0, 1, 1, + 1, 1, 1, 0, 0, 0, 0, 1, 0, 1, 1, 0, 1, 0, 0, 0, 0, 1, 1, 1, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 1, + 1, 1]⟩, + ⟨false, + [0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, + 1], + [1, 1, 1, 0, 0, 1, 0, 0, 1, 0, 1, 0, 1, 1, 0, 1, 1, 1, 0, 0, 0, 1, 0, 1, 1, 1, 1, 0, 0, 0, 0, + 0, 0, 1, 0, 0, 1, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, + 1]⟩, + ⟨false, + [1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1], + [0, 1, 0, 0, 0, 1, 1, 0, 1, 1, 1, 1, 0, 1, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, + 0, 1, 0, 1, 0, 0, 0, 1, 1, 1, 0, 1, 0, 1, 1, 0, 0, 1, 0, 1, 0, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, + 0, 1]⟩, + ⟨false, + [1, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, + 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, + 1], + [1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 1, 0, 0, 1, 1, 0, 1, 0, 0, 1, 1, 1, 1, 1, 0, 1, 0, 0, 0, 1, 1, + 0, 0, 1, 0, 1, 0, 1, 0, 1, 1, 1, 0, 1, 0, 1, 1, 0, 1, 1, 1, 1, 1, 0, 1, 1, 0, 1, 1, 0, 0, 1, + 0, 1]⟩, + ⟨false, + [0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, + 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, + 1], + [1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 1, 1, 0, 0, 1, + 1, 1, 1, 1, 0, 0, 0, 1, 0, 1, 0, 0, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 0, 1, 1, 1, 0, 0, 1, 1, 1, + 0, 1]⟩, + ⟨false, + [1, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, + 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, + 1], + [0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, + 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, + 1, 1]⟩, + ⟨false, + [0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, + 1], + [0, 1, 1, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 1, 1, 0, 1, 1, 1, 1, 1, 0, + 0, 1, 0, 1, 0, 1, 0, 0, 1, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1]⟩, + ⟨false, + [1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, + 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1], + [1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 1, 1, 0, 1, 1, 0, 1, 1, 1, 0, 0, 0, 1, 0, 1, + 1, 0, 0, 1, 1, 0, 1, 1, 0, 0, 1, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 1, 0, 0, 0, 1, 1]⟩, + ⟨false, + [0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, + 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1], + [1, 0, 1, 0, 0, 1, 0, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 0, 1, 0, 1, 0, 1, 1, + 1, 0, 0, 1, 0, 1, 1, 1, 1, 1, 1, 0, 1, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, + 1]⟩, + ⟨false, + [1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, + 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1], + [0, 1, 0, 1, 1, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, + 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 1, + 1, 1]⟩, + ⟨false, + [1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, + 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, + 1], + [1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 1, 1, 1, + 0, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0, 0, 1, 1, 1, 0, + 1]⟩, + ⟨false, + [0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, + 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1], + [1, 0, 1, 1, 0, 0, 1, 1, 1, 0, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 0, 0, 1, 1, 0, 1, 0, + 0, 0, 0, 0, 1, 0, 1, 1, 0, 0, 0, 1, 1, 1, 0, 1, 1, 0, 0, 1, 0, 0, 1, 1, 0, 1, 0, 0, 1, 1, 1, + 0, 1]⟩, + ⟨false, + [0, 1, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 1, + 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, + 1], + [1, 1, 0, 1, 0, 0, 1, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 1, 1, 1, 1, 0, + 0, 0, 1, 0, 0, 0, 1, 0, 1, 1, 0, 0, 1, 0, 0, 1, 1, 0, 1, 0, 1, 0, 0, 0, 1, 1, 0, 1, 0, 1, 1, + 0, 1]⟩, + ⟨false, + [0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, + 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, + 1], + [1, 1, 0, 0, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 0, 1, 0, 1, 1, 0, 1, 1, 0, + 0, 1, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, + 0, 1]⟩, + ⟨false, + [1, 1, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, + 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, + 1], + [0, 0, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 0, 0, 1, 0, 1, 0, 0, 1, 0, + 0, 0, 1, 1, 0, 1, 1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 0, 1, 1, + 0, 1]⟩, + ⟨false, + [0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, + 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, + 1], + [0, 1, 1, 0, 0, 0, 1, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 1, 0, 0, 1, 0, 1, 0, 1, 0, 1, 1, 1, + 1, 1, 0, 1, 0, 1, 1, 1, 1, 0, 0, 0, 1, 1, 0, 1, 1, 0, 0, 1, 1, 1, 1, 1, 0, 1, 1, 0, 0, 1, 1, + 0, 1]⟩, + ⟨false, + [1, 1, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 1, + 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, + 1], + [1, 0, 1, 0, 0, 1, 1, 1, 0, 1, 1, 0, 1, 0, 1, 0, 1, 1, 0, 0, 0, 0, 1, 1, 1, 0, 0, 1, 0, 1, 1, + 0, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 0, 1, 0, 0, 0, 1, + 1, 1]⟩, + ⟨false, + [0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, + 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, + 1], + [1, 1, 1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0, 1, 1, 1, 1, + 0, 1, 0, 1, 0, 0, 0, 1, 1, 1, 0, 1, 1, 0, 0, 0, 1, 0, 0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 1]⟩, + ⟨false, + [1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, + 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1], + [0, 1, 1, 1, 1, 1, 0, 1, 1, 0, 1, 0, 1, 1, 1, 0, 0, 0, 1, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, + 1, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 0, 1, 0, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 1, 0, 0, 1]⟩, + ⟨false, + [0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, + 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1], + [0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0, 1, 0, 0, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1, + 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 1, 0, 0, 0, 1, 1, 1, 0, 1, 0, 1, 1, 1, 0, 1, 1, 0, 1, 0, 0, + 1]⟩, + ⟨false, + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, + 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1], + [0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 1, 1, 0, 1, 0, 0, 0, 1, 1, 0, 1, 1, 1, 0, 0, 0, 0, 1, 1, 0, + 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 1, 1, 0, 1, 1, 0, 0, 1, + 1, 1]⟩, + ⟨false, + [1, 1, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, + 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, + 1], + [1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 1, + 1, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 0, 0, 1, 1]⟩, + ⟨false, + [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, + 0, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1], + [0, 1, 0, 1, 1, 0, 0, 0, 1, 0, 0, 1, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 1, 1, 0, + 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 0, 1, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, + 1]⟩, + ⟨false, + [1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, + 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1], + [1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 1, 1, 0, 1, 0, 0, 1, 1, 1, 0, 0, 1, 0, 1, 1, 1, 0, 0, 1, + 1, 1, 0, 1, 1, 1, 0, 1, 1, 1, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0, 1, 0, 0, 1, 1, + 1, 1]⟩, + ⟨false, + [0, 1, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, + 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, + 1], + [1, 1, 1, 1, 0, 1, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1, 0, 1, 0, + 0, 0, 0, 1, 1, 1, 1, 0, 0, 1, 0, 1, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 1, 0, 0, 1, 1, 1, 0, 1, 0, + 1]⟩, + ⟨false, + [1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, + 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1], + [0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 0, 1, 1, 1, 0, 1, 1, 0, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 0, 1, + 1, 0, 0, 1, 1, 1, 0, 0, 1, 1, 0, 0, 0, 1, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 1, 0, 1, 1, 1, 0, 1, + 0, 1]⟩, + ⟨false, + [0, 1, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, + 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1], + [0, 1, 1, 1, 1, 1, 1, 0, 1, 0, 0, 1, 1, 0, 1, 1, 1, 0, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 1, + 1, 0, 0, 0, 1, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 1, 0, 1, 0, 1, 1, 0, 1, 0, 0, 1, 0, 1, 1, 1, + 1, 1]⟩, + ⟨false, + [1, 1, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, + 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, + 1], + [1, 0, 1, 0, 1, 0, 1, 1, 0, 0, 0, 1, 0, 0, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 0, + 0, 0, 1, 0, 1, 1, 1, 1, 1, 1, 0, 1, 1, 0, 1, 0, 1, 1, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0, + 1]⟩, + ⟨false, + [1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, + 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1], + [0, 1, 1, 0, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 1, 0, 1, 0, 0, 1, 1, 0, 1, 1, 1, 1, + 1, 0, 0, 0, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 1, + 0, 1]⟩, + ⟨false, + [0, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, + 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 1], + [0, 1, 1, 1, 0, 0, 0, 1, 0, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, + 0, 1, 1, 0, 1, 0, 0, 0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1, 1, 1, 1, 1, + 1, 1]⟩, + ⟨false, + [0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, + 1], + [0, 1, 0, 0, 0, 1, 0, 1, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0, 1, 0, 1, 0, 1, 0, 0, 1, 0, 0, 0, 0, + 0, 0, 0, 1, 1, 0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0, 1, 1, 1, 0, 0, 0, 1]⟩, + ⟨false, + [0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, + 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1], + [0, 0, 1, 0, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 1, 0, 1, 0, 0, 0, 1, 1, 1, 0, 1, 1, 1, + 1, 1, 1, 1, 0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0, 1, 1, 0, 1]⟩, + ⟨false, + [1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, + 0, 1, 0, 1, 0, 0, 0, 1], + [1, 1, 1, 0, 1, 0, 1, 1, 1, 1, 0, 0, 0, 1, 1, 0, 1, 0, 1, 0, 1, 0, 0, 1, 0, 0, 1, 1, 0, 1, 1, + 1, 1, 1, 1, 0, 0, 1, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0, + 1]⟩, + ⟨false, + [0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1], + [1, 0, 0, 1, 0, 0, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 1, 0, 0, 0, 1, 1, 1, 0, 1, 0, 1, 0, 0, + 0, 0, 1, 0, 1, 1, 1, 0, 1, 1, 1, 0, 1, 1, 0, 1, 0, 1, 1, 0, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, + 1, 1]⟩, + ⟨false, + [1, 1, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, + 0, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, + 1], + [0, 0, 1, 1, 0, 1, 0, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, + 1, 0, 1, 1, 0, 0, 1, 0, 1, 1, 0, 0, 0, 1, 0, 1, 0, 0, 1, 0, 1, 0, 0, 0, 0, 1]⟩, + ⟨false, + [0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, + 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1], + [0, 0, 1, 1, 0, 0, 0, 1, 1, 0, 0, 1, 0, 1, 1, 1, 0, 0, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, + 1, 0, 0, 1, 1, 0, 1, 0, 1, 0, 1, 0, 0, 1, 0, 1, 0, 1, 1, 0, 1, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, + 1]⟩, + ⟨false, + [1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, + 0, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1], + [1, 1, 0, 1, 1, 1, 0, 0, 1, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, + 1, 0, 1, 1, 0, 0, 1, 1, 1, 0, 0, 1, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1]⟩, + ⟨false, + [1, 1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, + 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, + 1], + [0, 0, 0, 1, 1, 0, 1, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, + 1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, + 1]⟩, + ⟨false, + [1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1], + [1, 1, 0, 1, 0, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, + 0, 1, 1, 1, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1]⟩, + ⟨false, + [0, 1, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, + 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, + 1], + [1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, + 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1]⟩, + ⟨false, + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1], + [1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, + 1]⟩, + ⟨false, + [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1], + [0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1]⟩, + ⟨false, + [0, 1, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, + 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, + 1], + [0, 1]⟩] + +/-! ### Coprimality with `X^(p^32) - X`, for the prime factor `2` of `d = 64`. -/ + +/-- Square-and-multiply chain for `X^(p^32) mod f` (32 steps). -/ +def cop32Steps : List Step := [ + ⟨false, [0], [0, 0, 1]⟩, + ⟨false, [0], [0, 0, 0, 0, 1]⟩, + ⟨false, [0], [0, 0, 0, 0, 0, 0, 0, 0, 1]⟩, + ⟨false, [0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]⟩, + ⟨false, + [0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 1]⟩, + ⟨false, [1], [1, 1, 0, 1, 1]⟩, + ⟨false, [0], [1, 0, 1, 0, 0, 0, 1, 0, 1]⟩, + ⟨false, [0], [1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1]⟩, + ⟨false, + [0], + [1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, + 0, 1]⟩, + ⟨false, + [1], + [0, 1, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]⟩, + ⟨false, + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 1], + [0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 1, 0, 1, 1]⟩, + ⟨false, [0, 0, 1, 0, 0, 0, 1, 0, 1], [0, 0, 1, 1, 1, 1, 0, 1, 1, 0, 1, 1, 0, 0, 0, 0, 1]⟩, + ⟨false, + [0], + [0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 1]⟩, + ⟨false, + [1], + [1, 1, 0, 1, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, + 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1]⟩, + ⟨false, + [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1], + [0, 1, 1, 1, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 0, 0, 0, 0, 1, 0, 1, 1, 0, 0, + 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]⟩, + ⟨false, + [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1], + [1, 1, 1, 1, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 1, 0, 1]⟩, + ⟨false, + [0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1], + [1, 0, 1, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 0, 1, 1, 1, 0, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, + 0, 1, 1, 0, 0, 0, 0, 1, 0, 1, 0, 1, 1, 0, 1, 0, 1, 0, 1, 0, 1, 1]⟩, + ⟨false, + [1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, + 0, 0, 0, 1, 0, 0, 0, 1, 0, 1], + [0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 1, 0, 0, 1, 1, 1, 1, 1, 0, 1, 0, 1, 1, 0, 1, 1, 1, 1, 0, 1, 1, + 1, 1, 1, 1, 1, 0, 1, 1, 1, 0, 0, 1, 1, 1, 0, 0, 0, 1]⟩, + ⟨false, + [1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, + 0, 1], + [1, 1, 0, 0, 1, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 1, 0, 0, 1, + 0, 0, 1, 1, 1, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, + 1]⟩, + ⟨false, + [1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, + 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1], + [0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 0, 1, 0, 1, 0, 0, 1, 0, 0, 1, 1, 1, 0, 1, 0, 1, 1, 0, 0, + 0, 0, 1, 1, 1, 1, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 1, 1, 0, 0, 0, 1, 1, 1, 1, 0, 1, 0, 1, 1, 1, + 0, 1]⟩, + ⟨false, + [0, 1, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, + 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, + 1], + [0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 0, 1, 1, 1, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 1, 1, 1, + 1, 1, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0, 1, 0, 1, 0, 1, 1, 1, 1, 0, 1, 1, 1, 0, 0, 0, 0, 1, 1, + 0, 1]⟩, + ⟨false, + [1, 1, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, + 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, + 1], + [1, 0, 1, 0, 1, 1, 0, 0, 1, 1, 1, 0, 1, 0, 0, 0, 1, 1, 1, 0, 1, 1, 1, 0, 0, 1, 1, 1, 0, 1, 1, + 1, 1, 1, 0, 0, 0, 0, 1, 0, 1, 1, 0, 1, 0, 0, 0, 0, 1, 1, 1, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 1, + 1, 1]⟩, + ⟨false, + [0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, + 1], + [1, 1, 1, 0, 0, 1, 0, 0, 1, 0, 1, 0, 1, 1, 0, 1, 1, 1, 0, 0, 0, 1, 0, 1, 1, 1, 1, 0, 0, 0, 0, + 0, 0, 1, 0, 0, 1, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, + 1]⟩, + ⟨false, + [1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1], + [0, 1, 0, 0, 0, 1, 1, 0, 1, 1, 1, 1, 0, 1, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, + 0, 1, 0, 1, 0, 0, 0, 1, 1, 1, 0, 1, 0, 1, 1, 0, 0, 1, 0, 1, 0, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, + 0, 1]⟩, + ⟨false, + [1, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, + 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, + 1], + [1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 1, 0, 0, 1, 1, 0, 1, 0, 0, 1, 1, 1, 1, 1, 0, 1, 0, 0, 0, 1, 1, + 0, 0, 1, 0, 1, 0, 1, 0, 1, 1, 1, 0, 1, 0, 1, 1, 0, 1, 1, 1, 1, 1, 0, 1, 1, 0, 1, 1, 0, 0, 1, + 0, 1]⟩, + ⟨false, + [0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, + 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, + 1], + [1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 1, 1, 0, 0, 1, + 1, 1, 1, 1, 0, 0, 0, 1, 0, 1, 0, 0, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 0, 1, 1, 1, 0, 0, 1, 1, 1, + 0, 1]⟩, + ⟨false, + [1, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, + 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, + 1], + [0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, + 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, + 1, 1]⟩, + ⟨false, + [0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, + 1], + [0, 1, 1, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 1, 1, 0, 1, 1, 1, 1, 1, 0, + 0, 1, 0, 1, 0, 1, 0, 0, 1, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1]⟩, + ⟨false, + [1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, + 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1], + [1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 1, 1, 0, 1, 1, 0, 1, 1, 1, 0, 0, 0, 1, 0, 1, + 1, 0, 0, 1, 1, 0, 1, 1, 0, 0, 1, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 1, 0, 0, 0, 1, 1]⟩, + ⟨false, + [0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, + 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1], + [1, 0, 1, 0, 0, 1, 0, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 0, 1, 0, 1, 0, 1, 1, + 1, 0, 0, 1, 0, 1, 1, 1, 1, 1, 1, 0, 1, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, + 1]⟩, + ⟨false, + [1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, + 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1], + [0, 1, 0, 1, 1, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, + 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 1, + 1, 1]⟩, + ⟨false, + [1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, + 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, + 1], + [1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 1, 1, 1, + 0, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0, 0, 1, 1, 1, 0, + 1]⟩] + +/-- The residue `X^(p^32) mod f`. -/ +def cop32Rp : List ℕ := [1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, + 0, 1, 1, 1, 0, 1, 1, 1, 0, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 0, 0, 0, 1, + 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0, 0, 1, 1, 1, 0, 1] + +/-- `cop32W = (X^(p^32) mod f) - X`, the reduced form of `X^(p^32) - X`. -/ +def cop32W : List ℕ := [1, 1, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, + 0, 1, 1, 1, 0, 1, 1, 1, 0, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 0, 0, 0, 1, + 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0, 0, 1, 1, 1, 0, 1] + +/-- Bézout coefficient: `cop32U·f + cop32V·cop32W = 1`. -/ +def cop32U : List ℕ := [0, 0, 1, 1, 0, 1, 0, 1, 0, 0, 0, 1, 1, 0, 0, 1, 0, 1, 1, 0, 1, 1, 1, + 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 1, 1, 0, 0, 1, 0, 0, 0, 0, + 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1] + +/-- Bézout coefficient: `cop32U·f + cop32V·cop32W = 1`. -/ +def cop32V : List ℕ := [1, 1, 1, 0, 0, 0, 1, 1, 1, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 1, 1, 0, 1, + 0, 1, 1, 1, 1, 1, 1, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0, 0, 1, 0, + 1, 1, 0, 1, 0, 1, 0, 0, 1, 1, 1, 1, 0, 0, 1, 0, 1] + +end BF64.BaseCert diff --git a/CompPoly/Fields/Binary/BF64/Basic.lean b/CompPoly/Fields/Binary/BF64/Basic.lean new file mode 100644 index 00000000..194e6126 --- /dev/null +++ b/CompPoly/Fields/Binary/BF64/Basic.lean @@ -0,0 +1,200 @@ +/- +Copyright (c) 2026 CompPoly Contributors. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Nicolas Schleicher +-/ +module + +public import CompPoly.Fields.Binary.BF64.BaseCertificate +public import CompPoly.Data.Polynomial.Rabin +public import CompPoly.Data.RingTheory.CanonicalEuclideanDomain +public import Mathlib.Data.ZMod.Basic +public import Mathlib.RingTheory.AdjoinRoot +public import Mathlib.Tactic.ComputeDegree + +/-! +# `GF(2^64)` in a polynomial basis + +The field + +```text +GF(2)[x] / (x^64 + x^4 + x^3 + x + 1) +``` + +as a *flat* quotient by a single irreducible degree-64 polynomial, with an element +represented by the 64-bit word whose bit `i` is the coefficient of `x^i`. The modulus is +the standard low-weight pentanomial for this width, and its tail `x^4 + x^3 + x + 1` +(the constant `0x1B`) is what makes the reduction in `BF64/Reduce.lean` cheap. + +This is a different presentation from the binary tower in +`CompPoly/Fields/Binary/Tower/`, which builds `GF(2^64)` as an iterated quadratic +extension. The two fields are abstractly isomorphic but use different bases, so their +bit-level encodings disagree: on the same bit patterns `2 * 3` is `6` here and `1` in the +tower's rung. Neither is a substitute for the other where the encoding is observable. + +## Main definitions + +* `basePoly` — the modulus, as a `Polynomial (ZMod 2)`. Specification only. +* `BF64Quot` — the field, as `AdjoinRoot basePoly`. +* `baseTail` — the modulus below its leading term, `x^4 + x^3 + x + 1`. + +## Main statements + +* `basePoly_irreducible` — irreducibility, by Rabin's test against the kernel-checked + chains in `BF64.BaseCert`. +* `mul_pow_reduce` — the reduction identity `x^64 ≡ x^4 + x^3 + x + 1`. +* `card_bf64Quot` — `Fintype.card BF64Quot = 2 ^ 64`. + +## Implementation notes + +`basePoly` is `noncomputable` because Mathlib's `Polynomial` is a `Finsupp`, which has no +executable representation. It exists to state irreducibility and is never evaluated. +`CompPoly.Extension.ExtensionParams.poly` makes the same choice. + +`BF64Quot` is the *quotient* presentation, used for cardinality and as the target of the +bridge. The computable presentation that arithmetic runs on is `BF64` in +`CompPoly/Fields/Binary/BF64/Impl.lean`, a nominal type storing a `BitVec 64`; +the two are related by +`BF64.toQuot`, which `BF64.toQuot_injective` and `BF64.toQuot_surjective` show is a +bijection. + +## References + +* [Rabin80] Michael O. Rabin. Probabilistic Algorithms in Finite Fields. + SIAM Journal on Computing, 9(2):273-280, 1980. https://doi.org/10.1137/0209024 +-/ + +@[expose] public section + +namespace BF64 + +open Polynomial CompPoly.RabinCert BF64.BaseCert + +set_option maxHeartbeats 400000 +set_option maxRecDepth 4000 + +/-- Little-endian coefficients of `x^64 + x^4 + x^3 + x + 1`: the terms of degree +`0, 1, 3, 4` and `64`. -/ +def baseCoeffs : List ℕ := [1, 1, 0, 1, 1] ++ List.replicate 59 0 ++ [1] + +/-- The modulus `x^64 + x^4 + x^3 + x + 1` over `GF(2)`. Part of the specification only; +it is never evaluated. -/ +noncomputable def basePoly : Polynomial (ZMod 2) := X ^ 64 + X ^ 4 + X ^ 3 + X + 1 + +/-- A run of zero coefficients shifts the rest of the list up by that many degrees. -/ +theorem toPoly_replicate_zero {p : ℕ} (n : ℕ) (rest : List ℕ) : + toPoly p (List.replicate n 0 ++ rest) = X ^ n * toPoly p rest := by + induction n with + | zero => simp + | succ k ih => + rw [List.replicate_succ, List.cons_append, toPoly_cons, ih] + simp [pow_succ] + ring + +/-- The certificate's coefficient encoding denotes `basePoly`. -/ +theorem toPoly_baseCoeffs : toPoly 2 baseCoeffs = basePoly := by + show toPoly 2 ([1, 1, 0, 1, 1] ++ (List.replicate 59 0 ++ [1])) = basePoly + simp only [List.cons_append, List.nil_append, toPoly_cons, toPoly_replicate_zero, toPoly_nil, + Nat.cast_zero, Nat.cast_one, map_zero, map_one, basePoly] + ring + +/-- The modulus has degree `64`: the leading `X ^ 64` dominates the tail. -/ +theorem basePoly_natDegree : basePoly.natDegree = 64 := by + rw [basePoly]; compute_degree! + +/-- `basePoly_natDegree` in `degree` form, as the division lemmas expect. -/ +theorem basePoly_degree : basePoly.degree = (64 : ℕ) := by + rw [basePoly]; compute_degree! + +/-- The modulus is nonzero, so it can be divided by. -/ +theorem basePoly_ne_zero : basePoly ≠ 0 := by + intro h + have hd := basePoly_natDegree + rw [h, natDegree_zero] at hd + exact absurd hd (by norm_num) + +/-- The prime factors of `64`. `decide` cannot do this: `Nat.primeFactorsList` is +well-founded recursive and does not reduce in the kernel. -/ +private theorem primeFactors_sixtyFour : (64 : ℕ).primeFactors = {2} := by + rw [show (64 : ℕ) = 2 ^ 6 from by norm_num, + Nat.primeFactors_prime_pow (by norm_num) Nat.prime_two] + +/-- +`x^64 + x^4 + x^3 + x + 1` is irreducible over `GF(2)`, by Rabin's test against the +kernel-checked chains in `BF64.BaseCert`. + +Degree `64` has the single prime factor `2`, so the trace condition is joined by one +coprimality check, at exponent `2^32`. Note that the collapsed +`irreducible_of_rabin_prime_degree` is *unsound* at this degree — it would accept a +product of equal-degree factors — so the general `Polynomial.irreducible_of_rabin` is +used, with `primeFactors_sixtyFour` supplying the prime factors of `64`. +-/ +theorem basePoly_irreducible : Irreducible basePoly := by + refine Polynomial.irreducible_of_rabin (d := 64) ?_ (by norm_num) ?_ ?_ + · exact basePoly_natDegree + · rw [ZMod.card] + exact dvd_X_pow_sub_X_of_runChain (steps := traceSteps) toPoly_baseCoeffs basePoly_ne_zero + (by rfl) (by rfl) + · intro ℓ hℓ + rw [primeFactors_sixtyFour, Finset.mem_singleton] at hℓ + subst hℓ + rw [ZMod.card] + exact isCoprime_X_pow_sub_X_of_runChain (steps := cop32Steps) (rp := cop32Rp) + (w := cop32W) (u := cop32U) (v := cop32V) toPoly_baseCoeffs basePoly_ne_zero + (by rfl) (by rfl) (by rfl) (by rfl) + +instance : Fact (Irreducible basePoly) := ⟨basePoly_irreducible⟩ + +/-! ## The reduction identity -/ + +/-- The modulus below its leading term: `x^4 + x^3 + x + 1`, denoted by the reduction +constant `0x1B`. -/ +noncomputable def baseTail : Polynomial (ZMod 2) := X ^ 4 + X ^ 3 + X + 1 + +/-- The defining equation of `baseTail`, for rewriting. -/ +theorem baseTail_eq : baseTail = X ^ 4 + X ^ 3 + X + 1 := rfl + +/-- The defining equation of `basePoly`, for rewriting. -/ +theorem basePoly_eq : basePoly = X ^ 64 + X ^ 4 + X ^ 3 + X + 1 := rfl + +/-- The modulus split into its leading term and its tail. -/ +theorem basePoly_eq_add_tail : basePoly = X ^ 64 + baseTail := by + unfold basePoly baseTail; ring + +/-- `x^64 ≡ x^4 + x^3 + x + 1` modulo the modulus: multiplying by `X ^ 64` may be +replaced by multiplying by `baseTail`. -/ +theorem mul_pow_reduce (A : Polynomial (ZMod 2)) : + (A * X ^ 64) % basePoly = (A * baseTail) % basePoly := by + have hadd : (X : (ZMod 2)[X]) ^ 64 = basePoly + baseTail := by + rw [basePoly_eq_add_tail, add_assoc, CharTwo.add_self_eq_zero, add_zero] + rw [hadd, mul_add, show A * basePoly + A * baseTail = A * baseTail + basePoly * A from by ring, + CanonicalEuclideanDomain.add_mul_mod_right _ _ _ basePoly_ne_zero] + +/-! ## The base field -/ + +/-- `GF(2^64)` as the quotient `GF(2)[x] / (x^64 + x^4 + x^3 + x + 1)`. + +This is the specification-side presentation. Arithmetic runs on the computable `BF64` in +`CompPoly/Fields/Binary/BF64/Impl.lean`. -/ +noncomputable abbrev BF64Quot : Type := AdjoinRoot basePoly + +noncomputable instance : Field BF64Quot := AdjoinRoot.instField + +noncomputable instance : Fintype BF64Quot := by + let pb := AdjoinRoot.powerBasis basePoly_ne_zero + letI : Module.Finite (ZMod 2) BF64Quot := PowerBasis.finite pb + haveI : Finite BF64Quot := by + have : Module.finrank (ZMod 2) BF64Quot = pb.dim := PowerBasis.finrank pb + exact Finite.of_equiv (Fin pb.dim →₀ ZMod 2) (pb.basis.repr.toEquiv.symm) + exact Fintype.ofFinite BF64Quot + +/-- `BF64Quot` has `2^64` elements. -/ +theorem card_bf64Quot : Fintype.card BF64Quot = 2 ^ 64 := by + rw [Module.card_eq_pow_finrank (K := ZMod 2) (V := BF64Quot)] + let pb := AdjoinRoot.powerBasis basePoly_ne_zero + rw [PowerBasis.finrank pb] + have hdim : pb.dim = basePoly.natDegree := rfl + rw [hdim, basePoly_natDegree] + norm_num + +end BF64 diff --git a/CompPoly/Fields/Binary/BF64/Ext3.lean b/CompPoly/Fields/Binary/BF64/Ext3.lean new file mode 100644 index 00000000..9fc101bc --- /dev/null +++ b/CompPoly/Fields/Binary/BF64/Ext3.lean @@ -0,0 +1,210 @@ +/- +Copyright (c) 2026 CompPoly Contributors. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Nicolas Schleicher +-/ +module + +public import CompPoly.Fields.Binary.BF64.Impl +public import CompPoly.Fields.Extension +public import Mathlib.Algebra.Polynomial.SpecificDegree + +/-! +# The degree-3 extension of `GF(2^64)` + +```text +GF(2^64)[y] / (y^3 + y + 1), giving GF(2^192) +``` + +An element is `c0 + c1 * y + c2 * y^2` with each coefficient in `BF64`. The carrier comes +from the computable extension framework in `CompPoly/Fields/Extension/`. Its coefficient +vector is `Vector BF64 3`, with the three limbs ordered as `c0`, `c1`, and `c2`. +`Ext.coeffs` and `Ext.ofVector` give inverse maps between elements and these coordinates. + +Irreducibility needs no certificate here, unlike the degree-64 base modulus: a cubic is +irreducible exactly when it has no root, and a short characteristic-two argument rules one +out. See `CompPoly/Fields/KoalaBear/Ext5.lean` for the general monic-modulus pattern this +follows. + +## Main definitions + +* `ext3Poly` — the modulus `y^3 + y + 1` over `BF64`. +* `ext3Params` — its `CompPoly.Extension.ExtensionParams`. +* `Ext3` — the extension field itself. + +## Main statements + +* `ext3Poly_irreducible` — the cubic is irreducible over `BF64`. +* `ext3Params_poly` — the coefficient vector `#v[1, 1, 0]` denotes that cubic. +* `nat_card_ext3` — `Nat.card Ext3 = 2 ^ 192`. + +## Implementation notes + +`ext3Params_poly` is load-bearing: it ties the coefficient vector `#v[1, 1, 0]` to +`y ^ 3 + y + 1`. A wrong vector there would still compile and would silently give a +different field, so it is checked against reference vectors in +`tests/CompPolyTests/Fields/Binary/BF64.lean` rather than only re-derived. +-/ + +@[expose] public section + +namespace BF64 + +open Polynomial CompPoly.Extension + +/-! ## The defining cubic -/ + +/-- The extension modulus `y^3 + y + 1` over `GF(2^64)`. -/ +noncomputable def ext3Poly : Polynomial BF64 := X ^ 3 + X + 1 + +/-- The cubic has degree `3`. -/ +theorem ext3Poly_natDegree : ext3Poly.natDegree = 3 := by + rw [ext3Poly]; compute_degree! + +/-- `ext3Poly_natDegree` in `degree` form. -/ +theorem ext3Poly_degree : ext3Poly.degree = (3 : ℕ) := by + rw [ext3Poly]; compute_degree! + +/-- The cubic is monic, as the extension framework requires. -/ +theorem ext3Poly_monic : ext3Poly.Monic := by + rw [ext3Poly]; monicity! + +/-! ## Irreducibility + +A cubic is irreducible exactly when it has no root. A root `a` of `y^3 + y + 1` satisfies +`a^3 = a + 1`, from which `a^7 = 1`. The multiplicative group of `GF(2^64)` has order +`2^64 - 1`, which is coprime to `7`, so `a = 1` — and `1` is not a root. +-/ + +/-- A root of the cubic would have multiplicative order dividing `7`. -/ +private theorem pow_seven_of_isRoot {a : BF64} (h : ext3Poly.IsRoot a) : a ^ 7 = 1 := by + have h3 : a ^ 3 = a + 1 := by + have := h + rw [ext3Poly, Polynomial.IsRoot, Polynomial.eval_add, Polynomial.eval_add, + Polynomial.eval_pow, Polynomial.eval_X, Polynomial.eval_one] at this + rw [← sub_eq_zero, CharTwo.sub_eq_add, + show a ^ 3 + (a + 1) = a ^ 3 + a + 1 from by ring] + exact this + calc a ^ 7 = (a ^ 3) ^ 2 * a := by ring + _ = (a + 1) ^ 2 * a := by rw [h3] + _ = (a ^ 2 + 1) * a := by rw [CharTwo.add_sq, one_pow] + _ = a ^ 3 + a := by ring + _ = (a + 1) + a := by rw [h3] + _ = 1 := by rw [add_comm a 1, add_assoc, CharTwo.add_self_eq_zero, add_zero] + +/-- The cubic has no root in `GF(2^64)`. + +A root has `a ^ 7 = 1`, so its multiplicative order divides `7`. It also divides +`Fintype.card BF64 - 1 = 2 ^ 64 - 1`, which is coprime to `7`, so the order is `1` and +`a = 1`. But `1` is not a root. +-/ +theorem ext3Poly_no_root (a : BF64) : ¬ext3Poly.IsRoot a := by + let := Fintype.ofFinite BF64 + intro h + have h7 := pow_seven_of_isRoot h + have ha : a ≠ 0 := by + intro h0 + rw [h0, zero_pow (by norm_num)] at h7 + exact zero_ne_one h7 + -- the order divides 7 and divides the group order + have hdvd7 : orderOf a ∣ 7 := orderOf_dvd_of_pow_eq_one h7 + have hdvdcard : orderOf a ∣ 2 ^ 64 - 1 := by + have hc : a ^ (Fintype.card BF64 - 1) = 1 := FiniteField.pow_card_sub_one_eq_one a ha + rw [card_bf64] at hc + exact orderOf_dvd_of_pow_eq_one hc + have hcop : Nat.Coprime 7 (2 ^ 64 - 1) := by decide +kernel + have h1 : orderOf a = 1 := Nat.eq_one_of_dvd_coprimes hcop hdvd7 hdvdcard + have : a = 1 := orderOf_eq_one_iff.mp h1 + -- but 1 is not a root + rw [this] at h + rw [ext3Poly, Polynomial.IsRoot, Polynomial.eval_add, Polynomial.eval_add, + Polynomial.eval_pow, Polynomial.eval_X, Polynomial.eval_one, one_pow] at h + rw [show (1 : BF64) + 1 + 1 = 1 from by + rw [CharTwo.add_self_eq_zero, zero_add]] at h + exact one_ne_zero h + +/-- The cubic `y^3 + y + 1` is irreducible over `GF(2^64)`. -/ +theorem ext3Poly_irreducible : Irreducible ext3Poly := + Polynomial.irreducible_of_degree_le_three_of_not_isRoot + (by rw [ext3Poly_natDegree]; decide) ext3Poly_no_root + +instance : Fact (Irreducible ext3Poly) := ⟨ext3Poly_irreducible⟩ + +/-! ## The extension field -/ + +/-- Parameters of `GF(2^64)[y] / (y^3 + y + 1)`: degree three, with lower coefficients +`(1, 1, 0)` encoding `1 + y` below the leading `y^3`. -/ +def ext3Params : ExtensionParams BF64 where + d := 3 + two_le := by norm_num + lower := #v[1, 1, 0] + q := 2 ^ 64 + +instance : Fact (Nat.card BF64 = ext3Params.q) := + ⟨nat_card_bf64⟩ + +/-- The extension has degree three. -/ +@[simp] theorem ext3Params_d : ext3Params.d = 3 := rfl +/-- The base field has `2 ^ 64` elements. -/ +@[simp] theorem ext3Params_q : ext3Params.q = 2 ^ 64 := rfl + +/-- The parameters' defining polynomial is the cubic. -/ +theorem ext3Params_poly : ext3Params.poly = ext3Poly := by + have h0 : ext3Params.lowerCoeff ⟨0, by norm_num⟩ = 1 := rfl + have h1 : ext3Params.lowerCoeff ⟨1, by norm_num⟩ = 1 := rfl + have h2 : ext3Params.lowerCoeff ⟨2, by norm_num⟩ = 0 := rfl + rw [ExtensionParams.poly, ext3Poly] + show X ^ 3 + (∑ i : Fin 3, C (ext3Params.lowerCoeff i) * X ^ (i : ℕ)) = X ^ 3 + X + 1 + rw [Fin.sum_univ_three] + rw [show ext3Params.lowerCoeff (0 : Fin 3) = 1 from h0, + show ext3Params.lowerCoeff (1 : Fin 3) = 1 from h1, + show ext3Params.lowerCoeff (2 : Fin 3) = 0 from h2] + simp only [map_zero, map_one] + rw [show ((0 : Fin 3) : ℕ) = 0 from rfl, show ((1 : Fin 3) : ℕ) = 1 from rfl] + ring + +instance : Fact (Irreducible ext3Params.poly) := + ⟨ext3Params_poly ▸ ext3Poly_irreducible⟩ + +/-- `GF(2^192)`, the degree-three extension `GF(2^64)[y] / (y^3 + y + 1)`. + +Its coefficient vector has three limbs representing `c0 + c1 * y + c2 * y^2` in that order. -/ +abbrev Ext3 : Type := Ext ext3Params + +/-- The extension inherits characteristic two from its base field. -/ +instance : CharP Ext3 2 := + charP_of_injective_algebraMap' (R := BF64) (A := Ext3) 2 + +/-- The adjoined root `y` of `y^3 + y + 1`, as an element of `Ext3`. -/ +def ext3Gen : Ext3 := Ext.gen + +/-- +`ext3Gen` is the framework's `Ext.gen`. + +Deliberately **not** `@[simp]`: as a rewrite it fires before `ext3Gen_pow_three` can match, +which would knock that lemma out of the simp set. +-/ +theorem ext3Gen_eq_gen : ext3Gen = Ext.gen := rfl + +/-- `ext3Gen` maps to the adjoined root of the specification. -/ +@[simp] theorem toQuot_ext3Gen : Ext.toQuot ext3Gen = Ext.rt ext3Params := Ext.toQuot_gen + +/-- `ext3Gen` is a root of `y^3 + y + 1`, in the form `aeval` expects. -/ +theorem aeval_ext3Gen : aeval ext3Gen ext3Params.poly = 0 := Ext.aeval_gen_poly + +/-- **The defining relation**: the adjoined root satisfies `y^3 = y + 1`. -/ +@[simp] theorem ext3Gen_pow_three : ext3Gen ^ 3 = ext3Gen + 1 := by + have h := aeval_ext3Gen + rw [ext3Params_poly, ext3Poly] at h + simp only [map_add, map_pow, aeval_X, aeval_one] at h + rw [← sub_eq_zero, CharTwo.sub_eq_add, ← h, add_assoc] + +/-- `Ext3` has `2 ^ 192` elements, independently of an enumeration. -/ +@[simp] theorem nat_card_ext3 : Nat.card Ext3 = 2 ^ 192 := by + rw [Ext.nat_card_ext, ext3Params_q, ext3Params_d, ← pow_mul] + +/-- Any enumeration of `Ext3` has `2 ^ 192` elements. -/ +@[simp] theorem card_ext3 [Fintype Ext3] : Fintype.card Ext3 = 2 ^ 192 := by + rw [← Nat.card_eq_fintype_card, nat_card_ext3] + +end BF64 diff --git a/CompPoly/Fields/Binary/BF64/Impl.lean b/CompPoly/Fields/Binary/BF64/Impl.lean new file mode 100644 index 00000000..d7205348 --- /dev/null +++ b/CompPoly/Fields/Binary/BF64/Impl.lean @@ -0,0 +1,529 @@ +/- +Copyright (c) 2026 CompPoly Contributors. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Nicolas Schleicher +-/ +module + +public import CompPoly.Fields.Binary.BF64.Reduce +public import Mathlib.RingTheory.AdjoinRoot + +/-! +# The computable `GF(2^64)` carrier + +An element stores a 64-bit word whose bit `i` is the coefficient of `x^i`. Addition is `xor`, +multiplication is a carry-less product followed by `reduce`, and inversion is the +Itoh-Tsujii addition chain. The carrier maps into `AdjoinRoot basePoly` through +`BF64.toQuot`, so Mathlib's field theory applies while the operations stay executable. +The nominal carrier separates this polynomial presentation from raw words and binary towers. +`ofBitVec` and `toBitVec` expose its coordinates; field numerals use characteristic-two casts. +Finiteness is supplied as a proposition, with cardinality stated using `Nat.card`. Enumeration +is an explicit proof-side choice and is never constructed during native module startup. + +## Main definitions + +* `BF64` — the nominal carrier with `Add`, `Mul`, `Inv`, `CommRing` and `Field`. +* `BF64.ofBitVec`, `BF64.toBitVec` — explicit maps to and from its 64-bit coordinates. +* `BF64.toQuot` — the bridge into `BF64Quot`. +* `BF64.powTwoPow` — repeated squaring, `a ^ (2 ^ k)`. +* `BF64.invItohTsujii` — inversion by the Itoh-Tsujii addition chain. + +## Main statements + +* `BF64.toQuot_add`, `BF64.toQuot_mul` — the bridge is a ring homomorphism. +* `BF64.toQuot_injective`, `BF64.toQuot_surjective` — it is a bijection. +* `BF64.mul_invItohTsujii` — the addition chain really inverts. +* `BF64.nat_card_bf64` — `Nat.card BF64 = 2 ^ 64`. + +## Implementation notes + +The algebraic instances are written out field-by-field rather than obtained from +`Function.Injective.commRing`. That transport takes the bridge map as *data*, which makes +the whole structure noncomputable and shadows the computable `Mul` and `Pow`; because +`CompPoly.Extension.Ext.mul` reaches through the base field's `Field` instance, a +noncomputable base field would take the extension in `BF64/Ext3.lean` down with it too. +The `#guard` checks in `tests/CompPolyTests/Fields/Binary/BF64.lean` run the compiled +arithmetic and so fail the build if this ever regresses. + +`Pow` is `npowBinRec`, binary exponentiation, matching +`CompPoly.Extension.Ext.instPowNat`. The linear `npowRec` would need `2 ^ 64` +multiplications for a full-order exponent and is unusable in the kernel. +-/ + +@[expose] public section + +open Polynomial BinaryField + +/-- An element of `GF(2)[X] / (X^64 + X^4 + X^3 + X + 1)`, stored in a polynomial basis. -/ +structure BF64 where + /-- Bit `i` is the coefficient of `X^i`. -/ + toBitVec : BitVec 64 + deriving DecidableEq, BEq + +namespace BF64 + +/-- Construct a field element from its polynomial-basis bits. -/ +@[inline] def ofBitVec (a : BitVec 64) : BF64 := ⟨a⟩ + +/-- Reading the coordinates of a constructed element recovers the input word. -/ +@[simp] theorem toBitVec_ofBitVec (a : BitVec 64) : (ofBitVec a).toBitVec = a := rfl + +/-- Reconstructing an element from its coordinates recovers that element. -/ +@[simp] theorem ofBitVec_toBitVec (a : BF64) : ofBitVec a.toBitVec = a := rfl + +/-- Polynomial-basis bits uniquely determine a field element. -/ +theorem toBitVec_injective : Function.Injective toBitVec := by + intro a b h + cases a + cases b + cases h + rfl + +/-- Elements with equal polynomial-basis coordinates are equal. -/ +@[ext] theorem ext {a b : BF64} (h : a.toBitVec = b.toBitVec) : a = b := + toBitVec_injective h + +/-- The equivalence between field elements and their polynomial-basis coordinates. -/ +def equivBitVec : BF64 ≃ BitVec 64 where + toFun := toBitVec + invFun := ofBitVec + left_inv := ofBitVec_toBitVec + right_inv := toBitVec_ofBitVec + +instance : LawfulBEq BF64 where + eq_of_beq {a b} h := toBitVec_injective (eq_of_beq h) + rfl {a} := by + change (a.toBitVec == a.toBitVec) = true + exact BEq.rfl + +instance : Repr BF64 := ⟨fun a prec => reprPrec a.toBitVec prec⟩ + +instance : Zero BF64 := ⟨ofBitVec 0⟩ +instance : One BF64 := ⟨ofBitVec 1⟩ +instance : Inhabited BF64 := ⟨0⟩ + +/-- Zero has every polynomial-basis coefficient equal to zero. -/ +@[simp] theorem toBitVec_zero : (0 : BF64).toBitVec = 0#64 := rfl + +/-- One has only its constant coefficient equal to one. -/ +@[simp] theorem toBitVec_one : (1 : BF64).toBitVec = 1#64 := rfl + +/-- Addition in characteristic two is coefficientwise `xor`. -/ +instance : Add BF64 := ⟨fun a b => ofBitVec (a.toBitVec ^^^ b.toBitVec)⟩ + +/-- Negation is the identity in characteristic two. -/ +instance : Neg BF64 := ⟨fun a => a⟩ + +instance : Sub BF64 := ⟨fun a b => ofBitVec (a.toBitVec ^^^ b.toBitVec)⟩ + +/-- Multiply by reducing the carry-less product of the polynomial-basis words. -/ +def mul (a b : BF64) : BF64 := + ofBitVec (reduce (carryLessMul (w := 128) a.toBitVec b.toBitVec)) + +instance : Mul BF64 := ⟨mul⟩ + +/-- The polynomial denoted by a carrier value. -/ +noncomputable def toPolyBF64 (a : BF64) : Polynomial (ZMod 2) := + toPoly a.toBitVec + +/-- The bridge into the quotient. -/ +noncomputable def toQuot (a : BF64) : BF64Quot := + AdjoinRoot.mk basePoly (toPolyBF64 a) + +/-! ## Equation lemmas for the operations -/ + +/-- Addition unfolds to `xor`. -/ +theorem add_def (a b : BF64) : a + b = ofBitVec (a.toBitVec ^^^ b.toBitVec) := rfl + +/-- Multiplication uses the named executable product. -/ +theorem mul_def (a b : BF64) : a * b = mul a b := rfl + +/-- The product word is the reduced carry-less product of the input words. -/ +@[simp] theorem toBitVec_mul (a b : BF64) : + (a * b).toBitVec = reduce (carryLessMul (w := 128) a.toBitVec b.toBitVec) := rfl + +/-! ## The bridge is a ring homomorphism -/ + +@[simp] theorem toPolyBF64_zero : toPolyBF64 0 = 0 := by + show toPoly (0 : BitVec 64) = 0 + exact toPoly_zero_eq_zero + +@[simp] theorem toQuot_zero : toQuot 0 = 0 := by + rw [toQuot, toPolyBF64_zero, map_zero] + +@[simp] theorem toPolyBF64_add (a b : BF64) : + toPolyBF64 (a + b) = toPolyBF64 a + toPolyBF64 b := by + change toPoly (a.toBitVec ^^^ b.toBitVec) = toPoly a.toBitVec + toPoly b.toBitVec + exact toPoly_xor _ _ + +@[simp] theorem toQuot_add (a b : BF64) : toQuot (a + b) = toQuot a + toQuot b := by + rw [toQuot, toQuot, toQuot, toPolyBF64_add, map_add] + +/-- Multiplication agrees with the quotient's, because `reduce` computes the remainder. -/ +@[simp] theorem toQuot_mul (a b : BF64) : toQuot (a * b) = toQuot a * toQuot b := by + rw [toQuot, toQuot, toQuot, ← map_mul, toPolyBF64, toBitVec_mul, toPoly_reduce, + toPoly_carryLessMul _ _ (by omega)] + rw [AdjoinRoot.mk_eq_mk, toPolyBF64, toPolyBF64] + exact ⟨-(toPoly a.toBitVec * toPoly b.toBitVec / basePoly), by + rw [EuclideanDomain.mod_eq_sub_mul_div]; ring⟩ + +/-- Distinct carrier values denote distinct quotient elements. + +A difference of two carrier values has degree below 64, while the modulus has degree +exactly 64, so the modulus can divide it only when it is zero. -/ +theorem toQuot_injective : Function.Injective toQuot := by + intro a b h + have hsub : toPolyBF64 a - toPolyBF64 b = toPoly (a.toBitVec ^^^ b.toBitVec) := by + rw [toPoly_xor, toPolyBF64, toPolyBF64] + exact ZMod2Poly.sub_eq_add _ _ + have hdvd : basePoly ∣ toPolyBF64 a - toPolyBF64 b := AdjoinRoot.mk_eq_mk.mp h + have hzero : toPoly (a.toBitVec ^^^ b.toBitVec) = 0 := by + by_contra hnz + have hne : toPolyBF64 a - toPolyBF64 b ≠ 0 := by rw [hsub]; exact hnz + have hle := Polynomial.degree_le_of_dvd hdvd hne + rw [hsub, basePoly_degree] at hle + exact absurd (toPoly_degree_lt_w (w := 64) (by norm_num) + (a.toBitVec ^^^ b.toBitVec)) (not_lt.mpr hle) + have hxor : a.toBitVec ^^^ b.toBitVec = 0 := by + by_contra hnz + exact ((toPoly_ne_zero_iff_ne_zero (a.toBitVec ^^^ b.toBitVec)).mpr hnz) hzero + exact toBitVec_injective (BitVec.xor_eq_zero_iff.mp hxor) + +/-! ## Algebraic structure + +Every law is discharged by pushing through the injective `toQuot` into the quotient, +where it holds because `BF64Quot` is a commutative ring. The instances are +built field-by-field rather than by `Function.Injective.commRing`, because that transport +takes `toQuot` as data and would make the operations noncomputable. +-/ + +@[simp] theorem toPolyBF64_one : toPolyBF64 1 = 1 := + toPoly_one_eq_one (w := 64) (by norm_num) + +@[simp] theorem toQuot_one : toQuot 1 = 1 := by + rw [toQuot, toPolyBF64_one, map_one] + +theorem toQuot_inj {a b : BF64} : toQuot a = toQuot b ↔ a = b := + ⟨fun h => toQuot_injective h, fun h => h ▸ rfl⟩ + +/-- Addition is self-cancelling: the field has characteristic two. -/ +theorem add_self (a : BF64) : a + a = 0 := by + apply toBitVec_injective + exact BitVec.xor_self + +/-! ### Scalar and power operations + +Scalar multiples use the additive recursion. Natural casts use parity, and integer casts +use the corresponding natural cast and negation. Raw polynomial-basis words enter through +`ofBitVec`, independently of numeral casting. +-/ + +instance : SMul ℕ BF64 := ⟨nsmulRec⟩ +instance : SMul ℤ BF64 := ⟨zsmulRec nsmulRec⟩ + +/-- Cast a natural number by its parity in characteristic two. -/ +@[inline] def natCast (n : ℕ) : BF64 := if n % 2 = 0 then 0 else 1 + +instance : NatCast BF64 := ⟨natCast⟩ + +/-- Casting a successor adds the multiplicative unit. -/ +theorem natCast_succ (n : ℕ) : natCast (n + 1) = natCast n + 1 := by + apply toBitVec_injective + rcases Nat.mod_two_eq_zero_or_one n with h | h <;> + simp [natCast, Nat.add_mod, h, add_def] + +instance : IntCast BF64 := ⟨Int.castDef⟩ +instance : Pow BF64 ℕ := ⟨fun a n => npowBinRec n a⟩ + +theorem npow_def (a : BF64) (n : ℕ) : a ^ n = npowBinRec n a := rfl + +/-! ### The commutative-ring structure + +Every law is discharged by pushing through the injective `toQuot` into `BF64Quot`, +where it holds because the quotient is a commutative ring. The instances are written out +field-by-field rather than via `Function.Injective.commRing`: that transport takes `toQuot` +as *data*, which would make the whole structure noncomputable and shadow the computable +operations. This mirrors `CompPoly.Extension.Ext.instCommRing`. +-/ + +theorem toQuot_neg (a : BF64) : toQuot (-a) = -toQuot a := by + show toQuot a = -toQuot a + rw [eq_neg_iff_add_eq_zero, ← toQuot_add, add_self, toQuot_zero] + +theorem toQuot_sub (a b : BF64) : toQuot (a - b) = toQuot a - toQuot b := by + show toQuot (a + b) = toQuot a - toQuot b + rw [toQuot_add, sub_eq_add_neg] + congr 1 + rw [← toQuot_neg b] + rfl + +instance : AddCommGroup BF64 where + add_assoc a b c := toQuot_injective (by simp only [toQuot_add, add_assoc]) + zero_add a := toQuot_injective (by simp only [toQuot_add, toQuot_zero, zero_add]) + add_zero a := toQuot_injective (by simp only [toQuot_add, toQuot_zero, add_zero]) + add_comm a b := toQuot_injective (by simp only [toQuot_add, add_comm]) + neg_add_cancel a := + toQuot_injective (by simp only [toQuot_add, toQuot_neg, toQuot_zero, neg_add_cancel]) + sub_eq_add_neg a b := + toQuot_injective (by simp only [toQuot_sub, toQuot_add, toQuot_neg, sub_eq_add_neg]) + nsmul := nsmulRec + nsmul_zero _ := rfl + nsmul_succ _ _ := rfl + zsmul := zsmulRec nsmulRec + zsmul_zero' _ := rfl + zsmul_succ' _ _ := rfl + zsmul_neg' _ _ := rfl + +/-- `npowBinRec` agrees with the linear `npowRec`, so the power can be reasoned about by +ordinary recursion on the exponent while still *evaluating* by binary exponentiation. +Mathlib's `npowBinRec_succ` needs a `Semigroup`, which is not available until `mul_assoc` +below, so associativity is supplied here from the quotient. -/ +theorem npow_eq_npowRec (a : BF64) (n : ℕ) : a ^ n = npowRec n a := by + have hassoc : ∀ x y z : BF64, x * y * z = x * (y * z) := fun x y z => + toQuot_injective (by simp only [toQuot_mul, mul_assoc]) + let _ : Semigroup BF64 := { mul := (· * ·), mul_assoc := hassoc } + rw [npow_def, ← npowBinRecAuto, ← npowRec_eq_npowBinRec] + +theorem toQuot_npow (a : BF64) (n : ℕ) : toQuot (a ^ n) = toQuot a ^ n := by + induction n with + | zero => rw [npow_eq_npowRec, npowRec, pow_zero, toQuot_one] + | succ k ih => rw [npow_eq_npowRec, npowRec, ← npow_eq_npowRec, toQuot_mul, ih, pow_succ] + +/-- The quotient inherits characteristic two from `GF(2)`. -/ +instance : CharP (BF64Quot) 2 := by + have : CharP (ZMod 2) 2 := inferInstance + exact charP_of_injective_algebraMap' (ZMod 2) 2 + +theorem toQuot_natCast (n : ℕ) : toQuot (n : BF64) = (n : BF64Quot) := by + induction n with + | zero => show toQuot 0 = _; rw [toQuot_zero, Nat.cast_zero] + | succ k ih => + change toQuot (natCast k) = _ at ih + change toQuot (natCast (k + 1)) = _ + rw [natCast_succ, toQuot_add, ih, toQuot_one, Nat.cast_succ] + +instance : CommRing BF64 where + left_distrib a b c := toQuot_injective (by simp only [toQuot_mul, toQuot_add, mul_add]) + right_distrib a b c := toQuot_injective (by simp only [toQuot_mul, toQuot_add, add_mul]) + zero_mul a := toQuot_injective (by simp only [toQuot_mul, toQuot_zero, zero_mul]) + mul_zero a := toQuot_injective (by simp only [toQuot_mul, toQuot_zero, mul_zero]) + mul_assoc a b c := toQuot_injective (by simp only [toQuot_mul, mul_assoc]) + one_mul a := toQuot_injective (by simp only [toQuot_mul, toQuot_one, one_mul]) + mul_one a := toQuot_injective (by simp only [toQuot_mul, toQuot_one, mul_one]) + mul_comm a b := toQuot_injective (by simp only [toQuot_mul, mul_comm]) + npow n x := x ^ n + npow_zero x := toQuot_injective (by simp only [toQuot_npow, toQuot_one, pow_zero]) + npow_succ n x := toQuot_injective (by simp only [toQuot_npow, toQuot_mul, pow_succ]) + natCast n := (n : BF64) + natCast_zero := toQuot_injective (by simp only [toQuot_natCast, toQuot_zero, Nat.cast_zero]) + natCast_succ n := + toQuot_injective (by simp only [toQuot_natCast, toQuot_add, toQuot_one, Nat.cast_succ]) + intCast n := (n : BF64) + intCast_ofNat n := rfl + intCast_negSucc n := rfl + +/-! ### Inversion by Itoh-Tsujii + +Inversion uses the Itoh-Tsujii addition chain: `a⁻¹ = a^(2^64 - 2) = (a^(2^63 - 1))^2`, +with `a^(2^k - 1)` built along `1, 2, 3, 6, 7, 14, 15, 30, 31, 62, 63`. This is an +explicit algorithm rather than an existence proof, so the resulting inverse evaluates. +`BF128Ghash.inv_itoh_tsujii` is the analogous chain at degree 128. +-/ + +/-- Repeated squaring: `a ^ (2 ^ k)`. -/ +def powTwoPow (a : BF64) (k : ℕ) : BF64 := + match k with + | 0 => a + | n + 1 => powTwoPow (a * a) n + +theorem toQuot_powTwoPow (a : BF64) (k : ℕ) : + toQuot (powTwoPow a k) = toQuot a ^ (2 ^ k) := by + induction k generalizing a with + | zero => simp only [powTwoPow, pow_zero, pow_one] + | succ n ih => + simp only [powTwoPow] + rw [ih, toQuot_mul, ← sq, ← pow_mul, pow_succ, mul_comm] + +/-- The multiplicative inverse, by the Itoh-Tsujii addition chain, with `0⁻¹ = 0`. -/ +def invItohTsujii (a : BF64) : BF64 := + if a = 0 then 0 else + let u1 := a + let u2 := powTwoPow u1 1 * u1 + let u3 := powTwoPow u2 1 * u1 + let u6 := powTwoPow u3 3 * u3 + let u7 := powTwoPow u6 1 * u1 + let u14 := powTwoPow u7 7 * u7 + let u15 := powTwoPow u14 1 * u1 + let u30 := powTwoPow u15 15 * u15 + let u31 := powTwoPow u30 1 * u1 + let u62 := powTwoPow u31 31 * u31 + let u63 := powTwoPow u62 1 * u1 + u63 * u63 + +/-- The exponent identity behind one Itoh-Tsujii step. -/ +private theorem chain_exponent (n m : ℕ) : + (2 ^ n - 1) * 2 ^ m + (2 ^ m - 1) = 2 ^ (n + m) - 1 := by + have h1 : 1 ≤ 2 ^ n := Nat.one_le_two_pow + have h2 : 1 ≤ 2 ^ m := Nat.one_le_two_pow + rw [pow_add] + generalize 2 ^ n = A at * + generalize 2 ^ m = B at * + cases A with + | zero => omega + | succ a => + cases B with + | zero => omega + | succ b => simp [Nat.succ_mul, Nat.mul_succ] + +/-- The target of chain step `k`: `a ^ (2 ^ k - 1)`. -/ +private noncomputable def chainTarget (q : BF64Quot) (k : ℕ) : + BF64Quot := q ^ (2 ^ k - 1) + +/-- The Itoh-Tsujii step: combining the `n`- and `m`-targets gives the `n + m`-target. -/ +private theorem chainTarget_step {q x y : BF64Quot} {n m : ℕ} + (hx : x = chainTarget q n) (hy : y = chainTarget q m) : + x ^ (2 ^ m) * y = chainTarget q (n + m) := by + rw [hx, hy, chainTarget, chainTarget, chainTarget, ← pow_mul, ← pow_add, chain_exponent] + +/-- The Itoh-Tsujii chain computes `a ^ (2 ^ 64 - 2)`. -/ +theorem toQuot_invItohTsujii (a : BF64) (h : a ≠ 0) : + toQuot (invItohTsujii a) = toQuot a ^ (2 ^ 64 - 2) := by + rw [invItohTsujii, if_neg h] + set q := toQuot a with hq + have e1 : toQuot a = chainTarget q 1 := by + simp only [chainTarget, hq]; norm_num + have e2 : toQuot (powTwoPow a 1 * a) = chainTarget q 2 := by + rw [toQuot_mul, toQuot_powTwoPow] + exact chainTarget_step e1 e1 + have e3 : toQuot (powTwoPow (powTwoPow a 1 * a) 1 * a) = chainTarget q 3 := by + rw [toQuot_mul, toQuot_powTwoPow] + exact chainTarget_step e2 e1 + set u3 := powTwoPow (powTwoPow a 1 * a) 1 * a with hu3 + have e6 : toQuot (powTwoPow u3 3 * u3) = chainTarget q 6 := by + rw [toQuot_mul, toQuot_powTwoPow] + exact chainTarget_step e3 e3 + set u6 := powTwoPow u3 3 * u3 with hu6 + have e7 : toQuot (powTwoPow u6 1 * a) = chainTarget q 7 := by + rw [toQuot_mul, toQuot_powTwoPow] + exact chainTarget_step e6 e1 + set u7 := powTwoPow u6 1 * a with hu7 + have e14 : toQuot (powTwoPow u7 7 * u7) = chainTarget q 14 := by + rw [toQuot_mul, toQuot_powTwoPow] + exact chainTarget_step e7 e7 + set u14 := powTwoPow u7 7 * u7 with hu14 + have e15 : toQuot (powTwoPow u14 1 * a) = chainTarget q 15 := by + rw [toQuot_mul, toQuot_powTwoPow] + exact chainTarget_step e14 e1 + set u15 := powTwoPow u14 1 * a with hu15 + have e30 : toQuot (powTwoPow u15 15 * u15) = chainTarget q 30 := by + rw [toQuot_mul, toQuot_powTwoPow] + exact chainTarget_step e15 e15 + set u30 := powTwoPow u15 15 * u15 with hu30 + have e31 : toQuot (powTwoPow u30 1 * a) = chainTarget q 31 := by + rw [toQuot_mul, toQuot_powTwoPow] + exact chainTarget_step e30 e1 + set u31 := powTwoPow u30 1 * a with hu31 + have e62 : toQuot (powTwoPow u31 31 * u31) = chainTarget q 62 := by + rw [toQuot_mul, toQuot_powTwoPow] + exact chainTarget_step e31 e31 + set u62 := powTwoPow u31 31 * u31 with hu62 + have e63 : toQuot (powTwoPow u62 1 * a) = chainTarget q 63 := by + rw [toQuot_mul, toQuot_powTwoPow] + exact chainTarget_step e62 e1 + set u63 := powTwoPow u62 1 * a with hu63 + rw [toQuot_mul, e63, chainTarget, ← pow_add] + congr 1 + +/-! ### The field structure + +`BF64Quot` is a field because `basePoly` is irreducible, and `toQuot` is an +injective ring homomorphism onto it, so the carrier is a field too. Following +`CompPoly.Extension.Ext.instField`, the structure is assembled field-by-field so that every +operation stays computable. +-/ + +theorem toQuot_eq_zero_iff {a : BF64} : toQuot a = 0 ↔ a = 0 := by + rw [← toQuot_zero] + exact ⟨fun h => toQuot_injective h, fun h => h ▸ rfl⟩ + +theorem exists_pair_ne : ∃ x y : BF64, x ≠ y := + ⟨0, 1, by decide +kernel⟩ + +/-- The carrier is in bijection with `Fin (2 ^ 64)`, by its underlying representation. -/ +def equivFin : BF64 ≃ Fin (2 ^ 64) where + toFun a := a.toBitVec.toFin + invFun i := ofBitVec (BitVec.ofFin i) + left_inv _ := rfl + right_inv _ := rfl + +instance : Finite BF64 := Finite.of_equiv (Fin (2 ^ 64)) equivFin.symm + +/-- `BF64` has `2 ^ 64` elements, independently of an enumeration. -/ +theorem nat_card_bf64 : Nat.card BF64 = 2 ^ 64 := by + rw [Nat.card_congr equivFin, Nat.card_fin] + +/-- Any enumeration of `BF64` has `2 ^ 64` elements. -/ +theorem card_bf64 [Fintype BF64] : Fintype.card BF64 = 2 ^ 64 := by + rw [← Nat.card_eq_fintype_card, nat_card_bf64] + +/-- The bridge is surjective: it is injective between finite types of equal cardinality. -/ +theorem toQuot_surjective : Function.Surjective toQuot := by + refine (toQuot_injective.bijective_of_nat_card_le ?_).2 + rw [nat_card_bf64, Nat.card_eq_fintype_card, card_bf64Quot] + +/-- The Itoh-Tsujii inverse really is a multiplicative inverse. -/ +theorem mul_invItohTsujii {a : BF64} (h : a ≠ 0) : a * invItohTsujii a = 1 := by + have hq : toQuot a ≠ 0 := fun hz => h (toQuot_eq_zero_iff.mp hz) + refine toQuot_injective ?_ + rw [toQuot_mul, toQuot_invItohTsujii a h, toQuot_one, ← pow_succ'] + have hcard : toQuot a ^ (2 ^ 64 - 1) = 1 := by + have := FiniteField.pow_card_sub_one_eq_one (toQuot a) hq + rwa [card_bf64Quot] at this + rw [show 2 ^ 64 - 2 + 1 = 2 ^ 64 - 1 from by norm_num] + exact hcard + +/-- Every nonzero carrier value has a multiplicative inverse. -/ +theorem exists_mul_inv {a : BF64} (h : a ≠ 0) : ∃ b : BF64, a * b = 1 := + ⟨invItohTsujii a, mul_invItohTsujii h⟩ + +/-- Inversion is the Itoh-Tsujii chain, so it evaluates. -/ +instance : Inv BF64 := ⟨invItohTsujii⟩ + +instance : Div BF64 := ⟨fun a b => a * invItohTsujii b⟩ + +/-- Inversion unfolds to the Itoh-Tsujii chain. -/ +theorem inv_def (a : BF64) : a⁻¹ = invItohTsujii a := rfl + +/-- Division unfolds to multiplication by the Itoh-Tsujii inverse. -/ +theorem div_def (a b : BF64) : a / b = a * invItohTsujii b := rfl + +@[simp] theorem inv_zero_bf64 : (0 : BF64)⁻¹ = 0 := by + rw [inv_def, invItohTsujii, if_pos rfl] + +/-- `BF64` satisfies `IsField`, the bundled-data-free form of the field axioms. -/ +theorem isField_bf64 : IsField BF64 where + exists_pair_ne := exists_pair_ne + mul_comm := mul_comm + mul_inv_cancel := fun h => exists_mul_inv h + +/-- The carrier is a field, `GF(2^64)`. + +Assembled field-by-field around the explicit Itoh-Tsujii inverse, so inversion and division +evaluate rather than being extracted from an existence proof. -/ +instance : Field BF64 where + inv := invItohTsujii + div a b := a * invItohTsujii b + div_eq_mul_inv _ _ := rfl + exists_pair_ne := exists_pair_ne + mul_inv_cancel _ h := mul_invItohTsujii h + inv_zero := inv_zero_bf64 + qsmul := (Rat.castRec · * ·) + nnqsmul := (NNRat.castRec · * ·) + +/-- The base field has characteristic two, inherited through the bridge. -/ +instance : CharP BF64 2 where + cast_eq_zero_iff n := by + rw [← toQuot_eq_zero_iff, toQuot_natCast] + exact (CharP.cast_eq_zero_iff (BF64Quot) 2 n) + +end BF64 diff --git a/CompPoly/Fields/Binary/BF64/Reduce.lean b/CompPoly/Fields/Binary/BF64/Reduce.lean new file mode 100644 index 00000000..afbdb988 --- /dev/null +++ b/CompPoly/Fields/Binary/BF64/Reduce.lean @@ -0,0 +1,197 @@ +/- +Copyright (c) 2026 CompPoly Contributors. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Nicolas Schleicher +-/ +module + +public import CompPoly.Fields.Binary.BF64.Basic +public import CompPoly.Fields.Binary.Common + +/-! +# Reducing a carry-less product modulo the `GF(2^64)` modulus + +A product of two 64-bit values occupies 128 bits. Because `x^64 ≡ x^4 + x^3 + x + 1` +(`BF64.mul_pow_reduce`), the high half can be folded back down by a single carry-less +multiplication by `reductionConstant = 0x1B`. Each fold shrinks the excess, and two folds +land back inside 64 bits — no long division is needed. + +This mirrors `CompPoly.Fields.Binary.BF128Ghash`'s reduction for the GHASH polynomial, +at the 64-bit width and with this modulus. + +## Main definitions + +* `highHalf`, `lowHalf` — the two 64-bit halves of a 128-bit value. +* `reductionConstant` — `0x1B`, the modulus below its leading term. +* `foldStep` — one reduction fold. +* `reduce` — two folds and a truncation. + +## Main statements + +* `toPoly_reductionConstant` — `0x1B` denotes `baseTail`. +* `foldStep_mod` — a fold preserves the residue modulo the modulus. +* `foldStep_lt` — a fold shrinks the value. +* `toPoly_reduce` — `reduce` computes the remainder modulo the modulus. +-/ + +@[expose] public section + +namespace BF64 + +open Polynomial BinaryField + +/-! ## Halves of a double-width value -/ + +/-- The high 64 bits of a 128-bit value. -/ +def highHalf (x : BitVec 128) : BitVec 64 := BitVec.setWidth 64 (x >>> 64) + +/-- The low 64 bits of a 128-bit value. -/ +def lowHalf (x : BitVec 128) : BitVec 64 := BitVec.setWidth 64 x + +/-- Bit `i` of the high half is bit `64 + i` of the whole. -/ +theorem highHalf_testBit (x : BitVec 128) (i : ℕ) (h : i < 64) : + (highHalf x).toNat.testBit i = x.toNat.testBit (64 + i) := by + unfold highHalf + rw [BitVec.toNat_setWidth, Nat.testBit_mod_two_pow] + simp only [h, decide_true, Bool.true_and, BitVec.toNat_ushiftRight, Nat.testBit_shiftRight] + +/-- Bit `i` of the low half is bit `i` of the whole. -/ +theorem lowHalf_testBit (x : BitVec 128) (i : ℕ) (h : i < 64) : + (lowHalf x).toNat.testBit i = x.toNat.testBit i := by + unfold lowHalf + rw [BitVec.toNat_setWidth, Nat.testBit_mod_two_pow] + simp only [h, decide_true, Bool.true_and] + +/-- Splitting a 128-bit value into `high * x^64 + low`. -/ +theorem toPoly_halves (x : BitVec 128) : + toPoly x = toPoly (highHalf x) * X ^ 64 + toPoly (lowHalf x) := by + have hhi : (∑ i ∈ Finset.range (128 - 64), + if x.toNat.testBit (64 + i) then (X : (ZMod 2)[X]) ^ i else 0) + = toPoly (highHalf x) := by + rw [toPoly_eq_range (highHalf x), show (128 : ℕ) - 64 = 64 from rfl] + refine Finset.sum_congr rfl fun i hmem => ?_ + simp only [Finset.mem_range] at hmem + rw [highHalf_testBit x i hmem] + have hlo : (∑ i ∈ Finset.range 64, + if x.toNat.testBit i then (X : (ZMod 2)[X]) ^ i else 0) = toPoly (lowHalf x) := by + rw [toPoly_eq_range (lowHalf x)] + refine Finset.sum_congr rfl fun i hmem => ?_ + simp only [Finset.mem_range] at hmem + rw [lowHalf_testBit x i hmem] + rw [toPoly_split x 64 (by omega), hhi, hlo] + +/-- If `x < 2 ^ (64 + d)` then its high half is below `2 ^ d`. -/ +theorem highHalf_lt (x : BitVec 128) {d : ℕ} (hx : x.toNat < 2 ^ (64 + d)) : + (highHalf x).toNat < 2 ^ d := by + unfold highHalf + rw [BitVec.toNat_setWidth, BitVec.toNat_ushiftRight] + refine lt_of_le_of_lt (Nat.mod_le _ _) ?_ + rw [Nat.shiftRight_eq_div_pow] + exact Nat.div_lt_of_lt_mul (by rw [← pow_add]; exact hx) + +/-- The low half is always below `2 ^ 64`, being 64 bits wide. -/ +theorem lowHalf_lt (x : BitVec 128) : (lowHalf x).toNat < 2 ^ 64 := (lowHalf x).isLt + +/-- A carry-less product of values below `2 ^ p` and `2 ^ q` is below `2 ^ (p + q)`. -/ +theorem carryLessMul_lt {v w : ℕ} (a b : BitVec v) {p q : ℕ} + (ha : a.toNat < 2 ^ p) (hb : b.toNat < 2 ^ q) (h : v + v ≤ w) : + (carryLessMul (w := w) a b).toNat < 2 ^ (p + q) := by + apply BitVec_lt_two_pow_of_toPoly_degree_lt + rw [toPoly_carryLessMul a b h] + refine lt_of_le_of_lt (Polynomial.degree_mul_le _ _) ?_ + have hda := toPoly_degree_of_lt_two_pow a ha + have hdb := toPoly_degree_of_lt_two_pow b hb + rcases eq_or_ne (toPoly a) 0 with h0 | h0 + · simp [h0] + rcases eq_or_ne (toPoly b) 0 with h1 | h1 + · simp [h1] + · rw [Polynomial.degree_eq_natDegree h0, Polynomial.degree_eq_natDegree h1] at * + rw [← Nat.cast_add] + exact_mod_cast Nat.add_lt_add (by exact_mod_cast hda) (by exact_mod_cast hdb) + +/-! ## The reduction fold -/ + +/-- The reduction constant `0x1B`: the modulus below its leading term, +`x^4 + x^3 + x + 1`. -/ +def reductionConstant : BitVec 64 := 0x1B + +/-- **The reduction constant is the right one**: `0x1B` denotes `x^4 + x^3 + x + 1`. + +A wrong constant here would compile and silently give a different field, so this ties the +bit pattern to the polynomial rather than leaving it to the reader. -/ +theorem toPoly_reductionConstant : toPoly reductionConstant = baseTail := by + have h : reductionConstant = (1 <<< 4) ^^^ (1 <<< 3) ^^^ (1 <<< 1) ^^^ 1 := by decide +kernel + rw [h, baseTail_eq] + simp only [toPoly_xor] + rw [toPoly_one_shiftLeft 4 (by omega), + toPoly_one_shiftLeft 3 (by omega), + toPoly_one_shiftLeft 1 (by omega), + show (1 : BitVec 64) = BitVec.ofNat 64 1 from rfl, + toPoly_one_eq_one (w := 64) (h_w_pos := by omega)] + ring + +/-- The reduction constant fits in five bits, which bounds how much a fold can grow. -/ +theorem reductionConstant_lt : reductionConstant.toNat < 2 ^ 5 := by decide +kernel + +/-- One reduction fold: replace the high half's factor of `x^64` by `baseTail`. -/ +def foldStep (x : BitVec 128) : BitVec 128 := + carryLessMul (w := 128) (highHalf x) reductionConstant ^^^ zeroExtendTo (lowHalf x) + +/-- A fold preserves the residue modulo the modulus. -/ +theorem foldStep_mod (x : BitVec 128) : + toPoly (foldStep x) % basePoly = toPoly x % basePoly := by + unfold foldStep + rw [toPoly_xor, toPoly_carryLessMul (highHalf x) reductionConstant (by omega), + toPoly_reductionConstant, toPoly_zeroExtendTo (lowHalf x) (by omega), toPoly_halves x] + rw [CanonicalEuclideanDomain.add_mod_eq (hn := basePoly_ne_zero)] + conv_rhs => rw [CanonicalEuclideanDomain.add_mod_eq (hn := basePoly_ne_zero)] + rw [mul_pow_reduce (toPoly (highHalf x))] + +/-- A fold of a value below `2 ^ (64 + d)` lands below `2 ^ (max 64 (d + 5))`. -/ +theorem foldStep_lt (x : BitVec 128) {d : ℕ} (hx : x.toNat < 2 ^ (64 + d)) : + (foldStep x).toNat < 2 ^ (max 64 (d + 5)) := by + unfold foldStep + rw [BitVec.toNat_xor] + refine Nat.xor_lt_two_pow ?_ ?_ + · exact lt_of_lt_of_le + (carryLessMul_lt (highHalf x) reductionConstant (highHalf_lt x hx) + reductionConstant_lt (by omega)) + (Nat.pow_le_pow_right (by norm_num) (le_max_right 64 (d + 5))) + · rw [toNat_zeroExtendTo (lowHalf x) (by omega)] + exact lt_of_lt_of_le (lowHalf_lt x) + (Nat.pow_le_pow_right (by norm_num) (le_max_left 64 (d + 5))) + +/-! ## Full reduction -/ + +/-- Reduce a 128-bit carry-less product into the base field: two folds, then truncate. -/ +def reduce (x : BitVec 128) : BitVec 64 := lowHalf (foldStep (foldStep x)) + +/-- Two folds bring any 128-bit value below `2 ^ 64`. -/ +theorem foldStep_foldStep_lt (x : BitVec 128) : (foldStep (foldStep x)).toNat < 2 ^ 64 := by + have h1 : x.toNat < 2 ^ (64 + 64) := x.isLt + have h2 : (foldStep x).toNat < 2 ^ (64 + 5) := by simpa using foldStep_lt x h1 + simpa using foldStep_lt (foldStep x) h2 + +/-- Truncation is faithful on values already below `2 ^ 64`. -/ +theorem toPoly_lowHalf_of_lt (x : BitVec 128) (h : x.toNat < 2 ^ 64) : + toPoly (lowHalf x) = toPoly x := by + rw [toPoly_halves x] + have hhi : highHalf x = 0 := by + apply BitVec.eq_of_toNat_eq + simpa using highHalf_lt x (d := 0) (by simpa using h) + rw [hhi] + simp [toPoly_zero_eq_zero] + +/-- `reduce` computes the remainder of the denoted polynomial modulo the modulus. -/ +theorem toPoly_reduce (x : BitVec 128) : + toPoly (reduce x) = toPoly x % basePoly := by + unfold reduce + rw [toPoly_lowHalf_of_lt _ (foldStep_foldStep_lt x)] + have hmod : toPoly (foldStep (foldStep x)) % basePoly = toPoly x % basePoly := by + rw [foldStep_mod, foldStep_mod] + rw [← hmod] + refine ((Polynomial.mod_eq_self_iff basePoly_ne_zero).mpr ?_).symm + refine lt_of_lt_of_le (toPoly_degree_of_lt_two_pow _ (foldStep_foldStep_lt x)) ?_ + rw [basePoly_degree] + +end BF64 diff --git a/CompPoly/Fields/Binary/Common.lean b/CompPoly/Fields/Binary/Common.lean index 7dec374f..cdd2a9dc 100644 --- a/CompPoly/Fields/Binary/Common.lean +++ b/CompPoly/Fields/Binary/Common.lean @@ -33,6 +33,7 @@ direct GF(2^128) implementation (`BF128Ghash/`). - `clMul`: Carry-less multiplication of bit vectors - `toPoly_xor`: `toPoly (a ^^^ b) = toPoly a + toPoly b` - `toPoly_clMul`: `toPoly (clMul a b) = toPoly a * toPoly b` +- `toPoly_one_shiftLeft`: `toPoly (1 <<< n) = X^n` -/ @[expose] public section @@ -266,11 +267,33 @@ instance {w : Nat} : Std.Associative (α := BitVec w) BitVec.xor where ext i simp only [BitVec.xor_eq, BitVec.getElem_xor, Bool.bne_assoc] -/-- Carry-less (polynomial) multiplication of two 128-bit vectors. -/ -def clMul (a b : B128) : B256 := - Fin.foldl 128 (fun acc i => - if a.getLsbD i then acc ^^^ (to256 b <<< (i : Nat)) - else acc) (0 : B256) +/-- Widen a bit vector by zero-extension, at an arbitrary target width. -/ +def zeroExtendTo {v w : ℕ} (a : BitVec v) : BitVec w := BitVec.zeroExtend w a + +theorem toNat_zeroExtendTo {v w : ℕ} (a : BitVec v) (h : v ≤ w) : + (zeroExtendTo (w := w) a).toNat = a.toNat := by + unfold zeroExtendTo + simp [BitVec.toNat_setWidth] + exact Nat.mod_eq_of_lt (lt_of_lt_of_le a.isLt (Nat.pow_le_pow_right (by norm_num) h)) + +/-- `to256` is the 128-to-256 instance of `zeroExtendTo`. -/ +theorem to256_eq_zeroExtendTo (v : B128) : to256 v = zeroExtendTo v := rfl + +/-- Carry-less (polynomial) multiplication, at an arbitrary operand and result width. + +The result width `w` must admit the full product for the denotation to be faithful; +`toPoly_carryLessMul` carries that hypothesis as `v + v ≤ w`. -/ +def carryLessMul {v w : ℕ} (a b : BitVec v) : BitVec w := + Fin.foldl v (fun acc i => + if a.getLsbD i then acc ^^^ (zeroExtendTo b <<< (i : Nat)) + else acc) (0 : BitVec w) + +/-- Carry-less (polynomial) multiplication of two 128-bit vectors. + +The 128-bit instance of `carryLessMul`, kept under the name the GHASH development uses. -/ +def clMul (a b : B128) : B256 := carryLessMul a b + +theorem clMul_eq_carryLessMul (a b : B128) : clMul a b = carryLessMul (w := 256) a b := rfl /-- Carry-less squaring of a 128-bit vector. -/ def clSq (a : B128) : B256 := @@ -300,17 +323,63 @@ noncomputable def toPoly {w : Nat} (v : BitVec w) : (ZMod 2)[X] := /-- Unfold `clMul` into an always-XOR form so that `toPoly_fold_xor` applies directly in the proof of `toPoly_clMul`. -/ -lemma clMul_unfold (a b : B128) : - clMul a b = Fin.foldl 128 +lemma carryLessMul_unfold {v w : ℕ} (a b : BitVec v) : + carryLessMul (w := w) a b = Fin.foldl v (fun acc i => acc ^^^ (if a.getLsbD i - then to256 b <<< (i : Nat) else 0)) (0 : B256) := by - unfold clMul + then (zeroExtendTo b : BitVec w) <<< (i : Nat) else 0)) (0 : BitVec w) := by + unfold carryLessMul congr funext acc i cases h : BitVec.getLsbD a i · simp · simp +lemma clMul_unfold (a b : B128) : + clMul a b = Fin.foldl 128 + (fun acc i => acc ^^^ (if a.getLsbD i + then to256 b <<< (i : Nat) else 0)) (0 : B256) := + carryLessMul_unfold a b + +/-- A single set bit at position `n` denotes the monomial `X^n`. -/ +lemma toPoly_one_shiftLeft {w : Nat} (n : Nat) (h : n < w) : + toPoly (1 <<< n : BitVec w) = X^n := by + rw [toPoly] + rw [Finset.sum_eq_single (⟨n, h⟩ : Fin w)] + -- 1. The Main Term (j = n): Prove it equals X^n + · simp only + simp only [BitVec.natCast_eq_ofNat, ite_eq_left_iff, Bool.not_eq_true] + intro h_getLsb_eq_false + have h_getLsb_eq_true : (BitVec.ofNat w (1 <<< n)).getLsb ⟨n, h⟩ = true := by + rw [BitVec.getLsb] + simp only [BitVec.toNat_ofNat, Nat.testBit_mod_two_pow, h, decide_true, Nat.testBit_shiftLeft, + ge_iff_le, le_refl, tsub_self, Nat.testBit_zero, Nat.mod_succ, Bool.and_self] + rw [h_getLsb_eq_false] at h_getLsb_eq_true + absurd h_getLsb_eq_true + exact Bool.false_ne_true + -- 2. The Other Terms (j ≠ n): Prove they are 0 + · intro b _ hb_ne_n_fin + split_ifs with h_lsb + · -- Contradiction: If bit is set, b must equal n + exfalso + have h_getLsb_eq_false : ((1 <<< n) : BitVec w).getLsb b = false := by + rw [BitVec.getLsb] + have h_lhs : ((1 <<< n) : BitVec w).toNat = 1 <<< n := by + simp only [Nat.shiftLeft_eq, one_mul, BitVec.natCast_eq_ofNat, BitVec.toNat_ofNat] + apply Nat.mod_eq_of_lt + apply Nat.pow_lt_pow_right (ha := by omega) (h := by omega) + rw [h_lhs] + rw [Nat.one_shiftLeft] + rw [Nat.testBit_two_pow]; + let h_ne := Fin.val_ne_of_ne hb_ne_n_fin + exact decide_eq_false (id (Ne.symm h_ne)) + rw [h_getLsb_eq_false] at h_lsb + absurd h_lsb + exact Bool.false_ne_true + · rfl -- If bit is not set, result is 0 + -- 3. Universe Check: Prove n is in Finset.univ + · intro h_absurd + simp at h_absurd -- Finset.univ contains everything + lemma toPoly_one_eq_one {w : Nat} (h_w_pos : w > 0) : toPoly (BitVec.ofNat w 1) = 1 := by unfold toPoly -- For BitVec.ofNat w 1, only bit 0 is set, so only X^0 = 1 contributes @@ -710,32 +779,79 @@ theorem toPoly_shiftLeft_no_overflow {w d : ℕ} (a : BitVec w) (ha : a.toNat < · simp [toPoly_coeff, hn, hs, hns] · simp [toPoly_coeff, hn, hs] +/-- Widening does not change the polynomial denoted. -/ +lemma toPoly_zeroExtendTo {v w : ℕ} (a : BitVec v) (h : v ≤ w) : + toPoly (zeroExtendTo (w := w) a) = toPoly a := by + unfold toPoly BitVec.getLsb + rw [toNat_zeroExtendTo a h] + rw [Fin.sum_univ_eq_sum_range + (f := fun i => if a.toNat.testBit i then (X : (ZMod 2)[X]) ^ i else 0), + Fin.sum_univ_eq_sum_range + (f := fun i => if a.toNat.testBit i then (X : (ZMod 2)[X]) ^ i else 0)] + refine (Finset.sum_subset (s₁ := Finset.range v) (s₂ := Finset.range w) + (fun x hx => Finset.mem_range.mpr (lt_of_lt_of_le (Finset.mem_range.mp hx) h)) ?_).symm + intro i _ hnot + simp only [Finset.mem_range, not_lt] at hnot + have hlt : a.toNat < 2 ^ i := + lt_of_lt_of_le a.isLt (Nat.pow_le_pow_right (by norm_num) hnot) + simp [Nat.testBit_lt_two_pow hlt] + +/-- `carryLessMul` denotes the product of the denoted polynomials, provided the result +width admits the full product. -/ +lemma toPoly_carryLessMul {v w : ℕ} (a b : BitVec v) (h : v + v ≤ w) : + toPoly (carryLessMul (w := w) a b) = toPoly a * toPoly b := by + rw [carryLessMul_unfold] + rw [toPoly_fold_xor + (f := fun k => if a.getLsbD k = true then (zeroExtendTo b : BitVec w) <<< k else 0)] + conv_rhs => enter [1]; unfold toPoly + unfold BitVec.getLsb + rw [Fin.sum_univ_eq_sum_range + (f := fun i => if (BitVec.toNat a).testBit i = true then X ^ i else 0)] + rw [Finset.sum_mul] + apply Finset.sum_congr rfl + intro i hi + simp only [Finset.mem_range] at hi + unfold BitVec.getLsbD + split_ifs + · have hb : (zeroExtendTo b : BitVec w).toNat < 2 ^ v := by + rw [toNat_zeroExtendTo b (by omega)]; exact b.isLt + rw [toPoly_shiftLeft_no_overflow (d := v) (zeroExtendTo b) (ha := hb) + (h_no_overflow := by omega)] + rw [toPoly_zeroExtendTo b (by omega)] + ring + · simp [toPoly_zero_eq_zero] + lemma toPoly_clMul (a b : B128) : - toPoly (clMul a b) = toPoly a * toPoly b := by - rw [clMul_unfold] - rw [toPoly_fold_xor (f := fun k => if a.getLsbD k = true then to256 b <<< k else 0)] - conv_rhs => enter [1]; unfold toPoly - unfold BitVec.getLsb - rw [Fin.sum_univ_eq_sum_range - (f := fun i => if (BitVec.toNat a).testBit i = true - then X ^ i else 0)] - rw [Finset.sum_mul] - apply Finset.sum_congr rfl - intro i hi - simp only [Finset.mem_range] at hi - unfold BitVec.getLsbD - split_ifs - · have ha_proof : (to256 b).toNat < 2 ^ 128 := by - rw [to256_toNat] - exact b.isLt - have h_no_overflow_proof : 128 + i ≤ 256 := by - omega - rw [toPoly_shiftLeft_no_overflow (d := 128) (to256 b) - (ha := ha_proof) - (h_no_overflow := h_no_overflow_proof)] - rw [toPoly_128_extend_256] - ring - · simp [toPoly_zero_eq_zero] + toPoly (clMul a b) = toPoly a * toPoly b := + toPoly_carryLessMul a b (by norm_num) + +/-! ### Splitting a bit vector -/ + +/-- `toPoly` as a sum over the set bits, indexed by `ℕ`. -/ +lemma toPoly_eq_range {w : ℕ} (v : BitVec w) : + toPoly v = ∑ i ∈ Finset.range w, if v.toNat.testBit i then (X : (ZMod 2)[X]) ^ i else 0 := by + unfold toPoly BitVec.getLsb + rw [Fin.sum_univ_eq_sum_range + (f := fun i => if v.toNat.testBit i then (X : (ZMod 2)[X]) ^ i else 0)] + +/-- Splitting the denoted polynomial at bit position `n` into a high part carrying `X ^ n` +and a low part. -/ +lemma toPoly_split {w : ℕ} (v : BitVec w) (n : ℕ) (hn : n ≤ w) : + toPoly v + = (∑ i ∈ Finset.range (w - n), + if v.toNat.testBit (n + i) then (X : (ZMod 2)[X]) ^ i else 0) * X ^ n + + ∑ i ∈ Finset.range n, if v.toNat.testBit i then (X : (ZMod 2)[X]) ^ i else 0 := by + rw [toPoly_eq_range] + rw [← Finset.sum_range_add_sum_Ico _ hn] + rw [add_comm] + congr 1 + rw [Finset.sum_mul] + rw [Finset.sum_Ico_eq_sum_range] + apply Finset.sum_congr rfl + intro i _ + split_ifs with h + · rw [pow_add]; ring + · simp /-- Helper lemma to chain the modular squaring steps. -/ lemma chain_step {P : Polynomial (ZMod 2)} (hP : P ≠ 0) {k : ℕ} diff --git a/CompPoly/Fields/Binary/Tower/Abstract/Algebra.lean b/CompPoly/Fields/Binary/Tower/Abstract/Algebra.lean index 8d95ae65..8cc1b1ab 100644 --- a/CompPoly/Fields/Binary/Tower/Abstract/Algebra.lean +++ b/CompPoly/Fields/Binary/Tower/Abstract/Algebra.lean @@ -174,6 +174,7 @@ algebra over `BTField k`. -/ noncomputable instance : AlgebraTower (BTField) where algebraMap := towerAlgebraMap + algebraMap_self' := towerAlgebraMap_id commutes' := by intro i j h r x exact CommMonoid.mul_comm ((towerAlgebraMap i j h) r) x diff --git a/CompPoly/Fields/Binary/Tower/Concrete/Algebra.lean b/CompPoly/Fields/Binary/Tower/Concrete/Algebra.lean index 26edb49b..8f84c9e6 100644 --- a/CompPoly/Fields/Binary/Tower/Concrete/Algebra.lean +++ b/CompPoly/Fields/Binary/Tower/Concrete/Algebra.lean @@ -5,6 +5,7 @@ Authors: Chung Thai Nguyen, Quang Dao -/ module +public import CompPoly.Data.RingTheory.AlgebraTower public import CompPoly.Fields.Binary.Tower.Concrete.Field /-! @@ -204,6 +205,7 @@ algebra over `ConcreteBTField k`. -/ instance instAlgebraTowerConcreteBTF : AlgebraTower (ConcreteBTField) where algebraMap := concreteTowerAlgebraMap + algebraMap_self' := concreteTowerAlgebraMap_id commutes' := by intro i j h r x exact CommMonoid.mul_comm ((concreteTowerAlgebraMap i j h) r) x diff --git a/CompPoly/Fields/Binary/Tower/Concrete/BasisCoordinates.lean b/CompPoly/Fields/Binary/Tower/Concrete/BasisCoordinates.lean new file mode 100644 index 00000000..68a5786e --- /dev/null +++ b/CompPoly/Fields/Binary/Tower/Concrete/BasisCoordinates.lean @@ -0,0 +1,196 @@ +/- +Copyright (c) 2024 - 2025 ArkLib Contributors. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Chung Thai Nguyen, Quang Dao +-/ +module + +public import CompPoly.Fields.Binary.Tower.Concrete.Basis +public import CompPoly.Fields.Binary.Tower.Concrete.RelativeCoordinates +import CompPoly.Data.RingTheory.AlgebraTower.Basis + +/-! +# Existing multilinear basis and concrete coordinates + +The representation of `ConcreteBinaryTower.multilinearBasis` agrees at each numeric index +with `ConcreteBinaryTower.Coordinates.coordinates`. Consequently, packing a unit coefficient +vector computes the existing basis vector, and packing arbitrary coefficients is their linear +combination under the concrete tower embedding. + +The proof compares the existing basis's generator-product formula with the successor-vector +law in `AlgebraTower.natBasisVector_succ`. Both use low-first coordinate order: earlier tower +generators correspond to lower index bits. An auxiliary basis constructed with +`Module.Basis.ofEquivFun` is private to the identification proof. + +The generator-product basis and its ordering are those of [DP23], §2.3, with the +paper's generator `X_k` represented by `Z (k + 1)`. + +## References + +* [Diamond, B. E. and Posen, J., *Succinct arguments over towers of binary fields*][DP23] +-/ + +public section + +namespace ConcreteBinaryTower.Coordinates + +open AlgebraTower Module + +private noncomputable def coordinateBasis {i j : ℕ} (h : i ≤ j) : + let := ConcreteBTFieldAlgebra h + Basis (Fin (2 ^ (j - i))) (ConcreteBTField i) (ConcreteBTField j) := by + letI := ConcreteBTFieldAlgebra h + exact Basis.ofEquivFun (coordinates h) + +private theorem coordinateBasis_repr {i j : ℕ} (h : i ≤ j) (x : ConcreteBTField j) + (q : Fin (2 ^ (j - i))) : + let := ConcreteBTFieldAlgebra h + (coordinateBasis h).repr x q = coordinates h x q := rfl + +private theorem pack_single_eq_coordinateBasis {i j : ℕ} (h : i ≤ j) + (q : Fin (2 ^ (j - i))) : pack h (Pi.single q 1) = coordinateBasis h q := by + let := ConcreteBTFieldAlgebra h + apply (coordinateBasis h).repr.injective + ext r + rw [coordinateBasis_repr, congrFun (coordinates_pack h _) r, Basis.repr_self] + simp only [Pi.single_apply, Finsupp.single_apply, eq_comm] + +private theorem coordinateBasis_eq_natBasisOfLE {i j : ℕ} (h : i ≤ j) : + let := ConcreteBTFieldAlgebra h + coordinateBasis h = (natBasisOfLE succCoordinates h).reindex + (finCongr (coordinateSize_const 2 i (j - i))) := by + let := ConcreteBTFieldAlgebra h + apply Basis.eq_ofRepr_eq_repr + intro x q + rw [coordinateBasis_repr, Basis.repr_reindex_apply, natBasisOfLE_repr, + coordinates_eq_natCoordinatesConstOfLE, natCoordinatesConstOfLE_apply] + rfl + +private theorem succ_vector (k : ℕ) (q : Fin 2) : + (succCoordinates k).symm (Pi.single q 1) = Z (k + 1) ^ q.val := by + rw [succCoordinates_symm_apply, joinSucc_eq_map_mul_add] + fin_cases q + · change concreteTowerAlgebraMap k (k + 1) _ 0 * Z (k + 1) + + concreteTowerAlgebraMap k (k + 1) _ 1 = 1 + rw [map_zero, map_one, zero_mul, zero_add] + · change concreteTowerAlgebraMap k (k + 1) _ 1 * Z (k + 1) + + concreteTowerAlgebraMap k (k + 1) _ 0 = Z (k + 1) ^ 1 + rw [map_one, map_zero, one_mul, add_zero, pow_one] + +private theorem height_vector (i n : ℕ) (q : Fin (coordinateSize (fun _ => 2) i n)) : + natBasisVector succCoordinates i n q = + ∏ t : Fin n, concreteTowerAlgebraMap (i + t.val + 1) (i + n) (by omega) + (Z (i + t.val + 1) ^ Nat.getBit t.val q.val) := by + induction n with + | zero => + simp only [Finset.univ_eq_empty, Finset.prod_empty] + apply (natCoordinates succCoordinates i 0).injective + ext r + rw [natCoordinates_natBasisVector, natCoordinates_zero] + have hr : r = q := by + apply Fin.ext + have hr := r.isLt + have hq := q.isLt + simp only [coordinateSize_zero] at hr hq + omega + rw [hr, Pi.single_eq_same] + | succ n ih => + have hbit (t : Fin (n + 1)) : Nat.getBit t.val q.val = + if t.val < n then Nat.getBit t.val q.modNat.val else q.divNat.val := by + have hh := bit_revFinProdFinEquiv_symm_2_pow_succ + (j := ⟨q.val, by exact q.isLt.trans_eq (coordinateSize_const 2 i (n + 1))⟩) + (i := t) + simp only [revFinProdFinEquiv_symm_apply, Fin.val_cast, leftModNat, leftDivNat] at hh + change Nat.getBit t.val q.val = + if t.val < n then Nat.getBit t.val (q.val % coordinateSize (fun _ => 2) i n) + else q.val / coordinateSize (fun _ => 2) i n + simpa only [coordinateSize_const] using hh + have he : finProdFinEquiv (q.divNat, q.modNat) = q := + finProdFinEquiv.apply_symm_apply q + conv_lhs => rw [← he, natBasisVector_succ] + change concreteTowerAlgebraMap (i + n) (i + n + 1) _ + (natBasisVector succCoordinates i n q.modNat) * + (succCoordinates (i + n)).symm (Pi.single q.divNat 1) = _ + rw [ih, succ_vector, map_prod, Fin.prod_univ_castSucc] + apply congrArg₂ (· * ·) + · apply Finset.prod_congr rfl + intro t _ + rw [hbit] + simp only [Fin.val_castSucc, t.isLt, if_pos] + exact congrFun (congrArg DFunLike.coe + (concreteTowerAlgebraMap_assoc (i + n + 1) (i + n) (i + t.val + 1) + (by omega) (by omega))).symm _ + · rw [hbit] + simp only [Fin.val_last, lt_self_iff_false, if_false] + change Z (i + n + 1) ^ q.divNat.val = + concreteTowerAlgebraMap (i + n + 1) (i + n + 1) _ + (Z (i + n + 1) ^ q.divNat.val) + rw [concreteTowerAlgebraMap_id] + rfl + +private theorem cast_product (i n j : ℕ) (h : i + n = j) (q : ℕ) : + cast (congrArg ConcreteBTField h) + (∏ t : Fin n, concreteTowerAlgebraMap (i + t.val + 1) (i + n) (by omega) + (Z (i + t.val + 1) ^ Nat.getBit t.val q)) = + ∏ t : Fin n, concreteTowerAlgebraMap (i + t.val + 1) j (by omega) + (Z (i + t.val + 1) ^ Nat.getBit t.val q) := by + subst j + rfl + +private theorem pack_single_eq_product {i j : ℕ} (h : i ≤ j) + (q : Fin (2 ^ (j - i))) : + pack h (Pi.single q 1) = + ∏ t : Fin (j - i), concreteTowerAlgebraMap (i + t.val + 1) j (by omega) + (Z (i + t.val + 1) ^ Nat.getBit t.val q.val) := by + let := ConcreteBTFieldAlgebra h + rw [pack_single_eq_coordinateBasis, coordinateBasis_eq_natBasisOfLE, Basis.reindex_apply, + ← natBasisVectorOfLE_eq_natBasisOfLE, natBasisVectorOfLE_eq_natBasisVector, + height_vector, cast_product _ _ _ (Nat.add_sub_of_le h)] + rfl + +private theorem coordinateBasis_eq_multilinearBasis {i j : ℕ} (h : i ≤ j) : + let := ConcreteBTFieldAlgebra h + coordinateBasis h = multilinearBasis i j h := by + let := ConcreteBTFieldAlgebra h + apply Basis.eq_of_apply_eq + intro q + rw [← pack_single_eq_coordinateBasis, pack_single_eq_product, multilinearBasis_apply] + rfl + +/-- The coefficient at numeric index `q` in the multilinear basis is the executable coordinate +at the same index, for the scalar action induced by the concrete tower embedding. -/ +theorem multilinearBasis_repr {i j : ℕ} (h : i ≤ j) (x : ConcreteBTField j) + (q : Fin (2 ^ (j - i))) : + let := ConcreteBTFieldAlgebra h + (multilinearBasis i j h).repr x q = coordinates h x q := by + let := ConcreteBTFieldAlgebra h + rw [← coordinateBasis_eq_multilinearBasis, coordinateBasis_repr] + +/-- The multilinear basis vector at numeric index `q` has coordinate one at `q` +and coordinate zero at every other index. -/ +@[simp] theorem coordinates_multilinearBasis {i j : ℕ} (h : i ≤ j) + (q : Fin (2 ^ (j - i))) : + coordinates h (multilinearBasis i j h q) = Pi.single q 1 := by + let := ConcreteBTFieldAlgebra h + ext r + rw [← multilinearBasis_repr, Basis.repr_self] + simp only [Pi.single_apply, Finsupp.single_apply, eq_comm] + +/-- Packing a unit coefficient vector computes the multilinear basis vector at the same +numeric index. -/ +theorem pack_single_eq_multilinearBasis {i j : ℕ} (h : i ≤ j) + (q : Fin (2 ^ (j - i))) : + pack h (Pi.single q 1) = multilinearBasis i j h q := by + rw [pack_single_eq_coordinateBasis, coordinateBasis_eq_multilinearBasis] + +/-- Packing is the sum of the multilinear basis vectors multiplied by their +coefficients embedded from level `i` into level `j` through the concrete tower map. -/ +theorem pack_eq_sum_multilinearBasis {i j : ℕ} (h : i ≤ j) + (c : Fin (2 ^ (j - i)) → ConcreteBTField i) : + pack h c = ∑ q, concreteTowerAlgebraMap i j h (c q) * multilinearBasis i j h q := by + let := ConcreteBTFieldAlgebra h + have hsum := ((multilinearBasis i j h).sum_repr (pack h c)).symm + simpa only [multilinearBasis_repr, coordinates_pack, Algebra.smul_def, + algebraMap_ConcreteBTFieldAlgebra_def] using hsum + +end ConcreteBinaryTower.Coordinates diff --git a/CompPoly/Fields/Binary/Tower/Concrete/Coordinates.lean b/CompPoly/Fields/Binary/Tower/Concrete/Coordinates.lean new file mode 100644 index 00000000..acb81316 --- /dev/null +++ b/CompPoly/Fields/Binary/Tower/Concrete/Coordinates.lean @@ -0,0 +1,146 @@ +/- +Copyright (c) 2024 - 2025 ArkLib Contributors. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Chung Thai Nguyen, Quang Dao +-/ +module + +public import CompPoly.Fields.Binary.Tower.Concrete.Algebra + +/-! +# Successor coordinates for concrete binary tower fields + +`ConcreteBinaryTower.Coordinates.succCoordinates k` identifies level `k + 1` with two +coefficients in level `k`, ordered as the constant coefficient followed by the coefficient +of the new generator `Z (k + 1)`. Its scalar action is induced by the existing tower map +from level `k` to level `k + 1`. + +The underlying `split` and `join` functions use high/low order. The executable `low`, `high`, +and `joinSucc` adapters expose low/high order and their bodies for direct word reduction. +The linear equivalence is characterized by its application and inverse-application lemmas. +The decomposition and linearity proofs use the existing algebraic expansion lemmas in +`CompPoly.Fields.Binary.Tower.Concrete.Algebra`. + +These successor equivalences can be passed to `AlgebraTower.natCoordinatesOfLE` from +`CompPoly.Data.RingTheory.AlgebraTower.Coordinates` to obtain coordinates between any ordered +pair of levels. + +The tower and constant-first successor expansion are described in [DP23], §2.3. +The paper's generator `X_k` corresponds to `Z (k + 1)` here. + +## References + +* [Diamond, B. E. and Posen, J., *Succinct arguments over towers of binary fields*][DP23] +-/ + +public section + +namespace ConcreteBinaryTower.Coordinates + +/-- The constant coefficient of a level-`k + 1` word, stored in its low `2 ^ k` bits. -/ +@[expose] def low (k : ℕ) (x : ConcreteBTField (k + 1)) : ConcreteBTField k := + (split (by omega) x).2 + +/-- The coefficient of `Z (k + 1)`, stored in the high `2 ^ k` bits of the word. -/ +@[expose] def high (k : ℕ) (x : ConcreteBTField (k + 1)) : ConcreteBTField k := + (split (by omega) x).1 + +/-- Construct a level-`k + 1` word with constant coefficient `lo` and generator coefficient `hi`. -/ +@[expose] def joinSucc (k : ℕ) (lo hi : ConcreteBTField k) : ConcreteBTField (k + 1) := + join (by omega) hi lo + +/-- Reading the low coefficient after joining recovers it. -/ +@[simp] theorem low_joinSucc (k : ℕ) (lo hi : ConcreteBTField k) : + low k (joinSucc k lo hi) = lo := + congrArg Prod.snd (split_join_eq_split (k := k + 1) (by omega) hi lo) + +/-- Reading the high coefficient after joining recovers it. -/ +@[simp] theorem high_joinSucc (k : ℕ) (lo hi : ConcreteBTField k) : + high k (joinSucc k lo hi) = hi := + congrArg Prod.fst (split_join_eq_split (k := k + 1) (by omega) hi lo) + +/-- Joining the two coefficients recovers the original word. -/ +@[simp] theorem joinSucc_low_high (k : ℕ) (x : ConcreteBTField (k + 1)) : + joinSucc k (low k x) (high k x) = x := + join_split_eq_join (by omega) x + +/-- The low coefficient preserves the canonical field addition. -/ +theorem low_add (k : ℕ) (x y : ConcreteBTField (k + 1)) : + low k (x + y) = low k x + low k y := + congrArg Prod.snd (split_sum_eq_sum_split (k := k + 1) (by omega) + x y (high k x) (low k x) (high k y) (low k y) rfl rfl) + +/-- The high coefficient preserves the canonical field addition. -/ +theorem high_add (k : ℕ) (x y : ConcreteBTField (k + 1)) : + high k (x + y) = high k x + high k y := + congrArg Prod.fst (split_sum_eq_sum_split (k := k + 1) (by omega) + x y (high k x) (low k x) (high k y) (low k y) rfl rfl) + +/-- A joined word is the sum of its embedded constant and generator terms. -/ +theorem joinSucc_eq_map_mul_add (k : ℕ) (lo hi : ConcreteBTField k) : + joinSucc k lo hi = + concreteTowerAlgebraMap k (k + 1) (Nat.le_succ k) hi * Z (k + 1) + + concreteTowerAlgebraMap k (k + 1) (Nat.le_succ k) lo := by + rw [joinSucc, join_eq_join_via_add_smul] + rfl + +/-- The action induced by the tower embedding multiplies both coefficients by the scalar. -/ +theorem smul_joinSucc (k : ℕ) (a lo hi : ConcreteBTField k) : + letI := ConcreteBTFieldAlgebra (Nat.le_succ k) + a • joinSucc k lo hi = joinSucc k (a * lo) (a * hi) := by + let := ConcreteBTFieldAlgebra (Nat.le_succ k) + change concreteTowerAlgebraMap k (k + 1) (Nat.le_succ k) a * joinSucc k lo hi = _ + simp only [joinSucc_eq_map_mul_add, map_mul, mul_add, mul_assoc] + +/-- The constant coefficient commutes with the scalar action induced by the tower embedding. -/ +theorem low_smul (k : ℕ) (a : ConcreteBTField k) (x : ConcreteBTField (k + 1)) : + letI := ConcreteBTFieldAlgebra (Nat.le_succ k) + low k (a • x) = a * low k x := by + let := ConcreteBTFieldAlgebra (Nat.le_succ k) + conv_lhs => rw [← joinSucc_low_high k x, smul_joinSucc, low_joinSucc] + +/-- The generator coefficient commutes with the scalar action induced by the tower embedding. -/ +theorem high_smul (k : ℕ) (a : ConcreteBTField k) (x : ConcreteBTField (k + 1)) : + letI := ConcreteBTFieldAlgebra (Nat.le_succ k) + high k (a • x) = a * high k x := by + let := ConcreteBTFieldAlgebra (Nat.le_succ k) + conv_lhs => rw [← joinSucc_low_high k x, smul_joinSucc, high_joinSucc] + +/-- Coordinates `(constant coefficient, generator coefficient)` for the scalar action +induced by the tower embedding from level `k` to level `k + 1`. -/ +def succCoordinates (k : ℕ) : + letI := ConcreteBTFieldAlgebra (Nat.le_succ k) + ConcreteBTField (k + 1) ≃ₗ[ConcreteBTField k] (Fin 2 → ConcreteBTField k) := by + letI := ConcreteBTFieldAlgebra (Nat.le_succ k) + exact { + toFun := fun x => ![low k x, high k x] + invFun := fun c => joinSucc k (c 0) (c 1) + map_add' := fun x y => by + ext j + fin_cases j + · exact low_add k x y + · exact high_add k x y + map_smul' := fun a x => by + ext j + fin_cases j + · exact low_smul k a x + · exact high_smul k a x + left_inv := fun x => joinSucc_low_high k x + right_inv := fun c => by + ext j + fin_cases j + · exact low_joinSucc k (c 0) (c 1) + · exact high_joinSucc k (c 0) (c 1) } + +/-- The forward function is exactly low/high readback, in that order. -/ +@[simp] theorem succCoordinates_apply (k : ℕ) (x : ConcreteBTField (k + 1)) : + succCoordinates k x = ![low k x, high k x] := by + rfl + +/-- The inverse constructs a word with constant coefficient `c 0` and generator coefficient +`c 1`. -/ +@[simp] theorem succCoordinates_symm_apply (k : ℕ) (c : Fin 2 → ConcreteBTField k) : + (succCoordinates k).symm c = joinSucc k (c 0) (c 1) := by + rfl + +end ConcreteBinaryTower.Coordinates diff --git a/CompPoly/Fields/Binary/Tower/Concrete/Core.lean b/CompPoly/Fields/Binary/Tower/Concrete/Core.lean index 10e1575a..3a6893e1 100644 --- a/CompPoly/Fields/Binary/Tower/Concrete/Core.lean +++ b/CompPoly/Fields/Binary/Tower/Concrete/Core.lean @@ -6,7 +6,7 @@ Authors: Chung Thai Nguyen, Quang Dao module public import CompPoly.Data.Classes.DCast -public import CompPoly.Fields.Binary.Tower.Abstract.Basis +public import CompPoly.Fields.Binary.Tower.Support.DefiningPoly /-! # Concrete Binary Tower Core diff --git a/CompPoly/Fields/Binary/Tower/Concrete/Field.lean b/CompPoly/Fields/Binary/Tower/Concrete/Field.lean index 28e7c5c9..e9402f0c 100644 --- a/CompPoly/Fields/Binary/Tower/Concrete/Field.lean +++ b/CompPoly/Fields/Binary/Tower/Concrete/Field.lean @@ -6,11 +6,18 @@ Authors: Chung Thai Nguyen, Quang Dao module public import CompPoly.Fields.Binary.Tower.Concrete.Core +public import CompPoly.Fields.Binary.Tower.Support.IrreducibilityAndTraceMapProperty + +import Mathlib.Algebra.QuadraticAlgebra.Basic +import Mathlib.Algebra.Polynomial.Degree.SmallDegree /-! # Concrete Binary Tower Field Field-structure lemmas for successive levels of the concrete binary tower. + +Norm nonvanishing is certified using `QuadraticAlgebra.norm_eq_zero_iff_eq_zero` over the +predecessor field. This argument does not assume a field structure on the successor level. -/ @[expose] public section @@ -61,6 +68,30 @@ theorem concrete_mul_eq rw [add_comm (b:=a₁ * b₁), ←add_assoc, ←add_assoc, add_self_cancel, zero_add] · rfl +/-- Splitting recursive multiplication gives the high and low coefficients of the quadratic +product. -/ +private lemma split_concrete_mul + (prevBTFieldProps : ConcreteBTFieldProps (k := k - 1)) (a b : ConcreteBTField k) : + let a₁ := (split h_k a).1 + let a₀ := (split h_k a).2 + let b₁ := (split h_k b).1 + let b₀ := (split h_k b).2 + split h_k (concrete_mul a b) = + (concrete_mul a₀ b₁ + concrete_mul b₀ a₁ + + concrete_mul (concrete_mul a₁ b₁) (Z (k - 1)), + concrete_mul a₀ b₀ + concrete_mul a₁ b₁) := by + dsimp only + rw [concrete_mul_eq prevBTFieldProps a b + (a₁ := (split h_k a).1) (a₀ := (split h_k a).2) + (b₁ := (split h_k b).1) (b₀ := (split h_k b).2) (h_a := rfl) (h_b := rfl)] + exact split_join_eq_split h_k _ _ + +/-- Splitting a sum adds its high halves and its low halves separately. -/ +private lemma split_add (a b : ConcreteBTField k) : + split h_k (a + b) = + ((split h_k a).1 + (split h_k b).1, (split h_k a).2 + (split h_k b).2) := + split_sum_eq_sum_split h_k a b _ _ _ _ rfl rfl + lemma concrete_zero_mul (prevBTFieldProps : ConcreteBTFieldProps (k := k - 1)) (a : ConcreteBTField k) : concrete_mul (zero (k:=k)) a = zero (k:=k) := by @@ -230,121 +261,29 @@ lemma concrete_mul_comm rw [add_comm (a:= a₀ * b₁) (b:= b₀ * a₁)] simp only [and_self] +/-- The recursive multiplication is associative at a positive level whenever the predecessor +level satisfies its field laws. -/ lemma concrete_mul_assoc {h_k : k > 0} (prevBTFieldProps : ConcreteBTFieldProps (k := k - 1)) (a b c : ConcreteBTField k) : concrete_mul (concrete_mul a b) c = concrete_mul a (concrete_mul b c) := by let : Field (ConcreteBTField (k - 1)) := mkFieldInstance prevBTFieldProps - have hmul : ∀ (a b : ConcreteBTField (k - 1)), concrete_mul a b = a * b := fun a b => rfl - by_cases h_k_zero : k = 0 - · linarith - · -- Inductive case : k > 0 - -- Approach : utilize concrete_mul_eq of level (k - 1) - -- ⊢ concrete_mul (concrete_mul a b) c = concrete_mul a (concrete_mul b c) - let p1 := split h_k a - let p2 := split h_k b - let p3 := split h_k c - let a₁ := p1.fst - let a₀ := p1.snd - let b₁ := p2.fst - let b₀ := p2.snd - let c₁ := p3.fst - let c₀ := p3.snd - have h_split_a : split h_k a = (a₁, a₀) := by rfl - have h_split_b : split h_k b = (b₁, b₀) := by rfl - have h_split_c : split h_k c = (c₁, c₀) := by rfl - have h_a₁_a₀ : a = 《 a₁, a₀ 》 := by exact (join_of_split h_k a a₁ a₀) h_split_a - have h_b₁_b₀ : b = 《 b₁, b₀ 》 := by exact (join_of_split h_k b b₁ b₀) h_split_b - have h_c₁_c₀ : c = 《 c₁, c₀ 》 := by exact (join_of_split h_k c c₁ c₀) h_split_c - -- ⊢ concrete_mul (concrete_mul a b) c = concrete_mul a (concrete_mul b c) - have a_mul_b_eq := concrete_mul_eq prevBTFieldProps (h_k:=h_k) (a:=a) (b:=b) (a₁:=a₁) - (a₀:=a₀) (b₁:=b₁) (b₀:=b₀) (h_a:=h_split_a) (h_b:=h_split_b) - have b_mul_c_eq := concrete_mul_eq prevBTFieldProps (h_k:=h_k) (a:=b) (b:=c) (a₁:=b₁) - (a₀:=b₀) (b₁:=c₁) (b₀:=c₀) (h_a:=h_split_b) (h_b:=h_split_c) - set ab₁ := concrete_mul a₀ b₁ + concrete_mul b₀ a₁ - + concrete_mul (concrete_mul a₁ b₁) (Z (k - 1)) - set ab₀ := concrete_mul a₀ b₀ + concrete_mul a₁ b₁ - have h_split_a_mul_b : split h_k (concrete_mul a b) = (ab₁, ab₀) := by - exact (split_of_join h_k (concrete_mul a b) ab₁ ab₀ a_mul_b_eq).symm - set bc₁ := concrete_mul b₀ c₁ + concrete_mul c₀ b₁ - + concrete_mul (concrete_mul b₁ c₁) (Z (k - 1)) - set bc₀ := concrete_mul b₀ c₀ + concrete_mul b₁ c₁ - have h_split_b_mul_c : split h_k (concrete_mul b c) = (bc₁, bc₀) := by - exact (split_of_join h_k (concrete_mul b c) bc₁ bc₀ b_mul_c_eq).symm - - set ab := concrete_mul a b - set bc := concrete_mul b c - -- rw [a_mul_b_eq, b_mul_c_eq] - -- ⊢ concrete_mul ab c = concrete_mul a bc - have a_mul_bc_eq := concrete_mul_eq prevBTFieldProps (h_k:=h_k) (a:=a) (b:=bc) (a₁:=a₁) - (a₀:=a₀) (b₁:=bc₁) (b₀:=bc₀) (h_a:=h_split_a) (h_b:=h_split_b_mul_c.symm) - have ab_mul_c_eq := concrete_mul_eq prevBTFieldProps (h_k:=h_k) (a:=ab) (b:=c) (a₁:=ab₁) - (a₀:=ab₀) (b₁:=c₁) (b₀:=c₀) (h_a:=h_split_a_mul_b.symm) (h_b:=h_split_c) - set a_bc₁ := concrete_mul a₀ bc₁ + concrete_mul bc₀ a₁ - + concrete_mul (concrete_mul a₁ bc₁) (Z (k - 1)) - set a_bc₀ := concrete_mul a₀ bc₀ + concrete_mul a₁ bc₁ - have h_split_a_bc : split h_k (concrete_mul a bc) = (a_bc₁, a_bc₀) := by - exact (split_of_join h_k (concrete_mul a bc) a_bc₁ a_bc₀ a_mul_bc_eq).symm - set ab_c₁ := concrete_mul ab₀ c₁ + concrete_mul c₀ ab₁ - + concrete_mul (concrete_mul ab₁ c₁) (Z (k - 1)) - set ab_c₀ := concrete_mul ab₀ c₀ + concrete_mul ab₁ c₁ - have h_split_ab_c : split h_k (concrete_mul ab c) = (ab_c₁, ab_c₀) := by - exact (split_of_join h_k (concrete_mul ab c) ab_c₁ ab_c₀ ab_mul_c_eq).symm - - rw [a_mul_bc_eq, ab_mul_c_eq] -- convert concrete mul to join - rw [join_eq_join_iff] - -- ⊢ ab_c₁ = a_bc₁ ∧ ab_c₀ = a_bc₀ - unfold a_bc₁ ab_c₁ ab_c₀ a_bc₀ ab₀ ab₁ bc₀ bc₁ -- unfold all - simp_rw [hmul] - ring_nf - simp only [and_self] + have hmul : ∀ (x y : ConcreteBTField (k - 1)), concrete_mul x y = x * y := fun _ _ => rfl + apply (eq_iff_split_eq h_k _ _).mpr + simp only [split_concrete_mul prevBTFieldProps, hmul, Prod.mk.injEq] + constructor <;> ring +/-- At a positive level, recursive multiplication distributes over addition in its second +argument whenever the predecessor level satisfies its field laws. -/ lemma concrete_mul_left_distrib {h_k : k > 0} (prevBTFieldProps : ConcreteBTFieldProps (k := k - 1)) (a b c : ConcreteBTField k) : concrete_mul a (b + c) = concrete_mul a b + concrete_mul a c := by let : Field (ConcreteBTField (k - 1)) := mkFieldInstance prevBTFieldProps - have hmul : ∀ (a b : ConcreteBTField (k - 1)), concrete_mul a b = a * b := fun a b => rfl - by_cases h_k_zero : k = 0 - · linarith - · -- Inductive case : k > 0 - -- Approach : utilize concrete_mul_eq of level (k - 1) - -- ⊢ concrete_mul (concrete_mul a b) c = concrete_mul a (concrete_mul b c) - let p1 := split h_k a - let p2 := split h_k b - let p3 := split h_k c - let a₁ := p1.fst - let a₀ := p1.snd - let b₁ := p2.fst - let b₀ := p2.snd - let c₁ := p3.fst - let c₀ := p3.snd - have h_split_a : split h_k a = (a₁, a₀) := by rfl - have h_split_b : split h_k b = (b₁, b₀) := by rfl - have h_split_c : split h_k c = (c₁, c₀) := by rfl - have h_a₁_a₀ : a = 《 a₁, a₀ 》 := by exact (join_of_split h_k a a₁ a₀) h_split_a - have h_b₁_b₀ : b = 《 b₁, b₀ 》 := by exact (join_of_split h_k b b₁ b₀) h_split_b - have h_c₁_c₀ : c = 《 c₁, c₀ 》 := by exact (join_of_split h_k c c₁ c₀) h_split_c - have h_split_b_add_c : split h_k (b + c) = (b₁ + c₁, b₀ + c₀) := by - exact split_sum_eq_sum_split h_k (x₀:=b) (x₁:=c) (hi₀:=b₁) (lo₀:=b₀) - (hi₁:=c₁) (lo₁:=c₀) (h_split_x₀:=h_split_b) (h_split_x₁:=h_split_c) - -- ⊢ concrete_mul a (b + c) = concrete_mul a b + concrete_mul a c - conv => - lhs - -- rewrite a * (b + c) - rw [concrete_mul_eq prevBTFieldProps (h_k:=h_k) (a:=a) (b:=b + c) (a₁:=a₁) - (a₀:=a₀) (b₁:=b₁ + c₁) (b₀:=b₀ + c₀) (h_a:=h_split_a) (h_b:=h_split_b_add_c.symm)] - conv => - rhs - rw [concrete_mul_eq prevBTFieldProps (h_k:=h_k) (a:=a) (b:=b) (a₁:=a₁) - (a₀:=a₀) (b₁:=b₁) (b₀:=b₀) (h_a:=h_split_a) (h_b:=h_split_b)] - rw [concrete_mul_eq prevBTFieldProps (h_k:=h_k) (a:=a) (b:=c) (a₁:=a₁) - (a₀:=a₀) (b₁:=c₁) (b₀:=c₀) (h_a:=h_split_a) (h_b:=h_split_c)] - simp_rw [hmul] - rw [join_add_join] - rw [join_eq_join_iff] - ring_nf - simp only [and_self] + have hmul : ∀ (x y : ConcreteBTField (k - 1)), concrete_mul x y = x * y := fun _ _ => rfl + apply (eq_iff_split_eq h_k _ _).mpr + simp only [split_concrete_mul prevBTFieldProps, split_add, hmul, Prod.mk.injEq] + constructor <;> ring lemma concrete_mul_right_distrib {h_k : k > 0} (prevBTFieldProps : ConcreteBTFieldProps (k := k - 1)) @@ -355,85 +294,49 @@ lemma concrete_mul_right_distrib rw [concrete_mul_comm prevBTFieldProps (h_k:=h_k) (a:=b) (b:=c)] exact concrete_mul_left_distrib prevBTFieldProps (h_k:=h_k) (a:=c) (b:=a) (c:=b) +private lemma quadratic_no_root_of_irreducible {F : Type*} [Field F] {z : F} + (h : Irreducible (definingPoly z)) : ∀ r : F, r ^ 2 ≠ -1 + z * r := by + have hdeg : (definingPoly z).natDegree ≠ 1 := by + have hquad : (definingPoly z).natDegree = 2 := by + simpa only [definingPoly, C_1, one_mul] using + Polynomial.natDegree_quadratic (a := (1 : F)) (b := z) (c := 1) one_ne_zero + rw [hquad] + decide + intro r hr + apply h.not_isRoot_of_natDegree_ne_one hdeg (x := -r) + simp only [Polynomial.IsRoot.def, definingPoly, eval_add, eval_pow, eval_X, eval_mul, + eval_C, eval_one] + linear_combination hr + +/-- For a nonzero element at a positive level, `lo * (lo + hi * z) + hi * hi` is nonzero +in the predecessor field, where `lo` and `hi` are its low and high halves and `z = Z (k - 1)`. +This is the denominator in the quadratic inverse formula. -/ lemma norm_of_ne_zero_is_ne_zero {k : ℕ} {h_k_gt_0 : k > 0} (prevBTFieldResult : ConcreteBTFStepResult (k := k - 1)) (a : ConcreteBTField k) (h_a_ne_zero : a ≠ 0) : let a₁ := (split h_k_gt_0 a).1 let a₀ := (split h_k_gt_0 a).2 concrete_mul a₀ (a₀ + concrete_mul a₁ (Z (k - 1))) + concrete_mul a₁ a₁ ≠ 0 := by - let : Field (ConcreteBTField (k - 1)) := mkFieldInstance prevBTFieldResult.toConcreteBTFieldProps - have hmul : ∀ (a b : ConcreteBTField (k - 1)), concrete_mul a b = a * b := fun a b => rfl - -- Set up local variables for convenience - set a₁ := (split h_k_gt_0 a).1 - set a₀ := (split h_k_gt_0 a).2 - simp_rw [hmul] - rw [left_distrib] - have ha : a = 《a₁, a₀》 := by - apply (join_of_split h_k_gt_0 a a₁ a₀) rfl - set Na := a₀*a₀ + a₀*(a₁*Z (k - 1)) + a₁*a₁ -- ⊢ Na ≠ 0 - -- Main proof by contradiction - by_contra h_Na_is_zero - by_cases h_a₁_zero : a₁ = 0 - · -- Case 1 : a₁ = 0 - have h_a₀_ne_zero : a₀ ≠ 0 := by - intro h_a₀_zero - have h_a_is_zero : a = 0 := by - rw [ha, h_a₁_zero, h_a₀_zero] - rw! [←zero_is_0, ←zero_is_0, join_zero_zero] - rfl - exact h_a_ne_zero h_a_is_zero - have h_Na_eq_a₀_sq : Na = a₀ * a₀ := by - simp only [Na, Z, h_a₁_zero, mul_zero, add_zero, zero_mul] - rw [h_Na_eq_a₀_sq] at h_Na_is_zero -- h_Na_is_zero : a₀ * a₀ = 0 - -- In a field, a₀ * a₀ = 0 implies a₀ = 0. - have h_a₀_is_zero_from_mul := (mul_self_eq_zero).mp h_Na_is_zero - -- This contradicts our proof that a₀ is non-zero. - exact h_a₀_ne_zero h_a₀_is_zero_from_mul - · -- Case 2 : a₁ ≠ 0 - -- Since a₁ is a non-zero element of a field, its inverse exists. - set a₁_inv := a₁⁻¹ - set r := a₀ * a₁_inv - -- We have Na = 0. The goal is to manipulate this equation to show - -- that it implies the defining polynomial has a root in the base field. - have h_root : r*r + r*Z (k - 1) + 1 = 0 := by - have h_manip : (a₁_inv * a₁_inv) * Na = 0 := by rw [h_Na_is_zero, mul_zero] - rw [show Na = a₀*a₀ + (a₀*a₁)*Z (k - 1) + a₁*a₁ by { simp [Na]; ring }] at h_manip - rw [left_distrib, left_distrib] at h_manip - rw [h_manip.symm] - have h1 : r * r = a₁_inv * a₁_inv * (a₀ * a₀) := by ring - have h2 : r * Z (k - 1) = a₁_inv * a₁_inv * (a₀ * a₁ * Z (k - 1)) := by - apply Eq.symm - -- ⊢ a₁_inv * a₁_inv * (a₀ * a₁ * Z (k - 1)) = r * Z (k - 1) - calc _ = (a₁ * a₁_inv) * (a₀ * a₁_inv) * Z (k - 1) := by ring - _ = (a₀ * a₁_inv) * Z (k - 1) := by - rw [mul_inv_cancel₀ (a:=a₁) (by omega)]; norm_num - _ = _ := by rfl - have h3 : a₁_inv * a₁_inv * (a₁ * a₁) = 1 := by - calc _ = a₁_inv * (a₁_inv * a₁) * a₁ := by ring - _ = a₁_inv * a₁ * (a₁_inv * a₁) := by ring - _ = (a₁ * a₁_inv) * (a₁ * a₁_inv) := by ring - _ = 1 := by rw [mul_inv_cancel₀ (a:=a₁) (by omega)]; norm_num - rw [h1, h2, h3] - have h_is_root : (X^2 + C (Z (k - 1)) * X + 1).eval (r) = 0 := by - simp only [pow_two, eval_add, eval_mul, eval_X, eval_C, eval_one, ←h_root] - ring - -- A polynomial that has a root in its base field cannot be irreducible. - have h_not_irreducible : ¬ Irreducible (X^2 + C (Z (k - 1)) * X + 1) := by - apply not_irreducible_of_isRoot_of_degree_gt_one (X^2 + C (Z (k - 1)) * X + 1) - · use r - simp only [IsRoot.def, eval_add, eval_pow, eval_X, eval_mul, eval_C, eval_one] - rw [mul_comm, pow_two] - exact h_root - · let := prevBTFieldResult.instFintype - have h_deg := degree_definingPoly (s:=Z (k - 1)) - unfold definingPoly at h_deg - rw [h_deg]; norm_num - - -- This gives our final contradiction, because our field extension requires - -- the defining polynomial to be irreducible. - have h:= prevBTFieldResult.instIrreduciblePoly - unfold definingPoly at h - exact h_not_irreducible h + let : Field (ConcreteBTField (k - 1)) := + mkFieldInstance prevBTFieldResult.toConcreteBTFieldProps + let : Fact (∀ r : ConcreteBTField (k - 1), r ^ 2 ≠ -1 + Z (k - 1) * r) := + ⟨quadratic_no_root_of_irreducible prevBTFieldResult.instIrreduciblePoly⟩ + let lo := (split h_k_gt_0 a).2 + let hi := (split h_k_gt_0 a).1 + let x : QuadraticAlgebra (ConcreteBTField (k - 1)) (-1) (Z (k - 1)) := ⟨lo, hi⟩ + have hx : x ≠ 0 := by + intro h + have hlo : (split h_k_gt_0 a).2 = 0 := congrArg QuadraticAlgebra.re h + have hhi : (split h_k_gt_0 a).1 = 0 := congrArg QuadraticAlgebra.im h + apply h_a_ne_zero + rw [← join_split_eq_join h_k_gt_0 a, hhi, hlo] + exact join_zero_zero h_k_gt_0 + have hnorm : x.norm = lo * (lo + hi * Z (k - 1)) + hi * hi := by + change lo * lo + Z (k - 1) * lo * hi - (-1) * hi * hi = _ + ring + change lo * (lo + hi * Z (k - 1)) + hi * hi ≠ 0 + rw [← hnorm] + exact fun h => hx (QuadraticAlgebra.norm_eq_zero_iff_eq_zero.mp h) lemma concrete_mul_inv_cancel (prevBTFieldResult : ConcreteBTFStepResult (k := k - 1)) diff --git a/CompPoly/Fields/Binary/Tower/Concrete/RelativeCoordinates.lean b/CompPoly/Fields/Binary/Tower/Concrete/RelativeCoordinates.lean new file mode 100644 index 00000000..51e8a129 --- /dev/null +++ b/CompPoly/Fields/Binary/Tower/Concrete/RelativeCoordinates.lean @@ -0,0 +1,239 @@ +/- +Copyright (c) 2024 - 2025 ArkLib Contributors. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Chung Thai Nguyen, Quang Dao +-/ +module + +public import CompPoly.Fields.Binary.Tower.Concrete.Coordinates +public import CompPoly.Data.RingTheory.AlgebraTower.Coordinates + +/-! +# Relative coordinates for concrete binary tower fields + +`ConcreteBinaryTower.Coordinates.coordinates h` identifies level `j` with +`2 ^ (j - i)` coefficients in level `i`, for `h : i ≤ j`. The scalar action is induced by +the existing concrete tower embedding. Coordinate `q` is the block of `2 ^ i` bits starting +at bit `2 ^ i * q`; blocks and bits within each block retain their numeric order. + +The equivalence composes `succCoordinates` through `AlgebraTower.natCoordinatesConstOfLE`. +Its inverse `pack` reconstructs the original word. The readback theorems relate the algebraic +coordinates to bitvector slicing and natural-number bit blocks. They refer to raw stored words, +which differ from field numerals in characteristic two. + +The coefficient ordering follows the relative multilinear basis of [DP23], §2.3: +earlier tower generators correspond to lower index bits. + +## References + +* [Diamond, B. E. and Posen, J., *Succinct arguments over towers of binary fields*][DP23] +-/ + +public section + +namespace ConcreteBinaryTower.Coordinates + +open AlgebraTower + +/-- Coordinates of level `j` over level `i`, ordered by increasing bit-block offset, for the +scalar action induced by the concrete tower embedding associated with `h`. -/ +def coordinates {i j : ℕ} (h : i ≤ j) : + letI := ConcreteBTFieldAlgebra h + ConcreteBTField j ≃ₗ[ConcreteBTField i] (Fin (2 ^ (j - i)) → ConcreteBTField i) := + natCoordinatesConstOfLE succCoordinates h + +/-- The relative equivalence is the constant-two composition of the low-first successor +coordinate equivalences, with the scalar action induced by the concrete tower embedding. -/ +theorem coordinates_eq_natCoordinatesConstOfLE {i j : ℕ} (h : i ≤ j) : + coordinates h = natCoordinatesConstOfLE succCoordinates h := by + rfl + +/-- Construct a level-`j` word from level-`i` coefficients, with coefficient `q` occupying +the block of `2 ^ i` bits starting at bit `2 ^ i * q`. -/ +def pack {i j : ℕ} (h : i ≤ j) (c : Fin (2 ^ (j - i)) → ConcreteBTField i) : + ConcreteBTField j := (coordinates h).symm c + +/-- Reading the coordinates of a packed word recovers every input coefficient. -/ +@[simp] theorem coordinates_pack {i j : ℕ} (h : i ≤ j) + (c : Fin (2 ^ (j - i)) → ConcreteBTField i) : coordinates h (pack h c) = c := + (coordinates h).apply_symm_apply c + +/-- Packing all coordinates recovers the original word. -/ +@[simp] theorem pack_coordinates {i j : ℕ} (h : i ≤ j) (x : ConcreteBTField j) : + pack h (coordinates h x) = x := (coordinates h).symm_apply_apply x + +/-- At equal endpoints the single coordinate is the original word. -/ +@[simp] theorem coordinates_self (i : ℕ) (h : i ≤ i) (x : ConcreteBTField i) + (q : Fin (2 ^ (i - i))) : coordinates h x q = x := + natCoordinatesConstOfLE_self succCoordinates i x q + +/-- Packing at equal endpoints returns the single coefficient. -/ +@[simp] theorem pack_self (i : ℕ) (h : i ≤ i) (c : Fin (2 ^ (i - i)) → ConcreteBTField i) : + pack h c = c 0 := + (coordinates_self i h (pack h c) 0).symm.trans (congrFun (coordinates_pack h c) 0) + +/-- Coordinates preserve the canonical field addition, coefficient by coefficient. -/ +theorem coordinates_add {i j : ℕ} (h : i ≤ j) (x y : ConcreteBTField j) : + coordinates h (x + y) = coordinates h x + coordinates h y := + (coordinates h).map_add x y + +/-- Packing preserves coefficientwise canonical field addition. -/ +theorem pack_add {i j : ℕ} (h : i ≤ j) + (c d : Fin (2 ^ (j - i)) → ConcreteBTField i) : + pack h (c + d) = pack h c + pack h d := (coordinates h).symm.map_add c d + +/-- The tower embedding's scalar action multiplies each coordinate by the lower-level scalar. -/ +theorem coordinates_smul {i j : ℕ} (h : i ≤ j) (a : ConcreteBTField i) + (x : ConcreteBTField j) (q : Fin (2 ^ (j - i))) : + letI := ConcreteBTFieldAlgebra h + coordinates h (a • x) q = a * coordinates h x q := + congrFun ((coordinates h).map_smul a x) q + +/-- Scaling all coefficients commutes with packing under the tower embedding's scalar action. -/ +theorem pack_smul {i j : ℕ} (h : i ≤ j) (a : ConcreteBTField i) + (c : Fin (2 ^ (j - i)) → ConcreteBTField i) : + letI := ConcreteBTFieldAlgebra h + pack h (fun q => a * c q) = a • pack h c := + (coordinates h).symm.map_smul a c + +private theorem low_eq_setWidth (k : ℕ) (x : ConcreteBTField (k + 1)) : + low k x = x.setWidth (2 ^ k) := by + apply BitVec.eq_of_toNat_eq + simp only [low, split, ← BitVec.dcast_bitvec_toNat_eq, + BitVec.extractLsb, BitVec.extractLsb', BitVec.toNat_ofNat, BitVec.toNat_setWidth, + Nat.add_one_sub_one, Nat.sub_zero, Nat.shiftRight_zero] + rw [Nat.sub_add_cancel ((Nat.one_le_two_pow : 1 ≤ 2 ^ k))] + +private theorem high_eq_setWidth (k : ℕ) (x : ConcreteBTField (k + 1)) : + high k x = (BitVec.ushiftRight x (2 ^ k)).setWidth (2 ^ k) := by + apply BitVec.eq_of_toNat_eq + simp only [high, split, ← BitVec.dcast_bitvec_toNat_eq, + BitVec.extractLsb, BitVec.extractLsb', BitVec.toNat_ofNat, BitVec.toNat_setWidth, + Nat.add_one_sub_one] + congr 2 + have hp := (Nat.one_le_two_pow : 1 ≤ 2 ^ k) + rw [pow_succ, mul_two] + omega + +private theorem succCoordinates_low (k : ℕ) (x : ConcreteBTField (k + 1)) : + succCoordinates k x 0 = x.setWidth (2 ^ k) := by + rw [succCoordinates_apply] + exact low_eq_setWidth k x + +private theorem succCoordinates_high (k : ℕ) (x : ConcreteBTField (k + 1)) : + succCoordinates k x 1 = (BitVec.ushiftRight x (2 ^ k)).setWidth (2 ^ k) := by + rw [succCoordinates_apply] + exact high_eq_setWidth k x + +private theorem slice_low {w m len offset : ℕ} (x : BitVec w) + (h : offset + len ≤ m) : + ((x.setWidth m) >>> offset).setWidth len = (x >>> offset).setWidth len := by + apply BitVec.eq_of_getLsbD_eq + intro b hb + have hm : offset + b < m := by omega + simp only [BitVec.getLsbD_setWidth, BitVec.getLsbD_ushiftRight, hb, hm, + decide_true, Bool.true_and] + +private theorem slice_high {w m len offset : ℕ} (x : BitVec w) + (h : offset + len ≤ m) : + (((x >>> m).setWidth m) >>> offset).setWidth len = + (x >>> (m + offset)).setWidth len := by + rw [slice_low _ h] + apply BitVec.eq_of_getLsbD_eq + intro b hb + simp only [BitVec.getLsbD_setWidth, BitVec.getLsbD_ushiftRight, Nat.add_assoc] + +private theorem height_bits (i n : ℕ) (x : ConcreteBTField (i + n)) + (q : Fin (AlgebraTower.coordinateSize (fun _ => 2) i n)) : + AlgebraTower.natCoordinates succCoordinates i n x q = + (BitVec.ushiftRight x (2 ^ i * q.val)).setWidth (2 ^ i) := by + induction n with + | zero => + have hq : q.val = 0 := by + have := q.isLt + change q.val < 1 at this + omega + simp only [AlgebraTower.natCoordinates_zero, hq, Nat.mul_zero, + BitVec.ushiftRight_eq, BitVec.ushiftRight_zero] + exact (BitVec.setWidth_eq (x : BitVec _)).symm + | succ n ih => + rw [AlgebraTower.natCoordinates_succ, ih] + have size := AlgebraTower.coordinateSize_const 2 i n + by_cases hq : q.val < 2 ^ n + · have outer : q.divNat = 0 := by + apply Fin.ext + change q.val / AlgebraTower.coordinateSize (fun _ => 2) i n = 0 + rw [size, Nat.div_eq_of_lt hq] + have inner : q.modNat.val = q.val := by + change q.val % AlgebraTower.coordinateSize (fun _ => 2) i n = q.val + rw [size, Nat.mod_eq_of_lt hq] + rw [outer, inner, succCoordinates_low] + change (((x : BitVec _).setWidth (2 ^ (i + n))) >>> (2 ^ i * q.val)).setWidth (2 ^ i) = _ + apply slice_low + calc + 2 ^ i * q.val + 2 ^ i = 2 ^ i * (q.val + 1) := by rw [Nat.mul_add, Nat.mul_one] + _ ≤ 2 ^ i * 2 ^ n := Nat.mul_le_mul_left _ (Nat.succ_le_of_lt hq) + _ = 2 ^ (i + n) := (Nat.pow_add _ _ _).symm + · have bound : q.val < 2 ^ n + 2 ^ n := by + have hh := q.isLt + change q.val < 2 * AlgebraTower.coordinateSize (fun _ => 2) i n at hh + rw [size, two_mul] at hh + exact hh + have outer : q.divNat = 1 := by + apply Fin.ext + change q.val / AlgebraTower.coordinateSize (fun _ => 2) i n = 1 + rw [size] + apply Nat.div_eq_of_lt_le <;> omega + have inner : q.modNat.val = q.val - 2 ^ n := by + change q.val % AlgebraTower.coordinateSize (fun _ => 2) i n = q.val - 2 ^ n + rw [size, Nat.mod_eq_sub_mod (by omega), Nat.mod_eq_of_lt (by omega)] + rw [outer, inner, succCoordinates_high] + change (((BitVec.ushiftRight x (2 ^ (i + n))).setWidth (2 ^ (i + n))) >>> + (2 ^ i * (q.val - 2 ^ n))).setWidth (2 ^ i) = _ + have hcut : 2 ^ i * (q.val - 2 ^ n) + 2 ^ i ≤ 2 ^ (i + n) := by + calc + _ = 2 ^ i * (q.val - 2 ^ n + 1) := by rw [Nat.mul_add, Nat.mul_one] + _ ≤ 2 ^ i * 2 ^ n := Nat.mul_le_mul_left _ (by omega) + _ = 2 ^ (i + n) := (Nat.pow_add _ _ _).symm + simp only [BitVec.ushiftRight_eq] + rw [slice_high _ hcut] + have offset : 2 ^ (i + n) + 2 ^ i * (q.val - 2 ^ n) = 2 ^ i * q.val := by + rw [Nat.pow_add, ← Nat.mul_add, Nat.add_sub_of_le (by omega)] + rw [offset] + +private theorem bits_cast {i j : ℕ} (h : i = j) (x : ConcreteBTField i) : + cast (congrArg ConcreteBTField h) x = (x : BitVec _).cast (congrArg (2 ^ ·) h) := by + cases h + rfl + +/-- Coordinate `q` is the raw bit block of width `2 ^ i` starting at bit `2 ^ i * q`. -/ +theorem coordinates_eq_setWidth_ushiftRight {i j : ℕ} (h : i ≤ j) (x : ConcreteBTField j) + (q : Fin (2 ^ (j - i))) : + coordinates h x q = (BitVec.ushiftRight x (2 ^ i * q.val)).setWidth (2 ^ i) := by + unfold coordinates + rw [AlgebraTower.natCoordinatesConstOfLE_apply, + AlgebraTower.natCoordinatesOfLE_apply, height_bits, bits_cast (Nat.add_sub_of_le h).symm] + apply BitVec.eq_of_getLsbD_eq + intro b hb + simp only [BitVec.ushiftRight_eq, BitVec.getLsbD_setWidth, BitVec.getLsbD_ushiftRight, + BitVec.getLsbD_cast, Fin.val_cast] + +/-- Reading bit `b` within coordinate `q` reads the bit at offset `2 ^ i * q + b` +in the original word. The bound keeps `b` inside the coefficient's bit width. -/ +theorem getLsbD_coordinates {i j : ℕ} (h : i ≤ j) (x : ConcreteBTField j) + (q : Fin (2 ^ (j - i))) (b : ℕ) (hb : b < 2 ^ i) : + (coordinates h x q).getLsbD b = x.getLsbD (2 ^ i * q.val + b) := by + rw [coordinates_eq_setWidth_ushiftRight, BitVec.ushiftRight_eq] + simp only [BitVec.getLsbD_setWidth, BitVec.getLsbD_ushiftRight, hb, + decide_true, Bool.true_and] + +/-- The stored natural word of coordinate `q` is the block of `2 ^ i` bits starting at +bit `2 ^ i * q` in the original word. -/ +theorem toNat_coordinates {i j : ℕ} (h : i ≤ j) (x : ConcreteBTField j) + (q : Fin (2 ^ (j - i))) : + (coordinates h x q).toNat = + Nat.getMiddleBits (2 ^ i * q.val) (2 ^ i) (x : BitVec _).toNat := by + rw [coordinates_eq_setWidth_ushiftRight, BitVec.ushiftRight_eq, BitVec.toNat_setWidth, + BitVec.toNat_ushiftRight, Nat.getMiddleBits_eq_mod] + +end ConcreteBinaryTower.Coordinates diff --git a/CompPoly/Fields/Binary/Tower/Equiv.lean b/CompPoly/Fields/Binary/Tower/Equiv.lean index 16042771..cf0d83a9 100644 --- a/CompPoly/Fields/Binary/Tower/Equiv.lean +++ b/CompPoly/Fields/Binary/Tower/Equiv.lean @@ -5,6 +5,7 @@ Authors: Chung Thai Nguyen, Quang Dao -/ module +public import CompPoly.Fields.Binary.Tower.Abstract.Basis public import CompPoly.Fields.Binary.Tower.Concrete.Basis /-! diff --git a/CompPoly/Fields/Binary/Tower/Fast.lean b/CompPoly/Fields/Binary/Tower/Fast.lean new file mode 100644 index 00000000..e8564c60 --- /dev/null +++ b/CompPoly/Fields/Binary/Tower/Fast.lean @@ -0,0 +1,1422 @@ +/- +Copyright (c) 2026 CompPoly Contributors. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Georgios Raikos +-/ +module + +public import CompPoly.Fields.Binary.Tower.Concrete.Field +public import CompPoly.Fields.Binary.Tower.FastDefs +public import Mathlib.Algebra.CharP.Two +public import Mathlib.Algebra.Field.Defs +public import Mathlib.Algebra.Group.InjSurj +public import Mathlib.Algebra.Ring.Equiv +public import Mathlib.Algebra.Ring.InjSurj +public import Mathlib.Tactic.LinearCombination + +/-! +# Fast Binary Tower Arithmetic + +Packed machine-word implementation of `ConcreteBTField` arithmetic, same bit layout: +one `UInt64` for levels `k ≤ 6`, two limbs at level 7. The runtime definitions live in +the zero-import `FastDefs`; this module proves them correct against the concrete tower +by induction over recursive twins, giving `Field` instances and ring isomorphisms at +every width. +-/ + +@[expose] public section + +namespace ConcreteBinaryTower.Fast + +/-! ## Range bounds + +Every operation maps values below `2 ^ s` to values below `2 ^ s`; the shift/mask +helpers cross into `ℕ` once and the per-width lemmas chain them. -/ + +theorem and_mask_lt (s : ℕ) {a m : UInt64} (hm : m.toNat = 2 ^ s - 1) : + (a &&& m).toNat < 2 ^ s := by + rw [UInt64.toNat_and] + exact Nat.and_lt_two_pow _ (by rw [hm]; exact Nat.sub_lt (Nat.two_pow_pos s) Nat.one_pos) + +theorem xor_lt {x y : UInt64} {s : ℕ} (hx : x.toNat < 2 ^ s) (hy : y.toNat < 2 ^ s) : + (x ^^^ y).toNat < 2 ^ s := by + rw [UInt64.toNat_xor] + exact Nat.xor_lt_two_pow hx hy + +theorem shiftRight_lt (s : ℕ) {a sh : UInt64} {t : ℕ} (hsh : sh.toNat = s) + (hs : s < 64) (ha : a.toNat < 2 ^ (s + t)) : (a >>> sh).toNat < 2 ^ t := by + rw [UInt64.toNat_shiftRight, hsh, Nat.mod_eq_of_lt hs, Nat.shiftRight_eq_div_pow] + exact Nat.div_lt_of_lt_mul (by rw [← Nat.pow_add]; exact ha) + +theorem join_lt (s : ℕ) {hi lo sh : UInt64} (hsh : sh.toNat = s) (hs : 2 * s ≤ 64) + (hhi : hi.toNat < 2 ^ s) (hlo : lo.toNat < 2 ^ s) : + ((hi <<< sh) ||| lo).toNat < 2 ^ (2 * s) := by + rw [UInt64.toNat_or, UInt64.toNat_shiftLeft, hsh, Nat.mod_eq_of_lt (by omega : s < 64)] + refine Nat.or_lt_two_pow ?_ (Nat.lt_of_lt_of_le hlo (Nat.pow_le_pow_right (by omega) (by omega))) + have hval : hi.toNat <<< s < 2 ^ (2 * s) := by + rw [Nat.shiftLeft_eq, Nat.two_mul, Nat.pow_add] + exact (Nat.mul_lt_mul_right (Nat.two_pow_pos s)).mpr hhi + exact Nat.lt_of_le_of_lt (Nat.mod_le _ _) hval + +/-- Both half bounds for a literal shift/mask split. -/ +theorem half_lit_lt (s : ℕ) (m sh : UInt64) {v : UInt64} (hm : m.toNat = 2 ^ s - 1) + (hsh : sh.toNat = s) (hs : s < 64) (hv : v.toNat < 2 ^ (s + s)) : + (v >>> sh).toNat < 2 ^ s ∧ (v &&& m).toNat < 2 ^ s := + ⟨shiftRight_lt s hsh hs hv, and_mask_lt s hm⟩ + +/-- Half bounds of a 16-bit word. -/ +theorem half16_lt {v : UInt64} (hv : v.toNat < 2 ^ 16) : + (v >>> 8).toNat < 2 ^ 8 ∧ (v &&& 0xFF).toNat < 2 ^ 8 := + half_lit_lt 8 0xFF 8 (by decide) (by decide) (by omega) hv + +/-- Half bounds of a 32-bit word. -/ +theorem half32_lt {v : UInt64} (hv : v.toNat < 2 ^ 32) : + (v >>> 16).toNat < 2 ^ 16 ∧ (v &&& 0xFFFF).toNat < 2 ^ 16 := + half_lit_lt 16 0xFFFF 16 (by decide) (by decide) (by omega) hv + +/-- Half bounds of a full word. -/ +theorem half64_lt (v : UInt64) : + (v >>> 32).toNat < 2 ^ 32 ∧ (v &&& 0xFFFFFFFF).toNat < 2 ^ 32 := + half_lit_lt 32 0xFFFFFFFF 32 (by decide) (by decide) (by omega) (UInt64.toNat_lt v) + +/-! ### Proof-side recursive twins + +The runtime ladder is unrolled for code generation; proofs run over structurally +recursive twins, connected to each rung by `rfl` bridges (`mul8_eq_rec`, ...) and +meaningful for `k ≤ 6` (one word). -/ + +/-- Recursive twin of the `mulByZk` ladder. -/ +def mulByZRec : ℕ → UInt64 → UInt64 + | 0, v => v + | k + 1, v => + let sh := UInt64.ofNat (2 ^ k) + let m := ((1 : UInt64) <<< sh) - 1 + let v0 := v &&& m + let v1 := v >>> sh + ((v0 ^^^ mulByZRec k v1) <<< sh) ||| v1 + +/-- Recursive twin of the multiplication ladder. -/ +def mulRec : ℕ → UInt64 → UInt64 → UInt64 + | 0, a, b => a &&& b + | k + 1, a, b => + let sh := UInt64.ofNat (2 ^ k) + let m := ((1 : UInt64) <<< sh) - 1 + let a0 := a &&& m + let a1 := a >>> sh + let b0 := b &&& m + let b1 := b >>> sh + let p0 := mulRec k a0 b0 + let p2 := mulRec k a1 b1 + let p1 := mulRec k (a0 ^^^ a1) (b0 ^^^ b1) + let lo := p0 ^^^ p2 + ((p1 ^^^ lo ^^^ mulByZRec k p2) <<< sh) ||| lo + +/-- Recursive twin of the squaring ladder; level 0 is the identity (`v² = v` in GF(2)). -/ +def sqRec : ℕ → UInt64 → UInt64 + | 0, v => v + | k + 1, v => + let sh := UInt64.ofNat (2 ^ k) + let m := ((1 : UInt64) <<< sh) - 1 + let s0 := sqRec k (v &&& m) + let s1 := sqRec k (v >>> sh) + ((mulByZRec k s1) <<< sh) ||| (s0 ^^^ s1) + +/-- Recursive twin of the inversion ladder; level 0 is the identity. -/ +def invRec : ℕ → UInt64 → UInt64 + | 0, v => v + | k + 1, v => + let sh := UInt64.ofNat (2 ^ k) + let m := ((1 : UInt64) <<< sh) - 1 + let v0 := v &&& m + let v1 := v >>> sh + let next := v0 ^^^ mulByZRec k v1 + let delta := mulRec k v0 next ^^^ sqRec k v1 + let d := invRec k delta + ((mulRec k d v1) <<< sh) ||| (mulRec k d next) + +/-! One-step unfoldings as `rfl` theorems, so proofs rewrite with these instead of +realizing each twin's equation lemmas over and over. -/ + +theorem mulByZRec_succ (k : ℕ) (v : UInt64) : + mulByZRec (k + 1) v = + let sh := UInt64.ofNat (2 ^ k) + let m := ((1 : UInt64) <<< sh) - 1 + let v0 := v &&& m + let v1 := v >>> sh + ((v0 ^^^ mulByZRec k v1) <<< sh) ||| v1 := rfl + +theorem mulRec_succ (k : ℕ) (a b : UInt64) : + mulRec (k + 1) a b = + let sh := UInt64.ofNat (2 ^ k) + let m := ((1 : UInt64) <<< sh) - 1 + let a0 := a &&& m + let a1 := a >>> sh + let b0 := b &&& m + let b1 := b >>> sh + let p0 := mulRec k a0 b0 + let p2 := mulRec k a1 b1 + let p1 := mulRec k (a0 ^^^ a1) (b0 ^^^ b1) + let lo := p0 ^^^ p2 + ((p1 ^^^ lo ^^^ mulByZRec k p2) <<< sh) ||| lo := rfl + +theorem sqRec_succ (k : ℕ) (v : UInt64) : + sqRec (k + 1) v = + let sh := UInt64.ofNat (2 ^ k) + let m := ((1 : UInt64) <<< sh) - 1 + let s0 := sqRec k (v &&& m) + let s1 := sqRec k (v >>> sh) + ((mulByZRec k s1) <<< sh) ||| (s0 ^^^ s1) := rfl + +theorem invRec_succ (k : ℕ) (v : UInt64) : + invRec (k + 1) v = + let sh := UInt64.ofNat (2 ^ k) + let m := ((1 : UInt64) <<< sh) - 1 + let v0 := v &&& m + let v1 := v >>> sh + let next := v0 ^^^ mulByZRec k v1 + let delta := mulRec k v0 next ^^^ sqRec k v1 + let d := invRec k delta + ((mulRec k d v1) <<< sh) ||| (mulRec k d next) := rfl + +theorem toNat_ofNat_two_pow {k : ℕ} (hk : k ≤ 5) : + (UInt64.ofNat (2 ^ k)).toNat = 2 ^ k := + UInt64.toNat_ofNat_of_lt' + (Nat.lt_of_le_of_lt (Nat.pow_le_pow_right (by omega) hk) (by norm_num [UInt64.size])) + +theorem toNat_mask_two_pow {k : ℕ} (hk : k ≤ 5) : + (((1 : UInt64) <<< UInt64.ofNat (2 ^ k)) - 1).toNat = 2 ^ 2 ^ k - 1 := by + have h32 : 2 ^ k ≤ 32 := by + calc 2 ^ k ≤ 2 ^ 5 := Nat.pow_le_pow_right (by omega) hk + _ = 32 := rfl + have hpow : 2 ^ 2 ^ k ≤ 2 ^ 32 := Nat.pow_le_pow_right (by omega) h32 + have hshift : ((1 : UInt64) <<< UInt64.ofNat (2 ^ k)).toNat = 2 ^ 2 ^ k := by + rw [UInt64.toNat_shiftLeft, toNat_ofNat_two_pow hk, + show (1 : UInt64).toNat = 1 from rfl, + Nat.mod_eq_of_lt (by omega : 2 ^ k < 64), Nat.shiftLeft_eq, Nat.one_mul] + exact Nat.mod_eq_of_lt (Nat.lt_of_le_of_lt hpow (by norm_num)) + have hle : (1 : UInt64) ≤ (1 : UInt64) <<< UInt64.ofNat (2 ^ k) := by + rw [UInt64.le_iff_toNat_le, hshift] + exact Nat.two_pow_pos _ + rw [UInt64.toNat_sub_of_le _ _ hle, hshift] + rfl + +/-- The arithmetic facts every inductive step needs about the half-width `2 ^ k`. -/ +theorem rec_step_bounds {k : ℕ} (hk : k + 1 ≤ 6) : + k ≤ 5 ∧ 2 * 2 ^ k ≤ 64 ∧ 2 ^ k + 2 ^ k = 2 ^ (k + 1) ∧ + (2 : ℕ) ^ 2 ^ (k + 1) = 2 ^ (2 * 2 ^ k) := by + have hk5 : k ≤ 5 := by omega + have h32 : 2 ^ k ≤ 32 := Nat.pow_le_pow_right (by omega) hk5 + exact ⟨hk5, by omega, by rw [Nat.pow_succ]; omega, by rw [Nat.pow_succ, Nat.mul_comm]⟩ + +/-- Both halves of an in-range word are in range at the half level. -/ +theorem half_lt {k : ℕ} (hk : k + 1 ≤ 6) {v : UInt64} + (hv : v.toNat < 2 ^ 2 ^ (k + 1)) : + (v >>> UInt64.ofNat (2 ^ k)).toNat < 2 ^ 2 ^ k + ∧ (v &&& ((1 : UInt64) <<< UInt64.ofNat (2 ^ k) - 1)).toNat < 2 ^ 2 ^ k := by + obtain ⟨hk5, h2s, hsplit, _⟩ := rec_step_bounds hk + have hv' : v.toNat < 2 ^ (2 ^ k + 2 ^ k) := by rw [hsplit]; exact hv + exact ⟨shiftRight_lt (2 ^ k) (toNat_ofNat_two_pow hk5) (by omega) hv', + and_mask_lt (2 ^ k) (toNat_mask_two_pow hk5)⟩ + +/-- One-word `mulByZ` bound, by induction on the level. -/ +theorem mulByZRec_lt : ∀ (k : ℕ), k ≤ 6 → ∀ (v : UInt64), + v.toNat < 2 ^ 2 ^ k → (mulByZRec k v).toNat < 2 ^ 2 ^ k + | 0, _, _, hv => hv + | k + 1, hk, v, hv => by + obtain ⟨hk5, h2s, _, hpow⟩ := rec_step_bounds hk + obtain ⟨hv1, hv0⟩ := half_lt hk hv + have hrec := mulByZRec_lt k (Nat.le_of_succ_le hk) _ hv1 + rw [hpow] + exact join_lt (2 ^ k) (toNat_ofNat_two_pow hk5) h2s (xor_lt hv0 hrec) hv1 + +/-- One-word multiplication bound, by induction on the level. -/ +theorem mulRec_lt : ∀ (k : ℕ), k ≤ 6 → ∀ (a b : UInt64), + a.toNat < 2 ^ 2 ^ k → b.toNat < 2 ^ 2 ^ k → (mulRec k a b).toNat < 2 ^ 2 ^ k + | 0, _, a, b, ha, _ => by + show (a &&& b).toNat < 2 ^ 2 ^ 0 + exact Nat.lt_of_le_of_lt (by rw [UInt64.toNat_and]; exact Nat.and_le_left) ha + | k + 1, hk, a, b, ha, hb => by + obtain ⟨hk5, h2s, _, hpow⟩ := rec_step_bounds hk + have hk6 : k ≤ 6 := Nat.le_of_succ_le hk + obtain ⟨ha1, ha0⟩ := half_lt hk ha + obtain ⟨hb1, hb0⟩ := half_lt hk hb + have hp0 := mulRec_lt k hk6 _ _ ha0 hb0 + have hp2 := mulRec_lt k hk6 _ _ ha1 hb1 + have hp1 := mulRec_lt k hk6 _ _ (xor_lt ha0 ha1) (xor_lt hb0 hb1) + have hlo := xor_lt hp0 hp2 + have hz := mulByZRec_lt k hk6 _ hp2 + rw [hpow] + exact join_lt (2 ^ k) (toNat_ofNat_two_pow hk5) h2s + (xor_lt (xor_lt hp1 hlo) hz) hlo + +/-- One-word squaring bound, by induction on the level. -/ +theorem sqRec_lt : ∀ (k : ℕ), k ≤ 6 → ∀ (v : UInt64), + v.toNat < 2 ^ 2 ^ k → (sqRec k v).toNat < 2 ^ 2 ^ k + | 0, _, _, hv => hv + | k + 1, hk, v, hv => by + obtain ⟨hk5, h2s, _, hpow⟩ := rec_step_bounds hk + have hk6 : k ≤ 6 := Nat.le_of_succ_le hk + obtain ⟨hv1, hv0⟩ := half_lt hk hv + have hs0 := sqRec_lt k hk6 _ hv0 + have hs1 := sqRec_lt k hk6 _ hv1 + have hz := mulByZRec_lt k hk6 _ hs1 + rw [hpow] + exact join_lt (2 ^ k) (toNat_ofNat_two_pow hk5) h2s hz (xor_lt hs0 hs1) + +/-- One-word inversion bound, by induction on the level. -/ +theorem invRec_lt : ∀ (k : ℕ), k ≤ 6 → ∀ (v : UInt64), + v.toNat < 2 ^ 2 ^ k → (invRec k v).toNat < 2 ^ 2 ^ k + | 0, _, _, hv => hv + | k + 1, hk, v, hv => by + obtain ⟨hk5, h2s, _, hpow⟩ := rec_step_bounds hk + have hk6 : k ≤ 6 := Nat.le_of_succ_le hk + obtain ⟨hv1, hv0⟩ := half_lt hk hv + have hnext := xor_lt hv0 (mulByZRec_lt k hk6 _ hv1) + have hdel := xor_lt (mulRec_lt k hk6 _ _ hv0 hnext) (sqRec_lt k hk6 _ hv1) + have hd := invRec_lt k hk6 _ hdel + rw [hpow] + exact join_lt (2 ^ k) (toNat_ofNat_two_pow hk5) h2s + (mulRec_lt k hk6 _ _ hd hv1) (mulRec_lt k hk6 _ _ hd hnext) + +/-! ### Rung-twin bridges and per-width bounds -/ + +theorem mulByZ1_eq_rec (v : UInt64) : mulByZ1 v = mulByZRec 1 v := rfl +theorem mulByZ2_eq_rec (v : UInt64) : mulByZ2 v = mulByZRec 2 v := rfl +theorem mulByZ3_eq_rec (v : UInt64) : mulByZ3 v = mulByZRec 3 v := rfl +theorem mulByZ4_eq_rec (v : UInt64) : mulByZ4 v = mulByZRec 4 v := rfl +theorem mulByZ5_eq_rec (v : UInt64) : mulByZ5 v = mulByZRec 5 v := rfl + +theorem mulByZ6_eq_rec (v : UInt64) : mulByZ6 v = mulByZRec 6 v := by + conv_rhs => rw [mulByZRec_succ] + simp only [← mulByZ5_eq_rec] + rfl + +theorem xor_xor_cancel (x y z : UInt64) : x ^^^ (y ^^^ z) ^^^ z = x ^^^ y := + UInt64.toNat_inj.mp (by + simp only [UInt64.toNat_xor] + rw [Nat.xor_assoc, Nat.xor_assoc, Nat.xor_self, Nat.xor_zero]) + +theorem mul2_eq_rec (a b : UInt64) : mul2 a b = mulRec 1 a b := by + simp only [mul2, mulRec, mulByZRec] + rw [xor_xor_cancel] + rfl + +theorem mul4_eq_rec (a b : UInt64) : mul4 a b = mulRec 2 a b := by + conv_rhs => rw [mulRec_succ] + simp only [← mul2_eq_rec, ← mulByZ1_eq_rec] + rfl + +theorem mul8_eq_rec (a b : UInt64) : mul8 a b = mulRec 3 a b := by + conv_rhs => rw [mulRec_succ] + simp only [← mul4_eq_rec, ← mulByZ2_eq_rec] + rfl + +theorem mul16_eq_rec (a b : UInt64) : mul16 a b = mulRec 4 a b := by + conv_rhs => rw [mulRec_succ] + simp only [← mul8_eq_rec, ← mulByZ3_eq_rec] + rfl + +theorem mul32_eq_rec (a b : UInt64) : mul32 a b = mulRec 5 a b := by + conv_rhs => rw [mulRec_succ] + simp only [← mul16_eq_rec, ← mulByZ4_eq_rec] + rfl + +theorem mul64_eq_rec (a b : UInt64) : mul64 a b = mulRec 6 a b := by + conv_rhs => rw [mulRec_succ] + simp only [← mul32_eq_rec, ← mulByZ5_eq_rec] + rfl + +theorem sq2_eq_rec (v : UInt64) : sq2 v = sqRec 1 v := rfl + +theorem sq4_eq_rec (v : UInt64) : sq4 v = sqRec 2 v := by + conv_rhs => rw [sqRec_succ] + simp only [← sq2_eq_rec, ← mulByZ1_eq_rec] + rfl + +theorem sq8_eq_rec (v : UInt64) : sq8 v = sqRec 3 v := by + conv_rhs => rw [sqRec_succ] + simp only [← sq4_eq_rec, ← mulByZ2_eq_rec] + rfl + +theorem sq16_eq_rec (v : UInt64) : sq16 v = sqRec 4 v := by + conv_rhs => rw [sqRec_succ] + simp only [← sq8_eq_rec, ← mulByZ3_eq_rec] + rfl + +theorem sq32_eq_rec (v : UInt64) : sq32 v = sqRec 5 v := by + conv_rhs => rw [sqRec_succ] + simp only [← sq16_eq_rec, ← mulByZ4_eq_rec] + rfl + +theorem sq64_eq_rec (v : UInt64) : sq64 v = sqRec 6 v := by + conv_rhs => rw [sqRec_succ] + simp only [← sq32_eq_rec, ← mulByZ5_eq_rec] + rfl + +theorem inv2_eq_rec (v : UInt64) : inv2 v = invRec 1 v := rfl + +theorem inv4_eq_rec (v : UInt64) : inv4 v = invRec 2 v := by + conv_rhs => rw [invRec_succ] + simp only [← mul2_eq_rec, ← sq2_eq_rec, ← mulByZ1_eq_rec, ← inv2_eq_rec] + rfl + +theorem inv8_eq_rec (v : UInt64) : inv8 v = invRec 3 v := by + conv_rhs => rw [invRec_succ] + simp only [← mul4_eq_rec, ← sq4_eq_rec, ← mulByZ2_eq_rec, ← inv4_eq_rec] + rfl + +theorem inv16_eq_rec (v : UInt64) : inv16 v = invRec 4 v := by + conv_rhs => rw [invRec_succ] + simp only [← mul8_eq_rec, ← sq8_eq_rec, ← mulByZ3_eq_rec, ← inv8_eq_rec] + rfl + +theorem inv32_eq_rec (v : UInt64) : inv32 v = invRec 5 v := by + conv_rhs => rw [invRec_succ] + simp only [← mul16_eq_rec, ← sq16_eq_rec, ← mulByZ4_eq_rec, ← inv16_eq_rec] + rfl + +theorem inv64_eq_rec (v : UInt64) : inv64 v = invRec 6 v := by + conv_rhs => rw [invRec_succ] + simp only [← mul32_eq_rec, ← sq32_eq_rec, ← mulByZ5_eq_rec, ← inv32_eq_rec] + rfl + +theorem mul8_lt {a b : UInt64} (ha : a.toNat < 2 ^ 8) (hb : b.toNat < 2 ^ 8) : + (mul8 a b).toNat < 2 ^ 8 := by + rw [mul8_eq_rec]; exact mulRec_lt 3 (by omega) a b ha hb + +theorem mul16_lt {a b : UInt64} (ha : a.toNat < 2 ^ 16) (hb : b.toNat < 2 ^ 16) : + (mul16 a b).toNat < 2 ^ 16 := by + rw [mul16_eq_rec]; exact mulRec_lt 4 (by omega) a b ha hb + +theorem mul32_lt {a b : UInt64} (ha : a.toNat < 2 ^ 32) (hb : b.toNat < 2 ^ 32) : + (mul32 a b).toNat < 2 ^ 32 := by + rw [mul32_eq_rec]; exact mulRec_lt 5 (by omega) a b ha hb + +theorem mulByZ3_lt {v : UInt64} (hv : v.toNat < 2 ^ 8) : (mulByZ3 v).toNat < 2 ^ 8 := by + rw [mulByZ3_eq_rec]; exact mulByZRec_lt 3 (by omega) v hv + +theorem mulByZ4_lt {v : UInt64} (hv : v.toNat < 2 ^ 16) : (mulByZ4 v).toNat < 2 ^ 16 := by + rw [mulByZ4_eq_rec]; exact mulByZRec_lt 4 (by omega) v hv + +theorem mulByZ5_lt {v : UInt64} (hv : v.toNat < 2 ^ 32) : (mulByZ5 v).toNat < 2 ^ 32 := by + rw [mulByZ5_eq_rec]; exact mulByZRec_lt 5 (by omega) v hv + +theorem sq8_lt {v : UInt64} (hv : v.toNat < 2 ^ 8) : (sq8 v).toNat < 2 ^ 8 := by + rw [sq8_eq_rec]; exact sqRec_lt 3 (by omega) v hv + +theorem sq16_lt {v : UInt64} (hv : v.toNat < 2 ^ 16) : (sq16 v).toNat < 2 ^ 16 := by + rw [sq16_eq_rec]; exact sqRec_lt 4 (by omega) v hv + +theorem sq32_lt {v : UInt64} (hv : v.toNat < 2 ^ 32) : (sq32 v).toNat < 2 ^ 32 := by + rw [sq32_eq_rec]; exact sqRec_lt 5 (by omega) v hv + +theorem inv8_lt {v : UInt64} (hv : v.toNat < 2 ^ 8) : (inv8 v).toNat < 2 ^ 8 := by + rw [inv8_eq_rec]; exact invRec_lt 3 (by omega) v hv + +theorem inv16_lt {v : UInt64} (hv : v.toNat < 2 ^ 16) : (inv16 v).toNat < 2 ^ 16 := by + rw [inv16_eq_rec]; exact invRec_lt 4 (by omega) v hv + +theorem inv32_lt {v : UInt64} (hv : v.toNat < 2 ^ 32) : (inv32 v).toNat < 2 ^ 32 := by + rw [inv32_eq_rec]; exact invRec_lt 5 (by omega) v hv + +/-! ### Table bridges + +`getElem_ofFn` turns a table lookup back into the generating ladder call, so each `*T` +rung equals its ladder twin on in-range words and no table is kernel-evaluated. -/ + +/-- Lookup in a 256-entry `Array.ofFn` table is the generating function. -/ +theorem byteTable_get_eq {f : UInt64 → UInt64} + (hf : ∀ v : UInt64, v.toNat < 2 ^ 8 → (f v).toNat < 2 ^ 8) + {v : UInt64} (hv : v.toNat < 2 ^ 8) : + ((⟨Array.ofFn (n := 256) fun i => (f (UInt64.ofNat i)).toUInt8⟩ : ByteArray).get! + v.toNat).toUInt64 = f v := by + have hfv : (f v).toNat < 256 := by have := hf v hv; omega + simp only [ByteArray.get!] + rw [getElem!_pos _ _ (by simp only [Array.size_ofFn]; omega)] + simp only [Array.getElem_ofFn] + rw [UInt64.ofNat_toNat] + apply UInt64.toNat_inj.mp + simp only [UInt8.toNat_toUInt64, UInt64.toNat_toUInt8, Nat.mod_eq_of_lt hfv] + +theorem mul8T_eq_mul8 {a b : UInt64} (ha : a.toNat < 2 ^ 8) (hb : b.toNat < 2 ^ 8) : + mul8T a b = mul8 a b := by + have hidx : ((a <<< 8) + b).toNat = a.toNat * 256 + b.toNat := by + have h8 : ((8 : UInt64).toNat % 64) = 8 := by decide + rw [UInt64.toNat_add, UInt64.toNat_shiftLeft, h8, Nat.shiftLeft_eq] + omega + have hbound : (mul8 a b).toNat < 256 := by have := mul8_lt ha hb; omega + rw [mul8T, hidx] + simp only [mul8Table, ByteArray.get!] + rw [getElem!_pos _ _ (by simp only [Array.size_ofFn]; omega)] + simp only [Array.getElem_ofFn] + have hdiv : (a.toNat * 256 + b.toNat) / 256 = a.toNat := by omega + have hmod : (a.toNat * 256 + b.toNat) % 256 = b.toNat := by omega + rw [hdiv, hmod, UInt64.ofNat_toNat, UInt64.ofNat_toNat] + apply UInt64.toNat_inj.mp + simp only [UInt8.toNat_toUInt64, UInt64.toNat_toUInt8, Nat.mod_eq_of_lt hbound] + +theorem mulByZ3T_eq_mulByZ3 {v : UInt64} (hv : v.toNat < 2 ^ 8) : + mulByZ3T v = mulByZ3 v := + byteTable_get_eq (fun _ h => mulByZ3_lt h) hv + +theorem sq8T_eq_sq8 {v : UInt64} (hv : v.toNat < 2 ^ 8) : sq8T v = sq8 v := + byteTable_get_eq (fun _ h => sq8_lt h) hv + +theorem inv8T_eq_inv8 {v : UInt64} (hv : v.toNat < 2 ^ 8) : inv8T v = inv8 v := + byteTable_get_eq (fun _ h => inv8_lt h) hv + +theorem mul16T_eq_mul16 {a b : UInt64} (ha : a.toNat < 2 ^ 16) (hb : b.toNat < 2 ^ 16) : + mul16T a b = mul16 a b := by + obtain ⟨ha1, ha0⟩ := half16_lt ha + obtain ⟨hb1, hb0⟩ := half16_lt hb + simp only [mul16T, mul16] + rw [mul8T_eq_mul8 ha0 hb0, mul8T_eq_mul8 ha1 hb1, + mul8T_eq_mul8 (xor_lt ha0 ha1) (xor_lt hb0 hb1), + mulByZ3T_eq_mulByZ3 (mul8_lt ha1 hb1)] + +theorem mulByZ4T_eq_mulByZ4 {v : UInt64} (hv : v.toNat < 2 ^ 16) : + mulByZ4T v = mulByZ4 v := by + obtain ⟨hv1, -⟩ := half16_lt hv + simp only [mulByZ4T, mulByZ4] + rw [mulByZ3T_eq_mulByZ3 hv1] + +theorem sq16T_eq_sq16 {v : UInt64} (hv : v.toNat < 2 ^ 16) : + sq16T v = sq16 v := by + obtain ⟨hv1, hv0⟩ := half16_lt hv + simp only [sq16T, sq16] + rw [sq8T_eq_sq8 hv0, sq8T_eq_sq8 hv1, mulByZ3T_eq_mulByZ3 (sq8_lt hv1)] + +theorem inv16T_eq_inv16 {v : UInt64} (hv : v.toNat < 2 ^ 16) : + inv16T v = inv16 v := by + obtain ⟨hv1, hv0⟩ := half16_lt hv + have hnext := xor_lt hv0 (mulByZ3_lt hv1) + have hdelta := xor_lt (mul8_lt hv0 hnext) (sq8_lt hv1) + simp only [inv16T, inv16] + rw [mulByZ3T_eq_mulByZ3 hv1, sq8T_eq_sq8 hv1, mul8T_eq_mul8 hv0 hnext, + inv8T_eq_inv8 hdelta, + mul8T_eq_mul8 (inv8_lt hdelta) hv1, + mul8T_eq_mul8 (inv8_lt hdelta) hnext] + +theorem mul32T_eq_mul32 {a b : UInt64} (ha : a.toNat < 2 ^ 32) (hb : b.toNat < 2 ^ 32) : + mul32T a b = mul32 a b := by + obtain ⟨ha1, ha0⟩ := half32_lt ha + obtain ⟨hb1, hb0⟩ := half32_lt hb + simp only [mul32T, mul32] + rw [mul16T_eq_mul16 ha0 hb0, mul16T_eq_mul16 ha1 hb1, + mul16T_eq_mul16 (xor_lt ha0 ha1) (xor_lt hb0 hb1), + mulByZ4T_eq_mulByZ4 (mul16_lt ha1 hb1)] + +theorem mulByZ5T_eq_mulByZ5 {v : UInt64} (hv : v.toNat < 2 ^ 32) : + mulByZ5T v = mulByZ5 v := by + obtain ⟨hv1, -⟩ := half32_lt hv + simp only [mulByZ5T, mulByZ5] + rw [mulByZ4T_eq_mulByZ4 hv1] + +theorem sq32T_eq_sq32 {v : UInt64} (hv : v.toNat < 2 ^ 32) : + sq32T v = sq32 v := by + obtain ⟨hv1, hv0⟩ := half32_lt hv + simp only [sq32T, sq32] + rw [sq16T_eq_sq16 hv0, sq16T_eq_sq16 hv1, mulByZ4T_eq_mulByZ4 (sq16_lt hv1)] + +theorem inv32T_eq_inv32 {v : UInt64} (hv : v.toNat < 2 ^ 32) : + inv32T v = inv32 v := by + obtain ⟨hv1, hv0⟩ := half32_lt hv + have hnext := xor_lt hv0 (mulByZ4_lt hv1) + have hdelta := xor_lt (mul16_lt hv0 hnext) (sq16_lt hv1) + simp only [inv32T, inv32] + rw [mulByZ4T_eq_mulByZ4 hv1, sq16T_eq_sq16 hv1, mul16T_eq_mul16 hv0 hnext, + inv16T_eq_inv16 hdelta, + mul16T_eq_mul16 (inv16_lt hdelta) hv1, + mul16T_eq_mul16 (inv16_lt hdelta) hnext] + +theorem mul64T_eq_mul64 (a b : UInt64) : + mul64T a b = mul64 a b := by + obtain ⟨ha1, ha0⟩ := half64_lt a + obtain ⟨hb1, hb0⟩ := half64_lt b + simp only [mul64T, mul64] + rw [mul32T_eq_mul32 ha0 hb0, mul32T_eq_mul32 ha1 hb1, + mul32T_eq_mul32 (xor_lt ha0 ha1) (xor_lt hb0 hb1), + mulByZ5T_eq_mulByZ5 (mul32_lt ha1 hb1)] + +theorem mulByZ6T_eq_mulByZ6 (v : UInt64) : + mulByZ6T v = mulByZ6 v := by + obtain ⟨hv1, -⟩ := half64_lt v + simp only [mulByZ6T, mulByZ6] + rw [mulByZ5T_eq_mulByZ5 hv1] + +theorem sq64T_eq_sq64 (v : UInt64) : + sq64T v = sq64 v := by + obtain ⟨hv1, hv0⟩ := half64_lt v + simp only [sq64T, sq64] + rw [sq32T_eq_sq32 hv0, sq32T_eq_sq32 hv1, mulByZ5T_eq_mulByZ5 (sq32_lt hv1)] + +theorem inv64T_eq_inv64 (v : UInt64) : + inv64T v = inv64 v := by + obtain ⟨hv1, hv0⟩ := half64_lt v + have hnext := xor_lt hv0 (mulByZ5_lt hv1) + have hdelta := xor_lt (mul32_lt hv0 hnext) (sq32_lt hv1) + simp only [inv64T, inv64] + rw [mulByZ5T_eq_mulByZ5 hv1, sq32T_eq_sq32 hv1, mul32T_eq_mul32 hv0 hnext, + inv32T_eq_inv32 hdelta, + mul32T_eq_mul32 (inv32_lt hdelta) hv1, + mul32T_eq_mul32 (inv32_lt hdelta) hnext] + +/-! ## Correctness against the spec + +The twins agree with `concrete_mul` / `concrete_inv` on in-range words, by induction on +the level; statements go through `fromNat` so the spec side reasons inside +`ConcreteBTField`. -/ + +theorem toNat_fromNat {k n : ℕ} (h : n < 2 ^ 2 ^ k) : + BitVec.toNat (fromNat (k := k) n) = n := by + show (BitVec.ofNat (2 ^ k) n).toNat = n + rw [BitVec.toNat_ofNat] + exact Nat.mod_eq_of_lt h + +theorem fromNat_toNat {k : ℕ} (x : ConcreteBTField k) : fromNat x.toNat = x := + BitVec.eq_of_toNat_eq (toNat_fromNat x.isLt) + +theorem eq_zero_or_one {v : UInt64} (hv : v.toNat < 2 ^ 2 ^ 0) : v = 0 ∨ v = 1 := by + rcases (by omega : v.toNat = 0 ∨ v.toNat = 1) with h | h + · exact Or.inl (UInt64.toNat_inj.mp h) + · exact Or.inr (UInt64.toNat_inj.mp h) + +theorem fromNat_zero {k : ℕ} : + fromNat (k := k) (0 : UInt64).toNat = (0 : ConcreteBTField k) := rfl + +theorem fromNat_one {k : ℕ} : + fromNat (k := k) (1 : UInt64).toNat = (1 : ConcreteBTField k) := rfl + +theorem shiftRight_toNat {k : ℕ} (hk : k ≤ 5) (a : UInt64) : + (a >>> UInt64.ofNat (2 ^ k)).toNat = a.toNat >>> 2 ^ k := by + have h32 : 2 ^ k ≤ 32 := Nat.pow_le_pow_right (by omega) hk + rw [UInt64.toNat_shiftRight, toNat_ofNat_two_pow hk, Nat.mod_eq_of_lt (by omega)] + +theorem and_mask_toNat {k : ℕ} (hk : k ≤ 5) (a : UInt64) : + (a &&& ((1 : UInt64) <<< UInt64.ofNat (2 ^ k) - 1)).toNat + = a.toNat &&& (2 ^ 2 ^ k - 1) := by + rw [UInt64.toNat_and, toNat_mask_two_pow hk] + +theorem join_word_toNat {k : ℕ} (hk : k + 1 ≤ 6) {hi : UInt64} + (hhi : hi.toNat < 2 ^ 2 ^ k) (lo : UInt64) : + ((hi <<< UInt64.ofNat (2 ^ k)) ||| lo).toNat = hi.toNat <<< 2 ^ k ||| lo.toNat := by + obtain ⟨hk5, h2s, hsplit, hpow⟩ := rec_step_bounds hk + rw [UInt64.toNat_or, UInt64.toNat_shiftLeft, toNat_ofNat_two_pow hk5, + Nat.mod_eq_of_lt (show 2 ^ k < 64 by omega), + Nat.mod_eq_of_lt (show hi.toNat <<< 2 ^ k < 2 ^ 64 by + rw [Nat.shiftLeft_eq] + calc hi.toNat * 2 ^ 2 ^ k + < 2 ^ 2 ^ k * 2 ^ 2 ^ k := (Nat.mul_lt_mul_right (Nat.two_pow_pos _)).mpr hhi + _ = 2 ^ (2 * 2 ^ k) := by rw [Nat.two_mul, Nat.pow_add] + _ ≤ 2 ^ 64 := Nat.pow_le_pow_right (by omega) h2s)] + +theorem nat_join_shiftRight {H L s : ℕ} (hL : L < 2 ^ s) : + (H <<< s ||| L) >>> s = H := by + rw [← Nat.shiftLeft_add_eq_or_of_lt hL, Nat.shiftLeft_eq, Nat.shiftRight_eq_div_pow, + Nat.mul_comm H (2 ^ s), Nat.mul_add_div (Nat.two_pow_pos s), Nat.div_eq_of_lt hL, + Nat.add_zero] + +theorem nat_join_and {H L s : ℕ} (hL : L < 2 ^ s) : + (H <<< s ||| L) &&& (2 ^ s - 1) = L := by + rw [← Nat.shiftLeft_add_eq_or_of_lt hL, Nat.and_two_pow_sub_one_eq_mod, Nat.shiftLeft_eq, + Nat.mul_comm H (2 ^ s), Nat.mul_add_mod, Nat.mod_eq_of_lt hL] + +theorem fromNat_xor {k : ℕ} (x y : UInt64) : + fromNat (k := k) (x ^^^ y).toNat = fromNat x.toNat + fromNat y.toNat := by + rw [UInt64.toNat_xor] + exact sum_fromNat_eq_from_xor_Nat _ _ + +theorem fromNat_join {k : ℕ} (hk : k + 1 ≤ 6) {hi lo : UInt64} + (hhi : hi.toNat < 2 ^ 2 ^ k) (hlo : lo.toNat < 2 ^ 2 ^ k) : + fromNat (k := k + 1) ((hi <<< UInt64.ofNat (2 ^ k)) ||| lo).toNat + = (《 fromNat (k := k) hi.toNat, fromNat (k := k) lo.toNat 》 : + ConcreteBTField (k + 1)) := by + obtain ⟨hk5, h2s, hsplit, hpow⟩ := rec_step_bounds hk + have hX := join_word_toNat hk hhi lo + have hXlt : ((hi <<< UInt64.ofNat (2 ^ k)) ||| lo).toNat < 2 ^ 2 ^ (k + 1) := by + rw [hpow] + exact join_lt (2 ^ k) (toNat_ofNat_two_pow hk5) h2s hhi hlo + refine (join_eq_bitvec_iff_fromNat (Nat.succ_pos k) _ _ _).mpr ⟨?_, ?_⟩ + · simp only [Nat.succ_sub_one] + congr 1 + rw [toNat_fromNat hXlt, hX, nat_join_shiftRight hlo] + · simp only [Nat.succ_sub_one] + congr 1 + rw [toNat_fromNat hXlt, hX, nat_join_and hlo] + +theorem split_fromNat {k : ℕ} (hk : k + 1 ≤ 6) {a : UInt64} + (ha : a.toNat < 2 ^ 2 ^ (k + 1)) : + split (Nat.succ_pos k) (fromNat (k := k + 1) a.toNat) + = (fromNat (k := k) (a >>> UInt64.ofNat (2 ^ k)).toNat, + fromNat (k := k) (a &&& ((1 : UInt64) <<< UInt64.ofNat (2 ^ k) - 1)).toNat) := by + have hk5 : k ≤ 5 := by omega + refine (split_bitvec_eq_iff_fromNat (Nat.succ_pos k) _ _ _).mpr ⟨?_, ?_⟩ + · simp only [Nat.succ_sub_one] + congr 1 + rw [shiftRight_toNat hk5, toNat_fromNat ha] + · simp only [Nat.succ_sub_one] + congr 1 + rw [and_mask_toNat hk5, toNat_fromNat ha] + +theorem concrete_mul_eq_mul {k : ℕ} (x y : ConcreteBTField k) : + concrete_mul x y = x * y := rfl + +/-- `concrete_mul`'s one-level structure theorem, restated with all indices at the +half level `k` (the original lives at `k + 1 - 1`, which blocks syntactic rewriting). -/ +theorem concrete_mul_step {k : ℕ} (a b : ConcreteBTField (k + 1)) + {a₁ a₀ b₁ b₀ : ConcreteBTField k} + (ha : (a₁, a₀) = split (Nat.succ_pos k) a) (hb : (b₁, b₀) = split (Nat.succ_pos k) b) : + concrete_mul a b + = (《 concrete_mul a₀ b₁ + concrete_mul b₀ a₁ + + concrete_mul (concrete_mul a₁ b₁) (Z k), + concrete_mul a₀ b₀ + concrete_mul a₁ b₁ 》 : ConcreteBTField (k + 1)) := + (getBTFResult (k + 1)).mul_eq a b (Nat.succ_pos k) ha hb + +/-- `mulByZRec` computes multiplication by the level generator on the spec side. -/ +theorem mulByZRec_correct : ∀ (k : ℕ), k ≤ 6 → ∀ (v : UInt64), v.toNat < 2 ^ 2 ^ k → + fromNat (k := k) (mulByZRec k v).toNat = concrete_mul (fromNat v.toNat) (Z k) + | 0, _, v, hv => by + show fromNat (k := 0) v.toNat = concrete_mul (fromNat v.toNat) (Z 0) + rw [show Z 0 = ConcreteBinaryTower.one from rfl, concrete_mul_one0] + | k + 1, hk, v, hv => by + obtain ⟨hv1, hv0⟩ := half_lt hk hv + have hz := mulByZRec_lt k (Nat.le_of_succ_le hk) _ hv1 + have hIH := mulByZRec_correct k (Nat.le_of_succ_le hk) (v >>> UInt64.ofNat (2 ^ k)) hv1 + have hZsplit : ((one : ConcreteBTField k), (zero : ConcreteBTField k)) + = split (Nat.succ_pos k) (Z (k + 1)) := (split_Z (Nat.succ_pos k)).symm + have hme := concrete_mul_step (fromNat v.toNat) (Z (k + 1)) + (split_fromNat hk hv).symm hZsplit + simp only [mulByZRec_succ] + rw [fromNat_join hk (xor_lt hv0 hz) hv1, fromNat_xor, hIH] + refine Eq.trans ?_ hme.symm + simp only [concrete_mul_eq_mul, one_is_1, zero_is_0, mul_one, mul_zero, zero_mul, + add_zero, zero_add] + +/-- `mulRec` agrees with `concrete_mul` on in-range words. -/ +theorem mulRec_correct : ∀ (k : ℕ), k ≤ 6 → ∀ (a b : UInt64), + a.toNat < 2 ^ 2 ^ k → b.toNat < 2 ^ 2 ^ k → + fromNat (k := k) (mulRec k a b).toNat = concrete_mul (fromNat a.toNat) (fromNat b.toNat) + | 0, _, a, b, ha, hb => by + rcases eq_zero_or_one ha with rfl | rfl <;> rcases eq_zero_or_one hb with rfl | rfl + · show ConcreteBinaryTower.zero = concrete_mul zero zero + rw [concrete_zero_mul0] + · show ConcreteBinaryTower.zero = concrete_mul zero one + rw [concrete_zero_mul0] + · show ConcreteBinaryTower.zero = concrete_mul one zero + rw [concrete_mul_zero0] + · show ConcreteBinaryTower.one = concrete_mul one one + rw [concrete_mul_one0] + | k + 1, hk, a, b, ha, hb => by + have hk6 : k ≤ 6 := Nat.le_of_succ_le hk + obtain ⟨ha1, ha0⟩ := half_lt hk ha + obtain ⟨hb1, hb0⟩ := half_lt hk hb + have hp0 := mulRec_lt k hk6 _ _ ha0 hb0 + have hp2 := mulRec_lt k hk6 _ _ ha1 hb1 + have hp1 := mulRec_lt k hk6 _ _ (xor_lt ha0 ha1) (xor_lt hb0 hb1) + have hz := mulByZRec_lt k hk6 _ hp2 + have hZ := mulByZRec_correct k hk6 _ hp2 + have h00 := mulRec_correct k hk6 _ _ ha0 hb0 + have h11 := mulRec_correct k hk6 _ _ ha1 hb1 + have hss := mulRec_correct k hk6 _ _ (xor_lt ha0 ha1) (xor_lt hb0 hb1) + have hme := concrete_mul_step (fromNat a.toNat) (fromNat b.toNat) + (split_fromNat hk ha).symm (split_fromNat hk hb).symm + simp only [mulRec_succ] + rw [fromNat_join hk (xor_lt (xor_lt hp1 (xor_lt hp0 hp2)) hz) (xor_lt hp0 hp2)] + simp only [fromNat_xor] + rw [hZ, h00, h11, hss] + refine Eq.trans ?_ hme.symm + simp only [concrete_mul_eq_mul, fromNat_xor] + refine congrArg₂ (fun x y : ConcreteBTField k => (《 x, y 》 : ConcreteBTField (k + 1))) ?_ ?_ + · linear_combination (fromNat (k := k) (a &&& ((1 : UInt64) <<< UInt64.ofNat (2 ^ k) - 1)).toNat + * fromNat (k := k) (b &&& ((1 : UInt64) <<< UInt64.ofNat (2 ^ k) - 1)).toNat + + fromNat (k := k) (a >>> UInt64.ofNat (2 ^ k)).toNat + * fromNat (k := k) (b >>> UInt64.ofNat (2 ^ k)).toNat) + * CharTwo.two_eq_zero (R := ConcreteBTField k) + · rfl + +/-- `sqRec` computes the spec square on in-range words. -/ +theorem sqRec_correct : ∀ (k : ℕ), k ≤ 6 → ∀ (v : UInt64), v.toNat < 2 ^ 2 ^ k → + fromNat (k := k) (sqRec k v).toNat = concrete_mul (fromNat v.toNat) (fromNat v.toNat) + | 0, hk, v, hv => by + have h := mulRec_correct 0 hk v v hv hv + rwa [show mulRec 0 v v = v from UInt64.and_self] at h + | k + 1, hk, v, hv => by + have hk6 : k ≤ 6 := Nat.le_of_succ_le hk + obtain ⟨hv1, hv0⟩ := half_lt hk hv + have hs0 := sqRec_lt k hk6 _ hv0 + have hs1 := sqRec_lt k hk6 _ hv1 + have hz := mulByZRec_lt k hk6 _ hs1 + have hZ := mulByZRec_correct k hk6 _ hs1 + have h0 := sqRec_correct k hk6 _ hv0 + have h1 := sqRec_correct k hk6 _ hv1 + have hme := concrete_mul_step (fromNat v.toNat) (fromNat v.toNat) + (split_fromNat hk hv).symm (split_fromNat hk hv).symm + simp only [sqRec_succ] + rw [fromNat_join hk hz (xor_lt hs0 hs1), fromNat_xor, hZ, h0, h1] + refine Eq.trans ?_ hme.symm + simp only [concrete_mul_eq_mul] + rw [← two_mul, CharTwo.two_eq_zero (R := ConcreteBTField k), zero_mul, zero_add] + +theorem split_zero' {k : ℕ} : split (Nat.succ_pos k) (0 : ConcreteBTField (k + 1)) + = ((0 : ConcreteBTField k), (0 : ConcreteBTField k)) := split_zero (Nat.succ_pos k) + +theorem split_one' {k : ℕ} : split (Nat.succ_pos k) (1 : ConcreteBTField (k + 1)) + = ((0 : ConcreteBTField k), (1 : ConcreteBTField k)) := split_one (Nat.succ_pos k) + +/-- `concrete_inv`'s one-level descent, restated at the half level `k`; the `a = 0` +and `a = 1` branches satisfy the same formula. -/ +theorem concrete_inv_step {k : ℕ} (a : ConcreteBTField (k + 1)) + {a₁ a₀ : ConcreteBTField k} (ha : (a₁, a₀) = split (Nat.succ_pos k) a) : + concrete_inv a + = (《 (concrete_mul + (concrete_inv (concrete_mul a₀ (a₀ + concrete_mul a₁ (Z k)) + + concrete_mul a₁ a₁)) a₁ : ConcreteBTField k), + (concrete_mul + (concrete_inv (concrete_mul a₀ (a₀ + concrete_mul a₁ (Z k)) + + concrete_mul a₁ a₁)) (a₀ + concrete_mul a₁ (Z k)) : ConcreteBTField k) 》 : + ConcreteBTField (k + 1)) := by + by_cases h0 : a = 0 + · subst h0 + rw [split_zero'] at ha + obtain ⟨rfl, rfl⟩ := ha + rw [concrete_inv_zero] + simp only [concrete_mul_eq_mul, zero_mul, mul_zero, add_zero] + simp only [← zero_is_0] + exact (join_zero_zero (Nat.succ_pos k)).symm + by_cases h1 : a = 1 + · subst h1 + rw [split_one'] at ha + obtain ⟨rfl, rfl⟩ := ha + simp only [concrete_mul_eq_mul, zero_mul, mul_zero, mul_one, add_zero, concrete_inv_one] + simp only [← zero_is_0, ← one_is_1] + exact (join_zero_one (Nat.succ_pos k)).symm + · rw [concrete_inv, dif_neg (Nat.succ_ne_zero k), dif_neg h0, dif_neg h1] + simp_rw [← ha] + rfl + +/-- `invRec` agrees with `concrete_inv` on in-range words. -/ +theorem invRec_correct : ∀ (k : ℕ), k ≤ 6 → ∀ (v : UInt64), v.toNat < 2 ^ 2 ^ k → + fromNat (k := k) (invRec k v).toNat = concrete_inv (fromNat v.toNat) + | 0, _, v, hv => by + simp only [invRec] + rcases eq_zero_or_one hv with rfl | rfl + · rw [fromNat_zero, concrete_inv_zero] + · rw [fromNat_one, concrete_inv_one] + | k + 1, hk, v, hv => by + have hk6 : k ≤ 6 := Nat.le_of_succ_le hk + obtain ⟨hv1, hv0⟩ := half_lt hk hv + have hnext := xor_lt hv0 (mulByZRec_lt k hk6 _ hv1) + have hdel := xor_lt (mulRec_lt k hk6 _ _ hv0 hnext) (sqRec_lt k hk6 _ hv1) + have hd := invRec_lt k hk6 _ hdel + have hZ := mulByZRec_correct k hk6 _ hv1 + have hIH := invRec_correct k hk6 _ hdel + have hm0 := mulRec_correct k hk6 _ _ hv0 hnext + have hsq := sqRec_correct k hk6 _ hv1 + have hout1 := mulRec_correct k hk6 _ _ hd hv1 + have hout0 := mulRec_correct k hk6 _ _ hd hnext + have hme := concrete_inv_step (fromNat v.toNat) (split_fromNat hk hv).symm + simp only [invRec_succ] + rw [fromNat_join hk (mulRec_lt k hk6 _ _ hd hv1) (mulRec_lt k hk6 _ _ hd hnext), + hout1, hout0, hIH, fromNat_xor, hm0, hsq, fromNat_xor, hZ] + exact hme.symm + +/-! ## Carrier -/ + +/-- A level-`k` element in packed form: the low `2 ^ 2 ^ k` bits of a machine word, +sharing the `ConcreteBTField k` bit layout. Widths above 64 bits use `FastBT128`. -/ +structure FastBT (k : ℕ) where + val : UInt64 + isLt : val.toNat < 2 ^ 2 ^ k + +instance {k : ℕ} : DecidableEq (FastBT k) := fun a b => + decidable_of_iff (a.val = b.val) (by cases a; cases b; simp only [FastBT.mk.injEq]) + +/-- Truncating constructor from `ℕ`. -/ +@[inline] def ofNat (k n : ℕ) : FastBT k := + .mk (UInt64.ofNat (n % 2 ^ 2 ^ k)) <| by + show n % 2 ^ 2 ^ k % 2 ^ 64 < 2 ^ 2 ^ k + exact Nat.mod_lt_of_lt (Nat.mod_lt _ (Nat.two_pow_pos _)) + +/-- The canonical value of a packed element. -/ +def FastBT.toNat {k : ℕ} (x : FastBT k) : ℕ := x.val.toNat + +variable {k : ℕ} + +def zero : FastBT k := .mk 0 (Nat.two_pow_pos _) + +def one : FastBT k := .mk 1 (Nat.one_lt_two_pow_iff.mpr (Nat.two_pow_pos k).ne') + +instance : Zero (FastBT k) := ⟨zero⟩ +instance : One (FastBT k) := ⟨one⟩ + +/-- Addition is bitwise XOR. -/ +@[inline] def add (a b : FastBT k) : FastBT k := .mk (a.val ^^^ b.val) (xor_lt a.isLt b.isLt) + +instance : Add (FastBT k) where add + +instance : Neg (FastBT k) := ⟨id⟩ +instance : Sub (FastBT k) where sub a b := a + b +instance : SMul ℕ (FastBT k) := ⟨fun n x => if n % 2 = 0 then 0 else x⟩ +instance : SMul ℤ (FastBT k) := ⟨fun n x => if n % 2 = 0 then 0 else x⟩ +instance : NatCast (FastBT k) := ⟨fun n => if n % 2 = 0 then 0 else 1⟩ +instance : IntCast (FastBT k) := ⟨fun n => if n % 2 = 0 then 0 else 1⟩ + +@[simp] theorem val_zero : (0 : FastBT k).val = 0 := rfl +@[simp] theorem val_one : (1 : FastBT k).val = 1 := rfl +@[simp] theorem val_add (a b : FastBT k) : (a + b).val = a.val ^^^ b.val := rfl +@[simp] theorem neg_def (a : FastBT k) : -a = a := rfl +@[simp] theorem sub_def (a b : FastBT k) : a - b = a + b := rfl + +/-! ## Conversions -/ + +/-- The bridge into the `BitVec` model; bit layouts agree, so this is `toNat`-exact. -/ +def toConcrete (x : FastBT k) : ConcreteBTField k := fromNat x.val.toNat + +/-- Master bridge lemma: `toConcrete` preserves the numeric value. -/ +@[simp] theorem toConcrete_toNat (x : FastBT k) : + BitVec.toNat (toConcrete x) = x.val.toNat := toNat_fromNat x.isLt + +theorem toConcrete_injective : Function.Injective (toConcrete (k := k)) := by + intro a b h + have hval : a.val = b.val := by + have := congrArg BitVec.toNat h + rw [toConcrete_toNat, toConcrete_toNat] at this + exact UInt64.toNat_inj.mp this + cases a; cases b + simp only [FastBT.mk.injEq] + exact hval + +theorem ofConcrete_val_toNat {k : ℕ} (hk : k ≤ 6) (x : ConcreteBTField k) : + (UInt64.ofNat x.toNat).toNat = x.toNat := by + show x.toNat % 2 ^ 64 = x.toNat + refine Nat.mod_eq_of_lt (Nat.lt_of_lt_of_le x.isLt ?_) + exact Nat.pow_le_pow_right (by omega) + (Nat.le_trans (Nat.pow_le_pow_right (by omega) hk) (by norm_num)) + +/-- Repack a concrete element; one-word levels only (`k ≤ 6`). -/ +def ofConcrete {k : ℕ} (x : ConcreteBTField k) (hk : k ≤ 6 := by omega) : FastBT k := + .mk (UInt64.ofNat x.toNat) <| by rw [ofConcrete_val_toNat hk]; exact x.isLt + +@[simp] theorem toConcrete_ofConcrete {k : ℕ} (x : ConcreteBTField k) (hk : k ≤ 6) : + toConcrete (ofConcrete x hk) = x := by + show fromNat (UInt64.ofNat x.toNat).toNat = x + rw [ofConcrete_val_toNat hk] + exact fromNat_toNat x + +@[simp] theorem ofConcrete_toConcrete {k : ℕ} (a : FastBT k) (hk : k ≤ 6) : + ofConcrete (toConcrete a) hk = a := + toConcrete_injective (toConcrete_ofConcrete (toConcrete a) hk) + +@[simp] theorem toConcrete_zero : toConcrete (0 : FastBT k) = 0 := fromNat_zero + +@[simp] theorem toConcrete_one : toConcrete (1 : FastBT k) = 1 := fromNat_one + +@[simp] theorem toConcrete_add (a b : FastBT k) : + toConcrete (a + b) = toConcrete a + toConcrete b := fromNat_xor a.val b.val + +@[simp] theorem toConcrete_neg (a : FastBT k) : toConcrete (-a) = -(toConcrete a) := rfl + +@[simp] theorem toConcrete_sub (a b : FastBT k) : + toConcrete (a - b) = toConcrete a - toConcrete b := by + rw [sub_def, toConcrete_add, sub_eq_add_neg, ← toConcrete_neg, neg_def] + +theorem toConcrete_if_zero {p : Prop} [Decidable p] (x : FastBT k) : + toConcrete (if p then 0 else x) = if p then ConcreteBinaryTower.zero else toConcrete x := by + by_cases h : p + · rw [if_pos h, if_pos h, toConcrete_zero] + exact zero_is_0.symm + · rw [if_neg h, if_neg h] + +theorem toConcrete_nsmul (n : ℕ) (x : FastBT k) : + toConcrete (n • x) = n • toConcrete x := toConcrete_if_zero x + +theorem toConcrete_zsmul (n : ℤ) (x : FastBT k) : + toConcrete (n • x) = n • toConcrete x := toConcrete_if_zero x + +theorem toConcrete_natCast (n : ℕ) : + toConcrete (n : FastBT k) = (n : ConcreteBTField k) := by + rw [CharP.cast_eq_mod (ConcreteBTField k) 2 n] + show toConcrete (if n % 2 = 0 then 0 else 1) = _ + rcases (by omega : n % 2 = 0 ∨ n % 2 = 1) with h2 | h2 + · rw [if_pos h2, toConcrete_zero, h2, Nat.cast_zero] + · rw [if_neg (by omega), toConcrete_one, h2, Nat.cast_one] + +theorem toConcrete_intCast (n : ℤ) : + toConcrete (n : FastBT k) = (n : ConcreteBTField k) := by + rw [CharP.intCast_eq_intCast_mod (R := ConcreteBTField k) 2 (a := n), Nat.cast_ofNat] + show toConcrete (if n % 2 = 0 then 0 else 1) = _ + rcases (by omega : n % 2 = 0 ∨ n % 2 = 1) with h2 | h2 + · rw [if_pos h2, toConcrete_zero, h2, Int.cast_zero] + · rw [if_neg (by omega), toConcrete_one, h2, Int.cast_one] + +instance : AddCommGroup (FastBT k) := + toConcrete_injective.addCommGroup toConcrete toConcrete_zero toConcrete_add + toConcrete_neg toConcrete_sub (fun x n => toConcrete_nsmul n x) + (fun x n => toConcrete_zsmul n x) + +/-! ## Field operations + +`Mul`/`Inv` instances per usable width with their `toConcrete` transport lemmas; +`fieldOfHoms` assembles the `Field` instances from the injective bridge. -/ + +/-- Level-3 elements, GF(2^8). -/ +abbrev BT8 := FastBT 3 +/-- Level-4 elements, GF(2^16). -/ +abbrev BT16 := FastBT 4 +/-- Level-5 elements, GF(2^32). -/ +abbrev BT32 := FastBT 5 +/-- Level-6 elements, GF(2^64). -/ +abbrev BT64 := FastBT 6 + +/-- GF(2^8) carrier multiplication. -/ +@[inline] def BT8.mul (a b : BT8) : BT8 := + .mk (mul8T a.val b.val) (by rw [mul8T_eq_mul8 a.isLt b.isLt]; exact mul8_lt a.isLt b.isLt) +/-- GF(2^16) carrier multiplication. -/ +@[inline] def BT16.mul (a b : BT16) : BT16 := + .mk (mul16T a.val b.val) (by rw [mul16T_eq_mul16 a.isLt b.isLt]; exact mul16_lt a.isLt b.isLt) +/-- GF(2^32) carrier multiplication. -/ +@[inline] def BT32.mul (a b : BT32) : BT32 := + .mk (mul32T a.val b.val) (by rw [mul32T_eq_mul32 a.isLt b.isLt]; exact mul32_lt a.isLt b.isLt) +/-- GF(2^64) carrier multiplication. -/ +@[inline] def BT64.mul (a b : BT64) : BT64 := .mk (mul64T a.val b.val) (UInt64.toNat_lt _) + +instance : Mul BT8 := ⟨BT8.mul⟩ +instance : Mul BT16 := ⟨BT16.mul⟩ +instance : Mul BT32 := ⟨BT32.mul⟩ +instance : Mul BT64 := ⟨BT64.mul⟩ + +@[simp] theorem val_mul_bt8 (a b : BT8) : (a * b).val = mul8 a.val b.val := + mul8T_eq_mul8 a.isLt b.isLt +@[simp] theorem val_mul_bt16 (a b : BT16) : (a * b).val = mul16 a.val b.val := + mul16T_eq_mul16 a.isLt b.isLt +@[simp] theorem val_mul_bt32 (a b : BT32) : (a * b).val = mul32 a.val b.val := + mul32T_eq_mul32 a.isLt b.isLt +@[simp] theorem val_mul_bt64 (a b : BT64) : (a * b).val = mul64 a.val b.val := + mul64T_eq_mul64 a.val b.val + +@[simp] theorem toConcrete_mul_bt8 (a b : BT8) : + toConcrete (a * b) = toConcrete a * toConcrete b := by + show fromNat (mul8T a.val b.val).toNat = _ + rw [mul8T_eq_mul8 a.isLt b.isLt, mul8_eq_rec] + exact mulRec_correct 3 (by omega) a.val b.val a.isLt b.isLt + +@[simp] theorem toConcrete_mul_bt16 (a b : BT16) : + toConcrete (a * b) = toConcrete a * toConcrete b := by + show fromNat (mul16T a.val b.val).toNat = _ + rw [mul16T_eq_mul16 a.isLt b.isLt, mul16_eq_rec] + exact mulRec_correct 4 (by omega) a.val b.val a.isLt b.isLt + +@[simp] theorem toConcrete_mul_bt32 (a b : BT32) : + toConcrete (a * b) = toConcrete a * toConcrete b := by + show fromNat (mul32T a.val b.val).toNat = _ + rw [mul32T_eq_mul32 a.isLt b.isLt, mul32_eq_rec] + exact mulRec_correct 5 (by omega) a.val b.val a.isLt b.isLt + +@[simp] theorem toConcrete_mul_bt64 (a b : BT64) : + toConcrete (a * b) = toConcrete a * toConcrete b := by + show fromNat (mul64T a.val b.val).toNat = _ + rw [mul64T_eq_mul64 a.val b.val, mul64_eq_rec] + exact mulRec_correct 6 (by omega) a.val b.val a.isLt b.isLt + +instance : Inv BT8 := + ⟨fun a => .mk (inv8T a.val) (by rw [inv8T_eq_inv8 a.isLt]; exact inv8_lt a.isLt)⟩ +instance : Inv BT16 := + ⟨fun a => .mk (inv16T a.val) (by rw [inv16T_eq_inv16 a.isLt]; exact inv16_lt a.isLt)⟩ +instance : Inv BT32 := + ⟨fun a => .mk (inv32T a.val) (by rw [inv32T_eq_inv32 a.isLt]; exact inv32_lt a.isLt)⟩ +instance : Inv BT64 := ⟨fun a => .mk (inv64T a.val) (UInt64.toNat_lt _)⟩ + +@[simp] theorem val_inv_bt8 (a : BT8) : (a⁻¹).val = inv8 a.val := inv8T_eq_inv8 a.isLt +@[simp] theorem val_inv_bt16 (a : BT16) : (a⁻¹).val = inv16 a.val := inv16T_eq_inv16 a.isLt +@[simp] theorem val_inv_bt32 (a : BT32) : (a⁻¹).val = inv32 a.val := inv32T_eq_inv32 a.isLt +@[simp] theorem val_inv_bt64 (a : BT64) : (a⁻¹).val = inv64 a.val := inv64T_eq_inv64 a.val + +@[simp] theorem toConcrete_inv_bt8 (a : BT8) : toConcrete a⁻¹ = (toConcrete a)⁻¹ := by + show fromNat (inv8T a.val).toNat = _ + rw [inv8T_eq_inv8 a.isLt, inv8_eq_rec] + exact invRec_correct 3 (by omega) a.val a.isLt + +@[simp] theorem toConcrete_inv_bt16 (a : BT16) : toConcrete a⁻¹ = (toConcrete a)⁻¹ := by + show fromNat (inv16T a.val).toNat = _ + rw [inv16T_eq_inv16 a.isLt, inv16_eq_rec] + exact invRec_correct 4 (by omega) a.val a.isLt + +@[simp] theorem toConcrete_inv_bt32 (a : BT32) : toConcrete a⁻¹ = (toConcrete a)⁻¹ := by + show fromNat (inv32T a.val).toNat = _ + rw [inv32T_eq_inv32 a.isLt, inv32_eq_rec] + exact invRec_correct 5 (by omega) a.val a.isLt + +@[simp] theorem toConcrete_inv_bt64 (a : BT64) : toConcrete a⁻¹ = (toConcrete a)⁻¹ := by + show fromNat (inv64T a.val).toNat = _ + rw [inv64T_eq_inv64 a.val, inv64_eq_rec] + exact invRec_correct 6 (by omega) a.val a.isLt + +theorem toConcrete_npowRec {k : ℕ} [Mul (FastBT k)] + (hmul : ∀ a b : FastBT k, toConcrete (a * b) = toConcrete a * toConcrete b) + (a : FastBT k) : ∀ (n : ℕ), toConcrete (npowRec n a) = toConcrete a ^ n + | 0 => by rw [npowRec, pow_zero, toConcrete_one] + | n + 1 => by rw [npowRec, pow_succ, hmul, toConcrete_npowRec hmul a n] + +/-- Assemble a width's `Field` instance from its `toConcrete` multiplication and +inversion lemmas. -/ +@[reducible] def fieldOfHoms {k : ℕ} [Mul (FastBT k)] [Inv (FastBT k)] + (hmul : ∀ a b : FastBT k, toConcrete (a * b) = toConcrete a * toConcrete b) + (hinv : ∀ a : FastBT k, toConcrete a⁻¹ = (toConcrete a)⁻¹) : Field (FastBT k) := + letI : Pow (FastBT k) ℕ := ⟨fun a n => npowRec n a⟩ + letI cr : CommRing (FastBT k) := toConcrete_injective.commRing toConcrete + toConcrete_zero toConcrete_one toConcrete_add hmul toConcrete_neg toConcrete_sub + toConcrete_nsmul toConcrete_zsmul (fun a n => toConcrete_npowRec hmul a n) + toConcrete_natCast toConcrete_intCast + { cr with + inv := Inv.inv + exists_pair_ne := ⟨0, 1, fun h => zero_ne_one (α := ConcreteBTField k) + (by rw [← toConcrete_zero, ← toConcrete_one (k := k), h])⟩ + mul_inv_cancel := fun a ha => toConcrete_injective (by + rw [hmul, hinv, toConcrete_one] + exact mul_inv_cancel₀ fun h0 => ha (toConcrete_injective + (by rw [h0, toConcrete_zero]))) + inv_zero := toConcrete_injective (by rw [hinv, toConcrete_zero, inv_zero]) + qsmul := _ + nnqsmul := _ } + +instance : Field BT8 := fieldOfHoms toConcrete_mul_bt8 toConcrete_inv_bt8 +instance : Field BT16 := fieldOfHoms toConcrete_mul_bt16 toConcrete_inv_bt16 +instance : Field BT32 := fieldOfHoms toConcrete_mul_bt32 toConcrete_inv_bt32 +instance : Field BT64 := fieldOfHoms toConcrete_mul_bt64 toConcrete_inv_bt64 + +@[reducible] def ringEquivOfHom {k : ℕ} [Mul (FastBT k)] (hk : k ≤ 6) + (hmul : ∀ a b : FastBT k, toConcrete (a * b) = toConcrete a * toConcrete b) : + FastBT k ≃+* ConcreteBTField k where + toFun := toConcrete + invFun x := ofConcrete x hk + left_inv a := ofConcrete_toConcrete a hk + right_inv x := toConcrete_ofConcrete x hk + map_mul' := hmul + map_add' := toConcrete_add + +/-- Ring isomorphism between `BT8` and the concrete level-3 tower field. -/ +def ringEquivBT8 : BT8 ≃+* ConcreteBTField 3 := ringEquivOfHom (by omega) toConcrete_mul_bt8 +/-- Ring isomorphism between `BT16` and the concrete level-4 tower field. -/ +def ringEquivBT16 : BT16 ≃+* ConcreteBTField 4 := ringEquivOfHom (by omega) toConcrete_mul_bt16 +/-- Ring isomorphism between `BT32` and the concrete level-5 tower field. -/ +def ringEquivBT32 : BT32 ≃+* ConcreteBTField 5 := ringEquivOfHom (by omega) toConcrete_mul_bt32 +/-- Ring isomorphism between `BT64` and the concrete level-6 tower field. -/ +def ringEquivBT64 : BT64 ≃+* ConcreteBTField 6 := ringEquivOfHom (by omega) toConcrete_mul_bt64 + +/-- Multiply by the level generator `Z k`; one-word levels only (`k ≤ 6`). -/ +@[inline] def FastBT.mulByZ {k : ℕ} (a : FastBT k) (_hk : k ≤ 6 := by omega) : FastBT k := + match k, a with + | 0, a => a + | 1, a => .mk (mulByZ1 a.val) (by rw [mulByZ1_eq_rec]; exact mulByZRec_lt 1 (by omega) _ a.isLt) + | 2, a => .mk (mulByZ2 a.val) (by rw [mulByZ2_eq_rec]; exact mulByZRec_lt 2 (by omega) _ a.isLt) + | 3, a => .mk (mulByZ3T a.val) + (by rw [mulByZ3T_eq_mulByZ3 a.isLt]; exact mulByZ3_lt a.isLt) + | 4, a => .mk (mulByZ4T a.val) + (by rw [mulByZ4T_eq_mulByZ4 a.isLt]; exact mulByZ4_lt a.isLt) + | 5, a => .mk (mulByZ5T a.val) + (by rw [mulByZ5T_eq_mulByZ5 a.isLt]; exact mulByZ5_lt a.isLt) + | 6, a => .mk (mulByZ6T a.val) (UInt64.toNat_lt _) + | _ + 7, a => a + +theorem FastBT.mulByZ_val : ∀ {k : ℕ} (a : FastBT k) (hk : k ≤ 6), + (a.mulByZ hk).val = mulByZRec k a.val + | 0, _, _ => rfl + | 1, a, _ => mulByZ1_eq_rec a.val + | 2, a, _ => mulByZ2_eq_rec a.val + | 3, a, _ => (mulByZ3T_eq_mulByZ3 a.isLt).trans (mulByZ3_eq_rec a.val) + | 4, a, _ => (mulByZ4T_eq_mulByZ4 a.isLt).trans (mulByZ4_eq_rec a.val) + | 5, a, _ => (mulByZ5T_eq_mulByZ5 a.isLt).trans (mulByZ5_eq_rec a.val) + | 6, a, _ => (mulByZ6T_eq_mulByZ6 a.val).trans (mulByZ6_eq_rec a.val) + | _ + 7, _, hk => absurd hk (by omega) + +theorem toConcrete_mulByZ {k : ℕ} (a : FastBT k) (hk : k ≤ 6) : + toConcrete (a.mulByZ hk) = toConcrete a * Z k := by + show fromNat (a.mulByZ hk).val.toNat = _ + rw [a.mulByZ_val hk] + exact mulByZRec_correct k hk a.val a.isLt + +/-- Square via the dedicated ladder, cheaper than `a * a`; one-word levels only. -/ +@[inline] def FastBT.square {k : ℕ} (a : FastBT k) (_hk : k ≤ 6 := by omega) : FastBT k := + match k, a with + | 0, a => a + | 1, a => .mk (sq2 a.val) (by rw [sq2_eq_rec]; exact sqRec_lt 1 (by omega) _ a.isLt) + | 2, a => .mk (sq4 a.val) (by rw [sq4_eq_rec]; exact sqRec_lt 2 (by omega) _ a.isLt) + | 3, a => .mk (sq8T a.val) (by rw [sq8T_eq_sq8 a.isLt]; exact sq8_lt a.isLt) + | 4, a => .mk (sq16T a.val) (by rw [sq16T_eq_sq16 a.isLt]; exact sq16_lt a.isLt) + | 5, a => .mk (sq32T a.val) (by rw [sq32T_eq_sq32 a.isLt]; exact sq32_lt a.isLt) + | 6, a => .mk (sq64T a.val) (UInt64.toNat_lt _) + | _ + 7, a => a + +theorem FastBT.square_val : ∀ {k : ℕ} (a : FastBT k) (hk : k ≤ 6), + (a.square hk).val = sqRec k a.val + | 0, _, _ => rfl + | 1, a, _ => sq2_eq_rec a.val + | 2, a, _ => sq4_eq_rec a.val + | 3, a, _ => (sq8T_eq_sq8 a.isLt).trans (sq8_eq_rec a.val) + | 4, a, _ => (sq16T_eq_sq16 a.isLt).trans (sq16_eq_rec a.val) + | 5, a, _ => (sq32T_eq_sq32 a.isLt).trans (sq32_eq_rec a.val) + | 6, a, _ => (sq64T_eq_sq64 a.val).trans (sq64_eq_rec a.val) + | _ + 7, _, hk => absurd hk (by omega) + +theorem toConcrete_square {k : ℕ} (a : FastBT k) (hk : k ≤ 6) : + toConcrete (a.square hk) = toConcrete a * toConcrete a := by + show fromNat (a.square hk).val.toNat = _ + rw [a.square_val hk] + exact sqRec_correct k hk a.val a.isLt + +/-! ## Level 7: GF(2^128) + +The tower split falls on the limb boundary, so the halves are the limbs. -/ + +theorem join_add_join {k : ℕ} (a b c d : ConcreteBTField k) : + (《 a, b 》 : ConcreteBTField (k + 1)) + (《 c, d 》 : ConcreteBTField (k + 1)) + = (《 a + c, b + d 》 : ConcreteBTField (k + 1)) := + join_of_split (Nat.succ_pos k) _ _ _ + (split_sum_eq_sum_split (Nat.succ_pos k) _ _ a b c d + (split_join_eq_split (Nat.succ_pos k) a b) (split_join_eq_split (Nat.succ_pos k) c d)) + +/-! Width-64 spec forms of the word operations; every `UInt64` is in range at level 6. -/ + +theorem fromNat_mul64 (a b : UInt64) : + fromNat (k := 6) (mul64 a b).toNat = concrete_mul (fromNat a.toNat) (fromNat b.toNat) := by + rw [mul64_eq_rec] + exact mulRec_correct 6 le_rfl a b (UInt64.toNat_lt a) (UInt64.toNat_lt b) + +theorem fromNat_sq64 (v : UInt64) : + fromNat (k := 6) (sq64 v).toNat = concrete_mul (fromNat v.toNat) (fromNat v.toNat) := by + rw [sq64_eq_rec] + exact sqRec_correct 6 le_rfl v (UInt64.toNat_lt v) + +theorem fromNat_mulByZ6 (v : UInt64) : + fromNat (k := 6) (mulByZ6 v).toNat = concrete_mul (fromNat v.toNat) (Z 6) := by + rw [mulByZ6_eq_rec] + exact mulByZRec_correct 6 le_rfl v (UInt64.toNat_lt v) + +theorem fromNat_inv64 (v : UInt64) : + fromNat (k := 6) (inv64 v).toNat = concrete_inv (fromNat v.toNat) := by + rw [inv64_eq_rec] + exact invRec_correct 6 le_rfl v (UInt64.toNat_lt v) + +namespace FastBT128 + +instance : Zero FastBT128 := ⟨0, 0⟩ +instance : One FastBT128 := ⟨1, 0⟩ + +instance : Add FastBT128 := ⟨add⟩ +instance : Neg FastBT128 := ⟨id⟩ +instance : Sub FastBT128 where sub a b := a + b +instance : Mul FastBT128 := ⟨mul⟩ + +/-! ### Conversions and algebra + +At level 7 the tower halves are the limbs, so `toConcrete` maps into the join directly +and each correctness proof is one application of the level-6 results. -/ + +instance : SMul ℕ FastBT128 := ⟨fun n x => if n % 2 = 0 then 0 else x⟩ +instance : SMul ℤ FastBT128 := ⟨fun n x => if n % 2 = 0 then 0 else x⟩ +instance : NatCast FastBT128 := ⟨fun n => if n % 2 = 0 then 0 else 1⟩ +instance : IntCast FastBT128 := ⟨fun n => if n % 2 = 0 then 0 else 1⟩ +instance : Inv FastBT128 := ⟨inv⟩ + +@[simp] theorem neg_def (a : FastBT128) : -a = a := rfl +@[simp] theorem sub_def (a b : FastBT128) : a - b = a + b := rfl + +/-- The bridge into the `BitVec` model, with the limbs as the tower halves. -/ +def toConcrete (v : FastBT128) : ConcreteBTField 7 := + (《 fromNat (k := 6) v.hi.toNat, fromNat (k := 6) v.lo.toNat 》 : ConcreteBTField 7) + +theorem toConcrete_injective : Function.Injective toConcrete := by + intro a b h + obtain ⟨h1, h0⟩ := (join_eq_join_iff (Nat.succ_pos 6) _ _ _ _).mp h + have hhi : a.hi = b.hi := UInt64.toNat_inj.mp (by + have h' := congrArg BitVec.toNat h1 + rwa [toNat_fromNat (UInt64.toNat_lt _), toNat_fromNat (UInt64.toNat_lt _)] at h') + have hlo : a.lo = b.lo := UInt64.toNat_inj.mp (by + have h' := congrArg BitVec.toNat h0 + rwa [toNat_fromNat (UInt64.toNat_lt _), toNat_fromNat (UInt64.toNat_lt _)] at h') + cases a; cases b + simp only [FastBT128.mk.injEq] + exact ⟨hlo, hhi⟩ + +@[simp] theorem toConcrete_zero : toConcrete (0 : FastBT128) = 0 := by + show (《 fromNat (k := 6) (0 : UInt64).toNat, fromNat (k := 6) (0 : UInt64).toNat 》 : + ConcreteBTField 7) = 0 + rw [fromNat_zero] + simp only [← zero_is_0] + exact join_zero_zero (Nat.succ_pos 6) + +@[simp] theorem toConcrete_one : toConcrete (1 : FastBT128) = 1 := by + show (《 fromNat (k := 6) (0 : UInt64).toNat, fromNat (k := 6) (1 : UInt64).toNat 》 : + ConcreteBTField 7) = 1 + rw [fromNat_zero, fromNat_one] + simp only [← zero_is_0, ← one_is_1] + exact join_zero_one (Nat.succ_pos 6) + +@[simp] theorem toConcrete_add (a b : FastBT128) : + toConcrete (a + b) = toConcrete a + toConcrete b := by + show (《 fromNat (k := 6) (a.hi ^^^ b.hi).toNat, fromNat (k := 6) (a.lo ^^^ b.lo).toNat 》 : + ConcreteBTField 7) = _ + rw [fromNat_xor, fromNat_xor, ← join_add_join] + rfl + +@[simp] theorem toConcrete_neg (a : FastBT128) : toConcrete (-a) = -(toConcrete a) := rfl + +@[simp] theorem toConcrete_sub (a b : FastBT128) : + toConcrete (a - b) = toConcrete a - toConcrete b := by + rw [sub_def, toConcrete_add, sub_eq_add_neg, ← toConcrete_neg, neg_def] + +theorem toConcrete_if_zero {p : Prop} [Decidable p] (x : FastBT128) : + toConcrete (if p then 0 else x) = if p then ConcreteBinaryTower.zero else toConcrete x := by + by_cases h : p + · rw [if_pos h, if_pos h, toConcrete_zero] + exact zero_is_0.symm + · rw [if_neg h, if_neg h] + +theorem toConcrete_nsmul (n : ℕ) (x : FastBT128) : + toConcrete (n • x) = n • toConcrete x := toConcrete_if_zero x + +theorem toConcrete_zsmul (n : ℤ) (x : FastBT128) : + toConcrete (n • x) = n • toConcrete x := toConcrete_if_zero x + +instance : AddCommGroup FastBT128 := + toConcrete_injective.addCommGroup toConcrete toConcrete_zero toConcrete_add + toConcrete_neg toConcrete_sub (fun x n => toConcrete_nsmul n x) + (fun x n => toConcrete_zsmul n x) + +theorem toConcrete_natCast (n : ℕ) : + toConcrete (n : FastBT128) = (n : ConcreteBTField 7) := by + rw [CharP.cast_eq_mod (ConcreteBTField 7) 2 n] + show toConcrete (if n % 2 = 0 then 0 else 1) = _ + rcases (by omega : n % 2 = 0 ∨ n % 2 = 1) with h2 | h2 + · rw [if_pos h2, toConcrete_zero, h2, Nat.cast_zero] + · rw [if_neg (by omega), toConcrete_one, h2, Nat.cast_one] + +theorem toConcrete_intCast (n : ℤ) : + toConcrete (n : FastBT128) = (n : ConcreteBTField 7) := by + rw [CharP.intCast_eq_intCast_mod (R := ConcreteBTField 7) 2 (a := n), Nat.cast_ofNat] + show toConcrete (if n % 2 = 0 then 0 else 1) = _ + rcases (by omega : n % 2 = 0 ∨ n % 2 = 1) with h2 | h2 + · rw [if_pos h2, toConcrete_zero, h2, Int.cast_zero] + · rw [if_neg (by omega), toConcrete_one, h2, Int.cast_one] + +@[simp] theorem toConcrete_mul (a b : FastBT128) : + toConcrete (a * b) = toConcrete a * toConcrete b := by + have hme := concrete_mul_step (toConcrete a) (toConcrete b) + (split_of_join (Nat.succ_pos 6) (toConcrete a) (fromNat (k := 6) a.hi.toNat) + (fromNat (k := 6) a.lo.toNat) rfl) + (split_of_join (Nat.succ_pos 6) (toConcrete b) (fromNat (k := 6) b.hi.toNat) + (fromNat (k := 6) b.lo.toNat) rfl) + show (《 fromNat (k := 6) (mul64T (a.lo ^^^ a.hi) (b.lo ^^^ b.hi) + ^^^ (mul64T a.lo b.lo ^^^ mul64T a.hi b.hi) + ^^^ mulByZ6T (mul64T a.hi b.hi)).toNat, + fromNat (k := 6) (mul64T a.lo b.lo ^^^ mul64T a.hi b.hi).toNat 》 : + ConcreteBTField 7) = _ + simp only [mul64T_eq_mul64, mulByZ6T_eq_mulByZ6, fromNat_xor, fromNat_mul64, + fromNat_mulByZ6] + refine Eq.trans ?_ hme.symm + simp only [concrete_mul_eq_mul] + refine congrArg₂ (fun x y : ConcreteBTField 6 => (《 x, y 》 : ConcreteBTField 7)) ?_ ?_ + · linear_combination (fromNat (k := 6) a.lo.toNat * fromNat (k := 6) b.lo.toNat + + fromNat (k := 6) a.hi.toNat * fromNat (k := 6) b.hi.toNat) + * CharTwo.two_eq_zero (R := ConcreteBTField 6) + · rfl + +theorem toConcrete_mulByZ (v : FastBT128) : + toConcrete v.mulByZ = toConcrete v * Z 7 := by + have hZsplit : ((ConcreteBinaryTower.one : ConcreteBTField 6), + (ConcreteBinaryTower.zero : ConcreteBTField 6)) + = split (Nat.succ_pos 6) (Z 7) := (split_Z (Nat.succ_pos 6)).symm + have hme := concrete_mul_step (toConcrete v) (Z 7) + (split_of_join (Nat.succ_pos 6) (toConcrete v) (fromNat (k := 6) v.hi.toNat) + (fromNat (k := 6) v.lo.toNat) rfl) hZsplit + show (《 fromNat (k := 6) (v.lo ^^^ mulByZ6T v.hi).toNat, fromNat (k := 6) v.hi.toNat 》 : + ConcreteBTField 7) = _ + simp only [mulByZ6T_eq_mulByZ6, fromNat_xor, fromNat_mulByZ6] + refine Eq.trans ?_ hme.symm + simp only [concrete_mul_eq_mul, one_is_1, zero_is_0, mul_one, mul_zero, zero_mul, + add_zero, zero_add] + +theorem toConcrete_square (v : FastBT128) : + toConcrete v.square = toConcrete v * toConcrete v := by + have hme := concrete_mul_step (toConcrete v) (toConcrete v) + (split_of_join (Nat.succ_pos 6) (toConcrete v) (fromNat (k := 6) v.hi.toNat) + (fromNat (k := 6) v.lo.toNat) rfl) + (split_of_join (Nat.succ_pos 6) (toConcrete v) (fromNat (k := 6) v.hi.toNat) + (fromNat (k := 6) v.lo.toNat) rfl) + show (《 fromNat (k := 6) (mulByZ6T (sq64T v.hi)).toNat, + fromNat (k := 6) (sq64T v.lo ^^^ sq64T v.hi).toNat 》 : ConcreteBTField 7) = _ + simp only [sq64T_eq_sq64, mulByZ6T_eq_mulByZ6, fromNat_xor, fromNat_sq64, + fromNat_mulByZ6] + refine Eq.trans ?_ hme.symm + simp only [concrete_mul_eq_mul] + rw [← two_mul, CharTwo.two_eq_zero (R := ConcreteBTField 6), zero_mul, zero_add] + +@[simp] theorem toConcrete_inv (v : FastBT128) : toConcrete v⁻¹ = (toConcrete v)⁻¹ := by + have hme := concrete_inv_step (toConcrete v) + (split_of_join (Nat.succ_pos 6) (toConcrete v) (fromNat (k := 6) v.hi.toNat) + (fromNat (k := 6) v.lo.toNat) rfl) + show (《 fromNat (k := 6) + (mul64T (inv64T (mul64T v.lo (v.lo ^^^ mulByZ6T v.hi) ^^^ sq64T v.hi)) v.hi).toNat, + fromNat (k := 6) + (mul64T (inv64T (mul64T v.lo (v.lo ^^^ mulByZ6T v.hi) ^^^ sq64T v.hi)) + (v.lo ^^^ mulByZ6T v.hi)).toNat 》 : ConcreteBTField 7) = _ + simp only [mul64T_eq_mul64, mulByZ6T_eq_mulByZ6, sq64T_eq_sq64, inv64T_eq_inv64, + fromNat_xor, fromNat_mul64, fromNat_sq64, fromNat_mulByZ6, fromNat_inv64] + exact hme.symm + +theorem toConcrete_npowRec (a : FastBT128) : + ∀ (n : ℕ), toConcrete (npowRec n a) = toConcrete a ^ n + | 0 => by rw [npowRec, pow_zero, toConcrete_one] + | n + 1 => by rw [npowRec, pow_succ, toConcrete_mul, toConcrete_npowRec a n] + +instance : Field FastBT128 := + letI : Pow FastBT128 ℕ := ⟨fun a n => npowRec n a⟩ + letI cr : CommRing FastBT128 := toConcrete_injective.commRing toConcrete + toConcrete_zero toConcrete_one toConcrete_add toConcrete_mul toConcrete_neg + toConcrete_sub toConcrete_nsmul toConcrete_zsmul + (fun a n => toConcrete_npowRec a n) toConcrete_natCast toConcrete_intCast + { cr with + inv := Inv.inv + exists_pair_ne := ⟨0, 1, fun h => zero_ne_one (α := ConcreteBTField 7) + (by rw [← toConcrete_zero, ← toConcrete_one, h])⟩ + mul_inv_cancel := fun a ha => toConcrete_injective (by + rw [toConcrete_mul, toConcrete_inv, toConcrete_one] + exact mul_inv_cancel₀ fun h0 => ha (toConcrete_injective + (by rw [h0, toConcrete_zero]))) + inv_zero := toConcrete_injective (by rw [toConcrete_inv, toConcrete_zero, inv_zero]) + qsmul := _ + nnqsmul := _ } + +/-- Repack a concrete level-7 element into limbs. -/ +def ofConcrete (x : ConcreteBTField 7) : FastBT128 := ofNat x.toNat + +@[simp] theorem toConcrete_ofConcrete (x : ConcreteBTField 7) : + toConcrete (ofConcrete x) = x := by + refine ((join_eq_bitvec_iff_fromNat (Nat.succ_pos 6) x _ _).mpr ⟨?_, ?_⟩).symm + · simp only [Nat.succ_sub_one] + congr 1 + show (x.toNat >>> 64) % 2 ^ 64 = x.toNat >>> 2 ^ 6 + refine Nat.mod_eq_of_lt ?_ + rw [Nat.shiftRight_eq_div_pow] + exact Nat.div_lt_of_lt_mul (by rw [← Nat.pow_add]; exact x.isLt) + · simp only [Nat.succ_sub_one] + congr 1 + show x.toNat % 2 ^ 64 = x.toNat &&& 2 ^ 2 ^ 6 - 1 + rw [Nat.and_two_pow_sub_one_eq_mod] + rfl + +@[simp] theorem ofConcrete_toConcrete (a : FastBT128) : ofConcrete (toConcrete a) = a := + toConcrete_injective (toConcrete_ofConcrete (toConcrete a)) + +/-- Ring isomorphism between `FastBT128` and the concrete level-7 tower field. -/ +def ringEquiv : FastBT128 ≃+* ConcreteBTField 7 where + toFun := toConcrete + invFun := ofConcrete + left_inv := ofConcrete_toConcrete + right_inv := toConcrete_ofConcrete + map_mul' := toConcrete_mul + map_add' := toConcrete_add + +end FastBT128 + +end ConcreteBinaryTower.Fast diff --git a/CompPoly/Fields/Binary/Tower/FastDefs.lean b/CompPoly/Fields/Binary/Tower/FastDefs.lean new file mode 100644 index 00000000..9771dd41 --- /dev/null +++ b/CompPoly/Fields/Binary/Tower/FastDefs.lean @@ -0,0 +1,412 @@ +/- +Copyright (c) 2026 CompPoly Contributors. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Georgios Raikos +-/ +module + +/-! +# Fast binary tower: runtime definitions (zero-import) + +The runtime definitions of the packed binary tower arithmetic, split out of +`CompPoly.Fields.Binary.Tower.Fast` verbatim. All correctness statements about them +live in that sibling module, which imports this one. + +This module deliberately has **zero imports**: downstream consumers put it into +`precompileModules` native-compilation lanes, and `precompileModules` compiles the +entire import closure, so the runtime definitions must not pull in mathlib. +-/ + +@[expose] public section + +namespace ConcreteBinaryTower.Fast + +/-! ## Raw word operations + +Names carry the operand bit width (`mul8` multiplies level-3 values in the low 8 bits); +`mulByZk` multiplies by the tower generator `Z k`. A multiplication rung recombines the +Karatsuba half-products `p0 = a₀b₀`, `p2 = a₁b₁`, `p1 = (a₀+a₁)(b₀+b₁)` as +`lo = p0 + p2`, `hi = p1 + lo + Z·p2`; squaring drops the cross term; inversion is the +quadratic descent of `concrete_inv`. Inputs are assumed in range. -/ + +/-- GF(4) multiplication (level 1). `Z 0 = 1` collapses the generic recombination +`hi = p1 + lo + Z·p2` to `hi = p1 + p0`, saving a xor on the ladder's hottest rung. -/ +@[inline] def mul2 (a b : UInt64) : UInt64 := + let a0 := a &&& 1 + let a1 := a >>> 1 + let b0 := b &&& 1 + let b1 := b >>> 1 + let p0 := a0 &&& b0 + let p2 := a1 &&& b1 + let p1 := (a0 ^^^ a1) &&& (b0 ^^^ b1) + ((p1 ^^^ p0) <<< 1) ||| (p0 ^^^ p2) + +/-- Multiplication by the level-1 generator `Z 1`. -/ +@[inline] def mulByZ1 (v : UInt64) : UInt64 := + let v0 := v &&& 1 + let v1 := v >>> 1 + ((v0 ^^^ v1) <<< 1) ||| v1 + +/-- GF(4) squaring. -/ +@[inline] def sq2 (v : UInt64) : UInt64 := + let v0 := v &&& 1 + let v1 := v >>> 1 + (v1 <<< 1) ||| (v0 ^^^ v1) + +/-- GF(4) inversion (`0 ↦ 0`), in the shape of the recursive twin so `inv2_eq_rec` +is `rfl`. -/ +@[inline] def inv2 (v : UInt64) : UInt64 := + let v0 := v &&& 1 + let v1 := v >>> 1 + let next := v0 ^^^ v1 + let delta := (v0 &&& next) ^^^ v1 + ((delta &&& v1) <<< 1) ||| (delta &&& next) + +/-- GF(2^4) multiplication (level 2). -/ +@[inline] def mul4 (a b : UInt64) : UInt64 := + let a0 := a &&& 0x3 + let a1 := a >>> 2 + let b0 := b &&& 0x3 + let b1 := b >>> 2 + let p0 := mul2 a0 b0 + let p2 := mul2 a1 b1 + let p1 := mul2 (a0 ^^^ a1) (b0 ^^^ b1) + let lo := p0 ^^^ p2 + ((p1 ^^^ lo ^^^ mulByZ1 p2) <<< 2) ||| lo + +/-- Multiplication by the level-2 generator `Z 2`. -/ +@[inline] def mulByZ2 (v : UInt64) : UInt64 := + let v0 := v &&& 0x3 + let v1 := v >>> 2 + ((v0 ^^^ mulByZ1 v1) <<< 2) ||| v1 + +/-- GF(2^4) squaring. -/ +@[inline] def sq4 (v : UInt64) : UInt64 := + let s0 := sq2 (v &&& 0x3) + let s1 := sq2 (v >>> 2) + ((mulByZ1 s1) <<< 2) ||| (s0 ^^^ s1) + +/-- GF(2^4) inversion (`0 ↦ 0`). -/ +@[inline] def inv4 (v : UInt64) : UInt64 := + let v0 := v &&& 0x3 + let v1 := v >>> 2 + let next := v0 ^^^ mulByZ1 v1 + let delta := mul2 v0 next ^^^ sq2 v1 + let d := inv2 delta + ((mul2 d v1) <<< 2) ||| (mul2 d next) + +/-- GF(2^8) multiplication (level 3). -/ +@[inline] def mul8 (a b : UInt64) : UInt64 := + let a0 := a &&& 0xF + let a1 := a >>> 4 + let b0 := b &&& 0xF + let b1 := b >>> 4 + let p0 := mul4 a0 b0 + let p2 := mul4 a1 b1 + let p1 := mul4 (a0 ^^^ a1) (b0 ^^^ b1) + let lo := p0 ^^^ p2 + ((p1 ^^^ lo ^^^ mulByZ2 p2) <<< 4) ||| lo + +/-- Multiplication by the level-3 generator `Z 3`. -/ +@[inline] def mulByZ3 (v : UInt64) : UInt64 := + let v0 := v &&& 0xF + let v1 := v >>> 4 + ((v0 ^^^ mulByZ2 v1) <<< 4) ||| v1 + +/-- GF(2^8) squaring. -/ +@[inline] def sq8 (v : UInt64) : UInt64 := + let s0 := sq4 (v &&& 0xF) + let s1 := sq4 (v >>> 4) + ((mulByZ2 s1) <<< 4) ||| (s0 ^^^ s1) + +/-- GF(2^8) inversion (`0 ↦ 0`). -/ +@[inline] def inv8 (v : UInt64) : UInt64 := + let v0 := v &&& 0xF + let v1 := v >>> 4 + let next := v0 ^^^ mulByZ2 v1 + let delta := mul4 v0 next ^^^ sq4 v1 + let d := inv4 delta + ((mul4 d v1) <<< 4) ||| (mul4 d next) + +/-- GF(2^16) multiplication (level 4). -/ +@[inline] def mul16 (a b : UInt64) : UInt64 := + let a0 := a &&& 0xFF + let a1 := a >>> 8 + let b0 := b &&& 0xFF + let b1 := b >>> 8 + let p0 := mul8 a0 b0 + let p2 := mul8 a1 b1 + let p1 := mul8 (a0 ^^^ a1) (b0 ^^^ b1) + let lo := p0 ^^^ p2 + ((p1 ^^^ lo ^^^ mulByZ3 p2) <<< 8) ||| lo + +/-- Multiplication by the level-4 generator `Z 4`. -/ +@[inline] def mulByZ4 (v : UInt64) : UInt64 := + let v0 := v &&& 0xFF + let v1 := v >>> 8 + ((v0 ^^^ mulByZ3 v1) <<< 8) ||| v1 + +/-- GF(2^16) squaring. -/ +@[inline] def sq16 (v : UInt64) : UInt64 := + let s0 := sq8 (v &&& 0xFF) + let s1 := sq8 (v >>> 8) + ((mulByZ3 s1) <<< 8) ||| (s0 ^^^ s1) + +/-- GF(2^16) inversion (`0 ↦ 0`). -/ +@[inline] def inv16 (v : UInt64) : UInt64 := + let v0 := v &&& 0xFF + let v1 := v >>> 8 + let next := v0 ^^^ mulByZ3 v1 + let delta := mul8 v0 next ^^^ sq8 v1 + let d := inv8 delta + ((mul8 d v1) <<< 8) ||| (mul8 d next) + +/-- GF(2^32) multiplication (level 5). Outlined: inlining the whole ladder above this +width exceeds the compiler's recursion depth. -/ +def mul32 (a b : UInt64) : UInt64 := + let a0 := a &&& 0xFFFF + let a1 := a >>> 16 + let b0 := b &&& 0xFFFF + let b1 := b >>> 16 + let p0 := mul16 a0 b0 + let p2 := mul16 a1 b1 + let p1 := mul16 (a0 ^^^ a1) (b0 ^^^ b1) + let lo := p0 ^^^ p2 + ((p1 ^^^ lo ^^^ mulByZ4 p2) <<< 16) ||| lo + +/-- Multiplication by the level-5 generator `Z 5`. -/ +@[inline] def mulByZ5 (v : UInt64) : UInt64 := + let v0 := v &&& 0xFFFF + let v1 := v >>> 16 + ((v0 ^^^ mulByZ4 v1) <<< 16) ||| v1 + +/-- GF(2^32) squaring. -/ +@[inline] def sq32 (v : UInt64) : UInt64 := + let s0 := sq16 (v &&& 0xFFFF) + let s1 := sq16 (v >>> 16) + ((mulByZ4 s1) <<< 16) ||| (s0 ^^^ s1) + +/-- GF(2^32) inversion (`0 ↦ 0`). -/ +@[inline] def inv32 (v : UInt64) : UInt64 := + let v0 := v &&& 0xFFFF + let v1 := v >>> 16 + let next := v0 ^^^ mulByZ4 v1 + let delta := mul16 v0 next ^^^ sq16 v1 + let d := inv16 delta + ((mul16 d v1) <<< 16) ||| (mul16 d next) + +/-- GF(2^64) multiplication (level 6). Outlined, see `mul32`. -/ +def mul64 (a b : UInt64) : UInt64 := + let a0 := a &&& 0xFFFFFFFF + let a1 := a >>> 32 + let b0 := b &&& 0xFFFFFFFF + let b1 := b >>> 32 + let p0 := mul32 a0 b0 + let p2 := mul32 a1 b1 + let p1 := mul32 (a0 ^^^ a1) (b0 ^^^ b1) + let lo := p0 ^^^ p2 + ((p1 ^^^ lo ^^^ mulByZ5 p2) <<< 32) ||| lo + +/-- Multiplication by the level-6 generator `Z 6`. -/ +@[inline] def mulByZ6 (v : UInt64) : UInt64 := + let v0 := v &&& 0xFFFFFFFF + let v1 := v >>> 32 + ((v0 ^^^ mulByZ5 v1) <<< 32) ||| v1 + +/-- GF(2^64) squaring. -/ +@[inline] def sq64 (v : UInt64) : UInt64 := + let s0 := sq32 (v &&& 0xFFFFFFFF) + let s1 := sq32 (v >>> 32) + ((mulByZ5 s1) <<< 32) ||| (s0 ^^^ s1) + +/-- GF(2^64) inversion (`0 ↦ 0`). -/ +@[inline] def inv64 (v : UInt64) : UInt64 := + let v0 := v &&& 0xFFFFFFFF + let v1 := v >>> 32 + let next := v0 ^^^ mulByZ5 v1 + let delta := mul32 v0 next ^^^ sq32 v1 + let d := inv32 delta + ((mul32 d v1) <<< 32) ||| (mul32 d next) + +/-! ## GF(2^8) tables and table-based rungs + +Level-3 operations as byte-table lookups, tables generated from the ladder by +`Array.ofFn`; the wider `*T` rungs rebuild the ladder on the table base. -/ + +/-- `mul8` product table. -/ +def mul8Table : ByteArray := + ⟨Array.ofFn (n := 65536) fun i => + (mul8 (UInt64.ofNat (i / 256)) (UInt64.ofNat (i % 256))).toUInt8⟩ + +/-- `mulByZ3` table. -/ +def mulByZ3Table : ByteArray := + ⟨Array.ofFn (n := 256) fun i => (mulByZ3 (UInt64.ofNat i)).toUInt8⟩ + +/-- `sq8` table. -/ +def sq8Table : ByteArray := + ⟨Array.ofFn (n := 256) fun i => (sq8 (UInt64.ofNat i)).toUInt8⟩ + +/-- `inv8` table (`0 ↦ 0`). -/ +def inv8Table : ByteArray := + ⟨Array.ofFn (n := 256) fun i => (inv8 (UInt64.ofNat i)).toUInt8⟩ + +/-- `mul8` by table lookup. -/ +@[inline] def mul8T (a b : UInt64) : UInt64 := + (mul8Table.get! ((a <<< 8) + b).toNat).toUInt64 + +/-- `mulByZ3` by table lookup. -/ +@[inline] def mulByZ3T (v : UInt64) : UInt64 := (mulByZ3Table.get! v.toNat).toUInt64 + +/-- `sq8` by table lookup. -/ +@[inline] def sq8T (v : UInt64) : UInt64 := (sq8Table.get! v.toNat).toUInt64 + +/-- `inv8` by table lookup. -/ +@[inline] def inv8T (v : UInt64) : UInt64 := (inv8Table.get! v.toNat).toUInt64 + +/-- Table-based twin of `mulByZ4`. -/ +@[inline] def mulByZ4T (v : UInt64) : UInt64 := + let v0 := v &&& 0xFF + let v1 := v >>> 8 + ((v0 ^^^ mulByZ3T v1) <<< 8) ||| v1 + +/-- Table-based twin of `mulByZ5`. -/ +@[inline] def mulByZ5T (v : UInt64) : UInt64 := + let v0 := v &&& 0xFFFF + let v1 := v >>> 16 + ((v0 ^^^ mulByZ4T v1) <<< 16) ||| v1 + +/-- Table-based twin of `mulByZ6`. -/ +@[inline] def mulByZ6T (v : UInt64) : UInt64 := + let v0 := v &&& 0xFFFFFFFF + let v1 := v >>> 32 + ((v0 ^^^ mulByZ5T v1) <<< 32) ||| v1 + +/-- Table-based twin of `mul16`. -/ +@[inline] def mul16T (a b : UInt64) : UInt64 := + let a0 := a &&& 0xFF + let a1 := a >>> 8 + let b0 := b &&& 0xFF + let b1 := b >>> 8 + let p0 := mul8T a0 b0 + let p2 := mul8T a1 b1 + let p1 := mul8T (a0 ^^^ a1) (b0 ^^^ b1) + let lo := p0 ^^^ p2 + ((p1 ^^^ lo ^^^ mulByZ3T p2) <<< 8) ||| lo + +/-- Table-based twin of `mul32`. -/ +def mul32T (a b : UInt64) : UInt64 := + let a0 := a &&& 0xFFFF + let a1 := a >>> 16 + let b0 := b &&& 0xFFFF + let b1 := b >>> 16 + let p0 := mul16T a0 b0 + let p2 := mul16T a1 b1 + let p1 := mul16T (a0 ^^^ a1) (b0 ^^^ b1) + let lo := p0 ^^^ p2 + ((p1 ^^^ lo ^^^ mulByZ4T p2) <<< 16) ||| lo + +/-- Table-based twin of `mul64`. -/ +def mul64T (a b : UInt64) : UInt64 := + let a0 := a &&& 0xFFFFFFFF + let a1 := a >>> 32 + let b0 := b &&& 0xFFFFFFFF + let b1 := b >>> 32 + let p0 := mul32T a0 b0 + let p2 := mul32T a1 b1 + let p1 := mul32T (a0 ^^^ a1) (b0 ^^^ b1) + let lo := p0 ^^^ p2 + ((p1 ^^^ lo ^^^ mulByZ5T p2) <<< 32) ||| lo + +/-- Table-based twin of `sq16`. -/ +@[inline] def sq16T (v : UInt64) : UInt64 := + let s0 := sq8T (v &&& 0xFF) + let s1 := sq8T (v >>> 8) + ((mulByZ3T s1) <<< 8) ||| (s0 ^^^ s1) + +/-- Table-based twin of `sq32`. -/ +@[inline] def sq32T (v : UInt64) : UInt64 := + let s0 := sq16T (v &&& 0xFFFF) + let s1 := sq16T (v >>> 16) + ((mulByZ4T s1) <<< 16) ||| (s0 ^^^ s1) + +/-- Table-based twin of `sq64`. -/ +@[inline] def sq64T (v : UInt64) : UInt64 := + let s0 := sq32T (v &&& 0xFFFFFFFF) + let s1 := sq32T (v >>> 32) + ((mulByZ5T s1) <<< 32) ||| (s0 ^^^ s1) + +/-- Table-based twin of `inv16`. -/ +@[inline] def inv16T (v : UInt64) : UInt64 := + let v0 := v &&& 0xFF + let v1 := v >>> 8 + let next := v0 ^^^ mulByZ3T v1 + let delta := mul8T v0 next ^^^ sq8T v1 + let d := inv8T delta + ((mul8T d v1) <<< 8) ||| (mul8T d next) + +/-- Table-based twin of `inv32`. -/ +@[inline] def inv32T (v : UInt64) : UInt64 := + let v0 := v &&& 0xFFFF + let v1 := v >>> 16 + let next := v0 ^^^ mulByZ4T v1 + let delta := mul16T v0 next ^^^ sq16T v1 + let d := inv16T delta + ((mul16T d v1) <<< 16) ||| (mul16T d next) + +/-- Table-based twin of `inv64`. -/ +@[inline] def inv64T (v : UInt64) : UInt64 := + let v0 := v &&& 0xFFFFFFFF + let v1 := v >>> 32 + let next := v0 ^^^ mulByZ5T v1 + let delta := mul32T v0 next ^^^ sq32T v1 + let d := inv32T delta + ((mul32T d v1) <<< 32) ||| (mul32T d next) + +/-! ## Level 7: GF(2^128) + +The tower split falls on the limb boundary, so the halves are the limbs. -/ + +/-- A level-7 tower field element as two limbs, `lo` the low half. -/ +structure FastBT128 where + lo : UInt64 + hi : UInt64 + deriving DecidableEq, Inhabited + +namespace FastBT128 + +/-- Addition is limbwise XOR. -/ +@[inline] def add (a b : FastBT128) : FastBT128 := ⟨a.lo ^^^ b.lo, a.hi ^^^ b.hi⟩ + +/-- Multiplication: Karatsuba over the limbs with a `Z 6` generator reduction. -/ +@[inline] def mul (a b : FastBT128) : FastBT128 := + let p0 := mul64T a.lo b.lo + let p2 := mul64T a.hi b.hi + let p1 := mul64T (a.lo ^^^ a.hi) (b.lo ^^^ b.hi) + let lo := p0 ^^^ p2 + ⟨lo, p1 ^^^ lo ^^^ mulByZ6T p2⟩ + +/-- Multiply by the level-7 generator `Z 7`: swap halves, fold `Z 6` into the new high. -/ +@[inline] def mulByZ (v : FastBT128) : FastBT128 := ⟨v.hi, v.lo ^^^ mulByZ6T v.hi⟩ + +/-- Squaring: the Karatsuba cross term vanishes in characteristic 2. -/ +@[inline] def square (v : FastBT128) : FastBT128 := + let s0 := sq64T v.lo + let s1 := sq64T v.hi + ⟨s0 ^^^ s1, mulByZ6T s1⟩ + +/-- Inversion by quadratic descent (`0 ↦ 0`); same recursion as `concrete_inv`. -/ +@[inline] def inv (v : FastBT128) : FastBT128 := + let next := v.lo ^^^ mulByZ6T v.hi + let delta := mul64T v.lo next ^^^ sq64T v.hi + let d := inv64T delta + ⟨mul64T d next, mul64T d v.hi⟩ + +/-- Truncating constructor from `Nat`, low limb first. -/ +def ofNat (n : Nat) : FastBT128 := ⟨UInt64.ofNat n, UInt64.ofNat (n >>> 64)⟩ + +/-- The canonical value of a two-limb element. -/ +def toNat (v : FastBT128) : Nat := v.lo.toNat + v.hi.toNat * 2 ^ 64 + +end FastBT128 + +end ConcreteBinaryTower.Fast diff --git a/CompPoly/Fields/Binary/Tower/TensorAlgebra.lean b/CompPoly/Fields/Binary/Tower/TensorAlgebra.lean index 8b198a53..340454ab 100644 --- a/CompPoly/Fields/Binary/Tower/TensorAlgebra.lean +++ b/CompPoly/Fields/Binary/Tower/TensorAlgebra.lean @@ -5,130 +5,10 @@ Authors: Chung Thai Nguyen, Quang Dao -/ module -public import Mathlib.Algebra.MvPolynomial.Basic -public import Mathlib.GroupTheory.MonoidLocalization.Basic -public import Mathlib.LinearAlgebra.TensorProduct.Basis -public import Mathlib.RingTheory.TensorProduct.Maps -public import Mathlib.LinearAlgebra.StdBasis +public import CompPoly.LinearAlgebra.TensorProduct.Basis /-! -# Generalized Tensor Algebra and Dual View +# Tensor product bases for binary towers -This file develops the algebraic theory of the tensor product algebra `A := R ⊗[K] C` -for arbitrary field extensions `R/K` and `C/K`, upon the existing `TensorProduct` module. - -## Main Definitions - -- `Basis.baseChangeRight` : the lift of a basis of `Left` to an `Right`-basis - of the base change `Left ⊗[K] Right`. - -## TODOs -- multilinear bases of tensor algebra over binary tower subfields -- Proximity Gap for Tensor Algebras - -## References - -* [Lang, S., *Algebra*][Lan02] -* [Diamond, B.E. and Posen, J., *Polylogarithmic Proofs for Multilinears over Binary Towers*][DP24] -* [Diamond, B.E. and Posen, J., *Succinct arguments over towers of binary fields*][DP23] --/ - -@[expose] public section -open TensorProduct - -section DualView --- This section formalizes the dual view for any finite field extensions. - -variable {K : Type*} {Left : Type*} {Right : Type*} {ι : Type*} - [CommSemiring K] [CommSemiring Left] [CommSemiring Right] - [Algebra K Left] [Algebra K Right] - -noncomputable instance : CommSemiring (Left ⊗[K] Right) := Algebra.TensorProduct.instCommSemiring -noncomputable instance : Algebra K (Left ⊗[K] Right) := Algebra.TensorProduct.instAlgebra -noncomputable instance : Algebra Left (Left ⊗[K] Right) := Algebra.TensorProduct.leftAlgebra -noncomputable instance : Algebra Right (Left ⊗[K] Right) := Algebra.TensorProduct.rightAlgebra - --- Let's create a local notation `e` for the equivalence to make the code more readable. -local notation "e" => Algebra.TensorProduct.comm K Right Left - --- The lemma with the completed proof. -lemma comm_map_smul_tmul (s s' : Right) (m : Left) : - e (s • (s' ⊗ₜ[K] m)) = s • (e (s' ⊗ₜ[K] m)) := by - -- Unfold the scalar multiplication on both sides using its fundamental definition. - -- `r • x` is defined as `algebraMap K A r * x`. - rw [Algebra.smul_def, Algebra.smul_def] - -- Now that `algebraMap` is exposed, we can specify which algebra instance to use. - -- On the LHS, the instance is `leftAlgebra`. - rw [show algebraMap Right (Right ⊗[K] Left) = - (Algebra.TensorProduct.includeLeftRingHom).comp (algebraMap Right Right) by rfl] - -- On the RHS, we need to specify the `rightAlgebra` instance for `Left ⊗[K] Right`. - -- We must first apply `comm` to get the expression into the right form. - rw [Algebra.TensorProduct.comm_tmul] - -- Now the term is `(algebraMap Right (Left ⊗[K] Right) s) * (m ⊗ₜ[K] s')`. - -- We specify the algebraMap for the `rightAlgebra`. - rw [show algebraMap Right (Left ⊗[K] Right) = - (Algebra.TensorProduct.includeRight).toRingHom.comp (algebraMap Right Right) by rfl] - -- Now, simplify all the compositions and applications. - simp only [Algebra.algebraMap_self, RingHomCompTriple.comp_eq, - Algebra.TensorProduct.includeLeftRingHom_apply, Algebra.TensorProduct.tmul_mul_tmul, one_mul, - Algebra.TensorProduct.comm_tmul, AlgHom.toRingHom_eq_coe, RingHom.coe_coe, - Algebra.TensorProduct.includeRight_apply] - -lemma comm_map_smul_add (s : Right) (x y : Right ⊗[K] Left) - (hx : e (s • x) = s • (e x)) (hy : e (s • y) = s • (e y)) : - e (s • x) + e (s • y) = s • e x + s • e y := by - -- This follows from the fact that `smul` and `e` are both additive homomorphisms. - simp only [hx, hy] - -/-- -A helper definition to package `Algebra.TensorProduct.comm` as an `Right`-linear equivalence. -It takes the existing K-algebra equivalence and adds a proof that it is also Right-linear. -We make the types explicit arguments to avoid type inference issues. +This module re-exports the generic tensor basis API with explicit right scalar actions. -/ -noncomputable def commSEquiv : Right ⊗[K] Left ≃ₗ[Right] Left ⊗[K] Right := - { Algebra.TensorProduct.comm K Right Left with - map_smul' := fun s x => by - -- The proof that the commutativity map respects the Right-scalar action. - induction x using TensorProduct.induction_on with - | zero => - simp only [AlgEquiv.toEquiv_eq_coe, smul_zero, Equiv.toFun_as_coe, EquivLike.coe_coe, - map_zero, RingHom.id_apply]; - | tmul s' m => - simp only [AlgEquiv.toEquiv_eq_coe, Equiv.toFun_as_coe, EquivLike.coe_coe, RingHom.id_apply, - Algebra.TensorProduct.comm_tmul]; - -- ⊢ e (s • s' ⊗ₜ[K] m) = s • m ⊗ₜ[K] s' - exact comm_map_smul_tmul (Right:=Right) (Left:=Left) s s' m - | add x y hx hy => - simp only [AlgEquiv.toEquiv_eq_coe, Equiv.toFun_as_coe, EquivLike.coe_coe, RingHom.id_apply, - smul_add, - map_add] at * - exact comm_map_smul_add (Right:=Right) (Left:=Left) s x y hx hy - } - -open Module -/-- -The lift of an `K`-basis of `Left` to an `Right`-basis of the base change `Left ⊗[K] Right`. -This is the right-sided counterpart to `Basis.baseChange`. --/ -noncomputable def Basis.baseChangeRight (b : Basis ι K Left) : Basis ι Right (Left ⊗[K] Right) := by - -- We now call our helper with explicit arguments. - exact (b.baseChange Right).map (commSEquiv (Right:=Right) (Left:=Left)) - -lemma Basis.baseChangeRight_repr_tmul (b : Basis ι K Left) (x y i) : - (Basis.baseChangeRight (b:=b) (Right:=Right)).repr (x ⊗ₜ y) i = b.repr x i • y := by - rw [Basis.baseChangeRight] - rw [Basis.map_repr] -- rewrite the Basis mapped via the AlgEquiv (commSEquiv Right Left) - simp only [commSEquiv, AlgEquiv.toEquiv_eq_coe, Equiv.toFun_as_coe, EquivLike.coe_coe, - Equiv.invFun_as_coe, AlgEquiv.symm_toEquiv_eq_symm, Algebra.TensorProduct.comm_symm, - LinearEquiv.trans_apply, LinearEquiv.coe_symm_mk, Algebra.TensorProduct.comm_tmul, - Basis.baseChange_repr_tmul] - -@[simp] -lemma Basis.baseChangeRight_apply (b : Basis ι K Left) (i : ι) : - (Basis.baseChangeRight (b:=b) (Right:=Right)) i = b i ⊗ₜ[K] 1 := by - simp only [baseChangeRight, Basis.baseChange, commSEquiv, AlgEquiv.toEquiv_eq_coe, - Equiv.toFun_as_coe, EquivLike.coe_coe, Equiv.invFun_as_coe, AlgEquiv.symm_toEquiv_eq_symm, - Basis.map_apply, Basis.coe_reindex, Function.comp_apply, Equiv.punitProd_symm_apply, - Basis.tensorProduct_apply, Basis.singleton_apply, LinearEquiv.coe_mk, LinearMap.coe_mk, - AddHom.coe_mk, Algebra.TensorProduct.comm_tmul] -end DualView diff --git a/CompPoly/Fields/Extension.lean b/CompPoly/Fields/Extension.lean index b22c177c..b2ad6908 100644 --- a/CompPoly/Fields/Extension.lean +++ b/CompPoly/Fields/Extension.lean @@ -5,8 +5,10 @@ Authors: Derek Sorensen -/ module +public import CompPoly.Fields.Extension.Arithmetic public import CompPoly.Fields.Extension.Binomial public import CompPoly.Fields.Extension.Bridge +public import CompPoly.Fields.Extension.Cardinality public import CompPoly.Fields.Extension.Defs public import CompPoly.Fields.Extension.Field @@ -19,12 +21,14 @@ individual modules for details: * `CompPoly/Fields/Extension/Binomial.lean` — irreducibility of `X^d - W` over a finite field, via Rabin's test collapsed to two base-field exponentiations. -* `CompPoly/Fields/Extension/Defs.lean` — `ExtensionParams` (an arbitrary monic modulus) and the +* `CompPoly/Fields/Extension/Arithmetic.lean` — `ExtensionParams` (an arbitrary monic modulus), the binomial front-end `BinomialParams`, plus the coefficient-vector carrier `Ext P` with its ring - operations (`shiftReduce`, `monomialMod`, `mul`). + operations (`shiftReduce`, `monomialMod`, `mul`) and the inverse candidate. +* `CompPoly/Fields/Extension/Defs.lean` — polynomial specifications and binomial correspondence. * `CompPoly/Fields/Extension/Bridge.lean` — `toQuot : Ext P → AdjoinRoot P.poly`, the multiply-by-`X` law `toQuot_shiftReduce`, and the `CommRing` structure. -* `CompPoly/Fields/Extension/Field.lean` — bijectivity, cardinality, and the `Field` structure. +* `CompPoly/Fields/Extension/Cardinality.lean` — optional finiteness and cardinality certificates. +* `CompPoly/Fields/Extension/Field.lean` — bijectivity and the certified `Field` structure. -/ @[expose] public section diff --git a/CompPoly/Fields/Extension/Arithmetic.lean b/CompPoly/Fields/Extension/Arithmetic.lean new file mode 100644 index 00000000..8e006607 --- /dev/null +++ b/CompPoly/Fields/Extension/Arithmetic.lean @@ -0,0 +1,379 @@ +/- +Copyright (c) 2026 CompPoly Contributors. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Derek Sorensen +-/ +module + +public import Mathlib.Algebra.Ring.Defs +public import Mathlib.Algebra.BigOperators.Fin + +/-! +# Computable arithmetic for monic quotient presentations + +`ExtensionParams F` stores the degree, lower modulus coefficients and an explicit inverse-exponent +parameter `q`. `Ext P` is one nominal carrier with coefficient vectors in ascending degree order. +Its coordinate maps require no algebraic structure; arithmetic uses a ring of coefficients. +The presentation index separates parameter values for a fixed coefficient algebra. Operations +and their theorems are relative to the supplied ring or field structure on `F`; the raw carrier +does not separate alternative algebra structures on that same type. Distinct coefficient +presentations should use nominal coefficient types and explicit ring maps. The polynomial quotient +interpretation uses commutative coefficients; the raw formulas are defined for any ring. + +Multiplication reduces monomials with `shiftReduce`. The executable reduction-table implementation +`mulTbl` is connected to `mul` by `mul_eq_mulTbl`. Power uses binary exponentiation, and the +canonical inverse candidate uses the literal exponent `q^d - 2`. + +This module supplies no cardinality or irreducibility certificate. Polynomial specifications and +quotient correspondence belong to `Extension/Defs.lean` and `Extension/Bridge.lean`; certified field +laws belong to `Extension/Field.lean`. Raw parameters with an incorrect `q` still admit arithmetic +but do not thereby acquire field laws. The binomial conversion describes the modulus `X^d - W`. +-/ + +@[expose] public section + +namespace CompPoly.Extension + +variable {F : Type*} [Ring F] + +/-- +The data defining an extension `F[X] / f` by a monic modulus `f` of degree `d`. + +The modulus is stored by its `d` lower coefficients: `f = X^d + ∑_{i < d} lower[i] · X^i`. The +leading coefficient is an implicit `1`, so `f` is monic by construction. + +Irreducibility is not part of these parameters: the quotient is a commutative ring for every +monic modulus over a commutative ring. Field laws additionally require finite-cardinality and +irreducibility certificates. +-/ +structure ExtensionParams (F : Type*) where + /-- The degree of the extension. -/ + d : ℕ + /-- Degree at least two; a degree-one "extension" is just `F`. -/ + two_le : 2 ≤ d + /-- The lower coefficients of the monic modulus, little-endian: `lower[i]` is the coefficient + of `X^i` in `poly`, for `i < d`. The coefficient of `X^d` is an implicit `1`. -/ + lower : Vector F d + /-- The proposed base cardinality, stored explicitly for the inverse exponent. + + Raw arithmetic does not certify this value. Field laws require a separate proof that + `Nat.card F = q`, together with finiteness and irreducibility. -/ + q : ℕ + +namespace ExtensionParams + +variable (P : ExtensionParams F) + +/-- The coefficient of `X^i` in the lower part of the modulus. -/ +@[inline] def lowerCoeff (i : Fin P.d) : F := P.lower[i.val] + +/-- The lower modulus coefficient at index `k`, or zero when `P.d ≤ k`. -/ +def lowerCoeffNat (k : ℕ) : F := if h : k < P.d then P.lower[k] else 0 + +@[simp] theorem lowerCoeffNat_coe (i : Fin P.d) : P.lowerCoeffNat (i : ℕ) = P.lowerCoeff i := by + rw [lowerCoeffNat, dif_pos i.isLt]; rfl + +theorem lowerCoeffNat_of_ge {k : ℕ} (h : P.d ≤ k) : P.lowerCoeffNat k = 0 := dif_neg (by omega) + +omit [Ring F] in +theorem d_pos : 0 < P.d := by have := P.two_le; omega + +end ExtensionParams + +/-- +The carrier of the quotient by the monic modulus in `P`, with coefficients in ascending +order of powers. The parameter remains part of the type even when two moduli have equal degree. +-/ +structure Ext {F : Type*} (P : ExtensionParams F) : Type _ where + /-- The coefficient of `X^i` is stored at index `i`. -/ + coeffs : Vector F P.d + +namespace Ext + +variable {P : ExtensionParams F} + +/-- Build an element from coefficients in ascending order of powers. -/ +@[inline] def ofVector (v : Vector F P.d) : Ext P := ⟨v⟩ + +omit [Ring F] in +/-- Extracting the coefficients of a constructed element returns the input vector. -/ +@[simp] theorem coeffs_ofVector (v : Vector F P.d) : coeffs (ofVector (P := P) v) = v := rfl + +omit [Ring F] in +/-- Reconstructing an element from its coefficient vector returns that element. -/ +@[simp] theorem ofVector_coeffs (x : Ext P) : ofVector (coeffs x) = x := rfl + +omit [Ring F] in +/-- The coefficient vector uniquely determines an element. -/ +theorem coeffs_injective : Function.Injective (coeffs (P := P)) := + fun _ _ h => congrArg ofVector h + +/-- Build an element from a coefficient function. -/ +@[inline] def ofFn (g : Fin P.d → F) : Ext P := ofVector (Vector.ofFn g) + +/-- The coefficient of `X^i`. -/ +@[inline] def coeff (x : Ext P) (i : Fin P.d) : F := (coeffs x)[i.val] + +omit [Ring F] in +@[simp] theorem coeff_ofFn (g : Fin P.d → F) (i : Fin P.d) : coeff (ofFn g) i = g i := by + simp [coeff, ofFn, ofVector] + +omit [Ring F] in +/-- Two elements with the same coefficients are equal. -/ +@[ext] theorem ext {x y : Ext P} (h : ∀ i, coeff x i = coeff y i) : x = y := + coeffs_injective (Vector.ext fun i hi => h ⟨i, hi⟩) + +omit [Ring F] in +theorem ofFn_coeff (x : Ext P) : ofFn (coeff x) = x := by ext i; simp + +/-- Coefficient vectors are exactly functions out of `Fin d`. -/ +def equivFn (P : ExtensionParams F) : Ext P ≃ (Fin P.d → F) where + toFun := coeff + invFun := ofFn + left_inv := ofFn_coeff + right_inv g := funext fun i => coeff_ofFn g i + +/-- The coefficient at index `i`, or zero when `P.d ≤ i`. -/ +def coeffNat (x : Ext P) (i : ℕ) : F := if h : i < P.d then coeff x ⟨i, h⟩ else 0 + +@[simp] theorem coeffNat_coe (x : Ext P) (i : Fin P.d) : coeffNat x (i : ℕ) = coeff x i := by + rw [coeffNat, dif_pos i.isLt] + +theorem coeffNat_of_lt (x : Ext P) {i : ℕ} (h : i < P.d) : coeffNat x i = coeff x ⟨i, h⟩ := + dif_pos h + +theorem coeffNat_of_ge (x : Ext P) {i : ℕ} (h : P.d ≤ i) : coeffNat x i = 0 := + dif_neg (by omega) + +/-! ### Distinguished elements + +`ofBase` places a coefficient-ring element in the constant coordinate, and `gen` has the +coordinates of `X`. The quotient bridge over a field promotes the constant embedding to an +`Algebra` structure and identifies the reduced power `gen ^ d` with `monomialMod d`. +-/ + +/-- Place a coefficient-ring element in the constant coordinate. -/ +@[inline] def ofBase (c : F) : Ext P := ofFn fun i => if (i : ℕ) = 0 then c else 0 + +/-- The coordinate vector with coefficient one at `X` and zero elsewhere. -/ +def gen : Ext P := ofFn fun i => if (i : ℕ) = 1 then 1 else 0 + +/-! ### Operations + +Multiplication is defined in terms of `shiftReduce` — the "multiply by `X`, reduce mod `f`" +map — whose iterates `monomialMod k = shiftReduce^[k] 1` are the reduced monomials `X^k mod f`. +Everything downstream is proved from the single homomorphism law +`toQuot (shiftReduce e) = rt * toQuot e`. +-/ + +instance : Zero (Ext P) := ⟨ofFn fun _ => 0⟩ +instance : One (Ext P) := ⟨ofFn fun i => if (i : ℕ) = 0 then 1 else 0⟩ +instance : Add (Ext P) := ⟨fun x y => ofFn fun i => coeff x i + coeff y i⟩ +instance : Neg (Ext P) := ⟨fun x => ofFn fun i => -coeff x i⟩ +instance : Sub (Ext P) := ⟨fun x y => ofFn fun i => coeff x i - coeff y i⟩ +instance : SMul F (Ext P) := ⟨fun c x => ofFn fun i => c * coeff x i⟩ + +/-- +Multiply by `X` and reduce modulo `f`. + +`X · (∑ eᵢ Xⁱ) = ∑ eᵢ X^(i+1)`, whose top term `e_{d-1} X^d` wraps via `X^d = -∑ lowerₘ Xᵐ`. +So coefficient `m` of the reduced result is `e_{m-1} - e_{d-1} · lowerₘ`, with `e_{-1} = 0`. +This is the single linear map whose iterates build the reduction table `red`. +-/ +def shiftReduce (e : Ext P) : Ext P := + ofFn fun m => + (if (m : ℕ) = 0 then 0 else coeffNat e ((m : ℕ) - 1)) + - coeffNat e (P.d - 1) * P.lowerCoeff m + +/-- The reduced form of `X^k` modulo `f`, obtained by iterating `shiftReduce` (multiply by `X`, +reduce) `k` times from `1 = X^0`. Its image under `toQuot` is `rt ^ k`. -/ +def monomialMod (k : ℕ) : Ext P := (shiftReduce)^[k] 1 + +/-- +The monic-reduction product formula, representing multiplication in `F[X] / f` +when the coefficient ring is commutative. + +Each product monomial `Xⁱ⁺ʲ` is reduced modulo `f` by `monomialMod (i + j)`, so coefficient `m` +of the product collects `xᵢ · yⱼ · [X^(i+j) mod f]ₘ` over all pairs `(i, j)`. +-/ +@[inline, specialize] +def mul (x y : Ext P) : Ext P := + ofFn fun m => + ∑ i : Fin P.d, ∑ j : Fin P.d, + coeff x i * coeff y j * coeff (monomialMod ((i : ℕ) + (j : ℕ))) m + +/-- +The reduction table: `red P` holds `X^k mod f` for every `k ≤ 2d - 2`, i.e. every exponent a +product of two reduced elements can reach. + +This is the table the `shiftReduce` docstring above refers to. It exists purely for speed: `mul` +is the specification, and `mulTbl` below is the compiled implementation that consults this table. +-/ +def red (P : ExtensionParams F) : Vector (Ext P) (2 * P.d - 1) := + Vector.ofFn fun k => monomialMod (k : ℕ) + +@[simp] theorem red_getElem {k : ℕ} (hk : k < 2 * P.d - 1) : + (red P)[k] = monomialMod k := by + simp only [red, Vector.getElem_ofFn] + +/-- +Table-driven multiplication: the compiled implementation of `mul`. + +Mathematically identical to `mul`, but the reduced monomials `X^(i+j) mod f` are computed once +into `red` instead of being re-derived by `monomialMod` for every output coefficient. That drops +the cost from roughly `O(d^5)` to `O(d^3)`: `mul` evaluates `shiftReduce^[i+j]` once per +`(m, i, j)` triple, so the same `d`-fold iteration is repeated `d^3` times. + +`mul` remains the definition everything is proved about; `mul_eq_mulTbl` below swaps this in for +compilation via `@[csimp]`. +-/ +@[inline, specialize] +def mulTbl (x y : Ext P) : Ext P := + let tbl := red P + ofFn fun m => + ∑ i : Fin P.d, ∑ j : Fin P.d, + coeff x i * coeff y j * + coeff (tbl[(i : ℕ) + (j : ℕ)]'(by + have hi := i.isLt; have hj := j.isLt; have hd := P.two_le; omega)) m + +@[csimp] theorem mul_eq_mulTbl : @mul = @mulTbl := by + funext F _ P x y + refine Ext.ext fun m => ?_ + simp only [mul, mulTbl, coeff_ofFn] + refine Finset.sum_congr rfl fun i _ => Finset.sum_congr rfl fun j _ => ?_ + rw [red_getElem] + +instance : Mul (Ext P) := ⟨mul⟩ + +/-- `Nat`-power by binary exponentiation, so `x ^ n` costs `O(log n)` multiplications. -/ +instance : Pow (Ext P) ℕ := ⟨fun x n => npowBinRec n x⟩ + +instance : NatCast (Ext P) := ⟨fun n => ofFn fun i => if (i : ℕ) = 0 then (n : F) else 0⟩ +instance : IntCast (Ext P) := ⟨fun n => ofFn fun i => if (i : ℕ) = 0 then (n : F) else 0⟩ + +instance [DecidableEq F] : DecidableEq (Ext P) := fun x y => + decidable_of_iff (x.coeffs = y.coeffs) coeffs_injective.eq_iff + +instance [BEq F] : BEq (Ext P) := ⟨fun x y => x.coeffs == y.coeffs⟩ + +instance [BEq F] [LawfulBEq F] : LawfulBEq (Ext P) where + eq_of_beq {x y} h := by + exact coeffs_injective (eq_of_beq h) + rfl {x} := by + change (x.coeffs == x.coeffs) = true + exact BEq.rfl + +instance [Repr F] : Repr (Ext P) := ⟨fun x prec => reprPrec x.coeffs prec⟩ +instance : Inhabited (Ext P) := ⟨0⟩ + +/-! ### Coefficients of the operations -/ + +@[simp] theorem coeff_zero (i : Fin P.d) : coeff (0 : Ext P) i = 0 := coeff_ofFn _ _ +@[simp] theorem coeff_one (i : Fin P.d) : + coeff (1 : Ext P) i = if (i : ℕ) = 0 then 1 else 0 := coeff_ofFn _ _ +@[simp] theorem coeff_add (x y : Ext P) (i : Fin P.d) : + coeff (x + y) i = coeff x i + coeff y i := coeff_ofFn _ _ +@[simp] theorem coeff_neg (x : Ext P) (i : Fin P.d) : coeff (-x) i = -coeff x i := coeff_ofFn _ _ +@[simp] theorem coeff_sub (x y : Ext P) (i : Fin P.d) : + coeff (x - y) i = coeff x i - coeff y i := coeff_ofFn _ _ +@[simp] theorem coeff_smul (c : F) (x : Ext P) (i : Fin P.d) : + coeff (c • x) i = c * coeff x i := coeff_ofFn _ _ + +@[simp] theorem coeff_shiftReduce (e : Ext P) (m : Fin P.d) : + coeff (shiftReduce e) m = + (if (m : ℕ) = 0 then 0 else coeffNat e ((m : ℕ) - 1)) + - coeffNat e (P.d - 1) * P.lowerCoeff m := coeff_ofFn _ _ + +@[simp] theorem coeff_mul (x y : Ext P) (m : Fin P.d) : + coeff (x * y) m = + ∑ i : Fin P.d, ∑ j : Fin P.d, + coeff x i * coeff y j * coeff (monomialMod ((i : ℕ) + (j : ℕ))) m := + coeff_ofFn _ _ + +@[simp] theorem coeff_ofBase (c : F) (i : Fin P.d) : + coeff (ofBase (P := P) c) i = if (i : ℕ) = 0 then c else 0 := coeff_ofFn _ _ + +@[simp] theorem coeff_gen (i : Fin P.d) : + coeff (gen : Ext P) i = if (i : ℕ) = 1 then 1 else 0 := coeff_ofFn _ _ + +/-- `ofBase` agrees with `1` on the multiplicative unit. -/ +@[simp] theorem ofBase_one : ofBase (P := P) (1 : F) = 1 := rfl + +/-- `ofBase` agrees with `0`. -/ +@[simp] theorem ofBase_zero : ofBase (P := P) (0 : F) = 0 := by + ext i; simp only [coeff_ofBase, coeff_zero, ite_self] + +/-- `ofBase` agrees with the `ℕ`-cast, so scalars and numerals do not diverge. -/ +@[simp] theorem ofBase_natCast (n : ℕ) : ofBase (P := P) (n : F) = (n : Ext P) := rfl + +/-- `ofBase` agrees with the `ℤ`-cast. -/ +@[simp] theorem ofBase_intCast (n : ℤ) : ofBase (P := P) (n : F) = (n : Ext P) := rfl + +@[simp] theorem coeff_natCast (n : ℕ) (i : Fin P.d) : + coeff (n : Ext P) i = if (i : ℕ) = 0 then (n : F) else 0 := coeff_ofFn _ _ + +@[simp] theorem coeff_intCast (n : ℤ) (i : Fin P.d) : + coeff (n : Ext P) i = if (i : ℕ) = 0 then (n : F) else 0 := coeff_ofFn _ _ + +theorem pow_def (x : Ext P) (n : ℕ) : x ^ n = npowBinRec n x := rfl + +/-- The canonical inverse candidate `x ^ (q^d - 2)`. + +Inverse laws require finite-cardinality and irreducibility certificates. For arbitrary `q`, +this operation need not send zero to zero. -/ +def inv (x : Ext P) : Ext P := x ^ (P.q ^ P.d - 2) + +instance instInv : Inv (Ext P) := ⟨inv⟩ +instance instDiv : Div (Ext P) := ⟨fun x y => x * inv y⟩ + +theorem inv_def (x : Ext P) : x⁻¹ = x ^ (P.q ^ P.d - 2) := rfl +theorem div_def (x y : Ext P) : x / y = x * y⁻¹ := rfl + +end Ext + +/-! ### Binomial extensions as a special case + +A binomial extension `F[X] / (X^d - W)` is the case `lower = (-W, 0, …, 0)`. `BinomialParams` +keeps the `W`-only interface; `toExtensionParams` maps it into the general framework. The +polynomial correspondence and binomial irreducibility criterion are proved separately. +-/ + +/-- +Parameters for the quotient `F[X] / (X^d - W)`: the degree, constant `W`, and a proposed +base cardinality. The modulus has lower coefficients `(-W, 0, …, 0)`. +-/ +structure BinomialParams (F : Type*) where + /-- The degree of the extension. -/ + d : ℕ + /-- The extension adjoins a `d`-th root of `W`. -/ + W : F + /-- Degree at least two; a degree-one "extension" is just `F`. -/ + two_le : 2 ≤ d + /-- The proposed base cardinality used by the inverse exponent. -/ + q : ℕ + +namespace BinomialParams + +variable (P : BinomialParams F) + +omit [Ring F] in +theorem d_pos : 0 < P.d := by have := P.two_le; omega + +/-- The general-framework parameters for the binomial modulus `X^d - W`: the lower coefficient +vector is `(-W, 0, …, 0)`. -/ +def toExtensionParams : ExtensionParams F where + d := P.d + two_le := P.two_le + lower := Vector.ofFn fun i => if (i : ℕ) = 0 then -P.W else 0 + q := P.q + +@[simp] theorem toExtensionParams_d : P.toExtensionParams.d = P.d := rfl +@[simp] theorem toExtensionParams_q : P.toExtensionParams.q = P.q := rfl + +@[simp] theorem toExtensionParams_lowerCoeff (i : Fin P.toExtensionParams.d) : + P.toExtensionParams.lowerCoeff i = if (i : ℕ) = 0 then -P.W else 0 := by + simp only [ExtensionParams.lowerCoeff, toExtensionParams, Vector.getElem_ofFn] + +end BinomialParams + +end CompPoly.Extension diff --git a/CompPoly/Fields/Extension/Bridge.lean b/CompPoly/Fields/Extension/Bridge.lean index a21c0e71..ce7bb07e 100644 --- a/CompPoly/Fields/Extension/Bridge.lean +++ b/CompPoly/Fields/Extension/Bridge.lean @@ -45,7 +45,7 @@ namespace CompPoly.Extension.Ext open Polynomial AdjoinRoot -variable {F : Type*} [Field F] [Fintype F] {P : ExtensionParams F} +variable {F : Type*} [Field F] {P : ExtensionParams F} /-- The specification of the extension: the quotient ring `F[X] / f`. -/ scoped notation "Quot[" P "]" => AdjoinRoot (ExtensionParams.poly P) diff --git a/CompPoly/Fields/Extension/Cardinality.lean b/CompPoly/Fields/Extension/Cardinality.lean new file mode 100644 index 00000000..b269ef9e --- /dev/null +++ b/CompPoly/Fields/Extension/Cardinality.lean @@ -0,0 +1,64 @@ +/- +Copyright (c) 2026 CompPoly Contributors. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Derek Sorensen +-/ +module + +public import CompPoly.Fields.Extension.Arithmetic +public import Mathlib.SetTheory.Cardinal.Finite + +/-! +# Finiteness and cardinality of monic quotient presentations + +The coordinate equivalence gives `Finite (Ext P)` from `Finite F`, and gives an optional +`Fintype (Ext P)` when an enumeration of the base is available. A separate certificate +`Fact (Nat.card F = P.q)` identifies the stored exponent parameter with the base cardinality. +The resulting extension cardinality is `P.q ^ P.d`. + +These facts require neither a polynomial quotient bridge nor irreducibility. All finiteness +and cardinality assumptions used by field laws are propositions, so executable arithmetic +never receives an enumeration dictionary merely to use those laws. +-/ + +@[expose] public section + +namespace CompPoly.Extension + +/-- The certified cardinality agrees with any enumeration of the base type. -/ +theorem ExtensionParams.card_eq {F : Type*} [Fintype F] (P : ExtensionParams F) + [Fact (Nat.card F = P.q)] : Fintype.card F = P.q := by + rw [← Nat.card_eq_fintype_card] + exact Fact.out + +/-- The certified binomial base cardinality agrees with any enumeration. -/ +theorem BinomialParams.card_eq {F : Type*} [Fintype F] (P : BinomialParams F) + [Fact (Nat.card F = P.q)] : Fintype.card F = P.q := by + rw [← Nat.card_eq_fintype_card] + exact Fact.out + +instance {F : Type*} [Ring F] (P : BinomialParams F) [Fact (Nat.card F = P.q)] : + Fact (Nat.card F = P.toExtensionParams.q) := ⟨(show Nat.card F = P.q from Fact.out)⟩ + +end CompPoly.Extension + +namespace CompPoly.Extension.Ext + +variable {F : Type*} {P : ExtensionParams F} + +instance instFinite [Finite F] : Finite (Ext P) := + Finite.of_equiv (Fin P.d → F) (equivFn P).symm + +instance instFintype [Fintype F] : Fintype (Ext P) := Fintype.ofEquiv _ (equivFn P).symm + +/-- The enumerated extension cardinality is the certified base cardinality to the degree. -/ +theorem card_ext [Fintype F] [Fact (Nat.card F = P.q)] : Fintype.card (Ext P) = P.q ^ P.d := by + rw [Fintype.card_congr (equivFn P), Fintype.card_fun, P.card_eq, Fintype.card_fin] + +/-- The extension has `q^d` elements, independently of any chosen enumeration. -/ +theorem nat_card_ext [Finite F] [Fact (Nat.card F = P.q)] : + Nat.card (Ext P) = P.q ^ P.d := by + let := Fintype.ofFinite F + rw [Nat.card_eq_fintype_card, card_ext] + +end CompPoly.Extension.Ext diff --git a/CompPoly/Fields/Extension/Defs.lean b/CompPoly/Fields/Extension/Defs.lean index 8cfff03f..616b7b5c 100644 --- a/CompPoly/Fields/Extension/Defs.lean +++ b/CompPoly/Fields/Extension/Defs.lean @@ -5,49 +5,19 @@ Authors: Derek Sorensen -/ module -public import CompPoly.Fields.Extension.Binomial -public import Mathlib.Algebra.BigOperators.Fin +public import CompPoly.Fields.Extension.Arithmetic +public import Mathlib.Algebra.Polynomial.BigOperators /-! -# Computable extension fields by an arbitrary monic modulus +# Polynomial specifications for monic quotient presentations -A degree-`d` extension of `F` is `F[X] / f` for a monic `f` of degree `d`. Elements are -represented as dense coefficient vectors of length exactly `d`, so arithmetic is straight-line: -no trimming and no size branching. Multiplication expands each product monomial `Xⁱ⁺ʲ` through -`monomialMod (i + j)`, the reduced form of `Xⁱ⁺ʲ` modulo `f`, obtained by iterating a single -"multiply by `X`, reduce mod `f`" linear map, `shiftReduce`. +`ExtensionParams.poly` interprets the lower coefficient vector as the monic polynomial +`X^d + ∑ i, C lower[i] * X^i`. The binomial specification is `X^d - W`; its conversion to +an arbitrary monic presentation preserves that polynomial. -The parameters are bundled into `ExtensionParams` and carried as a *type index* (`Ext P`), so two -different extensions of the same base field are different types and cannot have their instances -confused. - -The special case `f = X^d - W` (a binomial extension) is recovered by -`BinomialParams.toExtensionParams`, whose `lower` vector is `(-W, 0, …, 0)`; see -`Extension/Binomial.lean` -for the irreducibility criterion that discharges `Fact (Irreducible P.poly)` in that case. - -This file supplies only the operations and the elementary `coeff` lemmas — no algebraic -structure. `CompPoly/Fields/Extension/Bridge.lean` relates them to `AdjoinRoot P.poly` and -establishes `CommRing`; `CompPoly/Fields/Extension/Field.lean` adds inversion and `Field`. - -## Main definitions - -* `ExtensionParams`: the degree `d`, the lower coefficients of the monic modulus, and the - base-field cardinality `q`. -* `Ext P`: the carrier, `Vector F P.d`. -* `Ext.shiftReduce`: multiply by `X` and reduce mod `f`; iterated to build `Ext.monomialMod`. -* `Ext.monomialMod k`: the reduced form of `X^k` modulo `f`. -* `Ext.mul`: multiplication, expanding product monomials through `monomialMod`. -* `BinomialParams.toExtensionParams`: the binomial special case `f = X^d - W`. - -## Implementation notes - -There is no degree-bound invariant to maintain: `Ext P` has length exactly `P.d`, so the bound -is *structural* rather than a proposition carried alongside the data. This subtree is therefore -independent of the `CPolynomial` stack — it imports none of `CompPoly/Univariate/`. Where a -degree bound is needed on the *polynomial* side (to show the representative chosen by -`Ext.toQuot` is the canonical one), it is proved directly in -`CompPoly/Fields/Extension/Bridge.lean` as `degree_repr_lt`. +The carrier and executable operations are defined in `Extension/Arithmetic.lean`. These +specifications do not certify the cardinality parameter or irreducibility. The quotient bridge +and field laws are supplied separately in `Extension/Bridge.lean` and `Extension/Field.lean`. -/ @[expose] public section @@ -58,51 +28,9 @@ open Polynomial variable {F : Type*} [Field F] -/-- -The data defining an extension `F[X] / f` by a monic modulus `f` of degree `d`. - -The modulus is stored by its `d` lower coefficients: `f = X^d + ∑_{i < d} lower[i] · X^i`. The -leading coefficient is an implicit `1`, so `f` is monic by construction. - -Irreducibility is deliberately *not* a field here: the commutative-ring structure on `Ext P` -does not need it, and requiring it would force every consumer of the ring operations to carry -the proof. `Ext.instField` takes `[Fact (Irreducible P.poly)]` separately, mirroring -`AdjoinRoot`. --/ -structure ExtensionParams (F : Type*) [Field F] [Fintype F] where - /-- The degree of the extension. -/ - d : ℕ - /-- Degree at least two; a degree-one "extension" is just `F`. -/ - two_le : 2 ≤ d - /-- The lower coefficients of the monic modulus, little-endian: `lower[i]` is the coefficient - of `X^i` in `poly`, for `i < d`. The coefficient of `X^d` is an implicit `1`. -/ - lower : Vector F d - /-- The cardinality of the base field, as a numeral. - - This is carried as *data* rather than read off `Fintype.card F` because inversion is - Fermat-based and must evaluate the exponent at runtime: for `F = ZMod p` with `p` around - `2^31`, `Fintype.card F` would enumerate all of `Fin p`. Supply `card_eq` as `ZMod.card _`. -/ - q : ℕ - /-- `q` really is the cardinality of the base field. -/ - card_eq : Fintype.card F = q - namespace ExtensionParams -variable [Fintype F] (P : ExtensionParams F) - -/-- The coefficient of `X^i` in the lower part of the modulus. -/ -@[inline] def lowerCoeff (i : Fin P.d) : F := P.lower[i.val] - -/-- `lowerCoeff` extended by zero outside the valid range, for reindexing sums in -`CompPoly/Fields/Extension/Bridge.lean`. -/ -def lowerCoeffNat (k : ℕ) : F := if h : k < P.d then P.lower[k] else 0 - -@[simp] theorem lowerCoeffNat_coe (i : Fin P.d) : P.lowerCoeffNat (i : ℕ) = P.lowerCoeff i := by - rw [lowerCoeffNat, dif_pos i.isLt]; rfl - -theorem lowerCoeffNat_of_ge {k : ℕ} (h : P.d ≤ k) : P.lowerCoeffNat k = 0 := dif_neg (by omega) - -theorem d_pos : 0 < P.d := by have := P.two_le; omega +variable (P : ExtensionParams F) /-- The monic defining polynomial `X^d + ∑_{i < d} lower[i] · X^i`. Part of the specification only; the computable arithmetic on `Ext P` never evaluates it. -/ @@ -129,245 +57,9 @@ theorem monic_poly : P.poly.Monic := by end ExtensionParams -/-- -The carrier of the extension `F[X] / f`: a dense coefficient vector of length `P.d`, -little-endian (index `i` is the coefficient of `X^i`). --/ -def Ext {F : Type*} [Field F] [Fintype F] (P : ExtensionParams F) : Type _ := Vector F P.d - -namespace Ext - -variable [Fintype F] {P : ExtensionParams F} - -/-- View an element as its coefficient vector. This is the identity. -/ -@[inline] def coeffs (x : Ext P) : Vector F P.d := x - -/-- Build an element from a coefficient vector. This is the identity. -/ -@[inline] def ofVector (v : Vector F P.d) : Ext P := v - -/-- Build an element from a coefficient function. -/ -@[inline] def ofFn (g : Fin P.d → F) : Ext P := ofVector (Vector.ofFn g) - -/-- The coefficient of `X^i`. -/ -@[inline] def coeff (x : Ext P) (i : Fin P.d) : F := (coeffs x)[i.val] - -@[simp] theorem coeff_ofFn (g : Fin P.d → F) (i : Fin P.d) : coeff (ofFn g) i = g i := by - simp [coeff, ofFn, ofVector, coeffs] - -/-- Two elements with the same coefficients are equal. -/ -@[ext] theorem ext {x y : Ext P} (h : ∀ i, coeff x i = coeff y i) : x = y := - Vector.ext fun i hi => h ⟨i, hi⟩ - -theorem ofFn_coeff (x : Ext P) : ofFn (coeff x) = x := by ext i; simp - -/-- `coeff` extended by zero outside the valid range. Handy for reindexing sums in -`CompPoly/Fields/Extension/Bridge.lean` without carrying `Fin` bound proofs. -/ -def coeffNat (x : Ext P) (i : ℕ) : F := if h : i < P.d then coeff x ⟨i, h⟩ else 0 - -@[simp] theorem coeffNat_coe (x : Ext P) (i : Fin P.d) : coeffNat x (i : ℕ) = coeff x i := by - rw [coeffNat, dif_pos i.isLt] - -theorem coeffNat_of_lt (x : Ext P) {i : ℕ} (h : i < P.d) : coeffNat x i = coeff x ⟨i, h⟩ := - dif_pos h - -theorem coeffNat_of_ge (x : Ext P) {i : ℕ} (h : P.d ≤ i) : coeffNat x i = 0 := - dif_neg (by omega) - -/-! ### Distinguished elements - -`ofBase` embeds the base field as constant coefficients and `gen` is the class of `X`, i.e. the -adjoined root of `f`. They are the two elements a consumer of the extension needs by name; -`CompPoly/Fields/Extension/Bridge.lean` promotes `ofBase` to an `Algebra` structure and proves -`gen ^ d = xPowD` (the reduced form of `X^d`). --/ - -/-- The base field embedded into the extension, as the constant coefficient. -/ -@[inline] def ofBase (c : F) : Ext P := ofFn fun i => if (i : ℕ) = 0 then c else 0 - -/-- The adjoined root: the class of `X`. -/ -def gen : Ext P := ofFn fun i => if (i : ℕ) = 1 then 1 else 0 - -/-! ### Operations - -Multiplication is defined in terms of `shiftReduce` — the "multiply by `X`, reduce mod `f`" -map — whose iterates `monomialMod k = shiftReduce^[k] 1` are the reduced monomials `X^k mod f`. -Everything downstream is proved from the single homomorphism law -`toQuot (shiftReduce e) = rt * toQuot e` in `CompPoly/Fields/Extension/Bridge.lean`. --/ - -instance : Zero (Ext P) := ⟨ofFn fun _ => 0⟩ -instance : One (Ext P) := ⟨ofFn fun i => if (i : ℕ) = 0 then 1 else 0⟩ -instance : Add (Ext P) := ⟨fun x y => ofFn fun i => coeff x i + coeff y i⟩ -instance : Neg (Ext P) := ⟨fun x => ofFn fun i => -coeff x i⟩ -instance : Sub (Ext P) := ⟨fun x y => ofFn fun i => coeff x i - coeff y i⟩ -instance : SMul F (Ext P) := ⟨fun c x => ofFn fun i => c * coeff x i⟩ - -/-- -Multiply by `X` and reduce modulo `f`. - -`X · (∑ eᵢ Xⁱ) = ∑ eᵢ X^(i+1)`, whose top term `e_{d-1} X^d` wraps via `X^d = -∑ lowerₘ Xᵐ`. -So coefficient `m` of the reduced result is `e_{m-1} - e_{d-1} · lowerₘ`, with `e_{-1} = 0`. -This is the single linear map whose iterates build the reduction table `red`. --/ -def shiftReduce (e : Ext P) : Ext P := - ofFn fun m => - (if (m : ℕ) = 0 then 0 else coeffNat e ((m : ℕ) - 1)) - - coeffNat e (P.d - 1) * P.lowerCoeff m - -/-- The reduced form of `X^k` modulo `f`, obtained by iterating `shiftReduce` (multiply by `X`, -reduce) `k` times from `1 = X^0`. Its image under `toQuot` is `rt ^ k`. -/ -def monomialMod (k : ℕ) : Ext P := (shiftReduce)^[k] 1 - -/-- -Multiplication in `F[X] / f`. - -Each product monomial `Xⁱ⁺ʲ` is reduced modulo `f` by `monomialMod (i + j)`, so coefficient `m` -of the product collects `xᵢ · yⱼ · [X^(i+j) mod f]ₘ` over all pairs `(i, j)`. --/ -@[inline, specialize] -def mul (x y : Ext P) : Ext P := - ofFn fun m => - ∑ i : Fin P.d, ∑ j : Fin P.d, - coeff x i * coeff y j * coeff (monomialMod ((i : ℕ) + (j : ℕ))) m - -/-- -The reduction table: `red P` holds `X^k mod f` for every `k ≤ 2d - 2`, i.e. every exponent a -product of two reduced elements can reach. - -This is the table the `shiftReduce` docstring above refers to. It exists purely for speed: `mul` -is the specification, and `mulTbl` below is the compiled implementation that consults this table. --/ -def red (P : ExtensionParams F) : Vector (Ext P) (2 * P.d - 1) := - Vector.ofFn fun k => monomialMod (k : ℕ) - -@[simp] theorem red_getElem {k : ℕ} (hk : k < 2 * P.d - 1) : - (red P)[k] = monomialMod k := by - simp only [red, Vector.getElem_ofFn] - -/-- -Table-driven multiplication: the compiled implementation of `mul`. - -Mathematically identical to `mul`, but the reduced monomials `X^(i+j) mod f` are computed once -into `red` instead of being re-derived by `monomialMod` for every output coefficient. That drops -the cost from roughly `O(d^5)` to `O(d^3)`: `mul` evaluates `shiftReduce^[i+j]` once per -`(m, i, j)` triple, so the same `d`-fold iteration is repeated `d^3` times. - -`mul` remains the definition everything is proved about; `mul_eq_mulTbl` below swaps this in for -compilation via `@[csimp]`. --/ -@[inline, specialize] -def mulTbl (x y : Ext P) : Ext P := - let tbl := red P - ofFn fun m => - ∑ i : Fin P.d, ∑ j : Fin P.d, - coeff x i * coeff y j * - coeff (tbl[(i : ℕ) + (j : ℕ)]'(by - have hi := i.isLt; have hj := j.isLt; have hd := P.two_le; omega)) m - -@[csimp] theorem mul_eq_mulTbl : @mul = @mulTbl := by - funext F _ _ P x y - refine Ext.ext fun m => ?_ - simp only [mul, mulTbl, coeff_ofFn] - refine Finset.sum_congr rfl fun i _ => Finset.sum_congr rfl fun j _ => ?_ - rw [red_getElem] - -instance : Mul (Ext P) := ⟨mul⟩ - -/-- `Nat`-power by binary exponentiation, so `x ^ n` costs `O(log n)` multiplications. -/ -instance : Pow (Ext P) ℕ := ⟨fun x n => npowBinRec n x⟩ - -instance : NatCast (Ext P) := ⟨fun n => ofFn fun i => if (i : ℕ) = 0 then (n : F) else 0⟩ -instance : IntCast (Ext P) := ⟨fun n => ofFn fun i => if (i : ℕ) = 0 then (n : F) else 0⟩ - -instance [DecidableEq F] : DecidableEq (Ext P) := - inferInstanceAs (DecidableEq (Vector F P.d)) -instance [BEq F] : BEq (Ext P) := inferInstanceAs (BEq (Vector F P.d)) -instance [Repr F] : Repr (Ext P) := inferInstanceAs (Repr (Vector F P.d)) -instance : Inhabited (Ext P) := ⟨0⟩ - -/-! ### Coefficients of the operations -/ - -@[simp] theorem coeff_zero (i : Fin P.d) : coeff (0 : Ext P) i = 0 := coeff_ofFn _ _ -@[simp] theorem coeff_one (i : Fin P.d) : - coeff (1 : Ext P) i = if (i : ℕ) = 0 then 1 else 0 := coeff_ofFn _ _ -@[simp] theorem coeff_add (x y : Ext P) (i : Fin P.d) : - coeff (x + y) i = coeff x i + coeff y i := coeff_ofFn _ _ -@[simp] theorem coeff_neg (x : Ext P) (i : Fin P.d) : coeff (-x) i = -coeff x i := coeff_ofFn _ _ -@[simp] theorem coeff_sub (x y : Ext P) (i : Fin P.d) : - coeff (x - y) i = coeff x i - coeff y i := coeff_ofFn _ _ -@[simp] theorem coeff_smul (c : F) (x : Ext P) (i : Fin P.d) : - coeff (c • x) i = c * coeff x i := coeff_ofFn _ _ - -@[simp] theorem coeff_shiftReduce (e : Ext P) (m : Fin P.d) : - coeff (shiftReduce e) m = - (if (m : ℕ) = 0 then 0 else coeffNat e ((m : ℕ) - 1)) - - coeffNat e (P.d - 1) * P.lowerCoeff m := coeff_ofFn _ _ - -@[simp] theorem coeff_mul (x y : Ext P) (m : Fin P.d) : - coeff (x * y) m = - ∑ i : Fin P.d, ∑ j : Fin P.d, - coeff x i * coeff y j * coeff (monomialMod ((i : ℕ) + (j : ℕ))) m := - coeff_ofFn _ _ - -@[simp] theorem coeff_ofBase (c : F) (i : Fin P.d) : - coeff (ofBase (P := P) c) i = if (i : ℕ) = 0 then c else 0 := coeff_ofFn _ _ - -@[simp] theorem coeff_gen (i : Fin P.d) : - coeff (gen : Ext P) i = if (i : ℕ) = 1 then 1 else 0 := coeff_ofFn _ _ - -/-- `ofBase` agrees with `1` on the multiplicative unit. -/ -@[simp] theorem ofBase_one : ofBase (P := P) (1 : F) = 1 := rfl - -/-- `ofBase` agrees with `0`. -/ -@[simp] theorem ofBase_zero : ofBase (P := P) (0 : F) = 0 := by - ext i; simp only [coeff_ofBase, coeff_zero, ite_self] - -/-- `ofBase` agrees with the `ℕ`-cast, so scalars and numerals do not diverge. -/ -@[simp] theorem ofBase_natCast (n : ℕ) : ofBase (P := P) (n : F) = (n : Ext P) := rfl - -/-- `ofBase` agrees with the `ℤ`-cast. -/ -@[simp] theorem ofBase_intCast (n : ℤ) : ofBase (P := P) (n : F) = (n : Ext P) := rfl - -@[simp] theorem coeff_natCast (n : ℕ) (i : Fin P.d) : - coeff (n : Ext P) i = if (i : ℕ) = 0 then (n : F) else 0 := coeff_ofFn _ _ - -@[simp] theorem coeff_intCast (n : ℤ) (i : Fin P.d) : - coeff (n : Ext P) i = if (i : ℕ) = 0 then (n : F) else 0 := coeff_ofFn _ _ - -theorem pow_def (x : Ext P) (n : ℕ) : x ^ n = npowBinRec n x := rfl - -end Ext - -/-! ### Binomial extensions as a special case - -A binomial extension `F[X] / (X^d - W)` is the case `lower = (-W, 0, …, 0)`. `BinomialParams` -keeps the ergonomic `W`-only interface (and its dedicated irreducibility criterion in -`Extension/Binomial.lean`); `toExtensionParams` maps it into the general framework, and -`toExtensionParams_poly` -identifies the two spellings of the defining polynomial. --/ - -/-- -The data defining a binomial extension `F[X] / (X^d - W)`. A thin front-end for the special -case `ExtensionParams` with `lower = (-W, 0, …, 0)`; see `BinomialParams.toExtensionParams`. --/ -structure BinomialParams (F : Type*) [Field F] [Fintype F] where - /-- The degree of the extension. -/ - d : ℕ - /-- The extension adjoins a `d`-th root of `W`. -/ - W : F - /-- Degree at least two; a degree-one "extension" is just `F`. -/ - two_le : 2 ≤ d - /-- The cardinality of the base field, as a numeral. Supply as `ZMod.card _`. -/ - q : ℕ - /-- `q` really is the cardinality of the base field. -/ - card_eq : Fintype.card F = q - namespace BinomialParams -variable [Fintype F] (P : BinomialParams F) - -theorem d_pos : 0 < P.d := by have := P.two_le; omega +variable (P : BinomialParams F) /-- The defining polynomial `X^d - W`. Part of the specification only. -/ noncomputable def poly : F[X] := X ^ P.d - C P.W @@ -376,22 +68,6 @@ noncomputable def poly : F[X] := X ^ P.d - C P.W theorem monic_poly : P.poly.Monic := monic_X_pow_sub_C _ (by have := P.two_le; omega) -/-- The general-framework parameters for the binomial modulus `X^d - W`: the lower coefficient -vector is `(-W, 0, …, 0)`. -/ -def toExtensionParams : ExtensionParams F where - d := P.d - two_le := P.two_le - lower := Vector.ofFn fun i => if (i : ℕ) = 0 then -P.W else 0 - q := P.q - card_eq := P.card_eq - -@[simp] theorem toExtensionParams_d : P.toExtensionParams.d = P.d := rfl -@[simp] theorem toExtensionParams_q : P.toExtensionParams.q = P.q := rfl - -@[simp] theorem toExtensionParams_lowerCoeff (i : Fin P.toExtensionParams.d) : - P.toExtensionParams.lowerCoeff i = if (i : ℕ) = 0 then -P.W else 0 := by - simp only [ExtensionParams.lowerCoeff, toExtensionParams, Vector.getElem_ofFn] - /-- The general-framework polynomial of a binomial agrees with `X^d - W`. -/ theorem toExtensionParams_poly : P.toExtensionParams.poly = P.poly := by have hsum : (∑ i : Fin P.toExtensionParams.d, diff --git a/CompPoly/Fields/Extension/Field.lean b/CompPoly/Fields/Extension/Field.lean index 0fd7c693..4c7c683a 100644 --- a/CompPoly/Fields/Extension/Field.lean +++ b/CompPoly/Fields/Extension/Field.lean @@ -6,6 +6,7 @@ Authors: Derek Sorensen module public import CompPoly.Fields.Extension.Bridge +public import CompPoly.Fields.Extension.Cardinality public import Mathlib.FieldTheory.Finite.Basic /-! @@ -13,8 +14,9 @@ public import Mathlib.FieldTheory.Finite.Basic `toQuot` is not just injective but bijective: every class in `F[X] / f` has a unique representative of degree `< d`. That gives `Ext.ringEquivQuot : Ext P ≃+* AdjoinRoot P.poly`, -hence `Fintype.card (Ext P) = q ^ d`, and — when the defining polynomial is irreducible — a -`Field` structure. +and the separately certified base cardinality gives `Nat.card (Ext P) = q ^ d`. A `Field` +structure requires `[Finite F]`, `[Fact (Nat.card F = P.q)]`, and irreducibility of the defining +polynomial. Inversion is by Fermat's little theorem, `x⁻¹ = x ^ (q^d - 2)`, matching how `CompPoly/Fields/Montgomery/Native32Field.lean` inverts in the base field. Exponentiation is @@ -28,16 +30,16 @@ structure `noncomputable`, which then shadows the computable `Mul` and `Pow`. Th ## Main definitions and statements -* `Ext.equivFn`: `Ext P ≃ (Fin P.d → F)`, giving `Fintype (Ext P)` and its cardinality. * `Ext.toQuot_surjective`, `Ext.ringEquivQuot`: `Ext P ≃+* AdjoinRoot P.poly`. * `Ext.inv`: Fermat inversion. -* `Ext.instField`: the `Field` structure, given `[Fact (Irreducible P.poly)]`. +* `Ext.instField`: the `Field` structure, given finite base field, certified `Nat.card F = P.q`, + and irreducibility of `P.poly`. ## Implementation notes -`Ext.inv` costs `O(d · log q)` extension multiplications. A norm-based inverse using the -Frobenius map — which on a binomial basis is a coordinate-wise scaling when `d ∣ q - 1` — would -be roughly an order of magnitude faster. See `ROADMAP.md`. +The inverse candidate is defined in `Extension/Arithmetic.lean`; its correctness depends on +the certificates above. `Extension/Cardinality.lean` supplies the finite-coordinate facts +without importing the polynomial quotient bridge. -/ @[expose] public section @@ -46,21 +48,7 @@ namespace CompPoly.Extension.Ext open Polynomial AdjoinRoot -variable {F : Type*} [Field F] [Fintype F] {P : ExtensionParams F} - -/-! ### Cardinality -/ - -/-- Coefficient vectors are exactly functions out of `Fin d`. -/ -def equivFn (P : ExtensionParams F) : Ext P ≃ (Fin P.d → F) where - toFun := coeff - invFun := ofFn - left_inv := ofFn_coeff - right_inv g := funext fun i => coeff_ofFn g i - -instance instFintype : Fintype (Ext P) := Fintype.ofEquiv _ (equivFn P).symm - -theorem card_ext : Fintype.card (Ext P) = P.q ^ P.d := by - rw [Fintype.card_congr (equivFn P), Fintype.card_fun, P.card_eq, Fintype.card_fin] +variable {F : Type*} [Field F] {P : ExtensionParams F} instance instNontrivial : Nontrivial (Ext P) := ⟨⟨0, 1, fun h => by @@ -69,7 +57,8 @@ instance instNontrivial : Nontrivial (Ext P) := exact zero_ne_one hc⟩⟩ /-- `4 ≤ q ^ d`, since `2 ≤ q` and `2 ≤ d`. Used to justify the Fermat exponent `q ^ d - 2`. -/ -theorem four_le_card_pow : 4 ≤ P.q ^ P.d := by +theorem four_le_card_pow [Finite F] [Fact (Nat.card F = P.q)] : 4 ≤ P.q ^ P.d := by + let := Fintype.ofFinite F have hq : 2 ≤ P.q := by rw [← P.card_eq]; exact Fintype.one_lt_card calc (4 : ℕ) = 2 ^ 2 := by norm_num _ ≤ 2 ^ P.d := Nat.pow_le_pow_right (by omega) P.two_le @@ -107,32 +96,20 @@ noncomputable def ringEquivQuot (P : ExtensionParams F) : Ext P ≃+* Quot[P] := @[simp] theorem ringEquivQuot_apply (x : Ext P) : ringEquivQuot P x = toQuot x := rfl /-- The quotient is finite, transported along the ring equivalence. -/ -noncomputable instance instFintypeQuot : Fintype Quot[P] := +noncomputable instance instFintypeQuot [Fintype F] : Fintype Quot[P] := Fintype.ofEquiv _ (ringEquivQuot P).toEquiv -theorem card_quot : Fintype.card Quot[P] = P.q ^ P.d := by +theorem card_quot [Fintype F] [Fact (Nat.card F = P.q)] : Fintype.card Quot[P] = P.q ^ P.d := by rw [← Fintype.card_congr (ringEquivQuot P).toEquiv, card_ext] /-! ### Inversion -Inversion needs no irreducibility hypothesis to *define* — it is just exponentiation — only to -be correct, so it and the `Inv`/`Div` instances live outside the section below. `Div` in -particular must exist before the `Field` instance is assembled, because `NNRat.castRec` uses it. +The canonical inverse candidate and its `Inv`/`Div` instances come from the raw arithmetic +module. The following laws require certified cardinality; cancellation also requires +irreducibility. -/ -/-- -Inversion by Fermat's little theorem: `x⁻¹ = x ^ (q^d - 2)`, since the multiplicative group of -the extension has order `q^d - 1`. Zero is sent to zero, as `Field` requires. --/ -def inv (x : Ext P) : Ext P := x ^ (P.q ^ P.d - 2) - -instance instInv : Inv (Ext P) := ⟨inv⟩ -instance instDiv : Div (Ext P) := ⟨fun x y => x * inv y⟩ - -theorem inv_def (x : Ext P) : x⁻¹ = x ^ (P.q ^ P.d - 2) := rfl -theorem div_def (x y : Ext P) : x / y = x * y⁻¹ := rfl - -theorem inv_zero' : (0 : Ext P)⁻¹ = 0 := by +theorem inv_zero' [Finite F] [Fact (Nat.card F = P.q)] : (0 : Ext P)⁻¹ = 0 := by have h4 := four_le_card_pow (P := P) rw [inv_def, zero_pow (by omega)] @@ -140,9 +117,10 @@ theorem inv_zero' : (0 : Ext P)⁻¹ = 0 := by section Irreducible -variable [Fact (Irreducible P.poly)] +variable [Finite F] [Fact (Nat.card F = P.q)] [Fact (Irreducible P.poly)] theorem mul_inv_cancel' {x : Ext P} (hx : x ≠ 0) : x * x⁻¹ = 1 := by + let := Fintype.ofFinite F refine toQuot_injective ?_ rw [toQuot_mul, inv_def, toQuot_pow, toQuot_one, ← pow_succ'] have hz : toQuot x ≠ 0 := fun h => hx (toQuot_injective (by rw [h, toQuot_zero])) diff --git a/CompPoly/Fields/Goldilocks.lean b/CompPoly/Fields/Goldilocks.lean index 6b5e78fd..940e718b 100644 --- a/CompPoly/Fields/Goldilocks.lean +++ b/CompPoly/Fields/Goldilocks.lean @@ -1,29 +1,20 @@ /- Copyright (c) 2024 ArkLib Contributors. All rights reserved. Released under Apache 2.0 license as described in the file LICENSE. -Authors: Quang Dao +Authors: Quang Dao, Varun Thakore -/ + module -public import CompPoly.Fields.PrattCertificate +public import CompPoly.Fields.Goldilocks.Basic +public import CompPoly.Fields.Goldilocks.Fast /-! # Goldilocks prime field `2^{64} - 2^{32} + 1` - This is the field used in Plonky2/3. + Facade module for the Goldilocks field. It re-exports the canonical `ZMod` model + from `CompPoly.Fields.Goldilocks.Basic` and the native-word implementation from + `CompPoly.Fields.Goldilocks.Fast`. -/ @[expose] public section - -namespace Goldilocks - -@[reducible] -def fieldSize : Nat := 2 ^ 64 - 2 ^ 32 + 1 - -abbrev Field := ZMod fieldSize - -theorem is_prime : Nat.Prime fieldSize := by - unfold fieldSize - pratt - -end Goldilocks diff --git a/CompPoly/Fields/Goldilocks/Basic.lean b/CompPoly/Fields/Goldilocks/Basic.lean new file mode 100644 index 00000000..56e06f03 --- /dev/null +++ b/CompPoly/Fields/Goldilocks/Basic.lean @@ -0,0 +1,48 @@ +/- +Copyright (c) 2024 ArkLib Contributors. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Quang Dao, Varun Thakore +-/ + +module + +public import CompPoly.Fields.Basic +public import CompPoly.Fields.PrattCertificate + +/-! + # Goldilocks prime field `2^{64} - 2^{32} + 1` + + This is the field used in Plonky2/3. +-/ + +@[expose] public section + +namespace Goldilocks + +/-- The Goldilocks prime modulus, `2^64 - 2^32 + 1`. -/ +@[reducible] +def fieldSize : Nat := 2 ^ 64 - 2 ^ 32 + 1 + +/-- The canonical mathematical Goldilocks field, implemented as integers modulo +`fieldSize`. -/ +abbrev Field := ZMod fieldSize + +/-- The Goldilocks modulus is prime, verified by a Pratt certificate. -/ +theorem is_prime : Nat.Prime fieldSize := by + unfold fieldSize + pratt + +/-- Register primality of `fieldSize` for Mathlib instances such as `ZMod.instField`. -/ +instance : Fact (Nat.Prime fieldSize) := ⟨is_prime⟩ + +/-- The canonical Goldilocks carrier is a field because its modulus is prime. -/ +instance : _root_.Field Field := ZMod.instField fieldSize + +/-- Goldilocks has characteristic different from two. -/ +instance : NonBinaryField Field where + char_neq_2 := by + -- `decide` can discharge this concrete ZMod equality. + simpa [Field, fieldSize] using + (by decide : (2 : ZMod (2 ^ 64 - 2 ^ 32 + 1)) ≠ 0) + +end Goldilocks diff --git a/CompPoly/Fields/Goldilocks/Fast.lean b/CompPoly/Fields/Goldilocks/Fast.lean new file mode 100644 index 00000000..5a4b13c6 --- /dev/null +++ b/CompPoly/Fields/Goldilocks/Fast.lean @@ -0,0 +1,679 @@ +/- +Copyright (c) 2026 CompPoly Contributors. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Varun Thakore +-/ +module + +public import CompPoly.Fields.Goldilocks.FastReduction +public import Mathlib.Algebra.Field.TransferInstance +public import Mathlib.FieldTheory.Finite.Basic + +/-! +# Fast Goldilocks Field + +Verified `UInt64`-backed implementation of Goldilocks arithmetic, `p = 2^64 - 2^32 + 1`. +The carrier is a subtype of `UInt64` holding canonical representatives, wrapping the raw +kernels from `FastDefs` with the bounds proved in `FastReduction`. Every operation is +identified with its counterpart in the canonical `ZMod` model, and the field instances +are transferred across `toField`. + +Reduction rests on `2^64 ≡ 2^32 - 1 (mod p)`, so a 128-bit product folds back into one +word with shifts, one multiply by `2^32 - 1`, and carry corrections. +-/ + +@[expose] public section + +namespace Goldilocks +namespace Fast + +/-! ## Carrier and arithmetic -/ + + +/-- The fast native-word Goldilocks field carrier, stored as a canonical residue. -/ +abbrev Field : Type := { x : UInt64 // x.toNat < Goldilocks.fieldSize } + +/-- Fast representatives have decidable equality through their `UInt64` value. -/ +instance : DecidableEq Field := inferInstance + +/-- The raw canonical word backing a fast Goldilocks element. -/ +@[inline] +def raw (x : Field) : UInt64 := x.val + +/-- Reading the raw word from a subtype literal returns its stored word. -/ +@[simp] +theorem raw_mk (x : UInt64) (h : x.toNat < Goldilocks.fieldSize) : + raw ⟨x, h⟩ = x := rfl + +/-- `raw` is the underlying `UInt64` value. -/ +@[simp] +theorem raw_eq_val (x : Field) : raw x = x.val := rfl + +/-- Reduce a native `UInt64` modulo Goldilocks. -/ +@[inline] +def reduceUInt64 (x : UInt64) : Field := + ⟨reduceUInt64Raw x, reduceUInt64Raw_lt x⟩ + +/-- One-word reduction preserves the represented canonical field element. -/ +@[simp] +theorem reduceUInt64_cast (x : UInt64) : + ((reduceUInt64 x).val.toNat : Goldilocks.Field) = + (x.toNat : Goldilocks.Field) := by + exact reduceUInt64Raw_cast x + +/-- The zero fast Goldilocks element. -/ +@[inline] +def zero : Field := ⟨0, by decide⟩ + +/-- The one fast Goldilocks element. -/ +@[inline] +def one : Field := ⟨1, by decide⟩ + +/-- Build a fast element from a canonical natural representative. -/ +@[inline] +def ofCanonicalNat (n : Nat) (h : n < Goldilocks.fieldSize) : Field := + ⟨UInt64.ofNat n, by + have hn : n < UInt64.size := Nat.lt_trans h fieldSize_lt_uint64Size + rw [UInt64.toNat_ofNat'] + rw [Nat.mod_eq_of_lt] + · exact h + · simpa [UInt64.size] using hn⟩ + +/-- Convert a natural number into fast canonical representation. -/ +@[inline] +def ofNat (n : Nat) : Field := + ofCanonicalNat (n % Goldilocks.fieldSize) (Nat.mod_lt _ fieldSize_pos) + +/-- Convert a 64-bit word into fast canonical representation. -/ +@[inline] +def ofUInt64 (x : UInt64) : Field := + reduceUInt64 x + +/-- Convert from the canonical `ZMod` Goldilocks field into fast canonical form. -/ +@[inline] +def ofField (x : Goldilocks.Field) : Field := + ofCanonicalNat x.val (ZMod.val_lt x) + +/-- Convert an integer into fast canonical representation. -/ +@[inline] +def ofInt (z : Int) : Field := + ofField (z : Goldilocks.Field) + +/-- Convert a fast Goldilocks element to its canonical natural representative. -/ +@[inline] +def toNat (x : Field) : Nat := + x.val.toNat + +/-- Reading the natural representative of a subtype literal returns its stored word's +natural value. -/ +@[simp] +theorem toNat_mk (x : UInt64) (h : x.toNat < Goldilocks.fieldSize) : + toNat ⟨x, h⟩ = x.toNat := rfl + +/-- `toNat` is the natural value of the underlying `UInt64` word. -/ +@[simp] +theorem toNat_eq_val_toNat (x : Field) : toNat x = x.val.toNat := rfl + +/-- Convert a fast Goldilocks element to the canonical `ZMod` Goldilocks field. -/ +@[inline] +def toField (x : Field) : Goldilocks.Field := + (toNat x : Goldilocks.Field) + +/-- Fast modular addition in canonical form. -/ +@[inline] +def add (x y : Field) : Field := + let lo := x.val + y.val + let carry := decide (lo < x.val) + ⟨reduceAddWithCarryRaw lo carry, + reduceAddWithCarryRaw_lt lo carry + (addWithCarry_bound x.val y.val x.property y.property)⟩ + +/-- Fast modular negation in canonical form. -/ +@[inline] +def neg (x : Field) : Field := + ⟨negRaw x.val, negRaw_lt x.val x.property⟩ + +/-- Fast modular subtraction in canonical form. -/ +@[inline] +def sub (x y : Field) : Field := + ⟨subRaw x.val y.val, subRaw_lt x.val y.val x.property y.property⟩ + +/-- Fast modular multiplication in canonical form. -/ +@[inline] +def mul (x y : Field) : Field := + ⟨reduceMulRaw x.val y.val, reduceMulRaw_lt x.val y.val⟩ + +/-- Fast squaring. -/ +@[inline] +def square (x : Field) : Field := + mul x x + +/-- Repeated squaring: `squareN x n` computes `x^(2^n)`. -/ +@[inline] +def squareN (x : Field) : Nat → Field + | 0 => x + | n + 1 => square (squareN x n) + +/-- Exponentiation over the fast representation using binary exponentiation. -/ +@[inline] +def pow (x : Field) (n : Nat) : Field := + @npowBinRec Field ⟨one⟩ ⟨mul⟩ n x + +/-- Fermat exponent used for inversion in the Goldilocks prime field. -/ +@[inline] +private def invExponent : Nat := Goldilocks.fieldSize - 2 + +/-- Fast modular inversion using an addition chain for `p - 2`. + +For Goldilocks, `p - 2 = 0xFFFFFFFEFFFFFFFF`. The chain builds +`x^(2^31 - 1)`, derives `x^(2^32 - 2)` and `x^(2^32 - 1)`, then combines them as + +`(2^32 - 2) * 2^32 + (2^32 - 1) = p - 2`. +-/ +def inv (x : Field) : Field := + let t2 := mul (square x) x + let t4 := mul (squareN t2 2) t2 + let t8 := mul (squareN t4 4) t4 + let t16 := mul (squareN t8 8) t8 + let t31 := + mul (squareN t16 15) + (mul (squareN t8 7) + (mul (squareN t4 3) + (mul (square t2) x))) + let t32m2 := square t31 + let t32m1 := mul t32m2 x + mul (squareN t32m2 32) t32m1 + +/-- Division through inversion and fast multiplication. -/ +@[inline] +def div (x y : Field) : Field := + mul x (inv y) + +/-- Use fast zero for standard `0` notation. -/ +instance instZeroField : Zero Field where + zero := zero + +/-- Use fast one for standard `1` notation. -/ +instance instOneField : One Field where + one := one + +/-- The raw word backing fast zero is zero. -/ +@[simp] +theorem raw_zero : raw (0 : Field) = 0 := rfl + +/-- The raw word backing fast one is one. -/ +@[simp] +theorem raw_one : raw (1 : Field) = 1 := rfl + +/-- The natural representative of fast zero is zero. -/ +@[simp] +theorem toNat_zero : toNat (0 : Field) = 0 := rfl + +/-- The natural representative of fast one is one. -/ +@[simp] +theorem toNat_one : toNat (1 : Field) = 1 := rfl + +/-- Use fast addition for standard `+` notation. -/ +instance instAddField : Add Field where + add := add + +/-- Use fast negation for standard unary `-` notation. -/ +instance instNegField : Neg Field where + neg := neg + +/-- Use fast subtraction for standard `-` notation. -/ +instance instSubField : Sub Field where + sub := sub + +/-- Use fast multiplication for standard `*` notation. -/ +instance instMulField : Mul Field where + mul := mul + +/-- Use fast inversion for standard inverse notation. -/ +instance instInvField : Inv Field where + inv := inv + +/-- Use fast division for standard `/` notation. -/ +instance instDivField : Div Field where + div := div + +/-- Use `ofNat` for natural-number casts into fast Goldilocks. -/ +instance instNatCastField : NatCast Field where + natCast := ofNat + +/-- Interpret integer casts through the canonical Goldilocks field. -/ +instance instIntCastField : IntCast Field where + intCast := ofInt + +/-- Natural scalar multiplication is multiplication by the corresponding fast natural cast. -/ +instance instNatSMulField : SMul Nat Field where + smul n x := (n : Field) * x + +/-- Integer scalar multiplication is multiplication by the corresponding fast integer cast. -/ +instance instIntSMulField : SMul Int Field where + smul n x := (n : Field) * x + +/-- Use fast binary exponentiation for natural powers. -/ +instance instPowFieldNat : Pow Field Nat where + pow := pow + +/-- Use fast natural powers and inversion for integer powers. -/ +instance instPowFieldInt : Pow Field Int where + pow x n := + match n with + | Int.ofNat k => pow x k + | Int.negSucc k => pow (inv x) (k + 1) + +/-- Interpret nonnegative rational casts through the canonical Goldilocks field. -/ +instance instNNRatCastField : NNRatCast Field where + nnratCast q := ofField (q : Goldilocks.Field) + +/-- Interpret rational casts through the canonical Goldilocks field. -/ +instance instRatCastField : RatCast Field where + ratCast q := ofField (q : Goldilocks.Field) + +/-- Nonnegative rational scalar multiplication is transported through the canonical field. -/ +instance instNNRatSMulField : SMul ℚ≥0 Field where + smul q x := ofField (q • toField x) + +/-- Rational scalar multiplication is transported through the canonical field. -/ +instance instRatSMulField : SMul ℚ Field where + smul q x := ofField (q • toField x) + +/-! ## Correctness against the canonical model -/ + + +/-- Converting a canonical natural representative to fast form preserves its value. -/ +@[simp] +private theorem toField_ofCanonicalNat (n : Nat) (h : n < Goldilocks.fieldSize) : + toField (ofCanonicalNat n h) = (n : Goldilocks.Field) := by + unfold toField toNat ofCanonicalNat + have hn : n < UInt64.size := Nat.lt_trans h fieldSize_lt_uint64Size + rw [UInt64.toNat_ofNat'] + rw [Nat.mod_eq_of_lt (by simpa [UInt64.size] using hn)] + +/-- Converting a canonical natural representative to fast form and reading it back is +the identity. -/ +@[simp] +private theorem toNat_ofCanonicalNat (n : Nat) (h : n < Goldilocks.fieldSize) : + toNat (ofCanonicalNat n h) = n := by + unfold toNat ofCanonicalNat + have hn : n < UInt64.size := Nat.lt_trans h fieldSize_lt_uint64Size + rw [UInt64.toNat_ofNat'] + exact Nat.mod_eq_of_lt (by simpa [UInt64.size] using hn) + +/-- The natural representative of a fast value built from `n` is `n` reduced modulo the +Goldilocks modulus. -/ +@[simp] +theorem toNat_ofNat (n : Nat) : + toNat (ofNat n) = n % Goldilocks.fieldSize := by + unfold ofNat + rw [toNat_ofCanonicalNat] + +/-- The natural representative of a fast value built from a `UInt64` is that word's +natural value reduced modulo the Goldilocks modulus. -/ +@[simp] +theorem toNat_ofUInt64 (x : UInt64) : + toNat (ofUInt64 x) = x.toNat % Goldilocks.fieldSize := by + -- Reduce to the raw word first: unfolding the subtype would leave the membership + -- proof depending on the term being rewritten. + have hraw : toNat (ofUInt64 x) = (reduceUInt64Raw x).toNat := rfl + rw [hraw] + have hx_two := uint64_toNat_lt_two_fieldSize x + unfold reduceUInt64Raw + by_cases hx : x < modulus + · rw [if_pos hx] + rw [UInt64.lt_iff_toNat_lt, modulus_toNat] at hx + exact (Nat.mod_eq_of_lt hx).symm + · rw [if_neg hx] + rw [UInt64.lt_iff_toNat_lt, modulus_toNat] at hx + have hp : Goldilocks.fieldSize ≤ x.toNat := Nat.le_of_not_gt hx + have hle : modulus ≤ x := by + rw [UInt64.le_iff_toNat_le, modulus_toNat] + exact hp + -- One subtraction canonicalizes, because every `UInt64` is below `2 * fieldSize`. + rw [UInt64.toNat_sub_of_le _ _ hle, modulus_toNat, Nat.mod_eq_sub_mod hp, + Nat.mod_eq_of_lt (by omega)] + +/-- Converting from the canonical `ZMod` field to fast form preserves the canonical +representative. -/ +@[simp] +theorem toNat_ofField (x : Goldilocks.Field) : toNat (ofField x) = x.val := by + unfold ofField + rw [toNat_ofCanonicalNat] + +/-- Converting a natural number to fast form agrees with the same natural cast in the +canonical field. -/ +@[simp] +theorem toField_ofNat (n : Nat) : + toField (ofNat n) = (n : Goldilocks.Field) := by + unfold ofNat + rw [toField_ofCanonicalNat] + rw [← ZMod.natCast_zmod_val (n : Goldilocks.Field)] + rw [ZMod.val_natCast] + +/-- Converting a `UInt64` to fast form agrees with casting its natural value into the +canonical field. -/ +@[simp] +theorem toField_ofUInt64 (x : UInt64) : + toField (ofUInt64 x) = (x.toNat : Goldilocks.Field) := by + unfold toField toNat ofUInt64 + exact reduceUInt64_cast x + +/-- Converting an integer to fast form agrees with casting it into the canonical field. -/ +@[simp] +theorem toField_ofInt (z : Int) : + toField (ofInt z) = (z : Goldilocks.Field) := by + unfold ofInt ofField + rw [toField_ofCanonicalNat] + exact ZMod.natCast_zmod_val (z : Goldilocks.Field) + +/-- Converting from the canonical field to fast form and back is the identity. -/ +@[simp] +theorem toField_ofField (x : Goldilocks.Field) : toField (ofField x) = x := by + unfold ofField + rw [toField_ofCanonicalNat] + exact ZMod.natCast_zmod_val x + +/-- Converting from fast form to the canonical field and back is the identity. -/ +@[simp] +theorem ofField_toField (x : Field) : ofField (toField x) = x := by + apply Subtype.ext + apply UInt64.toNat_inj.mp + change toNat (ofField (toField x)) = toNat x + unfold ofField toField + rw [toNat_ofCanonicalNat] + exact ZMod.val_natCast_of_lt x.property + +/-- The canonical-field interpretation distinguishes fast Goldilocks values. -/ +theorem toField_injective : Function.Injective toField := + Function.LeftInverse.injective ofField_toField + +/-- Fermat-style inversion in the canonical Goldilocks field. -/ +private lemma canonical_inv_eq_pow (a : Goldilocks.Field) (ha : a ≠ 0) : + a⁻¹ = a ^ (Goldilocks.fieldSize - 2) := by + have hcard : Fintype.card Goldilocks.Field = Goldilocks.fieldSize := + ZMod.card Goldilocks.fieldSize + have h1 : a ^ (Goldilocks.fieldSize - 1) = 1 := by + have h := FiniteField.pow_card_sub_one_eq_one a ha + rw [hcard] at h + exact h + have hmul : a * a ^ (Goldilocks.fieldSize - 2) = 1 := by + rw [← pow_succ'] + show a ^ (Goldilocks.fieldSize - 2 + 1) = 1 + have : Goldilocks.fieldSize - 2 + 1 = Goldilocks.fieldSize - 1 := by + unfold Goldilocks.fieldSize + omega + rw [this] + exact h1 + exact (eq_inv_of_mul_eq_one_left (by rwa [mul_comm])).symm + +/-- Fast zero maps to canonical zero. -/ +@[simp] +theorem toField_zero : toField (0 : Field) = 0 := by + decide + +/-- Fast one maps to canonical one. -/ +@[simp] +theorem toField_one : toField (1 : Field) = 1 := by + decide + +/-- Fast addition agrees with canonical-field addition. -/ +@[simp] +theorem toField_add (x y : Field) : toField (x + y) = toField x + toField y := by + change + (((add x y).val.toNat : Goldilocks.Field) = + (x.val.toNat : Goldilocks.Field) + (y.val.toNat : Goldilocks.Field)) + unfold add + rw [reduceAddWithCarryRaw_cast _ _ + (addWithCarry_bound x.val y.val x.property y.property)] + let lo := x.val + y.val + let carry := decide (lo < x.val) + have hvalue := addWithCarry_value x.val y.val + change lo.toNat + (if carry then UInt64.size else 0) = x.val.toNat + y.val.toNat at hvalue + change + ((lo.toNat : Goldilocks.Field) + + (if carry then (UInt64.size : Goldilocks.Field) else 0) = + (x.val.toNat : Goldilocks.Field) + (y.val.toNat : Goldilocks.Field)) + by_cases hcarry : carry = true + · simp only [hcarry, if_true] at hvalue ⊢ + rw [← Nat.cast_add, hvalue, Nat.cast_add] + · simp only [hcarry, Bool.false_eq_true, if_false, add_zero] at hvalue ⊢ + rw [hvalue, Nat.cast_add] + +/-- Fast negation agrees with canonical-field negation. -/ +@[simp] +theorem toField_neg (x : Field) : toField (-x) = -toField x := by + change toField (neg x) = -(toField x) + unfold neg toField toNat + exact negRaw_cast x.val x.property + +/-- Fast subtraction agrees with canonical-field subtraction. -/ +@[simp] +theorem toField_sub (x y : Field) : toField (x - y) = toField x - toField y := by + change toField (sub x y) = toField x - toField y + unfold sub toField toNat + exact subRaw_cast x.val y.val x.property y.property + +/-- Fast multiplication agrees with canonical-field multiplication. -/ +@[simp] +theorem toField_mul (x y : Field) : toField (x * y) = toField x * toField y := by + change toField (mul x y) = toField x * toField y + unfold mul toField toNat + exact reduceMulRaw_cast x.val y.val + +/-- The named fast multiplication function agrees with canonical-field multiplication. -/ +@[simp] +theorem toField_mul_def (x y : Field) : toField (mul x y) = toField x * toField y := + toField_mul x y + +/-- Fast squaring agrees with multiplying the canonical field value by itself. -/ +@[simp] +theorem toField_square (x : Field) : toField (square x) = toField x * toField x := by + change toField (x * x) = toField x * toField x + rw [toField_mul] + +/-- Repeated fast squaring agrees with raising to `2^n` in the canonical field. -/ +@[simp] +theorem toField_squareN (x : Field) (n : Nat) : + toField (squareN x n) = toField x ^ (2 ^ n) := by + induction n generalizing x with + | zero => + unfold squareN + simp + | succ n ih => + unfold squareN + rw [toField_square, ih] + rw [← pow_add] + congr 1 + rw [Nat.pow_succ] + omega + +/-- Fast multiplication is associative, proved by transporting to the canonical field. -/ +private theorem mul_assoc_field (x y z : Field) : (x * y) * z = x * (y * z) := by + apply toField_injective + rw [toField_mul, toField_mul, toField_mul, toField_mul] + ring + +/-- Binary exponentiation satisfies the expected successor equation. -/ +private theorem pow_succ (x : Field) (n : Nat) : pow x (n + 1) = pow x n * x := by + unfold pow + let _ : Semigroup Field := { + mul := (· * ·) + mul_assoc := mul_assoc_field + } + exact npowBinRec_succ n x + +/-- Fast natural-power computation agrees with powers in the canonical field. -/ +@[simp] +theorem toField_pow (x : Field) (n : Nat) : toField (pow x n) = toField x ^ n := by + induction n with + | zero => + unfold pow + rw [npowBinRec_zero] + rw [toField_one] + simp + | succ n ih => + rw [pow_succ, toField_mul, ih, _root_.pow_succ] + +/-- The optimized inversion chain computes the Fermat inverse exponent. -/ +private theorem toField_inv_chain (x : Field) : + toField (inv x) = toField x ^ invExponent := by + unfold inv + simp only [toField_mul_def, toField_square, toField_squareN] + ring_nf + simp [invExponent, Goldilocks.fieldSize] + +/-- Fast inversion agrees with canonical inversion before notation is unfolded. -/ +private theorem toField_inv_raw (x : Field) : toField (inv x) = (toField x)⁻¹ := by + rw [toField_inv_chain] + by_cases hx : toField x = 0 + · rw [hx] + simp [invExponent, Goldilocks.fieldSize] + · simpa [invExponent] using (canonical_inv_eq_pow (toField x) hx).symm + +/-- Fast inversion agrees with inversion in the canonical field. -/ +@[simp] +theorem toField_inv (x : Field) : toField x⁻¹ = (toField x)⁻¹ := by + change toField (inv x) = (toField x)⁻¹ + exact toField_inv_raw x + +/-- Division is multiplication by inverse at the level of canonical interpretation. -/ +private theorem toField_div_mul_inv (x y : Field) : + toField (div x y) = toField x * toField (inv y) := by + unfold div + change toField (x * inv y) = toField x * toField (inv y) + exact toField_mul x (inv y) + +/-- Fast division agrees with division in the canonical field. -/ +@[simp] +theorem toField_div (x y : Field) : toField (x / y) = toField x / toField y := by + change toField (div x y) = toField x / toField y + rw [toField_div_mul_inv, toField_inv_raw y] + rfl + +/-- Natural casts in the fast field agree with natural casts in the canonical field. -/ +@[simp] +theorem toField_natCast (n : Nat) : toField (n : Field) = (n : Goldilocks.Field) := by + change toField (ofNat n) = (n : Goldilocks.Field) + rw [toField_ofNat] + +/-- Integer casts in the fast field agree with integer casts in the canonical field. -/ +@[simp] +theorem toField_intCast (n : Int) : toField (n : Field) = (n : Goldilocks.Field) := by + change toField (ofInt n) = (n : Goldilocks.Field) + rw [toField_ofInt] + +/-- Fast natural scalar multiplication agrees with canonical-field scalar multiplication. -/ +@[simp] +theorem toField_nsmul (n : Nat) (x : Field) : toField (n • x) = n • toField x := by + change toField ((n : Field) * x) = n • toField x + rw [toField_mul, toField_natCast] + rw [nsmul_eq_mul] + +/-- Fast integer scalar multiplication agrees with canonical-field scalar multiplication. -/ +@[simp] +theorem toField_zsmul (n : Int) (x : Field) : toField (n • x) = n • toField x := by + change toField ((n : Field) * x) = n • toField x + rw [toField_mul, toField_intCast] + rw [zsmul_eq_mul] + +/-- Standard fast natural powers agree with powers in the canonical field. -/ +@[simp] +theorem toField_npow (x : Field) (n : Nat) : toField (x ^ n) = toField x ^ n := by + change toField (pow x n) = toField x ^ n + rw [toField_pow] + +/-- Standard fast integer powers agree with integer powers in the canonical field. -/ +@[simp] +theorem toField_zpow (x : Field) (n : Int) : toField (x ^ n) = toField x ^ n := by + cases n with + | ofNat n => + change toField (pow x n) = toField x ^ (Int.ofNat n) + rw [toField_pow] + exact (zpow_natCast (toField x) n).symm + | negSucc n => + change toField (pow (inv x) (n + 1)) = toField x ^ (Int.negSucc n) + have hinv : toField (inv x) = (toField x)⁻¹ := toField_inv_raw x + rw [toField_pow, hinv, zpow_negSucc, inv_pow] + +/-- Nonnegative rational casts in the fast field agree with canonical-field casts. -/ +@[simp] +theorem toField_nnratCast (q : ℚ≥0) : toField (q : Field) = (q : Goldilocks.Field) := by + change toField (ofField (q : Goldilocks.Field)) = (q : Goldilocks.Field) + rw [toField_ofField] + +/-- Rational casts in the fast field agree with canonical-field casts. -/ +@[simp] +theorem toField_ratCast (q : ℚ) : toField (q : Field) = (q : Goldilocks.Field) := by + change toField (ofField (q : Goldilocks.Field)) = (q : Goldilocks.Field) + rw [toField_ofField] + +/-- Fast nonnegative rational scalar multiplication agrees with canonical-field scalar +multiplication. -/ +@[simp] +theorem toField_nnqsmul (q : ℚ≥0) (x : Field) : toField (q • x) = q • toField x := by + change toField (ofField (q • toField x)) = q • toField x + rw [toField_ofField] + +/-- Fast rational scalar multiplication agrees with canonical-field scalar multiplication. -/ +@[simp] +theorem toField_qsmul (q : ℚ) (x : Field) : toField (q • x) = q • toField x := by + change toField (ofField (q • toField x)) = q • toField x + rw [toField_ofField] + +/-! ## Canonical bridge and field instances -/ + + +/-- Ring equivalence between the fast representation and canonical Goldilocks. -/ +def ringEquiv : Field ≃+* Goldilocks.Field where + toFun := toField + invFun := ofField + left_inv := ofField_toField + right_inv := toField_ofField + map_add' := toField_add + map_mul' := toField_mul + +/-- Applying `ringEquiv` is the same as interpreting a fast value canonically. -/ +@[simp] +theorem ringEquiv_apply (x : Field) : ringEquiv x = toField x := rfl + +/-- Applying the inverse `ringEquiv` converts a canonical value into fast form. -/ +@[simp] +theorem ringEquiv_symm_apply (x : Goldilocks.Field) : ringEquiv.symm x = ofField x := rfl + +/-- Field instance transferred from canonical Goldilocks through `toField`. -/ +instance (priority := low) instField : _root_.Field Field := + toField_injective.field toField + toField_zero + toField_one + toField_add + toField_mul + toField_neg + toField_sub + toField_inv + toField_div + toField_nsmul + toField_zsmul + toField_nnqsmul + toField_qsmul + toField_npow + toField_zpow + toField_natCast + toField_intCast + toField_nnratCast + toField_ratCast + +/-- Commutative-ring instance inherited from the transferred field structure. -/ +instance (priority := low) instCommRing : CommRing Field := by + infer_instance + +/-- Fast Goldilocks is a non-binary field. -/ +instance (priority := low) instNonBinaryField : NonBinaryField Field where + char_neq_2 := by + intro h + have hv := congrArg Subtype.val h + exact (by decide : (2 : UInt64) ≠ 0) hv + +end Fast +end Goldilocks diff --git a/CompPoly/Fields/Goldilocks/FastDefs.lean b/CompPoly/Fields/Goldilocks/FastDefs.lean new file mode 100644 index 00000000..ebdb0b20 --- /dev/null +++ b/CompPoly/Fields/Goldilocks/FastDefs.lean @@ -0,0 +1,106 @@ +/- +Copyright (c) 2026 CompPoly Contributors. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Varun Thakore +-/ +module + +/-! +# Fast Goldilocks: runtime definitions (zero-import) + +The runtime definitions of the native-word Goldilocks arithmetic, split out of +`CompPoly.Fields.Goldilocks.Fast` verbatim. All correctness statements about them +live in that sibling module, which imports this one. + +This module deliberately has **zero imports**: downstream consumers put it into +`precompileModules` native-compilation lanes, and `precompileModules` compiles the +entire import closure, so the runtime definitions must not pull in mathlib. +-/ + +@[expose] public section + +namespace Goldilocks.Fast + +/-! ## Word constants -/ + +/-- Goldilocks modulus `2^64 - 2^32 + 1` as a native word. -/ +@[inline] +def modulus : UInt64 := 0xffffffff00000001 + +/-- Two's complement of the modulus: `2^64 - modulus = 2^32 - 1 = 0xFFFFFFFF`. -/ +@[inline] +def negModulus : UInt64 := 0xffffffff + +/-! ## Raw word kernels + +Every kernel takes canonical `UInt64` inputs and returns a canonical representative +below the modulus. Correctness lives in `CompPoly.Fields.Goldilocks.Fast`. -/ + + +/-- Full 64-by-64 product as `(lo, hi)` words, computed from 32-bit limbs. -/ +@[inline] +def wideMul (x y : UInt64) : UInt64 × UInt64 := + let xLo := x &&& negModulus + let xHi := x >>> 32 + let yLo := y &&& negModulus + let yHi := y >>> 32 + let p00 := xLo * yLo + let p01 := xLo * yHi + let p10 := xHi * yLo + let p11 := xHi * yHi + let carry := (p00 >>> 32) + (p01 &&& negModulus) + (p10 &&& negModulus) + let hi := p11 + (p01 >>> 32) + (p10 >>> 32) + (carry >>> 32) + (x * y, hi) + +/-- Raw one-word reduction for a `UInt64` value. + +Since every `UInt64` is below `2^64 = p + 2^32 - 1`, one subtraction by `p` +is enough to canonicalize a native word. +-/ +@[inline] +def reduceUInt64Raw (x : UInt64) : UInt64 := + if x < modulus then x else x - modulus + +/-- Raw reduction of a 128-bit integer represented by low and high words modulo Goldilocks. -/ +@[inline] +def reduceUInt128Raw (lo hi : UInt64) : UInt64 := + let hi_hi := hi >>> 32 + let hi_lo := hi &&& negModulus + + let borrow := lo < hi_hi + let t0 := lo - hi_hi + let t0 := if borrow then t0 - negModulus else t0 + + let t1 := hi_lo * negModulus + + let t2 := t0 + t1 + let overflow := t2 < t0 + let t2 := if overflow then t2 + negModulus else t2 + + reduceUInt64Raw t2 + +/-- Raw reduction of a 64-by-64 product modulo Goldilocks. -/ +@[inline] +def reduceMulRaw (x y : UInt64) : UInt64 := + let product := wideMul x y + reduceUInt128Raw product.1 product.2 + +/-- Raw one-step reduction for a 65-bit addition represented by low word and carry. -/ +@[inline] +def reduceAddWithCarryRaw (lo : UInt64) (carry : Bool) : UInt64 := + if carry then + lo + negModulus + else + reduceUInt64Raw lo + +/-- Raw modular negation in canonical form. -/ +@[inline] +def negRaw (x : UInt64) : UInt64 := + if x = 0 then 0 else modulus - x + +/-- Raw modular subtraction in canonical form. -/ +@[inline] +def subRaw (x y : UInt64) : UInt64 := + if y ≤ x then x - y else x - y - negModulus + +end Goldilocks.Fast diff --git a/CompPoly/Fields/Goldilocks/FastReduction.lean b/CompPoly/Fields/Goldilocks/FastReduction.lean new file mode 100644 index 00000000..5afe7a03 --- /dev/null +++ b/CompPoly/Fields/Goldilocks/FastReduction.lean @@ -0,0 +1,883 @@ +/- +Copyright (c) 2026 CompPoly Contributors. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Varun Thakore +-/ +module + +public import CompPoly.Fields.Goldilocks.Basic +public import CompPoly.Fields.Goldilocks.FastDefs + +/-! +# Fast Goldilocks: word-level correctness + +Correctness of the raw `UInt64` kernels in `CompPoly.Fields.Goldilocks.FastDefs`: +low-level word lemmas (limb splitting, borrow and overflow correction, the 64-by-64 +product decomposition), then for each kernel a bound showing the result is below the +modulus and a cast lemma identifying it with the corresponding operation in +`Goldilocks.Field`. + +The field carrier and its API are built on these in +`CompPoly.Fields.Goldilocks.Fast`. +-/ + +@[expose] public section + +namespace Goldilocks +namespace Fast + +/-! ## Low-level `UInt64` lemmas -/ + +/-- The native `UInt64` modulus agrees with the mathematical Goldilocks modulus. -/ +@[simp] +theorem modulus_toNat : modulus.toNat = Goldilocks.fieldSize := by + decide + +/-- The native negated-modulus constant agrees with `2^32 - 1`. -/ +@[simp] +theorem negModulus_toNat : negModulus.toNat = 2 ^ 32 - 1 := by + decide + +/-- The Goldilocks modulus is positive. -/ +theorem fieldSize_pos : 0 < Goldilocks.fieldSize := by + decide + +/-- The Goldilocks modulus fits in a `UInt64`. -/ +theorem fieldSize_lt_uint64Size : Goldilocks.fieldSize < UInt64.size := by + decide + +/-- Every `UInt64` value is below twice the Goldilocks modulus. -/ +theorem uint64_toNat_lt_two_fieldSize (x : UInt64) : + x.toNat < 2 * Goldilocks.fieldSize := by + exact Nat.lt_trans (UInt64.toNat_lt_size x) (by decide) + +/-- The folding congruence used by the Goldilocks reducer. + +`2^64 ≡ 2^32 - 1 (mod p)`. +-/ +theorem uint64_cast_eq_negModulus : + (UInt64.size : Goldilocks.Field) = (negModulus.toNat : Goldilocks.Field) := by + decide + +/-- Multiplying by `2^32 - 1` after shifting by `2^32` is negation modulo Goldilocks. -/ +theorem pow32_mul_negModulus_cast : + ((2 ^ 32 : Nat) : Goldilocks.Field) * + (negModulus.toNat : Goldilocks.Field) = + -1 := by + decide + +/-- Right shifting a `UInt64` by 32 gives division by `2^32` on naturals. -/ +theorem shiftRight32_toNat (x : UInt64) : + (x >>> 32).toNat = x.toNat / 2 ^ 32 := by + rw [UInt64.toNat_shiftRight] + have h : (32 : UInt64).toNat % 64 = 32 := by + decide + rw [h, Nat.shiftRight_eq_div_pow] + +/-- Masking with `2^32 - 1` gives the low 32 bits on naturals. -/ +theorem and_negModulus_toNat (x : UInt64) : + (x &&& negModulus).toNat = x.toNat % 2 ^ 32 := by + rw [← UInt64.toNat_toBitVec (x &&& negModulus)] + rw [UInt64.toBitVec_and] + rw [BitVec.toNat_and] + rw [UInt64.toNat_toBitVec, UInt64.toNat_toBitVec] + rw [negModulus_toNat] + rw [Nat.and_two_pow_sub_one_eq_mod] + +/-- A `UInt64` subtraction with the Goldilocks borrow correction represents subtraction modulo `p`. + +The assumption says the subtrahend is a 32-bit limb, which is the case for +`hi >>> 32` in the 128-bit reducer. +-/ +theorem subBorrow_cast (a b : UInt64) (hb : b.toNat < 2 ^ 32) : + (((if a < b then a - b - negModulus else a - b).toNat) : Goldilocks.Field) = + (a.toNat : Goldilocks.Field) - (b.toNat : Goldilocks.Field) := by + by_cases h : a < b + · rw [if_pos h] + have hlt : a.toNat < b.toNat := by + simpa [UInt64.lt_iff_toNat_lt] using h + have hb_le_size : b.toNat ≤ UInt64.size := Nat.le_of_lt (UInt64.toNat_lt_size b) + have hsub_lt_size : UInt64.size - b.toNat + a.toNat < 2 ^ 64 := by + have hsize : UInt64.size = 2 ^ 64 := rfl + rw [hsize] at hb_le_size ⊢ + omega + have hsub_raw : (a - b).toNat = UInt64.size - b.toNat + a.toNat := by + rw [UInt64.toNat_sub] + exact Nat.mod_eq_of_lt hsub_lt_size + have hneg_le : negModulus ≤ a - b := by + rw [UInt64.le_iff_toNat_le] + rw [hsub_raw, negModulus_toNat] + have hsize : UInt64.size = 2 ^ 64 := rfl + rw [hsize] + omega + rw [UInt64.toNat_sub_of_le _ _ hneg_le] + rw [hsub_raw] + have hneg_le_nat : negModulus.toNat ≤ UInt64.size - b.toNat + a.toNat := by + rw [← hsub_raw] + rwa [UInt64.le_iff_toNat_le] at hneg_le + rw [Nat.cast_sub hneg_le_nat] + rw [Nat.cast_add] + rw [Nat.cast_sub hb_le_size] + rw [uint64_cast_eq_negModulus] + ring + · rw [if_neg h] + have hle : b ≤ a := by + rw [UInt64.le_iff_toNat_le] + rw [UInt64.lt_iff_toNat_lt] at h + exact Nat.le_of_not_gt h + have hle_nat : b.toNat ≤ a.toNat := by + rwa [UInt64.le_iff_toNat_le] at hle + rw [UInt64.toNat_sub_of_le _ _ hle] + rw [Nat.cast_sub hle_nat] + +/-- A bounded `UInt64` addition with the Goldilocks overflow correction represents +addition modulo `p`. -/ +theorem addOverflowBounded_cast (a b : UInt64) + (hbound : a.toNat + b.toNat < 2 * UInt64.size - negModulus.toNat) : + (((if a + b < a then a + b + negModulus else a + b).toNat) : + Goldilocks.Field) = + (a.toNat : Goldilocks.Field) + (b.toNat : Goldilocks.Field) := by + by_cases hsum : a.toNat + b.toNat < UInt64.size + · have hnot : ¬a + b < a := by + intro hlt + have hlt_nat : (a + b).toNat < a.toNat := by + simpa [UInt64.lt_iff_toNat_lt] using hlt + rw [UInt64.toNat_add, Nat.mod_eq_of_lt hsum] at hlt_nat + omega + rw [if_neg hnot] + rw [UInt64.toNat_add, Nat.mod_eq_of_lt hsum, Nat.cast_add] + · have hsize_le : UInt64.size ≤ a.toNat + b.toNat := Nat.le_of_not_gt hsum + have hlt : a + b < a := by + rw [UInt64.lt_iff_toNat_lt] + rw [UInt64.toNat_add] + rw [Nat.mod_eq_sub_mod hsize_le] + have hdiff_lt : a.toNat + b.toNat - UInt64.size < UInt64.size := by + have ha := UInt64.toNat_lt_size a + have hb := UInt64.toNat_lt_size b + omega + rw [Nat.mod_eq_of_lt hdiff_lt] + have hb := UInt64.toNat_lt_size b + omega + rw [if_pos hlt] + have hsum_mod : (a + b).toNat = a.toNat + b.toNat - UInt64.size := by + rw [UInt64.toNat_add] + rw [Nat.mod_eq_sub_mod hsize_le] + have hdiff_lt : a.toNat + b.toNat - UInt64.size < UInt64.size := by + have ha := UInt64.toNat_lt_size a + have hb := UInt64.toNat_lt_size b + omega + rw [Nat.mod_eq_of_lt hdiff_lt] + rw [UInt64.toNat_add] + rw [hsum_mod] + have hno_second : a.toNat + b.toNat - UInt64.size + negModulus.toNat < UInt64.size := by + omega + rw [Nat.mod_eq_of_lt hno_second] + rw [Nat.cast_add] + rw [Nat.cast_sub hsize_le] + rw [uint64_cast_eq_negModulus] + rw [Nat.cast_add] + ring + +/-- Multiplication by `2^32 - 1` does not overflow for a 32-bit limb. -/ +theorem mul_negModulus_toNat_of_lt (x : UInt64) (hx : x.toNat < 2 ^ 32) : + (x * negModulus).toNat = x.toNat * negModulus.toNat := by + rw [UInt64.toNat_mul] + rw [Nat.mod_eq_of_lt] + rw [negModulus_toNat] + omega + +/-- The product of a 32-bit limb by `2^32 - 1` leaves enough headroom for correction. -/ +theorem mul_negModulus_toNat_le (x : UInt64) (hx : x.toNat < 2 ^ 32) : + (x * negModulus).toNat ≤ UInt64.size - 2 * negModulus.toNat := by + rw [mul_negModulus_toNat_of_lt x hx] + rw [negModulus_toNat] + have hx_le : x.toNat ≤ 2 ^ 32 - 1 := by + omega + have hmul : x.toNat * (2 ^ 32 - 1) ≤ (2 ^ 32 - 1) * (2 ^ 32 - 1) := + Nat.mul_le_mul_right _ hx_le + have hconst : (2 ^ 32 - 1) * (2 ^ 32 - 1) ≤ UInt64.size - 2 * (2 ^ 32 - 1) := by + decide + exact Nat.le_trans hmul hconst + +/-- Splitting a high word into 32-bit limbs matches the Goldilocks folding congruence. -/ +theorem hi_split_cast (hi : UInt64) : + (hi.toNat : Goldilocks.Field) * (UInt64.size : Goldilocks.Field) = + ((hi &&& negModulus).toNat : Goldilocks.Field) * + (negModulus.toNat : Goldilocks.Field) - + ((hi >>> 32).toNat : Goldilocks.Field) := by + have hsplit_nat : hi.toNat = hi.toNat % 2 ^ 32 + 2 ^ 32 * (hi.toNat / 2 ^ 32) := by + rw [Nat.mod_add_div] + have hcast_split : + (hi.toNat : Goldilocks.Field) = + ((hi.toNat % 2 ^ 32 : Nat) : Goldilocks.Field) + + (((2 ^ 32 : Nat) : Goldilocks.Field) * + ((hi.toNat / 2 ^ 32 : Nat) : Goldilocks.Field)) := by + simpa [Nat.cast_add, Nat.cast_mul] using + congrArg (fun n : Nat => (n : Goldilocks.Field)) hsplit_nat + rw [hcast_split] + rw [and_negModulus_toNat, shiftRight32_toNat, uint64_cast_eq_negModulus] + rw [add_mul] + conv_lhs => + enter [2] + rw [mul_assoc] + rw [mul_comm ((hi.toNat / 2 ^ 32 : Nat) : Goldilocks.Field)] + rw [← mul_assoc] + rw [pow32_mul_negModulus_cast] + ring + +/-- A `UInt64` value decomposes into its low and high 32-bit limbs. -/ +theorem uint64_split32 (x : UInt64) : + x.toNat = (x &&& negModulus).toNat + 2 ^ 32 * (x >>> 32).toNat := by + rw [and_negModulus_toNat, shiftRight32_toNat] + rw [Nat.mod_add_div] + +/-- The low 32-bit limb of a `UInt64` is below `2^32`. -/ +theorem uint64_low32_lt (x : UInt64) : + (x &&& negModulus).toNat < 2 ^ 32 := by + rw [and_negModulus_toNat] + exact Nat.mod_lt _ (by decide) + +/-- The high 32-bit limb of a `UInt64` is below `2^32`. -/ +theorem uint64_high32_lt (x : UInt64) : + (x >>> 32).toNat < 2 ^ 32 := by + rw [shiftRight32_toNat] + have hx := UInt64.toNat_lt_size x + change x.toNat < 2 ^ 64 at hx + exact Nat.div_lt_of_lt_mul hx + +/-- Multiplying two 32-bit limbs does not overflow `UInt64`. -/ +theorem mul32_toNat (a b : UInt64) (ha : a.toNat < 2 ^ 32) (hb : b.toNat < 2 ^ 32) : + (a * b).toNat = a.toNat * b.toNat := by + rw [UInt64.toNat_mul] + rw [Nat.mod_eq_of_lt] + nlinarith + +/-- Algebraic decomposition of a product after splitting both factors into 32-bit limbs. -/ +theorem product_split32 (x y : UInt64) : + x.toNat * y.toNat = + (x &&& negModulus).toNat * (y &&& negModulus).toNat + + 2 ^ 32 * + ((x &&& negModulus).toNat * (y >>> 32).toNat + + (x >>> 32).toNat * (y &&& negModulus).toNat) + + 2 ^ 64 * ((x >>> 32).toNat * (y >>> 32).toNat) := by + rw [uint64_split32 x, uint64_split32 y] + ring_nf + +/-- Low word returned by a 64-by-64 product implementation. -/ +theorem wideMul_low_toNat (lo : UInt64) (x y : UInt64) (hlo : lo = x * y) : + lo.toNat = x.toNat * y.toNat % UInt64.size := by + rw [hlo, UInt64.toNat_mul] + +/-- Pure Nat carry formula for the high word of a 32-bit-limb 64-by-64 product. -/ +theorem wideMul_high_nat + (p00 p01 p10 p11 : Nat) + (_hp00 : p00 < 2 ^ 64) + (_hp01 : p01 < 2 ^ 64) + (_hp10 : p10 < 2 ^ 64) + (_hp11 : p11 < 2 ^ 64) : + let B := 2 ^ 32 + let carry := p00 / B + p01 % B + p10 % B + p11 + p01 / B + p10 / B + carry / B = + (p00 + B * (p01 + p10) + B ^ 2 * p11) / B ^ 2 := by + dsimp + let carry := p00 / 4294967296 + p01 % 4294967296 + p10 % 4294967296 + let q := p11 + p01 / 4294967296 + p10 / 4294967296 + carry / 4294967296 + have hN : + p00 + 4294967296 * (p01 + p10) + 18446744073709551616 * p11 = + p00 % 4294967296 + 4294967296 * (carry % 4294967296) + + 18446744073709551616 * q := by + have hp00d : p00 % 4294967296 + 4294967296 * (p00 / 4294967296) = p00 := + Nat.mod_add_div p00 4294967296 + have hp01d : p01 % 4294967296 + 4294967296 * (p01 / 4294967296) = p01 := + Nat.mod_add_div p01 4294967296 + have hp10d : p10 % 4294967296 + 4294967296 * (p10 / 4294967296) = p10 := + Nat.mod_add_div p10 4294967296 + have hcd : carry % 4294967296 + 4294967296 * (carry / 4294967296) = carry := + Nat.mod_add_div carry 4294967296 + subst q + subst carry + omega + rw [hN] + change q = + (p00 % 4294967296 + 4294967296 * (carry % 4294967296) + + 18446744073709551616 * q) / + 18446744073709551616 + rw [Nat.mul_comm 18446744073709551616 q] + rw [Nat.add_mul_div_right _ _ (show 0 < 18446744073709551616 by decide)] + rw [Nat.div_eq_of_lt] + · rw [Nat.zero_add] + · subst carry + have hmod0 : p00 % 4294967296 < 4294967296 := Nat.mod_lt _ (by decide) + have hmod1 : + (p00 / 4294967296 + p01 % 4294967296 + p10 % 4294967296) % + 4294967296 < + 4294967296 := Nat.mod_lt _ (by decide) + omega + +/-- High word returned by the 32-bit-limb `UInt64` multiplication algorithm. -/ +theorem wideMul_high_toNat + (x y hi : UInt64) + (hhi : + hi = + let xLo := x &&& negModulus + let xHi := x >>> 32 + let yLo := y &&& negModulus + let yHi := y >>> 32 + let p00 := xLo * yLo + let p01 := xLo * yHi + let p10 := xHi * yLo + let p11 := xHi * yHi + let carry := (p00 >>> 32) + (p01 &&& negModulus) + (p10 &&& negModulus) + p11 + (p01 >>> 32) + (p10 >>> 32) + (carry >>> 32)) : + hi.toNat = x.toNat * y.toNat / UInt64.size := by + let xLo := x &&& negModulus + let xHi := x >>> 32 + let yLo := y &&& negModulus + let yHi := y >>> 32 + let p00 := xLo * yLo + let p01 := xLo * yHi + let p10 := xHi * yLo + let p11 := xHi * yHi + let carry := (p00 >>> 32) + (p01 &&& negModulus) + (p10 &&& negModulus) + have hxLo_lt : xLo.toNat < 2 ^ 32 := by + subst xLo + exact uint64_low32_lt x + have hxHi_lt : xHi.toNat < 2 ^ 32 := by + subst xHi + exact uint64_high32_lt x + have hyLo_lt : yLo.toNat < 2 ^ 32 := by + subst yLo + exact uint64_low32_lt y + have hyHi_lt : yHi.toNat < 2 ^ 32 := by + subst yHi + exact uint64_high32_lt y + have hp00_nat : p00.toNat = xLo.toNat * yLo.toNat := by + subst p00 + exact mul32_toNat xLo yLo hxLo_lt hyLo_lt + have hp01_nat : p01.toNat = xLo.toNat * yHi.toNat := by + subst p01 + exact mul32_toNat xLo yHi hxLo_lt hyHi_lt + have hp10_nat : p10.toNat = xHi.toNat * yLo.toNat := by + subst p10 + exact mul32_toNat xHi yLo hxHi_lt hyLo_lt + have hp11_nat : p11.toNat = xHi.toNat * yHi.toNat := by + subst p11 + exact mul32_toNat xHi yHi hxHi_lt hyHi_lt + have hp00_lt : p00.toNat < 2 ^ 64 := by + rw [hp00_nat] + nlinarith [hxLo_lt, hyLo_lt] + have hp01_lt : p01.toNat < 2 ^ 64 := by + rw [hp01_nat] + nlinarith [hxLo_lt, hyHi_lt] + have hp10_lt : p10.toNat < 2 ^ 64 := by + rw [hp10_nat] + nlinarith [hxHi_lt, hyLo_lt] + have hp11_lt : p11.toNat < 2 ^ 64 := by + rw [hp11_nat] + nlinarith [hxHi_lt, hyHi_lt] + have hcarry_nat : + carry.toNat = p00.toNat / 2 ^ 32 + p01.toNat % 2 ^ 32 + p10.toNat % 2 ^ 32 := by + subst carry + rw [UInt64.toNat_add, UInt64.toNat_add] + rw [shiftRight32_toNat, and_negModulus_toNat, and_negModulus_toNat] + have hp00_hi_lt : p00.toNat / 2 ^ 32 < 2 ^ 32 := by + rw [Nat.div_lt_iff_lt_mul (by decide : 0 < 2 ^ 32)] + simpa [pow_add] using hp00_lt + have hp01_lo_lt : p01.toNat % 2 ^ 32 < 2 ^ 32 := Nat.mod_lt _ (by decide) + have hp10_lo_lt : p10.toNat % 2 ^ 32 < 2 ^ 32 := Nat.mod_lt _ (by decide) + have hsum01 : + p00.toNat / 2 ^ 32 + p01.toNat % 2 ^ 32 < 2 ^ 64 := by + omega + have hsum012 : + p00.toNat / 2 ^ 32 + p01.toNat % 2 ^ 32 + p10.toNat % 2 ^ 32 < + 2 ^ 64 := by + omega + rw [Nat.mod_eq_of_lt hsum01, Nat.mod_eq_of_lt hsum012] + have hwide := + wideMul_high_nat p00.toNat p01.toNat p10.toNat p11.toNat hp00_lt hp01_lt hp10_lt hp11_lt + have hwide' : + p11.toNat + p01.toNat / 2 ^ 32 + p10.toNat / 2 ^ 32 + + (p00.toNat / 2 ^ 32 + p01.toNat % 2 ^ 32 + p10.toNat % 2 ^ 32) / 2 ^ 32 = + (p00.toNat + 2 ^ 32 * (p01.toNat + p10.toNat) + (2 ^ 32) ^ 2 * p11.toNat) / + (2 ^ 32) ^ 2 := by + simpa using hwide + have hquot_lt : + p11.toNat + p01.toNat / 2 ^ 32 + p10.toNat / 2 ^ 32 + carry.toNat / 2 ^ 32 < + UInt64.size := by + rw [hcarry_nat] + rw [hwide'] + have hprod_bound : x.toNat * y.toNat < UInt64.size * UInt64.size := by + exact + mul_lt_mul'' (UInt64.toNat_lt_size x) (UInt64.toNat_lt_size y) (Nat.zero_le _) + (Nat.zero_le _) + have hsplit : + p00.toNat + 2 ^ 32 * (p01.toNat + p10.toNat) + (2 ^ 32) ^ 2 * p11.toNat = + x.toNat * y.toNat := by + rw [hp00_nat, hp01_nat, hp10_nat, hp11_nat] + subst p00 + subst p01 + subst p10 + subst p11 + subst xLo + subst xHi + subst yLo + subst yHi + simpa [pow_add, pow_mul] using (product_split32 x y).symm + rw [hsplit] + change x.toNat * y.toNat / UInt64.size < UInt64.size + rw [Nat.div_lt_iff_lt_mul (by decide : 0 < UInt64.size)] + exact hprod_bound + have hhi_nat : + hi.toNat = p11.toNat + p01.toNat / 2 ^ 32 + p10.toNat / 2 ^ 32 + carry.toNat / 2 ^ 32 := by + rw [hhi] + dsimp only + change (p11 + (p01 >>> 32) + (p10 >>> 32) + (carry >>> 32)).toNat = + p11.toNat + p01.toNat / 2 ^ 32 + p10.toNat / 2 ^ 32 + carry.toNat / 2 ^ 32 + rw [UInt64.toNat_add, UInt64.toNat_add, UInt64.toNat_add] + rw [shiftRight32_toNat, shiftRight32_toNat, shiftRight32_toNat] + have hsum01 : p11.toNat + p01.toNat / 2 ^ 32 < UInt64.size := by + have hle : p11.toNat + p01.toNat / 2 ^ 32 ≤ + p11.toNat + p01.toNat / 2 ^ 32 + p10.toNat / 2 ^ 32 + carry.toNat / 2 ^ 32 := by + omega + exact lt_of_le_of_lt hle hquot_lt + have hsum012 : + p11.toNat + p01.toNat / 2 ^ 32 + p10.toNat / 2 ^ 32 < UInt64.size := by + have hle : p11.toNat + p01.toNat / 2 ^ 32 + p10.toNat / 2 ^ 32 ≤ + p11.toNat + p01.toNat / 2 ^ 32 + p10.toNat / 2 ^ 32 + carry.toNat / 2 ^ 32 := by + omega + exact lt_of_le_of_lt hle hquot_lt + have hquot_lt_pow : + p11.toNat + p01.toNat / 2 ^ 32 + p10.toNat / 2 ^ 32 + carry.toNat / 2 ^ 32 < + 2 ^ 64 := by + simpa [UInt64.size] using hquot_lt + have hsum01_pow : p11.toNat + p01.toNat / 2 ^ 32 < 2 ^ 64 := by + simpa [UInt64.size] using hsum01 + have hsum012_pow : + p11.toNat + p01.toNat / 2 ^ 32 + p10.toNat / 2 ^ 32 < 2 ^ 64 := by + simpa [UInt64.size] using hsum012 + rw [Nat.mod_eq_of_lt hsum01_pow, Nat.mod_eq_of_lt hsum012_pow, + Nat.mod_eq_of_lt hquot_lt_pow] + rw [hhi_nat] + rw [hcarry_nat] + rw [hwide'] + have hsplit : + p00.toNat + 2 ^ 32 * (p01.toNat + p10.toNat) + (2 ^ 32) ^ 2 * p11.toNat = + x.toNat * y.toNat := by + rw [hp00_nat, hp01_nat, hp10_nat, hp11_nat] + subst p00 + subst p01 + subst p10 + subst p11 + subst xLo + subst xHi + subst yLo + subst yHi + simpa [pow_add, pow_mul] using (product_split32 x y).symm + rw [hsplit] + rfl + +/-- Combined semantic correctness of a 64-by-64 product represented by low and high words. -/ +theorem wideMul_cast + (x y lo hi : UInt64) + (hlo : lo = x * y) + (hhi : hi.toNat = x.toNat * y.toNat / UInt64.size) : + (lo.toNat : Goldilocks.Field) + + (hi.toNat : Goldilocks.Field) * (UInt64.size : Goldilocks.Field) = + (x.toNat : Goldilocks.Field) * (y.toNat : Goldilocks.Field) := by + rw [wideMul_low_toNat lo x y hlo, hhi] + rw [← Nat.cast_mul, ← Nat.cast_add, Nat.mul_comm (x.toNat * y.toNat / UInt64.size), + Nat.mod_add_div, Nat.cast_mul] + +/-! ## Raw kernel correctness -/ + +/-- The raw one-word reducer returns a canonical representative. -/ +theorem reduceUInt64Raw_lt (x : UInt64) : + (reduceUInt64Raw x).toNat < Goldilocks.fieldSize := by + unfold reduceUInt64Raw + by_cases hx : x < modulus + · rw [if_pos hx] + rw [UInt64.lt_iff_toNat_lt, modulus_toNat] at hx + exact hx + · rw [if_neg hx] + have hmod_le_x_nat : Goldilocks.fieldSize ≤ x.toNat := by + rw [UInt64.lt_iff_toNat_lt, modulus_toNat] at hx + exact Nat.le_of_not_gt hx + have hmod_le_x : modulus ≤ x := by + rw [UInt64.le_iff_toNat_le, modulus_toNat] + exact hmod_le_x_nat + rw [UInt64.toNat_sub_of_le _ _ hmod_le_x, modulus_toNat] + have hx_lt_two := uint64_toNat_lt_two_fieldSize x + omega + +/-- One-word reduction preserves the represented canonical field element. -/ +theorem reduceUInt64Raw_cast (x : UInt64) : + ((reduceUInt64Raw x).toNat : Goldilocks.Field) = + (x.toNat : Goldilocks.Field) := by + unfold reduceUInt64Raw + by_cases hx : x < modulus + · rw [if_pos hx] + · rw [if_neg hx] + have hmod_le_x : modulus ≤ x := by + rw [UInt64.le_iff_toNat_le, modulus_toNat] + rw [UInt64.lt_iff_toNat_lt, modulus_toNat] at hx + exact Nat.le_of_not_gt hx + rw [UInt64.toNat_sub_of_le _ _ hmod_le_x, modulus_toNat] + rw [Nat.cast_sub (by + rw [UInt64.le_iff_toNat_le, modulus_toNat] at hmod_le_x + exact hmod_le_x)] + simp + +/-- The raw 128-bit reducer returns a canonical representative below the modulus. -/ +theorem reduceUInt128Raw_lt (lo hi : UInt64) : + (reduceUInt128Raw lo hi).toNat < Goldilocks.fieldSize := by + unfold reduceUInt128Raw + apply reduceUInt64Raw_lt + +/-- Semantic correctness of raw 128-bit Goldilocks reduction. -/ +theorem reduceUInt128Raw_cast (lo hi : UInt64) : + ((reduceUInt128Raw lo hi).toNat : Goldilocks.Field) = + (lo.toNat : Goldilocks.Field) + + (hi.toNat : Goldilocks.Field) * (UInt64.size : Goldilocks.Field) := by + let hi_hi := hi >>> 32 + let hi_lo := hi &&& negModulus + let t0 := if lo < hi_hi then lo - hi_hi - negModulus else lo - hi_hi + let t1 := hi_lo * negModulus + let t2 := if t0 + t1 < t0 then t0 + t1 + negModulus else t0 + t1 + change ((reduceUInt64Raw t2).toNat : Goldilocks.Field) = + (lo.toNat : Goldilocks.Field) + + (hi.toNat : Goldilocks.Field) * (UInt64.size : Goldilocks.Field) + have hred := reduceUInt64Raw_cast t2 + change ((reduceUInt64Raw t2).toNat : Goldilocks.Field) = + (t2.toNat : Goldilocks.Field) at hred + rw [hred] + have hhi_hi_lt : hi_hi.toNat < 2 ^ 32 := by + rw [show hi_hi = hi >>> 32 by rfl, shiftRight32_toNat] + have hhi := UInt64.toNat_lt_size hi + change hi.toNat < 2 ^ 64 at hhi + omega + have hhi_lo_lt : hi_lo.toNat < 2 ^ 32 := by + rw [show hi_lo = hi &&& negModulus by rfl, and_negModulus_toNat] + exact Nat.mod_lt _ (by decide) + have ht0_cast : + (t0.toNat : Goldilocks.Field) = + (lo.toNat : Goldilocks.Field) - (hi_hi.toNat : Goldilocks.Field) := by + rw [show t0 = if lo < hi_hi then lo - hi_hi - negModulus else lo - hi_hi by rfl] + exact subBorrow_cast lo hi_hi hhi_hi_lt + have ht1_cast : + (t1.toNat : Goldilocks.Field) = + (hi_lo.toNat : Goldilocks.Field) * + (negModulus.toNat : Goldilocks.Field) := by + rw [show t1 = hi_lo * negModulus by rfl] + rw [mul_negModulus_toNat_of_lt hi_lo hhi_lo_lt] + rw [Nat.cast_mul] + have ht2_bound : t0.toNat + t1.toNat < 2 * UInt64.size - negModulus.toNat := by + have ht0_lt := UInt64.toNat_lt_size t0 + have ht1_le : t1.toNat ≤ UInt64.size - 2 * negModulus.toNat := by + simpa [t1] using mul_negModulus_toNat_le hi_lo hhi_lo_lt + have htwice_neg_le_size : 2 * negModulus.toNat ≤ UInt64.size := by + decide + omega + have ht2_cast : + (t2.toNat : Goldilocks.Field) = + (t0.toNat : Goldilocks.Field) + (t1.toNat : Goldilocks.Field) := by + rw [show t2 = if t0 + t1 < t0 then t0 + t1 + negModulus else t0 + t1 by rfl] + exact addOverflowBounded_cast t0 t1 ht2_bound + rw [ht2_cast, ht0_cast, ht1_cast] + rw [hi_split_cast hi] + ring + +/-- Product reduction returns a canonical representative below the modulus. -/ +theorem reduceMulRaw_lt (x y : UInt64) : + (reduceMulRaw x y).toNat < Goldilocks.fieldSize := by + unfold reduceMulRaw + apply reduceUInt128Raw_lt + +/-- Semantic correctness of native 64-by-64 product reduction. -/ +theorem reduceMulRaw_cast (x y : UInt64) : + ((reduceMulRaw x y).toNat : Goldilocks.Field) = + (x.toNat : Goldilocks.Field) * (y.toNat : Goldilocks.Field) := by + unfold reduceMulRaw + rw [reduceUInt128Raw_cast] + exact + wideMul_cast x y (wideMul x y).1 (wideMul x y).2 + (by unfold wideMul; rfl) + (by + unfold wideMul + exact wideMul_high_toNat x y _ rfl) + +/-- Addition reduction returns a canonical representative. -/ +theorem reduceAddWithCarryRaw_lt (lo : UInt64) (carry : Bool) + (h : + lo.toNat + (if carry then UInt64.size else 0) < 2 * Goldilocks.fieldSize) : + (reduceAddWithCarryRaw lo carry).toNat < Goldilocks.fieldSize := by + unfold reduceAddWithCarryRaw + cases carry + · simp only [Bool.false_eq_true, if_false] + exact reduceUInt64Raw_lt lo + · simp only [↓reduceIte] at h ⊢ + rw [UInt64.toNat_add] + have hsum_lt_field : lo.toNat + negModulus.toNat < Goldilocks.fieldSize := by + rw [negModulus_toNat, Goldilocks.fieldSize] + rw [Goldilocks.fieldSize, UInt64.size] at h + omega + have hsum_lt_size : lo.toNat + negModulus.toNat < UInt64.size := + Nat.lt_trans hsum_lt_field fieldSize_lt_uint64Size + rw [Nat.mod_eq_of_lt hsum_lt_size] + exact hsum_lt_field + +/-- Semantic correctness of addition reduction with carry. -/ +theorem reduceAddWithCarryRaw_cast (lo : UInt64) (carry : Bool) + (h : + lo.toNat + (if carry then UInt64.size else 0) < 2 * Goldilocks.fieldSize) : + ((reduceAddWithCarryRaw lo carry).toNat : Goldilocks.Field) = + (lo.toNat : Goldilocks.Field) + + (if carry then (UInt64.size : Goldilocks.Field) else 0) := by + unfold reduceAddWithCarryRaw + cases carry + · simp only [Bool.false_eq_true, if_false, add_zero] + exact reduceUInt64Raw_cast lo + · simp only [↓reduceIte] + change lo.toNat + UInt64.size < 2 * Goldilocks.fieldSize at h + rw [UInt64.toNat_add] + have hsum_lt_field : lo.toNat + negModulus.toNat < Goldilocks.fieldSize := by + rw [negModulus_toNat, Goldilocks.fieldSize] + rw [Goldilocks.fieldSize, UInt64.size] at h + omega + have hsum_lt_size : lo.toNat + negModulus.toNat < UInt64.size := + Nat.lt_trans hsum_lt_field fieldSize_lt_uint64Size + rw [Nat.mod_eq_of_lt hsum_lt_size] + rw [Nat.cast_add] + rw [uint64_cast_eq_negModulus] + +/-- The wrapped word and carry produced by adding two canonical representatives is bounded. -/ +theorem addWithCarry_bound (x y : UInt64) + (hx : x.toNat < Goldilocks.fieldSize) + (hy : y.toNat < Goldilocks.fieldSize) : + let lo := x + y + let carry := decide (lo < x) + lo.toNat + (if carry then UInt64.size else 0) < 2 * Goldilocks.fieldSize := by + intro lo carry + by_cases hcarry : carry + · simp only [hcarry] + have hlo_lt_x : lo.toNat < x.toNat := by + simpa [carry, UInt64.lt_iff_toNat_lt] using hcarry + have hsum_ge_size : UInt64.size ≤ x.toNat + y.toNat := by + by_contra hnot + have hsum_lt_size : x.toNat + y.toNat < UInt64.size := + Nat.lt_of_not_ge hnot + have hlo_eq : lo.toNat = x.toNat + y.toNat := by + rw [show lo = x + y by rfl, UInt64.toNat_add] + exact Nat.mod_eq_of_lt hsum_lt_size + omega + have hsum_lt_2size : x.toNat + y.toNat < 2 * UInt64.size := by + nlinarith [UInt64.toNat_lt_size x, UInt64.toNat_lt_size y] + have hlo_eq : lo.toNat = x.toNat + y.toNat - UInt64.size := by + rw [show lo = x + y by rfl, UInt64.toNat_add] + rw [Nat.mod_eq_sub_mod (show x.toNat + y.toNat ≥ UInt64.size by + exact hsum_ge_size)] + rw [Nat.mod_eq_of_lt] + omega + rw [hlo_eq] + change x.toNat + y.toNat - UInt64.size + UInt64.size < + 2 * Goldilocks.fieldSize + have hsum_lt_field : x.toNat + y.toNat < 2 * Goldilocks.fieldSize := by + omega + omega + · simp only [hcarry] + exact uint64_toNat_lt_two_fieldSize lo + +/-- The wrapped word and carry produced by native addition reconstruct the exact Nat sum. -/ +theorem addWithCarry_value (x y : UInt64) : + let lo := x + y + let carry := decide (lo < x) + lo.toNat + (if carry then UInt64.size else 0) = x.toNat + y.toNat := by + intro lo carry + by_cases hcarry : carry + · simp only [hcarry] + have hsum_ge_size : UInt64.size ≤ x.toNat + y.toNat := by + by_contra hnot + have hsum_lt_size : x.toNat + y.toNat < UInt64.size := + Nat.lt_of_not_ge hnot + have hlo_eq : lo.toNat = x.toNat + y.toNat := by + rw [show lo = x + y by rfl, UInt64.toNat_add] + exact Nat.mod_eq_of_lt hsum_lt_size + have hlo_lt_x : lo.toNat < x.toNat := by + simpa [carry, UInt64.lt_iff_toNat_lt] using hcarry + omega + have hsum_lt_2size : x.toNat + y.toNat < 2 * UInt64.size := by + nlinarith [UInt64.toNat_lt_size x, UInt64.toNat_lt_size y] + have hlo_eq : lo.toNat = x.toNat + y.toNat - UInt64.size := by + rw [show lo = x + y by rfl, UInt64.toNat_add] + rw [Nat.mod_eq_sub_mod (show x.toNat + y.toNat ≥ UInt64.size by + exact hsum_ge_size)] + rw [Nat.mod_eq_of_lt] + omega + rw [hlo_eq] + change x.toNat + y.toNat - UInt64.size + UInt64.size = + x.toNat + y.toNat + omega + · simp only [hcarry] + have hnot_lo_lt_x : ¬lo.toNat < x.toNat := by + intro hlo_lt_x + apply hcarry + simpa [carry, UInt64.lt_iff_toNat_lt] using hlo_lt_x + have hsum_lt_size : x.toNat + y.toNat < UInt64.size := by + by_contra hnot + have hsum_ge_size : UInt64.size ≤ x.toNat + y.toNat := + Nat.le_of_not_gt hnot + have hsum_lt_2size : x.toNat + y.toNat < 2 * UInt64.size := by + nlinarith [UInt64.toNat_lt_size x, UInt64.toNat_lt_size y] + have hlo_eq : lo.toNat = x.toNat + y.toNat - UInt64.size := by + rw [show lo = x + y by rfl, UInt64.toNat_add] + rw [Nat.mod_eq_sub_mod (show x.toNat + y.toNat ≥ UInt64.size by + exact hsum_ge_size)] + rw [Nat.mod_eq_of_lt] + omega + have hy_lt_size : y.toNat < UInt64.size := UInt64.toNat_lt_size y + omega + rw [show lo = x + y by rfl, UInt64.toNat_add] + exact Nat.mod_eq_of_lt hsum_lt_size + +/-- Raw negation returns a canonical representative when given one. -/ +theorem negRaw_lt (x : UInt64) (hx : x.toNat < Goldilocks.fieldSize) : + (negRaw x).toNat < Goldilocks.fieldSize := by + unfold negRaw + by_cases hzero : x = 0 + · rw [if_pos hzero] + decide + · rw [if_neg hzero] + have hx_ne_nat : x.toNat ≠ 0 := by + intro hz + apply hzero + apply UInt64.toNat_inj.mp + rw [hz] + decide + have hx_pos : 0 < x.toNat := Nat.pos_of_ne_zero hx_ne_nat + have hx_le_mod : x ≤ modulus := by + rw [UInt64.le_iff_toNat_le, modulus_toNat] + exact Nat.le_of_lt hx + rw [UInt64.toNat_sub_of_le _ _ hx_le_mod, modulus_toNat] + omega + +/-- Raw negation agrees with canonical-field negation. -/ +theorem negRaw_cast (x : UInt64) (hx : x.toNat < Goldilocks.fieldSize) : + ((negRaw x).toNat : Goldilocks.Field) = + -((x.toNat : Goldilocks.Field)) := by + unfold negRaw + by_cases hzero : x = 0 + · rw [if_pos hzero] + have hxNat : x.toNat = 0 := by + simpa using congrArg UInt64.toNat hzero + rw [hxNat] + simp + · rw [if_neg hzero] + have hle : x ≤ modulus := by + rw [UInt64.le_iff_toNat_le, modulus_toNat] + exact Nat.le_of_lt hx + rw [UInt64.toNat_sub_of_le _ _ hle, modulus_toNat] + rw [Nat.cast_sub (by + rw [UInt64.le_iff_toNat_le, modulus_toNat] at hle + exact hle)] + rw [ZMod.natCast_self] + ring + +/-- Raw subtraction returns a canonical representative when given canonical operands. -/ +theorem subRaw_lt (x y : UInt64) + (hx : x.toNat < Goldilocks.fieldSize) + (hy : y.toNat < Goldilocks.fieldSize) : + (subRaw x y).toNat < Goldilocks.fieldSize := by + unfold subRaw + by_cases hxy : y ≤ x + · rw [if_pos hxy] + rw [UInt64.toNat_sub_of_le _ _ hxy] + have hy_le_x : y.toNat ≤ x.toNat := by + simpa [UInt64.le_iff_toNat_le] using hxy + omega + · rw [if_neg hxy] + have hx_lt_y : x.toNat < y.toNat := by + have hnot : ¬y.toNat ≤ x.toNat := by + intro hle + apply hxy + rw [UInt64.le_iff_toNat_le] + exact hle + exact Nat.lt_of_not_ge hnot + have hraw_lt_size : 2 ^ 64 - y.toNat + x.toNat < 2 ^ 64 := by + have hx_lt_size : x.toNat < 2 ^ 64 := by + simpa [UInt64.size] using UInt64.toNat_lt_size x + have hy_lt_size : y.toNat < 2 ^ 64 := by + simpa [UInt64.size] using UInt64.toNat_lt_size y + omega + have hraw_toNat : + (x - y).toNat = 2 ^ 64 - y.toNat + x.toNat := by + rw [UInt64.toNat_sub] + exact Nat.mod_eq_of_lt hraw_lt_size + have hneg_le_raw_nat : negModulus.toNat ≤ (x - y).toNat := by + rw [hraw_toNat, negModulus_toNat] + have hy_lt_field : y.toNat < 2 ^ 64 - 2 ^ 32 + 1 := by + simpa [Goldilocks.fieldSize] using hy + omega + have hneg_le_raw : negModulus ≤ x - y := by + rw [UInt64.le_iff_toNat_le] + exact hneg_le_raw_nat + rw [UInt64.toNat_sub_of_le _ _ hneg_le_raw, hraw_toNat, negModulus_toNat] + change 2 ^ 64 - y.toNat + x.toNat - (2 ^ 32 - 1) < + 2 ^ 64 - 2 ^ 32 + 1 + have hx_lt_field : x.toNat < 2 ^ 64 - 2 ^ 32 + 1 := by + simpa [Goldilocks.fieldSize] using hx + have hy_lt_field : y.toNat < 2 ^ 64 - 2 ^ 32 + 1 := by + simpa [Goldilocks.fieldSize] using hy + omega + +/-- Raw subtraction agrees with canonical-field subtraction for canonical operands. -/ +theorem subRaw_cast (x y : UInt64) + (_hx : x.toNat < Goldilocks.fieldSize) + (hy : y.toNat < Goldilocks.fieldSize) : + ((subRaw x y).toNat : Goldilocks.Field) = + (x.toNat : Goldilocks.Field) - (y.toNat : Goldilocks.Field) := by + unfold subRaw + by_cases hxy : y ≤ x + · rw [if_pos hxy] + rw [UInt64.toNat_sub_of_le _ _ hxy] + rw [Nat.cast_sub (by + rw [UInt64.le_iff_toNat_le] at hxy + exact hxy)] + · rw [if_neg hxy] + have hx_lt_y : x.toNat < y.toNat := by + have hnot : ¬y.toNat ≤ x.toNat := by + intro hle + apply hxy + rw [UInt64.le_iff_toNat_le] + exact hle + exact Nat.lt_of_not_ge hnot + have hraw_lt_size : 2 ^ 64 - y.toNat + x.toNat < 2 ^ 64 := by + have hx_lt_size : x.toNat < 2 ^ 64 := by + simpa [UInt64.size] using UInt64.toNat_lt_size x + have hy_lt_size : y.toNat < 2 ^ 64 := by + simpa [UInt64.size] using UInt64.toNat_lt_size y + omega + have hraw_toNat : + (x - y).toNat = 2 ^ 64 - y.toNat + x.toNat := by + rw [UInt64.toNat_sub] + exact Nat.mod_eq_of_lt hraw_lt_size + have hneg_le_raw_nat : negModulus.toNat ≤ (x - y).toNat := by + rw [hraw_toNat, negModulus_toNat] + have hy_lt_field : y.toNat < 2 ^ 64 - 2 ^ 32 + 1 := by + simpa [Goldilocks.fieldSize] using hy + omega + have hneg_le_raw : negModulus ≤ x - y := by + rw [UInt64.le_iff_toNat_le] + exact hneg_le_raw_nat + rw [UInt64.toNat_sub_of_le _ _ hneg_le_raw, hraw_toNat, negModulus_toNat] + change (((UInt64.size - y.toNat + x.toNat - negModulus.toNat : Nat) : + Goldilocks.Field) = + (x.toNat : Goldilocks.Field) - (y.toNat : Goldilocks.Field)) + have hneg_le_concrete : negModulus.toNat ≤ UInt64.size - y.toNat + x.toNat := by + simpa [UInt64.size, hraw_toNat] using hneg_le_raw_nat + rw [Nat.cast_sub hneg_le_concrete] + rw [Nat.cast_add] + rw [Nat.cast_sub (Nat.le_of_lt (UInt64.toNat_lt_size y))] + rw [uint64_cast_eq_negModulus] + ring + +end Fast +end Goldilocks diff --git a/CompPoly/Fields/Hachi/Ext4.lean b/CompPoly/Fields/Hachi/Ext4.lean index c5423799..ce0f4449 100644 --- a/CompPoly/Fields/Hachi/Ext4.lean +++ b/CompPoly/Fields/Hachi/Ext4.lean @@ -48,7 +48,9 @@ def ext4Params : BinomialParams Field where W := 2 two_le := by norm_num q := fieldSize - card_eq := ZMod.card _ + +instance : Fact (Nat.card Field = ext4Params.q) := + ⟨by rw [Nat.card_eq_fintype_card]; exact ZMod.card _⟩ @[simp] theorem ext4Params_d : ext4Params.d = 4 := rfl @[simp] theorem ext4Params_W : ext4Params.W = 2 := rfl diff --git a/CompPoly/Fields/KoalaBear/Ext4.lean b/CompPoly/Fields/KoalaBear/Ext4.lean index c639fa96..c3825662 100644 --- a/CompPoly/Fields/KoalaBear/Ext4.lean +++ b/CompPoly/Fields/KoalaBear/Ext4.lean @@ -50,7 +50,9 @@ def ext4Params : BinomialParams Field where W := 3 two_le := by norm_num q := fieldSize - card_eq := ZMod.card _ + +instance : Fact (Nat.card Field = ext4Params.q) := + ⟨by rw [Nat.card_eq_fintype_card]; exact ZMod.card _⟩ @[simp] theorem ext4Params_d : ext4Params.d = 4 := rfl @[simp] theorem ext4Params_W : ext4Params.W = 3 := rfl diff --git a/CompPoly/Fields/KoalaBear/Ext5.lean b/CompPoly/Fields/KoalaBear/Ext5.lean index 050f11f2..3981b69c 100644 --- a/CompPoly/Fields/KoalaBear/Ext5.lean +++ b/CompPoly/Fields/KoalaBear/Ext5.lean @@ -39,7 +39,9 @@ def ext5Params : ExtensionParams Field where two_le := by norm_num lower := #v[-1, 0, 1, 0, 0] q := fieldSize - card_eq := ZMod.card _ + +instance : Fact (Nat.card Field = ext5Params.q) := + ⟨by rw [Nat.card_eq_fintype_card]; exact ZMod.card _⟩ @[simp] theorem ext5Params_d : ext5Params.d = 5 := rfl @[simp] theorem ext5Params_q : ext5Params.q = fieldSize := rfl diff --git a/CompPoly/Fields/KoalaBear/Ext5/QuinticCertData.lean b/CompPoly/Fields/KoalaBear/Ext5/QuinticCertData.lean index 40d73bd0..d1bd40af 100644 --- a/CompPoly/Fields/KoalaBear/Ext5/QuinticCertData.lean +++ b/CompPoly/Fields/KoalaBear/Ext5/QuinticCertData.lean @@ -12,9 +12,18 @@ public import CompPoly.Data.Polynomial.RabinCertificate The modulus `f` has little-endian coefficients `[2130706432, 0, 1, 0, 0, 1]`. -GENERATED by `scripts/gen_rabin_certificate.py --p 2130706433 --f '-1,0,1,0,0,1'`. -Do not edit by hand; regenerate instead. Nothing here is trusted — the kernel -re-checks every step through `CompPoly.RabinCert.runChain`. +GENERATED. Do not edit by hand; regenerate with: + +```sh +python3 scripts/gen_rabin_certificate.py --p 2130706433 \ + --f='-1,0,1,0,0,1' \ + --lean --namespace KoalaBear.QuinticCert \ + --authors 'Derek Sorensen' +``` + +Nothing here is trusted — the kernel re-checks every step through +`CompPoly.RabinCert.runChain`, so incorrect data fails to compile rather than +producing a false theorem. -/ @[expose] public section diff --git a/CompPoly/Fields/KoalaBear/Ext5/QuinticIrreducible.lean b/CompPoly/Fields/KoalaBear/Ext5/QuinticIrreducible.lean index b93f5029..68291488 100644 --- a/CompPoly/Fields/KoalaBear/Ext5/QuinticIrreducible.lean +++ b/CompPoly/Fields/KoalaBear/Ext5/QuinticIrreducible.lean @@ -14,10 +14,10 @@ public import Mathlib.Tactic.NormNum.Prime # Irreducibility of `X^5 + X^2 - 1` over KoalaBear The quintic `X^5 + X^2 - 1` is irreducible over `KoalaBear.Field`, proved by Rabin's test at -prime degree (`CompPoly.RabinCert.irreducible_of_rabin_prime_degree`). No degree-5 *binomial* -does the job: `gcd(5, p - 1) = 1`, so `x ↦ x^5` is a bijection on KoalaBear and every `X^5 - W` -has a root. This non-binomial quintic is the defining polynomial of the degree-5 extension in -`CompPoly/Fields/KoalaBear/Ext5.lean`. +prime degree (`CompPoly.RabinCert.irreducible_of_rabin_prime_degree_of_card`). No degree-5 +*binomial* does the job: `gcd(5, p - 1) = 1`, so `x ↦ x^5` is a bijection on KoalaBear and every +`X^5 - W` has a root. This non-binomial quintic is the defining polynomial of the degree-5 +extension in `CompPoly/Fields/KoalaBear/Ext5.lean`. The two Rabin conditions — `f ∣ X^(p^5) - X` and `IsCoprime f (X^p - X)` — are discharged by kernel-checked certificates from `CompPoly/Fields/KoalaBear/Ext5/QuinticCertData.lean` @@ -110,12 +110,13 @@ theorem quintic_bezout_check : kernel-checked certificates for both conditions. -/ theorem quinticPoly_irreducible : Irreducible quinticPoly := by have hcard : Fintype.card Field = fieldSize := ZMod.card _ - refine irreducible_of_rabin_prime_degree (by norm_num) quinticPoly_natDegree ?_ ?_ - · rw [hcard] - exact dvd_X_pow_sub_X_of_runChain toPoly_quinticL quinticPoly_ne_zero + -- Keep the certificate and theorem on the same ZMod instance path during kernel replay. + refine @irreducible_of_rabin_prime_degree_of_card (ZMod fieldSize) + (@ZMod.instField fieldSize instFactPrimeFieldSize) (ZMod.fintype fieldSize) + quinticPoly 5 fieldSize hcard (by norm_num) quinticPoly_natDegree ?_ ?_ + · exact dvd_X_pow_sub_X_of_runChain toPoly_quinticL quinticPoly_ne_zero quintic_trace_chain quintic_trace_exp - · rw [hcard] - exact isCoprime_X_pow_sub_X_of_runChain toPoly_quinticL quinticPoly_ne_zero + · exact isCoprime_X_pow_sub_X_of_runChain toPoly_quinticL quinticPoly_ne_zero quintic_frob_chain quintic_frob_exp quintic_w_check quintic_bezout_check instance : Fact (Irreducible quinticPoly) := ⟨quinticPoly_irreducible⟩ diff --git a/CompPoly/Fields/KoalaBear/Ext6.lean b/CompPoly/Fields/KoalaBear/Ext6.lean index f80429d1..e6090c6a 100644 --- a/CompPoly/Fields/KoalaBear/Ext6.lean +++ b/CompPoly/Fields/KoalaBear/Ext6.lean @@ -52,7 +52,9 @@ def ext6Params : ExtensionParams Field where two_le := by norm_num lower := #v[1, 0, 0, 1, 0, 0] q := fieldSize - card_eq := ZMod.card _ + +instance : Fact (Nat.card Field = ext6Params.q) := + ⟨by rw [Nat.card_eq_fintype_card]; exact ZMod.card _⟩ @[simp] theorem ext6Params_d : ext6Params.d = 6 := rfl @[simp] theorem ext6Params_q : ext6Params.q = fieldSize := rfl diff --git a/CompPoly/Fields/KoalaBear/Ext6/SexticCertData.lean b/CompPoly/Fields/KoalaBear/Ext6/SexticCertData.lean index a85e1867..4ca2651b 100644 --- a/CompPoly/Fields/KoalaBear/Ext6/SexticCertData.lean +++ b/CompPoly/Fields/KoalaBear/Ext6/SexticCertData.lean @@ -16,9 +16,18 @@ The modulus `f` has little-endian coefficients `[1, 0, 0, 1, 0, 0, 1]`. prime factor of `d` (2, 3), at exponents p^3, p^2 respectively. Checking only the linear-factor case would admit a product of equal-degree factors. -GENERATED by `scripts/gen_rabin_certificate.py --p 2130706433 --f '1,0,0,1,0,0,1'`. -Do not edit by hand; regenerate instead. Nothing here is trusted — the kernel -re-checks every step through `CompPoly.RabinCert.runChain`. +GENERATED. Do not edit by hand; regenerate with: + +```sh +python3 scripts/gen_rabin_certificate.py --p 2130706433 \ + --f='1,0,0,1,0,0,1' \ + --lean --namespace KoalaBear.SexticCert \ + --authors 'Derek Sorensen' +``` + +Nothing here is trusted — the kernel re-checks every step through +`CompPoly.RabinCert.runChain`, so incorrect data fails to compile rather than +producing a false theorem. -/ @[expose] public section diff --git a/CompPoly/Fields/KoalaBear/Ext6/SexticIrreducible.lean b/CompPoly/Fields/KoalaBear/Ext6/SexticIrreducible.lean index e566afd9..5e4424bb 100644 --- a/CompPoly/Fields/KoalaBear/Ext6/SexticIrreducible.lean +++ b/CompPoly/Fields/KoalaBear/Ext6/SexticIrreducible.lean @@ -138,15 +138,15 @@ theorem sextic_cop2_bezout_check : factors, with kernel-checked certificates for all three conditions. -/ theorem sexticPoly_irreducible : Irreducible sexticPoly := by have hcard : Fintype.card Field = fieldSize := ZMod.card _ - refine irreducible_of_rabin_degree_six sexticPoly_natDegree ?_ ?_ ?_ - · rw [hcard] - exact dvd_X_pow_sub_X_of_runChain toPoly_sexticL sexticPoly_ne_zero + -- Keep the certificate and theorem on the same ZMod instance path during kernel replay. + refine @irreducible_of_rabin_degree_six_of_card (ZMod fieldSize) + (@ZMod.instField fieldSize instFactPrimeFieldSize) (ZMod.fintype fieldSize) + sexticPoly fieldSize hcard sexticPoly_natDegree ?_ ?_ ?_ + · exact dvd_X_pow_sub_X_of_runChain toPoly_sexticL sexticPoly_ne_zero sextic_trace_chain sextic_trace_exp - · rw [hcard] - exact isCoprime_X_pow_sub_X_of_runChain toPoly_sexticL sexticPoly_ne_zero + · exact isCoprime_X_pow_sub_X_of_runChain toPoly_sexticL sexticPoly_ne_zero sextic_cop3_chain sextic_cop3_exp sextic_cop3_w_check sextic_cop3_bezout_check - · rw [hcard] - exact isCoprime_X_pow_sub_X_of_runChain toPoly_sexticL sexticPoly_ne_zero + · exact isCoprime_X_pow_sub_X_of_runChain toPoly_sexticL sexticPoly_ne_zero sextic_cop2_chain sextic_cop2_exp sextic_cop2_w_check sextic_cop2_bezout_check instance : Fact (Irreducible sexticPoly) := ⟨sexticPoly_irreducible⟩ diff --git a/CompPoly/Fields/Mersenne.lean b/CompPoly/Fields/Mersenne.lean deleted file mode 100644 index fea1c22f..00000000 --- a/CompPoly/Fields/Mersenne.lean +++ /dev/null @@ -1,29 +0,0 @@ -/- -Copyright (c) 2024 ArkLib Contributors. All rights reserved. -Released under Apache 2.0 license as described in the file LICENSE. -Authors: Quang Dao --/ -module - -public import CompPoly.Fields.PrattCertificate - -/-! - # Mersenne prime field `2^{31} - 1` - - This is the field used in Circle STARKs. --/ - -@[expose] public section - -namespace Mersenne31 - -@[reducible] -def fieldSize : Nat := 2 ^ 31 - 1 - -abbrev Field := ZMod fieldSize - -theorem is_prime : Nat.Prime fieldSize := by - unfold fieldSize - pratt - -end Mersenne31 diff --git a/CompPoly/Fields/Mersenne31.lean b/CompPoly/Fields/Mersenne31.lean new file mode 100644 index 00000000..98bf2ee8 --- /dev/null +++ b/CompPoly/Fields/Mersenne31.lean @@ -0,0 +1,20 @@ +/- +Copyright (c) 2024 CompPoly Contributors. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Quang Dao, Varun Thakore +-/ + +module + +public import CompPoly.Fields.Mersenne31.Basic +public import CompPoly.Fields.Mersenne31.Fast + +/-! + # Mersenne31 prime field `2^{31} - 1` + + Facade module for the Mersenne31 field. It re-exports the canonical `ZMod` model + from `CompPoly.Fields.Mersenne31.Basic` and the native-word implementation from + `CompPoly.Fields.Mersenne31.Fast`. +-/ + +@[expose] public section diff --git a/CompPoly/Fields/Mersenne31/Basic.lean b/CompPoly/Fields/Mersenne31/Basic.lean new file mode 100644 index 00000000..56bfffb2 --- /dev/null +++ b/CompPoly/Fields/Mersenne31/Basic.lean @@ -0,0 +1,48 @@ +/- +Copyright (c) 2024 CompPoly Contributors. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Quang Dao, Varun Thakore +-/ + +module + +public import CompPoly.Fields.Basic +public import CompPoly.Fields.PrattCertificate + +/-! + # Mersenne prime field `2^{31} - 1` + + This is the field used in Circle STARKs. +-/ + +@[expose] public section + +namespace Mersenne31 + +/-- The Mersenne31 prime modulus `2^31 - 1`. -/ +@[reducible] +def fieldSize : Nat := 2 ^ 31 - 1 + +/-- The canonical mathematical Mersenne31 field, implemented as integers modulo +`fieldSize`. -/ +abbrev Field := ZMod fieldSize + +/-- The Mersenne31 modulus is prime, verified by a Pratt certificate. -/ +theorem is_prime : Nat.Prime fieldSize := by + unfold fieldSize + pratt + +/-- Register primality of `fieldSize` for Mathlib instances such as `ZMod.instField`. -/ +instance : Fact (Nat.Prime fieldSize) := ⟨is_prime⟩ + +/-- The canonical Mersenne31 carrier is a field because its modulus is prime. -/ +instance : _root_.Field Field := ZMod.instField fieldSize + +/-- Mersenne31 has characteristic different from two. -/ +instance : NonBinaryField Field where + char_neq_2 := by + -- `decide` can discharge this concrete ZMod equality. + simpa [Field, fieldSize] using + (by decide : (2 : ZMod (2 ^ 31 - 1)) ≠ 0) + +end Mersenne31 diff --git a/CompPoly/Fields/Mersenne31/Fast.lean b/CompPoly/Fields/Mersenne31/Fast.lean new file mode 100644 index 00000000..84fc906e --- /dev/null +++ b/CompPoly/Fields/Mersenne31/Fast.lean @@ -0,0 +1,980 @@ +/- +Copyright (c) 2026 CompPoly Contributors. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Varun Thakore +-/ + +module + +public import CompPoly.Fields.Mersenne31.Basic +public import Mathlib.Algebra.Field.TransferInstance +public import Mathlib.Data.Nat.Bitwise +public import Mathlib.FieldTheory.Finite.Basic + +/-! +# FastMersenne31 — UInt32-backed Mersenne-31 field arithmetic + +This module provides a native-word implementation of Mersenne31 arithmetic as a +sidecar to the canonical `Mersenne31.Field := ZMod Mersenne31.fieldSize` +model. + +Fast field values are stored as canonical `UInt32` representatives below +`2^31 - 1`. Multiplication reduces a bounded `UInt64` product using the Mersenne +identity `2^31 ≡ 1 (mod 2^31 - 1)`: split at bit 31, add the halves, then perform +one final subtraction if needed. +-/ + +public section + +namespace Mersenne31 +namespace Fast + +/-- The fast native-word Mersenne31 field carrier, stored as a canonical `UInt32` +representative below the prime modulus. -/ +abbrev Field : Type := { x : UInt32 // x.toNat < Mersenne31.fieldSize } + +/-- Fast representatives have decidable equality through their `UInt32` value. -/ +instance : DecidableEq Field := inferInstance + +/-- The Mersenne31 prime `2^31 - 1` as a native `UInt32`. -/ +@[inline] +def modulus : UInt32 := 0x7FFFFFFF + +/-- The Mersenne31 prime `2^31 - 1` as a native `UInt64`. -/ +@[inline] +private def modulus64 : UInt64 := 0x7FFFFFFF + +/-- The native `UInt32` modulus agrees with the mathematical Mersenne31 modulus. -/ +@[simp] +theorem modulus_toNat : modulus.toNat = Mersenne31.fieldSize := by + decide + +/-- The native `UInt64` modulus agrees with the mathematical Mersenne31 modulus. -/ +@[simp] +private theorem modulus64_toNat : modulus64.toNat = Mersenne31.fieldSize := by + decide + +/-- The Mersenne31 modulus is positive. -/ +private theorem fieldSize_pos : 0 < Mersenne31.fieldSize := by + decide + +/-- The Mersenne31 modulus fits in a `UInt32`. -/ +private theorem fieldSize_lt_uint32Size : Mersenne31.fieldSize < UInt32.size := by + decide + +/-- A product of two canonical Mersenne31 representatives fits in a `UInt64`. -/ +private theorem fieldSize_mul_fieldSize_lt_two64 : + Mersenne31.fieldSize * Mersenne31.fieldSize < 2 ^ 64 := by + decide + +/-- A sum of two canonical Mersenne31 representatives does not overflow `UInt32`. -/ +private theorem fieldSize_add_fieldSize_lt_uint32Size : + Mersenne31.fieldSize + Mersenne31.fieldSize < UInt32.size := by + decide + +/-- The raw native word backing a fast Mersenne31 element. -/ +@[expose, inline] +def raw (x : Field) : UInt32 := x.val + +/-- Reading the raw word from a subtype literal returns its stored word. -/ +@[simp] +theorem raw_mk (x : UInt32) (h : x.toNat < Mersenne31.fieldSize) : + raw ⟨x, h⟩ = x := rfl + +/-- `raw` is the underlying `UInt32` value. -/ +@[simp] +theorem raw_eq_val (x : Field) : raw x = x.val := rfl + +/-- Use the canonical zero representative for standard `0` notation. -/ +instance instZeroField : Zero Field where + zero := ⟨0, by decide⟩ + +/-- Use the canonical one representative for standard `1` notation. -/ +instance instOneField : One Field where + one := ⟨1, by decide⟩ + +/-- Build a fast element from a canonical natural representative. -/ +@[inline] +private def ofCanonicalNat (n : Nat) (h : n < Mersenne31.fieldSize) : Field := + ⟨UInt32.ofNat n, by + have hn : n < UInt32.size := Nat.lt_trans h fieldSize_lt_uint32Size + simpa [UInt32.toNat_ofNat', Nat.mod_eq_of_lt hn] using h⟩ + +/-- Convert a natural number into fast representation by reducing modulo Mersenne31. -/ +@[inline] +def ofNat (n : Nat) : Field := + ofCanonicalNat (n % Mersenne31.fieldSize) (Nat.mod_lt _ fieldSize_pos) + +/-- Convert a fast Mersenne31 element to its canonical natural representative. -/ +@[expose, inline] +def toNat (x : Field) : Nat := + x.val.toNat + +/-- Reading the natural representative from a subtype literal returns the stored word's +natural value. -/ +@[simp] +theorem toNat_mk (x : UInt32) (h : x.toNat < Mersenne31.fieldSize) : + toNat ⟨x, h⟩ = x.toNat := rfl + +/-- `toNat` is the natural value of the underlying `UInt32` word. -/ +@[simp] +theorem toNat_eq_val_toNat (x : Field) : toNat x = x.val.toNat := rfl + +/-- Convert from the canonical `ZMod` Mersenne31 field into fast representation. -/ +@[inline] +def ofField (x : Mersenne31.Field) : Field := + ofCanonicalNat x.val (ZMod.val_lt x) + +/-- Convert a fast Mersenne31 element to the canonical `ZMod` field. -/ +@[inline] +def toField (x : Field) : Mersenne31.Field := + (toNat x : Mersenne31.Field) + +/-- Convert a 32-bit word into fast representation. -/ +@[inline] +def ofUInt32 (x : UInt32) : Field := + ofNat x.toNat + +/-- Convert an integer into fast representation. -/ +@[inline] +def ofInt (z : Int) : Field := + ofField (z : Mersenne31.Field) + +/-- Use `ofNat` for natural-number casts into fast Mersenne31. -/ +instance instNatCastField : NatCast Field where + natCast := ofNat + +/-- Interpret integer casts through the canonical Mersenne31 field. -/ +instance instIntCastField : IntCast Field where + intCast := ofInt + +/-- Interpret nonnegative rational casts through the canonical Mersenne31 field. -/ +instance instNNRatCastField : NNRatCast Field where + nnratCast q := ofField (q : Mersenne31.Field) + +/-- Interpret rational casts through the canonical Mersenne31 field. -/ +instance instRatCastField : RatCast Field where + ratCast q := ofField (q : Mersenne31.Field) + +/-- The raw word backing fast zero is zero. -/ +@[simp] +theorem raw_zero : raw (0 : Field) = 0 := rfl + +/-- The raw word backing fast one is one. -/ +@[simp] +theorem raw_one : raw (1 : Field) = 1 := rfl + +/-- The natural representative of fast zero is zero. -/ +@[simp] +theorem toNat_zero : toNat (0 : Field) = 0 := rfl + +/-- The natural representative of fast one is one. -/ +@[simp] +theorem toNat_one : toNat (1 : Field) = 1 := rfl + +/-- Convert a canonical natural representative to fast form and read it back unchanged. -/ +@[simp] +private theorem toNat_ofCanonicalNat (n : Nat) (h : n < Mersenne31.fieldSize) : + toNat (ofCanonicalNat n h) = n := by + unfold toNat ofCanonicalNat + have hn : n < UInt32.size := Nat.lt_trans h fieldSize_lt_uint32Size + simp [UInt32.toNat_ofNat', Nat.mod_eq_of_lt hn] + +/-- Converting a canonical natural representative to fast form agrees with the same +natural cast in the canonical `ZMod` field. -/ +@[simp] +private theorem toField_ofCanonicalNat (n : Nat) (h : n < Mersenne31.fieldSize) : + toField (ofCanonicalNat n h) = (n : Mersenne31.Field) := by + unfold toField + rw [toNat_ofCanonicalNat] + +/-- Converting a natural number to fast form and reading it back gives its canonical +representative modulo Mersenne31. -/ +@[simp] +theorem toNat_ofNat (n : Nat) : + toNat (ofNat n) = n % Mersenne31.fieldSize := by + unfold ofNat + rw [toNat_ofCanonicalNat] + +/-- Converting a natural number to fast form agrees with the same natural cast in the +canonical field. -/ +@[simp] +theorem toField_ofNat (n : Nat) : + toField (ofNat n) = (n : Mersenne31.Field) := by + unfold ofNat + rw [toField_ofCanonicalNat] + rw [← ZMod.natCast_zmod_val (n : Mersenne31.Field)] + rw [ZMod.val_natCast] + +/-- Converting a `UInt32` to fast form agrees with casting its natural value into the +canonical field. -/ +@[simp] +theorem toField_ofUInt32 (x : UInt32) : + toField (ofUInt32 x) = (x.toNat : Mersenne31.Field) := by + unfold ofUInt32 + rw [toField_ofNat] + +/-- Converting a `UInt32` to fast form and reading it back gives its value modulo +Mersenne31. -/ +@[simp] +theorem toNat_ofUInt32 (x : UInt32) : + toNat (ofUInt32 x) = x.toNat % Mersenne31.fieldSize := by + unfold ofUInt32 + rw [toNat_ofNat] + +/-- Converting from the canonical field to fast form and reading the natural +representative returns the canonical `ZMod` representative. -/ +@[simp] +theorem toNat_ofField (x : Mersenne31.Field) : toNat (ofField x) = x.val := by + unfold ofField + rw [toNat_ofCanonicalNat] + +/-- Converting from the canonical `ZMod` field to fast form and back is the identity. -/ +@[simp] +theorem toField_ofField (x : Mersenne31.Field) : + toField (ofField x) = x := by + unfold ofField + rw [toField_ofCanonicalNat] + exact ZMod.natCast_zmod_val x + +/-- Converting an integer to fast form agrees with casting it into the canonical field. -/ +@[simp] +theorem toField_ofInt (z : Int) : + toField (ofInt z) = (z : Mersenne31.Field) := by + unfold ofInt + rw [toField_ofField] + +/-- Converting from fast form to the canonical `ZMod` field and back is the identity. -/ +@[simp] +theorem ofField_toField (x : Field) : + ofField (toField x) = x := by + apply Subtype.ext + apply UInt32.toNat_inj.mp + change toNat (ofField (toField x)) = toNat x + unfold ofField toField + rw [toNat_ofCanonicalNat] + exact ZMod.val_natCast_of_lt x.property + +/-- The canonical-field projection is injective because `ofField` is its left inverse. -/ +private theorem toField_injective : Function.Injective toField := + Function.LeftInverse.injective ofField_toField + +/-- The fast zero maps to zero in the canonical field. -/ +@[simp] +theorem toField_zero : toField (0 : Field) = 0 := by + decide + +/-- The fast one maps to one in the canonical field. -/ +@[simp] +theorem toField_one : toField (1 : Field) = 1 := by + decide + +/-- Natural casts in the fast field agree with natural casts in the canonical field. -/ +@[simp] +theorem toField_natCast (n : Nat) : + toField (n : Field) = (n : Mersenne31.Field) := by + change toField (ofNat n) = (n : Mersenne31.Field) + unfold ofNat + rw [toField_ofCanonicalNat] + rw [← ZMod.natCast_zmod_val (n : Mersenne31.Field)] + rw [ZMod.val_natCast] + +/-- Integer casts in the fast field agree with integer casts in the canonical field. -/ +@[simp] +theorem toField_intCast (n : Int) : + toField (n : Field) = (n : Mersenne31.Field) := by + change toField (ofInt n) = (n : Mersenne31.Field) + rw [toField_ofInt] + +/-- Nonnegative rational casts in the fast field agree with canonical-field casts. -/ +@[simp] +theorem toField_nnratCast (q : ℚ≥0) : + toField (q : Field) = (q : Mersenne31.Field) := by + change toField (ofField (q : Mersenne31.Field)) = (q : Mersenne31.Field) + rw [toField_ofField] + +/-- Rational casts in the fast field agree with canonical-field casts. -/ +@[simp] +theorem toField_ratCast (q : ℚ) : + toField (q : Field) = (q : Mersenne31.Field) := by + change toField (ofField (q : Mersenne31.Field)) = (q : Mersenne31.Field) + rw [toField_ofField] + +/-- Raw one-step reduction for a `UInt32` known to represent a value below `2 * p`. + +If the word is already below `p`, keep it; otherwise subtract `p`. -/ +@[inline] +private def reduceUInt32Lt2ModulusRaw (x : UInt32) : UInt32 := + if x < modulus then x else x - modulus + +/-- The raw one-step reducer returns a canonical representative below the modulus. -/ +private theorem reduceUInt32Lt2ModulusRaw_lt (x : UInt32) + (h : x.toNat < 2 * Mersenne31.fieldSize) : + (reduceUInt32Lt2ModulusRaw x).toNat < Mersenne31.fieldSize := by + unfold reduceUInt32Lt2ModulusRaw + by_cases hx : x < modulus + · rw [if_pos hx] + rw [UInt32.lt_iff_toNat_lt, modulus_toNat] at hx + exact hx + · rw [if_neg hx] + have hmod_le_x : modulus ≤ x := by + rw [UInt32.le_iff_toNat_le, modulus_toNat] + rw [UInt32.lt_iff_toNat_lt, modulus_toNat] at hx + exact Nat.le_of_not_gt hx + rw [UInt32.toNat_sub_of_le _ _ hmod_le_x, modulus_toNat] + omega + +/-- Reduce a `UInt32` known to represent a value below `2 * p` into fast field form. -/ +@[inline] +private def reduceUInt32Lt2Modulus (x : UInt32) + (h : x.toNat < 2 * Mersenne31.fieldSize) : Field := + ⟨reduceUInt32Lt2ModulusRaw x, reduceUInt32Lt2ModulusRaw_lt x h⟩ + +/-- One-step `UInt32` reduction preserves the represented canonical field element. + +This is the semantic correctness lemma for `reduceUInt32Lt2Modulus`: subtracting the +Mersenne31 modulus when needed changes the native representative, but not its value +in `ZMod fieldSize`. -/ +private theorem reduceUInt32Lt2Modulus_cast (x : UInt32) + (h : x.toNat < 2 * Mersenne31.fieldSize) : + ((reduceUInt32Lt2Modulus x h).val.toNat : Mersenne31.Field) = + (x.toNat : Mersenne31.Field) := by + change ((reduceUInt32Lt2ModulusRaw x).toNat : Mersenne31.Field) = + (x.toNat : Mersenne31.Field) + unfold reduceUInt32Lt2ModulusRaw + by_cases hx : x < modulus + · rw [if_pos hx] + · rw [if_neg hx] + have hmod_le_x : modulus ≤ x := by + rw [UInt32.le_iff_toNat_le, modulus_toNat] + rw [UInt32.lt_iff_toNat_lt, modulus_toNat] at hx + exact Nat.le_of_not_gt hx + rw [UInt32.toNat_sub_of_le _ _ hmod_le_x, modulus_toNat] + rw [Nat.cast_sub (by + rw [UInt32.le_iff_toNat_le, modulus_toNat] at hmod_le_x + exact hmod_le_x)] + simp + +/-- Fast modular addition. -/ +@[inline] +def add (x y : Field) : Field := + reduceUInt32Lt2Modulus (x.val + y.val) (by + rw [UInt32.toNat_add] + have hsum_lt : x.val.toNat + y.val.toNat < UInt32.size := by + nlinarith [x.property, y.property, fieldSize_add_fieldSize_lt_uint32Size] + rw [Nat.mod_eq_of_lt hsum_lt] + nlinarith [x.property, y.property]) + +/-- Use fast modular addition for the standard `+` notation on Mersenne31 fast elements. -/ +instance instAddField : Add Field where + add := add + +/-- Fast addition agrees with addition in the canonical `ZMod` Mersenne31 field. -/ +@[simp] +theorem toField_add (x y : Field) : + toField (x + y) = toField x + toField y := by + unfold instAddField add toField toNat + have hbound : (x.val + y.val).toNat < 2 * Mersenne31.fieldSize := by + rw [UInt32.toNat_add] + have hsum_lt : x.val.toNat + y.val.toNat < UInt32.size := by + nlinarith [x.property, y.property, fieldSize_add_fieldSize_lt_uint32Size] + rw [Nat.mod_eq_of_lt hsum_lt] + nlinarith [x.property, y.property] + have hred := reduceUInt32Lt2Modulus_cast (x.val + y.val) hbound + change ((reduceUInt32Lt2ModulusRaw (x.val + y.val)).toNat : + Mersenne31.Field) = + ((x.val + y.val).toNat : Mersenne31.Field) at hred + change ((reduceUInt32Lt2ModulusRaw (x.val + y.val)).toNat : + Mersenne31.Field) = + (x.val.toNat : Mersenne31.Field) + (y.val.toNat : Mersenne31.Field) + rw [hred] + rw [UInt32.toNat_add] + have hsum_lt : x.val.toNat + y.val.toNat < UInt32.size := by + nlinarith [x.property, y.property, fieldSize_add_fieldSize_lt_uint32Size] + rw [Nat.mod_eq_of_lt hsum_lt] + rw [Nat.cast_add] + +/-- Fast modular subtraction, using `x - y` when no underflow occurs and +`x + p - y` otherwise. -/ +@[inline] +def sub (x y : Field) : Field := + if hxy : x.val ≥ y.val then + ⟨x.val - y.val, by + have hy_le_x : y.val.toNat ≤ x.val.toNat := by + rw [← UInt32.le_iff_toNat_le] + exact hxy + rw [UInt32.toNat_sub_of_le _ _ hxy] + omega⟩ + else + ⟨x.val + modulus - y.val, by + have hsum_lt : x.val.toNat + Mersenne31.fieldSize < UInt32.size := by + nlinarith [x.property, fieldSize_add_fieldSize_lt_uint32Size] + have hsum_eq : + (x.val + modulus).toNat = x.val.toNat + Mersenne31.fieldSize := by + rw [UInt32.toNat_add, modulus_toNat, Nat.mod_eq_of_lt hsum_lt] + have hy_le_sum : y.val ≤ x.val + modulus := by + rw [UInt32.le_iff_toNat_le, hsum_eq] + omega + have hx_lt_y : x.val.toNat < y.val.toNat := by + have hx_not_ge : ¬y.val.toNat ≤ x.val.toNat := by + intro hle + apply hxy + rw [ge_iff_le, UInt32.le_iff_toNat_le] + exact hle + omega + rw [UInt32.toNat_sub_of_le _ _ hy_le_sum, hsum_eq] + omega⟩ + +/-- Use fast modular subtraction for the standard `-` notation on Mersenne31 fast elements. -/ +instance instSubField : Sub Field where + sub := sub + +/-- Fast subtraction agrees with subtraction in the canonical `ZMod` Mersenne31 field. -/ +@[simp] +theorem toField_sub (x y : Field) : toField (x - y) = toField x - toField y := by + change (((sub x y).val.toNat : Mersenne31.Field) = + (x.val.toNat : Mersenne31.Field) - (y.val.toNat : Mersenne31.Field)) + unfold sub + by_cases hxy : x.val ≥ y.val + · rw [dif_pos hxy] + rw [UInt32.toNat_sub_of_le _ _ hxy] + rw [Nat.cast_sub (by + rw [ge_iff_le, UInt32.le_iff_toNat_le] at hxy + exact hxy)] + · rw [dif_neg hxy] + have hsum_lt : x.val.toNat + Mersenne31.fieldSize < UInt32.size := by + nlinarith [x.property, fieldSize_add_fieldSize_lt_uint32Size] + have hsum_eq : + (x.val + modulus).toNat = x.val.toNat + Mersenne31.fieldSize := by + rw [UInt32.toNat_add, modulus_toNat, Nat.mod_eq_of_lt hsum_lt] + have hy_le_sum : y.val ≤ x.val + modulus := by + rw [UInt32.le_iff_toNat_le, hsum_eq] + omega + rw [UInt32.toNat_sub_of_le _ _ hy_le_sum, hsum_eq] + rw [Nat.cast_sub (by + rw [UInt32.le_iff_toNat_le, hsum_eq] at hy_le_sum + exact hy_le_sum)] + rw [Nat.cast_add] + simp + +/-- Fast modular negation. Zero remains zero; nonzero `x` maps to `p - x`. -/ +@[inline] +def neg (x : Field) : Field := + if hx : x.val = 0 then + 0 + else + ⟨modulus - x.val, by + have hle : x.val ≤ modulus := by + rw [UInt32.le_iff_toNat_le, modulus_toNat] + exact Nat.le_of_lt x.property + have hxpos : 0 < x.val.toNat := by + apply Nat.pos_of_ne_zero + intro hzero + apply hx + apply UInt32.toNat_inj.mp + simpa using hzero + rw [UInt32.toNat_sub_of_le _ _ hle, modulus_toNat] + omega⟩ + +/-- Use fast modular negation for the standard unary `-` notation on Mersenne31 fast elements. -/ +instance instNegField : Neg Field where + neg := neg + +/-- Fast negation agrees with negation in the canonical `ZMod` Mersenne31 field. -/ +@[simp] +theorem toField_neg (x : Field) : toField (-x) = -toField x := by + change (((neg x).val.toNat : Mersenne31.Field) = + -(x.val.toNat : Mersenne31.Field)) + unfold neg + by_cases hx : x.val = 0 + · rw [dif_pos hx] + have hxNat : x.val.toNat = 0 := by + simpa using congrArg UInt32.toNat hx + rw [hxNat] + change ((toNat (0 : Field) : Mersenne31.Field)) = 0 + rw [toNat_zero] + simp + · rw [dif_neg hx] + have hle : x.val ≤ modulus := by + rw [UInt32.le_iff_toNat_le, modulus_toNat] + exact Nat.le_of_lt x.property + rw [UInt32.toNat_sub_of_le _ _ hle, modulus_toNat] + rw [Nat.cast_sub (by + rw [UInt32.le_iff_toNat_le, modulus_toNat] at hle + exact hle)] + rw [ZMod.natCast_self] + simp + +/-- Raw Mersenne reduction for bounded `UInt64` inputs below `p * p`. + +This splits the native word at bit 31 and uses `2^31 ≡ 1 mod p`. For inputs +below `p * p`, the split sum is below `2 * p`, so one `UInt32` reduction +canonicalizes it. -/ +@[inline] +private def reduceUInt64Raw (x : UInt64) : UInt32 := + let lo := x &&& modulus64 + let hi := x >>> 31 + let s := (lo + hi).toUInt32 + reduceUInt32Lt2ModulusRaw s + +/-- The native bit split used by `reduceUInt64Raw` agrees with the Nat-level +Mersenne split. -/ +private theorem reduceUInt64Raw_split_toNat (x : UInt64) + (hsum_lt_uint32 : x.toNat % 2 ^ 31 + x.toNat / 2 ^ 31 < 2 ^ 32) : + ((x &&& modulus64) + (x >>> 31)).toUInt32.toNat = + x.toNat % 2 ^ 31 + x.toNat / 2 ^ 31 := by + simp only [UInt64.toNat_toUInt32, UInt64.toNat_add, UInt64.toNat_and, + UInt64.toNat_shiftRight, modulus64_toNat, Nat.shiftRight_eq_div_pow] + rw [show Mersenne31.fieldSize = 2 ^ 31 - 1 by decide] + rw [show UInt64.toNat 31 % 64 = 31 by decide] + rw [Nat.and_two_pow_sub_one_eq_mod] + rw [Nat.mod_eq_of_lt (Nat.lt_trans hsum_lt_uint32 (by decide : 2 ^ 32 < 2 ^ 64))] + rw [Nat.mod_eq_of_lt hsum_lt_uint32] + +/-- For inputs below `p * p`, the Mersenne split sum is both below `2 * p` and small +enough to fit in a `UInt32`. -/ +private theorem reduceUInt64_split_sum_bounds (x : UInt64) + (h : x.toNat < Mersenne31.fieldSize * Mersenne31.fieldSize) : + x.toNat % 2 ^ 31 + x.toNat / 2 ^ 31 < 2 * Mersenne31.fieldSize ∧ + x.toNat % 2 ^ 31 + x.toNat / 2 ^ 31 < 2 ^ 32 := by + have hlow_le : x.toNat % 2 ^ 31 ≤ Mersenne31.fieldSize := by + have hlow_lt : x.toNat % 2 ^ 31 < 2 ^ 31 := Nat.mod_lt _ (by decide) + have hfield : Mersenne31.fieldSize = 2 ^ 31 - 1 := by + decide + omega + have hhi_lt : x.toNat / 2 ^ 31 < Mersenne31.fieldSize := by + rw [Nat.div_lt_iff_lt_mul] + · have hfield_lt_pow31 : Mersenne31.fieldSize < 2 ^ 31 := by + decide + nlinarith [h, hfield_lt_pow31, fieldSize_pos] + · decide + have hsum_lt_two_fieldSize : + x.toNat % 2 ^ 31 + x.toNat / 2 ^ 31 < 2 * Mersenne31.fieldSize := by + have hsum := Nat.add_lt_add_of_le_of_lt hlow_le hhi_lt + nlinarith + have htwo_lt_uint32 : 2 * Mersenne31.fieldSize < 2 ^ 32 := by + simpa [UInt32.size, two_mul] using fieldSize_add_fieldSize_lt_uint32Size + exact ⟨hsum_lt_two_fieldSize, Nat.lt_trans hsum_lt_two_fieldSize htwo_lt_uint32⟩ + +/-- The bounded `UInt64` Mersenne reducer returns a canonical representative below `p`. -/ +private theorem reduceUInt64Raw_lt (x : UInt64) + (h : x.toNat < Mersenne31.fieldSize * Mersenne31.fieldSize) : + (reduceUInt64Raw x).toNat < Mersenne31.fieldSize := by + unfold reduceUInt64Raw + apply reduceUInt32Lt2ModulusRaw_lt + obtain ⟨hsum_lt_two_fieldSize, hsum_lt_uint32⟩ := reduceUInt64_split_sum_bounds x h + rw [reduceUInt64Raw_split_toNat x hsum_lt_uint32] + exact hsum_lt_two_fieldSize + +/-- Reduce a bounded `UInt64` modulo Mersenne31. -/ +@[inline] +private def reduceUInt64 (x : UInt64) + (h : x.toNat < Mersenne31.fieldSize * Mersenne31.fieldSize) : Field := + ⟨reduceUInt64Raw x, reduceUInt64Raw_lt x h⟩ + +/-- Bounded `UInt64` reduction preserves the represented canonical field element. -/ +private theorem reduceUInt64_cast (x : UInt64) + (h : x.toNat < Mersenne31.fieldSize * Mersenne31.fieldSize) : + ((reduceUInt64 x h).val.toNat : Mersenne31.Field) = + (x.toNat : Mersenne31.Field) := by + change ((reduceUInt64Raw x).toNat : Mersenne31.Field) = + (x.toNat : Mersenne31.Field) + unfold reduceUInt64Raw + obtain ⟨hsum_lt_two_fieldSize, hsum_lt_uint32⟩ := reduceUInt64_split_sum_bounds x h + have hword : + ((x &&& modulus64) + (x >>> 31)).toUInt32.toNat = + x.toNat % 2 ^ 31 + x.toNat / 2 ^ 31 := + reduceUInt64Raw_split_toNat x hsum_lt_uint32 + have hred := reduceUInt32Lt2Modulus_cast + ((x &&& modulus64) + (x >>> 31)).toUInt32 (by + rw [hword] + exact hsum_lt_two_fieldSize) + change ((reduceUInt32Lt2ModulusRaw + ((x &&& modulus64) + (x >>> 31)).toUInt32).toNat : + Mersenne31.Field) = + (((x &&& modulus64) + (x >>> 31)).toUInt32.toNat : + Mersenne31.Field) at hred + rw [hred, hword] + conv_rhs => rw [← Nat.mod_add_div x.toNat (2 ^ 31)] + rw [Nat.cast_add, Nat.cast_add, Nat.cast_mul] + have hpow : ((2 ^ 31 : Nat) : Mersenne31.Field) = 1 := by + decide + rw [hpow, one_mul, add_comm] + +/-- Interpreting a bounded `UInt64` reduction in the canonical field gives the original +`UInt64` value modulo `p`. -/ +@[simp] +private theorem toField_reduceUInt64 (x : UInt64) + (h : x.toNat < Mersenne31.fieldSize * Mersenne31.fieldSize) : + toField (reduceUInt64 x h) = (x.toNat : Mersenne31.Field) := by + exact reduceUInt64_cast x h + +/-- Fast modular multiplication. -/ +@[inline] +def mul (x y : Field) : Field := + reduceUInt64 (x.val.toUInt64 * y.val.toUInt64) (by + simp only [UInt64.toNat_mul, UInt32.toNat_toUInt64] + have hprod : x.val.toNat * y.val.toNat < 2 ^ 64 := by + nlinarith [x.property, y.property, fieldSize_mul_fieldSize_lt_two64] + rw [Nat.mod_eq_of_lt hprod] + nlinarith [x.property, y.property]) + +/-- Use fast modular multiplication for the standard `*` notation on Mersenne31 fast +elements. -/ +instance instMulField : Mul Field where + mul := mul + +/-- Natural scalar multiplication is multiplication by the corresponding fast natural cast. -/ +instance instNatSMulField : SMul Nat Field where + smul n x := (n : Field) * x + +/-- Integer scalar multiplication is multiplication by the corresponding fast integer cast. -/ +instance instIntSMulField : SMul Int Field where + smul n x := (n : Field) * x + +/-- Nonnegative rational scalar multiplication is transported through the canonical field. -/ +instance instNNRatSMulField : SMul ℚ≥0 Field where + smul q x := ofField (q • toField x) + +/-- Rational scalar multiplication is transported through the canonical field. -/ +instance instRatSMulField : SMul ℚ Field where + smul q x := ofField (q • toField x) + +/-- Fast multiplication agrees with multiplication in the canonical `ZMod` Mersenne31 +field. -/ +@[simp] +theorem toField_mul (x y : Field) : toField (x * y) = toField x * toField y := by + change toField (mul x y) = toField x * toField y + unfold mul + rw [toField_reduceUInt64] + simp only [UInt64.toNat_mul, UInt32.toNat_toUInt64] + have hprod : x.val.toNat * y.val.toNat < 2 ^ 64 := by + nlinarith [x.property, y.property, fieldSize_mul_fieldSize_lt_two64] + rw [Nat.mod_eq_of_lt hprod] + rw [Nat.cast_mul] + rfl + +/-- Fast squaring. -/ +@[inline] +def square (x : Field) : Field := + mul x x + +/-- Fast squaring agrees with multiplying the canonical field value by itself. -/ +@[simp] +theorem toField_square (x : Field) : toField (square x) = toField x * toField x := by + unfold square + exact toField_mul x x + +/-- Exponentiation over the fast representation using binary exponentiation. -/ +@[inline] +def pow (x : Field) (n : Nat) : Field := + @npowBinRec Field ⟨(1 : Field)⟩ ⟨mul⟩ n x + +/-- Use fast binary exponentiation for natural powers. -/ +instance instPowFieldNat : Pow Field Nat where + pow := pow + +/-- Fast multiplication is associative because it agrees with canonical-field +multiplication. -/ +private theorem mul_assoc_field (x y z : Field) : (x * y) * z = x * (y * z) := by + apply toField_injective + rw [toField_mul, toField_mul, toField_mul, toField_mul] + ring + +/-- Successor rule for the local binary exponentiation routine. -/ +private theorem pow_succ (x : Field) (n : Nat) : pow x (n + 1) = pow x n * x := by + unfold pow + let : Semigroup Field := { + mul := (· * ·) + mul_assoc := mul_assoc_field + } + exact npowBinRec_succ n x + +/-- Fast natural-power computation agrees with powers in the canonical field. -/ +@[simp] +theorem toField_pow (x : Field) (n : Nat) : toField (pow x n) = toField x ^ n := by + induction n with + | zero => + unfold pow + rw [npowBinRec_zero] + rw [toField_one] + simp + | succ n ih => + rw [pow_succ, toField_mul, ih, _root_.pow_succ] + +/-- The concrete Fermat exponent `p - 2` used for inversion in the Mersenne31 prime field. + +For a nonzero field element `x` over a prime field of size `p`, Fermat's little +theorem gives `x^(p - 1) = 1`, so `x^(p - 2)` is the multiplicative inverse of +`x`. -/ +@[inline] +private def invExponent : Nat := Mersenne31.fieldSize - 2 + +/-- Four squarings followed by multiplication by the next 4-bit exponent digit. -/ +@[inline] +private def shift4Mul (acc digit : Field) : Field := + mul (square (square (square (square acc)))) digit + +/-- Fast modular inversion using a fixed Fermat addition chain for `x^(p - 2)`. + +The exponent is `p - 2 = 2147483645 = 0x7FFFFFFD`. The chain builds the +hex digits `7 F F F F F F D` by repeatedly applying `shift4Mul`, which raises +the accumulator to the 16th power and multiplies by the next 4-bit digit. + +This sends zero to zero, matching Lean's field inverse convention. -/ +@[inline] +def inv (x : Field) : Field := + let x2 := square x -- x^2 + let x3 := mul x2 x -- x^3 + let x6 := square x3 -- x^6 + let x7 := mul x6 x -- x^7 + let x13 := mul x7 x6 -- x^13 + let x14 := square x7 -- x^14 + let x15 := mul x14 x -- x^15 + let acc := shift4Mul x7 x15 -- x^127 + let acc := shift4Mul acc x15 -- x^2047 + let acc := shift4Mul acc x15 -- x^32767 + let acc := shift4Mul acc x15 -- x^524287 + let acc := shift4Mul acc x15 -- x^8388607 + let acc := shift4Mul acc x15 -- x^134217727 + shift4Mul acc x13 -- x^2147483645 + +/-- Use Fermat-based fast inversion for the standard inverse notation. -/ +instance instInvField : Inv Field where + inv := inv + +/-- Multiplication of two fast values adds their tracked exponents after projection to +the canonical field. -/ +private theorem toField_mul_pow (base x y : Field) (m n : Nat) + (hx : toField x = toField base ^ m) (hy : toField y = toField base ^ n) : + toField (mul x y) = toField base ^ (m + n) := by + change toField (x * y) = toField base ^ (m + n) + rw [toField_mul, hx, hy, ← pow_add] + +/-- `shift4Mul acc digit` projects to `acc^16 * digit` in the canonical field. -/ +private theorem toField_shift4Mul (acc digit : Field) : + toField (shift4Mul acc digit) = toField acc ^ 16 * toField digit := by + unfold shift4Mul + change toField (square (square (square (square acc))) * digit) = + toField acc ^ 16 * toField digit + rw [toField_mul] + repeat rw [toField_square] + ring + +/-- Exponent-tracking form of `toField_shift4Mul`: shift by one hexadecimal digit and +add the next digit exponent. -/ +private theorem toField_shift4Mul_pow (base acc digit : Field) (e d : Nat) + (hacc : toField acc = toField base ^ e) (hdigit : toField digit = toField base ^ d) : + toField (shift4Mul acc digit) = toField base ^ (16 * e + d) := by + rw [toField_shift4Mul, hacc, hdigit] + rw [← pow_mul, ← pow_add] + congr 1 + omega + +/-- The fixed inversion chain computes exactly the Fermat exponent `p - 2`. -/ +private theorem toField_inv_pow (x : Field) : + toField (inv x) = toField x ^ invExponent := by + unfold inv + let x2 := square x + let x3 := mul x2 x + let x6 := square x3 + let x7 := mul x6 x + let x13 := mul x7 x6 + let x14 := square x7 + let x15 := mul x14 x + let acc1 := shift4Mul x7 x15 + let acc2 := shift4Mul acc1 x15 + let acc3 := shift4Mul acc2 x15 + let acc4 := shift4Mul acc3 x15 + let acc5 := shift4Mul acc4 x15 + let acc6 := shift4Mul acc5 x15 + have hx1 : toField x = toField x ^ 1 := by simp + have hx2 : toField x2 = toField x ^ 2 := by + dsimp [x2] + rw [toField_square, pow_two] + have hx3 : toField x3 = toField x ^ 3 := by + dsimp [x3] + simpa using toField_mul_pow x x2 x 2 1 hx2 hx1 + have hx6 : toField x6 = toField x ^ 6 := by + dsimp [x6] + rw [toField_square, hx3, ← pow_add] + have hx7 : toField x7 = toField x ^ 7 := by + dsimp [x7] + simpa using toField_mul_pow x x6 x 6 1 hx6 hx1 + have hx13 : toField x13 = toField x ^ 13 := by + dsimp [x13] + simpa using toField_mul_pow x x7 x6 7 6 hx7 hx6 + have hx14 : toField x14 = toField x ^ 14 := by + dsimp [x14] + rw [toField_square, hx7, ← pow_add] + have hx15 : toField x15 = toField x ^ 15 := by + dsimp [x15] + simpa using toField_mul_pow x x14 x 14 1 hx14 hx1 + have hacc1 : toField acc1 = toField x ^ 127 := by + dsimp [acc1] + simpa using toField_shift4Mul_pow x x7 x15 7 15 hx7 hx15 + have hacc2 : toField acc2 = toField x ^ 2047 := by + dsimp [acc2] + simpa using toField_shift4Mul_pow x acc1 x15 127 15 hacc1 hx15 + have hacc3 : toField acc3 = toField x ^ 32767 := by + dsimp [acc3] + simpa using toField_shift4Mul_pow x acc2 x15 2047 15 hacc2 hx15 + have hacc4 : toField acc4 = toField x ^ 524287 := by + dsimp [acc4] + simpa using toField_shift4Mul_pow x acc3 x15 32767 15 hacc3 hx15 + have hacc5 : toField acc5 = toField x ^ 8388607 := by + dsimp [acc5] + simpa using toField_shift4Mul_pow x acc4 x15 524287 15 hacc4 hx15 + have hacc6 : toField acc6 = toField x ^ 134217727 := by + dsimp [acc6] + simpa using toField_shift4Mul_pow x acc5 x15 8388607 15 hacc5 hx15 + have hfinal := toField_shift4Mul_pow x acc6 x13 134217727 13 hacc6 hx13 + simpa [invExponent, Mersenne31.fieldSize] using hfinal + +/-- Fast inversion agrees with inversion in the canonical field. -/ +@[simp] +theorem toField_inv (x : Field) : toField x⁻¹ = (toField x)⁻¹ := by + change toField (inv x) = (toField x)⁻¹ + rw [toField_inv_pow] + by_cases hx : toField x = 0 + · rw [hx] + simp [invExponent, Mersenne31.fieldSize] + · symm + apply inv_eq_of_mul_eq_one_right + calc + toField x * toField x ^ invExponent = toField x ^ (invExponent + 1) := by + rw [pow_succ'] + _ = toField x ^ (Mersenne31.fieldSize - 1) := by + congr 1 + _ = 1 := by + exact ZMod.pow_card_sub_one_eq_one hx + +/-- Division through inversion and fast multiplication. -/ +@[inline] +def div (x y : Field) : Field := + mul x (inv y) + +/-- Use fast inversion and multiplication for the standard `/` notation. -/ +instance instDivField : Div Field where + div := div + +/-- Fast division agrees with division in the canonical field. -/ +@[simp] +theorem toField_div (x y : Field) : toField (x / y) = toField x / toField y := by + change toField (div x y) = toField x / toField y + unfold div + change toField (x * inv y) = toField x / toField y + rw [toField_mul] + change toField x * toField y⁻¹ = toField x / toField y + rw [toField_inv] + rfl + +/-- Use fast natural powers and inversion for integer powers. -/ +instance instPowFieldInt : Pow Field Int where + pow x n := + match n with + | Int.ofNat k => pow x k + | Int.negSucc k => pow (inv x) (k + 1) + +/-- Fast natural scalar multiplication agrees with canonical-field scalar multiplication. -/ +@[simp] +theorem toField_nsmul (n : Nat) (x : Field) : toField (n • x) = n • toField x := by + change toField ((n : Field) * x) = n • toField x + rw [toField_mul, toField_natCast, nsmul_eq_mul] + +/-- Fast integer scalar multiplication agrees with canonical-field scalar multiplication. -/ +@[simp] +theorem toField_zsmul (n : Int) (x : Field) : toField (n • x) = n • toField x := by + change toField ((n : Field) * x) = n • toField x + rw [toField_mul, toField_intCast, zsmul_eq_mul] + +/-- Standard fast natural powers agree with powers in the canonical field. -/ +@[simp] +theorem toField_npow (x : Field) (n : Nat) : toField (x ^ n) = toField x ^ n := by + change toField (pow x n) = toField x ^ n + exact toField_pow x n + +/-- Standard fast integer powers agree with integer powers in the canonical field. -/ +@[simp] +theorem toField_zpow (x : Field) (n : Int) : toField (x ^ n) = toField x ^ n := by + cases n with + | ofNat k => + change toField (pow x k) = toField x ^ (k : Int) + rw [toField_pow, zpow_natCast] + | negSucc k => + change toField (pow (inv x) (k + 1)) = toField x ^ Int.negSucc k + rw [toField_pow] + change toField x⁻¹ ^ (k + 1) = toField x ^ Int.negSucc k + rw [toField_inv, zpow_negSucc, inv_pow] + +/-- Fast nonnegative rational scalar multiplication agrees with canonical-field scalar +multiplication. -/ +@[simp] +theorem toField_nnqsmul (q : ℚ≥0) (x : Field) : toField (q • x) = q • toField x := by + change toField (ofField (q • toField x)) = q • toField x + rw [toField_ofField] + +/-- Fast rational scalar multiplication agrees with canonical-field scalar multiplication. -/ +@[simp] +theorem toField_qsmul (q : ℚ) (x : Field) : toField (q • x) = q • toField x := by + change toField (ofField (q • toField x)) = q • toField x + rw [toField_ofField] + +/-- Ring equivalence between the fast representation and canonical `Mersenne31.Field`. -/ +@[expose] +def ringEquiv : Field ≃+* Mersenne31.Field where + toFun := toField + invFun := ofField + left_inv := ofField_toField + right_inv := toField_ofField + map_add' := toField_add + map_mul' := toField_mul + +/-- Applying `ringEquiv` is the same as interpreting a fast value canonically. -/ +@[simp] +theorem ringEquiv_apply (x : Field) : ringEquiv x = toField x := rfl + +/-- Applying the inverse `ringEquiv` converts a canonical value into fast form. -/ +@[simp] +theorem ringEquiv_symm_apply (x : Mersenne31.Field) : ringEquiv.symm x = ofField x := rfl + +/-- Field instance transferred from canonical Mersenne31 through `toField`. -/ +@[no_expose] +instance (priority := low) instField : _root_.Field Field := + toField_injective.field toField + toField_zero + toField_one + toField_add + toField_mul + toField_neg + toField_sub + toField_inv + toField_div + toField_nsmul + toField_zsmul + toField_nnqsmul + toField_qsmul + toField_npow + toField_zpow + toField_natCast + toField_intCast + toField_nnratCast + toField_ratCast + +/-- Commutative-ring instance inherited from the transferred field structure. -/ +instance (priority := low) instCommRing : CommRing Field := by + infer_instance + +/-- Fast Mersenne31 is a non-binary field. -/ +instance (priority := low) instNonBinaryField : NonBinaryField Field where + char_neq_2 := by + change ((2 : Nat) : Field) ≠ 0 + intro h + exact NonBinaryField.char_neq_2 (F := Mersenne31.Field) (by + calc + (2 : Mersenne31.Field) = toField ((2 : Nat) : Field) := (toField_natCast 2).symm + _ = toField (0 : Field) := congrArg toField h + _ = 0 := toField_zero) + +end Fast +end Mersenne31 diff --git a/CompPoly/Fields/Montgomery/ScalarFft.lean b/CompPoly/Fields/Montgomery/ScalarFft.lean new file mode 100644 index 00000000..38ab2f98 --- /dev/null +++ b/CompPoly/Fields/Montgomery/ScalarFft.lean @@ -0,0 +1,73 @@ +/- +Copyright (c) 2026 CompPoly Contributors. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Gregor Mitscha-Baude +-/ +module + +public import CompPoly.Fields.Montgomery.Native64x8Defs + +/-! +# Radix-2 DIT FFT over eight-limb Montgomery elements (zero-import) + +An in-place radix-2 decimation-in-time FFT over `Limbs8` Montgomery residues: a bit-reversal +permutation followed by `logN` rounds of butterflies against a precomputed Montgomery-form +twiddle table. Like the arithmetic in `CompPoly.Fields.Montgomery.Native64x8Defs`, the loop +nest is generic over the modulus, taking `q` and `negInv` explicitly. + +As explained in `CompPoly.Fields.Montgomery.Native64x8Defs`, this module deliberately imports +nothing beyond that (itself zero-import) module: downstream consumers put it into +`precompileModules` native-compilation lanes, and `precompileModules` compiles the +entire import closure — so the runtime definitions must not pull in mathlib. + +This module contains runtime definitions only; correctness specifications live downstream +for now. +-/ + +@[expose] public section + +namespace Montgomery +namespace ScalarFft + +open Native64x8 (Limbs8 add sub mul) + +/-- Bit-reversal permutation index. -/ +def bitreverse (n l : Nat) : Nat := Id.run do + let mut r := 0 + let mut m := n + for _ in [0:l] do + r := (r <<< 1) ||| (m &&& 1) + m := m >>> 1 + return r + +/-- In-place radix-2 DIT FFT over eight-limb Montgomery residues modulo `q`: bit-reversal +permutation, then `logN` rounds of butterflies against the Montgomery-form twiddles `tw`. -/ +def fft (q : Limbs8) (negInv : UInt64) (a0 : Array Limbs8) (tw : Array Limbs8) + (logN : Nat) : Array Limbs8 := Id.run do + let n := a0.size + let mut a := a0 + for k in [0:n] do + let rk := bitreverse k logN + if k < rk then + let ak := a[k]! + let ark := a[rk]! + a := (a.set! k ark).set! rk ak + let mut half := 1 + for _ in [0:logN] do + let chunk := 2 * half + let twiddleChunk := n / chunk + for c in [0:n / chunk] do + let s := c * chunk + for j in [0:half] do + let twdl := tw[j * twiddleChunk]! + let aIdx := s + j + let bIdx := s + half + j + let aOld := a[aIdx]! + let t := mul q negInv twdl a[bIdx]! + a := a.set! aIdx (add q aOld t) + a := a.set! bIdx (sub q aOld t) + half := chunk + return a + +end ScalarFft +end Montgomery diff --git a/CompPoly/Fields/Pasta.lean b/CompPoly/Fields/Pasta.lean new file mode 100644 index 00000000..bca45de2 --- /dev/null +++ b/CompPoly/Fields/Pasta.lean @@ -0,0 +1,17 @@ +/- +Copyright (c) 2026 CompPoly Contributors. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Gregor Mitscha-Baude +-/ +module + +public import CompPoly.Fields.Pasta.Basic +public import CompPoly.Fields.Pasta.Fast + +/-! +# Pasta (Pallas / Vesta) fields + +Facade module for the Pasta base fields. It re-exports the canonical `ZMod` models with +their primality certificates from `CompPoly.Fields.Pasta.Basic` and the native-word +eight-limb Montgomery implementations from `CompPoly.Fields.Pasta.Fast`. +-/ diff --git a/CompPoly/Fields/Pasta/Basic.lean b/CompPoly/Fields/Pasta/Basic.lean new file mode 100644 index 00000000..16d17a20 --- /dev/null +++ b/CompPoly/Fields/Pasta/Basic.lean @@ -0,0 +1,216 @@ +/- +Copyright (c) 2026 CompPoly Contributors. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Daira-Emma Hopwood, Gregor Mitscha-Baude +-/ +module + +public import CompPoly.Fields.PrattCertificate + +/-! +# The Pasta (Pallas / Vesta) base prime fields + +The Pallas and Vesta curves form a 2-cycle: the base field of one is the scalar field of the +other. This module defines the two underlying 255-bit primes together with their Lucas/Pratt +primality certificates, following the pattern of `CompPoly.Fields.Secp256k1`. + +* `Pallas.baseFieldSize` is the Pallas base field size, equal to the Vesta scalar field size; +* `Vesta.baseFieldSize` is the Vesta base field size, equal to the Pallas scalar field size. + +Both primes are `1 mod 2 ^ 32` and have 2-adicity 32. The certificates were generated from +the factorizations of `p - 1` and `q - 1`; see . +-/ + +@[expose] public section + +namespace Pallas + +/-- The base field size of the Pallas curve, which is the scalar field size of Vesta. -/ +@[reducible] +def baseFieldSize : Nat := + 0x40000000000000000000000000000000224698fc094cf91b992d30ed00000001 + +/-- The Pallas base field as a `ZMod`. -/ +abbrev BaseField := ZMod baseFieldSize + +/-- The Pallas base field size is prime. -/ +theorem baseFieldSize_is_prime : Nat.Prime baseFieldSize := by + unfold baseFieldSize + refine PrattCertificate'.out + (p := 28948022309329048855892746252171976963363056481941560715954676764349967630337) + ⟨5, (by reduce_mod_char), ?_⟩ + refine .split [2 ^ 32, 3, 463, 539204044132271846773, + 8999194758858563409123804352480028797519453] (fun r hr => ?_) (by norm_num) + simp at hr + rcases hr with hr | hr | hr | hr | hr + all_goals rw [hr] + · exact .prime 2 32 _ (by pratt) (by reduce_mod_char; decide) (by norm_num) + · exact .prime 3 1 _ (by pratt) (by reduce_mod_char; decide) (by norm_num) + · exact .prime 463 1 _ (by pratt) (by reduce_mod_char; decide) (by norm_num) + · refine .prime 539204044132271846773 1 _ ?_ (by reduce_mod_char; decide) (by norm_num) + refine PrattCertificate'.out (p := 539204044132271846773) ⟨5, (by reduce_mod_char), ?_⟩ + refine .split [2 ^ 2, 3 ^ 5, 89, 14923, 417677162933] (fun r hr => ?_) (by norm_num) + simp at hr + rcases hr with hr | hr | hr | hr | hr + all_goals rw [hr] + · exact .prime 2 2 _ (by pratt) (by reduce_mod_char; decide) (by norm_num) + · exact .prime 3 5 _ (by pratt) (by reduce_mod_char; decide) (by norm_num) + · exact .prime 89 1 _ (by pratt) (by reduce_mod_char; decide) (by norm_num) + · exact .prime 14923 1 _ (by pratt) (by reduce_mod_char; decide) (by norm_num) + · refine .prime 417677162933 1 _ ?_ (by reduce_mod_char; decide) (by norm_num) + refine PrattCertificate'.out (p := 417677162933) ⟨2, (by reduce_mod_char), ?_⟩ + refine .split [2 ^ 2, 59, 1973, 897019] (fun r hr => ?_) (by norm_num) + simp at hr + rcases hr with hr | hr | hr | hr + all_goals rw [hr] + · exact .prime 2 2 _ (by pratt) (by reduce_mod_char; decide) (by norm_num) + · exact .prime 59 1 _ (by pratt) (by reduce_mod_char; decide) (by norm_num) + · exact .prime 1973 1 _ (by pratt) (by reduce_mod_char; decide) (by norm_num) + · exact .prime 897019 1 _ (by pratt) (by reduce_mod_char; decide) (by norm_num) + · refine .prime 8999194758858563409123804352480028797519453 1 _ ?_ (by reduce_mod_char; decide) + (by norm_num) + refine PrattCertificate'.out (p := 8999194758858563409123804352480028797519453) + ⟨2, (by reduce_mod_char), ?_⟩ + refine .split [2 ^ 2, 3 ^ 4, 11, 2531, 115603, 1197907, 22160661629, 325086459374267] + (fun r hr => ?_) (by norm_num) + simp at hr + rcases hr with hr | hr | hr | hr | hr | hr | hr | hr + all_goals rw [hr] + · exact .prime 2 2 _ (by pratt) (by reduce_mod_char; decide) (by norm_num) + · exact .prime 3 4 _ (by pratt) (by reduce_mod_char; decide) (by norm_num) + · exact .prime 11 1 _ (by pratt) (by reduce_mod_char; decide) (by norm_num) + · exact .prime 2531 1 _ (by pratt) (by reduce_mod_char; decide) (by norm_num) + · exact .prime 115603 1 _ (by pratt) (by reduce_mod_char; decide) (by norm_num) + · exact .prime 1197907 1 _ (by pratt) (by reduce_mod_char; decide) (by norm_num) + · refine .prime 22160661629 1 _ ?_ (by reduce_mod_char; decide) (by norm_num) + refine PrattCertificate'.out (p := 22160661629) ⟨3, (by reduce_mod_char), ?_⟩ + refine .split [2 ^ 2, 7, 19, 41655379] (fun r hr => ?_) (by norm_num) + simp at hr + rcases hr with hr | hr | hr | hr + all_goals rw [hr] + · exact .prime 2 2 _ (by pratt) (by reduce_mod_char; decide) (by norm_num) + · exact .prime 7 1 _ (by pratt) (by reduce_mod_char; decide) (by norm_num) + · exact .prime 19 1 _ (by pratt) (by reduce_mod_char; decide) (by norm_num) + · exact .prime 41655379 1 _ (by pratt) (by reduce_mod_char; decide) (by norm_num) + · refine .prime 325086459374267 1 _ ?_ (by reduce_mod_char; decide) (by norm_num) + refine PrattCertificate'.out (p := 325086459374267) ⟨2, (by reduce_mod_char), ?_⟩ + refine .split [2, 509, 413527, 772231] (fun r hr => ?_) (by norm_num) + simp at hr + rcases hr with hr | hr | hr | hr + all_goals rw [hr] + · exact .prime 2 1 _ (by pratt) (by reduce_mod_char; decide) (by norm_num) + · exact .prime 509 1 _ (by pratt) (by reduce_mod_char; decide) (by norm_num) + · exact .prime 413527 1 _ (by pratt) (by reduce_mod_char; decide) (by norm_num) + · exact .prime 772231 1 _ (by pratt) (by reduce_mod_char; decide) (by norm_num) + +instance : Fact (Nat.Prime baseFieldSize) := ⟨baseFieldSize_is_prime⟩ + +instance : Field BaseField := ZMod.instField baseFieldSize + +end Pallas + +namespace Vesta + +/-- The base field size of the Vesta curve, which is the scalar field size of Pallas. -/ +@[reducible] +def baseFieldSize : Nat := + 0x40000000000000000000000000000000224698fc0994a8dd8c46eb2100000001 + +/-- The Vesta base field as a `ZMod`. -/ +abbrev BaseField := ZMod baseFieldSize + +/-- The Vesta base field size is prime. -/ +theorem baseFieldSize_is_prime : Nat.Prime baseFieldSize := by + unfold baseFieldSize + refine PrattCertificate'.out + (p := 28948022309329048855892746252171976963363056481941647379679742748393362948097) + ⟨5, (by reduce_mod_char), ?_⟩ + refine .split [2 ^ 32, 3 ^ 2, 1709, 24859, 1690502597179744445941507, + 10427374428728808478656897599072717] (fun r hr => ?_) (by norm_num) + simp at hr + rcases hr with hr | hr | hr | hr | hr | hr + all_goals rw [hr] + · exact .prime 2 32 _ (by pratt) (by reduce_mod_char; decide) (by norm_num) + · exact .prime 3 2 _ (by pratt) (by reduce_mod_char; decide) (by norm_num) + · exact .prime 1709 1 _ (by pratt) (by reduce_mod_char; decide) (by norm_num) + · exact .prime 24859 1 _ (by pratt) (by reduce_mod_char; decide) (by norm_num) + · refine .prime 1690502597179744445941507 1 _ ?_ (by reduce_mod_char; decide) (by norm_num) + refine PrattCertificate'.out (p := 1690502597179744445941507) ⟨2, (by reduce_mod_char), ?_⟩ + refine .split [2, 3, 13, 4129989133, 5247740253619] (fun r hr => ?_) (by norm_num) + simp at hr + rcases hr with hr | hr | hr | hr | hr + all_goals rw [hr] + · exact .prime 2 1 _ (by pratt) (by reduce_mod_char; decide) (by norm_num) + · exact .prime 3 1 _ (by pratt) (by reduce_mod_char; decide) (by norm_num) + · exact .prime 13 1 _ (by pratt) (by reduce_mod_char; decide) (by norm_num) + · refine .prime 4129989133 1 _ ?_ (by reduce_mod_char; decide) (by norm_num) + refine PrattCertificate'.out (p := 4129989133) ⟨5, (by reduce_mod_char), ?_⟩ + refine .split [2 ^ 2, 3, 359, 958679] (fun r hr => ?_) (by norm_num) + simp at hr + rcases hr with hr | hr | hr | hr + all_goals rw [hr] + · exact .prime 2 2 _ (by pratt) (by reduce_mod_char; decide) (by norm_num) + · exact .prime 3 1 _ (by pratt) (by reduce_mod_char; decide) (by norm_num) + · exact .prime 359 1 _ (by pratt) (by reduce_mod_char; decide) (by norm_num) + · exact .prime 958679 1 _ (by pratt) (by reduce_mod_char; decide) (by norm_num) + · refine .prime 5247740253619 1 _ ?_ (by reduce_mod_char; decide) (by norm_num) + refine PrattCertificate'.out (p := 5247740253619) ⟨2, (by reduce_mod_char), ?_⟩ + refine .split [2, 3 ^ 3, 17, 71, 80513981] (fun r hr => ?_) (by norm_num) + simp at hr + rcases hr with hr | hr | hr | hr | hr + all_goals rw [hr] + · exact .prime 2 1 _ (by pratt) (by reduce_mod_char; decide) (by norm_num) + · exact .prime 3 3 _ (by pratt) (by reduce_mod_char; decide) (by norm_num) + · exact .prime 17 1 _ (by pratt) (by reduce_mod_char; decide) (by norm_num) + · exact .prime 71 1 _ (by pratt) (by reduce_mod_char; decide) (by norm_num) + · exact .prime 80513981 1 _ (by pratt) (by reduce_mod_char; decide) (by norm_num) + · refine .prime 10427374428728808478656897599072717 1 _ ?_ (by reduce_mod_char; decide) + (by norm_num) + refine PrattCertificate'.out (p := 10427374428728808478656897599072717) + ⟨2, (by reduce_mod_char), ?_⟩ + refine .split [2 ^ 2, 294793, 4229279, 399082391, 5239247429827] (fun r hr => ?_) + (by norm_num) + simp at hr + rcases hr with hr | hr | hr | hr | hr + all_goals rw [hr] + · exact .prime 2 2 _ (by pratt) (by reduce_mod_char; decide) (by norm_num) + · exact .prime 294793 1 _ (by pratt) (by reduce_mod_char; decide) (by norm_num) + · exact .prime 4229279 1 _ (by pratt) (by reduce_mod_char; decide) (by norm_num) + · exact .prime 399082391 1 _ (by pratt) (by reduce_mod_char; decide) (by norm_num) + · refine .prime 5239247429827 1 _ ?_ (by reduce_mod_char; decide) (by norm_num) + refine PrattCertificate'.out (p := 5239247429827) ⟨2, (by reduce_mod_char), ?_⟩ + refine .split [2, 3 ^ 2, 757, 12149, 31649] (fun r hr => ?_) (by norm_num) + simp at hr + rcases hr with hr | hr | hr | hr | hr + all_goals rw [hr] + · exact .prime 2 1 _ (by pratt) (by reduce_mod_char; decide) (by norm_num) + · exact .prime 3 2 _ (by pratt) (by reduce_mod_char; decide) (by norm_num) + · exact .prime 757 1 _ (by pratt) (by reduce_mod_char; decide) (by norm_num) + · exact .prime 12149 1 _ (by pratt) (by reduce_mod_char; decide) (by norm_num) + · exact .prime 31649 1 _ (by pratt) (by reduce_mod_char; decide) (by norm_num) + +instance : Fact (Nat.Prime baseFieldSize) := ⟨baseFieldSize_is_prime⟩ + +instance : Field BaseField := ZMod.instField baseFieldSize + +end Vesta + +namespace Pallas + +/-- The scalar field size of Pallas is the base field size of Vesta. -/ +abbrev scalarFieldSize : Nat := Vesta.baseFieldSize + +/-- The Pallas scalar field is the Vesta base field. -/ +abbrev ScalarField := Vesta.BaseField + +end Pallas + +namespace Vesta + +/-- The scalar field size of Vesta is the base field size of Pallas. -/ +abbrev scalarFieldSize : Nat := Pallas.baseFieldSize + +/-- The Vesta scalar field is the Pallas base field. -/ +abbrev ScalarField := Pallas.BaseField + +end Vesta diff --git a/CompPoly/Fields/Pasta/Fast.lean b/CompPoly/Fields/Pasta/Fast.lean new file mode 100644 index 00000000..72585d2a --- /dev/null +++ b/CompPoly/Fields/Pasta/Fast.lean @@ -0,0 +1,93 @@ +/- +Copyright (c) 2026 CompPoly Contributors. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Gregor Mitscha-Baude +-/ +module + +public import CompPoly.Fields.Pasta.Basic +public import CompPoly.Fields.Montgomery.Native64x8Field + +/-! +# Fast Pasta base fields + +Native-word Montgomery implementations of the Pallas and Vesta base field arithmetic. The +shared algorithms and proofs live in `CompPoly.Fields.Montgomery.Native64x8Field`; this module +supplies the two sets of constants and the concrete API. + +Both Pasta primes are `1 mod 2 ^ 32`, so both have `montgomeryNegInv = 2 ^ 32 - 1`. +-/ + +@[expose] public section + +namespace Pallas.Fast + +open Montgomery.Native64x8 (Mont64x8Field FastField) + +/-! ## Parameters and carrier -/ + +/-- The per-field data realizing the Pallas base field as a fast eight-limb Montgomery +field. -/ +instance instMont64x8Field : Mont64x8Field Pallas.baseFieldSize where + prime := Pallas.baseFieldSize_is_prime + modulusLimbs := ⟨0x1, 0x992d30ed, 0x94cf91b, 0x224698fc, 0x0, 0x0, 0x0, 0x40000000⟩ + rModModulus := + ⟨0xfffffffd, 0x34786d38, 0xe41914ad, 0x992c350b, 0xffffffff, 0xffffffff, 0xffffffff, + 0x3fffffff⟩ + r2ModModulus := + ⟨0xf, 0x8c78ecb3, 0x8b0de0e7, 0xd7d30dbd, 0xc3c95d18, 0x7797a99b, 0x7b9cb714, 0x96d41af⟩ + montgomeryNegInv := 0xffffffff + +/-- The fast native-word Pallas base field carrier, stored as a Montgomery residue. -/ +abbrev Field : Type := FastField Pallas.baseFieldSize + +/-! ## Conversions -/ + +/-- Convert from the canonical `ZMod` Pallas base field into fast Montgomery form. -/ +@[inline] +def ofField (x : Pallas.BaseField) : Field := + Montgomery.Native64x8.FastField.ofField x + +/-! ## Canonical bridge -/ + +/-- Ring equivalence between the fast Montgomery representation and `Pallas.BaseField`. -/ +def ringEquiv : Field ≃+* Pallas.BaseField := + Montgomery.Native64x8.FastField.ringEquiv Pallas.baseFieldSize + +end Pallas.Fast + +namespace Vesta.Fast + +open Montgomery.Native64x8 (Mont64x8Field FastField) + +/-! ## Parameters and carrier -/ + +/-- The per-field data realizing the Vesta base field as a fast eight-limb Montgomery +field. -/ +instance instMont64x8Field : Mont64x8Field Vesta.baseFieldSize where + prime := Vesta.baseFieldSize_is_prime + modulusLimbs := ⟨0x1, 0x8c46eb21, 0x994a8dd, 0x224698fc, 0x0, 0x0, 0x0, 0x40000000⟩ + rModModulus := + ⟨0xfffffffd, 0x5b2b3e9c, 0xe3420567, 0x992c350b, 0xffffffff, 0xffffffff, 0xffffffff, + 0x3fffffff⟩ + r2ModModulus := + ⟨0xf, 0xfc9678ff, 0x891a16e3, 0x67bb433d, 0x4ccf590, 0x7fae2310, 0x7ccfdaa9, 0x96d41af⟩ + montgomeryNegInv := 0xffffffff + +/-- The fast native-word Vesta base field carrier, stored as a Montgomery residue. -/ +abbrev Field : Type := FastField Vesta.baseFieldSize + +/-! ## Conversions -/ + +/-- Convert from the canonical `ZMod` Vesta base field into fast Montgomery form. -/ +@[inline] +def ofField (x : Vesta.BaseField) : Field := + Montgomery.Native64x8.FastField.ofField x + +/-! ## Canonical bridge -/ + +/-- Ring equivalence between the fast Montgomery representation and `Vesta.BaseField`. -/ +def ringEquiv : Field ≃+* Vesta.BaseField := + Montgomery.Native64x8.FastField.ringEquiv Vesta.baseFieldSize + +end Vesta.Fast diff --git a/CompPoly/Fields/README.md b/CompPoly/Fields/README.md index b46fb53d..7d61d1f5 100644 --- a/CompPoly/Fields/README.md +++ b/CompPoly/Fields/README.md @@ -22,7 +22,9 @@ This directory contains formally verified field infrastructure used in zero-know | **BN254/Fast.lean** | Eight-limb Montgomery instantiation of the BN254 scalar field (`Mont64x8Field` and `GcdData` constants, `ScalarField`, `ringEquiv`). | | **Extension.lean** | Facade for the field-extension stack (arbitrary monic modulus; binomial as a special case). | | **Extension/Binomial.lean** | Irreducibility of `X^d - W` over a finite field: Rabin's test collapsed to two base-field exponentiations (`irreducible_X_pow_four_sub_C_iff`). | -| **Extension/Defs.lean** | `ExtensionParams` (degree, lower coefficients of the monic modulus, base cardinality), `BinomialParams` and its `toExtensionParams`, and the carrier `Ext P = Vector F d` with its ring operations — including the `red` reduction table and the `@[csimp]`-registered `mulTbl`. | +| **Extension/Arithmetic.lean** | `ExtensionParams` (degree, lower coefficients of the monic modulus, base cardinality), `BinomialParams` and its `toExtensionParams`, and the presentation-indexed carrier `Ext P` with length-`d` coefficient vectors and ring operations — including the `red` reduction table and the `@[csimp]`-registered `mulTbl`. | +| **Extension/Defs.lean** | Polynomial specifications, degree/monicity, and binomial correspondence. | +| **Extension/Cardinality.lean** | Proof-only finiteness, optional enumeration, and certified cardinality of the coefficient carrier. | | **Extension/Bridge.lean** | `toQuot : Ext P → AdjoinRoot P.poly`, its ring-hom and injectivity proofs, and `CommRing (Ext P)`. | | **Extension/Field.lean** | Bijectivity (`ringEquivQuot`), cardinality, Fermat inversion, and `Field (Ext P)`. | | **BabyBear/Ext4.lean** | \(\mathrm{BabyBear}[X]/(X^4 - 11)\). | @@ -34,13 +36,18 @@ This directory contains formally verified field infrastructure used in zero-know | **KoalaBear/Ext6/SexticCertData.lean** | Generated Rabin certificate data for the sextic, including one coprimality certificate per prime factor of 6. Do not hand-edit. | | **KoalaBear/Ext6/SexticIrreducible.lean** | Kernel-checked irreducibility of \(X^6 + X^3 + 1\) via Rabin's test at composite degree. | | **KoalaBear/Ext6/GaloisField.lean** | Opt-in bridge identifying `Ext6` with Mathlib's abstract `GaloisField KoalaBear.fieldSize 6` (ArkLib's `KoalaSextic` parameter point). Separate module so the GaloisField import is not forced on `Ext6` users. | +| **Goldilocks.lean** | Facade for the \(2^{64} - 2^{32} + 1\) Plonky2/3 field, re-exporting the canonical `ZMod` model and fast native-word implementation. | +| **Goldilocks/Basic.lean** | Canonical \(2^{64} - 2^{32} + 1\) field model and primality proof. | +| **Goldilocks/Fast.lean** | Verified `UInt64` implementation of Goldilocks arithmetic. A single-word 64-bit prime fits neither Montgomery carrier — `Mont32Field` requires modulus < 2^31 and `Mont64x8Field` is an eight-limb layout — so this is a bespoke implementation resting on \(2^{64} \equiv 2^{32} - 1\). | +| **Goldilocks/FastDefs.lean** | Zero-import runtime word kernels behind `Goldilocks/Fast.lean`, kept importless so `precompileModules` lanes can compile them. | | **Hachi.lean** | \(2^{32} - 99\) — 32-bit prime field. **Name provisional.** Included as a 32-bit example rather than a production target: it exercises a base field with no Montgomery fast path (`Mont32Field` requires modulus < 2^31) and two-adicity 2, so no radix-2 NTT domain exists for it. | | **Hachi/Ext4.lean** | \(\mathrm{Hachi}[X]/(X^4 - 2)\). | -| **Goldilocks.lean** | \(2^{64} - 2^{32} + 1\) — Plonky2/3. | | **KoalaBear.lean** | Facade for KoalaBear modules, re-exporting the canonical field and fast native-word implementation. | | **KoalaBear/Basic.lean** | \(2^{31} - 2^{24} + 1\) — lean Ethereum spec. | | **KoalaBear/Fast.lean** | KoalaBear-namespaced API over the shared fast-field implementation (`Montgomery/Native32Field.lean`): thin wrappers forwarding the native `UInt32` Montgomery-residue operations and their `KoalaBear.Field` equivalence (`@[simp]`) lemmas. | -| **Mersenne.lean** | \(2^{31} - 1\) — Circle STARKs. | +| **Mersenne31.lean** | Facade for the \(2^{31} - 1\) Circle STARK field, re-exporting the canonical `ZMod` model and fast native-word implementation. | +| **Mersenne31/Basic.lean** | Canonical \(2^{31} - 1\) field model and primality proof. | +| **Mersenne31/Fast.lean** | Verified `UInt32` implementation of Mersenne31 arithmetic. | | **Montgomery/Basic.lean** | Radix-generic Montgomery reduction, field-agnostic number theory shared by the fast prime fields. | | **Montgomery/Native32.lean** | Raw `UInt32`/`UInt64` Montgomery reduction over explicit word constants, including bounds and correctness. | | **Montgomery/Native32Field.lean** | Per-field parameters, the shared `FastField` carrier, arithmetic, instances, and canonical-field bridge. | @@ -50,6 +57,10 @@ This directory contains formally verified field infrastructure used in zero-know | **Montgomery/Native64x8Field.lean** | The `Mont64x8Field` class, `FastField` carrier, arithmetic, instances, and canonical-field bridge for moduli below `2^255`. | | **Montgomery/Native64x8InvDefs.lean** | Mathlib-free binary-GCD inversion runtime ([eprint 2020/972](https://eprint.iacr.org/2020/972)): the `GcdData` schedule, the candidate, and the checked `invGcdRaw`. | | **Montgomery/Native64x8Inv.lean** | Correctness of the checked inversion (`invGcdRaw`, wrapper `FastField.invGcd`), the divstep coefficient bounds, and the candidate's mac-width safety. | +| **Montgomery/ScalarFft.lean** | Zero-import in-place radix-2 DIT FFT over eight-limb Montgomery residues and precomputed twiddles. | +| **Pasta.lean** | Facade for the Pasta modules, re-exporting the canonical Pallas/Vesta base fields and their fast native-word implementations. | +| **Pasta/Basic.lean** | The two 255-bit Pasta base primes (Pallas base = Vesta scalar and vice versa), with Pratt primality certificates. | +| **Pasta/Fast.lean** | Pallas- and Vesta-namespaced API over the shared eight-limb fast-field implementation, with per-field constants and canonical-field bridges. | | **Secp256k1.lean** | Base and scalar fields for the Secp256k1 curve (used in Bitcoin/Ethereum). | ## Binary-field modules @@ -57,16 +68,21 @@ This directory contains formally verified field infrastructure used in zero-know The `Binary/` subtree provides characteristic-2 field infrastructure used by GHASH and additive-NTT workflows: - `Binary/BF128Ghash/*` — GF(2^128) model, implementation, and certificates. +- `Binary/BF64/*` — polynomial-basis GF(2^64) (`GF(2)[x]/(x^64 + x^4 + x^3 + x + 1)`) with a nominal carrier storing `BitVec 64` coordinates, plus its degree-3 extension GF(2^192). A different basis from the GF(2^64) rung of `Binary/Tower/`, so the two disagree on bit-level encodings. - `Binary/AdditiveNTT/*` — additive-NTT domain/algorithm/correctness stack. - `Binary/Tower/*` — abstract/concrete binary tower-field constructions and supporting lemmas. +- `Binary/Tower/Fast.lean` — packed machine-word tower arithmetic with a GF(2^8) table base, proven against the concrete tower; `Field` instances up to GF(2^128). +- `Binary/Tower/FastDefs.lean` — zero-import runtime definitions of the packed tower arithmetic, for `precompileModules` consumers. ## Field extensions -`Extension/` provides computable `F[X]/f` arithmetic for an arbitrary monic `f` in odd -characteristic, with the `Field` structure proved against `AdjoinRoot f`, plus +`Extension/` provides computable `F[X]/f` arithmetic for an arbitrary monic `f` over any finite +base field, with the `Field` structure proved against `AdjoinRoot f`, plus `Algebra F (Ext P)`, a base embedding `ofBase`, and the adjoined root `gen`. Binomial moduli `X^d - W` are the special case entered through `BinomialParams.toExtensionParams`, and get -`gen ^ d = ofBase W`. +`gen ^ d = ofBase W`. Nothing assumes odd characteristic: `Binary/BF64/Ext3.lean` instantiates +the framework over `GF(2^64)`, though the odd-characteristic instances are still the +better-exercised path. Irreducibility of the defining polynomial comes from Rabin's test. For a binomial it collapses to two exponentiations in the base field — no generated certificates. For a general modulus it uses diff --git a/CompPoly/LinearAlgebra/PolynomialMatrix.lean b/CompPoly/LinearAlgebra/PolynomialMatrix.lean index 9314a412..729d4002 100644 --- a/CompPoly/LinearAlgebra/PolynomialMatrix.lean +++ b/CompPoly/LinearAlgebra/PolynomialMatrix.lean @@ -7,6 +7,8 @@ module public import CompPoly.LinearAlgebra.PolynomialMatrix.Basic public import CompPoly.LinearAlgebra.PolynomialMatrix.Degree +public import CompPoly.LinearAlgebra.PolynomialMatrix.Operations +public import CompPoly.LinearAlgebra.PolynomialMatrix.Approximant public import CompPoly.LinearAlgebra.PolynomialMatrix.Shifted public import CompPoly.LinearAlgebra.PolynomialMatrix.RowSpan public import CompPoly.LinearAlgebra.PolynomialMatrix.ShiftedReduction diff --git a/CompPoly/LinearAlgebra/PolynomialMatrix/Approximant.lean b/CompPoly/LinearAlgebra/PolynomialMatrix/Approximant.lean new file mode 100644 index 00000000..a157149a --- /dev/null +++ b/CompPoly/LinearAlgebra/PolynomialMatrix/Approximant.lean @@ -0,0 +1,19 @@ +/- +Copyright (c) 2026 CompPoly Contributors. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Valerii Huhnin +-/ + +module + +public import CompPoly.LinearAlgebra.PolynomialMatrix.Approximant.Basic +public import CompPoly.LinearAlgebra.PolynomialMatrix.Approximant.PMBasis +public import CompPoly.LinearAlgebra.PolynomialMatrix.Approximant.PartialLinearization +public import CompPoly.LinearAlgebra.PolynomialMatrix.Approximant.ModularEquation +public import CompPoly.LinearAlgebra.PolynomialMatrix.Approximant.Correctness + +/-! +# Approximant-Basis Polynomial-Matrix Infrastructure +-/ + +@[expose] public section diff --git a/CompPoly/LinearAlgebra/PolynomialMatrix/Approximant/Basic.lean b/CompPoly/LinearAlgebra/PolynomialMatrix/Approximant/Basic.lean new file mode 100644 index 00000000..7c64e8db --- /dev/null +++ b/CompPoly/LinearAlgebra/PolynomialMatrix/Approximant/Basic.lean @@ -0,0 +1,90 @@ +/- +Copyright (c) 2026 CompPoly Contributors. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Valerii Huhnin +-/ + +module + +public import CompPoly.LinearAlgebra.PolynomialMatrix.Operations + +/-! +# X-Adic Approximant Problems + +Basic data structures for approximant-basis computations over polynomial +matrices. + +## References + +* [Beckermann, B., and Labahn, G., *A uniform approach for the fast + computation of matrix-type Pade approximants*][BL94] +* [Giorgi, P., Jeannerod, C.-P., and Villard, G., *On the complexity of + polynomial matrix computations*][GJV03] +-/ + +@[expose] public section + +namespace CompPoly + +namespace PolynomialMatrix + +namespace Approximant + +variable {F : Type*} + +/-- A row approximant problem `p * matrix = 0 mod X^orders`. -/ +structure XAdicProblem (F : Type*) [Zero F] where + orders : Array Nat + matrix : PolynomialMatrix F + +/-- Maximum X-adic order in a problem. -/ +def maxOrder [Zero F] (problem : XAdicProblem F) : Nat := + problem.orders.foldl max 0 + +/-- Sum of X-adic orders. -/ +def totalOrder [Zero F] (problem : XAdicProblem F) : Nat := + problem.orders.foldl (fun acc order ↦ acc + order) 0 + +/-- Truncate every problem order to at most `d`. -/ +def lowerOrders [Zero F] (problem : XAdicProblem F) (d : Nat) : Array Nat := + problem.orders.map fun order ↦ min order d + +/-- Remaining orders after the first `d` coefficients have been consumed. -/ +def residualOrders [Zero F] (problem : XAdicProblem F) (d : Nat) : Array Nat := + problem.orders.map fun order ↦ order - d + +/-- Shift update used by the second recursive PM-basis call. -/ +def updateShiftByRows [Zero F] [BEq F] + (basis : PolynomialMatrix F) (shift : Array Nat) : Array Nat := + (List.range basis.size).map + (fun i ↦ + match rowShiftedDegree? (basis.getD i #[]) shift with + | none => shift.getD i 0 + | some degree => degree) |>.toArray + +/-- Residual matrix `(P * A) div X^d`, using an explicit product kernel and +truncating to the requested residual orders columnwise. -/ +def residualMatrixWithProduct [Semiring F] [BEq F] [LawfulBEq F] + (productKernel : + Array Nat → PolynomialMatrix F → PolynomialMatrix F → PolynomialMatrix F) + (basis : PolynomialMatrix F) (matrix : PolynomialMatrix F) + (d : Nat) (orders : Array Nat) : PolynomialMatrix F := + let product := productKernel (orders.map fun order ↦ order + d) basis matrix + PolynomialMatrix.ofFn product.size (PolynomialMatrix.MatrixWidth product) fun i j ↦ + PolynomialMatrix.divXTrunc d (orders.getD j 0) + (PolynomialMatrix.rowGet (product.getD i #[]) j) + +/-- Residual matrix `(P * A) div X^d`, truncated to the requested residual +orders columnwise, using the direct low-product row-column kernel. -/ +def residualMatrix [Semiring F] [BEq F] [LawfulBEq F] + (lowCtx : PolynomialMatrix.MulLowContext F) + (basis : PolynomialMatrix F) (matrix : PolynomialMatrix F) + (d : Nat) (orders : Array Nat) : PolynomialMatrix F := + residualMatrixWithProduct (PolynomialMatrix.mulTruncColumnWith lowCtx) + basis matrix d orders + +end Approximant + +end PolynomialMatrix + +end CompPoly diff --git a/CompPoly/LinearAlgebra/PolynomialMatrix/Approximant/Correctness.lean b/CompPoly/LinearAlgebra/PolynomialMatrix/Approximant/Correctness.lean new file mode 100644 index 00000000..143d52b3 --- /dev/null +++ b/CompPoly/LinearAlgebra/PolynomialMatrix/Approximant/Correctness.lean @@ -0,0 +1,70 @@ +/- +Copyright (c) 2026 CompPoly Contributors. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Valerii Huhnin +-/ + +module + +public import CompPoly.LinearAlgebra.PolynomialMatrix.Approximant.ModularEquation + +/-! +# Approximant-Basis Correctness Surface + +Named theorem surface for X-adic approximant bases and diagonal modular +solution bases. The executable contexts carry the current proof obligations. +-/ + +@[expose] public section + +namespace CompPoly + +namespace PolynomialMatrix + +namespace Approximant + +variable {F : Type*} [Field F] [BEq F] [LawfulBEq F] + +/-- Rows returned by a modular solution-basis context satisfy the modular +equation. -/ +theorem modularSolutionBasis_sound + (ctx : ModularSolutionBasisContext F) (equation : ModularEquation F) + (shift : Array Nat) (degreeBound? : Option Nat) {row : PolynomialRow F} + (hrow : row ∈ MatrixRows (ctx.solutionBasis equation shift degreeBound?)) : + rowSatisfiesModularBool ctx.mulContext ctx.modContext row + equation.matrix equation.moduli = true := + ctx.sound equation shift degreeBound? row hrow + +/-- Solution-basis completeness/minimality contract, relative to the +caller-supplied degree bound: a solution row within the bound (vacuous for +`none`) is matched by a returned basis row whose shifted degree does not +exceed the bound — the solution's own degree when no bound is supplied. -/ +theorem modularSolutionBasis_complete_minimal + (ctx : ModularSolutionBasisContext F) (equation : ModularEquation F) + (shift : Array Nat) (degreeBound? : Option Nat) {row : PolynomialRow F} + {rowDegree : Nat} + (hmonic : ∀ b, b < equation.moduli.size → (equation.moduli.getD b 0).monic) + (hcols : equation.moduli.size ≤ MatrixWidth equation.matrix) + (hshift : shift.size = equation.solutionWidth) + (hrow : + rowSatisfiesModularBool ctx.mulContext ctx.modContext row + equation.matrix equation.moduli = true) + (hnonzero : rowIsZero row = false) + (hwidth : row.size ≤ equation.solutionWidth) + (hdegree : rowShiftedDegree? row shift = some rowDegree) + (hbound : ∀ bound, degreeBound? = some bound → rowDegree ≤ bound) : + (∀ basisRow, + basisRow ∈ MatrixRows (ctx.solutionBasis equation shift degreeBound?) → + basisRow.size ≤ equation.solutionWidth) ∧ + ∃ basisRow degree, + basisRow ∈ MatrixRows (ctx.solutionBasis equation shift degreeBound?) ∧ + rowShiftedDegree? basisRow shift = some degree ∧ + degree ≤ degreeBound?.getD rowDegree := + ctx.complete_minimal equation shift degreeBound? row rowDegree hmonic hcols + hshift hrow hnonzero hwidth hdegree hbound + +end Approximant + +end PolynomialMatrix + +end CompPoly diff --git a/CompPoly/LinearAlgebra/PolynomialMatrix/Approximant/ModularEquation.lean b/CompPoly/LinearAlgebra/PolynomialMatrix/Approximant/ModularEquation.lean new file mode 100644 index 00000000..5927347e --- /dev/null +++ b/CompPoly/LinearAlgebra/PolynomialMatrix/Approximant/ModularEquation.lean @@ -0,0 +1,242 @@ +/- +Copyright (c) 2026 CompPoly Contributors. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Valerii Huhnin +-/ + +module + +public import CompPoly.LinearAlgebra.PolynomialMatrix.Approximant.ModularEquation.Completeness + +/-! +# Diagonal Modular Equation Solver Context + +Umbrella module for the diagonal modular-equation solver: definitions, +soundness, the completeness development, and the production +`ModularSolutionBasisContext` instance backed by the X-adic PM-basis. +-/ + +@[expose] public section + +namespace CompPoly + +namespace PolynomialMatrix + +namespace Approximant + +variable {F : Type*} [Field F] [BEq F] [LawfulBEq F] + +/-- Diagonal modular-equation solution-basis context obtained from the +exact-nullspace lift and an X-adic PM-basis context. -/ +def modularSolutionBasisContextViaPMBasis + (mulCtx : CPolynomial.MulContext F) (modCtx : CPolynomial.ModContext F) + (pmCtx : PMBasisContext F) : ModularSolutionBasisContext F where + mulContext := mulCtx + modContext := modCtx + solutionBasis := filteredSolutionBasisViaPMBasis mulCtx modCtx pmCtx + sound := by + intro equation shift degreeBound? row hrow + exact filteredSolutionBasisViaPMBasis_sound hrow + complete_minimal := by + -- With the degree gate passed, the adaptive best itself meets the bound, + -- so the adaptive candidate set suffices. Otherwise the certified gated + -- window takes over: solutions at or above the best adaptive degree are + -- dominated by the adaptive rows of `combined`; solutions inside the + -- window are dominated by a verification row through + -- `me_verification_dominates`, with the fallback solution + -- `e_p * prod(moduli)` covering degrees beyond the saturated window. + intro equation shift degreeBound? row d hmonic hcols hshift hsat hnz hwidth + hd hdbound + classical + obtain ⟨j0, hj0, hj0ne⟩ := exists_nonzero_entry_of_rowIsZero_false hnz + have hpos : 0 < equation.solutionWidth := by omega + cases hgate : degreeGatePassed degreeBound? + (leastSolutionRowDegree? + (adaptiveSolutionBasis mulCtx modCtx pmCtx equation shift degreeBound?).filtered + shift) with + | true => + -- Gate passed: the adaptive candidate set is returned, and its least + -- row meets the caller's bound. + obtain ⟨bound, best, hBound, hBest, hle⟩ := + degreeGatePassed_eq_true_iff.mp hgate + have hresult : filteredSolutionBasisViaPMBasis mulCtx modCtx pmCtx + equation shift degreeBound? = + (adaptiveSolutionBasis mulCtx modCtx pmCtx equation shift degreeBound?).filtered := by + simp only [filteredSolutionBasisViaPMBasis] + rw [if_pos hgate] + rw [leastSolutionRowDegree?] at hBest + rcases Option.map_eq_some_iff.mp hBest with ⟨choice, hchoice, hchoicedeg⟩ + obtain ⟨hidx, hrowEq, hcdeg⟩ := leastShiftedDegreeChoice?_some_valid hchoice + refine ⟨?_, choice.row, choice.degree, ?_, hcdeg, ?_⟩ + · intro basisRow hbasisRow + rw [hresult] at hbasisRow + exact le_of_eq (me_adaptiveBasis_width basisRow hbasisRow) + · rw [hresult, MatrixRows, hrowEq] + exact me_getD_mem_toList #[] hidx + · rw [hBound, Option.getD_some] + omega + | false => + -- Gate failed or no bound supplied: the certified union is returned. + have hmain : ∃ basisRow degree, + basisRow ∈ MatrixRows + ((adaptiveSolutionBasis mulCtx modCtx pmCtx equation shift degreeBound?).filtered ++ + filterModularSolutionRows mulCtx modCtx equation + (windowedSolutionBasisViaPMBasis modCtx pmCtx equation shift + (gatedWindowBound equation shift degreeBound? + (leastSolutionRowDegree? + (adaptiveSolutionBasis mulCtx modCtx pmCtx equation + shift degreeBound?).filtered shift)))) ∧ + rowShiftedDegree? basisRow shift = some degree ∧ + degree ≤ degreeBound?.getD d := by + cases hbnd : degreeBound? with + | some bound => + -- Bounded window `min(bound, cap) + maxShift`. + have hdb : d ≤ bound := hdbound bound hbnd + simp only [gatedWindowBound, Option.getD_some] + by_cases hdw : d ≤ + min bound (pivotWindowCap equation) + maxShiftDegree shift + · -- The solution row itself fits the gated window. + obtain ⟨bRow, degB, hmem, hdegB, hleB⟩ := me_verification_dominates + mulCtx modCtx pmCtx equation shift + (min bound (pivotWindowCap equation) + maxShiftDegree shift) + hmonic hcols hshift hpos hsat hnz hwidth hd hdw + refine ⟨bRow, degB, ?_, hdegB, by omega⟩ + rw [MatrixRows, Array.toList_append] + exact List.mem_append.mpr (Or.inr hmem) + · -- Beyond the window the bound forces saturation at the full + -- pivot window, where the fallback row dominates. + obtain ⟨prow, e, hsatP, hnzP, hsizeP, hdegP, heP⟩ := + me_prodRow_facts mulCtx modCtx equation shift (p := j0) hmonic + hcols (by omega) + obtain ⟨bRow, degB, hmem, hdegB, hleB⟩ := me_verification_dominates + mulCtx modCtx pmCtx equation shift + (min bound (pivotWindowCap equation) + maxShiftDegree shift) + hmonic hcols hshift hpos hsatP hnzP (le_of_eq hsizeP) hdegP + (by omega) + refine ⟨bRow, degB, ?_, hdegB, by omega⟩ + rw [MatrixRows, Array.toList_append] + exact List.mem_append.mpr (Or.inr hmem) + | none => + simp only [gatedWindowBound, Option.getD_none] + by_cases hcase : ∃ B, leastSolutionRowDegree? + (adaptiveSolutionBasis mulCtx modCtx pmCtx equation shift none).filtered + shift = some B ∧ B ≤ d + · -- An adaptive row already dominates. + obtain ⟨B, hB, hBd⟩ := hcase + rw [leastSolutionRowDegree?] at hB + rcases Option.map_eq_some_iff.mp hB with ⟨choice, hchoice, hchoicedeg⟩ + obtain ⟨hidx, hrowEq, hcdeg⟩ := leastShiftedDegreeChoice?_some_valid hchoice + refine ⟨choice.row, choice.degree, ?_, hcdeg, by omega⟩ + rw [MatrixRows, Array.toList_append] + refine List.mem_append.mpr (Or.inl ?_) + rw [hrowEq] + exact me_getD_mem_toList #[] hidx + · -- Route through the certified verification window. + push Not at hcase + have hwindow : ∃ (rowStar : PolynomialRow F) (e : Nat), + rowSatisfiesModularBool mulCtx modCtx rowStar equation.matrix + equation.moduli = true ∧ + rowIsZero rowStar = false ∧ + rowStar.size ≤ equation.solutionWidth ∧ + rowShiftedDegree? rowStar shift = some e ∧ + e ≤ verificationWindowBound equation shift + (leastSolutionRowDegree? + (adaptiveSolutionBasis mulCtx modCtx pmCtx equation + shift none).filtered shift) ∧ + e ≤ d := by + by_cases hdwindow : d ≤ verificationWindowBound equation shift + (leastSolutionRowDegree? + (adaptiveSolutionBasis mulCtx modCtx pmCtx equation + shift none).filtered shift) + · exact ⟨row, d, hsat, hnz, hwidth, hd, hdwindow, le_refl d⟩ + · -- The window is saturated at the full pivot window. + have hfull : verificationWindowBound equation shift + (leastSolutionRowDegree? + (adaptiveSolutionBasis mulCtx modCtx pmCtx equation + shift none).filtered shift) = + pivotWindowCap equation + maxShiftDegree shift := by + cases hbest : leastSolutionRowDegree? + (adaptiveSolutionBasis mulCtx modCtx pmCtx equation + shift none).filtered shift with + | none => + simp [verificationWindowBound, fullWindowDegreeBound] + | some B => + have hdB : d < B := hcase B hbest + rw [hbest] at hdwindow + simp only [verificationWindowBound] at hdwindow ⊢ + omega + obtain ⟨prow, e, hsatP, hnzP, hsizeP, hdegP, heP⟩ := + me_prodRow_facts mulCtx modCtx equation shift (p := j0) hmonic + hcols (by omega) + exact ⟨prow, e, hsatP, hnzP, le_of_eq hsizeP, hdegP, by omega, + by omega⟩ + obtain ⟨rowStar, e, hsatS, hnzS, hwidthS, hdegS, heB, heD⟩ := hwindow + obtain ⟨bRow, degB, hmem, hdegB, hleB⟩ := me_verification_dominates + mulCtx modCtx pmCtx equation shift + (verificationWindowBound equation shift + (leastSolutionRowDegree? + (adaptiveSolutionBasis mulCtx modCtx pmCtx equation + shift none).filtered shift)) + hmonic hcols hshift hpos hsatS hnzS hwidthS hdegS heB + refine ⟨bRow, degB, ?_, hdegB, by omega⟩ + rw [MatrixRows, Array.toList_append] + refine List.mem_append.mpr (Or.inr ?_) + exact hmem + obtain ⟨bRow, degB, hmemC, hdegB, hled⟩ := hmain + -- The combined candidate set is nonempty, so the repair branch is + -- skipped. + have hsizepos : 0 < + ((adaptiveSolutionBasis mulCtx modCtx pmCtx equation shift degreeBound?).filtered ++ + filterModularSolutionRows mulCtx modCtx equation + (windowedSolutionBasisViaPMBasis modCtx pmCtx equation shift + (gatedWindowBound equation shift degreeBound? + (leastSolutionRowDegree? + (adaptiveSolutionBasis mulCtx modCtx pmCtx equation + shift degreeBound?).filtered shift)))).size := by + rcases List.getElem_of_mem hmemC with ⟨i, hi, _⟩ + rw [MatrixRows, Array.length_toList] at hi + omega + have hresult : filteredSolutionBasisViaPMBasis mulCtx modCtx pmCtx equation + shift degreeBound? = + (adaptiveSolutionBasis mulCtx modCtx pmCtx equation shift degreeBound?).filtered ++ + filterModularSolutionRows mulCtx modCtx equation + (windowedSolutionBasisViaPMBasis modCtx pmCtx equation shift + (gatedWindowBound equation shift degreeBound? + (leastSolutionRowDegree? + (adaptiveSolutionBasis mulCtx modCtx pmCtx equation + shift degreeBound?).filtered shift))) := by + simp only [filteredSolutionBasisViaPMBasis] + rw [hgate] + simp only [Bool.false_eq_true, if_false] + rw [if_neg (by simp only [beq_iff_eq]; omega)] + constructor + · -- Width discipline of the returned rows. + intro basisRow hbasisRow + rw [hresult, MatrixRows, Array.toList_append, List.mem_append] at hbasisRow + rcases hbasisRow with h | h + · exact le_of_eq (me_adaptiveBasis_width basisRow h) + · have hsub := me_filterModularSolutionRows_subset h + have hsub' : basisRow ∈ MatrixRows (compactNonzeroRows + (principalSolutionRows equation.solutionWidth + (pmCtx.basis + (fullWindowExactNullspaceProblem modCtx equation + (gatedWindowBound equation shift degreeBound? + (leastSolutionRowDegree? + (adaptiveSolutionBasis mulCtx modCtx pmCtx equation + shift degreeBound?).filtered shift))) + (exactNullspaceShift shift equation.modularWidth + (gatedWindowBound equation shift degreeBound? + (leastSolutionRowDegree? + (adaptiveSolutionBasis mulCtx modCtx pmCtx equation + shift degreeBound?).filtered shift)))))) := hsub + exact le_of_eq + (me_principalSolutionRows_width (compactNonzeroRows_subset hsub')) + · refine ⟨bRow, degB, ?_, hdegB, hled⟩ + rw [hresult] + exact hmemC + +end Approximant + +end PolynomialMatrix + +end CompPoly diff --git a/CompPoly/LinearAlgebra/PolynomialMatrix/Approximant/ModularEquation/Basic.lean b/CompPoly/LinearAlgebra/PolynomialMatrix/Approximant/ModularEquation/Basic.lean new file mode 100644 index 00000000..76366b63 --- /dev/null +++ b/CompPoly/LinearAlgebra/PolynomialMatrix/Approximant/ModularEquation/Basic.lean @@ -0,0 +1,773 @@ +/- +Copyright (c) 2026 CompPoly Contributors. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Valerii Huhnin +-/ + +module + +-- `coeff`, `ofArray` and friends are declared in bare `public section`s, so their +-- bodies are opaque downstream; see `docs/wiki/module-system.md`. +import all CompPoly.Univariate.Basic +import all CompPoly.Univariate.Raw.Core +public import CompPoly.LinearAlgebra.PolynomialMatrix.Approximant.PartialLinearization +public import CompPoly.LinearAlgebra.PolynomialMatrix.RowSelection + +/-! +# Diagonal Modular Equations + +GS-independent solution-basis interface for systems +`p * matrix = 0 mod diag(moduli)`. + +The production solver follows the degree-first pattern: it solves chunked +exact-nullspace X-adic problems whose partial-linearization windows are grown +adaptively per principal coordinate, doubling only the windows of coordinates +whose shifted pivot degree has not been discovered yet. Because a window stops +growing once its coordinate's pivot is found, the total window mass stays +within a constant factor of the true pivot-degree mass, which is at most the +modulus degree mass `sigma`. Every round is therefore an X-adic problem with +`O(m)` chunk rows and total order `O(sigma)`, and the number of rounds is +logarithmic, preserving the `~O(m^(omega-1) * sigma)` solver target. + +The chunked X-adic problems are relaxations: their orders certify exactness +only for rows whose chunk coefficients stay below the chunk size, and the +relaxed kernel module also contains uncertified rows that can crowd exact +solutions out of the adaptive rounds. The solver therefore backs the adaptive +loop with a certified verification solve (`windowedSolutionBasisViaPMBasis`): +solutions at or above the adaptive best are dominated by the adaptive rows, +and solutions below it lie inside the certified window, whose orders rule out +uncertified rows. + +Callers that only need a witness under a known degree bound pass it as +`degreeBound?`; when the adaptive best already meets the bound the certified +solve is skipped entirely, and when it does not the certified window is shrunk +to the bound. Passing `none` requests a degree-minimal answer and always runs +the verification solve over the best-degree-shrunk window. + +## References + +* [Giorgi, P., Jeannerod, C.-P., and Villard, G., *On the complexity of + polynomial matrix computations*][GJV03] +* [Storjohann, A., *Notes on computing minimal approximant bases*][Sto06] +* [Chowdhury, M. F. I., Jeannerod, C.-P., Neiger, V., Schost, E., and + Villard, G., *Faster algorithms for multivariate interpolation with + multiplicities and simultaneous polynomial approximations*][CJNSV15] +-/ + +@[expose] public section + +namespace CompPoly + +namespace PolynomialMatrix + +namespace Approximant + +variable {F : Type*} [Field F] [BEq F] [LawfulBEq F] + +/-- Diagonal modular-equation data. Rows of `matrix` are solution coordinates; +columns are reduced independently by `moduli`. -/ +structure ModularEquation (F : Type*) [Zero F] where + moduli : Array (CPolynomial F) + matrix : PolynomialMatrix F + +/-- Number of principal solution coordinates. -/ +def ModularEquation.solutionWidth (equation : ModularEquation F) : Nat := + equation.matrix.size + +/-- Number of modular columns. -/ +def ModularEquation.modularWidth (equation : ModularEquation F) : Nat := + equation.moduli.size + +/-- Diagonal rows `-diag(moduli)` for the exact-nullspace lift. -/ +def negativeDiagonalRows (moduli : Array (CPolynomial F)) : PolynomialMatrix F := + ofFn moduli.size moduli.size fun i j ↦ + if i == j then -moduli.getD i 0 else 0 + +/-- Exact-nullspace lift `[F; -diag(M)]`. -/ +def exactNullspaceLift (equation : ModularEquation F) : PolynomialMatrix F := + equation.matrix ++ negativeDiagonalRows equation.moduli + +/-- Principal solution rows: keep the first `solutionWidth` entries of each +expanded nullspace row. -/ +def principalSolutionRows (solutionWidth : Nat) (basis : PolynomialMatrix F) : + PolynomialMatrix F := + basis.map fun row ↦ + (List.range solutionWidth).map (fun j ↦ rowGet row j) |>.toArray + +/-- Build the X-adic exact-nullspace problem used by the modular-equation +solver. -/ +def exactNullspaceProblem (equation : ModularEquation F) : XAdicProblem F := + { orders := linearizedOrders equation.solutionWidth equation.moduli + matrix := exactNullspaceLift equation } + +/-- Entry-aware X-adic orders for a chunked exact-nullspace lift. A balanced +in-window solution has chunk coefficients of degree below `delta`, and the +chunked principal entries of column `b` are reduced below `deg M_b`, so +`delta + maxEntryDeg + 1` low coefficients certify that its principal product +vanishes exactly. This is never larger than the generic +`deg M_b + delta + 1` order and is much smaller when the relation entries have +low degree. -/ +def chunkedLiftOrders (delta modularWidth : Nat) + (chunkedPrincipal : PolynomialMatrix F) : Array Nat := + (List.range modularWidth).map + (fun b ↦ + let maxEntryDegree := chunkedPrincipal.foldl + (fun acc row ↦ + let entry := rowGet row b + if entry == 0 then acc else max acc entry.natDegree) + 0 + delta + maxEntryDegree + 1) |>.toArray + +/-- Principal rows for the chunked exact-nullspace lift. Chunk row +`(coord, offset)` stores `X^offset` times the corresponding original relation +row, reduced columnwise by the diagonal moduli. Reducing keeps every entry of +column `b` below `deg M_b`, matching the `E * F mod M` expansion from the +design notes and keeping chunked entry degrees independent of the offsets. -/ +def chunkedPrincipalRows (modCtx : CPolynomial.ModContext F) + (equation : ModularEquation F) + (plan : PartialLinearizationPlan) : PolynomialMatrix F := + ofFn plan.chunks.size (MatrixWidth equation.matrix) fun i j ↦ + let chunk := plan.chunks.getD i { coord := 0, offset := 0 } + modByMonicWith modCtx + (shiftPolynomialX chunk.offset + (rowGet (equation.matrix.getD chunk.coord #[]) j)) + (equation.moduli.getD j 0) + +/-- Chunked exact-nullspace lift for a partial-linearization plan. -/ +def chunkedExactNullspaceLift (modCtx : CPolynomial.ModContext F) + (equation : ModularEquation F) + (plan : PartialLinearizationPlan) : PolynomialMatrix F := + chunkedPrincipalRows modCtx equation plan ++ negativeDiagonalRows equation.moduli + +/-- Build the X-adic exact-nullspace problem after principal-coordinate chunk +expansion, with generic partial-linearization orders. -/ +def chunkedExactNullspaceProblem (modCtx : CPolynomial.ModContext F) + (equation : ModularEquation F) + (plan : PartialLinearizationPlan) : XAdicProblem F := + { orders := linearizedOrders equation.solutionWidth equation.moduli + matrix := chunkedExactNullspaceLift modCtx equation plan } + +/-- Build the chunked X-adic exact-nullspace problem with entry-aware orders. +The `shift` argument is kept for call-site symmetry; orders depend only on the +chunked entry degrees and the chunk size. -/ +def chunkedExactNullspaceProblemForShift (modCtx : CPolynomial.ModContext F) + (equation : ModularEquation F) + (plan : PartialLinearizationPlan) (_shift : Array Nat) : XAdicProblem F := + let principal := chunkedPrincipalRows modCtx equation plan + { orders := chunkedLiftOrders plan.delta equation.modularWidth principal + matrix := principal ++ negativeDiagonalRows equation.moduli } + +/-- Shifted pivot-degree profile discovered for the principal solution +coordinates. `none` means that the discovery pass did not see a row pivoting in +that coordinate, so partial linearization uses its conservative fallback. -/ +structure PivotDegreeProfile where + degrees : Array (Option Nat) +deriving Repr, BEq + +/-- Empty shifted pivot-degree profile for a fixed principal width. -/ +def emptyPivotDegreeProfile (solutionWidth : Nat) : PivotDegreeProfile := + { degrees := Array.replicate solutionWidth none } + +/-- Insert a discovered pivot degree, keeping the smallest degree for each +principal leading position. -/ +def PivotDegreeProfile.insert (profile : PivotDegreeProfile) + (position degree : Nat) : PivotDegreeProfile := + let current := profile.degrees.getD position none + let next := + match current with + | none => degree + | some old => min old degree + { degrees := profile.degrees.setIfInBounds position (some next) } + +/-- Whether the profile has discovered a pivot degree for every coordinate. -/ +def PivotDegreeProfile.coversAll (profile : PivotDegreeProfile) : Bool := + profile.degrees.all fun degree ↦ degree.isSome + +/-- Whether the profile has discovered a pivot degree for any coordinate. -/ +def PivotDegreeProfile.discoveredAny (profile : PivotDegreeProfile) : Bool := + profile.degrees.any fun degree ↦ degree.isSome + +/-- Merge the principal pivot degrees observed in `rows` into a profile. -/ +def pivotDegreeProfileMergeRows (profile : PivotDegreeProfile) + (solutionWidth : Nat) (rows : PolynomialMatrix F) (shift : Array Nat) : + PivotDegreeProfile := + (List.range rows.size).foldl + (fun profile i ↦ + let row := rows.getD i #[] + match rowShiftedLeadingPosition? row shift, rowShiftedDegree? row shift with + | some position, some degree => + if position < solutionWidth then + profile.insert position degree + else + profile + | _, _ => profile) + profile + +/-- Discover principal pivot degrees from compressed candidate rows. -/ +def pivotDegreeProfileFromRows (solutionWidth : Nat) + (rows : PolynomialMatrix F) (shift : Array Nat) : + PivotDegreeProfile := + pivotDegreeProfileMergeRows (emptyPivotDegreeProfile solutionWidth) + solutionWidth rows shift + +/-- Solve a diagonal modular equation with a supplied partial-linearization plan. -/ +def solutionBasisWithPlanViaPMBasis (modCtx : CPolynomial.ModContext F) + (pmCtx : PMBasisContext F) + (equation : ModularEquation F) (shift : Array Nat) + (plan : PartialLinearizationPlan) : PolynomialMatrix F := + let xadic := chunkedExactNullspaceProblemForShift modCtx equation plan shift + let expandedShift := chunkedExactNullspaceShift plan shift + compactNonzeroRows (compressChunkedPrincipalRows plan (pmCtx.basis xadic expandedShift)) + +/-- Solve once with the conservative shift-spread window. This is a debug +entry point only: its chunk count can grow quadratically in the module width +for spread-out shifts, so the production solver uses the adaptive +window-escalation loop instead. -/ +def solutionBasisViaPMBasis (modCtx : CPolynomial.ModContext F) + (pmCtx : PMBasisContext F) + (equation : ModularEquation F) (shift : Array Nat) : PolynomialMatrix F := + let plan := partialLinearizationPlan equation.solutionWidth equation.modularWidth + equation.moduli shift + solutionBasisWithPlanViaPMBasis modCtx pmCtx equation shift plan + +/-- Known-degree reconstruction pass: build the partial-linearization plan from +the discovered pivot degrees and solve the X-adic problem directly for that +profile. -/ +def knownDegreeSolutionBasisViaPMBasis (modCtx : CPolynomial.ModContext F) + (pmCtx : PMBasisContext F) + (equation : ModularEquation F) (shift : Array Nat) + (profile : PivotDegreeProfile) : PolynomialMatrix F := + let plan := partialLinearizationPlanFromPivotDegrees equation.solutionWidth + equation.modularWidth equation.moduli shift profile.degrees + solutionBasisWithPlanViaPMBasis modCtx pmCtx equation shift plan + +/-- Solve once without principal-coordinate chunking. This is an explicit +tiny-leaf/debug entry point; the default modular-equation context does not use +it as a production fallback. -/ +def unchunkedSolutionBasisViaPMBasis (modCtx : CPolynomial.ModContext F) + (pmCtx : PMBasisContext F) + (equation : ModularEquation F) (shift : Array Nat) : PolynomialMatrix F := + let plan := unchunkedPartialLinearizationPlan equation.solutionWidth + equation.modularWidth equation.moduli + solutionBasisWithPlanViaPMBasis modCtx pmCtx equation shift plan + +/-- Keep only rows that satisfy the original diagonal modular equation after +compression from the exact-nullspace / X-adic bridge. -/ +def filterModularSolutionRows + (mulCtx : CPolynomial.MulContext F) (modCtx : CPolynomial.ModContext F) + (equation : ModularEquation F) (rows : PolynomialMatrix F) : + PolynomialMatrix F := + rows.filter fun row ↦ + if rowIsZero row then + false + else + rowSatisfiesModularBool mulCtx modCtx row equation.matrix equation.moduli + +/-- Residual rows `B * F mod diag(M)` for candidate solution rows `B`. -/ +def modularResidualRows + (mulCtx : CPolynomial.MulContext F) (modCtx : CPolynomial.ModContext F) + (equation : ModularEquation F) (rows : PolynomialMatrix F) : + PolynomialMatrix F := + modDiagonalWith modCtx equation.moduli + (mulWith mulCtx rows equation.matrix) + +/-- Shift used for polynomial combinations of compressed candidate rows. The +degree of a coefficient multiplying row `i` is measured relative to the shifted +degree already carried by that candidate row. -/ +def candidateRowShift (rows : PolynomialMatrix F) (shift : Array Nat) : Array Nat := + updateShiftByRows rows shift + +/-- Upper bound for the adaptive search window above each coordinate's shift. +The row `e_j * lcm(moduli)` is always a solution and `deg lcm(moduli)` is at +most the modulus degree mass, so every coordinate's minimal shifted pivot +degree is within this window. -/ +def pivotWindowCap (equation : ModularEquation F) : Nat := + modulusDegreeMass equation.moduli + +/-- Coefficient-degree bound for the full-window fallback problem. Any row of +the lifted module whose shifted degree does not exceed the shifted degree of an +in-window solution has plain coefficient degrees at most +`pivotWindowCap + maxShiftDegree shift`: comparing two principal coordinates +costs at most the shift spread, and `pivotWindowCap` bounds every minimal +pivot degree. -/ +def fullWindowDegreeBound (equation : ModularEquation F) + (shift : Array Nat) : Nat := + pivotWindowCap equation + maxShiftDegree shift + +/-- Exact-nullspace lift with the relation entries reduced columnwise by the +moduli, so every principal entry of column `b` has degree below `deg M_b` and +the quotient coefficients of exact solutions stay below the solution degree. -/ +def reducedExactNullspaceLift (modCtx : CPolynomial.ModContext F) + (equation : ModularEquation F) : PolynomialMatrix F := + ofFn equation.solutionWidth equation.modularWidth + (fun i j ↦ + modByMonicWith modCtx (rowGet (equation.matrix.getD i #[]) j) + (equation.moduli.getD j 0)) ++ + negativeDiagonalRows equation.moduli + +/-- Unchunked exact-nullspace problem with orders certifying exactness across +the whole pivot window: a lifted row with principal coefficient degrees at most +`bound` and quotient coefficient degrees at most `bound + 1` produces column-`b` +products of degree below `deg M_b + bound + 2`, so vanishing to that X-adic +order forces the product to vanish exactly. Unlike the chunked relaxation, +this problem admits no uncertified rows below the in-window solution degrees, +at the cost of an order mass larger by a factor of the module width. -/ +def fullWindowExactNullspaceProblem (modCtx : CPolynomial.ModContext F) + (equation : ModularEquation F) (bound : Nat) : XAdicProblem F := + { orders := equation.moduli.map fun modulus ↦ modulus.natDegree + bound + 2 + matrix := reducedExactNullspaceLift modCtx equation } + +/-- Certified full-window solve: compute a minimal basis of the unchunked +exact-nullspace problem whose orders cover the entire pivot window, and keep +the principal columns. Every returned row that pivots at or below an in-window +solution degree is an exact modular solution, so this entry point cannot lose +the solution basis to uncertified low-degree rows. It is used as the fallback +when the chunked adaptive solver discovers nothing. -/ +def fullWindowSolutionBasisViaPMBasis (modCtx : CPolynomial.ModContext F) + (pmCtx : PMBasisContext F) (equation : ModularEquation F) + (shift : Array Nat) : PolynomialMatrix F := + let bound := fullWindowDegreeBound equation shift + let problem := fullWindowExactNullspaceProblem modCtx equation bound + let expandedShift := exactNullspaceShift shift equation.modularWidth bound + compactNonzeroRows + (principalSolutionRows equation.solutionWidth + (pmCtx.basis problem expandedShift)) + +/-- Certified verification window sized by the best exact degree already in +hand. Solutions at or above the best adaptive degree are dominated by the +adaptive result itself, so the certified window only has to cover the degrees +strictly below it; when the adaptive loop found nothing it falls back to the +full pivot window. -/ +def verificationWindowBound (equation : ModularEquation F) + (shift : Array Nat) (bestDegree? : Option Nat) : Nat := + match bestDegree? with + | none => fullWindowDegreeBound equation shift + | some best => min best (pivotWindowCap equation) + maxShiftDegree shift + +/-- Certified solve over an explicit coefficient-degree window: an unchunked +exact-nullspace problem whose orders certify exactness for every row dominated +by an in-window solution. Its order mass is `sigma + s * bound` instead of +the full window's `(s + 1) * sigma`, so when the window is shrunk to a +near-minimal degree this costs about one extra cheap round. -/ +def windowedSolutionBasisViaPMBasis (modCtx : CPolynomial.ModContext F) + (pmCtx : PMBasisContext F) (equation : ModularEquation F) + (shift : Array Nat) (bound : Nat) : PolynomialMatrix F := + compactNonzeroRows + (principalSolutionRows equation.solutionWidth + (pmCtx.basis (fullWindowExactNullspaceProblem modCtx equation bound) + (exactNullspaceShift shift equation.modularWidth bound))) + +/-- Certified verification solve over the best-degree-shrunk window sized by +`verificationWindowBound`. -/ +def verificationSolutionBasisViaPMBasis (modCtx : CPolynomial.ModContext F) + (pmCtx : PMBasisContext F) (equation : ModularEquation F) + (shift : Array Nat) (bestDegree? : Option Nat) : PolynomialMatrix F := + windowedSolutionBasisViaPMBasis modCtx pmCtx equation shift + (verificationWindowBound equation shift bestDegree?) + +/-- Whether a caller-supplied degree bound certifies the adaptive result: the +gate passes when the best exact degree already found does not exceed the +bound, in which case any solution within the bound is dominated by the +adaptive rows up to the bound itself and the certified verification solve is +unnecessary. -/ +def degreeGatePassed (degreeBound? bestDegree? : Option Nat) : Bool := + match degreeBound?, bestDegree? with + | some bound, some best => best ≤ bound + | _, _ => false + +/-- Verification window for a gated solve. With a caller-supplied degree +bound the window only has to cover solutions within the bound, so it is +`min(bound, cap) + maxShift`; without one it falls back to the best-degree +window of `verificationWindowBound`. -/ +def gatedWindowBound (equation : ModularEquation F) + (shift : Array Nat) (degreeBound? bestDegree? : Option Nat) : Nat := + match degreeBound? with + | some bound => min bound (pivotWindowCap equation) + maxShiftDegree shift + | none => verificationWindowBound equation shift bestDegree? + +/-- The degree gate passes exactly for a bound certified by an adaptive best. -/ +theorem degreeGatePassed_eq_true_iff {degreeBound? bestDegree? : Option Nat} : + degreeGatePassed degreeBound? bestDegree? = true ↔ + ∃ bound best, degreeBound? = some bound ∧ bestDegree? = some best ∧ + best ≤ bound := by + cases degreeBound? with + | none => simp [degreeGatePassed] + | some bound => + cases bestDegree? with + | none => simp [degreeGatePassed] + | some best => simp [degreeGatePassed] + +/-- Pivot-degree assignment for one adaptive round: discovered coordinates use +their observed pivot degrees, undiscovered coordinates use the current +escalation window above their shift entry. -/ +def adaptiveProfileDegrees (shift : Array Nat) (profile : PivotDegreeProfile) + (budgets : Array Nat) (solutionWidth : Nat) : Array (Option Nat) := + (List.range solutionWidth).map + (fun j ↦ + match profile.degrees.getD j none with + | some degree => some degree + | none => some (shift.getD j 0 + budgets.getD j 0)) |>.toArray + +/-- Least shifted degree among accumulated solution rows. -/ +def leastSolutionRowDegree? (rows : PolynomialMatrix F) (shift : Array Nat) : + Option Nat := + (leastShiftedDegreeChoice? rows shift).map fun choice ↦ choice.degree + +/-- A coordinate needs no wider search window once its pivot degree is +discovered, its window has reached the cap, or its window already covers every +degree below the best solution row found so far. The last rule is what keeps +the loop from growing windows for coordinates that cannot improve the answer: +a row pivoting at `j` with shifted degree below the current best would lie +inside the already-searched window. Improving on `best` needs a row of degree +at most `best - 1`, so the window `shift[j] + budget[j]` suffices once +`best <= shift[j] + budget[j] + 1`. -/ +def coordinateSettled (profile : PivotDegreeProfile) (budgets : Array Nat) + (cap : Nat) (bestDegree? : Option Nat) (shift : Array Nat) (j : Nat) : Bool := + (profile.degrees.getD j none).isSome || + cap ≤ budgets.getD j 0 || + (match bestDegree? with + | some best => best ≤ shift.getD j 0 + budgets.getD j 0 + 1 + | none => false) + +/-- Whether every principal coordinate is settled for the current windows. -/ +def allCoordinatesSettled (profile : PivotDegreeProfile) (budgets : Array Nat) + (cap : Nat) (bestDegree? : Option Nat) (shift : Array Nat) + (solutionWidth : Nat) : Bool := + (List.range solutionWidth).all fun j ↦ + coordinateSettled profile budgets cap bestDegree? shift j + +/-- Double the escalation windows of coordinates that are not settled, clamped +at the window cap. Settled coordinates keep their window so the total window +mass stays within a constant factor of the useful pivot-degree mass. -/ +def escalateUnsettledBudgets (profile : PivotDegreeProfile) + (budgets : Array Nat) (cap : Nat) (bestDegree? : Option Nat) + (shift : Array Nat) : Array Nat := + (List.range budgets.size).map + (fun j ↦ + let budget := budgets.getD j 0 + if coordinateSettled profile budgets cap bestDegree? shift j then + budget + else + min cap (2 * max 1 budget)) |>.toArray + +/-- State carried between adaptive solution-basis rounds. `filtered` +accumulates the exact solution rows found across all rounds. -/ +structure AdaptiveSolveState (F : Type*) [Zero F] where + profile : PivotDegreeProfile + budgets : Array Nat + filtered : PolynomialMatrix F + raw : PolynomialMatrix F + +/-- One adaptive round: solve the chunked exact-nullspace problem for the +current window assignment, keep the rows that satisfy the original diagonal +congruences, and merge the observed shifted pivot degrees into the profile. -/ +def adaptiveSolutionRound + (mulCtx : CPolynomial.MulContext F) (modCtx : CPolynomial.ModContext F) + (pmCtx : PMBasisContext F) (equation : ModularEquation F) + (shift : Array Nat) (state : AdaptiveSolveState F) : AdaptiveSolveState F := + let degrees := adaptiveProfileDegrees shift state.profile state.budgets + equation.solutionWidth + let plan := partialLinearizationPlanFromPivotDegrees equation.solutionWidth + equation.modularWidth equation.moduli shift degrees + let rows := solutionBasisWithPlanViaPMBasis modCtx pmCtx equation shift plan + let filtered := filterModularSolutionRows mulCtx modCtx equation rows + { profile := pivotDegreeProfileMergeRows state.profile equation.solutionWidth + filtered shift + budgets := state.budgets + filtered := state.filtered ++ filtered + raw := rows } + +/-- Fuel-bounded adaptive window-escalation loop. Rounds stop as soon as the +caller's degree bound is already met by the best exact row found, or every +principal coordinate is settled: discovered, saturated, or unable to beat the +best solution row already in hand. -/ +def adaptiveSolutionLoop + (mulCtx : CPolynomial.MulContext F) (modCtx : CPolynomial.ModContext F) + (pmCtx : PMBasisContext F) (equation : ModularEquation F) + (shift : Array Nat) (degreeBound? : Option Nat) (cap : Nat) : + Nat → AdaptiveSolveState F → AdaptiveSolveState F + | 0, state => state + | fuel + 1, state => + let next := adaptiveSolutionRound mulCtx modCtx pmCtx equation shift state + let bestDegree? := leastSolutionRowDegree? next.filtered shift + if next.filtered.size == 0 && !next.profile.discoveredAny then + -- Zero discovery means the chunked relaxation is dominated by + -- uncertified rows below the solution degrees; growing the windows + -- multiplies the round cost without producing new pivot information, + -- so stop here and let the caller run the certified full-window + -- fallback instead. + next + else if degreeGatePassed degreeBound? bestDegree? then + -- The best exact row already meets the caller's degree bound, so the + -- gated solver returns the accumulated candidate set as-is; further + -- escalation rounds cannot change anything the caller observes. + next + else if allCoordinatesSettled next.profile next.budgets cap bestDegree? shift + equation.solutionWidth then + next + else + let escalated := escalateUnsettledBudgets next.profile next.budgets cap + bestDegree? shift + if escalated == next.budgets then + next + else + adaptiveSolutionLoop mulCtx modCtx pmCtx equation shift degreeBound? cap + fuel { next with budgets := escalated } + +/-- Run the adaptive degree-first solver: discover the shifted pivot-degree +profile with geometrically growing per-coordinate windows, where the final +round doubles as the known-degree reconstruction for all discovered +coordinates. The initial window is one chunk per coordinate. A caller with +a degree bound stops the escalation as soon as the bound is met. -/ +def adaptiveSolutionBasis + (mulCtx : CPolynomial.MulContext F) (modCtx : CPolynomial.ModContext F) + (pmCtx : PMBasisContext F) (equation : ModularEquation F) + (shift : Array Nat) (degreeBound? : Option Nat) : AdaptiveSolveState F := + let delta := chunkDelta equation.solutionWidth equation.moduli + let cap := max delta (pivotWindowCap equation) + let fuel := Nat.log2 (max 1 cap) + 2 + adaptiveSolutionLoop mulCtx modCtx pmCtx equation shift degreeBound? cap fuel + { profile := emptyPivotDegreeProfile equation.solutionWidth + budgets := Array.replicate equation.solutionWidth (max 1 (delta - 1)) + filtered := #[] + raw := #[] } + +/-- Discover the shifted pivot-degree profile through the adaptive solver. -/ +def discoverPivotDegreeProfileViaPMBasis + (mulCtx : CPolynomial.MulContext F) (modCtx : CPolynomial.ModContext F) + (pmCtx : PMBasisContext F) (equation : ModularEquation F) + (shift : Array Nat) : PivotDegreeProfile := + (adaptiveSolutionBasis mulCtx modCtx pmCtx equation shift none).profile + +/-- One residual reconstruction pass. If compressed rows `B` are not themselves +exact modular solutions, solve for polynomial combinations `C` such that +`C * (B * F mod M) = 0 mod M`, then return `C * B`. The residual equation is +solved with the same adaptive solver, without a further repair recursion. + +The candidate rows are first reduced to one representative per shifted leading +position. The reduction steps are unimodular, so the generated row module is +unchanged, while the residual equation's solution width stays bounded by the +principal width instead of the raw candidate count; without this bound the +repair solve can be quadratically wider than the original equation. -/ +def repairSolutionRowsViaPMBasis + (mulCtx : CPolynomial.MulContext F) (modCtx : CPolynomial.ModContext F) + (pmCtx : PMBasisContext F) (equation : ModularEquation F) + (shift : Array Nat) (rows : PolynomialMatrix F) : PolynomialMatrix F := + let reduced := compactNonzeroRows (reduceKernelLeafRowsByPivots rows shift) + let residualEquation : ModularEquation F := + { moduli := equation.moduli + matrix := modularResidualRows mulCtx modCtx equation reduced } + let repairState := adaptiveSolutionBasis mulCtx modCtx pmCtx residualEquation + (candidateRowShift reduced shift) none + PolynomialMatrix.mulStrassenWith pmCtx.runtime.lowMulContext + pmCtx.runtime.leafCutoff repairState.filtered reduced + +/-- Debug helper for tiny problems that intentionally disables +principal-coordinate chunking, keeping an unchunked solve available for +inspection without letting the production context bypass partial +linearization. -/ +def debugUnchunkedFilteredSolutionBasisViaPMBasis + (mulCtx : CPolynomial.MulContext F) (modCtx : CPolynomial.ModContext F) + (pmCtx : PMBasisContext F) (equation : ModularEquation F) + (shift : Array Nat) : PolynomialMatrix F := + filterModularSolutionRows mulCtx modCtx equation + (unchunkedSolutionBasisViaPMBasis modCtx pmCtx equation shift) + +/-- Known-degree reconstruction followed by the original diagonal-equation +guard. The reconstruction plan is built from discovered shifted pivot degrees, +solved as a chunked exact-nullspace PM-basis problem, and compressed back to the +principal solution coordinates. -/ +def knownDegreeFilteredSolutionBasisViaPMBasis + (mulCtx : CPolynomial.MulContext F) (modCtx : CPolynomial.ModContext F) + (pmCtx : PMBasisContext F) (equation : ModularEquation F) + (shift : Array Nat) (profile : PivotDegreeProfile) : PolynomialMatrix F := + let rows := knownDegreeSolutionBasisViaPMBasis modCtx pmCtx equation shift profile + let filtered := filterModularSolutionRows mulCtx modCtx equation rows + if filtered.size == 0 then + filterModularSolutionRows mulCtx modCtx equation + (repairSolutionRowsViaPMBasis mulCtx modCtx pmCtx equation shift rows) + else + filtered + +/-- Solver exposed through the modular-equation context: run the adaptive +degree-first window-escalation loop, whose final round is the known-degree +reconstruction for every discovered coordinate, then back it with a certified +verification solve. When the caller supplies a degree bound and the best +exact degree already found meets it, the adaptive candidate set is returned +as-is: any solution within the bound is then matched by an adaptive row up to +the bound, so the certified solve adds nothing the caller can observe. +Otherwise a certified verification solve runs over the gated window — +`min(bound, cap) + maxShift` with a bound, the best-degree-shrunk window +without one — and the union of both filtered candidate sets is returned: +solutions at or above the adaptive best are dominated by the adaptive rows; +solutions strictly below it lie inside the certified window, whose orders rule +out uncertified relaxed rows — so the union always contains an exact row of +minimal shifted degree, including in the partial-masking regime where +uncertified chunked-kernel rows crowd lower exact solutions out of every +adaptive round. The adaptive rows come first, so ties keep the adaptive +choice. The residual repair pass remains as a guard when both candidate sets +are empty. The filter, verification, and repair are semantic guards around +the exact-nullspace bridge; they do not call any alternate interpolation +backend. -/ +def filteredSolutionBasisViaPMBasis + (mulCtx : CPolynomial.MulContext F) (modCtx : CPolynomial.ModContext F) + (pmCtx : PMBasisContext F) (equation : ModularEquation F) + (shift : Array Nat) (degreeBound? : Option Nat) : PolynomialMatrix F := + let final := adaptiveSolutionBasis mulCtx modCtx pmCtx equation shift degreeBound? + let best? := leastSolutionRowDegree? final.filtered shift + if degreeGatePassed degreeBound? best? then + final.filtered + else + let verification := filterModularSolutionRows mulCtx modCtx equation + (windowedSolutionBasisViaPMBasis modCtx pmCtx equation shift + (gatedWindowBound equation shift degreeBound? best?)) + let combined := final.filtered ++ verification + if combined.size == 0 then + filterModularSolutionRows mulCtx modCtx equation + (repairSolutionRowsViaPMBasis mulCtx modCtx pmCtx equation shift final.raw) + else + combined + +/-- Rows kept by the modular solution filter satisfy the modular predicate. -/ +theorem rowSatisfiesModularBool_of_mem_filterModularSolutionRows + {mulCtx : CPolynomial.MulContext F} {modCtx : CPolynomial.ModContext F} + {equation : ModularEquation F} {rows : PolynomialMatrix F} + {row : PolynomialRow F} + (hrow : row ∈ MatrixRows (filterModularSolutionRows mulCtx modCtx equation rows)) : + rowSatisfiesModularBool mulCtx modCtx row equation.matrix equation.moduli = true := by + rw [MatrixRows, filterModularSolutionRows] at hrow + have hmem : row ∈ rows ∧ rowIsZero row = false ∧ + rowSatisfiesModularBool mulCtx modCtx row equation.matrix equation.moduli = true := by + simpa using hrow + exact hmem.2.2 + +/-- One adaptive round preserves modular soundness of the accumulated filtered +rows. -/ +theorem adaptiveSolutionRound_filtered_sound + {mulCtx : CPolynomial.MulContext F} {modCtx : CPolynomial.ModContext F} + {pmCtx : PMBasisContext F} {equation : ModularEquation F} + {shift : Array Nat} {state : AdaptiveSolveState F} + (hstate : ∀ row ∈ MatrixRows state.filtered, + rowSatisfiesModularBool mulCtx modCtx row equation.matrix equation.moduli = true) : + ∀ row ∈ MatrixRows + (adaptiveSolutionRound mulCtx modCtx pmCtx equation shift state).filtered, + rowSatisfiesModularBool mulCtx modCtx row equation.matrix equation.moduli = true := by + intro row hrow + simp only [adaptiveSolutionRound, MatrixRows, Array.toList_append, + List.mem_append] at hrow + rcases hrow with hold | hnew + · exact hstate row hold + · exact rowSatisfiesModularBool_of_mem_filterModularSolutionRows hnew + +/-- The adaptive escalation loop preserves modular soundness of the accumulated +filtered rows. -/ +theorem adaptiveSolutionLoop_filtered_sound + {mulCtx : CPolynomial.MulContext F} {modCtx : CPolynomial.ModContext F} + {pmCtx : PMBasisContext F} {equation : ModularEquation F} + {shift : Array Nat} {degreeBound? : Option Nat} {cap : Nat} : + ∀ (fuel : Nat) (state : AdaptiveSolveState F), + (∀ row ∈ MatrixRows state.filtered, + rowSatisfiesModularBool mulCtx modCtx row equation.matrix equation.moduli = true) → + ∀ row ∈ MatrixRows + (adaptiveSolutionLoop mulCtx modCtx pmCtx equation shift degreeBound? cap + fuel state).filtered, + rowSatisfiesModularBool mulCtx modCtx row equation.matrix equation.moduli = true := by + intro fuel + induction fuel with + | zero => + intro state hstate row hrow + exact hstate row hrow + | succ fuel ih => + intro state hstate row hrow + rw [adaptiveSolutionLoop] at hrow + have hnext := adaptiveSolutionRound_filtered_sound + (pmCtx := pmCtx) (shift := shift) (state := state) hstate + split at hrow + · exact hnext row hrow + · split at hrow + · exact hnext row hrow + · split at hrow + · exact hnext row hrow + · dsimp only [] at hrow + split at hrow + · exact hnext row hrow + · refine ih _ ?_ row hrow + intro r hr + exact hnext r hr + +/-- Rows accumulated by the adaptive solver satisfy the modular predicate. -/ +theorem adaptiveSolutionBasis_filtered_sound + {mulCtx : CPolynomial.MulContext F} {modCtx : CPolynomial.ModContext F} + {pmCtx : PMBasisContext F} {equation : ModularEquation F} + {shift : Array Nat} {degreeBound? : Option Nat} : + ∀ row ∈ MatrixRows + (adaptiveSolutionBasis mulCtx modCtx pmCtx equation shift + degreeBound?).filtered, + rowSatisfiesModularBool mulCtx modCtx row equation.matrix equation.moduli = true := by + rw [adaptiveSolutionBasis] + exact adaptiveSolutionLoop_filtered_sound _ _ + (by intro r hr; simp [MatrixRows] at hr) + +/-- Every row returned by the filtered PM-basis modular solver satisfies the +original diagonal modular equation. -/ +theorem filteredSolutionBasisViaPMBasis_sound + {mulCtx : CPolynomial.MulContext F} {modCtx : CPolynomial.ModContext F} + {pmCtx : PMBasisContext F} {equation : ModularEquation F} + {shift : Array Nat} {degreeBound? : Option Nat} {row : PolynomialRow F} + (hrow : row ∈ MatrixRows + (filteredSolutionBasisViaPMBasis mulCtx modCtx pmCtx equation shift + degreeBound?)) : + rowSatisfiesModularBool mulCtx modCtx row equation.matrix equation.moduli = true := by + simp only [filteredSolutionBasisViaPMBasis] at hrow + split at hrow + · exact adaptiveSolutionBasis_filtered_sound row hrow + · split at hrow + · exact rowSatisfiesModularBool_of_mem_filterModularSolutionRows hrow + · rw [MatrixRows, Array.toList_append, List.mem_append] at hrow + rcases hrow with hmem | hmem + · exact adaptiveSolutionBasis_filtered_sound row hmem + · exact rowSatisfiesModularBool_of_mem_filterModularSolutionRows hmem + +/-- Modular-equation solution-basis context with theorem fields. + +The solver takes an optional caller-supplied degree bound. The +completeness/minimality contract is bound-relative: for any nonzero in-width +solution row of the diagonal modular equation whose shifted degree fits the +bound (vacuous for `none`), the returned basis stays inside the principal +width and contains a row whose shifted degree does not exceed the bound — the +given solution's own degree when no bound is supplied. The contract assumes +monic moduli, a relation matrix wide enough to expose every modular column to +the executable row predicate, and a shift aligned with the principal solution +width. -/ +structure ModularSolutionBasisContext (F : Type*) [Field F] [BEq F] [LawfulBEq F] where + mulContext : CPolynomial.MulContext F + modContext : CPolynomial.ModContext F + solutionBasis : ModularEquation F → Array Nat → Option Nat → PolynomialMatrix F + sound : + ∀ equation shift degreeBound? row, + row ∈ MatrixRows (solutionBasis equation shift degreeBound?) → + rowSatisfiesModularBool mulContext modContext row equation.matrix equation.moduli = true + complete_minimal : + ∀ equation shift degreeBound? row rowDegree, + (∀ b, b < equation.moduli.size → (equation.moduli.getD b 0).monic) → + equation.moduli.size ≤ MatrixWidth equation.matrix → + shift.size = equation.solutionWidth → + rowSatisfiesModularBool mulContext modContext row equation.matrix + equation.moduli = true → + rowIsZero row = false → + row.size ≤ equation.solutionWidth → + rowShiftedDegree? row shift = some rowDegree → + (∀ bound, degreeBound? = some bound → rowDegree ≤ bound) → + (∀ basisRow, + basisRow ∈ MatrixRows (solutionBasis equation shift degreeBound?) → + basisRow.size ≤ equation.solutionWidth) ∧ + ∃ basisRow degree, + basisRow ∈ MatrixRows (solutionBasis equation shift degreeBound?) ∧ + rowShiftedDegree? basisRow shift = some degree ∧ + degree ≤ degreeBound?.getD rowDegree + +end Approximant + +end PolynomialMatrix + +end CompPoly diff --git a/CompPoly/LinearAlgebra/PolynomialMatrix/Approximant/ModularEquation/Completeness.lean b/CompPoly/LinearAlgebra/PolynomialMatrix/Approximant/ModularEquation/Completeness.lean new file mode 100644 index 00000000..d1cf4955 --- /dev/null +++ b/CompPoly/LinearAlgebra/PolynomialMatrix/Approximant/ModularEquation/Completeness.lean @@ -0,0 +1,1145 @@ +/- +Copyright (c) 2026 CompPoly Contributors. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Valerii Huhnin +-/ + +module + +-- `coeff`, `ofArray` and friends are declared in bare `public section`s, so their +-- bodies are opaque downstream; see `docs/wiki/module-system.md`. +import all CompPoly.Univariate.Basic +import all CompPoly.Univariate.Raw.Core +public import CompPoly.LinearAlgebra.PolynomialMatrix.Approximant.ModularEquation.Basic +public import CompPoly.Univariate.DivisionCorrectness + +/-! +# Filtered Modular Solver Completeness + +Completeness/minimality of the filtered PM-basis modular solver: every +nonzero in-width solution of a monic diagonal modular equation is dominated +by a returned row, via the certified verification window. +-/ + +@[expose] public section + +namespace CompPoly + +namespace PolynomialMatrix + +namespace Approximant + +variable {F : Type*} [Field F] [BEq F] [LawfulBEq F] + +/-! ## Completeness/minimality of the filtered PM-basis modular solver + +The argument routes every modular solution through the certified verification +window. A nonzero in-width solution row of shifted degree `e ≤ bound` lifts +to an exact row of the reduced exact-nullspace problem whose expanded shifted +degree is at most `e + bound + 1`. The PM-basis minimality contract yields a +basis row of dominated expanded degree, the verification orders force its +column products to vanish exactly, and its principal truncation is therefore a +modular solution of shifted degree at most `e` surviving both filters. +Solutions above the verification window are handled either by the adaptive +rows themselves or by falling back to the always-available solution +`e_p * prod(moduli)`, which fits the saturated window. -/ + +section CompleteMinimal + +/-! ### Generic access and summation helpers -/ + +private theorem me_getD_list_range_map {α : Type*} (g : Nat → α) (n j : Nat) (d : α) : + (((List.range n).map g).toArray).getD j d = if j < n then g j else d := by + rw [Array.getD_eq_getD_getElem?, List.getElem?_toArray, List.getElem?_map] + by_cases hj : j < n + · rw [List.getElem?_range hj, Option.map_some, Option.getD_some, if_pos hj] + · rw [List.getElem?_eq_none (by simpa using Nat.le_of_not_lt hj), Option.map_none, + Option.getD_none, if_neg hj] + +private theorem me_getD_append_left {α : Type*} {A B : Array α} {i : Nat} (d : α) + (hi : i < A.size) : + (A ++ B).getD i d = A.getD i d := by + rw [Array.getD_eq_getD_getElem?, Array.getD_eq_getD_getElem?, + Array.getElem?_append_left hi] + +private theorem me_getD_append_right {α : Type*} {A B : Array α} {i : Nat} (d : α) + (hi : A.size ≤ i) : + (A ++ B).getD i d = B.getD (i - A.size) d := by + rw [Array.getD_eq_getD_getElem?, Array.getD_eq_getD_getElem?, + Array.getElem?_append_right hi] + +private theorem me_getD_replicate {α : Type*} {n : Nat} (a d : α) {i : Nat} + (hi : i < n) : + (Array.replicate n a).getD i d = a := by + rw [Array.getD_eq_getD_getElem?, Array.getElem?_replicate, if_pos hi, Option.getD_some] + +/-- In-bounds `getD` values are list members. -/ +theorem me_getD_mem_toList {α : Type*} {xs : Array α} {i : Nat} (d : α) + (hi : i < xs.size) : xs.getD i d ∈ xs.toList := by + rw [Array.getD_eq_getD_getElem?, Array.getElem?_eq_getElem hi, Option.getD_some] + exact Array.getElem_mem_toList hi + +omit [BEq F] [LawfulBEq F] in +private theorem me_matrixWidth_eq_getD (M : PolynomialMatrix F) : + MatrixWidth M = (M.getD 0 #[]).size := by + unfold MatrixWidth + rw [Array.getD_eq_getD_getElem?] + cases M[0]? <;> rfl + +private theorem me_sum_range_add {M : Type*} [AddCommMonoid M] (f : Nat → M) + (m n : Nat) : + ∑ k ∈ Finset.range (m + n), f k = + (∑ k ∈ Finset.range m, f k) + ∑ k ∈ Finset.range n, f (m + k) := by + induction n with + | zero => simp + | succ n ih => + rw [show m + (n + 1) = (m + n) + 1 from rfl, Finset.sum_range_succ, ih, + Finset.sum_range_succ, add_assoc] + +private theorem me_toPoly_sum (f : Nat → CPolynomial F) (n : Nat) : + (∑ k ∈ Finset.range n, f k).toPoly = ∑ k ∈ Finset.range n, (f k).toPoly := by + induction n with + | zero => simp [CPolynomial.toPoly_zero] + | succ n ih => + rw [Finset.sum_range_succ, Finset.sum_range_succ, CPolynomial.toPoly_add, ih] + +/-- Column entries of a row-by-matrix product as `toPoly` sums over any index +range covering the row. The matrix multiplication context is irrelevant to +the value. -/ +private theorem me_rowMul_toPoly (mulCtx : CPolynomial.MulContext F) + (row : PolynomialRow F) (M : PolynomialMatrix F) {j n : Nat} + (hj : j < MatrixWidth M) (hn : row.size ≤ n) : + (rowGet (rowMulMatrixWith mulCtx row M) j).toPoly = + ∑ k ∈ Finset.range n, + (rowGet row k).toPoly * (rowGet (M.getD k #[]) j).toPoly := by + rw [rowGet_rowMulMatrixWith mulCtx row M hj, me_toPoly_sum] + rw [show ∑ k ∈ Finset.range row.size, + (rowGet row k * rowGet (M.getD k #[]) j).toPoly = + ∑ k ∈ Finset.range row.size, + (rowGet row k).toPoly * (rowGet (M.getD k #[]) j).toPoly from + Finset.sum_congr rfl fun k _ ↦ CPolynomial.toPoly_mul _ _] + refine Finset.sum_subset + (fun x hx ↦ Finset.mem_range.mpr + (lt_of_lt_of_le (Finset.mem_range.mp hx) hn)) + fun k _hk hknot ↦ ?_ + have hk : row.size ≤ k := by simpa using hknot + rw [rowGet_of_size_le hk, CPolynomial.toPoly_zero, zero_mul] + +omit [BEq F] [LawfulBEq F] in +private theorem me_natDegree_sum_le {n : Nat} (f : Nat → Polynomial F) {D : Nat} + (h : ∀ k, k < n → (f k).natDegree ≤ D) : + (∑ k ∈ Finset.range n, f k).natDegree ≤ D := by + induction n with + | zero => simp + | succ n ih => + rw [Finset.sum_range_succ] + refine le_trans (Polynomial.natDegree_add_le _ _) ?_ + exact max_le (ih fun k hk ↦ h k (by omega)) (h n (by omega)) + +omit [BEq F] [LawfulBEq F] in +private theorem me_eq_zero_of_X_pow_dvd_of_natDegree_lt {p : Polynomial F} {n : Nat} + (hdvd : (Polynomial.X : Polynomial F) ^ n ∣ p) (hdeg : p.natDegree < n) : + p = 0 := by + by_contra hp + have hle := Polynomial.natDegree_le_of_dvd hdvd hp + rw [Polynomial.natDegree_X_pow] at hle + omega + +/-! ### Zero-row and shifted-degree helpers -/ + +private theorem me_rowIsZero_false_of_entry {row : PolynomialRow F} {j : Nat} + (hj : j < row.size) (hne : rowGet row j ≠ 0) : + rowIsZero row = false := by + cases hzero : rowIsZero row with + | false => rfl + | true => + exfalso + refine hne (rowIsZero_iff.mp hzero (rowGet row j) ?_) + rw [rowGet] + exact me_getD_mem_toList 0 hj + +private theorem me_rowGet_eq_zero_of_rowIsZero {row : PolynomialRow F} + (h : rowIsZero row = true) (k : Nat) : rowGet row k = 0 := by + rcases Nat.lt_or_ge k row.size with hk | hk + · refine rowIsZero_iff.mp h (rowGet row k) ?_ + rw [rowGet] + exact me_getD_mem_toList 0 hk + · exact rowGet_of_size_le hk + +omit [LawfulBEq F] in +private theorem me_rowIsZero_of_forall {row : PolynomialRow F} + (h : ∀ j, j < row.size → rowGet row j = 0) : + RowIsZero row := by + intro p hp + rcases List.getElem_of_mem hp with ⟨j, hj, hget⟩ + have hj' : j < row.size := by simpa using hj + have hzero := h j hj' + rw [rowGet, Array.getD_eq_getD_getElem?, Array.getElem?_eq_getElem hj', + Option.getD_some] at hzero + rw [← hget] + simpa [Array.getElem_toList] using hzero + +/-- Nonzero rows have a shifted degree. -/ +theorem me_rowShiftedDegree_isSome {row : PolynomialRow F} {shift : Array Nat} + (hnz : rowIsZero row = false) : + ∃ d, rowShiftedDegree? row shift = some d := by + cases hdeg : rowShiftedDegree? row shift with + | none => + rw [rowIsZero_iff.mpr (rowShiftedDegree?_eq_none_iff.mp hdeg)] at hnz + simp at hnz + | some d => exact ⟨d, rfl⟩ + +private theorem me_shiftedEntryDegree_eq {row : PolynomialRow F} {shift : Array Nat} + {j : Nat} (hne : rowGet row j ≠ 0) : + shiftedEntryDegree? row shift j = + some ((rowGet row j).natDegree + shift.getD j 0) := by + rw [shiftedEntryDegree?, if_neg (by simpa using hne)] + +private theorem me_shiftedEntryDegree_some {row : PolynomialRow F} {shift : Array Nat} + {j e : Nat} (h : shiftedEntryDegree? row shift j = some e) : + rowGet row j ≠ 0 ∧ e = (rowGet row j).natDegree + shift.getD j 0 := by + by_cases hne : rowGet row j = 0 + · rw [shiftedEntryDegree?] at h + simp [hne] at h + · rw [me_shiftedEntryDegree_eq hne] at h + exact ⟨hne, (Option.some.inj h).symm⟩ + +private theorem me_entry_le_of_rowShiftedDegree {row : PolynomialRow F} + {shift : Array Nat} {d j : Nat} + (hdeg : rowShiftedDegree? row shift = some d) (hne : rowGet row j ≠ 0) : + (rowGet row j).natDegree + shift.getD j 0 ≤ d := by + have hj : j < row.size := by + by_contra hj + exact hne (rowGet_of_size_le (Nat.le_of_not_lt hj)) + exact shiftedEntryDegree?_le_of_rowShiftedDegree?_eq_some hdeg hj + (me_shiftedEntryDegree_eq hne) + +private theorem me_rowShiftedDegree_attained {row : PolynomialRow F} + {shift : Array Nat} {d : Nat} (hdeg : rowShiftedDegree? row shift = some d) : + ∃ j, j < row.size ∧ rowGet row j ≠ 0 ∧ + d = (rowGet row j).natDegree + shift.getD j 0 := by + rcases exists_shiftedEntryDegree?_eq_of_rowShiftedDegree?_eq_some hdeg with + ⟨j, hj, hentry⟩ + rcases me_shiftedEntryDegree_some hentry with ⟨hne, he⟩ + exact ⟨j, hj, hne, he⟩ + +/-! ### `maxShiftDegree` and modulus-product bounds -/ + +private theorem me_foldl_max_init_le (g : Nat → Nat) : + ∀ (l : List Nat) (acc : Nat), acc ≤ l.foldl (fun a i ↦ max a (g i)) acc := by + intro l + induction l with + | nil => intro acc; exact Nat.le_refl _ + | cons x l ih => + intro acc + exact le_trans (Nat.le_max_left acc (g x)) (ih (max acc (g x))) + +private theorem me_le_foldl_max (g : Nat → Nat) : + ∀ (l : List Nat) (acc : Nat) (x : Nat), x ∈ l → + g x ≤ l.foldl (fun a i ↦ max a (g i)) acc := by + intro l + induction l with + | nil => intro acc x hx; cases hx + | cons y l ih => + intro acc x hx + rcases List.mem_cons.mp hx with rfl | hx + · exact le_trans (Nat.le_max_right acc (g x)) (me_foldl_max_init_le g l _) + · exact ih _ x hx + +private theorem me_shift_le_maxShiftDegree (shift : Array Nat) (j : Nat) : + shift.getD j 0 ≤ maxShiftDegree shift := by + rw [maxShiftDegree] + rcases Nat.lt_or_ge j shift.size with hj | hj + · exact me_le_foldl_max (fun i ↦ shift.getD i 0) (List.range shift.size) 0 j + (List.mem_range.mpr hj) + · rw [Array.getD_eq_getD_getElem?, Array.getElem?_eq_none hj] + exact Nat.zero_le _ + +/-- Product of all moduli, used as the universal fallback solution entry. -/ +private def me_moduliProduct (moduli : Array (CPolynomial F)) : CPolynomial F := + moduli.foldl (fun acc m ↦ acc * m) 1 + +private theorem me_foldl_mul_toPoly : + ∀ (l : List (CPolynomial F)) (acc : CPolynomial F), + (l.foldl (fun a m ↦ a * m) acc).toPoly = + acc.toPoly * (l.map CPolynomial.toPoly).prod := by + intro l + induction l with + | nil => intro acc; simp + | cons m l ih => + intro acc + rw [List.foldl_cons, ih, CPolynomial.toPoly_mul, List.map_cons, List.prod_cons] + ring + +private theorem me_moduliProduct_toPoly (moduli : Array (CPolynomial F)) : + (me_moduliProduct moduli).toPoly = (moduli.toList.map CPolynomial.toPoly).prod := by + rw [me_moduliProduct, ← Array.foldl_toList, me_foldl_mul_toPoly, + CPolynomial.toPoly_one, one_mul] + +private theorem me_moduliProduct_dvd {moduli : Array (CPolynomial F)} {b : Nat} + (hb : b < moduli.size) : + (moduli.getD b 0).toPoly ∣ (me_moduliProduct moduli).toPoly := by + rw [me_moduliProduct_toPoly] + exact List.dvd_prod (List.mem_map.mpr ⟨moduli.getD b 0, me_getD_mem_toList 0 hb, rfl⟩) + +omit [BEq F] [LawfulBEq F] in +private theorem me_list_prod_ne_zero : + ∀ (l : List (Polynomial F)), (∀ p ∈ l, p ≠ 0) → l.prod ≠ 0 + | [], _ => by simp + | p :: l, h => by + rw [List.prod_cons] + exact mul_ne_zero (h p (by simp)) + (me_list_prod_ne_zero l fun q hq ↦ h q (by simp [hq])) + +private theorem me_moduliProduct_ne_zero {moduli : Array (CPolynomial F)} + (hmonic : ∀ b, b < moduli.size → (moduli.getD b 0).monic) : + me_moduliProduct moduli ≠ 0 := by + intro hzero + have htoPoly : (me_moduliProduct moduli).toPoly = 0 := by + rw [hzero, CPolynomial.toPoly_zero] + rw [me_moduliProduct_toPoly] at htoPoly + refine me_list_prod_ne_zero (moduli.toList.map CPolynomial.toPoly) ?_ htoPoly + intro p hp + rcases List.mem_map.mp hp with ⟨q, hq, rfl⟩ + rcases List.getElem_of_mem hq with ⟨b, hb, hget⟩ + have hb' : b < moduli.size := by simpa using hb + have hqd : moduli.getD b 0 = q := by + rw [Array.getD_eq_getD_getElem?, Array.getElem?_eq_getElem hb', Option.getD_some] + simpa [Array.getElem_toList] using hget + have hmq := hmonic b hb' + rw [hqd] at hmq + exact Polynomial.Monic.ne_zero ((CPolynomial.monic_toPoly_iff q).mp hmq) + +omit [BEq F] [LawfulBEq F] in +private theorem me_foldl_add_mono : + ∀ (l : List (CPolynomial F)) (a b : Nat), a ≤ b → + l.foldl (fun acc m ↦ acc + m.natDegree) a ≤ + l.foldl (fun acc m ↦ acc + m.natDegree) b := by + intro l + induction l with + | nil => intro a b h; exact h + | cons m l ih => intro a b h; exact ih _ _ (Nat.add_le_add_right h _) + +private theorem me_foldl_mul_natDegree_le : + ∀ (l : List (CPolynomial F)) (acc : CPolynomial F), + (l.foldl (fun a m ↦ a * m) acc).toPoly.natDegree ≤ + l.foldl (fun a m ↦ a + m.natDegree) acc.toPoly.natDegree := by + intro l + induction l with + | nil => intro acc; exact Nat.le_refl _ + | cons m l ih => + intro acc + rw [List.foldl_cons, List.foldl_cons] + refine le_trans (ih (acc * m)) (me_foldl_add_mono l _ _ ?_) + rw [CPolynomial.toPoly_mul] + refine le_trans Polynomial.natDegree_mul_le ?_ + rw [CPolynomial.natDegree_toPoly] + +private theorem me_moduliProduct_natDegree_le (moduli : Array (CPolynomial F)) : + (me_moduliProduct moduli).toPoly.natDegree ≤ modulusDegreeMass moduli := by + rw [me_moduliProduct, modulusDegreeMass, ← Array.foldl_toList, ← Array.foldl_toList] + have h := me_foldl_mul_natDegree_le moduli.toList 1 + simpa [CPolynomial.toPoly_one] using h + +/-! ### Modular-reduction semantics (local copies of the GS bridge lemmas) -/ + +private theorem me_modByMonicWith_toPoly (modCtx : CPolynomial.ModContext F) + {p M : CPolynomial F} (hM : Polynomial.Monic M.toPoly) : + (modByMonicWith modCtx p M).toPoly = p.toPoly %ₘ M.toPoly := by + have hMne : M ≠ 0 := by + intro hzero + have : M.toPoly = 0 := by rw [hzero, CPolynomial.toPoly_zero] + exact hM.ne_zero this + rw [modByMonicWith, if_neg (by simpa using hMne), modCtx.modByMonic_eq_modByMonic] + exact CPolynomial.modByMonic_toPoly_eq_modByMonic p M + ((CPolynomial.monic_toPoly_iff M).mpr hM) + +private theorem me_dvd_modByMonicWith_sub (modCtx : CPolynomial.ModContext F) + {p M : CPolynomial F} (hM : Polynomial.Monic M.toPoly) : + M.toPoly ∣ (modByMonicWith modCtx p M).toPoly - p.toPoly := by + rw [me_modByMonicWith_toPoly modCtx hM] + refine ⟨-(p.toPoly /ₘ M.toPoly), ?_⟩ + calc p.toPoly %ₘ M.toPoly - p.toPoly + = p.toPoly %ₘ M.toPoly - + (p.toPoly %ₘ M.toPoly + M.toPoly * (p.toPoly /ₘ M.toPoly)) := by + rw [Polynomial.modByMonic_add_div p.toPoly M.toPoly] + _ = M.toPoly * -(p.toPoly /ₘ M.toPoly) := by ring + +private theorem me_modByMonicWith_eq_zero_iff_dvd (modCtx : CPolynomial.ModContext F) + {p M : CPolynomial F} (hM : Polynomial.Monic M.toPoly) : + modByMonicWith modCtx p M = 0 ↔ M.toPoly ∣ p.toPoly := by + rw [← Polynomial.modByMonic_eq_zero_iff_dvd hM, ← me_modByMonicWith_toPoly modCtx hM] + constructor + · intro h + rw [h, CPolynomial.toPoly_zero] + · intro h + exact (CPolynomial.toPoly_eq_zero_iff _).mp h + +private theorem me_divByMonic_mul_eq {p M : CPolynomial F} (hM : M.monic) + (hdvd : M.toPoly ∣ p.toPoly) : + (p.divByMonic M).toPoly * M.toPoly = p.toPoly := by + have hMonic : Polynomial.Monic M.toPoly := (CPolynomial.monic_toPoly_iff M).mp hM + have hmod : p.toPoly %ₘ M.toPoly = 0 := + (Polynomial.modByMonic_eq_zero_iff_dvd hMonic).mpr hdvd + have hdecomp := Polynomial.modByMonic_add_div p.toPoly M.toPoly + rw [hmod, zero_add] at hdecomp + rw [CPolynomial.divByMonic_toPoly_eq_divByMonic p M hM, mul_comm] + exact hdecomp + +/-- The executable modular row predicate, columnwise. -/ +private theorem me_rowSatisfies_iff (mulCtx : CPolynomial.MulContext F) + (modCtx : CPolynomial.ModContext F) (row : PolynomialRow F) + (M : PolynomialMatrix F) (moduli : Array (CPolynomial F)) : + rowSatisfiesModularBool mulCtx modCtx row M moduli = true ↔ + ∀ b, b < moduli.size → + modByMonicWith modCtx (rowGet (rowMulMatrixWith mulCtx row M) b) + (moduli.getD b 0) = 0 := by + rw [rowSatisfiesModularBool, rowMulMatrixModDiagonalWith, rowModDiagonalWith, + Array.all_eq_true] + constructor + · intro h b hb + have hb' : b < (((List.range moduli.size).map fun j ↦ + modByMonicWith modCtx (rowGet (rowMulMatrixWith mulCtx row M) j) + (moduli.getD j 0)).toArray).size := by + simpa using hb + have := h b hb' + simpa using this + · intro h b hb + have hb' : b < moduli.size := by simpa using hb + simpa using h b hb' + +/-! ### Membership plumbing for the pipeline filters -/ + +/-- Filtered modular-solution rows come from the input rows. -/ +theorem me_filterModularSolutionRows_subset + {mulCtx : CPolynomial.MulContext F} {modCtx : CPolynomial.ModContext F} + {equation : ModularEquation F} {rows : PolynomialMatrix F} + {row : PolynomialRow F} + (hrow : row ∈ MatrixRows (filterModularSolutionRows mulCtx modCtx equation rows)) : + row ∈ MatrixRows rows := by + rw [MatrixRows, filterModularSolutionRows] at hrow + rw [MatrixRows] + have hmem : row ∈ rows ∧ rowIsZero row = false ∧ + rowSatisfiesModularBool mulCtx modCtx row equation.matrix equation.moduli = true := by + simpa using hrow + simpa using hmem.1 + +private theorem me_mem_filterModularSolutionRows + {mulCtx : CPolynomial.MulContext F} {modCtx : CPolynomial.ModContext F} + {equation : ModularEquation F} {rows : PolynomialMatrix F} + {row : PolynomialRow F} + (hmem : row ∈ MatrixRows rows) (hnz : rowIsZero row = false) + (hsat : rowSatisfiesModularBool mulCtx modCtx row equation.matrix + equation.moduli = true) : + row ∈ MatrixRows (filterModularSolutionRows mulCtx modCtx equation rows) := by + rw [MatrixRows] at hmem + rw [MatrixRows, filterModularSolutionRows] + have h : row ∈ rows ∧ rowIsZero row = false ∧ + rowSatisfiesModularBool mulCtx modCtx row equation.matrix equation.moduli = true := + ⟨by simpa using hmem, hnz, hsat⟩ + simpa using h + +omit [LawfulBEq F] in +private theorem me_mem_compactNonzeroRows {rows : PolynomialMatrix F} + {row : PolynomialRow F} (hmem : row ∈ MatrixRows rows) + (hnz : rowIsZero row = false) : + row ∈ MatrixRows (compactNonzeroRows rows) := by + rw [MatrixRows] at hmem + rw [MatrixRows, compactNonzeroRows] + have h : row ∈ rows ∧ rowIsZero row = false := ⟨by simpa using hmem, hnz⟩ + simpa using h + +omit [BEq F] [LawfulBEq F] in +private theorem me_mem_principalSolutionRows {sW : Nat} {basis : PolynomialMatrix F} + {row : PolynomialRow F} (hmem : row ∈ MatrixRows basis) : + ((List.range sW).map fun j ↦ rowGet row j).toArray ∈ + MatrixRows (principalSolutionRows sW basis) := by + rw [MatrixRows, principalSolutionRows, Array.toList_map] + exact List.mem_map.mpr ⟨row, hmem, rfl⟩ + +omit [BEq F] [LawfulBEq F] in +/-- Principal solution rows keep the basis row width. -/ +theorem me_principalSolutionRows_width {sW : Nat} + {basis : PolynomialMatrix F} {row : PolynomialRow F} + (hrow : row ∈ MatrixRows (principalSolutionRows sW basis)) : + row.size = sW := by + rw [MatrixRows, principalSolutionRows, Array.toList_map] at hrow + rcases List.mem_map.mp hrow with ⟨r, _hr, rfl⟩ + simp + +/-! ### Width discipline of the adaptive pipeline -/ + +private theorem me_solutionBasisWithPlan_width + {modCtx : CPolynomial.ModContext F} {pmCtx : PMBasisContext F} + {equation : ModularEquation F} {shift : Array Nat} + {plan : PartialLinearizationPlan} {row : PolynomialRow F} + (hrow : row ∈ MatrixRows + (solutionBasisWithPlanViaPMBasis modCtx pmCtx equation shift plan)) : + row.size = plan.solutionWidth := by + simp only [solutionBasisWithPlanViaPMBasis] at hrow + have hsub := compactNonzeroRows_subset hrow + rw [MatrixRows, compressChunkedPrincipalRows, Array.toList_map] at hsub + rcases List.mem_map.mp hsub with ⟨r, _hr, rfl⟩ + simp [compressChunkedPrincipalRow] + +private theorem me_adaptiveRound_width + {mulCtx : CPolynomial.MulContext F} {modCtx : CPolynomial.ModContext F} + {pmCtx : PMBasisContext F} {equation : ModularEquation F} + {shift : Array Nat} {state : AdaptiveSolveState F} + (hstate : ∀ r ∈ MatrixRows state.filtered, r.size = equation.solutionWidth) : + ∀ r ∈ MatrixRows + (adaptiveSolutionRound mulCtx modCtx pmCtx equation shift state).filtered, + r.size = equation.solutionWidth := by + intro r hr + simp only [adaptiveSolutionRound, MatrixRows, Array.toList_append, + List.mem_append] at hr + rcases hr with hold | hnew + · exact hstate r hold + · have hsub := me_filterModularSolutionRows_subset hnew + have hsize := me_solutionBasisWithPlan_width hsub + rw [hsize] + simp [partialLinearizationPlanFromPivotDegrees] + +private theorem me_adaptiveLoop_width + {mulCtx : CPolynomial.MulContext F} {modCtx : CPolynomial.ModContext F} + {pmCtx : PMBasisContext F} {equation : ModularEquation F} + {shift : Array Nat} {degreeBound? : Option Nat} {cap : Nat} : + ∀ (fuel : Nat) (state : AdaptiveSolveState F), + (∀ r ∈ MatrixRows state.filtered, r.size = equation.solutionWidth) → + ∀ r ∈ MatrixRows + (adaptiveSolutionLoop mulCtx modCtx pmCtx equation shift degreeBound? + cap fuel state).filtered, + r.size = equation.solutionWidth := by + intro fuel + induction fuel with + | zero => + intro state hstate r hr + exact hstate r hr + | succ fuel ih => + intro state hstate r hr + rw [adaptiveSolutionLoop] at hr + have hnext := me_adaptiveRound_width (mulCtx := mulCtx) (modCtx := modCtx) + (pmCtx := pmCtx) (shift := shift) (state := state) hstate + split at hr + · exact hnext r hr + · split at hr + · exact hnext r hr + · split at hr + · exact hnext r hr + · dsimp only [] at hr + split at hr + · exact hnext r hr + · refine ih _ ?_ r hr + intro r' hr' + exact hnext r' hr' + +/-- Every adaptive solution-basis row has the linearized width. -/ +theorem me_adaptiveBasis_width + {mulCtx : CPolynomial.MulContext F} {modCtx : CPolynomial.ModContext F} + {pmCtx : PMBasisContext F} {equation : ModularEquation F} + {shift : Array Nat} {degreeBound? : Option Nat} : + ∀ r ∈ MatrixRows + (adaptiveSolutionBasis mulCtx modCtx pmCtx equation shift + degreeBound?).filtered, + r.size = equation.solutionWidth := by + rw [adaptiveSolutionBasis] + exact me_adaptiveLoop_width _ _ (by intro r hr; simp [MatrixRows] at hr) + +/-! ### The fallback solution `e_p * prod(moduli)` -/ + +/-- Structure of the fallback row `e_p * prod(moduli)`. -/ +theorem me_prodRow_facts (mulCtx : CPolynomial.MulContext F) + (modCtx : CPolynomial.ModContext F) (equation : ModularEquation F) + (shift : Array Nat) {p : Nat} + (hmonic : ∀ b, b < equation.moduli.size → (equation.moduli.getD b 0).monic) + (hcols : equation.moduli.size ≤ MatrixWidth equation.matrix) + (hp : p < equation.solutionWidth) : + ∃ (prow : PolynomialRow F) (e : Nat), + rowSatisfiesModularBool mulCtx modCtx prow equation.matrix + equation.moduli = true ∧ + rowIsZero prow = false ∧ + prow.size = equation.solutionWidth ∧ + rowShiftedDegree? prow shift = some e ∧ + e ≤ pivotWindowCap equation + maxShiftDegree shift := by + classical + set sW := equation.solutionWidth with hsW + set prod := me_moduliProduct equation.moduli with hprodDef + set prow : PolynomialRow F := + ((List.range sW).map fun j ↦ if j = p then prod else 0).toArray with hprow + have hsize : prow.size = sW := by simp [hprow] + have hget : ∀ j, rowGet prow j = + if j < sW then (if j = p then prod else 0) else 0 := by + intro j + rw [hprow, rowGet, me_getD_list_range_map] + have hprodne : prod ≠ 0 := me_moduliProduct_ne_zero hmonic + have hnz : rowIsZero prow = false := by + refine me_rowIsZero_false_of_entry (j := p) (by omega) ?_ + rw [hget, if_pos hp, if_pos rfl] + exact hprodne + have hsat : rowSatisfiesModularBool mulCtx modCtx prow equation.matrix + equation.moduli = true := by + rw [me_rowSatisfies_iff] + intro b hb + rw [me_modByMonicWith_eq_zero_iff_dvd modCtx + ((CPolynomial.monic_toPoly_iff _).mp (hmonic b hb))] + rw [me_rowMul_toPoly mulCtx prow equation.matrix (lt_of_lt_of_le hb hcols) + (le_of_eq hsize)] + refine Finset.dvd_sum fun k hk ↦ ?_ + rw [hget, if_pos (Finset.mem_range.mp hk)] + by_cases hkp : k = p + · rw [if_pos hkp] + exact Dvd.dvd.mul_right (me_moduliProduct_dvd hb) _ + · rw [if_neg hkp, CPolynomial.toPoly_zero, zero_mul] + exact dvd_zero _ + obtain ⟨e, he⟩ := me_rowShiftedDegree_isSome (shift := shift) hnz + refine ⟨prow, e, hsat, hnz, hsize, he, ?_⟩ + obtain ⟨j, hjsize, hjne, hjeq⟩ := me_rowShiftedDegree_attained he + rw [hsize] at hjsize + have hjp : j = p := by + by_contra hne + refine hjne ?_ + rw [hget, if_pos hjsize, if_neg hne] + rw [hget, if_pos hjsize, if_pos hjp] at hjeq + have hdeg : prod.natDegree ≤ pivotWindowCap equation := by + rw [CPolynomial.natDegree_toPoly] + exact me_moduliProduct_natDegree_le equation.moduli + have hshiftle := me_shift_le_maxShiftDegree shift j + omega + +/-! ### The certified verification window -/ + +/-- Certified-window domination: any nonzero in-width modular solution row +whose shifted degree fits inside the verification window `bound` is +degree-dominated by a row of the filtered verification basis. -/ +theorem me_verification_dominates + (mulCtx : CPolynomial.MulContext F) (modCtx : CPolynomial.ModContext F) + (pmCtx : PMBasisContext F) (equation : ModularEquation F) + (shift : Array Nat) (bound : Nat) {rowStar : PolynomialRow F} {e : Nat} + (hmonic : ∀ b, b < equation.moduli.size → (equation.moduli.getD b 0).monic) + (hcols : equation.moduli.size ≤ MatrixWidth equation.matrix) + (hshift : shift.size = equation.solutionWidth) + (hpos : 0 < equation.solutionWidth) + (hsat : rowSatisfiesModularBool mulCtx modCtx rowStar equation.matrix + equation.moduli = true) + (hnz : rowIsZero rowStar = false) + (hwidth : rowStar.size ≤ equation.solutionWidth) + (hdeg : rowShiftedDegree? rowStar shift = some e) + (hebound : e ≤ bound) : + ∃ basisRow degree, + basisRow ∈ MatrixRows (filterModularSolutionRows mulCtx modCtx equation + (compactNonzeroRows (principalSolutionRows equation.solutionWidth + (pmCtx.basis (fullWindowExactNullspaceProblem modCtx equation bound) + (exactNullspaceShift shift equation.modularWidth bound))))) ∧ + rowShiftedDegree? basisRow shift = some degree ∧ degree ≤ e := by + classical + rw [show equation.modularWidth = equation.moduli.size from rfl] + set sW := equation.solutionWidth with hsWdef + set mW := equation.moduli.size with hmWdef + -- The reduced principal block and the lift matrix. + set Fred : PolynomialMatrix F := ofFn sW mW + (fun i j ↦ modByMonicWith modCtx (rowGet (equation.matrix.getD i #[]) j) + (equation.moduli.getD j 0)) with hFred + set liftM : PolynomialMatrix F := Fred ++ negativeDiagonalRows equation.moduli + with hliftM + have hFredSize : Fred.size = sW := ofFn_size _ _ _ + have hnegSize : (negativeDiagonalRows equation.moduli).size = mW := ofFn_size _ _ _ + have hliftSize : liftM.size = sW + mW := by + rw [hliftM, Array.size_append, hFredSize, hnegSize] + have hFredWidth : MatrixWidth Fred = mW := by + rw [hFred, MatrixWidth_ofFn, if_neg (by omega)] + have hFredEntry : ∀ {k b : Nat}, k < sW → b < mW → + rowGet (Fred.getD k #[]) b = + modByMonicWith modCtx (rowGet (equation.matrix.getD k #[]) b) + (equation.moduli.getD b 0) := by + intro k b hk hb + rw [hFred, rowGet_ofFn, if_pos ⟨hk, hb⟩] + have hliftWidth : MatrixWidth liftM = mW := by + rw [me_matrixWidth_eq_getD, hliftM, me_getD_append_left _ (by omega), hFred, + getD_ofFn, if_pos (by omega)] + simp + have hliftRows : ∀ r ∈ MatrixRows liftM, r.size = mW := by + intro r hr + rw [MatrixRows, hliftM, Array.toList_append, List.mem_append] at hr + rcases hr with hr | hr + · rcases List.getElem_of_mem hr with ⟨i, hi, hget⟩ + have hi' : i < Fred.size := by simpa using hi + have hgetD : Fred.getD i #[] = r := by + rw [Array.getD_eq_getD_getElem?, Array.getElem?_eq_getElem hi', Option.getD_some] + simpa [Array.getElem_toList] using hget + rw [← hgetD, hFred, getD_ofFn, if_pos (by omega)] + simp + · rcases List.getElem_of_mem hr with ⟨i, hi, hget⟩ + have hi' : i < (negativeDiagonalRows equation.moduli).size := by simpa using hi + have hgetD : (negativeDiagonalRows equation.moduli).getD i #[] = r := by + rw [Array.getD_eq_getD_getElem?, Array.getElem?_eq_getElem hi', Option.getD_some] + simpa [Array.getElem_toList] using hget + rw [← hgetD, negativeDiagonalRows, getD_ofFn, if_pos (by omega)] + simp only [List.size_toArray, List.length_map, List.length_range] + omega + have hwf : WellFormed liftM := by + intro r hr + rw [hliftWidth] + exact hliftRows r hr + have hliftL : ∀ {k b : Nat}, k < sW → b < mW → + rowGet (liftM.getD k #[]) b = + modByMonicWith modCtx (rowGet (equation.matrix.getD k #[]) b) + (equation.moduli.getD b 0) := by + intro k b hk hb + rw [hliftM, me_getD_append_left _ (by omega)] + exact hFredEntry hk hb + have hliftR : ∀ {c b : Nat}, c < mW → b < mW → + rowGet (liftM.getD (sW + c) #[]) b = + (if c == b then -(equation.moduli.getD c 0) else 0) := by + intro c b hc hb + rw [hliftM, me_getD_append_right _ (by omega)] + rw [show sW + c - Fred.size = c from by omega] + rw [negativeDiagonalRows, rowGet_ofFn, if_pos ⟨by omega, by omega⟩] + -- Monicity facts. + have hMon : ∀ {b : Nat}, b < mW → + Polynomial.Monic ((equation.moduli.getD b 0).toPoly) := + fun {b} hb ↦ (CPolynomial.monic_toPoly_iff _).mp (hmonic b hb) + have hMne : ∀ {b : Nat}, b < mW → ((equation.moduli.getD b 0).toPoly) ≠ 0 := + fun {b} hb ↦ (hMon hb).ne_zero + -- The verification problem and shift. + set P := fullWindowExactNullspaceProblem modCtx equation bound with hP + have hPmatrix : P.matrix = liftM := rfl + have hPordersSize : P.orders.size = mW := by + rw [hP, fullWindowExactNullspaceProblem] + simp only [Array.size_map] + omega + have hPorders : ∀ {b : Nat}, b < mW → + P.orders.getD b 0 = (equation.moduli.getD b 0).natDegree + bound + 2 := by + intro b hb + show (equation.moduli.map fun m ↦ m.natDegree + bound + 2).getD b 0 = _ + rw [Array.getD_eq_getD_getElem?, Array.getElem?_map, + Array.getElem?_eq_getElem (by omega), Option.map_some, Option.getD_some, + Array.getD_eq_getD_getElem?, Array.getElem?_eq_getElem (by omega), + Option.getD_some] + set eShift := exactNullspaceShift shift equation.moduli.size bound with heShift + have heShiftL : ∀ {k : Nat}, k < sW → + eShift.getD k 0 = shift.getD k 0 + bound + 1 := by + intro k hk + rw [heShift, exactNullspaceShift, liftedPrincipalShift, principalShiftOffset, + me_getD_append_left _ (by simp [hshift]; omega)] + rw [Array.getD_eq_getD_getElem?, Array.getElem?_map, + Array.getElem?_eq_getElem (by omega : k < shift.size), Option.map_some, + Option.getD_some, Array.getD_eq_getD_getElem?, + Array.getElem?_eq_getElem (by omega : k < shift.size), Option.getD_some] + omega + have heShiftR : ∀ {c : Nat}, c < mW → + eShift.getD (sW + c) 0 = bound := by + intro c hc + rw [heShift, exactNullspaceShift, quotientShift, + me_getD_append_right _ (by simp [liftedPrincipalShift, hshift])] + rw [show sW + c - (liftedPrincipalShift shift bound).size = c from by + simp [liftedPrincipalShift, hshift]] + exact me_getD_replicate _ _ hc + -- Divisibility of the original products. + have hdvdF : ∀ {b : Nat}, b < mW → + (equation.moduli.getD b 0).toPoly ∣ + ∑ k ∈ Finset.range sW, (rowGet rowStar k).toPoly * + (rowGet (equation.matrix.getD k #[]) b).toPoly := by + intro b hb + have hsat' := (me_rowSatisfies_iff mulCtx modCtx rowStar equation.matrix + equation.moduli).mp hsat b hb + have hdvd := (me_modByMonicWith_eq_zero_iff_dvd modCtx (hMon hb)).mp hsat' + rwa [me_rowMul_toPoly mulCtx rowStar equation.matrix + (lt_of_lt_of_le hb hcols) hwidth] at hdvd + -- The reduced products and their exact quotients. + set prodE := fun b ↦ rowGet (rowMulMatrixWith mulCtx rowStar Fred) b with hprodE + have hprodE_toPoly : ∀ {b : Nat}, b < mW → + (prodE b).toPoly = ∑ k ∈ Finset.range sW, (rowGet rowStar k).toPoly * + (rowGet (Fred.getD k #[]) b).toPoly := by + intro b hb + rw [hprodE] + exact me_rowMul_toPoly mulCtx rowStar Fred (by omega) hwidth + have hdvdRed : ∀ {b : Nat}, b < mW → + (equation.moduli.getD b 0).toPoly ∣ (prodE b).toPoly := by + intro b hb + have hdiff : (equation.moduli.getD b 0).toPoly ∣ + (prodE b).toPoly - + ∑ k ∈ Finset.range sW, (rowGet rowStar k).toPoly * + (rowGet (equation.matrix.getD k #[]) b).toPoly := by + rw [hprodE_toPoly hb, ← Finset.sum_sub_distrib] + refine Finset.dvd_sum fun k hk ↦ ?_ + have hk' : k < sW := Finset.mem_range.mp hk + rw [hFredEntry hk' hb] + have hsub := me_dvd_modByMonicWith_sub modCtx + (p := rowGet (equation.matrix.getD k #[]) b) (hMon hb) + have hfac : (rowGet rowStar k).toPoly * + (modByMonicWith modCtx (rowGet (equation.matrix.getD k #[]) b) + (equation.moduli.getD b 0)).toPoly - + (rowGet rowStar k).toPoly * + (rowGet (equation.matrix.getD k #[]) b).toPoly = + (rowGet rowStar k).toPoly * + ((modByMonicWith modCtx (rowGet (equation.matrix.getD k #[]) b) + (equation.moduli.getD b 0)).toPoly - + (rowGet (equation.matrix.getD k #[]) b).toPoly) := by + ring + rw [hfac] + exact Dvd.dvd.mul_left hsub _ + have hsum := dvd_add hdiff (hdvdF hb) + simpa using hsum + obtain ⟨quot, hquot⟩ : ∃ q : Nat → CPolynomial F, + ∀ b, q b = (prodE b).divByMonic (equation.moduli.getD b 0) := + ⟨fun b ↦ (prodE b).divByMonic (equation.moduli.getD b 0), fun _ ↦ rfl⟩ + have hquotExact : ∀ {b : Nat}, b < mW → + (quot b).toPoly * (equation.moduli.getD b 0).toPoly = (prodE b).toPoly := by + intro b hb + rw [hquot b, me_divByMonic_mul_eq (hmonic b hb) (hdvdRed hb)] + -- Degree bounds on the witness entries and quotients. + have hrowStarEntry : ∀ {k : Nat}, rowGet rowStar k ≠ 0 → + (rowGet rowStar k).toPoly.natDegree + shift.getD k 0 ≤ e := by + intro k hk + have h := me_entry_le_of_rowShiftedDegree hdeg hk + rwa [CPolynomial.natDegree_toPoly] at h + have hFredZero : ∀ {k b : Nat}, k < sW → b < mW → + (equation.moduli.getD b 0).natDegree = 0 → + (rowGet (Fred.getD k #[]) b).toPoly = 0 := by + intro k b hk hb hMdeg + rw [hFredEntry hk hb, me_modByMonicWith_toPoly modCtx (hMon hb)] + have hMone : (equation.moduli.getD b 0).toPoly = 1 := by + refine (Polynomial.Monic.natDegree_eq_zero (hMon hb)).mp ?_ + rwa [CPolynomial.natDegree_toPoly] at hMdeg + rw [hMone, Polynomial.modByMonic_one] + have hFredDegLt : ∀ {k b : Nat}, k < sW → b < mW → + (rowGet (Fred.getD k #[]) b).toPoly ≠ 0 → + (rowGet (Fred.getD k #[]) b).toPoly.natDegree < + (equation.moduli.getD b 0).natDegree := by + intro k b hk hb hne + rw [hFredEntry hk hb, me_modByMonicWith_toPoly modCtx (hMon hb)] at hne ⊢ + rw [CPolynomial.natDegree_toPoly] + exact Polynomial.natDegree_lt_natDegree hne + (Polynomial.degree_modByMonic_lt _ (hMon hb)) + have hquotDeg : ∀ {b : Nat}, b < mW → quot b ≠ 0 → + (quot b).toPoly.natDegree ≤ e := by + intro b hb hqz + have hqpoly : (quot b).toPoly ≠ 0 := + fun h ↦ hqz ((CPolynomial.toPoly_eq_zero_iff _).mp h) + by_cases hMdeg : (equation.moduli.getD b 0).natDegree = 0 + · exfalso + have hzero : (prodE b).toPoly = 0 := by + rw [hprodE_toPoly hb] + refine Finset.sum_eq_zero fun k hk ↦ ?_ + rw [hFredZero (Finset.mem_range.mp hk) hb hMdeg, mul_zero] + have hmul := hquotExact hb + rw [hzero] at hmul + rcases mul_eq_zero.mp hmul with h | h + · exact hqpoly h + · exact hMne hb h + · have h2 : (prodE b).toPoly.natDegree = + (quot b).toPoly.natDegree + (equation.moduli.getD b 0).natDegree := by + rw [← hquotExact hb, CPolynomial.natDegree_toPoly (equation.moduli.getD b 0)] + exact Polynomial.natDegree_mul hqpoly (hMne hb) + have h1 : (prodE b).toPoly.natDegree ≤ + e + (equation.moduli.getD b 0).natDegree - 1 := by + rw [hprodE_toPoly hb] + refine me_natDegree_sum_le _ fun k hk ↦ ?_ + by_cases hz : (rowGet rowStar k).toPoly = 0 + · rw [hz, zero_mul, Polynomial.natDegree_zero] + exact Nat.zero_le _ + by_cases hzF : (rowGet (Fred.getD k #[]) b).toPoly = 0 + · rw [hzF, mul_zero, Polynomial.natDegree_zero] + exact Nat.zero_le _ + · have hne : rowGet rowStar k ≠ 0 := + fun h ↦ hz (by rw [h, CPolynomial.toPoly_zero]) + have ha := hrowStarEntry hne + have hbnd := hFredDegLt hk hb hzF + refine le_trans Polynomial.natDegree_mul_le ?_ + omega + omega + -- The lifted exact-nullspace row. + set lifted : PolynomialRow F := ((List.range (sW + mW)).map + (fun k ↦ if k < sW then rowGet rowStar k else quot (k - sW))).toArray + with hlifted + have hliftedSize : lifted.size = sW + mW := by simp [hlifted] + have hliftedGet : ∀ k, rowGet lifted k = + if k < sW + mW then (if k < sW then rowGet rowStar k else quot (k - sW)) + else 0 := by + intro k + rw [hlifted, rowGet, me_getD_list_range_map] + have hliftedL : ∀ {k : Nat}, k < sW → rowGet lifted k = rowGet rowStar k := by + intro k hk + rw [hliftedGet, if_pos (by omega), if_pos hk] + have hliftedR : ∀ {c : Nat}, c < mW → rowGet lifted (sW + c) = quot c := by + intro c hc + rw [hliftedGet, if_pos (by omega), if_neg (by omega)] + rw [show sW + c - sW = c from by omega] + obtain ⟨j0, hj0, hj0ne⟩ := exists_nonzero_entry_of_rowIsZero_false hnz + have hj0sW : j0 < sW := by omega + have hnzL : rowIsZero lifted = false := by + refine me_rowIsZero_false_of_entry (j := j0) (by omega) ?_ + rw [hliftedL hj0sW] + exact hj0ne + -- The lifted row is an exact solution of the verification problem. + have happroxL : ∀ j, j < P.orders.size → + truncateX (P.orders.getD j 0) + (rowGet (rowMulMatrixWith pmCtx.runtime.mulContext lifted P.matrix) j) = 0 := by + intro j hj + have hjm : j < mW := by omega + have hzero : rowGet (rowMulMatrixWith pmCtx.runtime.mulContext lifted + P.matrix) j = 0 := by + refine (CPolynomial.toPoly_eq_zero_iff _).mp ?_ + rw [hPmatrix] + rw [me_rowMul_toPoly pmCtx.runtime.mulContext lifted liftM (by omega) + (le_of_eq hliftedSize)] + rw [me_sum_range_add (fun k ↦ (rowGet lifted k).toPoly * + (rowGet (liftM.getD k #[]) j).toPoly) sW mW] + have hfirst : ∑ k ∈ Finset.range sW, (rowGet lifted k).toPoly * + (rowGet (liftM.getD k #[]) j).toPoly = (prodE j).toPoly := by + rw [hprodE_toPoly hjm] + refine Finset.sum_congr rfl fun k hk ↦ ?_ + have hk' : k < sW := Finset.mem_range.mp hk + rw [hliftedL hk', hliftM, me_getD_append_left _ (by omega)] + have hsecond : ∑ k ∈ Finset.range mW, (rowGet lifted (sW + k)).toPoly * + (rowGet (liftM.getD (sW + k) #[]) j).toPoly = + -((quot j).toPoly * (equation.moduli.getD j 0).toPoly) := by + rw [Finset.sum_eq_single_of_mem j (Finset.mem_range.mpr hjm) ?_] + · rw [hliftedR hjm, hliftR hjm hjm, if_pos (by simp), + CPolynomial.toPoly_neg] + ring + · intro c hc hcj + rw [hliftR (Finset.mem_range.mp hc) hjm, if_neg (by simpa using hcj), + CPolynomial.toPoly_zero, mul_zero] + rw [hfirst, hsecond, hquotExact hjm] + ring + rw [hzero] + exact truncateX_zero _ + -- Apply the PM-basis minimality contract to the lifted row. + obtain ⟨bRow, Db, hbMem, hbSize, hbDeg, hbDom⟩ := + pmCtx.complete_minimal P eShift lifted (by rw [hPmatrix]; omega) + (by rw [hPmatrix]; exact hwf) happroxL hnzL + (by rw [hPmatrix]; omega) + have hbSizeLe : bRow.size ≤ sW + mW := by + rw [hPmatrix, hliftSize] at hbSize + exact hbSize + -- The lifted row stays inside the expanded window. + obtain ⟨Dl, hDl⟩ := me_rowShiftedDegree_isSome (shift := eShift) hnzL + have hDlle : Dl ≤ e + bound + 1 := by + obtain ⟨j, hjsize, hjne, hjeq⟩ := me_rowShiftedDegree_attained hDl + rw [hliftedSize] at hjsize + rcases Nat.lt_or_ge j sW with hjL | hjR + · rw [heShiftL hjL, hliftedL hjL] at hjeq + have hne' : rowGet rowStar j ≠ 0 := by rwa [hliftedL hjL] at hjne + have h := me_entry_le_of_rowShiftedDegree hdeg hne' + rw [CPolynomial.natDegree_toPoly] at h + rw [CPolynomial.natDegree_toPoly] at hjeq + omega + · have hcm : j - sW < mW := by omega + rw [show j = sW + (j - sW) from by omega] at hjne hjeq + rw [hliftedR hcm] at hjne hjeq + rw [heShiftR hcm] at hjeq + have hqd := hquotDeg hcm hjne + rw [CPolynomial.natDegree_toPoly] at hjeq + omega + have hDb : Db ≤ e + bound + 1 := le_trans (hbDom Dl hDl) hDlle + -- Entry bounds on the dominated basis row. + have hbEntryL : ∀ {k : Nat}, k < sW → rowGet bRow k ≠ 0 → + (rowGet bRow k).toPoly.natDegree + shift.getD k 0 ≤ e := by + intro k hk hne + have h := me_entry_le_of_rowShiftedDegree hbDeg hne + rw [heShiftL hk] at h + rw [CPolynomial.natDegree_toPoly] at h + omega + have hbEntryR : ∀ {c : Nat}, c < mW → rowGet bRow (sW + c) ≠ 0 → + (rowGet bRow (sW + c)).toPoly.natDegree ≤ e + 1 := by + intro c hc hne + have h := me_entry_le_of_rowShiftedDegree hbDeg hne + rw [heShiftR hc] at h + rw [CPolynomial.natDegree_toPoly] at h + omega + -- The verification orders force exact column products for the basis row. + have hsound := pmCtx.sound P eShift bRow hbMem + have hTzero : ∀ {j : Nat}, j < mW → + ∑ k ∈ Finset.range (sW + mW), (rowGet bRow k).toPoly * + (rowGet (liftM.getD k #[]) j).toPoly = 0 := by + intro j hjm + have hX := hsound j (by omega) + rw [truncateX_eq_zero_iff_X_pow_dvd] at hX + rw [hPmatrix] at hX + rw [me_rowMul_toPoly pmCtx.runtime.mulContext bRow liftM (by omega) + hbSizeLe] at hX + rw [hPorders hjm] at hX + refine me_eq_zero_of_X_pow_dvd_of_natDegree_lt hX ?_ + have hdegT : (∑ k ∈ Finset.range (sW + mW), (rowGet bRow k).toPoly * + (rowGet (liftM.getD k #[]) j).toPoly).natDegree ≤ + (equation.moduli.getD j 0).natDegree + bound + 1 := by + refine me_natDegree_sum_le _ fun k hk ↦ ?_ + by_cases hz : (rowGet bRow k).toPoly = 0 + · rw [hz, zero_mul, Polynomial.natDegree_zero] + exact Nat.zero_le _ + have hne : rowGet bRow k ≠ 0 := + fun h ↦ hz (by rw [h, CPolynomial.toPoly_zero]) + rcases Nat.lt_or_ge k sW with hkL | hkR + · by_cases hzF : (rowGet (liftM.getD k #[]) j).toPoly = 0 + · rw [hzF, mul_zero, Polynomial.natDegree_zero] + exact Nat.zero_le _ + · have hzF' : (rowGet (Fred.getD k #[]) j).toPoly ≠ 0 := by + rwa [hliftM, me_getD_append_left _ (by omega)] at hzF + have hMdeg1 : 1 ≤ (equation.moduli.getD j 0).natDegree := by + by_contra hcon + exact hzF' (hFredZero hkL hjm (by omega)) + have hFle := hFredDegLt hkL hjm hzF' + have ha := hbEntryL hkL hne + have hentry : rowGet (liftM.getD k #[]) j = rowGet (Fred.getD k #[]) j := by + rw [hliftM, me_getD_append_left _ (by omega)] + rw [hentry] + refine le_trans Polynomial.natDegree_mul_le ?_ + omega + · have hcm : k - sW < mW := by omega + rw [show k = sW + (k - sW) from by omega, hliftR hcm hjm] + by_cases hcj : k - sW = j + · rw [if_pos (by simpa using hcj)] + have hb1 : (rowGet bRow (sW + j)).toPoly.natDegree ≤ e + 1 := by + refine hbEntryR hjm ?_ + rwa [show sW + j = k from by omega] + refine le_trans Polynomial.natDegree_mul_le ?_ + rw [hcj, CPolynomial.toPoly_neg, Polynomial.natDegree_neg, + ← CPolynomial.natDegree_toPoly (equation.moduli.getD j 0)] + omega + · rw [if_neg (by simpa using hcj), CPolynomial.toPoly_zero, mul_zero, + Polynomial.natDegree_zero] + exact Nat.zero_le _ + omega + have hbProd : ∀ {j : Nat}, j < mW → + ∑ k ∈ Finset.range sW, (rowGet bRow k).toPoly * + (rowGet (Fred.getD k #[]) j).toPoly = + (rowGet bRow (sW + j)).toPoly * (equation.moduli.getD j 0).toPoly := by + intro j hjm + have h0 := hTzero hjm + rw [me_sum_range_add (fun k ↦ (rowGet bRow k).toPoly * + (rowGet (liftM.getD k #[]) j).toPoly) sW mW] at h0 + have hfirst : ∑ k ∈ Finset.range sW, (rowGet bRow k).toPoly * + (rowGet (liftM.getD k #[]) j).toPoly = + ∑ k ∈ Finset.range sW, (rowGet bRow k).toPoly * + (rowGet (Fred.getD k #[]) j).toPoly := by + refine Finset.sum_congr rfl fun k hk ↦ ?_ + have hk' : k < sW := Finset.mem_range.mp hk + rw [hliftM, me_getD_append_left _ (by omega)] + have hsecond : ∑ k ∈ Finset.range mW, (rowGet bRow (sW + k)).toPoly * + (rowGet (liftM.getD (sW + k) #[]) j).toPoly = + -((rowGet bRow (sW + j)).toPoly * (equation.moduli.getD j 0).toPoly) := by + rw [Finset.sum_eq_single_of_mem j (Finset.mem_range.mpr hjm) ?_] + · rw [hliftR hjm hjm, if_pos (by simp), CPolynomial.toPoly_neg] + ring + · intro c hc hcj + rw [hliftR (Finset.mem_range.mp hc) hjm, if_neg (by simpa using hcj), + CPolynomial.toPoly_zero, mul_zero] + rw [hfirst, hsecond] at h0 + have h0' : ∑ k ∈ Finset.range sW, (rowGet bRow k).toPoly * + (rowGet (Fred.getD k #[]) j).toPoly - + (rowGet bRow (sW + j)).toPoly * (equation.moduli.getD j 0).toPoly = 0 := by + rw [sub_eq_add_neg] + exact h0 + exact sub_eq_zero.mp h0' + -- The principal truncation of the basis row. + set bPrin : PolynomialRow F := + ((List.range sW).map fun j ↦ rowGet bRow j).toArray with hbPrin + have hbPrinSize : bPrin.size = sW := by simp [hbPrin] + have hbPrinGet : ∀ k, rowGet bPrin k = if k < sW then rowGet bRow k else 0 := by + intro k + rw [hbPrin, rowGet, me_getD_list_range_map] + have hbDvdF : ∀ {j : Nat}, j < mW → (equation.moduli.getD j 0).toPoly ∣ + ∑ k ∈ Finset.range sW, (rowGet bRow k).toPoly * + (rowGet (equation.matrix.getD k #[]) j).toPoly := by + intro j hjm + have hsumdvd : (equation.moduli.getD j 0).toPoly ∣ + ∑ k ∈ Finset.range sW, (rowGet bRow k).toPoly * + (rowGet (Fred.getD k #[]) j).toPoly := by + rw [hbProd hjm] + exact dvd_mul_left _ _ + have hdiff : (equation.moduli.getD j 0).toPoly ∣ + (∑ k ∈ Finset.range sW, (rowGet bRow k).toPoly * + (rowGet (Fred.getD k #[]) j).toPoly) - + ∑ k ∈ Finset.range sW, (rowGet bRow k).toPoly * + (rowGet (equation.matrix.getD k #[]) j).toPoly := by + rw [← Finset.sum_sub_distrib] + refine Finset.dvd_sum fun k hk ↦ ?_ + rw [hFredEntry (Finset.mem_range.mp hk) hjm] + have hsub := me_dvd_modByMonicWith_sub modCtx + (p := rowGet (equation.matrix.getD k #[]) j) (hMon hjm) + have hfac : (rowGet bRow k).toPoly * + (modByMonicWith modCtx (rowGet (equation.matrix.getD k #[]) j) + (equation.moduli.getD j 0)).toPoly - + (rowGet bRow k).toPoly * + (rowGet (equation.matrix.getD k #[]) j).toPoly = + (rowGet bRow k).toPoly * + ((modByMonicWith modCtx (rowGet (equation.matrix.getD k #[]) j) + (equation.moduli.getD j 0)).toPoly - + (rowGet (equation.matrix.getD k #[]) j).toPoly) := by + ring + rw [hfac] + exact Dvd.dvd.mul_left hsub _ + have h := dvd_sub hsumdvd hdiff + simpa using h + have hbPrinSat : rowSatisfiesModularBool mulCtx modCtx bPrin equation.matrix + equation.moduli = true := by + rw [me_rowSatisfies_iff] + intro b hb + rw [me_modByMonicWith_eq_zero_iff_dvd modCtx (hMon hb)] + rw [me_rowMul_toPoly mulCtx bPrin equation.matrix (lt_of_lt_of_le hb hcols) + (le_of_eq hbPrinSize)] + have hcong : ∑ k ∈ Finset.range sW, (rowGet bPrin k).toPoly * + (rowGet (equation.matrix.getD k #[]) b).toPoly = + ∑ k ∈ Finset.range sW, (rowGet bRow k).toPoly * + (rowGet (equation.matrix.getD k #[]) b).toPoly := by + refine Finset.sum_congr rfl fun k hk ↦ ?_ + rw [hbPrinGet, if_pos (Finset.mem_range.mp hk)] + rw [hcong] + exact hbDvdF hb + have hbPrinNz : rowIsZero bPrin = false := by + cases hzero : rowIsZero bPrin with + | false => rfl + | true => + exfalso + have hallP : ∀ k, k < sW → rowGet bRow k = 0 := by + intro k hk + have h := me_rowGet_eq_zero_of_rowIsZero hzero k + rwa [hbPrinGet, if_pos hk] at h + have hallQ : ∀ c, c < mW → rowGet bRow (sW + c) = 0 := by + intro c hc + have hprod := hbProd hc + have hzeroSum : ∑ k ∈ Finset.range sW, (rowGet bRow k).toPoly * + (rowGet (Fred.getD k #[]) c).toPoly = 0 := + Finset.sum_eq_zero fun k hk ↦ by + rw [hallP k (Finset.mem_range.mp hk), CPolynomial.toPoly_zero, + zero_mul] + rw [hzeroSum] at hprod + rcases mul_eq_zero.mp hprod.symm with h | h + · exact (CPolynomial.toPoly_eq_zero_iff _).mp h + · exact absurd h (hMne hc) + have hzRow : RowIsZero bRow := by + refine me_rowIsZero_of_forall fun k hk ↦ ?_ + rcases Nat.lt_or_ge k sW with h1 | h1 + · exact hallP k h1 + · have hcm : k - sW < mW := by omega + rw [show k = sW + (k - sW) from by omega] + exact hallQ _ hcm + have hnone := (rowShiftedDegree?_eq_none_iff + (row := bRow) (shift := eShift)).mpr hzRow + rw [hbDeg] at hnone + cases hnone + have hbPrinMemF : bPrin ∈ MatrixRows (filterModularSolutionRows mulCtx modCtx + equation (compactNonzeroRows (principalSolutionRows sW + (pmCtx.basis P eShift)))) := by + refine me_mem_filterModularSolutionRows ?_ hbPrinNz hbPrinSat + refine me_mem_compactNonzeroRows ?_ hbPrinNz + exact me_mem_principalSolutionRows hbMem + obtain ⟨degB, hdegB⟩ := me_rowShiftedDegree_isSome (shift := shift) hbPrinNz + have hdegBle : degB ≤ e := by + obtain ⟨j, hjsize, hjne, hjeq⟩ := me_rowShiftedDegree_attained hdegB + rw [hbPrinSize] at hjsize + have hjne' : rowGet bRow j ≠ 0 := by rwa [hbPrinGet, if_pos hjsize] at hjne + have h := hbEntryL hjsize hjne' + rw [hbPrinGet, if_pos hjsize] at hjeq + rw [CPolynomial.natDegree_toPoly] at hjeq + omega + exact ⟨bPrin, degB, hbPrinMemF, hdegB, hdegBle⟩ + +end CompleteMinimal + +end Approximant + +end PolynomialMatrix + +end CompPoly diff --git a/CompPoly/LinearAlgebra/PolynomialMatrix/Approximant/PMBasis.lean b/CompPoly/LinearAlgebra/PolynomialMatrix/Approximant/PMBasis.lean new file mode 100644 index 00000000..00f23a9e --- /dev/null +++ b/CompPoly/LinearAlgebra/PolynomialMatrix/Approximant/PMBasis.lean @@ -0,0 +1,69 @@ +/- +Copyright (c) 2026 CompPoly Contributors. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Valerii Huhnin +-/ + +module + +public import CompPoly.LinearAlgebra.PolynomialMatrix.Approximant.PMBasis.Correctness + +/-! +# Recursive PM-Basis + +Umbrella module for the divide-and-conquer PM-basis: executable definitions, +correctness development, and the production `PMBasisContext` instances backed +by the recursive driver with scalar dense-kernel leaves. +-/ + +@[expose] public section + +namespace CompPoly + +namespace PolynomialMatrix + +namespace Approximant + +variable {F : Type*} [Field F] [BEq F] [LawfulBEq F] + +/-- PM-basis context backed by the recursive driver, scalar dense-kernel leaves, +and an independently tuned basis-composition cutoff. -/ +def kernelLeafPMBasisContextWithLowAndCompose (mulCtx : CPolynomial.MulContext F) + (lowCtx : PolynomialMatrix.MulLowContext F) + (leafCutoff composeLeafCutoff : Nat) : PMBasisContext F where + runtime := kernelLeafRuntimeWithLowAndCompose mulCtx lowCtx leafCutoff + composeLeafCutoff + basis := pmBasis (kernelLeafRuntimeWithLowAndCompose mulCtx lowCtx leafCutoff + composeLeafCutoff) + sound := by + intro problem shift row hrow + exact pmBasis_kernelLeaf_approximates mulCtx lowCtx leafCutoff + composeLeafCutoff problem shift hrow + complete_minimal := by + -- Shifted-minimality of the recursive PM-basis (the predictable-degree + -- property of minimal approximant bases): every nonzero X-adic solution + -- row is degree-dominated by some basis row. + let : DecidableEq F := instDecidableEqOfLawfulBEq + intro problem shift row hpos hwf happrox hnz hwidth + exact pmBasis_kernelLeaf_complete_minimal mulCtx lowCtx leafCutoff + composeLeafCutoff problem shift row hpos hwf happrox hnz hwidth + +/-- PM-basis context backed by the recursive driver and scalar dense-kernel +leaves. -/ +def kernelLeafPMBasisContextWithLow (mulCtx : CPolynomial.MulContext F) + (lowCtx : PolynomialMatrix.MulLowContext F) + (leafCutoff : Nat) : PMBasisContext F := + kernelLeafPMBasisContextWithLowAndCompose mulCtx lowCtx leafCutoff leafCutoff + +/-- PM-basis context whose low products are obtained by truncating full +products. -/ +def kernelLeafPMBasisContext (mulCtx : CPolynomial.MulContext F) + (leafCutoff : Nat) : PMBasisContext F := + kernelLeafPMBasisContextWithLow mulCtx + (PolynomialMatrix.MulLowContext.fromMulContext mulCtx) leafCutoff + +end Approximant + +end PolynomialMatrix + +end CompPoly diff --git a/CompPoly/LinearAlgebra/PolynomialMatrix/Approximant/PMBasis/Correctness.lean b/CompPoly/LinearAlgebra/PolynomialMatrix/Approximant/PMBasis/Correctness.lean new file mode 100644 index 00000000..ede7a25e --- /dev/null +++ b/CompPoly/LinearAlgebra/PolynomialMatrix/Approximant/PMBasis/Correctness.lean @@ -0,0 +1,1233 @@ +/- +Copyright (c) 2026 CompPoly Contributors. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Valerii Huhnin +-/ + +module + +-- `coeff`, `ofArray` and friends are declared in bare `public section`s, so their +-- bodies are opaque downstream; see `docs/wiki/module-system.md`. +import all CompPoly.Univariate.Basic +import all CompPoly.Univariate.Raw.Core +public import CompPoly.LinearAlgebra.PolynomialMatrix.Approximant.PMBasis.Recursion +public import CompPoly.LinearAlgebra.PolynomialMatrix.Approximant.PMBasis.KernelLeafCompleteness +public import CompPoly.LinearAlgebra.PolynomialMatrix.MuldersStorjohannCorrectness.WeakPopovMinimal + +/-! +# Recursive PM-Basis Correctness + +Soundness and shifted minimality of the kernel-leaf recursive PM-basis: +every produced row satisfies the X-adic conditions with the principal width, +the recursion generates the full solution module, and the root-normalized +basis is shifted weak Popov, so the predictable-degree property yields a +basis row dominating every nonzero solution. +-/ + +@[expose] public section + +namespace CompPoly + +namespace PolynomialMatrix + +namespace Approximant + +variable {F : Type*} [Field F] [BEq F] [LawfulBEq F] + +/-- Every row of the fuel-bounded kernel-leaf PM-basis core approximates the +problem and has the principal row width. -/ +theorem pmBasisWithFuelCore_kernelLeaf_rows (mulCtx : CPolynomial.MulContext F) + (lowCtx : PolynomialMatrix.MulLowContext F) + (leafCutoff composeLeafCutoff : Nat) : + ∀ (fuel : Nat) (problem : XAdicProblem F) (shift : Array Nat), + ∀ row ∈ MatrixRows (pmBasisWithFuelCore + (kernelLeafRuntimeWithLowAndCompose mulCtx lowCtx leafCutoff + composeLeafCutoff) fuel problem shift), + RowApproximates mulCtx problem row ∧ row.size = problem.matrix.size := by + intro fuel + induction fuel with + | zero => + intro problem shift row hrow + rw [pmBasisWithFuelCore] at hrow + exact kernelLeafBasis_rows mulCtx problem shift row + (compactNonzeroRows_subset hrow) + | succ fuel ih => + intro problem shift row hrow + rw [pmBasisWithFuelCore] at hrow + split at hrow + · exact kernelLeafBasis_rows mulCtx problem shift row + (compactNonzeroRows_subset hrow) + · set d₁ := maxOrder problem / 2 with hd₁ + set lower : XAdicProblem F := + { orders := lowerOrders problem d₁, matrix := problem.matrix } with hlower + set P₁ := pmBasisWithFuelCore + (kernelLeafRuntimeWithLowAndCompose mulCtx lowCtx leafCutoff + composeLeafCutoff) fuel lower shift with hP₁def + set resOrders := residualOrders problem d₁ with hresOrders + set Rmat := residualMatrixWithProduct + (mulTruncColumnStrassenWith lowCtx composeLeafCutoff) P₁ + problem.matrix d₁ resOrders with hRmatdef + set residual : XAdicProblem F := + { orders := resOrders, matrix := Rmat } with hresidual + set shifted := updateShiftByRows P₁ shift with hshifted + set P₂ := pmBasisWithFuelCore + (kernelLeafRuntimeWithLowAndCompose mulCtx lowCtx leafCutoff + composeLeafCutoff) fuel residual shifted with hP₂def + have hrow2 : row ∈ MatrixRows (compactNonzeroRows + (mulStrassenWith lowCtx composeLeafCutoff P₂ P₁)) := hrow + have hmem := compactNonzeroRows_subset hrow2 + have hnz := compactNonzeroRows_nonzero hrow2 + rw [mulStrassenWith_eq_mulWith, matrixRows_mulWith] at hmem + rcases List.mem_map.mp hmem with ⟨p₂, hp₂mem, rfl⟩ + have hctx : rowMulMatrixWith lowCtx.mulContext p₂ P₁ = + rowMulMatrixWith mulCtx p₂ P₁ := rowMulMatrixWith_ctx _ _ _ _ + rw [hctx] at hnz ⊢ + have hIH₁ : ∀ r ∈ MatrixRows P₁, + RowApproximates mulCtx lower r ∧ r.size = problem.matrix.size := + fun r hr ↦ ih lower shift r hr + have hIH₂ := ih residual shifted p₂ hp₂mem + rcases Nat.eq_zero_or_pos P₁.size with hP₁0 | hP₁0 + · exfalso + have hP₁empty : P₁ = #[] := Array.eq_empty_of_size_eq_zero hP₁0 + have hsize0 : (rowMulMatrixWith mulCtx p₂ P₁).size = 0 := by + rw [rowMulMatrixWith_size, hP₁empty] + rfl + rw [Array.eq_empty_of_size_eq_zero hsize0] at hnz + simp [rowIsZero] at hnz + · -- Shape facts for the residual matrix. + have hprodsize : (mulTruncColumnStrassenWith lowCtx composeLeafCutoff + (resOrders.map fun order ↦ order + d₁) P₁ problem.matrix).size = + P₁.size := mulTruncColumnStrassenWith_size _ _ _ _ _ + have hRsize : Rmat.size = P₁.size := by + rw [hRmatdef] + simp only [residualMatrixWithProduct] + rw [ofFn_size, hprodsize] + have hprodwidth : 0 < P₁.size → + MatrixWidth (mulTruncColumnStrassenWith lowCtx composeLeafCutoff + (resOrders.map fun order ↦ order + d₁) P₁ problem.matrix) = + MatrixWidth problem.matrix := by + intro hpos + rw [mulTruncColumnStrassenWith_eq_truncateColumns] + refine matrixWidth_eq_of_first_row + (by rw [truncateColumns_size, mulWith_size]; omega) ?_ + intro r hr + rw [MatrixRows, truncateColumns, Array.toList_map] at hr + rcases List.mem_map.mp hr with ⟨r', hr', rfl⟩ + have hsize' : (rowTruncateColumns + (resOrders.map fun order ↦ order + d₁) r').size = r'.size := by + simp [rowTruncateColumns] + rw [hsize'] + have hr'' : r' ∈ (MatrixRows P₁).map fun r ↦ + rowMulMatrixWith lowCtx.mulContext r problem.matrix := by + rw [← matrixRows_mulWith] + exact hr' + rcases List.mem_map.mp hr'' with ⟨r'', _hr'', rfl⟩ + rw [rowMulMatrixWith_size] + have hRwidth : 0 < P₁.size → + MatrixWidth problem.matrix ≤ MatrixWidth Rmat := by + intro hpos + rw [hRmatdef] + simp only [residualMatrixWithProduct] + rw [MatrixWidth_ofFn, if_neg (by omega), hprodwidth hpos] + constructor + · refine rowApproximates_composed mulCtx hIH₁ hRsize hRwidth ?_ hIH₂.1 + intro l j hl hj hjw t ht + have hd₁lt : d₁ < problem.orders.getD j 0 := by omega + have hjres : j < resOrders.size := by + rw [hresOrders, residualOrders] + simpa using hj + have hRentry : rowGet (Rmat.getD l #[]) j = + divXTrunc d₁ (resOrders.getD j 0) + (rowGet ((mulTruncColumnStrassenWith lowCtx composeLeafCutoff + (resOrders.map fun order ↦ order + d₁) P₁ + problem.matrix).getD l #[]) j) := by + rw [hRmatdef] + simp only [residualMatrixWithProduct] + rw [rowGet_ofFn, if_pos ⟨by rw [hprodsize]; exact hl, + by rw [hprodwidth hP₁0]; exact hjw⟩] + rw [hRentry, divXTrunc_coeff, + if_pos (by rw [hresOrders, residualOrders, + natArray_map_getD _ _ hj]; omega)] + rw [mulTruncColumnStrassenWith_entry lowCtx composeLeafCutoff _ P₁ + problem.matrix (by omega) j] + rw [truncateX_coeff, if_pos (by + rw [natArray_map_getD _ _ hjres, hresOrders, residualOrders, + natArray_map_getD _ _ hj] + omega)] + rw [rowMulMatrixWith_ctx lowCtx.mulContext mulCtx] + · rw [rowMulMatrixWith_size, + matrixWidth_eq_of_first_row hP₁0 (fun r hr ↦ (hIH₁ r hr).2)] + +/-- Every row of the kernel-leaf recursive PM-basis satisfies the X-adic +approximant conditions. -/ +theorem pmBasis_kernelLeaf_approximates (mulCtx : CPolynomial.MulContext F) + (lowCtx : PolynomialMatrix.MulLowContext F) + (leafCutoff composeLeafCutoff : Nat) + (problem : XAdicProblem F) (shift : Array Nat) {row : PolynomialRow F} + (hrow : row ∈ MatrixRows (pmBasis + (kernelLeafRuntimeWithLowAndCompose mulCtx lowCtx leafCutoff + composeLeafCutoff) problem shift)) : + RowApproximates mulCtx problem row := by + rw [pmBasis, pmBasisWithFuel, pmBasisNormalizeRoot] at hrow + refine completeMissingPivotRows_approximates mulCtx problem shift ?_ hrow + intro r hr + have hr' := compactNonzeroRows_subset hr + rw [reduceKernelLeafRows] at hr' + refine reduceKernelLeafRowsByPivots_invariant (RowApproximates mulCtx problem) + (fun t r' s ht hr'' ↦ + rowApproximates_cancelKernelLeafLeadingTerm mulCtx problem s ht hr'') + shift ?_ hr' + intro y hy + exact (pmBasisWithFuelCore_kernelLeaf_rows mulCtx lowCtx leafCutoff + composeLeafCutoff _ problem shift y hy).1 + +/-! ## Generation completeness of the recursive PM-basis core + +Every nonzero solution row of an X-adic problem lies in the row module +generated by the recursive PM-basis core. Together with the shifted weak-Popov +shape of the root normalization this yields the predictable-degree minimality +of the final basis. -/ + +omit [LawfulBEq F] in +/-- Zero rows read as zero in every coordinate. -/ +private theorem pm_rowGet_eq_zero_of_rowIsZero {row : PolynomialRow F} + (h : RowIsZero row) (j : Nat) : rowGet row j = 0 := by + rcases Nat.lt_or_ge j row.size with hj | hj + · rw [rowGet, array_getD_of_lt' _ _ hj] + exact h row[j] (Array.getElem_mem_toList hj) + · rw [rowGet, array_getD_of_le' _ _ hj] + +omit [LawfulBEq F] in +/-- Rows reading as zero in every coordinate are zero rows. -/ +private theorem pm_rowIsZero_of_rowGet {row : PolynomialRow F} + (h : ∀ j, rowGet row j = 0) : RowIsZero row := by + intro p hp + rcases List.getElem_of_mem hp with ⟨j, hj, hget⟩ + have hj' : j < row.size := by simpa using hj + have hzero := h j + rw [rowGet, array_getD_of_lt' _ _ hj'] at hzero + rw [← hget, Array.getElem_toList] + exact hzero + +/-- Spans of all-zero row sets contain only zero rows. -/ +private theorem pm_rowIsZero_of_mem_rowSpan_all_zero {X : PolynomialMatrix F} + (hall : ∀ r ∈ MatrixRows X, RowIsZero r) {row : PolynomialRow F} + (hrow : row ∈ RowSpan X) : RowIsZero row := by + rcases hrow with ⟨coeffs, _hsize, rfl⟩ + refine pm_rowIsZero_of_rowGet fun j ↦ ?_ + rw [pm_rowGet_rowLinearCombination] + refine Finset.sum_eq_zero fun i hi ↦ ?_ + rw [pm_rowGet_eq_zero_of_rowIsZero + (hall _ (getD_mem_matrixRows (Finset.mem_range.mp hi))) j, mul_zero] + +/-- Span transfer for uniform-width matrices: nonzero span members move along +any row map that covers the nonzero source rows. -/ +private theorem pm_mem_rowSpan_of_nonzero_rows_mem [DecidableEq F] + {A B : PolynomialMatrix F} {n : Nat} (hn : 0 < n) + (hA : ∀ r ∈ MatrixRows A, r.size = n) + (hB : ∀ r ∈ MatrixRows B, r.size = n) + (hrows : ∀ r ∈ MatrixRows A, ¬ RowIsZero r → r ∈ RowSpan B) + {row : PolynomialRow F} (hrow : row ∈ RowSpan A) (hnz : ¬ RowIsZero row) : + row ∈ RowSpan B := by + by_cases hallzero : ∀ r ∈ MatrixRows A, RowIsZero r + · exact absurd (pm_rowIsZero_of_mem_rowSpan_all_zero hallzero hrow) hnz + · have hex : ∃ r ∈ MatrixRows A, ¬ RowIsZero r := by + by_contra hnot + refine hallzero fun r hr ↦ ?_ + by_contra hz + exact hnot ⟨r, hr, hz⟩ + rcases hex with ⟨r₀, hr₀mem, hr₀nz⟩ + have hr₀B := hrows r₀ hr₀mem hr₀nz + have hBpos : 0 < B.size := by + rcases Nat.eq_zero_or_pos B.size with h0 | hp + · exfalso + rw [Array.eq_empty_of_size_eq_zero h0] at hr₀B + have hr₀empty := eq_empty_of_mem_rowSpan_empty hr₀B + have := hA r₀ hr₀mem + rw [hr₀empty] at this + simp at this + omega + · exact hp + have hApos : 0 < A.size := size_pos_of_mem_matrixRows hr₀mem + have hwB : MatrixWidth B = n := matrixWidth_eq_of_first_row hBpos hB + refine rowSpan_subset_of_rows_mem (wellFormed_of_sizes hB) + ((matrixWidth_eq_of_first_row hApos hA).trans hwB.symm) ?_ hrow + intro r hr + by_cases hz : RowIsZero r + · have hreq : r = zeroRow n := by + rw [← hA r hr] + exact rowIsZero_eq_zeroRow hz + rw [hreq, ← hwB] + exact zeroRow_mem_rowSpan (wellFormed_of_sizes hB) + · exact hrows r hr hz + +/-- Nonzero rows survive zero-row compaction. -/ +private theorem pm_mem_matrixRows_compactNonzeroRows {X : PolynomialMatrix F} + {r : PolynomialRow F} (hr : r ∈ MatrixRows X) (hnz : ¬ RowIsZero r) : + r ∈ MatrixRows (compactNonzeroRows X) := by + rw [MatrixRows, ← Array.mem_def] at hr ⊢ + rw [compactNonzeroRows, Array.mem_filter] + refine ⟨hr, ?_⟩ + cases hb : rowIsZero r + · rfl + · exact absurd (rowIsZero_iff.1 hb) hnz + +/-- Zero-row compaction keeps every nonzero span member. -/ +private theorem pm_mem_rowSpan_compactNonzeroRows [DecidableEq F] + {X : PolynomialMatrix F} {n : Nat} (hn : 0 < n) + (hsizes : ∀ r ∈ MatrixRows X, r.size = n) + {row : PolynomialRow F} (hrow : row ∈ RowSpan X) (hnz : ¬ RowIsZero row) : + row ∈ RowSpan (compactNonzeroRows X) := by + refine pm_mem_rowSpan_of_nonzero_rows_mem hn hsizes + (fun r hr ↦ hsizes r (compactNonzeroRows_subset hr)) ?_ hrow hnz + intro r hr hrnz + exact matrix_row_mem_rowSpan + (wellFormed_of_sizes fun s hs ↦ hsizes s (compactNonzeroRows_subset hs)) + (pm_mem_matrixRows_compactNonzeroRows hr hrnz) + +/-- `toPoly` distinguishes canonical polynomials. -/ +private theorem pm_eq_of_toPoly_eq {a b : CPolynomial F} + (h : a.toPoly = b.toPoly) : a = b := by + apply CPolynomial.eq_iff_coeff.2 + intro t + rw [CPolynomial.coeff_toPoly, CPolynomial.coeff_toPoly, h] + +/-- Column entries of a combination-by-matrix product as coefficient-weighted +sums of the row-by-matrix products. -/ +private theorem pm_combination_mul_toPoly (mulCtx : CPolynomial.MulContext F) + {P₁ M : PolynomialMatrix F} (q : PolynomialRow F) {j : Nat} + (hj : j < MatrixWidth M) : + (rowGet (rowMulMatrixWith mulCtx (rowLinearCombination q P₁) M) j).toPoly = + ∑ l ∈ Finset.range P₁.size, (rowGet q l).toPoly * + (rowGet (rowMulMatrixWith mulCtx (P₁.getD l #[]) M) j).toPoly := by + rw [rowGet_rowMulMatrixWith_toPoly mulCtx _ M hj] + have hentry : ∀ k, (rowGet (rowLinearCombination q P₁) k).toPoly = + ∑ l ∈ Finset.range P₁.size, + (rowGet q l).toPoly * (rowGet (P₁.getD l #[]) k).toPoly := by + intro k + rw [pm_rowGet_rowLinearCombination, + pm_toPoly_finset_sum (fun l ↦ q.getD l 0 * rowGet (P₁.getD l #[]) k) + P₁.size] + refine Finset.sum_congr rfl fun l _hl ↦ ?_ + rw [CPolynomial.toPoly_mul] + rfl + calc ∑ k ∈ Finset.range M.size, + (rowGet (rowLinearCombination q P₁) k).toPoly * + (rowGet (M.getD k #[]) j).toPoly + = ∑ k ∈ Finset.range M.size, ∑ l ∈ Finset.range P₁.size, + (rowGet q l).toPoly * (rowGet (P₁.getD l #[]) k).toPoly * + (rowGet (M.getD k #[]) j).toPoly := by + refine Finset.sum_congr rfl fun k _hk ↦ ?_ + rw [hentry k, Finset.sum_mul] + _ = ∑ l ∈ Finset.range P₁.size, ∑ k ∈ Finset.range M.size, + (rowGet q l).toPoly * (rowGet (P₁.getD l #[]) k).toPoly * + (rowGet (M.getD k #[]) j).toPoly := Finset.sum_comm + _ = ∑ l ∈ Finset.range P₁.size, (rowGet q l).toPoly * + (rowGet (rowMulMatrixWith mulCtx (P₁.getD l #[]) M) j).toPoly := by + refine Finset.sum_congr rfl fun l _hl ↦ ?_ + rw [rowGet_rowMulMatrixWith_toPoly mulCtx _ M hj, Finset.mul_sum] + refine Finset.sum_congr rfl fun k _hk ↦ ?_ + ring + +/-- Converse of the composition soundness step: the coefficient row of a +full-problem solution against a lower basis solves the residual problem. -/ +private theorem pm_rowApproximates_residual_of_combination + (mulCtx : CPolynomial.MulContext F) + {problem : XAdicProblem F} {d₁ : Nat} {P₁ Rmat : PolynomialMatrix F} + (hP₁ : ∀ r ∈ MatrixRows P₁, + RowApproximates mulCtx + { orders := lowerOrders problem d₁, matrix := problem.matrix } r ∧ + r.size = problem.matrix.size) + (hRsize : Rmat.size = P₁.size) + (hRwidth : MatrixWidth Rmat ≤ MatrixWidth problem.matrix) + (hR : ∀ l j, l < P₁.size → j < problem.orders.size → + j < MatrixWidth problem.matrix → + ∀ t, t < problem.orders.getD j 0 - d₁ → + CPolynomial.coeff (rowGet (Rmat.getD l #[]) j) t = + CPolynomial.coeff + (rowGet (rowMulMatrixWith mulCtx (P₁.getD l #[]) problem.matrix) j) + (t + d₁)) + {q : PolynomialRow F} + (happrox : RowApproximates mulCtx problem (rowLinearCombination q P₁)) : + RowApproximates mulCtx + { orders := residualOrders problem d₁, matrix := Rmat } q := by + rw [rowApproximates_iff] + intro j hj hjw + have hj' : j < problem.orders.size := by + simpa [residualOrders] using hj + have hjwM : j < MatrixWidth problem.matrix := Nat.lt_of_lt_of_le hjw hRwidth + set o := problem.orders.getD j 0 with ho + have horder : (residualOrders problem d₁).getD j 0 = o - d₁ := by + rw [residualOrders, natArray_map_getD _ _ hj'] + rw [horder] + rcases Nat.le_total o d₁ with hod | hod + · rw [Nat.sub_eq_zero_of_le hod, pow_zero] + exact one_dvd _ + · rcases Nat.eq_zero_or_pos P₁.size with hP₁0 | _hP₁0 + · rw [hRsize, hP₁0, Finset.range_zero, Finset.sum_empty] + exact dvd_zero _ + · have hP₁dvd : ∀ l, l < P₁.size → + (Polynomial.X : Polynomial F) ^ d₁ ∣ + (rowGet (rowMulMatrixWith mulCtx (P₁.getD l #[]) + problem.matrix) j).toPoly := by + intro l hl + have hmem := getD_mem_matrixRows hl + have happroxl := (hP₁ _ hmem).1 j (by simpa [lowerOrders] using hj') + rw [truncateX_eq_zero_iff_X_pow_dvd] at happroxl + have horderl : (lowerOrders problem d₁).getD j 0 = min o d₁ := by + rw [lowerOrders, natArray_map_getD _ _ hj'] + rwa [horderl, Nat.min_eq_right hod] at happroxl + have hquot : ∀ l, ∃ sl : Polynomial F, l < P₁.size → + (rowGet (rowMulMatrixWith mulCtx (P₁.getD l #[]) + problem.matrix) j).toPoly = Polynomial.X ^ d₁ * sl := by + intro l + by_cases hl : l < P₁.size + · rcases hP₁dvd l hl with ⟨sl, hsl⟩ + exact ⟨sl, fun _ ↦ hsl⟩ + · exact ⟨0, fun h ↦ absurd h hl⟩ + choose s hs using hquot + have hrowdvd : (Polynomial.X : Polynomial F) ^ o ∣ + ∑ l ∈ Finset.range P₁.size, (rowGet q l).toPoly * + (rowGet (rowMulMatrixWith mulCtx (P₁.getD l #[]) + problem.matrix) j).toPoly := by + have h0 := (rowApproximates_iff mulCtx problem _).1 happrox j hj' hjwM + rwa [← rowGet_rowMulMatrixWith_toPoly mulCtx _ problem.matrix hjwM, + pm_combination_mul_toPoly mulCtx q hjwM] at h0 + have hsum : ∑ l ∈ Finset.range P₁.size, (rowGet q l).toPoly * + (rowGet (rowMulMatrixWith mulCtx (P₁.getD l #[]) + problem.matrix) j).toPoly = + Polynomial.X ^ d₁ * + ∑ l ∈ Finset.range P₁.size, (rowGet q l).toPoly * s l := by + rw [Finset.mul_sum] + refine Finset.sum_congr rfl fun l hl ↦ ?_ + rw [hs l (Finset.mem_range.mp hl)] + ring + rw [hsum, show o = d₁ + (o - d₁) from by omega, pow_add] at hrowdvd + have hquotsum : (Polynomial.X : Polynomial F) ^ (o - d₁) ∣ + ∑ l ∈ Finset.range P₁.size, (rowGet q l).toPoly * s l := + (mul_dvd_mul_iff_left (pow_ne_zero d₁ Polynomial.X_ne_zero)).1 hrowdvd + have hcong : ∀ l, l < P₁.size → + (Polynomial.X : Polynomial F) ^ (o - d₁) ∣ + (rowGet (Rmat.getD l #[]) j).toPoly - s l := by + intro l hl + rw [Polynomial.X_pow_dvd_iff] + intro t ht + rw [Polynomial.coeff_sub, ← CPolynomial.coeff_toPoly, + hR l j hl hj' hjwM t (by omega), CPolynomial.coeff_toPoly] + have hcoeff : ((rowGet (rowMulMatrixWith mulCtx (P₁.getD l #[]) + problem.matrix) j).toPoly).coeff (t + d₁) = + (Polynomial.X ^ d₁ * s l).coeff (t + d₁) := by + rw [hs l hl] + rw [Polynomial.coeff_X_pow_mul] at hcoeff + rw [hcoeff] + exact sub_self _ + have hfinal : ∑ k ∈ Finset.range Rmat.size, (rowGet q k).toPoly * + (rowGet (Rmat.getD k #[]) j).toPoly = + (∑ l ∈ Finset.range P₁.size, (rowGet q l).toPoly * s l) + + ∑ l ∈ Finset.range P₁.size, (rowGet q l).toPoly * + ((rowGet (Rmat.getD l #[]) j).toPoly - s l) := by + rw [hRsize, ← Finset.sum_add_distrib] + refine Finset.sum_congr rfl fun l _hl ↦ ?_ + ring + rw [hfinal] + exact dvd_add hquotsum + (Finset.dvd_sum fun l hl ↦ + Dvd.dvd.mul_left (hcong l (Finset.mem_range.mp hl)) _) + +/-- Combinations against a basis of combinations are combinations against the +composed product matrix. -/ +private theorem pm_rowLinearCombination_combination + (mulCtx : CPolynomial.MulContext F) + {P₁ P₂ : PolynomialMatrix F} {n : Nat} + (hP₁pos : 0 < P₁.size) (hP₂pos : 0 < P₂.size) + (hsizes : ∀ r ∈ MatrixRows P₁, r.size = n) + (w : Array (CPolynomial F)) : + rowLinearCombination (rowLinearCombination w P₂) P₁ = + rowLinearCombination w (mulWith mulCtx P₂ P₁) := by + have hw₁ : MatrixWidth P₁ = n := matrixWidth_eq_of_first_row hP₁pos hsizes + have hCsize : (mulWith mulCtx P₂ P₁).size = P₂.size := mulWith_size mulCtx P₂ P₁ + have hCsizes : ∀ r ∈ MatrixRows (mulWith mulCtx P₂ P₁), r.size = n := by + intro r hr + rw [matrixRows_mulWith] at hr + rcases List.mem_map.mp hr with ⟨p₂, _hp₂, rfl⟩ + rw [rowMulMatrixWith_size, hw₁] + have hCw : MatrixWidth (mulWith mulCtx P₂ P₁) = n := + matrixWidth_eq_of_first_row (by omega) hCsizes + refine pm_row_ext ?_ fun j ↦ ?_ + · rw [pm_rowLinearCombination_size hsizes hw₁, + pm_rowLinearCombination_size hCsizes hCw] + · rw [pm_rowGet_rowLinearCombination, pm_rowGet_rowLinearCombination, hCsize] + rcases Nat.lt_or_ge j n with hj | hj + · refine pm_eq_of_toPoly_eq ?_ + rw [pm_toPoly_finset_sum + (fun l ↦ (rowLinearCombination w P₂).getD l 0 * rowGet (P₁.getD l #[]) j) + P₁.size] + rw [pm_toPoly_finset_sum + (fun i ↦ w.getD i 0 * rowGet ((mulWith mulCtx P₂ P₁).getD i #[]) j) + P₂.size] + have hjw₁ : j < MatrixWidth P₁ := by omega + calc ∑ l ∈ Finset.range P₁.size, + ((rowLinearCombination w P₂).getD l 0 * + rowGet (P₁.getD l #[]) j).toPoly + = ∑ l ∈ Finset.range P₁.size, ∑ i ∈ Finset.range P₂.size, + (w.getD i 0).toPoly * (rowGet (P₂.getD i #[]) l).toPoly * + (rowGet (P₁.getD l #[]) j).toPoly := by + refine Finset.sum_congr rfl fun l _hl ↦ ?_ + rw [CPolynomial.toPoly_mul] + have hwl : ((rowLinearCombination w P₂).getD l 0).toPoly = + ∑ i ∈ Finset.range P₂.size, + (w.getD i 0).toPoly * (rowGet (P₂.getD i #[]) l).toPoly := by + rw [show (rowLinearCombination w P₂).getD l 0 = + rowGet (rowLinearCombination w P₂) l from rfl, + pm_rowGet_rowLinearCombination, + pm_toPoly_finset_sum + (fun i ↦ w.getD i 0 * rowGet (P₂.getD i #[]) l) P₂.size] + refine Finset.sum_congr rfl fun i _hi ↦ ?_ + rw [CPolynomial.toPoly_mul] + rw [hwl, Finset.sum_mul] + _ = ∑ i ∈ Finset.range P₂.size, ∑ l ∈ Finset.range P₁.size, + (w.getD i 0).toPoly * (rowGet (P₂.getD i #[]) l).toPoly * + (rowGet (P₁.getD l #[]) j).toPoly := Finset.sum_comm + _ = ∑ i ∈ Finset.range P₂.size, + (w.getD i 0 * rowGet ((mulWith mulCtx P₂ P₁).getD i #[]) j).toPoly := by + refine Finset.sum_congr rfl fun i hi ↦ ?_ + rw [CPolynomial.toPoly_mul, + mulWith_getD mulCtx P₂ P₁ (Finset.mem_range.mp hi), + rowGet_rowMulMatrixWith_toPoly mulCtx _ P₁ hjw₁, Finset.mul_sum] + refine Finset.sum_congr rfl fun l _hl ↦ ?_ + ring + · refine Eq.trans (Finset.sum_eq_zero fun l hl ↦ ?_) + (Finset.sum_eq_zero fun i hi ↦ ?_).symm + · have hzero : rowGet (P₁.getD l #[]) j = 0 := by + rw [rowGet, array_getD_of_le' _ _ (by + rw [hsizes _ (getD_mem_matrixRows (Finset.mem_range.mp hl))] + omega)] + rw [hzero, mul_zero] + · have hi' : i < (mulWith mulCtx P₂ P₁).size := by + rw [hCsize] + exact Finset.mem_range.mp hi + have hzero : rowGet ((mulWith mulCtx P₂ P₁).getD i #[]) j = 0 := by + rw [rowGet, array_getD_of_le' _ _ (by + rw [hCsizes _ (getD_mem_matrixRows hi')] + omega)] + rw [hzero, mul_zero] + +/-- **Generation completeness of the recursive PM-basis core.** Every nonzero +solution row of an X-adic problem lies in the row module generated by the +fuel-bounded recursive PM-basis core. -/ +theorem pmBasisWithFuelCore_kernelLeaf_rowSpan_complete [DecidableEq F] + (mulCtx : CPolynomial.MulContext F) + (lowCtx : PolynomialMatrix.MulLowContext F) + (leafCutoff composeLeafCutoff : Nat) : + ∀ (fuel : Nat) (problem : XAdicProblem F) (shift : Array Nat), + 0 < problem.matrix.size → WellFormed problem.matrix → + ∀ row : PolynomialRow F, RowApproximates mulCtx problem row → + row.size = problem.matrix.size → ¬ RowIsZero row → + row ∈ RowSpan (pmBasisWithFuelCore + (kernelLeafRuntimeWithLowAndCompose mulCtx lowCtx leafCutoff + composeLeafCutoff) fuel problem shift) := by + intro fuel + induction fuel with + | zero => + intro problem shift hpos hwf row happrox hsize hnz + rw [pmBasisWithFuelCore] + exact pm_mem_rowSpan_compactNonzeroRows hpos + (fun r hr ↦ (kernelLeafBasis_rows mulCtx problem shift r hr).2) + (kernelLeafBasis_rowSpan_complete mulCtx problem shift hpos hwf + happrox hsize hnz) + hnz + | succ fuel ih => + intro problem shift hpos hwf row happrox hsize hnz + rw [pmBasisWithFuelCore] + split + · exact pm_mem_rowSpan_compactNonzeroRows hpos + (fun r hr ↦ (kernelLeafBasis_rows mulCtx problem shift r hr).2) + (kernelLeafBasis_rowSpan_complete mulCtx problem shift hpos hwf + happrox hsize hnz) + hnz + · set d₁ := maxOrder problem / 2 with hd₁ + set lower : XAdicProblem F := + { orders := lowerOrders problem d₁, matrix := problem.matrix } + with hlower + set P₁ := pmBasisWithFuelCore + (kernelLeafRuntimeWithLowAndCompose mulCtx lowCtx leafCutoff + composeLeafCutoff) fuel lower shift with hP₁def + set resOrders := residualOrders problem d₁ with hresOrders + set Rmat := residualMatrixWithProduct + (mulTruncColumnStrassenWith lowCtx composeLeafCutoff) P₁ + problem.matrix d₁ resOrders with hRmatdef + set residual : XAdicProblem F := + { orders := resOrders, matrix := Rmat } with hresidual + set shifted := updateShiftByRows P₁ shift with hshifted + set P₂ := pmBasisWithFuelCore + (kernelLeafRuntimeWithLowAndCompose mulCtx lowCtx leafCutoff + composeLeafCutoff) fuel residual shifted with hP₂def + have hIH₁ : ∀ r ∈ MatrixRows P₁, + RowApproximates mulCtx lower r ∧ r.size = problem.matrix.size := + fun r hr ↦ pmBasisWithFuelCore_kernelLeaf_rows mulCtx lowCtx + leafCutoff composeLeafCutoff fuel lower shift r hr + -- The row solves the lower-order problem. + have hlowapprox : RowApproximates mulCtx lower row := by + rw [rowApproximates_iff] at happrox ⊢ + intro j hj hjw + have hj' : j < problem.orders.size := by + simpa [hlower, lowerOrders] using hj + refine dvd_trans (pow_dvd_pow _ ?_) (happrox j hj' hjw) + have horder : (lowerOrders problem d₁).getD j 0 = + min (problem.orders.getD j 0) d₁ := by + rw [lowerOrders, natArray_map_getD _ _ hj'] + rw [hlower] + simp only [horder] + omega + have hrow₁ : row ∈ RowSpan P₁ := + ih lower shift hpos hwf row hlowapprox hsize hnz + rcases Nat.eq_zero_or_pos P₁.size with hP₁0 | hP₁0 + · exfalso + rw [Array.eq_empty_of_size_eq_zero hP₁0] at hrow₁ + have hempty := eq_empty_of_mem_rowSpan_empty hrow₁ + rw [hempty] at hsize + simp at hsize + omega + · rcases hrow₁ with ⟨q, hqsize, hqeq⟩ + -- Shape facts for the residual matrix. + have hprodsize : (mulTruncColumnStrassenWith lowCtx composeLeafCutoff + (resOrders.map fun order ↦ order + d₁) P₁ problem.matrix).size = + P₁.size := mulTruncColumnStrassenWith_size _ _ _ _ _ + have hRsize : Rmat.size = P₁.size := by + rw [hRmatdef] + simp only [residualMatrixWithProduct] + rw [ofFn_size, hprodsize] + have hprodwidth : + MatrixWidth (mulTruncColumnStrassenWith lowCtx composeLeafCutoff + (resOrders.map fun order ↦ order + d₁) P₁ problem.matrix) = + MatrixWidth problem.matrix := by + rw [mulTruncColumnStrassenWith_eq_truncateColumns] + refine matrixWidth_eq_of_first_row + (by rw [truncateColumns_size, mulWith_size]; omega) ?_ + intro r hr + rw [MatrixRows, truncateColumns, Array.toList_map] at hr + rcases List.mem_map.mp hr with ⟨r', hr', rfl⟩ + have hsize' : (rowTruncateColumns + (resOrders.map fun order ↦ order + d₁) r').size = r'.size := by + simp [rowTruncateColumns] + rw [hsize'] + have hr'' : r' ∈ (MatrixRows P₁).map fun r ↦ + rowMulMatrixWith lowCtx.mulContext r problem.matrix := by + rw [← matrixRows_mulWith] + exact hr' + rcases List.mem_map.mp hr'' with ⟨r'', _hr'', rfl⟩ + rw [rowMulMatrixWith_size] + have hRwidth : MatrixWidth Rmat = MatrixWidth problem.matrix := by + rw [hRmatdef] + simp only [residualMatrixWithProduct] + rw [MatrixWidth_ofFn, if_neg (by omega), hprodwidth] + have hRcong : ∀ l j, l < P₁.size → j < problem.orders.size → + j < MatrixWidth problem.matrix → + ∀ t, t < problem.orders.getD j 0 - d₁ → + CPolynomial.coeff (rowGet (Rmat.getD l #[]) j) t = + CPolynomial.coeff + (rowGet (rowMulMatrixWith mulCtx (P₁.getD l #[]) + problem.matrix) j) (t + d₁) := by + intro l j hl hj hjw t ht + have hd₁lt : d₁ < problem.orders.getD j 0 := by omega + have hjres : j < resOrders.size := by + rw [hresOrders, residualOrders] + simpa using hj + have hRentry : rowGet (Rmat.getD l #[]) j = + divXTrunc d₁ (resOrders.getD j 0) + (rowGet ((mulTruncColumnStrassenWith lowCtx composeLeafCutoff + (resOrders.map fun order ↦ order + d₁) P₁ + problem.matrix).getD l #[]) j) := by + rw [hRmatdef] + simp only [residualMatrixWithProduct] + rw [rowGet_ofFn, if_pos ⟨by rw [hprodsize]; exact hl, + by rw [hprodwidth]; exact hjw⟩] + rw [hRentry, divXTrunc_coeff, + if_pos (by rw [hresOrders, residualOrders, + natArray_map_getD _ _ hj]; omega)] + rw [mulTruncColumnStrassenWith_entry lowCtx composeLeafCutoff _ P₁ + problem.matrix (by omega) j] + rw [truncateX_coeff, if_pos (by + rw [natArray_map_getD _ _ hjres, hresOrders, residualOrders, + natArray_map_getD _ _ hj] + omega)] + rw [rowMulMatrixWith_ctx lowCtx.mulContext mulCtx] + -- The combination coefficients are nonzero and solve the residual. + have hq_nz : ¬ RowIsZero q := by + intro hqz + apply hnz + rw [hqeq] + refine pm_rowIsZero_of_rowGet fun j ↦ ?_ + rw [pm_rowGet_rowLinearCombination] + refine Finset.sum_eq_zero fun i _hi ↦ ?_ + rw [show q.getD i 0 = rowGet q i from rfl, + pm_rowGet_eq_zero_of_rowIsZero hqz, zero_mul] + have hqapprox : RowApproximates mulCtx residual q := by + rw [hresidual, hresOrders] + refine pm_rowApproximates_residual_of_combination mulCtx hIH₁ + hRsize (le_of_eq hRwidth) hRcong ?_ + rw [← hqeq] + exact happrox + have hwf_R : WellFormed Rmat := by + refine wellFormed_of_sizes + (n := MatrixWidth (mulTruncColumnStrassenWith lowCtx + composeLeafCutoff (resOrders.map fun order ↦ order + d₁) P₁ + problem.matrix)) ?_ + intro r hr + rcases List.getElem_of_mem hr with ⟨i, hi, hget⟩ + have hi' : i < Rmat.size := by simpa [MatrixRows] using hi + have hr_eq : Rmat.getD i #[] = r := by + rw [array_getD_of_lt' _ _ hi', ← Array.getElem_toList] + exact hget + rw [← hr_eq, hRmatdef] + simp only [residualMatrixWithProduct] + rw [getD_ofFn, if_pos (by + rw [hRmatdef] at hi' + simpa [residualMatrixWithProduct, ofFn_size] using hi')] + simp + have hRpos : 0 < Rmat.size := by omega + have hq_size : q.size = Rmat.size := by omega + have hrow₂ : q ∈ RowSpan P₂ := + ih residual shifted hRpos hwf_R q hqapprox hq_size hq_nz + rcases hrow₂ with ⟨w, hwsize, hweq⟩ + have hP₂pos : 0 < P₂.size := by + rcases Nat.eq_zero_or_pos P₂.size with h0 | hp + · exfalso + rw [Array.eq_empty_of_size_eq_zero h0] at hweq + have hqempty : q = #[] := by + rw [hweq, rowLinearCombination] + rfl + rw [hqempty] at hq_nz + exact hq_nz rowIsZero_empty + · exact hp + -- Compose the two combinations through the product basis. + have hdistrib : row = + rowLinearCombination w (mulWith lowCtx.mulContext P₂ P₁) := by + rw [hqeq, hweq] + exact pm_rowLinearCombination_combination lowCtx.mulContext hP₁0 + hP₂pos (fun r hr ↦ (hIH₁ r hr).2) w + have hfinal : row ∈ RowSpan (compactNonzeroRows + (mulStrassenWith lowCtx composeLeafCutoff P₂ P₁)) := by + rw [mulStrassenWith_eq_mulWith] + refine pm_mem_rowSpan_compactNonzeroRows hpos ?_ ?_ hnz + · intro r hr + rw [matrixRows_mulWith] at hr + rcases List.mem_map.mp hr with ⟨p₂, _hp₂, rfl⟩ + rw [rowMulMatrixWith_size, + matrixWidth_eq_of_first_row hP₁0 fun r hr ↦ (hIH₁ r hr).2] + · exact ⟨w, by rw [mulWith_size]; omega, hdistrib⟩ + exact hfinal + +/-! ## Root normalization shape and shifted minimality -/ + +omit [BEq F] [LawfulBEq F] in +/-- Zero padding on the right does not change row reads. -/ +private theorem pm_rowGet_append_replicate (row : PolynomialRow F) + (m k : Nat) : + rowGet (row ++ Array.replicate m (0 : CPolynomial F)) k = rowGet row k := by + rcases Nat.lt_or_ge k row.size with hk | hk + · rw [rowGet, rowGet, pm_append_getD_left _ hk] + · rw [rowGet, rowGet, pm_append_getD_right _ hk, array_getD_of_le' _ _ hk] + rcases Nat.lt_or_ge (k - row.size) m with h | h + · rw [array_getD_of_lt' _ _ (by simpa using h), Array.getElem_replicate] + · rw [array_getD_of_le' _ _ (by simpa using h)] + +/-- Zero padding preserves the X-adic approximant conditions. -/ +private theorem pm_rowApproximates_append_replicate + (mulCtx : CPolynomial.MulContext F) (problem : XAdicProblem F) + {row : PolynomialRow F} (m : Nat) + (h : RowApproximates mulCtx problem row) : + RowApproximates mulCtx problem + (row ++ Array.replicate m (0 : CPolynomial F)) := by + rw [rowApproximates_iff] at h ⊢ + intro j hj hjw + have hsum : ∑ k ∈ Finset.range problem.matrix.size, + (rowGet (row ++ Array.replicate m (0 : CPolynomial F)) k).toPoly * + (rowGet (problem.matrix.getD k #[]) j).toPoly = + ∑ k ∈ Finset.range problem.matrix.size, + (rowGet row k).toPoly * (rowGet (problem.matrix.getD k #[]) j).toPoly := + Finset.sum_congr rfl fun k _hk ↦ by rw [pm_rowGet_append_replicate] + rw [hsum] + exact h j hj hjw + +/-- Coordinates reading zero contribute no shifted entry degree. -/ +private theorem pm_shiftedEntryDegree_eq_none_of_rowGet_eq_zero + {row : PolynomialRow F} {shift : Array Nat} {j : Nat} + (h : rowGet row j = 0) : shiftedEntryDegree? row shift j = none := by + simp [shiftedEntryDegree?, h] + +omit [LawfulBEq F] in +/-- Shifted entry degrees only depend on row reads. -/ +private theorem pm_shiftedEntryDegree_congr {row row' : PolynomialRow F} + {shift : Array Nat} {j : Nat} (h : rowGet row' j = rowGet row j) : + shiftedEntryDegree? row' shift j = shiftedEntryDegree? row shift j := by + simp only [shiftedEntryDegree?, h] + +/-- Zero padding preserves the shifted row degree. -/ +private theorem pm_rowShiftedDegree_append_replicate (row : PolynomialRow F) + (shift : Array Nat) (m : Nat) : + rowShiftedDegree? (row ++ Array.replicate m (0 : CPolynomial F)) shift = + rowShiftedDegree? row shift := by + set row' := row ++ Array.replicate m (0 : CPolynomial F) with hrow' + have hget : ∀ j, rowGet row' j = rowGet row j := + pm_rowGet_append_replicate row m + have hentry : ∀ j, + shiftedEntryDegree? row' shift j = shiftedEntryDegree? row shift j := + fun j ↦ pm_shiftedEntryDegree_congr (hget j) + have hzero_iff : RowIsZero row' ↔ RowIsZero row := by + constructor + · intro h + refine pm_rowIsZero_of_rowGet fun j ↦ ?_ + rw [← hget j] + exact pm_rowGet_eq_zero_of_rowIsZero h j + · intro h + refine pm_rowIsZero_of_rowGet fun j ↦ ?_ + rw [hget j] + exact pm_rowGet_eq_zero_of_rowIsZero h j + cases hd : rowShiftedDegree? row shift with + | none => + rw [rowShiftedDegree?_eq_none_iff] at hd ⊢ + exact hzero_iff.mpr hd + | some d => + cases hd' : rowShiftedDegree? row' shift with + | none => + exfalso + rw [rowShiftedDegree?_eq_none_iff] at hd' + have hnone : rowShiftedDegree? row shift = none := + rowShiftedDegree?_eq_none_iff.2 (hzero_iff.mp hd') + rw [hd] at hnone + cases hnone + | some d' => + rcases exists_shiftedEntryDegree?_eq_of_rowShiftedDegree?_eq_some hd + with ⟨j, hj, hej⟩ + rcases exists_shiftedEntryDegree?_eq_of_rowShiftedDegree?_eq_some hd' + with ⟨j', hj', hej'⟩ + have hj'row : j' < row.size := by + by_contra hge + have hzero : rowGet row j' = 0 := by + rw [rowGet, array_getD_of_le' _ _ (by omega)] + rw [hentry j', + pm_shiftedEntryDegree_eq_none_of_rowGet_eq_zero hzero] at hej' + cases hej' + have h1 : d' ≤ d := + shiftedEntryDegree?_le_of_rowShiftedDegree?_eq_some hd hj'row + (by rw [← hentry j']; exact hej') + have hsize' : row'.size = row.size + m := by + rw [hrow'] + simp + have h2 : d ≤ d' := + shiftedEntryDegree?_le_of_rowShiftedDegree?_eq_some (j := j) hd' + (by omega) (by rw [hentry j]; exact hej) + exact congrArg some (by omega) + +/-- High monomial coefficients are nonzero. -/ +private theorem pm_coeffXPower_one_ne_zero (d : Nat) : + coeffXPower (1 : F) d ≠ 0 := by + intro h + have hcoeff : CPolynomial.coeff (coeffXPower (1 : F) d) d = 1 := by + rw [CPolynomial.coeff_toPoly, coeffXPower_toPoly, Polynomial.coeff_C_mul, + Polynomial.coeff_X_pow, if_pos rfl, mul_one] + rw [h, CPolynomial.coeff_zero] at hcoeff + exact zero_ne_one hcoeff + +/-- Degree of a high monomial coefficient. -/ +private theorem pm_coeffXPower_one_natDegree (d : Nat) : + (coeffXPower (1 : F) d).natDegree = d := by + rw [CPolynomial.natDegree_toPoly, coeffXPower_toPoly, Polynomial.C_1, + one_mul, Polynomial.natDegree_X_pow] + +/-- Diagonal shifted entry degree of a monomial unit row. -/ +private theorem pm_shiftedEntryDegree_monomialUnitRow_self {n i : Nat} + (cap : Nat) (shift : Array Nat) (hi : i < n) : + shiftedEntryDegree? (monomialUnitRow (F := F) n i cap) shift i = + some (cap + shift.getD i 0) := by + have hget : rowGet (monomialUnitRow (F := F) n i cap) i = + coeffXPower 1 cap := by + rw [pm_rowGet_monomialUnitRow, if_pos ⟨hi, rfl⟩] + rw [shiftedEntryDegree?_eq_some_of_rowGet_ne_zero (by + rw [hget] + exact pm_coeffXPower_one_ne_zero cap), + hget, pm_coeffXPower_one_natDegree] + +/-- Off-diagonal shifted entry degrees of a monomial unit row are undefined. -/ +private theorem pm_shiftedEntryDegree_monomialUnitRow_ne {n i j : Nat} + (cap : Nat) (shift : Array Nat) (hij : i ≠ j) : + shiftedEntryDegree? (monomialUnitRow (F := F) n i cap) shift j = none := by + refine pm_shiftedEntryDegree_eq_none_of_rowGet_eq_zero ?_ + rw [pm_rowGet_monomialUnitRow, if_neg fun h ↦ hij h.2] + +/-- Shifted row degree of a monomial unit row. -/ +private theorem pm_rowShiftedDegree_monomialUnitRow {n i : Nat} (cap : Nat) + (shift : Array Nat) (hi : i < n) : + rowShiftedDegree? (monomialUnitRow (F := F) n i cap) shift = + some (cap + shift.getD i 0) := by + cases hd : rowShiftedDegree? (monomialUnitRow (F := F) n i cap) shift with + | none => + exfalso + have hz := rowShiftedDegree?_eq_none_iff.1 hd + have hzero := pm_rowGet_eq_zero_of_rowIsZero hz i + rw [pm_rowGet_monomialUnitRow, if_pos ⟨hi, rfl⟩] at hzero + exact pm_coeffXPower_one_ne_zero cap hzero + | some d => + rcases exists_shiftedEntryDegree?_eq_of_rowShiftedDegree?_eq_some hd + with ⟨j, hj, hej⟩ + rcases eq_or_ne i j with rfl | hij + · rw [pm_shiftedEntryDegree_monomialUnitRow_self cap shift hi] at hej + rw [← hej] + · rw [pm_shiftedEntryDegree_monomialUnitRow_ne cap shift hij] at hej + cases hej + +/-- Shifted leading position of a monomial unit row. -/ +private theorem pm_rowShiftedLeadingPosition_monomialUnitRow [DecidableEq F] + {n i : Nat} (cap : Nat) (shift : Array Nat) (hi : i < n) : + rowShiftedLeadingPosition? (monomialUnitRow (F := F) n i cap) shift = + some i := by + have hdeg := pm_rowShiftedDegree_monomialUnitRow (F := F) cap shift hi + rcases rowShiftedLeadingPosition?_some_of_degree hdeg with ⟨pos, hpos⟩ + have hentry := rowShiftedLeadingPosition?_entry_eq hdeg hpos + rcases eq_or_ne i pos with rfl | hne + · exact hpos + · rw [pm_shiftedEntryDegree_monomialUnitRow_ne cap shift hne] at hentry + cases hentry + +omit [LawfulBEq F] in +/-- Stored rows witness their shifted leading position in the row set. -/ +private theorem pm_rowsContainLeadingPosition_of_getD + {rows : PolynomialMatrix F} {shift : Array Nat} {i p : Nat} + (hi : i < rows.size) + (hpos : rowShiftedLeadingPosition? (rows.getD i #[]) shift = some p) : + rowsContainLeadingPosition rows shift p = true := by + rw [rowsContainLeadingPosition, Array.any_eq_true] + refine ⟨i, hi, ?_⟩ + rw [← array_getD_of_lt' rows #[] hi, hpos] + exact beq_self_eq_true p + +omit [LawfulBEq F] in +/-- Pivot tables whose slots store their own leading positions extract to +shifted weak-Popov row sets. -/ +private theorem pm_pivotRows_shiftedWeakPopov + {pivots : Array (Option (PolynomialRow F))} {shift : Array Nat} + (hinv : ∀ p r, pivots.getD p none = some r → + rowShiftedLeadingPosition? r shift = some p) : + ShiftedWeakPopov (pivotRows pivots) shift := by + have hpair : (pivots.toList.filterMap id).Pairwise + (fun a b ↦ rowShiftedLeadingPosition? a shift ≠ + rowShiftedLeadingPosition? b shift) := by + rw [List.pairwise_filterMap, List.pairwise_iff_getElem] + intro u v hu hv huv b hb b' hb' + have hu' : u < pivots.size := by simpa using hu + have hv' : v < pivots.size := by simpa using hv + have hbu : pivots.getD u none = some b := by + rw [array_getD_of_lt' _ _ hu', ← Array.getElem_toList] + exact hb + have hbv : pivots.getD v none = some b' := by + rw [array_getD_of_lt' _ _ hv', ← Array.getElem_toList] + exact hb' + rw [hinv u b hbu, hinv v b' hbv] + intro hcontra + cases hcontra + omega + rw [List.pairwise_iff_getElem] at hpair + have hsz : (pivotRows pivots).size = (pivots.toList.filterMap id).length := by + rw [pivotRows] + simp + have hget : ∀ k, k < (pivotRows pivots).size → + ∀ hk : k < (pivots.toList.filterMap id).length, + (pivotRows pivots).getD k #[] = (pivots.toList.filterMap id)[k] := by + intro k hk hk' + rw [pivotRows, array_getD_of_lt' _ _ (by simpa using hk'), + List.getElem_toArray] + intro i j hi hj hij hpi hpj + rcases Nat.lt_or_ge i j with h | h + · rw [hget i hi (by omega), hget j hj (by omega)] + exact hpair i j (by omega) (by omega) h + · have hji : j < i := by omega + rw [hget i hi (by omega), hget j hj (by omega)] + exact (hpair j i (by omega) (by omega) hji).symm + +/-- Per-row shape facts for the missing-pivot completion rows. -/ +private theorem pm_missingCompletionRows_facts [DecidableEq F] + (problem : XAdicProblem F) (shift : Array Nat) (rows : PolynomialMatrix F) + {k : Nat} (hk : k < (missingCompletionRows problem shift rows).size) : + ∃ i, i < problem.matrix.size ∧ + rowShiftedLeadingPosition? + ((missingCompletionRows problem shift rows).getD k #[]) shift = + some i ∧ + rowsContainLeadingPosition rows shift i = false := by + have hmem : (missingCompletionRows problem shift rows).getD k #[] ∈ + (List.range problem.matrix.size).filterMap (fun i ↦ + if rowsContainLeadingPosition rows shift i then none + else some (monomialUnitRow problem.matrix.size i + (leafDegreeCap problem))) := by + rw [missingCompletionRows] at hk ⊢ + rw [array_getD_of_lt' _ _ hk, List.getElem_toArray] + exact List.getElem_mem _ + rcases List.mem_filterMap.mp hmem with ⟨i, hi, hg⟩ + rw [List.mem_range] at hi + by_cases hc : rowsContainLeadingPosition rows shift i + · rw [if_pos hc] at hg + cases hg + · rw [if_neg hc] at hg + refine ⟨i, hi, ?_, by simpa using hc⟩ + rw [← Option.some.inj hg] + exact pm_rowShiftedLeadingPosition_monomialUnitRow + (leafDegreeCap problem) shift hi + +/-- Distinct missing-pivot completion rows have distinct shifted leading +positions. -/ +private theorem pm_missingCompletionRows_pairwise [DecidableEq F] + (problem : XAdicProblem F) (shift : Array Nat) (rows : PolynomialMatrix F) + {k k' : Nat} (hk : k < (missingCompletionRows problem shift rows).size) + (hk' : k' < (missingCompletionRows problem shift rows).size) + (hkk : k < k') : + rowShiftedLeadingPosition? + ((missingCompletionRows problem shift rows).getD k #[]) shift ≠ + rowShiftedLeadingPosition? + ((missingCompletionRows problem shift rows).getD k' #[]) shift := by + have hmlist : missingCompletionRows problem shift rows = + ((List.range problem.matrix.size).filterMap (fun i ↦ + if rowsContainLeadingPosition rows shift i then none + else some (monomialUnitRow problem.matrix.size i + (leafDegreeCap problem)))).toArray := by + simp only [missingCompletionRows] + have hpair : ((List.range problem.matrix.size).filterMap (fun i ↦ + if rowsContainLeadingPosition rows shift i then none + else some (monomialUnitRow (F := F) problem.matrix.size i + (leafDegreeCap problem)))).Pairwise + (fun a b ↦ rowShiftedLeadingPosition? a shift ≠ + rowShiftedLeadingPosition? b shift) := by + rw [List.pairwise_filterMap, List.pairwise_iff_getElem] + intro u v hu hv huv b hb b' hb' + have hu' : u < problem.matrix.size := by simpa using hu + have hv' : v < problem.matrix.size := by simpa using hv + simp only [List.getElem_range] at hb hb' + have hbpos : rowShiftedLeadingPosition? b shift = some u := by + by_cases hc : rowsContainLeadingPosition rows shift u + · rw [if_pos hc] at hb + cases hb + · rw [if_neg hc] at hb + rw [← Option.some.inj hb] + exact pm_rowShiftedLeadingPosition_monomialUnitRow + (leafDegreeCap problem) shift hu' + have hbpos' : rowShiftedLeadingPosition? b' shift = some v := by + by_cases hc : rowsContainLeadingPosition rows shift v + · rw [if_pos hc] at hb' + cases hb' + · rw [if_neg hc] at hb' + rw [← Option.some.inj hb'] + exact pm_rowShiftedLeadingPosition_monomialUnitRow + (leafDegreeCap problem) shift hv' + rw [hbpos, hbpos'] + intro hcontra + cases hcontra + omega + rw [List.pairwise_iff_getElem] at hpair + rw [hmlist] at hk hk' ⊢ + have hk_len : k < ((List.range problem.matrix.size).filterMap (fun i ↦ + if rowsContainLeadingPosition rows shift i then none + else some (monomialUnitRow (F := F) problem.matrix.size i + (leafDegreeCap problem)))).length := by + simpa using hk + have hk'_len : k' < ((List.range problem.matrix.size).filterMap (fun i ↦ + if rowsContainLeadingPosition rows shift i then none + else some (monomialUnitRow (F := F) problem.matrix.size i + (leafDegreeCap problem)))).length := by + simpa using hk' + rw [array_getD_of_lt' _ _ hk, array_getD_of_lt' _ _ hk', + List.getElem_toArray, List.getElem_toArray] + exact hpair k k' hk_len hk'_len hkk + +/-- Missing-pivot completion preserves the shifted weak-Popov property. -/ +private theorem pm_completeMissingPivotRows_shiftedWeakPopov [DecidableEq F] + (problem : XAdicProblem F) (shift : Array Nat) {rows : PolynomialMatrix F} + (hwp : ShiftedWeakPopov rows shift) : + ShiftedWeakPopov (completeMissingPivotRows problem shift rows) shift := by + intro i j hi hj hij hpi hpj + rw [completeMissingPivotRows, Array.size_append] at hi hj + rw [completeMissingPivotRows] at hpi hpj ⊢ + rcases Nat.lt_or_ge i rows.size with hi' | hi' <;> + rcases Nat.lt_or_ge j rows.size with hj' | hj' + · rw [pm_append_getD_left _ hi'] at hpi + rw [pm_append_getD_left _ hj'] at hpj + rw [pm_append_getD_left _ hi', pm_append_getD_left _ hj'] + exact hwp i j hi' hj' hij hpi hpj + · rw [pm_append_getD_left _ hi'] at hpi ⊢ + rw [pm_append_getD_right _ hj'] at hpj ⊢ + cases hp : rowShiftedLeadingPosition? (rows.getD i #[]) shift with + | none => exact absurd hp hpi + | some p => + rcases pm_missingCompletionRows_facts problem shift rows + (k := j - rows.size) (by omega) with ⟨im, _him, hposm, hcontm⟩ + rw [hposm] + intro heq + cases Option.some.inj heq + have htrue := pm_rowsContainLeadingPosition_of_getD hi' hp + rw [hcontm] at htrue + cases htrue + · rw [pm_append_getD_left _ hj'] at hpj ⊢ + rw [pm_append_getD_right _ hi'] at hpi ⊢ + cases hp : rowShiftedLeadingPosition? (rows.getD j #[]) shift with + | none => exact absurd hp hpj + | some p => + rcases pm_missingCompletionRows_facts problem shift rows + (k := i - rows.size) (by omega) with ⟨im, _him, hposm, hcontm⟩ + rw [hposm] + intro heq + cases Option.some.inj heq + have htrue := pm_rowsContainLeadingPosition_of_getD hj' hp + rw [hcontm] at htrue + cases htrue + · rw [pm_append_getD_right _ hi', pm_append_getD_right _ hj'] + have hki : i - rows.size < (missingCompletionRows problem shift rows).size := + by omega + have hkj : j - rows.size < (missingCompletionRows problem shift rows).size := + by omega + rcases Nat.lt_or_ge (i - rows.size) (j - rows.size) with h | h + · exact pm_missingCompletionRows_pairwise problem shift rows hki hkj h + · have hlt : j - rows.size < i - rows.size := by omega + exact (pm_missingCompletionRows_pairwise problem shift rows hkj hki + hlt).symm + +/-- **Shifted minimality of the recursive PM-basis.** Every nonzero X-adic +solution row is shifted-degree dominated by some row of the root-normalized +recursive PM-basis. -/ +theorem pmBasis_kernelLeaf_complete_minimal [DecidableEq F] + (mulCtx : CPolynomial.MulContext F) + (lowCtx : PolynomialMatrix.MulLowContext F) + (leafCutoff composeLeafCutoff : Nat) + (problem : XAdicProblem F) (shift : Array Nat) (row : PolynomialRow F) + (hpos : 0 < problem.matrix.size) (hwf : WellFormed problem.matrix) + (happrox : RowApproximates mulCtx problem row) + (hnz : rowIsZero row = false) (hwidth : row.size ≤ problem.matrix.size) : + ∃ basisRow degree, + basisRow ∈ MatrixRows (pmBasis + (kernelLeafRuntimeWithLowAndCompose mulCtx lowCtx leafCutoff + composeLeafCutoff) problem shift) ∧ + basisRow.size ≤ problem.matrix.size ∧ + rowShiftedDegree? basisRow shift = some degree ∧ + ∀ rowDegree, rowShiftedDegree? row shift = some rowDegree → + degree ≤ rowDegree := by + set n := problem.matrix.size with hn + set runtime := kernelLeafRuntimeWithLowAndCompose mulCtx lowCtx leafCutoff + composeLeafCutoff with hruntime + -- Pad the solution row to the principal width. + set row' := row ++ Array.replicate (n - row.size) (0 : CPolynomial F) + with hrow' + have hrow'size : row'.size = n := by + rw [hrow'] + simp + omega + have hrownz : ¬ RowIsZero row := by + intro h + rw [← rowIsZero_iff, hnz] at h + cases h + have hrow'nz : ¬ RowIsZero row' := by + intro h + refine hrownz fun p hp ↦ h p ?_ + rw [hrow', Array.toList_append] + exact List.mem_append.mpr (Or.inl hp) + have hrow'approx : RowApproximates mulCtx problem row' := + pm_rowApproximates_append_replicate mulCtx problem (n - row.size) happrox + have hrow'deg : rowShiftedDegree? row' shift = rowShiftedDegree? row shift := + pm_rowShiftedDegree_append_replicate row shift (n - row.size) + -- The padded row lies in the span of the recursive core. + set core := pmBasisWithFuelCore runtime (pmBasisFuel problem) problem shift + with hcore + have hcoresizes : ∀ r ∈ MatrixRows core, r.size = n := + fun r hr ↦ (pmBasisWithFuelCore_kernelLeaf_rows mulCtx lowCtx leafCutoff + composeLeafCutoff (pmBasisFuel problem) problem shift r hr).2 + have hrow'span : row' ∈ RowSpan core := + pmBasisWithFuelCore_kernelLeaf_rowSpan_complete mulCtx lowCtx leafCutoff + composeLeafCutoff (pmBasisFuel problem) problem shift hpos hwf row' + hrow'approx hrow'size hrow'nz + -- Lift the span through the root pivot reduction. + set red := reduceKernelLeafRowsByPivots core shift with hred + have hred_rows : ∀ r ∈ MatrixRows red, r.size = n ∧ rowIsZero r = false := + reduceKernelLeafRowsByPivots_rows hcoresizes + have hrow'red : row' ∈ RowSpan red := + pm_mem_rowSpan_of_nonzero_rows_mem hpos hcoresizes + (fun r hr ↦ (hred_rows r hr).1) + (fun r hr hrnz ↦ + reduceKernelLeafRowsByPivots_rowSpan_superset hcoresizes hr hrnz) + hrow'span hrow'nz + have hcmp_eq : compactNonzeroRows red = red := by + rw [compactNonzeroRows, Array.filter_eq_self] + intro r hr + rw [(hred_rows r (by rw [MatrixRows, ← Array.mem_def]; exact hr)).2] + rfl + -- The final basis is the completed reduction. + have hbasis_eq : pmBasis runtime problem shift = + completeMissingPivotRows problem shift red := by + rw [pmBasis, pmBasisWithFuel, pmBasisNormalizeRoot, reduceKernelLeafRows, + ← hcore, ← hred, hcmp_eq] + have hfinsizes : ∀ r ∈ MatrixRows (completeMissingPivotRows problem shift + red), r.size = n := by + intro r hr + rw [completeMissingPivotRows, MatrixRows, Array.toList_append] at hr + rcases List.mem_append.mp hr with hr | hr + · exact (hred_rows r hr).1 + · rw [missingCompletionRows, List.toList_toArray] at hr + rcases List.mem_filterMap.mp hr with ⟨i, _hi, hg⟩ + by_cases hc : rowsContainLeadingPosition red shift i + · rw [if_pos hc] at hg + cases hg + · rw [if_neg hc] at hg + rw [← Option.some.inj hg, hn] + simp [monomialUnitRow] + have hrow'fin : row' ∈ RowSpan (completeMissingPivotRows problem shift + red) := by + refine pm_mem_rowSpan_of_nonzero_rows_mem hpos + (fun r hr ↦ (hred_rows r hr).1) hfinsizes ?_ hrow'red hrow'nz + intro r hr _hrnz + refine matrix_row_mem_rowSpan (wellFormed_of_sizes hfinsizes) ?_ + rw [completeMissingPivotRows, MatrixRows, Array.toList_append] + exact List.mem_append.mpr (Or.inl hr) + -- The completed reduction is shifted weak-Popov. + have hwp_red : ShiftedWeakPopov red shift := by + have hred_eq : red = pivotRows (core.toList.foldl + (fun pivots row ↦ insertKernelLeafPivotRowWithFuel + (reduceKernelLeafFuel core shift) pivots shift row) + (Array.replicate (MatrixWidth core) none)) := by + rw [hred, reduceKernelLeafRowsByPivots, ← Array.foldl_toList] + rw [hred_eq] + refine pm_pivotRows_shiftedWeakPopov fun p r hget ↦ ?_ + have hinit : ∀ p r, + (Array.replicate (MatrixWidth core) + (none : Option (PolynomialRow F))).getD p none = some r → + r.size = n ∧ rowShiftedLeadingPosition? r shift = some p := by + intro p r hget + rcases Nat.lt_or_ge p + (Array.replicate (MatrixWidth core) + (none : Option (PolynomialRow F))).size with hp | hp + · rw [array_getD_of_lt' _ _ hp, Array.getElem_replicate] at hget + cases hget + · rw [array_getD_of_le' _ _ hp] at hget + cases hget + exact (insertKernelLeaf_foldl_pivotInv (n := n) core.toList + (reduceKernelLeafFuel core shift) + (Array.replicate (MatrixWidth core) none) shift + (fun r hr ↦ hcoresizes r hr) hinit p r hget).2 + have hwp_fin : ShiftedWeakPopov + (completeMissingPivotRows problem shift red) shift := + pm_completeMissingPivotRows_shiftedWeakPopov problem shift hwp_red + -- Apply the predictable-degree property of weak-Popov matrices. + have hdeg : rowShiftedDegree? row' shift ≠ none := by + rw [hrow'deg] + intro h + exact hrownz (rowShiftedDegree?_eq_none_iff.1 h) + obtain ⟨outRow, outDeg, rowDeg, houtmem, houtdeg, hrowdeg, hle⟩ := + shiftedWeakPopov_least_row_minimal + (completeMissingPivotRows problem shift red) shift row' + (wellFormed_of_sizes hfinsizes) hwp_fin hrow'fin hdeg + refine ⟨outRow, outDeg, by rw [hbasis_eq]; exact houtmem, + le_of_eq (hfinsizes outRow houtmem), houtdeg, ?_⟩ + intro rowDegree hrowDegree + rw [hrow'deg, hrowDegree] at hrowdeg + cases Option.some.inj hrowdeg + exact hle + +end Approximant + +end PolynomialMatrix + +end CompPoly diff --git a/CompPoly/LinearAlgebra/PolynomialMatrix/Approximant/PMBasis/KernelLeaf.lean b/CompPoly/LinearAlgebra/PolynomialMatrix/Approximant/PMBasis/KernelLeaf.lean new file mode 100644 index 00000000..a2388dd1 --- /dev/null +++ b/CompPoly/LinearAlgebra/PolynomialMatrix/Approximant/PMBasis/KernelLeaf.lean @@ -0,0 +1,414 @@ +/- +Copyright (c) 2026 CompPoly Contributors. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Valerii Huhnin +-/ + +module + +public import CompPoly.LinearAlgebra.Dense +public import CompPoly.LinearAlgebra.PolynomialMatrix.Approximant.Basic +public import CompPoly.LinearAlgebra.PolynomialMatrix.MuldersStorjohannCorrectness.RowOps + +/-! +# Scalar Kernel-Leaf PM-Basis Definitions + +Executable definitions for the classical scalar-kernel PM-basis leaf: the +dense coefficient matrix and its row-array RREF kernel, reconstruction of +polynomial rows from kernel vectors, monomial completion rows, and the +shifted pivot-table reduction used to compact leaf bases. +-/ + +@[expose] public section + +namespace CompPoly + +namespace PolynomialMatrix + +namespace Approximant + +variable {F : Type*} [Field F] [BEq F] [LawfulBEq F] + +/-- Coefficient degree cap used by the classical scalar-kernel PM-basis leaf. -/ +def leafDegreeCap (problem : XAdicProblem F) : Nat := + max 1 (maxOrder problem) + +/-- Coefficient-equation indices `(column, coefficientDegree)`. -/ +def coefficientEquationIndices (orders : Array Nat) : Array (Nat × Nat) := Id.run do + let mut out := #[] + for j in [0:orders.size] do + for t in [0:orders.getD j 0] do + out := out.push (j, t) + pure out + +/-- Dense scalar coefficient matrix for the bounded leaf problem. -/ +def coefficientMatrix (problem : XAdicProblem F) : DenseMatrix F := + let degreeCap := leafDegreeCap problem + let equations := coefficientEquationIndices problem.orders + DenseMatrix.ofFn equations.size (problem.matrix.size * degreeCap) fun row col ↦ + let equation := equations.getD row (0, 0) + let matrixCol := equation.1 + let coeffDegree := equation.2 + let coord := col / degreeCap + let coordDegree := col % degreeCap + if coordDegree ≤ coeffDegree then + CPolynomial.coeff + (PolynomialMatrix.rowGet (problem.matrix.getD coord #[]) matrixCol) + (coeffDegree - coordDegree) + else + 0 + +/-- One scalar coefficient row for the bounded leaf problem. -/ +def coefficientMatrixRow (problem : XAdicProblem F) (degreeCap : Nat) + (equation : Nat × Nat) : Array F := + (List.range (problem.matrix.size * degreeCap)).map + (fun col ↦ + let matrixCol := equation.1 + let coeffDegree := equation.2 + let coord := col / degreeCap + let coordDegree := col % degreeCap + if coordDegree ≤ coeffDegree then + CPolynomial.coeff + (PolynomialMatrix.rowGet (problem.matrix.getD coord #[]) matrixCol) + (coeffDegree - coordDegree) + else + 0) |>.toArray + +/-- Scalar coefficient rows for the bounded leaf problem. This is the same +matrix as `coefficientMatrix`, represented directly as row arrays for the tiny +leaf RREF routine. -/ +def coefficientMatrixRows (problem : XAdicProblem F) : Array (Array F) := + let degreeCap := leafDegreeCap problem + (coefficientEquationIndices problem.orders).map + (coefficientMatrixRow problem degreeCap) + +/-- Swap two scalar rows in a row-array matrix. -/ +def swapScalarRows (rows : Array (Array F)) (rowA rowB : Nat) : + Array (Array F) := + let a := rows.getD rowA #[] + let b := rows.getD rowB #[] + (rows.setIfInBounds rowA b).setIfInBounds rowB a + +/-- Find a nonzero pivot row at or below `startRow` in column `col`. -/ +def findScalarPivotRow (rows : Array (Array F)) (startRow col : Nat) : + Option Nat := + (List.range' startRow (rows.size - startRow)).find? fun row ↦ + (rows.getD row #[]).getD col 0 != 0 + +/-- Scale a scalar row so that column `pivotCol` becomes one. -/ +def normalizeScalarRow (row : Array F) (pivotCol : Nat) : Array F := + let pivot := row.getD pivotCol 0 + if pivot == 0 then + row + else + row.map fun x ↦ x / pivot + +/-- Add `factor * source` to `target`, using zero defaults for ragged rows. -/ +def addScaledScalarRow (target source : Array F) (factor : F) : Array F := + (List.range (max target.size source.size)).map + (fun col ↦ target.getD col 0 + factor * source.getD col 0) |>.toArray + +/-- Normalize one pivot row and clear the pivot column in all other rows. -/ +def normalizeAndEliminateScalarRows (rows : Array (Array F)) + (pivotRow pivotCol : Nat) : Array (Array F) := + let pivot := (rows.getD pivotRow #[]).getD pivotCol 0 + if pivot == 0 then + rows + else + let pivotVector := normalizeScalarRow (rows.getD pivotRow #[]) pivotCol + let rows := rows.setIfInBounds pivotRow pivotVector + (List.range rows.size).foldl + (fun rows row ↦ + if row == pivotRow then + rows + else + let factor := -((rows.getD row #[]).getD pivotCol 0) + if factor == 0 then + rows + else + rows.setIfInBounds row + (addScaledScalarRow (rows.getD row #[]) pivotVector factor)) + rows + +/-- RREF result for a scalar row-array matrix. -/ +structure ScalarRrefResult where + rows : Array (Array F) + pivots : Array Nat + +/-- Fuel-bounded row-array RREF for tiny scalar coefficient matrices. -/ +def scalarRrefRowsLoop (cols : Nat) : + Nat → Nat → Nat → Array (Array F) → Array Nat → ScalarRrefResult (F := F) + | 0, _col, _row, rows, pivots => { rows := rows, pivots := pivots } + | fuel + 1, col, row, rows, pivots => + if col >= cols || row >= rows.size then + { rows := rows, pivots := pivots } + else + match findScalarPivotRow rows row col with + | none => scalarRrefRowsLoop cols fuel (col + 1) row rows pivots + | some pivotRow => + let swapped := swapScalarRows rows pivotRow row + let reduced := normalizeAndEliminateScalarRows swapped row col + scalarRrefRowsLoop cols fuel (col + 1) (row + 1) reduced + (pivots.push col) + +/-- Row-array RREF for tiny scalar coefficient matrices. -/ +def scalarRrefRows (rows : Array (Array F)) (cols : Nat) : + ScalarRrefResult (F := F) := + scalarRrefRowsLoop cols (cols + 1) 0 0 rows #[] + +/-- Kernel basis vector for one free column of a row-array RREF matrix. -/ +def basisVectorForFreeColumnRows (rows : Array (Array F)) + (pivots : Array Nat) (cols free : Nat) : Array F := + Array.ofFn fun i : Fin cols ↦ + if i.val == free then + 1 + else + match DenseMatrix.pivotRowOfColumn? pivots i.val with + | none => 0 + | some row => -((rows.getD row #[]).getD free 0) + +/-- Homogeneous scalar-kernel basis for a row-array matrix. -/ +def homogeneousKernelBasisRows (rows : Array (Array F)) (cols : Nat) : + Array (Array F) := + let R := scalarRrefRows rows cols + (DenseMatrix.freeColumns cols R.pivots).map + (basisVectorForFreeColumnRows R.rows R.pivots cols) + +/-- Convert one scalar kernel vector back into a polynomial row. -/ +def vectorToPolynomialRow (degreeCap solutionWidth : Nat) (v : Array F) : + PolynomialRow F := + (List.range solutionWidth).map + (fun coord ↦ + CPolynomial.ofArray + ((List.range degreeCap).map + (fun degree ↦ v.getD (coord * degreeCap + degree) 0) |>.toArray)) |>.toArray + +/-- Polynomial `c * X^d`, built without the `CPolynomial.monomial` +`DecidableEq` assumption. -/ +def coeffXPower (c : F) (d : Nat) : CPolynomial F := + CPolynomial.ofArray ((Array.replicate d 0).push c) + +/-- Multiply a row by `c * X^d`, using the coefficient-array monomial builder. -/ +def polynomialScaleCoeffX (c : F) (d : Nat) (p : CPolynomial F) : + CPolynomial F := + if c == 0 then + 0 + else if p == 0 then + 0 + else + CPolynomial.ofArray + ((List.replicate d 0 ++ p.val.toList.map (fun a ↦ c * a)).toArray) + +/-- Multiply a row by `c * X^d`, using coefficient shifting instead of generic +polynomial multiplication by a monomial. -/ +def rowScaleCoeffX (c : F) (d : Nat) (row : PolynomialRow F) : + PolynomialRow F := + row.map fun p ↦ polynomialScaleCoeffX c d p + +/-- Monomial row `X^d * e_i`, used to complete bounded-kernel leaves to a full +approximant basis. -/ +def monomialUnitRow (width i d : Nat) : PolynomialRow F := + (List.range width).map + (fun j ↦ if i == j then coeffXPower 1 d else 0) |>.toArray + +/-- Trivial high-degree approximants present in every X-adic problem. These +rows are essential when the bounded scalar kernel has fewer rows than the module +rank. -/ +def kernelLeafCompletionRows (problem : XAdicProblem F) : + PolynomialMatrix F := + let degreeCap := leafDegreeCap problem + (List.range problem.matrix.size).map + (fun i ↦ monomialUnitRow problem.matrix.size i degreeCap) |>.toArray + +/-- Whether a row set already contains a row with a given shifted leading +position. -/ +def rowsContainLeadingPosition (rows : PolynomialMatrix F) + (shift : Array Nat) (position : Nat) : Bool := + rows.any fun row ↦ + match rowShiftedLeadingPosition? row shift with + | some p => p == position + | none => false + +/-- High monomial rows for shifted leading positions not represented by `rows`. +These rows are always valid approximants and keep recursive residual problems +from losing coordinates after compact row reduction. -/ +def missingCompletionRows (problem : XAdicProblem F) + (shift : Array Nat) (rows : PolynomialMatrix F) : + PolynomialMatrix F := + let degreeCap := leafDegreeCap problem + (List.range problem.matrix.size).filterMap + (fun i ↦ + if rowsContainLeadingPosition rows shift i then + none + else + some (monomialUnitRow problem.matrix.size i degreeCap)) |>.toArray + +/-- Add high monomial approximants for missing pivot positions. -/ +def completeMissingPivotRows (problem : XAdicProblem F) + (shift : Array Nat) (rows : PolynomialMatrix F) : + PolynomialMatrix F := + rows ++ missingCompletionRows problem shift rows + +/-- Cancel the shifted leading term of `target` by `reducer`, when their shifted +leading positions agree. This is the small-leaf analogue of polynomial-matrix +row reduction; it is used only after the bounded scalar kernel has already been +computed. -/ +def cancelKernelLeafLeadingTerm + (target reducer : PolynomialRow F) (shift : Array Nat) : PolynomialRow F := + match rowShiftedLeadingTerm? target shift, rowShiftedLeadingTerm? reducer shift with + | some t, some r => + if t.position == r.position then + if r.coeff == 0 then + target + else + rowSub target (rowScaleCoeffX (t.coeff / r.coeff) (t.degree - r.degree) reducer) + else + target + | _, _ => target + +/-- One inner-loop update for finding a leading-position conflict in a bounded +kernel leaf. -/ +def kernelLeafConflictInRowStep? (rows : PolynomialMatrix F) (shift : Array Nat) + (i : Nat) (found : Option (Nat × Nat)) (j : Nat) : Option (Nat × Nat) := + match found with + | some _ => found + | none => + match rowShiftedLeadingPosition? (rows.getD i #[]) shift, + rowShiftedLeadingPosition? (rows.getD j #[]) shift with + | some pi, some pj => if pi == pj then some (i, j) else none + | _, _ => none + +/-- Scan one row for a shifted-leading-position conflict in a bounded kernel +leaf. -/ +def kernelLeafConflictInRow? (rows : PolynomialMatrix F) (shift : Array Nat) + (i : Nat) (found : Option (Nat × Nat)) : Option (Nat × Nat) := + (List.range' (i + 1) (rows.size - (i + 1))).foldl + (kernelLeafConflictInRowStep? rows shift i) found + +/-- Scan all row pairs for the first shifted-leading-position conflict. -/ +def kernelLeafConflictFrom? (rows : PolynomialMatrix F) (shift : Array Nat) + (found : Option (Nat × Nat)) : Option (Nat × Nat) := + (List.range rows.size).foldl + (fun acc i ↦ kernelLeafConflictInRow? rows shift i acc) found + +/-- First pair of nonzero bounded-kernel rows with the same shifted leading +position. -/ +def kernelLeafConflict? (rows : PolynomialMatrix F) (shift : Array Nat) : + Option (Nat × Nat) := + kernelLeafConflictFrom? rows shift none + +/-- One shifted-reduction step for bounded scalar-kernel rows. -/ +def reduceKernelLeafStep (rows : PolynomialMatrix F) (shift : Array Nat) + (i j : Nat) : PolynomialMatrix F := + let rowI := rows.getD i #[] + let rowJ := rows.getD j #[] + match rowShiftedDegree? rowI shift, rowShiftedDegree? rowJ shift with + | some degI, some degJ => + if degI ≤ degJ then + replaceRow rows j (cancelKernelLeafLeadingTerm rowJ rowI shift) + else + replaceRow rows i (cancelKernelLeafLeadingTerm rowI rowJ shift) + | _, _ => rows + +/-- Fuel for bounded-kernel shifted reduction. The scalar leaf is already a +small base case, so this conservative degree-width bound is acceptable here. -/ +def reduceKernelLeafFuel (rows : PolynomialMatrix F) (shift : Array Nat) : Nat := + let maxDegree := (List.range rows.size).foldl + (fun acc i ↦ + match rowShiftedDegree? (rows.getD i #[]) shift with + | none => acc + | some degree => max acc degree) + 0 + (rows.size + 1) * (MatrixWidth rows + 1) * (maxDegree + 1) + +/-- Extract the nonempty pivot rows from a leading-position table. -/ +def pivotRows (pivots : Array (Option (PolynomialRow F))) : + PolynomialMatrix F := + pivots.toList.filterMap id |>.toArray + +/-- Insert one row into a shifted weak-Popov pivot table. Conflicts are resolved +only at the current leading position, avoiding the repeated global pair scans +used by the simple reference reducer. -/ +def insertKernelLeafPivotRowWithFuel : + Nat → Array (Option (PolynomialRow F)) → Array Nat → PolynomialRow F → + Array (Option (PolynomialRow F)) + | 0, pivots, _shift, _row => pivots + | fuel + 1, pivots, shift, row => + match rowShiftedLeadingTerm? row shift with + | none => pivots + | some target => + match pivots.getD target.position none with + | none => pivots.setIfInBounds target.position (some row) + | some pivot => + match rowShiftedLeadingTerm? pivot shift with + | none => pivots.setIfInBounds target.position (some row) + | some reducer => + if target.shiftedDegree < reducer.shiftedDegree then + let reducedPivot := cancelKernelLeafLeadingTerm pivot row shift + let pivots := pivots.setIfInBounds target.position (some row) + insertKernelLeafPivotRowWithFuel fuel pivots shift reducedPivot + else + let reducedRow := cancelKernelLeafLeadingTerm row pivot shift + insertKernelLeafPivotRowWithFuel fuel pivots shift reducedRow + +/-- Pivot-table shifted reduction for bounded scalar-kernel rows. -/ +def reduceKernelLeafRowsByPivots (rows : PolynomialMatrix F) (shift : Array Nat) : + PolynomialMatrix F := + let fuel := reduceKernelLeafFuel rows shift + let pivots := rows.foldl + (fun pivots row ↦ insertKernelLeafPivotRowWithFuel fuel pivots shift row) + (Array.replicate (MatrixWidth rows) none) + pivotRows pivots + +/-- Shift-reduce the bounded scalar-kernel rows before compacting them. This +keeps one low representative per shifted leading position instead of selecting +arbitrary low-degree kernel vectors. -/ +def reduceKernelLeafWithFuel : + Nat → PolynomialMatrix F → Array Nat → PolynomialMatrix F + | 0, rows, _shift => rows + | fuel + 1, rows, shift => + match kernelLeafConflict? rows shift with + | none => rows + | some (i, j) => + reduceKernelLeafWithFuel fuel (reduceKernelLeafStep rows shift i j) shift + +/-- Shift-reduced bounded scalar-kernel rows for the PM-basis leaf. -/ +def reduceKernelLeafRows (rows : PolynomialMatrix F) (shift : Array Nat) : + PolynomialMatrix F := + reduceKernelLeafRowsByPivots rows shift + +/-- Insert one bounded-kernel row into a small shifted-reduced leaf basis. This +keeps the live reduction matrix near the module width instead of reducing the +entire scalar kernel at once. -/ +def insertKernelLeafRowIncremental (basis : PolynomialMatrix F) + (shift : Array Nat) (row : PolynomialRow F) : PolynomialMatrix F := + (reduceKernelLeafRows (basis.push row) shift).filter fun row ↦ !rowIsZero row + +/-- Shift-reduce all bounded scalar-kernel rows incrementally. The dense scalar +kernel can have many rows, but after every insertion the weak-Popov conflict loop +works on the current reduced basis plus one candidate row. -/ +def reduceKernelLeafRowsIncremental (rows : PolynomialMatrix F) + (shift : Array Nat) : PolynomialMatrix F := + rows.foldl (fun basis row ↦ insertKernelLeafRowIncremental basis shift row) #[] + +/-- Classical scalar-kernel leaf for small X-adic approximant problems. -/ +def kernelLeafBasis (problem : XAdicProblem F) (shift : Array Nat) : + PolynomialMatrix F := + let degreeCap := leafDegreeCap problem + let scalarRows := (homogeneousKernelBasisRows (coefficientMatrixRows problem) + (problem.matrix.size * degreeCap)).map + (vectorToPolynomialRow degreeCap problem.matrix.size) + completeMissingPivotRows problem shift + (reduceKernelLeafRowsIncremental + (scalarRows ++ kernelLeafCompletionRows problem) shift) + +/-- Remove zero rows before a recursively computed approximant basis is used as +the coordinate system for the next residual problem. -/ +def compactNonzeroRows (rows : PolynomialMatrix F) : PolynomialMatrix F := + rows.filter fun row ↦ !rowIsZero row + +end Approximant + +end PolynomialMatrix + +end CompPoly diff --git a/CompPoly/LinearAlgebra/PolynomialMatrix/Approximant/PMBasis/KernelLeafCompleteness.lean b/CompPoly/LinearAlgebra/PolynomialMatrix/Approximant/PMBasis/KernelLeafCompleteness.lean new file mode 100644 index 00000000..0332fff1 --- /dev/null +++ b/CompPoly/LinearAlgebra/PolynomialMatrix/Approximant/PMBasis/KernelLeafCompleteness.lean @@ -0,0 +1,605 @@ +/- +Copyright (c) 2026 CompPoly Contributors. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Valerii Huhnin +-/ + +module + +-- `coeff`, `ofArray` and friends are declared in bare `public section`s, so their +-- bodies are opaque downstream; see `docs/wiki/module-system.md`. +import all CompPoly.Univariate.Basic +import all CompPoly.Univariate.Raw.Core +public import CompPoly.LinearAlgebra.PolynomialMatrix.Approximant.PMBasis.KernelLeafSoundness +public import CompPoly.LinearAlgebra.PolynomialMatrix.Approximant.PMBasis.KernelLeafSpan + +/-! +# Kernel-Leaf Basis Generation Completeness + +Every nonzero solution row of an X-adic problem lies in the row module +generated by the kernel-leaf basis: the high part is generated by the +monomial completion rows and the bounded remainder by the scalar kernel. +-/ + +@[expose] public section + +namespace CompPoly + +namespace PolynomialMatrix + +namespace Approximant + +variable {F : Type*} [Field F] [BEq F] [LawfulBEq F] + +/-! ## Kernel-leaf basis generation completeness + +Every solution row of an X-adic problem splits into a low part, whose packed +coefficient vector lies in the bounded scalar kernel, plus a high part in which +every entry is divisible by `X^(leafDegreeCap problem)`. The low part is a +constant-coefficient combination of the reconstructed scalar-kernel rows by +`homogeneousKernelBasisRows_complete`, the high part is a polynomial +combination of the monomial completion rows, and the reduction and completion +steps only enlarge the generated row module. Together these facts show that +the kernel-leaf basis generates every solution row. -/ + +section GenerationCompleteness + +variable {α β : Type*} + +omit [Field F] [BEq F] [LawfulBEq F] in +/-- `getD` of an array append, left side. -/ +theorem pm_append_getD_left {A B : Array α} (d : α) {i : Nat} + (hi : i < A.size) : (A ++ B).getD i d = A.getD i d := by + rw [Array.getD_eq_getD_getElem?, Array.getD_eq_getD_getElem?, + Array.getElem?_append_left hi] + +omit [Field F] [BEq F] [LawfulBEq F] in +/-- `getD` of an array append, right side. -/ +theorem pm_append_getD_right {A B : Array α} (d : α) {i : Nat} + (hi : A.size ≤ i) : (A ++ B).getD i d = B.getD (i - A.size) d := by + rw [Array.getD_eq_getD_getElem?, Array.getD_eq_getD_getElem?, + Array.getElem?_append_right hi] + +omit [Field F] [BEq F] [LawfulBEq F] in +private theorem pm_map_getD (f : α → β) (xs : Array α) (d : α) (e : β) + {i : Nat} (hi : i < xs.size) : + (xs.map f).getD i e = f (xs.getD i d) := by + rw [array_getD_of_lt' _ _ (by simpa using hi), Array.getElem_map, + array_getD_of_lt' _ _ hi] + +end GenerationCompleteness + +/-- Constant polynomial factors act coefficientwise. -/ +private theorem pm_coeff_C_mul (c : F) (p : CPolynomial F) (t : Nat) : + CPolynomial.coeff (CPolynomial.C c * p) t = c * CPolynomial.coeff p t := by + rw [CPolynomial.coeff_toPoly, CPolynomial.toPoly_mul, CPolynomial.C_toPoly, + Polynomial.coeff_C_mul, ← CPolynomial.coeff_toPoly] + +/-- Coefficients of finite polynomial sums are coefficient sums. -/ +private theorem pm_coeff_finset_sum (f : Nat → CPolynomial F) (m t : Nat) : + CPolynomial.coeff (∑ i ∈ Finset.range m, f i) t = + ∑ i ∈ Finset.range m, CPolynomial.coeff (f i) t := by + rw [CPolynomial.coeff_toPoly, pm_toPoly_finset_sum, Polynomial.finsetSum_coeff] + exact Finset.sum_congr rfl fun i _hi ↦ (CPolynomial.coeff_toPoly (f i) t).symm + +/-- Coefficients of a product with the monomial `X^d`. -/ +private theorem pm_coeff_mul_coeffXPower (q : CPolynomial F) (d t : Nat) : + CPolynomial.coeff (q * coeffXPower 1 d) t = + if d ≤ t then CPolynomial.coeff q (t - d) else 0 := by + rw [CPolynomial.coeff_toPoly, CPolynomial.toPoly_mul, coeffXPower_toPoly, + Polynomial.C_1, one_mul, Polynomial.coeff_mul_X_pow', + ← CPolynomial.coeff_toPoly] + +/-- A polynomial with vanishing low coefficients truncates to zero. -/ +private theorem pm_truncateX_eq_zero_of_coeff (order : Nat) {p : CPolynomial F} + (h : ∀ t, t < order → CPolynomial.coeff p t = 0) : + truncateX order p = 0 := by + apply CPolynomial.eq_iff_coeff.2 + intro t + rw [truncateX_coeff, CPolynomial.coeff_zero] + rcases Nat.lt_or_ge t order with ht | ht + · rw [if_pos ht] + exact h t ht + · rw [if_neg (by omega)] + +/-- A polynomial with vanishing low coefficients factors through `X^cap` with +quotient `divXTrunc cap`. -/ +private theorem pm_high_factor {cap : Nat} {p : CPolynomial F} + (hlow : ∀ t, t < cap → CPolynomial.coeff p t = 0) : + divXTrunc cap p.val.size p * coeffXPower 1 cap = p := by + apply CPolynomial.eq_iff_coeff.2 + intro t + rw [pm_coeff_mul_coeffXPower] + rcases Nat.lt_or_ge t cap with ht | ht + · rw [if_neg (by omega), hlow t ht] + · rw [if_pos ht, divXTrunc_coeff] + rcases Nat.lt_or_ge (t - cap) p.val.size with hs | hs + · rw [if_pos hs, Nat.sub_add_cancel ht] + · rw [if_neg (by omega), CPolynomial.coeff_eq_zero_of_size_le p (by omega)] + +/-- Entries of a monomial unit row. -/ +theorem pm_rowGet_monomialUnitRow (n i d k : Nat) : + rowGet (monomialUnitRow (F := F) n i d) k = + if k < n ∧ i = k then coeffXPower 1 d else 0 := by + rcases Nat.lt_or_ge k n with hk | hk + · rw [monomialUnitRow, rowGet, Array.getD_eq_getD_getElem?, + List.getElem?_toArray, List.getElem?_map, List.getElem?_range hk, + Option.map_some, Option.getD_some] + by_cases hik : i = k + · rw [if_pos (beq_iff_eq.mpr hik), if_pos ⟨hk, hik⟩] + · rw [if_neg (by simpa using hik), if_neg fun h ↦ hik h.2] + · rw [monomialUnitRow, rowGet, Array.getD_eq_getD_getElem?, + List.getElem?_toArray, List.getElem?_eq_none (by simpa using hk), + Option.getD_none, if_neg fun h ↦ absurd h.1 (Nat.not_lt.mpr hk)] + +/-- The completion matrix has one row per module coordinate. -/ +private theorem pm_completionRows_size (problem : XAdicProblem F) : + (kernelLeafCompletionRows problem).size = problem.matrix.size := by + simp [kernelLeafCompletionRows] + +/-- In-range rows of the completion matrix are monomial unit rows. -/ +private theorem pm_completionRows_getD (problem : XAdicProblem F) {i : Nat} + (hi : i < problem.matrix.size) : + (kernelLeafCompletionRows problem).getD i #[] = + monomialUnitRow problem.matrix.size i (leafDegreeCap problem) := by + simp only [kernelLeafCompletionRows] + rw [Array.getD_eq_getD_getElem?, List.getElem?_toArray, List.getElem?_map, + List.getElem?_range hi, Option.map_some, Option.getD_some] + +/-- Entries of a row linear combination are coefficient-weighted entry sums. -/ +theorem pm_rowGet_rowLinearCombination (coeffs : Array (CPolynomial F)) + (M : PolynomialMatrix F) (j : Nat) : + rowGet (rowLinearCombination coeffs M) j = + ∑ i ∈ Finset.range M.size, coeffs.getD i 0 * rowGet (M.getD i #[]) j := by + rw [rowLinearCombination] + have hfold : ∀ m : Nat, + rowGet ((List.range m).foldl + (fun acc i ↦ rowAdd acc + (rowScalePolynomial (coeffs.getD i 0) (M.getD i #[]))) + (zeroRow (MatrixWidth M))) j = + ∑ i ∈ Finset.range m, coeffs.getD i 0 * rowGet (M.getD i #[]) j := by + intro m + induction m with + | zero => simp [rowGet_zeroRow] + | succ m ih => + rw [List.range_succ, List.foldl_append, List.foldl_cons, List.foldl_nil, + rowGet_rowAdd, ih, rowGet_rowScalePolynomial, Finset.sum_range_succ] + exact hfold M.size + +/-- Row linear combinations of uniform-width matrices keep that width. -/ +theorem pm_rowLinearCombination_size {M : PolynomialMatrix F} {n : Nat} + (hsizes : ∀ r ∈ MatrixRows M, r.size = n) (hw : MatrixWidth M = n) + (coeffs : Array (CPolynomial F)) : + (rowLinearCombination coeffs M).size = n := by + rw [rowLinearCombination] + have hfold : ∀ m : Nat, m ≤ M.size → + ((List.range m).foldl + (fun acc i ↦ rowAdd acc + (rowScalePolynomial (coeffs.getD i 0) (M.getD i #[]))) + (zeroRow (MatrixWidth M))).size = n := by + intro m + induction m with + | zero => + intro _ + simp [zeroRow, hw] + | succ m ih => + intro hm + rw [List.range_succ, List.foldl_append, List.foldl_cons, List.foldl_nil, + rowAdd_size, ih (by omega), rowScalePolynomial_size, + hsizes _ (getD_mem_matrixRows (by omega)), Nat.max_self] + exact hfold M.size (Nat.le_refl _) + +omit [BEq F] [LawfulBEq F] in +/-- Rows agreeing in size and on every `rowGet` entry are equal. -/ +theorem pm_row_ext {a b : PolynomialRow F} (hsize : a.size = b.size) + (h : ∀ j, rowGet a j = rowGet b j) : a = b := by + refine Array.ext hsize fun j hj hj' ↦ ?_ + have hget := h j + rw [rowGet, rowGet, array_getD_of_lt' _ _ hj, array_getD_of_lt' _ _ hj'] + at hget + exact hget + +/-- Rows whose entries are all divisible by `X^(leafDegreeCap problem)` satisfy +every X-adic condition. -/ +private theorem pm_rowApproximates_of_entry_dvd (mulCtx : CPolynomial.MulContext F) + (problem : XAdicProblem F) {row : PolynomialRow F} + (h : ∀ k, (Polynomial.X : Polynomial F) ^ leafDegreeCap problem ∣ + (rowGet row k).toPoly) : + RowApproximates mulCtx problem row := by + rw [rowApproximates_iff] + intro j hj _hjw + refine Finset.dvd_sum fun k _hk ↦ ?_ + refine Dvd.dvd.mul_right (dvd_trans (pow_dvd_pow _ ?_) (h k)) _ + exact le_trans (getD_le_maxOrder problem hj) (Nat.le_max_right 1 _) + +/-- All kernel-leaf input rows have the principal row width. -/ +private theorem pm_kernelLeafUnion_sizes (problem : XAdicProblem F) : + ∀ r ∈ MatrixRows + ((homogeneousKernelBasisRows (coefficientMatrixRows problem) + (problem.matrix.size * leafDegreeCap problem)).map + (vectorToPolynomialRow (leafDegreeCap problem) problem.matrix.size) ++ + kernelLeafCompletionRows problem), + r.size = problem.matrix.size := by + intro r hr + rw [MatrixRows, Array.toList_append] at hr + rcases List.mem_append.mp hr with hr | hr + · rw [Array.toList_map] at hr + rcases List.mem_map.mp hr with ⟨w, _hw, rfl⟩ + simp [vectorToPolynomialRow] + · simp only [kernelLeafCompletionRows] at hr + rcases List.mem_map.mp hr with ⟨i, _hi, rfl⟩ + simp [monomialUnitRow] + +/-- **Low part.** A bounded solution row, all of whose entry coefficients live +below the leaf degree cap, lies in the row span of the kernel-leaf input rows: +its packed coefficient vector is orthogonal to the scalar coefficient matrix, +so scalar-kernel completeness reconstructs it as a constant-coefficient +combination of the reconstructed kernel rows. -/ +private theorem pm_lowRow_mem_rowSpan_union (mulCtx : CPolynomial.MulContext F) + (problem : XAdicProblem F) (hpos : 0 < problem.matrix.size) + (hwf : WellFormed problem.matrix) {rem : PolynomialRow F} + (happrox : RowApproximates mulCtx problem rem) + (hsize : rem.size = problem.matrix.size) + (hdeg : ∀ k, k < rem.size → ∀ a, leafDegreeCap problem ≤ a → + CPolynomial.coeff (rowGet rem k) a = 0) : + rem ∈ RowSpan + ((homogeneousKernelBasisRows (coefficientMatrixRows problem) + (problem.matrix.size * leafDegreeCap problem)).map + (vectorToPolynomialRow (leafDegreeCap problem) problem.matrix.size) ++ + kernelLeafCompletionRows problem) := by + have hcap_pos : 0 < leafDegreeCap problem := le_max_left 1 _ + have hrt := vectorToPolynomialRow_rowToCoefficientVector problem hsize hdeg + have hdot := coefficientMatrixRows_dot_eq_zero_of_approximates mulCtx problem + happrox hwf + have hcomplete := homogeneousKernelBasisRows_complete + (coefficientMatrixRows problem) + (problem.matrix.size * leafDegreeCap problem) hdot + set cols := problem.matrix.size * leafDegreeCap problem with hcols + set kb := homogeneousKernelBasisRows (coefficientMatrixRows problem) cols + with hkbdef + set free := DenseMatrix.freeColumns cols + (scalarRrefRows (coefficientMatrixRows problem) cols).pivots with hfreedef + set v := rowToCoefficientVector problem rem with hvdef + set M := kb.map + (vectorToPolynomialRow (leafDegreeCap problem) problem.matrix.size) ++ + kernelLeafCompletionRows problem with hMdef + have hMsizes : ∀ r ∈ MatrixRows M, r.size = problem.matrix.size := by + rw [hMdef, hkbdef, hcols] + exact pm_kernelLeafUnion_sizes problem + have hMsize : M.size = kb.size + problem.matrix.size := by + rw [hMdef, Array.size_append, Array.size_map, pm_completionRows_size] + have hMpos : 0 < M.size := by omega + have hMwidth : MatrixWidth M = problem.matrix.size := + matrixWidth_eq_of_first_row hMpos hMsizes + set coeffs : Array (CPolynomial F) := Array.ofFn (fun i : Fin M.size ↦ + if i.val < kb.size then CPolynomial.C (v.getD (free.getD i.val 0) 0) + else 0) + with hcoeffsdef + have hcoeffs_getD : ∀ x, x < M.size → + coeffs.getD x 0 = + if x < kb.size then CPolynomial.C (v.getD (free.getD x 0) 0) + else 0 := by + intro x hx + rw [hcoeffsdef, array_getD_of_lt' _ _ (by simpa using hx)] + simp + refine ⟨coeffs, by rw [hcoeffsdef]; simp, ?_⟩ + refine pm_row_ext + (by rw [hsize, pm_rowLinearCombination_size hMsizes hMwidth]) ?_ + intro j + rw [pm_rowGet_rowLinearCombination] + apply CPolynomial.eq_iff_coeff.2 + intro a + rw [pm_coeff_finset_sum] + have hsub : Finset.range kb.size ⊆ Finset.range M.size := by + intro x hx + rw [Finset.mem_range] at hx ⊢ + omega + have hvan : ∀ x ∈ Finset.range M.size, x ∉ Finset.range kb.size → + CPolynomial.coeff (coeffs.getD x 0 * rowGet (M.getD x #[]) j) a = 0 := by + intro x hx hxn + have hxM : x < M.size := Finset.mem_range.mp hx + have hxkb : kb.size ≤ x := by + rcases Nat.lt_or_ge x kb.size with h | h + · exact absurd (Finset.mem_range.mpr h) hxn + · exact h + rw [hcoeffs_getD x hxM, if_neg (by omega), zero_mul, + CPolynomial.coeff_zero] + refine Eq.trans ?_ (Finset.sum_subset hsub hvan) + have hterm : ∀ i ∈ Finset.range kb.size, + CPolynomial.coeff (coeffs.getD i 0 * rowGet (M.getD i #[]) j) a = + v.getD (free.getD i 0) 0 * + (if j < problem.matrix.size ∧ a < leafDegreeCap problem then + (kb.getD i #[]).getD (j * leafDegreeCap problem + a) 0 + else 0) := by + intro i hi + have hikb : i < kb.size := Finset.mem_range.mp hi + have hiM : i < M.size := by omega + have hMget : M.getD i #[] = + vectorToPolynomialRow (leafDegreeCap problem) problem.matrix.size + (kb.getD i #[]) := by + rw [hMdef, pm_append_getD_left _ (by rw [Array.size_map]; exact hikb), + pm_map_getD _ _ #[] _ hikb] + rw [hcoeffs_getD i hiM, if_pos hikb, hMget, pm_coeff_C_mul, + rowGet_vectorToPolynomialRow_coeff] + have hlhs : CPolynomial.coeff (rowGet rem j) a = + if j < problem.matrix.size ∧ a < leafDegreeCap problem then + v.getD (j * leafDegreeCap problem + a) 0 + else 0 := by + rw [← hrt, rowGet_vectorToPolynomialRow_coeff] + rw [hlhs, Finset.sum_congr rfl hterm] + by_cases hcond : j < problem.matrix.size ∧ a < leafDegreeCap problem + · rw [if_pos hcond] + obtain ⟨hidx, -, -⟩ := pm_pack_index hcap_pos hcond.1 hcond.2 + refine Eq.trans (hcomplete _ (by rw [hcols]; exact hidx)) ?_ + exact Finset.sum_congr rfl fun i _hi ↦ by rw [if_pos hcond] + · rw [if_neg hcond] + exact (Finset.sum_eq_zero fun i _hi ↦ by rw [if_neg hcond, mul_zero]).symm + +/-- **High part.** A row whose entry coefficients all vanish below the leaf +degree cap lies in the row span of the kernel-leaf input rows: every entry +factors through `X^(leafDegreeCap problem)`, so the row is a polynomial +combination of the monomial completion rows. -/ +private theorem pm_highRow_mem_rowSpan_union (problem : XAdicProblem F) + (hpos : 0 < problem.matrix.size) {hi : PolynomialRow F} + (hsize : hi.size = problem.matrix.size) + (hlow : ∀ k a, a < leafDegreeCap problem → + CPolynomial.coeff (rowGet hi k) a = 0) : + hi ∈ RowSpan + ((homogeneousKernelBasisRows (coefficientMatrixRows problem) + (problem.matrix.size * leafDegreeCap problem)).map + (vectorToPolynomialRow (leafDegreeCap problem) problem.matrix.size) ++ + kernelLeafCompletionRows problem) := by + set A := (homogeneousKernelBasisRows (coefficientMatrixRows problem) + (problem.matrix.size * leafDegreeCap problem)).map + (vectorToPolynomialRow (leafDegreeCap problem) problem.matrix.size) + with hAdef + set M := A ++ kernelLeafCompletionRows problem with hMdef + have hMsizes : ∀ r ∈ MatrixRows M, r.size = problem.matrix.size := by + rw [hMdef, hAdef] + exact pm_kernelLeafUnion_sizes problem + have hMsize : M.size = A.size + problem.matrix.size := by + rw [hMdef, Array.size_append, pm_completionRows_size] + have hMpos : 0 < M.size := by omega + have hMwidth : MatrixWidth M = problem.matrix.size := + matrixWidth_eq_of_first_row hMpos hMsizes + have hMget_right : ∀ {b : Nat}, A.size ≤ b → b < M.size → + M.getD b #[] = + monomialUnitRow problem.matrix.size (b - A.size) + (leafDegreeCap problem) := by + intro b hb hbM + rw [hMdef, pm_append_getD_right _ hb, + pm_completionRows_getD problem (by omega)] + set coeffs : Array (CPolynomial F) := Array.ofFn (fun i : Fin M.size ↦ + if A.size ≤ i.val then + divXTrunc (leafDegreeCap problem) + ((rowGet hi (i.val - A.size)).val.size) (rowGet hi (i.val - A.size)) + else 0) + with hcoeffsdef + have hcoeffs_getD : ∀ x, x < M.size → + coeffs.getD x 0 = + if A.size ≤ x then + divXTrunc (leafDegreeCap problem) + ((rowGet hi (x - A.size)).val.size) (rowGet hi (x - A.size)) + else 0 := by + intro x hx + rw [hcoeffsdef, array_getD_of_lt' _ _ (by simpa using hx)] + simp + refine ⟨coeffs, by rw [hcoeffsdef]; simp, ?_⟩ + refine pm_row_ext + (by rw [hsize, pm_rowLinearCombination_size hMsizes hMwidth]) ?_ + intro j + rw [pm_rowGet_rowLinearCombination] + by_cases hj : j < problem.matrix.size + · have hother : ∀ b ∈ Finset.range M.size, b ≠ A.size + j → + coeffs.getD b 0 * rowGet (M.getD b #[]) j = 0 := by + intro b hb hbne + have hbM : b < M.size := Finset.mem_range.mp hb + rcases Nat.lt_or_ge b A.size with hbA | hbA + · rw [hcoeffs_getD b hbM, if_neg (by omega), zero_mul] + · rw [hMget_right hbA hbM, pm_rowGet_monomialUnitRow, + if_neg (by rintro ⟨-, h2⟩; exact hbne (by omega)), mul_zero] + refine (Eq.trans (Finset.sum_eq_single_of_mem (A.size + j) + (Finset.mem_range.mpr (by omega)) hother) ?_).symm + rw [hcoeffs_getD _ (by omega), if_pos (Nat.le_add_right _ _), + hMget_right (Nat.le_add_right _ _) (by omega), Nat.add_sub_cancel_left, + pm_rowGet_monomialUnitRow, if_pos ⟨hj, rfl⟩] + exact pm_high_factor fun t ht ↦ hlow j t ht + · have hzero : rowGet hi j = 0 := by + rw [rowGet, array_getD_of_le' _ _ (by omega)] + rw [hzero] + refine (Finset.sum_eq_zero fun b hb ↦ ?_).symm + have hbM : b < M.size := Finset.mem_range.mp hb + rcases Nat.lt_or_ge b A.size with hbA | hbA + · rw [hcoeffs_getD b hbM, if_neg (by omega), zero_mul] + · rw [hMget_right hbA hbM, pm_rowGet_monomialUnitRow, + if_neg fun h ↦ hj h.1, mul_zero] + +/-- **Decomposition.** Every solution row splits as a truncated low part plus +an `X^(leafDegreeCap)`-divisible high part, both of which stay in the row span +of the kernel-leaf input rows. -/ +private theorem pm_solution_mem_rowSpan_union (mulCtx : CPolynomial.MulContext F) + (problem : XAdicProblem F) (hpos : 0 < problem.matrix.size) + (hwf : WellFormed problem.matrix) {row : PolynomialRow F} + (happrox : RowApproximates mulCtx problem row) + (hsize : row.size = problem.matrix.size) : + row ∈ RowSpan + ((homogeneousKernelBasisRows (coefficientMatrixRows problem) + (problem.matrix.size * leafDegreeCap problem)).map + (vectorToPolynomialRow (leafDegreeCap problem) problem.matrix.size) ++ + kernelLeafCompletionRows problem) := by + set hi : PolynomialRow F := Array.ofFn (fun k : Fin row.size ↦ + rowGet row k.val - + truncateX (leafDegreeCap problem) (rowGet row k.val)) + with hhidef + have hhisize : hi.size = row.size := by + rw [hhidef] + simp + have hhiget : ∀ k, rowGet hi k = + rowGet row k - truncateX (leafDegreeCap problem) (rowGet row k) := by + intro k + rcases Nat.lt_or_ge k row.size with hk | hk + · rw [rowGet, array_getD_of_lt' _ _ (by omega)] + simp only [hhidef, Array.getElem_ofFn] + · rw [rowGet, array_getD_of_le' _ _ (by omega), rowGet, + array_getD_of_le' _ _ hk, truncateX_zero_eq_zero, sub_zero] + have hhilow : ∀ k a, a < leafDegreeCap problem → + CPolynomial.coeff (rowGet hi k) a = 0 := by + intro k a ha + rw [hhiget k, CPolynomial.coeff_sub, truncateX_coeff, if_pos ha, sub_self] + have hhiapprox : RowApproximates mulCtx problem hi := by + refine pm_rowApproximates_of_entry_dvd mulCtx problem fun k ↦ ?_ + rw [← truncateX_eq_zero_iff_X_pow_dvd] + exact pm_truncateX_eq_zero_of_coeff _ fun t ht ↦ hhilow k t ht + have hremget : ∀ k, rowGet (rowSub row hi) k = + truncateX (leafDegreeCap problem) (rowGet row k) := by + intro k + rw [rowGet_rowSub, hhiget k, sub_sub_cancel] + have hremsize : (rowSub row hi).size = problem.matrix.size := by + rw [rowSub_size, hhisize, Nat.max_self, hsize] + have hremdeg : ∀ k, k < (rowSub row hi).size → ∀ a, + leafDegreeCap problem ≤ a → + CPolynomial.coeff (rowGet (rowSub row hi) k) a = 0 := by + intro k _hk a ha + rw [hremget k, truncateX_coeff, if_neg (by omega)] + have hremapprox : RowApproximates mulCtx problem (rowSub row hi) := + rowApproximates_rowSub mulCtx problem happrox hhiapprox + have hsplit : rowAdd (rowSub row hi) hi = row := rowSub_add_cancel hhisize + rw [← hsplit] + exact rowAdd_mem_rowSpan + (pm_lowRow_mem_rowSpan_union mulCtx problem hpos hwf hremapprox hremsize + hremdeg) + (pm_highRow_mem_rowSpan_union problem hpos (hhisize.trans hsize) hhilow) + +/-- The incremental kernel-leaf reduction keeps a uniform input row width. -/ +private theorem pm_reduceIncremental_sizes {n : Nat} + {rows : PolynomialMatrix F} (shift : Array Nat) + (hsizes : ∀ r ∈ MatrixRows rows, r.size = n) : + ∀ r ∈ MatrixRows (reduceKernelLeafRowsIncremental rows shift), + r.size = n := by + intro r hr + refine reduceKernelLeafRowsIncremental_invariant (fun row ↦ row.size = n) + ?_ shift hsizes hr + intro target reducer shift' ht hrd + rw [cancelKernelLeafLeadingTerm] + split + · split + · split + · exact ht + · rw [rowSub_size, rowScaleCoeffX_size] + omega + · exact ht + · exact ht + +/-- **Generation completeness of the kernel-leaf basis.** Every nonzero +solution row of an X-adic problem lies in the row module generated by the +kernel-leaf basis. -/ +theorem kernelLeafBasis_rowSpan_complete [DecidableEq F] + (mulCtx : CPolynomial.MulContext F) (problem : XAdicProblem F) + (shift : Array Nat) (hpos : 0 < problem.matrix.size) + (hwf : WellFormed problem.matrix) {row : PolynomialRow F} + (happrox : RowApproximates mulCtx problem row) + (hsize : row.size = problem.matrix.size) + (_hnz : ¬ RowIsZero row) : + row ∈ RowSpan (kernelLeafBasis problem shift) := by + have hrow_union := pm_solution_mem_rowSpan_union mulCtx problem hpos hwf + happrox hsize + set union := (homogeneousKernelBasisRows (coefficientMatrixRows problem) + (problem.matrix.size * leafDegreeCap problem)).map + (vectorToPolynomialRow (leafDegreeCap problem) problem.matrix.size) ++ + kernelLeafCompletionRows problem with hunion + set reduced := reduceKernelLeafRowsIncremental union shift with hreduced + have hbasis_eq : kernelLeafBasis problem shift = + reduced ++ missingCompletionRows problem shift reduced := by + rw [hreduced, hunion] + simp only [kernelLeafBasis, completeMissingPivotRows] + have hunionsizes : ∀ r ∈ MatrixRows union, r.size = problem.matrix.size := by + rw [hunion] + exact pm_kernelLeafUnion_sizes problem + have hredsizes : ∀ r ∈ MatrixRows reduced, r.size = problem.matrix.size := by + rw [hreduced] + exact pm_reduceIncremental_sizes shift hunionsizes + have hwit_mem : monomialUnitRow (F := F) problem.matrix.size 0 + (leafDegreeCap problem) ∈ MatrixRows union := by + rw [hunion, MatrixRows, Array.toList_append] + refine List.mem_append.mpr (Or.inr ?_) + simp only [kernelLeafCompletionRows] + exact List.mem_map.mpr ⟨0, List.mem_range.mpr hpos, rfl⟩ + have hwit_size : (monomialUnitRow (F := F) problem.matrix.size 0 + (leafDegreeCap problem)).size = problem.matrix.size := by + simp [monomialUnitRow] + have hwit_nz : ¬ RowIsZero (monomialUnitRow (F := F) problem.matrix.size 0 + (leafDegreeCap problem)) := by + intro hz + have hget : rowGet (monomialUnitRow (F := F) problem.matrix.size 0 + (leafDegreeCap problem)) 0 = coeffXPower 1 (leafDegreeCap problem) := by + rw [pm_rowGet_monomialUnitRow, if_pos ⟨hpos, rfl⟩] + have hmem0 : (monomialUnitRow (F := F) problem.matrix.size 0 + (leafDegreeCap problem))[0]'(by omega) ∈ + (monomialUnitRow (F := F) problem.matrix.size 0 + (leafDegreeCap problem)).toList := + Array.getElem_mem_toList (by omega) + have hzero := hz _ hmem0 + rw [rowGet, array_getD_of_lt' _ _ (by omega), hzero] at hget + have hone : CPolynomial.coeff (coeffXPower (1 : F) (leafDegreeCap problem)) + (leafDegreeCap problem) = 1 := by + rw [CPolynomial.coeff_toPoly, coeffXPower_toPoly, Polynomial.coeff_C_mul, + Polynomial.coeff_X_pow, if_pos rfl, mul_one] + rw [← hget, CPolynomial.coeff_zero] at hone + exact zero_ne_one hone + have hwit_span : monomialUnitRow (F := F) problem.matrix.size 0 + (leafDegreeCap problem) ∈ RowSpan reduced := by + rw [hreduced] + exact reduceKernelLeafRowsIncremental_rowSpan_superset hunionsizes + hwit_mem hwit_nz + have hredpos : 0 < reduced.size := by + rcases Nat.eq_zero_or_pos reduced.size with h0 | hp + · exfalso + rw [Array.eq_empty_of_size_eq_zero h0] at hwit_span + have hweq := eq_empty_of_mem_rowSpan_empty hwit_span + rw [hweq] at hwit_size + simp at hwit_size + omega + · exact hp + have hunionpos : 0 < union.size := by + rw [hunion, Array.size_append, pm_completionRows_size] + omega + have hunionwidth : MatrixWidth union = problem.matrix.size := + matrixWidth_eq_of_first_row hunionpos hunionsizes + have hredwidth : MatrixWidth reduced = problem.matrix.size := + matrixWidth_eq_of_first_row hredpos hredsizes + have hsub1 : RowSpan union ⊆ RowSpan reduced := by + refine rowSpan_subset_of_rows_mem (wellFormed_of_sizes hredsizes) + (hunionwidth.trans hredwidth.symm) ?_ + intro r hr + by_cases hz : RowIsZero r + · have hreq : r = zeroRow problem.matrix.size := by + have hzr := rowIsZero_eq_zeroRow hz + rwa [hunionsizes r hr] at hzr + rw [hreq] + have hzero := zeroRow_mem_rowSpan (wellFormed_of_sizes hredsizes) + rwa [hredwidth] at hzero + · rw [hreduced] + exact reduceKernelLeafRowsIncremental_rowSpan_superset hunionsizes hr hz + have hbasissizes : ∀ r ∈ MatrixRows (kernelLeafBasis problem shift), + r.size = problem.matrix.size := + fun r hr ↦ (kernelLeafBasis_rows mulCtx problem shift r hr).2 + have hbasispos : 0 < (kernelLeafBasis problem shift).size := by + rw [hbasis_eq, Array.size_append] + omega + have hbasiswidth : MatrixWidth (kernelLeafBasis problem shift) = + problem.matrix.size := + matrixWidth_eq_of_first_row hbasispos hbasissizes + have hsub2 : RowSpan reduced ⊆ RowSpan (kernelLeafBasis problem shift) := by + refine rowSpan_subset_of_rows_mem (wellFormed_of_sizes hbasissizes) + (hredwidth.trans hbasiswidth.symm) ?_ + intro r hr + refine matrix_row_mem_rowSpan (wellFormed_of_sizes hbasissizes) ?_ + rw [hbasis_eq, MatrixRows, Array.toList_append] + exact List.mem_append.mpr (Or.inl hr) + exact hsub2 (hsub1 hrow_union) + +end Approximant + +end PolynomialMatrix + +end CompPoly diff --git a/CompPoly/LinearAlgebra/PolynomialMatrix/Approximant/PMBasis/KernelLeafScalar.lean b/CompPoly/LinearAlgebra/PolynomialMatrix/Approximant/PMBasis/KernelLeafScalar.lean new file mode 100644 index 00000000..f9b5f4ea --- /dev/null +++ b/CompPoly/LinearAlgebra/PolynomialMatrix/Approximant/PMBasis/KernelLeafScalar.lean @@ -0,0 +1,1344 @@ +/- +Copyright (c) 2026 CompPoly Contributors. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Valerii Huhnin +-/ + +module + +public import CompPoly.LinearAlgebra.PolynomialMatrix.Approximant.PMBasis.KernelLeaf +public import Mathlib.Algebra.BigOperators.Group.Finset.Basic +public import Mathlib.Algebra.BigOperators.Group.Finset.Piecewise +public import Mathlib.Algebra.Order.Field.Basic + +/-! +# Scalar Kernel Leaf Correctness + +Soundness and completeness of the row-array scalar RREF kernel used by the +PM-basis leaf: every emitted vector is orthogonal to the input rows, and +every orthogonal vector is an `F`-linear combination of the emitted basis. +-/ + +@[expose] public section + +namespace CompPoly + +namespace PolynomialMatrix + +namespace Approximant + +variable {F : Type*} [Field F] [BEq F] [LawfulBEq F] + +/-! ## Scalar kernel leaf soundness + +Soundness of the row-array scalar RREF kernel used by the PM-basis leaf: +every vector produced by `homogeneousKernelBasisRows rows cols` has size +`cols` and is orthogonal (over the first `cols` coordinates) to every input +row. -/ + +/-! ## Generic array access lemmas -/ + +section ArrayAccess + +variable {α : Type*} + +/-- `getD` at an out-of-bounds index returns the default. -/ +theorem array_getD_of_le' (xs : Array α) (d : α) {i : Nat} + (h : xs.size ≤ i) : xs.getD i d = d := by + rw [Array.getD_eq_getD_getElem?, Array.getElem?_eq_none h] + rfl + +/-- `getD` at an in-bounds index returns the indexed element. -/ +theorem array_getD_of_lt' (xs : Array α) (d : α) {i : Nat} + (h : i < xs.size) : xs.getD i d = xs[i] := by + rw [Array.getD_eq_getD_getElem?, Array.getElem?_eq_getElem h] + rfl + +/-- `getD` after `setIfInBounds` in coordinates. -/ +theorem array_getD_setIfInBounds (xs : Array α) (j : Nat) (a : α) + (i : Nat) (d : α) : + (xs.setIfInBounds j a).getD i d = + if j = i ∧ j < xs.size then a else xs.getD i d := by + rw [Array.getD_eq_getD_getElem?, Array.getElem?_setIfInBounds] + by_cases hji : j = i + · subst hji + by_cases hj : j < xs.size + · simp [hj] + · simp [hj, Array.getD_eq_getD_getElem?] + · simp [hji, Array.getD_eq_getD_getElem?] + +private theorem array_getD_push (xs : Array α) (a : α) (i : Nat) (d : α) : + (xs.push a).getD i d = + if i = xs.size then a else xs.getD i d := by + rw [Array.getD_eq_getD_getElem?, Array.getElem?_push] + by_cases hi : i = xs.size + · simp [hi] + · simp [hi, Array.getD_eq_getD_getElem?] + +end ArrayAccess + +/-! ## Coordinate access helpers -/ + +section Coordinates + +variable {F : Type*} + +private theorem array_getD_eq_zero_of_le [Zero F] {r : Array F} {k : Nat} + (h : r.size ≤ k) : r.getD k 0 = 0 := + array_getD_of_le' r 0 h + +private theorem array_getD_eq_getElem [Zero F] {r : Array F} {k : Nat} + (h : k < r.size) : r.getD k 0 = r[k] := + array_getD_of_lt' r 0 h + +/-- Coordinatewise description of `addScaledScalarRow`. -/ +theorem addScaledScalarRow_getD [Field F] (target source : Array F) + (factor : F) (k : Nat) : + (addScaledScalarRow target source factor).getD k 0 = + target.getD k 0 + factor * source.getD k 0 := by + unfold addScaledScalarRow + by_cases hk : k < max target.size source.size + · rw [array_getD_eq_getElem (by simpa using hk)] + simp [List.getElem_toArray] + · rw [array_getD_eq_zero_of_le (by simpa using Nat.le_of_not_lt hk)] + have htarget : target.getD k 0 = 0 := + array_getD_eq_zero_of_le (by omega) + have hsource : source.getD k 0 = 0 := + array_getD_eq_zero_of_le (by omega) + rw [htarget, hsource, mul_zero, add_zero] + +/-- Coordinatewise description of `normalizeScalarRow` at a nonzero pivot. -/ +theorem normalizeScalarRow_getD [Field F] [BEq F] [LawfulBEq F] + (row : Array F) (pivotCol : Nat) (hpivot : row.getD pivotCol 0 ≠ 0) + (k : Nat) : + (normalizeScalarRow row pivotCol).getD k 0 = + row.getD k 0 / row.getD pivotCol 0 := by + unfold normalizeScalarRow + rw [if_neg (by simpa using hpivot)] + by_cases hk : k < row.size + · rw [array_getD_eq_getElem (by simpa using hk), array_getD_eq_getElem hk] + simp + · rw [array_getD_eq_zero_of_le (by simpa using Nat.le_of_not_lt hk), + array_getD_eq_zero_of_le (Nat.le_of_not_lt hk), zero_div] + +end Coordinates + +/-! ## Scalar dot products and orthogonality -/ + +section Dot + +variable {F : Type*} + +/-- Dot product of the first `cols` coordinates of two scalar rows, with +zero defaults beyond the stored lengths. -/ +def scalarDot [Field F] (cols : Nat) (r v : Array F) : F := + ∑ k ∈ Finset.range cols, r.getD k 0 * v.getD k 0 + +/-- The empty row is orthogonal to everything. -/ +theorem scalarDot_empty [Field F] (cols : Nat) (v : Array F) : + scalarDot cols (#[] : Array F) v = 0 := by + unfold scalarDot + refine Finset.sum_eq_zero fun k _ ↦ ?_ + rw [array_getD_eq_zero_of_le (by simp), zero_mul] + +/-- `scalarDot` is additive along `addScaledScalarRow`. -/ +theorem scalarDot_addScaledScalarRow [Field F] (cols : Nat) + (target source v : Array F) (factor : F) : + scalarDot cols (addScaledScalarRow target source factor) v = + scalarDot cols target v + factor * scalarDot cols source v := by + unfold scalarDot + rw [Finset.mul_sum, ← Finset.sum_add_distrib] + refine Finset.sum_congr rfl fun k _ ↦ ?_ + rw [addScaledScalarRow_getD] + ring + +/-- A row recovers from its normalization by rescaling with the pivot. -/ +theorem scalarDot_eq_pivot_mul_normalize [Field F] [BEq F] [LawfulBEq F] + (cols : Nat) (row v : Array F) (pivotCol : Nat) + (hpivot : row.getD pivotCol 0 ≠ 0) : + scalarDot cols row v = + row.getD pivotCol 0 * scalarDot cols (normalizeScalarRow row pivotCol) v := by + unfold scalarDot + rw [Finset.mul_sum] + refine Finset.sum_congr rfl fun k _ ↦ ?_ + rw [normalizeScalarRow_getD row pivotCol hpivot] + field_simp + +/-- Orthogonality of `v` to every row of a row-array matrix, expressed via +total `getD` access so that out-of-range indices are harmless. -/ +def OrthRows [Field F] (cols : Nat) (rows : Array (Array F)) (v : Array F) : + Prop := + ∀ i, scalarDot cols (rows.getD i #[]) v = 0 + +end Dot + +/-! ## Elementary-step characterizations -/ + +section Steps + +variable {F : Type*} + +/-- Swapping rows preserves the row count. -/ +theorem swapScalarRows_size (rows : Array (Array F)) (a b : Nat) : + (swapScalarRows rows a b).size = rows.size := by + simp [swapScalarRows, Array.size_setIfInBounds] + +private theorem swapScalarRows_getD (rows : Array (Array F)) {a b : Nat} + (ha : a < rows.size) (hb : b < rows.size) (i : Nat) : + (swapScalarRows rows a b).getD i #[] = + if i = b then rows.getD a #[] + else if i = a then rows.getD b #[] + else rows.getD i #[] := by + unfold swapScalarRows + rw [array_getD_setIfInBounds, Array.size_setIfInBounds, + array_getD_setIfInBounds] + by_cases hib : i = b + · subst hib + simp [hb] + · by_cases hia : i = a + · subst hia + have hba : ¬b = i := fun h ↦ hib h.symm + simp [hba, hib, ha] + · have hba : ¬b = i := fun h ↦ hib h.symm + have haa : ¬a = i := fun h ↦ hia h.symm + simp [hba, haa, hib, hia] + +private theorem findScalarPivotRow_eq_some [Field F] [BEq F] [LawfulBEq F] + {rows : Array (Array F)} {startRow col p : Nat} + (h : findScalarPivotRow rows startRow col = some p) : + startRow ≤ p ∧ p < rows.size ∧ (rows.getD p #[]).getD col 0 ≠ 0 := by + unfold findScalarPivotRow at h + have hmem := List.mem_of_find?_eq_some h + have hbounds := List.mem_range'_1.mp hmem + have hpred := List.find?_some h + refine ⟨hbounds.1, by omega, ?_⟩ + exact bne_iff_ne.mp hpred + +private theorem findScalarPivotRow_eq_none [Field F] [BEq F] [LawfulBEq F] + {rows : Array (Array F)} {startRow col : Nat} + (h : findScalarPivotRow rows startRow col = none) : + ∀ i, startRow ≤ i → (rows.getD i #[]).getD col 0 = 0 := by + intro i hi + by_cases hsize : i < rows.size + · have hmem : i ∈ List.range' startRow (rows.size - startRow) := + List.mem_range'_1.mpr ⟨hi, by omega⟩ + have hfail := List.find?_eq_none.mp h i hmem + simpa using hfail + · rw [array_getD_of_le' rows #[] (Nat.le_of_not_lt hsize)] + exact array_getD_of_le' _ _ (by simp) + +end Steps + +/-! ## Backward span: row operations are invertible -/ + +section Backward + +variable {F : Type*} + +/-- The elimination step performed for each row inside +`normalizeAndEliminateScalarRows`, lifted to a top-level definition so that +fold lemmas can be stated about it. -/ +private def elimStep [Field F] [BEq F] (pivotRow : Nat) + (pivotVector : Array F) (pivotCol : Nat) (rows : Array (Array F)) + (row : Nat) : Array (Array F) := + if row == pivotRow then + rows + else if -((rows.getD row #[]).getD pivotCol 0) == 0 then + rows + else + rows.setIfInBounds row + (addScaledScalarRow (rows.getD row #[]) pivotVector + (-((rows.getD row #[]).getD pivotCol 0))) + +private theorem normalizeAndEliminateScalarRows_eq_foldl [Field F] [BEq F] + [LawfulBEq F] (rows : Array (Array F)) (pivotRow pivotCol : Nat) + (h : (rows.getD pivotRow #[]).getD pivotCol 0 ≠ 0) : + normalizeAndEliminateScalarRows rows pivotRow pivotCol = + (List.range + (rows.setIfInBounds pivotRow + (normalizeScalarRow (rows.getD pivotRow #[]) pivotCol)).size).foldl + (elimStep pivotRow + (normalizeScalarRow (rows.getD pivotRow #[]) pivotCol) pivotCol) + (rows.setIfInBounds pivotRow + (normalizeScalarRow (rows.getD pivotRow #[]) pivotCol)) := by + unfold normalizeAndEliminateScalarRows elimStep + rw [if_neg (by simpa using h)] + +private theorem elimStep_size [Field F] [BEq F] (pivotRow : Nat) + (pivotVector : Array F) (pivotCol : Nat) (rows : Array (Array F)) + (row : Nat) : + (elimStep pivotRow pivotVector pivotCol rows row).size = rows.size := by + unfold elimStep + by_cases hrp : (row == pivotRow) = true + · rw [if_pos hrp] + · rw [if_neg hrp] + by_cases hfac : (-((rows.getD row #[]).getD pivotCol 0) == 0) = true + · rw [if_pos hfac] + · rw [if_neg hfac] + exact Array.size_setIfInBounds + +private theorem foldl_elimStep_size [Field F] [BEq F] (pivotRow : Nat) + (pivotVector : Array F) (pivotCol : Nat) : + ∀ (l : List Nat) (acc : Array (Array F)), + (l.foldl (elimStep pivotRow pivotVector pivotCol) acc).size = acc.size := by + intro l + induction l with + | nil => intro acc; rfl + | cons a l ih => + intro acc + rw [List.foldl_cons, ih, elimStep_size] + +private theorem elimStep_getD_pivotRow [Field F] [BEq F] (pivotRow : Nat) + (pivotVector : Array F) (pivotCol : Nat) (rows : Array (Array F)) + (row : Nat) : + (elimStep pivotRow pivotVector pivotCol rows row).getD pivotRow #[] = + rows.getD pivotRow #[] := by + unfold elimStep + by_cases hrp : (row == pivotRow) = true + · rw [if_pos hrp] + · rw [if_neg hrp] + by_cases hfac : (-((rows.getD row #[]).getD pivotCol 0) == 0) = true + · rw [if_pos hfac] + · rw [if_neg hfac] + have hne : ¬(row = pivotRow ∧ row < rows.size) := by + intro hc + exact hrp (beq_iff_eq.mpr hc.1) + rw [array_getD_setIfInBounds, if_neg hne] + +private theorem foldl_elimStep_getD_pivotRow [Field F] [BEq F] + (pivotRow : Nat) (pivotVector : Array F) (pivotCol : Nat) : + ∀ (l : List Nat) (acc : Array (Array F)), + ((l.foldl (elimStep pivotRow pivotVector pivotCol) acc).getD + pivotRow #[]) = acc.getD pivotRow #[] := by + intro l + induction l with + | nil => intro acc; rfl + | cons a l ih => + intro acc + rw [List.foldl_cons, ih, elimStep_getD_pivotRow] + +private theorem orthRows_of_elimStep [Field F] [BEq F] [LawfulBEq F] + {cols pivotRow pivotCol : Nat} {pivotVector v : Array F} + {rows : Array (Array F)} {a : Nat} + (hpv : scalarDot cols pivotVector v = 0) + (h : OrthRows cols (elimStep pivotRow pivotVector pivotCol rows a) v) : + OrthRows cols rows v := by + unfold elimStep at h + by_cases hap : (a == pivotRow) = true + · rwa [if_pos hap] at h + · rw [if_neg hap] at h + by_cases hfac : (-((rows.getD a #[]).getD pivotCol 0) == 0) = true + · rwa [if_pos hfac] at h + · rw [if_neg hfac] at h + intro i + by_cases hia : i = a + · subst hia + have hi := h i + rw [array_getD_setIfInBounds] at hi + by_cases hsize : i < rows.size + · rw [if_pos ⟨rfl, hsize⟩, scalarDot_addScaledScalarRow, hpv, + mul_zero, add_zero] at hi + exact hi + · have hne : ¬(i = i ∧ i < rows.size) := fun hc ↦ hsize hc.2 + rwa [if_neg hne] at hi + · have hi := h i + have hne : ¬(a = i ∧ a < rows.size) := fun hc ↦ hia hc.1.symm + rwa [array_getD_setIfInBounds, if_neg hne] at hi + +private theorem orthRows_of_foldl_elimStep [Field F] [BEq F] [LawfulBEq F] + {cols pivotRow pivotCol : Nat} {pivotVector v : Array F} + (hpv : scalarDot cols pivotVector v = 0) : + ∀ (l : List Nat) (acc : Array (Array F)), + OrthRows cols (l.foldl (elimStep pivotRow pivotVector pivotCol) acc) v → + OrthRows cols acc v := by + intro l + induction l with + | nil => intro acc h; exact h + | cons a l ih => + intro acc h + rw [List.foldl_cons] at h + exact orthRows_of_elimStep hpv (ih _ h) + +private theorem orthRows_of_normalizeAndEliminate [Field F] [BEq F] + [LawfulBEq F] {cols : Nat} {rows : Array (Array F)} + {pivotRow pivotCol : Nat} {v : Array F} (hpr : pivotRow < rows.size) + (h : OrthRows cols (normalizeAndEliminateScalarRows rows pivotRow pivotCol) + v) : + OrthRows cols rows v := by + by_cases hpivot : (rows.getD pivotRow #[]).getD pivotCol 0 = 0 + · unfold normalizeAndEliminateScalarRows at h + rwa [if_pos (by simpa using hpivot)] at h + · rw [normalizeAndEliminateScalarRows_eq_foldl rows pivotRow pivotCol hpivot] + at h + have hpv_entry : + (rows.setIfInBounds pivotRow + (normalizeScalarRow (rows.getD pivotRow #[]) pivotCol)).getD + pivotRow #[] = + normalizeScalarRow (rows.getD pivotRow #[]) pivotCol := by + rw [array_getD_setIfInBounds, if_pos ⟨rfl, hpr⟩] + have hpv : scalarDot cols + (normalizeScalarRow (rows.getD pivotRow #[]) pivotCol) v = 0 := by + have hp := h pivotRow + rwa [foldl_elimStep_getD_pivotRow, hpv_entry] at hp + have h₁ := orthRows_of_foldl_elimStep hpv _ _ h + intro i + by_cases hip : i = pivotRow + · subst hip + rw [scalarDot_eq_pivot_mul_normalize cols _ v pivotCol hpivot, hpv, + mul_zero] + · have hi := h₁ i + have hne : ¬(pivotRow = i ∧ pivotRow < rows.size) := + fun hc ↦ hip hc.1.symm + rwa [array_getD_setIfInBounds, if_neg hne] at hi + +private theorem orthRows_of_scalarRrefRowsLoop [Field F] [BEq F] [LawfulBEq F] + {cols : Nat} {v : Array F} : + ∀ (fuel col row : Nat) (rows : Array (Array F)) (pivots : Array Nat), + OrthRows cols + (scalarRrefRowsLoop cols fuel col row rows pivots).rows v → + OrthRows cols rows v := by + intro fuel + induction fuel with + | zero => + intro col row rows pivots h + exact h + | succ fuel ih => + intro col row rows pivots h + simp only [scalarRrefRowsLoop] at h + split at h + · exact h + · rename_i hguard + have hrow : row < rows.size := by + simp only [ge_iff_le, Bool.or_eq_true, decide_eq_true_eq, + not_or, not_le] at hguard + exact hguard.2 + cases hfind : findScalarPivotRow rows row col with + | none => + rw [hfind] at h + exact ih _ _ _ _ h + | some p => + rw [hfind] at h + obtain ⟨hp_le, hp_lt, _⟩ := findScalarPivotRow_eq_some hfind + have hreduced := ih _ _ _ _ h + have hswapped : OrthRows cols (swapScalarRows rows p row) v := + orthRows_of_normalizeAndEliminate + (by rwa [swapScalarRows_size]) hreduced + intro i + by_cases hib : i = row + · subst hib + have hs := hswapped p + rw [swapScalarRows_getD rows hp_lt hrow] at hs + by_cases hpr : p = i + · rwa [if_pos hpr, hpr] at hs + · rwa [if_neg hpr, if_pos rfl] at hs + · by_cases hia : i = p + · subst hia + have hs := hswapped row + rwa [swapScalarRows_getD rows hp_lt hrow, if_pos rfl] at hs + · have hs := hswapped i + rwa [swapScalarRows_getD rows hp_lt hrow, if_neg hib, + if_neg hia] at hs + +/-- Orthogonality to all rows of the final RREF matrix transfers back to all +rows of the original matrix: every row operation is invertible. -/ +theorem orthRows_of_scalarRrefRows [Field F] [BEq F] [LawfulBEq F] + {cols : Nat} {rows : Array (Array F)} {v : Array F} + (h : OrthRows cols (scalarRrefRows rows cols).rows v) : + OrthRows cols rows v := + orthRows_of_scalarRrefRowsLoop _ _ _ _ _ h + +end Backward + +/-! ## Forward RREF shape invariants -/ + +section Forward + +variable {F : Type*} + +private theorem elimStep_getD_other [Field F] [BEq F] {pivotRow : Nat} + {pivotVector : Array F} {pivotCol : Nat} {rows : Array (Array F)} + {a i : Nat} (hia : i ≠ a) : + (elimStep pivotRow pivotVector pivotCol rows a).getD i #[] = + rows.getD i #[] := by + unfold elimStep + by_cases hap : (a == pivotRow) = true + · rw [if_pos hap] + · rw [if_neg hap] + by_cases hfac : (-((rows.getD a #[]).getD pivotCol 0) == 0) = true + · rw [if_pos hfac] + · rw [if_neg hfac] + have hne : ¬(a = i ∧ a < rows.size) := fun hc ↦ hia hc.1.symm + rw [array_getD_setIfInBounds, if_neg hne] + +private theorem elimStep_getD_self_entry [Field F] [BEq F] [LawfulBEq F] + (pivotRow : Nat) (pivotVector : Array F) (pivotCol : Nat) + (rows : Array (Array F)) (a k : Nat) : + ((elimStep pivotRow pivotVector pivotCol rows a).getD a #[]).getD k 0 = + if a = pivotRow then + (rows.getD a #[]).getD k 0 + else + (rows.getD a #[]).getD k 0 - + (rows.getD a #[]).getD pivotCol 0 * pivotVector.getD k 0 := by + unfold elimStep + by_cases hap : (a == pivotRow) = true + · rw [if_pos hap, if_pos (beq_iff_eq.mp hap)] + · have hap' : ¬a = pivotRow := by simpa using hap + rw [if_neg hap, if_neg hap'] + by_cases hfac : (-((rows.getD a #[]).getD pivotCol 0) == 0) = true + · have hzero : (rows.getD a #[]).getD pivotCol 0 = 0 := + neg_eq_zero.mp (beq_iff_eq.mp hfac) + rw [if_pos hfac, hzero, zero_mul, sub_zero] + · rw [if_neg hfac] + by_cases hsize : a < rows.size + · rw [array_getD_setIfInBounds, if_pos ⟨rfl, hsize⟩, + addScaledScalarRow_getD, neg_mul, ← sub_eq_add_neg] + · have hne : ¬(a = a ∧ a < rows.size) := fun hc ↦ hsize hc.2 + rw [array_getD_setIfInBounds, if_neg hne, + array_getD_of_le' rows #[] (Nat.le_of_not_lt hsize)] + have h0 : ∀ j, (#[] : Array F).getD j 0 = 0 := + fun j ↦ array_getD_of_le' _ _ (by simp) + rw [h0, h0, zero_mul, sub_zero] + +private theorem foldl_elimStep_getD_entry [Field F] [BEq F] [LawfulBEq F] + (pivotRow : Nat) (pivotVector : Array F) (pivotCol : Nat) : + ∀ (l : List Nat), l.Nodup → + ∀ (acc : Array (Array F)) (i k : Nat), + (((l.foldl (elimStep pivotRow pivotVector pivotCol) acc).getD + i #[]).getD k 0) = + if i ∈ l ∧ i ≠ pivotRow then + (acc.getD i #[]).getD k 0 - + (acc.getD i #[]).getD pivotCol 0 * pivotVector.getD k 0 + else + (acc.getD i #[]).getD k 0 := by + intro l + induction l with + | nil => + intro _ acc i k + have hne : ¬(i ∈ ([] : List Nat) ∧ i ≠ pivotRow) := by simp + rw [List.foldl_nil, if_neg hne] + | cons a l ih => + intro hnodup acc i k + obtain ⟨ha_notin, hl⟩ := List.nodup_cons.mp hnodup + rw [List.foldl_cons, ih hl] + by_cases hia : i = a + · subst hia + have hnot : ¬(i ∈ l ∧ i ≠ pivotRow) := fun hc ↦ ha_notin hc.1 + rw [if_neg hnot, elimStep_getD_self_entry] + by_cases hip : i = pivotRow + · have hnot2 : ¬(i ∈ i :: l ∧ i ≠ pivotRow) := fun hc ↦ hc.2 hip + rw [if_pos hip, if_neg hnot2] + · rw [if_neg hip, if_pos ⟨by simp, hip⟩] + · rw [elimStep_getD_other hia] + by_cases hil : i ∈ l ∧ i ≠ pivotRow + · rw [if_pos hil, if_pos ⟨List.mem_cons_of_mem a hil.1, hil.2⟩] + · have hnot : ¬(i ∈ a :: l ∧ i ≠ pivotRow) := by + intro hc + rcases List.mem_cons.mp hc.1 with h | h + · exact hia h + · exact hil ⟨h, hc.2⟩ + rw [if_neg hil, if_neg hnot] + +private theorem normalizeAndEliminateScalarRows_getD_entry [Field F] [BEq F] + [LawfulBEq F] (rows : Array (Array F)) (pivotRow pivotCol : Nat) + (hpr : pivotRow < rows.size) + (hpivot : (rows.getD pivotRow #[]).getD pivotCol 0 ≠ 0) (i k : Nat) : + ((normalizeAndEliminateScalarRows rows pivotRow pivotCol).getD + i #[]).getD k 0 = + if i = pivotRow then + (rows.getD pivotRow #[]).getD k 0 / + (rows.getD pivotRow #[]).getD pivotCol 0 + else + (rows.getD i #[]).getD k 0 - + (rows.getD i #[]).getD pivotCol 0 * + ((rows.getD pivotRow #[]).getD k 0 / + (rows.getD pivotRow #[]).getD pivotCol 0) := by + rw [normalizeAndEliminateScalarRows_eq_foldl rows pivotRow pivotCol hpivot, + foldl_elimStep_getD_entry pivotRow _ pivotCol _ List.nodup_range] + have hrows₁_pr : + (rows.setIfInBounds pivotRow + (normalizeScalarRow (rows.getD pivotRow #[]) pivotCol)).getD + pivotRow #[] = + normalizeScalarRow (rows.getD pivotRow #[]) pivotCol := by + rw [array_getD_setIfInBounds, if_pos ⟨rfl, hpr⟩] + have hpv_getD := normalizeScalarRow_getD (rows.getD pivotRow #[]) pivotCol + hpivot + by_cases hip : i = pivotRow + · subst hip + have hnot : ¬(i ∈ List.range + (rows.setIfInBounds i + (normalizeScalarRow (rows.getD i #[]) pivotCol)).size ∧ i ≠ i) := + fun hc ↦ hc.2 rfl + rw [if_neg hnot, if_pos rfl, hrows₁_pr, hpv_getD] + · rw [if_neg hip] + have hrows₁_i : + (rows.setIfInBounds pivotRow + (normalizeScalarRow (rows.getD pivotRow #[]) pivotCol)).getD + i #[] = rows.getD i #[] := by + have hne : ¬(pivotRow = i ∧ pivotRow < rows.size) := + fun hc ↦ hip hc.1.symm + rw [array_getD_setIfInBounds, if_neg hne] + by_cases hisize : i < rows.size + · have hmem : i ∈ List.range + (rows.setIfInBounds pivotRow + (normalizeScalarRow (rows.getD pivotRow #[]) pivotCol)).size := by + rw [List.mem_range, Array.size_setIfInBounds] + exact hisize + rw [if_pos ⟨hmem, hip⟩, hrows₁_i, hpv_getD] + · have hnot : ¬(i ∈ List.range + (rows.setIfInBounds pivotRow + (normalizeScalarRow (rows.getD pivotRow #[]) pivotCol)).size ∧ + i ≠ pivotRow) := by + intro hc + have := List.mem_range.mp hc.1 + rw [Array.size_setIfInBounds] at this + exact hisize this + rw [if_neg hnot, hrows₁_i, + array_getD_of_le' rows #[] (Nat.le_of_not_lt hisize)] + have h0 : ∀ j, (#[] : Array F).getD j 0 = 0 := + fun j ↦ array_getD_of_le' _ _ (by simp) + rw [h0, h0, zero_mul, sub_zero] + +/-- Shape contract satisfied by the result of `scalarRrefRows`: recorded +pivot columns are strictly increasing and below `cols`, each pivot column is +a unit column with its one in the corresponding pivot row, and every row at +or beyond the pivot count vanishes on all columns below `cols`. -/ +structure ScalarRrefSpec [Field F] (cols : Nat) + (R : ScalarRrefResult (F := F)) : Prop where + mono : ∀ s t, s < t → t < R.pivots.size → + R.pivots.getD s 0 < R.pivots.getD t 0 + pivots_lt : ∀ t, t < R.pivots.size → R.pivots.getD t 0 < cols + unit : ∀ t, t < R.pivots.size → ∀ i, + (R.rows.getD i #[]).getD (R.pivots.getD t 0) 0 = if i = t then 1 else 0 + tail_zero : ∀ i, R.pivots.size ≤ i → ∀ k, k < cols → + (R.rows.getD i #[]).getD k 0 = 0 + +private theorem scalarRrefRowsLoop_spec [Field F] [BEq F] [LawfulBEq F] + {cols : Nat} : + ∀ (fuel col row : Nat) (rows : Array (Array F)) (pivots : Array Nat), + cols < col + fuel → + row = pivots.size → + (∀ s t, s < t → t < pivots.size → + pivots.getD s 0 < pivots.getD t 0) → + (∀ t, t < pivots.size → pivots.getD t 0 < col) → + (∀ t, t < pivots.size → pivots.getD t 0 < cols) → + (∀ t, t < pivots.size → ∀ i, + (rows.getD i #[]).getD (pivots.getD t 0) 0 = if i = t then 1 else 0) → + (∀ i, row ≤ i → ∀ k, k < col → (rows.getD i #[]).getD k 0 = 0) → + ScalarRrefSpec cols (scalarRrefRowsLoop cols fuel col row rows pivots) := by + intro fuel + induction fuel with + | zero => + intro col row rows pivots hfuel hrow hmono _hltcol hltcols hunit hzero + exact ⟨hmono, hltcols, hunit, fun i hi k hk ↦ + hzero i (le_of_eq_of_le hrow hi) k (Nat.lt_trans hk hfuel)⟩ + | succ fuel ih => + intro col row rows pivots hfuel hrow hmono hltcol hltcols hunit hzero + simp only [scalarRrefRowsLoop] + split + · rename_i hguard + have hguard' : cols ≤ col ∨ rows.size ≤ row := by + simpa [ge_iff_le, Bool.or_eq_true, decide_eq_true_eq] using hguard + refine ⟨hmono, hltcols, hunit, ?_⟩ + intro i hi k hk + have hi' : pivots.size ≤ i := hi + rcases hguard' with hcase | hcase + · exact hzero i (le_of_eq_of_le hrow hi') k (by omega) + · have hsize : rows.size ≤ i := by omega + rw [array_getD_of_le' rows #[] hsize] + exact array_getD_of_le' _ _ (by simp) + · rename_i hguard + have hguard' : col < cols ∧ row < rows.size := by + simpa [ge_iff_le, Bool.or_eq_true, decide_eq_true_eq, not_or, + not_le] using hguard + obtain ⟨hcol, hrowlt⟩ := hguard' + cases hfind : findScalarPivotRow rows row col with + | none => + apply ih (col + 1) row rows pivots (by omega) hrow hmono + (fun t ht ↦ Nat.lt_succ_of_lt (hltcol t ht)) hltcols hunit + intro i hi k hk + by_cases hkcol : k < col + · exact hzero i hi k hkcol + · have hk_eq : k = col := by omega + subst hk_eq + exact findScalarPivotRow_eq_none hfind i hi + | some p => + obtain ⟨hp_le, hp_lt, hp_ne⟩ := findScalarPivotRow_eq_some hfind + show ScalarRrefSpec cols (scalarRrefRowsLoop cols fuel (col + 1) + (row + 1) + (normalizeAndEliminateScalarRows (swapScalarRows rows p row) + row col) + (pivots.push col)) + have hgetD := swapScalarRows_getD rows hp_lt hrowlt + have hsize := swapScalarRows_size rows p row + generalize hS : swapScalarRows rows p row = S at hgetD hsize ⊢ + have hS_zero : ∀ i, row ≤ i → ∀ k, k < col → + (S.getD i #[]).getD k 0 = 0 := by + intro i hi k hk + rw [hgetD i] + by_cases hir : i = row + · rw [if_pos hir] + exact hzero p hp_le k hk + · rw [if_neg hir] + by_cases hip : i = p + · rw [if_pos hip] + exact hzero row (le_refl row) k hk + · rw [if_neg hip] + exact hzero i hi k hk + have hS_unit : ∀ t, t < pivots.size → ∀ i, + (S.getD i #[]).getD (pivots.getD t 0) 0 = + if i = t then 1 else 0 := by + intro t ht i + rw [hgetD i] + by_cases hir : i = row + · rw [if_pos hir, hzero p hp_le _ (hltcol t ht), + if_neg (by omega : ¬i = t)] + · rw [if_neg hir] + by_cases hip : i = p + · rw [if_pos hip, hzero row (le_refl row) _ (hltcol t ht)] + have hit : ¬i = t := by omega + rw [if_neg hit] + · rw [if_neg hip] + exact hunit t ht i + have hS_pivot : (S.getD row #[]).getD col 0 ≠ 0 := by + rw [hgetD row, if_pos rfl] + exact hp_ne + have hE := normalizeAndEliminateScalarRows_getD_entry S row col + (by omega) hS_pivot + apply ih (col + 1) (row + 1) _ (pivots.push col) (by omega) + · rw [Array.size_push] + omega + · intro s t hst ht + rw [Array.size_push] at ht + rw [array_getD_push, array_getD_push] + by_cases hts : t = pivots.size + · rw [if_pos hts, if_neg (by omega : ¬s = pivots.size)] + exact hltcol s (by omega) + · rw [if_neg hts, if_neg (by omega : ¬s = pivots.size)] + exact hmono s t hst (by omega) + · intro t ht + rw [Array.size_push] at ht + rw [array_getD_push] + by_cases hts : t = pivots.size + · rw [if_pos hts] + exact Nat.lt_succ_self col + · rw [if_neg hts] + exact Nat.lt_succ_of_lt (hltcol t (by omega)) + · intro t ht + rw [Array.size_push] at ht + rw [array_getD_push] + by_cases hts : t = pivots.size + · rw [if_pos hts] + exact hcol + · rw [if_neg hts] + exact hltcols t (by omega) + · intro t ht i + rw [Array.size_push] at ht + rw [array_getD_push] + by_cases hts : t = pivots.size + · rw [if_pos hts, hE i col] + by_cases hir : i = row + · rw [if_pos hir, div_self hS_pivot, + if_pos (by omega : i = t)] + · rw [if_neg hir, div_self hS_pivot, mul_one, sub_self, + if_neg (by omega : ¬i = t)] + · rw [if_neg hts] + have ht' : t < pivots.size := by omega + have hSrow_c : (S.getD row #[]).getD (pivots.getD t 0) 0 = 0 := by + rw [hgetD row, if_pos rfl] + exact hzero p hp_le _ (hltcol t ht') + rw [hE i (pivots.getD t 0)] + by_cases hir : i = row + · rw [if_pos hir, hSrow_c, zero_div, + if_neg (by omega : ¬i = t)] + · rw [if_neg hir, hSrow_c, zero_div, mul_zero, sub_zero] + exact hS_unit t ht' i + · intro i hi k hk + rw [hE i k, if_neg (by omega : ¬i = row)] + by_cases hkc : k < col + · rw [hS_zero i (by omega) k hkc, + hS_zero row (le_refl row) k hkc, zero_div, mul_zero, + sub_zero] + · have hk_eq : k = col := by omega + subst hk_eq + rw [div_self hS_pivot, mul_one, sub_self] + +/-- The row-array RREF driver satisfies the RREF shape contract. -/ +theorem scalarRrefRows_spec [Field F] [BEq F] [LawfulBEq F] + (rows : Array (Array F)) (cols : Nat) : + ScalarRrefSpec cols (scalarRrefRows rows cols) := by + unfold scalarRrefRows + apply scalarRrefRowsLoop_spec (cols + 1) 0 0 rows #[] (by omega) rfl + · intro s t _ ht + simp at ht + · intro t ht + simp at ht + · intro t ht + simp at ht + · intro t ht + simp at ht + · intro i _ k hk + omega + +end Forward + +/-! ## Kernel basis vectors -/ + +section Kernel + +variable {F : Type*} + +private theorem containsNat_false_getD_ne {xs : Array Nat} {x : Nat} + (h : DenseMatrix.containsNat xs x = false) : + ∀ t, t < xs.size → xs.getD t 0 ≠ x := by + intro t ht heq + have htrue : DenseMatrix.containsNat xs x = true := by + unfold DenseMatrix.containsNat + rw [Array.any_eq_true] + refine ⟨t, ht, ?_⟩ + rw [← array_getD_of_lt' xs 0 ht, heq] + exact beq_iff_eq.mpr rfl + rw [h] at htrue + exact Bool.false_ne_true htrue + +private theorem freeColumns_mem {cols : Nat} {pivots : Array Nat} {free : Nat} + (h : free ∈ (DenseMatrix.freeColumns cols pivots).toList) : + free < cols ∧ ∀ t, t < pivots.size → pivots.getD t 0 ≠ free := by + unfold DenseMatrix.freeColumns at h + rw [List.toList_toArray, List.mem_filter] at h + refine ⟨List.mem_range.mp h.1, ?_⟩ + have hfalse : DenseMatrix.containsNat pivots free = false := by + simpa using h.2 + exact containsNat_false_getD_ne hfalse + +private theorem pivotRowOfColumn?_some {pivots : Array Nat} {col t : Nat} + (h : DenseMatrix.pivotRowOfColumn? pivots col = some t) : + t < pivots.size ∧ pivots.getD t 0 = col := by + unfold DenseMatrix.pivotRowOfColumn? at h + have hmem := List.mem_of_find?_eq_some h + have hpred := List.find?_some h + exact ⟨List.mem_range.mp hmem, beq_iff_eq.mp hpred⟩ + +private theorem pivotRowOfColumn?_eq_some_of_mono {pivots : Array Nat} + (hmono : ∀ s t, s < t → t < pivots.size → + pivots.getD s 0 < pivots.getD t 0) + {t : Nat} (ht : t < pivots.size) : + DenseMatrix.pivotRowOfColumn? pivots (pivots.getD t 0) = some t := by + cases hfind : DenseMatrix.pivotRowOfColumn? pivots (pivots.getD t 0) with + | none => + exfalso + unfold DenseMatrix.pivotRowOfColumn? at hfind + exact List.find?_eq_none.mp hfind t (List.mem_range.mpr ht) + (beq_iff_eq.mpr rfl) + | some r => + obtain ⟨hr, hreq⟩ := pivotRowOfColumn?_some hfind + have hrt : r = t := by + rcases Nat.lt_trichotomy r t with hlt | heq | hgt + · exact absurd hreq (Nat.ne_of_lt (hmono r t hlt ht)) + · exact heq + · exact absurd hreq.symm (Nat.ne_of_lt (hmono t r hgt hr)) + rw [hrt] + +private theorem pivotRowOfColumn?_eq_none {pivots : Array Nat} {k : Nat} + (h : ∀ t, t < pivots.size → pivots.getD t 0 ≠ k) : + DenseMatrix.pivotRowOfColumn? pivots k = none := by + unfold DenseMatrix.pivotRowOfColumn? + rw [List.find?_eq_none] + intro x hx + simpa using h x (List.mem_range.mp hx) + +private theorem basisVectorForFreeColumnRows_size [Field F] + (rows : Array (Array F)) (pivots : Array Nat) (cols free : Nat) : + (basisVectorForFreeColumnRows rows pivots cols free).size = cols := by + unfold basisVectorForFreeColumnRows + exact Array.size_ofFn + +private theorem basisVectorForFreeColumnRows_getD_free [Field F] + (rows : Array (Array F)) (pivots : Array Nat) (cols : Nat) {free : Nat} + (hfree : free < cols) : + (basisVectorForFreeColumnRows rows pivots cols free).getD free 0 = 1 := by + unfold basisVectorForFreeColumnRows + rw [array_getD_of_lt' _ 0 (by rw [Array.size_ofFn]; exact hfree), + Array.getElem_ofFn] + dsimp only + rw [if_pos (beq_iff_eq.mpr rfl)] + +private theorem basisVectorForFreeColumnRows_getD_none [Field F] + (rows : Array (Array F)) (pivots : Array Nat) (cols free : Nat) {k : Nat} + (hk : k < cols) (hkf : ¬k = free) + (hnone : DenseMatrix.pivotRowOfColumn? pivots k = none) : + (basisVectorForFreeColumnRows rows pivots cols free).getD k 0 = 0 := by + unfold basisVectorForFreeColumnRows + rw [array_getD_of_lt' _ 0 (by rw [Array.size_ofFn]; exact hk), + Array.getElem_ofFn] + dsimp only + rw [if_neg (by simpa using hkf)] + simp only [hnone] + +private theorem basisVectorForFreeColumnRows_getD_some [Field F] + (rows : Array (Array F)) (pivots : Array Nat) (cols free : Nat) {k t : Nat} + (hk : k < cols) (hkf : ¬k = free) + (hsome : DenseMatrix.pivotRowOfColumn? pivots k = some t) : + (basisVectorForFreeColumnRows rows pivots cols free).getD k 0 = + -((rows.getD t #[]).getD free 0) := by + unfold basisVectorForFreeColumnRows + rw [array_getD_of_lt' _ 0 (by rw [Array.size_ofFn]; exact hk), + Array.getElem_ofFn] + dsimp only + rw [if_neg (by simpa using hkf)] + simp only [hsome] + +private theorem scalarDot_basisVector [Field F] [BEq F] [LawfulBEq F] + {cols : Nat} {R : ScalarRrefResult (F := F)} + (hspec : ScalarRrefSpec cols R) {free : Nat} (hfree : free < cols) + (hfreeNot : ∀ t, t < R.pivots.size → R.pivots.getD t 0 ≠ free) + (i : Nat) : + scalarDot cols (R.rows.getD i #[]) + (basisVectorForFreeColumnRows R.rows R.pivots cols free) = 0 := by + by_cases hi : i < R.pivots.size + · have hterm : ∀ k ∈ Finset.range cols, + (R.rows.getD i #[]).getD k 0 * + (basisVectorForFreeColumnRows R.rows R.pivots cols + free).getD k 0 = + (if k = free then (R.rows.getD i #[]).getD free 0 else 0) + + (if k = R.pivots.getD i 0 then + -((R.rows.getD i #[]).getD free 0) else 0) := by + intro k hk + have hk' : k < cols := Finset.mem_range.mp hk + by_cases hkf : k = free + · subst hkf + have hkp : ¬k = R.pivots.getD i 0 := fun h ↦ hfreeNot i hi h.symm + rw [basisVectorForFreeColumnRows_getD_free R.rows R.pivots cols hk', + if_pos rfl, if_neg hkp, mul_one, add_zero] + · rw [if_neg hkf, zero_add] + by_cases hkpiv : ∃ t, t < R.pivots.size ∧ R.pivots.getD t 0 = k + · obtain ⟨t, ht, htk⟩ := hkpiv + have hsome : DenseMatrix.pivotRowOfColumn? R.pivots k = some t := by + rw [← htk] + exact pivotRowOfColumn?_eq_some_of_mono hspec.mono ht + rw [basisVectorForFreeColumnRows_getD_some R.rows R.pivots cols + free hk' hkf hsome] + rw [← htk, hspec.unit t ht i] + by_cases hit : i = t + · subst hit + rw [if_pos rfl, if_pos rfl, one_mul] + · rw [if_neg hit, zero_mul] + have hne : ¬R.pivots.getD t 0 = R.pivots.getD i 0 := by + intro heq + rcases Nat.lt_trichotomy t i with hlt | heq' | hgt + · exact absurd heq (Nat.ne_of_lt (hspec.mono t i hlt hi)) + · exact hit heq'.symm + · exact absurd heq.symm (Nat.ne_of_lt (hspec.mono i t hgt ht)) + rw [if_neg hne] + · have hnone : DenseMatrix.pivotRowOfColumn? R.pivots k = none := + pivotRowOfColumn?_eq_none fun t ht heq ↦ hkpiv ⟨t, ht, heq⟩ + have hne : ¬k = R.pivots.getD i 0 := fun h ↦ hkpiv ⟨i, hi, h.symm⟩ + rw [basisVectorForFreeColumnRows_getD_none R.rows R.pivots cols + free hk' hkf hnone, mul_zero, if_neg hne] + unfold scalarDot + rw [Finset.sum_congr rfl hterm, Finset.sum_add_distrib, + Finset.sum_ite_eq_of_mem' (Finset.range cols) free _ + (Finset.mem_range.mpr hfree), + Finset.sum_ite_eq_of_mem' (Finset.range cols) (R.pivots.getD i 0) _ + (Finset.mem_range.mpr (hspec.pivots_lt i hi)), + add_neg_cancel] + · have hi' : R.pivots.size ≤ i := Nat.le_of_not_lt hi + unfold scalarDot + refine Finset.sum_eq_zero fun k hk ↦ ?_ + rw [hspec.tail_zero i hi' k (Finset.mem_range.mp hk), zero_mul] + +end Kernel + +/-! ## Main soundness theorems -/ + +section Main + +variable {F : Type*} + +/-- Every vector produced by `homogeneousKernelBasisRows rows cols` has +exactly `cols` stored coordinates. -/ +theorem homogeneousKernelBasisRows_size [Field F] [BEq F] [LawfulBEq F] + {rows : Array (Array F)} {cols : Nat} {v : Array F} + (hv : v ∈ (homogeneousKernelBasisRows rows cols).toList) : + v.size = cols := by + simp only [homogeneousKernelBasisRows] at hv + rw [Array.toList_map, List.mem_map] at hv + obtain ⟨free, _, rfl⟩ := hv + exact basisVectorForFreeColumnRows_size _ _ _ _ + +/-- **Soundness of the scalar kernel leaf.** Every vector produced by +`homogeneousKernelBasisRows rows cols` is orthogonal, over the coordinates +`0, …, cols - 1`, to every row of the input matrix. No width hypothesis on +the input rows is needed because the dot product only inspects the first +`cols` coordinates (with zero defaults). -/ +theorem homogeneousKernelBasisRows_dot_eq_zero [Field F] [BEq F] [LawfulBEq F] + {rows : Array (Array F)} {cols : Nat} {v : Array F} + (hv : v ∈ (homogeneousKernelBasisRows rows cols).toList) + {r : Array F} (hr : r ∈ rows.toList) : + ∑ k ∈ Finset.range cols, r.getD k 0 * v.getD k 0 = 0 := by + simp only [homogeneousKernelBasisRows] at hv + rw [Array.toList_map, List.mem_map] at hv + obtain ⟨free, hfree_mem, rfl⟩ := hv + obtain ⟨hfree_lt, hfree_not⟩ := freeColumns_mem hfree_mem + have hspec := scalarRrefRows_spec rows cols + have horthR : OrthRows cols (scalarRrefRows rows cols).rows + (basisVectorForFreeColumnRows (scalarRrefRows rows cols).rows + (scalarRrefRows rows cols).pivots cols free) := + fun i ↦ scalarDot_basisVector hspec hfree_lt hfree_not i + have horth := orthRows_of_scalarRrefRows horthR + obtain ⟨j, hj, hrj⟩ := List.mem_iff_getElem.mp hr + have hj' : j < rows.size := by simpa using hj + have hr_eq : rows.getD j #[] = r := by + rw [array_getD_of_lt' rows #[] hj', ← Array.getElem_toList hj'] + exact hrj + have hdot := horth j + rw [hr_eq] at hdot + exact hdot + +/-! ### Forward span: row operations preserve orthogonality -/ + +private theorem orthRows_elimStep_forward [Field F] [BEq F] + {cols pivotRow pivotCol : Nat} {pivotVector v : Array F} + {rows : Array (Array F)} {a : Nat} + (hpv : scalarDot cols pivotVector v = 0) + (h : OrthRows cols rows v) : + OrthRows cols (elimStep pivotRow pivotVector pivotCol rows a) v := by + unfold elimStep + by_cases hap : (a == pivotRow) = true + · rw [if_pos hap] + exact h + · rw [if_neg hap] + by_cases hfac : (-((rows.getD a #[]).getD pivotCol 0) == 0) = true + · rw [if_pos hfac] + exact h + · rw [if_neg hfac] + intro i + rw [array_getD_setIfInBounds] + by_cases hcond : a = i ∧ a < rows.size + · rw [if_pos hcond, scalarDot_addScaledScalarRow, h a, hpv, mul_zero, + add_zero] + · rw [if_neg hcond] + exact h i + +private theorem orthRows_foldl_elimStep_forward [Field F] [BEq F] + {cols pivotRow pivotCol : Nat} {pivotVector v : Array F} + (hpv : scalarDot cols pivotVector v = 0) : + ∀ (l : List Nat) (acc : Array (Array F)), + OrthRows cols acc v → + OrthRows cols (l.foldl (elimStep pivotRow pivotVector pivotCol) acc) + v := by + intro l + induction l with + | nil => + intro acc h + exact h + | cons a l ih => + intro acc h + rw [List.foldl_cons] + exact ih _ (orthRows_elimStep_forward hpv h) + +private theorem orthRows_normalizeAndEliminate_forward [Field F] [BEq F] + [LawfulBEq F] {cols : Nat} {rows : Array (Array F)} + {pivotRow pivotCol : Nat} {v : Array F} (h : OrthRows cols rows v) : + OrthRows cols (normalizeAndEliminateScalarRows rows pivotRow pivotCol) + v := by + by_cases hpivot : (rows.getD pivotRow #[]).getD pivotCol 0 = 0 + · unfold normalizeAndEliminateScalarRows + rw [if_pos (by simpa using hpivot)] + exact h + · rw [normalizeAndEliminateScalarRows_eq_foldl rows pivotRow pivotCol hpivot] + have hpv : scalarDot cols + (normalizeScalarRow (rows.getD pivotRow #[]) pivotCol) v = 0 := by + have h0 := h pivotRow + rw [scalarDot_eq_pivot_mul_normalize cols _ v pivotCol hpivot] at h0 + exact (mul_eq_zero.mp h0).resolve_left hpivot + refine orthRows_foldl_elimStep_forward hpv _ _ ?_ + intro i + rw [array_getD_setIfInBounds] + by_cases hcond : pivotRow = i ∧ pivotRow < rows.size + · rw [if_pos hcond] + exact hpv + · rw [if_neg hcond] + exact h i + +private theorem orthRows_swapScalarRows_forward [Field F] {cols : Nat} + {rows : Array (Array F)} {a b : Nat} (ha : a < rows.size) + (hb : b < rows.size) {v : Array F} (h : OrthRows cols rows v) : + OrthRows cols (swapScalarRows rows a b) v := by + intro i + rw [swapScalarRows_getD rows ha hb] + by_cases hib : i = b + · rw [if_pos hib] + exact h a + · rw [if_neg hib] + by_cases hia : i = a + · rw [if_pos hia] + exact h b + · rw [if_neg hia] + exact h i + +private theorem orthRows_scalarRrefRowsLoop_forward [Field F] [BEq F] + [LawfulBEq F] {cols : Nat} {v : Array F} : + ∀ (fuel col row : Nat) (rows : Array (Array F)) (pivots : Array Nat), + OrthRows cols rows v → + OrthRows cols (scalarRrefRowsLoop cols fuel col row rows pivots).rows + v := by + intro fuel + induction fuel with + | zero => + intro col row rows pivots h + exact h + | succ fuel ih => + intro col row rows pivots h + simp only [scalarRrefRowsLoop] + split + · exact h + · rename_i hguard + have hrow : row < rows.size := by + simp only [ge_iff_le, Bool.or_eq_true, decide_eq_true_eq, + not_or, not_le] at hguard + exact hguard.2 + cases hfind : findScalarPivotRow rows row col with + | none => exact ih _ _ _ _ h + | some p => + obtain ⟨_, hp_lt, _⟩ := findScalarPivotRow_eq_some hfind + exact ih _ _ _ _ + (orthRows_normalizeAndEliminate_forward + (orthRows_swapScalarRows_forward hp_lt hrow h)) + +private theorem orthRows_scalarRrefRows_forward [Field F] [BEq F] [LawfulBEq F] + {cols : Nat} {rows : Array (Array F)} {v : Array F} + (h : OrthRows cols rows v) : + OrthRows cols (scalarRrefRows rows cols).rows v := + orthRows_scalarRrefRowsLoop_forward _ _ _ _ _ h + +/-! ### Free-column bookkeeping for the completeness theorem -/ + +private theorem freeColumns_getD_mem {cols : Nat} {pivots : Array Nat} + {i : Nat} (hi : i < (DenseMatrix.freeColumns cols pivots).size) : + (DenseMatrix.freeColumns cols pivots).getD i 0 ∈ + (DenseMatrix.freeColumns cols pivots).toList := by + rw [array_getD_of_lt' _ 0 hi, ← Array.getElem_toList (by simpa using hi)] + exact List.getElem_mem _ + +private theorem freeColumns_getD_inj {cols : Nat} {pivots : Array Nat} + {i j : Nat} (hi : i < (DenseMatrix.freeColumns cols pivots).size) + (hj : j < (DenseMatrix.freeColumns cols pivots).size) + (h : (DenseMatrix.freeColumns cols pivots).getD i 0 = + (DenseMatrix.freeColumns cols pivots).getD j 0) : + i = j := by + rw [array_getD_of_lt' _ 0 hi, array_getD_of_lt' _ 0 hj] at h + unfold DenseMatrix.freeColumns at hi hj h + rw [List.getElem_toArray, List.getElem_toArray] at h + have hnodup : ((List.range cols).filter + fun col ↦ !(DenseMatrix.containsNat pivots col)).Nodup := + List.Nodup.filter _ List.nodup_range + exact (List.Nodup.getElem_inj_iff hnodup).mp h + +private theorem containsNat_true_exists {xs : Array Nat} {x : Nat} + (h : DenseMatrix.containsNat xs x = true) : + ∃ t, t < xs.size ∧ xs.getD t 0 = x := by + unfold DenseMatrix.containsNat at h + rw [Array.any_eq_true] at h + obtain ⟨t, ht, hbeq⟩ := h + refine ⟨t, ht, ?_⟩ + rw [array_getD_of_lt' xs 0 ht] + exact beq_iff_eq.mp hbeq + +private theorem freeColumns_or_pivot {cols : Nat} {pivots : Array Nat} + {k : Nat} (hk : k < cols) : + (∃ j, j < (DenseMatrix.freeColumns cols pivots).size ∧ + (DenseMatrix.freeColumns cols pivots).getD j 0 = k) ∨ + ∃ t, t < pivots.size ∧ pivots.getD t 0 = k := by + cases hc : DenseMatrix.containsNat pivots k with + | true => exact Or.inr (containsNat_true_exists hc) + | false => + left + have hmem : k ∈ (DenseMatrix.freeColumns cols pivots).toList := by + unfold DenseMatrix.freeColumns + rw [List.toList_toArray, List.mem_filter] + refine ⟨List.mem_range.mpr hk, ?_⟩ + rw [hc] + rfl + obtain ⟨j, hj, hjk⟩ := List.mem_iff_getElem.mp hmem + have hj' : j < (DenseMatrix.freeColumns cols pivots).size := by + simpa using hj + refine ⟨j, hj', ?_⟩ + rw [array_getD_of_lt' _ 0 hj', ← Array.getElem_toList hj] + exact hjk + +private theorem basisVector_getD_freeColumn [Field F] + (rows : Array (Array F)) (pivots : Array Nat) (cols : Nat) {i j : Nat} + (hi : i < (DenseMatrix.freeColumns cols pivots).size) + (hj : j < (DenseMatrix.freeColumns cols pivots).size) : + (basisVectorForFreeColumnRows rows pivots cols + ((DenseMatrix.freeColumns cols pivots).getD i 0)).getD + ((DenseMatrix.freeColumns cols pivots).getD j 0) 0 = + if j = i then 1 else 0 := by + obtain ⟨hjlt, hjnot⟩ := freeColumns_mem (freeColumns_getD_mem hj) + by_cases hij : j = i + · rw [if_pos hij, hij] + exact basisVectorForFreeColumnRows_getD_free rows pivots cols + (freeColumns_mem (freeColumns_getD_mem hi)).1 + · rw [if_neg hij] + exact basisVectorForFreeColumnRows_getD_none rows pivots cols _ hjlt + (fun hc ↦ hij (freeColumns_getD_inj hj hi hc)) + (pivotRowOfColumn?_eq_none hjnot) + +/-- Coordinatewise completeness of the kernel basis vectors against a fixed +RREF result: any vector orthogonal to the RREF rows agrees, below `cols`, +with the combination of basis vectors whose coefficients are its values at +the free columns. -/ +private theorem kernelBasis_complete_aux [Field F] [BEq F] [LawfulBEq F] + {cols : Nat} {R : ScalarRrefResult (F := F)} + (hspec : ScalarRrefSpec cols R) {v : Array F} + (horthR : OrthRows cols R.rows v) : + ∀ k, k < cols → + v.getD k 0 = + ∑ i ∈ Finset.range (DenseMatrix.freeColumns cols R.pivots).size, + v.getD ((DenseMatrix.freeColumns cols R.pivots).getD i 0) 0 * + (basisVectorForFreeColumnRows R.rows R.pivots cols + ((DenseMatrix.freeColumns cols R.pivots).getD i 0)).getD k 0 := by + have hfree_coord : ∀ j, j < (DenseMatrix.freeColumns cols R.pivots).size → + v.getD ((DenseMatrix.freeColumns cols R.pivots).getD j 0) 0 = + ∑ i ∈ Finset.range (DenseMatrix.freeColumns cols R.pivots).size, + v.getD ((DenseMatrix.freeColumns cols R.pivots).getD i 0) 0 * + (basisVectorForFreeColumnRows R.rows R.pivots cols + ((DenseMatrix.freeColumns cols R.pivots).getD i 0)).getD + ((DenseMatrix.freeColumns cols R.pivots).getD j 0) 0 := by + intro j hj + have h0 : ∀ i ∈ Finset.range (DenseMatrix.freeColumns cols R.pivots).size, + i ≠ j → + v.getD ((DenseMatrix.freeColumns cols R.pivots).getD i 0) 0 * + (basisVectorForFreeColumnRows R.rows R.pivots cols + ((DenseMatrix.freeColumns cols R.pivots).getD i 0)).getD + ((DenseMatrix.freeColumns cols R.pivots).getD j 0) 0 = 0 := by + intro i hi hij + rw [basisVector_getD_freeColumn R.rows R.pivots cols + (Finset.mem_range.mp hi) hj, if_neg (fun hc ↦ hij hc.symm), mul_zero] + rw [Finset.sum_eq_single_of_mem j (Finset.mem_range.mpr hj) h0, + basisVector_getD_freeColumn R.rows R.pivots cols hj hj, if_pos rfl, + mul_one] + intro k hk + rcases freeColumns_or_pivot (pivots := R.pivots) hk with + ⟨j, hj, hjk⟩ | ⟨t, ht, htk⟩ + · rw [← hjk] + exact hfree_coord j hj + · have hrow_v := horthR t + unfold scalarDot at hrow_v + have hrow_b : ∀ i, i < (DenseMatrix.freeColumns cols R.pivots).size → + ∑ k' ∈ Finset.range cols, (R.rows.getD t #[]).getD k' 0 * + (basisVectorForFreeColumnRows R.rows R.pivots cols + ((DenseMatrix.freeColumns cols R.pivots).getD i 0)).getD k' 0 = + 0 := by + intro i hi + obtain ⟨hilt, hinot⟩ := freeColumns_mem (freeColumns_getD_mem hi) + have hb := scalarDot_basisVector hspec hilt hinot t + unfold scalarDot at hb + exact hb + have hsum : ∑ k' ∈ Finset.range cols, + (R.rows.getD t #[]).getD k' 0 * + (v.getD k' 0 - + ∑ i ∈ Finset.range (DenseMatrix.freeColumns cols R.pivots).size, + v.getD ((DenseMatrix.freeColumns cols R.pivots).getD i 0) 0 * + (basisVectorForFreeColumnRows R.rows R.pivots cols + ((DenseMatrix.freeColumns cols R.pivots).getD i 0)).getD + k' 0) = 0 := by + simp only [mul_sub] + rw [Finset.sum_sub_distrib, hrow_v, zero_sub, neg_eq_zero] + simp only [Finset.mul_sum] + rw [Finset.sum_comm] + refine Finset.sum_eq_zero fun i hi ↦ ?_ + have hswap : ∀ k' : Nat, + (R.rows.getD t #[]).getD k' 0 * + (v.getD ((DenseMatrix.freeColumns cols R.pivots).getD i 0) 0 * + (basisVectorForFreeColumnRows R.rows R.pivots cols + ((DenseMatrix.freeColumns cols R.pivots).getD i 0)).getD + k' 0) = + v.getD ((DenseMatrix.freeColumns cols R.pivots).getD i 0) 0 * + ((R.rows.getD t #[]).getD k' 0 * + (basisVectorForFreeColumnRows R.rows R.pivots cols + ((DenseMatrix.freeColumns cols R.pivots).getD i 0)).getD + k' 0) := + fun k' ↦ mul_left_comm _ _ _ + rw [Finset.sum_congr rfl fun k' _ ↦ hswap k', ← Finset.mul_sum, + hrow_b i (Finset.mem_range.mp hi), mul_zero] + have hothers : ∀ k' ∈ Finset.range cols, k' ≠ R.pivots.getD t 0 → + (R.rows.getD t #[]).getD k' 0 * + (v.getD k' 0 - + ∑ i ∈ Finset.range (DenseMatrix.freeColumns cols R.pivots).size, + v.getD ((DenseMatrix.freeColumns cols R.pivots).getD i 0) 0 * + (basisVectorForFreeColumnRows R.rows R.pivots cols + ((DenseMatrix.freeColumns cols R.pivots).getD i 0)).getD + k' 0) = 0 := by + intro k' hk' hne + rcases freeColumns_or_pivot (pivots := R.pivots) + (Finset.mem_range.mp hk') with ⟨j, hj, hjk'⟩ | ⟨s, hs, hsk'⟩ + · rw [← hjk', ← hfree_coord j hj, sub_self, mul_zero] + · have hst : ¬t = s := fun hc ↦ hne (by rw [hc]; exact hsk'.symm) + rw [← hsk', hspec.unit s hs t, if_neg hst, zero_mul] + rw [Finset.sum_eq_single_of_mem (R.pivots.getD t 0) + (Finset.mem_range.mpr (hspec.pivots_lt t ht)) hothers, + hspec.unit t ht t, if_pos rfl, one_mul, sub_eq_zero] at hsum + rw [← htk] + exact hsum + +private theorem homogeneousKernelBasisRows_size_eq [Field F] [BEq F] + [LawfulBEq F] (rows : Array (Array F)) (cols : Nat) : + (homogeneousKernelBasisRows rows cols).size = + (DenseMatrix.freeColumns cols (scalarRrefRows rows cols).pivots).size := by + simp only [homogeneousKernelBasisRows, Array.size_map] + +private theorem homogeneousKernelBasisRows_getD_eq [Field F] [BEq F] + [LawfulBEq F] (rows : Array (Array F)) (cols : Nat) {i : Nat} + (hi : i < + (DenseMatrix.freeColumns cols (scalarRrefRows rows cols).pivots).size) : + (homogeneousKernelBasisRows rows cols).getD i #[] = + basisVectorForFreeColumnRows (scalarRrefRows rows cols).rows + (scalarRrefRows rows cols).pivots cols + ((DenseMatrix.freeColumns cols + (scalarRrefRows rows cols).pivots).getD i 0) := by + simp only [homogeneousKernelBasisRows] + rw [array_getD_of_lt' _ #[] (by rw [Array.size_map]; exact hi), + Array.getElem_map, array_getD_of_lt' _ 0 hi] + +/-- **Completeness of the scalar kernel leaf.** Every vector orthogonal to +all input rows over the coordinates `0, …, cols - 1` is, coordinatewise below +`cols`, the `F`-linear combination of the emitted kernel basis vectors whose +coefficients are the values of the vector at the corresponding free +columns. -/ +theorem homogeneousKernelBasisRows_complete [Field F] [BEq F] [LawfulBEq F] + (rows : Array (Array F)) (cols : Nat) {v : Array F} + (hv : ∀ r ∈ rows.toList, + ∑ k ∈ Finset.range cols, r.getD k 0 * v.getD k 0 = 0) : + ∀ k, k < cols → + v.getD k 0 = + ∑ i ∈ Finset.range (homogeneousKernelBasisRows rows cols).size, + v.getD ((DenseMatrix.freeColumns cols + (scalarRrefRows rows cols).pivots).getD i 0) 0 * + ((homogeneousKernelBasisRows rows cols).getD i #[]).getD k 0 := by + have horth : OrthRows cols rows v := by + intro i + by_cases hi : i < rows.size + · have hmem : rows[i] ∈ rows.toList := by + rw [← Array.getElem_toList (by simpa using hi)] + exact List.getElem_mem _ + have h0 := hv rows[i] hmem + unfold scalarDot + rw [array_getD_of_lt' rows #[] hi] + exact h0 + · rw [array_getD_of_le' rows #[] (Nat.le_of_not_lt hi)] + exact scalarDot_empty cols v + have hmain := kernelBasis_complete_aux (scalarRrefRows_spec rows cols) + (orthRows_scalarRrefRows_forward horth) + intro k hk + rw [homogeneousKernelBasisRows_size_eq rows cols, hmain k hk] + refine Finset.sum_congr rfl fun i hi ↦ ?_ + rw [homogeneousKernelBasisRows_getD_eq rows cols (Finset.mem_range.mp hi)] + +end Main + +end Approximant + +end PolynomialMatrix + +end CompPoly diff --git a/CompPoly/LinearAlgebra/PolynomialMatrix/Approximant/PMBasis/KernelLeafSoundness.lean b/CompPoly/LinearAlgebra/PolynomialMatrix/Approximant/PMBasis/KernelLeafSoundness.lean new file mode 100644 index 00000000..e892f352 --- /dev/null +++ b/CompPoly/LinearAlgebra/PolynomialMatrix/Approximant/PMBasis/KernelLeafSoundness.lean @@ -0,0 +1,621 @@ +/- +Copyright (c) 2026 CompPoly Contributors. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Valerii Huhnin +-/ + +module + +-- `coeff`, `ofArray` and friends are declared in bare `public section`s, so their +-- bodies are opaque downstream; see `docs/wiki/module-system.md`. +import all CompPoly.Univariate.Basic +import all CompPoly.Univariate.Raw.Core +public import CompPoly.LinearAlgebra.PolynomialMatrix.Approximant.PMBasis.XAdicSoundness +public import CompPoly.LinearAlgebra.PolynomialMatrix.Approximant.PMBasis.KernelLeafScalar + +/-! +# Kernel-Leaf Basis Soundness + +Every row of the kernel-leaf basis satisfies the X-adic approximant +conditions and has the principal row width: kernel vectors reconstruct to +solutions, and the reduction, completion, and compaction steps preserve +soundness. +-/ + +@[expose] public section + +namespace CompPoly + +namespace PolynomialMatrix + +namespace Approximant + +variable {F : Type*} [Field F] [BEq F] [LawfulBEq F] + +/-! ## Kernel-leaf basis soundness -/ + +/-- Coefficient-shift row scaling preserves the row size. -/ +theorem rowScaleCoeffX_size (c : F) (d : Nat) (row : PolynomialRow F) : + (rowScaleCoeffX c d row).size = row.size := by + simp [rowScaleCoeffX] + +private theorem insertKernelLeafPivotRowWithFuel_invariant + (Q : PolynomialRow F → Prop) + (hclosed : ∀ (target reducer : PolynomialRow F) (shift : Array Nat), + Q target → Q reducer → Q (cancelKernelLeafLeadingTerm target reducer shift)) : + ∀ (fuel : Nat) (pivots : Array (Option (PolynomialRow F))) + (shift : Array Nat) (row : PolynomialRow F), + (∀ p r, pivots.getD p none = some r → Q r) → Q row → + ∀ p r, + (insertKernelLeafPivotRowWithFuel fuel pivots shift row).getD p none = + some r → Q r := by + intro fuel + induction fuel with + | zero => + intro pivots shift row hpivots _hrow p r hget + exact hpivots p r hget + | succ fuel ih => + intro pivots shift row hpivots hrow p r hget + rw [insertKernelLeafPivotRowWithFuel] at hget + have hset : ∀ (position : Nat) (newRow : PolynomialRow F), + Q newRow → + ∀ p' r', + (pivots.setIfInBounds position (some newRow)).getD p' none = some r' → + Q r' := by + intro position newRow hnew p' r' hget' + by_cases hpos : p' = position ∧ position < pivots.size + · rcases hpos with ⟨hp, hlt⟩ + subst hp + rw [Array.getD_eq_getD_getElem?, + Array.getElem?_setIfInBounds_self_of_lt hlt, Option.getD_some] at hget' + cases hget' + exact hnew + · rcases Nat.lt_or_ge p' pivots.size with hplt | hpge + · have hne : p' ≠ position := fun hcontra ↦ hpos ⟨hcontra, hcontra ▸ hplt⟩ + rw [Array.getD_eq_getD_getElem?, + Array.getElem?_setIfInBounds_ne (by omega)] at hget' + exact hpivots p' r' (by rw [Array.getD_eq_getD_getElem?]; exact hget') + · rw [Array.getD_eq_getD_getElem?, Array.getElem?_eq_none + (by simpa using hpge)] at hget' + cases hget' + cases hterm : rowShiftedLeadingTerm? row shift with + | none => + simp only [hterm] at hget + exact hpivots p r hget + | some target => + simp only [hterm] at hget + cases hpivot : pivots.getD target.position none with + | none => + simp only [hpivot] at hget + exact hset target.position row hrow p r hget + | some pivot => + simp only [hpivot] at hget + cases hpterm : rowShiftedLeadingTerm? pivot shift with + | none => + simp only [hpterm] at hget + exact hset target.position row hrow p r hget + | some reducer => + simp only [hpterm] at hget + have hpivotrow : Q pivot := hpivots target.position pivot hpivot + split at hget + · exact ih (pivots.setIfInBounds target.position (some row)) shift + (cancelKernelLeafLeadingTerm pivot row shift) + (fun p' r' hget' ↦ hset target.position row hrow p' r' hget') + (hclosed _ _ _ hpivotrow hrow) p r hget + · exact ih pivots shift + (cancelKernelLeafLeadingTerm row pivot shift) hpivots + (hclosed _ _ _ hrow hpivotrow) p r hget + +omit [BEq F] [LawfulBEq F] in +/-- Any predicate holding for all stored pivot rows holds for all extracted rows. -/ +theorem pivotRows_invariant (Q : PolynomialRow F → Prop) + {pivots : Array (Option (PolynomialRow F))} + (hpivots : ∀ p r, pivots.getD p none = some r → Q r) + {row : PolynomialRow F} (hrow : row ∈ MatrixRows (pivotRows pivots)) : + Q row := by + rw [MatrixRows, pivotRows, List.toList_toArray] at hrow + rcases List.mem_filterMap.mp hrow with ⟨entry, hentry, hid⟩ + rcases List.getElem_of_mem hentry with ⟨p, hp, hget⟩ + refine hpivots p row ?_ + rw [Array.getD_eq_getD_getElem?, Array.getElem?_eq_getElem (by simpa using hp), + Option.getD_some] + rw [show pivots[p] = pivots.toList[p] from by rw [Array.getElem_toList], hget] + exact hid + +/-- Any predicate closed under leading-term cancellation is preserved by the +pivot-table reduction. -/ +theorem reduceKernelLeafRowsByPivots_invariant (Q : PolynomialRow F → Prop) + (hclosed : ∀ (target reducer : PolynomialRow F) (shift : Array Nat), + Q target → Q reducer → Q (cancelKernelLeafLeadingTerm target reducer shift)) + {rows : PolynomialMatrix F} (shift : Array Nat) + (hrows : ∀ row ∈ MatrixRows rows, Q row) + {row : PolynomialRow F} + (hrow : row ∈ MatrixRows (reduceKernelLeafRowsByPivots rows shift)) : + Q row := by + rw [reduceKernelLeafRowsByPivots] at hrow + refine pivotRows_invariant Q ?_ hrow + have hfold : ∀ (l : List (PolynomialRow F)) + (pivots : Array (Option (PolynomialRow F))), + (∀ r ∈ l, Q r) → + (∀ p r, pivots.getD p none = some r → Q r) → + ∀ p r, + (l.foldl (fun pivots row ↦ + insertKernelLeafPivotRowWithFuel (reduceKernelLeafFuel rows shift) + pivots shift row) pivots).getD p none = some r → Q r := by + intro l + induction l with + | nil => + intro pivots _hl hpivots p r hget + exact hpivots p r hget + | cons head tail ih => + intro pivots hl hpivots p r hget + rw [List.foldl_cons] at hget + refine ih _ (fun r hr ↦ hl r (List.mem_cons_of_mem head hr)) ?_ p r hget + intro p' r' hget' + exact insertKernelLeafPivotRowWithFuel_invariant Q hclosed + (reduceKernelLeafFuel rows shift) pivots shift head hpivots + (hl head List.mem_cons_self) p' r' hget' + rw [← Array.foldl_toList] + refine hfold rows.toList _ (fun r hr ↦ hrows r hr) ?_ + intro p r hget + rw [Array.getD_eq_getD_getElem?] at hget + rcases Nat.lt_or_ge p (Array.replicate (MatrixWidth rows) + (none : Option (PolynomialRow F))).size with hp | hp + · rw [Array.getElem?_eq_getElem hp, Option.getD_some] at hget + rw [Array.getElem_replicate] at hget + cases hget + · rw [Array.getElem?_eq_none hp] at hget + cases hget + +omit [BEq F] [LawfulBEq F] in +/-- A row of `rows.push row` is either a row of `rows` or `row` itself. -/ +theorem mem_matrixRows_push {rows : PolynomialMatrix F} + {row r : PolynomialRow F} (hr : r ∈ MatrixRows (rows.push row)) : + r ∈ MatrixRows rows ∨ r = row := by + rw [MatrixRows, Array.toList_push, List.mem_append] at hr + rcases hr with hr | hr + · exact Or.inl hr + · exact Or.inr (by simpa using hr) + +/-- Any predicate closed under leading-term cancellation is preserved by the +incremental pivot reduction. -/ +theorem reduceKernelLeafRowsIncremental_invariant + (Q : PolynomialRow F → Prop) + (hclosed : ∀ (target reducer : PolynomialRow F) (shift : Array Nat), + Q target → Q reducer → Q (cancelKernelLeafLeadingTerm target reducer shift)) + {rows : PolynomialMatrix F} (shift : Array Nat) + (hrows : ∀ row ∈ MatrixRows rows, Q row) + {row : PolynomialRow F} + (hrow : row ∈ MatrixRows (reduceKernelLeafRowsIncremental rows shift)) : + Q row := by + rw [reduceKernelLeafRowsIncremental, ← Array.foldl_toList] at hrow + have hfold : ∀ (l : List (PolynomialRow F)) (basis : PolynomialMatrix F), + (∀ r ∈ l, Q r) → + (∀ r ∈ MatrixRows basis, Q r) → + ∀ r ∈ MatrixRows (l.foldl + (fun basis row ↦ insertKernelLeafRowIncremental basis shift row) basis), + Q r := by + intro l + induction l with + | nil => + intro basis _hl hbasis r hr + exact hbasis r hr + | cons head tail ih => + intro basis hl hbasis r hr + rw [List.foldl_cons] at hr + refine ih _ (fun x hx ↦ hl x (List.mem_cons_of_mem head hx)) ?_ r hr + intro x hx + rw [insertKernelLeafRowIncremental] at hx + have hx' : x ∈ MatrixRows (reduceKernelLeafRows (basis.push head) shift) := by + rw [MatrixRows] at hx ⊢ + have hmem : x ∈ reduceKernelLeafRows (basis.push head) shift ∧ + rowIsZero x = false := by + simpa using hx + simpa using hmem.1 + rw [reduceKernelLeafRows] at hx' + refine reduceKernelLeafRowsByPivots_invariant Q hclosed shift ?_ hx' + intro y hy + rcases mem_matrixRows_push hy with hy | rfl + · exact hbasis y hy + · exact hl _ List.mem_cons_self + exact hfold rows.toList #[] (fun r hr ↦ hrows r hr) + (by intro r hr; simp [MatrixRows] at hr) row hrow + +private theorem pm_foldl_push_toList {α β : Type*} (g : α → β) : + ∀ (l : List α) (acc : Array β), + (l.foldl (fun out x ↦ out.push (g x)) acc).toList = acc.toList ++ l.map g := by + intro l + induction l with + | nil => intro acc; simp + | cons head tail ih => + intro acc + rw [List.foldl_cons, ih, Array.toList_push, List.map_cons, + List.append_assoc, List.singleton_append] + +private theorem pm_doubleFoldl_toList (orders : Array Nat) : + ∀ (l : List Nat) (acc : Array (Nat × Nat)), + (l.foldl (fun out j ↦ (List.range (orders.getD j 0)).foldl + (fun out t ↦ out.push (j, t)) out) acc).toList = + acc.toList ++ l.flatMap + (fun j ↦ (List.range (orders.getD j 0)).map (fun t ↦ (j, t))) := by + intro l + induction l with + | nil => intro acc; simp + | cons head tail ih => + intro acc + rw [List.foldl_cons, ih, pm_foldl_push_toList, List.flatMap_cons, + List.append_assoc] + +/-- Every in-range equation index appears in `coefficientEquationIndices`. -/ +private theorem mem_coefficientEquationIndices {orders : Array Nat} {j t : Nat} + (hj : j < orders.size) (ht : t < orders.getD j 0) : + (j, t) ∈ (coefficientEquationIndices orders).toList := by + rw [coefficientEquationIndices] + simp only [Std.Legacy.Range.forIn_eq_forIn_range', Std.Legacy.Range.size, + Nat.sub_zero, Nat.add_sub_cancel, Nat.div_one, + List.forIn_pure_yield_eq_foldl, bind_pure_comp, map_pure, bind_pure, + Id.run_pure] + have hrange : ∀ n : Nat, List.range' 0 n = List.range n := fun n ↦ + (List.range_eq_range' (n := n)).symm + simp only [hrange] + rw [pm_doubleFoldl_toList] + simp only [List.nil_append] + exact List.mem_flatMap.mpr ⟨j, List.mem_range.mpr hj, + List.mem_map.mpr ⟨t, List.mem_range.mpr ht, rfl⟩⟩ + +private theorem pm_sum_range_mul {M : Type*} [AddCommMonoid M] (cap : Nat) + (f : Nat → M) : + ∀ n : Nat, + ∑ col ∈ Finset.range (n * cap), f col = + ∑ k ∈ Finset.range n, ∑ a ∈ Finset.range cap, f (k * cap + a) := by + intro n + induction n with + | zero => simp + | succ n ih => + rw [Nat.succ_mul, Finset.sum_range_add, ih, Finset.sum_range_succ] + +omit [BEq F] [LawfulBEq F] in +/-- In-range entries of a scalar coefficient row. -/ +private theorem coefficientMatrixRow_getD (problem : XAdicProblem F) + (degreeCap : Nat) (equation : Nat × Nat) {col : Nat} + (hcol : col < problem.matrix.size * degreeCap) : + (coefficientMatrixRow problem degreeCap equation).getD col 0 = + if col % degreeCap ≤ equation.2 then + CPolynomial.coeff + (rowGet (problem.matrix.getD (col / degreeCap) #[]) equation.1) + (equation.2 - col % degreeCap) + else 0 := by + rw [coefficientMatrixRow, Array.getD_eq_getD_getElem?, List.getElem?_toArray, + List.getElem?_map, List.getElem?_range hcol, Option.map_some, Option.getD_some] + +/-- Coefficients of polynomial rows reconstructed from scalar vectors. -/ +theorem rowGet_vectorToPolynomialRow_coeff (cap width : Nat) + (v : Array F) (k a : Nat) : + CPolynomial.coeff (rowGet (vectorToPolynomialRow cap width v) k) a = + if k < width ∧ a < cap then v.getD (k * cap + a) 0 else 0 := by + rw [vectorToPolynomialRow, rowGet] + rcases Nat.lt_or_ge k width with hk | hk + · rw [Array.getD_eq_getD_getElem?, List.getElem?_toArray, List.getElem?_map, + List.getElem?_range hk, Option.map_some, Option.getD_some, + CPolynomial.coeff_ofArray] + rcases Nat.lt_or_ge a cap with ha | ha + · rw [if_pos ⟨hk, ha⟩, Array.getD_eq_getD_getElem?, List.getElem?_toArray, + List.getElem?_map, List.getElem?_range ha, Option.map_some, + Option.getD_some] + · rw [if_neg (by omega), Array.getD_eq_getD_getElem?, List.getElem?_toArray, + List.getElem?_eq_none (by simpa using ha), Option.getD_none] + · rw [Array.getD_eq_getD_getElem?, List.getElem?_toArray, + List.getElem?_eq_none (by simpa using hk), Option.getD_none, + if_neg (by omega)] + exact CPolynomial.coeff_zero a + +/-- Polynomial rows reconstructed from scalar kernel vectors satisfy the +X-adic conditions of the bounded leaf problem. -/ +theorem vectorToPolynomialRow_approximates (mulCtx : CPolynomial.MulContext F) + (problem : XAdicProblem F) {v : Array F} + (hv : v ∈ (homogeneousKernelBasisRows (coefficientMatrixRows problem) + (problem.matrix.size * leafDegreeCap problem)).toList) : + RowApproximates mulCtx problem + (vectorToPolynomialRow (leafDegreeCap problem) problem.matrix.size v) := by + rw [rowApproximates_iff] + intro j hj _hjw + rw [Polynomial.X_pow_dvd_iff] + intro t ht + have hcap_pos : 0 < leafDegreeCap problem := le_max_left 1 _ + have htcap : t < leafDegreeCap problem := + lt_of_lt_of_le ht (le_trans (getD_le_maxOrder problem hj) (Nat.le_max_right 1 _)) + have hr : coefficientMatrixRow problem (leafDegreeCap problem) (j, t) ∈ + (coefficientMatrixRows problem).toList := by + rw [coefficientMatrixRows, Array.toList_map] + exact List.mem_map.mpr ⟨(j, t), mem_coefficientEquationIndices hj ht, rfl⟩ + have hdot := homogeneousKernelBasisRows_dot_eq_zero hv hr + rw [Polynomial.finsetSum_coeff] + refine Eq.trans ?_ hdot + rw [pm_sum_range_mul] + refine Finset.sum_congr rfl fun k hk ↦ ?_ + have hk' : k < problem.matrix.size := Finset.mem_range.mp hk + have hentry : ∀ a, a < leafDegreeCap problem → + (coefficientMatrixRow problem (leafDegreeCap problem) (j, t)).getD + (k * leafDegreeCap problem + a) 0 = + if a ≤ t then + CPolynomial.coeff (rowGet (problem.matrix.getD k #[]) j) (t - a) + else 0 := by + intro a ha + have hcol : k * leafDegreeCap problem + a < + problem.matrix.size * leafDegreeCap problem := by + have h1 : k * leafDegreeCap problem + a < + (k + 1) * leafDegreeCap problem := by + rw [Nat.succ_mul] + omega + exact lt_of_lt_of_le h1 (Nat.mul_le_mul_right _ (by omega)) + have hdiv : (k * leafDegreeCap problem + a) / leafDegreeCap problem = k := by + rw [Nat.add_comm, Nat.add_mul_div_right _ _ hcap_pos, + Nat.div_eq_of_lt ha, Nat.zero_add] + have hmod : (k * leafDegreeCap problem + a) % leafDegreeCap problem = a := by + rw [Nat.add_comm, Nat.add_mul_mod_self_right, Nat.mod_eq_of_lt ha] + rw [coefficientMatrixRow_getD problem _ _ hcol, hdiv, hmod] + rw [Polynomial.coeff_mul, Finset.Nat.sum_antidiagonal_eq_sum_range_succ_mk] + refine Eq.trans (Finset.sum_congr rfl fun a ha ↦ ?_) + (Finset.sum_subset (fun x hx ↦ Finset.mem_range.mpr + (Nat.lt_of_lt_of_le (Finset.mem_range.mp hx) htcap)) fun a ha hnot ↦ ?_) + · have ha' : a < t + 1 := Finset.mem_range.mp ha + rw [← CPolynomial.coeff_toPoly, ← CPolynomial.coeff_toPoly, + rowGet_vectorToPolynomialRow_coeff, if_pos ⟨hk', by omega⟩, + hentry a (by omega), if_pos (by omega)] + exact mul_comm _ _ + · have ha1 : a < leafDegreeCap problem := Finset.mem_range.mp ha + have ha2 : t + 1 ≤ a := by + by_contra hcon + exact hnot (Finset.mem_range.mpr (by omega)) + rw [hentry a ha1, if_neg (by omega), zero_mul] + +/-- Arithmetic of the packed index `k * cap + a`. -/ +theorem pm_pack_index {cap : Nat} (hcap : 0 < cap) {size k a : Nat} + (hk : k < size) (ha : a < cap) : + k * cap + a < size * cap ∧ (k * cap + a) / cap = k ∧ + (k * cap + a) % cap = a := by + refine ⟨?_, ?_, ?_⟩ + · have h1 : k * cap + a < (k + 1) * cap := by + rw [Nat.succ_mul] + omega + exact lt_of_lt_of_le h1 (Nat.mul_le_mul_right _ (by omega)) + · rw [Nat.add_comm, Nat.add_mul_div_right _ _ hcap, Nat.div_eq_of_lt ha, + Nat.zero_add] + · rw [Nat.add_comm, Nat.add_mul_mod_self_right, Nat.mod_eq_of_lt ha] + +/-- Pack the coefficients below the leaf degree cap of a polynomial row into +one flat scalar vector: coefficient `a` of coordinate `k` is stored at index +`k * leafDegreeCap problem + a`. This is the coefficient-side inverse of +`vectorToPolynomialRow`. -/ +def rowToCoefficientVector (problem : XAdicProblem F) (row : PolynomialRow F) : + Array F := + (List.range (problem.matrix.size * leafDegreeCap problem)).map + (fun c ↦ CPolynomial.coeff (rowGet row (c / leafDegreeCap problem)) + (c % leafDegreeCap problem)) |>.toArray + +omit [BEq F] [LawfulBEq F] in +/-- In-range entries of the packed coefficient vector. -/ +private theorem rowToCoefficientVector_getD (problem : XAdicProblem F) + (row : PolynomialRow F) {c : Nat} + (hc : c < problem.matrix.size * leafDegreeCap problem) : + (rowToCoefficientVector problem row).getD c 0 = + CPolynomial.coeff (rowGet row (c / leafDegreeCap problem)) + (c % leafDegreeCap problem) := by + rw [rowToCoefficientVector, Array.getD_eq_getD_getElem?, + List.getElem?_toArray, List.getElem?_map, List.getElem?_range hc, + Option.map_some, Option.getD_some] + +/-- Reconstructing a polynomial row from its packed coefficient vector is the +identity on rows of the principal width whose coefficients respect the leaf +degree cap. -/ +theorem vectorToPolynomialRow_rowToCoefficientVector + (problem : XAdicProblem F) {row : PolynomialRow F} + (hsize : row.size = problem.matrix.size) + (hdeg : ∀ k, k < row.size → ∀ a, leafDegreeCap problem ≤ a → + CPolynomial.coeff (rowGet row k) a = 0) : + vectorToPolynomialRow (leafDegreeCap problem) problem.matrix.size + (rowToCoefficientVector problem row) = row := by + have hcap_pos : 0 < leafDegreeCap problem := le_max_left 1 _ + have hcoord : ∀ k, + rowGet (vectorToPolynomialRow (leafDegreeCap problem) + problem.matrix.size (rowToCoefficientVector problem row)) k = + rowGet row k := by + intro k + apply CPolynomial.eq_iff_coeff.2 + intro a + rw [rowGet_vectorToPolynomialRow_coeff] + rcases Nat.lt_or_ge k problem.matrix.size with hk | hk + · rcases Nat.lt_or_ge a (leafDegreeCap problem) with ha | ha + · obtain ⟨hc, hdiv, hmod⟩ := pm_pack_index hcap_pos hk ha + rw [if_pos ⟨hk, ha⟩, rowToCoefficientVector_getD problem row hc, + hdiv, hmod] + · rw [if_neg (by omega), hdeg k (by omega) a ha] + · rw [if_neg (by omega)] + have hzero : rowGet row k = 0 := by + rw [rowGet, Array.getD_eq_getD_getElem?, + Array.getElem?_eq_none (by omega)] + rfl + rw [hzero] + exact (CPolynomial.coeff_zero a).symm + refine Array.ext ?_ fun i hi hi' ↦ ?_ + · rw [vectorToPolynomialRow] + simp only [List.size_toArray, List.length_map, List.length_range, hsize] + · have h := hcoord i + rw [rowGet, rowGet, array_getD_of_lt' _ 0 hi, array_getD_of_lt' _ 0 hi'] + at h + exact h + +/-- Conversely to `mem_coefficientEquationIndices`, every member of +`coefficientEquationIndices` is an in-range equation index. -/ +private theorem mem_coefficientEquationIndices_bounds {orders : Array Nat} + {j t : Nat} + (h : (j, t) ∈ (coefficientEquationIndices orders).toList) : + j < orders.size ∧ t < orders.getD j 0 := by + rw [coefficientEquationIndices] at h + simp only [Std.Legacy.Range.forIn_eq_forIn_range', Std.Legacy.Range.size, + Nat.sub_zero, Nat.add_sub_cancel, Nat.div_one, + List.forIn_pure_yield_eq_foldl, bind_pure_comp, map_pure, bind_pure, + Id.run_pure] at h + have hrange : ∀ n : Nat, List.range' 0 n = List.range n := fun n ↦ + (List.range_eq_range' (n := n)).symm + simp only [hrange] at h + rw [pm_doubleFoldl_toList] at h + simp only [List.nil_append] at h + obtain ⟨j', hj', hmem⟩ := List.mem_flatMap.mp h + obtain ⟨t', ht', heq⟩ := List.mem_map.mp hmem + have h1 : j' = j := congrArg Prod.fst heq + have h2 : t' = t := congrArg Prod.snd heq + subst h1 + subst h2 + exact ⟨List.mem_range.mp hj', List.mem_range.mp ht'⟩ + +/-- **Converse coefficient bridge.** A polynomial row of a well-formed +bounded leaf problem that satisfies the X-adic conditions yields a packed +coefficient vector orthogonal to every scalar coefficient row. -/ +theorem coefficientMatrixRows_dot_eq_zero_of_approximates + (mulCtx : CPolynomial.MulContext F) (problem : XAdicProblem F) + {row : PolynomialRow F} + (happrox : RowApproximates mulCtx problem row) + (hwf : WellFormed problem.matrix) : + ∀ r ∈ (coefficientMatrixRows problem).toList, + ∑ c ∈ Finset.range (problem.matrix.size * leafDegreeCap problem), + r.getD c 0 * (rowToCoefficientVector problem row).getD c 0 = 0 := by + intro r hr + simp only [coefficientMatrixRows] at hr + rw [Array.toList_map] at hr + obtain ⟨eqn, heqmem, rfl⟩ := List.mem_map.mp hr + obtain ⟨j, t⟩ := eqn + obtain ⟨hj, ht⟩ := mem_coefficientEquationIndices_bounds heqmem + have hcap_pos : 0 < leafDegreeCap problem := le_max_left 1 _ + have htcap : t < leafDegreeCap problem := + lt_of_lt_of_le ht (le_trans (getD_le_maxOrder problem hj) + (Nat.le_max_right 1 _)) + have hdvd : (Polynomial.X : Polynomial F) ^ (problem.orders.getD j 0) ∣ + ∑ k ∈ Finset.range problem.matrix.size, + (rowGet row k).toPoly * + (rowGet (problem.matrix.getD k #[]) j).toPoly := by + rcases Nat.lt_or_ge j (MatrixWidth problem.matrix) with hjw | hjw + · exact (rowApproximates_iff mulCtx problem row).mp happrox j hj hjw + · have hzero : ∀ k ∈ Finset.range problem.matrix.size, + (rowGet row k).toPoly * + (rowGet (problem.matrix.getD k #[]) j).toPoly = 0 := by + intro k hk + have hk' : k < problem.matrix.size := Finset.mem_range.mp hk + have hmemrow : problem.matrix.getD k #[] ∈ + MatrixRows problem.matrix := by + rw [MatrixRows, array_getD_of_lt' _ #[] hk', + ← Array.getElem_toList (by simpa using hk')] + exact List.getElem_mem _ + have hsz := hwf _ hmemrow + have h0 : rowGet (problem.matrix.getD k #[]) j = 0 := by + rw [rowGet, Array.getD_eq_getD_getElem?, + Array.getElem?_eq_none (by omega)] + rfl + rw [h0, CPolynomial.toPoly_zero, mul_zero] + rw [Finset.sum_eq_zero hzero] + exact dvd_zero _ + have hcoeff := Polynomial.X_pow_dvd_iff.mp hdvd t ht + have hentry : ∀ k, k < problem.matrix.size → + ∀ a, a < leafDegreeCap problem → + (coefficientMatrixRow problem (leafDegreeCap problem) (j, t)).getD + (k * leafDegreeCap problem + a) 0 = + if a ≤ t then + CPolynomial.coeff (rowGet (problem.matrix.getD k #[]) j) (t - a) + else 0 := by + intro k hk a ha + obtain ⟨hc, hdiv, hmod⟩ := pm_pack_index hcap_pos hk ha + rw [coefficientMatrixRow_getD problem _ _ hc, hdiv, hmod] + have hw : ∀ k, k < problem.matrix.size → + ∀ a, a < leafDegreeCap problem → + (rowToCoefficientVector problem row).getD + (k * leafDegreeCap problem + a) 0 = + CPolynomial.coeff (rowGet row k) a := by + intro k hk a ha + obtain ⟨hc, hdiv, hmod⟩ := pm_pack_index hcap_pos hk ha + rw [rowToCoefficientVector_getD problem row hc, hdiv, hmod] + have hinner : ∀ k, k < problem.matrix.size → + ∑ a ∈ Finset.range (leafDegreeCap problem), + (coefficientMatrixRow problem (leafDegreeCap problem) (j, t)).getD + (k * leafDegreeCap problem + a) 0 * + (rowToCoefficientVector problem row).getD + (k * leafDegreeCap problem + a) 0 = + Polynomial.coeff ((rowGet row k).toPoly * + (rowGet (problem.matrix.getD k #[]) j).toPoly) t := by + intro k hk + symm + rw [Polynomial.coeff_mul, Finset.Nat.sum_antidiagonal_eq_sum_range_succ_mk] + refine Eq.trans (Finset.sum_congr rfl fun a ha ↦ ?_) + (Finset.sum_subset (fun x hx ↦ Finset.mem_range.mpr + (Nat.lt_of_lt_of_le (Finset.mem_range.mp hx) htcap)) + fun a ha hnot ↦ ?_) + · have ha' : a < t + 1 := Finset.mem_range.mp ha + rw [← CPolynomial.coeff_toPoly, ← CPolynomial.coeff_toPoly, + hentry k hk a (by omega), if_pos (by omega), hw k hk a (by omega)] + exact mul_comm _ _ + · have ha1 : a < leafDegreeCap problem := Finset.mem_range.mp ha + have ha2 : t + 1 ≤ a := by + by_contra hcon + exact hnot (Finset.mem_range.mpr (by omega)) + rw [hentry k hk a ha1, if_neg (by omega), zero_mul] + rw [pm_sum_range_mul, + Finset.sum_congr rfl fun k hk ↦ hinner k (Finset.mem_range.mp hk), + ← Polynomial.finsetSum_coeff] + exact hcoeff + +/-- Every kernel-leaf basis row approximates the problem and has the principal +row width. -/ +theorem kernelLeafBasis_rows (mulCtx : CPolynomial.MulContext F) + (problem : XAdicProblem F) (shift : Array Nat) : + ∀ row ∈ MatrixRows (kernelLeafBasis problem shift), + RowApproximates mulCtx problem row ∧ row.size = problem.matrix.size := by + set Q : PolynomialRow F → Prop := fun row ↦ + RowApproximates mulCtx problem row ∧ row.size = problem.matrix.size with hQ + have hclosed : ∀ (target reducer : PolynomialRow F) (shift' : Array Nat), + Q target → Q reducer → + Q (cancelKernelLeafLeadingTerm target reducer shift') := by + intro target reducer shift' ht hr + refine ⟨rowApproximates_cancelKernelLeafLeadingTerm mulCtx problem shift' + ht.1 hr.1, ?_⟩ + rw [cancelKernelLeafLeadingTerm] + split + · split + · split + · exact ht.2 + · rw [rowSub_size, rowScaleCoeffX_size] + have h1 := ht.2 + have h2 := hr.2 + omega + · exact ht.2 + · exact ht.2 + intro row hrow + simp only [kernelLeafBasis] at hrow + rw [MatrixRows, completeMissingPivotRows, Array.toList_append] at hrow + rcases List.mem_append.mp hrow with hmem | hmem + · refine reduceKernelLeafRowsIncremental_invariant Q hclosed shift ?_ hmem + intro r hr + rw [MatrixRows, Array.toList_append] at hr + rcases List.mem_append.mp hr with hr | hr + · rw [Array.toList_map] at hr + rcases List.mem_map.mp hr with ⟨w, hw, rfl⟩ + refine ⟨vectorToPolynomialRow_approximates mulCtx problem hw, ?_⟩ + simp [vectorToPolynomialRow] + · simp only [kernelLeafCompletionRows] at hr + rcases List.mem_map.mp hr with ⟨i, _hi, rfl⟩ + refine ⟨rowApproximates_monomialUnitRow mulCtx problem + (fun j hj ↦ le_trans (getD_le_maxOrder problem hj) + (Nat.le_max_right 1 _)), ?_⟩ + simp [monomialUnitRow] + · have happrox := missingCompletionRows_approximates mulCtx problem shift _ hmem + refine ⟨happrox, ?_⟩ + rw [missingCompletionRows, List.toList_toArray] at hmem + rcases List.mem_filterMap.mp hmem with ⟨i, _hi, hsome⟩ + split at hsome + · cases hsome + · cases hsome + simp [monomialUnitRow] + +end Approximant + +end PolynomialMatrix + +end CompPoly diff --git a/CompPoly/LinearAlgebra/PolynomialMatrix/Approximant/PMBasis/KernelLeafSpan.lean b/CompPoly/LinearAlgebra/PolynomialMatrix/Approximant/PMBasis/KernelLeafSpan.lean new file mode 100644 index 00000000..6b89f5d4 --- /dev/null +++ b/CompPoly/LinearAlgebra/PolynomialMatrix/Approximant/PMBasis/KernelLeafSpan.lean @@ -0,0 +1,1216 @@ +/- +Copyright (c) 2026 CompPoly Contributors. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Valerii Huhnin +-/ + +module + +public import CompPoly.LinearAlgebra.PolynomialMatrix.Approximant.PMBasis.KernelLeafSoundness +public import CompPoly.LinearAlgebra.PolynomialMatrix.Approximant.PMBasis.XAdicSoundness +public import CompPoly.LinearAlgebra.PolynomialMatrix.MuldersStorjohannCorrectness.MatrixRows +public import CompPoly.LinearAlgebra.PolynomialMatrix.MuldersStorjohannCorrectness.Measure + +/-! +# Kernel-Leaf Reduction Row-Span Soundness + +The shifted pivot-table reduction preserves the generated row module: the +fuel bound dominates the shifted row measure, every displaced row re-enters +the table as a reduced combination, and the incremental reduction loop keeps +every input row inside the span of its output. +-/ + +@[expose] public section + +namespace CompPoly + +namespace PolynomialMatrix + +namespace Approximant + +variable {F : Type*} [Field F] [BEq F] [LawfulBEq F] + +/-! ## Kernel-leaf reduction row-span soundness + +The pivot-table reduction only ever replaces rows by row operations that are +invertible inside the generated row module, so no original row leaves the row +span. The proofs below make this precise: leading-term cancellation is the +Mulders-Storjohann cancellation in disguise, the pivot-table insertion loop is +tracked through a fuel-indexed measure argument, and the incremental reducer +chains the per-call result through row-span transitivity. -/ + +/-- Coefficient-shift scaling agrees with monomial multiplication. -/ +private theorem polynomialScaleCoeffX_eq_monomial_mul [DecidableEq F] + (c : F) (d : Nat) (p : CPolynomial F) : + polynomialScaleCoeffX c d p = CPolynomial.monomial d c * p := by + apply (CPolynomial.eq_iff_coeff).2 + intro i + rw [CPolynomial.coeff_monomial_mul, CPolynomial.coeff_toPoly, + polynomialScaleCoeffX_toPoly, mul_assoc, Polynomial.coeff_C_mul, + Polynomial.X_pow_mul, Polynomial.coeff_mul_X_pow'] + split_ifs with h + · rw [CPolynomial.coeff_toPoly] + · rw [mul_zero] + +/-- Coefficient-shift row scaling agrees with monomial row scaling. -/ +private theorem rowScaleCoeffX_eq_rowScaleMonomial [DecidableEq F] + (c : F) (d : Nat) (row : PolynomialRow F) : + rowScaleCoeffX c d row = rowScaleMonomial c d row := by + rw [rowScaleCoeffX, rowScaleMonomial, rowScalePolynomial] + exact Array.map_congr_left fun p _hp ↦ polynomialScaleCoeffX_eq_monomial_mul c d p + +/-- The kernel-leaf cancellation is the Mulders-Storjohann cancellation. -/ +private theorem cancelKernelLeafLeadingTerm_eq_cancelShifted [DecidableEq F] + (target reducer : PolynomialRow F) (shift : Array Nat) : + cancelKernelLeafLeadingTerm target reducer shift = + cancelShiftedLeadingTerm target reducer shift := by + rw [cancelKernelLeafLeadingTerm, cancelShiftedLeadingTerm] + cases rowShiftedLeadingTerm? target shift with + | none => rfl + | some t => + cases rowShiftedLeadingTerm? reducer shift with + | none => rfl + | some r => simp only [rowScaleCoeffX_eq_rowScaleMonomial] + +/-- Kernel-leaf cancellation preserves the row width. -/ +private theorem cancelKernelLeafLeadingTerm_size + {target reducer : PolynomialRow F} {shift : Array Nat} + (hsize : reducer.size = target.size) : + (cancelKernelLeafLeadingTerm target reducer shift).size = target.size := by + rw [cancelKernelLeafLeadingTerm] + split + · split + · split + · rfl + · rw [rowSub_size, rowScaleCoeffX_size, hsize, Nat.max_self] + · rfl + · rfl + +/-- Kernel-leaf cancellation stays inside a row span. -/ +private theorem cancelKernelLeafLeadingTerm_mem_rowSpan [DecidableEq F] + {M : PolynomialMatrix F} {target reducer : PolynomialRow F} + {shift : Array Nat} + (htarget : target ∈ RowSpan M) (hreducer : reducer ∈ RowSpan M) : + cancelKernelLeafLeadingTerm target reducer shift ∈ RowSpan M := by + rw [cancelKernelLeafLeadingTerm_eq_cancelShifted] + exact cancelShiftedLeadingTerm_mem_rowSpan htarget hreducer + +/-- Kernel-leaf cancellation can be undone inside a row span. -/ +private theorem cancelKernelLeafLeadingTerm_target_mem_rowSpan [DecidableEq F] + {M : PolynomialMatrix F} {target reducer : PolynomialRow F} + {shift : Array Nat} + (hcancel : cancelKernelLeafLeadingTerm target reducer shift ∈ RowSpan M) + (hreducer : reducer ∈ RowSpan M) + (hsize : reducer.size = target.size) : + target ∈ RowSpan M := by + rw [cancelKernelLeafLeadingTerm_eq_cancelShifted] at hcancel + exact cancelShiftedLeadingTerm_target_mem_rowSpan hcancel hreducer hsize + +/-- Kernel-leaf cancellation strictly decreases the shifted row measure. -/ +private theorem cancelKernelLeafLeadingTerm_shiftedRowMeasure_lt [DecidableEq F] + {target reducer : PolynomialRow F} {shift : Array Nat} + {t r : ShiftedLeadingTerm F} + (ht : rowShiftedLeadingTerm? target shift = some t) + (hr : rowShiftedLeadingTerm? reducer shift = some r) + (hpos : t.position = r.position) + (hle : r.shiftedDegree ≤ t.shiftedDegree) + (hsize : reducer.size = target.size) : + shiftedRowMeasure (cancelKernelLeafLeadingTerm target reducer shift) shift < + shiftedRowMeasure target shift := by + rw [cancelKernelLeafLeadingTerm_eq_cancelShifted] + exact cancelShiftedLeadingTerm_shiftedRowMeasure_lt ht hr hpos hle hsize + +omit [LawfulBEq F] in +/-- A zero row is the zero row of its width. -/ +theorem rowIsZero_eq_zeroRow {row : PolynomialRow F} + (h : RowIsZero row) : row = zeroRow row.size := by + apply Array.ext + · simp [zeroRow] + · intro j hj hj' + have hzero : row[j] = 0 := h row[j] (Array.getElem_mem_toList hj) + simp [zeroRow, hzero] + +/-- Rows without a shifted leading term are zero rows. -/ +private theorem rowIsZero_of_rowShiftedLeadingTerm?_eq_none + {row : PolynomialRow F} {shift : Array Nat} + (h : rowShiftedLeadingTerm? row shift = none) : RowIsZero row := by + cases hdeg : rowShiftedDegree? row shift with + | none => exact rowShiftedDegree?_eq_none_iff.1 hdeg + | some d => + rcases rowShiftedLeadingPosition?_some_of_degree hdeg with ⟨pos, hpos⟩ + rcases rowShiftedLeadingTerm?_some_of_position hpos with ⟨term, hterm, _⟩ + rw [hterm] at h + cases h + +/-- Rows with a shifted leading position are nonzero. -/ +private theorem not_rowIsZero_of_rowShiftedLeadingPosition?_eq_some + {row : PolynomialRow F} {shift : Array Nat} {p : Nat} + (h : rowShiftedLeadingPosition? row shift = some p) : ¬ RowIsZero row := by + intro hz + have hdeg : rowShiftedDegree? row shift = none := + rowShiftedDegree?_eq_none_iff.2 hz + rw [rowShiftedLeadingPosition?, hdeg] at h + cases h + +/-- A `some` slot index of an option array is in bounds. -/ +private theorem pivot_getD_some_lt_size {α : Type*} + {pivots : Array (Option α)} {p : Nat} {r : α} + (h : pivots.getD p none = some r) : p < pivots.size := by + by_contra hge + rw [array_getD_of_le' pivots none (Nat.le_of_not_lt hge)] at h + cases h + +omit [BEq F] [LawfulBEq F] in +/-- Every `some` slot of a pivot table appears among its pivot rows. -/ +private theorem mem_pivotRows_of_getD + {pivots : Array (Option (PolynomialRow F))} {p : Nat} + {r : PolynomialRow F} (h : pivots.getD p none = some r) : + r ∈ MatrixRows (pivotRows pivots) := by + have hp : p < pivots.size := pivot_getD_some_lt_size h + rw [MatrixRows, pivotRows, List.toList_toArray] + refine List.mem_filterMap.mpr ⟨some r, ?_, rfl⟩ + rw [array_getD_of_lt' pivots none hp] at h + rw [← h] + exact Array.getElem_mem_toList hp + +omit [BEq F] [LawfulBEq F] in +/-- Matrices with a member row are nonempty. -/ +theorem size_pos_of_mem_matrixRows {M : PolynomialMatrix F} + {r : PolynomialRow F} (h : r ∈ MatrixRows M) : 0 < M.size := by + rw [MatrixRows] at h + have hne := List.ne_nil_of_mem h + rw [← Array.length_toList] + exact List.length_pos_of_ne_nil hne + +omit [BEq F] [LawfulBEq F] in +/-- Uniform row widths make a matrix well formed. -/ +theorem wellFormed_of_sizes {M : PolynomialMatrix F} {n : Nat} + (h : ∀ r ∈ MatrixRows M, r.size = n) : WellFormed M := by + intro r hr + rw [matrixWidth_eq_of_first_row (size_pos_of_mem_matrixRows hr) h] + exact h r hr + +omit [BEq F] [LawfulBEq F] in +/-- Pivot rows inherit a uniform slot width. -/ +private theorem pivotRows_sizes {n : Nat} + {pivots : Array (Option (PolynomialRow F))} + (hsz : ∀ p r, pivots.getD p none = some r → r.size = n) : + ∀ r ∈ MatrixRows (pivotRows pivots), r.size = n := by + intro r hr + exact pivotRows_invariant (fun row ↦ row.size = n) hsz hr + +/-- Stored pivot rows lie in the pivot-row span. -/ +private theorem stored_mem_rowSpan_pivotRows [DecidableEq F] {n : Nat} + {pivots : Array (Option (PolynomialRow F))} + (hsz : ∀ p r, pivots.getD p none = some r → r.size = n) + {p : Nat} {r : PolynomialRow F} (hget : pivots.getD p none = some r) : + r ∈ RowSpan (pivotRows pivots) := + matrix_row_mem_rowSpan (wellFormed_of_sizes (pivotRows_sizes hsz)) + (mem_pivotRows_of_getD hget) + +/-- The width-`n` zero row lies in any nonempty pivot-row span of width `n`. -/ +private theorem zeroRow_mem_rowSpan_pivotRows [DecidableEq F] {n : Nat} + {pivots : Array (Option (PolynomialRow F))} + (hsz : ∀ p r, pivots.getD p none = some r → r.size = n) + {q : Nat} {s : PolynomialRow F} (hentry : pivots.getD q none = some s) : + zeroRow (F := F) n ∈ RowSpan (pivotRows pivots) := by + have hmem := mem_pivotRows_of_getD hentry + have hwidth : MatrixWidth (pivotRows pivots) = n := + matrixWidth_eq_of_first_row (size_pos_of_mem_matrixRows hmem) + (pivotRows_sizes hsz) + have hzero := zeroRow_mem_rowSpan (wellFormed_of_sizes (pivotRows_sizes hsz)) + rwa [hwidth] at hzero + +/-- Measure contribution of one pivot-table slot. -/ +private def pivotEntryMeasure [DecidableEq F] (shift : Array Nat) : + Option (PolynomialRow F) → Nat + | none => 0 + | some r => shiftedRowMeasure r shift + +/-- Total shifted measure of all stored pivot-table rows. -/ +private def pivotTableMeasure [DecidableEq F] + (pivots : Array (Option (PolynomialRow F))) (shift : Array Nat) : Nat := + (List.range pivots.size).foldl + (fun acc p ↦ acc + pivotEntryMeasure shift (pivots.getD p none)) 0 + +private theorem foldRange_add_eq_zero (n : Nat) {f : Nat → Nat} + (h : ∀ k, k < n → f k = 0) : + (List.range n).foldl (fun acc k ↦ acc + f k) 0 = 0 := by + induction n with + | zero => simp + | succ n ih => + rw [List.range_succ, List.foldl_append] + simp only [List.foldl_cons, List.foldl_nil] + rw [ih (fun k hk ↦ h k (by omega)), h n (by omega)] + +private theorem foldRange_add_update (n : Nat) (f : Nat → Nat) {p : Nat} + (hp : p < n) (v : Nat) : + (List.range n).foldl (fun acc k ↦ acc + (if k = p then v else f k)) 0 + + f p = + (List.range n).foldl (fun acc k ↦ acc + f k) 0 + v := by + induction n with + | zero => omega + | succ n ih => + rw [List.range_succ, List.foldl_append, List.foldl_append] + simp only [List.foldl_cons, List.foldl_nil] + by_cases hpn : p = n + · subst hpn + have hpref : + (List.range p).foldl + (fun acc k ↦ acc + (if k = p then v else f k)) 0 = + (List.range p).foldl (fun acc k ↦ acc + f k) 0 := by + apply foldRange_add_eq_of_pointwise + intro k hk + rw [if_neg (by omega)] + rw [hpref, if_pos rfl] + omega + · have hstep := ih (by omega) + rw [if_neg (fun h ↦ hpn h.symm)] + omega + +omit [LawfulBEq F] in +/-- The empty pivot table has measure zero. -/ +private theorem pivotTableMeasure_replicate [DecidableEq F] + (w : Nat) (shift : Array Nat) : + pivotTableMeasure + (Array.replicate w (none : Option (PolynomialRow F))) shift = 0 := by + rw [pivotTableMeasure, Array.size_replicate] + apply foldRange_add_eq_zero + intro k hk + rw [array_getD_of_lt' _ _ (by simpa using hk), Array.getElem_replicate] + rfl + +omit [LawfulBEq F] in +/-- Pivot-table measure of an in-bounds slot update. -/ +private theorem pivotTableMeasure_setIfInBounds [DecidableEq F] + (pivots : Array (Option (PolynomialRow F))) (shift : Array Nat) + {p : Nat} (hp : p < pivots.size) (row : PolynomialRow F) : + pivotTableMeasure (pivots.setIfInBounds p (some row)) shift + + pivotEntryMeasure shift (pivots.getD p none) = + pivotTableMeasure pivots shift + shiftedRowMeasure row shift := by + rw [pivotTableMeasure, pivotTableMeasure, Array.size_setIfInBounds] + have hpoint : + (List.range pivots.size).foldl + (fun acc k ↦ acc + + pivotEntryMeasure shift + ((pivots.setIfInBounds p (some row)).getD k none)) 0 = + (List.range pivots.size).foldl + (fun acc k ↦ acc + + (if k = p then shiftedRowMeasure row shift + else pivotEntryMeasure shift (pivots.getD k none))) 0 := by + apply foldRange_add_eq_of_pointwise + intro k hk + rw [array_getD_setIfInBounds] + by_cases hkp : k = p + · subst hkp + rw [if_pos ⟨rfl, hp⟩, if_pos rfl] + rfl + · rw [if_neg (fun hcon ↦ hkp hcon.1.symm), if_neg hkp] + rw [hpoint] + exact foldRange_add_update pivots.size _ hp (shiftedRowMeasure row shift) + +/-- Pivot-table insertion preserves the table size. -/ +private theorem insertKernelLeafPivotRowWithFuel_size : + ∀ (fuel : Nat) (pivots : Array (Option (PolynomialRow F))) + (shift : Array Nat) (row : PolynomialRow F), + (insertKernelLeafPivotRowWithFuel fuel pivots shift row).size = + pivots.size := by + intro fuel + induction fuel with + | zero => intro pivots shift row; rfl + | succ fuel ih => + intro pivots shift row + rw [insertKernelLeafPivotRowWithFuel] + split + · rfl + · split + · exact Array.size_setIfInBounds + · split + · exact Array.size_setIfInBounds + · split + · simp only [] + rw [ih, Array.size_setIfInBounds] + · simp only [] + rw [ih] + +omit [LawfulBEq F] in +private theorem pivotEntryMeasure_none [DecidableEq F] (shift : Array Nat) : + pivotEntryMeasure (F := F) shift none = 0 := rfl + +omit [LawfulBEq F] in +private theorem pivotEntryMeasure_some [DecidableEq F] (shift : Array Nat) + (r : PolynomialRow F) : + pivotEntryMeasure shift (some r) = shiftedRowMeasure r shift := rfl + +/-- Pivot-table insertion preserves slot widths and slot leading positions. -/ +private theorem insertKernelLeafPivotRowWithFuel_pivotInv {n : Nat} : + ∀ (fuel : Nat) (pivots : Array (Option (PolynomialRow F))) + (shift : Array Nat) (row : PolynomialRow F), + row.size = n → + (∀ p r, pivots.getD p none = some r → + r.size = n ∧ rowShiftedLeadingPosition? r shift = some p) → + ∀ p r, + (insertKernelLeafPivotRowWithFuel fuel pivots shift row).getD p none = + some r → + r.size = n ∧ rowShiftedLeadingPosition? r shift = some p := by + intro fuel + induction fuel with + | zero => + intro pivots shift row _hrow hinv p r hget + exact hinv p r hget + | succ fuel ih => + intro pivots shift row hrow hinv p r hget + have hset : ∀ (position : Nat) (newRow : PolynomialRow F), + newRow.size = n → + rowShiftedLeadingPosition? newRow shift = some position → + ∀ p' r', + (pivots.setIfInBounds position (some newRow)).getD p' none = + some r' → + r'.size = n ∧ rowShiftedLeadingPosition? r' shift = some p' := by + intro position newRow hsize hpos p' r' hget' + rw [array_getD_setIfInBounds] at hget' + by_cases hp' : position = p' ∧ position < pivots.size + · rw [if_pos hp'] at hget' + cases hget' + exact ⟨hsize, hp'.1 ▸ hpos⟩ + · rw [if_neg hp'] at hget' + exact hinv p' r' hget' + rw [insertKernelLeafPivotRowWithFuel] at hget + cases hterm : rowShiftedLeadingTerm? row shift with + | none => + simp only [hterm] at hget + exact hinv p r hget + | some target => + simp only [hterm] at hget + have hrowpos : rowShiftedLeadingPosition? row shift = + some target.position := + (rowShiftedLeadingTerm?_some_data hterm).2.1 + cases hpiv : pivots.getD target.position none with + | none => + simp only [hpiv] at hget + exact hset target.position row hrow hrowpos p r hget + | some pivot => + simp only [hpiv] at hget + cases hpterm : rowShiftedLeadingTerm? pivot shift with + | none => + simp only [hpterm] at hget + exact hset target.position row hrow hrowpos p r hget + | some reducer => + simp only [hpterm] at hget + have hpivn : pivot.size = n := + (hinv target.position pivot hpiv).1 + split at hget + · refine ih (pivots.setIfInBounds target.position (some row)) + shift (cancelKernelLeafLeadingTerm pivot row shift) ?_ ?_ + p r hget + · rw [cancelKernelLeafLeadingTerm_size (by omega)] + exact hpivn + · exact hset target.position row hrow hrowpos + · refine ih pivots shift + (cancelKernelLeafLeadingTerm row pivot shift) ?_ hinv + p r hget + rw [cancelKernelLeafLeadingTerm_size (by omega)] + exact hrow + +/-- Pivot-table insertion never erases an occupied slot. -/ +private theorem insertKernelLeafPivotRowWithFuel_persist : + ∀ (fuel : Nat) (pivots : Array (Option (PolynomialRow F))) + (shift : Array Nat) (row : PolynomialRow F) {q : Nat} + {s : PolynomialRow F}, + pivots.getD q none = some s → + ∃ s', + (insertKernelLeafPivotRowWithFuel fuel pivots shift row).getD q none = + some s' := by + intro fuel + induction fuel with + | zero => + intro pivots shift row q s hq + exact ⟨s, hq⟩ + | succ fuel ih => + intro pivots shift row q s hq + have hset : ∀ (position : Nat) (newRow : PolynomialRow F), + ∃ s', + (pivots.setIfInBounds position (some newRow)).getD q none = + some s' := by + intro position newRow + rw [array_getD_setIfInBounds] + by_cases hpq : position = q ∧ position < pivots.size + · rw [if_pos hpq] + exact ⟨newRow, rfl⟩ + · rw [if_neg hpq] + exact ⟨s, hq⟩ + rw [insertKernelLeafPivotRowWithFuel] + split + · exact ⟨s, hq⟩ + · split + · exact hset _ row + · split + · exact hset _ row + · split + · simp only [] + rcases hset _ row with ⟨s', hs'⟩ + exact ih _ shift _ hs' + · simp only [] + exact ih _ shift _ hq + +/-- Pivot-table insertion grows the table measure by at most the measure of +the inserted row. -/ +private theorem insertKernelLeafPivotRowWithFuel_measure_le [DecidableEq F] + {n : Nat} : + ∀ (fuel : Nat) (pivots : Array (Option (PolynomialRow F))) + (shift : Array Nat) (row : PolynomialRow F), + pivots.size = n → + row.size = n → + (∀ p r, pivots.getD p none = some r → + r.size = n ∧ rowShiftedLeadingPosition? r shift = some p) → + pivotTableMeasure (insertKernelLeafPivotRowWithFuel fuel pivots shift row) + shift ≤ + shiftedRowMeasure row shift + pivotTableMeasure pivots shift := by + intro fuel + induction fuel with + | zero => + intro pivots shift row _ _ _ + exact Nat.le_add_left _ _ + | succ fuel ih => + intro pivots shift row hpsize hrow hinv + rw [insertKernelLeafPivotRowWithFuel] + split + · exact Nat.le_add_left _ _ + · rename_i target hterm + have hrowpos : rowShiftedLeadingPosition? row shift = + some target.position := + (rowShiftedLeadingTerm?_some_data hterm).2.1 + have hb : target.position < pivots.size := by + have := rowShiftedLeadingPosition?_lt hrowpos + omega + split + · rename_i hpiv + have hmeas := pivotTableMeasure_setIfInBounds pivots shift hb row + rw [hpiv, pivotEntryMeasure_none] at hmeas + omega + · rename_i pivot hpiv + have hpivn : pivot.size = n := (hinv target.position pivot hpiv).1 + have hpivpos : rowShiftedLeadingPosition? pivot shift = + some target.position := (hinv target.position pivot hpiv).2 + split + · rename_i hpterm + rcases rowShiftedLeadingTerm?_some_of_position hpivpos with + ⟨term, hterm', _⟩ + rw [hterm'] at hpterm + cases hpterm + · rename_i reducer hpterm + have hredpos : target.position = reducer.position := by + have hd := (rowShiftedLeadingTerm?_some_data hpterm).2.1 + rw [hpivpos] at hd + exact Option.some.inj hd + split + · rename_i hlt + simp only [] + have hsetinv : ∀ p r, + (pivots.setIfInBounds target.position (some row)).getD p + none = some r → + r.size = n ∧ rowShiftedLeadingPosition? r shift = some p := by + intro p r hget' + rw [array_getD_setIfInBounds] at hget' + by_cases hp' : target.position = p ∧ + target.position < pivots.size + · rw [if_pos hp'] at hget' + cases hget' + exact ⟨hrow, hp'.1 ▸ hrowpos⟩ + · rw [if_neg hp'] at hget' + exact hinv p r hget' + have hredsize : + (cancelKernelLeafLeadingTerm pivot row shift).size = n := by + rw [cancelKernelLeafLeadingTerm_size (by omega)] + exact hpivn + have hih := ih (pivots.setIfInBounds target.position (some row)) + shift (cancelKernelLeafLeadingTerm pivot row shift) + (by rw [Array.size_setIfInBounds]; exact hpsize) hredsize + hsetinv + have hmeas := pivotTableMeasure_setIfInBounds pivots shift hb row + rw [hpiv, pivotEntryMeasure_some] at hmeas + have hdec : + shiftedRowMeasure (cancelKernelLeafLeadingTerm pivot row + shift) shift < shiftedRowMeasure pivot shift := + cancelKernelLeafLeadingTerm_shiftedRowMeasure_lt hpterm hterm + hredpos.symm (Nat.le_of_lt hlt) (by omega) + omega + · rename_i hlt + simp only [] + have hredsize : + (cancelKernelLeafLeadingTerm row pivot shift).size = n := by + rw [cancelKernelLeafLeadingTerm_size (by omega)] + exact hrow + have hih := ih pivots shift + (cancelKernelLeafLeadingTerm row pivot shift) hpsize hredsize + hinv + have hdec : + shiftedRowMeasure (cancelKernelLeafLeadingTerm row pivot + shift) shift < shiftedRowMeasure row shift := + cancelKernelLeafLeadingTerm_shiftedRowMeasure_lt hterm hpterm + hredpos (Nat.le_of_not_lt hlt) (by omega) + omega + +/-- Pivot-table insertion keeps the carried row and every stored row inside +the row span of the resulting pivot rows. The fuel hypothesis is the exact +measure bound consumed by the recursion. -/ +private theorem insertKernelLeafPivotRowWithFuel_rowSpan [DecidableEq F] + {n : Nat} : + ∀ (fuel : Nat) (pivots : Array (Option (PolynomialRow F))) + (shift : Array Nat) (row : PolynomialRow F), + pivots.size = n → + row.size = n → + (∀ p r, pivots.getD p none = some r → + r.size = n ∧ rowShiftedLeadingPosition? r shift = some p) → + shiftedRowMeasure row shift + pivotTableMeasure pivots shift < fuel → + (∀ p r, pivots.getD p none = some r → + r ∈ RowSpan + (pivotRows + (insertKernelLeafPivotRowWithFuel fuel pivots shift row))) ∧ + ((¬ RowIsZero row ∨ ∃ q s, pivots.getD q none = some s) → + row ∈ RowSpan + (pivotRows + (insertKernelLeafPivotRowWithFuel fuel pivots shift row))) := by + intro fuel + induction fuel with + | zero => + intro pivots shift row _ _ _ hfuel + exact absurd hfuel (Nat.not_lt_zero _) + | succ fuel ih => + intro pivots shift row hpsize hrow hinv hfuel + rw [insertKernelLeafPivotRowWithFuel] + split + · rename_i hterm + refine ⟨fun p r hget ↦ + stored_mem_rowSpan_pivotRows (fun p r h ↦ (hinv p r h).1) hget, ?_⟩ + intro hcase + have hz : RowIsZero row := + rowIsZero_of_rowShiftedLeadingTerm?_eq_none hterm + rcases hcase with hnz | ⟨q, s, hqs⟩ + · exact absurd hz hnz + · have hrow0 : row = zeroRow n := by + rw [← hrow] + exact rowIsZero_eq_zeroRow hz + rw [hrow0] + exact zeroRow_mem_rowSpan_pivotRows (fun p r h ↦ (hinv p r h).1) hqs + · rename_i target hterm + have hrowpos : rowShiftedLeadingPosition? row shift = + some target.position := + (rowShiftedLeadingTerm?_some_data hterm).2.1 + have hb : target.position < pivots.size := by + have := rowShiftedLeadingPosition?_lt hrowpos + omega + have hsetinv : ∀ p r, + (pivots.setIfInBounds target.position (some row)).getD p none = + some r → + r.size = n ∧ rowShiftedLeadingPosition? r shift = some p := by + intro p r hget' + rw [array_getD_setIfInBounds] at hget' + by_cases hp' : target.position = p ∧ target.position < pivots.size + · rw [if_pos hp'] at hget' + cases hget' + exact ⟨hrow, hp'.1 ▸ hrowpos⟩ + · rw [if_neg hp'] at hget' + exact hinv p r hget' + have hgetrow : + (pivots.setIfInBounds target.position (some row)).getD + target.position none = some row := by + rw [array_getD_setIfInBounds, if_pos ⟨rfl, hb⟩] + split + · rename_i hpiv + refine ⟨?_, fun _ ↦ + stored_mem_rowSpan_pivotRows (fun p r h ↦ (hsetinv p r h).1) + hgetrow⟩ + intro p r hget + have hpne : ¬ (target.position = p ∧ + target.position < pivots.size) := by + rintro ⟨hpe, -⟩ + rw [← hpe, hpiv] at hget + cases hget + have hget' : + (pivots.setIfInBounds target.position (some row)).getD p none = + some r := by + rw [array_getD_setIfInBounds, if_neg hpne] + exact hget + exact stored_mem_rowSpan_pivotRows (fun p r h ↦ (hsetinv p r h).1) + hget' + · rename_i pivot hpiv + have hpivn : pivot.size = n := (hinv target.position pivot hpiv).1 + have hpivpos : rowShiftedLeadingPosition? pivot shift = + some target.position := (hinv target.position pivot hpiv).2 + split + · rename_i hpterm + rcases rowShiftedLeadingTerm?_some_of_position hpivpos with + ⟨term, hterm', _⟩ + rw [hterm'] at hpterm + cases hpterm + · rename_i reducer hpterm + have hredpos : target.position = reducer.position := by + have hd := (rowShiftedLeadingTerm?_some_data hpterm).2.1 + rw [hpivpos] at hd + exact Option.some.inj hd + split + · rename_i hlt + simp only [] + have hredsize : + (cancelKernelLeafLeadingTerm pivot row shift).size = n := by + rw [cancelKernelLeafLeadingTerm_size (by omega)] + exact hpivn + have hmeas := pivotTableMeasure_setIfInBounds pivots shift hb row + rw [hpiv, pivotEntryMeasure_some] at hmeas + have hdec : + shiftedRowMeasure (cancelKernelLeafLeadingTerm pivot row + shift) shift < shiftedRowMeasure pivot shift := + cancelKernelLeafLeadingTerm_shiftedRowMeasure_lt hpterm hterm + hredpos.symm (Nat.le_of_lt hlt) (by omega) + have hIH := ih (pivots.setIfInBounds target.position (some row)) + shift (cancelKernelLeafLeadingTerm pivot row shift) + (by rw [Array.size_setIfInBounds]; exact hpsize) hredsize + hsetinv (by omega) + have hrowspan := hIH.1 target.position row hgetrow + have hredspan := hIH.2 + (Or.inr ⟨target.position, row, hgetrow⟩) + have hpivspan := + cancelKernelLeafLeadingTerm_target_mem_rowSpan hredspan + hrowspan (by omega) + refine ⟨?_, fun _ ↦ hrowspan⟩ + intro p r hget + by_cases hp : p = target.position + · subst hp + rw [hpiv] at hget + cases hget + exact hpivspan + · have hpne : ¬ (target.position = p ∧ + target.position < pivots.size) := by + rintro ⟨hpe, -⟩ + exact hp hpe.symm + have hget' : + (pivots.setIfInBounds target.position (some row)).getD p + none = some r := by + rw [array_getD_setIfInBounds, if_neg hpne] + exact hget + exact hIH.1 p r hget' + · rename_i hlt + simp only [] + have hredsize : + (cancelKernelLeafLeadingTerm row pivot shift).size = n := by + rw [cancelKernelLeafLeadingTerm_size (by omega)] + exact hrow + have hdec : + shiftedRowMeasure (cancelKernelLeafLeadingTerm row pivot + shift) shift < shiftedRowMeasure row shift := + cancelKernelLeafLeadingTerm_shiftedRowMeasure_lt hterm hpterm + hredpos (Nat.le_of_not_lt hlt) (by omega) + have hIH := ih pivots shift + (cancelKernelLeafLeadingTerm row pivot shift) hpsize hredsize + hinv (by omega) + have hpivspan := hIH.1 target.position pivot hpiv + have hredspan := hIH.2 + (Or.inr ⟨target.position, pivot, hpiv⟩) + have hrowspan := + cancelKernelLeafLeadingTerm_target_mem_rowSpan hredspan + hpivspan (by omega) + exact ⟨fun p r hget ↦ hIH.1 p r hget, fun _ ↦ hrowspan⟩ + +/-- The row span of the empty matrix only contains the empty row. -/ +theorem eq_empty_of_mem_rowSpan_empty {x : PolynomialRow F} + (hx : x ∈ RowSpan (#[] : PolynomialMatrix F)) : x = #[] := by + rcases hx with ⟨coeffs, _, rfl⟩ + rfl + +/-- A nonempty pivot-row span certifies a stored slot. -/ +private theorem exists_entry_of_mem_rowSpan_pivotRows + {pivots : Array (Option (PolynomialRow F))} {x : PolynomialRow F} + (hx : x ∈ RowSpan (pivotRows pivots)) (hxne : x ≠ #[]) : + ∃ q s, pivots.getD q none = some s := by + by_cases h : (pivotRows pivots).size = 0 + · have hempty : pivotRows pivots = #[] := Array.eq_empty_of_size_eq_zero h + rw [hempty] at hx + exact absurd (eq_empty_of_mem_rowSpan_empty hx) hxne + · have h0 : 0 < (pivotRows pivots).size := Nat.pos_of_ne_zero h + have hmem : (pivotRows pivots)[0] ∈ MatrixRows (pivotRows pivots) := by + rw [MatrixRows] + exact Array.getElem_mem_toList h0 + exact pivotRows_invariant (fun _ ↦ ∃ q s, pivots.getD q none = some s) + (fun q s hqs ↦ ⟨q, s, hqs⟩) hmem + +/-- Lift membership in one pivot-row span to a later pivot-row span when every +stored row of the first table stays in the later span. -/ +private theorem rowSpan_pivotRows_trans [DecidableEq F] {n : Nat} + {pivots tableT : Array (Option (PolynomialRow F))} + (hsz : ∀ p r, pivots.getD p none = some r → r.size = n) + (hszT : ∀ p r, tableT.getD p none = some r → r.size = n) + (hmono : ∀ p r, pivots.getD p none = some r → + r ∈ RowSpan (pivotRows tableT)) + (hpersist : ∀ q s, pivots.getD q none = some s → + ∃ s', tableT.getD q none = some s') + {x : PolynomialRow F} (hxnz : ¬ RowIsZero x) + (hx : x ∈ RowSpan (pivotRows pivots)) : + x ∈ RowSpan (pivotRows tableT) := by + have hxne : x ≠ #[] := by + intro h + apply hxnz + rw [h] + intro p hp + simp at hp + rcases exists_entry_of_mem_rowSpan_pivotRows hx hxne with ⟨q, s, hqs⟩ + rcases hpersist q s hqs with ⟨s', hqs'⟩ + have hw1 : MatrixWidth (pivotRows pivots) = n := + matrixWidth_eq_of_first_row + (size_pos_of_mem_matrixRows (mem_pivotRows_of_getD hqs)) + (pivotRows_sizes hsz) + have hw2 : MatrixWidth (pivotRows tableT) = n := + matrixWidth_eq_of_first_row + (size_pos_of_mem_matrixRows (mem_pivotRows_of_getD hqs')) + (pivotRows_sizes hszT) + rcases hx with ⟨coeffs, _, rfl⟩ + exact rowLinearCombination_mem_rowSpan_of_rows_mem + (wellFormed_of_sizes (pivotRows_sizes hszT)) (hw1.trans hw2.symm) + (fun r hr ↦ pivotRows_invariant _ hmono hr) coeffs + +/-- Folded pivot-table insertion preserves the table invariant. -/ +theorem insertKernelLeaf_foldl_pivotInv {n : Nat} : + ∀ (l : List (PolynomialRow F)) (fuel : Nat) + (pivots : Array (Option (PolynomialRow F))) (shift : Array Nat), + (∀ r ∈ l, r.size = n) → + (∀ p r, pivots.getD p none = some r → + r.size = n ∧ rowShiftedLeadingPosition? r shift = some p) → + ∀ p r, + (l.foldl (fun pv r ↦ insertKernelLeafPivotRowWithFuel fuel pv shift r) + pivots).getD p none = some r → + r.size = n ∧ rowShiftedLeadingPosition? r shift = some p := by + intro l + induction l with + | nil => + intro fuel pivots shift _ hinv p r hget + exact hinv p r hget + | cons head tail ihl => + intro fuel pivots shift hl hinv p r hget + rw [List.foldl_cons] at hget + exact ihl fuel _ shift (fun r hr ↦ hl r (List.mem_cons_of_mem head hr)) + (insertKernelLeafPivotRowWithFuel_pivotInv fuel pivots shift head + (hl head List.mem_cons_self) hinv) p r hget + +/-- Folded pivot-table insertion never erases an occupied slot. -/ +private theorem insertKernelLeaf_foldl_persist : + ∀ (l : List (PolynomialRow F)) (fuel : Nat) + (pivots : Array (Option (PolynomialRow F))) (shift : Array Nat) + {q : Nat} {s : PolynomialRow F}, + pivots.getD q none = some s → + ∃ s', + (l.foldl (fun pv r ↦ insertKernelLeafPivotRowWithFuel fuel pv shift r) + pivots).getD q none = some s' := by + intro l + induction l with + | nil => + intro fuel pivots shift q s hq + exact ⟨s, hq⟩ + | cons head tail ihl => + intro fuel pivots shift q s hq + rw [List.foldl_cons] + rcases insertKernelLeafPivotRowWithFuel_persist fuel pivots shift head hq + with ⟨s', hs'⟩ + exact ihl fuel _ shift hs' + +/-- Folded pivot-table insertion keeps every inserted nonzero row and every +initially stored row inside the row span of the final pivot rows. -/ +private theorem insertKernelLeaf_foldl_rowSpan [DecidableEq F] {n : Nat} : + ∀ (l : List (PolynomialRow F)) (fuel : Nat) + (pivots : Array (Option (PolynomialRow F))) (shift : Array Nat), + pivots.size = n → + (∀ r ∈ l, r.size = n) → + (∀ p r, pivots.getD p none = some r → + r.size = n ∧ rowShiftedLeadingPosition? r shift = some p) → + pivotTableMeasure pivots shift + + (l.map (fun r ↦ shiftedRowMeasure r shift)).sum < fuel → + (∀ p r, pivots.getD p none = some r → + r ∈ RowSpan (pivotRows + (l.foldl (fun pv r ↦ insertKernelLeafPivotRowWithFuel fuel pv shift r) + pivots))) ∧ + (∀ r ∈ l, ¬ RowIsZero r → + r ∈ RowSpan (pivotRows + (l.foldl (fun pv r ↦ insertKernelLeafPivotRowWithFuel fuel pv shift r) + pivots))) := by + intro l + induction l with + | nil => + intro fuel pivots shift _ _ hinv _ + refine ⟨fun p r hget ↦ + stored_mem_rowSpan_pivotRows (fun p r h ↦ (hinv p r h).1) hget, ?_⟩ + intro r hr + simp at hr + | cons head tail ihl => + intro fuel pivots shift hpsize hl hinv hfuel + rw [List.map_cons, List.sum_cons] at hfuel + have hheadsize : head.size = n := hl head List.mem_cons_self + have htailsizes : ∀ r ∈ tail, r.size = n := + fun r hr ↦ hl r (List.mem_cons_of_mem head hr) + rw [List.foldl_cons] + set pivots₁ := insertKernelLeafPivotRowWithFuel fuel pivots shift head + with hp₁ + have hsize₁ : pivots₁.size = n := by + rw [hp₁, insertKernelLeafPivotRowWithFuel_size] + exact hpsize + have hinv₁ : ∀ p r, pivots₁.getD p none = some r → + r.size = n ∧ rowShiftedLeadingPosition? r shift = some p := + insertKernelLeafPivotRowWithFuel_pivotInv fuel pivots shift head + hheadsize hinv + have hm₁ : pivotTableMeasure pivots₁ shift ≤ + shiftedRowMeasure head shift + pivotTableMeasure pivots shift := + insertKernelLeafPivotRowWithFuel_measure_le fuel pivots shift head + hpsize hheadsize hinv + have hC := insertKernelLeafPivotRowWithFuel_rowSpan fuel pivots shift + head hpsize hheadsize hinv (by omega) + have hIH := ihl fuel pivots₁ shift hsize₁ htailsizes hinv₁ (by omega) + have htrans : ∀ {x : PolynomialRow F}, ¬ RowIsZero x → + x ∈ RowSpan (pivotRows pivots₁) → + x ∈ RowSpan (pivotRows + (tail.foldl + (fun pv r ↦ insertKernelLeafPivotRowWithFuel fuel pv shift r) + pivots₁)) := by + intro x hxnz hx + exact rowSpan_pivotRows_trans (fun p r h ↦ (hinv₁ p r h).1) + (fun p r h ↦ + (insertKernelLeaf_foldl_pivotInv tail fuel pivots₁ shift + htailsizes hinv₁ p r h).1) + hIH.1 + (fun q s hqs ↦ + insertKernelLeaf_foldl_persist tail fuel pivots₁ shift hqs) + hxnz hx + constructor + · intro p r hget + have hrnz : ¬ RowIsZero r := + not_rowIsZero_of_rowShiftedLeadingPosition?_eq_some (hinv p r hget).2 + exact htrans hrnz (hC.1 p r hget) + · intro r hr hrnz + rcases List.mem_cons.mp hr with heq | hrtail + · subst heq + exact htrans hrnz (hC.2 (Or.inl hrnz)) + · exact hIH.2 r hrtail hrnz + +/-- The max-degree fold step used by `reduceKernelLeafFuel`. -/ +private def kernelLeafDegreeStep (rows : PolynomialMatrix F) + (shift : Array Nat) (acc : Nat) (i : Nat) : Nat := + match rowShiftedDegree? (rows.getD i #[]) shift with + | none => acc + | some degree => max acc degree + +omit [LawfulBEq F] in +private theorem reduceKernelLeafFuel_eq (rows : PolynomialMatrix F) + (shift : Array Nat) : + reduceKernelLeafFuel rows shift = + (rows.size + 1) * (MatrixWidth rows + 1) * + ((List.range rows.size).foldl (kernelLeafDegreeStep rows shift) 0 + + 1) := rfl + +omit [LawfulBEq F] in +private theorem kernelLeafDegreeStep_fold_le_acc (rows : PolynomialMatrix F) + (shift : Array Nat) : + ∀ (xs : List Nat) (acc : Nat), + acc ≤ xs.foldl (kernelLeafDegreeStep rows shift) acc := by + intro xs + induction xs with + | nil => intro acc; exact Nat.le_refl acc + | cons x xs ihx => + intro acc + rw [List.foldl_cons] + cases hx : rowShiftedDegree? (rows.getD x #[]) shift with + | none => + have hstep : kernelLeafDegreeStep rows shift acc x = acc := by + simp only [kernelLeafDegreeStep, hx] + rw [hstep] + exact ihx acc + | some d => + have hstep : kernelLeafDegreeStep rows shift acc x = max acc d := by + simp only [kernelLeafDegreeStep, hx] + rw [hstep] + exact Nat.le_trans (Nat.le_max_left acc d) (ihx (max acc d)) + +omit [LawfulBEq F] in +private theorem kernelLeafDegreeStep_fold_bound (rows : PolynomialMatrix F) + (shift : Array Nat) : + ∀ (xs : List Nat) (acc : Nat) {i d : Nat}, + i ∈ xs → + rowShiftedDegree? (rows.getD i #[]) shift = some d → + d ≤ xs.foldl (kernelLeafDegreeStep rows shift) acc := by + intro xs + induction xs with + | nil => + intro acc i d hi _ + cases hi + | cons x xs ihx => + intro acc i d hi hdeg + rw [List.foldl_cons] + rcases List.mem_cons.mp hi with heq | hi' + · have hdeg' : rowShiftedDegree? (rows.getD x #[]) shift = some d := + heq ▸ hdeg + have hstep : kernelLeafDegreeStep rows shift acc x = max acc d := by + simp only [kernelLeafDegreeStep, hdeg'] + rw [hstep] + exact Nat.le_trans (Nat.le_max_right acc d) + (kernelLeafDegreeStep_fold_le_acc rows shift xs (max acc d)) + · exact ihx _ hi' hdeg + +omit [LawfulBEq F] in +/-- Total shifted measure of all matrix rows is below the kernel-leaf fuel. -/ +private theorem sum_shiftedRowMeasure_lt_reduceKernelLeafFuel [DecidableEq F] + {rows : PolynomialMatrix F} {shift : Array Nat} {n : Nat} + (hsizes : ∀ r ∈ MatrixRows rows, r.size = n) + (hw : MatrixWidth rows = n) : + (rows.toList.map (fun r ↦ shiftedRowMeasure r shift)).sum < + reduceKernelLeafFuel rows shift := by + set D := (List.range rows.size).foldl (kernelLeafDegreeStep rows shift) 0 + with hD + have hbound : ∀ r ∈ rows.toList, + shiftedRowMeasure r shift ≤ (D + 1) * (n + 1) := by + intro r hr + rcases List.getElem_of_mem hr with ⟨i, hi, hget⟩ + have hi' : i < rows.size := by simpa using hi + have hgetD : rows.getD i #[] = r := by + rw [array_getD_of_lt' _ _ hi', + show rows[i] = rows.toList[i] from (Array.getElem_toList hi').symm] + exact hget + have hrsize : r.size = n := hsizes r hr + have hmle := shiftedRowMeasure_le_of_degree_bound + (row := r) (shift := shift) (d := D) ?_ + · rw [hrsize] at hmle + exact hmle + · intro rowDeg hdeg + refine kernelLeafDegreeStep_fold_bound rows shift (List.range rows.size) + 0 (List.mem_range.mpr hi') ?_ + rw [hgetD] + exact hdeg + have hsum := List.sum_le_card_nsmul + (rows.toList.map (fun r ↦ shiftedRowMeasure r shift)) ((D + 1) * (n + 1)) + (by + intro x hx + rcases List.mem_map.mp hx with ⟨r, hr, rfl⟩ + exact hbound r hr) + rw [List.length_map, Array.length_toList, smul_eq_mul] at hsum + rw [reduceKernelLeafFuel_eq, hw, ← hD] + calc (rows.toList.map (fun r ↦ shiftedRowMeasure r shift)).sum + ≤ rows.size * ((D + 1) * (n + 1)) := hsum + _ < (rows.size + 1) * ((D + 1) * (n + 1)) := + Nat.mul_lt_mul_of_pos_right (Nat.lt_succ_self rows.size) + (by positivity) + _ = (rows.size + 1) * (n + 1) * (D + 1) := by ring + +/-- The pivot-table reduction preserves the generated row module: every +nonzero source row stays inside the row span of the reduced matrix. -/ +theorem reduceKernelLeafRowsByPivots_rowSpan_superset [DecidableEq F] + {rows : PolynomialMatrix F} {shift : Array Nat} {n : Nat} + (hsizes : ∀ r ∈ MatrixRows rows, r.size = n) + {row : PolynomialRow F} (hrow : row ∈ MatrixRows rows) + (hnz : ¬ RowIsZero row) : + row ∈ RowSpan (reduceKernelLeafRowsByPivots rows shift) := by + have hpos : 0 < rows.size := size_pos_of_mem_matrixRows hrow + have hw : MatrixWidth rows = n := matrixWidth_eq_of_first_row hpos hsizes + rw [reduceKernelLeafRowsByPivots] + rw [← Array.foldl_toList] + refine (insertKernelLeaf_foldl_rowSpan (n := n) rows.toList + (reduceKernelLeafFuel rows shift) + (Array.replicate (MatrixWidth rows) none) shift ?_ ?_ ?_ ?_).2 row hrow hnz + · rw [Array.size_replicate] + exact hw + · exact fun r hr ↦ hsizes r hr + · intro p r hget + rcases Nat.lt_or_ge p + (Array.replicate (MatrixWidth rows) + (none : Option (PolynomialRow F))).size with hp | hp + · rw [array_getD_of_lt' _ _ hp, Array.getElem_replicate] at hget + cases hget + · rw [array_getD_of_le' _ _ hp] at hget + cases hget + · rw [pivotTableMeasure_replicate, Nat.zero_add] + exact sum_shiftedRowMeasure_lt_reduceKernelLeafFuel hsizes hw + +/-- Reduced kernel-leaf rows keep the uniform width and are nonzero. -/ +theorem reduceKernelLeafRowsByPivots_rows {n : Nat} + {rows : PolynomialMatrix F} {shift : Array Nat} + (hsizes : ∀ r ∈ MatrixRows rows, r.size = n) : + ∀ r ∈ MatrixRows (reduceKernelLeafRowsByPivots rows shift), + r.size = n ∧ rowIsZero r = false := by + intro r hr + rw [reduceKernelLeafRowsByPivots, ← Array.foldl_toList] at hr + have hinit : ∀ p r, + (Array.replicate (MatrixWidth rows) + (none : Option (PolynomialRow F))).getD p none = some r → + r.size = n ∧ rowShiftedLeadingPosition? r shift = some p := by + intro p r hget + rcases Nat.lt_or_ge p + (Array.replicate (MatrixWidth rows) + (none : Option (PolynomialRow F))).size with hp | hp + · rw [array_getD_of_lt' _ _ hp, Array.getElem_replicate] at hget + cases hget + · rw [array_getD_of_le' _ _ hp] at hget + cases hget + have hinv := insertKernelLeaf_foldl_pivotInv (n := n) rows.toList + (reduceKernelLeafFuel rows shift) + (Array.replicate (MatrixWidth rows) none) shift + (fun r hr ↦ hsizes r hr) hinit + refine pivotRows_invariant + (fun r ↦ r.size = n ∧ rowIsZero r = false) ?_ hr + intro p r hget + refine ⟨(hinv p r hget).1, ?_⟩ + have hnz := not_rowIsZero_of_rowShiftedLeadingPosition?_eq_some + (hinv p r hget).2 + cases hb : rowIsZero r + · rfl + · exact absurd (rowIsZero_iff.1 hb) hnz + +/-- The incremental insertion step is the plain pivot reduction of the pushed +matrix: the nonzero-row filter never removes anything. -/ +private theorem insertKernelLeafRowIncremental_eq {n : Nat} + {basis : PolynomialMatrix F} {shift : Array Nat} {row : PolynomialRow F} + (hsizes : ∀ r ∈ MatrixRows (basis.push row), r.size = n) : + insertKernelLeafRowIncremental basis shift row = + reduceKernelLeafRowsByPivots (basis.push row) shift := by + rw [insertKernelLeafRowIncremental, reduceKernelLeafRows] + rw [Array.filter_eq_self] + intro r hr + have hr' : r ∈ MatrixRows (reduceKernelLeafRowsByPivots (basis.push row) + shift) := by + rw [MatrixRows] + exact Array.mem_def.mp hr + rw [(reduceKernelLeafRowsByPivots_rows hsizes r hr').2] + rfl + +omit [BEq F] [LawfulBEq F] in +/-- Rows of a matrix stay rows after a push. -/ +private theorem mem_matrixRows_push_left {basis : PolynomialMatrix F} + {row r : PolynomialRow F} (hr : r ∈ MatrixRows basis) : + r ∈ MatrixRows (basis.push row) := by + rw [MatrixRows, Array.toList_push] + exact List.mem_append.mpr (Or.inl hr) + +omit [BEq F] [LawfulBEq F] in +/-- The pushed row is a row of the pushed matrix. -/ +private theorem mem_matrixRows_push_self {basis : PolynomialMatrix F} + {row : PolynomialRow F} : row ∈ MatrixRows (basis.push row) := by + rw [MatrixRows, Array.toList_push] + exact List.mem_append.mpr (Or.inr List.mem_cons_self) + +omit [LawfulBEq F] in +/-- The empty row is a zero row. -/ +theorem rowIsZero_empty : RowIsZero (#[] : PolynomialRow F) := by + intro p hp + simp at hp + +/-- Incremental kernel-leaf insertion keeps every previously accounted nonzero +row and every inserted nonzero row inside the row span of the final basis. -/ +private theorem insertIncremental_foldl_rowSpan [DecidableEq F] {n : Nat} : + ∀ (l : List (PolynomialRow F)) (basis : PolynomialMatrix F) + (shift : Array Nat), + (∀ r ∈ l, r.size = n) → + (∀ r ∈ MatrixRows basis, r.size = n) → + (∀ r ∈ MatrixRows basis, rowIsZero r = false) → + (∀ x : PolynomialRow F, ¬ RowIsZero x → x ∈ RowSpan basis → + x ∈ RowSpan (l.foldl + (fun b r ↦ insertKernelLeafRowIncremental b shift r) basis)) ∧ + (∀ r ∈ l, ¬ RowIsZero r → + r ∈ RowSpan (l.foldl + (fun b r ↦ insertKernelLeafRowIncremental b shift r) basis)) := by + intro l + induction l with + | nil => + intro basis shift _ _ _ + refine ⟨fun x _ hx ↦ hx, ?_⟩ + intro r hr + simp at hr + | cons head tail ihl => + intro basis shift hl hbsz hbnz + have hheadsize : head.size = n := hl head List.mem_cons_self + have htailsizes : ∀ r ∈ tail, r.size = n := + fun r hr ↦ hl r (List.mem_cons_of_mem head hr) + rw [List.foldl_cons] + have hpushsz : ∀ r ∈ MatrixRows (basis.push head), r.size = n := by + intro r hr + rcases mem_matrixRows_push hr with hr' | rfl + · exact hbsz r hr' + · exact hheadsize + set basis₁ := insertKernelLeafRowIncremental basis shift head with hb₁ + have hb₁eq : + basis₁ = reduceKernelLeafRowsByPivots (basis.push head) shift := + insertKernelLeafRowIncremental_eq hpushsz + have hb₁sz : ∀ r ∈ MatrixRows basis₁, r.size = n := by + rw [hb₁eq] + exact fun r hr ↦ (reduceKernelLeafRowsByPivots_rows hpushsz r hr).1 + have hb₁nz : ∀ r ∈ MatrixRows basis₁, rowIsZero r = false := by + rw [hb₁eq] + exact fun r hr ↦ (reduceKernelLeafRowsByPivots_rows hpushsz r hr).2 + have hIH := ihl basis₁ shift htailsizes hb₁sz hb₁nz + have hstep : ∀ r ∈ MatrixRows (basis.push head), ¬ RowIsZero r → + r ∈ RowSpan basis₁ := by + intro r hr hrnz + rw [hb₁eq] + exact reduceKernelLeafRowsByPivots_rowSpan_superset hpushsz hr hrnz + have hbasisnz : ∀ r ∈ MatrixRows basis, ¬ RowIsZero r := by + intro r hr hz + have hzb := rowIsZero_iff.2 hz + rw [hbnz r hr] at hzb + cases hzb + have hA : ∀ x : PolynomialRow F, ¬ RowIsZero x → x ∈ RowSpan basis → + x ∈ RowSpan basis₁ := by + intro x hxnz hx + by_cases hbsize : basis.size = 0 + · have hbempty : basis = #[] := + Array.eq_empty_of_size_eq_zero hbsize + rw [hbempty] at hx + have hxempty := eq_empty_of_mem_rowSpan_empty hx + rw [hxempty] at hxnz + exact absurd rowIsZero_empty hxnz + · have hbpos : 0 < basis.size := Nat.pos_of_ne_zero hbsize + have hwb : MatrixWidth basis = n := + matrixWidth_eq_of_first_row hbpos hbsz + have hb0mem : basis[0] ∈ MatrixRows basis := by + rw [MatrixRows] + exact Array.getElem_mem_toList hbpos + have hb0nz : ¬ RowIsZero basis[0] := hbasisnz basis[0] hb0mem + have hb0span : basis[0] ∈ RowSpan basis₁ := + hstep basis[0] (mem_matrixRows_push_left hb0mem) hb0nz + have hb₁pos : 0 < basis₁.size := by + rcases Nat.eq_zero_or_pos basis₁.size with h0 | hpos' + · have hb₁empty : basis₁ = #[] := + Array.eq_empty_of_size_eq_zero h0 + rw [hb₁empty] at hb0span + have h0eq := eq_empty_of_mem_rowSpan_empty hb0span + rw [h0eq] at hb0nz + exact absurd rowIsZero_empty hb0nz + · exact hpos' + have hwb₁ : MatrixWidth basis₁ = n := + matrixWidth_eq_of_first_row hb₁pos hb₁sz + rcases hx with ⟨coeffs, _, rfl⟩ + refine rowLinearCombination_mem_rowSpan_of_rows_mem + (wellFormed_of_sizes hb₁sz) (hwb.trans hwb₁.symm) ?_ coeffs + intro r hr + exact hstep r (mem_matrixRows_push_left hr) (hbasisnz r hr) + constructor + · intro x hxnz hx + exact hIH.1 x hxnz (hA x hxnz hx) + · intro r hr hrnz + rcases List.mem_cons.mp hr with heq | hrtail + · subst heq + exact hIH.1 r hrnz (hstep r mem_matrixRows_push_self hrnz) + · exact hIH.2 r hrtail hrnz + +/-- The incremental kernel-leaf reduction preserves the generated row module: +every nonzero source row stays inside the row span of the reduced basis. -/ +theorem reduceKernelLeafRowsIncremental_rowSpan_superset [DecidableEq F] + {rows : PolynomialMatrix F} {shift : Array Nat} {n : Nat} + (hsizes : ∀ r ∈ MatrixRows rows, r.size = n) + {row : PolynomialRow F} (hrow : row ∈ MatrixRows rows) + (hnz : ¬ RowIsZero row) : + row ∈ RowSpan (reduceKernelLeafRowsIncremental rows shift) := by + rw [reduceKernelLeafRowsIncremental, ← Array.foldl_toList] + refine (insertIncremental_foldl_rowSpan (n := n) rows.toList #[] shift + (fun r hr ↦ hsizes r hr) ?_ ?_).2 row hrow hnz + · intro r hr + simp [MatrixRows] at hr + · intro r hr + simp [MatrixRows] at hr + +end Approximant + +end PolynomialMatrix + +end CompPoly diff --git a/CompPoly/LinearAlgebra/PolynomialMatrix/Approximant/PMBasis/Recursion.lean b/CompPoly/LinearAlgebra/PolynomialMatrix/Approximant/PMBasis/Recursion.lean new file mode 100644 index 00000000..97be4835 --- /dev/null +++ b/CompPoly/LinearAlgebra/PolynomialMatrix/Approximant/PMBasis/Recursion.lean @@ -0,0 +1,185 @@ +/- +Copyright (c) 2026 CompPoly Contributors. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Valerii Huhnin +-/ + +module + +public import CompPoly.LinearAlgebra.PolynomialMatrix.Approximant.PMBasis.KernelLeaf + +/-! +# Recursive PM-Basis Driver + +Runtime data for recursive PM-basis computation, the fuel-bounded +divide-and-conquer driver, root normalization, the `pmBasis` entry point, and +the `PMBasisContext` contract structure. + +## References + +* [Beckermann, B., and Labahn, G., *A uniform approach for the fast + computation of matrix-type Pade approximants*][BL94] +* [Giorgi, P., Jeannerod, C.-P., and Villard, G., *On the complexity of + polynomial matrix computations*][GJV03] +-/ + +@[expose] public section + +namespace CompPoly + +namespace PolynomialMatrix + +namespace Approximant + +variable {F : Type*} [Field F] [BEq F] [LawfulBEq F] + +/-- Runtime data for recursive PM-basis computation. -/ +structure PMBasisRuntime (F : Type*) [Field F] [BEq F] [LawfulBEq F] where + mulContext : CPolynomial.MulContext F + lowMulContext : PolynomialMatrix.MulLowContext F + composeBasis : PolynomialMatrix F → PolynomialMatrix F → PolynomialMatrix F + residualProduct : + Array Nat → PolynomialMatrix F → PolynomialMatrix F → PolynomialMatrix F + leafCutoff : Nat + leafBasis : XAdicProblem F → Array Nat → PolynomialMatrix F + +/-- Recursive PM-basis runtime using the scalar dense-kernel routine as its +small-leaf solver and an independently tuned basis-composition cutoff. -/ +def kernelLeafRuntimeWithLowAndCompose (mulCtx : CPolynomial.MulContext F) + (lowCtx : PolynomialMatrix.MulLowContext F) + (leafCutoff composeLeafCutoff : Nat) : + PMBasisRuntime F where + mulContext := mulCtx + lowMulContext := lowCtx + composeBasis := PolynomialMatrix.mulStrassenWith lowCtx composeLeafCutoff + residualProduct := PolynomialMatrix.mulTruncColumnStrassenWith lowCtx + composeLeafCutoff + leafCutoff := leafCutoff + leafBasis := kernelLeafBasis + +/-- Recursive PM-basis runtime using the scalar dense-kernel routine as its +small-leaf solver. -/ +def kernelLeafRuntimeWithLow (mulCtx : CPolynomial.MulContext F) + (lowCtx : PolynomialMatrix.MulLowContext F) (leafCutoff : Nat) : + PMBasisRuntime F := + kernelLeafRuntimeWithLowAndCompose mulCtx lowCtx leafCutoff leafCutoff + +/-- Compatibility runtime whose low products are obtained by truncating full +products. -/ +def kernelLeafRuntime (mulCtx : CPolynomial.MulContext F) (leafCutoff : Nat) : + PMBasisRuntime F := + kernelLeafRuntimeWithLow mulCtx (PolynomialMatrix.MulLowContext.fromMulContext mulCtx) + leafCutoff + +/-- Fuel-bounded recursive PM-basis driver, compacting zero rows after each +leaf and composition step. + +Internal nodes return the composed product `P₂ * P₁` without re-reduction: +composition of minimal half-bases under the updated shift is itself a basis of +the full-order approximant module, so re-reducing at every node would only add +work that is quadratic in the row degrees and outside the PM-basis cost model. +A single weak-Popov normalization pass runs once at the root entry points. -/ +def pmBasisWithFuelCore (runtime : PMBasisRuntime F) : + Nat → XAdicProblem F → Array Nat → PolynomialMatrix F + | 0, problem, shift => compactNonzeroRows (runtime.leafBasis problem shift) + | fuel + 1, problem, shift => + let order := maxOrder problem + if order ≤ runtime.leafCutoff || order ≤ 1 then + compactNonzeroRows (runtime.leafBasis problem shift) + else + let d₁ := order / 2 + let lower : XAdicProblem F := + { orders := lowerOrders problem d₁, matrix := problem.matrix } + let P₁ := pmBasisWithFuelCore runtime fuel lower shift + let residualOrders := residualOrders problem d₁ + let residual : XAdicProblem F := + { orders := residualOrders + matrix := residualMatrixWithProduct runtime.residualProduct P₁ + problem.matrix d₁ residualOrders } + let shifted := updateShiftByRows P₁ shift + let P₂ := pmBasisWithFuelCore runtime fuel residual shifted + compactNonzeroRows (runtime.composeBasis P₂ P₁) + +/-- Root normalization for a recursively composed approximant basis: one +weak-Popov reduction pass plus completion rows for any leading position that +lost its representative. When the recursion preserved minimality this pass +performs no cascading cancellations; it is a semantic guard, not part of the +recursive cost model. -/ +def pmBasisNormalizeRoot (problem : XAdicProblem F) (shift : Array Nat) + (basis : PolynomialMatrix F) : PolynomialMatrix F := + completeMissingPivotRows problem shift + (compactNonzeroRows (reduceKernelLeafRows basis shift)) + +/-- Fuel-bounded recursive PM-basis driver with root normalization. -/ +def pmBasisWithFuel (runtime : PMBasisRuntime F) + (fuel : Nat) (problem : XAdicProblem F) (shift : Array Nat) : + PolynomialMatrix F := + pmBasisNormalizeRoot problem shift (pmBasisWithFuelCore runtime fuel problem shift) + +/-- Default fuel choice, large enough to split each positive order down to the +leaf cutoff. -/ +def pmBasisFuel (problem : XAdicProblem F) : Nat := + maxOrder problem + 1 + +/-- Recursive PM-basis entry point. -/ +def pmBasis (runtime : PMBasisRuntime F) + (problem : XAdicProblem F) (shift : Array Nat) : PolynomialMatrix F := + pmBasisWithFuel runtime (pmBasisFuel problem) problem shift + +/-- Monomial completion never returns an empty matrix for problems with at +least one module row: an empty candidate set is completed with one monomial +row per module coordinate. -/ +theorem completeMissingPivotRows_size_pos + (problem : XAdicProblem F) (shift : Array Nat) (rows : PolynomialMatrix F) + (hsize : 0 < problem.matrix.size) : + 0 < (completeMissingPivotRows problem shift rows).size := by + rw [completeMissingPivotRows, Array.size_append] + by_cases hrows : rows.size = 0 + · have hempty : rows = #[] := Array.eq_empty_of_size_eq_zero hrows + subst hempty + have hmissing : (missingCompletionRows problem shift (#[] : PolynomialMatrix F)).size = + problem.matrix.size := by + simp [missingCompletionRows, rowsContainLeadingPosition] + omega + · omega + +/-- The root-normalized recursive PM-basis is nonempty for problems with at +least one module row. -/ +theorem pmBasisNormalizeRoot_size_pos + (problem : XAdicProblem F) (shift : Array Nat) (basis : PolynomialMatrix F) + (hsize : 0 < problem.matrix.size) : + 0 < (pmBasisNormalizeRoot problem shift basis).size := by + rw [pmBasisNormalizeRoot] + exact completeMissingPivotRows_size_pos problem shift _ hsize + +/-- Context packaging the executable PM-basis operation with theorem fields. -/ +structure PMBasisContext (F : Type*) [Field F] [BEq F] [LawfulBEq F] where + runtime : PMBasisRuntime F + basis : XAdicProblem F → Array Nat → PolynomialMatrix F := pmBasis runtime + sound : + ∀ problem shift row, + row ∈ MatrixRows (basis problem shift) → + ∀ j, j < problem.orders.size → + truncateX (problem.orders.getD j 0) + (rowGet (rowMulMatrixWith runtime.mulContext row problem.matrix) j) = 0 + complete_minimal : + ∀ problem shift row, + 0 < problem.matrix.size → + WellFormed problem.matrix → + (∀ j, j < problem.orders.size → + truncateX (problem.orders.getD j 0) + (rowGet (rowMulMatrixWith runtime.mulContext row problem.matrix) j) = 0) → + rowIsZero row = false → + row.size ≤ problem.matrix.size → + ∃ basisRow degree, + basisRow ∈ MatrixRows (basis problem shift) ∧ + basisRow.size ≤ problem.matrix.size ∧ + rowShiftedDegree? basisRow shift = some degree ∧ + ∀ rowDegree, rowShiftedDegree? row shift = some rowDegree → + degree ≤ rowDegree + +end Approximant + +end PolynomialMatrix + +end CompPoly diff --git a/CompPoly/LinearAlgebra/PolynomialMatrix/Approximant/PMBasis/XAdicSoundness.lean b/CompPoly/LinearAlgebra/PolynomialMatrix/Approximant/PMBasis/XAdicSoundness.lean new file mode 100644 index 00000000..7e1dfa76 --- /dev/null +++ b/CompPoly/LinearAlgebra/PolynomialMatrix/Approximant/PMBasis/XAdicSoundness.lean @@ -0,0 +1,795 @@ +/- +Copyright (c) 2026 CompPoly Contributors. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Valerii Huhnin +-/ + +module + +-- `coeff`, `ofArray` and friends are declared in bare `public section`s, so their +-- bodies are opaque downstream; see `docs/wiki/module-system.md`. +import all CompPoly.Univariate.Basic +import all CompPoly.Univariate.Raw.Core +public import CompPoly.LinearAlgebra.PolynomialMatrix.Approximant.PMBasis.KernelLeaf +public import CompPoly.LinearAlgebra.PolynomialMatrix.StrassenCorrectness +public import Mathlib.Algebra.BigOperators.Group.Finset.Basic + +/-! +# X-Adic Row Soundness Toolkit + +The `RowApproximates` predicate, its divisibility characterization, closure +under the row operations used by the leaf reduction and completion steps, and +the soundness of one divide-and-conquer composition step. +-/ + +@[expose] public section + +namespace CompPoly + +namespace PolynomialMatrix + +namespace Approximant + +variable {F : Type*} [Field F] [BEq F] [LawfulBEq F] + +/-! ## X-adic row soundness toolkit + +Semantic tools for proving that every row produced by the recursive PM-basis +driver satisfies the X-adic approximant conditions. Soundness is phrased via +`X^order` divisibility of the `toPoly` image, which makes it closed under the +row operations used by the reduction, completion, and composition steps. -/ + +/-- A row approximates an X-adic problem when every column product vanishes to +the required order. -/ +def RowApproximates (mulCtx : CPolynomial.MulContext F) + (problem : XAdicProblem F) (row : PolynomialRow F) : Prop := + ∀ j, j < problem.orders.size → + truncateX (problem.orders.getD j 0) + (rowGet (rowMulMatrixWith mulCtx row problem.matrix) j) = 0 + +/-- Truncation vanishes exactly on `X^order`-multiples under `toPoly`. -/ +theorem truncateX_eq_zero_iff_X_pow_dvd (order : Nat) (p : CPolynomial F) : + truncateX order p = 0 ↔ (Polynomial.X : Polynomial F) ^ order ∣ p.toPoly := by + rw [Polynomial.X_pow_dvd_iff] + constructor + · intro h t ht + have hcoeff := congrArg (fun q ↦ CPolynomial.coeff q t) h + simp only [truncateX_coeff, if_pos ht, CPolynomial.coeff_zero] at hcoeff + rw [← CPolynomial.coeff_toPoly] + exact hcoeff + · intro h + apply CPolynomial.eq_iff_coeff.2 + intro t + rw [truncateX_coeff, CPolynomial.coeff_zero] + split + · rename_i ht + rw [CPolynomial.coeff_toPoly] + exact h t ht + · rfl + +/-- Truncation of the zero polynomial is zero. -/ +theorem truncateX_zero_eq_zero (order : Nat) : + truncateX (F := F) order 0 = 0 := by + rw [truncateX_eq_zero_iff_X_pow_dvd, CPolynomial.toPoly_zero] + exact dvd_zero _ + +private theorem pm_foldl_add_eq_sum {M : Type*} [AddCommMonoid M] (f : Nat → M) : + ∀ n : Nat, + (List.range n).foldl (fun acc k ↦ acc + f k) 0 = ∑ k ∈ Finset.range n, f k := by + intro n + induction n with + | zero => simp + | succ n ih => + rw [List.range_succ, List.foldl_append, ih, List.foldl_cons, List.foldl_nil, + Finset.sum_range_succ] + +/-- `toPoly` commutes with finite range sums. -/ +theorem pm_toPoly_finset_sum (f : Nat → CPolynomial F) (n : Nat) : + (∑ k ∈ Finset.range n, f k).toPoly = ∑ k ∈ Finset.range n, (f k).toPoly := by + induction n with + | zero => + simp [CPolynomial.toPoly_zero] + | succ n ih => + rw [Finset.sum_range_succ, Finset.sum_range_succ, CPolynomial.toPoly_add, ih] + +private theorem rowMulMatrixWith_size (mulCtx : CPolynomial.MulContext F) + (row : PolynomialRow F) (M : PolynomialMatrix F) : + (rowMulMatrixWith mulCtx row M).size = MatrixWidth M := by + simp [rowMulMatrixWith] + +private theorem rowGet_rowMulMatrixWith_of_width_le (mulCtx : CPolynomial.MulContext F) + (row : PolynomialRow F) (M : PolynomialMatrix F) {j : Nat} + (hj : MatrixWidth M ≤ j) : + rowGet (rowMulMatrixWith mulCtx row M) j = 0 := by + rw [rowGet, Array.getD_eq_getD_getElem?, Array.getElem?_eq_none + (by rw [rowMulMatrixWith_size]; omega)] + rfl + +/-- Column entries of a row-by-matrix product as sums over the matrix height, +under `toPoly`. -/ +theorem rowGet_rowMulMatrixWith_toPoly (mulCtx : CPolynomial.MulContext F) + (row : PolynomialRow F) (M : PolynomialMatrix F) {j : Nat} + (hj : j < MatrixWidth M) : + (rowGet (rowMulMatrixWith mulCtx row M) j).toPoly = + ∑ k ∈ Finset.range M.size, + (rowGet row k).toPoly * (rowGet (M.getD k #[]) j).toPoly := by + rw [rowMulMatrixWith, rowGet, Array.getD_eq_getD_getElem?, List.getElem?_toArray, + List.getElem?_map, List.getElem?_range hj, Option.map_some, Option.getD_some] + rw [pm_foldl_add_eq_sum (fun k ↦ mulCtx.mul (rowGet row k) (rowGet (M.getD k #[]) j))] + have hsum : ∀ n : Nat, + (∑ k ∈ Finset.range n, + mulCtx.mul (rowGet row k) (rowGet (M.getD k #[]) j)).toPoly = + ∑ k ∈ Finset.range n, + (rowGet row k).toPoly * (rowGet (M.getD k #[]) j).toPoly := by + intro n + rw [pm_toPoly_finset_sum] + refine Finset.sum_congr rfl fun k _hk ↦ ?_ + rw [mulCtx.mul_eq_mul, CPolynomial.toPoly_mul] + rw [hsum] + rcases Nat.le_total row.size M.size with h | h + · refine Finset.sum_subset + (by intro x hx; simp only [Finset.mem_range] at hx ⊢; omega) + fun k _hk hknot ↦ ?_ + have hk : row.size ≤ k := by simpa using hknot + have hzero : rowGet row k = 0 := by + rw [rowGet, Array.getD_eq_getD_getElem?, Array.getElem?_eq_none hk] + rfl + rw [hzero, CPolynomial.toPoly_zero, zero_mul] + · symm + refine Finset.sum_subset + (by intro x hx; simp only [Finset.mem_range] at hx ⊢; omega) + fun k _hk hknot ↦ ?_ + have hk : M.size ≤ k := by simpa using hknot + have hzero : M.getD k #[] = #[] := by + rw [Array.getD_eq_getD_getElem?, Array.getElem?_eq_none hk] + rfl + rw [hzero, show rowGet (#[] : PolynomialRow F) j = 0 from rfl, + CPolynomial.toPoly_zero, mul_zero] + +/-- Divisibility form of the approximant condition, one column at a time. -/ +theorem rowApproximates_iff (mulCtx : CPolynomial.MulContext F) + (problem : XAdicProblem F) (row : PolynomialRow F) : + RowApproximates mulCtx problem row ↔ + ∀ j, j < problem.orders.size → j < MatrixWidth problem.matrix → + (Polynomial.X : Polynomial F) ^ (problem.orders.getD j 0) ∣ + ∑ k ∈ Finset.range problem.matrix.size, + (rowGet row k).toPoly * + (rowGet (problem.matrix.getD k #[]) j).toPoly := by + constructor + · intro h j hj hjw + have := h j hj + rw [truncateX_eq_zero_iff_X_pow_dvd, + rowGet_rowMulMatrixWith_toPoly mulCtx row problem.matrix hjw] at this + exact this + · intro h j hj + rcases Nat.lt_or_ge j (MatrixWidth problem.matrix) with hjw | hjw + · rw [truncateX_eq_zero_iff_X_pow_dvd, + rowGet_rowMulMatrixWith_toPoly mulCtx row problem.matrix hjw] + exact h j hj hjw + · rw [rowGet_rowMulMatrixWith_of_width_le mulCtx row problem.matrix hjw] + exact truncateX_zero_eq_zero _ + +/-- Row subtraction preserves the approximant condition. -/ +theorem rowApproximates_rowSub (mulCtx : CPolynomial.MulContext F) + (problem : XAdicProblem F) {a b : PolynomialRow F} + (ha : RowApproximates mulCtx problem a) + (hb : RowApproximates mulCtx problem b) : + RowApproximates mulCtx problem (rowSub a b) := by + rw [rowApproximates_iff] at ha hb ⊢ + intro j hj hjw + have hsum : ∑ k ∈ Finset.range problem.matrix.size, + (rowGet (rowSub a b) k).toPoly * + (rowGet (problem.matrix.getD k #[]) j).toPoly = + (∑ k ∈ Finset.range problem.matrix.size, + (rowGet a k).toPoly * (rowGet (problem.matrix.getD k #[]) j).toPoly) - + ∑ k ∈ Finset.range problem.matrix.size, + (rowGet b k).toPoly * (rowGet (problem.matrix.getD k #[]) j).toPoly := by + rw [← Finset.sum_sub_distrib] + refine Finset.sum_congr rfl fun k _hk ↦ ?_ + rw [rowGet_rowSub, CPolynomial.toPoly_sub, sub_mul] + rw [hsum] + exact dvd_sub (ha j hj hjw) (hb j hj hjw) + +private theorem polynomialScaleCoeffX_zero (c : F) (d : Nat) : + polynomialScaleCoeffX c d (0 : CPolynomial F) = 0 := by + rw [polynomialScaleCoeffX] + split + · rfl + · rw [if_pos (by simp)] + +/-- Coefficient-shift scaling under `toPoly`. -/ +theorem polynomialScaleCoeffX_toPoly (c : F) (d : Nat) (p : CPolynomial F) : + (polynomialScaleCoeffX c d p).toPoly = + Polynomial.C c * Polynomial.X ^ d * p.toPoly := by + rw [polynomialScaleCoeffX] + by_cases hc : c == 0 + · rw [if_pos hc] + have hc' : c = 0 := by simpa using hc + rw [hc', CPolynomial.toPoly_zero] + simp + · rw [if_neg hc] + by_cases hp : p == 0 + · rw [if_pos hp] + have hp' : p = 0 := by simpa using hp + rw [hp', CPolynomial.toPoly_zero] + simp + · rw [if_neg hp] + apply Polynomial.ext + intro t + rw [← CPolynomial.coeff_toPoly, CPolynomial.coeff_ofArray] + have hrhs : (Polynomial.C c * Polynomial.X ^ d * p.toPoly).coeff t = + if d ≤ t then c * (p.toPoly.coeff (t - d)) else 0 := by + rw [mul_assoc, Polynomial.coeff_C_mul, Polynomial.X_pow_mul, + Polynomial.coeff_mul_X_pow'] + split + · rfl + · rw [mul_zero] + rw [hrhs] + rcases Nat.lt_or_ge t d with htd | htd + · rw [if_neg (by omega)] + rw [Array.getD_eq_getD_getElem?, List.getElem?_toArray, + List.getElem?_append_left (by simpa using htd), + List.getElem?_replicate_of_lt htd] + rfl + · rw [if_pos htd] + rw [Array.getD_eq_getD_getElem?, List.getElem?_toArray, + List.getElem?_append_right (by simpa using htd)] + rw [List.length_replicate] + rw [List.getElem?_map] + rw [← CPolynomial.coeff_toPoly] + rcases Nat.lt_or_ge (t - d) p.val.toList.length with hin | hin + · rw [List.getElem?_eq_getElem hin, Option.map_some, Option.getD_some] + congr 1 + rw [show CPolynomial.coeff p (t - d) = p.val.coeff (t - d) from rfl] + rw [CPolynomial.Raw.coeff] + rw [Array.getD_eq_getD_getElem?, Array.getElem?_eq_getElem + (by simpa using hin), Option.getD_some] + simp [Array.getElem_toList] + · rw [List.getElem?_eq_none hin, Option.map_none, Option.getD_none] + rw [show CPolynomial.coeff p (t - d) = p.val.coeff (t - d) from rfl] + rw [CPolynomial.Raw.coeff, Array.getD_eq_getD_getElem?, + Array.getElem?_eq_none (by simpa using hin)] + simp + +private theorem rowGet_rowScaleCoeffX (c : F) (d : Nat) (row : PolynomialRow F) + (k : Nat) : + rowGet (rowScaleCoeffX c d row) k = polynomialScaleCoeffX c d (rowGet row k) := by + rcases Nat.lt_or_ge k row.size with hk | hk + · rw [rowScaleCoeffX, rowGet, Array.getD_eq_getD_getElem?, + Array.getElem?_eq_getElem (by simpa using hk), Option.getD_some, + Array.getElem_map] + rw [rowGet, Array.getD_eq_getD_getElem?, Array.getElem?_eq_getElem hk, + Option.getD_some] + · rw [rowScaleCoeffX, rowGet, Array.getD_eq_getD_getElem?, + Array.getElem?_eq_none (by simpa using hk)] + rw [rowGet, Array.getD_eq_getD_getElem?, Array.getElem?_eq_none hk] + rw [show ((none : Option (CPolynomial F)).getD 0) = 0 from rfl, + polynomialScaleCoeffX_zero] + +/-- Coefficient-shift row scaling preserves the approximant condition. -/ +theorem rowApproximates_rowScaleCoeffX (mulCtx : CPolynomial.MulContext F) + (problem : XAdicProblem F) {row : PolynomialRow F} (c : F) (d : Nat) + (h : RowApproximates mulCtx problem row) : + RowApproximates mulCtx problem (rowScaleCoeffX c d row) := by + rw [rowApproximates_iff] at h ⊢ + intro j hj hjw + have hsum : ∑ k ∈ Finset.range problem.matrix.size, + (rowGet (rowScaleCoeffX c d row) k).toPoly * + (rowGet (problem.matrix.getD k #[]) j).toPoly = + Polynomial.C c * Polynomial.X ^ d * + ∑ k ∈ Finset.range problem.matrix.size, + (rowGet row k).toPoly * (rowGet (problem.matrix.getD k #[]) j).toPoly := by + rw [Finset.mul_sum] + refine Finset.sum_congr rfl fun k _hk ↦ ?_ + rw [rowGet_rowScaleCoeffX, polynomialScaleCoeffX_toPoly, mul_assoc, mul_assoc] + rw [hsum] + exact Dvd.dvd.mul_left (h j hj hjw) _ + +/-- Leading-term cancellation preserves the approximant condition. -/ +theorem rowApproximates_cancelKernelLeafLeadingTerm + (mulCtx : CPolynomial.MulContext F) (problem : XAdicProblem F) + {target reducer : PolynomialRow F} (shift : Array Nat) + (htarget : RowApproximates mulCtx problem target) + (hreducer : RowApproximates mulCtx problem reducer) : + RowApproximates mulCtx problem + (cancelKernelLeafLeadingTerm target reducer shift) := by + rw [cancelKernelLeafLeadingTerm] + split + · split + · split + · exact htarget + · exact rowApproximates_rowSub mulCtx problem htarget + (rowApproximates_rowScaleCoeffX mulCtx problem _ _ hreducer) + · exact htarget + · exact htarget + +/-- Pivot-table insertion preserves the approximant condition of all stored +rows. -/ +theorem insertKernelLeafPivotRowWithFuel_approximates + (mulCtx : CPolynomial.MulContext F) (problem : XAdicProblem F) : + ∀ (fuel : Nat) (pivots : Array (Option (PolynomialRow F))) + (shift : Array Nat) (row : PolynomialRow F), + (∀ p r, pivots.getD p none = some r → RowApproximates mulCtx problem r) → + RowApproximates mulCtx problem row → + ∀ p r, + (insertKernelLeafPivotRowWithFuel fuel pivots shift row).getD p none = + some r → + RowApproximates mulCtx problem r := by + intro fuel + induction fuel with + | zero => + intro pivots shift row hpivots _hrow p r hget + exact hpivots p r hget + | succ fuel ih => + intro pivots shift row hpivots hrow p r hget + rw [insertKernelLeafPivotRowWithFuel] at hget + have hset : ∀ (position : Nat) (newRow : PolynomialRow F), + RowApproximates mulCtx problem newRow → + ∀ p' r', + (pivots.setIfInBounds position (some newRow)).getD p' none = some r' → + RowApproximates mulCtx problem r' := by + intro position newRow hnew p' r' hget' + by_cases hpos : p' = position ∧ position < pivots.size + · rcases hpos with ⟨hp, hlt⟩ + subst hp + rw [Array.getD_eq_getD_getElem?, + Array.getElem?_setIfInBounds_self_of_lt hlt, Option.getD_some] at hget' + cases hget' + exact hnew + · rcases Nat.lt_or_ge p' pivots.size with hplt | hpge + · have hne : p' ≠ position := by + intro hcontra + exact hpos ⟨hcontra, hcontra ▸ hplt⟩ + rw [Array.getD_eq_getD_getElem?, + Array.getElem?_setIfInBounds_ne (by omega)] at hget' + exact hpivots p' r' (by rw [Array.getD_eq_getD_getElem?]; exact hget') + · rw [Array.getD_eq_getD_getElem?, Array.getElem?_eq_none + (by simpa using hpge)] at hget' + cases hget' + cases hterm : rowShiftedLeadingTerm? row shift with + | none => + simp only [hterm] at hget + exact hpivots p r hget + | some target => + simp only [hterm] at hget + cases hpivot : pivots.getD target.position none with + | none => + simp only [hpivot] at hget + exact hset target.position row hrow p r hget + | some pivot => + simp only [hpivot] at hget + cases hpterm : rowShiftedLeadingTerm? pivot shift with + | none => + simp only [hpterm] at hget + exact hset target.position row hrow p r hget + | some reducer => + simp only [hpterm] at hget + have hpivotrow : RowApproximates mulCtx problem pivot := + hpivots target.position pivot hpivot + split at hget + · refine ih (pivots.setIfInBounds target.position (some row)) shift + (cancelKernelLeafLeadingTerm pivot row shift) ?_ ?_ p r hget + · intro p' r' hget' + exact hset target.position row hrow p' r' hget' + · exact rowApproximates_cancelKernelLeafLeadingTerm mulCtx problem + shift hpivotrow hrow + · exact ih pivots shift (cancelKernelLeafLeadingTerm row pivot shift) + hpivots + (rowApproximates_cancelKernelLeafLeadingTerm mulCtx problem + shift hrow hpivotrow) + p r hget + +/-- Rows extracted from a sound pivot table satisfy the approximant +condition. -/ +theorem pivotRows_approximates (mulCtx : CPolynomial.MulContext F) + (problem : XAdicProblem F) {pivots : Array (Option (PolynomialRow F))} + (hpivots : ∀ p r, pivots.getD p none = some r → + RowApproximates mulCtx problem r) + {row : PolynomialRow F} (hrow : row ∈ MatrixRows (pivotRows pivots)) : + RowApproximates mulCtx problem row := by + rw [MatrixRows, pivotRows] at hrow + rw [List.toList_toArray] at hrow + rcases List.mem_filterMap.mp hrow with ⟨entry, hentry, hid⟩ + rcases List.getElem_of_mem hentry with ⟨p, hp, hget⟩ + refine hpivots p row ?_ + rw [Array.getD_eq_getD_getElem?, Array.getElem?_eq_getElem (by simpa using hp), + Option.getD_some] + rw [show pivots[p] = pivots.toList[p] from by rw [Array.getElem_toList]] + rw [hget] + exact hid + +/-- Pivot-table reduction preserves the approximant condition. -/ +theorem reduceKernelLeafRowsByPivots_approximates + (mulCtx : CPolynomial.MulContext F) (problem : XAdicProblem F) + {rows : PolynomialMatrix F} (shift : Array Nat) + (hrows : ∀ row, row ∈ MatrixRows rows → RowApproximates mulCtx problem row) + {row : PolynomialRow F} + (hrow : row ∈ MatrixRows (reduceKernelLeafRowsByPivots rows shift)) : + RowApproximates mulCtx problem row := by + rw [reduceKernelLeafRowsByPivots] at hrow + refine pivotRows_approximates mulCtx problem ?_ hrow + have hfold : ∀ (l : List (PolynomialRow F)) + (pivots : Array (Option (PolynomialRow F))), + (∀ r, r ∈ l → RowApproximates mulCtx problem r) → + (∀ p r, pivots.getD p none = some r → RowApproximates mulCtx problem r) → + ∀ p r, + (l.foldl (fun pivots row ↦ + insertKernelLeafPivotRowWithFuel (reduceKernelLeafFuel rows shift) + pivots shift row) pivots).getD p none = some r → + RowApproximates mulCtx problem r := by + intro l + induction l with + | nil => + intro pivots _hl hpivots p r hget + exact hpivots p r hget + | cons head tail ih => + intro pivots hl hpivots p r hget + rw [List.foldl_cons] at hget + refine ih _ (fun r hr ↦ hl r (List.mem_cons_of_mem head hr)) ?_ p r hget + intro p' r' hget' + exact insertKernelLeafPivotRowWithFuel_approximates mulCtx problem + (reduceKernelLeafFuel rows shift) pivots shift head hpivots + (hl head List.mem_cons_self) p' r' hget' + rw [← Array.foldl_toList] + refine hfold rows.toList _ (fun r hr ↦ hrows r hr) ?_ + intro p r hget + rw [Array.getD_eq_getD_getElem?] at hget + rcases Nat.lt_or_ge p (Array.replicate (MatrixWidth rows) + (none : Option (PolynomialRow F))).size with hp | hp + · rw [Array.getElem?_eq_getElem hp, Option.getD_some] at hget + rw [Array.getElem_replicate] at hget + cases hget + · rw [Array.getElem?_eq_none hp] at hget + cases hget + +omit [LawfulBEq F] in +/-- Compaction preserves row membership soundness. -/ +theorem compactNonzeroRows_subset {rows : PolynomialMatrix F} + {row : PolynomialRow F} (hrow : row ∈ MatrixRows (compactNonzeroRows rows)) : + row ∈ MatrixRows rows := by + rw [MatrixRows, compactNonzeroRows] at hrow + rw [MatrixRows] + have hmem : row ∈ rows ∧ rowIsZero row = false := by simpa using hrow + simpa using hmem.1 + +omit [LawfulBEq F] in +/-- Compacted rows are nonzero. -/ +theorem compactNonzeroRows_nonzero {rows : PolynomialMatrix F} + {row : PolynomialRow F} (hrow : row ∈ MatrixRows (compactNonzeroRows rows)) : + rowIsZero row = false := by + rw [MatrixRows, compactNonzeroRows] at hrow + have hmem : row ∈ rows ∧ rowIsZero row = false := by simpa using hrow + exact hmem.2 + +/-- The monomial `coeffXPower c d` under `toPoly`. -/ +theorem coeffXPower_toPoly (c : F) (d : Nat) : + (coeffXPower c d).toPoly = Polynomial.C c * Polynomial.X ^ d := by + apply Polynomial.ext + intro t + rw [← CPolynomial.coeff_toPoly, coeffXPower, CPolynomial.coeff_ofArray] + rw [Polynomial.coeff_C_mul, Polynomial.coeff_X_pow] + rcases Nat.lt_trichotomy t d with ht | ht | ht + · rw [Array.getD_eq_getD_getElem?, Array.getElem?_push_lt + (by simpa using ht), Option.getD_some, Array.getElem_replicate, + if_neg (by omega), mul_zero] + · subst ht + rw [Array.getD_eq_getD_getElem?] + rw [show ((Array.replicate t (0 : F)).push c)[t]? = some c from by + rw [Array.getElem?_push] + simp] + simp + · rw [Array.getD_eq_getD_getElem?, Array.getElem?_eq_none (by simp; omega)] + simp [Nat.ne_of_gt ht] + +/-- Monomial unit rows of sufficiently high degree satisfy every X-adic +condition. -/ +theorem rowApproximates_monomialUnitRow (mulCtx : CPolynomial.MulContext F) + (problem : XAdicProblem F) {i d : Nat} + (hd : ∀ j, j < problem.orders.size → problem.orders.getD j 0 ≤ d) : + RowApproximates mulCtx problem + (monomialUnitRow problem.matrix.size i d) := by + rw [rowApproximates_iff] + intro j hj hjw + refine Finset.dvd_sum fun k _hk ↦ ?_ + rcases Nat.lt_or_ge k problem.matrix.size with hk | hk + · have hentry : rowGet (monomialUnitRow (F := F) problem.matrix.size i d) k = + if i == k then coeffXPower 1 d else 0 := by + rw [monomialUnitRow, rowGet, Array.getD_eq_getD_getElem?, + List.getElem?_toArray, List.getElem?_map, List.getElem?_range hk, + Option.map_some, Option.getD_some] + rw [hentry] + by_cases hik : i == k + · rw [if_pos hik, coeffXPower_toPoly] + calc (Polynomial.X : Polynomial F) ^ (problem.orders.getD j 0) + ∣ (Polynomial.X : Polynomial F) ^ d := pow_dvd_pow _ (hd j hj) + _ ∣ Polynomial.C (1 : F) * Polynomial.X ^ d * + (rowGet (problem.matrix.getD k #[]) j).toPoly := + Dvd.dvd.mul_right (Dvd.dvd.mul_left dvd_rfl _) _ + · rw [if_neg hik, CPolynomial.toPoly_zero, zero_mul] + exact dvd_zero _ + · have hentry : rowGet (monomialUnitRow (F := F) problem.matrix.size i d) k = + 0 := by + rw [monomialUnitRow, rowGet, Array.getD_eq_getD_getElem?, + List.getElem?_toArray, List.getElem?_eq_none (by simpa using hk), + Option.getD_none] + rw [hentry, CPolynomial.toPoly_zero, zero_mul] + exact dvd_zero _ + +omit [BEq F] [LawfulBEq F] in +/-- Every entry of the order vector is bounded by `maxOrder`. -/ +theorem getD_le_maxOrder (problem : XAdicProblem F) {j : Nat} + (hj : j < problem.orders.size) : + problem.orders.getD j 0 ≤ maxOrder problem := by + rw [maxOrder] + have hmem : problem.orders.getD j 0 ∈ problem.orders.toList := by + rw [Array.getD_eq_getD_getElem?, Array.getElem?_eq_getElem hj, Option.getD_some] + exact Array.getElem_mem_toList hj + rw [← Array.foldl_toList] + have hgen : ∀ (l : List Nat) (acc : Nat) (x : Nat), x ∈ l → + x ≤ l.foldl max acc := by + intro l + induction l with + | nil => + intro acc x hx + cases hx + | cons head tail ih => + intro acc x hx + rw [List.foldl_cons] + rcases List.mem_cons.mp hx with hx | hx + · subst hx + have hbase : max acc x ≤ tail.foldl max (max acc x) := by + have hmono : ∀ (l : List Nat) (a : Nat), a ≤ l.foldl max a := by + intro l + induction l with + | nil => intro a; exact le_rfl + | cons h t iht => + intro a + rw [List.foldl_cons] + exact le_trans (Nat.le_max_left a h) (iht (max a h)) + exact hmono tail (max acc x) + omega + · exact ih (max acc head) x hx + exact hgen problem.orders.toList 0 _ hmem + +/-- Missing-pivot completion rows satisfy every X-adic condition. -/ +theorem missingCompletionRows_approximates (mulCtx : CPolynomial.MulContext F) + (problem : XAdicProblem F) (shift : Array Nat) (rows : PolynomialMatrix F) + {row : PolynomialRow F} + (hrow : row ∈ MatrixRows (missingCompletionRows problem shift rows)) : + RowApproximates mulCtx problem row := by + rw [MatrixRows, missingCompletionRows] at hrow + rw [List.toList_toArray] at hrow + rcases List.mem_filterMap.mp hrow with ⟨i, _hi, hsome⟩ + split at hsome + · cases hsome + · cases hsome + refine rowApproximates_monomialUnitRow mulCtx problem fun j hj ↦ ?_ + exact le_trans (getD_le_maxOrder problem hj) (Nat.le_max_right 1 _) + +/-- Pivot completion preserves the approximant condition. -/ +theorem completeMissingPivotRows_approximates (mulCtx : CPolynomial.MulContext F) + (problem : XAdicProblem F) (shift : Array Nat) {rows : PolynomialMatrix F} + (hrows : ∀ row, row ∈ MatrixRows rows → RowApproximates mulCtx problem row) + {row : PolynomialRow F} + (hrow : row ∈ MatrixRows (completeMissingPivotRows problem shift rows)) : + RowApproximates mulCtx problem row := by + rw [MatrixRows, completeMissingPivotRows, Array.toList_append] at hrow + rcases List.mem_append.mp hrow with hmem | hmem + · exact hrows row hmem + · exact missingCompletionRows_approximates mulCtx problem shift rows hmem + +/-! ## Composition soundness -/ + +/-- Coefficients of the shifted truncation `divXTrunc`. -/ +theorem divXTrunc_coeff (shift order : Nat) (p : CPolynomial F) (t : Nat) : + CPolynomial.coeff (divXTrunc shift order p) t = + if t < order then CPolynomial.coeff p (t + shift) else 0 := by + rw [divXTrunc, CPolynomial.coeff_ofArray] + rcases Nat.lt_or_ge t order with ht | ht + · rw [if_pos ht, Array.getD_eq_getD_getElem?, List.getElem?_toArray, + List.getElem?_map, List.getElem?_range ht, Option.map_some, Option.getD_some] + · rw [if_neg (by omega), Array.getD_eq_getD_getElem?, List.getElem?_toArray, + List.getElem?_eq_none (by simpa using ht), Option.getD_none] + +/-- The row-by-matrix product does not depend on the multiplication context. -/ +theorem rowMulMatrixWith_ctx (ctx₁ ctx₂ : CPolynomial.MulContext F) + (row : PolynomialRow F) (M : PolynomialMatrix F) : + rowMulMatrixWith ctx₁ row M = rowMulMatrixWith ctx₂ row M := by + rw [rowMulMatrixWith, rowMulMatrixWith] + congr 1 + refine List.map_congr_left fun j _hj ↦ ?_ + refine List.foldl_ext _ _ _ fun acc k _hk ↦ ?_ + rw [ctx₁.mul_eq_mul, ctx₂.mul_eq_mul] + +omit [BEq F] [LawfulBEq F] in +/-- `getD` of a mapped natural-number array at an in-bounds index. -/ +theorem natArray_map_getD (f : Nat → Nat) (a : Array Nat) {j : Nat} + (hj : j < a.size) : + (a.map f).getD j 0 = f (a.getD j 0) := by + rw [Array.getD_eq_getD_getElem?, Array.getD_eq_getD_getElem?, + Array.getElem?_map, Array.getElem?_eq_getElem hj] + rfl + +omit [BEq F] [LawfulBEq F] in +/-- A nonempty matrix with uniform row width `n` has `MatrixWidth` `n`. -/ +theorem matrixWidth_eq_of_first_row {M : PolynomialMatrix F} {n : Nat} + (hne : 0 < M.size) + (hsize : ∀ r ∈ MatrixRows M, r.size = n) : + MatrixWidth M = n := by + rw [MatrixWidth, Array.getElem?_eq_getElem hne] + exact hsize M[0] (by rw [MatrixRows]; exact Array.getElem_mem_toList hne) + +omit [BEq F] [LawfulBEq F] in +/-- In-bounds `getD` rows are matrix rows. -/ +theorem getD_mem_matrixRows {M : PolynomialMatrix F} {l : Nat} + (hl : l < M.size) : M.getD l #[] ∈ MatrixRows M := by + rw [MatrixRows, Array.getD_eq_getD_getElem?, Array.getElem?_eq_getElem hl, + Option.getD_some] + exact Array.getElem_mem_toList hl + +/-- Column entries of a doubly composed row product, when the inner basis has +uniform row width. -/ +private theorem rowGet_composed_toPoly (mulCtx : CPolynomial.MulContext F) + {P₁ M : PolynomialMatrix F} + (hsize : ∀ r ∈ MatrixRows P₁, r.size = M.size) + (p₂ : PolynomialRow F) {j : Nat} (hj : j < MatrixWidth M) : + (rowGet (rowMulMatrixWith mulCtx (rowMulMatrixWith mulCtx p₂ P₁) M) j).toPoly = + ∑ l ∈ Finset.range P₁.size, (rowGet p₂ l).toPoly * + (rowGet (rowMulMatrixWith mulCtx (P₁.getD l #[]) M) j).toPoly := by + rcases Nat.eq_zero_or_pos P₁.size with hP₁ | hP₁ + · rw [rowGet_rowMulMatrixWith_toPoly mulCtx _ M hj, hP₁] + rw [Finset.range_zero, Finset.sum_empty] + refine Finset.sum_eq_zero fun k _hk ↦ ?_ + have hzero : rowGet (rowMulMatrixWith mulCtx p₂ P₁) k = 0 := by + have hwidth : MatrixWidth P₁ = 0 := by + rw [MatrixWidth, Array.getElem?_eq_none (by omega)] + rw [rowGet, Array.getD_eq_getD_getElem?, Array.getElem?_eq_none + (by rw [rowMulMatrixWith_size, hwidth]; omega)] + rfl + rw [hzero, CPolynomial.toPoly_zero, zero_mul] + · have hwidth₁ : MatrixWidth P₁ = M.size := + matrixWidth_eq_of_first_row hP₁ hsize + rw [rowGet_rowMulMatrixWith_toPoly mulCtx _ M hj] + have hentry : ∀ k, k < M.size → + (rowGet (rowMulMatrixWith mulCtx p₂ P₁) k).toPoly = + ∑ l ∈ Finset.range P₁.size, + (rowGet p₂ l).toPoly * (rowGet (P₁.getD l #[]) k).toPoly := by + intro k hk + exact rowGet_rowMulMatrixWith_toPoly mulCtx p₂ P₁ (by omega) + calc ∑ k ∈ Finset.range M.size, + (rowGet (rowMulMatrixWith mulCtx p₂ P₁) k).toPoly * + (rowGet (M.getD k #[]) j).toPoly + = ∑ k ∈ Finset.range M.size, ∑ l ∈ Finset.range P₁.size, + (rowGet p₂ l).toPoly * (rowGet (P₁.getD l #[]) k).toPoly * + (rowGet (M.getD k #[]) j).toPoly := by + refine Finset.sum_congr rfl fun k hk ↦ ?_ + rw [hentry k (Finset.mem_range.mp hk), Finset.sum_mul] + _ = ∑ l ∈ Finset.range P₁.size, ∑ k ∈ Finset.range M.size, + (rowGet p₂ l).toPoly * (rowGet (P₁.getD l #[]) k).toPoly * + (rowGet (M.getD k #[]) j).toPoly := Finset.sum_comm + _ = ∑ l ∈ Finset.range P₁.size, (rowGet p₂ l).toPoly * + (rowGet (rowMulMatrixWith mulCtx (P₁.getD l #[]) M) j).toPoly := by + refine Finset.sum_congr rfl fun l _hl ↦ ?_ + rw [rowGet_rowMulMatrixWith_toPoly mulCtx _ M hj, Finset.mul_sum] + refine Finset.sum_congr rfl fun k _hk ↦ ?_ + ring + +/-- Soundness of one PM-basis composition step: a residual-approximant row +times a lower-approximant basis approximates the full problem. -/ +theorem rowApproximates_composed (mulCtx : CPolynomial.MulContext F) + {problem : XAdicProblem F} {d₁ : Nat} + {P₁ Rmat : PolynomialMatrix F} + (hP₁ : ∀ r ∈ MatrixRows P₁, + RowApproximates mulCtx + { orders := lowerOrders problem d₁, matrix := problem.matrix } r ∧ + r.size = problem.matrix.size) + (hRsize : Rmat.size = P₁.size) + (hRwidth : 0 < P₁.size → MatrixWidth problem.matrix ≤ MatrixWidth Rmat) + (hR : ∀ l j, l < P₁.size → j < problem.orders.size → + j < MatrixWidth problem.matrix → + ∀ t, t < problem.orders.getD j 0 - d₁ → + CPolynomial.coeff (rowGet (Rmat.getD l #[]) j) t = + CPolynomial.coeff + (rowGet (rowMulMatrixWith mulCtx (P₁.getD l #[]) problem.matrix) j) + (t + d₁)) + {p₂ : PolynomialRow F} + (hp₂ : RowApproximates mulCtx + { orders := residualOrders problem d₁, matrix := Rmat } p₂) : + RowApproximates mulCtx problem (rowMulMatrixWith mulCtx p₂ P₁) := by + rw [rowApproximates_iff] + intro j hj hjw + rw [← rowGet_rowMulMatrixWith_toPoly mulCtx _ problem.matrix hjw, + rowGet_composed_toPoly mulCtx (fun r hr ↦ (hP₁ r hr).2) p₂ hjw] + set o := problem.orders.getD j 0 with ho + have hP₁dvd : ∀ l, l < P₁.size → + (Polynomial.X : Polynomial F) ^ (min o d₁) ∣ + (rowGet (rowMulMatrixWith mulCtx (P₁.getD l #[]) + problem.matrix) j).toPoly := by + intro l hl + have hmem := getD_mem_matrixRows hl + have happrox := (hP₁ _ hmem).1 j (by simpa [lowerOrders] using hj) + rw [truncateX_eq_zero_iff_X_pow_dvd] at happrox + have horder : (lowerOrders problem d₁).getD j 0 = min o d₁ := by + rw [lowerOrders, natArray_map_getD _ _ hj] + rwa [horder] at happrox + rcases Nat.le_total o d₁ with hod | hod + · refine Finset.dvd_sum fun l hl ↦ ?_ + have hdvd := hP₁dvd l (Finset.mem_range.mp hl) + rw [Nat.min_eq_left hod] at hdvd + exact Dvd.dvd.mul_left hdvd _ + · rcases Nat.eq_zero_or_pos P₁.size with hP₁0 | hP₁0 + · rw [hP₁0, Finset.range_zero, Finset.sum_empty] + exact dvd_zero _ + have hwidthR : j < MatrixWidth Rmat := by + have := hRwidth hP₁0 + omega + have hquot : ∀ l, ∃ sl : Polynomial F, + l < P₁.size → + (rowGet (rowMulMatrixWith mulCtx (P₁.getD l #[]) + problem.matrix) j).toPoly = Polynomial.X ^ d₁ * sl := by + intro l + by_cases hl : l < P₁.size + · have hdvd := hP₁dvd l hl + rw [Nat.min_eq_right (by omega)] at hdvd + rcases hdvd with ⟨sl, hsl⟩ + exact ⟨sl, fun _ ↦ hsl⟩ + · exact ⟨0, fun h ↦ absurd h hl⟩ + choose s hs using hquot + have hcong : ∀ l, l < P₁.size → + (Polynomial.X : Polynomial F) ^ (o - d₁) ∣ + (rowGet (Rmat.getD l #[]) j).toPoly - s l := by + intro l hl + rw [Polynomial.X_pow_dvd_iff] + intro t ht + rw [Polynomial.coeff_sub, ← CPolynomial.coeff_toPoly, + hR l j hl hj hjw t (by omega)] + rw [CPolynomial.coeff_toPoly] + have hcoeff : ((rowGet (rowMulMatrixWith mulCtx (P₁.getD l #[]) + problem.matrix) j).toPoly).coeff (t + d₁) = + (Polynomial.X ^ d₁ * s l).coeff (t + d₁) := by + rw [hs l hl] + rw [Polynomial.coeff_X_pow_mul] at hcoeff + rw [hcoeff] + exact sub_self _ + have hresidual : (Polynomial.X : Polynomial F) ^ (o - d₁) ∣ + ∑ l ∈ Finset.range P₁.size, + (rowGet p₂ l).toPoly * (rowGet (Rmat.getD l #[]) j).toPoly := by + have hjres : j < (residualOrders problem d₁).size := by + rw [residualOrders] + simpa using hj + have happrox := hp₂ j hjres + rw [truncateX_eq_zero_iff_X_pow_dvd, + rowGet_rowMulMatrixWith_toPoly mulCtx p₂ Rmat hwidthR] at happrox + have horder : (residualOrders problem d₁).getD j 0 = o - d₁ := by + rw [residualOrders, natArray_map_getD _ _ hj] + rw [horder, hRsize] at happrox + exact happrox + have hquotsum : (Polynomial.X : Polynomial F) ^ (o - d₁) ∣ + ∑ l ∈ Finset.range P₁.size, (rowGet p₂ l).toPoly * s l := by + have hdiff : (Polynomial.X : Polynomial F) ^ (o - d₁) ∣ + (∑ l ∈ Finset.range P₁.size, + (rowGet p₂ l).toPoly * (rowGet (Rmat.getD l #[]) j).toPoly) - + ∑ l ∈ Finset.range P₁.size, (rowGet p₂ l).toPoly * s l := by + rw [← Finset.sum_sub_distrib] + refine Finset.dvd_sum fun l hl ↦ ?_ + rw [← mul_sub] + exact Dvd.dvd.mul_left (hcong l (Finset.mem_range.mp hl)) _ + have hsub := dvd_sub hresidual hdiff + simpa using hsub + have hsum : ∑ l ∈ Finset.range P₁.size, (rowGet p₂ l).toPoly * + (rowGet (rowMulMatrixWith mulCtx (P₁.getD l #[]) + problem.matrix) j).toPoly = + Polynomial.X ^ d₁ * + ∑ l ∈ Finset.range P₁.size, (rowGet p₂ l).toPoly * s l := by + rw [Finset.mul_sum] + refine Finset.sum_congr rfl fun l hl ↦ ?_ + rw [hs l (Finset.mem_range.mp hl)] + ring + rw [hsum, show o = d₁ + (o - d₁) from by omega, pow_add] + exact mul_dvd_mul_left _ hquotsum + +end Approximant + +end PolynomialMatrix + +end CompPoly diff --git a/CompPoly/LinearAlgebra/PolynomialMatrix/Approximant/PartialLinearization.lean b/CompPoly/LinearAlgebra/PolynomialMatrix/Approximant/PartialLinearization.lean new file mode 100644 index 00000000..4b023fc9 --- /dev/null +++ b/CompPoly/LinearAlgebra/PolynomialMatrix/Approximant/PartialLinearization.lean @@ -0,0 +1,235 @@ +/- +Copyright (c) 2026 CompPoly Contributors. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Valerii Huhnin +-/ + +module + +public import CompPoly.LinearAlgebra.PolynomialMatrix.Approximant.PMBasis + +/-! +# Partial-Linearization Parameters + +Small executable helpers used by diagonal modular-equation solvers to size the +expanded X-adic problem without using one global oversized order. + +## References + +* [Storjohann, A., *Notes on computing minimal approximant bases*][Sto06] +-/ + +@[expose] public section + +namespace CompPoly + +namespace PolynomialMatrix + +namespace Approximant + +variable {F : Type*} + +/-- Ceiling division with `1` as the zero-width fallback. -/ +def ceilDivFallback (n d : Nat) : Nat := + if d == 0 then 1 else (n + d - 1) / d + +/-- Degree mass of diagonal moduli. -/ +def modulusDegreeMass [Zero F] (moduli : Array (CPolynomial F)) : Nat := + moduli.foldl (fun acc modulus ↦ acc + modulus.natDegree) 0 + +/-- Chunk size `Delta = ceil(sigma / m)` used for partial linearization. -/ +def chunkDelta [Zero F] (solutionWidth : Nat) + (moduli : Array (CPolynomial F)) : Nat := + max 1 (ceilDivFallback (modulusDegreeMass moduli) solutionWidth) + +/-- X-adic orders for the exact-nullspace lift, using the local chunk size. -/ +def linearizedOrders [Zero F] (solutionWidth : Nat) + (moduli : Array (CPolynomial F)) : Array Nat := + let delta := chunkDelta solutionWidth moduli + moduli.map fun modulus ↦ modulus.natDegree + delta + 1 + +/-- Maximum shifted-degree offset in a principal-coordinate shift. -/ +def maxShiftDegree (shift : Array Nat) : Nat := + (List.range shift.size).foldl + (fun acc i ↦ max acc (shift.getD i 0)) 0 + +/-- Offset added to every principal coordinate when lifting to exact-nullspace +coordinates. Adding the same positive offset to every principal coordinate +preserves their relative shifted degrees; quotient coordinates are shifted +separately above the principal search window. -/ +def principalShiftOffset (_shift : Array Nat) (delta : Nat) : Nat := + delta + 1 + +/-- Quotient-coordinate shift used by the exact-nullspace lift. It is kept at +the chunk degree so quotient coordinates do not dominate the shifted degree of +principal relation rows. Rows that compress to zero are removed later by the +principal-row filter. -/ +def quotientShift (_shift : Array Nat) (delta : Nat) : Nat := + delta + +/-- Principal-coordinate shift used inside the exact-nullspace lift. -/ +def liftedPrincipalShift (shift : Array Nat) (delta : Nat) : Array Nat := + shift.map fun degree ↦ degree + principalShiftOffset shift delta + +/-- Extend the principal solution shift with low quotient shifts. -/ +def exactNullspaceShift (shift : Array Nat) (quotientWidth delta : Nat) : + Array Nat := + liftedPrincipalShift shift delta ++ + Array.replicate quotientWidth (quotientShift shift delta) + +/-- One chunk of a principal solution coordinate. It represents +`X^offset * chunkPoly` in coordinate `coord`. -/ +structure PrincipalChunk where + coord : Nat + offset : Nat +deriving Repr, BEq, DecidableEq + +/-- Number of chunks used for one principal coordinate from a shifted-degree +profile. -/ +def principalChunkCount (solutionWidth : Nat) (shift : Array Nat) + (delta j : Nat) : Nat := + let maxShift := (List.range solutionWidth).foldl + (fun acc i ↦ max acc (shift.getD i 0)) 0 + max 1 (ceilDivFallback (maxShift + 1 - shift.getD j 0) delta) + +/-- Principal chunks induced by the known shifted-degree profile. -/ +def principalChunks (solutionWidth : Nat) (shift : Array Nat) + (delta : Nat) : Array PrincipalChunk := Id.run do + let mut chunks := #[] + for j in [0:solutionWidth] do + let count := principalChunkCount solutionWidth shift delta j + for c in [0:count] do + chunks := chunks.push { coord := j, offset := c * delta } + pure chunks + +/-- One unshifted chunk for each principal coordinate. -/ +def principalUnitChunks (solutionWidth : Nat) : Array PrincipalChunk := + (List.range solutionWidth).map + (fun coord ↦ { coord := coord, offset := 0 }) |>.toArray + +/-- Fallback shifted pivot degree used before discovery has produced a degree for +one coordinate. -/ +def fallbackPivotDegree (solutionWidth : Nat) (shift : Array Nat) : Nat := + (List.range solutionWidth).foldl + (fun acc i ↦ max acc (shift.getD i 0)) 0 + +/-- Pivot degree for one coordinate, falling back to the shift spread when the +profile has not discovered that coordinate. -/ +def pivotDegreeAt (solutionWidth : Nat) (shift : Array Nat) + (pivotDegrees : Array (Option Nat)) (j : Nat) : Nat := + match pivotDegrees.getD j none with + | some degree => degree + | none => fallbackPivotDegree solutionWidth shift + +/-- Number of chunks for one principal coordinate from a discovered shifted +pivot-degree profile. -/ +def principalChunkCountFromPivotDegree (shift : Array Nat) + (delta j pivotDegree : Nat) : Nat := + max 1 (ceilDivFallback (pivotDegree + 1 - shift.getD j 0) delta) + +/-- Principal chunks induced by a discovered shifted pivot-degree profile. -/ +def principalChunksFromPivotDegrees (solutionWidth : Nat) (shift : Array Nat) + (delta : Nat) (pivotDegrees : Array (Option Nat)) : + Array PrincipalChunk := Id.run do + let mut chunks := #[] + for j in [0:solutionWidth] do + let degree := pivotDegreeAt solutionWidth shift pivotDegrees j + let count := principalChunkCountFromPivotDegree shift delta j degree + for c in [0:count] do + chunks := chunks.push { coord := j, offset := c * delta } + pure chunks + +/-- Executable partial-linearization plan for the exact-nullspace lift. -/ +structure PartialLinearizationPlan where + solutionWidth : Nat + quotientWidth : Nat + delta : Nat + chunks : Array PrincipalChunk + +/-- Build a chunk plan from the modular shape and shifted-degree profile. -/ +def partialLinearizationPlan [Zero F] (solutionWidth quotientWidth : Nat) + (moduli : Array (CPolynomial F)) (shift : Array Nat) : + PartialLinearizationPlan := + let delta := chunkDelta solutionWidth moduli + { solutionWidth := solutionWidth + quotientWidth := quotientWidth + delta := delta + chunks := principalChunks solutionWidth shift delta } + +/-- Build a chunk plan from a discovered shifted pivot-degree profile. -/ +def partialLinearizationPlanFromPivotDegrees [Zero F] + (solutionWidth quotientWidth : Nat) (moduli : Array (CPolynomial F)) + (shift : Array Nat) (pivotDegrees : Array (Option Nat)) : + PartialLinearizationPlan := + let delta := chunkDelta solutionWidth moduli + { solutionWidth := solutionWidth + quotientWidth := quotientWidth + delta := delta + chunks := principalChunksFromPivotDegrees solutionWidth shift delta pivotDegrees } + +/-- Plan with partial linearization disabled for the principal coordinates. -/ +def unchunkedPartialLinearizationPlan [Zero F] + (solutionWidth quotientWidth : Nat) (moduli : Array (CPolynomial F)) : + PartialLinearizationPlan := + { solutionWidth := solutionWidth + quotientWidth := quotientWidth + delta := chunkDelta solutionWidth moduli + chunks := principalUnitChunks solutionWidth } + +/-- Monomial `X^offset` as a canonical polynomial, built without requiring the +`CPolynomial.X` nontriviality instance. -/ +def xPowPolynomial [Zero F] [One F] [BEq F] [LawfulBEq F] + (offset : Nat) : CPolynomial F := + CPolynomial.ofArray ((Array.replicate offset 0).push 1) + +/-- Multiply a polynomial by `X^offset` by shifting its coefficient array. +This is an `O(offset + deg p)` array operation; it must not go through generic +polynomial multiplication, which would cost `O(offset * deg p)`. -/ +def shiftPolynomialX [Semiring F] [BEq F] [LawfulBEq F] + (offset : Nat) (p : CPolynomial F) : CPolynomial F := + if p == 0 then + 0 + else + CPolynomial.ofArray ((List.replicate offset (0 : F) ++ p.val.toList).toArray) + +/-- Shift every entry of a row by `X^offset`. -/ +def shiftRowX [Semiring F] [BEq F] [LawfulBEq F] + (offset : Nat) (row : PolynomialRow F) : PolynomialRow F := + row.map fun p ↦ shiftPolynomialX offset p + +/-- Shift for the chunked exact-nullspace problem. -/ +def chunkedExactNullspaceShift (plan : PartialLinearizationPlan) + (shift : Array Nat) : Array Nat := + plan.chunks.map + (fun chunk ↦ + shift.getD chunk.coord 0 + chunk.offset + + principalShiftOffset shift plan.delta) ++ + Array.replicate plan.quotientWidth (quotientShift shift plan.delta) + +/-- Compress one row in chunked coordinates back to the principal solution +coordinates. -/ +def compressChunkedPrincipalRow [Semiring F] [BEq F] [LawfulBEq F] + (plan : PartialLinearizationPlan) (row : PolynomialRow F) : + PolynomialRow F := + (List.range plan.solutionWidth).map + (fun coord ↦ + (List.range plan.chunks.size).foldl + (fun acc chunkIdx ↦ + let chunk := plan.chunks.getD chunkIdx { coord := 0, offset := 0 } + if chunk.coord == coord then + acc + shiftPolynomialX chunk.offset (rowGet row chunkIdx) + else + acc) + 0) |>.toArray + +/-- Compress every row in a chunked basis back to the principal coordinates. -/ +def compressChunkedPrincipalRows [Semiring F] [BEq F] [LawfulBEq F] + (plan : PartialLinearizationPlan) (basis : PolynomialMatrix F) : + PolynomialMatrix F := + basis.map fun row ↦ compressChunkedPrincipalRow plan row + +end Approximant + +end PolynomialMatrix + +end CompPoly diff --git a/CompPoly/LinearAlgebra/PolynomialMatrix/MuldersStorjohannCorrectness/Fast.lean b/CompPoly/LinearAlgebra/PolynomialMatrix/MuldersStorjohannCorrectness/Fast.lean index 1a02f0a7..159432e0 100644 --- a/CompPoly/LinearAlgebra/PolynomialMatrix/MuldersStorjohannCorrectness/Fast.lean +++ b/CompPoly/LinearAlgebra/PolynomialMatrix/MuldersStorjohannCorrectness/Fast.lean @@ -10,10 +10,11 @@ public import CompPoly.LinearAlgebra.PolynomialMatrix.MuldersStorjohannCorrectne /-! # Fast Mulders-Storjohann Reduction Agrees With the Direct Definition -The fast reducer caches shifted leading positions once per scan and cancels -leading terms through the fused `rowSubScaledShift` update. This file proves it -extensionally equal to `muldersStorjohannReduce`, so every correctness result -transfers, and packages it as a certified `ShiftedRowReducerContext`. +The fast reducer caches shifted leading positions once per conflict scan and +cancels leading terms through the fused `rowSubScaledShift` update. This file +proves it extensionally equal to `muldersStorjohannReduce`, so every +correctness result transfers, and packages it as a certified +`ShiftedRowReducerContext`. -/ @[expose] public section @@ -156,7 +157,7 @@ theorem muldersStorjohannReduceWithFuelFast_eq : rw [cachedLeadingConflict?_eq] cases shiftedLeadingConflict? M shift with | none => rfl - | some _ => + | some pair => simp only [muldersStorjohannStepFast_eq, ih] theorem muldersStorjohannReduceFast_eq (M : PolynomialMatrix F) diff --git a/CompPoly/LinearAlgebra/PolynomialMatrix/MuldersStorjohannCorrectness/Reduction.lean b/CompPoly/LinearAlgebra/PolynomialMatrix/MuldersStorjohannCorrectness/Reduction.lean index efd3dbf2..c6c64626 100644 --- a/CompPoly/LinearAlgebra/PolynomialMatrix/MuldersStorjohannCorrectness/Reduction.lean +++ b/CompPoly/LinearAlgebra/PolynomialMatrix/MuldersStorjohannCorrectness/Reduction.lean @@ -289,6 +289,43 @@ theorem muldersStorjohannReduce_no_conflict (muldersStorjohannFuel M shift) M shift hM (shiftedMatrixMeasure_lt_muldersStorjohannFuel hM) +/-- The fueled reducer is deterministic: two runs that both end without a +shifted leading conflict stopped at the same (first conflict-free) matrix of +the common step orbit, regardless of their fuel. -/ +theorem muldersStorjohannReduceWithFuel_eq_of_no_conflict : + ∀ (f g : Nat) (M : PolynomialMatrix F) (shift : Array Nat), + shiftedLeadingConflict? (muldersStorjohannReduceWithFuel f M shift) shift = + none → + shiftedLeadingConflict? (muldersStorjohannReduceWithFuel g M shift) shift = + none → + muldersStorjohannReduceWithFuel f M shift = + muldersStorjohannReduceWithFuel g M shift := by + intro f + induction f with + | zero => + intro g M shift hf hg + simp only [muldersStorjohannReduceWithFuel] at hf + cases g with + | zero => rfl + | succ g => simp only [muldersStorjohannReduceWithFuel, hf] + | succ f ih => + intro g M shift hf hg + cases hconf : shiftedLeadingConflict? M shift with + | none => + cases g with + | zero => simp only [muldersStorjohannReduceWithFuel, hconf] + | succ g => simp only [muldersStorjohannReduceWithFuel, hconf] + | some pair => + rcases pair with ⟨i, j⟩ + simp only [muldersStorjohannReduceWithFuel, hconf] at hf ⊢ + cases g with + | zero => + simp only [muldersStorjohannReduceWithFuel, hconf] at hg + cases hg + | succ g => + simp only [muldersStorjohannReduceWithFuel, hconf] at hg ⊢ + exact ih g (muldersStorjohannStep M shift i j) shift hf hg + theorem muldersStorjohannReduceWithFuel_rowSpan_subset (fuel : Nat) (M : PolynomialMatrix F) (shift : Array Nat) (hM : WellFormed M) : RowSpan (muldersStorjohannReduceWithFuel fuel M shift) ⊆ RowSpan M := by diff --git a/CompPoly/LinearAlgebra/PolynomialMatrix/MuldersStorjohannCorrectness/WeakPopovMinimal.lean b/CompPoly/LinearAlgebra/PolynomialMatrix/MuldersStorjohannCorrectness/WeakPopovMinimal.lean new file mode 100644 index 00000000..59aaedeb --- /dev/null +++ b/CompPoly/LinearAlgebra/PolynomialMatrix/MuldersStorjohannCorrectness/WeakPopovMinimal.lean @@ -0,0 +1,293 @@ +/- +Copyright (c) 2026 CompPoly Contributors. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Valerii Huhnin +-/ + +module + +-- `coeff`, `ofArray` and friends are declared in bare `public section`s, so their +-- bodies are opaque downstream; see `docs/wiki/module-system.md`. +import all CompPoly.Univariate.Basic +import all CompPoly.Univariate.Raw.Core +public import CompPoly.LinearAlgebra.PolynomialMatrix.MuldersStorjohannCorrectness.Reduction + +/-! +# Shifted Weak-Popov Least-Row Minimality + +Generalized predictable-degree property: any shifted weak-Popov matrix contains a +row whose shifted degree is a lower bound for the shifted degree of every row-span +member. This is the reducer-independent core of +`muldersStorjohannReduce_least_row_minimal`, stated for an arbitrary well-formed +shifted weak-Popov matrix and without any alignment hypothesis between the shift +size and the matrix width. +-/ + +@[expose] public section + +namespace CompPoly + +namespace PolynomialMatrix + +variable {F : Type*} [Field F] [BEq F] [LawfulBEq F] [DecidableEq F] + +/-- Predictable-degree property of shifted weak-Popov matrices: every row-span +member with a defined shifted degree is bounded below by the shifted degree of +some matrix row. Requires neither `shift.size = MatrixWidth B` nor any other +shift alignment hypothesis. -/ +theorem shiftedWeakPopov_least_row_minimal + (B : PolynomialMatrix F) (shift : Array Nat) (row : PolynomialRow F) + (hB : WellFormed B) + (hpopov : ShiftedWeakPopov B shift) + (hrow : row ∈ RowSpan B) + (hdeg : rowShiftedDegree? row shift ≠ none) : + ∃ outRow outDeg rowDeg, + outRow ∈ MatrixRows B ∧ + rowShiftedDegree? outRow shift = some outDeg ∧ + rowShiftedDegree? row shift = some rowDeg ∧ + outDeg ≤ rowDeg := by + rcases hrow with ⟨coeffs, _hcoeffsSize, hrowEq⟩ + subst row + cases hrowDeg : rowShiftedDegree? (rowLinearCombination coeffs B) shift with + | none => + exact False.elim (hdeg hrowDeg) + | some rowDeg => + let S := rowCombinationTermSupport coeffs B shift + have hS : S.Nonempty := + rowCombinationTermSupport_nonempty_of_combination_degree_some + hB hrowDeg + obtain ⟨imax, himaxS, hmaxDegree⟩ := + Finset.exists_max_image S + (rowCombinationTermDegree coeffs B shift) hS + let D := rowCombinationTermDegree coeffs B shift imax + let T := S.filter fun i ↦ rowCombinationTermDegree coeffs B shift i = D + have hT : T.Nonempty := by + refine ⟨imax, ?_⟩ + exact Finset.mem_filter.mpr ⟨himaxS, rfl⟩ + obtain ⟨i, hiT, hmaxPos⟩ := + Finset.exists_max_image T (rowCombinationTermPosition B shift) hT + have hiS : i ∈ S := (Finset.mem_filter.mp hiT).1 + have hiDegreeEq : + rowCombinationTermDegree coeffs B shift i = D := + (Finset.mem_filter.mp hiT).2 + rcases (Finset.mem_filter.mp hiS) with + ⟨hiRange, hcoeffI, hrowDegI_ne⟩ + have hi : i < B.size := Finset.mem_range.mp hiRange + cases hrowDegI : rowShiftedDegree? (B.getD i #[]) shift with + | none => + exact False.elim (hrowDegI_ne hrowDegI) + | some outDeg => + rcases rowShiftedLeadingPosition?_some_of_degree hrowDegI with + ⟨posI, hposI⟩ + have hposIRow : posI < (B.getD i #[]).size := + rowShiftedLeadingPosition?_lt hposI + have hposIWidth : posI < MatrixWidth B := by + rw [matrix_getD_size_of_wellFormed hB hi] at hposIRow + exact hposIRow + have hentryIData := + shiftedEntryDegree?_eq_some_iff_data.1 + (rowShiftedLeadingPosition?_entry_eq hrowDegI hposI) + rcases hentryIData with ⟨hentryINe, hentryIShift⟩ + let coeffI := coeffs.getD i 0 + let entryI := rowGet (B.getD i #[]) posI + let k := coeffI.natDegree + entryI.natDegree + have hdegreeI_expr : + rowCombinationTermDegree coeffs B shift i = + coeffI.natDegree + outDeg := by + unfold rowCombinationTermDegree coeffI + rw [hrowDegI] + have hD_eq : D = k + shift.getD posI 0 := by + rw [← hiDegreeEq, hdegreeI_expr, ← hentryIShift] + unfold k entryI + omega + have hselectedCoeffNe : + (coeffs.getD i 0 * rowGet (B.getD i #[]) posI).coeff k ≠ 0 := by + simpa [coeffI, entryI, k] using + cpoly_coeff_mul_natDegree_add_ne_zero + (P := coeffI) (Q := entryI) hcoeffI hentryINe + have hotherCoeffZero : + ∀ l, l < B.size → l ≠ i → + (coeffs.getD l 0 * rowGet (B.getD l #[]) posI).coeff k = 0 := by + intro l hl hli + by_contra hcoeffAtK + let coeffL := coeffs.getD l 0 + let entryL := rowGet (B.getD l #[]) posI + have hprodNonzero : coeffL * entryL ≠ 0 := by + intro hprodZero + exact hcoeffAtK (by + have hprodZero' : + coeffs.getD l 0 * rowGet (B.getD l #[]) posI = 0 := by + simpa [coeffL, entryL] using hprodZero + rw [hprodZero'] + exact CPolynomial.coeff_zero k) + have hcoeffL : coeffL ≠ 0 := by + intro hzero + exact hprodNonzero (by simp [coeffL, hzero]) + have hentryL : entryL ≠ 0 := by + intro hzero + exact hprodNonzero (by simp [entryL, hzero]) + have hposIRowL : posI < (B.getD l #[]).size := by + rw [matrix_getD_size_of_wellFormed hB hl] + exact hposIWidth + have hrowDegL_ne : + rowShiftedDegree? (B.getD l #[]) shift ≠ none := by + intro hnone + have hrowZero := + (rowShiftedDegree?_eq_none_iff + (row := B.getD l #[]) (shift := shift)).1 hnone + have hentryZero : rowGet (B.getD l #[]) posI = 0 := by + unfold rowGet + rw [Array.getD_eq_getD_getElem?, + Array.getElem?_eq_getElem hposIRowL] + exact hrowZero (B.getD l #[])[posI] + (by simpa only [Array.mem_def] using + Array.getElem_mem_toList hposIRowL) + exact hentryL (by simpa [entryL] using hentryZero) + have hlS : l ∈ S := + Finset.mem_filter.mpr + ⟨Finset.mem_range.mpr hl, hcoeffL, hrowDegL_ne⟩ + cases hrowDegL : rowShiftedDegree? (B.getD l #[]) shift with + | none => + exact hrowDegL_ne hrowDegL + | some outDegL => + rcases rowShiftedLeadingPosition?_some_of_degree hrowDegL with + ⟨posL, hposL⟩ + have hdegreeL_le_D : + rowCombinationTermDegree coeffs B shift l ≤ D := by + simpa [D] using hmaxDegree l hlS + have hk_le_prod : + k ≤ (coeffL * entryL).natDegree := + CPolynomial.le_natDegree_of_ne_zero hcoeffAtK + have hprodDeg_le : + (coeffL * entryL).natDegree ≤ + coeffL.natDegree + entryL.natDegree := + cpoly_natDegree_mul_le coeffL entryL + have hentryBound : + entryL.natDegree + shift.getD posI 0 ≤ outDegL := by + exact rowShiftedDegree?_entry_bound hrowDegL + hposIRowL (by simpa [entryL] using hentryL) + have hD_le_degreeL : D ≤ coeffL.natDegree + outDegL := by + rw [hD_eq] + omega + have hdegreeL_eq_D : coeffL.natDegree + outDegL = D := by + have hdegreeL_expr : + rowCombinationTermDegree coeffs B shift l = + coeffL.natDegree + outDegL := by + unfold rowCombinationTermDegree coeffL + rw [hrowDegL] + have hle : coeffL.natDegree + outDegL ≤ D := by + simpa [hdegreeL_expr] using hdegreeL_le_D + exact le_antisymm hle hD_le_degreeL + have hD_le_entry : + D ≤ coeffL.natDegree + + (entryL.natDegree + shift.getD posI 0) := by + rw [hD_eq] + omega + have hentryShiftEq : + entryL.natDegree + shift.getD posI 0 = outDegL := by + omega + have hentryShiftL : + shiftedEntryDegree? (B.getD l #[]) shift posI = + some outDegL := by + have hsome := + shiftedEntryDegree?_eq_some_of_rowGet_ne_zero + (row := B.getD l #[]) (shift := shift) (j := posI) + (by simpa [entryL] using hentryL) + have hentryShiftEq' : + (rowGet (B.getD l #[]) posI).natDegree + + shift.getD posI 0 = outDegL := by + simpa [entryL] using hentryShiftEq + rw [hentryShiftEq'] at hsome + exact hsome + have hposI_le_posL : posI ≤ posL := + rowShiftedLeadingPosition?_le_of_entry_eq_degree + hrowDegL hposL hposIRowL hentryShiftL + have hlDegreeEq : + rowCombinationTermDegree coeffs B shift l = D := by + have hdegreeL_expr : + rowCombinationTermDegree coeffs B shift l = + coeffL.natDegree + outDegL := by + unfold rowCombinationTermDegree coeffL + rw [hrowDegL] + rw [hdegreeL_expr, hdegreeL_eq_D] + have hlT : l ∈ T := + Finset.mem_filter.mpr ⟨hlS, hlDegreeEq⟩ + have hposL_le_posI : posL ≤ posI := by + have hle := hmaxPos l hlT + have hposLval : + rowCombinationTermPosition B shift l = posL := by + unfold rowCombinationTermPosition + rw [hposL] + rfl + have hposIval : + rowCombinationTermPosition B shift i = posI := by + unfold rowCombinationTermPosition + rw [hposI] + rfl + rwa [hposLval, hposIval] at hle + have hposEq : posL = posI := + le_antisymm hposL_le_posI hposI_le_posL + have hposNe := + hpopov l i hl hi hli + (by rw [hposL]; simp) + (by rw [hposI]; simp) + exact hposNe (by rw [hposL, hposI, hposEq]) + have hcomboCoeffNe : + (rowGet (rowLinearCombination coeffs B) posI).coeff k ≠ 0 := by + rw [rowGet_rowLinearCombination_coeff] + rw [Finset.sum_eq_single i] + · exact hselectedCoeffNe + · intro l hlRange hli + exact hotherCoeffZero l (Finset.mem_range.mp hlRange) hli + · intro hiNot + exact False.elim (hiNot (Finset.mem_range.mpr hi)) + have hcomboEntryNe : + rowGet (rowLinearCombination coeffs B) posI ≠ 0 := by + intro hzero + exact hcomboCoeffNe (by + rw [hzero] + exact CPolynomial.coeff_zero k) + have hcomboPos : posI < (rowLinearCombination coeffs B).size := by + simpa [rowLinearCombination_size hB coeffs] using hposIWidth + have hcomboEntryBound : + (rowGet (rowLinearCombination coeffs B) posI).natDegree + + shift.getD posI 0 ≤ rowDeg := + rowShiftedDegree?_entry_bound hrowDeg hcomboPos hcomboEntryNe + have hk_le_combo : + k ≤ (rowGet (rowLinearCombination coeffs B) posI).natDegree := + CPolynomial.le_natDegree_of_ne_zero hcomboCoeffNe + have hD_le_rowDeg : D ≤ rowDeg := by + rw [hD_eq] + exact le_trans (Nat.add_le_add_right hk_le_combo _) + hcomboEntryBound + have houtDeg_le_D : outDeg ≤ D := by + rw [← hiDegreeEq, hdegreeI_expr] + omega + refine ⟨B.getD i #[], outDeg, rowDeg, ?_, hrowDegI, rfl, ?_⟩ + · rw [MatrixRows] + rw [Array.getD_eq_getD_getElem?, Array.getElem?_eq_getElem hi] + exact Array.getElem_mem_toList hi + · exact le_trans houtDeg_le_D hD_le_rowDeg + +/-- Least-row minimality of the Mulders-Storjohann reducer, re-derived from the +generalized shifted weak-Popov predictable-degree property. -/ +theorem muldersStorjohannReduce_least_row_minimal_of_weakPopov + (M : PolynomialMatrix F) (shift : Array Nat) (row : PolynomialRow F) + (hM : WellFormed M) (hshift : shift.size = MatrixWidth M) + (hrow : row ∈ RowSpan M) + (hdeg : rowShiftedDegree? row shift ≠ none) : + ∃ outRow outDeg rowDeg, + outRow ∈ MatrixRows (muldersStorjohannReduce M shift) ∧ + rowShiftedDegree? outRow shift = some outDeg ∧ + rowShiftedDegree? row shift = some rowDeg ∧ + outDeg ≤ rowDeg := by + refine shiftedWeakPopov_least_row_minimal + (muldersStorjohannReduce M shift) shift row + (muldersStorjohannReduce_wellFormed M shift hM) + (muldersStorjohannReduce_weakPopov M shift hM hshift) + ?_ hdeg + rwa [muldersStorjohannReduce_rowSpan_eq M shift hM] + +end PolynomialMatrix + +end CompPoly diff --git a/CompPoly/LinearAlgebra/PolynomialMatrix/Operations.lean b/CompPoly/LinearAlgebra/PolynomialMatrix/Operations.lean new file mode 100644 index 00000000..db41711c --- /dev/null +++ b/CompPoly/LinearAlgebra/PolynomialMatrix/Operations.lean @@ -0,0 +1,492 @@ +/- +Copyright (c) 2026 CompPoly Contributors. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Valerii Huhnin +-/ + +module + +-- `ofArray` and `Raw.coeff` are declared in bare `public section`s, so their bodies +-- are opaque downstream; see `docs/wiki/module-system.md`. +import all CompPoly.Univariate.Basic +import all CompPoly.Univariate.Raw.Core +public import CompPoly.LinearAlgebra.PolynomialMatrix.Shifted +public import CompPoly.Univariate.Context + +/-! +# Polynomial-Matrix Operations + +Reusable executable operations for polynomial rows and row-major polynomial +matrices. The multiplication and reduction entry points take explicit +univariate operation contexts so concrete fields can supply fast polynomial +arithmetic. +-/ + +@[expose] public section + +namespace CompPoly + +namespace PolynomialMatrix + +variable {F : Type*} + +/-- Keep the coefficients of degree `< order`. -/ +def truncateX [Zero F] [BEq F] [LawfulBEq F] (order : Nat) + (p : CPolynomial F) : CPolynomial F := + CPolynomial.ofArray + ((List.range order).map (fun i ↦ CPolynomial.coeff p i) |>.toArray) + +/-- Coefficients of an `X`-adic truncation. -/ +theorem truncateX_coeff [Semiring F] [BEq F] [LawfulBEq F] (order : Nat) + (p : CPolynomial F) (i : Nat) : + CPolynomial.coeff (truncateX order p) i = + if i < order then CPolynomial.coeff p i else 0 := by + rw [truncateX] + rw [show CPolynomial.coeff (CPolynomial.ofArray + ((List.range order).map (fun i ↦ CPolynomial.coeff p i) |>.toArray)) i = + CPolynomial.Raw.coeff + ((List.range order).map (fun i ↦ CPolynomial.coeff p i) |>.toArray) i from by + rw [CPolynomial.ofArray, CPolynomial.coeff] + rw [CPolynomial.Raw.Trim.coeff_eq_coeff]] + rw [CPolynomial.Raw.coeff, Array.getD_eq_getD_getElem?, List.getElem?_toArray, + List.getElem?_map] + by_cases hi : i < order + · rw [List.getElem?_range hi] + simp [hi] + · rw [List.getElem?_eq_none (by simpa using Nat.le_of_not_lt hi)] + simp [hi] + +/-- Polynomial-matrix low-product backend. The full multiplication context is +kept beside the low-product operation because recursive PM-basis still needs +ordinary basis composition. -/ +structure MulLowContext (F : Type*) [Semiring F] [BEq F] [LawfulBEq F] where + mulContext : CPolynomial.MulContext F + mulLow : Nat → CPolynomial F → CPolynomial F → CPolynomial F + /-- The backend returns exactly the truncated canonical product. -/ + mulLow_eq : ∀ order p q, mulLow order p q = truncateX order (p * q) + +namespace MulLowContext + +/-- Low-product backend obtained by truncating a full univariate product. -/ +def fromMulContext [Semiring F] [BEq F] [LawfulBEq F] + (mulCtx : CPolynomial.MulContext F) : MulLowContext F where + mulContext := mulCtx + mulLow order p q := + CPolynomial.ofArray + ((List.range order).map (fun i ↦ CPolynomial.coeff (mulCtx.mul p q) i) |>.toArray) + mulLow_eq := by + intro order p q + rw [mulCtx.mul_eq_mul] + rfl + +/-- Low-product backend backed directly by a raw low-product implementation. -/ +def raw [Semiring F] [BEq F] [LawfulBEq F] + (mulCtx : CPolynomial.MulContext F) + (rawCtx : CPolynomial.Raw.MulLowContext F) : MulLowContext F where + mulContext := mulCtx + mulLow order p q := CPolynomial.ofArray (rawCtx.mulLow order p.val q.val) + mulLow_eq := by + intro order p q + apply CPolynomial.eq_iff_coeff.2 + intro i + rw [truncateX_coeff] + rw [show CPolynomial.coeff (CPolynomial.ofArray (rawCtx.mulLow order p.val q.val)) i = + CPolynomial.Raw.coeff (rawCtx.mulLow order p.val q.val) i from by + rw [CPolynomial.ofArray, CPolynomial.coeff] + rw [CPolynomial.Raw.Trim.coeff_eq_coeff]] + rw [CPolynomial.Raw.mulLow_coeff] + by_cases hi : i < order + · simp only [hi, if_true] + rw [CPolynomial.coeff_mul, CPolynomial.Raw.mul_coeff] + · simp [hi] + +end MulLowContext + +/-- Build a row-major polynomial matrix from an indexed entry function. -/ +def ofFn [Zero F] (rows width : Nat) (entry : Nat → Nat → CPolynomial F) : + PolynomialMatrix F := + (List.range rows).map + (fun i ↦ (List.range width).map (fun j ↦ entry i j) |>.toArray) |>.toArray + +/-- The zero matrix of a fixed shape. -/ +def zero [Zero F] (rows width : Nat) : PolynomialMatrix F := + ofFn rows width fun _ _ ↦ 0 + +/-- The polynomial identity matrix of size `n`. -/ +def identity [Semiring F] [BEq F] [LawfulBEq F] [Nontrivial F] (n : Nat) : + PolynomialMatrix F := + ofFn n n fun i j ↦ if i == j then 1 else 0 + +/-- Matrix transpose, using zero defaults for ragged input rows. -/ +def transpose [Zero F] (M : PolynomialMatrix F) : PolynomialMatrix F := + ofFn (MatrixWidth M) M.size fun i j ↦ rowGet (M.getD j #[]) i + +/-- Dot product of two polynomial rows using an explicit univariate +multiplication context. -/ +def rowDotWith [Semiring F] [BEq F] [LawfulBEq F] + (mulCtx : CPolynomial.MulContext F) (a b : PolynomialRow F) : + CPolynomial F := + (List.range (max a.size b.size)).foldl + (fun acc k ↦ acc + mulCtx.mul (rowGet a k) (rowGet b k)) 0 + +/-- Row-by-matrix product using an explicit univariate multiplication context. -/ +def rowMulMatrixWith [Semiring F] [BEq F] [LawfulBEq F] + (mulCtx : CPolynomial.MulContext F) (row : PolynomialRow F) + (M : PolynomialMatrix F) : PolynomialRow F := + (List.range (MatrixWidth M)).map + (fun j ↦ + (List.range row.size).foldl + (fun acc k ↦ acc + mulCtx.mul (rowGet row k) (rowGet (M.getD k #[]) j)) 0) |>.toArray + +/-- Matrix product using an explicit univariate multiplication context. -/ +def mulWith [Semiring F] [BEq F] [LawfulBEq F] + (mulCtx : CPolynomial.MulContext F) (A B : PolynomialMatrix F) : + PolynomialMatrix F := + A.map fun row ↦ rowMulMatrixWith mulCtx row B + +/-- Matrix product backed by canonical univariate multiplication. -/ +def mul [Semiring F] [BEq F] [LawfulBEq F] + (A B : PolynomialMatrix F) : PolynomialMatrix F := + mulWith CPolynomial.MulContext.naive A B + +/-- Pointwise matrix addition, using zero defaults for ragged inputs. -/ +def add [Semiring F] [BEq F] [LawfulBEq F] + (A B : PolynomialMatrix F) : PolynomialMatrix F := + ofFn (max A.size B.size) (max (MatrixWidth A) (MatrixWidth B)) fun i j ↦ + rowGet (A.getD i #[]) j + rowGet (B.getD i #[]) j + +/-- Pointwise matrix subtraction, using zero defaults for ragged inputs. -/ +def sub [Ring F] [BEq F] [LawfulBEq F] + (A B : PolynomialMatrix F) : PolynomialMatrix F := + ofFn (max A.size B.size) (max (MatrixWidth A) (MatrixWidth B)) fun i j ↦ + rowGet (A.getD i #[]) j - rowGet (B.getD i #[]) j + +/-- Extract a rectangular block with zero defaults for out-of-range entries. -/ +def block [Zero F] (M : PolynomialMatrix F) + (rowStart rowCount colStart colCount : Nat) : PolynomialMatrix F := + ofFn rowCount colCount fun i j ↦ + rowGet (M.getD (rowStart + i) #[]) (colStart + j) + +/-- Join four equally sized square blocks into one square matrix. -/ +def joinSquareBlocks [Zero F] (half : Nat) + (C₁₁ C₁₂ C₂₁ C₂₂ : PolynomialMatrix F) : PolynomialMatrix F := + ofFn (2 * half) (2 * half) fun i j ↦ + if i < half then + if j < half then + rowGet (C₁₁.getD i #[]) j + else + rowGet (C₁₂.getD i #[]) (j - half) + else + if j < half then + rowGet (C₂₁.getD (i - half) #[]) j + else + rowGet (C₂₂.getD (i - half) #[]) (j - half) + +/-- Fuel-bounded doubling loop for the smallest power of two at least `target`. -/ +def nextPowerOfTwoAtLeastLoop (target : Nat) : + Nat → Nat → Nat + | 0, current => current + | fuel + 1, current => + if target ≤ current then + current + else + nextPowerOfTwoAtLeastLoop target fuel (2 * current) + +/-- Smallest power of two at least `target`, with `1` returned for `0`. -/ +def nextPowerOfTwoAtLeast (target : Nat) : Nat := + nextPowerOfTwoAtLeastLoop target (target + 1) 1 + +/-- Runtime dimension controlling rectangular polynomial-matrix multiplication. -/ +def multiplicationDimension [Zero F] (A B : PolynomialMatrix F) : Nat := + max A.size (max (MatrixWidth A) (max B.size (MatrixWidth B))) + +/-- Pad a matrix to an `n × n` square using the zero-default block extractor. -/ +def padSquare [Zero F] (n : Nat) (M : PolynomialMatrix F) : + PolynomialMatrix F := + block M 0 n 0 n + +/-- Trim a matrix to a rectangular output shape. -/ +def trimShape [Zero F] (rows width : Nat) (M : PolynomialMatrix F) : + PolynomialMatrix F := + block M 0 rows 0 width + +/-- Slice `count` natural-number entries, using zero defaults out of bounds. -/ +def natArraySlice (values : Array Nat) (start count : Nat) : Array Nat := + (List.range count).map (fun i ↦ values.getD (start + i) 0) |>.toArray + +/-- Pointwise maximum of two natural-number arrays. -/ +def maxNatArrays (a b : Array Nat) : Array Nat := + (List.range (max a.size b.size)).map + (fun i ↦ max (a.getD i 0) (b.getD i 0)) |>.toArray + +/-- Number of coefficients needed to represent a polynomial exactly. -/ +def polynomialCoeffCap [Zero F] [BEq F] (p : CPolynomial F) : Nat := + if p == 0 then 0 else p.natDegree + 1 + +/-- Number of low coefficients sufficient for one product term exactly. -/ +def productCoeffCap [Zero F] [BEq F] (p q : CPolynomial F) : Nat := + if p == 0 || q == 0 then 0 else p.natDegree + q.natDegree + 1 + +/-- Per-entry coefficient cap for one row-by-matrix product entry. -/ +def rowMulMatrixEntryCoeffCap [Zero F] [BEq F] + (row : PolynomialRow F) (M : PolynomialMatrix F) (j : Nat) : Nat := + (List.range row.size).foldl + (fun acc k ↦ max acc + (productCoeffCap (rowGet row k) (rowGet (M.getD k #[]) j))) 0 + +/-- Truncate one row with independent output-column orders. -/ +def rowTruncateColumns [Zero F] [BEq F] [LawfulBEq F] + (orders : Array Nat) (row : PolynomialRow F) : PolynomialRow F := + (List.range row.size).map + (fun j ↦ truncateX (orders.getD j 0) (rowGet row j)) |>.toArray + +/-- Truncate a matrix with independent output-column orders. -/ +def truncateColumns [Zero F] [BEq F] [LawfulBEq F] + (orders : Array Nat) (M : PolynomialMatrix F) : PolynomialMatrix F := + M.map fun row ↦ rowTruncateColumns orders row + +/-- Multiply and retain only coefficients of degree `< order`. -/ +def mulTruncXWith [Semiring F] [BEq F] [LawfulBEq F] + (mulCtx : CPolynomial.MulContext F) (order : Nat) + (p q : CPolynomial F) : CPolynomial F := + truncateX order (mulCtx.mul p q) + +/-- Low-product entry point for the first `order` coefficients. -/ +def mulLowXWith [Semiring F] [BEq F] [LawfulBEq F] + (lowCtx : MulLowContext F) (order : Nat) + (p q : CPolynomial F) : CPolynomial F := + lowCtx.mulLow order p q + +/-- Divide by `X^shift` and keep `order` coefficients. -/ +def divXTrunc [Zero F] [BEq F] [LawfulBEq F] (shift order : Nat) + (p : CPolynomial F) : CPolynomial F := + CPolynomial.ofArray + ((List.range order).map (fun i ↦ CPolynomial.coeff p (i + shift)) |>.toArray) + +/-- Reduce by a monic modulus when one is present. A zero modulus is treated as +an absent modulus and leaves the input unchanged. -/ +def modByMonicWith [Field F] [BEq F] [LawfulBEq F] + (modCtx : CPolynomial.ModContext F) (p modulus : CPolynomial F) : + CPolynomial F := + if modulus == 0 then p else modCtx.modByMonic p modulus + +/-- Reduce a row by independent diagonal moduli. The output width is the number +of supplied moduli. -/ +def rowModDiagonalWith [Field F] [BEq F] [LawfulBEq F] + (modCtx : CPolynomial.ModContext F) (moduli : Array (CPolynomial F)) + (row : PolynomialRow F) : PolynomialRow F := + (List.range moduli.size).map + (fun j ↦ modByMonicWith modCtx (rowGet row j) (moduli.getD j 0)) |>.toArray + +/-- Reduce every matrix row by independent diagonal moduli. -/ +def modDiagonalWith [Field F] [BEq F] [LawfulBEq F] + (modCtx : CPolynomial.ModContext F) (moduli : Array (CPolynomial F)) + (M : PolynomialMatrix F) : PolynomialMatrix F := + M.map fun row ↦ rowModDiagonalWith modCtx moduli row + +/-- Row-by-matrix product followed by diagonal modular reduction. -/ +def rowMulMatrixModDiagonalWith [Field F] [BEq F] [LawfulBEq F] + (mulCtx : CPolynomial.MulContext F) (modCtx : CPolynomial.ModContext F) + (row : PolynomialRow F) (M : PolynomialMatrix F) + (moduli : Array (CPolynomial F)) : PolynomialRow F := + rowModDiagonalWith modCtx moduli (rowMulMatrixWith mulCtx row M) + +/-- Row-by-matrix product with independent output-column truncation. Column +`j` keeps coefficients of degree `< orders[j]`; this is the residual-window +primitive used by recursive PM-basis. -/ +def rowMulMatrixTruncColumnWith [Semiring F] [BEq F] [LawfulBEq F] + (lowCtx : MulLowContext F) (orders : Array Nat) + (row : PolynomialRow F) (M : PolynomialMatrix F) : PolynomialRow F := + (List.range (MatrixWidth M)).map + (fun j ↦ + let order := orders.getD j 0 + (List.range row.size).foldl + (fun acc k ↦ + acc + mulLowXWith lowCtx order + (rowGet row k) (rowGet (M.getD k #[]) j)) 0) |>.toArray + +/-- Matrix product with independent output-column truncation. -/ +def mulTruncColumnWith [Semiring F] [BEq F] [LawfulBEq F] + (lowCtx : MulLowContext F) (orders : Array Nat) + (A B : PolynomialMatrix F) : PolynomialMatrix F := + A.map fun row ↦ rowMulMatrixTruncColumnWith lowCtx orders row B + +/-- Fuel-bounded Strassen-style matrix product with independent output-column +truncation orders. -/ +def mulTruncColumnStrassenWithFuel [Ring F] [BEq F] [LawfulBEq F] + (lowCtx : MulLowContext F) (leafCutoff : Nat) : + Nat → Array Nat → PolynomialMatrix F → PolynomialMatrix F → PolynomialMatrix F + | 0, orders, A, B => mulTruncColumnWith lowCtx orders A B + | fuel + 1, orders, A, B => + let n := A.size + let dimension := multiplicationDimension A B + if dimension ≤ leafCutoff || dimension ≤ 1 then + mulTruncColumnWith lowCtx orders A B + else if + MatrixWidth A == n && B.size == n && MatrixWidth B == n && + n % 2 == 0 then + let h := n / 2 + let leftOrders := natArraySlice orders 0 h + let rightOrders := natArraySlice orders h h + let pairOrders := maxNatArrays leftOrders rightOrders + let A₁₁ := block A 0 h 0 h + let A₁₂ := block A 0 h h h + let A₂₁ := block A h h 0 h + let A₂₂ := block A h h h h + let B₁₁ := block B 0 h 0 h + let B₁₂ := block B 0 h h h + let B₂₁ := block B h h 0 h + let B₂₂ := block B h h h h + let recMul := mulTruncColumnStrassenWithFuel lowCtx leafCutoff fuel + let M₁ := recMul pairOrders (add A₁₁ A₂₂) (add B₁₁ B₂₂) + let M₂ := recMul pairOrders (add A₂₁ A₂₂) B₁₁ + let M₃ := recMul rightOrders A₁₁ (sub B₁₂ B₂₂) + let M₄ := recMul leftOrders A₂₂ (sub B₂₁ B₁₁) + let M₅ := recMul pairOrders (add A₁₁ A₁₂) B₂₂ + let M₆ := recMul rightOrders (sub A₂₁ A₁₁) (add B₁₁ B₁₂) + let M₇ := recMul leftOrders (sub A₁₂ A₂₂) (add B₂₁ B₂₂) + let C₁₁ := add (sub (add M₁ M₄) M₅) M₇ + let C₁₂ := add M₃ M₅ + let C₂₁ := add M₂ M₄ + let C₂₂ := add (sub (add M₁ M₃) M₂) M₆ + truncateColumns orders (joinSquareBlocks h C₁₁ C₁₂ C₂₁ C₂₂) + else + let paddedSize := nextPowerOfTwoAtLeast dimension + let paddedOrders := natArraySlice orders 0 paddedSize + let product := mulTruncColumnStrassenWithFuel lowCtx leafCutoff fuel + paddedOrders (padSquare paddedSize A) (padSquare paddedSize B) + trimShape A.size (MatrixWidth B) product + +/-- Strassen-style matrix product with independent output-column truncation +orders and conservative default fuel. -/ +def mulTruncColumnStrassenWith [Ring F] [BEq F] [LawfulBEq F] + (lowCtx : MulLowContext F) (leafCutoff : Nat) + (orders : Array Nat) (A B : PolynomialMatrix F) : PolynomialMatrix F := + let n := multiplicationDimension A B + mulTruncColumnStrassenWithFuel lowCtx leafCutoff (n + 1) orders A B + +/-- Row-by-matrix product with per-output-entry degree caps inferred from input +degree profiles. This reconstructs the exact row product while routing every +term through low-product multiplication. -/ +def rowMulMatrixBoundedWith [Semiring F] [BEq F] [LawfulBEq F] + (lowCtx : MulLowContext F) (row : PolynomialRow F) + (M : PolynomialMatrix F) : PolynomialRow F := + (List.range (MatrixWidth M)).map + (fun j ↦ + let order := rowMulMatrixEntryCoeffCap row M j + (List.range row.size).foldl + (fun acc k ↦ + acc + mulLowXWith lowCtx order + (rowGet row k) (rowGet (M.getD k #[]) j)) 0) |>.toArray + +/-- Matrix product reconstructed from inferred per-entry degree caps. Recursive +composition uses this only as its small-leaf and fuel-exhausted fallback. -/ +def mulBoundedWith [Semiring F] [BEq F] [LawfulBEq F] + (lowCtx : MulLowContext F) (A B : PolynomialMatrix F) : + PolynomialMatrix F := + A.map fun row ↦ rowMulMatrixBoundedWith lowCtx row B + +/-- Fuel-bounded Strassen-style matrix product. + +Small inputs and exhausted fuel use the bounded row-column product. Larger +rectangular or odd-sized inputs are padded to square power-of-two shape, routed +through the recursive block product, and trimmed back to the requested output +shape. -/ +def mulStrassenWithFuel [Ring F] [BEq F] [LawfulBEq F] + (lowCtx : MulLowContext F) (leafCutoff : Nat) : + Nat → PolynomialMatrix F → PolynomialMatrix F → PolynomialMatrix F + | 0, A, B => mulBoundedWith lowCtx A B + | fuel + 1, A, B => + let n := A.size + let dimension := multiplicationDimension A B + if dimension ≤ leafCutoff || dimension ≤ 1 then + mulBoundedWith lowCtx A B + else if + MatrixWidth A == n && B.size == n && MatrixWidth B == n && + n % 2 == 0 then + let h := n / 2 + let A₁₁ := block A 0 h 0 h + let A₁₂ := block A 0 h h h + let A₂₁ := block A h h 0 h + let A₂₂ := block A h h h h + let B₁₁ := block B 0 h 0 h + let B₁₂ := block B 0 h h h + let B₂₁ := block B h h 0 h + let B₂₂ := block B h h h h + let recMul := mulStrassenWithFuel lowCtx leafCutoff fuel + let M₁ := recMul (add A₁₁ A₂₂) (add B₁₁ B₂₂) + let M₂ := recMul (add A₂₁ A₂₂) B₁₁ + let M₃ := recMul A₁₁ (sub B₁₂ B₂₂) + let M₄ := recMul A₂₂ (sub B₂₁ B₁₁) + let M₅ := recMul (add A₁₁ A₁₂) B₂₂ + let M₆ := recMul (sub A₂₁ A₁₁) (add B₁₁ B₁₂) + let M₇ := recMul (sub A₁₂ A₂₂) (add B₂₁ B₂₂) + let C₁₁ := add (sub (add M₁ M₄) M₅) M₇ + let C₁₂ := add M₃ M₅ + let C₂₁ := add M₂ M₄ + let C₂₂ := add (sub (add M₁ M₃) M₂) M₆ + joinSquareBlocks h C₁₁ C₁₂ C₂₁ C₂₂ + else + let paddedSize := nextPowerOfTwoAtLeast dimension + let product := mulStrassenWithFuel lowCtx leafCutoff fuel + (padSquare paddedSize A) (padSquare paddedSize B) + trimShape A.size (MatrixWidth B) product + +/-- Strassen-style matrix product with a conservative default fuel. -/ +def mulStrassenWith [Ring F] [BEq F] [LawfulBEq F] + (lowCtx : MulLowContext F) (leafCutoff : Nat) + (A B : PolynomialMatrix F) : PolynomialMatrix F := + let n := multiplicationDimension A B + mulStrassenWithFuel lowCtx leafCutoff (n + 1) A B + +/-- Executable modular-equation row predicate. -/ +def rowSatisfiesModularBool [Field F] [BEq F] [LawfulBEq F] + (mulCtx : CPolynomial.MulContext F) (modCtx : CPolynomial.ModContext F) + (row : PolynomialRow F) (M : PolynomialMatrix F) + (moduli : Array (CPolynomial F)) : Bool := + (rowMulMatrixModDiagonalWith mulCtx modCtx row M moduli).all fun p ↦ p == 0 + +/-- Shifted row-degree profile for all rows. -/ +def shiftedRowDegreeProfile [Zero F] [BEq F] (M : PolynomialMatrix F) + (shift : Array Nat) : Array (Option Nat) := + M.map fun row ↦ rowShiftedDegree? row shift + +/-- Candidate row selected by least-shifted-degree scanning. -/ +structure RowChoice (F : Type*) [Zero F] where + index : Nat + row : PolynomialRow F + degree : Nat + +/-- Tie-breaking order for least-shifted-degree row selection. -/ +def betterRowChoice [Zero F] (candidate current : RowChoice F) : Bool := + candidate.degree < current.degree || + (candidate.degree == current.degree && candidate.index < current.index) + +/-- One left-to-right scan step for least-shifted-degree row selection. -/ +def leastShiftedDegreeRowStep? [Zero F] [BEq F] + (M : PolynomialMatrix F) (shift : Array Nat) + (best : Option (RowChoice F)) (i : Nat) : Option (RowChoice F) := + let row := M.getD i #[] + match rowShiftedDegree? row shift with + | none => best + | some degree => + let candidate : RowChoice F := { index := i, row := row, degree := degree } + match best with + | none => some candidate + | some current => + if betterRowChoice candidate current then some candidate else best + +/-- Scan row indices for the best least-shifted-degree candidate. -/ +def leastShiftedDegreeChoice? [Zero F] [BEq F] + (M : PolynomialMatrix F) (shift : Array Nat) : Option (RowChoice F) := + (List.range M.size).foldl (leastShiftedDegreeRowStep? M shift) none + +/-- Select a nonzero row of least shifted degree. -/ +def leastShiftedDegreeRow? [Zero F] [BEq F] + (M : PolynomialMatrix F) (shift : Array Nat) : + Option (PolynomialRow F) := + (leastShiftedDegreeChoice? M shift).map fun choice ↦ choice.row + +end PolynomialMatrix + +end CompPoly diff --git a/CompPoly/LinearAlgebra/PolynomialMatrix/RowSelection.lean b/CompPoly/LinearAlgebra/PolynomialMatrix/RowSelection.lean new file mode 100644 index 00000000..b6450452 --- /dev/null +++ b/CompPoly/LinearAlgebra/PolynomialMatrix/RowSelection.lean @@ -0,0 +1,400 @@ +/- +Copyright (c) 2026 CompPoly Contributors. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Valerii Huhnin +-/ + +module + +public import CompPoly.LinearAlgebra.PolynomialMatrix.Operations + +/-! +# Least-Shifted-Degree Row Selection Helpers + +Correctness facts for the generic least-shifted-degree row scan in +`PolynomialMatrix.Operations`. +-/ + +@[expose] public section + +namespace CompPoly + + + +namespace PolynomialMatrix + + + +variable {F : Type*} [Field F] [BEq F] + +def RowChoiceValid (M : PolynomialMatrix F) (shift : Array Nat) + (choice : RowChoice F) : Prop := + choice.index < M.size ∧ choice.row = M.getD choice.index #[] ∧ + rowShiftedDegree? choice.row shift = some choice.degree + +omit [BEq F] in +private theorem betterRowChoice_true_candidate_degree_le + {candidate current : RowChoice F} + (h : betterRowChoice candidate current = true) : + candidate.degree ≤ current.degree := by + unfold betterRowChoice at h + simp at h + rcases h with hlt | ⟨heq, _hidx⟩ + · omega + · have hdegree : candidate.degree = current.degree := heq + omega +omit [BEq F] in +private theorem betterRowChoice_false_current_degree_le + {candidate current : RowChoice F} + (h : betterRowChoice candidate current = false) : + current.degree ≤ candidate.degree := by + unfold betterRowChoice at h + by_cases hlt : candidate.degree < current.degree + · simp [hlt] at h + · omega + +omit [BEq F] in +private theorem betterRowChoice_not_true_current_degree_le + {candidate current : RowChoice F} + (h : ¬ betterRowChoice candidate current = true) : + current.degree ≤ candidate.degree := + betterRowChoice_false_current_degree_le (Bool.eq_false_iff.mpr h) + + +private theorem leastShiftedDegreeRowStep?_none + {M : PolynomialMatrix F} {shift : Array Nat} + {best : Option (RowChoice F)} {i : Nat} + (h : leastShiftedDegreeRowStep? M shift best i = none) : + best = none ∧ rowShiftedDegree? (M[i]?.getD #[]) shift = none := by + unfold leastShiftedDegreeRowStep? at h + cases hdeg : rowShiftedDegree? (M[i]?.getD #[]) shift with + | none => + have hbest : best = none := by + simpa [leastShiftedDegreeRowStep?, hdeg] using h + exact ⟨hbest, rfl⟩ + | some degree => + cases best with + | none => + simp [hdeg] at h + | some current => + by_cases hbetter : + betterRowChoice { index := i, row := M[i]?.getD #[], degree := degree } current + · simp [hdeg, hbetter] at h + · simp [hdeg, hbetter] at h + + +private theorem leastShiftedDegreeRowStep?_some_of_best_some + {M : PolynomialMatrix F} {shift : Array Nat} + {best : Option (RowChoice F)} {current : RowChoice F} {i : Nat} + (hbest : best = some current) : + ∃ choice, leastShiftedDegreeRowStep? M shift best i = some choice := by + unfold leastShiftedDegreeRowStep? + cases hdeg : rowShiftedDegree? (M[i]?.getD #[]) shift with + | none => + refine ⟨current, ?_⟩ + simp [hbest, hdeg] + | some degree => + by_cases hbetter : + betterRowChoice { index := i, row := M[i]?.getD #[], degree := degree } current + · exact ⟨{ index := i, row := M[i]?.getD #[], degree := degree }, + by simp [hbest, hdeg, hbetter]⟩ + · exact ⟨current, + by simp [hbest, hdeg, hbetter]⟩ + + +private theorem leastShiftedDegreeRowStep?_preserves_degree_le + {M : PolynomialMatrix F} {shift : Array Nat} + {best : Option (RowChoice F)} {choice : RowChoice F} {d i : Nat} + (hbest : ∃ current, best = some current ∧ current.degree ≤ d) + (hstep : leastShiftedDegreeRowStep? M shift best i = some choice) : + choice.degree ≤ d := by + rcases hbest with ⟨current, hbest, hcurrent⟩ + unfold leastShiftedDegreeRowStep? at hstep + cases hdeg : rowShiftedDegree? (M[i]?.getD #[]) shift with + | none => + have hstep' : some current = some choice := by + simpa [leastShiftedDegreeRowStep?, hdeg, hbest] using hstep + have hchoice : choice = current := by + injection hstep' with hEq + exact hEq.symm + rw [hchoice] + exact hcurrent + | some degree => + let candidate : RowChoice F := + { index := i, row := M[i]?.getD #[], degree := degree } + by_cases hbetter : betterRowChoice candidate current + · have hstep' : some candidate = some choice := by + simpa [leastShiftedDegreeRowStep?, hdeg, hbest, candidate, hbetter] using hstep + have hchoice : choice = candidate := by + injection hstep' with hEq + exact hEq.symm + rw [hchoice] + exact le_trans (betterRowChoice_true_candidate_degree_le hbetter) hcurrent + · have hstep' : some current = some choice := by + simpa [leastShiftedDegreeRowStep?, hdeg, hbest, candidate, hbetter] using hstep + have hchoice : choice = current := by + injection hstep' with hEq + exact hEq.symm + rw [hchoice] + exact hcurrent + + +private theorem leastShiftedDegreeRowStep?_degree_le_of_row + {M : PolynomialMatrix F} {shift : Array Nat} + {best : Option (RowChoice F)} {choice : RowChoice F} {d i : Nat} + (hdeg : rowShiftedDegree? (M[i]?.getD #[]) shift = some d) + (hstep : leastShiftedDegreeRowStep? M shift best i = some choice) : + choice.degree ≤ d := by + let candidate : RowChoice F := { index := i, row := M[i]?.getD #[], degree := d } + cases best with + | none => + have hstep' : some candidate = some choice := by + simpa [leastShiftedDegreeRowStep?, hdeg, candidate] using hstep + have hchoice : choice = candidate := by + injection hstep' with hEq + exact hEq.symm + simp [hchoice, candidate] + | some current => + by_cases hbetter : betterRowChoice candidate current + · have hstep' : some candidate = some choice := by + simpa [leastShiftedDegreeRowStep?, hdeg, candidate, hbetter] using hstep + have hchoice : choice = candidate := by + injection hstep' with hEq + exact hEq.symm + simp [hchoice, candidate] + · have hstep' : some current = some choice := by + simpa [leastShiftedDegreeRowStep?, hdeg, candidate, hbetter] using hstep + have hchoice : choice = current := by + injection hstep' with hEq + exact hEq.symm + rw [hchoice] + exact betterRowChoice_not_true_current_degree_le hbetter + + +private theorem leastShiftedDegreeFold_some_of_best_some + {M : PolynomialMatrix F} {shift : Array Nat} + (xs : List Nat) {best : Option (RowChoice F)} {current : RowChoice F} + (hbest : best = some current) : + ∃ choice, + xs.foldl (leastShiftedDegreeRowStep? M shift) best = some choice := by + induction xs generalizing best current with + | nil => + exact ⟨current, hbest⟩ + | cons i xs ih => + rcases leastShiftedDegreeRowStep?_some_of_best_some + (M := M) (shift := shift) (i := i) hbest with + ⟨choice, hstep⟩ + exact ih (best := leastShiftedDegreeRowStep? M shift best i) + (current := choice) hstep + +private theorem leastShiftedDegreeFold_preserves_degree_le + {M : PolynomialMatrix F} {shift : Array Nat} + (xs : List Nat) {best : Option (RowChoice F)} {choice : RowChoice F} {d : Nat} + (hbest : ∃ current, best = some current ∧ current.degree ≤ d) + (hfold : xs.foldl (leastShiftedDegreeRowStep? M shift) best = some choice) : + choice.degree ≤ d := by + induction xs generalizing best with + | nil => + rcases hbest with ⟨current, hbest, hcurrent⟩ + rw [hbest] at hfold + cases hfold + exact hcurrent + | cons i xs ih => + rcases hbest with ⟨current, hbest, hcurrent⟩ + rcases leastShiftedDegreeRowStep?_some_of_best_some + (M := M) (shift := shift) (i := i) hbest with + ⟨stepChoice, hstep⟩ + have hstepBound : stepChoice.degree ≤ d := + leastShiftedDegreeRowStep?_preserves_degree_le + (M := M) (shift := shift) (i := i) + ⟨current, hbest, hcurrent⟩ hstep + exact ih (best := leastShiftedDegreeRowStep? M shift best i) + ⟨stepChoice, hstep, hstepBound⟩ + (by simpa only [List.foldl_cons] using hfold) + +private theorem leastShiftedDegreeFold_degree_le_of_mem + {M : PolynomialMatrix F} {shift : Array Nat} + (xs : List Nat) {best : Option (RowChoice F)} {choice : RowChoice F} + {i d : Nat} + (hfold : xs.foldl (leastShiftedDegreeRowStep? M shift) best = some choice) + (hi : i ∈ xs) + (hdeg : rowShiftedDegree? (M[i]?.getD #[]) shift = some d) : + choice.degree ≤ d := by + induction xs generalizing best with + | nil => + simp at hi + | cons x xs ih => + simp at hi + rcases hi with hix | hi + · subst x + cases hstep : leastShiftedDegreeRowStep? M shift best i with + | none => + rcases leastShiftedDegreeRowStep?_none hstep with ⟨_hbest, hrowNone⟩ + rw [hrowNone] at hdeg + contradiction + | some stepChoice => + have hstepBound : stepChoice.degree ≤ d := + leastShiftedDegreeRowStep?_degree_le_of_row hdeg hstep + exact leastShiftedDegreeFold_preserves_degree_le + (M := M) (shift := shift) xs + ⟨stepChoice, rfl, hstepBound⟩ + (by simpa [hstep] using hfold) + · exact ih (best := leastShiftedDegreeRowStep? M shift best x) + (by simpa only [List.foldl_cons] using hfold) hi + +private theorem leastShiftedDegreeFold_none + {M : PolynomialMatrix F} {shift : Array Nat} + (xs : List Nat) {best : Option (RowChoice F)} + (hfold : xs.foldl (leastShiftedDegreeRowStep? M shift) best = none) : + best = none ∧ + ∀ i, i ∈ xs → rowShiftedDegree? (M[i]?.getD #[]) shift = none := by + induction xs generalizing best with + | nil => + exact ⟨hfold, by simp⟩ + | cons i xs ih => + have htail := ih (best := leastShiftedDegreeRowStep? M shift best i) + (by simpa only [List.foldl_cons] using hfold) + rcases leastShiftedDegreeRowStep?_none htail.1 with ⟨hbest, hrow⟩ + refine ⟨hbest, ?_⟩ + intro j hj + simp at hj + rcases hj with hji | hj + · subst j + exact hrow + · exact htail.2 j hj + + +private theorem leastShiftedDegreeFold_valid + {M : PolynomialMatrix F} {shift : Array Nat} + (xs : List Nat) {best : Option (RowChoice F)} + (hbest : ∀ choice, best = some choice → RowChoiceValid M shift choice) + (hxs : ∀ i, i ∈ xs → i < M.size) : + ∀ choice, + xs.foldl (leastShiftedDegreeRowStep? M shift) best = some choice → + RowChoiceValid M shift choice := by + induction xs generalizing best with + | nil => + intro choice hfold + exact hbest choice hfold + | cons i xs ih => + intro choice hfold + have hstepValid : + ∀ stepChoice, + leastShiftedDegreeRowStep? M shift best i = some stepChoice → + RowChoiceValid M shift stepChoice := by + intro stepChoice hstep + cases hdeg : rowShiftedDegree? (M[i]?.getD #[]) shift with + | none => + have hstep' : best = some stepChoice := by + simpa [leastShiftedDegreeRowStep?, hdeg] using hstep + exact hbest stepChoice hstep' + | some degree => + let candidate : RowChoice F := + { index := i, row := M[i]?.getD #[], degree := degree } + cases best with + | none => + have hstep' : some candidate = some stepChoice := by + simpa [leastShiftedDegreeRowStep?, hdeg, candidate] using hstep + have hstepChoice : stepChoice = candidate := by + injection hstep' with hEq + exact hEq.symm + rw [hstepChoice] + exact ⟨by simpa [candidate] using hxs i (by simp), + by simp [candidate, Array.getD_eq_getD_getElem?], + by simpa [candidate] using hdeg⟩ + | some current => + by_cases hbetter : betterRowChoice candidate current + · have hstep' : some candidate = some stepChoice := by + simpa [leastShiftedDegreeRowStep?, hdeg, candidate, hbetter] using hstep + have hstepChoice : stepChoice = candidate := by + injection hstep' with hEq + exact hEq.symm + rw [hstepChoice] + exact ⟨by simpa [candidate] using hxs i (by simp), + by simp [candidate, Array.getD_eq_getD_getElem?], + by simpa [candidate] using hdeg⟩ + · have hstep' : some current = some stepChoice := by + simpa [leastShiftedDegreeRowStep?, hdeg, candidate, hbetter] using hstep + have hstepChoice : stepChoice = current := by + injection hstep' with hEq + exact hEq.symm + rw [hstepChoice] + exact hbest current rfl + exact ih + (best := leastShiftedDegreeRowStep? M shift best i) + hstepValid + (by + intro j hj + exact hxs j (by simp [hj])) + choice + (by simpa only [List.foldl_cons] using hfold) + + +theorem leastShiftedDegreeChoice?_some_valid + {M : PolynomialMatrix F} {shift : Array Nat} {choice : RowChoice F} + (hchoice : leastShiftedDegreeChoice? M shift = some choice) : + RowChoiceValid M shift choice := by + unfold leastShiftedDegreeChoice? at hchoice + exact leastShiftedDegreeFold_valid + (M := M) (shift := shift) (List.range M.size) + (by intro choice h; cases h) + (by intro i hi; exact List.mem_range.mp hi) + choice hchoice + + +theorem leastShiftedDegreeChoice?_degree_le + {M : PolynomialMatrix F} {shift : Array Nat} + {choice : RowChoice F} {i d : Nat} + (hchoice : leastShiftedDegreeChoice? M shift = some choice) + (hi : i < M.size) + (hdeg : rowShiftedDegree? (M.getD i #[]) shift = some d) : + choice.degree ≤ d := by + unfold leastShiftedDegreeChoice? at hchoice + have hdeg' : rowShiftedDegree? (M[i]?.getD #[]) shift = some d := by + simpa [Array.getD_eq_getD_getElem?] using hdeg + exact leastShiftedDegreeFold_degree_le_of_mem + (M := M) (shift := shift) (List.range M.size) + hchoice (List.mem_range.mpr hi) hdeg' + + +theorem leastShiftedDegreeChoice?_some_of_degree + {M : PolynomialMatrix F} {shift : Array Nat} {i d : Nat} + (hi : i < M.size) + (hdeg : rowShiftedDegree? (M.getD i #[]) shift = some d) : + ∃ choice, leastShiftedDegreeChoice? M shift = some choice ∧ choice.degree ≤ d := by + cases hchoice : leastShiftedDegreeChoice? M shift with + | none => + unfold leastShiftedDegreeChoice? at hchoice + rcases leastShiftedDegreeFold_none + (M := M) (shift := shift) (List.range M.size) hchoice with + ⟨_hbest, hall⟩ + have hnone := hall i (List.mem_range.mpr hi) + have hnone' : rowShiftedDegree? (M.getD i #[]) shift = none := by + simpa [Array.getD_eq_getD_getElem?] using hnone + rw [hnone'] at hdeg + contradiction + | some choice => + refine ⟨choice, rfl, ?_⟩ + exact leastShiftedDegreeChoice?_degree_le hchoice hi hdeg + + +theorem leastShiftedDegreeRow?_some_valid + {M : PolynomialMatrix F} {shift : Array Nat} {row : PolynomialRow F} + (hrow : leastShiftedDegreeRow? M shift = some row) : + ∃ choice, + leastShiftedDegreeChoice? M shift = some choice ∧ + RowChoiceValid M shift choice ∧ choice.row = row := by + unfold leastShiftedDegreeRow? at hrow + cases hchoice : leastShiftedDegreeChoice? M shift with + | none => + simp [hchoice] at hrow + | some choice => + simp [hchoice] at hrow + refine ⟨choice, rfl, leastShiftedDegreeChoice?_some_valid hchoice, ?_⟩ + exact hrow + +end PolynomialMatrix + + + +end CompPoly diff --git a/CompPoly/LinearAlgebra/PolynomialMatrix/StrassenCorrectness.lean b/CompPoly/LinearAlgebra/PolynomialMatrix/StrassenCorrectness.lean new file mode 100644 index 00000000..8d5131e7 --- /dev/null +++ b/CompPoly/LinearAlgebra/PolynomialMatrix/StrassenCorrectness.lean @@ -0,0 +1,818 @@ +/- +Copyright (c) 2026 CompPoly Contributors. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Valerii Huhnin +-/ + +module + +-- `coeff`, `ofArray` and friends are declared in bare `public section`s, so their +-- bodies are opaque downstream; see `docs/wiki/module-system.md`. +import all CompPoly.Univariate.Basic +import all CompPoly.Univariate.Raw.Core +public import CompPoly.LinearAlgebra.PolynomialMatrix.Operations +public import Mathlib.Tactic.Abel + +/-! +# Strassen Polynomial-Matrix Product Correctness + +Correctness proofs for the Strassen-style polynomial-matrix products in +`CompPoly.LinearAlgebra.PolynomialMatrix.Operations`: + +* `rowGet_rowMulMatrixWith`: entry semantics of the naive row-by-matrix product. +* `mulBoundedWith_eq_mulWith`: the degree-capped low-product reconstruction equals + the naive product. +* `mulStrassenWith_eq_mulWith` and `mulStrassenWithFuel_eq_mulWith`: the Strassen + recursion computes exactly the naive matrix product, as arrays. +* `mulTruncColumnStrassenWith_eq_truncateColumns` and + `mulTruncColumnStrassenWith_entry`: the column-truncated Strassen recursion + computes exactly the column-truncated naive product. +-/ + +@[expose] public section + +namespace CompPoly + +namespace PolynomialMatrix + +variable {F : Type*} + +/-! ## Generic array and fold helpers -/ + +private theorem getD_of_lt {α : Type*} (a : Array α) (d : α) {i : Nat} + (hi : i < a.size) : a.getD i d = a[i] := by + rw [Array.getD_eq_getD_getElem?, Array.getElem?_eq_getElem hi, Option.getD_some] + +private theorem getD_of_le {α : Type*} (a : Array α) (d : α) {i : Nat} + (hi : a.size ≤ i) : a.getD i d = d := by + rw [Array.getD_eq_getD_getElem?, Array.getElem?_eq_none hi, Option.getD_none] + +private theorem getD_list_range_map {α : Type*} (g : Nat → α) (n j : Nat) (d : α) : + (((List.range n).map g).toArray).getD j d = if j < n then g j else d := by + rw [Array.getD_eq_getD_getElem?, List.getElem?_toArray, List.getElem?_map] + by_cases hj : j < n + · rw [List.getElem?_range hj] + simp [hj] + · rw [List.getElem?_eq_none (by simpa using Nat.le_of_not_lt hj)] + simp [hj] + +private theorem foldl_add_eq_add_sum {M : Type*} [AddCommMonoid M] (g : Nat → M) : + ∀ (n : Nat) (init : M), + (List.range n).foldl (fun acc k ↦ acc + g k) init = + init + ∑ k ∈ Finset.range n, g k + | 0, init => by simp + | n + 1, init => by + rw [List.range_succ, List.foldl_append, foldl_add_eq_add_sum g n init, + List.foldl_cons, List.foldl_nil, Finset.sum_range_succ, add_assoc] + +private theorem foldl_add_eq_sum {M : Type*} [AddCommMonoid M] (g : Nat → M) (n : Nat) : + (List.range n).foldl (fun acc k ↦ acc + g k) 0 = ∑ k ∈ Finset.range n, g k := by + rw [foldl_add_eq_add_sum, zero_add] + +private theorem le_foldl_max_init (g : Nat → Nat) : + ∀ (l : List Nat) (init : Nat), init ≤ l.foldl (fun acc k ↦ max acc (g k)) init + | [], _ => Nat.le_refl _ + | x :: t, init => + Nat.le_trans (Nat.le_max_left init (g x)) (le_foldl_max_init g t (max init (g x))) + +private theorem le_foldl_max (g : Nat → Nat) : + ∀ (l : List Nat) (init : Nat) {k : Nat}, k ∈ l → + g k ≤ l.foldl (fun acc i ↦ max acc (g i)) init + | x :: t, init, k, hk => by + rcases List.mem_cons.1 hk with rfl | hk + · exact Nat.le_trans (Nat.le_max_right init (g k)) (le_foldl_max_init g t _) + · exact le_foldl_max g t _ hk + +/-- The fuel-bounded doubling loop reaches its target when given enough fuel. -/ +private theorem le_nextPowerOfTwoAtLeastLoop (target : Nat) : + ∀ (fuel current : Nat), 1 ≤ current → target ≤ current + fuel → + target ≤ nextPowerOfTwoAtLeastLoop target fuel current + | 0, current, _, h => by + simpa [nextPowerOfTwoAtLeastLoop] using h + | fuel + 1, current, hcur, h => by + rw [nextPowerOfTwoAtLeastLoop] + split + · assumption + · exact le_nextPowerOfTwoAtLeastLoop target fuel (2 * current) (by omega) (by omega) + +/-- `nextPowerOfTwoAtLeast` is at least its target. -/ +theorem le_nextPowerOfTwoAtLeast (target : Nat) : + target ≤ nextPowerOfTwoAtLeast target := + le_nextPowerOfTwoAtLeastLoop target (target + 1) 1 (Nat.le_refl 1) (by omega) + +/-- Entry access for `natArraySlice`. -/ +theorem natArraySlice_getD (values : Array Nat) (start count j : Nat) : + (natArraySlice values start count).getD j 0 = + if j < count then values.getD (start + j) 0 else 0 := by + rw [natArraySlice, getD_list_range_map] + +/-- Entry access for `maxNatArrays`. -/ +theorem maxNatArrays_getD (a b : Array Nat) (j : Nat) : + (maxNatArrays a b).getD j 0 = max (a.getD j 0) (b.getD j 0) := by + rw [maxNatArrays, getD_list_range_map] + split + · rfl + · rename_i h + rw [getD_of_le a 0 (by omega), getD_of_le b 0 (by omega), Nat.max_self] + +/-! ## Row access helpers -/ + +/-- Reading a row past its width yields zero. -/ +theorem rowGet_of_size_le [Zero F] {row : PolynomialRow F} {j : Nat} + (hj : row.size ≤ j) : rowGet row j = 0 := + getD_of_le row 0 hj + +private theorem rowGet_getD_of_size_le [Zero F] {M : PolynomialMatrix F} {i : Nat} + (hi : M.size ≤ i) (j : Nat) : rowGet (M.getD i #[]) j = 0 := by + rw [getD_of_le M #[] hi] + exact rowGet_of_size_le (Nat.zero_le j) + +private theorem rowGet_list_range_map [Zero F] (g : Nat → CPolynomial F) (w j : Nat) : + rowGet (((List.range w).map g).toArray) j = if j < w then g j else 0 := by + rw [rowGet, getD_list_range_map] + +/-! ## `truncateX` algebra -/ + +/-- Truncation of the zero polynomial. -/ +theorem truncateX_zero [Semiring F] [BEq F] [LawfulBEq F] (order : Nat) : + truncateX order (0 : CPolynomial F) = 0 := by + rw [CPolynomial.eq_iff_coeff] + intro i + simp only [truncateX_coeff, CPolynomial.coeff_zero, ite_self] + +/-- Truncation distributes over addition. -/ +theorem truncateX_add [Semiring F] [BEq F] [LawfulBEq F] (order : Nat) + (p q : CPolynomial F) : + truncateX order (p + q) = truncateX order p + truncateX order q := by + rw [CPolynomial.eq_iff_coeff] + intro i + simp only [truncateX_coeff, CPolynomial.coeff_add] + by_cases hi : i < order + · simp only [if_pos hi] + · simp only [if_neg hi, add_zero] + +/-- Truncation distributes over subtraction. -/ +theorem truncateX_sub [Ring F] [BEq F] [LawfulBEq F] (order : Nat) + (p q : CPolynomial F) : + truncateX order (p - q) = truncateX order p - truncateX order q := by + rw [CPolynomial.eq_iff_coeff] + intro i + simp only [truncateX_coeff, CPolynomial.coeff_sub] + by_cases hi : i < order + · simp only [if_pos hi] + · simp only [if_neg hi, sub_zero] + +/-- Nested truncations keep the smaller order. -/ +theorem truncateX_truncateX [Semiring F] [BEq F] [LawfulBEq F] (o o' : Nat) + (p : CPolynomial F) : + truncateX o (truncateX o' p) = truncateX (min o o') p := by + rw [CPolynomial.eq_iff_coeff] + intro i + simp only [truncateX_coeff] + by_cases h₁ : i < o + · by_cases h₂ : i < o' + · rw [if_pos h₁, if_pos h₂, if_pos (by omega)] + · rw [if_pos h₁, if_neg h₂, if_neg (by omega)] + · rw [if_neg h₁, if_neg (by omega)] + +/-- Truncation distributes over finite range sums. -/ +theorem truncateX_sum [Semiring F] [BEq F] [LawfulBEq F] (order n : Nat) + (f : Nat → CPolynomial F) : + truncateX order (∑ k ∈ Finset.range n, f k) = + ∑ k ∈ Finset.range n, truncateX order (f k) := by + rw [CPolynomial.eq_iff_coeff] + intro i + rw [truncateX_coeff, CPolynomial.coeff_finset_sum, CPolynomial.coeff_finset_sum] + simp only [truncateX_coeff] + by_cases hi : i < order + · simp only [if_pos hi] + · simp only [if_neg hi, Finset.sum_const_zero] + +private theorem coeff_eq_zero_of_natDegree_lt [Zero F] [BEq F] [LawfulBEq F] + {p : CPolynomial F} {i : Nat} (h : p.natDegree < i) : + CPolynomial.coeff p i = 0 := by + by_contra hne + exact absurd (CPolynomial.le_natDegree_of_ne_zero hne) (by omega) + +private theorem coeff_mul_eq_zero_of_natDegree_lt [Semiring F] [BEq F] [LawfulBEq F] + {p q : CPolynomial F} {i : Nat} (h : p.natDegree + q.natDegree < i) : + CPolynomial.coeff (p * q) i = 0 := by + rw [CPolynomial.coeff_mul] + refine Finset.sum_eq_zero fun k hk ↦ ?_ + rcases Nat.lt_or_ge p.natDegree k with hk' | hk' + · rw [coeff_eq_zero_of_natDegree_lt hk', zero_mul] + · rw [coeff_eq_zero_of_natDegree_lt (p := q) (by omega), mul_zero] + +/-- A product is unchanged by truncation past its coefficient cap. -/ +theorem truncateX_mul_of_productCoeffCap_le [Semiring F] [BEq F] [LawfulBEq F] + {p q : CPolynomial F} {order : Nat} (h : productCoeffCap p q ≤ order) : + truncateX order (p * q) = p * q := by + by_cases hz : (p == 0 || q == 0) = true + · simp only [Bool.or_eq_true, beq_iff_eq] at hz + rcases hz with hp | hq + · rw [hp, CPolynomial.zero_mul, truncateX_zero] + · rw [hq, CPolynomial.mul_zero, truncateX_zero] + · rw [productCoeffCap, if_neg hz] at h + rw [CPolynomial.eq_iff_coeff] + intro i + rw [truncateX_coeff] + split + · rfl + · exact (coeff_mul_eq_zero_of_natDegree_lt (by omega)).symm + +/-! ## `ofFn` access toolkit -/ + +/-- Row count of `ofFn`. -/ +theorem ofFn_size [Zero F] (rows width : Nat) (entry : Nat → Nat → CPolynomial F) : + (ofFn rows width entry).size = rows := by + simp only [ofFn, List.size_toArray, List.length_map, List.length_range] + +private theorem getElem_ofFn [Zero F] {rows width : Nat} + {entry : Nat → Nat → CPolynomial F} {i : Nat} + (hi : i < (ofFn rows width entry).size) : + (ofFn rows width entry)[i] = ((List.range width).map (entry i)).toArray := by + simp only [ofFn, List.getElem_toArray, List.getElem_map, List.getElem_range] + +/-- Row access for `ofFn` with zero defaults. -/ +theorem getD_ofFn [Zero F] (rows width : Nat) (entry : Nat → Nat → CPolynomial F) + (i : Nat) : + (ofFn rows width entry).getD i #[] = + if i < rows then ((List.range width).map (entry i)).toArray else #[] := by + rw [ofFn, getD_list_range_map] + +/-- Entry access for `ofFn` with zero defaults. -/ +theorem rowGet_ofFn [Zero F] (rows width : Nat) (entry : Nat → Nat → CPolynomial F) + (i j : Nat) : + rowGet ((ofFn rows width entry).getD i #[]) j = + if i < rows ∧ j < width then entry i j else 0 := by + rw [getD_ofFn] + by_cases hi : i < rows + · rw [if_pos hi, rowGet_list_range_map] + by_cases hj : j < width + · rw [if_pos hj, if_pos ⟨hi, hj⟩] + · rw [if_neg hj, if_neg (fun hc ↦ hj hc.2)] + · rw [if_neg hi, if_neg (fun hc ↦ hi hc.1)] + exact rowGet_of_size_le (Nat.zero_le j) + +private theorem matrixWidth_eq_getD_size [Zero F] (M : PolynomialMatrix F) : + MatrixWidth M = (M.getD 0 #[]).size := by + unfold MatrixWidth + rw [Array.getD_eq_getD_getElem?] + cases M[0]? <;> rfl + +/-- Width of `ofFn`. -/ +theorem MatrixWidth_ofFn [Zero F] (rows width : Nat) + (entry : Nat → Nat → CPolynomial F) : + MatrixWidth (ofFn rows width entry) = if rows = 0 then 0 else width := by + rw [matrixWidth_eq_getD_size, getD_ofFn] + by_cases h : rows = 0 + · subst h + rw [if_neg (by omega), if_pos rfl] + rfl + · rw [if_pos (by omega), if_neg h] + simp only [List.size_toArray, List.length_map, List.length_range] + +/-- Width of a square `ofFn`. -/ +theorem MatrixWidth_ofFn_square [Zero F] (n : Nat) + (entry : Nat → Nat → CPolynomial F) : + MatrixWidth (ofFn n n entry) = n := by + rw [MatrixWidth_ofFn] + split <;> omega + +/-- Two `ofFn` matrices with entrywise-equal in-range entries are equal. -/ +theorem ofFn_congr [Zero F] {rows width : Nat} {f g : Nat → Nat → CPolynomial F} + (h : ∀ i < rows, ∀ j < width, f i j = g i j) : + ofFn rows width f = ofFn rows width g := by + rw [ofFn, ofFn] + refine congrArg List.toArray (List.map_congr_left fun i hi ↦ ?_) + refine congrArg List.toArray (List.map_congr_left fun j hj ↦ ?_) + exact h i (List.mem_range.mp hi) j (List.mem_range.mp hj) + +private theorem map_eq_ofFn [Zero F] {M : PolynomialMatrix F} {w : Nat} + {f : PolynomialRow F → PolynomialRow F} {entry : Nat → Nat → CPolynomial F} + (hf : ∀ i, i < M.size → + f (M.getD i #[]) = ((List.range w).map (entry i)).toArray) : + M.map f = ofFn M.size w entry := by + refine Array.ext (by rw [Array.size_map, ofFn_size]) fun i hi₁ hi₂ ↦ ?_ + rw [Array.size_map] at hi₁ + rw [Array.getElem_map, getElem_ofFn, ← getD_of_lt M #[] hi₁] + exact hf i hi₁ + +/-! ## Naive product semantics -/ + +/-- Width of a naive row-by-matrix product. -/ +theorem rowMulMatrixWith_size [Semiring F] [BEq F] [LawfulBEq F] + (mulCtx : CPolynomial.MulContext F) (row : PolynomialRow F) + (M : PolynomialMatrix F) : + (rowMulMatrixWith mulCtx row M).size = MatrixWidth M := by + simp only [rowMulMatrixWith, List.size_toArray, List.length_map, List.length_range] + +/-- Entry semantics of the naive row-by-matrix product. -/ +theorem rowGet_rowMulMatrixWith [Semiring F] [BEq F] [LawfulBEq F] + (mulCtx : CPolynomial.MulContext F) (row : PolynomialRow F) + (M : PolynomialMatrix F) {j : Nat} (hj : j < MatrixWidth M) : + rowGet (rowMulMatrixWith mulCtx row M) j = + ∑ k ∈ Finset.range row.size, rowGet row k * rowGet (M.getD k #[]) j := by + rw [rowMulMatrixWith, rowGet_list_range_map, if_pos hj, foldl_add_eq_sum] + simp only [mulCtx.mul_eq_mul] + +/-- The naive row-by-matrix product is zero past the matrix width. -/ +theorem rowGet_rowMulMatrixWith_of_width_le [Semiring F] [BEq F] [LawfulBEq F] + (mulCtx : CPolynomial.MulContext F) (row : PolynomialRow F) + (M : PolynomialMatrix F) {j : Nat} (hj : MatrixWidth M ≤ j) : + rowGet (rowMulMatrixWith mulCtx row M) j = 0 := + rowGet_of_size_le (by rw [rowMulMatrixWith_size]; exact hj) + +private theorem sum_entry_eq_of_matrix_le [Semiring F] [BEq F] [LawfulBEq F] + (row : PolynomialRow F) (B : PolynomialMatrix F) (j : Nat) {n : Nat} + (hn : B.size ≤ n) : + ∑ k ∈ Finset.range n, rowGet row k * rowGet (B.getD k #[]) j = + ∑ k ∈ Finset.range B.size, rowGet row k * rowGet (B.getD k #[]) j := + Finset.eventually_constant_sum + (fun k hk ↦ by rw [rowGet_getD_of_size_le hk, CPolynomial.mul_zero]) hn + +private theorem sum_entry_eq_of_row_le [Semiring F] [BEq F] [LawfulBEq F] + (row : PolynomialRow F) (B : PolynomialMatrix F) (j : Nat) {n : Nat} + (hn : row.size ≤ n) : + ∑ k ∈ Finset.range n, rowGet row k * rowGet (B.getD k #[]) j = + ∑ k ∈ Finset.range row.size, rowGet row k * rowGet (B.getD k #[]) j := + Finset.eventually_constant_sum + (fun k hk ↦ by rw [rowGet_of_size_le hk, CPolynomial.zero_mul]) hn + +private theorem sum_entry_row_size_eq [Semiring F] [BEq F] [LawfulBEq F] + (row : PolynomialRow F) (B : PolynomialMatrix F) (j : Nat) : + ∑ k ∈ Finset.range row.size, rowGet row k * rowGet (B.getD k #[]) j = + ∑ k ∈ Finset.range B.size, rowGet row k * rowGet (B.getD k #[]) j := by + rw [← sum_entry_eq_of_row_le row B j (Nat.le_max_left row.size B.size), + sum_entry_eq_of_matrix_le row B j (Nat.le_max_right row.size B.size)] + +/-- The naive matrix product as an `ofFn` matrix of convolution sums. -/ +theorem mulWith_eq_ofFn [Semiring F] [BEq F] [LawfulBEq F] + (mulCtx : CPolynomial.MulContext F) (A B : PolynomialMatrix F) : + mulWith mulCtx A B = + ofFn A.size (MatrixWidth B) (fun i j ↦ + ∑ k ∈ Finset.range B.size, + rowGet (A.getD i #[]) k * rowGet (B.getD k #[]) j) := by + refine map_eq_ofFn fun i hi ↦ ?_ + rw [rowMulMatrixWith] + refine congrArg List.toArray (List.map_congr_left fun j hj ↦ ?_) + rw [foldl_add_eq_sum] + simp only [mulCtx.mul_eq_mul] + exact sum_entry_row_size_eq (A.getD i #[]) B j + +/-- Row count of the naive matrix product. -/ +theorem mulWith_size [Semiring F] [BEq F] [LawfulBEq F] + (mulCtx : CPolynomial.MulContext F) (A B : PolynomialMatrix F) : + (mulWith mulCtx A B).size = A.size := by + rw [mulWith, Array.size_map] + +/-- Rows of the naive matrix product. -/ +theorem mulWith_getD [Semiring F] [BEq F] [LawfulBEq F] + (mulCtx : CPolynomial.MulContext F) (A B : PolynomialMatrix F) {i : Nat} + (hi : i < A.size) : + (mulWith mulCtx A B).getD i #[] = rowMulMatrixWith mulCtx (A.getD i #[]) B := by + rw [mulWith, Array.getD_map_of_lt _ _ _ hi, getD_of_lt A #[] hi] + +/-- Row list of the naive matrix product. -/ +theorem matrixRows_mulWith [Semiring F] [BEq F] [LawfulBEq F] + (mulCtx : CPolynomial.MulContext F) (A B : PolynomialMatrix F) : + MatrixRows (mulWith mulCtx A B) = + (MatrixRows A).map fun row ↦ rowMulMatrixWith mulCtx row B := by + rw [MatrixRows, MatrixRows, mulWith, Array.toList_map] + +/-! ## Bounded product correctness -/ + +/-- The degree-capped row product equals the naive row product. -/ +theorem rowMulMatrixBoundedWith_eq_rowMulMatrixWith [Semiring F] [BEq F] [LawfulBEq F] + (lowCtx : MulLowContext F) (row : PolynomialRow F) (M : PolynomialMatrix F) : + rowMulMatrixBoundedWith lowCtx row M = + rowMulMatrixWith lowCtx.mulContext row M := by + simp only [rowMulMatrixBoundedWith, rowMulMatrixWith] + refine congrArg List.toArray (List.map_congr_left fun j hj ↦ ?_) + rw [foldl_add_eq_sum, foldl_add_eq_sum] + refine Finset.sum_congr rfl fun k hk ↦ ?_ + rw [mulLowXWith, lowCtx.mulLow_eq, lowCtx.mulContext.mul_eq_mul] + refine truncateX_mul_of_productCoeffCap_le ?_ + rw [rowMulMatrixEntryCoeffCap] + exact le_foldl_max + (fun k ↦ productCoeffCap (rowGet row k) (rowGet (M.getD k #[]) j)) _ _ + (List.mem_range.mpr (Finset.mem_range.mp hk)) + +/-- The degree-capped matrix product equals the naive matrix product. -/ +theorem mulBoundedWith_eq_mulWith [Semiring F] [BEq F] [LawfulBEq F] + (lowCtx : MulLowContext F) (A B : PolynomialMatrix F) : + mulBoundedWith lowCtx A B = mulWith lowCtx.mulContext A B := by + rw [mulBoundedWith, mulWith] + exact congrArg (fun f ↦ Array.map f A) + (funext fun row ↦ rowMulMatrixBoundedWith_eq_rowMulMatrixWith lowCtx row B) + +/-! ## Column-truncated naive product semantics -/ + +/-- Entry access for column truncation of a row. -/ +theorem rowGet_rowTruncateColumns [Semiring F] [BEq F] [LawfulBEq F] + (orders : Array Nat) (row : PolynomialRow F) (j : Nat) : + rowGet (rowTruncateColumns orders row) j = + truncateX (orders.getD j 0) (rowGet row j) := by + rw [rowTruncateColumns, rowGet_list_range_map] + split + · rfl + · rename_i h + rw [rowGet_of_size_le (Nat.le_of_not_lt h), truncateX_zero] + +/-- Row count of a column-truncated matrix. -/ +theorem truncateColumns_size [Semiring F] [BEq F] [LawfulBEq F] + (orders : Array Nat) (M : PolynomialMatrix F) : + (truncateColumns orders M).size = M.size := by + rw [truncateColumns, Array.size_map] + +/-- Rows of a column-truncated matrix. -/ +theorem truncateColumns_getD [Semiring F] [BEq F] [LawfulBEq F] + (orders : Array Nat) (M : PolynomialMatrix F) {i : Nat} (hi : i < M.size) : + (truncateColumns orders M).getD i #[] = + rowTruncateColumns orders (M.getD i #[]) := by + rw [truncateColumns, Array.getD_map_of_lt _ _ _ hi, getD_of_lt M #[] hi] + +/-- The truncated row product is the truncation of the naive row product. -/ +theorem rowMulMatrixTruncColumnWith_eq [Semiring F] [BEq F] [LawfulBEq F] + (lowCtx : MulLowContext F) (orders : Array Nat) (row : PolynomialRow F) + (M : PolynomialMatrix F) : + rowMulMatrixTruncColumnWith lowCtx orders row M = + rowTruncateColumns orders (rowMulMatrixWith lowCtx.mulContext row M) := by + simp only [rowMulMatrixTruncColumnWith, rowTruncateColumns, rowMulMatrixWith_size] + refine congrArg List.toArray (List.map_congr_left fun j hj ↦ ?_) + rw [foldl_add_eq_sum, + rowGet_rowMulMatrixWith lowCtx.mulContext row M (List.mem_range.mp hj), + truncateX_sum] + refine Finset.sum_congr rfl fun k _ ↦ ?_ + rw [mulLowXWith, lowCtx.mulLow_eq] + +/-- The column-truncated product is the truncation of the naive product. -/ +theorem mulTruncColumnWith_eq [Semiring F] [BEq F] [LawfulBEq F] + (lowCtx : MulLowContext F) (orders : Array Nat) (A B : PolynomialMatrix F) : + mulTruncColumnWith lowCtx orders A B = + truncateColumns orders (mulWith lowCtx.mulContext A B) := by + rw [mulTruncColumnWith, truncateColumns, mulWith, Array.map_map] + refine congrArg (fun f ↦ Array.map f A) (funext fun row ↦ ?_) + simp only [Function.comp_apply] + exact rowMulMatrixTruncColumnWith_eq lowCtx orders row B + +/-! ## Structural `ofFn` rewrites for the block combinators -/ + +/-- Pointwise addition of equally shaped `ofFn` matrices. -/ +theorem add_ofFn [Semiring F] [BEq F] [LawfulBEq F] (rows width : Nat) + (f g : Nat → Nat → CPolynomial F) : + add (ofFn rows width f) (ofFn rows width g) = + ofFn rows width (fun i j ↦ f i j + g i j) := by + by_cases h : rows = 0 + · subst h + simp [add, ofFn] + · simp only [add, ofFn_size, MatrixWidth_ofFn, if_neg h, Nat.max_self] + refine ofFn_congr fun i hi j hj ↦ ?_ + rw [rowGet_ofFn, rowGet_ofFn, if_pos ⟨hi, hj⟩, if_pos ⟨hi, hj⟩] + +/-- Pointwise subtraction of equally shaped `ofFn` matrices. -/ +theorem sub_ofFn [Ring F] [BEq F] [LawfulBEq F] (rows width : Nat) + (f g : Nat → Nat → CPolynomial F) : + sub (ofFn rows width f) (ofFn rows width g) = + ofFn rows width (fun i j ↦ f i j - g i j) := by + by_cases h : rows = 0 + · subst h + simp [sub, ofFn] + · simp only [sub, ofFn_size, MatrixWidth_ofFn, if_neg h, Nat.max_self] + refine ofFn_congr fun i hi j hj ↦ ?_ + rw [rowGet_ofFn, rowGet_ofFn, if_pos ⟨hi, hj⟩, if_pos ⟨hi, hj⟩] + +/-- The naive product of square `ofFn` matrices. -/ +theorem mulWith_ofFn_ofFn [Semiring F] [BEq F] [LawfulBEq F] + (mulCtx : CPolynomial.MulContext F) (n : Nat) + (f g : Nat → Nat → CPolynomial F) : + mulWith mulCtx (ofFn n n f) (ofFn n n g) = + ofFn n n (fun i j ↦ ∑ k ∈ Finset.range n, f i k * g k j) := by + rw [mulWith_eq_ofFn, ofFn_size, MatrixWidth_ofFn_square] + refine ofFn_congr fun i hi j hj ↦ ?_ + rw [ofFn_size] + refine Finset.sum_congr rfl fun k hk ↦ ?_ + have hk' := Finset.mem_range.mp hk + rw [rowGet_ofFn, rowGet_ofFn, if_pos ⟨hi, hk'⟩, if_pos ⟨hk', hj⟩] + +/-- Column truncation of an `ofFn` matrix. -/ +theorem truncateColumns_ofFn [Semiring F] [BEq F] [LawfulBEq F] (orders : Array Nat) + (rows width : Nat) (entry : Nat → Nat → CPolynomial F) : + truncateColumns orders (ofFn rows width entry) = + ofFn rows width (fun i j ↦ truncateX (orders.getD j 0) (entry i j)) := by + rw [truncateColumns] + have h : ∀ i, i < (ofFn rows width entry).size → + rowTruncateColumns orders ((ofFn rows width entry).getD i #[]) = + ((List.range width).map + (fun j ↦ truncateX (orders.getD j 0) (entry i j))).toArray := by + intro i hi + rw [ofFn_size] at hi + rw [getD_ofFn, if_pos hi, rowTruncateColumns] + simp only [List.size_toArray, List.length_map, List.length_range] + refine congrArg List.toArray (List.map_congr_left fun j hj ↦ ?_) + rw [rowGet_list_range_map, if_pos (List.mem_range.mp hj)] + rw [map_eq_ofFn h, ofFn_size] + +/-! ## Strassen seven-product sum identities -/ + +private theorem mul_neg' [Ring F] [BEq F] [LawfulBEq F] (p q : CPolynomial F) : + p * -q = -(p * q) := + eq_neg_of_add_eq_zero_left + (by rw [← CPolynomial.mul_add, CPolynomial.neg_add_cancel, CPolynomial.mul_zero]) + +private theorem neg_mul' [Ring F] [BEq F] [LawfulBEq F] (p q : CPolynomial F) : + (-p) * q = -(p * q) := + eq_neg_of_add_eq_zero_left + (by rw [← CPolynomial.add_mul, CPolynomial.neg_add_cancel, CPolynomial.zero_mul]) + +private theorem mul_sub' [Ring F] [BEq F] [LawfulBEq F] (p q r : CPolynomial F) : + p * (q - r) = p * q - p * r := by + rw [sub_eq_add_neg, CPolynomial.mul_add, mul_neg', ← sub_eq_add_neg] + +private theorem sub_mul' [Ring F] [BEq F] [LawfulBEq F] (p q r : CPolynomial F) : + (p - q) * r = p * r - q * r := by + rw [sub_eq_add_neg, CPolynomial.add_mul, neg_mul', ← sub_eq_add_neg] + +private theorem strassen_sum₁₁ [Ring F] [BEq F] [LawfulBEq F] (h : Nat) + (x₁ x₂ x₄ y₁ y₃ y₄ : Nat → CPolynomial F) : + ((∑ k ∈ Finset.range h, (x₁ k + x₄ k) * (y₁ k + y₄ k)) + + (∑ k ∈ Finset.range h, x₄ k * (y₃ k - y₁ k)) - + ∑ k ∈ Finset.range h, (x₁ k + x₂ k) * y₄ k) + + ∑ k ∈ Finset.range h, (x₂ k - x₄ k) * (y₃ k + y₄ k) = + (∑ k ∈ Finset.range h, x₁ k * y₁ k) + ∑ k ∈ Finset.range h, x₂ k * y₃ k := by + simp only [← Finset.sum_add_distrib, ← Finset.sum_sub_distrib] + refine Finset.sum_congr rfl fun k _ ↦ ?_ + simp only [CPolynomial.mul_add, CPolynomial.add_mul, mul_sub', sub_mul'] + abel + +private theorem strassen_sum₁₂ [Ring F] [BEq F] [LawfulBEq F] (h : Nat) + (x₁ x₂ y₂ y₄ : Nat → CPolynomial F) : + (∑ k ∈ Finset.range h, x₁ k * (y₂ k - y₄ k)) + + ∑ k ∈ Finset.range h, (x₁ k + x₂ k) * y₄ k = + (∑ k ∈ Finset.range h, x₁ k * y₂ k) + ∑ k ∈ Finset.range h, x₂ k * y₄ k := by + simp only [← Finset.sum_add_distrib] + refine Finset.sum_congr rfl fun k _ ↦ ?_ + simp only [CPolynomial.add_mul, mul_sub'] + abel + +private theorem strassen_sum₂₁ [Ring F] [BEq F] [LawfulBEq F] (h : Nat) + (x₃ x₄ y₁ y₃ : Nat → CPolynomial F) : + (∑ k ∈ Finset.range h, (x₃ k + x₄ k) * y₁ k) + + ∑ k ∈ Finset.range h, x₄ k * (y₃ k - y₁ k) = + (∑ k ∈ Finset.range h, x₃ k * y₁ k) + ∑ k ∈ Finset.range h, x₄ k * y₃ k := by + simp only [← Finset.sum_add_distrib] + refine Finset.sum_congr rfl fun k _ ↦ ?_ + simp only [CPolynomial.add_mul, mul_sub'] + abel + +private theorem strassen_sum₂₂ [Ring F] [BEq F] [LawfulBEq F] (h : Nat) + (x₁ x₃ x₄ y₁ y₂ y₄ : Nat → CPolynomial F) : + ((∑ k ∈ Finset.range h, (x₁ k + x₄ k) * (y₁ k + y₄ k)) + + (∑ k ∈ Finset.range h, x₁ k * (y₂ k - y₄ k)) - + ∑ k ∈ Finset.range h, (x₃ k + x₄ k) * y₁ k) + + ∑ k ∈ Finset.range h, (x₃ k - x₁ k) * (y₁ k + y₂ k) = + (∑ k ∈ Finset.range h, x₃ k * y₂ k) + ∑ k ∈ Finset.range h, x₄ k * y₄ k := by + simp only [← Finset.sum_add_distrib, ← Finset.sum_sub_distrib] + refine Finset.sum_congr rfl fun k _ ↦ ?_ + simp only [CPolynomial.mul_add, CPolynomial.add_mul, mul_sub', sub_mul'] + abel + +/-! ## Padding step -/ + +private theorem multiplicationDimension_bounds [Zero F] (A B : PolynomialMatrix F) : + A.size ≤ multiplicationDimension A B ∧ B.size ≤ multiplicationDimension A B ∧ + MatrixWidth B ≤ multiplicationDimension A B := by + unfold multiplicationDimension + omega + +private theorem pad_step [Semiring F] [BEq F] [LawfulBEq F] + (mulCtx : CPolynomial.MulContext F) (A B : PolynomialMatrix F) {s : Nat} + (hA : A.size ≤ s) (hBs : B.size ≤ s) (hBw : MatrixWidth B ≤ s) : + trimShape A.size (MatrixWidth B) (mulWith mulCtx (padSquare s A) (padSquare s B)) = + mulWith mulCtx A B := by + conv_rhs => rw [mulWith_eq_ofFn] + simp only [padSquare, trimShape, block, Nat.zero_add, mulWith_ofFn_ofFn] + refine ofFn_congr fun i hi j hj ↦ ?_ + rw [rowGet_ofFn, if_pos ⟨lt_of_lt_of_le hi hA, lt_of_lt_of_le hj hBw⟩] + exact sum_entry_eq_of_matrix_le (A.getD i #[]) B j hBs + +private theorem trunc_pad_step [Semiring F] [BEq F] [LawfulBEq F] + (mulCtx : CPolynomial.MulContext F) (orders : Array Nat) + (A B : PolynomialMatrix F) {s : Nat} + (hA : A.size ≤ s) (hBs : B.size ≤ s) (hBw : MatrixWidth B ≤ s) : + trimShape A.size (MatrixWidth B) + (truncateColumns (natArraySlice orders 0 s) + (mulWith mulCtx (padSquare s A) (padSquare s B))) = + truncateColumns orders (mulWith mulCtx A B) := by + conv_rhs => rw [mulWith_eq_ofFn, truncateColumns_ofFn] + simp only [padSquare, trimShape, block, Nat.zero_add, mulWith_ofFn_ofFn, + truncateColumns_ofFn] + refine ofFn_congr fun i hi j hj ↦ ?_ + rw [rowGet_ofFn, if_pos ⟨lt_of_lt_of_le hi hA, lt_of_lt_of_le hj hBw⟩, + natArraySlice_getD, if_pos (lt_of_lt_of_le hj hBw), Nat.zero_add] + exact congrArg (truncateX (orders.getD j 0)) + (sum_entry_eq_of_matrix_le (A.getD i #[]) B j hBs) + +/-! ## Full Strassen correctness -/ + +/-- The fuel-bounded Strassen product equals the naive matrix product. -/ +theorem mulStrassenWithFuel_eq_mulWith [Ring F] [BEq F] [LawfulBEq F] + (lowCtx : MulLowContext F) (leafCutoff fuel : Nat) (A B : PolynomialMatrix F) : + mulStrassenWithFuel lowCtx leafCutoff fuel A B = mulWith lowCtx.mulContext A B := by + induction fuel generalizing A B with + | zero => + simp only [mulStrassenWithFuel] + exact mulBoundedWith_eq_mulWith lowCtx A B + | succ fuel ih => + simp only [mulStrassenWithFuel] + split + · exact mulBoundedWith_eq_mulWith lowCtx A B + split + · rename_i hguard + simp only [Bool.and_eq_true, beq_iff_eq] at hguard + obtain ⟨⟨⟨_hAw, hBs⟩, hBw⟩, heven⟩ := hguard + simp only [ih] + generalize hh : A.size / 2 = h + have hAs : A.size = h + h := by omega + conv_rhs => rw [mulWith_eq_ofFn] + simp only [hBw, hBs, hAs] + simp only [block, joinSquareBlocks, two_mul, Nat.zero_add, add_ofFn, sub_ofFn, + mulWith_ofFn_ofFn] + refine ofFn_congr fun i hi j hj ↦ ?_ + by_cases hi' : i < h <;> by_cases hj' : j < h + · rw [if_pos hi', if_pos hj', rowGet_ofFn, if_pos ⟨hi', hj'⟩, + Finset.sum_range_add] + exact strassen_sum₁₁ h _ _ _ _ _ _ + · obtain ⟨j', rfl⟩ : ∃ j', j = h + j' := ⟨j - h, by omega⟩ + have hj'' : j' < h := by omega + rw [if_pos hi', if_neg hj', Nat.add_sub_cancel_left, rowGet_ofFn, + if_pos ⟨hi', hj''⟩, Finset.sum_range_add] + exact strassen_sum₁₂ h _ _ _ _ + · obtain ⟨i', rfl⟩ : ∃ i', i = h + i' := ⟨i - h, by omega⟩ + have hi'' : i' < h := by omega + rw [if_neg hi', Nat.add_sub_cancel_left, if_pos hj', rowGet_ofFn, + if_pos ⟨hi'', hj'⟩, Finset.sum_range_add] + exact strassen_sum₂₁ h _ _ _ _ + · obtain ⟨i', rfl⟩ : ∃ i', i = h + i' := ⟨i - h, by omega⟩ + obtain ⟨j', rfl⟩ : ∃ j', j = h + j' := ⟨j - h, by omega⟩ + have hi'' : i' < h := by omega + have hj'' : j' < h := by omega + rw [if_neg hi', if_neg hj', Nat.add_sub_cancel_left, + Nat.add_sub_cancel_left, rowGet_ofFn, if_pos ⟨hi'', hj''⟩, + Finset.sum_range_add] + exact strassen_sum₂₂ h _ _ _ _ _ _ + · rw [ih] + obtain ⟨hdA, hdB, hdW⟩ := multiplicationDimension_bounds A B + have hs := le_nextPowerOfTwoAtLeast (multiplicationDimension A B) + exact pad_step lowCtx.mulContext A B (hdA.trans hs) (hdB.trans hs) + (hdW.trans hs) + +/-- The Strassen product equals the naive matrix product. -/ +theorem mulStrassenWith_eq_mulWith [Ring F] [BEq F] [LawfulBEq F] + (lowCtx : MulLowContext F) (leafCutoff : Nat) (A B : PolynomialMatrix F) : + mulStrassenWith lowCtx leafCutoff A B = mulWith lowCtx.mulContext A B := + mulStrassenWithFuel_eq_mulWith lowCtx leafCutoff (multiplicationDimension A B + 1) A B + +/-- Row count of the Strassen product. -/ +theorem mulStrassenWith_size [Ring F] [BEq F] [LawfulBEq F] + (lowCtx : MulLowContext F) (leafCutoff : Nat) (A B : PolynomialMatrix F) : + (mulStrassenWith lowCtx leafCutoff A B).size = A.size := by + rw [mulStrassenWith_eq_mulWith, mulWith_size] + +/-- Rows of the Strassen product are the naive row-by-matrix products. -/ +theorem mulStrassenWith_getD [Ring F] [BEq F] [LawfulBEq F] + (lowCtx : MulLowContext F) (leafCutoff : Nat) (A B : PolynomialMatrix F) + {i : Nat} (hi : i < A.size) : + (mulStrassenWith lowCtx leafCutoff A B).getD i #[] = + rowMulMatrixWith lowCtx.mulContext (A.getD i #[]) B := by + rw [mulStrassenWith_eq_mulWith, mulWith_getD lowCtx.mulContext A B hi] + +/-- Row list of the Strassen product. -/ +theorem matrixRows_mulStrassenWith [Ring F] [BEq F] [LawfulBEq F] + (lowCtx : MulLowContext F) (leafCutoff : Nat) (A B : PolynomialMatrix F) : + MatrixRows (mulStrassenWith lowCtx leafCutoff A B) = + (MatrixRows A).map fun row ↦ rowMulMatrixWith lowCtx.mulContext row B := by + rw [mulStrassenWith_eq_mulWith, matrixRows_mulWith] + +/-! ## Column-truncated Strassen correctness -/ + +/-- The fuel-bounded column-truncated Strassen product equals the +column-truncated naive matrix product. -/ +theorem mulTruncColumnStrassenWithFuel_eq_truncateColumns [Ring F] [BEq F] [LawfulBEq F] + (lowCtx : MulLowContext F) (leafCutoff fuel : Nat) (orders : Array Nat) + (A B : PolynomialMatrix F) : + mulTruncColumnStrassenWithFuel lowCtx leafCutoff fuel orders A B = + truncateColumns orders (mulWith lowCtx.mulContext A B) := by + induction fuel generalizing orders A B with + | zero => + simp only [mulTruncColumnStrassenWithFuel] + exact mulTruncColumnWith_eq lowCtx orders A B + | succ fuel ih => + simp only [mulTruncColumnStrassenWithFuel] + split + · exact mulTruncColumnWith_eq lowCtx orders A B + split + · rename_i hguard + simp only [Bool.and_eq_true, beq_iff_eq] at hguard + obtain ⟨⟨⟨_hAw, hBs⟩, hBw⟩, heven⟩ := hguard + simp only [ih] + generalize hh : A.size / 2 = h + have hAs : A.size = h + h := by omega + conv_rhs => rw [mulWith_eq_ofFn, truncateColumns_ofFn] + simp only [hBw, hBs, hAs] + simp only [block, joinSquareBlocks, two_mul, Nat.zero_add, add_ofFn, sub_ofFn, + mulWith_ofFn_ofFn, truncateColumns_ofFn] + refine ofFn_congr fun i hi j hj ↦ ?_ + by_cases hi' : i < h <;> by_cases hj' : j < h + · rw [if_pos hi', if_pos hj', rowGet_ofFn, if_pos ⟨hi', hj'⟩] + simp only [natArraySlice_getD, maxNatArrays_getD, if_pos hj', Nat.zero_add] + have hmin₁ : min (orders.getD j 0) + (max (orders.getD j 0) (orders.getD (h + j) 0)) = orders.getD j 0 := + Nat.min_eq_left (Nat.le_max_left _ _) + simp only [truncateX_add, truncateX_sub, truncateX_truncateX, hmin₁, + Nat.min_self] + simp only [← truncateX_add, ← truncateX_sub] + rw [Finset.sum_range_add] + exact congrArg (truncateX (orders.getD j 0)) (strassen_sum₁₁ h _ _ _ _ _ _) + · obtain ⟨j', rfl⟩ : ∃ j', j = h + j' := ⟨j - h, by omega⟩ + have hj'' : j' < h := by omega + rw [if_pos hi', if_neg hj', Nat.add_sub_cancel_left, rowGet_ofFn, + if_pos ⟨hi', hj''⟩] + simp only [natArraySlice_getD, maxNatArrays_getD, if_pos hj'', Nat.zero_add] + have hmin₁ : min (orders.getD (h + j') 0) + (max (orders.getD j' 0) (orders.getD (h + j') 0)) = + orders.getD (h + j') 0 := + Nat.min_eq_left (Nat.le_max_right _ _) + simp only [truncateX_add, truncateX_truncateX, hmin₁, Nat.min_self] + simp only [← truncateX_add] + rw [Finset.sum_range_add] + exact congrArg (truncateX (orders.getD (h + j') 0)) + (strassen_sum₁₂ h _ _ _ _) + · obtain ⟨i', rfl⟩ : ∃ i', i = h + i' := ⟨i - h, by omega⟩ + have hi'' : i' < h := by omega + rw [if_neg hi', Nat.add_sub_cancel_left, if_pos hj', rowGet_ofFn, + if_pos ⟨hi'', hj'⟩] + simp only [natArraySlice_getD, maxNatArrays_getD, if_pos hj', Nat.zero_add] + have hmin₁ : min (orders.getD j 0) + (max (orders.getD j 0) (orders.getD (h + j) 0)) = orders.getD j 0 := + Nat.min_eq_left (Nat.le_max_left _ _) + simp only [truncateX_add, truncateX_truncateX, hmin₁, Nat.min_self] + simp only [← truncateX_add] + rw [Finset.sum_range_add] + exact congrArg (truncateX (orders.getD j 0)) (strassen_sum₂₁ h _ _ _ _) + · obtain ⟨i', rfl⟩ : ∃ i', i = h + i' := ⟨i - h, by omega⟩ + obtain ⟨j', rfl⟩ : ∃ j', j = h + j' := ⟨j - h, by omega⟩ + have hi'' : i' < h := by omega + have hj'' : j' < h := by omega + rw [if_neg hi', if_neg hj', Nat.add_sub_cancel_left, + Nat.add_sub_cancel_left, rowGet_ofFn, if_pos ⟨hi'', hj''⟩] + simp only [natArraySlice_getD, maxNatArrays_getD, if_pos hj'', Nat.zero_add] + have hmin₁ : min (orders.getD (h + j') 0) + (max (orders.getD j' 0) (orders.getD (h + j') 0)) = + orders.getD (h + j') 0 := + Nat.min_eq_left (Nat.le_max_right _ _) + simp only [truncateX_add, truncateX_sub, truncateX_truncateX, hmin₁, + Nat.min_self] + simp only [← truncateX_add, ← truncateX_sub] + rw [Finset.sum_range_add] + exact congrArg (truncateX (orders.getD (h + j') 0)) + (strassen_sum₂₂ h _ _ _ _ _ _) + · rw [ih] + obtain ⟨hdA, hdB, hdW⟩ := multiplicationDimension_bounds A B + have hs := le_nextPowerOfTwoAtLeast (multiplicationDimension A B) + exact trunc_pad_step lowCtx.mulContext orders A B (hdA.trans hs) + (hdB.trans hs) (hdW.trans hs) + +/-- The column-truncated Strassen product equals the column-truncated naive +matrix product. -/ +theorem mulTruncColumnStrassenWith_eq_truncateColumns [Ring F] [BEq F] [LawfulBEq F] + (lowCtx : MulLowContext F) (leafCutoff : Nat) (orders : Array Nat) + (A B : PolynomialMatrix F) : + mulTruncColumnStrassenWith lowCtx leafCutoff orders A B = + truncateColumns orders (mulWith lowCtx.mulContext A B) := + mulTruncColumnStrassenWithFuel_eq_truncateColumns lowCtx leafCutoff + (multiplicationDimension A B + 1) orders A B + +/-- Row count of the column-truncated Strassen product. -/ +theorem mulTruncColumnStrassenWith_size [Ring F] [BEq F] [LawfulBEq F] + (lowCtx : MulLowContext F) (leafCutoff : Nat) (orders : Array Nat) + (A B : PolynomialMatrix F) : + (mulTruncColumnStrassenWith lowCtx leafCutoff orders A B).size = A.size := by + rw [mulTruncColumnStrassenWith_eq_truncateColumns, truncateColumns_size, + mulWith_size] + +/-- Entries of the column-truncated Strassen product are the order-truncated +naive product entries. -/ +theorem mulTruncColumnStrassenWith_entry [Ring F] [BEq F] [LawfulBEq F] + (lowCtx : MulLowContext F) (leafCutoff : Nat) (orders : Array Nat) + (A B : PolynomialMatrix F) {i : Nat} (hi : i < A.size) (j : Nat) : + rowGet ((mulTruncColumnStrassenWith lowCtx leafCutoff orders A B).getD i #[]) j = + truncateX (orders.getD j 0) + (rowGet (rowMulMatrixWith lowCtx.mulContext (A.getD i #[]) B) j) := by + rw [mulTruncColumnStrassenWith_eq_truncateColumns, + truncateColumns_getD orders _ (by rw [mulWith_size]; exact hi), + mulWith_getD lowCtx.mulContext A B hi, rowGet_rowTruncateColumns] + +end PolynomialMatrix + +end CompPoly diff --git a/CompPoly/LinearAlgebra/README.md b/CompPoly/LinearAlgebra/README.md index 78cadad6..9bc293b6 100644 --- a/CompPoly/LinearAlgebra/README.md +++ b/CompPoly/LinearAlgebra/README.md @@ -1,4 +1,4 @@ -# Computable Linear Algebra +# Linear Algebra Executable matrices for [CompPoly](../../README.md), in two independent flavours: dense matrices over a field, and row-oriented matrices whose entries are @@ -6,6 +6,9 @@ univariate polynomials. Both exist to serve the Guruswami-Sudan interpolation backends (see [`../../docs/wiki/coding-theory.md`](../../docs/wiki/coding-theory.md)), but neither depends on the decoder and both are usable on their own. +`TensorProduct/Basis.lean` contains generic tensor basis theory with explicit +scalar actions, independently of the executable matrix layers. + ## Types | Type | Description | @@ -82,7 +85,39 @@ fused `rowSubScaledShift` update. direct definitions transfers to the fast ones. Write proofs against the direct version; call the fast one. -## Conventions +## Tensor product bases (`TensorProduct/`) + +`TensorProduct/Basis.lean` provides `Module.Basis.baseChangeRight`: a basis of +`Left ⊗[K] Right` over `Right` with basis vectors `b i ⊗ₜ[K] 1` and scalars acting +on the right factor. Its construction uses Mathlib's +`Algebra.TensorProduct.commRight.toLinearEquiv`. The coordinate formula is +`baseChangeRight_repr_tmul`; the basis vectors are exposed by +`baseChangeRight_apply`. Standard `Basis.sum_repr` and `Basis.repr_sum_self` +give reconstruction and coordinate recovery. + +Mathlib provides direct tensor instances for `Algebra`, `Module`, `DistribMulAction` +and `SMul`, using the left factor. For a right view on equal factors, select all +four locally: + +```lean +letI rightAlgebra : Algebra L (L ⊗[K] L) := Algebra.TensorProduct.rightAlgebra +letI : Module L (L ⊗[K] L) := rightAlgebra.toModule +letI : DistribMulAction L (L ⊗[K] L) := rightAlgebra.toModule.toDistribMulAction +letI : SMul L (L ⊗[K] L) := rightAlgebra.toSMul +let bRight := b.baseChangeRight (Right := L) +``` + +The module selection allows right-basis projections such as `bRight.repr`. +The `SMul` selection makes `c • z` act on the right factor, and `DistribMulAction` +ensures that ordinary laws such as `smul_add` and `mul_smul` use that same action. +Selecting the algebra alone does not override those direct defaults. No alternative +global instance is installed by this module or its binary-tower re-export. + +This recipe chooses one meaning for scalar notation in its scope. Both coordinate +functions can coexist after their respective module choices have been fixed; +the recipe does not provide two simultaneous meanings for `•` on the same carrier. + +## Matrix conventions - Both layers are `Array`-backed and index with plain `Nat`, with out-of-bounds reads returning a default rather than requiring a proof at the call site. Shape diff --git a/CompPoly/LinearAlgebra/TensorProduct/Basis.lean b/CompPoly/LinearAlgebra/TensorProduct/Basis.lean new file mode 100644 index 00000000..347ba6db --- /dev/null +++ b/CompPoly/LinearAlgebra/TensorProduct/Basis.lean @@ -0,0 +1,74 @@ +/- +Copyright (c) 2025 ArkLib Contributors. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Chung Thai Nguyen, Quang Dao +-/ +module + +public import Mathlib.LinearAlgebra.TensorProduct.Basis +public import Mathlib.RingTheory.TensorProduct.Maps + +/-! +# Bases for the right scalar action on tensor products + +`Module.Basis.baseChangeRight` lifts a basis of `Left` over `K` to a basis of +`Left ⊗[K] Right` over `Right`, with scalars acting on the right tensor factor. +The action is explicit in each declaration; importing this module preserves Mathlib's +default action on the left factor, including when `Left = Right`. + +For equal factors, select the right `Algebra`, `Module`, `DistribMulAction` and `SMul` +locally when combining this basis with scalar notation and module laws. Tensor products +provide independent default instances at each of these levels. + +For background on tensor-product bases, see [Lan02]. For equal field factors, this +right-action basis is the basis used for the row representation in [DP24], §2.5. + +## References + +* [Lang, S., *Algebra*][Lan02] +* [Diamond, B. E. and Posen, J., *Polylogarithmic proofs for multilinears over binary towers*][DP24] +-/ + +@[expose] public section + +open scoped TensorProduct + +namespace Module.Basis + +variable {K Left Right ι : Type*} [CommSemiring K] [Semiring Left] [CommSemiring Right] + [Algebra K Left] [Algebra K Right] + +/-- Lift a `K`-basis of `Left` to a `Right`-basis of `Left ⊗[K] Right`, using the +right-factor scalar action. This is the right-sided counterpart to `Basis.baseChange`. -/ +noncomputable def baseChangeRight (b : Basis ι K Left) : + letI rightAlgebra := Algebra.TensorProduct.rightAlgebra (R := K) (A := Left) (B := Right) + letI := rightAlgebra.toModule + Basis ι Right (Left ⊗[K] Right) := by + letI rightAlgebra := Algebra.TensorProduct.rightAlgebra (R := K) (A := Left) (B := Right) + letI := rightAlgebra.toModule + exact (b.baseChange Right).map (Algebra.TensorProduct.commRight K Right Left).toLinearEquiv + +/-- Coordinates of a pure tensor in the basis for the right-factor scalar action. -/ +@[simp] +lemma baseChangeRight_repr_tmul (b : Basis ι K Left) (x : Left) (y : Right) (i : ι) : + letI rightAlgebra := Algebra.TensorProduct.rightAlgebra (R := K) (A := Left) (B := Right) + letI := rightAlgebra.toModule + (b.baseChangeRight (Right := Right)).repr (x ⊗ₜ[K] y) i = b.repr x i • y := by + let rightAlgebra := Algebra.TensorProduct.rightAlgebra (R := K) (A := Left) (B := Right) + let := rightAlgebra.toModule + rw [baseChangeRight, Basis.map_repr] + change (b.baseChange Right).repr (y ⊗ₜ[K] x) i = b.repr x i • y + exact baseChange_repr_tmul Right b y x i + +/-- The right-action basis vectors are the original basis vectors tensored with one. -/ +@[simp] +lemma baseChangeRight_apply (b : Basis ι K Left) (i : ι) : + letI rightAlgebra := Algebra.TensorProduct.rightAlgebra (R := K) (A := Left) (B := Right) + letI := rightAlgebra.toModule + b.baseChangeRight (Right := Right) i = b i ⊗ₜ[K] 1 := by + let rightAlgebra := Algebra.TensorProduct.rightAlgebra (R := K) (A := Left) (B := Right) + let := rightAlgebra.toModule + simp only [baseChangeRight, Basis.map_apply, AlgEquiv.toLinearEquiv_apply, + baseChange_apply, Algebra.TensorProduct.commRight_tmul] + +end Module.Basis diff --git a/CompPoly/Multilinear/Basic.lean b/CompPoly/Multilinear/Basic.lean index 623fd988..3b8ef932 100644 --- a/CompPoly/Multilinear/Basic.lean +++ b/CompPoly/Multilinear/Basic.lean @@ -1,7 +1,7 @@ /- Copyright (c) 2025 CompPoly. All rights reserved. Released under Apache 2.0 license as described in the file LICENSE. -Authors: Quang Dao, Chung Thai Nguyen +Authors: Quang Dao, Chung Thai Nguyen, Aristotle (Harmonic), Elias Judin -/ module @@ -524,6 +524,18 @@ def eval₂Mle (p : CMlPolynomialEval R n) (f : R →+* S) (x : Vector S n) : S def eval (p : CMlPolynomialEval R n) (x : Vector R n) : R := Vector.dotProduct p (lagrangeBasis x) +/-- Evaluation commutes with scalar multiplication of a hypercube table. -/ +theorem eval_smul (a : R) (p : CMlPolynomialEval R n) (x : Vector R n) : + eval (a • p) x = a * eval p x := by + unfold eval + rw [Vector.dotProduct_eq_root_dotProduct, Vector.dotProduct_eq_root_dotProduct] + simp only [_root_.dotProduct, Finset.mul_sum] + apply Finset.sum_congr rfl + intro i _ + rw [Vector.get_eq_getElem (a • p) i, Vector.get_eq_getElem p i, + Vector.getElem_smul] + simp only [smul_eq_mul, mul_assoc] + /-- Evaluate a `CMlPolynomialEval` at a point using a ring homomorphism -/ def eval₂ (p : CMlPolynomialEval R n) (f : R →+* S) (x : Vector S n) : S := eval (map f p) x @@ -584,6 +596,45 @@ theorem eval_mle_eq_eval (p : CMlPolynomialEval R n) (x : Vector R n) : · simp only [eval] exact eval_mle_step_dot_product p x +/-- The multilinear equality kernel is the product of its coordinatewise affine factors. -/ +theorem eqTilde_eq_prod (w x : Vector R n) : + eqTilde w x = ∏ i : Fin n, (w[i] * x[i] + (1 - w[i]) * (1 - x[i])) := by + rw [eqTilde, ← eval_mle_eq_eval] + induction n with + | zero => simp [evalMle, evalMleValues] + | succ n ih => + rw [evalMle_succ] + have hstep : evalMleLayer (lagrangeBasis w) x.head = + (w.head * x.head + (1 - w.head) * (1 - x.head)) • lagrangeBasis w.tail := by + apply Vector.ext + intro j hj + rw [← Vector.get_eq_getElem (evalMleLayer (lagrangeBasis w) x.head) ⟨j, hj⟩] + rw [← Vector.get_eq_getElem + ((w.head * x.head + (1 - w.head) * (1 - x.head)) • lagrangeBasis w.tail) + ⟨j, hj⟩] + rw [evalMleLayer_get, lagrange_basis_even, lagrange_basis_odd] + rw [Vector.get_eq_getElem + ((w.head * x.head + (1 - w.head) * (1 - x.head)) • lagrangeBasis w.tail) + ⟨j, hj⟩] + rw [Vector.getElem_smul] + simp only [smul_eq_mul] + change (1 - x.head) * ((1 - w.head) * (lagrangeBasis w.tail).get ⟨j, hj⟩) + + x.head * (w.head * (lagrangeBasis w.tail).get ⟨j, hj⟩) = + (w.head * x.head + (1 - w.head) * (1 - x.head)) * + (lagrangeBasis w.tail).get ⟨j, hj⟩ + ring + rw [hstep, eval_mle_eq_eval, eval_smul, ← eval_mle_eq_eval, ih, + Fin.prod_univ_succ] + congr 1 + exact Finset.prod_congr rfl fun i _ => by simp [Nat.add_comm] + +/-- The equality kernel factors across appended coordinate blocks. -/ +theorem eqTilde_append {m : ℕ} (w₁ x₁ : Vector R n) (w₂ x₂ : Vector R m) : + eqTilde (w₁ ++ w₂) (x₁ ++ x₂) = eqTilde w₁ x₁ * eqTilde w₂ x₂ := by + simp only [eqTilde_eq_prod] + rw [Fin.prod_univ_add] + congr 1 <;> exact Finset.prod_congr rfl fun i _ => by simp + /-- Multilinear-extension interpolation through a ring homomorphism agrees with the dot-product evaluator. -/ theorem eval₂_mle_eq_eval₂ (p : CMlPolynomialEval R n) (f : R →+* S) (x : Vector S n) : diff --git a/CompPoly/Multivariate/MvPolyEquiv/Core.lean b/CompPoly/Multivariate/MvPolyEquiv/Core.lean index 1912a350..9ef03a4b 100644 --- a/CompPoly/Multivariate/MvPolyEquiv/Core.lean +++ b/CompPoly/Multivariate/MvPolyEquiv/Core.lean @@ -6,6 +6,7 @@ Authors: Frantisek Silvasi, Julian Sutherland, Andrei Burdușa, Dimitris Mitsios module public import Batteries.Data.Vector.Lemmas +public import CompPoly.Data.List.Lemmas public import CompPoly.Multivariate.CMvPolynomial public import Mathlib.Algebra.MvPolynomial.Basic public import Mathlib.Algebra.MvPolynomial.Equiv @@ -61,7 +62,7 @@ noncomputable def toCMvPolynomial (p : MvPolynomial (Fin n) R) : CMvPolynomial n exact h₁ case distinct => simp only [List.pairwise_map] - exact List.distinct_of_inj_nodup CMvMonomial.injective_ofFinsupp (Finset.nodup_toList _) + exact List.Nodup.pairwise_ne_map CMvMonomial.injective_ofFinsupp (Finset.nodup_toList _) grind ⟩ @@ -83,7 +84,7 @@ theorem toCMvPolynomial_fromCMvPolynomial {p : CMvPolynomial n R} : grind case distinct => simp only [Std.compare_eq_iff_eq, List.pairwise_map] - exact List.distinct_of_inj_nodup CMvMonomial.injective_ofFinsupp (Finset.nodup_toList _) + exact List.Nodup.pairwise_ne_map CMvMonomial.injective_ofFinsupp (Finset.nodup_toList _) omit [BEq R] [LawfulBEq R] in @[grind =, simp] @@ -107,7 +108,7 @@ theorem fromCMvPolynomial_toCMvPolynomial {p : MvPolynomial (Fin n) R} : (mem := by simp; use m) (distinct := ?distinct)] case distinct => simp only [Std.compare_eq_iff_eq, List.pairwise_map] - exact List.distinct_of_inj_nodup CMvMonomial.injective_ofFinsupp (Finset.nodup_toList _) + exact List.Nodup.pairwise_ne_map CMvMonomial.injective_ofFinsupp (Finset.nodup_toList _) · have : ∀ x ∈ s, CMvMonomial.ofFinsupp x ≠ CMvMonomial.ofFinsupp m := by aesop grind diff --git a/CompPoly/Multivariate/Operations.lean b/CompPoly/Multivariate/Operations.lean index f513670f..d8192417 100644 --- a/CompPoly/Multivariate/Operations.lean +++ b/CompPoly/Multivariate/Operations.lean @@ -460,6 +460,45 @@ attribute [grind =] aeval_C aeval_X aeval_add aeval_mul aeval_zero aeval_one aeval_pow aeval_neg aeval_sub +/-- The computable substitution `bind₁` agrees with Mathlib substitution after +transporting through `fromCMvPolynomial`. -/ +theorem fromCMvPolynomial_bind₁ {n m : ℕ} {R : Type*} [CommSemiring R] [BEq R] + [LawfulBEq R] (f : Fin n → CMvPolynomial m R) (p : CMvPolynomial n R) : + fromCMvPolynomial (bind₁ f p) = + MvPolynomial.eval₂ MvPolynomial.C (fun i => fromCMvPolynomial (f i)) + (fromCMvPolynomial p) := by + rw [bind₁_eq_aeval] + unfold aeval + have h := MvPolynomial.map_eval₂Hom + (f := algebraMap R (CMvPolynomial m R)) + (g := f) + (φ := (CPoly.polyRingEquiv (n := m) (R := R)).toRingHom) + (p := fromCMvPolynomial p) + have hcomp : + ((CPoly.polyRingEquiv (n := m) (R := R)).toRingHom).comp + (algebraMap R (CMvPolynomial m R)) = MvPolynomial.C := by + ext r μ + rw [RingHom.comp_apply] + change MvPolynomial.coeff μ + (fromCMvPolynomial (algebraMap R (CMvPolynomial m R) r)) = + MvPolynomial.coeff μ (MvPolynomial.C r) + rw [show (algebraMap R (CMvPolynomial m R)) r = CMvPolynomial.C (n := m) r from rfl] + rw [fromCMvPolynomial_C] + rw [eval₂_equiv (p := p) (f := algebraMap R (CMvPolynomial m R)) (vals := f)] + have h' : + fromCMvPolynomial + (MvPolynomial.eval₂ (algebraMap R (CMvPolynomial m R)) f + (fromCMvPolynomial p)) = + MvPolynomial.eval₂ + (((CPoly.polyRingEquiv (n := m) (R := R)).toRingHom).comp + (algebraMap R (CMvPolynomial m R))) + (fun i => fromCMvPolynomial (f i)) + (fromCMvPolynomial p) := by + simpa only [polyRingEquiv, polyEquiv, RingEquiv.toRingHom_eq_coe, + MvPolynomial.coe_eval₂Hom, RingHom.coe_coe, RingEquiv.coe_mk, Equiv.coe_fn_mk] using h + rw [hcomp] at h' + exact h' + end CMvPolynomial namespace Lawful diff --git a/CompPoly/Multivariate/PartialEval.lean b/CompPoly/Multivariate/PartialEval.lean new file mode 100644 index 00000000..5408fc66 --- /dev/null +++ b/CompPoly/Multivariate/PartialEval.lean @@ -0,0 +1,187 @@ +/- +Copyright (c) 2026 CompPoly. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Cody Gunton +-/ +module + +public import CompPoly.Multivariate.Operations +public import Mathlib.Algebra.MvPolynomial.Degrees + +/-! +# Partial evaluation of the first variable of a `CMvPolynomial` + +`partialEvalFirst a p` fixes variable `0` of a multivariate computable +polynomial to a scalar value, together with its evaluation lemma and the +per-variable degree-bound lemma. +-/ + +@[expose] public section + +namespace CPoly + +namespace CMvPolynomial + +variable {n : ℕ} {R : Type*} [CommSemiring R] [BEq R] [LawfulBEq R] + +omit [BEq R] [LawfulBEq R] in +private lemma partialEvalFirst_subst_degreeOf_le [Nontrivial R] (a : R) + (i : Fin n) (j : Fin (n + 1)) : + MvPolynomial.degreeOf i + (Fin.cases (MvPolynomial.C a) (fun k : Fin n => MvPolynomial.X k) j : + MvPolynomial (Fin n) R) ≤ if j = i.succ then 1 else 0 := by + cases j using Fin.cases with + | zero => + simp only [Fin.cases_zero, MvPolynomial.degreeOf_C, zero_le] + | succ j => + by_cases h : j = i + · subst h + simp only [Fin.cases_succ, MvPolynomial.degreeOf_X_self, ↓reduceIte, Std.le_refl] + · have hsucc : Fin.succ j ≠ i.succ := by + intro h' + exact h (Fin.succ_inj.mp h') + have hi_ne_j : i ≠ j := fun hij => h hij.symm + simp only [Fin.cases_succ, MvPolynomial.degreeOf_X, hi_ne_j, ↓reduceIte, hsucc, + Std.le_refl] + +omit [BEq R] [LawfulBEq R] in +private lemma partialEvalFirst_eval₂_monomial_degreeOf_le [Nontrivial R] {deg : ℕ} + (a : R) (i : Fin n) (s : Fin (n + 1) →₀ ℕ) (c : R) + (hs : s i.succ ≤ deg) : + MvPolynomial.degreeOf i + (MvPolynomial.eval₂ MvPolynomial.C + (fun j : Fin (n + 1) => + Fin.cases (MvPolynomial.C a) (fun k : Fin n => MvPolynomial.X k) j) + (MvPolynomial.monomial s c) : MvPolynomial (Fin n) R) ≤ deg := by + rw [MvPolynomial.eval₂_monomial] + refine (MvPolynomial.degreeOf_C_mul_le _ i c).trans ?_ + rw [Finsupp.prod] + refine (MvPolynomial.degreeOf_prod_le i s.support + (fun j => (Fin.cases (MvPolynomial.C a) (fun k : Fin n => MvPolynomial.X k) j : + MvPolynomial (Fin n) R) ^ s j)).trans ?_ + calc + (∑ x ∈ s.support, + MvPolynomial.degreeOf i + ((Fin.cases (MvPolynomial.C a) (fun k : Fin n => MvPolynomial.X k) x : + MvPolynomial (Fin n) R) ^ s x)) + ≤ ∑ x ∈ s.support, s x * (if x = i.succ then 1 else 0) := by + refine Finset.sum_le_sum ?_ + intro x hx + exact (MvPolynomial.degreeOf_pow_le i + (Fin.cases (MvPolynomial.C a) (fun k : Fin n => MvPolynomial.X k) x : + MvPolynomial (Fin n) R) (s x)).trans + (Nat.mul_le_mul_left _ (partialEvalFirst_subst_degreeOf_le a i x)) + _ ≤ s i.succ := by + classical + by_cases hi : i.succ ∈ s.support + · rw [Finset.sum_eq_single i.succ] + · simp only [↓reduceIte, mul_one, Std.le_refl] + · intro x hx hne + simp only [hne, ↓reduceIte, mul_zero] + · intro hnot + exact False.elim (hnot hi) + · rw [Finset.sum_eq_zero] + · simp only [zero_le] + · intro x hx + have hne : x ≠ i.succ := fun h => hi (h ▸ hx) + simp only [hne, ↓reduceIte, mul_zero] + _ ≤ deg := hs + +omit [BEq R] [LawfulBEq R] in +private lemma partialEvalFirst_eval₂_degreeOf_le [Nontrivial R] {deg : ℕ} + (a : R) (i : Fin n) (p : MvPolynomial (Fin (n + 1)) R) + (hDeg : ∀ s ∈ p.support, s i.succ ≤ deg) : + MvPolynomial.degreeOf i + (MvPolynomial.eval₂ MvPolynomial.C + (fun j : Fin (n + 1) => + Fin.cases (MvPolynomial.C a) (fun k : Fin n => MvPolynomial.X k) j) + p : MvPolynomial (Fin n) R) ≤ deg := by + rw [MvPolynomial.eval₂_eq] + refine (MvPolynomial.degreeOf_sum_le i p.support + (fun s => MvPolynomial.C (p.coeff s) * + ∏ x ∈ s.support, + (Fin.cases (MvPolynomial.C a) (fun k : Fin n => MvPolynomial.X k) x : + MvPolynomial (Fin n) R) ^ s x)).trans ?_ + apply Finset.sup_le + intro s hs + simpa only [MvPolynomial.eval₂_monomial, Finsupp.prod] using + partialEvalFirst_eval₂_monomial_degreeOf_le (n := n) (R := R) + (deg := deg) a i s (p.coeff s) (hDeg s hs) + +/-! ## Core operation -/ + +/-- Fix variable 0 of a multivariate polynomial to a scalar value `a`. -/ +def partialEvalFirst (a : R) (p : CMvPolynomial (n + 1) R) : CMvPolynomial n R := + bind₁ (Fin.cons (C a) X) p + +/-! ## Evaluation lemma -/ + +/-- `partialEvalFirst a p` correctly implements partial evaluation. -/ +theorem partialEvalFirst_eval (a : R) (p : CMvPolynomial (n + 1) R) (v : Fin n → R) : + (partialEvalFirst a p).eval v = p.eval (Fin.cons a v) := by + unfold partialEvalFirst + rw [eval_equiv, fromCMvPolynomial_bind₁] + rw [MvPolynomial.eval₂_comp_left] + have hc : (MvPolynomial.eval v).comp MvPolynomial.C = RingHom.id R := by + ext r + simp only [RingHom.coe_comp, Function.comp_apply, MvPolynomial.eval_C, RingHom.id_apply] + have hv : + (⇑(MvPolynomial.eval v) ∘ + fun i => fromCMvPolynomial + (((Fin.cons (CMvPolynomial.C (n := n) a) + (fun i : Fin n => CMvPolynomial.X (R := R) i)) : + Fin (n + 1) → CMvPolynomial n R) i)) = + Fin.cons a v := by + funext i + cases i using Fin.cases with + | zero => simp only [Function.comp_apply, Fin.cons_zero, fromCMvPolynomial_C, + MvPolynomial.eval_C] + | succ i => simp only [Function.comp_apply, Fin.cons_succ, fromCMvPolynomial_X, + MvPolynomial.eval_X] + rw [hc, hv] + exact (eval_equiv (p := p) (vals := Fin.cons a v)).symm + +/-! ## Degree preservation -/ + +/-- `partialEvalFirst` preserves degree bounds for each remaining variable. -/ +theorem partialEvalFirst_degreeOf_le [Nontrivial R] {deg : ℕ} (a : R) + (i : Fin n) (p : CMvPolynomial (n + 1) R) + (hDeg : ∀ mono ∈ Lawful.monomials p, mono.degreeOf i.succ ≤ deg) : + ∀ mono ∈ Lawful.monomials (partialEvalFirst a p), mono.degreeOf i ≤ deg := by + intro mono hmono + have hSupport : + ∀ s ∈ (fromCMvPolynomial p).support, s i.succ ≤ deg := by + apply MvPolynomial.degreeOf_le_iff.mp + have hdegree := congrFun (degreeOf_equiv (p := p) (S := R)) i.succ + rw [← hdegree] + unfold CMvPolynomial.degreeOf + apply Finset.sup_le + intro mono hmono + exact hDeg mono (by simpa using hmono) + have hEval : + MvPolynomial.degreeOf i (fromCMvPolynomial (partialEvalFirst a p)) ≤ deg := by + unfold partialEvalFirst + rw [fromCMvPolynomial_bind₁] + have hvars : + (fun i : Fin (n + 1) => + fromCMvPolynomial + (((Fin.cons (CMvPolynomial.C (n := n) a) + (fun i : Fin n => CMvPolynomial.X (R := R) i)) : + Fin (n + 1) → CMvPolynomial n R) i)) = + (fun j : Fin (n + 1) => + Fin.cases (MvPolynomial.C a) (fun k : Fin n => MvPolynomial.X k) j) := by + funext j + cases j using Fin.cases with + | zero => simp only [Fin.cons_zero, fromCMvPolynomial_C, Fin.cases_zero] + | succ j => simp only [Fin.cons_succ, fromCMvPolynomial_X, Fin.cases_succ] + rw [hvars] + exact partialEvalFirst_eval₂_degreeOf_le (n := n) (R := R) a i + (fromCMvPolynomial p) hSupport + have hdegree := congrFun (degreeOf_equiv (p := partialEvalFirst a p) (S := R)) i + rw [← hdegree] at hEval + exact (Finset.le_sup (s := (Lawful.monomials (partialEvalFirst a p)).toFinset) + (f := fun m => m.degreeOf i) (by simpa using hmono)).trans hEval + +end CMvPolynomial + +end CPoly diff --git a/CompPoly/Multivariate/Unlawful.lean b/CompPoly/Multivariate/Unlawful.lean index b84b0846..7baa64b6 100644 --- a/CompPoly/Multivariate/Unlawful.lean +++ b/CompPoly/Multivariate/Unlawful.lean @@ -6,7 +6,6 @@ Authors: Frantisek Silvasi, Julian Sutherland, Andrei Burdusa module public import CompPoly.Multivariate.CMvMonomial -public import CompPoly.Multivariate.Wheels public import CompPoly.Data.ExtTreeMap.ExtTreeMap public import Mathlib.Algebra.Lie.OfAssociative diff --git a/CompPoly/Multivariate/Wheels.lean b/CompPoly/Multivariate/Wheels.lean deleted file mode 100644 index 61854788..00000000 --- a/CompPoly/Multivariate/Wheels.lean +++ /dev/null @@ -1,29 +0,0 @@ -/- -Copyright (c) 2025 CompPoly. All rights reserved. -Released under Apache 2.0 license as described in the file LICENSE. -Authors: Frantisek Silvasi, Julian Sutherland, Andrei Burdusa --/ -module - -public import Aesop -public import Mathlib.Logic.Function.Defs -public import Mathlib.Tactic.Cases - -/-! -# Auxiliary lemmas for multivariate polynomials --/ - -@[expose] public section -lemma List.distinct_of_inj_nodup {α β : Type*} {l : List α} {f : α → β} - (h₁ : Function.Injective f) (h₂ : l.Nodup) : - List.Pairwise (fun a b => f a ≠ f b) l := by - induction' l with hd tl ih - · simp - · simp_all only [ne_eq, List.nodup_cons, List.pairwise_cons, and_true, forall_const] - intros a' ha' contra - have : hd = a' := by aesop - grind - -lemma Option.filter_irrel {α : Type} {o : Option α} {p : α → Bool} - (h : ∀ x, x ∈ o → p x) : o.filter p = o := by - aesop (add simp Option.filter) diff --git a/CompPoly/Univariate/Roots.lean b/CompPoly/Univariate/Roots.lean index 275bd0da..d971599d 100644 --- a/CompPoly/Univariate/Roots.lean +++ b/CompPoly/Univariate/Roots.lean @@ -7,6 +7,8 @@ module public import CompPoly.Univariate.Roots.Correctness public import CompPoly.Univariate.Roots.Enumeration +public import CompPoly.Univariate.Roots.LasVegas +public import CompPoly.Univariate.Roots.Shoup public import CompPoly.Univariate.Roots.SmoothSubgroup /-! diff --git a/CompPoly/Univariate/Roots/LasVegas.lean b/CompPoly/Univariate/Roots/LasVegas.lean new file mode 100644 index 00000000..09f43ff1 --- /dev/null +++ b/CompPoly/Univariate/Roots/LasVegas.lean @@ -0,0 +1,19 @@ +/- +Copyright (c) 2026 CompPoly Contributors. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Valerii Huhnin +-/ + +module + +public import CompPoly.Univariate.Roots.LasVegas.Basic +public import CompPoly.Univariate.Roots.LasVegas.Correctness + +/-! +# Las Vegas Finite-Field Root Splitting + +Public module for bounded Las Vegas finite-field root splitting with exhaustive +enumeration fallback. +-/ + +@[expose] public section diff --git a/CompPoly/Univariate/Roots/LasVegas/Basic.lean b/CompPoly/Univariate/Roots/LasVegas/Basic.lean new file mode 100644 index 00000000..d42a15f2 --- /dev/null +++ b/CompPoly/Univariate/Roots/LasVegas/Basic.lean @@ -0,0 +1,332 @@ +/- +Copyright (c) 2026 CompPoly Contributors. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Valerii Huhnin +-/ + +module + +public import CompPoly.Univariate.Roots.Enumeration +public import CompPoly.Univariate.Roots.RootProduct +public import CompPoly.Univariate.Roots.Shoup.Basic + +/-! +# Las Vegas Linear-Factor Splitting + +Pure, bounded Las Vegas splitting for finite-field root products. The splitter +uses the odd-field Cantor-Zassenhaus branch for odd fields, the characteristic +two trace branch when supplied with compatible trace metadata, and exhaustive +enumeration after the configured probe cutoff or when no randomized branch is +available. +-/ + +@[expose] public section + +namespace CompPoly + +namespace CPolynomial + +namespace Roots + +namespace FiniteField + +/-- Lazy pure probe source for deterministic Las Vegas splitting. -/ +structure ProbeFamily (F : Type*) [Field F] [BEq F] [LawfulBEq F] where + probe : Nat → CPolynomial F → Nat → CPolynomial F + +/-- A simple deterministic affine probe source derived from a natural seed. -/ +def seededLinearProbeFamily {F : Type*} [Field F] [BEq F] [LawfulBEq F] + (seed : Nat) : ProbeFamily F where + probe q factor attempt := + let slope : F := (seed + 17 * attempt + factor.val.size + q + 1 : Nat) + let constant : F := (seed + 97 * attempt + 13 * factor.val.size + 3 * q + 1 : Nat) + CPolynomial.C slope * (CPolynomial.X : CPolynomial F) + CPolynomial.C constant + +/-- Probe family reading a finite table of probe polynomials by attempt index. -/ +def tableProbeFamily {F : Type*} [Field F] [BEq F] [LawfulBEq F] + (table : List (CPolynomial F)) : ProbeFamily F where + probe _q _factor attempt := table.getD attempt 0 + +/-- Configuration for bounded Las Vegas splitting. -/ +structure LasVegasConfig where + cutoff : Nat + tryOddRandomizedSplitting : Bool := true + tryEvenTraceSplitting : Bool := true + +/-- Root-product input predicate for the Las Vegas splitter. -/ +def lasVegasSplitterInput {F : Type*} [Field F] + (q : Nat) (p : CPolynomial F) : Prop := + p ≠ 0 ∧ p.toPoly ∣ ((Polynomial.X : Polynomial F) ^ q - Polynomial.X) + +/-- Reduce a canonical polynomial modulo a canonical modulus with the selected raw backend. -/ +def reduceModWith {F : Type*} [Field F] [BEq F] [LawfulBEq F] + (D : CPolynomial.Raw.ModContext F) (modulus h : CPolynomial F) : + CPolynomial F := + if modulus == 0 then + h + else + CPolynomial.ofArray (D.modByMonic h.val (CPolynomial.monicNormalize modulus).val) + +/-- `base^exponent mod modulus`, lifted through the selected raw arithmetic backends. -/ +def powModWith {F : Type*} [Field F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + (modulus base : CPolynomial F) (exponent : Nat) : + CPolynomial F := + CPolynomial.ofArray (CPolynomial.Raw.powModWith M D modulus.val base.val exponent) + +def tracePowerSumPolynomialLoopWith {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + (modulus : CPolynomial F) (p : Nat) : + Nat → CPolynomial F → CPolynomial F → CPolynomial F + | 0, _power, acc => acc + | steps + 1, power, acc => + let acc := reduceModWith D modulus (acc + power) + let nextPower := powModWith M D modulus power p + tracePowerSumPolynomialLoopWith M D modulus p steps nextPower acc + +/-- Polynomial-residue analogue of Shoup's trace power sum. -/ +def tracePowerSumPolynomialWith {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + (modulus : CPolynomial F) (p k : Nat) (h : CPolynomial F) : + CPolynomial F := + tracePowerSumPolynomialLoopWith M D modulus p k (reduceModWith D modulus h) 0 + +/-- +Deterministic trace-coordinate probe family cycling through the Shoup basis. + +For characteristic-two contexts this produces probes `beta * X` in basis order, +which makes the Las Vegas trace branch a bounded randomized-style splitter for +fixed deterministic benchmark and adapter contexts. +-/ +def traceBasisProbeFamily {F : Type*} [Field F] [BEq F] [LawfulBEq F] + (ctx : SmallPrimeTraceContext F) : + ProbeFamily F where + probe _q _factor attempt := + let beta := ctx.basis.getD (attempt % ctx.basis.size) (1 : F) + CPolynomial.C beta * (CPolynomial.X : CPolynomial F) + +/-- Keep only nonconstant children with strictly smaller representation size than the parent. -/ +def isNontrivialProperChild {F : Type*} [Field F] [BEq F] [LawfulBEq F] + (parent child : CPolynomial F) : Bool := + !(child == 0) && !(child == 1) && child.val.size < parent.val.size + +/-- Filter candidate children down to nonconstant, proper children. -/ +def nontrivialProperChildren {F : Type*} [Field F] [BEq F] [LawfulBEq F] + (parent : CPolynomial F) (children : Array (CPolynomial F)) : + Array (CPolynomial F) := + children.filter fun child ↦ isNontrivialProperChild parent child + +def quotientAfterChild {F : Type*} [Field F] [BEq F] [LawfulBEq F] + (parent child : CPolynomial F) : CPolynomial F := + if isNontrivialProperChild parent child then + CPolynomial.monicNormalize (parent / child) + else + parent + +/-- +One odd-field Cantor-Zassenhaus degree-one split attempt. + +The probe is reduced modulo `g`, then the split candidates are +`gcd(g, h)`, `gcd(g / zeroPart, h^((q - 1) / 2) - 1)`, and the remaining +quotient. The attempt succeeds only when at least two nonconstant proper +distinct children are produced. +-/ +def cantorZassenhausOddAttemptWith {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + (q : Nat) (probes : ProbeFamily F) (g : CPolynomial F) (attempt : Nat) : + Option (Array (CPolynomial F)) := + let g := CPolynomial.monicNormalize g + let h := reduceModWith D g (probes.probe q g attempt) + let s := powModWith M D g h ((q - 1) / 2) + let zeroPart := CPolynomial.monicNormalize (CPolynomial.gcdMonic g h) + let afterZero := quotientAfterChild g zeroPart + let squarePart := CPolynomial.monicNormalize + (CPolynomial.gcdMonic afterZero (s - (1 : CPolynomial F))) + let afterSquare := quotientAfterChild afterZero squarePart + let children := (nontrivialProperChildren g #[zeroPart, squarePart, afterSquare]).eraseDups + if children.size >= 2 then some children else none + +/-- +One characteristic-two trace split attempt. + +The probe is reduced modulo `g`, then the trace power sum +`h + h^2 + ... + h^(2^(m-1))` is computed modulo `g` using the supplied Shoup +small-prime trace metadata. The attempt succeeds only when `gcd(g, Tr(h))` and +the remaining quotient are both nonconstant proper children. +-/ +def cantorZassenhausEvenTraceAttemptWith {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + (traceCtx : SmallPrimeTraceContext F) + (q : Nat) (probes : ProbeFamily F) (g : CPolynomial F) (attempt : Nat) : + Option (Array (CPolynomial F)) := + let g := CPolynomial.monicNormalize g + let h := reduceModWith D g (probes.probe q g attempt) + let tracePoly := tracePowerSumPolynomialWith M D g traceCtx.p traceCtx.k h + let tracePart := CPolynomial.monicNormalize (CPolynomial.gcdMonic g tracePoly) + let complement := quotientAfterChild g tracePart + let children := nontrivialProperChildren g #[tracePart, complement] + if children.size >= 2 then some children else none + +def tryOddSplitAttemptsWith {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + (q : Nat) (probes : ProbeFamily F) (g : CPolynomial F) : + Nat → Nat → Option (Array (CPolynomial F)) + | 0, _ => none + | attempts + 1, offset => + match cantorZassenhausOddAttemptWith M D q probes g offset with + | some children => some children + | none => tryOddSplitAttemptsWith M D q probes g attempts (offset + 1) + +def tryEvenTraceSplitAttemptsWith {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + (traceCtx : SmallPrimeTraceContext F) + (q : Nat) (probes : ProbeFamily F) (g : CPolynomial F) : + Nat → Nat → Option (Array (CPolynomial F)) + | 0, _ => none + | attempts + 1, offset => + match cantorZassenhausEvenTraceAttemptWith M D traceCtx q probes g offset with + | some children => some children + | none => tryEvenTraceSplitAttemptsWith M D traceCtx q probes g attempts (offset + 1) + +def traceContextMatchesQ {F : Type*} [Field F] [BEq F] [LawfulBEq F] + (traceCtx : SmallPrimeTraceContext F) (q : Nat) : Bool := + traceCtx.p == 2 && traceCtx.q == q + +def lasVegasSplitLoopWith {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + (enumeration : FieldEnumeration F) (traceCtx? : Option (SmallPrimeTraceContext F)) + (cfg : LasVegasConfig) + (probes : ProbeFamily F) (q : Nat) : + Nat → List (CPolynomial F) → Array (CPolynomial F) → Array (CPolynomial F) + | 0, _stack, out => out + | _fuel + 1, [], out => out + | fuel + 1, g :: stack, out => + let g := CPolynomial.monicNormalize g + if g == 0 || g == 1 then + lasVegasSplitLoopWith M D enumeration traceCtx? cfg probes q fuel stack out + else if isRepresentedLinearFactor g then + lasVegasSplitLoopWith M D enumeration traceCtx? cfg probes q fuel stack (out.push g) + else if cfg.tryOddRandomizedSplitting && q % 2 == 1 then + match tryOddSplitAttemptsWith M D q probes g cfg.cutoff 0 with + | some children => + lasVegasSplitLoopWith M D enumeration traceCtx? cfg probes q fuel + (children.toList ++ stack) out + | none => + lasVegasSplitLoopWith M D enumeration traceCtx? cfg probes q fuel stack + (out ++ enumeratedLinearFactors enumeration g) + else if cfg.tryEvenTraceSplitting && q % 2 == 0 then + match traceCtx? with + | some traceCtx => + if traceContextMatchesQ traceCtx q then + match tryEvenTraceSplitAttemptsWith M D traceCtx q probes g cfg.cutoff 0 with + | some children => + lasVegasSplitLoopWith M D enumeration traceCtx? cfg probes q fuel + (children.toList ++ stack) out + | none => + lasVegasSplitLoopWith M D enumeration traceCtx? cfg probes q fuel stack + (out ++ enumeratedLinearFactors enumeration g) + else + lasVegasSplitLoopWith M D enumeration traceCtx? cfg probes q fuel stack + (out ++ enumeratedLinearFactors enumeration g) + | none => + lasVegasSplitLoopWith M D enumeration traceCtx? cfg probes q fuel stack + (out ++ enumeratedLinearFactors enumeration g) + else + lasVegasSplitLoopWith M D enumeration traceCtx? cfg probes q fuel stack + (out ++ enumeratedLinearFactors enumeration g) + +/-- Candidate linear factors from bounded Las Vegas splitting plus enumeration fallback. -/ +def lasVegasSplitCandidatesWithTrace? {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + (enumeration : FieldEnumeration F) (traceCtx? : Option (SmallPrimeTraceContext F)) + (cfg : LasVegasConfig) + (probes : ProbeFamily F) (q : Nat) (p : CPolynomial F) : + Array (CPolynomial F) := + let p := CPolynomial.monicNormalize p + let fuel := 2 * p.val.size + 1 + lasVegasSplitLoopWith M D enumeration traceCtx? cfg probes q fuel [p] #[] + +/-- Candidate linear factors using odd splitting only, plus enumeration fallback. -/ +def lasVegasSplitCandidatesWith {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + (enumeration : FieldEnumeration F) (cfg : LasVegasConfig) + (probes : ProbeFamily F) (q : Nat) (p : CPolynomial F) : + Array (CPolynomial F) := + lasVegasSplitCandidatesWithTrace? M D enumeration none cfg probes q p + +/-- Candidate linear factors using supplied characteristic-two trace metadata when applicable. -/ +def lasVegasSplitCandidatesWithTrace {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + (enumeration : FieldEnumeration F) (traceCtx : SmallPrimeTraceContext F) + (cfg : LasVegasConfig) (probes : ProbeFamily F) (q : Nat) (p : CPolynomial F) : + Array (CPolynomial F) := + lasVegasSplitCandidatesWithTrace? M D enumeration (some traceCtx) cfg probes q p + +/-- Las Vegas splitting with optional trace metadata, returning only represented linear factors. -/ +def lasVegasSplitLinearFactorsWithTrace? {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + (enumeration : FieldEnumeration F) (traceCtx? : Option (SmallPrimeTraceContext F)) + (cfg : LasVegasConfig) + (probes : ProbeFamily F) (q : Nat) (p : CPolynomial F) : + Array (CPolynomial F) := + representedLinearFactorsOnly + (lasVegasSplitCandidatesWithTrace? M D enumeration traceCtx? cfg probes q p) + +/-- Las Vegas splitting, returning only represented linear factors. -/ +def lasVegasSplitLinearFactorsWith {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + (enumeration : FieldEnumeration F) (cfg : LasVegasConfig) + (probes : ProbeFamily F) (q : Nat) (p : CPolynomial F) : + Array (CPolynomial F) := + lasVegasSplitLinearFactorsWithTrace? M D enumeration none cfg probes q p + +/-- Las Vegas splitting using supplied characteristic-two trace metadata when applicable. -/ +def lasVegasSplitLinearFactorsWithTrace {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + (enumeration : FieldEnumeration F) (traceCtx : SmallPrimeTraceContext F) + (cfg : LasVegasConfig) (probes : ProbeFamily F) (q : Nat) (p : CPolynomial F) : + Array (CPolynomial F) := + lasVegasSplitLinearFactorsWithTrace? M D enumeration (some traceCtx) cfg probes q p + +/-- The final Las Vegas filter only keeps represented linear factors. -/ +theorem lasVegasSplitLinearFactorsWith_sound {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + (enumeration : FieldEnumeration F) (cfg : LasVegasConfig) + (probes : ProbeFamily F) (q : Nat) {p factor : CPolynomial F} + (h : factor ∈ + (lasVegasSplitLinearFactorsWith M D enumeration cfg probes q p).toList) : + IsLinearFactor factor := by + exact representedLinearFactorsOnly_sound h + +/-- The final Las Vegas filter with trace metadata only keeps represented linear factors. -/ +theorem lasVegasSplitLinearFactorsWithTrace_sound {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + (enumeration : FieldEnumeration F) (traceCtx : SmallPrimeTraceContext F) + (cfg : LasVegasConfig) (probes : ProbeFamily F) (q : Nat) {p factor : CPolynomial F} + (h : factor ∈ + (lasVegasSplitLinearFactorsWithTrace M D enumeration traceCtx cfg probes q p).toList) : + IsLinearFactor factor := by + exact representedLinearFactorsOnly_sound h + +end FiniteField + +end Roots + +end CPolynomial + +end CompPoly diff --git a/CompPoly/Univariate/Roots/LasVegas/Correctness.lean b/CompPoly/Univariate/Roots/LasVegas/Correctness.lean new file mode 100644 index 00000000..34554ce2 --- /dev/null +++ b/CompPoly/Univariate/Roots/LasVegas/Correctness.lean @@ -0,0 +1,17 @@ +/- +Copyright (c) 2026 CompPoly Contributors. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Valerii Huhnin +-/ + +module + +public import CompPoly.Univariate.Roots.LasVegas.Correctness.Loop + +/-! +# Correctness Surface for Las Vegas Splitting + +Collector module for the bounded Las Vegas splitter correctness surface. +-/ + +@[expose] public section diff --git a/CompPoly/Univariate/Roots/LasVegas/Correctness/Common.lean b/CompPoly/Univariate/Roots/LasVegas/Correctness/Common.lean new file mode 100644 index 00000000..e03c190e --- /dev/null +++ b/CompPoly/Univariate/Roots/LasVegas/Correctness/Common.lean @@ -0,0 +1,1060 @@ +/- +Copyright (c) 2026 CompPoly Contributors. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Valerii Huhnin +-/ + +module + +-- `eval`, `natDegree` and friends are declared in a bare `public section`, so their +-- bodies are opaque downstream. The proofs below step through the `Raw` layer these +-- wrappers are defined by, which is the same-package implementation dependency +-- `import all` exists for; see `docs/wiki/module-system.md`. +import all CompPoly.Univariate.Basic +public import CompPoly.Univariate.Roots.LasVegas.Basic +public import Mathlib.Algebra.Polynomial.Div + +/-! +# Shared Correctness Lemmas for Las Vegas Splitting + +Shared algebraic, root-preservation, degree, and work-measure lemmas used by +the Las Vegas splitter correctness modules. +-/ + +@[expose] public section + +namespace CompPoly + +namespace CPolynomial + +namespace Roots + +namespace FiniteField + +theorem finiteFieldRootProductWith_lasVegasSplitterInput {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + (ctx : FiniteFieldContext F) {p : CPolynomial F} (hp : p ≠ 0) : + lasVegasSplitterInput ctx.q (finiteFieldRootProductWith M D ctx p) := by + exact ⟨finiteFieldRootProductWith_ne_zero_of_ne_zero M D ctx hp, + finiteFieldRootProductWith_dvd_frobenius_of_context M D ctx hp⟩ + +/-- Default-backend field-root products satisfy the Las Vegas splitter input predicate. -/ +theorem finiteFieldRootProduct_lasVegasSplitterInput {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (ctx : FiniteFieldContext F) {p : CPolynomial F} (hp : p ≠ 0) : + lasVegasSplitterInput ctx.q (finiteFieldRootProduct ctx p) := by + exact finiteFieldRootProductWith_lasVegasSplitterInput + CPolynomial.Raw.MulContext.naive CPolynomial.Raw.ModContext.naive ctx hp + +theorem eval_reduceModWith_eq_self_of_root {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (D : CPolynomial.Raw.ModContext F) + {modulus h : CPolynomial F} {a : F} + (hroot : CPolynomial.eval a modulus = 0) : + CPolynomial.eval a (reduceModWith D modulus h) = CPolynomial.eval a h := by + unfold reduceModWith + by_cases hzero : modulus == 0 + · rw [if_pos hzero] + · rw [if_neg hzero] + have hrootMonic : CPolynomial.eval a (CPolynomial.monicNormalize modulus) = 0 := + monicNormalize_root_of_root hroot + change + CPolynomial.Raw.eval a + (D.modByMonic h.val (CPolynomial.monicNormalize modulus).val).trim = + CPolynomial.Raw.eval a h.val + rw [CPolynomial.Raw.eval_trim_eq_eval] + rw [D.modByMonic_eq_modByMonic _ _ (CPolynomial.trim_eq h) + (CPolynomial.trim_eq (CPolynomial.monicNormalize modulus))] + exact CPolynomial.Raw.eval_modByMonic_eq_self_of_eval_eq_zero + h.val (CPolynomial.monicNormalize modulus).val hrootMonic + +theorem eval_powModWith_eq_pow {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + {modulus base : CPolynomial F} {a : F} + (hroot : CPolynomial.eval a modulus = 0) (n : Nat) : + CPolynomial.eval a (powModWith M D modulus base n) = CPolynomial.eval a base ^ n := by + unfold powModWith + change + CPolynomial.Raw.eval a + (CPolynomial.Raw.powModWith M D modulus.val base.val n).trim = + CPolynomial.Raw.eval a base.val ^ n + rw [CPolynomial.Raw.eval_trim_eq_eval] + exact raw_eval_powModWith_eq_pow M D hroot n + +theorem nontrivialProperChildren_mem_of_mem {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + {parent child : CPolynomial F} {children : Array (CPolynomial F)} + (hmem : child ∈ children.toList) + (hproper : isNontrivialProperChild parent child = true) : + child ∈ (nontrivialProperChildren parent children).toList := by + unfold nontrivialProperChildren + simp [hmem, hproper] + +theorem proper_of_mem_nontrivialProperChildren {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + {parent child : CPolynomial F} {children : Array (CPolynomial F)} + (hmem : child ∈ (nontrivialProperChildren parent children).toList) : + isNontrivialProperChild parent child = true := by + unfold nontrivialProperChildren at hmem + simp at hmem + rcases hmem with ⟨_hmem, hproper⟩ + exact hproper + +lemma mem_eraseDups_fold {α : Type*} [BEq α] [LawfulBEq α] + (a : α) : ∀ (xs : List α) (out : Array α), + a ∈ List.foldl (fun out x ↦ if x ∈ out then out else out.push x) out xs → + a ∈ out ∨ a ∈ xs := by + intro xs + induction xs with + | nil => + intro out h + exact Or.inl h + | cons x xs ih => + intro out h + simp only [List.foldl_cons] at h + by_cases hx : x ∈ out + · have h' := ih out (by simpa [hx] using h) + cases h' with + | inl hout => exact Or.inl hout + | inr hxs => exact Or.inr (by simp [hxs]) + · have h' := ih (out.push x) (by simpa [hx] using h) + cases h' with + | inl hout => + simp at hout + cases hout with + | inl hout => exact Or.inl hout + | inr hax => exact Or.inr (by simp [hax]) + | inr hxs => exact Or.inr (by simp [hxs]) + +lemma mem_of_mem_eraseDups {α : Type*} [BEq α] [LawfulBEq α] + {xs : Array α} {a : α} (h : a ∈ xs.eraseDups) : a ∈ xs := by + unfold Array.eraseDups at h + rcases xs with ⟨l⟩ + simp at h ⊢ + have hh := mem_eraseDups_fold a l #[] h + simpa using hh + +lemma mem_eraseDups_fold_of_mem {α : Type*} [BEq α] [LawfulBEq α] + (a : α) : ∀ (xs : List α) (out : Array α), + a ∈ out ∨ a ∈ xs → + a ∈ List.foldl (fun out x ↦ if x ∈ out then out else out.push x) out xs := by + intro xs + induction xs with + | nil => + intro out h + simpa using h + | cons x xs ih => + intro out h + simp only [List.foldl_cons] + by_cases hx : x ∈ out + · simp [hx] + apply ih out + cases h with + | inl hout => exact Or.inl hout + | inr hmem => + simp at hmem + cases hmem with + | inl hax => exact Or.inl (by simpa [hax] using hx) + | inr hxs => exact Or.inr hxs + · simp [hx] + apply ih (out.push x) + cases h with + | inl hout => exact Or.inl (by simp [hout]) + | inr hmem => + simp at hmem + cases hmem with + | inl hax => exact Or.inl (by simp [hax]) + | inr hxs => exact Or.inr hxs + +lemma mem_eraseDups_of_mem {α : Type*} [BEq α] [LawfulBEq α] + {xs : Array α} {a : α} (h : a ∈ xs) : a ∈ xs.eraseDups := by + unfold Array.eraseDups + rcases xs with ⟨l⟩ + simp at h ⊢ + exact mem_eraseDups_fold_of_mem a l #[] (Or.inr h) + +theorem middle_proper_of_triple_eraseDups_filter_size_ge_two_of_not_left {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + {parent left middle right : CPolynomial F} + (hleft : ¬ isNontrivialProperChild parent left = true) + (hsize : 2 ≤ (nontrivialProperChildren parent #[left, middle, right]).eraseDups.size) : + isNontrivialProperChild parent middle = true := by + by_cases hmiddle : isNontrivialProperChild parent middle = true + · exact hmiddle + · by_cases hright : isNontrivialProperChild parent right = true + · simp [nontrivialProperChildren, hleft, hmiddle, hright, Array.eraseDups] at hsize + · simp [nontrivialProperChildren, hleft, hmiddle, hright, Array.eraseDups] at hsize + +theorem right_proper_of_triple_eraseDups_filter_size_ge_two_of_not_left {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + {parent left middle right : CPolynomial F} + (hleft : ¬ isNontrivialProperChild parent left = true) + (hsize : 2 ≤ (nontrivialProperChildren parent #[left, middle, right]).eraseDups.size) : + isNontrivialProperChild parent right = true := by + by_cases hmiddle : isNontrivialProperChild parent middle = true + · by_cases hright : isNontrivialProperChild parent right = true + · exact hright + · simp [nontrivialProperChildren, hleft, hmiddle, hright, Array.eraseDups] at hsize + · by_cases hright : isNontrivialProperChild parent right = true + · exact hright + · simp [nontrivialProperChildren, hleft, hmiddle, hright, Array.eraseDups] at hsize + +theorem right_proper_of_triple_eraseDups_filter_size_ge_two_of_not_middle {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + {parent left middle right : CPolynomial F} + (hmiddle : ¬ isNontrivialProperChild parent middle = true) + (hsize : 2 ≤ (nontrivialProperChildren parent #[left, middle, right]).eraseDups.size) : + isNontrivialProperChild parent right = true := by + by_cases hleft : isNontrivialProperChild parent left = true + · by_cases hright : isNontrivialProperChild parent right = true + · exact hright + · simp [nontrivialProperChildren, hleft, hmiddle, hright, Array.eraseDups] at hsize + · by_cases hright : isNontrivialProperChild parent right = true + · exact hright + · simp [nontrivialProperChildren, hleft, hmiddle, hright, Array.eraseDups] at hsize + +theorem left_proper_of_pair_filter_size_ge_two {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + {parent left right : CPolynomial F} + (hsize : 2 ≤ (nontrivialProperChildren parent #[left, right]).size) : + isNontrivialProperChild parent left = true := by + by_cases hleft : isNontrivialProperChild parent left = true + · exact hleft + · by_cases hright : isNontrivialProperChild parent right = true + · simp [nontrivialProperChildren, hleft, hright] at hsize + · simp [nontrivialProperChildren, hleft, hright] at hsize + +theorem right_proper_of_pair_filter_size_ge_two {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + {parent left right : CPolynomial F} + (hsize : 2 ≤ (nontrivialProperChildren parent #[left, right]).size) : + isNontrivialProperChild parent right = true := by + by_cases hleft : isNontrivialProperChild parent left = true + · by_cases hright : isNontrivialProperChild parent right = true + · exact hright + · simp [nontrivialProperChildren, hleft, hright] at hsize + · by_cases hright : isNontrivialProperChild parent right = true + · exact hright + · simp [nontrivialProperChildren, hleft, hright] at hsize + +theorem third_proper_of_triple_filter_size_ge_two_of_not_left {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + {parent left middle right : CPolynomial F} + (hleft : ¬ isNontrivialProperChild parent left = true) + (hsize : 2 ≤ (nontrivialProperChildren parent #[left, middle, right]).size) : + isNontrivialProperChild parent right = true := by + by_cases hmiddle : isNontrivialProperChild parent middle = true + · by_cases hright : isNontrivialProperChild parent right = true + · exact hright + · simp [nontrivialProperChildren, hleft, hmiddle, hright] at hsize + · by_cases hright : isNontrivialProperChild parent right = true + · exact hright + · simp [nontrivialProperChildren, hleft, hmiddle, hright] at hsize + +theorem quotientAfterChild_root_of_not_child_root {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + {parent child : CPolynomial F} {a : F} + (hdiv : child.toPoly ∣ parent.toPoly) + (hparent : CPolynomial.eval a parent = 0) + (hchild : CPolynomial.eval a child ≠ 0) : + CPolynomial.eval a (quotientAfterChild parent child) = 0 := by + unfold quotientAfterChild + by_cases hproper : isNontrivialProperChild parent child = true + · rw [if_pos hproper] + exact monicNormalize_div_root_of_dvd_of_root_of_ne_root hdiv hparent hchild + · rw [if_neg hproper] + exact hparent + +theorem val_size_eq_natDegree_add_one_of_ne_zero {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + {p : CPolynomial F} (hp : p ≠ 0) : + p.val.size = p.natDegree + 1 := by + cases hs : p.val.size with + | zero => + exfalso + apply hp + apply CPolynomial.ext + exact Array.eq_empty_of_size_eq_zero hs + | succ n => + simp [CPolynomial.natDegree, hs] + +theorem val_size_le_of_toPoly_natDegree_le {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + {p q : CPolynomial F} (hq : q ≠ 0) + (hdegree : p.toPoly.natDegree ≤ q.toPoly.natDegree) : + p.val.size ≤ q.val.size := by + by_cases hp : p = 0 + · subst p + change 0 ≤ q.val.size + exact Nat.zero_le _ + · rw [val_size_eq_natDegree_add_one_of_ne_zero hp, + val_size_eq_natDegree_add_one_of_ne_zero hq] + rw [CPolynomial.natDegree_toPoly p, CPolynomial.natDegree_toPoly q] + omega + +theorem val_size_lt_of_toPoly_degree_lt {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + {p q : CPolynomial F} (hq : q ≠ 0) + (hdegree : p.toPoly.degree < q.toPoly.degree) : + p.val.size < q.val.size := by + by_cases hp : p = 0 + · subst p + cases hs : q.val.size with + | zero => + apply False.elim + apply hq + apply CPolynomial.ext + exact Array.eq_empty_of_size_eq_zero hs + | succ n => + change (0 : Nat) < n + 1 + exact Nat.zero_lt_succ n + · rw [val_size_eq_natDegree_add_one_of_ne_zero hp, + val_size_eq_natDegree_add_one_of_ne_zero hq] + rw [CPolynomial.natDegree_toPoly p, CPolynomial.natDegree_toPoly q] + have hpPoly : p.toPoly ≠ 0 := (CPolynomial.toPoly_eq_zero_iff p).not.mpr hp + have hqPoly : q.toPoly ≠ 0 := (CPolynomial.toPoly_eq_zero_iff q).not.mpr hq + rw [Polynomial.degree_eq_natDegree hpPoly, Polynomial.degree_eq_natDegree hqPoly] at hdegree + have hnat : p.toPoly.natDegree < q.toPoly.natDegree := by + exact_mod_cast hdegree + omega + +theorem monicNormalize_size_le_self {F : Type*} + [Field F] [BEq F] [LawfulBEq F] (p : CPolynomial F) : + (CPolynomial.monicNormalize p).val.size ≤ p.val.size := by + let : DecidableEq F := instDecidableEqOfLawfulBEq + by_cases hp : p = 0 + · subst p + have hzero : CPolynomial.monicNormalize (0 : CPolynomial F) = 0 := by + apply (CPolynomial.toPoly_eq_zero_iff _).1 + rw [CPolynomial.monicNormalize_toPoly_eq_normalize, CPolynomial.toPoly_zero, normalize_zero] + rw [hzero] + · have hpNorm : CPolynomial.monicNormalize p ≠ 0 := monicNormalize_ne_zero_of_ne_zero hp + rw [val_size_eq_natDegree_add_one_of_ne_zero hpNorm, + val_size_eq_natDegree_add_one_of_ne_zero hp] + rw [CPolynomial.natDegree_toPoly (CPolynomial.monicNormalize p), + CPolynomial.natDegree_toPoly p] + rw [CPolynomial.monicNormalize_toPoly_eq_normalize] + have hpPoly : p.toPoly ≠ 0 := (CPolynomial.toPoly_eq_zero_iff p).not.mpr hp + have hnormPoly : normalize p.toPoly ≠ 0 := by + rw [← CPolynomial.monicNormalize_toPoly_eq_normalize] + exact (CPolynomial.toPoly_eq_zero_iff _).not.mpr hpNorm + have hdegree : (normalize p.toPoly).degree = p.toPoly.degree := + Polynomial.degree_normalize + rw [Polynomial.degree_eq_natDegree hnormPoly, + Polynomial.degree_eq_natDegree hpPoly] at hdegree + have hnat : (normalize p.toPoly).natDegree = p.toPoly.natDegree := by + exact_mod_cast hdegree + omega + +theorem polynomial_natDegree_le_of_degree_le {F : Type*} [Field F] + {p q : Polynomial F} (hq : q ≠ 0) (hdegree : p.degree ≤ q.degree) : + p.natDegree ≤ q.natDegree := by + by_cases hp : p = 0 + · subst p + exact Nat.zero_le _ + · rw [Polynomial.degree_eq_natDegree hp, Polynomial.degree_eq_natDegree hq] at hdegree + exact_mod_cast hdegree + +theorem quotientAfterChild_size_le_parent {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + {parent child : CPolynomial F} (hparent : parent ≠ 0) : + (quotientAfterChild parent child).val.size ≤ parent.val.size := by + unfold quotientAfterChild + by_cases hproper : isNontrivialProperChild parent child = true + · rw [if_pos hproper] + let : DecidableEq F := instDecidableEqOfLawfulBEq + apply val_size_le_of_toPoly_natDegree_le hparent + rw [CPolynomial.monicNormalize_toPoly_eq_normalize] + change (normalize (CPolynomial.div parent child).toPoly).natDegree ≤ + parent.toPoly.natDegree + rw [CPolynomial.div_toPoly_eq_div] + apply polynomial_natDegree_le_of_degree_le + · exact (CPolynomial.toPoly_eq_zero_iff parent).not.mpr hparent + · rw [Polynomial.degree_normalize] + exact Polynomial.degree_div_le parent.toPoly child.toPoly + · rw [if_neg hproper] + +theorem toPoly_ne_one_of_ne_one {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + {p : CPolynomial F} (hp : p ≠ 1) : + p.toPoly ≠ 1 := by + intro hpoly + apply hp + apply (CPolynomial.eq_iff_coeff).2 + intro i + rw [CPolynomial.coeff_toPoly, CPolynomial.coeff_toPoly, hpoly, CPolynomial.toPoly_one] + +theorem eq_of_toPoly_eq {F : Type*} + [Field F] [BEq F] [LawfulBEq F] {p q : CPolynomial F} + (hpoly : p.toPoly = q.toPoly) : p = q := by + apply (CPolynomial.eq_iff_coeff).2 + intro i + rw [CPolynomial.coeff_toPoly, CPolynomial.coeff_toPoly, hpoly] + +theorem eq_of_monic_toPoly_dvd_of_natDegree_le {F : Type*} + [Field F] [BEq F] [LawfulBEq F] {p q : CPolynomial F} + (hp : p.toPoly.Monic) (hq : q.toPoly.Monic) + (hdvd : p.toPoly ∣ q.toPoly) + (hdegree : q.toPoly.natDegree ≤ p.toPoly.natDegree) : + q = p := by + apply eq_of_toPoly_eq + exact Polynomial.eq_of_monic_of_dvd_of_natDegree_le hp hq hdvd hdegree + +theorem monicNormalize_toPoly_degree_pos_of_proper {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + {parent p : CPolynomial F} + (hproper : isNontrivialProperChild parent (CPolynomial.monicNormalize p) = true) : + 0 < (CPolynomial.monicNormalize p).toPoly.degree := by + let : DecidableEq F := instDecidableEqOfLawfulBEq + unfold isNontrivialProperChild at hproper + simp at hproper + have hpNormNe : CPolynomial.monicNormalize p ≠ 0 := hproper.1.1 + have hpNormNotOne : CPolynomial.monicNormalize p ≠ 1 := hproper.1.2 + have hpPoly : p.toPoly ≠ 0 := by + intro hpZero + apply hpNormNe + exact (CPolynomial.toPoly_eq_zero_iff _).1 (by + rw [CPolynomial.monicNormalize_toPoly_eq_normalize, hpZero, normalize_zero]) + have hmonic : (CPolynomial.monicNormalize p).toPoly.Monic := by + rw [CPolynomial.monicNormalize_toPoly_eq_normalize] + exact Polynomial.monic_normalize hpPoly + exact (Polynomial.Monic.degree_pos hmonic).2 + (toPoly_ne_one_of_ne_one hpNormNotOne) + +theorem quotientAfterChild_size_lt_parent_of_monicNormalize_proper {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + {parent childSource : CPolynomial F} (hparent : parent ≠ 0) + (hproper : + isNontrivialProperChild parent (CPolynomial.monicNormalize childSource) = true) : + (quotientAfterChild parent (CPolynomial.monicNormalize childSource)).val.size < + parent.val.size := by + let : DecidableEq F := instDecidableEqOfLawfulBEq + unfold quotientAfterChild + rw [if_pos hproper] + apply val_size_lt_of_toPoly_degree_lt hparent + rw [CPolynomial.monicNormalize_toPoly_eq_normalize] + change + (normalize + (CPolynomial.div parent (CPolynomial.monicNormalize childSource)).toPoly).degree < + parent.toPoly.degree + rw [CPolynomial.div_toPoly_eq_div, Polynomial.degree_normalize] + exact Polynomial.degree_div_lt + ((CPolynomial.toPoly_eq_zero_iff parent).not.mpr hparent) + (monicNormalize_toPoly_degree_pos_of_proper hproper) + +theorem div_ne_zero_of_dvd_of_ne_zero {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + {parent child : CPolynomial F} + (hdiv : child.toPoly ∣ parent.toPoly) (hparent : parent ≠ 0) + (hchild : child ≠ 0) : + CPolynomial.div parent child ≠ 0 := by + intro hquot + have hparentPoly : parent.toPoly ≠ 0 := + (CPolynomial.toPoly_eq_zero_iff parent).not.mpr hparent + have hchildPoly : child.toPoly ≠ 0 := + (CPolynomial.toPoly_eq_zero_iff child).not.mpr hchild + rcases hdiv with ⟨r, hr⟩ + have hquotPoly : parent.toPoly / child.toPoly = 0 := by + have h := congrArg CPolynomial.toPoly hquot + -- `toPoly 0` no longer reduces to `0` definitionally downstream, so bridge it with + -- the characterization lemma instead. + have hzero : (0 : CPolynomial F).toPoly = 0 := + (CPolynomial.toPoly_eq_zero_iff (0 : CPolynomial F)).mpr rfl + rw [CPolynomial.div_toPoly_eq_div, hzero] at h + exact h + have hdivPoly : parent.toPoly / child.toPoly = r := by + exact (EuclideanDomain.eq_div_of_mul_eq_right hchildPoly hr.symm).symm + have hrzero : r = 0 := by + rw [← hdivPoly] + exact hquotPoly + apply hparentPoly + rw [hr, hrzero] + simp + +theorem quotientAfterChild_ne_zero_of_dvd {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + {parent child : CPolynomial F} + (hdiv : child.toPoly ∣ parent.toPoly) (hparent : parent ≠ 0) : + quotientAfterChild parent child ≠ 0 := by + unfold quotientAfterChild + by_cases hproper : isNontrivialProperChild parent child = true + · rw [if_pos hproper] + have hchild : child ≠ 0 := by + unfold isNontrivialProperChild at hproper + simp at hproper + exact hproper.1.1 + exact monicNormalize_ne_zero_of_ne_zero + (div_ne_zero_of_dvd_of_ne_zero hdiv hparent hchild) + · rw [if_neg hproper] + exact hparent + +theorem quotientAfterChild_toPoly_dvd_parent {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + {parent child : CPolynomial F} + (hdiv : child.toPoly ∣ parent.toPoly) : + (quotientAfterChild parent child).toPoly ∣ parent.toPoly := by + unfold quotientAfterChild + by_cases hproper : isNontrivialProperChild parent child = true + · rw [if_pos hproper] + have hchild : child ≠ 0 := by + unfold isNontrivialProperChild at hproper + simp at hproper + exact hproper.1.1 + have hchildPoly : child.toPoly ≠ 0 := + (CPolynomial.toPoly_eq_zero_iff child).not.mpr hchild + rcases hdiv with ⟨r, hr⟩ + have hquotPoly : parent.toPoly / child.toPoly = r := by + exact (EuclideanDomain.eq_div_of_mul_eq_right hchildPoly hr.symm).symm + have hquot_dvd_parent : parent.toPoly / child.toPoly ∣ parent.toPoly := by + rw [hquotPoly] + exact ⟨child.toPoly, by + rw [hr] + exact _root_.mul_comm child.toPoly r⟩ + have hdivC : (CPolynomial.div parent child).toPoly ∣ parent.toPoly := by + rw [CPolynomial.div_toPoly_eq_div] + exact hquot_dvd_parent + exact (toPoly_monicNormalize_dvd_self (CPolynomial.div parent child)).trans hdivC + · rw [if_neg hproper] + +theorem child_quotient_natDegree_le_parent {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + {parent child : CPolynomial F} + (hproper : isNontrivialProperChild parent child = true) + (hdiv : child.toPoly ∣ parent.toPoly) (hparent : parent ≠ 0) : + child.toPoly.natDegree + (quotientAfterChild parent child).toPoly.natDegree ≤ + parent.toPoly.natDegree := by + let : DecidableEq F := instDecidableEqOfLawfulBEq + have hchild : child ≠ 0 := by + unfold isNontrivialProperChild at hproper + simp at hproper + exact hproper.1.1 + have hparentPoly : parent.toPoly ≠ 0 := + (CPolynomial.toPoly_eq_zero_iff parent).not.mpr hparent + have hchildPoly : child.toPoly ≠ 0 := + (CPolynomial.toPoly_eq_zero_iff child).not.mpr hchild + have hdegreeLe : child.toPoly.degree ≤ parent.toPoly.degree := + Polynomial.degree_le_of_dvd hdiv hparentPoly + have hdegreeAdd : + child.toPoly.degree + (parent.toPoly / child.toPoly).degree = + parent.toPoly.degree := + Polynomial.degree_add_div hchildPoly hdegreeLe + have hquotC : CPolynomial.div parent child ≠ 0 := + div_ne_zero_of_dvd_of_ne_zero hdiv hparent hchild + have hnormC : CPolynomial.monicNormalize (CPolynomial.div parent child) ≠ 0 := + monicNormalize_ne_zero_of_ne_zero hquotC + have hnormPoly : + normalize (parent.toPoly / child.toPoly) ≠ 0 := by + have hnormPolyC : + (CPolynomial.monicNormalize (CPolynomial.div parent child)).toPoly ≠ 0 := + (CPolynomial.toPoly_eq_zero_iff _).not.mpr hnormC + simpa [CPolynomial.monicNormalize_toPoly_eq_normalize, + CPolynomial.div_toPoly_eq_div] using hnormPolyC + unfold quotientAfterChild + rw [if_pos hproper] + rw [CPolynomial.monicNormalize_toPoly_eq_normalize] + change child.toPoly.natDegree + + (normalize (CPolynomial.div parent child).toPoly).natDegree ≤ + parent.toPoly.natDegree + rw [CPolynomial.div_toPoly_eq_div] + have hdegreeAddNorm : + child.toPoly.degree + (normalize (parent.toPoly / child.toPoly)).degree = + parent.toPoly.degree := by + rw [Polynomial.degree_normalize] + exact hdegreeAdd + rw [Polynomial.degree_eq_natDegree hchildPoly, + Polynomial.degree_eq_natDegree hnormPoly, + Polynomial.degree_eq_natDegree hparentPoly] at hdegreeAddNorm + have hnat : + child.toPoly.natDegree + (normalize (parent.toPoly / child.toPoly)).natDegree = + parent.toPoly.natDegree := by + exact_mod_cast hdegreeAddNorm + exact le_of_eq hnat + +noncomputable def splitWork {F : Type*} [Field F] [BEq F] [LawfulBEq F] + (p : CPolynomial F) : Nat := + 2 * p.toPoly.natDegree - 1 + +noncomputable def stackWork {F : Type*} [Field F] [BEq F] [LawfulBEq F] : + List (CPolynomial F) → Nat + | [] => 0 + | p :: ps => splitWork p + stackWork ps + +noncomputable def normSplitWork {F : Type*} [Field F] [BEq F] [LawfulBEq F] + (p : CPolynomial F) : Nat := + splitWork (CPolynomial.monicNormalize p) + +noncomputable def normStackWork {F : Type*} [Field F] [BEq F] [LawfulBEq F] : + List (CPolynomial F) → Nat + | [] => 0 + | p :: ps => normSplitWork p + normStackWork ps + +theorem stackWork_append {F : Type*} + [Field F] [BEq F] [LawfulBEq F] : + ∀ xs ys : List (CPolynomial F), + stackWork (xs ++ ys) = stackWork xs + stackWork ys := by + intro xs + induction xs with + | nil => + intro ys + simp [stackWork] + | cons x xs ih => + intro ys + simp [stackWork, ih, Nat.add_assoc] + +theorem stackWork_push {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (out : Array (CPolynomial F)) (x : CPolynomial F) : + stackWork (out.push x).toList = stackWork out.toList + splitWork x := by + rw [show (out.push x).toList = out.toList ++ [x] by simp] + rw [stackWork_append] + simp [stackWork] + +theorem stackWork_eraseDups_fold_le {F : Type*} + [Field F] [BEq F] [LawfulBEq F] : + ∀ (xs : List (CPolynomial F)) (out : Array (CPolynomial F)), + stackWork (List.foldl + (fun out x ↦ if x ∈ out then out else out.push x) out xs).toList ≤ + stackWork out.toList + stackWork xs := by + intro xs + induction xs with + | nil => + intro out + simp [stackWork] + | cons x xs ih => + intro out + simp only [List.foldl_cons] + by_cases hx : x ∈ out + · have hle := ih out + simp [hx, stackWork] at hle ⊢ + omega + · have hle := ih (out.push x) + rw [stackWork_push] at hle + simp [hx, stackWork] at hle ⊢ + omega + +theorem stackWork_eraseDups_le {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (xs : Array (CPolynomial F)) : + stackWork xs.eraseDups.toList ≤ stackWork xs.toList := by + unfold Array.eraseDups + rcases xs with ⟨l⟩ + simpa [stackWork] using + (stackWork_eraseDups_fold_le (F := F) l #[]) + +theorem stackWork_eraseDups_triple_dup_right_le {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (x y : CPolynomial F) : + stackWork (#[x, y, y].eraseDups).toList ≤ splitWork x + splitWork y := by + unfold Array.eraseDups + by_cases hxy : x = y + · subst y + simp [stackWork] + · simp + have hyx : ¬ y = x := fun hyx ↦ hxy hyx.symm + simp [hyx, stackWork] + +theorem normStackWork_append {F : Type*} + [Field F] [BEq F] [LawfulBEq F] : + ∀ xs ys : List (CPolynomial F), + normStackWork (xs ++ ys) = normStackWork xs + normStackWork ys := by + intro xs + induction xs with + | nil => + intro ys + simp [normStackWork] + | cons x xs ih => + intro ys + simp [normStackWork, ih, Nat.add_assoc] + +theorem monicNormalize_toPoly_natDegree_eq {F : Type*} + [Field F] [BEq F] [LawfulBEq F] (p : CPolynomial F) : + (CPolynomial.monicNormalize p).toPoly.natDegree = p.toPoly.natDegree := by + let : DecidableEq F := instDecidableEqOfLawfulBEq + by_cases hp : p = 0 + · subst p + rw [CPolynomial.monicNormalize_toPoly_eq_normalize, CPolynomial.toPoly_zero, normalize_zero] + · have hpNorm : CPolynomial.monicNormalize p ≠ 0 := + monicNormalize_ne_zero_of_ne_zero hp + rw [CPolynomial.monicNormalize_toPoly_eq_normalize] + have hpPoly : p.toPoly ≠ 0 := (CPolynomial.toPoly_eq_zero_iff p).not.mpr hp + have hnormPoly : normalize p.toPoly ≠ 0 := by + rw [← CPolynomial.monicNormalize_toPoly_eq_normalize] + exact (CPolynomial.toPoly_eq_zero_iff _).not.mpr hpNorm + have hdegree : (normalize p.toPoly).degree = p.toPoly.degree := + Polynomial.degree_normalize + rw [Polynomial.degree_eq_natDegree hnormPoly, + Polynomial.degree_eq_natDegree hpPoly] at hdegree + exact_mod_cast hdegree + +theorem monicNormalize_zero {F : Type*} + [Field F] [BEq F] [LawfulBEq F] : + CPolynomial.monicNormalize (0 : CPolynomial F) = 0 := by + let : DecidableEq F := instDecidableEqOfLawfulBEq + apply (CPolynomial.toPoly_eq_zero_iff _).1 + rw [CPolynomial.monicNormalize_toPoly_eq_normalize, CPolynomial.toPoly_zero, normalize_zero] + +theorem splitWork_monicNormalize_eq {F : Type*} + [Field F] [BEq F] [LawfulBEq F] (p : CPolynomial F) : + splitWork (CPolynomial.monicNormalize p) = splitWork p := by + unfold splitWork + rw [monicNormalize_toPoly_natDegree_eq] + +theorem normSplitWork_eq_splitWork {F : Type*} + [Field F] [BEq F] [LawfulBEq F] (p : CPolynomial F) : + normSplitWork p = splitWork p := by + unfold normSplitWork + exact splitWork_monicNormalize_eq p + +theorem normStackWork_eq_stackWork {F : Type*} + [Field F] [BEq F] [LawfulBEq F] : + ∀ xs : List (CPolynomial F), normStackWork xs = stackWork xs := by + intro xs + induction xs with + | nil => + simp [normStackWork, stackWork] + | cons x xs ih => + simp [normStackWork, stackWork, normSplitWork_eq_splitWork, ih] + +theorem splitWork_pos_of_natDegree_pos {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + {p : CPolynomial F} (hdegree : 0 < p.toPoly.natDegree) : + 1 ≤ splitWork p := by + unfold splitWork + omega + +theorem normSplitWork_pos_of_monicNormalize_ne_zero_ne_one {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + {p : CPolynomial F} + (hzero : CPolynomial.monicNormalize p ≠ 0) + (hone : CPolynomial.monicNormalize p ≠ 1) : + 1 ≤ normSplitWork p := by + let : DecidableEq F := instDecidableEqOfLawfulBEq + unfold normSplitWork + apply splitWork_pos_of_natDegree_pos + apply Polynomial.natDegree_pos_iff_degree_pos.mpr + have hpPoly : p.toPoly ≠ 0 := by + intro hpZero + apply hzero + apply (CPolynomial.toPoly_eq_zero_iff _).1 + rw [CPolynomial.monicNormalize_toPoly_eq_normalize, hpZero, normalize_zero] + have hmonic : (CPolynomial.monicNormalize p).toPoly.Monic := by + rw [CPolynomial.monicNormalize_toPoly_eq_normalize] + exact Polynomial.monic_normalize hpPoly + exact (Polynomial.Monic.degree_pos hmonic).2 + (toPoly_ne_one_of_ne_one hone) + +theorem monicNormalize_toPoly_monic_of_ne_zero {F : Type*} + [Field F] [BEq F] [LawfulBEq F] {p : CPolynomial F} (hp : p ≠ 0) : + (CPolynomial.monicNormalize p).toPoly.Monic := by + let : DecidableEq F := instDecidableEqOfLawfulBEq + rw [CPolynomial.monicNormalize_toPoly_eq_normalize] + exact Polynomial.monic_normalize ((CPolynomial.toPoly_eq_zero_iff p).not.mpr hp) + +theorem splitWork_pos_of_monic_ne_zero_ne_one {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + {p : CPolynomial F} (hmonic : p.toPoly.Monic) + (_hzero : p ≠ 0) (hone : p ≠ 1) : + 1 ≤ splitWork p := by + apply splitWork_pos_of_natDegree_pos + exact Polynomial.natDegree_pos_iff_degree_pos.mpr + ((Polynomial.Monic.degree_pos hmonic).2 (toPoly_ne_one_of_ne_one hone)) + +theorem toPoly_natDegree_pos_of_monic_ne_zero_ne_one {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + {p : CPolynomial F} (hmonic : p.toPoly.Monic) + (_hzero : p ≠ 0) (hone : p ≠ 1) : + 0 < p.toPoly.natDegree := by + exact Polynomial.natDegree_pos_iff_degree_pos.mpr + ((Polynomial.Monic.degree_pos hmonic).2 (toPoly_ne_one_of_ne_one hone)) + +theorem normSplitWork_pos_of_monic_ne_zero_ne_one {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + {p : CPolynomial F} (hmonic : p.toPoly.Monic) + (hzero : p ≠ 0) (hone : p ≠ 1) : + 1 ≤ normSplitWork p := by + rw [normSplitWork_eq_splitWork] + exact splitWork_pos_of_monic_ne_zero_ne_one hmonic hzero hone + +theorem quotientAfterChild_toPoly_monic_of_dvd {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + {parent child : CPolynomial F} + (hparentMonic : parent.toPoly.Monic) + (hdiv : child.toPoly ∣ parent.toPoly) (hparent : parent ≠ 0) : + (quotientAfterChild parent child).toPoly.Monic := by + unfold quotientAfterChild + by_cases hproper : isNontrivialProperChild parent child = true + · rw [if_pos hproper] + have hchild : child ≠ 0 := by + unfold isNontrivialProperChild at hproper + simp at hproper + exact hproper.1.1 + exact monicNormalize_toPoly_monic_of_ne_zero + (div_ne_zero_of_dvd_of_ne_zero hdiv hparent hchild) + · rw [if_neg hproper] + exact hparentMonic + +theorem eq_of_monic_dvd_of_val_size_le {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + {divisor parent : CPolynomial F} + (hdivisorMonic : divisor.toPoly.Monic) (hparentMonic : parent.toPoly.Monic) + (hdiv : divisor.toPoly ∣ parent.toPoly) + (hdivisor : divisor ≠ 0) (hparent : parent ≠ 0) + (hsize : parent.val.size ≤ divisor.val.size) : + parent = divisor := by + apply eq_of_monic_toPoly_dvd_of_natDegree_le hdivisorMonic hparentMonic hdiv + rw [val_size_eq_natDegree_add_one_of_ne_zero hparent, + val_size_eq_natDegree_add_one_of_ne_zero hdivisor] at hsize + rw [CPolynomial.natDegree_toPoly parent, CPolynomial.natDegree_toPoly divisor] at hsize + omega + +theorem eq_of_not_proper_of_monic_dvd {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + {parent child : CPolynomial F} + (hchildMonic : child.toPoly.Monic) (hparentMonic : parent.toPoly.Monic) + (hdiv : child.toPoly ∣ parent.toPoly) + (hchildNe : child ≠ 0) (hparentNe : parent ≠ 0) (hchildNotOne : child ≠ 1) + (hnotProper : ¬ isNontrivialProperChild parent child = true) : + parent = child := by + have hnotSize : ¬ child.val.size < parent.val.size := by + intro hsize + apply hnotProper + unfold isNontrivialProperChild + simp [hchildNe, hchildNotOne, hsize] + exact eq_of_monic_dvd_of_val_size_le hchildMonic hparentMonic hdiv + hchildNe hparentNe (Nat.le_of_not_gt hnotSize) + +def normStackReady {F : Type*} [Field F] [BEq F] [LawfulBEq F] + (stack : List (CPolynomial F)) : Prop := + ∀ g, g ∈ stack → CPolynomial.monicNormalize g ≠ 0 ∧ CPolynomial.monicNormalize g ≠ 1 + +theorem normStackReady_tail {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + {g : CPolynomial F} {stack : List (CPolynomial F)} + (hready : normStackReady (g :: stack)) : + normStackReady stack := by + intro child hchild + exact hready child (by simp [hchild]) + +theorem normStackReady_append {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + {xs ys : List (CPolynomial F)} + (hxs : normStackReady xs) (hys : normStackReady ys) : + normStackReady (xs ++ ys) := by + intro g hmem + rw [List.mem_append] at hmem + rcases hmem with hmem | hmem + · exact hxs g hmem + · exact hys g hmem + +theorem normStackWork_pos_of_mem {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + {stack : List (CPolynomial F)} {p : CPolynomial F} + (hready : normStackReady stack) (hmem : p ∈ stack) : + 1 ≤ normStackWork stack := by + induction stack with + | nil => + simp at hmem + | cons g stack ih => + simp at hmem + rcases hmem with hmem | hmem + · subst p + have hheadReady := hready g (by simp) + have hpos := normSplitWork_pos_of_monicNormalize_ne_zero_ne_one + hheadReady.1 hheadReady.2 + simp [normStackWork] + omega + · have htail := ih (normStackReady_tail hready) hmem + simp [normStackWork] + omega + +theorem normSplitWork_pos_ready {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + {p : CPolynomial F} (hpos : 1 ≤ normSplitWork p) : + CPolynomial.monicNormalize p ≠ 0 ∧ CPolynomial.monicNormalize p ≠ 1 := by + constructor + · intro hzero + unfold normSplitWork splitWork at hpos + rw [hzero, CPolynomial.toPoly_zero] at hpos + simp at hpos + · intro hone + unfold normSplitWork splitWork at hpos + rw [hone, CPolynomial.toPoly_one] at hpos + simp at hpos + +theorem normStackWork_tail_le_of_cons_le {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + {fuel : Nat} {g : CPolynomial F} {stack : List (CPolynomial F)} + (hwork : normStackWork (g :: stack) ≤ fuel + 1) + (hpos : 1 ≤ normSplitWork g) : + normStackWork stack ≤ fuel := by + simp [normStackWork] at hwork + omega + +theorem normStackWork_split_stack_le {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + {fuel : Nat} {g : CPolynomial F} {stack : List (CPolynomial F)} + {children : Array (CPolynomial F)} + (hwork : normStackWork (g :: stack) ≤ fuel + 1) + (hpos : 1 ≤ normSplitWork g) + (hchildren : stackWork children.toList ≤ normSplitWork g - 1) : + normStackWork (children.toList ++ stack) ≤ fuel := by + rw [normStackWork_append] + rw [normStackWork_eq_stackWork children.toList] + simp [normStackWork] at hwork + omega + +theorem monicNormalize_toPoly_natDegree_pos_of_proper {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + {parent p : CPolynomial F} + (hproper : isNontrivialProperChild parent (CPolynomial.monicNormalize p) = true) : + 0 < (CPolynomial.monicNormalize p).toPoly.natDegree := by + exact Polynomial.natDegree_pos_iff_degree_pos.mpr + (monicNormalize_toPoly_degree_pos_of_proper hproper) + +theorem splitWork_pair_le_of_natDegree_sum_le {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + {parent left right : CPolynomial F} + (hleft : 0 < left.toPoly.natDegree) + (hright : 0 < right.toPoly.natDegree) + (hsum : left.toPoly.natDegree + right.toPoly.natDegree ≤ + parent.toPoly.natDegree) : + splitWork left + splitWork right ≤ splitWork parent - 1 := by + unfold splitWork + omega + +theorem splitWork_triple_le_of_natDegree_sum_le {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + {parent left middle right : CPolynomial F} + (hleft : 0 < left.toPoly.natDegree) + (hmiddle : 0 < middle.toPoly.natDegree) + (hright : 0 < right.toPoly.natDegree) + (hsum : left.toPoly.natDegree + middle.toPoly.natDegree + + right.toPoly.natDegree ≤ parent.toPoly.natDegree) : + splitWork left + (splitWork middle + (splitWork right + 0)) ≤ + splitWork parent - 1 := by + unfold splitWork + omega + +theorem splitWork_triple_le_of_first_two_pos_natDegree_sum_le {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + {parent left middle right : CPolynomial F} + (hleft : 0 < left.toPoly.natDegree) + (hmiddle : 0 < middle.toPoly.natDegree) + (hsum : left.toPoly.natDegree + middle.toPoly.natDegree + + right.toPoly.natDegree ≤ parent.toPoly.natDegree) : + splitWork left + (splitWork middle + (splitWork right + 0)) ≤ + splitWork parent - 1 := by + unfold splitWork + by_cases hright : 0 < right.toPoly.natDegree + · omega + · have hrightZero : right.toPoly.natDegree = 0 := by omega + omega + +theorem proper_child_of_proper_intermediate {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + {parent intermediate child : CPolynomial F} + (hproper : isNontrivialProperChild intermediate child = true) + (hsize : intermediate.val.size ≤ parent.val.size) : + isNontrivialProperChild parent child = true := by + unfold isNontrivialProperChild at hproper ⊢ + simp at hproper ⊢ + exact ⟨hproper.1, lt_of_lt_of_le hproper.2 hsize⟩ + +theorem child_ne_zero_of_proper {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + {parent child : CPolynomial F} + (hproper : isNontrivialProperChild parent child = true) : + child ≠ 0 := by + unfold isNontrivialProperChild at hproper + simp at hproper + exact hproper.1.1 + +theorem child_ne_one_of_root {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + {child : CPolynomial F} {a : F} (hroot : CPolynomial.eval a child = 0) : + child ≠ 1 := by + intro hchild + subst child + rw [CPolynomial.eval_one] at hroot + exact one_ne_zero hroot + +theorem proper_child_of_ne_zero_root_size_lt {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + {parent child : CPolynomial F} {a : F} + (hchild : child ≠ 0) (hroot : CPolynomial.eval a child = 0) + (hsize : child.val.size < parent.val.size) : + isNontrivialProperChild parent child = true := by + unfold isNontrivialProperChild + simp [hchild, child_ne_one_of_root hroot, hsize] + +theorem child_size_le_fuel_of_proper_of_parent_size_le_succ {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + {parent child : CPolynomial F} {fuel : Nat} + (hproper : isNontrivialProperChild parent child = true) + (hparent : parent.val.size ≤ fuel + 1) : + child.val.size ≤ fuel := by + unfold isNontrivialProperChild at hproper + simp at hproper + omega + +theorem split_child_or_quotient_root {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + {parent child : CPolynomial F} {a : F} + (hdiv : child.toPoly ∣ parent.toPoly) + (hparent : CPolynomial.eval a parent = 0) : + (isNontrivialProperChild parent child = true ∧ CPolynomial.eval a child = 0) ∨ + CPolynomial.eval a (quotientAfterChild parent child) = 0 := by + by_cases hchild : CPolynomial.eval a child = 0 + · by_cases hproper : isNontrivialProperChild parent child = true + · exact Or.inl ⟨hproper, hchild⟩ + · right + unfold quotientAfterChild + rw [if_neg hproper] + exact hparent + · right + exact quotientAfterChild_root_of_not_child_root hdiv hparent hchild + +/-- The monic gcd with a nonzero left argument is nonzero. -/ +theorem gcdMonic_ne_zero_left {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + {p : CPolynomial F} (r : CPolynomial F) (hp : p ≠ 0) : + CPolynomial.gcdMonic p r ≠ 0 := by + let : DecidableEq F := instDecidableEqOfLawfulBEq + intro hzero + have hpoly := congrArg CPolynomial.toPoly hzero + rw [CPolynomial.gcdMonic_toPoly_eq_normalize_gcd, CPolynomial.toPoly_zero, + normalize_eq_zero, EuclideanDomain.gcd_eq_zero_iff] at hpoly + exact (CPolynomial.toPoly_eq_zero_iff p).not.mpr hp hpoly.1 + +/-- A root of the monic gcd is exactly a common root of both arguments. -/ +theorem eval_monicNormalize_gcdMonic_eq_zero_iff {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (p r : CPolynomial F) (z : F) : + CPolynomial.eval z (CPolynomial.monicNormalize (CPolynomial.gcdMonic p r)) = 0 ↔ + CPolynomial.eval z p = 0 ∧ CPolynomial.eval z r = 0 := by + let : DecidableEq F := instDecidableEqOfLawfulBEq + simp only [CPolynomial.eval_toPoly] + rw [CPolynomial.monicNormalize_toPoly_eq_normalize, + CPolynomial.gcdMonic_toPoly_eq_normalize_gcd, normalize_idem] + constructor + · intro hroot + have hdvd : Polynomial.X - Polynomial.C z ∣ + EuclideanDomain.gcd p.toPoly r.toPoly := + dvd_normalize_iff.mp (Polynomial.dvd_iff_isRoot.mpr hroot) + exact ⟨Polynomial.dvd_iff_isRoot.mp (hdvd.trans (EuclideanDomain.gcd_dvd_left _ _)), + Polynomial.dvd_iff_isRoot.mp (hdvd.trans (EuclideanDomain.gcd_dvd_right _ _))⟩ + · rintro ⟨hp, hr⟩ + exact Polynomial.dvd_iff_isRoot.mp + (dvd_normalize_iff.mpr + (EuclideanDomain.dvd_gcd (Polynomial.dvd_iff_isRoot.mpr hp) + (Polynomial.dvd_iff_isRoot.mpr hr))) + +end FiniteField + +end Roots + +end CPolynomial + +end CompPoly diff --git a/CompPoly/Univariate/Roots/LasVegas/Correctness/EvenTrace.lean b/CompPoly/Univariate/Roots/LasVegas/Correctness/EvenTrace.lean new file mode 100644 index 00000000..38dd9cca --- /dev/null +++ b/CompPoly/Univariate/Roots/LasVegas/Correctness/EvenTrace.lean @@ -0,0 +1,641 @@ +/- +Copyright (c) 2026 CompPoly Contributors. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Valerii Huhnin +-/ + +module + +public import CompPoly.Univariate.Roots.LasVegas.Correctness.Common + +/-! +# Even-Trace Split Correctness for Las Vegas Splitting + +Correctness lemmas for the characteristic-two trace split attempt and retry loop. +-/ + +@[expose] public section + +namespace CompPoly + +namespace CPolynomial + +namespace Roots + +namespace FiniteField + +private theorem cantorZassenhausEvenTraceAttemptWith_root {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + (traceCtx : SmallPrimeTraceContext F) + (q : Nat) (probes : ProbeFamily F) {g : CPolynomial F} {a : F} + {attempt : Nat} {children : Array (CPolynomial F)} + (htry : + cantorZassenhausEvenTraceAttemptWith M D traceCtx q probes g attempt = some children) + (hg : g ≠ 0) (hroot : CPolynomial.eval a g = 0) : + ∃ child, child ∈ children.toList ∧ CPolynomial.eval a child = 0 := by + unfold cantorZassenhausEvenTraceAttemptWith at htry + simp only at htry + split at htry + next hsize => + injection htry with hchildren + subst children + let g' := CPolynomial.monicNormalize g + let h := reduceModWith D g' (probes.probe q g' attempt) + let tracePoly := tracePowerSumPolynomialWith M D g' traceCtx.p traceCtx.k h + let tracePart := CPolynomial.monicNormalize (CPolynomial.gcdMonic g' tracePoly) + let complement := quotientAfterChild g' tracePart + have hroot' : CPolynomial.eval a g' = 0 := (monicNormalize_root_iff hg).2 hroot + have hdivTrace : tracePart.toPoly ∣ g'.toPoly := by + dsimp [tracePart] + exact (toPoly_monicNormalize_dvd_self _).trans + (toPoly_gcdMonic_dvd_left g' tracePoly) + by_cases htraceRoot : CPolynomial.eval a tracePart = 0 + · refine ⟨tracePart, ?_, htraceRoot⟩ + change tracePart ∈ (nontrivialProperChildren g' #[tracePart, complement]).toList + have hsize' : 2 ≤ (nontrivialProperChildren g' #[tracePart, complement]).size := by + simpa [g', h, tracePoly, tracePart, complement] using hsize + have hproperTrace : isNontrivialProperChild g' tracePart = true := + left_proper_of_pair_filter_size_ge_two hsize' + exact nontrivialProperChildren_mem_of_mem (by simp) hproperTrace + · have hcomplementRoot : CPolynomial.eval a complement = 0 := by + dsimp [complement] + exact quotientAfterChild_root_of_not_child_root hdivTrace hroot' htraceRoot + refine ⟨complement, ?_, hcomplementRoot⟩ + change complement ∈ (nontrivialProperChildren g' #[tracePart, complement]).toList + have hsize' : 2 ≤ (nontrivialProperChildren g' #[tracePart, complement]).size := by + simpa [g', h, tracePoly, tracePart, complement] using hsize + have hproperComplement : isNontrivialProperChild g' complement = true := + right_proper_of_pair_filter_size_ge_two hsize' + exact nontrivialProperChildren_mem_of_mem (by simp) hproperComplement + next hsize => + simp at htry + +theorem cantorZassenhausEvenTraceAttemptWith_child_proper {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + (traceCtx : SmallPrimeTraceContext F) + (q : Nat) (probes : ProbeFamily F) {g child : CPolynomial F} + {attempt : Nat} {children : Array (CPolynomial F)} + (htry : + cantorZassenhausEvenTraceAttemptWith M D traceCtx q probes g attempt = some children) + (hmem : child ∈ children.toList) : + isNontrivialProperChild (CPolynomial.monicNormalize g) child = true := by + unfold cantorZassenhausEvenTraceAttemptWith at htry + simp only at htry + split at htry + next hsize => + injection htry with hchildren + subst children + exact proper_of_mem_nontrivialProperChildren hmem + next hsize => + simp at htry + +theorem cantorZassenhausEvenTraceAttemptWith_stackWork_le {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + (traceCtx : SmallPrimeTraceContext F) + (q : Nat) (probes : ProbeFamily F) {g : CPolynomial F} + {attempt : Nat} {children : Array (CPolynomial F)} + (htry : + cantorZassenhausEvenTraceAttemptWith M D traceCtx q probes g attempt = some children) + (hg : g ≠ 0) : + stackWork children.toList ≤ splitWork (CPolynomial.monicNormalize g) - 1 := by + unfold cantorZassenhausEvenTraceAttemptWith at htry + simp only at htry + split at htry + next hsize => + injection htry with hchildren + subst children + let g' := CPolynomial.monicNormalize g + let h := reduceModWith D g' (probes.probe q g' attempt) + let tracePoly := tracePowerSumPolynomialWith M D g' traceCtx.p traceCtx.k h + let tracePart := CPolynomial.monicNormalize (CPolynomial.gcdMonic g' tracePoly) + let complement := quotientAfterChild g' tracePart + have hg' : g' ≠ 0 := monicNormalize_ne_zero_of_ne_zero hg + have hsize' : 2 ≤ (nontrivialProperChildren g' #[tracePart, complement]).size := by + simpa [g', h, tracePoly, tracePart, complement] using hsize + have hproperTrace : isNontrivialProperChild g' tracePart = true := + left_proper_of_pair_filter_size_ge_two hsize' + have hproperComplement : isNontrivialProperChild g' complement = true := + right_proper_of_pair_filter_size_ge_two hsize' + have hchildrenList : + (nontrivialProperChildren g' #[tracePart, complement]).toList = + [tracePart, complement] := by + unfold nontrivialProperChildren + simp [hproperTrace, hproperComplement] + have hdivTrace : tracePart.toPoly ∣ g'.toPoly := by + dsimp [tracePart] + exact (toPoly_monicNormalize_dvd_self _).trans + (toPoly_gcdMonic_dvd_left g' tracePoly) + have hsum : + tracePart.toPoly.natDegree + complement.toPoly.natDegree ≤ + g'.toPoly.natDegree := by + dsimp [complement] + exact child_quotient_natDegree_le_parent hproperTrace hdivTrace hg' + have htracePos : 0 < tracePart.toPoly.natDegree := by + have hproperTrace' : + isNontrivialProperChild g' + (CPolynomial.monicNormalize (CPolynomial.gcdMonic g' tracePoly)) = true := by + simpa [tracePart] using hproperTrace + simpa [tracePart] using + (monicNormalize_toPoly_natDegree_pos_of_proper hproperTrace') + have hcomplementPos : 0 < complement.toPoly.natDegree := by + have hproperComplement' : + isNontrivialProperChild g' + (CPolynomial.monicNormalize (g' / tracePart)) = true := by + simpa [complement, quotientAfterChild, hproperTrace, Div.div] using hproperComplement + simpa [complement, quotientAfterChild, hproperTrace, Div.div] using + (monicNormalize_toPoly_natDegree_pos_of_proper hproperComplement') + rw [hchildrenList] + simp [stackWork] + have hsum' : + tracePart.toPoly.natDegree + complement.toPoly.natDegree ≤ + (CPolynomial.monicNormalize g).toPoly.natDegree := by + simpa [g'] using hsum + exact splitWork_pair_le_of_natDegree_sum_le htracePos hcomplementPos hsum' + next _hsize => + simp at htry + +theorem cantorZassenhausEvenTraceAttemptWith_child_normSplitWork_pos {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + (traceCtx : SmallPrimeTraceContext F) + (q : Nat) (probes : ProbeFamily F) {g child : CPolynomial F} + {attempt : Nat} {children : Array (CPolynomial F)} + (htry : + cantorZassenhausEvenTraceAttemptWith M D traceCtx q probes g attempt = some children) + (hg : g ≠ 0) (hmem : child ∈ children.toList) : + 1 ≤ normSplitWork child := by + unfold cantorZassenhausEvenTraceAttemptWith at htry + simp only at htry + split at htry + next hsize => + injection htry with hchildren + subst children + let g' := CPolynomial.monicNormalize g + let h := reduceModWith D g' (probes.probe q g' attempt) + let tracePoly := tracePowerSumPolynomialWith M D g' traceCtx.p traceCtx.k h + let tracePart := CPolynomial.monicNormalize (CPolynomial.gcdMonic g' tracePoly) + let complement := quotientAfterChild g' tracePart + have hg' : g' ≠ 0 := monicNormalize_ne_zero_of_ne_zero hg + have hsize' : 2 ≤ (nontrivialProperChildren g' #[tracePart, complement]).size := by + simpa [g', h, tracePoly, tracePart, complement] using hsize + have hproperTrace : isNontrivialProperChild g' tracePart = true := + left_proper_of_pair_filter_size_ge_two hsize' + have hproperComplement : isNontrivialProperChild g' complement = true := + right_proper_of_pair_filter_size_ge_two hsize' + have hchildrenList : + (nontrivialProperChildren g' #[tracePart, complement]).toList = + [tracePart, complement] := by + unfold nontrivialProperChildren + simp [hproperTrace, hproperComplement] + rw [hchildrenList] at hmem + simp at hmem + rcases hmem with hmem | hmem + · subst child + have htraceNe : tracePart ≠ 0 := by + unfold isNontrivialProperChild at hproperTrace + simp at hproperTrace + exact hproperTrace.1.1 + have htraceNotOne : tracePart ≠ 1 := by + unfold isNontrivialProperChild at hproperTrace + simp at hproperTrace + exact hproperTrace.1.2 + have htraceMonic : tracePart.toPoly.Monic := by + dsimp [tracePart] + exact monicNormalize_toPoly_monic_of_ne_zero + (gcdMonic_ne_zero_of_left hg') + exact normSplitWork_pos_of_monic_ne_zero_ne_one + htraceMonic htraceNe htraceNotOne + · subst child + have hcomplementNe : complement ≠ 0 := by + unfold isNontrivialProperChild at hproperComplement + simp at hproperComplement + exact hproperComplement.1.1 + have hcomplementNotOne : complement ≠ 1 := by + unfold isNontrivialProperChild at hproperComplement + simp at hproperComplement + exact hproperComplement.1.2 + have hdivTrace : tracePart.toPoly ∣ g'.toPoly := by + dsimp [tracePart] + exact (toPoly_monicNormalize_dvd_self _).trans + (toPoly_gcdMonic_dvd_left g' tracePoly) + have hg'Monic : g'.toPoly.Monic := by + dsimp [g'] + exact monicNormalize_toPoly_monic_of_ne_zero hg + have hcomplementMonic : complement.toPoly.Monic := by + dsimp [complement] + exact quotientAfterChild_toPoly_monic_of_dvd hg'Monic hdivTrace hg' + exact normSplitWork_pos_of_monic_ne_zero_ne_one + hcomplementMonic hcomplementNe hcomplementNotOne + next _hsize => + simp at htry + +set_option maxHeartbeats 1600000 in +/-- The trace split attempt depends on the probe family only through the probe +it actually draws. -/ +theorem cantorZassenhausEvenTraceAttemptWith_probe_congr {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + (traceCtx : SmallPrimeTraceContext F) + (q : Nat) {p1 p2 : ProbeFamily F} {g : CPolynomial F} {a1 a2 : Nat} + (hprobe : p1.probe q (CPolynomial.monicNormalize g) a1 = + p2.probe q (CPolynomial.monicNormalize g) a2) : + cantorZassenhausEvenTraceAttemptWith M D traceCtx q p1 g a1 = + cantorZassenhausEvenTraceAttemptWith M D traceCtx q p2 g a2 := by + simp only [cantorZassenhausEvenTraceAttemptWith] + rw [hprobe] + +set_option maxHeartbeats 1600000 in +/-- The trace retry loop depends on the probe family only through the probes it +actually draws. -/ +theorem tryEvenTraceSplitAttemptsWith_probe_congr {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + (traceCtx : SmallPrimeTraceContext F) + (q : Nat) {p1 p2 : ProbeFamily F} {g : CPolynomial F} : + ∀ (attempts offset1 offset2 : Nat), + (∀ i, i < attempts → + p1.probe q (CPolynomial.monicNormalize g) (offset1 + i) = + p2.probe q (CPolynomial.monicNormalize g) (offset2 + i)) → + tryEvenTraceSplitAttemptsWith M D traceCtx q p1 g attempts offset1 = + tryEvenTraceSplitAttemptsWith M D traceCtx q p2 g attempts offset2 := by + intro attempts + induction attempts with + | zero => + intro o1 o2 _hagree + unfold tryEvenTraceSplitAttemptsWith + rfl + | succ attempts ih => + intro o1 o2 hagree + unfold tryEvenTraceSplitAttemptsWith + rw [cantorZassenhausEvenTraceAttemptWith_probe_congr M D traceCtx q (g := g) + (by simpa using hagree 0 (by omega))] + cases htry : cantorZassenhausEvenTraceAttemptWith M D traceCtx q p2 g o2 with + | some c => simp + | none => + simp only + exact ih (o1 + 1) (o2 + 1) (by + intro i hi + have h := hagree (i + 1) (by omega) + simpa [Nat.add_assoc, Nat.add_comm, Nat.add_left_comm] using h) + +/-- A successful trace retry loop result is produced by some single attempt. -/ +theorem tryEvenTraceSplitAttemptsWith_eq_some_exists_attempt {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + (traceCtx : SmallPrimeTraceContext F) + (q : Nat) (probes : ProbeFamily F) {g : CPolynomial F} : + ∀ (attempts offset : Nat) {children : Array (CPolynomial F)}, + tryEvenTraceSplitAttemptsWith M D traceCtx q probes g attempts offset = + some children → + ∃ attempt, + cantorZassenhausEvenTraceAttemptWith M D traceCtx q probes g attempt = + some children := by + intro attempts + induction attempts with + | zero => + intro offset children htry + cases htry + | succ attempts ih => + intro offset children htry + unfold tryEvenTraceSplitAttemptsWith at htry + cases hsplit : cantorZassenhausEvenTraceAttemptWith M D traceCtx q probes g + offset with + | none => + simp [hsplit] at htry + exact ih (offset + 1) htry + | some splitChildren => + simp [hsplit] at htry + subst children + exact ⟨offset, hsplit⟩ + +theorem tryEvenTraceSplitAttemptsWith_root {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + (traceCtx : SmallPrimeTraceContext F) + (q : Nat) (probes : ProbeFamily F) {g : CPolynomial F} {a : F} + (hg : g ≠ 0) (hroot : CPolynomial.eval a g = 0) : + ∀ attempts offset {children : Array (CPolynomial F)}, + tryEvenTraceSplitAttemptsWith M D traceCtx q probes g attempts offset = some children → + ∃ child, child ∈ children.toList ∧ CPolynomial.eval a child = 0 := by + intro attempts + induction attempts with + | zero => + intro offset children htry + simp [tryEvenTraceSplitAttemptsWith] at htry + | succ attempts ih => + intro offset children htry + rw [tryEvenTraceSplitAttemptsWith] at htry + cases hsplit : + cantorZassenhausEvenTraceAttemptWith M D traceCtx q probes g offset with + | none => + simp [hsplit] at htry + exact ih (offset + 1) htry + | some splitChildren => + simp [hsplit] at htry + subst children + exact cantorZassenhausEvenTraceAttemptWith_root + M D traceCtx q probes hsplit hg hroot + +private theorem tryEvenTraceSplitAttemptsWith_child_proper {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + (traceCtx : SmallPrimeTraceContext F) + (q : Nat) (probes : ProbeFamily F) {g child : CPolynomial F} : + ∀ attempts offset {children : Array (CPolynomial F)}, + tryEvenTraceSplitAttemptsWith M D traceCtx q probes g attempts offset = some children → + child ∈ children.toList → + isNontrivialProperChild (CPolynomial.monicNormalize g) child = true := by + intro attempts + induction attempts with + | zero => + intro offset children htry hmem + simp [tryEvenTraceSplitAttemptsWith] at htry + | succ attempts ih => + intro offset children htry hmem + rw [tryEvenTraceSplitAttemptsWith] at htry + cases hsplit : + cantorZassenhausEvenTraceAttemptWith M D traceCtx q probes g offset with + | none => + simp [hsplit] at htry + exact ih (offset + 1) htry hmem + | some splitChildren => + simp [hsplit] at htry + subst children + exact cantorZassenhausEvenTraceAttemptWith_child_proper + M D traceCtx q probes hsplit hmem + +theorem tryEvenTraceSplitAttemptsWith_stackWork_le {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + (traceCtx : SmallPrimeTraceContext F) + (q : Nat) (probes : ProbeFamily F) {g : CPolynomial F} + (hg : g ≠ 0) : + ∀ attempts offset {children : Array (CPolynomial F)}, + tryEvenTraceSplitAttemptsWith M D traceCtx q probes g attempts offset = some children → + stackWork children.toList ≤ splitWork (CPolynomial.monicNormalize g) - 1 := by + intro attempts + induction attempts with + | zero => + intro offset children htry + simp [tryEvenTraceSplitAttemptsWith] at htry + | succ attempts ih => + intro offset children htry + rw [tryEvenTraceSplitAttemptsWith] at htry + cases hsplit : + cantorZassenhausEvenTraceAttemptWith M D traceCtx q probes g offset with + | none => + simp [hsplit] at htry + exact ih (offset + 1) htry + | some splitChildren => + simp [hsplit] at htry + subst children + exact cantorZassenhausEvenTraceAttemptWith_stackWork_le + M D traceCtx q probes hsplit hg + +theorem tryEvenTraceSplitAttemptsWith_child_normSplitWork_pos {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + (traceCtx : SmallPrimeTraceContext F) + (q : Nat) (probes : ProbeFamily F) {g child : CPolynomial F} + (hg : g ≠ 0) : + ∀ attempts offset {children : Array (CPolynomial F)}, + tryEvenTraceSplitAttemptsWith M D traceCtx q probes g attempts offset = some children → + child ∈ children.toList → + 1 ≤ normSplitWork child := by + intro attempts + induction attempts with + | zero => + intro offset children htry hmem + simp [tryEvenTraceSplitAttemptsWith] at htry + | succ attempts ih => + intro offset children htry hmem + rw [tryEvenTraceSplitAttemptsWith] at htry + cases hsplit : + cantorZassenhausEvenTraceAttemptWith M D traceCtx q probes g offset with + | none => + simp [hsplit] at htry + exact ih (offset + 1) htry hmem + | some splitChildren => + simp [hsplit] at htry + subst children + exact cantorZassenhausEvenTraceAttemptWith_child_normSplitWork_pos + M D traceCtx q probes hsplit hg hmem + +private theorem eval_add_eq {F : Type*} [Field F] [BEq F] [LawfulBEq F] + (z : F) (p q : CPolynomial F) : + CPolynomial.eval z (p + q) = CPolynomial.eval z p + CPolynomial.eval z q := by + rw [CPolynomial.eval_toPoly, CPolynomial.toPoly_add, Polynomial.eval_add, + ← CPolynomial.eval_toPoly, ← CPolynomial.eval_toPoly] + +private theorem eval_tracePowerSumPolynomialLoopWith {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + {modulus : CPolynomial F} {z : F} + (hroot : CPolynomial.eval z modulus = 0) (p : Nat) : + ∀ (steps : Nat) (power acc : CPolynomial F), + CPolynomial.eval z (tracePowerSumPolynomialLoopWith M D modulus p steps power acc) = + CPolynomial.eval z acc + + ∑ i ∈ Finset.range steps, CPolynomial.eval z power ^ p ^ i := by + intro steps + induction steps with + | zero => + intro power acc + rw [tracePowerSumPolynomialLoopWith] + simp + | succ steps ih => + intro power acc + rw [tracePowerSumPolynomialLoopWith] + rw [ih, eval_reduceModWith_eq_self_of_root D hroot, eval_add_eq, + eval_powModWith_eq_pow M D hroot p] + have hpow : ∀ i : Nat, + (CPolynomial.eval z power ^ p) ^ p ^ i = CPolynomial.eval z power ^ p ^ (i + 1) := by + intro i + rw [← pow_mul, pow_succ, Nat.mul_comm (p ^ i) p] + rw [Finset.sum_congr rfl fun i _ ↦ hpow i, Finset.sum_range_succ'] + rw [pow_zero, pow_one] + ring + +/-- Evaluating the modular trace power sum at a root of the modulus computes +the field-level trace power sum of the probe value. -/ +theorem eval_tracePowerSumPolynomialWith_eq {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + {modulus : CPolynomial F} {z : F} + (hroot : CPolynomial.eval z modulus = 0) (p k : Nat) (h : CPolynomial F) : + CPolynomial.eval z (tracePowerSumPolynomialWith M D modulus p k h) = + tracePowerSum p k (CPolynomial.eval z h) := by + unfold tracePowerSumPolynomialWith + rw [eval_tracePowerSumPolynomialLoopWith M D hroot p k _ _, + eval_reduceModWith_eq_self_of_root D hroot, tracePowerSum_eq_sum_range] + have hzero : CPolynomial.eval z (0 : CPolynomial F) = 0 := by + rw [CPolynomial.eval_toPoly, CPolynomial.toPoly_zero, Polynomial.eval_zero] + rw [hzero, _root_.zero_add] + +private theorem two_le_size_filter_pair {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + {parent u v : CPolynomial F} + (hu : isNontrivialProperChild parent u = true) + (hv : isNontrivialProperChild parent v = true) : + 2 ≤ (nontrivialProperChildren parent #[u, v]).size := by + unfold nontrivialProperChildren + rw [← Array.length_toList, Array.toList_filter] + simp [hu, hv] + +theorem cantorZassenhausEvenTraceAttemptWith_size_ge_two {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + (traceCtx : SmallPrimeTraceContext F) + (q : Nat) (probes : ProbeFamily F) {g : CPolynomial F} + {attempt : Nat} {children : Array (CPolynomial F)} + (htry : + cantorZassenhausEvenTraceAttemptWith M D traceCtx q probes g attempt = some children) : + 2 ≤ children.size := by + unfold cantorZassenhausEvenTraceAttemptWith at htry + simp only at htry + split at htry + next hsize => + injection htry with hchildren + subst children + simpa using hsize + next _hsize => + simp at htry + +theorem cantorZassenhausEvenTraceAttemptWith_child_dvd_input {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + (traceCtx : SmallPrimeTraceContext F) + (q : Nat) (probes : ProbeFamily F) {g child : CPolynomial F} + {attempt : Nat} {children : Array (CPolynomial F)} + (htry : + cantorZassenhausEvenTraceAttemptWith M D traceCtx q probes g attempt = some children) + (hmem : child ∈ children.toList) : + child.toPoly ∣ g.toPoly := by + unfold cantorZassenhausEvenTraceAttemptWith at htry + simp only at htry + split at htry + next _hsize => + injection htry with hchildren + subst children + let g' := CPolynomial.monicNormalize g + let h := reduceModWith D g' (probes.probe q g' attempt) + let tracePoly := tracePowerSumPolynomialWith M D g' traceCtx.p traceCtx.k h + let tracePart := CPolynomial.monicNormalize (CPolynomial.gcdMonic g' tracePoly) + let complement := quotientAfterChild g' tracePart + have hdivTrace : tracePart.toPoly ∣ g'.toPoly := by + dsimp [tracePart] + exact (toPoly_monicNormalize_dvd_self _).trans + (toPoly_gcdMonic_dvd_left g' tracePoly) + have hmem' : child ∈ (nontrivialProperChildren g' #[tracePart, complement]).toList := + hmem + have hpair : child = tracePart ∨ child = complement := by + unfold nontrivialProperChildren at hmem' + rw [Array.toList_filter, List.mem_filter] at hmem' + simpa using hmem'.1 + have hnorm : g'.toPoly ∣ g.toPoly := toPoly_monicNormalize_dvd_self g + rcases hpair with hchild | hchild + · subst hchild + exact hdivTrace.trans hnorm + · subst hchild + exact (quotientAfterChild_toPoly_dvd_parent hdivTrace).trans hnorm + next _hsize => + simp at htry + +/-- +A fixed probe whose trace power sums separate two roots of `g` forces the +characteristic-two trace split attempt to succeed: the root with trace zero +lands in `gcd(g, Tr(h))` and the root with nonzero trace survives into the +complement, so both candidates are nontrivial proper children. +-/ +theorem cantorZassenhausEvenTraceAttemptWith_success_of_trace_separated {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + (traceCtx : SmallPrimeTraceContext F) + (q : Nat) {g h : CPolynomial F} {a b : F} (attempt : Nat) + (hg : g ≠ 0) + (hrootA : CPolynomial.eval a g = 0) + (hrootB : CPolynomial.eval b g = 0) + (hsepA : tracePowerSum traceCtx.p traceCtx.k (CPolynomial.eval a h) = 0) + (hsepB : tracePowerSum traceCtx.p traceCtx.k (CPolynomial.eval b h) ≠ 0) : + ∃ children, + cantorZassenhausEvenTraceAttemptWith M D traceCtx q + ({ probe := fun _q _factor _attempt ↦ h } : ProbeFamily F) + g attempt = some children := by + cases htry : cantorZassenhausEvenTraceAttemptWith M D traceCtx q + ({ probe := fun _q _factor _attempt ↦ h } : ProbeFamily F) g attempt with + | some children => exact ⟨children, rfl⟩ + | none => + exfalso + unfold cantorZassenhausEvenTraceAttemptWith at htry + simp only at htry + split at htry + next hsize => simp at htry + next hsize => + apply hsize + let g' := CPolynomial.monicNormalize g + let h' := reduceModWith D g' h + let tracePoly := tracePowerSumPolynomialWith M D g' traceCtx.p traceCtx.k h' + let tracePart := CPolynomial.monicNormalize (CPolynomial.gcdMonic g' tracePoly) + let complement := quotientAfterChild g' tracePart + have hg' : g' ≠ 0 := monicNormalize_ne_zero_of_ne_zero hg + have hgMonic : g'.toPoly.Monic := monicNormalize_toPoly_monic_of_ne_zero hg + have hA' : CPolynomial.eval a g' = 0 := (monicNormalize_root_iff hg).2 hrootA + have hB' : CPolynomial.eval b g' = 0 := (monicNormalize_root_iff hg).2 hrootB + have hhA : CPolynomial.eval a h' = CPolynomial.eval a h := + eval_reduceModWith_eq_self_of_root D hA' + have hhB : CPolynomial.eval b h' = CPolynomial.eval b h := + eval_reduceModWith_eq_self_of_root D hB' + have htraceA : CPolynomial.eval a tracePoly = 0 := by + show CPolynomial.eval a + (tracePowerSumPolynomialWith M D g' traceCtx.p traceCtx.k h') = 0 + rw [eval_tracePowerSumPolynomialWith_eq M D hA', hhA] + exact hsepA + have htraceB : CPolynomial.eval b tracePoly ≠ 0 := by + show CPolynomial.eval b + (tracePowerSumPolynomialWith M D g' traceCtx.p traceCtx.k h') ≠ 0 + rw [eval_tracePowerSumPolynomialWith_eq M D hB', hhB] + exact hsepB + have htraceIff : ∀ z : F, CPolynomial.eval z tracePart = 0 ↔ + CPolynomial.eval z g' = 0 ∧ CPolynomial.eval z tracePoly = 0 := + fun z ↦ eval_monicNormalize_gcdMonic_eq_zero_iff g' tracePoly z + have hdivTrace : tracePart.toPoly ∣ g'.toPoly := + (toPoly_monicNormalize_dvd_self _).trans (toPoly_gcdMonic_dvd_left g' tracePoly) + have htracePartNe : tracePart ≠ 0 := + monicNormalize_ne_zero_of_ne_zero (gcdMonic_ne_zero_left tracePoly hg') + have htracePartMonic : tracePart.toPoly.Monic := + monicNormalize_toPoly_monic_of_ne_zero (gcdMonic_ne_zero_left tracePoly hg') + have htracePartA : CPolynomial.eval a tracePart = 0 := + (htraceIff a).2 ⟨hA', htraceA⟩ + have htracePartBne : CPolynomial.eval b tracePart ≠ 0 := by + intro hb + exact htraceB ((htraceIff b).1 hb).2 + have htraceSizeLt : tracePart.val.size < g'.val.size := by + by_contra hnot + apply htracePartBne + have heq : g' = tracePart := + eq_of_monic_dvd_of_val_size_le htracePartMonic hgMonic hdivTrace + htracePartNe hg' (Nat.le_of_not_lt hnot) + rw [← heq] + exact hB' + have hproperTrace : isNontrivialProperChild g' tracePart = true := + proper_child_of_ne_zero_root_size_lt htracePartNe htracePartA htraceSizeLt + have hcompB : CPolynomial.eval b complement = 0 := + quotientAfterChild_root_of_not_child_root hdivTrace hB' htracePartBne + have hcompNe : complement ≠ 0 := quotientAfterChild_ne_zero_of_dvd hdivTrace hg' + have hcompSizeLt : complement.val.size < g'.val.size := + quotientAfterChild_size_lt_parent_of_monicNormalize_proper hg' hproperTrace + have hproperComp : isNontrivialProperChild g' complement = true := + proper_child_of_ne_zero_root_size_lt hcompNe hcompB hcompSizeLt + have hfinal : 2 ≤ (nontrivialProperChildren g' #[tracePart, complement]).size := + two_le_size_filter_pair hproperTrace hproperComp + simpa [g', h', tracePoly, tracePart, complement] using hfinal + +end FiniteField + +end Roots + +end CPolynomial + +end CompPoly diff --git a/CompPoly/Univariate/Roots/LasVegas/Correctness/Loop.lean b/CompPoly/Univariate/Roots/LasVegas/Correctness/Loop.lean new file mode 100644 index 00000000..e7b11ddc --- /dev/null +++ b/CompPoly/Univariate/Roots/LasVegas/Correctness/Loop.lean @@ -0,0 +1,511 @@ +/- +Copyright (c) 2026 CompPoly Contributors. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Valerii Huhnin +-/ + +module + +-- `natDegree` and friends are declared in bare `public section`s, so their bodies +-- are opaque downstream; see `docs/wiki/module-system.md`. +import all CompPoly.Univariate.Basic +public import CompPoly.Univariate.Roots.LasVegas.Correctness.EvenTrace +public import CompPoly.Univariate.Roots.LasVegas.Correctness.Odd + +/-! +# Loop Correctness Surface for Las Vegas Splitting + +Completeness and packaging lemmas for the bounded Las Vegas splitter loop and +its enumeration fallback. +-/ + +@[expose] public section + +namespace CompPoly + +namespace CPolynomial + +namespace Roots + +namespace FiniteField + +private theorem representedEnumeratedLinearFactors_complete {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (enumeration : FieldEnumeration F) {p : CPolynomial F} {a : F} + (hroot : CPolynomial.eval a p = 0) : + ∃ factor, + factor ∈ (representedLinearFactorsOnly (enumeratedLinearFactors enumeration p)).toList ∧ + IsLinearRootFactorCandidate factor a := by + refine ⟨CPolynomial.linearFactor a, ?_, linearFactor_isRootFactorCandidate a⟩ + apply representedLinearFactorsOnly_mem_of_mem + · rw [enumeratedLinearFactors] + simpa using + (List.mem_map.mpr + ⟨a, rootsInFieldByEnumeration_complete enumeration hroot, rfl⟩) + · exact linearFactor_isRepresentedLinearFactor a + +private theorem linearFactor_mem_enumeratedLinearFactors {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (enumeration : FieldEnumeration F) {p : CPolynomial F} {a : F} + (hroot : CPolynomial.eval a p = 0) : + CPolynomial.linearFactor a ∈ (enumeratedLinearFactors enumeration p).toList := by + rw [enumeratedLinearFactors] + simpa using + (List.mem_map.mpr + ⟨a, rootsInFieldByEnumeration_complete enumeration hroot, rfl⟩) + +private theorem lasVegasSplitLoopWith_mem_of_mem_out {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + (enumeration : FieldEnumeration F) (traceCtx? : Option (SmallPrimeTraceContext F)) + (cfg : LasVegasConfig) (probes : ProbeFamily F) (q : Nat) + {factor : CPolynomial F} : + ∀ fuel stack out, + factor ∈ out.toList → + factor ∈ + (lasVegasSplitLoopWith M D enumeration traceCtx? cfg probes q fuel stack out).toList := by + intro fuel + induction fuel with + | zero => + intro stack out hmem + simp [lasVegasSplitLoopWith, hmem] + | succ fuel ih => + intro stack out hmem + cases stack with + | nil => + simp [lasVegasSplitLoopWith, hmem] + | cons g stack => + rw [lasVegasSplitLoopWith.eq_def] + simp only + by_cases hskip : (CPolynomial.monicNormalize g == 0 || + CPolynomial.monicNormalize g == 1) = true + · rw [if_pos hskip] + exact ih stack out hmem + · rw [if_neg hskip] + by_cases hlin : isRepresentedLinearFactor (CPolynomial.monicNormalize g) = true + · rw [if_pos hlin] + exact ih stack (out.push (CPolynomial.monicNormalize g)) (by simp [hmem]) + · rw [if_neg hlin] + by_cases hodd : (cfg.tryOddRandomizedSplitting && q % 2 == 1) = true + · rw [if_pos hodd] + cases htry : + tryOddSplitAttemptsWith M D q probes (CPolynomial.monicNormalize g) + cfg.cutoff 0 with + | none => + simp only + exact ih stack + (out ++ enumeratedLinearFactors enumeration (CPolynomial.monicNormalize g)) + (by simp [hmem]) + | some children => + simp only + exact ih (children.toList ++ stack) out hmem + · rw [if_neg hodd] + by_cases heven : (cfg.tryEvenTraceSplitting && q % 2 == 0) = true + · rw [if_pos heven] + cases traceCtx? with + | none => + simp only + exact ih stack + (out ++ enumeratedLinearFactors enumeration (CPolynomial.monicNormalize g)) + (by simp [hmem]) + | some traceCtx => + simp only + by_cases hmatch : traceContextMatchesQ traceCtx q = true + · rw [if_pos hmatch] + cases htry : + tryEvenTraceSplitAttemptsWith M D traceCtx q probes + (CPolynomial.monicNormalize g) cfg.cutoff 0 with + | none => + simp only + exact ih stack + (out ++ enumeratedLinearFactors enumeration + (CPolynomial.monicNormalize g)) + (by simp [hmem]) + | some children => + simp only + exact ih (children.toList ++ stack) out hmem + · rw [if_neg hmatch] + exact ih stack + (out ++ enumeratedLinearFactors enumeration + (CPolynomial.monicNormalize g)) + (by simp [hmem]) + · rw [if_neg heven] + exact ih stack + (out ++ enumeratedLinearFactors enumeration (CPolynomial.monicNormalize g)) + (by simp [hmem]) + +private theorem lasVegasSplitLoopWith_represented_mem_of_mem_out {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + (enumeration : FieldEnumeration F) (traceCtx? : Option (SmallPrimeTraceContext F)) + (cfg : LasVegasConfig) (probes : ProbeFamily F) (q : Nat) + {factor : CPolynomial F} {fuel : Nat} {stack : List (CPolynomial F)} + {out : Array (CPolynomial F)} + (hmem : factor ∈ out.toList) (hlin : isRepresentedLinearFactor factor = true) : + factor ∈ + (representedLinearFactorsOnly + (lasVegasSplitLoopWith M D enumeration traceCtx? cfg probes q fuel stack out)).toList := by + apply representedLinearFactorsOnly_mem_of_mem + · exact lasVegasSplitLoopWith_mem_of_mem_out + M D enumeration traceCtx? cfg probes q fuel stack out hmem + · exact hlin + +private theorem lasVegasSplitLoopWith_fallback_complete {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + (enumeration : FieldEnumeration F) (traceCtx? : Option (SmallPrimeTraceContext F)) + (cfg : LasVegasConfig) (probes : ProbeFamily F) (q : Nat) + {fuel : Nat} {stack : List (CPolynomial F)} {out : Array (CPolynomial F)} + {g : CPolynomial F} {a : F} (hroot : CPolynomial.eval a g = 0) : + ∃ factor, + factor ∈ + (representedLinearFactorsOnly + (lasVegasSplitLoopWith M D enumeration traceCtx? cfg probes q fuel stack + (out ++ enumeratedLinearFactors enumeration g))).toList ∧ + IsLinearRootFactorCandidate factor a := by + refine ⟨CPolynomial.linearFactor a, ?_, linearFactor_isRootFactorCandidate a⟩ + apply lasVegasSplitLoopWith_represented_mem_of_mem_out + M D enumeration traceCtx? cfg probes q + · simp [linearFactor_mem_enumeratedLinearFactors enumeration hroot] + · exact linearFactor_isRepresentedLinearFactor a + +set_option maxHeartbeats 800000 in +private theorem lasVegasSplitLoopWith_complete_of_state {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + (enumeration : FieldEnumeration F) (traceCtx? : Option (SmallPrimeTraceContext F)) + (cfg : LasVegasConfig) (probes : ProbeFamily F) (q : Nat) {a : F} : + ∀ fuel stack out, + normStackWork stack ≤ fuel → + normStackReady stack → + ((∃ factor, factor ∈ out.toList ∧ + isRepresentedLinearFactor factor = true ∧ + IsLinearRootFactorCandidate factor a) ∨ + (∃ g, g ∈ stack ∧ g ≠ 0 ∧ CPolynomial.eval a g = 0)) → + ∃ factor, + factor ∈ + (representedLinearFactorsOnly + (lasVegasSplitLoopWith M D enumeration traceCtx? cfg probes q + fuel stack out)).toList ∧ + IsLinearRootFactorCandidate factor a := by + intro fuel + induction fuel with + | zero => + intro stack out hwork hready hstate + rcases hstate with hout | hstack + · rcases hout with ⟨factor, hmem, hlin, hcand⟩ + exact ⟨factor, + lasVegasSplitLoopWith_represented_mem_of_mem_out + M D enumeration traceCtx? cfg probes q hmem hlin, + hcand⟩ + · rcases hstack with ⟨g, hmem, _hg, _hroot⟩ + have hpos := normStackWork_pos_of_mem hready hmem + omega + | succ fuel ih => + intro stack out hwork hready hstate + rcases hstate with hout | hstack + · rcases hout with ⟨factor, hmem, hlin, hcand⟩ + exact ⟨factor, + lasVegasSplitLoopWith_represented_mem_of_mem_out + M D enumeration traceCtx? cfg probes q hmem hlin, + hcand⟩ + · cases stack with + | nil => + rcases hstack with ⟨g, hmem, _hg, _hroot⟩ + simp at hmem + | cons g stack => + rcases hstack with ⟨rootPoly, hmemRoot, hrootNe, hrootEval⟩ + have hmemRoot' : rootPoly = g ∨ rootPoly ∈ stack := by + simpa using hmemRoot + have hheadReady := hready g (by simp) + have hheadPos : 1 ≤ normSplitWork g := + normSplitWork_pos_of_monicNormalize_ne_zero_ne_one + hheadReady.1 hheadReady.2 + have htailWork : normStackWork stack ≤ fuel := + normStackWork_tail_le_of_cons_le hwork hheadPos + have htailReady : normStackReady stack := + normStackReady_tail hready + rw [lasVegasSplitLoopWith.eq_def] + simp only + by_cases hskip : + (CPolynomial.monicNormalize g == 0 || + CPolynomial.monicNormalize g == 1) = true + · rw [if_pos hskip] + rcases hmemRoot' with hrootHead | hrootTail + · subst rootPoly + simp [hheadReady.1, hheadReady.2] at hskip + · exact ih stack out htailWork htailReady + (Or.inr ⟨rootPoly, hrootTail, hrootNe, hrootEval⟩) + · rw [if_neg hskip] + have hgNorm : CPolynomial.monicNormalize g ≠ 0 := hheadReady.1 + by_cases hlin : isRepresentedLinearFactor (CPolynomial.monicNormalize g) = true + · rw [if_pos hlin] + rcases hmemRoot' with hrootHead | hrootTail + · subst rootPoly + have hrootNorm : + CPolynomial.eval a (CPolynomial.monicNormalize g) = 0 := + (monicNormalize_root_iff hrootNe).2 hrootEval + exact ih stack (out.push (CPolynomial.monicNormalize g)) + htailWork htailReady + (Or.inl + ⟨CPolynomial.monicNormalize g, by simp, hlin, + representedLinearFactor_candidate_of_root hlin hrootNorm⟩) + · exact ih stack (out.push (CPolynomial.monicNormalize g)) + htailWork htailReady + (Or.inr ⟨rootPoly, hrootTail, hrootNe, hrootEval⟩) + · rw [if_neg hlin] + have hfallback : + ∃ factor, + factor ∈ + (representedLinearFactorsOnly + (lasVegasSplitLoopWith M D enumeration traceCtx? cfg probes q fuel stack + (out ++ enumeratedLinearFactors enumeration + (CPolynomial.monicNormalize g)))).toList ∧ + IsLinearRootFactorCandidate factor a := by + rcases hmemRoot' with hrootHead | hrootTail + · subst rootPoly + have hrootNorm : + CPolynomial.eval a (CPolynomial.monicNormalize g) = 0 := + (monicNormalize_root_iff hrootNe).2 hrootEval + exact lasVegasSplitLoopWith_fallback_complete + M D enumeration traceCtx? cfg probes q hrootNorm + · exact ih stack + (out ++ enumeratedLinearFactors enumeration (CPolynomial.monicNormalize g)) + htailWork htailReady + (Or.inr ⟨rootPoly, hrootTail, hrootNe, hrootEval⟩) + by_cases hodd : (cfg.tryOddRandomizedSplitting && q % 2 == 1) = true + · rw [if_pos hodd] + cases htry : + tryOddSplitAttemptsWith M D q probes (CPolynomial.monicNormalize g) + cfg.cutoff 0 with + | none => + simp only + exact hfallback + | some children => + simp only + have hchildrenWork : + stackWork children.toList ≤ normSplitWork g - 1 := by + have hworkChildren := + tryOddSplitAttemptsWith_stackWork_le M D q probes + hgNorm cfg.cutoff 0 htry + simpa [normSplitWork, splitWork_monicNormalize_eq] using hworkChildren + have hnextWork : + normStackWork (children.toList ++ stack) ≤ fuel := + normStackWork_split_stack_le hwork hheadPos hchildrenWork + have hchildrenReady : normStackReady children.toList := by + intro child hchild + exact normSplitWork_pos_ready + (tryOddSplitAttemptsWith_child_normSplitWork_pos + M D q probes hgNorm cfg.cutoff 0 htry hchild) + have hnextReady : normStackReady (children.toList ++ stack) := + normStackReady_append hchildrenReady htailReady + rcases hmemRoot' with hrootHead | hrootTail + · subst rootPoly + have hrootNorm : + CPolynomial.eval a (CPolynomial.monicNormalize g) = 0 := + (monicNormalize_root_iff hrootNe).2 hrootEval + rcases tryOddSplitAttemptsWith_root M D q probes + hgNorm hrootNorm cfg.cutoff 0 htry with + ⟨child, hchildMem, hchildRoot⟩ + have hchildNe : child ≠ 0 := by + intro hzero + have hchildReady := hchildrenReady child hchildMem + exact hchildReady.1 + (by simpa [hzero] using (monicNormalize_zero (F := F))) + exact ih (children.toList ++ stack) out hnextWork hnextReady + (Or.inr + ⟨child, by simp [hchildMem], hchildNe, hchildRoot⟩) + · exact ih (children.toList ++ stack) out hnextWork hnextReady + (Or.inr + ⟨rootPoly, by simp [hrootTail], hrootNe, hrootEval⟩) + · rw [if_neg hodd] + by_cases heven : (cfg.tryEvenTraceSplitting && q % 2 == 0) = true + · rw [if_pos heven] + cases traceCtx? with + | none => + simp only + exact hfallback + | some traceCtx => + simp only + by_cases hmatch : traceContextMatchesQ traceCtx q = true + · rw [if_pos hmatch] + cases htry : + tryEvenTraceSplitAttemptsWith M D traceCtx q probes + (CPolynomial.monicNormalize g) cfg.cutoff 0 with + | none => + simp only + exact hfallback + | some children => + simp only + have hchildrenWork : + stackWork children.toList ≤ normSplitWork g - 1 := by + have hworkChildren := + tryEvenTraceSplitAttemptsWith_stackWork_le + M D traceCtx q probes hgNorm cfg.cutoff 0 htry + simpa [normSplitWork, splitWork_monicNormalize_eq] using + hworkChildren + have hnextWork : + normStackWork (children.toList ++ stack) ≤ fuel := + normStackWork_split_stack_le hwork hheadPos hchildrenWork + have hchildrenReady : normStackReady children.toList := by + intro child hchild + exact normSplitWork_pos_ready + (tryEvenTraceSplitAttemptsWith_child_normSplitWork_pos + M D traceCtx q probes hgNorm cfg.cutoff 0 htry hchild) + have hnextReady : normStackReady (children.toList ++ stack) := + normStackReady_append hchildrenReady htailReady + rcases hmemRoot' with hrootHead | hrootTail + · subst rootPoly + have hrootNorm : + CPolynomial.eval a (CPolynomial.monicNormalize g) = 0 := + (monicNormalize_root_iff hrootNe).2 hrootEval + rcases tryEvenTraceSplitAttemptsWith_root + M D traceCtx q probes hgNorm hrootNorm cfg.cutoff 0 htry with + ⟨child, hchildMem, hchildRoot⟩ + have hchildNe : child ≠ 0 := by + intro hzero + have hchildReady := hchildrenReady child hchildMem + exact hchildReady.1 + (by simpa [hzero] using (monicNormalize_zero (F := F))) + exact ih (children.toList ++ stack) out hnextWork hnextReady + (Or.inr + ⟨child, by simp [hchildMem], hchildNe, hchildRoot⟩) + · exact ih (children.toList ++ stack) out hnextWork hnextReady + (Or.inr + ⟨rootPoly, by simp [hrootTail], hrootNe, hrootEval⟩) + · rw [if_neg hmatch] + exact hfallback + · rw [if_neg heven] + exact hfallback + +private theorem lasVegasSplitLinearFactorsWithTrace?_complete {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + (enumeration : FieldEnumeration F) (traceCtx? : Option (SmallPrimeTraceContext F)) + (cfg : LasVegasConfig) (probes : ProbeFamily F) (q : Nat) + {p : CPolynomial F} {a : F} + (_hvalid : lasVegasSplitterInput q p) (hp : p ≠ 0) + (hroot : CPolynomial.eval a p = 0) : + ∃ factor, + factor ∈ + (lasVegasSplitLinearFactorsWithTrace? M D enumeration traceCtx? cfg probes q p).toList ∧ + IsLinearRootFactorCandidate factor a := by + let p' := CPolynomial.monicNormalize p + have hp' : p' ≠ 0 := by + dsimp [p'] + exact monicNormalize_ne_zero_of_ne_zero hp + have hroot' : CPolynomial.eval a p' = 0 := by + dsimp [p'] + exact (monicNormalize_root_iff hp).2 hroot + have hwork : normStackWork [p'] ≤ 2 * p'.val.size + 1 := by + rw [show normStackWork [p'] = splitWork p' by + simp [normStackWork, normSplitWork, splitWork_monicNormalize_eq]] + unfold splitWork + rw [val_size_eq_natDegree_add_one_of_ne_zero hp', CPolynomial.natDegree_toPoly] + omega + have hready : normStackReady [p'] := by + intro g hmem + simp at hmem + subst g + constructor + · exact monicNormalize_ne_zero_of_ne_zero hp' + · intro hone + have hrootNorm : CPolynomial.eval a (CPolynomial.monicNormalize p') = 0 := + monicNormalize_root_of_root hroot' + rw [hone, CPolynomial.eval_one] at hrootNorm + exact one_ne_zero hrootNorm + have hloop := + lasVegasSplitLoopWith_complete_of_state M D enumeration traceCtx? cfg probes q + (a := a) (2 * p'.val.size + 1) [p'] #[] + hwork hready (Or.inr ⟨p', by simp, hp', hroot'⟩) + simpa [lasVegasSplitLinearFactorsWithTrace?, lasVegasSplitCandidatesWithTrace?, p'] using hloop + +/-- Completeness surface for bounded Las Vegas splitting plus enumeration fallback. -/ +theorem lasVegasSplitLinearFactorsWith_complete {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + (enumeration : FieldEnumeration F) (cfg : LasVegasConfig) + (probes : ProbeFamily F) (q : Nat) + {p : CPolynomial F} {a : F} + (hvalid : lasVegasSplitterInput q p) (hp : p ≠ 0) + (hroot : CPolynomial.eval a p = 0) : + ∃ factor, + factor ∈ + (lasVegasSplitLinearFactorsWith M D enumeration cfg probes q p).toList ∧ + IsLinearRootFactorCandidate factor a := by + simpa [lasVegasSplitLinearFactorsWith] using + lasVegasSplitLinearFactorsWithTrace?_complete + M D enumeration none cfg probes q hvalid hp hroot + +/-- Completeness surface for bounded Las Vegas splitting with characteristic-two trace metadata. -/ +theorem lasVegasSplitLinearFactorsWithTrace_complete {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + (enumeration : FieldEnumeration F) (traceCtx : SmallPrimeTraceContext F) + (cfg : LasVegasConfig) (probes : ProbeFamily F) (q : Nat) + {p : CPolynomial F} {a : F} + (hvalid : lasVegasSplitterInput q p) (hp : p ≠ 0) + (hroot : CPolynomial.eval a p = 0) : + ∃ factor, + factor ∈ + (lasVegasSplitLinearFactorsWithTrace M D enumeration traceCtx cfg probes q p).toList ∧ + IsLinearRootFactorCandidate factor a := by + simpa [lasVegasSplitLinearFactorsWithTrace] using + lasVegasSplitLinearFactorsWithTrace?_complete + M D enumeration (some traceCtx) cfg probes q hvalid hp hroot + +/-- Package bounded Las Vegas splitting as a finite-field linear-factor splitter. -/ +def lasVegasLinearFactorProductSplitterWith {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + (_ctx : FiniteFieldContext F) (enumeration : FieldEnumeration F) + (cfg : LasVegasConfig) (probes : ProbeFamily F) : + LinearFactorProductSplitter F where + splitLinearFactors := fun q p ↦ + lasVegasSplitLinearFactorsWith M D enumeration cfg probes q p + validInput := fun q p ↦ lasVegasSplitterInput q p + sound := by + intro q p factor h + exact lasVegasSplitLinearFactorsWith_sound M D enumeration cfg probes q h + complete := by + intro q p a hvalid hp hroot + exact lasVegasSplitLinearFactorsWith_complete + M D enumeration cfg probes q hvalid hp hroot + +/-- Package bounded Las Vegas trace splitting as a finite-field linear-factor splitter. -/ +def lasVegasLinearFactorProductSplitterWithTrace {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + (_ctx : FiniteFieldContext F) (enumeration : FieldEnumeration F) + (traceCtx : SmallPrimeTraceContext F) + (cfg : LasVegasConfig) (probes : ProbeFamily F) : + LinearFactorProductSplitter F where + splitLinearFactors := fun q p ↦ + lasVegasSplitLinearFactorsWithTrace M D enumeration traceCtx cfg probes q p + validInput := fun q p ↦ lasVegasSplitterInput q p + sound := by + intro q p factor h + exact lasVegasSplitLinearFactorsWithTrace_sound M D enumeration traceCtx cfg probes q h + complete := by + intro q p a hvalid hp hroot + exact lasVegasSplitLinearFactorsWithTrace_complete + M D enumeration traceCtx cfg probes q hvalid hp hroot + +/-- Alias emphasizing the root-product precondition used by Las Vegas completeness. -/ +theorem rootProduct_satisfies_lasVegasSplitterInput {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + (ctx : FiniteFieldContext F) (enumeration : FieldEnumeration F) + (cfg : LasVegasConfig) (probes : ProbeFamily F) + {p : CPolynomial F} (hp : p ≠ 0) : + (lasVegasLinearFactorProductSplitterWith M D ctx enumeration cfg probes).validInput ctx.q + (finiteFieldRootProductWith M D ctx p) := by + exact finiteFieldRootProductWith_lasVegasSplitterInput M D ctx hp + +end FiniteField + +end Roots + +end CPolynomial + +end CompPoly diff --git a/CompPoly/Univariate/Roots/LasVegas/Correctness/Odd.lean b/CompPoly/Univariate/Roots/LasVegas/Correctness/Odd.lean new file mode 100644 index 00000000..44e51cb7 --- /dev/null +++ b/CompPoly/Univariate/Roots/LasVegas/Correctness/Odd.lean @@ -0,0 +1,1109 @@ +/- +Copyright (c) 2026 CompPoly Contributors. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Valerii Huhnin +-/ + +module + +-- `natDegree` and friends are declared in bare `public section`s, so their bodies +-- are opaque downstream; see `docs/wiki/module-system.md`. +import all CompPoly.Univariate.Basic +public import CompPoly.Univariate.Roots.LasVegas.Correctness.Common +public import CompPoly.Univariate.Roots.LasVegas.OddBucket + +/-! +# Odd Split Correctness for Las Vegas Splitting + +Correctness lemmas for the odd-characteristic Cantor-Zassenhaus split attempt +and retry loop. +-/ + +@[expose] public section + +namespace CompPoly + +namespace CPolynomial + +namespace Roots + +namespace FiniteField + +private theorem cantorZassenhausOddAttemptWith_root {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + (q : Nat) (probes : ProbeFamily F) {g : CPolynomial F} {a : F} + {attempt : Nat} {children : Array (CPolynomial F)} + (htry : cantorZassenhausOddAttemptWith M D q probes g attempt = some children) + (hg : g ≠ 0) (hroot : CPolynomial.eval a g = 0) : + ∃ child, child ∈ children.toList ∧ CPolynomial.eval a child = 0 := by + unfold cantorZassenhausOddAttemptWith at htry + simp only at htry + split at htry + next hsize => + injection htry with hchildren + subst children + let g' := CPolynomial.monicNormalize g + let h := reduceModWith D g' (probes.probe q g' attempt) + let s := powModWith M D g' h ((q - 1) / 2) + let zeroPart := CPolynomial.monicNormalize (CPolynomial.gcdMonic g' h) + let afterZero := quotientAfterChild g' zeroPart + let squarePart := CPolynomial.monicNormalize + (CPolynomial.gcdMonic afterZero (s - (1 : CPolynomial F))) + let afterSquare := quotientAfterChild afterZero squarePart + have hg' : g' ≠ 0 := monicNormalize_ne_zero_of_ne_zero hg + have hroot' : CPolynomial.eval a g' = 0 := (monicNormalize_root_iff hg).2 hroot + have hsize' : + 2 ≤ (nontrivialProperChildren g' #[zeroPart, squarePart, afterSquare]).eraseDups.size := by + simpa [g', h, s, zeroPart, afterZero, squarePart, afterSquare] using hsize + have hdivZero : zeroPart.toPoly ∣ g'.toPoly := by + dsimp [zeroPart] + exact (toPoly_monicNormalize_dvd_self _).trans + (toPoly_gcdMonic_dvd_left g' h) + have hdivSquare : squarePart.toPoly ∣ afterZero.toPoly := by + dsimp [squarePart] + exact (toPoly_monicNormalize_dvd_self _).trans + (toPoly_gcdMonic_dvd_left afterZero (s - (1 : CPolynomial F))) + have hzeroMem (hproperZero : isNontrivialProperChild g' zeroPart = true) : + zeroPart ∈ + ((nontrivialProperChildren g' + #[zeroPart, squarePart, afterSquare]).eraseDups).toList := by + have hraw : + zeroPart ∈ (nontrivialProperChildren g' #[zeroPart, squarePart, afterSquare]).toList := + nontrivialProperChildren_mem_of_mem (by simp) hproperZero + simpa using (mem_eraseDups_of_mem (by simpa using hraw)) + have hsquareMem (hproperSquare : isNontrivialProperChild g' squarePart = true) : + squarePart ∈ + ((nontrivialProperChildren g' + #[zeroPart, squarePart, afterSquare]).eraseDups).toList := by + have hraw : + squarePart ∈ (nontrivialProperChildren g' #[zeroPart, squarePart, afterSquare]).toList := + nontrivialProperChildren_mem_of_mem (by simp) hproperSquare + simpa using (mem_eraseDups_of_mem (by simpa using hraw)) + have hafterMem (hproperAfter : isNontrivialProperChild g' afterSquare = true) : + afterSquare ∈ + ((nontrivialProperChildren g' + #[zeroPart, squarePart, afterSquare]).eraseDups).toList := by + have hraw : + afterSquare ∈ (nontrivialProperChildren g' #[zeroPart, squarePart, afterSquare]).toList := + nontrivialProperChildren_mem_of_mem (by simp) hproperAfter + simpa using (mem_eraseDups_of_mem (by simpa using hraw)) + have hafterZeroRoot_of_not_zero + (hzeroRoot : CPolynomial.eval a zeroPart ≠ 0) : + CPolynomial.eval a afterZero = 0 := by + dsimp [afterZero] + exact quotientAfterChild_root_of_not_child_root hdivZero hroot' hzeroRoot + have hafterSquareRoot_of_afterZero + (hafterZeroRoot : CPolynomial.eval a afterZero = 0) + (hsquareRoot : CPolynomial.eval a squarePart ≠ 0) : + CPolynomial.eval a afterSquare = 0 := by + dsimp [afterSquare] + exact quotientAfterChild_root_of_not_child_root hdivSquare hafterZeroRoot hsquareRoot + by_cases hzeroRoot : CPolynomial.eval a zeroPart = 0 + · by_cases hproperZero : isNontrivialProperChild g' zeroPart = true + · refine ⟨zeroPart, hzeroMem hproperZero, hzeroRoot⟩ + · have hafterZeroRoot : CPolynomial.eval a afterZero = 0 := by + dsimp [afterZero, quotientAfterChild] + rw [if_neg hproperZero] + exact hroot' + by_cases hsquareRoot : CPolynomial.eval a squarePart = 0 + · have hproperSquare : + isNontrivialProperChild g' squarePart = true := + middle_proper_of_triple_eraseDups_filter_size_ge_two_of_not_left + hproperZero hsize' + refine ⟨squarePart, hsquareMem hproperSquare, hsquareRoot⟩ + · have hafterRoot : CPolynomial.eval a afterSquare = 0 := + hafterSquareRoot_of_afterZero hafterZeroRoot hsquareRoot + have hproperAfter : + isNontrivialProperChild g' afterSquare = true := + right_proper_of_triple_eraseDups_filter_size_ge_two_of_not_left + hproperZero hsize' + refine ⟨afterSquare, hafterMem hproperAfter, hafterRoot⟩ + · have hafterZeroRoot : CPolynomial.eval a afterZero = 0 := + hafterZeroRoot_of_not_zero hzeroRoot + by_cases hsquareRoot : CPolynomial.eval a squarePart = 0 + · by_cases hproperSquare : isNontrivialProperChild g' squarePart = true + · refine ⟨squarePart, hsquareMem hproperSquare, hsquareRoot⟩ + · have hafterRoot : CPolynomial.eval a afterSquare = 0 := by + have hafterZeroSizeLe : afterZero.val.size ≤ g'.val.size := by + dsimp [afterZero] + exact quotientAfterChild_size_le_parent hg' + have hnotProperAfterZero : + ¬ isNontrivialProperChild afterZero squarePart = true := by + intro hproperAfterZero + exact hproperSquare + (proper_child_of_proper_intermediate hproperAfterZero hafterZeroSizeLe) + dsimp [afterSquare, quotientAfterChild] + rw [if_neg hnotProperAfterZero] + exact hafterZeroRoot + have hproperAfter : + isNontrivialProperChild g' afterSquare = true := + right_proper_of_triple_eraseDups_filter_size_ge_two_of_not_middle + hproperSquare hsize' + refine ⟨afterSquare, hafterMem hproperAfter, hafterRoot⟩ + · have hafterRoot : CPolynomial.eval a afterSquare = 0 := + hafterSquareRoot_of_afterZero hafterZeroRoot hsquareRoot + by_cases hproperSquare : isNontrivialProperChild g' squarePart = true + · by_cases hproperAfter : isNontrivialProperChild g' afterSquare = true + · refine ⟨afterSquare, hafterMem hproperAfter, hafterRoot⟩ + · by_cases hproperZero : isNontrivialProperChild g' zeroPart = true + · have hafterZeroNe : afterZero ≠ 0 := + quotientAfterChild_ne_zero_of_dvd hdivZero hg' + have hafterSquareNe : afterSquare ≠ 0 := + quotientAfterChild_ne_zero_of_dvd hdivSquare hafterZeroNe + have hafterZeroSize : afterZero.val.size < g'.val.size := by + have hproperZero' : + isNontrivialProperChild g' + (CPolynomial.monicNormalize (CPolynomial.gcdMonic g' h)) = true := by + simpa [zeroPart] using hproperZero + simpa [afterZero, zeroPart] using + (quotientAfterChild_size_lt_parent_of_monicNormalize_proper + (parent := g') (childSource := CPolynomial.gcdMonic g' h) + hg' hproperZero') + have hafterSquareSizeLe : afterSquare.val.size ≤ afterZero.val.size := by + dsimp [afterSquare] + exact quotientAfterChild_size_le_parent hafterZeroNe + have hafterSquareSize : afterSquare.val.size < g'.val.size := + lt_of_le_of_lt hafterSquareSizeLe hafterZeroSize + have hproperAfter' : + isNontrivialProperChild g' afterSquare = true := + proper_child_of_ne_zero_root_size_lt hafterSquareNe hafterRoot hafterSquareSize + exact False.elim (hproperAfter hproperAfter') + · have hproperAfter' : + isNontrivialProperChild g' afterSquare = true := + right_proper_of_triple_eraseDups_filter_size_ge_two_of_not_left + hproperZero hsize' + exact False.elim (hproperAfter hproperAfter') + · have hproperAfter : + isNontrivialProperChild g' afterSquare = true := + right_proper_of_triple_eraseDups_filter_size_ge_two_of_not_middle + hproperSquare hsize' + refine ⟨afterSquare, hafterMem hproperAfter, hafterRoot⟩ + next _hsize => + simp at htry + +theorem cantorZassenhausOddAttemptWith_child_proper {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + (q : Nat) (probes : ProbeFamily F) {g child : CPolynomial F} + {attempt : Nat} {children : Array (CPolynomial F)} + (htry : cantorZassenhausOddAttemptWith M D q probes g attempt = some children) + (hmem : child ∈ children.toList) : + isNontrivialProperChild (CPolynomial.monicNormalize g) child = true := by + unfold cantorZassenhausOddAttemptWith at htry + simp only at htry + split at htry + next _hsize => + injection htry with hchildren + subst children + have hraw := mem_of_mem_eraseDups (by simpa using hmem) + exact proper_of_mem_nontrivialProperChildren (by simpa using hraw) + next _hsize => + simp at htry + +theorem cantorZassenhausOddAttemptWith_size_ge_two {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + (q : Nat) (probes : ProbeFamily F) {g : CPolynomial F} + {attempt : Nat} {children : Array (CPolynomial F)} + (htry : cantorZassenhausOddAttemptWith M D q probes g attempt = some children) : + 2 ≤ children.size := by + unfold cantorZassenhausOddAttemptWith at htry + simp only at htry + split at htry + next hsize => + injection htry with hchildren + subst children + simpa using hsize + next _hsize => + simp at htry + +theorem cantorZassenhausOddAttemptWith_ne_zero {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + (q : Nat) (probes : ProbeFamily F) {g : CPolynomial F} + {attempt : Nat} {children : Array (CPolynomial F)} + (htry : cantorZassenhausOddAttemptWith M D q probes g attempt = some children) : + g ≠ 0 := by + intro hg + have hsize := cantorZassenhausOddAttemptWith_size_ge_two M D q probes htry + have hpos : 0 < children.size := by omega + let child : CPolynomial F := children[0] + have hmem : child ∈ children.toList := by + exact Array.getElem_mem_toList hpos + have hproper := + cantorZassenhausOddAttemptWith_child_proper M D q probes htry hmem + unfold isNontrivialProperChild at hproper + simp [hg, monicNormalize_zero] at hproper + exact Nat.not_lt_zero _ hproper.2 + +theorem cantorZassenhausOddAttemptWith_root_preserved {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + (q : Nat) (probes : ProbeFamily F) {g : CPolynomial F} {a : F} + {attempt : Nat} {children : Array (CPolynomial F)} + (htry : cantorZassenhausOddAttemptWith M D q probes g attempt = some children) + (hroot : CPolynomial.eval a g = 0) : + ∃ child, child ∈ children.toList ∧ CPolynomial.eval a child = 0 := by + exact cantorZassenhausOddAttemptWith_root M D q probes htry + (cantorZassenhausOddAttemptWith_ne_zero M D q probes htry) hroot + +theorem cantorZassenhausOddAttemptWith_child_dvd_input {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + (q : Nat) (probes : ProbeFamily F) {g child : CPolynomial F} + {attempt : Nat} {children : Array (CPolynomial F)} + (htry : cantorZassenhausOddAttemptWith M D q probes g attempt = some children) + (hmem : child ∈ children.toList) : + child.toPoly ∣ g.toPoly := by + unfold cantorZassenhausOddAttemptWith at htry + simp only at htry + split at htry + next _hsize => + injection htry with hchildren + subst children + let g' := CPolynomial.monicNormalize g + let h := reduceModWith D g' (probes.probe q g' attempt) + let s := powModWith M D g' h ((q - 1) / 2) + let zeroPart := CPolynomial.monicNormalize (CPolynomial.gcdMonic g' h) + let afterZero := quotientAfterChild g' zeroPart + let squarePart := CPolynomial.monicNormalize + (CPolynomial.gcdMonic afterZero (s - (1 : CPolynomial F))) + let afterSquare := quotientAfterChild afterZero squarePart + have hdivZero : zeroPart.toPoly ∣ g'.toPoly := by + dsimp [zeroPart] + exact (toPoly_monicNormalize_dvd_self _).trans + (toPoly_gcdMonic_dvd_left g' h) + have hafterZeroDvd : afterZero.toPoly ∣ g'.toPoly := by + dsimp [afterZero] + exact quotientAfterChild_toPoly_dvd_parent hdivZero + have hdivSquare : squarePart.toPoly ∣ afterZero.toPoly := by + dsimp [squarePart] + exact (toPoly_monicNormalize_dvd_self _).trans + (toPoly_gcdMonic_dvd_left afterZero (s - (1 : CPolynomial F))) + have hafterSquareDvd : afterSquare.toPoly ∣ g'.toPoly := by + dsimp [afterSquare] + exact (quotientAfterChild_toPoly_dvd_parent hdivSquare).trans hafterZeroDvd + have hraw := mem_of_mem_eraseDups (by simpa using hmem) + unfold nontrivialProperChildren at hraw + simp at hraw + rcases hraw with ⟨hrawMem, _hproper⟩ + have hchildDvdNorm : child.toPoly ∣ g'.toPoly := by + rcases hrawMem with hchild | hchild | hchild + · subst child + exact hdivZero + · subst child + exact hdivSquare.trans hafterZeroDvd + · subst child + exact hafterSquareDvd + exact hchildDvdNorm.trans (by simpa [g'] using toPoly_monicNormalize_dvd_self g) + next _hsize => + simp at htry + +theorem cantorZassenhausOddAttemptWith_child_normSplitWork_pos {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + (q : Nat) (probes : ProbeFamily F) {g child : CPolynomial F} + {attempt : Nat} {children : Array (CPolynomial F)} + (htry : cantorZassenhausOddAttemptWith M D q probes g attempt = some children) + (hg : g ≠ 0) (hmem : child ∈ children.toList) : + 1 ≤ normSplitWork child := by + unfold cantorZassenhausOddAttemptWith at htry + simp only at htry + split at htry + next _hsize => + injection htry with hchildren + subst children + let g' := CPolynomial.monicNormalize g + let h := reduceModWith D g' (probes.probe q g' attempt) + let s := powModWith M D g' h ((q - 1) / 2) + let zeroPart := CPolynomial.monicNormalize (CPolynomial.gcdMonic g' h) + let afterZero := quotientAfterChild g' zeroPart + let squarePart := CPolynomial.monicNormalize + (CPolynomial.gcdMonic afterZero (s - (1 : CPolynomial F))) + let afterSquare := quotientAfterChild afterZero squarePart + have hg' : g' ≠ 0 := monicNormalize_ne_zero_of_ne_zero hg + have hdivZero : zeroPart.toPoly ∣ g'.toPoly := by + dsimp [zeroPart] + exact (toPoly_monicNormalize_dvd_self _).trans + (toPoly_gcdMonic_dvd_left g' h) + have hafterZeroNe : afterZero ≠ 0 := by + dsimp [afterZero] + exact quotientAfterChild_ne_zero_of_dvd hdivZero hg' + have hg'Monic : g'.toPoly.Monic := by + dsimp [g'] + exact monicNormalize_toPoly_monic_of_ne_zero hg + have hafterZeroMonic : afterZero.toPoly.Monic := by + dsimp [afterZero] + exact quotientAfterChild_toPoly_monic_of_dvd hg'Monic hdivZero hg' + have hdivSquare : squarePart.toPoly ∣ afterZero.toPoly := by + dsimp [squarePart] + exact (toPoly_monicNormalize_dvd_self _).trans + (toPoly_gcdMonic_dvd_left afterZero (s - (1 : CPolynomial F))) + have hraw := mem_of_mem_eraseDups (by simpa using hmem) + unfold nontrivialProperChildren at hraw + simp at hraw + rcases hraw with ⟨hrawMem, hproper⟩ + rcases hrawMem with hchild | hchild | hchild + · subst child + have hzeroNe : zeroPart ≠ 0 := by + unfold isNontrivialProperChild at hproper + simp at hproper + exact hproper.1.1 + have hzeroNotOne : zeroPart ≠ 1 := by + unfold isNontrivialProperChild at hproper + simp at hproper + exact hproper.1.2 + have hzeroMonic : zeroPart.toPoly.Monic := by + dsimp [zeroPart] + exact monicNormalize_toPoly_monic_of_ne_zero + (gcdMonic_ne_zero_of_left hg') + exact normSplitWork_pos_of_monic_ne_zero_ne_one + hzeroMonic hzeroNe hzeroNotOne + · subst child + have hsquareNe : squarePart ≠ 0 := by + unfold isNontrivialProperChild at hproper + simp at hproper + exact hproper.1.1 + have hsquareNotOne : squarePart ≠ 1 := by + unfold isNontrivialProperChild at hproper + simp at hproper + exact hproper.1.2 + have hsquareMonic : squarePart.toPoly.Monic := by + dsimp [squarePart] + exact monicNormalize_toPoly_monic_of_ne_zero + (gcdMonic_ne_zero_of_left hafterZeroNe) + exact normSplitWork_pos_of_monic_ne_zero_ne_one + hsquareMonic hsquareNe hsquareNotOne + · subst child + have hafterNe : afterSquare ≠ 0 := by + unfold isNontrivialProperChild at hproper + simp at hproper + exact hproper.1.1 + have hafterNotOne : afterSquare ≠ 1 := by + unfold isNontrivialProperChild at hproper + simp at hproper + exact hproper.1.2 + have hafterMonic : afterSquare.toPoly.Monic := by + dsimp [afterSquare] + exact quotientAfterChild_toPoly_monic_of_dvd + hafterZeroMonic hdivSquare hafterZeroNe + exact normSplitWork_pos_of_monic_ne_zero_ne_one + hafterMonic hafterNe hafterNotOne + next _hsize => + simp at htry + +set_option maxHeartbeats 800000 in +theorem cantorZassenhausOddAttemptWith_stackWork_le {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + (q : Nat) (probes : ProbeFamily F) {g : CPolynomial F} + {attempt : Nat} {children : Array (CPolynomial F)} + (htry : cantorZassenhausOddAttemptWith M D q probes g attempt = some children) + (hg : g ≠ 0) : + stackWork children.toList ≤ splitWork (CPolynomial.monicNormalize g) - 1 := by + unfold cantorZassenhausOddAttemptWith at htry + simp only at htry + split at htry + next hsize => + injection htry with hchildren + subst children + let g' := CPolynomial.monicNormalize g + let h := reduceModWith D g' (probes.probe q g' attempt) + let s := powModWith M D g' h ((q - 1) / 2) + let zeroPart := CPolynomial.monicNormalize (CPolynomial.gcdMonic g' h) + let afterZero := quotientAfterChild g' zeroPart + let squarePart := CPolynomial.monicNormalize + (CPolynomial.gcdMonic afterZero (s - (1 : CPolynomial F))) + let afterSquare := quotientAfterChild afterZero squarePart + have hg' : g' ≠ 0 := monicNormalize_ne_zero_of_ne_zero hg + have hsize' : + 2 ≤ (nontrivialProperChildren g' #[zeroPart, squarePart, afterSquare]).eraseDups.size := by + simpa [g', h, s, zeroPart, afterZero, squarePart, afterSquare] using hsize + have hdivZero : zeroPart.toPoly ∣ g'.toPoly := by + dsimp [zeroPart] + exact (toPoly_monicNormalize_dvd_self _).trans + (toPoly_gcdMonic_dvd_left g' h) + have hg'Monic : g'.toPoly.Monic := by + dsimp [g'] + exact monicNormalize_toPoly_monic_of_ne_zero hg + have hafterZeroNe : afterZero ≠ 0 := by + dsimp [afterZero] + exact quotientAfterChild_ne_zero_of_dvd hdivZero hg' + have hafterZeroMonic : afterZero.toPoly.Monic := by + dsimp [afterZero] + exact quotientAfterChild_toPoly_monic_of_dvd hg'Monic hdivZero hg' + have hdivSquare : squarePart.toPoly ∣ afterZero.toPoly := by + dsimp [squarePart] + exact (toPoly_monicNormalize_dvd_self _).trans + (toPoly_gcdMonic_dvd_left afterZero (s - (1 : CPolynomial F))) + by_cases hproperZero : isNontrivialProperChild g' zeroPart = true + · have hzeroPos : 0 < zeroPart.toPoly.natDegree := by + have hproperZero' : + isNontrivialProperChild g' + (CPolynomial.monicNormalize (CPolynomial.gcdMonic g' h)) = true := by + simpa [zeroPart] using hproperZero + simpa [zeroPart] using + (monicNormalize_toPoly_natDegree_pos_of_proper hproperZero') + have hzeroAfterSum : + zeroPart.toPoly.natDegree + afterZero.toPoly.natDegree ≤ + g'.toPoly.natDegree := by + dsimp [afterZero] + exact child_quotient_natDegree_le_parent hproperZero hdivZero hg' + by_cases hproperSquareAfter : isNontrivialProperChild afterZero squarePart = true + · have hafterZeroSizeLe : afterZero.val.size ≤ g'.val.size := by + dsimp [afterZero] + exact quotientAfterChild_size_le_parent hg' + have hproperSquareG : isNontrivialProperChild g' squarePart = true := + proper_child_of_proper_intermediate hproperSquareAfter hafterZeroSizeLe + have hsquarePos : 0 < squarePart.toPoly.natDegree := by + have hproperSquare' : + isNontrivialProperChild afterZero + (CPolynomial.monicNormalize + (CPolynomial.gcdMonic afterZero (s - (1 : CPolynomial F)))) = true := by + simpa [squarePart] using hproperSquareAfter + simpa [squarePart] using + (monicNormalize_toPoly_natDegree_pos_of_proper hproperSquare') + have hsquareAfterSum : + squarePart.toPoly.natDegree + afterSquare.toPoly.natDegree ≤ + afterZero.toPoly.natDegree := by + dsimp [afterSquare] + exact child_quotient_natDegree_le_parent hproperSquareAfter hdivSquare hafterZeroNe + have hsum : + zeroPart.toPoly.natDegree + squarePart.toPoly.natDegree + + afterSquare.toPoly.natDegree ≤ g'.toPoly.natDegree := by + omega + have hraw : + stackWork + ((nontrivialProperChildren g' + #[zeroPart, squarePart, afterSquare]).eraseDups).toList ≤ + splitWork zeroPart + (splitWork squarePart + (splitWork afterSquare + 0)) := by + have herase := + stackWork_eraseDups_le + (nontrivialProperChildren g' #[zeroPart, squarePart, afterSquare]) + by_cases hproperAfterG : isNontrivialProperChild g' afterSquare = true + · unfold nontrivialProperChildren at herase ⊢ + simp [hproperZero, hproperSquareG, hproperAfterG, stackWork] at herase ⊢ + omega + · unfold nontrivialProperChildren at herase ⊢ + simp [hproperZero, hproperSquareG, hproperAfterG, stackWork] at herase ⊢ + omega + have hwork : + splitWork zeroPart + (splitWork squarePart + (splitWork afterSquare + 0)) ≤ + splitWork g' - 1 := + splitWork_triple_le_of_first_two_pos_natDegree_sum_le hzeroPos hsquarePos hsum + exact le_trans hraw (by simpa [g'] using hwork) + · have hafterEq : afterSquare = afterZero := by + dsimp [afterSquare, quotientAfterChild] + rw [if_neg hproperSquareAfter] + by_cases hproperSquareG : isNontrivialProperChild g' squarePart = true + · have hsquareNe : squarePart ≠ 0 := by + unfold isNontrivialProperChild at hproperSquareG + simp at hproperSquareG + exact hproperSquareG.1.1 + have hsquareNotOne : squarePart ≠ 1 := by + unfold isNontrivialProperChild at hproperSquareG + simp at hproperSquareG + exact hproperSquareG.1.2 + have hsquareMonic : squarePart.toPoly.Monic := by + dsimp [squarePart] + exact monicNormalize_toPoly_monic_of_ne_zero + (gcdMonic_ne_zero_of_left hafterZeroNe) + have hsqEq : afterZero = squarePart := + eq_of_not_proper_of_monic_dvd hsquareMonic hafterZeroMonic hdivSquare + hsquareNe hafterZeroNe hsquareNotOne hproperSquareAfter + have hafterZeroPos : 0 < afterZero.toPoly.natDegree := by + rw [hsqEq] + exact toPoly_natDegree_pos_of_monic_ne_zero_ne_one + hsquareMonic hsquareNe hsquareNotOne + have hpair : + splitWork zeroPart + splitWork afterZero ≤ splitWork g' - 1 := + splitWork_pair_le_of_natDegree_sum_le hzeroPos hafterZeroPos hzeroAfterSum + have hraw : + stackWork + ((nontrivialProperChildren g' + #[zeroPart, squarePart, afterSquare]).eraseDups).toList ≤ + splitWork zeroPart + splitWork afterZero := by + rw [hafterEq, hsqEq] + unfold nontrivialProperChildren + simp [hproperZero, hproperSquareG] + exact stackWork_eraseDups_triple_dup_right_le zeroPart squarePart + exact le_trans hraw (by simpa [g'] using hpair) + · have hproperAfterG : + isNontrivialProperChild g' afterSquare = true := + right_proper_of_triple_eraseDups_filter_size_ge_two_of_not_middle + hproperSquareG hsize' + have hafterZeroProper : isNontrivialProperChild g' afterZero = true := by + simpa [hafterEq] using hproperAfterG + have hafterZeroPos : 0 < afterZero.toPoly.natDegree := by + have hafterZeroNe' : afterZero ≠ 0 := by + unfold isNontrivialProperChild at hafterZeroProper + simp at hafterZeroProper + exact hafterZeroProper.1.1 + have hafterZeroNotOne : afterZero ≠ 1 := by + unfold isNontrivialProperChild at hafterZeroProper + simp at hafterZeroProper + exact hafterZeroProper.1.2 + exact toPoly_natDegree_pos_of_monic_ne_zero_ne_one + hafterZeroMonic hafterZeroNe' hafterZeroNotOne + have hpair : + splitWork zeroPart + splitWork afterZero ≤ splitWork g' - 1 := + splitWork_pair_le_of_natDegree_sum_le hzeroPos hafterZeroPos hzeroAfterSum + have hraw : + stackWork + ((nontrivialProperChildren g' + #[zeroPart, squarePart, afterSquare]).eraseDups).toList ≤ + splitWork zeroPart + splitWork afterZero := by + rw [hafterEq] + unfold nontrivialProperChildren + simpa [hproperZero, hproperSquareG, hafterZeroProper, stackWork] using + (stackWork_eraseDups_le (#[zeroPart, afterZero] : Array (CPolynomial F))) + exact le_trans hraw (by simpa [g'] using hpair) + · have hafterZeroEq : afterZero = g' := by + dsimp [afterZero, quotientAfterChild] + rw [if_neg hproperZero] + have hproperSquareG : + isNontrivialProperChild g' squarePart = true := + middle_proper_of_triple_eraseDups_filter_size_ge_two_of_not_left + hproperZero hsize' + have hproperAfterG : + isNontrivialProperChild g' afterSquare = true := + right_proper_of_triple_eraseDups_filter_size_ge_two_of_not_left + hproperZero hsize' + have hproperSquareAfter : isNontrivialProperChild afterZero squarePart = true := by + simpa [hafterZeroEq] using hproperSquareG + have hsquarePos : 0 < squarePart.toPoly.natDegree := by + have hproperSquare' : + isNontrivialProperChild g' + (CPolynomial.monicNormalize + (CPolynomial.gcdMonic afterZero (s - (1 : CPolynomial F)))) = true := by + simpa [squarePart] using hproperSquareG + simpa [squarePart] using + (monicNormalize_toPoly_natDegree_pos_of_proper hproperSquare') + have hafterMonic : afterSquare.toPoly.Monic := by + dsimp [afterSquare] + exact quotientAfterChild_toPoly_monic_of_dvd + (by simpa [hafterZeroEq] using hg'Monic) hdivSquare + (by simpa [hafterZeroEq] using hg') + have hafterPos : 0 < afterSquare.toPoly.natDegree := by + have hafterNe : afterSquare ≠ 0 := by + unfold isNontrivialProperChild at hproperAfterG + simp at hproperAfterG + exact hproperAfterG.1.1 + have hafterNotOne : afterSquare ≠ 1 := by + unfold isNontrivialProperChild at hproperAfterG + simp at hproperAfterG + exact hproperAfterG.1.2 + exact toPoly_natDegree_pos_of_monic_ne_zero_ne_one + hafterMonic hafterNe hafterNotOne + have hsquareAfterSum : + squarePart.toPoly.natDegree + afterSquare.toPoly.natDegree ≤ + g'.toPoly.natDegree := by + have hsum := child_quotient_natDegree_le_parent hproperSquareAfter hdivSquare + (by simpa [hafterZeroEq] using hg') + simpa [hafterZeroEq, afterSquare] using hsum + have hpair : + splitWork squarePart + splitWork afterSquare ≤ splitWork g' - 1 := + splitWork_pair_le_of_natDegree_sum_le hsquarePos hafterPos hsquareAfterSum + have hraw : + stackWork + ((nontrivialProperChildren g' + #[zeroPart, squarePart, afterSquare]).eraseDups).toList ≤ + splitWork squarePart + splitWork afterSquare := by + have herase := + stackWork_eraseDups_le + (nontrivialProperChildren g' #[zeroPart, squarePart, afterSquare]) + unfold nontrivialProperChildren at herase ⊢ + simp [hproperZero, hproperSquareG, hproperAfterG, stackWork] at herase ⊢ + omega + exact le_trans hraw (by simpa [g'] using hpair) + next _hsize => + simp at htry + +set_option maxHeartbeats 1600000 in +/-- The odd split attempt depends on the probe family only through the probe +it actually draws. -/ +theorem cantorZassenhausOddAttemptWith_probe_congr {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + (q : Nat) {p1 p2 : ProbeFamily F} {g : CPolynomial F} {a1 a2 : Nat} + (hprobe : p1.probe q (CPolynomial.monicNormalize g) a1 = + p2.probe q (CPolynomial.monicNormalize g) a2) : + cantorZassenhausOddAttemptWith M D q p1 g a1 = + cantorZassenhausOddAttemptWith M D q p2 g a2 := by + simp only [cantorZassenhausOddAttemptWith] + rw [hprobe] + +set_option maxHeartbeats 1600000 in +/-- The odd retry loop depends on the probe family only through the probes it +actually draws. -/ +theorem tryOddSplitAttemptsWith_probe_congr {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + (q : Nat) {p1 p2 : ProbeFamily F} {g : CPolynomial F} : + ∀ (attempts offset1 offset2 : Nat), + (∀ i, i < attempts → + p1.probe q (CPolynomial.monicNormalize g) (offset1 + i) = + p2.probe q (CPolynomial.monicNormalize g) (offset2 + i)) → + tryOddSplitAttemptsWith M D q p1 g attempts offset1 = + tryOddSplitAttemptsWith M D q p2 g attempts offset2 := by + intro attempts + induction attempts with + | zero => + intro o1 o2 _hagree + unfold tryOddSplitAttemptsWith + rfl + | succ attempts ih => + intro o1 o2 hagree + unfold tryOddSplitAttemptsWith + rw [cantorZassenhausOddAttemptWith_probe_congr M D q (g := g) + (by simpa using hagree 0 (by omega))] + cases htry : cantorZassenhausOddAttemptWith M D q p2 g o2 with + | some c => simp + | none => + simp only + exact ih (o1 + 1) (o2 + 1) (by + intro i hi + have h := hagree (i + 1) (by omega) + simpa [Nat.add_assoc, Nat.add_comm, Nat.add_left_comm] using h) + +/-- A successful retry loop result is produced by some single attempt. -/ +theorem tryOddSplitAttemptsWith_eq_some_exists_attempt {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + (q : Nat) (probes : ProbeFamily F) {g : CPolynomial F} : + ∀ (attempts offset : Nat) {children : Array (CPolynomial F)}, + tryOddSplitAttemptsWith M D q probes g attempts offset = some children → + ∃ attempt, cantorZassenhausOddAttemptWith M D q probes g attempt = some children := by + intro attempts + induction attempts with + | zero => + intro offset children htry + cases htry + | succ attempts ih => + intro offset children htry + unfold tryOddSplitAttemptsWith at htry + cases hsplit : cantorZassenhausOddAttemptWith M D q probes g offset with + | none => + simp [hsplit] at htry + exact ih (offset + 1) htry + | some splitChildren => + simp [hsplit] at htry + subst children + exact ⟨offset, hsplit⟩ + +theorem tryOddSplitAttemptsWith_root {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + (q : Nat) (probes : ProbeFamily F) {g : CPolynomial F} {a : F} + (hg : g ≠ 0) (hroot : CPolynomial.eval a g = 0) : + ∀ attempts offset {children : Array (CPolynomial F)}, + tryOddSplitAttemptsWith M D q probes g attempts offset = some children → + ∃ child, child ∈ children.toList ∧ CPolynomial.eval a child = 0 := by + intro attempts + induction attempts using Nat.rec with + | zero => + intro offset children htry + cases htry + | succ attempts ih => + intro offset children htry + unfold tryOddSplitAttemptsWith at htry + cases hsplit : cantorZassenhausOddAttemptWith M D q probes g offset with + | none => + simp [hsplit] at htry + exact ih (offset + 1) htry + | some splitChildren => + simp [hsplit] at htry + subst children + exact cantorZassenhausOddAttemptWith_root M D q probes hsplit hg hroot + +private theorem tryOddSplitAttemptsWith_child_proper {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + (q : Nat) (probes : ProbeFamily F) {g child : CPolynomial F} : + ∀ attempts offset {children : Array (CPolynomial F)}, + tryOddSplitAttemptsWith M D q probes g attempts offset = some children → + child ∈ children.toList → + isNontrivialProperChild (CPolynomial.monicNormalize g) child = true := by + intro attempts + induction attempts using Nat.rec with + | zero => + intro offset children htry hmem + cases htry + | succ attempts ih => + intro offset children htry hmem + unfold tryOddSplitAttemptsWith at htry + cases hsplit : cantorZassenhausOddAttemptWith M D q probes g offset with + | none => + simp [hsplit] at htry + exact ih (offset + 1) htry hmem + | some splitChildren => + simp [hsplit] at htry + subst children + exact cantorZassenhausOddAttemptWith_child_proper M D q probes hsplit hmem + +theorem tryOddSplitAttemptsWith_child_normSplitWork_pos {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + (q : Nat) (probes : ProbeFamily F) {g child : CPolynomial F} + (hg : g ≠ 0) : + ∀ attempts offset {children : Array (CPolynomial F)}, + tryOddSplitAttemptsWith M D q probes g attempts offset = some children → + child ∈ children.toList → + 1 ≤ normSplitWork child := by + intro attempts + induction attempts using Nat.rec with + | zero => + intro offset children htry hmem + cases htry + | succ attempts ih => + intro offset children htry hmem + unfold tryOddSplitAttemptsWith at htry + cases hsplit : cantorZassenhausOddAttemptWith M D q probes g offset with + | none => + simp [hsplit] at htry + exact ih (offset + 1) htry hmem + | some splitChildren => + simp [hsplit] at htry + subst children + exact cantorZassenhausOddAttemptWith_child_normSplitWork_pos + M D q probes hsplit hg hmem + +theorem tryOddSplitAttemptsWith_stackWork_le {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + (q : Nat) (probes : ProbeFamily F) {g : CPolynomial F} + (hg : g ≠ 0) : + ∀ attempts offset {children : Array (CPolynomial F)}, + tryOddSplitAttemptsWith M D q probes g attempts offset = some children → + stackWork children.toList ≤ splitWork (CPolynomial.monicNormalize g) - 1 := by + intro attempts + induction attempts using Nat.rec with + | zero => + intro offset children htry + cases htry + | succ attempts ih => + intro offset children htry + unfold tryOddSplitAttemptsWith at htry + cases hsplit : cantorZassenhausOddAttemptWith M D q probes g offset with + | none => + simp [hsplit] at htry + exact ih (offset + 1) htry + | some splitChildren => + simp [hsplit] at htry + subst children + exact cantorZassenhausOddAttemptWith_stackWork_le M D q probes hsplit hg + +private theorem two_le_length_of_mem_of_mem_of_ne {α : Type*} {l : List α} {u v : α} + (hu : u ∈ l) (hv : v ∈ l) (huv : u ≠ v) : + 2 ≤ l.length := by + rcases l with _ | ⟨w, _ | ⟨w', t⟩⟩ + · cases hu + · rw [List.mem_singleton] at hu hv + exact absurd (hu.trans hv.symm) huv + · simp only [List.length_cons] + omega + +private theorem two_le_size_eraseDups_of_mem_of_mem_of_ne {α : Type*} [BEq α] [LawfulBEq α] + {xs : Array α} {u v : α} + (hu : u ∈ xs) (hv : v ∈ xs) (huv : u ≠ v) : + 2 ≤ xs.eraseDups.size := by + have hu' : u ∈ xs.eraseDups.toList := by simpa using mem_eraseDups_of_mem hu + have hv' : v ∈ xs.eraseDups.toList := by simpa using mem_eraseDups_of_mem hv + simpa using two_le_length_of_mem_of_mem_of_ne hu' hv' huv + +/-- +Core success argument: if the probe value vanishes at the root `x` but not at +the root `y`, or the probe values at `x` and `y` are nonzero with Euler powers +one and not-one respectively, the split candidates separate `x` from `y` and +the attempt succeeds. +-/ +private theorem cantorZassenhausOddAttemptWith_success_aux {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + (q : Nat) {g h : CPolynomial F} {x y : F} (attempt : Nat) + (hg : g ≠ 0) + (hrootX : CPolynomial.eval x g = 0) + (hrootY : CPolynomial.eval y g = 0) + (hY0 : CPolynomial.eval y h ≠ 0) + (hcase : + CPolynomial.eval x h = 0 ∨ + (CPolynomial.eval x h ≠ 0 ∧ CPolynomial.eval x h ^ ((q - 1) / 2) = 1 ∧ + CPolynomial.eval y h ^ ((q - 1) / 2) ≠ 1)) : + ∃ children, + cantorZassenhausOddAttemptWith M D q + ({ probe := fun _q _factor _attempt ↦ h } : ProbeFamily F) + g attempt = some children := by + cases htry : cantorZassenhausOddAttemptWith M D q + ({ probe := fun _q _factor _attempt ↦ h } : ProbeFamily F) g attempt with + | some children => exact ⟨children, rfl⟩ + | none => + exfalso + unfold cantorZassenhausOddAttemptWith at htry + simp only at htry + split at htry + next hsize => simp at htry + next hsize => + apply hsize + let g' := CPolynomial.monicNormalize g + let h' := reduceModWith D g' h + let s := powModWith M D g' h' ((q - 1) / 2) + let zeroPart := CPolynomial.monicNormalize (CPolynomial.gcdMonic g' h') + let afterZero := quotientAfterChild g' zeroPart + let squarePart := CPolynomial.monicNormalize + (CPolynomial.gcdMonic afterZero (s - (1 : CPolynomial F))) + let afterSquare := quotientAfterChild afterZero squarePart + have hg' : g' ≠ 0 := monicNormalize_ne_zero_of_ne_zero hg + have hgMonic : g'.toPoly.Monic := monicNormalize_toPoly_monic_of_ne_zero hg + have hX' : CPolynomial.eval x g' = 0 := (monicNormalize_root_iff hg).2 hrootX + have hY' : CPolynomial.eval y g' = 0 := (monicNormalize_root_iff hg).2 hrootY + have hhX : CPolynomial.eval x h' = CPolynomial.eval x h := + eval_reduceModWith_eq_self_of_root D hX' + have hhY : CPolynomial.eval y h' = CPolynomial.eval y h := + eval_reduceModWith_eq_self_of_root D hY' + have hsY : CPolynomial.eval y (s - 1) = + CPolynomial.eval y h ^ ((q - 1) / 2) - 1 := by + rw [CPolynomial.eval_sub, CPolynomial.eval_one, + eval_powModWith_eq_pow M D hY' ((q - 1) / 2), hhY] + have hzeroIff : ∀ z : F, CPolynomial.eval z zeroPart = 0 ↔ + CPolynomial.eval z g' = 0 ∧ CPolynomial.eval z h' = 0 := + fun z ↦ eval_monicNormalize_gcdMonic_eq_zero_iff g' h' z + have hdivZero : zeroPart.toPoly ∣ g'.toPoly := + (toPoly_monicNormalize_dvd_self _).trans (toPoly_gcdMonic_dvd_left g' h') + have hzeroNe : zeroPart ≠ 0 := + monicNormalize_ne_zero_of_ne_zero (gcdMonic_ne_zero_left h' hg') + have hzeroMonic : zeroPart.toPoly.Monic := + monicNormalize_toPoly_monic_of_ne_zero (gcdMonic_ne_zero_left h' hg') + have hzeroYne : CPolynomial.eval y zeroPart ≠ 0 := by + intro hzy + apply hY0 + rw [← hhY] + exact ((hzeroIff y).1 hzy).2 + have hafterZeroY : CPolynomial.eval y afterZero = 0 := + quotientAfterChild_root_of_not_child_root hdivZero hY' hzeroYne + have hafterZeroNe : afterZero ≠ 0 := + quotientAfterChild_ne_zero_of_dvd hdivZero hg' + have hafterZeroDvd : afterZero.toPoly ∣ g'.toPoly := + quotientAfterChild_toPoly_dvd_parent hdivZero + have hafterZeroMonic : afterZero.toPoly.Monic := + quotientAfterChild_toPoly_monic_of_dvd hgMonic hdivZero hg' + have hafterZeroSizeLe : afterZero.val.size ≤ g'.val.size := + quotientAfterChild_size_le_parent hg' + have hsquareIff : ∀ z : F, CPolynomial.eval z squarePart = 0 ↔ + CPolynomial.eval z afterZero = 0 ∧ + CPolynomial.eval z (s - (1 : CPolynomial F)) = 0 := + fun z ↦ eval_monicNormalize_gcdMonic_eq_zero_iff afterZero + (s - (1 : CPolynomial F)) z + have hdivSquare : squarePart.toPoly ∣ afterZero.toPoly := + (toPoly_monicNormalize_dvd_self _).trans + (toPoly_gcdMonic_dvd_left afterZero (s - (1 : CPolynomial F))) + have hsquareNe : squarePart ≠ 0 := + monicNormalize_ne_zero_of_ne_zero + (gcdMonic_ne_zero_left (s - (1 : CPolynomial F)) hafterZeroNe) + have hsquareMonic : squarePart.toPoly.Monic := + monicNormalize_toPoly_monic_of_ne_zero + (gcdMonic_ne_zero_left (s - (1 : CPolynomial F)) hafterZeroNe) + have hafterSquareNe : afterSquare ≠ 0 := + quotientAfterChild_ne_zero_of_dvd hdivSquare hafterZeroNe + have hfinal : 2 ≤ + ((nontrivialProperChildren g' + #[zeroPart, squarePart, afterSquare]).eraseDups).size := by + rcases hcase with hX0 | ⟨hX0, hXm, hYm⟩ + · -- `x` is a root of the zero part, `y` survives into the later parts. + have hzeroX : CPolynomial.eval x zeroPart = 0 := + (hzeroIff x).2 ⟨hX', by rw [hhX]; exact hX0⟩ + have hzeroSizeLt : zeroPart.val.size < g'.val.size := by + by_contra hnot + apply hzeroYne + have heq : g' = zeroPart := + eq_of_monic_dvd_of_val_size_le hzeroMonic hgMonic hdivZero hzeroNe hg' + (Nat.le_of_not_lt hnot) + rw [← heq] + exact hY' + have hproperZero : isNontrivialProperChild g' zeroPart = true := + proper_child_of_ne_zero_root_size_lt hzeroNe hzeroX hzeroSizeLt + have hafterZeroSizeLt : afterZero.val.size < g'.val.size := + quotientAfterChild_size_lt_parent_of_monicNormalize_proper hg' hproperZero + by_cases hsqY : CPolynomial.eval y squarePart = 0 + · have hsquareSizeLt : squarePart.val.size < g'.val.size := by + have hle : squarePart.val.size ≤ afterZero.val.size := + val_size_le_of_toPoly_natDegree_le hafterZeroNe + (Polynomial.natDegree_le_of_dvd hdivSquare + ((CPolynomial.toPoly_eq_zero_iff afterZero).not.mpr hafterZeroNe)) + omega + have hproperSquare : isNontrivialProperChild g' squarePart = true := + proper_child_of_ne_zero_root_size_lt hsquareNe hsqY hsquareSizeLt + have hne : zeroPart ≠ squarePart := by + intro heq + apply hzeroYne + rw [heq] + exact hsqY + refine two_le_size_eraseDups_of_mem_of_mem_of_ne ?_ ?_ hne + · have hmem := nontrivialProperChildren_mem_of_mem + (parent := g') (children := #[zeroPart, squarePart, afterSquare]) + (by simp) hproperZero + simpa using hmem + · have hmem := nontrivialProperChildren_mem_of_mem + (parent := g') (children := #[zeroPart, squarePart, afterSquare]) + (by simp) hproperSquare + simpa using hmem + · have hafterSquareY : CPolynomial.eval y afterSquare = 0 := + quotientAfterChild_root_of_not_child_root hdivSquare hafterZeroY hsqY + have hafterSquareSizeLt : afterSquare.val.size < g'.val.size := by + have hle : afterSquare.val.size ≤ afterZero.val.size := + quotientAfterChild_size_le_parent hafterZeroNe + omega + have hproperAfter : isNontrivialProperChild g' afterSquare = true := + proper_child_of_ne_zero_root_size_lt hafterSquareNe hafterSquareY + hafterSquareSizeLt + have hne : zeroPart ≠ afterSquare := by + intro heq + apply hzeroYne + rw [heq] + exact hafterSquareY + refine two_le_size_eraseDups_of_mem_of_mem_of_ne ?_ ?_ hne + · have hmem := nontrivialProperChildren_mem_of_mem + (parent := g') (children := #[zeroPart, squarePart, afterSquare]) + (by simp) hproperZero + simpa using hmem + · have hmem := nontrivialProperChildren_mem_of_mem + (parent := g') (children := #[zeroPart, squarePart, afterSquare]) + (by simp) hproperAfter + simpa using hmem + · -- `x` lands in the square part, `y` in the remaining quotient. + have hzeroXne : CPolynomial.eval x zeroPart ≠ 0 := by + intro hzx + apply hX0 + rw [← hhX] + exact ((hzeroIff x).1 hzx).2 + have hafterZeroX : CPolynomial.eval x afterZero = 0 := + quotientAfterChild_root_of_not_child_root hdivZero hX' hzeroXne + have hsX : CPolynomial.eval x (s - 1) = 0 := by + rw [CPolynomial.eval_sub, CPolynomial.eval_one, + eval_powModWith_eq_pow M D hX' ((q - 1) / 2), hhX, hXm, sub_self] + have hsquareX : CPolynomial.eval x squarePart = 0 := + (hsquareIff x).2 ⟨hafterZeroX, hsX⟩ + have hsquareYne : CPolynomial.eval y squarePart ≠ 0 := by + intro hsy + apply hYm + have hy1 := ((hsquareIff y).1 hsy).2 + rw [hsY] at hy1 + exact sub_eq_zero.mp hy1 + have hsquareSizeLt : squarePart.val.size < g'.val.size := by + by_contra hnot + apply hsquareYne + have heq : g' = squarePart := + eq_of_monic_dvd_of_val_size_le hsquareMonic hgMonic + (hdivSquare.trans hafterZeroDvd) hsquareNe hg' (Nat.le_of_not_lt hnot) + rw [← heq] + exact hY' + have hproperSquare : isNontrivialProperChild g' squarePart = true := + proper_child_of_ne_zero_root_size_lt hsquareNe hsquareX hsquareSizeLt + have hafterSquareY : CPolynomial.eval y afterSquare = 0 := + quotientAfterChild_root_of_not_child_root hdivSquare hafterZeroY hsquareYne + have hproperSqAfterZero : isNontrivialProperChild afterZero squarePart = true := by + by_contra hnot + apply hsquareYne + have heq : afterZero = squarePart := + eq_of_not_proper_of_monic_dvd hsquareMonic hafterZeroMonic hdivSquare + hsquareNe hafterZeroNe (child_ne_one_of_root hsquareX) hnot + rw [← heq] + exact hafterZeroY + have hafterSquareSizeLt : afterSquare.val.size < g'.val.size := by + have hlt : afterSquare.val.size < afterZero.val.size := + quotientAfterChild_size_lt_parent_of_monicNormalize_proper hafterZeroNe + hproperSqAfterZero + omega + have hproperAfter : isNontrivialProperChild g' afterSquare = true := + proper_child_of_ne_zero_root_size_lt hafterSquareNe hafterSquareY + hafterSquareSizeLt + have hne : squarePart ≠ afterSquare := by + intro heq + apply hsquareYne + rw [heq] + exact hafterSquareY + refine two_le_size_eraseDups_of_mem_of_mem_of_ne ?_ ?_ hne + · have hmem := nontrivialProperChildren_mem_of_mem + (parent := g') (children := #[zeroPart, squarePart, afterSquare]) + (by simp) hproperSquare + simpa using hmem + · have hmem := nontrivialProperChildren_mem_of_mem + (parent := g') (children := #[zeroPart, squarePart, afterSquare]) + (by simp) hproperAfter + simpa using hmem + simpa [g', h', s, zeroPart, afterZero, squarePart, afterSquare] using hfinal + +/-- +A fixed probe whose Euler buckets separate two distinct roots of `g` forces the +odd Cantor-Zassenhaus split attempt to succeed. + +The split candidates of `cantorZassenhausOddAttemptWith` are the zero part +`gcd(g, h)`, the square part `gcd(g / zeroPart, h ^ ((q - 1) / 2) - 1)`, and +the remaining quotient. Roots in the `zero`, `square`, and `nonsquare` Euler +buckets of the probe land in these three candidates respectively, so two roots +in different buckets witness two distinct nontrivial proper children. + +The hypothesis `hg : g ≠ 0` is essential: for `g = 0` both root hypotheses hold +vacuously while the attempt always fails, because no candidate child can have +representation size strictly below the parent size `0`. No distinctness +hypothesis on `a` and `b` is needed: bucket separation already forces `a ≠ b`. +-/ +theorem cantorZassenhausOddAttemptWith_success_of_bucket_separated {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + (q : Nat) {g h : CPolynomial F} {a b : F} (attempt : Nat) + (hg : g ≠ 0) + (hrootA : CPolynomial.eval a g = 0) + (hrootB : CPolynomial.eval b g = 0) + (hsep : + oddCZBucket q (CPolynomial.eval a h) ≠ + oddCZBucket q (CPolynomial.eval b h)) : + ∃ children, + cantorZassenhausOddAttemptWith M D q + ({ probe := fun _q _factor _attempt ↦ h } : ProbeFamily F) + g attempt = some children := by + cases hbA : oddCZBucket q (CPolynomial.eval a h) with + | zero => + have hA := (oddCZBucket_eq_zero_iff q (CPolynomial.eval a h)).1 hbA + cases hbB : oddCZBucket q (CPolynomial.eval b h) with + | zero => exact absurd (hbA.trans hbB.symm) hsep + | square => + have hB := (oddCZBucket_eq_square_iff q (CPolynomial.eval b h)).1 hbB + exact cantorZassenhausOddAttemptWith_success_aux M D q attempt hg hrootA hrootB + hB.1 (Or.inl hA) + | nonsquare => + have hB := (oddCZBucket_eq_nonsquare_iff q (CPolynomial.eval b h)).1 hbB + exact cantorZassenhausOddAttemptWith_success_aux M D q attempt hg hrootA hrootB + hB.1 (Or.inl hA) + | square => + have hA := (oddCZBucket_eq_square_iff q (CPolynomial.eval a h)).1 hbA + cases hbB : oddCZBucket q (CPolynomial.eval b h) with + | zero => + have hB := (oddCZBucket_eq_zero_iff q (CPolynomial.eval b h)).1 hbB + exact cantorZassenhausOddAttemptWith_success_aux M D q attempt hg hrootB hrootA + hA.1 (Or.inl hB) + | square => exact absurd (hbA.trans hbB.symm) hsep + | nonsquare => + have hB := (oddCZBucket_eq_nonsquare_iff q (CPolynomial.eval b h)).1 hbB + exact cantorZassenhausOddAttemptWith_success_aux M D q attempt hg hrootA hrootB + hB.1 (Or.inr ⟨hA.1, hA.2, hB.2⟩) + | nonsquare => + have hA := (oddCZBucket_eq_nonsquare_iff q (CPolynomial.eval a h)).1 hbA + cases hbB : oddCZBucket q (CPolynomial.eval b h) with + | zero => + have hB := (oddCZBucket_eq_zero_iff q (CPolynomial.eval b h)).1 hbB + exact cantorZassenhausOddAttemptWith_success_aux M D q attempt hg hrootB hrootA + hA.1 (Or.inl hB) + | square => + have hB := (oddCZBucket_eq_square_iff q (CPolynomial.eval b h)).1 hbB + exact cantorZassenhausOddAttemptWith_success_aux M D q attempt hg hrootB hrootA + hA.1 (Or.inr ⟨hB.1, hB.2, hA.2⟩) + | nonsquare => exact absurd (hbA.trans hbB.symm) hsep + +end FiniteField + +end Roots + +end CPolynomial + +end CompPoly diff --git a/CompPoly/Univariate/Roots/LasVegas/OddBucket.lean b/CompPoly/Univariate/Roots/LasVegas/OddBucket.lean new file mode 100644 index 00000000..8eb612a0 --- /dev/null +++ b/CompPoly/Univariate/Roots/LasVegas/OddBucket.lean @@ -0,0 +1,85 @@ +/- +Copyright (c) 2026 CompPoly Contributors. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Valerii Huhnin +-/ + +module + +public import Mathlib.Algebra.GroupWithZero.Defs + +/-! +# Euler Buckets for Odd Cantor-Zassenhaus Splitting + +Deterministic three-way classifier for field values seen by one odd-field +Cantor-Zassenhaus probe: zero values, nonzero values whose Euler power +`x ^ ((q - 1) / 2)` equals one, and the remaining nonzero values. + +This module is shared by the deterministic correctness surface and the +probability surface, so it must stay free of `PMF` and measure-theory imports. +-/ + +@[expose] public section + +namespace CompPoly + +namespace CPolynomial + +namespace Roots + +namespace FiniteField + +/-- Euler bucket of a field value under one odd Cantor-Zassenhaus probe. -/ +inductive OddCZBucket where + | zero + | square + | nonsquare + deriving DecidableEq, Repr + +/-- +Classify a value by its Euler power `x ^ ((q - 1) / 2)`. + +The `nonsquare` bucket is the residual branch `x ≠ 0 ∧ x ^ ((q - 1) / 2) ≠ 1`; +identifying it with `x ^ ((q - 1) / 2) = -1` additionally needs `q` to be the +odd cardinality of the field, which is a probability-side bridge lemma. +-/ +def oddCZBucket {F : Type*} [MonoidWithZero F] [BEq F] [LawfulBEq F] + (q : Nat) (x : F) : OddCZBucket := + if x == 0 then + OddCZBucket.zero + else if x ^ ((q - 1) / 2) == 1 then + OddCZBucket.square + else + OddCZBucket.nonsquare + +/-- The zero bucket contains exactly the zero value. -/ +theorem oddCZBucket_eq_zero_iff {F : Type*} [MonoidWithZero F] [BEq F] [LawfulBEq F] + (q : Nat) (x : F) : + oddCZBucket q x = OddCZBucket.zero ↔ x = 0 := by + by_cases hx : x = 0 + · simp [oddCZBucket, hx] + · by_cases hpow : x ^ ((q - 1) / 2) = 1 <;> simp [oddCZBucket, hx, hpow] + +/-- The square bucket contains exactly the nonzero values with Euler power one. -/ +theorem oddCZBucket_eq_square_iff {F : Type*} [MonoidWithZero F] [BEq F] [LawfulBEq F] + (q : Nat) (x : F) : + oddCZBucket q x = OddCZBucket.square ↔ x ≠ 0 ∧ x ^ ((q - 1) / 2) = 1 := by + by_cases hx : x = 0 + · simp [oddCZBucket, hx] + · by_cases hpow : x ^ ((q - 1) / 2) = 1 <;> simp [oddCZBucket, hx, hpow] + +/-- The nonsquare bucket is the residual branch of the classifier. -/ +theorem oddCZBucket_eq_nonsquare_iff {F : Type*} [MonoidWithZero F] [BEq F] [LawfulBEq F] + (q : Nat) (x : F) : + oddCZBucket q x = OddCZBucket.nonsquare ↔ x ≠ 0 ∧ x ^ ((q - 1) / 2) ≠ 1 := by + by_cases hx : x = 0 + · simp [oddCZBucket, hx] + · by_cases hpow : x ^ ((q - 1) / 2) = 1 <;> simp [oddCZBucket, hx, hpow] + +end FiniteField + +end Roots + +end CPolynomial + +end CompPoly diff --git a/CompPoly/Univariate/Roots/LasVegas/Probability.lean b/CompPoly/Univariate/Roots/LasVegas/Probability.lean new file mode 100644 index 00000000..bdb23c59 --- /dev/null +++ b/CompPoly/Univariate/Roots/LasVegas/Probability.lean @@ -0,0 +1,43 @@ +/- +Copyright (c) 2026 CompPoly Contributors. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Valerii Huhnin +-/ + +module + +public import CompPoly.Univariate.Roots.LasVegas.Probability.Basic +public import CompPoly.Univariate.Roots.LasVegas.Probability.Uniform +public import CompPoly.Univariate.Roots.LasVegas.Probability.OddBuckets +public import CompPoly.Univariate.Roots.LasVegas.Probability.OddTrial +public import CompPoly.Univariate.Roots.LasVegas.Probability.EvenTrace +public import CompPoly.Univariate.Roots.LasVegas.Probability.Repeated +public import CompPoly.Univariate.Roots.LasVegas.Probability.Recursive + +/-! +# Probability Surface for Las Vegas Root Splitting + +Import facade for the PMF-based theorem surface of the randomized performance +story of the Las Vegas finite-field root splitter. The probability modules are +intentionally separate from the executable splitter modules: runtime root +search remains pure and deterministic for a fixed `ProbeFamily`, while these +modules model idealized uniform probes. + +- `Probability.Basic`: event probabilities, uniform enumeration models, trial + results, and small PMF mass lemmas. +- `Probability.Uniform`: uniform field-element, coefficient-array, and probe + distributions, plus pair-evaluation uniformity. +- `Probability.OddBuckets`: Euler criterion bridge and bucket counting for the + deterministic classifier in `LasVegas.OddBucket`. +- `Probability.OddTrial`: the single odd Cantor-Zassenhaus trial PMF, the + half-success theorem for uniform probes, and the root-product adapters. +- `Probability.EvenTrace`: trace-fiber counting and the half-success and + geometric fallback theorems for the characteristic-two trace branch. +- `Probability.Repeated`: repeated one-factor trials and geometric fallback + bounds. +- `Probability.Recursive`: the abstract recursive fallback model, the + multi-factor splitting process that witnesses it, and the binomial-tail + bound on full recursive fallback. +-/ + +@[expose] public section diff --git a/CompPoly/Univariate/Roots/LasVegas/Probability/Basic.lean b/CompPoly/Univariate/Roots/LasVegas/Probability/Basic.lean new file mode 100644 index 00000000..3f815995 --- /dev/null +++ b/CompPoly/Univariate/Roots/LasVegas/Probability/Basic.lean @@ -0,0 +1,167 @@ +/- +Copyright (c) 2026 CompPoly Contributors. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Valerii Huhnin +-/ + +module + +public import CompPoly.Univariate.Roots.LasVegas +public import Mathlib.Probability.ProbabilityMassFunction.Constructions + +/-! +# Probability Basics for Las Vegas Root Splitting + +Shared probability-side vocabulary for the Las Vegas splitter: event +probabilities, uniform enumeration models, split-trial results, and small PMF +lemmas about trial success and failure mass. + +This module is intentionally separate from the executable splitter modules: +runtime root search remains pure and deterministic for a fixed `ProbeFamily`, +while the probability modules model idealized uniform probes. Runtime and +correctness modules must not import this file or any other probability module. +-/ + +@[expose] public section + +open scoped Classical ENNReal NNReal BigOperators + +namespace CompPoly + +namespace CPolynomial + +namespace Roots + +namespace FiniteField + +namespace FieldEnumeration + +/-- A complete field enumeration is nonempty when the field type has a zero. -/ +theorem size_pos {F : Type*} [Zero F] (enumeration : FieldEnumeration F) : + 0 < enumeration.size := by + rcases enumeration.complete 0 with ⟨i, _hi⟩ + exact Nat.zero_lt_of_lt i.isLt + +end FieldEnumeration + +/-- +Probability-only strengthening of `FieldEnumeration`: the lazy enumeration has +no duplicate indices and its size is the finite-field cardinality `q`. +-/ +structure UniformFieldEnumeration (F : Type*) (q : Nat) where + toFieldEnumeration : FieldEnumeration F + injective_elem : Function.Injective toFieldEnumeration.elem + size_eq_q : toFieldEnumeration.size = q + +namespace UniformFieldEnumeration + +/-- A probability-uniform enumeration has positive cardinality. -/ +theorem q_pos {F : Type*} [Zero F] {q : Nat} + (enumeration : UniformFieldEnumeration F q) : + 0 < q := by + rw [← enumeration.size_eq_q] + exact enumeration.toFieldEnumeration.size_pos + +end UniformFieldEnumeration + +/-- +Probability-facing odd-field model: the uniform enumeration size `q` is the +cardinality of the finite field, and that cardinality is odd. + +This intentionally stays separate from the runtime splitter contract +`lasVegasSplitterInput`, which must not accumulate probability hypotheses. +-/ +structure OddUniformFieldModel (F : Type*) (q : Nat) [Fintype F] + (enumeration : UniformFieldEnumeration F q) : Prop where + q_odd : q % 2 = 1 + card_eq : Fintype.card F = q + +/-- Result of one randomized split trial. -/ +inductive TrialResult (F : Type*) [Zero F] where + | split (children : Array (CPolynomial F)) + | failed + +namespace TrialResult + +/-- A trial succeeds when it returns proper child factors. -/ +def IsSuccess {F : Type*} [Zero F] : TrialResult F → Prop + | split _children => True + | failed => False + +/-- Child factors returned by a successful trial, or an empty array after failure. -/ +def children {F : Type*} [Zero F] : TrialResult F → Array (CPolynomial F) + | split children => children + | failed => #[] + +/-- A list of trials has not split the current factor. -/ +def allFailed {F : Type*} [Zero F] : List (TrialResult F) → Prop + | [] => True + | trial :: trials => ¬ trial.IsSuccess ∧ allFailed trials + +end TrialResult + +/-- Probability of an event under a PMF, expressed through the PMF outer measure. -/ +noncomputable def eventProbability {α : Type*} (dist : PMF α) (event : Set α) : ℝ≥0∞ := + dist.toOuterMeasure event + +/-- Probability that a split trial succeeds. -/ +noncomputable def trialSuccessProbability {F : Type*} [Zero F] + (dist : PMF (TrialResult F)) : + ℝ≥0∞ := + eventProbability dist {trial | trial.IsSuccess} + +/-- Trial failure probability is the PMF mass of the failed outcome. -/ +theorem trialFailureProbability_eq_failed_mass {F : Type*} [Zero F] + (dist : PMF (TrialResult F)) : + eventProbability dist {trial | ¬ trial.IsSuccess} = dist TrialResult.failed := by + rw [eventProbability, ← PMF.toOuterMeasure_apply_singleton dist TrialResult.failed] + congr + ext trial + cases trial with + | split children => + constructor + · intro h + exact (h trivial).elim + · intro h + cases h + | failed => + constructor + · intro _h + rfl + · intro _h hfalse + exact hfalse + +/-- Success probability and failed mass of a trial distribution sum to one. -/ +theorem trialSuccessProbability_add_failed_mass {F : Type*} [Zero F] + (dist : PMF (TrialResult F)) : + dist TrialResult.failed + trialSuccessProbability dist = 1 := by + rw [trialSuccessProbability, eventProbability, PMF.toOuterMeasure_apply] + trans dist TrialResult.failed + ∑' trial, if trial = TrialResult.failed then 0 else dist trial + · congr 1 + apply tsum_congr + intro trial + cases trial <;> simp [TrialResult.IsSuccess] + · rw [← ENNReal.tsum_eq_add_tsum_ite + (f := fun trial : TrialResult F ↦ dist trial) TrialResult.failed] + exact dist.tsum_coe + +/-- A trial that succeeds with probability at least `1 / 2` fails with at most `1 / 2`. -/ +theorem trialFailureProbability_le_half_of_success {F : Type*} [Zero F] + (dist : PMF (TrialResult F)) + (hsuccess : (2 : ℝ≥0∞)⁻¹ ≤ trialSuccessProbability dist) : + eventProbability dist {trial | ¬ trial.IsSuccess} ≤ (2 : ℝ≥0∞)⁻¹ := by + rw [trialFailureProbability_eq_failed_mass] + have htotal := trialSuccessProbability_add_failed_mass dist + have hsum_le : dist TrialResult.failed + (2 : ℝ≥0∞)⁻¹ ≤ 1 := by + rw [← htotal] + exact add_le_add le_rfl hsuccess + simpa [ENNReal.one_sub_inv_two] using + ENNReal.le_sub_of_add_le_right (by simp : (2 : ℝ≥0∞)⁻¹ ≠ ∞) hsum_le + +end FiniteField + +end Roots + +end CPolynomial + +end CompPoly diff --git a/CompPoly/Univariate/Roots/LasVegas/Probability/EvenTrace.lean b/CompPoly/Univariate/Roots/LasVegas/Probability/EvenTrace.lean new file mode 100644 index 00000000..f7707a1e --- /dev/null +++ b/CompPoly/Univariate/Roots/LasVegas/Probability/EvenTrace.lean @@ -0,0 +1,605 @@ +/- +Copyright (c) 2026 CompPoly Contributors. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Valerii Huhnin +-/ + +module + +-- `natDegree` and friends are declared in bare `public section`s, so their bodies +-- are opaque downstream; see `docs/wiki/module-system.md`. +import all CompPoly.Univariate.Basic +public import CompPoly.Univariate.Roots.LasVegas.Probability.OddTrial +public import Mathlib.FieldTheory.Finite.Basic + +/-! +# Probability Surface for Characteristic-Two Trace Splitting + +Probability analysis of the characteristic-two trace split branch under +idealized uniform probes: trace power sums take only the values zero and one, +each trace fiber has exactly half the field, so a uniform probe separates two +distinct roots into different trace fibers with probability at least `1 / 2` +and one trace split attempt succeeds with probability at least `1 / 2`. +-/ + +@[expose] public section + +open scoped Classical ENNReal NNReal BigOperators + +namespace CompPoly + +namespace CPolynomial + +namespace Roots + +namespace FiniteField + +/-- +Probability-facing model for the characteristic-two trace splitter: the trace +context has base prime two and its cardinality matches both the uniform +enumeration size and the field cardinality. +-/ +structure EvenTraceUniformFieldModel (F : Type*) (q : Nat) + [Field F] [Fintype F] [BEq F] [LawfulBEq F] + (traceCtx : SmallPrimeTraceContext F) + (enumeration : UniformFieldEnumeration F q) : Prop where + p_eq : traceCtx.p = 2 + q_eq : traceCtx.q = q + card_eq : Fintype.card F = q + +/-- Trace power sums square to themselves over a binary field. -/ +private theorem tracePowerSum_mul_self {F : Type*} [Field F] [Fintype F] + {k : Nat} (hcard : Fintype.card F = 2 ^ k) (hchar : ringChar F = 2) (x : F) : + tracePowerSum 2 k x * tracePowerSum 2 k x = tracePowerSum 2 k x := by + have : CharP F 2 := hchar ▸ ringChar.charP F + have : Fact (Nat.Prime 2) := ⟨Nat.prime_two⟩ + rw [tracePowerSum_eq_sum_range] + have hsq : (∑ i ∈ Finset.range k, x ^ 2 ^ i) * (∑ i ∈ Finset.range k, x ^ 2 ^ i) = + ∑ i ∈ Finset.range k, x ^ 2 ^ (i + 1) := by + rw [← _root_.pow_two, sum_pow_char] + exact Finset.sum_congr rfl fun i _ ↦ by + rw [← pow_mul, pow_succ, Nat.mul_comm (2 ^ i) 2, pow_mul] + rw [hsq] + have hfrob : x ^ 2 ^ k = x := by + have h := FiniteField.pow_card x + rwa [hcard] at h + have h1 : (∑ i ∈ Finset.range k, x ^ 2 ^ (i + 1)) + x ^ 2 ^ 0 = + (∑ i ∈ Finset.range k, x ^ 2 ^ i) + x ^ 2 ^ k := by + rw [← Finset.sum_range_succ' (fun i ↦ x ^ 2 ^ i) k, Finset.sum_range_succ] + rw [pow_zero, pow_one, hfrob] at h1 + exact add_right_cancel h1 + +/-- Trace power sums over a binary field take only the values zero and one. -/ +theorem tracePowerSum_eq_zero_or_one {F : Type*} [Field F] [Fintype F] + {k : Nat} (hcard : Fintype.card F = 2 ^ k) (hchar : ringChar F = 2) (x : F) : + tracePowerSum 2 k x = 0 ∨ tracePowerSum 2 k x = 1 := by + have hself := tracePowerSum_mul_self hcard hchar x + have hsplit : tracePowerSum 2 k x * (tracePowerSum 2 k x - 1) = 0 := by + rw [_root_.mul_sub, hself, MulOneClass.mul_one, sub_self] + rcases mul_eq_zero.mp hsplit with h | h + · exact Or.inl h + · exact Or.inr (sub_eq_zero.mp h) + +/-- The polynomial whose roots form a trace fiber. -/ +private noncomputable def traceFiberPoly {F : Type*} [Field F] (k : Nat) (c : F) : + Polynomial F := + (∑ i ∈ Finset.range k, (Polynomial.X : Polynomial F) ^ 2 ^ i) - Polynomial.C c + +private theorem eval_traceFiberPoly {F : Type*} [Field F] (k : Nat) (c y : F) : + (traceFiberPoly k c).eval y = tracePowerSum 2 k y - c := by + unfold traceFiberPoly + rw [Polynomial.eval_sub, Polynomial.eval_finsetSum, Polynomial.eval_C, + tracePowerSum_eq_sum_range] + congr 1 + exact Finset.sum_congr rfl fun i _ ↦ by rw [Polynomial.eval_pow, Polynomial.eval_X] + +private theorem traceFiberPoly_natDegree {F : Type*} [Field F] + {k : Nat} (hk : 1 ≤ k) (c : F) : + (traceFiberPoly k c).natDegree = 2 ^ (k - 1) := by + unfold traceFiberPoly + obtain ⟨k', rfl⟩ : ∃ k', k = k' + 1 := ⟨k - 1, by omega⟩ + rw [Finset.sum_range_succ] + have hre : (∑ i ∈ Finset.range k', (Polynomial.X : Polynomial F) ^ 2 ^ i) + + (Polynomial.X : Polynomial F) ^ 2 ^ k' - Polynomial.C c = + (Polynomial.X : Polynomial F) ^ 2 ^ k' + + ((∑ i ∈ Finset.range k', (Polynomial.X : Polynomial F) ^ 2 ^ i) - + Polynomial.C c) := by + ring + have hpos : 0 < 2 ^ k' := Nat.two_pow_pos k' + have hsmall : ((∑ i ∈ Finset.range k', (Polynomial.X : Polynomial F) ^ 2 ^ i) - + Polynomial.C c).natDegree < 2 ^ k' := by + refine lt_of_le_of_lt (Polynomial.natDegree_sub_le _ _) ?_ + apply max_lt + · refine lt_of_le_of_lt + (Polynomial.natDegree_sum_le_of_forall_le (n := 2 ^ k' - 1) _ _ + fun i hi ↦ ?_) ?_ + · show ((Polynomial.X : Polynomial F) ^ 2 ^ i).natDegree ≤ 2 ^ k' - 1 + rw [Polynomial.natDegree_X_pow] + have hlt : 2 ^ i < 2 ^ k' := + Nat.pow_lt_pow_right (by omega) (Finset.mem_range.mp hi) + omega + · omega + · rw [Polynomial.natDegree_C] + exact hpos + rw [hre, Polynomial.natDegree_add_eq_left_of_natDegree_lt + (by rw [Polynomial.natDegree_X_pow]; exact hsmall), Polynomial.natDegree_X_pow] + simp + +/-- Every trace fiber contains at most half the field. -/ +private theorem card_tracePowerSum_fiber_le {F : Type*} [Field F] [Fintype F] + {k : Nat} (hk : 1 ≤ k) (c : F) : + (Finset.univ.filter fun y : F ↦ tracePowerSum 2 k y = c).card ≤ 2 ^ (k - 1) := by + have hpos : 0 < 2 ^ (k - 1) := Nat.two_pow_pos (k - 1) + have hne : traceFiberPoly (F := F) k c ≠ 0 := by + intro h0 + have hdeg := traceFiberPoly_natDegree (F := F) hk c + rw [h0, Polynomial.natDegree_zero] at hdeg + omega + calc (Finset.univ.filter fun y : F ↦ tracePowerSum 2 k y = c).card + ≤ (traceFiberPoly (F := F) k c).roots.toFinset.card := by + apply Finset.card_le_card + intro y hy + rw [Multiset.mem_toFinset, Polynomial.mem_roots'] + refine ⟨hne, ?_⟩ + show (traceFiberPoly (F := F) k c).eval y = 0 + rw [eval_traceFiberPoly, (Finset.mem_filter.mp hy).2, sub_self] + _ ≤ Multiset.card (traceFiberPoly (F := F) k c).roots := + Multiset.toFinset_card_le _ + _ ≤ (traceFiberPoly (F := F) k c).natDegree := Polynomial.card_roots' _ + _ = 2 ^ (k - 1) := traceFiberPoly_natDegree hk c + +/-- +Two independent uniform field values land in different trace fibers with +probability at least `1 / 2`. +-/ +theorem tracePowerSum_pair_separated_probability_ge_half {F : Type*} + [Field F] [Fintype F] [BEq F] [LawfulBEq F] + {q k : Nat} (enumeration : UniformFieldEnumeration F q) + (hcard : Fintype.card F = q) (hq : q = 2 ^ k) (hk : 1 ≤ k) : + (2 : ℝ≥0∞)⁻¹ ≤ + eventProbability + ((uniformFieldElementPMF enumeration.toFieldEnumeration).bind + fun x ↦ (uniformFieldElementPMF enumeration.toFieldEnumeration).map + fun y ↦ (x, y)) + {xy : F × F | tracePowerSum 2 k xy.1 ≠ tracePowerSum 2 k xy.2} := by + apply uniformPair_separated_probability_ge_half_of_fiber_card_le enumeration + (fun x ↦ tracePowerSum 2 k x) hcard (m := 2 ^ (k - 1)) + · intro x + refine ⟨Finset.univ.filter fun y : F ↦ tracePowerSum 2 k y = tracePowerSum 2 k x, + fun y hy ↦ Finset.mem_filter.mpr ⟨Finset.mem_univ y, hy⟩, ?_⟩ + exact card_tracePowerSum_fiber_le hk (tracePowerSum 2 k x) + · have h2 : 2 ^ k = 2 * 2 ^ (k - 1) := by + conv_lhs => rw [show k = (k - 1) + 1 by omega] + rw [pow_succ, Nat.mul_comm] + omega + +/-- One characteristic-two trace split trial under a uniform probe distribution. -/ +noncomputable def evenTraceTrialPMF {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + (traceCtx : SmallPrimeTraceContext F) + (enumeration : FieldEnumeration F) (q coefficientCount : Nat) + (g : CPolynomial F) (attempt : Nat) : + PMF (TrialResult F) := + (uniformProbePMF enumeration coefficientCount).map fun h ↦ + match cantorZassenhausEvenTraceAttemptWith M D traceCtx q + ({ probe := fun _q _factor _attempt ↦ h } : ProbeFamily F) g attempt with + | some children => TrialResult.split children + | none => TrialResult.failed + +/-- +Uniform probe polynomials make one characteristic-two trace split attempt +succeed with probability at least `1 / 2`: probe values at two distinct roots +are an independent uniform pair, the pair lands in different trace fibers with +probability at least `1 / 2`, and trace-separated probes force the executable +attempt to succeed. +-/ +theorem evenTraceTrial_success_probability_ge_half_of_two_le {F : Type*} + [Field F] [Fintype F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + (traceCtx : SmallPrimeTraceContext F) + {q : Nat} (enumeration : UniformFieldEnumeration F q) + (g : CPolynomial F) (coefficientCount attempt : Nat) + (hcount : 2 ≤ coefficientCount) + (hfield : EvenTraceUniformFieldModel F q traceCtx enumeration) + (hroots : HasTwoDistinctRoots g) + (hg : g ≠ 0) : + (2 : ℝ≥0∞)⁻¹ ≤ + trialSuccessProbability + (evenTraceTrialPMF M D traceCtx enumeration.toFieldEnumeration q + coefficientCount g attempt) := by + obtain ⟨a, b, hab, hra, hrb⟩ := hroots + have hq : q = 2 ^ traceCtx.k := by + rw [← hfield.q_eq, traceCtx.q_eq, hfield.p_eq] + have hq2 : 2 ≤ q := by + have hlt := Fintype.one_lt_card (α := F) + have hcard := hfield.card_eq + omega + have hk : 1 ≤ traceCtx.k := by + rcases Nat.eq_zero_or_pos traceCtx.k with hk0 | hk1 + · rw [hk0, pow_zero] at hq + omega + · exact hk1 + have hcard2 : Fintype.card F = 2 ^ traceCtx.k := by + rw [hfield.card_eq, hq] + have hchar : ringChar F = 2 := by + apply FiniteField.even_card_iff_char_two.mpr + rw [hcard2] + have hdvd : (2 : Nat) ∣ 2 ^ traceCtx.k := dvd_pow_self 2 (by omega) + omega + have hzero_one := fun x : F ↦ tracePowerSum_eq_zero_or_one hcard2 hchar x + have hpush_eq := uniformProbePMF_map_eval_pair enumeration coefficientCount hab hcount + rw [trialSuccessProbability, eventProbability, evenTraceTrialPMF, + PMF.toOuterMeasure_map_apply] + have hsubset : {h : CPolynomial F | + tracePowerSum 2 traceCtx.k (CPolynomial.eval a h) ≠ + tracePowerSum 2 traceCtx.k (CPolynomial.eval b h)} ⊆ + (fun h : CPolynomial F ↦ + match cantorZassenhausEvenTraceAttemptWith M D traceCtx q + ({ probe := fun _q _factor _attempt ↦ h } : ProbeFamily F) g attempt with + | some children => TrialResult.split children + | none => TrialResult.failed) ⁻¹' {trial | trial.IsSuccess} := by + intro h hsep + rw [Set.mem_ofPred_eq] at hsep + have hsucc : ∃ children, + cantorZassenhausEvenTraceAttemptWith M D traceCtx q + ({ probe := fun _q _factor _attempt ↦ h } : ProbeFamily F) + g attempt = some children := by + rcases hzero_one (CPolynomial.eval a h) with hA | hA <;> + rcases hzero_one (CPolynomial.eval b h) with hB | hB + · exact absurd (hA.trans hB.symm) hsep + · exact cantorZassenhausEvenTraceAttemptWith_success_of_trace_separated + M D traceCtx q attempt hg hra hrb + (by rw [hfield.p_eq]; exact hA) + (by rw [hfield.p_eq, hB]; exact one_ne_zero) + · exact cantorZassenhausEvenTraceAttemptWith_success_of_trace_separated + M D traceCtx q attempt hg hrb hra + (by rw [hfield.p_eq]; exact hB) + (by rw [hfield.p_eq, hA]; exact one_ne_zero) + · exact absurd (hA.trans hB.symm) hsep + obtain ⟨children, hchildren⟩ := hsucc + simp only [Set.mem_preimage, Set.mem_ofPred_eq, hchildren] + exact trivial + have hpre : ((fun h : CPolynomial F ↦ + (CPolynomial.eval a h, CPolynomial.eval b h)) ⁻¹' + {xy : F × F | tracePowerSum 2 traceCtx.k xy.1 ≠ + tracePowerSum 2 traceCtx.k xy.2}) = + {h : CPolynomial F | + tracePowerSum 2 traceCtx.k (CPolynomial.eval a h) ≠ + tracePowerSum 2 traceCtx.k (CPolynomial.eval b h)} := rfl + have hge : (2 : ℝ≥0∞)⁻¹ ≤ + (uniformProbePMF enumeration.toFieldEnumeration + coefficientCount).toOuterMeasure + {h : CPolynomial F | + tracePowerSum 2 traceCtx.k (CPolynomial.eval a h) ≠ + tracePowerSum 2 traceCtx.k (CPolynomial.eval b h)} := by + rw [← hpre, ← PMF.toOuterMeasure_map_apply, hpush_eq] + exact tracePowerSum_pair_separated_probability_ge_half enumeration + hfield.card_eq hq hk + exact le_trans hge ((uniformProbePMF enumeration.toFieldEnumeration + coefficientCount).toOuterMeasure.mono hsubset) + +/-- +Uniform probe polynomials make one characteristic-two trace split attempt +succeed with probability at least `1 / 2`. +-/ +theorem evenTraceTrial_success_probability_ge_half {F : Type*} + [Field F] [Fintype F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + (traceCtx : SmallPrimeTraceContext F) + {q : Nat} (enumeration : UniformFieldEnumeration F q) + (g : CPolynomial F) (attempt : Nat) + (hfield : EvenTraceUniformFieldModel F q traceCtx enumeration) + (hroots : HasTwoDistinctRoots g) + (hdegree : 2 ≤ CPolynomial.natDegree g) : + (2 : ℝ≥0∞)⁻¹ ≤ + trialSuccessProbability + (evenTraceTrialPMF M D traceCtx enumeration.toFieldEnumeration q + (CPolynomial.natDegree g) g attempt) := by + have hg : g ≠ 0 := by + intro hzero + rw [hzero] at hdegree + have hnat : CPolynomial.natDegree (0 : CPolynomial F) = 0 := rfl + omega + exact evenTraceTrial_success_probability_ge_half_of_two_le M D traceCtx enumeration g + (CPolynomial.natDegree g) attempt hdegree hfield hroots hg + +/-- Independent repeated trace split trials for one unresolved factor. -/ +noncomputable def repeatedEvenTraceTrialsPMF {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + (traceCtx : SmallPrimeTraceContext F) + (enumeration : FieldEnumeration F) (q coefficientCount : Nat) + (g : CPolynomial F) : + Nat → Nat → PMF (List (TrialResult F)) + | 0, _offset => pure [] + | attempts + 1, offset => do + let trial ← evenTraceTrialPMF M D traceCtx enumeration q coefficientCount g offset + let trials ← repeatedEvenTraceTrialsPMF M D traceCtx enumeration q coefficientCount g + attempts (offset + 1) + pure (trial :: trials) + +/-- Probability that the trace branch reaches fallback after all attempts fail. -/ +noncomputable def fallbackAfterEvenTraceAttemptsProbability {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + (traceCtx : SmallPrimeTraceContext F) + (enumeration : FieldEnumeration F) (q coefficientCount : Nat) + (g : CPolynomial F) (attempts offset : Nat) : + ℝ≥0∞ := + eventProbability + (repeatedEvenTraceTrialsPMF M D traceCtx enumeration q coefficientCount g + attempts offset) + {trials | TrialResult.allFailed trials} + +private theorem toOuterMeasure_map_cons_allFailed {F : Type*} [Zero F] + (dist : PMF (List (TrialResult F))) (trial : TrialResult F) : + (dist.map (List.cons trial)).toOuterMeasure {trials | TrialResult.allFailed trials} = + if ¬ trial.IsSuccess then + dist.toOuterMeasure {trials | TrialResult.allFailed trials} + else + 0 := by + cases trial <;> + simp [PMF.toOuterMeasure_map_apply, TrialResult.IsSuccess, TrialResult.allFailed] + +private theorem fallbackAfterEvenTraceAttemptsProbability_succ {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + (traceCtx : SmallPrimeTraceContext F) + (enumeration : FieldEnumeration F) (q coefficientCount : Nat) + (g : CPolynomial F) (attempts offset : Nat) : + fallbackAfterEvenTraceAttemptsProbability M D traceCtx enumeration q coefficientCount g + (attempts + 1) offset = + eventProbability + (evenTraceTrialPMF M D traceCtx enumeration q coefficientCount g offset) + {trial | ¬ trial.IsSuccess} * + fallbackAfterEvenTraceAttemptsProbability M D traceCtx enumeration q coefficientCount + g attempts (offset + 1) := by + change (PMF.bind (evenTraceTrialPMF M D traceCtx enumeration q coefficientCount g offset) + (fun trial ↦ PMF.map (List.cons trial) + (repeatedEvenTraceTrialsPMF M D traceCtx enumeration q coefficientCount g attempts + (offset + 1)))).toOuterMeasure + {trials | TrialResult.allFailed trials} = + (evenTraceTrialPMF M D traceCtx enumeration q coefficientCount g offset).toOuterMeasure + {trial | ¬ trial.IsSuccess} * + (repeatedEvenTraceTrialsPMF M D traceCtx enumeration q coefficientCount g attempts + (offset + 1)).toOuterMeasure + {trials | TrialResult.allFailed trials} + rw [PMF.toOuterMeasure_bind_apply] + trans (∑' trial, (if ¬ trial.IsSuccess then + (evenTraceTrialPMF M D traceCtx enumeration q coefficientCount g offset) trial + else 0) * + (repeatedEvenTraceTrialsPMF M D traceCtx enumeration q coefficientCount g attempts + (offset + 1)).toOuterMeasure + {trials | TrialResult.allFailed trials}) + · apply tsum_congr + intro trial + rw [toOuterMeasure_map_cons_allFailed] + by_cases hfail : ¬ trial.IsSuccess <;> simp [hfail] + · conv_rhs => + rw [PMF.toOuterMeasure_apply] + rw [← ENNReal.tsum_mul_right] + apply tsum_congr + intro trial + by_cases hsuccess : trial.IsSuccess <;> simp [hsuccess] + +/-- +If every trace trial succeeds with probability at least `1 / 2`, the +probability of using fallback after `attempts` trials is at most `2^-attempts`. +-/ +theorem fallbackAfterEvenTraceAttempts_probability_le_geometric {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + (traceCtx : SmallPrimeTraceContext F) + (enumeration : FieldEnumeration F) (q coefficientCount : Nat) + (g : CPolynomial F) (attempts offset : Nat) + (hstep : ∀ i, i < attempts → + (2 : ℝ≥0∞)⁻¹ ≤ + trialSuccessProbability + (evenTraceTrialPMF M D traceCtx enumeration q coefficientCount g (offset + i))) : + fallbackAfterEvenTraceAttemptsProbability M D traceCtx enumeration q coefficientCount g + attempts offset ≤ + ((2 : ℝ≥0∞)⁻¹) ^ attempts := by + induction attempts generalizing offset with + | zero => + rw [fallbackAfterEvenTraceAttemptsProbability, eventProbability, + repeatedEvenTraceTrialsPMF] + exact le_trans + ((pure ([] : List (TrialResult F)) : PMF (List (TrialResult F))).toOuterMeasure.mono + (Set.subset_univ _)) + (by simp [PMF.toOuterMeasure_apply]) + | succ attempts ih => + rw [fallbackAfterEvenTraceAttemptsProbability_succ] + let half : ℝ≥0∞ := (2 : ℝ≥0∞)⁻¹ + have hfail : eventProbability + (evenTraceTrialPMF M D traceCtx enumeration q coefficientCount g offset) + {trial | ¬ trial.IsSuccess} ≤ half := by + exact trialFailureProbability_le_half_of_success _ (hstep 0 (by omega)) + have hrest : + fallbackAfterEvenTraceAttemptsProbability M D traceCtx enumeration q + coefficientCount g attempts (offset + 1) ≤ + half ^ attempts := by + exact ih (offset + 1) (by + intro i hi + have h := hstep (i + 1) (by omega) + simpa [Nat.add_assoc, Nat.add_comm, Nat.add_left_comm] using h) + refine (mul_le_mul' hfail hrest).trans ?_ + change half * half ^ attempts ≤ half ^ (attempts + 1) + rw [pow_succ] + exact le_of_eq (_root_.mul_comm (a := half) (b := half ^ attempts)) + +/-- +For a squarefree root product over a binary field, the uniform probe source +reaches enumeration fallback after `attempts` trace trials with probability at +most `2^-attempts`. +-/ +theorem fallbackAfterEvenTraceAttempts_probability_le_geometric_of_uniformProbe {F : Type*} + [Field F] [Fintype F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + (traceCtx : SmallPrimeTraceContext F) + {q : Nat} (enumeration : UniformFieldEnumeration F q) + (g : CPolynomial F) (attempts offset : Nat) + (hfield : EvenTraceUniformFieldModel F q traceCtx enumeration) + (hrootProduct : RootProductProbabilityInput q g) + (hdegree : 2 ≤ CPolynomial.natDegree g) : + fallbackAfterEvenTraceAttemptsProbability M D traceCtx + enumeration.toFieldEnumeration q (CPolynomial.natDegree g) g attempts offset ≤ + ((2 : ℝ≥0∞)⁻¹) ^ attempts := by + apply fallbackAfterEvenTraceAttempts_probability_le_geometric + intro i _hi + exact evenTraceTrial_success_probability_ge_half M D traceCtx enumeration g (offset + i) + hfield (hrootProduct.hasTwoDistinctRoots hdegree) hdegree + +set_option maxHeartbeats 1600000 in +/-- +Bridge to the executable trace retry loop: the deterministic +`tryEvenTraceSplitAttemptsWith`, driven by a probe table whose entries are +sampled independently and uniformly, exhausts all attempts with probability at +most `2 ^ -attempts`. +-/ +theorem tryEvenTraceSplitAttemptsWith_uniformTable_none_le_geometric {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + (traceCtx : SmallPrimeTraceContext F) + (enumeration : FieldEnumeration F) (q coefficientCount : Nat) (g : CPolynomial F) + (hstep : (2 : ℝ≥0∞)⁻¹ ≤ + trialSuccessProbability + (evenTraceTrialPMF M D traceCtx enumeration q coefficientCount g 0)) : + ∀ attempts : Nat, + eventProbability (uniformProbeTablePMF enumeration coefficientCount attempts) + {table : List (CPolynomial F) | + tryEvenTraceSplitAttemptsWith M D traceCtx q (tableProbeFamily table) g + attempts 0 = none} ≤ + ((2 : ℝ≥0∞)⁻¹) ^ attempts := by + have hfailset : ((fun h : CPolynomial F ↦ + (match cantorZassenhausEvenTraceAttemptWith M D traceCtx q + ({ probe := fun _q _factor _attempt ↦ h } : ProbeFamily F) g 0 with + | some children => TrialResult.split children + | none => TrialResult.failed)) ⁻¹' + {trial : TrialResult F | ¬ trial.IsSuccess}) = + {h : CPolynomial F | cantorZassenhausEvenTraceAttemptWith M D traceCtx q + ({ probe := fun _q _factor _attempt ↦ h } : ProbeFamily F) g 0 = none} := by + ext h + cases h0 : cantorZassenhausEvenTraceAttemptWith M D traceCtx q + ({ probe := fun _q _factor _attempt ↦ h } : ProbeFamily F) g 0 <;> + simp [h0, TrialResult.IsSuccess] + have hfail : (uniformProbePMF enumeration coefficientCount).toOuterMeasure + {h : CPolynomial F | cantorZassenhausEvenTraceAttemptWith M D traceCtx q + ({ probe := fun _q _factor _attempt ↦ h } : ProbeFamily F) g 0 = none} ≤ + (2 : ℝ≥0∞)⁻¹ := by + rw [← hfailset, ← PMF.toOuterMeasure_map_apply] + have hhalf := trialFailureProbability_le_half_of_success + (evenTraceTrialPMF M D traceCtx enumeration q coefficientCount g 0) hstep + rw [eventProbability, evenTraceTrialPMF] at hhalf + exact hhalf + intro attempts + induction attempts with + | zero => + rw [eventProbability, uniformProbeTablePMF, PMF.toOuterMeasure_pure_apply, + if_pos (by + rw [Set.mem_ofPred_eq] + unfold tryEvenTraceSplitAttemptsWith + rfl)] + simp + | succ attempts ih => + rw [eventProbability, uniformProbeTablePMF, PMF.toOuterMeasure_bind_apply] + have hsection : ∀ (h : CPolynomial F) (rest : List (CPolynomial F)), + (tryEvenTraceSplitAttemptsWith M D traceCtx q (tableProbeFamily (h :: rest)) g + (attempts + 1) 0 = none) ↔ + (cantorZassenhausEvenTraceAttemptWith M D traceCtx q + ({ probe := fun _q _factor _attempt ↦ h } : ProbeFamily F) g 0 = none ∧ + tryEvenTraceSplitAttemptsWith M D traceCtx q (tableProbeFamily rest) g + attempts 0 = none) := by + intro h rest + unfold tryEvenTraceSplitAttemptsWith + rw [cantorZassenhausEvenTraceAttemptWith_probe_congr M D traceCtx q (g := g) + (p2 := ({ probe := fun _q _factor _attempt ↦ h } : ProbeFamily F)) + (a2 := 0) (by simp [tableProbeFamily])] + rw [tryEvenTraceSplitAttemptsWith_probe_congr M D traceCtx q + (p1 := tableProbeFamily (h :: rest)) (p2 := tableProbeFamily rest) (g := g) + attempts (0 + 1) 0 (by + intro i _hi + simp [tableProbeFamily, Nat.add_comm])] + cases h0 : cantorZassenhausEvenTraceAttemptWith M D traceCtx q + ({ probe := fun _q _factor _attempt ↦ h } : ProbeFamily F) g 0 with + | some c => simp + | none => + rw [← tryEvenTraceSplitAttemptsWith.eq_def] + simp + have hweight : ∀ h : CPolynomial F, + ((uniformProbeTablePMF enumeration coefficientCount attempts).map + (List.cons h)).toOuterMeasure + {table : List (CPolynomial F) | + tryEvenTraceSplitAttemptsWith M D traceCtx q (tableProbeFamily table) g + (attempts + 1) 0 = none} = + if cantorZassenhausEvenTraceAttemptWith M D traceCtx q + ({ probe := fun _q _factor _attempt ↦ h } : ProbeFamily F) g 0 = none then + (uniformProbeTablePMF enumeration coefficientCount attempts).toOuterMeasure + {table : List (CPolynomial F) | + tryEvenTraceSplitAttemptsWith M D traceCtx q (tableProbeFamily table) g + attempts 0 = none} + else 0 := by + intro h + rw [PMF.toOuterMeasure_map_apply] + by_cases h0 : cantorZassenhausEvenTraceAttemptWith M D traceCtx q + ({ probe := fun _q _factor _attempt ↦ h } : ProbeFamily F) g 0 = none + · rw [if_pos h0] + congr 1 + ext rest + rw [Set.mem_preimage, Set.mem_ofPred_eq, Set.mem_ofPred_eq, hsection h rest] + simp [h0] + · rw [if_neg h0] + have hempty : (List.cons h ⁻¹' + {table : List (CPolynomial F) | + tryEvenTraceSplitAttemptsWith M D traceCtx q (tableProbeFamily table) g + (attempts + 1) 0 = none}) = ∅ := by + ext rest + rw [Set.mem_preimage, Set.mem_ofPred_eq, hsection h rest] + simp [h0] + rw [hempty] + simp + calc (∑' h : CPolynomial F, uniformProbePMF enumeration coefficientCount h * + ((uniformProbeTablePMF enumeration coefficientCount attempts).map + (List.cons h)).toOuterMeasure + {table : List (CPolynomial F) | + tryEvenTraceSplitAttemptsWith M D traceCtx q (tableProbeFamily table) g + (attempts + 1) 0 = none}) + ≤ ∑' h : CPolynomial F, + (if cantorZassenhausEvenTraceAttemptWith M D traceCtx q + ({ probe := fun _q _factor _attempt ↦ h } : ProbeFamily F) g 0 = none + then uniformProbePMF enumeration coefficientCount h else 0) * + ((2 : ℝ≥0∞)⁻¹) ^ attempts := by + apply ENNReal.tsum_le_tsum + intro h + rw [hweight h] + by_cases h0 : cantorZassenhausEvenTraceAttemptWith M D traceCtx q + ({ probe := fun _q _factor _attempt ↦ h } : ProbeFamily F) g 0 = none + · rw [if_pos h0, if_pos h0] + exact mul_le_mul' le_rfl (by + have := ih + rwa [eventProbability] at this) + · rw [if_neg h0, if_neg h0] + simp + _ = (uniformProbePMF enumeration coefficientCount).toOuterMeasure + {h : CPolynomial F | cantorZassenhausEvenTraceAttemptWith M D traceCtx q + ({ probe := fun _q _factor _attempt ↦ h } : ProbeFamily F) g 0 = none} * + ((2 : ℝ≥0∞)⁻¹) ^ attempts := by + rw [ENNReal.tsum_mul_right] + congr 1 + rw [PMF.toOuterMeasure_apply] + apply tsum_congr + intro h + rw [Set.indicator_apply] + rfl + _ ≤ (2 : ℝ≥0∞)⁻¹ * ((2 : ℝ≥0∞)⁻¹) ^ attempts := + _root_.mul_le_mul_left hfail _ + _ = ((2 : ℝ≥0∞)⁻¹) ^ (attempts + 1) := by + rw [pow_succ, _root_.mul_comm] + +end FiniteField + +end Roots + +end CPolynomial + +end CompPoly diff --git a/CompPoly/Univariate/Roots/LasVegas/Probability/OddBuckets.lean b/CompPoly/Univariate/Roots/LasVegas/Probability/OddBuckets.lean new file mode 100644 index 00000000..3b335bf5 --- /dev/null +++ b/CompPoly/Univariate/Roots/LasVegas/Probability/OddBuckets.lean @@ -0,0 +1,255 @@ +/- +Copyright (c) 2026 CompPoly Contributors. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Valerii Huhnin +-/ + +module + +public import CompPoly.Univariate.Roots.LasVegas.OddBucket +public import CompPoly.Univariate.Roots.LasVegas.Probability.Uniform +public import Mathlib.FieldTheory.Finite.Basic + +/-! +# Euler Bucket Counting for Odd Cantor-Zassenhaus Splitting + +Finite-field counting facts about the deterministic Euler bucket classifier +`oddCZBucket`: the Euler criterion bridge, the bucket cardinalities +`1`, `(q - 1) / 2`, and `(q - 1) / 2`, and the resulting probability that two +independent uniform field values land in different buckets. + +These theorems are field-theoretic, not algorithmic: they do not mention +`CPolynomial`, gcds, or the Las Vegas loop. +-/ + +@[expose] public section + +open scoped Classical ENNReal NNReal BigOperators + +namespace CompPoly + +namespace CPolynomial + +namespace Roots + +namespace FiniteField + +private theorem two_le_q_of_card_eq {F : Type*} [Field F] [Fintype F] + {q : Nat} (hcard : Fintype.card F = q) : + 2 ≤ q := by + have hlt := Fintype.one_lt_card (α := F) + omega + +private theorem ringChar_ne_two_of_odd_card {F : Type*} [Field F] [Fintype F] + {q : Nat} (hodd : q % 2 = 1) (hcard : Fintype.card F = q) : + ringChar F ≠ 2 := by + intro hchar + have heven := FiniteField.even_card_iff_char_two.mp hchar + rw [hcard] at heven + omega + +private theorem pow_half_eq_one_or_neg_one {F : Type*} [Field F] [Fintype F] + {q : Nat} (hodd : q % 2 = 1) (hcard : Fintype.card F = q) + {x : F} (hx : x ≠ 0) : + x ^ ((q - 1) / 2) = 1 ∨ x ^ ((q - 1) / 2) = -1 := by + have hq2 : 2 ≤ q := two_le_q_of_card_eq hcard + have hferm : x ^ (q - 1) = 1 := by + have h := FiniteField.pow_card_sub_one_eq_one x hx + rwa [hcard] at h + have hsq : x ^ ((q - 1) / 2) * x ^ ((q - 1) / 2) = 1 := by + rw [← pow_add, show (q - 1) / 2 + (q - 1) / 2 = q - 1 by omega] + exact hferm + exact mul_self_eq_one_iff.mp hsq + +/-- Euler criterion bridge: the square bucket consists of the nonzero squares. -/ +theorem oddCZBucket_eq_square_iff_isSquare {F : Type*} + [Field F] [Fintype F] [BEq F] [LawfulBEq F] + {q : Nat} (hodd : q % 2 = 1) (hcard : Fintype.card F = q) (x : F) : + oddCZBucket q x = OddCZBucket.square ↔ x ≠ 0 ∧ IsSquare x := by + have hchar := ringChar_ne_two_of_odd_card hodd hcard + have hexp : Fintype.card F / 2 = (q - 1) / 2 := by + rw [hcard] + omega + rw [oddCZBucket_eq_square_iff] + constructor + · rintro ⟨hx0, hxm⟩ + refine ⟨hx0, (FiniteField.isSquare_iff hchar hx0).mpr ?_⟩ + rw [hexp] + exact hxm + · rintro ⟨hx0, hsq⟩ + refine ⟨hx0, ?_⟩ + have h := (FiniteField.isSquare_iff hchar hx0).mp hsq + rwa [hexp] at h + +/-- +Euler criterion bridge: over an odd field of cardinality `q`, the residual +nonsquare bucket is exactly the locus `x ^ ((q - 1) / 2) = -1`. +-/ +theorem oddCZBucket_eq_nonsquare_iff_pow_eq_neg_one {F : Type*} + [Field F] [Fintype F] [BEq F] [LawfulBEq F] + {q : Nat} (hodd : q % 2 = 1) (hcard : Fintype.card F = q) (x : F) : + oddCZBucket q x = OddCZBucket.nonsquare ↔ x ^ ((q - 1) / 2) = -1 := by + have hq2 : 2 ≤ q := two_le_q_of_card_eq hcard + have hneg : (-1 : F) ≠ 1 := + Ring.neg_one_ne_one_of_char_ne_two (ringChar_ne_two_of_odd_card hodd hcard) + rw [oddCZBucket_eq_nonsquare_iff] + constructor + · rintro ⟨hx0, hxm⟩ + rcases pow_half_eq_one_or_neg_one hodd hcard hx0 with hone | hnegone + · exact absurd hone hxm + · exact hnegone + · intro hxm + have hx0 : x ≠ 0 := by + intro hzero + rw [hzero, zero_pow (by omega : (q - 1) / 2 ≠ 0)] at hxm + exact neg_ne_zero.mpr one_ne_zero hxm.symm + refine ⟨hx0, ?_⟩ + rw [hxm] + exact hneg + +/-- The zero Euler bucket has exactly one element. -/ +theorem card_oddCZBucket_zero {F : Type*} + [Field F] [Fintype F] [BEq F] [LawfulBEq F] (q : Nat) : + (Finset.univ.filter fun x : F ↦ oddCZBucket q x = OddCZBucket.zero).card = 1 := by + have hfe : (Finset.univ.filter fun x : F ↦ oddCZBucket q x = OddCZBucket.zero) = {0} := by + ext x + simp [oddCZBucket_eq_zero_iff] + rw [hfe, Finset.card_singleton] + +private theorem card_oddCZBucket_square_add_nonsquare {F : Type*} + [Field F] [Fintype F] [BEq F] [LawfulBEq F] + {q : Nat} (hcard : Fintype.card F = q) : + (Finset.univ.filter fun x : F ↦ oddCZBucket q x = OddCZBucket.square).card + + (Finset.univ.filter fun x : F ↦ oddCZBucket q x = OddCZBucket.nonsquare).card = + q - 1 := by + have hzero := Finset.card_filter_add_card_filter_not (s := (Finset.univ : Finset F)) + (fun x : F ↦ oddCZBucket q x = OddCZBucket.zero) + have hsplit := Finset.card_filter_add_card_filter_not + (s := Finset.univ.filter fun x : F ↦ ¬ oddCZBucket q x = OddCZBucket.zero) + (fun x : F ↦ oddCZBucket q x = OddCZBucket.square) + rw [Finset.filter_filter, Finset.filter_filter] at hsplit + have hsq : (Finset.univ.filter fun x : F ↦ + ¬ oddCZBucket q x = OddCZBucket.zero ∧ oddCZBucket q x = OddCZBucket.square) = + Finset.univ.filter fun x : F ↦ oddCZBucket q x = OddCZBucket.square := by + apply Finset.filter_congr + intro x _ + cases hbx : oddCZBucket q x <;> simp + have hns : (Finset.univ.filter fun x : F ↦ + ¬ oddCZBucket q x = OddCZBucket.zero ∧ ¬ oddCZBucket q x = OddCZBucket.square) = + Finset.univ.filter fun x : F ↦ oddCZBucket q x = OddCZBucket.nonsquare := by + apply Finset.filter_congr + intro x _ + cases hbx : oddCZBucket q x <;> simp + rw [hsq, hns] at hsplit + have hcardz := card_oddCZBucket_zero (F := F) q + have hcardu : (Finset.univ : Finset F).card = q := by + rw [Finset.card_univ, hcard] + omega + +private theorem card_oddCZBucket_square_eq_nonsquare {F : Type*} + [Field F] [Fintype F] [BEq F] [LawfulBEq F] + {q : Nat} (hodd : q % 2 = 1) (hcard : Fintype.card F = q) : + (Finset.univ.filter fun x : F ↦ oddCZBucket q x = OddCZBucket.square).card = + (Finset.univ.filter fun x : F ↦ oddCZBucket q x = OddCZBucket.nonsquare).card := by + have hchar := ringChar_ne_two_of_odd_card hodd hcard + have hneg : (-1 : F) ≠ 1 := Ring.neg_one_ne_one_of_char_ne_two hchar + obtain ⟨c, hcns⟩ := FiniteField.exists_nonsquare (F := F) hchar + have hc0 : c ≠ 0 := by + intro hzero + exact hcns (by rw [hzero]; exact ⟨0, by simp⟩) + have hcm : c ^ ((q - 1) / 2) = -1 := by + rw [← oddCZBucket_eq_nonsquare_iff_pow_eq_neg_one hodd hcard] + cases hbc : oddCZBucket q c with + | zero => exact absurd ((oddCZBucket_eq_zero_iff q c).1 hbc) hc0 + | square => + exact absurd ((oddCZBucket_eq_square_iff_isSquare hodd hcard c).1 hbc).2 hcns + | nonsquare => rfl + refine Finset.card_bij' (fun x _ ↦ c * x) (fun y _ ↦ c⁻¹ * y) ?_ ?_ ?_ ?_ + · intro x hx + rw [Finset.mem_filter] at hx ⊢ + obtain ⟨hx0, hxm⟩ := (oddCZBucket_eq_square_iff q x).1 hx.2 + refine ⟨Finset.mem_univ _, ?_⟩ + rw [oddCZBucket_eq_nonsquare_iff] + refine ⟨mul_ne_zero hc0 hx0, ?_⟩ + rw [mul_pow, hcm, hxm] + simpa using hneg + · intro y hy + rw [Finset.mem_filter] at hy ⊢ + obtain ⟨hy0, _⟩ := (oddCZBucket_eq_nonsquare_iff q y).1 hy.2 + have hym : y ^ ((q - 1) / 2) = -1 := + (oddCZBucket_eq_nonsquare_iff_pow_eq_neg_one hodd hcard y).1 hy.2 + refine ⟨Finset.mem_univ _, ?_⟩ + rw [oddCZBucket_eq_square_iff] + refine ⟨mul_ne_zero (inv_ne_zero hc0) hy0, ?_⟩ + rw [mul_pow, inv_pow, hcm, hym] + exact inv_mul_cancel₀ (neg_ne_zero.mpr one_ne_zero) + · intro x _ + show c⁻¹ * (c * x) = x + rw [← _root_.mul_assoc, inv_mul_cancel₀ hc0, _root_.one_mul] + · intro y _ + show c * (c⁻¹ * y) = y + rw [← _root_.mul_assoc, mul_inv_cancel₀ hc0, _root_.one_mul] + +/-- The square Euler bucket of an odd field of cardinality `q` has `(q - 1) / 2` elements. -/ +theorem card_oddCZBucket_square {F : Type*} + [Field F] [Fintype F] [BEq F] [LawfulBEq F] + {q : Nat} (hodd : q % 2 = 1) (hcard : Fintype.card F = q) : + (Finset.univ.filter fun x : F ↦ oddCZBucket q x = OddCZBucket.square).card = + (q - 1) / 2 := by + have hadd := card_oddCZBucket_square_add_nonsquare (F := F) hcard + have heq := card_oddCZBucket_square_eq_nonsquare hodd hcard + have hq2 := two_le_q_of_card_eq hcard + omega + +/-- The nonsquare Euler bucket of an odd field of cardinality `q` has `(q - 1) / 2` elements. -/ +theorem card_oddCZBucket_nonsquare {F : Type*} + [Field F] [Fintype F] [BEq F] [LawfulBEq F] + {q : Nat} (hodd : q % 2 = 1) (hcard : Fintype.card F = q) : + (Finset.univ.filter fun x : F ↦ oddCZBucket q x = OddCZBucket.nonsquare).card = + (q - 1) / 2 := by + have hadd := card_oddCZBucket_square_add_nonsquare (F := F) hcard + have heq := card_oddCZBucket_square_eq_nonsquare hodd hcard + have hq2 := two_le_q_of_card_eq hcard + omega + +/-- +Two independent uniform field values land in different Euler buckets with +probability at least `1 / 2`. + +Every Euler bucket has at most `(q - 1) / 2` elements, so for each first value +at least `q - (q - 1) / 2 = (q + 1) / 2` choices of the second value land in a +different bucket, giving separation probability at least `(q + 1) / (2 * q)`. +-/ +theorem oddCZBucket_pair_separated_probability_ge_half {F : Type*} + [Field F] [Fintype F] [BEq F] [LawfulBEq F] + {q : Nat} (enumeration : UniformFieldEnumeration F q) + (hodd : q % 2 = 1) (hcard : Fintype.card F = q) : + (2 : ℝ≥0∞)⁻¹ ≤ + eventProbability + ((uniformFieldElementPMF enumeration.toFieldEnumeration).bind + fun x ↦ (uniformFieldElementPMF enumeration.toFieldEnumeration).map + fun y ↦ (x, y)) + {xy | oddCZBucket q xy.1 ≠ oddCZBucket q xy.2} := by + have hq2 : 2 ≤ q := two_le_q_of_card_eq hcard + apply uniformPair_separated_probability_ge_half_of_fiber_card_le enumeration + (fun x ↦ oddCZBucket q x) hcard (m := (q - 1) / 2) + · intro x + refine ⟨Finset.univ.filter fun y : F ↦ oddCZBucket q y = oddCZBucket q x, + fun y hy ↦ Finset.mem_filter.mpr ⟨Finset.mem_univ y, hy⟩, ?_⟩ + cases hbx : oddCZBucket q x with + | zero => + have hfe := card_oddCZBucket_zero (F := F) q + omega + | square => + rw [card_oddCZBucket_square hodd hcard] + | nonsquare => + rw [card_oddCZBucket_nonsquare hodd hcard] + · omega + +end FiniteField + +end Roots + +end CPolynomial + +end CompPoly diff --git a/CompPoly/Univariate/Roots/LasVegas/Probability/OddTrial.lean b/CompPoly/Univariate/Roots/LasVegas/Probability/OddTrial.lean new file mode 100644 index 00000000..9db1d869 --- /dev/null +++ b/CompPoly/Univariate/Roots/LasVegas/Probability/OddTrial.lean @@ -0,0 +1,372 @@ +/- +Copyright (c) 2026 CompPoly Contributors. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Valerii Huhnin +-/ + +module + +-- `natDegree` is declared in a bare `public section`, so `natDegree 0 = 0` no +-- longer holds by `rfl` downstream; see `docs/wiki/module-system.md`. +import all CompPoly.Univariate.Basic +public import CompPoly.Univariate.Roots.LasVegas.Probability.OddBuckets +public import Mathlib.Algebra.Polynomial.Roots +public import Mathlib.Algebra.Squarefree.Basic + +/-! +# One Odd Cantor-Zassenhaus Trial Under Uniform Probes + +The single-trial probability surface for odd-field Las Vegas splitting: the +trial PMF induced by uniform probes, the half-success model, and the theorem +that uniform probes really achieve success probability at least `1 / 2` for +squarefree root products with at least two distinct roots. +-/ + +@[expose] public section + +open scoped Classical ENNReal NNReal BigOperators + +namespace CompPoly + +namespace CPolynomial + +namespace Roots + +namespace FiniteField + +/-- The factor has at least two distinct field roots. -/ +def HasTwoDistinctRoots {F : Type*} [Semiring F] (g : CPolynomial F) : Prop := + ∃ a b : F, a ≠ b ∧ CPolynomial.eval a g = 0 ∧ CPolynomial.eval b g = 0 + +/-- +Probability-facing root-product model for the recursive fallback analysis: the +factor is a nonzero squarefree product of linear factors over the field, with +as many roots as its degree, dividing the Frobenius polynomial `X ^ q - X`. + +This intentionally strengthens the runtime splitter contract +`lasVegasSplitterInput` instead of overloading it with probability details. +-/ +structure RootProductProbabilityInput {F : Type*} [Field F] [BEq F] [LawfulBEq F] + (q : Nat) (g : CPolynomial F) : Prop where + nonzero : g ≠ 0 + splits_over_field : + g.toPoly ∣ ((Polynomial.X : Polynomial F) ^ q - Polynomial.X) + squarefree : Squarefree g.toPoly + roots_card_eq_degree : g.toPoly.roots.card = g.toPoly.natDegree + +/-- +A squarefree root product of degree at least two has two distinct field roots. +-/ +theorem RootProductProbabilityInput.hasTwoDistinctRoots {F : Type*} + [Field F] [BEq F] [LawfulBEq F] {q : Nat} {g : CPolynomial F} + (hinput : RootProductProbabilityInput q g) + (hdegree : 2 ≤ CPolynomial.natDegree g) : + HasTwoDistinctRoots g := by + have hcard2 : 2 ≤ g.toPoly.roots.card := by + rw [hinput.roots_card_eq_degree, ← CPolynomial.natDegree_toPoly] + exact hdegree + have hnodup : g.toPoly.roots.Nodup := by + rw [Multiset.nodup_iff_count_le_one] + intro z + rw [Polynomial.count_roots] + by_contra hgt + have hsq : (Polynomial.X - Polynomial.C z) * (Polynomial.X - Polynomial.C z) ∣ + g.toPoly := by + have hpow : (Polynomial.X - Polynomial.C z) ^ 2 ∣ g.toPoly := + (pow_dvd_pow _ (by omega)).trans (Polynomial.pow_rootMultiplicity_dvd g.toPoly z) + rwa [_root_.pow_two] at hpow + exact Polynomial.not_isUnit_X_sub_C z (hinput.squarefree _ hsq) + have hfin : 1 < g.toPoly.roots.toFinset.card := by + rw [Multiset.toFinset_card_of_nodup hnodup] + omega + obtain ⟨a, ha, b, hb, hab⟩ := Finset.one_lt_card.mp hfin + have hroot : ∀ z ∈ g.toPoly.roots.toFinset, CPolynomial.eval z g = 0 := by + intro z hz + rw [CPolynomial.eval_toPoly] + exact (Polynomial.mem_roots'.mp (Multiset.mem_toFinset.mp hz)).2 + exact ⟨a, b, hab, hroot a ha, hroot b hb⟩ + +/-- +Over a field of cardinality `q`, the runtime splitter contract already implies +the full probability-facing root-product model: every divisor of the Frobenius +polynomial `X ^ q - X` is squarefree and splits with as many roots as its +degree. +-/ +theorem rootProductProbabilityInput_of_lasVegasSplitterInput {F : Type*} + [Field F] [Fintype F] [BEq F] [LawfulBEq F] {q : Nat} {g : CPolynomial F} + (hvalid : lasVegasSplitterInput q g) (hcard : Fintype.card F = q) : + RootProductProbabilityInput q g := by + obtain ⟨hne, hdvd⟩ := hvalid + have hq2 : 2 ≤ q := by + have hlt := Fintype.one_lt_card (α := F) + omega + have hXne : ((Polynomial.X : Polynomial F) ^ q - Polynomial.X) ≠ 0 := + FiniteField.X_pow_card_sub_X_ne_zero F (by omega) + have hXdeg : ((Polynomial.X : Polynomial F) ^ q - Polynomial.X).natDegree = q := + FiniteField.X_pow_card_sub_X_natDegree_eq F (by omega) + have hXroots : ((Polynomial.X : Polynomial F) ^ q - Polynomial.X).roots = + (Finset.univ : Finset F).val := by + rw [← hcard] + exact FiniteField.roots_X_pow_card_sub_X (K := F) + have hXsplits : ((Polynomial.X : Polynomial F) ^ q - Polynomial.X).Splits := by + rw [Polynomial.splits_iff_card_roots, hXroots, hXdeg] + simpa using hcard + have hXsf : Squarefree ((Polynomial.X : Polynomial F) ^ q - Polynomial.X) := by + apply Polynomial.Separable.squarefree + rw [← Polynomial.nodup_roots_iff_of_splits hXne hXsplits, hXroots] + exact (Finset.univ : Finset F).nodup + refine ⟨hne, hdvd, hXsf.squarefree_of_dvd hdvd, ?_⟩ + exact Polynomial.splits_iff_card_roots.mp (hXsplits.of_dvd hXne hdvd) + +/-- +Finite-field root products satisfy the probability-facing root-product model. +This is the adapter from the concrete `finiteFieldRootProductWith` construction +to the hypotheses of the half-success and recursive fallback theorems. +-/ +theorem finiteFieldRootProductWith_rootProductProbabilityInput {F : Type*} + [Field F] [Fintype F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + (ctx : FiniteFieldContext F) {p : CPolynomial F} (hp : p ≠ 0) : + RootProductProbabilityInput ctx.q (finiteFieldRootProductWith M D ctx p) := by + apply rootProductProbabilityInput_of_lasVegasSplitterInput + (finiteFieldRootProductWith_lasVegasSplitterInput M D ctx hp) + rw [← Nat.card_eq_fintype_card] + exact ctx.card_eq + +/-- Default-backend root products satisfy the probability-facing model. -/ +theorem finiteFieldRootProduct_rootProductProbabilityInput {F : Type*} + [Field F] [Fintype F] [BEq F] [LawfulBEq F] + (ctx : FiniteFieldContext F) {p : CPolynomial F} (hp : p ≠ 0) : + RootProductProbabilityInput ctx.q (finiteFieldRootProduct ctx p) := + finiteFieldRootProductWith_rootProductProbabilityInput + CPolynomial.Raw.MulContext.naive CPolynomial.Raw.ModContext.naive ctx hp + +/-- +The algebraic validity expected from a successful randomized split: nontrivial +proper children, divisibility by the parent, and root preservation. +-/ +def IsProperRootPreservingSplit {F : Type*} [Field F] [BEq F] [LawfulBEq F] + (parent : CPolynomial F) (children : Array (CPolynomial F)) : Prop := + 2 ≤ children.size ∧ + (∀ child, child ∈ children.toList → + child ≠ 0 ∧ child ≠ 1 ∧ child.val.size < parent.val.size ∧ + child.toPoly ∣ parent.toPoly) ∧ + (∀ a : F, CPolynomial.eval a parent = 0 → + ∃ child, child ∈ children.toList ∧ CPolynomial.eval a child = 0) + +/-- One odd-field Cantor-Zassenhaus trial under a uniform probe distribution. -/ +noncomputable def oddSplitTrialPMF {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + (enumeration : FieldEnumeration F) (q coefficientCount : Nat) + (g : CPolynomial F) (attempt : Nat) : + PMF (TrialResult F) := + (uniformProbePMF enumeration coefficientCount).map fun h ↦ + match cantorZassenhausOddAttemptWith M D q + ({ probe := fun _q _factor _attempt ↦ h } : ProbeFamily F) g attempt with + | some children => TrialResult.split children + | none => TrialResult.failed + +/-- A successful trial in the support of the PMF is an algebraically valid split. -/ +theorem oddSplitTrialPMF_support_success_valid {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + (enumeration : FieldEnumeration F) (q coefficientCount : Nat) + (g : CPolynomial F) (attempt : Nat) {children : Array (CPolynomial F)} + (hmem : TrialResult.split children ∈ + (oddSplitTrialPMF M D enumeration q coefficientCount g attempt).support) : + IsProperRootPreservingSplit g children := by + rw [oddSplitTrialPMF, PMF.mem_support_map_iff] at hmem + rcases hmem with ⟨probe, _hprobe, htrial⟩ + let probes : ProbeFamily F := { probe := fun _q _factor _attempt ↦ probe } + cases htry : cantorZassenhausOddAttemptWith M D q probes g attempt with + | none => + simp [probes, htry] at htrial + | some splitChildren => + simp [probes, htry] at htrial + subst children + refine ⟨?_, ?_, ?_⟩ + · simpa using cantorZassenhausOddAttemptWith_size_ge_two M D q probes htry + · intro child hchild + have hproper := cantorZassenhausOddAttemptWith_child_proper M D q probes htry hchild + unfold isNontrivialProperChild at hproper + simp at hproper + have hnormSizeLe : (CPolynomial.monicNormalize g).val.size ≤ g.val.size := + monicNormalize_size_le_self g + exact ⟨hproper.1.1, hproper.1.2, by omega, + cantorZassenhausOddAttemptWith_child_dvd_input M D q probes htry hchild⟩ + · intro a hroot + exact cantorZassenhausOddAttemptWith_root_preserved M D q probes htry hroot + +/-- +Model predicate for the finite-field counting argument behind one odd-field +Cantor-Zassenhaus trial. +-/ +def OddSplitTrialHalfSuccessModel {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + {q : Nat} (enumeration : UniformFieldEnumeration F q) + (g : CPolynomial F) (attempt : Nat) : Prop := + (2 : ℝ≥0∞)⁻¹ ≤ + trialSuccessProbability + (oddSplitTrialPMF M D enumeration.toFieldEnumeration q + (CPolynomial.natDegree g) g attempt) + +/-- +Uniform probe polynomials really make one odd Cantor-Zassenhaus attempt succeed +with probability at least `1 / 2`. + +The proof combines pair-evaluation uniformity of `uniformProbePMF`, the Euler +bucket separation probability, and the deterministic theorem that a probe whose +buckets separate two roots forces the executable attempt to succeed. + +The hypothesis `hdegree` is essential and not implied by `hroots`: for `g = 0` +every pair of points consists of roots, yet the trial samples zero-coefficient +probes and always fails. +-/ +theorem oddSplitTrial_success_probability_ge_half_of_two_le {F : Type*} + [Field F] [Fintype F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + {q : Nat} (enumeration : UniformFieldEnumeration F q) + (g : CPolynomial F) (coefficientCount attempt : Nat) + (hcount : 2 ≤ coefficientCount) + (hfield : OddUniformFieldModel F q enumeration) + (hroots : HasTwoDistinctRoots g) + (hg : g ≠ 0) : + (2 : ℝ≥0∞)⁻¹ ≤ + trialSuccessProbability + (oddSplitTrialPMF M D enumeration.toFieldEnumeration q coefficientCount + g attempt) := by + obtain ⟨a, b, hab, hra, hrb⟩ := hroots + have hpush_eq := uniformProbePMF_map_eval_pair enumeration coefficientCount hab hcount + -- Bucket-separated probes force the executable attempt to succeed. + rw [trialSuccessProbability, eventProbability, oddSplitTrialPMF, + PMF.toOuterMeasure_map_apply] + have hsubset : {h : CPolynomial F | + oddCZBucket q (CPolynomial.eval a h) ≠ oddCZBucket q (CPolynomial.eval b h)} ⊆ + (fun h : CPolynomial F ↦ + match cantorZassenhausOddAttemptWith M D q + ({ probe := fun _q _factor _attempt ↦ h } : ProbeFamily F) g attempt with + | some children => TrialResult.split children + | none => TrialResult.failed) ⁻¹' {trial | trial.IsSuccess} := by + intro h hsep + obtain ⟨children, hchildren⟩ := + cantorZassenhausOddAttemptWith_success_of_bucket_separated M D q attempt hg + hra hrb hsep + simp only [Set.mem_preimage, Set.mem_ofPred_eq, hchildren] + exact trivial + have hpre : ((fun h : CPolynomial F ↦ + (CPolynomial.eval a h, CPolynomial.eval b h)) ⁻¹' + {xy : F × F | oddCZBucket q xy.1 ≠ oddCZBucket q xy.2}) = + {h : CPolynomial F | + oddCZBucket q (CPolynomial.eval a h) ≠ oddCZBucket q (CPolynomial.eval b h)} := rfl + have hge : (2 : ℝ≥0∞)⁻¹ ≤ + (uniformProbePMF enumeration.toFieldEnumeration + coefficientCount).toOuterMeasure + {h : CPolynomial F | + oddCZBucket q (CPolynomial.eval a h) ≠ oddCZBucket q (CPolynomial.eval b h)} := by + rw [← hpre, ← PMF.toOuterMeasure_map_apply, hpush_eq] + exact oddCZBucket_pair_separated_probability_ge_half enumeration + hfield.q_odd hfield.card_eq + exact le_trans hge ((uniformProbePMF enumeration.toFieldEnumeration + coefficientCount).toOuterMeasure.mono hsubset) + +/-- +Random affine probes with both coefficients drawn uniformly and independently +already achieve the half-success bound: the evaluation pair at two distinct +roots covers all of `F × F`, so no higher-degree probes are needed. +-/ +theorem oddSplitTrial_success_probability_ge_half_linear {F : Type*} + [Field F] [Fintype F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + {q : Nat} (enumeration : UniformFieldEnumeration F q) + (g : CPolynomial F) (attempt : Nat) + (hfield : OddUniformFieldModel F q enumeration) + (hroots : HasTwoDistinctRoots g) + (hg : g ≠ 0) : + (2 : ℝ≥0∞)⁻¹ ≤ + trialSuccessProbability + (oddSplitTrialPMF M D enumeration.toFieldEnumeration q 2 g attempt) := + oddSplitTrial_success_probability_ge_half_of_two_le M D enumeration g 2 attempt + le_rfl hfield hroots hg + +/-- +Uniform probe polynomials really make one odd Cantor-Zassenhaus attempt succeed +with probability at least `1 / 2`. + +The hypothesis `hdegree` is essential and not implied by `hroots`: for `g = 0` +every pair of points consists of roots, yet the trial samples zero-coefficient +probes and always fails. +-/ +theorem oddSplitTrialHalfSuccessModel_of_uniformProbe {F : Type*} + [Field F] [Fintype F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + {q : Nat} (enumeration : UniformFieldEnumeration F q) + (g : CPolynomial F) (attempt : Nat) + (hfield : OddUniformFieldModel F q enumeration) + (hroots : HasTwoDistinctRoots g) + (hdegree : 2 ≤ CPolynomial.natDegree g) : + OddSplitTrialHalfSuccessModel M D enumeration g attempt := by + have hg : g ≠ 0 := by + intro hzero + rw [hzero] at hdegree + have hnat : CPolynomial.natDegree (0 : CPolynomial F) = 0 := rfl + omega + unfold OddSplitTrialHalfSuccessModel + exact oddSplitTrial_success_probability_ge_half_of_two_le M D enumeration g + (CPolynomial.natDegree g) attempt hdegree hfield hroots hg + +/-- +For a squarefree finite-field root product over an odd field, one uniform +Cantor-Zassenhaus trial succeeds with probability at least `1 / 2`. + +Unlike the historical model-assisted shape (kept below as +`oddSplitTrial_success_probability_ge_half_of_model`), this theorem no longer +accepts the probability bound itself as an input. +-/ +theorem oddSplitTrial_success_probability_ge_half {F : Type*} + [Field F] [Fintype F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + {q : Nat} (enumeration : UniformFieldEnumeration F q) + (g : CPolynomial F) (attempt : Nat) + (hfield : OddUniformFieldModel F q enumeration) + (hrootProduct : RootProductProbabilityInput q g) + (hdegree : 2 ≤ CPolynomial.natDegree g) : + (2 : ℝ≥0∞)⁻¹ ≤ + trialSuccessProbability + (oddSplitTrialPMF M D enumeration.toFieldEnumeration q + (CPolynomial.natDegree g) g attempt) := + oddSplitTrialHalfSuccessModel_of_uniformProbe M D enumeration g attempt + hfield (hrootProduct.hasTwoDistinctRoots hdegree) hdegree + +/-- +Compatibility shape of the half-success theorem that still accepts the +half-success model as an assumption. Prefer +`oddSplitTrial_success_probability_ge_half`, which derives the model from the +uniform probe source. +-/ +theorem oddSplitTrial_success_probability_ge_half_of_model {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + {q : Nat} (enumeration : UniformFieldEnumeration F q) + (g : CPolynomial F) (attempt : Nat) + (hvalid : lasVegasSplitterInput q g) + (hodd : q % 2 = 1) + (hdegree : 2 ≤ CPolynomial.natDegree g) + (hmodel : lasVegasSplitterInput q g → + q % 2 = 1 → + 2 ≤ CPolynomial.natDegree g → + OddSplitTrialHalfSuccessModel M D enumeration g attempt) : + (2 : ℝ≥0∞)⁻¹ ≤ + trialSuccessProbability + (oddSplitTrialPMF M D enumeration.toFieldEnumeration q + (CPolynomial.natDegree g) g attempt) := by + exact hmodel hvalid hodd hdegree + +end FiniteField + +end Roots + +end CPolynomial + +end CompPoly diff --git a/CompPoly/Univariate/Roots/LasVegas/Probability/Recursive.lean b/CompPoly/Univariate/Roots/LasVegas/Probability/Recursive.lean new file mode 100644 index 00000000..a8cc6752 --- /dev/null +++ b/CompPoly/Univariate/Roots/LasVegas/Probability/Recursive.lean @@ -0,0 +1,927 @@ +/- +Copyright (c) 2026 CompPoly Contributors. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Valerii Huhnin +-/ + +module + +-- `natDegree` and friends are declared in bare `public section`s, so their bodies +-- are opaque downstream; see `docs/wiki/module-system.md`. +import all CompPoly.Univariate.Basic +public import CompPoly.Univariate.Roots.LasVegas.Probability.EvenTrace +public import CompPoly.Univariate.Roots.LasVegas.Probability.OddTrial + +/-! +# Recursive Fallback Analysis for Las Vegas Splitting + +The abstract binomial-tail bound for recursive fallback, together with the +multi-factor splitting process that witnesses it: the process tracks the full +stack of unresolved factors, gives every splittable factor its own batch of +independent uniform probes, and records which factors fell back to enumeration. +-/ + +@[expose] public section + +open scoped Classical ENNReal NNReal BigOperators + +namespace CompPoly + +namespace CPolynomial + +namespace Roots + +namespace FiniteField + +/-- +Binomial-tail expression used by the planned full-recursion fallback bound. +`Finset.range (degree - 1)` indexes `0, ..., degree - 2`. +-/ +noncomputable def binomialFallbackTail (attempts degree : Nat) : ℝ≥0∞ := + ((2 : ℝ≥0∞)⁻¹) ^ attempts * + ((Finset.range (degree - 1)).sum fun j ↦ (Nat.choose attempts j : ℝ≥0∞)) + +/-- A cutoff meets the binomial-tail target for field cardinality `q`. -/ +def binomialTailCutoffPredicate (q degree attempts : Nat) : Prop := + binomialFallbackTail attempts degree ≤ (q : ℝ≥0∞)⁻¹ + +/-- +Model predicate for the planned recursive splitter: it packages the future +independence and root-product hypotheses needed for the binomial fallback tail. +-/ +def RecursiveFallbackProbabilityModel {Ω : Type*} + (process : PMF Ω) (fallbackEvent : Set Ω) + (attempts degree : Nat) : Prop := + ∃ badRankEvent : Nat → Set Ω, + fallbackEvent ⊆ ⋃ j, badRankEvent j ∧ + (∀ j, j ∉ Finset.range (degree - 1) → + eventProbability process (badRankEvent j) = 0) ∧ + (∀ j, j ∈ Finset.range (degree - 1) → + eventProbability process (badRankEvent j) ≤ + ((2 : ℝ≥0∞)⁻¹) ^ attempts * (Nat.choose attempts j : ℝ≥0∞)) + +/-- +Target theorem for the full recursive splitter: under the independence and +root-product hypotheses represented by the future recursion model, fallback is +bounded by the binomial tail. +-/ +theorem recursiveFallback_probability_le_binomialTail {Ω : Type*} + (process : PMF Ω) (fallbackEvent : Set Ω) + (attempts degree : Nat) + (hmodel : RecursiveFallbackProbabilityModel process fallbackEvent attempts degree) : + eventProbability process fallbackEvent ≤ binomialFallbackTail attempts degree := by + rcases hmodel with ⟨badRankEvent, hcover, hzero, hbound⟩ + let half : ℝ≥0∞ := (2 : ℝ≥0∞)⁻¹ + calc + eventProbability process fallbackEvent + ≤ eventProbability process (⋃ j, badRankEvent j) := by + simpa [eventProbability] using process.toOuterMeasure.mono hcover + _ ≤ ∑' j, eventProbability process (badRankEvent j) := by + simpa [eventProbability] using + (MeasureTheory.measure_iUnion_le (μ := process.toOuterMeasure) badRankEvent) + _ ≤ ∑' j : Nat, + if j ∈ Finset.range (degree - 1) then + half ^ attempts * (Nat.choose attempts j : ℝ≥0∞) + else + 0 := by + apply ENNReal.tsum_le_tsum + intro j + by_cases hj : j ∈ Finset.range (degree - 1) + · simpa [half, hj] using hbound j hj + · simp [hj, hzero j hj] + _ = binomialFallbackTail attempts degree := by + unfold binomialFallbackTail + rw [tsum_eq_sum (s := Finset.range (degree - 1))] + · trans (Finset.range (degree - 1)).sum + (fun j ↦ half ^ attempts * (Nat.choose attempts j : ℝ≥0∞)) + · apply Finset.sum_congr rfl + intro j hj + simp [hj] + · simp [half, Finset.mul_sum] + · intro j hj + simp [hj] + +theorem toOuterMeasure_apply_le_one {α : Type*} (p : PMF α) (s : Set α) : + p.toOuterMeasure s ≤ 1 := by + rw [PMF.toOuterMeasure_apply] + calc (∑' a, s.indicator (⇑p) a) + ≤ ∑' a, p a := ENNReal.tsum_le_tsum fun a ↦ Set.indicator_le_self s (⇑p) a + _ = 1 := p.tsum_coe + +/-- First successful split among independent trials, or `none` after exhaustion. -/ +noncomputable def firstSuccessPMF {F : Type*} [Zero F] + (trial : Nat → PMF (TrialResult F)) : + Nat → Nat → PMF (Option (Array (CPolynomial F))) + | 0, _offset => PMF.pure none + | attempts + 1, offset => + (trial offset).bind fun + | TrialResult.split children => PMF.pure (some children) + | TrialResult.failed => firstSuccessPMF trial attempts (offset + 1) + +/-- Trials that succeed at rate one half are all survived with probability +at most `2 ^ -attempts`. -/ +theorem firstSuccessPMF_apply_none_le_geometric {F : Type*} [Zero F] + (trial : Nat → PMF (TrialResult F)) : + ∀ attempts offset, + (∀ i, i < attempts → + (2 : ℝ≥0∞)⁻¹ ≤ trialSuccessProbability (trial (offset + i))) → + (firstSuccessPMF trial attempts offset) none ≤ ((2 : ℝ≥0∞)⁻¹) ^ attempts := by + intro attempts + induction attempts with + | zero => + intro offset _hstep + rw [firstSuccessPMF] + simp + | succ attempts ih => + intro offset hstep + rw [firstSuccessPMF, PMF.bind_apply] + trans ((trial offset) TrialResult.failed * + (firstSuccessPMF trial attempts (offset + 1)) none) + · refine le_of_eq (tsum_eq_single TrialResult.failed fun t ht ↦ ?_) + cases t with + | split children => simp + | failed => exact absurd rfl ht + · have hfail : (trial offset) TrialResult.failed ≤ (2 : ℝ≥0∞)⁻¹ := by + rw [← trialFailureProbability_eq_failed_mass] + exact trialFailureProbability_le_half_of_success _ (hstep 0 (by omega)) + have hrest := ih (offset + 1) (by + intro i hi + have h := hstep (i + 1) (by omega) + simpa [Nat.add_assoc, Nat.add_comm, Nat.add_left_comm] using h) + refine le_trans (mul_le_mul' hfail hrest) (le_of_eq ?_) + rw [pow_succ, _root_.mul_comm] + +/-- A successful batch outcome lies in the support of some individual trial. -/ +theorem firstSuccessPMF_support_some {F : Type*} [Zero F] + (trial : Nat → PMF (TrialResult F)) : + ∀ attempts offset {children : Array (CPolynomial F)}, + some children ∈ (firstSuccessPMF trial attempts offset).support → + ∃ attempt, TrialResult.split children ∈ (trial attempt).support := by + intro attempts + induction attempts with + | zero => + intro offset children hmem + rw [firstSuccessPMF] at hmem + have heq := (PMF.mem_support_pure_iff _ _).mp hmem + exact absurd heq (by simp) + | succ attempts ih => + intro offset children hmem + rw [firstSuccessPMF, PMF.mem_support_bind_iff] at hmem + obtain ⟨t, ht, hmem2⟩ := hmem + cases t with + | split c => + have hc : some children = some c := (PMF.mem_support_pure_iff _ _).mp hmem2 + have hceq : children = c := by + injection hc with hc' + rw [← hceq] at ht + exact ⟨offset, ht⟩ + | failed => exact ih (offset + 1) hmem2 + +/-- +Facts a split trial guarantees about successful children: at least two of them, +each a nonzero nonconstant divisor of the parent, with total split work +strictly below the parent's. +-/ +def IsSplitStep {F : Type*} [Field F] [BEq F] [LawfulBEq F] + (g : CPolynomial F) (children : Array (CPolynomial F)) : Prop := + 2 ≤ children.size ∧ + (∀ child ∈ children.toList, + child ≠ 0 ∧ child.toPoly ∣ g.toPoly ∧ 1 ≤ child.toPoly.natDegree) ∧ + stackWork children.toList ≤ splitWork g - 1 + +/-- Number of splits a stack of unresolved factors can still require. -/ +noncomputable def stackSplitBudget {F : Type*} [Field F] [BEq F] [LawfulBEq F] : + List (CPolynomial F) → Nat + | [] => 0 + | g :: stack => (g.toPoly.natDegree - 1) + stackSplitBudget stack + +theorem stackSplitBudget_append {F : Type*} [Field F] [BEq F] [LawfulBEq F] + (xs ys : List (CPolynomial F)) : + stackSplitBudget (xs ++ ys) = stackSplitBudget xs + stackSplitBudget ys := by + induction xs with + | nil => simp [stackSplitBudget] + | cons g xs ih => + simp only [List.cons_append, stackSplitBudget, ih] + omega + +theorem stackWork_eq_two_mul_budget_add_length {F : Type*} + [Field F] [BEq F] [LawfulBEq F] (l : List (CPolynomial F)) + (hpos : ∀ g ∈ l, 1 ≤ g.toPoly.natDegree) : + stackWork l = 2 * stackSplitBudget l + l.length := by + induction l with + | nil => simp [stackWork, stackSplitBudget] + | cons g l ih => + have hg := hpos g (by simp) + have hl := ih fun x hx ↦ hpos x (by simp [hx]) + simp only [stackWork, stackSplitBudget, splitWork, List.length_cons] + omega + +/-- +Multi-factor Las Vegas splitting process for an abstract per-factor trial +family. + +The stack of unresolved factors is processed sequentially. Every factor of +degree at least two receives its own batch of `attempts` independent trials; +the resulting trace records, in processing order, whether each such factor +fell back to enumeration (`true`) or split into children that are pushed back +onto the stack (`false`). This is the idealized-probe model of +`lasVegasSplitLoopWith`, which retries each unresolved factor up to the +configured cutoff before enumerating it. +-/ +noncomputable def recursiveSplitProcessPMF {F : Type*} [Field F] [BEq F] [LawfulBEq F] + (trial : CPolynomial F → Nat → PMF (TrialResult F)) (attempts : Nat) : + Nat → List (CPolynomial F) → PMF (List Bool) + | 0, _stack => PMF.pure [] + | _fuel + 1, [] => PMF.pure [] + | fuel + 1, g :: stack => + if CPolynomial.natDegree g < 2 then + recursiveSplitProcessPMF trial attempts fuel stack + else + (firstSuccessPMF (trial g) attempts 0).bind fun + | none => + (recursiveSplitProcessPMF trial attempts fuel stack).map (List.cons true) + | some children => + (recursiveSplitProcessPMF trial attempts fuel + (children.toList ++ stack)).map (List.cons false) + +/-- Process traces never exceed the split budget of the initial stack. -/ +theorem recursiveSplitProcessPMF_support_length {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (trial : CPolynomial F → Nat → PMF (TrialResult F)) (attempts : Nat) + (hsplit : ∀ (g : CPolynomial F), g ≠ 0 → + ∀ (attempt : Nat) (children : Array (CPolynomial F)), + TrialResult.split children ∈ (trial g attempt).support → + IsSplitStep g children) : + ∀ fuel (stack : List (CPolynomial F)) {tr2 : List Bool}, + tr2 ∈ (recursiveSplitProcessPMF trial attempts fuel stack).support → + tr2.length ≤ stackSplitBudget stack := by + intro fuel + induction fuel with + | zero => + intro stack tr2 hmem + rw [recursiveSplitProcessPMF] at hmem + have heq := (PMF.mem_support_pure_iff _ _).mp hmem + subst heq + simp + | succ fuel ih => + intro stack tr2 hmem + cases stack with + | nil => + rw [recursiveSplitProcessPMF] at hmem + have heq := (PMF.mem_support_pure_iff _ _).mp hmem + subst heq + simp + | cons g stack => + rw [recursiveSplitProcessPMF] at hmem + by_cases hdeg : CPolynomial.natDegree g < 2 + · rw [if_pos hdeg] at hmem + refine le_trans (ih stack hmem) ?_ + simp only [stackSplitBudget] + omega + · rw [if_neg hdeg] at hmem + rw [PMF.mem_support_bind_iff] at hmem + obtain ⟨o, ho, hmem2⟩ := hmem + have hdeg2 : 2 ≤ g.toPoly.natDegree := by + have h := Nat.le_of_not_lt hdeg + rwa [CPolynomial.natDegree_toPoly] at h + cases o with + | none => + rw [PMF.mem_support_map_iff] at hmem2 + obtain ⟨tr, htr, heq⟩ := hmem2 + subst heq + have hlen := ih stack htr + simp only [stackSplitBudget, List.length_cons] + omega + | some children => + rw [PMF.mem_support_map_iff] at hmem2 + obtain ⟨tr, htr, heq⟩ := hmem2 + subst heq + have hlen := ih (children.toList ++ stack) htr + rw [stackSplitBudget_append] at hlen + have hg : g ≠ 0 := by + intro h0 + apply hdeg + rw [h0] + have hzero : CPolynomial.natDegree (0 : CPolynomial F) = 0 := rfl + omega + obtain ⟨att, hmemt⟩ := firstSuccessPMF_support_some (trial g) attempts 0 ho + obtain ⟨hsize2, hchild, hwork⟩ := hsplit g hg att children hmemt + have hsw := stackWork_eq_two_mul_budget_add_length children.toList + fun c hc ↦ (hchild c hc).2.2 + have hlen2 : children.toList.length = children.size := by simp + simp only [splitWork] at hwork + simp only [stackSplitBudget, List.length_cons] + omega + +/-- +Each processed factor falls back with probability at most `2 ^ -attempts`: +the `j`-th entry of the process trace is `true` with at most that probability. +-/ +theorem recursiveSplitProcessPMF_rank_le_geometric {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (trial : CPolynomial F → Nat → PMF (TrialResult F)) {q : Nat} (attempts : Nat) + (hsplit : ∀ (g : CPolynomial F), g ≠ 0 → + ∀ (attempt : Nat) (children : Array (CPolynomial F)), + TrialResult.split children ∈ (trial g attempt).support → + IsSplitStep g children) + (hhalf : ∀ (g : CPolynomial F), lasVegasSplitterInput q g → + 2 ≤ CPolynomial.natDegree g → ∀ attempt : Nat, + (2 : ℝ≥0∞)⁻¹ ≤ trialSuccessProbability (trial g attempt)) : + ∀ fuel (stack : List (CPolynomial F)) (j : Nat), + (∀ g ∈ stack, lasVegasSplitterInput q g) → + eventProbability + (recursiveSplitProcessPMF trial attempts fuel stack) + {tr2 : List Bool | tr2[j]? = some true} ≤ + ((2 : ℝ≥0∞)⁻¹) ^ attempts := by + intro fuel + induction fuel with + | zero => + intro stack j _hstack + rw [recursiveSplitProcessPMF, eventProbability, PMF.toOuterMeasure_pure_apply, + if_neg (by simp)] + exact zero_le + | succ fuel ih => + intro stack j hstack + cases stack with + | nil => + rw [recursiveSplitProcessPMF, eventProbability, PMF.toOuterMeasure_pure_apply, + if_neg (by simp)] + exact zero_le + | cons g stack => + rw [recursiveSplitProcessPMF] + by_cases hdeg : CPolynomial.natDegree g < 2 + · rw [if_pos hdeg] + exact ih stack j fun x hx ↦ hstack x (by simp [hx]) + · rw [if_neg hdeg] + rw [eventProbability, PMF.toOuterMeasure_bind_apply] + have hvalid := hstack g (by simp) + have hdeg2 : 2 ≤ CPolynomial.natDegree g := Nat.le_of_not_lt hdeg + have hnone : (firstSuccessPMF (trial g) attempts 0) none ≤ + ((2 : ℝ≥0∞)⁻¹) ^ attempts := + firstSuccessPMF_apply_none_le_geometric (trial g) attempts 0 fun i _hi ↦ + hhalf g hvalid hdeg2 (0 + i) + cases j with + | zero => + trans ((firstSuccessPMF (trial g) attempts 0) none * + ((recursiveSplitProcessPMF trial attempts fuel stack).map + (List.cons true)).toOuterMeasure + {tr2 : List Bool | tr2[0]? = some true}) + · refine le_of_eq (tsum_eq_single none fun o ho ↦ ?_) + cases o with + | none => exact absurd rfl ho + | some children => + have hzero : ((recursiveSplitProcessPMF trial attempts fuel + (children.toList ++ stack)).map (List.cons false)).toOuterMeasure + {tr2 : List Bool | tr2[0]? = some true} = 0 := by + rw [PMF.toOuterMeasure_map_apply] + have hpre : (List.cons false ⁻¹' + {tr2 : List Bool | tr2[0]? = some true}) = ∅ := by + ext tr + simp + rw [hpre] + simp + rw [hzero, MulZeroClass.mul_zero] + · refine le_trans (mul_le_mul' le_rfl (toOuterMeasure_apply_le_one _ _)) ?_ + rw [MulOneClass.mul_one] + exact hnone + | succ j' => + trans (∑' o : Option (Array (CPolynomial F)), + (firstSuccessPMF (trial g) attempts 0) o * ((2 : ℝ≥0∞)⁻¹) ^ attempts) + · apply ENNReal.tsum_le_tsum + intro o + by_cases hzero : (firstSuccessPMF (trial g) attempts 0) o = 0 + · simp [hzero] + · apply mul_le_mul' le_rfl + cases o with + | none => + rw [PMF.toOuterMeasure_map_apply] + have hpre : (List.cons true ⁻¹' + {tr2 : List Bool | tr2[j' + 1]? = some true}) = + {tr2 : List Bool | tr2[j']? = some true} := by + ext tr + simp + rw [hpre] + exact ih stack j' fun x hx ↦ hstack x (by simp [hx]) + | some children => + rw [PMF.toOuterMeasure_map_apply] + have hpre : (List.cons false ⁻¹' + {tr2 : List Bool | tr2[j' + 1]? = some true}) = + {tr2 : List Bool | tr2[j']? = some true} := by + ext tr + simp + rw [hpre] + obtain ⟨att, hmemt⟩ := firstSuccessPMF_support_some (trial g) + attempts 0 ((PMF.mem_support_iff _ _).mpr hzero) + obtain ⟨_hsize2, hchild, _hwork⟩ := hsplit g hvalid.1 att + children hmemt + apply ih (children.toList ++ stack) j' + intro x hx + rw [List.mem_append] at hx + rcases hx with hx | hx + · exact ⟨(hchild x hx).1, (hchild x hx).2.1.trans hvalid.2⟩ + · exact hstack x (by simp [hx]) + · rw [ENNReal.tsum_mul_right, PMF.tsum_coe, _root_.one_mul] + +/-- Trace entries beyond the split budget of the stack never appear. -/ +theorem recursiveSplitProcessPMF_rank_eq_zero_of_budget_le {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (trial : CPolynomial F → Nat → PMF (TrialResult F)) (attempts : Nat) + (hsplit : ∀ (g : CPolynomial F), g ≠ 0 → + ∀ (attempt : Nat) (children : Array (CPolynomial F)), + TrialResult.split children ∈ (trial g attempt).support → + IsSplitStep g children) + (fuel : Nat) (stack : List (CPolynomial F)) {j : Nat} + (hj : stackSplitBudget stack ≤ j) : + eventProbability + (recursiveSplitProcessPMF trial attempts fuel stack) + {tr2 : List Bool | tr2[j]? = some true} = 0 := by + rw [eventProbability, PMF.toOuterMeasure_apply_eq_zero_iff, Set.disjoint_left] + intro tr2 hsupp hmem + have hlen := recursiveSplitProcessPMF_support_length trial attempts hsplit + fuel stack hsupp + rw [Set.mem_ofPred_eq, List.getElem?_eq_none (l := tr2) (by omega)] at hmem + simp at hmem + +/-- +The multi-factor splitting process satisfies the recursive fallback model: +the `j`-th rank event is "the `j`-th processed splittable factor fell back", +there are at most `degree - 1` such factors, and each falls back with +probability at most `2 ^ -attempts ≤ choose attempts j * 2 ^ -attempts`. +-/ +theorem recursiveSplitProcessPMF_recursiveFallbackProbabilityModel {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (trial : CPolynomial F → Nat → PMF (TrialResult F)) {q : Nat} + (g : CPolynomial F) (attempts fuel : Nat) + (hsplit : ∀ (g : CPolynomial F), g ≠ 0 → + ∀ (attempt : Nat) (children : Array (CPolynomial F)), + TrialResult.split children ∈ (trial g attempt).support → + IsSplitStep g children) + (hhalf : ∀ (g : CPolynomial F), lasVegasSplitterInput q g → + 2 ≤ CPolynomial.natDegree g → ∀ attempt : Nat, + (2 : ℝ≥0∞)⁻¹ ≤ trialSuccessProbability (trial g attempt)) + (hvalid : lasVegasSplitterInput q g) + (hattempts : CPolynomial.natDegree g - 2 ≤ attempts) : + RecursiveFallbackProbabilityModel + (recursiveSplitProcessPMF trial attempts fuel [g]) + {tr2 : List Bool | true ∈ tr2} + attempts (CPolynomial.natDegree g) := by + refine ⟨fun j ↦ {tr2 : List Bool | tr2[j]? = some true}, ?_, ?_, ?_⟩ + · intro tr2 htr2 + rw [Set.mem_ofPred_eq] at htr2 + obtain ⟨i, hi⟩ := List.mem_iff_getElem?.mp htr2 + exact Set.mem_iUnion.mpr ⟨i, hi⟩ + · intro j hj + rw [Finset.mem_range, not_lt] at hj + apply recursiveSplitProcessPMF_rank_eq_zero_of_budget_le trial attempts hsplit + have hbudget : stackSplitBudget [g] = g.toPoly.natDegree - 1 := by + simp [stackSplitBudget] + have hdeg := (CPolynomial.natDegree_toPoly g).symm + omega + · intro j hj + rw [Finset.mem_range] at hj + have hrank := recursiveSplitProcessPMF_rank_le_geometric trial attempts hsplit hhalf + fuel [g] j (by + intro x hx + rw [List.mem_singleton] at hx + subst hx + exact hvalid) + refine le_trans hrank ?_ + have hjk : j ≤ attempts := by omega + have hC : (1 : ℝ≥0∞) ≤ (Nat.choose attempts j : ℝ≥0∞) := by + exact_mod_cast Nat.choose_pos hjk + exact le_mul_of_one_le_right' hC + +/-- +Generic recursive fallback bound: any per-factor trial family that succeeds at +rate one half on splitter-valid factors keeps the full recursion's fallback +probability below the binomial tail. +-/ +theorem recursiveSplitProcessPMF_fallback_probability_le_binomialTail {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (trial : CPolynomial F → Nat → PMF (TrialResult F)) {q : Nat} + (g : CPolynomial F) (attempts fuel : Nat) + (hsplit : ∀ (g : CPolynomial F), g ≠ 0 → + ∀ (attempt : Nat) (children : Array (CPolynomial F)), + TrialResult.split children ∈ (trial g attempt).support → + IsSplitStep g children) + (hhalf : ∀ (g : CPolynomial F), lasVegasSplitterInput q g → + 2 ≤ CPolynomial.natDegree g → ∀ attempt : Nat, + (2 : ℝ≥0∞)⁻¹ ≤ trialSuccessProbability (trial g attempt)) + (hvalid : lasVegasSplitterInput q g) + (hattempts : CPolynomial.natDegree g - 2 ≤ attempts) : + eventProbability + (recursiveSplitProcessPMF trial attempts fuel [g]) + {tr2 : List Bool | true ∈ tr2} ≤ + binomialFallbackTail attempts (CPolynomial.natDegree g) := + recursiveFallback_probability_le_binomialTail _ _ _ _ + (recursiveSplitProcessPMF_recursiveFallbackProbabilityModel trial g attempts fuel + hsplit hhalf hvalid hattempts) + +/-- Successful odd split attempts are split steps. -/ +theorem cantorZassenhausOddAttemptWith_isSplitStep {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + (q : Nat) (probes : ProbeFamily F) {g : CPolynomial F} (hg : g ≠ 0) + {attempt : Nat} {children : Array (CPolynomial F)} + (htry : cantorZassenhausOddAttemptWith M D q probes g attempt = some children) : + IsSplitStep g children := by + refine ⟨cantorZassenhausOddAttemptWith_size_ge_two M D q _ htry, ?_, ?_⟩ + · intro child hchild + have hproper := cantorZassenhausOddAttemptWith_child_proper M D q _ htry hchild + have hpos := cantorZassenhausOddAttemptWith_child_normSplitWork_pos M D q _ + htry hg hchild + have hns : normSplitWork child = splitWork child := normSplitWork_eq_splitWork child + rw [hns] at hpos + simp only [splitWork] at hpos + exact ⟨child_ne_zero_of_proper hproper, + cantorZassenhausOddAttemptWith_child_dvd_input M D q _ htry hchild, by omega⟩ + · have hwork := cantorZassenhausOddAttemptWith_stackWork_le M D q _ htry hg + rwa [splitWork_monicNormalize_eq] at hwork + +/-- Successful retry-loop results are split steps. -/ +theorem tryOddSplitAttemptsWith_isSplitStep {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + (q : Nat) (probes : ProbeFamily F) {g : CPolynomial F} (hg : g ≠ 0) + {attempts offset : Nat} {children : Array (CPolynomial F)} + (htry : tryOddSplitAttemptsWith M D q probes g attempts offset = some children) : + IsSplitStep g children := by + obtain ⟨attempt, hatt⟩ := + tryOddSplitAttemptsWith_eq_some_exists_attempt M D q probes attempts offset htry + exact cantorZassenhausOddAttemptWith_isSplitStep M D q probes hg hatt + +/-- Successful odd split trials are split steps. -/ +theorem oddSplitTrialPMF_support_isSplitStep {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + (enumeration : FieldEnumeration F) (q : Nat) {g : CPolynomial F} (hg : g ≠ 0) + {attempt : Nat} {children : Array (CPolynomial F)} + (hmem : TrialResult.split children ∈ + (oddSplitTrialPMF M D enumeration q (CPolynomial.natDegree g) g attempt).support) : + IsSplitStep g children := by + rw [oddSplitTrialPMF, PMF.mem_support_map_iff] at hmem + obtain ⟨probe, _hprobe, heq⟩ := hmem + cases htry : cantorZassenhausOddAttemptWith M D q + ({ probe := fun _q _factor _attempt ↦ probe } : ProbeFamily F) g attempt with + | none => + simp only [htry] at heq + exact absurd heq (by simp) + | some c => + simp only [htry] at heq + have hceq : c = children := by + injection heq with h + exact hceq ▸ cantorZassenhausOddAttemptWith_isSplitStep M D q _ hg htry + +/-- Successful trace split attempts are split steps. -/ +theorem cantorZassenhausEvenTraceAttemptWith_isSplitStep {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + (traceCtx : SmallPrimeTraceContext F) + (q : Nat) (probes : ProbeFamily F) {g : CPolynomial F} (hg : g ≠ 0) + {attempt : Nat} {children : Array (CPolynomial F)} + (htry : cantorZassenhausEvenTraceAttemptWith M D traceCtx q probes g attempt = + some children) : + IsSplitStep g children := by + refine ⟨cantorZassenhausEvenTraceAttemptWith_size_ge_two M D traceCtx q _ htry, ?_, ?_⟩ + · intro child hchild + have hproper := cantorZassenhausEvenTraceAttemptWith_child_proper M D traceCtx q _ + htry hchild + have hpos := cantorZassenhausEvenTraceAttemptWith_child_normSplitWork_pos + M D traceCtx q _ htry hg hchild + have hns : normSplitWork child = splitWork child := normSplitWork_eq_splitWork child + rw [hns] at hpos + simp only [splitWork] at hpos + exact ⟨child_ne_zero_of_proper hproper, + cantorZassenhausEvenTraceAttemptWith_child_dvd_input M D traceCtx q _ + htry hchild, by omega⟩ + · have hwork := cantorZassenhausEvenTraceAttemptWith_stackWork_le M D traceCtx q _ + htry hg + rwa [splitWork_monicNormalize_eq] at hwork + +/-- Successful trace retry-loop results are split steps. -/ +theorem tryEvenTraceSplitAttemptsWith_isSplitStep {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + (traceCtx : SmallPrimeTraceContext F) + (q : Nat) (probes : ProbeFamily F) {g : CPolynomial F} (hg : g ≠ 0) + {attempts offset : Nat} {children : Array (CPolynomial F)} + (htry : tryEvenTraceSplitAttemptsWith M D traceCtx q probes g attempts offset = + some children) : + IsSplitStep g children := by + obtain ⟨attempt, hatt⟩ := + tryEvenTraceSplitAttemptsWith_eq_some_exists_attempt M D traceCtx q probes + attempts offset htry + exact cantorZassenhausEvenTraceAttemptWith_isSplitStep M D traceCtx q probes hg hatt + +/-- Successful trace split trials are split steps. -/ +theorem evenTraceTrialPMF_support_isSplitStep {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + (traceCtx : SmallPrimeTraceContext F) + (enumeration : FieldEnumeration F) (q : Nat) {g : CPolynomial F} (hg : g ≠ 0) + {attempt : Nat} {children : Array (CPolynomial F)} + (hmem : TrialResult.split children ∈ + (evenTraceTrialPMF M D traceCtx enumeration q (CPolynomial.natDegree g) + g attempt).support) : + IsSplitStep g children := by + rw [evenTraceTrialPMF, PMF.mem_support_map_iff] at hmem + obtain ⟨probe, _hprobe, heq⟩ := hmem + cases htry : cantorZassenhausEvenTraceAttemptWith M D traceCtx q + ({ probe := fun _q _factor _attempt ↦ probe } : ProbeFamily F) g attempt with + | none => + simp only [htry] at heq + exact absurd heq (by simp) + | some c => + simp only [htry] at heq + have hceq : c = children := by + injection heq with h + exact hceq ▸ cantorZassenhausEvenTraceAttemptWith_isSplitStep M D traceCtx q _ + hg htry + +/-- Multi-factor odd Cantor-Zassenhaus splitting under uniform probes. -/ +noncomputable def recursiveOddSplitProcessPMF {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + (enumeration : FieldEnumeration F) (q attempts : Nat) : + Nat → List (CPolynomial F) → PMF (List Bool) := + recursiveSplitProcessPMF + (fun g attempt ↦ oddSplitTrialPMF M D enumeration q (CPolynomial.natDegree g) g attempt) + attempts + +/-- The odd multi-factor process satisfies the recursive fallback model. -/ +theorem recursiveOddSplitProcessPMF_recursiveFallbackProbabilityModel {F : Type*} + [Field F] [Fintype F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + {q : Nat} (enumeration : UniformFieldEnumeration F q) + (g : CPolynomial F) (attempts fuel : Nat) + (hfield : OddUniformFieldModel F q enumeration) + (hvalid : lasVegasSplitterInput q g) + (hattempts : CPolynomial.natDegree g - 2 ≤ attempts) : + RecursiveFallbackProbabilityModel + (recursiveOddSplitProcessPMF M D enumeration.toFieldEnumeration q attempts fuel [g]) + {tr2 : List Bool | true ∈ tr2} + attempts (CPolynomial.natDegree g) := + recursiveSplitProcessPMF_recursiveFallbackProbabilityModel _ g attempts fuel + (fun _g hg _att _children hmem ↦ + oddSplitTrialPMF_support_isSplitStep M D enumeration.toFieldEnumeration q hg hmem) + (fun g hvalid hdeg attempt ↦ + oddSplitTrial_success_probability_ge_half M D enumeration g attempt hfield + (rootProductProbabilityInput_of_lasVegasSplitterInput hvalid hfield.card_eq) hdeg) + hvalid hattempts + +/-- +Full recursive fallback bound for the odd branch: starting from a root product +of degree `d`, the probability that any factor anywhere in the recursion falls +back to exhaustive enumeration is at most the binomial tail +`2 ^ -attempts * ∑_{j ≤ d - 2} C(attempts, j)`. +-/ +theorem recursiveOddSplitProcessPMF_fallback_probability_le_binomialTail {F : Type*} + [Field F] [Fintype F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + {q : Nat} (enumeration : UniformFieldEnumeration F q) + (g : CPolynomial F) (attempts fuel : Nat) + (hfield : OddUniformFieldModel F q enumeration) + (hvalid : lasVegasSplitterInput q g) + (hattempts : CPolynomial.natDegree g - 2 ≤ attempts) : + eventProbability + (recursiveOddSplitProcessPMF M D enumeration.toFieldEnumeration q attempts fuel [g]) + {tr2 : List Bool | true ∈ tr2} ≤ + binomialFallbackTail attempts (CPolynomial.natDegree g) := + recursiveFallback_probability_le_binomialTail _ _ _ _ + (recursiveOddSplitProcessPMF_recursiveFallbackProbabilityModel M D enumeration g + attempts fuel hfield hvalid hattempts) + +/-- Multi-factor characteristic-two trace splitting under uniform probes. -/ +noncomputable def recursiveEvenTraceSplitProcessPMF {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + (traceCtx : SmallPrimeTraceContext F) + (enumeration : FieldEnumeration F) (q attempts : Nat) : + Nat → List (CPolynomial F) → PMF (List Bool) := + recursiveSplitProcessPMF + (fun g attempt ↦ evenTraceTrialPMF M D traceCtx enumeration q + (CPolynomial.natDegree g) g attempt) + attempts + +/-- The trace multi-factor process satisfies the recursive fallback model. -/ +theorem recursiveEvenTraceSplitProcessPMF_recursiveFallbackProbabilityModel {F : Type*} + [Field F] [Fintype F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + (traceCtx : SmallPrimeTraceContext F) + {q : Nat} (enumeration : UniformFieldEnumeration F q) + (g : CPolynomial F) (attempts fuel : Nat) + (hfield : EvenTraceUniformFieldModel F q traceCtx enumeration) + (hvalid : lasVegasSplitterInput q g) + (hattempts : CPolynomial.natDegree g - 2 ≤ attempts) : + RecursiveFallbackProbabilityModel + (recursiveEvenTraceSplitProcessPMF M D traceCtx enumeration.toFieldEnumeration q + attempts fuel [g]) + {tr2 : List Bool | true ∈ tr2} + attempts (CPolynomial.natDegree g) := + recursiveSplitProcessPMF_recursiveFallbackProbabilityModel _ g attempts fuel + (fun _g hg _att _children hmem ↦ + evenTraceTrialPMF_support_isSplitStep M D traceCtx enumeration.toFieldEnumeration q + hg hmem) + (fun g hvalid hdeg attempt ↦ + evenTraceTrial_success_probability_ge_half M D traceCtx enumeration g attempt hfield + ((rootProductProbabilityInput_of_lasVegasSplitterInput hvalid + hfield.card_eq).hasTwoDistinctRoots hdeg) hdeg) + hvalid hattempts + +/-- +Full recursive fallback bound for the characteristic-two trace branch. +-/ +theorem recursiveEvenTraceSplitProcessPMF_fallback_probability_le_binomialTail {F : Type*} + [Field F] [Fintype F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + (traceCtx : SmallPrimeTraceContext F) + {q : Nat} (enumeration : UniformFieldEnumeration F q) + (g : CPolynomial F) (attempts fuel : Nat) + (hfield : EvenTraceUniformFieldModel F q traceCtx enumeration) + (hvalid : lasVegasSplitterInput q g) + (hattempts : CPolynomial.natDegree g - 2 ≤ attempts) : + eventProbability + (recursiveEvenTraceSplitProcessPMF M D traceCtx enumeration.toFieldEnumeration q + attempts fuel [g]) + {tr2 : List Bool | true ∈ tr2} ≤ + binomialFallbackTail attempts (CPolynomial.natDegree g) := + recursiveFallback_probability_le_binomialTail _ _ _ _ + (recursiveEvenTraceSplitProcessPMF_recursiveFallbackProbabilityModel M D traceCtx + enumeration g attempts fuel hfield hvalid hattempts) + +/-- +Per-factor trial of the full Las Vegas backend: the odd Cantor-Zassenhaus +branch for odd `q`, the characteristic-two trace branch when trace metadata is +supplied, mirroring the branch selection of `lasVegasSplitLoopWith`. +-/ +noncomputable def lasVegasSplitTrialPMF {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + (traceCtx? : Option (SmallPrimeTraceContext F)) + (enumeration : FieldEnumeration F) (q : Nat) + (g : CPolynomial F) (attempt : Nat) : PMF (TrialResult F) := + match traceCtx? with + | some traceCtx => + if q % 2 = 1 then + oddSplitTrialPMF M D enumeration q (CPolynomial.natDegree g) g attempt + else + evenTraceTrialPMF M D traceCtx enumeration q (CPolynomial.natDegree g) g attempt + | none => oddSplitTrialPMF M D enumeration q (CPolynomial.natDegree g) g attempt + +/-- +Probability-facing model for the full Las Vegas backend: either the field has +odd cardinality, or it is a binary field presented with matching trace +metadata. +-/ +inductive LasVegasUniformFieldModel (F : Type*) (q : Nat) + [Field F] [Fintype F] [BEq F] [LawfulBEq F] + (traceCtx? : Option (SmallPrimeTraceContext F)) + (enumeration : UniformFieldEnumeration F q) : Prop + | odd (hodd : q % 2 = 1) (hfield : OddUniformFieldModel F q enumeration) + | evenTrace (traceCtx : SmallPrimeTraceContext F) + (hctx : traceCtx? = some traceCtx) (heven : q % 2 = 0) + (hfield : EvenTraceUniformFieldModel F q traceCtx enumeration) + +theorem LasVegasUniformFieldModel.card_eq {F : Type*} {q : Nat} + [Field F] [Fintype F] [BEq F] [LawfulBEq F] + {traceCtx? : Option (SmallPrimeTraceContext F)} + {enumeration : UniformFieldEnumeration F q} + (hmodel : LasVegasUniformFieldModel F q traceCtx? enumeration) : + Fintype.card F = q := by + cases hmodel with + | odd _ hfield => exact hfield.card_eq + | evenTrace _ _ _ hfield => exact hfield.card_eq + +/-- Successful backend trials are split steps. -/ +theorem lasVegasSplitTrialPMF_support_isSplitStep {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + (traceCtx? : Option (SmallPrimeTraceContext F)) + (enumeration : FieldEnumeration F) (q : Nat) {g : CPolynomial F} (hg : g ≠ 0) + {attempt : Nat} {children : Array (CPolynomial F)} + (hmem : TrialResult.split children ∈ + (lasVegasSplitTrialPMF M D traceCtx? enumeration q g attempt).support) : + IsSplitStep g children := by + unfold lasVegasSplitTrialPMF at hmem + cases traceCtx? with + | none => exact oddSplitTrialPMF_support_isSplitStep M D enumeration q hg hmem + | some traceCtx => + dsimp only at hmem + by_cases hodd : q % 2 = 1 + · rw [if_pos hodd] at hmem + exact oddSplitTrialPMF_support_isSplitStep M D enumeration q hg hmem + · rw [if_neg hodd] at hmem + exact evenTraceTrialPMF_support_isSplitStep M D traceCtx enumeration q hg hmem + +/-- Backend trials on splitter-valid factors succeed at rate one half. -/ +theorem lasVegasSplitTrialPMF_success_probability_ge_half {F : Type*} + [Field F] [Fintype F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + (traceCtx? : Option (SmallPrimeTraceContext F)) + {q : Nat} (enumeration : UniformFieldEnumeration F q) + (hmodel : LasVegasUniformFieldModel F q traceCtx? enumeration) + (g : CPolynomial F) (hvalid : lasVegasSplitterInput q g) + (hdegree : 2 ≤ CPolynomial.natDegree g) (attempt : Nat) : + (2 : ℝ≥0∞)⁻¹ ≤ + trialSuccessProbability + (lasVegasSplitTrialPMF M D traceCtx? enumeration.toFieldEnumeration q g attempt) := by + have hinput : RootProductProbabilityInput q g := + rootProductProbabilityInput_of_lasVegasSplitterInput hvalid hmodel.card_eq + cases hmodel with + | odd hodd hfield => + have heq : lasVegasSplitTrialPMF M D traceCtx? enumeration.toFieldEnumeration q + g attempt = + oddSplitTrialPMF M D enumeration.toFieldEnumeration q + (CPolynomial.natDegree g) g attempt := by + unfold lasVegasSplitTrialPMF + cases traceCtx? <;> simp [hodd] + rw [heq] + exact oddSplitTrial_success_probability_ge_half M D enumeration g attempt hfield + hinput hdegree + | evenTrace traceCtx hctx heven hfield => + have heq : lasVegasSplitTrialPMF M D traceCtx? enumeration.toFieldEnumeration q + g attempt = + evenTraceTrialPMF M D traceCtx enumeration.toFieldEnumeration q + (CPolynomial.natDegree g) g attempt := by + unfold lasVegasSplitTrialPMF + rw [hctx] + dsimp only + rw [if_neg (by omega : ¬q % 2 = 1)] + rw [heq] + exact evenTraceTrial_success_probability_ge_half M D traceCtx enumeration g attempt + hfield (hinput.hasTwoDistinctRoots hdegree) hdegree + +/-- +The full Las Vegas backend recursion under idealized uniform probes, +dispatching between the odd and trace branches by field characteristic. +-/ +noncomputable def lasVegasRecursiveSplitProcessPMF {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + (traceCtx? : Option (SmallPrimeTraceContext F)) + (enumeration : FieldEnumeration F) (q attempts : Nat) : + Nat → List (CPolynomial F) → PMF (List Bool) := + recursiveSplitProcessPMF (lasVegasSplitTrialPMF M D traceCtx? enumeration q) attempts + +/-- +Backend-level recursive fallback bound: over any finite field — odd, or binary +with matching trace metadata — the full Las Vegas recursion on a splitter-valid +input of degree `d` reaches exhaustive enumeration anywhere with probability at +most the binomial tail `2 ^ -attempts * ∑_{j ≤ d - 2} C(attempts, j)`. +-/ +theorem lasVegasRecursiveSplitProcessPMF_fallback_probability_le_binomialTail {F : Type*} + [Field F] [Fintype F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + (traceCtx? : Option (SmallPrimeTraceContext F)) + {q : Nat} (enumeration : UniformFieldEnumeration F q) + (g : CPolynomial F) (attempts fuel : Nat) + (hmodel : LasVegasUniformFieldModel F q traceCtx? enumeration) + (hvalid : lasVegasSplitterInput q g) + (hattempts : CPolynomial.natDegree g - 2 ≤ attempts) : + eventProbability + (lasVegasRecursiveSplitProcessPMF M D traceCtx? enumeration.toFieldEnumeration q + attempts fuel [g]) + {tr2 : List Bool | true ∈ tr2} ≤ + binomialFallbackTail attempts (CPolynomial.natDegree g) := + recursiveSplitProcessPMF_fallback_probability_le_binomialTail _ g attempts fuel + (fun _g hg _att _children hmem ↦ + lasVegasSplitTrialPMF_support_isSplitStep M D traceCtx? + enumeration.toFieldEnumeration q hg hmem) + (fun g hvalid hdeg attempt ↦ + lasVegasSplitTrialPMF_success_probability_ge_half M D traceCtx? enumeration hmodel + g hvalid hdeg attempt) + hvalid hattempts + +/-- +Headline backend theorem: for any nonzero polynomial over a finite field — odd, +or binary with matching trace metadata — the Las Vegas backend run on its +finite-field root product reaches exhaustive enumeration anywhere in the +recursion with probability at most the binomial tail in the attempt cutoff. +-/ +theorem finiteFieldRootProductWith_lasVegas_fallback_probability_le_binomialTail + {F : Type*} [Field F] [Fintype F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + (ctx : FiniteFieldContext F) (traceCtx? : Option (SmallPrimeTraceContext F)) + (enumeration : UniformFieldEnumeration F ctx.q) + {p : CPolynomial F} (attempts fuel : Nat) + (hmodel : LasVegasUniformFieldModel F ctx.q traceCtx? enumeration) + (hp : p ≠ 0) + (hattempts : + CPolynomial.natDegree (finiteFieldRootProductWith M D ctx p) - 2 ≤ attempts) : + eventProbability + (lasVegasRecursiveSplitProcessPMF M D traceCtx? enumeration.toFieldEnumeration + ctx.q attempts fuel [finiteFieldRootProductWith M D ctx p]) + {tr2 : List Bool | true ∈ tr2} ≤ + binomialFallbackTail attempts + (CPolynomial.natDegree (finiteFieldRootProductWith M D ctx p)) := + lasVegasRecursiveSplitProcessPMF_fallback_probability_le_binomialTail M D traceCtx? + enumeration _ attempts fuel hmodel + (finiteFieldRootProductWith_lasVegasSplitterInput M D ctx hp) hattempts + +end FiniteField + +end Roots + +end CPolynomial + +end CompPoly diff --git a/CompPoly/Univariate/Roots/LasVegas/Probability/Repeated.lean b/CompPoly/Univariate/Roots/LasVegas/Probability/Repeated.lean new file mode 100644 index 00000000..9f6b57be --- /dev/null +++ b/CompPoly/Univariate/Roots/LasVegas/Probability/Repeated.lean @@ -0,0 +1,995 @@ +/- +Copyright (c) 2026 CompPoly Contributors. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Valerii Huhnin +-/ + +module + +-- `natDegree` and friends are declared in bare `public section`s, so their bodies +-- are opaque downstream; see `docs/wiki/module-system.md`. +import all CompPoly.Univariate.Basic +public import CompPoly.Univariate.Roots.LasVegas.Probability.OddTrial +public import CompPoly.Univariate.Roots.LasVegas.Probability.Recursive + +/-! +# Repeated Odd Trials and Geometric Fallback Bounds + +Independent repeated odd-field split trials for one unresolved factor, the +geometric bound on reaching enumeration fallback, and the one-factor witness of +the recursive fallback model. The `_of_uniformProbe` variants discharge the +per-trial half-success hypothesis from the actual uniform probe source. +-/ + +@[expose] public section + +open scoped Classical ENNReal NNReal BigOperators + +namespace CompPoly + +namespace CPolynomial + +namespace Roots + +namespace FiniteField + +/-- Independent repeated odd-field split trials for one unresolved factor. -/ +noncomputable def repeatedOddSplitTrialsPMF {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + (enumeration : FieldEnumeration F) (q coefficientCount : Nat) + (g : CPolynomial F) : + Nat → Nat → PMF (List (TrialResult F)) + | 0, _offset => pure [] + | attempts + 1, offset => do + let trial ← oddSplitTrialPMF M D enumeration q coefficientCount g offset + let trials ← repeatedOddSplitTrialsPMF M D enumeration q coefficientCount g attempts + (offset + 1) + pure (trial :: trials) + +/-- Probability that the one-factor loop reaches fallback after all split attempts fail. -/ +noncomputable def fallbackAfterOddAttemptsProbability {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + (enumeration : FieldEnumeration F) (q coefficientCount : Nat) + (g : CPolynomial F) (attempts offset : Nat) : + ℝ≥0∞ := + eventProbability + (repeatedOddSplitTrialsPMF M D enumeration q coefficientCount g attempts offset) + {trials | TrialResult.allFailed trials} + +private theorem toOuterMeasure_map_cons_allFailed {F : Type*} [Zero F] + (dist : PMF (List (TrialResult F))) (trial : TrialResult F) : + (dist.map (List.cons trial)).toOuterMeasure {trials | TrialResult.allFailed trials} = + if ¬ trial.IsSuccess then + dist.toOuterMeasure {trials | TrialResult.allFailed trials} + else + 0 := by + cases trial <;> + simp [PMF.toOuterMeasure_map_apply, TrialResult.IsSuccess, TrialResult.allFailed] + +private theorem fallbackAfterOddAttemptsProbability_succ {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + (enumeration : FieldEnumeration F) (q coefficientCount : Nat) + (g : CPolynomial F) (attempts offset : Nat) : + fallbackAfterOddAttemptsProbability M D enumeration q coefficientCount g + (attempts + 1) offset = + eventProbability + (oddSplitTrialPMF M D enumeration q coefficientCount g offset) + {trial | ¬ trial.IsSuccess} * + fallbackAfterOddAttemptsProbability M D enumeration q coefficientCount g + attempts (offset + 1) := by + change (PMF.bind (oddSplitTrialPMF M D enumeration q coefficientCount g offset) + (fun trial ↦ PMF.map (List.cons trial) + (repeatedOddSplitTrialsPMF M D enumeration q coefficientCount g attempts + (offset + 1)))).toOuterMeasure + {trials | TrialResult.allFailed trials} = + (oddSplitTrialPMF M D enumeration q coefficientCount g offset).toOuterMeasure + {trial | ¬ trial.IsSuccess} * + (repeatedOddSplitTrialsPMF M D enumeration q coefficientCount g attempts + (offset + 1)).toOuterMeasure + {trials | TrialResult.allFailed trials} + rw [PMF.toOuterMeasure_bind_apply] + trans (∑' trial, (if ¬ trial.IsSuccess then + (oddSplitTrialPMF M D enumeration q coefficientCount g offset) trial else 0) * + (repeatedOddSplitTrialsPMF M D enumeration q coefficientCount g attempts + (offset + 1)).toOuterMeasure + {trials | TrialResult.allFailed trials}) + · apply tsum_congr + intro trial + rw [toOuterMeasure_map_cons_allFailed] + by_cases hfail : ¬ trial.IsSuccess <;> simp [hfail] + · conv_rhs => + rw [PMF.toOuterMeasure_apply] + rw [← ENNReal.tsum_mul_right] + apply tsum_congr + intro trial + by_cases hsuccess : trial.IsSuccess <;> simp [hsuccess] + +/-- +If every one-factor trial succeeds with probability at least `1 / 2`, the +probability of using fallback after `attempts` trials is at most `2^-attempts`. +-/ +theorem fallbackAfterOddAttempts_probability_le_geometric {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + (enumeration : FieldEnumeration F) (q coefficientCount : Nat) + (g : CPolynomial F) (attempts offset : Nat) + (hstep : ∀ i, i < attempts → + (2 : ℝ≥0∞)⁻¹ ≤ + trialSuccessProbability + (oddSplitTrialPMF M D enumeration q coefficientCount g (offset + i))) : + fallbackAfterOddAttemptsProbability M D enumeration q coefficientCount g attempts offset ≤ + ((2 : ℝ≥0∞)⁻¹) ^ attempts := by + induction attempts generalizing offset with + | zero => + rw [fallbackAfterOddAttemptsProbability, eventProbability, repeatedOddSplitTrialsPMF] + exact le_trans + ((pure ([] : List (TrialResult F)) : PMF (List (TrialResult F))).toOuterMeasure.mono + (Set.subset_univ _)) + (by simp [PMF.toOuterMeasure_apply]) + | succ attempts ih => + rw [fallbackAfterOddAttemptsProbability_succ] + let half : ℝ≥0∞ := (2 : ℝ≥0∞)⁻¹ + have hfail : eventProbability + (oddSplitTrialPMF M D enumeration q coefficientCount g offset) + {trial | ¬ trial.IsSuccess} ≤ half := by + exact trialFailureProbability_le_half_of_success _ (hstep 0 (by omega)) + have hrest : + fallbackAfterOddAttemptsProbability M D enumeration q coefficientCount g attempts + (offset + 1) ≤ + half ^ attempts := by + exact ih (offset + 1) (by + intro i hi + have h := hstep (i + 1) (by omega) + simpa [Nat.add_assoc, Nat.add_comm, Nat.add_left_comm] using h) + refine (mul_le_mul' hfail hrest).trans ?_ + change half * half ^ attempts ≤ half ^ (attempts + 1) + rw [pow_succ] + exact le_of_eq (_root_.mul_comm (a := half) (b := half ^ attempts)) + +/-- +For a squarefree finite-field root product over an odd field, the uniform probe +source reaches enumeration fallback after `attempts` one-factor trials with +probability at most `2^-attempts`. +-/ +theorem fallbackAfterOddAttempts_probability_le_geometric_of_uniformProbe {F : Type*} + [Field F] [Fintype F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + {q : Nat} (enumeration : UniformFieldEnumeration F q) + (g : CPolynomial F) (attempts offset : Nat) + (hfield : OddUniformFieldModel F q enumeration) + (hrootProduct : RootProductProbabilityInput q g) + (hdegree : 2 ≤ CPolynomial.natDegree g) : + fallbackAfterOddAttemptsProbability M D enumeration.toFieldEnumeration q + (CPolynomial.natDegree g) g attempts offset ≤ + ((2 : ℝ≥0∞)⁻¹) ^ attempts := by + apply fallbackAfterOddAttempts_probability_le_geometric + intro i _hi + exact oddSplitTrial_success_probability_ge_half M D enumeration g (offset + i) + hfield hrootProduct hdegree + +/-- +The repeated one-factor odd-field trial source satisfies the recursive fallback +model by concentrating the bad-rank mass at rank `0`. +-/ +theorem repeatedOddSplitTrialsPMF_recursiveFallbackProbabilityModel {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + (enumeration : FieldEnumeration F) (q coefficientCount : Nat) + (g : CPolynomial F) (attempts offset degree : Nat) + (hdegree : 2 ≤ degree) + (hstep : ∀ i, i < attempts → + (2 : ℝ≥0∞)⁻¹ ≤ + trialSuccessProbability + (oddSplitTrialPMF M D enumeration q coefficientCount g (offset + i))) : + RecursiveFallbackProbabilityModel + (repeatedOddSplitTrialsPMF M D enumeration q coefficientCount g attempts offset) + {trials | TrialResult.allFailed trials} + attempts degree := by + refine + ⟨fun j ↦ if j = 0 then {trials | TrialResult.allFailed trials} else ∅, + ?_, ?_, ?_⟩ + · intro trials htrials + exact Set.mem_iUnion.mpr ⟨0, by simpa using htrials⟩ + · intro j hj + by_cases hj0 : j = 0 + · subst j + have hzero_mem : 0 ∈ Finset.range (degree - 1) := by + simp + omega + exact (hj hzero_mem).elim + · simp [hj0, eventProbability] + · intro j _hj + by_cases hj0 : j = 0 + · subst j + simpa [fallbackAfterOddAttemptsProbability] using + fallbackAfterOddAttempts_probability_le_geometric M D enumeration q coefficientCount + g attempts offset hstep + · simp [hj0, eventProbability] + +/-- +The one-factor recursive fallback model witness, with the per-trial +half-success hypothesis discharged from the actual uniform probe source. +-/ +theorem repeatedOddSplitTrialsPMF_recursiveFallbackProbabilityModel_of_uniformProbe {F : Type*} + [Field F] [Fintype F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + {q : Nat} (enumeration : UniformFieldEnumeration F q) + (g : CPolynomial F) (attempts offset degree : Nat) + (hdeg : 2 ≤ degree) + (hfield : OddUniformFieldModel F q enumeration) + (hrootProduct : RootProductProbabilityInput q g) + (hdegree : 2 ≤ CPolynomial.natDegree g) : + RecursiveFallbackProbabilityModel + (repeatedOddSplitTrialsPMF M D enumeration.toFieldEnumeration q + (CPolynomial.natDegree g) g attempts offset) + {trials | TrialResult.allFailed trials} + attempts degree := by + apply repeatedOddSplitTrialsPMF_recursiveFallbackProbabilityModel M D + enumeration.toFieldEnumeration q (CPolynomial.natDegree g) g attempts offset degree hdeg + intro i _hi + exact oddSplitTrial_success_probability_ge_half M D enumeration g (offset + i) + hfield hrootProduct hdegree + +set_option maxHeartbeats 1600000 in +/-- +Bridge to the executable retry loop: the deterministic `tryOddSplitAttemptsWith`, +driven by a probe table whose entries are sampled independently and uniformly, +exhausts all attempts with probability at most `2 ^ -attempts`. + +Unlike the trial-PMF theorems, the randomized object here is the actual +runtime function applied to an honestly sampled `ProbeFamily`. +-/ +theorem tryOddSplitAttemptsWith_uniformTable_none_le_geometric {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + (enumeration : FieldEnumeration F) (q coefficientCount : Nat) (g : CPolynomial F) + (hstep : (2 : ℝ≥0∞)⁻¹ ≤ + trialSuccessProbability (oddSplitTrialPMF M D enumeration q coefficientCount g 0)) : + ∀ attempts : Nat, + eventProbability (uniformProbeTablePMF enumeration coefficientCount attempts) + {table : List (CPolynomial F) | + tryOddSplitAttemptsWith M D q (tableProbeFamily table) g attempts 0 = none} ≤ + ((2 : ℝ≥0∞)⁻¹) ^ attempts := by + have hfailset : ((fun h : CPolynomial F ↦ + (match cantorZassenhausOddAttemptWith M D q + ({ probe := fun _q _factor _attempt ↦ h } : ProbeFamily F) g 0 with + | some children => TrialResult.split children + | none => TrialResult.failed)) ⁻¹' + {trial : TrialResult F | ¬ trial.IsSuccess}) = + {h : CPolynomial F | cantorZassenhausOddAttemptWith M D q + ({ probe := fun _q _factor _attempt ↦ h } : ProbeFamily F) g 0 = none} := by + ext h + cases h0 : cantorZassenhausOddAttemptWith M D q + ({ probe := fun _q _factor _attempt ↦ h } : ProbeFamily F) g 0 <;> + simp [h0, TrialResult.IsSuccess] + have hfail : (uniformProbePMF enumeration coefficientCount).toOuterMeasure + {h : CPolynomial F | cantorZassenhausOddAttemptWith M D q + ({ probe := fun _q _factor _attempt ↦ h } : ProbeFamily F) g 0 = none} ≤ + (2 : ℝ≥0∞)⁻¹ := by + rw [← hfailset, ← PMF.toOuterMeasure_map_apply] + have hhalf := trialFailureProbability_le_half_of_success + (oddSplitTrialPMF M D enumeration q coefficientCount g 0) hstep + rw [eventProbability, oddSplitTrialPMF] at hhalf + exact hhalf + intro attempts + induction attempts with + | zero => + rw [eventProbability, uniformProbeTablePMF, PMF.toOuterMeasure_pure_apply, + if_pos (by + rw [Set.mem_ofPred_eq] + unfold tryOddSplitAttemptsWith + rfl)] + simp + | succ attempts ih => + rw [eventProbability, uniformProbeTablePMF, PMF.toOuterMeasure_bind_apply] + have hsection : ∀ (h : CPolynomial F) (rest : List (CPolynomial F)), + (tryOddSplitAttemptsWith M D q (tableProbeFamily (h :: rest)) g + (attempts + 1) 0 = none) ↔ + (cantorZassenhausOddAttemptWith M D q + ({ probe := fun _q _factor _attempt ↦ h } : ProbeFamily F) g 0 = none ∧ + tryOddSplitAttemptsWith M D q (tableProbeFamily rest) g attempts 0 = none) := by + intro h rest + unfold tryOddSplitAttemptsWith + rw [cantorZassenhausOddAttemptWith_probe_congr M D q (g := g) + (p2 := ({ probe := fun _q _factor _attempt ↦ h } : ProbeFamily F)) + (a2 := 0) (by simp [tableProbeFamily])] + rw [tryOddSplitAttemptsWith_probe_congr M D q + (p1 := tableProbeFamily (h :: rest)) (p2 := tableProbeFamily rest) (g := g) + attempts (0 + 1) 0 (by + intro i _hi + simp [tableProbeFamily, Nat.add_comm])] + cases h0 : cantorZassenhausOddAttemptWith M D q + ({ probe := fun _q _factor _attempt ↦ h } : ProbeFamily F) g 0 with + | some c => simp + | none => + rw [← tryOddSplitAttemptsWith.eq_def] + simp + have hweight : ∀ h : CPolynomial F, + ((uniformProbeTablePMF enumeration coefficientCount attempts).map + (List.cons h)).toOuterMeasure + {table : List (CPolynomial F) | + tryOddSplitAttemptsWith M D q (tableProbeFamily table) g + (attempts + 1) 0 = none} = + if cantorZassenhausOddAttemptWith M D q + ({ probe := fun _q _factor _attempt ↦ h } : ProbeFamily F) g 0 = none then + (uniformProbeTablePMF enumeration coefficientCount attempts).toOuterMeasure + {table : List (CPolynomial F) | + tryOddSplitAttemptsWith M D q (tableProbeFamily table) g attempts 0 = none} + else 0 := by + intro h + rw [PMF.toOuterMeasure_map_apply] + by_cases h0 : cantorZassenhausOddAttemptWith M D q + ({ probe := fun _q _factor _attempt ↦ h } : ProbeFamily F) g 0 = none + · rw [if_pos h0] + congr 1 + ext rest + rw [Set.mem_preimage, Set.mem_ofPred_eq, Set.mem_ofPred_eq, hsection h rest] + simp [h0] + · rw [if_neg h0] + have hempty : (List.cons h ⁻¹' + {table : List (CPolynomial F) | + tryOddSplitAttemptsWith M D q (tableProbeFamily table) g + (attempts + 1) 0 = none}) = ∅ := by + ext rest + rw [Set.mem_preimage, Set.mem_ofPred_eq, hsection h rest] + simp [h0] + rw [hempty] + simp + calc (∑' h : CPolynomial F, uniformProbePMF enumeration coefficientCount h * + ((uniformProbeTablePMF enumeration coefficientCount attempts).map + (List.cons h)).toOuterMeasure + {table : List (CPolynomial F) | + tryOddSplitAttemptsWith M D q (tableProbeFamily table) g + (attempts + 1) 0 = none}) + ≤ ∑' h : CPolynomial F, + (if cantorZassenhausOddAttemptWith M D q + ({ probe := fun _q _factor _attempt ↦ h } : ProbeFamily F) g 0 = none + then uniformProbePMF enumeration coefficientCount h else 0) * + ((2 : ℝ≥0∞)⁻¹) ^ attempts := by + apply ENNReal.tsum_le_tsum + intro h + rw [hweight h] + by_cases h0 : cantorZassenhausOddAttemptWith M D q + ({ probe := fun _q _factor _attempt ↦ h } : ProbeFamily F) g 0 = none + · rw [if_pos h0, if_pos h0] + exact mul_le_mul' le_rfl (by + have := ih + rwa [eventProbability] at this) + · rw [if_neg h0, if_neg h0] + simp + _ = (uniformProbePMF enumeration coefficientCount).toOuterMeasure + {h : CPolynomial F | cantorZassenhausOddAttemptWith M D q + ({ probe := fun _q _factor _attempt ↦ h } : ProbeFamily F) g 0 = none} * + ((2 : ℝ≥0∞)⁻¹) ^ attempts := by + rw [ENNReal.tsum_mul_right] + congr 1 + rw [PMF.toOuterMeasure_apply] + apply tsum_congr + intro h + rw [Set.indicator_apply] + rfl + _ ≤ (2 : ℝ≥0∞)⁻¹ * ((2 : ℝ≥0∞)⁻¹) ^ attempts := + _root_.mul_le_mul_left hfail _ + _ = ((2 : ℝ≥0∞)⁻¹) ^ (attempts + 1) := by + rw [pow_succ, _root_.mul_comm] + +/-- +For a squarefree odd-field root product, the executable retry loop driven by an +honestly sampled uniform probe table reaches fallback with probability at most +`2 ^ -attempts`. +-/ +theorem tryOddSplitAttemptsWith_uniformTable_none_le_geometric_of_uniformProbe {F : Type*} + [Field F] [Fintype F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + {q : Nat} (enumeration : UniformFieldEnumeration F q) + (g : CPolynomial F) (attempts : Nat) + (hfield : OddUniformFieldModel F q enumeration) + (hrootProduct : RootProductProbabilityInput q g) + (hdegree : 2 ≤ CPolynomial.natDegree g) : + eventProbability + (uniformProbeTablePMF enumeration.toFieldEnumeration + (CPolynomial.natDegree g) attempts) + {table : List (CPolynomial F) | + tryOddSplitAttemptsWith M D q (tableProbeFamily table) g attempts 0 = none} ≤ + ((2 : ℝ≥0∞)⁻¹) ^ attempts := + tryOddSplitAttemptsWith_uniformTable_none_le_geometric M D + enumeration.toFieldEnumeration q (CPolynomial.natDegree g) g + (oddSplitTrial_success_probability_ge_half M D enumeration g 0 hfield + hrootProduct hdegree) + attempts + +/-- +Deterministic multi-factor splitter driven by per-factor probe tables: the +`n`-th processed splittable factor is handed the `n`-th table through the +abstract per-factor split function `tryTable`. This is a table-driven, fully +computable mirror of the randomized branch of `lasVegasSplitLoopWith`, +recording per-factor fallback flags. +-/ +def recursiveSplitWithTables {F : Type*} [Field F] [BEq F] [LawfulBEq F] + (tryTable : List (CPolynomial F) → CPolynomial F → Option (Array (CPolynomial F))) : + Nat → List (CPolynomial F) → List (List (CPolynomial F)) → List Bool + | 0, _stack, _tables => [] + | _fuel + 1, [], _tables => [] + | fuel + 1, g :: stack, tables => + if CPolynomial.natDegree g < 2 then + recursiveSplitWithTables tryTable fuel stack tables + else + match tables with + | [] => [] + | table :: tables => + match tryTable table g with + | none => true :: recursiveSplitWithTables tryTable fuel stack tables + | some children => + false :: recursiveSplitWithTables tryTable fuel + (children.toList ++ stack) tables + +/-- Independent uniform probe tables, one per processed factor. -/ +noncomputable def uniformProbeTablesPMF {F : Type*} [Field F] [BEq F] [LawfulBEq F] + (enumeration : FieldEnumeration F) (coefficientCount attempts : Nat) : + Nat → PMF (List (List (CPolynomial F))) + | 0 => PMF.pure [] + | n + 1 => + (uniformProbeTablePMF enumeration coefficientCount attempts).bind fun table ↦ + (uniformProbeTablesPMF enumeration coefficientCount attempts n).map + (List.cons table) + +section RecursiveTables + +variable {F : Type*} [Field F] [BEq F] [LawfulBEq F] +variable (tryTable : List (CPolynomial F) → CPolynomial F → Option (Array (CPolynomial F))) + +private theorem recursiveSplitWithTables_skip (fuel : Nat) {g : CPolynomial F} + (stack : List (CPolynomial F)) (hdeg : CPolynomial.natDegree g < 2) + (tables : List (List (CPolynomial F))) : + recursiveSplitWithTables tryTable (fuel + 1) (g :: stack) tables = + recursiveSplitWithTables tryTable fuel stack tables := by + rw [recursiveSplitWithTables.eq_def] + dsimp only + rw [if_pos hdeg] + +private theorem recursiveSplitWithTables_nil_tables (fuel : Nat) {g : CPolynomial F} + (stack : List (CPolynomial F)) (hdeg : ¬ CPolynomial.natDegree g < 2) : + recursiveSplitWithTables tryTable (fuel + 1) (g :: stack) [] = [] := by + rw [recursiveSplitWithTables.eq_def] + dsimp only + rw [if_neg hdeg] + +private theorem recursiveSplitWithTables_cons_none (fuel : Nat) {g : CPolynomial F} + (stack : List (CPolynomial F)) (hdeg : ¬ CPolynomial.natDegree g < 2) + {table : List (CPolynomial F)} (tables : List (List (CPolynomial F))) + (htry : tryTable table g = none) : + recursiveSplitWithTables tryTable (fuel + 1) (g :: stack) (table :: tables) = + true :: recursiveSplitWithTables tryTable fuel stack tables := by + rw [recursiveSplitWithTables.eq_def] + dsimp only + rw [if_neg hdeg] + simp [htry] + +private theorem recursiveSplitWithTables_cons_some (fuel : Nat) {g : CPolynomial F} + (stack : List (CPolynomial F)) (hdeg : ¬ CPolynomial.natDegree g < 2) + {table : List (CPolynomial F)} (tables : List (List (CPolynomial F))) + {children : Array (CPolynomial F)} + (htry : tryTable table g = some children) : + recursiveSplitWithTables tryTable (fuel + 1) (g :: stack) (table :: tables) = + false :: recursiveSplitWithTables tryTable fuel + (children.toList ++ stack) tables := by + rw [recursiveSplitWithTables.eq_def] + dsimp only + rw [if_neg hdeg] + simp [htry] + +/-- Table-driven traces never exceed the split budget of the initial stack. -/ +theorem recursiveSplitWithTables_length_le + (hsplit : ∀ (g : CPolynomial F), g ≠ 0 → + ∀ (table : List (CPolynomial F)) (children : Array (CPolynomial F)), + tryTable table g = some children → IsSplitStep g children) : + ∀ fuel (stack : List (CPolynomial F)) (tables : List (List (CPolynomial F))), + (recursiveSplitWithTables tryTable fuel stack tables).length ≤ + stackSplitBudget stack := by + intro fuel + induction fuel with + | zero => + intro stack tables + rw [recursiveSplitWithTables.eq_def] + simp + | succ fuel ih => + intro stack tables + cases stack with + | nil => + rw [recursiveSplitWithTables.eq_def] + simp + | cons g stack => + by_cases hdeg : CPolynomial.natDegree g < 2 + · rw [recursiveSplitWithTables_skip tryTable fuel stack hdeg tables] + refine le_trans (ih stack tables) ?_ + simp only [stackSplitBudget] + omega + · have hdeg2 : 2 ≤ g.toPoly.natDegree := by + have h := Nat.le_of_not_lt hdeg + rwa [CPolynomial.natDegree_toPoly] at h + cases tables with + | nil => + rw [recursiveSplitWithTables_nil_tables tryTable fuel stack hdeg] + simp + | cons table tables => + have hg : g ≠ 0 := by + intro h0 + apply hdeg + rw [h0] + have hzero : CPolynomial.natDegree (0 : CPolynomial F) = 0 := rfl + omega + cases htry : tryTable table g with + | none => + rw [recursiveSplitWithTables_cons_none tryTable fuel stack hdeg + tables htry] + have hlen := ih stack tables + simp only [stackSplitBudget, List.length_cons] + omega + | some children => + rw [recursiveSplitWithTables_cons_some tryTable fuel stack hdeg + tables htry] + have hlen := ih (children.toList ++ stack) tables + rw [stackSplitBudget_append] at hlen + obtain ⟨hsize2, hchild, hwork⟩ := hsplit g hg table children htry + have hsw := stackWork_eq_two_mul_budget_add_length children.toList + fun c hc ↦ (hchild c hc).2.2 + have hlen2 : children.toList.length = children.size := by simp + simp only [splitWork] at hwork + simp only [stackSplitBudget, List.length_cons] + omega + +/-- +Each processed factor of the table-driven splitter falls back with probability +at most `2 ^ -attempts` under independently sampled uniform probe tables. +-/ +theorem recursiveSplitWithTables_rank_le_geometric {q : Nat} + (enumeration : FieldEnumeration F) (coefficientCount attempts : Nat) + (hsplit : ∀ (g : CPolynomial F), g ≠ 0 → + ∀ (table : List (CPolynomial F)) (children : Array (CPolynomial F)), + tryTable table g = some children → IsSplitStep g children) + (hfail : ∀ (g : CPolynomial F), lasVegasSplitterInput q g → + 2 ≤ CPolynomial.natDegree g → + (uniformProbeTablePMF enumeration coefficientCount attempts).toOuterMeasure + {table : List (CPolynomial F) | tryTable table g = none} ≤ + ((2 : ℝ≥0∞)⁻¹) ^ attempts) : + ∀ fuel (stack : List (CPolynomial F)) (j n : Nat), + (∀ g ∈ stack, lasVegasSplitterInput q g) → + eventProbability + (uniformProbeTablesPMF enumeration coefficientCount attempts n) + {tables : List (List (CPolynomial F)) | + (recursiveSplitWithTables tryTable fuel stack tables)[j]? = some true} ≤ + ((2 : ℝ≥0∞)⁻¹) ^ attempts := by + intro fuel + induction fuel with + | zero => + intro stack j n _hstack + have hE : {tables : List (List (CPolynomial F)) | + (recursiveSplitWithTables tryTable 0 stack tables)[j]? = some true} = + (∅ : Set (List (List (CPolynomial F)))) := by + ext tables + rw [Set.mem_ofPred_eq, recursiveSplitWithTables.eq_def] + simp + rw [eventProbability, hE] + simp + | succ fuel ih => + intro stack j n hstack + cases stack with + | nil => + have hE : {tables : List (List (CPolynomial F)) | + (recursiveSplitWithTables tryTable (fuel + 1) [] tables)[j]? = + some true} = (∅ : Set (List (List (CPolynomial F)))) := by + ext tables + rw [Set.mem_ofPred_eq, recursiveSplitWithTables.eq_def] + simp + rw [eventProbability, hE] + simp + | cons g stack => + by_cases hdeg : CPolynomial.natDegree g < 2 + · have hE : {tables : List (List (CPolynomial F)) | + (recursiveSplitWithTables tryTable (fuel + 1) (g :: stack) + tables)[j]? = some true} = + {tables : List (List (CPolynomial F)) | + (recursiveSplitWithTables tryTable fuel stack tables)[j]? = + some true} := by + ext tables + rw [Set.mem_ofPred_eq, Set.mem_ofPred_eq, + recursiveSplitWithTables_skip tryTable fuel stack hdeg tables] + rw [hE] + exact ih stack j n fun x hx ↦ hstack x (by simp [hx]) + · have hvalid := hstack g (by simp) + have hg : g ≠ 0 := hvalid.1 + have hdeg2 : 2 ≤ CPolynomial.natDegree g := Nat.le_of_not_lt hdeg + have hbridge := hfail g hvalid hdeg2 + cases n with + | zero => + rw [uniformProbeTablesPMF, eventProbability, + PMF.toOuterMeasure_pure_apply, if_neg (by + rw [Set.mem_ofPred_eq, + recursiveSplitWithTables_nil_tables tryTable fuel stack hdeg] + simp)] + exact zero_le + | succ n => + rw [uniformProbeTablesPMF, eventProbability, + PMF.toOuterMeasure_bind_apply] + cases j with + | zero => + trans (∑' table : List (CPolynomial F), + (if tryTable table g = none then + uniformProbeTablePMF enumeration coefficientCount attempts table + else 0)) + · apply ENNReal.tsum_le_tsum + intro table + rw [PMF.toOuterMeasure_map_apply] + by_cases htry : tryTable table g = none + · rw [if_pos htry] + refine le_trans (mul_le_mul' le_rfl + (toOuterMeasure_apply_le_one _ _)) ?_ + rw [MulOneClass.mul_one] + · rw [if_neg htry] + obtain ⟨children, hchildren⟩ := + Option.ne_none_iff_exists'.mp htry + have hpre : (List.cons table ⁻¹' + {tables : List (List (CPolynomial F)) | + (recursiveSplitWithTables tryTable (fuel + 1) + (g :: stack) tables)[0]? = some true}) = ∅ := by + ext tables + rw [Set.mem_preimage, Set.mem_ofPred_eq, + recursiveSplitWithTables_cons_some tryTable fuel + stack hdeg tables hchildren] + simp + rw [hpre] + simp + · refine le_trans (le_of_eq ?_) hbridge + rw [PMF.toOuterMeasure_apply] + apply tsum_congr + intro table + rw [Set.indicator_apply] + rfl + | succ j' => + trans (∑' table : List (CPolynomial F), + uniformProbeTablePMF enumeration coefficientCount attempts table * + ((2 : ℝ≥0∞)⁻¹) ^ attempts) + · apply ENNReal.tsum_le_tsum + intro table + apply mul_le_mul' le_rfl + rw [PMF.toOuterMeasure_map_apply] + cases htry : tryTable table g with + | none => + have hpre : (List.cons table ⁻¹' + {tables : List (List (CPolynomial F)) | + (recursiveSplitWithTables tryTable (fuel + 1) + (g :: stack) tables)[j' + 1]? = some true}) = + {tables : List (List (CPolynomial F)) | + (recursiveSplitWithTables tryTable fuel stack + tables)[j']? = some true} := by + ext tables + rw [Set.mem_preimage, Set.mem_ofPred_eq, Set.mem_ofPred_eq, + recursiveSplitWithTables_cons_none tryTable fuel + stack hdeg tables htry] + simp + rw [hpre] + exact ih stack j' n fun x hx ↦ hstack x (by simp [hx]) + | some children => + have hpre : (List.cons table ⁻¹' + {tables : List (List (CPolynomial F)) | + (recursiveSplitWithTables tryTable (fuel + 1) + (g :: stack) tables)[j' + 1]? = some true}) = + {tables : List (List (CPolynomial F)) | + (recursiveSplitWithTables tryTable fuel + (children.toList ++ stack) tables)[j']? = + some true} := by + ext tables + rw [Set.mem_preimage, Set.mem_ofPred_eq, Set.mem_ofPred_eq, + recursiveSplitWithTables_cons_some tryTable fuel + stack hdeg tables htry] + simp + rw [hpre] + obtain ⟨_hsize2, hchild, _hwork⟩ := + hsplit g hg table children htry + apply ih (children.toList ++ stack) j' n + intro x hx + rw [List.mem_append] at hx + rcases hx with hx | hx + · exact ⟨(hchild x hx).1, (hchild x hx).2.1.trans hvalid.2⟩ + · exact hstack x (by simp [hx]) + · rw [ENNReal.tsum_mul_right, PMF.tsum_coe, _root_.one_mul] + +/-- Table-driven trace entries beyond the split budget never appear. -/ +theorem recursiveSplitWithTables_rank_eq_zero_of_budget_le + (enumeration : FieldEnumeration F) (coefficientCount attempts fuel n : Nat) + (stack : List (CPolynomial F)) {j : Nat} + (hsplit : ∀ (g : CPolynomial F), g ≠ 0 → + ∀ (table : List (CPolynomial F)) (children : Array (CPolynomial F)), + tryTable table g = some children → IsSplitStep g children) + (hj : stackSplitBudget stack ≤ j) : + eventProbability + (uniformProbeTablesPMF enumeration coefficientCount attempts n) + {tables : List (List (CPolynomial F)) | + (recursiveSplitWithTables tryTable fuel stack tables)[j]? = some true} = + 0 := by + have hE : {tables : List (List (CPolynomial F)) | + (recursiveSplitWithTables tryTable fuel stack tables)[j]? = some true} = + (∅ : Set (List (List (CPolynomial F)))) := by + ext tables + rw [Set.mem_ofPred_eq] + have hlen := recursiveSplitWithTables_length_le tryTable hsplit fuel stack tables + rw [List.getElem?_eq_none (by omega)] + simp + rw [eventProbability, hE] + simp + +/-- +The table-driven splitter satisfies the recursive fallback model under +independently sampled uniform probe tables. +-/ +theorem recursiveSplitWithTables_recursiveFallbackProbabilityModel {q : Nat} + (enumeration : FieldEnumeration F) + (g : CPolynomial F) (coefficientCount attempts fuel n : Nat) + (hsplit : ∀ (g : CPolynomial F), g ≠ 0 → + ∀ (table : List (CPolynomial F)) (children : Array (CPolynomial F)), + tryTable table g = some children → IsSplitStep g children) + (hfail : ∀ (g : CPolynomial F), lasVegasSplitterInput q g → + 2 ≤ CPolynomial.natDegree g → + (uniformProbeTablePMF enumeration coefficientCount attempts).toOuterMeasure + {table : List (CPolynomial F) | tryTable table g = none} ≤ + ((2 : ℝ≥0∞)⁻¹) ^ attempts) + (hvalid : lasVegasSplitterInput q g) + (hattempts : CPolynomial.natDegree g - 2 ≤ attempts) : + RecursiveFallbackProbabilityModel + (uniformProbeTablesPMF enumeration coefficientCount attempts n) + {tables : List (List (CPolynomial F)) | + true ∈ recursiveSplitWithTables tryTable fuel [g] tables} + attempts (CPolynomial.natDegree g) := by + refine ⟨fun j ↦ {tables : List (List (CPolynomial F)) | + (recursiveSplitWithTables tryTable fuel [g] tables)[j]? = some true}, + ?_, ?_, ?_⟩ + · intro tables htables + rw [Set.mem_ofPred_eq] at htables + obtain ⟨i, hi⟩ := List.mem_iff_getElem?.mp htables + exact Set.mem_iUnion.mpr ⟨i, hi⟩ + · intro j hj + rw [Finset.mem_range, not_lt] at hj + apply recursiveSplitWithTables_rank_eq_zero_of_budget_le tryTable enumeration + coefficientCount attempts fuel n [g] hsplit + have hbudget : stackSplitBudget [g] = g.toPoly.natDegree - 1 := by + simp [stackSplitBudget] + have hdeg := (CPolynomial.natDegree_toPoly g).symm + omega + · intro j hj + rw [Finset.mem_range] at hj + have hrank := recursiveSplitWithTables_rank_le_geometric tryTable enumeration + coefficientCount attempts hsplit hfail fuel [g] j n (by + intro x hx + rw [List.mem_singleton] at hx + subst hx + exact hvalid) + refine le_trans hrank ?_ + have hjk : j ≤ attempts := by omega + have hC : (1 : ℝ≥0∞) ≤ (Nat.choose attempts j : ℝ≥0∞) := by + exact_mod_cast Nat.choose_pos hjk + exact le_mul_of_one_le_right' hC + +/-- +Generic recursive table bridge: the deterministic table-driven splitter, with +one independently sampled uniform probe table per processed factor, reaches +exhaustive enumeration anywhere in the recursion with probability at most the +binomial tail. +-/ +theorem recursiveSplitWithTables_fallback_probability_le_binomialTail {q : Nat} + (enumeration : FieldEnumeration F) + (g : CPolynomial F) (coefficientCount attempts fuel n : Nat) + (hsplit : ∀ (g : CPolynomial F), g ≠ 0 → + ∀ (table : List (CPolynomial F)) (children : Array (CPolynomial F)), + tryTable table g = some children → IsSplitStep g children) + (hfail : ∀ (g : CPolynomial F), lasVegasSplitterInput q g → + 2 ≤ CPolynomial.natDegree g → + (uniformProbeTablePMF enumeration coefficientCount attempts).toOuterMeasure + {table : List (CPolynomial F) | tryTable table g = none} ≤ + ((2 : ℝ≥0∞)⁻¹) ^ attempts) + (hvalid : lasVegasSplitterInput q g) + (hattempts : CPolynomial.natDegree g - 2 ≤ attempts) : + eventProbability + (uniformProbeTablesPMF enumeration coefficientCount attempts n) + {tables : List (List (CPolynomial F)) | + true ∈ recursiveSplitWithTables tryTable fuel [g] tables} ≤ + binomialFallbackTail attempts (CPolynomial.natDegree g) := + recursiveFallback_probability_le_binomialTail _ _ _ _ + (recursiveSplitWithTables_recursiveFallbackProbabilityModel tryTable enumeration g + coefficientCount attempts fuel n hsplit hfail hvalid hattempts) + +end RecursiveTables + +/-- Multi-factor odd Cantor-Zassenhaus splitting driven by probe tables. -/ +def recursiveOddSplitWithTables {F : Type*} [Field F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + (q attempts : Nat) : + Nat → List (CPolynomial F) → List (List (CPolynomial F)) → List Bool := + recursiveSplitWithTables fun table g ↦ + tryOddSplitAttemptsWith M D q (tableProbeFamily table) g attempts 0 + +/-- +Recursive table bridge for the odd branch: the deterministic table-driven +splitter, run on a splitter-valid input of degree `d` with one independently +sampled uniform probe table per processed factor, reaches exhaustive +enumeration anywhere in the recursion with probability at most the binomial +tail `2 ^ -attempts * ∑_{j ≤ d - 2} C(attempts, j)`. + +The number `n` of supplied tables is arbitrary: missing tables only truncate +the run, and `d - 1` tables always suffice. +-/ +theorem recursiveOddSplitWithTables_fallback_probability_le_binomialTail {F : Type*} + [Field F] [Fintype F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + {q : Nat} (enumeration : UniformFieldEnumeration F q) + (g : CPolynomial F) (coefficientCount attempts fuel n : Nat) + (hcount : 2 ≤ coefficientCount) + (hfield : OddUniformFieldModel F q enumeration) + (hvalid : lasVegasSplitterInput q g) + (hattempts : CPolynomial.natDegree g - 2 ≤ attempts) : + eventProbability + (uniformProbeTablesPMF enumeration.toFieldEnumeration coefficientCount attempts n) + {tables : List (List (CPolynomial F)) | + true ∈ recursiveOddSplitWithTables M D q attempts fuel [g] tables} ≤ + binomialFallbackTail attempts (CPolynomial.natDegree g) := by + apply recursiveSplitWithTables_fallback_probability_le_binomialTail _ + enumeration.toFieldEnumeration g coefficientCount attempts fuel n + (fun _g hg _table _children htry ↦ + tryOddSplitAttemptsWith_isSplitStep M D q _ hg htry) + ?_ hvalid hattempts + intro g hvalid hdeg + have hinput : RootProductProbabilityInput q g := + rootProductProbabilityInput_of_lasVegasSplitterInput hvalid hfield.card_eq + have hbridge := tryOddSplitAttemptsWith_uniformTable_none_le_geometric M D + enumeration.toFieldEnumeration q coefficientCount g + (oddSplitTrial_success_probability_ge_half_of_two_le M D enumeration g + coefficientCount 0 hcount hfield (hinput.hasTwoDistinctRoots hdeg) hvalid.1) + attempts + rwa [eventProbability] at hbridge + +/-- +Per-factor table-driven split attempt of the full Las Vegas backend: the odd +Cantor-Zassenhaus retry loop for odd `q`, the characteristic-two trace retry +loop when trace metadata is supplied, mirroring the branch selection of +`lasVegasSplitLoopWith`. +-/ +def lasVegasTryTableSplit {F : Type*} [Field F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + (traceCtx? : Option (SmallPrimeTraceContext F)) (q attempts : Nat) + (table : List (CPolynomial F)) (g : CPolynomial F) : + Option (Array (CPolynomial F)) := + match traceCtx? with + | some traceCtx => + if q % 2 = 1 then + tryOddSplitAttemptsWith M D q (tableProbeFamily table) g attempts 0 + else + tryEvenTraceSplitAttemptsWith M D traceCtx q (tableProbeFamily table) g + attempts 0 + | none => tryOddSplitAttemptsWith M D q (tableProbeFamily table) g attempts 0 + +/-- The full Las Vegas backend driven by per-factor probe tables. -/ +def lasVegasSplitWithTables {F : Type*} [Field F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + (traceCtx? : Option (SmallPrimeTraceContext F)) (q attempts : Nat) : + Nat → List (CPolynomial F) → List (List (CPolynomial F)) → List Bool := + recursiveSplitWithTables (lasVegasTryTableSplit M D traceCtx? q attempts) + +/-- Successful backend table attempts are split steps. -/ +theorem lasVegasTryTableSplit_isSplitStep {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + (traceCtx? : Option (SmallPrimeTraceContext F)) (q attempts : Nat) + {g : CPolynomial F} (hg : g ≠ 0) + {table : List (CPolynomial F)} {children : Array (CPolynomial F)} + (htry : lasVegasTryTableSplit M D traceCtx? q attempts table g = some children) : + IsSplitStep g children := by + unfold lasVegasTryTableSplit at htry + cases traceCtx? with + | none => exact tryOddSplitAttemptsWith_isSplitStep M D q _ hg htry + | some traceCtx => + dsimp only at htry + by_cases hodd : q % 2 = 1 + · rw [if_pos hodd] at htry + exact tryOddSplitAttemptsWith_isSplitStep M D q _ hg htry + · rw [if_neg hodd] at htry + exact tryEvenTraceSplitAttemptsWith_isSplitStep M D traceCtx q _ hg htry + +/-- Backend table attempts on splitter-valid factors fail with probability at +most `2 ^ -attempts`. -/ +theorem lasVegasTryTableSplit_uniformTable_none_le_geometric {F : Type*} + [Field F] [Fintype F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + (traceCtx? : Option (SmallPrimeTraceContext F)) + {q : Nat} (enumeration : UniformFieldEnumeration F q) + (coefficientCount attempts : Nat) + (hcount : 2 ≤ coefficientCount) + (hmodel : LasVegasUniformFieldModel F q traceCtx? enumeration) + (g : CPolynomial F) (hvalid : lasVegasSplitterInput q g) + (hdegree : 2 ≤ CPolynomial.natDegree g) : + (uniformProbeTablePMF enumeration.toFieldEnumeration coefficientCount + attempts).toOuterMeasure + {table : List (CPolynomial F) | + lasVegasTryTableSplit M D traceCtx? q attempts table g = none} ≤ + ((2 : ℝ≥0∞)⁻¹) ^ attempts := by + have hinput : RootProductProbabilityInput q g := + rootProductProbabilityInput_of_lasVegasSplitterInput hvalid hmodel.card_eq + have hg : g ≠ 0 := hvalid.1 + cases hmodel with + | odd hodd hfield => + have hset : {table : List (CPolynomial F) | + lasVegasTryTableSplit M D traceCtx? q attempts table g = none} = + {table : List (CPolynomial F) | + tryOddSplitAttemptsWith M D q (tableProbeFamily table) g attempts 0 = + none} := by + ext table + rw [Set.mem_ofPred_eq, Set.mem_ofPred_eq] + unfold lasVegasTryTableSplit + cases traceCtx? <;> simp [hodd] + rw [hset] + have hbridge := tryOddSplitAttemptsWith_uniformTable_none_le_geometric M D + enumeration.toFieldEnumeration q coefficientCount g + (oddSplitTrial_success_probability_ge_half_of_two_le M D enumeration g + coefficientCount 0 hcount hfield (hinput.hasTwoDistinctRoots hdegree) hg) + attempts + rwa [eventProbability] at hbridge + | evenTrace traceCtx hctx heven hfield => + have hset : {table : List (CPolynomial F) | + lasVegasTryTableSplit M D traceCtx? q attempts table g = none} = + {table : List (CPolynomial F) | + tryEvenTraceSplitAttemptsWith M D traceCtx q (tableProbeFamily table) g + attempts 0 = none} := by + ext table + rw [Set.mem_ofPred_eq, Set.mem_ofPred_eq] + unfold lasVegasTryTableSplit + rw [hctx] + dsimp only + rw [if_neg (by omega : ¬ q % 2 = 1)] + rw [hset] + have hbridge := tryEvenTraceSplitAttemptsWith_uniformTable_none_le_geometric M D + traceCtx enumeration.toFieldEnumeration q coefficientCount g + (evenTraceTrial_success_probability_ge_half_of_two_le M D traceCtx enumeration g + coefficientCount 0 hcount hfield (hinput.hasTwoDistinctRoots hdegree) hg) + attempts + rwa [eventProbability] at hbridge + +/-- +Backend recursive table bridge: over any finite field — odd, or binary with +matching trace metadata — the deterministic table-driven Las Vegas backend, run +on a splitter-valid input of degree `d` with one independently sampled uniform +probe table per processed factor, reaches exhaustive enumeration anywhere in +the recursion with probability at most the binomial tail +`2 ^ -attempts * ∑_{j ≤ d - 2} C(attempts, j)`. +-/ +theorem lasVegasSplitWithTables_fallback_probability_le_binomialTail {F : Type*} + [Field F] [Fintype F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + (traceCtx? : Option (SmallPrimeTraceContext F)) + {q : Nat} (enumeration : UniformFieldEnumeration F q) + (g : CPolynomial F) (coefficientCount attempts fuel n : Nat) + (hcount : 2 ≤ coefficientCount) + (hmodel : LasVegasUniformFieldModel F q traceCtx? enumeration) + (hvalid : lasVegasSplitterInput q g) + (hattempts : CPolynomial.natDegree g - 2 ≤ attempts) : + eventProbability + (uniformProbeTablesPMF enumeration.toFieldEnumeration coefficientCount attempts n) + {tables : List (List (CPolynomial F)) | + true ∈ lasVegasSplitWithTables M D traceCtx? q attempts fuel [g] tables} ≤ + binomialFallbackTail attempts (CPolynomial.natDegree g) := + recursiveSplitWithTables_fallback_probability_le_binomialTail _ + enumeration.toFieldEnumeration g coefficientCount attempts fuel n + (fun _g hg _table _children htry ↦ + lasVegasTryTableSplit_isSplitStep M D traceCtx? q attempts hg htry) + (fun g hvalid hdeg ↦ + lasVegasTryTableSplit_uniformTable_none_le_geometric M D traceCtx? enumeration + coefficientCount attempts hcount hmodel g hvalid hdeg) + hvalid hattempts + +end FiniteField + +end Roots + +end CPolynomial + +end CompPoly diff --git a/CompPoly/Univariate/Roots/LasVegas/Probability/Uniform.lean b/CompPoly/Univariate/Roots/LasVegas/Probability/Uniform.lean new file mode 100644 index 00000000..10555f47 --- /dev/null +++ b/CompPoly/Univariate/Roots/LasVegas/Probability/Uniform.lean @@ -0,0 +1,614 @@ +/- +Copyright (c) 2026 CompPoly Contributors. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Valerii Huhnin +-/ + +module + +-- These wrappers live in bare `public section`s, so their bodies are opaque +-- downstream while the proofs below step through the `Raw` layer they are defined by. +-- `import all` is the same-package implementation dependency for exactly this; see +-- `docs/wiki/module-system.md`. +import all CompPoly.Univariate.Basic +public import CompPoly.Univariate.Roots.LasVegas.Probability.Basic + +/-! +# Uniform Probe Distributions for Las Vegas Root Splitting + +Idealized uniform sources for the Las Vegas probability story: uniform field +elements induced by a lazy enumeration, independent uniform coefficient arrays, +and the uniform probe-polynomial distribution they induce. + +The deep target of this module is pair-evaluation uniformity: a uniform probe +with at least two sampled coefficients evaluates at two distinct field points +to a uniform pair on `F × F`. +-/ + +@[expose] public section + +open scoped Classical ENNReal NNReal BigOperators + +namespace CompPoly + +namespace CPolynomial + +namespace Roots + +namespace FiniteField + +/-- Uniform sampling from enumeration indices. -/ +noncomputable def uniformFieldIndexPMF {F : Type*} [Zero F] + (enumeration : FieldEnumeration F) : + PMF (Fin enumeration.size) := + PMF.ofFinset + (fun _ : Fin enumeration.size ↦ (enumeration.size : ℝ≥0∞)⁻¹) + Finset.univ + (by + simp only [Finset.sum_const, nsmul_eq_mul] + have hne : (enumeration.size : ℝ≥0∞) ≠ 0 := by + exact_mod_cast Nat.ne_of_gt enumeration.size_pos + simpa [Fintype.card_fin] using + ENNReal.mul_inv_cancel hne (ENNReal.natCast_ne_top enumeration.size)) + (by + intro i hi + exact (hi (Finset.mem_univ i)).elim) + +/-- Uniform field-element sampling induced by a lazy enumeration. -/ +noncomputable def uniformFieldElementPMF {F : Type*} [Zero F] + (enumeration : FieldEnumeration F) : + PMF F := + (uniformFieldIndexPMF enumeration).map enumeration.elem + +/-- Complete injective enumerations put every field element in the PMF support. -/ +theorem uniformFieldElementPMF_support_eq_univ {F : Type*} [Zero F] {q : Nat} + (enumeration : UniformFieldEnumeration F q) : + (uniformFieldElementPMF enumeration.toFieldEnumeration).support = Set.univ := by + ext a + simp [uniformFieldElementPMF, uniformFieldIndexPMF] + exact enumeration.toFieldEnumeration.complete a + +/-- Every field element has probability `1 / q` under a uniform field enumeration. -/ +theorem uniformFieldElementPMF_apply {F : Type*} [Zero F] {q : Nat} + (enumeration : UniformFieldEnumeration F q) (a : F) : + uniformFieldElementPMF enumeration.toFieldEnumeration a = (q : ℝ≥0∞)⁻¹ := by + rcases enumeration.toFieldEnumeration.complete a with ⟨i, hi⟩ + simp [uniformFieldElementPMF, uniformFieldIndexPMF, enumeration.size_eq_q] + trans ∑ j : Fin enumeration.toFieldEnumeration.size, if j = i then (q : ℝ≥0∞)⁻¹ else 0 + · apply Finset.sum_congr rfl + intro j _hj + by_cases hji : j = i + · subst j + simp [hi] + · have hne : a ≠ enumeration.toFieldEnumeration.elem j := by + intro haj + apply hji + apply enumeration.injective_elem + rw [hi] + exact haj.symm + simp [hne, hji] + · simp + +/-- +Uniform PMF over coefficient arrays of fixed length, sampled independently from +the supplied field enumeration. +-/ +noncomputable def uniformCoefficientArrayPMF {F : Type*} [Zero F] + (enumeration : FieldEnumeration F) : + Nat → PMF (Array F) + | 0 => pure #[] + | n + 1 => do + let coeffs ← uniformCoefficientArrayPMF enumeration n + let coeff ← uniformFieldElementPMF enumeration + pure (coeffs.push coeff) + +private theorem uniformCoefficientArrayPMF_support_size {F : Type*} [Zero F] + (enumeration : FieldEnumeration F) : + ∀ coefficientCount {coeffs : Array F}, + coeffs ∈ (uniformCoefficientArrayPMF enumeration coefficientCount).support → + coeffs.size = coefficientCount := by + intro coefficientCount + induction coefficientCount with + | zero => + intro coeffs hcoeffs + rw [uniformCoefficientArrayPMF] at hcoeffs + have heq : coeffs = #[] := + (PMF.mem_support_pure_iff (a := (#[] : Array F)) (a' := coeffs)).mp hcoeffs + simp [heq] + | succ n ih => + intro coeffs hcoeffs + rw [uniformCoefficientArrayPMF] at hcoeffs + change coeffs ∈ + ((uniformCoefficientArrayPMF enumeration n).bind fun coeffs0 ↦ + (uniformFieldElementPMF enumeration).bind fun coeff ↦ + pure (coeffs0.push coeff)).support at hcoeffs + rw [PMF.mem_support_bind_iff] at hcoeffs + rcases hcoeffs with ⟨coeffs0, hcoeffs0, htail⟩ + rw [PMF.mem_support_bind_iff] at htail + rcases htail with ⟨coeff, _hcoeff, hpush⟩ + have hpush_eq : coeffs = coeffs0.push coeff := + (PMF.mem_support_pure_iff (a := coeffs0.push coeff) (a' := coeffs)).mp hpush + subst coeffs + simp [ih hcoeffs0] + +private theorem ofArray_natDegree_lt_size_or_eq_zero {F : Type*} + [Zero F] [BEq F] [LawfulBEq F] (coeffs : Array F) : + (CPolynomial.ofArray coeffs).natDegree < coeffs.size ∨ CPolynomial.ofArray coeffs = 0 := by + cases hval : (CPolynomial.ofArray coeffs).val.size with + | zero => + right + apply CPolynomial.ext + exact Array.eq_empty_of_size_eq_zero hval + | succ n => + left + have hnat : (CPolynomial.ofArray coeffs).natDegree = n := by + simp [CPolynomial.natDegree, hval] + have htrim_le : (CPolynomial.ofArray coeffs).val.size ≤ coeffs.size := by + simpa [CPolynomial.ofArray] using + CPolynomial.Raw.Trim.size_le_size (p := (coeffs : CPolynomial.Raw F)) + omega + +/-- Uniform probe polynomial of degree bounded by the supplied coefficient count. -/ +noncomputable def uniformProbePMF {F : Type*} [Field F] [BEq F] [LawfulBEq F] + (enumeration : FieldEnumeration F) (coefficientCount : Nat) : + PMF (CPolynomial F) := + (uniformCoefficientArrayPMF enumeration coefficientCount).map fun coeffs ↦ + CPolynomial.ofArray coeffs + +/-- +Probes sampled with `coefficientCount` coefficients have degree below that +bound, unless zero. +-/ +theorem uniformProbePMF_support_natDegree_lt {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (enumeration : FieldEnumeration F) (coefficientCount : Nat) + {probe : CPolynomial F} + (hprobe : probe ∈ (uniformProbePMF enumeration coefficientCount).support) : + CPolynomial.natDegree probe < coefficientCount ∨ probe = 0 := by + rw [uniformProbePMF, PMF.mem_support_map_iff] at hprobe + rcases hprobe with ⟨coeffs, hcoeffs, hprobe_eq⟩ + subst probe + have hsize := uniformCoefficientArrayPMF_support_size enumeration coefficientCount hcoeffs + simpa [hsize] using ofArray_natDegree_lt_size_or_eq_zero (F := F) coeffs + +private theorem uniformCoefficientArrayPMF_zero {F : Type*} [Zero F] + (enumeration : FieldEnumeration F) : + uniformCoefficientArrayPMF enumeration 0 = PMF.pure #[] := by + rw [uniformCoefficientArrayPMF] + rfl + +private theorem uniformCoefficientArrayPMF_succ {F : Type*} [Zero F] + (enumeration : FieldEnumeration F) (n : Nat) : + uniformCoefficientArrayPMF enumeration (n + 1) = + (uniformCoefficientArrayPMF enumeration n).bind fun coeffs ↦ + (uniformFieldElementPMF enumeration).bind fun coeff ↦ + PMF.pure (coeffs.push coeff) := by + rw [uniformCoefficientArrayPMF] + rfl + +/-- Appending a coefficient shifts the evaluation by the corresponding monomial. -/ +private theorem eval_ofArray_push {F : Type*} [Field F] [BEq F] [LawfulBEq F] + (coeffs : Array F) (c : F) (z : F) : + CPolynomial.eval z (CPolynomial.ofArray (coeffs.push c)) = + CPolynomial.eval z (CPolynomial.ofArray coeffs) + c * z ^ coeffs.size := by + rw [CPolynomial.eval_toPoly, CPolynomial.eval_toPoly, + CPolynomial.ofArray_toPoly, CPolynomial.ofArray_toPoly] + have hpush : CPolynomial.Raw.toPoly (coeffs.push c) = + CPolynomial.Raw.toPoly coeffs + + Polynomial.C c * Polynomial.X ^ coeffs.size := by + apply Polynomial.ext + intro i + rw [Polynomial.coeff_add, CPolynomial.Raw.coeff_toPoly, CPolynomial.Raw.coeff_toPoly, + Polynomial.coeff_C_mul, Polynomial.coeff_X_pow] + show (coeffs.push c).getD i 0 = coeffs.getD i 0 + c * if i = coeffs.size then 1 else 0 + rw [Array.getD_eq_getD_getElem?, Array.getD_eq_getD_getElem?, Array.getElem?_push] + by_cases hi : i = coeffs.size + · subst hi + simp + · simp [hi] + rw [hpush, Polynomial.eval_add, Polynomial.eval_mul, Polynomial.eval_C, + Polynomial.eval_pow, Polynomial.eval_X] + +/-- Evaluation of the two-coefficient probe `c₀ + c₁ * X`. -/ +private theorem eval_ofArray_pair {F : Type*} [Field F] [BEq F] [LawfulBEq F] + (c0 c1 z : F) : + CPolynomial.eval z (CPolynomial.ofArray #[c0, c1]) = c0 + c1 * z := by + have hempty : CPolynomial.Raw.toPoly (#[] : Array F) = 0 := by + apply Polynomial.ext + intro i + rw [CPolynomial.Raw.coeff_toPoly] + simp [CPolynomial.Raw.coeff] + have h1 : (#[c0, c1] : Array F) = ((#[] : Array F).push c0).push c1 := rfl + rw [h1, eval_ofArray_push, eval_ofArray_push] + rw [CPolynomial.eval_toPoly, CPolynomial.ofArray_toPoly, hempty] + simp + +private theorem toOuterMeasure_congr_on_support {α : Type*} (p : PMF α) {S T : Set α} + (h : ∀ a ∈ p.support, (a ∈ S ↔ a ∈ T)) : + p.toOuterMeasure S = p.toOuterMeasure T := by + rw [PMF.toOuterMeasure_apply, PMF.toOuterMeasure_apply] + apply tsum_congr + intro a + by_cases hz : p a = 0 + · rw [Set.indicator_apply, Set.indicator_apply] + by_cases hS : a ∈ S <;> by_cases hT : a ∈ T <;> simp [hS, hT, hz] + · rw [Set.indicator_apply, Set.indicator_apply, + if_congr (h a ((p.mem_support_iff a).mpr hz)) rfl rfl] + +/-- +The two-coefficient uniform array hits any prescribed evaluation pair at two +distinct points with probability exactly `q⁻¹ * q⁻¹`. +-/ +private theorem uniformCoefficientArrayPMF_two_eval_pair {F : Type*} + [Field F] [BEq F] [LawfulBEq F] {q : Nat} + (enumeration : UniformFieldEnumeration F q) {a b : F} (hab : a ≠ b) (x y : F) : + eventProbability + (uniformCoefficientArrayPMF enumeration.toFieldEnumeration 2) + {coeffs : Array F | CPolynomial.eval a (CPolynomial.ofArray coeffs) = x ∧ + CPolynomial.eval b (CPolynomial.ofArray coeffs) = y} = + (q : ℝ≥0∞)⁻¹ * (q : ℝ≥0∞)⁻¹ := by + have hab' : a - b ≠ 0 := sub_ne_zero.mpr hab + set c1s : F := (x - y) / (a - b) with hc1s + set c0s : F := x - c1s * a with hc0s + have harr : uniformCoefficientArrayPMF enumeration.toFieldEnumeration 2 = + (uniformFieldElementPMF enumeration.toFieldEnumeration).bind fun c0 ↦ + (uniformFieldElementPMF enumeration.toFieldEnumeration).bind fun c1 ↦ + (PMF.pure #[c0, c1] : PMF (Array F)) := by + rw [show (2 : Nat) = 1 + 1 from rfl, uniformCoefficientArrayPMF_succ, + show (1 : Nat) = 0 + 1 from rfl, uniformCoefficientArrayPMF_succ, + uniformCoefficientArrayPMF_zero] + simp only [PMF.pure_bind, PMF.bind_bind] + rfl + have hmem : ∀ c0 c1 : F, + ((#[c0, c1] : Array F) ∈ + {coeffs : Array F | CPolynomial.eval a (CPolynomial.ofArray coeffs) = x ∧ + CPolynomial.eval b (CPolynomial.ofArray coeffs) = y}) ↔ + (c0 = c0s ∧ c1 = c1s) := by + intro c0 c1 + rw [Set.mem_ofPred_eq, eval_ofArray_pair, eval_ofArray_pair] + constructor + · rintro ⟨hx, hy⟩ + have hc1 : c1 = c1s := by + rw [hc1s, eq_div_iff hab'] + rw [← hx, ← hy] + ring + refine ⟨?_, hc1⟩ + rw [hc0s, ← hc1, ← hx] + ring + · rintro ⟨h0, h1⟩ + subst h0 + subst h1 + constructor + · rw [hc0s] + ring + · rw [hc0s, hc1s] + field_simp + ring + rw [eventProbability, harr, PMF.toOuterMeasure_bind_apply] + have hstep : ∀ c0 : F, + ((uniformFieldElementPMF enumeration.toFieldEnumeration).bind fun c1 ↦ + (PMF.pure #[c0, c1] : PMF (Array F))).toOuterMeasure + {coeffs : Array F | CPolynomial.eval a (CPolynomial.ofArray coeffs) = x ∧ + CPolynomial.eval b (CPolynomial.ofArray coeffs) = y} = + if c0 = c0s then (q : ℝ≥0∞)⁻¹ else 0 := by + intro c0 + rw [PMF.toOuterMeasure_bind_apply] + by_cases hc0 : c0 = c0s + · rw [if_pos hc0] + calc (∑' c1 : F, uniformFieldElementPMF enumeration.toFieldEnumeration c1 * + (PMF.pure #[c0, c1] : PMF (Array F)).toOuterMeasure + {coeffs : Array F | CPolynomial.eval a (CPolynomial.ofArray coeffs) = x ∧ + CPolynomial.eval b (CPolynomial.ofArray coeffs) = y}) + = ∑' c1 : F, if c1 = c1s then + uniformFieldElementPMF enumeration.toFieldEnumeration c1 else 0 := by + apply tsum_congr + intro c1 + rw [PMF.toOuterMeasure_pure_apply] + by_cases hc1 : c1 = c1s + · rw [if_pos ((hmem c0 c1).2 ⟨hc0, hc1⟩), if_pos hc1] + simp + · rw [if_neg (fun hin ↦ hc1 ((hmem c0 c1).1 hin).2), if_neg hc1] + simp + _ = uniformFieldElementPMF enumeration.toFieldEnumeration c1s := + tsum_ite_eq c1s _ + _ = (q : ℝ≥0∞)⁻¹ := uniformFieldElementPMF_apply enumeration c1s + · rw [if_neg hc0] + calc (∑' c1 : F, uniformFieldElementPMF enumeration.toFieldEnumeration c1 * + (PMF.pure #[c0, c1] : PMF (Array F)).toOuterMeasure + {coeffs : Array F | CPolynomial.eval a (CPolynomial.ofArray coeffs) = x ∧ + CPolynomial.eval b (CPolynomial.ofArray coeffs) = y}) + = ∑' _c1 : F, (0 : ℝ≥0∞) := by + apply tsum_congr + intro c1 + rw [PMF.toOuterMeasure_pure_apply, + if_neg (fun hin ↦ hc0 ((hmem c0 c1).1 hin).1)] + simp + _ = 0 := tsum_zero + calc (∑' c0 : F, uniformFieldElementPMF enumeration.toFieldEnumeration c0 * + ((uniformFieldElementPMF enumeration.toFieldEnumeration).bind fun c1 ↦ + (PMF.pure #[c0, c1] : PMF (Array F))).toOuterMeasure + {coeffs : Array F | CPolynomial.eval a (CPolynomial.ofArray coeffs) = x ∧ + CPolynomial.eval b (CPolynomial.ofArray coeffs) = y}) + = ∑' c0 : F, if c0 = c0s then + uniformFieldElementPMF enumeration.toFieldEnumeration c0 * (q : ℝ≥0∞)⁻¹ + else 0 := by + apply tsum_congr + intro c0 + rw [hstep c0] + by_cases hc0 : c0 = c0s + · rw [if_pos hc0, if_pos hc0] + · rw [if_neg hc0, if_neg hc0] + simp + _ = uniformFieldElementPMF enumeration.toFieldEnumeration c0s * (q : ℝ≥0∞)⁻¹ := + tsum_ite_eq c0s _ + _ = (q : ℝ≥0∞)⁻¹ * (q : ℝ≥0∞)⁻¹ := by + rw [uniformFieldElementPMF_apply enumeration c0s] + +/-- +The uniform coefficient-array source hits any prescribed evaluation pair at two +distinct points with probability exactly `q⁻¹ * q⁻¹`, for any coefficient count +of at least two. +-/ +private theorem uniformCoefficientArrayPMF_eval_pair {F : Type*} + [Field F] [BEq F] [LawfulBEq F] {q : Nat} + (enumeration : UniformFieldEnumeration F q) {a b : F} (hab : a ≠ b) : + ∀ n : Nat, 2 ≤ n → ∀ x y : F, + eventProbability + (uniformCoefficientArrayPMF enumeration.toFieldEnumeration n) + {coeffs : Array F | CPolynomial.eval a (CPolynomial.ofArray coeffs) = x ∧ + CPolynomial.eval b (CPolynomial.ofArray coeffs) = y} = + (q : ℝ≥0∞)⁻¹ * (q : ℝ≥0∞)⁻¹ := by + intro n hn + induction n, hn using Nat.le_induction with + | base => + intro x y + exact uniformCoefficientArrayPMF_two_eval_pair enumeration hab x y + | succ n hn ih => + intro x y + rw [eventProbability, uniformCoefficientArrayPMF_succ, PMF.bind_comm, + PMF.toOuterMeasure_bind_apply] + have hterm : ∀ c : F, + ((uniformCoefficientArrayPMF enumeration.toFieldEnumeration n).bind + fun coeffs ↦ (PMF.pure (coeffs.push c) : PMF (Array F))).toOuterMeasure + {coeffs : Array F | CPolynomial.eval a (CPolynomial.ofArray coeffs) = x ∧ + CPolynomial.eval b (CPolynomial.ofArray coeffs) = y} = + (q : ℝ≥0∞)⁻¹ * (q : ℝ≥0∞)⁻¹ := by + intro c + have hmapped : ((uniformCoefficientArrayPMF enumeration.toFieldEnumeration n).bind + fun coeffs ↦ (PMF.pure (coeffs.push c) : PMF (Array F))) = + (uniformCoefficientArrayPMF enumeration.toFieldEnumeration n).map + (fun coeffs ↦ coeffs.push c) := rfl + have hcongr := toOuterMeasure_congr_on_support + (p := uniformCoefficientArrayPMF enumeration.toFieldEnumeration n) + (S := (fun coeffs : Array F ↦ coeffs.push c) ⁻¹' + {coeffs : Array F | CPolynomial.eval a (CPolynomial.ofArray coeffs) = x ∧ + CPolynomial.eval b (CPolynomial.ofArray coeffs) = y}) + (T := {coeffs : Array F | + CPolynomial.eval a (CPolynomial.ofArray coeffs) = x - c * a ^ n ∧ + CPolynomial.eval b (CPolynomial.ofArray coeffs) = y - c * b ^ n}) + (fun coeffs hsupp ↦ by + have hsize := uniformCoefficientArrayPMF_support_size + enumeration.toFieldEnumeration n hsupp + rw [Set.mem_preimage, Set.mem_ofPred_eq, Set.mem_ofPred_eq, + eval_ofArray_push, eval_ofArray_push, hsize] + constructor + · rintro ⟨h1, h2⟩ + constructor + · rw [← h1]; ring + · rw [← h2]; ring + · rintro ⟨h1, h2⟩ + rw [h1, h2] + constructor <;> ring) + rw [hmapped, PMF.toOuterMeasure_map_apply, hcongr] + have hev := ih (x - c * a ^ n) (y - c * b ^ n) + rw [eventProbability] at hev + exact hev + trans (∑' c : F, uniformFieldElementPMF enumeration.toFieldEnumeration c * + ((q : ℝ≥0∞)⁻¹ * (q : ℝ≥0∞)⁻¹)) + · exact tsum_congr fun c ↦ by rw [hterm c] + · rw [ENNReal.tsum_mul_right, PMF.tsum_coe, _root_.one_mul] + +/-- +Pair-evaluation uniformity: a uniform probe with at least two sampled +coefficients evaluates at two distinct points to a uniform pair on `F × F`. + +For a fixed coefficient tail, the map +`(c₀, c₁) ↦ (c₀ + c₁ * a + tail(a), c₀ + c₁ * b + tail(b))` is a bijection of +`F × F` because `a ≠ b`, so exactly one of the `q ^ 2` equally likely +`(c₀, c₁)` choices hits the target pair `(x, y)`. +-/ +theorem uniformProbePMF_eval_pair_apply {F : Type*} + [Field F] [BEq F] [LawfulBEq F] {q : Nat} + (enumeration : UniformFieldEnumeration F q) + (coefficientCount : Nat) {a b : F} (x y : F) + (hab : a ≠ b) (hcount : 2 ≤ coefficientCount) : + eventProbability + (uniformProbePMF enumeration.toFieldEnumeration coefficientCount) + {h | CPolynomial.eval a h = x ∧ CPolynomial.eval b h = y} = + (q : ℝ≥0∞)⁻¹ * (q : ℝ≥0∞)⁻¹ := by + rw [eventProbability, uniformProbePMF, PMF.toOuterMeasure_map_apply] + have hev := uniformCoefficientArrayPMF_eval_pair enumeration hab + coefficientCount hcount x y + rw [eventProbability] at hev + exact hev + +/-- Independent uniform probe tables of a fixed length. -/ +noncomputable def uniformProbeTablePMF {F : Type*} [Field F] [BEq F] [LawfulBEq F] + (enumeration : FieldEnumeration F) (coefficientCount : Nat) : + Nat → PMF (List (CPolynomial F)) + | 0 => PMF.pure [] + | attempts + 1 => + (uniformProbePMF enumeration coefficientCount).bind fun h ↦ + (uniformProbeTablePMF enumeration coefficientCount attempts).map (List.cons h) + +/-- Every pair of field elements is equally likely under two independent uniform draws. -/ +theorem uniformFieldElementPMF_pair_apply {F : Type*} [Zero F] {q : Nat} + (enumeration : UniformFieldEnumeration F q) (xy : F × F) : + ((uniformFieldElementPMF enumeration.toFieldEnumeration).bind + fun x ↦ (uniformFieldElementPMF enumeration.toFieldEnumeration).map + fun y ↦ (x, y)) xy = (q : ℝ≥0∞)⁻¹ * (q : ℝ≥0∞)⁻¹ := by + obtain ⟨x, y⟩ := xy + rw [PMF.bind_apply] + have hxy : ∀ x' : F, + ((uniformFieldElementPMF enumeration.toFieldEnumeration).map + fun y' ↦ (x', y')) (x, y) = + if x' = x then uniformFieldElementPMF enumeration.toFieldEnumeration y else 0 := by + intro x' + rw [PMF.map_apply] + by_cases hx : x' = x + · subst hx + rw [if_pos rfl] + trans (∑' y' : F, if y' = y then + uniformFieldElementPMF enumeration.toFieldEnumeration y' else 0) + · apply tsum_congr + intro y' + by_cases hy : y' = y + · subst hy + simp + · rw [if_neg fun hp ↦ hy (Prod.ext_iff.mp hp).2.symm, if_neg hy] + · exact tsum_ite_eq y _ + · rw [if_neg hx] + trans (∑' _y' : F, (0 : ℝ≥0∞)) + · apply tsum_congr + intro y' + rw [if_neg fun hp ↦ hx ((Prod.ext_iff.mp hp).1.symm)] + · exact tsum_zero + trans (∑' x' : F, if x' = x then + uniformFieldElementPMF enumeration.toFieldEnumeration x' * + uniformFieldElementPMF enumeration.toFieldEnumeration y else 0) + · apply tsum_congr + intro x' + rw [hxy x'] + by_cases hx : x' = x + · rw [if_pos hx, if_pos hx] + · rw [if_neg hx, if_neg hx] + simp + · rw [tsum_ite_eq x fun x' ↦ uniformFieldElementPMF enumeration.toFieldEnumeration x' * + uniformFieldElementPMF enumeration.toFieldEnumeration y, + uniformFieldElementPMF_apply enumeration x, + uniformFieldElementPMF_apply enumeration y] + +/-- +The evaluation pair of a uniform probe at two distinct points pushes forward to +two independent uniform field elements. +-/ +theorem uniformProbePMF_map_eval_pair {F : Type*} + [Field F] [BEq F] [LawfulBEq F] {q : Nat} + (enumeration : UniformFieldEnumeration F q) (coefficientCount : Nat) {a b : F} + (hab : a ≠ b) (hcount : 2 ≤ coefficientCount) : + (uniformProbePMF enumeration.toFieldEnumeration coefficientCount).map + (fun h ↦ (CPolynomial.eval a h, CPolynomial.eval b h)) = + (uniformFieldElementPMF enumeration.toFieldEnumeration).bind + fun x ↦ (uniformFieldElementPMF enumeration.toFieldEnumeration).map + fun y ↦ (x, y) := by + apply PMF.ext + intro xy + rw [uniformFieldElementPMF_pair_apply enumeration xy] + obtain ⟨x, y⟩ := xy + rw [← PMF.toOuterMeasure_apply_singleton, PMF.toOuterMeasure_map_apply] + have hpre : ((fun h : CPolynomial F ↦ + (CPolynomial.eval a h, CPolynomial.eval b h)) ⁻¹' {(x, y)}) = + {h : CPolynomial F | CPolynomial.eval a h = x ∧ CPolynomial.eval b h = y} := by + ext h + simp [Prod.ext_iff] + rw [hpre] + exact uniformProbePMF_eval_pair_apply enumeration coefficientCount x y hab hcount + +/-- +If every fiber of a classifier has at most `m` elements out of `q ≤ 2 * (q - m)`, +two independent uniform field values are separated by the classifier with +probability at least `1 / 2`. +-/ +theorem uniformPair_separated_probability_ge_half_of_fiber_card_le {F : Type*} + [Zero F] [Fintype F] {β : Type*} {q m : Nat} + (enumeration : UniformFieldEnumeration F q) (f : F → β) + (hcard : Fintype.card F = q) + (hfiber : ∀ x : F, ∃ s : Finset F, (∀ y : F, f y = f x → y ∈ s) ∧ s.card ≤ m) + (hhalf : q ≤ 2 * (q - m)) : + (2 : ℝ≥0∞)⁻¹ ≤ + eventProbability + ((uniformFieldElementPMF enumeration.toFieldEnumeration).bind + fun x ↦ (uniformFieldElementPMF enumeration.toFieldEnumeration).map + fun y ↦ (x, y)) + {xy : F × F | f xy.1 ≠ f xy.2} := by + have hq1 : 1 ≤ q := enumeration.q_pos + have hqne : (q : ℝ≥0∞) ≠ 0 := Nat.cast_ne_zero.mpr (by omega) + have hqnetop : (q : ℝ≥0∞) ≠ ⊤ := ENNReal.natCast_ne_top q + set u := uniformFieldElementPMF enumeration.toFieldEnumeration with hu + have hN_ge : ∀ x : F, q - m ≤ + (Finset.univ.filter fun y : F ↦ f x ≠ f y).card := by + intro x + have hpart := Finset.card_filter_add_card_filter_not (s := (Finset.univ : Finset F)) + (fun y : F ↦ f x = f y) + have hsame : (Finset.univ.filter fun y : F ↦ f x = f y).card ≤ m := by + obtain ⟨s, hsub, hcard_s⟩ := hfiber x + refine le_trans (Finset.card_le_card fun y hy ↦ ?_) hcard_s + exact hsub y (Finset.mem_filter.mp hy).2.symm + have hcardu : (Finset.univ : Finset F).card = q := by + rw [Finset.card_univ, hcard] + have hfeq : (Finset.univ.filter fun y : F ↦ f x ≠ f y) = + Finset.univ.filter fun y : F ↦ ¬ f x = f y := by + apply Finset.filter_congr + intro y _ + exact Iff.rfl + rw [hfeq] + omega + have hinner : ∀ x : F, + u.toOuterMeasure {y : F | f x ≠ f y} = + ((Finset.univ.filter fun y : F ↦ f x ≠ f y).card : ℝ≥0∞) * (q : ℝ≥0∞)⁻¹ := by + intro x + rw [PMF.toOuterMeasure_apply, tsum_fintype, Finset.sum_indicator_eq_sum_filter] + have hfeq : (Finset.univ.filter fun y : F ↦ y ∈ {y : F | f x ≠ f y}) = + Finset.univ.filter fun y : F ↦ f x ≠ f y := by + apply Finset.filter_congr + intro y _ + exact Iff.rfl + rw [hfeq] + calc (∑ y ∈ Finset.univ.filter (fun y : F ↦ f x ≠ f y), u y) + = ∑ _y ∈ Finset.univ.filter (fun y : F ↦ f x ≠ f y), (q : ℝ≥0∞)⁻¹ := + Finset.sum_congr rfl fun y _ ↦ by + rw [hu] + exact uniformFieldElementPMF_apply enumeration y + _ = _ := by rw [Finset.sum_const, nsmul_eq_mul] + have hmap : ∀ x : F, + (u.map fun y ↦ (x, y)).toOuterMeasure {xy : F × F | f xy.1 ≠ f xy.2} = + ((Finset.univ.filter fun y : F ↦ f x ≠ f y).card : ℝ≥0∞) * (q : ℝ≥0∞)⁻¹ := by + intro x + rw [PMF.toOuterMeasure_map_apply] + exact hinner x + have hev : eventProbability (u.bind fun x ↦ u.map fun y ↦ (x, y)) + {xy : F × F | f xy.1 ≠ f xy.2} = + ∑' x : F, u x * + (((Finset.univ.filter fun y : F ↦ f x ≠ f y).card : ℝ≥0∞) * (q : ℝ≥0∞)⁻¹) := by + rw [eventProbability, PMF.toOuterMeasure_bind_apply] + exact tsum_congr fun x ↦ by rw [hmap x] + have hstepA : (2 : ℝ≥0∞)⁻¹ ≤ ((q - m : ℕ) : ℝ≥0∞) * (q : ℝ≥0∞)⁻¹ := by + rw [← div_eq_mul_inv, ENNReal.le_div_iff_mul_le (Or.inl hqne) (Or.inl hqnetop)] + calc (2 : ℝ≥0∞)⁻¹ * q + ≤ (2 : ℝ≥0∞)⁻¹ * ((2 * (q - m) : ℕ) : ℝ≥0∞) := + _root_.mul_le_mul_right (by exact_mod_cast hhalf) _ + _ = ((q - m : ℕ) : ℝ≥0∞) := by + push_cast + rw [← _root_.mul_assoc, ENNReal.inv_mul_cancel two_ne_zero ENNReal.ofNat_ne_top, + _root_.one_mul] + rw [hev] + calc (2 : ℝ≥0∞)⁻¹ + ≤ ((q - m : ℕ) : ℝ≥0∞) * (q : ℝ≥0∞)⁻¹ := hstepA + _ = ∑' _x : F, (q : ℝ≥0∞)⁻¹ * (((q - m : ℕ) : ℝ≥0∞) * (q : ℝ≥0∞)⁻¹) := by + rw [tsum_fintype, Finset.sum_const, Finset.card_univ, hcard, nsmul_eq_mul, + ← _root_.mul_assoc, ENNReal.mul_inv_cancel hqne hqnetop, _root_.one_mul] + _ ≤ ∑' x : F, u x * + (((Finset.univ.filter fun y : F ↦ f x ≠ f y).card : ℝ≥0∞) * (q : ℝ≥0∞)⁻¹) := by + apply ENNReal.tsum_le_tsum + intro x + rw [hu, uniformFieldElementPMF_apply enumeration x] + exact _root_.mul_le_mul_right + (_root_.mul_le_mul_left (Nat.cast_le.mpr (hN_ge x)) _) _ + +end FiniteField + +end Roots + +end CPolynomial + +end CompPoly diff --git a/CompPoly/Univariate/Roots/Shoup.lean b/CompPoly/Univariate/Roots/Shoup.lean new file mode 100644 index 00000000..b1d8f231 --- /dev/null +++ b/CompPoly/Univariate/Roots/Shoup.lean @@ -0,0 +1,19 @@ +/- +Copyright (c) 2026 CompPoly Contributors. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Valerii Huhnin +-/ + +module + +public import CompPoly.Univariate.Roots.Shoup.Basic +public import CompPoly.Univariate.Roots.Shoup.Correctness +public import CompPoly.Univariate.Roots.Shoup.FrobeniusLinear + +/-! +# Shoup-Style Small-Characteristic Root Splitting + +Public module for the Shoup-style trace splitter. +-/ + +@[expose] public section diff --git a/CompPoly/Univariate/Roots/Shoup/Basic.lean b/CompPoly/Univariate/Roots/Shoup/Basic.lean new file mode 100644 index 00000000..caee305b --- /dev/null +++ b/CompPoly/Univariate/Roots/Shoup/Basic.lean @@ -0,0 +1,443 @@ +/- +Copyright (c) 2026 CompPoly Contributors. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Valerii Huhnin +-/ + +module + +public import CompPoly.Univariate.Roots.RootProduct +public import CompPoly.Univariate.Roots.Splitter +public import CompPoly.Univariate.ToPoly.Core + +/-! +# Shoup-Style Small-Characteristic Linear-Factor Splitting + +Executable trace-coordinate splitter for finite-field root products over fields +presented as `GF(p^k)` with small base characteristic `p`. The splitter refines a +valid root product by `k` trace coordinates and the `p` embedded base constants; +it does not enumerate all `p^k` field elements, following the small-characteristic +Frobenius-map factoring method of von zur Gathen and Shoup [vzGS92]. + +## References + +* [J. von zur Gathen and Victor Shoup, *Computing Frobenius maps and factoring + polynomials*, Computational Complexity 2, 187-224, 1992][vzGS92] +-/ + +@[expose] public section + +namespace CompPoly + +namespace CPolynomial + +namespace Roots + +namespace FiniteField + +/-- Trace power sum `z + z^p + ... + z^(p^(k-1))`. -/ +def tracePowerSum {F : Type*} [Field F] (p k : Nat) (z : F) : F := + (List.range k).foldl (fun acc i ↦ acc + z ^ (p ^ i)) 0 + +/-- The list-fold trace power sum is the corresponding finite range sum. -/ +theorem tracePowerSum_eq_sum_range {F : Type*} [Field F] (p k : Nat) (z : F) : + tracePowerSum p k z = ∑ i ∈ Finset.range k, z ^ (p ^ i) := by + unfold tracePowerSum + induction k with + | zero => + simp + | succ k ih => + rw [List.range_succ, List.foldl_append] + simp [ih, Finset.sum_range_succ] + +/-- +Executable and proof data for a Shoup-style small-characteristic trace splitter. + +The field-theoretic facts are carried by the context so the generic executable +splitter can stay representation-independent. Concrete fields can discharge the +trace-image and separating-basis obligations using their own basis libraries. +-/ +structure SmallPrimeTraceContext (F : Type*) [Field F] [BEq F] [LawfulBEq F] + extends FiniteFieldContext F where + p : Nat + k : Nat + p_prime : Nat.Prime p + q_eq : q = p ^ k + baseConstants : Array F + baseConstants_size : baseConstants.size = p + basis : Array F + basis_size : basis.size = k + traceValue : F → F + traceValue_eq_powerSum : ∀ z, traceValue z = tracePowerSum p k z + traceValue_mem_base : ∀ z, traceValue z ∈ baseConstants.toList + trace_separates : + ∀ {a b : F}, a ≠ b → + ∃ beta, beta ∈ basis.toList ∧ traceValue (beta * (a - b)) ≠ 0 + +/-- +Input predicate for the Shoup splitter. + +Completeness is only claimed for nonzero products of distinct field-linear +factors, represented here by divisibility into `X^q - X`. This is the contract +satisfied by the finite-field root product, not by arbitrary input polynomials. +-/ +def shoupSplitterInput {F : Type*} [Field F] [BEq F] [LawfulBEq F] + (ctx : SmallPrimeTraceContext F) (p : CPolynomial F) : Prop := + p ≠ 0 ∧ + p.toPoly ∣ ((Polynomial.X : Polynomial F) ^ ctx.q - Polynomial.X) + +/-- Modular powers `X^(p^i) mod modulus` for `0 ≤ i < k`. -/ +def modularXPowersWith {F : Type*} [Field F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + (modulus : CPolynomial F) (p k : Nat) : Array (CPolynomial F) := + Array.ofFn fun i : Fin k ↦ xPowModWith M D modulus (p ^ i.val) + +/-- `Trace(beta * X) mod modulus`, using precomputed modular Frobenius powers. -/ +def traceCoordinatePolynomialFromPowers {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (ctx : SmallPrimeTraceContext F) (beta : F) + (powers : Array (CPolynomial F)) : + CPolynomial F := + (List.range ctx.k).foldl + (fun acc i ↦ + acc + CPolynomial.C (beta ^ (ctx.p ^ i)) * powers.getD i 0) + 0 + +/-- `Trace(beta * X) mod modulus`, built from the modular Frobenius powers. -/ +def traceCoordinatePolynomialWith {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + (ctx : SmallPrimeTraceContext F) (modulus : CPolynomial F) (beta : F) : + CPolynomial F := + (List.range ctx.k).foldl + (fun acc i ↦ + acc + CPolynomial.C (beta ^ (ctx.p ^ i)) * + xPowModWith M D modulus (ctx.p ^ i)) + 0 + +private theorem traceCoordinatePolynomialFromPowers_modularXPowersWith {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + (ctx : SmallPrimeTraceContext F) (modulus : CPolynomial F) (beta : F) : + traceCoordinatePolynomialFromPowers ctx beta + (modularXPowersWith M D modulus ctx.p ctx.k) = + traceCoordinatePolynomialWith M D ctx modulus beta := by + unfold traceCoordinatePolynomialFromPowers traceCoordinatePolynomialWith modularXPowersWith + have hget : ∀ i, i ∈ List.range ctx.k → + Array.getD + (Array.ofFn fun i : Fin ctx.k ↦ xPowModWith M D modulus (ctx.p ^ i.val)) + i 0 = + xPowModWith M D modulus (ctx.p ^ i) := by + intro i hi + have hi' : i < ctx.k := List.mem_range.mp hi + unfold Array.getD + simp [hi'] + have hfold : ∀ (xs : List Nat) (acc : CPolynomial F), + (∀ i, i ∈ xs → + Array.getD + (Array.ofFn fun i : Fin ctx.k ↦ xPowModWith M D modulus (ctx.p ^ i.val)) + i 0 = + xPowModWith M D modulus (ctx.p ^ i)) → + List.foldl + (fun acc i ↦ acc + CPolynomial.C (beta ^ ctx.p ^ i) * + Array.getD + (Array.ofFn fun i : Fin ctx.k ↦ xPowModWith M D modulus (ctx.p ^ i.val)) + i 0) + acc xs = + List.foldl + (fun acc i ↦ acc + CPolynomial.C (beta ^ ctx.p ^ i) * + xPowModWith M D modulus (ctx.p ^ i)) + acc xs := by + intro xs + induction xs with + | nil => + intro acc _hxs + simp + | cons i xs ih => + intro acc hxs + simp only [List.foldl_cons] + rw [hxs i (by simp)] + apply ih + intro j hj + exact hxs j (by simp [hj]) + exact hfold (List.range ctx.k) 0 hget + +/-- Drop zero/unit children and keep the remaining monic gcd child. -/ +def pushNontrivialChild {F : Type*} [Field F] [BEq F] [LawfulBEq F] + (children : Array (CPolynomial F)) (child : CPolynomial F) : + Array (CPolynomial F) := + if child == 0 || child == 1 then children else children.push child + +/-- Refine one current factor by one trace coordinate and all base constants. -/ +def shoupRefineFactorWith {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + (ctx : SmallPrimeTraceContext F) (beta : F) (u : CPolynomial F) : + Array (CPolynomial F) := + let u := CPolynomial.monicNormalize u + if u == 0 || u == 1 then + #[] + else if isRepresentedLinearFactor u then + #[u] + else + let tracePoly := traceCoordinatePolynomialWith M D ctx u beta + ctx.baseConstants.foldl + (fun children c ↦ + let child := CPolynomial.monicNormalize + (CPolynomial.gcdMonic u (tracePoly - CPolynomial.C c)) + pushNontrivialChild children child) + #[] + +/-- Refine all current factors by one trace coordinate. -/ +def shoupRefineFactorsWith {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + (ctx : SmallPrimeTraceContext F) (beta : F) (factors : Array (CPolynomial F)) : + Array (CPolynomial F) := + factors.foldl + (fun out factor ↦ out ++ shoupRefineFactorWith M D ctx beta factor) + #[] + +abbrev ShoupTracePowerCache (F : Type*) [Field F] [BEq F] [LawfulBEq F] := + List (CPolynomial F × Array (CPolynomial F)) + +private def lookupTracePowers {F : Type*} [Field F] [BEq F] [LawfulBEq F] + (u : CPolynomial F) : + ShoupTracePowerCache F → Option (Array (CPolynomial F)) + | [] => none + | (v, powers) :: rest => + if u == v then + some powers + else + lookupTracePowers u rest + +private def tracePowersWithCache {F : Type*} [Field F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + (ctx : SmallPrimeTraceContext F) (u : CPolynomial F) + (cache : ShoupTracePowerCache F) : + Array (CPolynomial F) × ShoupTracePowerCache F := + match lookupTracePowers u cache with + | some powers => (powers, cache) + | none => + let powers := modularXPowersWith M D u ctx.p ctx.k + (powers, (u, powers) :: cache) + +private def shoupRefineFactorCachedWith {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + (ctx : SmallPrimeTraceContext F) (beta : F) + (cache : ShoupTracePowerCache F) (u : CPolynomial F) : + ShoupTracePowerCache F × Array (CPolynomial F) := + let u := CPolynomial.monicNormalize u + if u == 0 || u == 1 then + (cache, #[]) + else if isRepresentedLinearFactor u then + (cache, #[u]) + else + let (powers, cache) := tracePowersWithCache M D ctx u cache + let tracePoly := traceCoordinatePolynomialFromPowers ctx beta powers + let children := + ctx.baseConstants.foldl + (fun children c ↦ + let child := CPolynomial.monicNormalize + (CPolynomial.gcdMonic u (tracePoly - CPolynomial.C c)) + pushNontrivialChild children child) + #[] + (cache, children) + +private def shoupRefineFactorsCachedWith {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + (ctx : SmallPrimeTraceContext F) (beta : F) + (cache : ShoupTracePowerCache F) (factors : Array (CPolynomial F)) : + ShoupTracePowerCache F × Array (CPolynomial F) := + factors.foldl + (fun state factor ↦ + let (cache, out) := state + let (cache, children) := shoupRefineFactorCachedWith M D ctx beta cache factor + (cache, out ++ children)) + (cache, #[]) + +private def shoupRefineBasisCachedWith {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + (ctx : SmallPrimeTraceContext F) (basis : List F) + (cache : ShoupTracePowerCache F) (factors : Array (CPolynomial F)) : + Array (CPolynomial F) := + (basis.foldl + (fun state beta ↦ + let (cache, factors) := state + shoupRefineFactorsCachedWith M D ctx beta cache factors) + (cache, factors)).2 + +private def shoupSplitCandidatesCachedWith {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + (ctx : SmallPrimeTraceContext F) (p : CPolynomial F) : + Array (CPolynomial F) := + let p := CPolynomial.monicNormalize p + if p == 0 || p == 1 then + #[] + else if isRepresentedLinearFactor p then + #[p] + else + shoupRefineBasisCachedWith M D ctx ctx.basis.toList [] #[p] + +/-- Final linear-only filter for the splitter interface's unconditional soundness field. -/ +def representedLinearFactorsOnly {F : Type*} [Field F] [BEq F] + (factors : Array (CPolynomial F)) : Array (CPolynomial F) := + factors.foldl + (fun out factor ↦ + if isRepresentedLinearFactor factor then out.push factor else out) + #[] + +private theorem representedLinearFactorsOnly_go_sound {F : Type*} + [Field F] [BEq F] [LawfulBEq F] : + ∀ (factors : List (CPolynomial F)) (out : Array (CPolynomial F)), + (∀ {factor : CPolynomial F}, factor ∈ out → IsLinearFactor factor) → + ∀ {factor : CPolynomial F}, + factor ∈ + factors.foldl + (fun out factor ↦ + if isRepresentedLinearFactor factor then out.push factor else out) + out → + IsLinearFactor factor := by + intro factors + induction factors with + | nil => + intro out hout factor h + exact hout h + | cons x xs ih => + intro out hout factor h + simp only [List.foldl_cons] at h + by_cases hlin : isRepresentedLinearFactor x = true + · simp [hlin] at h + exact ih (out.push x) (by + intro factor hmem + simp at hmem + rcases hmem with hmem | hfactor + · exact hout hmem + · subst factor + exact isRepresentedLinearFactor_sound hlin) h + · simp [hlin] at h + exact ih out hout h + +/-- The final Shoup filter only keeps represented linear factors. -/ +theorem representedLinearFactorsOnly_sound {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + {factors : Array (CPolynomial F)} {factor : CPolynomial F} + (h : factor ∈ (representedLinearFactorsOnly factors).toList) : + IsLinearFactor factor := by + unfold representedLinearFactorsOnly at h + rcases factors with ⟨factors⟩ + exact representedLinearFactorsOnly_go_sound factors #[] (by + intro factor hmem + simp at hmem) (by + simpa using h) + +private theorem mem_representedLinearFactorsOnly_foldl_of_mem_out {F : Type*} + [Field F] [BEq F] [LawfulBEq F] : + ∀ (factors : List (CPolynomial F)) (out : Array (CPolynomial F)) + {factor : CPolynomial F}, + factor ∈ out → + factor ∈ + (factors.foldl + (fun out factor ↦ + if isRepresentedLinearFactor factor then out.push factor else out) + out) := by + intro factors + induction factors with + | nil => + intro out factor hmem + exact hmem + | cons x xs ih => + intro out factor hmem + simp only [List.foldl_cons] + by_cases hxlin : isRepresentedLinearFactor x = true + · simp [hxlin] + exact ih (out.push x) (by simp [hmem]) + · simp [hxlin] + exact ih out hmem + +private theorem mem_representedLinearFactorsOnly_foldl_of_mem_input {F : Type*} + [Field F] [BEq F] [LawfulBEq F] : + ∀ (factors : List (CPolynomial F)) (out : Array (CPolynomial F)) + {factor : CPolynomial F}, + factor ∈ factors → + isRepresentedLinearFactor factor = true → + factor ∈ + (factors.foldl + (fun out factor ↦ + if isRepresentedLinearFactor factor then out.push factor else out) + out) := by + intro factors + induction factors with + | nil => + intro out factor hmem _hlin + simp at hmem + | cons x xs ih => + intro out factor hmem hlin + simp only [List.foldl_cons] + simp at hmem + rcases hmem with hhead | htail + · subst factor + simp [hlin] + exact mem_representedLinearFactorsOnly_foldl_of_mem_out xs (out.push x) (by simp) + · by_cases hxlin : isRepresentedLinearFactor x = true + · simp [hxlin] + exact ih (out.push x) htail hlin + · simp [hxlin] + exact ih out htail hlin + +/-- The final linear-factor filter keeps represented factors that were already present. -/ +theorem representedLinearFactorsOnly_mem_of_mem {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + {factors : Array (CPolynomial F)} {factor : CPolynomial F} + (hmem : factor ∈ factors.toList) + (hlin : isRepresentedLinearFactor factor = true) : + factor ∈ (representedLinearFactorsOnly factors).toList := by + unfold representedLinearFactorsOnly + rcases factors with ⟨factors⟩ + simpa using mem_representedLinearFactorsOnly_foldl_of_mem_input factors #[] hmem hlin + +/-- Trace-coordinate refinement candidates before the final linear-only filter. -/ +@[implemented_by shoupSplitCandidatesCachedWith] +def shoupSplitCandidatesWith {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + (ctx : SmallPrimeTraceContext F) (p : CPolynomial F) : + Array (CPolynomial F) := + let p := CPolynomial.monicNormalize p + if p == 0 || p == 1 then + #[] + else if isRepresentedLinearFactor p then + #[p] + else + ctx.basis.foldl + (fun factors beta ↦ shoupRefineFactorsWith M D ctx beta factors) + #[p] + +/-- Shoup trace-coordinate splitting, returning only represented linear factors. -/ +def shoupSplitLinearFactorsWith {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + (ctx : SmallPrimeTraceContext F) (p : CPolynomial F) : + Array (CPolynomial F) := + representedLinearFactorsOnly (shoupSplitCandidatesWith M D ctx p) + +/-- Soundness of the final Shoup linear-factor array. -/ +theorem shoupSplitLinearFactorsWith_sound {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + (ctx : SmallPrimeTraceContext F) {p factor : CPolynomial F} + (h : factor ∈ (shoupSplitLinearFactorsWith M D ctx p).toList) : + IsLinearFactor factor := by + exact representedLinearFactorsOnly_sound h + +end FiniteField + +end Roots + +end CPolynomial + +end CompPoly diff --git a/CompPoly/Univariate/Roots/Shoup/Correctness.lean b/CompPoly/Univariate/Roots/Shoup/Correctness.lean new file mode 100644 index 00000000..df11fc1a --- /dev/null +++ b/CompPoly/Univariate/Roots/Shoup/Correctness.lean @@ -0,0 +1,1410 @@ +/- +Copyright (c) 2026 CompPoly Contributors. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Valerii Huhnin +-/ + +module + +-- These wrappers live in bare `public section`s, so their bodies are opaque +-- downstream while the proofs below step through the `Raw` layer they are defined by. +-- `import all` is the same-package implementation dependency for exactly this; see +-- `docs/wiki/module-system.md`. +import all CompPoly.Univariate.Basic +import all CompPoly.Univariate.Modular +import all CompPoly.Univariate.Raw.Division +import all CompPoly.Univariate.Raw.Modular +import all CompPoly.Univariate.ToPoly.Core +public import CompPoly.Univariate.Roots.Correctness +public import CompPoly.Univariate.Roots.Shoup.Basic +public import CompPoly.Univariate.Roots.Shoup.FrobeniusLinear +public import Mathlib.Algebra.CharP.CharAndCard + +/-! +# Correctness Surface for Shoup-Style Trace Splitting + +This file states the proof obligations for the executable Shoup trace splitter. +The main completeness contract is intentionally limited to valid root products: +nonzero divisors of `X^q - X`, as produced by the finite-field root-product +construction. +-/ + +@[expose] public section + +namespace CompPoly + +namespace CPolynomial + +namespace Roots + +namespace FiniteField + +/-- Evaluation of a Shoup modular power at a root of the modulus. -/ +theorem eval_shoupModularXPowerWith_eq_pow {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + (ctx : SmallPrimeTraceContext F) {modulus : CPolynomial F} {a : F} + (hroot : CPolynomial.eval a modulus = 0) (i : Nat) : + CPolynomial.eval a (xPowModWith M D modulus (ctx.p ^ i)) = a ^ (ctx.p ^ i) := by + exact eval_xPowModWith_eq_pow M D hroot (ctx.p ^ i) + +/-- Trace-coordinate polynomial evaluation at a root of the modulus. -/ +theorem eval_traceCoordinatePolynomialWith {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + (ctx : SmallPrimeTraceContext F) {modulus : CPolynomial F} {a beta : F} + (hroot : CPolynomial.eval a modulus = 0) : + CPolynomial.eval a (traceCoordinatePolynomialWith M D ctx modulus beta) = + ctx.traceValue (beta * a) := by + have evalAdd : + ∀ p q : CPolynomial F, + CPolynomial.eval a (p + q) = CPolynomial.eval a p + CPolynomial.eval a q := by + intro p q + rw [CPolynomial.eval_toPoly, CPolynomial.toPoly_add, Polynomial.eval_add, + ← CPolynomial.eval_toPoly, ← CPolynomial.eval_toPoly] + have evalZero : CPolynomial.eval a (0 : CPolynomial F) = 0 := by + rw [CPolynomial.eval_toPoly, CPolynomial.toPoly_zero, Polynomial.eval_zero] + rw [ctx.traceValue_eq_powerSum] + unfold traceCoordinatePolynomialWith tracePowerSum + have hgo : ∀ (xs : List Nat) (acc : CPolynomial F) (accVal : F), + CPolynomial.eval a acc = accVal → + CPolynomial.eval a + (xs.foldl + (fun acc i ↦ + acc + CPolynomial.C (beta ^ (ctx.p ^ i)) * + xPowModWith M D modulus (ctx.p ^ i)) + acc) = + xs.foldl (fun acc i ↦ acc + (beta * a) ^ (ctx.p ^ i)) accVal := by + intro xs + induction xs with + | nil => + intro acc accVal hacc + simpa using hacc + | cons i is ih => + intro acc accVal hacc + simp only [List.foldl_cons] + apply ih + rw [evalAdd, CPolynomial.eval_mul, CPolynomial.eval_C, + eval_xPowModWith_eq_pow M D hroot, hacc] + rw [mul_pow] + exact hgo (List.range ctx.k) 0 0 evalZero + +/-- A Shoup trace context has the advertised base characteristic. -/ +private theorem smallPrimeTraceContext_charP {F : Type*} + [Field F] [BEq F] [LawfulBEq F] (ctx : SmallPrimeTraceContext F) : + CharP F ctx.p := by + let : Finite F := ctx.finite + let : Fintype F := Fintype.ofFinite F + let : Fact ctx.p.Prime := ⟨ctx.p_prime⟩ + have hcard : Fintype.card F = ctx.p ^ ctx.k := by + rw [← Nat.card_eq_fintype_card, ctx.card_eq, ctx.q_eq] + exact charP_of_card_eq_prime_pow hcard + +/-- The trace power-sum is additive for subtraction. -/ +theorem traceValue_sub {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (ctx : SmallPrimeTraceContext F) (x y : F) : + ctx.traceValue (x - y) = ctx.traceValue x - ctx.traceValue y := by + let : CharP F ctx.p := smallPrimeTraceContext_charP ctx + let : ExpChar F ctx.p := ExpChar.prime ctx.p_prime + rw [ctx.traceValue_eq_powerSum, ctx.traceValue_eq_powerSum, ctx.traceValue_eq_powerSum] + unfold tracePowerSum + have hgo : ∀ (xs : List Nat) (accx accy : F), + xs.foldl (fun acc i ↦ acc + (x - y) ^ (ctx.p ^ i)) (accx - accy) = + xs.foldl (fun acc i ↦ acc + x ^ (ctx.p ^ i)) accx - + xs.foldl (fun acc i ↦ acc + y ^ (ctx.p ^ i)) accy := by + intro xs + induction xs with + | nil => + intro accx accy + simp + | cons i is ih => + intro accx accy + simp only [List.foldl_cons] + have hstep : accx - accy + (x - y) ^ (ctx.p ^ i) = + (accx + x ^ (ctx.p ^ i)) - (accy + y ^ (ctx.p ^ i)) := by + rw [sub_pow_expChar_pow] + ring + rw [hstep] + exact ih (accx + x ^ (ctx.p ^ i)) (accy + y ^ (ctx.p ^ i)) + simpa using hgo (List.range ctx.k) 0 0 + +/-- Trace coordinates convert equal bucket values into zero trace on differences. -/ +theorem traceValue_mul_sub_eq_zero_of_eq {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (ctx : SmallPrimeTraceContext F) {beta a b : F} + (htrace : ctx.traceValue (beta * a) = ctx.traceValue (beta * b)) : + ctx.traceValue (beta * (a - b)) = 0 := by + rw [mul_sub, traceValue_sub ctx, htrace, sub_self] + +/-- Matching every trace coordinate in the separating basis forces equality. -/ +theorem eq_of_traceValue_eq_on_basis {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (ctx : SmallPrimeTraceContext F) {a b : F} + (htrace : ∀ beta, beta ∈ ctx.basis.toList → + ctx.traceValue (beta * a) = ctx.traceValue (beta * b)) : + a = b := by + by_contra hne + rcases ctx.trace_separates hne with ⟨beta, hbeta, hsep⟩ + exact hsep (traceValue_mul_sub_eq_zero_of_eq ctx (htrace beta hbeta)) + +/-- The trace-coordinate gcd bucket preserves roots whose trace coordinate matches the bucket. -/ +theorem shoup_gcdBucket_root {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + (ctx : SmallPrimeTraceContext F) {u : CPolynomial F} {a beta c : F} + (hroot : CPolynomial.eval a u = 0) + (htrace : ctx.traceValue (beta * a) = c) : + CPolynomial.eval a + (CPolynomial.monicNormalize + (CPolynomial.gcdMonic u + (traceCoordinatePolynomialWith M D ctx u beta - CPolynomial.C c))) = 0 := by + let witness := traceCoordinatePolynomialWith M D ctx u beta - CPolynomial.C c + have hwitnessRoot : CPolynomial.eval a witness = 0 := by + dsimp [witness] + rw [CPolynomial.eval_sub, eval_traceCoordinatePolynomialWith M D ctx hroot, + CPolynomial.eval_C, htrace] + ring + exact monicNormalize_root_of_root (gcdMonic_root_of_left_right hroot hwitnessRoot) + +/-- Roots of a trace-coordinate gcd bucket are roots of the parent in that bucket. -/ +theorem shoup_gcdBucket_root_iff {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + (ctx : SmallPrimeTraceContext F) {u : CPolynomial F} {a beta c : F} + (hu : u ≠ 0) : + CPolynomial.eval a + (CPolynomial.monicNormalize + (CPolynomial.gcdMonic u + (traceCoordinatePolynomialWith M D ctx u beta - CPolynomial.C c))) = 0 ↔ + CPolynomial.eval a u = 0 ∧ ctx.traceValue (beta * a) = c := by + let : DecidableEq F := instDecidableEqOfLawfulBEq + let witness := traceCoordinatePolynomialWith M D ctx u beta - CPolynomial.C c + have hgcdNe : CPolynomial.gcdMonic u witness ≠ 0 := gcdMonic_ne_zero_of_left hu + rw [monicNormalize_root_iff hgcdNe, gcdMonic_root_iff_left_right] + constructor + · intro h + rcases h with ⟨hroot, hwitness⟩ + refine ⟨hroot, ?_⟩ + dsimp [witness] at hwitness + rw [CPolynomial.eval_sub, eval_traceCoordinatePolynomialWith M D ctx hroot, + CPolynomial.eval_C] at hwitness + exact sub_eq_zero.mp hwitness + · intro h + rcases h with ⟨hroot, htrace⟩ + refine ⟨hroot, ?_⟩ + dsimp [witness] + rw [CPolynomial.eval_sub, eval_traceCoordinatePolynomialWith M D ctx hroot, + CPolynomial.eval_C, htrace] + ring + +private theorem monicNormalize_toPoly_dvd_self {F : Type*} + [Field F] [BEq F] [LawfulBEq F] [DecidableEq F] + (p : CPolynomial F) : + (CPolynomial.monicNormalize p).toPoly ∣ p.toPoly := by + rw [CPolynomial.monicNormalize_toPoly_eq_normalize] + exact (normalize_associated p.toPoly).dvd + +private theorem gcdMonic_toPoly_dvd_left {F : Type*} + [Field F] [BEq F] [LawfulBEq F] [DecidableEq F] + (p q : CPolynomial F) : + (CPolynomial.gcdMonic p q).toPoly ∣ p.toPoly := by + rw [CPolynomial.gcdMonic_toPoly_eq_normalize_gcd] + exact (normalize_associated (EuclideanDomain.gcd p.toPoly q.toPoly)).dvd.trans + (EuclideanDomain.gcd_dvd_left p.toPoly q.toPoly) + +private theorem pushNontrivialChild_mem_of_ne_zero_ne_one {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + {children : Array (CPolynomial F)} {child : CPolynomial F} + (h0 : child ≠ 0) (h1 : child ≠ 1) : + child ∈ (pushNontrivialChild children child).toList := by + unfold pushNontrivialChild + have hskip : ¬ (child == 0 || child == 1) = true := by + intro h + have hcases : child = 0 ∨ child = 1 := by + simpa using h + rcases hcases with hzero | hone + · exact h0 hzero + · exact h1 hone + rw [if_neg hskip] + simp + +private theorem pushNontrivialChild_mem_of_mem {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + {children : Array (CPolynomial F)} {child factor : CPolynomial F} + (hmem : factor ∈ children.toList) : + factor ∈ (pushNontrivialChild children child).toList := by + unfold pushNontrivialChild + by_cases hskip : (child == 0 || child == 1) = true + · rw [if_pos hskip] + exact hmem + · rw [if_neg hskip] + simp [hmem] + +private theorem mem_pushNontrivialChild {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + {children : Array (CPolynomial F)} {child factor : CPolynomial F} + (hmem : factor ∈ (pushNontrivialChild children child).toList) : + factor ∈ children.toList ∨ factor = child := by + unfold pushNontrivialChild at hmem + by_cases hskip : (child == 0 || child == 1) = true + · rw [if_pos hskip] at hmem + exact Or.inl hmem + · rw [if_neg hskip] at hmem + simp at hmem + rcases hmem with hmem | hfactor + · exact Or.inl (by simpa using hmem) + · exact Or.inr hfactor + +private theorem shoupRefineBaseConstants_root {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + (ctx : SmallPrimeTraceContext F) (beta : F) {u : CPolynomial F} {a : F} + (hu : u ≠ 0) (hroot : CPolynomial.eval a u = 0) : + ∀ (constants : List F) (acc : Array (CPolynomial F)), + (∃ factor, factor ∈ acc.toList ∧ factor ≠ 0 ∧ CPolynomial.eval a factor = 0) ∨ + ctx.traceValue (beta * a) ∈ constants → + ∃ factor, + factor ∈ + (constants.foldl + (fun children c ↦ + let child := CPolynomial.monicNormalize + (CPolynomial.gcdMonic u + (traceCoordinatePolynomialWith M D ctx u beta - CPolynomial.C c)) + pushNontrivialChild children child) + acc).toList ∧ + factor ≠ 0 ∧ + CPolynomial.eval a factor = 0 := by + intro constants + induction constants with + | nil => + intro acc h + rcases h with hacc | hmem + · exact hacc + · simp at hmem + | cons c cs ih => + intro acc h + simp only [List.foldl_cons] + apply ih + rcases h with hacc | hmem + · left + rcases hacc with ⟨factor, hfactor, hfactorNe, hrootFactor⟩ + refine ⟨factor, pushNontrivialChild_mem_of_mem hfactor, hfactorNe, hrootFactor⟩ + · simp at hmem + rcases hmem with htarget | htail + · left + subst c + let child := CPolynomial.monicNormalize + (CPolynomial.gcdMonic u + (traceCoordinatePolynomialWith M D ctx u beta - + CPolynomial.C (ctx.traceValue (beta * a)))) + have hchildRoot : CPolynomial.eval a child = 0 := by + dsimp [child] + exact shoup_gcdBucket_root M D ctx hroot rfl + have hgcdNe : CPolynomial.gcdMonic u + (traceCoordinatePolynomialWith M D ctx u beta - + CPolynomial.C (ctx.traceValue (beta * a))) ≠ 0 := + gcdMonic_ne_zero_of_left hu + have hchildNe : child ≠ 0 := monicNormalize_ne_zero_of_ne_zero hgcdNe + have hchildNotOne : child ≠ 1 := by + intro hone + rw [hone] at hchildRoot + rw [eval_one a] at hchildRoot + exact (one_ne_zero hchildRoot).elim + refine ⟨child, ?_, hchildNe, hchildRoot⟩ + exact pushNontrivialChild_mem_of_ne_zero_ne_one hchildNe hchildNotOne + · right + exact htail + +private theorem shoupRefineBaseConstants_dvd {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + (ctx : SmallPrimeTraceContext F) (beta : F) {u : CPolynomial F} : + ∀ (constants : List F) (acc : Array (CPolynomial F)), + (∀ factor, factor ∈ acc.toList → factor.toPoly ∣ u.toPoly) → + ∀ factor, + factor ∈ + (constants.foldl + (fun children c ↦ + let child := CPolynomial.monicNormalize + (CPolynomial.gcdMonic u + (traceCoordinatePolynomialWith M D ctx u beta - CPolynomial.C c)) + pushNontrivialChild children child) + acc).toList → + factor.toPoly ∣ u.toPoly := by + let : DecidableEq F := instDecidableEqOfLawfulBEq + intro constants + induction constants with + | nil => + intro acc hacc factor hmem + exact hacc factor hmem + | cons c cs ih => + intro acc hacc factor hmem + simp only [List.foldl_cons] at hmem + apply ih (pushNontrivialChild acc + (CPolynomial.monicNormalize + (CPolynomial.gcdMonic u + (traceCoordinatePolynomialWith M D ctx u beta - CPolynomial.C c)))) + · intro factor hfactor + rcases mem_pushNontrivialChild hfactor with hold | hnew + · exact hacc factor hold + · subst factor + exact (monicNormalize_toPoly_dvd_self + (CPolynomial.gcdMonic u + (traceCoordinatePolynomialWith M D ctx u beta - CPolynomial.C c))).trans + (gcdMonic_toPoly_dvd_left u + (traceCoordinatePolynomialWith M D ctx u beta - CPolynomial.C c)) + · exact hmem + +private theorem shoupRefineFactorWith_root {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + (ctx : SmallPrimeTraceContext F) (beta : F) {u : CPolynomial F} {a : F} + (hu : u ≠ 0) (hroot : CPolynomial.eval a u = 0) : + ∃ factor, + factor ∈ (shoupRefineFactorWith M D ctx beta u).toList ∧ + factor ≠ 0 ∧ + CPolynomial.eval a factor = 0 := by + unfold shoupRefineFactorWith + let u' := CPolynomial.monicNormalize u + have hu' : u' ≠ 0 := monicNormalize_ne_zero_of_ne_zero hu + have hroot' : CPolynomial.eval a u' = 0 := (monicNormalize_root_iff hu).2 hroot + by_cases hzero : (u' == 0 || u' == 1) = true + · have hcases : u' = 0 ∨ u' = 1 := by + simpa [u'] using hzero + rcases hcases with h0 | h1 + · exact (hu' h0).elim + · rw [h1] at hroot' + rw [eval_one a] at hroot' + exact (one_ne_zero hroot').elim + · rw [if_neg hzero] + by_cases hlin : isRepresentedLinearFactor u' = true + · rw [if_pos hlin] + refine ⟨u', ?_, hu', hroot'⟩ + simp [u'] + · rw [if_neg hlin] + rw [← Array.foldl_toList] + exact shoupRefineBaseConstants_root M D ctx beta hu' hroot' + ctx.baseConstants.toList #[] (Or.inr (ctx.traceValue_mem_base (beta * a))) + +private theorem shoupRefineFactorWith_dvd {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + (ctx : SmallPrimeTraceContext F) (beta : F) {u factor : CPolynomial F} + (hmem : factor ∈ (shoupRefineFactorWith M D ctx beta u).toList) : + factor.toPoly ∣ u.toPoly := by + let : DecidableEq F := instDecidableEqOfLawfulBEq + unfold shoupRefineFactorWith at hmem + let u' := CPolynomial.monicNormalize u + by_cases hzero : (u' == 0 || u' == 1) = true + · rw [if_pos hzero] at hmem + simp at hmem + · rw [if_neg hzero] at hmem + by_cases hlin : isRepresentedLinearFactor u' = true + · rw [if_pos hlin] at hmem + simp at hmem + subst factor + exact monicNormalize_toPoly_dvd_self u + · rw [if_neg hlin] at hmem + rw [← Array.foldl_toList] at hmem + have hdivU' : factor.toPoly ∣ u'.toPoly := + shoupRefineBaseConstants_dvd M D ctx beta ctx.baseConstants.toList #[] + (by + intro factor hfactor + simp at hfactor) + factor hmem + exact hdivU'.trans (monicNormalize_toPoly_dvd_self u) + +private theorem shoupRefineFactorsWith_root {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + (ctx : SmallPrimeTraceContext F) (beta : F) + {factors : Array (CPolynomial F)} {a : F} + (hroot : + ∃ factor, + factor ∈ factors.toList ∧ factor ≠ 0 ∧ CPolynomial.eval a factor = 0) : + ∃ factor, + factor ∈ (shoupRefineFactorsWith M D ctx beta factors).toList ∧ + factor ≠ 0 ∧ + CPolynomial.eval a factor = 0 := by + unfold shoupRefineFactorsWith + rcases factors with ⟨factorList⟩ + rw [← Array.foldl_toList] + have hgo : ∀ (xs : List (CPolynomial F)) (acc : Array (CPolynomial F)), + (∃ factor, + factor ∈ acc.toList ∧ factor ≠ 0 ∧ CPolynomial.eval a factor = 0) ∨ + (∃ factor, + factor ∈ xs ∧ factor ≠ 0 ∧ CPolynomial.eval a factor = 0) → + ∃ factor, + factor ∈ + (xs.foldl + (fun out factor ↦ out ++ shoupRefineFactorWith M D ctx beta factor) + acc).toList ∧ + factor ≠ 0 ∧ + CPolynomial.eval a factor = 0 := by + intro xs + induction xs with + | nil => + intro acc h + rcases h with hacc | htail + · exact hacc + · rcases htail with ⟨factor, hmem, _⟩ + simp at hmem + | cons x xs ih => + intro acc h + simp only [List.foldl_cons] + apply ih + rcases h with hacc | htail + · left + rcases hacc with ⟨factor, hmem, hne, hrootFactor⟩ + refine ⟨factor, ?_, hne, hrootFactor⟩ + simpa using Array.mem_append_left + (shoupRefineFactorWith M D ctx beta x) (by simpa using hmem) + · rcases htail with ⟨factor, hmem, hne, hrootFactor⟩ + simp at hmem + rcases hmem with hhead | htail + · subst factor + left + rcases shoupRefineFactorWith_root M D ctx beta hne hrootFactor with + ⟨child, hchildMem, hchildNe, hchildRoot⟩ + refine ⟨child, ?_, hchildNe, hchildRoot⟩ + simpa using Array.mem_append_right acc (by simpa using hchildMem) + · right + exact ⟨factor, htail, hne, hrootFactor⟩ + exact hgo factorList #[] (Or.inr (by simpa using hroot)) + +private theorem shoupRefineFactorsWith_dvd {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + (ctx : SmallPrimeTraceContext F) (beta : F) + {p factor : CPolynomial F} {factors : Array (CPolynomial F)} + (hparents : ∀ parent, parent ∈ factors.toList → parent.toPoly ∣ p.toPoly) + (hmem : factor ∈ (shoupRefineFactorsWith M D ctx beta factors).toList) : + factor.toPoly ∣ p.toPoly := by + unfold shoupRefineFactorsWith at hmem + rcases factors with ⟨factorList⟩ + rw [← Array.foldl_toList] at hmem + have hgo : ∀ (xs : List (CPolynomial F)) (out : Array (CPolynomial F)), + (∀ child, child ∈ out.toList → child.toPoly ∣ p.toPoly) → + (∀ parent, parent ∈ xs → parent.toPoly ∣ p.toPoly) → + ∀ child, + child ∈ + (xs.foldl + (fun out factor ↦ out ++ shoupRefineFactorWith M D ctx beta factor) + out).toList → + child.toPoly ∣ p.toPoly := by + intro xs + induction xs with + | nil => + intro out hout _ child hchild + exact hout child hchild + | cons x xs ih => + intro out hout hxs child hchild + simp only [List.foldl_cons] at hchild + apply ih (out ++ shoupRefineFactorWith M D ctx beta x) + · intro child hmemChild + simp at hmemChild + rcases hmemChild with hleft | hright + · exact hout child (by simpa using hleft) + · exact (shoupRefineFactorWith_dvd M D ctx beta (by simpa using hright)).trans + (hxs x (by simp)) + · intro parent hparent + exact hxs parent (by simp [hparent]) + · exact hchild + exact hgo factorList #[] + (by + intro child hchild + simp at hchild) + (by + intro parent hparent + exact hparents parent (by simpa using hparent)) + factor hmem + +private theorem shoupRefineBasisWith_root {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + (ctx : SmallPrimeTraceContext F) : + ∀ (basis : List F) (factors : Array (CPolynomial F)) {a : F}, + (∃ factor, + factor ∈ factors.toList ∧ factor ≠ 0 ∧ CPolynomial.eval a factor = 0) → + ∃ factor, + factor ∈ + (basis.foldl + (fun factors beta ↦ shoupRefineFactorsWith M D ctx beta factors) + factors).toList ∧ + factor ≠ 0 ∧ + CPolynomial.eval a factor = 0 := by + intro basis + induction basis with + | nil => + intro factors a hroot + exact hroot + | cons beta rest ih => + intro factors a hroot + simp only [List.foldl_cons] + exact ih (shoupRefineFactorsWith M D ctx beta factors) + (shoupRefineFactorsWith_root M D ctx beta hroot) + +private theorem shoupRefineBasisWith_dvd {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + (ctx : SmallPrimeTraceContext F) {p : CPolynomial F} : + ∀ (basis : List F) (factors : Array (CPolynomial F)) {factor : CPolynomial F}, + (∀ parent, parent ∈ factors.toList → parent.toPoly ∣ p.toPoly) → + factor ∈ + (basis.foldl + (fun factors beta ↦ shoupRefineFactorsWith M D ctx beta factors) + factors).toList → + factor.toPoly ∣ p.toPoly := by + intro basis + induction basis with + | nil => + intro factors factor hparents hmem + exact hparents factor hmem + | cons beta rest ih => + intro factors factor hparents hmem + simp only [List.foldl_cons] at hmem + apply ih (shoupRefineFactorsWith M D ctx beta factors) + · intro parent hparent + exact shoupRefineFactorsWith_dvd M D ctx beta hparents hparent + · exact hmem + +private theorem representedLinearFactor_root_unique {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + {p : CPolynomial F} {a b : F} + (hlin : isRepresentedLinearFactor p = true) + (ha : CPolynomial.eval a p = 0) (hb : CPolynomial.eval b p = 0) : + b = a := by + have ca := representedLinearFactor_candidate_of_root hlin ha + have cb := representedLinearFactor_candidate_of_root hlin hb + have haeq : p.coeff 1 * a = -p.coeff 0 := by + have h := ca.2 + rw [add_eq_zero_iff_eq_neg] at h + rw [h] + simp + have hbeq : p.coeff 1 * b = -p.coeff 0 := by + have h := cb.2 + rw [add_eq_zero_iff_eq_neg] at h + rw [h] + simp + have hsub : p.coeff 1 * (b - a) = 0 := by + rw [mul_sub, hbeq, haeq, sub_self] + have hbmina : b - a = 0 := (mul_eq_zero.mp hsub).resolve_left ca.1.2 + exact sub_eq_zero.mp hbmina + +private theorem monicNormalize_zero {F : Type*} + [Field F] [BEq F] [LawfulBEq F] : + CPolynomial.monicNormalize (0 : CPolynomial F) = 0 := by + unfold CPolynomial.monicNormalize CPolynomial.Raw.monicNormalize CPolynomial.ofArray + apply Subtype.ext + change + (if ((0 : CPolynomial F).val : CPolynomial.Raw F).trim == 0 then + (0 : CPolynomial.Raw F) + else + ((0 : CPolynomial F).val : CPolynomial.Raw F).trim.leadingCoeff⁻¹ • + ((0 : CPolynomial F).val : CPolynomial.Raw F).trim).trim = + (0 : CPolynomial.Raw F) + rw [if_pos] + · exact CPolynomial.Raw.zero_canonical + · change (((0 : CPolynomial.Raw F).trim) == 0) = true + rw [CPolynomial.Raw.zero_canonical] + exact beq_self_eq_true (0 : CPolynomial.Raw F) + +private def rootsAgreeOn {F : Type*} [Field F] [BEq F] [LawfulBEq F] + (ctx : SmallPrimeTraceContext F) (coords : List F) (factor : CPolynomial F) : + Prop := + ∀ a b : F, CPolynomial.eval a factor = 0 → CPolynomial.eval b factor = 0 → + ∀ beta, beta ∈ coords → + ctx.traceValue (beta * a) = ctx.traceValue (beta * b) + +private theorem rootsAgreeOn_of_root_unique {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (ctx : SmallPrimeTraceContext F) (coords : List F) {factor : CPolynomial F} + (hunique : ∀ {a b : F}, CPolynomial.eval a factor = 0 → + CPolynomial.eval b factor = 0 → b = a) : + rootsAgreeOn ctx coords factor := by + intro a b hrootA hrootB beta _hbeta + have hba : b = a := hunique hrootA hrootB + subst b + rfl + +private theorem rootsAgreeOn_monicNormalize {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (ctx : SmallPrimeTraceContext F) (coords : List F) {u : CPolynomial F} + (hu : u ≠ 0) (huAgree : rootsAgreeOn ctx coords u) : + rootsAgreeOn ctx coords (CPolynomial.monicNormalize u) := by + intro a b hrootA hrootB beta hbeta + exact huAgree a b ((monicNormalize_root_iff hu).1 hrootA) + ((monicNormalize_root_iff hu).1 hrootB) beta hbeta + +private theorem shoupRefineBaseConstants_rootsAgreeOn {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + (ctx : SmallPrimeTraceContext F) (beta : F) {u : CPolynomial F} + (hu : u ≠ 0) {coords : List F} + (huAgree : rootsAgreeOn ctx coords u) : + ∀ (constants : List F) (acc : Array (CPolynomial F)), + (∀ factor, factor ∈ acc.toList → + rootsAgreeOn ctx (coords ++ [beta]) factor) → + ∀ factor, + factor ∈ + (constants.foldl + (fun children c ↦ + let child := CPolynomial.monicNormalize + (CPolynomial.gcdMonic u + (traceCoordinatePolynomialWith M D ctx u beta - CPolynomial.C c)) + pushNontrivialChild children child) + acc).toList → + rootsAgreeOn ctx (coords ++ [beta]) factor := by + intro constants + induction constants with + | nil => + intro acc hacc factor hmem + exact hacc factor hmem + | cons c cs ih => + intro acc hacc factor hmem + simp only [List.foldl_cons] at hmem + apply ih (pushNontrivialChild acc + (CPolynomial.monicNormalize + (CPolynomial.gcdMonic u + (traceCoordinatePolynomialWith M D ctx u beta - CPolynomial.C c)))) + · intro factor hfactor + rcases mem_pushNontrivialChild hfactor with hold | hnew + · exact hacc factor hold + · subst factor + intro a b hrootA hrootB gamma hgamma + rw [List.mem_append] at hgamma + rcases hgamma with hgamma | hgamma + · exact huAgree + a b + ((shoup_gcdBucket_root_iff M D ctx hu).1 hrootA).1 + ((shoup_gcdBucket_root_iff M D ctx hu).1 hrootB).1 + gamma hgamma + · simp at hgamma + subst gamma + exact (((shoup_gcdBucket_root_iff M D ctx hu).1 hrootA).2).trans + (((shoup_gcdBucket_root_iff M D ctx hu).1 hrootB).2).symm + · exact hmem + +private theorem shoupRefineFactorWith_rootsAgreeOn {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + (ctx : SmallPrimeTraceContext F) (beta : F) {u factor : CPolynomial F} + {coords : List F} + (huAgree : rootsAgreeOn ctx coords u) + (hmem : factor ∈ (shoupRefineFactorWith M D ctx beta u).toList) : + rootsAgreeOn ctx (coords ++ [beta]) factor := by + unfold shoupRefineFactorWith at hmem + let u' := CPolynomial.monicNormalize u + by_cases hzero : (u' == 0 || u' == 1) = true + · rw [if_pos hzero] at hmem + simp at hmem + · rw [if_neg hzero] at hmem + by_cases hlin : isRepresentedLinearFactor u' = true + · rw [if_pos hlin] at hmem + simp at hmem + subst factor + exact rootsAgreeOn_of_root_unique ctx (coords ++ [beta]) + (fun hrootA hrootB ↦ + representedLinearFactor_root_unique hlin hrootA hrootB) + · rw [if_neg hlin] at hmem + rw [← Array.foldl_toList] at hmem + have hu' : u' ≠ 0 := by + intro hz + have hskip : (u' == 0 || u' == 1) = true := by + simp [hz] + exact hzero hskip + have hu : u ≠ 0 := by + intro hz + subst u + exact hu' (by simpa [u'] using (monicNormalize_zero : CPolynomial.monicNormalize + (0 : CPolynomial F) = 0)) + have huAgree' : rootsAgreeOn ctx coords u' := + rootsAgreeOn_monicNormalize ctx coords hu huAgree + exact shoupRefineBaseConstants_rootsAgreeOn M D ctx beta hu' + huAgree' ctx.baseConstants.toList #[] (by + intro factor hfactor + simp at hfactor) factor hmem + +private theorem shoupRefineFactorsWith_rootsAgreeOn {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + (ctx : SmallPrimeTraceContext F) (beta : F) + {coords : List F} {factors : Array (CPolynomial F)} {factor : CPolynomial F} + (hparents : ∀ parent, parent ∈ factors.toList → + rootsAgreeOn ctx coords parent) + (hmem : factor ∈ (shoupRefineFactorsWith M D ctx beta factors).toList) : + rootsAgreeOn ctx (coords ++ [beta]) factor := by + unfold shoupRefineFactorsWith at hmem + rcases factors with ⟨factorList⟩ + rw [← Array.foldl_toList] at hmem + have hgo : ∀ (xs : List (CPolynomial F)) (out : Array (CPolynomial F)), + (∀ child, child ∈ out.toList → + rootsAgreeOn ctx (coords ++ [beta]) child) → + (∀ parent, parent ∈ xs → rootsAgreeOn ctx coords parent) → + ∀ child, + child ∈ + (xs.foldl + (fun out factor ↦ out ++ shoupRefineFactorWith M D ctx beta factor) + out).toList → + rootsAgreeOn ctx (coords ++ [beta]) child := by + intro xs + induction xs with + | nil => + intro out hout _ child hchild + exact hout child hchild + | cons x xs ih => + intro out hout hxs child hchild + simp only [List.foldl_cons] at hchild + apply ih (out ++ shoupRefineFactorWith M D ctx beta x) + · intro child hmemChild + simp at hmemChild + rcases hmemChild with hleft | hright + · exact hout child (by simpa using hleft) + · exact shoupRefineFactorWith_rootsAgreeOn M D ctx beta + (hxs x (by simp)) (by simpa using hright) + · intro parent hparent + exact hxs parent (by simp [hparent]) + · exact hchild + exact hgo factorList #[] + (by + intro child hchild + simp at hchild) + (by + intro parent hparent + exact hparents parent (by simpa using hparent)) + factor hmem + +private theorem shoupRefineBasisWith_rootsAgreeOn {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + (ctx : SmallPrimeTraceContext F) : + ∀ (basis coords : List F) (factors : Array (CPolynomial F)) {factor : CPolynomial F}, + (∀ parent, parent ∈ factors.toList → rootsAgreeOn ctx coords parent) → + factor ∈ + (basis.foldl + (fun factors beta ↦ shoupRefineFactorsWith M D ctx beta factors) + factors).toList → + rootsAgreeOn ctx (coords ++ basis) factor := by + intro basis + induction basis with + | nil => + intro coords factors factor hparents hmem + simpa using hparents factor hmem + | cons beta rest ih => + intro coords factors factor hparents hmem + simp only [List.foldl_cons] at hmem + have hstep : + ∀ parent, parent ∈ (shoupRefineFactorsWith M D ctx beta factors).toList → + rootsAgreeOn ctx (coords ++ [beta]) parent := by + intro parent hparent + exact shoupRefineFactorsWith_rootsAgreeOn M D ctx beta hparents hparent + have hfinal := ih (coords ++ [beta]) + (shoupRefineFactorsWith M D ctx beta factors) hstep hmem + simpa [List.append_assoc] using hfinal + +theorem shoupSplitCandidatesWith_root {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + (ctx : SmallPrimeTraceContext F) {p : CPolynomial F} {a : F} + (hp : p ≠ 0) (hroot : CPolynomial.eval a p = 0) : + ∃ factor, + factor ∈ (shoupSplitCandidatesWith M D ctx p).toList ∧ + factor ≠ 0 ∧ + CPolynomial.eval a factor = 0 := by + unfold shoupSplitCandidatesWith + let p' := CPolynomial.monicNormalize p + have hp' : p' ≠ 0 := monicNormalize_ne_zero_of_ne_zero hp + have hroot' : CPolynomial.eval a p' = 0 := (monicNormalize_root_iff hp).2 hroot + by_cases hzero : (p' == 0 || p' == 1) = true + · have hcases : p' = 0 ∨ p' = 1 := by + simpa [p'] using hzero + rcases hcases with h0 | h1 + · exact (hp' h0).elim + · rw [h1] at hroot' + rw [eval_one a] at hroot' + exact (one_ne_zero hroot').elim + · rw [if_neg hzero] + by_cases hlin : isRepresentedLinearFactor p' = true + · rw [if_pos hlin] + refine ⟨p', ?_, hp', hroot'⟩ + simp [p'] + · rw [if_neg hlin] + rw [← Array.foldl_toList] + exact shoupRefineBasisWith_root M D ctx ctx.basis.toList #[p'] + ⟨p', by simp [p'], hp', hroot'⟩ + +theorem shoupSplitCandidatesWith_dvd_input {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + (ctx : SmallPrimeTraceContext F) {p factor : CPolynomial F} + (hmem : factor ∈ (shoupSplitCandidatesWith M D ctx p).toList) : + factor.toPoly ∣ p.toPoly := by + let : DecidableEq F := instDecidableEqOfLawfulBEq + unfold shoupSplitCandidatesWith at hmem + let p' := CPolynomial.monicNormalize p + by_cases hzero : (p' == 0 || p' == 1) = true + · rw [if_pos hzero] at hmem + simp at hmem + · rw [if_neg hzero] at hmem + by_cases hlin : isRepresentedLinearFactor p' = true + · rw [if_pos hlin] at hmem + simp at hmem + subst factor + exact monicNormalize_toPoly_dvd_self p + · rw [if_neg hlin] at hmem + rw [← Array.foldl_toList] at hmem + exact shoupRefineBasisWith_dvd M D ctx ctx.basis.toList #[p'] + (by + intro parent hparent + simp at hparent + subst parent + exact monicNormalize_toPoly_dvd_self p) + hmem + +/- The remaining algorithmic invariant: two field roots of the same final Shoup +candidate have equal trace coordinates for every basis element, hence are equal. -/ +private theorem shoupSplitCandidatesWith_root_unique {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + (ctx : SmallPrimeTraceContext F) {p factor : CPolynomial F} {a b : F} + (hmem : factor ∈ (shoupSplitCandidatesWith M D ctx p).toList) + (hrootA : CPolynomial.eval a factor = 0) + (hrootB : CPolynomial.eval b factor = 0) : + b = a := by + unfold shoupSplitCandidatesWith at hmem + let p' := CPolynomial.monicNormalize p + by_cases hzero : (p' == 0 || p' == 1) = true + · rw [if_pos hzero] at hmem + simp at hmem + · rw [if_neg hzero] at hmem + by_cases hlin : isRepresentedLinearFactor p' = true + · rw [if_pos hlin] at hmem + simp at hmem + subst factor + exact representedLinearFactor_root_unique hlin hrootA hrootB + · rw [if_neg hlin] at hmem + rw [← Array.foldl_toList] at hmem + have hagree : rootsAgreeOn ctx ctx.basis.toList factor := by + have hbasis := shoupRefineBasisWith_rootsAgreeOn M D ctx + ctx.basis.toList [] #[p'] (by + intro parent hparent + simp at hparent + subst parent + intro x y hrootX hrootY beta hbeta + simp at hbeta) hmem + simpa using hbasis + exact (eq_of_traceValue_eq_on_basis ctx + (fun beta hbeta ↦ hagree a b hrootA hrootB beta hbeta)).symm + +/-- +Remaining Shoup linearization obligation. + +The checked lemmas above show that candidates divide the valid root product and +that valid divisors of `X^q - X` with a unique field root are represented +linear. The remaining algorithmic proof is `shoupSplitCandidatesWith_root_unique`. +-/ +private theorem shoupSplitCandidatesWith_valid_factor_represented {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + (ctx : SmallPrimeTraceContext F) {p factor : CPolynomial F} {a : F} + (hvalid : shoupSplitterInput ctx p) + (hmem : factor ∈ (shoupSplitCandidatesWith M D ctx p).toList) + (hfactorNe : factor ≠ 0) + (hfactorRoot : CPolynomial.eval a factor = 0) : + isRepresentedLinearFactor factor = true := by + exact isRepresentedLinearFactor_of_dvd_frobenius_unique_root ctx hfactorNe + ((shoupSplitCandidatesWith_dvd_input M D ctx hmem).trans hvalid.2) + hfactorRoot (fun b hrootB ↦ + shoupSplitCandidatesWith_root_unique M D ctx hmem hfactorRoot hrootB) + +/-- The Shoup splitter is complete for valid root-product inputs. -/ +theorem shoupSplitLinearFactorsWith_complete {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + (ctx : SmallPrimeTraceContext F) {p : CPolynomial F} {a : F} + (hvalid : shoupSplitterInput ctx p) + (hp : p ≠ 0) (hroot : CPolynomial.eval a p = 0) : + ∃ factor, + factor ∈ (shoupSplitLinearFactorsWith M D ctx p).toList ∧ + IsLinearRootFactorCandidate factor a := by + rcases shoupSplitCandidatesWith_root M D ctx hp hroot with + ⟨factor, hmem, hfactorNe, hfactorRoot⟩ + have hlin : isRepresentedLinearFactor factor = true := + shoupSplitCandidatesWith_valid_factor_represented M D ctx hvalid hmem + hfactorNe hfactorRoot + refine ⟨factor, ?_, representedLinearFactor_candidate_of_root hlin hfactorRoot⟩ + unfold shoupSplitLinearFactorsWith + exact representedLinearFactorsOnly_mem_of_mem hmem hlin + +/-- Adapt the Shoup trace splitter to the generic linear-factor-product interface. -/ +def shoupLinearFactorProductSplitterWith {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + (ctx : SmallPrimeTraceContext F) : + LinearFactorProductSplitter F where + splitLinearFactors := fun _q p ↦ shoupSplitLinearFactorsWith M D ctx p + validInput := fun _q p ↦ shoupSplitterInput ctx p + sound := by + intro _q p factor h + exact shoupSplitLinearFactorsWith_sound M D ctx h + complete := by + intro _q p a hvalid hp hroot + exact shoupSplitLinearFactorsWith_complete M D ctx hvalid hp hroot + +/-- Shoup trace splitter using the default raw multiplication and remainder backends. -/ +def shoupLinearFactorProductSplitter {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (ctx : SmallPrimeTraceContext F) : + LinearFactorProductSplitter F := + shoupLinearFactorProductSplitterWith CPolynomial.Raw.MulContext.naive + CPolynomial.Raw.ModContext.naive ctx + +/-- The Shoup splitter only emits represented linear factors. -/ +theorem shoupLinearFactorProductSplitterWith_sound {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + (ctx : SmallPrimeTraceContext F) {q : Nat} {p factor : CPolynomial F} + (h : factor ∈ + ((shoupLinearFactorProductSplitterWith M D ctx).splitLinearFactors q p).toList) : + IsLinearFactor factor := by + exact (shoupLinearFactorProductSplitterWith M D ctx).sound q p factor h + +/-- The Shoup splitter is complete for valid root-product inputs. -/ +theorem shoupLinearFactorProductSplitterWith_complete {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + (ctx : SmallPrimeTraceContext F) {q : Nat} {p : CPolynomial F} {a : F} + (hvalid : (shoupLinearFactorProductSplitterWith M D ctx).validInput q p) + (hp : p ≠ 0) (hroot : CPolynomial.eval a p = 0) : + ∃ factor, + factor ∈ + ((shoupLinearFactorProductSplitterWith M D ctx).splitLinearFactors q p).toList ∧ + IsLinearRootFactorCandidate factor a := by + exact (shoupLinearFactorProductSplitterWith M D ctx).complete q p a hvalid hp hroot + +private theorem raw_monicNormalize_trim {F : Type*} [Field F] [BEq F] [LawfulBEq F] + (p : CPolynomial.Raw F) : + (CPolynomial.Raw.monicNormalize p).trim = CPolynomial.Raw.monicNormalize p := by + unfold CPolynomial.Raw.monicNormalize + let q := p.trim + by_cases hzero : q == (0 : CPolynomial.Raw F) + · have hzeroEq : q = 0 := by + simpa using hzero + have hpzeroEq : p.trim = (#[] : CPolynomial.Raw F) := by + simpa [q] using hzeroEq + simp [hpzeroEq, CPolynomial.Raw.Trim.canonical_empty] + · have hzeroNe : q ≠ (0 : CPolynomial.Raw F) := by + intro hq + exact hzero (by simp [hq]) + have hpzeroNe : ¬p.trim = (#[] : CPolynomial.Raw F) := by + intro hp + exact hzeroNe (by simpa [q] using hp) + simp [hpzeroNe] + change (CPolynomial.Raw.mk + (Array.map (fun r ↦ (p.trim).leadingCoeff⁻¹ * r) p.trim)).trim = + CPolynomial.Raw.mk (Array.map (fun r ↦ (p.trim).leadingCoeff⁻¹ * r) p.trim) + apply CPolynomial.Raw.Trim.non_zero_map (fun r ↦ q.leadingCoeff⁻¹ * r) + · intro r hr + apply mul_eq_zero.mp at hr + rcases hr with hinv | hr + · have hlead0 : q.leadingCoeff = 0 := by + exact inv_eq_zero.mp hinv + have hcanon : q.trim = q := by + simpa [q] using CPolynomial.Raw.Trim.trim_twice p + have hcrit := (CPolynomial.Raw.Trim.trim_eq_iff_size_eq_zero_or_getLastD_ne_zero + (p := q)).mp hcanon + rcases hcrit with hsize | hlast + · have hqempty : q = (#[] : CPolynomial.Raw F) := by + apply Array.eq_empty_of_size_eq_zero + exact hsize + exact False.elim (hzero (by simp [hqempty])) + · unfold CPolynomial.Raw.leadingCoeff at hlead0 + rw [hcanon] at hlead0 + exact (hlast hlead0).elim + · exact hr + · simpa [q] using CPolynomial.Raw.Trim.trim_twice p + +private theorem raw_monicNormalize_trim_arg {F : Type*} [Field F] [BEq F] [LawfulBEq F] + (p : CPolynomial.Raw F) : + CPolynomial.Raw.monicNormalize p.trim = CPolynomial.Raw.monicNormalize p := by + unfold CPolynomial.Raw.monicNormalize + rw [CPolynomial.Raw.Trim.trim_twice] + +private theorem raw_monicNormalize_toPoly_eq_normalize {F : Type*} + [Field F] [BEq F] [LawfulBEq F] [DecidableEq F] + (p : CPolynomial.Raw F) : + (CPolynomial.ofArray (CPolynomial.Raw.monicNormalize p)).toPoly = + normalize (CPolynomial.ofArray p).toPoly := by + have h := CPolynomial.monicNormalize_toPoly_eq_normalize (CPolynomial.ofArray p) + unfold CPolynomial.monicNormalize at h + unfold CPolynomial.ofArray at h + rw [raw_monicNormalize_trim_arg] at h + exact h + +private theorem raw_monicNormalize_toPoly_monic {F : Type*} + [Field F] [BEq F] [LawfulBEq F] [DecidableEq F] + {p : CPolynomial.Raw F} (hp : p.trim ≠ 0) : + (CPolynomial.ofArray (CPolynomial.Raw.monicNormalize p)).toPoly.Monic := by + rw [raw_monicNormalize_toPoly_eq_normalize] + have hpC : CPolynomial.ofArray p ≠ 0 := by + intro h + have hval := congrArg Subtype.val h + -- ofArray p = 0 ⇒ p.trim = (0 : Raw).val = #[] + unfold CPolynomial.ofArray at hval + have hzero : (0 : CPolynomial F).val = (#[ ] : CPolynomial.Raw F) := rfl + exact hp (hval.trans hzero) + have hpPoly : (CPolynomial.ofArray p).toPoly ≠ 0 := + (CPolynomial.toPoly_eq_zero_iff (CPolynomial.ofArray p)).not.mpr hpC + exact Polynomial.monic_normalize hpPoly + +private theorem raw_modContext_toPoly_eq_modByMonic {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (D : CPolynomial.Raw.ModContext F) {p q : CPolynomial.Raw F} + (hpTrim : p.trim = p) (hqTrim : q.trim = q) + (hqMonic : (CPolynomial.ofArray q).monic) : + (CPolynomial.ofArray (D.modByMonic p q)).toPoly = + (CPolynomial.ofArray p).toPoly %ₘ (CPolynomial.ofArray q).toPoly := by + rw [D.modByMonic_eq_modByMonic p q hpTrim hqTrim] + have hpval : (CPolynomial.ofArray p).val = p := by + unfold CPolynomial.ofArray + exact hpTrim + have hqval : (CPolynomial.ofArray q).val = q := by + unfold CPolynomial.ofArray + exact hqTrim + have h := CPolynomial.modByMonic_toPoly_eq_modByMonic + (CPolynomial.ofArray p) (CPolynomial.ofArray q) hqMonic + -- Same bridge as `RootProduct.raw_modContext_toPoly_eq_modByMonic`. + rw [CPolynomial.ofArray_toPoly] + simpa [CPolynomial.modByMonic, CPolynomial.toPoly, hpval, hqval, + CPolynomial.ofArray_toPoly] using h + +private theorem raw_mulModWith_toPoly_eq_modByMonic {F : Type*} + [Field F] [BEq F] [LawfulBEq F] [DecidableEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + {modulus p q : CPolynomial.Raw F} (hmod : modulus.trim ≠ 0) : + (CPolynomial.ofArray (CPolynomial.Raw.mulModWith M D modulus p q)).toPoly = + ((CPolynomial.ofArray p).toPoly * (CPolynomial.ofArray q).toPoly) %ₘ + (CPolynomial.ofArray (CPolynomial.Raw.monicNormalize modulus)).toPoly := by + unfold CPolynomial.Raw.mulModWith + have hzero : ¬modulus.trim == (0 : CPolynomial.Raw F) := by + intro h + exact hmod (LawfulBEq.eq_of_beq h) + rw [if_neg hzero] + have hproductTrim : (M.mul p q).trim = M.mul p q := by + rw [M.mul_eq_mul] + exact CPolynomial.Raw.mul_is_trimmed p q + have hqMonic : (CPolynomial.ofArray (CPolynomial.Raw.monicNormalize modulus)).monic := + (CPolynomial.monic_toPoly_iff _).mpr (raw_monicNormalize_toPoly_monic hmod) + rw [raw_modContext_toPoly_eq_modByMonic D hproductTrim + (raw_monicNormalize_trim modulus) hqMonic] + rw [M.mul_eq_mul, CPolynomial.ofArray_toPoly, CPolynomial.ofArray_toPoly, + CPolynomial.Raw.toPoly_mul] + rw [CPolynomial.ofArray_toPoly, CPolynomial.ofArray_toPoly] + +private theorem polynomial_modByMonic_idem {F : Type*} [Field F] + {p q : Polynomial F} (hq : q.Monic) : + (p %ₘ q) %ₘ q = p %ₘ q := by + refine Polynomial.modByMonic_eq_of_dvd_sub hq ?_ + refine ⟨-(p /ₘ q), ?_⟩ + calc + p %ₘ q - p = p %ₘ q - (p %ₘ q + q * (p /ₘ q)) := by + rw [Polynomial.modByMonic_add_div p] + _ = q * -(p /ₘ q) := by + ring + +private theorem polynomial_mul_modByMonic_congr {F : Type*} [Field F] + {a b c d m : Polynomial F} + (ha : a %ₘ m = b %ₘ m) (hc : c %ₘ m = d %ₘ m) : + (a * c) %ₘ m = (b * d) %ₘ m := by + calc + (a * c) %ₘ m = (a %ₘ m * (c %ₘ m)) %ₘ m := by + rw [Polynomial.mul_modByMonic] + _ = (b %ₘ m * (d %ₘ m)) %ₘ m := by + rw [ha, hc] + _ = (b * d) %ₘ m := by + exact (Polynomial.mul_modByMonic b d m).symm + +private theorem polynomial_pow_modByMonic_congr {F : Type*} [Field F] + {a b m : Polynomial F} (h : a %ₘ m = b %ₘ m) : + ∀ n, a ^ n %ₘ m = b ^ n %ₘ m := by + intro n + induction n with + | zero => simp + | succ n ih => + rw [pow_succ, pow_succ] + exact polynomial_mul_modByMonic_congr ih h + +private theorem raw_powModBinaryAuxWith_toPoly_modByMonic {F : Type*} + [Field F] [BEq F] [LawfulBEq F] [DecidableEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + {modulus acc current : CPolynomial.Raw F} (hmod : modulus.trim ≠ 0) : + ∀ n, + (CPolynomial.ofArray + (CPolynomial.Raw.powModBinaryAuxWith M D modulus n acc current)).toPoly %ₘ + (CPolynomial.ofArray (CPolynomial.Raw.monicNormalize modulus)).toPoly = + ((CPolynomial.ofArray acc).toPoly * (CPolynomial.ofArray current).toPoly ^ n) %ₘ + (CPolynomial.ofArray (CPolynomial.Raw.monicNormalize modulus)).toPoly := by + intro n + induction n using Nat.strongRecOn generalizing acc current with + | ind n ih => + cases n with + | zero => + rw [CPolynomial.Raw.powModBinaryAuxWith, pow_zero] + congr 1 + ring + | succ n => + rw [CPolynomial.Raw.powModBinaryAuxWith] + let m := (CPolynomial.ofArray (CPolynomial.Raw.monicNormalize modulus)).toPoly + let acc' := + if (n + 1) % 2 == 1 then + CPolynomial.Raw.mulModWith M D modulus acc current + else + acc + let current' := CPolynomial.Raw.mulModWith M D modulus current current + have ih' := ih ((n + 1) / 2) + (Nat.div_lt_self (Nat.succ_pos n) (by decide)) + (acc := acc') (current := current') + change + (CPolynomial.ofArray + (CPolynomial.Raw.powModBinaryAuxWith M D modulus + ((n + 1) / 2) acc' current')).toPoly %ₘ + m = + ((CPolynomial.ofArray acc).toPoly * + (CPolynomial.ofArray current).toPoly ^ (n + 1)) %ₘ m + rw [ih'] + have hcurrent : + (CPolynomial.ofArray current').toPoly %ₘ m = + ((CPolynomial.ofArray current).toPoly * + (CPolynomial.ofArray current).toPoly) %ₘ m := by + dsimp [current', m] + rw [raw_mulModWith_toPoly_eq_modByMonic M D hmod] + exact polynomial_modByMonic_idem (raw_monicNormalize_toPoly_monic hmod) + by_cases hodd : (n + 1) % 2 == 1 + · have hacc : + (CPolynomial.ofArray acc').toPoly %ₘ m = + ((CPolynomial.ofArray acc).toPoly * + (CPolynomial.ofArray current).toPoly) %ₘ m := by + dsimp [acc', m] + rw [if_pos hodd] + rw [raw_mulModWith_toPoly_eq_modByMonic M D hmod] + exact polynomial_modByMonic_idem (raw_monicNormalize_toPoly_monic hmod) + have hpow : + ((CPolynomial.ofArray current).toPoly * + (CPolynomial.ofArray current).toPoly) ^ ((n + 1) / 2) = + (CPolynomial.ofArray current).toPoly ^ n := by + rw [mul_pow, ← pow_add] + congr 1 + have hoddNat : (n + 1) % 2 = 1 := by + simpa using hodd + omega + have hcurrentPow : + (CPolynomial.ofArray current').toPoly ^ ((n + 1) / 2) %ₘ m = + (CPolynomial.ofArray current).toPoly ^ n %ₘ m := by + have h := polynomial_pow_modByMonic_congr hcurrent ((n + 1) / 2) + rwa [hpow] at h + calc + ((CPolynomial.ofArray acc').toPoly * + (CPolynomial.ofArray current').toPoly ^ ((n + 1) / 2)) %ₘ m = + (((CPolynomial.ofArray acc).toPoly * + (CPolynomial.ofArray current).toPoly) * + (CPolynomial.ofArray current).toPoly ^ n) %ₘ m := by + exact polynomial_mul_modByMonic_congr hacc hcurrentPow + _ = ((CPolynomial.ofArray acc).toPoly * + (CPolynomial.ofArray current).toPoly ^ (n + 1)) %ₘ m := by + rw [pow_succ] + ring_nf + · have hacc : + (CPolynomial.ofArray acc').toPoly %ₘ m = + (CPolynomial.ofArray acc).toPoly %ₘ m := by + dsimp [acc'] + rw [if_neg hodd] + have hpow : + ((CPolynomial.ofArray current).toPoly * + (CPolynomial.ofArray current).toPoly) ^ ((n + 1) / 2) = + (CPolynomial.ofArray current).toPoly ^ (n + 1) := by + rw [mul_pow, ← pow_add] + congr 1 + have hoddNat : (n + 1) % 2 = 0 := by + simpa using hodd + omega + have hcurrentPow : + (CPolynomial.ofArray current').toPoly ^ ((n + 1) / 2) %ₘ m = + (CPolynomial.ofArray current).toPoly ^ (n + 1) %ₘ m := by + have h := polynomial_pow_modByMonic_congr hcurrent ((n + 1) / 2) + rwa [hpow] at h + exact polynomial_mul_modByMonic_congr hacc hcurrentPow + +private theorem raw_powModWith_X_toPoly_modByMonic {F : Type*} + [Field F] [BEq F] [LawfulBEq F] [DecidableEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + {modulus : CPolynomial.Raw F} (hmod : modulus.trim ≠ 0) (q : Nat) : + (CPolynomial.ofArray + (CPolynomial.Raw.powModWith M D modulus CPolynomial.Raw.X q)).toPoly %ₘ + (CPolynomial.ofArray (CPolynomial.Raw.monicNormalize modulus)).toPoly = + ((Polynomial.X : Polynomial F) ^ q) %ₘ + (CPolynomial.ofArray (CPolynomial.Raw.monicNormalize modulus)).toPoly := by + unfold CPolynomial.Raw.powModWith + have hzero : ¬modulus.trim == (0 : CPolynomial.Raw F) := by + intro h + exact hmod (LawfulBEq.eq_of_beq h) + rw [if_neg hzero] + let m := (CPolynomial.ofArray (CPolynomial.Raw.monicNormalize modulus)).toPoly + let oneMod := D.modByMonic (1 : CPolynomial.Raw F) (CPolynomial.Raw.monicNormalize modulus) + change + (CPolynomial.ofArray + (CPolynomial.Raw.powModBinaryAuxWith M D modulus q oneMod CPolynomial.Raw.X)).toPoly %ₘ + m = + ((Polynomial.X : Polynomial F) ^ q) %ₘ m + rw [raw_powModBinaryAuxWith_toPoly_modByMonic M D hmod] + have hOneTrim : (1 : CPolynomial.Raw F).trim = 1 := by + change CPolynomial.Raw.trim (#[] |>.push (1 : F)) = (#[] |>.push (1 : F)) + apply CPolynomial.Raw.Trim.push_trim + simp + have hone : + (CPolynomial.ofArray oneMod).toPoly %ₘ m = + (1 : Polynomial F) %ₘ m := by + dsimp [oneMod, m] + rw [raw_modContext_toPoly_eq_modByMonic D hOneTrim + (raw_monicNormalize_trim modulus) + ((CPolynomial.monic_toPoly_iff _).mpr (raw_monicNormalize_toPoly_monic hmod))] + rw [CPolynomial.ofArray_toPoly, CPolynomial.Raw.toPoly_one] + exact polynomial_modByMonic_idem (raw_monicNormalize_toPoly_monic hmod) + have hX : + (CPolynomial.ofArray CPolynomial.Raw.X).toPoly ^ q %ₘ m = + ((Polynomial.X : Polynomial F) ^ q) %ₘ m := by + rw [CPolynomial.ofArray_toPoly, CPolynomial.Raw.toPoly_X] + have hmul := polynomial_mul_modByMonic_congr hone hX + simpa [one_mul] using hmul + +private theorem raw_xModWith_toPoly_modByMonic {F : Type*} + [Field F] [BEq F] [LawfulBEq F] [DecidableEq F] + (D : CPolynomial.Raw.ModContext F) + {modulus : CPolynomial.Raw F} (hmod : modulus.trim ≠ 0) : + (CPolynomial.ofArray (CPolynomial.Raw.xModWith D modulus)).toPoly %ₘ + (CPolynomial.ofArray (CPolynomial.Raw.monicNormalize modulus)).toPoly = + (Polynomial.X : Polynomial F) %ₘ + (CPolynomial.ofArray (CPolynomial.Raw.monicNormalize modulus)).toPoly := by + unfold CPolynomial.Raw.xModWith + have hzero : ¬modulus.trim == (0 : CPolynomial.Raw F) := by + intro h + exact hmod (LawfulBEq.eq_of_beq h) + rw [if_neg hzero] + let m := (CPolynomial.ofArray (CPolynomial.Raw.monicNormalize modulus)).toPoly + have hXTrim : (CPolynomial.Raw.X : CPolynomial.Raw F).trim = CPolynomial.Raw.X := by + exact CPolynomial.Raw.X_canonical + rw [raw_modContext_toPoly_eq_modByMonic D hXTrim + (raw_monicNormalize_trim modulus) + ((CPolynomial.monic_toPoly_iff _).mpr (raw_monicNormalize_toPoly_monic hmod))] + rw [CPolynomial.ofArray_toPoly, CPolynomial.Raw.toPoly_X] + exact polynomial_modByMonic_idem (raw_monicNormalize_toPoly_monic hmod) + +private theorem raw_xPowSubXModWith_toPoly_modByMonic {F : Type*} + [Field F] [BEq F] [LawfulBEq F] [DecidableEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + {modulus : CPolynomial.Raw F} (hmod : modulus.trim ≠ 0) (q : Nat) : + (CPolynomial.ofArray + (CPolynomial.Raw.xPowSubXModWith M D q modulus)).toPoly %ₘ + (CPolynomial.ofArray (CPolynomial.Raw.monicNormalize modulus)).toPoly = + (((Polynomial.X : Polynomial F) ^ q - Polynomial.X) %ₘ + (CPolynomial.ofArray (CPolynomial.Raw.monicNormalize modulus)).toPoly) := by + unfold CPolynomial.Raw.xPowSubXModWith + rw [CPolynomial.ofArray_toPoly, CPolynomial.Raw.toPoly_sub, Polynomial.sub_modByMonic] + rw [← CPolynomial.ofArray_toPoly + (CPolynomial.Raw.powModWith M D modulus CPolynomial.Raw.X q), + ← CPolynomial.ofArray_toPoly (CPolynomial.Raw.xModWith D modulus)] + rw [raw_powModWith_X_toPoly_modByMonic M D hmod q, + raw_xModWith_toPoly_modByMonic D hmod] + rw [Polynomial.sub_modByMonic] + +/-- The finite-field root product divides the finite-field Frobenius polynomial. + +This is the modular-gcd bridge: the executable root product uses the congruent +modular witness `(X^q mod p) - (X mod p)` instead of materializing `X^q - X`. +-/ +theorem finiteFieldRootProductWith_dvd_frobenius {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + (ctx : SmallPrimeTraceContext F) {p : CPolynomial F} (_hp : p ≠ 0) : + (finiteFieldRootProductWith M D ctx.toFiniteFieldContext p).toPoly ∣ + ((Polynomial.X : Polynomial F) ^ ctx.q - Polynomial.X) := by + let : DecidableEq F := instDecidableEqOfLawfulBEq + rw [finiteFieldRootProductWith_toPoly_eq_normalize_gcd M D ctx.toFiniteFieldContext _hp] + apply dvd_trans (normalize_associated _).dvd + let pMonic := CPolynomial.monicNormalize p + let witness := + (CPolynomial.ofArray + (CPolynomial.Raw.xPowSubXModWith M D ctx.q pMonic.val)).toPoly + have hgLeft : EuclideanDomain.gcd pMonic.toPoly witness ∣ pMonic.toPoly := + EuclideanDomain.gcd_dvd_left _ _ + have hgRight : EuclideanDomain.gcd pMonic.toPoly witness ∣ witness := + EuclideanDomain.gcd_dvd_right _ _ + have hpPoly : p.toPoly ≠ 0 := + (CPolynomial.toPoly_eq_zero_iff p).not.mpr _hp + have hpMonicPoly : pMonic.toPoly.Monic := by + dsimp [pMonic] + rw [CPolynomial.monicNormalize_toPoly_eq_normalize] + exact Polynomial.monic_normalize hpPoly + have hpMonicNe : pMonic ≠ 0 := monicNormalize_ne_zero_of_ne_zero _hp + have hmod : pMonic.val.trim ≠ 0 := by + intro htrim + apply hpMonicNe + apply CPolynomial.ext + rw [CPolynomial.trim_eq] at htrim + -- Zero is the empty raw array. + change pMonic.val = (#[ ] : CPolynomial.Raw F) + exact htrim.trans (by rfl) + have hnormModulus : + (CPolynomial.ofArray (CPolynomial.Raw.monicNormalize pMonic.val)).toPoly = + pMonic.toPoly := by + rw [raw_monicNormalize_toPoly_eq_normalize] + rw [CPolynomial.ofArray_toPoly] + exact hpMonicPoly.normalize_eq_self + have hwitnessMod : + witness %ₘ pMonic.toPoly = + (((Polynomial.X : Polynomial F) ^ ctx.q - Polynomial.X) %ₘ pMonic.toPoly) := by + dsimp [witness] + have h := raw_xPowSubXModWith_toPoly_modByMonic M D hmod ctx.q + rwa [hnormModulus] at h + have hpMonic : + pMonic.toPoly ∣ (Polynomial.X : Polynomial F) ^ ctx.q - Polynomial.X - witness := by + rw [← Polynomial.modByMonic_eq_zero_iff_dvd hpMonicPoly] + rw [Polynomial.sub_modByMonic] + rw [← hwitnessMod] + simp + simpa [sub_eq_add_neg, add_assoc, add_left_comm, add_comm] using + dvd_add (hgLeft.trans hpMonic) hgRight + +/-- Finite-field root products satisfy the Shoup splitter's valid-input predicate. -/ +theorem finiteFieldRootProductWith_shoupSplitterInput {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + (ctx : SmallPrimeTraceContext F) {p : CPolynomial F} (hp : p ≠ 0) : + shoupSplitterInput ctx (finiteFieldRootProductWith M D ctx.toFiniteFieldContext p) := by + exact ⟨finiteFieldRootProductWith_ne_zero_of_ne_zero M D ctx.toFiniteFieldContext hp, + finiteFieldRootProductWith_dvd_frobenius M D ctx hp⟩ + +/-- Default-backend finite-field root products satisfy the Shoup valid-input predicate. -/ +theorem finiteFieldRootProduct_shoupSplitterInput {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (ctx : SmallPrimeTraceContext F) {p : CPolynomial F} (hp : p ≠ 0) : + shoupSplitterInput ctx (finiteFieldRootProduct ctx.toFiniteFieldContext p) := by + exact finiteFieldRootProductWith_shoupSplitterInput + CPolynomial.Raw.MulContext.naive CPolynomial.Raw.ModContext.naive ctx hp + +/-- Alias emphasizing the root-product precondition used by Shoup completeness. -/ +theorem rootProduct_satisfies_shoupSplitterInput {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) + (ctx : SmallPrimeTraceContext F) {p : CPolynomial F} (hp : p ≠ 0) : + (shoupLinearFactorProductSplitterWith M D ctx).validInput ctx.q + (finiteFieldRootProductWith M D ctx.toFiniteFieldContext p) := by + exact finiteFieldRootProductWith_shoupSplitterInput M D ctx hp + +end FiniteField + +end Roots + +end CPolynomial + +end CompPoly diff --git a/CompPoly/Univariate/Roots/Shoup/FrobeniusLinear.lean b/CompPoly/Univariate/Roots/Shoup/FrobeniusLinear.lean new file mode 100644 index 00000000..c1eedec7 --- /dev/null +++ b/CompPoly/Univariate/Roots/Shoup/FrobeniusLinear.lean @@ -0,0 +1,133 @@ +/- +Copyright (c) 2026 CompPoly Contributors. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Valerii Huhnin +-/ + +module + +-- `natDegree` and `coeff` are declared in a bare `public section`, so their bodies are +-- opaque downstream. The proofs below reason about the underlying array directly, which +-- is the same-package implementation dependency `import all` exists for; see +-- `docs/wiki/module-system.md`. +import all CompPoly.Univariate.Basic +public import CompPoly.Univariate.Roots.Shoup.Basic +public import Mathlib.Algebra.CharP.CharAndCard + +/-! +# Frobenius Divisors with One Root are Linear + +This file contains the field-theoretic bridge used by the Shoup trace splitter: +a nonzero divisor of `X^q - X` over `GF(q)` with exactly one field root is a +represented nonconstant linear factor. +-/ + +@[expose] public section + +namespace CompPoly + +namespace CPolynomial + +namespace Roots + +namespace FiniteField + +/-- +A nonzero divisor of `X^q - X` with exactly one field root is represented as a +linear factor. +-/ +theorem isRepresentedLinearFactor_of_dvd_frobenius_unique_root {F : Type*} + [Field F] [BEq F] [LawfulBEq F] + (ctx : SmallPrimeTraceContext F) {factor : CPolynomial F} {a : F} + (hfactorNe : factor ≠ 0) + (hdvd : factor.toPoly ∣ ((Polynomial.X : Polynomial F) ^ ctx.q - Polynomial.X)) + (hroot : CPolynomial.eval a factor = 0) + (hunique : ∀ b, CPolynomial.eval b factor = 0 → b = a) : + isRepresentedLinearFactor factor = true := by + let : DecidableEq F := instDecidableEqOfLawfulBEq + let : Finite F := ctx.finite + let : Fintype F := Fintype.ofFinite F + have hqcard : ctx.q = Fintype.card F := by + rw [← ctx.card_eq, Nat.card_eq_fintype_card] + have hfactorPolyNe : factor.toPoly ≠ 0 := + (CPolynomial.toPoly_eq_zero_iff factor).not.mpr hfactorNe + have hfrobNe : ((Polynomial.X : Polynomial F) ^ ctx.q - Polynomial.X) ≠ 0 := by + rw [hqcard] + exact FiniteField.X_pow_card_sub_X_ne_zero F Fintype.one_lt_card + have hsplitFrob : Polynomial.Splits + ((Polynomial.X : Polynomial F) ^ ctx.q - Polynomial.X) := by + rw [hqcard] + rw [Polynomial.splits_iff_card_roots] + rw [FiniteField.roots_X_pow_card_sub_X] + rw [← Finset.card_def, Finset.card_univ] + rw [FiniteField.X_pow_card_sub_X_natDegree_eq F Fintype.one_lt_card] + have hsplitFactor : Polynomial.Splits factor.toPoly := + hsplitFrob.of_dvd hfrobNe hdvd + have hrootsLe : factor.toPoly.roots ≤ + (((Polynomial.X : Polynomial F) ^ ctx.q - Polynomial.X).roots) := + Polynomial.roots.le_of_dvd hfrobNe hdvd + have hfrobRoots : (((Polynomial.X : Polynomial F) ^ ctx.q - Polynomial.X).roots) = + (Finset.univ : Finset F).val := by + rw [hqcard] + exact FiniteField.roots_X_pow_card_sub_X F + have hfrobNodup : + (((Polynomial.X : Polynomial F) ^ ctx.q - Polynomial.X).roots).Nodup := by + rw [hfrobRoots] + exact Finset.nodup _ + have hfactorRootsNodup : factor.toPoly.roots.Nodup := + Multiset.nodup_of_le hrootsLe hfrobNodup + have haMem : a ∈ factor.toPoly.roots := by + rw [Polynomial.mem_roots hfactorPolyNe] + simpa [Polynomial.IsRoot, CPolynomial.eval_toPoly] using hroot + have htoFinset : factor.toPoly.roots.toFinset = {a} := by + ext b + constructor + · intro hb + rw [Finset.mem_singleton] + apply hunique b + have hbroot : b ∈ factor.toPoly.roots := by + simpa using (Multiset.mem_toFinset.mp hb) + rw [Polynomial.mem_roots hfactorPolyNe] at hbroot + simpa [Polynomial.IsRoot, CPolynomial.eval_toPoly] using hbroot + · intro hb + rw [Finset.mem_singleton] at hb + subst b + exact Multiset.mem_toFinset.mpr haMem + have hrootsCard : factor.toPoly.roots.card = 1 := by + have hcardFin : factor.toPoly.roots.toFinset.card = factor.toPoly.roots.card := + Multiset.toFinset_card_of_nodup hfactorRootsNodup + rw [← hcardFin, htoFinset] + simp + have hnatPoly : factor.toPoly.natDegree = 1 := by + simpa [hrootsCard] using hsplitFactor.natDegree_eq_card_roots + have hnat : factor.natDegree = 1 := by + rw [CPolynomial.natDegree_toPoly] + exact hnatPoly + have hsize : factor.val.size ≤ 2 := by + cases hs : factor.val.size with + | zero => + have hzero : factor = 0 := by + apply CPolynomial.ext + exact Array.eq_empty_of_size_eq_zero hs + exact (hfactorNe hzero).elim + | succ n => + have hn : n = 1 := by + have hnatVal : factor.natDegree = n := by + simp [CPolynomial.natDegree, hs] + omega + omega + have hcoeff : factor.coeff 1 ≠ 0 := by + have hlead := CPolynomial.leadingCoeff_ne_zero hfactorNe + rw [CPolynomial.leadingCoeff_eq_coeff_natDegree, hnat] at hlead + exact hlead + unfold isRepresentedLinearFactor + simp [hsize] + simpa [CPolynomial.coeff, CPolynomial.Raw.coeff] using hcoeff + +end FiniteField + +end Roots + +end CPolynomial + +end CompPoly diff --git a/README.md b/README.md index bc4b1514..3862f374 100644 --- a/README.md +++ b/README.md @@ -94,7 +94,7 @@ import CompPoly - **Multilinear (`CMlPolynomial`, `CMlPolynomialEval`)**: coefficient and Boolean-hypercube evaluation representations, basis conversions, and equivalence to Mathlib's multilinear polynomial surface. - **Univariate (`CPolynomial`)**: full ring structure, core operations (`C`, `X`, `monomial`, `coeff`, `eval`, `eval₂`, degree/leading/support), `ringEquiv` to Mathlib `Polynomial`, Lagrange interpolation (`basis`, `interpolate`, `interpolatePow`), and fixed-domain barycentric interpolation for repeated-query evaluation. - **Bivariate (`CBivariate`)**: specialized `CPolynomial (CPolynomial R)` API with `X`, `Y`, `monomialXY`, evaluation, leading coefficients, `swap`, and equivalence to `Polynomial (Polynomial R)`. -- **Fields**: broad set of finite-field instances (BabyBear, KoalaBear, Goldilocks, Mersenne31, Secp256k1, and the BN254/BLS12-381/BLS12-377 scalar fields), native-word Montgomery arithmetic for the 31-bit and 255-bit-and-below primes, computable extension fields `F[X]/f` for an arbitrary monic modulus, binary tower support, and additive NTT infrastructure. +- **Fields**: broad set of finite-field instances (BabyBear, KoalaBear, Goldilocks, Mersenne31, Secp256k1, and the BN254/BLS12-381/BLS12-377 scalar fields), native-word Montgomery arithmetic for the 31-bit and 255-bit-and-below primes, a verified single-word `UInt64` Goldilocks implementation, computable extension fields `F[X]/f` for an arbitrary monic modulus, binary tower support, and additive NTT infrastructure. - **Coding theory**: Reed-Solomon encoding identified with the forward NTT, Gao's unique decoder with soundness/completeness and a farness certificate on refusal, and a Guruswami-Sudan list decoder with pluggable interpolation and root-finding backends, all proved correct. - **Linear algebra**: executable dense matrices (Gauss-Jordan, homogeneous kernels) and polynomial matrices with Mulders-Storjohann shifted row reduction, each with a proven-equal allocation-efficient fast path. diff --git a/ROADMAP.md b/ROADMAP.md index b17449d1..fe8e8e41 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -43,6 +43,8 @@ CompPoly aims to be the premier formally verified library for computable polynom - ✅ `algebra`, `module`: Algebra and module structures - ✅ `degrees`; ✅ `eval₂Hom`: Degree utilities and evaluation homomorphisms - ✅ `finSuccEquiv`: Variable manipulation equivalences (for `CMvPolynomial`) + - ✅ `partialEvalFirst`: Partial evaluation fixing the first variable, with its + evaluation and per-variable degree-bound lemmas - ✅ `isEmptyRingEquiv` for `CMvPolynomial 0 R` - ✅ `smulZeroClass`: Scalar multiplication with zero behavior - ✅ `sumToIter`: Iteration utility with reconstruction/API lemmas @@ -87,6 +89,7 @@ CompPoly aims to be the premier formally verified library for computable polynom `Polynomial.irreducible_of_rabin` so the two soundness proofs do not need parallel maintenance - 64-bit-radix Montgomery layer, so `Hachi` gets a `FastField` base + - ✅ Polynomial-basis `GF(2^64)` and its degree-3 extension `GF(2^192)` (`Fields/Binary/BF64/`), a flat quotient by an irreducible degree-64 pentanomial rather than an iterated quadratic tower - ✅ Implement a specialized Bivariate polynomial type, e.g. as `CPolynomial (CPolynomial R)` with specialized polynomial operations (that can then be optimized) **Success Criteria**: Zero `sorry`s in core operations, all ring structures complete, clean build with no warnings, reasonable proof ergonomics. @@ -107,6 +110,9 @@ CompPoly aims to be the premier formally verified library for computable polynom - ✅ Eight-limb Montgomery carrier with CIOS multiplication for moduli below `2^255` (`Montgomery/Native64x8*.lean`, `Mont64x8Field`), instantiated by `BN254/Fast.lean`, `BLS12_381/Fast.lean`, and `BLS12_377/Fast.lean` + - ✅ Single-word `UInt64` carriers for 64-bit and 31-bit primes outside the + Montgomery bounds (`Goldilocks/Fast.lean`, `Mersenne31/Fast.lean`), reducing + via the modulus identity rather than Montgomery residues - ✅ Checked binary-GCD inversion for the eight-limb fields (`Montgomery/Native64x8Inv.lean`, [eprint 2020/972](https://eprint.iacr.org/2020/972)), benchmarked against `ZMod` extended Euclid and Fermat in `fields-mont64x8-*-inv` @@ -144,7 +150,15 @@ CompPoly aims to be the premier formally verified library for computable polynom 6. **Benchmarking** - ✅ Basic, reproducible evaluation benchmark executable (`lake exe CompPolyBench`; see `bench/README.md`) - ✅ CI build/run with artifact upload (GitHub Actions `lean_action_ci.yml`) - - 🔄 Expand regression coverage and published performance baselines + - ✅ Operation-level coverage: base-field `mul`/`add`/`inv`/`pow` over + KoalaBear, BabyBear, Mersenne31 and Goldilocks, the eight-limb Montgomery + multiply, the binary tower's table-driven kernels, the standalone + multiplicative NTT over `n = 2^8 … 2^16`, Reed-Solomon encoding, and the + schoolbook/NTT crossover. Field and kernel rows are chained and reported + per operation, latency and throughput separately; see + [`docs/wiki/benchmarking.md`](docs/wiki/benchmarking.md) + - 🔄 Published performance baselines, and a measured comparison against a + pinned external implementation ([`BENCHMARKING.md`](BENCHMARKING.md) §13) 7. **Bivariate polynomial operations** - ✅ Optimize the existing bivariate polynomial type `CPolynomial (CPolynomial R)`: @@ -173,10 +187,10 @@ CompPoly aims to be the premier formally verified library for computable polynom `decode_none_farness`, which reads decoder refusal as a farness certificate - ✅ Implement Guruswami-Sudan list-decoding algorithm (`Bivariate/GuruswamiSudan/`), following the interpolation-and-root-finding - decomposition of [GS99]: a backend-parametric `Core` / `Context` with dense - and Lee-O'Sullivan ([LOS06]) interpolation and Roth-Ruckenstein ([RR00]) and - Alekhnovich ([Ale05]) root search, instantiated in `Implementations` and - `Executable` + decomposition of [GS99]: a backend-parametric `Core` / `Context` with dense, + Lee-O'Sullivan ([LOS06]), approximant-basis (PM-Basis), and hybrid + interpolation, plus Roth-Ruckenstein ([RR00]) and Alekhnovich ([Ale05]) root + search, instantiated in `Implementations` and `Executable` - ✅ Proofs of correctness: `gsCore_sound`, `gsCore_complete_of_interpolate`, and `gsCore_complete_of_roots_all_valid_witnesses` in `CoreCorrectness.lean`, stated against the context contracts so they hold for every backend @@ -192,7 +206,12 @@ CompPoly aims to be the premier formally verified library for computable polynom - ✅ Smooth multiplicative-subgroup refinement splitting for finite fields whose multiplicative group admits a smooth schedule ([MOV92], `Roots/SmoothSubgroup/`), benchmarked as `univariate-roots-finite-field-*` - - 🔄 Splitting strategies for fields with no smooth refinement schedule + - ✅ Shoup-style small-characteristic trace splitting ([vzGS92], + `Roots/Shoup/`) and bounded Las Vegas Cantor–Zassenhaus (`Roots/LasVegas/`, + odd-char and char-2 trace branches with probability proofs) for fields + without a smooth refinement schedule + - 🔄 Named high-width binary-tower `SmallPrimeTraceContext` instances (32/64) + and optional GF(2^{48})/GF(2^{72}) carriers for production char-2 benches 10. **Computable linear algebra** - ✅ Dense row-major matrices with row operations, RREF shape and semantics, and @@ -205,6 +224,11 @@ CompPoly aims to be the premier formally verified library for computable polynom `LinearAlgebra/PolynomialMatrix/`). The fast variants are proved extensionally equal to the direct ones in `MuldersStorjohannCorrectness/Fast.lean`, so every correctness result transfers. + - ✅ Order-basis (approximant) layer over polynomial matrices + (`PolynomialMatrix/Approximant/`): modular key equations, the divide-and-conquer + PM-Basis recursion with X-adic soundness and kernel-leaf completeness, and + partial linearization, alongside supporting row selection, minimal weak-Popov + forms, and Strassen multiplication used by the recursion. **Success Criteria**: notable speedup for large polynomial operations, verified correctness, benchmarks demonstrating competitive performance with industry-standard implementations. diff --git a/bench/.gitignore b/bench/.gitignore index 63b9ec19..89f9ac04 100644 --- a/bench/.gitignore +++ b/bench/.gitignore @@ -1,2 +1 @@ -report-*.md -results-*.jsonl +out/ diff --git a/bench/CompPolyBench/Bivariate/Basic.lean b/bench/CompPolyBench/Bivariate/Basic.lean index 671f848a..4ca2d887 100644 --- a/bench/CompPolyBench/Bivariate/Basic.lean +++ b/bench/CompPolyBench/Bivariate/Basic.lean @@ -19,16 +19,15 @@ open CompPoly namespace CompPolyBench -/-- Benchmark group metadata for `CompPoly.Bivariate.Basic`. -/ -def bivariateGroupInfos : List BenchGroupInfo := [ - ⟨"bivariate-full-koalabear", "Bivariate full evaluation (KoalaBear)"⟩, - ⟨"bivariate-full-goldilocks", "Bivariate full evaluation (Goldilocks)"⟩, - ⟨"bivariate-full-bn254", "Bivariate full evaluation (BN254)"⟩ -] +/-- Number of distinct evaluation points cycled by the bivariate benchmarks. + +Also the period of every body in this file in its iteration index, and so the +digest length of every group here. -/ +private def bivariatePointCount : Nat := 32 /-- Shared input-shape label for bivariate evaluation benchmarks. -/ private def bivariateInputShape : String := - "xDegree<8, yDegree<64, one nonzero per 4 coeffs, 32 points" + s!"xDegree<8, yDegree<64, one nonzero per 4 coeffs, {bivariatePointCount} points" /-- Build a bivariate polynomial from generated coefficients. -/ private def buildCBivariate {R : Type*} @@ -45,46 +44,42 @@ private def buildCBivariate {R : Type*} /-- Run bivariate full-evaluation benchmarks over a generic prime `ZMod` field. -/ private def runBivariateZMod (modulus : Nat) [Fact (Nat.Prime modulus)] (key nameSuffix fieldName fieldTitle : String) - (largeHornerYxMeasured mediumHornerYxMeasured smallHornerYxMeasured : Nat) - (largeHornerXyMeasured mediumHornerXyMeasured smallHornerXyMeasured : Nat) (preset : BenchPreset) (gen : StdGen) : IO (BenchGroup × StdGen) := do let (terms, gen) := (zmodArray modulus 512 true).run gen let (points, gen) := (zmodArray modulus 64 false).run gen let poly := buildCBivariate terms let evalPoint (i : Nat) : ZMod modulus × ZMod modulus := - let offset := 2 * (i % 32) + let offset := 2 * (i % bivariatePointCount) (points.getD (offset % points.size) 0, points.getD ((offset + 1) % points.size) 0) - let warmup := warmupIterations preset - let measured := measuredIterations preset - let hornerYxMeasured := - preset.selectNat largeHornerYxMeasured mediumHornerYxMeasured smallHornerYxMeasured - let hornerXyMeasured := - preset.selectNat largeHornerXyMeasured mediumHornerXyMeasured smallHornerXyMeasured - let checksumIterations := groupChecksumIterations measured [ - hornerYxMeasured, hornerXyMeasured - ] - let naive ← runTimed - ("bivariate-full-eval-naive" ++ nameSuffix) "CBivariate" "evalEval" fieldName - bivariateInputShape preset warmup measured + let checksumIterations := digestPeriod bivariatePointCount + let naive ← runTimedSpec + { name := ("bivariate-full-eval-naive" ++ nameSuffix), representation := "CBivariate", + method := "evalEval", field := fieldName, inputShape := bivariateInputShape, + digestIterations := checksumIterations } + preset (fun i ↦ let point := evalPoint i CBivariate.evalEval point.1 point.2 poly) - checksumZMod (checksumIterations := checksumIterations) - let hornerYx ← runTimed - ("bivariate-full-eval-horner-yx" ++ nameSuffix) "CBivariate" "evalEvalHornerYThenX" - fieldName bivariateInputShape preset warmup hornerYxMeasured + checksumZMod + let hornerYx ← runTimedSpec + { name := ("bivariate-full-eval-horner-yx" ++ nameSuffix), representation := "CBivariate", + method := "evalEvalHornerYThenX", field := fieldName, inputShape := bivariateInputShape, + digestIterations := checksumIterations } + preset (fun i ↦ let point := evalPoint i CBivariate.evalEvalHornerYThenX point.1 point.2 poly) - checksumZMod (checksumIterations := checksumIterations) - let hornerXy ← runTimed - ("bivariate-full-eval-horner-xy" ++ nameSuffix) "CBivariate" "evalEvalHornerXThenY" - fieldName bivariateInputShape preset warmup hornerXyMeasured + checksumZMod + let hornerXy ← runTimedSpec + { name := ("bivariate-full-eval-horner-xy" ++ nameSuffix), representation := "CBivariate", + method := "evalEvalHornerXThenY", field := fieldName, inputShape := bivariateInputShape, + digestIterations := checksumIterations } + preset (fun i ↦ let point := evalPoint i CBivariate.evalEvalHornerXThenY point.1 point.2 poly) - checksumZMod (checksumIterations := checksumIterations) + checksumZMod pure ({ groupKey := key, title := "Bivariate full evaluation (" ++ fieldTitle ++ ")", @@ -97,70 +92,70 @@ private def runKoalaBearBivariate (preset : BenchPreset) (gen : StdGen) : let (points, gen) := (koalaBearPoints 64).run gen let p := buildCBivariate terms let evalPoint (i : Nat) : KoalaBear.Field × KoalaBear.Field := - let offset := 2 * (i % 32) + let offset := 2 * (i % bivariatePointCount) (points.getD (offset % points.size) 0, points.getD ((offset + 1) % points.size) 0) let fastTerms := koalaBearFastArray terms let fastPoints := koalaBearFastArray points let fastP := buildCBivariate fastTerms let fastEvalPoint (i : Nat) : KoalaBear.Fast.Field × KoalaBear.Fast.Field := - let offset := 2 * (i % 32) + let offset := 2 * (i % bivariatePointCount) (fastPoints.getD (offset % fastPoints.size) 0, fastPoints.getD ((offset + 1) % fastPoints.size) 0) - let warmup := warmupIterations preset - let measured := measuredIterations preset - let hornerYxMeasured := preset.selectNat 11000 1600 300 - let hornerXyMeasured := preset.selectNat 100000 14000 3000 - let fastMeasured := preset.selectNat 14000 2000 400 - let fastHornerYxMeasured := preset.selectNat 35000 5000 1000 - let fastHornerXyMeasured := preset.selectNat 1680000 240000 48000 - let checksumIterations := groupChecksumIterations measured [ - hornerYxMeasured, hornerXyMeasured, fastMeasured, fastHornerYxMeasured, - fastHornerXyMeasured - ] - let naive ← runTimed - "bivariate-full-eval-naive" "CBivariate" "evalEval" "KoalaBear.Field" - bivariateInputShape preset warmup measured + let checksumIterations := digestPeriod bivariatePointCount + let naive ← runTimedSpec + { name := "bivariate-full-eval-naive", representation := "CBivariate", method := "evalEval", + field := "KoalaBear.Field", inputShape := bivariateInputShape, + digestIterations := checksumIterations } + preset (fun i ↦ let point := evalPoint i CBivariate.evalEval point.1 point.2 p) - checksumKoalaBear (checksumIterations := checksumIterations) - let fastNaive ← runTimed - "bivariate-full-eval-naive-fast" "CBivariate" "evalEval" "KoalaBear.Fast.Field" - bivariateInputShape preset warmup fastMeasured + checksumKoalaBear + let fastNaive ← runTimedSpec + { name := "bivariate-full-eval-naive-fast", representation := "CBivariate", + method := "evalEval", field := "KoalaBear.Fast.Field", inputShape := bivariateInputShape, + digestIterations := checksumIterations } + preset (fun i ↦ let point := fastEvalPoint i CBivariate.evalEval point.1 point.2 fastP) - checksumKoalaBearFast (checksumIterations := checksumIterations) - let hornerYx ← runTimed - "bivariate-full-eval-horner-yx" "CBivariate" "evalEvalHornerYThenX" "KoalaBear.Field" - bivariateInputShape preset warmup hornerYxMeasured + checksumKoalaBearFast + let hornerYx ← runTimedSpec + { name := "bivariate-full-eval-horner-yx", representation := "CBivariate", + method := "evalEvalHornerYThenX", field := "KoalaBear.Field", + inputShape := bivariateInputShape, digestIterations := checksumIterations } + preset (fun i ↦ let point := evalPoint i CBivariate.evalEvalHornerYThenX point.1 point.2 p) - checksumKoalaBear (checksumIterations := checksumIterations) - let fastHornerYx ← runTimed - "bivariate-full-eval-horner-yx-fast" "CBivariate" "evalEvalHornerYThenX" - "KoalaBear.Fast.Field" - bivariateInputShape preset warmup fastHornerYxMeasured + checksumKoalaBear + let fastHornerYx ← runTimedSpec + { name := "bivariate-full-eval-horner-yx-fast", representation := "CBivariate", + method := "evalEvalHornerYThenX", field := "KoalaBear.Fast.Field", + inputShape := bivariateInputShape, digestIterations := checksumIterations } + preset (fun i ↦ let point := fastEvalPoint i CBivariate.evalEvalHornerYThenX point.1 point.2 fastP) - checksumKoalaBearFast (checksumIterations := checksumIterations) - let hornerXy ← runTimed - "bivariate-full-eval-horner-xy" "CBivariate" "evalEvalHornerXThenY" "KoalaBear.Field" - bivariateInputShape preset warmup hornerXyMeasured + checksumKoalaBearFast + let hornerXy ← runTimedSpec + { name := "bivariate-full-eval-horner-xy", representation := "CBivariate", + method := "evalEvalHornerXThenY", field := "KoalaBear.Field", + inputShape := bivariateInputShape, digestIterations := checksumIterations } + preset (fun i ↦ let point := evalPoint i CBivariate.evalEvalHornerXThenY point.1 point.2 p) - checksumKoalaBear (checksumIterations := checksumIterations) - let fastHornerXy ← runTimed - "bivariate-full-eval-horner-xy-fast" "CBivariate" "evalEvalHornerXThenY" - "KoalaBear.Fast.Field" - bivariateInputShape preset warmup fastHornerXyMeasured + checksumKoalaBear + let fastHornerXy ← runTimedSpec + { name := "bivariate-full-eval-horner-xy-fast", representation := "CBivariate", + method := "evalEvalHornerXThenY", field := "KoalaBear.Fast.Field", + inputShape := bivariateInputShape, digestIterations := checksumIterations } + preset (fun i ↦ let point := fastEvalPoint i CBivariate.evalEvalHornerXThenY point.1 point.2 fastP) - checksumKoalaBearFast (checksumIterations := checksumIterations) + checksumKoalaBearFast pure ({ groupKey := "bivariate-full-koalabear", title := "Bivariate full evaluation (KoalaBear)", @@ -172,14 +167,14 @@ private def runGoldilocksBivariate (preset : BenchPreset) (gen : StdGen) : IO (BenchGroup × StdGen) := do runBivariateZMod Goldilocks.fieldSize "bivariate-full-goldilocks" "-goldilocks" "Goldilocks.Field" - "Goldilocks" 12000 1700 350 30000 4500 900 preset gen + "Goldilocks" preset gen /-- Run the BN254 bivariate full-evaluation benchmark. -/ private def runBn254Bivariate (preset : BenchPreset) (gen : StdGen) : IO (BenchGroup × StdGen) := do runBivariateZMod BN254.scalarFieldSize "bivariate-full-bn254" "-bn254" "BN254.ScalarField" "BN254" - 12000 1700 350 27000 4000 800 preset gen + preset gen /-- Runnable bivariate benchmark tasks. -/ def bivariateTasks : List BenchTask := [ @@ -194,9 +189,4 @@ def bivariateTasks : List BenchTask := [ runBn254Bivariate ] -/-- Run selected bivariate full-evaluation benchmarks. -/ -def runBivariate (preset : BenchPreset) (selection : BenchSelection) (gen : StdGen) : - IO (Array BenchGroup × StdGen) := do - runSelectedTasks bivariateTasks preset selection gen - end CompPolyBench diff --git a/bench/CompPolyBench/Bivariate/Factor.lean b/bench/CompPolyBench/Bivariate/Factor.lean index 0d8f7054..8a185ee4 100644 --- a/bench/CompPolyBench/Bivariate/Factor.lean +++ b/bench/CompPolyBench/Bivariate/Factor.lean @@ -32,19 +32,6 @@ open CompPoly namespace CompPolyBench -/-- Benchmark group metadata for bivariate linear division (3 fields × 3 sizes). -/ -def factorGroupInfos : List BenchGroupInfo := [ - ⟨"bivariate-divlinear-koalabear-y8", "Bivariate division by Y - f (KoalaBear, yDeg<8)"⟩, - ⟨"bivariate-divlinear-koalabear-y16", "Bivariate division by Y - f (KoalaBear, yDeg<16)"⟩, - ⟨"bivariate-divlinear-koalabear-y32", "Bivariate division by Y - f (KoalaBear, yDeg<32)"⟩, - ⟨"bivariate-divlinear-goldilocks-y8", "Bivariate division by Y - f (Goldilocks, yDeg<8)"⟩, - ⟨"bivariate-divlinear-goldilocks-y16", "Bivariate division by Y - f (Goldilocks, yDeg<16)"⟩, - ⟨"bivariate-divlinear-goldilocks-y32", "Bivariate division by Y - f (Goldilocks, yDeg<32)"⟩, - ⟨"bivariate-divlinear-bn254-y8", "Bivariate division by Y - f (BN254, yDeg<8)"⟩, - ⟨"bivariate-divlinear-bn254-y16", "Bivariate division by Y - f (BN254, yDeg<16)"⟩, - ⟨"bivariate-divlinear-bn254-y32", "Bivariate division by Y - f (BN254, yDeg<32)"⟩ -] - /-- Input-shape label for a given coefficient count (`xDegree < 8`, so `yDegree < terms / 8`). -/ private def factorInputShape (terms : Nat) : String := @@ -80,7 +67,6 @@ private def linearDivisor {R : Type*} one `Y`-degree size (`terms` coefficients, `yDegree < terms / 8`). -/ private def runFactorZMod (modulus : Nat) [Fact (Nat.Prime modulus)] (key fieldName fieldTitle nameSuffix yLabel : String) (terms : Nat) - (largeHorner mediumHorner smallHorner largeMonic mediumMonic smallMonic : Nat) (preset : BenchPreset) (gen : StdGen) : IO (BenchGroup × StdGen) := do let (qTerms, gen) := (zmodArray modulus terms false).run gen @@ -88,26 +74,24 @@ private def runFactorZMod (modulus : Nat) [Fact (Nat.Prime modulus)] let (perturb, gen) := (zmodArray modulus 64 false).run gen let q := buildCBivariate qTerms let f := buildCPolynomial fTerms - let warmup := warmupIterations preset - let measured := measuredIterations preset - let hornerMeasured := preset.selectNat largeHorner mediumHorner smallHorner - let monicMeasured := preset.selectNat largeMonic mediumMonic smallMonic - let checksumIterations := groupChecksumIterations measured [hornerMeasured, monicMeasured] + let checksumIterations := digestPeriod perturb.size let shape := factorInputShape terms let fAt (i : Nat) : CPolynomial (ZMod modulus) := f + CPolynomial.C (perturb.getD (i % perturb.size) 0) let checksumBiv (p : CBivariate (ZMod modulus)) : Nat := checksumCPolynomial (checksumCPolynomial checksumZMod) p - let horner ← runTimed - ("bivariate-deflate-horner-" ++ yLabel ++ nameSuffix) "CBivariate" "divByLinearY" fieldName - shape preset warmup hornerMeasured - (fun i ↦ (CBivariate.divByLinearY q (fAt i)).1) - checksumBiv (checksumIterations := checksumIterations) - let monic ← runTimed - ("bivariate-deflate-divbymonic-" ++ yLabel ++ nameSuffix) "CBivariate" "divByMonic" fieldName - shape preset warmup monicMeasured + let horner ← runTimedSpec + { name := ("bivariate-deflate-horner-" ++ yLabel ++ nameSuffix), + representation := "CBivariate", method := "divByLinearY", field := fieldName, + inputShape := shape, digestIterations := checksumIterations } + preset (fun i ↦ (CBivariate.divByLinearY q (fAt i)).1) checksumBiv + let monic ← runTimedSpec + { name := ("bivariate-deflate-divbymonic-" ++ yLabel ++ nameSuffix), + representation := "CBivariate", method := "divByMonic", field := fieldName, + inputShape := shape, digestIterations := checksumIterations } + preset (fun i ↦ (CPolynomial.divByMonic q (linearDivisor (fAt i)) : CBivariate (ZMod modulus))) - checksumBiv (checksumIterations := checksumIterations) + checksumBiv pure ({ groupKey := key, title := "Bivariate division by Y - f (" ++ fieldTitle ++ ", " ++ yLabel ++ ")", @@ -115,7 +99,6 @@ private def runFactorZMod (modulus : Nat) [Fact (Nat.Prime modulus)] /-- Run the KoalaBear comparison at one `Y`-degree size. -/ private def runFactorKoalaBear (key yLabel : String) (terms : Nat) - (largeHorner mediumHorner smallHorner largeMonic mediumMonic smallMonic : Nat) (preset : BenchPreset) (gen : StdGen) : IO (BenchGroup × StdGen) := do let (qTerms, gen) := (koalaBearArray terms false).run gen @@ -123,26 +106,24 @@ private def runFactorKoalaBear (key yLabel : String) (terms : Nat) let (perturb, gen) := (koalaBearArray 64 false).run gen let q := buildCBivariate qTerms let f := buildCPolynomial fTerms - let warmup := warmupIterations preset - let measured := measuredIterations preset - let hornerMeasured := preset.selectNat largeHorner mediumHorner smallHorner - let monicMeasured := preset.selectNat largeMonic mediumMonic smallMonic - let checksumIterations := groupChecksumIterations measured [hornerMeasured, monicMeasured] + let checksumIterations := digestPeriod perturb.size let shape := factorInputShape terms let fAt (i : Nat) : CPolynomial KoalaBear.Field := f + CPolynomial.C (perturb.getD (i % perturb.size) 0) let checksumBiv (p : CBivariate KoalaBear.Field) : Nat := checksumCPolynomial (checksumCPolynomial checksumKoalaBear) p - let horner ← runTimed - ("bivariate-deflate-horner-" ++ yLabel) "CBivariate" "divByLinearY" "KoalaBear.Field" - shape preset warmup hornerMeasured - (fun i ↦ (CBivariate.divByLinearY q (fAt i)).1) - checksumBiv (checksumIterations := checksumIterations) - let monic ← runTimed - ("bivariate-deflate-divbymonic-" ++ yLabel) "CBivariate" "divByMonic" "KoalaBear.Field" - shape preset warmup monicMeasured + let horner ← runTimedSpec + { name := ("bivariate-deflate-horner-" ++ yLabel), representation := "CBivariate", + method := "divByLinearY", field := "KoalaBear.Field", inputShape := shape, + digestIterations := checksumIterations } + preset (fun i ↦ (CBivariate.divByLinearY q (fAt i)).1) checksumBiv + let monic ← runTimedSpec + { name := ("bivariate-deflate-divbymonic-" ++ yLabel), representation := "CBivariate", + method := "divByMonic", field := "KoalaBear.Field", inputShape := shape, + digestIterations := checksumIterations } + preset (fun i ↦ (CPolynomial.divByMonic q (linearDivisor (fAt i)) : CBivariate KoalaBear.Field)) - checksumBiv (checksumIterations := checksumIterations) + checksumBiv pure ({ groupKey := key, title := "Bivariate division by Y - f (KoalaBear, " ++ yLabel ++ ")", @@ -152,37 +133,37 @@ private def runFactorKoalaBear (key yLabel : String) (terms : Nat) def factorTasks : List BenchTask := [ BenchTask.fromGroupRunner ⟨"bivariate-divlinear-koalabear-y8", "Bivariate division by Y - f (KoalaBear, yDeg<8)"⟩ - (runFactorKoalaBear "bivariate-divlinear-koalabear-y8" "y8" 64 800 120 10 350 50 8), + (runFactorKoalaBear "bivariate-divlinear-koalabear-y8" "y8" 64), BenchTask.fromGroupRunner ⟨"bivariate-divlinear-koalabear-y16", "Bivariate division by Y - f (KoalaBear, yDeg<16)"⟩ - (runFactorKoalaBear "bivariate-divlinear-koalabear-y16" "y16" 128 250 40 5 100 15 4), + (runFactorKoalaBear "bivariate-divlinear-koalabear-y16" "y16" 128), BenchTask.fromGroupRunner ⟨"bivariate-divlinear-koalabear-y32", "Bivariate division by Y - f (KoalaBear, yDeg<32)"⟩ - (runFactorKoalaBear "bivariate-divlinear-koalabear-y32" "y32" 256 60 10 3 20 4 2), + (runFactorKoalaBear "bivariate-divlinear-koalabear-y32" "y32" 256), BenchTask.fromGroupRunner ⟨"bivariate-divlinear-goldilocks-y8", "Bivariate division by Y - f (Goldilocks, yDeg<8)"⟩ (runFactorZMod Goldilocks.fieldSize "bivariate-divlinear-goldilocks-y8" "Goldilocks.Field" - "Goldilocks" "-goldilocks" "y8" 64 300 45 8 200 30 6), + "Goldilocks" "-goldilocks" "y8" 64), BenchTask.fromGroupRunner ⟨"bivariate-divlinear-goldilocks-y16", "Bivariate division by Y - f (Goldilocks, yDeg<16)"⟩ (runFactorZMod Goldilocks.fieldSize "bivariate-divlinear-goldilocks-y16" "Goldilocks.Field" - "Goldilocks" "-goldilocks" "y16" 128 120 20 5 50 10 4), + "Goldilocks" "-goldilocks" "y16" 128), BenchTask.fromGroupRunner ⟨"bivariate-divlinear-goldilocks-y32", "Bivariate division by Y - f (Goldilocks, yDeg<32)"⟩ (runFactorZMod Goldilocks.fieldSize "bivariate-divlinear-goldilocks-y32" "Goldilocks.Field" - "Goldilocks" "-goldilocks" "y32" 256 30 6 3 13 3 2), + "Goldilocks" "-goldilocks" "y32" 256), BenchTask.fromGroupRunner ⟨"bivariate-divlinear-bn254-y8", "Bivariate division by Y - f (BN254, yDeg<8)"⟩ (runFactorZMod BN254.scalarFieldSize "bivariate-divlinear-bn254-y8" "BN254.ScalarField" - "BN254" "-bn254" "y8" 64 250 40 6 150 25 5), + "BN254" "-bn254" "y8" 64), BenchTask.fromGroupRunner ⟨"bivariate-divlinear-bn254-y16", "Bivariate division by Y - f (BN254, yDeg<16)"⟩ (runFactorZMod BN254.scalarFieldSize "bivariate-divlinear-bn254-y16" "BN254.ScalarField" - "BN254" "-bn254" "y16" 128 60 12 4 40 8 3), + "BN254" "-bn254" "y16" 128), BenchTask.fromGroupRunner ⟨"bivariate-divlinear-bn254-y32", "Bivariate division by Y - f (BN254, yDeg<32)"⟩ (runFactorZMod BN254.scalarFieldSize "bivariate-divlinear-bn254-y32" "BN254.ScalarField" - "BN254" "-bn254" "y32" 256 15 4 2 10 3 2) + "BN254" "-bn254" "y32" 256) ] end CompPolyBench diff --git a/bench/CompPolyBench/Bivariate/GuruswamiSudan.lean b/bench/CompPolyBench/Bivariate/GuruswamiSudan.lean index e71854a6..9bea46b9 100644 --- a/bench/CompPolyBench/Bivariate/GuruswamiSudan.lean +++ b/bench/CompPolyBench/Bivariate/GuruswamiSudan.lean @@ -30,22 +30,19 @@ private def runGsInterpolationSystemKoala (preset : BenchPreset) (gen : StdGen) let fastMessage := cpolyOfArray (koalaBearFastArray coeffs) let points := gsSmallBenchmarkPoints message let fastPoints := gsSmallBenchmarkPoints fastMessage - let warmup := gsWarmupIterations preset - let measured := preset.selectNat 3 1 1 - let fastMeasured := preset.selectNat 10 2 1 - let checksumIterations := groupChecksumIterations measured [fastMeasured] - let row <- runTimed - "guruswami-sudan-interp-system" "DenseMatrix" - "Interpolation system construction" - "KoalaBear.Field" gsSmallInterpInputShape preset warmup measured - (fun _ ↦ interpolationMatrix points gsSmallParams) - (checksumDenseMatrix checksumKoalaBear) checksumIterations - let fastRow <- runTimed - "guruswami-sudan-interp-system-fast" "DenseMatrix" - "Interpolation system construction" - "KoalaBear.Fast.Field" gsSmallInterpInputShape preset warmup fastMeasured - (fun _ ↦ interpolationMatrix fastPoints gsSmallParams) - (checksumDenseMatrix checksumKoalaBearFast) checksumIterations + let checksumIterations := digestPeriod 1 + let row <- runTimedSpec + { name := "guruswami-sudan-interp-system", representation := "DenseMatrix", + method := "Interpolation system construction", field := "KoalaBear.Field", + inputShape := gsSmallInterpInputShape, digestIterations := checksumIterations } + preset (fun _ ↦ interpolationMatrix points gsSmallParams) + (checksumDenseMatrix checksumKoalaBear) + let fastRow <- runTimedSpec + { name := "guruswami-sudan-interp-system-fast", representation := "DenseMatrix", + method := "Interpolation system construction", field := "KoalaBear.Fast.Field", + inputShape := gsSmallInterpInputShape, digestIterations := checksumIterations } + preset (fun _ ↦ interpolationMatrix fastPoints gsSmallParams) + (checksumDenseMatrix checksumKoalaBearFast) pure ({ groupKey := "guruswami-sudan-interp-system-small-koalabear", title := "Guruswami-Sudan dense interpolation system construction, small (KoalaBear)", @@ -61,37 +58,31 @@ private def runGsInterpolationSolveKoala (preset : BenchPreset) (gen : StdGen) : let fastPoints := gsSmallBenchmarkPoints fastMessage let matrix := interpolationMatrix points gsSmallParams let fastMatrix := interpolationMatrix fastPoints gsSmallParams - let warmup := gsWarmupIterations preset - let measured := preset.selectNat 1 1 1 - let fastMeasured := preset.selectNat 2 1 1 - let inPlaceMeasured := preset.selectNat 8 2 1 - let fastInPlaceMeasured := preset.selectNat 16 3 1 - let checksumIterations := groupChecksumIterations measured - [fastMeasured, inPlaceMeasured, fastInPlaceMeasured] - let row <- runTimed - "guruswami-sudan-interp-solve-copying" "DenseMatrix" - "Homogeneous interpolation solve, copying" - "KoalaBear.Field" gsSmallInterpInputShape preset warmup measured - (fun _ ↦ DenseMatrix.homogeneousWitness matrix) - (checksumOptionArray checksumKoalaBear) checksumIterations - let inPlaceRow <- runTimed - "guruswami-sudan-interp-solve" "DenseMatrix" - "Homogeneous interpolation solve, in-place" - "KoalaBear.Field" gsSmallInterpInputShape preset warmup inPlaceMeasured - (fun _ ↦ DenseMatrix.homogeneousWitnessInPlace matrix) - (checksumOptionArray checksumKoalaBear) checksumIterations - let fastRow <- runTimed - "guruswami-sudan-interp-solve-copying-fast" "DenseMatrix" - "Homogeneous interpolation solve, copying" - "KoalaBear.Fast.Field" gsSmallInterpInputShape preset warmup fastMeasured - (fun _ ↦ DenseMatrix.homogeneousWitness fastMatrix) - (checksumOptionArray checksumKoalaBearFast) checksumIterations - let fastInPlaceRow <- runTimed - "guruswami-sudan-interp-solve-inplace-fast" "DenseMatrix" - "Homogeneous interpolation solve, in-place" - "KoalaBear.Fast.Field" gsSmallInterpInputShape preset warmup fastInPlaceMeasured - (fun _ ↦ DenseMatrix.homogeneousWitnessInPlace fastMatrix) - (checksumOptionArray checksumKoalaBearFast) checksumIterations + let checksumIterations := digestPeriod 1 + let row <- runTimedSpec + { name := "guruswami-sudan-interp-solve-copying", representation := "DenseMatrix", + method := "Homogeneous interpolation solve, copying", field := "KoalaBear.Field", + inputShape := gsSmallInterpInputShape, digestIterations := checksumIterations } + preset (fun _ ↦ DenseMatrix.homogeneousWitness matrix) + (checksumOptionArray checksumKoalaBear) + let inPlaceRow <- runTimedSpec + { name := "guruswami-sudan-interp-solve", representation := "DenseMatrix", + method := "Homogeneous interpolation solve, in-place", field := "KoalaBear.Field", + inputShape := gsSmallInterpInputShape, digestIterations := checksumIterations } + preset (fun _ ↦ DenseMatrix.homogeneousWitnessInPlace matrix) + (checksumOptionArray checksumKoalaBear) + let fastRow <- runTimedSpec + { name := "guruswami-sudan-interp-solve-copying-fast", representation := "DenseMatrix", + method := "Homogeneous interpolation solve, copying", field := "KoalaBear.Fast.Field", + inputShape := gsSmallInterpInputShape, digestIterations := checksumIterations } + preset (fun _ ↦ DenseMatrix.homogeneousWitness fastMatrix) + (checksumOptionArray checksumKoalaBearFast) + let fastInPlaceRow <- runTimedSpec + { name := "guruswami-sudan-interp-solve-inplace-fast", representation := "DenseMatrix", + method := "Homogeneous interpolation solve, in-place", field := "KoalaBear.Fast.Field", + inputShape := gsSmallInterpInputShape, digestIterations := checksumIterations } + preset (fun _ ↦ DenseMatrix.homogeneousWitnessInPlace fastMatrix) + (checksumOptionArray checksumKoalaBearFast) pure ({ groupKey := "guruswami-sudan-interp-solve-small-koalabear", title := "Guruswami-Sudan dense interpolation solving, small (KoalaBear)", @@ -105,68 +96,90 @@ private def runGsInterpolationSmallKoala (preset : BenchPreset) (gen : StdGen) : let fastMessage := cpolyOfArray (koalaBearFastArray coeffs) let points := gsSmallBenchmarkPoints message let fastPoints := gsSmallBenchmarkPoints fastMessage - let warmup := gsWarmupIterations preset - let denseMeasured := preset.selectNat 1 1 1 - let leeDirectMeasured := preset.selectNat 100 15 3 - let leeSubproductMeasured := preset.selectNat 90 13 3 - let fastDenseMeasured := preset.selectNat 2 1 1 - let fastLeeDirectMeasured := preset.selectNat 600 90 20 - let fastLeeSubproductMeasured := preset.selectNat 400 60 10 - let checksumIterations := groupChecksumIterations denseMeasured [ - leeDirectMeasured, leeSubproductMeasured, fastDenseMeasured, - fastLeeDirectMeasured, fastLeeSubproductMeasured - ] - let denseRow <- runTimed - "guruswami-sudan-interp-dense-small" "CBivariate" - "Dense linear" - "KoalaBear.Field" gsSmallInterpInputShape preset warmup denseMeasured + let checksumIterations := digestPeriod 1 + let denseRow <- runTimedSpec + { name := "guruswami-sudan-interp-dense-small", representation := "CBivariate", + method := "Dense linear", field := "KoalaBear.Field", inputShape := gsSmallInterpInputShape, + digestIterations := checksumIterations } + preset (fun _ ↦ koalaBearDenseInterpContext.interpolate points gsSmallParams) (checksumInterpolationValidityOption points gsSmallParams) - checksumIterations - let leeDirectRow <- runTimed - "guruswami-sudan-interp-lee-direct-small" "CBivariate" - "Lee-O'Sullivan direct" - "KoalaBear.Field" gsSmallInterpInputShape preset warmup leeDirectMeasured + let leeDirectRow <- runTimedSpec + { name := "guruswami-sudan-interp-lee-direct-small", representation := "CBivariate", + method := "Lee-O'Sullivan direct", field := "KoalaBear.Field", + inputShape := gsSmallInterpInputShape, digestIterations := checksumIterations } + preset (fun _ ↦ koalaBearLeeDirectInterpContext.interpolate points gsSmallParams) (checksumInterpolationValidityOption points gsSmallParams) - checksumIterations - let leeSubproductRow <- runTimed - "guruswami-sudan-interp-lee-subproduct-small" "CBivariate" - "Lee-O'Sullivan subproduct" - "KoalaBear.Field" gsSmallInterpInputShape preset warmup leeSubproductMeasured + let leeSubproductRow <- runTimedSpec + { name := "guruswami-sudan-interp-lee-subproduct-small", representation := "CBivariate", + method := "Lee-O'Sullivan subproduct", field := "KoalaBear.Field", + inputShape := gsSmallInterpInputShape, digestIterations := checksumIterations } + preset (fun _ ↦ koalaBearLeeSubproductInterpContext.interpolate points gsSmallParams) (checksumInterpolationValidityOption points gsSmallParams) - checksumIterations - let fastDenseRow <- runTimed - "guruswami-sudan-interp-dense-small-fast" "CBivariate" - "Dense linear" - "KoalaBear.Fast.Field" gsSmallInterpInputShape preset warmup fastDenseMeasured + let fastDenseRow <- runTimedSpec + { name := "guruswami-sudan-interp-dense-small-fast", representation := "CBivariate", + method := "Dense linear", field := "KoalaBear.Fast.Field", + inputShape := gsSmallInterpInputShape, digestIterations := checksumIterations } + preset (fun _ ↦ fastKoalaBearDenseInterpContext.interpolate fastPoints gsSmallParams) (checksumInterpolationValidityOption fastPoints gsSmallParams) - checksumIterations - let fastLeeDirectRow <- runTimed - "guruswami-sudan-interp-lee-direct-small-fast" "CBivariate" - "Lee-O'Sullivan direct" - "KoalaBear.Fast.Field" gsSmallInterpInputShape preset warmup fastLeeDirectMeasured + let fastLeeDirectRow <- runTimedSpec + { name := "guruswami-sudan-interp-lee-direct-small-fast", representation := "CBivariate", + method := "Lee-O'Sullivan direct", field := "KoalaBear.Fast.Field", + inputShape := gsSmallInterpInputShape, digestIterations := checksumIterations } + preset (fun _ ↦ fastKoalaBearLeeDirectInterpContext.interpolate fastPoints gsSmallParams) (checksumInterpolationValidityOption fastPoints gsSmallParams) - checksumIterations - let fastLeeSubproductRow <- runTimed - "guruswami-sudan-interp-lee-subproduct-small-fast" "CBivariate" - "Lee-O'Sullivan subproduct" - "KoalaBear.Fast.Field" gsSmallInterpInputShape preset warmup - fastLeeSubproductMeasured + let fastLeeSubproductRow <- runTimedSpec + { name := "guruswami-sudan-interp-lee-subproduct-small-fast", representation := "CBivariate", + method := "Lee-O'Sullivan subproduct", field := "KoalaBear.Fast.Field", + inputShape := gsSmallInterpInputShape, digestIterations := checksumIterations } + preset (fun _ ↦ fastKoalaBearLeeSubproductInterpContext.interpolate fastPoints gsSmallParams) (checksumInterpolationValidityOption fastPoints gsSmallParams) - checksumIterations + let approximantRow <- runTimedSpec + { name := "guruswami-sudan-interp-approximant-small", representation := "CBivariate", + method := "Approximant basis (PM-Basis)", field := "KoalaBear.Field", + inputShape := gsSmallInterpInputShape, digestIterations := checksumIterations } + preset + (fun _ ↦ koalaBearApproximantBasisSubproductInterpContext.interpolate points + gsSmallParams) + (checksumInterpolationValidityOption points gsSmallParams) + let hybridRow <- runTimedSpec + { name := "guruswami-sudan-interp-hybrid-small", representation := "CBivariate", + method := "Hybrid (budgeted Lee-O'Sullivan with approximant fallback)", + field := "KoalaBear.Field", inputShape := gsSmallInterpInputShape, + digestIterations := checksumIterations } + preset + (fun _ ↦ koalaBearHybridInterpContext.interpolate points gsSmallParams) + (checksumInterpolationValidityOption points gsSmallParams) + let fastApproximantRow <- runTimedSpec + { name := "guruswami-sudan-interp-approximant-small-fast", representation := "CBivariate", + method := "Approximant basis (PM-Basis)", field := "KoalaBear.Fast.Field", + inputShape := gsSmallInterpInputShape, digestIterations := checksumIterations } + preset + (fun _ ↦ fastKoalaBearApproximantBasisSubproductInterpContext.interpolate + fastPoints gsSmallParams) + (checksumInterpolationValidityOption fastPoints gsSmallParams) + let fastHybridRow <- runTimedSpec + { name := "guruswami-sudan-interp-hybrid-small-fast", representation := "CBivariate", + method := "Hybrid (budgeted Lee-O'Sullivan with approximant fallback)", + field := "KoalaBear.Fast.Field", inputShape := gsSmallInterpInputShape, + digestIterations := checksumIterations } + preset + (fun _ ↦ fastKoalaBearHybridInterpContext.interpolate fastPoints gsSmallParams) + (checksumInterpolationValidityOption fastPoints gsSmallParams) pure ({ groupKey := "guruswami-sudan-interp-small-koalabear", title := "Guruswami-Sudan interpolation, small (KoalaBear)", records := #[ denseRow, leeDirectRow, leeSubproductRow, - fastDenseRow, fastLeeDirectRow, fastLeeSubproductRow + fastDenseRow, fastLeeDirectRow, fastLeeSubproductRow, + approximantRow, hybridRow, fastApproximantRow, fastHybridRow ] }, gen) @@ -177,69 +190,72 @@ private def runGsRootKoala (preset : BenchPreset) (gen : StdGen) : let fastMessage := cpolyOfArray (koalaBearFastArray coeffs) let Q := nonlinearRootBenchmarkQ message let fastQ := nonlinearRootBenchmarkQ fastMessage - let warmup := gsWarmupIterations preset - let measured := preset.selectNat 20 3 1 - let nttFastMeasured := preset.selectNat 20 3 1 - let fastMeasured := preset.selectNat 80 10 2 - let fastNttFastMeasured := preset.selectNat 80 10 2 - let alekhnovichMeasured := preset.selectNat 10 2 1 - let alekhnovichNttFastMeasured := preset.selectNat 10 2 1 - let alekhnovichFastMeasured := preset.selectNat 30 5 1 - let alekhnovichFastNttFastMeasured := preset.selectNat 30 5 1 - let checksumIterations := groupChecksumIterations measured [ - nttFastMeasured, fastMeasured, fastNttFastMeasured, alekhnovichMeasured, - alekhnovichNttFastMeasured, alekhnovichFastMeasured, alekhnovichFastNttFastMeasured - ] - let row <- runTimed - "guruswami-sudan-root-roth" "CBivariate" - "Roth-Ruckenstein root finding with nonlinear field-root equations" - "KoalaBear.Field" gsRootShape preset warmup measured - (fun _ ↦ rothRuckensteinRootsYDegreeLt koalaFieldRoots Q gsMessageDegree) - checksumPolynomialArrayKoala checksumIterations - let nttFastRow <- runTimed - "guruswami-sudan-root-roth-nttfast" "CBivariate" - "Roth-Ruckenstein root finding with NTTFast field-root equations" - "KoalaBear.Field" gsRootShape preset warmup nttFastMeasured + let checksumIterations := digestPeriod 1 + let row <- runTimedSpec + { name := "guruswami-sudan-root-roth", representation := "CBivariate", + method := "Roth-Ruckenstein root finding with nonlinear field-root equations", + field := "KoalaBear.Field", inputShape := gsRootShape, + digestIterations := checksumIterations } + preset (fun _ ↦ rothRuckensteinRootsYDegreeLt koalaFieldRoots Q gsMessageDegree) + checksumPolynomialArrayKoala + let nttFastRow <- runTimedSpec + { name := "guruswami-sudan-root-roth-nttfast", representation := "CBivariate", + method := "Roth-Ruckenstein root finding with NTTFast field-root equations", + field := "KoalaBear.Field", inputShape := gsRootShape, + digestIterations := checksumIterations } + preset (fun _ ↦ rothRuckensteinRootsYDegreeLt koalaFieldRootsFast Q gsMessageDegree) - checksumPolynomialArrayKoala checksumIterations - let fastRow <- runTimed - "guruswami-sudan-root-roth-fast" "CBivariate" - "Roth-Ruckenstein root finding with nonlinear field-root equations" - "KoalaBear.Fast.Field" gsRootShape preset warmup fastMeasured + checksumPolynomialArrayKoala + let fastRow <- runTimedSpec + { name := "guruswami-sudan-root-roth-fast", representation := "CBivariate", + method := "Roth-Ruckenstein root finding with nonlinear field-root equations", + field := "KoalaBear.Fast.Field", inputShape := gsRootShape, + digestIterations := checksumIterations } + preset (fun _ ↦ rothRuckensteinRootsYDegreeLt koalaFastFieldRoots fastQ gsMessageDegree) - checksumPolynomialArrayKoalaFast checksumIterations - let fastNttFastRow <- runTimed - "guruswami-sudan-root-roth-fast-nttfast" "CBivariate" - "Roth-Ruckenstein root finding with NTTFast field-root equations" - "KoalaBear.Fast.Field" gsRootShape preset warmup fastNttFastMeasured + checksumPolynomialArrayKoalaFast + let fastNttFastRow <- runTimedSpec + { name := "guruswami-sudan-root-roth-fast-nttfast", representation := "CBivariate", + method := "Roth-Ruckenstein root finding with NTTFast field-root equations", + field := "KoalaBear.Fast.Field", inputShape := gsRootShape, + digestIterations := checksumIterations } + preset (fun _ ↦ rothRuckensteinRootsYDegreeLt koalaFastFieldRootsFast fastQ gsMessageDegree) - checksumPolynomialArrayKoalaFast checksumIterations - let alekhnovichRow <- runTimed - "guruswami-sudan-root-alekhnovich" "CBivariate" - "Alekhnovich root finding with nonlinear field-root equations" - "KoalaBear.Field" gsRootShape preset warmup alekhnovichMeasured + checksumPolynomialArrayKoalaFast + let alekhnovichRow <- runTimedSpec + { name := "guruswami-sudan-root-alekhnovich", representation := "CBivariate", + method := "Alekhnovich root finding with nonlinear field-root equations", + field := "KoalaBear.Field", inputShape := gsRootShape, + digestIterations := checksumIterations } + preset (fun _ ↦ alekhnovichRootsYDegreeLt koalaFieldRoots Q gsMessageDegree) - checksumPolynomialArrayKoala checksumIterations - let alekhnovichNttFastRow <- runTimed - "guruswami-sudan-root-alekhnovich-nttfast" "CBivariate" - "Alekhnovich root finding with NTTFast field-root equations" - "KoalaBear.Field" gsRootShape preset warmup alekhnovichNttFastMeasured + checksumPolynomialArrayKoala + let alekhnovichNttFastRow <- runTimedSpec + { name := "guruswami-sudan-root-alekhnovich-nttfast", representation := "CBivariate", + method := "Alekhnovich root finding with NTTFast field-root equations", + field := "KoalaBear.Field", inputShape := gsRootShape, + digestIterations := checksumIterations } + preset (fun _ ↦ alekhnovichRootsYDegreeLt koalaFieldRootsFast Q gsMessageDegree) - checksumPolynomialArrayKoala checksumIterations - let alekhnovichFastRow <- runTimed - "guruswami-sudan-root-alekhnovich-fast" "CBivariate" - "Alekhnovich root finding with nonlinear field-root equations" - "KoalaBear.Fast.Field" gsRootShape preset warmup alekhnovichFastMeasured + checksumPolynomialArrayKoala + let alekhnovichFastRow <- runTimedSpec + { name := "guruswami-sudan-root-alekhnovich-fast", representation := "CBivariate", + method := "Alekhnovich root finding with nonlinear field-root equations", + field := "KoalaBear.Fast.Field", inputShape := gsRootShape, + digestIterations := checksumIterations } + preset (fun _ ↦ alekhnovichRootsYDegreeLt koalaFastFieldRoots fastQ gsMessageDegree) - checksumPolynomialArrayKoalaFast checksumIterations - let alekhnovichFastNttFastRow <- runTimed - "guruswami-sudan-root-alekhnovich-fast-nttfast" "CBivariate" - "Alekhnovich root finding with NTTFast field-root equations" - "KoalaBear.Fast.Field" gsRootShape preset warmup alekhnovichFastNttFastMeasured + checksumPolynomialArrayKoalaFast + let alekhnovichFastNttFastRow <- runTimedSpec + { name := "guruswami-sudan-root-alekhnovich-fast-nttfast", representation := "CBivariate", + method := "Alekhnovich root finding with NTTFast field-root equations", + field := "KoalaBear.Fast.Field", inputShape := gsRootShape, + digestIterations := checksumIterations } + preset (fun _ ↦ alekhnovichRootsYDegreeLt koalaFastFieldRootsFast fastQ gsMessageDegree) - checksumPolynomialArrayKoalaFast checksumIterations + checksumPolynomialArrayKoalaFast pure ({ groupKey := "guruswami-sudan-root-koalabear", title := "Guruswami-Sudan root finding (KoalaBear)", @@ -255,7 +271,9 @@ private def runGsPackedFilterKoala (preset : BenchPreset) (gen : StdGen) : let points := codewordPoints message let fastPoints := codewordPoints fastMessage let radius : Nat := 0 - let candidateCount := preset.selectNat 128 64 32 + -- An input shape, not a budget: varying it by preset made this group's digest + -- preset-dependent no matter how the digest length was chosen. + let candidateCount : Nat := 128 let inputShape := s!"n={gsPointCount},k={gsMessageDegree},cand={candidateCount},r={radius}" let candidates : Array (CPolynomial KoalaBear.Field) := (List.range candidateCount).map (fun i ↦ @@ -263,22 +281,20 @@ private def runGsPackedFilterKoala (preset : BenchPreset) (gen : StdGen) : let fastCandidates : Array (CPolynomial KoalaBear.Fast.Field) := (List.range candidateCount).map (fun i ↦ fastMessage + CPolynomial.C ((i + 1 : Nat) : KoalaBear.Fast.Field)) |>.toArray - let warmup := gsWarmupIterations preset - let measured := preset.selectNat 20 3 1 - let fastMeasured := preset.selectNat 200 30 5 - let checksumIterations := groupChecksumIterations measured [fastMeasured] - let row <- runTimed - "guruswami-sudan-packed-filter" "CPolynomial" - "Packed distance filtering" - "KoalaBear.Field" inputShape preset warmup measured - (fun _ ↦ candidates.filter (passesCandidateDistance points radius)) - checksumPolynomialArrayKoala checksumIterations - let fastRow <- runTimed - "guruswami-sudan-packed-filter-fast" "CPolynomial" - "Packed distance filtering" - "KoalaBear.Fast.Field" inputShape preset warmup fastMeasured + let checksumIterations := digestPeriod 1 + let row <- runTimedSpec + { name := "guruswami-sudan-packed-filter", representation := "CPolynomial", + method := "Packed distance filtering", field := "KoalaBear.Field", inputShape := inputShape, + digestIterations := checksumIterations } + preset (fun _ ↦ candidates.filter (passesCandidateDistance points radius)) + checksumPolynomialArrayKoala + let fastRow <- runTimedSpec + { name := "guruswami-sudan-packed-filter-fast", representation := "CPolynomial", + method := "Packed distance filtering", field := "KoalaBear.Fast.Field", + inputShape := inputShape, digestIterations := checksumIterations } + preset (fun _ ↦ fastCandidates.filter (passesCandidateDistance fastPoints radius)) - checksumPolynomialArrayKoalaFast checksumIterations + checksumPolynomialArrayKoalaFast pure ({ groupKey := "guruswami-sudan-packed-filter-koalabear", title := "Guruswami-Sudan packed distance filtering (KoalaBear)", diff --git a/bench/CompPolyBench/Bivariate/GuruswamiSudan/Core.lean b/bench/CompPolyBench/Bivariate/GuruswamiSudan/Core.lean index 76165f1f..eb076108 100644 --- a/bench/CompPolyBench/Bivariate/GuruswamiSudan/Core.lean +++ b/bench/CompPolyBench/Bivariate/GuruswamiSudan/Core.lean @@ -31,116 +31,112 @@ def runGsCoreSmallKoala (preset : BenchPreset) (gen : StdGen) : alekhnovichRootContext KoalaBear.Field koalaBearFieldRootContext let fastAlekRootContext := alekhnovichRootContext KoalaBear.Fast.Field fastKoalaBearFieldRootContext - let warmup := gsWarmupIterations preset - let denseMeasured := preset.selectNat 1 1 1 - let leeDirectMeasured := preset.selectNat 100 15 3 - let leeSubproductMeasured := preset.selectNat 90 13 3 - let fastDenseMeasured := preset.selectNat 2 1 1 - let fastLeeDirectMeasured := preset.selectNat 600 90 20 - let fastLeeSubproductMeasured := preset.selectNat 400 60 10 - let alekDenseMeasured := denseMeasured - let alekLeeDirectMeasured := leeDirectMeasured - let alekLeeSubproductMeasured := leeSubproductMeasured - let fastAlekDenseMeasured := fastDenseMeasured - let fastAlekLeeDirectMeasured := fastLeeDirectMeasured - let fastAlekLeeSubproductMeasured := fastLeeSubproductMeasured - let checksumIterations := groupChecksumIterations denseMeasured [ - leeDirectMeasured, leeSubproductMeasured, fastDenseMeasured, - fastLeeDirectMeasured, fastLeeSubproductMeasured, alekDenseMeasured, - alekLeeDirectMeasured, alekLeeSubproductMeasured, fastAlekDenseMeasured, - fastAlekLeeDirectMeasured, fastAlekLeeSubproductMeasured - ] - let denseRow <- runTimed - "guruswami-sudan-core-dense-small" "CBivariate" - "Dense linear + RR roots" - "KoalaBear.Field" gsSmallInterpInputShape preset warmup denseMeasured + let checksumIterations := digestPeriod 1 + let denseRow <- runTimedSpec + { name := "guruswami-sudan-core-dense-small", representation := "CBivariate", + method := "Dense linear + RR roots", field := "KoalaBear.Field", + inputShape := gsSmallInterpInputShape, digestIterations := checksumIterations } + preset (fun _ ↦ gsCore points koalaBearDenseInterpContext koalaBearRothRootContext gsSmallParams) - checksumPolynomialArrayKoala checksumIterations - let denseAlekRow <- runTimed - "guruswami-sudan-core-dense-small-alekhnovich" "CBivariate" - "Dense linear + Alekhnovich roots" - "KoalaBear.Field" gsSmallInterpInputShape preset warmup alekDenseMeasured + checksumPolynomialArrayKoala + let denseAlekRow <- runTimedSpec + { name := "guruswami-sudan-core-dense-small-alekhnovich", representation := "CBivariate", + method := "Dense linear + Alekhnovich roots", field := "KoalaBear.Field", + inputShape := gsSmallInterpInputShape, digestIterations := checksumIterations } + preset (fun _ ↦ gsCore points koalaBearDenseInterpContext alekRootContext gsSmallParams) - checksumPolynomialArrayKoala checksumIterations - let leeDirectRow <- runTimed - "guruswami-sudan-core-lee-direct-small" "CBivariate" - "Lee-O'Sullivan direct + RR roots" - "KoalaBear.Field" gsSmallInterpInputShape preset warmup leeDirectMeasured + checksumPolynomialArrayKoala + let leeDirectRow <- runTimedSpec + { name := "guruswami-sudan-core-lee-direct-small", representation := "CBivariate", + method := "Lee-O'Sullivan direct + RR roots", field := "KoalaBear.Field", + inputShape := gsSmallInterpInputShape, digestIterations := checksumIterations } + preset (fun _ ↦ gsCore points koalaBearLeeDirectInterpContext koalaBearRothRootContext gsSmallParams) - checksumPolynomialArrayKoala checksumIterations - let leeDirectAlekRow <- runTimed - "guruswami-sudan-core-lee-direct-small-alekhnovich" "CBivariate" - "Lee-O'Sullivan direct + Alekhnovich roots" - "KoalaBear.Field" gsSmallInterpInputShape preset warmup alekLeeDirectMeasured + checksumPolynomialArrayKoala + let leeDirectAlekRow <- runTimedSpec + { name := "guruswami-sudan-core-lee-direct-small-alekhnovich", representation := "CBivariate", + method := "Lee-O'Sullivan direct + Alekhnovich roots", field := "KoalaBear.Field", + inputShape := gsSmallInterpInputShape, digestIterations := checksumIterations } + preset (fun _ ↦ gsCore points koalaBearLeeDirectInterpContext alekRootContext gsSmallParams) - checksumPolynomialArrayKoala checksumIterations - let leeSubproductRow <- runTimed - "guruswami-sudan-core-lee-subproduct-small" "CBivariate" - "Lee-O'Sullivan subproduct + RR roots" - "KoalaBear.Field" gsSmallInterpInputShape preset warmup leeSubproductMeasured + checksumPolynomialArrayKoala + let leeSubproductRow <- runTimedSpec + { name := "guruswami-sudan-core-lee-subproduct-small", representation := "CBivariate", + method := "Lee-O'Sullivan subproduct + RR roots", field := "KoalaBear.Field", + inputShape := gsSmallInterpInputShape, digestIterations := checksumIterations } + preset (fun _ ↦ gsCore points koalaBearLeeSubproductInterpContext koalaBearRothRootContext gsSmallParams) - checksumPolynomialArrayKoala checksumIterations - let leeSubproductAlekRow <- runTimed - "guruswami-sudan-core-lee-subproduct-small-alekhnovich" "CBivariate" - "Lee-O'Sullivan subproduct + Alekhnovich roots" - "KoalaBear.Field" gsSmallInterpInputShape preset warmup alekLeeSubproductMeasured + checksumPolynomialArrayKoala + let leeSubproductAlekRow <- runTimedSpec + { name := "guruswami-sudan-core-lee-subproduct-small-alekhnovich", + representation := "CBivariate", method := "Lee-O'Sullivan subproduct + Alekhnovich roots", + field := "KoalaBear.Field", inputShape := gsSmallInterpInputShape, + digestIterations := checksumIterations } + preset (fun _ ↦ gsCore points koalaBearLeeSubproductInterpContext alekRootContext gsSmallParams) - checksumPolynomialArrayKoala checksumIterations - let fastDenseRow <- runTimed - "guruswami-sudan-core-dense-small-fast" "CBivariate" - "Dense linear + RR roots" - "KoalaBear.Fast.Field" gsSmallInterpInputShape preset warmup fastDenseMeasured + checksumPolynomialArrayKoala + let fastDenseRow <- runTimedSpec + { name := "guruswami-sudan-core-dense-small-fast", representation := "CBivariate", + method := "Dense linear + RR roots", field := "KoalaBear.Fast.Field", + inputShape := gsSmallInterpInputShape, digestIterations := checksumIterations } + preset (fun _ ↦ gsCore fastPoints fastKoalaBearDenseInterpContext fastKoalaBearRothRootContext gsSmallParams) - checksumPolynomialArrayKoalaFast checksumIterations - let fastDenseAlekRow <- runTimed - "guruswami-sudan-core-dense-small-alekhnovich-fast" "CBivariate" - "Dense linear + Alekhnovich roots" - "KoalaBear.Fast.Field" gsSmallInterpInputShape preset warmup fastAlekDenseMeasured + checksumPolynomialArrayKoalaFast + let fastDenseAlekRow <- runTimedSpec + { name := "guruswami-sudan-core-dense-small-alekhnovich-fast", representation := "CBivariate", + method := "Dense linear + Alekhnovich roots", field := "KoalaBear.Fast.Field", + inputShape := gsSmallInterpInputShape, digestIterations := checksumIterations } + preset (fun _ ↦ gsCore fastPoints fastKoalaBearDenseInterpContext fastAlekRootContext gsSmallParams) - checksumPolynomialArrayKoalaFast checksumIterations - let fastLeeDirectRow <- runTimed - "guruswami-sudan-core-lee-direct-small-fast" "CBivariate" - "Lee-O'Sullivan direct + RR roots" - "KoalaBear.Fast.Field" gsSmallInterpInputShape preset warmup fastLeeDirectMeasured + checksumPolynomialArrayKoalaFast + let fastLeeDirectRow <- runTimedSpec + { name := "guruswami-sudan-core-lee-direct-small-fast", representation := "CBivariate", + method := "Lee-O'Sullivan direct + RR roots", field := "KoalaBear.Fast.Field", + inputShape := gsSmallInterpInputShape, digestIterations := checksumIterations } + preset (fun _ ↦ gsCore fastPoints fastKoalaBearLeeDirectInterpContext fastKoalaBearRothRootContext gsSmallParams) - checksumPolynomialArrayKoalaFast checksumIterations - let fastLeeDirectAlekRow <- runTimed - "guruswami-sudan-core-lee-direct-small-alekhnovich-fast" "CBivariate" - "Lee-O'Sullivan direct + Alekhnovich roots" - "KoalaBear.Fast.Field" gsSmallInterpInputShape preset warmup fastAlekLeeDirectMeasured + checksumPolynomialArrayKoalaFast + let fastLeeDirectAlekRow <- runTimedSpec + { name := "guruswami-sudan-core-lee-direct-small-alekhnovich-fast", + representation := "CBivariate", method := "Lee-O'Sullivan direct + Alekhnovich roots", + field := "KoalaBear.Fast.Field", inputShape := gsSmallInterpInputShape, + digestIterations := checksumIterations } + preset (fun _ ↦ gsCore fastPoints fastKoalaBearLeeDirectInterpContext fastAlekRootContext gsSmallParams) - checksumPolynomialArrayKoalaFast checksumIterations - let fastLeeSubproductRow <- runTimed - "guruswami-sudan-core-lee-subproduct-small-fast" "CBivariate" - "Lee-O'Sullivan subproduct + RR roots" - "KoalaBear.Fast.Field" gsSmallInterpInputShape preset warmup fastLeeSubproductMeasured + checksumPolynomialArrayKoalaFast + let fastLeeSubproductRow <- runTimedSpec + { name := "guruswami-sudan-core-lee-subproduct-small-fast", representation := "CBivariate", + method := "Lee-O'Sullivan subproduct + RR roots", field := "KoalaBear.Fast.Field", + inputShape := gsSmallInterpInputShape, digestIterations := checksumIterations } + preset (fun _ ↦ gsCore fastPoints fastKoalaBearLeeSubproductInterpContext fastKoalaBearRothRootContext gsSmallParams) - checksumPolynomialArrayKoalaFast checksumIterations - let fastLeeSubproductAlekRow <- runTimed - "guruswami-sudan-core-lee-subproduct-small-alekhnovich-fast" "CBivariate" - "Lee-O'Sullivan subproduct + Alekhnovich roots" - "KoalaBear.Fast.Field" gsSmallInterpInputShape preset warmup - fastAlekLeeSubproductMeasured + checksumPolynomialArrayKoalaFast + let fastLeeSubproductAlekRow <- runTimedSpec + { name := "guruswami-sudan-core-lee-subproduct-small-alekhnovich-fast", + representation := "CBivariate", method := "Lee-O'Sullivan subproduct + Alekhnovich roots", + field := "KoalaBear.Fast.Field", inputShape := gsSmallInterpInputShape, + digestIterations := checksumIterations } + preset (fun _ ↦ gsCore fastPoints fastKoalaBearLeeSubproductInterpContext fastAlekRootContext gsSmallParams) - checksumPolynomialArrayKoalaFast checksumIterations + checksumPolynomialArrayKoalaFast pure ({ groupKey := "guruswami-sudan-core-small-koalabear", title := "Guruswami-Sudan full core, small (KoalaBear)", @@ -163,122 +159,126 @@ def runGsFilteredCoreSmallKoala (preset : BenchPreset) (gen : StdGen) : alekhnovichRootContext KoalaBear.Field koalaBearFieldRootContext let fastAlekRootContext := alekhnovichRootContext KoalaBear.Fast.Field fastKoalaBearFieldRootContext - let warmup := gsWarmupIterations preset - let denseMeasured := preset.selectNat 1 1 1 - let leeDirectMeasured := preset.selectNat 100 15 3 - let leeSubproductMeasured := preset.selectNat 90 13 3 - let fastDenseMeasured := preset.selectNat 2 1 1 - let fastLeeDirectMeasured := preset.selectNat 600 90 20 - let fastLeeSubproductMeasured := preset.selectNat 400 60 10 - let alekDenseMeasured := denseMeasured - let alekLeeDirectMeasured := leeDirectMeasured - let alekLeeSubproductMeasured := leeSubproductMeasured - let fastAlekDenseMeasured := fastDenseMeasured - let fastAlekLeeDirectMeasured := fastLeeDirectMeasured - let fastAlekLeeSubproductMeasured := fastLeeSubproductMeasured - let checksumIterations := groupChecksumIterations denseMeasured [ - leeDirectMeasured, leeSubproductMeasured, fastDenseMeasured, - fastLeeDirectMeasured, fastLeeSubproductMeasured, alekDenseMeasured, - alekLeeDirectMeasured, alekLeeSubproductMeasured, fastAlekDenseMeasured, - fastAlekLeeDirectMeasured, fastAlekLeeSubproductMeasured - ] - let denseRow <- runTimed - "guruswami-sudan-filtered-core-dense-small" "CBivariate" - "Dense linear + RR roots + filter" - "KoalaBear.Field" gsSmallFilteredShape preset warmup denseMeasured + let checksumIterations := digestPeriod 1 + let denseRow <- runTimedSpec + { name := "guruswami-sudan-filtered-core-dense-small", representation := "CBivariate", + method := "Dense linear + RR roots + filter", field := "KoalaBear.Field", + inputShape := gsSmallFilteredShape, digestIterations := checksumIterations } + preset (fun _ ↦ gsFilteredCore points koalaBearDenseInterpContext koalaBearRothRootContext gsSmallParams 0) - checksumPolynomialArrayKoala checksumIterations - let denseAlekRow <- runTimed - "guruswami-sudan-filtered-core-dense-small-alekhnovich" "CBivariate" - "Dense linear + Alekhnovich roots + filter" - "KoalaBear.Field" gsSmallFilteredShape preset warmup alekDenseMeasured + checksumPolynomialArrayKoala + let denseAlekRow <- runTimedSpec + { name := "guruswami-sudan-filtered-core-dense-small-alekhnovich", + representation := "CBivariate", method := "Dense linear + Alekhnovich roots + filter", + field := "KoalaBear.Field", inputShape := gsSmallFilteredShape, + digestIterations := checksumIterations } + preset (fun _ ↦ gsFilteredCore points koalaBearDenseInterpContext alekRootContext gsSmallParams 0) - checksumPolynomialArrayKoala checksumIterations - let leeDirectRow <- runTimed - "guruswami-sudan-filtered-core-lee-direct-small" "CBivariate" - "Lee-O'Sullivan direct + RR roots + filter" - "KoalaBear.Field" gsSmallFilteredShape preset warmup leeDirectMeasured + checksumPolynomialArrayKoala + let leeDirectRow <- runTimedSpec + { name := "guruswami-sudan-filtered-core-lee-direct-small", representation := "CBivariate", + method := "Lee-O'Sullivan direct + RR roots + filter", field := "KoalaBear.Field", + inputShape := gsSmallFilteredShape, digestIterations := checksumIterations } + preset (fun _ ↦ gsFilteredCore points koalaBearLeeDirectInterpContext koalaBearRothRootContext gsSmallParams 0) - checksumPolynomialArrayKoala checksumIterations - let leeDirectAlekRow <- runTimed - "guruswami-sudan-filtered-core-lee-direct-small-alekhnovich" "CBivariate" - "Lee-O'Sullivan direct + Alekhnovich roots + filter" - "KoalaBear.Field" gsSmallFilteredShape preset warmup alekLeeDirectMeasured + checksumPolynomialArrayKoala + let leeDirectAlekRow <- runTimedSpec + { name := "guruswami-sudan-filtered-core-lee-direct-small-alekhnovich", + representation := "CBivariate", + method := "Lee-O'Sullivan direct + Alekhnovich roots + filter", field := "KoalaBear.Field", + inputShape := gsSmallFilteredShape, digestIterations := checksumIterations } + preset (fun _ ↦ gsFilteredCore points koalaBearLeeDirectInterpContext alekRootContext gsSmallParams 0) - checksumPolynomialArrayKoala checksumIterations - let leeSubproductRow <- runTimed - "guruswami-sudan-filtered-core-lee-subproduct-small" "CBivariate" - "Lee-O'Sullivan subproduct + RR roots + filter" - "KoalaBear.Field" gsSmallFilteredShape preset warmup leeSubproductMeasured + checksumPolynomialArrayKoala + let leeSubproductRow <- runTimedSpec + { name := "guruswami-sudan-filtered-core-lee-subproduct-small", representation := "CBivariate", + method := "Lee-O'Sullivan subproduct + RR roots + filter", field := "KoalaBear.Field", + inputShape := gsSmallFilteredShape, digestIterations := checksumIterations } + preset (fun _ ↦ gsFilteredCore points koalaBearLeeSubproductInterpContext koalaBearRothRootContext gsSmallParams 0) - checksumPolynomialArrayKoala checksumIterations - let leeSubproductAlekRow <- runTimed - "guruswami-sudan-filtered-core-lee-subproduct-small-alekhnovich" "CBivariate" - "Lee-O'Sullivan subproduct + Alekhnovich roots + filter" - "KoalaBear.Field" gsSmallFilteredShape preset warmup alekLeeSubproductMeasured + checksumPolynomialArrayKoala + let leeSubproductAlekRow <- runTimedSpec + { name := "guruswami-sudan-filtered-core-lee-subproduct-small-alekhnovich", + representation := "CBivariate", + method := "Lee-O'Sullivan subproduct + Alekhnovich roots + filter", + field := "KoalaBear.Field", inputShape := gsSmallFilteredShape, + digestIterations := checksumIterations } + preset (fun _ ↦ gsFilteredCore points koalaBearLeeSubproductInterpContext alekRootContext gsSmallParams 0) - checksumPolynomialArrayKoala checksumIterations - let fastDenseRow <- runTimed - "guruswami-sudan-filtered-core-dense-small-fast" "CBivariate" - "Dense linear + RR roots + filter" - "KoalaBear.Fast.Field" gsSmallFilteredShape preset warmup fastDenseMeasured + checksumPolynomialArrayKoala + let fastDenseRow <- runTimedSpec + { name := "guruswami-sudan-filtered-core-dense-small-fast", representation := "CBivariate", + method := "Dense linear + RR roots + filter", field := "KoalaBear.Fast.Field", + inputShape := gsSmallFilteredShape, digestIterations := checksumIterations } + preset (fun _ ↦ gsFilteredCore fastPoints fastKoalaBearDenseInterpContext fastKoalaBearRothRootContext gsSmallParams 0) - checksumPolynomialArrayKoalaFast checksumIterations - let fastDenseAlekRow <- runTimed - "guruswami-sudan-filtered-core-dense-small-alekhnovich-fast" "CBivariate" - "Dense linear + Alekhnovich roots + filter" - "KoalaBear.Fast.Field" gsSmallFilteredShape preset warmup fastAlekDenseMeasured + checksumPolynomialArrayKoalaFast + let fastDenseAlekRow <- runTimedSpec + { name := "guruswami-sudan-filtered-core-dense-small-alekhnovich-fast", + representation := "CBivariate", method := "Dense linear + Alekhnovich roots + filter", + field := "KoalaBear.Fast.Field", inputShape := gsSmallFilteredShape, + digestIterations := checksumIterations } + preset (fun _ ↦ gsFilteredCore fastPoints fastKoalaBearDenseInterpContext fastAlekRootContext gsSmallParams 0) - checksumPolynomialArrayKoalaFast checksumIterations - let fastLeeDirectRow <- runTimed - "guruswami-sudan-filtered-core-lee-direct-small-fast" "CBivariate" - "Lee-O'Sullivan direct + RR roots + filter" - "KoalaBear.Fast.Field" gsSmallFilteredShape preset warmup fastLeeDirectMeasured + checksumPolynomialArrayKoalaFast + let fastLeeDirectRow <- runTimedSpec + { name := "guruswami-sudan-filtered-core-lee-direct-small-fast", + representation := "CBivariate", method := "Lee-O'Sullivan direct + RR roots + filter", + field := "KoalaBear.Fast.Field", inputShape := gsSmallFilteredShape, + digestIterations := checksumIterations } + preset (fun _ ↦ gsFilteredCore fastPoints fastKoalaBearLeeDirectInterpContext fastKoalaBearRothRootContext gsSmallParams 0) - checksumPolynomialArrayKoalaFast checksumIterations - let fastLeeDirectAlekRow <- runTimed - "guruswami-sudan-filtered-core-lee-direct-small-alekhnovich-fast" "CBivariate" - "Lee-O'Sullivan direct + Alekhnovich roots + filter" - "KoalaBear.Fast.Field" gsSmallFilteredShape preset warmup fastAlekLeeDirectMeasured + checksumPolynomialArrayKoalaFast + let fastLeeDirectAlekRow <- runTimedSpec + { name := "guruswami-sudan-filtered-core-lee-direct-small-alekhnovich-fast", + representation := "CBivariate", + method := "Lee-O'Sullivan direct + Alekhnovich roots + filter", + field := "KoalaBear.Fast.Field", inputShape := gsSmallFilteredShape, + digestIterations := checksumIterations } + preset (fun _ ↦ gsFilteredCore fastPoints fastKoalaBearLeeDirectInterpContext fastAlekRootContext gsSmallParams 0) - checksumPolynomialArrayKoalaFast checksumIterations - let fastLeeSubproductRow <- runTimed - "guruswami-sudan-filtered-core-lee-subproduct-small-fast" "CBivariate" - "Lee-O'Sullivan subproduct + RR roots + filter" - "KoalaBear.Fast.Field" gsSmallFilteredShape preset warmup fastLeeSubproductMeasured + checksumPolynomialArrayKoalaFast + let fastLeeSubproductRow <- runTimedSpec + { name := "guruswami-sudan-filtered-core-lee-subproduct-small-fast", + representation := "CBivariate", method := "Lee-O'Sullivan subproduct + RR roots + filter", + field := "KoalaBear.Fast.Field", inputShape := gsSmallFilteredShape, + digestIterations := checksumIterations } + preset (fun _ ↦ gsFilteredCore fastPoints fastKoalaBearLeeSubproductInterpContext fastKoalaBearRothRootContext gsSmallParams 0) - checksumPolynomialArrayKoalaFast checksumIterations - let fastLeeSubproductAlekRow <- runTimed - "guruswami-sudan-filtered-core-lee-subproduct-small-alekhnovich-fast" "CBivariate" - "Lee-O'Sullivan subproduct + Alekhnovich roots + filter" - "KoalaBear.Fast.Field" gsSmallFilteredShape preset warmup - fastAlekLeeSubproductMeasured + checksumPolynomialArrayKoalaFast + let fastLeeSubproductAlekRow <- runTimedSpec + { name := "guruswami-sudan-filtered-core-lee-subproduct-small-alekhnovich-fast", + representation := "CBivariate", + method := "Lee-O'Sullivan subproduct + Alekhnovich roots + filter", + field := "KoalaBear.Fast.Field", inputShape := gsSmallFilteredShape, + digestIterations := checksumIterations } + preset (fun _ ↦ gsFilteredCore fastPoints fastKoalaBearLeeSubproductInterpContext fastAlekRootContext gsSmallParams 0) - checksumPolynomialArrayKoalaFast checksumIterations + checksumPolynomialArrayKoalaFast pure ({ groupKey := "guruswami-sudan-filtered-core-small-koalabear", title := "Guruswami-Sudan filtered core, small (KoalaBear)", diff --git a/bench/CompPolyBench/Bivariate/GuruswamiSudan/ReceivedWord.lean b/bench/CompPolyBench/Bivariate/GuruswamiSudan/ReceivedWord.lean index b4916322..55ac3fc1 100644 --- a/bench/CompPolyBench/Bivariate/GuruswamiSudan/ReceivedWord.lean +++ b/bench/CompPolyBench/Bivariate/GuruswamiSudan/ReceivedWord.lean @@ -80,67 +80,58 @@ def guruswamiSudanReceivedWordGroupInfos : List BenchGroupInfo := [ private def runGsInterpolationNonCodewordSmallKoala (preset : BenchPreset) (gen : StdGen) : IO (Prod BenchGroup StdGen) := do let (inputs, gen) := perturbedSmallInputs gen - let warmup := gsWarmupIterations preset - let denseMeasured := preset.selectNat 1 1 1 - let leeDirectMeasured := preset.selectNat 15 2 1 - let leeSubproductMeasured := preset.selectNat 15 2 1 - let fastDenseMeasured := preset.selectNat 2 1 1 - let fastLeeDirectMeasured := preset.selectNat 80 11 2 - let fastLeeSubproductMeasured := preset.selectNat 70 10 2 - let checksumIterations := groupChecksumIterations denseMeasured [ - leeDirectMeasured, leeSubproductMeasured, fastDenseMeasured, - fastLeeDirectMeasured, fastLeeSubproductMeasured - ] - let denseRow <- runTimed - "guruswami-sudan-interp-dense-noncodeword-small" "CBivariate" - "Dense linear" - "KoalaBear.Field" gsNonCodewordSmallInputShape preset warmup denseMeasured + let checksumIterations := digestPeriod 1 + let denseRow <- runTimedSpec + { name := "guruswami-sudan-interp-dense-noncodeword-small", representation := "CBivariate", + method := "Dense linear", field := "KoalaBear.Field", + inputShape := gsNonCodewordSmallInputShape, digestIterations := checksumIterations } + preset (fun _ ↦ koalaBearDenseInterpContext.interpolate inputs.points gsSmallParams) (checksumInterpolationValidityOption inputs.points gsSmallParams) - checksumIterations - let leeDirectRow <- runTimed - "guruswami-sudan-interp-lee-direct-noncodeword-small" "CBivariate" - "Lee-O'Sullivan direct" - "KoalaBear.Field" gsNonCodewordSmallInputShape preset warmup leeDirectMeasured + let leeDirectRow <- runTimedSpec + { name := "guruswami-sudan-interp-lee-direct-noncodeword-small", + representation := "CBivariate", method := "Lee-O'Sullivan direct", + field := "KoalaBear.Field", inputShape := gsNonCodewordSmallInputShape, + digestIterations := checksumIterations } + preset (fun _ ↦ koalaBearLeeDirectInterpContext.interpolate inputs.points gsSmallParams) (checksumInterpolationValidityOption inputs.points gsSmallParams) - checksumIterations - let leeSubproductRow <- runTimed - "guruswami-sudan-interp-lee-subproduct-noncodeword-small" "CBivariate" - "Lee-O'Sullivan subproduct" - "KoalaBear.Field" gsNonCodewordSmallInputShape preset warmup leeSubproductMeasured + let leeSubproductRow <- runTimedSpec + { name := "guruswami-sudan-interp-lee-subproduct-noncodeword-small", + representation := "CBivariate", method := "Lee-O'Sullivan subproduct", + field := "KoalaBear.Field", inputShape := gsNonCodewordSmallInputShape, + digestIterations := checksumIterations } + preset (fun _ ↦ koalaBearLeeSubproductInterpContext.interpolate inputs.points gsSmallParams) (checksumInterpolationValidityOption inputs.points gsSmallParams) - checksumIterations - let fastDenseRow <- runTimed - "guruswami-sudan-interp-dense-noncodeword-small-fast" "CBivariate" - "Dense linear" - "KoalaBear.Fast.Field" gsNonCodewordSmallInputShape preset warmup - fastDenseMeasured + let fastDenseRow <- runTimedSpec + { name := "guruswami-sudan-interp-dense-noncodeword-small-fast", + representation := "CBivariate", method := "Dense linear", field := "KoalaBear.Fast.Field", + inputShape := gsNonCodewordSmallInputShape, digestIterations := checksumIterations } + preset (fun _ ↦ fastKoalaBearDenseInterpContext.interpolate inputs.fastPoints gsSmallParams) (checksumInterpolationValidityOption inputs.fastPoints gsSmallParams) - checksumIterations - let fastLeeDirectRow <- runTimed - "guruswami-sudan-interp-lee-direct-noncodeword-small-fast" "CBivariate" - "Lee-O'Sullivan direct" - "KoalaBear.Fast.Field" gsNonCodewordSmallInputShape preset warmup - fastLeeDirectMeasured + let fastLeeDirectRow <- runTimedSpec + { name := "guruswami-sudan-interp-lee-direct-noncodeword-small-fast", + representation := "CBivariate", method := "Lee-O'Sullivan direct", + field := "KoalaBear.Fast.Field", inputShape := gsNonCodewordSmallInputShape, + digestIterations := checksumIterations } + preset (fun _ ↦ fastKoalaBearLeeDirectInterpContext.interpolate inputs.fastPoints gsSmallParams) (checksumInterpolationValidityOption inputs.fastPoints gsSmallParams) - checksumIterations - let fastLeeSubproductRow <- runTimed - "guruswami-sudan-interp-lee-subproduct-noncodeword-small-fast" "CBivariate" - "Lee-O'Sullivan subproduct" - "KoalaBear.Fast.Field" gsNonCodewordSmallInputShape preset warmup - fastLeeSubproductMeasured + let fastLeeSubproductRow <- runTimedSpec + { name := "guruswami-sudan-interp-lee-subproduct-noncodeword-small-fast", + representation := "CBivariate", method := "Lee-O'Sullivan subproduct", + field := "KoalaBear.Fast.Field", inputShape := gsNonCodewordSmallInputShape, + digestIterations := checksumIterations } + preset (fun _ ↦ fastKoalaBearLeeSubproductInterpContext.interpolate inputs.fastPoints gsSmallParams) (checksumInterpolationValidityOption inputs.fastPoints gsSmallParams) - checksumIterations pure ({ groupKey := "guruswami-sudan-interp-noncodeword-small-koalabear", title := "Guruswami-Sudan interpolation on perturbed received word, small (KoalaBear)", @@ -157,132 +148,130 @@ private def runGsCoreNonCodewordSmallKoala (preset : BenchPreset) alekhnovichRootContext KoalaBear.Field koalaBearFieldRootContext let fastAlekRootContext := alekhnovichRootContext KoalaBear.Fast.Field fastKoalaBearFieldRootContext - let warmup := gsWarmupIterations preset - let denseMeasured := preset.selectNat 1 1 1 - let leeDirectMeasured := preset.selectNat 15 2 1 - let leeSubproductMeasured := preset.selectNat 15 2 1 - let fastDenseMeasured := preset.selectNat 2 1 1 - let fastLeeDirectMeasured := preset.selectNat 80 11 2 - let fastLeeSubproductMeasured := preset.selectNat 70 10 2 - let alekDenseMeasured := denseMeasured - let alekLeeDirectMeasured := leeDirectMeasured - let alekLeeSubproductMeasured := leeSubproductMeasured - let fastAlekDenseMeasured := fastDenseMeasured - let fastAlekLeeDirectMeasured := fastLeeDirectMeasured - let fastAlekLeeSubproductMeasured := fastLeeSubproductMeasured - let checksumIterations := groupChecksumIterations denseMeasured [ - leeDirectMeasured, leeSubproductMeasured, fastDenseMeasured, - fastLeeDirectMeasured, fastLeeSubproductMeasured, alekDenseMeasured, - alekLeeDirectMeasured, alekLeeSubproductMeasured, fastAlekDenseMeasured, - fastAlekLeeDirectMeasured, fastAlekLeeSubproductMeasured - ] - let denseRow <- runTimed - "guruswami-sudan-core-dense-noncodeword-small" "CBivariate" - "Dense linear + RR roots" - "KoalaBear.Field" gsNonCodewordSmallInputShape preset warmup denseMeasured + let checksumIterations := digestPeriod 1 + let denseRow <- runTimedSpec + { name := "guruswami-sudan-core-dense-noncodeword-small", representation := "CBivariate", + method := "Dense linear + RR roots", field := "KoalaBear.Field", + inputShape := gsNonCodewordSmallInputShape, digestIterations := checksumIterations } + preset (fun _ ↦ (gsCore inputs.points koalaBearDenseInterpContext koalaBearRothRootContext gsSmallParams).filter (passesCandidateDistance inputs.points gsNonCodewordSmallErrors)) - checksumPolynomialArrayKoala checksumIterations - let denseAlekRow <- runTimed - "guruswami-sudan-core-dense-noncodeword-small-alekhnovich" "CBivariate" - "Dense linear + Alekhnovich roots" - "KoalaBear.Field" gsNonCodewordSmallInputShape preset warmup alekDenseMeasured + checksumPolynomialArrayKoala + let denseAlekRow <- runTimedSpec + { name := "guruswami-sudan-core-dense-noncodeword-small-alekhnovich", + representation := "CBivariate", method := "Dense linear + Alekhnovich roots", + field := "KoalaBear.Field", inputShape := gsNonCodewordSmallInputShape, + digestIterations := checksumIterations } + preset (fun _ ↦ (gsCore inputs.points koalaBearDenseInterpContext alekRootContext gsSmallParams).filter (passesCandidateDistance inputs.points gsNonCodewordSmallErrors)) - checksumPolynomialArrayKoala checksumIterations - let leeDirectRow <- runTimed - "guruswami-sudan-core-lee-direct-noncodeword-small" "CBivariate" - "Lee-O'Sullivan direct + RR roots" - "KoalaBear.Field" gsNonCodewordSmallInputShape preset warmup leeDirectMeasured + checksumPolynomialArrayKoala + let leeDirectRow <- runTimedSpec + { name := "guruswami-sudan-core-lee-direct-noncodeword-small", representation := "CBivariate", + method := "Lee-O'Sullivan direct + RR roots", field := "KoalaBear.Field", + inputShape := gsNonCodewordSmallInputShape, digestIterations := checksumIterations } + preset (fun _ ↦ (gsCore inputs.points koalaBearLeeDirectInterpContext koalaBearRothRootContext gsSmallParams).filter (passesCandidateDistance inputs.points gsNonCodewordSmallErrors)) - checksumPolynomialArrayKoala checksumIterations - let leeDirectAlekRow <- runTimed - "guruswami-sudan-core-lee-direct-noncodeword-small-alekhnovich" "CBivariate" - "Lee-O'Sullivan direct + Alekhnovich roots" - "KoalaBear.Field" gsNonCodewordSmallInputShape preset warmup alekLeeDirectMeasured + checksumPolynomialArrayKoala + let leeDirectAlekRow <- runTimedSpec + { name := "guruswami-sudan-core-lee-direct-noncodeword-small-alekhnovich", + representation := "CBivariate", method := "Lee-O'Sullivan direct + Alekhnovich roots", + field := "KoalaBear.Field", inputShape := gsNonCodewordSmallInputShape, + digestIterations := checksumIterations } + preset (fun _ ↦ (gsCore inputs.points koalaBearLeeDirectInterpContext alekRootContext gsSmallParams).filter (passesCandidateDistance inputs.points gsNonCodewordSmallErrors)) - checksumPolynomialArrayKoala checksumIterations - let leeSubproductRow <- runTimed - "guruswami-sudan-core-lee-subproduct-noncodeword-small" "CBivariate" - "Lee-O'Sullivan subproduct + RR roots" - "KoalaBear.Field" gsNonCodewordSmallInputShape preset warmup leeSubproductMeasured + checksumPolynomialArrayKoala + let leeSubproductRow <- runTimedSpec + { name := "guruswami-sudan-core-lee-subproduct-noncodeword-small", + representation := "CBivariate", method := "Lee-O'Sullivan subproduct + RR roots", + field := "KoalaBear.Field", inputShape := gsNonCodewordSmallInputShape, + digestIterations := checksumIterations } + preset (fun _ ↦ (gsCore inputs.points koalaBearLeeSubproductInterpContext koalaBearRothRootContext gsSmallParams).filter (passesCandidateDistance inputs.points gsNonCodewordSmallErrors)) - checksumPolynomialArrayKoala checksumIterations - let leeSubproductAlekRow <- runTimed - "guruswami-sudan-core-lee-subproduct-noncodeword-small-alekhnovich" "CBivariate" - "Lee-O'Sullivan subproduct + Alekhnovich roots" - "KoalaBear.Field" gsNonCodewordSmallInputShape preset warmup alekLeeSubproductMeasured + checksumPolynomialArrayKoala + let leeSubproductAlekRow <- runTimedSpec + { name := "guruswami-sudan-core-lee-subproduct-noncodeword-small-alekhnovich", + representation := "CBivariate", method := "Lee-O'Sullivan subproduct + Alekhnovich roots", + field := "KoalaBear.Field", inputShape := gsNonCodewordSmallInputShape, + digestIterations := checksumIterations } + preset (fun _ ↦ (gsCore inputs.points koalaBearLeeSubproductInterpContext alekRootContext gsSmallParams).filter (passesCandidateDistance inputs.points gsNonCodewordSmallErrors)) - checksumPolynomialArrayKoala checksumIterations - let fastDenseRow <- runTimed - "guruswami-sudan-core-dense-noncodeword-small-fast" "CBivariate" - "Dense linear + RR roots" - "KoalaBear.Fast.Field" gsNonCodewordSmallInputShape preset warmup fastDenseMeasured + checksumPolynomialArrayKoala + let fastDenseRow <- runTimedSpec + { name := "guruswami-sudan-core-dense-noncodeword-small-fast", representation := "CBivariate", + method := "Dense linear + RR roots", field := "KoalaBear.Fast.Field", + inputShape := gsNonCodewordSmallInputShape, digestIterations := checksumIterations } + preset (fun _ ↦ (gsCore inputs.fastPoints fastKoalaBearDenseInterpContext fastKoalaBearRothRootContext gsSmallParams).filter (passesCandidateDistance inputs.fastPoints gsNonCodewordSmallErrors)) - checksumPolynomialArrayKoalaFast checksumIterations - let fastDenseAlekRow <- runTimed - "guruswami-sudan-core-dense-noncodeword-small-alekhnovich-fast" "CBivariate" - "Dense linear + Alekhnovich roots" - "KoalaBear.Fast.Field" gsNonCodewordSmallInputShape preset warmup - fastAlekDenseMeasured + checksumPolynomialArrayKoalaFast + let fastDenseAlekRow <- runTimedSpec + { name := "guruswami-sudan-core-dense-noncodeword-small-alekhnovich-fast", + representation := "CBivariate", method := "Dense linear + Alekhnovich roots", + field := "KoalaBear.Fast.Field", inputShape := gsNonCodewordSmallInputShape, + digestIterations := checksumIterations } + preset (fun _ ↦ (gsCore inputs.fastPoints fastKoalaBearDenseInterpContext fastAlekRootContext gsSmallParams).filter (passesCandidateDistance inputs.fastPoints gsNonCodewordSmallErrors)) - checksumPolynomialArrayKoalaFast checksumIterations - let fastLeeDirectRow <- runTimed - "guruswami-sudan-core-lee-direct-noncodeword-small-fast" "CBivariate" - "Lee-O'Sullivan direct + RR roots" - "KoalaBear.Fast.Field" gsNonCodewordSmallInputShape preset warmup - fastLeeDirectMeasured + checksumPolynomialArrayKoalaFast + let fastLeeDirectRow <- runTimedSpec + { name := "guruswami-sudan-core-lee-direct-noncodeword-small-fast", + representation := "CBivariate", method := "Lee-O'Sullivan direct + RR roots", + field := "KoalaBear.Fast.Field", inputShape := gsNonCodewordSmallInputShape, + digestIterations := checksumIterations } + preset (fun _ ↦ (gsCore inputs.fastPoints fastKoalaBearLeeDirectInterpContext fastKoalaBearRothRootContext gsSmallParams).filter (passesCandidateDistance inputs.fastPoints gsNonCodewordSmallErrors)) - checksumPolynomialArrayKoalaFast checksumIterations - let fastLeeDirectAlekRow <- runTimed - "guruswami-sudan-core-lee-direct-noncodeword-small-alekhnovich-fast" "CBivariate" - "Lee-O'Sullivan direct + Alekhnovich roots" - "KoalaBear.Fast.Field" gsNonCodewordSmallInputShape preset warmup - fastAlekLeeDirectMeasured + checksumPolynomialArrayKoalaFast + let fastLeeDirectAlekRow <- runTimedSpec + { name := "guruswami-sudan-core-lee-direct-noncodeword-small-alekhnovich-fast", + representation := "CBivariate", method := "Lee-O'Sullivan direct + Alekhnovich roots", + field := "KoalaBear.Fast.Field", inputShape := gsNonCodewordSmallInputShape, + digestIterations := checksumIterations } + preset (fun _ ↦ (gsCore inputs.fastPoints fastKoalaBearLeeDirectInterpContext fastAlekRootContext gsSmallParams).filter (passesCandidateDistance inputs.fastPoints gsNonCodewordSmallErrors)) - checksumPolynomialArrayKoalaFast checksumIterations - let fastLeeSubproductRow <- runTimed - "guruswami-sudan-core-lee-subproduct-noncodeword-small-fast" "CBivariate" - "Lee-O'Sullivan subproduct + RR roots" - "KoalaBear.Fast.Field" gsNonCodewordSmallInputShape preset warmup - fastLeeSubproductMeasured + checksumPolynomialArrayKoalaFast + let fastLeeSubproductRow <- runTimedSpec + { name := "guruswami-sudan-core-lee-subproduct-noncodeword-small-fast", + representation := "CBivariate", method := "Lee-O'Sullivan subproduct + RR roots", + field := "KoalaBear.Fast.Field", inputShape := gsNonCodewordSmallInputShape, + digestIterations := checksumIterations } + preset (fun _ ↦ (gsCore inputs.fastPoints fastKoalaBearLeeSubproductInterpContext fastKoalaBearRothRootContext gsSmallParams).filter (passesCandidateDistance inputs.fastPoints gsNonCodewordSmallErrors)) - checksumPolynomialArrayKoalaFast checksumIterations - let fastLeeSubproductAlekRow <- runTimed - "guruswami-sudan-core-lee-subproduct-noncodeword-small-alekhnovich-fast" "CBivariate" - "Lee-O'Sullivan subproduct + Alekhnovich roots" - "KoalaBear.Fast.Field" gsNonCodewordSmallInputShape preset warmup - fastAlekLeeSubproductMeasured + checksumPolynomialArrayKoalaFast + let fastLeeSubproductAlekRow <- runTimedSpec + { name := "guruswami-sudan-core-lee-subproduct-noncodeword-small-alekhnovich-fast", + representation := "CBivariate", method := "Lee-O'Sullivan subproduct + Alekhnovich roots", + field := "KoalaBear.Fast.Field", inputShape := gsNonCodewordSmallInputShape, + digestIterations := checksumIterations } + preset (fun _ ↦ (gsCore inputs.fastPoints fastKoalaBearLeeSubproductInterpContext fastAlekRootContext gsSmallParams).filter (passesCandidateDistance inputs.fastPoints gsNonCodewordSmallErrors)) - checksumPolynomialArrayKoalaFast checksumIterations + checksumPolynomialArrayKoalaFast pure ({ groupKey := "guruswami-sudan-core-noncodeword-small-koalabear", title := "Guruswami-Sudan full core on perturbed received word, small (KoalaBear)", @@ -301,128 +290,130 @@ private def runGsFilteredCoreNonCodewordSmallKoala (preset : BenchPreset) alekhnovichRootContext KoalaBear.Field koalaBearFieldRootContext let fastAlekRootContext := alekhnovichRootContext KoalaBear.Fast.Field fastKoalaBearFieldRootContext - let warmup := gsWarmupIterations preset - let denseMeasured := preset.selectNat 1 1 1 - let leeDirectMeasured := preset.selectNat 15 2 1 - let leeSubproductMeasured := preset.selectNat 15 2 1 - let fastDenseMeasured := preset.selectNat 2 1 1 - let fastLeeDirectMeasured := preset.selectNat 80 11 2 - let fastLeeSubproductMeasured := preset.selectNat 70 10 2 - let alekDenseMeasured := denseMeasured - let alekLeeDirectMeasured := leeDirectMeasured - let alekLeeSubproductMeasured := leeSubproductMeasured - let fastAlekDenseMeasured := fastDenseMeasured - let fastAlekLeeDirectMeasured := fastLeeDirectMeasured - let fastAlekLeeSubproductMeasured := fastLeeSubproductMeasured - let checksumIterations := groupChecksumIterations denseMeasured [ - leeDirectMeasured, leeSubproductMeasured, fastDenseMeasured, - fastLeeDirectMeasured, fastLeeSubproductMeasured, alekDenseMeasured, - alekLeeDirectMeasured, alekLeeSubproductMeasured, fastAlekDenseMeasured, - fastAlekLeeDirectMeasured, fastAlekLeeSubproductMeasured - ] - let denseRow <- runTimed - "guruswami-sudan-filtered-core-dense-noncodeword-small" "CBivariate" - "Dense linear + RR roots + filter" - "KoalaBear.Field" gsNonCodewordSmallFilteredShape preset warmup denseMeasured + let checksumIterations := digestPeriod 1 + let denseRow <- runTimedSpec + { name := "guruswami-sudan-filtered-core-dense-noncodeword-small", + representation := "CBivariate", method := "Dense linear + RR roots + filter", + field := "KoalaBear.Field", inputShape := gsNonCodewordSmallFilteredShape, + digestIterations := checksumIterations } + preset (fun _ ↦ gsFilteredCore inputs.points koalaBearDenseInterpContext koalaBearRothRootContext gsSmallParams gsNonCodewordSmallErrors) - checksumPolynomialArrayKoala checksumIterations - let denseAlekRow <- runTimed - "guruswami-sudan-filtered-core-dense-noncodeword-small-alekhnovich" "CBivariate" - "Dense linear + Alekhnovich roots + filter" - "KoalaBear.Field" gsNonCodewordSmallFilteredShape preset warmup alekDenseMeasured + checksumPolynomialArrayKoala + let denseAlekRow <- runTimedSpec + { name := "guruswami-sudan-filtered-core-dense-noncodeword-small-alekhnovich", + representation := "CBivariate", method := "Dense linear + Alekhnovich roots + filter", + field := "KoalaBear.Field", inputShape := gsNonCodewordSmallFilteredShape, + digestIterations := checksumIterations } + preset (fun _ ↦ gsFilteredCore inputs.points koalaBearDenseInterpContext alekRootContext gsSmallParams gsNonCodewordSmallErrors) - checksumPolynomialArrayKoala checksumIterations - let leeDirectRow <- runTimed - "guruswami-sudan-filtered-core-lee-direct-noncodeword-small" "CBivariate" - "Lee-O'Sullivan direct + RR roots + filter" - "KoalaBear.Field" gsNonCodewordSmallFilteredShape preset warmup leeDirectMeasured + checksumPolynomialArrayKoala + let leeDirectRow <- runTimedSpec + { name := "guruswami-sudan-filtered-core-lee-direct-noncodeword-small", + representation := "CBivariate", method := "Lee-O'Sullivan direct + RR roots + filter", + field := "KoalaBear.Field", inputShape := gsNonCodewordSmallFilteredShape, + digestIterations := checksumIterations } + preset (fun _ ↦ gsFilteredCore inputs.points koalaBearLeeDirectInterpContext koalaBearRothRootContext gsSmallParams gsNonCodewordSmallErrors) - checksumPolynomialArrayKoala checksumIterations - let leeDirectAlekRow <- runTimed - "guruswami-sudan-filtered-core-lee-direct-noncodeword-small-alekhnovich" "CBivariate" - "Lee-O'Sullivan direct + Alekhnovich roots + filter" - "KoalaBear.Field" gsNonCodewordSmallFilteredShape preset warmup alekLeeDirectMeasured + checksumPolynomialArrayKoala + let leeDirectAlekRow <- runTimedSpec + { name := "guruswami-sudan-filtered-core-lee-direct-noncodeword-small-alekhnovich", + representation := "CBivariate", + method := "Lee-O'Sullivan direct + Alekhnovich roots + filter", field := "KoalaBear.Field", + inputShape := gsNonCodewordSmallFilteredShape, digestIterations := checksumIterations } + preset (fun _ ↦ gsFilteredCore inputs.points koalaBearLeeDirectInterpContext alekRootContext gsSmallParams gsNonCodewordSmallErrors) - checksumPolynomialArrayKoala checksumIterations - let leeSubproductRow <- runTimed - "guruswami-sudan-filtered-core-lee-subproduct-noncodeword-small" "CBivariate" - "Lee-O'Sullivan subproduct + RR roots + filter" - "KoalaBear.Field" gsNonCodewordSmallFilteredShape preset warmup leeSubproductMeasured + checksumPolynomialArrayKoala + let leeSubproductRow <- runTimedSpec + { name := "guruswami-sudan-filtered-core-lee-subproduct-noncodeword-small", + representation := "CBivariate", method := "Lee-O'Sullivan subproduct + RR roots + filter", + field := "KoalaBear.Field", inputShape := gsNonCodewordSmallFilteredShape, + digestIterations := checksumIterations } + preset (fun _ ↦ gsFilteredCore inputs.points koalaBearLeeSubproductInterpContext koalaBearRothRootContext gsSmallParams gsNonCodewordSmallErrors) - checksumPolynomialArrayKoala checksumIterations - let leeSubproductAlekRow <- runTimed - "guruswami-sudan-filtered-core-lee-subproduct-noncodeword-small-alekhnovich" "CBivariate" - "Lee-O'Sullivan subproduct + Alekhnovich roots + filter" - "KoalaBear.Field" gsNonCodewordSmallFilteredShape preset warmup - alekLeeSubproductMeasured + checksumPolynomialArrayKoala + let leeSubproductAlekRow <- runTimedSpec + { name := "guruswami-sudan-filtered-core-lee-subproduct-noncodeword-small-alekhnovich", + representation := "CBivariate", + method := "Lee-O'Sullivan subproduct + Alekhnovich roots + filter", + field := "KoalaBear.Field", inputShape := gsNonCodewordSmallFilteredShape, + digestIterations := checksumIterations } + preset (fun _ ↦ gsFilteredCore inputs.points koalaBearLeeSubproductInterpContext alekRootContext gsSmallParams gsNonCodewordSmallErrors) - checksumPolynomialArrayKoala checksumIterations - let fastDenseRow <- runTimed - "guruswami-sudan-filtered-core-dense-noncodeword-small-fast" "CBivariate" - "Dense linear + RR roots + filter" - "KoalaBear.Fast.Field" gsNonCodewordSmallFilteredShape preset warmup - fastDenseMeasured + checksumPolynomialArrayKoala + let fastDenseRow <- runTimedSpec + { name := "guruswami-sudan-filtered-core-dense-noncodeword-small-fast", + representation := "CBivariate", method := "Dense linear + RR roots + filter", + field := "KoalaBear.Fast.Field", inputShape := gsNonCodewordSmallFilteredShape, + digestIterations := checksumIterations } + preset (fun _ ↦ gsFilteredCore inputs.fastPoints fastKoalaBearDenseInterpContext fastKoalaBearRothRootContext gsSmallParams gsNonCodewordSmallErrors) - checksumPolynomialArrayKoalaFast checksumIterations - let fastDenseAlekRow <- runTimed - "guruswami-sudan-filtered-core-dense-noncodeword-small-alekhnovich-fast" "CBivariate" - "Dense linear + Alekhnovich roots + filter" - "KoalaBear.Fast.Field" gsNonCodewordSmallFilteredShape preset warmup - fastAlekDenseMeasured + checksumPolynomialArrayKoalaFast + let fastDenseAlekRow <- runTimedSpec + { name := "guruswami-sudan-filtered-core-dense-noncodeword-small-alekhnovich-fast", + representation := "CBivariate", method := "Dense linear + Alekhnovich roots + filter", + field := "KoalaBear.Fast.Field", inputShape := gsNonCodewordSmallFilteredShape, + digestIterations := checksumIterations } + preset (fun _ ↦ gsFilteredCore inputs.fastPoints fastKoalaBearDenseInterpContext fastAlekRootContext gsSmallParams gsNonCodewordSmallErrors) - checksumPolynomialArrayKoalaFast checksumIterations - let fastLeeDirectRow <- runTimed - "guruswami-sudan-filtered-core-lee-direct-noncodeword-small-fast" "CBivariate" - "Lee-O'Sullivan direct + RR roots + filter" - "KoalaBear.Fast.Field" gsNonCodewordSmallFilteredShape preset warmup - fastLeeDirectMeasured + checksumPolynomialArrayKoalaFast + let fastLeeDirectRow <- runTimedSpec + { name := "guruswami-sudan-filtered-core-lee-direct-noncodeword-small-fast", + representation := "CBivariate", method := "Lee-O'Sullivan direct + RR roots + filter", + field := "KoalaBear.Fast.Field", inputShape := gsNonCodewordSmallFilteredShape, + digestIterations := checksumIterations } + preset (fun _ ↦ gsFilteredCore inputs.fastPoints fastKoalaBearLeeDirectInterpContext fastKoalaBearRothRootContext gsSmallParams gsNonCodewordSmallErrors) - checksumPolynomialArrayKoalaFast checksumIterations - let fastLeeDirectAlekRow <- runTimed - "guruswami-sudan-filtered-core-lee-direct-noncodeword-small-alekhnovich-fast" "CBivariate" - "Lee-O'Sullivan direct + Alekhnovich roots + filter" - "KoalaBear.Fast.Field" gsNonCodewordSmallFilteredShape preset warmup - fastAlekLeeDirectMeasured + checksumPolynomialArrayKoalaFast + let fastLeeDirectAlekRow <- runTimedSpec + { name := "guruswami-sudan-filtered-core-lee-direct-noncodeword-small-alekhnovich-fast", + representation := "CBivariate", + method := "Lee-O'Sullivan direct + Alekhnovich roots + filter", + field := "KoalaBear.Fast.Field", inputShape := gsNonCodewordSmallFilteredShape, + digestIterations := checksumIterations } + preset (fun _ ↦ gsFilteredCore inputs.fastPoints fastKoalaBearLeeDirectInterpContext fastAlekRootContext gsSmallParams gsNonCodewordSmallErrors) - checksumPolynomialArrayKoalaFast checksumIterations - let fastLeeSubproductRow <- runTimed - "guruswami-sudan-filtered-core-lee-subproduct-noncodeword-small-fast" "CBivariate" - "Lee-O'Sullivan subproduct + RR roots + filter" - "KoalaBear.Fast.Field" gsNonCodewordSmallFilteredShape preset warmup - fastLeeSubproductMeasured + checksumPolynomialArrayKoalaFast + let fastLeeSubproductRow <- runTimedSpec + { name := "guruswami-sudan-filtered-core-lee-subproduct-noncodeword-small-fast", + representation := "CBivariate", method := "Lee-O'Sullivan subproduct + RR roots + filter", + field := "KoalaBear.Fast.Field", inputShape := gsNonCodewordSmallFilteredShape, + digestIterations := checksumIterations } + preset (fun _ ↦ gsFilteredCore inputs.fastPoints fastKoalaBearLeeSubproductInterpContext fastKoalaBearRothRootContext gsSmallParams gsNonCodewordSmallErrors) - checksumPolynomialArrayKoalaFast checksumIterations - let fastLeeSubproductAlekRow <- runTimed - "guruswami-sudan-filtered-core-lee-subproduct-noncodeword-small-alekhnovich-fast" "CBivariate" - "Lee-O'Sullivan subproduct + Alekhnovich roots + filter" - "KoalaBear.Fast.Field" gsNonCodewordSmallFilteredShape preset warmup - fastAlekLeeSubproductMeasured + checksumPolynomialArrayKoalaFast + let fastLeeSubproductAlekRow <- runTimedSpec + { name := "guruswami-sudan-filtered-core-lee-subproduct-noncodeword-small-alekhnovich-fast", + representation := "CBivariate", + method := "Lee-O'Sullivan subproduct + Alekhnovich roots + filter", + field := "KoalaBear.Fast.Field", inputShape := gsNonCodewordSmallFilteredShape, + digestIterations := checksumIterations } + preset (fun _ ↦ gsFilteredCore inputs.fastPoints fastKoalaBearLeeSubproductInterpContext fastAlekRootContext gsSmallParams gsNonCodewordSmallErrors) - checksumPolynomialArrayKoalaFast checksumIterations + checksumPolynomialArrayKoalaFast pure ({ groupKey := "guruswami-sudan-filtered-core-noncodeword-small-koalabear", title := "Guruswami-Sudan filtered core on perturbed received word, small (KoalaBear)", diff --git a/bench/CompPolyBench/Bivariate/GuruswamiSudan/Shared.lean b/bench/CompPolyBench/Bivariate/GuruswamiSudan/Shared.lean index 5a017bd9..c2c2580c 100644 --- a/bench/CompPolyBench/Bivariate/GuruswamiSudan/Shared.lean +++ b/bench/CompPolyBench/Bivariate/GuruswamiSudan/Shared.lean @@ -109,13 +109,6 @@ def checksumPolynomialArrayKoalaFast (ps : Array (CPolynomial KoalaBear.Fast.Field)) : Nat := checksumArray (checksumCPolynomial checksumKoalaBearFast) ps -def gsWarmupIterations (preset : BenchPreset) : Nat := - preset.selectNat 1 0 0 - -/- -Preset iteration counts are fixed per benchmark row to keep total runtimes -comparable within each group across `small`, `medium`, and `large` runs. --/ /-- Benchmark group metadata for Guruswami-Sudan cost-center rows. -/ def guruswamiSudanGroupInfos : List BenchGroupInfo := [ ⟨"guruswami-sudan-interp-system-small-koalabear", diff --git a/bench/CompPolyBench/Common.lean b/bench/CompPolyBench/Common.lean index 4d3d4d6d..6831c1e4 100644 --- a/bench/CompPolyBench/Common.lean +++ b/bench/CompPolyBench/Common.lean @@ -6,7 +6,9 @@ Authors: Valerii Huhnin module public import Init.Data.Random +public import CompPolyBench.Harness.Budget public import Lean.Data.Json.Parser +public import Lean.Data.Json.Printer public import Std.Time public import CompPoly.Fields.KoalaBear public import CompPoly.Fields.BabyBear @@ -45,87 +47,41 @@ def BenchPreset.name : BenchPreset → String | BenchPreset.medium => "medium" | BenchPreset.large => "large" -/-- Return the precomputed value for the active benchmark preset. -/ -def BenchPreset.selectNat (preset : BenchPreset) (large medium small : Nat) : Nat := - match preset with - | BenchPreset.large => large - | BenchPreset.medium => medium - | BenchPreset.small => small +/-- Measurement budget for the active benchmark preset. -/-- Warmup iteration count for ordinary evaluation benchmarks. -/ -def warmupIterations (preset : BenchPreset) : Nat := - preset.selectNat 100 10 0 - -/-- Measured iteration count for ordinary evaluation benchmarks. -/ -def measuredIterations (preset : BenchPreset) : Nat := - preset.selectNat 5000 700 150 - -/-- Warmup iteration count for batch-evaluation benchmarks over the base input shape. -/ -def batchWarmupIterations (preset : BenchPreset) : Nat := - preset.selectNat 1 1 0 - -/-- Measured iteration count for batch-evaluation benchmarks over the base input shape. -/ -def batchMeasuredIterations (preset : BenchPreset) : Nat := - preset.selectNat 5 1 1 - -/-- Warmup iteration count for batch-evaluation benchmarks over the medium input shape. -/ -def mediumBatchWarmupIterations (preset : BenchPreset) : Nat := - preset.selectNat 1 1 0 - -/-- Measured iteration count for batch-evaluation benchmarks over the medium input shape. -/ -def mediumBatchMeasuredIterations (preset : BenchPreset) : Nat := - preset.selectNat 10 1 1 - -/-- Warmup iteration count for batch-evaluation benchmarks over the large input shape. -/ -def largeBatchWarmupIterations (preset : BenchPreset) : Nat := - preset.selectNat 1 1 0 - -/-- Measured iteration count for batch-evaluation benchmarks over the large input shape. -/ -def largeBatchMeasuredIterations (preset : BenchPreset) : Nat := - preset.selectNat 10 1 1 - -/-- Warmup iteration count for direct monic-remainder benchmarks. -/ -def modWarmupIterations (preset : BenchPreset) : Nat := - preset.selectNat 1 1 0 - -/-- Measured iteration count for direct monic-remainder benchmarks. -/ -def modMeasuredIterations (preset : BenchPreset) : Nat := - preset.selectNat 20 3 1 - -/-- Warmup iteration count for direct monic-remainder benchmarks over the medium input shape. -/ -def mediumModWarmupIterations (preset : BenchPreset) : Nat := - preset.selectNat 1 1 0 - -/-- Measured iteration count for direct monic-remainder benchmarks over the medium input shape. -/ -def mediumModMeasuredIterations (preset : BenchPreset) : Nat := - preset.selectNat 5 1 1 - -/-- Warmup iteration count for direct univariate multiplication benchmarks. -/ -def mulWarmupIterations (preset : BenchPreset) : Nat := - preset.selectNat 1 1 0 - -/-- Measured iteration count for direct univariate multiplication benchmarks. -/ -def mulMeasuredIterations (preset : BenchPreset) : Nat := - preset.selectNat 20 3 1 - -/-- Warmup iteration count for the base additive NTT benchmark. -/ -def additiveNttWarmupIterations (preset : BenchPreset) : Nat := - preset.selectNat 10 1 0 - -/-- Measured iteration count for the base additive NTT benchmark. -/ -def additiveNttMeasuredIterations (preset : BenchPreset) : Nat := - preset.selectNat 1000 150 30 +What a preset selects. It used to select an iteration count per benchmark, from +a hand-written `large medium small` triple at each of 228 call sites; a count is +not comparable between two rows of one table, goes stale as the code it measures +gets faster, and has to be re-guessed on every machine. A wall-clock budget is +comparable, and `Harness.Budget` works the count out per row. -/ +def BenchPreset.budget : BenchPreset → BenchBudget + | BenchPreset.large => largeBudget + | BenchPreset.medium => mediumBudget + | BenchPreset.small => smallBudget /-- Primality witness used for generic `ZMod` benchmarks over `KoalaBear`. -/ instance : Fact (Nat.Prime KoalaBear.fieldSize) where out := KoalaBear.is_prime -/-- Primality witness used for generic `ZMod` benchmarks over `Goldilocks`. -/ -instance : Fact (Nat.Prime Goldilocks.fieldSize) where - out := Goldilocks.is_prime - /-- Result row emitted by one timed benchmark case. -/ structure BenchRecord where + /-- Registry key of the group this row belongs to. + + `runTimedSpec` does not know it — a row is built before it is placed in a + group — so it is stamped in `flattenGroups` from `BenchGroup.groupKey`, which + `BenchTask.fromGroupRunner` single-sources from the registry that `--list` and + `bench/ci-groups.txt` validate against. Empty until then. -/ + groupKey : String := "" + /-- Report title of the group this row belongs to, stamped alongside the key. -/ + groupTitle : String := "" + /-- Which rows of the group this one must agree with on a digest. + + A group is a set of rows measured together; it is not always a set of rows + computing the *same value*. A field's `mul` and its `add` belong in one table + and cannot share a digest. Rows are partitioned by this label and agreement is + required within each part, so one group can carry several comparisons. Empty + is a class like any other, which is what every pre-existing group uses. -/ + digestClass : String := "" name : String representation : String method : String @@ -136,8 +92,22 @@ structure BenchRecord where checksumIterations : Nat measuredIterations : Nat totalNanos : Nat - averageNanos : Nat + /-- Median per-iteration cost in nanoseconds. + + A median, not a mean; it was called `averageNanos` until the name was found to + be describing the wrong statistic. -/ + medianNanos : Nat + /-- Elementary operations one iteration of this body performs. + + A property of the *problem*, not of the implementation: the rows of a group + must agree on it, or a per-unit figure would divide away the very difference + the group exists to show. One for an ordinary row; the chain length for a + chained operation; `(n / 2) * log n` for a transform. -/ + workUnits : Nat := 1 checksum : Nat + sinkDigest : UInt64 + stats : SampleStats + samples : Array Nat /-- A set of benchmark rows expected to produce matching checksums. -/ structure BenchGroup where @@ -180,13 +150,27 @@ def BenchSelection.filterTasks (selection : BenchSelection) tasks.filter fun task ↦ selection.selectsAny (task.infos.map fun info ↦ info.groupKey) -/-- Build one registry task from metadata and a single-group runner. -/ +/-- Derive a benchmark group's input generator from its key. + +Seeding per group rather than threading one generator through the run is what +makes a group's inputs independent of which other groups ran, and in what order. +Without it `--group X` and `--groups X,Y` measure different inputs, a group added +anywhere changes the inputs of every group after it, and no digest can be +compared across runs. -/ +def genFor (groupKey : String) : StdGen := + mkStdGen (seed ^^^ (String.hash groupKey).toNat) + +/-- Build one registry task from metadata and a single-group runner. + +The group's key and title come from `info`, which is what `--list` and the CI +allowlist validate against, so a runner cannot drift from its registration. The +incoming generator is passed through untouched; each group draws its own. -/ def BenchTask.fromGroupRunner (info : BenchGroupInfo) (runGroup : BenchPreset → StdGen → IO (BenchGroup × StdGen)) : BenchTask where infos := [info] runTask := fun preset _ gen ↦ do - let (group, gen) ← runGroup preset gen - pure (#[group], gen) + let (group, _) ← runGroup preset (genFor info.groupKey) + pure (#[{ group with groupKey := info.groupKey, title := info.title }], gen) /-- Total measured runtime across all benchmark records in a group. -/ def totalGroupNanos (records : List BenchRecord) : Nat := @@ -255,6 +239,12 @@ def formatNanosAuto (nanos : Nat) : String := let unit := chooseTimeUnit [nanos] formatNanosWithUnit unit nanos +/-- Render in the shared unit, falling back to a labeled per-value unit when the +shared unit would collapse the value to `<0.01`. -/ +def formatNanosInUnitOrAuto (unit : TimeUnit) (nanos : Nat) : String := + let rendered := formatNanosInUnit unit nanos + if rendered == "<0.01" then formatNanosAuto nanos else rendered + /-- Run selected tasks from a registry and concatenate their emitted groups. -/ def runSelectedTasks (tasks : List BenchTask) (preset : BenchPreset) (selection : BenchSelection) (gen : StdGen) : IO (Array BenchGroup × StdGen) := do @@ -263,9 +253,13 @@ def runSelectedTasks (tasks : List BenchTask) (preset : BenchPreset) (selection for task in selection.filterTasks tasks do let (taskGroups, nextGen) ← task.runTask preset selection gen gen := nextGen + let validateOnly ← validateOnlyRef.get for group in taskGroups do - let groupTotal := totalGroupNanos group.records.toList - IO.println s!"finished {group.groupKey} in {formatNanosAuto groupTotal}" + if validateOnly then + IO.println s!"validated {group.groupKey}" + else + let groupTotal := totalGroupNanos group.records.toList + IO.println s!"finished {group.groupKey} in {formatNanosAuto groupTotal}" groups := groups.push group pure (groups, gen) @@ -288,13 +282,24 @@ def makeRunId : IO String := do let started ← Std.Time.PlainDateTime.now pure <| started.format "yyMMdd-HHmmss" +/-- Directory holding generated benchmark output. + +A single directory rather than files dropped beside the sources, so a local run +does not accumulate reports in `bench/` and CI's artifact glob cannot pick up +anything but the run it just made. -/ +def outputDir : System.FilePath := "bench" / "out" + /-- Path for the generated JSONL benchmark results. -/ def resultsPath (runId : String) : System.FilePath := - "bench" / ("results-" ++ runId ++ ".jsonl") + outputDir / ("results-" ++ runId ++ ".jsonl") /-- Path for the generated Markdown benchmark report. -/ def reportPath (runId : String) : System.FilePath := - "bench" / ("report-" ++ runId ++ ".md") + outputDir / ("report-" ++ runId ++ ".md") + +/-- Path for the per-run provenance manifest. -/ +def manifestPath (runId : String) : System.FilePath := + outputDir / ("manifest-" ++ runId ++ ".json") /-- Trim command output and normalize empty output to the empty string. -/ def trimCommandOutput (s : String) : String := @@ -359,11 +364,60 @@ def memTotalGib (output : String) : Option String := go (output.splitOn "\n") /-- Collect best-effort GitHub runner or local machine metadata. -/ +def sysctlValue (key : String) : IO (Option String) := + runInfoCommand "sysctl" #["-n", key] + +/-- Size column of a BSD `df -h` row. + +`df --output=size` is GNU-only, so the darwin probe parses the full table and the +size is the second field rather than the first. -/ +def dfRootSizeBsd (output : String) : Option String := + match output.splitOn "\n" with + | _header :: row :: _ => + match whitespaceFields row with + | _fs :: size :: _ => some size + | _ => none + | _ => none + +/-- Convert a byte count reported by `sysctl` to whole gibibytes. -/ +def bytesToGib (text : String) : Option String := + (text.trimAscii.toString.toNat?).map fun bytes ↦ + toString (bytes / (1024 * 1024 * 1024)) ++ " GiB" + +/-- Collect host details on darwin, where none of the Linux probes exist. -/ +def collectDarwinHardware : IO RunnerHardware := do + let cpuModel ← sysctlValue "machdep.cpu.brand_string" + let logicalCpus ← sysctlValue "hw.logicalcpu" + let physicalCpus ← sysctlValue "hw.physicalcpu" + let memBytes ← sysctlValue "hw.memsize" + let dfRoot ← runInfoCommand "df" #["-h", "/"] + pure { + runnerOs := some "macOS" + runnerArch := none + cpuModel := cpuModel + logicalCpus := logicalCpus + coresPerSocket := physicalCpus + threadsPerCore := none + sockets := some "1" + ramTotal := memBytes.bind bytesToGib + rootDisk := dfRoot.bind dfRootSizeBsd + hypervisor := none } + +/-- Collect host details, preferring the Linux probes and falling back to darwin's. + +The Linux path is the one CI takes; the darwin path exists so a local run reports +which machine produced a number instead of `unavailable outside GitHub Actions`. -/ def collectRunnerHardware : IO RunnerHardware := do let runnerOs ← IO.getEnv "RUNNER_OS" let runnerArch ← IO.getEnv "RUNNER_ARCH" let nproc ← runInfoCommand "nproc" #[] let lscpu ← runInfoCommand "lscpu" #["--json"] + if lscpu.isNone && nproc.isNone then + let darwin ← collectDarwinHardware + if darwin.cpuModel.isSome then + return { darwin with + runnerOs := runnerOs.orElse fun _ ↦ darwin.runnerOs + runnerArch := runnerArch } let meminfo ← try let text ← IO.FS.readFile "/proc/meminfo" @@ -425,6 +479,10 @@ def koalaBearArray (size : Nat) (sparse : Bool) : StateM StdGen (Array KoalaBear def koalaBearFastArray (xs : Array KoalaBear.Field) : Array KoalaBear.Fast.Field := xs.map KoalaBear.Fast.ofField +/-- Convert Goldilocks field inputs to the native-word Goldilocks representation. -/ +def goldilocksFastArray (xs : Array Goldilocks.Field) : Array Goldilocks.Fast.Field := + xs.map Goldilocks.Fast.ofField + /-- Generate KoalaBear coefficients with a nonzero every `sparseStride` entries. -/ def koalaBearArrayWithStride (size sparseStride : Nat) : StateM StdGen (Array KoalaBear.Field) := do @@ -481,6 +539,13 @@ def checksumBabyBear (x : BabyBear.Field) : Nat := def checksumBabyBearFast (x : BabyBear.Fast.Field) : Nat := x.toNat +/-- Convert a fast Goldilocks element to a checksum word. + +The carrier is an `abbrev` for a `Subtype`, so dot notation would resolve to +`Subtype.toNat`; call the field's own `toNat` directly. -/ +def checksumGoldilocksFast (x : Goldilocks.Fast.Field) : Nat := + Goldilocks.Fast.toNat x + /-- Convert a `ZMod` element to a checksum word. -/ def checksumZMod {modulus : Nat} (x : ZMod modulus) : Nat := ZMod.val x @@ -505,47 +570,147 @@ def checksumCPolynomial [Zero α] (checksum : α → Nat) (p : CPolynomial α) : def checksumRawPolynomial (checksum : α → Nat) (p : CPolynomial.Raw α) : Nat := checksumArray checksum p -/-- Compute the checksum iteration count shared by a benchmark group. -/ -def groupChecksumIterations (first : Nat) (rest : List Nat) : Nat := - rest.foldl Nat.min first +/-! ### Native sinks + +`UInt64`-native digests for the timed region, for carriers whose `Nat` digest +would allocate. Pass one as `runTimed`'s `sink` argument; the untimed validation +pass keeps using the `Nat` checksum either way. -/ + +/-- Sink a fast Goldilocks element by its underlying word. + +`Goldilocks.fieldSize` exceeds `2 ^ 63`, so the `Nat` digest allocates a bignum +on most inputs while the word itself is free. -/ +@[inline] def sinkGoldilocksFast (x : Goldilocks.Fast.Field) : UInt64 := + Subtype.val x + +/-- Sink a `ZMod` element by truncating its canonical value. + +Kept explicit because it is *not* free: for a modulus above `2 ^ 63` the +canonical value is a bignum, so a `ZMod` row carries an irreducible sink cost +that its fast counterpart does not. -/ +@[inline] def sinkZMod {modulus : Nat} (x : ZMod modulus) : UInt64 := + natSink (ZMod.val x) + +/-- Ceiling on digest-pass iterations. + +The digest pass re-runs the benchmark body, so leaving it equal to the measured +iteration count made correctness checking cost as much as measurement. The cap is +at or above every benchmark's operand-pool size, so the oracle still sees every +input it did before. -/ +def digestIterationCap : Nat := 256 + +/-- Digest iterations for a body whose result cycles with period `period`. + +The period is a property of the benchmark body, never of the preset or the +machine: a digest derived from an iteration count is not comparable across runs, +and once those counts come from a wall-clock budget it would differ between +machines too, which makes committed digest fixtures impossible rather than merely +awkward. Truncating to the period is not a weaker check — iterations past one full +cycle recompute a bit-identical result. -/ +def digestPeriod (period : Nat) : Nat := max 1 (min digestIterationCap period) + +/-- Everything about one benchmark row except its body, its digest, and its sink. + +Introduced because `runTimed` took five consecutive `String` arguments across +228 call sites, where a transposed pair is a silent mislabelling rather than a +type error. The three `α`-dependent arguments stay outside: giving `BenchSpec` a +type parameter to carry `sink` would put one on every literal in the suite in +order to serve the forty rows that override it, and a group with a `ZMod` row +beside a `Fast` row has a different result type per row anyway. -/ +structure BenchSpec where + /-- Row name, unique within the suite. -/ + name : String + /-- Representation label, such as `ZMod` or `UInt64`. -/ + representation : String + /-- Operation label, such as `mul` or `inv (Fermat chain)`. -/ + method : String + /-- Field or configuration label. -/ + field : String + /-- Input-shape label, shared by every row of a group. -/ + inputShape : String + /-- Iterations of the untimed digest pass. + + Must be the body's period in `i`, never preset-shaped: see `digestPeriod`. -/ + digestIterations : Nat + /-- Which rows of the group this row must agree with on a digest. + + Leave empty when every row of the group computes the same value. Set it to + separate the comparisons inside a group that carries more than one; see + `BenchRecord.digestClass`. -/ + digestClass : String := "" + /-- Elementary operations one iteration of the body performs. + + Left at one for a row that performs its operation once. Set it and the report + gains a per-unit column; see `BenchRecord.workUnits` for why every row of a + group has to agree on the value. -/ + workUnits : Nat := 1 + /-- Opt out of the `--validate-only` short circuit, for the harness + self-check, which has to be measured even when nothing else is. -/ + forceTiming : Bool := false +deriving Inhabited /-- Time one benchmark closure and package its metadata and checksum. -The checksum is computed before timing over `checksumIterations`, the minimum -measured-iteration count used by the records in the surrounding benchmark group. -The timed loop consumes each result so implementations with different iteration -counts remain comparable within the same group. +The strong `Nat` digest runs *before* timing, over `spec.digestIterations` — the +period of the body in its iteration index — and is what the group agreement +check compares. + +Inside the timed region each result is folded through `sink` instead, which +defaults to truncating the `Nat` digest and should be overridden with a +`UInt64`-native digest wherever the benchmark is cheap enough for the digest to +show up in the measurement. + +The row is then sized from `preset.budget`: a calibration ramp doubles as warmup +and estimates the per-iteration cost, and that estimate decides how many +iterations one sample holds and how many samples are affordable. Nothing about +the shape of the work is chosen here — an expensive row still reports `n=1`, but +now only when one iteration genuinely exhausts the budget. + +Under `--validate-only` neither calibration nor sampling runs and the record +carries digests alone. Skipping calibration is the point: a ramp on a +thirteen-second body costs thirteen seconds, and `--validate-only` is the only +benchmark step on the blocking CI path. `forceTiming` opts out of the short +circuit, for the harness self-check, whose canary has nothing to compare +against a floor that was never measured. -/ -def runTimed (name representation method field inputShape : String) (preset : BenchPreset) - (warmup measured : Nat) (run : Nat → α) (checksum : α → Nat) - (checksumIterations : Nat := measured) : IO BenchRecord := do - for i in [0:warmup] do - let _ := run i - pure () +@[specialize] def runTimedSpec (spec : BenchSpec) (preset : BenchPreset) + (run : Nat → α) (checksum : α → Nat) + (sink : α → UInt64 := fun x ↦ natSink (checksum x)) : IO BenchRecord := do + let body : Nat → UInt64 → UInt64 := fun i acc ↦ sinkStep acc (sink (run i)) let mut validationChecksum := 0 - for i in [0:checksumIterations] do + for i in [0:spec.digestIterations] do validationChecksum := mixChecksum validationChecksum (checksum (run i)) - let start ← IO.monoNanosNow - let mut timingChecksum := 0 - for i in [0:measured] do - timingChecksum := mixChecksum timingChecksum (checksum (run i)) - let stop ← IO.monoNanosNow - let _ := timingChecksum - let total := stop - start + let validateOnly := (← validateOnlyRef.get) && !spec.forceTiming + let budget := preset.budget + -- The ramp is not discounted for the digest pass the way a fixed warmup count + -- used to be. It cannot be: the budget is in nanoseconds and the digest pass is + -- untimed. Nor is it worth it — for a cheap body the digest is at most 256 + -- iterations against tens of milliseconds of ramp, and for an expensive one the + -- ramp stops after its first step either way. + let calibration ← if validateOnly then pure default else + calibrate budget.warmupNanos body + let plan := if validateOnly then { itersPerSample := 0, sampleCount := 0 } else + planFromCalibration budget calibration.picosPerIteration + let sampled ← collectSamples calibration.sink plan body pure { - name := name - representation := representation - method := method + name := spec.name + representation := spec.representation + method := spec.method preset := preset.name - field := field - inputShape := inputShape - warmupIterations := warmup - checksumIterations := checksumIterations - measuredIterations := measured - totalNanos := total - averageNanos := if measured = 0 then 0 else total / measured + field := spec.field + inputShape := spec.inputShape + warmupIterations := calibration.iterations + checksumIterations := spec.digestIterations + measuredIterations := sampled.totalIterations + totalNanos := sampled.totalNanos + medianNanos := sampled.stats.medianPicos / 1000 + workUnits := spec.workUnits + digestClass := spec.digestClass checksum := validationChecksum + sinkDigest := sampled.sink + stats := sampled.stats + samples := sampled.samples } /-- Append benchmark records from `ys` onto `xs`. -/ @@ -556,17 +721,140 @@ def appendRecords (xs ys : Array BenchRecord) : Array BenchRecord := def appendGroups (xs ys : Array BenchGroup) : Array BenchGroup := ys.foldl (init := xs) fun acc group ↦ acc.push group -/-- Flatten grouped benchmark records for JSONL output. -/ +/-- Flatten grouped benchmark records for JSONL output, stamping group identity. + +The key and the title live only in the Markdown report otherwise, so a JSONL +consumer has to reconstruct the grouping from row names. Stamped here rather +than at `runTimedSpec`, which genuinely does not know which group a row will +end up in. -/ def flattenGroups (groups : Array BenchGroup) : Array BenchRecord := - groups.foldl (init := #[]) fun acc group ↦ appendRecords acc group.records + groups.foldl (init := #[]) fun acc group ↦ + appendRecords acc (group.records.map fun record ↦ + { record with groupKey := group.groupKey, groupTitle := group.title }) + +/-! ### Run manifest + +What produced a number, recorded beside it. Budget-driven sizing costs the suite +its one previously-stable provenance signal: `measured_iterations` used to be a +written-down constant, and is now a function of how fast the machine was when +the row was calibrated. Nothing else in the JSONL says which commit, which +toolchain, or which hardware a run came from. -/-- Render a benchmark string field as a JSON string. -/ +Deliberately a separate file rather than a header line in the JSONL: every +consumer of that file assumes uniform records, and a header would break all of +them at once. +-/ + +/-- Provenance for one benchmark run. -/ +structure RunManifest where + /-- Timestamp identifier shared with the results and report filenames. -/ + runId : String + /-- `git rev-parse HEAD`, or `none` outside a checkout. -/ + commit : Option String + /-- Whether the working tree had uncommitted changes. + + Not optional in spirit: a timing taken from a dirty tree is not attributable + to anything, and the flag is the only way a reader finds that out later. -/ + dirty : Bool + /-- Contents of `lean-toolchain`. -/ + toolchain : Option String + /-- Preset name, and the budget it resolved to. -/ + preset : BenchPreset + /-- Whether this run collected timings at all. -/ + validateOnly : Bool + /-- Group keys requested, or `none` for the whole suite. -/ + selection : Option (List String) + /-- Groups and rows actually produced. -/ + groupCount : Nat + /-- Rows actually produced. -/ + recordCount : Nat + /-- Host details, as the Markdown report collects them. -/ + hardware : RunnerHardware + +/-- Collect the commit and dirty flag, tolerating a non-checkout. -/ +def collectGitProvenance : IO (Option String × Bool) := do + let commit ← runInfoCommand "git" #["rev-parse", "HEAD"] + let status ← runInfoCommand "git" #["status", "--porcelain"] + -- `runInfoCommand` maps empty output to `none`, so a clean tree reads as + -- `none` and any modification at all reads as `some`. + pure (commit, status.isSome) + +/-- Read the pinned toolchain, tolerating its absence. -/ +def collectToolchain : IO (Option String) := do + try + let text ← IO.FS.readFile "lean-toolchain" + let trimmed := trimCommandOutput text + pure <| if trimmed.isEmpty then none else some trimmed + catch _ => + pure none + +/-- Gather everything the manifest records about this run. -/ +def collectRunManifest (runId : String) (preset : BenchPreset) (validateOnly : Bool) + (selection : BenchSelection) (groupCount recordCount : Nat) : IO RunManifest := do + let (commit, dirty) ← collectGitProvenance + let toolchain ← collectToolchain + let hardware ← collectRunnerHardware + pure { + runId := runId + commit := commit + dirty := dirty + toolchain := toolchain + preset := preset + validateOnly := validateOnly + selection := match selection with + | BenchSelection.all => none + | BenchSelection.only keys => some keys + groupCount := groupCount + recordCount := recordCount + hardware := hardware } + +/-- Render a manifest as pretty-printed JSON. -/ +def RunManifest.render (manifest : RunManifest) : String := + let str (value : Option String) : Lean.Json := + match value with + | some text => Lean.Json.str text + | none => Lean.Json.null + let budget := manifest.preset.budget + let json := Lean.Json.mkObj [ + ("run_id", Lean.Json.str manifest.runId), + ("commit", str manifest.commit), + ("dirty", Lean.Json.bool manifest.dirty), + ("toolchain", str manifest.toolchain), + ("preset", Lean.Json.str manifest.preset.name), + ("validate_only", Lean.Json.bool manifest.validateOnly), + ("seed", Lean.Json.num seed), + ("budget", Lean.Json.mkObj [ + ("warmup_nanos", Lean.Json.num budget.warmupNanos), + ("sample_nanos", Lean.Json.num budget.sampleNanos), + ("sample_count", Lean.Json.num budget.sampleCount), + ("measure_nanos", Lean.Json.num budget.measureNanos)]), + ("selection", match manifest.selection with + | none => Lean.Json.null + | some keys => Lean.Json.arr (keys.map Lean.Json.str).toArray), + ("group_count", Lean.Json.num manifest.groupCount), + ("record_count", Lean.Json.num manifest.recordCount), + ("hardware", Lean.Json.mkObj [ + ("runner_os", str manifest.hardware.runnerOs), + ("runner_arch", str manifest.hardware.runnerArch), + ("cpu_model", str manifest.hardware.cpuModel), + ("logical_cpus", str manifest.hardware.logicalCpus), + ("cores_per_socket", str manifest.hardware.coresPerSocket), + ("threads_per_core", str manifest.hardware.threadsPerCore), + ("sockets", str manifest.hardware.sockets), + ("ram_total", str manifest.hardware.ramTotal), + ("root_disk", str manifest.hardware.rootDisk), + ("hypervisor", str manifest.hardware.hypervisor)])] + json.pretty ++ "\n" + +/-- Render a benchmark string field as a JSON string, escaped. -/ def jsonString (s : String) : String := - "\"" ++ s ++ "\"" + Lean.Json.renderString s /-- Render one benchmark record as a JSONL row. -/ def BenchRecord.toJsonLine (record : BenchRecord) : String := "{" ++ String.intercalate "," [ + "\"group_key\":" ++ jsonString record.groupKey, + "\"group_title\":" ++ jsonString record.groupTitle, "\"name\":" ++ jsonString record.name, "\"representation\":" ++ jsonString record.representation, "\"method\":" ++ jsonString record.method, @@ -577,8 +865,24 @@ def BenchRecord.toJsonLine (record : BenchRecord) : String := "\"checksum_iterations\":" ++ toString record.checksumIterations, "\"measured_iterations\":" ++ toString record.measuredIterations, "\"total_nanos\":" ++ toString record.totalNanos, - "\"average_nanos\":" ++ toString record.averageNanos, - "\"checksum\":" ++ toString record.checksum + "\"median_nanos\":" ++ toString record.medianNanos, + "\"work_units\":" ++ toString record.workUnits, + "\"digest_class\":" ++ jsonString record.digestClass, + "\"checksum\":" ++ toString record.checksum, + "\"sink_digest\":" ++ toString record.sinkDigest, + "\"sample_count\":" ++ toString record.stats.count, + "\"iters_per_sample\":" ++ toString record.stats.itersPerSample, + "\"unreplicated\":" ++ (if record.stats.unreplicated then "true" else "false"), + "\"min_picos\":" ++ toString record.stats.minPicos, + "\"median_picos\":" ++ toString record.stats.medianPicos, + "\"mean_picos\":" ++ toString record.stats.meanPicos, + "\"p95_picos\":" ++ toString record.stats.p95Picos, + "\"stddev_picos\":" ++ toString record.stats.stddevPicos, + "\"mad_picos\":" ++ toString record.stats.madPicos, + "\"mild_outliers\":" ++ toString record.stats.mildOutliers, + "\"severe_outliers\":" ++ toString record.stats.severeOutliers, + "\"samples_picos\":[" ++ + String.intercalate "," (record.samples.toList.map toString) ++ "]" ] ++ "}" /-- Render all benchmark records as JSONL. -/ @@ -601,11 +905,11 @@ def padRight (s : String) (width : Nat) : String := def padLeft (s : String) (width : Nat) : String := spaces (width - s.length) ++ s -/-- Drop missing optional lines while preserving present ones. -/ -def keepSome : List (Option String) → List String +/-- Drop missing optional entries while preserving present ones. -/ +def keepSome {α : Type*} : List (Option α) → List α | [] => [] - | some line :: lines => line :: keepSome lines - | none :: lines => keepSome lines + | some value :: values => value :: keepSome values + | none :: values => keepSome values /-- Compute the Markdown width required for a result table column. -/ def columnWidth (records : List BenchRecord) @@ -644,7 +948,7 @@ def renderMarkdownTable (columns : List (String × Bool × (BenchRecord → Stri markdownRow headers widths (columns.map (fun _ ↦ false)) :: markdownRow separator widths (columns.map (fun _ ↦ false)) :: rows -/-- Return the shared checksum for a group if all rows have the same checksum. -/ +/-- Return the shared checksum for a list of rows if all of them agree. -/ def matchingChecksum? (records : List BenchRecord) : Option Nat := match records with | [] => none @@ -656,6 +960,23 @@ def matchingChecksum? (records : List BenchRecord) : Option Nat := else none +/-- The digest classes present in a group, in first-appearance order. -/ +def digestClasses (records : List BenchRecord) : List String := + records.foldl (init := []) fun seen record ↦ + if seen.contains record.digestClass then seen else seen ++ [record.digestClass] + +/-- The rows of one digest class. -/ +def recordsInClass (records : List BenchRecord) (cls : String) : List BenchRecord := + records.filter fun record ↦ record.digestClass == cls + +/-- Whether every digest class in a group agrees internally. + +Agreement is required *within* a class, not across the group: a group carrying a +field's `mul` and its `add` has two classes and two digests, and demanding one +digest for both would be demanding that multiplication equal addition. -/ +def classesAgree (records : List BenchRecord) : Bool := + (digestClasses records).all fun cls ↦ (matchingChecksum? (recordsInClass records cls)).isSome + /-- Return a shared string field for a group if all rows agree. -/ def matchingString? (records : List BenchRecord) (field : BenchRecord → String) : Option String := match records with @@ -688,15 +1009,31 @@ def renderSharedNatLine (label : String) (records : List BenchRecord) (field : BenchRecord → Nat) : Option String := (matchingNat? records field).map fun value ↦ "- " ++ label ++ ": `" ++ toString value ++ "`" -/-- Render a short checksum status line for a benchmark group. -/ -def renderChecksumStatus (records : List BenchRecord) : String := - match matchingChecksum? records with - | some checksum => "- Checksum: `" ++ toString checksum ++ "`" - | none => "- Checksum: **ERROR: mismatch detected**" +/-- Render a short checksum status line for a benchmark group. -/-- Return benchmark groups whose rows do not have a shared checksum. -/ +One digest when the group has a single class, and one per class when it has +several, so a multi-comparison group still shows what agreed with what. -/ +def renderChecksumStatus (records : List BenchRecord) : String := + let render (cls : String) : String := + let label := if cls.isEmpty then "" else cls ++ ": " + match matchingChecksum? (recordsInClass records cls) with + | some checksum => label ++ "`" ++ toString checksum ++ "`" + | none => label ++ "**ERROR: mismatch detected**" + "- Checksum: " ++ String.intercalate ", " ((digestClasses records).map render) + +/-- Return benchmark groups in which some digest class does not agree. -/ def checksumMismatchGroups (groups : Array BenchGroup) : List BenchGroup := - groups.toList.filter fun group ↦ (matchingChecksum? group.records.toList).isNone + groups.toList.filter fun group ↦ !classesAgree group.records.toList + +/-- Return benchmark groups whose rows disagree on `workUnits`. + +Rows of one group measure the same problem at the same shape, so a disagreement +here is a mis-specified group rather than something to render around: the +per-unit column would otherwise divide each row by a different denominator and +silently flatten the ratio the group exists to report. -/ +def workUnitsMismatchGroups (groups : Array BenchGroup) : List BenchGroup := + groups.toList.filter fun group ↦ + (matchingNat? group.records.toList (fun record ↦ record.workUnits)).isNone /-- Lookup a rendered implementation label by exact benchmark metadata. -/ def lookupImplementationLabel? : String → List (String × String) → Option String @@ -816,19 +1153,64 @@ def implementationLabelInGroup (records : List BenchRecord) (record : BenchRecor label else if record.field == "BabyBear.Fast.Field" then label ++ " (fast BabyBear)" + else if record.field == "Goldilocks.Fast.Field" then + label ++ " (fast Goldilocks)" else label ++ " (" ++ record.field ++ ")" -/-- Columns rendered in a group result table after shared metadata is lifted out. -/ +/-- Render a record's sample dispersion as a percentage of its median. + +Reads `n=1` where a benchmark could not be replicated at all, `(n=k)` where it +was replicated too few times for the spread to mean much, and `!k` where Tukey +labelled `k` samples as severe outliers. Outliers are labelled, never dropped; +the full per-sample vector is in the JSONL. -/ +def renderSpread (record : BenchRecord) : String := + let stats := record.stats + if stats.count ≤ 1 then + "n=1" + else + let tenths := + if stats.medianPicos = 0 then 0 else 1000 * stats.madPicos / stats.medianPicos + let base := "±" ++ toString (tenths / 10) ++ "." ++ toString (tenths % 10) ++ "%" + let base := if stats.unreplicated then base ++ " (n=" ++ toString stats.count ++ ")" else base + if stats.severeOutliers > 0 then base ++ " !" ++ toString stats.severeOutliers else base + +/-- Per-unit cost column, present only when the group declares work units. + +Picoseconds rather than a chosen unit: a per-unit figure is normally +sub-nanosecond, which is exactly where `chooseTimeUnit` would render it as +`0.000`. Not emitted into the JSONL — that file carries `work_units` and the +full statistics, and a consumer dividing for itself does not inherit the +truncation this column accepts for the sake of a readable table. -/ +def perUnitColumn? (records : List BenchRecord) : + Option (String × Bool × (BenchRecord → String)) := + match matchingNat? records (fun record ↦ record.workUnits) with + | some units => + if units > 1 then + some ("Per unit (ps)", true, fun record ↦ toString (record.stats.medianPicos / units)) + else + none + | none => none + +/-- Columns rendered in a group result table after shared metadata is lifted out. + +Warmup and sample count are columns rather than shared metadata lines because +calibration sizes each row separately: two rows of one group no longer agree on +either, so `matchingNat?` would silently drop both lines from every report. Only +the digest length is still shared by construction. -/ def groupResultColumns (records : List BenchRecord) (totalUnit avgUnit : TimeUnit) : List (String × Bool × (BenchRecord → String)) := [ ("Implementation", false, implementationLabelInGroup records), + ("Warmup", true, fun r ↦ toString r.warmupIterations), ("Iterations", true, fun r ↦ toString r.measuredIterations), + ("Samples", true, fun r ↦ toString r.stats.count), ("Total (" ++ totalUnit.label ++ ")", true, fun r ↦ - formatNanosInUnit totalUnit r.totalNanos), - ("Avg (" ++ avgUnit.label ++ ")", true, fun r ↦ - formatNanosInUnit avgUnit r.averageNanos) + formatNanosInUnitOrAuto totalUnit r.totalNanos), + ("Median (" ++ avgUnit.label ++ ")", true, fun r ↦ + formatNanosInUnitOrAuto avgUnit r.medianNanos) + ] ++ keepSome [perUnitColumn? records] ++ [ + ("Spread", true, renderSpread) ] /-- Shared metadata rendered before each benchmark group result table. -/ @@ -837,7 +1219,6 @@ def renderGroupMetadata (records : List BenchRecord) (totalUnit : TimeUnit) : Li renderSharedStringLine "Representation" records (fun r ↦ r.representation), renderSharedStringLine "Field / configuration" records (fun r ↦ r.field), renderSharedStringLine "Input shape" records (fun r ↦ r.inputShape), - renderSharedNatLine "Warmup iterations" records (fun r ↦ r.warmupIterations), renderSharedNatLine "Checksum iterations" records (fun r ↦ r.checksumIterations) ] ++ [ "- Total group time: `" ++ formatNanosWithUnit totalUnit (totalGroupNanos records) ++ @@ -850,7 +1231,7 @@ def renderGroupResults (group : BenchGroup) : List String := let records := group.records.toList let groupTotal := totalGroupNanos records let totalUnit := chooseTimeUnit (groupTotal :: records.map fun r ↦ r.totalNanos) - let avgUnit := chooseTimeUnit (records.map fun r ↦ r.averageNanos) + let avgUnit := chooseTimeUnit (records.map fun r ↦ r.medianNanos) [ "### " ++ group.title, "", @@ -917,4 +1298,40 @@ def renderMarkdown (hardware : RunnerHardware) (preset : BenchPreset) (groups : "" ] ++ (groups.toList.map renderGroupResults).foldr List.append []) ++ "\n" +/-- Render one row of the validation report. -/ +private def validationRow (group : BenchGroup) : String := + let records := group.records.toList + let status := + if classesAgree records then + let digests := (digestClasses records).filterMap fun cls ↦ + (matchingChecksum? (recordsInClass records cls)).map toString + "agree | `" ++ String.intercalate "`, `" digests ++ "`" + else + "**MISMATCH** | -" + "| `" ++ group.groupKey ++ "` | " ++ toString group.records.size ++ " | " ++ status ++ " |" + +/-- Render the report for a `--validate-only` run. + +Show whether each group's implementations agree on a digest. Ordinary workloads +are untimed; harness checks may retain samples through `forceTiming`. This report +omits timing values, including any harness samples. -/ +def renderValidationMarkdown (preset : BenchPreset) (groups : Array BenchGroup) : String := + let mismatches := checksumMismatchGroups groups + String.intercalate "\n" ([ + "# Benchmark Validation Report", + "", + "- Seed: `" ++ toString seed ++ "`", + "- Preset: `" ++ preset.name ++ "`", + "- Groups checked: `" ++ toString groups.size ++ "`", + "- Mismatched groups: `" ++ toString mismatches.length ++ "`", + "", + "Ordinary workloads were not timed; harness checks may still record samples.", + "Every implementation in a group is run over the same inputs and must agree", + "on a digest; a disagreement means one of them is wrong. Run the benchmark", + "workflow for workload timings.", + "", + "| Group | Rows | Implementations | Digest |", + "| ----- | ---: | --------------- | ------ |" + ] ++ groups.toList.map validationRow) ++ "\n" + end CompPolyBench diff --git a/bench/CompPolyBench/Fields/Arith.lean b/bench/CompPolyBench/Fields/Arith.lean new file mode 100644 index 00000000..3044d7bb --- /dev/null +++ b/bench/CompPolyBench/Fields/Arith.lean @@ -0,0 +1,531 @@ +/- +Copyright (c) 2026 CompPoly Contributors. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Derek Sorensen +-/ +module + +public import CompPolyBench.Fields.Inputs +public import CompPolyBench.Harness.Chain + +/-! +# Base-field arithmetic benchmarks + +`mul`, `add`, `inv` and `pow` over the four small prime fields a STARK prover +spends its time in — KoalaBear, BabyBear, Mersenne31 and Goldilocks — and +`mul` over the three eight-limb pairing scalar fields, BN254, BLS12-381 and +BLS12-377. Each group runs the canonical `ZMod` implementation beside the +verified native-word one on the same inputs, so the group digest cross-checks +the two. + +## Why these are chains + +A field multiplication is one or two nanoseconds and the harness's own +per-iteration cost is about 1.8 ns, so a row that performs the operation once +per timed iteration reports the harness. Every row here instead performs the +operation `chainRounds` times per iteration and divides by `workUnits`; see +`CompPolyBench.Harness.Chain` for why the chain touches no array and allocates +nothing. + +The chain is seeded from an operand pool indexed by the iteration counter. That +costs one boxed array read per `chainRounds` operations — under a tenth of a +percent — and buys two things: the body genuinely depends on `i`, so it is +neither cached as a closed term nor hoisted out of the sample loop, and the +untimed digest sees a whole pool of inputs rather than one. + +## Scaffolding other field groups share + +`ChainRep`, `chainLatencyRow`, `chainThroughputRow`, `runBinOpGroup` and +`runUnOpGroup` are the chained-group scaffolding, and are used from the binary +tower groups as well. They live here rather than beside `Harness/Chain.lean` +because they need `runTimedSpec`, which `Harness/Chain.lean` deliberately does +not import. + +## Latency and throughput + +Reported separately, as Plonky3's `benchmark_mul_latency` and +`benchmark_mul_throughput` are, because they are different numbers and a prover +is bounded by different ones in different places. The latency row chains the +operation against a fixed operand, so each step waits on the last. The +throughput row runs ten independent accumulators, so the pipeline can overlap +them. `mul` and `add` carry both; `inv` and `pow` carry latency only, matching +the peer, and use a shorter chain because one operation already costs tens of +multiplications. +-/ + +public section + +open CompPoly + +namespace CompPolyBench + +/-- Operand-pool size for the chained field benchmarks, and so their digest length. -/ +def fieldPoolSize : Nat := 64 + +/-- Rounds in an inversion or exponentiation chain. + +One `unrollBlock`, twenty times shorter than `chainRounds`, because a single +inversion is tens of multiplications and the canonical `ZMod` row is three +orders of magnitude slower again. Still long enough that the harness floor is +under a thousandth of the row. -/ +def expChainRounds : Nat := unrollBlock + +/-- Latency-chain depth for the eight-limb scalar fields. + +A quarter of `chainRounds`: a 256-bit Montgomery multiply is an order of +magnitude more than a 32-bit one and the canonical row three further orders, +so the full depth would put a single iteration past the sample budget. Chosen +to be both a whole number of `unrollBlock`s and `throughputWidth` times a whole +number of `throughputUnroll`s, so the latency and throughput rows of a group +agree on `workUnits` — which the group check enforces. -/ +def heavyChainRounds : Nat := 5 * unrollBlock + +/-- Throughput-chain depth pairing with `heavyChainRounds`. -/ +def heavyThroughputRounds : Nat := 8 * throughputUnroll + +/-- Exponent used by the `pow` benchmarks. + +A 31-bit exponent of Hamming weight 16, so the binary ladder performs 30 +squarings and 15 multiplications on every field. The same exponent everywhere, +so the fields are comparable; nothing about it is field-specific. -/ +def powExponent : Nat := 0x5A5A5A5A + +/-- One representation of a field, as a chained benchmark row needs it. + +The operation itself is deliberately *not* a field of this structure. It is +passed as a direct argument to the `@[specialize]` runners below, so the +compiler sees a known function at the chain's call site; through a structure +field it would be an indirect call per operation, which for a one-nanosecond +multiply is the whole measurement. -/ +structure ChainRep (F : Type) where + /-- Representation label for the report, such as `ZMod` or `UInt32`. -/ + representation : String + /-- Carrier name for the report, such as `KoalaBear.Fast.Field`. -/ + field : String + /-- Row-name suffix distinguishing this representation, `zmod` or `fast`. -/ + suffix : String + /-- Nonzero operands, drawn from the group's random stream. -/ + pool : Array F + /-- The chain's fixed second operand, and the fallback for an out-of-range read. -/ + constant : F + /-- Digest for the untimed validation pass. -/ + checksum : F → Nat + /-- Cheap digest for the timed region. -/ + sink : F → UInt64 + +/-- Replace zeros, so an inversion chain never meets the field's junk value. + +`(0 : F)⁻¹` is `0` in Lean, so a pool containing zero would give an inversion +chain a fixed point and measure one input rather than the pool. -/ +def nonzeroPool {F : Type} [Zero F] [One F] [DecidableEq F] (xs : Array F) : Array F := + xs.map fun x ↦ if x = 0 then 1 else x + +/-- Input-shape label shared by every row of a chained field group. -/ +def chainShape (rounds : Nat) : String := + s!"{fieldPoolSize} seeds, {rounds}-operation chains" + +/-- Time a dependent chain of `op` over one representation. -/ +@[specialize] def chainLatencyRow {F : Type} (fieldTag opTag method cls : String) + (rounds : Nat) (rep : ChainRep F) (op : F → F) (preset : BenchPreset) : IO BenchRecord := + runTimedSpec + { name := s!"{fieldTag}-{opTag}-{rep.suffix}", representation := rep.representation, + method := method, field := rep.field, inputShape := chainShape rounds, + digestIterations := digestPeriod fieldPoolSize, workUnits := latencyUnits rounds, + digestClass := cls } + preset + (fun i ↦ chainLatency op rounds (rep.pool.getD (i % fieldPoolSize) rep.constant)) + rep.checksum (sink := rep.sink) + +/-- Time ten independent chains of `op` over one representation. -/ +@[specialize] def chainThroughputRow {F : Type} (fieldTag opTag method cls : String) + (rounds : Nat) (rep : ChainRep F) (op : F → F → F) (preset : BenchPreset) : IO BenchRecord := + runTimedSpec + { name := s!"{fieldTag}-{opTag}-{rep.suffix}", representation := rep.representation, + method := method, field := rep.field, inputShape := chainShape (throughputUnitsOf rounds), + digestIterations := digestPeriod fieldPoolSize, workUnits := throughputUnitsOf rounds, + digestClass := cls } + preset + (fun i ↦ + let seed (k : Nat) : F := rep.pool.getD ((i + k) % fieldPoolSize) rep.constant + chainThroughput op op rounds + (seed 0) (seed 1) (seed 2) (seed 3) (seed 4) + (seed 5) (seed 6) (seed 7) (seed 8) (seed 9)) + rep.checksum (sink := rep.sink) + +/-- Time a binary field operation, both shapes, over both representations. -/ +@[specialize] def runBinOpGroup {S F : Type} (groupKey title fieldTag opTag : String) + (latencyRounds tputRounds : Nat) + (slow : ChainRep S) (slowOp : S → S → S) + (fast : ChainRep F) (fastOp : F → F → F) + (preset : BenchPreset) : IO BenchGroup := do + -- Bound to locals, not read through `slow.constant` inside the lambda. A + -- projection there is lifted into the operation itself, so the chain pays a + -- `lean_ctor_get` and an unbox per operation. Out-of-order execution hides + -- that behind a long operation — KoalaBear `mul` did not move — but not + -- behind a short one: KoalaBear `add` went from 1400 ps to 1116 ps. + let slowConstant := slow.constant + let fastConstant := fast.constant + let slowLatency ← chainLatencyRow fieldTag opTag s!"{opTag} (latency)" "latency" + latencyRounds slow (fun x ↦ slowOp x slowConstant) preset + let fastLatency ← chainLatencyRow fieldTag opTag s!"{opTag} (latency)" "latency" + latencyRounds fast (fun x ↦ fastOp x fastConstant) preset + let slowThroughput ← chainThroughputRow fieldTag opTag s!"{opTag} (throughput)" + "throughput" tputRounds slow slowOp preset + let fastThroughput ← chainThroughputRow fieldTag opTag s!"{opTag} (throughput)" + "throughput" tputRounds fast fastOp preset + pure { groupKey := groupKey, title := title, + records := #[slowLatency, fastLatency, slowThroughput, fastThroughput] } + +/-- Time a unary field operation as a dependent chain over both representations. + +The chained step is `op (x + constant)` rather than `op x`: iterating a bare +inversion alternates between two values, which measures one input rather than +the pool. The addition it costs is a percent of an inversion. -/ +@[specialize] def runUnOpGroup {S F : Type} (groupKey title fieldTag opTag : String) + (slowMethod fastMethod : String) + (slow : ChainRep S) (slowAdd : S → S → S) (slowOp : S → S) + (fast : ChainRep F) (fastAdd : F → F → F) (fastOp : F → F) + (preset : BenchPreset) : IO BenchGroup := do + let slowConstant := slow.constant + let fastConstant := fast.constant + let slowRecord ← chainLatencyRow fieldTag opTag slowMethod "" expChainRounds slow + (fun x ↦ slowOp (slowAdd x slowConstant)) preset + let fastRecord ← chainLatencyRow fieldTag opTag fastMethod "" expChainRounds fast + (fun x ↦ fastOp (fastAdd x fastConstant)) preset + pure { groupKey := groupKey, title := title, records := #[slowRecord, fastRecord] } + +/-! ## The four fields + +Each field contributes a pair of `ChainRep`s drawn from one operand pool, so +the canonical and native-word rows of every group see the same inputs. The +group runners below are written out one per operation rather than generated, +because the operation has to reach `chainLatency` as a statically known +function: routed through a closure it becomes an indirect call per operation, +which for these fields is more than the operation. -/ + +/-- KoalaBear operands, canonical and native-word, from one pool. -/ +private def koalaBearReps (gen : StdGen) : + ChainRep KoalaBear.Field × ChainRep KoalaBear.Fast.Field × StdGen := + let (values, gen) := (koalaBearArray fieldPoolSize false).run gen + let pool := nonzeroPool values + let fastPool := koalaBearFastArray pool + ({ representation := "ZMod", field := "KoalaBear.Field", suffix := "zmod", + pool := pool, constant := pool.getD 0 1, checksum := checksumKoalaBear, + sink := fun x ↦ natSink (checksumKoalaBear x) }, + { representation := "UInt32", field := "KoalaBear.Fast.Field", suffix := "fast", + pool := fastPool, constant := fastPool.getD 0 1, checksum := checksumKoalaBearFast, + sink := fun x ↦ natSink (checksumKoalaBearFast x) }, + gen) + +/-- BabyBear operands, canonical and native-word, from one pool. -/ +private def babyBearReps (gen : StdGen) : + ChainRep BabyBear.Field × ChainRep BabyBear.Fast.Field × StdGen := + let (values, gen) := (babyBearArray fieldPoolSize false).run gen + let pool := nonzeroPool values + let fastPool := babyBearFastArray pool + ({ representation := "ZMod", field := "BabyBear.Field", suffix := "zmod", + pool := pool, constant := pool.getD 0 1, checksum := checksumBabyBear, + sink := fun x ↦ natSink (checksumBabyBear x) }, + { representation := "UInt32", field := "BabyBear.Fast.Field", suffix := "fast", + pool := fastPool, constant := fastPool.getD 0 1, checksum := checksumBabyBearFast, + sink := fun x ↦ natSink (checksumBabyBearFast x) }, + gen) + +/-- Mersenne31 operands, canonical and native-word, from one pool. -/ +private def mersenne31Reps (gen : StdGen) : + ChainRep Mersenne31.Field × ChainRep Mersenne31.Fast.Field × StdGen := + let (values, gen) := (mersenne31Array fieldPoolSize false).run gen + let pool := nonzeroPool values + let fastPool := mersenne31FastArray pool + ({ representation := "ZMod", field := "Mersenne31.Field", suffix := "zmod", + pool := pool, constant := pool.getD 0 1, checksum := checksumZMod, + sink := fun x ↦ natSink (checksumZMod x) }, + { representation := "UInt32", field := "Mersenne31.Fast.Field", suffix := "fast", + pool := fastPool, constant := fastPool.getD 0 1, checksum := checksumMersenne31Fast, + sink := fun x ↦ natSink (checksumMersenne31Fast x) }, + gen) + +/-- Goldilocks operands, canonical and native-word, from one pool. + +Both rows need an explicit sink: the modulus exceeds `2 ^ 63`, so a `Nat` +digest allocates a bignum on most inputs. -/ +private def goldilocksReps (gen : StdGen) : + ChainRep Goldilocks.Field × ChainRep Goldilocks.Fast.Field × StdGen := + let (values, gen) := (zmodArray Goldilocks.fieldSize fieldPoolSize false).run gen + let pool := nonzeroPool values + let fastPool := goldilocksFastArray pool + ({ representation := "ZMod", field := "Goldilocks.Field", suffix := "zmod", + pool := pool, constant := pool.getD 0 1, checksum := checksumZMod, + sink := sinkZMod }, + { representation := "UInt64", field := "Goldilocks.Fast.Field", suffix := "fast", + pool := fastPool, constant := fastPool.getD 0 1, checksum := checksumGoldilocksFast, + sink := sinkGoldilocksFast }, + gen) + +/-! ### KoalaBear -/ + +/-- Time KoalaBear multiplication. -/ +private def runKoalaBearMul (preset : BenchPreset) (gen : StdGen) : + IO (BenchGroup × StdGen) := do + let (slow, fast, gen) := koalaBearReps gen + let group ← runBinOpGroup "fields-koalabear-mul" "KoalaBear multiplication" + "koalabear" "mul" chainRounds throughputRounds slow (· * ·) fast Montgomery.Native32.mul preset + pure (group, gen) + +/-- Time KoalaBear addition. -/ +private def runKoalaBearAdd (preset : BenchPreset) (gen : StdGen) : + IO (BenchGroup × StdGen) := do + let (slow, fast, gen) := koalaBearReps gen + let group ← runBinOpGroup "fields-koalabear-add" "KoalaBear addition" + "koalabear" "add" chainRounds throughputRounds slow (· + ·) fast Montgomery.Native32.add preset + pure (group, gen) + +/-- Time KoalaBear inversion. -/ +private def runKoalaBearInv (preset : BenchPreset) (gen : StdGen) : + IO (BenchGroup × StdGen) := do + let (slow, fast, gen) := koalaBearReps gen + let group ← runUnOpGroup "fields-koalabear-inv" "KoalaBear inversion" + "koalabear" "inv" "inv (ZMod.inv)" "inv (Fermat chain)" slow (· + ·) (·⁻¹) + fast Montgomery.Native32.add Montgomery.Native32.inv preset + pure (group, gen) + +/-- Time KoalaBear exponentiation. -/ +private def runKoalaBearPow (preset : BenchPreset) (gen : StdGen) : + IO (BenchGroup × StdGen) := do + let (slow, fast, gen) := koalaBearReps gen + let group ← runUnOpGroup "fields-koalabear-pow" "KoalaBear exponentiation" + "koalabear" "pow" "pow (binary ladder)" "pow (binary ladder)" + slow (· + ·) (npowBinRec powExponent ·) + fast Montgomery.Native32.add + (Montgomery.Native32.pow · powExponent) preset + pure (group, gen) + +/-! ### BabyBear -/ + +/-- Time BabyBear multiplication. -/ +private def runBabyBearMul (preset : BenchPreset) (gen : StdGen) : + IO (BenchGroup × StdGen) := do + let (slow, fast, gen) := babyBearReps gen + let group ← runBinOpGroup "fields-babybear-mul" "BabyBear multiplication" + "babybear" "mul" chainRounds throughputRounds slow (· * ·) fast Montgomery.Native32.mul preset + pure (group, gen) + +/-- Time BabyBear addition. -/ +private def runBabyBearAdd (preset : BenchPreset) (gen : StdGen) : + IO (BenchGroup × StdGen) := do + let (slow, fast, gen) := babyBearReps gen + let group ← runBinOpGroup "fields-babybear-add" "BabyBear addition" + "babybear" "add" chainRounds throughputRounds slow (· + ·) fast Montgomery.Native32.add preset + pure (group, gen) + +/-- Time BabyBear inversion. -/ +private def runBabyBearInv (preset : BenchPreset) (gen : StdGen) : + IO (BenchGroup × StdGen) := do + let (slow, fast, gen) := babyBearReps gen + let group ← runUnOpGroup "fields-babybear-inv" "BabyBear inversion" + "babybear" "inv" "inv (ZMod.inv)" "inv (Fermat chain)" slow (· + ·) (·⁻¹) + fast Montgomery.Native32.add Montgomery.Native32.inv preset + pure (group, gen) + +/-- Time BabyBear exponentiation. -/ +private def runBabyBearPow (preset : BenchPreset) (gen : StdGen) : + IO (BenchGroup × StdGen) := do + let (slow, fast, gen) := babyBearReps gen + let group ← runUnOpGroup "fields-babybear-pow" "BabyBear exponentiation" + "babybear" "pow" "pow (binary ladder)" "pow (binary ladder)" + slow (· + ·) (npowBinRec powExponent ·) + fast Montgomery.Native32.add + (Montgomery.Native32.pow · powExponent) preset + pure (group, gen) + +/-! ### Mersenne31 -/ + +/-- Time Mersenne31 multiplication. -/ +private def runMersenne31Mul (preset : BenchPreset) (gen : StdGen) : + IO (BenchGroup × StdGen) := do + let (slow, fast, gen) := mersenne31Reps gen + let group ← runBinOpGroup "fields-mersenne31-mul" "Mersenne31 multiplication" + "mersenne31" "mul" chainRounds throughputRounds slow (· * ·) fast Mersenne31.Fast.mul preset + pure (group, gen) + +/-- Time Mersenne31 addition. -/ +private def runMersenne31Add (preset : BenchPreset) (gen : StdGen) : + IO (BenchGroup × StdGen) := do + let (slow, fast, gen) := mersenne31Reps gen + let group ← runBinOpGroup "fields-mersenne31-add" "Mersenne31 addition" + "mersenne31" "add" chainRounds throughputRounds slow (· + ·) fast Mersenne31.Fast.add preset + pure (group, gen) + +/-- Time Mersenne31 inversion. -/ +private def runMersenne31Inv (preset : BenchPreset) (gen : StdGen) : + IO (BenchGroup × StdGen) := do + let (slow, fast, gen) := mersenne31Reps gen + let group ← runUnOpGroup "fields-mersenne31-inv" "Mersenne31 inversion" + "mersenne31" "inv" "inv (ZMod.inv)" "inv (Fermat chain)" slow (· + ·) (·⁻¹) + fast Mersenne31.Fast.add Mersenne31.Fast.inv preset + pure (group, gen) + +/-- Time Mersenne31 exponentiation. -/ +private def runMersenne31Pow (preset : BenchPreset) (gen : StdGen) : + IO (BenchGroup × StdGen) := do + let (slow, fast, gen) := mersenne31Reps gen + let group ← runUnOpGroup "fields-mersenne31-pow" "Mersenne31 exponentiation" + "mersenne31" "pow" "pow (binary ladder)" "pow (binary ladder)" + slow (· + ·) (npowBinRec powExponent ·) + fast Mersenne31.Fast.add (Mersenne31.Fast.pow · powExponent) preset + pure (group, gen) + +/-! ### Goldilocks -/ + +/-- Time Goldilocks multiplication. -/ +private def runGoldilocksMul (preset : BenchPreset) (gen : StdGen) : + IO (BenchGroup × StdGen) := do + let (slow, fast, gen) := goldilocksReps gen + let group ← runBinOpGroup "fields-goldilocks-mul" "Goldilocks multiplication" + "goldilocks" "mul" chainRounds throughputRounds slow (· * ·) fast Goldilocks.Fast.mul preset + pure (group, gen) + +/-- Time Goldilocks addition. -/ +private def runGoldilocksAdd (preset : BenchPreset) (gen : StdGen) : + IO (BenchGroup × StdGen) := do + let (slow, fast, gen) := goldilocksReps gen + let group ← runBinOpGroup "fields-goldilocks-add" "Goldilocks addition" + "goldilocks" "add" chainRounds throughputRounds slow (· + ·) fast Goldilocks.Fast.add preset + pure (group, gen) + +/-- Time Goldilocks inversion. -/ +private def runGoldilocksInv (preset : BenchPreset) (gen : StdGen) : + IO (BenchGroup × StdGen) := do + let (slow, fast, gen) := goldilocksReps gen + let group ← runUnOpGroup "fields-goldilocks-inv" "Goldilocks inversion" + "goldilocks" "inv" "inv (ZMod.inv)" "inv (Fermat chain)" slow (· + ·) (·⁻¹) + fast Goldilocks.Fast.add Goldilocks.Fast.inv preset + pure (group, gen) + +/-- Time Goldilocks exponentiation. -/ +private def runGoldilocksPow (preset : BenchPreset) (gen : StdGen) : + IO (BenchGroup × StdGen) := do + let (slow, fast, gen) := goldilocksReps gen + let group ← runUnOpGroup "fields-goldilocks-pow" "Goldilocks exponentiation" + "goldilocks" "pow" "pow (binary ladder)" "pow (binary ladder)" + slow (· + ·) (npowBinRec powExponent ·) + fast Goldilocks.Fast.add (Goldilocks.Fast.pow · powExponent) preset + pure (group, gen) + +/-! ### Eight-limb pairing scalar fields + +`mul` only. Inversion over these carriers already has a group of its own in +`Fields/Montgomery.lean`, which compares three algorithms rather than two +representations. Both rows need an explicit sink: the canonical value is a +254- to 255-bit bignum, and `sinkMont64x8` reads two limbs instead of +reassembling one. -/ + +/-- BN254 scalar operands, canonical and eight-limb, from one pool. -/ +private def bn254Reps (gen : StdGen) : + ChainRep BN254.ScalarField × ChainRep BN254.Fast.ScalarField × StdGen := + let (values, gen) := (zmodArray BN254.scalarFieldSize fieldPoolSize false).run gen + let pool := nonzeroPool values + let fastPool := bn254FastArray pool + ({ representation := "ZMod", field := "BN254.ScalarField", suffix := "zmod", + pool := pool, constant := pool.getD 0 1, checksum := checksumZMod, sink := sinkZMod }, + { representation := "Limbs8", field := "BN254.Fast.ScalarField", suffix := "fast", + pool := fastPool, constant := fastPool.getD 0 1, checksum := checksumBn254Fast, + sink := sinkMont64x8 }, + gen) + +/-- BLS12-381 scalar operands, canonical and eight-limb, from one pool. -/ +private def bls12_381Reps (gen : StdGen) : + ChainRep BLS12_381.ScalarField × ChainRep BLS12_381.Fast.ScalarField × StdGen := + let (values, gen) := (zmodArray BLS12_381.scalarFieldSize fieldPoolSize false).run gen + let pool := nonzeroPool values + let fastPool := bls12_381FastArray pool + ({ representation := "ZMod", field := "BLS12_381.ScalarField", suffix := "zmod", + pool := pool, constant := pool.getD 0 1, checksum := checksumZMod, sink := sinkZMod }, + { representation := "Limbs8", field := "BLS12_381.Fast.ScalarField", suffix := "fast", + pool := fastPool, constant := fastPool.getD 0 1, checksum := checksumBls12_381Fast, + sink := sinkMont64x8 }, + gen) + +/-- BLS12-377 scalar operands, canonical and eight-limb, from one pool. -/ +private def bls12_377Reps (gen : StdGen) : + ChainRep BLS12_377.ScalarField × ChainRep BLS12_377.Fast.ScalarField × StdGen := + let (values, gen) := (zmodArray BLS12_377.scalarFieldSize fieldPoolSize false).run gen + let pool := nonzeroPool values + let fastPool := bls12_377FastArray pool + ({ representation := "ZMod", field := "BLS12_377.ScalarField", suffix := "zmod", + pool := pool, constant := pool.getD 0 1, checksum := checksumZMod, sink := sinkZMod }, + { representation := "Limbs8", field := "BLS12_377.Fast.ScalarField", suffix := "fast", + pool := fastPool, constant := fastPool.getD 0 1, checksum := checksumBls12_377Fast, + sink := sinkMont64x8 }, + gen) + +/-- Time BN254 scalar multiplication. -/ +private def runBn254Mul (preset : BenchPreset) (gen : StdGen) : + IO (BenchGroup × StdGen) := do + let (slow, fast, gen) := bn254Reps gen + let group ← runBinOpGroup "fields-bn254-mul" "BN254 scalar multiplication" + "bn254" "mul" heavyChainRounds heavyThroughputRounds + slow (· * ·) fast Montgomery.Native64x8.FastField.mul preset + pure (group, gen) + +/-- Time BLS12-381 scalar multiplication. -/ +private def runBls12_381Mul (preset : BenchPreset) (gen : StdGen) : + IO (BenchGroup × StdGen) := do + let (slow, fast, gen) := bls12_381Reps gen + let group ← runBinOpGroup "fields-bls12-381-mul" "BLS12-381 scalar multiplication" + "bls12-381" "mul" heavyChainRounds heavyThroughputRounds + slow (· * ·) fast Montgomery.Native64x8.FastField.mul preset + pure (group, gen) + +/-- Time BLS12-377 scalar multiplication. -/ +private def runBls12_377Mul (preset : BenchPreset) (gen : StdGen) : + IO (BenchGroup × StdGen) := do + let (slow, fast, gen) := bls12_377Reps gen + let group ← runBinOpGroup "fields-bls12-377-mul" "BLS12-377 scalar multiplication" + "bls12-377" "mul" heavyChainRounds heavyThroughputRounds + slow (· * ·) fast Montgomery.Native64x8.FastField.mul preset + pure (group, gen) + +/-- Registry entries for the base-field arithmetic benchmarks. -/ +def fieldArithTasks : List BenchTask := [ + BenchTask.fromGroupRunner ⟨"fields-koalabear-mul", "KoalaBear multiplication"⟩ + runKoalaBearMul, + BenchTask.fromGroupRunner ⟨"fields-koalabear-add", "KoalaBear addition"⟩ + runKoalaBearAdd, + BenchTask.fromGroupRunner ⟨"fields-koalabear-inv", "KoalaBear inversion"⟩ + runKoalaBearInv, + BenchTask.fromGroupRunner ⟨"fields-koalabear-pow", "KoalaBear exponentiation"⟩ + runKoalaBearPow, + BenchTask.fromGroupRunner ⟨"fields-babybear-mul", "BabyBear multiplication"⟩ + runBabyBearMul, + BenchTask.fromGroupRunner ⟨"fields-babybear-add", "BabyBear addition"⟩ + runBabyBearAdd, + BenchTask.fromGroupRunner ⟨"fields-babybear-inv", "BabyBear inversion"⟩ + runBabyBearInv, + BenchTask.fromGroupRunner ⟨"fields-babybear-pow", "BabyBear exponentiation"⟩ + runBabyBearPow, + BenchTask.fromGroupRunner ⟨"fields-mersenne31-mul", "Mersenne31 multiplication"⟩ + runMersenne31Mul, + BenchTask.fromGroupRunner ⟨"fields-mersenne31-add", "Mersenne31 addition"⟩ + runMersenne31Add, + BenchTask.fromGroupRunner ⟨"fields-mersenne31-inv", "Mersenne31 inversion"⟩ + runMersenne31Inv, + BenchTask.fromGroupRunner ⟨"fields-mersenne31-pow", "Mersenne31 exponentiation"⟩ + runMersenne31Pow, + BenchTask.fromGroupRunner ⟨"fields-goldilocks-mul", "Goldilocks multiplication"⟩ + runGoldilocksMul, + BenchTask.fromGroupRunner ⟨"fields-goldilocks-add", "Goldilocks addition"⟩ + runGoldilocksAdd, + BenchTask.fromGroupRunner ⟨"fields-goldilocks-inv", "Goldilocks inversion"⟩ + runGoldilocksInv, + BenchTask.fromGroupRunner ⟨"fields-goldilocks-pow", "Goldilocks exponentiation"⟩ + runGoldilocksPow, + BenchTask.fromGroupRunner ⟨"fields-bn254-mul", "BN254 scalar multiplication"⟩ + runBn254Mul, + BenchTask.fromGroupRunner ⟨"fields-bls12-381-mul", "BLS12-381 scalar multiplication"⟩ + runBls12_381Mul, + BenchTask.fromGroupRunner ⟨"fields-bls12-377-mul", "BLS12-377 scalar multiplication"⟩ + runBls12_377Mul +] + +end CompPolyBench diff --git a/bench/CompPolyBench/Fields/Binary/AdditiveNTT/Impl.lean b/bench/CompPolyBench/Fields/Binary/AdditiveNTT/Impl.lean index bfc6e270..b6fb8c20 100644 --- a/bench/CompPolyBench/Fields/Binary/AdditiveNTT/Impl.lean +++ b/bench/CompPolyBench/Fields/Binary/AdditiveNTT/Impl.lean @@ -17,13 +17,6 @@ open ConcreteBinaryTower namespace CompPolyBench -/-- Benchmark group metadata for `CompPoly.Fields.Binary.AdditiveNTT.Impl`. -/ -def additiveNttGroupInfos : List BenchGroupInfo := [ - ⟨"additive-ntt-btf3-l2-r2", "Additive NTT BTF3 l=2 R_rate=2"⟩, - ⟨"additive-ntt-btf3-l4-r2", "Additive NTT BTF3 l=4 R_rate=2"⟩, - ⟨"additive-ntt-btf4-l7-r2", "Additive NTT BTF4 l=7 R_rate=2"⟩ -] - /-- Checksum all output values from a `BTF₃` additive NTT benchmark. -/ private def checksumBtf3Output {n : Nat} (output : Fin (2 ^ n) → AdditiveNTT.BTF₃) : Nat := (List.finRange (2 ^ n)).foldl @@ -36,10 +29,45 @@ private def checksumBtf3OutputArray {n : Nat} (output : Array AdditiveNTT.BTF₃ /-- Checksum a concrete binary-tower additive NTT output array. -/ private def checksumConcreteBtfOutputArray {k n : Nat} (output : Array (ConcreteBTField k)) : Nat := + let values := AdditiveNTT.arrayToFinFunction (2 ^ n) output (List.finRange (2 ^ n)).foldl - (fun acc i ↦ - mixChecksum acc - (checksumConcreteBtf ((AdditiveNTT.arrayToFinFunction (2 ^ n) output) i))) 0 + (fun acc i ↦ mixChecksum acc (checksumConcreteBtf (values i))) 0 + +/-! ### Timed-region sinks + +The reference row returns `Fin (2 ^ n) → α` — a *function*, so an output value +does not exist until an index is applied — while the fast row returns a +materialised `Array`. Realising the whole result is therefore part of the +reference row's work and not part of the fast row's, so both sinks fold over +every output position: sampling a few positions would leave the reference row +computing a fraction of what the fast row computes and the ratio would be +meaningless. + +What these avoid, relative to the `Nat` digests above, is the bignum +`mixChecksum` and the per-iteration `List.finRange (2 ^ n)` materialisation. The +fold itself is a handful of machine instructions per output. -/ + +/-- Fold every position of a `Fin`-indexed output into a sink accumulator. -/ +@[inline] private def sinkFinAll {m : Nat} (toNat : α → Nat) (output : Fin m → α) : UInt64 := + Nat.fold m (fun i h acc ↦ sinkStep acc (natSink (toNat (output ⟨i, h⟩)))) 0 + +/-- Fold every element of an output array into a sink accumulator. -/ +@[inline] private def sinkArrayAll (toNat : α → Nat) (output : Array α) : UInt64 := + output.foldl (fun acc x ↦ sinkStep acc (natSink (toNat x))) 0 + +/-- Sink a `BTF₃` additive NTT output function. -/ +@[inline] private def sinkBtf3Output {n : Nat} (output : Fin (2 ^ n) → AdditiveNTT.BTF₃) : + UInt64 := + sinkFinAll checksumBtf3 output + +/-- Sink a `BTF₃` additive NTT output array. -/ +@[inline] private def sinkBtf3OutputArray (output : Array AdditiveNTT.BTF₃) : UInt64 := + sinkArrayAll checksumBtf3 output + +/-- Sink a concrete binary-tower additive NTT output array. -/ +@[inline] private def sinkConcreteBtfOutputArray {k : Nat} (output : Array (ConcreteBTField k)) : + UInt64 := + sinkArrayAll checksumConcreteBtf output /-- Run an additive NTT over `BTF₃`. -/ private def runBtf3Ntt (ℓ R_rate : Nat) (h_ℓ_add_R_rate : ℓ + R_rate < 2 ^ 3) @@ -81,7 +109,7 @@ private def runConcreteBtfNttFast (k ℓ R_rate : Nat) /-- Run one additive NTT benchmark pair over `BTF₃`. -/ private def runAdditiveNttCase (ℓ R_rate : Nat) (h_ℓ_add_R_rate : ℓ + R_rate < 2 ^ 3) - (key currentName fastName : String) (warmup measured fastMeasured : Nat) + (key currentName fastName : String) (preset : BenchPreset) (gen : StdGen) : IO (BenchGroup × StdGen) := do let inputSize := 2 ^ ℓ @@ -91,17 +119,19 @@ private def runAdditiveNttCase (ℓ R_rate : Nat) (h_ℓ_add_R_rate : ℓ + R_ra fun i ↦ ConcreteBinaryTower.fromNat (k := 3) (values.getD i.val 0) let fieldLabel := s!"ConcreteBTField 0 -> BTF3, l={ℓ}, R_rate={R_rate}" let inputShape := s!"{inputSize} input coeffs, {outputSize} output evals" - let checksumIterations := groupChecksumIterations measured [fastMeasured] - let currentRecord ← runTimed - currentName "computableAdditiveNTT" "computableAdditiveNTT" - fieldLabel inputShape preset warmup measured - (fun _ ↦ runBtf3Ntt ℓ R_rate h_ℓ_add_R_rate input) - (checksumBtf3Output (n := ℓ + R_rate)) (checksumIterations := checksumIterations) - let fastRecord ← runTimed - fastName "computableAdditiveNTTFast" "computableAdditiveNTTFast" - fieldLabel inputShape preset warmup fastMeasured - (fun _ ↦ runBtf3NttFast ℓ R_rate h_ℓ_add_R_rate input) - (checksumBtf3OutputArray (n := ℓ + R_rate)) (checksumIterations := checksumIterations) + let checksumIterations := digestPeriod 1 + let currentRecord ← runTimedSpec + { name := currentName, representation := "computableAdditiveNTT", + method := "computableAdditiveNTT", field := fieldLabel, inputShape := inputShape, + digestIterations := checksumIterations } + preset (fun _ ↦ runBtf3Ntt ℓ R_rate h_ℓ_add_R_rate input) + (checksumBtf3Output (n := ℓ + R_rate)) (sink := sinkBtf3Output) + let fastRecord ← runTimedSpec + { name := fastName, representation := "computableAdditiveNTTFast", + method := "computableAdditiveNTTFast", field := fieldLabel, inputShape := inputShape, + digestIterations := checksumIterations } + preset (fun _ ↦ runBtf3NttFast ℓ R_rate h_ℓ_add_R_rate input) + (checksumBtf3OutputArray (n := ℓ + R_rate)) (sink := sinkBtf3OutputArray) pure ({ groupKey := key, title := s!"Additive NTT BTF3 l={ℓ} R_rate={R_rate}", @@ -110,7 +140,7 @@ private def runAdditiveNttCase (ℓ R_rate : Nat) (h_ℓ_add_R_rate : ℓ + R_ra /-- Run one fast-only additive NTT benchmark over a concrete binary-tower field. -/ private def runAdditiveNttFastLargeCase (k ℓ R_rate : Nat) (h_ℓ_add_R_rate : ℓ + R_rate < 2 ^ k) (key fastName : String) - (warmup measured : Nat) (preset : BenchPreset) (gen : StdGen) : + (preset : BenchPreset) (gen : StdGen) : IO (BenchGroup × StdGen) := do let inputSize := 2 ^ ℓ let outputSize := 2 ^ (ℓ + R_rate) @@ -119,11 +149,12 @@ private def runAdditiveNttFastLargeCase (k ℓ R_rate : Nat) fun i ↦ ConcreteBinaryTower.fromNat (k := k) (values.getD i.val 0) let fieldLabel := s!"ConcreteBTField 0 -> BTF{k}, l={ℓ}, R_rate={R_rate}" let inputShape := s!"{inputSize} input coeffs, {outputSize} output evals" - let fastRecord ← runTimed - fastName "computableAdditiveNTTFast" "computableAdditiveNTTFast" - fieldLabel inputShape preset warmup measured - (fun _ ↦ runConcreteBtfNttFast k ℓ R_rate h_ℓ_add_R_rate input) - (checksumConcreteBtfOutputArray (k := k) (n := ℓ + R_rate)) + let fastRecord ← runTimedSpec + { name := fastName, representation := "computableAdditiveNTTFast", + method := "computableAdditiveNTTFast", field := fieldLabel, inputShape := inputShape, + digestIterations := digestPeriod 1 } + preset (fun _ ↦ runConcreteBtfNttFast k ℓ R_rate h_ℓ_add_R_rate input) + (checksumConcreteBtfOutputArray (k := k) (n := ℓ + R_rate)) (sink := sinkConcreteBtfOutputArray) pure ({ groupKey := key, title := s!"Additive NTT BTF{k} l={ℓ} R_rate={R_rate}", @@ -132,30 +163,21 @@ private def runAdditiveNttFastLargeCase (k ℓ R_rate : Nat) /-- Run the `BTF₃` additive NTT benchmark with `ℓ = 2` and `R_rate = 2`. -/ private def runAdditiveNttBtf3L2R2 (preset : BenchPreset) (gen : StdGen) : IO (BenchGroup × StdGen) := do - let warmup := additiveNttWarmupIterations preset - let measured := additiveNttMeasuredIterations preset - let fastMeasured := preset.selectNat 13000 2000 400 runAdditiveNttCase 2 2 (by omega) - "additive-ntt-btf3-l2-r2" "additive-ntt-btf3" "additive-ntt-btf3-fast" - warmup measured fastMeasured preset gen + "additive-ntt-btf3-l2-r2" "additive-ntt-btf3" "additive-ntt-btf3-fast" preset gen /-- Run the `BTF₃` additive NTT benchmark with `ℓ = 4` and `R_rate = 2`. -/ private def runAdditiveNttBtf3L4R2 (preset : BenchPreset) (gen : StdGen) : IO (BenchGroup × StdGen) := do - let warmup := preset.selectNat 2 1 0 - let measured := preset.selectNat 10 1 1 - let fastMeasured := preset.selectNat 2000 300 60 runAdditiveNttCase 4 2 (by omega) "additive-ntt-btf3-l4-r2" "additive-ntt-btf3-l4-r2" - "additive-ntt-btf3-l4-r2-fast" warmup measured fastMeasured preset gen + "additive-ntt-btf3-l4-r2-fast" preset gen /-- Run the `BTF₄` fast-only additive NTT benchmark with `ℓ = 7` and `R_rate = 2`. -/ private def runAdditiveNttBtf4L7R2 (preset : BenchPreset) (gen : StdGen) : IO (BenchGroup × StdGen) := do - let warmup := preset.selectNat 1 1 0 - let measured := preset.selectNat 10 1 1 runAdditiveNttFastLargeCase 4 7 2 (by omega) - "additive-ntt-btf4-l7-r2" "additive-ntt-btf4-l7-r2-fast" warmup measured preset gen + "additive-ntt-btf4-l7-r2" "additive-ntt-btf4-l7-r2-fast" preset gen /-- Runnable additive-NTT benchmark tasks. -/ def additiveNttTasks : List BenchTask := [ @@ -170,9 +192,4 @@ def additiveNttTasks : List BenchTask := [ runAdditiveNttBtf4L7R2 ] -/-- Run selected additive NTT benchmarks. -/ -def runAdditiveNtt (preset : BenchPreset) (selection : BenchSelection) (gen : StdGen) : - IO (Array BenchGroup × StdGen) := do - runSelectedTasks additiveNttTasks preset selection gen - end CompPolyBench diff --git a/bench/CompPolyBench/Fields/Binary/Tower.lean b/bench/CompPolyBench/Fields/Binary/Tower.lean new file mode 100644 index 00000000..77fdd624 --- /dev/null +++ b/bench/CompPolyBench/Fields/Binary/Tower.lean @@ -0,0 +1,88 @@ +/- +Copyright (c) 2026 CompPoly Contributors. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Georgios Raikos +-/ +module + +public import CompPolyBench.Common +public import CompPoly.Fields.Binary.Tower.Fast + +/-! +# Binary tower field benchmarks + +Times GF(2^128) multiplication and inversion, `BitVec` spec vs packed-word +implementation, cross-checked by the group checksum. Sub-microsecond rows include the +harness's fixed per-iteration cost (roughly 0.4 us), so they are regression +indicators, not operation latencies. +-/ + +public section + +open ConcreteBinaryTower + +namespace CompPolyBench + +/-- Input-shape label shared by the tower benchmarks. -/ +private def towerShape : String := "64 random 128-bit elements, pairwise" + +/-- Limb-fold checksum for packed tower elements; avoids building the 128-bit value. -/ +def checksumFastBT128 (x : Fast.FastBT128) : Nat := x.lo.toNat ^^^ x.hi.toNat + +/-- The same limb fold on the concrete representation. -/ +def checksumConcreteBt128 (x : ConcreteBTField 7) : Nat := + BitVec.toNat x % 2 ^ 64 ^^^ BitVec.toNat x >>> 64 + +/-- Pairwise operand sampler over a fixed pool. -/ +@[inline] private def towerSampler {E : Type} (xs : Array E) (one : E) : Nat → E × E := + fun i ↦ (xs.getD (i % xs.size) one, xs.getD ((i + 17) % xs.size) one) + +/-- Time one GF(2^128) operation over the spec and the packed implementation. -/ +@[specialize] private def runTowerGroup (groupKey title method : String) + (concreteOp : ConcreteBTField 7 → ConcreteBTField 7 → ConcreteBTField 7) + (fastOp : Fast.FastBT128 → Fast.FastBT128 → Fast.FastBT128) + (preset : BenchPreset) (gen : StdGen) : IO (BenchGroup × StdGen) := do + let (values, gen) := (randomNatArray 64 (2 ^ 128 - 1)).run gen + let concreteSample := towerSampler + (values.map fun n ↦ (fromNat n : ConcreteBTField 7)) (fromNat 1) + let fastSample := towerSampler (values.map Fast.FastBT128.ofNat) (.ofNat 1) + let checksumIterations := digestPeriod values.size + let concreteRecord ← runTimedSpec + { name := "tower-bt128", representation := "ConcreteBTField", + method := (method ++ " (ConcreteBTField)"), field := "GF(2^128)", inputShape := towerShape, + digestIterations := checksumIterations } + preset (fun i ↦ let (a, b) := concreteSample i; concreteOp a b) + checksumConcreteBt128 + let fastRecord ← runTimedSpec + { name := "tower-bt128-fast", representation := "FastBT128", + method := (method ++ " (FastBT128)"), field := "GF(2^128)", inputShape := towerShape, + digestIterations := checksumIterations } + preset (fun i ↦ let (a, b) := fastSample i; fastOp a b) checksumFastBT128 + pure ({ groupKey := groupKey, title := title, + records := #[concreteRecord, fastRecord] }, gen) + +/-- Run the GF(2^128) multiplication benchmark. -/ +private def runTowerMul (preset : BenchPreset) (gen : StdGen) : + IO (BenchGroup × StdGen) := do + runTowerGroup "fields-tower-bt128-mul" "Binary tower multiplication (GF(2^128))" "mul" + concrete_mul Fast.FastBT128.mul + preset gen + +/-- Run the GF(2^128) inversion benchmark. -/ +private def runTowerInv (preset : BenchPreset) (gen : StdGen) : + IO (BenchGroup × StdGen) := do + runTowerGroup "fields-tower-bt128-inv" "Binary tower inversion (GF(2^128))" "inv" + (fun a _ ↦ concrete_inv a) (fun a _ ↦ a.inv) + preset gen + +/-- Registry entries for the binary tower benchmarks. -/ +def towerTasks : List BenchTask := [ + BenchTask.fromGroupRunner + ⟨"fields-tower-bt128-mul", "Binary tower multiplication (GF(2^128))"⟩ + runTowerMul, + BenchTask.fromGroupRunner + ⟨"fields-tower-bt128-inv", "Binary tower inversion (GF(2^128))"⟩ + runTowerInv +] + +end CompPolyBench diff --git a/bench/CompPolyBench/Fields/Binary/Tower/Scalar.lean b/bench/CompPolyBench/Fields/Binary/Tower/Scalar.lean new file mode 100644 index 00000000..8da7b900 --- /dev/null +++ b/bench/CompPolyBench/Fields/Binary/Tower/Scalar.lean @@ -0,0 +1,108 @@ +/- +Copyright (c) 2026 CompPoly Contributors. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Derek Sorensen +-/ +module + +public import CompPolyBench.Fields.Arith +public import CompPoly.Fields.Binary.Tower.FastDefs + +/-! +# Binary tower scalar kernels: table-driven against recursive + +`Tower/FastDefs.lean` carries each of `GF(2^8)` multiplication, `GF(2^64)` +multiplication and `GF(2^64)` inversion twice: once as the recursive tower +construction and once driven by a precomputed table. The two are proved equal, +and nothing measured which is faster — which is the whole reason the table +exists. + +All six are `UInt64 → UInt64` or `UInt64 → UInt64 → UInt64` on a bare machine +word, so they are the cleanest possible chain targets: no carrier to unbox and +no allocation. `fields-tower-bt128-mul` and `-inv` remain the group for the +128-bit packed representation against its `BitVec` spec; these are a level +below that. + +The table rows load a `ByteArray` at module initialisation, so their first +iterations touch cold memory. The calibration ramp doubles as warmup and is +long enough that this does not reach the samples. +-/ + +public section + +open ConcreteBinaryTower + +namespace CompPolyBench + +/-- Word-level operands for the tower's scalar kernels. + +`GF(2^64)` addition is `xor`, so `nonzeroPool` is not needed: the inversion +chain's step already mixes with a fixed operand, and `inv64 0 = 0` is only +reachable if the seed and the constant coincide. -/ +private def towerWordRep (suffix representation field : String) (pool : Array UInt64) : + ChainRep UInt64 := + { representation := representation, field := field, suffix := suffix, + pool := pool, constant := pool.getD 0 1, + checksum := fun x ↦ x.toNat, sink := u64Sink } + +/-- Draw a pool of nonzero words below `2 ^ bits` from the group's stream. + +The bound is not cosmetic. `mul8T_eq_mul8` (`Tower/Fast.lean:441`) holds only +for operands below `2 ^ 8`, because `mul8T` indexes a 65536-entry table with +`(a <<< 8) + b`; fed a full machine word it reads out of range, `get!` returns +zero, and the group reports a digest mismatch and a meaningless time. The +level-6 kernels take the whole word. -/ +private def towerWordPool (bits : Nat) (gen : StdGen) : Array UInt64 × StdGen := + let (values, gen) := (randomNatArray fieldPoolSize (2 ^ bits - 2)).run gen + (values.map fun n ↦ UInt64.ofNat (n + 1), gen) + +/-- Time `GF(2^8)` multiplication, recursive against table-driven. -/ +private def runTowerMul8 (preset : BenchPreset) (gen : StdGen) : + IO (BenchGroup × StdGen) := do + let (pool, gen) := towerWordPool 8 gen + let group ← runBinOpGroup "fields-tower-bt8-mul" + "Binary tower multiplication (GF(2^8)), table against recursive" + "tower-bt8" "mul" chainRounds throughputRounds + (towerWordRep "rec" "UInt64" "GF(2^8) recursive" pool) Fast.mul8 + (towerWordRep "table" "UInt64" "GF(2^8) table" pool) Fast.mul8T preset + pure (group, gen) + +/-- Time `GF(2^64)` multiplication, recursive against table-driven. -/ +private def runTowerMul64 (preset : BenchPreset) (gen : StdGen) : + IO (BenchGroup × StdGen) := do + let (pool, gen) := towerWordPool 64 gen + let group ← runBinOpGroup "fields-tower-bt64-mul" + "Binary tower multiplication (GF(2^64)), table against recursive" + "tower-bt64" "mul" chainRounds throughputRounds + (towerWordRep "rec" "UInt64" "GF(2^64) recursive" pool) Fast.mul64 + (towerWordRep "table" "UInt64" "GF(2^64) table" pool) Fast.mul64T preset + pure (group, gen) + +/-- Time `GF(2^64)` inversion, recursive against table-driven. -/ +private def runTowerInv64 (preset : BenchPreset) (gen : StdGen) : + IO (BenchGroup × StdGen) := do + let (pool, gen) := towerWordPool 64 gen + let group ← runUnOpGroup "fields-tower-bt64-inv-word" + "Binary tower inversion (GF(2^64)), table against recursive" + "tower-bt64" "inv" "inv (recursive)" "inv (table)" + (towerWordRep "rec" "UInt64" "GF(2^64) recursive" pool) (· ^^^ ·) Fast.inv64 + (towerWordRep "table" "UInt64" "GF(2^64) table" pool) (· ^^^ ·) Fast.inv64T preset + pure (group, gen) + +/-- Registry entries for the tower's scalar kernels. -/ +def towerScalarTasks : List BenchTask := [ + BenchTask.fromGroupRunner + ⟨"fields-tower-bt8-mul", + "Binary tower multiplication (GF(2^8)), table against recursive"⟩ + runTowerMul8, + BenchTask.fromGroupRunner + ⟨"fields-tower-bt64-mul", + "Binary tower multiplication (GF(2^64)), table against recursive"⟩ + runTowerMul64, + BenchTask.fromGroupRunner + ⟨"fields-tower-bt64-inv-word", + "Binary tower inversion (GF(2^64)), table against recursive"⟩ + runTowerInv64 +] + +end CompPolyBench diff --git a/bench/CompPolyBench/Fields/Extension.lean b/bench/CompPolyBench/Fields/Extension.lean index 08a7af3c..93551b12 100644 --- a/bench/CompPolyBench/Fields/Extension.lean +++ b/bench/CompPolyBench/Fields/Extension.lean @@ -57,17 +57,8 @@ def checksumKoalaBearExt5 (x : KoalaBear.Ext5) : Nat := def checksumKoalaBearExt6 (x : KoalaBear.Ext6) : Nat := x.coeffs.toArray.foldl (fun acc z ↦ acc + z.val) 0 -/-- Benchmark group metadata for the extension fields. -/ -def extensionGroupInfos : List BenchGroupInfo := [ - ⟨"fields-extension-koalabear-ext4-mul", "Degree-4 extension multiplication (KoalaBear)"⟩, - ⟨"fields-extension-koalabear-ext4-inv", "Degree-4 extension inversion (KoalaBear)"⟩, - ⟨"fields-extension-babybear-ext4-mul", "Degree-4 extension multiplication (BabyBear)"⟩, - ⟨"fields-extension-babybear-ext4-inv", "Degree-4 extension inversion (BabyBear)"⟩, - ⟨"fields-extension-koalabear-ext5-mul", "Degree-5 extension multiplication (KoalaBear)"⟩, - ⟨"fields-extension-koalabear-ext5-inv", "Degree-5 extension inversion (KoalaBear)"⟩, - ⟨"fields-extension-koalabear-ext6-mul", "Degree-6 extension multiplication (KoalaBear)"⟩, - ⟨"fields-extension-koalabear-ext6-inv", "Degree-6 extension inversion (KoalaBear)"⟩ -] +/-- Operand-pool size of `extSampler`, and so the period of every body here. -/ +private def extPoolSize : Nat := 64 /-- Time one extension operation over a field-specific sample, packaged as a single-record group. @@ -76,19 +67,20 @@ Time one extension operation over a field-specific sample, packaged as a single- -/ private def runExtOp {E : Type} (groupKey title name method fieldName shape : String) (checksum : E → Nat) (sample : Nat → E × E) (op : E → E → E) - (measured : Nat) (preset : BenchPreset) (gen : StdGen) : IO (BenchGroup × StdGen) := do - let record ← runTimed name "Extension.Ext" method fieldName shape preset - (warmupIterations preset) measured - (fun i ↦ let (a, b) := sample i; op a b) checksum + (preset : BenchPreset) (gen : StdGen) : IO (BenchGroup × StdGen) := do + let record ← runTimedSpec + { name := name, representation := "Extension.Ext", method := method, field := fieldName, + inputShape := shape, digestIterations := digestPeriod extPoolSize } + preset (fun i ↦ let (a, b) := sample i; op a b) checksum pure ({ groupKey := groupKey, title := title, records := #[record] }, gen) -/-- Build the pairwise operand sampler for an extension over a `ZMod` base field. -/ -private def extSampler {F : Type*} [Field F] [Fintype F] {P : ExtensionParams F} +/-- Build a pairwise sampler by filling extension coordinates from the supplied values. -/ +private def extSampler {F : Type*} [Ring F] {P : ExtensionParams F} (values : Array F) : Nat → Ext P × Ext P := let elem (i : Nat) : Ext P := Ext.ofFn fun j ↦ values.getD ((i * P.d + j.val) % values.size) 0 - let xs : Array (Ext P) := Array.ofFn (n := 64) fun i ↦ elem i.val - fun i ↦ (xs.getD (i % 64) 1, xs.getD ((i + 17) % 64) 1) + let xs : Array (Ext P) := Array.ofFn (n := extPoolSize) fun i ↦ elem i.val + fun i ↦ (xs.getD (i % extPoolSize) 1, xs.getD ((i + 17) % extPoolSize) 1) /-- Run the KoalaBear degree-4 multiplication benchmark. -/ private def runKoalaBearExt4Mul (preset : BenchPreset) (gen : StdGen) : @@ -98,7 +90,7 @@ private def runKoalaBearExt4Mul (preset : BenchPreset) (gen : StdGen) : "Degree-4 extension multiplication (KoalaBear)" "extension-mul" "mul" "KoalaBear.Ext4" (extShape 4) checksumKoalaBearExt4 (extSampler (P := KoalaBear.ext4Params.toExtensionParams) values) (· * ·) - (preset.selectNat 200000 30000 6000) preset gen + preset gen /-- Run the KoalaBear degree-4 inversion benchmark. -/ private def runKoalaBearExt4Inv (preset : BenchPreset) (gen : StdGen) : @@ -109,7 +101,7 @@ private def runKoalaBearExt4Inv (preset : BenchPreset) (gen : StdGen) : (extShape 4) checksumKoalaBearExt4 (extSampler (P := KoalaBear.ext4Params.toExtensionParams) values) (fun a _ ↦ a⁻¹) - (preset.selectNat 2000 300 60) preset gen + preset gen /-- Run the BabyBear degree-4 multiplication benchmark. -/ private def runBabyBearExt4Mul (preset : BenchPreset) (gen : StdGen) : @@ -119,7 +111,7 @@ private def runBabyBearExt4Mul (preset : BenchPreset) (gen : StdGen) : "Degree-4 extension multiplication (BabyBear)" "extension-mul" "mul" "BabyBear.Ext4" (extShape 4) checksumBabyBearExt4 (extSampler (P := BabyBear.ext4Params.toExtensionParams) values) (· * ·) - (preset.selectNat 200000 30000 6000) preset gen + preset gen /-- Run the BabyBear degree-4 inversion benchmark. -/ private def runBabyBearExt4Inv (preset : BenchPreset) (gen : StdGen) : @@ -130,7 +122,7 @@ private def runBabyBearExt4Inv (preset : BenchPreset) (gen : StdGen) : (extShape 4) checksumBabyBearExt4 (extSampler (P := BabyBear.ext4Params.toExtensionParams) values) (fun a _ ↦ a⁻¹) - (preset.selectNat 2000 300 60) preset gen + preset gen /-- Run the KoalaBear degree-5 multiplication benchmark. -/ private def runKoalaBearExt5Mul (preset : BenchPreset) (gen : StdGen) : @@ -140,7 +132,7 @@ private def runKoalaBearExt5Mul (preset : BenchPreset) (gen : StdGen) : "Degree-5 extension multiplication (KoalaBear)" "extension-mul" "mul" "KoalaBear.Ext5" (extShape 5) checksumKoalaBearExt5 (extSampler (P := KoalaBear.ext5Params) values) (· * ·) - (preset.selectNat 200000 30000 6000) preset gen + preset gen /-- Run the KoalaBear degree-5 inversion benchmark. -/ private def runKoalaBearExt5Inv (preset : BenchPreset) (gen : StdGen) : @@ -150,7 +142,7 @@ private def runKoalaBearExt5Inv (preset : BenchPreset) (gen : StdGen) : "Degree-5 extension inversion (KoalaBear)" "extension-inv" "inv (Fermat)" "KoalaBear.Ext5" (extShape 5) checksumKoalaBearExt5 (extSampler (P := KoalaBear.ext5Params) values) (fun a _ ↦ a⁻¹) - (preset.selectNat 2000 300 60) preset gen + preset gen /-- Run the KoalaBear degree-6 multiplication benchmark. -/ private def runKoalaBearExt6Mul (preset : BenchPreset) (gen : StdGen) : @@ -160,7 +152,7 @@ private def runKoalaBearExt6Mul (preset : BenchPreset) (gen : StdGen) : "Degree-6 extension multiplication (KoalaBear)" "extension-mul" "mul" "KoalaBear.Ext6" (extShape 6) checksumKoalaBearExt6 (extSampler (P := KoalaBear.ext6Params) values) (· * ·) - (preset.selectNat 200000 30000 6000) preset gen + preset gen /-- Run the KoalaBear degree-6 inversion benchmark. -/ private def runKoalaBearExt6Inv (preset : BenchPreset) (gen : StdGen) : @@ -170,7 +162,7 @@ private def runKoalaBearExt6Inv (preset : BenchPreset) (gen : StdGen) : "Degree-6 extension inversion (KoalaBear)" "extension-inv" "inv (Fermat)" "KoalaBear.Ext6" (extShape 6) checksumKoalaBearExt6 (extSampler (P := KoalaBear.ext6Params) values) (fun a _ ↦ a⁻¹) - (preset.selectNat 2000 300 60) preset gen + preset gen /-- Registry entries for the extension benchmarks. -/ def extensionTasks : List BenchTask := [ diff --git a/bench/CompPolyBench/Fields/Inputs.lean b/bench/CompPolyBench/Fields/Inputs.lean new file mode 100644 index 00000000..b588e003 --- /dev/null +++ b/bench/CompPolyBench/Fields/Inputs.lean @@ -0,0 +1,107 @@ +/- +Copyright (c) 2026 CompPoly Contributors. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Derek Sorensen +-/ +module + +public import CompPolyBench.Common +public import CompPoly.Fields.Mersenne31 +public import CompPoly.Fields.BN254 +public import CompPoly.Fields.BLS12_381 +public import CompPoly.Fields.BLS12_377 + +/-! +# Field inputs, checksums and sinks + +Per-field benchmark scaffolding for the concrete fields that +`CompPolyBench.Common` does not already carry: Mersenne31 and the three +eight-limb Montgomery scalar fields. + +These live here rather than in `CompPolyBench.Common` because that module is +imported by every benchmark, and the field modules below are needed by a +handful. The division is by import cost, not by kind. + +Three things are provided per field, and the distinction between the last two +is the one that goes wrong: + +* an **input generator**, drawing from the group's random stream; +* a **checksum** into `Nat`, folded by the untimed validation pass and compared + across the implementations in a group, so it must be a faithful function of + the canonical value; +* a **sink** into `UInt64`, folded inside the timed region purely to keep the + result live. A sink is never compared with anything, so it may be as lossy as + it likes — but it must be *cheap*, and comparably cheap for every row of a + group, or the group's ratio measures the sinks. +-/ + +public section + +open CompPoly + +namespace CompPolyBench + +/-! ## Mersenne31 + +`2 ^ 31 - 1`, the field Plonky3 uses for its Circle STARK. The canonical value +fits a machine word, so the `Nat` checksum doubles as a sink and no explicit +`sink :=` is needed. -/ + +/-- Generate Mersenne31 coefficients with the same shape controls as `zmodArray`. -/ +def mersenne31Array (size : Nat) (sparse : Bool) : StateM StdGen (Array Mersenne31.Field) := + zmodArray Mersenne31.fieldSize size sparse + +/-- Convert Mersenne31 field inputs to the native-word representation. -/ +def mersenne31FastArray (xs : Array Mersenne31.Field) : Array Mersenne31.Fast.Field := + xs.map Mersenne31.Fast.ofField + +/-- Convert a fast Mersenne31 element to a checksum word. + +The carrier is an `abbrev` for a `Subtype`, so dot notation would resolve to +`Subtype.toNat`; call the field's own `toNat` directly. -/ +def checksumMersenne31Fast (x : Mersenne31.Fast.Field) : Nat := + Mersenne31.Fast.toNat x + +/-! ## Eight-limb Montgomery scalar fields + +BN254, BLS12-381 and BLS12-377. The canonical value is a 254- to 255-bit +bignum, so both representations need an explicit sink: `sinkZMod` for the +canonical side and `sinkMont64x8` for the fast one. -/ + +/-- Convert BN254 field inputs to the native eight-limb representation. -/ +def bn254FastArray (xs : Array BN254.ScalarField) : Array BN254.Fast.ScalarField := + xs.map BN254.Fast.ofField + +/-- Convert a fast BN254 element to a checksum word. -/ +def checksumBn254Fast (x : BN254.Fast.ScalarField) : Nat := + x.toNat + +/-- Convert BLS12-381 field inputs to the native eight-limb representation. -/ +def bls12_381FastArray (xs : Array BLS12_381.ScalarField) : + Array BLS12_381.Fast.ScalarField := + xs.map BLS12_381.Fast.ofField + +/-- Convert a fast BLS12-381 element to a checksum word. -/ +def checksumBls12_381Fast (x : BLS12_381.Fast.ScalarField) : Nat := + x.toNat + +/-- Convert BLS12-377 field inputs to the native eight-limb representation. -/ +def bls12_377FastArray (xs : Array BLS12_377.ScalarField) : + Array BLS12_377.Fast.ScalarField := + xs.map BLS12_377.Fast.ofField + +/-- Convert a fast BLS12-377 element to a checksum word. -/ +def checksumBls12_377Fast (x : BLS12_377.Fast.ScalarField) : Nat := + x.toNat + +/-- Sink an eight-limb Montgomery element by two of its limbs. + +`toNat` reassembles a 256-bit bignum, which costs more than the multiplication +under test. The limbs are already unboxed `UInt64` fields of the carrier, so +this is two loads and an exclusive or. Lossy by construction; the untimed +digest is what establishes correctness. -/ +@[inline] def sinkMont64x8 {modulus : Nat} [Montgomery.Native64x8.Mont64x8Field modulus] + (x : Montgomery.Native64x8.FastField modulus) : UInt64 := + (Subtype.val x).l0 ^^^ (Subtype.val x).l7 + +end CompPolyBench diff --git a/bench/CompPolyBench/Fields/Montgomery.lean b/bench/CompPolyBench/Fields/Montgomery.lean index 2bbf65dd..6c9ff10b 100644 --- a/bench/CompPolyBench/Fields/Montgomery.lean +++ b/bench/CompPolyBench/Fields/Montgomery.lean @@ -28,37 +28,27 @@ namespace CompPolyBench /-- Input-shape label shared by the scalar inversion benchmarks. -/ private def scalarInvShape : String := "256 random elements" -/-- Benchmark group metadata for scalar-field inversion. -/ -def montgomeryInvGroupInfos : List BenchGroupInfo := [ - ⟨"fields-mont64x8-bn254-inv", "Scalar-field inversion (BN254)"⟩, - ⟨"fields-mont64x8-bls12-381-inv", "Scalar-field inversion (BLS12-381)"⟩, - ⟨"fields-mont64x8-bls12-377-inv", "Scalar-field inversion (BLS12-377)"⟩ -] - /-- Time the three inversion implementations of one scalar field as a single group. -/ private def runScalarInv (modulus : Nat) [Mont64x8Field modulus] [GcdData modulus] (groupKey title fieldName fastFieldName : String) - (zmodBudget gcdBudget fermatBudget : BenchPreset → Nat) (preset : BenchPreset) (gen : StdGen) : IO (BenchGroup × StdGen) := do let (values, gen) := (zmodArray modulus 256 false).run gen let fastValues := values.map FastField.ofField - let warmup := warmupIterations preset - let zmodMeasured := zmodBudget preset - let gcdMeasured := gcdBudget preset - let fermatMeasured := fermatBudget preset - let checksumIterations := groupChecksumIterations zmodMeasured [gcdMeasured, fermatMeasured] - let zmodRecord ← runTimed "scalar-inv-xgcd" "ZMod" "inv (xgcd)" fieldName - scalarInvShape preset warmup zmodMeasured - (fun i ↦ (values.getD (i % values.size) 1)⁻¹) - checksumZMod (checksumIterations := checksumIterations) - let gcdRecord ← runTimed "scalar-inv-gcd" "Mont64x8" "inv (binary GCD)" fastFieldName - scalarInvShape preset warmup gcdMeasured - (fun i ↦ (fastValues.getD (i % fastValues.size) 1).invGcd) - (fun x ↦ x.toNat) (checksumIterations := checksumIterations) - let fermatRecord ← runTimed "scalar-inv-fermat" "Mont64x8" "inv (Fermat)" fastFieldName - scalarInvShape preset warmup fermatMeasured - (fun i ↦ (fastValues.getD (i % fastValues.size) 1).inv) - (fun x ↦ x.toNat) (checksumIterations := checksumIterations) + let checksumIterations := digestPeriod values.size + let zmodRecord ← runTimedSpec + { name := "scalar-inv-xgcd", representation := "ZMod", method := "inv (xgcd)", + field := fieldName, inputShape := scalarInvShape, digestIterations := checksumIterations } + preset (fun i ↦ (values.getD (i % values.size) 1)⁻¹) checksumZMod + let gcdRecord ← runTimedSpec + { name := "scalar-inv-gcd", representation := "Mont64x8", method := "inv (binary GCD)", + field := fastFieldName, inputShape := scalarInvShape, digestIterations := checksumIterations } + preset (fun i ↦ (fastValues.getD (i % fastValues.size) 1).invGcd) + (fun x ↦ x.toNat) + let fermatRecord ← runTimedSpec + { name := "scalar-inv-fermat", representation := "Mont64x8", method := "inv (Fermat)", + field := fastFieldName, inputShape := scalarInvShape, digestIterations := checksumIterations } + preset (fun i ↦ (fastValues.getD (i % fastValues.size) 1).inv) + (fun x ↦ x.toNat) pure ({ groupKey := groupKey, title := title, records := #[zmodRecord, gcdRecord, fermatRecord] }, gen) @@ -67,24 +57,21 @@ private def runBn254ScalarInv (preset : BenchPreset) (gen : StdGen) : IO (BenchGroup × StdGen) := do runScalarInv BN254.scalarFieldSize "fields-mont64x8-bn254-inv" "Scalar-field inversion (BN254)" "BN254.ScalarField" "BN254.Fast.ScalarField" - (fun p ↦ p.selectNat 20000 3000 600) (fun p ↦ p.selectNat 100000 15000 3000) - (fun p ↦ p.selectNat 24000 3600 720) preset gen + preset gen /-- Run the BLS12-381 scalar inversion benchmark. -/ private def runBls12_381ScalarInv (preset : BenchPreset) (gen : StdGen) : IO (BenchGroup × StdGen) := do runScalarInv BLS12_381.scalarFieldSize "fields-mont64x8-bls12-381-inv" "Scalar-field inversion (BLS12-381)" "BLS12_381.ScalarField" "BLS12_381.Fast.ScalarField" - (fun p ↦ p.selectNat 20000 3000 600) (fun p ↦ p.selectNat 100000 15000 3000) - (fun p ↦ p.selectNat 24000 3600 720) preset gen + preset gen /-- Run the BLS12-377 scalar inversion benchmark. -/ private def runBls12_377ScalarInv (preset : BenchPreset) (gen : StdGen) : IO (BenchGroup × StdGen) := do runScalarInv BLS12_377.scalarFieldSize "fields-mont64x8-bls12-377-inv" "Scalar-field inversion (BLS12-377)" "BLS12_377.ScalarField" "BLS12_377.Fast.ScalarField" - (fun p ↦ p.selectNat 20000 3000 600) (fun p ↦ p.selectNat 100000 15000 3000) - (fun p ↦ p.selectNat 24000 3600 720) preset gen + preset gen /-- Registry entries for the scalar-field inversion benchmarks. -/ def montgomeryInvTasks : List BenchTask := [ diff --git a/bench/CompPolyBench/Harness/Budget.lean b/bench/CompPolyBench/Harness/Budget.lean new file mode 100644 index 00000000..b58dbeba --- /dev/null +++ b/bench/CompPolyBench/Harness/Budget.lean @@ -0,0 +1,197 @@ +/- +Copyright (c) 2026 CompPoly Contributors. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Derek Sorensen +-/ +module + +public import CompPolyBench.Harness.Sample + +/-! +# Benchmark Measurement Budgets + +Sizing a benchmark from a wall-clock budget instead of a written-down iteration +count. + +The suite used to carry one hand-tuned iteration count per benchmark per preset. +A count is the wrong unit: it is not comparable between two rows of the same +table, it goes stale as the code it measures gets faster, and choosing one for a +new benchmark is guesswork that has to be redone on every machine. A budget is +comparable, and the harness can work the count out for itself. + +The scheme is the one `BENCHMARKING.md` §6.3 describes: + +1. a geometric ramp times 1, 2, 4, … iterations until `warmupNanos` of work has + accumulated, and doubles as the benchmark's warmup; +2. the per-iteration cost estimate from that ramp sizes one sample to + `sampleNanos`; +3. `sampleCount` samples are collected, subject to a total `measureNanos`. + +Two properties are deliberate. The cost estimate comes from the ramp's **last +step alone** rather than its accumulated total, because the early steps run cold +and would bias the estimate high, which would size samples short and inflate the +dispersion the suite exists to report. And `sampleCount` is fixed once the plan +is chosen: no sample is skipped because the clock has run on. Re-checking +elapsed time between samples would make the count depend on how busy the machine +happened to be, which is a worse trade than the seconds it would save. +-/ + +public section + +namespace CompPolyBench + +/-- Wall-clock budgets for one benchmark row. + +Replaces the per-benchmark iteration counts. Every field is nanoseconds except +`sampleCount`. -/ +structure BenchBudget where + /-- Work to accumulate in the calibration ramp before measuring. The ramp is + also the benchmark's warmup, so this is the warmup budget. -/ + warmupNanos : Nat + /-- How long one timed sample should take. + + Deliberately the same at every preset. A sample is a mean over + `itersPerSample` iterations, so raising that count averages dispersion away; if + this varied by preset then `--small` and `--large` would report structurally + different spread for identical code and the reported spread would stop being + comparable between them. It is a clock-resolution knob, not a quality knob: + the two clock reads bounding a sample cost tens of nanoseconds, which is + negligible against any value here. -/ + sampleNanos : Nat + /-- Samples to collect when `measureNanos` allows it. The one honest quality + axis, so this is what a preset mainly varies. -/ + sampleCount : Nat + /-- Total time one row may spend inside timed regions. + + A second ceiling, and a necessary one: `sampleNanos * sampleCount` is a small + fraction of a second, so without a separate total the workloads costing + seconds per iteration could never be replicated at all. A target rather than a + guarantee — the first iteration always runs to completion. -/ + measureNanos : Nat +deriving Inhabited + +/-- Outcome of the calibration ramp. -/ +structure Calibration where + /-- Per-iteration cost in picoseconds, taken from the ramp's last step only. -/ + picosPerIteration : Nat + /-- Iterations the ramp executed. These are the row's warmup. -/ + iterations : Nat + /-- Nanoseconds the ramp spent. -/ + nanos : Nat + /-- Sink accumulator carried out of the ramp, to be threaded into the timed + samples so neither loop can be eliminated. -/ + sink : UInt64 +deriving Inhabited + +/-- Doublings the calibration ramp may perform. + +Structural rather than a tuning knob: the wall-clock stop condition fires first +for any body costing more than a fraction of a nanosecond, so this exists only to +bound the loop. `2 ^ 40` iterations of even a one-nanosecond body is over a +minute, so the limit is never the binding constraint. -/ +def calibrationRampLimit : Nat := 40 + +/-- Time a benchmark body over a geometric ramp, and estimate its per-iteration +cost. + +Returns the estimate from the final ramp step, the iterations executed, and the +sink accumulator. The accumulator must be threaded into the timed samples: a ramp +whose result is discarded is eliminable, which is how warmup came to be a no-op +before. -/ +@[specialize] def calibrate (warmupNanos : Nat) (body : Nat → UInt64 → UInt64) : + IO Calibration := do + let mut acc : UInt64 := 0 + let mut iters := 1 + let mut executed := 0 + let mut elapsed := 0 + let mut lastNanos := 0 + let mut lastIters := 0 + for _ in [0:calibrationRampLimit] do + if elapsed ≥ warmupNanos then + break + let sample ← timeIterations iters acc body + acc := sample.sink + executed := executed + iters + elapsed := elapsed + sample.nanos + lastNanos := sample.nanos + lastIters := iters + iters := iters * 2 + pure { + picosPerIteration := picosPerIteration lastNanos lastIters + iterations := executed + nanos := elapsed + sink := acc } + +/-- Size a benchmark's samples from a calibrated per-iteration cost. + +`itersPerSample` is chosen so one sample takes about `budget.sampleNanos`, and +`sampleCount` is capped by what `budget.measureNanos` can pay for. Both are at +least one, so a workload whose single iteration exhausts the whole budget still +produces exactly one sample — which `Stats.summarise` then marks unreplicated +rather than reporting as a number with an implied precision it does not have. -/ +def planFromCalibration (budget : BenchBudget) (picosPerIteration : Nat) : + SamplingPlan := + let cost := max 1 picosPerIteration + let itersPerSample := max 1 (budget.sampleNanos * 1000 / cost) + let samplePicos := max 1 (itersPerSample * cost) + let affordable := budget.measureNanos * 1000 / samplePicos + { itersPerSample := itersPerSample + sampleCount := max 1 (min budget.sampleCount affordable) } + +/-! ## The three preset budgets + +`sampleNanos` is 1 ms at every preset, for the reason recorded on the field. +What a preset varies is how many samples it asks for and how long a single row +may spend in total. +-/ + +/-- Budget for `--large`: 200 ms of warmup, 50 samples, one minute per row. -/ +def largeBudget : BenchBudget := + { warmupNanos := 200000000, sampleNanos := 1000000, sampleCount := 50, + measureNanos := 60000000000 } + +/-- Budget for `--medium`, the default and what CI runs: 50 ms of warmup, 20 +samples, two seconds per row. -/ +def mediumBudget : BenchBudget := + { warmupNanos := 50000000, sampleNanos := 1000000, sampleCount := 20, + measureNanos := 2000000000 } + +/-- Budget for `--small`: 20 ms of warmup, 10 samples, 0.2 s per row. -/ +def smallBudget : BenchBudget := + { warmupNanos := 20000000, sampleNanos := 1000000, sampleCount := 10, + measureNanos := 200000000 } + +/-! ## Sizing checks + +Sample sizing at the three interesting scales, plus the degenerate ones. +`SamplingPlan` carries no `DecidableEq`, so these check the fields rather than +the structure. They run at elaboration time, so a wrong one fails `lake build`. +-/ + +section Guards + +-- A 1.5 ns body: a sample is two thirds of a million iterations, and every +-- sample the count asks for is affordable. +#guard (planFromCalibration largeBudget 1500).itersPerSample == 666666 +#guard (planFromCalibration largeBudget 1500).sampleCount == 50 + +-- A body that already costs one sample's worth of time: one iteration per +-- sample, still fully replicated. +#guard (planFromCalibration largeBudget 1000000000).itersPerSample == 1 +#guard (planFromCalibration largeBudget 1000000000).sampleCount == 50 + +-- A 13 s body cannot be split, so the total budget decides how many samples +-- there are: four at `--large`, and one at `--medium`, where it is reported as +-- unreplicated rather than averaged. +#guard (planFromCalibration largeBudget 13000000000000).itersPerSample == 1 +#guard (planFromCalibration largeBudget 13000000000000).sampleCount == 4 +#guard (planFromCalibration mediumBudget 13000000000000).sampleCount == 1 + +-- A cost estimate of zero -- a body too cheap for the clock to resolve, or a +-- ramp that never ran -- must not divide by zero and must still sample. +#guard (planFromCalibration smallBudget 0).itersPerSample == 1000000000 +#guard (planFromCalibration smallBudget 0).sampleCount == 10 + +end Guards + +end CompPolyBench diff --git a/bench/CompPolyBench/Harness/Chain.lean b/bench/CompPolyBench/Harness/Chain.lean new file mode 100644 index 00000000..67be6a27 --- /dev/null +++ b/bench/CompPolyBench/Harness/Chain.lean @@ -0,0 +1,199 @@ +/- +Copyright (c) 2026 CompPoly Contributors. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Derek Sorensen +-/ +module + +public import CompPolyBench.Harness.Sink + +/-! +# Operation Chains + +Measuring one field operation, rather than measuring the harness around it. + +The suite's ordinary body shape performs its operation once per timed iteration +and reads its operands out of a pool: `xs.getD (i % xs.size) unit`. For a +polynomial that is fine. For a field multiply it is not — the generated C for +`goldilocks-mul-fast` performs two `lean_nat_mod`s, two `lean_nat_add`s, two +`lean_nat_dec_lt`s, two `lean_array_get_size`s, two `lean_array_fget`s, an +unbox and thirteen refcount operations around a single multiply, and the +measured 3.16 ns/iteration against a 1.80 ns harness floor is almost entirely +that scaffolding. + +So a chain performs `n` operations per iteration with **no array and no `Nat`** +in the loop, and the row divides by `n` through `BenchSpec.workUnits`. At +`n = 1280` the per-iteration floor contributes under two picoseconds per +operation. + +## Why not an array + +`Subtype` erases to its payload, so `Goldilocks.Fast.Field` is a bare `UInt64` +in compiled code. `Array` does not inherit that: every element is a +`lean_object*`, and `lean_box_uint64` *allocates*. An `Array` of a 64-bit +carrier is a pointer array over separately allocated cells — two dependent +loads and a possible refcount write per element. A dependent `add` chain is +about one cycle per operation and cannot be fed from that. + +This is also why the chain lengths here are not Plonky3's. Its +`benchmark_mul_latency` folds over a flat `Vec` of 10000 elements; matching +that *element count* would mismatch the *working set*, which is the quantity +that decides what a memory-touching loop measures. Matching the **shape** — +latency separated from throughput — is what makes the two comparable. + +## Why not `for` and `let mut` + +`ForIn` threads a single state value, so ten mutable locals become a nested +`Prod`. `Prod` has two computationally relevant fields, so it is not a trivial +structure and does not erase: nine heap allocations per round around ten +multiplies. Every chain below is therefore a tail-recursive function with +scalar parameters, and `Harness/SelfCheck.lean` measures what is left. + +## Why the inner block is unrolled + +The loop counter is a structurally-recursive `Nat`. That allocates nothing — +small naturals are tagged immediates — but the emitted C still spends a +`lean_nat_sub`, a `lean_dec` and a `lean_nat_dec_eq` on every round, which is +several times a Montgomery multiply. So the counter runs the *blocks* and each +block is a straight-line `unrollBlock` of operations with no counter at all, +putting the loop overhead at one part in `unrollBlock` rather than one part in +one. +-/ + +public section + +namespace CompPolyBench + +/-- Operations in one straight-line, counter-free block. -/ +def unrollBlock : Nat := 64 + +/-- Rounds in a chain, and so the `workUnits` of a latency row. + +Large enough that the per-iteration harness floor (about 1.8 ns) contributes +under two picoseconds per operation, and small enough that a `ZMod` row — three +orders of magnitude slower than its native counterpart — still fits comfortably +inside one sample. A whole number of `unrollBlock`s, and equal to +`throughputUnits`, so a latency row and a throughput row perform the same number +of operations and their per-unit costs are directly comparable. -/ +def chainRounds : Nat := 20 * unrollBlock + +/-- Independent accumulators in a throughput chain. + +Ten, as Plonky3 uses, so the two measure the same amount of instruction-level +parallelism. Eleven live values including the counter, which fits the register +file on both aarch64 and x86-64 — the same constraint the peer is under. -/ +def throughputWidth : Nat := 10 + +/-- Straight-line rounds inside one throughput loop step. -/ +def throughputUnroll : Nat := 4 + +/-- Rounds in a throughput chain; `throughputWidth` operations each. + +A whole number of `throughputUnroll`s, for the same reason `chainRounds` is a +whole number of `unrollBlock`s. -/ +def throughputRounds : Nat := 32 * throughputUnroll + +/-- Operations a latency chain of `rounds` actually performs. + +`chainLatency` runs whole `unrollBlock`s, so a `rounds` that is not a multiple +of one is rounded down. Rows take their `workUnits` from here rather than from +the `rounds` they asked for, so a badly chosen depth measures fewer operations +than its name suggests instead of dividing by a count the machine never +performed. -/ +def latencyUnits (rounds : Nat) : Nat := unrollBlock * (rounds / unrollBlock) + +/-- Operations a throughput chain of `rounds` actually performs. + +`throughputWidth` per round, whole `throughputUnroll`s only; see +`latencyUnits`. -/ +def throughputUnitsOf (rounds : Nat) : Nat := + throughputWidth * throughputUnroll * (rounds / throughputUnroll) + +/-- Work units performed by one throughput iteration at `throughputRounds`. + +Equal to `chainRounds` by construction; see the note there. -/ +def throughputUnits : Nat := throughputUnitsOf throughputRounds + +/-- Apply `op` eight times, straight-line. + +Composed with itself to build the `unrollBlock`-deep block the chain loops over, +so the loop counter is paid once per 64 operations instead of once per one. -/ +@[inline] def apply8 {F : Type} (op : F → F) (x : F) : F := + op (op (op (op (op (op (op (op x))))))) + +/-- Apply `op` to `x` in a dependent chain, `rounds` deep. + +Every step depends on the previous one, so this measures operation **latency**: +the pipeline cannot overlap two steps. `op` is a direct argument rather than a +`Mul` resolved from a `[Field F]` dictionary — `Goldilocks.Fast.instField` is +declared `(priority := low)` precisely so the concrete instance wins at concrete +sites, and a generic body would pay a projection chain and an indirect call per +step, several times the cost of the operation under test. -/ +@[specialize] def chainLatency {F : Type} (op : F → F) (rounds : Nat) (x : F) : F := + let rec @[specialize] go (n : Nat) (acc : F) : F := + match n with + | 0 => acc + | n + 1 => go n (apply8 (apply8 op) acc) + go (rounds / unrollBlock) x + +/-- Apply `op` across ten independent accumulators, `rounds` times. + +No accumulator depends on another within a round, so the pipeline can overlap +them and this measures operation **throughput**. Combined at the end with +`join`, so the whole computation stays live and returns a single value. + +Ten scalar parameters rather than a tuple or a `for` loop with ten `let mut` +bindings: see the note at the top of this file on `Prod` not erasing. -/ +@[specialize] def chainThroughput {F : Type} (op : F → F → F) (join : F → F → F) + (rounds : Nat) (a b c d e f g h i j : F) : F := + let rec @[specialize] go (n : Nat) (a b c d e f g h i j : F) : F := + match n with + | 0 => + join (join (join (join a b) (join c d)) (join (join e f) (join g h))) (join i j) + | n + 1 => + let a0 := a; let b0 := b; let c0 := c; let d0 := d; let e0 := e + let f0 := f; let g0 := g; let h0 := h; let i0 := i; let j0 := j + let a1 := op a0 b0 + let b1 := op b0 c0 + let c1 := op c0 d0 + let d1 := op d0 e0 + let e1 := op e0 f0 + let f1 := op f0 g0 + let g1 := op g0 h0 + let h1 := op h0 i0 + let i1 := op i0 j0 + let j1 := op j0 a0 + let a2 := op a1 b1 + let b2 := op b1 c1 + let c2 := op c1 d1 + let d2 := op d1 e1 + let e2 := op e1 f1 + let f2 := op f1 g1 + let g2 := op g1 h1 + let h2 := op h1 i1 + let i2 := op i1 j1 + let j2 := op j1 a1 + let a3 := op a2 b2 + let b3 := op b2 c2 + let c3 := op c2 d2 + let d3 := op d2 e2 + let e3 := op e2 f2 + let f3 := op f2 g2 + let g3 := op g2 h2 + let h3 := op h2 i2 + let i3 := op i2 j2 + let j3 := op j2 a2 + let a4 := op a3 b3 + let b4 := op b3 c3 + let c4 := op c3 d3 + let d4 := op d3 e3 + let e4 := op e3 f3 + let f4 := op f3 g3 + let g4 := op g3 h3 + let h4 := op h3 i3 + let i4 := op i3 j3 + let j4 := op j3 a3 + go n a4 b4 c4 d4 e4 f4 g4 h4 i4 j4 + go (rounds / throughputUnroll) a b c d e f g h i j + +end CompPolyBench diff --git a/bench/CompPolyBench/Harness/Sample.lean b/bench/CompPolyBench/Harness/Sample.lean new file mode 100644 index 00000000..0667e8bc --- /dev/null +++ b/bench/CompPolyBench/Harness/Sample.lean @@ -0,0 +1,76 @@ +/- +Copyright (c) 2026 CompPoly Contributors. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Derek Sorensen +-/ +module + +public import CompPolyBench.Harness.Stats +public import CompPolyBench.Harness.Timer + +/-! +# Benchmark Sampling + +Collecting a benchmark's cost as a *set* of samples rather than one total. + +A row's cost is measured as several timed samples so the spread between them is +visible; `Harness.Budget` decides how many and how long each one is. A benchmark +whose single iteration already exhausts the budget cannot be split and is +reported as unreplicated rather than as a number with an implied precision it +does not have. +-/ + +public section + +namespace CompPolyBench + +/-- How a total iteration budget is divided into timed samples. -/ +structure SamplingPlan where + /-- Iterations timed inside each sample. -/ + itersPerSample : Nat + /-- Number of samples to collect. -/ + sampleCount : Nat +deriving Inhabited + +/-- Elapsed nanoseconds of one sample converted to picoseconds per iteration. -/ +@[inline] def picosPerIteration (nanos iters : Nat) : Nat := + if iters = 0 then 0 else nanos * 1000 / iters + +/-- Result of sampling one benchmark. -/ +structure SampledRun where + /-- Summary statistics over the samples. -/ + stats : SampleStats + /-- Per-sample cost in picoseconds per iteration, in collection order. -/ + samples : Array Nat + /-- Total nanoseconds spent inside timed regions. -/ + totalNanos : Nat + /-- Total iterations timed. -/ + totalIterations : Nat + /-- Final sink accumulator, carried out so the loops cannot be eliminated. -/ + sink : UInt64 +deriving Inhabited + +/-- Collect `plan.sampleCount` timed samples of a benchmark body. + +`init` seeds the sink accumulator, and must be the one carried out of whatever +warmed the body — the calibration ramp in practice. Threading it is what keeps +that earlier loop from being eliminable. Every sample replays the same iteration +indices, so samples differ only in machine state and not in the work performed. -/ +@[specialize] def collectSamples (init : UInt64) (plan : SamplingPlan) + (body : Nat → UInt64 → UInt64) : IO SampledRun := do + let mut acc := init + let mut samples : Array Nat := Array.emptyWithCapacity plan.sampleCount + let mut totalNanos := 0 + for _ in [0:plan.sampleCount] do + let sample ← timeIterations plan.itersPerSample acc body + acc := sample.sink + samples := samples.push (picosPerIteration sample.nanos plan.itersPerSample) + totalNanos := totalNanos + sample.nanos + pure { + stats := summarise plan.itersPerSample samples + samples := samples + totalNanos := totalNanos + totalIterations := plan.itersPerSample * plan.sampleCount + sink := acc } + +end CompPolyBench diff --git a/bench/CompPolyBench/Harness/SelfCheck.lean b/bench/CompPolyBench/Harness/SelfCheck.lean new file mode 100644 index 00000000..92673c06 --- /dev/null +++ b/bench/CompPolyBench/Harness/SelfCheck.lean @@ -0,0 +1,221 @@ +/- +Copyright (c) 2026 CompPoly Contributors. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Derek Sorensen +-/ +module + +public import CompPolyBench.Common +public import CompPolyBench.Harness.Chain + +/-! +# Harness Self-Check + +Two synthetic benchmarks that measure the harness itself. + +`harness-floor` times an empty body, giving the per-iteration cost of the loop +and the sink. Every other benchmark's reported cost sits on top of this floor. + +`harness-canary` times a body with a known, deliberately non-eliminable cost. If +it ever measures close to the floor, benchmark bodies are being optimised away and +the run fails rather than reporting an impressive number. + +`harness-chain-floor` and `harness-chain-linearity` do the same two jobs for the +chained bodies in `Harness/Chain.lean`, which have their own floor (the chain's +loop, not the harness's) and their own way of going wrong (a chain the compiler +collapses still reports a plausible number). +-/ + +public section + +namespace CompPolyBench + +/-- Group metadata for the harness self-check. -/ +def harnessGroupInfos : List BenchGroupInfo := [ + ⟨"harness-floor", "Harness loop and sink floor"⟩, + ⟨"harness-canary", "Harness dead-code canary"⟩, + ⟨"harness-chain-floor", "Chain loop floor, per operation"⟩, + ⟨"harness-chain-linearity", "Chain linearity check"⟩ +] + +/-- Rounds of mixing performed by one canary iteration. -/ +def canaryRounds : Nat := 256 + +/-- One round of the canary's mixing chain. -/ +@[inline] private def canaryRound (x : UInt64) : UInt64 := + let y := (x ^^^ (x >>> 33)) * 0xFF51AFD7ED558CCD + y ^^^ (y >>> 29) + +/-- Deliberately non-eliminable work, `canaryRounds` rounds deep. + +`@[noinline]` and the dependence on the iteration index keep this from being +constant-folded. -/ +@[noinline] private def canaryWork (x : UInt64) : UInt64 := + let rec go (n : Nat) (acc : UInt64) : UInt64 := + match n with + | 0 => acc + | n + 1 => go n (canaryRound acc) + go canaryRounds x + +/-- Least multiple by which the canary's per-iteration cost must exceed the floor's. + +The check is a ratio rather than an absolute duration so it is machine +independent: an eliminated canary body collapses onto the floor whatever the +hardware. + +It compares **per-iteration medians**, not totals. Totals only separate the two +rows while they run the same number of iterations; once iteration counts come +from a wall-clock budget the totals are equalised by construction, and a check +on them would either throw on every run or, lowered to accommodate that, pass +vacuously forever — leaving the harness with no dead-code detection at all. -/ +def canaryFloorRatio : Nat := 3 + +/-- Digest length for the self-check benchmarks. + +Both bodies are unbounded in the iteration index, so they have no period. Their +digests are not correctness oracles — nothing is cross-checked against them — so +the length only has to be fixed. -/ +private def harnessDigestIterations : Nat := 16 + +/-- Shift distance in the chain floor's mixing step. -/ +private def chainFloorShift : UInt64 := 7 + +/-- The cheapest operation a chain can honestly carry. + +A shift and a wrapping add: two ALU operations, each depending on the last, so a +chain of them runs at the machine's dependent-issue rate. The per-unit number +the row reports is the chain machinery *plus* this pair, so it is an upper bound +on the machinery alone; when it lands at the pair's own dependent latency, the +machinery costs nothing measurable. + +**It must mix two algebras.** The obvious candidate, `x ^^^ (x >>> 7)`, is the +`GF(2)`-linear map `I + S`; in characteristic two `(I + S) ^ 64 = I + S ^ 64`, +and `S ^ 64` shifts right by 448, so **a 64-deep block of it is the identity**. +LLVM finds this, and the row then reports 15 ps per operation — a sixteenth of a +cycle — while still passing the linearity check below, because what collapses is +each block rather than the loop over blocks. Addition carries between bits and +so does not commute with the shift that way, and there is no reassociation rule +that merges the two. -/ +@[inline] private def chainFloorStep (x : UInt64) : UInt64 := + x + (x >>> chainFloorShift) + +/-- `chainFloorStep` as a binary operation, for the throughput chain. + +The same two ALU operations, so the throughput floor and the latency floor +differ only in whether consecutive operations depend on each other. -/ +@[inline] private def chainFloorPair (x y : UInt64) : UInt64 := + x + (y >>> chainFloorShift) + +/-- Least ratio by which an eight-times-longer chain must outcost a short one. + +One-sided and slack, deliberately. A chain that is executed reports a ratio near +eight; a chain the compiler has collapsed reports one. Demanding the exact +factor instead would fail on correct builds, because eight times the rounds is +not eight times the cache behaviour and the sample count moves with the cost. -/ +def chainLinearityRatio : Nat := 4 + +/-- Chain length multiplier used by the linearity check. -/ +def chainLinearityFactor : Nat := 8 + +/-- Time the harness and chain floors, and reject a body that has been folded away. -/ +private def runHarnessSelfCheck (preset : BenchPreset) (selection : BenchSelection) + (gen : StdGen) : IO (Array BenchGroup × StdGen) := do + let floorRecord ← runTimedSpec + { name := "harness-floor", representation := "UInt64", method := "empty body", field := "none", + inputShape := "no input", digestIterations := harnessDigestIterations, + forceTiming := true } + preset (fun i ↦ i.toUInt64) (fun x ↦ x.toNat) (sink := u64Sink) + let canaryRecord ← runTimedSpec + { name := "harness-canary", representation := "UInt64", + method := s!"{canaryRounds} mixing rounds", field := "none", inputShape := "no input", + digestIterations := harnessDigestIterations, forceTiming := true } + preset (fun i ↦ canaryWork i.toUInt64) (fun x ↦ x.toNat) (sink := u64Sink) + let floorPicos := floorRecord.stats.medianPicos + let canaryPicos := canaryRecord.stats.medianPicos + if floorPicos == 0 || canaryPicos == 0 then + throw <| IO.userError <| + s!"harness self-check produced a zero per-iteration median (floor {floorPicos}ps, " ++ + s!"canary {canaryPicos}ps): the clock could not resolve the loop, so the canary " ++ + "check below cannot say anything and no timing in this run is trustworthy." + if canaryPicos < canaryFloorRatio * floorPicos then + throw <| IO.userError <| + s!"harness canary collapsed onto the loop floor: canary {canaryPicos}ps per " ++ + s!"iteration vs floor {floorPicos}ps (expected at least {canaryFloorRatio}x). " ++ + "Benchmark bodies are being optimised away, so every measured time in this " ++ + "run is meaningless." + -- Chain scaffolding, and proof that a chain is executed rather than folded. + -- `chainRounds` is a compile-time constant but `mixer` is not, so the chain + -- body depends on a runtime local and cannot be lifted to a one-time thunk. + let (seeds, gen) := (randomNatArray throughputWidth (2 ^ 32 - 1)).run gen + let seedAt (k : Nat) : UInt64 := UInt64.ofNat (seeds.getD k k + 1) + let mixer : UInt64 := seedAt 0 + let chainBody (rounds : Nat) : Nat → UInt64 := + fun _ ↦ chainLatency chainFloorStep rounds mixer + let throughputBody : Nat → UInt64 := + fun _ ↦ chainThroughput chainFloorPair (· + ·) throughputRounds + (seedAt 0) (seedAt 1) (seedAt 2) (seedAt 3) (seedAt 4) + (seedAt 5) (seedAt 6) (seedAt 7) (seedAt 8) (seedAt 9) + let chainFloorRecord ← runTimedSpec + { name := "harness-chain-floor", representation := "UInt64", + method := s!"{chainRounds}x shift-add, dependent", field := "none", + inputShape := "no input", digestIterations := 1, workUnits := chainRounds, + digestClass := "latency", forceTiming := true } + preset (chainBody chainRounds) (fun x ↦ x.toNat) (sink := u64Sink) + let throughputFloorRecord ← runTimedSpec + { name := "harness-chain-floor", representation := "UInt64", + method := s!"{throughputUnits}x shift-add, {throughputWidth}-wide", + field := "none", inputShape := "no input", digestIterations := 1, + workUnits := throughputUnits, digestClass := "throughput", + forceTiming := true } + preset throughputBody (fun x ↦ x.toNat) (sink := u64Sink) + -- Both linearity rows keep `workUnits = 1`. The check compares totals, and the + -- two rows deliberately do different amounts of work, which a shared per-unit + -- count could not describe. + let shortRecord ← runTimedSpec + { name := "harness-chain-linearity-short", representation := "UInt64", + method := s!"{chainRounds} rounds", field := "none", inputShape := "no input", + digestIterations := 1, digestClass := "short", forceTiming := true } + preset (chainBody chainRounds) (fun x ↦ x.toNat) (sink := u64Sink) + let longRecord ← runTimedSpec + { name := "harness-chain-linearity-long", representation := "UInt64", + method := s!"{chainLinearityFactor * chainRounds} rounds", field := "none", + inputShape := "no input", digestIterations := 1, digestClass := "long", + forceTiming := true } + preset (chainBody (chainLinearityFactor * chainRounds)) (fun x ↦ x.toNat) + (sink := u64Sink) + let shortPicos := shortRecord.stats.medianPicos + let longPicos := longRecord.stats.medianPicos + if longPicos < chainLinearityRatio * shortPicos then + throw <| IO.userError <| + s!"chained benchmark bodies are not being executed: {chainLinearityFactor}x the " ++ + s!"rounds cost {longPicos}ps against {shortPicos}ps, a ratio under " ++ + s!"{chainLinearityRatio}x. Every per-unit number in this run divides by a " ++ + "work count the machine did not perform." + let mut groups := #[] + if selection.selects "harness-floor" then + groups := groups.push + { groupKey := "harness-floor", title := "Harness loop and sink floor", + records := #[floorRecord] } + if selection.selects "harness-canary" then + groups := groups.push + { groupKey := "harness-canary", title := "Harness dead-code canary", + records := #[canaryRecord] } + if selection.selects "harness-chain-floor" then + groups := groups.push + { groupKey := "harness-chain-floor", title := "Chain loop floor, per operation", + records := #[chainFloorRecord, throughputFloorRecord] } + if selection.selects "harness-chain-linearity" then + groups := groups.push + { groupKey := "harness-chain-linearity", title := "Chain linearity check", + records := #[shortRecord, longRecord] } + pure (groups, gen) + +/-- Registry entry for the harness self-check. + +Both benchmarks are measured whenever either is selected, because the canary +check is a comparison between them. -/ +def harnessTasks : List BenchTask := [ + { infos := harnessGroupInfos, runTask := runHarnessSelfCheck } +] + +end CompPolyBench diff --git a/bench/CompPolyBench/Harness/Sink.lean b/bench/CompPolyBench/Harness/Sink.lean new file mode 100644 index 00000000..2f50e70a --- /dev/null +++ b/bench/CompPolyBench/Harness/Sink.lean @@ -0,0 +1,58 @@ +/- +Copyright (c) 2026 CompPoly Contributors. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Derek Sorensen +-/ +module + +/-! +# Benchmark Result Sinks + +Allocation-free `UInt64` digests whose only purpose is to keep a benchmark result +live inside the timed region. Correctness is established by the untimed +validation pass, which keeps the strong `Nat` digest in `CompPolyBench.Common`. + +Nothing here is comparable across runs or across implementations; a sink value is +never reported. +-/ + +public section + +namespace CompPolyBench + +/-- Fold one result word into a running sink accumulator. + +The inline fold makes the next accumulator depend on the result word. Callers +must keep the accumulator live through the timed region and consume it afterward. +Inspect the compiled timing path to check that the benchmark work is retained. -/ +@[inline] def sinkStep (acc x : UInt64) : UInt64 := + let mixed := (acc ^^^ x) * 0x9E3779B97F4A7C15 + (mixed <<< 27) ||| (mixed >>> 37) + +/-- Truncate a `Nat` digest word to a sink word. + +The fallback sink for benchmarks that have not declared a cheaper one. Free for +results whose digest already fits a machine word, one bignum reduction otherwise. -/ +@[inline] def natSink (n : Nat) : UInt64 := + n.toUInt64 + +/-- Sink a `UInt64`-backed result directly. -/ +@[inline] def u64Sink (x : UInt64) : UInt64 := + x + +/-- Sink a fixed four-position sample of an array. + +Aggregate results must never be walked in full inside the timed region; the +untimed validation pass digests every element. -/ +@[inline] def arraySampleSink (toU64 : α → UInt64) (xs : Array α) : UInt64 := + let n := xs.size + if n = 0 then + 0 + else + let pick (i : Nat) : UInt64 := + match xs[i]? with + | some x => toU64 x + | none => 0 + sinkStep (sinkStep (sinkStep (pick 0) (pick (n / 3))) (pick (2 * n / 3))) (pick (n - 1)) + +end CompPolyBench diff --git a/bench/CompPolyBench/Harness/Stats.lean b/bench/CompPolyBench/Harness/Stats.lean new file mode 100644 index 00000000..177c3b64 --- /dev/null +++ b/bench/CompPolyBench/Harness/Stats.lean @@ -0,0 +1,122 @@ +/- +Copyright (c) 2026 CompPoly Contributors. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Derek Sorensen +-/ +module + +/-! +# Benchmark Sample Statistics + +Summary statistics over the per-sample costs collected for one benchmark. + +Costs are carried in **picoseconds per iteration** so that dividing a sample's +elapsed nanoseconds by its iteration count does not truncate sub-nanosecond +operations to zero. + +Outliers are *labelled*, never dropped: a sample that took ten times the median +is data about the machine, and silently discarding it is how a harness comes to +report a stability it does not have. +-/ + +public section + +namespace CompPolyBench + +/-- Summary of the per-sample costs of one benchmark, in picoseconds per iteration. -/ +structure SampleStats where + /-- Number of samples collected. -/ + count : Nat + /-- Iterations timed within each sample. -/ + itersPerSample : Nat + /-- Fastest sample. -/ + minPicos : Nat + /-- Median sample; the headline number. -/ + medianPicos : Nat + /-- Arithmetic mean of the samples. -/ + meanPicos : Nat + /-- 95th percentile, by nearest rank. -/ + p95Picos : Nat + /-- Population standard deviation. -/ + stddevPicos : Nat + /-- Median absolute deviation from the median. -/ + madPicos : Nat + /-- Samples beyond 1.5x the interquartile range from the quartiles. -/ + mildOutliers : Nat + /-- Samples beyond 3x the interquartile range from the quartiles. -/ + severeOutliers : Nat + /-- Whether too few samples were collected for the spread to mean anything. -/ + unreplicated : Bool +deriving Inhabited + +/-- Least number of samples for which dispersion is reported as meaningful. -/ +def replicationThreshold : Nat := 5 + +/-- Sample at a fractional position of a sorted array, by nearest rank. -/ +private def quantile (sorted : Array Nat) (numerator denominator : Nat) : Nat := + if sorted.isEmpty then 0 + else + let idx := min (sorted.size - 1) (sorted.size * numerator / denominator) + sorted.getD idx 0 + +/-- Median of a sorted array; the mean of the middle pair when the size is even. -/ +private def medianOfSorted (sorted : Array Nat) : Nat := + let n := sorted.size + if n = 0 then 0 + else if n % 2 = 1 then sorted.getD (n / 2) 0 + else (sorted.getD (n / 2 - 1) 0 + sorted.getD (n / 2) 0) / 2 + +/-- Integer square root, by Newton iteration. -/ +private def natSqrt (n : Nat) : Nat := + if n < 2 then n + else + let rec step (guess fuel : Nat) : Nat := + match fuel with + | 0 => guess + | fuel + 1 => + let next := (guess + n / guess) / 2 + if next ≥ guess then guess else step next fuel + step n (n.log2 + 2) + +/-- Summarise per-iteration sample costs, in picoseconds. -/ +def summarise (itersPerSample : Nat) (picosPerIteration : Array Nat) : SampleStats := + let sorted := picosPerIteration.qsort (· < ·) + let n := sorted.size + if n = 0 then + { count := 0, itersPerSample := itersPerSample, minPicos := 0, medianPicos := 0, + meanPicos := 0, p95Picos := 0, stddevPicos := 0, madPicos := 0, + mildOutliers := 0, severeOutliers := 0, unreplicated := true } + else + let total := sorted.foldl (· + ·) 0 + let mean := total / n + let median := medianOfSorted sorted + let variance := sorted.foldl (init := 0) fun acc x ↦ + let d := if x ≥ mean then x - mean else mean - x + acc + d * d + let absDeviations := (sorted.map fun x ↦ if x ≥ median then x - median else median - x) + let q1 := quantile sorted 1 4 + let q3 := quantile sorted 3 4 + let iqr := q3 - q1 + -- A zero interquartile range collapses both fences onto the quartiles, which + -- would label every sample that differs at all as a severe outlier. Samples + -- that agree to the picosecond are the opposite of an outlier signal. + let label := iqr ≠ 0 + let mildLow := q1 - min q1 (3 * iqr / 2) + let mildHigh := q3 + 3 * iqr / 2 + let severeLow := q1 - min q1 (3 * iqr) + let severeHigh := q3 + 3 * iqr + { count := n + itersPerSample := itersPerSample + minPicos := sorted.getD 0 0 + medianPicos := median + meanPicos := mean + p95Picos := quantile sorted 95 100 + stddevPicos := natSqrt (variance / n) + madPicos := medianOfSorted (absDeviations.qsort (· < ·)) + mildOutliers := if !label then 0 else sorted.foldl (init := 0) fun acc x ↦ + if x < mildLow || x > mildHigh then acc + 1 else acc + severeOutliers := if !label then 0 else sorted.foldl (init := 0) fun acc x ↦ + if x < severeLow || x > severeHigh then acc + 1 else acc + unreplicated := n < replicationThreshold } + +end CompPolyBench diff --git a/bench/CompPolyBench/Harness/Timer.lean b/bench/CompPolyBench/Harness/Timer.lean new file mode 100644 index 00000000..702909cd --- /dev/null +++ b/bench/CompPolyBench/Harness/Timer.lean @@ -0,0 +1,55 @@ +/- +Copyright (c) 2026 CompPoly Contributors. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Derek Sorensen +-/ +module + +public import CompPolyBench.Harness.Sink + +/-! +# Benchmark Timing Core + +The single place in the harness that reads the clock. + +A benchmark body has type `Nat → UInt64 → UInt64`: it takes the iteration index +and the sink accumulator and returns the updated accumulator. Threading the +accumulator through the body is what keeps the benchmark result live, so a body +whose result is discarded cannot be written by accident. +-/ + +public section + +namespace CompPolyBench + +/-- Whether this process is running in validation-only mode. + +Set from the command line and read by `runTimedSpec`. When set, ordinary workloads +run their checksum validation without calibration or timed samples. Harness checks +with `forceTiming := true` still calibrate and collect samples. -/ +initialize validateOnlyRef : IO.Ref Bool ← IO.mkRef false + +/-- Elapsed time for one timed sample, with the sink accumulator it produced. -/ +structure TimedSample where + /-- Nanoseconds spent inside the timed region. -/ + nanos : Nat + /-- Final sink accumulator, carried out so the loop cannot be eliminated. -/ + sink : UInt64 +deriving Inhabited + +/-- Run a benchmark body `iters` times and return the elapsed nanoseconds. + +`init` seeds the sink accumulator, normally carried in from the calibration +ramp. The accumulator is bound and returned before the closing clock read, so +the loop is sequenced inside the timed region. -/ +@[inline] def timeIterations (iters : Nat) (init : UInt64) + (body : Nat → UInt64 → UInt64) : IO TimedSample := do + let mut acc := init + let start ← IO.monoNanosNow + for i in [0:iters] do + acc := body i acc + let forced := acc + let stop ← IO.monoNanosNow + pure { nanos := stop - start, sink := forced } + +end CompPolyBench diff --git a/bench/CompPolyBench/Multilinear/Basic.lean b/bench/CompPolyBench/Multilinear/Basic.lean index 1ba36941..ec2e041e 100644 --- a/bench/CompPolyBench/Multilinear/Basic.lean +++ b/bench/CompPolyBench/Multilinear/Basic.lean @@ -22,6 +22,12 @@ namespace CompPolyBench /-- Number of multilinear polynomials used by many-MLE benchmarks. -/ private def manyMlePolyCount : Nat := 256 +/-- Number of distinct evaluation points cycled by the single-polynomial groups. + +Also the period of their bodies in the iteration index, and so their digest +length. -/ +private def multilinearPointCount : Nat := 32 + /-- Number of variables used by many-MLE benchmarks. -/ private def manyMleVarCount : Nat := 12 @@ -53,58 +59,44 @@ private def mlePolysOfFlatArray {R : Type*} [Zero R] (polyCount n : Nat) (values polys := polys.push (CMlPolynomialEval.ofArray polyValues n) return polys -/-- Benchmark group metadata for `CompPoly.Multilinear.Basic`. -/ -def multilinearGroupInfos : List BenchGroupInfo := [ - ⟨"multilinear-coeff-koalabear", "Multilinear coefficient-form evaluation (KoalaBear)"⟩, - ⟨"multilinear-hypercube-koalabear", "Multilinear hypercube-form evaluation (KoalaBear)"⟩, - ⟨manyMleKoalaBearGroupKey, manyMleKoalaBearTitle⟩, - ⟨"multilinear-coeff-goldilocks", "Multilinear coefficient-form evaluation (Goldilocks)"⟩, - ⟨"multilinear-hypercube-goldilocks", - "Multilinear hypercube-form evaluation (Goldilocks)"⟩ -] - /-- Run KoalaBear coefficient-form multilinear evaluation benchmarks. -/ private def runKoalaBearMultilinearCoeff (preset : BenchPreset) (gen : StdGen) : IO (BenchGroup × StdGen) := do let (coeffs, gen) := (koalaBearVector 256 false).run gen let (points, gen) := (koalaBearPoints 256).run gen let coeffPoly : CMlPolynomial KoalaBear.Field 8 := CMlPolynomial.ofArray coeffs 8 - let evalPoint (offset : Nat) : Vector KoalaBear.Field 8 := - Vector.ofFn fun j ↦ points.getD ((offset + j.val) % points.size) 0 + let evalPoint (i : Nat) : Vector KoalaBear.Field 8 := + Vector.ofFn fun j ↦ points.getD ((i % multilinearPointCount + j.val) % points.size) 0 let fastCoeffs := koalaBearFastArray coeffs let fastPoints := koalaBearFastArray points let fastCoeffPoly : CMlPolynomial KoalaBear.Fast.Field 8 := CMlPolynomial.ofArray fastCoeffs 8 - let fastEvalPoint (offset : Nat) : Vector KoalaBear.Fast.Field 8 := - Vector.ofFn fun j ↦ fastPoints.getD ((offset + j.val) % fastPoints.size) 0 - let warmup := warmupIterations preset - let measured := measuredIterations preset - let hornerMeasured := preset.selectNat 120000 17000 3500 - let fastMeasured := preset.selectNat 7000 1000 200 - let fastHornerMeasured := preset.selectNat 245000 35000 7000 - let checksumIterations := groupChecksumIterations measured [ - hornerMeasured, fastMeasured, fastHornerMeasured - ] - let coeffEval ← runTimed - "multilinear-coeff-eval" "CMlPolynomial" "eval" "KoalaBear.Field" - "8 vars, 256 coefficients, 32 points" preset warmup measured - (fun i ↦ CMlPolynomial.eval coeffPoly (evalPoint (i % 32))) - checksumKoalaBear (checksumIterations := checksumIterations) - let fastCoeffEval ← runTimed - "multilinear-coeff-eval-fast" "CMlPolynomial" "eval" "KoalaBear.Fast.Field" - "8 vars, 256 coefficients, 32 points" preset warmup fastMeasured - (fun i ↦ CMlPolynomial.eval fastCoeffPoly (fastEvalPoint (i % 32))) - checksumKoalaBearFast (checksumIterations := checksumIterations) - let coeffHorner ← runTimed - "multilinear-coeff-horner" "CMlPolynomial" "evalHorner" "KoalaBear.Field" - "8 vars, 256 coefficients, 32 points" preset warmup hornerMeasured - (fun i ↦ CMlPolynomial.evalHorner coeffPoly (evalPoint (i % 32))) - checksumKoalaBear (checksumIterations := checksumIterations) - let fastCoeffHorner ← runTimed - "multilinear-coeff-horner-fast" "CMlPolynomial" "evalHorner" - "KoalaBear.Fast.Field" - "8 vars, 256 coefficients, 32 points" preset warmup fastHornerMeasured - (fun i ↦ CMlPolynomial.evalHorner fastCoeffPoly (fastEvalPoint (i % 32))) - checksumKoalaBearFast (checksumIterations := checksumIterations) + let fastEvalPoint (i : Nat) : Vector KoalaBear.Fast.Field 8 := + Vector.ofFn fun j ↦ fastPoints.getD ((i % multilinearPointCount + j.val) % fastPoints.size) 0 + let checksumIterations := digestPeriod multilinearPointCount + let coeffEval ← runTimedSpec + { name := "multilinear-coeff-eval", representation := "CMlPolynomial", method := "eval", + field := "KoalaBear.Field", inputShape := "8 vars, 256 coefficients, 32 points", + digestIterations := checksumIterations } + preset (fun i ↦ CMlPolynomial.eval coeffPoly (evalPoint i)) + checksumKoalaBear + let fastCoeffEval ← runTimedSpec + { name := "multilinear-coeff-eval-fast", representation := "CMlPolynomial", method := "eval", + field := "KoalaBear.Fast.Field", inputShape := "8 vars, 256 coefficients, 32 points", + digestIterations := checksumIterations } + preset (fun i ↦ CMlPolynomial.eval fastCoeffPoly (fastEvalPoint i)) + checksumKoalaBearFast + let coeffHorner ← runTimedSpec + { name := "multilinear-coeff-horner", representation := "CMlPolynomial", + method := "evalHorner", field := "KoalaBear.Field", + inputShape := "8 vars, 256 coefficients, 32 points", digestIterations := checksumIterations } + preset (fun i ↦ CMlPolynomial.evalHorner coeffPoly (evalPoint i)) + checksumKoalaBear + let fastCoeffHorner ← runTimedSpec + { name := "multilinear-coeff-horner-fast", representation := "CMlPolynomial", + method := "evalHorner", field := "KoalaBear.Fast.Field", + inputShape := "8 vars, 256 coefficients, 32 points", digestIterations := checksumIterations } + preset + (fun i ↦ CMlPolynomial.evalHorner fastCoeffPoly (fastEvalPoint i)) checksumKoalaBearFast pure ({ groupKey := "multilinear-coeff-koalabear", title := "Multilinear coefficient-form evaluation (KoalaBear)", @@ -117,44 +109,43 @@ private def runKoalaBearMultilinearHypercube (preset : BenchPreset) (gen : StdGe let (evals, gen) := (koalaBearVector 256 false).run gen let (points, gen) := (koalaBearPoints 256).run gen let evalPoly : CMlPolynomialEval KoalaBear.Field 8 := CMlPolynomialEval.ofArray evals 8 - let evalPoint (offset : Nat) : Vector KoalaBear.Field 8 := - Vector.ofFn fun j ↦ points.getD ((offset + j.val) % points.size) 0 + let evalPoint (i : Nat) : Vector KoalaBear.Field 8 := + Vector.ofFn fun j ↦ points.getD ((i % multilinearPointCount + j.val) % points.size) 0 let fastEvals := koalaBearFastArray evals let fastPoints := koalaBearFastArray points let fastEvalPoly : CMlPolynomialEval KoalaBear.Fast.Field 8 := CMlPolynomialEval.ofArray fastEvals 8 - let fastEvalPoint (offset : Nat) : Vector KoalaBear.Fast.Field 8 := - Vector.ofFn fun j ↦ fastPoints.getD ((offset + j.val) % fastPoints.size) 0 - let warmup := warmupIterations preset - let measured := measuredIterations preset - let mleMeasured := preset.selectNat 90000 13000 2500 - let fastMeasured := preset.selectNat 8400 1200 240 - let fastMleMeasured := preset.selectNat 280000 40000 8000 - let checksumIterations := groupChecksumIterations measured [ - mleMeasured, fastMeasured, fastMleMeasured - ] - let hypercubeEval ← runTimed - "multilinear-hypercube-eval" "CMlPolynomialEval" "eval" "KoalaBear.Field" - "8 vars, 256 hypercube values, 32 points" preset warmup measured - (fun i ↦ CMlPolynomialEval.eval evalPoly (evalPoint (i % 32))) - checksumKoalaBear (checksumIterations := checksumIterations) - let fastHypercubeEval ← runTimed - "multilinear-hypercube-eval-fast" "CMlPolynomialEval" "eval" - "KoalaBear.Fast.Field" - "8 vars, 256 hypercube values, 32 points" preset warmup fastMeasured - (fun i ↦ CMlPolynomialEval.eval fastEvalPoly (fastEvalPoint (i % 32))) - checksumKoalaBearFast (checksumIterations := checksumIterations) - let hypercubeMle ← runTimed - "multilinear-hypercube-mle" "CMlPolynomialEval" "evalMle" "KoalaBear.Field" - "8 vars, 256 hypercube values, 32 points" preset warmup mleMeasured - (fun i ↦ CMlPolynomialEval.evalMle evalPoly (evalPoint (i % 32))) - checksumKoalaBear (checksumIterations := checksumIterations) - let fastHypercubeMle ← runTimed - "multilinear-hypercube-mle-fast" "CMlPolynomialEval" "evalMle" - "KoalaBear.Fast.Field" - "8 vars, 256 hypercube values, 32 points" preset warmup fastMleMeasured - (fun i ↦ CMlPolynomialEval.evalMle fastEvalPoly (fastEvalPoint (i % 32))) - checksumKoalaBearFast (checksumIterations := checksumIterations) + let fastEvalPoint (i : Nat) : Vector KoalaBear.Fast.Field 8 := + Vector.ofFn fun j ↦ fastPoints.getD ((i % multilinearPointCount + j.val) % fastPoints.size) 0 + let checksumIterations := digestPeriod multilinearPointCount + let hypercubeEval ← runTimedSpec + { name := "multilinear-hypercube-eval", representation := "CMlPolynomialEval", + method := "eval", field := "KoalaBear.Field", + inputShape := "8 vars, 256 hypercube values, 32 points", + digestIterations := checksumIterations } + preset (fun i ↦ CMlPolynomialEval.eval evalPoly (evalPoint i)) + checksumKoalaBear + let fastHypercubeEval ← runTimedSpec + { name := "multilinear-hypercube-eval-fast", representation := "CMlPolynomialEval", + method := "eval", field := "KoalaBear.Fast.Field", + inputShape := "8 vars, 256 hypercube values, 32 points", + digestIterations := checksumIterations } + preset + (fun i ↦ CMlPolynomialEval.eval fastEvalPoly (fastEvalPoint i)) checksumKoalaBearFast + let hypercubeMle ← runTimedSpec + { name := "multilinear-hypercube-mle", representation := "CMlPolynomialEval", + method := "evalMle", field := "KoalaBear.Field", + inputShape := "8 vars, 256 hypercube values, 32 points", + digestIterations := checksumIterations } + preset (fun i ↦ CMlPolynomialEval.evalMle evalPoly (evalPoint i)) + checksumKoalaBear + let fastHypercubeMle ← runTimedSpec + { name := "multilinear-hypercube-mle-fast", representation := "CMlPolynomialEval", + method := "evalMle", field := "KoalaBear.Fast.Field", + inputShape := "8 vars, 256 hypercube values, 32 points", + digestIterations := checksumIterations } + preset + (fun i ↦ CMlPolynomialEval.evalMle fastEvalPoly (fastEvalPoint i)) checksumKoalaBearFast pure ({ groupKey := "multilinear-hypercube-koalabear", title := "Multilinear hypercube-form evaluation (KoalaBear)", @@ -177,35 +168,32 @@ private def runKoalaBearMultilinearManyMle (preset : BenchPreset) (gen : StdGen) mlePolysOfFlatArray manyMlePolyCount manyMleVarCount fastValues let fastX : Vector KoalaBear.Fast.Field manyMleVarCount := Vector.ofFn fun j ↦ fastPoints.getD j.val 0 - let warmup := preset.selectNat 1 1 0 - let scalarMeasured := preset.selectNat 140 20 4 - let byLayersMeasured := preset.selectNat 200 30 6 - let fastScalarMeasured := preset.selectNat 525 75 15 - let fastByLayersMeasured := preset.selectNat 800 115 25 - let checksumIterations := groupChecksumIterations scalarMeasured [ - byLayersMeasured, fastScalarMeasured, fastByLayersMeasured - ] - let scalar ← runTimed - "multilinear-many-mle-scalar-loop" "Array CMlPolynomialEval" "evalManyMle" - "KoalaBear.Field" manyMleShape preset warmup scalarMeasured - (fun _ ↦ CMlPolynomialEval.evalManyMle polys x) - (checksumArray checksumKoalaBear) (checksumIterations := checksumIterations) - let byLayers ← runTimed - "multilinear-many-mle-by-layers" "Array CMlPolynomialEval" - "evalManyMleByLayers" "KoalaBear.Field" manyMleShape preset warmup byLayersMeasured - (fun _ ↦ CMlPolynomialEval.evalManyMleByLayers polys x) - (checksumArray checksumKoalaBear) (checksumIterations := checksumIterations) - let fastScalar ← runTimed - "multilinear-many-mle-scalar-loop-fast" "Array CMlPolynomialEval" "evalManyMle" - "KoalaBear.Fast.Field" manyMleShape preset warmup fastScalarMeasured - (fun _ ↦ CMlPolynomialEval.evalManyMle fastPolys fastX) - (checksumArray checksumKoalaBearFast) (checksumIterations := checksumIterations) - let fastByLayers ← runTimed - "multilinear-many-mle-by-layers-fast" "Array CMlPolynomialEval" - "evalManyMleByLayers" "KoalaBear.Fast.Field" manyMleShape preset warmup - fastByLayersMeasured + let checksumIterations := digestPeriod 1 + let scalar ← runTimedSpec + { name := "multilinear-many-mle-scalar-loop", representation := "Array CMlPolynomialEval", + method := "evalManyMle", field := "KoalaBear.Field", inputShape := manyMleShape, + digestIterations := checksumIterations } + preset (fun _ ↦ CMlPolynomialEval.evalManyMle polys x) + (checksumArray checksumKoalaBear) + let byLayers ← runTimedSpec + { name := "multilinear-many-mle-by-layers", representation := "Array CMlPolynomialEval", + method := "evalManyMleByLayers", field := "KoalaBear.Field", inputShape := manyMleShape, + digestIterations := checksumIterations } + preset (fun _ ↦ CMlPolynomialEval.evalManyMleByLayers polys x) + (checksumArray checksumKoalaBear) + let fastScalar ← runTimedSpec + { name := "multilinear-many-mle-scalar-loop-fast", representation := "Array CMlPolynomialEval", + method := "evalManyMle", field := "KoalaBear.Fast.Field", inputShape := manyMleShape, + digestIterations := checksumIterations } + preset (fun _ ↦ CMlPolynomialEval.evalManyMle fastPolys fastX) + (checksumArray checksumKoalaBearFast) + let fastByLayers ← runTimedSpec + { name := "multilinear-many-mle-by-layers-fast", representation := "Array CMlPolynomialEval", + method := "evalManyMleByLayers", field := "KoalaBear.Fast.Field", inputShape := manyMleShape, + digestIterations := checksumIterations } + preset (fun _ ↦ CMlPolynomialEval.evalManyMleByLayers fastPolys fastX) - (checksumArray checksumKoalaBearFast) (checksumIterations := checksumIterations) + (checksumArray checksumKoalaBearFast) pure ({ groupKey := manyMleKoalaBearGroupKey, title := manyMleKoalaBearTitle, @@ -219,22 +207,23 @@ private def runGoldilocksMultilinearCoeff (preset : BenchPreset) (gen : StdGen) let (goldilocksPoints, gen) := (zmodArray Goldilocks.fieldSize 256 false).run gen let goldilocksCoeffPoly : CMlPolynomial Goldilocks.Field 8 := CMlPolynomial.ofArray goldilocksCoeffs 8 - let goldilocksEvalPoint (offset : Nat) : Vector Goldilocks.Field 8 := - Vector.ofFn fun j ↦ goldilocksPoints.getD ((offset + j.val) % goldilocksPoints.size) 0 - let warmup := warmupIterations preset - let measured := measuredIterations preset - let hornerMeasured := preset.selectNat 32000 4500 900 - let checksumIterations := groupChecksumIterations measured [hornerMeasured] - let goldilocksCoeffEval ← runTimed - "multilinear-coeff-eval-goldilocks" "CMlPolynomial" "eval" "Goldilocks.Field" - "8 vars, 256 coefficients, 32 points" preset warmup measured - (fun i ↦ CMlPolynomial.eval goldilocksCoeffPoly (goldilocksEvalPoint (i % 32))) - checksumZMod (checksumIterations := checksumIterations) - let goldilocksCoeffHorner ← runTimed - "multilinear-coeff-horner-goldilocks" "CMlPolynomial" "evalHorner" "Goldilocks.Field" - "8 vars, 256 coefficients, 32 points" preset warmup hornerMeasured - (fun i ↦ CMlPolynomial.evalHorner goldilocksCoeffPoly (goldilocksEvalPoint (i % 32))) - checksumZMod (checksumIterations := checksumIterations) + let goldilocksEvalPoint (i : Nat) : Vector Goldilocks.Field 8 := + Vector.ofFn fun j ↦ + goldilocksPoints.getD ((i % multilinearPointCount + j.val) % goldilocksPoints.size) 0 + let checksumIterations := digestPeriod multilinearPointCount + let goldilocksCoeffEval ← runTimedSpec + { name := "multilinear-coeff-eval-goldilocks", representation := "CMlPolynomial", + method := "eval", field := "Goldilocks.Field", + inputShape := "8 vars, 256 coefficients, 32 points", digestIterations := checksumIterations } + preset + (fun i ↦ CMlPolynomial.eval goldilocksCoeffPoly (goldilocksEvalPoint i)) checksumZMod + let goldilocksCoeffHorner ← runTimedSpec + { name := "multilinear-coeff-horner-goldilocks", representation := "CMlPolynomial", + method := "evalHorner", field := "Goldilocks.Field", + inputShape := "8 vars, 256 coefficients, 32 points", digestIterations := checksumIterations } + preset + (fun i ↦ CMlPolynomial.evalHorner goldilocksCoeffPoly (goldilocksEvalPoint i)) + checksumZMod pure ({ groupKey := "multilinear-coeff-goldilocks", title := "Multilinear coefficient-form evaluation (Goldilocks)", @@ -248,22 +237,25 @@ private def runGoldilocksMultilinearHypercube (preset : BenchPreset) (gen : StdG let (goldilocksPoints, gen) := (zmodArray Goldilocks.fieldSize 256 false).run gen let goldilocksEvalPoly : CMlPolynomialEval Goldilocks.Field 8 := CMlPolynomialEval.ofArray goldilocksEvals 8 - let goldilocksEvalPoint (offset : Nat) : Vector Goldilocks.Field 8 := - Vector.ofFn fun j ↦ goldilocksPoints.getD ((offset + j.val) % goldilocksPoints.size) 0 - let warmup := warmupIterations preset - let measured := measuredIterations preset - let mleMeasured := preset.selectNat 25000 3500 700 - let checksumIterations := groupChecksumIterations measured [mleMeasured] - let goldilocksHypercubeEval ← runTimed - "multilinear-hypercube-eval-goldilocks" "CMlPolynomialEval" "eval" "Goldilocks.Field" - "8 vars, 256 hypercube values, 32 points" preset warmup measured - (fun i ↦ CMlPolynomialEval.eval goldilocksEvalPoly (goldilocksEvalPoint (i % 32))) - checksumZMod (checksumIterations := checksumIterations) - let goldilocksHypercubeMle ← runTimed - "multilinear-hypercube-mle-goldilocks" "CMlPolynomialEval" "evalMle" "Goldilocks.Field" - "8 vars, 256 hypercube values, 32 points" preset warmup mleMeasured - (fun i ↦ CMlPolynomialEval.evalMle goldilocksEvalPoly (goldilocksEvalPoint (i % 32))) - checksumZMod (checksumIterations := checksumIterations) + let goldilocksEvalPoint (i : Nat) : Vector Goldilocks.Field 8 := + Vector.ofFn fun j ↦ + goldilocksPoints.getD ((i % multilinearPointCount + j.val) % goldilocksPoints.size) 0 + let checksumIterations := digestPeriod multilinearPointCount + let goldilocksHypercubeEval ← runTimedSpec + { name := "multilinear-hypercube-eval-goldilocks", representation := "CMlPolynomialEval", + method := "eval", field := "Goldilocks.Field", + inputShape := "8 vars, 256 hypercube values, 32 points", + digestIterations := checksumIterations } + preset + (fun i ↦ CMlPolynomialEval.eval goldilocksEvalPoly (goldilocksEvalPoint i)) checksumZMod + let goldilocksHypercubeMle ← runTimedSpec + { name := "multilinear-hypercube-mle-goldilocks", representation := "CMlPolynomialEval", + method := "evalMle", field := "Goldilocks.Field", + inputShape := "8 vars, 256 hypercube values, 32 points", + digestIterations := checksumIterations } + preset + (fun i ↦ CMlPolynomialEval.evalMle goldilocksEvalPoly (goldilocksEvalPoint i)) + checksumZMod pure ({ groupKey := "multilinear-hypercube-goldilocks", title := "Multilinear hypercube-form evaluation (Goldilocks)", @@ -290,9 +282,4 @@ def multilinearTasks : List BenchTask := [ runGoldilocksMultilinearHypercube ] -/-- Run selected coefficient-form and hypercube-form multilinear evaluation benchmarks. -/ -def runMultilinear (preset : BenchPreset) (selection : BenchSelection) (gen : StdGen) : - IO (Array BenchGroup × StdGen) := do - runSelectedTasks multilinearTasks preset selection gen - end CompPolyBench diff --git a/bench/CompPolyBench/Multivariate/CMvPolynomial.lean b/bench/CompPolyBench/Multivariate/CMvPolynomial.lean index 612d9da7..c7b5d6a1 100644 --- a/bench/CompPolyBench/Multivariate/CMvPolynomial.lean +++ b/bench/CompPolyBench/Multivariate/CMvPolynomial.lean @@ -18,14 +18,6 @@ open CompPoly namespace CompPolyBench -/-- Benchmark group metadata for `CompPoly.Multivariate.CMvPolynomial`. -/ -def multivariateGroupInfos : List BenchGroupInfo := [ - ⟨"multivariate-dense-koalabear", "Multivariate dense evaluation (KoalaBear)"⟩, - ⟨"multivariate-sparse-koalabear", "Multivariate sparse evaluation (KoalaBear)"⟩, - ⟨"multivariate-dense-goldilocks", "Multivariate dense evaluation (Goldilocks)"⟩, - ⟨"multivariate-sparse-goldilocks", "Multivariate sparse evaluation (Goldilocks)"⟩ -] - /-- Number of variables used by multivariate evaluation benchmarks. -/ private def multivariateVars : Nat := 5 @@ -62,7 +54,6 @@ private def buildCMvPolynomial {R : Type*} [CommSemiring R] [BEq R] [LawfulBEq R /-- Benchmark dense multivariate evaluation over a generic prime `ZMod` field. -/ private def runDenseMultivariateZMod (modulus : Nat) [Fact (Nat.Prime modulus)] (key nameSuffix fieldName fieldTitle : String) - (largeHornerMeasured mediumHornerMeasured smallHornerMeasured : Nat) (preset : BenchPreset) (gen : StdGen) : IO (BenchGroup × StdGen) := do let (terms, gen) := (zmodArray modulus multivariateTermSlots false).run gen @@ -70,21 +61,20 @@ private def runDenseMultivariateZMod (modulus : Nat) [Fact (Nat.Prime modulus)] let poly := buildCMvPolynomial terms let evalPoint (offset : Nat) : Fin multivariateVars → ZMod modulus := fun j ↦ points.getD ((offset + j.val) % points.size) 0 - let warmup := warmupIterations preset - let measured := measuredIterations preset - let hornerMeasured := - preset.selectNat largeHornerMeasured mediumHornerMeasured smallHornerMeasured - let checksumIterations := groupChecksumIterations measured [hornerMeasured] - let denseEval ← runTimed - ("multivariate-dense-eval-" ++ nameSuffix) "CMvPolynomial" "eval" fieldName - multivariateDenseShape preset warmup measured - (fun i ↦ CPoly.CMvPolynomial.eval (evalPoint (i % multivariatePointCount)) poly) - checksumZMod (checksumIterations := checksumIterations) - let denseHorner ← runTimed - ("multivariate-dense-horner-" ++ nameSuffix) "CMvPolynomial" "evalHorner" fieldName - multivariateDenseShape preset warmup hornerMeasured + let checksumIterations := digestPeriod multivariatePointCount + let denseEval ← runTimedSpec + { name := ("multivariate-dense-eval-" ++ nameSuffix), representation := "CMvPolynomial", + method := "eval", field := fieldName, inputShape := multivariateDenseShape, + digestIterations := checksumIterations } + preset + (fun i ↦ CPoly.CMvPolynomial.eval (evalPoint (i % multivariatePointCount)) poly) checksumZMod + let denseHorner ← runTimedSpec + { name := ("multivariate-dense-horner-" ++ nameSuffix), representation := "CMvPolynomial", + method := "evalHorner", field := fieldName, inputShape := multivariateDenseShape, + digestIterations := checksumIterations } + preset (fun i ↦ CPoly.CMvPolynomial.evalHorner (evalPoint (i % multivariatePointCount)) poly) - checksumZMod (checksumIterations := checksumIterations) + checksumZMod pure ({ groupKey := key, title := "Multivariate dense evaluation (" ++ fieldTitle ++ ")", @@ -94,7 +84,6 @@ private def runDenseMultivariateZMod (modulus : Nat) [Fact (Nat.Prime modulus)] /-- Benchmark sparse multivariate evaluation over a generic prime `ZMod` field. -/ private def runSparseMultivariateZMod (modulus : Nat) [Fact (Nat.Prime modulus)] (key nameSuffix fieldName fieldTitle : String) - (largeHornerMeasured mediumHornerMeasured smallHornerMeasured : Nat) (preset : BenchPreset) (gen : StdGen) : IO (BenchGroup × StdGen) := do let (terms, gen) := (zmodArrayWithStride modulus multivariateTermSlots 16).run gen @@ -102,21 +91,20 @@ private def runSparseMultivariateZMod (modulus : Nat) [Fact (Nat.Prime modulus)] let poly := buildCMvPolynomial terms let evalPoint (offset : Nat) : Fin multivariateVars → ZMod modulus := fun j ↦ points.getD ((offset + j.val) % points.size) 0 - let warmup := warmupIterations preset - let measured := measuredIterations preset - let hornerMeasured := - preset.selectNat largeHornerMeasured mediumHornerMeasured smallHornerMeasured - let checksumIterations := groupChecksumIterations measured [hornerMeasured] - let sparseEval ← runTimed - ("multivariate-sparse-eval-" ++ nameSuffix) "CMvPolynomial" "eval" fieldName - multivariateSparseShape preset warmup measured - (fun i ↦ CPoly.CMvPolynomial.eval (evalPoint (i % multivariatePointCount)) poly) - checksumZMod (checksumIterations := checksumIterations) - let sparseHorner ← runTimed - ("multivariate-sparse-horner-" ++ nameSuffix) "CMvPolynomial" "evalHorner" fieldName - multivariateSparseShape preset warmup hornerMeasured + let checksumIterations := digestPeriod multivariatePointCount + let sparseEval ← runTimedSpec + { name := ("multivariate-sparse-eval-" ++ nameSuffix), representation := "CMvPolynomial", + method := "eval", field := fieldName, inputShape := multivariateSparseShape, + digestIterations := checksumIterations } + preset + (fun i ↦ CPoly.CMvPolynomial.eval (evalPoint (i % multivariatePointCount)) poly) checksumZMod + let sparseHorner ← runTimedSpec + { name := ("multivariate-sparse-horner-" ++ nameSuffix), representation := "CMvPolynomial", + method := "evalHorner", field := fieldName, inputShape := multivariateSparseShape, + digestIterations := checksumIterations } + preset (fun i ↦ CPoly.CMvPolynomial.evalHorner (evalPoint (i % multivariatePointCount)) poly) - checksumZMod (checksumIterations := checksumIterations) + checksumZMod pure ({ groupKey := key, title := "Multivariate sparse evaluation (" ++ fieldTitle ++ ")", @@ -136,37 +124,37 @@ private def runKoalaBearMultivariateDense (preset : BenchPreset) (gen : StdGen) let fastPoly := buildCMvPolynomial fastTerms let fastEvalPoint (offset : Nat) : Fin multivariateVars → KoalaBear.Fast.Field := fun j ↦ fastPoints.getD ((offset + j.val) % fastPoints.size) 0 - let warmup := warmupIterations preset - let measured := measuredIterations preset - let hornerMeasured := preset.selectNat 10000 1500 300 - let fastMeasured := preset.selectNat 14000 2000 400 - let fastHornerMeasured := preset.selectNat 31500 4500 900 - let checksumIterations := groupChecksumIterations measured [ - hornerMeasured, fastMeasured, fastHornerMeasured - ] - let denseEval ← runTimed - "multivariate-dense-eval" "CMvPolynomial" "eval" "KoalaBear.Field" - multivariateDenseShape preset warmup measured + let checksumIterations := digestPeriod multivariatePointCount + let denseEval ← runTimedSpec + { name := "multivariate-dense-eval", representation := "CMvPolynomial", method := "eval", + field := "KoalaBear.Field", inputShape := multivariateDenseShape, + digestIterations := checksumIterations } + preset (fun i ↦ CPoly.CMvPolynomial.eval (evalPoint (i % multivariatePointCount)) poly) - checksumKoalaBear (checksumIterations := checksumIterations) - let fastDenseEval ← runTimed - "multivariate-dense-eval-fast" "CMvPolynomial" "eval" "KoalaBear.Fast.Field" - multivariateDenseShape preset warmup fastMeasured + checksumKoalaBear + let fastDenseEval ← runTimedSpec + { name := "multivariate-dense-eval-fast", representation := "CMvPolynomial", method := "eval", + field := "KoalaBear.Fast.Field", inputShape := multivariateDenseShape, + digestIterations := checksumIterations } + preset (fun i ↦ CPoly.CMvPolynomial.eval (fastEvalPoint (i % multivariatePointCount)) fastPoly) - checksumKoalaBearFast (checksumIterations := checksumIterations) - let denseHorner ← runTimed - "multivariate-dense-horner" "CMvPolynomial" "evalHorner" "KoalaBear.Field" - multivariateDenseShape preset warmup hornerMeasured + checksumKoalaBearFast + let denseHorner ← runTimedSpec + { name := "multivariate-dense-horner", representation := "CMvPolynomial", + method := "evalHorner", field := "KoalaBear.Field", inputShape := multivariateDenseShape, + digestIterations := checksumIterations } + preset (fun i ↦ CPoly.CMvPolynomial.evalHorner (evalPoint (i % multivariatePointCount)) poly) - checksumKoalaBear (checksumIterations := checksumIterations) - let fastDenseHorner ← runTimed - "multivariate-dense-horner-fast" "CMvPolynomial" "evalHorner" - "KoalaBear.Fast.Field" - multivariateDenseShape preset warmup fastHornerMeasured + checksumKoalaBear + let fastDenseHorner ← runTimedSpec + { name := "multivariate-dense-horner-fast", representation := "CMvPolynomial", + method := "evalHorner", field := "KoalaBear.Fast.Field", + inputShape := multivariateDenseShape, digestIterations := checksumIterations } + preset (fun i ↦ CPoly.CMvPolynomial.evalHorner (fastEvalPoint (i % multivariatePointCount)) fastPoly) - checksumKoalaBearFast (checksumIterations := checksumIterations) + checksumKoalaBearFast pure ({ groupKey := "multivariate-dense-koalabear", title := "Multivariate dense evaluation (KoalaBear)", @@ -186,37 +174,37 @@ private def runKoalaBearMultivariateSparse (preset : BenchPreset) (gen : StdGen) let fastPoly := buildCMvPolynomial fastTerms let fastEvalPoint (offset : Nat) : Fin multivariateVars → KoalaBear.Fast.Field := fun j ↦ fastPoints.getD ((offset + j.val) % fastPoints.size) 0 - let warmup := warmupIterations preset - let measured := measuredIterations preset - let hornerMeasured := preset.selectNat 10000 1500 300 - let fastMeasured := preset.selectNat 14000 2000 400 - let fastHornerMeasured := preset.selectNat 31500 4500 900 - let checksumIterations := groupChecksumIterations measured [ - hornerMeasured, fastMeasured, fastHornerMeasured - ] - let sparseEval ← runTimed - "multivariate-sparse-eval" "CMvPolynomial" "eval" "KoalaBear.Field" - multivariateSparseShape preset warmup measured + let checksumIterations := digestPeriod multivariatePointCount + let sparseEval ← runTimedSpec + { name := "multivariate-sparse-eval", representation := "CMvPolynomial", method := "eval", + field := "KoalaBear.Field", inputShape := multivariateSparseShape, + digestIterations := checksumIterations } + preset (fun i ↦ CPoly.CMvPolynomial.eval (evalPoint (i % multivariatePointCount)) poly) - checksumKoalaBear (checksumIterations := checksumIterations) - let fastSparseEval ← runTimed - "multivariate-sparse-eval-fast" "CMvPolynomial" "eval" "KoalaBear.Fast.Field" - multivariateSparseShape preset warmup fastMeasured + checksumKoalaBear + let fastSparseEval ← runTimedSpec + { name := "multivariate-sparse-eval-fast", representation := "CMvPolynomial", method := "eval", + field := "KoalaBear.Fast.Field", inputShape := multivariateSparseShape, + digestIterations := checksumIterations } + preset (fun i ↦ CPoly.CMvPolynomial.eval (fastEvalPoint (i % multivariatePointCount)) fastPoly) - checksumKoalaBearFast (checksumIterations := checksumIterations) - let sparseHorner ← runTimed - "multivariate-sparse-horner" "CMvPolynomial" "evalHorner" "KoalaBear.Field" - multivariateSparseShape preset warmup hornerMeasured + checksumKoalaBearFast + let sparseHorner ← runTimedSpec + { name := "multivariate-sparse-horner", representation := "CMvPolynomial", + method := "evalHorner", field := "KoalaBear.Field", inputShape := multivariateSparseShape, + digestIterations := checksumIterations } + preset (fun i ↦ CPoly.CMvPolynomial.evalHorner (evalPoint (i % multivariatePointCount)) poly) - checksumKoalaBear (checksumIterations := checksumIterations) - let fastSparseHorner ← runTimed - "multivariate-sparse-horner-fast" "CMvPolynomial" "evalHorner" - "KoalaBear.Fast.Field" - multivariateSparseShape preset warmup fastHornerMeasured + checksumKoalaBear + let fastSparseHorner ← runTimedSpec + { name := "multivariate-sparse-horner-fast", representation := "CMvPolynomial", + method := "evalHorner", field := "KoalaBear.Fast.Field", + inputShape := multivariateSparseShape, digestIterations := checksumIterations } + preset (fun i ↦ CPoly.CMvPolynomial.evalHorner (fastEvalPoint (i % multivariatePointCount)) fastPoly) - checksumKoalaBearFast (checksumIterations := checksumIterations) + checksumKoalaBearFast pure ({ groupKey := "multivariate-sparse-koalabear", title := "Multivariate sparse evaluation (KoalaBear)", @@ -227,15 +215,13 @@ private def runKoalaBearMultivariateSparse (preset : BenchPreset) (gen : StdGen) private def runGoldilocksMultivariateDense (preset : BenchPreset) (gen : StdGen) : IO (BenchGroup × StdGen) := do runDenseMultivariateZMod Goldilocks.fieldSize - "multivariate-dense-goldilocks" "goldilocks" "Goldilocks.Field" "Goldilocks" - 14000 2000 400 preset gen + "multivariate-dense-goldilocks" "goldilocks" "Goldilocks.Field" "Goldilocks" preset gen /-- Run Goldilocks sparse multivariate evaluation benchmarks. -/ private def runGoldilocksMultivariateSparse (preset : BenchPreset) (gen : StdGen) : IO (BenchGroup × StdGen) := do runSparseMultivariateZMod Goldilocks.fieldSize - "multivariate-sparse-goldilocks" "goldilocks" "Goldilocks.Field" "Goldilocks" - 11000 1600 300 preset gen + "multivariate-sparse-goldilocks" "goldilocks" "Goldilocks.Field" "Goldilocks" preset gen /-- Runnable multivariate benchmark tasks. -/ def multivariateTasks : List BenchTask := [ @@ -253,9 +239,4 @@ def multivariateTasks : List BenchTask := [ runGoldilocksMultivariateSparse ] -/-- Run selected sparse and dense multivariate evaluation benchmarks. -/ -def runMultivariate (preset : BenchPreset) (selection : BenchSelection) (gen : StdGen) : - IO (Array BenchGroup × StdGen) := do - runSelectedTasks multivariateTasks preset selection gen - end CompPolyBench diff --git a/bench/CompPolyBench/Setup.lean b/bench/CompPolyBench/Setup.lean index d0c1360a..a5a56f6a 100644 --- a/bench/CompPolyBench/Setup.lean +++ b/bench/CompPolyBench/Setup.lean @@ -8,9 +8,13 @@ module public import CompPolyBench.Bivariate.Basic public import CompPolyBench.Bivariate.Factor public import CompPolyBench.Bivariate.GuruswamiSudan +public import CompPolyBench.Fields.Arith public import CompPolyBench.Fields.Binary.AdditiveNTT.Impl +public import CompPolyBench.Fields.Binary.Tower +public import CompPolyBench.Fields.Binary.Tower.Scalar public import CompPolyBench.Fields.Extension public import CompPolyBench.Fields.Montgomery +public import CompPolyBench.Harness.SelfCheck public import CompPolyBench.Multilinear.Basic public import CompPolyBench.Multivariate.CMvPolynomial public import CompPolyBench.Univariate @@ -27,8 +31,9 @@ namespace CompPolyBench /-- Runnable benchmark registry. -/ def allTasks : List BenchTask := - univariateTasks ++ multivariateTasks ++ multilinearTasks ++ bivariateTasks ++ factorTasks ++ - guruswamiSudanTasks ++ additiveNttTasks ++ extensionTasks ++ montgomeryInvTasks + harnessTasks ++ univariateTasks ++ multivariateTasks ++ multilinearTasks ++ bivariateTasks ++ + factorTasks ++ guruswamiSudanTasks ++ additiveNttTasks ++ extensionTasks ++ + montgomeryInvTasks ++ towerTasks ++ towerScalarTasks ++ fieldArithTasks /-- Metadata for every benchmark group accepted by the command-line selector. -/ def allGroupInfos : List BenchGroupInfo := @@ -78,6 +83,7 @@ def setPresetMode (current : Option BenchPreset) (preset : BenchPreset) : /-- Command selected by benchmark CLI arguments. -/ inductive BenchCommand where | run (selection : BenchSelection) (output : BenchOutput) (preset : BenchPreset) + (validateOnly : Bool) | list | help @@ -91,7 +97,12 @@ def usage : String := " lake exe CompPolyBench --groups \n" ++ " lake exe CompPolyBench [--small|--medium|--large] [--markdown-only|--json-only] " ++ " [ ...]\n" ++ - " lake exe CompPolyBench [ ...]\n" + " lake exe CompPolyBench --validate-only [--groups ]\n" ++ + " lake exe CompPolyBench [ ...]\n" ++ + "\n" ++ + " --validate-only check that each group's implementations agree, collecting\n" ++ + " no timings. This is the correctness half of the suite and\n" ++ + " is what CI runs; use the benchmark workflow for timings.\n" /-- Split a comma-separated CLI argument into nonempty group keys. -/ def splitGroupKeys (s : String) : List String := @@ -103,10 +114,11 @@ def knownGroupKey (key : String) : Bool := /-- Parse benchmark CLI arguments. -/ partial def parseArgs : List String → Except String BenchCommand - | [] => Except.ok (BenchCommand.run BenchSelection.all BenchOutput.all BenchPreset.large) + | [] => + Except.ok (BenchCommand.run BenchSelection.all BenchOutput.all BenchPreset.large false) | args => let rec go (args : List String) (keys : List String) (output : Option BenchOutput) - (preset : Option BenchPreset) : Except String BenchCommand := + (preset : Option BenchPreset) (validateOnly : Bool) : Except String BenchCommand := match args with | [] => let unknown := keys.filter fun key ↦ !knownGroupKey key @@ -116,34 +128,35 @@ partial def parseArgs : List String → Except String BenchCommand if keys.isEmpty then BenchSelection.all else BenchSelection.only keys.reverse Except.ok <| BenchCommand.run selection (output.getD BenchOutput.all) - (preset.getD BenchPreset.large) + (preset.getD BenchPreset.large) validateOnly | key :: _ => Except.error s!"unknown benchmark group `{key}`; use `--list`" | "--help" :: _ => Except.ok BenchCommand.help | "-h" :: _ => Except.ok BenchCommand.help | "--list" :: _ => Except.ok BenchCommand.list | "--small" :: rest => - setPresetMode preset BenchPreset.small >>= go rest keys output + setPresetMode preset BenchPreset.small >>= (go rest keys output · validateOnly) | "--medium" :: rest => - setPresetMode preset BenchPreset.medium >>= go rest keys output + setPresetMode preset BenchPreset.medium >>= (go rest keys output · validateOnly) | "--large" :: rest => - setPresetMode preset BenchPreset.large >>= go rest keys output + setPresetMode preset BenchPreset.large >>= (go rest keys output · validateOnly) + | "--validate-only" :: rest => go rest keys output preset true | "--markdown-only" :: rest => setOutputMode output BenchOutput.markdownOnly >>= fun output ↦ - go rest keys output preset + go rest keys output preset validateOnly | "--json-only" :: rest => setOutputMode output BenchOutput.jsonOnly >>= fun output ↦ - go rest keys output preset - | "--group" :: key :: rest => go rest (key :: keys) output preset + go rest keys output preset validateOnly + | "--group" :: key :: rest => go rest (key :: keys) output preset validateOnly | "--group" :: [] => Except.error "missing value after `--group`" | "--groups" :: rawKeys :: rest => - go rest ((splitGroupKeys rawKeys).reverse ++ keys) output preset + go rest ((splitGroupKeys rawKeys).reverse ++ keys) output preset validateOnly | "--groups" :: [] => Except.error "missing value after `--groups`" | arg :: rest => if arg.startsWith "-" then Except.error s!"unknown option `{arg}`" else - go rest (arg :: keys) output preset - go args [] none none + go rest (arg :: keys) output preset validateOnly + go args [] none none false /-- Print all runnable benchmark group keys. -/ def printGroupList : IO Unit := do @@ -152,25 +165,41 @@ def printGroupList : IO Unit := do IO.println s!" {info.groupKey} - {info.title}" /-- Run selected benchmark groups and write the requested reports. -/ -def runSelected (selection : BenchSelection) (output : BenchOutput) (preset : BenchPreset) : - IO UInt32 := do +def runSelected (selection : BenchSelection) (output : BenchOutput) (preset : BenchPreset) + (validateOnly : Bool) : IO UInt32 := do let runId ← makeRunId let gen := mkStdGen seed let (groups, _) ← runSelectedTasks allTasks preset selection gen let records := flattenGroups groups + IO.FS.createDirAll outputDir + -- Written for every run, including `--validate-only` and `--markdown-only`: a + -- result nobody can attribute to a commit and a machine is not worth keeping. + let manifest ← collectRunManifest runId preset validateOnly selection groups.size records.size + IO.FS.writeFile (manifestPath runId) manifest.render if output.writeJson then IO.FS.writeFile (resultsPath runId) (renderJsonl records) if output.writeMarkdown then - let hardware ← collectRunnerHardware - IO.FS.writeFile (reportPath runId) (renderMarkdown hardware preset groups) + if validateOnly then + IO.FS.writeFile (reportPath runId) (renderValidationMarkdown preset groups) + else + IO.FS.writeFile (reportPath runId) (renderMarkdown manifest.hardware preset groups) IO.println <| - s!"wrote {records.size} benchmark records in {groups.size} groups for run {runId}" - match checksumMismatchGroups groups with - | [] => pure 0 - | mismatchedGroups => - for group in mismatchedGroups do - IO.eprintln s!"ERROR: checksum mismatch in benchmark group `{group.groupKey}`" - pure 1 + if validateOnly then + s!"validated {records.size} benchmark records in {groups.size} groups for run {runId}" + else + s!"wrote {records.size} benchmark records in {groups.size} groups for run {runId}" + let mut failed := false + for group in checksumMismatchGroups groups do + IO.eprintln s!"ERROR: checksum mismatch in benchmark group `{group.groupKey}`" + failed := true + -- Rows of one group measure the same problem, so disagreeing on `workUnits` + -- means the group is mis-specified rather than merely unrenderable. + for group in workUnitsMismatchGroups groups do + IO.eprintln <| + s!"ERROR: rows of benchmark group `{group.groupKey}` disagree on workUnits; " ++ + "every row of a group must declare the same problem size" + failed := true + pure (if failed then 1 else 0) /-- Execute the benchmark command selected by command-line arguments. -/ def run (args : List String) : IO UInt32 := do @@ -185,7 +214,8 @@ def run (args : List String) : IO UInt32 := do | Except.ok BenchCommand.list => printGroupList pure 0 - | Except.ok (BenchCommand.run selection output preset) => - runSelected selection output preset + | Except.ok (BenchCommand.run selection output preset validateOnly) => + validateOnlyRef.set validateOnly + runSelected selection output preset validateOnly end CompPolyBench diff --git a/bench/CompPolyBench/Univariate.lean b/bench/CompPolyBench/Univariate.lean index a9fe4ff5..46cff4e2 100644 --- a/bench/CompPolyBench/Univariate.lean +++ b/bench/CompPolyBench/Univariate.lean @@ -10,6 +10,9 @@ public import CompPolyBench.Univariate.BatchEval public import CompPolyBench.Univariate.ManyEval public import CompPolyBench.Univariate.NTT.FastMul public import CompPolyBench.Univariate.NTT.FastMulLow +public import CompPolyBench.Univariate.NTT.Crossover +public import CompPolyBench.Univariate.NTT.Transform +public import CompPolyBench.Univariate.ReedSolomon public import CompPolyBench.Univariate.Roots.FiniteField /-! @@ -20,21 +23,11 @@ public section namespace CompPolyBench -/-- Benchmark group metadata for all univariate benchmark modules. -/ -def univariateGroupInfos : List BenchGroupInfo := - univariateBasicGroupInfos ++ univariateBatchEvalGroupInfos ++ - univariateManyEvalGroupInfos ++ univariateNttFastMulGroupInfos ++ - univariateNttFastMulLowGroupInfos ++ univariateFiniteFieldRootGroupInfos - /-- Runnable univariate benchmark tasks. -/ def univariateTasks : List BenchTask := univariateBasicTasks ++ univariateBatchEvalTasks ++ univariateManyEvalTasks ++ univariateNttFastMulTasks ++ - univariateNttFastMulLowTasks ++ univariateFiniteFieldRootTasks - -/-- Run selected univariate benchmarks. -/ -def runUnivariate (preset : BenchPreset) (selection : BenchSelection) (gen : StdGen) : - IO (Array BenchGroup × StdGen) := do - runSelectedTasks univariateTasks preset selection gen + univariateNttFastMulLowTasks ++ univariateFiniteFieldRootTasks ++ + nttTransformTasks ++ reedSolomonTasks ++ crossoverTasks end CompPolyBench diff --git a/bench/CompPolyBench/Univariate/Basic.lean b/bench/CompPolyBench/Univariate/Basic.lean index 57e51917..24f44d2c 100644 --- a/bench/CompPolyBench/Univariate/Basic.lean +++ b/bench/CompPolyBench/Univariate/Basic.lean @@ -6,9 +6,7 @@ Authors: Valerii Huhnin module public import CompPolyBench.Univariate.Common -public import CompPoly.Fields.BN254 -public import CompPoly.Fields.BLS12_381 -public import CompPoly.Fields.BLS12_377 +public import CompPolyBench.Fields.Inputs public import CompPoly.Univariate.NTT.FastMulLow public import CompPoly.Univariate.NTTFast.FastMulLow @@ -22,45 +20,27 @@ open CompPoly namespace CompPolyBench -/-- Benchmark group metadata for `CompPoly.Univariate.Basic`. -/ -def univariateBasicGroupInfos : List BenchGroupInfo := [ - ⟨"univariate-dense-koalabear", "Univariate dense evaluation (KoalaBear)"⟩, - ⟨"univariate-sparse-koalabear", "Univariate sparse evaluation (KoalaBear)"⟩, - ⟨"univariate-monic-remainder-small-koalabear", - "Univariate monic remainder, small (KoalaBear)"⟩, - ⟨"univariate-monic-remainder-medium-koalabear", - "Univariate monic remainder, medium (KoalaBear)"⟩, - ⟨"univariate-dense-goldilocks", "Univariate dense evaluation (Goldilocks)"⟩, - ⟨"univariate-dense-bn254", "Univariate dense evaluation (BN254)"⟩, - ⟨"univariate-dense-bls12-381", "Univariate dense evaluation (BLS12-381)"⟩, - ⟨"univariate-dense-bls12-377", "Univariate dense evaluation (BLS12-377)"⟩, - ⟨"univariate-dense-babybear", "Univariate dense evaluation (BabyBear)"⟩ -] - /-- Benchmark dense univariate evaluation over a generic prime `ZMod` field. -/ private def runDenseUnivariateZMod (modulus : Nat) [Fact (Nat.Prime modulus)] (key nameSuffix fieldName fieldTitle : String) - (largeHornerMeasured mediumHornerMeasured smallHornerMeasured : Nat) (preset : BenchPreset) (gen : StdGen) : IO (BenchGroup × StdGen) := do let (denseCoeffs, gen) := (zmodArray modulus 512 false).run gen let (points, gen) := (zmodArray modulus 32 false).run gen let densePoly := cpolyOfArray denseCoeffs - let warmup := warmupIterations preset - let measured := measuredIterations preset - let hornerMeasured := - preset.selectNat largeHornerMeasured mediumHornerMeasured smallHornerMeasured - let checksumIterations := groupChecksumIterations measured [hornerMeasured] - let sumRecord ← runTimed - ("univariate-dense-sum-" ++ nameSuffix) "CPolynomial" "eval sum-of-powers" fieldName - "degree<512, dense, 32 points" preset warmup measured - (fun i ↦ CPolynomial.eval (points.getD (i % points.size) 0) densePoly) - checksumZMod (checksumIterations := checksumIterations) - let hornerRecord ← runTimed - ("univariate-dense-horner-" ++ nameSuffix) "CPolynomial" "evalHorner" fieldName - "degree<512, dense, 32 points" preset warmup hornerMeasured - (fun i ↦ CPolynomial.evalHorner (points.getD (i % points.size) 0) densePoly) - checksumZMod (checksumIterations := checksumIterations) + let checksumIterations := digestPeriod points.size + let sumRecord ← runTimedSpec + { name := ("univariate-dense-sum-" ++ nameSuffix), representation := "CPolynomial", + method := "eval sum-of-powers", field := fieldName, + inputShape := "degree<512, dense, 32 points", digestIterations := checksumIterations } + preset (fun i ↦ CPolynomial.eval (points.getD (i % points.size) 0) densePoly) + checksumZMod + let hornerRecord ← runTimedSpec + { name := ("univariate-dense-horner-" ++ nameSuffix), representation := "CPolynomial", + method := "evalHorner", field := fieldName, inputShape := "degree<512, dense, 32 points", + digestIterations := checksumIterations } + preset + (fun i ↦ CPolynomial.evalHorner (points.getD (i % points.size) 0) densePoly) checksumZMod pure ({ groupKey := key, title := "Univariate dense evaluation (" ++ fieldTitle ++ ")", @@ -76,7 +56,6 @@ private def runDenseUnivariateWithFast {F G : Type} (key fieldTitle canonicalFieldName fastFieldName : String) (genCoeffs : Nat → StateM StdGen (Array F)) (toFast : Array F → Array G) (canonicalChecksum : F → Nat) (fastChecksum : G → Nat) - (hornerBudget fastBudget fastHornerBudget : BenchPreset → Nat) (preset : BenchPreset) (gen : StdGen) : IO (BenchGroup × StdGen) := do let (denseCoeffs, gen) := (genCoeffs 512).run gen @@ -85,34 +64,32 @@ private def runDenseUnivariateWithFast {F G : Type} let fastDenseCoeffs := toFast denseCoeffs let fastPoints := toFast points let fastDensePoly := cpolyOfArray fastDenseCoeffs - let warmup := warmupIterations preset - let measured := measuredIterations preset - let hornerMeasured := hornerBudget preset - let fastMeasured := fastBudget preset - let fastHornerMeasured := fastHornerBudget preset - let checksumIterations := groupChecksumIterations measured [ - hornerMeasured, fastMeasured, fastHornerMeasured - ] - let denseSum ← runTimed - "univariate-dense-sum" "CPolynomial" "eval sum-of-powers" canonicalFieldName - "degree<512, dense, 32 points" preset warmup measured - (fun i ↦ CPolynomial.eval (points.getD (i % points.size) 0) densePoly) - canonicalChecksum (checksumIterations := checksumIterations) - let fastDenseSum ← runTimed - "univariate-dense-sum-fast" "CPolynomial" "eval sum-of-powers" - fastFieldName "degree<512, dense, 32 points" preset warmup fastMeasured - (fun i ↦ CPolynomial.eval (fastPoints.getD (i % fastPoints.size) 0) fastDensePoly) - fastChecksum (checksumIterations := checksumIterations) - let denseHorner ← runTimed - "univariate-dense-horner" "CPolynomial" "evalHorner" canonicalFieldName - "degree<512, dense, 32 points" preset warmup hornerMeasured - (fun i ↦ CPolynomial.evalHorner (points.getD (i % points.size) 0) densePoly) - canonicalChecksum (checksumIterations := checksumIterations) - let fastDenseHorner ← runTimed - "univariate-dense-horner-fast" "CPolynomial" "evalHorner" fastFieldName - "degree<512, dense, 32 points" preset warmup fastHornerMeasured + let checksumIterations := digestPeriod points.size + let denseSum ← runTimedSpec + { name := "univariate-dense-sum", representation := "CPolynomial", + method := "eval sum-of-powers", field := canonicalFieldName, + inputShape := "degree<512, dense, 32 points", digestIterations := checksumIterations } + preset (fun i ↦ CPolynomial.eval (points.getD (i % points.size) 0) densePoly) + canonicalChecksum + let fastDenseSum ← runTimedSpec + { name := "univariate-dense-sum-fast", representation := "CPolynomial", + method := "eval sum-of-powers", field := fastFieldName, + inputShape := "degree<512, dense, 32 points", digestIterations := checksumIterations } + preset + (fun i ↦ CPolynomial.eval (fastPoints.getD (i % fastPoints.size) 0) fastDensePoly) fastChecksum + let denseHorner ← runTimedSpec + { name := "univariate-dense-horner", representation := "CPolynomial", method := "evalHorner", + field := canonicalFieldName, inputShape := "degree<512, dense, 32 points", + digestIterations := checksumIterations } + preset + (fun i ↦ CPolynomial.evalHorner (points.getD (i % points.size) 0) densePoly) canonicalChecksum + let fastDenseHorner ← runTimedSpec + { name := "univariate-dense-horner-fast", representation := "CPolynomial", + method := "evalHorner", field := fastFieldName, inputShape := "degree<512, dense, 32 points", + digestIterations := checksumIterations } + preset (fun i ↦ CPolynomial.evalHorner (fastPoints.getD (i % fastPoints.size) 0) fastDensePoly) - fastChecksum (checksumIterations := checksumIterations) + fastChecksum pure ({ groupKey := key, title := "Univariate dense evaluation (" ++ fieldTitle ++ ")", @@ -126,8 +103,7 @@ private def runKoalaBearUnivariateDense (preset : BenchPreset) (gen : StdGen) : "univariate-dense-koalabear" "KoalaBear" "KoalaBear.Field" "KoalaBear.Fast.Field" (fun size ↦ koalaBearArray size false) koalaBearFastArray checksumKoalaBear checksumKoalaBearFast - (fun p ↦ p.selectNat 45000 6500 1300) (fun p ↦ p.selectNat 63000 9000 1800) - (fun p ↦ p.selectNat 490000 70000 14000) preset gen + preset gen /-- Benchmark dense BabyBear univariate evaluation. -/ private def runBabyBearUnivariateDense (preset : BenchPreset) (gen : StdGen) : @@ -136,8 +112,7 @@ private def runBabyBearUnivariateDense (preset : BenchPreset) (gen : StdGen) : "univariate-dense-babybear" "BabyBear" "BabyBear.Field" "BabyBear.Fast.Field" (fun size ↦ babyBearArray size false) babyBearFastArray checksumBabyBear checksumBabyBearFast - (fun p ↦ p.selectNat 45000 6500 1300) (fun p ↦ p.selectNat 63000 9000 1800) - (fun p ↦ p.selectNat 490000 70000 14000) preset gen + preset gen /-- Benchmark sparse KoalaBear univariate evaluation. -/ private def runKoalaBearUnivariateSparse (preset : BenchPreset) (gen : StdGen) : @@ -148,36 +123,37 @@ private def runKoalaBearUnivariateSparse (preset : BenchPreset) (gen : StdGen) : let fastSparseCoeffs := koalaBearFastArray sparseCoeffs let fastPoints := koalaBearFastArray points let fastSparsePoly := cpolyOfArray fastSparseCoeffs - let warmup := warmupIterations preset - let measured := measuredIterations preset - let hornerMeasured := preset.selectNat 50000 7000 1500 - let fastMeasured := preset.selectNat 63000 9000 1800 - let fastHornerMeasured := preset.selectNat 490000 70000 14000 - let checksumIterations := groupChecksumIterations measured [ - hornerMeasured, fastMeasured, fastHornerMeasured - ] - let sparseSum ← runTimed - "univariate-sparse-sum" "CPolynomial" "eval sum-of-powers" "KoalaBear.Field" - "degree<512, one nonzero per 4 coeffs, 32 points" preset warmup measured - (fun i ↦ CPolynomial.eval (points.getD (i % points.size) 0) sparsePoly) - checksumKoalaBear (checksumIterations := checksumIterations) - let fastSparseSum ← runTimed - "univariate-sparse-sum-fast" "CPolynomial" "eval sum-of-powers" - "KoalaBear.Fast.Field" - "degree<512, one nonzero per 4 coeffs, 32 points" preset warmup fastMeasured + let checksumIterations := digestPeriod points.size + let sparseSum ← runTimedSpec + { name := "univariate-sparse-sum", representation := "CPolynomial", + method := "eval sum-of-powers", field := "KoalaBear.Field", + inputShape := "degree<512, one nonzero per 4 coeffs, 32 points", + digestIterations := checksumIterations } + preset (fun i ↦ CPolynomial.eval (points.getD (i % points.size) 0) sparsePoly) + checksumKoalaBear + let fastSparseSum ← runTimedSpec + { name := "univariate-sparse-sum-fast", representation := "CPolynomial", + method := "eval sum-of-powers", field := "KoalaBear.Fast.Field", + inputShape := "degree<512, one nonzero per 4 coeffs, 32 points", + digestIterations := checksumIterations } + preset (fun i ↦ CPolynomial.eval (fastPoints.getD (i % fastPoints.size) 0) fastSparsePoly) - checksumKoalaBearFast (checksumIterations := checksumIterations) - let sparseHorner ← runTimed - "univariate-sparse-horner" "CPolynomial" "evalHorner" "KoalaBear.Field" - "degree<512, one nonzero per 4 coeffs, 32 points" preset warmup hornerMeasured - (fun i ↦ CPolynomial.evalHorner (points.getD (i % points.size) 0) sparsePoly) - checksumKoalaBear (checksumIterations := checksumIterations) - let fastSparseHorner ← runTimed - "univariate-sparse-horner-fast" "CPolynomial" "evalHorner" "KoalaBear.Fast.Field" - "degree<512, one nonzero per 4 coeffs, 32 points" preset warmup fastHornerMeasured + checksumKoalaBearFast + let sparseHorner ← runTimedSpec + { name := "univariate-sparse-horner", representation := "CPolynomial", method := "evalHorner", + field := "KoalaBear.Field", inputShape := "degree<512, one nonzero per 4 coeffs, 32 points", + digestIterations := checksumIterations } + preset + (fun i ↦ CPolynomial.evalHorner (points.getD (i % points.size) 0) sparsePoly) checksumKoalaBear + let fastSparseHorner ← runTimedSpec + { name := "univariate-sparse-horner-fast", representation := "CPolynomial", + method := "evalHorner", field := "KoalaBear.Fast.Field", + inputShape := "degree<512, one nonzero per 4 coeffs, 32 points", + digestIterations := checksumIterations } + preset (fun i ↦ CPolynomial.evalHorner (fastPoints.getD (i % fastPoints.size) 0) fastSparsePoly) - checksumKoalaBearFast (checksumIterations := checksumIterations) + checksumKoalaBearFast pure ({ groupKey := "univariate-sparse-koalabear", title := "Univariate sparse evaluation (KoalaBear)", @@ -220,81 +196,79 @@ private def runKoalaBearUnivariateMonicRemainderSmall (preset : BenchPreset) (ge CPolynomial.ModContext.reversal fastNttWithFallbackLowMul let fastReversalNttFastLowMod : CPolynomial.ModContext KoalaBear.Fast.Field := CPolynomial.ModContext.reversal fastNttFastWithFallbackLowMul - let warmup := modWarmupIterations preset - let measured := modMeasuredIterations preset - let remainderMeasured := preset.selectNat 3400 500 100 - let reversalConvolutionMeasured := preset.selectNat 650 100 20 - let reversalNttMeasured := preset.selectNat 550 80 20 - let reversalNttFastMeasured := preset.selectNat 2200 300 60 - let fastMeasured := preset.selectNat 140 20 4 - let fastRemainderMeasured := preset.selectNat 15400 2200 440 - let fastReversalConvolutionMeasured := preset.selectNat 1400 200 40 - let fastReversalNttMeasured := preset.selectNat 2100 300 60 - let fastReversalNttFastMeasured := preset.selectNat 11200 1600 320 - let checksumIterations := groupChecksumIterations measured [ - remainderMeasured, reversalConvolutionMeasured, reversalNttMeasured, reversalNttFastMeasured, - fastMeasured, fastRemainderMeasured, fastReversalConvolutionMeasured, - fastReversalNttMeasured, fastReversalNttFastMeasured - ] - let smallModNaive ← runTimed - "univariate-mod-by-monic-naive" "CPolynomial" "modByMonic" "KoalaBear.Field" - univariateModShape preset warmup measured - (fun _ ↦ CPolynomial.modByMonic batchPoly modDivisor) - (checksumCPolynomial checksumKoalaBear) (checksumIterations := checksumIterations) - let fastSmallModNaive ← runTimed - "univariate-mod-by-monic-naive-fast" "CPolynomial" "modByMonic" - "KoalaBear.Fast.Field" - univariateModShape preset warmup fastMeasured - (fun _ ↦ CPolynomial.modByMonic fastBatchPoly fastModDivisor) - (checksumCPolynomial checksumKoalaBearFast) (checksumIterations := checksumIterations) - let smallModRemainder ← runTimed - "univariate-mod-by-monic-remainder-only" "CPolynomial" "modByMonicRemainderOnly" - "KoalaBear.Field" - univariateModShape preset warmup remainderMeasured + let checksumIterations := digestPeriod 1 + let smallModNaive ← runTimedSpec + { name := "univariate-mod-by-monic-naive", representation := "CPolynomial", + method := "modByMonic", field := "KoalaBear.Field", inputShape := univariateModShape, + digestIterations := checksumIterations } + preset (fun _ ↦ CPolynomial.modByMonic batchPoly modDivisor) + (checksumCPolynomial checksumKoalaBear) + let fastSmallModNaive ← runTimedSpec + { name := "univariate-mod-by-monic-naive-fast", representation := "CPolynomial", + method := "modByMonic", field := "KoalaBear.Fast.Field", inputShape := univariateModShape, + digestIterations := checksumIterations } + preset (fun _ ↦ CPolynomial.modByMonic fastBatchPoly fastModDivisor) + (checksumCPolynomial checksumKoalaBearFast) + let smallModRemainder ← runTimedSpec + { name := "univariate-mod-by-monic-remainder-only", representation := "CPolynomial", + method := "modByMonicRemainderOnly", field := "KoalaBear.Field", + inputShape := univariateModShape, digestIterations := checksumIterations } + preset (fun _ ↦ CPolynomial.modByMonicRemainderOnly batchPoly modDivisor) - (checksumCPolynomial checksumKoalaBear) (checksumIterations := checksumIterations) - let fastSmallModRemainder ← runTimed - "univariate-mod-by-monic-remainder-only-fast" "CPolynomial" "modByMonicRemainderOnly" - "KoalaBear.Fast.Field" - univariateModShape preset warmup fastRemainderMeasured + (checksumCPolynomial checksumKoalaBear) + let fastSmallModRemainder ← runTimedSpec + { name := "univariate-mod-by-monic-remainder-only-fast", representation := "CPolynomial", + method := "modByMonicRemainderOnly", field := "KoalaBear.Fast.Field", + inputShape := univariateModShape, digestIterations := checksumIterations } + preset (fun _ ↦ CPolynomial.modByMonicRemainderOnly fastBatchPoly fastModDivisor) - (checksumCPolynomial checksumKoalaBearFast) (checksumIterations := checksumIterations) - let smallModReversalConvolution ← runTimed - "univariate-mod-by-monic-reversal-convolution-low-mul" "CPolynomial" - "modByMonicByReversal, MulLowContext.convolution" "KoalaBear.Field" - univariateModShape preset warmup reversalConvolutionMeasured + (checksumCPolynomial checksumKoalaBearFast) + let smallModReversalConvolution ← runTimedSpec + { name := "univariate-mod-by-monic-reversal-convolution-low-mul", + representation := "CPolynomial", method := "modByMonicByReversal, MulLowContext.convolution", + field := "KoalaBear.Field", inputShape := univariateModShape, + digestIterations := checksumIterations } + preset (fun _ ↦ reversalConvolutionLowMod.modByMonic batchPoly modDivisor) - (checksumCPolynomial checksumKoalaBear) (checksumIterations := checksumIterations) - let fastSmallModReversalConvolution ← runTimed - "univariate-mod-by-monic-reversal-convolution-low-mul-fast" "CPolynomial" - "modByMonicByReversal, MulLowContext.convolution" "KoalaBear.Fast.Field" - univariateModShape preset warmup fastReversalConvolutionMeasured + (checksumCPolynomial checksumKoalaBear) + let fastSmallModReversalConvolution ← runTimedSpec + { name := "univariate-mod-by-monic-reversal-convolution-low-mul-fast", + representation := "CPolynomial", method := "modByMonicByReversal, MulLowContext.convolution", + field := "KoalaBear.Fast.Field", inputShape := univariateModShape, + digestIterations := checksumIterations } + preset (fun _ ↦ fastReversalConvolutionLowMod.modByMonic fastBatchPoly fastModDivisor) - (checksumCPolynomial checksumKoalaBearFast) (checksumIterations := checksumIterations) - let smallModReversalNtt ← runTimed - "univariate-mod-by-monic-reversal-ntt-low-mul" "CPolynomial" - "modByMonicByReversal, FastMulLow.withFallback" "KoalaBear.Field" - univariateModShape preset warmup reversalNttMeasured - (fun _ ↦ reversalNttLowMod.modByMonic batchPoly modDivisor) - (checksumCPolynomial checksumKoalaBear) (checksumIterations := checksumIterations) - let fastSmallModReversalNtt ← runTimed - "univariate-mod-by-monic-reversal-ntt-low-mul-fast" "CPolynomial" - "modByMonicByReversal, FastMulLow.withFallback" "KoalaBear.Fast.Field" - univariateModShape preset warmup fastReversalNttMeasured + (checksumCPolynomial checksumKoalaBearFast) + let smallModReversalNtt ← runTimedSpec + { name := "univariate-mod-by-monic-reversal-ntt-low-mul", representation := "CPolynomial", + method := "modByMonicByReversal, FastMulLow.withFallback", field := "KoalaBear.Field", + inputShape := univariateModShape, digestIterations := checksumIterations } + preset (fun _ ↦ reversalNttLowMod.modByMonic batchPoly modDivisor) + (checksumCPolynomial checksumKoalaBear) + let fastSmallModReversalNtt ← runTimedSpec + { name := "univariate-mod-by-monic-reversal-ntt-low-mul-fast", representation := "CPolynomial", + method := "modByMonicByReversal, FastMulLow.withFallback", field := "KoalaBear.Fast.Field", + inputShape := univariateModShape, digestIterations := checksumIterations } + preset (fun _ ↦ fastReversalNttLowMod.modByMonic fastBatchPoly fastModDivisor) - (checksumCPolynomial checksumKoalaBearFast) (checksumIterations := checksumIterations) - let smallModReversalNttFast ← runTimed - "univariate-mod-by-monic-reversal-ntt-fast-low-mul" "CPolynomial" - "modByMonicByReversal, NTTFast.FastMulLow.withFallback" "KoalaBear.Field" - univariateModShape preset warmup reversalNttFastMeasured + (checksumCPolynomial checksumKoalaBearFast) + let smallModReversalNttFast ← runTimedSpec + { name := "univariate-mod-by-monic-reversal-ntt-fast-low-mul", representation := "CPolynomial", + method := "modByMonicByReversal, NTTFast.FastMulLow.withFallback", + field := "KoalaBear.Field", inputShape := univariateModShape, + digestIterations := checksumIterations } + preset (fun _ ↦ reversalNttFastLowMod.modByMonic batchPoly modDivisor) - (checksumCPolynomial checksumKoalaBear) (checksumIterations := checksumIterations) - let fastSmallModReversalNttFast ← runTimed - "univariate-mod-by-monic-reversal-ntt-fast-low-mul-fast" "CPolynomial" - "modByMonicByReversal, NTTFast.FastMulLow.withFallback" "KoalaBear.Fast.Field" - univariateModShape preset warmup fastReversalNttFastMeasured + (checksumCPolynomial checksumKoalaBear) + let fastSmallModReversalNttFast ← runTimedSpec + { name := "univariate-mod-by-monic-reversal-ntt-fast-low-mul-fast", + representation := "CPolynomial", + method := "modByMonicByReversal, NTTFast.FastMulLow.withFallback", + field := "KoalaBear.Fast.Field", inputShape := univariateModShape, + digestIterations := checksumIterations } + preset (fun _ ↦ fastReversalNttFastLowMod.modByMonic fastBatchPoly fastModDivisor) - (checksumCPolynomial checksumKoalaBearFast) (checksumIterations := checksumIterations) + (checksumCPolynomial checksumKoalaBearFast) pure ({ groupKey := "univariate-monic-remainder-small-koalabear", title := "Univariate monic remainder, small (KoalaBear)", @@ -340,71 +314,76 @@ private def runKoalaBearUnivariateMonicRemainderMedium (preset : BenchPreset) (g CPolynomial.ModContext.reversal fastNttWithFallbackLowMul let fastReversalNttFastLowMod : CPolynomial.ModContext KoalaBear.Fast.Field := CPolynomial.ModContext.reversal fastNttFastWithFallbackLowMul - let warmup := mediumModWarmupIterations preset - let measured := mediumModMeasuredIterations preset - let remainderMeasured := preset.selectNat 30 5 1 - let reversalNttMeasured := preset.selectNat 200 30 5 - let reversalNttFastMeasured := preset.selectNat 900 130 30 - let fastRemainderMeasured := preset.selectNat 175 25 5 - let fastMeasured := preset.selectNat 14 2 1 - let fastReversalNttMeasured := preset.selectNat 840 120 24 - let fastReversalNttFastMeasured := preset.selectNat 9800 1400 280 - let checksumIterations := groupChecksumIterations measured [ - remainderMeasured, reversalNttMeasured, reversalNttFastMeasured, fastRemainderMeasured, - fastMeasured, fastReversalNttMeasured, fastReversalNttFastMeasured - ] - let mediumModRemainder ← runTimed - "univariate-mod-by-monic-medium-remainder-only" "CPolynomial" - "modByMonicRemainderOnly" "KoalaBear.Field" - mediumUnivariateModShape preset warmup remainderMeasured + let checksumIterations := digestPeriod 1 + let mediumModRemainder ← runTimedSpec + { name := "univariate-mod-by-monic-medium-remainder-only", representation := "CPolynomial", + method := "modByMonicRemainderOnly", field := "KoalaBear.Field", + inputShape := mediumUnivariateModShape, digestIterations := checksumIterations } + preset (fun _ ↦ CPolynomial.modByMonicRemainderOnly mediumBatchPoly mediumModDivisor) - (checksumCPolynomial checksumKoalaBear) (checksumIterations := checksumIterations) - let fastMediumModRemainder ← runTimed - "univariate-mod-by-monic-medium-remainder-only-fast" "CPolynomial" - "modByMonicRemainderOnly" "KoalaBear.Fast.Field" - mediumUnivariateModShape preset warmup fastRemainderMeasured + (checksumCPolynomial checksumKoalaBear) + let fastMediumModRemainder ← runTimedSpec + { name := "univariate-mod-by-monic-medium-remainder-only-fast", + representation := "CPolynomial", method := "modByMonicRemainderOnly", + field := "KoalaBear.Fast.Field", inputShape := mediumUnivariateModShape, + digestIterations := checksumIterations } + preset (fun _ ↦ CPolynomial.modByMonicRemainderOnly fastMediumBatchPoly fastMediumModDivisor) - (checksumCPolynomial checksumKoalaBearFast) (checksumIterations := checksumIterations) - let mediumModReversalConvolution ← runTimed - "univariate-mod-by-monic-medium-reversal-convolution-low-mul" "CPolynomial" - "modByMonicByReversal, MulLowContext.convolution" "KoalaBear.Field" - mediumUnivariateModShape preset warmup measured + (checksumCPolynomial checksumKoalaBearFast) + let mediumModReversalConvolution ← runTimedSpec + { name := "univariate-mod-by-monic-medium-reversal-convolution-low-mul", + representation := "CPolynomial", method := "modByMonicByReversal, MulLowContext.convolution", + field := "KoalaBear.Field", inputShape := mediumUnivariateModShape, + digestIterations := checksumIterations } + preset (fun _ ↦ reversalConvolutionLowMod.modByMonic mediumBatchPoly mediumModDivisor) - (checksumCPolynomial checksumKoalaBear) (checksumIterations := checksumIterations) - let fastMediumModReversalConvolution ← runTimed - "univariate-mod-by-monic-medium-reversal-convolution-low-mul-fast" "CPolynomial" - "modByMonicByReversal, MulLowContext.convolution" "KoalaBear.Fast.Field" - mediumUnivariateModShape preset warmup fastMeasured + (checksumCPolynomial checksumKoalaBear) + let fastMediumModReversalConvolution ← runTimedSpec + { name := "univariate-mod-by-monic-medium-reversal-convolution-low-mul-fast", + representation := "CPolynomial", method := "modByMonicByReversal, MulLowContext.convolution", + field := "KoalaBear.Fast.Field", inputShape := mediumUnivariateModShape, + digestIterations := checksumIterations } + preset (fun _ ↦ fastReversalConvolutionLowMod.modByMonic fastMediumBatchPoly fastMediumModDivisor) - (checksumCPolynomial checksumKoalaBearFast) (checksumIterations := checksumIterations) - let mediumModReversalNtt ← runTimed - "univariate-mod-by-monic-medium-reversal-ntt-low-mul" "CPolynomial" - "modByMonicByReversal, FastMulLow.withFallback" "KoalaBear.Field" - mediumUnivariateModShape preset warmup reversalNttMeasured + (checksumCPolynomial checksumKoalaBearFast) + let mediumModReversalNtt ← runTimedSpec + { name := "univariate-mod-by-monic-medium-reversal-ntt-low-mul", + representation := "CPolynomial", method := "modByMonicByReversal, FastMulLow.withFallback", + field := "KoalaBear.Field", inputShape := mediumUnivariateModShape, + digestIterations := checksumIterations } + preset (fun _ ↦ reversalNttLowMod.modByMonic mediumBatchPoly mediumModDivisor) - (checksumCPolynomial checksumKoalaBear) (checksumIterations := checksumIterations) - let fastMediumModReversalNtt ← runTimed - "univariate-mod-by-monic-medium-reversal-ntt-low-mul-fast" "CPolynomial" - "modByMonicByReversal, FastMulLow.withFallback" "KoalaBear.Fast.Field" - mediumUnivariateModShape preset warmup fastReversalNttMeasured + (checksumCPolynomial checksumKoalaBear) + let fastMediumModReversalNtt ← runTimedSpec + { name := "univariate-mod-by-monic-medium-reversal-ntt-low-mul-fast", + representation := "CPolynomial", method := "modByMonicByReversal, FastMulLow.withFallback", + field := "KoalaBear.Fast.Field", inputShape := mediumUnivariateModShape, + digestIterations := checksumIterations } + preset (fun _ ↦ fastReversalNttLowMod.modByMonic fastMediumBatchPoly fastMediumModDivisor) - (checksumCPolynomial checksumKoalaBearFast) (checksumIterations := checksumIterations) - let mediumModReversalNttFast ← runTimed - "univariate-mod-by-monic-medium-reversal-ntt-fast-low-mul" "CPolynomial" - "modByMonicByReversal, NTTFast.FastMulLow.withFallback" "KoalaBear.Field" - mediumUnivariateModShape preset warmup reversalNttFastMeasured + (checksumCPolynomial checksumKoalaBearFast) + let mediumModReversalNttFast ← runTimedSpec + { name := "univariate-mod-by-monic-medium-reversal-ntt-fast-low-mul", + representation := "CPolynomial", + method := "modByMonicByReversal, NTTFast.FastMulLow.withFallback", + field := "KoalaBear.Field", inputShape := mediumUnivariateModShape, + digestIterations := checksumIterations } + preset (fun _ ↦ reversalNttFastLowMod.modByMonic mediumBatchPoly mediumModDivisor) - (checksumCPolynomial checksumKoalaBear) (checksumIterations := checksumIterations) - let fastMediumModReversalNttFast ← runTimed - "univariate-mod-by-monic-medium-reversal-ntt-fast-low-mul-fast" "CPolynomial" - "modByMonicByReversal, NTTFast.FastMulLow.withFallback" "KoalaBear.Fast.Field" - mediumUnivariateModShape preset warmup fastReversalNttFastMeasured + (checksumCPolynomial checksumKoalaBear) + let fastMediumModReversalNttFast ← runTimedSpec + { name := "univariate-mod-by-monic-medium-reversal-ntt-fast-low-mul-fast", + representation := "CPolynomial", + method := "modByMonicByReversal, NTTFast.FastMulLow.withFallback", + field := "KoalaBear.Fast.Field", inputShape := mediumUnivariateModShape, + digestIterations := checksumIterations } + preset (fun _ ↦ fastReversalNttFastLowMod.modByMonic fastMediumBatchPoly fastMediumModDivisor) - (checksumCPolynomial checksumKoalaBearFast) (checksumIterations := checksumIterations) + (checksumCPolynomial checksumKoalaBearFast) pure ({ groupKey := "univariate-monic-remainder-medium-koalabear", title := "Univariate monic remainder, medium (KoalaBear)", @@ -418,15 +397,7 @@ private def runGoldilocksUnivariateDense (preset : BenchPreset) (gen : StdGen) : IO (BenchGroup × StdGen) := do runDenseUnivariateZMod Goldilocks.fieldSize "univariate-dense-goldilocks" "goldilocks" "Goldilocks.Field" - "Goldilocks" 40000 6000 1200 preset gen - -/-- Convert BN254 field inputs to the native eight-limb representation. -/ -private def bn254FastArray (xs : Array BN254.ScalarField) : Array BN254.Fast.ScalarField := - xs.map BN254.Fast.ofField - -/-- Convert a fast BN254 element to a checksum word. -/ -private def checksumBn254Fast (x : BN254.Fast.ScalarField) : Nat := - x.toNat + "Goldilocks" preset gen /-- Benchmark dense BN254 univariate evaluation. -/ private def runBn254UnivariateDense (preset : BenchPreset) (gen : StdGen) : @@ -435,17 +406,7 @@ private def runBn254UnivariateDense (preset : BenchPreset) (gen : StdGen) : "univariate-dense-bn254" "BN254" "BN254.ScalarField" "BN254.Fast.ScalarField" (fun size ↦ zmodArray BN254.scalarFieldSize size false) bn254FastArray checksumZMod checksumBn254Fast - (fun p ↦ p.selectNat 40000 6000 1200) (fun p ↦ p.selectNat 20000 3000 600) - (fun p ↦ p.selectNat 160000 23000 4600) preset gen - -/-- Convert BLS12-381 field inputs to the native eight-limb representation. -/ -private def bls12_381FastArray (xs : Array BLS12_381.ScalarField) : - Array BLS12_381.Fast.ScalarField := - xs.map BLS12_381.Fast.ofField - -/-- Convert a fast BLS12-381 element to a checksum word. -/ -private def checksumBls12_381Fast (x : BLS12_381.Fast.ScalarField) : Nat := - x.toNat + preset gen /-- Benchmark dense BLS12-381 univariate evaluation. -/ private def runBls12_381UnivariateDense (preset : BenchPreset) (gen : StdGen) : @@ -455,17 +416,7 @@ private def runBls12_381UnivariateDense (preset : BenchPreset) (gen : StdGen) : "BLS12_381.Fast.ScalarField" (fun size ↦ zmodArray BLS12_381.scalarFieldSize size false) bls12_381FastArray checksumZMod checksumBls12_381Fast - (fun p ↦ p.selectNat 40000 6000 1200) (fun p ↦ p.selectNat 20000 3000 600) - (fun p ↦ p.selectNat 160000 23000 4600) preset gen - -/-- Convert BLS12-377 field inputs to the native eight-limb representation. -/ -private def bls12_377FastArray (xs : Array BLS12_377.ScalarField) : - Array BLS12_377.Fast.ScalarField := - xs.map BLS12_377.Fast.ofField - -/-- Convert a fast BLS12-377 element to a checksum word. -/ -private def checksumBls12_377Fast (x : BLS12_377.Fast.ScalarField) : Nat := - x.toNat + preset gen /-- Benchmark dense BLS12-377 univariate evaluation. -/ private def runBls12_377UnivariateDense (preset : BenchPreset) (gen : StdGen) : @@ -475,8 +426,7 @@ private def runBls12_377UnivariateDense (preset : BenchPreset) (gen : StdGen) : "BLS12_377.Fast.ScalarField" (fun size ↦ zmodArray BLS12_377.scalarFieldSize size false) bls12_377FastArray checksumZMod checksumBls12_377Fast - (fun p ↦ p.selectNat 40000 6000 1200) (fun p ↦ p.selectNat 20000 3000 600) - (fun p ↦ p.selectNat 160000 23000 4600) preset gen + preset gen /-- Runnable `CompPoly.Univariate.Basic` benchmark tasks. -/ def univariateBasicTasks : List BenchTask := [ @@ -511,9 +461,4 @@ def univariateBasicTasks : List BenchTask := [ runBabyBearUnivariateDense ] -/-- Run selected evaluation and public monic-remainder benchmarks. -/ -def runUnivariateBasic (preset : BenchPreset) (selection : BenchSelection) (gen : StdGen) : - IO (Array BenchGroup × StdGen) := do - runSelectedTasks univariateBasicTasks preset selection gen - end CompPolyBench diff --git a/bench/CompPolyBench/Univariate/BatchEval.lean b/bench/CompPolyBench/Univariate/BatchEval.lean index 989339c6..b72e6720 100644 --- a/bench/CompPolyBench/Univariate/BatchEval.lean +++ b/bench/CompPolyBench/Univariate/BatchEval.lean @@ -20,13 +20,6 @@ open CompPoly namespace CompPolyBench -/-- Benchmark group metadata for `CompPoly.Univariate.BatchEval`. -/ -def univariateBatchEvalGroupInfos : List BenchGroupInfo := [ - ⟨"univariate-batch-small-koalabear", "Univariate batch evaluation, small (KoalaBear)"⟩, - ⟨"univariate-batch-medium-koalabear", "Univariate batch evaluation, medium (KoalaBear)"⟩, - ⟨"univariate-batch-large-koalabear", "Univariate batch evaluation, large (KoalaBear)"⟩ -] - /-- Run the small KoalaBear univariate batch-evaluation benchmark group. -/ private def runKoalaBearUnivariateBatchSmall (preset : BenchPreset) (gen : StdGen) : IO (BenchGroup × StdGen) := do @@ -79,145 +72,159 @@ private def runKoalaBearUnivariateBatchSmall (preset : BenchPreset) (gen : StdGe CPolynomial.ModContext.reversal fastNttWithFallbackLowMul let fastReversalNttFastLowMod : CPolynomial.ModContext KoalaBear.Fast.Field := CPolynomial.ModContext.reversal fastNttFastWithFallbackLowMul - let warmup := batchWarmupIterations preset - let measured := batchMeasuredIterations preset - let sumMeasured := preset.selectNat 1750 250 50 - let hornerMeasured := preset.selectNat 11500 1600 300 - let remainderMeasured := preset.selectNat 900 130 30 - let nttMeasured := preset.selectNat 700 100 20 - let nttFastMeasured := preset.selectNat 800 120 25 - let reversalConvolutionMeasured := preset.selectNat 150 20 5 - let reversalNttMeasured := preset.selectNat 150 20 5 - let reversalNttFastMeasured := preset.selectNat 450 60 15 - let fastSumMeasured := preset.selectNat 19600 2800 560 - let fastHornerMeasured := preset.selectNat 84000 12000 2400 - let fastMeasured := preset.selectNat 35 5 1 - let fastRemainderMeasured := preset.selectNat 3850 550 110 - let fastNttMeasured := preset.selectNat 2800 400 80 - let fastNttFastMeasured := preset.selectNat 3150 450 90 - let fastReversalConvolutionMeasured := preset.selectNat 280 40 8 - let fastReversalNttMeasured := preset.selectNat 420 60 12 - let fastReversalNttFastMeasured := preset.selectNat 1750 250 50 - let checksumIterations := groupChecksumIterations measured [ - sumMeasured, hornerMeasured, remainderMeasured, nttMeasured, nttFastMeasured, - reversalConvolutionMeasured, reversalNttMeasured, reversalNttFastMeasured, - fastSumMeasured, fastHornerMeasured, fastMeasured, fastRemainderMeasured, - fastNttMeasured, fastNttFastMeasured, fastReversalConvolutionMeasured, - fastReversalNttMeasured, fastReversalNttFastMeasured - ] - let smallBatchSum ← runTimed - "univariate-batch-naive-sum" "CPolynomial" "evalBatch" "KoalaBear.Field" - univariateBatchShape preset warmup sumMeasured - (fun _ ↦ CPolynomial.evalBatch batchPoly batchPoints) - (checksumArray checksumKoalaBear) (checksumIterations := checksumIterations) - let fastSmallBatchSum ← runTimed - "univariate-batch-naive-sum-fast" "CPolynomial" "evalBatch" "KoalaBear.Fast.Field" - univariateBatchShape preset warmup fastSumMeasured - (fun _ ↦ CPolynomial.evalBatch fastBatchPoly fastBatchPoints) - (checksumArray checksumKoalaBearFast) (checksumIterations := checksumIterations) - let smallBatchHorner ← runTimed - "univariate-batch-naive-horner" "CPolynomial" "evalBatchHorner" "KoalaBear.Field" - univariateBatchShape preset warmup hornerMeasured - (fun _ ↦ CPolynomial.evalBatchHorner batchPoly batchPoints) - (checksumArray checksumKoalaBear) (checksumIterations := checksumIterations) - let fastSmallBatchHorner ← runTimed - "univariate-batch-naive-horner-fast" "CPolynomial" "evalBatchHorner" - "KoalaBear.Fast.Field" univariateBatchShape preset warmup fastHornerMeasured + let checksumIterations := digestPeriod 1 + let smallBatchSum ← runTimedSpec + { name := "univariate-batch-naive-sum", representation := "CPolynomial", method := "evalBatch", + field := "KoalaBear.Field", inputShape := univariateBatchShape, + digestIterations := checksumIterations } + preset (fun _ ↦ CPolynomial.evalBatch batchPoly batchPoints) + (checksumArray checksumKoalaBear) + let fastSmallBatchSum ← runTimedSpec + { name := "univariate-batch-naive-sum-fast", representation := "CPolynomial", + method := "evalBatch", field := "KoalaBear.Fast.Field", inputShape := univariateBatchShape, + digestIterations := checksumIterations } + preset (fun _ ↦ CPolynomial.evalBatch fastBatchPoly fastBatchPoints) + (checksumArray checksumKoalaBearFast) + let smallBatchHorner ← runTimedSpec + { name := "univariate-batch-naive-horner", representation := "CPolynomial", + method := "evalBatchHorner", field := "KoalaBear.Field", inputShape := univariateBatchShape, + digestIterations := checksumIterations } + preset (fun _ ↦ CPolynomial.evalBatchHorner batchPoly batchPoints) + (checksumArray checksumKoalaBear) + let fastSmallBatchHorner ← runTimedSpec + { name := "univariate-batch-naive-horner-fast", representation := "CPolynomial", + method := "evalBatchHorner", field := "KoalaBear.Fast.Field", + inputShape := univariateBatchShape, digestIterations := checksumIterations } + preset (fun _ ↦ CPolynomial.evalBatchHorner fastBatchPoly fastBatchPoints) - (checksumArray checksumKoalaBearFast) (checksumIterations := checksumIterations) - let smallBatchSubproductNaive ← runTimed - "univariate-batch-subproduct-naive-mul-naive-mod" "CPolynomial" - "evalBatchSubproduct naive mul/mod" "KoalaBear.Field" - univariateBatchShape preset warmup measured + (checksumArray checksumKoalaBearFast) + let smallBatchSubproductNaive ← runTimedSpec + { name := "univariate-batch-subproduct-naive-mul-naive-mod", representation := "CPolynomial", + method := "evalBatchSubproduct naive mul/mod", field := "KoalaBear.Field", + inputShape := univariateBatchShape, digestIterations := checksumIterations } + preset (fun _ ↦ CPolynomial.evalBatchSubproduct naiveMul naiveMod batchPoly batchPoints) - (checksumArray checksumKoalaBear) (checksumIterations := checksumIterations) - let fastSmallBatchSubproductNaive ← runTimed - "univariate-batch-subproduct-naive-mul-naive-mod-fast" "CPolynomial" - "evalBatchSubproduct naive mul/mod" "KoalaBear.Fast.Field" - univariateBatchShape preset warmup fastMeasured + (checksumArray checksumKoalaBear) + let fastSmallBatchSubproductNaive ← runTimedSpec + { name := "univariate-batch-subproduct-naive-mul-naive-mod-fast", + representation := "CPolynomial", method := "evalBatchSubproduct naive mul/mod", + field := "KoalaBear.Fast.Field", inputShape := univariateBatchShape, + digestIterations := checksumIterations } + preset (fun _ ↦ CPolynomial.evalBatchSubproduct fastNaiveMul fastNaiveMod fastBatchPoly fastBatchPoints) - (checksumArray checksumKoalaBearFast) (checksumIterations := checksumIterations) - let smallBatchSubproductRemainder ← runTimed - "univariate-batch-subproduct-naive-mul-remainder-only-mod" "CPolynomial" - "evalBatchSubproduct naive mul/remainder-only mod" "KoalaBear.Field" - univariateBatchShape preset warmup remainderMeasured + (checksumArray checksumKoalaBearFast) + let smallBatchSubproductRemainder ← runTimedSpec + { name := "univariate-batch-subproduct-naive-mul-remainder-only-mod", + representation := "CPolynomial", + method := "evalBatchSubproduct naive mul/remainder-only mod", field := "KoalaBear.Field", + inputShape := univariateBatchShape, digestIterations := checksumIterations } + preset (fun _ ↦ CPolynomial.evalBatchSubproduct naiveMul remainderOnlyMod batchPoly batchPoints) - (checksumArray checksumKoalaBear) (checksumIterations := checksumIterations) - let fastSmallBatchSubproductRemainder ← runTimed - "univariate-batch-subproduct-naive-mul-remainder-only-mod-fast" "CPolynomial" - "evalBatchSubproduct naive mul/remainder-only mod" "KoalaBear.Fast.Field" - univariateBatchShape preset warmup fastRemainderMeasured + (checksumArray checksumKoalaBear) + let fastSmallBatchSubproductRemainder ← runTimedSpec + { name := "univariate-batch-subproduct-naive-mul-remainder-only-mod-fast", + representation := "CPolynomial", + method := "evalBatchSubproduct naive mul/remainder-only mod", + field := "KoalaBear.Fast.Field", inputShape := univariateBatchShape, + digestIterations := checksumIterations } + preset (fun _ ↦ CPolynomial.evalBatchSubproduct fastNaiveMul fastRemainderOnlyMod fastBatchPoly fastBatchPoints) - (checksumArray checksumKoalaBearFast) (checksumIterations := checksumIterations) - let smallBatchSubproductNtt ← runTimed - "univariate-batch-subproduct-ntt-mul-remainder-only-mod" "CPolynomial" - "evalBatchSubproduct ntt mul/remainder-only mod" "KoalaBear.Field" - univariateBatchShape preset warmup nttMeasured + (checksumArray checksumKoalaBearFast) + let smallBatchSubproductNtt ← runTimedSpec + { name := "univariate-batch-subproduct-ntt-mul-remainder-only-mod", + representation := "CPolynomial", method := "evalBatchSubproduct ntt mul/remainder-only mod", + field := "KoalaBear.Field", inputShape := univariateBatchShape, + digestIterations := checksumIterations } + preset (fun _ ↦ CPolynomial.evalBatchSubproduct nttMul remainderOnlyMod batchPoly batchPoints) - (checksumArray checksumKoalaBear) (checksumIterations := checksumIterations) - let fastSmallBatchSubproductNtt ← runTimed - "univariate-batch-subproduct-ntt-mul-remainder-only-mod-fast" "CPolynomial" - "evalBatchSubproduct ntt mul/remainder-only mod" "KoalaBear.Fast.Field" - univariateBatchShape preset warmup fastNttMeasured + (checksumArray checksumKoalaBear) + let fastSmallBatchSubproductNtt ← runTimedSpec + { name := "univariate-batch-subproduct-ntt-mul-remainder-only-mod-fast", + representation := "CPolynomial", method := "evalBatchSubproduct ntt mul/remainder-only mod", + field := "KoalaBear.Fast.Field", inputShape := univariateBatchShape, + digestIterations := checksumIterations } + preset (fun _ ↦ CPolynomial.evalBatchSubproduct fastNttMul fastRemainderOnlyMod fastBatchPoly fastBatchPoints) - (checksumArray checksumKoalaBearFast) (checksumIterations := checksumIterations) - let smallBatchSubproductNttFast ← runTimed - "univariate-batch-subproduct-ntt-fast-mul-remainder-only-mod" "CPolynomial" - "evalBatchSubproduct ntt-fast mul/remainder-only mod" "KoalaBear.Field" - univariateBatchShape preset warmup nttFastMeasured + (checksumArray checksumKoalaBearFast) + let smallBatchSubproductNttFast ← runTimedSpec + { name := "univariate-batch-subproduct-ntt-fast-mul-remainder-only-mod", + representation := "CPolynomial", + method := "evalBatchSubproduct ntt-fast mul/remainder-only mod", field := "KoalaBear.Field", + inputShape := univariateBatchShape, digestIterations := checksumIterations } + preset (fun _ ↦ CPolynomial.evalBatchSubproduct nttFastMul remainderOnlyMod batchPoly batchPoints) - (checksumArray checksumKoalaBear) (checksumIterations := checksumIterations) - let fastSmallBatchSubproductNttFast ← runTimed - "univariate-batch-subproduct-ntt-fast-mul-remainder-only-mod-fast" "CPolynomial" - "evalBatchSubproduct ntt-fast mul/remainder-only mod" "KoalaBear.Fast.Field" - univariateBatchShape preset warmup fastNttFastMeasured + (checksumArray checksumKoalaBear) + let fastSmallBatchSubproductNttFast ← runTimedSpec + { name := "univariate-batch-subproduct-ntt-fast-mul-remainder-only-mod-fast", + representation := "CPolynomial", + method := "evalBatchSubproduct ntt-fast mul/remainder-only mod", + field := "KoalaBear.Fast.Field", inputShape := univariateBatchShape, + digestIterations := checksumIterations } + preset (fun _ ↦ CPolynomial.evalBatchSubproduct fastNttFastMul fastRemainderOnlyMod fastBatchPoly fastBatchPoints) - (checksumArray checksumKoalaBearFast) (checksumIterations := checksumIterations) - let smallBatchSubproductReversalConvolution ← runTimed - "univariate-batch-subproduct-naive-mul-reversal-convolution-low-mod" "CPolynomial" - "evalBatchSubproduct naive mul/reversal-convolution-low mod" "KoalaBear.Field" - univariateBatchShape preset warmup reversalConvolutionMeasured + (checksumArray checksumKoalaBearFast) + let smallBatchSubproductReversalConvolution ← runTimedSpec + { name := "univariate-batch-subproduct-naive-mul-reversal-convolution-low-mod", + representation := "CPolynomial", + method := "evalBatchSubproduct naive mul/reversal-convolution-low mod", + field := "KoalaBear.Field", inputShape := univariateBatchShape, + digestIterations := checksumIterations } + preset (fun _ ↦ CPolynomial.evalBatchSubproduct naiveMul reversalConvolutionLowMod batchPoly batchPoints) - (checksumArray checksumKoalaBear) (checksumIterations := checksumIterations) - let fastSmallBatchSubproductReversalConvolution ← runTimed - "univariate-batch-subproduct-naive-mul-reversal-convolution-low-mod-fast" - "CPolynomial" "evalBatchSubproduct naive mul/reversal-convolution-low mod" - "KoalaBear.Fast.Field" univariateBatchShape preset warmup fastReversalConvolutionMeasured + (checksumArray checksumKoalaBear) + let fastSmallBatchSubproductReversalConvolution ← runTimedSpec + { name := "univariate-batch-subproduct-naive-mul-reversal-convolution-low-mod-fast", + representation := "CPolynomial", + method := "evalBatchSubproduct naive mul/reversal-convolution-low mod", + field := "KoalaBear.Fast.Field", inputShape := univariateBatchShape, + digestIterations := checksumIterations } + preset (fun _ ↦ CPolynomial.evalBatchSubproduct fastNaiveMul fastReversalConvolutionLowMod fastBatchPoly fastBatchPoints) - (checksumArray checksumKoalaBearFast) (checksumIterations := checksumIterations) - let smallBatchSubproductReversalNtt ← runTimed - "univariate-batch-subproduct-ntt-mul-reversal-ntt-low-mod" "CPolynomial" - "evalBatchSubproduct ntt mul/reversal-ntt-low mod" "KoalaBear.Field" - univariateBatchShape preset warmup reversalNttMeasured + (checksumArray checksumKoalaBearFast) + let smallBatchSubproductReversalNtt ← runTimedSpec + { name := "univariate-batch-subproduct-ntt-mul-reversal-ntt-low-mod", + representation := "CPolynomial", + method := "evalBatchSubproduct ntt mul/reversal-ntt-low mod", field := "KoalaBear.Field", + inputShape := univariateBatchShape, digestIterations := checksumIterations } + preset (fun _ ↦ CPolynomial.evalBatchSubproduct nttMul reversalNttLowMod batchPoly batchPoints) - (checksumArray checksumKoalaBear) (checksumIterations := checksumIterations) - let fastSmallBatchSubproductReversalNtt ← runTimed - "univariate-batch-subproduct-ntt-mul-reversal-ntt-low-mod-fast" "CPolynomial" - "evalBatchSubproduct ntt mul/reversal-ntt-low mod" "KoalaBear.Fast.Field" - univariateBatchShape preset warmup fastReversalNttMeasured + (checksumArray checksumKoalaBear) + let fastSmallBatchSubproductReversalNtt ← runTimedSpec + { name := "univariate-batch-subproduct-ntt-mul-reversal-ntt-low-mod-fast", + representation := "CPolynomial", + method := "evalBatchSubproduct ntt mul/reversal-ntt-low mod", + field := "KoalaBear.Fast.Field", inputShape := univariateBatchShape, + digestIterations := checksumIterations } + preset (fun _ ↦ CPolynomial.evalBatchSubproduct fastNttMul fastReversalNttLowMod fastBatchPoly fastBatchPoints) - (checksumArray checksumKoalaBearFast) (checksumIterations := checksumIterations) - let smallBatchSubproductReversalNttFast ← runTimed - "univariate-batch-subproduct-ntt-fast-mul-reversal-ntt-fast-low-mod" "CPolynomial" - "evalBatchSubproduct ntt-fast mul/reversal-ntt-fast-low mod" "KoalaBear.Field" - univariateBatchShape preset warmup reversalNttFastMeasured + (checksumArray checksumKoalaBearFast) + let smallBatchSubproductReversalNttFast ← runTimedSpec + { name := "univariate-batch-subproduct-ntt-fast-mul-reversal-ntt-fast-low-mod", + representation := "CPolynomial", + method := "evalBatchSubproduct ntt-fast mul/reversal-ntt-fast-low mod", + field := "KoalaBear.Field", inputShape := univariateBatchShape, + digestIterations := checksumIterations } + preset (fun _ ↦ CPolynomial.evalBatchSubproduct nttFastMul reversalNttFastLowMod batchPoly batchPoints) - (checksumArray checksumKoalaBear) (checksumIterations := checksumIterations) - let fastSmallBatchSubproductReversalNttFast ← runTimed - "univariate-batch-subproduct-ntt-fast-mul-reversal-ntt-fast-low-mod-fast" - "CPolynomial" "evalBatchSubproduct ntt-fast mul/reversal-ntt-fast-low mod" - "KoalaBear.Fast.Field" univariateBatchShape preset warmup fastReversalNttFastMeasured + (checksumArray checksumKoalaBear) + let fastSmallBatchSubproductReversalNttFast ← runTimedSpec + { name := "univariate-batch-subproduct-ntt-fast-mul-reversal-ntt-fast-low-mod-fast", + representation := "CPolynomial", + method := "evalBatchSubproduct ntt-fast mul/reversal-ntt-fast-low mod", + field := "KoalaBear.Fast.Field", inputShape := univariateBatchShape, + digestIterations := checksumIterations } + preset (fun _ ↦ CPolynomial.evalBatchSubproduct fastNttFastMul fastReversalNttFastLowMod fastBatchPoly fastBatchPoints) - (checksumArray checksumKoalaBearFast) (checksumIterations := checksumIterations) + (checksumArray checksumKoalaBearFast) pure ({ groupKey := "univariate-batch-small-koalabear", title := "Univariate batch evaluation, small (KoalaBear)", @@ -272,119 +279,129 @@ private def runKoalaBearUnivariateBatchMedium (preset : BenchPreset) (gen : StdG CPolynomial.ModContext.reversal fastNttWithFallbackLowMul let fastReversalNttFastLowMod : CPolynomial.ModContext KoalaBear.Fast.Field := CPolynomial.ModContext.reversal fastNttFastWithFallbackLowMul - let warmup := mediumBatchWarmupIterations preset - let measured := mediumBatchMeasuredIterations preset - let hornerMeasured := preset.selectNat 150 20 5 - let reversalNttMeasured := preset.selectNat 50 10 1 - let reversalNttFastMeasured := preset.selectNat 170 25 5 - let fastMeasured := preset.selectNat 70 10 2 - let fastHornerMeasured := preset.selectNat 1400 200 40 - let fastRemainderMeasured := preset.selectNat 28 4 1 - let fastNttMeasured := preset.selectNat 28 4 1 - let fastNttFastMeasured := preset.selectNat 28 4 1 - let fastReversalNttMeasured := preset.selectNat 210 30 6 - let fastReversalNttFastMeasured := preset.selectNat 910 130 26 - let checksumIterations := groupChecksumIterations measured [ - hornerMeasured, reversalNttMeasured, reversalNttFastMeasured, fastMeasured, - fastHornerMeasured, fastRemainderMeasured, fastNttMeasured, fastNttFastMeasured, - fastReversalNttMeasured, fastReversalNttFastMeasured - ] - let mediumBatchSum ← runTimed - "univariate-batch-medium-naive-sum" "CPolynomial" "evalBatch" "KoalaBear.Field" - mediumUnivariateBatchShape preset warmup measured - (fun _ ↦ CPolynomial.evalBatch mediumBatchPoly mediumBatchPoints) - (checksumArray checksumKoalaBear) (checksumIterations := checksumIterations) - let fastMediumBatchSum ← runTimed - "univariate-batch-medium-naive-sum-fast" "CPolynomial" "evalBatch" - "KoalaBear.Fast.Field" - mediumUnivariateBatchShape preset warmup fastMeasured + let checksumIterations := digestPeriod 1 + let mediumBatchSum ← runTimedSpec + { name := "univariate-batch-medium-naive-sum", representation := "CPolynomial", + method := "evalBatch", field := "KoalaBear.Field", inputShape := mediumUnivariateBatchShape, + digestIterations := checksumIterations } + preset (fun _ ↦ CPolynomial.evalBatch mediumBatchPoly mediumBatchPoints) + (checksumArray checksumKoalaBear) + let fastMediumBatchSum ← runTimedSpec + { name := "univariate-batch-medium-naive-sum-fast", representation := "CPolynomial", + method := "evalBatch", field := "KoalaBear.Fast.Field", + inputShape := mediumUnivariateBatchShape, digestIterations := checksumIterations } + preset (fun _ ↦ CPolynomial.evalBatch fastMediumBatchPoly fastMediumBatchPoints) - (checksumArray checksumKoalaBearFast) (checksumIterations := checksumIterations) - let mediumBatchHorner ← runTimed - "univariate-batch-medium-naive-horner" "CPolynomial" "evalBatchHorner" "KoalaBear.Field" - mediumUnivariateBatchShape preset warmup hornerMeasured + (checksumArray checksumKoalaBearFast) + let mediumBatchHorner ← runTimedSpec + { name := "univariate-batch-medium-naive-horner", representation := "CPolynomial", + method := "evalBatchHorner", field := "KoalaBear.Field", + inputShape := mediumUnivariateBatchShape, digestIterations := checksumIterations } + preset (fun _ ↦ CPolynomial.evalBatchHorner mediumBatchPoly mediumBatchPoints) - (checksumArray checksumKoalaBear) (checksumIterations := checksumIterations) - let fastMediumBatchHorner ← runTimed - "univariate-batch-medium-naive-horner-fast" "CPolynomial" "evalBatchHorner" - "KoalaBear.Fast.Field" - mediumUnivariateBatchShape preset warmup fastHornerMeasured + (checksumArray checksumKoalaBear) + let fastMediumBatchHorner ← runTimedSpec + { name := "univariate-batch-medium-naive-horner-fast", representation := "CPolynomial", + method := "evalBatchHorner", field := "KoalaBear.Fast.Field", + inputShape := mediumUnivariateBatchShape, digestIterations := checksumIterations } + preset (fun _ ↦ CPolynomial.evalBatchHorner fastMediumBatchPoly fastMediumBatchPoints) - (checksumArray checksumKoalaBearFast) (checksumIterations := checksumIterations) - let mediumBatchSubproductRemainder ← runTimed - "univariate-batch-medium-subproduct-naive-mul-remainder-only-mod" "CPolynomial" - "evalBatchSubproduct naive mul/remainder-only mod" "KoalaBear.Field" - mediumUnivariateBatchShape preset warmup measured + (checksumArray checksumKoalaBearFast) + let mediumBatchSubproductRemainder ← runTimedSpec + { name := "univariate-batch-medium-subproduct-naive-mul-remainder-only-mod", + representation := "CPolynomial", + method := "evalBatchSubproduct naive mul/remainder-only mod", field := "KoalaBear.Field", + inputShape := mediumUnivariateBatchShape, digestIterations := checksumIterations } + preset (fun _ ↦ CPolynomial.evalBatchSubproduct naiveMul remainderOnlyMod mediumBatchPoly mediumBatchPoints) - (checksumArray checksumKoalaBear) (checksumIterations := checksumIterations) - let fastMediumBatchSubproductRemainder ← runTimed - "univariate-batch-medium-subproduct-naive-mul-remainder-only-mod-fast" "CPolynomial" - "evalBatchSubproduct naive mul/remainder-only mod" "KoalaBear.Fast.Field" - mediumUnivariateBatchShape preset warmup fastRemainderMeasured + (checksumArray checksumKoalaBear) + let fastMediumBatchSubproductRemainder ← runTimedSpec + { name := "univariate-batch-medium-subproduct-naive-mul-remainder-only-mod-fast", + representation := "CPolynomial", + method := "evalBatchSubproduct naive mul/remainder-only mod", + field := "KoalaBear.Fast.Field", inputShape := mediumUnivariateBatchShape, + digestIterations := checksumIterations } + preset (fun _ ↦ CPolynomial.evalBatchSubproduct fastNaiveMul fastRemainderOnlyMod fastMediumBatchPoly fastMediumBatchPoints) - (checksumArray checksumKoalaBearFast) (checksumIterations := checksumIterations) - let mediumBatchSubproductNtt ← runTimed - "univariate-batch-medium-subproduct-ntt-mul-remainder-only-mod" "CPolynomial" - "evalBatchSubproduct ntt mul/remainder-only mod" "KoalaBear.Field" - mediumUnivariateBatchShape preset warmup measured + (checksumArray checksumKoalaBearFast) + let mediumBatchSubproductNtt ← runTimedSpec + { name := "univariate-batch-medium-subproduct-ntt-mul-remainder-only-mod", + representation := "CPolynomial", method := "evalBatchSubproduct ntt mul/remainder-only mod", + field := "KoalaBear.Field", inputShape := mediumUnivariateBatchShape, + digestIterations := checksumIterations } + preset (fun _ ↦ CPolynomial.evalBatchSubproduct nttMul remainderOnlyMod mediumBatchPoly mediumBatchPoints) - (checksumArray checksumKoalaBear) (checksumIterations := checksumIterations) - let fastMediumBatchSubproductNtt ← runTimed - "univariate-batch-medium-subproduct-ntt-mul-remainder-only-mod-fast" "CPolynomial" - "evalBatchSubproduct ntt mul/remainder-only mod" "KoalaBear.Fast.Field" - mediumUnivariateBatchShape preset warmup fastNttMeasured + (checksumArray checksumKoalaBear) + let fastMediumBatchSubproductNtt ← runTimedSpec + { name := "univariate-batch-medium-subproduct-ntt-mul-remainder-only-mod-fast", + representation := "CPolynomial", method := "evalBatchSubproduct ntt mul/remainder-only mod", + field := "KoalaBear.Fast.Field", inputShape := mediumUnivariateBatchShape, + digestIterations := checksumIterations } + preset (fun _ ↦ CPolynomial.evalBatchSubproduct fastNttMul fastRemainderOnlyMod fastMediumBatchPoly fastMediumBatchPoints) - (checksumArray checksumKoalaBearFast) (checksumIterations := checksumIterations) - let mediumBatchSubproductNttFast ← runTimed - "univariate-batch-medium-subproduct-ntt-fast-mul-remainder-only-mod" "CPolynomial" - "evalBatchSubproduct ntt-fast mul/remainder-only mod" "KoalaBear.Field" - mediumUnivariateBatchShape preset warmup measured + (checksumArray checksumKoalaBearFast) + let mediumBatchSubproductNttFast ← runTimedSpec + { name := "univariate-batch-medium-subproduct-ntt-fast-mul-remainder-only-mod", + representation := "CPolynomial", + method := "evalBatchSubproduct ntt-fast mul/remainder-only mod", field := "KoalaBear.Field", + inputShape := mediumUnivariateBatchShape, digestIterations := checksumIterations } + preset (fun _ ↦ CPolynomial.evalBatchSubproduct nttFastMul remainderOnlyMod mediumBatchPoly mediumBatchPoints) - (checksumArray checksumKoalaBear) (checksumIterations := checksumIterations) - let fastMediumBatchSubproductNttFast ← runTimed - "univariate-batch-medium-subproduct-ntt-fast-mul-remainder-only-mod-fast" - "CPolynomial" "evalBatchSubproduct ntt-fast mul/remainder-only mod" - "KoalaBear.Fast.Field" - mediumUnivariateBatchShape preset warmup fastNttFastMeasured + (checksumArray checksumKoalaBear) + let fastMediumBatchSubproductNttFast ← runTimedSpec + { name := "univariate-batch-medium-subproduct-ntt-fast-mul-remainder-only-mod-fast", + representation := "CPolynomial", + method := "evalBatchSubproduct ntt-fast mul/remainder-only mod", + field := "KoalaBear.Fast.Field", inputShape := mediumUnivariateBatchShape, + digestIterations := checksumIterations } + preset (fun _ ↦ CPolynomial.evalBatchSubproduct fastNttFastMul fastRemainderOnlyMod fastMediumBatchPoly fastMediumBatchPoints) - (checksumArray checksumKoalaBearFast) (checksumIterations := checksumIterations) - let mediumBatchSubproductReversalNtt ← runTimed - "univariate-batch-medium-subproduct-ntt-mul-reversal-ntt-low-mod" "CPolynomial" - "evalBatchSubproduct ntt mul/reversal-ntt-low mod" "KoalaBear.Field" - mediumUnivariateBatchShape preset warmup reversalNttMeasured + (checksumArray checksumKoalaBearFast) + let mediumBatchSubproductReversalNtt ← runTimedSpec + { name := "univariate-batch-medium-subproduct-ntt-mul-reversal-ntt-low-mod", + representation := "CPolynomial", + method := "evalBatchSubproduct ntt mul/reversal-ntt-low mod", field := "KoalaBear.Field", + inputShape := mediumUnivariateBatchShape, digestIterations := checksumIterations } + preset (fun _ ↦ CPolynomial.evalBatchSubproduct nttMul reversalNttLowMod mediumBatchPoly mediumBatchPoints) - (checksumArray checksumKoalaBear) (checksumIterations := checksumIterations) - let fastMediumBatchSubproductReversalNtt ← runTimed - "univariate-batch-medium-subproduct-ntt-mul-reversal-ntt-low-mod-fast" - "CPolynomial" "evalBatchSubproduct ntt mul/reversal-ntt-low mod" - "KoalaBear.Fast.Field" - mediumUnivariateBatchShape preset warmup fastReversalNttMeasured + (checksumArray checksumKoalaBear) + let fastMediumBatchSubproductReversalNtt ← runTimedSpec + { name := "univariate-batch-medium-subproduct-ntt-mul-reversal-ntt-low-mod-fast", + representation := "CPolynomial", + method := "evalBatchSubproduct ntt mul/reversal-ntt-low mod", + field := "KoalaBear.Fast.Field", inputShape := mediumUnivariateBatchShape, + digestIterations := checksumIterations } + preset (fun _ ↦ CPolynomial.evalBatchSubproduct fastNttMul fastReversalNttLowMod fastMediumBatchPoly fastMediumBatchPoints) - (checksumArray checksumKoalaBearFast) (checksumIterations := checksumIterations) - let mediumBatchSubproductReversalNttFast ← runTimed - "univariate-batch-medium-subproduct-ntt-fast-mul-reversal-ntt-fast-low-mod" - "CPolynomial" "evalBatchSubproduct ntt-fast mul/reversal-ntt-fast-low mod" - "KoalaBear.Field" - mediumUnivariateBatchShape preset warmup reversalNttFastMeasured + (checksumArray checksumKoalaBearFast) + let mediumBatchSubproductReversalNttFast ← runTimedSpec + { name := "univariate-batch-medium-subproduct-ntt-fast-mul-reversal-ntt-fast-low-mod", + representation := "CPolynomial", + method := "evalBatchSubproduct ntt-fast mul/reversal-ntt-fast-low mod", + field := "KoalaBear.Field", inputShape := mediumUnivariateBatchShape, + digestIterations := checksumIterations } + preset (fun _ ↦ CPolynomial.evalBatchSubproduct nttFastMul reversalNttFastLowMod mediumBatchPoly mediumBatchPoints) - (checksumArray checksumKoalaBear) (checksumIterations := checksumIterations) - let fastMediumBatchSubproductReversalNttFast ← runTimed - "univariate-batch-medium-subproduct-ntt-fast-mul-reversal-ntt-fast-low-mod-fast" - "CPolynomial" "evalBatchSubproduct ntt-fast mul/reversal-ntt-fast-low mod" - "KoalaBear.Fast.Field" - mediumUnivariateBatchShape preset warmup fastReversalNttFastMeasured + (checksumArray checksumKoalaBear) + let fastMediumBatchSubproductReversalNttFast ← runTimedSpec + { name := "univariate-batch-medium-subproduct-ntt-fast-mul-reversal-ntt-fast-low-mod-fast", + representation := "CPolynomial", + method := "evalBatchSubproduct ntt-fast mul/reversal-ntt-fast-low mod", + field := "KoalaBear.Fast.Field", inputShape := mediumUnivariateBatchShape, + digestIterations := checksumIterations } + preset (fun _ ↦ CPolynomial.evalBatchSubproduct fastNttFastMul fastReversalNttFastLowMod fastMediumBatchPoly fastMediumBatchPoints) - (checksumArray checksumKoalaBearFast) (checksumIterations := checksumIterations) + (checksumArray checksumKoalaBearFast) pure ({ groupKey := "univariate-batch-medium-koalabear", title := "Univariate batch evaluation, medium (KoalaBear)", @@ -430,58 +447,59 @@ private def runKoalaBearUnivariateBatchLarge (preset : BenchPreset) (gen : StdGe CPolynomial.ModContext.reversal fastNttWithFallbackLowMul let fastReversalNttFastLowMod : CPolynomial.ModContext KoalaBear.Fast.Field := CPolynomial.ModContext.reversal fastNttFastWithFallbackLowMul - let warmup := largeBatchWarmupIterations preset - let measured := largeBatchMeasuredIterations preset - let reversalNttMeasured := preset.selectNat 20 3 1 - let reversalNttFastMeasured := preset.selectNat 80 10 2 - let fastMeasured := preset.selectNat 70 10 2 - let fastReversalNttMeasured := preset.selectNat 63 9 2 - let fastReversalNttFastMeasured := preset.selectNat 420 60 12 - let checksumIterations := groupChecksumIterations measured [ - reversalNttMeasured, reversalNttFastMeasured, fastMeasured, fastReversalNttMeasured, - fastReversalNttFastMeasured - ] - let largeBatchHorner ← runTimed - "univariate-batch-large-naive-horner" "CPolynomial" "evalBatchHorner" "KoalaBear.Field" - largeUnivariateBatchShape preset warmup measured - (fun _ ↦ CPolynomial.evalBatchHorner largeBatchPoly largeBatchPoints) - (checksumArray checksumKoalaBear) (checksumIterations := checksumIterations) - let fastLargeBatchHorner ← runTimed - "univariate-batch-large-naive-horner-fast" "CPolynomial" "evalBatchHorner" - "KoalaBear.Fast.Field" - largeUnivariateBatchShape preset warmup fastMeasured + let checksumIterations := digestPeriod 1 + let largeBatchHorner ← runTimedSpec + { name := "univariate-batch-large-naive-horner", representation := "CPolynomial", + method := "evalBatchHorner", field := "KoalaBear.Field", + inputShape := largeUnivariateBatchShape, digestIterations := checksumIterations } + preset (fun _ ↦ CPolynomial.evalBatchHorner largeBatchPoly largeBatchPoints) + (checksumArray checksumKoalaBear) + let fastLargeBatchHorner ← runTimedSpec + { name := "univariate-batch-large-naive-horner-fast", representation := "CPolynomial", + method := "evalBatchHorner", field := "KoalaBear.Fast.Field", + inputShape := largeUnivariateBatchShape, digestIterations := checksumIterations } + preset (fun _ ↦ CPolynomial.evalBatchHorner fastLargeBatchPoly fastLargeBatchPoints) - (checksumArray checksumKoalaBearFast) (checksumIterations := checksumIterations) - let largeBatchSubproductReversalNtt ← runTimed - "univariate-batch-large-subproduct-ntt-mul-reversal-ntt-low-mod" "CPolynomial" - "evalBatchSubproduct ntt mul/reversal-ntt-low mod" "KoalaBear.Field" - largeUnivariateBatchShape preset warmup reversalNttMeasured + (checksumArray checksumKoalaBearFast) + let largeBatchSubproductReversalNtt ← runTimedSpec + { name := "univariate-batch-large-subproduct-ntt-mul-reversal-ntt-low-mod", + representation := "CPolynomial", + method := "evalBatchSubproduct ntt mul/reversal-ntt-low mod", field := "KoalaBear.Field", + inputShape := largeUnivariateBatchShape, digestIterations := checksumIterations } + preset (fun _ ↦ CPolynomial.evalBatchSubproduct nttMul reversalNttLowMod largeBatchPoly largeBatchPoints) - (checksumArray checksumKoalaBear) (checksumIterations := checksumIterations) - let fastLargeBatchSubproductReversalNtt ← runTimed - "univariate-batch-large-subproduct-ntt-mul-reversal-ntt-low-mod-fast" "CPolynomial" - "evalBatchSubproduct ntt mul/reversal-ntt-low mod" "KoalaBear.Fast.Field" - largeUnivariateBatchShape preset warmup fastReversalNttMeasured + (checksumArray checksumKoalaBear) + let fastLargeBatchSubproductReversalNtt ← runTimedSpec + { name := "univariate-batch-large-subproduct-ntt-mul-reversal-ntt-low-mod-fast", + representation := "CPolynomial", + method := "evalBatchSubproduct ntt mul/reversal-ntt-low mod", + field := "KoalaBear.Fast.Field", inputShape := largeUnivariateBatchShape, + digestIterations := checksumIterations } + preset (fun _ ↦ CPolynomial.evalBatchSubproduct fastNttMul fastReversalNttLowMod fastLargeBatchPoly fastLargeBatchPoints) - (checksumArray checksumKoalaBearFast) (checksumIterations := checksumIterations) - let largeBatchSubproductReversalNttFast ← runTimed - "univariate-batch-large-subproduct-ntt-fast-mul-reversal-ntt-fast-low-mod" - "CPolynomial" "evalBatchSubproduct ntt-fast mul/reversal-ntt-fast-low mod" - "KoalaBear.Field" - largeUnivariateBatchShape preset warmup reversalNttFastMeasured + (checksumArray checksumKoalaBearFast) + let largeBatchSubproductReversalNttFast ← runTimedSpec + { name := "univariate-batch-large-subproduct-ntt-fast-mul-reversal-ntt-fast-low-mod", + representation := "CPolynomial", + method := "evalBatchSubproduct ntt-fast mul/reversal-ntt-fast-low mod", + field := "KoalaBear.Field", inputShape := largeUnivariateBatchShape, + digestIterations := checksumIterations } + preset (fun _ ↦ CPolynomial.evalBatchSubproduct nttFastMul reversalNttFastLowMod largeBatchPoly largeBatchPoints) - (checksumArray checksumKoalaBear) (checksumIterations := checksumIterations) - let fastLargeBatchSubproductReversalNttFast ← runTimed - "univariate-batch-large-subproduct-ntt-fast-mul-reversal-ntt-fast-low-mod-fast" - "CPolynomial" "evalBatchSubproduct ntt-fast mul/reversal-ntt-fast-low mod" - "KoalaBear.Fast.Field" - largeUnivariateBatchShape preset warmup fastReversalNttFastMeasured + (checksumArray checksumKoalaBear) + let fastLargeBatchSubproductReversalNttFast ← runTimedSpec + { name := "univariate-batch-large-subproduct-ntt-fast-mul-reversal-ntt-fast-low-mod-fast", + representation := "CPolynomial", + method := "evalBatchSubproduct ntt-fast mul/reversal-ntt-fast-low mod", + field := "KoalaBear.Fast.Field", inputShape := largeUnivariateBatchShape, + digestIterations := checksumIterations } + preset (fun _ ↦ CPolynomial.evalBatchSubproduct fastNttFastMul fastReversalNttFastLowMod fastLargeBatchPoly fastLargeBatchPoints) - (checksumArray checksumKoalaBearFast) (checksumIterations := checksumIterations) + (checksumArray checksumKoalaBearFast) pure ({ groupKey := "univariate-batch-large-koalabear", title := "Univariate batch evaluation, large (KoalaBear)", @@ -503,9 +521,4 @@ def univariateBatchEvalTasks : List BenchTask := [ runKoalaBearUnivariateBatchLarge ] -/-- Run selected univariate batch-evaluation benchmarks. -/ -def runUnivariateBatchEval (preset : BenchPreset) (selection : BenchSelection) (gen : StdGen) : - IO (Array BenchGroup × StdGen) := do - runSelectedTasks univariateBatchEvalTasks preset selection gen - end CompPolyBench diff --git a/bench/CompPolyBench/Univariate/ManyEval.lean b/bench/CompPolyBench/Univariate/ManyEval.lean index d9b5d7d4..216e91d6 100644 --- a/bench/CompPolyBench/Univariate/ManyEval.lean +++ b/bench/CompPolyBench/Univariate/ManyEval.lean @@ -39,12 +39,6 @@ private def cpolysOfFlatArray {R : Type*} [Zero R] [BEq R] [LawfulBEq R] polys := polys.push (cpolyOfArray polyCoeffs) return polys -/-- Benchmark group metadata for `CompPoly.Univariate.ManyEval`. -/ -def univariateManyEvalGroupInfos : List BenchGroupInfo := [ - ⟨"univariate-many-one-point-koalabear", - "Univariate many-polynomial one-point evaluation (KoalaBear)"⟩ -] - /-- Benchmark runner for KoalaBear many-polynomial one-point evaluation. -/ private def runKoalaBearManyEvalOnePoint (preset : BenchPreset) (gen : StdGen) : IO (BenchGroup × StdGen) := do @@ -57,36 +51,33 @@ private def runKoalaBearManyEvalOnePoint (preset : BenchPreset) (gen : StdGen) : let fastPoints := koalaBearFastArray points let fastPolys := cpolysOfFlatArray manyEvalPolyCount manyEvalCoeffSlots fastCoeffs let fastX := fastPoints.getD 0 0 - let warmup := preset.selectNat 1 1 0 - let hornerMeasured := preset.selectNat 110 15 3 - let sharedPowersMeasured := preset.selectNat 110 15 3 - let fastHornerMeasured := preset.selectNat 1200 170 35 - let fastSharedPowersMeasured := preset.selectNat 2100 300 60 - let checksumIterations := groupChecksumIterations hornerMeasured [ - sharedPowersMeasured, fastHornerMeasured, fastSharedPowersMeasured - ] - let horner ← runTimed - "univariate-many-one-point-horner" "Array CPolynomial" "evalManyHorner" - "KoalaBear.Field" manyEvalOnePointShape preset warmup hornerMeasured - (fun _ ↦ CPolynomial.evalManyHorner polys x) - (checksumArray checksumKoalaBear) (checksumIterations := checksumIterations) - let sharedPowers ← runTimed - "univariate-many-one-point-shared-powers-row-major" "Array CPolynomial" - "evalManySharedPowers" "KoalaBear.Field" manyEvalOnePointShape preset warmup - sharedPowersMeasured - (fun _ ↦ CPolynomial.evalManySharedPowers polys x) - (checksumArray checksumKoalaBear) (checksumIterations := checksumIterations) - let fastHorner ← runTimed - "univariate-many-one-point-horner-fast" "Array CPolynomial" "evalManyHorner" - "KoalaBear.Fast.Field" manyEvalOnePointShape preset warmup fastHornerMeasured - (fun _ ↦ CPolynomial.evalManyHorner fastPolys fastX) - (checksumArray checksumKoalaBearFast) (checksumIterations := checksumIterations) - let fastSharedPowers ← runTimed - "univariate-many-one-point-shared-powers-row-major-fast" "Array CPolynomial" - "evalManySharedPowers" "KoalaBear.Fast.Field" manyEvalOnePointShape preset warmup - fastSharedPowersMeasured - (fun _ ↦ CPolynomial.evalManySharedPowers fastPolys fastX) - (checksumArray checksumKoalaBearFast) (checksumIterations := checksumIterations) + let checksumIterations := digestPeriod 1 + let horner ← runTimedSpec + { name := "univariate-many-one-point-horner", representation := "Array CPolynomial", + method := "evalManyHorner", field := "KoalaBear.Field", inputShape := manyEvalOnePointShape, + digestIterations := checksumIterations } + preset (fun _ ↦ CPolynomial.evalManyHorner polys x) + (checksumArray checksumKoalaBear) + let sharedPowers ← runTimedSpec + { name := "univariate-many-one-point-shared-powers-row-major", + representation := "Array CPolynomial", method := "evalManySharedPowers", + field := "KoalaBear.Field", inputShape := manyEvalOnePointShape, + digestIterations := checksumIterations } + preset (fun _ ↦ CPolynomial.evalManySharedPowers polys x) + (checksumArray checksumKoalaBear) + let fastHorner ← runTimedSpec + { name := "univariate-many-one-point-horner-fast", representation := "Array CPolynomial", + method := "evalManyHorner", field := "KoalaBear.Fast.Field", + inputShape := manyEvalOnePointShape, digestIterations := checksumIterations } + preset (fun _ ↦ CPolynomial.evalManyHorner fastPolys fastX) + (checksumArray checksumKoalaBearFast) + let fastSharedPowers ← runTimedSpec + { name := "univariate-many-one-point-shared-powers-row-major-fast", + representation := "Array CPolynomial", method := "evalManySharedPowers", + field := "KoalaBear.Fast.Field", inputShape := manyEvalOnePointShape, + digestIterations := checksumIterations } + preset + (fun _ ↦ CPolynomial.evalManySharedPowers fastPolys fastX) (checksumArray checksumKoalaBearFast) pure ({ groupKey := "univariate-many-one-point-koalabear", title := "Univariate many-polynomial one-point evaluation (KoalaBear)", @@ -101,9 +92,4 @@ def univariateManyEvalTasks : List BenchTask := [ runKoalaBearManyEvalOnePoint ] -/-- Execute selected many-polynomial evaluation benchmarks. -/ -def runUnivariateManyEval (preset : BenchPreset) (selection : BenchSelection) (gen : StdGen) : - IO (Array BenchGroup × StdGen) := do - runSelectedTasks univariateManyEvalTasks preset selection gen - end CompPolyBench diff --git a/bench/CompPolyBench/Univariate/NTT/Crossover.lean b/bench/CompPolyBench/Univariate/NTT/Crossover.lean new file mode 100644 index 00000000..67b09874 --- /dev/null +++ b/bench/CompPolyBench/Univariate/NTT/Crossover.lean @@ -0,0 +1,98 @@ +/- +Copyright (c) 2026 CompPoly Contributors. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Derek Sorensen +-/ +module + +public import CompPolyBench.Univariate.Common +public import CompPoly.Univariate.NTTFast.FastMul +public import CompPoly.Univariate.NTTFast.Plan + +/-! +# Schoolbook / NTT crossover + +Where NTT-based multiplication starts to beat the schoolbook product. A sweep +of operand sizes, schoolbook against the planned NTT pipeline, over KoalaBear's +native-word representation. + +This replaces `tests/CompPolyTests/Univariate/NTT/Benchmark.lean`, which held +the only crossover logic in the repo, ran nothing under `lake test`, was +imported by nothing, and printed its results through `#eval`. +`BENCHMARKING.md` recorded it as "the specification for a future crossover +metric"; this is that metric, so the file goes. + +One group per operand size, because `workUnits` — the operand coefficient +count — must agree across a group, and the whole point of the sweep is that it +does not agree across sizes. The domain for each size is the smallest that +holds the convolution length `2k - 1`, and the plan is built outside the timed +closure, as a caller would. +-/ + +public section + +open CompPoly + +namespace CompPolyBench + +/-- Time schoolbook against planned NTT multiplication at one operand size. -/ +private def runCrossoverGroup (coeffs logN : Nat) (hlogN : logN ≤ KoalaBear.twoAdicity) + (preset : BenchPreset) (gen : StdGen) : IO (BenchGroup × StdGen) := do + let (lhsValues, gen) := (koalaBearArray coeffs false).run gen + let (rhsValues, gen) := (koalaBearArray coeffs false).run gen + let lhs := cpolyOfArray (koalaBearFastArray lhsValues) + let rhs := cpolyOfArray (koalaBearFastArray rhsValues) + let pairs := #[(lhs, rhs), (rhs, lhs)] + let pair (i : Nat) : CPolynomial KoalaBear.Fast.Field × CPolynomial KoalaBear.Fast.Field := + pairs.getD (i % 2) (lhs, rhs) + let domain := CPolynomial.NTT.KoalaBear.fastDomainOfLogN logN hlogN + let plan := CPolynomial.NTTFast.Plan.ofDomain domain + let checksum := checksumCPolynomial checksumKoalaBearFast + let shape := s!"degree<{coeffs} dense lhs/rhs, two orderings" + let schoolbook ← runTimedSpec + { name := s!"univariate-mul-crossover-schoolbook", representation := "CPolynomial", + method := "mul", field := "KoalaBear.Fast.Field", inputShape := shape, + digestIterations := digestPeriod 2, workUnits := coeffs } + preset (fun i ↦ let (p, q) := pair i; p * q) checksum + let planned ← runTimedSpec + { name := s!"univariate-mul-crossover-ntt", representation := "CPolynomial", + method := "NTTFast.Plan.fastMulImpl", field := "KoalaBear.Fast.Field", + inputShape := shape, digestIterations := digestPeriod 2, workUnits := coeffs } + preset (fun i ↦ let (p, q) := pair i; CPolynomial.NTTFast.Plan.fastMulImpl plan p q) + checksum + pure ({ groupKey := s!"univariate-mul-crossover-{coeffs}", + title := s!"Schoolbook against NTT multiplication, degree<{coeffs}", + records := #[schoolbook, planned] }, gen) + +/-- Registry entries for the crossover sweep. -/ +def crossoverTasks : List BenchTask := [ + BenchTask.fromGroupRunner + ⟨"univariate-mul-crossover-4", "Schoolbook against NTT multiplication, degree<4"⟩ + (runCrossoverGroup 4 3 (by decide)), + BenchTask.fromGroupRunner + ⟨"univariate-mul-crossover-8", "Schoolbook against NTT multiplication, degree<8"⟩ + (runCrossoverGroup 8 4 (by decide)), + BenchTask.fromGroupRunner + ⟨"univariate-mul-crossover-16", "Schoolbook against NTT multiplication, degree<16"⟩ + (runCrossoverGroup 16 5 (by decide)), + BenchTask.fromGroupRunner + ⟨"univariate-mul-crossover-32", "Schoolbook against NTT multiplication, degree<32"⟩ + (runCrossoverGroup 32 6 (by decide)), + BenchTask.fromGroupRunner + ⟨"univariate-mul-crossover-64", "Schoolbook against NTT multiplication, degree<64"⟩ + (runCrossoverGroup 64 7 (by decide)), + BenchTask.fromGroupRunner + ⟨"univariate-mul-crossover-128", "Schoolbook against NTT multiplication, degree<128"⟩ + (runCrossoverGroup 128 8 (by decide)), + BenchTask.fromGroupRunner + ⟨"univariate-mul-crossover-256", "Schoolbook against NTT multiplication, degree<256"⟩ + (runCrossoverGroup 256 9 (by decide)), + BenchTask.fromGroupRunner + ⟨"univariate-mul-crossover-512", "Schoolbook against NTT multiplication, degree<512"⟩ + (runCrossoverGroup 512 10 (by decide)), + BenchTask.fromGroupRunner + ⟨"univariate-mul-crossover-1024", "Schoolbook against NTT multiplication, degree<1024"⟩ + (runCrossoverGroup 1024 11 (by decide)) +] + +end CompPolyBench diff --git a/bench/CompPolyBench/Univariate/NTT/FastMul.lean b/bench/CompPolyBench/Univariate/NTT/FastMul.lean index 88f3c81b..9fc4db5b 100644 --- a/bench/CompPolyBench/Univariate/NTT/FastMul.lean +++ b/bench/CompPolyBench/Univariate/NTT/FastMul.lean @@ -20,28 +20,11 @@ open CompPoly namespace CompPolyBench -/-- Benchmark group metadata for `CompPoly.Univariate.NTT.FastMul`. -/ -def univariateNttFastMulGroupInfos : List BenchGroupInfo := [ - ⟨"univariate-mul-koalabear", "Univariate multiplication (KoalaBear)"⟩, - ⟨"univariate-mul-babybear", "Univariate multiplication (BabyBear)"⟩ -] - /-- Display and checksum operations associated with a benchmark field. -/ private structure BenchField (F : Type*) where id : String checksum : F → Nat -/-- Per-preset measured iteration budgets for the direct multiplication group. The -canonical naive row uses the shared `mulMeasuredIterations` budget. -/ -private structure MulBudgets where - ntt : BenchPreset → Nat - nttFast : BenchPreset → Nat - nttFastPlan : BenchPreset → Nat - fastNaive : BenchPreset → Nat - fastNtt : BenchPreset → Nat - fastNttFast : BenchPreset → Nat - fastNttFastPlan : BenchPreset → Nat - /-- Benchmark direct univariate multiplication and root-of-unity NTT variants over a canonical field and its native-word counterpart. `slug` distinguishes the two native-word NTT row names, whose canonical-representation names are already taken. -/ @@ -51,7 +34,7 @@ private def runUnivariateMulWithFast {F G : Type} (canonicalField : BenchField F) (fastField : BenchField G) (genCoeffs : Nat → StateM StdGen (Array F)) (toFast : Array F → Array G) (canonicalDomain : CPolynomial.NTT.Domain F) (fastDomain : CPolynomial.NTT.Domain G) - (budgets : MulBudgets) (preset : BenchPreset) (gen : StdGen) : + (preset : BenchPreset) (gen : StdGen) : IO (BenchGroup × StdGen) := do let (mulLhsCoeffs, gen) := (genCoeffs univariateMulCoeffSlots).run gen let (mulRhsCoeffs, gen) := (genCoeffs univariateMulCoeffSlots).run gen @@ -65,69 +48,67 @@ private def runUnivariateMulWithFast {F G : Type} let fastPlan := CPolynomial.NTTFast.Plan.ofDomain fastDomain let canonicalChecksum := checksumCPolynomial canonicalField.checksum let fastChecksum := checksumCPolynomial fastField.checksum - let warmup := mulWarmupIterations preset - let measured := mulMeasuredIterations preset - let nttMeasured := budgets.ntt preset - let nttFastMeasured := budgets.nttFast preset - let nttFastPlanMeasured := budgets.nttFastPlan preset - let fastMeasured := budgets.fastNaive preset - let fastNttMeasured := budgets.fastNtt preset - let fastNttFastMeasured := budgets.fastNttFast preset - let fastNttFastPlanMeasured := budgets.fastNttFastPlan preset - let checksumIterations := groupChecksumIterations measured [ - nttMeasured, nttFastMeasured, nttFastPlanMeasured, fastMeasured, fastNttMeasured, - fastNttFastMeasured, fastNttFastPlanMeasured - ] - let canonicalNaive ← runTimed - "univariate-mul-naive" "CPolynomial" "mul" canonicalField.id - univariateMulShape preset warmup measured - (fun _ ↦ mulLhsPoly * mulRhsPoly) canonicalChecksum - (checksumIterations := checksumIterations) - let fastNaive ← runTimed - "univariate-mul-naive-fast" "CPolynomial" "mul" fastField.id - univariateMulShape preset warmup fastMeasured - (fun _ ↦ fastMulLhsPoly * fastMulRhsPoly) fastChecksum - (checksumIterations := checksumIterations) - let canonicalNtt ← runTimed - "univariate-mul-ntt" "CPolynomial" (univariateMulNttMethod "FastMul.fastMulImpl") - canonicalField.id univariateMulShape preset warmup nttMeasured + let checksumIterations := digestPeriod 1 + let canonicalNaive ← runTimedSpec + { name := "univariate-mul-naive", representation := "CPolynomial", method := "mul", + field := canonicalField.id, inputShape := univariateMulShape, + digestIterations := checksumIterations } + preset (fun _ ↦ mulLhsPoly * mulRhsPoly) canonicalChecksum + let fastNaive ← runTimedSpec + { name := "univariate-mul-naive-fast", representation := "CPolynomial", method := "mul", + field := fastField.id, inputShape := univariateMulShape, + digestIterations := checksumIterations } + preset (fun _ ↦ fastMulLhsPoly * fastMulRhsPoly) fastChecksum + let canonicalNtt ← runTimedSpec + { name := "univariate-mul-ntt", representation := "CPolynomial", + method := (univariateMulNttMethod "FastMul.fastMulImpl"), field := canonicalField.id, + inputShape := univariateMulShape, digestIterations := checksumIterations } + preset (fun _ ↦ CPolynomial.NTT.FastMul.fastMulImpl canonicalDomain mulLhsPoly mulRhsPoly) - canonicalChecksum (checksumIterations := checksumIterations) - let fastNtt ← runTimed - s!"univariate-mul-ntt-{slug}-fast" "CPolynomial" - (univariateMulNttMethod "FastMul.fastMulImpl") fastField.id - univariateMulShape preset warmup fastNttMeasured + canonicalChecksum + let fastNtt ← runTimedSpec + { name := s!"univariate-mul-ntt-{slug}-fast", representation := "CPolynomial", + method := (univariateMulNttMethod "FastMul.fastMulImpl"), field := fastField.id, + inputShape := univariateMulShape, digestIterations := checksumIterations } + preset (fun _ ↦ CPolynomial.NTT.FastMul.fastMulImpl fastDomain fastMulLhsPoly fastMulRhsPoly) - fastChecksum (checksumIterations := checksumIterations) - let canonicalNttFast ← runTimed - "univariate-mul-ntt-fast" "CPolynomial" (univariateMulNttMethod "NTTFast.fastMulImpl") - canonicalField.id univariateMulShape preset warmup nttFastMeasured + fastChecksum + let canonicalNttFast ← runTimedSpec + { name := "univariate-mul-ntt-fast", representation := "CPolynomial", + method := (univariateMulNttMethod "NTTFast.fastMulImpl"), field := canonicalField.id, + inputShape := univariateMulShape, digestIterations := checksumIterations } + preset (fun _ ↦ CPolynomial.NTTFast.fastMulImpl canonicalDomain mulLhsPoly mulRhsPoly) - canonicalChecksum (checksumIterations := checksumIterations) - let fastNttFast ← runTimed - s!"univariate-mul-ntt-fast-{slug}-fast" "CPolynomial" - (univariateMulNttMethod "NTTFast.fastMulImpl") fastField.id - univariateMulShape preset warmup fastNttFastMeasured + canonicalChecksum + let fastNttFast ← runTimedSpec + { name := s!"univariate-mul-ntt-fast-{slug}-fast", representation := "CPolynomial", + method := (univariateMulNttMethod "NTTFast.fastMulImpl"), field := fastField.id, + inputShape := univariateMulShape, digestIterations := checksumIterations } + preset (fun _ ↦ CPolynomial.NTTFast.fastMulImpl fastDomain fastMulLhsPoly fastMulRhsPoly) - fastChecksum (checksumIterations := checksumIterations) - let canonicalNttFastPlan ← runTimed - "univariate-mul-ntt-fast-plan" "CPolynomial" - (univariateMulNttMethod - "NTTFast.Plan.fastMulImpl, cached twiddles, mixed radix-4 DIF/DIT, dual forward") - canonicalField.id univariateMulShape preset warmup nttFastPlanMeasured + fastChecksum + let canonicalNttFastPlan ← runTimedSpec + { name := "univariate-mul-ntt-fast-plan", representation := "CPolynomial", + method := (univariateMulNttMethod + "NTTFast.Plan.fastMulImpl, cached twiddles, mixed radix-4 DIF/DIT, dual forward"), + field := canonicalField.id, inputShape := univariateMulShape, + digestIterations := checksumIterations } + preset (fun _ ↦ CPolynomial.NTTFast.Plan.fastMulImpl canonicalPlan mulLhsPoly mulRhsPoly) - canonicalChecksum (checksumIterations := checksumIterations) - let fastNttFastPlan ← runTimed - "univariate-mul-ntt-fast-plan-fast" "CPolynomial" - (univariateMulNttMethod - "NTTFast.Plan.fastMulImpl, cached twiddles, mixed radix-4 DIF/DIT, dual forward") - fastField.id univariateMulShape preset warmup fastNttFastPlanMeasured + canonicalChecksum + let fastNttFastPlan ← runTimedSpec + { name := "univariate-mul-ntt-fast-plan-fast", representation := "CPolynomial", + method := (univariateMulNttMethod + "NTTFast.Plan.fastMulImpl, cached twiddles, mixed radix-4 DIF/DIT, dual forward"), + field := fastField.id, inputShape := univariateMulShape, + digestIterations := checksumIterations } + preset (fun _ ↦ CPolynomial.NTTFast.Plan.fastMulImpl fastPlan fastMulLhsPoly fastMulRhsPoly) - fastChecksum (checksumIterations := checksumIterations) + fastChecksum pure ({ groupKey := key, title := "Univariate multiplication (" ++ fieldTitle ++ ")", @@ -143,13 +124,6 @@ private def runKoalaBearUnivariateMul (preset : BenchPreset) (gen : StdGen) : ⟨"KoalaBear.Field", checksumKoalaBear⟩ ⟨"KoalaBear.Fast.Field", checksumKoalaBearFast⟩ (fun size ↦ koalaBearArray size false) koalaBearFastArray koalaBearMulNttDomain koalaBearFastMulNttDomain - { ntt := (·.selectNat 200 30 5) - nttFast := (·.selectNat 800 120 25) - nttFastPlan := (·.selectNat 850 120 25) - fastNaive := (·.selectNat 210 30 6) - fastNtt := (·.selectNat 630 90 18) - fastNttFast := (·.selectNat 2450 350 70) - fastNttFastPlan := (·.selectNat 2450 350 70) } preset gen /-- Benchmark BabyBear direct univariate multiplication and root-of-unity NTT variants. -/ @@ -160,13 +134,6 @@ private def runBabyBearUnivariateMul (preset : BenchPreset) (gen : StdGen) : ⟨"BabyBear.Field", checksumBabyBear⟩ ⟨"BabyBear.Fast.Field", checksumBabyBearFast⟩ (fun size ↦ babyBearArray size false) babyBearFastArray babyBearMulNttDomain babyBearFastMulNttDomain - { ntt := (·.selectNat 200 30 5) - nttFast := (·.selectNat 800 120 25) - nttFastPlan := (·.selectNat 850 120 25) - fastNaive := (·.selectNat 210 30 6) - fastNtt := (·.selectNat 630 90 18) - fastNttFast := (·.selectNat 2450 350 70) - fastNttFastPlan := (·.selectNat 2450 350 70) } preset gen /-- Runnable `CompPoly.Univariate.NTT.FastMul` benchmark tasks. -/ @@ -179,9 +146,4 @@ def univariateNttFastMulTasks : List BenchTask := [ runBabyBearUnivariateMul ] -/-- Run selected direct univariate multiplication and root-of-unity NTT benchmarks. -/ -def runUnivariateNttFastMul (preset : BenchPreset) (selection : BenchSelection) (gen : StdGen) : - IO (Array BenchGroup × StdGen) := do - runSelectedTasks univariateNttFastMulTasks preset selection gen - end CompPolyBench diff --git a/bench/CompPolyBench/Univariate/NTT/FastMulLow.lean b/bench/CompPolyBench/Univariate/NTT/FastMulLow.lean index 65bbc572..7153b296 100644 --- a/bench/CompPolyBench/Univariate/NTT/FastMulLow.lean +++ b/bench/CompPolyBench/Univariate/NTT/FastMulLow.lean @@ -19,11 +19,6 @@ open CompPoly namespace CompPolyBench -/-- Benchmark group metadata for `CompPoly.Univariate.NTT.FastMulLow`. -/ -def univariateNttFastMulLowGroupInfos : List BenchGroupInfo := [ - ⟨"univariate-low-product-koalabear", "Univariate low product (KoalaBear)"⟩ -] - /-- Benchmark low-product multiplication variants used by remainder and batch-evaluation paths. -/ private def runKoalaBearUnivariateLowProduct (preset : BenchPreset) (gen : StdGen) : IO (BenchGroup × StdGen) := do @@ -52,73 +47,71 @@ private def runKoalaBearUnivariateLowProduct (preset : BenchPreset) (gen : StdGe let fastNttFastWithFallbackLowMul : CPolynomial.Raw.MulLowContext KoalaBear.Fast.Field := CPolynomial.NTTFast.FastMulLow.withFallback koalaBearFastBestDomainForLength? - let warmup := mulWarmupIterations preset - let measured := mulMeasuredIterations preset - let convolutionMeasured := preset.selectNat 30 5 1 - let nttMeasured := preset.selectNat 100 15 3 - let nttFastMeasured := preset.selectNat 500 70 15 - let fastMeasured := preset.selectNat 210 30 6 - let fastConvolutionMeasured := preset.selectNat 70 10 2 - let fastNttMeasured := preset.selectNat 420 60 12 - let fastNttFastMeasured := preset.selectNat 1960 280 56 - let checksumIterations := groupChecksumIterations measured [ - convolutionMeasured, nttMeasured, nttFastMeasured, fastMeasured, fastConvolutionMeasured, - fastNttMeasured, fastNttFastMeasured - ] - let lowNaive ← runTimed - "univariate-mul-low-naive" "CPolynomial.Raw" "MulLowContext.naive" "KoalaBear.Field" - univariateMulLowShape preset warmup measured + let checksumIterations := digestPeriod 1 + let lowNaive ← runTimedSpec + { name := "univariate-mul-low-naive", representation := "CPolynomial.Raw", + method := "MulLowContext.naive", field := "KoalaBear.Field", + inputShape := univariateMulLowShape, digestIterations := checksumIterations } + preset (fun _ ↦ naiveLowMul.mulLow univariateMulLowOutputCoeffSlots mulLowLhsRaw mulLowRhsRaw) - (checksumRawPolynomial checksumKoalaBear) (checksumIterations := checksumIterations) - let fastLowNaive ← runTimed - "univariate-mul-low-naive-fast" "CPolynomial.Raw" "MulLowContext.naive" - "KoalaBear.Fast.Field" - univariateMulLowShape preset warmup fastMeasured + (checksumRawPolynomial checksumKoalaBear) + let fastLowNaive ← runTimedSpec + { name := "univariate-mul-low-naive-fast", representation := "CPolynomial.Raw", + method := "MulLowContext.naive", field := "KoalaBear.Fast.Field", + inputShape := univariateMulLowShape, digestIterations := checksumIterations } + preset (fun _ ↦ fastNaiveLowMul.mulLow univariateMulLowOutputCoeffSlots fastMulLowLhsRaw fastMulLowRhsRaw) - (checksumRawPolynomial checksumKoalaBearFast) (checksumIterations := checksumIterations) - let lowConvolution ← runTimed - "univariate-mul-low-convolution" "CPolynomial.Raw" "MulLowContext.convolution" - "KoalaBear.Field" - univariateMulLowShape preset warmup convolutionMeasured + (checksumRawPolynomial checksumKoalaBearFast) + let lowConvolution ← runTimedSpec + { name := "univariate-mul-low-convolution", representation := "CPolynomial.Raw", + method := "MulLowContext.convolution", field := "KoalaBear.Field", + inputShape := univariateMulLowShape, digestIterations := checksumIterations } + preset (fun _ ↦ convolutionLowMul.mulLow univariateMulLowOutputCoeffSlots mulLowLhsRaw mulLowRhsRaw) - (checksumRawPolynomial checksumKoalaBear) (checksumIterations := checksumIterations) - let fastLowConvolution ← runTimed - "univariate-mul-low-convolution-fast" "CPolynomial.Raw" "MulLowContext.convolution" - "KoalaBear.Fast.Field" - univariateMulLowShape preset warmup fastConvolutionMeasured + (checksumRawPolynomial checksumKoalaBear) + let fastLowConvolution ← runTimedSpec + { name := "univariate-mul-low-convolution-fast", representation := "CPolynomial.Raw", + method := "MulLowContext.convolution", field := "KoalaBear.Fast.Field", + inputShape := univariateMulLowShape, digestIterations := checksumIterations } + preset (fun _ ↦ fastConvolutionLowMul.mulLow univariateMulLowOutputCoeffSlots fastMulLowLhsRaw fastMulLowRhsRaw) - (checksumRawPolynomial checksumKoalaBearFast) (checksumIterations := checksumIterations) - let lowNtt ← runTimed - "univariate-mul-low-ntt-with-fallback" "CPolynomial.Raw" "FastMulLow.withFallback" - "KoalaBear.Field" - univariateMulLowShape preset warmup nttMeasured + (checksumRawPolynomial checksumKoalaBearFast) + let lowNtt ← runTimedSpec + { name := "univariate-mul-low-ntt-with-fallback", representation := "CPolynomial.Raw", + method := "FastMulLow.withFallback", field := "KoalaBear.Field", + inputShape := univariateMulLowShape, digestIterations := checksumIterations } + preset (fun _ ↦ nttWithFallbackLowMul.mulLow univariateMulLowOutputCoeffSlots mulLowLhsRaw mulLowRhsRaw) - (checksumRawPolynomial checksumKoalaBear) (checksumIterations := checksumIterations) - let fastLowNtt ← runTimed - "univariate-mul-low-ntt-with-fallback-fast" "CPolynomial.Raw" - "FastMulLow.withFallback" "KoalaBear.Fast.Field" - univariateMulLowShape preset warmup fastNttMeasured + (checksumRawPolynomial checksumKoalaBear) + let fastLowNtt ← runTimedSpec + { name := "univariate-mul-low-ntt-with-fallback-fast", representation := "CPolynomial.Raw", + method := "FastMulLow.withFallback", field := "KoalaBear.Fast.Field", + inputShape := univariateMulLowShape, digestIterations := checksumIterations } + preset (fun _ ↦ fastNttWithFallbackLowMul.mulLow univariateMulLowOutputCoeffSlots fastMulLowLhsRaw fastMulLowRhsRaw) - (checksumRawPolynomial checksumKoalaBearFast) (checksumIterations := checksumIterations) - let lowNttFast ← runTimed - "univariate-mul-low-ntt-fast-with-fallback" "CPolynomial.Raw" - "NTTFast.FastMulLow.withFallback" "KoalaBear.Field" - univariateMulLowShape preset warmup nttFastMeasured + (checksumRawPolynomial checksumKoalaBearFast) + let lowNttFast ← runTimedSpec + { name := "univariate-mul-low-ntt-fast-with-fallback", representation := "CPolynomial.Raw", + method := "NTTFast.FastMulLow.withFallback", field := "KoalaBear.Field", + inputShape := univariateMulLowShape, digestIterations := checksumIterations } + preset (fun _ ↦ nttFastWithFallbackLowMul.mulLow univariateMulLowOutputCoeffSlots mulLowLhsRaw mulLowRhsRaw) - (checksumRawPolynomial checksumKoalaBear) (checksumIterations := checksumIterations) - let fastLowNttFast ← runTimed - "univariate-mul-low-ntt-fast-with-fallback-fast" "CPolynomial.Raw" - "NTTFast.FastMulLow.withFallback" "KoalaBear.Fast.Field" - univariateMulLowShape preset warmup fastNttFastMeasured + (checksumRawPolynomial checksumKoalaBear) + let fastLowNttFast ← runTimedSpec + { name := "univariate-mul-low-ntt-fast-with-fallback-fast", + representation := "CPolynomial.Raw", method := "NTTFast.FastMulLow.withFallback", + field := "KoalaBear.Fast.Field", inputShape := univariateMulLowShape, + digestIterations := checksumIterations } + preset (fun _ ↦ fastNttFastWithFallbackLowMul.mulLow univariateMulLowOutputCoeffSlots fastMulLowLhsRaw fastMulLowRhsRaw) - (checksumRawPolynomial checksumKoalaBearFast) (checksumIterations := checksumIterations) + (checksumRawPolynomial checksumKoalaBearFast) pure ({ groupKey := "univariate-low-product-koalabear", title := "Univariate low product (KoalaBear)", @@ -133,10 +126,4 @@ def univariateNttFastMulLowTasks : List BenchTask := [ runKoalaBearUnivariateLowProduct ] -/-- Run selected low-product multiplication benchmarks. -/ -def runUnivariateNttFastMulLow (preset : BenchPreset) (selection : BenchSelection) - (gen : StdGen) : - IO (Array BenchGroup × StdGen) := do - runSelectedTasks univariateNttFastMulLowTasks preset selection gen - end CompPolyBench diff --git a/bench/CompPolyBench/Univariate/NTT/Transform.lean b/bench/CompPolyBench/Univariate/NTT/Transform.lean new file mode 100644 index 00000000..2bfe1623 --- /dev/null +++ b/bench/CompPolyBench/Univariate/NTT/Transform.lean @@ -0,0 +1,229 @@ +/- +Copyright (c) 2026 CompPoly Contributors. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Derek Sorensen +-/ +module + +public import CompPolyBench.Univariate.Common +public import CompPoly.Univariate.NTT.Forward +public import CompPoly.Univariate.NTT.Inverse +public import CompPoly.Univariate.NTTFast.Plan + +/-! +# Multiplicative NTT benchmarks + +The forward and inverse transforms on their own, swept over size, rather than +buried inside a multiplication. `BENCHMARKING.md` §13 names the multiplicative +NTT as one of the operations to measure against Plonky3, and until now the only +way to see it here was as one term of `univariate-mul-*`. + +Two implementations per direction, cross-checked by the group digest: + +* the reference radix-2 transform, `NTT.Forward.forwardImpl` and + `NTT.Inverse.inverseImpl`; +* the planned radix-4 transform, `NTTFast.Plan.forwardImpl` and + `NTTFast.Plan.inverseImpl`, over a `Plan` built outside the timed closure. + +Every body reads its input from a two-entry pool indexed by the iteration +counter. That is not decoration. The first version of this file precomputed +`spectrum` with the very expression the forward reference row then timed, and +the compiler recognised the two as the same: the row reported 6 ns for a +`2 ^ 12` transform and 1.6 million iterations per sample, at every size +identically. A body that varies with `i` can be neither shared with a value +computed outside the loop nor hoisted out of it. + +`workUnits` is `n / 2 * log n`, the radix-2 butterfly count. It is a property +of the *problem*, not of the implementation: giving the radix-4 row its own +smaller count would divide away exactly the algorithmic advantage the group +exists to show. + +## Bit reversal, and where it is allowed to appear + +`Plan.forwardImpl` returns its output bit-reversed relative to the reference +(`NTTFast/Correctness/Pipeline.lean:29`), and `Plan.inverseImpl` expects its +input that way. Neither permutation may enter the timed region: an +`Array.ofFn` of size `n` inside the body would make the fast row look slower, +a failure in the direction that reads as an honest result. + +So the forward group puts the permutation in the planned row's **checksum**, +which `runTimedSpec` uses only in the untimed pass, and gives that row an +explicit `sink` so the default does not drag the permuting checksum into the +measurement. The inverse group instead permutes the planned row's **input** +once, before timing. + +The permutation is defined here rather than imported from +`NTT.Transform.bitRevPermute`, because the reference forward transform is +built from that same function: importing it would let a single wrong +`bitRevNat` produce two compensating errors and a group that agrees on a wrong +digest. +-/ + +public section + +open CompPoly + +namespace CompPolyBench + +/-- Bit-reverse an index of `bits` bits. A local definition on purpose; see above. -/ +private def benchBitRev : Nat → Nat → Nat + | 0, _ => 0 + | bits + 1, i => ((i &&& 1) <<< bits) ||| benchBitRev bits (i >>> 1) + +/-- Apply the bit-reversal permutation to an array of length `2 ^ bits`. -/ +private def benchBitRevPermute {F : Type} (bits : Nat) (zero : F) (a : Array F) : Array F := + Array.ofFn (n := 2 ^ bits) fun i ↦ a.getD (benchBitRev bits i.1) zero + +/-- Radix-2 butterflies in a transform of size `2 ^ logN`. -/ +private def butterflyCount (logN : Nat) : Nat := 2 ^ logN / 2 * logN + +/-- Largest size at which the reference transform still earns a row. + +The reference radix-2 transform costs about 100 ns per butterfly against the +plan's 3.5 ns, so one reference inverse is 2.6 ms at `2 ^ 12` and 53 ms at +`2 ^ 16` — past the sample budget, and the suite's own guidance is that no +ratio may be read off an `n = 1` row. Above this the groups carry the planned +rows alone, and the cross-check lives at the sizes below. -/ +def referenceLogNCap : Nat := 12 + +/-- Time the forward and inverse transforms at one field and one size. + +`coeffs` holds two independently drawn inputs of `2 ^ logN` elements each; see +the note above on why the bodies index a pool rather than closing over one. -/ +@[specialize] private def runTransformGroup {F : Type} [Field F] [BEq F] [LawfulBEq F] + (groupKey title fieldName : String) (logN : Nat) + (domain : CPolynomial.NTT.Domain F) (coeffsA coeffsB : Array F) (checksum : F → Nat) + (sink : F → UInt64) (preset : BenchPreset) : IO BenchGroup := do + let plan := CPolynomial.NTTFast.Plan.ofDomain domain + let polys : Array (CPolynomial.Raw F) := #[coeffsA, coeffsB] + let spectra : Array (Array F) := + polys.map fun p ↦ CPolynomial.NTT.Forward.forwardImpl domain p + let spectraRev : Array (Array F) := spectra.map (benchBitRevPermute logN 0) + let poly (i : Nat) : CPolynomial.Raw F := polys.getD (i % 2) coeffsA + let spectrum (i : Nat) : Array F := spectra.getD (i % 2) coeffsA + let spectrumRev (i : Nat) : Array F := spectraRev.getD (i % 2) coeffsA + let shape := s!"n = 2^{logN}, two inputs" + let units := butterflyCount logN + let arrayChecksum := checksumArray checksum + let arraySink := arraySampleSink sink + let digests := digestPeriod 2 + let forwardPlanned ← runTimedSpec + { name := s!"ntt-forward-{fieldName}-plan", representation := "Array", + method := "radix-4 DIF (plan)", field := fieldName, inputShape := shape, + digestIterations := digests, workUnits := units, digestClass := "forward" } + preset (fun i ↦ CPolynomial.NTTFast.Plan.forwardImpl plan (poly i)) + (fun a ↦ arrayChecksum (benchBitRevPermute logN 0 a)) (sink := arraySink) + let inversePlanned ← runTimedSpec + { name := s!"ntt-inverse-{fieldName}-plan", representation := "Array", + method := "radix-4 DIT (plan)", field := fieldName, inputShape := shape, + digestIterations := digests, workUnits := units, digestClass := "inverse" } + preset (fun i ↦ CPolynomial.NTTFast.Plan.inverseImpl plan (spectrumRev i)) arrayChecksum + (sink := arraySink) + if logN > referenceLogNCap then + pure { groupKey := groupKey, title := title, + records := #[forwardPlanned, inversePlanned] } + else + let forwardReference ← runTimedSpec + { name := s!"ntt-forward-{fieldName}", representation := "Array", method := "radix-2", + field := fieldName, inputShape := shape, digestIterations := digests, + workUnits := units, digestClass := "forward" } + preset (fun i ↦ CPolynomial.NTT.Forward.forwardImpl domain (poly i)) arrayChecksum + (sink := arraySink) + let inverseReference ← runTimedSpec + { name := s!"ntt-inverse-{fieldName}", representation := "Array", method := "radix-2", + field := fieldName, inputShape := shape, digestIterations := digests, + workUnits := units, digestClass := "inverse" } + preset (fun i ↦ CPolynomial.NTT.Inverse.inverseImpl domain (spectrum i)) arrayChecksum + (sink := arraySink) + pure { groupKey := groupKey, title := title, + records := #[forwardReference, forwardPlanned, inverseReference, inversePlanned] } + +/-- Time the KoalaBear transforms at one size. -/ +private def runKoalaBearTransform (logN : Nat) (hlogN : logN ≤ KoalaBear.twoAdicity) + (preset : BenchPreset) (gen : StdGen) : IO (BenchGroup × StdGen) := do + let (valuesA, gen) := (koalaBearArray (2 ^ logN) false).run gen + let (valuesB, gen) := (koalaBearArray (2 ^ logN) false).run gen + let group ← runTransformGroup s!"ntt-koalabear-l{logN}" + s!"Multiplicative NTT, KoalaBear, n = 2^{logN}" "koalabear" logN + (CPolynomial.NTT.KoalaBear.fastDomainOfLogN logN hlogN) + (koalaBearFastArray valuesA) (koalaBearFastArray valuesB) + checksumKoalaBearFast (fun x ↦ natSink (checksumKoalaBearFast x)) preset + pure (group, gen) + +/-- Time the BabyBear transforms at one size. -/ +private def runBabyBearTransform (logN : Nat) (hlogN : logN ≤ BabyBear.twoAdicity) + (preset : BenchPreset) (gen : StdGen) : IO (BenchGroup × StdGen) := do + let (valuesA, gen) := (babyBearArray (2 ^ logN) false).run gen + let (valuesB, gen) := (babyBearArray (2 ^ logN) false).run gen + let group ← runTransformGroup s!"ntt-babybear-l{logN}" + s!"Multiplicative NTT, BabyBear, n = 2^{logN}" "babybear" logN + (CPolynomial.NTT.BabyBear.fastDomainOfLogN logN hlogN) + (babyBearFastArray valuesA) (babyBearFastArray valuesB) + checksumBabyBearFast (fun x ↦ natSink (checksumBabyBearFast x)) preset + pure (group, gen) + +/-- Time plan construction, which the transform groups deliberately hoist out. + +A `Plan` is a pure value built once per domain and reused, so its cost belongs +to setup rather than to a transform — but it is `O(n)` field multiplications +building the twiddle tables, so a caller that rebuilds one per transform pays +more than the transform. That is what this group is for. -/ +private def runPlanConstruction (preset : BenchPreset) (gen : StdGen) : + IO (BenchGroup × StdGen) := do + -- `Plan.ofDomain d` for a literal `d` is a closed term, which Lean evaluates + -- once and caches: the row then reports its true cost divided by the + -- iteration count. Indexing a pool by a runtime offset keeps the body live. + let (offsets, gen) := (randomNatArray 1 1).run gen + let offset := offsets.getD 0 0 + let smallDomain := CPolynomial.NTT.KoalaBear.fastDomainOfLogN 12 (by decide) + let largeDomain := CPolynomial.NTT.KoalaBear.fastDomainOfLogN 16 (by decide) + let smallPool := #[smallDomain, smallDomain] + let largePool := #[largeDomain, largeDomain] + let planChecksum (P : CPolynomial.NTTFast.Plan KoalaBear.Fast.Field) : Nat := + checksumArray (checksumArray checksumKoalaBearFast) P.twiddles + let planSink (P : CPolynomial.NTTFast.Plan KoalaBear.Fast.Field) : UInt64 := + arraySampleSink (arraySampleSink (fun x ↦ natSink (checksumKoalaBearFast x))) P.twiddles + let small ← runTimedSpec + { name := "ntt-plan-koalabear", representation := "Plan", method := "ofDomain", + field := "koalabear", inputShape := "n = 2^12", digestIterations := digestPeriod 2, + workUnits := 1, digestClass := "l12" } + preset + (fun i ↦ CPolynomial.NTTFast.Plan.ofDomain (smallPool.getD ((i + offset) % 2) smallDomain)) + planChecksum (sink := planSink) + let large ← runTimedSpec + { name := "ntt-plan-koalabear", representation := "Plan", method := "ofDomain", + field := "koalabear", inputShape := "n = 2^16", digestIterations := digestPeriod 2, + workUnits := 1, digestClass := "l16" } + preset + (fun i ↦ CPolynomial.NTTFast.Plan.ofDomain (largePool.getD ((i + offset) % 2) largeDomain)) + planChecksum (sink := planSink) + pure ({ groupKey := "ntt-plan-koalabear", title := "NTT plan construction (KoalaBear)", + records := #[small, large] }, gen) + +/-- Registry entries for the standalone transform benchmarks. -/ +def nttTransformTasks : List BenchTask := [ + BenchTask.fromGroupRunner ⟨"ntt-koalabear-l8", "Multiplicative NTT, KoalaBear, n = 2^8"⟩ + (runKoalaBearTransform 8 (by decide)), + BenchTask.fromGroupRunner ⟨"ntt-koalabear-l10", "Multiplicative NTT, KoalaBear, n = 2^10"⟩ + (runKoalaBearTransform 10 (by decide)), + BenchTask.fromGroupRunner ⟨"ntt-koalabear-l12", "Multiplicative NTT, KoalaBear, n = 2^12"⟩ + (runKoalaBearTransform 12 (by decide)), + BenchTask.fromGroupRunner ⟨"ntt-koalabear-l14", "Multiplicative NTT, KoalaBear, n = 2^14"⟩ + (runKoalaBearTransform 14 (by decide)), + BenchTask.fromGroupRunner ⟨"ntt-koalabear-l16", "Multiplicative NTT, KoalaBear, n = 2^16"⟩ + (runKoalaBearTransform 16 (by decide)), + BenchTask.fromGroupRunner ⟨"ntt-babybear-l8", "Multiplicative NTT, BabyBear, n = 2^8"⟩ + (runBabyBearTransform 8 (by decide)), + BenchTask.fromGroupRunner ⟨"ntt-babybear-l10", "Multiplicative NTT, BabyBear, n = 2^10"⟩ + (runBabyBearTransform 10 (by decide)), + BenchTask.fromGroupRunner ⟨"ntt-babybear-l12", "Multiplicative NTT, BabyBear, n = 2^12"⟩ + (runBabyBearTransform 12 (by decide)), + BenchTask.fromGroupRunner ⟨"ntt-babybear-l14", "Multiplicative NTT, BabyBear, n = 2^14"⟩ + (runBabyBearTransform 14 (by decide)), + BenchTask.fromGroupRunner ⟨"ntt-babybear-l16", "Multiplicative NTT, BabyBear, n = 2^16"⟩ + (runBabyBearTransform 16 (by decide)), + BenchTask.fromGroupRunner ⟨"ntt-plan-koalabear", "NTT plan construction (KoalaBear)"⟩ + runPlanConstruction +] + +end CompPolyBench diff --git a/bench/CompPolyBench/Univariate/ReedSolomon.lean b/bench/CompPolyBench/Univariate/ReedSolomon.lean new file mode 100644 index 00000000..bdd79d21 --- /dev/null +++ b/bench/CompPolyBench/Univariate/ReedSolomon.lean @@ -0,0 +1,93 @@ +/- +Copyright (c) 2026 CompPoly Contributors. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Derek Sorensen +-/ +module + +public import CompPolyBench.Univariate.Common +public import CompPoly.Univariate.ReedSolomon.NTTEncode + +/-! +# Reed-Solomon encoding benchmarks + +The definitional encoder against the certified NTT one. `ReedSolomon.encode` +evaluates the message polynomial at every domain node by Horner, which is +`Θ(n · k)`; `ReedSolomon.nttCodeword` is the forward NTT, `Θ(n log n)`. They +are *equal*, not merely equivalent — `forwardImpl_eq_encode` +(`ReedSolomon/NTTEncode.lean:71`) — so the group digest checks the pair rather +than only cross-checking two implementations of a shared spec. + +Rate one half, the FRI setting: a message of `n / 2` elements encoded to `n`. + +`workUnits` is `n`, the codeword length: the number of evaluations the problem +asks for, whichever way they are produced. + +The quadratic row is why the sizes stop where they do. At `n = 2^10` one +`encode` is already a millisecond; at `2^12` it is past the sample budget. So +the paired groups run at `2^8` and `2^10` and a third group carries the NTT +encoder alone at `2^14`, which is the shape `runAdditiveNttFastLargeCase` +already uses for the same reason. +-/ + +public section + +open CompPoly + +namespace CompPolyBench + +/-- Time the two encoders at one size, over KoalaBear's native-word representation. -/ +private def runEncodeGroup (logN : Nat) (hlogN : logN ≤ KoalaBear.twoAdicity) + (withQuadratic : Bool) (preset : BenchPreset) (gen : StdGen) : + IO (BenchGroup × StdGen) := do + let n := 2 ^ logN + let k := n / 2 + let (values, gen) := (koalaBearArray k false).run gen + let fastValues := koalaBearFastArray values + let domain := CPolynomial.NTT.KoalaBear.fastDomainOfLogN logN hlogN + let rsDomain := ReedSolomon.nttDomainToRS domain + let messageAt (j : Nat) : Vector KoalaBear.Fast.Field k := + ⟨Array.ofFn (n := k) fun i ↦ fastValues.getD ((i.1 + j) % k) 1, by simp⟩ + let messages := #[messageAt 0, messageAt 1] + let message (i : Nat) : Vector KoalaBear.Fast.Field k := messages.getD (i % 2) (messageAt 0) + let hk : k ≤ domain.n := by + simp only [CPolynomial.NTT.Domain.n, k, n] + exact Nat.div_le_self _ _ + let shape := s!"n = 2^{logN}, rate 1/2, two messages" + let checksum (v : Vector KoalaBear.Fast.Field _) : Nat := + checksumArray checksumKoalaBearFast v.toArray + let sink (v : Vector KoalaBear.Fast.Field _) : UInt64 := + arraySampleSink (fun x ↦ natSink (checksumKoalaBearFast x)) v.toArray + let nttRecord ← runTimedSpec + { name := "rs-encode-koalabear-ntt", representation := "Vector", + method := "nttCodeword", field := "koalabear", inputShape := shape, + digestIterations := digestPeriod 2, workUnits := n } + preset (fun i ↦ ReedSolomon.nttCodeword domain (message i) hk) checksum + (sink := sink) + let records ← if withQuadratic then do + let encodeRecord ← runTimedSpec + { name := "rs-encode-koalabear", representation := "Vector", + method := "encode (Horner per node)", field := "koalabear", inputShape := shape, + digestIterations := digestPeriod 2, workUnits := n } + preset (fun i ↦ ReedSolomon.encode rsDomain (message i)) checksum + (sink := sink) + pure #[encodeRecord, nttRecord] + else pure #[nttRecord] + pure ({ groupKey := s!"rs-encode-koalabear-l{logN}", + title := s!"Reed-Solomon encoding, KoalaBear, n = 2^{logN}", + records := records }, gen) + +/-- Registry entries for the Reed-Solomon encoding benchmarks. -/ +def reedSolomonTasks : List BenchTask := [ + BenchTask.fromGroupRunner + ⟨"rs-encode-koalabear-l8", "Reed-Solomon encoding, KoalaBear, n = 2^8"⟩ + (runEncodeGroup 8 (by decide) true), + BenchTask.fromGroupRunner + ⟨"rs-encode-koalabear-l10", "Reed-Solomon encoding, KoalaBear, n = 2^10"⟩ + (runEncodeGroup 10 (by decide) true), + BenchTask.fromGroupRunner + ⟨"rs-encode-koalabear-l14", "Reed-Solomon encoding, KoalaBear, n = 2^14"⟩ + (runEncodeGroup 14 (by decide) false) +] + +end CompPolyBench diff --git a/bench/CompPolyBench/Univariate/Roots/FiniteField.lean b/bench/CompPolyBench/Univariate/Roots/FiniteField.lean index cbc933d7..e49d308f 100644 --- a/bench/CompPolyBench/Univariate/Roots/FiniteField.lean +++ b/bench/CompPolyBench/Univariate/Roots/FiniteField.lean @@ -28,12 +28,25 @@ private def rootWorkloadDegree : Nat := rootWorkloadRootCount + 2 private def rootWorkloadDistinctRoots : Nat := rootWorkloadRootCount + 1 -private def rootWorkloadRootSeeds : List Nat := - [3, 3] ++ (List.range rootWorkloadRootCount).map (fun i ↦ i + 5) +/-- Root seeds for one workload polynomial, offset by `base`. + +`base` comes from the group's random stream rather than being written down, and +that is load-bearing rather than cosmetic. With fixed seeds the whole benchmark +body is a closed term — `p` is a nullary constant and so is the root context — +and Lean evaluates it once and hands every later iteration the cached array. The +row then reports its true cost divided by `itersPerSample`, which was 1 to 20 +under hand-tuned counts and is hundreds of thousands under a wall-clock budget. +Drawing `base` at run time makes the body depend on a local, which is the same +shape every other group in the suite already has. + +The structure is unchanged: `rootWorkloadDistinctRoots` distinct roots with one +of them repeated, so the degree and the root multiset shape do not move. -/ +private def rootWorkloadRootSeeds (base : Nat) : List Nat := + [base, base] ++ (List.range rootWorkloadRootCount).map (fun i ↦ base + i + 2) private def rootWorkloadShape : String := s!"degree={rootWorkloadDegree}, {rootWorkloadDistinctRoots} distinct roots, " ++ - "repeated root at 3" + "one of them repeated" private def productOfLinearRootSeeds {F : Type*} [Field F] [BEq F] [LawfulBEq F] (seeds : List Nat) : CPolynomial F := @@ -42,8 +55,8 @@ private def productOfLinearRootSeeds {F : Type*} 1 private def nonlinearRootPolynomial {F : Type*} - [Field F] [BEq F] [LawfulBEq F] : CPolynomial F := - productOfLinearRootSeeds rootWorkloadRootSeeds + [Field F] [BEq F] [LawfulBEq F] (base : Nat) : CPolynomial F := + productOfLinearRootSeeds (rootWorkloadRootSeeds base) private def insertSortedNat (x : Nat) : List Nat → List Nat | [] => [x] @@ -70,54 +83,48 @@ def univariateFiniteFieldRootGroupInfos : List BenchGroupInfo := [ private def runKoalaBearFiniteFieldRoots (preset : BenchPreset) (gen : StdGen) : IO (Prod BenchGroup StdGen) := do - let p : CPolynomial KoalaBear.Field := nonlinearRootPolynomial - let fastP : CPolynomial KoalaBear.Fast.Field := nonlinearRootPolynomial - let warmup := preset.selectNat 1 0 0 - let measured := preset.selectNat 10 1 1 - let nttMeasured := preset.selectNat 40 6 1 - let nttFastMeasured := preset.selectNat 120 17 3 - let fastMeasured := preset.selectNat 60 9 2 - let fastNttMeasured := preset.selectNat 120 17 3 - let fastNttFastMeasured := preset.selectNat 400 60 12 - let checksumIterations := groupChecksumIterations measured [ - nttMeasured, nttFastMeasured, fastMeasured, fastNttMeasured, fastNttFastMeasured - ] - let row <- runTimed - "univariate-roots-finite-field-naive" "CPolynomial" - "smooth cyclic, canonical" - "KoalaBear.Field" rootWorkloadShape preset warmup measured - (fun _ ↦ koalaBearFieldRootContext.rootsInField p) - (checksumNormalizedRoots checksumKoalaBear) checksumIterations - let nttRow <- runTimed - "univariate-roots-finite-field-ntt" "CPolynomial" - "smooth cyclic, NTT" - "KoalaBear.Field" rootWorkloadShape preset warmup nttMeasured - (fun _ ↦ koalaBearNttFieldRootContext.rootsInField p) - (checksumNormalizedRoots checksumKoalaBear) checksumIterations - let nttFastRow <- runTimed - "univariate-roots-finite-field-nttfast" "CPolynomial" - "smooth cyclic, NTTFast" - "KoalaBear.Field" rootWorkloadShape preset warmup nttFastMeasured - (fun _ ↦ koalaBearNttFastFieldRootContext.rootsInField p) - (checksumNormalizedRoots checksumKoalaBear) checksumIterations - let fastRow <- runTimed - "univariate-roots-finite-field-fast-naive" "CPolynomial" - "smooth cyclic, canonical" - "KoalaBear.Fast.Field" rootWorkloadShape preset warmup fastMeasured - (fun _ ↦ fastKoalaBearFieldRootContext.rootsInField fastP) - (checksumNormalizedRoots checksumKoalaBearFast) checksumIterations - let fastNttRow <- runTimed - "univariate-roots-finite-field-fast-ntt" "CPolynomial" - "smooth cyclic, NTT" - "KoalaBear.Fast.Field" rootWorkloadShape preset warmup fastNttMeasured - (fun _ ↦ fastKoalaBearNttFieldRootContext.rootsInField fastP) - (checksumNormalizedRoots checksumKoalaBearFast) checksumIterations - let fastNttFastRow <- runTimed - "univariate-roots-finite-field-fast-nttfast" "CPolynomial" - "smooth cyclic, NTTFast" - "KoalaBear.Fast.Field" rootWorkloadShape preset warmup fastNttFastMeasured + let (bases, gen) := (randomNatArray 1 1000).run gen + let base := bases.getD 0 1 + 1 + let p : CPolynomial KoalaBear.Field := nonlinearRootPolynomial base + let fastP : CPolynomial KoalaBear.Fast.Field := nonlinearRootPolynomial base + let checksumIterations := digestPeriod 1 + let row <- runTimedSpec + { name := "univariate-roots-finite-field-naive", representation := "CPolynomial", + method := "smooth cyclic, canonical", field := "KoalaBear.Field", + inputShape := rootWorkloadShape, digestIterations := checksumIterations } + preset (fun _ ↦ koalaBearFieldRootContext.rootsInField p) + (checksumNormalizedRoots checksumKoalaBear) + let nttRow <- runTimedSpec + { name := "univariate-roots-finite-field-ntt", representation := "CPolynomial", + method := "smooth cyclic, NTT", field := "KoalaBear.Field", inputShape := rootWorkloadShape, + digestIterations := checksumIterations } + preset (fun _ ↦ koalaBearNttFieldRootContext.rootsInField p) + (checksumNormalizedRoots checksumKoalaBear) + let nttFastRow <- runTimedSpec + { name := "univariate-roots-finite-field-nttfast", representation := "CPolynomial", + method := "smooth cyclic, NTTFast", field := "KoalaBear.Field", + inputShape := rootWorkloadShape, digestIterations := checksumIterations } + preset (fun _ ↦ koalaBearNttFastFieldRootContext.rootsInField p) + (checksumNormalizedRoots checksumKoalaBear) + let fastRow <- runTimedSpec + { name := "univariate-roots-finite-field-fast-naive", representation := "CPolynomial", + method := "smooth cyclic, canonical", field := "KoalaBear.Fast.Field", + inputShape := rootWorkloadShape, digestIterations := checksumIterations } + preset (fun _ ↦ fastKoalaBearFieldRootContext.rootsInField fastP) + (checksumNormalizedRoots checksumKoalaBearFast) + let fastNttRow <- runTimedSpec + { name := "univariate-roots-finite-field-fast-ntt", representation := "CPolynomial", + method := "smooth cyclic, NTT", field := "KoalaBear.Fast.Field", + inputShape := rootWorkloadShape, digestIterations := checksumIterations } + preset (fun _ ↦ fastKoalaBearNttFieldRootContext.rootsInField fastP) + (checksumNormalizedRoots checksumKoalaBearFast) + let fastNttFastRow <- runTimedSpec + { name := "univariate-roots-finite-field-fast-nttfast", representation := "CPolynomial", + method := "smooth cyclic, NTTFast", field := "KoalaBear.Fast.Field", + inputShape := rootWorkloadShape, digestIterations := checksumIterations } + preset (fun _ ↦ fastKoalaBearNttFastFieldRootContext.rootsInField fastP) - (checksumNormalizedRoots checksumKoalaBearFast) checksumIterations + (checksumNormalizedRoots checksumKoalaBearFast) pure ({ groupKey := "univariate-roots-finite-field-koalabear", title := "Univariate finite-field smooth-subgroup root search (KoalaBear)", diff --git a/bench/README.md b/bench/README.md index 8b497b1c..297396aa 100644 --- a/bench/README.md +++ b/bench/README.md @@ -46,17 +46,25 @@ lake exe CompPolyBench --markdown-only --groups univariate-low-product-koalabear ## Output -Each run writes generated JSONL and Markdown reports under `bench/`: +Each run writes generated JSONL and Markdown reports under `bench/out/`, which +is created on demand and ignored in its entirety: ```text -results-YYMMDD-HHMMSS.jsonl -report-YYMMDD-HHMMSS.md +bench/out/results-YYMMDD-HHMMSS.jsonl +bench/out/report-YYMMDD-HHMMSS.md +bench/out/manifest-YYMMDD-HHMMSS.json ``` +The manifest records what produced the numbers — commit, whether the tree was +dirty, toolchain, preset and the budget it resolved to, seed, selection, and +host details — and is written for every run, `--validate-only` included. It is +a separate file rather than a header line in the JSONL, because every consumer +of that file assumes uniform records. + By default, a run writes both files. A checksum mismatch is reported in the Markdown report and makes the executable exit nonzero after writing artifacts. -Within each group, checksums are computed over the shared prefix of iterations -run by every implementation in that group. +Within each group, checksums are computed over the group's `digestPeriod` — the +period of its bodies in the iteration index, capped at `digestIterationCap`. ## What Is Measured @@ -73,7 +81,15 @@ Roughly by area, with representative group prefixes: | Univariate root finding | `univariate-roots-finite-field-*` | | Additive NTT | `additive-ntt-btf*` | | Extension fields | `fields-extension-*-mul`, `fields-extension-*-inv` | +| Binary tower fields | `fields-tower-bt128-*`: `BitVec` spec vs packed-word implementation | +| Base-field arithmetic | `fields-{koalabear,babybear,mersenne31,goldilocks}-{mul,add,inv,pow}`: canonical `ZMod` vs native-word, latency and throughput | +| Pairing scalar multiplication | `fields-{bn254,bls12-381,bls12-377}-mul` | | Scalar-field inversion | `fields-mont64x8-*-inv`: `ZMod` extended Euclid vs checked binary GCD vs Fermat | +| Binary tower scalar kernels | `fields-tower-bt{8,64}-*`: table-driven vs recursive | +| Multiplicative NTT | `ntt-{koalabear,babybear}-l*` over `n = 2^8 … 2^16`, plus `ntt-plan-koalabear` | +| Reed-Solomon encoding | `rs-encode-koalabear-l*`: definitional encoder vs the certified NTT one | +| Schoolbook / NTT crossover | `univariate-mul-crossover-*`, degree<4 to degree<1024 | +| Harness self-check | `harness-floor`, `harness-canary`, `harness-chain-floor`, `harness-chain-linearity`: the harness measuring itself, see below | Use `--list` for the authoritative set; the prefixes above drift as groups are added. @@ -90,21 +106,168 @@ univariate-dense-bn254 univariate-dense-bls12-381 univariate-dense-bls12-377 ``` +## How A Benchmark Is Measured + +`runTimedSpec` does two passes over each benchmark body. + +The **validation pass** is untimed and folds a strong `Nat` digest +(`mixChecksum`) over the full result. It runs for `digestPeriod` iterations — +the period of the body in its iteration index, capped at `digestIterationCap`, +so the oracle sees every input without the pass costing as much as the +measurement it validates. This is what the group agreement check +compares, and it is the reason a wrong-but-fast implementation cannot be +benchmarked: a mismatch inside a group exits nonzero. + +The **timed pass** folds each result through `sink : α → UInt64` instead. A sink +exists only to keep the result live so the body cannot be optimised away; its +value is never compared against anything. The default sink truncates the `Nat` +digest, which is free when that digest already fits a machine word. Pass an +explicit `sink :=` when it does not: + +- carriers whose canonical value exceeds `2 ^ 63` — a `Nat` digest there + allocates a bignum on most inputs (`sinkGoldilocksFast`, `sinkZMod`); +- aggregate results — sink a fixed-position sample rather than walking the whole + structure, and make every row of a group sink the *same* shape, or the group's + ratio measures the digests rather than the implementations. + +Both rows of a group should carry comparable sink cost. Where a representation +makes that impossible — a `ZMod` element above `2 ^ 63` has no cheap word digest +while its fast counterpart does — the residual shows up in `harness-floor` +territory and the group's ratio is a lower bound on the real speedup. + +### Chained bodies and the per-unit column + +An operation of one or two nanoseconds cannot be measured one per timed +iteration: the harness floor is about the same size, and the operand-pool +idiom around it — `xs.getD (i % xs.size) unit` — is a boxed-`Nat` modulo, a +bounds check and a boxed array read, twice. So the field and kernel groups +perform their operation `workUnits` times per iteration, through the +combinators in `bench/CompPolyBench/Harness/Chain.lean`, and the report gains a +**Per unit (ps)** column dividing the median by that count. + +Two shapes, reported separately because a prover is bounded by different ones +in different places, and named as Plonky3 names them: + +- **latency** — each operation depends on the last, so the pipeline cannot + overlap two; +- **throughput** — ten independent accumulators, so it can. + +Every row of a group must agree on `workUnits`, because the count describes the +*problem* and not the implementation; a group whose rows disagree fails the +run. A per-unit number is **not** comparable with `harness-floor`, which is a +per-iteration cost: the chain floor for comparison is `harness-chain-floor`. + +### Sampling and dispersion + +A benchmark's cost is collected as a *set* of samples, not one total, and the +sizes come from the preset's wall-clock budget rather than from a written-down +iteration count. A calibration ramp times 1, 2, 4, … iterations until the +warmup budget is met — the ramp *is* the warmup — and its last step estimates +the per-iteration cost. That estimate fixes how many iterations make up a +`sampleNanos` sample, and `measureNanos` caps how many samples the row can +afford. Every sample replays the same iteration indices, so samples differ only +in machine state. + +A consequence worth knowing: `Iterations` is no longer comparable between runs, +because it depends on how fast the machine was when the row was calibrated. +`Median` and `Spread` are the columns to compare. + +Reports show the **median** sample as the headline number and a `Spread` column +holding the median absolute deviation as a percentage of the median: + +| Spread | Meaning | +|---|---| +| `±2.4%` | normal: 20 samples, MAD 2.4% of the median | +| `±1.1% (n=3)` | replicated, but too few times for the spread to mean much | +| `n=1` | one iteration exhausted the budget; a single unrepeated sample | +| `±0.4% !2` | two samples were labelled severe Tukey outliers | + +`n=1` rows carry no dispersion information at all and no ratio should be read +off them. They occur where a single iteration is already expensive; the fix is a +smaller input shape, not more iterations. + +Outliers are **labelled, never dropped**, at the conventional Tukey fences of +1.5x and 3x the interquartile range. Labelling is suppressed when the +interquartile range is zero, since fences of zero width would mark every sample +that differs at all. The full per-sample vector is emitted as `samples_picos` in +the JSONL, along with `min`, `median`, `mean`, `p95`, `stddev` and `mad` in +picoseconds per iteration. + +Warmup is at least one sample's worth of iterations regardless of the preset, so +no benchmark is measured entirely cold. + +### Harness self-check + +`harness-floor` times an empty body, giving the per-iteration cost of the loop +and the sink; every other benchmark's reported time sits on top of it. +`harness-canary` times a body with a known, non-eliminable cost and **fails the +run** if it does not exceed the floor by at least `canaryFloorRatio`. A benchmark +that has been optimised away otherwise looks exactly like a benchmark that got +very fast, and the canary is what tells the two apart. Both are measured whenever +either is selected, because the check is a comparison between them. + +`harness-chain-floor` and `harness-chain-linearity` do the same two jobs for +chained bodies. The floor group carries the cheapest honest operation in both +chain shapes, so a per-unit number can be read against something; the linearity +group **fails the run** unless eight times the chain length costs at least four +times as much, which is what catches a chain the compiler has collapsed. + +Both checks earn their keep. The chain floor's first operation was +`x ^^^ (x >>> 7)`, whose 64-deep block is algebraically the identity in +characteristic two, and LLVM found that: the row reported a sixteenth of a +cycle per operation *and the linearity check still passed*, because what +collapsed was each block rather than the loop over blocks. + ## Determinism -Input generation uses a fixed seed. Checksums are stable for the same group -selection and preset. They are a cross-check between implementations within one -group, not a value to compare across runs: the generator is threaded through the -selected groups in order, so changing the selection — or adding a group — changes -the inputs, and therefore the checksums, of the groups that follow it. +Each group derives its own input generator from its key (`genFor`), so a group's +inputs do not depend on which other groups ran, or in what order. Concretely: + +- `--group X` and `--groups X,Y` measure the same inputs for `X`, in either order; +- adding, removing or renaming a group changes nothing for any other group; +- the curated CI subset measures the same inputs as a full local run; +- a checksum is comparable across runs and across commits, so a change in one is + a real change in behaviour rather than a change in the input schedule. + +Checksums remain a cross-check between the implementations within a group; that +they are also stable across runs, and across presets, is what makes them usable +as regression fixtures. The digest length is the period of the group's bodies in +the iteration index, which is a property of the benchmark rather than of the +preset or the machine it runs on. + +## The two CI tracks + +Correctness and timing are separated, because only one of them is trustworthy on +a shared runner. + +**Correctness gates every PR.** `lean_action_ci.yml` runs + +```bash +lake exe CompPolyBench --medium --validate-only --groups "" +``` + +which does the untimed digest pass and the group agreement check but collects no +samples. It takes about 29 seconds of CPU over the curated set and fails the run on a +digest mismatch or a collapsed harness canary. `--validate-only` is worth running +locally for the same reason: it is the fast way to ask whether an implementation +is still correct. + +**Timings run on demand.** `benchmarks.yml` produces them three ways: **Actions → +Benchmarks → Run workflow** with a preset and optional group list, a `/bench` +comment on a PR from a repo member, or automatically on any PR touching +`bench/**`. Results are posted as a PR comment and uploaded as an artifact. -## CI +They are kept out of the blocking path deliberately, though not for the reason +you might expect. *Within* one run the shared runner is actually steadier than a +busy laptop — median MAD 0.2% against 1.4% locally — but severe outliers are +about twice as common, and neither figure is the one a gate needs. What a +regression gate compares is **runs against each other**, on a runner whose CPU +model changes between runs, and no single run can measure that. Until it is +measured, the timings are advisory. -GitHub Actions runs `lake exe CompPolyBench --medium` over the curated group list -in the `BENCH_CI_GROUPS` environment variable, uploads generated artifacts, and -appends the Markdown report to the step summary. +## The curated group set -CI does not run every registered group, so **a new group must be added to -`BENCH_CI_GROUPS` in `.github/workflows/lean_action_ci.yml` to be covered there**. -An unknown key in that list fails the run, so a renamed group is caught rather than -silently dropped. +Both tracks default to the group list in `bench/ci-groups.txt` — one key per +line, `#` comments ignored. Neither runs every registered group, so **a new group +must be added there to be covered**. An unknown key fails the run, so a renamed +group is caught rather than silently dropped. diff --git a/bench/ci-groups.txt b/bench/ci-groups.txt new file mode 100644 index 00000000..4cbd7504 --- /dev/null +++ b/bench/ci-groups.txt @@ -0,0 +1,77 @@ +# Curated benchmark group set, with two consumers: +# +# * the correctness gate in .github/workflows/lean_action_ci.yml, which runs +# `CompPolyBench --validate-only` over these groups on every PR; +# * the default selection of .github/workflows/benchmarks.yml, the on-demand +# timing workflow. +# +# Deliberately a subset. `lake exe CompPolyBench --list` shows every registered +# group; a new group is covered here only once added below, and an unknown key +# fails the run, so a renamed group is caught rather than silently dropped. +# +# Excluded on purpose are the groups whose single iteration costs seconds +# (univariate-batch-large-*, univariate-mod-by-monic-medium-*), and the sizes +# of a sweep that the middle of the sweep already covers: one size of the +# multiplicative NTT and two of the schoolbook/NTT crossover stand in for the +# rest, since a body that is wrong at one size is wrong at all of them. +# +# One key per line. Blank lines and `#` comments are ignored. +harness-floor +harness-canary +harness-chain-floor +harness-chain-linearity +univariate-dense-koalabear +univariate-dense-babybear +univariate-sparse-koalabear +univariate-monic-remainder-small-koalabear +univariate-dense-goldilocks +univariate-dense-bn254 +univariate-dense-bls12-381 +univariate-dense-bls12-377 +univariate-batch-small-koalabear +univariate-many-one-point-koalabear +univariate-mul-koalabear +univariate-mul-babybear +univariate-low-product-koalabear +univariate-roots-finite-field-koalabear +multivariate-dense-koalabear +multivariate-sparse-koalabear +multilinear-coeff-koalabear +multilinear-hypercube-koalabear +multilinear-many-mle-koalabear +bivariate-full-koalabear +bivariate-divlinear-koalabear-y32 +guruswami-sudan-interp-small-koalabear +guruswami-sudan-root-koalabear +guruswami-sudan-core-small-koalabear +guruswami-sudan-filtered-core-small-koalabear +additive-ntt-btf3-l2-r2 +additive-ntt-btf3-l4-r2 +additive-ntt-btf4-l7-r2 +fields-tower-bt128-mul +fields-tower-bt128-inv +fields-extension-koalabear-ext4-mul +fields-extension-koalabear-ext4-inv +fields-extension-babybear-ext4-mul +fields-extension-babybear-ext4-inv +fields-mont64x8-bn254-inv +fields-mont64x8-bls12-381-inv +fields-mont64x8-bls12-377-inv +fields-goldilocks-mul +fields-goldilocks-inv +fields-koalabear-mul +fields-koalabear-add +fields-koalabear-inv +fields-koalabear-pow +fields-babybear-mul +fields-mersenne31-mul +fields-bn254-mul +fields-tower-bt8-mul +fields-tower-bt64-mul +fields-tower-bt64-inv-word +ntt-koalabear-l12 +ntt-babybear-l12 +ntt-plan-koalabear +rs-encode-koalabear-l10 +univariate-mul-crossover-64 +univariate-mul-crossover-256 diff --git a/docs/wiki/README.md b/docs/wiki/README.md index fdf7c9e7..c5de3f87 100644 --- a/docs/wiki/README.md +++ b/docs/wiki/README.md @@ -11,6 +11,8 @@ are too specific or too changeable to keep at the repo root. - [`repo-map.md`](repo-map.md) - where to edit and how the main subtrees relate. - [`generated-files.md`](generated-files.md) - derived outputs and their sources of truth. +- [`benchmarking.md`](benchmarking.md) - how the benchmark suite measures, how to + read its output, and how to add a group. - [`build-cache.md`](build-cache.md) - Mathlib's olean cache and CompPoly's prebuilt release archive. - [`module-system.md`](module-system.md) - Lean module-system conventions, `meta` @@ -34,6 +36,8 @@ are too specific or too changeable to keep at the repo root. - `quickstart.md` for commands, validation, and CI expectations. - `repo-map.md` for repo structure and work-area routing. - `generated-files.md` for derived outputs and source-of-truth rules. + - `benchmarking.md` for benchmark measurement, output interpretation, and + adding groups. - `build-cache.md` for prebuilt-artifact caches and how they are published. - `module-system.md` for module headers, `public`/`meta` imports, and privacy. - `representations-and-bridges.md` for representation choice and Mathlib bridges. diff --git a/docs/wiki/benchmarking.md b/docs/wiki/benchmarking.md new file mode 100644 index 00000000..2300420d --- /dev/null +++ b/docs/wiki/benchmarking.md @@ -0,0 +1,252 @@ +# Benchmarking + +How the compiled benchmark suite measures, what its output means, and what to do +when adding a benchmark. [`bench/README.md`](../../bench/README.md) is the +operator's guide — invocation, presets, group selection, the group inventory. +This page owns the recurring guidance. + +## Commands + +```bash +lake build CompPolyBench +lake exe CompPolyBench --small # every registered group, timed +lake exe CompPolyBench --medium --validate-only # correctness only, no timings +lake exe CompPolyBench --groups fields-goldilocks-mul +lake exe CompPolyBench --list # authoritative group keys +``` + +Output lands in `bench/out/`, which is created on demand and ignored in its +entirety. A checksum mismatch inside a group makes the executable exit nonzero +after writing its artifacts, and CI's validation step has no +`continue-on-error`, so a mismatch fails the run. + +## Two tracks, because only one of them is trustworthy + +The suite does two separable jobs. Keeping them apart is the difference between a +gate you can believe and a gate that fails on noise. + +| | Correctness | Timing | +|---|---|---| +| What | digest pass, group agreement, harness canary | median, dispersion, outlier labels | +| Where | `lean_action_ci.yml`, **every PR** | `benchmarks.yml`, **on demand** | +| How | `--validate-only` over `bench/ci-groups.txt` | `--small`/`--medium`/`--large` | +| Cost | ~29s of CPU over the curated set, ~174s over all groups | minutes | +| Gates? | **yes**, fails the run | no, advisory | + +`--validate-only` runs the untimed digest pass and the agreement check and +collects no samples, so it is deterministic and machine-independent. That is +exactly what a gate should be. It is also the fast local answer to "is this +implementation still correct". + +Timings stay out of the blocking path, but the measured reason is not the +obvious one. On `ubuntu-latest` *within-run* dispersion came out **tighter** than +on a quiet local machine — median MAD 0.2% against 1.4% — while severe Tukey +outliers were about twice as common (56 of 172 rows against 27 of 286). A mostly +idle VM slice punctuated by preemption looks exactly like that. + +Neither number is what a gate needs. A regression gate compares **runs against +each other**, on a runner whose CPU model varies between runs, and a single run +cannot measure that variance. So the timings are advisory because cross-run +comparability is unvalidated, not because the runner is jittery. + +Three ways to get timings: **Actions → Benchmarks → Run workflow** with a preset +and optional group list; a `/bench` comment on a PR from a repo member, +optionally followed by a group list; or automatically on a PR touching +`bench/**`, since a change to the harness itself should be measured. Results +arrive as a PR comment and an artifact. + +One thing the canary needs: it compares timed totals, so under `--validate-only` +it would pass vacuously against a zero floor. `runTimed` therefore takes a +`forceTiming` flag that the self-check sets, and the canary keeps running (~50ms) +in both modes. If you touch that path, break the canary body deliberately and +confirm a `--validate-only` run still fails. + +## The two passes + +Every benchmark body is executed twice, for different purposes, and confusing +them is the main way benchmark numbers go wrong. + +The **validation pass** is untimed. It folds a strong `Nat` digest over the full +result, and it is what the cross-implementation agreement check compares. This is +why a wrong-but-fast implementation cannot be benchmarked here. It runs for the +period of the body in its iteration index (`digestPeriod`, capped at +`digestIterationCap`), and counts towards warmup, since it has already executed +the body. + +The **timed pass** folds each result through `sink : α → UInt64`. A sink exists +only to keep the result live so the body cannot be optimised away; its value is +never compared against anything. + +**A sink may only skip work the benchmark has already done.** Sampling a few +positions of a materialised `Array` is correct — the transform already computed +every element. Sampling a few positions of a `Fin n → α` is *not*: nothing has +been computed until an index is applied, so sampling makes that row do a fraction +of the work its counterpart does, and the group's ratio becomes meaningless. + +Pass an explicit `sink :=` whenever the default `Nat` digest would allocate — +carriers whose canonical value exceeds `2 ^ 63` are the usual case. Both rows of +a group should carry comparable sink cost; where a representation makes that +impossible, the group's ratio is a lower bound on the real speedup. + +## Reading a result + +The headline number is the **median** sample, not the mean and not a total. The +`Spread` column carries the median absolute deviation as a percentage of the +median: + +| Spread | Meaning | +|---|---| +| `±2.4%` | normal | +| `±1.1% (n=3)` | too few samples for the spread to mean much | +| `n=1` | one iteration exhausted the budget; a single unrepeated sample | +| `±0.4% !2` | two samples labelled severe Tukey outliers | + +**Never read a ratio off an `n=1` row.** Those benchmarks pin an input shape +large enough that one iteration exhausts the budget; the fix is a smaller shape, +not more iterations. + +Outliers are labelled, never dropped. The full per-sample vector is emitted as +`samples_picos` in the JSONL, with `min`, `median`, `mean`, `p95`, `stddev` and +`mad` in picoseconds per iteration. + +On a quiet local machine the median absolute deviation across replicated rows is +around 1.4% of the median, with a maximum near 5%. Treat differences below that +as noise, and expect a shared CI runner to be worse. + +`Warmup` and `Iterations` come from the preset's wall-clock budget, not from a +number written down beside the benchmark: a calibration ramp times 1, 2, 4, … +iterations until the warmup budget is met, and its last step estimates the +per-iteration cost that sizes the samples. So **`Iterations` is not comparable +between runs** — it depends on how fast the machine was when that row was +calibrated. Compare `Median` and `Spread`. `manifest-.json` records the +commit, dirty flag, toolchain, budgets, seed and host for exactly this reason. + +## The harness self-check + +`harness-floor` times an empty body: the per-iteration cost of the loop and the +sink, which every other benchmark sits on top of. `harness-canary` times a body +with a known non-eliminable cost and **fails the run** if it does not clear the +floor by `canaryFloorRatio`. + +The canary is not ceremony. A benchmark that has been optimised away looks +exactly like a benchmark that got very fast, and the difference is invisible in +the output. Anything that changes the timing path — inlining attributes, +specialisation, a new indirection between `runTimed` and the loop — should be +checked against the floor before and after. + +Note that a function interposed between the specialisation boundary and the timed +loop must carry `@[specialize]`, or the closure indirection returns and the floor +rises by an order of magnitude. + +## Determinism + +Each group derives its generator from its key, so a group's inputs do not depend +on which other groups ran or in what order. `--group X` and `--groups X,Y` agree, +the CI subset agrees with a full local run, and digests are comparable across +runs and commits. + +Digests remain preset-dependent, because the validation pass length derives from +the measured iteration count. + +Record `name` is **not** unique — `extension-mul` is emitted by the ext4, ext5 +and ext6 groups. Any tool comparing two result files must key on +`(name, field, input_shape)`. + +## Adding a benchmark + +1. Write a group runner returning a `BenchGroup`, and register it with + `BenchTask.fromGroupRunner`. The `BenchGroupInfo` you pass is authoritative + for the key and title. +2. Call `runTimedSpec` with a `BenchSpec` record. There is no iteration count to + choose — the preset's budget and the calibration ramp size the row. +3. Give the row a `workUnits` if it performs its operation more than once — + see "Chained bodies" below — and a `digestClass` if the group carries more + than one comparison. Every row of a group must agree on `workUnits`, and + must agree on a digest *within* each class; either disagreement fails the + run. +4. Set `digestIterations` to the **period of the body in its iteration index**, + via `digestPeriod`: 1 for a `fun _ ↦ …` body, the pool size for a body that + cycles one. It must never depend on the preset or on anything the machine + decides, or the digest stops being comparable across runs and fixtures become + impossible. Truncating to the period is not a weaker check — iterations past + one full cycle recompute a bit-identical result. +5. Make the body depend on `i`, through a value built at run time. There are + two ways to lose this and both have happened here. A body that is a *closed + term* is evaluated once and cached, and the row then reports its true cost + divided by `itersPerSample` — see finding 2 in `BENCHMARKING.md` §12.6, and + the plan-construction group, which reported 32 ns for two sizes that differ + by 14x. A body that is merely *loop-invariant* can be shared with a value + computed outside the loop: the NTT forward group precomputed its spectrum + with the same expression the reference row then timed, and that row reported + 6 ns for a `2^12` transform. Indexing a small pool by `i` closes both. +6. Give every implementation in a digest class the same `checksum`, so the + agreement check is meaningful. +7. Supply a `sink` if the default would allocate, and make the group's rows + symmetric under the rule above. +8. Add the key to `bench/ci-groups.txt` to have it covered by the correctness + gate and by the default selection of the on-demand timing workflow. An + unknown key fails the run, so a rename is caught rather than dropped. +9. New modules under `bench/` need no `./scripts/update-lib.sh` run; that script + globs `CompPoly/*.lean` only, and the lakefile globs `CompPolyBench` + submodules. + +## Chained bodies + +A field operation is one or two nanoseconds and the harness floor is about +1.8 ns, so a body that performs it once per iteration reports the harness. The +combinators in `bench/CompPolyBench/Harness/Chain.lean` perform it `workUnits` times +per iteration instead, and the report divides. + +Three properties of those combinators are load-bearing, and the obvious +alternative is measurably wrong in each case: + +- **No array.** `Subtype` erases to its payload but `Array` does not inherit + that: every element is a `lean_object*`, and `lean_box_uint64` allocates. A + one-cycle dependent chain cannot be fed from a pointer array. +- **No `for` with `let mut`.** `ForIn` threads one state value, so ten mutable + locals become a nested `Prod`, which does not erase — nine allocations per + round. +- **The operation is a direct argument of an `@[specialize]` runner**, never a + structure field and never a `[Field F]` projection. Through a closure it is + an indirect call per operation, which is more than the operation. + +Two consequences for a call site. Bind a captured constant to a local before +building the operation lambda: a projection inside it is lifted into the +operation and costs a load and an unbox per round. And take `workUnits` from +`latencyUnits` / `throughputUnitsOf` rather than from the depth you asked for, +since the chains run whole unrolled blocks and round a bad depth down. + +**Read the emitted IR when adding a chain.** `.lake/build/ir/**.c` should show +the specialised loop taking unboxed scalar parameters with no `lean_alloc_*` +in the body. `harness-chain-linearity` catches a chain that is not executed at +all; it does not catch one that is partly folded, and a chain of a +`GF(2)`-linear operation folds completely — see the note on `chainFloorStep` +in `bench/CompPolyBench/Harness/SelfCheck.lean`. + +## Known gaps + +Recorded so they are not rediscovered. The audit and plan live in +`BENCHMARKING.md` at the repo root. + +- A handful of rows are still `n=1`, all of them workloads whose single iteration + exhausts its budget. They need smaller input shapes, decided per benchmark; no + harness change reaches that. +- No result storage, baseline comparison, or regression gate for run-time + benchmarks; only build timing gets that treatment. +- Per-row floor subtraction is not reported, because the floor is + per-representation rather than global. +- No polynomial-matrix groups, and no `batchInverse` / `sumOfProducts` / + `dot_array` — Plonky3 benchmarks those and CompPoly does not have them yet, + so the feature comes before the measurement. No prime-field `square` group + either, deliberately: `square` is `mul x x` on every prime carrier here, and + Plonky3 has no field-level `square` benchmark for the same reason. +- The polynomial-basis `GF(2^64)` of `CompPoly/Fields/Binary/BF64/` and its + cubic extension have no group, and **cannot have one until a library bug is + fixed**. `BF64.instFintype` (`CompPoly/Fields/Binary/BF64/Impl.lean:391`) is + a closed constant whose value is a `Finset` of all `2 ^ 64` elements, and + Lean evaluates closed constants at module initialisation — so any executable + importing that module hangs before `main` runs. Elaboration never notices, + because the interpreter forces constants on demand, which is why the tests + build. Marking the instance `noncomputable` is not the fix: `Extension.Ext` + takes `[Fintype F]` and its operations then stop compiling, so the repair is + to `CompPoly/Fields/Extension/` rather than to the instance. diff --git a/docs/wiki/binary-fields-and-ntt.md b/docs/wiki/binary-fields-and-ntt.md index 6a9f64b2..e2933cc7 100644 --- a/docs/wiki/binary-fields-and-ntt.md +++ b/docs/wiki/binary-fields-and-ntt.md @@ -16,6 +16,13 @@ CompPoly/Fields/Binary/ Impl.lean XPowTwoPowGcdCertificate.lean XPowTwoPowModCertificate.lean + BF64.lean + BF64/ + BaseCertificate.lean + Basic.lean + Reduce.lean + Impl.lean + Ext3.lean Tower/ Abstract/* Concrete/* @@ -67,6 +74,53 @@ The GHASH model lives under `Binary/BF128Ghash/`. Use this area when the task is specifically about `GF(2^128)`, GHASH, or the certificate-based proof strategy for binary-field arithmetic. +## Polynomial-Basis GF(2^64) Surface + +`Binary/BF64/` builds `GF(2^64)` as the flat quotient +`GF(2)[x] / (x^64 + x^4 + x^3 + x + 1)`, together with its degree-three extension +`GF(2^192)`. + +This is a *different presentation* from the `GF(2^64)` that appears as level 6 of +`Binary/Tower/`. The tower builds it by iterated quadratic extension, so the two use +different bases and their bit-level encodings disagree — on the same bit patterns, `2 * 3` +is `6` here and `1` in the tower's rung. Neither substitutes for the other wherever the +encoding is observable. + +- [`../../CompPoly/Fields/Binary/BF64/BaseCertificate.lean`](../../CompPoly/Fields/Binary/BF64/BaseCertificate.lean) + holds generated Rabin certificate data; regenerate it with + [`../../scripts/gen_rabin_certificate.py`](../../scripts/gen_rabin_certificate.py) + rather than editing it. +- [`../../CompPoly/Fields/Binary/BF64/Basic.lean`](../../CompPoly/Fields/Binary/BF64/Basic.lean) + defines the modulus, proves it irreducible, and gives the quotient model `BF64Quot` with + its cardinality. Degree 64 is composite, so the general `Polynomial.irreducible_of_rabin` + is used; the collapsed prime-degree form is unsound here. +- [`../../CompPoly/Fields/Binary/BF64/Reduce.lean`](../../CompPoly/Fields/Binary/BF64/Reduce.lean) + folds a 128-bit carry-less product back into 64 bits using the reduction constant `0x1B`. +- [`../../CompPoly/Fields/Binary/BF64/Impl.lean`](../../CompPoly/Fields/Binary/BF64/Impl.lean) + carries the nominal field elements with `BitVec 64` coordinates, their quotient bridge, and the + `CommRing` / `Field` instances built around an Itoh-Tsujii inverse. +- [`../../CompPoly/Fields/Binary/BF64/Ext3.lean`](../../CompPoly/Fields/Binary/BF64/Ext3.lean) + instantiates the extension framework at `y^3 + y + 1`, whose irreducibility needs no + certificate. + +Use `BF64.ofBitVec` to construct polynomial-basis words and `BF64.toBitVec` to recover them; +the maps are inverse and introduce no implicit conversion to another field presentation. +For example, `BF64.ofBitVec (2#64)` denotes `X`, while `(2 : BF64)` is zero. Raw reference +vectors must use the coordinate constructor rather than field numeral casts. + +`BF64` and `BF64.Ext3` expose proof-only `Finite` instances and the cardinality theorems +`BF64.nat_card_bf64` and `BF64.nat_card_ext3`. They provide no default enumeration dictionary. +A proof that needs `Fintype` can choose `Fintype.ofFinite` locally; `card_bf64` and `card_ext3` +then apply to that chosen enumeration. Arithmetic does not enumerate these enormous fields. +The [native startup test](../../tests/README.md#native-startup-and-field-arithmetic) checks +linked initialization and actual base/extension arithmetic under resource bounds. + +The instances here are assembled field-by-field on purpose: a transport such as +`Function.Injective.commRing` takes the bridge as *data* and would make the arithmetic +noncomputable, which would also break `Ext3`. The `#guard` checks in +[`../../tests/CompPolyTests/Fields/Binary/BF64.lean`](../../tests/CompPolyTests/Fields/Binary/BF64.lean) +run the compiled arithmetic and fail the build if that ever regresses. + ## Binary Tower Surface The tower development splits into abstract theory, concrete constructions, and @@ -74,10 +128,37 @@ support lemmas: - `Tower/Abstract/*` - abstract tower definitions and algebra. - `Tower/Concrete/*` - concrete basis, core definitions, and field instances. + These modules use shared support lemmas without importing the abstract tower construction. + `Tower/Equiv.lean` imports both constructions to relate them; use that bridge or + `Tower/Impl.lean` when both presentations are needed. + [Concrete/Coordinates.lean](../../CompPoly/Fields/Binary/Tower/Concrete/Coordinates.lean) + supplies `ConcreteBinaryTower.Coordinates.succCoordinates`: an + executable linear equivalence from level `k + 1` to two level-`k` coefficients, ordered + constant term first and generator term second. It uses the existing tower embedding + for the scalar action. Pass it to `AlgebraTower.natCoordinatesOfLE` or + `AlgebraTower.natCoordinatesConstOfLE` for coordinates between arbitrary ordered levels. + [Concrete/RelativeCoordinates.lean](../../CompPoly/Fields/Binary/Tower/Concrete/RelativeCoordinates.lean) + specializes these maps as `coordinates` and `pack`, with round-trip and scalar-action laws. + Its readback theorems identify each coefficient with the corresponding raw bit block, + including natural-word and individual-bit readback. These coordinates use the tower field's + own embedding and retain low-first block order. + [Concrete/BasisCoordinates.lean](../../CompPoly/Fields/Binary/Tower/Concrete/BasisCoordinates.lean) + identifies these executable coordinates with the representation of `multilinearBasis` + at the same numeric indices. Its packing formula reconstructs a word as the sum of + embedded coefficients times those basis vectors. - `Tower/Support/*` - supporting lemmas about defining polynomials, linear independence, and finite-index helpers. -- `Tower/Equiv.lean`, `Tower/Impl.lean`, and `Tower/TensorAlgebra.lean` connect the - layers and expose useful transport lemmas. +- `Tower/Fast.lean` - packed machine-word tower arithmetic with a GF(2^8) + lookup-table base, proven against `ConcreteBTField`; `Field` instances and ring + isomorphisms at every level up to GF(2^128). Runtime definitions live in the + zero-import `Tower/FastDefs.lean` for `precompileModules` consumers. +- `Tower/Equiv.lean` and `Tower/Impl.lean` connect the layers and expose useful + transport lemmas. +- `Tower/TensorAlgebra.lean` re-exports the generic tensor basis API from + `CompPoly/LinearAlgebra/TensorProduct/Basis.lean`. Its right scalar action is + explicit; importing either path preserves Mathlib's default left action. See + [`../../CompPoly/LinearAlgebra/README.md`](../../CompPoly/LinearAlgebra/README.md) + for the local algebra, module and scalar-action selection needed for equal tensor factors. Use the tower subtree when the task is about characteristic-2 extensions more generally, not just GHASH. @@ -95,10 +176,14 @@ The additive-NTT stack is split by role rather than by one monolithic file: - [`../../CompPoly/Fields/Binary/AdditiveNTT/Algorithm.lean`](../../CompPoly/Fields/Binary/AdditiveNTT/Algorithm.lean) defines evaluation points, twiddle factors, stage updates, and the algorithm data flow. +- [`../../CompPoly/Fields/Binary/AdditiveNTT/Executable.lean`](../../CompPoly/Fields/Binary/AdditiveNTT/Executable.lean) + defines the generic function-backed and array-backed algorithms without importing a tower + construction. - [`../../CompPoly/Fields/Binary/AdditiveNTT/Impl.lean`](../../CompPoly/Fields/Binary/AdditiveNTT/Impl.lean) - packages the implementation-facing surface. + re-exports those algorithms with concrete tower bases, instances, and the existing example. - [`../../CompPoly/Fields/Binary/AdditiveNTT/Correctness.lean`](../../CompPoly/Fields/Binary/AdditiveNTT/Correctness.lean) - proves the implementation correct. + proves the generic implementations correct without requiring a concrete tower. The umbrella + `AdditiveNTT.lean` retains the combined generic and concrete surface for existing consumers. When changing additive NTT, expect to read several of these files together. Algorithm changes often cascade into `Intermediate`, `Impl`, and `Correctness`. @@ -107,13 +192,15 @@ Algorithm changes often cascade into `Intermediate`, `Impl`, and `Correctness`. - Shared BitVec or characteristic-2 helper lemma: `Binary/Common.lean` - GHASH field model or certificate proof: `Binary/BF128Ghash/` +- Polynomial-basis `GF(2^64)` or its cubic extension: `Binary/BF64/` - General tower-field structure or extension lemmas: `Binary/Tower/` - Additive-NTT basis, domain, or correctness proof: `Binary/AdditiveNTT/` ## Reading Order Suggestions - For GHASH: `Prelude` -> `Basic` -> `Impl` -> certificate files +- For polynomial-basis `GF(2^64)`: `Basic` -> `Reduce` -> `Impl` -> `Ext3` - For tower fields: `Prelude` / `Basic` -> `Abstract` or `Concrete` branch -> `Equiv` / `Impl` - For additive NTT: `Domain` -> `NovelPolynomialBasis` -> `Intermediate` -> - `Algorithm` -> `Impl` -> `Correctness` + `Algorithm` -> `Executable` -> `Correctness`; add `Impl` for concrete tower instances diff --git a/docs/wiki/coding-theory.md b/docs/wiki/coding-theory.md index 40ff59e9..bb0799d8 100644 --- a/docs/wiki/coding-theory.md +++ b/docs/wiki/coding-theory.md @@ -111,17 +111,23 @@ a context instance and inherits every theorem. ### Interpolation backends -Both produce a valid witness in the sense of `ValidInterpolationWitness`; they +All produce a valid witness in the sense of `ValidInterpolationWitness`; they differ in how the constrained linear system is solved. | Backend | Files | When to use | |---|---|---| | Dense | `Interpolation/Dense/{Algorithm,Correctness}.lean` | Small parameters. Builds the constraint matrix explicitly and calls the dense kernel solver. Simple, and the easiest to reason about. | | Lee-O'Sullivan | `Interpolation/LeeOSullivan/` ([LOS06]) | Larger parameters. Works from a Gröbner-basis perspective: build a module basis, then shift-reduce it with Mulders-Storjohann. `leeOSullivanInterpolate_sound` and `leeOSullivanInterpolate_complete`, with the argument split across nine files under `Correctness/`. | +| Approximant-basis | `Interpolation/ApproximantBasis/` + `LinearAlgebra/PolynomialMatrix/Approximant/` | Long codes / many errors. Solves modular key equations via a minimal approximant (PM-Basis) solver; cost is quasi-linear in length and independent of corruption level. | +| Hybrid | `Interpolation/Hybrid/` | Budgeted Lee–O'Sullivan with fallback to approximant (ski-rental style). Result equals one of the two verified backends. | + +Supporting pieces: `WitnessDivisibility*.lean` — quasi-linear multiplicity check +equivalent to pointwise Hasse derivatives (used in long-code validation). `Interpolation/Basic.lean` holds the constraints and normalized-witness helpers -shared by both, and `Interpolation/Correctness.lean` the shared results +shared by the backends, and `Interpolation/Correctness.lean` the shared results (`interpolationWitnessIsValidBool_iff`, `lowMessageDegreeInterpolation_sound`). +Named KoalaBear contexts for all four backends live in `Implementations.lean`. ### Root-finding backends @@ -157,11 +163,22 @@ specific to it. | `RootProduct.lean` | the product of linear factors over a root set | | `Correctness.lean` | `monicNormalize_root_iff`, `gcdMonic_root_iff_left_right`, and the divisibility transport lemmas | | `SmoothSubgroup/` | subgroup-refinement splitting ([MOV92]) for fields whose multiplicative group admits a smooth schedule | +| `Shoup/` | small-characteristic trace-coordinate splitting ([vzGS92]); `SmallPrimeTraceContext` + adapter to `LinearFactorProductSplitter` | +| `LasVegas/` | bounded Las Vegas Cantor–Zassenhaus (odd-char and char-2 trace branches) with explicit `ProbeFamily` randomness, deterministic fallback, and probability proofs | + +The pipeline consumes a `LinearFactorProductSplitter`. Three backends supply that +interface: + +| Backend | When to use | +|---|---| +| SmoothSubgroup | Multiplicative group admits a smooth cyclic refinement schedule | +| Shoup | Small base characteristic `p`, presented as `GF(p^k)`; splits via `k` trace coordinates without enumerating the field | +| LasVegas | General finite fields; odd-field CZ and/or even-trace attempts under a probe cutoff, with enumeration fallback | -The pipeline consumes a `LinearFactorProductSplitter`; `SmoothSubgroup/` supplies -a contract-bearing smooth context plus an adapter to that interface. A field with -no smooth refinement schedule needs a different splitter — that is the open work -here. Benchmarked as `univariate-roots-finite-field-*`. +Concrete coverage includes `ZMod` odd primes (Las Vegas tests) and degree-one +binary cases via `ZMod 2` and binary-tower level 0 (Shoup tests). Larger Tower +levels as char-2 Shoup contexts are the natural integration path for production +binary fields. ## Where To Start By Task @@ -182,8 +199,8 @@ here. Benchmarked as `univariate-roots-finite-field-*`. - Gao: `GaoDecoder` → `GaoCorrectness` (soundness → completeness → farness) - Guruswami-Sudan: `Context` → `Core` → `CoreCorrectness` → one interpolation backend → one root backend → `Filter` → `Executable` -- Root finding: `Context` → `Backend` → `Extraction` → `Correctness`, then - `SmoothSubgroup/` if the field admits it +- Root finding: `Context` → `Backend` → `Extraction` → `Correctness`, then the + splitter that fits the field (`SmoothSubgroup/`, `Shoup/`, or `LasVegas/`) Read `Context.lean` first for GS. The contexts are the interface the rest of the subtree is written against, and the correctness theorems are unreadable without @@ -196,8 +213,10 @@ knowing which contracts they assume. asks for it by name. - **No FRI or polynomial-commitment integration.** `decode_none_farness` is the hook a proximity test would use, but nothing consumes it yet. -- **Root finding needs a smooth multiplicative group** for the only splitter that - currently exists. +- **Binary Tower root contexts for high widths** (e.g. 32/64-bit) are not yet + packaged as named `SmallPrimeTraceContext` instances; the Shoup tests exercise + tower level 0 and `ZMod 2`. Wire higher levels when char-2 root search is needed + in production paths. - **List-size bounds are not formalized.** Soundness and completeness are proved relative to the supplied parameters; the Johnson-bound analysis that says how many candidates can survive is not. @@ -215,6 +234,12 @@ knowing which contracts they assume. Decoding of Reed-Solomon Codes*][Ale05] * [Menezes, A. J., van Oorschot, P. C., and Vanstone, S. A., *Subgroup Refinement Algorithms for Root Finding in GF(q)*][MOV92] +* [von zur Gathen, J., and Shoup, V., *Computing Frobenius maps and factoring + polynomials*][vzGS92] +* Cantor–Zassenhaus equal-degree factorization (odd characteristic) and + char-2 trace splitting as used in `LasVegas/` +* Beckermann–Labahn / Giorgi–Jeannerod–Villard PM-Basis approximant methods + as used in `PolynomialMatrix/Approximant/` and `Interpolation/ApproximantBasis/` BibTeX entries for these keys are in [`../../blueprint/src/references.bib`](../../blueprint/src/references.bib). diff --git a/docs/wiki/field-extensions.md b/docs/wiki/field-extensions.md index 2f1dbd3e..7477ebd0 100644 --- a/docs/wiki/field-extensions.md +++ b/docs/wiki/field-extensions.md @@ -1,14 +1,16 @@ # Field Extensions -`CompPoly/Fields/Extension/` is the computable field-extension framework for odd -characteristic. It models `F[X] / f` for an **arbitrary monic modulus** `f` as a dense -coefficient vector and proves it equal to `AdjoinRoot f`, so Mathlib field theory applies to -it. The parameters are `ExtensionParams` (the modulus stored by its lower coefficients); -binomials `X^d - W` keep the ergonomic front-end `BinomialParams`, mapped in by -`BinomialParams.toExtensionParams`. - -This page owns extension-field architecture. The characteristic-2 stack is a separate, -independent development — see [`binary-fields-and-ntt.md`](binary-fields-and-ntt.md). +`CompPoly/Fields/Extension/` is the computable field-extension framework. It models `F[X] / f` +for an **arbitrary monic modulus** `f` as a dense coefficient vector and proves it equal to +`AdjoinRoot f`, so Mathlib field theory applies to it. The parameters are `ExtensionParams` (the +modulus stored by its lower coefficients); binomials `X^d - W` keep the ergonomic front-end +`BinomialParams`, mapped in by `BinomialParams.toExtensionParams`. + +This page owns extension-field architecture. Nothing in it assumes odd characteristic, and +the framework has one characteristic-2 consumer: `BF64.Ext3`, the cubic extension of `GF(2^64)` +(see "A characteristic-2 consumer" below). The rest of the char-2 stack — the GHASH field, the +tower fields, the additive NTT — is a separate, independent development that predates this +framework; see [`binary-fields-and-ntt.md`](binary-fields-and-ntt.md). ## Binomials When Possible, General Moduli When Not @@ -21,10 +23,11 @@ binomial is available, prefer it — it buys two things: - **Cheap multiplication.** `X^d = W` means the high half of the schoolbook product folds back with a single scalar multiply, with no polynomial remainder step. -But a binomial is not always available. A degree-`d` binomial extension of `F_q` requires -`d ∣ q - 1`; when `gcd(d, q - 1) = 1` the map `x ↦ x^d` is a bijection and **every** `X^d - W` -has a root. Over KoalaBear, `p - 1 = 2^24 · 127`, so the only binomial degrees available are the -powers of two (up to `2^24`) and multiples of 127: +But a binomial is not always available. For `d > 1`, when `gcd(d, q - 1) = 1`, the map +`x ↦ x^d` is a bijection on `F_q`, so **every** `X^d - W` has a root and is reducible. +The condition `d ∣ q - 1` suffices for the exponent-divisibility side conditions in the binomial +criterion below, but is not necessary for irreducibility in general. Over KoalaBear, +`p - 1 = 2^24 · 127`, and the degrees considered here behave as follows: | `d` | bits | binomial? | |---|---|---| @@ -50,16 +53,27 @@ makes a cheap Frobenius and a norm-based inverse possible. See "Choosing a gener | Rabin's test, general | [`../../CompPoly/Data/Polynomial/Rabin.lean`](../../CompPoly/Data/Polynomial/Rabin.lean) | `irreducible_of_rabin`, `irreducible_iff_rabin` for any degree over any finite field | | Factor-degree bound | [`../../CompPoly/ToMathlib/Polynomial/Irreducible.lean`](../../CompPoly/ToMathlib/Polynomial/Irreducible.lean) | `exists_factor_natDegree_le_of_reducible` | | Binomial criterion | [`../../CompPoly/Fields/Extension/Binomial.lean`](../../CompPoly/Fields/Extension/Binomial.lean) | the collapse to base-field exponentiations; `irreducible_X_pow_four_sub_C_iff` | -| Rabin certificates | [`../../CompPoly/Data/Polynomial/RabinCertificate.lean`](../../CompPoly/Data/Polynomial/RabinCertificate.lean) | kernel-checked chains for non-binomial moduli; `runChain_sound`, `irreducible_of_rabin_prime_degree`, `irreducible_of_rabin_two_prime_factors`, `irreducible_of_rabin_degree_six` | -| Carrier and ring ops | [`../../CompPoly/Fields/Extension/Defs.lean`](../../CompPoly/Fields/Extension/Defs.lean) | `ExtensionParams`, `BinomialParams` (+ `toExtensionParams`), `Ext P`, `Ext.shiftReduce`, `Ext.monomialMod`, `Ext.mul` (spec), `Ext.red` + `Ext.mulTbl` (compiled, via `@[csimp]`) | +| Rabin certificates | [`../../CompPoly/Data/Polynomial/RabinCertificate.lean`](../../CompPoly/Data/Polynomial/RabinCertificate.lean) | kernel-checked chains for non-binomial moduli; `runChain_sound`, `irreducible_of_rabin_prime_degree`, `irreducible_of_rabin_two_prime_factors`, `irreducible_of_rabin_degree_six`, and the `_of_card` forms concrete callers use | +| Carrier and raw arithmetic | [`../../CompPoly/Fields/Extension/Arithmetic.lean`](../../CompPoly/Fields/Extension/Arithmetic.lean) | `ExtensionParams`, `BinomialParams` (+ `toExtensionParams`), `Ext P`, `Ext.shiftReduce`, `Ext.monomialMod`, `Ext.mul` (spec), `Ext.red` + `Ext.mulTbl` (compiled, via `@[csimp]`) | +| Polynomial specification | [`../../CompPoly/Fields/Extension/Defs.lean`](../../CompPoly/Fields/Extension/Defs.lean) | `ExtensionParams.poly`, degree/monicity, binomial correspondence | +| Finiteness and cardinality | [`../../CompPoly/Fields/Extension/Cardinality.lean`](../../CompPoly/Fields/Extension/Cardinality.lean) | `Finite`, optional `Fintype`, cardinality certificates, `card_ext`, `nat_card_ext` | | Bridge and `CommRing` | [`../../CompPoly/Fields/Extension/Bridge.lean`](../../CompPoly/Fields/Extension/Bridge.lean) | `toQuot`, `toQuot_shiftReduce`, `toQuot_mul`, `instCommRing` | -| Bijectivity and `Field` | [`../../CompPoly/Fields/Extension/Field.lean`](../../CompPoly/Fields/Extension/Field.lean) | `ringEquivQuot`, `card_ext`, `inv`, `instField` | +| Bijectivity and `Field` | [`../../CompPoly/Fields/Extension/Field.lean`](../../CompPoly/Fields/Extension/Field.lean) | `ringEquivQuot`, inverse correctness, `instField` | + +Import `CompPoly.Fields.Extension.Arithmetic` for raw presentations, coordinates and ring +arithmetic, including the canonical inverse candidate. The raw carrier has no algebraic +assumptions; arithmetic uses `Ring`. Import `Defs` for polynomial specifications, `Bridge` +for quotient correspondence and ring laws, or `Cardinality` for finite-coordinate facts. +`Field` requires a finite base field, a separate `Fact (Nat.card F = P.q)` certificate and +irreducibility. The binomial criterion remains in `CompPoly.Fields.Extension.Binomial`. +The `CompPoly.Fields.Extension` facade re-exports all six modules. `Data/Polynomial/Rabin.lean` generalizes the degree-128/GF(2) specialization `irreducible_of_rabin_128_passed_over_GF2` in `Fields/Binary/BF128Ghash/Basic.lean`, but does not -yet replace it — `Binary/` is deliberately untouched, so there are currently **two** Rabin -soundness proofs in the repo. Rebasing the GHASH one onto `irreducible_of_rabin` is a named -follow-up; until then, a fix to the argument needs applying in both places. +yet replace it, so there are currently **two** Rabin soundness proofs in the repo. Rebasing the +GHASH one onto `irreducible_of_rabin` is a named follow-up; until then, a fix to the argument +needs applying in both places. Note this is about the *GHASH* development specifically — +`BF64` does use the general pipeline, via the generated `Binary/BF64/BaseCertificate.lean`. Concrete instances live next to their base field: [`KoalaBear/Ext4.lean`](../../CompPoly/Fields/KoalaBear/Ext4.lean) (`X^4 - 3`), @@ -75,6 +89,24 @@ and [`KoalaBear/Ext6.lean`](../../CompPoly/Fields/KoalaBear/Ext6.lean) (`X^6 + X [`Ext6/SexticIrreducible.lean`](../../CompPoly/Fields/KoalaBear/Ext6/SexticIrreducible.lean) and [`Ext6/SexticCertData.lean`](../../CompPoly/Fields/KoalaBear/Ext6/SexticCertData.lean)). +### A characteristic-2 consumer + +[`Binary/BF64/Ext3.lean`](../../CompPoly/Fields/Binary/BF64/Ext3.lean) adjoins a root of +`y^3 + y + 1` over `GF(2^64)`, giving `GF(2^192)`. It is the framework's first and so far only +characteristic-2 instance. The selected modulus has a nonzero linear coefficient, so it uses +the *general* `ExtensionParams` path. Keeping that modulus fixes the intended polynomial-basis +presentation; `Ext ext3Params` has a coefficient vector of type `Vector BF64 3`. + +Two things about it are worth knowing when reading the rest of this page: + +- **The base field is not `ZMod p`.** `BF64` is a nominal carrier storing a `BitVec 64`, with carry-less + multiplication, so the performance figures below — all measured over `ZMod` — do not + characterise it. +- **Irreducibility needs no certificate.** A cubic is irreducible exactly when it has no root, + and a root of `y^3 + y + 1` would satisfy `a^7 = 1`; `gcd(7, 2^64 - 1) = 1` forces `a = 1`, + which is not a root. That is a short direct argument, not the Rabin pipeline. The *base* + modulus of `BF64` — the degree-64 one over `GF(2)` — does use the certificate pipeline. + [`Ext6/GaloisField.lean`](../../CompPoly/Fields/KoalaBear/Ext6/GaloisField.lean) is a separate opt-in module identifying `Ext6` with Mathlib's abstract `GaloisField KoalaBear.fieldSize 6`, so the computable arithmetic here can serve developments phrased over that (ArkLib's `KoalaSextic`, @@ -84,15 +116,15 @@ modulus, so the identification follows from `card_ext6` alone and is independent ## What The Interface Provides -Concretely, for `P : ExtensionParams F`: +Concretely, for `P : ExtensionParams F` over a field `F`: | Surface | Declarations | |---|---| -| Ring / field | `CommRing (Ext P)`, `Field (Ext P)` (the latter given `[Fact (Irreducible P.poly)]`) | +| Ring / field | `CommRing (Ext P)`; `Field (Ext P)` additionally requires `[Finite F]`, `[Fact (Nat.card F = P.q)]`, and `[Fact (Irreducible P.poly)]` | | Base field | `Ext.ofBase : F → Ext P`, `Ext.ofBaseRingHom`, `Algebra F (Ext P)` (hence `Module F (Ext P)` via `Algebra.toModule`) | | Adjoined root | `Ext.gen`, `Ext.gen_pow_d : gen ^ d = monomialMod d`, `Ext.aeval_gen_poly : aeval gen P.poly = 0`; for a binomial, `Ext.gen_pow_d_binomial : gen ^ d = ofBase W` | | Specification | `Ext.toQuot`, `Ext.ringEquivQuot : Ext P ≃+* AdjoinRoot P.poly` | -| Cardinality | `Fintype (Ext P)`, `Ext.card_ext : Fintype.card (Ext P) = q ^ d` | +| Cardinality | `Finite (Ext P)`, optional `Fintype (Ext P)`, `Ext.nat_card_ext : Nat.card (Ext P) = q ^ d` and enumeration compatibility `Ext.card_ext` | | Coefficients | `Ext.coeff`, `Ext.ofFn`, `Ext.equivFn : Ext P ≃ (Fin d → F)` | With the `Algebra` instance in place, ordinary Mathlib machinery — `aeval`, scalar towers, @@ -144,6 +176,15 @@ over `d.primeFactors`; the packaged forms are: | `6` | `irreducible_of_rabin_degree_six` | plus `IsCoprime f (X^(q^3) - X)` and `IsCoprime f (X^(q^2) - X)` | | two prime factors | `irreducible_of_rabin_two_prime_factors` | as above, `Nat.primeFactors d = {ℓ₁, ℓ₂}` supplied by the caller | +Each of the first two also has an `_of_card` form (`irreducible_of_rabin_prime_degree_of_card`, +`irreducible_of_rabin_degree_six_of_card`) taking the field size as a numeral `q` with +`hcard : Fintype.card F = q`, supplied as `ZMod.card _`. Concrete extensions use those: their +generated certificates are already stated in terms of `fieldSize`, so the conditions apply +directly instead of needing a `rw [hcard]` cast per condition. This mirrors +`irreducible_X_pow_four_sub_C_of_card` on the binomial side. The two forms are inter-derivable — +instantiating at `q := Fintype.card F` with `rfl` recovers the plain one — and +`tests/CompPolyTests/Data/Polynomial/RabinCertificate.lean` pins that round trip. + Concretely, over KoalaBear `(X^3 + X + 4)(X^3 + X - 4)` divides `X^(p^6) - X` and is coprime to `X^p - X`, so it satisfies the prime-degree conditions verbatim while being visibly reducible; only the `q^3` check rejects it. `Nat.primeFactors 6 = {2, 3}` cannot be closed by `decide` @@ -194,7 +235,10 @@ So prefer, in order: a cyclotomic `Φₙ` when one has the right degree; then a 1. Pick `W`. For `d = 4` over `q ≡ 1 mod 4`, any non-square works; prefer the smallest, so that multiplying by `W` is cheap. -2. Write the `BinomialParams`, supplying `card_eq := ZMod.card _`. +2. Write the raw `BinomialParams`, including literal `q`, and separately provide + `instance : Fact (Nat.card Field = params.q)`. For `ZMod`, rewrite + `Nat.card_eq_fintype_card` and use `ZMod.card _`. The cardinality certificate is forwarded + to `params.toExtensionParams`. 3. Prove irreducibility with `irreducible_X_pow_four_sub_C_of_card`. The two exponentiation goals need the type presented as `ZMod `, because `reduce_mod_char` reads the modulus syntactically and `fieldSize` is an expression like `2 ^ 31 - 2 ^ 24 + 1`. Use a @@ -210,13 +254,16 @@ That is about 60 lines. 1. Pick a monic irreducible `f` (confirm with `scripts/gen_rabin_certificate.py`, which exits nonzero if `f` is reducible). 2. Generate the certificate module: - `python3 scripts/gen_rabin_certificate.py --p

--f --lean --namespace `. + `python3 scripts/gen_rabin_certificate.py --p

--f= --lean --namespace `. 3. Write the irreducibility wrapper: `toPoly p fL = f`, `natDegree`, `f ≠ 0`, then the - chain/Bézout `rfl` checks and the assembly through `irreducible_of_rabin_prime_degree` - (prime `d`, see `KoalaBear/Ext5/QuinticIrreducible.lean`) or - `irreducible_of_rabin_degree_six` / `irreducible_of_rabin_two_prime_factors` (composite `d`, - see `KoalaBear/Ext6/SexticIrreducible.lean`). At composite `d` there is one chain plus Bézout - block per prime factor, named `copSteps`/`copRp`/… for `m = d / ℓ`. + chain/Bézout `rfl` checks and the assembly through + `irreducible_of_rabin_prime_degree_of_card` (prime `d`, see + `KoalaBear/Ext5/QuinticIrreducible.lean`) or `irreducible_of_rabin_degree_six_of_card` + (composite `d`, see `KoalaBear/Ext6/SexticIrreducible.lean`), passing + `hcard : Fintype.card Field = fieldSize := ZMod.card _`. At a composite `d` with no `_of_card` + form yet, use `irreducible_of_rabin_two_prime_factors` and cast each condition with + `rw [hcard]`. At composite `d` there is one chain plus Bézout block per prime factor, named + `copSteps`/`copRp`/… for `m = d / ℓ`. 4. Write the `ExtensionParams` (lower coefficients of `f`, little-endian) and prove `...Params.poly = f`; register the `Fact` and define the `abbrev` — see `KoalaBear/Ext5.lean` (supporting cert/proof files under `KoalaBear/Ext5/`). @@ -225,7 +272,9 @@ That is the generated data plus about 200 hand-written lines. ## Representation And Computability -`Ext P` is `Vector F P.d`: dense, little-endian, length exactly `d`. There is no degree-bound +`Ext P` retains `P` in a structure with a `Vector F P.d` coefficient field: dense, +little-endian, length exactly `d`. The inverse maps `Ext.coeffs` and `Ext.ofVector` +provide explicit access to this representation. There is no degree-bound invariant to maintain — the bound is *structural*, a consequence of the length, not a proposition carried alongside the data. As a result this subtree is **independent of the `CPolynomial` stack**: nothing under `CompPoly/Fields/Extension/` imports `CompPoly/Univariate/`. @@ -239,14 +288,29 @@ The one place a degree bound is needed on the *polynomial* side — showing that [`Univariate/ToPoly/Degree.lean`](../../CompPoly/Univariate/ToPoly/Degree.lean)), it exists but is **not** wired to this framework; connecting them would be new work. -`ExtensionParams` carries `d`, the modulus's lower coefficients, and the base-field cardinality -`q` as a *type index*, so two different extensions of the same base field are different types -whose instances cannot be confused. `q` is data rather than `Fintype.card F` because Fermat +`ExtensionParams` carries `d`, the modulus's lower coefficients, and a proposed base cardinality +`q` as a *type index*. With fixed operations on the coefficient type, different parameter +values give different presentation types. The raw carrier does not choose between multiple +ring dictionaries on the same coefficient type. `q` is data rather than `Fintype.card F` because Fermat inversion evaluates the exponent at runtime, and `Fintype.card (ZMod p)` would enumerate all of `Fin p`. `BinomialParams` is the ergonomic front-end for `X^d - W`, mapped in by `BinomialParams.toExtensionParams` (lower coefficients `(-W, 0, …, 0)`), with `toExtensionParams_poly` identifying the two spellings of the defining polynomial. +Cardinality correctness is separate from the raw parameters: field laws require `Finite F` +and `Fact (Nat.card F = P.q)`, both propositions. `Ext.inv` always evaluates the stored +exponent, even for an incorrect `q`; without the certificates it is only an inverse candidate. +For example, `q = 0` gives `0⁻¹ = 1`. The cardinality and field proofs create enumerations +locally when needed; no enumeration dictionary is passed to raw operations or the field +instance. `Ext.nat_card_ext` and the proof-only `Finite (Ext P)` instance support subsequent +certified extensions without requiring an executable enumeration. + +The raw and certificate boundaries are exercised in +[`RawArithmetic.lean`](../../tests/CompPolyTests/Fields/Extension/RawArithmetic.lean) and +[`Certificates.lean`](../../tests/CompPolyTests/Fields/Extension/Certificates.lean), including +an infinite coefficient ring, incorrect cardinality, and an infinite field with irreducible +modulus and true cardinality zero. + **The instances are assembled field-by-field, not by `Function.Injective.commRing` / `.field`.** Those transports take `toQuot` as data, which forces the resulting instance `noncomputable`. That is not merely cosmetic: `Monoid.toNatPow` then outranks `Ext.instPow`, and @@ -319,7 +383,7 @@ The remaining causes, in order of size: 2. **The base field is `ZMod p`**, i.e. boxed `Nat` arithmetic. Instantiating over `KoalaBear.Fast.Field` (`UInt32` Montgomery, [`Montgomery/Native32Field.lean`](../../CompPoly/Fields/Montgomery/Native32Field.lean)) - needs only `Fintype` for that carrier plus irreducibility transported along + needs `Finite` and a certified `Nat.card` for that carrier, plus irreducibility transported along `Montgomery.Native32.ringEquiv` with `Polynomial.mapEquiv`. No change to the framework. 3. **Inversion is Fermat** (`x ^ (q^d - 2)`), about `d · log q` extension multiplications — the ~140x ratio to `mul` above, and it is the reason the `#guard` regressions dominate the test diff --git a/docs/wiki/generated-files.md b/docs/wiki/generated-files.md index 0655063f..6190ade0 100644 --- a/docs/wiki/generated-files.md +++ b/docs/wiki/generated-files.md @@ -7,8 +7,10 @@ This page records which paths are source of truth and which are derived outputs. | Path | Status | How it is maintained | |---|---|---| | `CompPoly.lean` | Generated and committed | Regenerate with `./scripts/update-lib.sh` after adding, renaming, or deleting `CompPoly/**/*.lean` files. Emitted in module form: `module`, blank line, one `public import` per file. | -| `CompPoly/Fields/*/Ext*/`*`CertData.lean` | Generated and committed | Rabin irreducibility certificate data for non-binomial extension moduli. Regenerate with `scripts/gen_rabin_certificate.py --p

--f --lean --namespace `; the exact command is recorded in each file's docstring. Do not hand-edit. Nothing in them is trusted — the kernel re-checks every step through `CompPoly.RabinCert.runChain`. | -| `bench/report-*.md`, `bench/results-*.jsonl`, `bench/evaluation-bench-*` | Generated, not source | Produced by `lake exe CompPolyBench`; keep reports as local or CI artifacts. All three patterns are ignored — the first two by `bench/.gitignore`, `evaluation-bench-*` by the root `.gitignore` — so a benchmark run leaves the working tree clean. | +| `CompPoly/Fields/*/Ext*/`*`CertData.lean` | Generated and committed | Rabin irreducibility certificate data for non-binomial extension moduli. Regenerate with `scripts/gen_rabin_certificate.py --p

--f= --lean --namespace `; the exact command is recorded in each file's docstring. Do not hand-edit. Nothing in them is trusted — the kernel re-checks every step through `CompPoly.RabinCert.runChain`. | +| `CompPoly/Fields/Binary/BF64/BaseCertificate.lean` | Generated and committed | Same generator and same contract as the `CertData.lean` files above, for the degree-64 `GF(2)` base modulus of `BF64` rather than an `Ext*/` extension modulus — hence the different path and name. Regenerate with the `--p 2` command recorded in its docstring. Do not hand-edit; the kernel re-checks every step through `CompPoly.RabinCert.runChain`. | +| `bench/out/` | Generated, not source | Produced by `lake exe CompPolyBench`; keep reports as local or CI artifacts. The whole directory is ignored by `bench/.gitignore`, so a benchmark run leaves the working tree clean. | +| `scripts/axiom_baseline.json` | Generated and committed | Kernel-level axiom/`sorry` regression baseline. Regenerate with `lake exe axiomsweep --update-baseline` after a built `lake build`; commit the diff in the same PR that intentionally adds or removes baselineable taint. Checked by the enforcing axiom-sweep CI step; native-compiler trust cannot be baselined. | | `CLAUDE.md` | Compatibility symlink | Must remain a symlink to `AGENTS.md`; do not replace it with a separate copy. | | `.lake/` | Derived, not source | Local dependency cache and build output produced by Lake. Do not edit files here by hand. | | `.lake/build/` | Derived, not source | Build artifacts from `lake build` and `lake test`. Safe to delete and regenerate. | @@ -30,29 +32,39 @@ the script runs. ## Rabin Certificate Data -`CompPoly/Fields/KoalaBear/Ext5/QuinticCertData.lean` and -`CompPoly/Fields/KoalaBear/Ext6/SexticCertData.lean` are emitted verbatim by +`CompPoly/Fields/KoalaBear/Ext5/QuinticCertData.lean`, +`CompPoly/Fields/KoalaBear/Ext6/SexticCertData.lean`, and +`CompPoly/Fields/Binary/BF64/BaseCertificate.lean` are emitted verbatim by [`../../scripts/gen_rabin_certificate.py`](../../scripts/gen_rabin_certificate.py). The generator writes a complete, compilable module — header, docstring with the regenerating command, and the step lists — so the workflow after changing a modulus is: ```bash -python3 scripts/gen_rabin_certificate.py --p --f \ +python3 scripts/gen_rabin_certificate.py --p --f= \ --lean --namespace .Cert ./scripts/update-lib.sh ``` +Attach the coefficients with `--f=`: a modulus with a negative leading coefficient starts with +`-`, which the separated `--f ` form parses as an option name. + The generator's exit code is its verdict: non-zero means the polynomial is reducible. Run `python3 scripts/gen_rabin_certificate.py --self-test` to check the generator itself against known-answer cases before trusting a new certificate. +The generator is not restricted to odd characteristic or to extension moduli: +`Binary/BF64/BaseCertificate.lean` is the `--p 2` case, certifying the degree-64 modulus that +defines `BF64` itself. It lives beside its field rather than under an `Ext*/` directory, but it +is the same output under the same contract. + Regenerating an unchanged modulus is byte-identical, so these files are safe to re-emit as a consistency check. ## Derived Build State - `.lake/` and `.lake/build/` are generated by Lake and CI. -- `bench/results-*.jsonl` and `bench/report-*.md` are generated benchmark outputs. +- `bench/out/results-*.jsonl`, `bench/out/report-*.md` and + `bench/out/manifest-*.json` are generated benchmark outputs. - Build-timing reports and timing-data artifacts produced by [`../../scripts/build_timing_report.sh`](../../scripts/build_timing_report.sh) are CI artifacts, not committed repo state. diff --git a/docs/wiki/quickstart.md b/docs/wiki/quickstart.md index fbbee6f1..73e0ad54 100644 --- a/docs/wiki/quickstart.md +++ b/docs/wiki/quickstart.md @@ -37,6 +37,36 @@ lake build lake test ``` +### Native field arithmetic or startup changes + +Build the dedicated native smoke target and run it with the resource limits documented in +[`../../tests/README.md`](../../tests/README.md#native-startup-and-field-arithmetic). The checks +exercise linked module initialization and actual BF64/Ext3 arithmetic; `lake test` alone does not +exercise that startup path. + +### Filling a `sorry`, or work that must stay axiom-clean + +```bash +lake build +lake exe axiomsweep --check +``` + +`axiomsweep` is kernel-level axiom/`sorry` accounting for every reportable +declaration owned by a `CompPoly.*` module, diffed against the committed baseline +`scripts/axiom_baseline.json`. It sweeps the `CompPoly` library as imported by the +umbrella (`tests/` and `bench/` are outside it), and inherits the blind spots of any +environment walk (structure-field defaults and `example`s never enter the +environment) — see the module docstring in `scripts/AxiomSweep.lean`. It fails only on *new* +`sorryAx` or non-standard-axiom taint, so pre-existing gaps stay allowed. After +intentionally adding or closing a `sorry`, refresh and commit the baseline: + +```bash +lake exe axiomsweep --update-baseline +``` + +CI runs the same check as an enforcing gate (see `lean_action_ci.yml`). Native-compiler +trust is never baselineable. + ### Added, renamed, or deleted files under `CompPoly/` ```bash @@ -73,18 +103,28 @@ lake build CompPolyBench lake exe CompPolyBench --medium ``` -CI runs a curated subset rather than the full suite, so a new benchmark group must -be added to `BENCH_CI_GROUPS` in -[`../../.github/workflows/lean_action_ci.yml`](../../.github/workflows/lean_action_ci.yml) -to be covered there. See [`../../bench/README.md`](../../bench/README.md). +CI gates on benchmark *correctness*, not timings: + +```bash +lake exe CompPolyBench --medium --validate-only +``` + +Both CI tracks run a curated subset rather than the full suite, so a new +benchmark group must be added to `bench/ci-groups.txt` to be covered. Timings +come from the on-demand Benchmarks workflow. See +[`../../bench/README.md`](../../bench/README.md) and +[`benchmarking.md`](benchmarking.md). ## CI Mapping - [`../../.github/workflows/lean_action_ci.yml`](../../.github/workflows/lean_action_ci.yml) runs a **warm** (incremental) `lake build` by default — reusing cached Lake - oleans so only dirty modules rebuild — then `lake test`, and posts a - build-timing report. It also builds and runs `CompPolyBench --medium` over the curated - `BENCH_CI_GROUPS` selection, then uploads benchmark reports as CI artifacts. + oleans so only dirty modules rebuild — then `lake test`, a linked native field smoke + test under the documented resource bounds, and the axiom sweep as an enforcing gate, + and posts a build-timing report. It also builds `CompPolyBench` and runs it in + `--validate-only` mode over the curated group set in `bench/ci-groups.txt`, which checks + that each group's implementations agree without timing ordinary workloads; harness checks + may still record samples. It uploads the digests as an artifact. A full cold rebuild (`rm -rf .lake/build && lake build`) runs automatically when `lean-toolchain` or `lake-manifest.json` differs from the comparison base (PR base, previous push tip, or merge-base with `main` on manual dispatch). @@ -103,8 +143,15 @@ to be covered there. See [`../../bench/README.md`](../../bench/README.md). checks the `CLAUDE.md` symlink, local markdown links, and backticked file paths in the docs. -Four further workflows exist that are not part of the pass/fail gate: +Five further workflows exist that are not part of the pass/fail gate: +- [`../../.github/workflows/benchmarks.yml`](../../.github/workflows/benchmarks.yml) + produces benchmark **timings** **on demand only** — via **Actions → Benchmarks → + Run workflow**, a `/bench` comment from a repo member, or automatically on a PR + touching `bench/**`. Timings are kept out of the gate because `ubuntu-latest` is + a shared 2-vCPU VM whose wall-clock is not worth gating on; benchmark + *correctness* is gated by `lean_action_ci.yml` instead. It restores the build + caches but never saves them. - [`../../.github/workflows/summary.yml`](../../.github/workflows/summary.yml) posts a PR summary on open and on every new commit. It runs under `pull_request_target` and never builds or executes PR code — it reads the diff @@ -129,6 +176,7 @@ Use the direct scripts when debugging a specific failure: python3 ./scripts/check-docs-integrity.py lake test lake build CompPolyBench +lake exe axiomsweep --check ``` For more detail on the helper scripts, see diff --git a/docs/wiki/repo-map.md b/docs/wiki/repo-map.md index 8dfb8045..fc274c3c 100644 --- a/docs/wiki/repo-map.md +++ b/docs/wiki/repo-map.md @@ -18,7 +18,7 @@ CompPoly/ Multilinear/ multilinear coefficient and evaluation representations Bivariate/ specialized `CPolynomial (CPolynomial R)` layer GuruswamiSudan/ list decoder: interpolation and root-finding backends - LinearAlgebra/ dense matrices and polynomial matrices with row reduction + LinearAlgebra/ matrix algorithms and tensor basis theory Fields/ concrete fields plus binary-field and additive-NTT stack tests/ regression modules under `CompPolyTests` bench/ benchmark executable, runner docs, and local reports @@ -71,6 +71,12 @@ scripts/ repo utilities and validation helpers `CompPoly/LinearAlgebra/`. - Moving a reusable support lemma that should not live next to one specific feature: start in `CompPoly/Data/` or `CompPoly/ToMathlib/`. +- Composing chosen finite coordinates along a natural-number-indexed algebra tower: + use `CompPoly/Data/RingTheory/AlgebraTower/Coordinates.lean`. Its executable packing and + coordinate vectors correspond to the basis in + `CompPoly/Data/RingTheory/AlgebraTower/Basis.lean`; successor coordinate counts may vary. + Both leaves support arbitrary comparable endpoints, with constant-count coordinates also + presented on a `Fin` type whose size is a power of the successor count. - Adding regression coverage: start in `tests/` and mirror the source namespace when possible. - Updating benchmark coverage or reports: start in `bench/`. diff --git a/lake-manifest.json b/lake-manifest.json index ffbf6655..a5ba6c8d 100644 --- a/lake-manifest.json +++ b/lake-manifest.json @@ -5,10 +5,10 @@ "type": "git", "subDir": null, "scope": "leanprover-community", - "rev": "db584cd6d46c92f209a44c0f1c829460d327499d", + "rev": "0df444a360eaa60ab8c11dca51a86af692955474", "name": "mathlib", "manifestFile": "lake-manifest.json", - "inputRev": "v4.33.0", + "inputRev": "v4.33.1", "inherited": false, "configFile": "lakefile.lean"}, {"url": "https://github.com/leanprover-community/plausible", diff --git a/lakefile.lean b/lakefile.lean index 59c427fe..284fdcc4 100644 --- a/lakefile.lean +++ b/lakefile.lean @@ -21,7 +21,7 @@ package CompPoly where -- prioritize it when resolving toolchains for downstream projects. fixedToolchain := true -require "leanprover-community" / mathlib @ git "v4.33.0" +require "leanprover-community" / mathlib @ git "v4.33.1" @[default_target] lean_lib CompPoly where @@ -35,9 +35,21 @@ lean_lib CompPoly where lean_lib CompPolyTests where srcDir := "tests" +/-- Linked field-startup and arithmetic checks; see tests/README.md for bounded execution. -/ +lean_exe CompPolyNativeSmoke where + srcDir := "tests" + lean_lib CompPolyBenchLib where srcDir := "bench" globs := #[Glob.submodules `CompPolyBench] lean_exe CompPolyBench where srcDir := "bench" + +/-- Kernel-level axiom / `sorry` accounting with a committed regression baseline +(`scripts/axiom_baseline.json`). Runtime-imports the built CompPoly oleans, so run it +after `lake build`. See `scripts/AxiomSweep.lean`. -/ +lean_exe axiomsweep where + srcDir := "scripts" + root := `AxiomSweep + supportInterpreter := true diff --git a/lean-toolchain b/lean-toolchain index 025e5954..a8afa7d1 100644 --- a/lean-toolchain +++ b/lean-toolchain @@ -1 +1 @@ -leanprover/lean4:v4.33.0 +leanprover/lean4:v4.33.1 diff --git a/scripts/AxiomSweep.lean b/scripts/AxiomSweep.lean new file mode 100644 index 00000000..30a51199 --- /dev/null +++ b/scripts/AxiomSweep.lean @@ -0,0 +1,358 @@ +/- +Copyright (c) 2026 CompPoly Contributors. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Alexander Hicks +-/ +import Lean + +/-! +# Axiom sweep: whole-library kernel-level axiom and `sorry` accounting + +Walks the compiled environment (the same data the kernel checked) and computes, for every +declaration in `CompPoly.*` modules, the set of axioms its statement and proof ultimately +depend on — the same information as `#print axioms`, for the whole library at once. + +Because this reads elaborated `.olean` data rather than source text, it sees exactly what +the kernel accepted: private declarations and instances are reported, compiler-generated +auxiliaries are traversed (their taint surfaces on the parent declaration), and no +source-level heuristics are involved. The sweep covers what the root modules transitively +import — pair it with the repo's import-completeness gate so every +source file is actually in scope; an unimported file is invisible to any kernel-level +census. + +Known blind spots, shared with `#print axioms` (all environment-walking tools): +* structure-field **default values** and autoparams (`:= by sorry`) are re-elaborated at + each use site and attach to no swept constant of the defining module; +* `example`s never enter the environment; +* files not transitively imported by the swept roots are invisible (pair with the repo's + import-completeness gate). +A source-level `sorry` grep is the complementary check for the first two. + +Modes (run after `lake build`): + +``` +lake exe axiomsweep # summary only +lake exe axiomsweep --out report.json # also write the full per-declaration report +lake exe axiomsweep --check # gate against scripts/axiom_baseline.json +lake exe axiomsweep --update-baseline # rewrite the baseline from the current build +``` + +The committed baseline (`scripts/axiom_baseline.json`) records the currently-known +`sorryAx`-tainted declarations and any declarations depending on non-standard axioms +(anything beyond `propext`, `Classical.choice`, `Quot.sound`). Native trust is never +baselineable: bare `Lean.ofReduceBool` / `Lean.trustCompiler` dependencies and generated +`…._native..ax_*` axioms both make `--check` fail and prevent +`--update-baseline`. Other native axiom names are normalized under their owning +declaration so diagnostics remain stable across rebuilds. `--check` fails exactly when +a declaration is tainted that the baseline does not cover. When gaps are closed, +`--check` reports them and stays green; run `--update-baseline` to shrink the file in the +same PR. +-/ + +open Lean + +namespace AxiomSweep + +/-- Root modules swept when no `--root` is given. -/ +def defaultRoots : Array Name := #[`CompPoly] + +/-- Axioms that carry no extra trust assumptions beyond Lean's standard foundation. -/ +def standardAxioms : List Name := [``propext, ``Classical.choice, ``Quot.sound] + +/-- Axioms that may never be baselined: bare or generated native-compiler trust. +A baseline edit cannot green these — remove the dependency instead. (Zero hits today; +this floor keeps the baseline from ever becoming a second, laxer policy.) -/ +def neverAllowlistable (a : String) : Bool := + a == "Lean.ofReduceBool" || a == "Lean.trustCompiler" || + (a.splitOn "._native.").length != 1 + +/-- Phase 1: DFS. Compute, for every constant reachable from the work list, an +under-approximation of the set of axioms it transitively depends on, memoised across +roots via `memo`. Also records the finalisation order — a topological order of the +dependency graph except inside mutual-inductive cycles. + +`gray` marks constants whose dependencies are still being expanded. Back-edges (cycles, +which the kernel only permits inside mutual inductive families) contribute nothing in +this phase; `repair` below propagates to the true fixpoint. An axiom contributes itself +plus anything reachable through its *type* (matching Lean's own `CollectAxioms`). -/ +partial def collect (env : Environment) (stack : List Name) (gray : Std.HashSet Name) + (memo : Std.HashMap Name (Array Name)) (order : Array Name) : + Std.HashMap Name (Array Name) × Array Name := + match stack with + | [] => (memo, order) + | n :: rest => + if memo.contains n then + collect env rest gray memo order + else match env.find? n with + | none => collect env rest gray (memo.insert n #[]) order + | some ci => + let deps := ci.getUsedConstantsAsSet.toList + if gray.contains n then + let seed : Array Name := if ci matches .axiomInfo _ then #[n] else #[] + let axs := deps.foldl (init := seed) fun acc d => + match memo[d]? with + | some as => as.foldl (init := acc) fun acc a => + if acc.contains a then acc else acc.push a + | none => acc + collect env rest gray (memo.insert n axs) (order.push n) + else + let pending := deps.filter fun d => !memo.contains d && !gray.contains d + collect env (pending ++ stack) (gray.insert n) memo order + +/-- Phase 2: propagate to fixpoint. The DFS under-approximates inside mutual-inductive +cycles (a member's taint may not reach its siblings), and — because `memo` persists +across roots — anything finalised after reading such a member inherits the error. +Re-deriving every set in finalisation order until nothing changes computes the least +fixpoint of the closure equations: the true kernel-level axiom dependency set. This is +strictly more accurate than `#print axioms`, whose `CollectAxioms` has the same +mutual-family blind spot this phase repairs. Sets grow monotonically and are bounded, +so termination is immediate; in practice one or two passes suffice. -/ +partial def repair (env : Environment) (order : Array Name) + (memo : Std.HashMap Name (Array Name)) : Std.HashMap Name (Array Name) := + let (memo', changed) := order.foldl (init := (memo, false)) fun (memo, changed) n => + match env.find? n with + | none => (memo, changed) + | some ci => + let deps := ci.getUsedConstantsAsSet.toList + let seed : Array Name := if ci matches .axiomInfo _ then #[n] else #[] + let axs := deps.foldl (init := seed) fun acc d => + match memo[d]? with + | some as => as.foldl (init := acc) fun acc a => + if acc.contains a then acc else acc.push a + | none => acc + let old := (memo[n]?.getD #[]).size + if axs.size == old then (memo, changed) + else (memo.insert n axs, true) + if changed then repair env order memo' else memo' + +/-- One row of the per-declaration report. -/ +structure Entry where + name : String + module : String + kind : String + line : Option Nat + axioms : Array String + deriving ToJson + +/-- A declaration depending on axioms beyond the standard foundation (and `sorryAx`, +which is tracked separately). -/ +structure NonstandardEntry where + name : String + axioms : Array String + deriving FromJson, ToJson + +/-- The committed regression baseline. -/ +structure Baseline where + «sorry» : Array String + nonstandard : Array NonstandardEntry + deriving FromJson, ToJson + +/-- Collapse the volatile counter suffix of native trust axioms +(`Foo._native.native_decide.ax_1_1` → `Foo._native.native_decide`), so baselines key by +owning declaration rather than a rebuild-volatile counter. -/ +def normalizeAxiomName (s : String) : String := + match s.splitOn "._native." with + | [owner, tail] => + match tail.splitOn "." with + | tactic :: _ => owner ++ "._native." ++ tactic + | _ => s + | _ => s + +/-- Sort and deduplicate (normalisation can identify adjacent names). -/ +def dedupSort (a : Array String) : Array String := + (a.qsort (· < ·)).foldl (init := #[]) fun acc x => + if acc.back? == some x then acc else acc.push x + +def kindOf : ConstantInfo → String + | .axiomInfo _ => "axiom" + | .defnInfo _ => "def" + | .thmInfo _ => "theorem" + | .opaqueInfo _ => "opaque" + | .quotInfo _ => "quot" + | .inductInfo _ => "inductive" + | .ctorInfo _ => "constructor" + | .recInfo _ => "recursor" + +/-- Whether to report a constant: skip compiler-internal auxiliaries (`_proof_*`, +`match_*`, numbered equation lemmas, …), whose axiom footprint is inherited by their +parent declaration, but keep `private` declarations (checked under their user-facing +name, since the `_private` mangling would otherwise look internal). On-demand aux +lemmas with symbolic names (`.eq_def`, `.congr_simp`) are reported. -/ +def isReportable (n : Name) : Bool := + !n.hasMacroScopes && !((privateToUserName? n).getD n).isInternalDetail + +/-- Enumerate the reportable declarations of every module under one of `roots` and +compute their axiom closures. -/ +def buildEntries (roots : Array Name) : CoreM (Array Entry × Nat) := do + let env ← getEnv + let mut targets : Array (Name × Name) := #[] + let mut seen : Std.HashSet Name := {} + let mut moduleCount := 0 + for (mname, mdata) in env.header.moduleNames.zip env.header.moduleData do + if roots.any (·.isPrefixOf mname) then + moduleCount := moduleCount + 1 + for c in mdata.constNames do + -- A realised constant (e.g. `.congr_simp`) can appear in several modules' + -- `constNames`; report it once, under the first module that carries it. + if isReportable c && !seen.contains c then + seen := seen.insert c + targets := targets.push (c, mname) + let (memo0, order) := + targets.foldl (init := (({} : Std.HashMap Name (Array Name)), (#[] : Array Name))) + fun (memo, order) (c, _) => collect env [c] {} memo order + let memo := repair env order memo0 + let mut entries : Array Entry := #[] + for (c, mname) in targets do + let some ci := env.find? c | continue + let line := (← findDeclarationRanges? c).map (·.range.pos.line) + entries := entries.push { + name := c.toString + module := mname.toString + kind := kindOf ci + line := line + axioms := dedupSort ((memo[c]?.getD #[]).map (normalizeAxiomName ·.toString)) } + return (entries.qsort (fun a b => a.name < b.name), moduleCount) + +def isStandard (a : String) : Bool := + standardAxioms.any (toString · == a) + +def sorryAxName : String := "sorryAx" + +/-- Non-standard axioms of an entry: everything beyond the standard foundation, with +`sorryAx` tracked separately. -/ +def nonstandardOf (e : Entry) : Array String := + e.axioms.filter fun a => !isStandard a && a != sorryAxName + +/-- Project the current build's taint sets into baseline form (deterministically +sorted, since `entries` is sorted by name). -/ +def currentBaseline (entries : Array Entry) : Baseline where + «sorry» := (entries.filter (·.axioms.contains sorryAxName)).map (·.name) + nonstandard := entries.filterMap fun e => + let bad := nonstandardOf e + if bad.isEmpty then none else some { name := e.name, axioms := bad } + +/-- Report native-compiler trust that repository policy forbids baselining. Returns +whether any forbidden dependency was found. -/ +def reportNeverAllowlistable (cur : Baseline) : IO Bool := do + let floor := cur.nonstandard.filter fun e => e.axioms.any neverAllowlistable + if floor.isEmpty then + return false + IO.eprintln s!"axiomsweep: {floor.size} declaration(s) depend on never-allowlistable \ + axioms (native-compiler trust) — remove the dependency instead:" + for e in floor do + IO.eprintln s!" {e.name} : {e.axioms.filter neverAllowlistable}" + return true + +/-- Compare the current taint sets against the committed baseline. Returns the exit +code: `1` iff there is a regression (new taint not covered by the baseline). -/ +def runCheck (cur : Baseline) (basePath : String) : IO UInt32 := do + if !(← System.FilePath.pathExists basePath) then + IO.eprintln s!"axiomsweep: baseline {basePath} not found; \ + create it with `lake exe axiomsweep --update-baseline`" + return 2 + let base ← match Json.parse (← IO.FS.readFile basePath) >>= fromJson? (α := Baseline) with + | .ok b => pure b + | .error e => + IO.eprintln s!"axiomsweep: cannot parse baseline {basePath}: {e}" + return 2 + let newSorry := cur.«sorry».filter (!base.«sorry».contains ·) + let fixedSorry := base.«sorry».filter (!cur.«sorry».contains ·) + let newNonstd := cur.nonstandard.filter fun e => + match base.nonstandard.find? (·.name == e.name) with + | none => true + | some b => e.axioms.any (!b.axioms.contains ·) + let fixedNonstd := base.nonstandard.filter fun b => + match cur.nonstandard.find? (·.name == b.name) with + | none => true + | some c => b.axioms.any (!c.axioms.contains ·) + let mut failed ← reportNeverAllowlistable cur + if !newSorry.isEmpty then + failed := true + IO.eprintln s!"axiomsweep: {newSorry.size} declaration(s) newly depend on sorryAx \ + (not in {basePath}):" + for n in newSorry do IO.eprintln s!" {n}" + if !newNonstd.isEmpty then + failed := true + IO.eprintln s!"axiomsweep: {newNonstd.size} declaration(s) newly depend on \ + non-standard axioms (not in {basePath}):" + for e in newNonstd do IO.eprintln s!" {e.name} : {e.axioms}" + if failed then + IO.eprintln s!"axiomsweep: if intentional (new tagged sorry), refresh the baseline \ + with `lake exe axiomsweep --update-baseline` and commit the diff." + return 1 + if !fixedSorry.isEmpty || !fixedNonstd.isEmpty then + IO.println s!"axiomsweep: good news — {fixedSorry.size + fixedNonstd.size} baseline \ + entr(y/ies) no longer tainted; run `lake exe axiomsweep --update-baseline` to shrink \ + the baseline:" + for n in fixedSorry do IO.println s!" {n}" + for e in fixedNonstd do IO.println s!" {e.name}" + IO.println "axiomsweep: check passed (no new axiom/sorry taint)." + return 0 + +structure Config where + roots : Array Name := #[] + out? : Option String := none + check : Bool := false + update : Bool := false + baseline : String := "scripts/axiom_baseline.json" + +def parseArgs : List String → Config → Except String Config + | [], cfg => .ok cfg + | "--check" :: rest, cfg => parseArgs rest { cfg with check := true } + | "--update-baseline" :: rest, cfg => parseArgs rest { cfg with update := true } + | "--out" :: path :: rest, cfg => parseArgs rest { cfg with out? := some path } + | "--baseline" :: path :: rest, cfg => parseArgs rest { cfg with baseline := path } + | "--root" :: mod :: rest, cfg => + parseArgs rest { cfg with roots := cfg.roots.push mod.toName } + | arg :: _, _ => .error s!"axiomsweep: unknown or incomplete argument: {arg}\n\ + usage: lake exe axiomsweep [--out FILE] [--check] [--update-baseline] \ + [--baseline FILE] [--root MOD]*\n (--check and --update-baseline are mutually exclusive)" + +end AxiomSweep + +open AxiomSweep in +unsafe def main (args : List String) : IO UInt32 := do + let cfg ← match parseArgs args {} with + | .ok cfg => pure cfg + | .error e => IO.eprintln e; return 2 + if cfg.check && cfg.update then + IO.eprintln "axiomsweep: --check and --update-baseline are mutually exclusive" + return 2 + let roots := if cfg.roots.isEmpty then defaultRoots else cfg.roots + initSearchPath (← findSysroot) + enableInitializersExecution + let env ← try + importModules (roots.map ({ module := · })) {} (trustLevel := 1024) + (loadExts := true) + catch e => + IO.eprintln s!"axiomsweep: cannot import root modules {roots}: {e.toString}\n\ + (roots must be importable modules — glob-based libs without an umbrella \ + module cannot be swept by library name)" + return (2 : UInt32) + let ((entries, moduleCount), _) ← (buildEntries roots).toIO + { fileName := "", fileMap := default } { env } + let cur := currentBaseline entries + let distinctNonstd := cur.nonstandard.foldl (init := (#[] : Array String)) fun acc e => + e.axioms.foldl (init := acc) fun acc a => if acc.contains a then acc else acc.push a + IO.println s!"axiomsweep: {entries.size} declarations across {moduleCount} modules \ + under {roots}" + IO.println s!" sorryAx-tainted: {cur.«sorry».size}" + IO.println s!" non-standard-axiom-tainted: {cur.nonstandard.size} \ + (axioms: {distinctNonstd})" + if let some out := cfg.out? then + let report := Json.mkObj [ + ("roots", toJson (roots.map (·.toString))), + ("declarationCount", toJson entries.size), + ("declarations", toJson entries)] + IO.FS.writeFile out (report.pretty ++ "\n") + IO.println s!"axiomsweep: wrote report to {out}" + if cfg.update then + if ← reportNeverAllowlistable cur then + IO.eprintln "axiomsweep: refusing to write a baseline containing native trust." + return 1 + IO.FS.writeFile cfg.baseline ((toJson cur).pretty ++ "\n") + IO.println s!"axiomsweep: wrote baseline to {cfg.baseline}" + return 0 + if cfg.check then + return (← runCheck cur cfg.baseline) + return 0 diff --git a/scripts/README.md b/scripts/README.md index 7bc495b7..0df75f27 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -10,9 +10,20 @@ This directory contains the main helper scripts for local validation and CI supp checks. - `python3 ./scripts/check-docs-integrity.py` - verify the `CLAUDE.md` symlink, local markdown links, and backticked source paths across the handbook. +- `lake exe axiomsweep --check` - kernel-level axiom/`sorry` regression gate against + `scripts/axiom_baseline.json` (run after `lake build`). ## Script Inventory +### `AxiomSweep.lean` (`lake exe axiomsweep`) + +Kernel-level axiom/`sorry` accounting for every reportable `CompPoly.*` declaration, +computed from the built `.olean` environment, with a committed regression baseline +(`axiom_baseline.json`). `--check` fails only on new taint; `--update-baseline` +refreshes the baseline; `--out FILE` writes a full per-declaration report. Bare and +generated native-compiler trust is rejected regardless of the baseline. See the module +docstring for modes and known blind spots. + ### `update-lib.sh` Regenerates [`../CompPoly.lean`](../CompPoly.lean) by scanning tracked diff --git a/scripts/axiom_baseline.json b/scripts/axiom_baseline.json new file mode 100644 index 00000000..ffa44b24 --- /dev/null +++ b/scripts/axiom_baseline.json @@ -0,0 +1 @@ +{"sorry": [], "nonstandard": []} diff --git a/scripts/gen_rabin_certificate.py b/scripts/gen_rabin_certificate.py index 90af865c..d903cb62 100755 --- a/scripts/gen_rabin_certificate.py +++ b/scripts/gen_rabin_certificate.py @@ -28,8 +28,10 @@ python3 scripts/gen_rabin_certificate.py --self-test """ from __future__ import annotations -import argparse, json, sys +import argparse, json, subprocess, sys +import textwrap +LINE_LIMIT = 98 # Lean style limit is 100, but we leave room for a trailing comma or bracket. def poly_trim(a: list[int]) -> list[int]: a = a[:] @@ -260,9 +262,29 @@ def self_test() -> int: return 1 if failures else 0 +def _wrap_coeffs(coeffs: list[int], indent: str, limit: int = 98) -> str: + """Render a coefficient list as a bracketed Lean literal, filling lines up to + `limit` columns and continuing on `indent`-prefixed lines. Long lists (high-degree + moduli, or small primes with many terms) do not fit on one or two lines.""" + body_lines: list[str] = [] + cur = indent + "[" + first = True + for c in coeffs: + tok = str(c) if first else ", " + str(c) + # +1 leaves room for the closing bracket or a trailing comma. + if not first and len(cur) + len(tok) + 1 > limit: + body_lines.append(cur + ",") + cur = indent + " " + str(c) + else: + cur += tok + first = False + body_lines.append(cur + "]") + return "\n".join(body_lines).lstrip() + + def steps_to_lean(steps) -> str: - """Render a step list as a Lean `List CompPoly.RabinCert.Step` literal, - wrapping each step across two lines to respect the 100-column style limit.""" + """Render a step list as a Lean `List CompPoly.RabinCert.Step` literal, wrapping + each step so that every emitted line respects the 100-column style limit.""" rows = [] for s in steps: mulx = "true" if s["op"] == "mulX" else "false" @@ -271,13 +293,109 @@ def steps_to_lean(steps) -> str: one_line = f" ⟨{mulx}, [{q}], [{r}]⟩" if len(one_line) <= 98: rows.append(one_line) - else: - rows.append(f" ⟨{mulx}, [{q}],\n [{r}]⟩") + continue + two_line = f" ⟨{mulx}, [{q}],\n [{r}]⟩" + if all(len(ln) <= 98 for ln in two_line.split("\n")): + rows.append(two_line) + continue + # Both halves still overflow: wrap each coefficient list over as many + # lines as it needs. + q_wrapped = _wrap_coeffs(s["q"], " ") + r_wrapped = _wrap_coeffs(s["r"], " ") + rows.append(f" ⟨{mulx},\n {q_wrapped},\n {r_wrapped}⟩") return "[\n" + ",\n".join(rows) + "]" -def poly_to_lean(l: list[int]) -> str: - return "[" + ", ".join(str(c) for c in l) + "]" +def _wrap_command(p: int, f_arg: str, ns: str, authors: str) -> list[str]: + """Emit the regeneration command as shell lines, none exceeding the style limit. + `--f=` keeps a negative leading coefficient from parsing as an option name. A long + value is split by closing the quote before each break and reopening it at column 0 + on the next line, so the backslash falls outside the quotes where it continues the + line and the shell rejoins the pieces into one argument.""" + lines = ["python3 scripts/gen_rabin_certificate.py --p %d \\" % p] + head = " --f='" + limit = LINE_LIMIT + chunks: list[str] = [] + cur = "" + for piece in f_arg.split(","): + tok = piece if not cur else "," + piece + # The first chunk carries `head`; later ones only a reopening quote. Every + # chunk may gain a trailing `,'\\` (3) or a closing `' \\` (3). + room = limit - (len(head) if not chunks else 1) - 3 + if cur and len(cur) + len(tok) > room: + chunks.append(cur) + cur = piece + else: + cur += tok + if cur: + chunks.append(cur) + if len(chunks) == 1: + lines.append(head + chunks[0] + "' \\") + else: + lines.append(head + chunks[0] + ",'\\") + for c in chunks[1:-1]: + lines.append("'" + c + ",'\\") + lines.append("'" + chunks[-1] + "' \\") + lines.append(" --lean --namespace %s \\" % ns) + lines.append(" --authors '%s'" % authors) + return lines + + +# (coeff string, note) — moduli whose emitted command must survive a shell round-trip. +# The long cases force `_wrap_command` to split the value across lines; the short ones +# guard the unsplit branch and the negative leading coefficient. +FORMAT_TESTS = [ + ("1,1,1", "short, unsplit"), + ("-1,0,1,0,0,1", "negative leading coefficient (Ext5)"), + ("1," + ",".join(["0"] * 63) + ",1", "degree-64 GF(2) modulus, splits"), + ("-1," + ",".join(["0"] * 200) + ",1", "long and negative, splits several times"), +] + + +def format_test() -> int: + """Check that the emitted regeneration command survives a shell round-trip. + + `_wrap_command` splits a long `--f` value across lines, and the shell must rejoin + the pieces into exactly the original token: a backslash misplaced inside the quotes + would leave a literal backslash and newline in the value, which `int()` then + rejects. Returns a process exit code.""" + failures = 0 + for fstr, note in FORMAT_TESTS: + lines = _wrap_command(2130706433, fstr, "NS.Cert", "Test Author") + # `--lean ` is a documentation placeholder; `<` would redirect. + script = [ln for ln in lines if "--lean" not in ln] + script[-1] = script[-1].rstrip("\\").rstrip() + script[0] = script[0].replace( + "python3 scripts/gen_rabin_certificate.py", 'printf "%s\\n"', 1) + proc = subprocess.run(["bash", "-c", "\n".join(script)], + capture_output=True, text=True) + argv = proc.stdout.split("\n") + got = next((a[len("--f="):] for a in argv if a.startswith("--f=")), None) + overlong = [len(ln) for ln in lines if len(ln) > LINE_LIMIT] + ok = proc.returncode == 0 and got == fstr and not overlong + if not ok: + failures += 1 + status = "ok " if ok else "FAIL" + detail = "" + if proc.returncode != 0: + detail = " shell rejected the command: " + proc.stderr.strip() + elif got != fstr: + detail = " --f round-tripped as %r" % got + elif overlong: + detail = " lines over %d columns: %s" % (LINE_LIMIT, overlong) + print(f"[{status}] --f={fstr[:32]}{'...' if len(fstr) > 32 else ''} " + f"({len(lines)} lines) {note}{detail}") + print(f"\n{len(FORMAT_TESTS) - failures}/{len(FORMAT_TESTS)} passed") + return 1 if failures else 0 + + +def poly_to_lean(l: list[int], prefix_len: int = 0, indent: str = " ") -> str: + """Render a coefficient list, wrapping if the rendered `def` line would overflow. + `prefix_len` is the width of the `def : List ℕ := ` text preceding it.""" + one_line = "[" + ", ".join(str(c) for c in l) + "]" + if prefix_len + len(one_line) <= LINE_LIMIT: + return one_line + return _wrap_coeffs(l, indent, limit=LINE_LIMIT - prefix_len if prefix_len else LINE_LIMIT) def main() -> int: @@ -290,11 +408,17 @@ def main() -> int: help="write Lean data definitions (steps + Bezout) here") ap.add_argument("--namespace", type=str, default="QuinticCert", help="namespace for the emitted Lean definitions") + ap.add_argument("--authors", type=str, default="Derek Sorensen", + help="value for the Authors line of the generated copyright header") ap.add_argument("--self-test", action="store_true", - help="check the generator against known-answer cases and exit") + help="check the certificate arithmetic and the emitted command, then exit") args = ap.parse_args() if args.self_test: - return self_test() + print("== certificate arithmetic ==") + arithmetic = self_test() + print("\n== emitted command formatting ==") + formatting = format_test() + return arithmetic or formatting p = args.p f = [c % p for c in map(int, args.f.split(","))] cert = build_certificate(p, f) @@ -339,16 +463,20 @@ def main() -> int: f"def {sname} : List Step := {steps_to_lean(c['steps'])}", "", f"/-- The residue `X^({exp}) mod f`. -/", - f"def {rname} : List ℕ := {poly_to_lean(c['rp'])}", + f"def {rname} : List ℕ := " + f"{poly_to_lean(c['rp'], len(rname) + 20)}", "", f"/-- `{wname} = (X^({exp}) mod f) - X`, the reduced form of `X^({exp}) - X`. -/", - f"def {wname} : List ℕ := {poly_to_lean(c['w'])}", + f"def {wname} : List ℕ := " + f"{poly_to_lean(c['w'], len(wname) + 20)}", "", f"/-- Bézout coefficient: `{uname}·f + {vname}·{wname} = 1`. -/", - f"def {uname} : List ℕ := {poly_to_lean(c['u'])}", + f"def {uname} : List ℕ := " + f"{poly_to_lean(c['u'], len(uname) + 20)}", "", f"/-- Bézout coefficient: `{uname}·f + {vname}·{wname} = 1`. -/", - f"def {vname} : List ℕ := {poly_to_lean(c['v'])}", + f"def {vname} : List ℕ := " + f"{poly_to_lean(c['v'], len(vname) + 20)}", "", ] if is_prime_degree: @@ -360,23 +488,23 @@ def main() -> int: f"def frobSteps : List Step := {steps_to_lean(coprimes[0]['steps'])}", "", "/-- The residue `X^p mod f`. -/", - f"def rp : List ℕ := {poly_to_lean(coprimes[0]['rp'])}", + f"def rp : List ℕ := {poly_to_lean(coprimes[0]['rp'], 22)}", "", "/-- `w = (X^p mod f) - X`, the reduced form of `X^p - X`. -/", - f"def w : List ℕ := {poly_to_lean(coprimes[0]['w'])}", + f"def w : List ℕ := {poly_to_lean(coprimes[0]['w'], 21)}", "", "/-- Bézout coefficient: `u·f + v·w = 1`. -/", - f"def u : List ℕ := {poly_to_lean(coprimes[0]['u'])}", + f"def u : List ℕ := {poly_to_lean(coprimes[0]['u'], 21)}", "", "/-- Bézout coefficient: `u·f + v·w = 1`. -/", - f"def v : List ℕ := {poly_to_lean(coprimes[0]['v'])}", + f"def v : List ℕ := {poly_to_lean(coprimes[0]['v'], 21)}", "", ] lines = [ "/-", "Copyright (c) 2026 CompPoly Contributors. All rights reserved.", "Released under Apache 2.0 license as described in the file LICENSE.", - "Authors: Derek Sorensen", + f"Authors: {args.authors}", "-/", "module", "", @@ -385,7 +513,8 @@ def main() -> int: "/-!", f"# Rabin certificate data for `p = {p}`", "", - f"The modulus `f` has little-endian coefficients `{f}`.", + *textwrap.wrap(f"The modulus `f` has little-endian coefficients `{f}`.", + width=98, break_long_words=False, break_on_hyphens=False), "", *([] if is_prime_degree else [ f"`d = {d}` is composite, so Rabin's coprimality condition needs one certificate " @@ -396,9 +525,15 @@ def main() -> int: "linear-factor case would admit a product of equal-degree factors.", "", ]), - f"GENERATED by `scripts/gen_rabin_certificate.py --p {p} --f {args.f!r}`.", - "Do not edit by hand; regenerate instead. Nothing here is trusted — the kernel", - "re-checks every step through `CompPoly.RabinCert.runChain`.", + "GENERATED. Do not edit by hand; regenerate with:", + "", + "```sh", + *_wrap_command(p, args.f, args.namespace, args.authors), + "```", + "", + "Nothing here is trusted — the kernel re-checks every step through", + "`CompPoly.RabinCert.runChain`, so incorrect data fails to compile rather than", + "producing a false theorem.", "-/", "", "@[expose] public section", diff --git a/tests/CompPolyNativeSmoke.lean b/tests/CompPolyNativeSmoke.lean new file mode 100644 index 00000000..9968ebab --- /dev/null +++ b/tests/CompPolyNativeSmoke.lean @@ -0,0 +1,57 @@ +/- +Copyright (c) 2026 CompPoly Contributors. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: CompPoly Contributors +-/ +module + +public import CompPoly.Fields.Binary.BF64.Ext3 + +/-! +# Native field startup and arithmetic checks + +This executable imports BF64 and its cubic extension, then checks their canonical arithmetic. +The test guide documents resource limits for native initialization and execution. +Unlike compile-time guards, this target exercises the linked executable's module initializers. +The extension product uses the reference vector from the existing BF64 regression tests. +These checks provide implementation evidence; they are not kernel proofs or benchmarks. +-/ + +public section + +namespace CompPolyTests.NativeSmoke + +/-- Construct the three extension coefficients from polynomial-basis words in ascending order. -/ +private def fromWords (c0 c1 c2 : BitVec 64) : BF64.Ext3 := + CompPoly.Extension.Ext.ofFn fun i => + BF64.ofBitVec (if i.val = 0 then c0 else if i.val = 1 then c1 else c2) + +/-- Fail the native executable when an arithmetic result differs from its expected value. -/ +private def check (label : String) (ok : Bool) : IO Unit := + unless ok do throw (IO.userError s!"Native field check failed: `{label}`") + +/-- Exercise multiplication and inversion through generic field data. -/ +private def inverseProduct {F : Type*} [Field F] (x : F) : F := x * x⁻¹ + +/-- Check reduction, a reference extension product, and total inversion in both fields. -/ +def run : IO Unit := do + check "BF64 reduction" + (((BF64.ofBitVec (0x8000000000000000#64)) * BF64.ofBitVec (2#64)).toBitVec == 0x1b#64) + let x := BF64.ofBitVec (0x01090913877ed8ed#64) + check "BF64 addition" ((x + 1).toBitVec == 0x01090913877ed8ec#64) + check "BF64 inverse" (inverseProduct x == 1) + check "BF64 zero inverse" ((0 : BF64)⁻¹ == 0) + let a := fromWords 0x950e87d7f5606615 0x2c61275c9e6b6cf8 0x1f00bca0042db923 + let b := fromWords 0x6dbca290a9eab706 0x4c10a4fe30cffdda 0xf26fff4cc4fd394d + let product := fromWords 0x888a0fc35abaf5f6 0x68a84cbc132b0649 0x9fdeaf613003cabe + check "Ext3 reference product" (a * b == product) + check "Ext3 addition" + (a + 1 == fromWords 0x950e87d7f5606614 0x2c61275c9e6b6cf8 0x1f00bca0042db923) + check "Ext3 inverse" (inverseProduct a == 1) + check "Ext3 zero inverse" ((0 : BF64.Ext3)⁻¹ == 0) + IO.println "Native field startup and arithmetic checks passed." + +end CompPolyTests.NativeSmoke + +/-- Entry point for the bounded native smoke test. -/ +def main : IO Unit := CompPolyTests.NativeSmoke.run diff --git a/tests/CompPolyTests.lean b/tests/CompPolyTests.lean index 46acc6bf..386ef415 100644 --- a/tests/CompPolyTests.lean +++ b/tests/CompPolyTests.lean @@ -13,6 +13,7 @@ public import CompPolyTests.Bivariate.GuruswamiSudan.Compose public import CompPolyTests.Bivariate.GuruswamiSudan.Core public import CompPolyTests.Bivariate.GuruswamiSudan.Filter public import CompPolyTests.Bivariate.GuruswamiSudan.Hasse +public import CompPolyTests.Bivariate.GuruswamiSudan.Interpolation.ApproximantBasis public import CompPolyTests.Bivariate.GuruswamiSudan.Interpolation.Dense public import CompPolyTests.Bivariate.GuruswamiSudan.Interpolation.LeeOSullivan public import CompPolyTests.Bivariate.GuruswamiSudan.Root.Alekhnovich @@ -23,19 +24,46 @@ public import CompPolyTests.Bivariate.Multiplicity public import CompPolyTests.Bivariate.WeightedDegree public import CompPolyTests.Data.MvPolynomial.Notation public import CompPolyTests.Data.Polynomial.RabinCertificate +public import CompPolyTests.Data.RingTheory.AlgebraTower +public import CompPolyTests.Data.RingTheory.AlgebraTower.Coordinates public import CompPolyTests.Fields.BLS12_377.Fast public import CompPolyTests.Fields.BLS12_381.Fast public import CompPolyTests.Fields.BN254.Fast public import CompPolyTests.Fields.BabyBear.Fast +public import CompPolyTests.Fields.Binary.AdditiveNTT.Basis +public import CompPolyTests.Fields.Binary.AdditiveNTT.Compatibility +public import CompPolyTests.Fields.Binary.AdditiveNTT.Executable public import CompPolyTests.Fields.Binary.AdditiveNTT.NovelPolynomialBasis public import CompPolyTests.Fields.Binary.BF128Ghash.Prelude +public import CompPolyTests.Fields.Binary.BF64 +public import CompPolyTests.Fields.Binary.BF64.Presentation +public import CompPolyTests.Fields.Binary.Common +public import CompPolyTests.Fields.Binary.Tower.Algebra +public import CompPolyTests.Fields.Binary.Tower.BasisCoordinates +public import CompPolyTests.Fields.Binary.Tower.ConcreteImports +public import CompPolyTests.Fields.Binary.Tower.Coordinates +public import CompPolyTests.Fields.Binary.Tower.Fast +public import CompPolyTests.Fields.Binary.Tower.Norm +public import CompPolyTests.Fields.Binary.Tower.RelativeCoordinates +public import CompPolyTests.Fields.Binary.Tower.TensorAlgebra public import CompPolyTests.Fields.Extension.Arithmetic +public import CompPolyTests.Fields.Extension.Certificates public import CompPolyTests.Fields.Extension.Binomial +public import CompPolyTests.Fields.Extension.Presentation +public import CompPolyTests.Fields.Extension.RawArithmetic +public import CompPolyTests.Fields.Goldilocks.Fast public import CompPolyTests.Fields.KoalaBear.Fast +public import CompPolyTests.Fields.KoalaBear.SexticReplay +public import CompPolyTests.Fields.Mersenne31.Fast +public import CompPolyTests.Fields.Mersenne31.Instances +public import CompPolyTests.Fields.Pasta.Fast public import CompPolyTests.Fields.PrattCertificate public import CompPolyTests.LinearAlgebra.Dense +public import CompPolyTests.LinearAlgebra.PolynomialMatrix.Approximant +public import CompPolyTests.LinearAlgebra.TensorProduct.Basis public import CompPolyTests.Multilinear.Equiv public import CompPolyTests.Multivariate.CMvMonomial +public import CompPolyTests.Multivariate.PartialEval public import CompPolyTests.Multivariate.Restrict public import CompPolyTests.Multivariate.TypeclassMinimization public import CompPolyTests.Multivariate.VarsDegrees @@ -49,4 +77,6 @@ public import CompPolyTests.Univariate.NTT.Inverse public import CompPolyTests.Univariate.Raw public import CompPolyTests.Univariate.Roots.Enumeration public import CompPolyTests.Univariate.Roots.FiniteField +public import CompPolyTests.Univariate.Roots.LasVegas +public import CompPolyTests.Univariate.Roots.Shoup public import CompPolyTests.Univariate.ToPoly diff --git a/tests/CompPolyTests/Bivariate/GuruswamiSudan/Interpolation/ApproximantBasis.lean b/tests/CompPolyTests/Bivariate/GuruswamiSudan/Interpolation/ApproximantBasis.lean new file mode 100644 index 00000000..9ce6ec57 --- /dev/null +++ b/tests/CompPolyTests/Bivariate/GuruswamiSudan/Interpolation/ApproximantBasis.lean @@ -0,0 +1,232 @@ +/- +Copyright (c) 2026 CompPoly Contributors. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Valerii Huhnin +-/ + +module + +public meta import CompPoly.Bivariate.GuruswamiSudan.Interpolation.ApproximantBasis +public meta import CompPoly.Univariate.Roots.Enumeration +public meta import CompPoly.Bivariate.GuruswamiSudan.Core +public meta import CompPoly.Bivariate.GuruswamiSudan.Root.RothRuckenstein.Correctness +public meta import Mathlib.Algebra.Field.ZMod + +/-! +# Approximant-Basis Guruswami-Sudan Interpolation Tests + +Focused executable coverage for GS modular-data construction and diagonal +relation checks. +-/ + +public meta section + +namespace CompPolyTests + +open CompPoly +open CompPoly.GuruswamiSudan +open CompPoly.GuruswamiSudan.ApproximantBasis +open CompPoly.PolynomialMatrix + +namespace GuruswamiSudan.Interpolation.ApproximantBasis + +abbrev F3 := ZMod 3 +abbrev F5 := ZMod 5 + +instance : Fact (Nat.Prime 3) := + ⟨by decide⟩ + +instance : Fact (Nat.Prime 5) := + ⟨by decide⟩ + +private def X : CPolynomial F3 := + CPolynomial.X + +private def paramsS1 : GSInterpParams := + { messageDegree := 2, multiplicity := 1, weightedDegreeBound := 2 } + +private def paramsS2 : GSInterpParams := + { messageDegree := 2, multiplicity := 2, weightedDegreeBound := 4 } + +private def paramsS3 : GSInterpParams := + { messageDegree := 2, multiplicity := 3, weightedDegreeBound := 6 } + +private def lowParams : GSInterpParams := + { messageDegree := 1, multiplicity := 1, weightedDegreeBound := 0 } + +private def nonCodewordParams : GSInterpParams := + { messageDegree := 2, multiplicity := 1, weightedDegreeBound := 3 } + +private def nonCodewordStressParams : GSInterpParams := + { messageDegree := 2, multiplicity := 2, weightedDegreeBound := 3 } + +private def points : Array (F3 × F3) := + #[(0, 0), (1, 1)] + +private def nonCodewordPoints : Array (F5 × F5) := + #[(0, 0), (1, 1), (2, 0)] + +private def lowPoints : Array (F3 × F3) := + #[(0, 0)] + +private def duplicateXPoints : Array (F3 × F3) := + #[(0, 0), (0, 1)] + +private def directV : CPolynomial.VanishingPolynomialContext F3 := + CPolynomial.VanishingPolynomialContext.direct + +private def hornerE : CPolynomial.BatchEvalContext F3 := + CPolynomial.BatchEvalContext.horner F3 + +private def modCtx : CPolynomial.ModContext F3 := + CPolynomial.ModContext.remainderOnly + +private def directV5 : CPolynomial.VanishingPolynomialContext F5 := + CPolynomial.VanishingPolynomialContext.direct + +private def hornerE5 : CPolynomial.BatchEvalContext F5 := + CPolynomial.BatchEvalContext.horner F5 + +private def modCtx5 : CPolynomial.ModContext F5 := + CPolynomial.ModContext.remainderOnly + +private def naiveMul : CPolynomial.MulContext F3 := + CPolynomial.MulContext.naive + +private def naiveMul5 : CPolynomial.MulContext F5 := + CPolynomial.MulContext.naive + +private def dataS1 : GSModularData F3 := + buildGSModularData directV hornerE naiveMul modCtx points paramsS1 + +private def dataS2 : GSModularData F3 := + buildGSModularData directV hornerE naiveMul modCtx points paramsS2 + +private def dataS3 : GSModularData F3 := + buildGSModularData directV hornerE naiveMul modCtx points paramsS3 + +private def nonCodewordData : GSModularData F5 := + buildGSModularData directV5 hornerE5 naiveMul5 modCtx5 nonCodewordPoints + nonCodewordParams + +private def nonCodewordStressData : GSModularData F5 := + buildGSModularData directV5 hornerE5 naiveMul5 modCtx5 nonCodewordPoints + nonCodewordStressParams + +private def pmCtx : PolynomialMatrix.Approximant.PMBasisContext F3 := + PolynomialMatrix.Approximant.kernelLeafPMBasisContext + CPolynomial.MulContext.naive 32 + +private def solver : PolynomialMatrix.Approximant.ModularSolutionBasisContext F3 := + PolynomialMatrix.Approximant.modularSolutionBasisContextViaPMBasis + CPolynomial.MulContext.naive modCtx pmCtx + +private def pmCtx5 : PolynomialMatrix.Approximant.PMBasisContext F5 := + PolynomialMatrix.Approximant.kernelLeafPMBasisContext + CPolynomial.MulContext.naive 32 + +private def solver5 : PolynomialMatrix.Approximant.ModularSolutionBasisContext F5 := + PolynomialMatrix.Approximant.modularSolutionBasisContextViaPMBasis + CPolynomial.MulContext.naive modCtx5 pmCtx5 + +private def approxContext : GSInterpContext F3 := + approximantBasisInterpContext directV hornerE solver + +private def approxContext5 : GSInterpContext F5 := + approximantBasisInterpContext directV5 hornerE5 solver5 + +private def f3Elements : Array F3 := + #[0, 1, 2] + +private theorem f3Elements_complete : ContainsAllFieldElements f3Elements := by + intro a + -- Explicit membership witnesses: `Decidable (_ ∈ _.toList)` no longer synthesizes + -- for this carrier, so avoid `decide` here. + fin_cases a + · exact List.Mem.head _ + · exact List.Mem.tail _ (List.Mem.head _) + · exact List.Mem.tail _ (List.Mem.tail _ (List.Mem.head _)) + +private def fieldRoots : FieldRootContext F3 := + enumeratingFieldRootContext F3 f3Elements f3Elements_complete + +private def rootContext : GSRootContext F3 := + rothRuckensteinRootContext F3 fieldRoots + +private def rowYMinusX : PolynomialRow F3 := + #[-X, 1, 0] + +private def rowYMinusXSquared : PolynomialRow F3 := + #[X ^ 2, -(CPolynomial.C (2 : F3) * X), 1, 0, 0] + +#guard interpolationYCap paramsS1 == 2 +#guard interpolationWidth paramsS1 == 3 +#guard interpolationShifts paramsS1 == #[0, 1, 2] +#guard distinctXCoordinatesBool points +#guard !distinctXCoordinatesBool duplicateXPoints + +#guard dataS1.moduli.size == 1 +#guard dataS1.matrix.size == 3 +#guard MatrixWidth dataS1.matrix == 1 +#guard dataS1.shift == #[0, 1, 2] +#guard rowGet (dataS1.matrix.getD 0 #[]) 0 == 1 +#guard rowGet (dataS1.matrix.getD 1 #[]) 0 == X +#guard rowGet (dataS1.matrix.getD 2 #[]) 0 == X +#guard rowSatisfiesModularBool CPolynomial.MulContext.naive modCtx + rowYMinusX dataS1.matrix dataS1.moduli + +#guard dataS2.moduli.size == 2 +#guard dataS2.matrix.size == 5 +#guard MatrixWidth dataS2.matrix == 2 +#guard dataS2.moduli.getD 0 0 == dataS2.G ^ 2 +#guard dataS2.moduli.getD 1 0 == dataS2.G +#guard rowGet (dataS2.matrix.getD 0 #[]) 1 == 0 +#guard rowGet (dataS2.matrix.getD 1 #[]) 1 == 1 +#guard rowSatisfiesModularBool CPolynomial.MulContext.naive modCtx + rowYMinusXSquared dataS2.matrix dataS2.moduli + +#guard dataS3.moduli.size == 3 +#guard dataS3.matrix.size == 7 +#guard MatrixWidth dataS3.matrix == 3 +#guard dataS3.moduli.getD 0 0 == dataS3.G ^ 3 +#guard dataS3.moduli.getD 1 0 == dataS3.G ^ 2 +#guard dataS3.moduli.getD 2 0 == dataS3.G + +#guard approximantBasisPositiveInterpolate directV hornerE solver + duplicateXPoints paramsS1 == none + +#guard (approximantBasisInterpolate directV hornerE solver points paramsS1).isSome +#guard match approximantBasisInterpolate directV hornerE solver points paramsS1 with + | none => false + | some Q => interpolationWitnessIsValidBool points paramsS1 Q + +#guard (approximantBasisInterpolate directV hornerE solver lowPoints lowParams).isSome +#guard match approximantBasisInterpolate directV hornerE solver lowPoints lowParams with + | none => false + | some Q => interpolationWitnessIsValidBool lowPoints lowParams Q + +#guard (approximantBasisInterpolate directV5 hornerE5 solver5 + nonCodewordPoints nonCodewordParams).isSome +#guard match approximantBasisInterpolate directV5 hornerE5 solver5 + nonCodewordPoints nonCodewordParams with + | none => false + | some Q => interpolationWitnessIsValidBool nonCodewordPoints nonCodewordParams Q + +#guard (approximantBasisInterpolate directV5 hornerE5 solver5 + nonCodewordPoints nonCodewordStressParams).isSome +#guard match approximantBasisInterpolate directV5 hornerE5 solver5 + nonCodewordPoints nonCodewordStressParams with + | none => false + | some Q => interpolationWitnessIsValidBool nonCodewordPoints nonCodewordStressParams Q + +#guard (approxContext5.interpolate nonCodewordPoints nonCodewordStressParams).isSome +#guard match approxContext5.interpolate nonCodewordPoints nonCodewordStressParams with + | none => false + | some Q => interpolationWitnessIsValidBool nonCodewordPoints nonCodewordStressParams Q + +#guard (gsCore points approxContext rootContext paramsS1).size <= 3 +#guard (gsCore lowPoints approxContext rootContext lowParams).size <= 3 + +end GuruswamiSudan.Interpolation.ApproximantBasis + +end CompPolyTests diff --git a/tests/CompPolyTests/Bivariate/Kronecker.lean b/tests/CompPolyTests/Bivariate/Kronecker.lean index 3b3ec62c..c8750ecb 100644 --- a/tests/CompPolyTests/Bivariate/Kronecker.lean +++ b/tests/CompPolyTests/Bivariate/Kronecker.lean @@ -14,7 +14,7 @@ public meta import CompPolyTests.Bivariate.KroneckerCommon Packing coefficients and round-trip recovery for `kroneckerPack` / `kroneckerUnpack`, followed by a runtime check that, on small KoalaBear data, each pipeline (schoolbook, classic NTT, recursive NTT) returns the same product as direct multiplication. The full - timing comparison lives in `KroneckerBenchmark.lean`. + timing comparison lives in the `bivariate-full-*` benchmark groups. -/ public meta section diff --git a/tests/CompPolyTests/Bivariate/KroneckerBenchmark.lean b/tests/CompPolyTests/Bivariate/KroneckerBenchmark.lean deleted file mode 100644 index 5810679e..00000000 --- a/tests/CompPolyTests/Bivariate/KroneckerBenchmark.lean +++ /dev/null @@ -1,104 +0,0 @@ -/- -Copyright (c) 2026 CompPoly. All rights reserved. -Released under Apache 2.0 license as described in the file LICENSE. -Authors: Dimitris Mitsios --/ -module - -public meta import CompPolyTests.Bivariate.KroneckerCommon - -/-! - # Bivariate Multiplication Benchmark - - Manual benchmark comparing four strategies for bivariate multiplication over the - KoalaBear field: - - 1. **normal** — the schoolbook `CBivariate` product `p * q`; - 2. **kron** — Kronecker substitution with schoolbook univariate multiplication; - 3. **kron+NTT** — Kronecker with the classic NTT (`NTT.FastMul.withFallback`); - 4. **kron+FastNTT** — Kronecker with the recursive NTT (`NTTFast.withFallback`). - - All four results are checked for agreement at every size. Operands are square - (`degX = degY = n`) and the Kronecker gap is `D = 2 * n`, which always satisfies - `natDegreeX (p * q) < D`, so unpacking is faithful. Shared data (`mkBiv`, - `bestDomainForLength?`, `kronWith`) lives in `KroneckerCommon.lean`; the agreement - check lives in `Kronecker.lean`. - - Not part of the aggregate `CompPolyTests` build; run manually, e.g. - `lake build CompPolyTests.Bivariate.KroneckerBenchmark`. --/ - -public meta section - -namespace CompPoly -namespace CBivariate -namespace Benchmark - -open CBivariate.TestCommon - -/-- Sweep of square sizes (`degX = degY = n`). - -Kept modest because `#eval` runs in the interpreter; for larger sizes compile a native -executable (see the note at the bottom of this file). The NTT crossover on the packed -univariate operand (size `2 * n^2`) is already visible by `n = 16`–`32`. -/ -def benchSizes : Array Nat := #[4, 8, 12, 16, 24, 32] - -/-- Render an average millisecond count. -/ -def avgMsString (totalMs reps : Nat) : String := - s!"{(Float.ofNat totalMs) / (Float.ofNat reps)}" - -/-- Number of repetitions to use for a given square size. -/ -def repeatsFor (n : Nat) : Nat := - if n ≤ 8 then 10 - else if n ≤ 16 then 4 - else 1 - -/-- Time repeated calls to a thunk and return the final result. -/ -def timeRepeated {α : Type} (reps : Nat) (f : Unit → α) : IO (Nat × α) := do - let actualReps := max reps 1 - let start ← IO.monoMsNow - let mut last := f () - for _ in [1:actualReps] do - last := f () - let stop ← IO.monoMsNow - pure (stop - start, last) - -#eval show IO Unit from do - IO.println s!"sizes tested = {benchSizes.size} (degX = degY = n, gap D = 2n)" - IO.println "n | reps | normal ms | kron ms | kron+NTT ms | kron+FastNTT ms" - IO.println "------------------------------------------------------------------" - for i in [0:benchSizes.size] do - let n := benchSizes[i]! - let reps := repeatsFor n - let D := 2 * n - let p := mkBiv n n (41 + 13 * i) - let q := mkBiv n n (73 + 17 * i) - let (normalMs, normalRes) ← timeRepeated reps (fun _ ↦ p * q) - let (kronMs, kronRes) ← timeRepeated reps (fun _ ↦ kronWith (· * ·) D p q) - let (nttMs, nttRes) ← - timeRepeated reps (fun _ ↦ kronWith (CPolynomial.NTT.FastMul.withFallback - bestDomainForLength?) D p q) - let (fastMs, fastRes) ← - timeRepeated reps (fun _ ↦ kronWith (CPolynomial.NTTFast.withFallback - bestDomainForLength?) D p q) - unless (kronRes == normalRes) && (nttRes == normalRes) && (fastRes == normalRes) do - throw <| IO.userError s!"benchmark mismatch at n = {n}" - let row := - s!"{n} | {reps} | {avgMsString normalMs reps} | {avgMsString kronMs reps} | " ++ - s!"{avgMsString nttMs reps} | {avgMsString fastMs reps}" - IO.println row - -end Benchmark -end CBivariate -end CompPoly - -/- - ## Running larger sizes - - `#eval` runs in the interpreter, which dominates the absolute timings (the relative - comparison and crossover are still meaningful). For larger operands, compile natively: - turn the `#eval` body into `def main : IO Unit` and add a `lean_exe` target to the - lakefile, or run with `lake env lean --run` on a standalone copy. Native execution is - typically two to three orders of magnitude faster, pushing the practical sweep well past - `n = 32`. --/ diff --git a/tests/CompPolyTests/Data/Polynomial/RabinCertificate.lean b/tests/CompPolyTests/Data/Polynomial/RabinCertificate.lean index e83a2fc1..d4bcd651 100644 --- a/tests/CompPolyTests/Data/Polynomial/RabinCertificate.lean +++ b/tests/CompPolyTests/Data/Polynomial/RabinCertificate.lean @@ -15,7 +15,7 @@ public import Mathlib.Tactic.NormNum.Prime End-to-end exercise of `CompPoly/Data/Polynomial/RabinCertificate.lean` at a size where the certificate can be checked by hand: `X^2 + X + 1` is irreducible over `ZMod 5` (it has no roots: squares mod 5 are `{0, 1, 4}` and `x^2 + x + 1` hits `1, 3, 2, 3, 1`). The certificate -data below is what `scripts/gen_rabin_certificate.py --p 5 --f "1,1,1"` emits. +data below is what `scripts/gen_rabin_certificate.py --p 5 --f='1,1,1'` emits. This keeps the framework honest independently of the (much larger) KoalaBear quintic certificate in `CompPoly/Fields/KoalaBear/Ext5/QuinticCertData.lean`. @@ -272,4 +272,35 @@ theorem not_isCoprime : ¬ IsCoprime fRed ((X : (ZMod P)[X]) ^ (P ^ 3) - X) := b end ReducibleSextic +/-! ### The `_of_card` forms are equivalent to the plain ones + +`irreducible_of_rabin_prime_degree_of_card` and `irreducible_of_rabin_degree_six_of_card` state +their conditions at a caller-supplied numeral `q` with `hcard : Fintype.card F = q`, which is the +shape concrete extensions use. Their docstrings claim nothing is weakened; the two theorems below +are that claim, machine-checked. Instantiating at `q := Fintype.card F` with `rfl` has to recover +the plain form *verbatim*, so a future edit cannot silently add a hypothesis or shift an exponent. +The opposite direction is the `_of_card` proof body itself, checked whenever the library builds. +-/ + +namespace OfCardRoundTrip + +/-- `irreducible_of_rabin_prime_degree_of_card` recovers `irreducible_of_rabin_prime_degree`. -/ +theorem prime_degree_recovered {F : Type*} [Field F] [Fintype F] {f : F[X]} {d : ℕ} + (hd : d.Prime) (h_deg : f.natDegree = d) + (h_trace : f ∣ X ^ (Fintype.card F ^ d) - X) + (h_cop : IsCoprime f (X ^ Fintype.card F - X)) : + Irreducible f := + irreducible_of_rabin_prime_degree_of_card rfl hd h_deg h_trace h_cop + +/-- `irreducible_of_rabin_degree_six_of_card` recovers `irreducible_of_rabin_degree_six`. -/ +theorem degree_six_recovered {F : Type*} [Field F] [Fintype F] {f : F[X]} + (h_deg : f.natDegree = 6) + (h_trace : f ∣ X ^ (Fintype.card F ^ 6) - X) + (h_cop₃ : IsCoprime f (X ^ (Fintype.card F ^ 3) - X)) + (h_cop₂ : IsCoprime f (X ^ (Fintype.card F ^ 2) - X)) : + Irreducible f := + irreducible_of_rabin_degree_six_of_card rfl h_deg h_trace h_cop₃ h_cop₂ + +end OfCardRoundTrip + end CompPolyTests.RabinCertificate diff --git a/tests/CompPolyTests/Data/RingTheory/AlgebraTower.lean b/tests/CompPolyTests/Data/RingTheory/AlgebraTower.lean new file mode 100644 index 00000000..ae182b0f --- /dev/null +++ b/tests/CompPolyTests/Data/RingTheory/AlgebraTower.lean @@ -0,0 +1,145 @@ +/- +Copyright (c) 2026 CompPoly Contributors. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: CompPoly Contributors +-/ +module + +import CompPoly.Data.RingTheory.AlgebraTower +import Mathlib.Data.ZMod.Basic + +/-! +# Algebra tower identity and adjacent-map regression tests + +The projection `(x, y) ↦ (x, x)` on `GF(2) × GF(2)` is an idempotent ring endomorphism. +Using it between every pair of levels gives coherent maps whose self-maps are not identities. +The identity law excludes this family of maps. Identity maps on the same carrier give a valid +tower over a commutative semiring that is not a field. + +Using the projection only as the adjacent step gives a valid tower via `AlgebraTower.ofNatStep`. +Symbolic clients check composition and recover any existing natural-number-indexed tower +from its adjacent maps. +-/ + +namespace CompPolyTests.AlgebraTower + +section Generic + +variable {ι : Type*} [Preorder ι] {A : ι → Type*} + [∀ i, CommSemiring (A i)] [AlgebraTower A] + +example (i : ι) (h : i ≤ i) : + AlgebraTower.algebraMap (AT := A) i i h = RingHom.id (A i) := by + simp + +example (i : ι) (h : i ≤ i) (x : A i) : + AlgebraTower.algebraMap (AT := A) i i h x = x := by + simp only [AlgebraTower.algebraMap_self_apply] + +end Generic + +section NatStep + +variable {A : ℕ → Type*} [∀ k, CommSemiring (A k)] + (step : ∀ k, A k →+* A (k + 1)) + +example (i : ℕ) (h : i ≤ i) : + (AlgebraTower.ofNatStep step).algebraMap i i h = RingHom.id (A i) := + (AlgebraTower.ofNatStep step).algebraMap_self' i + +example (i : ℕ) (h : i ≤ i + 1) : + (AlgebraTower.ofNatStep step).algebraMap i (i + 1) h = step i := by + simp + +example {i j : ℕ} (h h' : i ≤ j) : + (AlgebraTower.ofNatStep step).algebraMap i j h = + (AlgebraTower.ofNatStep step).algebraMap i j h' := rfl + +example {i j k : ℕ} (hij : i ≤ j) (hjk : j ≤ k) (x : A i) : + (AlgebraTower.ofNatStep step).algebraMap i k (hij.trans hjk) x = + (AlgebraTower.ofNatStep step).algebraMap j k hjk + ((AlgebraTower.ofNatStep step).algebraMap i j hij x) := + congrArg (fun f : A i →+* A k => f x) + ((AlgebraTower.ofNatStep step).coherence' i j k hij hjk) + +example {i j k : ℕ} (hij : i ≤ j) (hjk : j ≤ k) : + letI := AlgebraTower.ofNatStep step + letI := AlgebraTower.toAlgebra (A := A) hij + letI := AlgebraTower.toAlgebra (A := A) hjk + letI := AlgebraTower.toAlgebra (A := A) (hij.trans hjk) + IsScalarTower (A i) (A j) (A k) := + AlgebraTower.toIsScalarTower (AlgebraTower.ofNatStep step) hij hjk + +-- Reconstructing an existing tower from its adjacent maps preserves every comparable map. +example [t : AlgebraTower A] {i j : ℕ} (h : i ≤ j) : + (AlgebraTower.ofNatStep (fun k => t.algebraMap k (k + 1) (Nat.le_succ k))).algebraMap + i j h = t.algebraMap i j h := by + induction h with + | refl => + exact ((AlgebraTower.ofNatStep (fun k => + t.algebraMap k (k + 1) (Nat.le_succ k))).algebraMap_self' i).trans + (t.algebraMap_self' i).symm + | @step j h ih => + rw [AlgebraTower.ofNatStep_algebraMap_succ_right _ h, ih, + t.coherence' i j (j + 1) h (Nat.le_succ j)] + +end NatStep + +private abbrev R := ZMod 2 × ZMod 2 + +/-- The ring endomorphism `(x, y) ↦ (x, x)`. -/ +private def diagonal : R →+* R where + toFun x := (x.1, x.1) + map_one' := rfl + map_zero' := rfl + map_add' _ _ := rfl + map_mul' _ _ := rfl + +-- Constant projection maps satisfy commutativity and composition. +example (r x : R) : diagonal r * x = x * diagonal r := mul_comm _ _ + +example : diagonal = diagonal.comp diagonal := by + ext x <;> rfl + +private theorem diagonal_ne_id : diagonal ≠ RingHom.id R := by + intro h + have h01 := congrArg (fun f : R →+* R => (f (0, 1)).2) h + change (0 : ZMod 2) = 1 at h01 + exact zero_ne_one h01 + +-- A tower cannot use this projection for every map. +example : ¬ ∃ t : AlgebraTower (fun _ : ℕ => R), + ∀ i j h, t.algebraMap i j h = diagonal := by + rintro ⟨t, ht⟩ + exact diagonal_ne_id ((ht 0 0 le_rfl).symm.trans (t.algebraMap_self' 0)) + +/-- The constant tower on `GF(2) × GF(2)` with identity maps. -/ +private abbrev constantTower : AlgebraTower (fun _ : ℕ => R) where + algebraMap _ _ _ := RingHom.id R + algebraMap_self' _ := rfl + commutes' _ _ _ r x := mul_comm r x + coherence' _ _ _ _ _ := rfl + +example (x : R) : constantTower.algebraMap 0 2 (by decide) x = x := rfl + +/-- A valid tower with noninjective adjacent maps on the same non-field carrier. -/ +private abbrev projectionTower : AlgebraTower (fun _ : ℕ => R) := + AlgebraTower.ofNatStep (fun _ => diagonal) + +example (k : ℕ) : projectionTower.algebraMap k k le_rfl (0, 1) = (0, 1) := by + rw [projectionTower.algebraMap_self'] + rfl + +example : projectionTower.algebraMap 0 2 (by decide) (0, 1) = (0, 0) := by + rw [AlgebraTower.ofNatStep_algebraMap_succ_right _ (show 0 ≤ 1 by decide), + AlgebraTower.ofNatStep_algebraMap_succ] + rfl + +example (k : ℕ) : + ¬ Function.Injective (projectionTower.algebraMap k (k + 1) (Nat.le_succ k)) := by + rw [AlgebraTower.ofNatStep_algebraMap_succ] + intro h + have he := h (show diagonal (0, 1) = diagonal (0, 0) from rfl) + exact one_ne_zero (congrArg Prod.snd he) + +end CompPolyTests.AlgebraTower diff --git a/tests/CompPolyTests/Data/RingTheory/AlgebraTower/Coordinates.lean b/tests/CompPolyTests/Data/RingTheory/AlgebraTower/Coordinates.lean new file mode 100644 index 00000000..327223f4 --- /dev/null +++ b/tests/CompPolyTests/Data/RingTheory/AlgebraTower/Coordinates.lean @@ -0,0 +1,289 @@ +/- +Copyright (c) 2026 CompPoly Contributors. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: CompPoly Contributors +-/ +module + +import CompPoly.Data.RingTheory.AlgebraTower.Basis +meta import CompPoly.Data.RingTheory.AlgebraTower.Coordinates + +/-! +# Finite-product tower coordinate tests + +Successive function rings give towers with arbitrary finite step ranks, using constant +functions as the tower maps. A rectangular two-step example distinguishes the two index +orders. Rank zero, rank one, and height zero exercise the boundary cases without field or +nontriviality assumptions. Symbolic clients check the selected scalar action and the actual +basis representation. + +Endpoint clients accept unrelated `i`, `j` and `h : i ≤ j` without client-side casts. Constant +coordinate counts zero, one and two exercise the normalized index type. A zero intermediate +count gives an explicitly noninjective tower map from a nontrivial source. +-/ + +namespace CompPolyTests.AlgebraTower.Coordinates + +open _root_.AlgebraTower + +private def FunctionTower (d : ℕ → ℕ) : ℕ → Type + | 0 => ℕ + | k + 1 => Fin (d k) → FunctionTower d k + +private instance functionTowerCommSemiring (d : ℕ → ℕ) : + (k : ℕ) → CommSemiring (FunctionTower d k) + | 0 => inferInstanceAs (CommSemiring ℕ) + | k + 1 => + letI := functionTowerCommSemiring d k + inferInstanceAs (CommSemiring (Fin (d k) → FunctionTower d k)) + +private def functionStep (d : ℕ → ℕ) (k : ℕ) : + FunctionTower d k →+* FunctionTower d (k + 1) := + Pi.constRingHom (Fin (d k)) (FunctionTower d k) + +private instance functionTower (d : ℕ → ℕ) : AlgebraTower (FunctionTower d) := + ofNatStep (functionStep d) + +private def functionCoordinates (d : ℕ → ℕ) (k : ℕ) : + letI := (functionTower d).toAlgebra (Nat.le_succ k) + FunctionTower d (k + 1) ≃ₗ[FunctionTower d k] (Fin (d k) → FunctionTower d k) := by + letI := (functionTower d).toAlgebra (Nat.le_succ k) + exact { + toFun := id + invFun := id + left_inv := fun _ => rfl + right_inv := fun _ => rfl + map_add' := fun _ _ => rfl + map_smul' := fun a x => by + funext b + change (((ofNatStep (functionStep d)).algebraMap k (k + 1) (Nat.le_succ k) a) * x) b = + a * x b + rw [ofNatStep_algebraMap_succ] + rfl } + +private abbrev ranks : ℕ → ℕ + | 0 => 2 + | 1 => 3 + | _ => 1 + +private def rectangular : FunctionTower ranks 2 := ![![10, 11], ![20, 21], ![30, 31]] + +-- Compiled execution uses the coordinate maps and packing, without the noncomputable Basis. +#guard List.ofFn (show Fin 6 → ℕ from + natCoordinates (functionCoordinates ranks) 0 2 rectangular) == + [10, 11, 20, 21, 30, 31] +#guard (show ℕ from natPack (functionCoordinates ranks) 0 2 + ![10, 11, 20, 21, 30, 31] 2 0) == 30 +#guard List.ofFn (show Fin 6 → ℕ from natCoordinates (functionCoordinates ranks) 0 2 + (natBasisVector (functionCoordinates ranks) 0 2 4)) == [0, 0, 0, 0, 1, 0] + +private theorem rectangular_pack : + natPack (functionCoordinates ranks) 0 2 ![10, 11, 20, 21, 30, 31] = rectangular := by + simp only [natPack_succ, natPack_zero] + funext b j + fin_cases b <;> fin_cases j <;> rfl + +-- The preceding-level coordinate varies fastest, even when the next rank differs. +example : natCoordinates (functionCoordinates ranks) 0 2 rectangular = + ![10, 11, 20, 21, 30, 31] := by + rw [← rectangular_pack, natCoordinates_natPack] + +-- The transposed order is still a bijection, but gives a different second coefficient. +example : natCoordinates (functionCoordinates ranks) 0 2 rectangular 1 ≠ + rectangular ((finProdFinEquiv.symm (1 : Fin (2 * 3))).2) + ((finProdFinEquiv.symm (1 : Fin (2 * 3))).1) := by + conv_lhs => rw [← rectangular_pack] + change natCoordinates (functionCoordinates ranks) 0 2 + (natPack (functionCoordinates ranks) 0 2 ![10, 11, 20, 21, 30, 31]) 1 ≠ rectangular 1 0 + rw [congrFun (natCoordinates_natPack (functionCoordinates ranks) 0 2 + ![10, 11, 20, 21, 30, 31]) 1] + change (11 : ℕ) ≠ 20 + decide + +example (x : ℕ) : natCoordinates (functionCoordinates ranks) 0 0 x 0 = x := + natCoordinates_zero _ _ _ _ + +-- A legitimate first basis vector need not be one in the target ring. +example : natBasisVector (functionCoordinates ranks) 0 1 0 ≠ + (1 : FunctionTower ranks 1) := by + intro h + have hone : natCoordinates (functionCoordinates ranks) 0 1 + (1 : FunctionTower ranks 1) 1 = 1 := by + rw [natCoordinates_succ, natCoordinates_zero] + rfl + have hc := congrArg (fun x : FunctionTower ranks 1 => + natCoordinates (functionCoordinates ranks) 0 1 x 1) h + rw [congrFun (natCoordinates_natBasisVector (functionCoordinates ranks) 0 1 0) 1, + hone] at hc + exact Nat.zero_ne_one hc + +example (x : FunctionTower (fun _ => 1) 3) : + natPack (functionCoordinates (fun _ => 1)) 1 2 + (natCoordinates (functionCoordinates (fun _ => 1)) 1 2 x) = x := + natPack_natCoordinates _ _ _ _ + +-- A zero successor rank is a valid free presentation of a zero function ring. +example (x : FunctionTower (fun _ => 0) 2) : + natPack (functionCoordinates (fun _ => 0)) 0 2 + (natCoordinates (functionCoordinates (fun _ => 0)) 0 2 x) = x := + natPack_natCoordinates _ _ _ _ + +example (r i n : ℕ) : coordinateSize (fun _ => r) i n = r ^ n := + coordinateSize_const r i n + +-- Endpoint transport preserves the independently specified rectangular order. +#guard List.ofFn (show Fin 6 → ℕ from + natCoordinatesOfLE (functionCoordinates ranks) (show 0 ≤ 2 by decide) + rectangular) == [10, 11, 20, 21, 30, 31] +#guard (show ℕ from natPackOfLE (functionCoordinates ranks) (show 0 ≤ 2 by decide) + ![10, 11, 20, 21, 30, 31] 2 0) == 30 + +example (x : FunctionTower ranks 1) (idx : Fin 2) : + natCoordinatesOfLE (functionCoordinates ranks) (show 0 ≤ 1 by decide) x idx = x idx := by + rw [natCoordinatesOfLE_apply, natCoordinates_succ, natCoordinates_zero] + change x ⟨idx.val / 1, _⟩ = x idx + congr 1 + exact Fin.ext (Nat.div_one idx.val) + +example : natBasisVectorOfLE (functionCoordinates ranks) (show 0 ≤ 2 by decide) 4 = + natBasisVector (functionCoordinates ranks) 0 2 4 := + natBasisVectorOfLE_eq_natBasisVector _ _ _ + +private def shiftedBinary : FunctionTower (fun _ => 2) 3 := + ![![![10, 11], ![20, 21]], ![![30, 31], ![40, 41]]] + +-- Four coefficients over level one retain both entries of each lower-level element. +#guard List.ofFn (fun idx => List.ofFn (show Fin 2 → ℕ from + natCoordinatesConstOfLE (functionCoordinates (fun _ => 2)) (show 1 ≤ 3 by decide) + shiftedBinary idx)) == [[10, 11], [20, 21], [30, 31], [40, 41]] +#guard (show ℕ from (natCoordinatesConstOfLE (functionCoordinates (fun _ => 2)) + (show 1 ≤ 3 by decide)).symm ![![10, 11], ![20, 21], ![30, 31], ![40, 41]] + 1 0 1) == 31 + +private abbrev zeroMiddle : ℕ → ℕ + | 0 => 2 + | 1 => 0 + | _ => 1 + +example : (0 : FunctionTower zeroMiddle 1) ≠ 1 := by + intro h + exact Nat.zero_ne_one (congrFun h 0) + +-- The distinct source elements above have the same image in the zero function ring. +example : (functionTower zeroMiddle).algebraMap 1 2 (by decide) 0 = + (functionTower zeroMiddle).algebraMap 1 2 (by decide) 1 := by + funext idx + exact Fin.elim0 idx + +example (x : FunctionTower zeroMiddle 3) : + natPackOfLE (functionCoordinates zeroMiddle) (show 1 ≤ 3 by decide) + (natCoordinatesOfLE (functionCoordinates zeroMiddle) (show 1 ≤ 3 by decide) x) = x := + natPackOfLE_natCoordinatesOfLE _ _ _ + +-- Zero successors give no coordinates at positive height, but one at height zero. +example (x : FunctionTower (fun _ => 0) 2) : + (natCoordinatesConstOfLE (functionCoordinates (fun _ => 0)) (show 0 ≤ 2 by decide)).symm + (fun idx : Fin 0 => Fin.elim0 idx) = x := by + apply (natCoordinatesConstOfLE (functionCoordinates (fun _ => 0)) + (show 0 ≤ 2 by decide)).injective + rw [LinearEquiv.apply_symm_apply] + funext idx + exact Fin.elim0 idx + +#guard List.ofFn (show Fin 1 → ℕ from + natCoordinatesConstOfLE (functionCoordinates (fun _ => 0)) + (Nat.le_refl 0) 37) == [37] + +example {i j : ℕ} (h : i ≤ j) (x : FunctionTower (fun _ => 1) j) : + (natCoordinatesConstOfLE (functionCoordinates (fun _ => 1)) h).symm + (natCoordinatesConstOfLE (functionCoordinates (fun _ => 1)) h x) = x := + LinearEquiv.symm_apply_apply _ _ + +section Generic + +variable {A : ℕ → Type*} [∀ k, CommSemiring (A k)] [t : AlgebraTower A] + {d : ℕ → ℕ} + (step : ∀ k, letI := t.toAlgebra (Nat.le_succ k) + A (k + 1) ≃ₗ[A k] (Fin (d k) → A k)) + +example (i n : ℕ) (a : A i) (x : A (i + n)) : + natCoordinates step i n (t.algebraMap i (i + n) (Nat.le_add_right i n) a * x) = + a • natCoordinates step i n x := by + let := t.toAlgebra (Nat.le_add_right i n) + exact (natCoordinates step i n).map_smul a x + +example (i n : ℕ) (c : Fin (coordinateSize d i n) → A i) : + letI := t.toAlgebra (Nat.le_add_right i n) + (natBasis step i n).repr (natPack step i n c) = c := by + let := t.toAlgebra (Nat.le_add_right i n) + funext j + rw [natBasis_repr, congrFun (natCoordinates_natPack step i n c) j] + +example (i n : ℕ) (c : Fin (coordinateSize d i n) → A i) : + letI := t.toAlgebra (Nat.le_add_right i n) + ∑ j, c j • natBasis step i n j = natPack step i n c := by + let := t.toAlgebra (Nat.le_add_right i n) + have hc : (natBasis step i n).repr (natPack step i n c) = c := by + funext j + rw [natBasis_repr, congrFun (natCoordinates_natPack step i n c) j] + simpa only [hc] using (natBasis step i n).sum_repr (natPack step i n c) + +-- These endpoint types contain neither a client-side carrier cast nor a rewritten endpoint. +example {i j : ℕ} (h : i ≤ j) (x : A j) : + natPackOfLE step h (natCoordinatesOfLE step h x) = x := + natPackOfLE_natCoordinatesOfLE _ _ _ + +example {i j : ℕ} (h : i ≤ j) (a : A i) (x : A j) : + natCoordinatesOfLE step h (t.algebraMap i j h a * x) = + a • natCoordinatesOfLE step h x := by + let := t.toAlgebra h + exact (natCoordinatesOfLE step h).map_smul a x + +example {i j : ℕ} (h h' : i ≤ j) : natCoordinatesOfLE step h = natCoordinatesOfLE step h' := + rfl + +example {i j : ℕ} (h h' : i ≤ j) (c : Fin (coordinateSize d i (j - i)) → A i) : + natPackOfLE step h c = natPackOfLE step h' c := rfl + +example {i j : ℕ} (h h' : i ≤ j) : + natBasisVectorOfLE step h = natBasisVectorOfLE step h' := rfl + +example {i j : ℕ} (h h' : i ≤ j) : natBasisOfLE step h = natBasisOfLE step h' := rfl + +example {i j : ℕ} (h : i ≤ j) (c : Fin (coordinateSize d i (j - i)) → A i) : + letI := t.toAlgebra h + (natBasisOfLE step h).repr (natPackOfLE step h c) = c := by + let := t.toAlgebra h + funext idx + rw [natBasisOfLE_repr, congrFun (natCoordinatesOfLE_natPackOfLE step h c) idx] + +example {i j : ℕ} (h : i ≤ j) (c : Fin (coordinateSize d i (j - i)) → A i) : + letI := t.toAlgebra h + ∑ idx, c idx • natBasisOfLE step h idx = natPackOfLE step h c := by + let := t.toAlgebra h + have hc : (natBasisOfLE step h).repr (natPackOfLE step h c) = c := by + funext idx + rw [natBasisOfLE_repr, congrFun (natCoordinatesOfLE_natPackOfLE step h c) idx] + simpa only [hc] using (natBasisOfLE step h).sum_repr (natPackOfLE step h c) + +example {i j : ℕ} (h : i ≤ j) (idx : Fin (coordinateSize d i (j - i))) : + natBasisOfLE step h idx = + cast (congrArg A (Nat.add_sub_of_le h)) (natBasis step i (j - i) idx) := + natBasisOfLE_apply _ _ _ + +variable {r : ℕ} + (constantStep : ∀ k, letI := t.toAlgebra (Nat.le_succ k) + A (k + 1) ≃ₗ[A k] (Fin r → A k)) + +example {i j : ℕ} (h h' : i ≤ j) : + natCoordinatesConstOfLE constantStep h = natCoordinatesConstOfLE constantStep h' := rfl + +example {i j : ℕ} (h : i ≤ j) (a : A i) (x : A j) : + natCoordinatesConstOfLE constantStep h (t.algebraMap i j h a * x) = + a • natCoordinatesConstOfLE constantStep h x := by + let := t.toAlgebra h + exact (natCoordinatesConstOfLE constantStep h).map_smul a x + +end Generic + +end CompPolyTests.AlgebraTower.Coordinates diff --git a/tests/CompPolyTests/Fields/Binary/AdditiveNTT/Basis.lean b/tests/CompPolyTests/Fields/Binary/AdditiveNTT/Basis.lean new file mode 100644 index 00000000..ab41184c --- /dev/null +++ b/tests/CompPolyTests/Fields/Binary/AdditiveNTT/Basis.lean @@ -0,0 +1,36 @@ +/- +Copyright (c) 2026 CompPoly Contributors. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: CompPoly Contributors +-/ +module + +import CompPoly.Fields.Binary.AdditiveNTT.Impl + +/-! +# Executable tower basis regressions + +The basis used by the additive NTT reconstructs arbitrary concrete tower elements and +recovers arbitrary coefficient vectors at every level, including level zero. +-/ + +open scoped BigOperators +open ConcreteBinaryTower AdditiveNTT + +namespace CompPolyTests.AdditiveNTTBasis + +example (k : ℕ) (x : ConcreteBTField k) : + letI := ConcreteBTFieldAlgebra (l := 0) (r := k) (h_le := by omega) + ∑ i, (multilinearBasis 0 k (by omega)).repr x i • computableBasisExplicit k i = x := by + let := ConcreteBTFieldAlgebra (l := 0) (r := k) (h_le := by omega) + simp_rw [computableBasisExplicit_eq_multilinearBasis] + exact (multilinearBasis 0 k (by omega)).sum_repr x + +example (k : ℕ) (c : Fin (2 ^ k) → ConcreteBTField 0) (i : Fin (2 ^ k)) : + letI := ConcreteBTFieldAlgebra (l := 0) (r := k) (h_le := by omega) + (multilinearBasis 0 k (by omega)).repr (∑ j, c j • computableBasisExplicit k j) i = c i := by + let := ConcreteBTFieldAlgebra (l := 0) (r := k) (h_le := by omega) + simp_rw [computableBasisExplicit_eq_multilinearBasis] + exact congrFun ((multilinearBasis 0 k (by omega)).repr_sum_self c) i + +end CompPolyTests.AdditiveNTTBasis diff --git a/tests/CompPolyTests/Fields/Binary/AdditiveNTT/Compatibility.lean b/tests/CompPolyTests/Fields/Binary/AdditiveNTT/Compatibility.lean new file mode 100644 index 00000000..4068b975 --- /dev/null +++ b/tests/CompPolyTests/Fields/Binary/AdditiveNTT/Compatibility.lean @@ -0,0 +1,34 @@ +/- +Copyright (c) 2024-2025 ArkLib Contributors. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Chung Thai Nguyen, Quang Dao +-/ +module + +import CompPoly.Fields.Binary.AdditiveNTT.AdditiveNTT + +/-! +# Additive NTT compatibility entry point + +The umbrella import retains concrete tower definitions and instances alongside the generic +algorithms and correctness theorems. +-/ + +namespace CompPolyTests.AdditiveNTTCompatibility + +open AdditiveNTT ConcreteBinaryTower + +example : BTF₃ = ConcreteBTField 3 := rfl + +example : (inferInstance : Field BTF₃) = instFieldConcrete := rfl + +example (k : ℕ) : NeZero (2 ^ k) := inferInstance + +example (k : ℕ) : + letI := ConcreteBTFieldAlgebra (show 0 ≤ k by omega) + LinearIndependent (ConcreteBTField 0) (computableBasisExplicit k) := + hβ_lin_indep_concrete k + +example : Fin 16 → BTF₃ := testNTTBTF₃ + +end CompPolyTests.AdditiveNTTCompatibility diff --git a/tests/CompPolyTests/Fields/Binary/AdditiveNTT/Executable.lean b/tests/CompPolyTests/Fields/Binary/AdditiveNTT/Executable.lean new file mode 100644 index 00000000..4917f050 --- /dev/null +++ b/tests/CompPolyTests/Fields/Binary/AdditiveNTT/Executable.lean @@ -0,0 +1,45 @@ +/- +Copyright (c) 2024-2025 ArkLib Contributors. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Chung Thai Nguyen, Quang Dao +-/ +module + +import CompPoly.Fields.Binary.AdditiveNTT.Correctness + +/-! +# Generic executable additive NTT clients + +The correctness entry point supports arbitrary fields and must not import a binary tower +construction. The import guard makes that boundary part of the regression suite. +-/ + +namespace CompPolyTests.AdditiveNTTExecutable + +open AdditiveNTT + +open Lean Elab Command in +run_cmd do + for name in (← getEnv).header.moduleNames do + if name.toString.startsWith "CompPoly.Fields.Binary.Tower." then + throwError "Generic additive NTT correctness imported a tower module: {name}" + +example {r ℓ R_rate : ℕ} [NeZero r] + {L 𝔽q : Type} [Field L] [Fintype L] [DecidableEq L] + [Field 𝔽q] [Fintype 𝔽q] [Algebra 𝔽q L] [Fact (Fintype.card 𝔽q = 2)] + (β : Fin r → L) [Fact (LinearIndependent 𝔽q β)] + (h : ℓ + R_rate < r) (a : Fin (2 ^ ℓ) → L) : + computableAdditiveNTT (𝔽q := 𝔽q) (β := β) h a = additiveNTT 𝔽q β h a := + computableAdditiveNTT_eq_additiveNTT β h a + +example {r ℓ R_rate : ℕ} [NeZero r] + {L 𝔽q : Type} [Field L] [Fintype L] [DecidableEq L] + [Field 𝔽q] [Fintype 𝔽q] [Algebra 𝔽q L] [Fact (Fintype.card 𝔽q = 2)] + [Fact (Nat.Prime (ringChar 𝔽q))] + (β : Fin r → L) [Fact (LinearIndependent 𝔽q β)] + (h : ℓ + R_rate < r) (a : Fin (2 ^ ℓ) → L) : + arrayToFinFunction (2 ^ (ℓ + R_rate)) (computableAdditiveNTTFast β h a) = + additiveNTT 𝔽q β h a := + computableAdditiveNTTFast_eq_additiveNTT β h a + +end CompPolyTests.AdditiveNTTExecutable diff --git a/tests/CompPolyTests/Fields/Binary/BF64.lean b/tests/CompPolyTests/Fields/Binary/BF64.lean new file mode 100644 index 00000000..9424bd6b --- /dev/null +++ b/tests/CompPolyTests/Fields/Binary/BF64.lean @@ -0,0 +1,131 @@ +/- +Copyright (c) 2026 CompPoly Contributors. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Nicolas Schleicher +-/ +module + +public meta import CompPoly.Fields.Binary.BF64 +public import CompPoly.Fields.Binary.BF64 + +/-! +# `GF(2^64)` and `GF(2^192)` regression tests + +Differential vectors for the polynomial-basis `GF(2^64)` of +`CompPoly/Fields/Binary/BF64/` and its cubic extension, plus guards that the definitions +are not degenerate. + +The vectors are independently produced reference values for this modulus and basis. They +are what distinguishes a correct port from a plausible one: a wrong modulus, a wrong +reduction constant, or a wrong extension coefficient vector all compile cleanly and +silently give a *different* field, but fail these. + +Base-field vectors are checked with `decide +kernel`, so the kernel evaluates the +carry-less product and the reduction. Extension vectors use `#guard`, which runs the +*compiled* arithmetic at elaboration time, following `CompPolyTests.Fields.Extension`: +that is deliberate, since a `Field` instance that regressed to noncomputable would fail +the build rather than pass silently. +-/ + +public meta section + +namespace CompPolyTests.Fields.Binary.BF64Poly + +open _root_.BF64 + +/-! ## Base-field differential vectors -/ + +/-- Reference `(a, b, a * b)` triples for `GF(2^64)` multiplication. -/ +def multiplicationVectors : List (BitVec 64 × BitVec 64 × BitVec 64) := + [(0x01090913877ed8ed, 0x66ab35ac2768468f, 0x50c4519dc383744a), + (0xa7715ae18f12a3b5, 0x05743059f43fa4f5, 0xeb64cd9cd9cda6df), + (0xbd3efb4705e79ddd, 0x3aff618604de4ae0, 0xc3d7a95fa9cb59bb)] + +/-- Multiplication agrees with the reference on every vector. + +This also pins computability: the kernel has to evaluate the carry-less product and the +reduction to check it. -/ +theorem multiplication_matches_reference : + multiplicationVectors.all + (fun v => reduce (BinaryField.carryLessMul (w := 128) v.1 v.2.1) == v.2.2) = true := by + decide +kernel + +/-! ## Finiteness without runtime enumeration -/ + +example : Finite _root_.BF64 := inferInstance +example : Finite Ext3 := inferInstance +example : Nat.card _root_.BF64 = 2 ^ 64 := nat_card_bf64 +example : Nat.card Ext3 = 2 ^ 192 := nat_card_ext3 + +example : True := by + fail_if_success + let _ : Fintype _root_.BF64 := inferInstance + fail_if_success + let _ : Fintype Ext3 := inferInstance + trivial + +example [Fintype _root_.BF64] : Fintype.card _root_.BF64 = 2 ^ 64 := card_bf64 +example [Fintype Ext3] : Fintype.card Ext3 = 2 ^ 192 := card_ext3 + +/-! ## Non-vacuity guards -/ + +/-- The polynomial element `X`, encoded by bit one, is not zero. -/ +theorem generator_ne_zero : (ofBitVec (2#64)) ≠ 0 := by decide +kernel + +/-- The polynomial element `X` is distinct from the constant polynomial one. -/ +theorem generator_ne_one : (ofBitVec (2#64)) ≠ 1 := by decide +kernel + +/-- Multiplication by one is the identity on a sample element, so `reduce` is not +collapsing everything to a constant. -/ +theorem one_mul_sample : + ((1 : _root_.BF64) * ofBitVec (0x01090913877ed8ed#64)) = + ofBitVec (0x01090913877ed8ed#64) := by + rw [_root_.BF64.mul_def]; decide +kernel + +/-- A product that genuinely wraps: the reduction is exercised, not bypassed. +`x^63 * x = x^64 ≡ x^4 + x^3 + x + 1 = 0x1B`. -/ +theorem reduction_is_exercised : + (ofBitVec (0x8000000000000000#64) * ofBitVec (2#64)) = ofBitVec (0x1B#64) := by + rw [_root_.BF64.mul_def]; decide +kernel + +/-! ## Extension-field vectors + +These use `#guard`, which runs the *compiled* arithmetic, so they fail the build if an +instance ever regresses to noncomputable. +-/ + +section Vectors + +open CompPoly.Extension + +private def limbs (c0 c1 c2 : BitVec 64) : Ext3 := + Ext.ofFn (fun i => ofBitVec (if (i : ℕ) = 0 then c0 else if (i : ℕ) = 1 then c1 else c2)) + +/-- The adjoined root `y`. -/ +private def y : Ext3 := limbs 0 1 0 + +-- The defining relation `y^3 = y + 1`. +#guard y * y * y == y + 1 + +-- First reference vector: a product and a square. +#guard limbs 0x950e87d7f5606615 0x2c61275c9e6b6cf8 0x1f00bca0042db923 + * limbs 0x6dbca290a9eab706 0x4c10a4fe30cffdda 0xf26fff4cc4fd394d + == limbs 0x888a0fc35abaf5f6 0x68a84cbc132b0649 0x9fdeaf613003cabe + +#guard limbs 0x950e87d7f5606615 0x2c61275c9e6b6cf8 0x1f00bca0042db923 + * limbs 0x950e87d7f5606615 0x2c61275c9e6b6cf8 0x1f00bca0042db923 + == limbs 0x8fba131ad5d46b8c 0x1c170457f537a805 0x3632cc098ca15135 + +-- Second reference vector. +#guard limbs 0x6814a2bc786a6d2d 0xa26b351e6c8042c5 0x54760e7fbc051c6c + * limbs 0xd4c08880a5a4666d 0x29610ae0eed8f1e7 0xc34bd8e2fe5213e5 + == limbs 0x2ad322ebf2f9043b 0x8ac800aa67154c80 0x6d0f76651d3c4d0c + +-- Inversion evaluates in both fields. +#guard ofBitVec (0x01090913877ed8ed#64) * (ofBitVec (0x01090913877ed8ed#64))⁻¹ == 1 +#guard (0 : _root_.BF64)⁻¹ == 0 +#guard y * y⁻¹ == 1 + +end Vectors + +end CompPolyTests.Fields.Binary.BF64Poly diff --git a/tests/CompPolyTests/Fields/Binary/BF64/Presentation.lean b/tests/CompPolyTests/Fields/Binary/BF64/Presentation.lean new file mode 100644 index 00000000..194e7c92 --- /dev/null +++ b/tests/CompPolyTests/Fields/Binary/BF64/Presentation.lean @@ -0,0 +1,75 @@ +/- +Copyright (c) 2026 CompPoly Contributors. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: CompPoly Contributors +-/ +module + +public meta import CompPoly.Fields.Binary.BF64.Impl +public meta import CompPoly.Fields.Binary.Tower.Fast +public import CompPoly.Fields.Binary.BF64.Impl +public import CompPoly.Fields.Binary.Tower.Fast + +/-! +# Polynomial-basis field presentation regressions + +BF64 elements require explicit conversion to raw words and cannot be reinterpreted as +binary-tower elements. The same raw word has different multiplication in the two fields; +field numerals and raw polynomial-basis coordinates remain distinct. +-/ + +public meta section + +namespace CompPolyTests.BF64Presentation + +open BF64 ConcreteBinaryTower + +example (_x : BF64) : True := by + fail_if_success + let _word : BitVec 64 := _x + fail_if_success + let _tower : ConcreteBTField 6 := _x + trivial + +example (_word : BitVec 64) (_tower : ConcreteBTField 6) : True := by + fail_if_success + let _x : BF64 := _word + fail_if_success + let _x : BF64 := _tower + trivial + +example (_x : BF64) (_y : ConcreteBTField 6) : True := by + fail_if_success + let _z := @Mul.mul BF64 inferInstance _x _y + fail_if_success + let _z := @Mul.mul (ConcreteBTField 6) inferInstance _y _x + trivial + +example (b : BitVec 64) : (ofBitVec b).toBitVec = b := rfl +example (x : BF64) : ofBitVec x.toBitVec = x := rfl +example : LawfulBEq BF64 := inferInstance + +-- Importing the field leaves raw-word arithmetic at its ordinary modular operations. +example : (inferInstance : Mul (BitVec 64)) = BitVec.instMul := rfl +#guard ((0x8000000000000000#64) * (2#64)).toNat == 0 + +-- A field numeral denotes repeated addition of one, not a polynomial-basis word. +#guard (2 : BF64) == 0 +#guard (0xffffffffffffffff : BF64) == 1 +#guard (0xfffffffffffffffe : BF64) == 0 +#guard ofBitVec (2#64) != 0 + +-- The same coordinates do not identify the two multiplication laws. +#guard ((ofBitVec (2#64)) * ofBitVec (2#64)).toBitVec == 4#64 +#guard (concrete_mul (k := 6) (2#64) (2#64)).toNat == 3 + +-- Generic field code retains the canonical executable operations and casts. +example (x y : BF64) : (inferInstance : Field BF64).mul x y = BF64.mul x y := rfl +example (x : BF64) : (inferInstance : Field BF64).inv x = invItohTsujii x := rfl +example (x y : BF64) : (inferInstance : Field BF64).div x y = x * invItohTsujii y := rfl +example (x : BF64) (n : ℕ) : + (inferInstance : Field BF64).npow n x = npowBinRec n x := rfl +example (n : ℕ) : (inferInstance : Field BF64).natCast n = (n : BF64) := rfl +example (n : ℤ) : (inferInstance : Field BF64).intCast n = (n : BF64) := rfl + +end CompPolyTests.BF64Presentation diff --git a/tests/CompPolyTests/Fields/Binary/Common.lean b/tests/CompPolyTests/Fields/Binary/Common.lean new file mode 100644 index 00000000..4d5e8921 --- /dev/null +++ b/tests/CompPolyTests/Fields/Binary/Common.lean @@ -0,0 +1,65 @@ +/- +Copyright (c) 2026 CompPoly. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Dimitris Mitsios +-/ +module + +public meta import CompPoly.Fields.Binary.Common + +/-! +# Carryless Multiplication Regression Tests + +`carryLessMul` replaced a `Finset.fold` over `Fin 256` that took `B256` inputs. +The removed implementation is kept here as `clMulBaseline`, generalized to the +operand width, and the two are required to agree, so the replacement stays +pinned to the behaviour it replaced. + +Both instantiations in use are covered: `clMul`, the 128-bit one the GHASH +development multiplies through, and the 64-bit one behind `BF64.mul`. The +baseline takes operands already widened to the result width, which is where the +original fold met them. +-/ + +public meta section + +namespace CompPolyTests.Fields.Binary + +open BinaryField + +/-- The removed implementation: a `Finset.fold` over the operand width, on +inputs already widened to the result width. -/ +private def clMulBaseline {w : ℕ} (a b : BitVec w) : BitVec w := + (Finset.univ : Finset (Fin w)).fold BitVec.xor 0 + (fun i => if a.getLsb i then b <<< i.val else 0) + +private def denseA : B128 := (0xDEADBEEFCAFEBABE0123456789ABCDEF : B128) +private def denseB : B128 := (0xFEEDFACEFEEDFACE1122334455667788 : B128) + +/-- A sparse operand of the shape `fold_step` produces as `R_val`. -/ +private def sparseB : B128 := (0x87 : B128) + +#guard clMul denseA denseB == clMulBaseline (to256 denseA) (to256 denseB) +#guard clMul denseA sparseB == clMulBaseline (to256 denseA) (to256 sparseB) +#guard clMul denseA 1 == to256 denseA +#guard clMul denseA 0 == 0 + +/-! ## The 64-bit instance + +`BF64.mul` is `reduce (carryLessMul (w := 128) a b)`, so the width-64 operand +case carries the same obligation as the width-128 one above. `0x1B` is the low +part of the `GF(2^64)` modulus, a sparse operand of the shape `reduce` feeds +back in. -/ + +private def dense64A : BitVec 64 := (0x0123456789ABCDEF : BitVec 64) +private def dense64B : BitVec 64 := (0xFEEDFACECAFEBABE : BitVec 64) +private def sparse64B : BitVec 64 := (0x1B : BitVec 64) + +#guard carryLessMul (w := 128) dense64A dense64B + == clMulBaseline (zeroExtendTo (w := 128) dense64A) (zeroExtendTo (w := 128) dense64B) +#guard carryLessMul (w := 128) dense64A sparse64B + == clMulBaseline (zeroExtendTo (w := 128) dense64A) (zeroExtendTo (w := 128) sparse64B) +#guard carryLessMul (w := 128) dense64A 1 == zeroExtendTo (w := 128) dense64A +#guard carryLessMul (w := 128) dense64A 0 == 0 + +end CompPolyTests.Fields.Binary diff --git a/tests/CompPolyTests/Fields/Binary/CommonBench.lean b/tests/CompPolyTests/Fields/Binary/CommonBench.lean deleted file mode 100644 index 3b4ae391..00000000 --- a/tests/CompPolyTests/Fields/Binary/CommonBench.lean +++ /dev/null @@ -1,100 +0,0 @@ -/- -Copyright (c) 2026 CompPoly. All rights reserved. -Released under Apache 2.0 license as described in the file LICENSE. -Authors: Dimitris Mitsios --/ -module - -public meta import CompPoly.Fields.Binary.Common - -/-! - # Benchmarks on new `clMul` vs. the old implementation - - The new `clMul : B128 → B128 → B256` uses `Fin.foldl` and widens with - `to256 b`. The old implementation was removed from Common.lean; it is - included below as `clMul_baseline : B256 → B256 → B256`. - - This file is not imported by `CompPolyTests.lean` so `lake test` stays - fast. - - ## Running - - ```bash - lake build CompPolyTests.Fields.Binary.CommonBench - ``` --/ - -public meta section - -open BinaryField - -/-- Old baseline: `Finset.fold` over `Fin 256`, takes `B256` inputs. -/ -private def clMul_baseline (a b : B256) : B256 := - (Finset.univ : Finset (Fin 256)).fold BitVec.xor 0 - (fun i => if a.getLsb i then b <<< i.val else 0) - -/-! ## Correctness — baseline and new version must agree -/ - -private def tv_a : B128 := (0xDEADBEEFCAFEBABE0123456789ABCDEF : B128) -private def tv_b : B128 := (0xFEEDFACEFEEDFACE1122334455667788 : B128) - --- Reference: widen then baseline -private def ref_result : B256 := clMul_baseline (to256 tv_a) (to256 tv_b) - -#guard clMul tv_a tv_b == ref_result - --- Sparse operand (like `R_val` in `fold_step`) -private def tv_sparse : B128 := (0x87 : B128) -private def ref_sparse : B256 := clMul_baseline (to256 tv_a) (to256 tv_sparse) - -#guard clMul tv_a tv_sparse == ref_sparse - --- Identity and zero -#guard clMul tv_a 1 == to256 tv_a -#guard clMul tv_a 0 == 0 - -/-! ## Benchmarks — fixed dense inputs -/ - -private def benchFixedBaseline (n : Nat) (a b : B256) : IO B256 := do - let mut r : B256 := 0 - for _ in List.range n do - r := clMul_baseline a b - return r - -private def benchFixedNew (n : Nat) (a b : B128) : IO B256 := do - let mut r : B256 := 0 - for _ in List.range n do - r := clMul a b - return r - -#eval timeit "=== Fixed dense 128x128 (10000 iters) ===" (pure ()) -#eval timeit " clMul_baseline(to256)" (benchFixedBaseline 10000 (to256 tv_a) (to256 tv_b)) -#eval timeit " clMul (new) " (benchFixedNew 10000 tv_a tv_b) - -/-! ## Benchmarks — varied inputs -/ - -private def benchVariedBaseline (n : Nat) (a b : B256) : IO B256 := do - let mut r : B256 := 0 - for i in List.range n do - let a' := a ^^^ (BitVec.ofNat 256 (i * 0x9E3779B97F4A7C15)) - let b' := b ^^^ (BitVec.ofNat 256 (i * 0x6C62272E07BB0142)) - r := clMul_baseline a' b' - return r - -private def benchVariedNew (n : Nat) (a b : B128) : IO B256 := do - let mut r : B256 := 0 - for i in List.range n do - let a' := a ^^^ (BitVec.ofNat 128 (i * 0x9E3779B97F4A7C15)) - let b' := b ^^^ (BitVec.ofNat 128 (i * 0x6C62272E07BB0142)) - r := clMul a' b' - return r - -#eval timeit "=== Varied inputs 128x128 (10000 iters) ===" (pure ()) -#eval timeit " clMul_baseline(to256)" (benchVariedBaseline 10000 (to256 tv_a) (to256 tv_b)) -#eval timeit " clMul (new) " (benchVariedNew 10000 tv_a tv_b) - -/-! ## Benchmarks — sparse inputs (128×8 bit, like fold_step) -/ - -#eval timeit "=== Sparse 128x8 bit (10000 iters) ===" (pure ()) -#eval timeit " clMul_baseline(to256)" (benchFixedBaseline 10000 (to256 tv_a) (to256 tv_sparse)) -#eval timeit " clMul (new) " (benchFixedNew 10000 tv_a tv_sparse) diff --git a/tests/CompPolyTests/Fields/Binary/Tower/Algebra.lean b/tests/CompPolyTests/Fields/Binary/Tower/Algebra.lean new file mode 100644 index 00000000..11e0d272 --- /dev/null +++ b/tests/CompPolyTests/Fields/Binary/Tower/Algebra.lean @@ -0,0 +1,57 @@ +/- +Copyright (c) 2026 CompPoly Contributors. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: CompPoly Contributors +-/ +module + +import CompPoly.Fields.Binary.Tower.Abstract.Algebra +import CompPoly.Fields.Binary.Tower.Concrete.Algebra + +/-! +# Binary tower algebra regression tests + +The abstract and concrete binary towers satisfy the self-map identity law at symbolic levels. +Adjacent maps are the canonical embeddings, and each map from level `k` to level `k + 2` +is the composite of two canonical embeddings. All checks quantify over arbitrary field elements. +-/ + +namespace CompPolyTests.BinaryTowerAlgebra + +open BinaryTower ConcreteBinaryTower + +example (k : ℕ) (h : k ≤ k) (x : BTField k) : + AlgebraTower.algebraMap (AT := BTField) k k h x = x := by + simp only [AlgebraTower.algebraMap_self_apply] + +example (k : ℕ) (h : k ≤ k) (x : ConcreteBTField k) : + AlgebraTower.algebraMap (AT := ConcreteBTField) k k h x = x := by + simp only [AlgebraTower.algebraMap_self_apply] + +example (k : ℕ) (x : BTField k) : + AlgebraTower.algebraMap (AT := BTField) k (k + 1) (by omega) x = + canonicalEmbedding k x := by + change towerAlgebraMap k (k + 1) _ x = _ + rw [towerAlgebraMap_succ_1] + +example (k : ℕ) (x : ConcreteBTField k) : + AlgebraTower.algebraMap (AT := ConcreteBTField) k (k + 1) (by omega) x = + canonicalAlgMap k x := by + change concreteTowerAlgebraMap k (k + 1) _ x = _ + rw [concreteTowerAlgebraMap_succ_1] + +example (k : ℕ) (x : BTField k) : + AlgebraTower.algebraMap (AT := BTField) k (k + 2) (by omega) x = + canonicalEmbedding (k + 1) (canonicalEmbedding k x) := by + change towerAlgebraMap k (k + 2) _ x = _ + rw [towerAlgebraMap_assoc (k + 2) (k + 1) k (by omega) (by omega)] + rw [towerAlgebraMap_succ_1, towerAlgebraMap_succ_1, RingHom.comp_apply] + +example (k : ℕ) (x : ConcreteBTField k) : + AlgebraTower.algebraMap (AT := ConcreteBTField) k (k + 2) (by omega) x = + canonicalAlgMap (k + 1) (canonicalAlgMap k x) := by + change concreteTowerAlgebraMap k (k + 2) _ x = _ + rw [concreteTowerAlgebraMap_assoc (k + 2) (k + 1) k (by omega) (by omega)] + rw [concreteTowerAlgebraMap_succ_1, concreteTowerAlgebraMap_succ_1, RingHom.comp_apply] + +end CompPolyTests.BinaryTowerAlgebra diff --git a/tests/CompPolyTests/Fields/Binary/Tower/BasisCoordinates.lean b/tests/CompPolyTests/Fields/Binary/Tower/BasisCoordinates.lean new file mode 100644 index 00000000..4ac3edb9 --- /dev/null +++ b/tests/CompPolyTests/Fields/Binary/Tower/BasisCoordinates.lean @@ -0,0 +1,137 @@ +/- +Copyright (c) 2024 - 2025 ArkLib Contributors. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Chung Thai Nguyen, Quang Dao +-/ +module + +import CompPoly.Fields.Binary.Tower.Concrete.BasisCoordinates + +/-! +# Concrete basis-coordinate correspondence tests + +Symbolic clients exercise arbitrary endpoints and the selected concrete scalar action. +Asymmetric coefficients distinguish the numeric basis order across skipped levels. The +128-bit cases retain bit 127, and a raw-word multiplication counterexample distinguishes +the field action from modular bitvector multiplication. +-/ + +namespace CompPolyTests.BinaryTowerBasisCoordinates + +open ConcreteBinaryTower ConcreteBinaryTower.Coordinates + +example {i j : ℕ} (h : i ≤ j) : + let := ConcreteBTFieldAlgebra h + (inferInstance : Algebra (ConcreteBTField i) (ConcreteBTField j)) = + ConcreteBTFieldAlgebra h := rfl + +example {i j : ℕ} (h : i ≤ j) (a : ConcreteBTField i) (x : ConcreteBTField j) + (q : Fin (2 ^ (j - i))) : + let := ConcreteBTFieldAlgebra h + (multilinearBasis i j h).repr + (@SMul.smul _ _ (ConcreteBTFieldAlgebra h).toSMul a x) q = + a * coordinates h x q := by + let := ConcreteBTFieldAlgebra h + exact (multilinearBasis_repr h _ q).trans (coordinates_smul h a x q) + +example (i : ℕ) (h : i ≤ i) (x : ConcreteBTField i) + (q : Fin (2 ^ (i - i))) : + let := ConcreteBTFieldAlgebra h + letI : Module (ConcreteBTField i) (ConcreteBTField i) := + (ConcreteBTFieldAlgebra h).toModule + (multilinearBasis i i h).repr x q = x := by + let := ConcreteBTFieldAlgebra h + let : Module (ConcreteBTField i) (ConcreteBTField i) := + (ConcreteBTFieldAlgebra h).toModule + rw [multilinearBasis_repr, coordinates_self] + +example (i : ℕ) (h : i ≤ i) (q : Fin (2 ^ (i - i))) : + multilinearBasis i i h q = 1 := by + rw [← pack_single_eq_multilinearBasis, pack_self] + have hq : q = 0 := by + apply Fin.ext + have hq := q.isLt + simp only [Nat.sub_self, pow_zero] at hq + simpa only [Fin.val_zero] using (Nat.eq_zero_of_le_zero (Nat.le_of_lt_succ hq)) + rw [hq, Pi.single_eq_same] + +-- The two middle coefficients differ, so exchanging the generator order changes the result. +example : + let := ConcreteBTFieldAlgebra (show 1 ≤ 3 by decide) + (multilinearBasis 1 3 (by decide)).repr (fromNat (k := 3) 121) = + Finsupp.equivFunOnFinite.symm ![fromNat 1, fromNat 2, fromNat 3, fromNat 1] := by + let := ConcreteBTFieldAlgebra (show 1 ≤ 3 by decide) + ext q + rw [multilinearBasis_repr, Finsupp.coe_equivFunOnFinite_symm] + fin_cases q <;> rw [coordinates_eq_setWidth_ushiftRight] <;> decide +kernel + +example : + let := ConcreteBTFieldAlgebra (show 1 ≤ 3 by decide) + (multilinearBasis 1 3 (by decide)).repr (fromNat (k := 3) 121) 1 ≠ + (multilinearBasis 1 3 (by decide)).repr (fromNat (k := 3) 121) 2 := by + let := ConcreteBTFieldAlgebra (show 1 ≤ 3 by decide) + rw [multilinearBasis_repr, multilinearBasis_repr, + coordinates_eq_setWidth_ushiftRight, coordinates_eq_setWidth_ushiftRight] + decide +kernel + +example : multilinearBasis 1 3 (by decide) 2 = fromNat (k := 3) 16 := by + apply (coordinates (show 1 ≤ 3 by decide)).injective + rw [coordinates_multilinearBasis] + ext q + fin_cases q <;> rw [coordinates_eq_setWidth_ushiftRight] <;> decide +kernel + +example : + (∑ q : Fin 4, concreteTowerAlgebraMap 1 3 (by decide) + (![fromNat 1, fromNat 2, fromNat 3, fromNat 1] q) * + multilinearBasis 1 3 (by decide) q) = fromNat (k := 3) 121 := by + refine (pack_eq_sum_multilinearBasis (show 1 ≤ 3 by decide) + ![fromNat 1, fromNat 2, fromNat 3, fromNat 1]).symm.trans ?_ + apply (coordinates (show 1 ≤ 3 by decide)).injective + rw [coordinates_pack] + ext q + fin_cases q <;> rw [coordinates_eq_setWidth_ushiftRight] <;> decide +kernel + +example : + let := ConcreteBTFieldAlgebra (show 6 ≤ 7 by decide) + ((multilinearBasis 6 7 (by decide)).repr + (fromNat (k := 7) (2 ^ 127 + 9 * 2 ^ 64 + 2 ^ 63 + 5)) 1).getLsbD 63 = true := by + let := ConcreteBTFieldAlgebra (show 6 ≤ 7 by decide) + rw [multilinearBasis_repr, getLsbD_coordinates _ _ _ _ (by decide)] + decide +kernel + +example : + (∑ q : Fin 2, concreteTowerAlgebraMap 6 7 (by decide) + (![fromNat (2 ^ 63 + 5), fromNat (2 ^ 63 + 9)] q) * + multilinearBasis 6 7 (by decide) q) = + fromNat (k := 7) (2 ^ 127 + 9 * 2 ^ 64 + 2 ^ 63 + 5) := by + refine (pack_eq_sum_multilinearBasis (show 6 ≤ 7 by decide) + ![fromNat (2 ^ 63 + 5), fromNat (2 ^ 63 + 9)]).symm.trans ?_ + apply (coordinates (show 6 ≤ 7 by decide)).injective + rw [coordinates_pack] + ext q + fin_cases q <;> rw [coordinates_eq_setWidth_ushiftRight] <;> decide +kernel + +private theorem scalar_word : + @SMul.smul _ _ (ConcreteBTFieldAlgebra (Nat.le_succ 1)).toSMul + (fromNat (k := 1) 3) (fromNat (k := 2) 13) = fromNat (k := 2) 11 := by + change concreteTowerAlgebraMap 1 2 (Nat.le_succ 1) (fromNat (k := 1) 3) * + fromNat (k := 2) 13 = _ + rw [concreteTowerAlgebraMap_succ_1] + change concrete_mul (fromNat (k := 2) 3) (fromNat (k := 2) 13) = _ + simp only [concrete_mul.eq_1, Nat.reduceEqDiff, Nat.reduceSub, ↓reduceDIte] + decide +kernel + +-- The field action gives raw word 11; modular multiplication gives raw word 7. +example : + let := ConcreteBTFieldAlgebra (Nat.le_succ 1) + (multilinearBasis 1 2 (Nat.le_succ 1)).repr + (@SMul.smul _ _ (ConcreteBTFieldAlgebra (Nat.le_succ 1)).toSMul + (fromNat (k := 1) 3) (fromNat (k := 2) 13)) 1 ≠ + (multilinearBasis 1 2 (Nat.le_succ 1)).repr + (BitVec.mul (fromNat (k := 2) 3) (fromNat (k := 2) 13)) 1 := by + let := ConcreteBTFieldAlgebra (Nat.le_succ 1) + rw [scalar_word, multilinearBasis_repr, multilinearBasis_repr, + coordinates_eq_setWidth_ushiftRight, coordinates_eq_setWidth_ushiftRight] + decide +kernel + +end CompPolyTests.BinaryTowerBasisCoordinates diff --git a/tests/CompPolyTests/Fields/Binary/Tower/ConcreteImports.lean b/tests/CompPolyTests/Fields/Binary/Tower/ConcreteImports.lean new file mode 100644 index 00000000..ca4f6fdb --- /dev/null +++ b/tests/CompPolyTests/Fields/Binary/Tower/ConcreteImports.lean @@ -0,0 +1,25 @@ +/- +Copyright (c) 2026 CompPoly Contributors. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: CompPoly Contributors +-/ +module + +import CompPoly.Fields.Binary.Tower.Concrete.BasisCoordinates + +/-! +# Concrete binary tower import boundary + +The concrete field, basis, and coordinate APIs must be usable without loading the abstract +tower construction. The basis-coordinate correspondence imports all of these concrete layers. +-/ + +namespace CompPolyTests.ConcreteTowerImports + +open Lean Elab Command in +run_cmd do + for name in (← getEnv).header.moduleNames do + if name.toString.startsWith "CompPoly.Fields.Binary.Tower.Abstract." then + throwError "Concrete tower API imported an abstract tower module: {name}" + +end CompPolyTests.ConcreteTowerImports diff --git a/tests/CompPolyTests/Fields/Binary/Tower/Coordinates.lean b/tests/CompPolyTests/Fields/Binary/Tower/Coordinates.lean new file mode 100644 index 00000000..e81a1fc4 --- /dev/null +++ b/tests/CompPolyTests/Fields/Binary/Tower/Coordinates.lean @@ -0,0 +1,115 @@ +/- +Copyright (c) 2024 - 2025 ArkLib Contributors. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Chung Thai Nguyen, Quang Dao +-/ +module + +import CompPoly.Fields.Binary.Tower.Concrete.Coordinates +import CompPoly.Data.RingTheory.AlgebraTower.Coordinates + +/-! +# Concrete binary tower coordinate tests + +Symbolic clients exercise successor coordinates and the generic endpoint API with the actual +concrete tower scalar actions. Asymmetric raw words distinguish low-first order, retain both +64-bit halves, and distinguish the field scalar action from modular bitvector multiplication. +-/ + +namespace CompPolyTests.BinaryTowerCoordinates + +open ConcreteBinaryTower ConcreteBinaryTower.Coordinates AlgebraTower + +-- The coordinate adapter retains the existing field and predecessor algebra dictionaries. +example (k : ℕ) : (inferInstance : Field (ConcreteBTField k)) = instFieldConcrete := rfl + +example (k : ℕ) : + (inferInstance : Algebra (ConcreteBTField k) (ConcreteBTField (k + 1))) = + ConcreteBTFieldAlgebra (Nat.le_succ k) := rfl + +example (k : ℕ) (x : ConcreteBTField (k + 1)) (c : Fin 2 → ConcreteBTField k) : + (succCoordinates k).symm (succCoordinates k x) = x ∧ + succCoordinates k ((succCoordinates k).symm c) = c := + ⟨(succCoordinates k).symm_apply_apply x, (succCoordinates k).apply_symm_apply c⟩ + +example (k : ℕ) (x : ConcreteBTField (k + 1)) (j : Fin 2) : + natCoordinates succCoordinates k 1 x j = succCoordinates k x j := by + rw [natCoordinates_succ, natCoordinates_zero] + fin_cases j <;> rfl + +example {i j : ℕ} (h : i ≤ j) (x : ConcreteBTField j) : + natPackOfLE succCoordinates h (natCoordinatesOfLE succCoordinates h x) = x := + natPackOfLE_natCoordinatesOfLE succCoordinates h x + +example {i j : ℕ} (h : i ≤ j) + (c : Fin (coordinateSize (fun _ => 2) i (j - i)) → ConcreteBTField i) : + natCoordinatesOfLE succCoordinates h (natPackOfLE succCoordinates h c) = c := + natCoordinatesOfLE_natPackOfLE succCoordinates h c + +example {i j : ℕ} (h : i ≤ j) (a : ConcreteBTField i) (x : ConcreteBTField j) : + natCoordinatesOfLE succCoordinates h + (@SMul.smul _ _ (ConcreteBTFieldAlgebra h).toSMul a x) = + a • natCoordinatesOfLE succCoordinates h x := + (natCoordinatesOfLE succCoordinates h).map_smul a x + +example (i : ℕ) (h : i ≤ i) (a x : ConcreteBTField i) + (j : Fin (coordinateSize (fun _ => 2) i (i - i))) : + natCoordinatesOfLE succCoordinates h + (@SMul.smul _ _ (ConcreteBTFieldAlgebra h).toSMul a x) j = a * x := by + rw [natCoordinatesOfLE_self] + change concreteTowerAlgebraMap i i h a * x = a * x + rw [concreteTowerAlgebraMap_id] + rfl + +example (k : ℕ) (x : ConcreteBTField (k + 2)) : + (natCoordinatesConstOfLE succCoordinates (show k ≤ k + 2 by omega)).symm + (natCoordinatesConstOfLE succCoordinates (show k ≤ k + 2 by omega) x) = x := + (natCoordinatesConstOfLE succCoordinates (show k ≤ k + 2 by omega)).symm_apply_apply x + +-- Two successor steps keep the old coordinate index fastest. +example : natCoordinatesConstOfLE succCoordinates (show 1 ≤ 3 by decide) + (fromNat (k := 3) 121) = ![fromNat 1, fromNat 2, fromNat 3, fromNat 1] := by + ext j + fin_cases j <;> + simp only [natCoordinatesConstOfLE_apply, natCoordinatesOfLE_apply, + natCoordinates_succ, natCoordinates_zero, succCoordinates_apply] <;> + decide +kernel + +-- Word nine has different constant and generator coefficients; reversing them is detectable. +example : succCoordinates 1 (fromNat (k := 2) 9) = ![fromNat 1, fromNat 2] := by + rw [succCoordinates_apply] + decide +kernel + +example : succCoordinates 1 (fromNat (k := 2) 9) ≠ ![fromNat 2, fromNat 1] := by + rw [succCoordinates_apply] + decide +kernel + +example : ((succCoordinates 1).symm ![fromNat 1, fromNat 2]).toNat = 9 := by + rw [succCoordinates_symm_apply] + decide +kernel + +example : (joinSucc 6 (fromNat (2 ^ 63 + 5)) (fromNat (2 ^ 62 + 9))).toNat = + 2 ^ 126 + 9 * 2 ^ 64 + 2 ^ 63 + 5 := by + decide +kernel + +-- The tower field action gives 14; multiplying the raw bitvectors would give 2 instead. +private theorem scalar_word : + @SMul.smul _ _ (ConcreteBTFieldAlgebra (Nat.le_succ 1)).toSMul + (fromNat (k := 1) 2) (fromNat (k := 2) 9) = fromNat (k := 2) 14 := by + change concreteTowerAlgebraMap 1 2 (Nat.le_succ 1) (fromNat (k := 1) 2) * + fromNat (k := 2) 9 = _ + rw [concreteTowerAlgebraMap_succ_1] + change concrete_mul (fromNat (k := 2) 2) (fromNat (k := 2) 9) = _ + simp only [concrete_mul.eq_1, Nat.reduceEqDiff, Nat.reduceSub, ↓reduceDIte] + decide +kernel + +example : succCoordinates 1 + (@SMul.smul _ _ (ConcreteBTFieldAlgebra (Nat.le_succ 1)).toSMul + (fromNat (k := 1) 2) (fromNat (k := 2) 9)) = ![fromNat 2, fromNat 3] := by + rw [scalar_word, succCoordinates_apply] + decide +kernel + +example : (2 : ConcreteBTField 1) ≠ fromNat (k := 1) 2 := by + decide +kernel + +end CompPolyTests.BinaryTowerCoordinates diff --git a/tests/CompPolyTests/Fields/Binary/Tower/Fast.lean b/tests/CompPolyTests/Fields/Binary/Tower/Fast.lean new file mode 100644 index 00000000..aa596ab5 --- /dev/null +++ b/tests/CompPolyTests/Fields/Binary/Tower/Fast.lean @@ -0,0 +1,98 @@ +/- +Copyright (c) 2026 CompPoly Contributors. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Georgios Raikos +-/ +module + +public meta import CompPoly.Fields.Binary.Tower.Fast + +/-! +# Fast Binary Tower Tests + +Regression checks for the packed-word tower: word-level guards cross-validated against +the concrete tower, plus `Field` instance coverage at each width. +-/ + +public meta section + +namespace ConcreteBinaryTower.Fast + +-- Additive structure +#guard ((ofNat 6 0xDEAD + ofNat 6 0xBEEF).val = (0xDEAD ^^^ 0xBEEF : UInt64)) +#guard (ofNat 6 0xDEAD + ofNat 6 0xDEAD : BT64) = 0 +#guard (0 + ofNat 5 0x12345678 : BT32) = ofNat 5 0x12345678 + +-- Multiplication, cross-checked against `concrete_mul` +#guard ((ofNat 3 0xAB * ofNat 3 0x3D : BT8)).val = 0xCF +#guard ((ofNat 4 0xABCD * ofNat 4 0x1234 : BT16)).val = 0xCF0C +#guard ((ofNat 5 0xDEADBEEF * ofNat 5 0x12345678 : BT32)).val = 0x94E989A6 +#guard ((ofNat 6 0xDEADBEEFCAFEBABE * ofNat 6 0x123456789ABCDEF0 : BT64)).val + = 0x4AE10FB8464BA9F5 +#guard ((ofNat 6 0xDEADBEEFCAFEBABE * 1 : BT64)).val = 0xDEADBEEFCAFEBABE + +-- Generator multiplication, cross-checked against `concrete_mul (Z k) ·` +#guard (ofNat 3 0xAB).mulByZ.val = 0xDA +#guard (ofNat 6 0xDEADBEEFCAFEBABE).mulByZ.val = 0x04CF6413DEADBEEF + +-- Squaring and inversion at the word level, cross-checked against `concrete_inv` +#guard sq64 0xDEADBEEFCAFEBABE = 0x8459990BA3148442 +#guard inv64 0xDEADBEEFCAFEBABE = 0x94D7EC832FAF447F +#guard mul64 0xDEADBEEFCAFEBABE (inv64 0xDEADBEEFCAFEBABE) = 1 +#guard inv64 0 = 0 +#guard inv64 1 = 1 + +-- Field structure on the carrier: inversion, division, powers, squaring +#guard ((ofNat 6 0xDEADBEEFCAFEBABE : BT64)⁻¹).val = 0x94D7EC832FAF447F +#guard (ofNat 6 0xDEADBEEFCAFEBABE * (ofNat 6 0xDEADBEEFCAFEBABE : BT64)⁻¹) = 1 +#guard ((0 : BT64)⁻¹) = 0 +#guard ((1 : BT32)⁻¹) = 1 +#guard (ofNat 5 0xDEADBEEF / ofNat 5 0xDEADBEEF : BT32) = 1 +#guard ((ofNat 3 0xAB : BT8) ^ 3) = ofNat 3 0xAB * ofNat 3 0xAB * ofNat 3 0xAB +#guard (ofNat 6 0xDEADBEEFCAFEBABE).square.val = 0x8459990BA3148442 +#guard (ofNat 3 0xAB).square = ofNat 3 0xAB * ofNat 3 0xAB + +-- Powers agree with the concrete tower through `toConcrete` +#guard ((ofNat 6 0xDEADBEEFCAFEBABE : BT64) ^ 0) = 1 +#guard toConcrete ((ofNat 6 0xDEADBEEFCAFEBABE : BT64) ^ 17) + = toConcrete (ofNat 6 0xDEADBEEFCAFEBABE : BT64) ^ 17 +#guard toConcrete ((ofNat 6 0xDEADBEEFCAFEBABE : BT64) ^ 255) + = toConcrete (ofNat 6 0xDEADBEEFCAFEBABE : BT64) ^ 255 + +-- Numerals reduce mod the characteristic; raw packed values go through `ofNat` +#guard (5 : BT16) = 1 +#guard (2 : BT64) = 0 +#guard (-3 : BT8) = 1 + +-- Table rungs agree with the ladder on full-width words +#guard mul64T 0xDEADBEEFCAFEBABE 0x123456789ABCDEF0 + = mul64 0xDEADBEEFCAFEBABE 0x123456789ABCDEF0 +#guard inv64T 0xDEADBEEFCAFEBABE = inv64 0xDEADBEEFCAFEBABE +#guard sq64T 0xDEADBEEFCAFEBABE = sq64 0xDEADBEEFCAFEBABE +#guard mulByZ6T 0xDEADBEEFCAFEBABE = mulByZ6 0xDEADBEEFCAFEBABE + +-- Level 7, cross-checked against `concrete_mul` / `concrete_inv` at `k = 7` +private def a128 : FastBT128 := ⟨0xDEADBEEFCAFEBABE, 0x0123456789ABCDEF⟩ +private def b128 : FastBT128 := ⟨0x123456789ABCDEF0, 0xFEDCBA9876543210⟩ + +#guard (a128 * b128) = ⟨0x29A88537675DA9F5, 0x899953DAF02F7327⟩ +#guard a128.inv = ⟨0xDD97DC695DE13852, 0xAC3CB6A3957A8E7C⟩ +#guard a128.square = ⟨0x06D8181BCEC18442, 0xA547828182818110⟩ +#guard a128.mulByZ = ⟨0x0123456789ABCDEF, 0x21607223CBDDFFD9⟩ +#guard (a128 * a128.inv) = 1 +#guard (a128 + a128) = 0 +#guard (a128 * b128) = (b128 * a128) + +-- Field structure at level 7 +#guard (a128 * a128⁻¹) = 1 +#guard (a128 / b128) * b128 = a128 +#guard (a128 ^ 2) = a128 * a128 +#guard (5 : FastBT128) = 1 +#guard (2 : FastBT128) = 0 + +-- Ring equivalence round trips +#guard ringEquivBT8.symm (ringEquivBT8 (ofNat 3 0xAB)) = ofNat 3 0xAB +#guard ringEquivBT64.symm (ringEquivBT64 (ofNat 6 0xDEADBEEF)) = ofNat 6 0xDEADBEEF +#guard FastBT128.ringEquiv.symm (FastBT128.ringEquiv a128) = a128 + +end ConcreteBinaryTower.Fast diff --git a/tests/CompPolyTests/Fields/Binary/Tower/Norm.lean b/tests/CompPolyTests/Fields/Binary/Tower/Norm.lean new file mode 100644 index 00000000..16182512 --- /dev/null +++ b/tests/CompPolyTests/Fields/Binary/Tower/Norm.lean @@ -0,0 +1,63 @@ +/- +Copyright (c) 2026 CompPoly Contributors. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: CompPoly Contributors +-/ +module + +import CompPoly.Fields.Binary.Tower.Concrete.Field +import Mathlib.Algebra.QuadraticAlgebra.Basic +import Mathlib.Data.ZMod.Basic + +/-! +# Binary tower norm tests + +The irreducible quadratic over `ZMod 2` gives a valid norm certificate. Replacing its linear +coefficient with zero produces a nonzero pair of norm zero, so the no-root requirement is +necessary. Concrete field clients retain the existing arithmetic and inverse denominator. +-/ + +namespace CompPolyTests.BinaryTowerNorm + +open ConcreteBinaryTower + +-- The predecessor certificate applies to every nonzero element of the first extension. +example (a : ConcreteBTField 1) (ha : a ≠ 0) : + let hi := (split (show 1 > 0 by decide) a).1 + let lo := (split (show 1 > 0 by decide) a).2 + concrete_mul lo (lo + concrete_mul hi (Z 0)) + concrete_mul hi hi ≠ 0 := + norm_of_ne_zero_is_ne_zero (getBTFResult 0) a ha + +-- Field dictionaries still select the exact executable multiplication and inverse. +example (k : ℕ) (a b : ConcreteBTField k) : + a * b = concrete_mul a b ∧ a⁻¹ = concrete_inv a := ⟨rfl, rfl⟩ + +-- All three nonzero low/high pairs have norm one over the two-element base field. +example : + (concrete_mul (one (k := 0)) (one + concrete_mul zero (Z 0)) + + concrete_mul zero zero = one) ∧ + (concrete_mul (zero (k := 0)) (zero + concrete_mul one (Z 0)) + + concrete_mul one one = one) ∧ + (concrete_mul (one (k := 0)) (one + concrete_mul one (Z 0)) + + concrete_mul one one = one) := by + change (1 * (1 + 0 * 1) + 0 * 0 = (1 : ConcreteBTField 0)) ∧ + (0 * (0 + 1 * 1) + 1 * 1 = (1 : ConcreteBTField 0)) ∧ + (1 * (1 + 1 * 1) + 1 * 1 = (1 : ConcreteBTField 0)) + simp only [mul_zero, mul_one, add_zero, zero_add, add_self_cancel, and_self] + +-- The no-root fact has a concrete inhabitant, independently of the tower recursion. +example (x : QuadraticAlgebra (ZMod 2) (-1) 1) : + x.norm = 0 ↔ x = 0 := by + let : Fact (∀ r : ZMod 2, r ^ 2 ≠ -1 + 1 * r) := ⟨by decide⟩ + exact QuadraticAlgebra.norm_eq_zero_iff_eq_zero + +-- Omitting the no-root condition would make norm nonvanishing false. +example : + let x : QuadraticAlgebra (ZMod 2) (-1) 0 := ⟨1, 1⟩ + x ≠ 0 ∧ x.norm = 0 := by + simp [QuadraticAlgebra.ext_iff, QuadraticAlgebra.norm_def] + +example : ¬∀ r : ZMod 2, r ^ 2 ≠ -1 + 0 * r := by + decide + +end CompPolyTests.BinaryTowerNorm diff --git a/tests/CompPolyTests/Fields/Binary/Tower/RelativeCoordinates.lean b/tests/CompPolyTests/Fields/Binary/Tower/RelativeCoordinates.lean new file mode 100644 index 00000000..54471ea1 --- /dev/null +++ b/tests/CompPolyTests/Fields/Binary/Tower/RelativeCoordinates.lean @@ -0,0 +1,142 @@ +/- +Copyright (c) 2024 - 2025 ArkLib Contributors. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Chung Thai Nguyen, Quang Dao +-/ +module + +import CompPoly.Fields.Binary.Tower.Concrete.RelativeCoordinates + +/-! +# Concrete endpoint coordinate tests + +Symbolic clients check arbitrary endpoints and the chosen concrete tower actions. Raw-word +examples check low-first coefficient order, all 128 bits, and the distinction between field +multiplication and modular bitvector multiplication. A bit-index counterexample checks the +bound in bitwise readback. +-/ + +namespace CompPolyTests.BinaryTowerRelativeCoordinates + +open ConcreteBinaryTower ConcreteBinaryTower.Coordinates + +example (k : ℕ) : (inferInstance : Field (ConcreteBTField k)) = instFieldConcrete := rfl + +example {i j : ℕ} (h : i ≤ j) : + letI := ConcreteBTFieldAlgebra h + (inferInstance : Algebra (ConcreteBTField i) (ConcreteBTField j)) = + ConcreteBTFieldAlgebra h := rfl + +example {i j : ℕ} (h : i ≤ j) (x : ConcreteBTField j) + (c : Fin (2 ^ (j - i)) → ConcreteBTField i) : + pack h (coordinates h x) = x ∧ coordinates h (pack h c) = c := + ⟨pack_coordinates h x, coordinates_pack h c⟩ + +example (i : ℕ) (h : i ≤ i) (x : ConcreteBTField i) + (q : Fin (2 ^ (i - i))) : coordinates h x q = x := coordinates_self i h x q + +example (i : ℕ) (h : i ≤ i) (c : Fin (2 ^ (i - i)) → ConcreteBTField i) : + pack h c = c 0 := pack_self i h c + +example (k : ℕ) (x : ConcreteBTField (k + 1)) : + coordinates (Nat.le_succ k) x 0 = x.setWidth (2 ^ k) := by + rw [coordinates_eq_setWidth_ushiftRight] + simp only [Fin.val_zero, Nat.mul_zero, BitVec.ushiftRight_eq, BitVec.ushiftRight_zero] + +example (k : ℕ) (x : ConcreteBTField (k + 2)) (q : Fin (2 ^ (k + 2 - k))) : + coordinates (show k ≤ k + 2 by omega) x q = + (BitVec.ushiftRight x (2 ^ k * q.val)).setWidth (2 ^ k) := + coordinates_eq_setWidth_ushiftRight _ x q + +-- All operations here elaborate to the concrete field and the explicitly selected endpoint action. +example {i j : ℕ} (h : i ≤ j) (a : ConcreteBTField i) (x : ConcreteBTField j) + (q : Fin (2 ^ (j - i))) : + letI : Field (ConcreteBTField i) := instFieldConcrete + coordinates h (@SMul.smul _ _ (ConcreteBTFieldAlgebra h).toSMul a x) q = + a * coordinates h x q := coordinates_smul h a x q + +example {i j : ℕ} (h : i ≤ j) (a : ConcreteBTField i) + (c : Fin (2 ^ (j - i)) → ConcreteBTField i) : + pack h (fun q => a * c q) = + @SMul.smul _ _ (ConcreteBTFieldAlgebra h).toSMul a (pack h c) := pack_smul h a c + +example {i j : ℕ} (h : i ≤ j) (x y : ConcreteBTField j) : + coordinates h (x + y) = coordinates h x + coordinates h y := coordinates_add h x y + +example {i j : ℕ} (h : i ≤ j) (c d : Fin (2 ^ (j - i)) → ConcreteBTField i) : + pack h (c + d) = pack h c + pack h d := pack_add h c d + +example (i : ℕ) (h : i ≤ i) (a x : ConcreteBTField i) + (q : Fin (2 ^ (i - i))) : + coordinates h (@SMul.smul _ _ (ConcreteBTFieldAlgebra h).toSMul a x) q = a * x := by + calc + _ = a * coordinates h x q := coordinates_smul h a x q + _ = a * x := congrArg (fun z => a * z) (coordinates_self i h x q) + +-- Two successor steps retain the order [1, 2, 3, 1], including the old index varying fastest. +example : coordinates (show 1 ≤ 3 by decide) (fromNat (k := 3) 121) = + ![fromNat 1, fromNat 2, fromNat 3, fromNat 1] := by + ext q + fin_cases q <;> rw [coordinates_eq_setWidth_ushiftRight] <;> decide +kernel + +example : coordinates (show 1 ≤ 3 by decide) (fromNat (k := 3) 121) ≠ + ![fromNat 1, fromNat 3, fromNat 2, fromNat 1] := by + intro h + have h₁ := congrFun h 1 + rw [coordinates_eq_setWidth_ushiftRight] at h₁ + contradiction + +example : pack (show 1 ≤ 3 by decide) + ![fromNat 1, fromNat 2, fromNat 3, fromNat 1] = fromNat (k := 3) 121 := by + apply (coordinates (show 1 ≤ 3 by decide)).injective + rw [coordinates_pack] + ext q + fin_cases q <;> rw [coordinates_eq_setWidth_ushiftRight] <;> decide +kernel + +example : coordinates (show 6 ≤ 7 by decide) + (fromNat (k := 7) (2 ^ 127 + 9 * 2 ^ 64 + 2 ^ 63 + 5)) = + ![fromNat (2 ^ 63 + 5), fromNat (2 ^ 63 + 9)] := by + ext q + fin_cases q <;> rw [coordinates_eq_setWidth_ushiftRight] <;> decide +kernel + +example : (coordinates (show 6 ≤ 7 by decide) + (fromNat (k := 7) (2 ^ 127 + 9 * 2 ^ 64 + 2 ^ 63 + 5)) 1).getLsbD 63 = true := by + rw [getLsbD_coordinates _ _ _ _ (by decide)] + decide +kernel + +example : (coordinates (show 1 ≤ 3 by decide) (fromNat (k := 3) 121) 2).toNat = 3 := by + rw [toNat_coordinates] + decide +kernel + +-- Without the within-block bound, readback could incorrectly expose a neighboring block. +example : (coordinates (show 1 ≤ 2 by decide) (fromNat (k := 2) 4) 0).getLsbD 2 ≠ + (fromNat (k := 2) 4).getLsbD 2 := by + rw [coordinates_eq_setWidth_ushiftRight] + decide +kernel + +private theorem scalar_word : + @SMul.smul _ _ (ConcreteBTFieldAlgebra (Nat.le_succ 1)).toSMul + (fromNat (k := 1) 2) (fromNat (k := 2) 9) = fromNat (k := 2) 14 := by + change concreteTowerAlgebraMap 1 2 (Nat.le_succ 1) (fromNat (k := 1) 2) * + fromNat (k := 2) 9 = _ + rw [concreteTowerAlgebraMap_succ_1] + change concrete_mul (fromNat (k := 2) 2) (fromNat (k := 2) 9) = _ + simp only [concrete_mul.eq_1, Nat.reduceEqDiff, Nat.reduceSub, ↓reduceDIte] + decide +kernel + +example : coordinates (Nat.le_succ 1) + (@SMul.smul _ _ (ConcreteBTFieldAlgebra (Nat.le_succ 1)).toSMul + (fromNat (k := 1) 2) (fromNat (k := 2) 9)) = ![fromNat 2, fromNat 3] := by + rw [scalar_word] + ext q + fin_cases q <;> rw [coordinates_eq_setWidth_ushiftRight] <;> decide +kernel + +-- Modular bitvector multiplication would produce raw word two instead of field word fourteen. +example : @SMul.smul _ _ (ConcreteBTFieldAlgebra (Nat.le_succ 1)).toSMul + (fromNat (k := 1) 2) (fromNat (k := 2) 9) ≠ fromNat (k := 2) 2 := by + rw [scalar_word] + decide +kernel + +example : (2 : ConcreteBTField 1) ≠ fromNat (k := 1) 2 := by decide +kernel + +end CompPolyTests.BinaryTowerRelativeCoordinates diff --git a/tests/CompPolyTests/Fields/Binary/Tower/TensorAlgebra.lean b/tests/CompPolyTests/Fields/Binary/Tower/TensorAlgebra.lean new file mode 100644 index 00000000..89e2c7c8 --- /dev/null +++ b/tests/CompPolyTests/Fields/Binary/Tower/TensorAlgebra.lean @@ -0,0 +1,33 @@ +/- +Copyright (c) 2026 CompPoly Contributors. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: CompPoly Contributors +-/ +module + +import CompPoly.Fields.Binary.Tower.TensorAlgebra + +/-! +# Tensor action compatibility import regression + +The binary-tower import path exposes the generic basis helper without changing the +default scalar action on equal tensor factors. +-/ + +open scoped TensorProduct + +namespace CompPolyTests.BinaryTowerTensorAlgebra + +example {K L : Type*} [CommSemiring K] [CommSemiring L] [Algebra K L] : + algebraMap L (L ⊗[K] L) = Algebra.TensorProduct.includeLeftRingHom := rfl + +example {K L : Type*} [CommSemiring K] [CommSemiring L] [Algebra K L] : + (inferInstance : Module L (L ⊗[K] L)) = TensorProduct.leftModule := rfl + +example {K L : Type*} [CommSemiring K] [CommSemiring L] [Algebra K L] : + (inferInstance : DistribMulAction L (L ⊗[K] L)) = TensorProduct.leftDistribMulAction := rfl + +example {K L : Type*} [CommSemiring K] [CommSemiring L] [Algebra K L] : + (inferInstance : SMul L (L ⊗[K] L)) = TensorProduct.leftHasSMul := rfl + +end CompPolyTests.BinaryTowerTensorAlgebra diff --git a/tests/CompPolyTests/Fields/Extension/Certificates.lean b/tests/CompPolyTests/Fields/Extension/Certificates.lean new file mode 100644 index 00000000..65455265 --- /dev/null +++ b/tests/CompPolyTests/Fields/Extension/Certificates.lean @@ -0,0 +1,90 @@ +/- +Copyright (c) 2026 CompPoly Contributors. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: CompPoly Contributors +-/ +module + +public meta import CompPoly.Fields.KoalaBear.Ext4 +public meta import Mathlib.Algebra.Polynomial.SpecificDegree +public import CompPoly.Fields.KoalaBear.Ext4 +public import Mathlib.Algebra.Polynomial.SpecificDegree + +/-! +# Cardinality certificates for extension field laws + +An incorrect finite cardinality and the true cardinality zero of an infinite field each +fail field admission despite irreducibility. Valid finite certificates support iterated +extensions without supplying enumeration dictionaries. +-/ + +public meta section + +namespace CompPolyTests.ExtensionCertificates + +open CompPoly.Extension Polynomial + +private abbrev wrongCardinality : ExtensionParams KoalaBear.Field := + { KoalaBear.ext4Params.toExtensionParams with q := 0 } + +private instance : Fact (Irreducible wrongCardinality.poly) := ⟨by + change Irreducible KoalaBear.ext4Params.toExtensionParams.poly + exact Fact.out⟩ + +example : Nat.card KoalaBear.Field ≠ wrongCardinality.q := by + rw [Nat.card_eq_fintype_card, ZMod.card] + decide + +example : True := by + fail_if_success + let _ : Fact (Nat.card KoalaBear.Field = wrongCardinality.q) := inferInstance + fail_if_success + let _ : Field (Ext wrongCardinality) := inferInstance + trivial + +#guard Ext.inv (0 : Ext wrongCardinality) == 1 + +private abbrev infiniteBase : ExtensionParams Rat := ⟨2, by decide, #v[1, 0], 0⟩ + +private theorem infiniteBase_poly : infiniteBase.poly = X ^ 2 + 1 := by + simp [ExtensionParams.poly, infiniteBase, ExtensionParams.lowerCoeff, Fin.sum_univ_two] + +private instance : Fact (Nat.card Rat = infiniteBase.q) := ⟨Nat.card_eq_zero_of_infinite⟩ + +private instance : Fact (Irreducible infiniteBase.poly) := ⟨by + rw [infiniteBase_poly] + apply Polynomial.irreducible_of_degree_le_three_of_not_isRoot + · have hd : (X ^ 2 + 1 : Rat[X]).natDegree = 2 := by compute_degree! + rw [hd] + decide + · intro a ha + simp only [Polynomial.IsRoot, eval_add, eval_pow, eval_X, eval_one] at ha + nlinarith [sq_nonneg a]⟩ + +example : True := by + fail_if_success + let _ : Finite Rat := inferInstance + fail_if_success + let _ : Field (Ext infiniteBase) := inferInstance + trivial + +#guard Ext.inv (0 : Ext infiniteBase) == 1 + +section Iterated + +variable {F : Type*} [Field F] [Finite F] {P : ExtensionParams F} + [Fact (Nat.card F = P.q)] [Fact (Irreducible P.poly)] + +example : Finite (Ext P) := inferInstance +example : Nat.card (Ext P) = P.q ^ P.d := Ext.nat_card_ext + +variable {Q : ExtensionParams (Ext P)} [Fact (Nat.card (Ext P) = Q.q)] + [Fact (Irreducible Q.poly)] + +example : Field (Ext Q) := inferInstance +example : Finite (Ext Q) := inferInstance +example (x : Ext Q) : (inferInstance : Field (Ext Q)).inv x = Ext.inv x := rfl + +end Iterated + +end CompPolyTests.ExtensionCertificates diff --git a/tests/CompPolyTests/Fields/Extension/Presentation.lean b/tests/CompPolyTests/Fields/Extension/Presentation.lean new file mode 100644 index 00000000..7e0b13a0 --- /dev/null +++ b/tests/CompPolyTests/Fields/Extension/Presentation.lean @@ -0,0 +1,78 @@ +/- +Copyright (c) 2026 CompPoly Contributors. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: CompPoly Contributors +-/ +module + +public meta import CompPoly.Fields.Extension.Field +public import CompPoly.Fields.Extension.Field + +/-! +# Extension presentation regressions + +The quotients by `X² + 1` and `X² + X + 1` over `GF(2)` have the same coefficient-vector +length but different multiplication. Their elements require an explicit coordinate conversion. +The first quotient is a ring; no irreducibility or field claim is made about that modulus. +-/ + +public meta section + +namespace CompPolyTests.ExtensionPresentation + +open CompPoly.Extension + +/-- Parameters for the monic modulus `X² + 1` over `GF(2)`. -/ +private def first : ExtensionParams (ZMod 2) where + d := 2 + two_le := le_rfl + lower := #v[1, 0] + q := 2 + +/-- Parameters for the monic modulus `X² + X + 1` over `GF(2)`. -/ +private def second : ExtensionParams (ZMod 2) where + d := 2 + two_le := le_rfl + lower := #v[1, 1] + q := 2 + +example (_x : Ext first) : True := by + fail_if_success + let _y : Ext second := _x + trivial + +example (_x : Ext first) (_y : Ext second) : True := by + fail_if_success + let _z := Ext.mul (P := first) _x _y + trivial + +example (v : Vector (ZMod 2) first.d) : Ext.coeffs (Ext.ofVector (P := first) v) = v := by + simp + +example (x : Ext first) : Ext.ofVector (Ext.coeffs x) = x := by + simp + +-- Multiplication in each presentation uses its own modulus. +#guard Ext.coeff ((Ext.gen : Ext first) * Ext.gen) ⟨1, by decide⟩ == 0 + +#guard Ext.coeff ((Ext.gen : Ext second) * Ext.gen) ⟨1, by decide⟩ == 1 + +-- Boolean equality remains lawful on the wrapped carrier. +example : LawfulBEq (Ext first) := inferInstance + +section Operations + +variable {F : Type*} [Field F] [Finite F] {P : ExtensionParams F} + [Fact (Nat.card F = P.q)] + [Fact (Irreducible P.poly)] + +-- Field projections retain the canonical executable operations. +example (x y : Ext P) : (inferInstance : Field (Ext P)).mul x y = Ext.mul x y := rfl +example (x : Ext P) : (inferInstance : Field (Ext P)).inv x = Ext.inv x := rfl +example (x y : Ext P) : (inferInstance : Field (Ext P)).div x y = x * Ext.inv y := rfl +example (x : Ext P) (n : ℕ) : + (inferInstance : Field (Ext P)).npow n x = npowBinRec n x := rfl + +end Operations + +end CompPolyTests.ExtensionPresentation diff --git a/tests/CompPolyTests/Fields/Extension/RawArithmetic.lean b/tests/CompPolyTests/Fields/Extension/RawArithmetic.lean new file mode 100644 index 00000000..7ff94952 --- /dev/null +++ b/tests/CompPolyTests/Fields/Extension/RawArithmetic.lean @@ -0,0 +1,41 @@ +/- +Copyright (c) 2026 CompPoly Contributors. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: CompPoly Contributors +-/ +module + +public meta import CompPoly.Fields.Extension.Arithmetic +public import CompPoly.Fields.Extension.Arithmetic + +/-! +# Raw extension arithmetic without field certificates + +An integer quotient and a coefficient carrier over Bool exercise the raw import boundary. +The inverse candidate with `q = 0` deliberately lacks inverse laws. +-/ + +public meta section + +namespace CompPolyTests.ExtensionRawArithmetic + +open CompPoly.Extension + +private abbrev params : ExtensionParams Int := ⟨2, by decide, #v[1, 0], 0⟩ + +#guard Ext.coeffs ((Ext.gen : Ext params) * Ext.gen) == #v[-1, 0] +#guard Ext.coeffs ((Ext.gen : Ext params) ^ 2) == #v[-1, 0] +#guard Ext.coeffs ((0 : Ext params)⁻¹) == #v[1, 0] + +example : (inferInstance : Mul (Ext params)).mul = Ext.mul := rfl +example : (inferInstance : Inv (Ext params)).inv = Ext.inv := rfl + +private abbrev bare : ExtensionParams Bool := ⟨2, by decide, #v[true, false], 0⟩ + +example (v : Vector Bool 2) : Ext.coeffs (Ext.ofVector (P := bare) v) = v := rfl + +private abbrev binomial : BinomialParams Int := ⟨2, -1, by decide, 0⟩ + +#guard Ext.coeffs ((Ext.gen : Ext binomial.toExtensionParams) ^ 2) == #v[-1, 0] + +end CompPolyTests.ExtensionRawArithmetic diff --git a/tests/CompPolyTests/Fields/Goldilocks/Fast.lean b/tests/CompPolyTests/Fields/Goldilocks/Fast.lean new file mode 100644 index 00000000..5d4c5cf1 --- /dev/null +++ b/tests/CompPolyTests/Fields/Goldilocks/Fast.lean @@ -0,0 +1,46 @@ +/- +Copyright (c) 2026 CompPoly Contributors. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Varun Thakore +-/ +module + +public meta import CompPoly.Fields.Goldilocks.Fast + +/-! +# Fast Goldilocks Field Tests + +Regression checks for the executable native-word representation. +-/ + +public meta section + +namespace Goldilocks.Fast + +private def p : Nat := Goldilocks.fieldSize + +#guard raw (0 : Field) = 0 +#guard raw (1 : Field) = 1 +#guard toNat (ofNat 73) = 73 +#guard toNat (ofNat p) = 0 +#guard toNat (ofNat (p + 73)) = 73 +#guard toNat (ofUInt64 (UInt64.ofNat (UInt64.size - 1))) = 4294967294 +#guard toNat ((ofNat (p - 1)) + (4 : Field)) = 3 +#guard toNat ((ofNat (p - 1)) + (ofNat (p - 1))) = p - 2 +#guard toNat ((17 : Field) - (6 : Field)) = 11 +#guard toNat ((6 : Field) - (17 : Field)) = p - 11 +#guard toNat (-(0 : Field)) = 0 +#guard toNat (-(1 : Field)) = p - 1 +#guard toNat ((ofNat (p - 1)) * (ofNat (p - 1))) = 1 +#guard toField (square (54321 : Field)) = ((54321 : Goldilocks.Field) ^ 2) +#guard toNat ((73 : Field) ^ 0) = 1 +#guard toNat ((73 : Field) ^ 1) = 73 +#guard toField ((987654321 : Field) ^ 19) = ((987654321 : Goldilocks.Field) ^ 19) +#guard toField ((987654321 : Field) ^ 511) = ((987654321 : Goldilocks.Field) ^ 511) +#guard toNat ((0 : Field)⁻¹) = 0 +#guard toNat ((73 : Field)⁻¹ * (73 : Field)) = 1 +#guard toNat ((73 : Field) / (73 : Field)) = 1 +#guard toField ((73 : Field)⁻¹) = ((73 : Goldilocks.Field)⁻¹) +#guard toField ((73 : Field) ^ (-5 : Int)) = ((73 : Goldilocks.Field) ^ (-5 : Int)) + +end Goldilocks.Fast diff --git a/tests/CompPolyTests/Fields/KoalaBear/SexticReplay.lean b/tests/CompPolyTests/Fields/KoalaBear/SexticReplay.lean new file mode 100644 index 00000000..53e2b3a3 --- /dev/null +++ b/tests/CompPolyTests/Fields/KoalaBear/SexticReplay.lean @@ -0,0 +1,32 @@ +/- +Copyright (c) 2026 CompPoly Contributors. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: CompPoly Contributors +-/ +module + +import CompPoly.Fields.KoalaBear.Ext6.SexticIrreducible +meta import Lean + +/-! +# Sextic certificate replay + +Recheck the imported proof body against its original statement with a fresh kernel checker. +The explicit import also makes Lake rerun this test when the certificate module changes. +-/ + +open Lean + +run_elab do + let env ← importModules #[{ module := `CompPoly.Fields.KoalaBear.Ext6.SexticIrreducible }] + {} (trustLevel := 0) (loadExts := false) (level := .private) + let some (.thmInfo info) := env.toKernelEnv.find? `KoalaBear.sexticPoly_irreducible + | throwError "Missing sextic irreducibility proof" + let declaration := Declaration.thmDecl { + name := `CompPolyTests.KoalaBear.replayed_sextic_irreducible + levelParams := info.levelParams + type := info.type + value := info.value } + match env.toKernelEnv.addDeclCore 0 0 declaration none with + | .ok _ => pure () + | .error error => throwError "Sextic kernel replay failed: {← error.toMessageData {} |>.toString}" diff --git a/tests/CompPolyTests/Fields/Mersenne31/Fast.lean b/tests/CompPolyTests/Fields/Mersenne31/Fast.lean new file mode 100644 index 00000000..0d305c9d --- /dev/null +++ b/tests/CompPolyTests/Fields/Mersenne31/Fast.lean @@ -0,0 +1,46 @@ +/- +Copyright (c) 2026 CompPoly Contributors. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Varun Thakore +-/ +module + +public meta import CompPoly.Fields.Mersenne31.Fast + +/-! +# Fast Mersenne31 Field Tests + +Regression checks for the executable native-word representation. +-/ + +public meta section + +namespace Mersenne31.Fast + +#guard (0 : Field).val = 0 +#guard (1 : Field).val = 1 +#guard toNat (ofUInt32 37) = 37 +#guard toNat (Mersenne31.fieldSize : Field) = 0 +#guard toNat (Mersenne31.fieldSize + 37 : Field) = 37 +#guard toNat ((Mersenne31.fieldSize - 1 : Field) + 2) = 1 +#guard toNat ((Mersenne31.fieldSize - 1 : Field) + (Mersenne31.fieldSize - 1 : Field)) = + Mersenne31.fieldSize - 2 +#guard toNat ((9 : Field) - 5) = 4 +#guard toNat ((5 : Field) - 9) = Mersenne31.fieldSize - 4 +#guard toNat (-(0 : Field)) = 0 +#guard toNat (-(1 : Field)) = Mersenne31.fieldSize - 1 +#guard toNat ((Mersenne31.fieldSize - 1 : Field) * (Mersenne31.fieldSize - 1 : Field)) = 1 +#guard toNat ((12345 : Field) * 12345) = 152399025 +#guard toNat ((37 : Field) ^ 0) = 1 +#guard toNat ((37 : Field) ^ 1) = 37 +#guard toField ((123456789 : Field) ^ 17) = ((123456789 : Mersenne31.Field) ^ 17) +#guard toField ((123456789 : Field) ^ 255) = ((123456789 : Mersenne31.Field) ^ 255) +#guard toNat ((0 : Field)⁻¹) = 0 +#guard toNat ((37 : Field)⁻¹ * 37) = 1 +#guard toNat ((37 : Field) / 37) = 1 +#guard toField ((37 : Field)⁻¹) = ((37 : Mersenne31.Field)⁻¹) +#guard toField ((37 : Field) ^ (-3 : Int)) = ((37 : Mersenne31.Field) ^ (-3 : Int)) +#guard ringEquiv ((123 : Field) + 456) = ((123 : Mersenne31.Field) + 456) +#guard ringEquiv ((123 : Field) * 456) = ((123 : Mersenne31.Field) * 456) + +end Mersenne31.Fast diff --git a/tests/CompPolyTests/Fields/Mersenne31/Instances.lean b/tests/CompPolyTests/Fields/Mersenne31/Instances.lean new file mode 100644 index 00000000..134939fd --- /dev/null +++ b/tests/CompPolyTests/Fields/Mersenne31/Instances.lean @@ -0,0 +1,37 @@ +/- +Copyright (c) 2026 CompPoly Contributors. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Adrien Lacombe +-/ +module + +public import CompPoly.Fields.Mersenne31.Fast + +/-! +# Mersenne31 Field Instance Tests + +Regression checks for the canonical and fast Mersenne31 field instances. +-/ + +public section + +namespace Mersenne31 + +example : Fact (Nat.Prime fieldSize) := inferInstance + +example : _root_.Field Field := inferInstance + +example : NonBinaryField Field := inferInstance + +example : (2 : Field) ≠ 0 := by + exact NonBinaryField.char_neq_2 + +end Mersenne31 + +namespace Mersenne31.Fast + +example : _root_.Field Field := inferInstance + +example : NonBinaryField Field := inferInstance + +end Mersenne31.Fast diff --git a/tests/CompPolyTests/Fields/Pasta/Fast.lean b/tests/CompPolyTests/Fields/Pasta/Fast.lean new file mode 100644 index 00000000..5ea4a2b7 --- /dev/null +++ b/tests/CompPolyTests/Fields/Pasta/Fast.lean @@ -0,0 +1,113 @@ +/- +Copyright (c) 2026 CompPoly Contributors. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Gregor Mitscha-Baude +-/ +module + +public meta import CompPoly.Fields.Pasta + +/-! +# Fast Pasta Field Tests + +Regression checks for the raw eight-limb operations and the Pallas/Vesta fast-field +instantiations against externally computed values. +-/ + +public meta section + +namespace CompPolyTests.Fields.Pasta + +open _root_.Montgomery.Native64x8 + +private def q : Limbs8 := Vesta.Fast.instMont64x8Field.modulusLimbs + +private def negInv : UInt64 := Vesta.Fast.instMont64x8Field.montgomeryNegInv + +private def rMod : Limbs8 := Vesta.Fast.instMont64x8Field.rModModulus + +private def modulus : ℕ := Vesta.baseFieldSize + +private def a : Limbs8 := + ⟨0x90abcdef, 0x12345678, 0x90abcdef, 0x12345678, 0x90abcdef, 0x12345678, 0x90abcdef, + 0x12345678⟩ + +private def b : Limbs8 := + ⟨0x98765432, 0x1fedcba0, 0x98765432, 0x1fedcba0, 0x98765432, 0x1fedcba0, 0x98765432, + 0xfedcba0⟩ + +private def montA : Limbs8 := + ⟨0x4657fff9, 0xb89185c2, 0x7fa63616, 0xac5206fd, 0xca71bcaa, 0x4a1382e8, 0xd2416c1e, + 0x3ec0c11b⟩ + +private def montB : Limbs8 := + ⟨0x96717045, 0xa13e865, 0xeda4684, 0x5fa53078, 0xadca6010, 0x8c0bc7ff, 0x6af4be5b, + 0x2bde1e93⟩ + +private def montAB : Limbs8 := + ⟨0x5bfbc390, 0xa6073176, 0x8f8933a9, 0xbed1aec1, 0x630437f1, 0xba615782, 0x92dc822b, + 0x77a4b3f⟩ + +#guard q.toNat = modulus +#guard condSub q q = Limbs8.zero +#guard condSub q Limbs8.zero = Limbs8.zero +#guard (add q a b).toNat = (a.toNat + b.toNat) % modulus +#guard add q a (neg q a) = Limbs8.zero +#guard (sub q a b).toNat = (a.toNat + (modulus - b.toNat)) % modulus +#guard (sub q b a).toNat = (b.toNat + (modulus - a.toNat)) % modulus +#guard (neg q a).toNat = (modulus - a.toNat) % modulus +#guard neg q Limbs8.zero = Limbs8.zero +#guard mul q negInv montA montB = montAB +#guard mul q negInv rMod rMod = rMod +#guard mul q negInv rMod Limbs8.zero = Limbs8.zero +#guard square q negInv rMod = rMod + +private abbrev F := Vesta.Fast.Field + +#guard (0 : F).toNat = 0 +#guard (1 : F).toNat = 1 +#guard (37 : F).toNat = 37 +#guard ((Vesta.baseFieldSize : F)).toNat = 0 +#guard ((12345 : F) * 12345).toNat = 12345 * 12345 +#guard ((0 : F) - 1).toNat = Vesta.baseFieldSize - 1 +#guard (-(1 : F)).toNat = Vesta.baseFieldSize - 1 +#guard (((Vesta.baseFieldSize - 1 : ℕ) : F) * ((Vesta.baseFieldSize - 1 : ℕ) : F)).toNat = 1 +#guard ((123456789 : F) ^ 17).toNat = 123456789 ^ 17 % Vesta.baseFieldSize +#guard ((37 : F)⁻¹ * 37).toNat = 1 +#guard ((37 : F) / 37).toNat = 1 +#guard ((0 : F)⁻¹).toNat = 0 + +private def pq : Limbs8 := Pallas.Fast.instMont64x8Field.modulusLimbs + +private def pNegInv : UInt64 := Pallas.Fast.instMont64x8Field.montgomeryNegInv + +private def pMontA : Limbs8 := + ⟨0x73c65f1d, 0x4e0a938e, 0xd71d5fef, 0x6a1193b, 0xe42540dc, 0xcb40dac3, 0xc6cffd09, + 0x335fa2c8⟩ + +private def pMontB : Limbs8 := + ⟨0x5153d947, 0x26211be3, 0x38e82d52, 0xcb949678, 0xb6f80c7e, 0xf369686f, 0x7c2907b8, + 0xd91e296⟩ + +private def pMontAB : Limbs8 := + ⟨0x8d6b3f08, 0xe5c0528e, 0x22646fad, 0xaec941cc, 0xfec4a4c0, 0x4f5e09aa, 0x69b735b5, + 0x179b8aa1⟩ + +#guard pq.toNat = Pallas.baseFieldSize +#guard mul pq pNegInv pMontA pMontB = pMontAB +#guard (add pq a b).toNat = (a.toNat + b.toNat) % Pallas.baseFieldSize +#guard condSub pq pq = Limbs8.zero + +private abbrev G := Pallas.Fast.Field + +#guard (37 : G).toNat = 37 +#guard ((12345 : G) * 12345).toNat = 12345 * 12345 +#guard ((37 : G)⁻¹ * 37).toNat = 1 + +#guard ((123456789 : F) ^ 17).toField = ((123456789 : Vesta.BaseField) ^ 17) +#guard ((37 : F)⁻¹).toField = ((37 : Vesta.BaseField)⁻¹) +#guard Vesta.Fast.ofField ((37 : Vesta.BaseField)⁻¹) = (37 : F)⁻¹ +#guard ((123456789 : G) ^ 17).toField = ((123456789 : Pallas.BaseField) ^ 17) +#guard ((37 : G)⁻¹).toField = ((37 : Pallas.BaseField)⁻¹) + +end CompPolyTests.Fields.Pasta diff --git a/tests/CompPolyTests/LinearAlgebra/PolynomialMatrix/Approximant.lean b/tests/CompPolyTests/LinearAlgebra/PolynomialMatrix/Approximant.lean new file mode 100644 index 00000000..a39df672 --- /dev/null +++ b/tests/CompPolyTests/LinearAlgebra/PolynomialMatrix/Approximant.lean @@ -0,0 +1,254 @@ +/- +Copyright (c) 2026 CompPoly Contributors. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Valerii Huhnin +-/ + +module + +public meta import CompPoly.LinearAlgebra.PolynomialMatrix.Approximant +public meta import Mathlib.Algebra.Field.ZMod + +/-! +# Polynomial-Matrix Approximant Tests + +Focused executable checks for X-adic approximant problem sizing, recursive +PM-basis plumbing, partial-linearization orders, and exact-nullspace lifts. +-/ + +public meta section + +namespace CompPolyTests + +open CompPoly +open CompPoly.PolynomialMatrix +open CompPoly.PolynomialMatrix.Approximant + +namespace LinearAlgebra.PolynomialMatrix.Approximant + +abbrev F3 := ZMod 3 + +instance : Fact (Nat.Prime 3) := + ⟨by decide⟩ + +private def X : CPolynomial F3 := + CPolynomial.X + +private def problem : XAdicProblem F3 := + { orders := #[2] + matrix := #[ + #[1], + #[X] + ] } + +private def fullRankProblem : XAdicProblem F3 := + { orders := #[1] + matrix := #[ + #[1] + ] } + +private def identityLeaf (problem : XAdicProblem F3) (_shift : Array Nat) : + PolynomialMatrix F3 := + PolynomialMatrix.identity problem.matrix.size + +private def lowCtx : PolynomialMatrix.MulLowContext F3 := + PolynomialMatrix.MulLowContext.fromMulContext CPolynomial.MulContext.naive + +private def runtime : PMBasisRuntime F3 := + { mulContext := CPolynomial.MulContext.naive + lowMulContext := lowCtx + composeBasis := PolynomialMatrix.mulStrassenWith lowCtx 1 + residualProduct := PolynomialMatrix.mulTruncColumnStrassenWith lowCtx 1 + leafCutoff := 1 + leafBasis := identityLeaf } + +private def kernelLeafRows : PolynomialMatrix F3 := + kernelLeafBasis problem #[0, 0] + +private def firstPivotOnlyRows : PolynomialMatrix F3 := + #[ + #[1, 0] + ] + +private def compositionLeft : PolynomialMatrix F3 := + #[ + #[1 + X, X ^ 2], + #[X, 1] + ] + +private def compositionRight : PolynomialMatrix F3 := + #[ + #[1, X], + #[X + 1, X ^ 2] + ] + +private def rectangularLeft : PolynomialMatrix F3 := + #[ + #[1, X], + #[X + 1, X ^ 2], + #[0, 1] + ] + +private def rectangularRight : PolynomialMatrix F3 := + #[ + #[X, 1, X ^ 2], + #[1, X + 1, 0] + ] + +private def moduli : Array (CPolynomial F3) := + #[X ^ 2, X] + +private def equation : ModularEquation F3 := + { moduli := moduli + matrix := #[ + #[1, X], + #[X, 1] + ] } + +private def chunkPlan : PartialLinearizationPlan := + partialLinearizationPlan 2 2 moduli #[0, 5] + +private def profileChunkPlan : PartialLinearizationPlan := + partialLinearizationPlanFromPivotDegrees 2 2 moduli #[0, 5] #[some 7, none] + +private def highDegreeProfileChunkPlan : PartialLinearizationPlan := + partialLinearizationPlanFromPivotDegrees 2 2 moduli #[0, 20] + #[some 25, some 45] + +private def noChunkPlan : PartialLinearizationPlan := + unchunkedPartialLinearizationPlan 2 2 moduli + +private def chunkedRow : PolynomialRow F3 := + #[1, X, 0, 1, 0, 0] + +private def profileRows : PolynomialMatrix F3 := + #[ + #[X ^ 2, 0], + #[0, X] + ] + +private def profileFromRows : PivotDegreeProfile := + pivotDegreeProfileFromRows 2 profileRows #[0, 5] + +private def productionPMCtx : PMBasisContext F3 := + kernelLeafPMBasisContext CPolynomial.MulContext.naive 1 + +private def remainderModCtx : CPolynomial.ModContext F3 := + CPolynomial.ModContext.remainderOnly + +private def discoveredProfile : PivotDegreeProfile := + discoverPivotDegreeProfileViaPMBasis CPolynomial.MulContext.naive + remainderModCtx productionPMCtx equation #[0, 5] + +private def knownDegreePlan : PartialLinearizationPlan := + partialLinearizationPlanFromPivotDegrees equation.solutionWidth equation.modularWidth + equation.moduli #[0, 5] discoveredProfile.degrees + +private def knownDegreeProblem : XAdicProblem F3 := + chunkedExactNullspaceProblemForShift remainderModCtx equation knownDegreePlan #[0, 5] + +private def knownDegreeExpandedRows : PolynomialMatrix F3 := + productionPMCtx.basis knownDegreeProblem + (chunkedExactNullspaceShift knownDegreePlan #[0, 5]) + +private def knownDegreeCompressedRows : PolynomialMatrix F3 := + compactNonzeroRows + (compressChunkedPrincipalRows knownDegreePlan knownDegreeExpandedRows) + +private def knownDegreeRows : PolynomialMatrix F3 := + knownDegreeSolutionBasisViaPMBasis remainderModCtx productionPMCtx equation #[0, 5] + discoveredProfile + +private def knownDegreeFilteredRows : PolynomialMatrix F3 := + filterModularSolutionRows CPolynomial.MulContext.naive + CPolynomial.ModContext.remainderOnly equation knownDegreeRows + +private def productionRows : PolynomialMatrix F3 := + filteredSolutionBasisViaPMBasis CPolynomial.MulContext.naive + CPolynomial.ModContext.remainderOnly productionPMCtx equation #[0, 5] none + +private def debugUnchunkedRows : PolynomialMatrix F3 := + debugUnchunkedFilteredSolutionBasisViaPMBasis CPolynomial.MulContext.naive + CPolynomial.ModContext.remainderOnly productionPMCtx equation #[0, 5] + +private def leastChoiceRows : PolynomialMatrix F3 := + #[ + #[X ^ 2, 0], + #[1, 0], + #[0, X] + ] + +#guard maxOrder problem == 2 +#guard totalOrder problem == 2 +#guard lowerOrders problem 1 == #[1] +#guard residualOrders problem 1 == #[1] +#guard leafDegreeCap problem == 2 +#guard (coefficientMatrix problem).rows == 2 +#guard (coefficientMatrix problem).cols == 4 +#guard kernelLeafRows.any fun row ↦ row == #[-X, 1] +#guard (kernelLeafBasis fullRankProblem #[0]).any fun row ↦ row == #[X] +#guard rowsContainLeadingPosition firstPivotOnlyRows #[0, 0] 0 +#guard !rowsContainLeadingPosition firstPivotOnlyRows #[0, 0] 1 +#guard missingCompletionRows problem #[0, 0] firstPivotOnlyRows == #[#[0, X ^ 2]] +#guard rowGet (rowMulMatrixTruncColumnWith lowCtx #[2] #[1, X] problem.matrix) 0 == 1 +#guard mulBoundedWith lowCtx compositionLeft compositionRight == + mulWith CPolynomial.MulContext.naive compositionLeft compositionRight +#guard mulStrassenWith lowCtx 1 compositionLeft compositionRight == + mulWith CPolynomial.MulContext.naive compositionLeft compositionRight +#guard mulStrassenWith lowCtx 1 rectangularLeft rectangularRight == + mulWith CPolynomial.MulContext.naive rectangularLeft rectangularRight +#guard mulTruncColumnStrassenWith lowCtx 1 #[2, 1, 3] + rectangularLeft rectangularRight == + mulTruncColumnWith lowCtx #[2, 1, 3] rectangularLeft rectangularRight + +#guard (pmBasis runtime problem #[0, 0]).size == 2 +#guard MatrixWidth (pmBasis runtime problem #[0, 0]) == 2 + +#guard modulusDegreeMass moduli == 3 +#guard chunkDelta 2 moduli == 2 +#guard linearizedOrders 2 moduli == #[5, 4] +#guard chunkPlan.chunks.size == 4 +#guard chunkedExactNullspaceShift chunkPlan #[0, 5] == #[3, 5, 7, 8, 2, 2] +#guard profileChunkPlan.chunks.size == 5 +#guard highDegreeProfileChunkPlan.chunks.size > profileChunkPlan.chunks.size +#guard noChunkPlan.chunks == #[{ coord := 0, offset := 0 }, { coord := 1, offset := 0 }] +#guard profileFromRows.degrees == #[some 2, some 6] +#guard (chunkedExactNullspaceLift remainderModCtx equation chunkPlan).size == 6 +#guard MatrixWidth (chunkedExactNullspaceLift remainderModCtx equation chunkPlan) == 2 +#guard rowGet ((chunkedExactNullspaceLift remainderModCtx equation chunkPlan).getD 1 #[]) 0 == 0 +#guard rowGet ((chunkedExactNullspaceLift remainderModCtx equation chunkPlan).getD 3 #[]) 0 == X +#guard compressChunkedPrincipalRow chunkPlan chunkedRow == #[1 + X ^ 3, 1] + +#guard equation.solutionWidth == 2 +#guard equation.modularWidth == 2 +#guard (exactNullspaceLift equation).size == 4 +#guard MatrixWidth (exactNullspaceLift equation) == 2 +#guard rowGet ((exactNullspaceLift equation).getD 2 #[]) 0 == -(X ^ 2) +#guard rowGet ((exactNullspaceLift equation).getD 3 #[]) 1 == -X +#guard (filterModularSolutionRows CPolynomial.MulContext.naive + CPolynomial.ModContext.remainderOnly equation #[PolynomialMatrix.zeroRow 2]).isEmpty + +#guard knownDegreeCompressedRows == knownDegreeRows +#guard !knownDegreeRows.isEmpty +#guard knownDegreeRows.all fun row ↦ + rowSatisfiesModularBool CPolynomial.MulContext.naive + CPolynomial.ModContext.remainderOnly row equation.matrix equation.moduli +#guard !knownDegreeFilteredRows.isEmpty +#guard knownDegreeFilteredRows.all fun row ↦ + rowSatisfiesModularBool CPolynomial.MulContext.naive + CPolynomial.ModContext.remainderOnly row equation.matrix equation.moduli +#guard !productionRows.isEmpty +#guard MatrixWidth productionRows == equation.solutionWidth +#guard productionRows.all fun row ↦ + rowSatisfiesModularBool CPolynomial.MulContext.naive + CPolynomial.ModContext.remainderOnly row equation.matrix equation.moduli +#guard debugUnchunkedRows.all fun row ↦ + rowSatisfiesModularBool CPolynomial.MulContext.naive + CPolynomial.ModContext.remainderOnly row equation.matrix equation.moduli +#guard match leastShiftedDegreeChoice? leastChoiceRows #[0, 3] with + | some choice => choice.index == 1 && choice.degree == 0 && choice.row == #[1, 0] + | none => false + +end LinearAlgebra.PolynomialMatrix.Approximant + +end CompPolyTests diff --git a/tests/CompPolyTests/LinearAlgebra/TensorProduct/Basis.lean b/tests/CompPolyTests/LinearAlgebra/TensorProduct/Basis.lean new file mode 100644 index 00000000..db753e03 --- /dev/null +++ b/tests/CompPolyTests/LinearAlgebra/TensorProduct/Basis.lean @@ -0,0 +1,177 @@ +/- +Copyright (c) 2026 CompPoly Contributors. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: CompPoly Contributors +-/ +module + +import CompPoly.LinearAlgebra.TensorProduct.Basis +import Mathlib.LinearAlgebra.Complex.Module + +/-! +# Tensor basis action regression tests + +Check that importing the right-action basis helper preserves the default left action, +that both coordinate systems reconstruct arbitrary tensors and recover arbitrary coordinates, +and that the actions and coordinates differ on the proper field extension `ℂ/ℝ`. +-/ + +open Module +open scoped TensorProduct BigOperators + +namespace CompPolyTests.TensorProductBasis + +section Actions + +variable {K L : Type*} [CommSemiring K] [CommSemiring L] [Algebra K L] + +-- The equal-factor case is where a global rightAlgebra instance would change the default. +example : algebraMap L (L ⊗[K] L) = Algebra.TensorProduct.includeLeftRingHom := rfl + +example : (inferInstance : Module L (L ⊗[K] L)) = TensorProduct.leftModule := rfl + +example : (inferInstance : DistribMulAction L (L ⊗[K] L)) = + TensorProduct.leftDistribMulAction := rfl + +example : (inferInstance : SMul L (L ⊗[K] L)) = TensorProduct.leftHasSMul := rfl + +example : + letI := Algebra.TensorProduct.rightAlgebra (R := K) (A := L) (B := L) + algebraMap L (L ⊗[K] L) = Algebra.TensorProduct.includeRight.toRingHom := rfl + +-- When the extension is the base itself, the two inclusions coincide. +example (x : K) : x ⊗ₜ[K] (1 : K) = (1 : K) ⊗ₜ[K] x := by + simpa only [smul_eq_mul, mul_one] using (TensorProduct.smul_tmul x (1 : K) (1 : K)) + +end Actions + +section Coordinates + +variable {K Left Right ι : Type*} [CommSemiring K] [Semiring Left] + [CommSemiring Right] [Algebra K Left] [Algebra K Right] [Fintype ι] + +example (b : Basis ι K Right) (z : Left ⊗[K] Right) : + ∑ i, (b.baseChange Left).repr z i • ((1 : Left) ⊗ₜ[K] b i) = z := by + simpa only [Basis.baseChange_apply] using (b.baseChange Left).sum_repr z + +example (b : Basis ι K Right) (c : ι → Left) (i : ι) : + (b.baseChange Left).repr (∑ j, c j • ((1 : Left) ⊗ₜ[K] b j)) i = c i := by + simpa only [Basis.baseChange_apply] using congrFun ((b.baseChange Left).repr_sum_self c) i + +example (b : Basis ι K Left) (z : Left ⊗[K] Right) : + letI := Algebra.TensorProduct.rightAlgebra (R := K) (A := Left) (B := Right) + ∑ i, (b.baseChangeRight (Right := Right)).repr z i • (b i ⊗ₜ[K] (1 : Right)) = z := by + let := Algebra.TensorProduct.rightAlgebra (R := K) (A := Left) (B := Right) + simpa only [Basis.baseChangeRight_apply] using (b.baseChangeRight (Right := Right)).sum_repr z + +example (b : Basis ι K Left) (c : ι → Right) (i : ι) : + letI := Algebra.TensorProduct.rightAlgebra (R := K) (A := Left) (B := Right) + (b.baseChangeRight (Right := Right)).repr + (∑ j, c j • (b j ⊗ₜ[K] (1 : Right))) i = c i := by + let := Algebra.TensorProduct.rightAlgebra (R := K) (A := Left) (B := Right) + simpa only [Basis.baseChangeRight_apply] using + congrFun ((b.baseChangeRight (Right := Right)).repr_sum_self c) i + +end Coordinates + +section EqualFactors + +variable {K L ι : Type*} [CommSemiring K] [CommSemiring L] [Algebra K L] [Fintype ι] + +-- Exercise the documented proof-local recipe on arbitrary tensors with equal factors. +example (b : Basis ι K L) (z : L ⊗[K] L) : + letI rightAlgebra := Algebra.TensorProduct.rightAlgebra (R := K) (A := L) (B := L) + letI := rightAlgebra.toModule + letI := rightAlgebra.toModule.toDistribMulAction + letI := rightAlgebra.toSMul + ∑ i, (b.baseChangeRight (Right := L)).repr z i • (b i ⊗ₜ[K] (1 : L)) = z := by + let rightAlgebra := Algebra.TensorProduct.rightAlgebra (R := K) (A := L) (B := L) + let := rightAlgebra.toModule + let := rightAlgebra.toModule.toDistribMulAction + let := rightAlgebra.toSMul + simpa only [Basis.baseChangeRight_apply] using (b.baseChangeRight (Right := L)).sum_repr z + +section LocalRightAction + +/-- The `L`-algebra structure on `L ⊗[K] L` induced by `r ↦ 1 ⊗ r`. -/ +noncomputable local instance tensorRightAlgebra : Algebra L (L ⊗[K] L) := + Algebra.TensorProduct.rightAlgebra + +noncomputable local instance : Module L (L ⊗[K] L) := + (tensorRightAlgebra (K := K) (L := L)).toModule + +noncomputable local instance : DistribMulAction L (L ⊗[K] L) := + (tensorRightAlgebra (K := K) (L := L)).toModule.toDistribMulAction + +noncomputable local instance : SMul L (L ⊗[K] L) := + (tensorRightAlgebra (K := K) (L := L)).toSMul + +example (b : Basis ι K L) (c : ι → L) (i : ι) : + (b.baseChangeRight (Right := L)).repr (∑ j, c j • (b j ⊗ₜ[K] (1 : L))) i = c i := by + simpa only [Basis.baseChangeRight_apply] using + congrFun ((b.baseChangeRight (Right := L)).repr_sum_self c) i + +example (x y r : L) : r • (x ⊗ₜ[K] y) = x ⊗ₜ[K] (r * y) := by + change (Algebra.TensorProduct.includeRight r : L ⊗[K] L) * (x ⊗ₜ[K] y) = _ + rw [Algebra.TensorProduct.includeRight_apply, Algebra.TensorProduct.tmul_mul_tmul, one_mul] + +-- Ordinary module and algebra lemmas must use the action selected for scalar notation. +example (r s : L) (x y : L ⊗[K] L) : + r • (x + y) = r • x + r • y ∧ (r + s) • x = r • x + s • x ∧ + (r * s) • x = r • (s • x) ∧ (1 : L) • x = x ∧ (0 : L) • x = 0 ∧ + r • (0 : L ⊗[K] L) = 0 ∧ r • x = algebraMap L (L ⊗[K] L) r * x ∧ + r • x * y = r • (x * y) ∧ x * (r • y) = r • (x * y) := by + exact ⟨smul_add r x y, add_smul r s x, mul_smul r s x, one_smul L x, + zero_smul L x, smul_zero r, Algebra.smul_def r x, + Algebra.smul_mul_assoc r x y, Algebra.mul_smul_comm r x y⟩ + +example (k : K) (r : L) (x : L ⊗[K] L) : (k • r) • x = k • (r • x) := + smul_assoc k r x + +end LocalRightAction + +-- The local hierarchy leaves all four surrounding defaults unchanged. +example : algebraMap L (L ⊗[K] L) = Algebra.TensorProduct.includeLeftRingHom := rfl +example : (inferInstance : Module L (L ⊗[K] L)) = TensorProduct.leftModule := rfl +example : (inferInstance : DistribMulAction L (L ⊗[K] L)) = + TensorProduct.leftDistribMulAction := rfl +example : (inferInstance : SMul L (L ⊗[K] L)) = TensorProduct.leftHasSMul := rfl + +end EqualFactors + +section ProperExtension + +open Complex + +/-- Complex coordinates in the left-action basis `1 ⊗ 1, 1 ⊗ I` of `ℂ ⊗[ℝ] ℂ`. -/ +private noncomputable def leftCoords (z : ℂ ⊗[ℝ] ℂ) : Fin 2 →₀ ℂ := + (basisOneI.baseChange ℂ).repr z + +/-- Complex coordinates in the right-action basis `1 ⊗ 1, I ⊗ 1` of `ℂ ⊗[ℝ] ℂ`. -/ +private noncomputable def rightCoords (z : ℂ ⊗[ℝ] ℂ) : Fin 2 →₀ ℂ := by + letI := Algebra.TensorProduct.rightAlgebra (R := ℝ) (A := ℂ) (B := ℂ) + letI := (Algebra.TensorProduct.rightAlgebra (R := ℝ) (A := ℂ) (B := ℂ)).toModule + exact (basisOneI.baseChangeRight (Right := ℂ)).repr z + +example : leftCoords (I ⊗ₜ[ℝ] (1 : ℂ)) 1 = 0 := by + simp [leftCoords, Basis.baseChange_repr_tmul, coe_basisOneI_repr] + +example : rightCoords (I ⊗ₜ[ℝ] (1 : ℂ)) 1 = 1 := by + simp [rightCoords, Basis.baseChangeRight_repr_tmul, coe_basisOneI_repr] + +-- Interchanging factors changes the tensor, even though the scalar types are identical. +example : I ⊗ₜ[ℝ] (1 : ℂ) ≠ (1 : ℂ) ⊗ₜ[ℝ] I := by + intro h + have bad := congrArg (fun z => leftCoords z 1) h + simp [leftCoords, Basis.baseChange_repr_tmul, coe_basisOneI_repr] at bad + +-- The row and column coefficient systems cannot be silently identified. +example : leftCoords (I ⊗ₜ[ℝ] (1 : ℂ)) ≠ rightCoords (I ⊗ₜ[ℝ] (1 : ℂ)) := by + intro h + have bad := congrArg (fun c : Fin 2 →₀ ℂ => c 1) h + simp [leftCoords, rightCoords, Basis.baseChange_repr_tmul, + Basis.baseChangeRight_repr_tmul, coe_basisOneI_repr] at bad + +end ProperExtension + +end CompPolyTests.TensorProductBasis diff --git a/tests/CompPolyTests/Multilinear/Equiv.lean b/tests/CompPolyTests/Multilinear/Equiv.lean index f5b89bff..e599240b 100644 --- a/tests/CompPolyTests/Multilinear/Equiv.lean +++ b/tests/CompPolyTests/Multilinear/Equiv.lean @@ -39,5 +39,13 @@ example (w x : Vector ℚ 2) : eqTilde w x = eval (lagrangeBasis w) x := by rfl +example (w x : Vector ℚ 2) : + eqTilde w x = ∏ i : Fin 2, (w[i] * x[i] + (1 - w[i]) * (1 - x[i])) := by + exact eqTilde_eq_prod w x + +example (w₁ x₁ : Vector ℚ 2) (w₂ x₂ : Vector ℚ 3) : + eqTilde (w₁ ++ w₂) (x₁ ++ x₂) = eqTilde w₁ x₁ * eqTilde w₂ x₂ := by + exact eqTilde_append w₁ x₁ w₂ x₂ + end CMlPolynomialEval end CompPoly diff --git a/tests/CompPolyTests/Multivariate/PartialEval.lean b/tests/CompPolyTests/Multivariate/PartialEval.lean new file mode 100644 index 00000000..25d7a48c --- /dev/null +++ b/tests/CompPolyTests/Multivariate/PartialEval.lean @@ -0,0 +1,82 @@ +/- +Copyright (c) 2026 CompPoly. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Derek Sorensen +-/ +module + +public import CompPoly.Multivariate.CMvPolynomialEvalLemmas +public import CompPoly.Multivariate.PartialEval + +/-! + # Multivariate Partial Evaluation Tests + + Checks what `partialEvalFirst` does on generators, that it agrees with direct + evaluation on a concrete polynomial, and that it does not raise the degree of + the remaining variables. + + The checks are symbolic rather than `decide`-based: `CMvPolynomial` is carried + by a quotient of `Std.ExtTreeMap`, so kernel reduction gets stuck on + `Quot.lift` (the same reason `Multivariate/Restrict.lean`'s tests are + symbolic). +-/ + +@[expose] public section + +namespace CPoly + +open _root_.CPoly.CMvPolynomial + +/-- `eval` on a variable. The library has `eval_C`/`eval_add`/`eval_mul` as +`simp` lemmas but no `eval_X`, so the concrete check below supplies it. -/ +private lemma eval_X_apply {n : ℕ} {R : Type*} [CommSemiring R] [BEq R] [LawfulBEq R] + (i : Fin n) (v : Fin n → R) : + (CMvPolynomial.X i : CMvPolynomial n R).eval v = v i := by + simp [eval_equiv, fromCMvPolynomial_X] + +/-! ## Action on generators -/ + +-- Fixing variable `0` leaves constants alone. +example (a c : ℚ) : + partialEvalFirst (n := 1) a (CMvPolynomial.C c) = CMvPolynomial.C c := by + simp only [partialEvalFirst, Nat.reduceAdd, bind₁_C] + +-- Variable `0` becomes the scalar it was fixed to. +example (a : ℚ) : + partialEvalFirst (n := 1) a (CMvPolynomial.X 0) = CMvPolynomial.C a := by + simp only [partialEvalFirst, Nat.reduceAdd, Fin.isValue, bind₁_X, Fin.cons_zero] + +-- Every other variable is shifted down by one. +example (a : ℚ) (i : Fin 1) : + partialEvalFirst a (CMvPolynomial.X i.succ) = CMvPolynomial.X i := by + simp only [partialEvalFirst, Nat.reduceAdd, bind₁_X, Fin.cons_succ] + +/-! ## Agreement with direct evaluation -/ + +/-- `X 0 * X 1 + 3` over `Fin 2`. -/ +private def testPoly : CMvPolynomial 2 ℚ := + CMvPolynomial.X 0 * CMvPolynomial.X 1 + CMvPolynomial.C 3 + +-- Fixing variable `0` to `2`, then evaluating the remaining variable at `5`, +-- gives `2 * 5 + 3 = 13`. +example : (partialEvalFirst (2 : ℚ) testPoly).eval (fun _ => 5) = 13 := by + rw [partialEvalFirst_eval] + simp only [testPoly, eval_add, eval_mul, eval_C, eval_X_apply, Fin.cons_zero] + norm_num + +-- The same value reached through the general theorem rather than by computing. +example : + (partialEvalFirst (2 : ℚ) testPoly).eval (fun _ => 5) + = testPoly.eval (Fin.cons 2 (fun _ => 5)) := + partialEvalFirst_eval _ _ _ + +/-! ## Degree preservation -/ + +-- Fixing a variable cannot raise the degree of any variable that remains: a +-- bound on variable `1` of `p` transfers to variable `0` of the restriction. +example (a : ℚ) (deg : ℕ) (p : CMvPolynomial 2 ℚ) + (hp : ∀ mono ∈ Lawful.monomials p, mono.degreeOf (Fin.succ 0) ≤ deg) : + ∀ mono ∈ Lawful.monomials (partialEvalFirst a p), mono.degreeOf 0 ≤ deg := + partialEvalFirst_degreeOf_le a 0 p hp + +end CPoly diff --git a/tests/CompPolyTests/Univariate/NTT/Benchmark.lean b/tests/CompPolyTests/Univariate/NTT/Benchmark.lean deleted file mode 100644 index 3c799f7a..00000000 --- a/tests/CompPolyTests/Univariate/NTT/Benchmark.lean +++ /dev/null @@ -1,112 +0,0 @@ -/- -Copyright (c) 2026 CompPoly. All rights reserved. -Released under Apache 2.0 license as described in the file LICENSE. -Authors: Salih Erdem Koçak, Doran Pamukçu --/ -module - -public meta import CompPoly.Univariate.NTT.FastMul -public meta import CompPoly.Univariate.NTT.KoalaBear - -/-! - # Univariate Multiplication Benchmark - - Manual benchmark for comparing NTT-based multiplication against the - existing raw polynomial multiplication across a range of operand sizes. --/ - -public meta section - -namespace CompPoly -namespace CPolynomial -namespace NTT -namespace Benchmark - -/-- Sweep of operand sizes to make the crossover point visible. -/ -def benchSizes : Array Nat := - #[4, 8, 12, 16, 24, 32, 48, 64, 96, 128, - 192, 256, 384, 512, 768, 1024, 1536, 2048, 2560, 3000] - -/-- Best-fitting KoalaBear NTT domain for a required convolution length. -/ -def bestDomainForLength? (requiredLen : Nat) : - Option (FittingDomain _root_.KoalaBear.Field requiredLen) := - CPolynomial.NTT.bestDomainForLength? _root_.KoalaBear.twoAdicity KoalaBear.domainOfLogN - (by intro _ _; rfl) requiredLen - -/-- Deterministic KoalaBear polynomial used by the manual benchmark. -/ -def mkPoly (n seed : Nat) : CPolynomial.Raw _root_.KoalaBear.Field := - Array.ofFn (fun i : Fin n ↦ (((i.1 + 1) * seed + i.1 * i.1 + 17) : - _root_.KoalaBear.Field)) - -/-- Operand size used by the one-off correctness check values. -/ -def checkSize : Nat := 512 - -/-- First polynomial for the one-off correctness check. -/ -def p : CPolynomial.Raw _root_.KoalaBear.Field := mkPoly checkSize 60 - -/-- Second polynomial for the one-off correctness check. -/ -def q : CPolynomial.Raw _root_.KoalaBear.Field := mkPoly checkSize 29 - -/-- Render an average millisecond count. -/ -def avgMsString (totalMs reps : Nat) : String := - s!"{(Float.ofNat totalMs) / (Float.ofNat reps)}" - -/-- Render the raw-over-NTT speedup ratio. -/ -def speedupString (nttMs rawMs : Nat) : String := - if nttMs = 0 then - "inf" - else - s!"{(Float.ofNat rawMs) / (Float.ofNat nttMs)}" - -/-- Number of repetitions to use for a benchmark size. -/ -def repeatsFor (n : Nat) : Nat := - if n ≤ 32 then 50 - else if n ≤ 128 then 20 - else if n ≤ 512 then 5 - else if n ≤ 1536 then 2 - else 1 - -/-- Time repeated calls to a thunk and return the final result. -/ -def timeRepeated {α : Type} (reps : Nat) (f : Unit → α) : IO (Nat × α) := do - let actualReps := max reps 1 - let start ← IO.monoMsNow - let mut last := f () - for _ in [1:actualReps] do - last := f () - let stop ← IO.monoMsNow - pure (stop - start, last) - -#eval show IO Unit from do - IO.println s!"sizes tested = {benchSizes.size}" - IO.println "size | reps | logN | domain | ntt avg ms | raw avg ms | winner | raw/ntt" - IO.println "-----------------------------------------------------------------------" - let mut crossover? : Option Nat := none - for i in [0:benchSizes.size] do - let n := benchSizes[i]! - let reps := repeatsFor n - let p := mkPoly n (41 + 13 * i) - let q := mkPoly n (73 + 17 * i) - let reqLen := Domain.requiredLength p q - let some ⟨benchDomain, _⟩ := bestDomainForLength? reqLen - | throw <| IO.userError - s!"no KoalaBear domain supports required length {reqLen} for size {n}" - let (nttMs, nttRes) ← timeRepeated reps (fun _ ↦ FastMul.Raw.fastMulImpl benchDomain p q) - let (rawMs, rawRes) ← timeRepeated reps (fun _ ↦ p * q) - unless nttRes = rawRes do - throw <| IO.userError s!"benchmark mismatch at size {n}" - let winner := if nttMs ≤ rawMs then "NTT" else "raw" - if crossover?.isNone && nttMs ≤ rawMs then - crossover? := some n - let row := - s!"{n} | {reps} | {benchDomain.logN} | {benchDomain.n} | " ++ - s!"{avgMsString nttMs reps} | {avgMsString rawMs reps} | " ++ - s!"{winner} | {speedupString nttMs rawMs}x" - IO.println row - match crossover? with - | some n => IO.println s!"first measured crossover: NTT wins at size {n}" - | none => IO.println "no measured crossover in this sweep" - -end Benchmark -end NTT -end CPolynomial -end CompPoly diff --git a/tests/CompPolyTests/Univariate/Roots/LasVegas.lean b/tests/CompPolyTests/Univariate/Roots/LasVegas.lean new file mode 100644 index 00000000..8bf716e5 --- /dev/null +++ b/tests/CompPolyTests/Univariate/Roots/LasVegas.lean @@ -0,0 +1,333 @@ +/- +Copyright (c) 2026 CompPoly Contributors. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Valerii Huhnin +-/ + +module + +public meta import CompPoly.Univariate.Roots.LasVegas +public meta import Mathlib.Algebra.Field.ZMod + +/-! +# Las Vegas Univariate Root Tests + +Deterministic probe coverage for odd-field Las Vegas splitting and the +characteristic-two trace branch. +-/ + +public meta section + +namespace CompPolyTests + +open CompPoly +open CompPoly.CPolynomial.Roots.FiniteField + +namespace Univariate.Roots.LasVegas + +abbrev F2 := ZMod 2 + +abbrev F5 := ZMod 5 + +abbrev F11 := ZMod 11 + +instance : Fact (Nat.Prime 2) := + ⟨by decide⟩ + +instance : Fact (Nat.Prime 5) := + ⟨by decide⟩ + +instance : Fact (Nat.Prime 11) := + ⟨by decide⟩ + +private def f5Ctx : FiniteFieldContext F5 where + q := 5 + finite := by infer_instance + card_eq := by + simp [F5, Nat.card_eq_fintype_card, ZMod.card] + frobenius_fixed := by decide + +private def f5Elements : Array F5 := + #[0, 1, 2, 3, 4] + +private theorem f5Elements_complete : ContainsAllFieldElements f5Elements := by + unfold ContainsAllFieldElements + decide + +private def f5Enumeration : FieldEnumeration F5 := + fieldEnumerationOfArray f5Elements f5Elements_complete + +private def xProbe : ProbeFamily F5 where + probe _q _factor _attempt := CPolynomial.X + +private def constantOneProbe : ProbeFamily F5 where + probe _q _factor _attempt := CPolynomial.C (1 : F5) + +private def failThenXProbe : ProbeFamily F5 where + probe _q _factor attempt := + if attempt = 0 then CPolynomial.C (1 : F5) else CPolynomial.X + +private def lvSplitter (cfg : LasVegasConfig) (probes : ProbeFamily F5) : + LinearFactorProductSplitter F5 := + lasVegasLinearFactorProductSplitterWith + CPolynomial.Raw.MulContext.naive CPolynomial.Raw.ModContext.naive + f5Ctx f5Enumeration cfg probes + +private def hasExactlyRoots {F : Type*} [BEq F] (roots expected : Array F) : Bool := + roots.size == expected.size && expected.all fun a ↦ roots.contains a + +private def twoRootProduct : CPolynomial F5 := + CPolynomial.linearFactor (1 : F5) * CPolynomial.linearFactor (2 : F5) + +private def immediateFactors : Array (CPolynomial F5) := + (lvSplitter { cutoff := 3 } xProbe).splitLinearFactors 5 twoRootProduct + +private def immediateRoots : Array F5 := + CPolynomial.rootsFromLinearFactors twoRootProduct immediateFactors + +#guard hasExactlyRoots immediateRoots #[(1 : F5), (2 : F5)] + +private def failThenSplitFactors : Array (CPolynomial F5) := + (lvSplitter { cutoff := 3 } failThenXProbe).splitLinearFactors 5 twoRootProduct + +private def failThenSplitRoots : Array F5 := + CPolynomial.rootsFromLinearFactors twoRootProduct failThenSplitFactors + +#guard hasExactlyRoots failThenSplitRoots #[(1 : F5), (2 : F5)] + +private def cutoffFallbackFactors : Array (CPolynomial F5) := + (lvSplitter { cutoff := 0 } constantOneProbe).splitLinearFactors 5 twoRootProduct + +private def cutoffFallbackRoots : Array F5 := + CPolynomial.rootsFromLinearFactors twoRootProduct cutoffFallbackFactors + +#guard hasExactlyRoots cutoffFallbackRoots #[(1 : F5), (2 : F5)] + +private def publicRoots (p : CPolynomial F5) : Array F5 := + CPolynomial.Roots.FiniteField.rootsInFiniteFieldWith + CPolynomial.Raw.MulContext.naive CPolynomial.Raw.ModContext.naive + f5Ctx (lvSplitter { cutoff := 3 } xProbe) p + +#guard publicRoots 0 == #[] +#guard publicRoots (CPolynomial.C (3 : F5)) == #[] +#guard publicRoots (CPolynomial.linearFactor (4 : F5)) == #[(4 : F5)] + +private def repeatedRootPolynomial : CPolynomial F5 := + CPolynomial.linearFactor (1 : F5) * + CPolynomial.linearFactor (1 : F5) * + CPolynomial.linearFactor (3 : F5) + +#guard hasExactlyRoots (publicRoots repeatedRootPolynomial) #[(1 : F5), (3 : F5)] + +private def noRootPolynomial : CPolynomial F5 := + CPolynomial.ofArray #[(2 : F5), 0, 1] + +#guard publicRoots noRootPolynomial == #[] + +private def f11Enumeration : FieldEnumeration F11 where + size := 11 + elem i := (i.val : F11) + complete := by + intro a + refine ⟨⟨a.val, ZMod.val_lt a⟩, ?_⟩ + exact ZMod.natCast_zmod_val a + +private def f11RootsFrom (start len : Nat) : List Nat := + (List.range len).map fun i ↦ start + i + +private def f11RootProduct (roots : List Nat) : CPolynomial F11 := + roots.foldl (fun p a ↦ p * CPolynomial.linearFactor (a : F11)) 1 + +private def f11LagrangeBasis (domain : List Nat) (x : Nat) : CPolynomial F11 := + let denom : F11 := + domain.foldl + (fun acc y ↦ if y == x then acc else acc * ((x : F11) - (y : F11))) 1 + CPolynomial.C denom⁻¹ * + domain.foldl + (fun p y ↦ if y == x then p else p * CPolynomial.linearFactor (y : F11)) 1 + +private def f11IndicatorOneOffZeros (domain zeros : List Nat) : CPolynomial F11 := + domain.foldl + (fun acc x ↦ if x ∈ zeros then acc else acc + f11LagrangeBasis domain x) 0 + +private def f11FullRootProduct : CPolynomial F11 := + f11RootProduct (f11RootsFrom 0 11) + +private def f11PrefixRootProduct (start len : Nat) : CPolynomial F11 := + f11RootProduct (f11RootsFrom start len) + +private def duplicateQuotientProbe : ProbeFamily F11 where + probe _q factor _attempt := + if factor == f11FullRootProduct then + f11IndicatorOneOffZeros (f11RootsFrom 0 11) (f11RootsFrom 0 5) + else if factor == f11PrefixRootProduct 0 5 then + f11IndicatorOneOffZeros (f11RootsFrom 0 5) [0] + else if factor == f11PrefixRootProduct 1 4 then + f11IndicatorOneOffZeros (f11RootsFrom 1 4) [1] + else if factor == f11PrefixRootProduct 2 3 then + f11IndicatorOneOffZeros (f11RootsFrom 2 3) [2] + else if factor == f11PrefixRootProduct 3 2 then + f11IndicatorOneOffZeros (f11RootsFrom 3 2) [3] + else + CPolynomial.C (1 : F11) + +private def duplicateQuotientConfig : LasVegasConfig where + cutoff := 1 + tryOddRandomizedSplitting := true + tryEvenTraceSplitting := false + +private def duplicateQuotientFactors : Array (CPolynomial F11) := + lasVegasSplitLinearFactorsWith + CPolynomial.Raw.MulContext.naive CPolynomial.Raw.ModContext.naive + f11Enumeration duplicateQuotientConfig duplicateQuotientProbe 11 + f11FullRootProduct + +#guard duplicateQuotientFactors.contains (CPolynomial.linearFactor (5 : F11)) + +private def f11Elements : Array F11 := + #[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10] + +private def f11TraceCtx : SmallPrimeTraceContext F11 where + q := 11 + finite := by infer_instance + card_eq := by + simp [F11, Nat.card_eq_fintype_card, ZMod.card] + frobenius_fixed := by decide + p := 11 + k := 1 + p_prime := by decide + q_eq := by decide + baseConstants := f11Elements + baseConstants_size := by rfl + basis := #[(1 : F11)] + basis_size := by rfl + traceValue := id + traceValue_eq_powerSum := by + intro z + simp [tracePowerSum] + traceValue_mem_base := by + intro z + fin_cases z <;> decide + trace_separates := by + intro a b hne + refine ⟨(1 : F11), by simp, ?_⟩ + simpa only [one_mul, id_eq] using (sub_ne_zero.mpr hne) + +private def duplicateQuotientTraceFactors : Array (CPolynomial F11) := + lasVegasSplitLinearFactorsWithTrace + CPolynomial.Raw.MulContext.naive CPolynomial.Raw.ModContext.naive + f11Enumeration f11TraceCtx duplicateQuotientConfig duplicateQuotientProbe 11 + f11FullRootProduct + +#guard duplicateQuotientTraceFactors.contains (CPolynomial.linearFactor (5 : F11)) + +private def f2TraceCtx : SmallPrimeTraceContext F2 where + q := 2 + finite := by infer_instance + card_eq := by + simp [F2, Nat.card_eq_fintype_card, ZMod.card] + frobenius_fixed := by decide + p := 2 + k := 1 + p_prime := by decide + q_eq := by decide + baseConstants := #[(0 : F2), 1] + baseConstants_size := by rfl + basis := #[(1 : F2)] + basis_size := by rfl + traceValue := id + traceValue_eq_powerSum := by + intro z + simp [tracePowerSum] + traceValue_mem_base := by decide + trace_separates := by + intro a b hne + refine ⟨(1 : F2), by simp, ?_⟩ + simpa only [one_mul, id_eq] using (sub_ne_zero.mpr hne) + +private def f2ElementsReversed : Array F2 := + #[(1 : F2), 0] + +private theorem f2ElementsReversed_complete : + ContainsAllFieldElements f2ElementsReversed := by + unfold ContainsAllFieldElements + decide + +private def f2Enumeration : FieldEnumeration F2 := + fieldEnumerationOfArray f2ElementsReversed f2ElementsReversed_complete + +private def xProbeF2 : ProbeFamily F2 where + probe _q _factor _attempt := CPolynomial.X + +private def constantOneProbeF2 : ProbeFamily F2 where + probe _q _factor _attempt := CPolynomial.C (1 : F2) + +private def failThenXProbeF2 : ProbeFamily F2 where + probe _q _factor attempt := + if attempt = 0 then CPolynomial.C (1 : F2) else CPolynomial.X + +private def lvTraceSplitter (cfg : LasVegasConfig) (probes : ProbeFamily F2) : + LinearFactorProductSplitter F2 := + lasVegasLinearFactorProductSplitterWithTrace + CPolynomial.Raw.MulContext.naive CPolynomial.Raw.ModContext.naive + f2TraceCtx.toFiniteFieldContext f2Enumeration f2TraceCtx cfg probes + +private def lvNoTraceSplitter (cfg : LasVegasConfig) (probes : ProbeFamily F2) : + LinearFactorProductSplitter F2 := + lasVegasLinearFactorProductSplitterWith + CPolynomial.Raw.MulContext.naive CPolynomial.Raw.ModContext.naive + f2TraceCtx.toFiniteFieldContext f2Enumeration cfg probes + +private def f2TwoRootProduct : CPolynomial F2 := + CPolynomial.linearFactor (0 : F2) * CPolynomial.linearFactor (1 : F2) + +private def f2TraceFactorOrder : Array (CPolynomial F2) := + #[CPolynomial.linearFactor (0 : F2), CPolynomial.linearFactor (1 : F2)] + +private def f2FallbackFactorOrder : Array (CPolynomial F2) := + #[CPolynomial.linearFactor (1 : F2), CPolynomial.linearFactor (0 : F2)] + +private def immediateTraceFactors : Array (CPolynomial F2) := + (lvTraceSplitter { cutoff := 2 } xProbeF2).splitLinearFactors 2 f2TwoRootProduct + +#guard immediateTraceFactors == f2TraceFactorOrder + +private def failThenTraceFactors : Array (CPolynomial F2) := + (lvTraceSplitter { cutoff := 2 } failThenXProbeF2).splitLinearFactors 2 f2TwoRootProduct + +#guard failThenTraceFactors == f2TraceFactorOrder + +private def cutoffFallbackTraceFactors : Array (CPolynomial F2) := + (lvTraceSplitter { cutoff := 1 } constantOneProbeF2).splitLinearFactors 2 f2TwoRootProduct + +#guard cutoffFallbackTraceFactors == f2FallbackFactorOrder + +private def missingTraceMetadataFallbackFactors : Array (CPolynomial F2) := + (lvNoTraceSplitter { cutoff := 2 } xProbeF2).splitLinearFactors 2 f2TwoRootProduct + +#guard missingTraceMetadataFallbackFactors == f2FallbackFactorOrder + +private def publicRootsF2 (p : CPolynomial F2) : Array F2 := + CPolynomial.Roots.FiniteField.rootsInFiniteFieldWith + CPolynomial.Raw.MulContext.naive CPolynomial.Raw.ModContext.naive + f2TraceCtx.toFiniteFieldContext (lvTraceSplitter { cutoff := 2 } xProbeF2) p + +#guard publicRootsF2 0 == #[] +#guard publicRootsF2 (CPolynomial.C (1 : F2)) == #[] +#guard publicRootsF2 (CPolynomial.linearFactor (1 : F2)) == #[(1 : F2)] + +private def repeatedRootPolynomialF2 : CPolynomial F2 := + CPolynomial.linearFactor (0 : F2) * + CPolynomial.linearFactor (0 : F2) * + CPolynomial.linearFactor (1 : F2) + +#guard hasExactlyRoots (publicRootsF2 repeatedRootPolynomialF2) #[(0 : F2), (1 : F2)] + +private def noRootPolynomialF2 : CPolynomial F2 := + CPolynomial.ofArray #[(1 : F2), 1, 1] + +#guard publicRootsF2 noRootPolynomialF2 == #[] + +end Univariate.Roots.LasVegas + +end CompPolyTests diff --git a/tests/CompPolyTests/Univariate/Roots/Shoup.lean b/tests/CompPolyTests/Univariate/Roots/Shoup.lean new file mode 100644 index 00000000..b8b57082 --- /dev/null +++ b/tests/CompPolyTests/Univariate/Roots/Shoup.lean @@ -0,0 +1,190 @@ +/- +Copyright (c) 2026 CompPoly Contributors. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Valerii Huhnin +-/ + +module + +public meta import CompPoly.Fields.Binary.Tower.Impl +public meta import CompPoly.Univariate.Roots.Shoup +public meta import Mathlib.Algebra.Field.ZMod + +/-! +# Shoup-Style Univariate Root Tests + +Focused executable coverage for the small-characteristic trace splitter over +`ZMod 2`, the degree-one binary-field case. +-/ + +public meta section + +namespace CompPolyTests + +open CompPoly +open CompPoly.CPolynomial.Roots.FiniteField + +namespace Univariate.Roots.Shoup + +abbrev F2 := ZMod 2 + +instance : Fact (Nat.Prime 2) := + ⟨by decide⟩ + +private def f2ShoupCtx : SmallPrimeTraceContext F2 where + q := 2 + finite := by infer_instance + card_eq := by + simp [F2, Nat.card_eq_fintype_card, ZMod.card] + frobenius_fixed := by decide + p := 2 + k := 1 + p_prime := by decide + q_eq := by decide + baseConstants := #[(0 : F2), 1] + baseConstants_size := by rfl + basis := #[(1 : F2)] + basis_size := by rfl + traceValue := id + traceValue_eq_powerSum := by + intro z + simp [tracePowerSum] + traceValue_mem_base := by decide + trace_separates := by + intro a b hne + refine ⟨(1 : F2), by simp, ?_⟩ + simpa only [one_mul, id_eq] using (sub_ne_zero.mpr hne) + +private def f2ShoupSplitter : LinearFactorProductSplitter F2 := + shoupLinearFactorProductSplitter f2ShoupCtx + +private def rootsInF2 (p : CPolynomial F2) : Array F2 := + rootsInFiniteField f2ShoupCtx.toFiniteFieldContext f2ShoupSplitter p + +private def f2NoRootQuadratic : CPolynomial F2 := + CPolynomial.ofArray #[(1 : F2), 1, 1] + +private def f2AllRoots : CPolynomial F2 := + CPolynomial.linearFactor (0 : F2) * CPolynomial.linearFactor (1 : F2) + +private def f2RepeatedRoots : CPolynomial F2 := + CPolynomial.linearFactor (0 : F2) * + CPolynomial.linearFactor (0 : F2) * + CPolynomial.linearFactor (1 : F2) + +private def f2PartialRoots : CPolynomial F2 := + CPolynomial.linearFactor (0 : F2) * f2NoRootQuadratic + +private def f2ZeroRootOnly : CPolynomial F2 := + CPolynomial.linearFactor (0 : F2) + +private def allRootsOut : Array F2 := + rootsInF2 f2AllRoots + +#guard allRootsOut.contains (0 : F2) +#guard allRootsOut.contains (1 : F2) +#guard allRootsOut.size == 2 + +private def repeatedRootsOut : Array F2 := + rootsInF2 f2RepeatedRoots + +#guard repeatedRootsOut.contains (0 : F2) +#guard repeatedRootsOut.contains (1 : F2) +#guard repeatedRootsOut.size == 2 + +private def zeroRootOut : Array F2 := + rootsInF2 f2ZeroRootOnly + +#guard zeroRootOut.contains (0 : F2) +#guard zeroRootOut.size == 1 + +#guard (rootsInF2 f2NoRootQuadratic).isEmpty + +private def partialRootsOut : Array F2 := + rootsInF2 f2PartialRoots + +#guard partialRootsOut.contains (0 : F2) +#guard !(partialRootsOut.contains (1 : F2)) +#guard partialRootsOut.size == 1 + +abbrev BT0 := ConcreteBinaryTower.ConcreteBTField 0 + +private theorem bt0_card : Nat.card BT0 = 2 := by + rw [Nat.card_eq_fintype_card] + simpa [BT0] using + (ConcreteBinaryTower.getBTFResult 0).fieldFintypeCard + +private def bt0ShoupCtx : SmallPrimeTraceContext BT0 where + q := 2 + finite := by infer_instance + card_eq := bt0_card + frobenius_fixed := by + intro a + have h := FiniteField.pow_card a + have hcard : Fintype.card BT0 = 2 := by + simpa [BT0] using (ConcreteBinaryTower.getBTFResult 0).fieldFintypeCard + rwa [hcard] at h + p := 2 + k := 1 + p_prime := by decide + q_eq := by decide + baseConstants := #[(0 : BT0), 1] + baseConstants_size := by rfl + basis := #[(1 : BT0)] + basis_size := by rfl + traceValue := id + traceValue_eq_powerSum := by + intro z + simp [tracePowerSum] + traceValue_mem_base := by decide + trace_separates := by + intro a b hne + refine ⟨(1 : BT0), by simp, ?_⟩ + simpa only [one_mul, id_eq] using (sub_ne_zero.mpr hne) + +private def bt0ShoupSplitter : LinearFactorProductSplitter BT0 := + shoupLinearFactorProductSplitter bt0ShoupCtx + +private def rootsInBT0 (p : CPolynomial BT0) : Array BT0 := + rootsInFiniteField bt0ShoupCtx.toFiniteFieldContext bt0ShoupSplitter p + +private def bt0NoRootQuadratic : CPolynomial BT0 := + CPolynomial.ofArray #[(1 : BT0), 1, 1] + +private def bt0AllRoots : CPolynomial BT0 := + CPolynomial.linearFactor (0 : BT0) * CPolynomial.linearFactor (1 : BT0) + +private def bt0RepeatedRoots : CPolynomial BT0 := + CPolynomial.linearFactor (0 : BT0) * + CPolynomial.linearFactor (0 : BT0) * + CPolynomial.linearFactor (1 : BT0) + +private def bt0PartialRoots : CPolynomial BT0 := + CPolynomial.linearFactor (0 : BT0) * bt0NoRootQuadratic + +private def bt0AllRootsOut : Array BT0 := + rootsInBT0 bt0AllRoots + +#guard bt0AllRootsOut.contains (0 : BT0) +#guard bt0AllRootsOut.contains (1 : BT0) +#guard bt0AllRootsOut.size == 2 + +private def bt0RepeatedRootsOut : Array BT0 := + rootsInBT0 bt0RepeatedRoots + +#guard bt0RepeatedRootsOut.contains (0 : BT0) +#guard bt0RepeatedRootsOut.contains (1 : BT0) +#guard bt0RepeatedRootsOut.size == 2 + +#guard (rootsInBT0 bt0NoRootQuadratic).isEmpty + +private def bt0PartialRootsOut : Array BT0 := + rootsInBT0 bt0PartialRoots + +#guard bt0PartialRootsOut.contains (0 : BT0) +#guard !(bt0PartialRootsOut.contains (1 : BT0)) +#guard bt0PartialRootsOut.size == 1 + +end Univariate.Roots.Shoup + +end CompPolyTests diff --git a/tests/README.md b/tests/README.md index 1204e665..1d8d2ce2 100644 --- a/tests/README.md +++ b/tests/README.md @@ -26,4 +26,29 @@ Build a single test module: ```bash lake build CompPolyTests.Univariate.Raw -``` \ No newline at end of file +``` + +## Native startup and field arithmetic + +`lake test` checks the Lean test modules. The separate `CompPolyNativeSmoke` executable checks +linked module initialization and BF64/Ext3 arithmetic. On Linux with GNU `timeout`, run from the +repository root: + +```bash +lake build --wfail CompPolyNativeSmoke && ( + set -euo pipefail + ulimit -v 4194304 + ulimit -c 0 + LEAN_NUM_THREADS=1 timeout --kill-after=5s 30s .lake/build/bin/CompPolyNativeSmoke +) +``` + +The build runs normally. Execution has a 4 GiB address-space limit, a 30-second deadline, and a +five-second termination grace period. These limits cover initialization before `main`, where +accidental full-field enumeration could otherwise exhaust memory. Arithmetic mismatches, startup +failures, resource exhaustion, and timeout all fail the command. + +The executable checks a wrapping BF64 product, an Ext3 reference product, addition, and inversion +including zero. It prints a success message only after every check passes. This is native +implementation evidence, not a proof of the compiler or a performance benchmark. Other platforms +can build the target; running it requires equivalent platform-specific resource limits.