From 32a0c29e41225e8cec2a2e1eab1dfab64f026aa0 Mon Sep 17 00:00:00 2001 From: Kobi Date: Mon, 17 Aug 2026 19:42:34 -0400 Subject: [PATCH 01/45] feat: make KoalaBear Rabin irreducibility certificates re-checkable by kernel replay (#306) The KoalaBear degree-5 and degree-6 extension irreducibility proofs (`quinticPoly_irreducible`, `sexticPoly_irreducible`) type-check fine at compile time but are effectively unbounded to *re-check* from an empty environment (Lean's `Environment.replay`, as used by `lean4checker` and by external proof-auditing tools): the check grows past 58 GiB without finishing. Cause: the Rabin lemmas are stated in terms of `Fintype.card F`, and the callers bridge the concrete field size in with `rw [hcard]`. On a cold re-check the kernel is forced to reduce `Fintype.card (ZMod p)` -- an enumeration of ~p (~2.1e9 for KoalaBear) elements -- to reconcile it with the numeral. Compilation avoids this because the elaborator handles the equation propositionally; replay re-faces the raw defeq. (The huge `X^(card^k)` power is *not* the cause -- it stays syntactically matched and is never reduced; the `X^4-C` degree-4 extension, which has no such enumeration, re-checks fine at the same closure size.) Fix: add explicit-cardinality wrappers `irreducible_of_rabin_prime_degree_of_card` and `irreducible_of_rabin_degree_six_of_card` that take the field size as a numeral `q` with `Fintype.card F = q`, proved by `subst hq` from the existing lemmas -- so they are definitionally the same statement, no axiom, nothing weakened. The two callers pass `q := fieldSize` and drop the `rw [hcard]` casts, so the certificates are stated with `q` concrete and the kernel never enumerates `Fintype.card`. Measured on the degree-6 case: re-checking the full 19,908-constant closure goes from unbounded (>58 GiB, killed) to ~9 s. Co-authored-by: Claude Opus 5 (1M context) --- .../Data/Polynomial/RabinCertificate.lean | 33 +++++++++++++++++++ .../KoalaBear/Ext5/QuinticIrreducible.lean | 9 +++-- .../KoalaBear/Ext6/SexticIrreducible.lean | 11 +++---- 3 files changed, 41 insertions(+), 12 deletions(-) diff --git a/CompPoly/Data/Polynomial/RabinCertificate.lean b/CompPoly/Data/Polynomial/RabinCertificate.lean index cc060bc5..f5df9fdd 100644 --- a/CompPoly/Data/Polynomial/RabinCertificate.lean +++ b/CompPoly/Data/Polynomial/RabinCertificate.lean @@ -389,4 +389,37 @@ 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 form of the prime-degree Rabin test. + +Identical content to `irreducible_of_rabin_prime_degree`, but the field size is +a caller-supplied numeral `q` (with `Fintype.card F = q`) rather than +`Fintype.card F` itself. This lets a caller state the trace and coprimality +certificates with `q` a concrete literal. That matters for **re-checking**: a +kernel replay of the resulting proof from an empty environment then never has to +reduce `Fintype.card F` (for a `ZMod p` field, an enumeration of ~`p` elements) +to reconcile it with the literal through a rewrite cast. `q` stays universally +quantified here, so this declaration itself carries no concrete large numeral, +and the two forms are definitionally equal, so nothing is weakened. -/ +theorem irreducible_of_rabin_prime_degree_of_card {F : Type*} [Field F] [Fintype F] + {f : F[X]} {d : ℕ} (q : ℕ) (hq : 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 hq + exact irreducible_of_rabin_prime_degree hd h_deg h_trace h_cop + +/-- Explicit-cardinality form of the degree-6 Rabin test. See +`irreducible_of_rabin_prime_degree_of_card` for why the numeral form matters for +re-checking; the same reasoning applies here at composite degree 6. -/ +theorem irreducible_of_rabin_degree_six_of_card {F : Type*} [Field F] [Fintype F] + {f : F[X]} (q : ℕ) (hq : 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 hq + exact irreducible_of_rabin_degree_six h_deg h_trace h_cop₃ h_cop₂ + end CompPoly.RabinCert diff --git a/CompPoly/Fields/KoalaBear/Ext5/QuinticIrreducible.lean b/CompPoly/Fields/KoalaBear/Ext5/QuinticIrreducible.lean index b93f5029..cf5ef709 100644 --- a/CompPoly/Fields/KoalaBear/Ext5/QuinticIrreducible.lean +++ b/CompPoly/Fields/KoalaBear/Ext5/QuinticIrreducible.lean @@ -110,12 +110,11 @@ 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 + refine irreducible_of_rabin_prime_degree_of_card 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/SexticIrreducible.lean b/CompPoly/Fields/KoalaBear/Ext6/SexticIrreducible.lean index e566afd9..bd5a293f 100644 --- a/CompPoly/Fields/KoalaBear/Ext6/SexticIrreducible.lean +++ b/CompPoly/Fields/KoalaBear/Ext6/SexticIrreducible.lean @@ -138,15 +138,12 @@ 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 + refine irreducible_of_rabin_degree_six_of_card 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⟩ From 7480a691ff87d178f0d0afd45454d8400e39e268 Mon Sep 17 00:00:00 2001 From: Alexander Hicks <25369263+alexanderlhicks@users.noreply.github.com> Date: Tue, 18 Aug 2026 01:52:51 +0100 Subject: [PATCH 02/45] doc(RabinCertificate): correct the _of_card rationale and signature (#307) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `_of_card` wrappers added in #306 are correct, but their docstrings justified the numeral form by claiming the `Fintype.card F` form makes a from-empty kernel replay reduce `Fintype.card F` (for a `ZMod p` field, an enumeration of ~`p` elements). It does not: `rw [hcard]` elaborates to `Eq.mpr (id (congrArg motive hcard)) cert`, whose kernel check only beta-reduces the motive, so `Fintype.card F` never reaches whnf position. Measured kernel type-checking of both proofs is single-digit milliseconds either way, with run-to-run variance exceeding the difference between the forms; and since Lean kernel-checks each theorem at `addDecl` during an ordinary build, a from-empty replay runs that same check. Replace the rationale with the one that does hold, and that `irreducible_X_pow_four_sub_C_of_card` already documents on the binomial side: the generated certificates are already stated in terms of `fieldSize`, so each Rabin condition applies directly instead of needing a `rw [hcard]` cast. Also drop the claim that the two forms are "definitionally equal" — the plain form is the `q := Fintype.card F` instance of the numeral one. Make `q` implicit and rename `hq` to `hcard`, matching the binomial `_of_card` form: `q` is uniquely determined by `hcard`, which precedes `h_trace`/`h_cop`, so inference never needs higher-order matching. Update `docs/wiki/field-extensions.md`, whose "Adding a new non-binomial extension" recipe still routed new extensions to the non-`_of_card` wrappers that the two canonical callers had moved away from, per the maintenance contract in `docs/wiki/README.md`. Pin the round trip in `CompPolyTests.RabinCertificate`: instantiating each `_of_card` form at `q := Fintype.card F` with `rfl` must recover the plain statement verbatim, so "nothing is weakened" is checked rather than asserted. The opposite direction is the wrapper's own proof body. Co-authored-by: Claude Opus 5 (1M context) --- .../Data/Polynomial/RabinCertificate.lean | 49 ++++++++++++------- .../KoalaBear/Ext5/QuinticIrreducible.lean | 11 ++--- .../KoalaBear/Ext6/SexticIrreducible.lean | 2 +- docs/wiki/field-extensions.md | 24 ++++++--- .../Data/Polynomial/RabinCertificate.lean | 31 ++++++++++++ 5 files changed, 85 insertions(+), 32 deletions(-) diff --git a/CompPoly/Data/Polynomial/RabinCertificate.lean b/CompPoly/Data/Polynomial/RabinCertificate.lean index f5df9fdd..d5fe966f 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,37 +391,46 @@ 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 form of the prime-degree Rabin test. - -Identical content to `irreducible_of_rabin_prime_degree`, but the field size is -a caller-supplied numeral `q` (with `Fintype.card F = q`) rather than -`Fintype.card F` itself. This lets a caller state the trace and coprimality -certificates with `q` a concrete literal. That matters for **re-checking**: a -kernel replay of the resulting proof from an empty environment then never has to -reduce `Fintype.card F` (for a `ZMod p` field, an enumeration of ~`p` elements) -to reconcile it with the literal through a rewrite cast. `q` stays universally -quantified here, so this declaration itself carries no concrete large numeral, -and the two forms are definitionally equal, so nothing is weakened. -/ +/-! ### 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, rather than first casting the goal with +`rw [hcard]`. 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 : ℕ) (hq : Fintype.card F = q) + {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 hq + subst hcard exact irreducible_of_rabin_prime_degree hd h_deg h_trace h_cop -/-- Explicit-cardinality form of the degree-6 Rabin test. See -`irreducible_of_rabin_prime_degree_of_card` for why the numeral form matters for -re-checking; the same reasoning applies here at composite degree 6. -/ +/-- **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 : ℕ) (hq : Fintype.card F = q) + {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 hq + subst hcard exact irreducible_of_rabin_degree_six h_deg h_trace h_cop₃ h_cop₂ end CompPoly.RabinCert diff --git a/CompPoly/Fields/KoalaBear/Ext5/QuinticIrreducible.lean b/CompPoly/Fields/KoalaBear/Ext5/QuinticIrreducible.lean index cf5ef709..eb429dfc 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,8 +110,7 @@ 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_of_card fieldSize hcard (by norm_num) - quinticPoly_natDegree ?_ ?_ + refine irreducible_of_rabin_prime_degree_of_card 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 · exact isCoprime_X_pow_sub_X_of_runChain toPoly_quinticL quinticPoly_ne_zero diff --git a/CompPoly/Fields/KoalaBear/Ext6/SexticIrreducible.lean b/CompPoly/Fields/KoalaBear/Ext6/SexticIrreducible.lean index bd5a293f..ea861c9a 100644 --- a/CompPoly/Fields/KoalaBear/Ext6/SexticIrreducible.lean +++ b/CompPoly/Fields/KoalaBear/Ext6/SexticIrreducible.lean @@ -138,7 +138,7 @@ 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_of_card fieldSize hcard sexticPoly_natDegree ?_ ?_ ?_ + refine irreducible_of_rabin_degree_six_of_card hcard sexticPoly_natDegree ?_ ?_ ?_ · exact dvd_X_pow_sub_X_of_runChain toPoly_sexticL sexticPoly_ne_zero sextic_trace_chain sextic_trace_exp · exact isCoprime_X_pow_sub_X_of_runChain toPoly_sexticL sexticPoly_ne_zero diff --git a/docs/wiki/field-extensions.md b/docs/wiki/field-extensions.md index 2f1dbd3e..56f5e2d6 100644 --- a/docs/wiki/field-extensions.md +++ b/docs/wiki/field-extensions.md @@ -50,7 +50,7 @@ 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` | +| 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 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]`) | | 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` | @@ -144,6 +144,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` @@ -212,11 +221,14 @@ That is about 60 lines. 2. Generate the certificate module: `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/`). diff --git a/tests/CompPolyTests/Data/Polynomial/RabinCertificate.lean b/tests/CompPolyTests/Data/Polynomial/RabinCertificate.lean index e83a2fc1..d1841d27 100644 --- a/tests/CompPolyTests/Data/Polynomial/RabinCertificate.lean +++ b/tests/CompPolyTests/Data/Polynomial/RabinCertificate.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 From ac87a1bc6e6b8665c8a70a83768b15767fd4c6d8 Mon Sep 17 00:00:00 2001 From: Bolton Bailey Date: Mon, 17 Aug 2026 17:59:17 -0700 Subject: [PATCH 03/45] refactor: relocate and rename the `Multivariate/Wheels.lean` lemmas (#305) `Multivariate/Wheels.lean` held two lemmas in the root `List` and `Option` namespaces with no multivariate content, which forced a cross-subtree import just to reach them. Move them to the repo's homes for generic lemmas and give them Mathlib-conforming names and proofs. - `List.distinct_of_inj_nodup` -> `List.Nodup.pairwise_ne_map` in `Data/List/Lemmas.lean`. The statement is `List.Nodup.map` transported along `List.pairwise_map`, so the induction, `aesop`, and `grind` collapse to a one-line term proof. - `Option.filter_irrel` -> `Option.filter_eq_self` in the new `Data/Option/Lemmas.lean`, generalized to `Type*` and restated as an iff to match `List.filter_eq_self` and `Array.filter_eq_self`. Proof is `cases o <;> simp`. `Multivariate/MvPolyEquiv/Core.lean` now imports `Data/List/Lemmas.lean` directly instead of picking the lemma up transitively through `Unlawful`. Co-authored-by: Claude Opus 5 (1M context) Co-authored-by: Alexander Hicks <25369263+alexanderlhicks@users.noreply.github.com> --- CompPoly.lean | 2 +- CompPoly/Data/List/Lemmas.lean | 9 +++++++ CompPoly/Data/Option/Lemmas.lean | 24 +++++++++++++++++ CompPoly/Multivariate/MvPolyEquiv/Core.lean | 7 ++--- CompPoly/Multivariate/Unlawful.lean | 1 - CompPoly/Multivariate/Wheels.lean | 29 --------------------- 6 files changed, 38 insertions(+), 34 deletions(-) create mode 100644 CompPoly/Data/Option/Lemmas.lean delete mode 100644 CompPoly/Multivariate/Wheels.lean diff --git a/CompPoly.lean b/CompPoly.lean index d069815a..8a28e5d8 100644 --- a/CompPoly.lean +++ b/CompPoly.lean @@ -59,6 +59,7 @@ 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 @@ -190,7 +191,6 @@ 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 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/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/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) From a09455a22fea4623a2a1c5b363cf6efc61486a83 Mon Sep 17 00:00:00 2001 From: Devon Tuma Date: Thu, 27 Aug 2026 19:06:00 -0500 Subject: [PATCH 04/45] chore: migrate CompPoly to Lean v4.33.1 (#309) --- lake-manifest.json | 4 ++-- lakefile.lean | 2 +- lean-toolchain | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) 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..dc2d9687 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 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 From c6b153029fd1ee00c0e5d0b4aff1d90a8c4a79b0 Mon Sep 17 00:00:00 2001 From: Kobi Date: Tue, 1 Sep 2026 14:53:19 -0400 Subject: [PATCH 05/45] doc(RabinCertificate): clarify the cold-replay mechanism (#308) Co-authored-by: Derek Sorensen --- CompPoly/Data/Polynomial/RabinCertificate.lean | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/CompPoly/Data/Polynomial/RabinCertificate.lean b/CompPoly/Data/Polynomial/RabinCertificate.lean index d5fe966f..b02c7c0e 100644 --- a/CompPoly/Data/Polynomial/RabinCertificate.lean +++ b/CompPoly/Data/Polynomial/RabinCertificate.lean @@ -405,8 +405,12 @@ caller-supplied `q` with `hcard : Fintype.card F = q`. Same shape as 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, rather than first casting the goal with -`rw [hcard]`. Supply `hcard` as `ZMod.card _`. +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 From ba5032e1486616bf1bc2ec1cf185c155f08f5ed7 Mon Sep 17 00:00:00 2001 From: Varun Thakore Date: Tue, 1 Sep 2026 15:03:51 -0400 Subject: [PATCH 06/45] feat(fields): add fast Mersenne31 arithmetic (#257) * feat(fields): add Mersenne31 field scaffold * feat(fields): implement fast Mersenne31 arithmetic * chore(fields): add comments * refactor(fields): modularize Mersenne31 basic and fast implementations * doc(fields): add Mersenne31 to readme * test(fields): add Mersenne31 tests * fix(fields): export Mersenne compatibility module --------- Co-authored-by: Derek Sorensen Co-authored-by: Derek Sorensen --- CompPoly.lean | 3 + CompPoly/Fields/Mersenne.lean | 23 +- CompPoly/Fields/Mersenne31.lean | 20 + CompPoly/Fields/Mersenne31/Basic.lean | 48 + CompPoly/Fields/Mersenne31/Fast.lean | 980 ++++++++++++++++++ CompPoly/Fields/README.md | 5 +- tests/CompPolyTests.lean | 1 + .../CompPolyTests/Fields/Mersenne31/Fast.lean | 44 + 8 files changed, 1105 insertions(+), 19 deletions(-) create mode 100644 CompPoly/Fields/Mersenne31.lean create mode 100644 CompPoly/Fields/Mersenne31/Basic.lean create mode 100644 CompPoly/Fields/Mersenne31/Fast.lean create mode 100644 tests/CompPolyTests/Fields/Mersenne31/Fast.lean diff --git a/CompPoly.lean b/CompPoly.lean index 8a28e5d8..645e54ad 100644 --- a/CompPoly.lean +++ b/CompPoly.lean @@ -132,6 +132,9 @@ 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 diff --git a/CompPoly/Fields/Mersenne.lean b/CompPoly/Fields/Mersenne.lean index fea1c22f..5ca22e5d 100644 --- a/CompPoly/Fields/Mersenne.lean +++ b/CompPoly/Fields/Mersenne.lean @@ -1,29 +1,16 @@ /- 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.Mersenne31 /-! - # Mersenne prime field `2^{31} - 1` +# Deprecated Mersenne31 compatibility import - This is the field used in Circle STARKs. +This module re-exports `CompPoly.Fields.Mersenne31` for compatibility with the former import path. -/ -@[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 +deprecated_module "Use `CompPoly.Fields.Mersenne31` instead" (since := "2026-08-25") 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/README.md b/CompPoly/Fields/README.md index b46fb53d..311770c3 100644 --- a/CompPoly/Fields/README.md +++ b/CompPoly/Fields/README.md @@ -40,7 +40,10 @@ This directory contains formally verified field infrastructure used in zero-know | **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. | +| **Mersenne.lean** | Deprecated compatibility re-export of `Mersenne31.lean`; retained so existing imports continue to work. | | **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. | diff --git a/tests/CompPolyTests.lean b/tests/CompPolyTests.lean index 46acc6bf..aabf7ba8 100644 --- a/tests/CompPolyTests.lean +++ b/tests/CompPolyTests.lean @@ -32,6 +32,7 @@ public import CompPolyTests.Fields.Binary.BF128Ghash.Prelude public import CompPolyTests.Fields.Extension.Arithmetic public import CompPolyTests.Fields.Extension.Binomial public import CompPolyTests.Fields.KoalaBear.Fast +public import CompPolyTests.Fields.Mersenne31.Fast public import CompPolyTests.Fields.PrattCertificate public import CompPolyTests.LinearAlgebra.Dense public import CompPolyTests.Multilinear.Equiv diff --git a/tests/CompPolyTests/Fields/Mersenne31/Fast.lean b/tests/CompPolyTests/Fields/Mersenne31/Fast.lean new file mode 100644 index 00000000..954aaa3f --- /dev/null +++ b/tests/CompPolyTests/Fields/Mersenne31/Fast.lean @@ -0,0 +1,44 @@ +/- +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)) + +end Mersenne31.Fast From bfea0dc853f13fa5ddd8da5fc0807d9a6a2d5106 Mon Sep 17 00:00:00 2001 From: Alexander Hicks <25369263+alexanderlhicks@users.noreply.github.com> Date: Tue, 1 Sep 2026 20:11:07 +0100 Subject: [PATCH 07/45] feat(scripts): kernel-level axiom sweep with committed regression baseline (#300) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(scripts): kernel-level axiom sweep with committed regression baseline Add `lake exe axiomsweep`: walks the compiled environment and computes, for every CompPoly.* declaration, its transitive axiom dependencies — the #print axioms information, library-wide, in one pass. Reads elaborated .olean data, so private and macro-generated declarations are included and no source heuristics are involved. Baseline at this commit: 7589 declarations across 275 modules, 0 sorryAx-tainted, 0 non-standard-axiom-tainted — the library is fully kernel-clean, and --check now keeps it that way (fails iff a declaration is tainted that scripts/axiom_baseline.json does not list; the 6 'sorry' tokens greps report on main are all inside comments). Wire-up: report-only CI step in lean_action_ci.yml after the warm rebuild; docs/wiki/quickstart.md documents the workflow. Co-Authored-By: Claude Fable 5 * fix(axiomsweep): adversarial-review fixes — fixpoint collector, floor, docs Collector: two-phase DFS + fixpoint repair. The one-pass DFS finalized self-referencing constants (every inductive/ctor pair) prematurely and memoized the wrong result for all later roots — confirmed by review to produce rows diverging from #print axioms on sibling repos. The repair pass re-derives every set in finalization order until stable: the least fixpoint = true kernel closure, strictly more accurate than #print axioms inside mutual families. Also: axiom *types* are traversed (CollectAxioms parity), duplicate constNames rows deduped (7589→7573), native trust-axiom names normalized to their owner (ax_N_M counters are Elab.async/toolchain-volatile), --check/--update-baseline mutually exclusive, unknown --root fails gracefully, nonstandard shrinkage detected, and bare Lean.ofReduceBool/Lean.trustCompiler are never baselinable (floor). Docs: known blind spots documented (structure-field defaults and examples never enter any environment walk; unimported files — paired with check_imports); scope stated (tests/ and bench/ outside the sweep); corrected sorry-token count (5, all comments); inventory rows added to scripts/README.md, docs/wiki/generated-files.md, AGENTS.md fast-start, quickstart CI mapping + lower-level commands. CI: infrastructure failures (exit != 1) now fail the step; only taint findings are report-only during the soak. Co-Authored-By: Claude Fable 5 * chore(axiomsweep): rewrap overlong docstring line; annotate UInt32 literal for 4.30/4.31 portability Co-Authored-By: Claude Fable 5 * bug: test the CI sweep * fix: remove bug * fix(axiomsweep): enforce taint policy --------- Co-authored-by: Claude Fable 5 Co-authored-by: Derek Sorensen Co-authored-by: Derek Sorensen --- .github/workflows/lean_action_ci.yml | 2 + AGENTS.md | 4 + docs/wiki/generated-files.md | 1 + docs/wiki/quickstart.md | 30 ++- lakefile.lean | 8 + scripts/AxiomSweep.lean | 358 +++++++++++++++++++++++++++ scripts/README.md | 11 + scripts/axiom_baseline.json | 1 + 8 files changed, 413 insertions(+), 2 deletions(-) create mode 100644 scripts/AxiomSweep.lean create mode 100644 scripts/axiom_baseline.json diff --git a/.github/workflows/lean_action_ci.yml b/.github/workflows/lean_action_ci.yml index 5e3548eb..3e3f9999 100644 --- a/.github/workflows/lean_action_ci.yml +++ b/.github/workflows/lean_action_ci.yml @@ -200,6 +200,8 @@ jobs: run: | bash scripts/build_timing_report.sh run test_path "$BUILD_TIMING_RESULTS" -- \ bash -eo pipefail -c 'lake test' + - name: Axiom sweep + run: lake exe axiomsweep --check - name: Fail on build warnings continue-on-error: true run: lake build --wfail 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/docs/wiki/generated-files.md b/docs/wiki/generated-files.md index 0655063f..da4b08d9 100644 --- a/docs/wiki/generated-files.md +++ b/docs/wiki/generated-files.md @@ -9,6 +9,7 @@ This page records which paths are source of truth and which are derived outputs. | `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. | +| `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. | diff --git a/docs/wiki/quickstart.md b/docs/wiki/quickstart.md index fbbee6f1..1d178b47 100644 --- a/docs/wiki/quickstart.md +++ b/docs/wiki/quickstart.md @@ -37,6 +37,29 @@ lake build lake test ``` +### 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 +`CompPoly.*` declaration, 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 @@ -82,8 +105,10 @@ to be covered there. See [`../../bench/README.md`](../../bench/README.md). - [`../../.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 + oleans so only dirty modules rebuild — then `lake test`, then the axiom sweep + as an enforcing gate, 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. `BENCH_CI_GROUPS` selection, then uploads benchmark reports as CI artifacts. A full cold rebuild (`rm -rf .lake/build && lake build`) runs automatically when `lean-toolchain` or `lake-manifest.json` differs from the comparison base @@ -129,6 +154,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/lakefile.lean b/lakefile.lean index dc2d9687..35947739 100644 --- a/lakefile.lean +++ b/lakefile.lean @@ -41,3 +41,11 @@ lean_lib CompPolyBenchLib where 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/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": []} From 8e84a81532bbeeee085757e04c31c6fd539c68f7 Mon Sep 17 00:00:00 2001 From: Georgios Raikos <57717352+graikos@users.noreply.github.com> Date: Wed, 2 Sep 2026 12:53:53 +0300 Subject: [PATCH 08/45] feat(fields): fast binary tower fields (up to 128 bits) (#286) * feat(fields): initial fast binary towers (k <= 6) * test(fields): fast binary tower regression guards * docs(fields): fast binary tower rows in README and wiki * feat(fields): fast binary towers multiplication + proofs * feat(fields): fast binary towers mult, sqr and inv * feat(fields): field instance for fast binary towers * feat(fields): small optimizations for towers * feat(fields): port fast binary tower onto main (Lean 4.32, module system) Module headers on Fast.lean and its tests, de-private helpers the module system requires in exposed bodies, regenerated import lists. Co-Authored-By: Claude Fable 5 * feat(fields): removed external C from fast BT; using tables for GF(2^8) * bench(fields): added benchmarks for fast BT implementation * feat(fields): minro proof changes in fast BT * chore(fields): added minor test case in fast BT * docs(fields): adding/modifying comments * refactor(fields): split defs in fast BT for zero-import * bench(fields): fixed result naming in fast BT benchmarks * bench(fields): fast BT smaller units in benchmarks --------- Co-authored-by: Claude Fable 5 Co-authored-by: Derek Sorensen Co-authored-by: Derek Sorensen --- .github/workflows/lean_action_ci.yml | 1 + CompPoly.lean | 2 + CompPoly/Fields/Binary/Tower/Fast.lean | 1422 +++++++++++++++++ CompPoly/Fields/Binary/Tower/FastDefs.lean | 412 +++++ CompPoly/Fields/README.md | 2 + bench/CompPolyBench/Common.lean | 10 +- bench/CompPolyBench/Fields/Binary/Tower.lean | 99 ++ bench/CompPolyBench/Setup.lean | 4 +- bench/README.md | 1 + docs/wiki/binary-fields-and-ntt.md | 4 + tests/CompPolyTests.lean | 1 + .../Fields/Binary/Tower/Fast.lean | 98 ++ 12 files changed, 2053 insertions(+), 3 deletions(-) create mode 100644 CompPoly/Fields/Binary/Tower/Fast.lean create mode 100644 CompPoly/Fields/Binary/Tower/FastDefs.lean create mode 100644 bench/CompPolyBench/Fields/Binary/Tower.lean create mode 100644 tests/CompPolyTests/Fields/Binary/Tower/Fast.lean diff --git a/.github/workflows/lean_action_ci.yml b/.github/workflows/lean_action_ci.yml index 3e3f9999..252411d5 100644 --- a/.github/workflows/lean_action_ci.yml +++ b/.github/workflows/lean_action_ci.yml @@ -43,6 +43,7 @@ env: 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, diff --git a/CompPoly.lean b/CompPoly.lean index 645e54ad..3470f905 100644 --- a/CompPoly.lean +++ b/CompPoly.lean @@ -104,6 +104,8 @@ public import CompPoly.Fields.Binary.Tower.Concrete.Basis public import CompPoly.Fields.Binary.Tower.Concrete.Core public import CompPoly.Fields.Binary.Tower.Concrete.Field 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 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/README.md b/CompPoly/Fields/README.md index 311770c3..468b9374 100644 --- a/CompPoly/Fields/README.md +++ b/CompPoly/Fields/README.md @@ -62,6 +62,8 @@ The `Binary/` subtree provides characteristic-2 field infrastructure used by GHA - `Binary/BF128Ghash/*` — GF(2^128) model, implementation, and certificates. - `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 diff --git a/bench/CompPolyBench/Common.lean b/bench/CompPolyBench/Common.lean index 4d3d4d6d..d8f7f918 100644 --- a/bench/CompPolyBench/Common.lean +++ b/bench/CompPolyBench/Common.lean @@ -255,6 +255,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 @@ -826,9 +832,9 @@ def groupResultColumns (records : List BenchRecord) (totalUnit avgUnit : TimeUni ("Implementation", false, implementationLabelInGroup records), ("Iterations", true, fun r ↦ toString r.measuredIterations), ("Total (" ++ totalUnit.label ++ ")", true, fun r ↦ - formatNanosInUnit totalUnit r.totalNanos), + formatNanosInUnitOrAuto totalUnit r.totalNanos), ("Avg (" ++ avgUnit.label ++ ")", true, fun r ↦ - formatNanosInUnit avgUnit r.averageNanos) + formatNanosInUnitOrAuto avgUnit r.averageNanos) ] /-- Shared metadata rendered before each benchmark group result table. -/ diff --git a/bench/CompPolyBench/Fields/Binary/Tower.lean b/bench/CompPolyBench/Fields/Binary/Tower.lean new file mode 100644 index 00000000..707537b8 --- /dev/null +++ b/bench/CompPolyBench/Fields/Binary/Tower.lean @@ -0,0 +1,99 @@ +/- +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" + +/-- Benchmark group metadata for the binary tower field. -/ +def towerGroupInfos : List BenchGroupInfo := [ + ⟨"fields-tower-bt128-mul", "Binary tower multiplication (GF(2^128))"⟩, + ⟨"fields-tower-bt128-inv", "Binary tower inversion (GF(2^128))"⟩ +] + +/-- 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) + (concreteBudget fastBudget : BenchPreset → Nat) + (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 warmup := warmupIterations preset + let concreteMeasured := concreteBudget preset + let fastMeasured := fastBudget preset + let checksumIterations := groupChecksumIterations concreteMeasured [fastMeasured] + let concreteRecord ← runTimed "tower-bt128" "ConcreteBTField" + (method ++ " (ConcreteBTField)") "GF(2^128)" + towerShape preset warmup concreteMeasured + (fun i ↦ let (a, b) := concreteSample i; concreteOp a b) + checksumConcreteBt128 (checksumIterations := checksumIterations) + let fastRecord ← runTimed "tower-bt128-fast" "FastBT128" + (method ++ " (FastBT128)") "GF(2^128)" + towerShape preset warmup fastMeasured + (fun i ↦ let (a, b) := fastSample i; fastOp a b) + checksumFastBT128 (checksumIterations := checksumIterations) + 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 + (fun p ↦ p.selectNat 1000 150 30) (fun p ↦ p.selectNat 2000000 300000 60000) + 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) + (fun p ↦ p.selectNat 500 75 15) (fun p ↦ p.selectNat 500000 75000 15000) + 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/Setup.lean b/bench/CompPolyBench/Setup.lean index d0c1360a..e4a6a27d 100644 --- a/bench/CompPolyBench/Setup.lean +++ b/bench/CompPolyBench/Setup.lean @@ -9,6 +9,7 @@ public import CompPolyBench.Bivariate.Basic public import CompPolyBench.Bivariate.Factor public import CompPolyBench.Bivariate.GuruswamiSudan public import CompPolyBench.Fields.Binary.AdditiveNTT.Impl +public import CompPolyBench.Fields.Binary.Tower public import CompPolyBench.Fields.Extension public import CompPolyBench.Fields.Montgomery public import CompPolyBench.Multilinear.Basic @@ -28,7 +29,8 @@ namespace CompPolyBench /-- Runnable benchmark registry. -/ def allTasks : List BenchTask := univariateTasks ++ multivariateTasks ++ multilinearTasks ++ bivariateTasks ++ factorTasks ++ - guruswamiSudanTasks ++ additiveNttTasks ++ extensionTasks ++ montgomeryInvTasks + guruswamiSudanTasks ++ additiveNttTasks ++ extensionTasks ++ montgomeryInvTasks ++ + towerTasks /-- Metadata for every benchmark group accepted by the command-line selector. -/ def allGroupInfos : List BenchGroupInfo := diff --git a/bench/README.md b/bench/README.md index 8b497b1c..85152a26 100644 --- a/bench/README.md +++ b/bench/README.md @@ -73,6 +73,7 @@ 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 | | Scalar-field inversion | `fields-mont64x8-*-inv`: `ZMod` extended Euclid vs checked binary GCD vs Fermat | Use `--list` for the authoritative set; the prefixes above drift as groups are diff --git a/docs/wiki/binary-fields-and-ntt.md b/docs/wiki/binary-fields-and-ntt.md index 6a9f64b2..f9f7ff99 100644 --- a/docs/wiki/binary-fields-and-ntt.md +++ b/docs/wiki/binary-fields-and-ntt.md @@ -76,6 +76,10 @@ support lemmas: - `Tower/Concrete/*` - concrete basis, core definitions, and field instances. - `Tower/Support/*` - supporting lemmas about defining polynomials, linear independence, and finite-index helpers. +- `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`, `Tower/Impl.lean`, and `Tower/TensorAlgebra.lean` connect the layers and expose useful transport lemmas. diff --git a/tests/CompPolyTests.lean b/tests/CompPolyTests.lean index aabf7ba8..be5bbfe8 100644 --- a/tests/CompPolyTests.lean +++ b/tests/CompPolyTests.lean @@ -29,6 +29,7 @@ public import CompPolyTests.Fields.BN254.Fast public import CompPolyTests.Fields.BabyBear.Fast public import CompPolyTests.Fields.Binary.AdditiveNTT.NovelPolynomialBasis public import CompPolyTests.Fields.Binary.BF128Ghash.Prelude +public import CompPolyTests.Fields.Binary.Tower.Fast public import CompPolyTests.Fields.Extension.Arithmetic public import CompPolyTests.Fields.Extension.Binomial public import CompPolyTests.Fields.KoalaBear.Fast 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 From 40ecb64665d3921d105b1b4d45d24fee9942fdd3 Mon Sep 17 00:00:00 2001 From: Adrien Lacombe <6303520+adrienlacombe@users.noreply.github.com> Date: Wed, 2 Sep 2026 12:03:02 +0200 Subject: [PATCH 09/45] test(fields): extend Mersenne31 regression coverage (#260) Co-authored-by: Derek Sorensen --- tests/CompPolyTests.lean | 1 + .../CompPolyTests/Fields/Mersenne31/Fast.lean | 2 + .../Fields/Mersenne31/Instances.lean | 37 +++++++++++++++++++ 3 files changed, 40 insertions(+) create mode 100644 tests/CompPolyTests/Fields/Mersenne31/Instances.lean diff --git a/tests/CompPolyTests.lean b/tests/CompPolyTests.lean index be5bbfe8..c4f26e4c 100644 --- a/tests/CompPolyTests.lean +++ b/tests/CompPolyTests.lean @@ -34,6 +34,7 @@ public import CompPolyTests.Fields.Extension.Arithmetic public import CompPolyTests.Fields.Extension.Binomial public import CompPolyTests.Fields.KoalaBear.Fast public import CompPolyTests.Fields.Mersenne31.Fast +public import CompPolyTests.Fields.Mersenne31.Instances public import CompPolyTests.Fields.PrattCertificate public import CompPolyTests.LinearAlgebra.Dense public import CompPolyTests.Multilinear.Equiv diff --git a/tests/CompPolyTests/Fields/Mersenne31/Fast.lean b/tests/CompPolyTests/Fields/Mersenne31/Fast.lean index 954aaa3f..0d305c9d 100644 --- a/tests/CompPolyTests/Fields/Mersenne31/Fast.lean +++ b/tests/CompPolyTests/Fields/Mersenne31/Fast.lean @@ -40,5 +40,7 @@ namespace Mersenne31.Fast #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 From e66e43617740d5ba1a55f23ec850f854d8c9b42e Mon Sep 17 00:00:00 2001 From: Derek Sorensen Date: Wed, 2 Sep 2026 11:50:27 +0100 Subject: [PATCH 10/45] feat(fields): add fast Goldilocks arithmetic (#311) * feat(fields): add fast Goldilocks arithmetic * tests(fields): add tests for goldilocks * refactor(fields): reland fast Goldilocks without the C extern tier Bring the relanded work up to current repo conventions. Namespace: `Basic.lean` used a nested `namespace Goldilocks.Basic`, which renamed the public API to `Goldilocks.Basic.fieldSize` / `.Field` and broke `bench/CompPolyBench/Common.lean`. Use plain `namespace Goldilocks`, matching `Mersenne31/Basic.lean`, so the existing names survive. Layout: collapse `Fast/{Internal,Reduction,Arithmetic,Theorems,Field}.lean` into `Fast.lean` plus a zero-import `FastDefs.lean`, matching the two-file idiom of `Binary/Tower/{Fast,FastDefs}.lean`. No field in `Fields/` has a `Fast/` subdirectory, and a single module is what makes `private` usable across these declarations. `FastDefs.lean` holds only raw word kernels so `precompileModules` lanes can compile it without pulling in mathlib. Module system: add `module` headers, `public import`, and `@[expose] public section`; the test file uses `public meta` since `#guard` evaluates compiled code during elaboration. Also rename `neg_modulus` to `negModulus` (`def`s are lowerCamelCase) and replace `letI` with `let` in a proof, per the style linter. * feat(fields): complete the Goldilocks fast bridge and split word-level proofs Add the `raw_*` / `toNat_*` bridge lemmas that `Mersenne31/Fast.lean` carries and this implementation omitted: `raw_mk`, `raw_eq_val`, `raw_zero`, `raw_one`, `toNat_mk`, `toNat_eq_val_toNat`, `toNat_zero`, `toNat_one`, `toNat_ofNat`, `toNat_ofUInt64`, and `toNat_ofField`. Only the `toField_*` half of the bridge was proved before, which left the natural-representative side unavailable to `simp`. `toNat_ofUInt64` reduces to the raw word before case-splitting: unfolding the subtype first leaves the membership proof depending on the term being rewritten. Split the word level into `FastReduction.lean` (low-level `UInt64` lemmas and raw kernel correctness) so no file exceeds the 1500-line lint limit. The three modules now divide by role: `FastDefs` runtime kernels, `FastReduction` their correctness, `Fast` the carrier, operations, canonical bridge, and instances. Also mark `invExponent` private and drop the `@[noinline]` on `inv`, which diverged from Mersenne31 without justification. * feat(bench): add Goldilocks arithmetic benchmark group Add `fields-goldilocks-mul` and `fields-goldilocks-inv`, each running the canonical `ZMod` implementation against the verified native-word one on shared inputs so the group checksum cross-checks the two. Registered last in `allTasks` because adding a group shifts the shared `StdGen` and would otherwise change the checksums of every group after it, and wired into `BENCH_CI_GROUPS` so CI covers them. Measured at the medium preset: multiplication 810ns to 643ns, inversion 10.59us to 1.04us. The modest multiplication ratio is because array indexing dominates that loop for both arms; inversion is the real win. `checksumGoldilocksFast` calls `Goldilocks.Fast.toNat` directly, since the carrier is an `abbrev` for a `Subtype` and dot notation would resolve to `Subtype.toNat`. Drop the local `Fact (Nat.Prime Goldilocks.fieldSize)` instance from the benchmark helpers, now redundant with the one in `Goldilocks/Basic.lean`. Document the field in `Fields/README.md`, `README.md`, `ROADMAP.md`, and the benchmark README. --------- Co-authored-by: Varun Thakore --- .github/workflows/lean_action_ci.yml | 3 +- CompPoly.lean | 4 + CompPoly/Fields/Goldilocks.lean | 23 +- CompPoly/Fields/Goldilocks/Basic.lean | 48 + CompPoly/Fields/Goldilocks/Fast.lean | 679 ++++++++++++++ CompPoly/Fields/Goldilocks/FastDefs.lean | 106 +++ CompPoly/Fields/Goldilocks/FastReduction.lean | 883 ++++++++++++++++++ CompPoly/Fields/README.md | 5 +- README.md | 2 +- ROADMAP.md | 3 + bench/CompPolyBench/Common.lean | 17 +- bench/CompPolyBench/Fields/Goldilocks.lean | 87 ++ bench/CompPolyBench/Setup.lean | 3 +- bench/README.md | 1 + tests/CompPolyTests.lean | 1 + .../CompPolyTests/Fields/Goldilocks/Fast.lean | 46 + 16 files changed, 1887 insertions(+), 24 deletions(-) create mode 100644 CompPoly/Fields/Goldilocks/Basic.lean create mode 100644 CompPoly/Fields/Goldilocks/Fast.lean create mode 100644 CompPoly/Fields/Goldilocks/FastDefs.lean create mode 100644 CompPoly/Fields/Goldilocks/FastReduction.lean create mode 100644 bench/CompPolyBench/Fields/Goldilocks.lean create mode 100644 tests/CompPolyTests/Fields/Goldilocks/Fast.lean diff --git a/.github/workflows/lean_action_ci.yml b/.github/workflows/lean_action_ci.yml index 252411d5..0144f60f 100644 --- a/.github/workflows/lean_action_ci.yml +++ b/.github/workflows/lean_action_ci.yml @@ -47,7 +47,8 @@ env: 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-mont64x8-bls12-377-inv, + fields-goldilocks-mul,fields-goldilocks-inv jobs: build: diff --git a/CompPoly.lean b/CompPoly.lean index 3470f905..8cb122d1 100644 --- a/CompPoly.lean +++ b/CompPoly.lean @@ -120,6 +120,10 @@ public import CompPoly.Fields.Extension.Bridge 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 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/README.md b/CompPoly/Fields/README.md index 468b9374..aa8d7ddd 100644 --- a/CompPoly/Fields/README.md +++ b/CompPoly/Fields/README.md @@ -34,9 +34,12 @@ 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. | 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..24921aee 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -107,6 +107,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` diff --git a/bench/CompPolyBench/Common.lean b/bench/CompPolyBench/Common.lean index d8f7f918..a2a8d454 100644 --- a/bench/CompPolyBench/Common.lean +++ b/bench/CompPolyBench/Common.lean @@ -120,10 +120,6 @@ def additiveNttMeasuredIterations (preset : BenchPreset) : Nat := 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 name : String @@ -431,6 +427,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 @@ -487,6 +487,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 @@ -822,6 +829,8 @@ 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 ++ ")" diff --git a/bench/CompPolyBench/Fields/Goldilocks.lean b/bench/CompPolyBench/Fields/Goldilocks.lean new file mode 100644 index 00000000..9c31bd14 --- /dev/null +++ b/bench/CompPolyBench/Fields/Goldilocks.lean @@ -0,0 +1,87 @@ +/- +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 CompPolyBench.Common +public import CompPoly.Fields.Goldilocks + +/-! +# Goldilocks field arithmetic benchmarks + +Times multiplication and inversion over the Goldilocks prime `2^64 - 2^32 + 1`. Each +group runs the canonical `ZMod` implementation and the verified native-word +implementation on shared inputs, so the group checksum cross-checks the two. + +Goldilocks fits neither Montgomery carrier — `Mont32Field` requires modulus `< 2^31` +and `Mont64x8Field` is an eight-limb layout — so the fast path is the single-word +`UInt64` implementation in `CompPoly.Fields.Goldilocks.Fast`. +-/ + +public section + +namespace CompPolyBench + +/-- Input-shape label shared by the Goldilocks arithmetic benchmarks. -/ +private def goldilocksShape : String := "256 random elements" + +/-- Benchmark group metadata for Goldilocks arithmetic. -/ +def goldilocksGroupInfos : List BenchGroupInfo := [ + ⟨"fields-goldilocks-mul", "Goldilocks multiplication"⟩, + ⟨"fields-goldilocks-inv", "Goldilocks inversion"⟩ +] + +/-- Time canonical against native-word Goldilocks multiplication as a single group. -/ +private def runGoldilocksMul (preset : BenchPreset) (gen : StdGen) : + IO (BenchGroup × StdGen) := do + let (values, gen) := (zmodArray Goldilocks.fieldSize 256 false).run gen + let fastValues := goldilocksFastArray values + let warmup := warmupIterations preset + let zmodMeasured := preset.selectNat 200000 30000 6000 + let fastMeasured := preset.selectNat 200000 30000 6000 + let checksumIterations := groupChecksumIterations zmodMeasured [fastMeasured] + let zmodRecord ← runTimed "goldilocks-mul-zmod" "ZMod" "mul" "Goldilocks.Field" + goldilocksShape preset warmup zmodMeasured + (fun i ↦ values.getD (i % values.size) 1 * values.getD ((i + 1) % values.size) 1) + checksumZMod (checksumIterations := checksumIterations) + let fastRecord ← runTimed "goldilocks-mul-fast" "UInt64" "mul" "Goldilocks.Fast.Field" + goldilocksShape preset warmup fastMeasured + (fun i ↦ fastValues.getD (i % fastValues.size) 1 * + fastValues.getD ((i + 1) % fastValues.size) 1) + checksumGoldilocksFast (checksumIterations := checksumIterations) + pure ({ groupKey := "fields-goldilocks-mul", title := "Goldilocks multiplication", + records := #[zmodRecord, fastRecord] }, gen) + +/-- Time canonical against native-word Goldilocks inversion as a single group. -/ +private def runGoldilocksInv (preset : BenchPreset) (gen : StdGen) : + IO (BenchGroup × StdGen) := do + let (values, gen) := (zmodArray Goldilocks.fieldSize 256 false).run gen + let fastValues := goldilocksFastArray values + let warmup := warmupIterations preset + let zmodMeasured := preset.selectNat 20000 3000 600 + let fastMeasured := preset.selectNat 24000 3600 720 + let checksumIterations := groupChecksumIterations zmodMeasured [fastMeasured] + let zmodRecord ← runTimed "goldilocks-inv-zmod" "ZMod" "inv" "Goldilocks.Field" + goldilocksShape preset warmup zmodMeasured + (fun i ↦ (values.getD (i % values.size) 1)⁻¹) + checksumZMod (checksumIterations := checksumIterations) + let fastRecord ← runTimed "goldilocks-inv-fast" "UInt64" "inv (Fermat chain)" + "Goldilocks.Fast.Field" goldilocksShape preset warmup fastMeasured + (fun i ↦ (fastValues.getD (i % fastValues.size) 1)⁻¹) + checksumGoldilocksFast (checksumIterations := checksumIterations) + pure ({ groupKey := "fields-goldilocks-inv", title := "Goldilocks inversion", + records := #[zmodRecord, fastRecord] }, gen) + +/-- Registry entries for the Goldilocks arithmetic benchmarks. -/ +def goldilocksTasks : List BenchTask := [ + BenchTask.fromGroupRunner + ⟨"fields-goldilocks-mul", "Goldilocks multiplication"⟩ + runGoldilocksMul, + BenchTask.fromGroupRunner + ⟨"fields-goldilocks-inv", "Goldilocks inversion"⟩ + runGoldilocksInv +] + +end CompPolyBench diff --git a/bench/CompPolyBench/Setup.lean b/bench/CompPolyBench/Setup.lean index e4a6a27d..66575c35 100644 --- a/bench/CompPolyBench/Setup.lean +++ b/bench/CompPolyBench/Setup.lean @@ -11,6 +11,7 @@ public import CompPolyBench.Bivariate.GuruswamiSudan public import CompPolyBench.Fields.Binary.AdditiveNTT.Impl public import CompPolyBench.Fields.Binary.Tower public import CompPolyBench.Fields.Extension +public import CompPolyBench.Fields.Goldilocks public import CompPolyBench.Fields.Montgomery public import CompPolyBench.Multilinear.Basic public import CompPolyBench.Multivariate.CMvPolynomial @@ -30,7 +31,7 @@ namespace CompPolyBench def allTasks : List BenchTask := univariateTasks ++ multivariateTasks ++ multilinearTasks ++ bivariateTasks ++ factorTasks ++ guruswamiSudanTasks ++ additiveNttTasks ++ extensionTasks ++ montgomeryInvTasks ++ - towerTasks + towerTasks ++ goldilocksTasks /-- Metadata for every benchmark group accepted by the command-line selector. -/ def allGroupInfos : List BenchGroupInfo := diff --git a/bench/README.md b/bench/README.md index 85152a26..d4b186ba 100644 --- a/bench/README.md +++ b/bench/README.md @@ -74,6 +74,7 @@ Roughly by area, with representative group prefixes: | 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 | +| Goldilocks arithmetic | `fields-goldilocks-{mul,inv}`: canonical `ZMod` vs single-word `UInt64` | | Scalar-field inversion | `fields-mont64x8-*-inv`: `ZMod` extended Euclid vs checked binary GCD vs Fermat | Use `--list` for the authoritative set; the prefixes above drift as groups are diff --git a/tests/CompPolyTests.lean b/tests/CompPolyTests.lean index c4f26e4c..1aadd98c 100644 --- a/tests/CompPolyTests.lean +++ b/tests/CompPolyTests.lean @@ -32,6 +32,7 @@ public import CompPolyTests.Fields.Binary.BF128Ghash.Prelude public import CompPolyTests.Fields.Binary.Tower.Fast public import CompPolyTests.Fields.Extension.Arithmetic public import CompPolyTests.Fields.Extension.Binomial +public import CompPolyTests.Fields.Goldilocks.Fast public import CompPolyTests.Fields.KoalaBear.Fast public import CompPolyTests.Fields.Mersenne31.Fast public import CompPolyTests.Fields.Mersenne31.Instances 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 From 19f07941500044950426b9ebccd96181a0205487 Mon Sep 17 00:00:00 2001 From: Derek Sorensen Date: Wed, 2 Sep 2026 15:07:33 +0100 Subject: [PATCH 11/45] feat(univariate): Shoup and Las Vegas root-search backends (#290) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(univariate): Shoup and Las Vegas root-search backends Re-land olympichek's Shoup trace splitter and bounded Las Vegas Cantor–Zassenhaus stack from #253/#254 onto current main (module system, Lean 4.32). Both implement LinearFactorProductSplitter for fields without a smooth multiplicative-subgroup schedule. - Roots/Shoup: small-char trace coordinates [vzGS92] with correctness - Roots/LasVegas: odd CZ + char-2 trace branches, ProbeFamily, probability - Tests on ZMod 2/5/11 and binary-tower level 0 (Tower integration path) - Docs/ROADMAP: close the non-smooth splitter gap; note high-width Tower SmallPrimeTraceContext instances as follow-up Port fixes for open-friendly CPolynomial APIs and module-system proof adjustments. No parallel Binary/Extension field stack in this PR. Co-authored-by: Derek Sorensen * fix(roots): adapt Shoup and Las Vegas proofs to Lean 4.33.1 The rebase onto current main left the build failing on module-system exposure and mathlib drift, not on any mathematical content. All 21 library modules and 3 test modules already carried `module`, `public import` and `@[expose] public section`; the failures were downstream of main tightening definition exposure. - `import all` for the same-package implementation dependencies these proofs step through (`Univariate.Basic`, `Modular`, `Raw.Division`, `Raw.Modular`, `ToPoly.Core`). `natDegree`, `coeff`, `eval`, `monicNormalize`, `modByMonic` and the `Raw` modular kernels live in bare `public section`s, so their bodies are opaque downstream and `simp [natDegree]`, `unfold monicNormalize`, `change` into the `Raw` layer and `natDegree 0 = 0 := rfl` all stopped working. This is the pattern `docs/wiki/module-system.md` prescribes for exactly this case. - Bridge `toPoly 0 = 0` with `toPoly_eq_zero_iff` rather than relying on definitional reduction. - `letI`/`haveI` to `let`/`have` (26 sites) per the `haveILetI` style linter, and `Set.mem_setOf_eq` to `Set.mem_ofPred_eq` (32 sites) for the mathlib 4.32 to 4.33 deprecation. - Prove the two `ContainsAllFieldElements` obligations with plain `decide` over the whole quantifier, matching `tests/.../Roots/Enumeration.lean`. List membership decidability now wants `BEq`/`LawfulBEq`, which the previous `fin_cases`-then-`decide` shape could not synthesize. A `natDegree_zero` characterization lemma in `Univariate/Basic.lean` would be the tidier long-term fix for the last of those; left out to keep this change inside the PR's own files. Build, tests, lint, imports, docs integrity and `axiomsweep --check` all pass; the sweep stays at 0 sorry and 0 non-standard axioms. --------- Co-authored-by: Valerii Huhnin --- CompPoly.lean | 20 + CompPoly/Univariate/Roots.lean | 2 + CompPoly/Univariate/Roots/LasVegas.lean | 19 + CompPoly/Univariate/Roots/LasVegas/Basic.lean | 332 ++++ .../Roots/LasVegas/Correctness.lean | 17 + .../Roots/LasVegas/Correctness/Common.lean | 1060 +++++++++++++ .../Roots/LasVegas/Correctness/EvenTrace.lean | 641 ++++++++ .../Roots/LasVegas/Correctness/Loop.lean | 511 ++++++ .../Roots/LasVegas/Correctness/Odd.lean | 1109 +++++++++++++ .../Univariate/Roots/LasVegas/OddBucket.lean | 85 + .../Roots/LasVegas/Probability.lean | 43 + .../Roots/LasVegas/Probability/Basic.lean | 167 ++ .../Roots/LasVegas/Probability/EvenTrace.lean | 605 +++++++ .../LasVegas/Probability/OddBuckets.lean | 255 +++ .../Roots/LasVegas/Probability/OddTrial.lean | 372 +++++ .../Roots/LasVegas/Probability/Recursive.lean | 927 +++++++++++ .../Roots/LasVegas/Probability/Repeated.lean | 995 ++++++++++++ .../Roots/LasVegas/Probability/Uniform.lean | 614 +++++++ CompPoly/Univariate/Roots/Shoup.lean | 19 + CompPoly/Univariate/Roots/Shoup/Basic.lean | 443 ++++++ .../Univariate/Roots/Shoup/Correctness.lean | 1410 +++++++++++++++++ .../Roots/Shoup/FrobeniusLinear.lean | 133 ++ ROADMAP.md | 7 +- docs/wiki/coding-theory.md | 33 +- tests/CompPolyTests.lean | 2 + .../Univariate/Roots/LasVegas.lean | 333 ++++ .../CompPolyTests/Univariate/Roots/Shoup.lean | 190 +++ 27 files changed, 10335 insertions(+), 9 deletions(-) create mode 100644 CompPoly/Univariate/Roots/LasVegas.lean create mode 100644 CompPoly/Univariate/Roots/LasVegas/Basic.lean create mode 100644 CompPoly/Univariate/Roots/LasVegas/Correctness.lean create mode 100644 CompPoly/Univariate/Roots/LasVegas/Correctness/Common.lean create mode 100644 CompPoly/Univariate/Roots/LasVegas/Correctness/EvenTrace.lean create mode 100644 CompPoly/Univariate/Roots/LasVegas/Correctness/Loop.lean create mode 100644 CompPoly/Univariate/Roots/LasVegas/Correctness/Odd.lean create mode 100644 CompPoly/Univariate/Roots/LasVegas/OddBucket.lean create mode 100644 CompPoly/Univariate/Roots/LasVegas/Probability.lean create mode 100644 CompPoly/Univariate/Roots/LasVegas/Probability/Basic.lean create mode 100644 CompPoly/Univariate/Roots/LasVegas/Probability/EvenTrace.lean create mode 100644 CompPoly/Univariate/Roots/LasVegas/Probability/OddBuckets.lean create mode 100644 CompPoly/Univariate/Roots/LasVegas/Probability/OddTrial.lean create mode 100644 CompPoly/Univariate/Roots/LasVegas/Probability/Recursive.lean create mode 100644 CompPoly/Univariate/Roots/LasVegas/Probability/Repeated.lean create mode 100644 CompPoly/Univariate/Roots/LasVegas/Probability/Uniform.lean create mode 100644 CompPoly/Univariate/Roots/Shoup.lean create mode 100644 CompPoly/Univariate/Roots/Shoup/Basic.lean create mode 100644 CompPoly/Univariate/Roots/Shoup/Correctness.lean create mode 100644 CompPoly/Univariate/Roots/Shoup/FrobeniusLinear.lean create mode 100644 tests/CompPolyTests/Univariate/Roots/LasVegas.lean create mode 100644 tests/CompPolyTests/Univariate/Roots/Shoup.lean diff --git a/CompPoly.lean b/CompPoly.lean index 8cb122d1..44eb5aae 100644 --- a/CompPoly.lean +++ b/CompPoly.lean @@ -272,7 +272,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/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/ROADMAP.md b/ROADMAP.md index 24921aee..31e660c7 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -195,7 +195,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 diff --git a/docs/wiki/coding-theory.md b/docs/wiki/coding-theory.md index 40ff59e9..ba932183 100644 --- a/docs/wiki/coding-theory.md +++ b/docs/wiki/coding-theory.md @@ -157,11 +157,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`; `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-*`. +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 | + +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 +193,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 +207,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 +228,10 @@ 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/` BibTeX entries for these keys are in [`../../blueprint/src/references.bib`](../../blueprint/src/references.bib). diff --git a/tests/CompPolyTests.lean b/tests/CompPolyTests.lean index 1aadd98c..732bd743 100644 --- a/tests/CompPolyTests.lean +++ b/tests/CompPolyTests.lean @@ -53,4 +53,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/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 From 2ee5257a6eafbddc4998efbe2144a29991d21844 Mon Sep 17 00:00:00 2001 From: Derek Sorensen Date: Wed, 2 Sep 2026 15:41:52 +0100 Subject: [PATCH 12/45] feat(linalg): order-basis approximant layer over polynomial matrices (#312) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(linalg): order-basis approximant layer over polynomial matrices The linear-algebra half of the Guruswami-Sudan approximant interpolation work from #255, split out so it can be reviewed on its own. It stands under ROADMAP item 10 independently of the decoder backends that consume it. Adds `LinearAlgebra/PolynomialMatrix/Approximant/`: - `ModularEquation/` — modular key equations with soundness and completeness - `PMBasis/` — the divide-and-conquer order-basis recursion, with X-adic soundness, kernel-leaf soundness/completeness, and the scalar and span kernel-leaf layers - `PartialLinearization.lean` — degree balancing for the recursion plus the supporting matrix pieces it needs: `Operations.lean`, `RowSelection.lean`, `StrassenCorrectness.lean` (fast multiplication used by the PM-Basis recursion), and `MuldersStorjohannCorrectness/WeakPopovMinimal.lean`. * fix(linalg): port the approximant layer to Lean 4.33.1 Module-system and toolchain adaptation for the relanded order-basis layer. - `import all` for the same-package implementation dependencies these proofs step through (`Univariate.Basic`, `Univariate.Raw.Core`). `coeff`, `ofArray` and the `Raw` wrappers sit in bare `public section`s, so their bodies are opaque downstream and `rw [CPolynomial.ofArray]`, `simp [Raw.coeff]` and `p.coeff i = (↑p).coeff i := rfl` all stopped working. This is the pattern `docs/wiki/module-system.md` prescribes for the case. - `letI` to `let` per the `haveILetI` style linter. - Record the layer under ROADMAP item 10. --------- Co-authored-by: Valerii Huhnin --- CompPoly.lean | 20 + CompPoly/LinearAlgebra/PolynomialMatrix.lean | 2 + .../PolynomialMatrix/Approximant.lean | 19 + .../PolynomialMatrix/Approximant/Basic.lean | 90 ++ .../Approximant/Correctness.lean | 70 + .../Approximant/ModularEquation.lean | 242 +++ .../Approximant/ModularEquation/Basic.lean | 773 ++++++++++ .../ModularEquation/Completeness.lean | 1145 ++++++++++++++ .../PolynomialMatrix/Approximant/PMBasis.lean | 69 + .../Approximant/PMBasis/Correctness.lean | 1233 +++++++++++++++ .../Approximant/PMBasis/KernelLeaf.lean | 414 +++++ .../PMBasis/KernelLeafCompleteness.lean | 605 ++++++++ .../Approximant/PMBasis/KernelLeafScalar.lean | 1344 +++++++++++++++++ .../PMBasis/KernelLeafSoundness.lean | 621 ++++++++ .../Approximant/PMBasis/KernelLeafSpan.lean | 1216 +++++++++++++++ .../Approximant/PMBasis/Recursion.lean | 185 +++ .../Approximant/PMBasis/XAdicSoundness.lean | 795 ++++++++++ .../Approximant/PartialLinearization.lean | 235 +++ .../MuldersStorjohannCorrectness/Fast.lean | 11 +- .../Reduction.lean | 37 + .../WeakPopovMinimal.lean | 293 ++++ .../PolynomialMatrix/Operations.lean | 492 ++++++ .../PolynomialMatrix/RowSelection.lean | 400 +++++ .../PolynomialMatrix/StrassenCorrectness.lean | 818 ++++++++++ ROADMAP.md | 5 + tests/CompPolyTests.lean | 1 + .../PolynomialMatrix/Approximant.lean | 254 ++++ 27 files changed, 11384 insertions(+), 5 deletions(-) create mode 100644 CompPoly/LinearAlgebra/PolynomialMatrix/Approximant.lean create mode 100644 CompPoly/LinearAlgebra/PolynomialMatrix/Approximant/Basic.lean create mode 100644 CompPoly/LinearAlgebra/PolynomialMatrix/Approximant/Correctness.lean create mode 100644 CompPoly/LinearAlgebra/PolynomialMatrix/Approximant/ModularEquation.lean create mode 100644 CompPoly/LinearAlgebra/PolynomialMatrix/Approximant/ModularEquation/Basic.lean create mode 100644 CompPoly/LinearAlgebra/PolynomialMatrix/Approximant/ModularEquation/Completeness.lean create mode 100644 CompPoly/LinearAlgebra/PolynomialMatrix/Approximant/PMBasis.lean create mode 100644 CompPoly/LinearAlgebra/PolynomialMatrix/Approximant/PMBasis/Correctness.lean create mode 100644 CompPoly/LinearAlgebra/PolynomialMatrix/Approximant/PMBasis/KernelLeaf.lean create mode 100644 CompPoly/LinearAlgebra/PolynomialMatrix/Approximant/PMBasis/KernelLeafCompleteness.lean create mode 100644 CompPoly/LinearAlgebra/PolynomialMatrix/Approximant/PMBasis/KernelLeafScalar.lean create mode 100644 CompPoly/LinearAlgebra/PolynomialMatrix/Approximant/PMBasis/KernelLeafSoundness.lean create mode 100644 CompPoly/LinearAlgebra/PolynomialMatrix/Approximant/PMBasis/KernelLeafSpan.lean create mode 100644 CompPoly/LinearAlgebra/PolynomialMatrix/Approximant/PMBasis/Recursion.lean create mode 100644 CompPoly/LinearAlgebra/PolynomialMatrix/Approximant/PMBasis/XAdicSoundness.lean create mode 100644 CompPoly/LinearAlgebra/PolynomialMatrix/Approximant/PartialLinearization.lean create mode 100644 CompPoly/LinearAlgebra/PolynomialMatrix/MuldersStorjohannCorrectness/WeakPopovMinimal.lean create mode 100644 CompPoly/LinearAlgebra/PolynomialMatrix/Operations.lean create mode 100644 CompPoly/LinearAlgebra/PolynomialMatrix/RowSelection.lean create mode 100644 CompPoly/LinearAlgebra/PolynomialMatrix/StrassenCorrectness.lean create mode 100644 tests/CompPolyTests/LinearAlgebra/PolynomialMatrix/Approximant.lean diff --git a/CompPoly.lean b/CompPoly.lean index 44eb5aae..40758a4f 100644 --- a/CompPoly.lean +++ b/CompPoly.lean @@ -163,6 +163,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 @@ -176,9 +192,13 @@ 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.Multilinear.Basic public import CompPoly.Multilinear.Equiv public import CompPoly.Multilinear.ManyEval 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/ROADMAP.md b/ROADMAP.md index 31e660c7..fd3a3885 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -213,6 +213,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/tests/CompPolyTests.lean b/tests/CompPolyTests.lean index 732bd743..1249a585 100644 --- a/tests/CompPolyTests.lean +++ b/tests/CompPolyTests.lean @@ -38,6 +38,7 @@ public import CompPolyTests.Fields.Mersenne31.Fast public import CompPolyTests.Fields.Mersenne31.Instances public import CompPolyTests.Fields.PrattCertificate public import CompPolyTests.LinearAlgebra.Dense +public import CompPolyTests.LinearAlgebra.PolynomialMatrix.Approximant public import CompPolyTests.Multilinear.Equiv public import CompPolyTests.Multivariate.CMvMonomial public import CompPolyTests.Multivariate.Restrict 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 From 9c0f84f1c5ecaae594ddd43914528bd4a6356555 Mon Sep 17 00:00:00 2001 From: Derek Sorensen Date: Wed, 2 Sep 2026 15:41:53 +0100 Subject: [PATCH 13/45] feat(bivariate): approximant-basis and hybrid GS interpolation (#313) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(bivariate): approximant-basis and hybrid GS interpolation Two further Guruswami-Sudan interpolation backends from #255, built on the order-basis layer. - `Interpolation/ApproximantBasis/` — solves the interpolation problem as a modular key equation through PM-Basis, quasi-linear in code length and independent of the corruption level - `Interpolation/Hybrid/` — budgeted Lee-O'Sullivan reduction with approximant fallback; correctness follows from equality to whichever verified backend it dispatched to - `Interpolation/WitnessDivisibility*` — the fast multiplicity check, proved equivalent to the Hasse pointwise check Wired into the existing `GSInterpContext` / `Implementations` pattern, so the `gsCore_sound` and `gsCore_complete_*` contracts carry over unchanged. * fix(bivariate): port GS approximant interpolation to 4.33.1 and benchmark it Port and measurement for the relanded interpolation backends. Module-system adaptation, same class as the rest of the reland: - `import all` for the same-package implementation dependencies these proofs step through (`Univariate.Basic`, `Univariate.Raw.Core`, `ToPoly.Core`). `linearFactor`, `coeff`, `ofArray` and `toPoly` sit in bare `public section`s, so their bodies are opaque downstream. - `eval_map_taylorAlgHom`: keep `taylorAlgHom` folded so the `@[simp, norm_cast]` `coe_taylorAlgHom` can fire, and discharge `taylorAlgHom x (C y) = C y` explicitly rather than by definitional reduction. - Drop two simp arguments the elaborator now reports as unused. - Prove the test's `ContainsAllFieldElements` obligation with explicit `List.Mem` witnesses; `Decidable (_ ∈ _.toList)` no longer synthesizes for this carrier, so neither `decide` nor `fin_cases <;> decide` applies. Benchmark: four rows added to the existing `guruswami-sudan-interp-small-koalabear` group, covering approximant-basis and hybrid over canonical and native-word KoalaBear. Adding rows rather than a new group keeps the shared `StdGen` untouched, so no other group's checksums move, and the group is already in `BENCH_CI_GROUPS`. What the numbers say at the small shape (n=64, k=16, m=2, small preset): Lee-O'Sullivan direct 13.6ms, approximant-basis 180.1ms, hybrid 16.5ms canonical; 2.3ms / 63.7ms / 3.9ms on the fast field. The approximant backend is an order of magnitude slower here, which is expected — its advantage is asymptotic in code length and the crossover is well above n=128, the largest shape the suite currently defines. The hybrid backend matching Lee-O'Sullivan almost exactly is the meaningful result: its budget correctly declines to fall back at this size. Substantiating the quasi-linear claim needs a large-n shape, which is a separate scope decision because of CI wall-clock. --------- Co-authored-by: Valerii Huhnin --- CompPoly.lean | 12 + CompPoly/Bivariate/Deriv.lean | 23 + .../GuruswamiSudan/Implementations.lean | 148 +++- .../GuruswamiSudan/Interpolation.lean | 22 + .../Interpolation/ApproximantBasis.lean | 17 + .../ApproximantBasis/Algorithm.lean | 85 +++ .../Interpolation/ApproximantBasis/Basic.lean | 142 ++++ .../ApproximantBasis/Correctness.lean | 646 ++++++++++++++++ .../ApproximantBasis/ModularData.lean | 696 ++++++++++++++++++ .../ApproximantBasis/Multiplicity.lean | 266 +++++++ .../GuruswamiSudan/Interpolation/Basic.lean | 23 + .../GuruswamiSudan/Interpolation/Hybrid.lean | 16 + .../Interpolation/Hybrid/Algorithm.lean | 112 +++ .../Interpolation/Hybrid/Correctness.lean | 146 ++++ .../Interpolation/LeeOSullivan/Basic.lean | 14 - .../Interpolation/WitnessDivisibility.lean | 120 +++ .../WitnessDivisibilityCorrectness.lean | 514 +++++++++++++ .../GuruswamiSudan/PolynomialCorrectness.lean | 9 +- ROADMAP.md | 8 +- .../Bivariate/GuruswamiSudan.lean | 42 +- docs/wiki/coding-theory.md | 12 +- tests/CompPolyTests.lean | 1 + .../Interpolation/ApproximantBasis.lean | 232 ++++++ 23 files changed, 3280 insertions(+), 26 deletions(-) create mode 100644 CompPoly/Bivariate/GuruswamiSudan/Interpolation.lean create mode 100644 CompPoly/Bivariate/GuruswamiSudan/Interpolation/ApproximantBasis.lean create mode 100644 CompPoly/Bivariate/GuruswamiSudan/Interpolation/ApproximantBasis/Algorithm.lean create mode 100644 CompPoly/Bivariate/GuruswamiSudan/Interpolation/ApproximantBasis/Basic.lean create mode 100644 CompPoly/Bivariate/GuruswamiSudan/Interpolation/ApproximantBasis/Correctness.lean create mode 100644 CompPoly/Bivariate/GuruswamiSudan/Interpolation/ApproximantBasis/ModularData.lean create mode 100644 CompPoly/Bivariate/GuruswamiSudan/Interpolation/ApproximantBasis/Multiplicity.lean create mode 100644 CompPoly/Bivariate/GuruswamiSudan/Interpolation/Hybrid.lean create mode 100644 CompPoly/Bivariate/GuruswamiSudan/Interpolation/Hybrid/Algorithm.lean create mode 100644 CompPoly/Bivariate/GuruswamiSudan/Interpolation/Hybrid/Correctness.lean create mode 100644 CompPoly/Bivariate/GuruswamiSudan/Interpolation/WitnessDivisibility.lean create mode 100644 CompPoly/Bivariate/GuruswamiSudan/Interpolation/WitnessDivisibilityCorrectness.lean create mode 100644 tests/CompPolyTests/Bivariate/GuruswamiSudan/Interpolation/ApproximantBasis.lean diff --git a/CompPoly.lean b/CompPoly.lean index 40758a4f..eb4b09b1 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 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/ROADMAP.md b/ROADMAP.md index fd3a3885..596113c5 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -176,10 +176,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 diff --git a/bench/CompPolyBench/Bivariate/GuruswamiSudan.lean b/bench/CompPolyBench/Bivariate/GuruswamiSudan.lean index e71854a6..f9fcc6db 100644 --- a/bench/CompPolyBench/Bivariate/GuruswamiSudan.lean +++ b/bench/CompPolyBench/Bivariate/GuruswamiSudan.lean @@ -112,9 +112,15 @@ private def runGsInterpolationSmallKoala (preset : BenchPreset) (gen : StdGen) : let fastDenseMeasured := preset.selectNat 2 1 1 let fastLeeDirectMeasured := preset.selectNat 600 90 20 let fastLeeSubproductMeasured := preset.selectNat 400 60 10 + let approximantMeasured := preset.selectNat 60 9 2 + let hybridMeasured := preset.selectNat 90 13 3 + let fastApproximantMeasured := preset.selectNat 300 45 10 + let fastHybridMeasured := preset.selectNat 400 60 10 let checksumIterations := groupChecksumIterations denseMeasured [ leeDirectMeasured, leeSubproductMeasured, fastDenseMeasured, - fastLeeDirectMeasured, fastLeeSubproductMeasured + fastLeeDirectMeasured, fastLeeSubproductMeasured, + approximantMeasured, hybridMeasured, + fastApproximantMeasured, fastHybridMeasured ] let denseRow <- runTimed "guruswami-sudan-interp-dense-small" "CBivariate" @@ -161,12 +167,44 @@ private def runGsInterpolationSmallKoala (preset : BenchPreset) (gen : StdGen) : gsSmallParams) (checksumInterpolationValidityOption fastPoints gsSmallParams) checksumIterations + let approximantRow <- runTimed + "guruswami-sudan-interp-approximant-small" "CBivariate" + "Approximant basis (PM-Basis)" + "KoalaBear.Field" gsSmallInterpInputShape preset warmup approximantMeasured + (fun _ ↦ koalaBearApproximantBasisSubproductInterpContext.interpolate points + gsSmallParams) + (checksumInterpolationValidityOption points gsSmallParams) + checksumIterations + let hybridRow <- runTimed + "guruswami-sudan-interp-hybrid-small" "CBivariate" + "Hybrid (budgeted Lee-O'Sullivan with approximant fallback)" + "KoalaBear.Field" gsSmallInterpInputShape preset warmup hybridMeasured + (fun _ ↦ koalaBearHybridInterpContext.interpolate points gsSmallParams) + (checksumInterpolationValidityOption points gsSmallParams) + checksumIterations + let fastApproximantRow <- runTimed + "guruswami-sudan-interp-approximant-small-fast" "CBivariate" + "Approximant basis (PM-Basis)" + "KoalaBear.Fast.Field" gsSmallInterpInputShape preset warmup + fastApproximantMeasured + (fun _ ↦ fastKoalaBearApproximantBasisSubproductInterpContext.interpolate + fastPoints gsSmallParams) + (checksumInterpolationValidityOption fastPoints gsSmallParams) + checksumIterations + let fastHybridRow <- runTimed + "guruswami-sudan-interp-hybrid-small-fast" "CBivariate" + "Hybrid (budgeted Lee-O'Sullivan with approximant fallback)" + "KoalaBear.Fast.Field" gsSmallInterpInputShape preset warmup fastHybridMeasured + (fun _ ↦ fastKoalaBearHybridInterpContext.interpolate fastPoints gsSmallParams) + (checksumInterpolationValidityOption fastPoints gsSmallParams) + checksumIterations 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) diff --git a/docs/wiki/coding-theory.md b/docs/wiki/coding-theory.md index ba932183..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 @@ -232,6 +238,8 @@ knowing which contracts they assume. 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/tests/CompPolyTests.lean b/tests/CompPolyTests.lean index 1249a585..f2767328 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 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 From e6fb7df4f4a91c54bc9ac997daa889a74eda6c7a Mon Sep 17 00:00:00 2001 From: Derek Sorensen Date: Wed, 2 Sep 2026 16:19:21 +0100 Subject: [PATCH 14/45] feat(multivariate): add partial evaluation of the first variable (#315) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(multivariate): add partial evaluation of the first variable - Multivariate/PartialEval.lean: partialEvalFirst (fix variable 0) with its evaluation and per-variable degree-bound lemmas. - Operations.lean: add the fromCMvPolynomial_bind₁ bridge lemma. * port(multivariate): migrate partial evaluation to the module system Land the salvageable half of the original change and drop the rest. - Module-system migration: `module` header, `public import`, and `@[expose] public section`, matching the rest of `Multivariate/`. Imports reduce to `Operations` plus `Mathlib.Algebra.MvPolynomial.Degrees`. - Drop the degree-bound wrappers. `CDegreeLE` and `CMvDegreeLE` were subtypes with no instances, operations or lemmas, and `IndividualDegreeLE` was unused even by the degree theorem that motivated it; all three existed only to give an external consumer names to refer to. - Replace every `simp`/`simpa` in the new code with `simp only`/`simpa only` per the repo tactic guidance. - Generalize the ring variable from `Type` to `Type*`. - Add regression coverage: the action on generators, agreement with direct evaluation on a concrete polynomial, and the degree bound. The checks are symbolic because the carrier is a quotient of `Std.ExtTreeMap`, so kernel reduction gets stuck on `Quot.lift`. * port(multivariate): harden partial-evaluation proofs against import drift - Use core `Fin.succ_inj` instead of Mathlib's `Fin.succ_injective`. The latter lives in `Mathlib/Data/Fin/SuccPred.lean` and was reachable here only through an incidental import closure rather than anything this file asks for. - Finish the `simp only` conversion in the test file. --------- Co-authored-by: Cody Gunton --- CompPoly.lean | 1 + CompPoly/Multivariate/Operations.lean | 39 ++++ CompPoly/Multivariate/PartialEval.lean | 187 ++++++++++++++++++ ROADMAP.md | 2 + tests/CompPolyTests.lean | 1 + .../Multivariate/PartialEval.lean | 82 ++++++++ 6 files changed, 312 insertions(+) create mode 100644 CompPoly/Multivariate/PartialEval.lean create mode 100644 tests/CompPolyTests/Multivariate/PartialEval.lean diff --git a/CompPoly.lean b/CompPoly.lean index eb4b09b1..d495f3a9 100644 --- a/CompPoly.lean +++ b/CompPoly.lean @@ -228,6 +228,7 @@ 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 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/ROADMAP.md b/ROADMAP.md index 596113c5..89c5a67a 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 diff --git a/tests/CompPolyTests.lean b/tests/CompPolyTests.lean index f2767328..3a698dba 100644 --- a/tests/CompPolyTests.lean +++ b/tests/CompPolyTests.lean @@ -42,6 +42,7 @@ public import CompPolyTests.LinearAlgebra.Dense public import CompPolyTests.LinearAlgebra.PolynomialMatrix.Approximant 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 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 From 322f12e6756bb2852f1e682e8cf0056b33e95ef7 Mon Sep 17 00:00:00 2001 From: Gregor's Bots Date: Wed, 2 Sep 2026 18:39:15 +0200 Subject: [PATCH 15/45] Eight-limb Montgomery field arithmetic for 255-bit moduli (Pasta) (#274) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(fields): Pasta base fields on the eight-limb Montgomery implementation Add `Fields/Pasta`, the per-field facade for the Pallas and Vesta base fields, following the KoalaBear layering: * `Pasta/Basic.lean` — the two 255-bit primes with Pratt primality certificates (ported from the certificates by Daira-Emma Hopwood, using CompPoly's own `PrattCertificate` infrastructure), their `Fact` and `Field` instances, and the 2-cycle abbreviations relating each curve's scalar field to the other's base field; * `Pasta/Fast.lean` — the `Mont64x8Field` constants for both fields and the namespaced `Pallas.Fast` / `Vesta.Fast` API over the shared eight-limb implementation, with the canonical-field bridge; * `Pasta.lean` — the facade re-exporting both. `Mont64x8Field` moves from the raw layer to `Montgomery/Native64x8Field` alongside the carrier it parameterizes, mirroring `Mont32Field` in `Montgomery/Native32Field`, and now carries `prime` like its single-word counterpart, so the bridge no longer takes a separate `Fact` argument. Tests cross-check both fast fields against canonical `ZMod` arithmetic for powers and inverses. Co-Authored-By: Claude Fable 5 * feat(fields): scalar radix-2 FFT over eight-limb Montgomery elements Co-Authored-By: Claude Fable 5 --------- Co-authored-by: Claude Fable 5 --- CompPoly.lean | 4 + CompPoly/Fields/Montgomery/ScalarFft.lean | 73 +++++++ CompPoly/Fields/Pasta.lean | 17 ++ CompPoly/Fields/Pasta/Basic.lean | 216 +++++++++++++++++++++ CompPoly/Fields/Pasta/Fast.lean | 93 +++++++++ CompPoly/Fields/README.md | 4 + tests/CompPolyTests.lean | 1 + tests/CompPolyTests/Fields/Pasta/Fast.lean | 113 +++++++++++ 8 files changed, 521 insertions(+) create mode 100644 CompPoly/Fields/Montgomery/ScalarFft.lean create mode 100644 CompPoly/Fields/Pasta.lean create mode 100644 CompPoly/Fields/Pasta/Basic.lean create mode 100644 CompPoly/Fields/Pasta/Fast.lean create mode 100644 tests/CompPolyTests/Fields/Pasta/Fast.lean diff --git a/CompPoly.lean b/CompPoly.lean index d495f3a9..c39e7598 100644 --- a/CompPoly.lean +++ b/CompPoly.lean @@ -162,6 +162,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 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 aa8d7ddd..67e4503f 100644 --- a/CompPoly/Fields/README.md +++ b/CompPoly/Fields/README.md @@ -56,6 +56,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 diff --git a/tests/CompPolyTests.lean b/tests/CompPolyTests.lean index 3a698dba..ed95eab6 100644 --- a/tests/CompPolyTests.lean +++ b/tests/CompPolyTests.lean @@ -37,6 +37,7 @@ public import CompPolyTests.Fields.Goldilocks.Fast public import CompPolyTests.Fields.KoalaBear.Fast 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 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 From 8ac9c982c6382aaabc85e9c7c50bae73c108e7c7 Mon Sep 17 00:00:00 2001 From: Derek Sorensen Date: Thu, 3 Sep 2026 14:54:45 +0100 Subject: [PATCH 16/45] refactor(fields)!: remove the deprecated CompPoly.Fields.Mersenne module (#316) `CompPoly/Fields/Mersenne.lean` was a compatibility re-export left behind when #257 renamed the real module to `CompPoly.Fields.Mersenne31` and split it into `Basic`/`Fast`. It declared nothing of its own. Nothing in the repository imported it except the generated `CompPoly.lean`, which picks up every tracked module and so inherited its deprecation warning: every `lake build` reported one, and `lake build --wfail` failed. Downstream consumers on the old path should import `CompPoly.Fields.Mersenne31`. --- CompPoly.lean | 1 - CompPoly/Fields/Mersenne.lean | 16 ---------------- CompPoly/Fields/README.md | 1 - 3 files changed, 18 deletions(-) delete mode 100644 CompPoly/Fields/Mersenne.lean diff --git a/CompPoly.lean b/CompPoly.lean index c39e7598..31949733 100644 --- a/CompPoly.lean +++ b/CompPoly.lean @@ -149,7 +149,6 @@ 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 diff --git a/CompPoly/Fields/Mersenne.lean b/CompPoly/Fields/Mersenne.lean deleted file mode 100644 index 5ca22e5d..00000000 --- a/CompPoly/Fields/Mersenne.lean +++ /dev/null @@ -1,16 +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, Varun Thakore --/ -module - -public import CompPoly.Fields.Mersenne31 - -/-! -# Deprecated Mersenne31 compatibility import - -This module re-exports `CompPoly.Fields.Mersenne31` for compatibility with the former import path. --/ - -deprecated_module "Use `CompPoly.Fields.Mersenne31` instead" (since := "2026-08-25") diff --git a/CompPoly/Fields/README.md b/CompPoly/Fields/README.md index 67e4503f..20910642 100644 --- a/CompPoly/Fields/README.md +++ b/CompPoly/Fields/README.md @@ -46,7 +46,6 @@ This directory contains formally verified field infrastructure used in zero-know | **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. | -| **Mersenne.lean** | Deprecated compatibility re-export of `Mersenne31.lean`; retained so existing imports continue to work. | | **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. | From 2aa593725644fb0fe7c578be68cc5ec96bbc8dd2 Mon Sep 17 00:00:00 2001 From: Alexander Hicks <25369263+alexanderlhicks@users.noreply.github.com> Date: Fri, 4 Sep 2026 09:43:55 +0100 Subject: [PATCH 17/45] feat(multilinear): factor the equality kernel (#318) Add product and append characterizations for the executable multilinear equality kernel, with a scalar-evaluation support lemma and regression tests. Adapted from Verified-zkEVM/leanth#10. Co-authored-by: Elias Judin --- CompPoly/Multilinear/Basic.lean | 53 +++++++++++++++++++++- tests/CompPolyTests/Multilinear/Equiv.lean | 8 ++++ 2 files changed, 60 insertions(+), 1 deletion(-) 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/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 From 5671e4c9829c101f3f3f5c5b66ef3a7e83342568 Mon Sep 17 00:00:00 2001 From: scaraven <34778974+scaraven@users.noreply.github.com> Date: Mon, 7 Sep 2026 14:47:23 +0100 Subject: [PATCH 18/45] refactor(fields): make carry-less multiplication width-generic (#320) * refactor(fields): make carry-less multiplication width-generic `BinaryField.clMul` was fixed at 128x128 -> 256 bits, so no other width could reuse it or its correctness proof. Add width-generic `carryLessMul {v w}` alongside `zeroExtendTo` and the `toPoly` splitting lemmas `toPoly_eq_range` and `toPoly_split`, and redefine `clMul` as its 128-bit instance. `clMul`, `clSq`, and `to256` keep their names and signatures, and `clMul_unfold`, `toPoly_clMul`, and `toPoly_128_extend_256` become corollaries of the generic versions, so the GHASH development is unchanged apart from its proofs collapsing. `clMulNat`, the Nat-based kernel checker, is untouched. Also fix `scripts/gen_rabin_certificate.py`, whose line wrapping broke at degree 64 over GF(2): it only ever wrapped a step across two lines and never wrapped `poly_to_lean` output, so a small prime with many terms emitted 152 lines over the 100-column limit. Wrap coefficient lists across as many lines as needed, emit the regeneration command as a fenced shell block, and replace the hardcoded `Authors:` line with an `--authors` flag. The generator still reproduces both committed KoalaBear certificates byte-for-byte and passes its self-tests. Co-Authored-By: Claude Opus 5 * fix(gen_rabin_certificate): Escape shell backslash outside of single quote to ensure 100 line limit is enforced Add --f= rather --f '...' so that argparse can accept negative coefficients for generating certificate data. Regenerate certificate data for Quintix and Sextic KoalaBear extensions, only headers have been changed and means that the certificates are now not byte for byte identical * fix(gen_rabin_certificate): update stale --f references and add wrapping/format test --------- Co-authored-by: Claude Opus 5 --- CompPoly/Fields/Binary/Common.lean | 143 ++++++++++---- .../KoalaBear/Ext5/QuinticCertData.lean | 15 +- .../Fields/KoalaBear/Ext6/SexticCertData.lean | 15 +- docs/wiki/field-extensions.md | 2 +- docs/wiki/generated-files.md | 7 +- scripts/gen_rabin_certificate.py | 179 +++++++++++++++--- .../Data/Polynomial/RabinCertificate.lean | 2 +- 7 files changed, 297 insertions(+), 66 deletions(-) diff --git a/CompPoly/Fields/Binary/Common.lean b/CompPoly/Fields/Binary/Common.lean index 7dec374f..aeb0cb58 100644 --- a/CompPoly/Fields/Binary/Common.lean +++ b/CompPoly/Fields/Binary/Common.lean @@ -266,11 +266,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 +322,23 @@ 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 + 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 +738,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/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/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/docs/wiki/field-extensions.md b/docs/wiki/field-extensions.md index 56f5e2d6..63327998 100644 --- a/docs/wiki/field-extensions.md +++ b/docs/wiki/field-extensions.md @@ -219,7 +219,7 @@ 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_of_card` (prime `d`, see diff --git a/docs/wiki/generated-files.md b/docs/wiki/generated-files.md index da4b08d9..e28a2d3f 100644 --- a/docs/wiki/generated-files.md +++ b/docs/wiki/generated-files.md @@ -7,7 +7,7 @@ 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`. | +| `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. | | `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. | @@ -38,11 +38,14 @@ writes a complete, compilable module — header, docstring with the regenerating 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. 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/CompPolyTests/Data/Polynomial/RabinCertificate.lean b/tests/CompPolyTests/Data/Polynomial/RabinCertificate.lean index d1841d27..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`. From 3468b38c8fd270f93f55a259220a8abc544e7437 Mon Sep 17 00:00:00 2001 From: scaraven <34778974+scaraven@users.noreply.github.com> Date: Mon, 7 Sep 2026 14:55:44 +0100 Subject: [PATCH 19/45] feat(fields): add polynomial-basis GF(2^64) and its cubic extension (#321) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor(fields): make carry-less multiplication width-generic `BinaryField.clMul` was fixed at 128x128 -> 256 bits, so no other width could reuse it or its correctness proof. Add width-generic `carryLessMul {v w}` alongside `zeroExtendTo` and the `toPoly` splitting lemmas `toPoly_eq_range` and `toPoly_split`, and redefine `clMul` as its 128-bit instance. `clMul`, `clSq`, and `to256` keep their names and signatures, and `clMul_unfold`, `toPoly_clMul`, and `toPoly_128_extend_256` become corollaries of the generic versions, so the GHASH development is unchanged apart from its proofs collapsing. `clMulNat`, the Nat-based kernel checker, is untouched. Also fix `scripts/gen_rabin_certificate.py`, whose line wrapping broke at degree 64 over GF(2): it only ever wrapped a step across two lines and never wrapped `poly_to_lean` output, so a small prime with many terms emitted 152 lines over the 100-column limit. Wrap coefficient lists across as many lines as needed, emit the regeneration command as a fenced shell block, and replace the hardcoded `Authors:` line with an `--authors` flag. The generator still reproduces both committed KoalaBear certificates byte-for-byte and passes its self-tests. Co-Authored-By: Claude Opus 5 * fix(gen_rabin_certificate): Escape shell backslash outside of single quote to ensure 100 line limit is enforced Add --f= rather --f '...' so that argparse can accept negative coefficients for generating certificate data. Regenerate certificate data for Quintix and Sextic KoalaBear extensions, only headers have been changed and means that the certificates are now not byte for byte identical * fix(gen_rabin_certificate): update stale --f references and add wrapping/format test * feat(fields): add polynomial-basis GF(2^64) and its cubic extension Add `GF(2)[x]/(x^64 + x^4 + x^3 + x + 1)` as a flat quotient by a single irreducible degree-64 pentanomial, together with its degree-3 extension `GF(2^64)[y]/(y^3 + y + 1)`, giving GF(2^192). CompPoly already reaches GF(2^64) as level 6 of `Fields/Binary/Tower/`, but that builds it by iterated quadratic extension. The two fields are abstractly isomorphic and 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 substitutes for the other wherever the encoding is observable, and no polynomial-basis GF(2^64) existed before this. An element is a `BitVec 64` whose bit `i` is the coefficient of `x^i`. Addition is `xor`, multiplication is a width-generic carry-less product folded back into 64 bits through the reduction constant `0x1B`, and inversion is an Itoh-Tsujii addition chain. The extension reuses the computable framework in `Fields/Extension/`, so its carrier is definitionally `Vector BF64 3`. Degree 64 is composite, so `irreducible_of_rabin_prime_degree` is unsound here -- a product of equal-degree factors passes its collapsed condition. The general `Polynomial.irreducible_of_rabin` is used instead, against certificate data regenerated by `scripts/gen_rabin_certificate.py` and replayed in the kernel. The cubic needs no certificate: a root would satisfy `a^7 = 1`, so its order divides both `7` and `2^64 - 1`, which are coprime. The algebraic instances are written out field-by-field rather than transported through `Function.Injective.commRing`, which takes the bridge map as data and would make the arithmetic noncomputable -- taking the extension down with it, since `Ext.mul` reaches through the base field's `Field` instance. `Pow` is `npowBinRec` rather than the linear `npowRec`; a full-order exponent needs about 64 multiplications instead of 2^64, which is the difference between usable and unusable in the kernel. Regression coverage checks base-field vectors with `decide +kernel` and extension vectors with `#guard`. The latter runs the compiled arithmetic, so a `Field` instance that regressed to noncomputable would fail the build rather than pass silently. Co-Authored-By: Claude Opus 5 * feat(fields): add `CharP Ext3 2` and the `ext3Gen` lemmas `Ext3` had no `CharP` instance. `BF64` and `BF64Quot` both carry one, but it is not found by instance search from `Algebra BF64 Ext3` alone, so the `CharTwo` API — `add_self_eq_zero`, `add_sq`, and the rest — was unavailable on the extension. That is the first thing a consumer of a binary field reaches for. Add the instance the same way `Impl.lean` derives it for `BF64Quot`, via `charP_of_injective_algebraMap'`, and place it ahead of the generator lemmas since the defining relation needs it. Also add the named-generator layer that `KoalaBear.Ext4` and `Ext5` provide but `Ext3` lacked: `ext3Gen`, `ext3Gen_eq_gen`, `toQuot_ext3Gen`, `aeval_ext3Gen`, and `ext3Gen_pow_three`, the defining relation `y^3 = y + 1` in usable form. Without these a consumer had to write `Ext.gen (P := ext3Params)` by hand and rewrite through `Ext.aeval_gen_poly` and `ext3Params_poly` themselves; the regression tests felt this and defined their own local `y`. Following `ext5Gen_eq_gen`, `ext3Gen_eq_gen` is deliberately not `@[simp]`, since as a rewrite it fires before `ext3Gen_pow_three` can match. `card_ext3` becomes `@[simp]` to match `card_ext4` and `card_ext5`. The relation is stated as `ext3Gen ^ 3 = ext3Gen + 1` rather than with `Ext.ofBase`, because `aeval_one` already normalises the constant to `1`; `KoalaBear` needs `Ext.ofBase` only because its constant is `-1`. `linear_combination` cannot discharge it — it reasons over the integers, so it will not turn `y^3 + y + 1 = 0` into `y^3 = y + 1` in characteristic two — hence `CharTwo.sub_eq_add` and `add_assoc`. Also correct the file headers: the copyright holder returns to `CompPoly Contributors`, matching every other file in the repository, with the individual credited on the `Authors:` line, and the facade picks up the author it was missing. Co-Authored-By: Claude Opus 5 * refactor(fields): tighten BF64 resource budgets and docstrings Review follow-ups on the BF64 modules, all confined to this PR's own files. Resource options were over-provisioned. `Basic.lean` declared `maxHeartbeats 4000000` and `Reduce.lean` `2000000` — the two largest values in the repository, against a previous maximum of 1600000 — but bisection shows `Basic.lean` compiles at 400000 and `Reduce.lean` at the 200000 default. The `maxRecDepth` escalations in `Reduce.lean` and `Ext3.lean` are likewise unnecessary; only `Basic.lean` needs a raised depth, and 4000 suffices for it. An inflated budget hides a future regression rather than preventing one. Also replaces a bare `simp` in `lowHalf_testBit` with `simp only`, per the tactic guidance in `CLAUDE.md`, and adds docstrings to the degree, monicity, and operation-unfolding lemmas that lacked them. `ROADMAP.md`: the new BF64 entry was inserted between "Basic field definitions" and the sub-bullets nested beneath it, orphaning that block. Moved it after, so it reads as the sibling it is. Verified: `lake build`, `lake test`, `lake exe axiomsweep --check` (0 sorry, 0 non-standard axioms), `lint-style.sh`, `check-imports.sh`, and `check-docs-integrity.py` all pass. Co-Authored-By: Claude Opus 5 * refactor(fields): carry toPoly_one_shiftLeft lemma into Common.lean to prevent BF64 from importing its sibling field * chore(fields): update documentation to include BF64 certificate and Char-2 dreferences --------- Co-authored-by: Claude Opus 5 --- CompPoly.lean | 6 + .../Fields/Binary/BF128Ghash/Prelude.lean | 41 +- CompPoly/Fields/Binary/BF64.lean | 21 + .../Fields/Binary/BF64/BaseCertificate.lean | 552 ++++++++++++++++++ CompPoly/Fields/Binary/BF64/Basic.lean | 199 +++++++ CompPoly/Fields/Binary/BF64/Ext3.lean | 202 +++++++ CompPoly/Fields/Binary/BF64/Impl.lean | 458 +++++++++++++++ CompPoly/Fields/Binary/BF64/Reduce.lean | 197 +++++++ CompPoly/Fields/Binary/Common.lean | 41 ++ CompPoly/Fields/README.md | 9 +- ROADMAP.md | 1 + docs/wiki/binary-fields-and-ntt.md | 44 ++ docs/wiki/field-extensions.md | 46 +- docs/wiki/generated-files.md | 11 +- tests/CompPolyTests.lean | 1 + tests/CompPolyTests/Fields/Binary/BF64.lean | 113 ++++ 16 files changed, 1885 insertions(+), 57 deletions(-) create mode 100644 CompPoly/Fields/Binary/BF64.lean create mode 100644 CompPoly/Fields/Binary/BF64/BaseCertificate.lean create mode 100644 CompPoly/Fields/Binary/BF64/Basic.lean create mode 100644 CompPoly/Fields/Binary/BF64/Ext3.lean create mode 100644 CompPoly/Fields/Binary/BF64/Impl.lean create mode 100644 CompPoly/Fields/Binary/BF64/Reduce.lean create mode 100644 tests/CompPolyTests/Fields/Binary/BF64.lean diff --git a/CompPoly.lean b/CompPoly.lean index 31949733..da6bc87c 100644 --- a/CompPoly.lean +++ b/CompPoly.lean @@ -105,6 +105,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 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..4fda54af --- /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 +`BitVec 64` 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..d37ccf13 --- /dev/null +++ b/CompPoly/Fields/Binary/BF64/Basic.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: 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 `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..5d6f465a --- /dev/null +++ b/CompPoly/Fields/Binary/BF64/Ext3.lean @@ -0,0 +1,202 @@ +/- +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/`, so `Ext ext3Params` +is definitionally `Vector BF64 3` — the three-limb layout, with no translation needed. + +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. +* `card_ext3` — `Fintype.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 + 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 + card_eq := 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)`. + +Definitionally `Vector BF64 3`, the three-limb layout `c0 + c1 * y + c2 * y^2`. -/ +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. -/ +@[simp] theorem card_ext3 : Fintype.card Ext3 = 2 ^ 192 := by + rw [Ext.card_ext, ext3Params_q, ext3Params_d, ← pow_mul] + +end BF64 diff --git a/CompPoly/Fields/Binary/BF64/Impl.lean b/CompPoly/Fields/Binary/BF64/Impl.lean new file mode 100644 index 00000000..2fb14574 --- /dev/null +++ b/CompPoly/Fields/Binary/BF64/Impl.lean @@ -0,0 +1,458 @@ +/- +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 is 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. + +## Main definitions + +* `BF64` — the carrier, `BitVec 64`, with `Add`, `Mul`, `Inv`, `CommRing` and `Field`. +* `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.card_bf64` — `Fintype.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 + +/-- `GF(2^64)` in its computable, machine representation: a 64-bit word whose bit `i` is +the coefficient of `x^i`. -/ +abbrev BF64 : Type := BitVec 64 + +namespace BF64 + +instance : Zero BF64 := ⟨(0 : BitVec 64)⟩ +instance : One BF64 := ⟨(1 : BitVec 64)⟩ + +/-- Addition in characteristic two is `xor`. -/ +instance : Add BF64 := ⟨fun a b => a ^^^ b⟩ + +/-- Negation is the identity in characteristic two. -/ +instance : Neg BF64 := ⟨fun a => a⟩ + +instance : Sub BF64 := ⟨fun a b => a ^^^ b⟩ + +/-- Multiplication: the carry-less product, reduced modulo the modulus. -/ +instance : Mul BF64 := + ⟨fun a b => reduce (carryLessMul (w := 128) a b)⟩ + +/-- The polynomial denoted by a carrier value. -/ +noncomputable def toPolyBF64 (a : BF64) : Polynomial (ZMod 2) := + toPoly (a : BitVec 64) + +/-- 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 = a ^^^ b := rfl + +/-- Multiplication unfolds to a carry-less product followed by `reduce`. -/ +theorem mul_def (a b : BF64) : a * b = reduce (carryLessMul (w := 128) a b) := 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 + rw [toPolyBF64, toPolyBF64, toPolyBF64, add_def] + 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, mul_def, toPoly_reduce, + toPoly_carryLessMul _ _ (by omega)] + rw [AdjoinRoot.mk_eq_mk, toPolyBF64, toPolyBF64] + exact ⟨-(toPoly a * toPoly b / 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 ^^^ b) := 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 ^^^ b) = 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 ^^^ b)) (not_lt.mpr hle) + have hxor : (a ^^^ b : BitVec 64) = 0 := by + by_contra hnz + exact ((toPoly_ne_zero_iff_ne_zero (a ^^^ b)).mpr hnz) hzero + have : a = b := by + have := congrArg (fun v => v ^^^ b) hxor + simpa [BitVec.xor_assoc] using this + exact this + +/-! ## 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 := BitVec.xor_self + +/-! ### Scalar and power operations + +In characteristic two an integer scalar multiple collapses to a parity test, and the +natural- and integer-number casts collapse likewise. Defining them in that closed form +keeps them computable and makes the transport conditions immediate. +-/ + +instance : SMul ℕ BF64 := ⟨nsmulRec⟩ +instance : SMul ℤ BF64 := ⟨zsmulRec nsmulRec⟩ +instance : NatCast BF64 := ⟨Nat.unaryCast⟩ +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 => + show toQuot ((k : BF64) + 1) = _ + rw [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.toFin + invFun i := BitVec.ofFin i + left_inv _ := rfl + right_inv _ := rfl + +instance : Fintype BF64 := Fintype.ofEquiv _ equivFin.symm + +/-- `BF64` has `2 ^ 64` elements. -/ +theorem card_bf64 : Fintype.card BF64 = 2 ^ 64 := by + rw [Fintype.card_congr equivFin, Fintype.card_fin] + +/-- The bridge is surjective: it is injective between finite types of equal cardinality. -/ +theorem toQuot_surjective : Function.Surjective toQuot := by + have hcard : Fintype.card BF64 = Fintype.card (BF64Quot) := by + rw [card_bf64, card_bf64Quot] + exact ((Fintype.bijective_iff_injective_and_card toQuot).mpr ⟨toQuot_injective, hcard⟩).2 + +/-- 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 aeb0cb58..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 @@ -339,6 +340,46 @@ lemma clMul_unfold (a b : B128) : 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 diff --git a/CompPoly/Fields/README.md b/CompPoly/Fields/README.md index 20910642..069a0d5c 100644 --- a/CompPoly/Fields/README.md +++ b/CompPoly/Fields/README.md @@ -66,6 +66,7 @@ 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 computable `BitVec 64` carrier, 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). @@ -73,11 +74,13 @@ The `Binary/` subtree provides characteristic-2 field infrastructure used by GHA ## 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/ROADMAP.md b/ROADMAP.md index 89c5a67a..f0f133cb 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -89,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. diff --git a/docs/wiki/binary-fields-and-ntt.md b/docs/wiki/binary-fields-and-ntt.md index f9f7ff99..008cb144 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,41 @@ 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 computable `BitVec 64` representation, its bridge to the quotient, 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. + +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 @@ -111,12 +153,14 @@ 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` -> diff --git a/docs/wiki/field-extensions.md b/docs/wiki/field-extensions.md index 63327998..3dfb2832 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 @@ -57,9 +59,10 @@ makes a cheap Frobenius and a norm-based inverse possible. See "Choosing a gener `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 +78,25 @@ 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, and it uses the *general* `ExtensionParams` path rather than +`BinomialParams`: over a char-2 field `X^3 - W = X^3 + W`, and the binomial criterion needs +`d ∣ q - 1`, which fails for `d = 3` and `q = 2^64` (`3 ∤ 2^64 - 1`). So `Ext P` is instantiated +directly, and `Ext ext3Params` is definitionally `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 `BitVec 64` carrier 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`, diff --git a/docs/wiki/generated-files.md b/docs/wiki/generated-files.md index e28a2d3f..dd5d2866 100644 --- a/docs/wiki/generated-files.md +++ b/docs/wiki/generated-files.md @@ -8,6 +8,7 @@ This page records which paths are source of truth and which are derived outputs. |---|---|---| | `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`. | +| `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/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. | | `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. | @@ -31,8 +32,9 @@ 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: @@ -50,6 +52,11 @@ The generator's exit code is its verdict: non-zero means the polynomial is reduc `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. diff --git a/tests/CompPolyTests.lean b/tests/CompPolyTests.lean index ed95eab6..df760a33 100644 --- a/tests/CompPolyTests.lean +++ b/tests/CompPolyTests.lean @@ -30,6 +30,7 @@ public import CompPolyTests.Fields.BN254.Fast public import CompPolyTests.Fields.BabyBear.Fast 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.Tower.Fast public import CompPolyTests.Fields.Extension.Arithmetic public import CompPolyTests.Fields.Extension.Binomial diff --git a/tests/CompPolyTests/Fields/Binary/BF64.lean b/tests/CompPolyTests/Fields/Binary/BF64.lean new file mode 100644 index 00000000..ec150336 --- /dev/null +++ b/tests/CompPolyTests/Fields/Binary/BF64.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: 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 + +/-! ## Non-vacuity guards -/ + +/-- The element `x`, a generator of the multiplicative group, is not zero. -/ +theorem generator_ne_zero : (0x2 : _root_.BF64) ≠ 0 := by decide +kernel + +/-- The generator is not one, so it is not a degenerate choice. -/ +theorem generator_ne_one : (0x2 : _root_.BF64) ≠ 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) * 0x01090913877ed8ed : _root_.BF64) = 0x01090913877ed8ed := 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 : + ((0x8000000000000000 : _root_.BF64) * 0x2 : _root_.BF64) = 0x1B := 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 : _root_.BF64) : Ext3 := + Ext.ofFn (fun i => 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 (0x01090913877ed8ed : _root_.BF64) * (0x01090913877ed8ed : _root_.BF64)⁻¹ == 1 +#guard (0 : _root_.BF64)⁻¹ == 0 +#guard y * y⁻¹ == 1 + +end Vectors + +end CompPolyTests.Fields.Binary.BF64Poly From b082daf2faa1cc74a572929bebe96bceae28d10a Mon Sep 17 00:00:00 2001 From: Alexander Hicks <25369263+alexanderlhicks@users.noreply.github.com> Date: Tue, 8 Sep 2026 20:40:02 +0100 Subject: [PATCH 20/45] fix(algebra): require identity maps in algebra towers (#324) * fix(algebra): require identity maps in algebra towers * refactor(algebra): polish tower identity contract --- CompPoly/Data/RingTheory/AlgebraTower.lean | 43 +++++++---- .../Fields/Binary/Tower/Abstract/Algebra.lean | 1 + .../Fields/Binary/Tower/Concrete/Algebra.lean | 1 + tests/CompPolyTests.lean | 2 + .../Data/RingTheory/AlgebraTower.lean | 74 +++++++++++++++++++ .../Fields/Binary/Tower/Algebra.lean | 57 ++++++++++++++ 6 files changed, 162 insertions(+), 16 deletions(-) create mode 100644 tests/CompPolyTests/Data/RingTheory/AlgebraTower.lean create mode 100644 tests/CompPolyTests/Fields/Binary/Tower/Algebra.lean diff --git a/CompPoly/Data/RingTheory/AlgebraTower.lean b/CompPoly/Data/RingTheory/AlgebraTower.lean index 5bc81078..79410d72 100644 --- a/CompPoly/Data/RingTheory/AlgebraTower.lean +++ b/CompPoly/Data/RingTheory/AlgebraTower.lean @@ -8,34 +8,34 @@ module 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` : 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) @@ -44,6 +44,17 @@ variable {ι : Type*} [Preorder ι] {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/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..7b030188 100644 --- a/CompPoly/Fields/Binary/Tower/Concrete/Algebra.lean +++ b/CompPoly/Fields/Binary/Tower/Concrete/Algebra.lean @@ -204,6 +204,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/tests/CompPolyTests.lean b/tests/CompPolyTests.lean index df760a33..7a4880d7 100644 --- a/tests/CompPolyTests.lean +++ b/tests/CompPolyTests.lean @@ -24,6 +24,7 @@ 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.Fields.BLS12_377.Fast public import CompPolyTests.Fields.BLS12_381.Fast public import CompPolyTests.Fields.BN254.Fast @@ -31,6 +32,7 @@ public import CompPolyTests.Fields.BabyBear.Fast 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.Tower.Algebra public import CompPolyTests.Fields.Binary.Tower.Fast public import CompPolyTests.Fields.Extension.Arithmetic public import CompPolyTests.Fields.Extension.Binomial diff --git a/tests/CompPolyTests/Data/RingTheory/AlgebraTower.lean b/tests/CompPolyTests/Data/RingTheory/AlgebraTower.lean new file mode 100644 index 00000000..fa60f58c --- /dev/null +++ b/tests/CompPolyTests/Data/RingTheory/AlgebraTower.lean @@ -0,0 +1,74 @@ +/- +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 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. +-/ + +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 + +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 + +end CompPolyTests.AlgebraTower 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 From bbc26a0980af9eca01c267fc5d5cb296fce69d82 Mon Sep 17 00:00:00 2001 From: Derek Sorensen Date: Wed, 9 Sep 2026 12:22:24 +0100 Subject: [PATCH 21/45] Shore up the benchmarking foundations: cheap sinks, sampling, and per-group seeding (#319) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * perf(bench): measure the benchmark, not the harness The timed loop folded a `Nat` checksum modulo the largest prime below 2^64 into every iteration, so each fold was a heap-allocating bignum multiply-and-mod inside the measured region. Fold results through a `UInt64` sink instead and keep the strong `Nat` digest in the untimed validation pass, where correctness is actually established. `runTimed` gains an optional `sink : α → UInt64` defaulting to a truncation of the existing `Nat` digest, so all 226 call sites keep working while the bignum leaves the timed region everywhere. Carriers whose canonical value exceeds 2^63 declare a native sink; `Goldilocks.Fast` and `ZMod` do so here. Add `harness-floor` and `harness-canary`. The floor times an empty body and is the per-iteration cost every other benchmark sits on top of; the canary times a known non-eliminable body and fails the run if it does not clear the floor by `canaryFloorRatio`. A benchmark that has been optimised away otherwise looks exactly like a benchmark that got very fast. Both additive-NTT sinks now fold over every output position. The reference row returns `Fin (2 ^ n) → α`, so realising the whole result is part of its work and not part of the `Array`-returning fast row's; a sink may only skip work the benchmark has already done. Measured on darwin/arm64 at `--small`: harness floor 1.89 ns/iter, `goldilocks-mul-fast` 619 ns -> 3 ns, `goldilocks-mul-zmod` 788 ns -> 365 ns, reported ratio 1.27x -> ~108x. Two full 68-group runs agree on every one of 250 validation digests. Also escape `jsonString` via `Lean.Json.renderString`, hoist `arrayToFinFunction` out of its fold in `checksumConcreteBtfOutputArray`, and emit `sink_digest` so the timed accumulator stays observably live. * feat(bench): collect samples instead of one total, and report dispersion Every benchmark reported one sample of one total, so a scheduler hiccup and a real regression were indistinguishable. Worse, 67 of 253 rows timed exactly one iteration and 107 timed three or fewer, which is where the large-NTT and batch-eval numbers live. Treat each benchmark's iteration count as a total-work budget and split it into up to `targetSampleCount` timed samples. Report the median as the headline number alongside min, mean, p95, standard deviation and median absolute deviation, and emit the whole per-sample vector so the distribution can be examined offline. Label Tukey outliers at the conventional 1.5x and 3x interquartile fences without dropping them, since a sample that took ten times the median is data about the machine. Suppress labelling when the interquartile range is zero, where both fences collapse onto the quartiles and mark every sample that differs at all. Where one iteration already exhausts the budget the row is reported as `n=1` rather than as a number with an implied precision it does not have. Cap the validation pass at `validationIterationCap`, and let it count towards warmup: it has already executed the body, so an expensive workload validated once now runs twice per benchmark rather than three times. Across a full 68-group `--small` run: 172 of 286 rows now carry five or more samples, median dispersion is 1.4% of the median with a 5.1% maximum, and the timed-region total is unchanged at 152 s. * feat(bench): seed each group from its key One StdGen was threaded through the selected groups in order, so a group's inputs depended on which groups ran before it. `--group X` and `--groups X,Y` measured different inputs for X, adding a group anywhere changed the inputs of every group after it, and the CI subset measured neither what a full local run measured nor what any earlier run measured. Confirmed empirically: 199 of 235 rows shared with a six-week-old result file differ for no reason other than groups having been added between the two commits. Derive each group's generator from its key inside `BenchTask.fromGroupRunner`. Every registered task goes through that function, so no group runner changes. Registration becomes authoritative while we are here: `fromGroupRunner` stamps the key and title from the `BenchGroupInfo` that `--list` and the CI allowlist validate against, so a runner can no longer drift from its registration. Remove 25 declarations with no references anywhere: ten per-area `runX` wrappers and fifteen `*GroupInfos` aggregate lists. Verified: a group measures identical inputs alone, alongside another group, and with the order reversed; and the exact `BENCH_CI_GROUPS` subset reproduces the full run's digest on all 184 comparable rows. * feat(bench): report the host, tidy output, and document the harness Hardware probing was `lscpu`, `nproc`, `/proc/meminfo` and `df --output`, all Linux-only, so every local report read `unavailable outside GitHub Actions` on the primary development platform. Fall back to `sysctl` when the Linux probes are absent, parsing the BSD `df -h` table where the size is the second field rather than the first. Write reports and results to `bench/out/` instead of dropping them beside the sources. Two ignore rules across two files become one, and CI's artifact glob can no longer pick up anything but the run it just made -- on a fresh checkout it never did, but locally it swept every stale file into the artifact. `tests/CompPolyTests/Fields/Binary/CommonBench.lean` was an unimported `#eval` benchmark, but it also carried four `#guard` correctness checks and the removed `Finset.fold` baseline that pins `clMul` to the behaviour it replaced, so CI has never run them. Move those into `Fields/Binary/Common.lean`, which the test driver imports, and delete the benchmark. `NTT/Benchmark.lean` stays: it holds the only NTT-vs-schoolbook crossover logic in the repo. Add `docs/wiki/benchmarking.md`, registered in both hand-maintained lists in the wiki README, covering the validation/timed split, the rule that a sink may only skip work the benchmark has already done, how to read dispersion, the harness self-check, determinism, and the remaining gaps. * ci(bench): gate on benchmark correctness, run timings on demand The benchmark step was doing two unrelated jobs in the blocking CI job: 41 groups cross-checking each canonical `ZMod` model against its native-word implementation on random inputs plus the harness canary, and a timing report. Only the first is worth gating on. `ubuntu-latest` is a shared 2-vCPU VM, and the median sample dispersion measured on a quiet local machine is 1.4%, so gating on those timings would gate on noise. Add `--validate-only`, which runs the untimed digest pass and the group agreement check and collects no samples. It is deterministic and machine independent, which is what a gate should be, and it is the fast local answer to whether an implementation is still correct. The mode is threaded through an `IO.Ref` set from the command line rather than a parameter, because every alternative means editing all 226 `runTimed` call sites. The canary needs an escape hatch: it compares timed totals, so with no samples collected `0 < 3 * 0` is false and it 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. Main CI now runs `--validate-only`. Over the 41 curated groups at `--medium` on darwin/arm64 that is 32s against 124s for the timed run it replaces, and it still fails the build on a digest mismatch or a collapsed canary. Timings move to a new `benchmarks.yml`, produced on demand three ways: Actions -> Benchmarks -> Run workflow with a preset and optional group list, a `/bench` comment from a repo member following the existing `/review` convention, or automatically on a PR touching `bench/**` -- the one place a path filter genuinely fits, since a change to the harness itself should be measured. It restores the build caches and never saves them, because the Actions cache is already documented as over quota. Path filtering the benchmarks themselves was considered and rejected: 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. `BENCH_CI_GROUPS` moves out of the workflow `env:` into `bench/ci-groups.txt`, since a second workflow cannot read a workflow-scoped variable and duplicating 41 keys invites drift. The list now sits next to the benchmarks it names. * docs(bench): correct the claim about shared-runner noise I asserted, here and in the wiki and both workflow comments, that sample dispersion on a shared CI runner would be worse than the 1.4% median MAD measured locally. The first real run of the timing workflow says the opposite: median MAD 0.2%, p90 0.5%, max 1.0% across 172 replicated rows. A 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, which is what a quiet baseline punctuated by preemption looks like. The decision to keep timings out of the gate is unchanged, but the reason was wrong. A gate compares runs against each other, on a runner whose CPU model varies between runs, and a single run cannot measure that variance. Timings are advisory because cross-run comparability is unvalidated, not because within-run noise is high. Also record the measured runner costs: the correctness gate is 46s where the timed run it replaced was about 167s. * test(fields): pin both widths of the carry-less multiply The baseline rescued from `CommonBench.lean` was written against `clMul`, but #320 has since made the multiplication width-generic and #321 added `BF64`, whose `mul` is the 64-bit instance. As merged, the rescued guards pinned only the 128-bit width; the new one was covered only indirectly, by #321's reference vectors, which pin the field rather than the multiplication against its predecessor. Generalize `clMulBaseline` in the operand width and add four guards at width 64. Verified by breaking one and confirming the build fails. Also record the `GF(2^64)` bench coverage gap in the wiki's known-gaps list: the tower groups are still the only binary-field timings. --- .github/workflows/benchmarks.yml | 244 +++ .github/workflows/lean_action_ci.yml | 57 +- .gitignore | 1 - BENCHMARKING.md | 1684 +++++++++++++++++ bench/.gitignore | 3 +- bench/CompPolyBench/Bivariate/Basic.lean | 12 - bench/CompPolyBench/Bivariate/Factor.lean | 13 - bench/CompPolyBench/Common.lean | 254 ++- .../Fields/Binary/AdditiveNTT/Impl.lean | 56 +- bench/CompPolyBench/Fields/Binary/Tower.lean | 6 - bench/CompPolyBench/Fields/Extension.lean | 12 - bench/CompPolyBench/Fields/Goldilocks.lean | 12 +- bench/CompPolyBench/Fields/Montgomery.lean | 7 - bench/CompPolyBench/Harness/Sample.lean | 93 + bench/CompPolyBench/Harness/SelfCheck.lean | 100 + bench/CompPolyBench/Harness/Sink.lean | 58 + bench/CompPolyBench/Harness/Stats.lean | 122 ++ bench/CompPolyBench/Harness/Timer.lean | 73 + bench/CompPolyBench/Multilinear/Basic.lean | 15 - .../Multivariate/CMvPolynomial.lean | 13 - bench/CompPolyBench/Setup.lean | 63 +- bench/CompPolyBench/Univariate.lean | 11 - bench/CompPolyBench/Univariate/Basic.lean | 20 - bench/CompPolyBench/Univariate/BatchEval.lean | 12 - bench/CompPolyBench/Univariate/ManyEval.lean | 11 - .../CompPolyBench/Univariate/NTT/FastMul.lean | 11 - .../Univariate/NTT/FastMulLow.lean | 11 - bench/README.md | 141 +- bench/ci-groups.txt | 58 + docs/wiki/README.md | 4 + docs/wiki/benchmarking.md | 182 ++ docs/wiki/generated-files.md | 4 +- docs/wiki/quickstart.md | 32 +- tests/CompPolyTests.lean | 1 + tests/CompPolyTests/Fields/Binary/Common.lean | 65 + .../Fields/Binary/CommonBench.lean | 100 - 36 files changed, 3155 insertions(+), 406 deletions(-) create mode 100644 .github/workflows/benchmarks.yml create mode 100644 BENCHMARKING.md create mode 100644 bench/CompPolyBench/Harness/Sample.lean create mode 100644 bench/CompPolyBench/Harness/SelfCheck.lean create mode 100644 bench/CompPolyBench/Harness/Sink.lean create mode 100644 bench/CompPolyBench/Harness/Stats.lean create mode 100644 bench/CompPolyBench/Harness/Timer.lean create mode 100644 bench/ci-groups.txt create mode 100644 docs/wiki/benchmarking.md create mode 100644 tests/CompPolyTests/Fields/Binary/Common.lean delete mode 100644 tests/CompPolyTests/Fields/Binary/CommonBench.lean diff --git a/.github/workflows/benchmarks.yml b/.github/workflows/benchmarks.yml new file mode 100644 index 00000000..84a93c27 --- /dev/null +++ b/.github/workflows/benchmarks.yml @@ -0,0 +1,244 @@ +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) + if [ -e "${results[0]}" ]; then + cp "${results[@]}" "$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 0144f60f..18725d5d 100644 --- a/.github/workflows/lean_action_ci.yml +++ b/.github/workflows/lean_action_ci.yml @@ -18,38 +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-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 - jobs: build: runs-on: ubuntu-latest @@ -209,19 +177,26 @@ jobs: 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) if [ -e "${results[0]}" ]; then cp "${results[@]}" "$EVALUATION_BENCH_ARTIFACT_DIR/" fi @@ -231,7 +206,7 @@ jobs: 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/BENCHMARKING.md b/BENCHMARKING.md new file mode 100644 index 00000000..9de5343a --- /dev/null +++ b/BENCHMARKING.md @@ -0,0 +1,1684 @@ +# 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`: + +- `tests/CompPolyTests/Univariate/NTT/Benchmark.lean` — NTT-vs-naive crossover + sweep over 20 operand sizes, `IO.monoMsNow`. +- `tests/CompPolyTests/Bivariate/KroneckerBenchmark.lean` +- `CommonBench.lean` under `tests/CompPolyTests/Fields/Binary/` (removed in 12.4; + its correctness guards now live in + `tests/CompPolyTests/Fields/Binary/Common.lean`) + +None is imported by `tests/CompPolyTests.lean`, so none runs under `lake test` +or in CI. Each documents its own manual invocation (`lake build +CompPolyTests.Bivariate.KroneckerBenchmark`). + +### 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 | `tests/CompPolyTests/Univariate/NTT/Benchmark.lean`, `tests/CompPolyTests/Bivariate/KroneckerBenchmark.lean` (a third, `CommonBench.lean`, was removed 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. + +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 | + +### 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. + +`tests/CompPolyTests/Univariate/NTT/Benchmark.lean` and +`KroneckerBenchmark.lean` are deliberately left in place: the former holds the +only NTT-vs-schoolbook crossover logic in the repo and is the specification for a +future crossover metric. + +**`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. 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..77aed53a 100644 --- a/bench/CompPolyBench/Bivariate/Basic.lean +++ b/bench/CompPolyBench/Bivariate/Basic.lean @@ -19,13 +19,6 @@ 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)"⟩ -] - /-- Shared input-shape label for bivariate evaluation benchmarks. -/ private def bivariateInputShape : String := "xDegree<8, yDegree<64, one nonzero per 4 coeffs, 32 points" @@ -194,9 +187,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..eb1361e0 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 := diff --git a/bench/CompPolyBench/Common.lean b/bench/CompPolyBench/Common.lean index a2a8d454..5c86a5d2 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.Sample 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 @@ -134,6 +136,9 @@ structure BenchRecord where totalNanos : Nat averageNanos : Nat checksum : Nat + sinkDigest : UInt64 + stats : SampleStats + samples : Array Nat /-- A set of benchmark rows expected to produce matching checksums. -/ structure BenchGroup where @@ -176,13 +181,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 := @@ -265,9 +284,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) @@ -290,13 +313,20 @@ 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") /-- Trim command output and normalize empty output to the empty string. -/ def trimCommandOutput (s : String) : String := @@ -361,11 +391,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" @@ -518,34 +597,73 @@ def checksumCPolynomial [Zero α] (checksum : α → Nat) (p : CPolynomial α) : def checksumRawPolynomial (checksum : α → Nat) (p : CPolynomial.Raw α) : Nat := checksumArray checksum p +/-! ### 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 validation-pass iterations. + +The validation 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 above every benchmark's operand-pool size, so the oracle still sees +every input it did before. -/ +def validationIterationCap : Nat := 256 + /-- Compute the checksum iteration count shared by a benchmark group. -/ def groupChecksumIterations (first : Nat) (rest : List Nat) : Nat := - rest.foldl Nat.min first + min validationIterationCap (rest.foldl Nat.min first) /-- 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` checksum runs *before* timing, over `checksumIterations` — the +minimum measured-iteration count used by the records in the surrounding group — +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. + +Under `--validate-only` no samples are collected and the record carries digests +alone. `forceTiming` opts out of that, for the harness self-check, whose canary +compares timed totals and would pass vacuously against a zero floor. -/ -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 runTimed (name representation method field inputShape : String) + (preset : BenchPreset) (warmup measured : Nat) (run : Nat → α) (checksum : α → Nat) + (checksumIterations : Nat := min validationIterationCap measured) + (sink : α → UInt64 := fun x ↦ natSink (checksum x)) + (forceTiming : Bool := false) : 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 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) && !forceTiming + let plan := if validateOnly then { itersPerSample := 0, sampleCount := 0 } else + planSamples measured + -- The validation pass above already executed the body, so it counts towards + -- reaching steady state. For an expensive workload validated once, this is the + -- difference between running it three times and running it twice. + let desiredWarmup := if validateOnly then 0 else max warmup plan.itersPerSample + let sampled ← collectSamples (desiredWarmup - min desiredWarmup checksumIterations) plan body + let total := sampled.totalNanos pure { name := name representation := representation @@ -553,12 +671,15 @@ def runTimed (name representation method field inputShape : String) (preset : Be preset := preset.name field := field inputShape := inputShape - warmupIterations := warmup + warmupIterations := desiredWarmup checksumIterations := checksumIterations - measuredIterations := measured + measuredIterations := sampled.totalIterations totalNanos := total - averageNanos := if measured = 0 then 0 else total / measured + averageNanos := sampled.stats.medianPicos / 1000 checksum := validationChecksum + sinkDigest := sampled.sink + stats := sampled.stats + samples := sampled.samples } /-- Append benchmark records from `ys` onto `xs`. -/ @@ -573,9 +694,9 @@ def appendGroups (xs ys : Array BenchGroup) : Array BenchGroup := def flattenGroups (groups : Array BenchGroup) : Array BenchRecord := groups.foldl (init := #[]) fun acc group ↦ appendRecords acc group.records -/-- Render a benchmark string field as a JSON string. -/ +/-- 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 := @@ -591,7 +712,21 @@ def BenchRecord.toJsonLine (record : BenchRecord) : String := "\"measured_iterations\":" ++ toString record.measuredIterations, "\"total_nanos\":" ++ toString record.totalNanos, "\"average_nanos\":" ++ toString record.averageNanos, - "\"checksum\":" ++ toString record.checksum + "\"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. -/ @@ -834,6 +969,23 @@ def implementationLabelInGroup (records : List BenchRecord) (record : BenchRecor else label ++ " (" ++ record.field ++ ")" +/-- 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 + /-- Columns rendered in a group result table after shared metadata is lifted out. -/ def groupResultColumns (records : List BenchRecord) (totalUnit avgUnit : TimeUnit) : List (String × Bool × (BenchRecord → String)) := @@ -842,8 +994,9 @@ def groupResultColumns (records : List BenchRecord) (totalUnit avgUnit : TimeUni ("Iterations", true, fun r ↦ toString r.measuredIterations), ("Total (" ++ totalUnit.label ++ ")", true, fun r ↦ formatNanosInUnitOrAuto totalUnit r.totalNanos), - ("Avg (" ++ avgUnit.label ++ ")", true, fun r ↦ - formatNanosInUnitOrAuto avgUnit r.averageNanos) + ("Median (" ++ avgUnit.label ++ ")", true, fun r ↦ + formatNanosInUnitOrAuto avgUnit r.averageNanos), + ("Spread", true, renderSpread) ] /-- Shared metadata rendered before each benchmark group result table. -/ @@ -853,7 +1006,8 @@ def renderGroupMetadata (records : List BenchRecord) (totalUnit : TimeUnit) : Li 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) + renderSharedNatLine "Checksum iterations" records (fun r ↦ r.checksumIterations), + renderSharedNatLine "Samples" records (fun r ↦ r.stats.count) ] ++ [ "- Total group time: `" ++ formatNanosWithUnit totalUnit (totalGroupNanos records) ++ "`", @@ -932,4 +1086,36 @@ 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 := + match matchingChecksum? records with + | some checksum => "agree | `" ++ toString checksum ++ "`" + | none => "**MISMATCH** | -" + "| `" ++ group.groupKey ++ "` | " ++ toString group.records.size ++ " | " ++ status ++ " |" + +/-- Render the report for a `--validate-only` run. + +Deliberately not the timing table: a validation run collects no samples, so +every duration would be zero. What it has to say is whether each group's +implementations agree, and on what digest. -/ +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 ++ "`", + "", + "No timings were collected. 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 timings.", + "", + "| Group | Rows | Implementations | Digest |", + "| ----- | ---: | --------------- | ------ |" + ] ++ groups.toList.map validationRow) ++ "\n" + end CompPolyBench diff --git a/bench/CompPolyBench/Fields/Binary/AdditiveNTT/Impl.lean b/bench/CompPolyBench/Fields/Binary/AdditiveNTT/Impl.lean index bfc6e270..79f0a1e8 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) @@ -97,11 +125,13 @@ private def runAdditiveNttCase (ℓ R_rate : Nat) (h_ℓ_add_R_rate : ℓ + R_ra fieldLabel inputShape preset warmup measured (fun _ ↦ runBtf3Ntt ℓ R_rate h_ℓ_add_R_rate input) (checksumBtf3Output (n := ℓ + R_rate)) (checksumIterations := checksumIterations) + (sink := sinkBtf3Output) 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) + (sink := sinkBtf3OutputArray) pure ({ groupKey := key, title := s!"Additive NTT BTF3 l={ℓ} R_rate={R_rate}", @@ -124,6 +154,7 @@ private def runAdditiveNttFastLargeCase (k ℓ R_rate : Nat) fieldLabel inputShape preset warmup measured (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}", @@ -170,9 +201,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 index 707537b8..ec2c397f 100644 --- a/bench/CompPolyBench/Fields/Binary/Tower.lean +++ b/bench/CompPolyBench/Fields/Binary/Tower.lean @@ -26,12 +26,6 @@ namespace CompPolyBench /-- Input-shape label shared by the tower benchmarks. -/ private def towerShape : String := "64 random 128-bit elements, pairwise" -/-- Benchmark group metadata for the binary tower field. -/ -def towerGroupInfos : List BenchGroupInfo := [ - ⟨"fields-tower-bt128-mul", "Binary tower multiplication (GF(2^128))"⟩, - ⟨"fields-tower-bt128-inv", "Binary tower inversion (GF(2^128))"⟩ -] - /-- 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 diff --git a/bench/CompPolyBench/Fields/Extension.lean b/bench/CompPolyBench/Fields/Extension.lean index 08a7af3c..d86455e4 100644 --- a/bench/CompPolyBench/Fields/Extension.lean +++ b/bench/CompPolyBench/Fields/Extension.lean @@ -57,18 +57,6 @@ 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)"⟩ -] - /-- Time one extension operation over a field-specific sample, packaged as a single-record group. diff --git a/bench/CompPolyBench/Fields/Goldilocks.lean b/bench/CompPolyBench/Fields/Goldilocks.lean index 9c31bd14..7e90bd5a 100644 --- a/bench/CompPolyBench/Fields/Goldilocks.lean +++ b/bench/CompPolyBench/Fields/Goldilocks.lean @@ -27,12 +27,6 @@ namespace CompPolyBench /-- Input-shape label shared by the Goldilocks arithmetic benchmarks. -/ private def goldilocksShape : String := "256 random elements" -/-- Benchmark group metadata for Goldilocks arithmetic. -/ -def goldilocksGroupInfos : List BenchGroupInfo := [ - ⟨"fields-goldilocks-mul", "Goldilocks multiplication"⟩, - ⟨"fields-goldilocks-inv", "Goldilocks inversion"⟩ -] - /-- Time canonical against native-word Goldilocks multiplication as a single group. -/ private def runGoldilocksMul (preset : BenchPreset) (gen : StdGen) : IO (BenchGroup × StdGen) := do @@ -45,12 +39,13 @@ private def runGoldilocksMul (preset : BenchPreset) (gen : StdGen) : let zmodRecord ← runTimed "goldilocks-mul-zmod" "ZMod" "mul" "Goldilocks.Field" goldilocksShape preset warmup zmodMeasured (fun i ↦ values.getD (i % values.size) 1 * values.getD ((i + 1) % values.size) 1) - checksumZMod (checksumIterations := checksumIterations) + checksumZMod (checksumIterations := checksumIterations) (sink := sinkZMod) let fastRecord ← runTimed "goldilocks-mul-fast" "UInt64" "mul" "Goldilocks.Fast.Field" goldilocksShape preset warmup fastMeasured (fun i ↦ fastValues.getD (i % fastValues.size) 1 * fastValues.getD ((i + 1) % fastValues.size) 1) checksumGoldilocksFast (checksumIterations := checksumIterations) + (sink := sinkGoldilocksFast) pure ({ groupKey := "fields-goldilocks-mul", title := "Goldilocks multiplication", records := #[zmodRecord, fastRecord] }, gen) @@ -66,11 +61,12 @@ private def runGoldilocksInv (preset : BenchPreset) (gen : StdGen) : let zmodRecord ← runTimed "goldilocks-inv-zmod" "ZMod" "inv" "Goldilocks.Field" goldilocksShape preset warmup zmodMeasured (fun i ↦ (values.getD (i % values.size) 1)⁻¹) - checksumZMod (checksumIterations := checksumIterations) + checksumZMod (checksumIterations := checksumIterations) (sink := sinkZMod) let fastRecord ← runTimed "goldilocks-inv-fast" "UInt64" "inv (Fermat chain)" "Goldilocks.Fast.Field" goldilocksShape preset warmup fastMeasured (fun i ↦ (fastValues.getD (i % fastValues.size) 1)⁻¹) checksumGoldilocksFast (checksumIterations := checksumIterations) + (sink := sinkGoldilocksFast) pure ({ groupKey := "fields-goldilocks-inv", title := "Goldilocks inversion", records := #[zmodRecord, fastRecord] }, gen) diff --git a/bench/CompPolyBench/Fields/Montgomery.lean b/bench/CompPolyBench/Fields/Montgomery.lean index 2bbf65dd..b0dae043 100644 --- a/bench/CompPolyBench/Fields/Montgomery.lean +++ b/bench/CompPolyBench/Fields/Montgomery.lean @@ -28,13 +28,6 @@ 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) diff --git a/bench/CompPolyBench/Harness/Sample.lean b/bench/CompPolyBench/Harness/Sample.lean new file mode 100644 index 00000000..98b205a0 --- /dev/null +++ b/bench/CompPolyBench/Harness/Sample.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.Harness.Stats +public import CompPolyBench.Harness.Timer + +/-! +# Benchmark Sampling + +Collecting a benchmark's cost as a *set* of samples rather than one total. + +The suite's per-benchmark iteration counts are treated as a total-work budget: +rather than timing all of them in one region and dividing, the budget is split +into `targetSampleCount` samples so the spread between them is visible. 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 + +/-- Samples aimed for when the iteration budget allows it. -/ +def targetSampleCount : Nat := 20 + +/-- Divide a total iteration budget into samples. + +Where the budget allows at least `targetSampleCount` iterations the budget is +split evenly. Below that each iteration becomes its own sample, which keeps as +much replication as the budget can pay for. -/ +def planSamples (totalIterations : Nat) : SamplingPlan := + if totalIterations = 0 then + { itersPerSample := 0, sampleCount := 0 } + else if totalIterations ≤ targetSampleCount then + { itersPerSample := 1, sampleCount := totalIterations } + else + { itersPerSample := totalIterations / targetSampleCount, sampleCount := targetSampleCount } + +/-- 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 + +/-- Warm a benchmark body, then collect `plan.sampleCount` timed samples of it. + +`warmup` is the number of *residual* warmup iterations to run; the caller is +expected to have already discounted any pass that executed the body beforehand. +Every sample replays the same iteration indices, so samples differ only in +machine state and not in the work performed. -/ +@[specialize] def collectSamples (warmup : Nat) (plan : SamplingPlan) + (body : Nat → UInt64 → UInt64) : IO SampledRun := do + let mut acc ← warmIterations warmup 0 body + 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..1cc513e0 --- /dev/null +++ b/bench/CompPolyBench/Harness/SelfCheck.lean @@ -0,0 +1,100 @@ +/- +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 + +/-! +# 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. +-/ + +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"⟩ +] + +/-- 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 must exceed the floor. + +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. -/ +def canaryFloorRatio : Nat := 3 + +/-- Measured iterations for the self-check benchmarks. -/ +private def harnessMeasuredIterations (preset : BenchPreset) : Nat := + preset.selectNat 2000000 500000 100000 + +/-- Time the harness floor and the canary, and reject a collapsed canary. -/ +private def runHarnessSelfCheck (preset : BenchPreset) (selection : BenchSelection) + (gen : StdGen) : IO (Array BenchGroup × StdGen) := do + let measured := harnessMeasuredIterations preset + let warmup := measured / 10 + let floorRecord ← runTimed "harness-floor" "UInt64" "empty body" + "none" "no input" preset warmup measured + (fun i ↦ i.toUInt64) (fun x ↦ x.toNat) (sink := u64Sink) (forceTiming := true) + let canaryRecord ← runTimed "harness-canary" "UInt64" s!"{canaryRounds} mixing rounds" + "none" "no input" preset warmup measured + (fun i ↦ canaryWork i.toUInt64) (fun x ↦ x.toNat) (sink := u64Sink) + (forceTiming := true) + if canaryRecord.totalNanos < canaryFloorRatio * floorRecord.totalNanos then + throw <| IO.userError <| + s!"harness canary collapsed onto the loop floor: canary {canaryRecord.totalNanos}ns " ++ + s!"vs floor {floorRecord.totalNanos}ns over {measured} iterations " ++ + s!"(expected at least {canaryFloorRatio}x). Benchmark bodies are being " ++ + "optimised away, so every measured time in this run is meaningless." + 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] } + 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..e04de346 --- /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. + +Kept `@[noinline]` so the fold survives optimisation; the `harness-floor` group +measures what this costs and `harness-canary` fails the run if it stops costing +anything. -/ +@[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..2db900da --- /dev/null +++ b/bench/CompPolyBench/Harness/Timer.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: 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 once from the command line rather than threaded through `runTimed`, whose +226 call sites all pass their arguments positionally. Read by `runTimed`, which +skips sample collection entirely when it is set. -/ +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 without timing it, returning the sink +accumulator so the loop has an observable result. + +The accumulator must be fed into the subsequent timed run; discarding it would +leave the warmup loop eliminable, which is how warmup came to be a no-op before. -/ +@[inline] def warmIterations (iters : Nat) (init : UInt64) + (body : Nat → UInt64 → UInt64) : IO UInt64 := do + let mut acc := init + for i in [0:iters] do + acc := body i acc + pure acc + +/-- Run a benchmark body `iters` times and return the elapsed nanoseconds. + +`init` seeds the sink accumulator, normally from `warmIterations`. 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 } + +/-- Warm a benchmark body and then time it, in one step. -/ +@[inline] def warmThenTime (warmup measured : Nat) + (body : Nat → UInt64 → UInt64) : IO TimedSample := do + let warmed ← warmIterations warmup 0 body + timeIterations measured warmed body + +end CompPolyBench diff --git a/bench/CompPolyBench/Multilinear/Basic.lean b/bench/CompPolyBench/Multilinear/Basic.lean index 1ba36941..960d417f 100644 --- a/bench/CompPolyBench/Multilinear/Basic.lean +++ b/bench/CompPolyBench/Multilinear/Basic.lean @@ -53,16 +53,6 @@ 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 @@ -290,9 +280,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..0303a854 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 @@ -253,9 +245,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 66575c35..a11bbb13 100644 --- a/bench/CompPolyBench/Setup.lean +++ b/bench/CompPolyBench/Setup.lean @@ -13,6 +13,7 @@ public import CompPolyBench.Fields.Binary.Tower public import CompPolyBench.Fields.Extension public import CompPolyBench.Fields.Goldilocks public import CompPolyBench.Fields.Montgomery +public import CompPolyBench.Harness.SelfCheck public import CompPolyBench.Multilinear.Basic public import CompPolyBench.Multivariate.CMvPolynomial public import CompPolyBench.Univariate @@ -29,9 +30,9 @@ namespace CompPolyBench /-- Runnable benchmark registry. -/ def allTasks : List BenchTask := - univariateTasks ++ multivariateTasks ++ multilinearTasks ++ bivariateTasks ++ factorTasks ++ - guruswamiSudanTasks ++ additiveNttTasks ++ extensionTasks ++ montgomeryInvTasks ++ - towerTasks ++ goldilocksTasks + harnessTasks ++ univariateTasks ++ multivariateTasks ++ multilinearTasks ++ bivariateTasks ++ + factorTasks ++ guruswamiSudanTasks ++ additiveNttTasks ++ extensionTasks ++ + montgomeryInvTasks ++ towerTasks ++ goldilocksTasks /-- Metadata for every benchmark group accepted by the command-line selector. -/ def allGroupInfos : List BenchGroupInfo := @@ -81,6 +82,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 @@ -94,7 +96,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 := @@ -106,10 +113,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 @@ -119,34 +127,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 @@ -155,19 +164,26 @@ 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 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 + let hardware ← collectRunnerHardware + IO.FS.writeFile (reportPath runId) (renderMarkdown hardware preset groups) IO.println <| - s!"wrote {records.size} benchmark records in {groups.size} groups for run {runId}" + 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}" match checksumMismatchGroups groups with | [] => pure 0 | mismatchedGroups => @@ -188,7 +204,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..275187dc 100644 --- a/bench/CompPolyBench/Univariate.lean +++ b/bench/CompPolyBench/Univariate.lean @@ -20,21 +20,10 @@ 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 - end CompPolyBench diff --git a/bench/CompPolyBench/Univariate/Basic.lean b/bench/CompPolyBench/Univariate/Basic.lean index 57e51917..e2bf9656 100644 --- a/bench/CompPolyBench/Univariate/Basic.lean +++ b/bench/CompPolyBench/Univariate/Basic.lean @@ -22,21 +22,6 @@ 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) @@ -511,9 +496,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..25fa999a 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 @@ -503,9 +496,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..034e10b7 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 @@ -101,9 +95,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/FastMul.lean b/bench/CompPolyBench/Univariate/NTT/FastMul.lean index 88f3c81b..3739ffa1 100644 --- a/bench/CompPolyBench/Univariate/NTT/FastMul.lean +++ b/bench/CompPolyBench/Univariate/NTT/FastMul.lean @@ -20,12 +20,6 @@ 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 @@ -179,9 +173,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..421e34d0 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 @@ -133,10 +128,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/README.md b/bench/README.md index d4b186ba..e8e981b7 100644 --- a/bench/README.md +++ b/bench/README.md @@ -46,17 +46,18 @@ 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 ``` 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. +run by every implementation in that group, capped at `validationIterationCap`. ## What Is Measured @@ -76,6 +77,7 @@ Roughly by area, with representative group prefixes: | Binary tower fields | `fields-tower-bt128-*`: `BitVec` spec vs packed-word implementation | | Goldilocks arithmetic | `fields-goldilocks-{mul,inv}`: canonical `ZMod` vs single-word `UInt64` | | Scalar-field inversion | `fields-mont64x8-*-inv`: `ZMod` extended Euclid vs checked binary GCD vs Fermat | +| Harness self-check | `harness-floor`, `harness-canary`: the harness measuring itself, see below | Use `--list` for the authoritative set; the prefixes above drift as groups are added. @@ -92,21 +94,126 @@ univariate-dense-bn254 univariate-dense-bls12-381 univariate-dense-bls12-377 ``` +## How A Benchmark Is Measured + +`runTimed` does two passes over each benchmark body. + +The **validation pass** is untimed and folds a strong `Nat` digest +(`mixChecksum`) over the full result. It is capped at +`validationIterationCap` iterations — above every benchmark's operand-pool +size, 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. + +### Sampling and dispersion + +A benchmark's cost is collected as a *set* of samples, not one total. Each +benchmark's iteration count is treated as a total-work budget and split into up +to `targetSampleCount` timed samples; every sample replays the same iteration +indices, so samples differ only in machine state. + +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. + ## 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 now also stable across runs is what makes them usable as regression +fixtures. Digests are still preset-dependent, because the validation pass runs +`min validationIterationCap` of the group's measured iteration count and that +count varies by preset. -## CI +## The two CI tracks -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. +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 "" +``` -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. +which does the untimed digest pass and the group agreement check but collects no +samples. It takes about 34 seconds 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. + +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. + +## The curated group set + +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..1de26e0b --- /dev/null +++ b/bench/ci-groups.txt @@ -0,0 +1,58 @@ +# 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-*). Validating this +# subset takes ~34s against ~138s for every registered group; widen it if that +# trade changes. +# +# One key per line. Blank lines and `#` comments are ignored. +harness-floor +harness-canary +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 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..b38bad18 --- /dev/null +++ b/docs/wiki/benchmarking.md @@ -0,0 +1,182 @@ +# 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 | ~34s over the curated set, ~138s 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 is capped at +`validationIterationCap` iterations 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. + +## 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. Give every implementation in the group the same `checksum`, so the agreement + check is meaningful. +3. Supply a `sink` if the default would allocate, and make the group's rows + symmetric under the rule above. +4. 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. +5. New modules under `bench/` need no `./scripts/update-lib.sh` run; that script + globs `CompPoly/*.lean` only, and the lakefile globs `CompPolyBench` + submodules. + +## Known gaps + +Recorded so they are not rediscovered. The audit and plan live in +`BENCHMARKING.md` at the repo root. + +- 67 rows are still `n=1`, all of them workloads whose single iteration exhausts + its budget. They need smaller input shapes, decided per benchmark. +- Iteration counts are still hand-tuned `selectNat` triples rather than wall-clock + budgets, so `Total` is not comparable between rows of one table. +- 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. +- Coverage gaps against the roadmap: no standalone multiplicative NTT/iNTT group, + no base-field microbenchmarks outside Goldilocks, no `add`/`square`/batch-inverse, + no Reed-Solomon or polynomial-matrix groups. +- The polynomial-basis `GF(2^64)` of `CompPoly/Fields/Binary/BF64/` and its cubic + extension have no group, so the only binary-field timings are the tower ones. + A `mul` group there would measure carry-less multiply plus sparse reduction + against the tower's packed-word path, which is the comparison the two + representations exist to settle. diff --git a/docs/wiki/generated-files.md b/docs/wiki/generated-files.md index dd5d2866..3d69b5dc 100644 --- a/docs/wiki/generated-files.md +++ b/docs/wiki/generated-files.md @@ -9,7 +9,7 @@ This page records which paths are source of truth and which are derived outputs. | `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`. | | `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/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. | +| `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. | @@ -63,7 +63,7 @@ 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` and `bench/out/report-*.md` 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 1d178b47..14911323 100644 --- a/docs/wiki/quickstart.md +++ b/docs/wiki/quickstart.md @@ -96,20 +96,27 @@ 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`, then the axiom sweep - as an enforcing gate, 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. - `BENCH_CI_GROUPS` selection, then uploads benchmark reports as CI artifacts. + 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 collecting timings, and 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). @@ -128,8 +135,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 diff --git a/tests/CompPolyTests.lean b/tests/CompPolyTests.lean index 7a4880d7..536d6e51 100644 --- a/tests/CompPolyTests.lean +++ b/tests/CompPolyTests.lean @@ -32,6 +32,7 @@ public import CompPolyTests.Fields.BabyBear.Fast 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.Common public import CompPolyTests.Fields.Binary.Tower.Algebra public import CompPolyTests.Fields.Binary.Tower.Fast public import CompPolyTests.Fields.Extension.Arithmetic 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) From 4ce69b905b618ad147e4d3a27ee0a841263d49a0 Mon Sep 17 00:00:00 2001 From: Derek Sorensen Date: Thu, 10 Sep 2026 13:13:11 +0100 Subject: [PATCH 22/45] Size benchmarks from a wall-clock budget instead of 228 hand-tuned counts (#337) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(bench): add wall-clock measurement budgets A geometric calibration ramp and the sizing arithmetic that will replace the 229 hand-tuned `selectNat` iteration counts. Nothing calls it yet, so this commit cannot change a reported number. The ramp doubles as warmup and threads the sink accumulator, so it is no more eliminable than the timed samples are. Its cost estimate comes from the last step alone: the early steps run cold, and an estimate biased high sizes samples short, which inflates the dispersion the suite exists to report. `planFromCalibration` carries two ceilings rather than one. A sample is sized to `sampleNanos`, which is deliberately preset-independent -- a sample is a mean over its iterations, so varying it by preset would make `--small` and `--large` report structurally different spread for identical code. A separate `measureNanos` total is what lets the workloads costing seconds per iteration be replicated at all; without it they would sit at one sample forever. Nine `#guard`s pin the sizing at 1.5 ns, at 1 ms, at 13 s under both a 60 s and a 2 s total, and at a zero cost estimate, which must neither divide by zero nor collapse the sample count. * refactor(bench): give runTimed a spec record, keeping the positional form `runTimed` took five consecutive `String` arguments across 228 call sites, where a transposed pair is a silent mislabelling rather than a type error. `runTimedSpec` takes them as a `BenchSpec` record instead. The three `α`-dependent arguments stay outside the record. Giving `BenchSpec` a type parameter so it could 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. The positional `runTimed` survives as a wrapper so the migration of the call sites is a separate commit that provably changes nothing. It goes away once they have all moved. Verified: `--validate-only` output is byte-identical to the previous commit at all three presets, over all 286 rows. * refactor(bench): move all 228 call sites onto the spec record Mechanical. Each site's five label strings move into a `BenchSpec` literal and every expression is carried over byte-identical; `digestIterations` takes whatever the site passed as `checksumIterations`, or the old default `min validationIterationCap measured` where it relied on it. No behaviour changes here, which is the point of keeping it separate: the whole edit is proved by diffing the non-timing columns of a full run. Two shapes needed care. `Univariate/Roots/FiniteField.lean` passed the digest count as a tenth positional argument rather than by name. `Harness/SelfCheck.lean` is the only user of `forceTiming`, which is now a spec field. Verified: `--validate-only` at `--small` is byte-identical to the previous commit over all 286 rows, and the build is warning-clean with no line over the 100-column limit. * refactor(bench): make digests the body's period, not the preset's count `checksumIterations` was `min 256 measured`, so a group's digest varied with the preset: 195 of the 282 rows shared across presets carried three different values. Under budget-driven sizing the same expression would make it vary with the *machine*, which turns committed digest fixtures from awkward into impossible. The rule is now that `digestIterations` is the **period of the body in its iteration index**, capped at 256. That is not a weaker check: iterations past one full cycle recompute a bit-identical result, so a `fun _ ↦ …` body folding its digest once sees exactly what folding it 256 times saw. `validationIterationCap` and `groupChecksumIterations` are replaced by `digestIterationCap` and `digestPeriod`, which takes a period rather than a list of iteration counts. Each group now names its own: fun _ ↦ … bodies digestPeriod 1 21 groups 32-point pools (univariate, multivariate, digestPeriod 13 groups multilinear, bivariate evaluation) 64-element pools (tower, extension, deflate) digestPeriod 5 groups 256-element pools (Goldilocks, Mont64x8) digestPeriod 4 groups The periods are single-sourced from what the runner already computes — `points.size`, `perturb.size`, `values.size` — or from a named constant where the literal was previously repeated at the call sites: `bivariatePointCount`, `multilinearPointCount`, `extPoolSize`. `multilinear`'s point helpers now take the raw iteration index and reduce internally, so the period has one home rather than twelve. Two sites are not periods. `harness-floor` and `harness-canary` are unbounded in `i` and are cross-checked against nothing, so they take a pinned `harnessDigestIterations`. And `guruswami-sudan-packed-filter` had `candidateCount := preset.selectNat 128 64 32` — an *input shape* wearing a budget's clothes, which no digest-length rule could have fixed. Pinned at 128; that group's `input_shape` column stops moving between presets too. Verified with `--validate-only` at all three presets over all groups: digests differing across presets 195 of 282 -> 0 of 284 columns that moved vs. b5fe1ad checksum, checksum_iterations, and input_shape on the two packed-filter rows columns that did not move warmup_iterations, measured_iterations, iters_per_sample, sample_count 203 of 284 rows changed digest at `--large`; the other 81 were already at their period (68 at 1, 13 at 256). The curated `--validate-only --medium` pass goes from 36.1 s to 33.0 s locally — a small saving, as §11.4.3 predicted, because the cost sits in rows that were already validated exactly once. * fix(bench): compare canary and floor per iteration, not by total The dead-code canary asserted `canary.totalNanos > 3 * floor.totalNanos`. That separates the two rows only while they run the same number of iterations, which is true today and stops being true one commit from now: sizing each row from a wall-clock budget equalises totals *by construction*. The assertion would then throw on every run, and the natural-looking repair — lowering `canaryFloorRatio` until it passes — would leave it passing vacuously forever, with the harness silently unable to detect that benchmark bodies are being optimised away. Compares `stats.medianPicos` instead, which is already per iteration and is what the ratio was always meant to express. Landing it before the sizing flip rather than after keeps the flip from having to be verified through a check that is throwing. A zero median on either row now throws separately: it means the clock could not resolve the loop, in which case the ratio cannot say anything either way. Observed ratios, so a future collapse shows up as a changed number rather than a threshold that happens still to pass: timed small 244x medium 160x large 353x --validate-only small 210x medium 390x large 776x Failure path re-checked as `docs/wiki/benchmarking.md` asks: with `canaryRounds := 0` the canary measures 1683 ps against a 2391 ps floor and `--validate-only --small` fails with the collapse message. * fix(bench): draw the root-search workload from the group's random stream The finite-field root group has been reporting its cost divided by `itersPerSample` since it was written. Its body was a closed term — `p` was a `let` bound to the nullary constant `nonlinearRootPolynomial`, and the root context is a constant too — so the whole computation was evaluated once and every later iteration in a sample got the cached array back. That is invisible while `itersPerSample` is 1, which is what the hand-tuned counts gave it at `--medium`, and it shows up as a clean division everywhere else. Measured at the counts in `HEAD~1`: row itersPerSample --medium --large ntt 1 -> 2 764 ms 376 ms (/2) nttfast 1 -> 6 232 ms 38.8 ms (/6) fast-naive 1 -> 3 484 ms 162 ms (/3) fast-ntt 1 -> 6 286 ms 47.6 ms (/6) fast-nttfast 3 -> 20 24.4 ms 3.66 ms (/6.7) The seeds are now offset by a `base` drawn from the group's own random stream, so the polynomial is a local built at run time rather than a constant the compiler can float out of the loop — the same shape every other group in the suite already has. Nothing about the workload moves: still degree 66, still `rootWorkloadDistinctRoots` distinct roots with one of them repeated. The digest period stays 1, so the `--validate-only` pass costs exactly what it did, which matters because this is the most expensive group on the blocking CI gate. Afterwards the medians hold still as `itersPerSample` goes from 1 to 20: ntt 721 ms -> 727 ms fast-ntt 276 ms -> 272 ms nttfast 229 ms -> 235 ms fast-nttfast 76.3 ms -> 73.9 ms fast-naive 450 ms -> 462 ms naive 2365 ms -> 2408 ms `fast-nttfast`'s real cost is 74 ms, not the 24 ms the suite has been reporting. All six rows still agree on one digest, and it is identical at all three presets. Found by the per-iteration median comparison that the sizing flip's verification calls for; landing it first so that comparison starts from honest numbers. * refactor(bench): size every benchmark from a wall-clock budget A preset now selects a `BenchBudget` — warmup nanoseconds, sample length, sample count, and a total ceiling per row — instead of an iteration count per benchmark. `runTimedSpec` calibrates each row against that budget with the geometric ramp added in `ae22c05`: the ramp doubles as warmup, its last step estimates the per-iteration cost, that estimate fixes `itersPerSample`, and `measureNanos` caps how many samples the row can afford. An iteration count was the wrong unit. It is not comparable between two rows of one table, it goes stale as the code it measures gets faster, and picking one for a new benchmark is guesswork that has to be redone on every machine — which is why 228 of them were written down by hand and then left alone. Deleted with the counts: `BenchPreset.selectNat`, the eleven `*WarmupIterations` / `*MeasuredIterations` helpers, `gsWarmupIterations`, `harnessMeasuredIterations`, `planSamples`, `targetSampleCount`, `warmThenTime`, `warmIterations`, the `MulBudgets` record, the `BenchPreset → Nat` parameters of the five generic runners, and the positional `runTimed` shim that existed only to make `b5fe1ad` mechanical. 236 budget `let`s go with them. `collectSamples` takes the ramp's accumulator instead of a warmup count, which is what keeps the ramp non-eliminable. Under `--validate-only` calibration does not run at all. A ramp on a thirteen-second body costs thirteen seconds, and `--validate-only` is the only benchmark step on the blocking CI path. Warmup and sample count become table columns rather than shared metadata lines. They were rendered with `matchingNat?`, which stops matching once two rows of a group are calibrated separately — the lines would have vanished from every report with no error. Only the digest length is still shared by construction. Verification, `--medium`, curated set, against `HEAD~1`: --validate-only at all three presets only the two forceTiming harness rows move; their digests are unchanged and the other 284 rows are byte-identical per-iteration medians 0 of 130 rows with >=5 samples fall outside 2x; the whole distribution is 0.878-1.086, median 1.006 calibration stability, 3 runs iters_per_sample within 1.004x (fast) and 1.009x (ZMod) on goldilocks-mul sample_count = 0 none sample_count = 1 2 rows, both flagged unreplicated rows with < 5 samples 22 -> 9 curated timed run 120.1 s -> 110.7 s The replication gain is the point: rows were under-sampled because a count was mis-tuned, and the harness can now tell the difference between "expensive" and "mis-tuned". What it does not fix is input shape — the rows still reading `n=1` have single iterations that genuinely exhaust the budget, and no harness change reaches that. One thing is lost deliberately: `measured_iterations` is no longer comparable across runs, since it depends on how fast the machine was during calibration. `Median` and `Spread` are the columns to compare. * feat(bench): put group identity in the JSONL and provenance in a manifest Two things the results file could not say. **Which group a row belongs to.** The key and the title lived only in the Markdown report, so a JSONL consumer had to reconstruct the grouping from row names. `group_key` and `group_title` are stamped in `flattenGroups` from `BenchGroup`, because `runTimedSpec` genuinely does not know — a row is built before it is placed in a group. The key comes from the same registry entry that `--list` and `bench/ci-groups.txt` validate against, via `BenchTask.fromGroupRunner`, so it cannot drift from the group it names. **What produced the numbers.** `bench/out/manifest-.json` records the commit, a dirty flag, the toolchain, the preset and the budget it resolved to, the seed, the selection, and the host. This matters more than it used to: `measured_iterations` was a written-down constant and is now a function of how fast the machine was during calibration, so the JSONL lost its one stable provenance signal in the previous commit. A timing from a dirty tree is not attributable to anything, which is why the flag is not optional. 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. Written for every run, `--validate-only` and `--markdown-only` included. Both workflows' artifact steps glob it, or it would never leave the runner. `renderMarkdown` now takes the hardware the manifest already collected instead of probing the host a second time. Verified with `--validate-only` at all three presets: `group_key` is populated on all 286 rows, and every pre-existing column is identical on 284 of them. The two that move are `harness-floor` and `harness-canary`, the only rows measured under `--validate-only`, and only in their calibrated iteration counts — 3.6%, which is what run-to-run calibration noise looks like. * docs(bench): record budget-driven sizing and the Radar deferral `BENCHMARKING.md` gains a §12.6 change-log entry in the voice of the existing ones: what the flip does, the two load-bearing design points (`sampleNanos` fixed across presets, `measureNanos` as a second ceiling), the three preparatory steps, and the three findings — the canary that would have inverted silently, the root group that had been dividing its cost by `itersPerSample` since it was written, and the two report lines that would have disappeared without an error. §11.6 now records Radar as deferred **by decision** rather than pending, so it is not rediscovered later as an oversight, and says why the regression gate stays blocked behind it. `docs/wiki/benchmarking.md` says how a row's size is now chosen and that `Iterations` stopped being comparable between runs, and "Adding a benchmark" gains the two rules a new row has to get right: `digestIterations` is the body's period in `i` and must never be preset- or machine-shaped, and the body must depend on `i` through a value built at run time, because a closed body is evaluated once and cached. --- .github/workflows/benchmarks.yml | 8 + .github/workflows/lean_action_ci.yml | 4 + BENCHMARKING.md | 88 +++ bench/CompPolyBench/Bivariate/Basic.lean | 134 ++--- bench/CompPolyBench/Bivariate/Factor.lean | 72 ++- .../Bivariate/GuruswamiSudan.lean | 350 ++++++------ .../Bivariate/GuruswamiSudan/Core.lean | 320 +++++------ .../GuruswamiSudan/ReceivedWord.lean | 419 +++++++------- .../Bivariate/GuruswamiSudan/Shared.lean | 7 - bench/CompPolyBench/Common.lean | 354 ++++++++---- .../Fields/Binary/AdditiveNTT/Impl.lean | 57 +- bench/CompPolyBench/Fields/Binary/Tower.lean | 29 +- bench/CompPolyBench/Fields/Extension.lean | 32 +- bench/CompPolyBench/Fields/Goldilocks.lean | 52 +- bench/CompPolyBench/Fields/Montgomery.lean | 42 +- bench/CompPolyBench/Harness/Budget.lean | 197 +++++++ bench/CompPolyBench/Harness/Sample.lean | 39 +- bench/CompPolyBench/Harness/SelfCheck.lean | 55 +- bench/CompPolyBench/Harness/Timer.lean | 30 +- bench/CompPolyBench/Multilinear/Basic.lean | 256 ++++----- .../Multivariate/CMvPolynomial.lean | 162 +++--- bench/CompPolyBench/Setup.lean | 7 +- bench/CompPolyBench/Univariate/Basic.lean | 399 +++++++------- bench/CompPolyBench/Univariate/BatchEval.lean | 521 +++++++++--------- bench/CompPolyBench/Univariate/ManyEval.lean | 57 +- .../CompPolyBench/Univariate/NTT/FastMul.lean | 131 ++--- .../Univariate/NTT/FastMulLow.lean | 102 ++-- .../Univariate/Roots/FiniteField.lean | 111 ++-- bench/README.md | 43 +- docs/wiki/benchmarking.md | 42 +- docs/wiki/generated-files.md | 3 +- 31 files changed, 2250 insertions(+), 1873 deletions(-) create mode 100644 bench/CompPolyBench/Harness/Budget.lean diff --git a/.github/workflows/benchmarks.yml b/.github/workflows/benchmarks.yml index 84a93c27..a037b0f9 100644 --- a/.github/workflows/benchmarks.yml +++ b/.github/workflows/benchmarks.yml @@ -181,9 +181,17 @@ jobs: 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 diff --git a/.github/workflows/lean_action_ci.yml b/.github/workflows/lean_action_ci.yml index 18725d5d..bfbf965e 100644 --- a/.github/workflows/lean_action_ci.yml +++ b/.github/workflows/lean_action_ci.yml @@ -197,9 +197,13 @@ jobs: mkdir -p "$EVALUATION_BENCH_ARTIFACT_DIR" 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 diff --git a/BENCHMARKING.md b/BENCHMARKING.md index 9de5343a..86bccd54 100644 --- a/BENCHMARKING.md +++ b/BENCHMARKING.md @@ -1272,6 +1272,17 @@ coverage work (§6.6, §7 Phase 4), the instruction-count track (§6.5, §7 Phas 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 @@ -1308,6 +1319,7 @@ reviews as a small diff and the stack merges bottom-up. Base of the stack is | 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`) @@ -1682,3 +1694,79 @@ 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. diff --git a/bench/CompPolyBench/Bivariate/Basic.lean b/bench/CompPolyBench/Bivariate/Basic.lean index 77aed53a..4ca2d887 100644 --- a/bench/CompPolyBench/Bivariate/Basic.lean +++ b/bench/CompPolyBench/Bivariate/Basic.lean @@ -19,9 +19,15 @@ open CompPoly namespace CompPolyBench +/-- 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*} @@ -38,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 ++ ")", @@ -90,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)", @@ -165,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 := [ diff --git a/bench/CompPolyBench/Bivariate/Factor.lean b/bench/CompPolyBench/Bivariate/Factor.lean index eb1361e0..8a185ee4 100644 --- a/bench/CompPolyBench/Bivariate/Factor.lean +++ b/bench/CompPolyBench/Bivariate/Factor.lean @@ -67,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 @@ -75,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 ++ ")", @@ -102,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 @@ -110,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 ++ ")", @@ -139,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 f9fcc6db..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,99 +96,83 @@ 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 approximantMeasured := preset.selectNat 60 9 2 - let hybridMeasured := preset.selectNat 90 13 3 - let fastApproximantMeasured := preset.selectNat 300 45 10 - let fastHybridMeasured := preset.selectNat 400 60 10 - let checksumIterations := groupChecksumIterations denseMeasured [ - leeDirectMeasured, leeSubproductMeasured, fastDenseMeasured, - fastLeeDirectMeasured, fastLeeSubproductMeasured, - approximantMeasured, hybridMeasured, - fastApproximantMeasured, fastHybridMeasured - ] - 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 <- runTimed - "guruswami-sudan-interp-approximant-small" "CBivariate" - "Approximant basis (PM-Basis)" - "KoalaBear.Field" gsSmallInterpInputShape preset warmup approximantMeasured + 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) - checksumIterations - let hybridRow <- runTimed - "guruswami-sudan-interp-hybrid-small" "CBivariate" - "Hybrid (budgeted Lee-O'Sullivan with approximant fallback)" - "KoalaBear.Field" gsSmallInterpInputShape preset warmup hybridMeasured + 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) - checksumIterations - let fastApproximantRow <- runTimed - "guruswami-sudan-interp-approximant-small-fast" "CBivariate" - "Approximant basis (PM-Basis)" - "KoalaBear.Fast.Field" gsSmallInterpInputShape preset warmup - fastApproximantMeasured + 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) - checksumIterations - let fastHybridRow <- runTimed - "guruswami-sudan-interp-hybrid-small-fast" "CBivariate" - "Hybrid (budgeted Lee-O'Sullivan with approximant fallback)" - "KoalaBear.Fast.Field" gsSmallInterpInputShape preset warmup fastHybridMeasured + 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) - checksumIterations pure ({ groupKey := "guruswami-sudan-interp-small-koalabear", title := "Guruswami-Sudan interpolation, small (KoalaBear)", @@ -215,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)", @@ -293,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 ↦ @@ -301,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 5c86a5d2..306b2922 100644 --- a/bench/CompPolyBench/Common.lean +++ b/bench/CompPolyBench/Common.lean @@ -6,7 +6,7 @@ Authors: Valerii Huhnin module public import Init.Data.Random -public import CompPolyBench.Harness.Sample +public import CompPolyBench.Harness.Budget public import Lean.Data.Json.Parser public import Lean.Data.Json.Printer public import Std.Time @@ -47,76 +47,17 @@ 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 @@ -124,6 +65,15 @@ instance : Fact (Nat.Prime KoalaBear.fieldSize) where /-- 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 := "" name : String representation : String method : String @@ -328,6 +278,10 @@ def resultsPath (runId : String) : System.FilePath := def reportPath (runId : String) : System.FilePath := 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 := let trimmed := s.trimAscii.toString @@ -618,63 +572,107 @@ that its fast counterpart does not. -/ @[inline] def sinkZMod {modulus : Nat} (x : ZMod modulus) : UInt64 := natSink (ZMod.val x) -/-- Ceiling on validation-pass iterations. - -The validation 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 above every benchmark's operand-pool size, so the oracle still sees -every input it did before. -/ -def validationIterationCap : Nat := 256 +/-- 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. -/-- Compute the checksum iteration count shared by a benchmark group. -/ -def groupChecksumIterations (first : Nat) (rest : List Nat) : Nat := - min validationIterationCap (rest.foldl Nat.min first) + Must be the body's period in `i`, never preset-shaped: see `digestPeriod`. -/ + digestIterations : Nat + /-- 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 strong `Nat` checksum runs *before* timing, over `checksumIterations` — the -minimum measured-iteration count used by the records in the surrounding group — -and is what the group agreement check compares. +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. -Under `--validate-only` no samples are collected and the record carries digests -alone. `forceTiming` opts out of that, for the harness self-check, whose canary -compares timed totals and would pass vacuously against a zero floor. +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. -/ -@[specialize] def runTimed (name representation method field inputShape : String) - (preset : BenchPreset) (warmup measured : Nat) (run : Nat → α) (checksum : α → Nat) - (checksumIterations : Nat := min validationIterationCap measured) - (sink : α → UInt64 := fun x ↦ natSink (checksum x)) - (forceTiming : Bool := false) : IO BenchRecord := do +@[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 validateOnly := (← validateOnlyRef.get) && !forceTiming + 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 - planSamples measured - -- The validation pass above already executed the body, so it counts towards - -- reaching steady state. For an expensive workload validated once, this is the - -- difference between running it three times and running it twice. - let desiredWarmup := if validateOnly then 0 else max warmup plan.itersPerSample - let sampled ← collectSamples (desiredWarmup - min desiredWarmup checksumIterations) plan body - let total := sampled.totalNanos + 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 := desiredWarmup - checksumIterations := checksumIterations + field := spec.field + inputShape := spec.inputShape + warmupIterations := calibration.iterations + checksumIterations := spec.digestIterations measuredIterations := sampled.totalIterations - totalNanos := total + totalNanos := sampled.totalNanos averageNanos := sampled.stats.medianPicos / 1000 checksum := validationChecksum sinkDigest := sampled.sink @@ -690,9 +688,130 @@ 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. + +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 := @@ -701,6 +820,8 @@ def jsonString (s : String) : String := /-- 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, @@ -986,12 +1107,19 @@ def renderSpread (record : BenchRecord) : String := let base := if stats.unreplicated then base ++ " (n=" ++ toString stats.count ++ ")" else base if stats.severeOutliers > 0 then base ++ " !" ++ toString stats.severeOutliers else base -/-- Columns rendered in a group result table after shared metadata is lifted out. -/ +/-- 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 ↦ formatNanosInUnitOrAuto totalUnit r.totalNanos), ("Median (" ++ avgUnit.label ++ ")", true, fun r ↦ @@ -1005,9 +1133,7 @@ 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), - renderSharedNatLine "Samples" records (fun r ↦ r.stats.count) + renderSharedNatLine "Checksum iterations" records (fun r ↦ r.checksumIterations) ] ++ [ "- Total group time: `" ++ formatNanosWithUnit totalUnit (totalGroupNanos records) ++ "`", diff --git a/bench/CompPolyBench/Fields/Binary/AdditiveNTT/Impl.lean b/bench/CompPolyBench/Fields/Binary/AdditiveNTT/Impl.lean index 79f0a1e8..b6fb8c20 100644 --- a/bench/CompPolyBench/Fields/Binary/AdditiveNTT/Impl.lean +++ b/bench/CompPolyBench/Fields/Binary/AdditiveNTT/Impl.lean @@ -109,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 ^ ℓ @@ -119,19 +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) - (sink := sinkBtf3Output) - 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) - (sink := sinkBtf3OutputArray) + 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}", @@ -140,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) @@ -149,12 +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)) - (sink := sinkConcreteBtfOutputArray) + 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}", @@ -163,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 := [ diff --git a/bench/CompPolyBench/Fields/Binary/Tower.lean b/bench/CompPolyBench/Fields/Binary/Tower.lean index ec2c397f..77fdd624 100644 --- a/bench/CompPolyBench/Fields/Binary/Tower.lean +++ b/bench/CompPolyBench/Fields/Binary/Tower.lean @@ -41,26 +41,23 @@ def checksumConcreteBt128 (x : ConcreteBTField 7) : Nat := @[specialize] private def runTowerGroup (groupKey title method : String) (concreteOp : ConcreteBTField 7 → ConcreteBTField 7 → ConcreteBTField 7) (fastOp : Fast.FastBT128 → Fast.FastBT128 → Fast.FastBT128) - (concreteBudget fastBudget : BenchPreset → Nat) (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 warmup := warmupIterations preset - let concreteMeasured := concreteBudget preset - let fastMeasured := fastBudget preset - let checksumIterations := groupChecksumIterations concreteMeasured [fastMeasured] - let concreteRecord ← runTimed "tower-bt128" "ConcreteBTField" - (method ++ " (ConcreteBTField)") "GF(2^128)" - towerShape preset warmup concreteMeasured - (fun i ↦ let (a, b) := concreteSample i; concreteOp a b) - checksumConcreteBt128 (checksumIterations := checksumIterations) - let fastRecord ← runTimed "tower-bt128-fast" "FastBT128" - (method ++ " (FastBT128)") "GF(2^128)" - towerShape preset warmup fastMeasured - (fun i ↦ let (a, b) := fastSample i; fastOp a b) - checksumFastBT128 (checksumIterations := checksumIterations) + 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) @@ -69,7 +66,6 @@ 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 - (fun p ↦ p.selectNat 1000 150 30) (fun p ↦ p.selectNat 2000000 300000 60000) preset gen /-- Run the GF(2^128) inversion benchmark. -/ @@ -77,7 +73,6 @@ 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) - (fun p ↦ p.selectNat 500 75 15) (fun p ↦ p.selectNat 500000 75000 15000) preset gen /-- Registry entries for the binary tower benchmarks. -/ diff --git a/bench/CompPolyBench/Fields/Extension.lean b/bench/CompPolyBench/Fields/Extension.lean index d86455e4..b0e54ff4 100644 --- a/bench/CompPolyBench/Fields/Extension.lean +++ b/bench/CompPolyBench/Fields/Extension.lean @@ -57,6 +57,9 @@ def checksumKoalaBearExt5 (x : KoalaBear.Ext5) : Nat := def checksumKoalaBearExt6 (x : KoalaBear.Ext6) : Nat := x.coeffs.toArray.foldl (fun acc z ↦ acc + z.val) 0 +/-- 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. @@ -64,10 +67,11 @@ 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. -/ @@ -75,8 +79,8 @@ private def extSampler {F : Type*} [Field F] [Fintype 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) : @@ -86,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) : @@ -97,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) : @@ -107,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) : @@ -118,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) : @@ -128,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) : @@ -138,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) : @@ -148,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) : @@ -158,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/Goldilocks.lean b/bench/CompPolyBench/Fields/Goldilocks.lean index 7e90bd5a..31d5eb34 100644 --- a/bench/CompPolyBench/Fields/Goldilocks.lean +++ b/bench/CompPolyBench/Fields/Goldilocks.lean @@ -32,20 +32,22 @@ private def runGoldilocksMul (preset : BenchPreset) (gen : StdGen) : IO (BenchGroup × StdGen) := do let (values, gen) := (zmodArray Goldilocks.fieldSize 256 false).run gen let fastValues := goldilocksFastArray values - let warmup := warmupIterations preset - let zmodMeasured := preset.selectNat 200000 30000 6000 - let fastMeasured := preset.selectNat 200000 30000 6000 - let checksumIterations := groupChecksumIterations zmodMeasured [fastMeasured] - let zmodRecord ← runTimed "goldilocks-mul-zmod" "ZMod" "mul" "Goldilocks.Field" - goldilocksShape preset warmup zmodMeasured - (fun i ↦ values.getD (i % values.size) 1 * values.getD ((i + 1) % values.size) 1) - checksumZMod (checksumIterations := checksumIterations) (sink := sinkZMod) - let fastRecord ← runTimed "goldilocks-mul-fast" "UInt64" "mul" "Goldilocks.Fast.Field" - goldilocksShape preset warmup fastMeasured + let checksumIterations := digestPeriod values.size + let zmodRecord ← runTimedSpec + { name := "goldilocks-mul-zmod", representation := "ZMod", method := "mul", + field := "Goldilocks.Field", inputShape := goldilocksShape, + digestIterations := checksumIterations } + preset + (fun i ↦ values.getD (i % values.size) 1 * values.getD ((i + 1) % values.size) 1) checksumZMod + (sink := sinkZMod) + let fastRecord ← runTimedSpec + { name := "goldilocks-mul-fast", representation := "UInt64", method := "mul", + field := "Goldilocks.Fast.Field", inputShape := goldilocksShape, + digestIterations := checksumIterations } + preset (fun i ↦ fastValues.getD (i % fastValues.size) 1 * fastValues.getD ((i + 1) % fastValues.size) 1) - checksumGoldilocksFast (checksumIterations := checksumIterations) - (sink := sinkGoldilocksFast) + checksumGoldilocksFast (sink := sinkGoldilocksFast) pure ({ groupKey := "fields-goldilocks-mul", title := "Goldilocks multiplication", records := #[zmodRecord, fastRecord] }, gen) @@ -54,19 +56,19 @@ private def runGoldilocksInv (preset : BenchPreset) (gen : StdGen) : IO (BenchGroup × StdGen) := do let (values, gen) := (zmodArray Goldilocks.fieldSize 256 false).run gen let fastValues := goldilocksFastArray values - let warmup := warmupIterations preset - let zmodMeasured := preset.selectNat 20000 3000 600 - let fastMeasured := preset.selectNat 24000 3600 720 - let checksumIterations := groupChecksumIterations zmodMeasured [fastMeasured] - let zmodRecord ← runTimed "goldilocks-inv-zmod" "ZMod" "inv" "Goldilocks.Field" - goldilocksShape preset warmup zmodMeasured - (fun i ↦ (values.getD (i % values.size) 1)⁻¹) - checksumZMod (checksumIterations := checksumIterations) (sink := sinkZMod) - let fastRecord ← runTimed "goldilocks-inv-fast" "UInt64" "inv (Fermat chain)" - "Goldilocks.Fast.Field" goldilocksShape preset warmup fastMeasured - (fun i ↦ (fastValues.getD (i % fastValues.size) 1)⁻¹) - checksumGoldilocksFast (checksumIterations := checksumIterations) - (sink := sinkGoldilocksFast) + let checksumIterations := digestPeriod values.size + let zmodRecord ← runTimedSpec + { name := "goldilocks-inv-zmod", representation := "ZMod", method := "inv", + field := "Goldilocks.Field", inputShape := goldilocksShape, + digestIterations := checksumIterations } + preset (fun i ↦ (values.getD (i % values.size) 1)⁻¹) checksumZMod + (sink := sinkZMod) + let fastRecord ← runTimedSpec + { name := "goldilocks-inv-fast", representation := "UInt64", method := "inv (Fermat chain)", + field := "Goldilocks.Fast.Field", inputShape := goldilocksShape, + digestIterations := checksumIterations } + preset (fun i ↦ (fastValues.getD (i % fastValues.size) 1)⁻¹) + checksumGoldilocksFast (sink := sinkGoldilocksFast) pure ({ groupKey := "fields-goldilocks-inv", title := "Goldilocks inversion", records := #[zmodRecord, fastRecord] }, gen) diff --git a/bench/CompPolyBench/Fields/Montgomery.lean b/bench/CompPolyBench/Fields/Montgomery.lean index b0dae043..6c9ff10b 100644 --- a/bench/CompPolyBench/Fields/Montgomery.lean +++ b/bench/CompPolyBench/Fields/Montgomery.lean @@ -31,27 +31,24 @@ private def scalarInvShape : String := "256 random elements" /-- 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) @@ -60,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/Sample.lean b/bench/CompPolyBench/Harness/Sample.lean index 98b205a0..0667e8bc 100644 --- a/bench/CompPolyBench/Harness/Sample.lean +++ b/bench/CompPolyBench/Harness/Sample.lean @@ -13,11 +13,10 @@ public import CompPolyBench.Harness.Timer Collecting a benchmark's cost as a *set* of samples rather than one total. -The suite's per-benchmark iteration counts are treated as a total-work budget: -rather than timing all of them in one region and dividing, the budget is split -into `targetSampleCount` samples so the spread between them is visible. 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 +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. -/ @@ -33,22 +32,6 @@ structure SamplingPlan where sampleCount : Nat deriving Inhabited -/-- Samples aimed for when the iteration budget allows it. -/ -def targetSampleCount : Nat := 20 - -/-- Divide a total iteration budget into samples. - -Where the budget allows at least `targetSampleCount` iterations the budget is -split evenly. Below that each iteration becomes its own sample, which keeps as -much replication as the budget can pay for. -/ -def planSamples (totalIterations : Nat) : SamplingPlan := - if totalIterations = 0 then - { itersPerSample := 0, sampleCount := 0 } - else if totalIterations ≤ targetSampleCount then - { itersPerSample := 1, sampleCount := totalIterations } - else - { itersPerSample := totalIterations / targetSampleCount, sampleCount := targetSampleCount } - /-- 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 @@ -67,15 +50,15 @@ structure SampledRun where sink : UInt64 deriving Inhabited -/-- Warm a benchmark body, then collect `plan.sampleCount` timed samples of it. +/-- Collect `plan.sampleCount` timed samples of a benchmark body. -`warmup` is the number of *residual* warmup iterations to run; the caller is -expected to have already discounted any pass that executed the body beforehand. -Every sample replays the same iteration indices, so samples differ only in -machine state and not in the work performed. -/ -@[specialize] def collectSamples (warmup : Nat) (plan : SamplingPlan) +`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 ← warmIterations warmup 0 body + let mut acc := init let mut samples : Array Nat := Array.emptyWithCapacity plan.sampleCount let mut totalNanos := 0 for _ in [0:plan.sampleCount] do diff --git a/bench/CompPolyBench/Harness/SelfCheck.lean b/bench/CompPolyBench/Harness/SelfCheck.lean index 1cc513e0..ea0a9f9a 100644 --- a/bench/CompPolyBench/Harness/SelfCheck.lean +++ b/bench/CompPolyBench/Harness/SelfCheck.lean @@ -49,35 +49,52 @@ constant-folded. -/ | n + 1 => go n (canaryRound acc) go canaryRounds x -/-- Least multiple by which the canary must exceed the floor. +/-- 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. -/ +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 -/-- Measured iterations for the self-check benchmarks. -/ -private def harnessMeasuredIterations (preset : BenchPreset) : Nat := - preset.selectNat 2000000 500000 100000 +/-- 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 /-- Time the harness floor and the canary, and reject a collapsed canary. -/ private def runHarnessSelfCheck (preset : BenchPreset) (selection : BenchSelection) (gen : StdGen) : IO (Array BenchGroup × StdGen) := do - let measured := harnessMeasuredIterations preset - let warmup := measured / 10 - let floorRecord ← runTimed "harness-floor" "UInt64" "empty body" - "none" "no input" preset warmup measured - (fun i ↦ i.toUInt64) (fun x ↦ x.toNat) (sink := u64Sink) (forceTiming := true) - let canaryRecord ← runTimed "harness-canary" "UInt64" s!"{canaryRounds} mixing rounds" - "none" "no input" preset warmup measured - (fun i ↦ canaryWork i.toUInt64) (fun x ↦ x.toNat) (sink := u64Sink) - (forceTiming := true) - if canaryRecord.totalNanos < canaryFloorRatio * floorRecord.totalNanos then + 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 {canaryRecord.totalNanos}ns " ++ - s!"vs floor {floorRecord.totalNanos}ns over {measured} iterations " ++ - s!"(expected at least {canaryFloorRatio}x). Benchmark bodies are being " ++ - "optimised away, so every measured time in this run is meaningless." + 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." let mut groups := #[] if selection.selects "harness-floor" then groups := groups.push diff --git a/bench/CompPolyBench/Harness/Timer.lean b/bench/CompPolyBench/Harness/Timer.lean index 2db900da..87f65e20 100644 --- a/bench/CompPolyBench/Harness/Timer.lean +++ b/bench/CompPolyBench/Harness/Timer.lean @@ -24,9 +24,9 @@ namespace CompPolyBench /-- Whether this process is running in validation-only mode. -Set once from the command line rather than threaded through `runTimed`, whose -226 call sites all pass their arguments positionally. Read by `runTimed`, which -skips sample collection entirely when it is set. -/ +Set once from the command line rather than carried on every `BenchSpec`. Read by +`runTimedSpec`, which skips calibration and sample collection entirely when it is +set. -/ initialize validateOnlyRef : IO.Ref Bool ← IO.mkRef false /-- Elapsed time for one timed sample, with the sink accumulator it produced. -/ @@ -37,23 +37,11 @@ structure TimedSample where sink : UInt64 deriving Inhabited -/-- Run a benchmark body `iters` times without timing it, returning the sink -accumulator so the loop has an observable result. - -The accumulator must be fed into the subsequent timed run; discarding it would -leave the warmup loop eliminable, which is how warmup came to be a no-op before. -/ -@[inline] def warmIterations (iters : Nat) (init : UInt64) - (body : Nat → UInt64 → UInt64) : IO UInt64 := do - let mut acc := init - for i in [0:iters] do - acc := body i acc - pure acc - /-- Run a benchmark body `iters` times and return the elapsed nanoseconds. -`init` seeds the sink accumulator, normally from `warmIterations`. The -accumulator is bound and returned before the closing clock read, so the loop is -sequenced inside the timed region. -/ +`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 @@ -64,10 +52,4 @@ sequenced inside the timed region. -/ let stop ← IO.monoNanosNow pure { nanos := stop - start, sink := forced } -/-- Warm a benchmark body and then time it, in one step. -/ -@[inline] def warmThenTime (warmup measured : Nat) - (body : Nat → UInt64 → UInt64) : IO TimedSample := do - let warmed ← warmIterations warmup 0 body - timeIterations measured warmed body - end CompPolyBench diff --git a/bench/CompPolyBench/Multilinear/Basic.lean b/bench/CompPolyBench/Multilinear/Basic.lean index 960d417f..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 @@ -59,42 +65,38 @@ private def runKoalaBearMultilinearCoeff (preset : BenchPreset) (gen : StdGen) : 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)", @@ -107,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)", @@ -167,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, @@ -209,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)", @@ -238,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)", diff --git a/bench/CompPolyBench/Multivariate/CMvPolynomial.lean b/bench/CompPolyBench/Multivariate/CMvPolynomial.lean index 0303a854..c7b5d6a1 100644 --- a/bench/CompPolyBench/Multivariate/CMvPolynomial.lean +++ b/bench/CompPolyBench/Multivariate/CMvPolynomial.lean @@ -54,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 @@ -62,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 ++ ")", @@ -86,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 @@ -94,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 ++ ")", @@ -128,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)", @@ -178,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)", @@ -219,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 := [ diff --git a/bench/CompPolyBench/Setup.lean b/bench/CompPolyBench/Setup.lean index a11bbb13..c6942429 100644 --- a/bench/CompPolyBench/Setup.lean +++ b/bench/CompPolyBench/Setup.lean @@ -171,14 +171,17 @@ def runSelected (selection : BenchSelection) (output : BenchOutput) (preset : Be 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 if validateOnly then IO.FS.writeFile (reportPath runId) (renderValidationMarkdown preset groups) else - let hardware ← collectRunnerHardware - IO.FS.writeFile (reportPath runId) (renderMarkdown hardware preset groups) + IO.FS.writeFile (reportPath runId) (renderMarkdown manifest.hardware preset groups) IO.println <| if validateOnly then s!"validated {records.size} benchmark records in {groups.size} groups for run {runId}" diff --git a/bench/CompPolyBench/Univariate/Basic.lean b/bench/CompPolyBench/Univariate/Basic.lean index e2bf9656..86ffa8f9 100644 --- a/bench/CompPolyBench/Univariate/Basic.lean +++ b/bench/CompPolyBench/Univariate/Basic.lean @@ -25,27 +25,24 @@ namespace CompPolyBench /-- 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 ++ ")", @@ -61,7 +58,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 @@ -70,34 +66,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 ++ ")", @@ -111,8 +105,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) : @@ -121,8 +114,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) : @@ -133,36 +125,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)", @@ -205,81 +198,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)", @@ -325,71 +316,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)", @@ -403,7 +399,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 + "Goldilocks" preset gen /-- Convert BN254 field inputs to the native eight-limb representation. -/ private def bn254FastArray (xs : Array BN254.ScalarField) : Array BN254.Fast.ScalarField := @@ -420,8 +416,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 + preset gen /-- Convert BLS12-381 field inputs to the native eight-limb representation. -/ private def bls12_381FastArray (xs : Array BLS12_381.ScalarField) : @@ -440,8 +435,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 + preset gen /-- Convert BLS12-377 field inputs to the native eight-limb representation. -/ private def bls12_377FastArray (xs : Array BLS12_377.ScalarField) : @@ -460,8 +454,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 := [ diff --git a/bench/CompPolyBench/Univariate/BatchEval.lean b/bench/CompPolyBench/Univariate/BatchEval.lean index 25fa999a..b72e6720 100644 --- a/bench/CompPolyBench/Univariate/BatchEval.lean +++ b/bench/CompPolyBench/Univariate/BatchEval.lean @@ -72,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)", @@ -265,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)", @@ -423,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)", diff --git a/bench/CompPolyBench/Univariate/ManyEval.lean b/bench/CompPolyBench/Univariate/ManyEval.lean index 034e10b7..216e91d6 100644 --- a/bench/CompPolyBench/Univariate/ManyEval.lean +++ b/bench/CompPolyBench/Univariate/ManyEval.lean @@ -51,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)", diff --git a/bench/CompPolyBench/Univariate/NTT/FastMul.lean b/bench/CompPolyBench/Univariate/NTT/FastMul.lean index 3739ffa1..9fc4db5b 100644 --- a/bench/CompPolyBench/Univariate/NTT/FastMul.lean +++ b/bench/CompPolyBench/Univariate/NTT/FastMul.lean @@ -25,17 +25,6 @@ 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. -/ @@ -45,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 @@ -59,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 ++ ")", @@ -137,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. -/ @@ -154,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. -/ diff --git a/bench/CompPolyBench/Univariate/NTT/FastMulLow.lean b/bench/CompPolyBench/Univariate/NTT/FastMulLow.lean index 421e34d0..7153b296 100644 --- a/bench/CompPolyBench/Univariate/NTT/FastMulLow.lean +++ b/bench/CompPolyBench/Univariate/NTT/FastMulLow.lean @@ -47,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)", 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 e8e981b7..075f54c5 100644 --- a/bench/README.md +++ b/bench/README.md @@ -52,12 +52,19 @@ is created on demand and ignored in its entirety: ```text 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, capped at `validationIterationCap`. +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 @@ -96,12 +103,12 @@ univariate-dense-bls12-381 univariate-dense-bls12-377 ## How A Benchmark Is Measured -`runTimed` does two passes over each benchmark body. +`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 is capped at -`validationIterationCap` iterations — above every benchmark's operand-pool -size, so the oracle sees every input, without the pass costing as much as the +(`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. @@ -125,10 +132,18 @@ territory and the group's ratio is a lower bound on the real speedup. ### Sampling and dispersion -A benchmark's cost is collected as a *set* of samples, not one total. Each -benchmark's iteration count is treated as a total-work budget and split into up -to `targetSampleCount` timed samples; every sample replays the same iteration -indices, so samples differ only in machine state. +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: @@ -176,10 +191,10 @@ inputs do not depend on which other groups ran, or in what order. Concretely: 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 now also stable across runs is what makes them usable as regression -fixtures. Digests are still preset-dependent, because the validation pass runs -`min validationIterationCap` of the group's measured iteration count and that -count varies by preset. +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 diff --git a/docs/wiki/benchmarking.md b/docs/wiki/benchmarking.md index b38bad18..512e5557 100644 --- a/docs/wiki/benchmarking.md +++ b/docs/wiki/benchmarking.md @@ -68,9 +68,10 @@ 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 is capped at -`validationIterationCap` iterations and counts towards warmup, since it has -already executed the body. +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 @@ -112,6 +113,14 @@ 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 @@ -148,14 +157,26 @@ and ext6 groups. Any tool comparing two result files must key on 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. Give every implementation in the group the same `checksum`, so the agreement +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. 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. +4. Make the body depend on `i`, through a value built at run time. 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 for a group that did this for months. +5. Give every implementation in the group the same `checksum`, so the agreement check is meaningful. -3. Supply a `sink` if the default would allocate, and make the group's rows +6. Supply a `sink` if the default would allocate, and make the group's rows symmetric under the rule above. -4. Add the key to `bench/ci-groups.txt` to have it covered by the correctness +7. 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. -5. New modules under `bench/` need no `./scripts/update-lib.sh` run; that script +8. New modules under `bench/` need no `./scripts/update-lib.sh` run; that script globs `CompPoly/*.lean` only, and the lakefile globs `CompPolyBench` submodules. @@ -164,10 +185,9 @@ and ext6 groups. Any tool comparing two result files must key on Recorded so they are not rediscovered. The audit and plan live in `BENCHMARKING.md` at the repo root. -- 67 rows are still `n=1`, all of them workloads whose single iteration exhausts - its budget. They need smaller input shapes, decided per benchmark. -- Iteration counts are still hand-tuned `selectNat` triples rather than wall-clock - budgets, so `Total` is not comparable between rows of one table. +- 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 diff --git a/docs/wiki/generated-files.md b/docs/wiki/generated-files.md index 3d69b5dc..6190ade0 100644 --- a/docs/wiki/generated-files.md +++ b/docs/wiki/generated-files.md @@ -63,7 +63,8 @@ consistency check. ## Derived Build State - `.lake/` and `.lake/build/` are generated by Lake and CI. -- `bench/out/results-*.jsonl` and `bench/out/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. From 6eda6de8c19e3978594788f17e844015d25394d2 Mon Sep 17 00:00:00 2001 From: Derek Sorensen Date: Tue, 15 Sep 2026 11:13:58 +0100 Subject: [PATCH 23/45] Measure the operations, not the harness: base fields, transforms, and chained bodies (#338) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(bench): report work units, and name the median statistic correctly Two things a row could not say, both needed before any operation-chain or transform benchmark can be read. **`workUnits`.** A row that performs one operation per iteration reports a per-iteration median and that is the number. A row that chains a thousand multiplications, or transforms 2^16 points, does not: its per-iteration median has to be divided by the work it did. `BenchSpec` and `BenchRecord` gain `workUnits`, the JSONL gains `work_units`, and the group table gains a `Per unit (ps)` column. `workUnits` describes the **problem**, not the implementation, and the rows of a group must agree on it. A radix-4 plan and a radix-2 reference perform different butterfly counts for the same transform; letting each row divide by its own count would divide away precisely the algorithmic difference the group exists to show. So the column is gated on `matchingNat?` rather than on "some row set it", and disagreement inside a group now fails the run the way a digest mismatch does — it means the group was specified wrong, not that the table needs a special case. The column is picoseconds rather than a chosen unit because a per-unit figure is normally sub-nanosecond, which is exactly where `chooseTimeUnit` renders `0.000`. It is deliberately *not* mirrored into the JSONL: that file carries `work_units` and the full statistics, and a consumer dividing for itself does not inherit the integer truncation this column accepts to stay readable. **`average_nanos` was a median.** `averageNanos := stats.medianPicos / 1000` — the field has described the wrong statistic since it was introduced. Renamed to `medianNanos` / `median_nanos`. Checked first that nothing consumes the JSONL: both workflows glob the results files as artifacts and neither parses a field, and `scripts/build_timing_report.sh` is about build timing, a different file. `keepSome` is generalised from `Option String` to any type so the optional column can use it. Verified digest-neutral: `--validate-only` at all three presets, only the two `forceTiming` harness rows move and only in their calibrated iteration counts (3.8% and 5.9%, which is run-to-run calibration noise); digests unchanged and the other 284 rows byte-identical. * feat(bench): let one group carry several digest comparisons `checksumMismatchGroups` required *every* row of a group to share a digest. That is right when a group is two implementations of one operation, and wrong as soon as a group wants to hold more than one comparison: a field's `mul` and its `add` belong in the same table and can obviously not agree on a digest. The consequence was structural, not cosmetic. Base-field coverage is a matrix of (field x operation x chain shape), and one-comparison-per-group turns it into roughly 83 two-row groups — 83 more keys to maintain by hand in `bench/ci-groups.txt`, and about 1200 lines of report in which every table has two rows and one ratio. Rows now carry a `digestClass` and agreement is required *within* a class rather than across the group. `classesAgree` replaces the group-wide check; `renderChecksumStatus` and the validation table print one digest per class. Empty is a class like any other, so every existing group keeps exactly the behaviour it had. This takes the coverage work from ~83 new groups of 2 rows to ~10 of 12-20, and it has a benefit beyond the count: rows in one group share a single `genFor` operand draw, so per-operation ratios within a field become directly comparable instead of each being measured against a different random input. Verified digest-neutral at all three presets: every pre-existing group has one class, so every digest and every agreement verdict is unchanged. * feat(bench): measure an operation in a chain, not one operation per loop The suite cannot currently measure a field operation. `harness-floor` is 1.80 ns/iteration and `goldilocks-mul-fast` is 3.16 ns; the generated C shows why. The operand-pool idiom every group uses, `xs.getD (i % xs.size) unit`, compiles to two boxed-`Nat` modulos, two bounds checks and two boxed array reads around a single unboxed multiply. The operation is a rounding error in its own measurement. `Harness/Chain.lean` runs the operation `n` times per timed iteration and the row divides by `n` through `workUnits`, in the two shapes Plonky3 separates: `chainLatency` (dependent, one operation at a time) and `chainThroughput` (ten independent accumulators). Three properties are load-bearing, and each is there because the obvious alternative is measurably wrong: - **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. This is also why the chain lengths are not Plonky3's element counts — matching the count would mismatch the working set. - **No `for` with `let mut`.** `ForIn` threads one state value, so ten mutable locals become a nested `Prod`, which has two relevant fields and does not erase: nine allocations per round around ten multiplies. Both combinators are tail-recursive with scalar parameters instead. - **The inner block is unrolled.** The `Nat` counter costs a `lean_nat_sub`, a `lean_dec` and a `lean_nat_dec_eq` per round, several times a Montgomery multiply. The counter now runs blocks of 64. Verified in the emitted IR rather than assumed. In `.lake/build/ir/CompPolyBench/Harness/SelfCheck.c` the specialised loops take `(lean_object* n, uint64_t acc)` and ten `uint64_t` accumulators respectively, contain zero `lean_alloc_*`, and spend one counter triple per 64 operations (latency) and per 40 (throughput). Two self-check groups, the chain analogues of `harness-floor` and `harness-canary`. `harness-chain-floor` carries the cheapest honest operation in both shapes: 530 ps per operation dependent and 109 ps ten-wide on an M3 Max, against a two-cycle pair at about 247 ps per cycle. The chain machinery therefore costs nothing measurable on top of the operation itself. `harness-chain-linearity` fails the run unless eight times the rounds costs at least four times as much; it measures 8.6x. The floor operation must mix two algebras, which is not obvious and cost a measurement to learn. The natural choice, `x ^^^ (x >>> 7)`, is the `GF(2)`-linear map `I + S`, and in characteristic two `(I + S) ^ 64` is `I + S ^ 64` — a shift right by 448, so a 64-deep block of it is the identity. LLVM finds this. The row reported 15 ps per operation, a sixteenth of a cycle, and the linearity check passed anyway, because what collapses is each block and not the loop over blocks. A wrapping add carries between bits and does not commute with the shift that way. Digest-neutral: `--medium --validate-only` over all groups gives 290 records against 286, the four new rows are the four added here, and no existing checksum moves. * feat(bench): share the per-field input, checksum and sink helpers `Univariate/Basic.lean` carried six `private` helpers — a fast-array conversion and a checksum for each of BN254, BLS12-381 and BLS12-377 — that the Montgomery arithmetic groups need too. Moved to a new `CompPolyBench/Fields/Inputs.lean`, together with Mersenne31's generator, conversion and checksum, and a sink for the eight-limb Montgomery carrier that reads two limbs instead of reassembling a 256-bit bignum. Not in `CompPolyBench.Common`, which every benchmark imports, because the field modules behind these are needed by a handful. The split is by import cost. Digest-neutral: the three curve groups validate to the checksums they had before. Deliberately excluded, and this is the surprise: `GF(2^64)` in the polynomial basis. `BF64.instFintype` (`CompPoly/Fields/Binary/BF64/Impl.lean:391`) is `Fintype.ofEquiv _ equivFin.symm`, a closed constant of a type whose value is a `Finset` of all `2 ^ 64` elements. Lean evaluates closed constants at module initialisation, so **any executable importing that module hangs before `main` runs** — `--list` never prints, memory climbs past 2.5 GB, and a sample shows the whole stack inside `_init_lp_CompPoly_BF64_instFintype___closed__3 → List.finRange`. Elaboration never notices, because the interpreter forces constants on demand, which is why the tests build. The BF64 and `Ext3` groups wait on a fix to that instance. * feat(bench): measure mul, add, inv and pow over the four small prime fields `BENCHMARKING.md` §13 names Plonky3 as the peer for BabyBear, KoalaBear, Goldilocks and Mersenne31 arithmetic. The suite measured Goldilocks `mul` and `inv` and nothing else on that list, and measured those with the operand-pool body shape, which reports the harness rather than the field. Sixteen groups: `mul`, `add`, `inv` and `pow` over the four fields, canonical `ZMod` beside the verified native-word representation, cross-checked by the group digest. `mul` and `add` carry a latency row and a throughput row, split the way Plonky3 splits `benchmark_mul_latency` from `benchmark_mul_throughput`, in one group under two digest classes. `inv` and `pow` carry latency only, as the peer does, over a chain twenty times shorter because one operation is tens of multiplications. The chain is seeded from an operand pool indexed by the iteration counter. One boxed array read per 1280 operations is under a tenth of a percent, and it buys a body that genuinely depends on `i` — so it is neither cached as a closed term nor hoisted out of the sample loop — and a digest over 64 inputs rather than one. The operation reaches `chainLatency` as a direct argument of an `@[specialize]` runner, never through a structure field or a `[Field F]` dictionary. This is why the group runners are written out one per field and operation instead of generated: a generated one would capture the operation in a closure, and an indirect call per operation is more than the operation. Verified in the IR — every fast row's loop specialises to `uint32_t` or `uint64_t` with zero `lean_alloc_*`, and KoalaBear's Montgomery multiply appears as unboxed 32- and 64-bit arithmetic with no boxing at all. `fields-goldilocks-mul` and `fields-goldilocks-inv` keep their keys but are now chained, so their digests change; `Fields/Goldilocks.lean` folds into the shared runner and `add` and `pow` join it. On an M3 Max at `--small`, per operation: | | latency | throughput | |---|---:|---:| | KoalaBear `mul` (fast) | 3.24 ns | 0.54 ns | | BabyBear `mul` (fast) | 3.34 ns | 0.53 ns | | Mersenne31 `mul` (fast) | 2.22 ns | 0.42 ns | | Goldilocks `mul` (fast) | 3.31 ns | 0.68 ns | | KoalaBear `add` (fast) | 1.12 ns | 0.60 ns | Roughly 13 cycles of dependent latency against 2.2 cycles of throughput for a Montgomery multiply, which is the shape of the algorithm rather than of the harness: the chain floor is 0.53 ns for a two-cycle pair. Inversion is 139 ns on fast KoalaBear against 1.14 us canonical, and 410 ns on fast Goldilocks against 8.66 us. One asymmetry the coverage turned up and does not explain: canonical BabyBear `mul` is 32.0 ns per operation against 5.9 ns for canonical KoalaBear and Mersenne31, all three being `ZMod p` for a 31-bit prime. * perf(fields): give BabyBear the explicit Field instance KoalaBear already has `KoalaBear/Basic.lean:63` declares `instance : Field Field := ZMod.instField fieldSize`. `BabyBear/Basic.lean` did not, and left synthesis to find the path itself. The two fields are otherwise the same shape — `abbrev Field := ZMod fieldSize` for a 31-bit prime — so the new `fields-babybear-mul` group put them side by side and the gap was visible immediately: | canonical `mul`, per operation | before | after | |---|---:|---:| | BabyBear | 32.0 ns | 5.92 ns | | KoalaBear | 5.94 ns | 5.94 ns | | Mersenne31 | 6.02 ns | 6.02 ns | 5.4x, and it lands exactly on the two fields that already had the instance. This is the case `CLAUDE.md` describes under Performance Guidelines: prefer an explicit instance construction where the synthesis path is long. `lake build` is warning-clean, `lake test` passes, and `lake exe axiomsweep --check` reports no new taint. * feat(bench): measure the eight-limb Montgomery multiply `mul` over the BN254, BLS12-381 and BLS12-377 scalar fields, canonical `ZMod` beside the eight-limb Montgomery carrier, latency and throughput. Inversion over these carriers already has `fields-mont64x8-*-inv`, which compares three algorithms rather than two representations, so this adds multiplication only. A quarter-depth chain: 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 one iteration past the sample budget. Choosing a shorter depth exposed a footgun, now closed. `chainLatency` runs whole `unrollBlock`s and `chainThroughput` whole `throughputUnroll`s, so a depth that is not a multiple of one is silently rounded down — and a row that took `workUnits` from the depth it *asked* for would then divide by operations the machine never performed. `latencyUnits` and `throughputUnitsOf` compute what the chain actually does, and every row now takes `workUnits` from those. A mismatched pair fails the group's workUnits check instead of reporting a number. On an M3 Max at `--small`, per operation: the eight-limb multiply is 37.8 ns (BN254), 39.7 ns (BLS12-381) and 37.3 ns (BLS12-377) against 196-212 ns canonical, a little over 5x. Throughput barely beats latency — 35.7 ns against 37.8 ns for BN254 — because each operation allocates its eight-limb result, so the chain is bound by allocation rather than by the dependency it was built to expose. * feat(bench): sweep the multiplicative NTT on its own, over size `BENCHMARKING.md` §13 names the multiplicative NTT as one of the operations to measure against Plonky3, and the only way to see it here was as one term of `univariate-mul-*`. Ten groups sweep the forward and inverse transforms over `n = 2^8` to `2^16` on KoalaBear and BabyBear, reference radix-2 against the planned radix-4, in one group per size under two digest classes. `workUnits` is the radix-2 butterfly count, `n / 2 * log n`. It is a property of the problem: giving the radix-4 row its own smaller count would divide away the algorithmic advantage the group exists to show. Bit reversal stays out of the timed region. `Plan.forwardImpl` returns bit-reversed output and `Plan.inverseImpl` expects bit-reversed input, so the forward group puts the permutation in the planned row's checksum — used only in the untimed pass — with an explicit sink so the default cannot drag it back in, and the inverse group permutes that row's input once, before timing. The permutation is defined locally rather than imported from `NTT.Transform.bitRevPermute`, which the reference transform is itself built from: importing it would let one wrong `bitRevNat` produce two compensating errors and a group that agrees on a wrong digest. Every body reads its input from a two-entry pool indexed by the iteration counter, and that is load-bearing. The first version precomputed the 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, 1.6 million iterations per sample, identically at every size. The plan construction group had the matching failure from the other documented cause — `Plan.ofDomain d` for a literal `d` is a closed term, evaluated once and cached — and reported 32 ns for both of its sizes. On an M3 Max at `--small`, per butterfly: the planned transform is 3.4-5.6 ns and the reference 82-124 ns, a factor of about 25. The reference is why the sweep splits at `2^12`: one reference inverse is 2.5 ms there and 53 ms at `2^16`, past the sample budget, so above the cap the groups carry the planned rows alone and the cross-check lives at the sizes below. Plan construction is 58.7 us at `2^12` and 837 us at `2^16` — linear, and about half a transform at the larger size, which is the number a caller tempted to rebuild a plan per transform needs to see. * feat(bench): measure Reed-Solomon encoding and the schoolbook/NTT crossover Two groups the roadmap has wanted and the suite has never had. **Reed-Solomon encoding.** `ReedSolomon.encode` evaluates the message polynomial at every domain node by Horner, `Θ(n · k)`; `nttCodeword` is the forward NTT, `Θ(n log n)`. They are *equal*, not merely equivalent — `forwardImpl_eq_encode` — so the group digest checks the identity rather than only cross-checking two implementations of a shared spec. Rate one half, the FRI setting. At `n = 2^8` the definitional encoder is 887 us against 125 us; at `2^10`, 17.0 ms against 0.60 ms. The quadratic row is why the paired groups stop at `2^10`, with a third group carrying the NTT encoder alone at `2^14`. Worth noting from those numbers: `nttCodeword` is built on `Forward.forwardImpl`, the reference radix-2 transform, so it inherits the 25x the new `ntt-*` groups measure against the planned one. Routing the certified encoder through a `Plan` is a change to `CompPoly/`, not to `bench/`, so it is left alone here. **Crossover.** Six sizes from degree<4 to degree<1024, schoolbook against the planned NTT pipeline. The crossover falls between 8 and 16: schoolbook wins at degree<4 (3.05 us against 3.97 us) and degree<8 (7.89 against 8.10), and loses from degree<16 (18.1 against 16.6) upward, reaching 41.7 against 32.7 at degree<32. Read that with a caveat the per-unit column makes visible: both rows sit within a third of 1 us per coefficient at *every* size across a 256-fold range. An NTT is `k log k`, so a cost exactly linear in `k` over that range says the shared `CPolynomial` path — canonicalisation and allocation — sets the scale for both rows, and the crossover measured here is a property of the API rather than of the transforms underneath it. Both groups index a two-entry input pool by the iteration counter, for the reason recorded in `NTT/Transform.lean`. * chore(tests): delete the two orphaned `#eval` benchmarks Neither was imported by `tests/CompPolyTests.lean`, so neither ran under `lake test` or in CI, and neither carried a `#guard` or a theorem. They printed timings through `#eval` at elaboration time and were invoked by hand. `BENCHMARKING.md` §12.6 recorded a deliberate decision to keep `Univariate/NTT/Benchmark.lean`: it held the only NTT-vs-schoolbook crossover logic in the repo and was "the specification for a future crossover metric". That metric is now the `univariate-mul-crossover-*` groups, so the commitment is discharged and the file goes with it. `Bivariate/KroneckerBenchmark.lean` measured the Kronecker-backed multiply that `bivariate-full-*` already covers. The audit sections of `BENCHMARKING.md` describe the state they audited and are left saying so; only the backticked paths are adjusted, in the shape §1.3 already uses for `CommonBench.lean`, so `python3 ./scripts/check-docs-integrity.py` stays green. * feat(bench): measure the tower's table-driven kernels against its recursive ones `Tower/FastDefs.lean` carries `GF(2^8)` multiplication, `GF(2^64)` multiplication and `GF(2^64)` inversion twice — once as the recursive tower construction, once driven by a precomputed table — proves the two equal, and never measured which is faster, which is the only reason the table exists. All six are bare `UInt64` kernels, so they chain with nothing to unbox and nothing to allocate. Per operation on an M3 Max at `--small`: | | recursive | table | | |---|---:|---:|---:| | `GF(2^8)` mul, latency | 18.4 ns | 2.49 ns | 7.4x | | `GF(2^8)` mul, throughput | 5.76 ns | 0.31 ns | 18x | | `GF(2^64)` mul, latency | 169 ns | 15.7 ns | 10.8x | | `GF(2^64)` inv, latency | 304 ns | 44.2 ns | 6.9x | The `GF(2^8)` group needs its operands below `2 ^ 8` and the bound is not cosmetic: `mul8T_eq_mul8` (`Tower/Fast.lean:441`) holds only there, because `mul8T` indexes a 65536-entry table with `(a <<< 8) + b`. Fed full machine words the first version read out of range, `get!` returned zero, and the group reported both a digest mismatch and a table that looked 4x *slower* than the recursion at latency while 10x faster at throughput. The agreement check caught it; the timings alone would not have. `ChainRep` and the four chained-group runners lose their `private` so this file can use them. They stay in `Fields/Arith.lean` rather than moving beside `Harness/Chain.lean`, which deliberately does not import `runTimedSpec`. * docs(bench): record the coverage work, and the two ways a body goes missing `bench/README.md` gains the group inventory for the new families, the per-unit column and the two chain shapes, a warning that a per-unit number is not comparable with `harness-floor`, and the chain half of the self-check. `docs/wiki/benchmarking.md` gains a "Chained bodies" section — the three load-bearing properties of the combinators and the two call-site rules that are easy to get wrong — and splits the "make the body depend on `i`" step into its two distinct failure modes, closed-term caching and loop-invariance, each with the group here that hit it. Its known-gaps list loses what this closes and gains the `BF64.instFintype` blocker in full, so nobody spends an afternoon rediscovering that importing that module hangs the executable before `main`. `BENCHMARKING.md` gains §12.7. `ROADMAP.md` item 6 marks operation-level coverage done and narrows what remains to baselines and the external comparison. `bench/ci-groups.txt` grows from 44 keys to 59: every new family is represented, and where a family is a sweep, the middle of it stands in for the rest — a body that is wrong at one size is wrong at all of them. The gate got cheaper anyway, ~34s to ~29s of CPU, because the BabyBear `Field` instance that the coverage turned up speeds up every canonical BabyBear row in the set. Two labels the new report made wrong are fixed while here: the canonical inverse rows said "inv (Fermat chain)" when `ZMod.inv` is extended Euclid, and the two binary-tower rows rendered identically because they shared a field label as well as a method. --- BENCHMARKING.md | 91 ++- CompPoly/Fields/BabyBear/Basic.lean | 2 + ROADMAP.md | 10 +- bench/CompPolyBench/Common.lean | 129 ++++- bench/CompPolyBench/Fields/Arith.lean | 531 ++++++++++++++++++ .../Fields/Binary/Tower/Scalar.lean | 108 ++++ bench/CompPolyBench/Fields/Goldilocks.lean | 85 --- bench/CompPolyBench/Fields/Inputs.lean | 107 ++++ bench/CompPolyBench/Harness/Chain.lean | 199 +++++++ bench/CompPolyBench/Harness/SelfCheck.lean | 108 +++- bench/CompPolyBench/Setup.lean | 23 +- bench/CompPolyBench/Univariate.lean | 6 +- bench/CompPolyBench/Univariate/Basic.lean | 30 +- .../Univariate/NTT/Crossover.lean | 98 ++++ .../Univariate/NTT/Transform.lean | 229 ++++++++ .../CompPolyBench/Univariate/ReedSolomon.lean | 93 +++ bench/README.md | 45 +- bench/ci-groups.txt | 25 +- docs/wiki/benchmarking.md | 88 ++- tests/CompPolyTests/Bivariate/Kronecker.lean | 2 +- .../Bivariate/KroneckerBenchmark.lean | 104 ---- .../Univariate/NTT/Benchmark.lean | 112 ---- 22 files changed, 1826 insertions(+), 399 deletions(-) create mode 100644 bench/CompPolyBench/Fields/Arith.lean create mode 100644 bench/CompPolyBench/Fields/Binary/Tower/Scalar.lean delete mode 100644 bench/CompPolyBench/Fields/Goldilocks.lean create mode 100644 bench/CompPolyBench/Fields/Inputs.lean create mode 100644 bench/CompPolyBench/Harness/Chain.lean create mode 100644 bench/CompPolyBench/Univariate/NTT/Crossover.lean create mode 100644 bench/CompPolyBench/Univariate/NTT/Transform.lean create mode 100644 bench/CompPolyBench/Univariate/ReedSolomon.lean delete mode 100644 tests/CompPolyTests/Bivariate/KroneckerBenchmark.lean delete mode 100644 tests/CompPolyTests/Univariate/NTT/Benchmark.lean diff --git a/BENCHMARKING.md b/BENCHMARKING.md index 86bccd54..cfaef5ce 100644 --- a/BENCHMARKING.md +++ b/BENCHMARKING.md @@ -127,16 +127,18 @@ report into `$GITHUB_STEP_SUMMARY`. Three files measure timings at *elaboration* time via `#eval`: -- `tests/CompPolyTests/Univariate/NTT/Benchmark.lean` — NTT-vs-naive crossover - sweep over 20 operand sizes, `IO.monoMsNow`. -- `tests/CompPolyTests/Bivariate/KroneckerBenchmark.lean` +- `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 is imported by `tests/CompPolyTests.lean`, so none runs under `lake test` -or in CI. Each documents its own manual invocation (`lake build -CompPolyTests.Bivariate.KroneckerBenchmark`). +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`) @@ -1102,7 +1104,7 @@ anywhere in this document. | 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 | `tests/CompPolyTests/Univariate/NTT/Benchmark.lean`, `tests/CompPolyTests/Bivariate/KroneckerBenchmark.lean` (a third, `CommonBench.lean`, was removed in 12.4) | +| Orphaned `#eval` benchmarks | none remain; `Benchmark.lean` and `KroneckerBenchmark.lean` were removed in 12.7, and a third, `CommonBench.lean`, in 12.4 | --- @@ -1552,10 +1554,12 @@ 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. -`tests/CompPolyTests/Univariate/NTT/Benchmark.lean` and -`KroneckerBenchmark.lean` are deliberately left in place: the former holds the -only NTT-vs-schoolbook crossover logic in the repo and is the specification for a -future crossover metric. +`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 @@ -1770,3 +1774,68 @@ 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/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/ROADMAP.md b/ROADMAP.md index f0f133cb..fe8e8e41 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -150,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)`: diff --git a/bench/CompPolyBench/Common.lean b/bench/CompPolyBench/Common.lean index 306b2922..d513f221 100644 --- a/bench/CompPolyBench/Common.lean +++ b/bench/CompPolyBench/Common.lean @@ -74,6 +74,14 @@ structure BenchRecord where 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 @@ -84,7 +92,18 @@ 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 @@ -613,6 +632,18 @@ structure BenchSpec where 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 @@ -673,7 +704,9 @@ against a floor that was never measured. checksumIterations := spec.digestIterations measuredIterations := sampled.totalIterations totalNanos := sampled.totalNanos - averageNanos := sampled.stats.medianPicos / 1000 + medianNanos := sampled.stats.medianPicos / 1000 + workUnits := spec.workUnits + digestClass := spec.digestClass checksum := validationChecksum sinkDigest := sampled.sink stats := sampled.stats @@ -832,7 +865,9 @@ 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, + "\"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, @@ -870,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) @@ -913,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 @@ -925,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 @@ -957,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 @@ -1107,6 +1175,23 @@ def renderSpread (record : BenchRecord) : String := 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 @@ -1123,7 +1208,8 @@ def groupResultColumns (records : List BenchRecord) (totalUnit avgUnit : TimeUni ("Total (" ++ totalUnit.label ++ ")", true, fun r ↦ formatNanosInUnitOrAuto totalUnit r.totalNanos), ("Median (" ++ avgUnit.label ++ ")", true, fun r ↦ - formatNanosInUnitOrAuto avgUnit r.averageNanos), + formatNanosInUnitOrAuto avgUnit r.medianNanos) + ] ++ keepSome [perUnitColumn? records] ++ [ ("Spread", true, renderSpread) ] @@ -1145,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, "", @@ -1216,9 +1302,12 @@ def renderMarkdown (hardware : RunnerHardware) (preset : BenchPreset) (groups : private def validationRow (group : BenchGroup) : String := let records := group.records.toList let status := - match matchingChecksum? records with - | some checksum => "agree | `" ++ toString checksum ++ "`" - | none => "**MISMATCH** | -" + 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. 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/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/Goldilocks.lean b/bench/CompPolyBench/Fields/Goldilocks.lean deleted file mode 100644 index 31d5eb34..00000000 --- a/bench/CompPolyBench/Fields/Goldilocks.lean +++ /dev/null @@ -1,85 +0,0 @@ -/- -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 CompPolyBench.Common -public import CompPoly.Fields.Goldilocks - -/-! -# Goldilocks field arithmetic benchmarks - -Times multiplication and inversion over the Goldilocks prime `2^64 - 2^32 + 1`. Each -group runs the canonical `ZMod` implementation and the verified native-word -implementation on shared inputs, so the group checksum cross-checks the two. - -Goldilocks fits neither Montgomery carrier — `Mont32Field` requires modulus `< 2^31` -and `Mont64x8Field` is an eight-limb layout — so the fast path is the single-word -`UInt64` implementation in `CompPoly.Fields.Goldilocks.Fast`. --/ - -public section - -namespace CompPolyBench - -/-- Input-shape label shared by the Goldilocks arithmetic benchmarks. -/ -private def goldilocksShape : String := "256 random elements" - -/-- Time canonical against native-word Goldilocks multiplication as a single group. -/ -private def runGoldilocksMul (preset : BenchPreset) (gen : StdGen) : - IO (BenchGroup × StdGen) := do - let (values, gen) := (zmodArray Goldilocks.fieldSize 256 false).run gen - let fastValues := goldilocksFastArray values - let checksumIterations := digestPeriod values.size - let zmodRecord ← runTimedSpec - { name := "goldilocks-mul-zmod", representation := "ZMod", method := "mul", - field := "Goldilocks.Field", inputShape := goldilocksShape, - digestIterations := checksumIterations } - preset - (fun i ↦ values.getD (i % values.size) 1 * values.getD ((i + 1) % values.size) 1) checksumZMod - (sink := sinkZMod) - let fastRecord ← runTimedSpec - { name := "goldilocks-mul-fast", representation := "UInt64", method := "mul", - field := "Goldilocks.Fast.Field", inputShape := goldilocksShape, - digestIterations := checksumIterations } - preset - (fun i ↦ fastValues.getD (i % fastValues.size) 1 * - fastValues.getD ((i + 1) % fastValues.size) 1) - checksumGoldilocksFast (sink := sinkGoldilocksFast) - pure ({ groupKey := "fields-goldilocks-mul", title := "Goldilocks multiplication", - records := #[zmodRecord, fastRecord] }, gen) - -/-- Time canonical against native-word Goldilocks inversion as a single group. -/ -private def runGoldilocksInv (preset : BenchPreset) (gen : StdGen) : - IO (BenchGroup × StdGen) := do - let (values, gen) := (zmodArray Goldilocks.fieldSize 256 false).run gen - let fastValues := goldilocksFastArray values - let checksumIterations := digestPeriod values.size - let zmodRecord ← runTimedSpec - { name := "goldilocks-inv-zmod", representation := "ZMod", method := "inv", - field := "Goldilocks.Field", inputShape := goldilocksShape, - digestIterations := checksumIterations } - preset (fun i ↦ (values.getD (i % values.size) 1)⁻¹) checksumZMod - (sink := sinkZMod) - let fastRecord ← runTimedSpec - { name := "goldilocks-inv-fast", representation := "UInt64", method := "inv (Fermat chain)", - field := "Goldilocks.Fast.Field", inputShape := goldilocksShape, - digestIterations := checksumIterations } - preset (fun i ↦ (fastValues.getD (i % fastValues.size) 1)⁻¹) - checksumGoldilocksFast (sink := sinkGoldilocksFast) - pure ({ groupKey := "fields-goldilocks-inv", title := "Goldilocks inversion", - records := #[zmodRecord, fastRecord] }, gen) - -/-- Registry entries for the Goldilocks arithmetic benchmarks. -/ -def goldilocksTasks : List BenchTask := [ - BenchTask.fromGroupRunner - ⟨"fields-goldilocks-mul", "Goldilocks multiplication"⟩ - runGoldilocksMul, - BenchTask.fromGroupRunner - ⟨"fields-goldilocks-inv", "Goldilocks inversion"⟩ - runGoldilocksInv -] - -end CompPolyBench 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/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/SelfCheck.lean b/bench/CompPolyBench/Harness/SelfCheck.lean index ea0a9f9a..92673c06 100644 --- a/bench/CompPolyBench/Harness/SelfCheck.lean +++ b/bench/CompPolyBench/Harness/SelfCheck.lean @@ -6,6 +6,7 @@ Authors: Derek Sorensen module public import CompPolyBench.Common +public import CompPolyBench.Harness.Chain /-! # Harness Self-Check @@ -18,6 +19,11 @@ 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 @@ -27,7 +33,9 @@ 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-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. -/ @@ -69,7 +77,47 @@ digests are not correctness oracles — nothing is cross-checked against them the length only has to be fixed. -/ private def harnessDigestIterations : Nat := 16 -/-- Time the harness floor and the canary, and reject a collapsed canary. -/ +/-- 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 @@ -95,6 +143,54 @@ private def runHarnessSelfCheck (preset : BenchPreset) (selection : BenchSelecti 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 @@ -104,6 +200,14 @@ private def runHarnessSelfCheck (preset : BenchPreset) (selection : BenchSelecti 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. diff --git a/bench/CompPolyBench/Setup.lean b/bench/CompPolyBench/Setup.lean index c6942429..a5a56f6a 100644 --- a/bench/CompPolyBench/Setup.lean +++ b/bench/CompPolyBench/Setup.lean @@ -8,10 +8,11 @@ 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.Goldilocks public import CompPolyBench.Fields.Montgomery public import CompPolyBench.Harness.SelfCheck public import CompPolyBench.Multilinear.Basic @@ -32,7 +33,7 @@ namespace CompPolyBench def allTasks : List BenchTask := harnessTasks ++ univariateTasks ++ multivariateTasks ++ multilinearTasks ++ bivariateTasks ++ factorTasks ++ guruswamiSudanTasks ++ additiveNttTasks ++ extensionTasks ++ - montgomeryInvTasks ++ towerTasks ++ goldilocksTasks + montgomeryInvTasks ++ towerTasks ++ towerScalarTasks ++ fieldArithTasks /-- Metadata for every benchmark group accepted by the command-line selector. -/ def allGroupInfos : List BenchGroupInfo := @@ -187,12 +188,18 @@ def runSelected (selection : BenchSelection) (output : BenchOutput) (preset : Be 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}" - 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 + 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 diff --git a/bench/CompPolyBench/Univariate.lean b/bench/CompPolyBench/Univariate.lean index 275187dc..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 /-! @@ -24,6 +27,7 @@ namespace CompPolyBench def univariateTasks : List BenchTask := univariateBasicTasks ++ univariateBatchEvalTasks ++ univariateManyEvalTasks ++ univariateNttFastMulTasks ++ - univariateNttFastMulLowTasks ++ univariateFiniteFieldRootTasks + univariateNttFastMulLowTasks ++ univariateFiniteFieldRootTasks ++ + nttTransformTasks ++ reedSolomonTasks ++ crossoverTasks end CompPolyBench diff --git a/bench/CompPolyBench/Univariate/Basic.lean b/bench/CompPolyBench/Univariate/Basic.lean index 86ffa8f9..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 @@ -401,14 +399,6 @@ private def runGoldilocksUnivariateDense (preset : BenchPreset) (gen : StdGen) : Goldilocks.fieldSize "univariate-dense-goldilocks" "goldilocks" "Goldilocks.Field" "Goldilocks" 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 - /-- Benchmark dense BN254 univariate evaluation. -/ private def runBn254UnivariateDense (preset : BenchPreset) (gen : StdGen) : IO (BenchGroup × StdGen) := do @@ -418,15 +408,6 @@ private def runBn254UnivariateDense (preset : BenchPreset) (gen : StdGen) : checksumZMod checksumBn254Fast 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 - /-- Benchmark dense BLS12-381 univariate evaluation. -/ private def runBls12_381UnivariateDense (preset : BenchPreset) (gen : StdGen) : IO (BenchGroup × StdGen) := do @@ -437,15 +418,6 @@ private def runBls12_381UnivariateDense (preset : BenchPreset) (gen : StdGen) : checksumZMod checksumBls12_381Fast 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 - /-- Benchmark dense BLS12-377 univariate evaluation. -/ private def runBls12_377UnivariateDense (preset : BenchPreset) (gen : StdGen) : IO (BenchGroup × StdGen) := do 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/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/README.md b/bench/README.md index 075f54c5..297396aa 100644 --- a/bench/README.md +++ b/bench/README.md @@ -82,9 +82,14 @@ Roughly by area, with representative group prefixes: | 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 | -| Goldilocks arithmetic | `fields-goldilocks-{mul,inv}`: canonical `ZMod` vs single-word `UInt64` | +| 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 | -| Harness self-check | `harness-floor`, `harness-canary`: the harness measuring itself, see below | +| 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. @@ -130,6 +135,28 @@ makes that impossible — a `ZMod` element above `2 ^ 63` has no cheap word dige 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 @@ -179,6 +206,18 @@ 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 Each group derives its own input generator from its key (`genFor`), so a group's @@ -208,7 +247,7 @@ 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 34 seconds over the curated set and fails the run on a +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. diff --git a/bench/ci-groups.txt b/bench/ci-groups.txt index 1de26e0b..4cbd7504 100644 --- a/bench/ci-groups.txt +++ b/bench/ci-groups.txt @@ -10,13 +10,16 @@ # 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-*). Validating this -# subset takes ~34s against ~138s for every registered group; widen it if that -# trade changes. +# (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 @@ -56,3 +59,19 @@ 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/benchmarking.md b/docs/wiki/benchmarking.md index 512e5557..2300420d 100644 --- a/docs/wiki/benchmarking.md +++ b/docs/wiki/benchmarking.md @@ -30,7 +30,7 @@ gate you can believe and a gate that fails on noise. | 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 | ~34s over the curated set, ~138s over all groups | minutes | +| 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 @@ -159,27 +159,70 @@ and ext6 groups. Any tool comparing two result files must key on 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. Set `digestIterations` to the **period of the body in its iteration index**, +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. -4. Make the body depend on `i`, through a value built at run time. 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 for a group that did this for months. -5. Give every implementation in the group the same `checksum`, so the agreement - check is meaningful. -6. Supply a `sink` if the default would allocate, and make the group's rows +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. -7. Add the key to `bench/ci-groups.txt` to have it covered by the correctness +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. -8. New modules under `bench/` need no `./scripts/update-lib.sh` run; that script +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 @@ -192,11 +235,18 @@ Recorded so they are not rediscovered. The audit and plan live in benchmarks; only build timing gets that treatment. - Per-row floor subtraction is not reported, because the floor is per-representation rather than global. -- Coverage gaps against the roadmap: no standalone multiplicative NTT/iNTT group, - no base-field microbenchmarks outside Goldilocks, no `add`/`square`/batch-inverse, - no Reed-Solomon or polynomial-matrix groups. -- The polynomial-basis `GF(2^64)` of `CompPoly/Fields/Binary/BF64/` and its cubic - extension have no group, so the only binary-field timings are the tower ones. - A `mul` group there would measure carry-less multiply plus sparse reduction - against the tower's packed-word path, which is the comparison the two - representations exist to settle. +- 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/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/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 From 8cf33c121b17af5ab7079bc4e422841b18839706 Mon Sep 17 00:00:00 2001 From: Alexander Hicks <25369263+alexanderlhicks@users.noreply.github.com> Date: Tue, 15 Sep 2026 16:05:01 +0100 Subject: [PATCH 24/45] refactor(linear-algebra): make tensor basis actions explicit (#325) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Keep Mathlib’s default tensor actions and expose the right-action basis through the generic API. Preserve the binary-tower compatibility import. Reviewed and validated PR head: 118a53382ee3b33553eb6acc5145772d09aa4381. --- CompPoly.lean | 1 + .../Fields/Binary/Tower/TensorAlgebra.lean | 126 +------------ CompPoly/LinearAlgebra/README.md | 39 +++- .../LinearAlgebra/TensorProduct/Basis.lean | 66 +++++++ docs/wiki/binary-fields-and-ntt.md | 9 +- docs/wiki/repo-map.md | 2 +- tests/CompPolyTests.lean | 2 + .../Fields/Binary/Tower/TensorAlgebra.lean | 33 ++++ .../LinearAlgebra/TensorProduct/Basis.lean | 177 ++++++++++++++++++ 9 files changed, 327 insertions(+), 128 deletions(-) create mode 100644 CompPoly/LinearAlgebra/TensorProduct/Basis.lean create mode 100644 tests/CompPolyTests/Fields/Binary/Tower/TensorAlgebra.lean create mode 100644 tests/CompPolyTests/LinearAlgebra/TensorProduct/Basis.lean diff --git a/CompPoly.lean b/CompPoly.lean index da6bc87c..0bdacb24 100644 --- a/CompPoly.lean +++ b/CompPoly.lean @@ -220,6 +220,7 @@ 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 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/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..ff33459e --- /dev/null +++ b/CompPoly/LinearAlgebra/TensorProduct/Basis.lean @@ -0,0 +1,66 @@ +/- +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. +-/ + +@[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/docs/wiki/binary-fields-and-ntt.md b/docs/wiki/binary-fields-and-ntt.md index 008cb144..f148450d 100644 --- a/docs/wiki/binary-fields-and-ntt.md +++ b/docs/wiki/binary-fields-and-ntt.md @@ -122,8 +122,13 @@ support lemmas: 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`, `Tower/Impl.lean`, and `Tower/TensorAlgebra.lean` connect the - layers and expose useful transport lemmas. +- `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. diff --git a/docs/wiki/repo-map.md b/docs/wiki/repo-map.md index 8dfb8045..51e377d3 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 diff --git a/tests/CompPolyTests.lean b/tests/CompPolyTests.lean index 536d6e51..02d60cad 100644 --- a/tests/CompPolyTests.lean +++ b/tests/CompPolyTests.lean @@ -35,6 +35,7 @@ public import CompPolyTests.Fields.Binary.BF64 public import CompPolyTests.Fields.Binary.Common public import CompPolyTests.Fields.Binary.Tower.Algebra public import CompPolyTests.Fields.Binary.Tower.Fast +public import CompPolyTests.Fields.Binary.Tower.TensorAlgebra public import CompPolyTests.Fields.Extension.Arithmetic public import CompPolyTests.Fields.Extension.Binomial public import CompPolyTests.Fields.Goldilocks.Fast @@ -45,6 +46,7 @@ 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 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/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 From 4dc26d77d24bb7e0b23b6a407e93853625806f8d Mon Sep 17 00:00:00 2001 From: Alexander Hicks <25369263+alexanderlhicks@users.noreply.github.com> Date: Tue, 15 Sep 2026 16:18:49 +0100 Subject: [PATCH 25/45] doc: restore tensor basis background references (#349) Restore module-level references for the tensor basis construction. Reviewed and validated PR head: 38a771e1ce072cf6d1ca50f00655ca0d37d5ad5d. --- CompPoly/LinearAlgebra/TensorProduct/Basis.lean | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CompPoly/LinearAlgebra/TensorProduct/Basis.lean b/CompPoly/LinearAlgebra/TensorProduct/Basis.lean index ff33459e..347ba6db 100644 --- a/CompPoly/LinearAlgebra/TensorProduct/Basis.lean +++ b/CompPoly/LinearAlgebra/TensorProduct/Basis.lean @@ -19,6 +19,14 @@ 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 From dd5d596ee0a868e4fc3ed76487fd00c62de5896c Mon Sep 17 00:00:00 2001 From: Alexander Hicks <25369263+alexanderlhicks@users.noreply.github.com> Date: Tue, 15 Sep 2026 16:30:14 +0100 Subject: [PATCH 26/45] fix(fields): retain extension presentations in the carrier type (#326) Reviewed and validated PR head: 708020141ec8d774e0787e67d35efba08899dc42. --- CompPoly/Fields/Binary/BF64/Ext3.lean | 7 +- CompPoly/Fields/Extension/Defs.lean | 69 +++++++++------- CompPoly/Fields/README.md | 2 +- docs/wiki/field-extensions.md | 6 +- tests/CompPolyTests.lean | 1 + .../Fields/Extension/Presentation.lean | 79 +++++++++++++++++++ 6 files changed, 131 insertions(+), 33 deletions(-) create mode 100644 tests/CompPolyTests/Fields/Extension/Presentation.lean diff --git a/CompPoly/Fields/Binary/BF64/Ext3.lean b/CompPoly/Fields/Binary/BF64/Ext3.lean index 5d6f465a..e8818c87 100644 --- a/CompPoly/Fields/Binary/BF64/Ext3.lean +++ b/CompPoly/Fields/Binary/BF64/Ext3.lean @@ -17,8 +17,9 @@ 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/`, so `Ext ext3Params` -is definitionally `Vector BF64 3` — the three-limb layout, with no translation needed. +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 @@ -164,7 +165,7 @@ instance : Fact (Irreducible ext3Params.poly) := /-- `GF(2^192)`, the degree-three extension `GF(2^64)[y] / (y^3 + y + 1)`. -Definitionally `Vector BF64 3`, the three-limb layout `c0 + c1 * y + c2 * y^2`. -/ +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. -/ diff --git a/CompPoly/Fields/Extension/Defs.lean b/CompPoly/Fields/Extension/Defs.lean index 8cfff03f..46cab804 100644 --- a/CompPoly/Fields/Extension/Defs.lean +++ b/CompPoly/Fields/Extension/Defs.lean @@ -17,9 +17,10 @@ no trimming and no size branching. Multiplication expands each product monomial `monomialMod (i + j)`, the reduced form of `Xⁱ⁺ʲ` modulo `f`, obtained by iterating a single "multiply by `X`, reduce mod `f`" linear map, `shiftReduce`. -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 parameters are bundled into `ExtensionParams` and retained in the structure type `Ext P`. +Elements of different presentations cannot be interchanged merely because their coefficient +vectors have the same length. The maps `Ext.coeffs` and `Ext.ofVector` expose the coordinates +explicitly. The special case `f = X^d - W` (a binomial extension) is recovered by `BinomialParams.toExtensionParams`, whose `lower` vector is `(-W, 0, …, 0)`; see @@ -34,7 +35,7 @@ establishes `CommRing`; `CompPoly/Fields/Extension/Field.lean` adds inversion an * `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 P`: the presentation-indexed carrier with coefficient vectors of length `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`. @@ -64,10 +65,8 @@ 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`. +Irreducibility is not part of these parameters: the quotient is a commutative ring for every +monic modulus. A field structure additionally requires irreducibility of the modulus. -/ structure ExtensionParams (F : Type*) [Field F] [Fintype F] where /-- The degree of the extension. -/ @@ -93,8 +92,7 @@ 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`. -/ +/-- 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 @@ -130,20 +128,29 @@ 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`). +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. -/ -def Ext {F : Type*} [Field F] [Fintype F] (P : ExtensionParams F) : Type _ := Vector F P.d +structure Ext {F : Type*} [Field F] [Fintype F] (P : ExtensionParams F) : Type _ where + /-- The coefficient of `X^i` is stored at index `i`. -/ + coeffs : 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 coefficients in ascending order of powers. -/ +@[inline] def ofVector (v : Vector F P.d) : Ext P := ⟨v⟩ -/-- Build an element from a coefficient vector. This is the identity. -/ -@[inline] def ofVector (v : Vector F P.d) : Ext P := v +/-- 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 + +/-- Reconstructing an element from its coefficient vector returns that element. -/ +@[simp] theorem ofVector_coeffs (x : Ext P) : ofVector (coeffs x) = x := rfl + +/-- 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) @@ -152,16 +159,15 @@ variable [Fintype F] {P : ExtensionParams F} @[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] + simp [coeff, ofFn, ofVector] /-- 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⟩ + coeffs_injective (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. -/ +/-- 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 @@ -279,10 +285,19 @@ 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 [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 -/ @@ -348,8 +363,8 @@ 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`. +Parameters for the quotient `F[X] / (X^d - W)`: the degree, constant `W`, and cardinality +of the base field. The modulus has lower coefficients `(-W, 0, …, 0)`. -/ structure BinomialParams (F : Type*) [Field F] [Fintype F] where /-- The degree of the extension. -/ diff --git a/CompPoly/Fields/README.md b/CompPoly/Fields/README.md index 069a0d5c..3dca721c 100644 --- a/CompPoly/Fields/README.md +++ b/CompPoly/Fields/README.md @@ -22,7 +22,7 @@ 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/Defs.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/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)\). | diff --git a/docs/wiki/field-extensions.md b/docs/wiki/field-extensions.md index 3dfb2832..8c0247a9 100644 --- a/docs/wiki/field-extensions.md +++ b/docs/wiki/field-extensions.md @@ -85,7 +85,7 @@ and [`KoalaBear/Ext6.lean`](../../CompPoly/Fields/KoalaBear/Ext6.lean) (`X^6 + X characteristic-2 instance, and it uses the *general* `ExtensionParams` path rather than `BinomialParams`: over a char-2 field `X^3 - W = X^3 + W`, and the binomial criterion needs `d ∣ q - 1`, which fails for `d = 3` and `q = 2^64` (`3 ∤ 2^64 - 1`). So `Ext P` is instantiated -directly, and `Ext ext3Params` is definitionally `Vector BF64 3`. +directly, and `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: @@ -259,7 +259,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/`. diff --git a/tests/CompPolyTests.lean b/tests/CompPolyTests.lean index 02d60cad..00119389 100644 --- a/tests/CompPolyTests.lean +++ b/tests/CompPolyTests.lean @@ -38,6 +38,7 @@ public import CompPolyTests.Fields.Binary.Tower.Fast public import CompPolyTests.Fields.Binary.Tower.TensorAlgebra public import CompPolyTests.Fields.Extension.Arithmetic public import CompPolyTests.Fields.Extension.Binomial +public import CompPolyTests.Fields.Extension.Presentation public import CompPolyTests.Fields.Goldilocks.Fast public import CompPolyTests.Fields.KoalaBear.Fast public import CompPolyTests.Fields.Mersenne31.Fast diff --git a/tests/CompPolyTests/Fields/Extension/Presentation.lean b/tests/CompPolyTests/Fields/Extension/Presentation.lean new file mode 100644 index 00000000..79f34026 --- /dev/null +++ b/tests/CompPolyTests/Fields/Extension/Presentation.lean @@ -0,0 +1,79 @@ +/- +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 + card_eq := ZMod.card 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 + card_eq := ZMod.card 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] [Fintype F] {P : ExtensionParams F} + [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 From c0e460b341e127b9e7826caf8011ea51091c95a5 Mon Sep 17 00:00:00 2001 From: Alexander Hicks <25369263+alexanderlhicks@users.noreply.github.com> Date: Tue, 15 Sep 2026 16:41:31 +0100 Subject: [PATCH 27/45] refactor(fields): narrow extension definition imports (#327) Reviewed and validated PR head: c0a50ffb795f449028c3a1607cf5397f46e51e05. --- CompPoly/Fields/Extension/Defs.lean | 3 ++- docs/wiki/field-extensions.md | 7 +++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CompPoly/Fields/Extension/Defs.lean b/CompPoly/Fields/Extension/Defs.lean index 46cab804..3748f116 100644 --- a/CompPoly/Fields/Extension/Defs.lean +++ b/CompPoly/Fields/Extension/Defs.lean @@ -5,8 +5,9 @@ Authors: Derek Sorensen -/ module -public import CompPoly.Fields.Extension.Binomial public import Mathlib.Algebra.BigOperators.Fin +public import Mathlib.Algebra.Polynomial.BigOperators +public import Mathlib.Data.Fintype.Card /-! # Computable extension fields by an arbitrary monic modulus diff --git a/docs/wiki/field-extensions.md b/docs/wiki/field-extensions.md index 8c0247a9..2e1f75d7 100644 --- a/docs/wiki/field-extensions.md +++ b/docs/wiki/field-extensions.md @@ -57,6 +57,13 @@ makes a cheap Frobenius and a norm-based inverse possible. See "Choosing a gener | 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` | +Import `CompPoly.Fields.Extension.Defs` for presentations and arithmetic, `Bridge` for +the quotient bridge and ring laws, or `Field` for the field structure. The binomial +irreducibility criterion has its own import, `CompPoly.Fields.Extension.Binomial`; +`Defs` does not import it. The `CompPoly.Fields.Extension` facade re-exports all four +modules. Presentation parameters still require a finite base field and its certified +cardinality, including when only `Defs` is imported. + `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, so there are currently **two** Rabin soundness proofs in the repo. Rebasing the From e5f87c8ef2c7018390c1fb159aec288c633a1e71 Mon Sep 17 00:00:00 2001 From: Alexander Hicks <25369263+alexanderlhicks@users.noreply.github.com> Date: Tue, 15 Sep 2026 16:55:50 +0100 Subject: [PATCH 28/45] fix(binary): isolate the BF64 presentation (#329) Reviewed and validated PR head: 9ba604f4073f53f1145b8660dda995768ffcb06b. --- CompPoly/Fields/Binary/BF64.lean | 2 +- CompPoly/Fields/Binary/BF64/Basic.lean | 3 +- CompPoly/Fields/Binary/BF64/Impl.lean | 142 +++++++++++++----- CompPoly/Fields/README.md | 2 +- docs/wiki/binary-fields-and-ntt.md | 7 +- tests/CompPolyTests.lean | 1 + tests/CompPolyTests/Fields/Binary/BF64.lean | 19 +-- .../Fields/Binary/BF64/Presentation.lean | 75 +++++++++ 8 files changed, 200 insertions(+), 51 deletions(-) create mode 100644 tests/CompPolyTests/Fields/Binary/BF64/Presentation.lean diff --git a/CompPoly/Fields/Binary/BF64.lean b/CompPoly/Fields/Binary/BF64.lean index 4fda54af..e387ab9d 100644 --- a/CompPoly/Fields/Binary/BF64.lean +++ b/CompPoly/Fields/Binary/BF64.lean @@ -15,7 +15,7 @@ public import CompPoly.Fields.Binary.BF64.Ext3 Facade module. It re-exports the specification-side quotient model from `CompPoly.Fields.Binary.BF64.Basic`, the reduction from `...Reduce`, the computable -`BitVec 64` carrier from `...Impl`, and the degree-three extension from `...Ext3`. +nominal word carrier from `...Impl`, and the degree-three extension from `...Ext3`. -/ @[expose] public section diff --git a/CompPoly/Fields/Binary/BF64/Basic.lean b/CompPoly/Fields/Binary/BF64/Basic.lean index d37ccf13..194e6126 100644 --- a/CompPoly/Fields/Binary/BF64/Basic.lean +++ b/CompPoly/Fields/Binary/BF64/Basic.lean @@ -53,7 +53,8 @@ executable representation. It exists to state irreducibility and is never evalua `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 `BitVec 64`; the two are related by +`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. diff --git a/CompPoly/Fields/Binary/BF64/Impl.lean b/CompPoly/Fields/Binary/BF64/Impl.lean index 2fb14574..909c002a 100644 --- a/CompPoly/Fields/Binary/BF64/Impl.lean +++ b/CompPoly/Fields/Binary/BF64/Impl.lean @@ -11,14 +11,17 @@ public import Mathlib.RingTheory.AdjoinRoot /-! # The computable `GF(2^64)` carrier -An element is a 64-bit word whose bit `i` is the coefficient of `x^i`. Addition is `xor`, +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. ## Main definitions -* `BF64` — the carrier, `BitVec 64`, with `Add`, `Mul`, `Inv`, `CommRing` and `Field`. +* `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. @@ -49,30 +52,77 @@ multiplications for a full-order exponent and is unusable in the kernel. open Polynomial BinaryField -/-- `GF(2^64)` in its computable, machine representation: a 64-bit word whose bit `i` is -the coefficient of `x^i`. -/ -abbrev BF64 : Type := BitVec 64 +/-- 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 -instance : Zero BF64 := ⟨(0 : BitVec 64)⟩ -instance : One BF64 := ⟨(1 : BitVec 64)⟩ +/-- Construct a field element from its polynomial-basis bits. -/ +@[inline] def ofBitVec (a : BitVec 64) : BF64 := ⟨a⟩ -/-- Addition in characteristic two is `xor`. -/ -instance : Add BF64 := ⟨fun a b => a ^^^ b⟩ +/-- 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 => a ^^^ b⟩ +instance : Sub BF64 := ⟨fun a b => ofBitVec (a.toBitVec ^^^ b.toBitVec)⟩ -/-- Multiplication: the carry-less product, reduced modulo the modulus. -/ -instance : Mul BF64 := - ⟨fun a b => reduce (carryLessMul (w := 128) a b)⟩ +/-- 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 : BitVec 64) + toPoly a.toBitVec /-- The bridge into the quotient. -/ noncomputable def toQuot (a : BF64) : BF64Quot := @@ -81,10 +131,14 @@ noncomputable def toQuot (a : BF64) : BF64Quot := /-! ## Equation lemmas for the operations -/ /-- Addition unfolds to `xor`. -/ -theorem add_def (a b : BF64) : a + b = a ^^^ b := rfl +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 -/-- Multiplication unfolds to a carry-less product followed by `reduce`. -/ -theorem mul_def (a b : BF64) : a * b = reduce (carryLessMul (w := 128) 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 -/ @@ -97,7 +151,7 @@ theorem mul_def (a b : BF64) : a * b = reduce (carryLessMul (w := 128) a b) := r @[simp] theorem toPolyBF64_add (a b : BF64) : toPolyBF64 (a + b) = toPolyBF64 a + toPolyBF64 b := by - rw [toPolyBF64, toPolyBF64, toPolyBF64, add_def] + 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 @@ -105,10 +159,10 @@ theorem mul_def (a b : BF64) : a * b = reduce (carryLessMul (w := 128) a b) := r /-- 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, mul_def, toPoly_reduce, + 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 * toPoly b / basePoly), by + exact ⟨-(toPoly a.toBitVec * toPoly b.toBitVec / basePoly), by rw [EuclideanDomain.mod_eq_sub_mul_div]; ring⟩ /-- Distinct carrier values denote distinct quotient elements. @@ -117,23 +171,21 @@ A difference of two carrier values has degree below 64, while the modulus has de 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 ^^^ b) := by + 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 ^^^ b) = 0 := by + 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 ^^^ b)) (not_lt.mpr hle) - have hxor : (a ^^^ b : BitVec 64) = 0 := by + 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 ^^^ b)).mpr hnz) hzero - have : a = b := by - have := congrArg (fun v => v ^^^ b) hxor - simpa [BitVec.xor_assoc] using this - exact this + 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 @@ -153,18 +205,31 @@ 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 := BitVec.xor_self +theorem add_self (a : BF64) : a + a = 0 := by + apply toBitVec_injective + exact BitVec.xor_self /-! ### Scalar and power operations -In characteristic two an integer scalar multiple collapses to a parity test, and the -natural- and integer-number casts collapse likewise. Defining them in that closed form -keeps them computable and makes the transport conditions immediate. +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⟩ -instance : NatCast BF64 := ⟨Nat.unaryCast⟩ + +/-- 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⟩ @@ -231,8 +296,9 @@ 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 => - show toQuot ((k : BF64) + 1) = _ - rw [toQuot_add, ih, toQuot_one, Nat.cast_succ] + 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]) @@ -383,8 +449,8 @@ theorem exists_pair_ne : ∃ x y : BF64, x ≠ y := /-- The carrier is in bijection with `Fin (2 ^ 64)`, by its underlying representation. -/ def equivFin : BF64 ≃ Fin (2 ^ 64) where - toFun a := a.toFin - invFun i := BitVec.ofFin i + toFun a := a.toBitVec.toFin + invFun i := ofBitVec (BitVec.ofFin i) left_inv _ := rfl right_inv _ := rfl diff --git a/CompPoly/Fields/README.md b/CompPoly/Fields/README.md index 3dca721c..c5da3c36 100644 --- a/CompPoly/Fields/README.md +++ b/CompPoly/Fields/README.md @@ -66,7 +66,7 @@ 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 computable `BitVec 64` carrier, 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/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). diff --git a/docs/wiki/binary-fields-and-ntt.md b/docs/wiki/binary-fields-and-ntt.md index f148450d..6ad64791 100644 --- a/docs/wiki/binary-fields-and-ntt.md +++ b/docs/wiki/binary-fields-and-ntt.md @@ -97,12 +97,17 @@ encoding is observable. - [`../../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 computable `BitVec 64` representation, its bridge to the quotient, and the + 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. + 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 diff --git a/tests/CompPolyTests.lean b/tests/CompPolyTests.lean index 00119389..1c7bcbce 100644 --- a/tests/CompPolyTests.lean +++ b/tests/CompPolyTests.lean @@ -32,6 +32,7 @@ public import CompPolyTests.Fields.BabyBear.Fast 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.Fast diff --git a/tests/CompPolyTests/Fields/Binary/BF64.lean b/tests/CompPolyTests/Fields/Binary/BF64.lean index ec150336..965e50e8 100644 --- a/tests/CompPolyTests/Fields/Binary/BF64.lean +++ b/tests/CompPolyTests/Fields/Binary/BF64.lean @@ -52,22 +52,23 @@ theorem multiplication_matches_reference : /-! ## Non-vacuity guards -/ -/-- The element `x`, a generator of the multiplicative group, is not zero. -/ -theorem generator_ne_zero : (0x2 : _root_.BF64) ≠ 0 := by decide +kernel +/-- The polynomial element `X`, encoded by bit one, is not zero. -/ +theorem generator_ne_zero : (ofBitVec (2#64)) ≠ 0 := by decide +kernel -/-- The generator is not one, so it is not a degenerate choice. -/ -theorem generator_ne_one : (0x2 : _root_.BF64) ≠ 1 := 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) * 0x01090913877ed8ed : _root_.BF64) = 0x01090913877ed8ed := by + ((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 : - ((0x8000000000000000 : _root_.BF64) * 0x2 : _root_.BF64) = 0x1B := by + (ofBitVec (0x8000000000000000#64) * ofBitVec (2#64)) = ofBitVec (0x1B#64) := by rw [_root_.BF64.mul_def]; decide +kernel /-! ## Extension-field vectors @@ -80,8 +81,8 @@ section Vectors open CompPoly.Extension -private def limbs (c0 c1 c2 : _root_.BF64) : Ext3 := - Ext.ofFn (fun i => if (i : ℕ) = 0 then c0 else if (i : ℕ) = 1 then c1 else c2) +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 @@ -104,7 +105,7 @@ private def y : Ext3 := limbs 0 1 0 == limbs 0x2ad322ebf2f9043b 0x8ac800aa67154c80 0x6d0f76651d3c4d0c -- Inversion evaluates in both fields. -#guard (0x01090913877ed8ed : _root_.BF64) * (0x01090913877ed8ed : _root_.BF64)⁻¹ == 1 +#guard ofBitVec (0x01090913877ed8ed#64) * (ofBitVec (0x01090913877ed8ed#64))⁻¹ == 1 #guard (0 : _root_.BF64)⁻¹ == 0 #guard y * y⁻¹ == 1 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 From cf340b215e1fdbd58dfa0a01227199fa2c80dcf6 Mon Sep 17 00:00:00 2001 From: Alexander Hicks <25369263+alexanderlhicks@users.noreply.github.com> Date: Tue, 15 Sep 2026 17:09:31 +0100 Subject: [PATCH 29/45] refactor(fields): separate extension arithmetic from finite certificates (#330) Reviewed and validated PR head: 16724834a94bdf415fc118996e4b21194e25a6ff. --- CompPoly.lean | 2 + CompPoly/Fields/BabyBear/Ext4.lean | 4 +- CompPoly/Fields/Binary/BF64/Ext3.lean | 4 +- CompPoly/Fields/Extension.lean | 10 +- CompPoly/Fields/Extension/Arithmetic.lean | 379 ++++++++++++++++++ CompPoly/Fields/Extension/Bridge.lean | 2 +- CompPoly/Fields/Extension/Cardinality.lean | 64 +++ CompPoly/Fields/Extension/Defs.lean | 360 +---------------- CompPoly/Fields/Extension/Field.lean | 62 +-- CompPoly/Fields/Hachi/Ext4.lean | 4 +- CompPoly/Fields/KoalaBear/Ext4.lean | 4 +- CompPoly/Fields/KoalaBear/Ext5.lean | 4 +- CompPoly/Fields/KoalaBear/Ext6.lean | 4 +- CompPoly/Fields/README.md | 4 +- bench/CompPolyBench/Fields/Extension.lean | 4 +- docs/wiki/field-extensions.md | 49 ++- tests/CompPolyTests.lean | 2 + .../Fields/Extension/Certificates.lean | 90 +++++ .../Fields/Extension/Presentation.lean | 5 +- .../Fields/Extension/RawArithmetic.lean | 41 ++ 20 files changed, 676 insertions(+), 422 deletions(-) create mode 100644 CompPoly/Fields/Extension/Arithmetic.lean create mode 100644 CompPoly/Fields/Extension/Cardinality.lean create mode 100644 tests/CompPolyTests/Fields/Extension/Certificates.lean create mode 100644 tests/CompPolyTests/Fields/Extension/RawArithmetic.lean diff --git a/CompPoly.lean b/CompPoly.lean index 0bdacb24..831bb3f5 100644 --- a/CompPoly.lean +++ b/CompPoly.lean @@ -133,8 +133,10 @@ 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 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/BF64/Ext3.lean b/CompPoly/Fields/Binary/BF64/Ext3.lean index e8818c87..675552c9 100644 --- a/CompPoly/Fields/Binary/BF64/Ext3.lean +++ b/CompPoly/Fields/Binary/BF64/Ext3.lean @@ -138,7 +138,9 @@ def ext3Params : ExtensionParams BF64 where two_le := by norm_num lower := #v[1, 1, 0] q := 2 ^ 64 - card_eq := card_bf64 + +instance : Fact (Nat.card BF64 = ext3Params.q) := + ⟨by rw [Nat.card_eq_fintype_card]; exact card_bf64⟩ /-- The extension has degree three. -/ @[simp] theorem ext3Params_d : ext3Params.d = 3 := rfl 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 3748f116..616b7b5c 100644 --- a/CompPoly/Fields/Extension/Defs.lean +++ b/CompPoly/Fields/Extension/Defs.lean @@ -5,51 +5,19 @@ Authors: Derek Sorensen -/ module -public import Mathlib.Algebra.BigOperators.Fin +public import CompPoly.Fields.Extension.Arithmetic public import Mathlib.Algebra.Polynomial.BigOperators -public import Mathlib.Data.Fintype.Card /-! -# 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 retained in the structure type `Ext P`. -Elements of different presentations cannot be interchanged merely because their coefficient -vectors have the same length. The maps `Ext.coeffs` and `Ext.ofVector` expose the coordinates -explicitly. - -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 presentation-indexed carrier with coefficient vectors of length `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 @@ -60,48 +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 not part of these parameters: the quotient is a commutative ring for every -monic modulus. A field structure additionally requires irreducibility of the modulus. --/ -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] - -/-- 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) - -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. -/ @@ -128,262 +57,9 @@ theorem monic_poly : P.poly.Monic := by 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*} [Field F] [Fintype F] (P : ExtensionParams F) : Type _ where - /-- The coefficient of `X^i` is stored at index `i`. -/ - coeffs : Vector F P.d - -namespace Ext - -variable [Fintype F] {P : ExtensionParams F} - -/-- Build an element from coefficients in ascending order of powers. -/ -@[inline] def ofVector (v : Vector F P.d) : Ext P := ⟨v⟩ - -/-- 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 - -/-- Reconstructing an element from its coefficient vector returns that element. -/ -@[simp] theorem ofVector_coeffs (x : Ext P) : ofVector (coeffs x) = x := rfl - -/-- 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] - -@[simp] theorem coeff_ofFn (g : Fin P.d → F) (i : Fin P.d) : coeff (ofFn g) i = g i := by - simp [coeff, ofFn, ofVector] - -/-- 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⟩) - -theorem ofFn_coeff (x : Ext P) : ofFn (coeff x) = x := by ext i; simp - -/-- 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` 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) := 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 - -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. --/ - -/-- -Parameters for the quotient `F[X] / (X^d - W)`: the degree, constant `W`, and cardinality -of the base field. The modulus has lower coefficients `(-W, 0, …, 0)`. --/ -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 @@ -392,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/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/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/README.md b/CompPoly/Fields/README.md index c5da3c36..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 presentation-indexed carrier `Ext P` with length-`d` coefficient vectors and 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)\). | diff --git a/bench/CompPolyBench/Fields/Extension.lean b/bench/CompPolyBench/Fields/Extension.lean index b0e54ff4..93551b12 100644 --- a/bench/CompPolyBench/Fields/Extension.lean +++ b/bench/CompPolyBench/Fields/Extension.lean @@ -74,8 +74,8 @@ private def runExtOp {E : Type} (groupKey title name method fieldName shape : St 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 diff --git a/docs/wiki/field-extensions.md b/docs/wiki/field-extensions.md index 2e1f75d7..06dc7735 100644 --- a/docs/wiki/field-extensions.md +++ b/docs/wiki/field-extensions.md @@ -53,16 +53,19 @@ makes a cheap Frobenius and a norm-based inverse possible. See "Choosing a gener | 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`, and the `_of_card` forms concrete callers use | -| 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]`) | +| 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.Defs` for presentations and arithmetic, `Bridge` for -the quotient bridge and ring laws, or `Field` for the field structure. The binomial -irreducibility criterion has its own import, `CompPoly.Fields.Extension.Binomial`; -`Defs` does not import it. The `CompPoly.Fields.Extension` facade re-exports all four -modules. Presentation parameters still require a finite base field and its certified -cardinality, including when only `Defs` is imported. +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 @@ -121,7 +124,7 @@ Concretely, for `P : ExtensionParams F`: | 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, @@ -232,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 @@ -282,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 @@ -362,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/tests/CompPolyTests.lean b/tests/CompPolyTests.lean index 1c7bcbce..222d48d5 100644 --- a/tests/CompPolyTests.lean +++ b/tests/CompPolyTests.lean @@ -38,8 +38,10 @@ public import CompPolyTests.Fields.Binary.Tower.Algebra public import CompPolyTests.Fields.Binary.Tower.Fast 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.Mersenne31.Fast 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 index 79f34026..7e0b13a0 100644 --- a/tests/CompPolyTests/Fields/Extension/Presentation.lean +++ b/tests/CompPolyTests/Fields/Extension/Presentation.lean @@ -28,7 +28,6 @@ private def first : ExtensionParams (ZMod 2) where two_le := le_rfl lower := #v[1, 0] q := 2 - card_eq := ZMod.card 2 /-- Parameters for the monic modulus `X² + X + 1` over `GF(2)`. -/ private def second : ExtensionParams (ZMod 2) where @@ -36,7 +35,6 @@ private def second : ExtensionParams (ZMod 2) where two_le := le_rfl lower := #v[1, 1] q := 2 - card_eq := ZMod.card 2 example (_x : Ext first) : True := by fail_if_success @@ -64,7 +62,8 @@ example : LawfulBEq (Ext first) := inferInstance section Operations -variable {F : Type*} [Field F] [Fintype F] {P : ExtensionParams F} +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. 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 From 290c351f90e8f16fd5ec080a8f982c2b96218a06 Mon Sep 17 00:00:00 2001 From: Alexander Hicks <25369263+alexanderlhicks@users.noreply.github.com> Date: Tue, 15 Sep 2026 17:21:18 +0100 Subject: [PATCH 30/45] fix(fields): avoid eager BF64 enumeration at native startup (#331) Reviewed and validated PR head: 6b2b9f2220e2465a5d42e81b1f6a51d18ebcb54d. --- .github/workflows/lean_action_ci.yml | 9 ++++ CompPoly/Fields/Binary/BF64/Ext3.lean | 15 ++++-- CompPoly/Fields/Binary/BF64/Impl.lean | 21 +++++--- docs/wiki/binary-fields-and-ntt.md | 7 +++ docs/wiki/quickstart.md | 20 +++++--- lakefile.lean | 4 ++ tests/CompPolyNativeSmoke.lean | 57 +++++++++++++++++++++ tests/CompPolyTests/Fields/Binary/BF64.lean | 17 ++++++ tests/README.md | 27 +++++++++- 9 files changed, 157 insertions(+), 20 deletions(-) create mode 100644 tests/CompPolyNativeSmoke.lean diff --git a/.github/workflows/lean_action_ci.yml b/.github/workflows/lean_action_ci.yml index bfbf965e..a3dcb23b 100644 --- a/.github/workflows/lean_action_ci.yml +++ b/.github/workflows/lean_action_ci.yml @@ -170,6 +170,15 @@ 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 diff --git a/CompPoly/Fields/Binary/BF64/Ext3.lean b/CompPoly/Fields/Binary/BF64/Ext3.lean index 675552c9..9fc101bc 100644 --- a/CompPoly/Fields/Binary/BF64/Ext3.lean +++ b/CompPoly/Fields/Binary/BF64/Ext3.lean @@ -36,7 +36,7 @@ follows. * `ext3Poly_irreducible` — the cubic is irreducible over `BF64`. * `ext3Params_poly` — the coefficient vector `#v[1, 1, 0]` denotes that cubic. -* `card_ext3` — `Fintype.card Ext3 = 2 ^ 192`. +* `nat_card_ext3` — `Nat.card Ext3 = 2 ^ 192`. ## Implementation notes @@ -99,6 +99,7 @@ A root has `a ^ 7 = 1`, so its multiplicative order divides `7`. It also divides `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 @@ -140,7 +141,7 @@ def ext3Params : ExtensionParams BF64 where q := 2 ^ 64 instance : Fact (Nat.card BF64 = ext3Params.q) := - ⟨by rw [Nat.card_eq_fintype_card]; exact card_bf64⟩ + ⟨nat_card_bf64⟩ /-- The extension has degree three. -/ @[simp] theorem ext3Params_d : ext3Params.d = 3 := rfl @@ -198,8 +199,12 @@ theorem aeval_ext3Gen : aeval ext3Gen ext3Params.poly = 0 := Ext.aeval_gen_poly 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. -/ -@[simp] theorem card_ext3 : Fintype.card Ext3 = 2 ^ 192 := by - rw [Ext.card_ext, ext3Params_q, ext3Params_d, ← pow_mul] +/-- `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 index 909c002a..d7205348 100644 --- a/CompPoly/Fields/Binary/BF64/Impl.lean +++ b/CompPoly/Fields/Binary/BF64/Impl.lean @@ -17,6 +17,8 @@ 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 @@ -31,7 +33,7 @@ The nominal carrier separates this polynomial presentation from raw words and bi * `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.card_bf64` — `Fintype.card BF64 = 2 ^ 64`. +* `BF64.nat_card_bf64` — `Nat.card BF64 = 2 ^ 64`. ## Implementation notes @@ -454,17 +456,20 @@ def equivFin : BF64 ≃ Fin (2 ^ 64) where left_inv _ := rfl right_inv _ := rfl -instance : Fintype BF64 := Fintype.ofEquiv _ equivFin.symm +instance : Finite BF64 := Finite.of_equiv (Fin (2 ^ 64)) equivFin.symm -/-- `BF64` has `2 ^ 64` elements. -/ -theorem card_bf64 : Fintype.card BF64 = 2 ^ 64 := by - rw [Fintype.card_congr equivFin, Fintype.card_fin] +/-- `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 - have hcard : Fintype.card BF64 = Fintype.card (BF64Quot) := by - rw [card_bf64, card_bf64Quot] - exact ((Fintype.bijective_iff_injective_and_card toQuot).mpr ⟨toQuot_injective, hcard⟩).2 + 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 diff --git a/docs/wiki/binary-fields-and-ntt.md b/docs/wiki/binary-fields-and-ntt.md index 6ad64791..48792086 100644 --- a/docs/wiki/binary-fields-and-ntt.md +++ b/docs/wiki/binary-fields-and-ntt.md @@ -108,6 +108,13 @@ the maps are inverse and introduce no implicit conversion to another field prese 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 diff --git a/docs/wiki/quickstart.md b/docs/wiki/quickstart.md index 14911323..73e0ad54 100644 --- a/docs/wiki/quickstart.md +++ b/docs/wiki/quickstart.md @@ -37,6 +37,13 @@ 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 @@ -45,7 +52,7 @@ lake exe axiomsweep --check ``` `axiomsweep` is kernel-level axiom/`sorry` accounting for every reportable -`CompPoly.*` declaration, diffed against the committed baseline +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 @@ -112,11 +119,12 @@ come from the on-demand Benchmarks workflow. See - [`../../.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`, then 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 collecting timings, and uploads the digests as an artifact. + 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). diff --git a/lakefile.lean b/lakefile.lean index 35947739..284fdcc4 100644 --- a/lakefile.lean +++ b/lakefile.lean @@ -35,6 +35,10 @@ 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] 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/Fields/Binary/BF64.lean b/tests/CompPolyTests/Fields/Binary/BF64.lean index 965e50e8..9424bd6b 100644 --- a/tests/CompPolyTests/Fields/Binary/BF64.lean +++ b/tests/CompPolyTests/Fields/Binary/BF64.lean @@ -50,6 +50,23 @@ theorem multiplication_matches_reference : (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. -/ 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. From 0e3555feda13521aebc9aa7d8d1ea389e17d116d Mon Sep 17 00:00:00 2001 From: Alexander Hicks <25369263+alexanderlhicks@users.noreply.github.com> Date: Tue, 15 Sep 2026 17:29:59 +0100 Subject: [PATCH 31/45] doc(fields): correct extension admission guidance (#332) Reviewed and validated PR head: 8bafbba98c43192eddb9fcfcfaeaf83df04aaa91. --- docs/wiki/field-extensions.md | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/docs/wiki/field-extensions.md b/docs/wiki/field-extensions.md index 06dc7735..7477ebd0 100644 --- a/docs/wiki/field-extensions.md +++ b/docs/wiki/field-extensions.md @@ -23,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? | |---|---|---| @@ -92,14 +93,13 @@ and [`KoalaBear/Ext6.lean`](../../CompPoly/Fields/KoalaBear/Ext6.lean) (`X^6 + X [`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, and it uses the *general* `ExtensionParams` path rather than -`BinomialParams`: over a char-2 field `X^3 - W = X^3 + W`, and the binomial criterion needs -`d ∣ q - 1`, which fails for `d = 3` and `q = 2^64` (`3 ∤ 2^64 - 1`). So `Ext P` is instantiated -directly, and `Ext ext3Params` has a coefficient vector of type `Vector BF64 3`. +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 `BitVec 64` carrier with carry-less +- **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, @@ -116,11 +116,11 @@ 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` | From e843f1c130a5798da8bc2a6ef3f6722eea388e62 Mon Sep 17 00:00:00 2001 From: Alexander Hicks <25369263+alexanderlhicks@users.noreply.github.com> Date: Tue, 15 Sep 2026 17:42:06 +0100 Subject: [PATCH 32/45] feat(binary): expose executable tower basis correspondence (#328) Reviewed and validated PR head: 3ad9a16fa5a6af259666f8219364e6603d5ed123. --- CompPoly/Fields/Binary/AdditiveNTT/Impl.lean | 34 ++++++++++-------- tests/CompPolyTests.lean | 1 + .../Fields/Binary/AdditiveNTT/Basis.lean | 36 +++++++++++++++++++ 3 files changed, 56 insertions(+), 15 deletions(-) create mode 100644 tests/CompPolyTests/Fields/Binary/AdditiveNTT/Basis.lean diff --git a/CompPoly/Fields/Binary/AdditiveNTT/Impl.lean b/CompPoly/Fields/Binary/AdditiveNTT/Impl.lean index ac521fd1..1b1373cc 100644 --- a/CompPoly/Fields/Binary/AdditiveNTT/Impl.lean +++ b/CompPoly/Fields/Binary/AdditiveNTT/Impl.lean @@ -317,27 +317,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/tests/CompPolyTests.lean b/tests/CompPolyTests.lean index 222d48d5..36afb01e 100644 --- a/tests/CompPolyTests.lean +++ b/tests/CompPolyTests.lean @@ -29,6 +29,7 @@ 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.NovelPolynomialBasis public import CompPolyTests.Fields.Binary.BF128Ghash.Prelude public import CompPolyTests.Fields.Binary.BF64 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 From 23a2627e9b3a4dca9a0d8765e9b2b3e9f15f45c3 Mon Sep 17 00:00:00 2001 From: Alexander Hicks <25369263+alexanderlhicks@users.noreply.github.com> Date: Tue, 15 Sep 2026 17:51:39 +0100 Subject: [PATCH 33/45] feat(algebra): construct towers from adjacent maps (#333) Reviewed and validated PR head: de73ab5edea4ee05304c2d3cc6bfc9d621ba5676. --- CompPoly/Data/RingTheory/AlgebraTower.lean | 46 ++++++++++++ .../Data/RingTheory/AlgebraTower.lean | 73 ++++++++++++++++++- 2 files changed, 118 insertions(+), 1 deletion(-) diff --git a/CompPoly/Data/RingTheory/AlgebraTower.lean b/CompPoly/Data/RingTheory/AlgebraTower.lean index 79410d72..a9e2dd1d 100644 --- a/CompPoly/Data/RingTheory/AlgebraTower.lean +++ b/CompPoly/Data/RingTheory/AlgebraTower.lean @@ -5,6 +5,7 @@ Authors: Chung Thai Nguyen, Quang Dao -/ module +public import Mathlib.Data.Nat.Init public import Mathlib.LinearAlgebra.Matrix.Reindex /-! @@ -15,6 +16,9 @@ homomorphisms between comparable levels. Self-maps are identities, and the maps along chains of indices. Each map induces an algebra structure on its target, and composition gives compatible scalar actions across three levels. +`AlgebraTower.ofNatStep` constructs a natural-number-indexed tower by composing chosen +homomorphisms between adjacent levels. These homomorphisms need not be injective. + An `AlgebraTowerEquiv` consists of ring equivalences at each level that commute with the tower maps. -/ @@ -39,6 +43,48 @@ class AlgebraTower {ι : Type*} [Preorder ι] (AT : ι → Type*) 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] diff --git a/tests/CompPolyTests/Data/RingTheory/AlgebraTower.lean b/tests/CompPolyTests/Data/RingTheory/AlgebraTower.lean index fa60f58c..ae182b0f 100644 --- a/tests/CompPolyTests/Data/RingTheory/AlgebraTower.lean +++ b/tests/CompPolyTests/Data/RingTheory/AlgebraTower.lean @@ -9,12 +9,16 @@ import CompPoly.Data.RingTheory.AlgebraTower import Mathlib.Data.ZMod.Basic /-! -# Algebra tower identity regression tests +# 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 @@ -34,6 +38,53 @@ example (i : ι) (h : i ≤ i) (x : A i) : 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)`. -/ @@ -71,4 +122,24 @@ private abbrev constantTower : AlgebraTower (fun _ : ℕ => R) where 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 From c79f957f613fe51d2ea016ed00efdc4e869c1e61 Mon Sep 17 00:00:00 2001 From: Alexander Hicks <25369263+alexanderlhicks@users.noreply.github.com> Date: Tue, 15 Sep 2026 17:59:08 +0100 Subject: [PATCH 34/45] feat(algebra): compose coordinates along natural-number towers (#334) Reviewed and validated PR head: e055696c1e5b568a7736898b57b1a05108489e7c. --- CompPoly.lean | 2 + .../Data/RingTheory/AlgebraTower/Basis.lean | 95 ++++++++++ .../RingTheory/AlgebraTower/Coordinates.lean | 162 ++++++++++++++++++ docs/wiki/repo-map.md | 4 + tests/CompPolyTests.lean | 1 + .../RingTheory/AlgebraTower/Coordinates.lean | 161 +++++++++++++++++ 6 files changed, 425 insertions(+) create mode 100644 CompPoly/Data/RingTheory/AlgebraTower/Basis.lean create mode 100644 CompPoly/Data/RingTheory/AlgebraTower/Coordinates.lean create mode 100644 tests/CompPolyTests/Data/RingTheory/AlgebraTower/Coordinates.lean diff --git a/CompPoly.lean b/CompPoly.lean index 831bb3f5..2c9bc074 100644 --- a/CompPoly.lean +++ b/CompPoly.lean @@ -77,6 +77,8 @@ 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 diff --git a/CompPoly/Data/RingTheory/AlgebraTower/Basis.lean b/CompPoly/Data/RingTheory/AlgebraTower/Basis.lean new file mode 100644 index 00000000..e491faaf --- /dev/null +++ b/CompPoly/Data/RingTheory/AlgebraTower/Basis.lean @@ -0,0 +1,95 @@ +/- +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. + +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 + +end AlgebraTower diff --git a/CompPoly/Data/RingTheory/AlgebraTower/Coordinates.lean b/CompPoly/Data/RingTheory/AlgebraTower/Coordinates.lean new file mode 100644 index 00000000..073ea83b --- /dev/null +++ b/CompPoly/Data/RingTheory/AlgebraTower/Coordinates.lean @@ -0,0 +1,162 @@ +/- +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`. + +The construction uses `LinearEquiv.restrictScalars`, `LinearEquiv.piCongrRight`, +`LinearEquiv.curry`, and `finProdFinEquiv`. +-/ + +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) + +end AlgebraTower diff --git a/docs/wiki/repo-map.md b/docs/wiki/repo-map.md index 51e377d3..6a4ae461 100644 --- a/docs/wiki/repo-map.md +++ b/docs/wiki/repo-map.md @@ -71,6 +71,10 @@ 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. - Adding regression coverage: start in `tests/` and mirror the source namespace when possible. - Updating benchmark coverage or reports: start in `bench/`. diff --git a/tests/CompPolyTests.lean b/tests/CompPolyTests.lean index 36afb01e..6f51e467 100644 --- a/tests/CompPolyTests.lean +++ b/tests/CompPolyTests.lean @@ -25,6 +25,7 @@ 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 diff --git a/tests/CompPolyTests/Data/RingTheory/AlgebraTower/Coordinates.lean b/tests/CompPolyTests/Data/RingTheory/AlgebraTower/Coordinates.lean new file mode 100644 index 00000000..4fc693fa --- /dev/null +++ b/tests/CompPolyTests/Data/RingTheory/AlgebraTower/Coordinates.lean @@ -0,0 +1,161 @@ +/- +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. +-/ + +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 + +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) + +end Generic + +end CompPolyTests.AlgebraTower.Coordinates From 80cbfe684bc49d0bb251e1f7ac369035fdc1c237 Mon Sep 17 00:00:00 2001 From: Alexander Hicks <25369263+alexanderlhicks@users.noreply.github.com> Date: Tue, 15 Sep 2026 18:07:19 +0100 Subject: [PATCH 35/45] feat(algebra-tower): add coordinates at arbitrary endpoints (#335) Reviewed and validated PR head: 5850f8c53fa9c2c5d6f165f3e3ba953f1ac1cf16. --- .../Data/RingTheory/AlgebraTower/Basis.lean | 44 ++++++ .../RingTheory/AlgebraTower/Coordinates.lean | 133 +++++++++++++++++- docs/wiki/repo-map.md | 2 + .../RingTheory/AlgebraTower/Coordinates.lean | 128 +++++++++++++++++ 4 files changed, 306 insertions(+), 1 deletion(-) diff --git a/CompPoly/Data/RingTheory/AlgebraTower/Basis.lean b/CompPoly/Data/RingTheory/AlgebraTower/Basis.lean index e491faaf..6618ce31 100644 --- a/CompPoly/Data/RingTheory/AlgebraTower/Basis.lean +++ b/CompPoly/Data/RingTheory/AlgebraTower/Basis.lean @@ -16,6 +16,10 @@ public import Mathlib.RingTheory.AlgebraTower `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 @@ -92,4 +96,44 @@ theorem natBasisVector_succ (i n : ℕ) (b : Fin (d (i + n))) 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 index 073ea83b..3e0ac89e 100644 --- a/CompPoly/Data/RingTheory/AlgebraTower/Coordinates.lean +++ b/CompPoly/Data/RingTheory/AlgebraTower/Coordinates.lean @@ -26,8 +26,13 @@ a unit coordinate vector. Both are executable, with no enumeration of the tower 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`. +`LinearEquiv.curry`, and `finProdFinEquiv`. Endpoint and coordinate-count identifications use +`LinearEquiv.cast`, `LinearEquiv.funCongrLeft`, and `finCongr`. -/ public section @@ -159,4 +164,130 @@ 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/docs/wiki/repo-map.md b/docs/wiki/repo-map.md index 6a4ae461..fc274c3c 100644 --- a/docs/wiki/repo-map.md +++ b/docs/wiki/repo-map.md @@ -75,6 +75,8 @@ scripts/ repo utilities and validation helpers 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/tests/CompPolyTests/Data/RingTheory/AlgebraTower/Coordinates.lean b/tests/CompPolyTests/Data/RingTheory/AlgebraTower/Coordinates.lean index 4fc693fa..327223f4 100644 --- a/tests/CompPolyTests/Data/RingTheory/AlgebraTower/Coordinates.lean +++ b/tests/CompPolyTests/Data/RingTheory/AlgebraTower/Coordinates.lean @@ -16,6 +16,10 @@ functions as the tower maps. A rectangular two-step example distinguishes the tw 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 @@ -127,6 +131,74 @@ example (x : FunctionTower (fun _ => 0) 2) : 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] @@ -156,6 +228,62 @@ example (i n : ℕ) (c : Fin (coordinateSize d i n) → A i) : 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 From 1defb6cea6822450c8d2d32e161c66f2efbb1f2a Mon Sep 17 00:00:00 2001 From: Alexander Hicks <25369263+alexanderlhicks@users.noreply.github.com> Date: Tue, 15 Sep 2026 18:15:48 +0100 Subject: [PATCH 36/45] feat(binary-tower): add concrete successor coordinates (#339) Reviewed and validated PR head: 874a5735fd03875ba64773ef29b31c9bac73eeed. --- CompPoly.lean | 1 + .../Binary/Tower/Concrete/Coordinates.lean | 139 ++++++++++++++++++ docs/wiki/binary-fields-and-ntt.md | 6 + tests/CompPolyTests.lean | 1 + .../Fields/Binary/Tower/Coordinates.lean | 115 +++++++++++++++ 5 files changed, 262 insertions(+) create mode 100644 CompPoly/Fields/Binary/Tower/Concrete/Coordinates.lean create mode 100644 tests/CompPolyTests/Fields/Binary/Tower/Coordinates.lean diff --git a/CompPoly.lean b/CompPoly.lean index 2c9bc074..a09e9817 100644 --- a/CompPoly.lean +++ b/CompPoly.lean @@ -121,6 +121,7 @@ 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.Coordinates public import CompPoly.Fields.Binary.Tower.Concrete.Core public import CompPoly.Fields.Binary.Tower.Concrete.Field public import CompPoly.Fields.Binary.Tower.Equiv diff --git a/CompPoly/Fields/Binary/Tower/Concrete/Coordinates.lean b/CompPoly/Fields/Binary/Tower/Concrete/Coordinates.lean new file mode 100644 index 00000000..c1814801 --- /dev/null +++ b/CompPoly/Fields/Binary/Tower/Concrete/Coordinates.lean @@ -0,0 +1,139 @@ +/- +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. +-/ + +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/docs/wiki/binary-fields-and-ntt.md b/docs/wiki/binary-fields-and-ntt.md index 48792086..d22cf3b5 100644 --- a/docs/wiki/binary-fields-and-ntt.md +++ b/docs/wiki/binary-fields-and-ntt.md @@ -128,6 +128,12 @@ support lemmas: - `Tower/Abstract/*` - abstract tower definitions and algebra. - `Tower/Concrete/*` - concrete basis, core definitions, and field instances. + [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. - `Tower/Support/*` - supporting lemmas about defining polynomials, linear independence, and finite-index helpers. - `Tower/Fast.lean` - packed machine-word tower arithmetic with a GF(2^8) diff --git a/tests/CompPolyTests.lean b/tests/CompPolyTests.lean index 6f51e467..314ba66d 100644 --- a/tests/CompPolyTests.lean +++ b/tests/CompPolyTests.lean @@ -37,6 +37,7 @@ 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.Coordinates public import CompPolyTests.Fields.Binary.Tower.Fast public import CompPolyTests.Fields.Binary.Tower.TensorAlgebra public import CompPolyTests.Fields.Extension.Arithmetic 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 From 323f6d6fc5763bf70d5eea689b422dc64a104509 Mon Sep 17 00:00:00 2001 From: Alexander Hicks <25369263+alexanderlhicks@users.noreply.github.com> Date: Tue, 15 Sep 2026 18:25:25 +0100 Subject: [PATCH 37/45] feat(binary-tower): add concrete endpoint coordinates (#340) Reviewed and validated PR head: a0a1262bc67790e799e0a9ba05e8428e08085223. --- CompPoly.lean | 1 + .../Tower/Concrete/RelativeCoordinates.lean | 226 ++++++++++++++++++ docs/wiki/binary-fields-and-ntt.md | 5 + tests/CompPolyTests.lean | 1 + .../Binary/Tower/RelativeCoordinates.lean | 142 +++++++++++ 5 files changed, 375 insertions(+) create mode 100644 CompPoly/Fields/Binary/Tower/Concrete/RelativeCoordinates.lean create mode 100644 tests/CompPolyTests/Fields/Binary/Tower/RelativeCoordinates.lean diff --git a/CompPoly.lean b/CompPoly.lean index a09e9817..121560c9 100644 --- a/CompPoly.lean +++ b/CompPoly.lean @@ -124,6 +124,7 @@ public import CompPoly.Fields.Binary.Tower.Concrete.Basis 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 diff --git a/CompPoly/Fields/Binary/Tower/Concrete/RelativeCoordinates.lean b/CompPoly/Fields/Binary/Tower/Concrete/RelativeCoordinates.lean new file mode 100644 index 00000000..65a8f3a6 --- /dev/null +++ b/CompPoly/Fields/Binary/Tower/Concrete/RelativeCoordinates.lean @@ -0,0 +1,226 @@ +/- +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. +-/ + +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 + +/-- 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/docs/wiki/binary-fields-and-ntt.md b/docs/wiki/binary-fields-and-ntt.md index d22cf3b5..2d55c8dc 100644 --- a/docs/wiki/binary-fields-and-ntt.md +++ b/docs/wiki/binary-fields-and-ntt.md @@ -134,6 +134,11 @@ support lemmas: 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. - `Tower/Support/*` - supporting lemmas about defining polynomials, linear independence, and finite-index helpers. - `Tower/Fast.lean` - packed machine-word tower arithmetic with a GF(2^8) diff --git a/tests/CompPolyTests.lean b/tests/CompPolyTests.lean index 314ba66d..8c586b14 100644 --- a/tests/CompPolyTests.lean +++ b/tests/CompPolyTests.lean @@ -39,6 +39,7 @@ public import CompPolyTests.Fields.Binary.Common public import CompPolyTests.Fields.Binary.Tower.Algebra public import CompPolyTests.Fields.Binary.Tower.Coordinates public import CompPolyTests.Fields.Binary.Tower.Fast +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 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 From 080d2268355c7d904c4e0b5222612a6f6a0ab626 Mon Sep 17 00:00:00 2001 From: Alexander Hicks <25369263+alexanderlhicks@users.noreply.github.com> Date: Tue, 15 Sep 2026 18:33:18 +0100 Subject: [PATCH 38/45] feat(binary-tower): identify concrete coordinates with the multilinear basis (#341) Reviewed and validated PR head: 5cedfc81ab5c9cdda951170a5166241902ae02ef. --- CompPoly.lean | 1 + .../Tower/Concrete/BasisCoordinates.lean | 189 ++++++++++++++++++ .../Tower/Concrete/RelativeCoordinates.lean | 6 + docs/wiki/binary-fields-and-ntt.md | 4 + tests/CompPolyTests.lean | 1 + .../Fields/Binary/Tower/BasisCoordinates.lean | 137 +++++++++++++ 6 files changed, 338 insertions(+) create mode 100644 CompPoly/Fields/Binary/Tower/Concrete/BasisCoordinates.lean create mode 100644 tests/CompPolyTests/Fields/Binary/Tower/BasisCoordinates.lean diff --git a/CompPoly.lean b/CompPoly.lean index 121560c9..ce0f09f8 100644 --- a/CompPoly.lean +++ b/CompPoly.lean @@ -121,6 +121,7 @@ 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 diff --git a/CompPoly/Fields/Binary/Tower/Concrete/BasisCoordinates.lean b/CompPoly/Fields/Binary/Tower/Concrete/BasisCoordinates.lean new file mode 100644 index 00000000..61a43469 --- /dev/null +++ b/CompPoly/Fields/Binary/Tower/Concrete/BasisCoordinates.lean @@ -0,0 +1,189 @@ +/- +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. +-/ + +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/RelativeCoordinates.lean b/CompPoly/Fields/Binary/Tower/Concrete/RelativeCoordinates.lean index 65a8f3a6..6c149d39 100644 --- a/CompPoly/Fields/Binary/Tower/Concrete/RelativeCoordinates.lean +++ b/CompPoly/Fields/Binary/Tower/Concrete/RelativeCoordinates.lean @@ -35,6 +35,12 @@ def coordinates {i j : ℕ} (h : i ≤ j) : 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) : diff --git a/docs/wiki/binary-fields-and-ntt.md b/docs/wiki/binary-fields-and-ntt.md index 2d55c8dc..397885c9 100644 --- a/docs/wiki/binary-fields-and-ntt.md +++ b/docs/wiki/binary-fields-and-ntt.md @@ -139,6 +139,10 @@ support lemmas: 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/Fast.lean` - packed machine-word tower arithmetic with a GF(2^8) diff --git a/tests/CompPolyTests.lean b/tests/CompPolyTests.lean index 8c586b14..df22f9d3 100644 --- a/tests/CompPolyTests.lean +++ b/tests/CompPolyTests.lean @@ -37,6 +37,7 @@ 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.Coordinates public import CompPolyTests.Fields.Binary.Tower.Fast public import CompPolyTests.Fields.Binary.Tower.RelativeCoordinates 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 From 255a2c24cbfefb1ad57cac14e84ce6e63f1eb691 Mon Sep 17 00:00:00 2001 From: Alexander Hicks <25369263+alexanderlhicks@users.noreply.github.com> Date: Tue, 15 Sep 2026 18:43:26 +0100 Subject: [PATCH 39/45] refactor(binary-tower): separate concrete construction imports from the abstract tower (#343) Reviewed and validated PR head: 3293141d038960a9f7bc530357429055279d0aa4. --- .../Fields/Binary/Tower/Concrete/Algebra.lean | 1 + .../Fields/Binary/Tower/Concrete/Core.lean | 2 +- .../Fields/Binary/Tower/Concrete/Field.lean | 1 + CompPoly/Fields/Binary/Tower/Equiv.lean | 1 + docs/wiki/binary-fields-and-ntt.md | 3 +++ tests/CompPolyTests.lean | 1 + .../Fields/Binary/Tower/ConcreteImports.lean | 25 +++++++++++++++++++ 7 files changed, 33 insertions(+), 1 deletion(-) create mode 100644 tests/CompPolyTests/Fields/Binary/Tower/ConcreteImports.lean diff --git a/CompPoly/Fields/Binary/Tower/Concrete/Algebra.lean b/CompPoly/Fields/Binary/Tower/Concrete/Algebra.lean index 7b030188..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 /-! 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..8dd54b20 100644 --- a/CompPoly/Fields/Binary/Tower/Concrete/Field.lean +++ b/CompPoly/Fields/Binary/Tower/Concrete/Field.lean @@ -6,6 +6,7 @@ Authors: Chung Thai Nguyen, Quang Dao module public import CompPoly.Fields.Binary.Tower.Concrete.Core +public import CompPoly.Fields.Binary.Tower.Support.IrreducibilityAndTraceMapProperty /-! # Concrete Binary Tower Field 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/docs/wiki/binary-fields-and-ntt.md b/docs/wiki/binary-fields-and-ntt.md index 397885c9..1048e37e 100644 --- a/docs/wiki/binary-fields-and-ntt.md +++ b/docs/wiki/binary-fields-and-ntt.md @@ -128,6 +128,9 @@ 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 diff --git a/tests/CompPolyTests.lean b/tests/CompPolyTests.lean index df22f9d3..56544250 100644 --- a/tests/CompPolyTests.lean +++ b/tests/CompPolyTests.lean @@ -38,6 +38,7 @@ 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.RelativeCoordinates 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 From 646ad3d431efc02a10e7980101b3e3fa6b9371c3 Mon Sep 17 00:00:00 2001 From: Alexander Hicks <25369263+alexanderlhicks@users.noreply.github.com> Date: Tue, 15 Sep 2026 18:53:23 +0100 Subject: [PATCH 40/45] refactor(fields): simplify concrete tower multiplication laws (#345) Reviewed and validated PR head: ac3f637ba5a828692a8de8a5264618d9048177dd. --- .../Fields/Binary/Tower/Concrete/Field.lean | 140 +++++------------- 1 file changed, 36 insertions(+), 104 deletions(-) diff --git a/CompPoly/Fields/Binary/Tower/Concrete/Field.lean b/CompPoly/Fields/Binary/Tower/Concrete/Field.lean index 8dd54b20..2a71b4cb 100644 --- a/CompPoly/Fields/Binary/Tower/Concrete/Field.lean +++ b/CompPoly/Fields/Binary/Tower/Concrete/Field.lean @@ -62,6 +62,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 @@ -231,121 +255,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)) From 5d8c3a4bb58f967b9f9d898c6a36f8863564849a Mon Sep 17 00:00:00 2001 From: Alexander Hicks <25369263+alexanderlhicks@users.noreply.github.com> Date: Tue, 15 Sep 2026 19:05:01 +0100 Subject: [PATCH 41/45] refactor(fields): reuse quadratic norm nonvanishing in the concrete tower (#344) Reviewed and validated PR head: d613a0e479744dfaee156c5ea83975f28d511043. --- .../Fields/Binary/Tower/Concrete/Field.lean | 116 +++++++----------- tests/CompPolyTests.lean | 1 + .../Fields/Binary/Tower/Norm.lean | 63 ++++++++++ 3 files changed, 107 insertions(+), 73 deletions(-) create mode 100644 tests/CompPolyTests/Fields/Binary/Tower/Norm.lean diff --git a/CompPoly/Fields/Binary/Tower/Concrete/Field.lean b/CompPoly/Fields/Binary/Tower/Concrete/Field.lean index 2a71b4cb..e9402f0c 100644 --- a/CompPoly/Fields/Binary/Tower/Concrete/Field.lean +++ b/CompPoly/Fields/Binary/Tower/Concrete/Field.lean @@ -8,10 +8,16 @@ 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 @@ -288,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/tests/CompPolyTests.lean b/tests/CompPolyTests.lean index 56544250..79b17e4d 100644 --- a/tests/CompPolyTests.lean +++ b/tests/CompPolyTests.lean @@ -41,6 +41,7 @@ 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 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 From 1ef929fac317d70562034d3142d1368d16e50a66 Mon Sep 17 00:00:00 2001 From: Alexander Hicks <25369263+alexanderlhicks@users.noreply.github.com> Date: Tue, 15 Sep 2026 19:15:53 +0100 Subject: [PATCH 42/45] refactor(additive-ntt): separate generic execution from concrete tower instances (#342) Reviewed and validated PR head: cfc67e71811b5d1cc91fefebe79c3575b1675f54. --- CompPoly.lean | 1 + .../Binary/AdditiveNTT/AdditiveNTT.lean | 1 + .../Binary/AdditiveNTT/Correctness.lean | 2 +- .../Fields/Binary/AdditiveNTT/Executable.lean | 310 ++++++++++++++++++ CompPoly/Fields/Binary/AdditiveNTT/Impl.lean | 288 +--------------- docs/wiki/binary-fields-and-ntt.md | 10 +- tests/CompPolyTests.lean | 2 + .../Binary/AdditiveNTT/Compatibility.lean | 34 ++ .../Fields/Binary/AdditiveNTT/Executable.lean | 45 +++ 9 files changed, 407 insertions(+), 286 deletions(-) create mode 100644 CompPoly/Fields/Binary/AdditiveNTT/Executable.lean create mode 100644 tests/CompPolyTests/Fields/Binary/AdditiveNTT/Compatibility.lean create mode 100644 tests/CompPolyTests/Fields/Binary/AdditiveNTT/Executable.lean diff --git a/CompPoly.lean b/CompPoly.lean index ce0f09f8..c768366a 100644 --- a/CompPoly.lean +++ b/CompPoly.lean @@ -99,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 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..af3f22b6 --- /dev/null +++ b/CompPoly/Fields/Binary/AdditiveNTT/Executable.lean @@ -0,0 +1,310 @@ +/- +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`. +-/ + +@[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 1b1373cc..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 diff --git a/docs/wiki/binary-fields-and-ntt.md b/docs/wiki/binary-fields-and-ntt.md index 1048e37e..e2933cc7 100644 --- a/docs/wiki/binary-fields-and-ntt.md +++ b/docs/wiki/binary-fields-and-ntt.md @@ -176,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`. @@ -199,4 +203,4 @@ Algorithm changes often cascade into `Intermediate`, `Impl`, and `Correctness`. - 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/tests/CompPolyTests.lean b/tests/CompPolyTests.lean index 79b17e4d..d301044a 100644 --- a/tests/CompPolyTests.lean +++ b/tests/CompPolyTests.lean @@ -31,6 +31,8 @@ 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 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 From c7cac0496af21dcb3b6aec2f2e90eaaf2545c55e Mon Sep 17 00:00:00 2001 From: Alexander Hicks <25369263+alexanderlhicks@users.noreply.github.com> Date: Tue, 15 Sep 2026 19:23:01 +0100 Subject: [PATCH 43/45] doc: cite binary tower coordinates and additive NTT (#350) Reviewed and validated PR head: c04946e0308d499cd0c0dbe62a764c8cbcf703a2. --- CompPoly/Fields/Binary/AdditiveNTT/Executable.lean | 8 ++++++++ .../Fields/Binary/Tower/Concrete/BasisCoordinates.lean | 7 +++++++ CompPoly/Fields/Binary/Tower/Concrete/Coordinates.lean | 7 +++++++ .../Fields/Binary/Tower/Concrete/RelativeCoordinates.lean | 7 +++++++ 4 files changed, 29 insertions(+) diff --git a/CompPoly/Fields/Binary/AdditiveNTT/Executable.lean b/CompPoly/Fields/Binary/AdditiveNTT/Executable.lean index af3f22b6..083066ac 100644 --- a/CompPoly/Fields/Binary/AdditiveNTT/Executable.lean +++ b/CompPoly/Fields/Binary/AdditiveNTT/Executable.lean @@ -19,6 +19,14 @@ 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 diff --git a/CompPoly/Fields/Binary/Tower/Concrete/BasisCoordinates.lean b/CompPoly/Fields/Binary/Tower/Concrete/BasisCoordinates.lean index 61a43469..68a5786e 100644 --- a/CompPoly/Fields/Binary/Tower/Concrete/BasisCoordinates.lean +++ b/CompPoly/Fields/Binary/Tower/Concrete/BasisCoordinates.lean @@ -21,6 +21,13 @@ The proof compares the existing basis's generator-product formula with the succe 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 diff --git a/CompPoly/Fields/Binary/Tower/Concrete/Coordinates.lean b/CompPoly/Fields/Binary/Tower/Concrete/Coordinates.lean index c1814801..acb81316 100644 --- a/CompPoly/Fields/Binary/Tower/Concrete/Coordinates.lean +++ b/CompPoly/Fields/Binary/Tower/Concrete/Coordinates.lean @@ -24,6 +24,13 @@ The decomposition and linearity proofs use the existing algebraic expansion lemm 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 diff --git a/CompPoly/Fields/Binary/Tower/Concrete/RelativeCoordinates.lean b/CompPoly/Fields/Binary/Tower/Concrete/RelativeCoordinates.lean index 6c149d39..51e8a129 100644 --- a/CompPoly/Fields/Binary/Tower/Concrete/RelativeCoordinates.lean +++ b/CompPoly/Fields/Binary/Tower/Concrete/RelativeCoordinates.lean @@ -20,6 +20,13 @@ The equivalence composes `succCoordinates` through `AlgebraTower.natCoordinatesC 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 From 1e470b46df33b5b6e361e31e3d463010d4c2c03c Mon Sep 17 00:00:00 2001 From: Alexander Hicks <25369263+alexanderlhicks@users.noreply.github.com> Date: Tue, 15 Sep 2026 19:37:50 +0100 Subject: [PATCH 44/45] doc(bench): clarify validation timing and sink behavior (#336) Reviewed and validated PR head: 2480ec0c8c5e5102b3567b0be919147946be88cb. --- bench/CompPolyBench/Common.lean | 13 +++++++------ bench/CompPolyBench/Harness/Sink.lean | 6 +++--- bench/CompPolyBench/Harness/Timer.lean | 6 +++--- 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/bench/CompPolyBench/Common.lean b/bench/CompPolyBench/Common.lean index d513f221..6831c1e4 100644 --- a/bench/CompPolyBench/Common.lean +++ b/bench/CompPolyBench/Common.lean @@ -1312,9 +1312,9 @@ private def validationRow (group : BenchGroup) : String := /-- Render the report for a `--validate-only` run. -Deliberately not the timing table: a validation run collects no samples, so -every duration would be zero. What it has to say is whether each group's -implementations agree, and on what digest. -/ +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" ([ @@ -1325,9 +1325,10 @@ def renderValidationMarkdown (preset : BenchPreset) (groups : Array BenchGroup) "- Groups checked: `" ++ toString groups.size ++ "`", "- Mismatched groups: `" ++ toString mismatches.length ++ "`", "", - "No timings were collected. 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 timings.", + "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 |", "| ----- | ---: | --------------- | ------ |" diff --git a/bench/CompPolyBench/Harness/Sink.lean b/bench/CompPolyBench/Harness/Sink.lean index e04de346..2f50e70a 100644 --- a/bench/CompPolyBench/Harness/Sink.lean +++ b/bench/CompPolyBench/Harness/Sink.lean @@ -22,9 +22,9 @@ namespace CompPolyBench /-- Fold one result word into a running sink accumulator. -Kept `@[noinline]` so the fold survives optimisation; the `harness-floor` group -measures what this costs and `harness-canary` fails the run if it stops costing -anything. -/ +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) diff --git a/bench/CompPolyBench/Harness/Timer.lean b/bench/CompPolyBench/Harness/Timer.lean index 87f65e20..702909cd 100644 --- a/bench/CompPolyBench/Harness/Timer.lean +++ b/bench/CompPolyBench/Harness/Timer.lean @@ -24,9 +24,9 @@ namespace CompPolyBench /-- Whether this process is running in validation-only mode. -Set once from the command line rather than carried on every `BenchSpec`. Read by -`runTimedSpec`, which skips calibration and sample collection entirely when it is -set. -/ +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. -/ From 0fdf1771c0396bd3665c2823a91b68bdf0aad7ec Mon Sep 17 00:00:00 2001 From: yudduy Date: Mon, 21 Sep 2026 15:33:14 -0700 Subject: [PATCH 45/45] Fix KoalaBear irreducibility proof replay Fresh-kernel leanchecker replay of the sextic and quintic irreducibility proofs fails with "(kernel) deep recursion detected". Pass the concrete ZMod fieldSize Field and Fintype instances explicitly to the Rabin wrappers and add the sextic addDeclCore replay regression. The theorem statements, polynomials, certificates, and Fact instances are unchanged. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01CGr4BwA4aq2b4seaRKoLzs --- .../KoalaBear/Ext5/QuinticIrreducible.lean | 5 ++- .../KoalaBear/Ext6/SexticIrreducible.lean | 5 ++- tests/CompPolyTests.lean | 1 + .../Fields/KoalaBear/SexticReplay.lean | 32 +++++++++++++++++++ 4 files changed, 41 insertions(+), 2 deletions(-) create mode 100644 tests/CompPolyTests/Fields/KoalaBear/SexticReplay.lean diff --git a/CompPoly/Fields/KoalaBear/Ext5/QuinticIrreducible.lean b/CompPoly/Fields/KoalaBear/Ext5/QuinticIrreducible.lean index eb429dfc..68291488 100644 --- a/CompPoly/Fields/KoalaBear/Ext5/QuinticIrreducible.lean +++ b/CompPoly/Fields/KoalaBear/Ext5/QuinticIrreducible.lean @@ -110,7 +110,10 @@ 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_of_card hcard (by norm_num) quinticPoly_natDegree ?_ ?_ + -- 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 · exact isCoprime_X_pow_sub_X_of_runChain toPoly_quinticL quinticPoly_ne_zero diff --git a/CompPoly/Fields/KoalaBear/Ext6/SexticIrreducible.lean b/CompPoly/Fields/KoalaBear/Ext6/SexticIrreducible.lean index ea861c9a..5e4424bb 100644 --- a/CompPoly/Fields/KoalaBear/Ext6/SexticIrreducible.lean +++ b/CompPoly/Fields/KoalaBear/Ext6/SexticIrreducible.lean @@ -138,7 +138,10 @@ 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_of_card hcard sexticPoly_natDegree ?_ ?_ ?_ + -- 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 · exact isCoprime_X_pow_sub_X_of_runChain toPoly_sexticL sexticPoly_ne_zero diff --git a/tests/CompPolyTests.lean b/tests/CompPolyTests.lean index d301044a..386ef415 100644 --- a/tests/CompPolyTests.lean +++ b/tests/CompPolyTests.lean @@ -53,6 +53,7 @@ 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 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}"